From 404d3a2dd32aa335271a6aec3490e53f28371ccc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:22:15 +0900 Subject: [PATCH 01/15] test(diagnostics): require explicit package subpath RED --- src/writingDiagnosticsPackage.test.ts | 130 ++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 src/writingDiagnosticsPackage.test.ts diff --git a/src/writingDiagnosticsPackage.test.ts b/src/writingDiagnosticsPackage.test.ts new file mode 100644 index 00000000..422422dd --- /dev/null +++ b/src/writingDiagnosticsPackage.test.ts @@ -0,0 +1,130 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { + DEFAULT_WRITING_DIAGNOSTIC_LIMITS, + TEXT_POSITION_PROJECTION_ID, + TEXT_POSITION_PROJECTION_VERSION, + WritingDiagnosticError, + WritingDiagnosticProjectionError, + buildTextProjectionMap, + resolveTextPositionSelector, + validateWritingDiagnostics, +} from './writing-diagnostics/index.js'; + +const repositoryRoot = process.cwd(); +const packageJson = JSON.parse( + readFileSync(join(repositoryRoot, 'package.json'), 'utf8'), +) as { + scripts: Record; + exports: Record< + string, + string | { types?: string; import?: string; require?: string } + >; +}; + +function readRepositoryFile(path: string): string { + return readFileSync(join(repositoryRoot, path), 'utf8'); +} + +describe('writing diagnostics package boundary', () => { + it('exports the strict contract and exact selector-resolution primitives', () => { + expect(DEFAULT_WRITING_DIAGNOSTIC_LIMITS.maximumDiagnostics).toBeGreaterThan(0); + expect(TEXT_POSITION_PROJECTION_ID).toBe('inkspan-prosemirror-text'); + expect(TEXT_POSITION_PROJECTION_VERSION).toBe(1); + expect(typeof WritingDiagnosticError).toBe('function'); + expect(typeof WritingDiagnosticProjectionError).toBe('function'); + expect(typeof validateWritingDiagnostics).toBe('function'); + expect(typeof buildTextProjectionMap).toBe('function'); + expect(typeof resolveTextPositionSelector).toBe('function'); + + const textNode = Object.freeze({ + isBlock: false, + isText: true, + isLeaf: false, + inlineContent: false, + text: 'Alpha', + nodeSize: 5, + }); + const documentNode = { + descendants( + visitor: (node: typeof textNode, position: number) => boolean | void, + ): void { + visitor(textNode, 1); + }, + } as Parameters[0]; + const projection = buildTextProjectionMap(documentNode); + expect(projection.text).toBe('Alpha'); + expect(projection.boundaryPositions).toEqual([1, 2, 3, 4, 5, 6]); + expect( + resolveTextPositionSelector( + documentNode, + { type: 'TextPositionSelector', start: 0, end: 5 }, + { + id: TEXT_POSITION_PROJECTION_ID, + version: TEXT_POSITION_PROJECTION_VERSION, + }, + ), + ).toEqual({ from: 1, to: 6 }); + }); + + it('publishes one explicit ESM, CommonJS, and declaration export map', () => { + expect(packageJson.exports['./writing-diagnostics']).toEqual({ + types: './dist/writing-diagnostics/index.d.ts', + import: './dist/cwl-writing-diagnostics.js', + require: './dist/cwl-writing-diagnostics.cjs', + }); + expect(packageJson.scripts.build).toContain( + 'vite build --config vite.writing-diagnostics.config.ts', + ); + expect(packageJson.scripts['verify:package']).toContain( + 'node ./scripts/verify-writing-diagnostics-subpath-package.mjs', + ); + }); + + it('defines a dual-format declaration build with sourcemaps', () => { + const configuration = readRepositoryFile( + 'vite.writing-diagnostics.config.ts', + ); + expect(configuration).toContain("src/writing-diagnostics/index.ts"); + expect(configuration).toContain("'cwl-writing-diagnostics.js'"); + expect(configuration).toContain("'cwl-writing-diagnostics.cjs'"); + expect(configuration).toContain("formats: ['es', 'cjs']"); + expect(configuration).toContain('sourcemap: true'); + expect(configuration).toContain('vite-plugin-dts'); + expect(configuration).not.toMatch(/@vitejs\/plugin-react|react|yjs/iu); + }); + + it('keeps the public barrel and packed consumer framework-neutral', () => { + const barrel = readRepositoryFile('src/writing-diagnostics/index.ts'); + expect(barrel).toContain("from '../writingDiagnostics.js'"); + expect(barrel).toContain("from '../writingDiagnosticProjection.js'"); + expect(barrel).toContain("from '../textPositionSelectorEvidence.js'"); + expect(barrel).not.toMatch( + /(?:@tiptap\/react|react(?:-dom)?|collaboration|yjs|components|extensions|fetch\s*\(|process\.env|import\.meta\.env)/iu, + ); + + const verifier = readRepositoryFile( + 'scripts/verify-writing-diagnostics-subpath-package.mjs', + ); + for (const requiredEvidence of [ + 'consumer.mjs', + 'consumer.cjs', + 'ssr-consumer.mjs', + 'consumer.ts', + 'strict: true', + 'skipLibCheck: false', + 'validateWritingDiagnostics', + 'buildTextProjectionMap', + 'resolveTextPositionSelector', + 'WritingDiagnosticProjectionError', + 'dynamicLoaderPattern', + 'ambientAuthorityPattern', + ]) { + expect(verifier).toContain(requiredEvidence); + } + expect(verifier).not.toMatch( + /from ['"](?:react|react-dom|yjs|@tiptap\/react)|require\(['"](?:react|react-dom|yjs|@tiptap\/react)/u, + ); + }); +}); From ed76f6d8dee0c1ba914b14fe0df8d294c4812326 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:22:38 +0900 Subject: [PATCH 02/15] ci(diagnostics): exercise package boundary RED --- .../writing-diagnostics-package-tdd.yml | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/writing-diagnostics-package-tdd.yml diff --git a/.github/workflows/writing-diagnostics-package-tdd.yml b/.github/workflows/writing-diagnostics-package-tdd.yml new file mode 100644 index 00000000..3c2338cd --- /dev/null +++ b/.github/workflows/writing-diagnostics-package-tdd.yml @@ -0,0 +1,42 @@ +name: Writing Diagnostics Package TDD + +on: + push: + branches: + - feat/writing-diagnostics-package + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: writing-diagnostics-package-tdd-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + framework-neutral-package: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + persist-credentials: false + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Run package-boundary acceptance + run: >- + pnpm exec vitest run + src/writingDiagnosticsPackage.test.ts + src/writingDiagnosticsExports.test.ts + --pool=forks + --maxWorkers=1 + - name: Typecheck package contracts + run: pnpm typecheck From 0b1679d14ca2197a41943a347d930e9716c7c456 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:24:42 +0900 Subject: [PATCH 03/15] feat(diagnostics): expose framework-neutral contract --- src/writing-diagnostics/index.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/writing-diagnostics/index.ts b/src/writing-diagnostics/index.ts index 83c7afd0..6302480d 100644 --- a/src/writing-diagnostics/index.ts +++ b/src/writing-diagnostics/index.ts @@ -1,7 +1,8 @@ /** * Framework-independent public surface for host-supplied writing diagnostics. * - * This subpath validates bounded revision-scoped diagnostic proposals. It does + * This subpath validates bounded revision-scoped diagnostic proposals and maps + * exact W3C text-position selectors to structural ProseMirror ranges. It does * not import React, create editor instances, call models/providers or networks, * persist authored content, infer language quality, or mutate a document. */ @@ -17,3 +18,21 @@ export type { WritingDiagnosticErrorCode, WritingDiagnosticLimits, } from '../writingDiagnostics.js'; +export { + WritingDiagnosticProjectionError, + buildTextProjectionMap, + resolveTextPositionSelector, +} from '../writingDiagnosticProjection.js'; +export type { + CwlWritingDiagnosticTextProjectionMap, + WritingDiagnosticProjectionErrorCode, +} from '../writingDiagnosticProjection.js'; +export { + TEXT_POSITION_PROJECTION_ID, + TEXT_POSITION_PROJECTION_VERSION, +} from '../textPositionSelectorEvidence.js'; +export type { + CwlEditorTextPositionSelector, + CwlEditorTextPositionSelectorEvidence, + CwlEditorTextProjectionIdentity, +} from '../textPositionSelectorEvidence.js'; From 3a6dd5237168d691a2b47aa12126c28ba9e421a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:24:58 +0900 Subject: [PATCH 04/15] build(diagnostics): add explicit package bundle --- vite.writing-diagnostics.config.ts | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 vite.writing-diagnostics.config.ts diff --git a/vite.writing-diagnostics.config.ts b/vite.writing-diagnostics.config.ts new file mode 100644 index 00000000..2dbf9d9c --- /dev/null +++ b/vite.writing-diagnostics.config.ts @@ -0,0 +1,37 @@ +import { resolve } from 'node:path'; +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; + +// Framework-neutral writing-diagnostic contract build: ZERO React, React DOM, +// TipTap editor UI/view, Yjs, provider, awareness, network, credential, +// persistence, naruon, contextual-orchestrator, email, or model imports. +// ProseMirror model appears only in erased TypeScript input types. +export default defineConfig({ + plugins: [ + dts({ + include: [ + 'src/writing-diagnostics', + 'src/writingDiagnostics.ts', + 'src/writingDiagnosticProjection.ts', + 'src/textPositionSelectorEvidence.ts', + 'src/graphemeBoundary.ts', + ], + exclude: ['src/**/*.test.ts', 'src/**/*.test.tsx', 'src/**/*.spec.ts'], + rollupTypes: false, + entryRoot: 'src', + }), + ], + build: { + emptyOutDir: false, + lib: { + entry: resolve(__dirname, 'src/writing-diagnostics/index.ts'), + name: 'InkspanWritingDiagnostics', + fileName: (format) => + format === 'es' + ? 'cwl-writing-diagnostics.js' + : 'cwl-writing-diagnostics.cjs', + formats: ['es', 'cjs'], + }, + sourcemap: true, + }, +}); From f741e145943f5a3c614ddcfa611be2d3de7cac16 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:26:39 +0900 Subject: [PATCH 05/15] test(diagnostics): verify packed framework-neutral consumers --- ...fy-writing-diagnostics-subpath-package.mjs | 377 ++++++++++++++++++ 1 file changed, 377 insertions(+) create mode 100644 scripts/verify-writing-diagnostics-subpath-package.mjs diff --git a/scripts/verify-writing-diagnostics-subpath-package.mjs b/scripts/verify-writing-diagnostics-subpath-package.mjs new file mode 100644 index 00000000..2834a76f --- /dev/null +++ b/scripts/verify-writing-diagnostics-subpath-package.mjs @@ -0,0 +1,377 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + renameSync, + rmSync, + symlinkSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const packageJson = JSON.parse( + readFileSync(join(repositoryRoot, 'package.json'), 'utf8'), +); +const verificationRoot = mkdtempSync( + join(tmpdir(), 'inkspan-writing-diagnostics-'), +); +const extractionDirectory = join(verificationRoot, 'extracted'); +const consumerDirectory = join(verificationRoot, 'consumer'); +const packageDirectory = join( + consumerDirectory, + 'node_modules', + ...packageJson.name.split('/'), +); + +const dynamicLoaderPattern = /(?:\bimport\s*\(|\brequire\s*\()/u; +const externalRuntimeImportPattern = + /(?:\bimport\s+(?:[^'";]*?\sfrom\s*)?['"][^'"]+['"]|\bexport\s+[^'";]*?\sfrom\s*['"][^'"]+['"])/u; +const ambientAuthorityPattern = + /(?:\bfetch\s*\(|\bXMLHttpRequest\b|\bWebSocket\b|\bEventSource\b|\bprocess\.env\b|\bimport\.meta\.env\b|\bDeno\.env\b|\bBun\.env\b)/u; +const forbiddenFrameworkPattern = + /(?:@tiptap\/react|react-dom|\breact\b|\byjs\b|y-prosemirror|contextual-orchestrator|\bnaruon\b)/iu; + +/** Execute one deterministic package-consumer command. */ +function run(command, argumentsList, cwd = repositoryRoot) { + return execFileSync(command, argumentsList, { + cwd, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + }); +} + +/** Build one real npm tarball and install its files without executing scripts. */ +function preparePackage() { + mkdirSync(extractionDirectory, { recursive: true }); + mkdirSync(dirname(packageDirectory), { recursive: true }); + const packOutput = run('npm', [ + 'pack', + '--json', + '--ignore-scripts', + '--pack-destination', + verificationRoot, + ]); + const packResult = JSON.parse(packOutput)[0]; + assert.equal(packResult.name, packageJson.name); + assert.equal(packResult.version, packageJson.version); + const tarballPath = join(verificationRoot, packResult.filename); + assert.ok(existsSync(tarballPath)); + run('tar', ['-xzf', tarballPath, '-C', extractionDirectory]); + renameSync(join(extractionDirectory, 'package'), packageDirectory); + writeFileSync( + join(consumerDirectory, 'package.json'), + '{"name":"inkspan-writing-diagnostics-consumer","private":true,"type":"module"}\n', + 'utf8', + ); + + // The public declarations use ProseMirror model types only. The packed fixture + // is extracted without package-manager installation, so expose the repository's + // frozen dependency solely for strict declaration resolution. + const repositoryTiptap = join(repositoryRoot, 'node_modules', '@tiptap'); + const consumerTiptap = join(consumerDirectory, 'node_modules', '@tiptap'); + assert.ok(existsSync(repositoryTiptap)); + symlinkSync(repositoryTiptap, consumerTiptap, 'dir'); +} + +/** Prove emitted JavaScript carries no framework, network, or credential authority. */ +function verifyAuthorityFreeBundles() { + for (const filename of [ + 'cwl-writing-diagnostics.js', + 'cwl-writing-diagnostics.cjs', + ]) { + const bundlePath = join(packageDirectory, 'dist', filename); + const bundleSource = readFileSync(bundlePath, 'utf8'); + assert.equal( + dynamicLoaderPattern.test(bundleSource), + false, + `${filename} must not invoke dynamic module loaders`, + ); + assert.doesNotMatch( + bundleSource, + externalRuntimeImportPattern, + `${filename} must not import external runtime authority`, + ); + assert.doesNotMatch( + bundleSource, + ambientAuthorityPattern, + `${filename} must not reference ambient network or credential authority`, + ); + assert.doesNotMatch( + bundleSource, + forbiddenFrameworkPattern, + `${filename} must remain framework and collaboration neutral`, + ); + } +} + +/** Return one complete strict host diagnostic for runtime consumer checks. */ +function diagnosticLiteral() { + const digestHex = '11'.repeat(32); + return { + diagnosticId: 'consumer-diagnostic', + documentRevision: { + algorithm: 'SHA-256', + digestHex, + strongEntityTag: `"sha256-${digestHex}"`, + }, + textProjection: { + id: 'inkspan-prosemirror-text', + version: 1, + }, + selector: { + type: 'TextPositionSelector', + start: 0, + end: 5, + }, + categoryCode: 'clarity', + priority: 'important', + title: 'Clarify the request', + explanation: 'State the requested action.', + suggestedReplacement: 'Omega', + provenance: { + workflowId: 'consumer-workflow', + workflowVersion: '1', + judgePolicyVersion: '1', + }, + }; +} + +/** Write a reusable fake structural document expression for runtime consumers. */ +function fakeDocumentSource() { + return `({ + descendants(visitor) { + visitor(Object.freeze({ + isBlock: false, + isText: true, + isLeaf: false, + inlineContent: false, + text: 'Alpha', + nodeSize: 5, + }), 1); + }, +})`; +} + +/** Exercise the exact public ESM, CommonJS, and SSR-safe subpath. */ +function verifyRuntimeConsumers() { + const diagnostic = JSON.stringify(diagnosticLiteral()); + const fakeDocument = fakeDocumentSource(); + const esmPath = join(consumerDirectory, 'consumer.mjs'); + writeFileSync( + esmPath, + `import assert from 'node:assert/strict'; +import { + DEFAULT_WRITING_DIAGNOSTIC_LIMITS, + TEXT_POSITION_PROJECTION_ID, + TEXT_POSITION_PROJECTION_VERSION, + WritingDiagnosticError, + WritingDiagnosticProjectionError, + buildTextProjectionMap, + resolveTextPositionSelector, + validateWritingDiagnostics, +} from '${packageJson.name}/writing-diagnostics'; +assert.equal(DEFAULT_WRITING_DIAGNOSTIC_LIMITS.maxDiagnostics, 256); +assert.equal(TEXT_POSITION_PROJECTION_ID, 'inkspan-prosemirror-text'); +assert.equal(TEXT_POSITION_PROJECTION_VERSION, 1); +assert.equal(typeof WritingDiagnosticError, 'function'); +assert.equal(typeof WritingDiagnosticProjectionError, 'function'); +const validated = validateWritingDiagnostics([${diagnostic}]); +assert.equal(validated.length, 1); +assert.equal(Object.isFrozen(validated), true); +const documentNode = ${fakeDocument}; +const projection = buildTextProjectionMap(documentNode); +assert.equal(projection.text, 'Alpha'); +assert.deepEqual( + resolveTextPositionSelector( + documentNode, + validated[0].selector, + validated[0].textProjection, + ), + { from: 1, to: 6 }, +); +`, + 'utf8', + ); + + const cjsPath = join(consumerDirectory, 'consumer.cjs'); + writeFileSync( + cjsPath, + `const assert = require('node:assert/strict'); +const diagnostics = require('${packageJson.name}/writing-diagnostics'); +assert.equal(diagnostics.DEFAULT_WRITING_DIAGNOSTIC_LIMITS.maxDiagnostics, 256); +assert.equal(diagnostics.TEXT_POSITION_PROJECTION_ID, 'inkspan-prosemirror-text'); +assert.equal(diagnostics.TEXT_POSITION_PROJECTION_VERSION, 1); +const validated = diagnostics.validateWritingDiagnostics([${diagnostic}]); +assert.equal(validated.length, 1); +const documentNode = ${fakeDocument}; +assert.equal(diagnostics.buildTextProjectionMap(documentNode).text, 'Alpha'); +assert.deepEqual( + diagnostics.resolveTextPositionSelector( + documentNode, + validated[0].selector, + validated[0].textProjection, + ), + { from: 1, to: 6 }, +); +const failure = new diagnostics.WritingDiagnosticProjectionError('selector'); +assert.equal(failure.code, 'selector'); +`, + 'utf8', + ); + + const ssrPath = join(consumerDirectory, 'ssr-consumer.mjs'); + writeFileSync( + ssrPath, + `import assert from 'node:assert/strict'; +import * as diagnostics from '${packageJson.name}/writing-diagnostics'; +assert.equal(typeof globalThis.document, 'undefined'); +assert.equal(typeof globalThis.window, 'undefined'); +assert.deepEqual(diagnostics.validateWritingDiagnostics([]), []); +assert.equal(typeof diagnostics.resolveTextPositionSelector, 'function'); +`, + 'utf8', + ); + + run(process.execPath, [esmPath], consumerDirectory); + run(process.execPath, [cjsPath], consumerDirectory); + run(process.execPath, [ssrPath], consumerDirectory); +} + +/** Compile one strict TypeScript consumer against only the public subpath. */ +function verifyDeclarationConsumer() { + const sourcePath = join(consumerDirectory, 'consumer.ts'); + const configurationPath = join(consumerDirectory, 'tsconfig.json'); + writeFileSync( + sourcePath, + `import { + DEFAULT_WRITING_DIAGNOSTIC_LIMITS, + TEXT_POSITION_PROJECTION_ID, + TEXT_POSITION_PROJECTION_VERSION, + WritingDiagnosticError, + WritingDiagnosticProjectionError, + buildTextProjectionMap, + resolveTextPositionSelector, + validateWritingDiagnostics, + type CwlEditorTextPositionSelector, + type CwlEditorTextPositionSelectorEvidence, + type CwlEditorTextProjectionIdentity, + type CwlWritingDiagnostic, + type CwlWritingDiagnosticPriority, + type CwlWritingDiagnosticProvenance, + type CwlWritingDiagnosticTextProjectionMap, + type WritingDiagnosticErrorCode, + type WritingDiagnosticLimits, + type WritingDiagnosticProjectionErrorCode, +} from '${packageJson.name}/writing-diagnostics'; +import type { Node as ProseMirrorNode } from '@tiptap/pm/model'; +declare const documentNode: ProseMirrorNode; +const digestHex = '11'.repeat(32); +const selector: CwlEditorTextPositionSelector = { + type: 'TextPositionSelector', + start: 0, + end: 5, +}; +const textProjection: CwlEditorTextProjectionIdentity = { + id: TEXT_POSITION_PROJECTION_ID, + version: TEXT_POSITION_PROJECTION_VERSION, +}; +const provenance: CwlWritingDiagnosticProvenance = { + workflowId: 'consumer-workflow', + workflowVersion: '1', + judgePolicyVersion: '1', +}; +const priority: CwlWritingDiagnosticPriority = 'important'; +const diagnostic: CwlWritingDiagnostic = { + diagnosticId: 'consumer-diagnostic', + documentRevision: { + algorithm: 'SHA-256', + digestHex, + strongEntityTag: \`"sha256-\${digestHex}"\`, + }, + textProjection, + selector, + categoryCode: 'clarity', + priority, + title: 'Clarify the request', + explanation: 'State the requested action.', + provenance, +}; +const limits: WritingDiagnosticLimits = { + maxDiagnostics: DEFAULT_WRITING_DIAGNOSTIC_LIMITS.maxDiagnostics, +}; +const validated = validateWritingDiagnostics([diagnostic], limits); +const map: CwlWritingDiagnosticTextProjectionMap = buildTextProjectionMap(documentNode); +const resolved = resolveTextPositionSelector( + documentNode, + validated[0]!.selector, + validated[0]!.textProjection, +); +const evidence: CwlEditorTextPositionSelectorEvidence = { + revision: diagnostic.documentRevision, + selector, + textProjection, +}; +const contractCode: WritingDiagnosticErrorCode = 'contract'; +const projectionCode: WritingDiagnosticProjectionErrorCode = 'selector'; +const contractFailure = new WritingDiagnosticError(contractCode); +const projectionFailure = new WritingDiagnosticProjectionError(projectionCode); +void [ + map.text, + resolved.from, + resolved.to, + evidence.revision.strongEntityTag, + contractFailure.code, + projectionFailure.code, +]; +`, + 'utf8', + ); + writeFileSync( + configurationPath, + `${JSON.stringify( + { + compilerOptions: { + noEmit: true, + strict: true, + skipLibCheck: false, + module: 'NodeNext', + moduleResolution: 'NodeNext', + target: 'ES2022', + lib: ['ES2022', 'DOM', 'DOM.Iterable'], + types: [], + }, + files: ['./consumer.ts'], + }, + null, + 2, + )}\n`, + 'utf8', + ); + const compilerPath = join( + repositoryRoot, + 'node_modules', + 'typescript', + 'bin', + 'tsc', + ); + assert.ok(existsSync(compilerPath)); + run(process.execPath, [compilerPath, '--project', configurationPath], consumerDirectory); +} + +try { + preparePackage(); + verifyAuthorityFreeBundles(); + verifyRuntimeConsumers(); + verifyDeclarationConsumer(); + console.log( + `Verified packed ${packageJson.name}/writing-diagnostics through authority-bounded ESM, CommonJS, SSR, and strict TypeScript consumers.`, + ); +} finally { + rmSync(verificationRoot, { recursive: true, force: true }); +} From 9e0a712f1eec7e26578d80128770ce54c8e8b774 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:27:12 +0900 Subject: [PATCH 06/15] test(diagnostics): align package contract names --- src/writingDiagnosticsPackage.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/writingDiagnosticsPackage.test.ts b/src/writingDiagnosticsPackage.test.ts index 422422dd..c9ed8769 100644 --- a/src/writingDiagnosticsPackage.test.ts +++ b/src/writingDiagnosticsPackage.test.ts @@ -29,7 +29,7 @@ function readRepositoryFile(path: string): string { describe('writing diagnostics package boundary', () => { it('exports the strict contract and exact selector-resolution primitives', () => { - expect(DEFAULT_WRITING_DIAGNOSTIC_LIMITS.maximumDiagnostics).toBeGreaterThan(0); + expect(DEFAULT_WRITING_DIAGNOSTIC_LIMITS.maxDiagnostics).toBeGreaterThan(0); expect(TEXT_POSITION_PROJECTION_ID).toBe('inkspan-prosemirror-text'); expect(TEXT_POSITION_PROJECTION_VERSION).toBe(1); expect(typeof WritingDiagnosticError).toBe('function'); @@ -52,7 +52,7 @@ describe('writing diagnostics package boundary', () => { ): void { visitor(textNode, 1); }, - } as Parameters[0]; + } as unknown as Parameters[0]; const projection = buildTextProjectionMap(documentNode); expect(projection.text).toBe('Alpha'); expect(projection.boundaryPositions).toEqual([1, 2, 3, 4, 5, 6]); @@ -92,7 +92,7 @@ describe('writing diagnostics package boundary', () => { expect(configuration).toContain("formats: ['es', 'cjs']"); expect(configuration).toContain('sourcemap: true'); expect(configuration).toContain('vite-plugin-dts'); - expect(configuration).not.toMatch(/@vitejs\/plugin-react|react|yjs/iu); + expect(configuration).not.toMatch(/@vitejs\/plugin-react|from ['"]react|from ['"]yjs/iu); }); it('keeps the public barrel and packed consumer framework-neutral', () => { From 43c3ccf435054650c91c78751c5525de1784bc92 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:28:59 +0900 Subject: [PATCH 07/15] ci(diagnostics): finalize package boundary once --- ...ting-diagnostics-package-finalize-once.yml | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 .github/workflows/writing-diagnostics-package-finalize-once.yml diff --git a/.github/workflows/writing-diagnostics-package-finalize-once.yml b/.github/workflows/writing-diagnostics-package-finalize-once.yml new file mode 100644 index 00000000..11fffbc4 --- /dev/null +++ b/.github/workflows/writing-diagnostics-package-finalize-once.yml @@ -0,0 +1,127 @@ +name: Writing Diagnostics Package Finalize Once + +on: + push: + branches: + - feat/writing-diagnostics-package + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: writing-diagnostics-package-finalize-once-${{ github.ref }} + cancel-in-progress: true + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + finalize-package-boundary: + runs-on: ubuntu-24.04 + timeout-minutes: 25 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + persist-credentials: true + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Publish explicit package map and scripts + run: | + python <<'PY' + import json + from pathlib import Path + + package_path = Path('package.json') + package = json.loads(package_path.read_text(encoding='utf-8')) + + diagnostics_export = { + 'types': './dist/writing-diagnostics/index.d.ts', + 'import': './dist/cwl-writing-diagnostics.js', + 'require': './dist/cwl-writing-diagnostics.cjs', + } + exports = package['exports'] + if './writing-diagnostics' in exports: + if exports['./writing-diagnostics'] != diagnostics_export: + raise SystemExit('existing writing-diagnostics export differs') + else: + rebuilt = {} + inserted = False + for key, value in exports.items(): + rebuilt[key] = value + if key == './text-position-selector': + rebuilt['./writing-diagnostics'] = diagnostics_export + inserted = True + if not inserted: + raise SystemExit('text-position-selector export anchor missing') + package['exports'] = rebuilt + + scripts = package['scripts'] + build_anchor = ( + 'vite build --config vite.text-position-selector.config.ts && ' + 'vite build --config vite.markdown.config.ts' + ) + build_replacement = ( + 'vite build --config vite.text-position-selector.config.ts && ' + 'vite build --config vite.writing-diagnostics.config.ts && ' + 'vite build --config vite.markdown.config.ts' + ) + if 'vite.writing-diagnostics.config.ts' not in scripts['build']: + if scripts['build'].count(build_anchor) != 1: + raise SystemExit('build script anchor changed') + scripts['build'] = scripts['build'].replace( + build_anchor, + build_replacement, + 1, + ) + + verifier = 'node ./scripts/verify-writing-diagnostics-subpath-package.mjs' + verify_anchor = ( + 'node ./scripts/verify-text-position-selector-subpath-package.mjs && ' + 'node ./scripts/verify-markdown-subpath-package.mjs' + ) + verify_replacement = ( + 'node ./scripts/verify-text-position-selector-subpath-package.mjs && ' + f'{verifier} && ' + 'node ./scripts/verify-markdown-subpath-package.mjs' + ) + if verifier not in scripts['verify:package']: + if scripts['verify:package'].count(verify_anchor) != 1: + raise SystemExit('package verifier anchor changed') + scripts['verify:package'] = scripts['verify:package'].replace( + verify_anchor, + verify_replacement, + 1, + ) + + package_path.write_text( + json.dumps(package, ensure_ascii=False, indent=2) + '\n', + encoding='utf-8', + ) + PY + - name: Verify focused package contract + run: | + pnpm exec vitest run \ + src/writingDiagnosticsPackage.test.ts \ + src/writingDiagnosticsExports.test.ts \ + --pool=forks --maxWorkers=1 + pnpm typecheck + pnpm build + node ./scripts/verify-writing-diagnostics-subpath-package.mjs + - name: Publish validated package map and remove this workflow + env: + TARGET_BRANCH: feat/writing-diagnostics-package + run: | + set -euo pipefail + rm .github/workflows/writing-diagnostics-package-finalize-once.yml + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add package.json .github/workflows/writing-diagnostics-package-finalize-once.yml + git diff --cached --check + git commit -m 'feat(diagnostics): publish explicit package subpath' + git push origin "HEAD:${TARGET_BRANCH}" From aa87a056d43c260a6f3270ab4b7eebff24acd74f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:30:03 +0900 Subject: [PATCH 08/15] docs(diagnostics): describe neutral package without names --- src/writing-diagnostics/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/writing-diagnostics/index.ts b/src/writing-diagnostics/index.ts index 6302480d..58669839 100644 --- a/src/writing-diagnostics/index.ts +++ b/src/writing-diagnostics/index.ts @@ -3,8 +3,9 @@ * * This subpath validates bounded revision-scoped diagnostic proposals and maps * exact W3C text-position selectors to structural ProseMirror ranges. It does - * not import React, create editor instances, call models/providers or networks, - * persist authored content, infer language quality, or mutate a document. + * not import a UI framework, create editor instances, call models, providers, + * or networks, persist authored content, infer language quality, or mutate a + * document. */ export { DEFAULT_WRITING_DIAGNOSTIC_LIMITS, From 02b408802a432f90d9a90808199302066d7eae3b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:31:22 +0000 Subject: [PATCH 09/15] feat(diagnostics): publish explicit package subpath --- ...ting-diagnostics-package-finalize-once.yml | 127 ------------------ package.json | 9 +- 2 files changed, 7 insertions(+), 129 deletions(-) delete mode 100644 .github/workflows/writing-diagnostics-package-finalize-once.yml diff --git a/.github/workflows/writing-diagnostics-package-finalize-once.yml b/.github/workflows/writing-diagnostics-package-finalize-once.yml deleted file mode 100644 index 11fffbc4..00000000 --- a/.github/workflows/writing-diagnostics-package-finalize-once.yml +++ /dev/null @@ -1,127 +0,0 @@ -name: Writing Diagnostics Package Finalize Once - -on: - push: - branches: - - feat/writing-diagnostics-package - workflow_dispatch: - -permissions: - contents: write - -concurrency: - group: writing-diagnostics-package-finalize-once-${{ github.ref }} - cancel-in-progress: true - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - finalize-package-boundary: - runs-on: ubuntu-24.04 - timeout-minutes: 25 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.sha }} - persist-credentials: true - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 22 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Publish explicit package map and scripts - run: | - python <<'PY' - import json - from pathlib import Path - - package_path = Path('package.json') - package = json.loads(package_path.read_text(encoding='utf-8')) - - diagnostics_export = { - 'types': './dist/writing-diagnostics/index.d.ts', - 'import': './dist/cwl-writing-diagnostics.js', - 'require': './dist/cwl-writing-diagnostics.cjs', - } - exports = package['exports'] - if './writing-diagnostics' in exports: - if exports['./writing-diagnostics'] != diagnostics_export: - raise SystemExit('existing writing-diagnostics export differs') - else: - rebuilt = {} - inserted = False - for key, value in exports.items(): - rebuilt[key] = value - if key == './text-position-selector': - rebuilt['./writing-diagnostics'] = diagnostics_export - inserted = True - if not inserted: - raise SystemExit('text-position-selector export anchor missing') - package['exports'] = rebuilt - - scripts = package['scripts'] - build_anchor = ( - 'vite build --config vite.text-position-selector.config.ts && ' - 'vite build --config vite.markdown.config.ts' - ) - build_replacement = ( - 'vite build --config vite.text-position-selector.config.ts && ' - 'vite build --config vite.writing-diagnostics.config.ts && ' - 'vite build --config vite.markdown.config.ts' - ) - if 'vite.writing-diagnostics.config.ts' not in scripts['build']: - if scripts['build'].count(build_anchor) != 1: - raise SystemExit('build script anchor changed') - scripts['build'] = scripts['build'].replace( - build_anchor, - build_replacement, - 1, - ) - - verifier = 'node ./scripts/verify-writing-diagnostics-subpath-package.mjs' - verify_anchor = ( - 'node ./scripts/verify-text-position-selector-subpath-package.mjs && ' - 'node ./scripts/verify-markdown-subpath-package.mjs' - ) - verify_replacement = ( - 'node ./scripts/verify-text-position-selector-subpath-package.mjs && ' - f'{verifier} && ' - 'node ./scripts/verify-markdown-subpath-package.mjs' - ) - if verifier not in scripts['verify:package']: - if scripts['verify:package'].count(verify_anchor) != 1: - raise SystemExit('package verifier anchor changed') - scripts['verify:package'] = scripts['verify:package'].replace( - verify_anchor, - verify_replacement, - 1, - ) - - package_path.write_text( - json.dumps(package, ensure_ascii=False, indent=2) + '\n', - encoding='utf-8', - ) - PY - - name: Verify focused package contract - run: | - pnpm exec vitest run \ - src/writingDiagnosticsPackage.test.ts \ - src/writingDiagnosticsExports.test.ts \ - --pool=forks --maxWorkers=1 - pnpm typecheck - pnpm build - node ./scripts/verify-writing-diagnostics-subpath-package.mjs - - name: Publish validated package map and remove this workflow - env: - TARGET_BRANCH: feat/writing-diagnostics-package - run: | - set -euo pipefail - rm .github/workflows/writing-diagnostics-package-finalize-once.yml - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add package.json .github/workflows/writing-diagnostics-package-finalize-once.yml - git diff --cached --check - git commit -m 'feat(diagnostics): publish explicit package subpath' - git push origin "HEAD:${TARGET_BRANCH}" diff --git a/package.json b/package.json index 4e55d924..a7bc0cc8 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,11 @@ "import": "./dist/cwl-text-position-selector.js", "require": "./dist/cwl-text-position-selector.cjs" }, + "./writing-diagnostics": { + "types": "./dist/writing-diagnostics/index.d.ts", + "import": "./dist/cwl-writing-diagnostics.js", + "require": "./dist/cwl-writing-diagnostics.cjs" + }, "./markdown": { "types": "./dist/markdown/index.d.ts", "import": "./dist/cwl-markdown.js", @@ -99,7 +104,7 @@ ], "scripts": { "dev": "vite", - "build": "tsc --noEmit && vite build && vite build --config vite.collaboration.config.ts && vite build --config vite.converter.config.ts && vite build --config vite.envelope-identity.config.ts && vite build --config vite.revision-evidence.config.ts && vite build --config vite.autosave.config.ts && vite build --config vite.text-position-selector.config.ts && vite build --config vite.markdown.config.ts && node ./scripts/copy-styles.mjs", + "build": "tsc --noEmit && vite build && vite build --config vite.collaboration.config.ts && vite build --config vite.converter.config.ts && vite build --config vite.envelope-identity.config.ts && vite build --config vite.revision-evidence.config.ts && vite build --config vite.autosave.config.ts && vite build --config vite.text-position-selector.config.ts && vite build --config vite.writing-diagnostics.config.ts && vite build --config vite.markdown.config.ts && node ./scripts/copy-styles.mjs", "build:demo": "vite build --config vite.demo.config.ts", "fonts": "node ./scripts/fetch-fonts.mjs", "preview": "vite preview", @@ -108,7 +113,7 @@ "test:watch": "vitest", "coverage": "vitest run --coverage", "test:package-config": "node --test ./scripts/revision-evidence-consumer-config.test.mjs ./scripts/release-metadata.test.mjs ./scripts/javascript-runtime-authority.test.mjs", - "verify:package": "pnpm run test:package-config && node ./tests/package/verify-package.mjs && node ./tests/package/verify-editor-placeholder-package.mjs && node ./scripts/verify-canonical-envelope-package.mjs && node ./scripts/verify-revision-evidence-package.mjs && node ./scripts/verify-framework-free-revision-evidence-package.mjs && node ./scripts/verify-framework-free-envelope-identity-package.mjs && node ./tests/package/verify-framework-free-autosave-package.mjs && node ./scripts/verify-text-position-selector-package.mjs && node ./scripts/verify-text-position-selector-subpath-package.mjs && node ./scripts/verify-markdown-subpath-package.mjs" + "verify:package": "pnpm run test:package-config && node ./tests/package/verify-package.mjs && node ./tests/package/verify-editor-placeholder-package.mjs && node ./scripts/verify-canonical-envelope-package.mjs && node ./scripts/verify-revision-evidence-package.mjs && node ./scripts/verify-framework-free-revision-evidence-package.mjs && node ./scripts/verify-framework-free-envelope-identity-package.mjs && node ./tests/package/verify-framework-free-autosave-package.mjs && node ./scripts/verify-text-position-selector-package.mjs && node ./scripts/verify-text-position-selector-subpath-package.mjs && node ./scripts/verify-writing-diagnostics-subpath-package.mjs && node ./scripts/verify-markdown-subpath-package.mjs" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", From 336216133fa7d8ee3cdc30545f1f580dd4e12c7f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:32:29 +0900 Subject: [PATCH 10/15] ci(diagnostics): run complete package acceptance --- .../workflows/writing-diagnostics-package-tdd.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/writing-diagnostics-package-tdd.yml b/.github/workflows/writing-diagnostics-package-tdd.yml index 3c2338cd..d886bdf9 100644 --- a/.github/workflows/writing-diagnostics-package-tdd.yml +++ b/.github/workflows/writing-diagnostics-package-tdd.yml @@ -19,7 +19,7 @@ env: jobs: framework-neutral-package: runs-on: ubuntu-24.04 - timeout-minutes: 20 + timeout-minutes: 30 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -40,3 +40,13 @@ jobs: --maxWorkers=1 - name: Typecheck package contracts run: pnpm typecheck + - name: Run complete production coverage gate + env: + NODE_OPTIONS: --max-old-space-size=6144 + run: pnpm coverage + - name: Build all package entrypoints + run: pnpm build + - name: Verify isolated packed-package consumers + run: pnpm verify:package + - name: Build demonstration application + run: pnpm build:demo From 8f4ff4e35169c9218978db0920641a0b78fd58f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:36:36 +0900 Subject: [PATCH 11/15] ci(diagnostics): document public diagnostics subpath once --- .../writing-diagnostics-package-docs-once.yml | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 .github/workflows/writing-diagnostics-package-docs-once.yml diff --git a/.github/workflows/writing-diagnostics-package-docs-once.yml b/.github/workflows/writing-diagnostics-package-docs-once.yml new file mode 100644 index 00000000..2777a024 --- /dev/null +++ b/.github/workflows/writing-diagnostics-package-docs-once.yml @@ -0,0 +1,85 @@ +name: Writing Diagnostics Package Docs Once + +on: + push: + branches: + - feat/writing-diagnostics-package + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: writing-diagnostics-package-docs-once-${{ github.ref }} + cancel-in-progress: true + +jobs: + document-public-subpath: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + persist-credentials: true + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - name: Document framework-neutral writing-diagnostics package boundary + run: | + python <<'PY' + from pathlib import Path + + distribution_path = Path('docs/package-distribution.md') + distribution = distribution_path.read_text(encoding='utf-8') + row_anchor = "| `@contextualwisdomlab/cwl-editor/text-position-selector` | `implemented_on_protected_main` — React-free text-position projection core implementing W3C `TextPositionSelector`; interactive capture, revision binding, authorization, persistence, and re-anchoring remain outside this subpath |\n" + row = "| `@contextualwisdomlab/cwl-editor/writing-diagnostics` | React-free revision-bound writing-diagnostic validation and text-position projection/resolution utilities; semantic review, model calls, policy, persistence, authorization, and editor mutation remain outside this subpath |\n" + if row not in distribution: + if distribution.count(row_anchor) != 1: + raise SystemExit('distribution row anchor changed') + distribution = distribution.replace(row_anchor, row_anchor + row, 1) + boundary_anchor = "- The text-position-selector subpath deliberately exposes only the deterministic\n projection constants, error type, selector constructor, and public value\n types. It does not expose the React imperative handle that captures editor\n state or bind a selector to a document revision. Hosts remain responsible for\n annotation identifiers/bodies, source-resource identity, authorization,\n tenancy, persistence, audit, and cross-revision re-anchoring.\n" + boundary = boundary_anchor + "- The writing-diagnostics subpath deliberately exposes only strict diagnostic\n validation, bounded diagnostic errors, deterministic text-projection mapping,\n and exact `TextPositionSelector` resolution. It imports no React, Yjs, provider,\n network, credential, model, naruon, or contextual-orchestrator runtime. It never\n decides whether authored text is wrong and never applies a replacement. The\n host owns semantic review, policy admission, authorization, persistence,\n revision capture, and any editor mutation.\n" + if 'The writing-diagnostics subpath deliberately exposes only strict diagnostic' not in distribution: + if distribution.count(boundary_anchor) != 1: + raise SystemExit('distribution boundary anchor changed') + distribution = distribution.replace(boundary_anchor, boundary, 1) + verify_anchor = "5. imports the root, collaboration, converter, autosave, envelope-identity,\n revision-evidence, text-position-selector, and Markdown surfaces through their\n" + verify_replacement = "5. imports the root, collaboration, converter, autosave, envelope-identity,\n revision-evidence, text-position-selector, writing-diagnostics, and Markdown surfaces through their\n" + if verify_anchor in distribution: + distribution = distribution.replace(verify_anchor, verify_replacement, 1) + distribution_path.write_text(distribution, encoding='utf-8') + + readme_path = Path('README.md') + readme = readme_path.read_text(encoding='utf-8') + capability_anchor = "- **Standalone conversion utilities** — browser/Node data-URI and base64 helpers\n are available without React or TipTap.\n" + capability = capability_anchor + "- **Framework-neutral writing diagnostics** — `@contextualwisdomlab/cwl-editor/writing-diagnostics` validates host-supplied revision-bound diagnostics and resolves exact text-position ranges without importing React, Yjs, a model SDK, credentials, or network authority. Semantic judgment and mutation remain host-owned.\n" + if '@contextualwisdomlab/cwl-editor/writing-diagnostics` validates host-supplied' not in readme: + if readme.count(capability_anchor) != 1: + raise SystemExit('README capability anchor changed') + readme = readme.replace(capability_anchor, capability, 1) + table_anchor = "| Text-position selector | `@contextualwisdomlab/cwl-editor/text-position-selector` | React-free deterministic W3C `TextPositionSelector` projection core |\n" + table_row = "| Writing diagnostics | `@contextualwisdomlab/cwl-editor/writing-diagnostics` | React-free revision-bound diagnostic validation and exact text-position projection/resolution core |\n" + if table_row not in readme: + if readme.count(table_anchor) != 1: + raise SystemExit('README table anchor changed') + readme = readme.replace(table_anchor, table_anchor + table_row, 1) + readme_path.write_text(readme, encoding='utf-8') + PY + - name: Verify package documentation contract + run: pnpm exec vitest run src/packageDistributionDocumentation.test.ts src/writingDiagnosticsPackage.test.ts --pool=forks --maxWorkers=1 + - name: Publish documentation and remove this workflow + env: + TARGET_BRANCH: feat/writing-diagnostics-package + run: | + set -euo pipefail + rm .github/workflows/writing-diagnostics-package-docs-once.yml + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add README.md docs/package-distribution.md .github/workflows/writing-diagnostics-package-docs-once.yml + git diff --cached --check + git commit -m 'docs(diagnostics): document public package boundary' + git push origin "HEAD:${TARGET_BRANCH}" From 54dba4a3df019908feb84d3a940c183fdf526783 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 13:37:14 +0000 Subject: [PATCH 12/15] docs(diagnostics): document public package boundary --- .../writing-diagnostics-package-docs-once.yml | 85 ------------------- README.md | 2 + docs/package-distribution.md | 10 ++- 3 files changed, 11 insertions(+), 86 deletions(-) delete mode 100644 .github/workflows/writing-diagnostics-package-docs-once.yml diff --git a/.github/workflows/writing-diagnostics-package-docs-once.yml b/.github/workflows/writing-diagnostics-package-docs-once.yml deleted file mode 100644 index 2777a024..00000000 --- a/.github/workflows/writing-diagnostics-package-docs-once.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: Writing Diagnostics Package Docs Once - -on: - push: - branches: - - feat/writing-diagnostics-package - workflow_dispatch: - -permissions: - contents: write - -concurrency: - group: writing-diagnostics-package-docs-once-${{ github.ref }} - cancel-in-progress: true - -jobs: - document-public-subpath: - runs-on: ubuntu-24.04 - timeout-minutes: 15 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.sha }} - persist-credentials: true - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 22 - cache: pnpm - - run: pnpm install --frozen-lockfile - - name: Document framework-neutral writing-diagnostics package boundary - run: | - python <<'PY' - from pathlib import Path - - distribution_path = Path('docs/package-distribution.md') - distribution = distribution_path.read_text(encoding='utf-8') - row_anchor = "| `@contextualwisdomlab/cwl-editor/text-position-selector` | `implemented_on_protected_main` — React-free text-position projection core implementing W3C `TextPositionSelector`; interactive capture, revision binding, authorization, persistence, and re-anchoring remain outside this subpath |\n" - row = "| `@contextualwisdomlab/cwl-editor/writing-diagnostics` | React-free revision-bound writing-diagnostic validation and text-position projection/resolution utilities; semantic review, model calls, policy, persistence, authorization, and editor mutation remain outside this subpath |\n" - if row not in distribution: - if distribution.count(row_anchor) != 1: - raise SystemExit('distribution row anchor changed') - distribution = distribution.replace(row_anchor, row_anchor + row, 1) - boundary_anchor = "- The text-position-selector subpath deliberately exposes only the deterministic\n projection constants, error type, selector constructor, and public value\n types. It does not expose the React imperative handle that captures editor\n state or bind a selector to a document revision. Hosts remain responsible for\n annotation identifiers/bodies, source-resource identity, authorization,\n tenancy, persistence, audit, and cross-revision re-anchoring.\n" - boundary = boundary_anchor + "- The writing-diagnostics subpath deliberately exposes only strict diagnostic\n validation, bounded diagnostic errors, deterministic text-projection mapping,\n and exact `TextPositionSelector` resolution. It imports no React, Yjs, provider,\n network, credential, model, naruon, or contextual-orchestrator runtime. It never\n decides whether authored text is wrong and never applies a replacement. The\n host owns semantic review, policy admission, authorization, persistence,\n revision capture, and any editor mutation.\n" - if 'The writing-diagnostics subpath deliberately exposes only strict diagnostic' not in distribution: - if distribution.count(boundary_anchor) != 1: - raise SystemExit('distribution boundary anchor changed') - distribution = distribution.replace(boundary_anchor, boundary, 1) - verify_anchor = "5. imports the root, collaboration, converter, autosave, envelope-identity,\n revision-evidence, text-position-selector, and Markdown surfaces through their\n" - verify_replacement = "5. imports the root, collaboration, converter, autosave, envelope-identity,\n revision-evidence, text-position-selector, writing-diagnostics, and Markdown surfaces through their\n" - if verify_anchor in distribution: - distribution = distribution.replace(verify_anchor, verify_replacement, 1) - distribution_path.write_text(distribution, encoding='utf-8') - - readme_path = Path('README.md') - readme = readme_path.read_text(encoding='utf-8') - capability_anchor = "- **Standalone conversion utilities** — browser/Node data-URI and base64 helpers\n are available without React or TipTap.\n" - capability = capability_anchor + "- **Framework-neutral writing diagnostics** — `@contextualwisdomlab/cwl-editor/writing-diagnostics` validates host-supplied revision-bound diagnostics and resolves exact text-position ranges without importing React, Yjs, a model SDK, credentials, or network authority. Semantic judgment and mutation remain host-owned.\n" - if '@contextualwisdomlab/cwl-editor/writing-diagnostics` validates host-supplied' not in readme: - if readme.count(capability_anchor) != 1: - raise SystemExit('README capability anchor changed') - readme = readme.replace(capability_anchor, capability, 1) - table_anchor = "| Text-position selector | `@contextualwisdomlab/cwl-editor/text-position-selector` | React-free deterministic W3C `TextPositionSelector` projection core |\n" - table_row = "| Writing diagnostics | `@contextualwisdomlab/cwl-editor/writing-diagnostics` | React-free revision-bound diagnostic validation and exact text-position projection/resolution core |\n" - if table_row not in readme: - if readme.count(table_anchor) != 1: - raise SystemExit('README table anchor changed') - readme = readme.replace(table_anchor, table_anchor + table_row, 1) - readme_path.write_text(readme, encoding='utf-8') - PY - - name: Verify package documentation contract - run: pnpm exec vitest run src/packageDistributionDocumentation.test.ts src/writingDiagnosticsPackage.test.ts --pool=forks --maxWorkers=1 - - name: Publish documentation and remove this workflow - env: - TARGET_BRANCH: feat/writing-diagnostics-package - run: | - set -euo pipefail - rm .github/workflows/writing-diagnostics-package-docs-once.yml - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add README.md docs/package-distribution.md .github/workflows/writing-diagnostics-package-docs-once.yml - git diff --cached --check - git commit -m 'docs(diagnostics): document public package boundary' - git push origin "HEAD:${TARGET_BRANCH}" diff --git a/README.md b/README.md index f2b02332..1f684ff6 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Office Open XML renderer for DOCX, XLSX, and PPTX. or runtime font request. - **Standalone conversion utilities** — browser/Node data-URI and base64 helpers are available without React or TipTap. +- **Framework-neutral writing diagnostics** — `@contextualwisdomlab/cwl-editor/writing-diagnostics` validates host-supplied revision-bound diagnostics and resolves exact text-position ranges without importing React, Yjs, a model SDK, credentials, or network authority. Semantic judgment and mutation remain host-owned. - **AI-authored Office files** — a network-free Python package renders strict JSON to DOCX, XLSX, or PPTX with formula-injection protection, losslessness checks, atomic publication, and a bundled JSON Schema. @@ -70,6 +71,7 @@ runtime. | Envelope identity | `@contextualwisdomlab/cwl-editor/envelope-identity` | Framework-independent bounded schema identity for host-owned migration routing | | Revision evidence | `@contextualwisdomlab/cwl-editor/revision-evidence` | Framework-independent canonical envelope, strong revision, and transition evidence | | Text-position selector | `@contextualwisdomlab/cwl-editor/text-position-selector` | React-free deterministic W3C `TextPositionSelector` projection core | +| Writing diagnostics | `@contextualwisdomlab/cwl-editor/writing-diagnostics` | React-free revision-bound diagnostic validation and exact text-position projection/resolution core | | Autosave | `@contextualwisdomlab/cwl-editor/autosave` | Provider-neutral bounded single-flight persistence coordination | | Headless Markdown | `@contextualwisdomlab/cwl-editor/markdown` | React-free deterministic Markdown/HTML/email/plain-text conversion | | Styles | `@contextualwisdomlab/cwl-editor/styles.css` | Editor layout and theming | diff --git a/docs/package-distribution.md b/docs/package-distribution.md index ddb4df0e..d87f63e8 100644 --- a/docs/package-distribution.md +++ b/docs/package-distribution.md @@ -17,6 +17,7 @@ integrations. | `@contextualwisdomlab/cwl-editor/envelope-identity` | Framework-independent identity-only envelope routing for bounded schema identity inspection; migration remains host-owned | | `@contextualwisdomlab/cwl-editor/revision-evidence` | Framework-independent revision evidence and document-transition evidence for local content equality/lineage claims | | `@contextualwisdomlab/cwl-editor/text-position-selector` | `implemented_on_protected_main` — React-free text-position projection core implementing W3C `TextPositionSelector`; interactive capture, revision binding, authorization, persistence, and re-anchoring remain outside this subpath | +| `@contextualwisdomlab/cwl-editor/writing-diagnostics` | React-free revision-bound writing-diagnostic validation and text-position projection/resolution utilities; semantic review, model calls, policy, persistence, authorization, and editor mutation remain outside this subpath | | `@contextualwisdomlab/cwl-editor/markdown` | `implemented_on_active_pr` — headless deterministic Markdown/HTML/email/plain-text conversion with the same safe-link and strict inline-raster policies as the editor, without importing the React/TipTap editor graph | | `@contextualwisdomlab/cwl-editor/styles.css` | Editor layout and theming | | `@contextualwisdomlab/cwl-editor/fonts.css` | Full offline KR/EN/JP/SC/TC/VI font bundle | @@ -81,6 +82,13 @@ embedded in the npm tarball. state or bind a selector to a document revision. Hosts remain responsible for annotation identifiers/bodies, source-resource identity, authorization, tenancy, persistence, audit, and cross-revision re-anchoring. +- The writing-diagnostics subpath deliberately exposes only strict diagnostic + validation, bounded diagnostic errors, deterministic text-projection mapping, + and exact `TextPositionSelector` resolution. It imports no React, Yjs, provider, + network, credential, model, naruon, or contextual-orchestrator runtime. It never + decides whether authored text is wrong and never applies a replacement. The + host owns semantic review, policy admission, authorization, persistence, + revision capture, and any editor mutation. - Envelope identity output is routing metadata only. It does not accept an unsupported document generation as current semantics and does not move schema registry, migration, persistence, rollback, or authorization authority into @@ -109,7 +117,7 @@ production library build. The verification chain: 4. rejects internal source, tests, demos, Office files, coverage output, and workflow files from the npm tarball; 5. imports the root, collaboration, converter, autosave, envelope-identity, - revision-evidence, text-position-selector, and Markdown surfaces through their + revision-evidence, text-position-selector, writing-diagnostics, and Markdown surfaces through their dedicated packed-consumer checks, including framework-free isolation where that is part of the public contract; 6. exercises supported ESM/CommonJS entrypoints and compiles strict TypeScript From d26f9ccb476e87c34fdb6b762d8729ea8ec85776 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:39:02 +0900 Subject: [PATCH 13/15] feat(diagnostics): export selector evidence errors --- src/writing-diagnostics/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/writing-diagnostics/index.ts b/src/writing-diagnostics/index.ts index 58669839..2f358490 100644 --- a/src/writing-diagnostics/index.ts +++ b/src/writing-diagnostics/index.ts @@ -31,9 +31,11 @@ export type { export { TEXT_POSITION_PROJECTION_ID, TEXT_POSITION_PROJECTION_VERSION, + TextPositionSelectorEvidenceError, } from '../textPositionSelectorEvidence.js'; export type { CwlEditorTextPositionSelector, CwlEditorTextPositionSelectorEvidence, CwlEditorTextProjectionIdentity, + TextPositionSelectorEvidenceErrorCode, } from '../textPositionSelectorEvidence.js'; From 6169b81345671407b35bb87fd60f2f406e67990f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 22:39:48 +0900 Subject: [PATCH 14/15] test(diagnostics): execute selector evidence errors --- src/writingDiagnosticsPackage.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/writingDiagnosticsPackage.test.ts b/src/writingDiagnosticsPackage.test.ts index c9ed8769..eb41ac41 100644 --- a/src/writingDiagnosticsPackage.test.ts +++ b/src/writingDiagnosticsPackage.test.ts @@ -5,6 +5,7 @@ import { DEFAULT_WRITING_DIAGNOSTIC_LIMITS, TEXT_POSITION_PROJECTION_ID, TEXT_POSITION_PROJECTION_VERSION, + TextPositionSelectorEvidenceError, WritingDiagnosticError, WritingDiagnosticProjectionError, buildTextProjectionMap, @@ -34,6 +35,14 @@ describe('writing diagnostics package boundary', () => { expect(TEXT_POSITION_PROJECTION_VERSION).toBe(1); expect(typeof WritingDiagnosticError).toBe('function'); expect(typeof WritingDiagnosticProjectionError).toBe('function'); + expect(typeof TextPositionSelectorEvidenceError).toBe('function'); + const evidenceError = new TextPositionSelectorEvidenceError( + 'grapheme_boundary', + ); + expect(evidenceError).toMatchObject({ + name: 'TextPositionSelectorEvidenceError', + code: 'grapheme_boundary', + }); expect(typeof validateWritingDiagnostics).toBe('function'); expect(typeof buildTextProjectionMap).toBe('function'); expect(typeof resolveTextPositionSelector).toBe('function'); From cb49b1a6d646b5ba15f6aa88e568adf323fe05fc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 05:04:23 +0900 Subject: [PATCH 15/15] fix(ci): pin package TDD pnpm bootstrap --- .github/workflows/writing-diagnostics-package-tdd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/writing-diagnostics-package-tdd.yml b/.github/workflows/writing-diagnostics-package-tdd.yml index d886bdf9..d8f98783 100644 --- a/.github/workflows/writing-diagnostics-package-tdd.yml +++ b/.github/workflows/writing-diagnostics-package-tdd.yml @@ -25,7 +25,7 @@ jobs: with: ref: ${{ github.sha }} persist-credentials: false - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22