Skip to content

v2.6.0

Choose a tag to compare

@github-actions github-actions released this 23 Jun 01:26
· 370 commits to main since this release

v2.6.0 - 2026-06-23

Annotated-Manuscript Deliverable — letter ↔ margin cross-links (Increment 3)

Navigation between the editorial letter and the marked-up copy is now bidirectional. The margin→letter direction already existed (every comment ends "(See letter §F-…)"); this adds letter→margin. A crosslink render treats the letter as a "second snapshot" and injects a CriticMarkup back-link span — {>>→ marked-up copy: <id> @ <kind>:<value><<} — immediately after each letter <!-- finding: F-… --> marker whose finding has a manifest annotation, copying the anchor verbatim from the gated manifest. The same reverse transform (delete every {>> … <<} span) proves the letter is untouched, behind the same two-sided sigil precondition (the letter, being authored prose, must not already contain a CriticMarkup sigil — without this the transform would silently delete an author's own span). A new crosslink validator gates bidirectional integrity: X1 the margin comment carries the forward link, X2 each back-link's anchor equals the manifest's (no drift), X3 no dangling either way (no phantom back-link; no missing reverse link), X4 no letter mutation; W1 annotated-but-uncited is advisory. Firewall-clean: the back-links carry only finding IDs + anchor tokens drawn verbatim from the manifest, never authored prose. Validators 42 → 43. Consumer-only — the shipped synthesis does not yet emit letter markers matching the annotation manifest, so the feature is inert on the real corpus until that producer lands; the canonical --check-all fixture's worked letter + crosslinked letter are hand-constructed.

Annotated-Manuscript export — DOCX with anchored comments → Google Docs (Increment 4)

The marked-up manuscript can now be exported as a .docx that imports into Google Docs with each finding as a native anchored comment — the professional editorial deliverable (Word/Google-Docs comments are the industry standard), and the DOCX target in one. annotation_export.py docx <run_folder> projects the gated manifest + snapshot into a .docx (Office Open XML — a ZIP of XML parts, stdlib-only: zipfile + hand-written XML, no python-docx): the manuscript text fills word/document.xml as one <w:p> per snapshot line, each finding's anchored span is wrapped <w:commentRangeStart/End w:id="N"/> + <w:commentReference w:id="N"/>, and word/comments.xml carries the verbatim comment — so Word and Google Docs render an anchored comment on the exact span. Firewall-clean by construction: a pure projection — the document text is the verbatim snapshot (XML-escaped via the same exact &/</> 3-entity pair as the HTML path, runs split only at comment boundaries), comments are verbatim, and the model assembles fixed OOXML boilerplate (7 parts incl. minimal styles.xml/settings.xml to avoid Word's repair-prompt) around copied bytes. The ZIP is byte-deterministic (ZIP_STORED; explicit ZipInfo per part with pinned date_time/create_system=3/external_attr; fixed order; seekable buffer; a w:date derived from the run date (the manifest runlabel at noon UTC — deterministic, not wall-clock, and no prior-day rollback in western timezones)) so a committed .docx fixture is byte-stable across machines. The new docx-export validator gates the on-disk artifact: D1 artifact integrity (the .docx equals a fresh deterministic build byte-for-byte — the authoritative lock for a binary, the HTML-H1 discipline), D2 text round-trip (the document.xml <w:t> text, exact-unescaped, one <w:p>/line + one trailing newline, reproduces the snapshot — the comment markup carries no body text), D3 comment resolution + fidelity (the commentRangeStart/End/Reference ids ↔ comments.xml ids form a bijection equal to the manifest finding set, and each comment is verbatim, keyed via the deterministic sorted(finding_id)→N map). Ships with the canonical example-annotated-manuscript/docx/ fixture wired into --check-all (byte-identical to a fresh export) and *.docx binary in .gitattributes. Validators 47 → 48. One-time maintainer acceptance: open the fixture in Word (no repair prompt) and import into Google Docs (comments land anchored) — OOXML on-paper validity doesn't guarantee either. PDF remains on the horizon; the manifest stays the one canonical artifact, every format (CriticMarkup, Obsidian, HTML, DOCX/GDocs) a projection.

Annotated-Manuscript export — self-contained read-only HTML (Increment 3)

