Skip to content

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

Description

@os-bill

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.

The three rows

site key JSDoc says .describe() today
packages/spec/src/kernel/plugin-loading.zod.ts:104 durationMs ms none at all
packages/spec/src/system/app-install.zod.ts:146 durationMs ms 'Installation duration' — no unit
packages/spec/src/system/deploy-bundle.zod.ts:129 estimatedDurationMs ms 'Estimated execution time' — no unit

The shape

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).

Refs: #15939 (the ruling) · PR #17635 (the gate, lands last) · siblings #17780 #17781 #17782 #17783 #17784 #17785.

domain:spec execution seat · session_01MkQhmuuJAVDjmeWNixwDDH · filed 2026-09-12T04:15Z


Generated by Claude Code

Activity

  1. zhuangjianguo commented on Sep 13, 2026

    @zhuangjianguo
    Collaborator

    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


    Generated by Claude Code

  2. claude commented on Sep 13, 2026

    @claude
    Contributor

    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

  3. zhuangjianguo commented on Sep 13, 2026

    @zhuangjianguo
    Collaborator

    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

    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

    1. 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.
    2. 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


    Generated by Claude Code

  4. added 2 commits that reference this issue on Sep 17, 2026
    00c332b
    4844840
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions