Skip to content

fix(ci): the Console Pin Gate never counts text objectui's own source carries as the published spec (#21709) - #21717

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21709-console-probe-collision
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-21709-console-probe-collision

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21709

Clause-②: no

What changes

The Console Pin Gate's probes no longer treat text that objectui's own source carries as evidence of either spec. A console bundle carries text from objectui's code as well as from the specs. objectui's component registry writes inputs descriptions that copy spec .describe() text, either verbatim or as a prefix it then extends. When the spec rewords one of those, the old text becomes published-only. objectui's literal still carries it, so a substring search found "the published spec" in a bundle built from this tree's spec. That is what has turned every merge-queue build red since #21699 (16d241a6af): the object-gantt markers describe and objectui's packages/plugin-gantt/src/index.tsx:190.

  • scripts/console-spec-probes.mjs: readHostSourceBlob(dir) reads objectui's tracked code files at the pin (git ls-files; test files excluded; a quote's backslash normalised). chooseProbes now takes that blob as hostBlob, required. It is a TypeError without it.
  • The rule (both legs): a candidate that is in the bundle and carried by objectui's source is not evidence. It is counted (counts.hostCarried), never decides a verdict, and is never returned as a probe, so it is never stamped. A candidate absent from the bundle is still evidence, whoever else carries it. The rule excuses presence, never absence.
  • One new refusal: if every published-only candidate is in the bundle and objectui carries every one, the stale leg has no probe left. That is neither "no skew" nor "absent", so the assert exits 2 (inconclusive). The old code answered that same state exit 1, with a false accusation. It never passed.
  • scripts/assert-console-spec-injection.mjs: a new required --objectui flag (objectui's build tree, the git checkout at the pin). Its messages report host-carried text separately, so a pass no longer prints "all N published-only descriptions are absent" when one of them is in the bundle.
  • scripts/build-console.sh (declared, and strictly needed by the route): the single assert call site passes --objectui "$BUILD_ROOT", the tree it just built. The only other way to find that tree is to reach two levels up from --vendored, which couples the assert to a path layout. A required flag means a forgotten call site fails loudly (exit 2).
  • scripts/check-console-injection.mjs: the replay logic is unchanged. It needs no objectui tree, because the build never stamps a host-carried probe. Its self-test gains battery 14, and batteries 12 and 13 now pass --objectui.
  • Same-class fix on the fresh leg, declared: the same rule applies to the injected-only witness. A witness that objectui writes itself is no proof of the injection. Before this change, a bundle that held objectui's literal and no spec at all passed; now it reads "neither spec appears" (exit 2). The four bounded-fix conditions all hold: same defect class, same function, a file nobody else has claimed, and the same gate family. It is pinned in battery 14.
  • The stamp shape is unchanged (stampVersion 1). The stamp now records the filtered choice, so a cache-hit replay agrees with the build.

Reproduction (base 7e0066af7a, the head of queue run 37187916146)

  • CI: queue run 37187916146, job 111393796807 ("Build the Console SPA at the pinned objectui SHA"). Its triage comment on docs(spec): re-anchor the dead tracker citations in packages/spec/src's test surface to the commits that decided them #21700 quotes ✗ Built console still carries the PUBLISHED @objectstack/spec. I could not read the raw job log from this container: the log blob host answered 403. So the per-candidate figure comes from the local build below, at the same commit.
  • Local, the real door: scripts/build-console.sh at 7e0066af7a, run under os-verify-lock.sh. It used objectui ab1879721595, and the vendored @objectstack/spec resolved to 17.6.0. Lock held 559s. Result: exit 1, (1 of 38 published-only descriptions), the first of them: "Extra vertical reference lines drawn like the Today marker ({ date, label?, color? })".
  • Measured, not assumed: over that real bundle (33.3 MB of JS), the published-only candidates present are exactly 1 of 38, and it is the markers text. objectui's tracked non-test source carries it. The injected-only candidates present are 26 of 26, and none of those is host-carried.

Route: (a), by measurement

  • (b), whole-literal matching, would fix today's collision. The markers text is not a whole literal in the real bundle.
  • It leaves half the family open. In the real bundle, 9 spec describes are whole literals inside objectui's own chunks, for example "Action IDs available for related records" in the plugin-grid chunk. Rewording any of those 9 would recreate this red under (b).
  • At the pin, over the module's own file filter, objectui's non-test code holds 18 literals equal to a spec describe and 18 more that begin with one.
  • (a) covers both shapes. It drops only text whose presence could not have been evidence anyway, and only when that text is in the bundle.
  • Leak detectability (pin 2 on the real bundle): I added the published 17.6.0 JS as a chunk to the real assets. The head assert exits 1 on 37 of 38, detector "Actions to execute during transition". Replaying the good build's stamp beside those assets also exits 1.
  • Cost: the host blob is 32.3 MB and reads in about 0.3 s.

Pins (battery 14, node scripts/check-console-injection.mjs --self-test)

Base: 44 assertions. Head: 67. Every fixture runs the real assert script and replays its stamp through evaluate(). The mirrored texts sort first, so an unfiltered pick would choose them.

  1. Pin 1 passes: the injected spec plus an objectui literal that begins with a published-only describe, plus a second literal equal to one (which exercises the quote-backslash normalisation). The stamp records staleDetector = a text objectui does not write. The replay passes.
  2. Pin 2 still fails: a bundle carrying the published spec's own JS fails with exit 1 and names a detector objectui does not write. No stamp is written. objectui's test file quotes that detector, so this case also pins that tests are not read as source.
  3. Pin 3, the replay agrees: the replay matches the assert on both bundles: 0 on bundle 1, and 1 ("carries the PUBLISHED") on bundle 2 with the good stamp beside it.

Battery 14 also covers:

  • the all-host-carried stale leg (exit 2, "cannot judge");
  • host-carried text absent from the bundle, which stays a valid detector (exit 0, stamped, replay 0);
  • the fresh-leg case (exit 2);
  • an objectui tree git cannot list (exit 2);
  • chooseProbes without hostBlob (TypeError).

Ablation. Predicted first and saved, then run through scripts/ablation-replace.mjs in wrap mode. The mutation was const hostCarried = new Set();. On disk the anchor went 1 to 0 and the marker 0 to 1. The restore was proven: blob 903c3e80cce1 equals HEAD, and git diff HEAD is empty.

  • Self-test: exit 1, with 13 failures, exactly the predicted list, all in battery 14 (pin 1 ×6, pin 2 detector wording ×2, pin 3 bundle-2 wording ×1, all-host-carried ×2, fresh leg ×2). No other battery failed.
  • Pin 2's exit 1 held with and without the fix.
  • Synthetic bundles under ablation: exit 1 1 of 38 with the markers detector, and exit 1 38 of 38.
  • Real bundle under ablation: exit 1 1 of 38 with the markers detector, and exit 1 38 of 38 with the published chunk added.

The real door on this head

  • End to end: scripts/build-console.sh on 93ea8e7d80 reused the built objectui tree, rebuilt the console at the pin, and ran the fixed assert. Exit 0, with 37 of 38 published-only descriptions are absent; 1 ... ARE in the bundle, and objectui's own source at the pin carries each of them too. Lock held 278s. The next CI steps then ran on that dist: pnpm check:console-sha exited 0, and pnpm check:console-injection --require-stamp exited 0 (self-test 67, replay passed).
  • That build was before the last commit. 93ea8e7d80 differs from the final head 0bcf6a0a95 only in a self-test fixture line in check-console-injection.mjs, and that self-test was re-run on 0bcf6a0a95.
  • PR side: all four changed paths are in ci.yml's console filter, so Console Pin Gate runs on this PR. Three of them (build-console.sh, the assert, the probes module) are in the dist cache key, so the key moves and the cache misses. The PR head therefore gets a full console build at the pin with this assert, and that is the CI reading of the real door.
  • Queue: once this merges, every queue build's Console Pin Gate runs this assert. release.yml's key hashes build-console.sh, so its next run rebuilds through the new call site too.

Gates (on 0bcf6a0a95)

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 32 commands. All 32 exited 0.
  • --ran reconciliation: 32 derived, 32 run, 0 NOT MEASURED, 0 unrun, every one with a recorded exit code. That includes pnpm check:console-injection, check:console-sha, check:ci-filter-parity (+ self-test), check:nul-bytes, check:entry-guard and check:pm-dispatch-gates (1976 cases, run detached).
  • eslint, narrowed: eslint.config.mjs lints all three changed .mjs files, so none is ignored. --format json reported 3 files, 0 errors and 0 warnings. The config has no type-aware linting, so this diff cannot move any untouched file's verdict. build-console.sh is outside eslint's population. The full pnpm lint is left to CI.

Changeset

skip-changeset: nothing published changes. The diff is root scripts/ only, and the root package is private and ships no files. I built the console dist (the one published package this build feeds) and grepped it: readHostSourceBlob / hostCarried have 0 hits, while the positive control, the markers literal, is found in 3 files.

Acceptance notes

  • Dead branch, not touched: the assert's "published spec is gone, but nothing unique ... was found" branch is unreachable, both before and after this change. The "neither" branch and the stale exit cover every way into it. Carrier: none.
  • Parity tests stay evidence: objectui's apps/console/src/__tests__/registry-inputs-spec-parity.test.ts quotes 2 of the 38 published-only describes. Tests are excluded from the host source, so in a real leak those two still count as evidence.
  • Same-tree pin: the reproduction and the real-door runs used the same tree as the failing queue run, 7e0066af7a (its head_sha). origin/main has since moved by four commits, and none of them touches this gate's inputs.

Generated by Claude Code

claude added 3 commits October 4, 2026 09:40
…n source carries

WIP: self-test battery follows.

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
…ence, on the build and the replay

Battery 14 of check-console-injection's self-test: the injected spec plus an
objectui literal beginning with (and one equal to) a published-only
description passes and stamps a detector objectui does not write; the
published spec itself still fails; the replay agrees on both bundles.

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
…he good build wrote no stamp

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Oct 4, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 4, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 4, 2026 10:56
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 4, 2026 10:56
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit a2b7328 Oct 4, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21709-console-probe-collision branch October 4, 2026 11:30
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…tead of a tracker number (stage 11) (objectstack-ai#21736)

Part of objectstack-ai#20749
Clause-②: no

Stage 11 of this card, and the second area of class (e): the test
strings shipped under `packages/spec/src`, as ruled in `5902360492` on
objectstack-ai#20513. This stage takes the whole `kernel/` directory. Its 102
test-title and test-message literals carried 106 tracker ids citing 69
records. Each id now either states what its record decided, in words
(form D), or is dropped where the title already says it. Text only: no
assertion, fixture value, test count or code comment changes.

## Census at the base (`3fa850cf00`, the claim's base)

Instrument: stage 10's `census10.cjs` (md5
`9d08602ab972b4b8643c90d64d40fa41`) and stage 9's `census.cjs` (md5
`6e42a45a926d375013c32d62f16a296e`), both byte-identical to the copies
stage 10 used. A literal counts as a test title when its folded message
is argument 0 of a `describe` / `it` / `test` call, `.each` / `.skip` /
`.only` chains included. Everything else is an "other" string.

