Skip to content

test(spec): derive the always-on mount boundary instead of pinning slice(0, 6) - #11416

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11046-derived-always-on-pin
Aug 23, 2026
Merged

test(spec): derive the always-on mount boundary instead of pinning slice(0, 6)#11416
os-steve merged 1 commit into
mainfrom
claude/issue-11046-derived-always-on-pin

Conversation

@claude

@claudeclaudeBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes#11046

Replaces the literal slice(0, 6) always-on prefix assertion with the boundary derived from what the pin is for, and retires the comment that told the next author to grow the slate after the sixth entry.

The rule, stated once

Every always-on entry that is not one of the services others bind into at kernel:ready is mounted after all of them.

BIND_TARGETS = ['queue', 'job', 'cache', 'settings'] — a role, never a count. The retired six bundled those four bind targets together with two of their readers (email, storage) and stopped one short of the third, sms at index 6, whose position relative to settings was then held by nothing. Bumping the 6 to a 7 would have been the same defect moved one position, so the count is gone entirely.

The rule lives in one predicate (orderingViolations) shared by the pin and its positive control, so the two cannot drift apart.

Reverse verification — is the derived form actually stronger?

Measured, not asserted. The separating mutation models a new bind target arriving: realtime joins both the slate and BIND_TARGETS. That is the class the literal pin structurally could not see, since it constrained indices 0-5 and nothing else.

limbold slice(0, 6)new derived rule
mutated slatePASSES — blind to itREDS, 1 failed / 21 passed
restoredpassespasses, 22/22
=== 3. PROVE mutation reached disk (both MUST be >=1) ===
src:1 tst:1
=== 4. OLD slice(0, 6) pin against the MUTATED slate ===
slate now : [...,"messaging","realtime","analytics"]
OLD slice(0,6) : ["queue","job","cache","settings","email","storage"]
OLD PIN VERDICT: PASSES — blind to this
=== 5. MUTATED limb: does the NEW derived pin red? ===
MUTATED_VITEST_EXIT=1
× mounts every other entry after ALL bind targets — derived, so tomorrow's entry is covered on arrival
Tests 1 failed | 21 passed (22)
=== 6. restore + PROVE it (both MUST be 0) ===
src:0 tst:0
=== 7. RESTORED limb: back to green? ===
RESTORED_VITEST_EXIT=0
Tests 22 passed (22)

Marker counts were 0 before the mutation and 1/1 after, so the mutation provably landed rather than being a no-op; the script restores from a trap so a mid-run kill cannot leave the tree mutated. No rebuild limb is needed here and none is claimed: the test imports ./platform-capabilitiesrelatively, so vitest transforms the source directly and no dist/ sits between the mutation and the assertion. (Contrast serve-settings-ordering.pin.test.ts, which reads built plugin classes and does need one.)

Where this is deliberately not a superset — please read

The derived form is strictly stronger on the invariant the pin exists for, and it is unbounded in slate length. It also stops asserting two things the literal form asserted only incidentally:

  • peer order among the bind targets (queue before job before cache), and
  • peer order between email and storage.

Nothing binds queue into job at kernel:ready, so neither was ever a mount-order constraint — pinning them was the same literalism this card retires. Bind-target membership is still asserted, by a new floor case, because the ordering rule would otherwise constrain less as targets went missing and constrain nothing at all if every one of them did.

Reader membership (email/storage/sms being on the slate) is no longer asserted here, and is not lost: serve-settings-ordering.pin.test.ts case 7 asserts exactly that, from the real plugin classes.

The "two homes" question the card asks the implementer to answer

Keep the @objectstack/cli derived pin. Not as tolerated duplication — the two are complementary and neither subsumes the other:

  • this file pins slate order. packages/spec sits below every plugin package, so it cannot import the plugin classes and therefore cannot verify that BIND_TARGETS still names the right services.
  • the CLI pin keeps that membership honest — it resolves the real plugin classes, proves each shipped settings reader declares the ordering edge, and proves the declaration is what moves resolution order (ADR-0116 / ADR-0049).

That divergence is real and is now written into this file's docblock rather than left to be rediscovered: a future service that others start binding into at kernel:ready has to be added to BIND_TARGETS by hand, and nothing in packages/spec will notice if it is not. The CLI pin is where that gap is closed.

Scope — one declared expansion beyond the card's stated file surface

The card's premise named the inviting comment at platform-capabilities.test.ts:137. There is a twin in the source file, at platform-capabilities.ts:204, and it is the load-bearing one — it sits directly above the array an author edits:

- // The first six are the pinned foundational prefix — grow the slate AFTER them.

