Context
#1921 (PR #1936) makes the "durable _meta subject must be an IRI" invariant span both the ingest and read paths, but the string-prefix term classifier is currently implemented in two private helpers:
isIriMetaSubject in packages/agent/src/sync/durable-integrity.ts (ingest admission)
isIriTerm in packages/agent/src/sync/responder/graph-plan.ts (responder read/paging)
They share the core contract (reject terms starting with _: or "), coupled today by a comment rather than by code.
Deliberate current difference (not a bug)
isIriMetaSubject('') returns false (fail-closed hardening: an empty/malformed _meta subject is dropped at admission), whereas the responder's isIriTerm('') returns true. This divergence is intentional and safe: the responder read path never sees an empty term post-ingest. It is called out here so unification is a conscious decision, not a silent behavior change.
Proposed follow-up
Extract a single canonical term-classifier in the layer that owns the string-encoded Quad contract (e.g. a shared sync/storage util) and import it from both durable-integrity and graph-plan. Decide explicitly how the shared helper treats the empty-string case (unifying to fail-closed would change the responder read path and needs its own review/tests). Cover the shared helper with IRI / blank-node / literal / empty-string cases.
Why deferred from #1921
#1921's scope is the ingest guard + its checkpoint-advance correctness. Unifying the classifier would reach into the responder read path (graph-plan.ts) and alter its empty-string handling — a separate, independently-reviewable change. Tracked here to keep #1921 focused.
Related: #1921, PR #1936. Raised by otReviewAgent on PR #1936 (durable-integrity.ts ~:1454 thread).
Context
#1921 (PR #1936) makes the "durable
_metasubject must be an IRI" invariant span both the ingest and read paths, but the string-prefix term classifier is currently implemented in two private helpers:isIriMetaSubjectinpackages/agent/src/sync/durable-integrity.ts(ingest admission)isIriTerminpackages/agent/src/sync/responder/graph-plan.ts(responder read/paging)They share the core contract (reject terms starting with
_:or"), coupled today by a comment rather than by code.Deliberate current difference (not a bug)
isIriMetaSubject('')returns false (fail-closed hardening: an empty/malformed_metasubject is dropped at admission), whereas the responder'sisIriTerm('')returns true. This divergence is intentional and safe: the responder read path never sees an empty term post-ingest. It is called out here so unification is a conscious decision, not a silent behavior change.Proposed follow-up
Extract a single canonical term-classifier in the layer that owns the string-encoded
Quadcontract (e.g. a sharedsync/storageutil) and import it from bothdurable-integrityandgraph-plan. Decide explicitly how the shared helper treats the empty-string case (unifying to fail-closed would change the responder read path and needs its own review/tests). Cover the shared helper with IRI / blank-node / literal / empty-string cases.Why deferred from #1921
#1921's scope is the ingest guard + its checkpoint-advance correctness. Unifying the classifier would reach into the responder read path (
graph-plan.ts) and alter its empty-string handling — a separate, independently-reviewable change. Tracked here to keep #1921 focused.Related: #1921, PR #1936. Raised by otReviewAgent on PR #1936 (durable-integrity.ts ~:1454 thread).