test(spec): the ADR-0010 envelope gate covers UNREGISTERED_KIND_SCHEMAS (#6931) - #7116
Conversation
…AS (#6931) `metadata-type-schemas.test.ts` holds the invariant that every metadata type either declares `...MetadataProtectionFields` or sits on an explicit debt list, and that debt list is empty with a comment calling it the end state — so the gate read as total coverage. It was not. The walk iterates `listMetadataTypeSchemaTypes()`, which deliberately excludes the three non-KIND stack collections bound in `UNREGISTERED_KIND_SCHEMAS` (webhook / connector / sharing_rule). Those three are real parse doors — #6245 wired them to `PUT /api/v1/meta/:type/:name` — so the one automated check for "declares no envelope" never ran over any of them, and all three were judged one at a time by hand: sharing_rule surfaced as a hard 422, connector only after a silent seven-key strip and a separate card a day later (#6362 / PR #6900), and webhook was fine by accident of #4001 batch 11 with nothing verifying it. A second `it.each` asserts the envelope property — and only that property — over `listUnregisteredKindSchemaTypes()`, sharing the same `objectShapes` walker and `rejectedEnvelopeKeys` probe as the registered walk so the two iterations cannot drift into judging the property differently. All three pass today, so this closes no live bug; the value is prospective, for a fourth entry. Reverse-verified in both directions, predicted before measured. Removing the spread from ConnectorSchema turns the new `connector DECLARES` case red while every case of the two original suites stays green — the blind spot, exhibited. Adding a fourth entry with no envelope turns its `DECLARES` case red while the #4001 campaign count is unmoved. Scope fence, per the triage ruling on #6931: this enrolls the three in the test's iteration only. `listUnregisteredKindSchemaTypes()` returns names and grants nothing — no MetadataTypeSchema membership, no DEFAULT_METADATA_TYPE_REGISTRY entry, no create seed, no authorization verdict, no place in the campaign count. `listMetadataTypeSchemaTypes()` output is byte-identical, and a pinned case keeps it that way, so #2657's B/C decision stays open and unprejudged. Fixes #6931 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJpiZS4AgkrDYwZ2Amh5mw
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
Closes the blind spot recorded in #6931:
metadata-type-schemas.test.tsholds the ADR-0010 envelope-declaration invariant, its debt list is empty and commented as the end state — and the three real parse doors bound inUNREGISTERED_KIND_SCHEMASwere never in its iteration.Premise, verified on
origin/main(f5a9bc2) before writing anythingmetadata-type-schemas.test.tsholds the ADR-0010 invariant;UNDECLARED_ENVELOPEis empty and its comment calls that "the end state"const UNDECLARED_ENVELOPE = new Set<string>([]), plus "This list is now EMPTY, and that is the end state — not a reason to delete it."listMetadataTypeSchemaTypes()) deliberately excludesUNREGISTERED_KIND_SCHEMAS(webhook/connector/sharing_rule)[#6245]commentorigin/main:webhook,connector,sharing_ruleeach resolve 1 object shape and each declares_packageIdPremise alive, so the work proceeded.
What changed
A second
it.eachin the same file, overlistUnregisteredKindSchemaTypes(), asserting envelope posture and nothing else: the schema declares the protection-fields spread, or sits on its own explicit debt-list constant (UNDECLARED_ENVELOPE_UNREGISTERED— empty, kept separate from the registered list on purpose). It carries the same reverse pin as its sibling, so the debt list cannot outlive the debt.Walker reuse, measured rather than assumed. Both iterations live in the same file and call the same
objectShapesstructural walker and the samerejectedEnvelopeKeysprobe — no second copy exists to drift. Only the SET each walks differs, which is the whole point of the split.One new export,
listUnregisteredKindSchemaTypes()(@objectstack/spec/kernel) — the issue's direction 2, so the check enumerates the map rather than hand-listing three names (a hand-list would not inherit a fourth entry, which is the entire prospective value). It returns names and grants nothing else.The fence
Triage ruling on #6931, quoted verbatim:
Held, and checked item by item:
listMetadataTypeSchemaTypes()output is byte-identical. Measured before and after; both runs return the same 26 names in the same order —["action","agent","api","app","book","capability","dashboard","dataset","datasource","doc","email_template","field","flow","hook","job","mapping","object","page","permission","position","report","seed","skill","tool","translation","view"]. The function body is untouched.#4001 — reports the campaign numberstill asserts 25 closed / 26 total and is green — including in reverse-verification run 2, where a fake fourth unregistered entry existed and the count did not move.metadata-create-seeds.test.ts(a create seed per registered member),capability-metadata-kind.test.tsandmetadata-type-api-registration.test.tsare green;MetadataTypeSchemaandDEFAULT_METADATA_TYPE_REGISTRYare not in the diff at all (metadata-plugin.zod.tsuntouched), andcheck:stack-collection-maps/check:meta-type-normalizedpass.sharing_ruleis strict andconnectoris not, and choosing between them is Studio metadata coverage gaps: surface remaining types + promote un-typed concepts #2657's call, not this suite's.stays OUT of the registered-kind setfails if any of the three ever appears inlistMetadataTypeSchemaTypes(). It is a signpost, not a veto: promotion remains available, it just cannot happen invisibly.Reverse verification — predictions written before each run
Run 1 — remove
...MetadataProtectionFieldsfromConnectorSchema(connector.zod.ts:880), then restoreconnector DECLARES the protection envelope→ RED, naming'connector''connector' does not declare _packageId, so the envelope is dropped on every parse through PUT /meta — silently, if the schema is not strict.connector does not REJECT the protection envelope→ GREEN (non-strict ⇒ silent strip, no 422)connectoris not in their iteration1 failed | 120 passed (121), the single failure in the new blockThat last row is the blind spot exhibited: the gate that exists to catch "declares no envelope" reports success while a bound parse door silently strips all seven keys.
Run 2 — add a fake fourth entry (
fake_kind: z.object({ name: z.string() }), no spread), then removefake_kind DECLARES the protection envelope→ RED'fake_kind'fake_kindresolves-schema / walker-understands / does-not-REJECT → GREEN1 failed | 124 passed (125)Both edits were reverted;
git statusclean of them before the commit.Gates
Every gate
.github/workflows/lint.ymlruns, one by one — all green:pnpm lint,pnpm --filter @objectstack/spec exec tsc --noEmit,turbo run typecheck(121/121), examples typecheck,@objectstack/downstream-contracttypecheck.pnpm check:*gates. Three (check:app-nav-i18n,check:i18n,check:i18n-coverage) first reported "nothing was measured — build first"; afterturbo run build(70/70) all three pass.check:type-check-coverage/check:type-check-debt(33 ledger entries re-measured, none above its recorded number).check:export-origins— NEW today, and the brief's expectation did not hold. A purely test-only change would not touch it; this change is not purely test-only, because the fenced fix needs the names export. So the one added export made bothapi-surface/kernel.jsonandexport-origins/kernel.jsonstale (0 breaking (removed/narrowed), 1 added). Regenerated exactly those two viagen:api-surface+gen:export-origins— one line added to each file, nothing else — andcheck:generatedthen reports all 11 artifacts up to date. Flagging this explicitly rather than letting it read as an untouched gate.node scripts/check-adr-0087-registration.mjs --base origin/main→this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)— nothing owed.Changeset: included (
@objectstack/spec: patch). The recent test-only analog that added a checked-in artifact plus a gate — #7090,test(spec): export-surface pins compare a build-time baseline instead of running tsc— carried exactly that, and this PR has the stronger reason: it adds a public export to a published package, so theskip-changeset"releases nothing" route does not apply.Tests: full suite green — 134/134 turbo tasks, zero failures.
@objectstack/lintwas run separately (68 files / 1770 tests, all pass): two earlier full-suite passes hit 5s and 30s vitest timeouts in its corpus sweeps under parallel container load, on cases unrelated to this diff, and the package is green in isolation.Fixes #6931
Generated by Claude Code