Skip to content

fix(cli): os dev --no-watch turns watch off, and a PACKAGE matching nothing fails loudly - #20839

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20681-dev-no-watch
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20681-dev-no-watch

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20681

Clause-②: no

What

os dev had an off branch for its watch-recompile loop (watchActive, and the loop comment's "Skipped when" list), but no argv reached it. Three changes, all in packages/cli/src/commands/dev.ts:

  1. watch gets allowNo: true, the declaration the sibling compile, restart and seed-admin flags already carry. os dev --no-watch now boots with the loop off, and bare os dev keeps it on. The decision that the loop and the stale-artifact remedy line read is now one exported function, devWatchActive, so the pin can drive it with what oclif parsed. The loop comment's --watch=false is now --no-watch.
  2. A PACKAGE argument that selects no workspace project exits 1 instead of 0. The orchestration command now passes --fail-if-no-match to pnpm. pnpm owns the filter grammar (names, globs, ./dir, ...pkg), so pnpm decides whether the filter matched, and the CLI does not read the workspace a second time. This is the change that closes os dev --watch=false: oclif has no =value form for a boolean flag, so that argv parses as --watch plus the PACKAGE false.
  3. --no-watch in monorepo orchestration mode is refused (exit 1, naming the flag). Change 1 also makes --no-watch reachable there. In that mode each package's own dev script decides whether it watches, and the CLI would have printed Watch: enabled right under the flag. Before this PR that argv was refused as a nonexistent flag (exit 2), so nothing that used to work is refused now.

The loud check on the PACKAGE argument fit inside dev.ts (one token plus the refusal block), so this PR delivers the whole card. Nothing is left over.

Measured

Before, at BASE 73155fedc, through the source entry (tsx packages/cli/bin/run-dev.js), in a temp dir holding only objectstack.config.ts:

  • os dev --no-watch printed Nonexistent flag: --no-watch and exited 2.
  • os dev --watch=false printed Package: false, Watch: enabled, $ pnpm --filter false dev, then No projects found in ..., and exited 0.

After, at 4804d8a7a:

  • Same dir, os dev --watch=false ran pnpm --filter false --fail-if-no-match dev, which printed No projects found in .... The CLI then printed ✗ Development mode failed: Command failed: pnpm --filter false --fail-if-no-match dev and exited 1.
  • At the repo root, os dev nonexistent-pkg-xyz printed No projects matched the filters and exited 1.
  • At a temp workspace root (pnpm-workspace.yaml, no config), os dev --no-watch was refused, naming --no-watch, and exited 1.
  • Positive control, repo root: os dev @objectstack/verify ran that package's tsc -w dev script, which was still running when the 60 s timeout stopped it (exit 124). So the added flag does not break a filter that matches.
  • examples/app-todo, os dev --no-watch --fresh -p RANDOM: Server is ready, and no watching ... line. Control without --no-watch: watching objectstack.config.ts, src — rebuild + restart on change.

pnpm added --fail-if-no-match in 8.13.1, according to pnpm's own CHANGELOG at v10.31.0. It was measured here on 10.31.0 only. The changeset says monorepo mode now needs pnpm 8.13.1 or later.

Tests

New pin packages/cli/test/dev-no-watch.pin.test.ts. It spawns the source entry, so it is in the integration tier (vitest-tiers.ts fires childProcess, entryBasename, tsxBin).

  • Parse and decide, through oclif's own Parser.parse over Dev.flags / Dev.args: --no-watch gives watch: false, and devWatchActive answers false. Bare gives true and true. --watch=false gives watch: true and package: 'false'.
  • Spawned: --watch=false in a project dir exits 1, and the ✗ line names false. The workspace fixture's dev script prints a marker. Bare os dev there exits 0 and prints it (the control). --no-watch there exits 1, its ✗ line names --no-watch, and the marker is absent.

Runs:

  • Pin: pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/dev-no-watch.pin.test.ts: 1 file, 8/8 passed.
  • Ablation, three legs through scripts/ablation-replace.mjs in wrap mode. Each restore was proven: blob == HEAD 02c1c4bb1d8e, and git diff HEAD empty. All three turned red, as predicted:
    • A, --fail-if-no-match dropped: 2 failed / 6 passed (expected +0 to be 1, and no ✗ line). The first attempt at this leg was a no-op. The tool refused it because the replacement was a substring of the anchor, so its count could not rise. It was re-run with a replacement that does not overlap the anchor.
    • B, orchestration refusal disabled: 2 failed / 6 passed (expected +0 to be 1, and the marker present).
    • C, watch loses allowNo: 2 failed / 6 passed (the parse throws, and the spawned run exits 2).
  • Unit tier, pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 235 of 237 files passed (3341 tests passed, 29 skipped). The other two, published-subpath-console.pin and published-subpath-hook-body.pin, refused because packages/cli/dist was not built (a prerequisite, not a result). After pnpm --filter @objectstack/cli build: 2/2 files, 29/29 tests passed.
  • pnpm --filter @objectstack/cli typecheck: exit 0. check:test-typecheck: OK ... 3 file(s) / 28 error(s) is the ledger's existing figure; the new test adds no error.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands, run without paths, derived 63 commands at c36db5507. All 63 exit 0. check:dual-build-cjs-loads and check:i18n-coverage first exited 3 (PREREQUISITE NOT MET) and then 0 once the packages were built. --ran reconciliation: 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN.
  • pnpm lint: exit 0 at c36db5507.

The gate, lint, unit and typecheck results are on the final head c36db5507. The pin and ablation runs were on fcf4f78e3, and the only later commit adds the changeset file.

Acceptance notes

  • The os dev options table in content/docs/deployment/cli.mdx lists neither -w, --[no-]watch nor --[no-]restart, --log-level, --preset, --admin-email or --admin-password. That gap predates this PR, no pin enumerates that table, and the file is outside this card's file surface. Carrier: none.
  • Monorepo orchestration mode forwards none of dev's other flags (--port, --fresh, --ui, --artifact together with a PACKAGE, and so on) and says nothing about dropping them. Not measured here. This PR refuses only the flag it made reachable.
  • The orchestration branch still prints Watch: enabled whenever watch is on. That line describes the child packages' own dev scripts, which the CLI does not control.
  • The Clause-② line is copied from the claim. For the at-tier review: the no-match refusal turns an exit-0 run that started nothing into exit 1, and monorepo mode now needs pnpm 8.13.1 or later, where before any pnpm worked.

Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: os dev (command, 30 pages)
  • 2 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 — 25 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 b28054654d12e6faf92509a69fa416d9f5b56c82 → packageMentionDocs.

Which tree this was computed on

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

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c36db55077d3f439811ec609f19bbbc9f7dae30c
Local-runs: none

Inputs read: card #20681 body and its three comments (triage 5895852973, claim 5908367350, os-dev-report 5909733295); PR #20839 body, file list (3 files, +257/-4, 261 changed lines, head repo is the base repo, no governed path) and the net diff from merge-base 73155fedcacc215565c4eef6d9899977e0707010 to the head; the 34 check-runs on the head, collapsed to 34 names with no duplicates: 31 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke opt-in), 0 failure — all seven required contexts success. At the merge-base: root package.json (packageManager pnpm@10.31.0, engines node 22 or later), packages/cli/package.json (17.5.0; exports closed to ., ./console, ./hook-body, ./package.json; engines node 22 or later, no pnpm range), content/docs/deployment/cli.mdx os dev section, and packages/cli/src/index.ts for question (d). The diffed dev.ts was read at the head for the context its hunks sit in.

① Derived judgments

(a) watch gains allowNo: true — right.

  • Accept set: gains exactly one token, --no-watch, which at BASE was a parse-time "Nonexistent flag" exit 2 (card measurement, reproduced by the dev). -w, bare --watch, every other flag and the positional are untouched by the diff. --watch=false parses exactly as before — oclif has no =value form for a boolean, so it is --watch plus the PACKAGE false (pinned: watch: true, package: 'false'). No argv that parsed before parses differently now.
  • Declared default default: true is unchanged and accurate: bare os dev gives watch: true and devWatchActive true (pinned). Help text "Watch objectstack.config.ts and src/, rebuilding on change (default: on). Disable with --no-watch." matches the watcher (watchPaths = [configPath] plus src/ when present; rebuild then restart) and the new spelling; oclif also renders --[no-]watch, so the "Disable with" clause is redundant, not wrong.
  • flags.watch !== false && !flags.artifact && configExists to devWatchActive({ watch, artifact, configExists }) is equivalent for a boolean watch. The loop comment's --watch=false becoming --no-watch corrects a spelling the CLI never read.

(b) --fail-if-no-match — right, and it is the only newly refused run on a supported pnpm.

  • The token is appended only on the packageName !== 'all' branch; os dev and os dev all at a workspace root still run bare pnpm dev, unchanged. With a PACKAGE, pnpm exits non-zero when the filter selects zero projects; the branch's existing catch prints ✗ Development mode failed: Command failed: pnpm --filter VALUE --fail-if-no-match dev and exits 1, so the value is named (through the echoed command, after pnpm's own "No projects matched the filters" line). At BASE that run exited 0 having started nothing — the card's second measurement.
  • A filter that matches keeps working (dev's positive control os dev @objectstack/verify ran that package's tsc -w). A matching filter whose packages lack a dev script is pnpm's business either way; the token does not touch it. So on pnpm 8.13.1 or later the no-match PACKAGE is the only newly non-zero run.
  • Letting pnpm judge its own filter grammar instead of re-reading the workspace in the CLI is the one-owner rule (PD Add comprehensive test suite for Zod schema validation #12, route and surface ownership Add metamodel interfaces for ObjectQL/ObjectUI contract #1). Right.
  • The floor: on a pnpm before 8.13.1 the token is an unknown option, so every PACKAGE-filtered run fails there — loudly (exit 1, command echoed) but without naming the floor. The repo declares pnpm 10.31.0, @objectstack/cli declares node 22 or later and no pnpm range, and the changeset states the floor. Accepted; graded in ② and flagged in ③.

(c) --no-watch refused in monorepo orchestration mode — right, minimal, inside the surface.

  • Reachability: (a) makes os dev PKG --no-watch, and os dev --no-watch at a workspace root without objectstack.config.ts, parse; both route to the orchestration branch (the single-environment gate is packageName === 'all' && (configExists || flags.artifact)). Without the block the CLI would print 🔄 Watch: enabled directly under the operator's --no-watch and run each package's own dev script — the declared-not-delivered shape PD chore: version packages #10 forbids and the card exists to close. Accepting and silently ignoring the flag would recreate the exit-0 no-op.
  • Newly refused set: nothing that ever worked — every argv it refuses was exit 2 at BASE.
  • Minimal: five lines, printError plus one remedy line naming both single-environment routes (config in cwd, or --artifact), process.exit(1) matching the branch's sibling refusal ("Config file not found"), no new flag, no tracker number in the prose. Ordered after the "Config file not found" check, so a directory with neither config nor workspace still gets the more useful error.
  • Surface: packages/cli/src/commands/dev.ts, the file the claim names; declared as deviation 1 with a must-fix rationale I accept.

(d) Exported devWatchActive — does not widen @objectstack/cli's published surface.

  • The exports map is closed to ., ./console, ./hook-body and ./package.json; there is no ./commands/* subpath and node 22 or later honours the map, so dist/commands/dev.js is not importable by a consumer. The . barrel (src/index.ts, untouched by the diff) re-exports only default as DevCommand from ./commands/dev.js, so the named export never reaches dist/index.d.ts. forwardSeedSettledToParent is already a named export of the same file with the same reachability — prior art. oclif's pattern loader reads the module's default export only. The pin reaches it by the relative ../src/commands/dev.js import, inside the package.

(e) Changeset .changeset/20681-dev-no-watch.md — accurate, with one wording note.

  • '@objectstack/cli': patch, Clause-②: no; the 20681- filename prefix is the repo's convention (every sibling at the merge-base is named that way); the body carries no tracker number.
  • Every sentence checked against the diff and the measurements: exit 2 before; --watch=false parsed as the PACKAGE false and exited 0 before; --no-watch off and bare on; no-match PACKAGE exits 1 naming the value through --fail-if-no-match; --no-watch in monorepo mode (PACKAGE given, or workspace root without config) exits 1 naming the flag; the remedy "project directory, or --artifact" matches the routing condition. Nothing claimed that the diff does not deliver.
  • The floor is stated ("Monorepo mode now needs pnpm 8.13.1 or later"). Wording note: the token is passed only with a PACKAGE argument, so os dev and os dev all at a workspace root carry no floor; the sentence is over-broad by that case, in the conservative direction. The seat may tighten it to "Monorepo mode with a PACKAGE argument" before landing; not verdict-bearing.

Gates and pin. All seven required contexts success on the head (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance, Governed Surface Queue Guard); Check Changeset success; the three skipped runs are path-filtered opt-ins. The pin drives the parse-and-decide half through oclif's own Parser.parse over Dev.flags and Dev.args, and the shell half through three spawned runs with a control proving the fixture starts when asked; the three ablation legs each went red on exactly the asserted cases. Its only escape from the package is to node_modules/.bin/tsx, which the cross-package gate deliberately does not flag. That the pin executed in CI rests on its integration-tier declaration plus Test Core's green; it was not separately verified here.

② Semver level

Decision: keep @objectstack/cli patch with bare Clause-②: no — the dev's option A. No ADR-0087 disposition is owed.

By AGENTS.md's changeset section the (narrowing) arm is BREAKING and is about a published contract, so the question is what the published door declared:

  1. The no-match PACKAGE. cli.mdx declares os dev [package] as "Start development mode with hot reload", shape 3 as "Monorepo root … orchestrates pnpm -r dev across packages", the example as os dev my-package # Workspace package (monorepo orchestration mode), and the positional's own help is "Package name or filter pattern". The declared value set is a workspace package or filter; the promise is to start development mode. A PACKAGE that selects nothing was never inside that set and never received the promise — the exit-0 nothing-started run is the card's defect, not an effect any caller was getting. Refusing it pulls the door back to the contract the docs already state. That is PR fix(runtime,metadata-protocol): the /automation write doors keep the packaged-base lock the /meta door keeps (#20679) #20817's pattern, the negative boundary of clause ② with no arm. PR fix(rest): /import reads a time cell by core's one time rule, so an exported 10:00:00.250 re-imports (#20722) #20829's pattern would apply if the refused value had sat inside the door's declared set with an effect callers relied on; here it did not.
  2. --no-watch. The flag's help said "(default)" and the code documented its off branch ("Skipped when: … user opted out"); the diff makes a declared opt-out reachable. A fix of declared-not-delivered, not a new capability — so not yes; and no (widening) is malformed under AGENTS.md anyway.
  3. The orchestration-mode refusal refuses only argv that was exit 2 before. No arm.
  4. The pnpm floor. The docs name pnpm with no version; the repo's only declared pnpm is 10.31.0; @objectstack/cli declares node 22 or later and no pnpm range. 8.13.1 (December 2023) predates node 22 and every pnpm the repo has ever declared, so no published contract admitted a lower pnpm. A newly stated requirement below every declared floor is not the narrowing of a declared range, and the changeset states it — the text an upgrading agent greps. Had the repo declared a pnpm range that 8.13.1 cuts into, this would be (narrowing), minor, with a disposition; it does not.

③ Boundary flags

Open question (bare no vs no (narrowing)): answered in ② — A, bare no, patch.

Deviations

  1. Orchestration-mode --no-watch refusal — accepted, judged in ①(c).
  2. devWatchActive extraction and export — accepted, ①(a) and ①(d): behaviour-preserving, no surface widening.
  3. Ablation leg A's first try was a no-op, re-run with a disjoint replacement — accepted; declared, and the re-run went red on the two asserted cases.
  4. Gate chunk 43-63 backgrounded and waited on a recorded PID — accepted; multi-agent discipline §8 permits waiting on a PID you recorded.
  5. Attribution: all three commits carry the model-free Claude-Session / Co-authored-by: Claude pair (verified on the branch) and the PR body carries the session-URL footer — AGENTS.md's form, to which the harness reminder is subordinate. Accepted.
  6. One SendMessage reply to the PM probe — procedural, accepted.
  7. Cleanup (servers ended by their own timeouts, worktree removed) — accepted.

Out-of-scope findings

  1. The cli.mdx os dev options table omits -w, --[no-]watch, --[no-]restart, --log-level and --preset (confirmed at the merge-base; --admin-email and --admin-password appear inline in the seed-admin row, not as rows). Pre-existing, the docs promise less rather than more, and the file is outside the claim's surface, so the acceptance note is the right carrier under PD chore: version packages #10. Escalated to the seat as a docs card to file: the CHANGELOG this PR ships now names --no-watch and the docs page's table does not.
  2. Monorepo orchestration mode forwards none of dev's other flags (--port, --fresh, --ui, --artifact with a PACKAGE) and says nothing — unmeasured. If it reproduces it is a declared-not-delivered shape (PD chore: version packages #10). Escalated to the seat: measure one (os dev PKG -p N at the repo root) and file a card naming the repro if it holds. Not this PR's scope.

Reviewer's own flags
3. Changeset wording: "Monorepo mode now needs pnpm 8.13.1 or later" is over-broad by the no-PACKAGE case (①(e)); tighten before landing if the seat wishes. Not verdict-bearing.
4. Floor enforcement: on a pnpm below 8.13.1 the failure is pnpm's "Unknown option" surfaced through the CLI's existing ✗ Development mode failed line — loud, but it does not name the floor. Whether shell-out dependency floors should be declared mechanically (engines.pnpm, or a version probe before the spawn) is a repo-wide decision, not this card's; noted for the seat, no card required now.

Implemented-by: claude/issue-20681-dev-no-watch
Reviewed-by: session_01VvcEokUG1tvVxkceYfR5XB

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 11:40
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit c90f9fb Sep 30, 2026
35 of 36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20681-dev-no-watch branch September 30, 2026 11:58
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ackages/observability/src and packages/verify/src to the commits that decided them (objectstack-ai#20842)

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

## What changed

This is stage 13 of the `domain:cli` lane of the dead-citation sweep:
`packages/observability/src` and `packages/verify/src` in **one PR**.
The maintainer ruled that in the handover `5903477632` on the card
(「合成一个 PR (Recommended)」). The ruling changes the one-package-per-PR
direction for these two packages only, so each package's before and
after census counts are listed separately below.

Every comment site in these two trees that cited a tracker number
answering 404 now cites the commit in this repository's history that
made the decision the line describes. The form is ruling C+D's form C
(comment `5749154545` on objectstack-ai#19123), as stages 1 to 12 of this card applied
it. The last stage was `plugin-dev`, landed as `f7c6d65f5`. The card
stays open for its later stages, so this PR says `Part of`.

In total, **4 sites on 4 lines in 3 files, covering 3 numbers**, now
cite **3 distinct commits**:

- `observability`: **1 site**, `src/semconv.ts:49` (a census site);
- `verify`: **3 sites**:
  - `src/harness.ts:580` (a census site);
- `src/erasure-transaction-authorization.test.ts:163` and `:167`. These
are test-file comments, which the census defers. Stages 1 to 12 took
test comments too.

Only comments changed: **4 lines out, 4 in**. Every one of them is a
site, with no reflow and no companion line. Every touched file keeps its
line count (178 / 955 / 185 at base and head), so no line citation into
these files moves.

**No citation number is added.** The only tracker numbers on added lines
are `objectstack-ai#9650` and `objectstack-ai#9835` at `semconv.ts:49`. The removed line already
carried both, and both answer 200. No PR number stands on an added line.

No ADR or ruling-record file records any of the three decisions. A grep
of `docs/adr/` and `scripts/adr-anchors/` for the 3 numbers, the 3 shas,
`afterResponse` and `http_request_duration_ms` reads 0 hits; the control
number `7329` reads 1 file in the same tree. So all three anchors are
commits.

**One `patch` changeset**, for `@objectstack/observability` only
(`.changeset/20594-observability-provenance-anchors.md`). Its rewritten
`//` line reaches the published `dist`. The `verify` rewrites leave
`verify`'s `dist` byte-identical, so `verify` takes no changeset. Both
results are measured below.

## Census, before and after, one package at a time

**Instrument:** the gate's own `node scripts/check-issue-citations.mjs
--census --json`, read-only and unchanged. The count is its
`allocated-but-absent` findings under each package's path. Both runs
enumerated the whole board (187 pages).

| package | before (base `22e584c9db`) | after (head `62e556317c`) |
|---|---|---|
| `packages/observability` | **1** (`src/semconv.ts:49`, `objectstack-ai#10004`) |
**0** |
| `packages/verify` | **1** (`src/harness.ts:580`, `objectstack-ai#10943`) | **0** |
| whole repository | 752 | 750 |

| run | tree | when (UTC) | board |
|---|---|---|---|
| before | base `22e584c9db` | 2026-09-30 10:33:12 to 10:37:06 |
frontier objectstack-ai#20838, 18,665 numbers |
| after | head `62e556317c` | 2026-09-30 10:49:43 to 10:53:16 | frontier
objectstack-ai#20839, 18,666 numbers |

The whole-repo drop of 2 is exactly these two sites. A site-by-site diff
of the two JSON outputs has 2 findings gone (`semconv.ts:49`,
`harness.ts:580`) and 0 added. The other three tallies are equal in both
runs: 33,155 citations that answer 200, 1,985 that answer as pull
requests, and 1,018 cross-repo.

**Supplementary scan (census-invisible spellings, test files and files
outside `src/`).** The scan covers every `#N` token (two to six digits)
in the two packages' 53 tracked files, `CHANGELOG.md` excluded, and
probes each by REST (2026-09-30 10:34Z, re-probed 10:58Z).

- It finds **90 distinct numbers**: 12 in `observability` and 79 in
`verify`, with 1 shared. 86 answer 200 and 4 answer 404: `objectstack-ai#10004`,
`objectstack-ai#10943`, `objectstack-ai#11477` and `objectstack-ai#15145`.
- Dead occurrences in `src/`: 1 in `observability` and 3 in `verify` at
base, 0 and 0 at head. None of them was in a string literal. At head all
232 `#N` tokens under the two `src/` trees answer 200.
- Spellings the census cannot see:
  - `#N-word`: 0.
  - `#A/#B`: 4 sites. Only `semconv.ts:49` held a dead member.
  - `option #N` or `clause #N`: 0.
  - `word-#N`: 1, the `pre-objectstack-ai#11477` at `:167`, rewritten.
- URL forms (`issues/N`, `pull/N`): 0. The only `github.com` strings are
the two `package.json` `repository`/`bugs` URLs.
- A re-grep of the four numbers with no word-boundary operator finds
only `verify/tsconfig.test.json:1` (see Acceptance notes). A control of
the same shape, `objectstack-ai#9835`, reads 2 lines of `semconv.ts`.

## Per-number table

`git blame` at the base (on a full, unshallowed history) ties each line
to the commit that wrote it. That commit's message and diff were read to
decide the anchor.

| number | sites (file:line) | anchor | what that commit decided |
|---|---|---|---|
| `objectstack-ai#10004` | `observability/src/semconv.ts:49` | `1e050a5b1` |
`http_request_duration_ms` is emitted from the
`IHttpServer.afterResponse` transport seam, so p95 latency sees every
inbound surface. It is the squash of the pull request numbered `objectstack-ai#10004`,
and the line cites it beside the two live numbers for the same seam
move. The line was written by `914c41302` (the
`http_request_errors_total` retirement), whose changeset, now the
released entry at `observability/CHANGELOG.md:985`, cites `objectstack-ai#10004` for
this same seam move. |
| `objectstack-ai#10943` | `verify/src/harness.ts:580` | `46d34ab7c` | The host
importer's undeclared fallback resolves from the caller's base, not from
`@objectstack/types`, and `bootStack` hands in `(s) => import(s)`. The
line blames to this commit. Stages 3 and 4 used the same anchor for the
same number in `cli` and `types`. |
| `objectstack-ai#11477` | `verify/src/erasure-transaction-authorization.test.ts:163`,
`:167` | `6dd3e6968` | `/admin/remove-user` gets the raw-mount shading
whose `gateAdmin` runs before the break-glass guard (ruled option A, as
its message records), so a plain member hears `PERMISSION_DENIED`. Both
lines blame to this commit. Its squashed message includes the
`test(verify)` step that rewrote this pin. The `plugin-auth` stage used
the same anchor for the same number. |

All three shas were checked the same way:

- Each has exactly 1 match under `rev-parse --disambiguate`.
- Each is a commit with one parent.
- `merge-base --is-ancestor` of each against base `22e584c9db` exits 0,
on a history that is not shallow (`--is-shallow-repository` false).
- The control legs: `1e050a5b1^` against the base exits 0, and
base-as-ancestor-of-`1e050a5b1` exits 1.

### Wording per site

- `semconv.ts:49`: `(objectstack-ai#9650 / objectstack-ai#9835 / objectstack-ai#10004)` becomes `(objectstack-ai#9650 / objectstack-ai#9835 /
commit 1e050a5)`. Precedents for a mixed list:
`cli/src/commands/lint.ts:915`, `mcp/src/plugin.ts:8`.
- `harness.ts:580`: `objectstack-ai#10943:` becomes `Commit 46d34ab:`. The
neighbouring `objectstack-ai#4700:`, `objectstack-ai#4719:` and `objectstack-ai#17911:` labels answer 200 and
stay.
- `erasure-transaction-authorization.test.ts:163`: `objectstack-ai#11477
(maintainer-ruled option A):` becomes `Commit 6dd3e69
(maintainer-ruled option A):`.
- `erasure-transaction-authorization.test.ts:167`: `the pre-objectstack-ai#11477
route` becomes `the route before that commit`, which refers back to
`:163` four lines up.

## Does the rewrite reach `dist`? Measured per package

Both packages were built at base `22e584c9db` (closure plus package,
`pnpm --workspace-concurrency=2 --filter '@objectstack/verify...'
--filter '@objectstack/observability...' build`, VERDICT 0) and again at
head `5a144efc39` (VERDICT 0). All 6 `dist` files of each package were
compared byte for byte.

- **`observability`: reaches `dist`.**
- `index.js` and `index.cjs` differ in exactly 1 line each: the
`semconv.ts:49` comment, which esbuild keeps inside the `SEMCONV` object
literal.
  - `index.d.ts`, `index.d.cts` and both maps are equal.
  - The base `dist` carried `objectstack-ai#10004` in `index.js` and `index.cjs`.
  - ⇒ a `patch` changeset.
- **`verify`: does not reach `dist`.** All 6 files are byte-equal at
base and head, and the base `dist` carried none of the three numbers. ⇒
no changeset.
- **Code-mutation control for `verify`,** which proves that "equal" was
a measurement and not a stale build.
- The mutation went through `scripts/ablation-replace.mjs` in wrap mode.
The anchor `const organizationsPkg = opts.organizationsPackage` went 1
to 0, and the marker `ABLMARK20594S13` went 0 to 1 in the source. The
blob moved `47a921ad0b` to `3f54cdebfe`.
- After a rebuild, `ablation-dist-preflight` found the marker in
`dist/index.js` and `dist/index.cjs`. `index.js`, `index.cjs` and both
maps differ from the head build, and both `.d.ts` files are equal.
- The restore leg: the restored blob equals HEAD `47a921ad0b` and `git
diff HEAD` is empty. After a rebuild, `preflight --absent` reads the
marker absent from all 6 files with a clean tree, and all 6 files are
byte-equal to the head build.
- The first control attempt used an anchor that was a prefix of its own
replacement. The tool refused it (anchor count 1 to 1) and restored the
file, so no build ran on it.
- The whole workspace build, `pnpm exec turbo run build
--filter='./packages/*' --filter='./packages/*/*' --concurrency=2`,
finished 71/71 (all cache hits). The `dist` of both packages is
byte-equal to the head build.

## Token guard

The guard compared TypeScript 6.0.3 parser leaf tokens (`getChildren`,
JSDoc nodes excluded) of the 3 source files at base `22e584c9db` and at
`5a144efc39`, over 3,523 base tokens:

- **the real diff: 0 files differ**;
- comment-insertion control: 0 differ;
- code-insertion control: 3 of 3 differ;
- string control (first character of the first string literal): 3 of 3
differ, first differing kind `StringLiteral`.

The script exited 0, and its controls ran in memory only. A first
attempt with a bare scanner was discarded: it misaligns inside template
literals and reported a false difference.

## Tests, typecheck, lint and gates (head `62e556317c`)

- **`observability` tests:** `pnpm --filter @objectstack/observability
exec vitest run --maxWorkers=2` → Test Files 7 passed (7), Tests 85
passed (85).
- **`verify` tests:** `pnpm --filter @objectstack/verify exec vitest run
--maxWorkers=2` → Test Files 16 passed (16), Tests 120 passed (120). The
package has 16 test files, `erasure-transaction-authorization.test.ts`
included.
- **`verify` typecheck:** `pnpm --filter @objectstack/verify typecheck`
passed, with VERDICT command-exit 0 (`tsc --noEmit`, then
`check:test-typecheck` over `tsconfig.test.json`: 0 files / 0 errors).
`tsc --listFiles` confirms that `tsconfig.json` reaches `harness.ts` and
`tsconfig.test.json` reaches both edited `verify` files.
- **`observability` typecheck:** the package has no `typecheck` script;
it is a DEBT entry in `check-type-check-coverage.mjs` at 11 errors. `tsc
-p packages/observability/tsconfig.json --noEmit` reads 11 errors, all
in `src/__tests__/`, none in `semconv.ts`, and the program includes
`semconv.ts`. `pnpm check:type-check-coverage` and `pnpm
check:type-check-debt` pass (exit 0).
- **`pnpm lint`:** the repo-wide `eslint . --no-inline-config` exits 0
(2026-09-30 10:54:08 to 10:57:25 UTC, at `62e556317c`).
- **Gates:** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` (run with no path) derived **60**
commands, and all 60 were run with their exit codes recorded before any
pipe.
  - 59 exited 0 on the first pass.
- `pnpm check:dual-build-cjs-loads` first exited 3 (PREREQUISITE NOT
MET: no `dist` for 33 packages). After the whole-workspace build it was
run again and exited 0.
- `--ran` reconciliation: 「60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN」,
exit 0.
- The diff-scoped `node scripts/check-issue-citations.mjs` judged the 2
citations this change adds (`objectstack-ai#9650`, `objectstack-ai#9835`); both answer 200, exit 0.
- `pnpm check:nul-bytes` passed, and a control-byte scan of the 4
changed files reads 0.
- **Main moved during the work:** `origin/main` moved 8 commits past the
base (to `1741c5dcb6`). `git diff --name-only` over the two packages and
the changeset path reads 0 files, so this branch was not merged forward.
The merge ref CI builds covers the joint tree.

## Acceptance notes

- **Left outside `src/**`, listed and not changed (a later stage of the
card):** `packages/verify/tsconfig.test.json:1` cites `objectstack-ai#15145`, which
answers 404. The other 14 citations outside `src/**` in the two packages
answer 200: `observability/vitest.config.ts:11`,
`verify/tsconfig.json:5` and `:13`, `verify/tsconfig.test.json:1`, `:2`,
`:32`, `:35`, `:43` and `:60`, and `verify/vitest.config.ts:8`, `:14`,
`:32`, `:63` and `:70`. `README.md` in either package carries no `#N`.
- **Release-owned, not a site:**
`packages/observability/CHANGELOG.md:985` carries `objectstack-ai#10004` (with
`objectstack-ai#9834`) in a released entry. This PR does not edit it.
- **Open-PR overlap** (read 2026-09-30 10:58Z): 13 open PRs. Only the
Version Packages PR objectstack-ai#20639 touches either package, and only in
`CHANGELOG.md` and `package.json`.
- **Not governed:** no path is on the governed-surface register. The
diff changes 19 lines, far under 5,000.

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

---------

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

Projects

None yet

2 participants