Skip to content

fix(engine-core): a system-context insert resolves the install's organization, or is refused — the runtime producer of the autonumber fork (#8844) - #8930

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-8844-system-write-organization-stamp
Aug 15, 2026
Merged

hotlong merged 2 commits into
mainfrom
claude/issue-8844-system-write-organization-stamp

Conversation

@hotlong

@hotlong hotlong commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #8844

The runtime twin of #8686, one layer up. That card fixed one producer of untenanted rows — the seed loader — and shipped a one-shot backfill for what it had already written. This is the other producer, which is still running: an ordinary application write made under a system execution context (a hook, a scheduled job, a custom endpoint, a runAs: system flow). A backfill cannot reach it, because it mints a fresh duplicate on every tick — which is what makes #8686's repair self-undoing on any install with server-side automation.

Implements the maintainer ruling in comment 5302919977 — Option 1, all five binding points, not re-adjudicated.

Premises re-verified on today's tree (main @ 8664a2c99, not the escalation's 6b6b606)

The escalation's line references were dated evidence; each was re-located by content and re-checked:

premise verdict on today's tree
the stamp is an INJECTED field, not an explicit assignment OK — packages/objectql/src/registry.ts:555, additions.organization_id = { ...TENANT_SCOPE_FIELD_DEF } (same line, re-checked by content)
engine.ts documents the beforeInsert stamp path OK — packages/objectql/src/engine.ts:8158
the defect is UPSTREAM of the counter OK, measured not inferred: SqlDriver.create calls injectTenantOnInsert at sql-driver.ts:4402 and fillAutoNumberFields at :4421, so the counter reads an already-stamped row. The direction of causation holds; the fix site does not move.
no chokepoint helper exists OK, re-confirmed. The edit site is the engine's stamp resolution.
fillAutoNumberFields is duplicated across drivers OK — and driver-turso reaches it by inheriting SqlDriver.create. Relevant to the tests, not the fix site: no driver changed.

What changed

packages/objectql/src/tenancy/system-write-organization.ts (new) — the ruling as one pure, directly-testable decision, plus the refusal class. engine.ts supplies the measured facts and resolves the answer into DriverOptions.tenantId — the same knob a session write sets, so injectTenantOnInsert stamps the column and the counter scopes by the real organization. That is why no driver had to change, on either of the two that carry fillAutoNumberFields.

The refusal is the dangerous half — how it was kept narrow

A refusal that is too broad breaks every system write on a walled install, including unattended automation. Four populations are outside the rule by construction, not by exemption:

population why
no organization column, tenancy.enabled: false (ADR-0066), federated (ADR-0015) there is no tenant column to fork a counter by. tenancy: { enabled: false } is the declared way to hold org-less rows — stated once on the object and checkable — rather than a per-write bypass flag, which is the lenient-consumer accommodation PD #12 forbids.
sys_ / cloud_ / ai_ platform namespaces rows deliberately global. This is #8672's reasoning, which binding point 5 confirms holds for platform objects and does not generalize to application objects. Same regexp the seed loader applies and #8686's backfill re-spells — the three write paths have to agree.
a write already carrying an organization on the execution context, on the record, or stamped by a beforeInsert hook (the resolution runs after the hooks for exactly this reason). That IS "carrying an explicit organization".
zero organizations exist yet first boot. Nothing to derive, and no second partition to fork away from — #8686's sys_organization-insert handoff adopts exactly these rows. Refusing here would refuse first boot itself.

Topology buckets, as measured (Zone 2 point 3): posture read live per call, service first (tenancyPostureProvider, what is ENFORCED) then env (resolveTenancyPosture, what was ASKED for) — the same two-step app-plugin.organizationWallActive uses, and never the demoted resolveMultiOrgEnabled(). single + 1 org: derive. single + 0 orgs: leave untenanted. single + 2 or more orgs: refuse (ambiguous). group/isolated: refuse (walled, without probing the database at all). An unreadable OS_TENANCY_POSTURE: degrades to the walled reading and refuses, because a typo'd posture is exactly where guessing single would be worst.

