test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key - #14830

Open
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate
Open

test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key#14830
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate

Conversation

@os-trump

@os-trumpos-trump commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14642

A gate that goes red when a /packages door's field allowlist drops a key the
producer stamps or the record declares. Both sides of the comparison are derived
by running real code; the only hand-kept artifacts are two annotated registers,
both compared loudly.

Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

File face

Two files, both new, both tests:

  • packages/rest/src/package-door-producer-key-carry.test.ts
  • packages/runtime/src/domains/package-door-producer-key-carry.test.ts

No production file is modified. In particular packages/rest/src/error-response.ts
is not touched — PR #14713 is in flight on it and the gate's honest landing
site was never that file. No CI job added, no CI filter root widened.

Base refreshed by mergingorigin/main (not rebasing, no force-push) at
47181632f2. That merge matters: PR #14785 landed packages/runtime/tsconfig.test.json
while this branch was open, so the runtime pin is now compiled by a real tsc
program. See Verification.

Is the near-miss still reachable on today's main?

  • For writable itself: no. Both doors carry it now, by two different
    mechanisms, and both are pinned by name (package-list-writable-carry.test.ts,
    packages-serializable-response.test.ts).
  • For the general case: yes, fully reachable. Nothing generalised past that
    one field. The ablation below reintroduces the drop four different ways and
    every one of them shipped a 200 with a field silently absent before this PR.

So the card's premise stands, with the triage comment's sharpening: the gap is
the next stamped or declared key, not writable.

The two doors do not share an invariant, and the gate does not pretend they do

REST doorruntime door
producergetMetaItems({type:'package'})registry.getAllPackages()
where writable comes fromstamped upstream, listed in the allowlistcomputed at the door, deliberately not in the allowlist
invariantthe allowlist contains every stamped keythe stamp runs after the projection

Asserting "the allowlist contains every stamped key" at the runtime door would
red on a correct door. So the runtime pin measures the stamp set as
served − record — never a list — and compares it for set equality against
an annotated register. A reorder empties that measured set and reds; a new stamp
grows it and reds until the decision is written down. That register is hand-kept
on purpose: swapping a silent list for a loud one is the mechanism this card
asked for.

Both gate readings

Four ablation legs, each mutating one production line, each restored and proven
restored. The mutation is proven on disk by an anchor count before/after, never
by the editor's exit code. Re-run in full after the merge; every number below
is identical before and after
, so the merge moved nothing.

legmutationgatereading
1drop 'writable' from REGISTRY_PACKAGE_RESPONSE_FIELDSRESTREDlist/detail PKG: writable, 6 drops across 3 packages
2drop 'installedVersion' from the same allowlistRESTREDlist/detail PKG: installedVersion, 6 drops
3reorder to toPackageResponse(withWritableVerdict(...))runtimeREDstamp set for com.example.showcase: expected [] to deeply equal [ 'writable' ]
4drop 'installedVersion' from INSTALLED_PACKAGE_RESPONSE_FIELDSruntimeREDlist/detail PKG: installedVersion, 6 drops
controltree restoredbothGREEN — REST 4/4, runtime 5/5

Leg 1 is the exact near-miss the card is about. Leg 3 is the exact regression the
runtime door's ordering comment warns about. Legs 2 and 4 are the generality
claim: a plain declared field, with no field-specific pin anywhere.

No rebuild leg is involved and none is owed. Both mutated files are resolved
by vitest as local source — the REST pin imports ./package-routes.js, the
runtime pin reaches ./domains/packages.js through ../http-dispatcher.js — so
dist/ is not on the resolution path for either subject and there is nothing to
preflight. The restore leg is proven by git diff HEAD being empty for both
doors after every leg, not by the restore command's exit code.

Two findings the ablation produced, both now fixed in this PR

Recording these because in both cases the first version of the gate was green
for a bad reason, and a reviewer should see that the readings above are not the
first ones taken.

  1. Object.keys was the wrong instrument.installPackage seats settings
    as an own property holding undefined. Both doors omit undefined-valued
    fields deliberately, and JSON.stringify drops them anyway, so the first run
    reported six false drops for a key no consumer could ever have observed. The
    detector now measures defined-valued keys.

  2. The fixture could not see two thirds of the record. Deleting
    installedVersion from either allowlist left both gates green on the
    first ablation: installPackage writes only the fields an install can know,
    so six of the twelve declared fields are simply absent from a freshly
    installed record. seatDeclaredFields now seats every field the record
    schema declares, and a control assertion fails if any declared slot becomes
    unobservable again.

On packages/spec

The card rules out deriving the production allowlist from packages/spec,
and this PR does not: both allowlists stay hand-written and are untouched. What
finding 2 derives from the schema is the test's expectation, which is the
card's own wording for the detector — "the projected key set ⊇ the producer's
stamped/declared key set minus an explicit, annotated exclusion list".
Deriving the expectation is precisely what turns a newly declared field into an
explicit decision at each door instead of a silent omission. It adds no import
edge either: @objectstack/spec is already a runtime dependency of both
packages. The rationale is spelled out at DECLARED_RECORD_KEYS in both files so
it cannot be mistaken for the rejected design.

Why two pins rather than one detector

Measured, not assumed. A single file would have to reach the other package's
door:

  • From packages/runtime, @objectstack/rest resolves to its dist/ (no
    vitest alias maps it to source), which would make the gate's verdict a function
    of build state. It would also growKNOWN_UNALIASED_TEST_IMPORTS in
    scripts/check-test-source-alias.mjs, a shrink-only registry that lists no
    @objectstack/rest entry for that package today.
  • @objectstack/rest cannot import @objectstack/runtime at all — the
    dependency runs the other way.

So: two pins, one shape. Each imports its own door as source, which is also
what lets a reviewer ablate one allowlist and watch exactly one gate go red.

