Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion packages/spec/scripts/lib/schema-section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,42 @@ function externalVocabularyNote(prop: any): string {
return ` (unit per ${standard.trim()})`;
}

/**
* The published half of the `dimensionless` exemption (#15676, ruling B on
* #14478) — the sibling of {@link externalVocabularyNote}, and one grammar with
* it rather than a second one.
*
* A key that carries `.meta({ dimensionless: '<what it counts>' })` is a count,
* a multiplier or a ratio whose describe prose happens to name a time unit
* belonging to something else in the sentence: `Failures seen in the last 5
* minutes` is a number of failures, not a number of minutes. The marker rides
* `z.toJSONSchema` verbatim, the same channel `externalVocabulary` / `xRef` /
* `xExpression` / `xEnumDeprecated` use, so it arrives here as a property of
* the JSON-Schema node.
*
* Printing it is what makes THIS exemption honest, on exactly the argument its
* sibling rests on. `check:duration-unit-keys` exists because a naked number
* beside prose naming a unit leaves the reader guessing; the rename is waived
* here because the number HAS no unit, and that reason is invisible on the
* page unless the page says it. A key exempted silently publishes the same
* guess the gate was built to remove — with the marker printed, the answer is
* stated: no unit, and here is what it counts instead.
*
* Both halves read the SAME declaration the gate reads — a non-empty string
* literal — so an empty or non-string marker, which exempts no key there,
* publishes nothing here. The page must never name a count the contract did
* not.
*
* Appended to the description cell for the reason its sibling is: it qualifies
* the prose already in that cell, and a marker on a handful of keys does not
* earn a column on every table in the reference.
*/
function dimensionlessNote(prop: any): string {
const counts = prop?.dimensionless;
if (typeof counts !== 'string' || counts.trim() === '') return '';
return ` (dimensionless — counts ${counts.trim()})`;
}

/**
* Render one schema's section, heading included.
*
Expand Down Expand Up @@ -465,7 +501,9 @@ export function renderSchemaSection(schemaName: string, schema: any, ctx: Sectio
// pipe), then pipes — an unescaped `|` (even inside a code span)
// splits the cell.
const desc = escapeMdxDescription(
((prop.description || '') + externalVocabularyNote(prop)).replace(/\n/g, ' '),
((prop.description || '')
+ externalVocabularyNote(prop)
+ dimensionlessNote(prop)).replace(/\n/g, ' '),
)
.replace(/\\/g, '\\\\')
.replace(/\|/g, '\\|');
Expand Down
115 changes: 115 additions & 0 deletions packages/spec/scripts/schema-section.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,3 +657,118 @@ describe('externalVocabulary — the published half of the duration-rule exempti
}
});
});

/**
* [#18500] The PUBLISHED half of the `dimensionless` exemption — the sibling of
* the block above, ruling B on #14478: the marker is one the gate honours AND
* the docs generator publishes. `check:duration-unit-keys` shipped the reader
* first; this is the other half.
*
* The argument is its sibling's, one step further. A bare `maxAge` leaves the
* reference-page reader guessing seconds from milliseconds; a bare
* `recentFailures` described as `Failures seen in the last 5 minutes` leaves
* them guessing whether the number IS that span. The marker says it is not —
* it counts failed attempts — and that reason reaches the page only if the
* page prints it.
*
* The marker reaches this renderer as a property of the JSON-Schema node,
* riding `z.toJSONSchema` verbatim (measured on zod 4.4.3 against
* `z.number().meta({ dimensionless: 'failed attempts' })`: the key arrives on
* the emitted node unchanged) — the same channel `externalVocabulary` / `xRef`
* / `xExpression` / `xEnumDeprecated` use.
*
* MEASURED (reverse verification): deleting the `dimensionlessNote(prop)` term
* from the description cell turns the first four cases below red and leaves the
* last two green — the last two assert the note's ABSENCE, which is what keeps
* it from decorating every row in the reference.
*/
describe('dimensionless — the published half of the duration-rule exemption', () => {
const withMarker = (marker: unknown) => ({
type: 'object',
properties: {
recentFailures: {
type: 'number',
description: 'Failures seen in the last 5 minutes',
...(marker === undefined ? {} : { dimensionless: marker }),
},
},
});

it('prints what the number counts, beside the prose whose unit belongs to something else', () => {
const md = renderSchemaSection('HealthSignal', withMarker('failed attempts'));

expect(md).toContain(
'Failures seen in the last 5 minutes (dimensionless — counts failed attempts)',
);
});

it('keeps the describe prose — the note QUALIFIES the number, it does not replace it', () => {
const md = renderSchemaSection('BackoffPolicy', withMarker('retry attempts'));

expect(md).toContain('Failures seen in the last 5 minutes');
expect(md).toContain('(dimensionless — counts retry attempts)');
});

it('renders inside a nested shape table too — one grammar, not two', () => {
const md = renderSchemaSection('RetryConfig', {
type: 'object',
properties: {
backoff: {
type: 'object',
description: 'Backoff options',
properties: {
multiplier: {
type: 'number',
description: 'Growth applied to the previous 30 second delay',
dimensionless: 'the factor each delay is multiplied by',
},
},
},
},
});

expect(md).toContain(
'Growth applied to the previous 30 second delay (dimensionless — counts the factor each delay is multiplied by)',
);
});

it('composes with the sibling marker — a key declaring both publishes both reasons', () => {
// Nothing in the gate makes the two exemptions exclusive, so the cell is
// built by appending both notes rather than choosing between them. Pinned
// because a renderer that picked one would look identical on every key that
// carries only one.
const md = renderSchemaSection('CacheStats', {
type: 'object',
properties: {
maxAge: {
type: 'number',
description: 'Entries seen in the last 60 seconds',
externalVocabulary: 'HTTP Cache-Control `max-age`',
dimensionless: 'cached entries',
},
},
});

expect(md).toContain(
'Entries seen in the last 60 seconds (unit per HTTP Cache-Control `max-age`) (dimensionless — counts cached entries)',
);
});

it('prints nothing for a key that declares no marker — the note is not decoration', () => {
const md = renderSchemaSection('HealthSignal', withMarker(undefined));

expect(md).toContain('Failures seen in the last 5 minutes');
expect(md).not.toContain('dimensionless');
});

it('prints nothing for an empty or non-string marker — an unverifiable claim publishes nothing', () => {
// The gate refuses these too (they exempt no key), so the page must not
// name a count the contract never declared. Held on the SAME inputs from
// both sides so the two halves cannot drift into disagreeing about what
// counts as a declaration.
for (const marker of ['', ' ', 42, null, { counts: 'failed attempts' }]) {
const md = renderSchemaSection('HealthSignal', withMarker(marker));
expect(md, `marker ${JSON.stringify(marker)}`).not.toContain('dimensionless');
}
});
});
Loading