diff --git a/packages/spec/docs-import-surface.baseline.json b/packages/spec/docs-import-surface.baseline.json index 64a2e49bd4..d03aeea4b4 100644 --- a/packages/spec/docs-import-surface.baseline.json +++ b/packages/spec/docs-import-surface.baseline.json @@ -1,141 +1,141 @@ { - "_comment": "Accepted gaps between the reference docs' import examples and the real export surface of @objectstack/spec (#4570): a documented JSON Schema whose entry point exports no matching type alias (\"no type export\") or no matching schema const (\"no schema const export\"). The name is omitted from the generated import line \u2014 the docs never advertise an import that cannot compile \u2014 and listed here so the omission is countable instead of silent. Shrink-only ratchet: a NEW gap fails check:docs (fix it by adding `export type X = z.infer;` next to the schema and regenerating api-surface.json, or by retiring the schema together with its alias), and a stale entry fails until its line is deleted. Growing this list is a maintainer decision that shows up as this file in the diff. Regenerate with: tsx scripts/build-docs.ts --update-import-baseline (after gen:schema).", + "_comment": "Accepted gaps between the reference docs' import examples and the real export surface of @objectstack/spec (#4570): a documented JSON Schema whose entry point exports no matching type alias (\"no type export\") or no matching schema const (\"no schema const export\"). The name is omitted from the generated import line — the docs never advertise an import that cannot compile — and listed here so the omission is countable instead of silent. Shrink-only ratchet: a NEW gap fails check:docs (fix it by adding `export type X = z.infer;` next to the schema and regenerating api-surface/, or by retiring the schema together with its alias), and a stale entry fails until its line is deleted. Growing this list is a maintainer decision that shows up as this file in the diff. Regenerate with: tsx scripts/build-docs.ts --update-import-baseline (after gen:schema).", "entries": [ - "ai/AIModelConfig \u2014 no type export", - "ai/CodeContent \u2014 no type export", - "ai/FileContent \u2014 no type export", - "ai/ImageContent \u2014 no type export", - "ai/TextContent \u2014 no type export", - "api/AnalyticsResultResponse \u2014 no type export", - "api/ApiMapping \u2014 no type export", - "api/Discovery \u2014 no type export", - "api/GetAnalyticsMetaRequest \u2014 no type export", - "api/HttpFindQueryParams \u2014 no type export", - "api/MetadataBulkUnregisterRequest \u2014 no type export", - "api/MetadataExportRequest \u2014 no type export", - "api/MetadataImportRequest \u2014 no type export", - "api/MetadataOverlaySaveRequest \u2014 no type export", - "api/MetadataQueryRequest \u2014 no type export", - "api/MetadataValidateRequest \u2014 no type export", - "api/SubscriptionEvent \u2014 no type export", - "automation/ApproverType \u2014 no type export", - "automation/FlowNodeAction \u2014 no type export", - "automation/FlowVariable \u2014 no type export", - "automation/GuardRef \u2014 no type export", - "automation/StateMachine \u2014 no type export", - "automation/StateNode \u2014 no type export", - "data/AddressValue \u2014 no type export", - "data/AggregationFunction \u2014 no type export", - "data/AggregationMetricType \u2014 no type export", - "data/CalendarDateValue \u2014 no type export", - "data/ClockTimeValue \u2014 no type export", - "data/ContextTokenPlaceholder \u2014 no type export", - "data/DataEngineAggregateRequest \u2014 no type export", - "data/DataEngineCountRequest \u2014 no type export", - "data/DataEngineDeleteRequest \u2014 no type export", - "data/DataEngineExecuteRequest \u2014 no type export", - "data/DataEngineFindOneRequest \u2014 no type export", - "data/DataEngineFindRequest \u2014 no type export", - "data/DataEngineInsertRequest \u2014 no type export", - "data/DataEngineUpdateRequest \u2014 no type export", - "data/DataEngineVectorFindRequest \u2014 no type export", - "data/DateGranularity \u2014 no type export", - "data/DateMacroPlaceholder \u2014 no type export", - "data/DimensionType \u2014 no type export", - "data/DriverSslToggle \u2014 no type export", - "data/DriverType \u2014 no type export", - "data/EqualityOperator \u2014 no type export", - "data/FileLikeValue \u2014 no type export", - "data/FileReferenceIdValue \u2014 no type export", - "data/FileValue \u2014 no type export", - "data/HookEvent \u2014 no type export", - "data/Index \u2014 no type export", - "data/InstantValue \u2014 no type export", - "data/LocationValue \u2014 no type export", - "data/Object \u2014 no type export", - "data/ObjectOwnershipEnum \u2014 no type export", - "data/Query \u2014 no type export", - "data/ReferenceIdValue \u2014 no type export", - "data/SeedMode \u2014 no type export", - "data/SetOperator \u2014 no type export", - "data/SpecialOperator \u2014 no type export", - "data/StringOperator \u2014 no type export", - "data/TimeUpdateInterval \u2014 no type export", - "data/TransformType \u2014 no type export", - "integration/ConnectorAction \u2014 no type export", - "integration/ConnectorInstanceAPIKeyAuth \u2014 no type export", - "integration/ConnectorInstanceBasicAuth \u2014 no type export", - "integration/ConnectorInstanceBearerAuth \u2014 no type export", - "integration/ConnectorInstanceNoAuth \u2014 no type export", - "integration/ConnectorTrigger \u2014 no type export", - "kernel/DependencyStatusEnum \u2014 no type export", - "kernel/EventMetadata \u2014 no type export", - "kernel/Manifest \u2014 no type export", - "kernel/MetadataCategoryEnum \u2014 no type export", - "kernel/PackageStatusEnum \u2014 no type export", - "kernel/Plugin \u2014 no type export", - "qa/TestActionType \u2014 no type export", - "qa/TestAssertionType \u2014 no type export", - "qa/TestContext \u2014 no type export", - "security/OWDModel \u2014 no type export", - "security/ShareRecipientType \u2014 no type export", - "security/SharingLevel \u2014 no type export", - "security/SharingRuleType \u2014 no type export", - "shared/IsolationLevelEnum \u2014 no type export", - "shared/MutationEventEnum \u2014 no type export", - "shared/SortDirectionEnum \u2014 no type export", - "studio/PanelLocation \u2014 no type export", - "system/AddFieldOperation \u2014 no type export", - "system/CreateObjectOperation \u2014 no type export", - "system/DeleteObjectOperation \u2014 no type export", - "system/DeployStatusEnum \u2014 no type export", - "system/ExecuteSqlOperation \u2014 no type export", - "system/KernelServiceMap \u2014 no type export", - "system/Locale \u2014 no type export", - "system/MetadataState \u2014 no type export", - "system/MigrationDependency \u2014 no type export", - "system/ModifyFieldOperation \u2014 no type export", - "system/RemoveFieldOperation \u2014 no type export", - "system/RenameObjectOperation \u2014 no type export", - "system/ServiceConfig \u2014 no type export", - "system/ServiceCriticality \u2014 no type export", - "system/ServiceStatus \u2014 no type export", - "ui/AIChatWindowProps \u2014 no type export", - "ui/ActionType \u2014 no type export", - "ui/BreakpointColumnMap \u2014 no type export", - "ui/BreakpointOrderMap \u2014 no type export", - "ui/CalendarConfig \u2014 no type export", - "ui/DerivedMeasureOp \u2014 no type export", - "ui/ElementButtonProps \u2014 no type export", - "ui/ElementFilterProps \u2014 no type export", - "ui/ElementFormProps \u2014 no type export", - "ui/ElementImageProps \u2014 no type export", - "ui/ElementMetadataViewerProps \u2014 no type export", - "ui/ElementNumberProps \u2014 no type export", - "ui/ElementRecordPickerProps \u2014 no type export", - "ui/ElementTextInputProps \u2014 no type export", - "ui/ElementTextProps \u2014 no type export", - "ui/GanttConfig \u2014 no type export", - "ui/GanttQuickFilter \u2014 no type export", - "ui/GroupingField \u2014 no type export", - "ui/KanbanConfig \u2014 no type export", - "ui/NavigationMode \u2014 no type export", - "ui/ObjectListView \u2014 no type export", - "ui/ObjectUserFilters \u2014 no type export", - "ui/PageAccordionProps \u2014 no type export", - "ui/PageCardProps \u2014 no type export", - "ui/PageComponentType \u2014 no type export", - "ui/PageHeaderProps \u2014 no type export", - "ui/PageTabsProps \u2014 no type export", - "ui/RecordActivityProps \u2014 no type export", - "ui/RecordChatterProps \u2014 no type export", - "ui/RecordDetailsProps \u2014 no type export", - "ui/RecordHighlightsField \u2014 no type export", - "ui/RecordHighlightsProps \u2014 no type export", - "ui/RecordPathProps \u2014 no type export", - "ui/RecordRelatedListProps \u2014 no type export", - "ui/ReportType \u2014 no type export", - "ui/TreeConfig \u2014 no type export", - "ui/ViewItemName \u2014 no type export" + "ai/AIModelConfig — no type export", + "ai/CodeContent — no type export", + "ai/FileContent — no type export", + "ai/ImageContent — no type export", + "ai/TextContent — no type export", + "api/AnalyticsResultResponse — no type export", + "api/ApiMapping — no type export", + "api/Discovery — no type export", + "api/GetAnalyticsMetaRequest — no type export", + "api/HttpFindQueryParams — no type export", + "api/MetadataBulkUnregisterRequest — no type export", + "api/MetadataExportRequest — no type export", + "api/MetadataImportRequest — no type export", + "api/MetadataOverlaySaveRequest — no type export", + "api/MetadataQueryRequest — no type export", + "api/MetadataValidateRequest — no type export", + "api/SubscriptionEvent — no type export", + "automation/ApproverType — no type export", + "automation/FlowNodeAction — no type export", + "automation/FlowVariable — no type export", + "automation/GuardRef — no type export", + "automation/StateMachine — no type export", + "automation/StateNode — no type export", + "data/AddressValue — no type export", + "data/AggregationFunction — no type export", + "data/AggregationMetricType — no type export", + "data/CalendarDateValue — no type export", + "data/ClockTimeValue — no type export", + "data/ContextTokenPlaceholder — no type export", + "data/DataEngineAggregateRequest — no type export", + "data/DataEngineCountRequest — no type export", + "data/DataEngineDeleteRequest — no type export", + "data/DataEngineExecuteRequest — no type export", + "data/DataEngineFindOneRequest — no type export", + "data/DataEngineFindRequest — no type export", + "data/DataEngineInsertRequest — no type export", + "data/DataEngineUpdateRequest — no type export", + "data/DataEngineVectorFindRequest — no type export", + "data/DateGranularity — no type export", + "data/DateMacroPlaceholder — no type export", + "data/DimensionType — no type export", + "data/DriverSslToggle — no type export", + "data/DriverType — no type export", + "data/EqualityOperator — no type export", + "data/FileLikeValue — no type export", + "data/FileReferenceIdValue — no type export", + "data/FileValue — no type export", + "data/HookEvent — no type export", + "data/Index — no type export", + "data/InstantValue — no type export", + "data/LocationValue — no type export", + "data/Object — no type export", + "data/ObjectOwnershipEnum — no type export", + "data/Query — no type export", + "data/ReferenceIdValue — no type export", + "data/SeedMode — no type export", + "data/SetOperator — no type export", + "data/SpecialOperator — no type export", + "data/StringOperator — no type export", + "data/TimeUpdateInterval — no type export", + "data/TransformType — no type export", + "integration/ConnectorAction — no type export", + "integration/ConnectorInstanceAPIKeyAuth — no type export", + "integration/ConnectorInstanceBasicAuth — no type export", + "integration/ConnectorInstanceBearerAuth — no type export", + "integration/ConnectorInstanceNoAuth — no type export", + "integration/ConnectorTrigger — no type export", + "kernel/DependencyStatusEnum — no type export", + "kernel/EventMetadata — no type export", + "kernel/Manifest — no type export", + "kernel/MetadataCategoryEnum — no type export", + "kernel/PackageStatusEnum — no type export", + "kernel/Plugin — no type export", + "qa/TestActionType — no type export", + "qa/TestAssertionType — no type export", + "qa/TestContext — no type export", + "security/OWDModel — no type export", + "security/ShareRecipientType — no type export", + "security/SharingLevel — no type export", + "security/SharingRuleType — no type export", + "shared/IsolationLevelEnum — no type export", + "shared/MutationEventEnum — no type export", + "shared/SortDirectionEnum — no type export", + "studio/PanelLocation — no type export", + "system/AddFieldOperation — no type export", + "system/CreateObjectOperation — no type export", + "system/DeleteObjectOperation — no type export", + "system/DeployStatusEnum — no type export", + "system/ExecuteSqlOperation — no type export", + "system/KernelServiceMap — no type export", + "system/Locale — no type export", + "system/MetadataState — no type export", + "system/MigrationDependency — no type export", + "system/ModifyFieldOperation — no type export", + "system/RemoveFieldOperation — no type export", + "system/RenameObjectOperation — no type export", + "system/ServiceConfig — no type export", + "system/ServiceCriticality — no type export", + "system/ServiceStatus — no type export", + "ui/AIChatWindowProps — no type export", + "ui/ActionType — no type export", + "ui/BreakpointColumnMap — no type export", + "ui/BreakpointOrderMap — no type export", + "ui/CalendarConfig — no type export", + "ui/DerivedMeasureOp — no type export", + "ui/ElementButtonProps — no type export", + "ui/ElementFilterProps — no type export", + "ui/ElementFormProps — no type export", + "ui/ElementImageProps — no type export", + "ui/ElementMetadataViewerProps — no type export", + "ui/ElementNumberProps — no type export", + "ui/ElementRecordPickerProps — no type export", + "ui/ElementTextInputProps — no type export", + "ui/ElementTextProps — no type export", + "ui/GanttConfig — no type export", + "ui/GanttQuickFilter — no type export", + "ui/GroupingField — no type export", + "ui/KanbanConfig — no type export", + "ui/NavigationMode — no type export", + "ui/ObjectListView — no type export", + "ui/ObjectUserFilters — no type export", + "ui/PageAccordionProps — no type export", + "ui/PageCardProps — no type export", + "ui/PageComponentType — no type export", + "ui/PageHeaderProps — no type export", + "ui/PageTabsProps — no type export", + "ui/RecordActivityProps — no type export", + "ui/RecordChatterProps — no type export", + "ui/RecordDetailsProps — no type export", + "ui/RecordHighlightsField — no type export", + "ui/RecordHighlightsProps — no type export", + "ui/RecordPathProps — no type export", + "ui/RecordRelatedListProps — no type export", + "ui/ReportType — no type export", + "ui/TreeConfig — no type export", + "ui/ViewItemName — no type export" ] } diff --git a/packages/spec/scripts/build-docs.ts b/packages/spec/scripts/build-docs.ts index ccae65f02a..9fa6dd8770 100644 --- a/packages/spec/scripts/build-docs.ts +++ b/packages/spec/scripts/build-docs.ts @@ -30,6 +30,7 @@ import { evaluateBaseline, loadEntrySurfaces, resolveImports, + serializeImportBaseline, type CategorySurface, } from './lib/docs-import-surface'; import { escapeMdxDescription } from './lib/escape-mdx'; @@ -249,18 +250,6 @@ const ENTRY_SURFACES: ReadonlyMap = loadEntrySurfaces( /** Every unresolvable import name this run met, one stable line each. */ const importGaps = new Set(); -const IMPORT_BASELINE_COMMENT = - 'Accepted gaps between the reference docs\' import examples and the real export surface of ' + - '@objectstack/spec (#4570): a documented JSON Schema whose entry point exports no matching type ' + - 'alias ("no type export") or no matching schema const ("no schema const export"). The name is ' + - 'omitted from the generated import line — the docs never advertise an import that cannot compile ' + - '— and listed here so the omission is countable instead of silent. Shrink-only ratchet: a NEW gap ' + - 'fails check:docs (fix it by adding `export type X = z.infer;` next to the schema ' + - 'and regenerating api-surface/, or by retiring the schema together with its alias), and a ' + - 'stale entry fails until its line is deleted. Growing this list is a maintainer decision that ' + - 'shows up as this file in the diff. Regenerate with: ' + - 'tsx scripts/build-docs.ts --update-import-baseline (after gen:schema).'; - /** * Resolve a schema name to its page, AS SEEN FROM the category being rendered. * Returns null when the schema isn't one we generate a page for, or when the @@ -943,10 +932,11 @@ if (managedCount > 0) { const gaps = [...importGaps].sort(); if (UPDATE_IMPORT_BASELINE) { - fs.writeFileSync( - IMPORT_BASELINE_PATH, - JSON.stringify({ _comment: IMPORT_BASELINE_COMMENT, entries: gaps }, null, 2) + '\n', - ); + // Serialized through the lib, never inline: the same function is what the + // round-trip pin in `docs-import-surface.test.ts` re-serializes the + // committed file with, so this writer and that file cannot drift into two + // encodings again (#5990). + fs.writeFileSync(IMPORT_BASELINE_PATH, serializeImportBaseline(gaps)); console.log(`Wrote ${gaps.length} import-surface gap(s) to ${path.relative(REPO_ROOT, IMPORT_BASELINE_PATH)} — review the diff before committing.`); } else { const baseline: { entries?: string[] } = fs.existsSync(IMPORT_BASELINE_PATH) diff --git a/packages/spec/scripts/docs-import-surface.test.ts b/packages/spec/scripts/docs-import-surface.test.ts index 0dd7adc23f..c02c443c65 100644 --- a/packages/spec/scripts/docs-import-surface.test.ts +++ b/packages/spec/scripts/docs-import-surface.test.ts @@ -10,15 +10,25 @@ * is not already in the baseline. A rule that only did the first would fix the * symptom and hide the cause — which is the state #4570 describes, one layer up. */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + import { describe, expect, it } from 'vitest'; import { evaluateBaseline, + IMPORT_BASELINE_COMMENT, loadEntrySurfaces, resolveImports, resolveTypeName, resolveValueName, + serializeImportBaseline, } from './lib/docs-import-surface'; +import { API_SURFACE_DIR_NAME, LEGACY_MONOLITH_NAMES } from './lib/sharded-artifacts'; + +const SPEC_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const BASELINE_PATH = path.join(SPEC_DIR, 'docs-import-surface.baseline.json'); /** * A miniature `api-surface.json`. Each name is one real shape the spec carries: @@ -145,3 +155,64 @@ describe('evaluateBaseline', () => { }); }); }); + +/** + * The committed file's BYTES, pinned against the one function that writes them + * (#5990). + * + * Everything above judges the baseline's *content* after `JSON.parse`, which is + * also all the `check:docs` ratchet judges — and that is why nothing noticed + * that `--update-import-baseline` emitted raw characters while the committed + * file carried `\uXXXX` escapes. Both encodings parse to identical entries, so + * the gate stayed green while every run of the command rewrote all 136 entry + * lines. The cost landed on review, not on CI: a shrink-only ratchet is + * designed so that a change to it is CONSPICUOUS, and 136 lines of encoding + * flip is precisely how a 1-line decision stops being conspicuous. + * + * These cases close it from the side the content assertions cannot see. They + * need no build and no `json-schema/` tree: the file is re-serialized from its + * OWN parsed entries, so what is being compared is encoding, not gap analysis. + */ +describe('docs-import-surface.baseline.json bytes', () => { + const bytes = fs.readFileSync(BASELINE_PATH, 'utf-8'); + const parsed = JSON.parse(bytes) as { _comment?: string; entries?: string[] }; + + it('carries the current comment — a stale one cannot sit there silently', () => { + // Asserted BEFORE the whole-file comparison so the failure names the cause. + // No gate compares `_comment` (`entries` is the criterion), which is how + // #6069 could rename `api-surface.json` to `api-surface/` in the source + // constant and leave the committed file pointing at a path that no longer + // exists. It was a deliberate handoff, not an oversight — re-running the + // writer to fix one prose line cost 137 lines of encoding churn. With the + // encodings unified that re-run is a 1-line diff, so the drift is now worth + // failing on. + expect(parsed._comment).toBe(IMPORT_BASELINE_COMMENT); + }); + + it('is byte-identical to what --update-import-baseline would write', () => { + // The round trip is the whole pin: writer and file share one serializer, so + // they can no longer hold two encodings. Indentation, key order and the + // trailing newline ride along for free. + expect(bytes).toBe(serializeImportBaseline(parsed.entries ?? [])); + }); + + it('spells non-ASCII as raw characters, like every other JSON in the repo', () => { + // The negative control, and the direction that actually regressed. Measured + // on the fix's base commit: of 305 tracked `.json` files, 151 carry + // non-ASCII and this was the only one that escaped it. Re-introducing an + // ASCII-escaping wrapper on either side turns this red instead of turning + // the next reviewer's diff into 136 lines of noise. + expect(bytes).not.toMatch(/\\u[0-9a-fA-F]{4}/); + expect(bytes).toContain('—'); + expect(serializeImportBaseline(['demo/Gadget — no type export'])).toContain('—'); + expect(serializeImportBaseline(['demo/Gadget — no type export'])).not.toMatch(/\\u[0-9a-fA-F]{4}/); + }); + + it('points regenerators at the sharded api-surface, not the retired monolith', () => { + // Names the drift #6069 left behind, from the module that owns both + // spellings — so a future re-shard of `api-surface/` cannot quietly leave + // this prose describing a layout the repo no longer has. + expect(IMPORT_BASELINE_COMMENT).toContain(`${API_SURFACE_DIR_NAME}/`); + expect(IMPORT_BASELINE_COMMENT).not.toContain(LEGACY_MONOLITH_NAMES[API_SURFACE_DIR_NAME]); + }); +}); diff --git a/packages/spec/scripts/lib/docs-import-surface.ts b/packages/spec/scripts/lib/docs-import-surface.ts index 6a5e3241f1..d4bdfad8f4 100644 --- a/packages/spec/scripts/lib/docs-import-surface.ts +++ b/packages/spec/scripts/lib/docs-import-surface.ts @@ -143,6 +143,72 @@ export function resolveImports( return { valueNames, typeNames, exampleValue: valueNames[0] ?? null, gaps }; } +// ── The committed baseline's BYTES ─────────────────────────────────────────── +// +// The comment and the serializer live here, next to the rules they describe, +// for one reason: the writer in `build-docs.ts` and the file on disk must be +// able to *prove* they agree, and a constant private to a script that executes +// on import cannot be read by a test. +// +// They disagreed until #5990. `--update-import-baseline` wrote raw characters +// (plain `JSON.stringify`) while the committed file carried `\uXXXX` escapes, +// so every run of the command rewrote all 136 entry lines to say exactly what +// they already said. Measured on `origin/main` at the time: 305 tracked `.json` +// files, 151 of them carrying non-ASCII, and this was the ONLY one that escaped +// it — the sibling ratchets (`dual-source-exports`, `react-declaration-parity`, +// `variant-docs`, `test-typecheck-debt`) and every generated artifact +// (`api-surface/`, `authorable-surface/`, `json-schema.manifest/`, +// `spec-changes.json`) all land raw. So the file was the outlier, not the +// writer, and #5990 converted the file once rather than teaching the writer to +// produce an escape nothing else in the repo produces. +// +// Why the churn was worth a fix rather than a habit: this is a SHRINK-ONLY +// ratchet whose whole design intent is that a change to it is CONSPICUOUS — +// growing it is a maintainer decision that is supposed to show up in the diff +// (`NOT_DRIVER_MANAGED` in `scripts/regen-artifacts.mjs` keeps it out of the +// merge driver for the same reason). 136 lines of encoding flip buries the one +// line that carries the decision. Twice measured, not theorised: PR #5976 had +// to re-serialize the generator's output by hand to get a 1-line diff, and +// PR #6069 — changing nothing but a path inside the prose below — still +// produced 137 insertions and 137 deletions. + +/** + * The `_comment` the baseline file carries, and the one place it is written. + * + * `serializeImportBaseline` is the ONLY writer, so editing this string makes + * the committed file stale — deliberately, and loudly: the round-trip pin in + * `docs-import-surface.test.ts` goes red until `--update-import-baseline` is + * re-run. That trade only became affordable once the encodings matched; while + * they did not, re-running the command cost 137 lines of noise, which is + * exactly why #6069 chose to leave the stale path (`api-surface.json`, a file + * that no longer exists) for #5990 to fix instead. + */ +export const IMPORT_BASELINE_COMMENT = + 'Accepted gaps between the reference docs\' import examples and the real export surface of ' + + '@objectstack/spec (#4570): a documented JSON Schema whose entry point exports no matching type ' + + 'alias ("no type export") or no matching schema const ("no schema const export"). The name is ' + + 'omitted from the generated import line — the docs never advertise an import that cannot compile ' + + '— and listed here so the omission is countable instead of silent. Shrink-only ratchet: a NEW gap ' + + 'fails check:docs (fix it by adding `export type X = z.infer;` next to the schema ' + + 'and regenerating api-surface/, or by retiring the schema together with its alias), and a ' + + 'stale entry fails until its line is deleted. Growing this list is a maintainer decision that ' + + 'shows up as this file in the diff. Regenerate with: ' + + 'tsx scripts/build-docs.ts --update-import-baseline (after gen:schema).'; + +/** + * The exact bytes `docs-import-surface.baseline.json` must carry for `entries`. + * + * Both sides of #5990 route through here — the `--update-import-baseline` + * writer and the test that re-serializes the committed file and demands the + * bytes back — so "the writer's encoding" and "the file's encoding" are no + * longer two facts that can drift apart. Plain `JSON.stringify` is the point, + * not an implementation detail: it emits raw characters, which is what the rest + * of the repo's JSON carries. + */ +export function serializeImportBaseline(entries: readonly string[]): string { + return JSON.stringify({ _comment: IMPORT_BASELINE_COMMENT, entries }, null, 2) + '\n'; +} + export interface BaselineVerdict { /** Gaps not covered by the baseline — a NEW dead import example. */ fresh: string[];