From c448fefe74ef944fdfce25dc3acd0fddb8ca8bb6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 01:08:40 +0000 Subject: [PATCH] =?UTF-8?q?fix(spec):=20=E5=9F=BA=E7=BA=BF=E7=BC=96?= =?UTF-8?q?=E7=A0=81=E7=BB=9F=E4=B8=80=E4=B8=BA=E8=A3=B8=E5=AD=97=E7=AC=A6?= =?UTF-8?q?,=E5=86=99=E7=9B=98=E7=AB=AF=E4=B8=8E=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=85=B1=E7=94=A8=E4=B8=80=E4=B8=AA=E5=BA=8F=E5=88=97=E5=8C=96?= =?UTF-8?q?=E5=99=A8=20(#5990)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--update-import-baseline` 用 JSON.stringify 写裸字符,而仓内 `docs-import-surface.baseline.json` 落盘是 \uXXXX 转义。两者 JSON.parse 后逐条等价,门禁只读 parse 后的 entries,所以一直是绿的 —— 代价全落在复核: 每跑一次该命令,136 条 entry 行被原样重写一遍,真实的 1 行决定被埋掉。 选路 2(基线统一为裸字符 + 一次性转换提交),判据是该 ratchet 的设计意图: 只减不增、变化应当显眼。实测支撑 —— 本仓 305 个 tracked .json 中 151 个带 非 ASCII,只有这一个用转义;同类手工棘轮与全部生成物都落裸字符,且 lib/sharded-artifacts.ts 的 serializeShard 已把「2 空格 + 结尾换行」的裸字符 写法确立为分片规范字节。选路 1 会让这个文件成为全仓唯一的转义孤例。 两侧一起改,不只改一侧: - IMPORT_BASELINE_COMMENT 与新的 serializeImportBaseline() 移入 lib/docs-import-surface.ts,写盘端改为调用它; - 基线文件由生成器重写(内容 100% 来自生成器),entries 在 parse 层与 origin/main 逐条相同(136 条不变),唯一语义变化是 #6069 有意留下的 过期路径 api-surface.json -> api-surface/; - 新增字节级往返 pin:读committed 文件 -> JSON.parse -> 用同一个序列化器 重新序列化 -> 要求字节相同。它同时钉住编码、缩进、键序、结尾换行,以及 _comment 的新鲜度,任一侧再分叉即红。 复现修复:连续两次跑 --update-import-baseline,文件 sha256 不变(修复前每次 都产生 137 增 137 删)。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5 --- .../spec/docs-import-surface.baseline.json | 274 +++++++++--------- packages/spec/scripts/build-docs.ts | 22 +- .../spec/scripts/docs-import-surface.test.ts | 71 +++++ .../spec/scripts/lib/docs-import-surface.ts | 66 +++++ 4 files changed, 280 insertions(+), 153 deletions(-) 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[];