Both instruments read **1696 messages / 1807 ids in 405 files at the
base**, which is stage 10's reading at its head exactly. `kernel/` reads
102 / 106, also stage 10's figure.

| directory | files | messages / ids | titles | other |
|:--|--:|--:|--:|--:|
| `data/` | 95 | 468 / 501 | 445 / 475 | 23 / 26 |
| `ui/` | 81 | 392 / 415 | 374 / 397 | 18 / 18 |
| `api/` | 40 | 189 / 201 | 181 / 193 | 8 / 8 |
| `system/` | 34 | 154 / 165 | 128 / 138 | 26 / 27 |
| (files directly in `src/`) | 30 | 118 / 120 | 117 / 119 | 1 / 1 |
| **`kernel/`** (this PR) | 36 | **102 / 106** | 92 / 96 | 10 / 10 |
| `shared/` | 21 | 85 / 95 | 73 / 81 | 12 / 14 |
| `contracts/` | 25 | 63 / 74 | 59 / 70 | 4 / 4 |
| `conversions/` | 9 | 34 / 34 | 34 / 34 | 0 |
| `security/` | 8 | 28 / 28 | 28 / 28 | 0 |
| `ai/` | 9 | 18 / 20 | 13 / 15 | 5 / 5 |
| `identity/` | 6 | 15 / 15 | 14 / 14 | 1 / 1 |
| `integration/` | 4 | 14 / 14 | 13 / 13 | 1 / 1 |
| `migrations/` | 2 | 9 / 12 | 9 / 12 | 0 |
| `marketplace/`, `meta-spelling/`, `studio/` | 5 | 7 / 7 | 7 / 7 | 0 |
| **total** | **405** | **1696 / 1807** | **1587 / 1692** | **109 /
115** |

- **Controls.** Lit, a title:
`kernel/capability-metadata-kind.test.ts:66` reads one message with
objectstack-ai#5961. Lit, a template-literal expect message:
`kernel/cli-command-contribution-retirement.test.ts:74` reads one
message. Dark: the `// ─── [objectstack-ai#17178] …` comment at
`kernel/execution-context.test.ts:205` reads 0 (the file's messages sit
at `:72`, `:167`, `:219`, `:233` and `:237`). Planted in a scratch copy:
an id added to a title reads 1 / 1, and an id in an added comment reads
0.
- **A wider pattern** (any `#` plus digits) reads 107 / 111 under
`kernel/` at the base. The five extra hits are hex colours (`#94A3B8`,
`#0f0`) in `functional-completeness.test.ts` and one `§3.10 objectstack-ai#3` section
reference in `manifest.zod.ts`, a non-test file. None is a tracker id.
At the head the wider pattern reads only those five, and the gate
pattern reads 0 / 0.
- **At the head:** 1594 messages / 1701 ids in 369 files. `kernel/`
reads 0 / 0. Nothing else moved.

## How the area was chosen

Stage 10's rule, applied before any card was read: rank whole
first-level directories by ids, and take the busiest one within about
10% of the ~100-id bound. The four busiest each exceed the bound alone:
`data/` (501), `ui/` (415), `api/` (201) and `system/` (165). The files
directly in `src/` (120) are 20% over. `kernel/` (106) is the busiest
whole directory within the bound, and its census reads exactly stage
10's 106, so the rule needed no second pass.

