Skip to content

feat(cli): Boot diagnostics noise budget, printer half: the line that needs the author first with its fix, stack traces only at debug - #22236

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-22160-dev-noise-budget
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-22160-dev-noise-budget

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #22160 (the printer half; owners' levels routed)
Clause-②: no

The maintainer's ruling, quoted on the card: 「建议给 dev 模式定一个"噪音预算":预期中的降级记 info,堆栈只在 --log-level debug 出现,真正需要动手的那条单独高亮并给一行修法。」 This PR is the printer's half, per the seat's scope cut (claim 6053785493). The level each other lane's line is logged at stays with its owner. The table below names each owner.

What changes (packages/cli/src/utils/format.ts, printBootDiagnostics only)

  • The record that needs the author prints first, once, highlighted, with its one-line fix. A closed table, ACTIONABLE_BOOT_CLASSES, holds one row today: @objectstack/objectql's [action-governance] declared script actions with NO handler (a dead button). It is matched the way fix(cli,plugin-auth): one boot line per warning class, each printed once, and the loopback OAuth notice at info #22097's restatedAbove matches. The highlighted line is the record's statement plus its subjects (the actions list from the record's structured tail). The fix is derived from the producer's own message: the text after its ; , as leadSentence derives a flow reason. Nothing is hand-copied. A reworded producer that drops the ; still prints highlighted and whole. It is never dropped.
  • The header counts the two apart: ⚠ Boot diagnostics — 1 needs your attention · 2 informational:. With nothing actionable the header reads ℹ Boot diagnostics — 4 informational (2 more already listed above):, with no alarm glyph. The exception is a capture that dropped records: the printer has not read those, so the header keeps the ⚠.
  • Stack traces are withheld at the default level. The record's structured tail is located (pretty, text and JSON renderings) and every stack property whose value is a V8 trace is dropped. A stack key that names something stays. The message stays. A record without a trace prints byte-identical. The last line counts what was withheld and points at --log-level debug.
  • The debug half needs no code. At --log-level debug (and info), serve never opens the boot-quiet window (isVerboseBootLevel). The same record streams live as ObjectLogger rendered it, stack included, and no replay block prints. Measured both before and after (below). serve.ts is untouched.
  • fix(cli,plugin-auth): one boot line per warning class, each printed once, and the loopback OAuth notice at info #22097's print-once and restatedAbove rules are unchanged.

Measured (origin/main 7d7943dd, built packages/cli/dist, os dev --fresh)

Blank starter (from packages/create-objectstack/src/templates/blank, namespace support_desk) plus the tutorial's four files, extracted verbatim from build-with-claude-code.mdx as it stood at 7d7943dd, --ui. At that commit Resolve was a script action with an unregistered target. #22204 has since made it a declarative update, which needs no handler (see the re-measure after the merge, below):

before:
  ⚠ Boot diagnostics — 3 warnings logged during startup:
    … WARN [Analytics] No admitObjectRead configured … (one line)
    … WARN [action-governance] declared script actions with NO handler — a button wired to nothing (ADR-0078); add a `body`, or register a handler under the declared `target` {"count":1,"actions":["support_desk_ticket:resolve_ticket"]}
    … WARN Insert operation failed {"object":"sys_migration","error":{"message":"UNIQUE constraint failed: sys_migration.id [statement and bound values redacted]","stack":"SqliteError: … (10 knex/driver/engine frames)"}}
after:
  ⚠ Boot diagnostics — 1 needs your attention · 2 informational:
    ⚠ [action-governance] declared script actions with NO handler — a button wired to nothing (ADR-0078): support_desk_ticket:resolve_ticket
      fix: add a `body`, or register a handler under the declared `target`
    … WARN [Analytics] No admitObjectRead configured … (one line)
    … WARN Insert operation failed {"object":"sys_migration","error":{"message":"UNIQUE constraint failed: sys_migration.id [statement and bound values redacted]"}}
    1 stack trace withheld — run with --log-level debug to watch the boot stream live, stacks included

examples/app-showcase (dev --seed-admin --fresh): before, ⚠ Boot diagnostics — 4 warnings logged during startup (2 more already listed above): with the sys_migration stack. After, ℹ Boot diagnostics — 4 informational (2 more already listed above): with the stack withheld. The four records are [Analytics], [SettingsService], sys_migration and [sharing-rule], none actionable. The banner's two Flows: class lines are unchanged.

--log-level debug on the blank project, before and after: 0 replay blocks; the sys_migration record streams live 1 time, carrying "stack":"SqliteError… 1 time.

On main the blank project shows 3 records, not the card's 4. The loopback OAuth line moved to info in #22097 and is gone at the default level.