Retiring the instruction in the test while leaving it verbatim above the declaration would have left the card's actual target — "the comment that invites the next author to grow the slate after the sixth entry" — in place at the one spot most likely to be read. It is retired here and the rule stated in its place.

The change is comment-only. Proved mechanically rather than by eyeballing the diff — the slate's token sequence is byte-identical before and after:

BEFORE: ["queue","job","cache","settings","email","storage","sms","sharing","messaging","analytics"]
AFTER : ["queue","job","cache","settings","email","storage","sms","sharing","messaging","analytics"]

Flagging it rather than assuming the expansion was wanted: this is one line of prose outside the declared surface, trivially revertible, and I would rather it be reviewed than quietly shipped.

packages/cli was not touched, per the ruling on the card.

Out of scope, filed not fixed

#11415packages/cli/test/serve-defaults.test.ts carries a third literal slice(0, 6) pin on Serve.ALWAYS_ON_CAPABILITIES (a re-export of this same slate), under a comment that states the retired instruction more explicitly than either comment this PR touched. Fenced off by this card's ruling, so it is recorded there and left alone here. That issue stays open; nothing in this PR addresses it.

Verification

Union re-run on the final commit 61be8f8dfd.

gateverdict
@objectstack/spec full suite419 passed (419) files, 11138 passed (11138) tests
@objectstack/spec typecheckcommand-exit 0tsc --noEmit, check:scripts-typecheck, check:test-typecheck (OK — test layer compiles)
check:nul-bytesEXIT=0
check:engine-double-contractEXIT=0368 (file, verb) row(s) held by the RETAINED ledger
check:where-matcherEXIT=00 silently-wrong and 0 unjudged matcher(s); none new
check:cross-package-test-inputsEXIT=0OK: 14 package(s) read outside themselves, all declared
check:spec-parsed-alias · check:test-source-alias · check:query-options-erasureEXIT=0
check:empty-state · check:liveness · check:strictness-ledger · check:variant-docsEXIT=0

Gate families derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, not from a recalled list. Exit codes were captured before any pipe.

Narrowed and declared: check:type-check-debt --re-measure and check:type-check-coverage are left to CI. Both are moved by a new test file; this diff adds none — it edits a file already inside packages/spec/tsconfig.test.json, whose test layer compiles clean above. Re-measuring needs the whole workspace closure built, which CI does anyway.

skip-changeset: this diff publishes nothing — one test file and one comment, with the slate's tokens proved unchanged.

Clause-② path limb fires on packages/spec/src/**, so this will be held for contract review. Expected: the diff changes no contract acceptance behaviour.


Generated by Claude Code

…ice(0, 6)
The literal prefix assertion bundled the four bind targets (queue/job/cache/
settings) together with two of their readers (email/storage) and stopped one
short of the third — sms, at index 6 — whose position was then held by nothing.
The comment above it told the next author to grow the slate AFTER those six,
which is exactly how sms came to be added outside the pin.
State the rule instead: every always-on entry that is not one of the services
others bind into at kernel:ready is mounted after all of them. Retire the
inviting comment in the test AND its twin above the declaration itself.
Part of #11046
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9cDbY2NBiVJWYx3BpWfH2
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 23, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 1 documentable anchor(s).

1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via PLATFORM_ALWAYS_ON_CAPABILITIES (symbol))

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.

What this run could not see

Coarse fallback — 126 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 bb006446adb558e5a86c009df9c0b08e98833011packageMentionDocs.

Which tree this was computed on

This run read content/docs from fc21071ce0c2a1f6ffab49884f727e223ed4961a — the merge of head 61be8f8dfd1fe5d9a9e138be4de0ab71fd25cebd into base bb006446adb558e5a86c009df9c0b08e98833011, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fc21071ce0c2a1f6ffab49884f727e223ed4961a && git checkout fc21071ce0c2a1f6ffab49884f727e223ed4961a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bb006446adb558e5a86c009df9c0b08e98833011 61be8f8dfd1fe5d9a9e138be4de0ab71fd25cebd && git checkout -B drift-repro bb006446adb558e5a86c009df9c0b08e98833011 && git merge --no-ff 61be8f8dfd1fe5d9a9e138be4de0ab71fd25cebd
node scripts/docs-audit/affected-docs.mjs --json bb006446adb558e5a86c009df9c0b08e98833011

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs bb006446adb558e5a86c009df9c0b08e98833011 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-steve
os-steve marked this pull request as ready for review August 23, 2026 16:59
@os-steve
os-steve added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit 68e8b4bAug 23, 2026
42 checks passed
@os-steve
os-steve deleted the claude/issue-11046-derived-always-on-pin branch August 23, 2026 17:21
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants

@os-steve@claude