**Named for the next stages:** `data/` (about five stages, by
subdirectory or file group; `data/driver/` alone is 52), `ui/` (about
four), `api/` (two), `system/` (two), the files directly in `src/` (one,
120), `shared/` (one, 95), `contracts/` with `conversions/` (one, 108),
and `security/`, `ai/`, `identity/`, `integration/`, `migrations/`,
`marketplace/`, `meta-spelling/` and `studio/` together (one, 96).

## What each id became

Of the 106 ids, 32 now state a decision in words, in 31 literals. 74 are
dropped where the title already explains them; two of those (objectstack-ai#14478) sit
in literals that also gained words for another id. Every record was read
with its comments through REST. 58 answer 200. Ten answer 404, and their
decisions were read from what landed. One is in a repository not
attached to this session.

| record | ids | result |
|:--|--:|:--|
| objectstack-ai#12007, objectstack-ai#11825, objectstack-ai#12340, objectstack-ai#4914, objectstack-ai#15932, objectstack-ai#11846, objectstack-ai#16059 | 10 of 20 |
Every expect message that read "must have zero holders after #N" or
"must not be exported after #N" now reads "after its retirement": each
record retired the names it lists. The other 10 sit in `describe` / `it`
titles that already say what was retired, and were dropped. objectstack-ai#11846
answers 404; its retirement was read from the CHANGELOG entry for
landing `0c2334f`. |
| objectstack-ai#7280 | 1 | "the ADR-0069 gate posture is a declared field":
`authGate` is declared on `ExecutionContextSchema`, not spread behind an
`as any`. |
| objectstack-ai#17178 | 1 | "SEED_WRITE_EXECUTION_CONTEXT — one spelling of the seed
posture": one exported constant replaced the private copies. |
| cloud#687 | 1 | "(the founding case: a roll-up that reads 0 forever)".
The cloud repository is not attached to this session (403). The decision
was read from ADR-0078's "Surfaced by" line and the CHANGELOG paragraph
on the founding case: a bare `{ type: 'summary' }` field read 0 forever,
and the rule now flags it as an error. |
| objectstack-ai#14192 (404) | 4 | "(the silent-drop measurement, inverted)", where
the title said "the card's measurement". Dropped from 3 titles that
state the refusal. Decision read from landing `4d0d944`:
`ManifestSchema` goes strict and refuses unknown keys inside
`manifest:`. |
| objectstack-ai#10726 (404) | 2 | "removed for the `http.server` mount,
maintainer-ruled 2026-08-22", where the title said "Option B". Read from
landing `bc56e18` and PR objectstack-ai#12417's body: Option B removes
`contributes.routes` and points authors at the imperative `http.server`
mount. Dropped once. |
| objectstack-ai#4148 | 1 | "the object/field unknown-key warnings survive the
generalization", where the title said "the objectstack-ai#4148 behaviours". |
| objectstack-ai#4001 | 3 | "(the evidence base for the strict tiers)", where the
title said "objectstack-ai#4001 evidence phase". Dropped from 2 titles that state the
pinned rule. |
| objectstack-ai#4167, objectstack-ai#8687 | 3 | "top-level stack keys (named, then refused at
parse)": objectstack-ai#4167 made an undeclared top-level key say so instead of
vanishing, and objectstack-ai#8687 ruled Shape B, a strict top level. Dropped once
more for objectstack-ai#8687. |
| objectstack-ai#15624 | 2 | "cache.ttl → deleted (the unread outer block is
retired)". Dropped once. |
| objectstack-ai#14478 | 3 | Dropped. The `→ ttlMs` / `→ timeoutMs` renames and "carry
their unit" already state the rule: the unit lives in the key name. |
| objectstack-ai#15939 | 1 | "RuntimeConfig.resourceLimits.timeout → timeoutMs (its
unit was named in JSDoc only)", where the title said "ruling A". Ruling
A renames the keys whose unit was named only in JSDoc, per file. |
| objectstack-ai#5086 | 2 | "(PUT /meta refuses the inlet)": a code-only kind's create
is refused with 403 `NOT_CREATABLE` before anything persists. |
| objectstack-ai#7743 | 1 | "UNCHANGED, the field overlay refusal stays", where the
title said "objectstack-ai#7743's overlay refusal". |
| objectstack-ai#8154 | 1 | "(the consumer contract of the per-type redaction hook)".
|
| objectstack-ai#21120 | 1 | "stored metadata ROWS — the family-wide seam every exit
routes through". This says only what the card's public summary says: one
shared seam, which every surface routes through or refuses. |
| objectstack-ai#6245 | 1 | "the bound-but-unregistered fence, pinned": schemas are
bound for those kinds WITHOUT registering them. |
| objectstack-ai#11263 | 1 | "PLATFORM_PLUGIN_WIRED_RUNTIMES — runtimes wired by
plugins[], not by a token": a sibling roster was added, and no token was
minted. |
| objectstack-ai#3366 | 1 | "classifyRequiredCapability — preflight for an installable
provider in this edition". |
| objectstack-ai#17676 | 1 | "package-registry carve-out — its persistence is
always-on core, split from `marketplace`", where the title said "ruling
A′". |
| objectstack-ai#16365 | 1 | "accepts %s, which the regex refused before the SemVer
widening", where the title said "the pre-objectstack-ai#16365 regex". The `%s` values
do not change. |
| objectstack-ai#17227 | 1 | "dashboard.header.actions stays titled — the first
carrier given item-level names". |
| dropped only (live) | 45 | objectstack-ai#3308 (2), objectstack-ai#3433, objectstack-ai#3760, objectstack-ai#3786, objectstack-ai#4212,
objectstack-ai#4509, objectstack-ai#4587, objectstack-ai#4657, objectstack-ai#4741, objectstack-ai#4834, objectstack-ai#4939, objectstack-ai#5488, objectstack-ai#5961, objectstack-ai#6881, objectstack-ai#6931,
objectstack-ai#7893, objectstack-ai#8586, objectstack-ai#10039 (2), objectstack-ai#11169, objectstack-ai#12032, objectstack-ai#12428, objectstack-ai#13613, objectstack-ai#15678 (7),
objectstack-ai#16328, objectstack-ai#16334, objectstack-ai#16449, objectstack-ai#17232 (2), objectstack-ai#17445, objectstack-ai#17780, objectstack-ai#18124 (2), objectstack-ai#18791
(3), objectstack-ai#19630, objectstack-ai#20102: each title already states the pinned decision. |
| dropped only (404) | 8 | objectstack-ai#10194 (landing `2306a76`: each bound entry
is its stack collection's schema), objectstack-ai#10338 (`d2619fd`: `target` optional,
the gate holds the flow requirement), objectstack-ai#10724 (`be21955`: the nine dead
members tombstoned), objectstack-ai#11330 (`a9ee98992`: the trust-tier text tells the
truth), objectstack-ai#11332 (`dce5cd4`: the three dead containers retired), objectstack-ai#13135
(`9e0ba21`: the paper customization protocol retired), objectstack-ai#17147 (2,
`aaacf1d5c`: the granted set is registered and refuses nothing, said
truthfully). Each title already carries what landed. |

