Skip to content

Commit 00c332b

Browse files
claude[bot]claude
andauthored
docs(spec): name the millisecond unit in three duration keys' describe() prose (#17953)
Fixes #17786 Clause-②: no Ruling A on #15939 (director seat, 2026-09-11T14:08Z, maintainer 「同意」, decision batch #115) split PR #17635's 21-row delta into seven per-file remediation cards and sequenced the gate itself to land last. This is the one of the seven that needs no rename: > …plus **one small card for the three describe-only sites** (`durationMs` ×2, `estimatedDurationMs`) ## What changed Three `.describe()` strings — the governed prose channel batch #65 named: *"The governed prose channel is `.describe()` / `.meta({ description })` — the text that reaches the reference pages and the published dist"*. Each of these keys already carries `Ms` in its name and already says milliseconds in the JSDoc above it; only the channel an author (very often a model, ADR-0033) actually reads was missing it. | site | key | `.describe()` before | `.describe()` after | |:--|:--|:--|:--| | `packages/spec/src/kernel/plugin-loading.zod.ts:104` | `durationMs` | none at all | `Duration in milliseconds` | | `packages/spec/src/system/app-install.zod.ts:146` | `durationMs` | `Installation duration` | `Installation duration in milliseconds` | | `packages/spec/src/system/deploy-bundle.zod.ts:129` | `estimatedDurationMs` | `Estimated execution time` | `Estimated execution time in milliseconds` | Plus the generated mirror of exactly those three rows under `content/docs/references/**`, and one `patch` changeset. ⛔ **Not a rename.** No key is added, removed or renamed, so there is no ADR-0087 conversion, no tombstone, and `packages/spec/src/migrations/registry.ts` is untouched — this PR does not contend with the open PRs that move that generated file. ## Premise verification, re-measured on this tree The card's table was measured on `origin/main` @ `fce7cd4c46`; this branch is cut from `bd25e897d`. All three sites were re-located **by content**, never by the card's line numbers. | premise | reading on this tree | |:--|:--| | three sites exist, one per file | found by content; the line numbers happen to be unmoved (104 / 146 / 129) | | lit control `z.number` per file | 6 / 2 / 7 — identical to the filing seat's reading | | dark control (fabricated key name) | 0 / 0 / 0 | | `plugin-loading` `durationMs` has no describe at all | confirmed — the whole file carried 2 `.describe(` calls, neither on this key | | the other two have a describe without a unit | confirmed verbatim | | every key name already carries its unit | confirmed — `durationMs`, `durationMs`, `estimatedDurationMs` | | the diff does not move `migrations/registry.ts` | confirmed — 7 changed paths, none of them that file | | `Clause-②: no` still holds on the actual diff | confirmed — no key, no export, no accept/reject outcome moves; `check:api-surface` and `check:authorable-surface` both green | | `system/MigrationPlan:estimatedDurationMs` stays put in the authorable ratchet | confirmed — `authorable-surface.base.json` and `authorable-surface/system.json` are byte-unchanged and still carry the entry (dark control 0) | ## The measurement the card asked for: is a changeset owed? The card and its triage comment both refused to let this be assumed. Measured after `pnpm --filter @objectstack/spec build`, over the paths `packages/spec` `files[]` actually publishes: | probe | `dist/` | `json-schema/` | verdict | |:--|--:|--:|:--| | **subject** `Duration in milliseconds` | 24 | 6 | ships | | **subject** `Installation duration in milliseconds` | 8 | 2 | ships | | **subject** `Estimated execution time in milliseconds` | 8 | 2 | ships | | **positive control** describe `Objects created/updated` | 4 | 2 | ships, as expected | | **negative control** `no exemption by blindness` (lives only under `packages/spec/scripts/`, a path outside `files[]`) | 0 | 0 | does not ship, as expected | | **dark control** a fabricated needle | 0 | 0 | 0 everywhere, so the zeros above are readings | ⇒ **A changeset is owed**, `patch`, `@objectstack/spec`. There is a second published consumer beyond `dist`: the generated reference pages, all three of which render the changed string. ⭐ **One measured refinement worth recording, because it cuts against the obvious reading of "reaches the published dist".** `dist/` alone does **not** discriminate the two prose channels. JSDoc text — and even a bare `//` line comment — rides into the emitted bundles verbatim: the JSDoc-only sentence `Objects created or updated` reads **4** in `dist/` and **0** in `json-schema/`, and the `//` comment `Plugin loaded at runtime` reads **4** in `dist/` and **0** in `json-schema/`. So `dist` presence is necessary and not sufficient evidence that a string reached the governed channel; `json-schema/` is the surface that decides it. The ruling is unaffected — describe prose is the only one of the two that reaches the reference pages and the JSON schemas — but a future round measuring "does it ship" with a `dist` grep alone would get a true answer for a false reason. ## Local verification Heavy runs routed through `scripts/pm/os-verify-lock.sh`; verdicts read from the lock's own `VERDICT command-exit` line, never a bare status. - ① 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 ① is a documented no-op here. - `pnpm --filter @objectstack/spec build` — `VERDICT command-exit 0` (203s, shared box), run before every dist-reading gate. - `pnpm --filter @objectstack/spec typecheck` — `VERDICT command-exit 0` (`tsc --noEmit` + `check:scripts-typecheck` + `check:test-typecheck`). - `pnpm --filter @objectstack/spec test` — `VERDICT command-exit 0`, **475 test files / 13504 tests passed**. - 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. - `pnpm --filter @objectstack/spec check:generated` proved exactly **1 of 15** artifacts stale — `content/docs/references/**` — and it was regenerated with the repo's own generator (`pnpm --filter @objectstack/spec gen:docs`), ⛔ never hand-edited. Re-run after regeneration: all 15 up to date. - Gate families derived from the **actual** change set with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, every command run with its exit code landed to disk, then reconciled with `--ran`. Results are in the report comment on #17786. - Consumer sweep, measured rather than predicted: no tracked file outside `packages/spec` and `content/docs/references` carries any of the three describe strings, and no test file anywhere names `plugin-loading.mdx`, `app-install.mdx` or `deploy-bundle.mdx` (lit control: 145 `.mdx` mentions do exist in test files, so that zero is a reading). The two cross-package tests that read `content/docs/references/**` at all read `automation/time-relative-trigger.mdx` and `api/analytics.mdx`. ## Deviation from the dispatched file surface — declared, not silent The dispatch declared the file surface as the three `.zod.ts` files plus `.changeset/`. This PR also carries **three regenerated `content/docs/references/**` pages**. That is not scope creep: `check:docs` is a required gate, AGENTS.md requires the spec artifacts be regenerated before pushing, and `check:generated` named this artifact and only this artifact as stale. The three regenerated rows are the same three rows this change edits, byte for byte. Nothing else in `content/docs/` moved. ## Acceptance notes - ⛔ `packages/spec/scripts/check-duration-unit-keys.ts` and its self-test are **untouched** — that gate belongs to PR #17635, which Ruling A sequences to land last. Its red on that branch is designed and is not addressed here. - ⛔ `content/docs/releases/` untouched. - `noted, not filed:` `packages/spec/src/kernel/plugin-loading.zod.ts` states every one of its member descriptions in JSDoc and gives only the enclosing object a `.describe()`, so the generated reference table for `PluginLoadingEvent` renders empty description cells for `type`, `pluginId`, `timestamp`, `metadata` and `error`. Only `durationMs` is in this card's population, so only it was given prose. Successor: whoever widens the describe-coverage question past duration keys — no PR or person is queued on that file today, so this is an observation with no carrier, deliberately not filed as a card. - `noted, not filed:` the JSDoc channel reaching `dist/` verbatim (measured above) means the phrase "JSDoc is developer commentary and 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 the ruling depends on the difference, and no gate reads it. Successor: none. ⛔ This PR is a draft and stays one. An in-seat clause-② contract review is owed before it may turn ready or enqueue — the clause-② enqueue gate's path leg fires on `packages/spec/src/**` regardless of the `no` declaration. That review is the PM's step. Authored in session `session_015c5G6TmpMKgnusmTpD7Ntt` (epic PM lane for the #15939 subtree; the claim comment on #17786 is `5651943470`). --- _Generated by [Claude Code](https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f62787c commit 00c332b

7 files changed

Lines changed: 24 additions & 6 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Three duration keys now name their unit in the `.describe()` prose that reaches the published output, not only in the key name and the JSDoc above them: `PluginLoadingEvent.durationMs` (`kernel/plugin-loading.zod.ts`), `AppInstallResult.durationMs` (`system/app-install.zod.ts`) and `MigrationPlan.estimatedDurationMs` (`system/deploy-bundle.zod.ts`).
6+
7+
The first carried no `.describe()` at all, so the generated reference row for `durationMs` rendered an empty description cell; the other two said `Installation duration` and `Estimated execution time`, naming a duration with no unit. All three JSDoc blocks already said milliseconds, and all three key names already carry `Ms`. Only the channel an author — very often a model (ADR-0033) — actually reads was missing it.
8+
9+
⛔ Not a rename, and no key moves: the unit is already in the key name, which is what the #14478 rule asks for. This is the describe-only remediation of Ruling A on #15939, and it is the one of the seven remediations that needs no ADR-0087 conversion, no tombstone and no published-key rename.
10+
11+
**The published surface was measured rather than assumed**, because a changeset is owed only if the changed text actually ships. Measured after `pnpm --filter @objectstack/spec build`, over the paths this package's `files[]` actually publishes:
12+
13+
- **The changed text ships.** `Duration in milliseconds` reads 24 occurrences across 12 `dist/` bundle files and 6 across `json-schema/`; the other two read 8 in `dist/` and 2 in `json-schema/` each. The generated reference pages under `content/docs/references/**` render all three rows and are regenerated in this change.
14+
- **Positive control that ships**: the neighbouring describe `Objects created/updated` — `dist` 4, `json-schema` 2.
15+
- **Negative control that does not ship**: `no exemption by blindness`, a sentence that exists only in `packages/spec/scripts/`, a path outside `files[]` — 0 across every published path, 1 in its own unpublished file.
16+
- **Dark control**: a fabricated needle reads 0 everywhere, so a zero above is a reading rather than a broken instrument.
17+
18+
One measured refinement worth recording for the next author, since it cuts against the obvious reading of "published output": **`dist/` alone does not discriminate the two prose channels.** JSDoc text and even a `//` line comment ride into the emitted bundles verbatim (`Objects created or updated`, JSDoc-only, reads 4 in `dist/`). What separates the channels is `json-schema/`, which carries describe prose and 0 comment prose. So `dist` presence is necessary and not sufficient evidence that a string reached the governed channel; the `json-schema/` reading is the one that decides it.

