Skip to content

feat(qa): os test prints suite and scenario names and selects scenarios by --tags - #20341

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-20289-os-test-names-tags-requires
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-20289-os-test-names-tags-requires

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Part of #20289

Clause-②: no

Rework round 1

The seat's order of record is comment 5861125164 on #20289, which also amended the claim's file surface to these three files, text only. This PR had made three texts false or stale. Commit 5d31b4d587 corrects them. It is a new commit on top of dd6b472c63, with no rebase, amend or force-push.

  1. docs/qa/platform-checklist/areas/cli.json, item cli.qa-suite-execution:
    • The knownGap that read "no scenario SELECTION exists: os test has exactly three flags …" now says what stays true. --tags selects scenarios (any-of, exact, case-sensitive). Deselected scenarios are counted and never passed. requires is declared but NOT CHECKED, pending the maintainer's decision on qa: os test prints suite and scenario names, filters by tags, and skips on unmet requires (5 keys) #20289.
    • The item's source note on qa.json#tags said the same false thing ("its dead tags/requires rows are why no scenario selection exists"), so it is corrected in the same edit.
    • revision goes from 4 to 5 with a history entry, as the checklist's change rule requires.
  2. content/docs/deployment/cli.mdx, § os test:
    • Documents --tags: comma list, any-of, exact and case-sensitive. Deselected scenarios are counted and never passed. A listed tag no loaded scenario carries is named, and an empty entry is refused. A selection that matches no scenario exits 0, or 1 under --fail-on-empty.
    • Documents the report lines: the suite heading NAME (FILE), the scenario line NAME [ID], and a failed scenario's description.
    • States that requires is not checked.
    • Adds one example line. The page agrees with os test --help, read at this head.
  3. packages/spec/scripts/liveness/check-liveness.mts, the qa paragraph of the header comment: suite.name and scenario.tags now have readers in os test. scenario.requires is the one key still unread: declared, NOT CHECKED, and its ledger row stays dead.

This round changes no code, and the requires question stays with the maintainer.

Verification at 5d31b4d587:

  • dispatch-gates --repo objectstack-ai/objectstack --commands was re-derived for the widened diff. It now names 118 families: the 114 of round 0 plus check:cli-examples-parity, check:pm-dispatch-gates, check:pm-governed-merges and bare-root-worklist.mjs --self-test.
  • The first pass ran while the fresh worktree's prerequisites were still being rebuilt. The gates that read built output answered exit 3 (or exit 1 for check:dts-closure, "declarations missing").
  • A clean second pass ran every family except check:pm-dispatch-gates once the rebuild had finished. All 117 exited 0. check:pm-dispatch-gates reads no built output; it exited 0 on the first pass after 1270 s.
  • Reconciliation with --ran (every line is COMMAND :: exit CODE): 118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN.
  • check:platform-checklist: OK — 15 areas, 266 items.
  • The edited script's own tests are the 8 files that name check-liveness, 6 of them beside it in scripts/liveness/. All passed: 8 files, 262 tests.

os test now reports the suite and scenario names an author writes and selects scenarios with --tags. That makes four of the qa-runner family's five ledger keys live. The fifth, TestScenario.requires, is not guessed. This PR leaves it dead with an honest describe(), and the report on #20289 puts the question to the maintainer. The card stays open for that half. This PR does not deliver the "unmet requires is SKIPPED with its reason" leg or its pin.

What changes

  • @objectstack/core: QA.TestResult gains scenarioName and description on both result envelopes: the completed run and the setup-failure early return. It also gains suiteName on every result that runSuite produces. suiteName is absent only from a lone runScenario call, which has no suite.
  • @objectstack/cli (os test):
    • The suite heading is 📄 Running suite: NAME (FILE). A file that fails to load is still headed by the file alone.
    • Each scenario line is Scenario: NAME [ID] (Nms), and shows the id once when the name and id are equal.
    • A failed scenario prints its description under its line.
    • A new --tags TAG[,TAG...] flag. Deselected scenarios are counted on the summary and never counted as passed. A requested tag that no loaded scenario carries is named there. An empty entry is refused at parse time.
  • @objectstack/spec: only the describe() of requires, requires.params and requires.plugins changed. They now say NOT CHECKED instead of reading as a guard, and the reference page was regenerated. In liveness/qa.json, name, scenarios.name, scenarios.description and scenarios.tags move to live and cite their readers as file#symbol. tags also cites a producer, the --tags value. requires stays dead with a re-measured note. The state-counts.md row moved from live 4 / dead 5 to live 8 / dead 1, and the README row was rewritten to match.

--tags semantics: any-of, exact, case-sensitive

A comma list selects a scenario that carries at least one listed tag. Why this form:

  • It is the reading of a comma list in Odoo's --test-tags, a metadata platform, and in Cucumber's comma form.
  • It is the everyday use of Playwright's --grep "@smoke|@critical": name the sets you want and get their union.
  • An all-of form or an expression language (Cucumber tag expressions, pytest -m) is a grammar to specify, parse and report errors against. No suite has needed one. The only suite in this repo, examples/app-showcase/qa/platform-smoke.test.json, tags one scenario smoke, crud.

Matching is exact and case-sensitive, because a tag is a name the author chose, not a pattern. With the flag, an untagged scenario is never selected. Without the flag, nothing changes: the selection is not consulted and every scenario runs. The help text states all of this.

Exit status, and the convention it follows

The repo's os test convention is that a run that executed nothing is not a failure by default, and --fail-on-empty is the opt-in strict reading. It is stated in Test.description in packages/cli/src/commands/test.ts and in content/docs/deployment/cli.mdx, § os test: "A pattern that matches no suite is not a failure by default".

A --tags selection that matches no scenario takes that same posture:

  • It prints No scenario matched --tags X. and exits 0.
  • Under --fail-on-empty it exits 1. The flag's description now names both cases.

A renamed tag in a CI step is the same trap as a renamed directory, and it gets the same opt-out. The Found N test suites., SUCCESS: All N scenarios passed. and FAILED: N scenarios failed. M passed. lines keep their spelling. The platform checklist item for os test reads the latter two.

Measured before and after

The target was a stub HTTP server that answers every request with 200. The suite had four scenarios:

  • one tagged smoke;
  • one tagged regression;
  • one declaring requires: { plugins: ["plugin-that-does-not-exist"], params: ["OS_TEST_PARAM_THAT_IS_NOT_SET"] };
  • one plain control.
probe base 10ea9eb2ed this branch
suite heading 📄 Running suite: probe.test.json 📄 Running suite: Probe suite: names, tags and requires (probe.test.json)
scenario line ✅ Scenario: s-tagged-smoke (28ms): the id only (control: the id is printed) ✅ Scenario: A scenario tagged smoke [s-tagged-smoke] (23ms)
--tags smoke exit 2, Nonexistent flag: --tags (control: --fail-on-empty parses, exit 0) exit 0; 1 scenario run; --tags smoke selected 1 of 4 scenarios; 3 deselected (not run, not counted as passed).
unmet requires ✅ PASSED, same as the no-requirements control unchanged, deliberately: see the next section

requires: why it is left dead and put to a decision

The dispatch rule was: "A sub-key you cannot judge honestly is a needs_decision, and ⛔ you don't guess." Neither sub-key can be judged honestly today:

  • plugins: os test reaches the target only over HTTP, through HttpTestAdapter. No server surface lists the loaded plugins. The discovery document (DiscoverySchema) advertises services and capabilities, not plugins. /.well-known/objectstack does not list them either. kernel.getPlugins() is in-process only. The spelling is also undefined: the npm package name or plugin.name.
  • params: it is undefined whose environment is meant. The runner's process.env is observable, but no step can consume a runner variable, because resolveVariables interpolates only captured context. The server's environment is not observable at all.

The decision is posted on #20289 with its options and the four-axis analysis. Until it is taken, the describe() says NOT CHECKED rather than implying a guard. The PR contains no speculative SKIPPED machinery, because every enforce option needs it and the retire option would leave it dead.

Verification