Scoped to INSERT, deliberately. The ruling's yardstick is "the way a session write does", and stamping the organization is an insert-side mechanism — an update neither stamps it nor can fork a counter. Extending it would not be following the session write, it would be inventing a second rule.

Tests, and the ablation that proves they can fail

  • packages/objectql/src/system-write-organization.test.ts — 20 cases: the decision function's branches, then the engine seam (DriverOptions.tenantId), each with its discriminating control.
  • packages/runtime/src/system-write-tenancy-autonumber-split.integration.test.ts — 4 cases on a real ObjectQL + SqlDriver over better-sqlite3, asserting on the database: _objectstack_sequences, the stored rows, the duplicate count.

Two ablation legs, disjoint red sets — each half of the fix is independently pinned. Directions were predicted before running; one prediction was wrong and the measured direction is reported:

ablation objectql runtime
A — refusal removed (silently defaults to __global__) 4 RED / 16 green: expected undefined to be 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED' 1 RED ([binding point 2]) / 3 green
B — derive-and-stamp removed, refusal intact 2 RED / 18 green: expected undefined to be 'org_msokm9oaz0cal87q' 3 RED / 1 green — [binding point 1] expected 5 to be 10, [#8686 regression] the __global__ row reappears beside the organization's

Correction to a prediction. Leg B was predicted to leave [first boot] green; it went RED (expected 2 to be 1). The case's second half asserts that once the organization exists the runtime producer is stamped, so it pins the derive branch too — the test is stronger than predicted, and this reports what was observed rather than what was expected.

packages/runtime resolves @objectstack/objectql from dist/, so every ablation leg was rebuilt and proved live in the artifact with node scripts/ablation-dist-preflight.mjs @objectstack/objectql '__OS_ABLATION_*__' (and proved absent again after restore). Without that, leg A's first runtime run was a false green — measured here, not hypothesised.

Verification — union re-run at final HEAD 445359f0b

Edited packages: spec 10644, objectql 3695, runtime 2456 tests, all passing; typecheck green across objectql and its 43 DOWNSTREAM consumers (prefix filter ...@objectstack/objectql).

Downstream test sweep (consumers, not dependencies): rest 1948, plugin-security 1235, plugin-auth 1238, plugin-sharing 599, plugin-approvals 484, plugin-audit 239, driver-turso 994, driver-mongodb 384, service-datasource 426, service-settings 456, service-messaging 229, service-queue 53, trigger-record-change 78, trigger-schedule 46, cli 1358, dogfood 779, http-conformance 72, verify 28 — all green. #8686's own seed-tenancy-autonumber-split.integration.test.ts runs green beside the new file.

Gates: every family scripts/pm/dispatch-gates.mjs derives from the final changed paths, plus check:error-code-casing, check:nul-bytes, check:engine-double-contract, check:tenant-chokepoint, check:type-check-coverage, check:type-check-debt --re-measure (on the built workspace), check:query-options-erasure, pnpm lint, and pnpm --filter @objectstack/spec check:generated (13/13 current).

One ratchet moved and was fixed at the source, not raised: check:query-options-erasure counted the new this.find(sys_organization, {…} as any) as a ninth erasing site in engine.ts (grandfathered for its existing eight only). The literal type-checks against EngineQueryOptions unmodified, so the cast came out — commit 445359f0b. Ratchet back at baseline, no ceiling touched.

Not done here, deliberately

No driver change (Option 2 is off the table). No renumbering of already-minted identifiers — reported instead, via #8928. #8672 remains open and untouched; this PR is the evidence its on-hold reasoning does not generalize, not a change to it.

The issue body was self-checked for sanitizer truncation: intact — its escaped arrow spans survived as entities and no prose is cut.


Generated by Claude Code

claude added 2 commits August 15, 2026 16:39
… inserts, or refuse (#8844)

The runtime twin of #8686's ruling, one layer up. A system-context write —
a hook, a scheduled job, a custom endpoint, a `runAs: system` flow — carried
no organization, so the tenant column landed NULL and the SQL driver's
autonumber counter filed the row under the `__global__` pseudo-tenant. One
object then ran two counters that could not see each other, and the
partitioned unique index `(COALESCE(organization_id, '__global__'), <field>)`
could not see across the two partitions either: a field declared `unique`
held the same value twice, with no error and no warning. Measured on 17.0.0
GA across five objects on one single-tenant install.

Per the 2026-08-15 maintainer ruling (Option 1), the engine's stamp
resolution now answers the same question a session write answers:

- single-tenant with exactly one organization: derive and stamp it, by
  threading `DriverOptions.tenantId` — the same knob a session write sets, so
  no driver changed (`fillAutoNumberFields` is duplicated in driver-sql and
  driver-turso);
- multi-organization (a walled posture, or a `single` posture whose data
  holds several organizations): refuse loudly with
  ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED (500, registered in the ADR-0112
  ledger) rather than silently default to `__global__`;
- already-minted duplicates are reported, never rewritten — the #8686
  posture, ruled again here. Nothing renumbers anything.

Three populations are outside the rule by construction so the refusal cannot
break automation that was never at risk: objects with no organization column,
`tenancy.enabled: false` objects and federated objects; the `sys_`/`cloud_`/
`ai_` platform namespaces (#8672's reasoning, which does not generalize to
application objects); and any write already carrying an organization. Before
any organization exists there is nothing to derive and no partition to fork
from, so first boot is untouched and #8686's handoff still adopts those rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
…8844)

`check:query-options-erasure` counted the new `this.find(sys_organization,
{...} as any)` as a ninth erasing site in engine.ts, which is grandfathered for
its EXISTING sites only. The literal type-checks against `EngineQueryOptions`
unmodified, so the cast was never needed — removed at the source rather than
laundered through `as unknown as`, and the ratchet is back at its baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeQRiAa7vYRVX5Fog7Zby8
@vercel

vercel Bot commented Aug 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 15, 2026 6:17pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/spec.

109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/objectql, packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

⛔ 7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 15, 2026
@hotlong
hotlong marked this pull request as ready for review August 15, 2026 20:22
@hotlong
hotlong added this pull request to the merge queue Aug 15, 2026
Merged via the queue into main with commit ff08691 Aug 15, 2026
28 checks passed
@hotlong
hotlong deleted the claude/issue-8844-system-write-organization-stamp branch August 15, 2026 20:40
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… list with a counterfactual pin (objectstack-ai#18700)

Fixes objectstack-ai#18560

**Clause-②: no**

`scripts/pm/check-widening-tells.mjs` is the TELL half of the clause-②
gate (the DECLARATION half, `check-clause2-carriers.mjs`, is a different
file and PR objectstack-ai#18681's subject). Its T1 reader decides a line declares a
schema member by what the property's VALUE opens with, and that
vocabulary was five alternatives inside one 130-character regex literal.
A form missing from it is not a line judged leniently — it is a line
that is not a key line at all: `memberTellKind` answers `null`, the row
neither fires nor spends the objectstack-ai#16943 replacement budget nor earns it on
the removed side, and nothing in the output says so. The silence is
indistinguishable from a correct `no`.

## The counterfactual, before and after

Re-derived here rather than inherited from the card's reading
(objectui#9647 comment 5707064702, an at-tier reviewer's measurement).
Run as `PM_SWEEP_REPO=objectstack-ai/objectui node
scripts/pm/check-widening-tells.mjs --declaration no --diff …`, the
BEFORE leg against the file as it stands at `6dfa3ea77` (this branch's
merge base), the AFTER leg at `7f1a81419`:

| diff | declaration | BEFORE (6dfa3ea) | AFTER (7f1a814) |
|---|---|---|---|
| objectui#9647, unmodified | `no` | exit 0, 1 file judged, **0 tells**
| exit 0, 1 file judged, 0 tells |
| objectui#9647 plus one key added through `stripImportedDefaults(` |
`no` | **exit 0, 0 tells** | **exit 4, T1 at
`packages/types/src/zod/data-display.zod.ts:584`** |
| the same widened diff | `yes` | exit 0 | exit 0 (a `yes` is never
blocked) |

Row 2 is the red this PR turns. Row 1 does NOT change verdict, and that
is the correction the re-derivation forced.

## What the re-derivation corrects

The two helpers the card names are objectui's REFUSAL family
(`packages/types/src/zod/tombstone.zod.ts`), read off the source:

- `retirementTombstone(guidance)` returns `z.never({ error: guidance
}).optional().describe(guidance)` — the same primitive as this repo's
`retiredKey()`;
- `handlerKeyRefusal(key, disposition, label)` returns a `z.custom`
predicate typed `never` that returns `false`, and its own docblock
records that "The predicate refuses EVERYTHING, a live function
included".

A key declared through either is a key an author may NO LONGER write.
Making those two fire a tell would re-mint, on 290 objectui key lines,
the exact false positive objectstack-ai#17955 removed on 255 objectstack ones — and a
false tell does not cost a word in a comment, it costs the false `yes`
this file's own header refuses to ask an author for.

The form that DOES carry a widening, and that no seat had named, is
`stripImportedDefaults()`
(`packages/types/src/zod/imported-defaults.ts`), whose docblock states
its contract as "the same TypeScript type, the same keys, the same
checks, the same registry metadata and the same accept set". It returns
a LIVE schema and is spelled at 45 key positions on the judged objectui
surface.

## The named list

`SCHEMA_PROPERTY_FORMS` — exported, frozen, and the constant
`SCHEMA_PROPERTY` is BUILT from it. Two fields carry two questions:
`pattern` (what makes the line a KEY LINE) and `writable` (whether the
key it declares is one an author may write). Counts are key-POSITION
counts, each with the tree it was taken against.

| form | writable | measured |
|---|---|---|
| `z.` | yes | 7,784 at objectstack `6dfa3ea77` · 1,482 at objectui
`15f01223d` |
| `lazySchema(` | yes | 0 at objectstack `6dfa3ea77` (live at
DECLARATION positions) |
| `strictObject(` | yes | 47 at objectstack `6dfa3ea77` |
| `*Schema` | yes | 995 at objectstack `6dfa3ea77` · 56 at objectui
`15f01223d` |
| `stripImportedDefaults(` | yes | **45 at objectui `15f01223d`**
(added) |
| `retiredKey(` | no | 255 at objectstack `6dfa3ea77` |
| `retirementTombstone(` | no | **187 at objectui `15f01223d`** (added)
|
| `handlerKeyRefusal(` | no | **90 at objectui `15f01223d`** (added) |
| `aliasKeyRefusal(` | no | **13 at objectui `15f01223d`** (added) |

The `writable: false` arm is objectstack-ai#17955's decline generalised from one
helper name to the family, on the SAME positive, line-local evidence:
the value must BE the call and nothing after it.
`declaresRetiredKeyTombstone` is renamed `declaresUnwritableKey`
accordingly; it is not imported by any other file.

**The `no` criterion is not loosened anywhere, and the direction is
provable rather than argued.** An unrecognised line reports NOTHING, so
no row that fires today can stop firing when the list grows. A self-test
case keeps the literal this replaced as the reference and asserts it:
every legacy verdict is byte-identical, and the only cells that move are
the four added forms moving from "not a key line" to "a key line" — one
direction, zero losses.

## The pins

New battery, registered on the roster and pinned: `'objectstack-ai#18560 — the
declaring vocabulary is a NAMED list, every form pinned by a
counterfactual fixture': 30` (39 cases run against a floor of 30). Its
unit is the FORM, not the assertion:

- a frozen fixture roster is asserted EQUAL to the form set, in both
directions and naming the offenders — a form added to the list without a
fixture reds, a form silently dropped from the list reds. The fixtures
are deliberately NOT generated from the list, which would make every
future form pass by construction;
- every form is asserted RECOGNISED (`memberTellKind` answers T1) —
writable or not, because an unrecognised line is invisible to both sides
of the budget, which is the defect itself;
- every form's fixture is then driven through `tellsInFile` and asserted
against its OWN register: a `writable` form must FIRE with its
file:line, an unwritable one must be recognised and DECLINE;
- every unwritable form carries the chained-arm control that FIRES, so
the decline is bound to the evidence the line carries and never to the
helper's name;
- the objectui#9647 shape is carried as a case in all three readings,
plus the `stripImportedDefaults(` widening with its refusal, its
file:line, the `yes` control and the default-board control.

`check:entry-guard` is the reason the vocabulary's structural guard is a
const initializer's `map` step rather than a top-level `for`: this file
exports bindings the sibling gate imports, so a load-time throw would
run inside the importer.

## The census — report-only, no re-grade, no state write

Has the silence already been relied on? **Zero confirmed `Clause-②: no`
landings through these forms, over 11 of 18 rows read.**

- **Horizon.** objectui's full history (`git rev-parse
--is-shallow-repository` = false, 10,282 commits, initial commit
2026-01-13) up to the checkout tip `15f01223d` (2026-09-16). PR
objectui#9647 itself is OUTSIDE it: `git merge-base --is-ancestor
604476d HEAD` exits 1, with the initial commit as the control leg at
exit 0 on the same non-shallow checkout. The judgeable part of that
window starts 2026-09-10, when objectstack-ai#17278 first let this CLI be told which
board it judges.
- **Population.** 46 commits add a key through one of the four added
forms on `packages/types/src/zod/**`; 18 land inside the judgeable
window.
- **Coverage — read (11 of 18):** 10 carry `Clause-②: yes` in the PR
body (objectui#8884, objectstack-ai#8895, objectstack-ai#8930, objectstack-ai#8967, objectstack-ai#9051, objectstack-ai#9338, objectstack-ai#9495, objectstack-ai#9539,
objectstack-ai#9541, objectstack-ai#9565); 1 (objectui#9443) carries a "Clause-② carriers" section
attaching `needs:contract-review` with no `yes`/`no` token.
- **Coverage — NOT ATTEMPTED (7 of 18):** objectui#8984, objectstack-ai#9254, objectstack-ai#9261,
objectstack-ai#9343, objectstack-ai#9544, objectstack-ai#9589, objectstack-ai#9621 carry no declaration in the PR body; the
remaining carrier is each card's claim comment, which was not read.
**This is NOT a zero** — it is seven rows unread and named.
- The commit-local changeset is a weak carrier in objectui: only 2
commits in the whole history carry a `Clause-②` line in a changeset,
which is why the census reads PR bodies rather than the tree.

## The ablation

From the committed fix, `HEAD` blob
`1b741ee6c22db6523475b698d392a8ecd41686d6`, under a `trap '…' EXIT INT
TERM` restoring `git checkout HEAD --
scripts/pm/check-widening-tells.mjs` at an absolute path. No build or
`dist/` is involved — the gate runs from source, so there is no rebuild
leg to prove.

- **mutation:** the `retirementTombstone(` and `handlerKeyRefusal(` rows
deleted from `SCHEMA_PROPERTY_FORMS`;
- **mutation proved on disk, not from an exit code:** anchor counts 1 →
0 for each form, file 257,365 → 256,697 bytes, blob
`9fb65edf6f68b1501a0aec29d0ddb4c95bcf44a2` which is not the HEAD blob;
- **predicted direction:** RED. **Observed:** RED — `--self-test` exits
1, "6 of 377 case(s) failed" plus the verdict-handshake report. The
failures name the mechanism: the orphaned-fixture case prints both
dropped forms, both RECOGNITION cases fail, both objectui#9647 cases
fail, and the one-direction case fails;
- **restore verified by hash, not by a return code:** `git hash-object`
back to `1b741ee6c22db6523475b698d392a8ecd41686d6`, `git diff HEAD`
empty, `git status --porcelain` clean, and the suite back to 381 cases
pass.

## Self-test

`pnpm check:pm-widening-tells` — **381 cases pass** (342 before this PR;
+39). Exit 0. The two gates that IMPORT this module were run too:
`check:pm-clause2-carriers` exit 0, `check:pm-prior-rulings` exit 0.

## Derived gates

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from the worktree, no hand-fed path list —
29 commands, each run with `$?` captured BEFORE any pipe. Reconciled
with `--ran`: "29 derived famil(ies) accounted for — 29 run, 0
NOT-MEASURED (a DERIVED zero — all 29 recorded an exit code and none of
them is 3)".

```
exit 0  node scripts/check-ci-filter-parity.mjs
exit 0  node scripts/check-closing-keyword-parity.mjs
exit 0  node scripts/check-closing-keyword-parity.mjs --self-test
exit 0  node scripts/check-comment-mask-corpus.mjs
exit 0  node scripts/check-declaration-mirrors.mjs
exit 0  node scripts/check-declaration-mirrors.mjs --self-test
exit 0  node scripts/check-scripts-symbol-anchors.mjs
exit 0  node scripts/check-scripts-symbol-anchors.mjs --self-test
exit 0  node scripts/check-self-test-wired.mjs
exit 0  node scripts/check-self-test-wired.mjs --self-test
exit 0  node scripts/check-self-test-workflow-commands.mjs
exit 0  node scripts/check-self-test-workflow-commands.mjs --self-test
exit 0  node scripts/check-whole-set-label-write.mjs
exit 0  node scripts/check-whole-set-label-write.mjs --self-test
exit 0  node scripts/pm/bare-root-worklist.mjs --self-test
exit 0  pnpm check:agent-test-spelling
exit 0  pnpm check:bash32-floor
exit 0  pnpm check:cli-command-ids
exit 0  pnpm check:cross-package-test-inputs
exit 0  pnpm check:driver-memory-census
exit 0  pnpm check:entry-guard
exit 0  pnpm check:nul-bytes
exit 0  pnpm check:parse-guard
exit 0  pnpm check:pm-dispatch-gates
exit 0  pnpm check:pm-widening-tells
exit 0  pnpm check:pnpm-filter-targets
exit 0  pnpm check:ratchet-remedy-authority
exit 0  pnpm check:refd-timer-probe
exit 0  pnpm check:watch-hint-literal
```

Repo-wide `pnpm lint` (`eslint . --no-inline-config`) at `7f1a81419`:
**exit 0**, no findings. Control-byte scan over the edited file: no
hits.

`skip-changeset`: `scripts/pm/**` is not published by any package's
`files[]`, and this diff touches nothing else.

## One boundary this does NOT close, recorded rather than left to be
found

A FILE-LOCAL declaring factory. Both trees mint them —
`placeholderFree(` (23 key lines), `strictIdent(` (12), `emptyProps(`
(9) at objectstack `6dfa3ea77`; `chatbotRequestBodyArm(` (2),
`retiredDeclarativeKanbanKey(` (1) at objectui `15f01223d` — and a list
of shared, exported helpers cannot name a factory private to one file. A
name-shaped heuristic is refused in the header, with the overturn
condition written down. Filed as its own finding in the report on the
card, not fixed here.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

2 participants