## Readers

- **Test-name filters:** none. A tracked-tree search for `-t` and
`--testNamePattern` finds only `packages/qa/dogfood/README.md:142` (`-t
"owner-scoped"`), which is unrelated.
- **Snapshots:** none. `kernel/` has no `__snapshots__`, and no `.snap`
file is tracked under `packages/spec`.
- **Titles by substring:** every old title, plus a window around each id
(256 needles), was searched across the tracked tree outside its own
file. No gate, doc or script matches one. At the head, 8 hits remain:
three sibling titles in other lanes' or stages' files
(`metadata-protocol/src/protocol.capability-write-door.test.ts:183`,
`spec/src/api/contract.test.ts:813`,
`spec/src/system/auth-config.test.ts:520`), three released
`packages/spec/CHANGELOG.md` entries, and one code comment in
`kernel/metadata-authoring-lint.ts:268`, which belongs to the comment
lane.

## Text-only proof

Stage 10's scratch tool (`textonly10.cjs`, md5
`d5e4801dbb4329ab1984da91e92fc47c`) compares base and head file by file
on three legs:
1. **Skeleton:** the full AST, with string pieces masked. It must be
identical.
2. **Comments:** every comment, byte-equal.
3. **Strings:** each string leaf that changed must sit in a test-call
title position, or on one of the 10 declared lines. Those are the expect
messages at `cli-command-contribution-retirement.test.ts:74` and `:86`,
`plugin-lifecycle-advanced-retirement.test.ts:103`, `:124` and `:141`,
`plugin-loading-retirement.test.ts:125`,
`plugin-security-scan-result-retirement.test.ts:123`,
`preview-mode-retirement.test.ts:193` and
`startup-orchestrator-retirement.test.ts:85` and `:106`. Each changed
leaf must carry a tracker id before and no `#` plus digits after.

