-
Notifications
You must be signed in to change notification settings - Fork 0
feat(review): add W3C text-position selector evidence #91
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
8b657da
test(review): define W3C text-position evidence contract
seonghobae 4f22353
feat(review): add deterministic text-position projection
seonghobae c381dce
feat(review): type text-position handle capture
seonghobae b940bbf
feat(review): capture W3C text-position evidence
seonghobae 71791ab
feat(review): export text-position evidence types
seonghobae c930061
test(review): cover Unicode and grapheme selector semantics
seonghobae 21535ab
test(review): cover pre-editor text-position fallback
seonghobae 7916c31
test(review): define leaf and segmenter failure contracts
seonghobae 7528edb
fix(review): fail closed when grapheme segmentation is unavailable
seonghobae 609394e
docs(review): record text-position selector authority
seonghobae 12274e6
docs(review): remove connector-only citation markers
seonghobae e4b6170
feat(review): export selector error code type
seonghobae 0e09458
docs(review): document W3C selector projection
seonghobae b417d8d
test(docs): bind text-position selector authority
seonghobae 3abcfb8
test(package): verify text-position selector consumer
seonghobae 16185c8
test(package): include selector consumer verification
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| # W3C text-position selector evidence | ||
|
|
||
| Status: Implemented on active PR | ||
|
|
||
| ## Purpose | ||
|
|
||
| Inkspan already exposes revision-scoped ProseMirror selection evidence. That contract is intentionally local to one editor state: ProseMirror positions are structural positions, not portable W3C text offsets. This active change adds a second, privacy-minimized interoperability representation that binds a W3C `TextPositionSelector` to the exact same immutable document revision without copying selected text into ordinary evidence metadata. | ||
|
|
||
| ## Standards authority | ||
|
|
||
| The W3C *Web Annotation Data Model* Recommendation defines `TextPositionSelector` using an inclusive `start` and exclusive `end` in a normalized text representation. Its text-position processing model counts Unicode code points rather than implementation code units and cautions that selection boundaries should not split grapheme clusters. Position-only selectors avoid copying quote text into the annotation graph, but they are sensitive to source changes; Inkspan therefore binds every selector to an exact revision rather than claiming durable cross-revision anchoring. | ||
|
|
||
| ProseMirror remains the editor-structure authority. Its document positions are tree-structural coordinates, and `Node.textBetween(from, to, blockSeparator, leafText)` is the primitive used by the versioned Inkspan projection. A ProseMirror position is never relabeled as a W3C position by identity. | ||
|
|
||
| ECMA-402 13th edition, June 2026 is the current published ECMAScript internationalization standard. Inkspan uses `Intl.Segmenter` with `granularity: 'grapheme'` to reject selection boundaries that do not coincide with grapheme-cluster boundaries. A runtime lacking that capability fails closed with the stable `segmenter_unavailable` classification instead of silently weakening the evidence contract. | ||
|
|
||
| ## Projection version 1 | ||
|
|
||
| `textProjection` is part of the public evidence because selector offsets are meaningless without a deterministic projection identity. | ||
|
|
||
| Projection v1 is: | ||
|
|
||
| - `id = "inkspan-prosemirror-text"`; | ||
| - `version = 1`; | ||
| - logical ProseMirror document order, independent of visual bidirectional rendering order; | ||
| - U+000A LINE FEED between block boundaries where ProseMirror `textBetween` inserts the configured block separator; | ||
| - U+FFFC OBJECT REPLACEMENT CHARACTER for supported non-text leaf nodes; | ||
| - Unicode-code-point counting for W3C `start` and `end`; | ||
| - inclusive `start` and exclusive `end`; | ||
| - grapheme-cluster boundary validation before evidence is returned. | ||
|
|
||
| Array/tree order and actual text content remain authoritative. The projection does not normalize Unicode text, reorder bidirectional text visually, or invent source quote text. | ||
|
|
||
| ## Atomicity | ||
|
|
||
| `getTextPositionSelectorEvidence()` captures one `editor.state` before asynchronous digest work begins. The projection and selector are derived from that captured `state.doc` and `state.selection`; the document envelope used for SHA-256 revision derivation is produced from the same captured `state.doc`. A live editor mutation after digest work starts cannot change the pending evidence object. | ||
|
|
||
| The returned top-level evidence, `selector`, and `textProjection` are frozen. Ordinary evidence contains no selected text, surrounding quote, complete document envelope, actor, tenant, timestamp, model identity, authorization decision, transport result, signature, or durable-write claim. | ||
|
|
||
| ## Failure semantics | ||
|
|
||
| - Before editor creation, the handle resolves to `null`, matching the existing revision-scoped selection fallback. | ||
| - A selection boundary inside a grapheme cluster fails with `TextPositionSelectorEvidenceError.code = "grapheme_boundary"`. | ||
| - Absence of supported `Intl.Segmenter` grapheme segmentation fails with `code = "segmenter_unavailable"`. | ||
| - Existing document-envelope and digest validation failures retain their own fail-closed behavior. | ||
| - The API never silently adjusts an invalid boundary to a nearby grapheme boundary because doing so would change the user's selected range without explicit authority. | ||
|
|
||
| ## Privacy and ownership | ||
|
|
||
| Inkspan owns only the deterministic projection and exact-revision selector evidence. Hosts own annotation identifiers and bodies, source-resource IRI policy, authentication, authorization, tenant isolation, durable persistence, retention, audit, collaborative anchors, re-anchoring after revisions, publication, and any W3C Annotation graph stored or transmitted outside the editor. | ||
|
|
||
| A revision digest plus text-position selector proves neither who selected the range, when it was selected, whether it was authorized, nor whether an annotation was durably accepted. Hosts must compare the bound revision before reusing the positions. If the document changed, the host chooses compare, merge, fork, reload, or a separately designed collaborative re-anchoring strategy. | ||
|
|
||
| ## Compatibility and rollback | ||
|
|
||
| Projection semantics are versioned. A future change to block separators, leaf representations, normalization, code-point interpretation, or grapheme policy must publish a new projection version rather than silently reinterpret stored v1 offsets. Unknown projection versions must fail closed in any future parser/consumer. | ||
|
|
||
| Rollback removes the new selector API while leaving the pre-existing ProseMirror revision-scoped selection evidence intact. Rollback does not authorize a host to reinterpret existing v1 W3C selectors as ProseMirror coordinates. | ||
|
|
||
| ## Verification | ||
|
|
||
| Permanent tests cover astral Unicode code points, bidirectional multi-block logical order, U+FFFC leaf-node projection, combining-mark grapheme rejection, unavailable-segmenter failure, same-state atomicity under delayed hashing, frozen evidence, pre-editor null behavior, and absence of source text in ordinary evidence. The repository's exact 100% owned production coverage gate applies to the implementation. | ||
|
|
||
| ## References — APA 7th | ||
|
|
||
| Ecma International. (2026). *ECMA-402: ECMAScript 2026 internationalization API specification* (13th ed.). https://ecma-international.org/publications-and-standards/standards/ecma-402/ | ||
|
|
||
| ProseMirror. (n.d.). *ProseMirror reference manual*. Retrieved August 10, 2026, from https://prosemirror.net/docs/ref/ | ||
|
|
||
| World Wide Web Consortium. (2017, February 23). *Web Annotation Data Model*. https://www.w3.org/TR/annotation-model/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| import assert from 'node:assert/strict'; | ||
| import { execFileSync } from 'node:child_process'; | ||
| import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; | ||
| import { dirname, join, resolve } from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import { createRequire } from 'node:module'; | ||
|
|
||
| const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); | ||
| const packageJson = JSON.parse( | ||
| readFileSync(join(repositoryRoot, 'package.json'), 'utf8'), | ||
| ); | ||
| const packageName = packageJson.name; | ||
| const verificationDirectory = mkdtempSync( | ||
| join(repositoryRoot, '.text-position-selector-verification-'), | ||
| ); | ||
|
|
||
| /** Execute one strict package-consumer verification command. */ | ||
| function run(command, argumentsList) { | ||
| return execFileSync(command, argumentsList, { | ||
| cwd: repositoryRoot, | ||
| encoding: 'utf8', | ||
| stdio: ['ignore', 'pipe', 'inherit'], | ||
| }); | ||
| } | ||
|
|
||
| try { | ||
| const esmPackage = await import(packageName); | ||
| assert.equal( | ||
| esmPackage.TEXT_POSITION_PROJECTION_ID, | ||
| 'inkspan-prosemirror-text', | ||
| ); | ||
| assert.equal(esmPackage.TEXT_POSITION_PROJECTION_VERSION, 1); | ||
| assert.equal(typeof esmPackage.TextPositionSelectorEvidenceError, 'function'); | ||
| assert.equal(typeof esmPackage.createTextPositionSelector, 'function'); | ||
|
|
||
| const require = createRequire(import.meta.url); | ||
| const commonJsPackage = require(packageName); | ||
| assert.equal( | ||
| commonJsPackage.TEXT_POSITION_PROJECTION_ID, | ||
| 'inkspan-prosemirror-text', | ||
| ); | ||
| assert.equal(commonJsPackage.TEXT_POSITION_PROJECTION_VERSION, 1); | ||
| assert.equal( | ||
| typeof commonJsPackage.TextPositionSelectorEvidenceError, | ||
| 'function', | ||
| ); | ||
| assert.equal(typeof commonJsPackage.createTextPositionSelector, 'function'); | ||
|
|
||
| const consumerPath = join(verificationDirectory, 'consumer.ts'); | ||
| writeFileSync( | ||
| consumerPath, | ||
| `import { | ||
| TEXT_POSITION_PROJECTION_ID, | ||
| TEXT_POSITION_PROJECTION_VERSION, | ||
| TextPositionSelectorEvidenceError, | ||
| type CwlEditorHandle, | ||
| type CwlEditorTextPositionSelectorEvidence, | ||
| type CwlEditorTextProjectionIdentity, | ||
| type TextPositionSelectorEvidenceErrorCode, | ||
| } from '${packageName}'; | ||
|
|
||
| declare const handle: CwlEditorHandle; | ||
| const captured: Promise<CwlEditorTextPositionSelectorEvidence | null> = | ||
| handle.getTextPositionSelectorEvidence(); | ||
| const projection: CwlEditorTextProjectionIdentity = { | ||
| id: TEXT_POSITION_PROJECTION_ID, | ||
| version: TEXT_POSITION_PROJECTION_VERSION, | ||
| }; | ||
| const failureCode: TextPositionSelectorEvidenceErrorCode = | ||
| 'segmenter_unavailable'; | ||
| const failure = new TextPositionSelectorEvidenceError(failureCode); | ||
| const checked: Promise<void> = captured.then((evidence) => { | ||
| if (evidence === null) return; | ||
| const start: number = evidence.selector.start; | ||
| const end: number = evidence.selector.end; | ||
| const tag: string = evidence.revision.strongEntityTag; | ||
| void [start, end, tag, projection]; | ||
| }); | ||
| void [failure.code, checked]; | ||
| `, | ||
| 'utf8', | ||
| ); | ||
|
|
||
| run('pnpm', [ | ||
| 'exec', | ||
| 'tsc', | ||
| '--noEmit', | ||
| '--strict', | ||
| '--skipLibCheck', | ||
| 'false', | ||
| '--module', | ||
| 'NodeNext', | ||
| '--moduleResolution', | ||
| 'NodeNext', | ||
| '--target', | ||
| 'ES2022', | ||
| '--lib', | ||
| 'ES2022,DOM,DOM.Iterable', | ||
| consumerPath, | ||
| ]); | ||
|
|
||
| console.log( | ||
| `Verified ${packageName}: W3C text-position selector ESM, CommonJS, and strict TypeScript consumer contracts.`, | ||
| ); | ||
| } finally { | ||
| rmSync(verificationDirectory, { recursive: true, force: true }); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.