The marked-up manuscript can now be exported as a self-contained .html openable in any browser — no Obsidian, no plugin, no Markdown tool. annotation_export.py html <run_folder> projects the gated manifest + snapshot into one HTML file: the manuscript in a faithful <pre class="manuscript"> (CSS white-space: pre-wrap + a serif face reads as prose while preserving the snapshot's exact bytes), with a footnote-style <sup id="ref-F-…"><a href="#fn-F-…">[F-…]</a></sup> marker at each anchor, and a <section class="findings"> listing each finding (<li id="fn-F-…">{verbatim comment} <a href="#ref-F-…">↩</a></li>) — so the browser gives native bidirectional anchor navigation, with embedded CSS and zero network refs. Firewall-clean by construction: a pure projection — the snapshot is HTML-escaped (&&amp; first, then </>), footnote markers are spliced between escaped prose segments at raw-snapshot offsets (escaping never touches a marker, so offsets never drift), and the new html-export validator gates it by identity: H1 round-trip (delete the manifest-keyed <sup id="ref-…"> markers + the exact 3-entity inverse unescape — not a general decoder — reproduces the snapshot byte-for-byte), H2 anchor resolution (<sup><li> bijection equal to the manifest finding-id set, catching an un-manifested marker/finding), H3 comment fidelity (each <li> equals the HTML-escaped verbatim comment + the exact back-ref). The validator reads and gates the on-disk html/ artifact. A <pre>-faithful view (not reflowed <p>/<h>, which would defeat the byte-exact round-trip) keeps the firewall proof exact; reflowed-prose HTML is a future increment. Ships with the canonical example-annotated-manuscript/html/ fixture wired into --check-all (byte-identical to a fresh export) and a hostile self-test (HTML metachars upstream of an anchor + literal &/< in a comment) that the metachar-free canonical fixture can't exercise. Validators 46 → 47. Google Docs is the next render target (DOCX/PDF on the horizon); the manifest stays the one canonical artifact, every format a projection.

Annotated-Manuscript Obsidian export — bidirectional letter cross-links (Increment 2)

The native-Obsidian export is now clickable both ways, with no plugin. Building on Increment 1's footnoted copy, annotation_export.py now also projects the Obsidian letter and wires the navigation: (a) each copy footnote definition gains a forward [[<letter>#^<finding-id>|→ letter]] wikilink (web-verified that wikilinks render clickable inside footnote definitions — the key contrast with CriticMarkup); (b) the Obsidian letter appends an Obsidian ^<finding-id> block id to each finding's line so those forward links resolve; (c) the gated crosslinked letter's CriticMarkup back-link spans convert in place to reverse [[<copy>#<heading>]] wikilinks, using the resolved snapshot heading text (Chapter 9) — never the manifest's normalized Ch 9 token — with a file-level [[<copy>]] link for line-range/quote/document anchors that have no addressable heading (W1). Heading-level reverse-nav is reliable because Obsidian's heading-anchor slug strips the footnote ref Increment 1 appended to the heading line (# Chapter 9[^F-RR-01] → resolves as Chapter 9, web-verified). Firewall-preserved: still a pure projection — the comment is carried verbatim (O3 now checks comment + the exact forward wikilink, no authored-text gap), and the letter's editorial prose is untouched: O5 proves stripping the Obsidian letter's additions (wikilinks + block ids) reproduces the same bytes as stripping the crosslinked letter's CriticMarkup spans (the X4 analog, two-sided precondition). O4 gates link resolution (every forward link → a real letter block id; every reverse link → a real copy heading, footnote refs stripped) — so a manifest-token-vs-heading-text mismatch fails at build. Extends obsidian-export (no new validator — still 46); the validator reads and gates the on-disk copy and letter. The canonical obsidian/ fixtures (copy + new letter) are wired into --check-all (both byte-identical to a fresh export). Scope stays Obsidian-only; read-only HTML → Google Docs are the next render targets, DOCX/PDF on the horizon. The manifest remains the one canonical artifact; every format (CriticMarkup, Obsidian, future HTML/GDocs/DOCX/PDF) is a projection.

Annotated-Manuscript Export — Obsidian native footnotes (no plugin)

The marked-up manuscript can now be opened in vanilla Obsidian, no plugin, with every finding as a clickable footnote. The new annotation_export.py projects the gated annotation manifest + snapshot into Obsidian-native Markdown: each finding becomes a footnote reference [^<finding_id>] at its anchor locus (quote → after the sentence; chapter/section → on the heading line; line-range → end of line; document → file-level), and its definition carries the verbatim manifest comment (which already includes the (See letter §id.) pointer). Obsidian renders footnotes natively — clickable superscript, hover preview, the core Footnotes View pane — whereas CriticMarkup {>> <<} shows as literal brace clutter without a community plugin; that (not anchor links, which are native) was the real obstacle. Firewall-clean by construction: a pure projection of the gated manifest — the reverse transform (strip the manifest-keyed [^id] refs + the trailing [^id]: definition block) reproduces the snapshot byte-for-byte (the A2 discipline, two-sided and manifest-keyed), and the comment is relocated verbatim, never re-authored. The new obsidian-export validator gates it: O1 round-trip to source (with a two-sided [^ precondition mirroring render/A2), O2 footnote resolution (ref↔definition bijection equal to the manifest finding_id set — the A4 forward+inverse multiset on footnotes, catching an un-manifested/authored footnote), O3 comment fidelity (each definition equals its manifest comment byte-for-byte). Ships with the canonical example-annotated-manuscript/obsidian/ fixture wired into --check-all (generate on a temp copy, gate, and assert byte-identical to the committed fixture — the producer-chain discipline). Validators 45 → 46. Scope: the annotated copy (Obsidian Increment 1); the Obsidian letter cross-links (block ids + back-link wikilinks using the resolved heading text) are Export Increment 2. The canonical render stays CriticMarkup — not the publishing-industry standard (Word Track Changes is), but the plain-text base whose no-mutation proof is cheap and which bridges to Word/Google-Docs comment mode via the Commentator plugin; Obsidian/HTML/GDocs/DOCX/PDF are all sibling projections of the one gated manifest.

Annotated-Manuscript Producer — evidence_quote Pass-5 pilot (Increment 2)

The character-precise quote rung is now live for Pass 5 (Character Audit) findings. Deliverable Increment 2 had shipped the consumer (the quote-locator + the A6 quote-integrity gate) but nothing populated evidence_quote; this increment makes Pass 5 attach it. When a Pass-5 finding is about a specific line (a flat-affect beat, a puppet-moment decision, a voice-drift sentence) the pass copies that line verbatim into the finding's evidence_quote, and the marked-up copy anchors the margin note at that exact sentence instead of the chapter. The discipline is written once in findings-ledger-format.md §"When to populate evidence_quote" (sentence-precision criterion; the copy-never-author firewall; location is by the quote's own bytes, not evidence_refs; single line only) and pointed to from the Pass-5 spec. Firewall-clean by construction: the build-time locator emits a quote anchor only when the span occurs in the snapshot verbatim and exactly once, so a non-verbatim, non-unique, multi-line, or fabricated quote simply degrades to the chapter/line rung and never reaches the margin; A6 is the validate-time backstop that rejects any forged quote anchor — a pass physically cannot smuggle authored prose. Demand-gated and per-pass: Pass 5 is the only adopter in this increment (Pass 8 reveal lines, the AI-prose flags, and citation-bearing nonfiction findings are deliberately deferred). Prose-only — no schema change (evidence_quote was already an optional apodictic.finding.v1 field), no new validator (count stays 43); the canonical --check-all fixture already proves the consumer end-to-end (F-QT-01 lights the rung; F-QAMB-01 degrades).

Annotated-Manuscript Deliverable — wired into the run flow (the producer, Increment 1)

A real Core/Full DE run now produces the marked-up manuscript + crosslinked letter, not just the validators on a hand-built fixture. At intake the manuscript is persisted as a frozen, LF-normalized snapshot — the deliverable's immutable left-hand side — for core-de / full-de runs (skipped for partial / fragment; an aborted run just leaves a benign provenance copy). At run-end, whenever the run wrote a full editorial letter (a *_Core_DE_Synthesis_* / *_Full_DE_Synthesis_* exists — a detectable artifact, not an abstract "workflow"; /ready writes one so it offers, partial / fragment / triage / audit do not), the orchestrator offers the deliverable and asks the author (a marked-up copy isn't always wanted). On yes it runs build → A1–A6 → render → X1–X4 staged in a temp copy, moving only gate-verified artifacts into the run folder — so the run folder is never left holding an unverified copy (verified-or-absent), the same temp-stage discipline the CI gate uses. The letter's finding-marker form is pinned to the canonical <!-- finding: F-… --> (a subset of what the honesty gates already accept, so nothing there changes) so the crosslink back-links resolve; the Severity-Calibration appendix keeps its distinct apodictic:severity_calibration form to avoid a second back-link. An existing un-annotated run folder is re-annotated through /start's diagnosed-node dispatch — conditioned on a no-annotated-copy glob, not a new next_action value — with no new command (so no registration burden). A new --check-all chain gate runs build→gate→render→gate on a temp copy of the canonical inputs and asserts the fresh build is byte-identical to the committed fixture (the fixtures are provably "what a fresh build emits"). No new validator; the count stays 43. Render-only, model-never-authors — every margin note remains a verbatim projection of a finding the passes already produced.

Annotated-Manuscript Deliverable — character-precise anchoring (Increment 2)

Margin comments can now anchor to the exact sentence, not just the chapter or scene. A finding carrying an optional verbatim evidence_quote (a manuscript substring) is matched against the snapshot; when it occurs verbatim and exactly once it anchors to that character span — a new quote rung, finest in the ladder — otherwise it degrades to the line/chapter ladder, never fabricating precision. The new A6 gate proves every quote anchor is a verbatim, unique manuscript span that the recorded offsets pin and that matches the finding's evidence_quote, closing the one new failure mode (a fabricated or mis-placed quote) the existing gates do not cover; the rendered margin is still only the comment (A5), so the firewall reduces to provenance-by-identity. The renderer is generalized to a unified character-offset splice (descending-offset insertion, byte-identical to Increment 1 for line/chapter anchors, so A2 still holds). evidence_quote is an optional apodictic.finding.v1 field — Increment-1 findings are unaffected, and no shipped pass emits it yet (the consumer + A6 ship now; populating it upstream is deferred). Single-line quotes only.

Annotated-Manuscript Round-Trip Re-Anchoring — reanchor

The marked-up copy is now revision-aware: when the writer revises, reanchor (scripts/reanchor.py; validate.sh reanchor <prior_run_folder> <new_snapshot>) carries draft N's margin annotations onto draft N+1 by re-resolving each anchor against the new snapshot, and classifies what happened — held (same locus), moved (a quote whose verbatim+unique text is now at a new offset), vanished (anchored prose gone — a candidate the finding was addressed), ambiguous (the span now occurs more than once — re-anchor refused), or not-re-anchorable (a bare line-range carries no text to search). This is the anchor/text-level complement to regression-diff's finding-level cross-round diff: a vanished anchor is hard evidence a finding was addressed; a quote that persists verbatim is hard evidence it wasn't. Firewall-clean by construction: re-anchoring is pure text search over the new snapshot — a re-anchored quote's offset is recomputed against N+1 by the same A6 identity that built it (verbatim + exactly once, else it degrades; the offset is never carried forward), and the margin comment is carried over byte-identical (RA2), never re-authored. The re-anchored manifest (held/moved only, bound to N+1) is gated by the existing A-machinery: a new ledger_optional mode on annotation_manifest.check runs A1 + A2 + A3 + A4-multiset + A6 against N+1 without requiring a re-diagnosed Findings Ledger (its A4/A5 ledger arms are legitimately absent — vanished Must-Fixes go to the re-anchoring report, and RA3 partition-completeness guarantees none is silently lost). The reanchor validator raises RA1 re-anchor integrity, RA2 comment fidelity, RA3 partition completeness (hard errors — the mechanical contract) and W1 candidate-resolved / W2 re-anchor refused (advisory, ERROR under --strict — the regression-diff posture). It prints to stdout (the diff-validator precedent); a human-readable re-anchoring report is orchestrator-written. Ships with a revised-draft fixture (example-reanchor-revised.md) wired into --check-all that exercises all five classes against the canonical manifest. Validators 44 → 45. This completes the deliverable's revision-awareness, building on draft-over-draft regression.

Annotated-Manuscript Deliverable (Increment 1)

