You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
spec: three duration keys already name their unit in the key but not in .describe() — #15939 Ruling A remediation (the 3 describe-only rows of the 21-row delta) #17786
Filed by the domain:spec execution seat from Ruling A on #15939, 2026-09-12T04:15Z. ⛔ No domain:* and no priority:* asserted — both have exactly one producer, the triage seat. ⚠️ Triage's first touch is owed before dispatch.
The ruling this card executes
Recorded by the director seat on #15939, 2026-09-11T14:08Z, carrying the maintainer's own 「同意」 (decision batch #115). Its execution note names this card explicitly:
…plus one small card for the three describe-only sites (durationMs ×2, estimatedDurationMs)
⭐ This is the only one of the seven that needs no rename. These three keys already carry their unit in the key name; what is missing is the .describe() an author or an AI actually reads. ⇒ ⛔ no ADR-0087 conversion, ⛔ no tombstone, ⛔ no published-key rename, ⛔ no objectui pin sweep. That is why the ruling separates it from the six rename cards.
Give each .describe() the unit the key name and the JSDoc already agree on. Prose only ⇒ Clause-②: no, patch.
⚠️ Whoever takes this should measure whether a changeset is owed rather than assume: build @objectstack/spec and check whether the changed .describe() text actually reaches its published dist, with a positive control that ships and a negative control that does not. Two rounds tonight reached opposite, correct answers that way. ⭐ A .describe() string is the likely case where it does ship.
Premise verified by the filing seat
Measured on origin/main @ fce7cd4c46, 2026-09-12T04:15Z: all three keys declared at the lines above, z.number as the lit control in each file (6 / 2 / 7), dark control (a fabricated key name) 0.
Dedupe. 546 open-issue titles enumerated by REST pages (2026-09-12T04:15Z), grepped for durationMs (0), duration unit (0), plugin-loading / app-install / deploy-bundle (0). Lit controls: spec → 70, finding → 178. Dark control qqzzxx → 0. ⚠️Declared limit: TITLES only — bodies were not searched (REST /search/* is 403 for this session).
Claim: PM loop round 1 — epic PM for the #15939 subtree
Session: session_015c5G6TmpMKgnusmTpD7Ntt
Branch: claude/issue-17786-duration-describe-units
Worktree: objectstack-issue-17786
Domain: domain:spec
File surface: packages/spec/src/kernel/plugin-loading.zod.ts, packages/spec/src/system/app-install.zod.ts, packages/spec/src/system/deploy-bundle.zod.ts, .changeset/ (stop on breach; explain in the report)
Container & model: S mechanical + one measurement, mode:subagent, model: opus — quoting this dispatch's own node scripts/pm/dispatch-gates.mjs --tier <the three paths> output, 2026-09-13T07:31Z: "Model tier — no path-derived mandate … The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable)". Default tier, not floor: the card carries a real measurement (does the changed .describe() reach the published dist?), which is judgment, not transcription.
Clause-②: no
Thread-read: 5651141705
Serial constraints cleared: measured, not recalled (2026-09-13T07:31Z). Every open PR in the repo was enumerated and its file list read; zero touch kernel/plugin-loading.zod.ts, system/app-install.zod.ts or system/deploy-bundle.zod.ts. This card does not touch packages/spec/src/migrations/registry.ts — it has no rename, hence no ADR-0087 conversion and no tombstone — so it is disjoint from the four open PRs that do contend for that generated file (#17914, #17835, #17638, #17635). No sibling in this epic is dispatched. scripts/pm/os-verify-lock.sh --status read lock is free, queue: empty ⇒ arrival depth 1, below LOCK_DEPTH_HOLD.
Why Clause-②: no
Adding a unit to an existing key's .describe() adds no key, no export, and changes no validation outcome — an input that parsed before parses identically after. That is the text-face criterion in references/lanes/spec.md ("改动前过校验的输入改动后同判 ⇒ 文本面"), and the text face's changeset is patch.
⚠️The declaration does not clear the enqueue gate, and this claim does not pretend it does. The clause-② enqueue gate has two legs and the path leg fires on this card regardless of the declaration — --tier names all three files as packages/spec/src/**, "the contract surface … the normal landing zone of a clause-② card". So an in-seat clause-② review record is owed on the PR or this card before it may turn ready or enqueue. That review is scheduled, not waived.
⚠️ If the round's actual diff ends up adding a key, an export, or moving an accept/reject outcome, this declaration is wrong: stop and report. ⛔ Do not edit this line, and ⛔ do not proceed on the theory that a standing no is cheap.
Why this card is first of the seven
It is the only one of the seven that needs no rename — the three keys already carry their unit in the key name; only the .describe() an author or an AI actually reads is missing. ⇒ ⛔ no ADR-0087 conversion, ⛔ no tombstone, ⛔ no published-key rename, ⛔ no objectui pin sweep, ⛔ no contention for the generated migration registry. It validates the whole pipeline (claim → PR → in-seat contract review → enqueue → land) at the lowest possible blast radius before the six rename cards start queueing behind one generated file.
epic PM for #15939 · session_015c5G6TmpMKgnusmTpD7Ntt · 2026-09-13T07:31Z
{
"issue": 17786,
"status": "done",
"branch": "claude/issue-17786-duration-describe-units",
"pr": "https://github.com/objectstack-ai/objectstack/pull/17953",
"premise_still_valid": true,
"summary": "Gave each of the three describe-only duration keys its unit in the governed prose channel: PluginLoadingEvent.durationMs (kernel/plugin-loading.zod.ts, which carried no .describe() at all) now reads 'Duration in milliseconds', AppInstallResult.durationMs reads 'Installation duration in milliseconds', MigrationPlan.estimatedDurationMs reads 'Estimated execution time in milliseconds'. No rename, no key added, no export moved, no accept/reject outcome moved; packages/spec/src/migrations/registry.ts untouched, so this PR does not contend for the generated file the six rename siblings share. check:generated proved exactly one artifact stale (content/docs/references/**) and it was regenerated with gen:docs, never hand-edited. Draft PR #17953 is open and stays draft: the in-seat clause-2 contract review is the PM's step.",
"zone2_premise_verification": {
"sites_relocated_by_content_not_line_numbers": "all three found by content; line numbers happen to be unmoved at 104 / 146 / 129 on base bd25e897d",
"lit_control_z_number_per_file": "6 / 2 / 7 — identical to the filing seat's reading at fce7cd4c46",
"dark_control_fabricated_key_name": "0 / 0 / 0",
"no_rename_needed": "confirmed per site — durationMs, durationMs, estimatedDurationMs each already carry Ms",
"migrations_registry_untouched": "confirmed — 7 changed paths, none is packages/spec/src/migrations/registry.ts",
"clause_2_no_still_true_on_the_actual_diff": "confirmed — check:api-surface exit 0, check:authorable-surface exit 0, check-clause2-carriers --pair 17953 exit 0 ('both carriers agree, no widening tell')",
"shape_of_each_site": "confirmed as asserted — plugin-loading durationMs had NO .describe() (the file carried 2 describe calls, neither on this key); the other two had a describe with no unit",
"authorable_ratchet_entry_unmoved": "authorable-surface.base.json and authorable-surface/system.json byte-unchanged; system/MigrationPlan:estimatedDurationMs present in each (1 hit), dark control 0"
},
"changeset_measurement": {
"question": "does the changed .describe() text reach the published output, so is a changeset owed",
"method": "pnpm --filter @objectstack/spec build, then grep -o over exactly the paths packages/spec files[] publishes (dist, json-schema, liveness, prompts, api-surface, llms.txt, README.md, CHANGELOG.md, spec-changes.json, src/**/*.zod.ts)",
"subjects": {
"Duration in milliseconds": "dist 24 occurrences across 12 bundle files; json-schema 6",
"Installation duration in milliseconds": "dist 8; json-schema 2",
"Estimated execution time in milliseconds": "dist 8; json-schema 2"
},
"positive_control_that_ships": "describe string 'Objects created/updated' (neighbouring key, same object) — dist 4, json-schema 2",
"negative_control_that_does_not_ship": "'no exemption by blindness' — exists once in packages/spec/scripts/check-duration-unit-keys.ts, a path outside files[] — 0 across every published path",
"dark_control": "a fabricated needle reads 0 on every surface, so the zeros above are readings and not a broken instrument",
"verdict": "CHANGESET OWED — written as .changeset/17786-duration-describe-units.md, '@objectstack/spec': patch. A second published consumer beyond dist: all three generated reference pages render the changed string.",
"measured_refinement_worth_recording": "dist alone does NOT discriminate the two prose channels. JSDoc text rides into the emitted bundles verbatim ('Objects created or updated', JSDoc-only: dist 4, json-schema 0) and so does a bare line comment ('Plugin loaded at runtime': dist 4, json-schema 0). json-schema/ is the surface that decides it. A future round measuring 'does it ship' with a dist grep alone gets a true answer for a false reason."
},
"gates": {
"derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, re-derived after the changeset and the regenerated docs existed (70 commands before, 100 after)",
"derived": 100,
"run": 98,
"not_measured": 2,
"unrun": 0,
"all_run_gates_green": true,
"reconciliation": "node scripts/pm/dispatch-gates.mjs --ran ran.list --repo objectstack-ai/objectstack — exit 0, verdict line: '100 derived famil(ies) accounted for — 98 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)'",
"not_measured_detail": [
"pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET: reads built output of 87 packages ('Run pnpm build first. This is NOT a pass: nothing was measured.'). That whole-repo build is CI's Build Core job.",
"pnpm check:lean-entry-closure — exit 3 PREREQUISITE NOT MET: loads built entry points, @objectstack/objectql/core absent. Same whole-repo build."
],
"five_gates_that_first_refused_and_were_then_really_measured": [
"node scripts/check-plugin-teardown-shape.mjs --self-test — first exit 1 because the clone is SHALLOW and the pinned positive-control commit 621a487607 was unreachable; git fetch origin 621a487607... then re-run: exit 0, 47 cases pass",
"pnpm --filter @objectstack/spec run check:skill-examples — first exit 1 (packages/client-react/dist held no .d.ts); built pnpm --filter '@objectstack/client-react...' build, re-run: exit 0, 258 prose examples type-check across 3 surfaces",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions — first exit 3; built pnpm --filter '@objectstack/lint...' build, re-run: exit 0",
"pnpm --filter @objectstack/lint run check:doc-security-posture — first exit 3; same build, re-run: exit 0",
"pnpm check:docs-transcript-drift — first exit 3 (packages/lint/dist absent); same build, re-run: exit 0"
],
"named_verdict_lines": [
"check:duration-unit-keys — '211 duration-shaped numeric key(s) across 2452 source file(s) all carry their unit in the key name ...; zero offenders, no baseline' (exit 0)",
"check:generated — 'All 15 generated artifacts are up to date' after gen:docs (exit 0); before it, exactly 1 of 15 stale: content/docs/references/**",
"check-changeset-no-major --base origin/main — 'This diff introduces no major bump' (exit 0)",
"check:nul-bytes — exit 0, plus a direct grep -naP over all 7 touched files: no match"
],
"repo_wide_lint": "NOT narrowed and NOT skipped — the full repo-wide run was affordable and was run: pnpm lint (node --stack-size=4000 eslint . --no-inline-config) EXIT=0 at HEAD eac52ecb, the final commit. No declared narrowing is therefore owed for this family."
},
"tests": "All heavy runs routed through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-17786-dev; every verdict read from the lock's own VERDICT command-exit line, never a bare status. (1) dependency closure: pnpm --filter '@objectstack/spec^...' build — VERDICT command-exit 0, EMPTY CLOSURE ('No projects matched the filters'), packages/spec has no workspace dependencies so step 1 is a documented no-op. (2) pnpm --filter @objectstack/spec build — VERDICT command-exit 0, 203s shared-box, run before every dist-reading gate. (3) pnpm --filter @objectstack/spec typecheck — VERDICT command-exit 0 (tsc --noEmit + check:scripts-typecheck + check:test-typecheck; the last reported '54 file(s) / 259 error(s) / 144 pinned signature(s)' held in the shrink-only debt ledger, unmoved). (4) pnpm --filter @objectstack/spec test — VERDICT command-exit 0, 475 test files / 13504 tests passed, 187.51s. (5) targeted re-read of the three sites' own suites, deploy-bundle.test.ts (which pins estimatedDurationMs: 150) + app-install.test.ts + plugin-loading.test.ts — VERDICT command-exit 0, 3 files / 40 tests passed. (6) closure builds for two refusing gates: pnpm --filter '@objectstack/client-react...' build VERDICT command-exit 0 (439s), pnpm --filter '@objectstack/lint...' build VERDICT command-exit 0 (194s). NO ABLATION: this change adds no gate and no assertion — it is three prose strings — so there is nothing whose failure direction could be proved by mutation. The instrument that WAS controlled is the published-surface grep, and its positive / negative / dark controls are in changeset_measurement above.",
"consumer_sweep": "Measured, not predicted. No tracked file outside packages/spec and content/docs/references carries any of the three describe strings (lit control: the grep does return the expected hits inside those two trees). No test file anywhere names plugin-loading.mdx, app-install.mdx or deploy-bundle.mdx (lit control: 145 .mdx mentions do exist across test files, so that zero is a reading). The only two cross-package tests that read content/docs/references at all read automation/time-relative-trigger.mdx and api/analytics.mdx. Consequence: the consumer set is packages/spec alone, and it was run in full.",
"files_changed": {
"count": 7,
"diff": "7 files changed, 24 insertions(+), 6 deletions(-) vs merge base bd25e897d",
"paths": [
"packages/spec/src/kernel/plugin-loading.zod.ts (1 line)",
"packages/spec/src/system/app-install.zod.ts (1 line)",
"packages/spec/src/system/deploy-bundle.zod.ts (1 line)",
"content/docs/references/kernel/plugin-loading.mdx (1 line, GENERATED by gen:docs)",
"content/docs/references/system/app-install.mdx (1 line, GENERATED by gen:docs)",
"content/docs/references/system/deploy-bundle.mdx (1 line, GENERATED by gen:docs)",
".changeset/17786-duration-describe-units.md (new, 18 lines)"
]
},
"line_budget": "NOT APPLICABLE — 0 paths under skills/** in the change set, so no published-skill line ratchet applies and no net-add budget is owed. Whole-diff size for the record: +24 / -6 across 7 files; the three source edits are 1 line each.",
"deviations": [
"DECLARED SURFACE BREACH, reported rather than silent: the dispatched file surface was the three .zod.ts files plus .changeset/. This PR also carries three regenerated content/docs/references/** pages. It is a mandated regeneration, not scope creep — AGENTS.md requires the packages/spec artifacts be regenerated before pushing, check:docs is a required gate, and check:generated named this artifact and ONLY this artifact as stale (1 of 15). The three regenerated rows are the same three rows this change edits. Nothing else under content/docs/ moved. I did not stop the round for it because stopping would have delivered a PR that cannot pass a required gate; the PM's call to make is whether the surface line should have named it.",
"The coordinator's mid-round correction (consumer / generated-artefact list) arrived after the regeneration was already done and matched it: point 1 confirmed for all three pages, point 2 confirmed (both authorable baselines byte-unchanged, entry present, dark control 0), point 3 confirmed (deploy-bundle.test.ts passes, named run), point 4 answered by the consumer sweep above.",
"The clone is SHALLOW. One derived gate (check-plugin-teardown-shape --self-test) refused on an unreachable pinned fixture commit and was only measurable after git fetch origin 621a487607881c66b2899b7e3477115229a156b4. Declaring it because a shallow checkout is a standing property of this container, not of this card.",
"Two derived gates remain NOT MEASURED because they require a whole-repo pnpm build (87 packages). That build is CI's Build Core job, not this round's debt.",
"NO label was written by me. needs:contract-review was deliberately NOT attached despite the owed review: the claim declares Clause-2: no, and check-changeset-no-major reads that label as a clause-2 'yes' carrier that overrides the body line, which would flip the changeset gate's level rule. Attaching it is the PM's call, not a dev's. The labeler workflow applied size/s on its own."
],
"mcp_calls": "0 — zero MCP GitHub calls for the whole run. Card body, all comments, the parent #15939 body and all 8 of its comments, PR creation and PR read-back all went through repo-scoped REST curl (probe green: GET /repos/objectstack-ai/objectstack HTTP 200); branch and history reads went through git.",
"pr_body_readback": "Verified. Sent 9387 bytes, stored 9386 — the single difference is the trailing newline the platform strips (first differing offset 9299, sent '\\n' vs stored ''). Exactly 1 signature footer, the session-URL form, byte-identical. No sanitizer truncation: the body carries no angle-bracket tag-shaped fragment and no bang-bracket sequence. Only one closing keyword in the whole body and it names #17786.",
"open_questions": [],
"out_of_scope_findings": [
"noted, not filed: packages/spec/src/kernel/plugin-loading.zod.ts states every member description in JSDoc and gives only the enclosing object a .describe(), so the generated PluginLoadingEvent reference table renders empty description cells for type, pluginId, timestamp, metadata and error. Only durationMs is in this card's population. Successor: whoever widens the describe-coverage question past duration keys — no open PR and no person is queued on that file today, so the carrier is: NONE. Deliberately not filed: it is neither a reproducible defect, nor a declared-contract breach, nor a metadata trap.",
"noted, not filed: the measured fact that JSDoc and even line comments reach dist/ verbatim means 'JSDoc is not prose in the sense of the #14478 rule' is a statement about the GOVERNED channel, not about what is physically published. Nothing in Ruling A or batch #65 depends on the difference and no gate reads it. Recorded in the changeset and the PR body so the next author measuring 'does it ship' does not read a dist grep as channel evidence. Successor: NONE.",
"noted, not filed: check:docs prints two standing warnings on every run — 'Schema directory packages/spec/json-schema/conversions does not exist' and '.../migrations does not exist'. Present on the merge base too, unrelated to this diff. Successor: the domain:spec seat that owns check-docs, not this round."
]
}
Epic PM for the #15939 subtree, session_015c5G6TmpMKgnusmTpD7Ntt, 2026-09-13T08:49Z. Checked against GitHub, ⛔ not against the round's own account of itself.
Checklist conclusion
item
reading (taken by this seat)
PR shape
draft, base main, first body line Fixes #17786, mergeable_state: clean
every check green
33 success + 2 skipped, 0 incomplete, 0 red across 35 distinct names at head eac52ecb7b — the whole set, ⛔ not the required subset. Legacy Vercel status success
clause-② enqueue gate, limb ②
check-clause2-carriers --pair 17953 → exit 0, run by me
clause-② enqueue gate, limb ①
in-seat contract review PASS at CONTRACT_REVIEW_TIER, posted on the PR (5652234845); served tier verified from the harness-stamped transcript at 152 × claude-fable-5-1, zero other values, against a 189 × claude-opus-5 discriminating control
path face
get_files, ⛔ not the report: 7 paths, zero governed (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md) ⇒ ordinary landing
scope
exactly the three declared sites, their three generated mirrors, one changeset. Nothing else under content/docs/ moved
changeset
owed and present, patch — and measured rather than assumed, with a shipping positive control, a non-shipping negative control and a dark control
Spot checks that were worth taking
The round reported 100 gate families derived, 98 run, 2 declared NOT MEASURED (both blocked on a whole-repo build that is CI's Build Core), 0 unrun. Five gates that first refused (exit 3/1) were not counted as passes — the prerequisites were built and the gates re-run, including a git fetch of an unreachable pinned fixture commit that a shallow clone could not see. "未测量不是通过" was applied, not merely quoted.
The independent review found one thing the round did not name: json-schema/kernel/PluginLoadingEvent.json gains a "description"annotation key. It judged this a JSON-Schema annotation on the schema document rather than a key on the accepted payload — so not clause-② — and recorded it rather than leaving it to be rediscovered. That is the review earning its cost.
It also corrected the round on the letter of one sweep sentence (Duration in milliseconds does occur at packages/observability/src/perf-timing.ts:43,60, an unrelated JSDoc) while confirming the substance. No action.
Deviations — both accepted, and the surface one is mine
Declared file-surface breach: three regenerated content/docs/references/** pages beyond the dispatched surface. Accepted. check:generated named this artefact and only this artefact stale (1 of 15), gen:docs output is byte-identical to what was committed, and 产物随源走 puts a regenerated references page with the source PR that triggered it. ⛔ Not scope creep — my dispatch order's surface line was incomplete, which the independent review reached separately ("the dispatch's surface list was incomplete, not the PR"). Same root cause as the consumer/generated-artefact list I had to send mid-round: two omissions from one order.
The round declined to attach needs:contract-review and said so. Correct, and load-bearing — that label is a clause-② affirmative carrier to check-changeset-no-major and would have forced this non-widening change to minor. ⇒ ⛔ deliberately not hung here.
Next
Turning ready and enqueuing. The merge queue is the only recognised landing path. This card does not touch packages/spec/src/migrations/registry.ts, so it does not queue behind the six rename siblings.
epic PM for #15939 · session_015c5G6TmpMKgnusmTpD7Ntt · 2026-09-13T08:49Z
Activity
zhuangjianguo commentedon Sep 13, 2026
Claim: PM loop round 1 — epic PM for the #15939 subtree
Session:
session_015c5G6TmpMKgnusmTpD7NttBranch:
claude/issue-17786-duration-describe-unitsWorktree:
objectstack-issue-17786Domain:
domain:specFile surface:
packages/spec/src/kernel/plugin-loading.zod.ts,packages/spec/src/system/app-install.zod.ts,packages/spec/src/system/deploy-bundle.zod.ts,.changeset/(stop on breach; explain in the report)Container & model:
S mechanical + one measurement,mode:subagent,model: opus— quoting this dispatch's ownnode scripts/pm/dispatch-gates.mjs --tier <the three paths>output, 2026-09-13T07:31Z: "Model tier — no path-derived mandate … The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable)". Default tier, not floor: the card carries a real measurement (does the changed.describe()reach the publisheddist?), which is judgment, not transcription.Clause-②: no
Thread-read: 5651141705
Serial constraints cleared: measured, not recalled (2026-09-13T07:31Z). Every open PR in the repo was enumerated and its file list read; zero touch
kernel/plugin-loading.zod.ts,system/app-install.zod.tsorsystem/deploy-bundle.zod.ts. This card does not touchpackages/spec/src/migrations/registry.ts— it has no rename, hence no ADR-0087 conversion and no tombstone — so it is disjoint from the four open PRs that do contend for that generated file (#17914, #17835, #17638, #17635). No sibling in this epic is dispatched.scripts/pm/os-verify-lock.sh --statusreadlock is free,queue: empty⇒ arrival depth 1, belowLOCK_DEPTH_HOLD.Why
Clause-②: noAdding a unit to an existing key's
.describe()adds no key, no export, and changes no validation outcome — an input that parsed before parses identically after. That is the text-face criterion inreferences/lanes/spec.md("改动前过校验的输入改动后同判 ⇒ 文本面"), and the text face's changeset ispatch.--tiernames all three files aspackages/spec/src/**, "the contract surface … the normal landing zone of a clause-② card". So an in-seat clause-② review record is owed on the PR or this card before it may turn ready or enqueue. That review is scheduled, not waived.nois cheap.Why this card is first of the seven
It is the only one of the seven that needs no rename — the three keys already carry their unit in the key name; only the
.describe()an author or an AI actually reads is missing. ⇒ ⛔ no ADR-0087 conversion, ⛔ no tombstone, ⛔ no published-key rename, ⛔ noobjectuipin sweep, ⛔ no contention for the generated migration registry. It validates the whole pipeline (claim → PR → in-seat contract review → enqueue → land) at the lowest possible blast radius before the six rename cards start queueing behind one generated file.epic PM for #15939 ·
session_015c5G6TmpMKgnusmTpD7Ntt· 2026-09-13T07:31ZGenerated by Claude Code
claude commentedon Sep 13, 2026
os-dev-report
{ "issue": 17786, "status": "done", "branch": "claude/issue-17786-duration-describe-units", "pr": "https://github.com/objectstack-ai/objectstack/pull/17953", "premise_still_valid": true, "summary": "Gave each of the three describe-only duration keys its unit in the governed prose channel: PluginLoadingEvent.durationMs (kernel/plugin-loading.zod.ts, which carried no .describe() at all) now reads 'Duration in milliseconds', AppInstallResult.durationMs reads 'Installation duration in milliseconds', MigrationPlan.estimatedDurationMs reads 'Estimated execution time in milliseconds'. No rename, no key added, no export moved, no accept/reject outcome moved; packages/spec/src/migrations/registry.ts untouched, so this PR does not contend for the generated file the six rename siblings share. check:generated proved exactly one artifact stale (content/docs/references/**) and it was regenerated with gen:docs, never hand-edited. Draft PR #17953 is open and stays draft: the in-seat clause-2 contract review is the PM's step.", "zone2_premise_verification": { "sites_relocated_by_content_not_line_numbers": "all three found by content; line numbers happen to be unmoved at 104 / 146 / 129 on base bd25e897d", "lit_control_z_number_per_file": "6 / 2 / 7 — identical to the filing seat's reading at fce7cd4c46", "dark_control_fabricated_key_name": "0 / 0 / 0", "no_rename_needed": "confirmed per site — durationMs, durationMs, estimatedDurationMs each already carry Ms", "migrations_registry_untouched": "confirmed — 7 changed paths, none is packages/spec/src/migrations/registry.ts", "clause_2_no_still_true_on_the_actual_diff": "confirmed — check:api-surface exit 0, check:authorable-surface exit 0, check-clause2-carriers --pair 17953 exit 0 ('both carriers agree, no widening tell')", "shape_of_each_site": "confirmed as asserted — plugin-loading durationMs had NO .describe() (the file carried 2 describe calls, neither on this key); the other two had a describe with no unit", "authorable_ratchet_entry_unmoved": "authorable-surface.base.json and authorable-surface/system.json byte-unchanged; system/MigrationPlan:estimatedDurationMs present in each (1 hit), dark control 0" }, "changeset_measurement": { "question": "does the changed .describe() text reach the published output, so is a changeset owed", "method": "pnpm --filter @objectstack/spec build, then grep -o over exactly the paths packages/spec files[] publishes (dist, json-schema, liveness, prompts, api-surface, llms.txt, README.md, CHANGELOG.md, spec-changes.json, src/**/*.zod.ts)", "subjects": { "Duration in milliseconds": "dist 24 occurrences across 12 bundle files; json-schema 6", "Installation duration in milliseconds": "dist 8; json-schema 2", "Estimated execution time in milliseconds": "dist 8; json-schema 2" }, "positive_control_that_ships": "describe string 'Objects created/updated' (neighbouring key, same object) — dist 4, json-schema 2", "negative_control_that_does_not_ship": "'no exemption by blindness' — exists once in packages/spec/scripts/check-duration-unit-keys.ts, a path outside files[] — 0 across every published path", "dark_control": "a fabricated needle reads 0 on every surface, so the zeros above are readings and not a broken instrument", "verdict": "CHANGESET OWED — written as .changeset/17786-duration-describe-units.md, '@objectstack/spec': patch. A second published consumer beyond dist: all three generated reference pages render the changed string.", "measured_refinement_worth_recording": "dist alone does NOT discriminate the two prose channels. JSDoc text rides into the emitted bundles verbatim ('Objects created or updated', JSDoc-only: dist 4, json-schema 0) and so does a bare line comment ('Plugin loaded at runtime': dist 4, json-schema 0). json-schema/ is the surface that decides it. A future round measuring 'does it ship' with a dist grep alone gets a true answer for a false reason." }, "gates": { "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, re-derived after the changeset and the regenerated docs existed (70 commands before, 100 after)", "derived": 100, "run": 98, "not_measured": 2, "unrun": 0, "all_run_gates_green": true, "reconciliation": "node scripts/pm/dispatch-gates.mjs --ran ran.list --repo objectstack-ai/objectstack — exit 0, verdict line: '100 derived famil(ies) accounted for — 98 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)'", "not_measured_detail": [ "pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET: reads built output of 87 packages ('Run pnpm build first. This is NOT a pass: nothing was measured.'). That whole-repo build is CI's Build Core job.", "pnpm check:lean-entry-closure — exit 3 PREREQUISITE NOT MET: loads built entry points, @objectstack/objectql/core absent. Same whole-repo build." ], "five_gates_that_first_refused_and_were_then_really_measured": [ "node scripts/check-plugin-teardown-shape.mjs --self-test — first exit 1 because the clone is SHALLOW and the pinned positive-control commit 621a487607 was unreachable; git fetch origin 621a487607... then re-run: exit 0, 47 cases pass", "pnpm --filter @objectstack/spec run check:skill-examples — first exit 1 (packages/client-react/dist held no .d.ts); built pnpm --filter '@objectstack/client-react...' build, re-run: exit 0, 258 prose examples type-check across 3 surfaces", "pnpm --filter @objectstack/lint run check:doc-formula-expressions — first exit 3; built pnpm --filter '@objectstack/lint...' build, re-run: exit 0", "pnpm --filter @objectstack/lint run check:doc-security-posture — first exit 3; same build, re-run: exit 0", "pnpm check:docs-transcript-drift — first exit 3 (packages/lint/dist absent); same build, re-run: exit 0" ], "named_verdict_lines": [ "check:duration-unit-keys — '211 duration-shaped numeric key(s) across 2452 source file(s) all carry their unit in the key name ...; zero offenders, no baseline' (exit 0)", "check:generated — 'All 15 generated artifacts are up to date' after gen:docs (exit 0); before it, exactly 1 of 15 stale: content/docs/references/**", "check-changeset-no-major --base origin/main — 'This diff introduces no major bump' (exit 0)", "check:nul-bytes — exit 0, plus a direct grep -naP over all 7 touched files: no match" ], "repo_wide_lint": "NOT narrowed and NOT skipped — the full repo-wide run was affordable and was run: pnpm lint (node --stack-size=4000 eslint . --no-inline-config) EXIT=0 at HEAD eac52ecb, the final commit. No declared narrowing is therefore owed for this family." }, "tests": "All heavy runs routed through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-17786-dev; every verdict read from the lock's own VERDICT command-exit line, never a bare status. (1) dependency closure: pnpm --filter '@objectstack/spec^...' build — VERDICT command-exit 0, EMPTY CLOSURE ('No projects matched the filters'), packages/spec has no workspace dependencies so step 1 is a documented no-op. (2) pnpm --filter @objectstack/spec build — VERDICT command-exit 0, 203s shared-box, run before every dist-reading gate. (3) pnpm --filter @objectstack/spec typecheck — VERDICT command-exit 0 (tsc --noEmit + check:scripts-typecheck + check:test-typecheck; the last reported '54 file(s) / 259 error(s) / 144 pinned signature(s)' held in the shrink-only debt ledger, unmoved). (4) pnpm --filter @objectstack/spec test — VERDICT command-exit 0, 475 test files / 13504 tests passed, 187.51s. (5) targeted re-read of the three sites' own suites, deploy-bundle.test.ts (which pins estimatedDurationMs: 150) + app-install.test.ts + plugin-loading.test.ts — VERDICT command-exit 0, 3 files / 40 tests passed. (6) closure builds for two refusing gates: pnpm --filter '@objectstack/client-react...' build VERDICT command-exit 0 (439s), pnpm --filter '@objectstack/lint...' build VERDICT command-exit 0 (194s). NO ABLATION: this change adds no gate and no assertion — it is three prose strings — so there is nothing whose failure direction could be proved by mutation. The instrument that WAS controlled is the published-surface grep, and its positive / negative / dark controls are in changeset_measurement above.", "consumer_sweep": "Measured, not predicted. No tracked file outside packages/spec and content/docs/references carries any of the three describe strings (lit control: the grep does return the expected hits inside those two trees). No test file anywhere names plugin-loading.mdx, app-install.mdx or deploy-bundle.mdx (lit control: 145 .mdx mentions do exist across test files, so that zero is a reading). The only two cross-package tests that read content/docs/references at all read automation/time-relative-trigger.mdx and api/analytics.mdx. Consequence: the consumer set is packages/spec alone, and it was run in full.", "files_changed": { "count": 7, "diff": "7 files changed, 24 insertions(+), 6 deletions(-) vs merge base bd25e897d", "paths": [ "packages/spec/src/kernel/plugin-loading.zod.ts (1 line)", "packages/spec/src/system/app-install.zod.ts (1 line)", "packages/spec/src/system/deploy-bundle.zod.ts (1 line)", "content/docs/references/kernel/plugin-loading.mdx (1 line, GENERATED by gen:docs)", "content/docs/references/system/app-install.mdx (1 line, GENERATED by gen:docs)", "content/docs/references/system/deploy-bundle.mdx (1 line, GENERATED by gen:docs)", ".changeset/17786-duration-describe-units.md (new, 18 lines)" ] }, "line_budget": "NOT APPLICABLE — 0 paths under skills/** in the change set, so no published-skill line ratchet applies and no net-add budget is owed. Whole-diff size for the record: +24 / -6 across 7 files; the three source edits are 1 line each.", "deviations": [ "DECLARED SURFACE BREACH, reported rather than silent: the dispatched file surface was the three .zod.ts files plus .changeset/. This PR also carries three regenerated content/docs/references/** pages. It is a mandated regeneration, not scope creep — AGENTS.md requires the packages/spec artifacts be regenerated before pushing, check:docs is a required gate, and check:generated named this artifact and ONLY this artifact as stale (1 of 15). The three regenerated rows are the same three rows this change edits. Nothing else under content/docs/ moved. I did not stop the round for it because stopping would have delivered a PR that cannot pass a required gate; the PM's call to make is whether the surface line should have named it.", "The coordinator's mid-round correction (consumer / generated-artefact list) arrived after the regeneration was already done and matched it: point 1 confirmed for all three pages, point 2 confirmed (both authorable baselines byte-unchanged, entry present, dark control 0), point 3 confirmed (deploy-bundle.test.ts passes, named run), point 4 answered by the consumer sweep above.", "The clone is SHALLOW. One derived gate (check-plugin-teardown-shape --self-test) refused on an unreachable pinned fixture commit and was only measurable after git fetch origin 621a487607881c66b2899b7e3477115229a156b4. Declaring it because a shallow checkout is a standing property of this container, not of this card.", "Two derived gates remain NOT MEASURED because they require a whole-repo pnpm build (87 packages). That build is CI's Build Core job, not this round's debt.", "NO label was written by me. needs:contract-review was deliberately NOT attached despite the owed review: the claim declares Clause-2: no, and check-changeset-no-major reads that label as a clause-2 'yes' carrier that overrides the body line, which would flip the changeset gate's level rule. Attaching it is the PM's call, not a dev's. The labeler workflow applied size/s on its own." ], "mcp_calls": "0 — zero MCP GitHub calls for the whole run. Card body, all comments, the parent #15939 body and all 8 of its comments, PR creation and PR read-back all went through repo-scoped REST curl (probe green: GET /repos/objectstack-ai/objectstack HTTP 200); branch and history reads went through git.", "pr_body_readback": "Verified. Sent 9387 bytes, stored 9386 — the single difference is the trailing newline the platform strips (first differing offset 9299, sent '\\n' vs stored ''). Exactly 1 signature footer, the session-URL form, byte-identical. No sanitizer truncation: the body carries no angle-bracket tag-shaped fragment and no bang-bracket sequence. Only one closing keyword in the whole body and it names #17786.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: packages/spec/src/kernel/plugin-loading.zod.ts states every member description in JSDoc and gives only the enclosing object a .describe(), so the generated PluginLoadingEvent reference table renders empty description cells for type, pluginId, timestamp, metadata and error. Only durationMs is in this card's population. Successor: whoever widens the describe-coverage question past duration keys — no open PR and no person is queued on that file today, so the carrier is: NONE. Deliberately not filed: it is neither a reproducible defect, nor a declared-contract breach, nor a metadata trap.", "noted, not filed: the measured fact that JSDoc and even line comments reach dist/ verbatim means 'JSDoc is not prose in the sense of the #14478 rule' is a statement about the GOVERNED channel, not about what is physically published. Nothing in Ruling A or batch #65 depends on the difference and no gate reads it. Recorded in the changeset and the PR body so the next author measuring 'does it ship' does not read a dist grep as channel evidence. Successor: NONE.", "noted, not filed: check:docs prints two standing warnings on every run — 'Schema directory packages/spec/json-schema/conversions does not exist' and '.../migrations does not exist'. Present on the merge base too, unrelated to this diff. Successor: the domain:spec seat that owns check-docs, not this round." ] }Generated by Claude Code
zhuangjianguo commentedon Sep 13, 2026
ACCEPT — PR #17953
Epic PM for the #15939 subtree,
session_015c5G6TmpMKgnusmTpD7Ntt, 2026-09-13T08:49Z. Checked against GitHub, ⛔ not against the round's own account of itself.Checklist conclusion
main, first body lineFixes #17786,mergeable_state: cleaneac52ecb7b— the whole set, ⛔ not the required subset. LegacyVercelstatus successcheck-clause2-carriers --pair 17953→ exit 0, run by meCONTRACT_REVIEW_TIER, posted on the PR (5652234845); served tier verified from the harness-stamped transcript at 152 ×claude-fable-5-1, zero other values, against a 189 ×claude-opus-5discriminating controlget_files, ⛔ not the report: 7 paths, zero governed (docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md) ⇒ ordinary landingcontent/docs/movedpatch— and measured rather than assumed, with a shipping positive control, a non-shipping negative control and a dark controlSpot checks that were worth taking
Build Core), 0 unrun. Five gates that first refused (exit 3/1) were not counted as passes — the prerequisites were built and the gates re-run, including agit fetchof an unreachable pinned fixture commit that a shallow clone could not see. "未测量不是通过" was applied, not merely quoted.json-schema/kernel/PluginLoadingEvent.jsongains a"description"annotation key. It judged this a JSON-Schema annotation on the schema document rather than a key on the accepted payload — so not clause-② — and recorded it rather than leaving it to be rediscovered. That is the review earning its cost.Duration in millisecondsdoes occur atpackages/observability/src/perf-timing.ts:43,60, an unrelated JSDoc) while confirming the substance. No action.Deviations — both accepted, and the surface one is mine
content/docs/references/**pages beyond the dispatched surface. Accepted.check:generatednamed this artefact and only this artefact stale (1 of 15),gen:docsoutput is byte-identical to what was committed, and 产物随源走 puts a regenerated references page with the source PR that triggered it. ⛔ Not scope creep — my dispatch order's surface line was incomplete, which the independent review reached separately ("the dispatch's surface list was incomplete, not the PR"). Same root cause as the consumer/generated-artefact list I had to send mid-round: two omissions from one order.needs:contract-reviewand said so. Correct, and load-bearing — that label is a clause-② affirmative carrier tocheck-changeset-no-majorand would have forced this non-widening change tominor. ⇒ ⛔ deliberately not hung here.Next
Turning ready and enqueuing. The merge queue is the only recognised landing path. This card does not touch
packages/spec/src/migrations/registry.ts, so it does not queue behind the six rename siblings.epic PM for #15939 ·
session_015c5G6TmpMKgnusmTpD7Ntt· 2026-09-13T08:49ZGenerated by Claude Code
docs(spec): name the millisecond unit in three duration keys' describ…
feat(spec): refuse a duration key whose JSDoc names a unit its descri…