fix(readiness): supported API paths to clear ADR-079 readiness findings - #658
Conversation
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>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdded 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. ChangesReadiness clearance
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
Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
…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>
|
This was written agentically; verify its assertions and edit accordingly: Adversarial cross-review — Codex
|
| 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.
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
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
📒 Files selected for processing (39)
openapi.yamlsrc/api/contract.integration.test.tssrc/api/editability.tssrc/api/paragraph-acknowledgement.tssrc/api/paragraph-clearance-routes.tssrc/api/readiness-clearance.integration.test.tssrc/api/router.tssrc/ast/index.tssrc/ast/schemas.test.tssrc/ast/spec-tree-schemas.tssrc/ast/types.tssrc/db/index.tssrc/db/migrations/055_paragraph_acknowledgement.tssrc/db/queries/paragraph-acknowledgement.tssrc/db/queries/paragraph-comment-closure.test.tssrc/db/queries/paragraph-comment-closure.tssrc/db/queries/paragraph-history.test.tssrc/db/queries/paragraph-history.tssrc/db/queries/paragraph-subtree.tssrc/db/queries/paragraphs-batch.tssrc/db/queries/paragraphs.test.tssrc/db/queries/paragraphs.tssrc/db/queries/reclassify.tssrc/db/queries/revision-snapshot.tssrc/db/queries/source-facts-rederive.test.tssrc/db/queries/source-facts-rederive.tssrc/db/queries/specs.test.tssrc/db/queries/specs.tssrc/generator/acknowledgement-render-parity.test.tssrc/lib/readiness-review.test.tssrc/lib/readiness-review.tssrc/mcp/capabilities.tssrc/mcp/contract-map.tssrc/mcp/contract-write-response-map.tssrc/mcp/paragraph-handlers.tssrc/mcp/paragraph-tools.integration.test.tssrc/mcp/paragraph-tools.tssrc/parser/docx/index.tssrc/parser/index.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>
# 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>
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, orbody_object_presentfinding. 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:
choiceTokenson text edit.applyParagraphUpdatenow re-derives only the
choiceTokenskey ofsource_factsfrom the newtext (reusing the parser's existing
scanChoiceTokens, exported through theparser barrel — no second detector), in the same statement/transaction as the
text write. Every other
source_factskey is spread through untouched;comment closure is explicitly NOT re-derived from text (it's an OOXML
artifact that doesn't survive round-trip).
paragraphs.acknowledgedboolean (migration 055) clears
specifier_note_present/body_object_presentWITHOUT 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 aboveREMOVABLE_NODE_TYPES). New endpoint:PATCH /specs/:id/paragraphs/:nodeId/acknowledgement.PATCH /specs/:id/paragraphs/:nodeId/comments/:index/closure.acceptCommentAsNotenow 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.yamland the MCP tool surface (acknowledge_paragraph,set_comment_closed, bothwritetier) are updated in lockstep; the INV-6write-pending ratchet baseline moves 62 → 64, a documented genuine scope
increase.
Design decisions (no ADR — sprint policy)
REMOVABLE_NODE_TYPESor the vanish mechanism — the existing comment abovethat set explains precisely why storing vanish on
note/objectwouldsilently 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 ownclose-commenthistory row and its ownbase_versionbump, sharing theouter write's already-resolved
content_version(one generation, twoparagraph_versionsrows: the note insert + the anchor's closure) — perparagraph-history.ts's own contract that every content-mutating writerecords history for every paragraph it touches. The pre-existing gate-free
idempotent-repeat fast path (
findExistingNoteByProvenance) is leftuntouched; it writes nothing today and this fix doesn't change that.
closeCommentHandler'sno-commentresult maps to 404 (a lookup miss),not
acceptAsNoteHandler's 422 (a validation failure) — there's no contentto validate here, only a location to find.
paragraph-comment-closure.tsis deliberately not namedcomment-closure.ts, to avoid a same-basename collision with the unrelatedread-only
src/parser/docx/comment-closure.ts/src/ast/comment-closure.ts.buildSubtree/buildSubtreeMeta/SubtreeRowwere split out ofparagraphs.tsinto a newparagraph-subtree.tscompanion file — threadingacknowledgedthrough pushed that file over the repo's enforcedmax-lines: 400cap.router.tsgot the same treatment via a newregisterParagraphClearanceRoutesextraction.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 onnode.meta.acknowledgedduring the design spike (reverted before realimplementation):
Reverted the mutation; suite returned to green. The shipped test asserts
parity across markdown,
.SEC, AND DOCX (unzippedword/document.xmlstring comparison —
Packer.toBuffer()embeds a fresh zip/docPropstimestamp 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 editassertion(
src/db/queries/source-facts-rederive.test.ts) — temporarily hadderiveNextSourceFactsdrop unrelated keys (return only the recomputedchoiceTokens) instead of spreadingexistingthrough: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:
acknowledgedwas silently stripped bySpecTreeSchema(confirmed).SpecNodeMetaSchemanever mirrored the newSpecNodeMetafield and has no.catchall(), sovalidateTree— which sits on the package-issuance andrevision-freeze paths — dropped it: an acknowledged note re-blocked a
package issuance and frozen snapshots lost the state. The single-spec
/generatepath skipsvalidateTree, which is why the end-to-end teststill passed and the bug hid. Same bug class as generator: round-trip drops manual page breaks (w:br type="page"), collapsing multi-page fixtures to one page #497's
pageBreakBefore.Fixed in
7d2b2d4e.stated — a note branch never emitted those findings on
maineither — butthe bypass it points at is real and is introduced here: pre-ADR-079 readiness gate: no supported edit-API path to clear choice-token/note/open-comment findings #545 the note
always blocked, so an open comment on a note now reaches a final issuance).
Acknowledgement now clears only
specifier_note_present. Fixed in7d2b2d4e.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
pnpm test: 258 files, 3650 tests, 0 failures.pnpm test:integration: 159 files, 1822 testspassed, 141 skipped (pre-existing fixture-conditional skips), 0 failures.
pnpm migrate/pnpm migrate:down, verified against a real Postgres instance) —corrected during review: the original
downre-narrowedparagraph_versions_op_checkwithout first removing the rows thewidened list had allowed, so it hard-failed
(
check constraint ... is violated by some row) the moment the featurehad actually been used. Fixed in
98bcf979(mirrors migration 029'sdown) and re-verified red→green with seededacknowledge/close-commenthistory rows present.— a final-mode issuance blocked by all four finding kinds, cleared
through the three supported API paths ALONE with
overrideReadinessGateunset, now succeeds — is asserted in
src/api/readiness-clearance.integration.test.ts("end-to-end: afinal-mode issuance blocked by all four finding kinds succeeds..."),
against a real generated DOCX (zip) payload.
pnpm lint(eslint + tsc --noEmit + prettier --check) green.the two review-fix commits).
🤖 Co-authored by Claude Sonnet 5. Closes #545.
Summary by CodeRabbit