- **Result:** 36 of 36 files SAME, 102 changed (92 title, 10 declared),
on all three legs.
- **Diff hunks:** exactly the 102 planned lines, with every file keeping
its line count.
- **Controls (10 of 10 as predicted, on scratch copies, each anchor hit
once):** identifier rename DIFF; numeric literal DIFF; comment edit
COMMENT DIFF; a non-title string with an id VIOLATION; a rewritten title
given a new id VIOLATION; a title that was id-free at base edited
VIOLATION; one title reverted to base SAME; a declared string keeping an
id VIOLATION; an undeclared expect message changed VIOLATION; a title
re-split into a `+` chain DIFF.

**Test counts:** the 36 files were run at the base (in a separate base
worktree) and at the head: 841 / 841 tests on both sides, with the same
count and status sequence per file in 36 of 36. 388 full test names
change, and each equals the base name with the planned replacements
applied.

## Changeset: `skip-changeset`

Measured, not assumed:
- `npm pack --dry-run` of `@objectstack/spec` lists 2068 files under
`files[]`. 0 of the 36 touched files are in it, and 0 `*.test.ts` at
all. The controls `src/kernel/manifest.zod.ts` and
`src/kernel/execution-context.zod.ts` are in it.
- In `dist/`, three new phrases and three old ones each read in 0 files.
The control `Plugin compatibility ranges (ADR-0025` reads in 20.

So this PR publishes nothing, and no changeset is added.

## Verification (at `e0ad8f50af`)

- `pnpm turbo run build` over all packages: 71 / 71 (at the first
commit), then `@objectstack/spec` rebuilt at `e0ad8f50af`.
- `@objectstack/spec`: `vitest run --project local`, 613 files and 18215
passed, 1 todo. `typecheck` exit 0, including `check:test-typecheck`,
whose program holds all 36 touched files.
- **Gates:** `dispatch-gates --commands` derived 79 families at
`e0ad8f50af`, and all 79 exit 0. `--ran` reconciles: 79 derived, 79 run,
0 NOT-MEASURED, 0 UNRUN.
- **ESLint, a proven narrowing:** `--no-inline-config` over the 36
files, 0 errors and 0 warnings. The population comes from ESLint's own
config: 36 configured, 0 ignored. No `parserOptions.project` or
`projectService`, so no untouched file's verdict can move.
- `check-governed-merges --test`: NOT governed, 204 changed lines.

## Acceptance notes

- **Code comments still carry ids** in these 36 files and in the
`kernel/` sources, for example `execution-context.test.ts:205`,
`metadata-authoring-lint.ts:268` and `functional-completeness.ts:148`.
They are the comment lane's, untouched here.
- **A sibling title in another package** repeats `objectstack-ai#5961 — capability`
(`packages/metadata-protocol/src/protocol.capability-write-door.test.ts:183`).
It is that package's test-string stage, not this one.
- **The second commit** (`e0ad8f50af`) rewords one title from this PR's
first commit, "the one carrier already titled", which read as a
tautology, to "the first carrier given item-level names". Every proof
above was re-run at that head.
- **`origin/main` moved** three commits past the base before this PR
opened (objectstack-ai#21717, objectstack-ai#21715, objectstack-ai#21660). None touches `packages/spec`, so
nothing was merged.

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

---------

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

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants