Skip to content

Commit 17965df

Browse files
os-billclaude
andauthored
feat(spec): publish the dimensionless marker on the reference page (#18684)
Fixes #18500 Clause-②: no PR #18486 landed the `dimensionless` schema marker with its **gate reader** and no **docs half**. Ruling B on #14478 put both halves in the mechanism, verbatim: > declared ON THE SCHEMA, never in a gate ledger … marker the gate honours **and the docs generator publishes** The sibling marker `externalVocabulary` has had its renderer in `packages/spec/scripts/lib/schema-section.ts` since #15626. `dimensionless` had none, so a key marked `dimensionless` would carry an in-schema declaration that the published reference page says nothing about. The gate's own header already asserts the half that did not exist. `check-duration-unit-keys.ts`, exemption class 4: *"Same mechanism as class 2, same literal-only validation, **same visibility**."* Class 2 is `externalVocabulary`, and its visibility **is** that renderer. This PR is what makes that sentence true. ## What landed - `dimensionlessNote(prop)` in `packages/spec/scripts/lib/schema-section.ts`, beside `externalVocabularyNote(prop)`, appended to the same description cell rather than given a column. - Six pins in `packages/spec/scripts/schema-section.test.ts`, mirroring the sibling's block one-for-one. A key described `Failures seen in the last 5 minutes` and carrying `.meta({ dimensionless: 'failed attempts' })` now publishes: ``` Failures seen in the last 5 minutes (dimensionless — counts failed attempts) ``` ### Why that wording The card suggested "counts `what`" and wrote "⛔ no ruling implied", so the shape was read off the declaration instead of taken from the suggestion. The marker's declared value is a **non-empty string literal naming what the number counts** — `check-duration-unit-keys.ts` refuses an empty string and a computed value, because an unverifiable claim exempts nothing — so the declared value is printed verbatim, and the same non-empty-string-literal test decides whether anything is printed at all. The word `dimensionless` is kept in front of it for the one thing the value alone does not state: the exemption's claim is that the number has **no unit**, which is exactly the ambiguity a reader has when the prose beside a naked number names a time unit. It also keeps one vocabulary across the marker name, the census line the gate prints, and the ruling. ## The three questions the dispatch asked **1. Which gate can actually go red on this change?** `packages/spec/scripts/schema-section.test.ts`, run by `pnpm --filter @objectstack/spec test` (vitest project `local`; the file is not listed in `vitest.repo-tests.json`) — in CI, `Test Core`. `check:docs` and `check:generated` structurally cannot move: the live population is **0** keys marked, so no reference page changes and `check:docs` is green both before and after. Established by ablation, not assumed — below. **2. Does the marker ride into the published JSON Schema via `z.toJSONSchema`?** — **Yes.** - Direct, on the zod this workspace installs (4.4.3): `z.number().meta({ dimensionless: 'failed attempts' })` emits `"dimensionless": "failed attempts"` on the node, unchanged. A fabricated marker name rides identically, so the channel is generic rather than per-key. - On the real generated tree, after `pnpm --filter @objectstack/spec gen:schema`: 18 files under `packages/spec/json-schema/` carry `"externalVocabulary"` (LIT control), **0** carry `"dimensionless"`, **0** carry the fabricated name (DARK controls). `packages/spec/json-schema/` has 0 tracked files, so this cannot be read from git — it was built and read. ⇒ **the docs generator is not the only publisher.** `json-schema` is in `packages/spec/package.json` `files[]`, so a marked key would also ship inside the npm tarball. Nothing is marked by this PR, so nothing moves today — but the *second* publisher is a fact the first card to mark a key inherits. **3. What should it print?** — answered from the declared value shape, above. ## Clause-② and the changeset Declared `no`, unchanged — the dispatch reserved that line, and question 2's answer does not move it for **this** diff: no key is marked, so no published byte changes. `skip-changeset`, measured rather than assumed: - `packages/spec/package.json` `files[]` = `dist`, `json-schema`, `liveness`, `prompts`, `llms.txt`, `README.md`, `src/**/*.zod.ts`, `CHANGELOG.md`, `api-surface`, `spec-changes.json`. `scripts/**` is not among them. - `dimensionlessNote` after the build: **0** hits in every one of those paths. Positive control on the same greps, `ObjectSchema`: `api-surface` 2, `llms.txt` 1, `README.md` 1, `src/**/*.zod.ts` 14. - `dist` cannot carry it either way: every `tsup` entry in `packages/spec/tsup.config.ts` is under `src/`, so `scripts/**` is not an entry. - `check:docs` green ⇒ **0** reference pages move. ⚠️ For the seat that marks the first key: at that moment the marker reaches **both** publishers, and `json-schema/` is published — so that card's declaration and changeset are a different question from this one's. ## Testing Union run at `d6f2148892` (the final commit; gate logs carry no sha, so this is the tree every number below was taken on). | run | result | |---|---| | `pnpm --filter @objectstack/spec test` (project `local`) | 484 files passed / 1 skipped, **13872 passed** / 1 skipped | | `pnpm --filter @objectstack/spec typecheck` (incl. `check:scripts-typecheck`, `check:test-typecheck`) | exit 0 | | `pnpm --filter @objectstack/spec check:docs` · `check:variant-docs` · `check:liveness` · `check:empty-state` · `check:strictness-ledger` | exit 0 each | | `pnpm lint` (`eslint . --no-inline-config`, whole repo — not narrowed) | exit 0 | | `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ...` | **57 derived, 52 run, 5 NOT MEASURED, 0 UNRUN** | The 5 NOT MEASURED are `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`, `check:type-check-debt` — each exited **3**, `PREREQUISITE NOT MET`, which those gates spell out as "not a pass and not a finding". All five read built output of the whole monorepo, which this worktree does not have; CI's `Build Core` is where they are measured. This diff adds no export, no entry point and no emitted file, so it has no channel to reach any of them. ### Red before green — ablation with an on-disk proof and a hash-verified restore Run from the committed state, under a `trap ... EXIT INT TERM`, against absolute paths: 1. `HEAD:packages/spec/scripts/lib/schema-section.ts` blob = `a92ba895b1f51e8eaf8945e45f8bf8c12ea77e90`. 2. Anchor count before mutation: **1**. Mutation deletes the `dimensionlessNote(prop)` term from the description cell. 3. **Landed on disk**, both directions: deleted text **0** occurrences after, injected marker **1** occurrence after; mutated blob `4f00017eab...` differs from the HEAD blob. 4. Red leg: `Tests 4 failed | 39 passed (43)` — the four cases that assert the note is printed. The two that assert its **absence** stayed green, which is the predicted direction and what keeps the note from decorating every row in the reference. 5. Restore leg: `git checkout HEAD -- path` (never a bare `git checkout --`), then hash re-read = `a92ba895b1...`, matching, **and** `git diff HEAD` empty. Green re-run at the final head: 43 passed. Predicted direction was stated before the run and matched: turn red, not "more diagnostics" and not "inverted". ## `skills/**` derivative **None.** Reading, not expectation: the diff is exactly 2 files (`git diff --name-only` against the merge base), **0** of them under `skills/`. `packages/spec/scripts/build-skill-docs.ts` does not import `schema-section`, and `check:skill-docs` / `check:skill-refs` were not derived for these paths. So no net-line-count reading is owed. ## Acceptance notes Out of scope, noted and deliberately not filed: - **The `.meta()` channel is generic, not a marker allowlist.** A fabricated key name rides `z.toJSONSchema` into `json-schema/` exactly as `externalVocabulary` and `dimensionless` do, and `json-schema/` is published. This is the documented mechanism (`xRef` / `xExpression` / `xEnumDeprecated` use the same channel by design), not a defect, and none of the three filing classes fits: no repro, no contract violated, and the hazard is metadata being *published* rather than silently dropped or refused. Who will meet this file next: #18124 step 4, the card that will first mark keys. - **Nothing mechanically ties "a marker the gate reads" to "a renderer in `schema-section.ts`".** The next marker added to `check-duration-unit-keys.ts` can repeat exactly this half-build. Not filed: it is undemonstrated drift today, ruling B is honoured on the tree as of this PR, and both markers that exist now carry both halves. ## Dispatch fences observed `packages/spec/src/migrations/**` and `scripts/check-cross-package-test-inputs.mjs` untouched (the latter moved on `main` under #18667 and arrived through a plain merge of `origin/main`, with no conflict and no edit by this branch). `packages/spec/scripts/check-duration-unit-keys.ts` read only. No live key was marked `dimensionless` — population stays 0, by measurement above. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1bc22b3 commit 17965df

2 files changed

Lines changed: 154 additions & 1 deletion

File tree

‎packages/spec/scripts/lib/schema-section.ts‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,42 @@ function externalVocabularyNote(prop: any): string {
259259
return ` (unit per ${standard.trim()})`;
260260
}
261261

262+
/**
263+
* The published half of the `dimensionless` exemption (#15676, ruling B on
264+
* #14478) — the sibling of {@link externalVocabularyNote}, and one grammar with
265+
* it rather than a second one.
266+
*
267+
* A key that carries `.meta({ dimensionless: '<what it counts>' })` is a count,
268+
* a multiplier or a ratio whose describe prose happens to name a time unit
269+
* belonging to something else in the sentence: `Failures seen in the last 5
270+
* minutes` is a number of failures, not a number of minutes. The marker rides
271+
* `z.toJSONSchema` verbatim, the same channel `externalVocabulary` / `xRef` /
272+
* `xExpression` / `xEnumDeprecated` use, so it arrives here as a property of
273+
* the JSON-Schema node.
274+
*
275+
* Printing it is what makes THIS exemption honest, on exactly the argument its
276+
* sibling rests on. `check:duration-unit-keys` exists because a naked number
277+
* beside prose naming a unit leaves the reader guessing; the rename is waived
278+
* here because the number HAS no unit, and that reason is invisible on the
279+
* page unless the page says it. A key exempted silently publishes the same
280+
* guess the gate was built to remove — with the marker printed, the answer is
281+
* stated: no unit, and here is what it counts instead.
282+
*
283+
* Both halves read the SAME declaration the gate reads — a non-empty string
284+
* literal — so an empty or non-string marker, which exempts no key there,
285+
* publishes nothing here. The page must never name a count the contract did
286+
* not.
287+
*
288+
* Appended to the description cell for the reason its sibling is: it qualifies
289+
* the prose already in that cell, and a marker on a handful of keys does not
290+
* earn a column on every table in the reference.
291+
*/
292+
function dimensionlessNote(prop: any): string {
293+
const counts = prop?.dimensionless;
294+
if (typeof counts !== 'string' || counts.trim() === '') return '';
295+
return ` (dimensionless — counts ${counts.trim()})`;
296+
}
297+
262298
/**
263299
* Render one schema's section, heading included.
264300
*
@@ -465,7 +501,9 @@ export function renderSchemaSection(schemaName: string, schema: any, ctx: Sectio
465501
// pipe), then pipes — an unescaped `|` (even inside a code span)
466502
// splits the cell.
467503
const desc = escapeMdxDescription(
468-
((prop.description || '') + externalVocabularyNote(prop)).replace(/\n/g, ' '),
504+
((prop.description || '')
505+
+ externalVocabularyNote(prop)
506+
+ dimensionlessNote(prop)).replace(/\n/g, ' '),
469507
)
470508
.replace(/\\/g, '\\\\')
471509
.replace(/\|/g, '\\|');

‎packages/spec/scripts/schema-section.test.ts‎

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,3 +657,118 @@ describe('externalVocabulary — the published half of the duration-rule exempti
657657
}
658658
});
659659
});
660+
661+
/**
662+
* [#18500] The PUBLISHED half of the `dimensionless` exemption — the sibling of
663+
* the block above, ruling B on #14478: the marker is one the gate honours AND
664+
* the docs generator publishes. `check:duration-unit-keys` shipped the reader
665+
* first; this is the other half.
666+
*
667+
* The argument is its sibling's, one step further. A bare `maxAge` leaves the
668+
* reference-page reader guessing seconds from milliseconds; a bare
669+
* `recentFailures` described as `Failures seen in the last 5 minutes` leaves
670+
* them guessing whether the number IS that span. The marker says it is not —
671+
* it counts failed attempts — and that reason reaches the page only if the
672+
* page prints it.
673+
*
674+
* The marker reaches this renderer as a property of the JSON-Schema node,
675+
* riding `z.toJSONSchema` verbatim (measured on zod 4.4.3 against
676+
* `z.number().meta({ dimensionless: 'failed attempts' })`: the key arrives on
677+
* the emitted node unchanged) — the same channel `externalVocabulary` / `xRef`
678+
* / `xExpression` / `xEnumDeprecated` use.
679+
*
680+
* MEASURED (reverse verification): deleting the `dimensionlessNote(prop)` term
681+
* from the description cell turns the first four cases below red and leaves the
682+
* last two green — the last two assert the note's ABSENCE, which is what keeps
683+
* it from decorating every row in the reference.
684+
*/
685+
describe('dimensionless — the published half of the duration-rule exemption', () => {
686+
const withMarker = (marker: unknown) => ({
687+
type: 'object',
688+
properties: {
689+
recentFailures: {
690+
type: 'number',
691+
description: 'Failures seen in the last 5 minutes',
692+
...(marker === undefined ? {} : { dimensionless: marker }),
693+
},
694+
},
695+
});
696+
697+
it('prints what the number counts, beside the prose whose unit belongs to something else', () => {
698+
const md = renderSchemaSection('HealthSignal', withMarker('failed attempts'));
699+
700+
expect(md).toContain(
701+
'Failures seen in the last 5 minutes (dimensionless — counts failed attempts)',
702+
);
703+
});
704+
705+
it('keeps the describe prose — the note QUALIFIES the number, it does not replace it', () => {
706+
const md = renderSchemaSection('BackoffPolicy', withMarker('retry attempts'));
707+
708+
expect(md).toContain('Failures seen in the last 5 minutes');
709+
expect(md).toContain('(dimensionless — counts retry attempts)');
710+
});
711+
712+
it('renders inside a nested shape table too — one grammar, not two', () => {
713+
const md = renderSchemaSection('RetryConfig', {
714+
type: 'object',
715+
properties: {
716+
backoff: {
717+
type: 'object',
718+
description: 'Backoff options',
719+
properties: {
720+
multiplier: {
721+
type: 'number',
722+
description: 'Growth applied to the previous 30 second delay',
723+
dimensionless: 'the factor each delay is multiplied by',
724+
},
725+
},
726+
},
727+
},
728+
});
729+
730+
expect(md).toContain(
731+
'Growth applied to the previous 30 second delay (dimensionless — counts the factor each delay is multiplied by)',
732+
);
733+
});
734+
735+
it('composes with the sibling marker — a key declaring both publishes both reasons', () => {
736+
// Nothing in the gate makes the two exemptions exclusive, so the cell is
737+
// built by appending both notes rather than choosing between them. Pinned
738+
// because a renderer that picked one would look identical on every key that
739+
// carries only one.
740+
const md = renderSchemaSection('CacheStats', {
741+
type: 'object',
742+
properties: {
743+
maxAge: {
744+
type: 'number',
745+
description: 'Entries seen in the last 60 seconds',
746+
externalVocabulary: 'HTTP Cache-Control `max-age`',
747+
dimensionless: 'cached entries',
748+
},
749+
},
750+
});
751+
752+
expect(md).toContain(
753+
'Entries seen in the last 60 seconds (unit per HTTP Cache-Control `max-age`) (dimensionless — counts cached entries)',
754+
);
755+
});
756+
757+
it('prints nothing for a key that declares no marker — the note is not decoration', () => {
758+
const md = renderSchemaSection('HealthSignal', withMarker(undefined));
759+
760+
expect(md).toContain('Failures seen in the last 5 minutes');
761+
expect(md).not.toContain('dimensionless');
762+
});
763+
764+
it('prints nothing for an empty or non-string marker — an unverifiable claim publishes nothing', () => {
765+
// The gate refuses these too (they exempt no key), so the page must not
766+
// name a count the contract never declared. Held on the SAME inputs from
767+
// both sides so the two halves cannot drift into disagreeing about what
768+
// counts as a declaration.
769+
for (const marker of ['', ' ', 42, null, { counts: 'failed attempts' }]) {
770+
const md = renderSchemaSection('HealthSignal', withMarker(marker));
771+
expect(md, `marker ${JSON.stringify(marker)}`).not.toContain('dimensionless');
772+
}
773+
});
774+
});

0 commit comments

Comments
 (0)