Repository navigation
[finding] the authorable-key deletion gate's reachability BFS reports integration/DataSyncConfig unreachable from the metadata-type roots — it is two hops from the connector root, so #4650 proof 2 waives a tombstone for a key a stack manifest really authors #17356
Description
What is wrong
computeSurfaceReachability() in packages/spec/scripts/build-schemas.ts is the whole of check (c) proof 2 (#4650, 2026-08-02 ruling): a deleted authorable-surface/ baseline line is legitimate when "the def is not reachable from the metadata-type roots (no metadata document is ever parsed by it), so its entry was over-collection and there is no author to tombstone for."
For integration/DataSyncConfig that answer is measurably false. The def sits two hops from a root the same file enumerates, and the runtime really does parse metadata documents against it.
The function's own docblock names this direction as the dangerous one:
Judged conservatively: one shared entry marks the whole def reachable — a false "reachable" demands a tombstone too many, a false "unreachable" would waive one silently.
This is that false "unreachable", in the tree, today.
The two readings, side by side
Reading A — what the gate says. pnpm --filter @objectstack/spec gen:schema on a branch that bare-deletes integration/DataSyncConfig:schedule, exit 0, printing:
ℹ️ 4 baseline deletion(s) since cca1dc0bfb1e carry their own proof (#4650):
- integration/DataSyncConfig:schedule — def not reachable from the 26 metadata-type roots
(BUILTIN_METADATA_TYPE_SCHEMAS + EXTRA_METADATA_TYPE_SCHEMAS overlay; BFS over this
build's in-memory Zod graph): an over-collected entry, never parsed against a
metadata document.
Reading B — what the graph actually holds. A tsx probe over the same modules the build imports, run with OS_EAGER_SCHEMAS=1 exactly as gen:schema does:
roots: 26
connector root === DeclarativeConnectorEntrySchema: true
connector root type tag: object
unwrap trail: object
shape keys sample: name,label,type,description,icon,authentication,provider,providerConfig
has syncConfig prop: true
syncConfig unwrap: optional -> object
syncConfig inner === DataSyncConfigSchema: true
So: getMetadataTypeSchema('connector') (an EXTRA_METADATA_TYPE_SCHEMAS entry, one of the 26 roots the message counts) resolves to an object whose syncConfig property, unwrapped through one optional, is the same instance as the exported DataSyncConfigSchema. reachableVia('integration/DataSyncConfig') should answer root-graph; it answers null.
Reading C — the runtime agrees with B, not A. Pin tests in packages/spec/src/cron-typed-positions-retirement.test.ts parse a connector through three doors and observe syncConfig being processed by that def each time: the bare ConnectorSchema, DeclarativeConnectorEntrySchema, and ObjectStackSchema with connectors: [...] — i.e. a real defineStack manifest, and PUT /api/v1/meta/connector/:name behind getMetadataTypeSchema. A metadata document IS parsed by this def.
Why it matters
Proof 2 exists to waive the tombstone requirement only where no author can receive a prescription. Where it answers falsely, the gate lets an authorable key be deleted from a non-strict schema with no tombstone and no ADR-0087 entry — which is precisely the silent-strip class ADR-0104 and #3733 name: the author keeps writing the key, gets a clean parse, and the setting never takes effect. Every gate stays green, repo-wide, for exactly the shape check (a) refuses in its own failure text.
The blast radius is not one key: any def reachable only through the same shape of edge is equally invisible, so the waiver is available to every future spec retirement that touches one.
Repro
- On a branch off
main, delete theschedulekey fromDataSyncConfigSchemainpackages/spec/src/integration/connector.zod.ts(noretiredKey(), no registry entry). - Delete the line
"integration/DataSyncConfig:schedule"frompackages/spec/authorable-surface/integration.json. pnpm --filter @objectstack/spec gen:schema— exits 0 and prints the proof-2 line quoted above.
Observed on objectstack-ai/objectstack at 1504dd1d77e9b0e0c53dd6fb4251c0874e1e5b7e (branch claude/issue-16320-retire-cron-typed-positions), against merge base cca1dc0bfb1e380442670843606d6a0c7762f6ba.
Where to look
packages/spec/scripts/build-schemas.ts:
computeSurfaceReachability()— the BFS,zodChildSchemas(node)and thebridgedmap;zodByDefKey— whether the instance it holds forintegration/DataSyncConfigis the instance the connector shape holds (the probe shows the EXPORTED schema is; the def-key map may hold another);zodShapeOf(node)for theconnectorroot and for theoptionalwrapper onsyncConfig.
Not diagnosed here on purpose — two independent candidates (an edge the child-walk does not follow, or a def-key map keyed on a different instance) and only one of them is the fault. This card names the contradiction and the reading that settles it.
Not in this card's scope
⛔ Nothing here argues that the four deletions on the branch above should be reverted; that route is a maintainer ruling and stands on its own. What is asked here is that proof 2 stop answering null for a def a manifest parses.
Suggested acceptance
- a
--self-testcase that fails when a def reachable through anoptional-wrapped property of anEXTRA_METADATA_TYPE_SCHEMASroot readsnull; reachableVia('integration/DataSyncConfig')answersroot-graphonmain;- the fix does not turn the conservatism around: a genuinely unreachable def still reads
null, pinned with its own case.
Generated by Claude Code
Activity