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
10 changes: 8 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: 레퍼런스
Expand Down Expand Up @@ -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 읽기와 재시도 정책은 그대로 유지합니다.

## 책임 기준

Expand Down
50 changes: 3 additions & 47 deletions docs/evaluate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)));

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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)) {
Expand Down
35 changes: 35 additions & 0 deletions docs/public-contract-checks.mjs
Original file line number Diff line number Diff line change
@@ -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);
}
32 changes: 2 additions & 30 deletions site/scripts/evaluate-live.mjs
Original file line number Diff line number Diff line change
@@ -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.");
Expand Down Expand Up @@ -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 (
Expand Down
2 changes: 2 additions & 0 deletions site/scripts/evaluate.mjs
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -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 [
/<title>json-document - Agent artifact editing<\/title>/,
Expand Down
65 changes: 65 additions & 0 deletions site/tests/unit/public-document-contract.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});