Every run below is at dd6b472c63, the final commit.

  • core:
    • pnpm --filter @objectstack/core test: 56 files and 1483 tests passed. src/qa/runner.test.ts has 20 tests, 3 of them new: names on every result, names on the setup-failure envelope, and a lone runScenario with no suite.
    • pnpm --filter @objectstack/core typecheck: exit 0.
  • cli:
    • vitest run --project unit: 227 of 230 files passed on the first run. Two files failed because packages/cli was not built yet. One file, hook-timeout-override-refusal.test.ts, timed out twice at 5000 ms under an 861 s shared-box run. After the CLI build, those 3 files passed 33 of 33 on a re-run.
    • The new test/qa-tags-selection.test.ts (unit) has 13 tests, including the unfiltered CONTROL.
    • The new test/qa-names-and-tags-run.test.ts (integration: it spawns bin/run-dev.js against a node:http stub) has 8 tests. They pin the names printed, --tags smoke exiting 0 because the failing scenario never ran, the unfiltered control exiting 1, a no-match selection exiting 0 by default and 1 under --fail-on-empty, and a malformed list refused before anything runs.
    • pnpm --filter @objectstack/cli typecheck: exit 0.
  • spec:
    • vitest run src/qa: 26 tests passed.
    • check:generated: check:docs was the one stale artifact. --fix regenerated it, and it is clean on re-check.
    • check:liveness: green, with qa 9 classified (live 8, dead 1).
  • Ablation (one leg, node scripts/ablation-replace.mjs, WRAP mode):
    • The mutation was return hits.length > 0; → return true; in selectScenariosByTags. The tool reported anchor 1 -> 0, and the blob went from 774d2436cf4f to cab531acb335. That is the only mutation.
    • Unit: 3 red out of 13. They were the any-of, exact-match and no-match pins. The CONTROL stayed green.
    • Integration: 3 red out of 8. They were --tags smoke, --tags regression and no-match. The unfiltered control and the names pins stayed green.
    • The direction was the expected one: the tests went red.
    • Restore: the blob after restore equals HEAD (774d2436cf4f), and git diff HEAD is empty.
    • The subject is imported through a relative path to src/, so no dist/ leg applies.
  • Gates: there are 114 derived commands (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands).
    • 108 exited 0 on the first pass.
    • 6 answered PREREQUISITE NOT MET (exit 3), which means not measured. After the missing builds, they were re-run: all 6 exited 0. They were check:skill-examples, check:dual-build-cjs-loads, check:i18n, check:i18n-coverage, check:i18n-walk-parity and check:type-check-debt (the last reported --re-measure: OK … none above its recorded number). The first pass of check:type-check-debt had died in its own internal build: plugin-pinyin-search could not find @objectstack/objectql declarations while a prerequisite build was rewriting dist/. The gate labelled that pass not measured itself.
    • Reconciliation with --ran (every line is COMMAND :: exit CODE): 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN.
  • Lint, measured narrowly:
    • pnpm exec eslint --no-inline-config --format json over the 6 changed TypeScript files: 6 results, 0 errors, 0 warnings.
    • The population comes from eslint's own config: isPathIgnored is false for all 6, and the 5 changed JSON, Markdown and MDX files are ignored.
    • Invariance: this repo's config enables no type-aware linting. parserOptions.project and projectService resolve to null for all 6, so this diff cannot move any untouched file's verdict.

Acceptance notes

  • The three texts that were outside the claimed file surface in round 0 (cli.mdx, the checklist knownGap and the checker's comment) are corrected in Rework round 1 above, after the seat amended the surface in 5861125164.
  • Semver:
    • @objectstack/cli is minor, for the new flag.
    • @objectstack/core is also minor, not patch. QA.TestResult gains three public fields, which is an additive API that programmatic consumers can read.
    • @objectstack/spec is patch, because only describe text and ledger data changed.
    • All three are in the same changesets fixed group.
  • Branch base: origin/main at d498113b50 is merged in with scripts/pm/os-regen-merge.sh, as merge commit 9056455134. The one conflict was packages/spec/liveness/README.md: main's translation and rest_api rows and this PR's qa row are all kept. The generated state-counts.md was regenerated, not hand-resolved, in 76e9a48821: qa is 8/1 on top of main's totals. Against that merge, git diff d498113b50 HEAD touches only this PR's 14 files.

Generated by Claude Code

TestResult now carries suiteName, scenarioName and description; os test
prints the suite name and each scenario's name beside its id, prints a
failed scenario's description, and gains a --tags flag (comma list,
any-of) whose deselected scenarios are counted and never reported as
passed.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
…ger rows live

qa.json: suite name, scenario name, description and tags flip to live
citing their readers; requires stays dead with a re-measured note, and
its describe() now says NOT CHECKED instead of reading as a guard.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
…names and --tags

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Generated by `pnpm --filter @objectstack/spec check:generated --fix`
(check:docs was the one stale artifact).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/core, @objectstack/spec, touching 16 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/liveness/README.md, packages/spec/liveness/qa.json, packages/spec/liveness/state-counts.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx (via os test (command, read off packages/cli/src/commands/test.ts))
  • content/docs/protocol/kernel/lifecycle.mdx (via os test (command, read off packages/cli/src/commands/test.ts))
What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/liveness/README.md, packages/spec/liveness/qa.json, packages/spec/liveness/state-counts.md) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 146 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 d498113b505f7b29b5e2c554f3152145edeb3461 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 51b76c46ea20de097e32027226e3e8f292d09060 — the merge of head 5b15c28fb146529543f02290ce18bf81d0f63df8 into base d498113b505f7b29b5e2c554f3152145edeb3461, 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 51b76c46ea20de097e32027226e3e8f292d09060 && git checkout 51b76c46ea20de097e32027226e3e8f292d09060
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d498113b505f7b29b5e2c554f3152145edeb3461 5b15c28fb146529543f02290ce18bf81d0f63df8 && git checkout -B drift-repro d498113b505f7b29b5e2c554f3152145edeb3461 && git merge --no-ff 5b15c28fb146529543f02290ce18bf81d0f63df8

node scripts/docs-audit/affected-docs.mjs --json d498113b505f7b29b5e2c554f3152145edeb3461

⚠️ 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 d498113b505f7b29b5e2c554f3152145edeb3461 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…lsify

cli.mdx documents --tags and the new report lines; the platform
checklist's os test knownGap and qa.json source note say what stays
true (tags select, requires NOT CHECKED), revision 5; the liveness
checker's qa comment names requires as the one key still unread.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 68/68 CONTRACT_REVIEW_TIER
Head-sha: 5d31b4d587c922595840651596b8e8125116dbd6

