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
25 changes: 25 additions & 0 deletions audits/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Canonical module audits

이 폴더는 공개 제품 문서가 아니라 repository 책임 감사를 위한 machine-readable
ledger를 소유합니다. 사이트는 결과를 표시할 수 있지만 감사 계약과 판정은
site-owned가 아닙니다.

`document-types.json`의 후보 분모는 `site/site-routes.json`의 `Document Types`
서브 메뉴입니다. 모든 후보는 `candidateProfiles`에 필요성, 역할, 현재 관찰된
schema와 근거 source symbol을 기록합니다. 이 schema는 감사 입력이며 정본 계약을
미리 확정하지 않습니다. 후보를 감사할 때는 다음 순서로 닫습니다.

1. package public entrypoint, Usage source registry와 live demo registry에서 runtime
closure를 정한다.
2. 독립 knowledge, decision, change reason 또는 lifecycle마다 responsibility
occurrence를 하나 만든다. 한 파일에 여러 책임이 있으면 passport를 나눈다.
3. 모든 occurrence에 `$canonical-module-audit`의 MECE disposition을 정확히 하나
기록한다.
4. `sourcePath`와 `symbol`을 실제 구현에 연결하고 `denominator`를 occurrence 수와
일치시킨다.
5. nonconforming occurrence가 남아 있으면 후보 상태를 `audited-tbd`로 유지한다.