Re-measured after merging origin/main (7b926f76, which carries #22204), at head f8a80640. The blank starter plus the tutorial's current four files (Resolve is now operation: 'update' + patch) prints ℹ Boot diagnostics — 2 informational:, which is [Analytics] and the sys_migration insert with its stack withheld, plus the hint line. Nothing there needs the author any more. The two remaining lines are #22154's and #22099's to lower.

Each non-actionable line and its owner (H5)

line (default level) level emitter owner card tracking its level
[Analytics] No admitObjectRead configured and no "security" service registered at init … warn packages/services/service-analytics/src/plugin.ts:1450 domain:services #22154 (open)
Insert operation failed {"object":"sys_migration", …} warn logged at packages/objectql/src/engine.ts:14006. The duplicate insert is plugin-auth's default-org owner bind recording id adr-0093-default-org-owner-bind twice (debug stream, blank project) domain:services (plugin-auth) #22099 (open)
[SettingsService] Pre-bind READ of namespace 'auth' … warn packages/services/service-settings/src/settings-service.ts:774 domain:services none known to this PR
[sharing-rule] active business-unit rule expands to NO recipients … warn packages/plugins/plugin-sharing/src/sharing-rule-service.ts:1524 plugin-sharing none known to this PR
N flow(s) declare a 'schedule' / 'time_relative' trigger but are NOT bound — disabled by deployment policy … banner class line (yellow ⚠), restating service-automation's warn printAutomationSummary (this file) restating packages/services/service-automation/src/plugin.ts:1533 claim routes it to service-automation; see the open question in the report none known to this PR

The dev did not search the tracker for the three "none known" rows. The role file reserves search for the filing seat. Dedupe words are in the report.

Pins

packages/cli/src/utils/format.boot-warning-classes.test.ts (unit tier; 11 existing + 7 new legs):

  • Real producers, all three log formats (it.each over pretty / text / json): a real ObjectLogger writes through the real BootLogCapture. The real runActionGovernanceInventory runs over a ticket whose Resolve is a script action with an unregistered target. The dead button prints once, first, highlighted, ending : support_desk_ticket:resolve_ticket. The next line is fix: plus the inventory's own message after its ; , read off the producer at call time. The header carries 1 needs your attention · 2 informational.
  • Stack withheld at the default level: a failed-insert record built from a real thrown Error is captured with its escaped stack (premise asserted). It prints with object and error.message and no "stack". No escaped frame is anywhere on screen. One hint line names the count and --log-level debug.
  • Stack at --log-level debug: isVerboseBootLevel('debug') holds. A debug-level ObjectLogger writes the same record to the terminal with its stack.
  • Formatter: a stack key that is not a trace prints unchanged. A capture with dropped records keeps the ⚠.
  • fix(cli,plugin-auth): one boot line per warning class, each printed once, and the loopback OAuth notice at info #22097's legs stay green. Three of them asserted the header string verbatim, which is the text this ruling replaces. Those three expectations now read the new header. Their withheld counts are unchanged ((3 more already listed above), (1 more …)), and every class and print-once assertion is untouched.

packages/cli/test/serve-boot-diagnostics-noise-budget.e2e.test.ts (new; integration tier by behaviour, nightly by name). The blank starter is scaffolded through the real on-ramp bin/. Added to it: the tutorial's ticket object, its current declarative Resolve, and one Escalate script action whose target nothing registers. Real os serve then boots. Boot diagnostics highlights exactly Escalate's record, with one fix: line. Its header says 1 needs your attention. Resolve, the control in the same boot, is named on no line, and no stack trace reaches the block. No informational count is pinned, because the owners above are moving those.

Ablations

Each ran on the committed fix through scripts/ablation-replace.mjs (WRAP mode). The unit legs ran at 77f6a48c. The boot-pin leg ran at 77f6a48c and again at f8a80640, red both times; format.ts is the same blob at both. Each anchor hit 1 time, the write was verified on disk by blob change, and each restore was proven blob == HEAD with an empty git diff HEAD. The test subjects import format.ts / boot-log-capture.ts from source, so no dist is involved. Expected direction: red. Observed: red every time.

leg mutation result
A class row's record text no longer matches 3 failed / 18 (the three format legs)
B stack traces kept 1 failed / 18 (the withholding leg)
C fix never split off (at = -1) 3 failed / 18
D any string stack withheld, trace or not 1 failed / 18 (the narrowness leg)
E dropped records no longer keep the alarm 1 failed / 18
G isVerboseBootLevel without debug 1 failed / 18 (the debug leg)
A, boot pin class row's record text no longer matches 1 failed / 3 (the highlight assertion)

Final restore proof: format.ts blob 9dc1b9d9 and boot-log-capture.ts blob d2545d2e equal HEAD's, and git diff HEAD is empty.

Tests and gates

All readings at head f8a80640, after merging origin/main 7b926f76; the merge had no conflicts and the CLI closure was rebuilt, 59/59.

  • pnpm --filter @objectstack/cli typecheck: exit 0. tsc --noEmit passed, then check:test-typecheck: OK … 3 file(s) / 28 error(s) / 6 pinned signature(s), unchanged. Both test files are in a tsc program (--listFilesOnly): the unit pin under tsconfig.json, the e2e under tsconfig.test.json.
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: Test Files 263 passed (263), Tests 3881 passed (3881).
  • The new e2e was run locally because the diff adds an integration-tier file: OS_TEST_TIERS=nightly pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/serve-boot-diagnostics-noise-budget.e2e.test.ts gave Tests 3 passed (3). The rest of the integration tier is declared to CI.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands derives 65 for this change set (5 paths vs merge base 7b926f76). Against the order's list, that adds check:cli-test-child-env and drops check:dispatcher-error-vocabulary and check:route-envelope, which no path here reaches. I ran the union of 68, full pnpm lint included. 67 exit 0. node scripts/check-empty-changeset.mjs --base origin/main exits 1: that is the release-note correction named under Changeset. dispatch-gates --ran: 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN.
  • Full pnpm lint (eslint . --no-inline-config): exit 0.
  • At e1a46a46, before the merge, check:dual-build-cjs-loads printed PREREQUISITE NOT MET because eight packages had no dist, so it measured nothing. After building them it was green (106 require entries across 66 packages load), and it is green at f8a80640.

Changeset

.changeset/22160-boot-diagnostics-noise-budget.md, @objectstack/cli patch.

A pending release note is corrected here, and that needs a person's confirmation. One sentence in .changeset/22073-boot-warning-one-line-per-class.md (pending, not yet released) quoted the old header verbatim: ⚠ Boot diagnostics — 5 warnings logged during startup (8 more already listed above):. This PR changes that header, so the sentence would publish false. It now quotes only the withheld count, (8 more already listed above), which stays true; nothing else in that note moved. node scripts/check-empty-changeset.mjs --base origin/main is red on this PR by its own design. It refuses any edit to a changeset the PR did not add, and for a deliberate correction it asks the PR to name the note and get it confirmed. The alternative is to restore the note from base and let the superseded sentence publish.

Acceptance notes

  • The card is not closed by this PR. Its pin 4 (the blank template boots with 0 warnings; the showcase boots with no non-actionable line above info) depends on the owners in the table above.
  • Boundary: at --log-level info the quiet window does not open either, so a record's stack streams there too. Changing what ObjectLogger renders at info is packages/core's, not this printer's.
  • Candidate actionable classes, not added (the table is closed and grows by a row with a real-producer pin): [action-governance] registered handlers with NO declaration … (its fix is hedged, not ;-shaped), and the runtime-assets Branding asset not served … To fix, … (its fix is introduced by To fix,, not by ; ). Neither appeared on the measured boots.
  • docs/qa/platform-checklist/areas/platform-core.json's showcase boot-health reading classifies the Flows: banner's ⚠ lines and needs no change. On a stock showcase the Boot diagnostics header is now ℹ.

Generated by Claude Code

claude added 6 commits October 8, 2026 06:36
The default banner's Boot diagnostics block now prints a record from a
closed table of actionable classes first, once, highlighted, with the
producer's own one-line fix under it (today: action-governance's
declared script actions with no handler). The header counts the
actionable records apart from the informational ones. A captured
record's stack trace is withheld and counted; --log-level debug streams
the same record live, stack included, because serve never opens the
quiet window at that level.

Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
Co-Authored-By: Claude <noreply@anthropic.com>
The dead-button record from objectql's real action inventory, logged by
the real ObjectLogger in all three formats, prints once, first,
highlighted, with the producer's own fix line, and the header counts it
apart. A captured record's stack is withheld at the default level with
its message kept; at --log-level debug the same record streams live,
stack included. The three header strings the earlier print-once legs
asserted move to the new header, their withheld counts unchanged.

Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
Co-Authored-By: Claude <noreply@anthropic.com>
…e line