① Derived judgments

  1. Four keys live, readers real. qa.name: runner.ts#runSuite spreads suiteName: suite.name onto every suite result; test.ts#suiteHeading prints 📄 Running suite: NAME (basename). scenarios.name/description: runner.ts#runScenario stamps scenarioName/description on BOTH envelopes (setup-failure return and completed run); test.ts#scenarioLabel prints NAME [ID], id once when equal; description printed only under a ❌ line (test.ts#run). scenarios.tags: test.ts#selectScenariosByTags, producer #parseTagsFlag. Every path#symbol anchor resolves under evidence.mts#isSymbolNamed; Spec property liveness green. Nit: README row says the name "rides every TestResult"; a lone runScenario result has no suiteName (qa.json's "every TestResult the suite produces" is the exact wording).
  2. --tags: any-of/exact/case-sensitive is tags.includes(tag) over scenario.tags ?? []; comma list trimmed, deduped; an empty entry throws inside the flag parse, before any suite loads. Deselected scenarios never reach runner.runSuite, so totalPassed cannot count them; they are counted on tagSelectionLine. No-match branch tags && totalSelected === 0 exits 0, or 1 under --fail-on-empty, and is reached only when totalFailed === 0: a load-refused or throwing suite still takes FAILED/exit 1. Same posture as the pre-existing Found 0 test suites. branch and Test.description (os test: 5 of the 8 declared action types cannot reach a stock server (wrong base path), and a zero-match glob exits 0 #7848). Pinned: qa-tags-selection.test.ts (13, incl. CONTROL) and the spawn pin qa-names-and-tags-run.test.ts (8: control exits 1; --tags smoke exits 0 with the failing id absent; no-match 0/1; malformed non-zero with no suite run). cli test is vitest run over all tiers, Test Core shards green. No path reads a filtered-out or erroring scenario as passed. Non-blocking: (a) the unknown-tag warning is gated on a positive totalSelected, so --tags X matching nothing beside a load-refused suite prints only --tags X selected 0 of N; (b) a malformed list exits 1 via oclif handle() (err.oclif?.exit ?? 1; parse.js rethrows the plain Error; no exitCodes.failedFlagParsing configured), not the CLIParseError exit-2 + INVOCATION ERROR line an unknown flag gets (invocation.ts#isInvocationError); the pin asserts non-zero only.
  3. TestResult gains suiteName?, scenarioName (required), description?: additive for readers. Grep at head (objectstack + objectui): the only consumer is scenarioLabel via Pick; no producer outside runner.ts; TestExecutionAdapter untouched; check:api-surface green. Printed lines match the docs. Three new core pins cover both envelopes and the lone runScenario.
  4. requires: the zod hunk is describe text plus one comment; row stays dead, verifiedAt 2026-09-27, note records the stub re-measure. No text in the diff claims a check (changeset, cli.mdx, cli.json, check-liveness.mts, README, generated mdx all say NOT CHECKED / nothing checks). The describe ("NOT CHECKED by os test or the core TestRunner: the scenario runs whether or not they hold…") is unambiguous. Nit: cli.mdx, cli.json and the changeset say a scenario naming a missing plugin "still runs, and fails on whatever the missing plugin causes"; it can also pass (the PR's own probe did). The describe's "surfaces only as the failure it causes" is the accurate form.
  5. cli.json: knownGap and the qa.json#tags source note are true at head; revision 4→5 with a history entry; steps/acceptance unchanged and clause 1 ("✅ with its scenarioId") still holds (fixture names ≠ ids, so the id is bracketed). Nit: the history says the id is "now in brackets after the name"; it is bare when equal. cli.mdx: every sentence matches the code and agrees with Test.description and the --tags/--fail-on-empty flag text; quoted lines equal tagSelectionLine and No scenario matched --tags …. check-liveness.mts: comment only, true.
  6. testing.mdx carries the describe strings verbatim; check:docs runs at lint.yml:5564 (Lint & Repo Gates green). state-counts.md qa 8/1, total 937/164/1119: hand recount of qa.json agrees; check:liveness byte-checks the artifact (countsArtifactErrors), green. Exact at the PR tree; stale once main is merged (③).
  7. Changeset: every sentence true (nit in 4). cli minor ✓ (new flag). core minor ✓ (three public fields on exported QA.TestResult; the required scenarioName would break only a producer, and none exists). spec patch ✓ (no shape change). All three sit in one fixed group (69 packages). Clause-②: no is its own line in the changeset and the PR body; readClause2Line reads declared/no/no arm. The zod diff widens no accept set.
    CI at head: 35 check-runs, 33 success, 2 skipped (Console Pin Gate, opt-in tarball smoke), 0 failed, run 00:15–00:38Z.

② Semver level

minor. @objectstack/cli minor (new --tags), @objectstack/core minor (additive public fields), @objectstack/spec patch (describe text and ledger data; check:api-surface green); one fixed group ⇒ minor release. Clause-②: no with no arm per scripts/pm/clause2-line.mjs: nothing widens or narrows an accept set, so AGENTS.md's "yes takes at least minor / (narrowing) is BREAKING" rules do not bite. Check Changeset green.

③ Boundary flags

  • requires: triage 5859588706 ENFORCE covers all five keys, including "unmet requires reported skipped with its reason, never passed". This PR delivers four, leaves the row dead with an honest describe, and opens Part of #20289; the maintainer's word is owed on the card, not in this PR.
  • git merge-tree --write-tree origin/main origin/claude/issue-20289-os-test-names-tags-requires (main = 26daf0b) is NOT clean: CONFLICT (content) in packages/spec/liveness/README.md (main 826f327 at 00:23Z edited the translation row :929 and 26daf0b at 01:26Z the rest_api row :939; the PR edits the adjacent qa row :930), and state-counts.md is "generated, not text-merged" (main total 936/166/9/1117 vs PR 937/164/12/1119). GitHub mergeable_state: dirty; CI ran before both commits. Fix: merge main, keep both README rows, run pnpm --filter @objectstack/spec gen:liveness-counts.

Implemented-by: claude/issue-20289-os-test-names-tags-requires
Reviewed-by: session_01QcAS3qiYYZNezaxZxaUdMV

VERDICT: PASS

…-test-names-tags-requires

# Conflicts:
#	packages/spec/liveness/README.md
The merge with origin/main took main's side of the generated
state-counts.md (os-regen step 2); gen:liveness-counts re-derives it:
qa 8/1 on top of main's totals.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Review nits, text only: a scenario naming a plugin the target lacks can
also pass, so cli.mdx, the checklist knownGap, the changeset and the
liveness README say the unmet requirement surfaces only as whatever
failure it causes, if any; the checklist history notes the id is bare
when it equals the name; the README qa row uses qa.json's wording for
suiteName (every TestResult the suite produces).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: 94/94 CONTRACT_REVIEW_TIER
Head-sha: 5b15c28fb146529543f02290ce18bf81d0f63df8

① Derived judgments

  1. Diff vs the new base d498113b50: the same 14 files, 624+/42−. A -U0 diff-of-diffs (hunk headers stripped) against the reviewed 10ea9eb2ed..5d31b4d587 differs only in: the changeset spec bullet (nit a); cli.mdx :1702–1703 (nit a); cli.json knownGap :456 (nit a) and history :561 (nit b); the README qa row :930 (nits a, c); and the state-counts.md total row, because the base moved 933/168/12/1119 → 936/166/9/1117. No other difference. runner.ts, test.ts, testing.zod.ts, qa.json and check-liveness.mts blobs are identical to 5d31b4d587. Merge 9056455134 (parents 5d31b4d587, d498113b50) adds no file beyond the 14; commit 76e9a48821 touches only state-counts.md; 5b15c28fb1 touches the four text files, 6/6 lines.
  2. README: vs d498113b50 only the qa row differs (1 insertion, 1 deletion); main's translation and rest_api rows hash-equal between d498113b50 and the merged tree. state-counts.md vs base: only qa 4/5 → 8/1 and total 936/166 → 940/162, other columns unchanged; arithmetic exact. check:liveness byte-checks the artifact (countsArtifactErrors); Spec property liveness is green at head.
  3. Nit texts, each TRUE at head: (a) "still runs, and the unmet requirement surfaces only as whatever failure it causes, if any" (cli.mdx adds "the scenario can still pass"; cli.json and README carry the same clause). Grep of the merged tree: nothing in packages/core/src/qa or commands/test.ts reads scenario.requires. (b) history: "now in brackets after the name, or bare when the name equals the id" matches scenarioLabel. (c) README: "stamped as suiteName on every TestResult the suite produces" matches runSuite; a lone runScenario is no longer over-claimed.
  4. Main 10ea9eb2ed..d498113b50 touched none of the qa surfaces except cli.mdx, in the os g sections (hunks :1406–1460, :2168–2171), disjoint from the PR's os test hunks (:1654–1716), auto-merged. Main's diff mentions no TestResult, os test, --tags, scenario.tags/requires, TestSuiteSchema or liveness/qa.json. In the merged tree the sole TestResult consumer is still scenarioLabel via Pick, and there is no requires reader.
  5. CI at head: 42 check-runs, 38 success, 4 skipped (Console Pin Gate, opt-in tarball smoke, and the 03:24Z re-fire of Auto Label / Check PR Size after the body edit), 0 failed. Lint & Repo Gates, Spec property liveness, Test Core and its 6 shards, Check Changeset, the five Type Check jobs and Build Docs are green. PR body's branch-base bullet is now true; mergeable_state: clean.

② Semver level

Unchanged: minor. cli minor, core minor, spec patch, one fixed group; Clause-②: no, no arm; the delta is docs text and a regenerated count.

③ Boundary flags

  • merge-tree vs current origin/main 29720975b6 (5 commits past d498113b50): driver-free, from a shared bare clone with no merge.os-regen driver, writes tree 3f9cc93755, exit 0, no conflict. The checkout's driver run also writes a tree and only notes that content/docs/references/qa/testing.mdx is generated: main's 9daced0b6f rewrote its frontmatter description: after d498113b50, so the next main merge owes gen:schema + gen:docs; the PR's copy is exact for its own tree (check:docs green).
  • requires: still the maintainer's decision on qa: os test prints suite and scenario names, filters by tags, and skips on unmet requires (5 keys) #20289; this PR leaves the row dead with an honest describe and opens Part of #20289.

Implemented-by: claude/issue-20289-os-test-names-tags-requires
Reviewed-by: session_01QcAS3qiYYZNezaxZxaUdMV

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 03:31
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 5a6267f Sep 28, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20289-os-test-names-tags-requires branch September 28, 2026 03:57
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…el, so `os g` scaffolds reach the stack (objectstack-ai#20333) (objectstack-ai#20363)

Fixes objectstack-ai#20333
Clause-②: no

## Summary

`npm create objectstack`'s blank starter imported `./src/objects` alone,
so everything `os g view|action|flow|dashboard|app|skill` wrote was
never loaded and `os validate` counted 0 of it. The starter now wires
the seven generator barrels `os init` wires since PR objectstack-ai#20329, in the
lines `os init` renders: `exportsOf` over `export {};` barrels, and
`requires: ['automation', 'triggers']`. The copy is bound to the CLI's
single source (`SCAFFOLD_WIRED_BARRELS` / `SCAFFOLD_WIRED_REQUIRES`,
derived from `GENERATOR_SCAFFOLD_TARGETS`) by a parity pin, so it is not
a second wiring rule. A per-PR pin drives `npm create objectstack` → `os
g object` (control) → `os g flow` → `os validate` and reads `Logic: 1
Flows`.

## What changed

-
`packages/create-objectstack/src/templates/blank/objectstack.config.ts`:
imports every wired barrel, declares the `exportsOf` helper, and hands
each barrel to its stack key. The objects import changes from
`'./src/objects/index.js'` to `'./src/objects'`, the extensionless form
`os init` renders, which the parity pin compares verbatim; the
template's `moduleResolution: bundler` resolves the directory index, and
a fresh scaffold type-checks. It carries `requires: ['automation',
'triggers']`. `automation` was already there for the three connector
plugins, and its comment keeps that reason.
- Six new `src/{views,actions,flows,dashboards,apps,skills}/index.ts`
barrels, byte-identical to what `os init` writes.
- Two pins in `packages/cli/test/` (below). The CLI is the only package
that can call the renderer, and it already depends on
`create-objectstack`.
- `packages/cli/package.json` gains
`@objectstack/connector-{rest,openapi,mcp}` as devDependencies (lockfile
+9 lines, one importer block). They exist only so the scaffolded project
the chain pin builds under the CLI's `node_modules` can resolve the
blank config's connector imports, and so CI builds them in
`@objectstack/cli#test`'s closure.
- `scripts/cross-package-test-inputs.mjs` and `turbo.json` declare the
blank config and `src/**` as inputs of `@objectstack/cli#test`, with a
witness for the barrel glob the scan cannot name.
- Docs this change made false (see below), and a `create-objectstack`
patch changeset.

## Why a static copy, and what binds it

`create-objectstack` cannot import the roster. The dependency edge runs
the other way, and the npx entry must not pull the CLI's closure: the
boundary `scripts/sync-scaffold-emission-policy.mjs` already documents.
Measured options:

- **Generate at build time.** The roster is computed from the
`GENERATORS` literal in `generate.ts`. Reading it at
`create-objectstack`'s build would need either text-parsing that
literal, or evaluating the CLI's source before the CLI's own
dependencies are built, which is a build-order cycle.
- **Parity pin over a static copy.** Chosen as the least machinery.
`create-objectstack-wiring-parity.test.ts` reads every expected line off
the CLI: the barrel import lines, the `exportsOf` line and the stack-key
lines of `TEMPLATES.app.configContent`, the `requires` tokens as a
superset of `SCAFFOLD_WIRED_REQUIRES`, and each empty barrel byte for
byte from `TEMPLATES.app.srcFiles`. A generator added to the roster, a
renderer change or a hand edit of the template reddens it (ablations A1
to A3).

## Measured before and after, through the real commands

The on-ramp's real `bin/` scaffolded `my-app --skip-install
--skip-skills` into a directory where the config's imports resolve, then
this repo's CLI ran.

| step | `origin/main` `c74de10a9` | this branch |
|:---|:---|:---|
| `os g object order_line` (control) | exit 0, reaches the stack | exit
0, reaches the stack |
| `os g flow order_line` | exit 0, **Not wired** | exit 0, reaches the
stack |
| `os validate` | exit 0, `Data: 2 Objects`, `Logic: 0 Flows` | exit 0,
`Data: 2 Objects`, `Logic: 1 Flows` |

- **`exportsOf` is required here too.** A fresh starter type-checks
(`tsc --noEmit`, 6.0.3, exit 0). The same starter with `Object.values`
on the empty barrels fails with 4 x TS2322 (actions, flows, dashboards,
apps). After generating the object and the flow it still type-checks.
- **`requires` boots.** `os dev --fresh` on a random port: the flow-less
fresh starter was healthy after about 22s, `/api/v1/ready` answered 200,
and `AutomationServicePlugin` and the record-change, schedule,
time-relative and api trigger plugins loaded, resolved through the CLI's
own dependencies. With the generated flow it was healthy after about 24s
and reported `Flows: 1 flow(s) 1 bound to triggers`. Neither boot
printed "not enabled" or "NOT installed".
- **Census.** `src/templates/` holds one starter, `blank`, which is also
the registry's only entry.

## Pins

- `packages/cli/test/create-objectstack-wiring-parity.test.ts` (unit,
per-PR): 20 cases, described above.
- `packages/cli/test/create-objectstack-stack-reach.test.ts`
(integration, per-PR, not `.e2e`): the chain above, with item names read
off the generator roster. It asserts the exit codes, the named subjects,
the absence of the wiring lines and of a `requires` line from `os g
flow`, and the `Data: 2 Objects` / `Logic: 1 Flows` counts. No prose is
pinned.

## Ablations

Each ran after the fix was committed. Mutations went through
`scripts/ablation-replace.mjs` in wrap mode, which verified the anchor
count and the blob change and restored with blob equal to HEAD and an
empty `git diff HEAD`.

- **A1, the wiring reverted** (the `flows: exportsOf(flows),` line
deleted, then `create-objectstack` rebuilt). `ablation-dist-preflight
--absent` confirmed the line was gone from `dist/`. Chain pin: 2 failed,
2 passed. The control and the scaffold stayed green, and `os g flow`
printed the wiring lines while validate read no `Logic: 1 Flows`. Parity
pin: 1 failed, 19 passed, on the stack-key comparison. Direction: red.
- **A1 restore.** Rebuilt; `ablation-dist-preflight` found the marker
present in `dist/templates/blank/objectstack.config.ts`, and the whole
tree was clean. Chain pin 4/4, parity pin 20/20.
- **A2, a barrel dropped from the template** (the `skills` key deleted):
parity 1 failed, 19 passed. Red.
- **A3, one barrel's bytes drifted from what `os init` writes**
(`views/index.ts` reworded): parity 1 failed, 19 passed. Red.
- After A2 and A3 the whole tree was clean, and parity was 20/20.

## Verification

Patch round 1, at HEAD `702a27775` (origin/main `a88a1bb39` merged at
`df0c0c846`): the 124 derived gates, `check-issue-citations --base
origin/main` and `check:scaffold-emission-policy` all exited 0 on the
first pass (`--ran`: 124 derived, 124 run, 0 NOT-MEASURED, 0 UNRUN),
including `check:doc-anchors`, `check:docs-audit-scope` and
`check-affected-docs`; `pnpm lint` exited 0; the parity pin 20/20, the
chain pin 4/4, and `create-objectstack` 16 files, 232 passed.

Round 0: all of the following ran at HEAD `d50d46fe0` (origin/main
`26daf0b03` merged).

- `pnpm --filter create-objectstack test`: 16 files, 232 passed.
`typecheck`: exit 0.
- `pnpm --filter @objectstack/cli typecheck`: exit 0, including
`check:test-typecheck`, whose ledger is unchanged.
- CLI `unit` project: 231 files, 3316 passed.
- CLI `integration`: this chain pin plus `generate-stack-reach.test.ts`,
2 files, 11 passed.
- `pnpm lint`: exit 0 over the whole repo, not narrowed.
- `node scripts/check-issue-citations.mjs --base origin/main`: exit 0.
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--ran`: 124 derived, 124 run, 0 NOT-MEASURED, 0 UNRUN. Three gates first
exited 3 with PREREQUISITE NOT MET (`check:skill-examples`,
`check:dual-build-cjs-loads`, `check:i18n-coverage`) and exited 0 after
a full build.
- `pnpm check:scaffold-emission-policy`: exit 0.

## Docs this change made false, and a surface note

These published lines described an objects-only starter and are
corrected in place:

- the blank starter's `README.md` Layout, plus its app remedy, which now
says to export the file from `src/apps/index.ts`;
- the shipped `AGENTS.md` rule 3, which prescribed `Object.values()`
(measured TS2322 on the now-empty barrels);
- the package `README.md` tree;
- `content/docs/getting-started/your-first-project.mdx`: its section-2
tree and config block;
- `content/docs/getting-started/build-with-claude-code.mdx` (patch round
1): step 3 said the agent wires the action, view and app through
`actions:` / `views:` / `apps:` keys in `defineStack()`. It now says
each file is exported from its directory's barrel
(`src/actions/index.ts`, `src/views/index.ts`, `src/apps/index.ts`),
which the starter's config already hands to `defineStack()`, matching
the shipped `AGENTS.md` rule 3. A sweep of `content/docs/` found no
other sentence telling a starter author to add a collection key;
- `content/docs/deployment/cli.mdx`.

In `cli.mdx`, the `os generate` section's "Not wired" example named "the
`npm create objectstack` starter", and its first-app walkthrough ran `os
generate action approve`. On the wired starter that action is refused
with exit 1: "Action 'approve' references object 'my_app_approve' which
is not defined in objects". The walkthrough now runs `object customer`,
then `flow customer`, then `action customer`, measured `UI: 1 Actions`
and `Logic: 1 Flows`. Its fixture callout now names the extra action.

`content/docs/**` and `packages/create-objectstack/README.md` were
outside the claim's first file surface; the seat amended the claim in
place to name them. They are edited under the agent contract's rule that
a published line this change makes false is repaired in the same PR. PR
objectstack-ai#20341 edits `cli.mdx` around lines 1619 to 1690, disjoint from these
hunks; PR objectstack-ai#20258 edited lines 1 to 7 of `your-first-project.mdx` and
`build-with-claude-code.mdx`, has since landed, and merged into this
branch without conflict.

`skills/objectstack-platform/SKILL.md` line 192 says the template
declares `requires: ['automation']`. That is now stale, but `skills/**`
is a governed Tier H surface, so it is **not** edited here; the seat
files it for the skills lane once this PR lands.

## Acceptance notes

- Byte-identical barrels inherit the article slip in `init.ts`'s
`renderEmptyWiredBarrel` ("a action", "a app"). `init.ts` is read-only
here. Whoever next edits that renderer carries it, and the parity pin
will then require the starter to follow.
- The old walkthrough's `os generate flow onboarding` also bound its
flow to an undeclared object. That was not silent: `os dev` warned "the
flow will never fire". The new walkthrough binds to the object it
creates.
- Measured in patch round 1, on a scaffolded starter holding the Build
with Claude Code step-3 files: exporting each from its barrel, with the
config untouched, gives `os validate` exit 0 with `Data: 2 Objects 6
Fields` and `UI: 1 Apps 1 Views 1 Actions`, the page's step-4 counts.
Adding `actions:` / `views:` / `apps:` keys beside the wired ones
instead still validates (the later key wins), but the starter's `tsc
--noEmit` fails with 3 x TS1117, and a later `os g view customer` then
reports Not wired, while the barrel-wired project reports it reaches the
stack.
- The `requires` pair is PR objectstack-ai#20329's shape. The standing family cards
for the rest of that seam are objectstack-ai#20331 and objectstack-ai#20332, both named on objectstack-ai#20215.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…stem-* migration entries states each lesson in words, not tracker numbers (stage 3) (objectstack-ai#20384)

Part of objectstack-ai#20233

Clause-②: no

**Stage 3 of a staged card.** The card stays open for later stages; this
PR carries no closing keyword. Text only: no entry id, `surface`, `from`
/ `to`, conversion or matching logic moves, and the chain rewrites
exactly what it rewrote before.

## What this does

`os migrate meta` prints every ADR-0087 semantic entry it crosses as one
block: `⚠ [protocol N] SURFACE → REPLACEMENT`, then `why:` (the entry's
`reason`) and `verify:` (its `acceptanceCriteria`). AGENTS.md's
runtime-string rule applies to all of it: 「Runtime strings — refusal
prose, prescriptions, anything an author is shown — carry no tracker
number (`pnpm check:doc-authoring`): the lesson goes into the text.」
Form **D** of ruling C+D on the parent card sets the shape: the lesson
in words, and no number, dead or alive.

This stage covers the next three families by site count, `driver-`,
`kernel-` and `system-`: **132 sites → 0** in the three prose fields.
None of the 26 entries carries a tracker id in `surface` (ruling A of
the stage-1 ACCEPT, `5858839916`, is checked and has nothing to do
here). Each site now says what the cited ruling, measurement or fix
decided. ADR ids stay. `registry.ts`, `spec-changes.json` and
`docs/protocol-upgrade-guide.md` are regenerated from the entries
(`gen:migration-registry`, `gen:spec-changes`, `gen:upgrade-guide`),
never hand-edited. The stage-1 pin now holds `engine-`, `ui-`,
`plugin-`, `driver-`, `kernel-` and `system-`.

## Census — tracker ids in the author-shown fields

**Instrument.** The stage-2 AST instrument, unchanged: a TypeScript-AST
walk over every `packages/spec/src/migrations/entries/**/*.ts`. For each
`entry` object literal it evaluates the string value of `replacement`,
`reason`, `acceptanceCriteria` and (counted separately) `surface`,
joining string literals with `+`, then counts `#` followed by 4 or 5
digits at a word boundary. **Validated first** by reproducing the
stage-1 readings on the stage-1 tree (`443b2f4fdc`, extracted with `git
archive`): `driver-` 7 entries / 44 sites (0 / 44 / 0, 25 distinct),
`kernel-` 9 / 44 (1 / 41 / 2, 11 distinct), `system-` 10 / 44 (0 / 42 /
2, 6 distinct), `engine-` 5 / 67, whole tree 266 entries / 1,016 sites /
9 `surface` sites — every figure equal to the stage-1 census. **Tree
measured:** `objectstack-ai/objectstack` at `569d4d2dbf` (this branch's
base). Unevaluable fields: 0.

**Controls, same run.**
- **Lit:** `17.aggregation-node-distinct-retired.ts` reads 7 sites
(replacement 1, reason 6), the reading stages 1 and 2 took.
- **Dark (comment lines):** 794 `//` lines in entry files carry a
tracker id, and none is counted. Comment lines belong to the sibling
card, and ⛔ this PR touches none (794 before and after).
- **Dark (field boundary):** the 7 `surface` sites left in the tree
(other families) count 0 in the three-field total and 7 in the `surface`
column.

**Re-measured on the base, matching the stage-1 census:** `driver-` 7
entries, **44** sites (replacement 0 / reason 44 / acceptanceCriteria
0), 25 distinct ids; `kernel-` 9 entries, **44** (1 / 41 / 2), 11
distinct; `system-` 10 entries, **44** (0 / 42 / 2), 6 distinct. 37
distinct ids across the three (the families share `objectstack-ai#14478`, `objectstack-ai#15939`,
`objectstack-ai#17635` and `objectstack-ai#3733`). `surface`: 0 in all three. Whole tree: 300
entries, **843** sites, 7 `surface` sites.

**After this PR:** `driver-` 0, `kernel-` 0, `system-` 0; `engine-`,
`ui-`, `plugin-` still 0; whole tree **843 → 711** sites; `surface` 7
(unchanged, other families).

| entry | sites (replacement / reason / acceptanceCriteria) |
|---|---|
| `17.driver-aggregate-undeclared-key-aliases-removed` | 6 (0 / 6 / 0) |
| `17.driver-capabilities-inert-bits-removed` | 4 (0 / 4 / 0) |
| `18.driver-options-timeout-to-timeout-ms` | 1 (0 / 1 / 0) |
| `17.driver-sql-distinct-bare-filter-typed` | 9 (0 / 9 / 0) |
| `18.driver-sql-unresolvable-where-column-refused` | 14 (0 / 14 / 0) |
| `18.driver-sql-upsert-cross-row-identity-merge-refused` | 9 (0 / 9 /
0) |
| `18.driver-turso-config-local-path-wasm-retired` | 1 (0 / 1 / 0) |
| `18.kernel-compatibility-matrix-estimated-migration-time-unit-in-key`
| 5 (0 / 5 / 0) |
| `18.kernel-context-preview-mode-retired` | 5 (1 / 4 / 0) |
| `18.kernel-event-bus-retention-unit-in-key` | 3 (0 / 3 / 0) |
| `18.kernel-health-check-and-hot-reload-durations-unit-in-key` | 7 (0 /
5 / 2) |
| `18.kernel-package-lifecycle-durations-unit-in-key` | 3 (0 / 3 / 0) |
| `18.kernel-plugin-health-report-durations-unit-in-key` | 3 (0 / 3 / 0)
|
| `18.kernel-plugin-security-durations-unit-in-key` | 4 (0 / 4 / 0) |
| `18.kernel-runtime-config-timeout-unit-in-key` | 11 (0 / 11 / 0) |
| `18.kernel-startup-orchestrator-durations-unit-in-key` | 3 (0 / 3 / 0)
|
| `18.system-cache-durations-unit-in-key` | 3 (0 / 3 / 0) |
| `18.system-collaboration-durations-unit-in-key` | 4 (0 / 4 / 0) |
| `18.system-failover-health-check-interval-unit-in-key` | 3 (0 / 3 / 0)
|
| `18.system-metrics-jsdoc-durations-unit-in-key` | 12 (0 / 12 / 0) |
| `18.system-metrics-window-durations-unit-in-key` | 5 (0 / 3 / 2) |
| `18.system-object-storage-durations-unit-in-key` | 3 (0 / 3 / 0) |
| `18.system-registry-config-durations-unit-in-key` | 3 (0 / 3 / 0) |
| `18.system-tracing-otel-exporter-durations-unit-in-key` | 5 (0 / 5 /
0) |
| `18.system-tracing-span-duration-unit-in-key` | 3 (0 / 3 / 0) |
| `18.system-worker-queue-rate-limit-duration-unit-in-key` | 3 (0 / 3 /
0) |
| **total, 26 entries** | **132 (1 / 127 / 4)** |

## Every citation read, and what the text now says

I read each cited issue or PR myself with single-card REST reads: the
body, and the comments where a ruling or a measurement lives. Ids are in
code spans so this body posts no cross-references. All 36 bare ids were
resolved against this repository, because every sentence that cites one
is about this repository's code; the one cross-repo id is `cloud#1651`.

| cited | what it decided (read) | how the text now carries it |
|---|---|---|
| `objectstack-ai#3733` | The pruned `cached` field key: measured, the parse succeeded
and the removed key was dropped without a word; the orphan schema was
deleted. | "an earlier field-key prune measured exactly that — the parse
succeeded and the removed key was dropped without a word" (health-check,
OTel exporter) |
| `objectstack-ai#3821` | The sharing-rule page: an unsortable query fell through to
an empty page, and the driver fix made an unsortable query lose its
ORDER BY, not its rows. | "the unknown-column recovery ladder (an
unsortable query loses its ORDER BY, not its rows)"; "the ladder's own
premise — rows matter more than their order"; "the ladder's recoveries"
|
| `objectstack-ai#4484` | `IDataDriver.findStream` removed: no production caller, two
of three implementations buffered the whole set, and no tombstone
because nothing parses a driver object. | "Retiring
`IDataDriver.findStream` (it had no production caller, and two of its
three implementations read the whole result set into memory …)";
"(`IDataDriver.findStream`, removed with no tombstone because nothing
parses a driver object)"; by entry id in the `distinct` entry |
| `objectstack-ai#4583` | The datasource ledger's dead keys removed; `capabilities.*`
went as a whole block (11 of 11 unread). | "was retired separately, as a
whole block nothing read" |
| `objectstack-ai#4634` | Audit of all 34 `DriverCapabilities` bits: 3 live, 31 dead
and tombstoned. | the entry already states the audit ("the follow-up
audit checked every bit"); the trailing id is dropped |
| `objectstack-ai#4914` | Maintainer, 2026-08-04: remove `manifest.loading` and
`PluginHotReloadSchema`; keep `HotReloadConfigSchema`, the side with an
implementation (`HotReloadManager`), as the start point. | "kept twice:
as the hot-reload vocabulary that had an implementation when the
manifest-side copy was removed, …" |
| `objectstack-ai#4984` | An org-axis red-line gate read only aliases the schema
rejects while its own fixtures spelt them: tests green, rule dead. |
"the family of the org-axis red-line gate that read only rejected
aliases while its own fixtures spelt them, so its tests stayed green and
the rule stayed dead" |
| `objectstack-ai#5181` | Narrow the query parameter of `IDataDriver`'s methods
(`DriverQuery`, no redundant `object`). | "neither the narrowing of
`IDataDriver`'s query parameters to `DriverQuery` nor the follow-through
…" |
| `objectstack-ai#5499` | Maintainer, 2026-08-05: freeze investment in `driver-memory`
/ `driver-mongodb`; fully lifted 2026-08-11 (comments `5249019855`,
`5252526378`). | "the maintainer's 2026-08-05 investment freeze on
driver-memory, which was lifted on 2026-08-11" |
| `objectstack-ai#5540` | Remove `IStorageService.list(prefix)`: zero consumers, and
the two adapters answered differently and both incompletely. | "(the
zero-consumer `IStorageService.list`, whose two adapters answered
differently and both incompletely)" |
| `objectstack-ai#6011` | Maintainer: close the `ctx.user` `roles` alias now. | "(the
`ctx.user` `roles` alias, closed at once on the maintainer's word rather
than given a window)" |
| `objectstack-ai#6075` | **404** — see Acceptance notes. | "the follow-through that
brought five drivers' implementations in line" |
| `objectstack-ai#6320` | `distinct`'s third argument meant different things on memory
and sql; the sql half was dispatched, the memory half held under the
freeze. | "(the measurement that found the two drivers reading this
argument differently split the fix: the sql half is this entry, and the
memory half was held back by that freeze)" |
| `objectstack-ai#6321` | `query.aggregate` / `agg.func` are undeclared aliases whose
only writers are driver fixtures; order: re-spell the fixtures, delete
the aliases, then narrow the signature. | "The removal ran in a fixed
order — the fixtures re-spelt first, the two alias branches deleted
second, the parameter narrowed to `DriverQuery` last — because the
reverse order yields red nobody can explain." |
| `objectstack-ai#6404` (PR) | Executed that order and narrowed `aggregate`'s query
parameter to `DriverQuery`. | the same sentence |
| `objectstack-ai#7929` | Maintainer, 2026-08-12, ruling B: `driver-sql`'s filter
refusal stops echoing `$field` operands, for every caller; the full
diagnostic goes to the server log. | "the same predicate-text disclosure
shape the driver's field-reference filter refusals had already been made
to stop echoing (the full diagnostic goes to the server log, never the
response)"; "that disclosure shape closed on the last dialect" |
| `objectstack-ai#8371` | Ruled option 2: a dotted filter key whose head is a
relation, a formula or a scalar is refused at both doors; a structured
head stays unjudged. | "the axis owned by the dotted-filter verdict,
which refuses a dotted key whose head is a relation, a formula or a
plain column at the protocol and engine doors" |
| `objectstack-ai#8592` | Measured on live MySQL: knex compiles the named conflict
target away. | stated by the entry ("knex drops the named keys before
the statement leaves the process"); the trailing id is dropped |
| `objectstack-ai#8621` | Option A: a pre-flight refusal when no unique index backs
the caller-named conflict target. | "Two earlier pre-flight refusals
closed the half where no unique index backed a caller-named target …" |
| `objectstack-ai#8622` | `id` becomes insert-only on the merge path: a merge on a
non-primary conflict key was measured rewriting the existing row's
primary key. | "`id` is insert-only on the merge path (made so once a
merge on a non-primary conflict key was measured rewriting the existing
row's primary key)" |
| `objectstack-ai#8755` | Ruling option A: a pre-flight refusal when a second unique
key could absorb a backed, caller-named target. | "… and the half where
a rival unique key could absorb a caller-named one" |
| `objectstack-ai#8790` | Maintainer, 2026-08-15: refuse both halves with
`INVALID_FILTER` / 400, naming the column. | "Ruled by the maintainer on
2026-08-15: refuse BOTH halves …"; "Recover-both was excluded by the
ruling's own argument" |
| `objectstack-ai#8807` | Maintainer, 2026-08-15: an upsert must never modify a row
whose identity the caller did not supply and whose conflict key it did
not name; enforcement delegated, blanket refusal excluded. | "Ruled by
the maintainer on 2026-08-15, as a contract principle …" (the principle
itself was already quoted verbatim) |
| `objectstack-ai#8926` | Maintainer, 2026-08-16, option A: MySQL's spelling joins the
one shared predicate (envelope and recoveries together). | "Addendum
2026-08-16." — the paragraph already states option A |
| `objectstack-ai#9061` (PR) | Implemented that option A. | the same |
| `objectstack-ai#11825` | Maintainer, 2026-08-25: retire the declarative
`AdvancedPluginLifecycleConfig` container; the classes stay a
host-driven library. | "… and as a host-driven library when the
declarative lifecycle config container was retired" |
| `objectstack-ai#11846` | **404** — see Acceptance notes. | "maintainer ruling
2026-08-27 (Option A: remove)"; "(as the removal ruling recorded)" |
| `objectstack-ai#14478` | Ruling B, 2026-09-02: a no-baseline gate plus an ADR-0087
rename of every offender (`DriverOptions.timeout` among the seven
named); ruling B again, 2026-09-05: the population is every authored and
every runtime-emitted duration, minus exemptions declared on the schema.
| "Maintainer ruling B on duration units (2026-09-02, its population
widened on 2026-09-05 to every authored and every runtime-emitted
duration, bar the exemptions a schema declares on the key itself)"; "the
duration-unit rule (…)" |
| `objectstack-ai#14519` | The two tenant timeouts published a describe naming no unit
(the unit sat in the JSDoc only); folded into the rename. | "the
unit-nowhere shape (no unit in the name or in the published describe,
first measured on two tenant timeouts)" |
| `objectstack-ai#15626` (PR) | Landed the gate and the seven founding renames, the
tenant `idleTimeout` → `idleTimeoutSeconds` among them. | "The tenant
half was already renamed, in the same change that landed the duration
gate itself" |
| `objectstack-ai#15678` | `kernel/`: the 14 remaining duration keys carry their unit
in the key name. | "the kernel-directory duration renames" / "the
kernel-directory round"; trailing ids dropped |
| `objectstack-ai#15679` | `system/`: the 15 remaining duration keys carry their unit
in the key name; `size` got an honest name. | "the system-directory
duration round"; trailing ids dropped |
| `objectstack-ai#15939` | The gate did not read JSDoc. Ruled 2026-09-07: refuse the
JSDoc / describe divergence. Ruled A 2026-09-11: remediate the
population per file first, land the widened gate last. | "Director-seat
ruling A of 2026-09-11 on the JSDoc-channel finding … a duration key
whose JSDoc names a unit its describe does not is refused, and the keys
in that shape are remediated per file before that refusal lands" |
| `objectstack-ai#16024` | Maintainer, 2026-09-06, per key: forward `timeout`, remove
`localPath` and `wasm`. | "ruled per key by the maintainer on
2026-09-06, once all three of this package's unread config keys had been
measured" |
| `objectstack-ai#17635` (PR) | The widened gate: refuse a duration key whose JSDoc
names a unit its describe does not; landed last. | "lands that widened
gate last, into a tree already clean" |
| `objectstack-ai#18669` | Ruling A, 2026-09-17: rename `FileValue.duration` and
`estimatedMigrationTime`, each with an ADR-0087 entry; no new closed
type, no narrowing of stored data. | "Maintainer ruling A of 2026-09-17
on the last two duration keys no closed duration type could express …" |
| `cloud#1651` | **Not readable from this session** — see Acceptance
notes. | "cloud — a census closed 2026-08-26: OS_PREVIEW_MODE there is a
routing-only switch …" |

