fix(message-parser): gate the functional route on the bound schema - #174
Merged
Conversation
trampoline_get_plugin_extension advertised pj.parser_functional.v1
whenever the instance had ANY registered SchemaHandler. A mixed-model
plugin — handlers for some schemas, legacy parse() for the rest, the
shape parse()'s own doc-comment sanctions for a generic flattener —
therefore claimed the functional route on every schema, and the final
parseScalars/parseObject dispatchers then rejected each message for the
unhandled ones ("parser does not register schema"). A 0.21 host that
prefers the functional route fails every message on those topics.
Once a schema is bound, advertisement now requires a handler for THAT
schema. Before binding, any registered handler still advertises the
capability: no schema-specific answer exists yet, and hosts are already
required to re-query after binding rather than cache an earlier absence.
Header-inline behavior fix; no ABI, vtable, protocol, or member-layout
change.
Closes #171
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
facontidavide
added a commit
that referenced
this pull request
Aug 9, 2026
…0.22.0) SDK core of the parser-extensibility v4 architecture (spec: pj-official-plugins PR #272) — the complete wasmer-free surface, with the dual-target module ABI frozen here: - pj.parser_route_claims.v1 extension: exact handler-table route claims (scalar/object, exact-only match), auto-implemented by MessageParserPluginBase from its handler table; delivery via get_plugin_extension, zero layout changes - pj.parser_functional.v2: object sink gains accept_object_spliced (one splice per object, input-space offsets, frozen per-type eligibility table in builtin_object_abi.h); frozen error-kind constants; v1 byte-identical - parser_module_abi.h: frozen pj_module_* export ABI (u64 module-space tokens, token-0 creation-error channel, 512-byte error buffer) + bounds-checked little-endian codecs for BindingInfo / parse-input / output-descriptor blocks - host claim catalog + route resolver: §4 admission matrix (priority bounds, wildcard/object rules, encoding registry, duplicate identity, provenance never from manifests), module-manifest ingestion, synthesized plugin claim ids (wildcard:<encoding>, handler:<encoding>:<type>), §5 selection (pin fail-closed → exact > wildcard → provenance tier → priority → identity tie-break) with split per-route probe caches and selection traces - native module loader (dlopen RTLD_LOCAL|RTLD_NOW, per-handle export resolution, session never-unload) + module runtime over the codecs with splice eligibility/bounds validation and the fault-vs-data-error strike tracker (3 strikes → quarantine → recreate; repeat → session disable) - authoring kit pj_base/include/pj_base/parser_module/: header-only, C++17, wasi-clean (own Status/Expected/arena; -fno-exceptions capable); CdrReader + CdrFieldLocator (XCDR1 traversal plans, depth caps, bounds), ProtoReader + ProtoFieldLocator, checked time normalization, canonical-wire ObjectWriter (PointCloud/Image + splice path), pj::FunctionalParser + PJ_FUNCTIONAL_PARSER macro, pj_add_parser_module() native target - wasm manifest custom-section codec (shared embed/read; 1b's tooling wraps it) + static wasm ABI conformance: wasi-sdk 27 reactor build of the same toy module, binary-format audit of export names/signatures, reactor model, and single manifest section — no wasmer, gated on PJ_WASI_SDK_ROOT, wired into linux CI Docs and the in-repo authoring skill are synchronized to this surface: a new parser-module authoring reference, the parser-module choice rule and route-claim semantics in the MessageParser guidance, and corrections to stale claims (wrong plugin-base include path, two-builder ObjectWriter, builtin-type inventory). Editing VERSION now re-runs configure, so a stale build tree can no longer stamp a previous version into the generated version header. Rebased onto main: the schema-aware functional-route gate from #174 now governs the v2 advertisement as well as v1, so a mixed-model parser bound to a schema it only implements through legacy parse() withdraws both revisions (a host prefers v2, so leaving v2 advertised would route every message on those topics into a parser that can only reject them). Tests: 75/75 Debug+ASAN (74 with the wasm gate skipped), incl. layout sentinels, golden byte fixtures, adversarial loader/runtime fixtures, and a kit-authored module E2E (load → admit → bind → full + spliced PointCloud). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
facontidavide
added a commit
that referenced
this pull request
Aug 9, 2026
…0.22.0) (#172) SDK core of the parser-extensibility v4 architecture (spec: pj-official-plugins PR #272) — the complete wasmer-free surface, with the dual-target module ABI frozen here: - pj.parser_route_claims.v1 extension: exact handler-table route claims (scalar/object, exact-only match), auto-implemented by MessageParserPluginBase from its handler table; delivery via get_plugin_extension, zero layout changes - pj.parser_functional.v2: object sink gains accept_object_spliced (one splice per object, input-space offsets, frozen per-type eligibility table in builtin_object_abi.h); frozen error-kind constants; v1 byte-identical - parser_module_abi.h: frozen pj_module_* export ABI (u64 module-space tokens, token-0 creation-error channel, 512-byte error buffer) + bounds-checked little-endian codecs for BindingInfo / parse-input / output-descriptor blocks - host claim catalog + route resolver: §4 admission matrix (priority bounds, wildcard/object rules, encoding registry, duplicate identity, provenance never from manifests), module-manifest ingestion, synthesized plugin claim ids (wildcard:<encoding>, handler:<encoding>:<type>), §5 selection (pin fail-closed → exact > wildcard → provenance tier → priority → identity tie-break) with split per-route probe caches and selection traces - native module loader (dlopen RTLD_LOCAL|RTLD_NOW, per-handle export resolution, session never-unload) + module runtime over the codecs with splice eligibility/bounds validation and the fault-vs-data-error strike tracker (3 strikes → quarantine → recreate; repeat → session disable) - authoring kit pj_base/include/pj_base/parser_module/: header-only, C++17, wasi-clean (own Status/Expected/arena; -fno-exceptions capable); CdrReader + CdrFieldLocator (XCDR1 traversal plans, depth caps, bounds), ProtoReader + ProtoFieldLocator, checked time normalization, canonical-wire ObjectWriter (PointCloud/Image + splice path), pj::FunctionalParser + PJ_FUNCTIONAL_PARSER macro, pj_add_parser_module() native target - wasm manifest custom-section codec (shared embed/read; 1b's tooling wraps it) + static wasm ABI conformance: wasi-sdk 27 reactor build of the same toy module, binary-format audit of export names/signatures, reactor model, and single manifest section — no wasmer, gated on PJ_WASI_SDK_ROOT, wired into linux CI Docs and the in-repo authoring skill are synchronized to this surface: a new parser-module authoring reference, the parser-module choice rule and route-claim semantics in the MessageParser guidance, and corrections to stale claims (wrong plugin-base include path, two-builder ObjectWriter, builtin-type inventory). Editing VERSION now re-runs configure, so a stale build tree can no longer stamp a previous version into the generated version header. Rebased onto main: the schema-aware functional-route gate from #174 now governs the v2 advertisement as well as v1, so a mixed-model parser bound to a schema it only implements through legacy parse() withdraws both revisions (a host prefers v2, so leaving v2 advertised would route every message on those topics into a parser that can only reject them). Tests: 75/75 Debug+ASAN (74 with the wasm gate skipped), incl. layout sentinels, golden byte fixtures, adversarial loader/runtime fixtures, and a kit-authored module E2E (load → admit → bind → full + spliced PointCloud). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #171.
Problem
trampoline_get_plugin_extensionadvertisedpj.parser_functional.v1whenever the instance had any registeredSchemaHandler:But
parseScalars/parseObjectarefinaland dispatch on the bound schema, erroring with"parser does not register schema: <bound>"when that specific schema has no handler. So a mixed-model plugin — handlers for some schemas, legacyparse()for the rest, which is exactly the shapeparse()'s own doc-comment sanctions ("a fallback for type names not in the registered table (e.g. a ROS-style generic flattener)") — claimed the functional route on every schema. An SDK 0.21 host that prefers that route then fails every message on the unhandled topics.This isn't hypothetical: PJ4's
streaming_caching_parser_plugintest fixture had this shape and went zero-rows the moment the host started preferring the functional route; it had to have its handler removed to keep working.Fix
Once a schema is bound, advertisement requires a handler for that schema. Before binding, any registered handler still advertises the capability — no schema-specific answer exists yet, and hosts are already required to re-query after binding rather than cache an earlier absence (the contract
HandlerRegisteredDuringBindEnablesExtensionWithoutCapabilityCachingpins).Header-inline behavior change only: no ABI, vtable, protocol,
struct_size, or member-layout change;abi/baseline.abiuntouched.Tests
Two new cases, both observed failing before the fix with
Actual: true(i.e. they reproduce the bug), passing after:MixedModelParserAdvertisesOnlyForHandledSchemas— a plugin with a handler forexample/Imageplus legacyparse(): bound to the handled schema it advertises; bound toexample/Unhandledit does not, andparseScalarsFunctionalreports the extension as unavailable rather than the host discovering it via a per-message parse error.RebindingToAnUnhandledSchemaWithdrawsTheFunctionalRoute— advertisement follows a re-bind in both directions.Pre-existing contracts verified unchanged:
NewlyBuiltParserExposesStableExtensionAutomatically(pre-bind capability advertisement) andHandlerRegisteredDuringBindEnablesExtensionWithoutCapabilityCaching(handler registered duringbindSchema) both still pass. Full suite green (64/64).Release
Per the versioning policy this is a PATCH-level fix to installed-header behavior; I propose
0.21.1when a release is next cut. Following the precedent of #169 (same class of fix) I have not bumpedVERSIONin this PR, and have not tagged anything. Note that open PRs #172/#173 currently claim0.22.0/0.23.0.Downstream
No PJ4 change is required — its parser host already re-queries
supportsFunctionalParsing()per call on both routes, so it simply stops taking the functional route for unhandled schemas. This should land before the official plugin fleet rebuilds on 0.21.🤖 Generated with Claude Code