Compare commits

...
Author SHA1 Message Date
jason.woltje 67f82a12d5 Q195 O2 W4 (#1799): Megavolt's round 4 review of 994c3f7fb (blob 2af460ab7): APPROVE, R3-B1 and R3-B2 closed, R4-N1 (re-migration after a pre-W4b restore) non-blocking for W4b; doc only 2026-10-22 18:47:51 -05:00
jason.woltje 5a91431ef9 Q195 O2 W4 (#1799): Megavolt's round 3 review of 54246cb32 (blob 0c4f55826): CHANGES, R2-B1 projection and R2-B2 closed, R3-B1 (reserved notes keyed on negative IDs) and R3-B2 (fence initial state and confirmation scope undefined); doc only 2026-10-22 18:47:37 -05:00
jason.woltje a773e2724c Q195 O2 W4 (#1799): Megavolt's round 2 review of 019a0ea5b (blob 14dfe92d9): CHANGES, B1-B9 and M1 closed, R2-B1 (paragraph-mark revisions, reserved-note leaves) and R2-B2 (release backup/restore keeps O2 text and revives revocations); doc only 2026-10-22 18:47:37 -05:00
jason.woltje 1e840418a1 Q195 O2 W4 (#1799): Megavolt's review of the DOCX citation design at 15e91538f (blob 662abdb7c): CHANGES, B1-B9 and M1 (Case 2 narrowing in O2-context turns goes to Mos/Jason); doc only 2026-10-22 18:47:37 -05:00
Rocko 994c3f7fb1 docs: define reserved notes and initial O2 restore fence 2026-10-22 18:47:01 -05:00
Rocko 54246cb32b docs: specify O2 projection marks and restore revocation fence 2026-10-22 18:42:16 -05:00
Rocko 019a0ea5ba docs: close W4 locator and access review gaps 2026-10-22 18:36:18 -05:00
Rocko 15e91538f2 docs: define committed DOCX citation and access contract 2026-10-22 18:23:37 -05:00
jason.woltje e36bce3526 docs: name save-group settings and enforce adoption gates 2026-10-22 18:13:49 -05:00
jason.woltje 17a48a7782 docs: fix O2 review gates, shipping and save semantics 2026-10-22 18:10:14 -05:00
jason.woltje e012bab0be docs: redesign Q195 for full WordCraft O2 editor 2026-10-22 18:03:56 -05:00
5 changed files with 2327 additions and 652 deletions

No files matched your search

+14
View File
@@ -5344,3 +5344,17 @@ Gander: after Duckworth-0100 design approval with C1-C5, prepared the two host-s
2026-10-22T18:11:57Z | Sage | Row 275 (#1780, Morgana, Duckworth) LANDED 3410df7c3. Before: refactor at 48e450ce8; viewers had no verb list of their own and board reads were not refused to users. After: a fast-forward of 3 commits; manifest 39847b15 (4 paths) blobs match; gate 17:52:12Z-18:05:00Z green (whole files 7/22/6, bus 834, cli 861, harness 151, business 231, seat 167, 10 scripts with 3 named skips), M6 and M10 killed; Duckworth APPROVE rev 3260 (comment 30717); note and done by sage-2897; #1780 comment 30728 and closed. N1 (board refusal test as a loop over BOARD_VERBS) is non-blocking.
2026-10-22 18:11:58 UTC | Gander row276 #1781 prepared, gate pending | Before: row253 left5legacy Case1 AS_USER calls unsupervised across sudo pty sessions. After: candidate uses its unchanged supervisor with per-call inner/outer budgets, separate streams, strict display-only failure trailer, fake-only paired process-model cases and15unique mutants. Duckworth0182 B1 corrected the plan before product code;0183 cleared build. Parse-only Bash checks pass; no Node, behavioral test, host, unit or config action. Runtime gate and publication await Sage.
2026-10-22T23:03:39Z | Gyro Q195 O2 rewrite, #1752 / Vikunja #448, mos-1356 | Before: landed O3 design 95e3f7166/blob 4cb59999. After: Q231/Q233 full WordCraft O2 design on gyro/editor-o2 from refactor c4f77373f; canonical git DOCX, original Markdown/wiki preservation, human cadence default 0 with instant accepted-save commits, full rich-feature first slice. Ten briefs include independent source/dependency security clearance before any build queue entry, exact editor-only CSP approval gate, restricted source/build/bridge, separate reviewed DOCX citation gate and flags-only agent access. Existing Case 2 semantics unchanged; any integration requirement change goes through Mos to Jason. Static checks: ten briefs/seven fields, independent reviewers, unique anchors, current source/base/blob pins, docs scope and append-only logs pass. No builds, browser/worker runs, code, queue or live changes. Fenton review pending; CSP and implementation gates remain closed.
2026-10-22T23:03:53Z | Gyro Q195 O2 push guard | CI queue guard identity gyro / purpose push returned exit 0, documented degraded-pass: CANNOT_ASSERT credential-unresolvable. Audit: /mnt/storage/scratch/gyro-q195-ci-queue-audit.jsonl. No credential fallback or CI-green claim.
2026-10-22T23:09:38Z | Gyro Q195 O2 fenton-0006 fixes | Before: e012bab0b left stale-source ambiguity, incomplete migration/shipping ownership, undefined typing transactions and an unmeasured CSP gate. After: D12 explicitly routes superseded-source/C2 reply notices to Mos/Jason and refuses adoption before W4b/notice path; Q2 remains open. F2-F6 add schema-edge placeholders and restore-accounting files, shipped runtime data/writing closure with no sandbox store mount, per-chapter DOCX and 2s-pause/15s-span save groups, volume/latency/growth gates, split-run clickable wikilinks and historical Obsidian graph warning, and S0 isolated existing-artifact CSP measurement before exact-header approval. Static ten-brief/ownership/settings/checklist checks pass. No build/browser run, product/queue/live change. Independent re-check pending.
2026-10-22T23:09:50Z | Gyro fenton-0006 push guard | identity gyro, purpose push, exit 0 documented degraded-pass CANNOT_ASSERT credential-unresolvable; audit gyro-q195-ci-queue-audit.jsonl. No fallback credentials or CI-green claim.
2026-10-22T23:10:14Z | Gyro mos-1358 ruling recorded | Q36 / Vikunja 562 keeps C2 superseded notices OPEN with Jason. Markdown adoption refusal until W4b PASS is now a separate settled Mos build-order ruling, not a Jason question. D12 and Q2 distinguish them; no notice build authorized.
2026-10-22T23:13:27Z | Gyro Q195 O2 fenton-0007 F7/F8 | Named human-only editor.saveGroupPauseMs=2000 and editor.saveGroupMaxMs=15000 with integer/range/cross-field validation and W3a UI/CLI refusal tests. W3b owns production adoption refusal until W4b; isolated fixture conversion remains testable without request-flag bypass. W4b owns adoption module/API tests and reviewed lifting only with committed-reader/citation readiness and, if Q36 approves, the separate notice row PASS. Q36 unresolved stays closed. Static ten-brief, setting-key/bounds, refusal/lift ownership and two-question checks pass. Docs only; no build, browser/worker run, product/queue/live change. Fenton checks F7/F8 on next push.
2026-10-22T23:13:48Z | Gyro mos-1359 / r3 push guard | D13 records save-group timing as Mos's accepted reading of Q226 F41, not Jason's ruling; both defaults remain human-only settings. Push guard identity gyro/purpose push: exit 0 documented degraded-pass, CANNOT_ASSERT credential-unresolvable; audit gyro-q195-ci-queue-audit.jsonl, no credential fallback or CI-green claim.
+3
View File
@@ -1549,3 +1549,6 @@ are never rewritten or removed; corrections are new entries.
2026-10-22 | Della | Row258 E01 3094e3bfa focused manifest review | APPROVED 11-path manifest/static F1/F2/N1 corrections; file and full-blob digest recipes verified. M1-M11 fixed; all execution/landing evidence still required in CLEAR. No shared-log edit, Node, tests, HTTPS Git or live action.
2026-10-22T17:55:40Z | Sage (Opus 5.5, 1ef1e4f8) | Row 253 landing; post-253 doc sweep | 253 LANDED 48e450ce8. Sweep on sage/sweep-253: Gosalyn Case 2 design and rows, 229 design, Gyro 0acbdb0d8, Della 179391055, 268 docs, seat dirs for Della, Scrooge, Huey, Louie and Ludwig, human-users-build:70 under Q173, DEFERRED 253 N1, 198 N2, row 203 text, 224 residual and 206 sharee list, and seat log lines.
2026-10-22 14:48 UTC | Researcher (Sonnet 5.5, 9bac0794) | Rows 220 and 216 resumed (sage-2708, sage-2716, sage-2719); local commits only | 220 e4a204836 on researcher/row220-r2 (on ea6dc9eff): C1 run tests, C2 default-reader M7 test, configDirectory fixture scope, BUILD.md round 2b. 216 4f3fbc25a on researcher/row216-r2 (on aca16404c): index cache, find/get/search/manuscript/universeInfo, PRD 1.14 levels. Nothing pushed, claimed or run; node --check not run. Waits for Sage's CLEAR and go; rebase onto 253 on a new branch before any gate.
2026-10-22T23:03:39Z | Gyro (Codex gpt-6.1-sol, mos-1356) | Q195/Q231/Q233 O2 design rewrite, gyro/editor-o2 from c4f77373f | Canonical queue next gyro: nothing. Ten docs-only briefs; full-feature WordCraft, independent 15-crate/199-package security review, editor-only CSP candidate, instant git DOCX saves and retained Markdown/wiki sources, committed-only DOCX citation gate, no agent story prose. Static structure/source-anchor/scope checks only; no code, build/run, queue or live change. Fenton review requested after push.
2026-10-22T23:09:38Z | Gyro (Codex gpt-6.1-sol, fenton-0006) | Q195 O2 design corrections on gyro/editor-o2 | F1 requirement boundary sent to Mos, adoption gated; F2-F6 source-checked docs fixes prepared. Ten briefs and two open owner items checked statically. No implementation, build/browser/worker run, queue or live action; Fenton re-check after push.
2026-10-22T23:13:27Z | Gyro (Codex gpt-6.1-sol, fenton-0007) | Q195 O2 r3 F7/F8 | Save-group keys/defaults/validation and W3b refusal/W4b lift ownership explicit. Static docs checks only. Q36 remains open; no implementation or gate lift. Direct Fenton/Mos handoff after push.
File diff suppressed because it is too large. Load diff
@@ -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 `<dataRoot>/writing/<business>/<project>/`.
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": "<sha256 of UTF-8 quote>"
}
```
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": "<sha256 UTF-8 text>",
"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/<business>/<thread>/session and claude-projects | no O2 content, prompt, model transcript, title or derived reply |
| threads/<business>/<thread>/sessions/<session>/session and claude-projects | same; O2 mode never binds these native stores |
| session-workspaces/<business>/<instance>/<session> | no O2 text or exports; no store mount |
| launches/<business>/<run> 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 |
| <dataRoot>/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 `<dataRoot>/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/<business>/<row>/<digest>.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
`<dataRoot>/backups/stack/<ACT>/bus/`; restore_store at :498ff moves current
bus/ to `<dataRoot>/bus.post-attempt-<stamp><suffix>/`. 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:
`<dataRoot>/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.
@@ -0,0 +1,528 @@
# Q195 O2 W4 review: committed DOCX citations and access
Reviewer Megavolt (Opus 5.5), assigned by Sage (sage-3061, mos-1360), Gitea #1799. Owner Rocko.
Reviewed: `docs/plans/2026-10-22_q195-docx-citations.md`, blob 662abdb7c, commit 15e91538f (one
docs-only commit on e36bce352). Against: parent blob 322ec50f2 (`docs/plans/2026-10-21_q195-writing-editor.md`)
D11, D12, D24-D26, W4 and W4b. Static review only: no tests, parser runs or live files.
## Verdict: CHANGES
The W4 Gate asks for "no unresolved locator or permission ambiguity" and for every bypass closed.
The design gets the core right, but nine items are still open against that bar (B1-B9). One part of
the design changes Case 2 behaviour and goes to Mos and Jason (M1). None of these needs a new
architecture. Each one needs a definition, a rule or an acceptance row.
## What holds
- Snapshot: manifest and DOCX come from one reachable commit. There is no worktree, export or
engine-memory fallback, and commit/blob is a locator, not an authority token (section 2).
- Identity: a copy or new import gets a new document ID with no inherited grant, share, pin or cache.
The same blob in another document is not selectable.
- Quote verification: exact part, story and ordinal, a code-point range, literal quote equality,
SHA-256 equality, blob and commit membership, and a pinned extractor revision. A stale locator
returns old text labelled stale and never relocates.
- Write paths: none. There is no native MCP, execute, save, replace, tracked-change or comment write,
and flags belong to W5 as metadata.
- Case 2: the line-cite schema, library grants and supporting-doc writer are unchanged, and no DOCX
paragraph is called a line. Q36 stays open and adoption stays closed (mos-1358). A19 and A20 test
this.
- Parser: transitional namespace only, closed relationship types, no DTD, entities or external
relationships, ZIP bounds before allocation, and refusal rather than a truncated success.
- Document text is data: comments, deletions and wiki tokens never become instructions.
- Text kept out of generic stores: the design keeps O2 text out of generic raw stores and logs,
allows audit events to carry only IDs and digests, and does not let a shared extraction cache act
as an authorization cache.
## Blocking findings
### B1. Several named records have no exact shape
Section 3 says "all objects reject unknown keys", but these objects are never specified:
- **The multi-paragraph citation.** Line 100-101 calls it "an ordered nonempty list of these
ranges", but `editor_resolve_citation` takes one `{locator}`. The design needs to give:
- the wrapper and its kind;
- whether each range carries its own quote and digest;
- whether every range must share commit, blob, extractor, view and part;
- whether the paragraphs must be contiguous;
- the maximum count;
- how the resolver and the share chip accept the list.
- **wikiTokens entries** (line 120 and section 5). The field names, offsets and forms (`link`,
`label`, `heading`, `embed`) are not given.
- **Anchor story.** Anchors are "a canonical decimal signed XML ID string" (line 127), but locator
stories are nonnegative (line 88). Use one grammar for both.
- **`paragraph.xmlId`.** It is "JSON null when absent" (line 89), but also "if provided" (line 97).
Two locators for the same range must not both be valid. State that null means "this node has no
`w14:paraId`" and that a node with one must carry it, or make it always null.
- **Departure from D25.** D25 says "resolve paragraph IDs when present, otherwise an ordinal". The
design makes the ordinal primary and the paraId only a check. I agree with that on the merits,
since paraId is not unique, but the design should name it as a deliberate departure from D25's
wording so that Sage's brief carries it. This is O2-only, not a Case 2 item.
- **The share record, the chip request and the refusal envelope.** Section 7 needs:
- the fields the WebUI sends to have a selection mapped;
- the fields of the stored share record;
- the error object's keys (section 6 gives only the codes).
### B2. Extraction is not closed enough to be deterministic
Section 5 lists containers and a few leaves, then refuses "unknown text-bearing elements". Offsets
depend on how each run-level leaf counts, so two conforming extractors can disagree. The design
needs a closed leaf table that gives each leaf's text contribution or refusal. At least these are
missing:
- `w:annotationRef`. Word starts every comment with it, so whether comments are readable at all
depends on an unstated choice.
- `w:footnoteRef` and `w:endnoteRef`, the number mark inside the note's own story.
- `w:noBreakHyphen`, `w:softHyphen`, `w:sym` and `w:ptab`.
- `w:moveFromRangeStart/End` and `w:moveToRangeStart/End`, which always come with the moves the
design supports.
- `w:commentRangeStart/End`, as anchors.
- `w:permStart/End`.
- `w:fldChar`, `w:instrText` and `w:fldSimple`, as refusals.
Two revision rules are also unstated:
- **Nested revisions.** A run is in `final` if and only if no ancestor is `del` or `moveFrom`, and in
`original` if and only if no ancestor is `ins` or `moveTo`. The design should say so.
- **Paragraph-mark revisions.** For `w:pPr/w:rPr/w:ins|w:del`, state that views never merge or split
paragraphs, so that an inserted paragraph shows as empty in `original`. That follows from "ordinals
do not change with view" but should be explicit.
### B3. `currentCommit` and staleness are ambiguous for multi-chapter projects
D10 puts one DOCX per chapter under one book manifest, and the store is per project
(`<dataRoot>/writing/<business>/<project>/`). `stale` is "exactly commit != currentCommit"
(line 268). If `currentCommit` is the project's newest commit, then saving chapter 2 marks every
chapter 1 citation stale and breaks chapter 1's latest cursors, even though chapter 1's blob did not
change.
Two things need a definition:
- `currentCommit`: for example, the newest published commit that changed this document's DOCX blob
or its manifest entry. Alternatively, define `stale` by blob plus manifest entry.
- The document's "published history" (line 49): which ref, and which commits count.
Add an acceptance row: a save to a sibling document leaves `stale:false`.
### B4. The permission model is prose, not data
Section 7 intersects six inputs, but the grant side has no schema. Before W4b can implement this
the same way twice, the design must answer:
- Where do document, part, view and comment scopes live: R5's access.json, a new bus table or the
role files? Who writes them, through which human-only verb, with what proof, receipt and revision?
Rows 207 and R5 already provide that pattern for business grants.
- Who is "the principal" in an agent turn: the human who sent the chip, the seat, or both?
- Can a `viewer`-level human share? Can a share ever expose a part or view that the sharing human's
own grant does not cover?
- In a thread with two humans, whose grants does `conversation-current` intersect?
- "Historical-view scope" (line 290) appears once and is undefined. Is it a separate scope, or the
live grant plus an exact share?
- The cursor binds a "grant revision" (line 282). Which revision counts?
### B5. Share semantics contradict each other
- Line 329-331 says a share "does not widen it or authorize a later version". Line 340 says "latest
whole-document reads require a whole-document share", and A15 says "whole-document latest share
explicit". The design must say what a whole-document chip binds:
- one commit or latest;
- whether it includes notes, comments and the `original`/`markup` views.
- Range shares: rows are whole paragraphs, so `editor_read_committed` must refuse under a range-only
share. Only `editor_resolve_citation` works with one. State this.
- `off` "revokes future reads". Does it also stop resolution of exact shares already sent?
- `conversation-current` lasts "until human revocation/session end". Which session is that: the bus
session, the thread session (row 269) or the Console login?
### B6. Paging rules contradict each other
- Overflow. The design says "response overflow never returns partial data with success", but paging
must end a page before a row that would overflow. State that the page ends before the first row
that would exceed 64 KiB, and that only a single row too large on its own refuses.
- Paragraphs over 64 KiB. The design says a caller "may request a checked subrange through a
separately bounded citation", but a citation needs the quote and its digest first, so such a
paragraph is unreachable. Either define a bounded subrange read (ordinal plus a code-point window,
returning text and a server-made locator) or declare these paragraphs unsupported in v1.
### B7. Git grants: the design does not forbid a remote
Case 2's library has a Gitea remote. Agent supporting-doc writes push through each seat's own Gitea
account (Q226, case2-rows.md:1634-1641), and seat tokens carry `write:repository` and can fork
(packages/bus/src/gitea-grant.mjs:29 and :297). The design says "no raw O2 git clone/read grant", but
nothing stops an O2 project repo from getting a remote, mirror or backup push that any seat account
can then list, fork or clone.
The design must state that O2 store repositories have no remote, and that no mirror, backup or push
reaches a host an agent account can access. It also needs a stack-copy/release rule: copies never
carry the store. Add an A16 probe: a seat Gitea token cannot list, fork or clone an O2 project.
### B8. The raw-session surfaces are deferred, not closed
Line 437-438 hands the surface inventory to the W4b brief. The W4 Gate wants raw sessions closed
by this design. The stores already exist and can be named now, each with a rule (no O2 text,
opaque reference only, or enforced envelope):
- seat session stores `threads/<business>/<thread>/session/` and `claude-projects/`,
`threads/<business>/<thread>/sessions/<session>/` and `session-workspaces/...`
(packages/seat/src/session.mjs:23-29);
- the bus store's event and thread-message tables, and row 264's sessions;
- the Console thread view;
- the CLI's `thread`, talk and follow;
- the Discord connector (packages/discord, including journal.mjs and context.mjs) and the
control-board Discord path;
- `<dataRoot>/runs/` run records.
Run records need a rule of their own. Invariant 5 makes them write-once, so O2 text written there
can't be withdrawn on revocation, only pruned. State that O2 content never enters a run record.
### B9. The migration and compatibility plan is too thin to review
W4 asks for "a reviewed migration/compatibility plan". Lines 33-35 say only "new O2 records, a schema
edge from Sage, no reinterpretation". The plan needs:
- **New tables and columns.** Name them, and state whether any existing table (thread messages,
events) gains a provenance column.
- **Rollback.** After `release.sh rollback` to a pre-W4b copy, the old code does not enforce the
envelope. Any O2-derived text stored in an existing table becomes readable through the old thread
views. Require that O2-derived content is stored only where pre-W4b code can't render it (a new
table, or opaque references in existing tables), and add a rollback probe.
- **Extractor revision retention.** Old locators need their old revision. Say whether activating a
release that drops a revision is allowed, which turns those citations into
`editor-version-unavailable`, or whether every published revision is retained.
## Mos/Jason item
**M1. O2-context turns narrow Case 2 behaviour.**
Section 7, lines 362-371, does two things in any turn that received O2 context:
- It refuses "generic supporting-doc writes, git writes, exports and arbitrary outgoing tools".
- It classifies every output of that turn, and of any resumed or forked descendant, with the O2
document scopes.
In a Case 2 thread, that means:
- C2-02's supporting-doc write, from the Q226 rows (mos-1309), stops working once the human shares
a chip;
- the agent's C2-06 replies become invisible to thread members without the O2 grant.
That changes a Case 2 requirement. Under the W4 Gate it goes to Jason through Mos before code, not
as a design choice.
My recommendation: confine O2 context to O2-dedicated threads or sessions. Case 2 semantics then
stay untouched, and the narrowing applies only where it was opted into. Otherwise Jason has to
approve the narrowing for mixed threads.
The rest of the design changes no Case 2 requirement. Q36 and D12 stay open with Jason as before,
and the design builds no notice.
## Non-blocking notes
- N1. Give the canonical serialization of the extractor build manifest that `extractor.revision`
digests, so that the digest can be recomputed independently.
- N2. A locator's `business` field must equal the broker-bound business. Otherwise refuse with
`editor-access-refused`, the same response as for a foreign ID.
- N3. The 100:1 expansion ratio: say whether it is per entry, total, or both.
- N4. Line 68-69 says "no implicit defaults except the reading view", but `limit` defaults to 20 and
`cursor` to null. Reword.
- N5. Hyperlinks, fields and text boxes refuse the projection. Many real chapters will then be
agent-unreadable. That is acceptable for v1, but the W4b brief and the human UI should say why a
chapter is unavailable.
- N6. Readiness should come from the verified release artifact (extractor manifest plus the
formatting-property list), not a system-config field, since configs are human-authored
(invariant 2).
- N7. Add acceptance rows for B3 (sibling save), B6 (oversized paragraph), B7 (seat token against an
O2 repo), B8 (each named store, including run records) and B9 (rollback).
## Next round
Revise the design for B1-B9 and send the new commit and blob to me and Sage. I re-review only the
changed sections and the items above. M1 waits on Mos and Jason. If the design takes the
O2-only-thread recommendation, M1 closes without a ruling.
## Round 2: 019a0ea5b (blob 14dfe92d9)
Reviewed: Rocko's round 2 commit 019a0ea5b, a fast-forward on 15e91538f, design blob 14dfe92d9.
I read the whole file again, not only the changed lines, because the change was large (+453/-120).
Static review only.
### Verdict: CHANGES
The design now closes B1-B9 and M1 as they were written. Two problems remain. R2-B1 is a
projection rule that contradicts D25's "final accepted reading". R2-B2 is an existing backup and
restore path that section 7 does not cover. Each needs a rule and an acceptance row, not a new
design.
### Round 1 items
- **B1, closed.**
- The `docx-selection` wrapper: 2..32 consecutive ranges, a shared snapshot, empty intermediate
paragraphs, one budget for the whole selection, atomic failure and one canonical form.
- `xmlId` is null exactly when the node has no paraId.
- The wikiToken and anchor grammars now match the locator.
- The chip, share and refusal envelopes are exact.
- The departure from D25 is named (lines 108-111).
- **B2, closed except for R2-B1.** The closed leaf table and the ancestor veto for nested revisions
are in (lines 268-310).
- **B3, closed.**
- `refs/heads/writing-published` advances first-parent with no force.
- Publication commits are defined per document, and `currentCommit` and stale are per document.
- A23 covers a sibling save.
- **B4, closed.**
- The principal is a tuple.
- Scope lives in `writing_access_events`, written by the business owner's existing human proof
only.
- Agent `mayShare` is false, and viewers cannot share.
- The authority vector names its sources.
- Two-human threads are refused (`editor-sharing-unsupported`).
- **B5, closed.**
- A range share allows resolve only.
- A document chip binds one exact publication.
- A latest share exists only under `conversation-current` with a whole-document chip.
- `off` and any policy change invalidate all earlier shares.
- The share's session is the new O2 execution session.
- **B6, closed.** A page ends before the first row that would overflow. A row too large for an
empty page is unsupported in v1, and A24 covers it.
- **B7, closed.**
- O2 repositories have no remotes, mirrors or forks, and the store refuses one.
- Copies exclude `writing/` and `writing-context/`.
- A25 includes a seat-token probe.
- **B8, closed except for R2-B2.** Section 7.4 names every store and renderer I listed, plus
`launches/` and `bus-host/sessions.json`. The run-records rule is explicit, and A26 probes each
store.
- **B9, closed except for R2-B2.**
- New tables are named, append-only, with no change to existing columns.
- Old renderers never query the new tables.
- A rollback probe and an extractor-retention rule are in.
- **M1, closed. No Mos/Jason item.** O2 context exists only in new, dedicated O2 conversations and
execution sessions. Existing C2 threads cannot be converted, attached, resumed or forked into O2
mode. C2-02 supporting-doc writes and C2-06 replies keep their meaning (lines 433-441), and A28
tests that. This adds new O2 behaviour and changes no Case 2 requirement.
- **N1-N7, addressed.** N1 is the canonical manifest bytes (lines 155-168). N2 is the business
check (lines 378-379). N3 is the ratio rule at line 257. N4 is the defaults wording at line 84.
N5 is the UI reason (lines 688-690). N6 is release-derived readiness. N7 is A23-A31.
### R2-B1. Paragraph-mark revisions drop accepted text, and reserved notes would refuse
**What the design says.** Lines 276-281: "an inserted paragraph mark makes that paragraph's
projected text empty in original; a deleted paragraph mark makes it empty in final."
**What the markup means.** In WordprocessingML, `w:pPr/w:rPr/w:del` marks only the paragraph mark
as deleted. Accepting the change merges this paragraph's text into the next one. The text is gone
only if its own runs sit under `w:del`, and the ancestor veto already handles that. `w:ins` on the
mark works the same way in `original`.
**Why that breaks the projection.** When a human joins two paragraphs with tracking on, the first
paragraph's live text disappears from `final`. A quote taken from the accepted reading then can't be
cited. That contradicts D25's "default final accepted reading" and the design's own definition at
line 267.
**Fix.** Paragraph-mark revisions never change text. Runs follow the ancestor rule alone. Since
views never merge or split paragraphs, a mark-only revision shows up only as a zero-width
whole-paragraph-mark span in `markup`. Extend A31 with a mark-only deletion whose text stays in
`final`, and a mark-only insertion whose text stays in `original`.
**Reserved notes.** The leaf table "is exhaustive; unknown leaves refuse" (line 305). Word writes a
separator note and a continuation-separator note into every `footnotes.xml` and `endnotes.xml`,
holding `w:separator` and `w:continuationSeparator` (and sometimes `w:continuationNotice`). Line
227-229 calls these notes structural, but the table doesn't name those leaves. Read literally, every
document that has footnotes refuses. Name the three leaves as allowed only inside reserved notes,
structurally validated and never projected. Say whether leaf validation covers the whole part or
only the requested story.
### R2-B2. The release backup and restore path keeps O2 text and can revive revoked access
**What the design says.**
- Section 7.4 forbids "plaintext backup" of O2 material (lines 625-626).
- Section 7.5 puts O2 message bodies, share selections (which hold quotes) and every grant and
policy event into new tables in the bus store (lines 638-663).
**What the release script does.** `scripts/stack-release.sh` copies that store, in plaintext, on
every activate:
- `backup_store` writes `$DATA/backups/stack/<ACT>/bus/bus.sqlite` (lines 474-493).
- It never prunes backups (line 18).
- `rehearse` reads them (line 22).
- `restore_store` moves the current `bus/` into a post-attempt directory and deletes nothing
(line 496).
So O2 text persists in backups and post-attempt directories that section 7.4 does not name.
**The restore gap.** `rollback --restore` puts back an older store. The guard counts `events` plus
the fixed `COUNTED` list (lines 16-17 and 418-420). The design keeps O2 writes out of generic
events (lines 659-661), and the new tables are not in `COUNTED`. A restore over revocations made
after the backup therefore passes the guard without Jason's `--events N`. It also silently brings
back the revoked `writing_access_events`, `writing_policy_events` and session-end rows. That
revives revoked grants and shares, against section 7.3's "a scope decrease/revocation denies even
historical shares".
**What the design needs to state.**
1. **Backups.** Either O2 bodies and quotes live outside `bus.sqlite` (for example under
`writing-context/`) so that release backups hold opaque rows only, or release backups and
post-attempt directories are named O2-classified stores in section 7.4, with mode, access and a
prune rule.
2. **The guard.** Every `writing_*` table joins the restore guard's counted list. The T46 guard
already requires every schema table to be classified.
3. **After a restore.** A restore that drops any `writing_*` row disables O2 readiness until the
owner reconfirms access. It never brings back an older grant, share or policy.
4. **Acceptance.** Add a row: a revocation after the backup, then a restore, must leave the revoked
share refused. A secret marker must stay absent from every backup and post-attempt path except
the named O2-classified ones.
### Non-blocking
- R2-N1. Define the agent `subject.id` for `editor.access.set` as the bus agent instance ID (the
principal tuple's `agentInstance`), so a grant can't name a role or a seat display name.
- R2-N2. Lines 330-332 restate the structural leaves already in the table. Drop the sentence or
point it to the table, so there is one list.
### Next round
Fix R2-B1 and R2-B2 and send the commit and blob to me and Sage. I'll re-check those two items and
any lines they touch.
## Round 3: 54246cb32 (blob 0c4f55826)
Reviewed: Rocko's round 3 commit 54246cb32, a fast-forward on 019a0ea5b, design blob 0c4f55826.
I read the whole delta and every definition it touches. I checked its citations against the same
commit: `COUNTED` is at `scripts/stack-release.sh:420`, T46 is at
`packages/cli/tests/stack-release.test.mjs:913`, and the guard counts `WHERE at > STAMP`, which
works with the `at` column every `writing_*` table has. Static review only.
### Verdict: CHANGES
R2-B1's paragraph-mark rule and all of R2-B2 are closed. One gap from R2-B1 is still open, and the
new fence adds a second. Each needs a sentence or two, not a redesign.
### Round 2 items
- **R2-B1, paragraph marks: closed.**
- A mark-only `w:ins`/`w:del` changes no projected text in any view.
- Paragraphs are never merged or split.
- Markup records a zero-width `paragraph-mark-insert`/`paragraph-mark-delete` span, and
`revisionSpans` allows those kinds with start=end.
- A31 covers both cases.
- **R2-B1, reserved-note leaves: open.** The table rows and whole-part validation are in (lines
301 and 311-318). How a reserved note is recognized is still wrong; see R3-B1.
- **R2-B2: closed.**
- Message bodies and share payloads leave SQLite for 0600 content objects under
`writing-context/objects/`. The bus tables hold only refs and digests.
- Backup and post-attempt trees are named O2-classified metadata stores, with modes, link checks,
rehearse limits and a receipt-based prune that only a human can run.
- All eight `writing_*` tables join `COUNTED` and T46, and row identities are compared.
- An independent fence epoch, set before every store swap, stops restored rows from granting
access, and owner confirmation can't revive them.
- A32 covers the revocation, restore, crash and marker sweep.
- **R2-N1: closed.** The agent `subject.id` is the agent instance ID.
- **R2-N2: closed.** The structural-leaf list now points to the table.
### R3-B1. Reserved notes are recognized by negative ID
Line 228 defines reserved notes as "negative IDs and type separator/continuationSeparator". Word
normally writes the separator as `w:id="-1"` and the continuation separator as `w:id="0"`, so the
continuation separator is not negative. A `w:type="continuationNotice"` note is a third reserved
type, and line 228 leaves it out, although the leaf table names it.
Read with lines 315-317 ("A separator leaf in a normal note/body/comment refuses"), the id-0
continuation separator counts as a normal note. Its `continuationSeparator` leaf then refuses, and
every footnoted document Word writes is refused again. ECMA-376 identifies these notes by `w:type`;
the ID is only a number. I'm working from Word's usual output here, so a fixture from real Word
should confirm it.
Fix:
- Classify a note as reserved by `w:type` in {separator, continuationSeparator,
continuationNotice}, with any integer ID.
- A normal note has no `w:type` or `w:type="normal"` and a canonical nonnegative ID.
- A reference to a reserved note refuses.
- Add Word's -1/0 layout to A31.
### R3-B2. The fence's first state and when confirmation is needed are undefined
Line 791 says a missing fence denies O2. Only release and the owner-proof handler may write it
(lines 762-763), and only "before any store swap" is a write moment defined (line 767). Nothing says
who creates the fence when W4b's tables are first created, or with what epoch.
It is also unclear whether `confirmedBusinesses` is consulted on a store that has never been
restored. Read literally, a fresh fence has no confirmed business, so O2 stays off until an
`editor.restore.confirm` that nothing calls for. The other reading, confirmation only after a
restore, needs a rule saying so.
Either way this is a gap in an authorization check: it fails closed, but an implementer has to
invent the enable path. Fix:
- State that the compatible controller creates the fence durably with the W4b migration (or at
first O2 enable), with a fresh epoch, `restoreOperation` null and the empty dropped-rows digest.
- State whether `confirmedBusinesses` gates O2 always or only after a fence whose
`restoreOperation` is non-null.
- Add both states to A32.
### Non-blocking
- R3-N1. Content objects are named by the SHA-256 of `{body,provenance}`. Two equal objects in
different businesses share one file, so a per-business prune or retention action can delete
another business's message. The digest kept in metadata backups also lets anyone confirm a
guessed short body ("ok"). Put the row `id` (or a random nonce) in the canonical object so each
digest is unique and can't be guessed.
- R3-N2. Line 791 says rolled-back fence state denies O2, but the fence keeps only its current epoch,
so nothing can detect a rollback. Either define the check (for example, a monotonic fence
generation that every new row also records, and deny when the bus holds a newer generation than
the fence) or drop the claim.
- R3-N3. `restore_store` begins at `scripts/stack-release.sh:498`; line 727 cites ":496ff", as my
round 2 did. Cosmetic.
### Next round
Fix R3-B1 and R3-B2 and send me and Sage the commit and blob. I'll re-check those two items and the
lines they touch.
## Round 4: 994c3f7fb (blob 2af460ab7)
Reviewed: Rocko's round 4 commit 994c3f7fb, a fast-forward on 54246cb32, design blob 2af460ab7.
I read the whole delta and the definitions it touches. Static review only. I did not download the
two public fixtures Rocko cites, so their sizes and hashes are Rocko's; what they report (-1/0 in
Word 15, 0/1 in Word 12) matches my round 3 claim and also shows that an ID can't identify a
reserved note.
### Verdict: APPROVE
Every blocker from rounds 1-3 is closed, and M1 closed in round 2 with no Mos/Jason item. The
design meets the W4 Gate (spec blob 322ec50f2, lines 806-850):
- no locator or permission ambiguity is left;
- new O2 copies, generic reads, git grants and raw sessions are closed as bypasses;
- quotes verify against a committed blob and an extractor revision;
- there is no unsaved-context or agent write path;
- Case 2 Markdown authorization is unchanged;
- a migration and compatibility plan is present.
W4b owns the implementation and the A1-A32 evidence. This approval covers the design, not passing
tests.
### Round 3 items
- **R3-B1: closed.**
- Reserved notes are classified by `w:type` in {separator, continuationSeparator,
continuationNotice}, with any canonical signed ID (line 227).
- A normal note has no type or type normal, plus a nonnegative ID; unknown types refuse.
- Any reference to a reserved note refuses, including an id-0 continuation separator.
- Duplicate IDs within a part refuse.
- A31 covers both the -1/0 and 0/1 layouts.
- **R3-B2: closed.**
- The first schema migration creates the fence durably before the tables commit, with a fresh
epoch, `restoreOperation` null, the `[]` digest and no confirmed businesses (line 781).
- Confirmation applies only when `restoreOperation` is non-null (line 819).
- A missing or malformed fence over existing `writing_*` tables or history fails closed and
never reinitializes (line 790), and a crash resumes on the same epoch.
- A32 covers both states.
- **R3-N1: closed.**
- Each object holds business, project, row and a private 32-byte nonce, and lives under
`objects/<business>/<row>/`.
- There's no cross-row deduplication, and retention is per owning row.
- **R3-N2: closed.** The rollback-detection claim is replaced by an honest limit: a same-uid
operator bypassing the protocol is outside what the file can prove.
- **R3-N3: closed.** The `:498` citation is fixed.
### Non-blocking, for W4b
- R4-N1. Line 790 allows initialization only with no `writing_*` tables and no fence. One case is
unnamed: a compatible-controller restore of a backup taken before W4b, then a re-migration. The
tables are then absent while a rotated fence with a non-null `restoreOperation` exists. Neither
reading opens a bypass, because the restore dropped every `writing_*` row. W4b should still say
that re-migration keeps the existing rotated epoch and its confirmation requirement, rather than
refusing forever or writing a new null fence. Add that case to A32.