From 0af5b04a558e330dbf3833f512fcca2699005ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B2=E1=84=8B=E1=85=AD=E1=86=BC=E1=84=90?= =?UTF-8?q?=E1=85=A2?= Date: Mon, 7 Sep 2026 20:41:22 +0900 Subject: [PATCH] =?UTF-8?q?fix(docs):=20Pages=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EC=9D=84=20=EC=A0=95=EB=B3=B8=20=EA=B3=B5?= =?UTF-8?q?=EA=B0=9C=20=EA=B3=84=EC=95=BD=EC=9C=BC=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=ED=95=9C=EB=8B=A4=20(#717)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 10 ++- docs/evaluate.mjs | 50 +------------- docs/public-contract-checks.mjs | 35 ++++++++++ site/scripts/evaluate-live.mjs | 32 +-------- site/scripts/evaluate.mjs | 2 + .../unit/public-document-contract.test.ts | 65 +++++++++++++++++++ 6 files changed, 115 insertions(+), 79 deletions(-) create mode 100644 docs/public-contract-checks.mjs create mode 100644 site/tests/unit/public-document-contract.test.ts diff --git a/docs/README.md b/docs/README.md index 096605bdb..8251569b0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,7 @@ docs |-- changelog.md # 사용자 영향 중심 변경 기록 |-- evaluate.mjs # 공개 문서 구조·내용 검증 +|-- public-contract-checks.mjs # 문서 원천·Pages 산출물·live 응답의 공통 공개 계약 검증 `-- public | |-- overview.md # JSON Document: Why / How / What | |-- api.md # JSON Document: 레퍼런스 @@ -133,8 +134,13 @@ reconciliation까지의 DOM 편집 정본은 `standards/dom-editing-lifecycle.md 현재 v3 portable root의 compatibility 정본은 `standards/json-document-v3/profile.md`, `standards/json-document-v3/public-surface.json`, 그리고 profile이 지정한 conformance vector와 language binding입니다. 이름 정본은 stable v3 -identifier나 동작을 바꾸지 않으며, 과거 version 문서는 v3 exact -21-symbol·six-member 계약을 확장하지 않습니다. +identifier나 동작을 바꾸지 않으며, 과거 version 문서는 정본 public surface의 +Root symbol·six-member 계약을 확장하지 않습니다. + +문서 원천, Pages 산출물, live 응답의 공개 계약 검사는 +`public-contract-checks.mjs`가 소유합니다. Root symbol 수는 Core의 +`public-contract.json`, 유효한 package 참조는 `api-reference/packages.mjs`에서 +읽습니다. 각 evaluator의 파일·HTTP 읽기와 재시도 정책은 그대로 유지합니다. ## 책임 기준 diff --git a/docs/evaluate.mjs b/docs/evaluate.mjs index d77815441..1415c63a5 100644 --- a/docs/evaluate.mjs +++ b/docs/evaluate.mjs @@ -2,6 +2,7 @@ import { readFileSync, readdirSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { spawnSync } from "node:child_process"; +import { validateLlmsContract, validatePublicPackageReferences } from "./public-contract-checks.mjs"; const root = dirname(dirname(fileURLToPath(import.meta.url))); @@ -137,34 +138,6 @@ const publicContract = readJson("packages/json-document/public-contract.json"); const rootPackage = readJson("package.json"); const implementationShape = read("standards/repository-implementation-shape.md"); const domEditingLifecycle = read("standards/dom-editing-lifecycle.md"); -const activeCompanionPackages = new Set([ - "@interactive-os/json-document-editing", - "@interactive-os/json-document-composer", - "@interactive-os/json-document-composer-react", - "@interactive-os/json-document-file-intake", - "@interactive-os/json-document-rich-text-suggestion", - "@interactive-os/json-document-rich-text-suggestion-react", - "@interactive-os/json-document-rich-text-mention", - "@interactive-os/json-document-rich-text-mention-react", - "@interactive-os/json-document-rich-text", - "@interactive-os/json-document-selection", - "@interactive-os/json-document-react", - "@interactive-os/json-document-react-hook-form", - "@interactive-os/json-document-ajv", - "@interactive-os/json-document-a2ui", - "@interactive-os/json-document-affordance", - "@interactive-os/json-document-ui-primitives-react", - "@interactive-os/json-document-animation-react", - "@interactive-os/json-document-markdown-react", - "@interactive-os/json-document-zod", - "@interactive-os/json-document-database", - "@interactive-os/json-document-calendar", - "@interactive-os/json-document-tanstack-table", - "@interactive-os/json-document-web", - "@interactive-os/json-document-contenteditable", - "@interactive-os/json-document-collaboration", - "@interactive-os/json-document-contenteditable-collaboration", -]); if (JSON.stringify(fileNames("docs/public")) !== JSON.stringify([ "adapter-clipboard.md", @@ -299,25 +272,8 @@ if (misplacedMarkdown.length > 0) { } for (const [name, source] of Object.entries(surfaces)) { - if (/@interactive-os\/json-document\/(?:session|react)\b/.test(source)) { - fail(`${name}: removed package subpath is still documented.`); - } - for ( - const match of source.matchAll( - /@interactive-os\/json-document-[a-z0-9-]+\b/g, - ) - ) { - if ( - !activeCompanionPackages.has(match[0]) - ) { - fail( - `${name}: removed json-document extension is still documented as current: ${match[0]}.`, - ); - } - } - if (/\blabs\/extensions\b/.test(source)) { - fail(`${name}: removed lab path is still documented as current.`); - } + const validate = name === "llms" ? validateLlmsContract : validatePublicPackageReferences; + validate(source, (message) => fail(`${name}: ${message}`)); } for (const [name, source] of Object.entries(surfaces)) { diff --git a/docs/public-contract-checks.mjs b/docs/public-contract-checks.mjs new file mode 100644 index 000000000..0de7b85c9 --- /dev/null +++ b/docs/public-contract-checks.mjs @@ -0,0 +1,35 @@ +import { readFileSync } from "node:fs"; +import { apiReferencePackages } from "./api-reference/packages.mjs"; + +const publicContract = JSON.parse(readFileSync(new URL("../packages/json-document/public-contract.json", import.meta.url), "utf8")); +const rootSymbolCount = publicContract.root.values.length + publicContract.root.types.length; +const publicPackages = new Set(apiReferencePackages.map(({ packageName }) => packageName)); + +/** Shared by source, artifact, and live documentation verification; no transport. */ +export function validatePublicPackageReferences(source, fail) { + if (/@interactive-os\/json-document\/(?:session|react)\b/.test(source)) { + fail("removed package subpath is still documented."); + } + for (const match of source.matchAll(/@interactive-os\/json-document-[a-z0-9-]+\b/g)) { + if (!publicPackages.has(match[0])) { + fail(`removed json-document extension is still documented as current: ${match[0]}.`); + } + } + if (/\blabs\/extensions\b/.test(source)) { + fail("removed lab path is still documented as current."); + } +} + +/** Validate llms.txt against the current Core contract and documented ecosystem. */ +export function validateLlmsContract(source, fail) { + for (const [pattern, requirement] of [ + [/^# json-document v3$/m, "v3 title"], + [new RegExp(`공개 Root는 정확히 다음 ${rootSymbolCount}개 symbol`), `${rootSymbolCount} Root symbols`], + [/`JSONDocument`의 필수 member는 정확히 여섯 개다/, "six-member JSONDocument contract"], + [/## Adapter, Connector와 companion/, "Adapter, Connector and companion boundary"], + [/@interactive-os\/editable/, "migration boundary for @interactive-os/editable"], + ]) { + if (!pattern.test(source)) fail(`llms.txt is missing ${requirement}.`); + } + validatePublicPackageReferences(source, fail); +} diff --git a/site/scripts/evaluate-live.mjs b/site/scripts/evaluate-live.mjs index 236cd94da..19ddb4788 100644 --- a/site/scripts/evaluate-live.mjs +++ b/site/scripts/evaluate-live.mjs @@ -1,23 +1,11 @@ import { readFileSync } from "node:fs"; import { validateSiteRoutes } from "./route-checks.mjs"; +import { validateLlmsContract } from "../../docs/public-contract-checks.mjs"; const siteUrl = (process.env.SITE_URL ?? "https://developer-1px.github.io/json-document").replace(/\/$/, ""); const attempts = Number(process.env.SITE_LIVE_ATTEMPTS ?? "18"); const delayMs = Number(process.env.SITE_LIVE_DELAY_MS ?? "10000"); const routes = JSON.parse(readFileSync(new URL("../site-routes.json", import.meta.url), "utf8")); -const activeCompanionPackages = new Set([ - "@interactive-os/json-document-selection", - "@interactive-os/json-document-editing", - "@interactive-os/json-document-react", - "@interactive-os/json-document-react-hook-form", - "@interactive-os/json-document-ajv", - "@interactive-os/json-document-zod", - "@interactive-os/json-document-tanstack-table", - "@interactive-os/json-document-web", - "@interactive-os/json-document-contenteditable", - "@interactive-os/json-document-collaboration", - "@interactive-os/json-document-contenteditable-collaboration", -]); validateSiteRoutes(routes, fail); const rootRoute = routes.find((route) => route.path === "/"); if (rootRoute === undefined) fail("live site routes are missing the root route."); @@ -82,23 +70,7 @@ async function checkOnce() { } const llms = await fetchText("/llms.txt"); - if ( - !/^# json-document v3$/m.test(llms) - || !/공개 Root는 정확히 다음 21개 symbol/.test(llms) - || !/`JSONDocument`의 필수 member는 정확히 여섯 개다/.test(llms) - || !/## Adapter, Connector와 companion/.test(llms) - || !/@interactive-os\/editable/.test(llms) - ) { - fail("live llms.txt is missing the v3 Core contract."); - } - const packageReferences = llms.match(/@interactive-os\/json-document-[a-z0-9-]+/g) ?? []; - if ( - packageReferences.some((packageName) => !activeCompanionPackages.has(packageName)) - || /@interactive-os\/json-document\/(?:session|react)\b/.test(llms) - || /\blabs\/extensions\b/.test(llms) - ) { - fail("live llms.txt still exposes a removed legacy surface."); - } + validateLlmsContract(llms, (message) => fail(`live ${message}`)); const manifest = JSON.parse(await fetchText("/site.webmanifest")); if ( diff --git a/site/scripts/evaluate.mjs b/site/scripts/evaluate.mjs index 547b6808d..145d14dc1 100644 --- a/site/scripts/evaluate.mjs +++ b/site/scripts/evaluate.mjs @@ -1,6 +1,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs"; import { dirname, join, relative, resolve } from "node:path"; import { routeFile, validateSiteRoutes } from "./route-checks.mjs"; +import { validateLlmsContract } from "../../docs/public-contract-checks.mjs"; const siteRoot = new URL("..", import.meta.url).pathname; const dist = join(siteRoot, "dist"); @@ -110,6 +111,7 @@ const fallback = read("404.html"); const robots = read("robots.txt"); const sitemap = read("sitemap.xml"); const manifest = JSON.parse(read("site.webmanifest")); +validateLlmsContract(read("llms.txt"), (message) => fail(`site dist ${message}`)); for (const pattern of [ /json-document - Agent artifact editing<\/title>/, diff --git a/site/tests/unit/public-document-contract.test.ts b/site/tests/unit/public-document-contract.test.ts new file mode 100644 index 000000000..7f00e5824 --- /dev/null +++ b/site/tests/unit/public-document-contract.test.ts @@ -0,0 +1,65 @@ +// @vitest-environment node +import { readFileSync } from "node:fs"; +import { describe, expect, test } from "vitest"; +import { apiReferencePackages } from "../../../docs/api-reference/packages.mjs"; +import { validateLlmsContract, validatePublicPackageReferences } from "../../../docs/public-contract-checks.mjs"; + +const llms = readFileSync(new URL("../../../docs/public/llms.txt", import.meta.url), "utf8"); +const publicContract = JSON.parse(readFileSync(new URL("../../../packages/json-document/public-contract.json", import.meta.url), "utf8")); +const symbolCount = publicContract.root.values.length + publicContract.root.types.length; + +function findings(source: string) { + const errors: string[] = []; + validateLlmsContract(source, (message: string) => errors.push(message)); + return errors; +} + +describe("public documentation contract", () => { + test("accepts the exact published llms source against the canonical Core contract", () => { + expect(symbolCount).toBe(23); + expect(findings(llms)).toEqual([]); + }); + + test.each([ + ["old Root symbol count", llms.replace(`${symbolCount}개 symbol`, "21개 symbol")], + ["missing v3 title", llms.replace("# json-document v3", "# json-document")], + ["missing six-member contract", llms.replace("`JSONDocument`의 필수 member는 정확히 여섯 개다", "JSON Document")], + ["missing companion boundary", llms.replace("## Adapter, Connector와 companion", "## Integrations")], + ["missing migration boundary", llms.replaceAll("@interactive-os/editable", "retired editor")], + ])("rejects %s", (_name, source) => { + expect(source).not.toBe(llms); + expect(findings(source)).not.toEqual([]); + }); + + test("accepts every package registered at the canonical documentation owner", () => { + const references = apiReferencePackages.map(({ packageName }) => packageName).join("\n"); + const errors: string[] = []; + validatePublicPackageReferences(references, (message: string) => errors.push(message)); + expect(errors).toEqual([]); + expect(findings(`${llms}\n${references}`)).toEqual([]); + }); + + test.each([ + "@interactive-os/json-document-removed-extension", + "@interactive-os/json-document/session", + "@interactive-os/json-document/react", + "labs/extensions", + ])("rejects retired reference %s in docs and llms", (reference) => { + const errors: string[] = []; + validatePublicPackageReferences(reference, (message: string) => errors.push(message)); + expect(errors).not.toEqual([]); + expect(findings(`${llms}\n${reference}`)).not.toEqual([]); + }); + + test.each([ + "../../../docs/evaluate.mjs", + "../../scripts/evaluate.mjs", + "../../scripts/evaluate-live.mjs", + ])("keeps %s wired to the canonical llms validator", (path) => { + const source = readFileSync(new URL(path, import.meta.url), "utf8"); + expect(/import\s*\{[^}]*validateLlmsContract[^}]*\}\s*from\s*["'][^"']*public-contract-checks\.mjs["']/.test(source)).toBe(true); + expect(/validateLlmsContract(?:\(|\s*:)/.test(source)).toBe(true); + expect(source).not.toContain("21개 symbol"); + expect(source).not.toContain("activeCompanionPackages"); + }); +});