The editorial letter's findings, now anchored in the margin of the manuscript itself — the third leg of the standard developmental-edit deliverable set (letter + book map + marked-up copy), and the one the framework did not previously produce. A new annotated-manuscript validator + apodictic.annotation.v1 manifest schema + CriticMarkup renderer (scripts/annotation_manifest.py, with build / render / validate modes) produce a comments-only marked-up copy over an immutable, LF-normalized manuscript snapshot: each margin note is a verbatim projection of an apodictic.finding.v1 field set, anchored by a four-rung ladder (line-range / section / chapter / document) that never fabricates precision. The no-mutation guarantee is mechanical — deleting every {>> … <<} span reproduces the snapshot byte-for-byte — and every body Must-Fix must reach the copy as a rendered span (A4). Firewall-bound: comments only, never tracked changes or suggested prose. Chapter-reference normalization is now shared (apodictic_artifacts.chapter_token) so the annotation and visualization surfaces bin chapters identically. Self-testable validators 41 → 42; --check-all gates a canonical snapshot + manifest + annotated-copy fixture. Promoted from Horizon Capacities Tier 1 and shipped the same day. See docs/annotated-manuscript.md.

Nonfiction Pre-Draft

Added the genre layer (Increment 5) to the Nonfiction Pre-Draft Pathway — a pre-draft scaffold that holds a genre to its genre-required argument structure. A new single-sourced apodictic.genre_profile.v1 schema names the genre (grant-proposal / academic-article / pitch-deck), the genre-required section roles it must seed, and the evaluator it is written for. The existing argument-spine validator gains B1–B4 + W4 (B1 invalid genre profile, B2 a declared section unseeded — the signature, B3 genre/form mismatch with normalized comparison, B4 duplicate profile, W4 thin canonical skeleton, overridable via argument-spine-genre) over the new block — no new validator; the validator count is unchanged (the genre layer rides argument-spine, like Increments 1–3). B2 matches the declared heading against a markdown heading's full text (modulo a trailing colon / whitespace), so a declared section is not seeded by an unrelated heading that merely contains its text as a substring. Three canonical worked examples (grant / academic / pitch) are gated by validate.sh --check-all under --strict. Dialectical Clarity's Genre & Audience Calibration gains a Pitch Deck entry and a Reviewer-Anticipation Lens (panel / peer / investor), and the form enum gains "pitch deck". The Firewall holds: the engine validates the writer's declared structure and surfaces which genre-required section is missing or unseeded; it authors no aims, contribution claim, objection text, or traction numbers, and the pitch-deck entry coaches no design or fundraising tactics.