No call-shaped token moves: a `name(` census over `registry.ts` is
identical before and after (297 distinct tokens), so textual
call-spelling ratchets read the same.

## Pin — `packages/cli/test/migrate-meta-engine-guidance.test.ts`,
widened

`COVERED_PREFIXES` is now `engine-`, `ui-`, `plugin-`, `driver-`,
`kernel-`, `system-`. The pin still spawns the real CLI (`os migrate
meta --from 16 --to 18`) once, locates each covered block **verbatim**
in stdout, and asserts the printed block — `surface` included — carries
no `#` plus 4 or 5 digits. Anti-vacuity:
- the `REWRITTEN` floor rises from 29 to **55** ids: the 26 entries of
this stage (7 `driver-`, 9 `kernel-`, 10 `system-`) are added, and every
covered prefix must still select at least one entry;
- presence in stdout is asserted before cleanliness (the
`driver-sql-unresolvable-where-column-refused` reason carries two
blank-line paragraph breaks, and its block is found verbatim);
- the detector is exercised on both sides first (lit on 4 and 5 digits,
dark on 3, 6 and `ADR-0112`).

The file keeps its stage-1 name; the header lists the six covered
families.

## Ablation — the widened pin can fail on a `driver-` block

From committed state, HEAD `1c0dc7ad54`, with
`scripts/ablation-replace.mjs` in wrap mode (it owns the restore trap)
and `scripts/ablation-dist-preflight.mjs` gating each leg. The bundle is
built from the generated `registry.ts`, so that is the file mutated.
- **Mutation.** In `registry.ts`, the reason of
`driver-sql-upsert-cross-row-identity-merge-refused`: anchor `pre-flight
refusals closed the half` → `pre-flight refusals (objectstack-ai#8621) closed the
half`. The tool read anchor 1 → 0 and replacement 0 → 1, blob `b41e1d44`
→ `8f8d226e`.
- **Mutate leg** (one lock turn: build, preflight, pin). Spec build exit
0. Preflight: marker present in 4 built files. Pin: **red**, `1 failed |
2 passed` — `driver-sql-upsert-cross-row-identity-merge-refused: the
printed guidance cites a tracker id: expected 'objectstack-ai#8621' to be undefined`.
- **Restore.** Tool-proven: blob `b41e1d44` == HEAD, `git diff HEAD`
empty.
- **Restore leg.** One lock turn, taken on the second try (the first
waited out its 540 s budget, exit 99, NOT MEASURED, the tree already
restored). Spec build exit 0. The `--absent` preflight found the marker
in none of 222 built files, with the working tree clean against HEAD.
Pin: **green**, `3 passed`.

