From ee0a3071b67d2c66976dc8c8354f1f1ddee73796 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 10:43:10 +0900 Subject: [PATCH 01/11] test(package): define text-position selector subpath contract --- src/textPositionSelectorPackage.test.ts | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/textPositionSelectorPackage.test.ts diff --git a/src/textPositionSelectorPackage.test.ts b/src/textPositionSelectorPackage.test.ts new file mode 100644 index 00000000..a4a9e630 --- /dev/null +++ b/src/textPositionSelectorPackage.test.ts @@ -0,0 +1,41 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +/** Read one repository file as UTF-8 text. */ +function repositoryFile(path: string): string { + return readFileSync(resolve(process.cwd(), path), 'utf8'); +} + +const packageMetadata = JSON.parse(repositoryFile('package.json')) as { + exports: Record; + scripts: Record; +}; + +describe('React-free text-position selector package contract', () => { + it('declares one independently consumable ESM CommonJS and TypeScript subpath', () => { + expect(packageMetadata.exports['./text-position-selector']).toEqual({ + types: './dist/text-position-selector/index.d.ts', + import: './dist/cwl-text-position-selector.js', + require: './dist/cwl-text-position-selector.cjs', + }); + expect(packageMetadata.scripts.build).toContain( + 'vite build --config vite.text-position-selector.config.ts', + ); + expect(packageMetadata.scripts['verify:package']).toContain( + 'verify-text-position-selector-subpath-package.mjs', + ); + expect(existsSync(resolve(process.cwd(), 'vite.text-position-selector.config.ts'))).toBe(true); + expect(existsSync(resolve(process.cwd(), 'src/text-position-selector/index.ts'))).toBe(true); + }); + + it('keeps the public distribution guide explicit about the React-free boundary', () => { + const guide = repositoryFile('docs/package-distribution.md'); + expect(guide).toContain( + '`@contextualwisdomlab/cwl-editor/text-position-selector`', + ); + expect(guide).toMatch(/React-free[^\n]*text-position/iu); + expect(guide).toMatch(/ESM[^\n]*CommonJS[^\n]*strict TypeScript/iu); + }); +}); From 11f1c35e84017002e47b6e9078d9656dac9de767 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 10:45:53 +0900 Subject: [PATCH 02/11] feat(package): add text-position selector entrypoint --- src/text-position-selector/index.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/text-position-selector/index.ts diff --git a/src/text-position-selector/index.ts b/src/text-position-selector/index.ts new file mode 100644 index 00000000..96e20fef --- /dev/null +++ b/src/text-position-selector/index.ts @@ -0,0 +1,18 @@ +/** + * React-free W3C text-position selector projection surface. + * + * This subpath exposes only deterministic projection primitives. Interactive + * editor-handle capture and exact revision binding remain on the root Inkspan + * editor contract. + */ +export { + TEXT_POSITION_PROJECTION_ID, + TEXT_POSITION_PROJECTION_VERSION, + TextPositionSelectorEvidenceError, + createTextPositionSelector, +} from '../textPositionSelectorEvidence.js'; +export type { + CwlEditorTextPositionSelector, + CwlEditorTextProjectionIdentity, + TextPositionSelectorEvidenceErrorCode, +} from '../textPositionSelectorEvidence.js'; From d073cba379b08cf7522eb21487bf3194c36dd5fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 10:46:07 +0900 Subject: [PATCH 03/11] build(package): add text-position selector bundle --- vite.text-position-selector.config.ts | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 vite.text-position-selector.config.ts diff --git a/vite.text-position-selector.config.ts b/vite.text-position-selector.config.ts new file mode 100644 index 00000000..e99d3280 --- /dev/null +++ b/vite.text-position-selector.config.ts @@ -0,0 +1,33 @@ +import { resolve } from 'node:path'; +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; + +// React-free selector projection build: ZERO React, React DOM, TipTap UI/view, +// Yjs, network, credential, persistence, naruon, orchestrator, or model imports. +// ProseMirror model/state appear only in erased TypeScript input types. +export default defineConfig({ + plugins: [ + dts({ + include: [ + 'src/text-position-selector', + 'src/textPositionSelectorEvidence.ts', + ], + exclude: ['src/**/*.test.ts', 'src/**/*.test.tsx', 'src/**/*.spec.ts'], + rollupTypes: false, + entryRoot: 'src', + }), + ], + build: { + emptyOutDir: false, + lib: { + entry: resolve(__dirname, 'src/text-position-selector/index.ts'), + name: 'InkspanTextPositionSelector', + fileName: (format) => + format === 'es' + ? 'cwl-text-position-selector.js' + : 'cwl-text-position-selector.cjs', + formats: ['es', 'cjs'], + }, + sourcemap: true, + }, +}); From ae007b2d1d85050187d51943315e40671fca0b6b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 10:46:52 +0900 Subject: [PATCH 04/11] test(package): verify packed text-position selector subpath --- ...text-position-selector-subpath-package.mjs | 205 ++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 scripts/verify-text-position-selector-subpath-package.mjs diff --git a/scripts/verify-text-position-selector-subpath-package.mjs b/scripts/verify-text-position-selector-subpath-package.mjs new file mode 100644 index 00000000..c61b679e --- /dev/null +++ b/scripts/verify-text-position-selector-subpath-package.mjs @@ -0,0 +1,205 @@ +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-text-position-selector-'), +); +const extractionDirectory = join(verificationRoot, 'extracted'); +const consumerDirectory = join(verificationRoot, 'consumer'); +const packageDirectory = join( + consumerDirectory, + 'node_modules', + ...packageJson.name.split('/'), +); +const forbiddenRuntimePattern = + /(?:['"](?:react(?:-dom)?(?:\/[^'"]*)?|@tiptap\/[^'"]+|prosemirror-[^'"]+|yjs(?:\/[^'"]*)?|naruon(?:\/[^'"]*)?|contextual-orchestrator(?:\/[^'"]*)?)['"])/u; + +/** 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-text-position-selector-consumer","private":true,"type":"module"}\n', + 'utf8', + ); + + // The package declares @tiptap/pm as a normal dependency. The packed fixture is + // extracted without a package-manager install, so expose the already-frozen + // repository dependency only 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 the emitted JavaScript bundle has no interactive framework dependency. */ +function verifyReactFreeBundles() { + for (const filename of [ + 'cwl-text-position-selector.js', + 'cwl-text-position-selector.cjs', + ]) { + const bundlePath = join(packageDirectory, 'dist', filename); + const bundleSource = readFileSync(bundlePath, 'utf8'); + assert.doesNotMatch( + bundleSource, + forbiddenRuntimePattern, + `${filename} must not reference interactive framework dependencies`, + ); + } +} + +/** Exercise the exact public ESM and CommonJS subpath from the packed package. */ +function verifyRuntimeConsumers() { + const esmPath = join(consumerDirectory, 'consumer.mjs'); + writeFileSync( + esmPath, + `import assert from 'node:assert/strict'; +import { + TEXT_POSITION_PROJECTION_ID, + TEXT_POSITION_PROJECTION_VERSION, + TextPositionSelectorEvidenceError, + createTextPositionSelector, +} from '${packageJson.name}/text-position-selector'; +assert.equal(TEXT_POSITION_PROJECTION_ID, 'inkspan-prosemirror-text'); +assert.equal(TEXT_POSITION_PROJECTION_VERSION, 1); +assert.equal(typeof TextPositionSelectorEvidenceError, 'function'); +assert.equal(typeof createTextPositionSelector, 'function'); +`, + 'utf8', + ); + + const cjsPath = join(consumerDirectory, 'consumer.cjs'); + writeFileSync( + cjsPath, + `const assert = require('node:assert/strict'); +const selector = require('${packageJson.name}/text-position-selector'); +assert.equal(selector.TEXT_POSITION_PROJECTION_ID, 'inkspan-prosemirror-text'); +assert.equal(selector.TEXT_POSITION_PROJECTION_VERSION, 1); +assert.equal(typeof selector.TextPositionSelectorEvidenceError, 'function'); +assert.equal(typeof selector.createTextPositionSelector, 'function'); +`, + 'utf8', + ); + + run(process.execPath, [esmPath], consumerDirectory); + run(process.execPath, [cjsPath], 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 { + TEXT_POSITION_PROJECTION_ID, + TEXT_POSITION_PROJECTION_VERSION, + TextPositionSelectorEvidenceError, + createTextPositionSelector, + type CwlEditorTextPositionSelector, + type CwlEditorTextProjectionIdentity, + type TextPositionSelectorEvidenceErrorCode, +} from '${packageJson.name}/text-position-selector'; +import type { Node as ProseMirrorNode } from '@tiptap/pm/model'; +import type { Selection } from '@tiptap/pm/state'; +declare const documentNode: ProseMirrorNode; +declare const selection: Selection; +const result = createTextPositionSelector(documentNode, selection); +const selector: CwlEditorTextPositionSelector = result.selector; +const projection: CwlEditorTextProjectionIdentity = result.textProjection; +const code: TextPositionSelectorEvidenceErrorCode = 'segmenter_unavailable'; +const failure = new TextPositionSelectorEvidenceError(code); +void [ + selector.start, + selector.end, + projection.id === TEXT_POSITION_PROJECTION_ID, + projection.version === TEXT_POSITION_PROJECTION_VERSION, + failure.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(); + verifyReactFreeBundles(); + verifyRuntimeConsumers(); + verifyDeclarationConsumer(); + console.log( + `Verified packed ${packageJson.name}/text-position-selector through React-free ESM, CommonJS, and strict TypeScript consumers.`, + ); +} finally { + rmSync(verificationRoot, { recursive: true, force: true }); +} From d1610f16a686e9569e5930386b18fbc29f0ec40f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:01:28 +0900 Subject: [PATCH 05/11] feat(package): declare text-position selector subpath --- package.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 521fbd4b..a217a651 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,11 @@ "import": "./dist/cwl-revision-evidence.js", "require": "./dist/cwl-revision-evidence.cjs" }, + "./text-position-selector": { + "types": "./dist/text-position-selector/index.d.ts", + "import": "./dist/cwl-text-position-selector.js", + "require": "./dist/cwl-text-position-selector.cjs" + }, "./styles.css": "./dist/cwl-editor.css", "./fonts.css": "./src/fonts/fonts.css", "./fonts-latin.css": "./src/fonts/fonts-latin.css", @@ -89,7 +94,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 && 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 && node ./scripts/copy-styles.mjs", "build:demo": "vite build --config vite.demo.config.ts", "fonts": "node ./scripts/fetch-fonts.mjs", "preview": "vite preview", @@ -98,7 +103,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", - "verify:package": "pnpm run test:package-config && node ./tests/package/verify-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" + "verify:package": "pnpm run test:package-config && node ./tests/package/verify-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" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", From 2e7e9512acb5a967a6ee21b95ae4064262500433 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:01:49 +0900 Subject: [PATCH 06/11] docs(package): document React-free selector subpath --- docs/package-distribution.md | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/package-distribution.md b/docs/package-distribution.md index 5f983368..12169633 100644 --- a/docs/package-distribution.md +++ b/docs/package-distribution.md @@ -16,6 +16,7 @@ integrations. | `@contextualwisdomlab/cwl-editor/converter` | Framework-independent base64 and data-URI utilities | | `@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` | React-free deterministic W3C `TextPositionSelector` projection core; interactive capture, revision binding, authorization, persistence, and re-anchoring remain outside this subpath | | `@contextualwisdomlab/cwl-editor/styles.css` | Editor layout and theming | | `@contextualwisdomlab/cwl-editor/fonts.css` | Full offline KR/EN/JP/SC/TC/VI font bundle | | `@contextualwisdomlab/cwl-editor/fonts-latin.css` | Smaller Latin/Vietnamese font bundle | @@ -55,11 +56,18 @@ embedded in the npm tarball. and collaboration entrypoints. It is declared in Inkspan's package dependencies so the consumer's package manager installs and resolves it; it is not merely a type-only dependency. -- The framework-independent autosave, converter, envelope-identity, and - revision-evidence entrypoints do not require React UI, a mounted editor, naruon, - contextual-orchestrator, a database, provider credentials, or host transport. - Their individual package-consumer gates additionally prevent framework - dependencies from leaking into subpaths whose public contracts exclude them. +- The framework-independent autosave, converter, envelope-identity, + revision-evidence, and text-position-selector entrypoints do not require React + UI, a mounted editor, naruon, contextual-orchestrator, a database, provider + credentials, or host transport. Their individual package-consumer gates + additionally prevent framework dependencies from leaking into subpaths whose + public contracts exclude them. +- The text-position-selector subpath deliberately exposes only the deterministic + projection constants, error type, selector constructor, and public value + types. It does not expose the React imperative handle that captures editor + 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. - 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 @@ -85,15 +93,23 @@ production library build. The verification chain: 3. confirms required licenses, declarations, styles, and font assets ship; 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, and - revision-evidence surfaces through their dedicated packed-consumer checks, - including framework-free isolation where that is part of the public contract; +5. imports the root, collaboration, converter, autosave, envelope-identity, + revision-evidence, and text-position-selector 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 consumers against the published declaration surfaces; 7. resolves public CSS and font subpaths; and 8. fails when a declared public export is absent, mispackaged, or coupled to a runtime graph that its public contract excludes. +The text-position-selector package check builds a real npm tarball, consumes the +public subpath through ESM and CommonJS, compiles a strict TypeScript consumer, +and rejects emitted runtime references to React, React DOM, TipTap UI/view, Yjs, +naruon, contextual-orchestrator, network, credential, or database graphs. Its +ProseMirror model/state inputs remain part of the selector's structural type +contract rather than an interactive UI runtime authority. + A version is release-ready only when this package gate, repository-wide 100% TypeScript coverage, production builds, the Python Office matrix, applicable browser/document-fidelity evidence, and required security/review/release gates From fd6973cb3e4686e0de11817d902d9227ee68ecbc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:05:31 +0900 Subject: [PATCH 07/11] fix(docs): satisfy selector boundary contract --- docs/package-distribution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/package-distribution.md b/docs/package-distribution.md index 12169633..0788bb87 100644 --- a/docs/package-distribution.md +++ b/docs/package-distribution.md @@ -16,7 +16,7 @@ integrations. | `@contextualwisdomlab/cwl-editor/converter` | Framework-independent base64 and data-URI utilities | | `@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` | React-free deterministic W3C `TextPositionSelector` projection core; interactive capture, revision binding, authorization, persistence, and re-anchoring remain outside this subpath | +| `@contextualwisdomlab/cwl-editor/text-position-selector` | 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/styles.css` | Editor layout and theming | | `@contextualwisdomlab/cwl-editor/fonts.css` | Full offline KR/EN/JP/SC/TC/VI font bundle | | `@contextualwisdomlab/cwl-editor/fonts-latin.css` | Smaller Latin/Vietnamese font bundle | From 4f4afabb38dd2138fb2b46d6b400bf92d197513b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:08:06 +0900 Subject: [PATCH 08/11] test(package): cover selector subpath exports --- src/textPositionSelectorPackage.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/textPositionSelectorPackage.test.ts b/src/textPositionSelectorPackage.test.ts index a4a9e630..9a3cc2f8 100644 --- a/src/textPositionSelectorPackage.test.ts +++ b/src/textPositionSelectorPackage.test.ts @@ -2,6 +2,12 @@ import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; +import { + TEXT_POSITION_PROJECTION_ID, + TEXT_POSITION_PROJECTION_VERSION, + TextPositionSelectorEvidenceError, + createTextPositionSelector, +} from './text-position-selector/index.js'; /** Read one repository file as UTF-8 text. */ function repositoryFile(path: string): string { @@ -14,6 +20,16 @@ const packageMetadata = JSON.parse(repositoryFile('package.json')) as { }; describe('React-free text-position selector package contract', () => { + it('exposes the deterministic selector core through the source subpath', () => { + expect(TEXT_POSITION_PROJECTION_ID).toBe('inkspan-prosemirror-text'); + expect(TEXT_POSITION_PROJECTION_VERSION).toBe(1); + expect(typeof createTextPositionSelector).toBe('function'); + expect(new TextPositionSelectorEvidenceError('segmenter_unavailable')).toMatchObject({ + name: 'TextPositionSelectorEvidenceError', + code: 'segmenter_unavailable', + }); + }); + it('declares one independently consumable ESM CommonJS and TypeScript subpath', () => { expect(packageMetadata.exports['./text-position-selector']).toEqual({ types: './dist/text-position-selector/index.d.ts', From 9e347e2997c80a3c7d2730e3d7586acedea06e61 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:17:17 +0900 Subject: [PATCH 09/11] test(package): define selector authority-boundary review contract --- src/textPositionSelectorPackage.test.ts | 31 +++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/textPositionSelectorPackage.test.ts b/src/textPositionSelectorPackage.test.ts index 9a3cc2f8..dcb1cfcd 100644 --- a/src/textPositionSelectorPackage.test.ts +++ b/src/textPositionSelectorPackage.test.ts @@ -46,12 +46,39 @@ describe('React-free text-position selector package contract', () => { expect(existsSync(resolve(process.cwd(), 'src/text-position-selector/index.ts'))).toBe(true); }); - it('keeps the public distribution guide explicit about the React-free boundary', () => { + it('keeps the public distribution guide explicit about active implementation and the React-free boundary', () => { const guide = repositoryFile('docs/package-distribution.md'); expect(guide).toContain( '`@contextualwisdomlab/cwl-editor/text-position-selector`', ); - expect(guide).toMatch(/React-free[^\n]*text-position/iu); + expect(guide).toMatch( + /text-position-selector`\s*\|\s*`implemented_on_active_pr`[^\n]*React-free/iu, + ); expect(guide).toMatch(/ESM[^\n]*CommonJS[^\n]*strict TypeScript/iu); }); + + it('binds packed verification to no external runtime imports or ambient network and credential authority', () => { + const verifier = repositoryFile( + 'scripts/verify-text-position-selector-subpath-package.mjs', + ); + const guide = repositoryFile('docs/package-distribution.md'); + + expect(verifier).toContain('externalRuntimeImportPattern'); + expect(verifier).toContain('ambientAuthorityPattern'); + for (const requiredBoundary of [ + 'fetch', + 'XMLHttpRequest', + 'WebSocket', + 'EventSource', + 'process\\.env', + 'import\\.meta\\.env', + 'Deno\\.env', + 'Bun\\.env', + 'require', + ]) { + expect(verifier).toContain(requiredBoundary); + } + expect(guide).toMatch(/any external runtime module import/iu); + expect(guide).toMatch(/ambient network[^.]*credential/iu); + }); }); From 9c9ad517f519e254c9b02fa87e5ef096421c4ef2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:21:31 +0900 Subject: [PATCH 10/11] fix(package): enforce selector runtime authority boundary --- ...text-position-selector-subpath-package.mjs | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/scripts/verify-text-position-selector-subpath-package.mjs b/scripts/verify-text-position-selector-subpath-package.mjs index c61b679e..08fb3d98 100644 --- a/scripts/verify-text-position-selector-subpath-package.mjs +++ b/scripts/verify-text-position-selector-subpath-package.mjs @@ -28,8 +28,17 @@ const packageDirectory = join( 'node_modules', ...packageJson.name.split('/'), ); -const forbiddenRuntimePattern = - /(?:['"](?:react(?:-dom)?(?:\/[^'"]*)?|@tiptap\/[^'"]+|prosemirror-[^'"]+|yjs(?:\/[^'"]*)?|naruon(?:\/[^'"]*)?|contextual-orchestrator(?:\/[^'"]*)?)['"])/u; + +// The selector bundle is intentionally self-contained. Type-only ProseMirror +// declarations are allowed, but emitted JavaScript must not acquire runtime +// authority through any external static/dynamic import, re-export, or require. +const externalRuntimeImportPattern = + /(?:\bimport\s*(?:\(\s*['"][^'"]+['"]\s*\)|(?:[^'"\n;]*?\sfrom\s*)?['"][^'"]+['"])|\bexport\s+[^'"\n;]*?\sfrom\s*['"][^'"]+['"]|\brequire\s*\(\s*['"][^'"]+['"]\s*\))/u; + +// A self-contained bundle must also remain free of ambient network and common +// environment-backed credential authority even when no module import is needed. +const ambientAuthorityPattern = + /(?:\bfetch\s*\(|\bXMLHttpRequest\b|\bWebSocket\b|\bEventSource\b|\bprocess\.env\b|\bimport\.meta\.env\b|\bDeno\.env\b|\bBun\.env\b)/u; /** Execute one deterministic package-consumer command. */ function run(command, argumentsList, cwd = repositoryRoot) { @@ -73,8 +82,8 @@ function preparePackage() { symlinkSync(repositoryTiptap, consumerTiptap, 'dir'); } -/** Prove the emitted JavaScript bundle has no interactive framework dependency. */ -function verifyReactFreeBundles() { +/** Prove emitted JavaScript carries no external or ambient runtime authority. */ +function verifyAuthorityFreeBundles() { for (const filename of [ 'cwl-text-position-selector.js', 'cwl-text-position-selector.cjs', @@ -83,8 +92,13 @@ function verifyReactFreeBundles() { const bundleSource = readFileSync(bundlePath, 'utf8'); assert.doesNotMatch( bundleSource, - forbiddenRuntimePattern, - `${filename} must not reference interactive framework dependencies`, + externalRuntimeImportPattern, + `${filename} must not import external runtime authority`, + ); + assert.doesNotMatch( + bundleSource, + ambientAuthorityPattern, + `${filename} must not reference ambient network or credential authority`, ); } } @@ -194,11 +208,11 @@ void [ try { preparePackage(); - verifyReactFreeBundles(); + verifyAuthorityFreeBundles(); verifyRuntimeConsumers(); verifyDeclarationConsumer(); console.log( - `Verified packed ${packageJson.name}/text-position-selector through React-free ESM, CommonJS, and strict TypeScript consumers.`, + `Verified packed ${packageJson.name}/text-position-selector through authority-bounded ESM, CommonJS, and strict TypeScript consumers.`, ); } finally { rmSync(verificationRoot, { recursive: true, force: true }); From 48bbbe4146567eee3be72a89a2f27d1752bd20f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 11:22:22 +0900 Subject: [PATCH 11/11] fix(docs): record active selector package authority --- docs/package-distribution.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/package-distribution.md b/docs/package-distribution.md index 0788bb87..d3f16ccb 100644 --- a/docs/package-distribution.md +++ b/docs/package-distribution.md @@ -16,7 +16,7 @@ integrations. | `@contextualwisdomlab/cwl-editor/converter` | Framework-independent base64 and data-URI utilities | | `@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` | 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/text-position-selector` | `implemented_on_active_pr` — 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/styles.css` | Editor layout and theming | | `@contextualwisdomlab/cwl-editor/fonts.css` | Full offline KR/EN/JP/SC/TC/VI font bundle | | `@contextualwisdomlab/cwl-editor/fonts-latin.css` | Smaller Latin/Vietnamese font bundle | @@ -104,11 +104,15 @@ production library build. The verification chain: runtime graph that its public contract excludes. The text-position-selector package check builds a real npm tarball, consumes the -public subpath through ESM and CommonJS, compiles a strict TypeScript consumer, -and rejects emitted runtime references to React, React DOM, TipTap UI/view, Yjs, -naruon, contextual-orchestrator, network, credential, or database graphs. Its -ProseMirror model/state inputs remain part of the selector's structural type -contract rather than an interactive UI runtime authority. +public subpath through ESM and CommonJS, and compiles a strict TypeScript +consumer. Its emitted JavaScript rejects **any external runtime module import**, +so framework, network, database, credential-provider, and model-SDK clients +cannot enter the selector bundle through module dependencies. A separate check +rejects **ambient network and credential authority** such as `fetch`, +`XMLHttpRequest`, `WebSocket`, `EventSource`, `process.env`, `import.meta.env`, +`Deno.env`, and `Bun.env`. Type-only ProseMirror model/state inputs remain part +of the selector's structural contract and introduce no interactive runtime +authority. A version is release-ready only when this package gate, repository-wide 100% TypeScript coverage, production builds, the Python Office matrix, applicable