Ledger 추가는 `npm run check:canonical-modules -w
@interactive-os/json-document-site`로 검증합니다. Guard는 내비게이션 분모 불일치,
누락된 source·symbol·passport field, 중복 occurrence, 잘못된 disposition과 성급한
TBD 종료를 실패시킵니다.
314 changes: 314 additions & 0 deletions audits/document-types.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions docs/evaluate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ function fail(message) {

const publicDocs = {
overview: read("docs/public/overview.md"),
applications: read("docs/public/applications.md"),
concepts: read("docs/public/concepts.md"),
foundation: read("docs/public/foundation.md"),
howWeBuild: read("docs/public/how-we-build.md"),
documentTypes: read("docs/public/document-types.md"),
selection: read("docs/public/selection.md"),
history: read("docs/public/history.md"),
Expand Down Expand Up @@ -199,6 +202,7 @@ if (JSON.stringify(fileNames("docs/public")) !== JSON.stringify([
"affordance.md",
"animation.md",
"api.md",
"applications.md",
"clipboard.md",
"collaboration-history.md",
"collaboration-lease.md",
Expand All @@ -217,8 +221,10 @@ if (JSON.stringify(fileNames("docs/public")) !== JSON.stringify([
"connectors.md",
"database.md",
"document-types.md",
"foundation.md",
"hands.md",
"history.md",
"how-we-build.md",
"intent-guide.md",
"intent.md",
"llms.txt",
Expand Down
38 changes: 38 additions & 0 deletions docs/public/applications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Applications

Application은 navigation, workflow, runtime과 제품 정책을 소유하는 완성된 제품
표면입니다. Artifact는 Application과 같은 앱이 아니라, 그 안에서 사람이 만들고
수정하며 agent와 주고받는 콘텐츠입니다.

## Calendar

[Calendar Application](/applications/calendar)은 Calendar Document Type, Editing,
Calendar Hand와 UI primitives를 day·week·month·year 제품 경험으로 조합합니다.

```text
Calendar Application
├─ Calendar Document Type · event, recurrence, interval
├─ Calendar Hand · selection, create, move, resize, history
├─ Calendar UI · grids, inspector, date controls
└─ App-owned · navigation, URL state, copy, fixture, layout
```

Calendar라는 이름 아래의 모든 코드를 App이 소유하지 않습니다. 재사용 책임은
각 canonical package에 남고 Application은 제품 조합과 정책만 소유합니다.

## AI Agent

[AI Agent Application](/applications/ai-agent)은 session runtime에 Composer,
Markdown, AG-UI와 A2UI projection을 조합합니다.

```text
AI Agent Application
├─ Composer · Mention Hands
├─ Markdown · Rich Text projection
├─ AG-UI → A2UI integration
└─ App-owned · session navigation, runtime connection, shell, policy
```

두 Application은 showcase가 아니라 책임을 발견하고 canonical API가 실제 제품에서
다시 소비되는지 검증하는 production composition root입니다. 개발 순환은
[How We Build](/docs/how-we-build)에서 설명합니다.
42 changes: 34 additions & 8 deletions docs/public/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,32 @@ Affordance ─ input grammar ─┐
UI Primitives ─ standard UI ├─ Host가 장르별 Hands를 조합
Rich Text 등 domain ────────┘

Hands를 제품 surface에 조합한 결과가 사람이 다루는 Artifact가 됩니다.
Hands를 surface에 조합한 결과가 사람이 다루는 Artifact가 됩니다. Artifact는
navigation이나 workflow를 소유하지 않는 콘텐츠입니다. Application은 Artifact와
다른 콘텐츠를 runtime과 제품 정책에 놓아 실제 제품 경험으로 제공합니다.
```

이 그림의 선은 허용된 의존·조합 방향입니다. 모든 노드를 순서대로 설치하라는
뜻이 아닙니다. Adapter와 Connector는 서로의 선행 계층이 아니며, 각각 플랫폼과
외부 라이브러리가 필요할 때 고릅니다. Collaboration은 다음 계층이 아니라
같은 `JSONDocument` 계약의 다른 구현입니다.

권장 읽기 순서는 `JSON Document → Document Types → Editing → Adapter → Connector → Affordance
→ UI Primitives → Hands → Artifact`입니다. 이 순서는 학습을 위한 서사일 뿐
package dependency를 주장하지 않습니다. Collaboration은 Core의 대체 구현과
profile 포함 관계를 따로 보기 위해 별도 묶음에서 읽습니다.
권장 읽기 순서는 `Foundation → Building Blocks → Hands → Artifact → Application`입니다.
Foundation 안에서는 JSON Document, Document Types, Editing과 Collaboration을,
Building Blocks에서는 Adapter, Connector, Affordance와 UI Primitives를 읽습니다.
이 순서는 학습을 위한 서사일 뿐 package dependency를 주장하지 않습니다.
Collaboration은 Core의 대체 구현과 profile 포함 관계로 Foundation 안에서 읽습니다.

프로젝트가 책임을 발견하는 방향은 이 읽기·구현 방향과 반대입니다.

```text
구현 의존: Foundation → Building Blocks → Hands → Artifact → Application
책임 발견: Application → 책임 발견 → Canonical Module → Application
```

먼저 제품을 만들고 실제 사용 흐름에서 반복되는 책임을 찾습니다. 추출된 책임은
canonical owner와 public API를 얻고, Application은 임시 구현 대신 그 API를 다시
소비합니다. 자세한 순환은 [How We Build](how-we-build.md)에서 설명합니다.

## JSON Document

Expand Down Expand Up @@ -107,8 +121,8 @@ Host 조합에서 함께 동작해야 닫힙니다. 재사용 책임은 owner pa

## Artifact

Artifact는 다음 책임 계층이 아니라 앞의 책임을 조합해 사람이 보고 고칠 수
있게 만든 결과입니다.
Artifact는 독립 App이 아니라 앞의 책임을 조합해 사람이 보고 고칠 수 있게 만든
Application 내부 콘텐츠입니다. navigation, workflow와 제품 정책은 소유하지 않습니다.
MD, PPT, Sheet는 서로 다른 화면과 Hands를 사용해도 같은 문서와 편집 계약을
공유할 수 있습니다.

Expand All @@ -117,13 +131,25 @@ MD, PPT, Sheet는 서로 다른 화면과 Hands를 사용해도 같은 문서와
놓는 정보 구조와 시각 가설만 확인하며, 실제 계약 증거는 각 Hands Live Demo와
package test에서 봅니다.

## Application

Application은 Artifact와 Hands를 실제 제품 경험으로 제공하는 최종 composition
root입니다. 주요 화면 영역과 실행 순서, URL과 navigation, 제품 copy와 fixture,
concrete runtime 연결은 Application에 남습니다. 문서의 의미, editing lifecycle,
platform translation과 반복 UI처럼 같은 역할과 책임을 갖는 코드는 canonical
module로 추출됩니다.

[Calendar와 AI Agent](/applications)는 제품에서 발견한 책임과 App에 남은 정책을
함께 보여 줍니다. Calendar Document Type, Calendar Hand와 Calendar Application은
같은 이름을 공유하지만 서로 다른 owner입니다.

## Collaboration

Collaboration은 JSON Document 계약을 여러 참여자의 인과 변경으로 구현합니다.
로컬 구현과 마찬가지로 값을 읽고, 변경을 적용하고, 결과를 구독하지만 내부
기록은 참여자의 변경 순서와 수렴을 다룹니다.

위치가 Artifact 다음인 것은 의존 방향이 아니라 문서 분류를 나타냅니다.
Collaboration은 Foundation 안에서 JSON Document와 같은 계약의 대체 구현으로 읽습니다.
협업 document를 Editing에 주입할 수 있지만 History command는 editor-local
History 대신 actor-local `runtime.history`로 연결해야 합니다. base → History →
Text profile의 포함 관계는 [Collaboration](collaboration.md)에 있습니다.
Expand Down
32 changes: 32 additions & 0 deletions docs/public/foundation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Foundation

Foundation은 Application, Artifact와 Hands가 공유하는 기반 계약입니다. 화면이나
제품 장르보다 먼저 값의 의미, 변경, 편집 상태와 협업 방식을 정의합니다.

## JSON Document

표의 셀과 문서의 블록은 생김새가 달라도 JSON 안에서 주소를 가집니다. 한 위치는
JSON Pointer로 가리키고 여러 위치는 JSONPath로 찾으며, 변경은 JSON Patch로
표현합니다. `JSONDocument`는 현재 값을 읽고, 찾고, 검증하고, 원자적으로 적용하고,
실제로 달라진 결과를 구독자에게 전달하는 공통 계약입니다.

## Document Types

Rich Text, Calendar, Database 같은 Document Type은 Foundation 위에서 데이터의
의미와 유효한 구조를 정의합니다. 같은 이름을 쓰는 Hand나 Application과는 별도
책임이며, 제품 화면이나 navigation을 소유하지 않습니다.

## Editing

선택, 보이는 순서, clipboard와 history처럼 편집하는 동안만 필요한 상태는 문서 값
옆에 둡니다. 화면 사건은 Intent가 되고 Editing은 현재 문서와 편집 상태를 읽어
처리합니다.

## Collaboration

협업은 다음 UI 계층이 아니라 같은 `JSONDocument` 계약의 다른 구현입니다. 여러
참여자의 변경을 인과 순서로 수렴시키면서도 Foundation의 읽기·변경·구독 진입점을
유지합니다.

다음으로 플랫폼과 생태계 연결을 고르려면 [Building Blocks](adapters.md)를,
전체 개념 관계를 먼저 보려면 [Concept Map](concepts.md)을 읽습니다.
46 changes: 46 additions & 0 deletions docs/public/how-we-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# 제품에서 정본 모듈을 발견하는 방법

json-document는 추상 계층을 먼저 완성한 뒤 제품에 적용하지 않습니다. Calendar나
AI Agent 같은 Application을 먼저 만들고, 실제 사용 흐름에서 반복되는 책임을
발견해 canonical module로 추출합니다. 제품은 추출된 공개 API를 다시 소비하며
경계를 검증합니다.

```text
Application을 만든다
실제 제품 사건과 반복 책임을 관찰한다
Document Type · Editing · Adapter · UI 책임을 분리한다
canonical module과 public API로 정본화한다
Application이 정본 API를 다시 소비한다
```

구현 의존 방향과 책임을 발견하는 방향은 서로 반대입니다.

```text
구현 의존: Foundation → Building Blocks → Hands → Artifact → Application
책임 발견: Application → 책임 발견 → Canonical Module → Application
```

여기서 Artifact는 독립 App이 아니라 Application이 만들고 편집하는 콘텐츠입니다.
Navigation, workflow, runtime과 제품 정책은 Application에 남습니다.

## Application에 남는 것

Application은 화면의 주요 영역과 실행 순서, URL과 navigation, 제품 copy,
permission, fixture, concrete runtime 연결을 소유합니다. 제품 전체를 제거했을 때
함께 사라지는 정책입니다.

## 모듈로 추출하는 것

문서의 의미와 유효성, selection과 history, 입력 번역, 반복되는 UI 동작처럼
제품 밖에서도 같은 역할과 책임을 갖는 코드는 canonical owner로 이동합니다.
한 Application에서만 발견됐더라도 독립적인 책임이면 이름과 경계를 갖습니다.

## 다시 제품으로 돌아오기

추출은 복사본을 하나 더 만드는 일이 아닙니다. Application의 임시 구현을 제거하고
canonical public API를 소비해야 순환이 닫힙니다. [Applications](/applications)는
각 제품에 남은 정책과 추출된 책임을 함께 보여 줍니다.
2 changes: 1 addition & 1 deletion site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"check:primitives": "node scripts/check-live-demo-primitives.mjs",
"check:ui-roles": "node scripts/check-ui-role-primitives.mjs",
"check:choice-id": "node scripts/check-choice-id.mjs",
"check:canonical-modules": "node scripts/check-canonical-module-closure.mjs",
"check:canonical-modules": "node scripts/check-canonical-module-closure.mjs && node scripts/check-document-type-audits.mjs && node scripts/check-documentation-page.mjs",
"check:interaction-handles": "node scripts/check-interaction-handles.mjs",
"check:contextual-affordance": "node scripts/check-contextual-affordance.mjs",
"check:product-shell-toolbar": "node scripts/check-product-shell-toolbar.mjs",
Expand Down
58 changes: 58 additions & 0 deletions site/scripts/check-document-type-audits.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";

const root = new URL("../..", import.meta.url).pathname;
const ledger = JSON.parse(readFileSync(join(root, "audits/document-types.json"), "utf8"));
const siteRoutes = JSON.parse(readFileSync(join(root, "site/site-routes.json"), "utf8"));
const allowed = new Set(["canonical consumer", "Host composition", "duplicate implementation", "canonical API gap", "missing canonical module", "mislocated module", "out of scope", "unverified"]);
const expectedCandidates = siteRoutes
.filter((route) => route.navigationGroup === "Document Types" && route.path !== "/docs/document-types")
.map((route) => route.path.slice("/docs/document-types/".length));

if (JSON.stringify(ledger.candidates) !== JSON.stringify(expectedCandidates)) throw new Error("Document Type audit candidates do not match the TBD navigation denominator");

for (const candidate of ledger.candidates) {
const profile = ledger.candidateProfiles[candidate];
if (profile === undefined) throw new Error(`${candidate} is missing its candidate profile`);
for (const field of ["why", "does", "schema", "sourcePath", "symbol"]) {
if (typeof profile[field] !== "string" || profile[field].trim() === "") throw new Error(`${candidate} candidate profile is missing ${field}`);
}
if (!Array.isArray(profile.fields) || profile.fields.length === 0) throw new Error(`${candidate} candidate profile is missing field descriptions`);
for (const field of profile.fields) {
if (typeof field.name !== "string" || field.name.trim() === "" || typeof field.description !== "string" || field.description.trim() === "") {
throw new Error(`${candidate} candidate profile has an incomplete field description`);
}
}
const sourceFile = join(root, profile.sourcePath);
if (!existsSync(sourceFile)) throw new Error(`${candidate} candidate profile source does not exist: ${profile.sourcePath}`);
if (!new RegExp(`\\b${profile.symbol}\\b`).test(readFileSync(sourceFile, "utf8"))) throw new Error(`${candidate} candidate profile symbol does not exist: ${profile.symbol}`);
}

for (const [candidate, audit] of Object.entries(ledger.audits)) {
if (!ledger.candidates.includes(candidate)) throw new Error(`unknown Document Type audit: ${candidate}`);
if (audit.denominator !== audit.occurrences.length) throw new Error(`${candidate} audit denominator mismatch: expected ${audit.denominator}, found ${audit.occurrences.length}`);
const ids = new Set();
for (const occurrence of audit.occurrences) {
if (ids.has(occurrence.id)) throw new Error(`${candidate} duplicate occurrence id: ${occurrence.id}`);
ids.add(occurrence.id);
if (!allowed.has(occurrence.disposition)) throw new Error(`${candidate}/${occurrence.id} has invalid disposition: ${occurrence.disposition}`);
for (const field of ["role", "knowledge", "decision", "changeReason", "stateLifecycle", "inputsOutputs", "currentOwner", "canonicalEvidence", "sourcePath", "intendedOwner", "nextCheck"]) {
if (typeof occurrence[field] !== "string" || occurrence[field].trim() === "") throw new Error(`${candidate}/${occurrence.id} is missing ${field}`);
}
const sourceFile = join(root, occurrence.sourcePath);
if (!existsSync(sourceFile)) throw new Error(`${candidate}/${occurrence.id} source does not exist: ${occurrence.sourcePath}`);
if (occurrence.symbol && !new RegExp(`\\b${occurrence.symbol}\\b`).test(readFileSync(sourceFile, "utf8"))) {
throw new Error(`${candidate}/${occurrence.id} symbol does not exist: ${occurrence.symbol}`);
}
}
}

const calendar = ledger.audits.calendar;
for (const role of ["Document Model", "Validation", "Projection", "Document Operation", "Editing lifecycle", "Affordance", "Web Adapter", "Hand composition", "Reusable UI behavior", "Host composition"]) {
if (!calendar.occurrences.some((occurrence) => occurrence.role === role)) throw new Error(`Calendar audit is missing role: ${role}`);
}
if (calendar.status !== "audited-tbd" || !calendar.occurrences.some((occurrence) => !["canonical consumer", "Host composition"].includes(occurrence.disposition))) {
throw new Error("Calendar must remain audited-tbd while nonconforming occurrences remain");
}

console.log(`Document Type audits ok; candidates=${ledger.candidates.length}; candidate profiles=${Object.keys(ledger.candidateProfiles).length}; audited=${Object.keys(ledger.audits).length}; Calendar occurrences=${calendar.occurrences.length}.`);
27 changes: 27 additions & 0 deletions site/scripts/check-documentation-page.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";

const root = new URL("../..", import.meta.url).pathname;
const docsRoot = join(root, "site/src/routes/docs");
const canonicalOwner = "DocumentationPage.tsx";
const consumers = ["DocsRoute.tsx", "ConceptsRoute.tsx", "DocumentTypeCandidateRoute.tsx"];

for (const name of readdirSync(docsRoot).filter((entry) => entry.endsWith(".tsx"))) {
const source = readFileSync(join(docsRoot, name), "utf8");
if (name !== canonicalOwner && name !== "MarkdownViewer.tsx" && /(?:MarkdownViewer|markdownHeadings)/.test(source)) {
throw new Error(`${name} bypasses the canonical DocumentationPage composition`);
}
}

const owner = readFileSync(join(docsRoot, canonicalOwner), "utf8");
for (const contract of ["PageFrame", "PageHeader", "MarkdownViewer", "markdownHeadings", "Documentation sections", "On this page", "max-w-3xl"]) {
if (!owner.includes(contract)) throw new Error(`DocumentationPage is missing canonical contract: ${contract}`);
}
for (const consumer of consumers) {
const source = readFileSync(join(docsRoot, consumer), "utf8");
if (!source.includes('from "./DocumentationPage"') || !source.includes("<DocumentationPage")) {
throw new Error(`${consumer} does not consume the canonical DocumentationPage`);
}
}

console.log(`DocumentationPage guard ok; owner=1; consumers=${consumers.length}; local bypasses=0.`);
2 changes: 2 additions & 0 deletions site/scripts/route-checks.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const routePathPattern = /^\/(?:[a-z0-9]+(?:-[a-z0-9]+)*\/?)*$/;
const navigationGroups = new Set([
"Introduction",
"JSON Document",
"Document Types",
"Collaboration",
Expand All @@ -10,6 +11,7 @@ const navigationGroups = new Set([
"UI Primitives",
"Hands",
"Artifact",
"Applications",
]);

export function validateSiteRoutes(routes, fail) {
Expand Down
Loading