The blank starter from the real on-ramp, given the tutorial's ticket
object and its handler-less Resolve action, boots real os serve; Boot
diagnostics highlights exactly that one record with a fix line, counts it
as the one needing attention, and carries no stack trace. No
informational count is pinned.

Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
Co-Authored-By: Claude <noreply@anthropic.com>
Also drops the header wording the pending one-line-per-class entry
quoted verbatim, which this change replaces; its withheld count stays.

Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
Co-Authored-By: Claude <noreply@anthropic.com>
The tutorial now writes Resolve as a declarative update with no handler,
so the boot pin carries it beside a handler-less Escalate script action:
exactly one line is highlighted, and Resolve is named on none. The unit
pin and the changeset stop calling the handler-less shape the tutorial's.

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

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

9 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
  • 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 — 28 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 0e9371f0c6104922e19b9bdaabc993b4d3dbca61 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 0e9371f0c6104922e19b9bdaabc993b4d3dbca61

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Confirmation from the domain:cli seat (#6024) · session_01RWZbGvPFcRKvUqASZtunCU · 2026-10-08T08:16Z.

The edit to .changeset/22073-boot-warning-one-line-per-class.md is a deliberate correction, and the seat confirms it.

  • That pending note quoted the header this PR replaces: ⚠ Boot diagnostics — 5 warnings logged during startup (8 more already listed above):.
  • Both notes ship in the same release. The one-line edit keeps only the still-true half, (8 more already listed above).
  • Restoring it from the base would publish a header that no longer prints.

Check Changeset is red on this PR by its own design: check-empty-changeset.mjs's DELIBERATE CORRECTION class, ruling D on #17712. It is not a required context. The full review is ACCEPT 6055714562 on #22160.

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