## Verification

Final head **`1c0dc7ad54`** for every line below; each heavy run went
through `scripts/pm/os-verify-lock.sh` (one turn, `VERDICT command-exit
0`, per-step exits recorded separately).

- **Build:** `pnpm exec turbo run build --concurrency=2
--filter='@objectstack/cli^...'` gives `Tasks: 55 successful, 55 total`.
- **Pin and its neighbour:** `pnpm --filter @objectstack/cli exec vitest
run --project integration --maxWorkers=2
test/migrate-meta-engine-guidance.test.ts
test/migrate-meta-default-range.test.ts` gives `Test Files 2 passed`,
`Tests 10 passed | 1 skipped` (the skip is the default-range file's own
pre-existing `skipIf`).
- **Spec tests that read these entries or the registry:** `pnpm --filter
@objectstack/spec exec vitest run --maxWorkers=2 src/migrations
src/kernel/preview-mode-retirement.test.ts
scripts/build-schemas-check-mode.test.ts` plus the 19 other spec test
files that read `MIGRATIONS_BY_MAJOR`, the registry or an entry file:
`Test Files 24 passed`, `Tests 696 passed`.
- **CLI unit:** `test/vitest-tiers-partition.test.ts` and
`src/utils/spec-release-changes.test.ts`: `Test Files 2 passed`, `Tests
28 passed`.
- **The call-spelling census that reads `registry.ts`:** `pnpm --filter
@objectstack/driver-sql exec vitest run --maxWorkers=2
src/sql-driver-query-signature.test.ts` gives 15 passed.
- **Typecheck:** `pnpm --filter @objectstack/spec typecheck` exits 0
(test layer: 53 files / 255 errors held in its ledger); `pnpm --filter
@objectstack/cli typecheck` exits 0 (test layer: 3 files / 28 errors
held, unchanged).
- **Gate families:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derives **89** families at
`1c0dc7ad54` (after `git fetch origin main`). `--ran` over the recorded
exit codes reads **89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN**, all
exit 0. They include `check:doc-authoring` ("16466 customer-facing
string(s) across 1135 spec sources clean"), `check:issue-citations`,
`check:migration-registry` ("registry.ts is current (300 semantic, 219
retired-key, 199 retired-def)"), `check:spec-changes`,
`check:upgrade-guide`, `check:generated` ("All 15 generated artifacts
are up to date"), `check:duration-unit-keys`, `check:nul-bytes`,
`check:adr-0087-registration` and `check:changeset-no-major`.
- `check:dual-build-cjs-loads` refused first with `PREREQUISITE NOT MET`
(exit 3: twelve packages outside the CLI closure had no `dist/`). Those
`dist/` directories were written later in the same pass (04:48–04:49Z,
inside the `check:type-check-debt` run, whose re-measure builds them);
re-run at the same head it exits 0 (104 entries / 66 packages / 659 CJS
files). The reconciled list takes that latest run.
- **Lint (a proven narrowing, not the repo-wide run, which is CI's):**
`eslint --no-inline-config --format json` over the 28 changed `.ts`
files reports 28 files, 0 errors, 0 warnings.
- The population is read from `eslint.config.mjs`:
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`, and all 28
are in it (no file-ignored warning).
- Invariance: the config enables no type-aware linting (no
`parserOptions.project`, no typed rules), so a text edit cannot move the
verdict on a file it does not touch.
- **Mergeability:** see Acceptance notes (driver-free `merge-tree`
against `862b6ce869` exits 0).

## Acceptance notes

- **Two dead ids, rewritten from the code on `main`.** `objectstack-ai#6075` and
`objectstack-ai#11846` answer 404 on both the issues and the pulls endpoint, re-probed
with a 200 control (`objectstack-ai#14478`).
- `objectstack-ai#6075` (`distinct`'s "never reached it" sentence):
`packages/drivers/driver-sql/CHANGELOG.md` (commit `d367f03`) and
`sql-driver-query-signature.test.ts` record what it did — the five
drivers' implementations followed `IDataDriver`'s `DriverQuery`
narrowing. The sentence now says exactly that.
- `objectstack-ai#11846` (preview mode): `packages/spec/CHANGELOG.md` (commit
`0c2334f`), `packages/spec/src/kernel/context.zod.ts` and
`preview-mode-retirement.test.ts` record the 2026-08-27 ruling (Option
A: remove), the three-repo zero-consumer measurement and the
re-declare-fresh condition. Dropped because `main` does not state them:
"decision-inbox batch 2" and "all four decision facets pointed the same
way". "The objectstack-ai#11846 card records the measurement" (objectui leg) now reads
"zero consumers, measured when the removal was ruled", which is what the
changelog and the test header say.
- **One cross-repo id this session cannot read.** `cloud#1651` answers
403 here: `objectstack-ai/cloud` is not attached to this session
(`add_repo` refused: no access). It is neither confirmed nor refuted, so
its sentence was rewritten from what `main` records about it —
`packages/spec/CHANGELOG.md` (`0c2334f`: closed 2026-08-26 with positive
controls, `RuntimeMode` zero hits, `ArtifactKernelFactory` 20+ hits and
never touching `previewMode`) and `context.zod.ts` (`OS_PREVIEW_MODE`
there is routing-only). The cloud-side detail `main` does not state —
`previewMode` "only as a local variable" whose effect is adding
wildcards to "CSRF" trusted origins — is dropped; the parenthesis that
replaces it describes this repository's own `serve.ts` (the one reader
of `OS_PREVIEW_MODE` here only widens better-auth's trusted origins to
preview-domain wildcards), which is measured on `main`.
- **Decision-batch numbers went too (invisible to the regex).** Nineteen
sites cited a decision batch as `#` plus two or three digits (`objectstack-ai#43` ×13,
`objectstack-ai#115` ×4, `objectstack-ai#151` ×1, `objectstack-ai#158` ×1). They are numbers an author is shown
and cannot follow, so each is dropped. One consequence worth naming: 13
entries said `Maintainer ruling B on objectstack-ai#14478 (2026-09-02, decision batch
objectstack-ai#43)`, which fused two rulings on the same card — B of 2026-09-02 (the
gate and the no-baseline rename) and B of 2026-09-05, decided in that
batch (the population: every authored and every runtime-emitted
duration, minus schema-declared exemptions). The sentence now names both
dates. The `objectstack-ai#158` sentence (the agreement shape ruled an offence on
2026-09-18) is corroborated by
`.changeset/18075-agreement-shape-is-an-offence.md` on `main`.
- **"issue NNNN" / "PR NNNN" spellings, checked by hand.** No
bare-number spelling exists in these 26 entries' author-shown text; the
two `PR` citations (`PR objectstack-ai#6404`, `PR objectstack-ai#9061`) were `#`-spelled, so the
instrument saw them and they are gone. The only `#` left in these 26
files is on `//` comment lines (sibling card's surface), including a
`Prime Directive objectstack-ai#13` reference.
- **A citation whose page says something narrower than the text.**
`kernel-health-check-and-hot-reload-durations-unit-in-key` called
`shutdownTimeout`'s shape "the objectstack-ai#14519 unit-nowhere shape". `objectstack-ai#14519`'s
keys carried their unit in the JSDoc; "unit nowhere" is the gate's name
for it (`check-duration-unit-keys.ts` header: "no unit ANYWHERE (the
objectstack-ai#14519 shape)"), because the gate did not read JSDoc. The sentence now
says what the shape is — no unit in the name or in the published
describe — and that it was first measured on two tenant timeouts.
- **A comment that my text edit makes slightly stale.**
`18.system-metrics-window-durations-unit-in-key.ts` carries a `//`
comment saying its acceptanceCriteria sentence "is objectstack-ai#15679's, left word
for word". That sentence now says "that JSDoc-channel gap is filed as a
finding of its own" where it said "is objectstack-ai#15939": same content, no number.
The comment is the sibling card's surface (comment lines), so it is
untouched here.
- **Three "card" references re-anchored.** Removing an id left "the same
card" in the Turso entry pointing at nothing; it now says "the same
measurement". The kernel entries' "renamed by this same card" carry no
number and were not otherwise rewritten, so they are left.
- **Cross-PR check: no open PR adds or edits a `driver-`, `kernel-` or
`system-` semantic entry.** Read at 2026-09-28T04:0xZ: the 18 open PRs'
file lists (`GET /pulls/{n}/files`) carry 0 files matching
`migrations/entries/semantic/NN.(driver|kernel|system)-*`. The Version
Packages PR (`objectstack-ai#17076`) lists more than 1,000 files; the 1,100 rows read
carry no entry file, and it is the bot-generated release PR. Nothing in
flight will be held by the widened pin on arrival.
- **`main` moved 4 commits past the base** (`862b6ce869`: `objectstack-ai#20364`,
`objectstack-ai#20341`, `objectstack-ai#20366`, `objectstack-ai#20352`); none touches
`packages/spec/src/migrations/` or the pin. A driver-free bare-clone
`merge-tree --write-tree` of this head against `862b6ce869` exits 0 with
no conflicted path, so `registry.ts` needs no merge, and `main` was not
merged in.
- **Generated projections** (`spec-changes.json`,
`docs/protocol-upgrade-guide.md`) are regenerated, as in stages 1 and 2;
their `--check` legs are green. Only the three `driver-` entries
registered at protocol 17 appear in them, which is why those diffs are
small.
- **No other test pins these entries' text.** A `git grep` of test files
for the 26 entry ids finds one (`preview-mode-retirement.test.ts`),
which names the entry in a comment and reads no prose; a grep of tests
for the 37 cited numbers finds only comment lines. So no test needed
re-pinning this stage (stage 2's `migrations.test.ts` case has no
counterpart here).

## Line budget

Entry files: **352 changed lines** (+228 / −124) across 26 files,
against the stage-1 ≈400 budget. The whole diff is **776 lines** (+516 /
−260) in 31 files. Of the rest, `registry.ts` is 352, the two
projections are 18 (`spec-changes.json` 12, the upgrade guide 6), the
widened pin is 33 and the changeset is 21.

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

---------

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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants