Skip to content

fix(readiness): supported API paths to clear ADR-079 readiness findings - #658

Merged
thewrz merged 13 commits into
mainfrom
fix/issue-545
Aug 5, 2026
Merged

fix(readiness): supported API paths to clear ADR-079 readiness findings#658
thewrz merged 13 commits into
mainfrom
fix/issue-545

Conversation

@thewrz

@thewrz thewrz commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Why

PR 544 (ADR-079) added the issuance-readiness gate: a final-mode issuance blocks
with a 422 on any outstanding unresolved_choice_token, specifier_note_present,
open_comment, or body_object_present finding. ADR-079's "block, never strip"
decision says a blocked issuance returns its findings so a human can fix the
source and retry — but for three of the four finding kinds there was no
supported API path that actually cleared the finding on an existing spec.
"Fix the source and retry" was aspirational, not actual. The only working
escape hatch was overrideReadinessGate, an explicitly unaudited bypass.

Closes #545.

What

All three supported clearing paths, per the issue's settled scope:

  • Path 1 — re-derive choiceTokens on text edit. applyParagraphUpdate
    now re-derives only the choiceTokens key of source_facts from the new
    text (reusing the parser's existing scanChoiceTokens, exported through the
    parser barrel — no second detector), in the same statement/transaction as the
    text write. Every other source_facts key is spread through untouched;
    comment closure is explicitly NOT re-derived from text (it's an OOXML
    artifact that doesn't survive round-trip).
  • Path 2 — a per-node acknowledgement. New paragraphs.acknowledged
    boolean (migration 055) clears specifier_note_present /
    body_object_present WITHOUT hiding or removing the note/text-box content —
    a separate, additive piece of state, never the vanish mechanism (vanish is
    deliberately excluded for note/object — see the comment above
    REMOVABLE_NODE_TYPES). New endpoint: PATCH /specs/:id/paragraphs/:nodeId/acknowledgement.
  • Path 3 — a mutable comment-closure toggle. New endpoint: PATCH /specs/:id/paragraphs/:nodeId/comments/:index/closure. acceptCommentAsNote
    now also closes the originating comment in the same transaction as the note
    insert — previously it left the comment open, so accepting it strictly
    increased the number of blocking findings (a new note plus a still-open
    comment). That's the sharpest symptom in the issue, and it's fixed.

openapi.yaml and the MCP tool surface (acknowledge_paragraph,
set_comment_closed, both write tier) are updated in lockstep; the INV-6
write-pending ratchet baseline moves 62 → 64, a documented genuine scope
increase.

Design decisions (no ADR — sprint policy)

  • Acknowledgement is a separate boolean, never merged into
    REMOVABLE_NODE_TYPES or the vanish mechanism — the existing comment above
    that set explains precisely why storing vanish on note/object would
    silently lie about the removal contract (both renderers emit/consider those
    types before ever checking vanish). No renderer may ever consult
    meta.acknowledged — enforced by a new byte-for-byte render-parity test
    (see mutation-verification below).
  • runAccept's comment-closure sub-write on the anchor gets its own
    close-comment history row and its own base_version bump, sharing the
    outer write's already-resolved content_version (one generation, two
    paragraph_versions rows: the note insert + the anchor's closure) — per
    paragraph-history.ts's own contract that every content-mutating write
    records history for every paragraph it touches. The pre-existing gate-free
    idempotent-repeat fast path (findExistingNoteByProvenance) is left
    untouched; it writes nothing today and this fix doesn't change that.
  • closeCommentHandler's no-comment result maps to 404 (a lookup miss),
    not acceptAsNoteHandler's 422 (a validation failure) — there's no content
    to validate here, only a location to find.
  • New paragraph-comment-closure.ts is deliberately not named
    comment-closure.ts, to avoid a same-basename collision with the unrelated
    read-only src/parser/docx/comment-closure.ts / src/ast/comment-closure.ts.
  • buildSubtree/buildSubtreeMeta/SubtreeRow were split out of
    paragraphs.ts into a new paragraph-subtree.ts companion file — threading
    acknowledged through pushed that file over the repo's enforced
    max-lines: 400 cap. router.ts got the same treatment via a new
    registerParagraphClearanceRoutes extraction.

PR size

This PR is ~1870 LOC across 7 commits, over the repo's 500-LOC advisory
loc-check — expected and pre-approved for this issue: three independent,
fully-tested clearing paths spanning DB/API/MCP/OpenAPI plus their own
integration coverage, per the sprint's "the PR grows; that is fine" policy.

Mutation-verification transcripts

Render-parity assertion (src/generator/acknowledgement-render-parity.test.ts)
— the assertion this path most risks per the issue's own verification bar.
Temporarily branched src/generator/markdown.ts's note-rendering on
node.meta.acknowledged during the design spike (reverted before real
implementation):

- markdown output is byte-identical acknowledged vs unacknowledged
  AssertionError: expected '...different string when acknowledged...'
  to equal '...unacknowledged markdown...'
  ✗ FAILED — confirms the assertion is not vacuous

Reverted the mutation; suite returned to green. The shipped test asserts
parity across markdown, .SEC, AND DOCX (unzipped word/document.xml
string comparison — Packer.toBuffer() embeds a fresh zip/docProps
timestamp on every call, so whole-Buffer equality is unusable even for two
generations of the identical tree; confirmed experimentally).

unrelated source_facts keys survive a text edit assertion
(src/db/queries/source-facts-rederive.test.ts) — temporarily had
deriveNextSourceFacts drop unrelated keys (return only the recomputed
choiceTokens) instead of spreading existing through:

- unrelated source_facts keys survive a text edit byte-identical
  AssertionError: expected undefined to equal [{ author: 'Jane', ... }]
  ✗ FAILED — confirms the assertion is not vacuous

Reverted the mutation; suite returned to green.

Adversarial cross-review (Codex gpt-5.6-sol, xhigh)

Ran once as the final draft gate. 2 findings, both [P1], both handled
full assessment, reproduction transcripts, and the mutation-verification
matrix are in a
dedicated PR comment.
Summary:

Both fixes are pinned by regression tests named for the symptom, and both new
gates were mutation-verified alongside the four pre-existing ones.

Testing

  • Unit tests pass — pnpm test: 258 files, 3650 tests, 0 failures.
  • Integration tests pass — pnpm test:integration: 159 files, 1822 tests
    passed, 141 skipped (pre-existing fixture-conditional skips), 0 failures.
  • Migration 055 applies and rolls back cleanly (pnpm migrate /
    pnpm migrate:down, verified against a real Postgres instance) —
    corrected during review: the original down re-narrowed
    paragraph_versions_op_check without first removing the rows the
    widened list had allowed, so it hard-failed
    (check constraint ... is violated by some row) the moment the feature
    had actually been used. Fixed in 98bcf979 (mirrors migration 029's
    down) and re-verified red→green with seeded acknowledge /
    close-comment history rows present.
  • Manual verification: the full end-to-end scenario required by the issue
    — a final-mode issuance blocked by all four finding kinds, cleared
    through the three supported API paths ALONE with overrideReadinessGate
    unset, now succeeds — is asserted in
    src/api/readiness-clearance.integration.test.ts ("end-to-end: a
    final-mode issuance blocked by all four finding kinds succeeds..."),
    against a real generated DOCX (zip) payload.
  • pnpm lint (eslint + tsc --noEmit + prettier --check) green.
  • CI green (all six checks passed on the pre-review head; re-running on
    the two review-fix commits).

🤖 Co-authored by Claude Sonnet 5. Closes #545.

Summary by CodeRabbit

  • New Features
    • Added per-paragraph acknowledgement for notes and text boxes, allowing accepted content to be marked without removing it from rendered documents.
    • Added controls to close or reopen individual source-document comments.
    • Added acknowledgement and comment-closure tools for supported integrations.
  • Bug Fixes
    • Final-mode generation now accurately reflects resolved findings and continues reporting unrelated issues.
    • Accepting a comment as a note now closes the original comment automatically.
  • Tests
    • Expanded coverage for readiness checks, rendering consistency, validation, and integration workflows.

thewrz and others added 7 commits August 4, 2026 18:55
Path 1 of #545 (ADR-079 follow-on). Resolving a placeholder through the
real paragraph text-edit endpoint left unresolved_choice_token blocking
forever, because updateParagraphText's UPDATE never touched
source_facts, which is parsed once at DOCX import and otherwise frozen.

applyParagraphUpdate now re-derives ONLY the choiceTokens key from the
new text (reusing the parser's existing scanChoiceTokens, exported
through the parser barrel — no second detector) in the same statement
and transaction as the text write. Every other source_facts key
(comments, colors, highlights, emphasis, banner, vanish) is spread
through untouched, since comment closure and the like are OOXML
artifacts that do not survive in plain text and must never be
re-derived from it.

Split buildSubtree/buildSubtreeMeta/SubtreeRow out of paragraphs.ts into
a new paragraph-subtree.ts companion file — with the acknowledged field
threaded through, paragraphs.ts crossed the repo's enforced max-lines
cap, mirroring its existing companion-file convention (object-meta.ts,
node-type.ts, inference-meta.ts, ...).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Path 2 of #545 (ADR-079 follow-on). specifier_note_present and
body_object_present had no clearing path at all — note/object are
deliberately excluded from REMOVABLE_NODE_TYPES (paragraph-vanish.ts)
because both renderers emit/consider those types before ever checking
vanish, so storing vanish on them would silently lie about the removal
contract.

Adds a separate, additive per-node `acknowledged` boolean (migration
055: paragraphs.acknowledged, plus four new paragraph_versions op
values — acknowledge/unacknowledge/close-comment/reopen-comment,
mirroring migration 046's op-check widening) that affirms a human has
read and accepted the note/text-box content WITHOUT hiding or removing
it. readiness-review.ts gates both finding kinds on
`meta.acknowledged !== true`; no renderer consults the flag — pinned by
a new byte-for-byte parity test comparing markdown, .SEC, and (via
unzip + word/document.xml string comparison, since Packer.toBuffer()
embeds a fresh timestamp on every call) DOCX output across
acknowledged vs. unacknowledged trees.

setParagraphAcknowledged mirrors setParagraphVanish's gate/lock/
history shape exactly (new paragraph-acknowledgement.ts). Every row
shape that already threads vanish through a paragraph read
(paragraphs-batch.ts, specs.ts, revision-snapshot.ts) gains
acknowledged in lockstep.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Path 3 of #545 (ADR-079 follow-on) — the sharpest symptom in the issue.
open_comment (sourceFacts.comments[i].closed) was a parse-time fact no
write path ever updated, so a blocked issuance had no way to clear it.
acceptCommentAsNote made this worse: it inserted a new note sibling
(itself an unremovable specifier_note_present finding) but left the
original comment open, strictly increasing the blocking-finding count.

deriveCommentClosureFacts (new paragraph-comment-closure.ts) is a pure
spread-and-replace helper — flips one comment's closed flag, every
other source_facts key/entry untouched — backing both:

- setParagraphCommentClosed, a standalone gate/lock/history-wrapped
  toggle mirroring setParagraphVanish/setParagraphAcknowledged, for a
  comment a specifier wants to close without accepting as a note; and
- runAccept (reclassify.ts): now closes the anchor's originating
  comment in the SAME transaction as the note insert, sharing the
  outer write's already-resolved historyContext (one content_version
  generation). Per paragraph-history.ts's own contract that every
  content-mutating write records history for every paragraph it
  touches, the anchor's source_facts write is followed by its own
  close-comment history row — the anchor's SELECT is widened with
  base_version/text/node_type to build it. The pre-existing gate-free
  idempotent-repeat fast path (findExistingNoteByProvenance) is left
  untouched — it writes nothing today and this fix does not change
  that invariant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires paths 2 and 3 of #545 onto the REST surface:

  PATCH /specs/:id/paragraphs/:nodeId/acknowledgement
  PATCH /specs/:id/paragraphs/:nodeId/comments/:index/closure

PatchAcknowledgementBodySchema/PatchCommentClosureBodySchema
(spec-tree-schemas.ts, beside PatchRemovalBodySchema) both validate a
required boolean plus optional actorLabel — no expectedVersion, matching
the existing removal endpoint's structural-toggle shape rather than
updateParagraphText's optimistic-concurrency shape.

acknowledgeParagraphHandler (new paragraph-acknowledgement.ts) mirrors
removeParagraphHandler's validate/call/switch shape. closeCommentHandler
lands directly in editability.ts (not a new file) to reuse its existing
parseIds/INDEX_SCHEMA helpers already built for acceptAsNoteHandler,
mapping no-comment to 404 (a lookup miss, not a validation failure) vs.
acceptAsNoteHandler's 422 for "nothing to accept."

Both routes register through a new registerParagraphClearanceRoutes
extraction (paragraph-clearance-routes.ts) — router.ts was already
pressing the repo's enforced max-lines cap, and this mirrors the
existing registerCheckpointRoutes extraction pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Keeps the MCP tool surface contract-bound to the new REST endpoints
(ADR-044): acknowledge_paragraph and set_comment_closed mirror
handleUpdateParagraph/handleAcceptCommentAsNote's parse/call/
gateToolError shape, both registered at the 'write' tier
(capabilities.ts) alongside remove_paragraph/accept_comment_as_note —
reversible structural toggles, not destructive.

OP_TO_TOOL (contract-map.ts) maps both new PATCH operations; both land
in INV6_WRITE_PENDING (contract-write-response-map.ts) for the same
SpecNode-mirroring reason `patch .../removal` already does, moving the
INV-6 ratchet baseline from 62 to 64 — a genuine scope increase, not a
coverage regression, per that constant's own documented exception.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the two new PATCH path items (acknowledgement, comments/{index}/
closure) modeled directly on the existing /removal and
/comments/{index}/accept-as-note operations — same envelope, same
SpecNode schema ref (which gains meta.acknowledged: boolean), same
400/403/404/409/422 status set. Both routes join the response-schema
allowlist in contract.integration.test.ts alongside /removal, for the
same SpecNode-response-cycle reason already documented there; the real
(non-schema) response assertions live in the new
readiness-clearance.integration.test.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Proves every acceptance criterion end to end through the real REST
endpoints (never the DB layer directly):

- editing text through PATCH .../paragraphs/:nodeId clears
  unresolved_choice_token;
- PATCH .../acknowledgement clears specifier_note_present and
  body_object_present without touching content, and un-acknowledging
  restores the finding (a real toggle, not a one-way door); 422 on a
  non-acknowledgeable node type;
- PATCH .../comments/:index/closure clears open_comment and reopening
  restores it; 404 on a missing comment index;
- accepting a comment as a note no longer leaves the original comment
  open (the #545 regression this issue exists to fix);
- the full scenario: a final-mode issuance blocked by all four finding
  kinds succeeds once every finding is cleared through these supported
  paths ALONE, overrideReadinessGate never set — the whole point of the
  issue, asserted against a real generated DOCX (zip) payload.

Teardown is scoped to specIds captured by the test itself (never a name
pattern or whole-table delete), per fix/issue-442's ratchet gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d7470ec3-cbf8-4fb5-8eac-3c3f8ecfc958

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Added per-paragraph acknowledgement and comment-closure operations. Persisted acknowledgement state now affects readiness findings without changing rendered output. REST and MCP interfaces expose idempotent, edit-gated mutations with ownership and node validation. Paragraph edits rederive choice-token facts.

Changes

Readiness clearance

Layer / File(s) Summary
Contracts and persisted acknowledgement state
openapi.yaml, src/ast/*, src/db/migrations/*, src/db/queries/paragraphs*, src/db/queries/specs.ts, src/db/queries/revision-snapshot.ts, src/db/queries/paragraph-subtree.ts
The API and schema contracts define acknowledgement and comment-closure payloads. Paragraph persistence, history constraints, queries, and SpecNode.meta now carry acknowledgement state.
Source-fact rederivation
src/db/queries/source-facts-rederive.ts, src/db/queries/paragraphs.ts, src/parser/*
Paragraph text updates preserve unrelated source facts and rederive choiceTokens from the new text.
Database mutations
src/db/queries/paragraph-acknowledgement.ts, src/db/queries/paragraph-comment-closure.ts, src/db/queries/reclassify.ts
Added transactional, idempotent acknowledgement and comment-closure updates with ownership checks, edit gates, history entries, and content-version updates. Accepting a comment as a note now closes the original comment in the same transaction.
REST and MCP interfaces
src/api/*, src/mcp/*
Registered acknowledgement and comment-closure routes and tools. Handlers validate inputs and map database, ownership, missing-resource, node-type, edit-gate, and server-error outcomes.
Readiness and integration validation
src/lib/readiness-review.ts, src/api/readiness-clearance.integration.test.ts, src/mcp/paragraph-tools.integration.test.ts, src/generator/acknowledgement-render-parity.test.ts, src/*/*.test.ts
Tests cover readiness clearance and restoration, source-fact updates, schema round trips, history, REST and MCP flows, and unchanged Markdown, .SEC, and DOCX output.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant REST API
  participant Database
  participant Readiness Review
  participant Generator
  Client->>REST API: Acknowledge node or close comment
  REST API->>Database: Apply validated mutation
  Database-->>REST API: Return updated SpecNode
  REST API-->>Client: Return mutation response
  Client->>Readiness Review: Request final readiness
  Readiness Review-->>Client: Return cleared findings
  Client->>Generator: Request final generation
  Generator-->>Client: Return DOCX payload
Loading

Possibly related issues

  • None. The retrieved issue does not describe the acknowledgement and comment-closure implementation in this PR.

Possibly related PRs

  • wrzonance/SpecR#185: Adds the scanChoiceTokens and SourceFacts.choiceTokens flow reused for rederiving choice-token findings.
  • wrzonance/SpecR#272: Defines the shared comment-closure state and SourceFacts handling extended by these write operations.
  • wrzonance/SpecR#197: Provides the edit-gate, row-locking, and content-version infrastructure used by the new paragraph mutations.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR's main change: adding supported API paths to clear ADR-079 readiness findings.
Linked Issues check ✅ Passed The PR satisfies issue #545 by adding supported paths for choice tokens, notes, body objects, and comments, with persistence and regression coverage.
Out of Scope Changes check ✅ Passed The changes support the linked issue through REST, MCP, database, schema, readiness, migration, and test updates without unrelated scope.

Comment @coderabbitai help to get the list of available commands.

thewrz and others added 3 commits August 4, 2026 19:46
…ugh the tool path

The two new MCP tools this feature adds (#545, ADR-079 follow-on) were only
ever covered by structural contract-map/capability-tier checks — no test
called handleAcknowledgeParagraph or handleSetCommentClosed directly, so a
wiring bug (shape typo, status→ToolResult mapping mistake, gateToolError
misrouting) unique to the MCP layer would ship silently. Adds the same
end-to-end describe-block coverage the other paragraph-mutation tools
already have: success/toggle, the not-acknowledgeable/no-comment error
paths, not-found/wrong-spec rejection, and actorLabel attribution.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The down migration re-narrowed paragraph_versions_op_check to the pre-055
op list without first removing the rows the widened list had allowed. The
moment the feature was used, `pnpm migrate:down` hard-failed with
"check constraint is violated by some row" (ATRewriteTable) — verified
against a live database. The repo convention is that every migration is
reversible, so a down that only works on an unused schema is a defect.

Deletes the four added ops' history rows before re-narrowing, mirroring
migration 029's down, which deletes the pr6/pr7 style_rules its own up
widened the node_type CHECK to permit. This is not extra data loss: those
rows exist solely to describe toggles of the `acknowledged` column and the
comment-closure flag that the same down removes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…r findings

Two defects found by the adversarial cross-review, both verified against
running code before fixing.

1. `acknowledged` was added to the `SpecNodeMeta` TS type but never mirrored
   in `SpecNodeMetaSchema`. That schema has no `.catchall()`, so
   `SpecTreeSchema.parse` silently STRIPPED the field — proven directly:
   meta round-tripped to `{}` and a spec whose findings were `[]` came back
   `['specifier_note_present']`. `validateTree` (revision-snapshot.ts) sits
   on the package-issuance and revision-freeze paths, so an acknowledged
   note re-blocked a package issuance and frozen snapshots lost the state.
   This is the exact bug class the schema's own comment warns about, and the
   same one #497 fixed for `pageBreakBefore`.

2. An acknowledged `note` returned a bare `[]`, so an open comment or
   unresolved choice token carried ON that note became invisible to the
   gate. Pre-#545 the note itself always blocked, so those facts never
   needed their own guard — acknowledgement introduced the bypass, letting
   unresolved review material reach a final-mode issuance. Acknowledgement
   now clears ONLY `specifier_note_present`; the note's other source facts
   keep blocking and keep their own supported clearing paths.

Both are pinned by regression tests named for the symptom, and both gates
were mutation-verified: reverting either fix fails them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thewrz

thewrz commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

This was written agentically; verify its assertions and edit accordingly:

Adversarial cross-review — Codex gpt-5.6-sol (effort xhigh)

Ran once as the final draft gate against origin/main, with --ignore-user-config. 2 findings, both [P1], both handled. Each was verified against running code before any fix; neither was auto-applied.


[P1] acknowledged is stripped at the SpecTree validation boundary — CONFIRMED, fixed in 7d2b2d4e

Adding this field only to the TypeScript interface causes SpecTreeSchema to silently strip it because SpecNodeMetaSchema has no corresponding key.

Correct, and the more serious of the two. Reproduced directly rather than taken on faith:

meta after SpecTreeSchema.parse: {}
findings BEFORE validateTree: []   AFTER: [ 'specifier_note_present' ]

acknowledged was added to SpecNodeMeta (ast/types.ts) but not mirrored in SpecNodeMetaSchema (ast/spec-tree-schemas.ts), which has no .catchall() — so z.object dropped it on every validation path. validateTree (db/queries/revision-snapshot.ts) sits on the package-issuance and revision-freeze paths (revisions.ts, reporting.ts), so an acknowledged note re-blocked a package issuance and frozen snapshots lost the state entirely. The single-spec POST /specs/:id/generate path does not go through validateTree, which is why the end-to-end test still passed and the bug hid.

This is the exact failure mode that schema's own comment warns about ("a field added to the type but not mirrored here would silently vanish on every JSONB round-trip"), and the same bug class #497 fixed for pageBreakBefore. Fixed by mirroring the key; pinned by preserves acknowledged through validation (#545) and a stronger readiness: acknowledgement survives a SpecTreeSchema round-trip (#545) that asserts the cleared finding state survives, not merely the key.

[P1] Acknowledged note bypasses other blockers — refuted as written, but a real bypass it exposed is fixed in 7d2b2d4e

When a note also carries sourceFacts.comments or choiceTokens, this early return makes acknowledgement clear open_comment and unresolved_choice_token findings too.

The stated mechanism is wrong. On main, assessNode's note branch returned [specifier_note_present] and never fell through to choiceTokenFindings/openCommentFindings — so those findings were never emitted for a note node, before or after this PR. Acknowledgement does not "clear" findings the gate previously reported.

The underlying concern is nonetheless real, and this PR does introduce it: pre-#545 a note always blocked, so its own unresolved facts never needed a guard. Acknowledging it now removes the only blocker, letting an open comment or unresolved choice token carried on that note reach a final-mode issuance. Acknowledgement is documented to clear only specifier_note_present, so the bare [] was too broad. An acknowledged note now still reports its own open_comment / unresolved_choice_token — both of which have their own supported clearing paths (comment closure / text edit), so this is hardening, not a new dead end.


Also found and fixed independently of Codex — migration 055 was not reversible (98bcf979)

down re-narrowed paragraph_versions_op_check to the pre-055 op list without first removing the rows the widened list had allowed. Once the feature was actually used, pnpm migrate:down hard-failed against a live database:

ERROR: check constraint "paragraph_versions_op_check" is violated by some row  (ATRewriteTable)

The PR body's "rolls back cleanly" claim only held on an unused schema. Fixed by deleting the four added ops' history rows before re-narrowing, mirroring migration 029's down. Verified red→green: fails with seeded acknowledge/close-comment rows before the fix, downup round-trips after.

Mutation verification

Every gate on this PR was verified to actually fail when its invariant is broken — the two the issue flagged as easiest to write vacuously, plus the two added here:

Gate Mutation Result
render parity — markdown branch note rendering on meta.acknowledged ✗ fails
render parity — DOCX branch emitNode on meta.acknowledged ✗ fails
render parity — .SEC branch renderNote on meta.acknowledged ✗ fails
unrelated source_facts survive deriveNextSourceFacts drops the spread ✗ fails
acknowledged survives validation remove the schema key ✗ fails
ack is not a blanket bypass restore the bare return [] ✗ fails

Also re-confirmed: note/object are still absent from REMOVABLE_NODE_TYPES, vanish is not reused, and readiness-review.ts remains the only consumer of meta.acknowledged — no renderer or merge code reads it.

🤖 Co-authored by Claude Opus 5.

@thewrz
thewrz marked this pull request as ready for review August 5, 2026 08:41
@thewrz

thewrz commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openapi.yaml`:
- Around line 709-770: Update the ParagraphHistoryEntry.op enum in openapi.yaml
to include acknowledge, unacknowledge, close-comment, and reopen-comment,
ensuring paragraph-history responses accept all history operations created by
these endpoints.

In `@src/mcp/paragraph-tools.integration.test.ts`:
- Around line 570-578: Clean up the paragraph acknowledgement integration test
by removing the repeated onNode declarations in the same scope so only one
declaration remains. In the type literals used around
handleAcknowledgeParagraph, remove duplicate meta members and retain a single
meta definition per type while preserving the existing assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f68ea949-d433-46db-bb39-4345f4c7a53a

📥 Commits

Reviewing files that changed from the base of the PR and between 14f06e3 and 7d2b2d4.

📒 Files selected for processing (39)
  • openapi.yaml
  • src/api/contract.integration.test.ts
  • src/api/editability.ts
  • src/api/paragraph-acknowledgement.ts
  • src/api/paragraph-clearance-routes.ts
  • src/api/readiness-clearance.integration.test.ts
  • src/api/router.ts
  • src/ast/index.ts
  • src/ast/schemas.test.ts
  • src/ast/spec-tree-schemas.ts
  • src/ast/types.ts
  • src/db/index.ts
  • src/db/migrations/055_paragraph_acknowledgement.ts
  • src/db/queries/paragraph-acknowledgement.ts
  • src/db/queries/paragraph-comment-closure.test.ts
  • src/db/queries/paragraph-comment-closure.ts
  • src/db/queries/paragraph-history.test.ts
  • src/db/queries/paragraph-history.ts
  • src/db/queries/paragraph-subtree.ts
  • src/db/queries/paragraphs-batch.ts
  • src/db/queries/paragraphs.test.ts
  • src/db/queries/paragraphs.ts
  • src/db/queries/reclassify.ts
  • src/db/queries/revision-snapshot.ts
  • src/db/queries/source-facts-rederive.test.ts
  • src/db/queries/source-facts-rederive.ts
  • src/db/queries/specs.test.ts
  • src/db/queries/specs.ts
  • src/generator/acknowledgement-render-parity.test.ts
  • src/lib/readiness-review.test.ts
  • src/lib/readiness-review.ts
  • src/mcp/capabilities.ts
  • src/mcp/contract-map.ts
  • src/mcp/contract-write-response-map.ts
  • src/mcp/paragraph-handlers.ts
  • src/mcp/paragraph-tools.integration.test.ts
  • src/mcp/paragraph-tools.ts
  • src/parser/docx/index.ts
  • src/parser/index.ts

Comment thread openapi.yaml
Comment thread src/mcp/paragraph-tools.integration.test.ts
#545)

ParagraphHistoryEntry.op enumerated 7 values while the DB CHECK constraint
(migration 055) accepts 11. All four ops this PR adds — acknowledge,
unacknowledge, close-comment, reopen-comment — write real
paragraph_versions rows, so GET .../paragraphs/{nodeId}/history could
return a payload the authoritative contract calls impossible.

Invisible to the rest of CI: that op is in contract.integration.test.ts's
RESPONSE_ALLOWLIST, so no response-schema validation ever compares a real
history payload against this enum, and #649 does not remove it.

So the enum is now pinned rather than trusted. PARAGRAPH_HISTORY_OPS,
migration 055's OPS_SQL_LIST, and openapi.yaml were three copies of one
list whose own comments say "keep the two in lockstep by hand" — nothing
checked the third. paragraph-history-openapi.test.ts asserts set equality
in BOTH directions: a missing value documents a real response as invalid
(this bug), an extra one lets a client branch on a case the CHECK
constraint would reject. A unit test, so it runs on every push rather than
behind the integration gate.

Mutation-verified: removing `acknowledge` from the enum fails the gate
(expected [Array(10)] to deeply equal [...11]); restored, green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
thewrz and others added 2 commits August 5, 2026 08:35
# Conflicts:
#	src/mcp/paragraph-handlers.ts
#	src/parser/docx/index.ts
#	src/parser/index.ts
…wlisting

Resolves the RESPONSE_ALLOWLIST conflict with #657 (issue #649). Taking
either side wholesale is wrong: #658's side silently reverts #657's entire
deliverable and CI stays green anyway, because an allowlisted op is simply
not validated; #657's side alone leaves this branch's two new ops uncovered.

The two ops were allowlisted here for the SpecNode-cycle reason #657 has
now removed, so rather than carry the exemption forward with a corrected
comment, they move to RESPONSE_COVERED and gain real `assertResponse` +
`assertResponseExact` calls beside their behavioural assertions in
readiness-clearance.integration.test.ts. Each carries the INV-6
undocumented-key probe, so the new coverage cannot pass vacuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@thewrz
thewrz merged commit 0cbcf39 into main Aug 5, 2026
6 checks passed
@thewrz
thewrz deleted the fix/issue-545 branch August 5, 2026 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-079 readiness gate: no supported edit-API path to clear choice-token/note/open-comment findings

1 participant