Workflows — Cross-Manuscript Author Voice/Craft Fingerprint (the writer's signature over a career)

A persistent profile that accumulates across the works an author collects under one author-root and surfaces movement — drift, range/growth, unconscious self-imitation, signature tics. APODICTIC measures voice within a manuscript (Pass 11; the AI-prose personal-baseline z-scores) but had no memory across works; this is that memory. It does no new stylometry — each apodictic.voice_fingerprint.v1 block consumes the single-voice AI-prose machinery (SETEC voice_profile/voice_distance + the personal-baseline z-scores via centroid_ref) and the module adds only the persistence-and-longitudinal-diagnosis layer. Persistence follows the Series_State.md convention exactly: the Author_Voice_Profile.md lives at an operator-designated author-root (no cross-project or machine-global state, no auto-discovery — "across works the author collects into one author-root," not "automatically across all your books"). It is descriptive (observations, not verdicts — "intended departure, or drift?", never "fix your voice"), carries no Must/Should/Could severity (a fingerprint is not a defect), and is local-only (no external call, never transmitted). The new author-fingerprint validator (scripts/author_fingerprint.py) enforces F1 schema (incl. a non-empty, scalar-valued metrics — the retcon_plan.scores nested-object precedent), F2 provenance (each fingerprint cites a source + centroid_ref naming a consumed audit output — presence-checked, since the external value can't be re-resolved), F3 same-register comparison (a drift/range claim referencing ≥2 fingerprints must share a register — the AI-prose domain-shift guard), plus advisory F4 descriptive-not-prescriptive (no editorial severity token, no "fix/vary your voice" directive; ERROR under --strict; per-id override fingerprint-frame), W1 insufficient data (no register has ≥2 fingerprints → seed-only), and W2 local-only hygiene (a missing local-only marker or an external URL — advisory WARN only, never gate-blocking: a marker scan can't prove a runtime call, so the binding guarantee stays the module's no-external-call rule). The POV Voice Profile (which fits per-POV-character and refuses single-POV work) is not the author centroid source — the single-voice fit is; POV is kept only for the optional protagonist-collapse sub-diagnostic. Ships the canonical example-author-voice-profile.md (two literary-fiction fingerprints, one within and one just outside the same-register band, with a same-register drift observation) wired into --check-all under --strict. Self-testable validators 51 → 52. Spec: docs/author-voice-fingerprint.md.

Tooling / DX — Codex README brought into the registry-generated path

README.codex.md's capability counts were the one inventory surface still hand-maintained: release-generate.mjs regenerated the Claude README.md count lines from release-registry.json, but the Codex README sat outside that path and had drifted to 34 available audits (16 craft) while the registry, plugin description, and Claude README all read 35 / 17 craft (bump-version.sh only ever updated its version callout). release-generate.mjs now also generates + --check-gates the three README.codex.md count lines — the Specialized-Audits line, the "list all N available audits" line, and the Framework-Version capabilities sentence — reusing the existing Claude-README builders, with a Codex-specific capabilities builder that regenerates only the Capabilities: … pipeline. span and leaves the bump-version.sh-owned "Current Codex manifest version is `X`." callout untouched (new paths.pluginReadmeCodex). This corrects the stale 34/16 counts to 35/17 and folds the Codex README into the CI drift gate (release-generate.mjs --check), closing the drift class rather than hand-patching one number. Resolves the capability-inventory finding from the 2026-06-19 capability audit.

Workflows — Content-Advisory / Sensitivity-Surface Derivation (what's depicted, where, how intensely)

A new specialized-audit deliverable that derives a reader/marketing-facing content advisory — a map of where the manuscript depicts intense material (violence, sexual content, self-harm, abuse, …), at what intensity, on- or off-page — for front-matter notes, retailer content-warning metadata, sensitivity-reader handoff, or the author's own awareness. APODICTIC's existing audits assess such content for craft and harm risk; none derived the descriptive, reader-facing artifact. This does: pure extraction over depicted content, anchored to loci, as apodictic.content_note.v1 blocks (category ∈ a closed nine-value taxonomy; intensity low/medium/high; depiction on-page/off-page/referenced; loci ≥1). It is opt-in by design — generated only under a <!-- content-advisory: opted-in --> marker (content warnings are contested; an unbidden one imposes a stance) — and descriptive, never evaluative: a note records that content is depicted and how, never that it is "gratuitous" and never that it should be cut. A content note is not a defect — it carries no Must/Should/Could severity; its intensity scale is orthogonal to the editorial scale, the Legal Risk Register precedent. The new content-advisory validator (scripts/content_advisory.py) enforces A1 schema (incl. the other→non-empty-label conditional the subset engine can't express, and non-empty loci elements), A2 locus presence/shape (the continuity-bible C2 shape), A3 no editorial-severity leak (no Must/Should/Could token in the prose or a label, and no apodictic:finding block in the advisory — the orthogonal-severity firewall), plus advisory W1 prescriptive drift (a "should cut/soften/tone-down …" construction — a modal/recommend verb governing a revision action, deliberately not bare descriptive adjectives like "excessive blood loss"; ERROR under --strict; per-id override advisory-eval) and W2 opt-in marker. If no advisory artifact is resolved it no-ops (exit 2), like legal-risk. The Increment-1 consume-don't-duplicate against Reception Risk / Consent / Erotic is prose-citation only (those audits emit no addressable per-instance IDs — the structured coverage diff is deferred until they do). Module homed in specialized-audits (alongside Reception Risk); the canonical example-content-advisory.md (an opt-in advisory with four notes across categories) lives in core-editor/references and is wired into --check-all under --strict. Self-testable validators 52 → 53. Spec: docs/content-advisory.md.

Workflows — Auto-Derived Continuity Bible (the narrative half of a style sheet)

A new core-editor deliverable that consolidates the canonical facts a manuscript has committed to — the narrative side of the editorial style sheet a human developmental editor returns. APODICTIC already scatters continuity across the Timeline, the SFF Rule Ledger, and the Pass-5 portraits; the Bible gathers the narrative half into one locus-anchored reference (Cast, Places, Objects, World Rules, Chronology) and, as a side effect, a Contradiction Ledger of the places the text commits to two facts at once. Each fact is a first-class apodictic.canon_fact.v1 block (category ∈ person/place/object/world-rule/chronology; value always a quoted string so numerics type-check; loci ≥1; consolidates pointing at a Timeline scene id when the fact is consumed rather than newly extracted). It is firewall-bound: it records stated facts only (never inferring an unstated one, never filling a gap), and surfaces contradictions, never resolves them — both conflicting values are catalogued and paired, choosing canon stays the author's call. The net-new territory is the grab-bag no single artifact owns — stated identity/physical facts (ages, spellings, aliases), named objects, and place details below the Timeline's setting column — so the Bible consumes and cites the surfaces the framework already produces rather than re-deriving them. The new continuity-bible validator (scripts/continuity_bible.py) enforces C1 schema (bad category enum, malformed CF-NN id, missing field, unquoted-numeric value, empty loci, duplicate id), C2 locus presence & shape (a coarse chapter/§/¶/line/page token — a well-formedness precondition, not a firewall proof; locus resolution into the manuscript is deferred to the shared snapshot layer), C3 contradiction integrity (a ## Contradiction Ledger row must pair ≥2 real canon_facts sharing entity+attribute but asserting different values — a bespoke markdown-table parse), plus advisory C4 chronology consume-vs-rederive (a chronology fact that doesn't consolidate to a real Timeline scene id; override <!-- override: bible-rederive CF-NN — … -->) and W1 coverage (a Timeline POV with no Cast entry, or a Timeline setting with no Places entry), both ERROR under --strict. C4 and W1 resolve against the project-root Timeline.md passed as a second input; Increment 1 sources character coverage from the Timeline POV column (the machine-readable name source) rather than the prose Pass-5 portraits, which have no machine-readable artifact yet. Ships with the canonical example-continuity-bible.md fixture (a Cast/Places/Objects/Chronology Bible consolidating example-timeline.md, with a real Mara-age contradiction) wired into validate.sh --check-all under --strict. Self-testable validators 49 → 50. Homed in core-editor; spec: docs/continuity-bible.md.

Infrastructure — Crash-resistance for malformed block payloads (the non-P1 sweep batch)

Follow-up to the validator-fleet hardening: the 2026-06-20 sweep's recurring crash-on-malformed class — a parse_*/check path reaching obj.get() (or .lower(), or char-iterating a string) on a valid-but-non-dict JSON block payload ([1,2,3], "str", 42, null-comment) and raising an uncaught traceback — is closed across the eight remaining (non-P1) sites the sweep confirmed (the four P1 sites were fixed earlier):

  • viz_manifest (manuscript-viz) — a non-object manifest block now reports a controlled E1 failure instead of crashing in check() (and never passes vacuously).
  • annotation_manifest (annotated-manuscript) — same, a non-object manifest block → controlled A1 failure.
  • crosslink_anchor_str tolerates a non-dict anchor (covers the X2 back-link path).
  • reanchorreclassify guards a non-dict annotation / non-dict anchor.
  • registry_check — a non-object project sidecar is treated as unparseable drift (R3), not a crash.
  • annotation_export (obsidian-export) — the footnote-definition sort filters non-dict entries before sorting, and the O3 comparison tolerates a null comment.
  • editor_scaffolding_read tolerates non-UTF-8 bytes (clean "cannot read" / exit 2, not a UnicodeDecodeError traceback).
  • regression_diff_mech_tokens coerces a non-string mechanism before .lower(), and _chapter_of guards a non-list evidence_refs (which was silently char-iterated, demoting a new-in-quiet-chapter W2 candidate to a plain new with no warning — a silent-loss bug, not just a crash).

Each fix mirrors apodictic_artifacts.validate_obj's own isinstance(obj, dict) guard, applied identically to both byte-identical script copies. Locked with direct-unit regression self-tests for reanchor, regression-diff, crosslink, manuscript-viz, editor_scaffolding (non-UTF-8 read), and registry_check (non-object sidecar). Crash-resistance is not statically gateable by the validator-conventions M1–M4 checks, so each site carries its own self-test lock.

A second, distinct crash class in the same sweep: a malformed finding_id (or ledger id) used as a dict KEY or a SORT key — a non-hashable id (a JSON list/object kept as-is by parse_*) raises unhashable type, and a mixed int/str id set raises sorted()'s '<' not supported between instances. The four P1-era sites (the annotation A1 uniqueness map) were fixed earlier; this batch closes the remaining sibling sites and centralizes one normalizer — apodictic_artifacts.fid_key (the lowest shared lib, so finding_trace/annotation_*/reanchor all route through it without a circular import; a valid string id is unchanged, a malformed id is str()-coerced — matching the "[^%s]"-rendered footnote ref so the multiset/provenance join still ties):

  • reanchor_fid normalizes the finding_id at build_reanchored (re-anchor map + re-annotation), the orig-comment lookup, and the comment-fidelity check.
  • annotation_manifest — the A1 uniqueness seen map, the comment-provenance led_obj lookups, the manifest_ids set, and the ledger-side index led_obj[o["id"]] (a malformed ledger finding id) all coerce through fid_key (an art-None-safe local mirror, since its apodictic_artifacts import is best-effort).
  • annotation_export — both build_obsidian sorts (the offset/fid ref sort + the definition-block sort) and the check_obsidian O2 manifest_set + O3 comment_of dict keys.
  • finding_trace / intake_interview / reader_instrument / viz_manifest — the four ledger_*-index functions (ledger_inventory, ledger_index, ledger_index, ledger_findings) all key the authoritative-ID set by a raw obj["id"] (the byte-identical pattern); each now routes that key through fid_key, so a malformed ledger finding id no longer crashes the index (reachable via each validator's public entrypoint — e.g. viz_manifest.check / reader_instrument.check / intake_interview.interview).

regression_diff already self-handled non-string ids (it skips them). Locked with non-hashable + mixed-type id regression self-tests (a list/mixed manifest finding_id, a non-hashable ledger id, and a non-hashable-id no-crash test on each of the four ledger_*-index functions), each raising against the pre-fix code.

Tooling — cross-platform (Windows) gate parity

The validator self-tests and host generators now pass on a non-UTF-8 / CRLF (Windows) checkout, not just on Linux CI — so a maintainer can run the real gate (bash scripts/validate.sh --check-all) locally and trust the result. Three checkout/locale artifacts were closed: (1) encoding="utf-8" is pinned on all 70 text-mode fixture/output writers across the validator suite, so a cp1252-default box no longer writes an em-dash as byte 0x97 that the UTF-8 reader then chokes on (bash scripts/validate.sh --self-test-all is 48/48 without PYTHONUTF8); (2) .gitattributes gains * text=auto eol=lf so every checkout stores and emits LF — fixing release-generate.mjs --check's \n-anchored README match and future-proofing the byte-identical dual script mirror against EOL drift (zero renormalization: all tracked blobs were already LF); and (3) build-codex.mjs normalizes path.sep/ before its allowlist check, so the parity scan no longer false-positives on Windows backslash paths. No change to any diagnostic output.

Draft-over-Draft Structural Regression Testing — regression-diff

A revision can no longer quietly lose ground. The new regression-diff validator (scripts/regression_diff.py; validate.sh regression-diff <prior_run_folder> <this_run_folder>) diffs the whole Findings Ledger across two revision rounds — did this revision resolve what it claimed, and did it break anything that was working? — a signal no single-round diagnosis can produce. Because finding IDs are unique per run (renumbered each round, no stable cross-round identity), cross-round matching is a deterministic heuristic: a current finding matches a prior one iff same origin code and equal chapter token (apodictic_artifacts.chapter_token) and ≥1 shared mechanism token (greedy, stable, one-to-one) — so every regression signal is a candidate for editor judgment, never a mechanical verdict. Classification: persisted / resolved-and-held / recurrence-candidate / new / new-in-quiet-chapter. The validator raises R1 round-linkage (ERROR — both ledgers parse, non-empty, distinct rounds: the one mechanical invariant), W1 recurrence-candidate (a finding the prior round marked <!-- resolved: F-… --> that re-appears — the fix may not have held; reverts to the prior round's severity on confirmation), W2 new-in-quiet-chapter (a finding in a chapter the prior round left quiet — candidate fix-induced breakage; override <!-- override: regression-cleared <runlabel>:<chapter> — investigated, not fix-induced -->), and W3 unexplained-drop. W1–W3 are advisory (re-diagnosing a changed manuscript legitimately shifts findings), ERROR under --strict — the finding-trace / state-card-diff posture; only R1 is a hard error. It reuses the shared primitives (apodictic_artifacts.parse_blocks + chapter_token, finding_trace.resolved_cited_ids, the state_card_diff.resolve two-snapshot scaffolding), prints to stdout like the sibling diff validators (the human-readable Regression Report is orchestrator-written at round-close, integrated in state-lifecycle.md §Cross-Round Regression Check), and ships with a paired two-round fixture (example-run-folder-r1/, -r2/) wired into --check-all (asserting round linkage plus the recurrence + quiet-chapter candidates under --strict). Validators 43 → 44. This is the dependency for round-trip re-anchoring of the annotated manuscript.

Harness Engineering — Harness Contracts v2 (schema-coverage gate + closed-key enforcement)

The completion increment of the Harness Contracts v2 roadmap line: the schemas already are the single source of truth for every structured artifact and each is bound to a validator, but nothing forced them to stay the only source. This ships the drift-kill keystone. A new schema-coverage validator (scripts/schema_coverage.py) proves disk reality matches a declarative schemas/_coverage.json binding table at --check-all: C2 no orphan schema (every apodictic.*.schema.json on disk is bound), C3 no phantom binding (every row's schema exists), C4 binding proven not asserted (the schema id literal is grep-reachable in a .py the bound dispatch arm delegates to — the "distrust count-shaped claims" discipline mechanized), C5 canonical-gate reachability (the exact canonical filename token appears in --check-all and the file exists, or an auditable self-test-only escape for the two registry schemas), plus C1 manifest validity, C1' closed-key table↔file agreement, and advisory W1 dead-exclusion (ERROR under --strict). The shared engine (apodictic_artifacts.py) now honors additionalProperties:false (opt-in per schema) — the misspelled-field kill — and the six flat const-tagged blocks (audit_trigger, readiness, severity_calibration, feedback_item, reader_question, scene_ethics, all of whose optional fields are declared) are closed; finding.v1 (forward-extended per pass), the sidecar, and gate_event stay open by design. A new schema_field_names() helper backs an advisory --check-docs prose lint that flags a field-set re-listing (a Fields:-labelled line) on a defer-marker doc that names a field the schema does not declare — catching divergence, not list-existence, and only on opt-in docs. The manifest dogfoods the discipline: it validates against two new schemas (apodictic.schema_coverage.v1, apodictic.schema_binding.v1), both themselves additionalProperties:false. execution-gates.v1.json is correctly not treated as an artifact (its name isn't *.schema.json, so the orphan glob already skips it — and listing it in non_artifact[] would trip W1). Wired into validate.sh --check-all against the real schemas/ dir (C2/C5/C1' only have teeth against disk reality), with hermetic hostile-fixture self-tests for every check. Self-testable validators 50 → 51 (derived from AGG_VALIDATORS, re-enumerated not asserted). No model seam — pure stdlib harness mechanics; the Firewall is untouched. Spec: docs/harness-contracts-v2.md.

Workflows — Interpretable Stylometric Explanation (descriptive style-feature labels over the voice fingerprint)

A descriptive labelling layer ON TOP of the Author Voice Fingerprint (#9). #9 measures how distinctive an author's voice is and persists it as scalar z-scores; a writer reading that profile learns that their voice sits 0.7 from their author centroid but not which measured features carry that distance, in plain language. This overlay attaches a natural-language gloss to a handful of the salient measured features — "the prose leans hard on the definite article", "function-word profile concentrated in the / and / but" — each bound by provenance (feature_ref) to the exact SETEC voice_profile feature it describes (families.<family>.top_features[].{feature, mean, sd, cv}). It does no new stylometry (it consumes the same per-family inventory #9 consumes) and offers no advice: it names a measured feature, it never prescribes a voice change and never fabricates a style claim. Rooted in Latent Space Interpretation for Stylistic Analysis and Explainable Authorship Attribution (arXiv:2409.07072), whose method attaches LLM-generated natural-language style descriptions to anchor points in a style-embedding space — this build ships the firewall-safe descriptive slice (the labels + their binding to a measured feature; the embedding/LLM that generates them is a deferred M2 seam).

This is the riskiest of its wave — one preposition from a Firewall breach — so the result schema is built so a "write more like X" directive is unrepresentable: there is no recommendation/target/goal/rewrite/compare_to_author field, frame and direction are closed descriptive enums (the indicative "elevated use of the definite article", never the imperative "elevate your diction"), and register is a comparability class, never a target author to emulate. The one residual free-text surface (label) is scanned by the validator's signature X4 gate.

The new style-explanation validator (scripts/style_explanation.py + validate.sh style-explanation) enforces X1 schema (apodictic.style_label.v1: closed feature_family/frame/direction/magnitude enums, SL-NN id, unique ids), X2 provenance / anti-fabrication (every label cites a non-empty feature_ref into a consumed measurement — an un-sourced label is fabricated; the #9 centroid_ref discipline per-feature), X3 no-severity-leak (no Must/Should/Could token in the prose or a label, no embedded apodictic:finding block — a style label is not a defect), the signature X4 descriptive-not-prescriptive (a prescriptive voice-directive OR a first-class comparison-to-emulate match — write/sound more like …, emulate …, model your prose on …; advisory, ERROR under --strict; per-id override style-frame SL-NN, prose-level override the bare style-frame), X5 same-register cluster (a describes-cluster label referencing ≥2 labels must share a register — the AI-prose domain-shift guard), advisory X6 local-only hygiene (a missing local-only marker or an external URL — WARN only, never gate-blocking), and W1 seed/coverage (a single glossed feature is a thin overlay; no blocks no-op). The adjective-ambiguous direction words (reduced/lower/elevated) read as indicative unless they govern a possessive object ("reduce your reliance …"), so a bare descriptive label never trips X4.

It does not duplicate #9: #9's unit is one work's position in stylometric space (a scalar metrics map, validator author_fingerprint.py); this unit is one feature's natural-language gloss (a label string + closed enums, validator style_explanation.py). They compose — both block types can coexist in one Author_Voice_Profile.md, or the labels can live in a sibling Author_Style_Explanation*.md — but neither subsumes the other. The module is descriptive, local-only, off the editorial severity scale, and ships no model dependency: the label-generating embedding/scoring model is a deferred M2 lazy-import + skipif seam, and the M1 validator never calls a model. Ships the canonical example-author-style-explanation.md (a local-only profile with three same-register function-words labels forming one coordinating cluster plus one out-of-cluster punctuation label) wired into --check-all under --strict. The self-testable validator count is derived from validate.sh's AGG_VALIDATORS list (no hand-maintained number). Spec: docs/interpretable-stylometric-explanation.md.

Workflows — Manuscript-Structure Visualizations

Chart 7-nonfiction — the claim ladder — now ships render-only (Manuscript-Visualization
Completion, M1). The apodictic.viz_manifest.v1 manifest gains four OPTIONAL, additive arrays
(co_presence / scene_functions / reveal_points / claim_ladder); the M1 render-only chart is
the claim ladder over apodictic.argument_spine.v1 (C0 thesis + C1…Cn subclaims) annotated with
support coverage from apodictic.support_plan.v1 (support type + in-hand/to-acquire, or a
bare-assertion marker). The manuscript-viz validator gains gates X1 (new-array schema + the
no-scene-axis firewall — a scene_ids/scene_id/section key on a claim_ladder[] object is a
hard failure), X5/X6 (claim-ladder provenance — claim_id resolves via the reused
argument_spine.spine_subclaim_ids() leading-Cn-token parser, label byte-equals the subclaim
string minus its Cn: token, each support[] item byte-copies a real support_plan.v1 block),
X7 (no duplicate rung), X8 (producer-present — no producer, no chart), and W3 (chart
coverage). No new schema and no new validator — the validator count stays DERIVED. Charts
4/5/6/7-fiction remain producer-gated; a claim-to-scene overlay is out of scope (no subclaim→location
producer exists). The canonical --check-all Structure Map gate now exercises the claim ladder
against the pre-draft Argument_State spine. Render-only and firewall-clean: no renderer or validator
in this layer calls a model.

Validators — POV Voice Profile §4e Must-Fix propagation validated

The Must-Fix row of the five POV Voice Profile rows in pass-dependencies.md §4e (Audit-Signal Propagation Table) is promoted from provisional to "Validated 2026-06-19 (audit-signal-propagation self-test + multi-POV fixture)." The audit-signal-propagation self-test now exercises it directly: four in-code cases in letter_checks.py run_self_test (pov_propagated_clean, pov_unpropagated_errors, pov_override_body_no_error, pov_override_body_warns) drive audit_signal_propagation(text) with a believable multi-POV editorial-letter snippet (Mara/Jon + Mara/Elen pairs) whose voice-collapse-on-2+-pairs + Blind-Swap-fail signal is expressed as a Must-Fix floor — the §4e Must-Fix row's signal class — under a ### POV Voice Profile Audit appendix subsection the validator parses as audit POV Voice Profile (slug pov-voice-profile). The cases assert: (positive) the Must-Fix floor propagating to a body Must-Fix via audit-name reference + shared evidence line L1840 is clean; (negative) the same signal unpropagated ERRORs; (override) the negative case plus a body <!-- override: audit-propagation-must-fix … --> marker yields no ERROR and one WARN. The validator's logic is unchanged — coverage only. Scope is honest: the propagation validator enforces canonical propagation of a strong audit-internal signal (hard gate / Must-Fix floor / HIGH) to the mandated synthesis tier, and has no mechanism to derive or assert a Should-Fix / Could-Fix tier or a downshift. The other four rows — the single-pair Should-Fix, the Blind-Swap-pass Could-Fix, and the two downshift modifiers (threshold-confidence and POV-mapping-provenance) — therefore remain provisional documented editorial conventions (not fixture-validated, the same posture as the Narrative-/Argument-Decision rows), tunable on a live multi-POV manuscript run.

Workflows — Promise-Contract Fidelity (does the pitch keep the promise the book makes?)

A new core-editor workflow module that points APODICTIC's contract-inference move at the author's own marketing copy — query, synopsis, blurb, logline — diagnosing the document-level fidelity gap between the persisted pitch and the inferred Contract: emphasis distortion (PCF1), reveal leak (PCF2), over-promise (PCF3), under-sell (PCF4), and cross-document inconsistency (PCF5). It consumes Shelf & Positioning (genre/comp/tone) rather than re-deriving it, and is firewall-bound: it flags the gap and a class of repair, and never drafts the replacement copy (Shelf & Positioning's Reframe Protocol owns rewriting — this is its diagnostic-only sibling). The pitch copy becomes a first-class persisted input, apodictic.pitch_copy.v1 (a [Project]_Pitch_Copy_[runlabel].md block per document, declared copy_type + verbatim text); persisting it is what turns the firewall guard into a substring check and the disclosure gate into a mechanical one. Findings are apodictic.finding.v1 blocks with origin PCF (F-PCF-NN), and adopt a PCF-scoped namespaced evidence-ref conventioncopy:<copy_type>¶<n> / contract:<FIELD> / ms:<locus> — so the two-sided gap is machine-checkable (the shared schema is unchanged for every other origin). The new promise-contract validator (scripts/promise_contract.py) enforces P1 two-sided gap (every F-PCF finding cites ≥1 well-formed copy: ref — the full copy:<copy_type>¶<n> grammar whose copy_type is actually persisted — AND ≥1 contract:/ms: ref; a locus-less, malformed, or dangling copy ref is no evidence and cannot satisfy the gate — the signature integrity check), P2 pitch copy persisted & typed, P3 reveal-leak form gate (a PCF2 against a synopsis is a calibration error — a synopsis discloses by design), plus advisory W1 drafted-copy leak (a multi-sentence quoted block in the report that isn't verbatim persisted copy — the Firewall; overridable per id) and W2 market-prediction drift (the prohibited sales-prediction phrase set — the #14 boundary; overridable), both ERROR under --strict. Resolved decision OD1: CONTROLLING IDEA is promoted to a first-class colon-delimited Contract schema field (alongside READER PROMISE / ENDING TYPE / NON-NEGOTIABLES) so PCF1/PCF4 resolve it directly. Ships with the canonical example-promise-contract.md fixture (a query committing PCF1+PCF4 with valid two-sided refs, and a disclosing synopsis that proves the P3 negative) wired into validate.sh --check-all. Homed in core-editor; spec: docs/promise-contract-audit.md.

Validators

48 → 49 self-testable validators. Added promise-contract (Promise-Contract Fidelity: P1 two-sided gap, P2 pitch copy persisted & typed, P3 reveal-leak form gate, W1 drafted-copy firewall leak, W2 market-prediction drift), with its own --self-test and a canonical --check-all gate over example-promise-contract.md.

Workflows — Reader-Persona Simulation (where the book lands differently by audience)

A new specialized-audit overlay on Pass 1 (Reader Dynamics): Pass 1 maps the experience of one composite reader, but a manuscript meets an audience with different tolerances. Persona simulation runs the same reader-experience lens through several declared reading dispositions and surfaces where the predicted experience diverges — and divergence is the diagnostic signal, exactly as contract-mismatch is. It sits on the sharpest firewall edge in the Horizon scan (the non-viable Simulated Reader Focus Group, item 17, which fabricates reader reactions as data), so the boundary is enforced by three mechanical guards, not rhetoric: a persona is a closed-key disposition vector, never an invented character (D5, a non-overridable ERROR — the real guarantee, since the subset engine silently allows unknown keys); every divergence is grounded in a real finding or Timeline locus (D2, the signature gate — an ungrounded prediction is a fabricated one); and a first-person reader-reaction quote presented as data is scanned (D4). A persona is an apodictic.persona.v1 block of disposition axes only — pace_tolerance / genre_familiarity / content_sensitivity / thematic_receptivity (the verbatim Argument-Engine Audience.Receptivity enum) / continuity_attention — plus id and a boolean target. Divergence is an apodictic.divergence.v1 block: an anchor, a nested per-persona experiences map (engaged/neutral/friction/disengage), a magnitude, and an optional asserted_severity. Severity honesty is preserved against persona-shopping (softening a real defect by finding some disposition for which it "works"): severity is locked against the target persona, so a divergence whose asserted_severity is lower than the anchored finding's locked Ledger severity fails D3 — "works for the expert, fails for the newcomer" is recorded as divergence, but if the newcomer is the target it stays a defect at full severity. The new persona-divergence validator (scripts/persona_divergence.py) enforces D1 schema (incl. the nested experiences enum and that each key is a declared persona — the retcon_plan.scores nested-object precedent), D2 grounded prediction, D3 target-severity anchoring (exactly one target:true; no downgrade), plus advisory D4 anti-fabrication (ERROR under --strict; override persona-quote D-NN) and W1 coverage (≥2 personas with a varying disposition axis); D5 is a non-overridable ERROR. Homed as a Pass-1 overlay in specialized-audits/references/craft/; ships the canonical example-persona-divergence-map.md + example-persona-divergence-ledger.md (a target newcomer and a genre-expert diverging on a locked Must-Fix Ch-3 lull) wired into --check-all under --strict. Self-testable validators 53 → 54. Spec: docs/reader-persona-simulation.md.

Round-Trip Re-Anchoring — the workflow glue (Increment 2)

The reanchor validator (Increment 1) classified held / moved / vanished / ambiguous / not-re-anchorable but only validated in memory — it never produced the revision-aware marked-up copy a revision loop needs on disk, and it left the anchor-level × finding-level join unwired. This increment closes the round-trip into a real workflow (the ROADMAP "Toward truly great" item #2):

  • reanchor.py emit <prior_run_folder> <new_snapshot> [-o <out_dir>] — re-anchors draft N's margin notes onto the revised draft N+1 and writes [Project]_Reanchored_Manifest_[runlabel].md + [Project]_Reanchored_Annotated_Manuscript_[runlabel].md (held/moved only, each comment carried byte-identical — relocate, never re-author). It re-gates RA1–RA3 before any write (refuses to emit an unverified re-anchor); the Reanchored_ infix keeps a carried-over copy distinct from a re-diagnosed one and out of the *_Annotation_Manifest_* glob.
  • reanchor.py crossref <prior_run_folder> <new_snapshot> <this_run_folder> — joins the round-trip's per-annotation classes against regression-diff's per-finding classes by finding_id (the §Q2 orchestrator join; regression_diff.crossref_classes returns the prior-keyed class map the re-anchored manifest shares). It corroborates the heuristic regression signal with anchor ground truth: vanished × resolved-and-held (the fix landed) and held/moved × recurrence-candidate (the fix did not hold — X1, advisory / ERROR under --strict).
  • Revision Round Protocol wiringstate-lifecycle.md gains a §Round-Trip Re-Anchoring step (snapshot the revised draft → classify + emit the revision-aware copy → cross-reference at round-close), the anchor-level sibling of the existing Cross-Round Regression Check.
  • --check-all round-trip glue chain gate — exercises emit → A-gate the emitted copy (ledger-optional) → crossref end-to-end on a temp copy of the canonical fixture, so the flow is proven to compose, not just the individual validators.

No new validator and no new command — this increment adds zero entries to AGG_VALIDATORS, so it leaves the validator count exactly where main already has it (the count is derived from AGG_VALIDATORS by validate.sh, never a hard-coded literal). On the current main that count is 50 (continuity-bible, PR #123, landed after this branch was cut), so the post-merge count stays 50 — this PR does not change it. The glue is two subcommands on the existing reanchor.py + one on regression_diff.py, both mirrored. Spec: docs/annotated-manuscript-reanchoring.md (Increment 2).

Onboarding — annotated-manuscript sample + host-positioning

"See It in Action" now includes a live annotated-manuscript sample (sample-annotated-manuscript.html) — the marked-up copy with severity-tagged, bidirectionally-linked margin findings, rendered from the canonical example-annotated-manuscript/ fixture via annotation_export.py html — so the annotated-manuscript deliverable (the standard "#1 human-DE deliverable") has a no-install showcase beside the editorial-letter, targeted-audit, and pre-writing samples. The install section's "Claude Code / Cowork (legacy host flow)" header, which contradicted the first-class routing table at the top of the install section, is reworded to plain "Claude Code & Cowork" (ordering unchanged).

Research — API Reliability Layer

The /research Citation Verifier and Field Reconnaissance modes now distinguish a
source that is genuinely not found from one that was not checked because the
index it needed was degraded or exhausted — closing the failure where a silently
degraded API masqueraded as a clean (or missing) result.

A new stdlib-only api_reliability.py (in the research scripts dir) adds, per
batch run: per-provider call budgets, a per-provider circuit breaker
(run-scoped — it never persists open across runs), and a reliability ledger.
response_cache.py gains TTL freshness (bibliographic metadata 30 days,
Wayback 7 days; env-overridable) wrapped in a backward-compatible envelope; the
no-sticky-error rule is preserved — transient _error payloads are still never
written to disk, even with a TTL.

academic_apis.py's batch output now carries a reliability block (per-provider
calls/ok/errors, budget, circuit state, and a coverage summary naming any
degraded provider), and each result gains resolution_status ∈ {resolved, not-found, not-checked}. Citation Verifier and Field Reconnaissance report a
Source coverage line: when coverage is DEGRADED, any unretrievable verdict on
a citation whose only candidate index was a degraded provider is reported as
NOT-CHECKED, not NOT-FOUND, and a DEGRADED state on a high-stakes / Pre-DE run is
disclosed as a blind spot in the synthesis Absence Inventory.

Reliability bookkeeping is default-on; APODICTIC_RELIABILITY=off omits the
top-level reliability block. The additive per-result
(resolution_status/degraded_providers) and summary (not_checked/not_found)
keys remain, additive and never altering an existing value, so the legacy
result/summary keys are unchanged. Per-provider budgets and TTLs are
env-overridable (APODICTIC_BUDGET_<PROVIDER>,
APODICTIC_CACHE_TTL_METADATA_DAYS, APODICTIC_CACHE_TTL_WAYBACK_DAYS).

Infrastructure — ID-scope the softness-downgrade override (closes the Deficit-Lock gate-bypass)

The 2026-06-20 validator sweep found a P1 override-parse / gate-bypass in softness-check: it tested SOFT_MARKER in body as a single global boolean, so one unscoped <!-- override: softness-downgrade --> marker anywhere in the letter blanket-acknowledged every locked finding's softness downgrade — dismantling the Deficit Lock (per-finding severity honesty), the framework's central value. The softness override is now ID-scoped: <!-- override: softness-downgrade F-<ORIGIN>-<NN> — <rationale> --> downgrades ERROR→WARN only for the named Finding Lifecycle ID (exact, boundary-matched so F-P5-021 / F-P5-02-extra cannot mask F-P5-02); a bare marker with no resolvable ID acknowledges nothing, and markers inside backtick code spans (documentation examples) are ignored. There is intentionally no blanket/all-findings form. Behavioral change: an id-less (legacy heuristic-path) locked finding can no longer be bare-acknowledged — it must carry a Lifecycle ID (which the Deficit Lock already requires for synthesis-bound findings). Locked with regression self-tests including the signature case — an override for one finding must not mask another finding's downgrade (override_masks_only_named_id). The canonical rule docs (output-policy.md §Severity Honesty Protocol, run-synthesis.md) are updated to the ID-scoped form.

The same loose-substring override idiom in timeline_checks (timeline-arithmetic-conflict, timeline-anchor-conflict, timeline-diff-undocumented) is hardened the same way — a shared _has_override(body, slug) requires the exact slug followed by a delimiter (so a suffixed slug like …-conflict-later is rejected) and strips code spans before scanning — across both the Python implementations and the three bash degrade arms in validate.sh (parity). New hostile self-tests cover the suffixed-slug and backtick-span cases.

(softness-check has no bash degrade arm — it delegates to python3 — so only its Python path needed the fix.)

Workflows — Uncertainty-Resolution Intake Interview (disambiguate what the text leaves open)

A narrow, optional loop on top of the existing intake: at the after-Pass-0/1 checkpoint (the Mid-Run Escalation Check seam), it asks the author to resolve a specific structural ambiguity the framework detected but cannot settle from the text — "is the non-linear ordering in Ch 4-6 a deliberate braid, or drift?" — and only that. APODICTIC's draft-then-validate intake and Shelf & Positioning already own genre/controlling-idea/reader-promise/audience capture, so this defers all of it and confines itself to the one thing nothing covers: was this detected feature intentional? That niche is a real structural guarantee, not a slogan — each query is an apodictic.intake_query.v1 block whose kind is a closed enum (timeline-order / pov-choice / tonal-shift / structural-device / register-straddle / other-detected-ambiguity), every value a flavor of intentional-vs-accidental, so the enum cannot express a contract ask. It is the author-facing sibling of the Beta-Reader Instrument (that turns uncertainties into reader questions; this turns them into author questions), and both are barred from softening a verdict. The firewall is calibrate the lens, never suppress a finding: treat_as_intended may direct how analysis treats the feature (assess the braid on its own terms) but may never pre-empt whether a finding is raised — that would be the author-editor concession loop through the front door, dismantling the Deficit Lock. The new intake-interview validator (scripts/intake_interview.py) enforces I1 schema, I2 no-contract-duplication (a question re-asking a contract element — advisory; ERROR under --strict; per-id override), I3 grounded ambiguity (a resolving ambiguity_ref against the Findings Ledger or a non-empty source_note for ID-less Pass-0 / Unresolved-Questions ambiguities — a query grounded in neither is manufactured, and a stated-but-dangling ref is an error), I4 calibrate-not-suppress (suppression phrasing in treat_as_intended — ERROR by default, with a negation guard so the spec's own recommended "it does not pre-suppress any finding" phrasing stays clean), plus advisory W1 coverage (a Pass-0/1 LOW/UNCERTAIN finding or an Unresolved-Questions bullet with no query). It runs only on interactive-input-capable hosts and never blocks a non-interactive run (it falls back to the framework's own intentionality inference). Wired into run-core.md at the after-Tier-1 checkpoint, with references/intake-interview.md as the protocol. Ships the canonical example-intake-interview.md + example-intake-interview-ledger.md (one ref-grounded query, one source_note-grounded query) wired into --check-all under --strict. Self-testable validators 50 → 51. Spec: docs/uncertainty-intake-interview.md.

Infrastructure — Validator-fleet hardening + the validator-conventions meta-linter

A fleet-wide adversarial sweep (2026-06-20, ~40 agents over 22 validators × 8 bug classes, each finding verified) surfaced a set of recurring latent bug classes that manual review had let drift. This change closes the two structural ones and gates them so they cannot recur, and fixes the highest-severity crashes.

  • New meta-linter validator-conventions (scripts/meta_lint.py) — a validator that validates the validators. M1: every AGG_VALIDATORS entry has a dispatcher case that handles --self-test. M2: no validator classifies inputs by a raw apodictic:<type> marker scan (membership in/not in OR a str/re scan op .find/.count/re.search/…); resolvers must classify on parsed blocks (the _has_block / art.parse_blocks idiom) — the substring anti-pattern misroutes a file that merely names a marker, silently false-passing. M3: the advertised count is derived from AGG_VALIDATORS (AGG_COUNT via $#), never hand-typed. M4: no orphan schema (every *.schema.json filename stem in the resolved schema dir is referenced; degrades to a WARN, not a clean PASS, if the resolver is unavailable). M5: no validator detects an override marker by a bare <!-- override: <slug> substring scan (Python "<!-- override: %s" % slug in body / bash grep -F "<!-- override:) — the override-marker sibling of M2; use the hardened override_marker.has_override / _has_override helpers. Wired into --self-test-all and the --check-all release gate (it is itself in AGG_VALIDATORS, so M1 checks the meta-linter against its own rule). Validators 55 → 56. Spec: docs/validator-conventions.md.
  • Resolver-substring class closed (13 validators)legal_risk, scene_ethics, feedback_triage, argument_spine, retcon_plan, continuity_bible, world_bible, finding_trace, reader_instrument, viz_manifest, crosslink, annotation_manifest, and promise_contract now classify input files on parsed blocks via a shared _has_block helper instead of a raw marker substring. Before: a decoy file naming a marker in prose, passed before the real artifact, won resolution and the validator reported "nothing to check" (exit 0) — silently masking a real failure. M2 now gates the whole class.
  • Crash-on-malformed P1s fixed (3)promise_contract.parse_findings, retcon_plan._score_errors, and state_card_diff.check_one guarded obj is not None only, so a non-dict JSON block payload ([1,2,3], "str", 42) reached obj.get() and raised an uncaught traceback. Each now guards isinstance(obj, dict) and surfaces the existing clean "expected a JSON object" error (controlled exit 1). Locked with per-validator non-dict-payload regression self-tests.
  • Crash-on-malformed P1 fixed — honesty_check (softness-check), distinct class — softness-check already filtered non-dict block payloads upstream (parse_locked_findings skips not isinstance(obj, dict)), so the non-dict-payload class never applied to it. Its real latent crash was a locked dict finding with a non-string id (e.g. {"id": 42, …}): the int/list id reached re.escape(fid) in _id_delivered_in_body and raised an uncaught TypeError. It now guards isinstance(fid, str), routing a non-string id to the id-less heuristic path (controlled ERROR, no crash). Locked with a load-bearing regression self-test (crash_nonstring_id_finding / crash_nonstring_id_with_override) that raises against the pre-fix code.

This PR also hardens the override-substring gate-bypass class (the override-marker sibling of the resolver-substring class above) across the bash arms + the recently-built Python gates, and introduces the shared helper + a gate so it cannot re-enter. The bare <!-- override: <slug> prefix test — exploitable via a suffix-collision slug (<slug>-but-not-really) or a code-span decoy (a marker quoted inside a backtick span / fenced block) — is replaced by a single shared hardened helper. The Python sites converted here (letter_checks, config_checks, editor_scaffolding, diagnostic_vocabulary, and — closing this PR's own timeline/softness hardening — timeline_checks and honesty_check, which had each carried a local re.compile(r"```...```|...") stripper that a multi-backtick / ~~~-fenced / multiline example could bypass) now route through override_marker: has_override (a state-machine code-span stripper + slug boundary-match) for the slug gates, or strip_code_spans for honesty_check's ID-scoped softness markers. All bash sites (severity-floor, audit-signal-propagation, underdiagnosis-triggers, ledger-consolidation, decision-layer-check, quality-risk-triggers, audit-tier-criterion, argument-recon-prerequisite, and the three timeline-* arms) delegate to that same helper via a stdin CLI (with a documented best-effort _has_override shell fallback for no-python3). A bash↔Python divergence (the legacy bash arm stripped only inline code spans, so a marker inside a fenced block was wrongly honored by bash though the Python path rejected it; and the bash literal rejected <!-- override: / <slug>—reason whitespace/dash forms Python accepts) is closed — both paths now share one implementation. strip_code_spans is a CommonMark-faithful state machine: a closing fence must be the fence run plus trailing whitespace only (a ~~~not-a-close / ```lang line is content, not a premature closer), an opener is capped at 3 leading spaces (a 4-space/tab-indented fence is indented code), and a backtick opener whose info string contains a backtick (```info`) is not a fence at all (CommonMark — a tilde info string is unrestricted) — so a malformed-fence example can neither expose nor suppress a live marker. Each site carries a code-span-decoy + suffix-collision regression. M5 gates the bare-substring form going forward — its Python string-body matcher is delimiter-aware, so an opposite-quote character inside the literal ("…author's…" / '…"quoted"…') cannot end the body early and evade the scan, and the body spans newlines so a multiline triple-quoted scan is caught too; zero violations across the converted fleet.

Scope — NOT yet fleet-complete (tracked follow-up). Five EARLIER validators still detect overrides with their own compiled-regex logic that is slug-boundary-matched but does not strip code spans, so a code-span-decoy override remains a latent bypass there: content_advisory (advisory-eval / advisory-eval-prose), persona_divergence (persona-quote — silences the D4 anti-fabrication boundary), intake_interview (intake-dup), author_fingerprint (fingerprint-frame), and world_bible (world-rule/-cost/-geo/-firewall). M5 does not flag them (it detects the bare-substring form, not a compiled regex). Migrating these to override_marker and extending M5 to gate the compiled-regex form is a round-2 PR (it builds on the override_marker + M5 this PR lands on main). The sweep's other remaining classes (non-P1 crash sites; lexical-regex over/under-fire heuristics the specs concede are fuzzy) are likewise deferred — not statically gateable by M1–M5.

Tooling — Windows EOL determinism in validator output writers

Completes the cross-platform gate parity from the previous wave. Once .gitattributes made every checkout LF, the canonical byte-identical export checks (html-export, obsidian-export, and the producer chain) failed on a Windows checkout because the export/validator scripts wrote their output with the platform-default newline (CRLF on Windows) — so a fresh build no longer matched the now-LF committed fixtures (Linux/CI was never affected: it wrote LF either way). Pinned newline="" on every text-mode write open() (93 call sites) plus the one os.fdopen sidecar writer in run_gate.py, across the validator suite, in both mirror dirs byte-identically, so every artifact is written LF on every platform. bash scripts/validate.sh --check-all is now green on a Windows (cp1252 + LF) checkout end-to-end. No byte change on Linux (text-mode \n was already written verbatim) and no change to any artifact content.

Workflows — Standalone Worldbuilding-Bible Coherence Tool (/world-bible)

A new optional-artifact tool that checks the SFF author's own pre-draft worldbuilding bible — the rules of the magic/tech and its cost, the geography, the order of events, the factions — for self-contradiction, before any manuscript exists. It is distinct from the framework's manuscript-facing SFF surfaces: the Genre Module SF/F checks a manuscript's Rule Ledger and Cost Matrix in-scene, and the SFF Worldbuilding Integration audit checks whether the world does narrative work (its thesis: "the core problem is not inconsistency, it is inertness"). This tool ports the genre module's consistency vocabulary (closed limits, cost types, scaling) to bible scope, pre-draft — checking the baseline the integration audit assumes — and is a sibling to the post-draft Auto-Derived Continuity Bible (same block-and-validator machinery, different lifecycle stage and input). Each stated world fact is a first-class apodictic.world_fact.v1 block (category ∈ rule/cost/place/distance/event/faction/entity, the discriminator that selects the contradiction arm; subject the grouping key; value always a quoted string so numerics type-check and parse; optional polarity for rules, cost for cost/rule facts, pair_subject for distance/event edges; loci ≥1). It is firewall-bound: it records stated facts only (never inventing an unstated rule, cost, distance, or date) and surfaces contradictions, never resolves them — both conflicting values are recorded and paired, and any recommendation stays abstract-structural ("the bible prices blood-magic two ways — reconcile or stage the escalation", never "the cost is one year of life"). The new world-bible validator (scripts/world_bible.py) enforces W1 schema plus a bespoke closed-key check (the subset schema engine silently admits unknown keys, so a misspelled field is caught here or the closed-set guarantee is hollow) and a bespoke cost value-type check (the schema leaves cost un-typed because the subset engine cannot express a string|null union, so a non-string/non-null cost — an unquoted 5, a list, a bool — fails W1 here instead of reaching the cost arm and crashing _norm_value), WD id-uniqueness, and the deterministic, stdlib-only, conservative contradiction arms: WB-R1 closed-set rule consistency (same subject + normalized value, can vs cannot / requires vs cannot), WB-C1 cost contradiction (two different stated costs for one subject) + advisory WB-C2 free-then-costed (the pre-draft form of Cost Amnesia; ERROR under --strict), WB-G1 distance contradiction (one edge, two parsed distances within a commensurable unit class — a spatial mile/league/km axis and a temporal travel-time day/hour axis are kept separate and never collide-check against each other, so a 6-day ride and 120 miles can both be true), WB-G2 chronology (a cycle in the happens-before graph via a stdlib DFS, or the same event at two Day N anchors), and a WF firewall prose scan (a resolution/invention verb leaking into the bible's prose; advisory, ERROR under --strict). Each conflict is overridable per-pair (<!-- override: world-rule|world-cost|world-geo WF-NN/WF-MM — <rationale> -->, order-insensitive; <!-- override: world-firewall — … --> for WF), recording an intended contradiction (a staged reveal, a documented cost escalation) without softening the verdict. Routed as the /world-bible command (tool-like /legal-risk, not a manuscript-pass audit), usable before or alongside drafting in the pre-writing pathway's spirit. Ships with the canonical example-worldbuilding-bible.md fixture (an epic-fantasy bible with two deliberately staged + overridden contradictions) wired into validate.sh --check-all under --strict. Self-testable validators 50 → 51. Homed in core-editor; spec: docs/worldbuilding-bible.md.