From 1ac76a417710555af70f74ce8f43c68f3019884c Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 22 Oct 2026 19:44:51 -0500 Subject: [PATCH] docs: import landed W4 baseline for chapter policy revision --- docs/plans/2026-10-22_q195-docx-citations.md | 981 +++++++++++++++++++ 1 file changed, 981 insertions(+) create mode 100644 docs/plans/2026-10-22_q195-docx-citations.md diff --git a/docs/plans/2026-10-22_q195-docx-citations.md b/docs/plans/2026-10-22_q195-docx-citations.md new file mode 100644 index 000000000..2af460ab7 --- /dev/null +++ b/docs/plans/2026-10-22_q195-docx-citations.md @@ -0,0 +1,981 @@ +# Q195 O2 W4: committed DOCX citations and access + +Design for Gitea #1799, assignment sage-3060/mos-1360. Owner Rocko; +independent reviewer Megavolt. Normative parent: `e36bce352`, +`docs/plans/2026-10-21_q195-writing-editor.md`, blob +`322ec50f2669cffc0f8256d6beffdaccc4d8dba8`, D12, D24-D26, W4 and W4b. +This is a proposed implementation contract, not access enablement or a parser +security PASS. Reviewer owns the separate verdict file; the author does not +write an approval there. + +## 1. Gates and compatibility + +W4b implements this contract only after independent W4 APPROVE and Sage's +accepted implementation brief, S0 and its other parent gates. A release must +ship extractor, resolver, authorization and sharing together; no partial +agent read rollout. Missing server readiness refuses every O2 read, resolve, +share and flag. A human setting, pin or request field cannot assert readiness. +W5 owns flags, not chapter writes. No agent execute, edit, replace, save, +tracked-change or comment-writing operation is introduced. + +Q36 / Vikunja 562 is OPEN. Production Markdown adoption remains closed under +mos-1358, independently of whether this design is approved. If Jason approves +superseded notices, a separate C2 reader/reply row must implement and pass +them before adoption; otherwise Mos records Jason's disposition before any +adoption brief proceeds. This document neither implements nor decides notices. + +Existing C2 Markdown, its line-cite schema, library grants and supporting-doc +writer remain unchanged. An authorized old Markdown original is historical +source, never the current DOCX or a permission to its later edits. No DOCX +paragraph is called a line. Following an adopted source from a C2 read, +indexing it as Markdown, changing C2 replies or a synchronized Markdown +projection requires a separately approved Case 2 amendment through Mos/Jason. +W4b must test old C2 fixtures unchanged alongside the new discriminated DOCX +contract. It may migrate only new O2 records, with a named schema edge Sage +allocates; it must not reinterpret existing line citations in place. + +## 2. Trust and document identity + +The host reader alone opens `/writing///`. +Business, project and document are opaque IDs resolved through the host's +owned manifest, never request paths. Each document has a stable ID and one +canonical DOCX path; a rename does not change its identity. A copy/new import +gets a new document ID and no inherited grant, share, pin or cache authority. +Origin metadata is evidence only. Copying a source does not launder access. + +A read snapshots one reachable committed manifest and its DOCX blob from the +same commit. It never combines HEAD's manifest with another commit's DOCX. +The reader accepts only server-resolved commits reachable from that document's +published history; arbitrary git objects and another document's same blob +are not selectable. Historical access also requires the document's current +live authorization. Removed history, missing blobs or missing extractor +revisions refuse; there is no worktree, export or engine-memory fallback. +The commit/blob field is a locator, not an authority token. + +The project's sole publication ref is `refs/heads/writing-published`; only +the human writer advances it, atomically and without force updates. Each +commit contains the book manifest. For a document, its publication commits +are the first-parent commits on that ref that add or change its DOCX blob or +its canonical manifest entry (including deletion/reinstatement). The first +commit is a publication; a sibling-only save is not. `currentCommit` is the +latest such commit for this document at the captured ref tip. Version latest +selects that commit; a locator commit must be one of those publication commits, +not an arbitrary intervening project commit. Deleted document entries refuse +current reads, including history, until explicitly reinstated and granted. +Published history is exactly that document's reachable publication sequence. +A sibling save does not change its currentCommit, stale flag or cursor. +Staleness is still exact commit inequality within that document, even if a +human deliberately saves an earlier byte-identical blob as a new publication. + +The writer's successful commit publishes atomically before Saved. A latest +lookup begun after that receipt resolves that published commit, not an old +cache entry. A lookup already in progress remains internally on its captured +commit. Return the captured version explicitly; never label it latest after +observing a newer publish. Recheck live grants immediately before returning +content, not just before asynchronous extraction. + +## 3. Exact wire schema, version 1 + +All objects reject unknown keys. IDs are nonempty ASCII `[A-Za-z0-9_-]{1,128}`; +commits/blobs are lowercase 40-hex Git object IDs in the repository's current +SHA-1 object format. A future object format requires a new schema revision. +Digest values are lowercase 64-hex SHA-256. Integers are safe nonnegative +JSON integers. No floats, coercion, duplicate JSON keys or implicit defaults +except the explicitly listed request defaults below. These are proposed O2-only records. + +A locator is exactly: + +```json +{ + "kind": "docx-range", "version": 1, + "business": "acme", "project": "book", "document": "chapter_1", + "commit": "<40 lowercase hex>", "blob": "<40 lowercase hex>", + "extractor": {"id": "mosaic-docx-text-v1", "revision": "<64 lowercase hex>"}, + "part": {"class": "body", "name": "word/document.xml", "story": "main"}, + "paragraph": {"ordinal": 0, "xmlId": null}, + "start": 0, "end": 5, "textView": "final", "quote": "Hello", + "quoteDigest": "" +} +``` + +Part triples are exact: body/main uses the body part; footnote/endnote/comment +uses its class-matching part and a canonical nonnegative decimal story ID +(`0` or `[1-9][0-9]*`), never a user path. Paragraph `xmlId` is JSON null +when absent. A textView is always recorded in every locator; there is no +resolver default. quote is a nonempty valid Unicode string bounded to 16,384 +code points and 64 KiB UTF-8. Unknown enum values or unexpected types refuse. + +The ordinal-primary rule below deliberately refines D25's wording ("IDs +when present, otherwise ordinal"): a possibly duplicated paraId is a required +cross-check, never the lookup key. This is an O2-only design choice for Sage's +implementation brief, not an amendment to Case 2. + +`paragraph.ordinal` is the zero-based paragraph index within the selected +story, in deterministic XML document order, including empty paragraphs and +table-cell paragraphs. `xmlId` is null exactly when the selected node has no w14:paraId, otherwise the literal eight-hex `w14:paraId`, +canonicalized uppercase. Ordinal is always present: paraId alone is not +unique or stable enough. The key is mandatory: passing null for a node that has the attribute refuses. +The non-null value must equal that node's attribute; duplicated IDs do not +trigger a search, since ordinal selects the node. Mismatches refuse. +Thus identical paragraphs remain distinct. `start < end <= codePointLength` +selects one paragraph, end exclusive. A multi-paragraph selection has the exact wrapper below, not a bare array: + +```json +{ + "kind":"docx-selection", "version":1, + "business":"acme", "project":"book", "document":"chapter_1", + "commit":"<40 lowercase hex>", "blob":"<40 lowercase hex>", + "extractor":{"id":"mosaic-docx-text-v1","revision":"<64 lowercase hex>"}, + "part":{"class":"body","name":"word/document.xml","story":"main"}, + "textView":"final", + "ranges":[ + {"paragraph":{"ordinal":0,"xmlId":null},"start":1,"end":5,"quote":"ello","quoteDigest":"<64 lowercase hex>"}, + {"paragraph":{"ordinal":1,"xmlId":null},"start":0,"end":5,"quote":"World","quoteDigest":"<64 lowercase hex>"} + ] +} +``` + +It has 2..32 ranges, all sharing the wrapper's exact document, publication, +blob, extractor, view and part. Ordinals are consecutive and increasing. +First range ends at its paragraph length, last starts at zero; intermediate +ranges cover whole paragraphs. Each range verifies its own quote/digest; +no inserted newline is included in any digest. Empty intermediate paragraphs +are represented by start=end=0, quote="" and SHA-256 of empty UTF-8; the first +and last are nonempty. Total quote budget is 16,384 code points and 64 KiB +UTF-8 for the entire selection. A one-paragraph selection uses docx-range +only: there is one canonical representation. No auto-merging, cross-story +selection or clipping. Resolver's locator union and a range chip accept +either exact kind. An atomic selection fails entirely if any range fails; +a response never returns only the valid fragments. + +`extractor.revision` is the digest of a canonical build manifest containing +extractor source blob IDs, projection rules revision and locked parser/ZIP +library versions. The release pins that manifest and independently verifies +its digest. It is not caller-supplied executable code or a mutable version +label. Its canonical manifest is exactly `{manifestVersion:1,id,rulesRevision, +sources,dependencies,formattingProperties}`. id is mosaic-docx-text-v1, +rulesRevision is integer 1; sources is a path-sorted array of `{path,blob}` +for every extractor/transitive source input; dependencies is a name-sorted +array of `{name,version,integrity}` from the lock, including ZIP/XML parsers; +formattingProperties is a sorted unique array of expanded XML names. +Duplicate keys/entries refuse. Canonical bytes are compact JSON, UTF-8, +no BOM/newline, recursively lexicographic ASCII object-key order; strings +escape only quote/backslash and U+0000..001F (lowercase four-hex \u escapes), +with other scalar values literal. Arrays preserve the specified sorted order, +integers are decimal without leading zeroes. revision is SHA-256 of those +bytes. Independent tooling must reproduce it. Server readiness is derived +from the verified release artifact containing that manifest and its reviewed +formatting list, not any human-authored config field. +Resolver requires that exact available revision; changed extraction +rules create a new revision, never silently reinterpret an old locator. + +A paragraph extraction record is exactly: + +```json +{ + "part": {"class": "footnote", "name": "word/footnotes.xml", "story": "7"}, + "paragraph": {"ordinal": 0, "xmlId": null}, + "textView": "final", "text": "Note text", "length": 9, + "textDigest": "", + "anchors": [], "wikiTokens": [] +} +``` + +Anchors have the exact shape `{kind,story,offset}`; allowed +kinds are `footnote-reference`, `endnote-reference`, `comment-start`, +`comment-end`, `comment-reference`; story is a canonical decimal nonnegative XML +ID string (`0` or `[1-9][0-9]*`, the same as note/comment locator stories), +offset a code-point boundary in this record. They never grant +access to their target. wikiTokens have exactly `{start,end,literal,form,target,label,heading}`. +form is link/label/heading/embed; label and heading are null except in their +corresponding form. target is 1..256 code points without `[`, `]`, `|`, `#`, +control characters or newline; a label/heading is 1..256 code points without +brackets, `|`, `#`, controls or newline. literal is respectively `[[target]]`, +`[[target|label]]`, `[[target#heading]]` or `![[target]]`. start/end are exact +paragraph code-point bounds, including the exclamation for embed. No nested +forms or combined label+heading/embed variants are interpreted in v1; they +remain text. Tokens are maximal nonoverlapping complete forms in left-to-right +order, with their literal equal to the text slice. They are not instructions +or external links. All text +and metadata are untrusted document data. Part/story IDs, excerpts and +comment content never enter system prompts as instructions. + +## 4. Story classes and package validation + +Version 1 accepts transitional WordprocessingML namespace +`http://schemas.openxmlformats.org/wordprocessingml/2006/main`, relationships +`http://schemas.openxmlformats.org/package/2006/relationships`, office +relationship types rooted at +`http://schemas.openxmlformats.org/officeDocument/2006/relationships/`, and +`w14:paraId` from `http://schemas.microsoft.com/office/word/2010/wordml`. +Prefixes are irrelevant; expanded namespace/local names decide. Strict OOXML +and unknown alternate-content choices refuse until a separately pinned +extractor supports them; never guess equivalent namespaces. + +| Class | Exact part names | Story selector | Agent exposure | +| --- | --- | --- | --- | +| body | word/document.xml | main | final by default, explicit grant | +| footnote | word/footnotes.xml | w:footnote/@w:id | own story plus owning document grant | +| endnote | word/endnotes.xml | w:endnote/@w:id | own story plus owning document grant | +| comment | word/comments.xml | w:comment/@w:id | only explicit comment scope | +| layout | word/headerN.xml, word/footerN.xml (N positive decimal) | none | denied in v1 | +| supporting | separately declared non-story document | separate identity | C2 tools/scopes only; no DOCX scope inheritance | +| opaque | media, styles, numbering, theme, fonts, custom XML, embedded objects | none | no raw bytes through O2 reader | + +Body/note/comment ownership comes solely from relationships in that committed +package, not filenames supplied by a request. A note/comment belongs to one +DOCX, never the book or an adjacent chapter. Classify reserved notes by +w:type in {separator, continuationSeparator, continuationNotice}, with any +canonical signed integer ID (`0` or `-?[1-9][0-9]*`, no plus/leading zeroes), +not by sign. A normal note has absent type or type normal and a canonical +nonnegative ID; unknown types refuse. Reserved notes are structural and never +readable stories. Any document reference to a reserved note refuses, including +an id-0 continuation separator; the reference must resolve to a normal note. Duplicate story IDs, dangling references or overlapping +ambiguous comment ownership refuse extraction. Unreferenced comment/note +stories are not agent-readable. Cross-paragraph comment anchors may be +recorded, but never concatenate their text with body text. + +The root `_rels/.rels` must contain exactly one internal officeDocument +relationship to `word/document.xml`. Its matching content type is +`application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml`. +Optional footnotes/endnotes/comments each have at most one matching internal +relationship and their matching `...wordprocessingml.{footnotes,endnotes,comments}+xml` +content type. Their relationship targets must be the exact part names above. +Reject normalized-path aliases, absolute paths, traversal, encoded separators, +backslashes, duplicate ZIP names, symlink entries, encryption and overlapping +ZIP data. Never extract ZIP entries onto the filesystem. + +Only internal relationships of types officeDocument, styles, numbering, +settings, webSettings, fontTable, theme, image, header, footer, footnotes, +endnotes and comments are accepted. Resolve internal targets relative to the +owning part within the package, reject escape and validate existence/type. +Reject every external relationship, hyperlink relationship, altChunk, +attachedTemplate, OLE, package embedding, VBA/macro content type or executable +part. Textual literal wiki links need no relationship. Unsupported rich +constructs refuse the agent projection, not the human save: the human can +retain a full document without it becoming readable to agents. + +Parse XML without DTDs, external entities, XInclude or network/file resolution. +Reject DOCTYPE/entity declarations. Bound before allocation: compressed blob +32 MiB, total expanded bytes 128 MiB, each entry 16 MiB, 2048 entries, +expansion ratio 100:1 both per entry and for the total (nonempty expanded data with zero compressed bytes refuses), XML depth 128, 1,000,000 nodes, 100,000 paragraphs, +1,000,000 code points per paragraph and 10,000,000 per document. Extraction +has a 5 s host deadline and bounded worker memory of 256 MiB. Exceeding any +bound yields `editor-document-unsupported`, no truncated success. These are +initial server safety ceilings, not grants or business quota settings; +changing them requires reviewed parser evidence. CRC/size mismatches refuse. + +## 5. Deterministic reading views + +`textView` enum is `final`, `original`, `markup`. Default requests use final. +Final is the accepted-reading projection, not an accept-changes operation. +A text run is visible in final iff no ancestor is w:del or w:moveFrom; in +original iff no ancestor is w:ins or w:moveTo. This ancestor veto applies to +nested revisions, so an insertion inside a deletion is absent in both final +and original. Markup includes both in source order with revision spans for +every revision container (nested spans may overlap). w:t and w:delText use +the same ancestor rule, never special substring searches. Revision IDs are +canonical nonnegative decimals; malformed or unmatched move ranges refuse. + +Paragraph-mark revisions under pPr/rPr mark only the paragraph boundary, +not the runs' text. A mark-only w:ins or w:del changes **no projected text** +in final or original: run content follows the ancestor rule alone. Although +accepting a mark deletion in Word joins paragraphs, this locator projection +never merges, splits or renumbers XML paragraphs in any view. It preserves +the text on both sides as separate ordinal records. In markup a paragraph +mark contributes a zero-width span at that paragraph's code-point length, +kind paragraph-mark-insert or paragraph-mark-delete with its revision ID; +it never becomes a whole-paragraph deletion span. A mark-only insertion's +text stays in original; a mark-only deletion's text stays in final. Inline +run revisions remain independent. No view edits or accepts/rejects changes. +Original/markup and comments require separate scopes, so final body access +never reveals hidden deletion/comment text. Revision author/date metadata +is omitted from agent responses in v1. + +Closed story/run leaf table (all names in w namespace unless stated): + +| Leaves | Text/anchor contribution | +| --- | --- | +| t, delText | decoded text subject to view/ancestor rule | +| tab | U+0009 | +| br, cr | U+000A, regardless of page/column type; layout kind does not affect offsets | +| noBreakHyphen | U+2011 | +| softHyphen | U+00AD | +| annotationRef | zero text; only allowed in comment story | +| separator, continuationSeparator, continuationNotice | structural only, allowed only inside reserved footnote/endnote stories; never projected | +| footnoteRef, endnoteRef | zero text; only in matching note story | +| footnoteReference, endnoteReference | zero text; matching anchor at current offset with canonical story ID | +| commentRangeStart, commentRangeEnd, commentReference | zero text; matching comment-start/end/reference anchor | +| moveFromRangeStart, moveFromRangeEnd, moveToRangeStart, moveToRangeEnd | zero text; validate paired IDs, do not duplicate revision text | +| bookmarkStart, bookmarkEnd, permStart, permEnd, proofErr, lastRenderedPageBreak | zero text; structural only, never access grants | +| sym, ptab | refuse requested story (font/position dependent) | +| fldChar, instrText, delInstrText, fldSimple | refuse requested story, including field results | +| drawing, pict, object, txbxContent, subDoc, altChunk | refuse requested story | + +Leaf validation covers **the entire selected XML part**, including all its +normal and reserved stories, before any requested story is projected. Reserved +separator/continuation notes are structurally validated against this same +closed vocabulary, with separator/continuationSeparator/continuationNotice +permitted only there, never a readable locator story. Each reserved ID/type +must be internally consistent; duplicate IDs within a part refuse. A separator +leaf in a normal note/body/comment refuses. A valid separator in a reserved +note does not make readable footnotes/endnotes unsupported. + +The table is exhaustive for story/run leaves; unknown leaves refuse, including +nontext leaves not explicitly named. Formatting properties belong only to +the separately closed manifest list. Anchors/range marks under invisible +revision ancestors are omitted in that view but still structurally validated. +Comment author data and ACL-like Word permission markers never supply Mosaic +authority. Unknown extension/AlternateContent text is unsupported, not ignored. + +Walk direct story paragraphs and table cells in XML order, including nested +tables in their containing cell order. Paragraph ordinals do not change with +view. Concatenate visible text across adjacent runs with no invented spaces. +Preserve decoded Unicode as stored: no NFC, case folding or whitespace +collapse. xml:space text remains +literal. Offsets count Unicode scalar values, not UTF-16 units or graphemes; +unpaired surrogates/invalid XML characters refuse. Field instructions, +hidden/vanished text and text boxes are not silently omitted: their presence +in a requested story makes v1 projection unsupported. Field results and +unknown semantic text-bearing elements likewise refuse until specified. +The projection traverses only these containers in the w namespace: +body, footnote, endnote, comment, tbl, tr, tc, p, r, ins, del, moveFrom, +moveTo, smartTag and sdtContent. A document root wraps body; note/comment +part roots wrap their corresponding stories. sdt may contain sdtPr (ignored +properties) and sdtContent only. Hyperlink containers are unsupported even +without a relationship. Unknown text-bearing containers refuse. Nontext +subtrees pPr, rPr, tblPr, tblGrid, trPr, tcPr and sdtPr may be ignored only +if they contain no story text elements, field codes, external references, +objects or unknown namespaces. Structural-leaf behavior is defined only by +the closed table above. w:sectPr is layout-only, with relationship targets +validated normally. +Formatting-property descendants are a closed W4b release artifact: its full +expanded-name list is included in the extractor manifest and independently +reviewed before enablement; any name absent from that list refuses. This +artifact can narrow the allowed property subtrees, never add a text-bearing +container to this v1 contract. Missing artifact is no readiness, not a +permissive descendant-text scraper. + +Reference marks contribute anchors at the current text boundary, not invented +note numbers or inline comment bodies. Each note/comment is separately paged. +A single range cannot straddle a view, paragraph or story; the selection wrapper +may cover contiguous paragraphs only under section 3. Extract literal +`[[target]]`, `[[target|label]]`, `[[target#heading]]` and `![[target]]` +after concatenating visible runs within one paragraph, so split-run tokens +survive formatting changes. Preserve complete literal bytes as UTF-8 after +XML decoding and code-point start/end; never follow OS paths or external URLs. +Targets resolve only via the host's owned-document resolver, with independent +authorization; unresolved targets stay literal and marked unresolved in human +UI. No link parsing grants reads. Nested/unclosed bracket sequences remain +plain text, and tokens cannot cross paragraph/view boundaries. + +## 6. Reading APIs, verification, paging and staleness + +These are proposed typed tool schemas over the existing runner socket, not +native WordCraft MCP. Caller/session/business come from the bound broker. +Agents cannot override them using object fields. + +- `editor_read_committed({project,document,version,textView,part,cursor,limit})`: + project, document, version and part are required; only textView, cursor and + limit may be omitted. version is exactly `{mode:"latest"}` or + `{mode:"commit",commit}`; part is + one class/name/story triple above; textView defaults final; cursor is null + on first page, limit 1..100 (default 20). No path, blob or draft argument. +- `editor_resolve_citation({locator})`: verify the exact immutable range; + it does not search for a nearby quote. Return exact quote and locator plus + `currentCommit` and `stale` after current authorization. +- `editor_flag` is W5's metadata-only write; W5 must validate the same + locator before storing a flag and may not write into the document. + +Read response is exactly `{version:1, business,project,document,commit,blob, +extractor,textView,part,rows,nextCursor,currentCommit,stale}`. Each row has the +extraction record above. The public envelope contains no absolute paths or +raw package attributes. `nextCursor` is null at end; `currentCommit` is the +current published document commit; stale is exactly commit != currentCommit. +A resolver response is exactly `{version:1,locator,currentCommit,stale}`: +the verified quote is the locator's quote, with no separate unverified text. +There is no default "first part" or automatic descent into notes/comments. The broker-bound business must equal +locator.business; a mismatch is editor-access-refused before object lookup. +Markup additionally requires `revisionSpans` on each +row: array of `{kind,id,start,end}`, kind insert/delete/move-from/move-to/ +paragraph-mark-insert/paragraph-mark-delete (mark spans have start=end), +with null-free canonical decimal IDs and valid boundaries; other views omit +that key. Limits cap output at 64 KiB UTF-8 JSON per response, including envelope and cursor. End a page before the first complete +row that would overflow, and return a cursor for that row. This is normal +paging, not partial-row success. If that first row cannot fit even on an +otherwise empty page, return editor-range-too-large with no text. Such a +paragraph is unsupported for agent reading in v1; no subrange read exists. +A human-visible "paragraph exceeds agent reading limit" reason appears only +after authorization. The human editor/save is unaffected. All empty paragraphs +still produce rows and advance the cursor. No page can return the same cursor +without progress. + +Cursor is a host-authenticated opaque token binding principal, session, +project/document, commit/blob, extractor, view, part, next ordinal, the complete +authority revision vector defined in section 7 and expiration (5 minutes). It carries no plaintext content. +Tampering, reuse by another session, wrong request parameters or a revoked +scope refuses. A latest cursor continues its captured snapshot with stale +metadata if a save intervenes; starting again with null cursor returns the +new publication. Cursor is not a capability and grants are rechecked every +page. Repeated ordinal ranges are stable within one committed projection. + +Resolver steps: authorize the current principal tuple, live scopes and exact/historical share (there is no separate undefined historical-view scope); load +that commit's manifest/blob; validate package; run the exact pinned extractor; +select exact part/story/ordinal and mandatory XML-ID value; select code-point range; +require literal quote equality and SHA-256(UTF-8 quote) equality. Require both +blob and commit membership, not digest alone. Any mismatch refuses +`editor-citation-invalid` without guessed text. After newer publication the +old valid locator returns `stale:true` with old text; it never auto-relocates +to identical paragraphs or another note. Jump opens that committed view with +an explicit old-version label. If current grant no longer allows it, even +old quote resolution refuses and the UI hides cached content. + +Errors: `editor-context-unavailable` for missing readiness; `editor-access-refused` +for absent ownership/grant/share (same response for nonexistent or foreign IDs); +`editor-document-unsupported` for parser limits or unsupported features after +authorization; `editor-version-unavailable` for authorized missing history or +extractor; `editor-citation-invalid` for locator/quote mismatch; +`editor-cursor-invalid` for invalid paging; `editor-range-too-large` for size. +The exact refusal envelope is `{ok:false,error:{code,message}}`; code is one +of those enum strings or editor-policy-conflict/editor-sharing-unsupported +(both 409, defined in section 7), and message is fixed server text for that code (no user +input). Both missing/foreign and ungranted IDs use the same code/message/status +403. Syntax errors before identity lookup use editor-citation-invalid, 400; +context unavailable is 503, cursor-invalid 400, version unavailable 409, +unsupported/range-too-large 422. Successful tools return `{ok:true,result}` +where result is the defined response; errors never include result/partial +rows. Return no source text, XML, path or untrusted parser error in refusals. + +## 7. Authority data, dedicated sessions and sharing + +### 7.1 Scope and principal records + +O2 context is permitted only in newly created **O2-dedicated conversations +and execution sessions**. Existing C2 threads/sessions cannot be converted, +attached to O2 context or resumed/forked into O2 mode; new IDs and a fresh +empty context are mandatory. There is no mixed-thread turn. The UI offers +"Open dedicated writing conversation", never silently changes an existing +chat's permissions. Existing C2-02 supporting-doc writes and C2-06 replies +retain their semantics. The narrowing below belongs only to this explicitly +opted-in O2 mode, closing review M1 without a Case 2 amendment. It changes +neither the Case 2 schema nor the meaning of its replies/line citations. + +Authorization principal is a tuple, not a seat's display name: +`{business,human,agentInstance,conversation,executionSession}`. human is the +stable authenticated identity that owns the O2 conversation; agentInstance +and executionSession come from its bound broker run. The request cannot +supply/replace them. All human readers and all agent lookups require live +R5 project/universe access plus the O2 grant below; role files grant the +operation only. The business owner is not an implicit reader of someone +else's private conversation. A viewer can read only their authorized human +view, never write a share/settings change or invoke an agent by sharing. +An owner/collaborator with current project write access may share within +their own readable O2 scope. Agent scope and sharing human scope intersect; +a human cannot share a part or view they lack. + +O2 scopes live in the new append-only bus table writing_access_events, not +access.json or role-file wildcard changes. Proposed human-only verb +`editor.access.set({project,document,subject,scope,expectedRevision})` takes +subject exactly `{kind:"human"|"agent",id}`; agent id is the business's +bus agent **instance ID**, equal to principal.agentInstance, never a role +definition ID or a seat display name. scope is exactly +`{enabled,parts,views,history,mayShare}`. parts is a sorted unique nonempty +subset of body/footnote/endnote/comment (all stories within the selected +class); views is a sorted unique nonempty subset final/original/markup; +history and mayShare are booleans. mayShare must be false for agent subjects. +enabled=false revokes all access; even its otherwise populated scope cannot +authorize. No wildcards or implicit note/comment inheritance. A comment +requires parts including comment and the requested view. history=true permits +older publications subject to a matching share; history=false permits only +the current publication. The document is always explicit. + +Only the existing business-owner human proof may write these policy grants, +through the existing trusted human CLI or authenticated write-enabled Console +transport, with the existing origin/session/proof checks. No new proof file, +caller or exemption. Not tester, agent, viewer or collaborator policy writes. +Sharing and scope are different: an authorized collaborator may send context +but cannot change grants. expectedRevision is the latest sequence for that +business/project/document/subject, zero when none. Compare, append and audit +are atomic; conflict returns editor-policy-conflict (409, fixed message). +Receipt is `{ok:true,result:{id,revision,previousRevision,scopeDigest}}`, where +revision is the appended seq, id is host-generated, and scopeDigest is SHA-256 +of canonical scope JSON using section 3 serialization. Missing grant denies. + +Each access decision records an authority vector exactly +`{restoreEpoch,humanAccess,agentAccess,projectAccess,threadAccess,sharingPolicy,share}`. +humanAccess and agentAccess are the respective latest writing_access_events +seq values; projectAccess is the existing R5 access revision; threadAccess +is the latest O2 conversation membership-event seq; sharingPolicy is the +latest writing_policy_events seq; share is the exact writing_shares seq. +restoreEpoch is the independent restore-fence epoch in section 7.5. +Missing inputs deny rather than zero-default except initial policy explicitly +materialized by the human writer as the default. A cursor binds every value; +any change invalidates it. Each new lookup reevaluates rights, including a +revocation between asynchronous extraction and response emission. + +### 7.2 One human per context; settings and chip requests + +An O2 conversation has exactly one owner human and one agent instance. No +second human joins it in v1: a thread-share attempt refuses +editor-sharing-unsupported (409), rather than borrowing the sender's grants +for someone else. A second human starts a separate O2 conversation, supplies +their own grant/share and gets no previous content. This is a new O2 mode, +not a restriction on existing C2 sharing. Human and agent must still have +matching business/project/universe rights. Read-only previews cannot send. + +Human-only `editor.sharing.set({conversation,mode,expectedRevision})` is +available only to that authenticated owner with current write access over +the same trusted write transports. mode is off/visible-chip-per-send/ +conversation-current. It atomically appends writing_policy_events and returns +`{ok:true,result:{id,revision,previousRevision,mode}}`. Default is the explicit +visible-chip-per-send policy on new conversation creation. Selecting off +immediately invalidates **all** prior shares, reads, resolves and cursors for +that conversation, not only new sends. Selecting another mode later does +not revive shares from the earlier policy revision. No pin changes a mode. + +In visible-chip-per-send the human sends exactly +`{conversation,message,expectedPolicyRevision,chip}` via the human send path. +message is human-authored free-chat text; W4b's bounded human API handles it, +never an agent-supplied impersonation. chip is null or one of: + +- `{kind:"range",document,baseCommit,selection}` where selection is one + docx-range/docx-selection locator, with its business/project/document/ + commit equal to the server-owned conversation/document and baseCommit. +- `{kind:"document",document,baseCommit,parts,views,history}` selecting a + whole-document **exact publication**, with explicit part/view lists and + history boolean constrained by both grants. + +Both chips require a server-issued human editor selection token as an +additional required `selectionToken` key. It binds authenticated login, +conversation, document, baseCommit, saved editor model revision, and either +exact locator or whole-document scope digest, expires in 2 minutes and is +single-use. The human editor adapter creates it only after exact mapping to +the committed projection. The request supplies no draft text or engine path +for mapping; mismatched/edited selection fails locally and server-side. +A stale base or document change before send refuses, requiring a fresh +visible chip; an old citation remains resolvable through its already-sent +share while authorized. Null chip sends no editor context. Before readiness, +the chip says unavailable and sends no document ID, title, path or selection. + +A range share permits only editor_resolve_citation for that exact locator +(or a range wholly contained within it with newly verified quote/digest). +It always refuses editor_read_committed, since rows would expose surrounding +text. A document exact share allows paging only its named publication, +parts/views; it never grants future saves. No implicit body-to-note/comment +or final-to-markup expansion occurs. + +conversation-current is an explicit human policy with a persistent visible +indicator and a required whole-document chip. The same chip fields bind +baseCommit and scope, but the share record has versionMode=latest: baseCommit +is the acknowledged publication at issuance, not a future-version ceiling. +The next authorized latest lookup after Saved follows currentCommit within +the granted part/view/history bounds. This exception to exact-version sharing +exists only in this explicit mode. Range chips are refused in this mode, +not silently upgraded. Pins and earlier exact shares never become latest. + +Latest shares expire on explicit policy revocation, owner grant loss, O2 +conversation close or **execution-session end/clear**. This means the new +O2 execution session, not the Console login, bus connection or a C2 thread +session. Relaunch/clear requires a fresh chip; a dropped socket/login does +not silently widen or prolong one. Exact shares may remain verifiable across +execution sessions through their same human conversation while live policy, +history permission and grants hold, but never supply replay context to a +new agent session without a new share. Their scope is citation verification +only after the issuing execution session ends. No cross-human context reuse. + +### 7.3 Stored share schema + +The immutable writing_shares payload is exactly +`{version,id,restoreEpoch,business,project,document,conversation,executionSession,message, +human,agentInstance,mode,versionMode,baseCommit,blob,extractor,scope,selection, +authority,policyRevision,issuedAt}`. version=1; IDs use section 3 grammar; +mode is the policy at issuance; versionMode is exact/latest; baseCommit/blob +are the acknowledged snapshot and extractor is its exact pinned revision. +scope is `{parts,views,history}` as above; selection is the verified locator +for a range, otherwise null. authority is the complete revision vector at +issuance; its share field equals this row's seq (assigned atomically). +restoreEpoch must equal authority.restoreEpoch and the live independent +fence. issuedAt is canonical UTC millisecond ISO timestamp. policyRevision equals +authority.sharingPolicy. message names a new writing_messages row, not a C2 +message. A unique `(conversation,message,document)` constraint and operation +idempotency key prevent duplicate grants on retry. No user-provided human, +agentInstance, authority, issuedAt, blob or share id is accepted. +A scope decrease/revocation denies even historical shares; an unrelated new +policy revision also invalidates prior policy-bound shares. A human who +needs them again explicitly reshares. Missing session/share is no context. + +### 7.4 Dedicated storage and every output surface + +Every turn receiving O2 context runs with the new O2-only allowlist of +read/resolve/flag plus its dedicated conversation reply. No generic file, +shell, git, supporting-doc writer or arbitrary outgoing tool is available. +This narrows only new O2 sessions, never C2 turns. All agent output from such +a turn is conservatively O2-derived; it stays in the dedicated conversation +with the union of the document provenance/scopes. No semantic quote detector +or agent self-labelling supplies this boundary. Human text and titles in that +conversation use the same dedicated storage, so a human pasting an O2 passage +cannot accidentally route it into an old renderer. O2 replies never enter +C2-06 processing. An O2 flag is metadata stored by W5, not a story edit. + +| Existing surface | Required O2 behavior | +| --- | --- | +| threads///session and claude-projects | no O2 content, prompt, model transcript, title or derived reply | +| threads///sessions//session and claude-projects | same; O2 mode never binds these native stores | +| session-workspaces/// | no O2 text or exports; no store mount | +| launches// including bundle, turns, runner.log and session | opaque IDs/status only, never O2 prompts/output; host transient O2 adapter instead | +| bus events, messages, message_threads and existing sessions rows | no O2 text/titles/quotes; only fixed readiness/status audit with opaque IDs if needed | +| Console legacy thread view | no O2 rows; dedicated writing view reauthorizes every read | +| CLI thread, talk, follow and raw-session tools | no O2 content; unsupported mode refuses, not generic rendering | +| Discord journal.mjs/context.mjs and control-board Discord delivery | no O2 conversation subscriptions/context/output; fixed unavailable notice only | +| /runs write-once evidence | never O2 text, prompts, titles, excerpts, stdout/stderr or derived output; only opaque run status | +| bus-host/sessions.json, launch logs and activity projections | IDs/liveness only, no document or conversation title | + +Host-only `/writing-context/` (0700 owner, no generic mount/remote) +holds the immutable dedicated message/share objects defined below, transient +O2 adapter material and any unavoidable encrypted native transcript. Object +files are regular 0600 owner files, no symlink/hardlink aliases. This is an +O2-classified content store; it is never included in the bus release backup, +post-attempt store, stack-copy, rehearse input or generic backup. Revocation +changes permission at read time, never rewrites a write-once run record. It is keyed to O2 session, not an alias/symlink into seat stores. +Release readiness requires an independently tested adapter that prevents +plaintext native persistence/logging outside this boundary, including crash +and timeout paths. If either Pi or Claude cannot meet it, that harness's O2 +tools stay off; do not expose the session then attempt post-hoc redaction of +write-once run records. Readiness is per adapter. No O2 text enters immutable +legacy evidence, even briefly. Stored dedicated replies are authorized afresh +at read time; live grant loss hides cached UI content and prevents prompt +replay/fork. Revocation cannot erase already delivered knowledge and the UI +must not promise that it can. + +O2-derived exports, extraction indexes, caches, previews, source copies and +git history retain document classification outside generic roots and C2 +library mappings. O2 repositories have **no git remotes**. No push, mirror, +fork, plaintext backup or remote replication of an O2 repository is allowed, +including through human credentials; no seat Gitea account can discover, +list, fork or clone one. Backup transport is out of scope and disabled, not +an implied exception. Stack-copy/release copies source code and extractor +artifacts only, never writing/, writing-context/, writing-control/, O2 content +objects, exports or repository object directories. The separate release bus +backup retains metadata rows only, under the private-store rules below; +it is not a source copy or permission to read context. No raw O2 Gitea grant may be minted. +Validate absence of remotes at store creation, before save/read and activation; +a new remote or object alternate disables O2, rather than trusting a read-only +mount. Symlinks, hardlinks, alternate worktrees and parent mounts must not +expose it to generic tools. Native engine APIs remain human-only. + +### 7.5 Named migration and rollback contract + +W4b adds the following new STRICT append-only bus tables (seq INTEGER primary +key, at TEXT canonical timestamp, id TEXT unique, all other fields NOT NULL +unless stated). Each row also has restore_epoch TEXT NOT NULL, equal to the +independent current fence epoch at write time. Every update/delete/replace +refuses; latest seq **within that epoch** is state. +JSON columns contain the exact canonical schema named above, not arbitrary +unvalidated JSON. No existing C2 messages/events/sessions column changes. + +| Table | Additional columns and key | +| --- | --- | +| writing_access_events | business TEXT, project TEXT, document TEXT, subject_kind TEXT (human/agent), subject_id TEXT, by_human TEXT, previous_revision INTEGER, scope_json TEXT, operation_id TEXT UNIQUE; latest per document/subject | +| writing_conversations | business TEXT, project TEXT, owner_human TEXT, agent_instance TEXT; one creation row per id, private O2 identity only | +| writing_conversation_events | conversation TEXT FK, op TEXT (close), by_human TEXT; latest membership/close revision, creation seq is initial threadAccess | +| writing_execution_sessions | conversation TEXT FK, execution_session TEXT UNIQUE, adapter_revision TEXT; no native transcript path | +| writing_session_events | execution_session TEXT FK, op TEXT (end/clear), by_principal TEXT; terminal event invalidates latest shares | +| writing_policy_events | conversation TEXT FK, mode TEXT, by_human TEXT, previous_revision INTEGER, operation_id TEXT UNIQUE | +| writing_messages | conversation TEXT FK, execution_session TEXT FK nullable for human-only messages, from_kind TEXT (human/agent), from_id TEXT, content_ref TEXT, content_digest TEXT, operation_id TEXT UNIQUE | +| writing_shares | conversation TEXT FK, message TEXT FK, document TEXT, payload_ref TEXT, payload_digest TEXT, operation_id TEXT UNIQUE; UNIQUE(conversation,message,document) | + +Each table also has business/project columns if not otherwise listed; FK and +application checks require agreement, not just ID existence. The canonical message content object is exactly `{version:1,business,project,row,nonce,body,provenance}`: +row is the owning writing_messages row ID; nonce is fresh cryptographically +random 32-byte lowercase hex, stored only inside the private object, not its +metadata backup. body is +the complete human/agent message string; provenance is an array of `{document,share,commit,blob,extractor}`, +sorted by document/share; every entry requires live scopes. The share content +object is exactly `{version:1,business,project,row,nonce,payload}`, where row +is its writing_shares row ID, nonce has the same fresh/private rule, and +payload is the full section 7.3 record including any quoted selection. +Neither message bodies nor share payloads/quotes enter bus.sqlite, WAL, +events, SQL error logs or backup manifests. Human messages with no O2 +attachment have provenance=[] but use the same private content store. +content_ref/payload_ref are opaque 64-hex object names, not paths; their +corresponding digest equals SHA-256 of the exact canonical UTF-8 object. +Objects reside at writing-context/objects///.json; +row/business/project must match both reference metadata and the authorized +caller. No cross-business or cross-row deduplication; retention is per owning +row and cannot delete another business's object. The private random nonce +prevents a metadata digest from confirming a guessed short body. No remote or +release-backup copy. Write/flush the immutable object before committing its +bus reference. A failed transaction leaves only an inaccessible orphan, +never a visible partial message/share; a missing or mismatched object refuses. +Object reads always require current authority, not knowledge of a digest. +No +existing renderer queries these new tables; generic bus recordEvent and +recordTask cannot write their rows. Access/policy/shares use only the trusted +handlers and atomic transactions. Grant seq values are table-specific, with +kind-qualified revision vector fields; never compare unrelated seq counters. + +Sage allocates the schema edge; migration creates these empty tables and +triggers, with no backfill/conversion of C2 citations, messages or raw sessions. +Production adoption stays refused. The writer registers new documents without +creating grants; the owner explicitly sets both human/agent document scopes. +O2 session creation and its default policy are one atomic operation behind +owner write proof; receipt `{id,executionSession,policyRevision}` contains no +content. No legacy thread/session can be attached by guessed ID. + +Old release rollback must not render O2 data. The new store schema version +normally makes old binaries refuse opening it, but security does not depend +on that: even a restored pre-W4b database reader can see only its unchanged +legacy tables with no O2 content. O2 messages/share/provenance remain in new +tables, never a tagged body in existing messages. Activating a pre-W4b copy +disables O2 context, never converts dedicated conversations to C2 or exports +transcripts for compatibility. O2 repositories/stores remain outside copies. +A rollback probe uses recognizable secret fixture text in O2-only content objects, +then invokes old Console/CLI/Discord/raw-session renderers: none may emit it, +its title, quote or prompt. No live rollback is part of acceptance testing. + +#### Release backup, restore counting and independent revocation fence + +The existing release behavior must be explicitly accounted for: at this base +`scripts/stack-release.sh:474-493` copies bus.sqlite into +`/backups/stack//bus/`; restore_store at :498ff moves current +bus/ to `/bus.post-attempt-/`. rehearse reads backup +stores, and release never prunes them. These paths remain **O2-classified +metadata stores** even though content/quotes are outside SQLite. Directories +are 0700 owner and files (including SQLite/WAL/SHM if present) 0600 owner, +with ownership, symlink/hardlink and generic mount checks. No remote, mirror, +seat token access or generic export. rehearse may read metadata for validation +only and cannot load content objects, emit quotes or restore readiness. + +Release does not silently delete evidence. A separate explicit human/ops +retention action may prune a stopped backup/post-attempt tree only after +checking it is not the selected recovery set or needed restore evidence, +recording its IDs/digests and a prune receipt. No automatic age-based prune, +no editing of retained metadata, no pruning runs/ under this policy. Until +that separately reviewed action exists, keep these private local metadata +trees indefinitely; it grants no content access. writing-context objects +have a distinct reviewed retention process and are never backed up by release. + +W4b owns the release guard changes, not a follow-up: all eight writing_* +tables above join COUNTED in scripts/stack-release.sh:420 and the T46 table +classification in packages/cli/tests/stack-release.test.mjs:913. They are +non-event writers, not classified as ignorable tables or double-counted as +generic events. Jason's existing exact --events N confirmation remains +required when the combined post-backup row count is nonzero. A restore plan +also compares writing row identities with the proposed source store; equal +aggregate counts cannot conceal a dropped revocation replaced by another row. +A schema with any unclassified writing_* table refuses restore readiness. + +An **independent host-private fence** lives outside bus/ and every backup: +`/writing-control/restore-fence.json`, directory 0700, regular file +0600, no links/mounts/remote/source-copy/rehearse access. Its exact schema is +`{version:1,epoch,restoreOperation,droppedRowsDigest,confirmedBusinesses}`. +epoch is a host-generated opaque ID; restoreOperation is null before the +first restore, otherwise a host-generated opaque operation ID; droppedRowsDigest +is SHA-256 of the canonical sorted dropped `(table,id)` list (empty allowed); +confirmedBusinesses is a sorted unique array of authenticated business IDs. +Only the trusted release controller and existing owner-proof handler write +it by durable atomic replacement; agents/testers/request paths cannot. +It is not a human-editable readiness setting: verified release readiness is +still required, and this file is an additional independent deny fence. + +**First creation:** the compatible release controller durably creates the +fence as part of W4b's first schema migration, before committing those empty +tables or enabling any handler. It uses a fresh epoch, restoreOperation=null, +droppedRowsDigest=SHA-256 of the canonical UTF-8 empty JSON array `[]`, and +confirmedBusinesses=[]. Every initial writing row uses that epoch. A normal +never-restored store does **not** require editor.restore.confirm: the +confirmation gate applies only when restoreOperation is non-null. Initial +live grants/shares and verified artifact readiness are still mandatory. + +Initialization is allowed only when the pre-migration schema has no writing_* +tables/history and there is no existing fence. If a crash leaves a durable +initial fence but the schema transaction uncommitted, migration resumes with +that same epoch, not another initial state. If writing tables/history exist +and the fence is missing/malformed, fail closed; never repair by creating a +new initial fence. Creating an initial fence is not a way around a previous +restore or revocation. W4b acceptance exercises both migration crash orders. + +Before **any** store swap/restore (not just one with nonzero counts), the +current compatible controller durably creates a fresh epoch, records dropped +writing rows and clears confirmedBusinesses. If fencing fails, restore +refuses before changing bus/. A crash anywhere keeps O2 denied. Old snapshot +rows have old restore_epoch; they cannot authorize even if their grant, +policy or share says enabled. Automatic failure recovery and rehearse-driven +restore use the same fence. Normal code rollback without store restore +keeps the fence and leaves O2 off on incompatible code. A pre-W4b controller +is not allowed to restore an O2-enabled data root: the active controller's +capability preflight refuses it; operations must use the compatible restore +controller even when the target runtime is old. + +After restore, the verified W4b runtime remains unavailable for O2 in every +unconfirmed business. New human-only +`editor.restore.confirm({expectedEpoch})`, under the business owner's existing +trusted human proof, acknowledges that business in the independent fence +and returns `{ok:true,result:{business,epoch,confirmed:true}}`. It restores +**no old grant/share/policy/conversation**. Fresh current-epoch grants, +a fresh dedicated conversation/default policy and fresh human chips must +be created explicitly. Past rows may be inspected by an authorized recovery +operator as metadata, but never used as current authority or prompt replay. +Every read/resolve/cursor/share checks matching restore_epoch and, when +restoreOperation is non-null, that business's explicit confirmation; old object bytes remaining on disk are not capabilities. +Thus even owner confirmation cannot revive a previously revoked old share. +Missing/malformed fence state denies O2. This current-epoch file does not +claim to detect a privileged manual rollback of both fence and database; +the supported restore protocol never restores the fence from a backup and +always rotates it first. A same-uid operator bypassing that protocol is not +a boundary this file can prove. --events N approves a +store operation only and is never a substitute for this recovery protocol. + +Acceptance seeds a secret in a content object and a share, takes a backup, +revokes access/sharing, then restores the backup with explicit row-loss +approval: the old share stays refused before and after owner confirmation; +a new grant/conversation/chip is needed. Sweep bus.sqlite, WAL/SHM, every +backups/stack and bus.post-attempt-* tree, rehearse output and release logs: +no secret/title/quote/body marker may appear. Only the explicitly named +private writing-context content object may contain it. Verify COUNTED/T46 +covers each table and simulate failure before/after each fence/store step. + +The XML/ZIP parser safety ceilings and absence-of-remote checks apply to +human store ingestion too where needed to prevent path escape and remote +replication; a projection-unsupported rich feature alone does not reject a +valid human save. Parser refusal codes have a fixed human-readable reason +category, never raw XML or a misleading empty context. Fields/hyperlinks/ +text boxes may make real chapters agent-unreadable in v1; the UI explicitly +says this and never claims an empty successful projection. + +Every extractor revision referenced by retained publication history or shares +is retained as a verified immutable host artifact. An activation missing one +refuses O2 readiness (no silent citation downgrade), including rollback to a +W4b-era release that drops a revision. A pre-W4b release can run with O2 wholly +off and cannot read those records. Retiring revisions requires a separately +reviewed retention/prune operation for all dependent O2 history/shares; not +an automatic stack-copy cleanup. Artifact caches are not authorization caches. + +## 8. Acceptance matrix and release evidence + +Each row needs a named fixture/test and a mutant or bypass probe in W4b's +packet. Independent fixtures construct OOXML directly and recompute Unicode +slices/digests outside the production extractor. Security checks fail closed; +no fixture-only readiness flag may exist in production APIs. + +| ID | Evidence required | Negative control / bypass | +| --- | --- | --- | +| A01 | Exact body quote independently recomputed from commit/blob/revision | change quote, digest, blob, commit or extractor; each refuses | +| A02 | Two identical paragraphs and notes have different locators | no content-search relocation; swapped ordinal/story refuses | +| A03 | XML ID absent/duplicate and ordinal behavior | missing/mismatched paraId cannot silently choose another node | +| A04 | Emoji, combining characters, tabs, breaks and split runs | UTF-16 offsets, normalization and whitespace collapse fail | +| A05 | Literal split-run wiki forms retained | no path/URL fetch; malformed/cross-paragraph tokens stay text | +| A06 | Final/original/markup independently verified with moves/deletions | final-only grant cannot read deletion/comment; no document rewrite | +| A07 | Body/note/endnote/comment exact ownership | orphan/dangling/cross-document references and duplicate IDs refuse | +| A08 | Package allowlist, aliases, CRC, XML/ZIP limits | external rel, macro, traversal, symlink, entity, ZIP bomb and timeout refuse | +| A09 | Commit snapshot and publication ordering | next latest after Saved gets new commit; no worktree/draft fallback | +| A10 | Historical resolve stale label | old quote stays old; removed grant/history refuses without text | +| A11 | Pagination complete/deterministic and bounded | tampered/foreign/revoked cursor and oversized row refuse | +| A12 | Cross-user/business/project/universe denial | guessed IDs, same blob elsewhere and copied document grant nothing | +| A13 | Visible range chip exact boundaries and default off before gate | no selection/title/path metadata before readiness; draft mapping refused | +| A14 | Every lookup intersects live grant and share | pin, old send, revoked scope and range-to-whole-document escalation fail | +| A15 | Whole-document latest share explicit; save rechecked | exact share cannot reveal future edits; persistent policy visible | +| A16 | Generic file/search/git/export/index paths denied | source copy, symlink, hardlink, alternate object root and new O2 copy probes | +| A17 | Raw session, fork/resume and transcript boundaries | revoked context cannot replay via raw tool or harness history | +| A18 | Agent tools exactly read/resolve/flag after gates | native MCP, execute, save, replacement and story comment writes refuse | +| A19 | Existing C2 Markdown reads/line cites unchanged | no DOCX-as-lines conversion or redirect following adoption | +| A20 | Adoption gate before/after readiness and Q36 disposition | absent/revoked server readiness and required notice readiness refuse | +| A21 | Human full-feature save remains separate | unsupported projection yields no agent context, never lossy human save | +| A22 | Scope revocation during extraction | final pre-emission check prevents async stale-authority leak | +| A23 | Sibling chapter save | currentCommit/stale/cursor for untouched document remain unchanged | +| A24 | Full page and oversized paragraph | end before overflow; first oversized row refuses without text; no unreachable subrange claim | +| A25 | No O2 git remote or release copy | seat Gitea list/fork/clone fails; configured remote/object alternate disables O2; copies exclude stores | +| A26 | Every section 7.4 store/renderer | secret marker absent in each named legacy store, raw session, CLI/Discord/Console view and runs/ | +| A27 | Pre-W4b rollback reader | new O2 rows never rendered through legacy tables; missing retained extractor disables O2 | +| A28 | Dedicated mode vs C2 | mixed attachment/resume/fork denied; unchanged C2 supporting writes/replies fixtures pass | +| A29 | Two humans/viewer/grant revisions | viewer cannot share; second human cannot join or reuse context; all cursor authority fields checked | +| A30 | Canonical schemas and complete selections | unknown keys, inconsistent ranges, xmlId null mismatch and noncanonical story IDs refuse | +| A31 | Closed leaf/revision table | every leaf/nested rule; mark-only deletion keeps text in final, mark-only insertion keeps text in original; markup mark spans zero-width; Word -1 separator/0 continuation-separator and older 0/1 reserved layouts validate, references to them refuse | +| A32 | Initial migration and revocation/restore | initial null-operation fence needs no restore confirmation but no grant is implicit; non-null restore fence needs confirmation and fresh authority; all tables counted/classified; old share stays refused; crash/backup-marker sweeps | + +Implementation acceptance includes writing, bus, harness, business, WebUI, +seat mount/import closure and stack-copy/release fixtures; explicit production +adoption refusal/lift tests; independent parser/policy/tool review; G1 and +Sage's allocated schema edge. Evidence must name skips and refuse enablement +when an acceptance guard was not exercised. W4's review alone lifts nothing. + +## 9. Handoff and unresolved decisions + +Megavolt reviews this separate document and records a verdict in +`docs/reviews/2026-10-22_q195-docx-citations.md`. Revisions repeat direct +commit/blob handoff to Megavolt and Sage. Q36 remains with Jason through Mos; +no adoption, C2 reader/reply notice change or schema reinterpretation ships +on this document. W4b's brief must allocate ownership for every surface in +A16/A17 and section 7.4 before code: an unowned enforcement path is a blocker, not a deferred security +note. No live files, editor source imports, parser execution or tests were +used to write this design. + +## Round 2 changes against review 08fcb8c1c + +B1: exact selection wrapper, canonical paragraph identity, wiki/anchor schema, +chip/share and refusal records; ordinal-primary D25 refinement explicit. +B2: closed leaves and nested/paragraph-mark revision rules. B3: per-document +publication sequence and sibling-save acceptance. B4/B5: concrete append-only +authority, human proof, exact/latest share policy, revision vector and +single-human dedicated session lifetime. B6: complete-row paging; oversized +paragraph unsupported, no circular subrange API. B7: no remotes/backup push, +seat token and copy probes. B8: named native stores, renderers and immutable +run evidence, with O2-only storage/adapter readiness. B9: tables/columns, +rollback separation and extractor retention. M1: dedicated O2 conversations +only; mixed C2 context refused, no C2 requirement change. N1-N7 addressed in +canonical manifest, bound-business check, ratio/default wording, UI unavailable +reason, release-derived readiness and new A23-A31 acceptance rows. These are +proposed definitions, not passing implementation tests. Q36/adoption unchanged. + +## Round 3 changes against review 0d627a2b3 + +R2-B1: paragraph-mark revisions preserve all run text; markup has a zero-width +mark span, never a fabricated whole-paragraph deletion. Reserved note leaves +are allowed only in reserved stories; whole-part structural validation is +explicit. A31 covers both mark-only cases and reserved notes. +R2-B2: bodies/quoted share payloads move out of SQLite into private content +objects; backup/post-attempt metadata paths are named with access/retention +rules. Every writing_* table is COUNTED/T46-classified. A non-restored deny +fence changes epoch before any store swap; owner confirmation cannot revive +old-epoch grants/shares. A32 tests revocation/restore and secret-marker sweeps. +R2-N1: agent grant subject means agentInstance. R2-N2: structural leaves have +one table. This remains design only; no code, restore, parser run or test. + +## Round 4 changes against review 9fb999abf + +R3-B1: reserved-note classification is by type with any canonical signed ID, +not sign; normal notes require nonnegative IDs, and references to reserved +notes refuse. A31 covers both actual Word layouts. R3-B2: first migration +creates the durable initial epoch before schema commit; null restoreOperation +needs no recovery confirmation, a restored epoch does. Missing established +fences never reinitialize. A32 covers first enable and post-restore states. +R3-N1: business/row/private random nonce are part of each content object and +its path, so no cross-business deduplication or guessed-body digest oracle. +R3-N2: removed the unsupported claim to detect privileged manual rollback +of the fence itself. R3-N3: restore_store anchor corrected to :498. + +### Public Word fixture confirmation (not an extractor test) + +Inspected only public Microsoft-maintained test packages in memory using +Python ZIP/XML readers, not the O2 parser or a live/user document. No binary +was imported into this repository. Source: +[OfficeDev/Open-Xml-PowerTools FootNote.docx](https://github.com/OfficeDev/Open-Xml-PowerTools/blob/a9187c5ec4199106562ec91c5ffd9f355251253b/NewDocxDocuments/FootNote.docx), +14,214 bytes; DOCX SHA-256 +`40e2de78f0f70d8acf3db734df13f107c92986f1a40865cdf9772a8b55ab5e20`. +Its app properties identify Microsoft Office Word, version 15.0000. +footnotes.xml has separator=-1, continuationSeparator=0, normal note=1; +endnotes.xml has separator=-1 and continuationSeparator=0. +footnotes.xml SHA-256 +`1da08d9c0a4a1af2706c035dfb176defd179686f81418134b796d49649e850a0`; +endnotes.xml SHA-256 +`f83e7f1c9f91b44db26b147b22ca615a13488bc6a8084febb392ee00a6ca4f5e`. +This confirms the reviewer's -1/0 example in a Word-identified fixture. + +An older independent Microsoft SDK fixture confirms why IDs cannot define +the class: +[Open-XML-SDK ForTestCase/footnote.docx](https://github.com/dotnet/Open-XML-SDK/blob/431ab05cf160248cc3885a4a766026d4f8243792/test/DocumentFormat.OpenXml.Tests.Assets/assets/TestDataStorage/v2FxTestFiles/ForTestCase/footnote.docx), +69,706 bytes, SHA-256 +`26dc62a5d00e2c3c1df60e0d5bf1cac43b142a090ea595b9ad11ce73e3dab268`. +It identifies Microsoft Office Word 12.0000 and uses reserved IDs 0/1 in +both parts, with normal footnotes 2/3. No claim is made that the whole +packages pass v1's otherwise deliberately restrictive projection rules.