diff --git a/docs/README.md b/docs/README.md index 096605bd..8251569b 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 d7781544..1415c63a 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 00000000..0de7b85c --- /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 236cd94d..19ddb478 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 547b6808..145d14dc 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 [ /