What I did NOT do, and why

  • Did not change either allowlist. Both are correct today; the card asked for
    the detector, not an edit to the lists.
  • Did not touch packages/rest/src/error-response.ts (hot file, PR fix(rest): consult the bespoke structured arms before the declared-status passthrough, so both error doors answer one refusal with one body #14713).
  • Did not add a changeset. The diff is two test files and nothing else. No
    package publishes a test file, so nothing user-visible ships. skip-changeset
    applied.
  • Did not widen a CI filter root or add a CI job.
  • Did not add anything to packages/runtime/test-typecheck-debt.json, and did
    not need to — see Verification. That ledger is byte-identical to origin/main
    in this branch.
  • Did not silence anything to reach green. No any, @ts-expect-error, or
    eslint-disable was added anywhere, and no assertion was weakened. The
    load-bearing comparison — set equality on the stamp set — runs over
    Set(string) and string[], not any.

Verification

All readings from 47181632f2, the merge commit at the head of this branch.
The dependency closure was built first (pnpm --filter '@objectstack/runtime^...' build),
because an unbuilt closure turns every workspace import into TS2307 and makes a
typecheck verdict a statement about a different world.

The runtime test-layer typecheck, which the merge newly applies to this PR

pnpm --filter @objectstack/runtime typecheckEXIT=0. It now chains
check:test-typecheck, whose own verdict line is:

check:test-typecheck: OK — @objectstack/runtime's test layer compiles under
packages/runtime/tsconfig.test.json; 27 file(s) / 191 error(s) / 69 pinned
signature(s) held in test-typecheck-debt.json (shrink-only and identity-pinned)

Three separate confirmations that the new runtime pin is genuinely covered and
carries no debt:

  • tsc --noEmit --project tsconfig.test.json --listFiles shows 1 hit for
    src/domains/package-door-producer-key-carry.test.ts — it is in the program.
    (This replaces the pre-merge reading of 0 hits, which was true on the old base
    and is now false.)
  • Error lines naming that file in the same run: 0. The 191 errors are exactly
    the ledgered ones in the other 27 files.
  • packages/runtime/test-typecheck-debt.json is byte-identical to
    origin/main's copy — nothing added, no number moved.

Other green

  • packages/rest gate — 4/4; packages/runtime gate — 5/5.
  • Sibling door pins re-run beside them after the merge:
    package-registry-item-projection, package-list-writable-carry,
    packages-serializable-response, packages-writable-verdict — 3 files /
    14 tests and 3 files / 23 tests, both EXIT=0.
  • pnpm --filter @objectstack/rest typecheck — EXIT=0, with --listFiles
    confirming 1 hit for the new REST test file.
  • 25 of the 28 gate families derived by scripts/pm/dispatch-gates.mjs --commands
    — exit 0, re-derived and re-run on the merged head (the family list is
    unchanged, and the change set is still exactly these two files).
    check:engine-double-contract was RED early on (my fixture's findOne double)
    and is green after removing that verb, which the producer path never calls.

Not measured, with the reason

  • check:dual-build-cjs-loads and check:type-check-debt --re-measure — exit
    3, "PREREQUISITE NOT MET": both need the whole workspace built
    (turbo run build --filter='./packages/*' --filter='./packages/*/*'), not just
    one closure. Exit 3 is neither a pass nor a finding. The sub-measurement that
    this diff could actually have moved — the runtime test-layer debt — is answered
    directly above by check:test-typecheck at EXIT=0 with the ledger unchanged.
    And check:dual-build-cjs-loads reads built output, which a diff adding only
    *.test.ts files cannot reach.
  • check-test-completeness — exit 3; it needs a saved turbo run test log,
    and its own failure text says to record it NOT MEASURED when run locally.

Narrowed, declared

The two packages' FULL suites were not run. Nine attempts on the shared verify
lock across this branch never got a long enough turn — holders included one run
at 917s and another at 1704s — and @objectstack/runtime's whole suite exceeds
what fits inside the container's foreground cap on a shared box. Narrowed to the
two new files plus the four sibling pins on the same two doors, all green above.
The diff modifies no production file, no config, and no turbo/vitest setting, so
the only way a sibling test's verdict could move is cross-file interference
inside a vitest worker; CI runs the full sharded suite on this PR regardless.

Generated by Claude Code

…drop a stamped key
Both `/packages` doors project the installed-package record through a
hand-written field allowlist. The trade that bought was explicit: drift
shows up as a missing field, never a 500. Within one day of it landing,
an ADR-0070 D2 `writable` verdict started reaching both doors and both
would have dropped it — a 200 with the field simply absent. One door was
saved by someone reading a sibling pin, the other by a merge conflict.
The field-specific pins that followed cover `writable` and nothing else.
These two gates cover the general case, deriving both sides from real
code rather than a hand-kept key list:
- REST door: `served ⊇ getMetaItems({type:'package'}) keys`, measured
through the real `ObjectStackProtocolImplementation` over a real
`SchemaRegistry`.
- runtime door: `served ⊇ record keys`, plus a set-equality register
for the keys the door stamps AFTER the projection — measured as
`served − record`, so a reorder empties it and reds.
The two doors solved the near-miss differently (the REST allowlist
contains `writable`; the runtime one deliberately does not and orders
instead), so the invariants are stated separately rather than assumed
symmetric. Only hand-kept artifacts are the annotated exclusion and
stamp registers, both compared loudly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…esence
`SchemaRegistry.installPackage` seats the optional record fields as own
properties holding `undefined`, so `'settings' in record` is true for a
package installed without settings. Both doors omit undefined-valued
fields deliberately and JSON.stringify drops them anyway, so counting
them made the gate red on every package for a key no consumer could have
observed — measured on the first run, six false drops.
Also drops the fixture's `findOne` double: `getMetaItems` never reaches
that verb, and `check:engine-double-contract` is right that a fake looser
than ObjectQL.findOne is worth refusing rather than stubbing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
… all of them
The ablation caught this and it is the more useful of the two findings:
deleting `installedVersion` from EITHER door's allowlist left both gates
GREEN. `installPackage` leaves 8 of the 12 declared fields as own
properties holding `undefined`, and a key the wire cannot carry is
correctly invisible to the detector — so the coverage assertion was
exercising 4 fields while reading as if it covered the record.
`seatDeclaredFields` fills every own key whose value is `undefined`,
derived from the record's own key set rather than from a list of field
names, so a field added tomorrow is seated without an edit. A control
assertion now fails if any declared slot is unobservable again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…Schema
The first ablation's most useful reading: deleting `installedVersion`
from either allowlist left both gates GREEN. Six of the twelve declared
record fields are ABSENT from a freshly installed record (installPackage
writes only what an install can know), so a gate watching only the
producer's live output cannot see them dropped.
The card asks for `served ⊇ the producer's stamped/DECLARED key set`, so
the declared half is derived from the record schema — in the TEST's
expectation, never in the production allowlist, which stays hand-written
and untouched. That distinction is the one the originating card ruled on
and it is spelled out at DECLARED_RECORD_KEYS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@os-trumpos-trump added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/l labels Sep 3, 2026 — with Claude
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f116b8f8d1b43ec2bfd64fbf0ebb0cf3c301b1c8packageMentionDocs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key - #14830

Open
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate
Open

test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key#14830
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate

Conversation

@os-trump

@os-trumpos-trump commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14642

A gate that goes red when a /packages door's field allowlist drops a key the
producer stamps or the record declares. Both sides of the comparison are derived
by running real code; the only hand-kept artifacts are two annotated registers,
both compared loudly.

Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

File face

Two files, both new, both tests:

  • packages/rest/src/package-door-producer-key-carry.test.ts
  • packages/runtime/src/domains/package-door-producer-key-carry.test.ts

No production file is modified. In particular packages/rest/src/error-response.ts
is not touched — PR #14713 is in flight on it and the gate's honest landing
site was never that file. No CI job added, no CI filter root widened.

Base refreshed by mergingorigin/main (not rebasing, no force-push) at
47181632f2. That merge matters: PR #14785 landed packages/runtime/tsconfig.test.json
while this branch was open, so the runtime pin is now compiled by a real tsc
program. See Verification.

Is the near-miss still reachable on today's main?

  • For writable itself: no. Both doors carry it now, by two different
    mechanisms, and both are pinned by name (package-list-writable-carry.test.ts,
    packages-serializable-response.test.ts).
  • For the general case: yes, fully reachable. Nothing generalised past that
    one field. The ablation below reintroduces the drop four different ways and
    every one of them shipped a 200 with a field silently absent before this PR.

So the card's premise stands, with the triage comment's sharpening: the gap is
the next stamped or declared key, not writable.

The two doors do not share an invariant, and the gate does not pretend they do

REST doorruntime door
producergetMetaItems({type:'package'})registry.getAllPackages()
where writable comes fromstamped upstream, listed in the allowlistcomputed at the door, deliberately not in the allowlist
invariantthe allowlist contains every stamped keythe stamp runs after the projection

Asserting "the allowlist contains every stamped key" at the runtime door would
red on a correct door. So the runtime pin measures the stamp set as
served − record — never a list — and compares it for set equality against
an annotated register. A reorder empties that measured set and reds; a new stamp
grows it and reds until the decision is written down. That register is hand-kept
on purpose: swapping a silent list for a loud one is the mechanism this card
asked for.

Both gate readings

Four ablation legs, each mutating one production line, each restored and proven
restored. The mutation is proven on disk by an anchor count before/after, never
by the editor's exit code. Re-run in full after the merge; every number below
is identical before and after
, so the merge moved nothing.

legmutationgatereading
1drop 'writable' from REGISTRY_PACKAGE_RESPONSE_FIELDSRESTREDlist/detail PKG: writable, 6 drops across 3 packages
2drop 'installedVersion' from the same allowlistRESTREDlist/detail PKG: installedVersion, 6 drops
3reorder to toPackageResponse(withWritableVerdict(...))runtimeREDstamp set for com.example.showcase: expected [] to deeply equal [ 'writable' ]
4drop 'installedVersion' from INSTALLED_PACKAGE_RESPONSE_FIELDSruntimeREDlist/detail PKG: installedVersion, 6 drops
controltree restoredbothGREEN — REST 4/4, runtime 5/5

Leg 1 is the exact near-miss the card is about. Leg 3 is the exact regression the
runtime door's ordering comment warns about. Legs 2 and 4 are the generality
claim: a plain declared field, with no field-specific pin anywhere.

No rebuild leg is involved and none is owed. Both mutated files are resolved
by vitest as local source — the REST pin imports ./package-routes.js, the
runtime pin reaches ./domains/packages.js through ../http-dispatcher.js — so
dist/ is not on the resolution path for either subject and there is nothing to
preflight. The restore leg is proven by git diff HEAD being empty for both
doors after every leg, not by the restore command's exit code.

Two findings the ablation produced, both now fixed in this PR

Recording these because in both cases the first version of the gate was green
for a bad reason, and a reviewer should see that the readings above are not the
first ones taken.

  1. Object.keys was the wrong instrument.installPackage seats settings
    as an own property holding undefined. Both doors omit undefined-valued
    fields deliberately, and JSON.stringify drops them anyway, so the first run
    reported six false drops for a key no consumer could ever have observed. The
    detector now measures defined-valued keys.

  2. The fixture could not see two thirds of the record. Deleting
    installedVersion from either allowlist left both gates green on the
    first ablation: installPackage writes only the fields an install can know,
    so six of the twelve declared fields are simply absent from a freshly
    installed record. seatDeclaredFields now seats every field the record
    schema declares, and a control assertion fails if any declared slot becomes
    unobservable again.

On packages/spec

The card rules out deriving the production allowlist from packages/spec,
and this PR does not: both allowlists stay hand-written and are untouched. What
finding 2 derives from the schema is the test's expectation, which is the
card's own wording for the detector — "the projected key set ⊇ the producer's
stamped/declared key set minus an explicit, annotated exclusion list".
Deriving the expectation is precisely what turns a newly declared field into an
explicit decision at each door instead of a silent omission. It adds no import
edge either: @objectstack/spec is already a runtime dependency of both
packages. The rationale is spelled out at DECLARED_RECORD_KEYS in both files so
it cannot be mistaken for the rejected design.

Why two pins rather than one detector

Measured, not assumed. A single file would have to reach the other package's
door:

  • From packages/runtime, @objectstack/rest resolves to its dist/ (no
    vitest alias maps it to source), which would make the gate's verdict a function
    of build state. It would also growKNOWN_UNALIASED_TEST_IMPORTS in
    scripts/check-test-source-alias.mjs, a shrink-only registry that lists no
    @objectstack/rest entry for that package today.
  • @objectstack/rest cannot import @objectstack/runtime at all — the
    dependency runs the other way.

So: two pins, one shape. Each imports its own door as source, which is also
what lets a reviewer ablate one allowlist and watch exactly one gate go red.

What I did NOT do, and why

  • Did not change either allowlist. Both are correct today; the card asked for
    the detector, not an edit to the lists.
  • Did not touch packages/rest/src/error-response.ts (hot file, PR fix(rest): consult the bespoke structured arms before the declared-status passthrough, so both error doors answer one refusal with one body #14713).
  • Did not add a changeset. The diff is two test files and nothing else. No
    package publishes a test file, so nothing user-visible ships. skip-changeset
    applied.
  • Did not widen a CI filter root or add a CI job.
  • Did not add anything to packages/runtime/test-typecheck-debt.json, and did
    not need to — see Verification. That ledger is byte-identical to origin/main
    in this branch.
  • Did not silence anything to reach green. No any, @ts-expect-error, or
    eslint-disable was added anywhere, and no assertion was weakened. The
    load-bearing comparison — set equality on the stamp set — runs over
    Set(string) and string[], not any.

Verification

All readings from 47181632f2, the merge commit at the head of this branch.
The dependency closure was built first (pnpm --filter '@objectstack/runtime^...' build),
because an unbuilt closure turns every workspace import into TS2307 and makes a
typecheck verdict a statement about a different world.

The runtime test-layer typecheck, which the merge newly applies to this PR

pnpm --filter @objectstack/runtime typecheckEXIT=0. It now chains
check:test-typecheck, whose own verdict line is:

check:test-typecheck: OK — @objectstack/runtime's test layer compiles under
packages/runtime/tsconfig.test.json; 27 file(s) / 191 error(s) / 69 pinned
signature(s) held in test-typecheck-debt.json (shrink-only and identity-pinned)

Three separate confirmations that the new runtime pin is genuinely covered and
carries no debt:

  • tsc --noEmit --project tsconfig.test.json --listFiles shows 1 hit for
    src/domains/package-door-producer-key-carry.test.ts — it is in the program.
    (This replaces the pre-merge reading of 0 hits, which was true on the old base
    and is now false.)
  • Error lines naming that file in the same run: 0. The 191 errors are exactly
    the ledgered ones in the other 27 files.
  • packages/runtime/test-typecheck-debt.json is byte-identical to
    origin/main's copy — nothing added, no number moved.

Other green

  • packages/rest gate — 4/4; packages/runtime gate — 5/5.
  • Sibling door pins re-run beside them after the merge:
    package-registry-item-projection, package-list-writable-carry,
    packages-serializable-response, packages-writable-verdict — 3 files /
    14 tests and 3 files / 23 tests, both EXIT=0.
  • pnpm --filter @objectstack/rest typecheck — EXIT=0, with --listFiles
    confirming 1 hit for the new REST test file.
  • 25 of the 28 gate families derived by scripts/pm/dispatch-gates.mjs --commands
    — exit 0, re-derived and re-run on the merged head (the family list is
    unchanged, and the change set is still exactly these two files).
    check:engine-double-contract was RED early on (my fixture's findOne double)
    and is green after removing that verb, which the producer path never calls.

Not measured, with the reason

  • check:dual-build-cjs-loads and check:type-check-debt --re-measure — exit
    3, "PREREQUISITE NOT MET": both need the whole workspace built
    (turbo run build --filter='./packages/*' --filter='./packages/*/*'), not just
    one closure. Exit 3 is neither a pass nor a finding. The sub-measurement that
    this diff could actually have moved — the runtime test-layer debt — is answered
    directly above by check:test-typecheck at EXIT=0 with the ledger unchanged.
    And check:dual-build-cjs-loads reads built output, which a diff adding only
    *.test.ts files cannot reach.
  • check-test-completeness — exit 3; it needs a saved turbo run test log,
    and its own failure text says to record it NOT MEASURED when run locally.

Narrowed, declared

The two packages' FULL suites were not run. Nine attempts on the shared verify
lock across this branch never got a long enough turn — holders included one run
at 917s and another at 1704s — and @objectstack/runtime's whole suite exceeds
what fits inside the container's foreground cap on a shared box. Narrowed to the
two new files plus the four sibling pins on the same two doors, all green above.
The diff modifies no production file, no config, and no turbo/vitest setting, so
the only way a sibling test's verdict could move is cross-file interference
inside a vitest worker; CI runs the full sharded suite on this PR regardless.

Generated by Claude Code

…drop a stamped key
Both `/packages` doors project the installed-package record through a
hand-written field allowlist. The trade that bought was explicit: drift
shows up as a missing field, never a 500. Within one day of it landing,
an ADR-0070 D2 `writable` verdict started reaching both doors and both
would have dropped it — a 200 with the field simply absent. One door was
saved by someone reading a sibling pin, the other by a merge conflict.
The field-specific pins that followed cover `writable` and nothing else.
These two gates cover the general case, deriving both sides from real
code rather than a hand-kept key list:
- REST door: `served ⊇ getMetaItems({type:'package'}) keys`, measured
through the real `ObjectStackProtocolImplementation` over a real
`SchemaRegistry`.
- runtime door: `served ⊇ record keys`, plus a set-equality register
for the keys the door stamps AFTER the projection — measured as
`served − record`, so a reorder empties it and reds.
The two doors solved the near-miss differently (the REST allowlist
contains `writable`; the runtime one deliberately does not and orders
instead), so the invariants are stated separately rather than assumed
symmetric. Only hand-kept artifacts are the annotated exclusion and
stamp registers, both compared loudly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…esence
`SchemaRegistry.installPackage` seats the optional record fields as own
properties holding `undefined`, so `'settings' in record` is true for a
package installed without settings. Both doors omit undefined-valued
fields deliberately and JSON.stringify drops them anyway, so counting
them made the gate red on every package for a key no consumer could have
observed — measured on the first run, six false drops.
Also drops the fixture's `findOne` double: `getMetaItems` never reaches
that verb, and `check:engine-double-contract` is right that a fake looser
than ObjectQL.findOne is worth refusing rather than stubbing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
… all of them
The ablation caught this and it is the more useful of the two findings:
deleting `installedVersion` from EITHER door's allowlist left both gates
GREEN. `installPackage` leaves 8 of the 12 declared fields as own
properties holding `undefined`, and a key the wire cannot carry is
correctly invisible to the detector — so the coverage assertion was
exercising 4 fields while reading as if it covered the record.
`seatDeclaredFields` fills every own key whose value is `undefined`,
derived from the record's own key set rather than from a list of field
names, so a field added tomorrow is seated without an edit. A control
assertion now fails if any declared slot is unobservable again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…Schema
The first ablation's most useful reading: deleting `installedVersion`
from either allowlist left both gates GREEN. Six of the twelve declared
record fields are ABSENT from a freshly installed record (installPackage
writes only what an install can know), so a gate watching only the
producer's live output cannot see them dropped.
The card asks for `served ⊇ the producer's stamped/DECLARED key set`, so
the declared half is derived from the record schema — in the TEST's
expectation, never in the production allowlist, which stays hand-written
and untouched. That distinction is the one the originating card ruled on
and it is spelled out at DECLARED_RECORD_KEYS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@os-trumpos-trump added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/l labels Sep 3, 2026 — with Claude
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f116b8f8d1b43ec2bfd64fbf0ebb0cf3c301b1c8packageMentionDocs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key - #14830

Open
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate
Open

test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key#14830
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate

Conversation

@os-trump

@os-trumpos-trump commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14642

A gate that goes red when a /packages door's field allowlist drops a key the
producer stamps or the record declares. Both sides of the comparison are derived
by running real code; the only hand-kept artifacts are two annotated registers,
both compared loudly.

Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

File face

Two files, both new, both tests:

  • packages/rest/src/package-door-producer-key-carry.test.ts
  • packages/runtime/src/domains/package-door-producer-key-carry.test.ts

No production file is modified. In particular packages/rest/src/error-response.ts
is not touched — PR #14713 is in flight on it and the gate's honest landing
site was never that file. No CI job added, no CI filter root widened.

Base refreshed by mergingorigin/main (not rebasing, no force-push) at
47181632f2. That merge matters: PR #14785 landed packages/runtime/tsconfig.test.json
while this branch was open, so the runtime pin is now compiled by a real tsc
program. See Verification.

Is the near-miss still reachable on today's main?

  • For writable itself: no. Both doors carry it now, by two different
    mechanisms, and both are pinned by name (package-list-writable-carry.test.ts,
    packages-serializable-response.test.ts).
  • For the general case: yes, fully reachable. Nothing generalised past that
    one field. The ablation below reintroduces the drop four different ways and
    every one of them shipped a 200 with a field silently absent before this PR.

So the card's premise stands, with the triage comment's sharpening: the gap is
the next stamped or declared key, not writable.

The two doors do not share an invariant, and the gate does not pretend they do

REST doorruntime door
producergetMetaItems({type:'package'})registry.getAllPackages()
where writable comes fromstamped upstream, listed in the allowlistcomputed at the door, deliberately not in the allowlist
invariantthe allowlist contains every stamped keythe stamp runs after the projection

Asserting "the allowlist contains every stamped key" at the runtime door would
red on a correct door. So the runtime pin measures the stamp set as
served − record — never a list — and compares it for set equality against
an annotated register. A reorder empties that measured set and reds; a new stamp
grows it and reds until the decision is written down. That register is hand-kept
on purpose: swapping a silent list for a loud one is the mechanism this card
asked for.

Both gate readings

Four ablation legs, each mutating one production line, each restored and proven
restored. The mutation is proven on disk by an anchor count before/after, never
by the editor's exit code. Re-run in full after the merge; every number below
is identical before and after
, so the merge moved nothing.

legmutationgatereading
1drop 'writable' from REGISTRY_PACKAGE_RESPONSE_FIELDSRESTREDlist/detail PKG: writable, 6 drops across 3 packages
2drop 'installedVersion' from the same allowlistRESTREDlist/detail PKG: installedVersion, 6 drops
3reorder to toPackageResponse(withWritableVerdict(...))runtimeREDstamp set for com.example.showcase: expected [] to deeply equal [ 'writable' ]
4drop 'installedVersion' from INSTALLED_PACKAGE_RESPONSE_FIELDSruntimeREDlist/detail PKG: installedVersion, 6 drops
controltree restoredbothGREEN — REST 4/4, runtime 5/5

Leg 1 is the exact near-miss the card is about. Leg 3 is the exact regression the
runtime door's ordering comment warns about. Legs 2 and 4 are the generality
claim: a plain declared field, with no field-specific pin anywhere.

No rebuild leg is involved and none is owed. Both mutated files are resolved
by vitest as local source — the REST pin imports ./package-routes.js, the
runtime pin reaches ./domains/packages.js through ../http-dispatcher.js — so
dist/ is not on the resolution path for either subject and there is nothing to
preflight. The restore leg is proven by git diff HEAD being empty for both
doors after every leg, not by the restore command's exit code.

Two findings the ablation produced, both now fixed in this PR

Recording these because in both cases the first version of the gate was green
for a bad reason, and a reviewer should see that the readings above are not the
first ones taken.

  1. Object.keys was the wrong instrument.installPackage seats settings
    as an own property holding undefined. Both doors omit undefined-valued
    fields deliberately, and JSON.stringify drops them anyway, so the first run
    reported six false drops for a key no consumer could ever have observed. The
    detector now measures defined-valued keys.

  2. The fixture could not see two thirds of the record. Deleting
    installedVersion from either allowlist left both gates green on the
    first ablation: installPackage writes only the fields an install can know,
    so six of the twelve declared fields are simply absent from a freshly
    installed record. seatDeclaredFields now seats every field the record
    schema declares, and a control assertion fails if any declared slot becomes
    unobservable again.

On packages/spec

The card rules out deriving the production allowlist from packages/spec,
and this PR does not: both allowlists stay hand-written and are untouched. What
finding 2 derives from the schema is the test's expectation, which is the
card's own wording for the detector — "the projected key set ⊇ the producer's
stamped/declared key set minus an explicit, annotated exclusion list".
Deriving the expectation is precisely what turns a newly declared field into an
explicit decision at each door instead of a silent omission. It adds no import
edge either: @objectstack/spec is already a runtime dependency of both
packages. The rationale is spelled out at DECLARED_RECORD_KEYS in both files so
it cannot be mistaken for the rejected design.

Why two pins rather than one detector

Measured, not assumed. A single file would have to reach the other package's
door:

  • From packages/runtime, @objectstack/rest resolves to its dist/ (no
    vitest alias maps it to source), which would make the gate's verdict a function
    of build state. It would also growKNOWN_UNALIASED_TEST_IMPORTS in
    scripts/check-test-source-alias.mjs, a shrink-only registry that lists no
    @objectstack/rest entry for that package today.
  • @objectstack/rest cannot import @objectstack/runtime at all — the
    dependency runs the other way.

So: two pins, one shape. Each imports its own door as source, which is also
what lets a reviewer ablate one allowlist and watch exactly one gate go red.

What I did NOT do, and why

  • Did not change either allowlist. Both are correct today; the card asked for
    the detector, not an edit to the lists.
  • Did not touch packages/rest/src/error-response.ts (hot file, PR fix(rest): consult the bespoke structured arms before the declared-status passthrough, so both error doors answer one refusal with one body #14713).
  • Did not add a changeset. The diff is two test files and nothing else. No
    package publishes a test file, so nothing user-visible ships. skip-changeset
    applied.
  • Did not widen a CI filter root or add a CI job.
  • Did not add anything to packages/runtime/test-typecheck-debt.json, and did
    not need to — see Verification. That ledger is byte-identical to origin/main
    in this branch.
  • Did not silence anything to reach green. No any, @ts-expect-error, or
    eslint-disable was added anywhere, and no assertion was weakened. The
    load-bearing comparison — set equality on the stamp set — runs over
    Set(string) and string[], not any.

Verification

All readings from 47181632f2, the merge commit at the head of this branch.
The dependency closure was built first (pnpm --filter '@objectstack/runtime^...' build),
because an unbuilt closure turns every workspace import into TS2307 and makes a
typecheck verdict a statement about a different world.

The runtime test-layer typecheck, which the merge newly applies to this PR

pnpm --filter @objectstack/runtime typecheckEXIT=0. It now chains
check:test-typecheck, whose own verdict line is:

check:test-typecheck: OK — @objectstack/runtime's test layer compiles under
packages/runtime/tsconfig.test.json; 27 file(s) / 191 error(s) / 69 pinned
signature(s) held in test-typecheck-debt.json (shrink-only and identity-pinned)

Three separate confirmations that the new runtime pin is genuinely covered and
carries no debt:

  • tsc --noEmit --project tsconfig.test.json --listFiles shows 1 hit for
    src/domains/package-door-producer-key-carry.test.ts — it is in the program.
    (This replaces the pre-merge reading of 0 hits, which was true on the old base
    and is now false.)
  • Error lines naming that file in the same run: 0. The 191 errors are exactly
    the ledgered ones in the other 27 files.
  • packages/runtime/test-typecheck-debt.json is byte-identical to
    origin/main's copy — nothing added, no number moved.

Other green

  • packages/rest gate — 4/4; packages/runtime gate — 5/5.
  • Sibling door pins re-run beside them after the merge:
    package-registry-item-projection, package-list-writable-carry,
    packages-serializable-response, packages-writable-verdict — 3 files /
    14 tests and 3 files / 23 tests, both EXIT=0.
  • pnpm --filter @objectstack/rest typecheck — EXIT=0, with --listFiles
    confirming 1 hit for the new REST test file.
  • 25 of the 28 gate families derived by scripts/pm/dispatch-gates.mjs --commands
    — exit 0, re-derived and re-run on the merged head (the family list is
    unchanged, and the change set is still exactly these two files).
    check:engine-double-contract was RED early on (my fixture's findOne double)
    and is green after removing that verb, which the producer path never calls.

Not measured, with the reason

  • check:dual-build-cjs-loads and check:type-check-debt --re-measure — exit
    3, "PREREQUISITE NOT MET": both need the whole workspace built
    (turbo run build --filter='./packages/*' --filter='./packages/*/*'), not just
    one closure. Exit 3 is neither a pass nor a finding. The sub-measurement that
    this diff could actually have moved — the runtime test-layer debt — is answered
    directly above by check:test-typecheck at EXIT=0 with the ledger unchanged.
    And check:dual-build-cjs-loads reads built output, which a diff adding only
    *.test.ts files cannot reach.
  • check-test-completeness — exit 3; it needs a saved turbo run test log,
    and its own failure text says to record it NOT MEASURED when run locally.

Narrowed, declared

The two packages' FULL suites were not run. Nine attempts on the shared verify
lock across this branch never got a long enough turn — holders included one run
at 917s and another at 1704s — and @objectstack/runtime's whole suite exceeds
what fits inside the container's foreground cap on a shared box. Narrowed to the
two new files plus the four sibling pins on the same two doors, all green above.
The diff modifies no production file, no config, and no turbo/vitest setting, so
the only way a sibling test's verdict could move is cross-file interference
inside a vitest worker; CI runs the full sharded suite on this PR regardless.

Generated by Claude Code

…drop a stamped key
Both `/packages` doors project the installed-package record through a
hand-written field allowlist. The trade that bought was explicit: drift
shows up as a missing field, never a 500. Within one day of it landing,
an ADR-0070 D2 `writable` verdict started reaching both doors and both
would have dropped it — a 200 with the field simply absent. One door was
saved by someone reading a sibling pin, the other by a merge conflict.
The field-specific pins that followed cover `writable` and nothing else.
These two gates cover the general case, deriving both sides from real
code rather than a hand-kept key list:
- REST door: `served ⊇ getMetaItems({type:'package'}) keys`, measured
through the real `ObjectStackProtocolImplementation` over a real
`SchemaRegistry`.
- runtime door: `served ⊇ record keys`, plus a set-equality register
for the keys the door stamps AFTER the projection — measured as
`served − record`, so a reorder empties it and reds.
The two doors solved the near-miss differently (the REST allowlist
contains `writable`; the runtime one deliberately does not and orders
instead), so the invariants are stated separately rather than assumed
symmetric. Only hand-kept artifacts are the annotated exclusion and
stamp registers, both compared loudly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…esence
`SchemaRegistry.installPackage` seats the optional record fields as own
properties holding `undefined`, so `'settings' in record` is true for a
package installed without settings. Both doors omit undefined-valued
fields deliberately and JSON.stringify drops them anyway, so counting
them made the gate red on every package for a key no consumer could have
observed — measured on the first run, six false drops.
Also drops the fixture's `findOne` double: `getMetaItems` never reaches
that verb, and `check:engine-double-contract` is right that a fake looser
than ObjectQL.findOne is worth refusing rather than stubbing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
… all of them
The ablation caught this and it is the more useful of the two findings:
deleting `installedVersion` from EITHER door's allowlist left both gates
GREEN. `installPackage` leaves 8 of the 12 declared fields as own
properties holding `undefined`, and a key the wire cannot carry is
correctly invisible to the detector — so the coverage assertion was
exercising 4 fields while reading as if it covered the record.
`seatDeclaredFields` fills every own key whose value is `undefined`,
derived from the record's own key set rather than from a list of field
names, so a field added tomorrow is seated without an edit. A control
assertion now fails if any declared slot is unobservable again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…Schema
The first ablation's most useful reading: deleting `installedVersion`
from either allowlist left both gates GREEN. Six of the twelve declared
record fields are ABSENT from a freshly installed record (installPackage
writes only what an install can know), so a gate watching only the
producer's live output cannot see them dropped.
The card asks for `served ⊇ the producer's stamped/DECLARED key set`, so
the declared half is derived from the record schema — in the TEST's
expectation, never in the production allowlist, which stays hand-written
and untouched. That distinction is the one the originating card ruled on
and it is spelled out at DECLARED_RECORD_KEYS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@os-trumpos-trump added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/l labels Sep 3, 2026 — with Claude
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f116b8f8d1b43ec2bfd64fbf0ebb0cf3c301b1c8packageMentionDocs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key - #14830

Open
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate
Open

test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key#14830
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate

Conversation

@os-trump

@os-trumpos-trump commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14642

A gate that goes red when a /packages door's field allowlist drops a key the
producer stamps or the record declares. Both sides of the comparison are derived
by running real code; the only hand-kept artifacts are two annotated registers,
both compared loudly.

Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

File face

Two files, both new, both tests:

  • packages/rest/src/package-door-producer-key-carry.test.ts
  • packages/runtime/src/domains/package-door-producer-key-carry.test.ts

No production file is modified. In particular packages/rest/src/error-response.ts
is not touched — PR #14713 is in flight on it and the gate's honest landing
site was never that file. No CI job added, no CI filter root widened.

Base refreshed by mergingorigin/main (not rebasing, no force-push) at
47181632f2. That merge matters: PR #14785 landed packages/runtime/tsconfig.test.json
while this branch was open, so the runtime pin is now compiled by a real tsc
program. See Verification.

Is the near-miss still reachable on today's main?

  • For writable itself: no. Both doors carry it now, by two different
    mechanisms, and both are pinned by name (package-list-writable-carry.test.ts,
    packages-serializable-response.test.ts).
  • For the general case: yes, fully reachable. Nothing generalised past that
    one field. The ablation below reintroduces the drop four different ways and
    every one of them shipped a 200 with a field silently absent before this PR.

So the card's premise stands, with the triage comment's sharpening: the gap is
the next stamped or declared key, not writable.

The two doors do not share an invariant, and the gate does not pretend they do

REST doorruntime door
producergetMetaItems({type:'package'})registry.getAllPackages()
where writable comes fromstamped upstream, listed in the allowlistcomputed at the door, deliberately not in the allowlist
invariantthe allowlist contains every stamped keythe stamp runs after the projection

Asserting "the allowlist contains every stamped key" at the runtime door would
red on a correct door. So the runtime pin measures the stamp set as
served − record — never a list — and compares it for set equality against
an annotated register. A reorder empties that measured set and reds; a new stamp
grows it and reds until the decision is written down. That register is hand-kept
on purpose: swapping a silent list for a loud one is the mechanism this card
asked for.

Both gate readings

Four ablation legs, each mutating one production line, each restored and proven
restored. The mutation is proven on disk by an anchor count before/after, never
by the editor's exit code. Re-run in full after the merge; every number below
is identical before and after
, so the merge moved nothing.

legmutationgatereading
1drop 'writable' from REGISTRY_PACKAGE_RESPONSE_FIELDSRESTREDlist/detail PKG: writable, 6 drops across 3 packages
2drop 'installedVersion' from the same allowlistRESTREDlist/detail PKG: installedVersion, 6 drops
3reorder to toPackageResponse(withWritableVerdict(...))runtimeREDstamp set for com.example.showcase: expected [] to deeply equal [ 'writable' ]
4drop 'installedVersion' from INSTALLED_PACKAGE_RESPONSE_FIELDSruntimeREDlist/detail PKG: installedVersion, 6 drops
controltree restoredbothGREEN — REST 4/4, runtime 5/5

Leg 1 is the exact near-miss the card is about. Leg 3 is the exact regression the
runtime door's ordering comment warns about. Legs 2 and 4 are the generality
claim: a plain declared field, with no field-specific pin anywhere.

No rebuild leg is involved and none is owed. Both mutated files are resolved
by vitest as local source — the REST pin imports ./package-routes.js, the
runtime pin reaches ./domains/packages.js through ../http-dispatcher.js — so
dist/ is not on the resolution path for either subject and there is nothing to
preflight. The restore leg is proven by git diff HEAD being empty for both
doors after every leg, not by the restore command's exit code.

Two findings the ablation produced, both now fixed in this PR

Recording these because in both cases the first version of the gate was green
for a bad reason, and a reviewer should see that the readings above are not the
first ones taken.

  1. Object.keys was the wrong instrument.installPackage seats settings
    as an own property holding undefined. Both doors omit undefined-valued
    fields deliberately, and JSON.stringify drops them anyway, so the first run
    reported six false drops for a key no consumer could ever have observed. The
    detector now measures defined-valued keys.

  2. The fixture could not see two thirds of the record. Deleting
    installedVersion from either allowlist left both gates green on the
    first ablation: installPackage writes only the fields an install can know,
    so six of the twelve declared fields are simply absent from a freshly
    installed record. seatDeclaredFields now seats every field the record
    schema declares, and a control assertion fails if any declared slot becomes
    unobservable again.

On packages/spec

The card rules out deriving the production allowlist from packages/spec,
and this PR does not: both allowlists stay hand-written and are untouched. What
finding 2 derives from the schema is the test's expectation, which is the
card's own wording for the detector — "the projected key set ⊇ the producer's
stamped/declared key set minus an explicit, annotated exclusion list".
Deriving the expectation is precisely what turns a newly declared field into an
explicit decision at each door instead of a silent omission. It adds no import
edge either: @objectstack/spec is already a runtime dependency of both
packages. The rationale is spelled out at DECLARED_RECORD_KEYS in both files so
it cannot be mistaken for the rejected design.

Why two pins rather than one detector

Measured, not assumed. A single file would have to reach the other package's
door:

  • From packages/runtime, @objectstack/rest resolves to its dist/ (no
    vitest alias maps it to source), which would make the gate's verdict a function
    of build state. It would also growKNOWN_UNALIASED_TEST_IMPORTS in
    scripts/check-test-source-alias.mjs, a shrink-only registry that lists no
    @objectstack/rest entry for that package today.
  • @objectstack/rest cannot import @objectstack/runtime at all — the
    dependency runs the other way.

So: two pins, one shape. Each imports its own door as source, which is also
what lets a reviewer ablate one allowlist and watch exactly one gate go red.

What I did NOT do, and why

  • Did not change either allowlist. Both are correct today; the card asked for
    the detector, not an edit to the lists.
  • Did not touch packages/rest/src/error-response.ts (hot file, PR fix(rest): consult the bespoke structured arms before the declared-status passthrough, so both error doors answer one refusal with one body #14713).
  • Did not add a changeset. The diff is two test files and nothing else. No
    package publishes a test file, so nothing user-visible ships. skip-changeset
    applied.
  • Did not widen a CI filter root or add a CI job.
  • Did not add anything to packages/runtime/test-typecheck-debt.json, and did
    not need to — see Verification. That ledger is byte-identical to origin/main
    in this branch.
  • Did not silence anything to reach green. No any, @ts-expect-error, or
    eslint-disable was added anywhere, and no assertion was weakened. The
    load-bearing comparison — set equality on the stamp set — runs over
    Set(string) and string[], not any.

Verification

All readings from 47181632f2, the merge commit at the head of this branch.
The dependency closure was built first (pnpm --filter '@objectstack/runtime^...' build),
because an unbuilt closure turns every workspace import into TS2307 and makes a
typecheck verdict a statement about a different world.

The runtime test-layer typecheck, which the merge newly applies to this PR

pnpm --filter @objectstack/runtime typecheckEXIT=0. It now chains
check:test-typecheck, whose own verdict line is:

check:test-typecheck: OK — @objectstack/runtime's test layer compiles under
packages/runtime/tsconfig.test.json; 27 file(s) / 191 error(s) / 69 pinned
signature(s) held in test-typecheck-debt.json (shrink-only and identity-pinned)

Three separate confirmations that the new runtime pin is genuinely covered and
carries no debt:

  • tsc --noEmit --project tsconfig.test.json --listFiles shows 1 hit for
    src/domains/package-door-producer-key-carry.test.ts — it is in the program.
    (This replaces the pre-merge reading of 0 hits, which was true on the old base
    and is now false.)
  • Error lines naming that file in the same run: 0. The 191 errors are exactly
    the ledgered ones in the other 27 files.
  • packages/runtime/test-typecheck-debt.json is byte-identical to
    origin/main's copy — nothing added, no number moved.

Other green

  • packages/rest gate — 4/4; packages/runtime gate — 5/5.
  • Sibling door pins re-run beside them after the merge:
    package-registry-item-projection, package-list-writable-carry,
    packages-serializable-response, packages-writable-verdict — 3 files /
    14 tests and 3 files / 23 tests, both EXIT=0.
  • pnpm --filter @objectstack/rest typecheck — EXIT=0, with --listFiles
    confirming 1 hit for the new REST test file.
  • 25 of the 28 gate families derived by scripts/pm/dispatch-gates.mjs --commands
    — exit 0, re-derived and re-run on the merged head (the family list is
    unchanged, and the change set is still exactly these two files).
    check:engine-double-contract was RED early on (my fixture's findOne double)
    and is green after removing that verb, which the producer path never calls.

Not measured, with the reason

  • check:dual-build-cjs-loads and check:type-check-debt --re-measure — exit
    3, "PREREQUISITE NOT MET": both need the whole workspace built
    (turbo run build --filter='./packages/*' --filter='./packages/*/*'), not just
    one closure. Exit 3 is neither a pass nor a finding. The sub-measurement that
    this diff could actually have moved — the runtime test-layer debt — is answered
    directly above by check:test-typecheck at EXIT=0 with the ledger unchanged.
    And check:dual-build-cjs-loads reads built output, which a diff adding only
    *.test.ts files cannot reach.
  • check-test-completeness — exit 3; it needs a saved turbo run test log,
    and its own failure text says to record it NOT MEASURED when run locally.

Narrowed, declared

The two packages' FULL suites were not run. Nine attempts on the shared verify
lock across this branch never got a long enough turn — holders included one run
at 917s and another at 1704s — and @objectstack/runtime's whole suite exceeds
what fits inside the container's foreground cap on a shared box. Narrowed to the
two new files plus the four sibling pins on the same two doors, all green above.
The diff modifies no production file, no config, and no turbo/vitest setting, so
the only way a sibling test's verdict could move is cross-file interference
inside a vitest worker; CI runs the full sharded suite on this PR regardless.

Generated by Claude Code

…drop a stamped key
Both `/packages` doors project the installed-package record through a
hand-written field allowlist. The trade that bought was explicit: drift
shows up as a missing field, never a 500. Within one day of it landing,
an ADR-0070 D2 `writable` verdict started reaching both doors and both
would have dropped it — a 200 with the field simply absent. One door was
saved by someone reading a sibling pin, the other by a merge conflict.
The field-specific pins that followed cover `writable` and nothing else.
These two gates cover the general case, deriving both sides from real
code rather than a hand-kept key list:
- REST door: `served ⊇ getMetaItems({type:'package'}) keys`, measured
through the real `ObjectStackProtocolImplementation` over a real
`SchemaRegistry`.
- runtime door: `served ⊇ record keys`, plus a set-equality register
for the keys the door stamps AFTER the projection — measured as
`served − record`, so a reorder empties it and reds.
The two doors solved the near-miss differently (the REST allowlist
contains `writable`; the runtime one deliberately does not and orders
instead), so the invariants are stated separately rather than assumed
symmetric. Only hand-kept artifacts are the annotated exclusion and
stamp registers, both compared loudly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…esence
`SchemaRegistry.installPackage` seats the optional record fields as own
properties holding `undefined`, so `'settings' in record` is true for a
package installed without settings. Both doors omit undefined-valued
fields deliberately and JSON.stringify drops them anyway, so counting
them made the gate red on every package for a key no consumer could have
observed — measured on the first run, six false drops.
Also drops the fixture's `findOne` double: `getMetaItems` never reaches
that verb, and `check:engine-double-contract` is right that a fake looser
than ObjectQL.findOne is worth refusing rather than stubbing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
… all of them
The ablation caught this and it is the more useful of the two findings:
deleting `installedVersion` from EITHER door's allowlist left both gates
GREEN. `installPackage` leaves 8 of the 12 declared fields as own
properties holding `undefined`, and a key the wire cannot carry is
correctly invisible to the detector — so the coverage assertion was
exercising 4 fields while reading as if it covered the record.
`seatDeclaredFields` fills every own key whose value is `undefined`,
derived from the record's own key set rather than from a list of field
names, so a field added tomorrow is seated without an edit. A control
assertion now fails if any declared slot is unobservable again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…Schema
The first ablation's most useful reading: deleting `installedVersion`
from either allowlist left both gates GREEN. Six of the twelve declared
record fields are ABSENT from a freshly installed record (installPackage
writes only what an install can know), so a gate watching only the
producer's live output cannot see them dropped.
The card asks for `served ⊇ the producer's stamped/DECLARED key set`, so
the declared half is derived from the record schema — in the TEST's
expectation, never in the production allowlist, which stays hand-written
and untouched. That distinction is the one the originating card ruled on
and it is spelled out at DECLARED_RECORD_KEYS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@os-trumpos-trump added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/l labels Sep 3, 2026 — with Claude
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f116b8f8d1b43ec2bfd64fbf0ebb0cf3c301b1c8packageMentionDocs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key - #14830

Open
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate
Open

test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key#14830
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate

Conversation

@os-trump

@os-trumpos-trump commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14642

A gate that goes red when a /packages door's field allowlist drops a key the
producer stamps or the record declares. Both sides of the comparison are derived
by running real code; the only hand-kept artifacts are two annotated registers,
both compared loudly.

Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

File face

Two files, both new, both tests:

  • packages/rest/src/package-door-producer-key-carry.test.ts
  • packages/runtime/src/domains/package-door-producer-key-carry.test.ts

No production file is modified. In particular packages/rest/src/error-response.ts
is not touched — PR #14713 is in flight on it and the gate's honest landing
site was never that file. No CI job added, no CI filter root widened.

Base refreshed by mergingorigin/main (not rebasing, no force-push) at
47181632f2. That merge matters: PR #14785 landed packages/runtime/tsconfig.test.json
while this branch was open, so the runtime pin is now compiled by a real tsc
program. See Verification.

Is the near-miss still reachable on today's main?

  • For writable itself: no. Both doors carry it now, by two different
    mechanisms, and both are pinned by name (package-list-writable-carry.test.ts,
    packages-serializable-response.test.ts).
  • For the general case: yes, fully reachable. Nothing generalised past that
    one field. The ablation below reintroduces the drop four different ways and
    every one of them shipped a 200 with a field silently absent before this PR.

So the card's premise stands, with the triage comment's sharpening: the gap is
the next stamped or declared key, not writable.

The two doors do not share an invariant, and the gate does not pretend they do

REST doorruntime door
producergetMetaItems({type:'package'})registry.getAllPackages()
where writable comes fromstamped upstream, listed in the allowlistcomputed at the door, deliberately not in the allowlist
invariantthe allowlist contains every stamped keythe stamp runs after the projection

Asserting "the allowlist contains every stamped key" at the runtime door would
red on a correct door. So the runtime pin measures the stamp set as
served − record — never a list — and compares it for set equality against
an annotated register. A reorder empties that measured set and reds; a new stamp
grows it and reds until the decision is written down. That register is hand-kept
on purpose: swapping a silent list for a loud one is the mechanism this card
asked for.

Both gate readings

Four ablation legs, each mutating one production line, each restored and proven
restored. The mutation is proven on disk by an anchor count before/after, never
by the editor's exit code. Re-run in full after the merge; every number below
is identical before and after
, so the merge moved nothing.

legmutationgatereading
1drop 'writable' from REGISTRY_PACKAGE_RESPONSE_FIELDSRESTREDlist/detail PKG: writable, 6 drops across 3 packages
2drop 'installedVersion' from the same allowlistRESTREDlist/detail PKG: installedVersion, 6 drops
3reorder to toPackageResponse(withWritableVerdict(...))runtimeREDstamp set for com.example.showcase: expected [] to deeply equal [ 'writable' ]
4drop 'installedVersion' from INSTALLED_PACKAGE_RESPONSE_FIELDSruntimeREDlist/detail PKG: installedVersion, 6 drops
controltree restoredbothGREEN — REST 4/4, runtime 5/5

Leg 1 is the exact near-miss the card is about. Leg 3 is the exact regression the
runtime door's ordering comment warns about. Legs 2 and 4 are the generality
claim: a plain declared field, with no field-specific pin anywhere.

No rebuild leg is involved and none is owed. Both mutated files are resolved
by vitest as local source — the REST pin imports ./package-routes.js, the
runtime pin reaches ./domains/packages.js through ../http-dispatcher.js — so
dist/ is not on the resolution path for either subject and there is nothing to
preflight. The restore leg is proven by git diff HEAD being empty for both
doors after every leg, not by the restore command's exit code.

Two findings the ablation produced, both now fixed in this PR

Recording these because in both cases the first version of the gate was green
for a bad reason, and a reviewer should see that the readings above are not the
first ones taken.

  1. Object.keys was the wrong instrument.installPackage seats settings
    as an own property holding undefined. Both doors omit undefined-valued
    fields deliberately, and JSON.stringify drops them anyway, so the first run
    reported six false drops for a key no consumer could ever have observed. The
    detector now measures defined-valued keys.

  2. The fixture could not see two thirds of the record. Deleting
    installedVersion from either allowlist left both gates green on the
    first ablation: installPackage writes only the fields an install can know,
    so six of the twelve declared fields are simply absent from a freshly
    installed record. seatDeclaredFields now seats every field the record
    schema declares, and a control assertion fails if any declared slot becomes
    unobservable again.

On packages/spec

The card rules out deriving the production allowlist from packages/spec,
and this PR does not: both allowlists stay hand-written and are untouched. What
finding 2 derives from the schema is the test's expectation, which is the
card's own wording for the detector — "the projected key set ⊇ the producer's
stamped/declared key set minus an explicit, annotated exclusion list".
Deriving the expectation is precisely what turns a newly declared field into an
explicit decision at each door instead of a silent omission. It adds no import
edge either: @objectstack/spec is already a runtime dependency of both
packages. The rationale is spelled out at DECLARED_RECORD_KEYS in both files so
it cannot be mistaken for the rejected design.

Why two pins rather than one detector

Measured, not assumed. A single file would have to reach the other package's
door:

  • From packages/runtime, @objectstack/rest resolves to its dist/ (no
    vitest alias maps it to source), which would make the gate's verdict a function
    of build state. It would also growKNOWN_UNALIASED_TEST_IMPORTS in
    scripts/check-test-source-alias.mjs, a shrink-only registry that lists no
    @objectstack/rest entry for that package today.
  • @objectstack/rest cannot import @objectstack/runtime at all — the
    dependency runs the other way.

So: two pins, one shape. Each imports its own door as source, which is also
what lets a reviewer ablate one allowlist and watch exactly one gate go red.

What I did NOT do, and why

  • Did not change either allowlist. Both are correct today; the card asked for
    the detector, not an edit to the lists.
  • Did not touch packages/rest/src/error-response.ts (hot file, PR fix(rest): consult the bespoke structured arms before the declared-status passthrough, so both error doors answer one refusal with one body #14713).
  • Did not add a changeset. The diff is two test files and nothing else. No
    package publishes a test file, so nothing user-visible ships. skip-changeset
    applied.
  • Did not widen a CI filter root or add a CI job.
  • Did not add anything to packages/runtime/test-typecheck-debt.json, and did
    not need to — see Verification. That ledger is byte-identical to origin/main
    in this branch.
  • Did not silence anything to reach green. No any, @ts-expect-error, or
    eslint-disable was added anywhere, and no assertion was weakened. The
    load-bearing comparison — set equality on the stamp set — runs over
    Set(string) and string[], not any.

Verification

All readings from 47181632f2, the merge commit at the head of this branch.
The dependency closure was built first (pnpm --filter '@objectstack/runtime^...' build),
because an unbuilt closure turns every workspace import into TS2307 and makes a
typecheck verdict a statement about a different world.

The runtime test-layer typecheck, which the merge newly applies to this PR

pnpm --filter @objectstack/runtime typecheckEXIT=0. It now chains
check:test-typecheck, whose own verdict line is:

check:test-typecheck: OK — @objectstack/runtime's test layer compiles under
packages/runtime/tsconfig.test.json; 27 file(s) / 191 error(s) / 69 pinned
signature(s) held in test-typecheck-debt.json (shrink-only and identity-pinned)

Three separate confirmations that the new runtime pin is genuinely covered and
carries no debt:

  • tsc --noEmit --project tsconfig.test.json --listFiles shows 1 hit for
    src/domains/package-door-producer-key-carry.test.ts — it is in the program.
    (This replaces the pre-merge reading of 0 hits, which was true on the old base
    and is now false.)
  • Error lines naming that file in the same run: 0. The 191 errors are exactly
    the ledgered ones in the other 27 files.
  • packages/runtime/test-typecheck-debt.json is byte-identical to
    origin/main's copy — nothing added, no number moved.

Other green

  • packages/rest gate — 4/4; packages/runtime gate — 5/5.
  • Sibling door pins re-run beside them after the merge:
    package-registry-item-projection, package-list-writable-carry,
    packages-serializable-response, packages-writable-verdict — 3 files /
    14 tests and 3 files / 23 tests, both EXIT=0.
  • pnpm --filter @objectstack/rest typecheck — EXIT=0, with --listFiles
    confirming 1 hit for the new REST test file.
  • 25 of the 28 gate families derived by scripts/pm/dispatch-gates.mjs --commands
    — exit 0, re-derived and re-run on the merged head (the family list is
    unchanged, and the change set is still exactly these two files).
    check:engine-double-contract was RED early on (my fixture's findOne double)
    and is green after removing that verb, which the producer path never calls.

Not measured, with the reason

  • check:dual-build-cjs-loads and check:type-check-debt --re-measure — exit
    3, "PREREQUISITE NOT MET": both need the whole workspace built
    (turbo run build --filter='./packages/*' --filter='./packages/*/*'), not just
    one closure. Exit 3 is neither a pass nor a finding. The sub-measurement that
    this diff could actually have moved — the runtime test-layer debt — is answered
    directly above by check:test-typecheck at EXIT=0 with the ledger unchanged.
    And check:dual-build-cjs-loads reads built output, which a diff adding only
    *.test.ts files cannot reach.
  • check-test-completeness — exit 3; it needs a saved turbo run test log,
    and its own failure text says to record it NOT MEASURED when run locally.

Narrowed, declared

The two packages' FULL suites were not run. Nine attempts on the shared verify
lock across this branch never got a long enough turn — holders included one run
at 917s and another at 1704s — and @objectstack/runtime's whole suite exceeds
what fits inside the container's foreground cap on a shared box. Narrowed to the
two new files plus the four sibling pins on the same two doors, all green above.
The diff modifies no production file, no config, and no turbo/vitest setting, so
the only way a sibling test's verdict could move is cross-file interference
inside a vitest worker; CI runs the full sharded suite on this PR regardless.

Generated by Claude Code

…drop a stamped key
Both `/packages` doors project the installed-package record through a
hand-written field allowlist. The trade that bought was explicit: drift
shows up as a missing field, never a 500. Within one day of it landing,
an ADR-0070 D2 `writable` verdict started reaching both doors and both
would have dropped it — a 200 with the field simply absent. One door was
saved by someone reading a sibling pin, the other by a merge conflict.
The field-specific pins that followed cover `writable` and nothing else.
These two gates cover the general case, deriving both sides from real
code rather than a hand-kept key list:
- REST door: `served ⊇ getMetaItems({type:'package'}) keys`, measured
through the real `ObjectStackProtocolImplementation` over a real
`SchemaRegistry`.
- runtime door: `served ⊇ record keys`, plus a set-equality register
for the keys the door stamps AFTER the projection — measured as
`served − record`, so a reorder empties it and reds.
The two doors solved the near-miss differently (the REST allowlist
contains `writable`; the runtime one deliberately does not and orders
instead), so the invariants are stated separately rather than assumed
symmetric. Only hand-kept artifacts are the annotated exclusion and
stamp registers, both compared loudly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…esence
`SchemaRegistry.installPackage` seats the optional record fields as own
properties holding `undefined`, so `'settings' in record` is true for a
package installed without settings. Both doors omit undefined-valued
fields deliberately and JSON.stringify drops them anyway, so counting
them made the gate red on every package for a key no consumer could have
observed — measured on the first run, six false drops.
Also drops the fixture's `findOne` double: `getMetaItems` never reaches
that verb, and `check:engine-double-contract` is right that a fake looser
than ObjectQL.findOne is worth refusing rather than stubbing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
… all of them
The ablation caught this and it is the more useful of the two findings:
deleting `installedVersion` from EITHER door's allowlist left both gates
GREEN. `installPackage` leaves 8 of the 12 declared fields as own
properties holding `undefined`, and a key the wire cannot carry is
correctly invisible to the detector — so the coverage assertion was
exercising 4 fields while reading as if it covered the record.
`seatDeclaredFields` fills every own key whose value is `undefined`,
derived from the record's own key set rather than from a list of field
names, so a field added tomorrow is seated without an edit. A control
assertion now fails if any declared slot is unobservable again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…Schema
The first ablation's most useful reading: deleting `installedVersion`
from either allowlist left both gates GREEN. Six of the twelve declared
record fields are ABSENT from a freshly installed record (installPackage
writes only what an install can know), so a gate watching only the
producer's live output cannot see them dropped.
The card asks for `served ⊇ the producer's stamped/DECLARED key set`, so
the declared half is derived from the record schema — in the TEST's
expectation, never in the production allowlist, which stays hand-written
and untouched. That distinction is the one the originating card ruled on
and it is spelled out at DECLARED_RECORD_KEYS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@os-trumpos-trump added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/l labels Sep 3, 2026 — with Claude
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f116b8f8d1b43ec2bfd64fbf0ebb0cf3c301b1c8packageMentionDocs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key - #14830

Open
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate
Open

test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key#14830
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate

Conversation

@os-trump

@os-trumpos-trump commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14642

A gate that goes red when a /packages door's field allowlist drops a key the
producer stamps or the record declares. Both sides of the comparison are derived
by running real code; the only hand-kept artifacts are two annotated registers,
both compared loudly.

Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

File face

Two files, both new, both tests:

  • packages/rest/src/package-door-producer-key-carry.test.ts
  • packages/runtime/src/domains/package-door-producer-key-carry.test.ts

No production file is modified. In particular packages/rest/src/error-response.ts
is not touched — PR #14713 is in flight on it and the gate's honest landing
site was never that file. No CI job added, no CI filter root widened.

Base refreshed by mergingorigin/main (not rebasing, no force-push) at
47181632f2. That merge matters: PR #14785 landed packages/runtime/tsconfig.test.json
while this branch was open, so the runtime pin is now compiled by a real tsc
program. See Verification.

Is the near-miss still reachable on today's main?

  • For writable itself: no. Both doors carry it now, by two different
    mechanisms, and both are pinned by name (package-list-writable-carry.test.ts,
    packages-serializable-response.test.ts).
  • For the general case: yes, fully reachable. Nothing generalised past that
    one field. The ablation below reintroduces the drop four different ways and
    every one of them shipped a 200 with a field silently absent before this PR.

So the card's premise stands, with the triage comment's sharpening: the gap is
the next stamped or declared key, not writable.

The two doors do not share an invariant, and the gate does not pretend they do

REST doorruntime door
producergetMetaItems({type:'package'})registry.getAllPackages()
where writable comes fromstamped upstream, listed in the allowlistcomputed at the door, deliberately not in the allowlist
invariantthe allowlist contains every stamped keythe stamp runs after the projection

Asserting "the allowlist contains every stamped key" at the runtime door would
red on a correct door. So the runtime pin measures the stamp set as
served − record — never a list — and compares it for set equality against
an annotated register. A reorder empties that measured set and reds; a new stamp
grows it and reds until the decision is written down. That register is hand-kept
on purpose: swapping a silent list for a loud one is the mechanism this card
asked for.

Both gate readings

Four ablation legs, each mutating one production line, each restored and proven
restored. The mutation is proven on disk by an anchor count before/after, never
by the editor's exit code. Re-run in full after the merge; every number below
is identical before and after
, so the merge moved nothing.

legmutationgatereading
1drop 'writable' from REGISTRY_PACKAGE_RESPONSE_FIELDSRESTREDlist/detail PKG: writable, 6 drops across 3 packages
2drop 'installedVersion' from the same allowlistRESTREDlist/detail PKG: installedVersion, 6 drops
3reorder to toPackageResponse(withWritableVerdict(...))runtimeREDstamp set for com.example.showcase: expected [] to deeply equal [ 'writable' ]
4drop 'installedVersion' from INSTALLED_PACKAGE_RESPONSE_FIELDSruntimeREDlist/detail PKG: installedVersion, 6 drops
controltree restoredbothGREEN — REST 4/4, runtime 5/5

Leg 1 is the exact near-miss the card is about. Leg 3 is the exact regression the
runtime door's ordering comment warns about. Legs 2 and 4 are the generality
claim: a plain declared field, with no field-specific pin anywhere.

No rebuild leg is involved and none is owed. Both mutated files are resolved
by vitest as local source — the REST pin imports ./package-routes.js, the
runtime pin reaches ./domains/packages.js through ../http-dispatcher.js — so
dist/ is not on the resolution path for either subject and there is nothing to
preflight. The restore leg is proven by git diff HEAD being empty for both
doors after every leg, not by the restore command's exit code.

Two findings the ablation produced, both now fixed in this PR

Recording these because in both cases the first version of the gate was green
for a bad reason, and a reviewer should see that the readings above are not the
first ones taken.

  1. Object.keys was the wrong instrument.installPackage seats settings
    as an own property holding undefined. Both doors omit undefined-valued
    fields deliberately, and JSON.stringify drops them anyway, so the first run
    reported six false drops for a key no consumer could ever have observed. The
    detector now measures defined-valued keys.

  2. The fixture could not see two thirds of the record. Deleting
    installedVersion from either allowlist left both gates green on the
    first ablation: installPackage writes only the fields an install can know,
    so six of the twelve declared fields are simply absent from a freshly
    installed record. seatDeclaredFields now seats every field the record
    schema declares, and a control assertion fails if any declared slot becomes
    unobservable again.

On packages/spec

The card rules out deriving the production allowlist from packages/spec,
and this PR does not: both allowlists stay hand-written and are untouched. What
finding 2 derives from the schema is the test's expectation, which is the
card's own wording for the detector — "the projected key set ⊇ the producer's
stamped/declared key set minus an explicit, annotated exclusion list".
Deriving the expectation is precisely what turns a newly declared field into an
explicit decision at each door instead of a silent omission. It adds no import
edge either: @objectstack/spec is already a runtime dependency of both
packages. The rationale is spelled out at DECLARED_RECORD_KEYS in both files so
it cannot be mistaken for the rejected design.

Why two pins rather than one detector

Measured, not assumed. A single file would have to reach the other package's
door:

  • From packages/runtime, @objectstack/rest resolves to its dist/ (no
    vitest alias maps it to source), which would make the gate's verdict a function
    of build state. It would also growKNOWN_UNALIASED_TEST_IMPORTS in
    scripts/check-test-source-alias.mjs, a shrink-only registry that lists no
    @objectstack/rest entry for that package today.
  • @objectstack/rest cannot import @objectstack/runtime at all — the
    dependency runs the other way.

So: two pins, one shape. Each imports its own door as source, which is also
what lets a reviewer ablate one allowlist and watch exactly one gate go red.

What I did NOT do, and why

  • Did not change either allowlist. Both are correct today; the card asked for
    the detector, not an edit to the lists.
  • Did not touch packages/rest/src/error-response.ts (hot file, PR fix(rest): consult the bespoke structured arms before the declared-status passthrough, so both error doors answer one refusal with one body #14713).
  • Did not add a changeset. The diff is two test files and nothing else. No
    package publishes a test file, so nothing user-visible ships. skip-changeset
    applied.
  • Did not widen a CI filter root or add a CI job.
  • Did not add anything to packages/runtime/test-typecheck-debt.json, and did
    not need to — see Verification. That ledger is byte-identical to origin/main
    in this branch.
  • Did not silence anything to reach green. No any, @ts-expect-error, or
    eslint-disable was added anywhere, and no assertion was weakened. The
    load-bearing comparison — set equality on the stamp set — runs over
    Set(string) and string[], not any.

Verification

All readings from 47181632f2, the merge commit at the head of this branch.
The dependency closure was built first (pnpm --filter '@objectstack/runtime^...' build),
because an unbuilt closure turns every workspace import into TS2307 and makes a
typecheck verdict a statement about a different world.

The runtime test-layer typecheck, which the merge newly applies to this PR

pnpm --filter @objectstack/runtime typecheckEXIT=0. It now chains
check:test-typecheck, whose own verdict line is:

check:test-typecheck: OK — @objectstack/runtime's test layer compiles under
packages/runtime/tsconfig.test.json; 27 file(s) / 191 error(s) / 69 pinned
signature(s) held in test-typecheck-debt.json (shrink-only and identity-pinned)

Three separate confirmations that the new runtime pin is genuinely covered and
carries no debt:

  • tsc --noEmit --project tsconfig.test.json --listFiles shows 1 hit for
    src/domains/package-door-producer-key-carry.test.ts — it is in the program.
    (This replaces the pre-merge reading of 0 hits, which was true on the old base
    and is now false.)
  • Error lines naming that file in the same run: 0. The 191 errors are exactly
    the ledgered ones in the other 27 files.
  • packages/runtime/test-typecheck-debt.json is byte-identical to
    origin/main's copy — nothing added, no number moved.

Other green

  • packages/rest gate — 4/4; packages/runtime gate — 5/5.
  • Sibling door pins re-run beside them after the merge:
    package-registry-item-projection, package-list-writable-carry,
    packages-serializable-response, packages-writable-verdict — 3 files /
    14 tests and 3 files / 23 tests, both EXIT=0.
  • pnpm --filter @objectstack/rest typecheck — EXIT=0, with --listFiles
    confirming 1 hit for the new REST test file.
  • 25 of the 28 gate families derived by scripts/pm/dispatch-gates.mjs --commands
    — exit 0, re-derived and re-run on the merged head (the family list is
    unchanged, and the change set is still exactly these two files).
    check:engine-double-contract was RED early on (my fixture's findOne double)
    and is green after removing that verb, which the producer path never calls.

Not measured, with the reason

  • check:dual-build-cjs-loads and check:type-check-debt --re-measure — exit
    3, "PREREQUISITE NOT MET": both need the whole workspace built
    (turbo run build --filter='./packages/*' --filter='./packages/*/*'), not just
    one closure. Exit 3 is neither a pass nor a finding. The sub-measurement that
    this diff could actually have moved — the runtime test-layer debt — is answered
    directly above by check:test-typecheck at EXIT=0 with the ledger unchanged.
    And check:dual-build-cjs-loads reads built output, which a diff adding only
    *.test.ts files cannot reach.
  • check-test-completeness — exit 3; it needs a saved turbo run test log,
    and its own failure text says to record it NOT MEASURED when run locally.

Narrowed, declared

The two packages' FULL suites were not run. Nine attempts on the shared verify
lock across this branch never got a long enough turn — holders included one run
at 917s and another at 1704s — and @objectstack/runtime's whole suite exceeds
what fits inside the container's foreground cap on a shared box. Narrowed to the
two new files plus the four sibling pins on the same two doors, all green above.
The diff modifies no production file, no config, and no turbo/vitest setting, so
the only way a sibling test's verdict could move is cross-file interference
inside a vitest worker; CI runs the full sharded suite on this PR regardless.

Generated by Claude Code

…drop a stamped key
Both `/packages` doors project the installed-package record through a
hand-written field allowlist. The trade that bought was explicit: drift
shows up as a missing field, never a 500. Within one day of it landing,
an ADR-0070 D2 `writable` verdict started reaching both doors and both
would have dropped it — a 200 with the field simply absent. One door was
saved by someone reading a sibling pin, the other by a merge conflict.
The field-specific pins that followed cover `writable` and nothing else.
These two gates cover the general case, deriving both sides from real
code rather than a hand-kept key list:
- REST door: `served ⊇ getMetaItems({type:'package'}) keys`, measured
through the real `ObjectStackProtocolImplementation` over a real
`SchemaRegistry`.
- runtime door: `served ⊇ record keys`, plus a set-equality register
for the keys the door stamps AFTER the projection — measured as
`served − record`, so a reorder empties it and reds.
The two doors solved the near-miss differently (the REST allowlist
contains `writable`; the runtime one deliberately does not and orders
instead), so the invariants are stated separately rather than assumed
symmetric. Only hand-kept artifacts are the annotated exclusion and
stamp registers, both compared loudly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…esence
`SchemaRegistry.installPackage` seats the optional record fields as own
properties holding `undefined`, so `'settings' in record` is true for a
package installed without settings. Both doors omit undefined-valued
fields deliberately and JSON.stringify drops them anyway, so counting
them made the gate red on every package for a key no consumer could have
observed — measured on the first run, six false drops.
Also drops the fixture's `findOne` double: `getMetaItems` never reaches
that verb, and `check:engine-double-contract` is right that a fake looser
than ObjectQL.findOne is worth refusing rather than stubbing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
… all of them
The ablation caught this and it is the more useful of the two findings:
deleting `installedVersion` from EITHER door's allowlist left both gates
GREEN. `installPackage` leaves 8 of the 12 declared fields as own
properties holding `undefined`, and a key the wire cannot carry is
correctly invisible to the detector — so the coverage assertion was
exercising 4 fields while reading as if it covered the record.
`seatDeclaredFields` fills every own key whose value is `undefined`,
derived from the record's own key set rather than from a list of field
names, so a field added tomorrow is seated without an edit. A control
assertion now fails if any declared slot is unobservable again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…Schema
The first ablation's most useful reading: deleting `installedVersion`
from either allowlist left both gates GREEN. Six of the twelve declared
record fields are ABSENT from a freshly installed record (installPackage
writes only what an install can know), so a gate watching only the
producer's live output cannot see them dropped.
The card asks for `served ⊇ the producer's stamped/DECLARED key set`, so
the declared half is derived from the record schema — in the TEST's
expectation, never in the production allowlist, which stays hand-written
and untouched. That distinction is the one the originating card ruled on
and it is spelled out at DECLARED_RECORD_KEYS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@os-trumpos-trump added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/l labels Sep 3, 2026 — with Claude
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f116b8f8d1b43ec2bfd64fbf0ebb0cf3c301b1c8packageMentionDocs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key - #14830

Open
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate
Open

test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key#14830
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate

Conversation

@os-trump

@os-trumpos-trump commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14642

A gate that goes red when a /packages door's field allowlist drops a key the
producer stamps or the record declares. Both sides of the comparison are derived
by running real code; the only hand-kept artifacts are two annotated registers,
both compared loudly.

Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

File face

Two files, both new, both tests:

  • packages/rest/src/package-door-producer-key-carry.test.ts
  • packages/runtime/src/domains/package-door-producer-key-carry.test.ts

No production file is modified. In particular packages/rest/src/error-response.ts
is not touched — PR #14713 is in flight on it and the gate's honest landing
site was never that file. No CI job added, no CI filter root widened.

Base refreshed by mergingorigin/main (not rebasing, no force-push) at
47181632f2. That merge matters: PR #14785 landed packages/runtime/tsconfig.test.json
while this branch was open, so the runtime pin is now compiled by a real tsc
program. See Verification.

Is the near-miss still reachable on today's main?

  • For writable itself: no. Both doors carry it now, by two different
    mechanisms, and both are pinned by name (package-list-writable-carry.test.ts,
    packages-serializable-response.test.ts).
  • For the general case: yes, fully reachable. Nothing generalised past that
    one field. The ablation below reintroduces the drop four different ways and
    every one of them shipped a 200 with a field silently absent before this PR.

So the card's premise stands, with the triage comment's sharpening: the gap is
the next stamped or declared key, not writable.

The two doors do not share an invariant, and the gate does not pretend they do

REST doorruntime door
producergetMetaItems({type:'package'})registry.getAllPackages()
where writable comes fromstamped upstream, listed in the allowlistcomputed at the door, deliberately not in the allowlist
invariantthe allowlist contains every stamped keythe stamp runs after the projection

Asserting "the allowlist contains every stamped key" at the runtime door would
red on a correct door. So the runtime pin measures the stamp set as
served − record — never a list — and compares it for set equality against
an annotated register. A reorder empties that measured set and reds; a new stamp
grows it and reds until the decision is written down. That register is hand-kept
on purpose: swapping a silent list for a loud one is the mechanism this card
asked for.

Both gate readings

Four ablation legs, each mutating one production line, each restored and proven
restored. The mutation is proven on disk by an anchor count before/after, never
by the editor's exit code. Re-run in full after the merge; every number below
is identical before and after
, so the merge moved nothing.

legmutationgatereading
1drop 'writable' from REGISTRY_PACKAGE_RESPONSE_FIELDSRESTREDlist/detail PKG: writable, 6 drops across 3 packages
2drop 'installedVersion' from the same allowlistRESTREDlist/detail PKG: installedVersion, 6 drops
3reorder to toPackageResponse(withWritableVerdict(...))runtimeREDstamp set for com.example.showcase: expected [] to deeply equal [ 'writable' ]
4drop 'installedVersion' from INSTALLED_PACKAGE_RESPONSE_FIELDSruntimeREDlist/detail PKG: installedVersion, 6 drops
controltree restoredbothGREEN — REST 4/4, runtime 5/5

Leg 1 is the exact near-miss the card is about. Leg 3 is the exact regression the
runtime door's ordering comment warns about. Legs 2 and 4 are the generality
claim: a plain declared field, with no field-specific pin anywhere.

No rebuild leg is involved and none is owed. Both mutated files are resolved
by vitest as local source — the REST pin imports ./package-routes.js, the
runtime pin reaches ./domains/packages.js through ../http-dispatcher.js — so
dist/ is not on the resolution path for either subject and there is nothing to
preflight. The restore leg is proven by git diff HEAD being empty for both
doors after every leg, not by the restore command's exit code.

Two findings the ablation produced, both now fixed in this PR

Recording these because in both cases the first version of the gate was green
for a bad reason, and a reviewer should see that the readings above are not the
first ones taken.

  1. Object.keys was the wrong instrument.installPackage seats settings
    as an own property holding undefined. Both doors omit undefined-valued
    fields deliberately, and JSON.stringify drops them anyway, so the first run
    reported six false drops for a key no consumer could ever have observed. The
    detector now measures defined-valued keys.

  2. The fixture could not see two thirds of the record. Deleting
    installedVersion from either allowlist left both gates green on the
    first ablation: installPackage writes only the fields an install can know,
    so six of the twelve declared fields are simply absent from a freshly
    installed record. seatDeclaredFields now seats every field the record
    schema declares, and a control assertion fails if any declared slot becomes
    unobservable again.

On packages/spec

The card rules out deriving the production allowlist from packages/spec,
and this PR does not: both allowlists stay hand-written and are untouched. What
finding 2 derives from the schema is the test's expectation, which is the
card's own wording for the detector — "the projected key set ⊇ the producer's
stamped/declared key set minus an explicit, annotated exclusion list".
Deriving the expectation is precisely what turns a newly declared field into an
explicit decision at each door instead of a silent omission. It adds no import
edge either: @objectstack/spec is already a runtime dependency of both
packages. The rationale is spelled out at DECLARED_RECORD_KEYS in both files so
it cannot be mistaken for the rejected design.

Why two pins rather than one detector

Measured, not assumed. A single file would have to reach the other package's
door:

  • From packages/runtime, @objectstack/rest resolves to its dist/ (no
    vitest alias maps it to source), which would make the gate's verdict a function
    of build state. It would also growKNOWN_UNALIASED_TEST_IMPORTS in
    scripts/check-test-source-alias.mjs, a shrink-only registry that lists no
    @objectstack/rest entry for that package today.
  • @objectstack/rest cannot import @objectstack/runtime at all — the
    dependency runs the other way.

So: two pins, one shape. Each imports its own door as source, which is also
what lets a reviewer ablate one allowlist and watch exactly one gate go red.

What I did NOT do, and why

  • Did not change either allowlist. Both are correct today; the card asked for
    the detector, not an edit to the lists.
  • Did not touch packages/rest/src/error-response.ts (hot file, PR fix(rest): consult the bespoke structured arms before the declared-status passthrough, so both error doors answer one refusal with one body #14713).
  • Did not add a changeset. The diff is two test files and nothing else. No
    package publishes a test file, so nothing user-visible ships. skip-changeset
    applied.
  • Did not widen a CI filter root or add a CI job.
  • Did not add anything to packages/runtime/test-typecheck-debt.json, and did
    not need to — see Verification. That ledger is byte-identical to origin/main
    in this branch.
  • Did not silence anything to reach green. No any, @ts-expect-error, or
    eslint-disable was added anywhere, and no assertion was weakened. The
    load-bearing comparison — set equality on the stamp set — runs over
    Set(string) and string[], not any.

Verification

All readings from 47181632f2, the merge commit at the head of this branch.
The dependency closure was built first (pnpm --filter '@objectstack/runtime^...' build),
because an unbuilt closure turns every workspace import into TS2307 and makes a
typecheck verdict a statement about a different world.

The runtime test-layer typecheck, which the merge newly applies to this PR

pnpm --filter @objectstack/runtime typecheckEXIT=0. It now chains
check:test-typecheck, whose own verdict line is:

check:test-typecheck: OK — @objectstack/runtime's test layer compiles under
packages/runtime/tsconfig.test.json; 27 file(s) / 191 error(s) / 69 pinned
signature(s) held in test-typecheck-debt.json (shrink-only and identity-pinned)

Three separate confirmations that the new runtime pin is genuinely covered and
carries no debt:

  • tsc --noEmit --project tsconfig.test.json --listFiles shows 1 hit for
    src/domains/package-door-producer-key-carry.test.ts — it is in the program.
    (This replaces the pre-merge reading of 0 hits, which was true on the old base
    and is now false.)
  • Error lines naming that file in the same run: 0. The 191 errors are exactly
    the ledgered ones in the other 27 files.
  • packages/runtime/test-typecheck-debt.json is byte-identical to
    origin/main's copy — nothing added, no number moved.

Other green

  • packages/rest gate — 4/4; packages/runtime gate — 5/5.
  • Sibling door pins re-run beside them after the merge:
    package-registry-item-projection, package-list-writable-carry,
    packages-serializable-response, packages-writable-verdict — 3 files /
    14 tests and 3 files / 23 tests, both EXIT=0.
  • pnpm --filter @objectstack/rest typecheck — EXIT=0, with --listFiles
    confirming 1 hit for the new REST test file.
  • 25 of the 28 gate families derived by scripts/pm/dispatch-gates.mjs --commands
    — exit 0, re-derived and re-run on the merged head (the family list is
    unchanged, and the change set is still exactly these two files).
    check:engine-double-contract was RED early on (my fixture's findOne double)
    and is green after removing that verb, which the producer path never calls.

Not measured, with the reason

  • check:dual-build-cjs-loads and check:type-check-debt --re-measure — exit
    3, "PREREQUISITE NOT MET": both need the whole workspace built
    (turbo run build --filter='./packages/*' --filter='./packages/*/*'), not just
    one closure. Exit 3 is neither a pass nor a finding. The sub-measurement that
    this diff could actually have moved — the runtime test-layer debt — is answered
    directly above by check:test-typecheck at EXIT=0 with the ledger unchanged.
    And check:dual-build-cjs-loads reads built output, which a diff adding only
    *.test.ts files cannot reach.
  • check-test-completeness — exit 3; it needs a saved turbo run test log,
    and its own failure text says to record it NOT MEASURED when run locally.

Narrowed, declared

The two packages' FULL suites were not run. Nine attempts on the shared verify
lock across this branch never got a long enough turn — holders included one run
at 917s and another at 1704s — and @objectstack/runtime's whole suite exceeds
what fits inside the container's foreground cap on a shared box. Narrowed to the
two new files plus the four sibling pins on the same two doors, all green above.
The diff modifies no production file, no config, and no turbo/vitest setting, so
the only way a sibling test's verdict could move is cross-file interference
inside a vitest worker; CI runs the full sharded suite on this PR regardless.

Generated by Claude Code

…drop a stamped key
Both `/packages` doors project the installed-package record through a
hand-written field allowlist. The trade that bought was explicit: drift
shows up as a missing field, never a 500. Within one day of it landing,
an ADR-0070 D2 `writable` verdict started reaching both doors and both
would have dropped it — a 200 with the field simply absent. One door was
saved by someone reading a sibling pin, the other by a merge conflict.
The field-specific pins that followed cover `writable` and nothing else.
These two gates cover the general case, deriving both sides from real
code rather than a hand-kept key list:
- REST door: `served ⊇ getMetaItems({type:'package'}) keys`, measured
through the real `ObjectStackProtocolImplementation` over a real
`SchemaRegistry`.
- runtime door: `served ⊇ record keys`, plus a set-equality register
for the keys the door stamps AFTER the projection — measured as
`served − record`, so a reorder empties it and reds.
The two doors solved the near-miss differently (the REST allowlist
contains `writable`; the runtime one deliberately does not and orders
instead), so the invariants are stated separately rather than assumed
symmetric. Only hand-kept artifacts are the annotated exclusion and
stamp registers, both compared loudly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…esence
`SchemaRegistry.installPackage` seats the optional record fields as own
properties holding `undefined`, so `'settings' in record` is true for a
package installed without settings. Both doors omit undefined-valued
fields deliberately and JSON.stringify drops them anyway, so counting
them made the gate red on every package for a key no consumer could have
observed — measured on the first run, six false drops.
Also drops the fixture's `findOne` double: `getMetaItems` never reaches
that verb, and `check:engine-double-contract` is right that a fake looser
than ObjectQL.findOne is worth refusing rather than stubbing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
… all of them
The ablation caught this and it is the more useful of the two findings:
deleting `installedVersion` from EITHER door's allowlist left both gates
GREEN. `installPackage` leaves 8 of the 12 declared fields as own
properties holding `undefined`, and a key the wire cannot carry is
correctly invisible to the detector — so the coverage assertion was
exercising 4 fields while reading as if it covered the record.
`seatDeclaredFields` fills every own key whose value is `undefined`,
derived from the record's own key set rather than from a list of field
names, so a field added tomorrow is seated without an edit. A control
assertion now fails if any declared slot is unobservable again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…Schema
The first ablation's most useful reading: deleting `installedVersion`
from either allowlist left both gates GREEN. Six of the twelve declared
record fields are ABSENT from a freshly installed record (installPackage
writes only what an install can know), so a gate watching only the
producer's live output cannot see them dropped.
The card asks for `served ⊇ the producer's stamped/DECLARED key set`, so
the declared half is derived from the record schema — in the TEST's
expectation, never in the production allowlist, which stays hand-written
and untouched. That distinction is the one the originating card ruled on
and it is spelled out at DECLARED_RECORD_KEYS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@os-trumpos-trump added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/l labels Sep 3, 2026 — with Claude
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f116b8f8d1b43ec2bfd64fbf0ebb0cf3c301b1c8packageMentionDocs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@os-trump@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key - #14830

Open
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate
Open

test(packages): gate that a package door's field allowlist cannot silently drop a stamped or declared key#14830
os-trump wants to merge 6 commits into
mainfrom
claude/issue-14642-package-door-field-allowlist-gate

Conversation

@os-trump

@os-trumpos-trump commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes#14642

A gate that goes red when a /packages door's field allowlist drops a key the
producer stamps or the record declares. Both sides of the comparison are derived
by running real code; the only hand-kept artifacts are two annotated registers,
both compared loudly.

Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza

File face

Two files, both new, both tests:

  • packages/rest/src/package-door-producer-key-carry.test.ts
  • packages/runtime/src/domains/package-door-producer-key-carry.test.ts

No production file is modified. In particular packages/rest/src/error-response.ts
is not touched — PR #14713 is in flight on it and the gate's honest landing
site was never that file. No CI job added, no CI filter root widened.

Base refreshed by mergingorigin/main (not rebasing, no force-push) at
47181632f2. That merge matters: PR #14785 landed packages/runtime/tsconfig.test.json
while this branch was open, so the runtime pin is now compiled by a real tsc
program. See Verification.

Is the near-miss still reachable on today's main?

  • For writable itself: no. Both doors carry it now, by two different
    mechanisms, and both are pinned by name (package-list-writable-carry.test.ts,
    packages-serializable-response.test.ts).
  • For the general case: yes, fully reachable. Nothing generalised past that
    one field. The ablation below reintroduces the drop four different ways and
    every one of them shipped a 200 with a field silently absent before this PR.

So the card's premise stands, with the triage comment's sharpening: the gap is
the next stamped or declared key, not writable.

The two doors do not share an invariant, and the gate does not pretend they do

REST doorruntime door
producergetMetaItems({type:'package'})registry.getAllPackages()
where writable comes fromstamped upstream, listed in the allowlistcomputed at the door, deliberately not in the allowlist
invariantthe allowlist contains every stamped keythe stamp runs after the projection

Asserting "the allowlist contains every stamped key" at the runtime door would
red on a correct door. So the runtime pin measures the stamp set as
served − record — never a list — and compares it for set equality against
an annotated register. A reorder empties that measured set and reds; a new stamp
grows it and reds until the decision is written down. That register is hand-kept
on purpose: swapping a silent list for a loud one is the mechanism this card
asked for.

Both gate readings

Four ablation legs, each mutating one production line, each restored and proven
restored. The mutation is proven on disk by an anchor count before/after, never
by the editor's exit code. Re-run in full after the merge; every number below
is identical before and after
, so the merge moved nothing.

legmutationgatereading
1drop 'writable' from REGISTRY_PACKAGE_RESPONSE_FIELDSRESTREDlist/detail PKG: writable, 6 drops across 3 packages
2drop 'installedVersion' from the same allowlistRESTREDlist/detail PKG: installedVersion, 6 drops
3reorder to toPackageResponse(withWritableVerdict(...))runtimeREDstamp set for com.example.showcase: expected [] to deeply equal [ 'writable' ]
4drop 'installedVersion' from INSTALLED_PACKAGE_RESPONSE_FIELDSruntimeREDlist/detail PKG: installedVersion, 6 drops
controltree restoredbothGREEN — REST 4/4, runtime 5/5

Leg 1 is the exact near-miss the card is about. Leg 3 is the exact regression the
runtime door's ordering comment warns about. Legs 2 and 4 are the generality
claim: a plain declared field, with no field-specific pin anywhere.

No rebuild leg is involved and none is owed. Both mutated files are resolved
by vitest as local source — the REST pin imports ./package-routes.js, the
runtime pin reaches ./domains/packages.js through ../http-dispatcher.js — so
dist/ is not on the resolution path for either subject and there is nothing to
preflight. The restore leg is proven by git diff HEAD being empty for both
doors after every leg, not by the restore command's exit code.

Two findings the ablation produced, both now fixed in this PR

Recording these because in both cases the first version of the gate was green
for a bad reason, and a reviewer should see that the readings above are not the
first ones taken.

  1. Object.keys was the wrong instrument.installPackage seats settings
    as an own property holding undefined. Both doors omit undefined-valued
    fields deliberately, and JSON.stringify drops them anyway, so the first run
    reported six false drops for a key no consumer could ever have observed. The
    detector now measures defined-valued keys.

  2. The fixture could not see two thirds of the record. Deleting
    installedVersion from either allowlist left both gates green on the
    first ablation: installPackage writes only the fields an install can know,
    so six of the twelve declared fields are simply absent from a freshly
    installed record. seatDeclaredFields now seats every field the record
    schema declares, and a control assertion fails if any declared slot becomes
    unobservable again.

On packages/spec

The card rules out deriving the production allowlist from packages/spec,
and this PR does not: both allowlists stay hand-written and are untouched. What
finding 2 derives from the schema is the test's expectation, which is the
card's own wording for the detector — "the projected key set ⊇ the producer's
stamped/declared key set minus an explicit, annotated exclusion list".
Deriving the expectation is precisely what turns a newly declared field into an
explicit decision at each door instead of a silent omission. It adds no import
edge either: @objectstack/spec is already a runtime dependency of both
packages. The rationale is spelled out at DECLARED_RECORD_KEYS in both files so
it cannot be mistaken for the rejected design.

Why two pins rather than one detector

Measured, not assumed. A single file would have to reach the other package's
door:

  • From packages/runtime, @objectstack/rest resolves to its dist/ (no
    vitest alias maps it to source), which would make the gate's verdict a function
    of build state. It would also growKNOWN_UNALIASED_TEST_IMPORTS in
    scripts/check-test-source-alias.mjs, a shrink-only registry that lists no
    @objectstack/rest entry for that package today.
  • @objectstack/rest cannot import @objectstack/runtime at all — the
    dependency runs the other way.

So: two pins, one shape. Each imports its own door as source, which is also
what lets a reviewer ablate one allowlist and watch exactly one gate go red.

What I did NOT do, and why

  • Did not change either allowlist. Both are correct today; the card asked for
    the detector, not an edit to the lists.
  • Did not touch packages/rest/src/error-response.ts (hot file, PR fix(rest): consult the bespoke structured arms before the declared-status passthrough, so both error doors answer one refusal with one body #14713).
  • Did not add a changeset. The diff is two test files and nothing else. No
    package publishes a test file, so nothing user-visible ships. skip-changeset
    applied.
  • Did not widen a CI filter root or add a CI job.
  • Did not add anything to packages/runtime/test-typecheck-debt.json, and did
    not need to — see Verification. That ledger is byte-identical to origin/main
    in this branch.
  • Did not silence anything to reach green. No any, @ts-expect-error, or
    eslint-disable was added anywhere, and no assertion was weakened. The
    load-bearing comparison — set equality on the stamp set — runs over
    Set(string) and string[], not any.

Verification

All readings from 47181632f2, the merge commit at the head of this branch.
The dependency closure was built first (pnpm --filter '@objectstack/runtime^...' build),
because an unbuilt closure turns every workspace import into TS2307 and makes a
typecheck verdict a statement about a different world.

The runtime test-layer typecheck, which the merge newly applies to this PR

pnpm --filter @objectstack/runtime typecheckEXIT=0. It now chains
check:test-typecheck, whose own verdict line is:

check:test-typecheck: OK — @objectstack/runtime's test layer compiles under
packages/runtime/tsconfig.test.json; 27 file(s) / 191 error(s) / 69 pinned
signature(s) held in test-typecheck-debt.json (shrink-only and identity-pinned)

Three separate confirmations that the new runtime pin is genuinely covered and
carries no debt:

  • tsc --noEmit --project tsconfig.test.json --listFiles shows 1 hit for
    src/domains/package-door-producer-key-carry.test.ts — it is in the program.
    (This replaces the pre-merge reading of 0 hits, which was true on the old base
    and is now false.)
  • Error lines naming that file in the same run: 0. The 191 errors are exactly
    the ledgered ones in the other 27 files.
  • packages/runtime/test-typecheck-debt.json is byte-identical to
    origin/main's copy — nothing added, no number moved.

Other green

  • packages/rest gate — 4/4; packages/runtime gate — 5/5.
  • Sibling door pins re-run beside them after the merge:
    package-registry-item-projection, package-list-writable-carry,
    packages-serializable-response, packages-writable-verdict — 3 files /
    14 tests and 3 files / 23 tests, both EXIT=0.
  • pnpm --filter @objectstack/rest typecheck — EXIT=0, with --listFiles
    confirming 1 hit for the new REST test file.
  • 25 of the 28 gate families derived by scripts/pm/dispatch-gates.mjs --commands
    — exit 0, re-derived and re-run on the merged head (the family list is
    unchanged, and the change set is still exactly these two files).
    check:engine-double-contract was RED early on (my fixture's findOne double)
    and is green after removing that verb, which the producer path never calls.

Not measured, with the reason

  • check:dual-build-cjs-loads and check:type-check-debt --re-measure — exit
    3, "PREREQUISITE NOT MET": both need the whole workspace built
    (turbo run build --filter='./packages/*' --filter='./packages/*/*'), not just
    one closure. Exit 3 is neither a pass nor a finding. The sub-measurement that
    this diff could actually have moved — the runtime test-layer debt — is answered
    directly above by check:test-typecheck at EXIT=0 with the ledger unchanged.
    And check:dual-build-cjs-loads reads built output, which a diff adding only
    *.test.ts files cannot reach.
  • check-test-completeness — exit 3; it needs a saved turbo run test log,
    and its own failure text says to record it NOT MEASURED when run locally.

Narrowed, declared

The two packages' FULL suites were not run. Nine attempts on the shared verify
lock across this branch never got a long enough turn — holders included one run
at 917s and another at 1704s — and @objectstack/runtime's whole suite exceeds
what fits inside the container's foreground cap on a shared box. Narrowed to the
two new files plus the four sibling pins on the same two doors, all green above.
The diff modifies no production file, no config, and no turbo/vitest setting, so
the only way a sibling test's verdict could move is cross-file interference
inside a vitest worker; CI runs the full sharded suite on this PR regardless.

Generated by Claude Code

…drop a stamped key
Both `/packages` doors project the installed-package record through a
hand-written field allowlist. The trade that bought was explicit: drift
shows up as a missing field, never a 500. Within one day of it landing,
an ADR-0070 D2 `writable` verdict started reaching both doors and both
would have dropped it — a 200 with the field simply absent. One door was
saved by someone reading a sibling pin, the other by a merge conflict.
The field-specific pins that followed cover `writable` and nothing else.
These two gates cover the general case, deriving both sides from real
code rather than a hand-kept key list:
- REST door: `served ⊇ getMetaItems({type:'package'}) keys`, measured
through the real `ObjectStackProtocolImplementation` over a real
`SchemaRegistry`.
- runtime door: `served ⊇ record keys`, plus a set-equality register
for the keys the door stamps AFTER the projection — measured as
`served − record`, so a reorder empties it and reds.
The two doors solved the near-miss differently (the REST allowlist
contains `writable`; the runtime one deliberately does not and orders
instead), so the invariants are stated separately rather than assumed
symmetric. Only hand-kept artifacts are the annotated exclusion and
stamp registers, both compared loudly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…esence
`SchemaRegistry.installPackage` seats the optional record fields as own
properties holding `undefined`, so `'settings' in record` is true for a
package installed without settings. Both doors omit undefined-valued
fields deliberately and JSON.stringify drops them anyway, so counting
them made the gate red on every package for a key no consumer could have
observed — measured on the first run, six false drops.
Also drops the fixture's `findOne` double: `getMetaItems` never reaches
that verb, and `check:engine-double-contract` is right that a fake looser
than ObjectQL.findOne is worth refusing rather than stubbing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
… all of them
The ablation caught this and it is the more useful of the two findings:
deleting `installedVersion` from EITHER door's allowlist left both gates
GREEN. `installPackage` leaves 8 of the 12 declared fields as own
properties holding `undefined`, and a key the wire cannot carry is
correctly invisible to the detector — so the coverage assertion was
exercising 4 fields while reading as if it covered the record.
`seatDeclaredFields` fills every own key whose value is `undefined`,
derived from the record's own key set rather than from a list of field
names, so a field added tomorrow is seated without an edit. A control
assertion now fails if any declared slot is unobservable again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…Schema
The first ablation's most useful reading: deleting `installedVersion`
from either allowlist left both gates GREEN. Six of the twelve declared
record fields are ABSENT from a freshly installed record (installPackage
writes only what an install can know), so a gate watching only the
producer's live output cannot see them dropped.
The card asks for `served ⊇ the producer's stamped/DECLARED key set`, so
the declared half is derived from the record schema — in the TEST's
expectation, never in the production allowlist, which stays hand-written
and untouched. That distinction is the one the originating card ruled on
and it is spelled out at DECLARED_RECORD_KEYS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
@os-trumpos-trump added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/l labels Sep 3, 2026 — with Claude
@github-actions

github-actionsBot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f116b8f8d1b43ec2bfd64fbf0ebb0cf3c301b1c8packageMentionDocs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/lskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

2 participants

@os-trump@claude