‎content/docs/references/kernel/plugin-loading.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Plugin loading lifecycle event
4343
| **type** | `Enum<'load-started' \| 'load-completed' \| 'load-failed' \| 'init-started' \| 'init-completed' \| 'init-failed' \| 'preload-started' \| 'preload-completed' \| 'cache-hit' \| … +5 more>` | ✅ | |
4444
| **pluginId** | `string` | ✅ | |
4545
| **timestamp** | `integer` | ✅ | |
46-
| **durationMs** | `integer` | optional | |
46+
| **durationMs** | `integer` | optional | Duration in milliseconds |
4747
| **metadata** | `Record<string, any>` | optional | |
4848
| **error** | `{ message: string; code?: string; stack?: string }` | optional | |
4949

‎content/docs/references/system/app-install.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ App install result
8686
| **installedObjects** | `string[]` | optional (default: `[]`) | Objects created/updated |
8787
| **createdTables** | `string[]` | optional (default: `[]`) | Database tables created |
8888
| **seededRecords** | `integer` | optional (default: `0`) | Seed records inserted |
89-
| **durationMs** | `integer` | optional | Installation duration |
89+
| **durationMs** | `integer` | optional | Installation duration in milliseconds |
9090
| **error** | `string` | optional | Error message on failure |
9191

9292

‎content/docs/references/system/deploy-bundle.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ Ordered migration plan
189189
| **statements** | `{ sql: string; reversible: boolean; rollbackSql?: string; order: integer }[]` | optional (default: `[]`) | Ordered DDL statements |
190190
| **dialect** | `string` | ✅ | Target SQL dialect |
191191
| **reversible** | `boolean` | optional (default: `true`) | Whether the plan can be fully rolled back |
192-
| **estimatedDurationMs** | `integer` | optional | Estimated execution time |
192+
| **estimatedDurationMs** | `integer` | optional | Estimated execution time in milliseconds |
193193

194194
### Nested Shape: `MigrationPlan.statements[number]`
195195

‎packages/spec/src/kernel/plugin-loading.zod.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export const PluginLoadingEventSchema = lazySchema(() => z.object({
101101
/**
102102
* Duration in milliseconds
103103
*/
104-
durationMs: z.number().int().min(0).optional(),
104+
durationMs: z.number().int().min(0).optional().describe('Duration in milliseconds'),
105105

106106
/**
107107
* Additional metadata

‎packages/spec/src/system/app-install.zod.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export const AppInstallResultSchema = lazySchema(() => z.object({
143143
seededRecords: z.number().int().min(0).default(0).describe('Seed records inserted'),
144144

145145
/** Installation duration in milliseconds */
146-
durationMs: z.number().int().min(0).optional().describe('Installation duration'),
146+
durationMs: z.number().int().min(0).optional().describe('Installation duration in milliseconds'),
147147

148148
/** Error message if installation failed */
149149
error: z.string().optional().describe('Error message on failure'),

‎packages/spec/src/system/deploy-bundle.zod.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export const MigrationPlanSchema = lazySchema(() => z.object({
126126
reversible: z.boolean().default(true).describe('Whether the plan can be fully rolled back'),
127127

128128
/** Estimated execution time in milliseconds */
129-
estimatedDurationMs: z.number().int().min(0).optional().describe('Estimated execution time'),
129+
estimatedDurationMs: z.number().int().min(0).optional().describe('Estimated execution time in milliseconds'),
130130
}).describe('Ordered migration plan'));
131131

132132
export type MigrationPlan = z.input<typeof MigrationPlanSchema>;

0 commit comments

Comments
 (0)