Skip to content

fix(unic-spec-review): reactive footer fallback when Confluence rejects an inline anchor#244

Merged
orioltf merged 5 commits into
developfrom
archon/task-feature-unic-spec-review-232-reactive-footer-fallb
Jun 9, 2026
Merged

fix(unic-spec-review): reactive footer fallback when Confluence rejects an inline anchor#244
orioltf merged 5 commits into
developfrom
archon/task-feature-unic-spec-review-232-reactive-footer-fallb

Conversation

@orioltf

@orioltf orioltf commented Jun 9, 2026

Copy link
Copy Markdown
Member

Why

ADR-0004 guarantees no Finding is ever dropped for lack of an anchor. That held only for mismatches our own resolver detected. A second, invisible class escaped it: when our resolver counts a unique match but Confluence's stored-body model disagrees (case, tag boundaries, entity/whitespace encoding), Confluence rejects the inline POST with HTTP 400. The old code mapped 400 → unreachable and the writer rethrew everything — so the Finding was posted nowhere and the user saw a raw API error. This restores the guarantee end-to-end.

Closes #232. Design locked via grill-with-docs (2026-06-09); source of truth is the amended ADR-0004.

What

  • atlassian-fetch — new FetchError kind 'rejected' for HTTP 400 (server reachable, refused our selection). 401/403auth-error, 404not-found, network/timeout/5xxunreachable unchanged. 429/5xx classification stays deferred. Typedef + doc comment updated.
  • confluence-writer — orchestration extracted into an injectable postFinding({pageId, finding, creds, fetch}){id, type, reason}; main() is now a thin argv/file/exit wrapper. The writer retries a footer comment only on kind:'rejected' from the inline POST; auth / not-found / network all fail loud and are not retried. A successful retry emits type:'footer', reason:'inline-rejected'.
  • review-spec — third anchoring line: Anchoring: footer fallback (inline rejected by Confluence).
  • Resolver untouched (E3 — option 3 dropped; no-op given the stripped-text anchor source).

Tests (node:test, injected fetch, no live Confluence)

  • atlassian-fetch.test.mjs: 400→rejected + a map-driven HTTP status classification block asserting 400/401/403/404/500.
  • confluence-writer.test.mjs (new): inline-400→footer (type:'footer'/reason:'inline-rejected'); inline-401→no footer call, fails loud; inline network-throw→no footer call, fails loud; inline-rejected + footer-500→fails loud, no phantom success; resolver no-anchor→footer endpoint only, reason unchanged.

432 tests pass, typecheck clean, verify:changelog ok → 0.1.11.

Deviation from the investigation

The network-throw test asserts kind:'unreachable' (not a raw TypeError): postJson wraps thrown fetch errors as FetchError(kind:'unreachable') before they reach postFinding. The corrected assertion still proves network errors fail loud and are never retried as footers.

🤖 Generated with Claude Code

orioltf and others added 3 commits June 9, 2026 17:45
…ts an inline anchor

When our resolver counts a unique anchor match but Confluence's stored-body
model disagrees, Confluence rejects the inline POST with HTTP 400 and the
Finding was posted nowhere — breaking the ADR-0004 "never dropped" guarantee
with a raw API error. The writer now retries rejected inline anchors as a
page-level footer comment, while auth/not-found/network errors still fail loud.

Changes:
- atlassian-fetch: add FetchError kind 'rejected' for HTTP 400 (server reachable,
  refused our selection); 401/403→auth-error, 404→not-found, 5xx/network→unreachable
  unchanged. Update FetchErrorKind typedef + doc comment.
- confluence-writer: extract injectable postFinding({pageId,finding,creds,fetch})
  returning {id,type,reason}; retry footer only on kind:'rejected'; main() is a
  thin argv/file/exit wrapper.
- review-spec: third anchoring line for reason 'inline-rejected'.
- tests: HTTP-400→rejected + full classification map in atlassian-fetch.test.mjs;
  new confluence-writer.test.mjs (inline-400→footer, inline-401/network→fail loud
  no footer call, footer-500→fail loud, resolver-footer→footer only).

Fixes #232

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

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

orioltf commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

🔍 Comprehensive PR Review

PR: #244 — fix(unic-spec-review): reactive footer fallback when Confluence rejects an inline anchor
Reviewed by: 5 specialized agents (code-review, error-handling, test-coverage, comment-quality, docs-impact)
Date: 2026-06-09


Summary

The reactive footer fallback is precisely implemented: only kind:'rejected' (HTTP 400) triggers a retry as footer comment — auth, not-found, network, and 5xx all fail loud. The postFinding extraction is clean, injectable, and tested. CI passes across all 9 matrix combinations (macOS/Ubuntu/Windows × Node 22/24). All five agents independently recommend APPROVE.

Verdict: ✅ APPROVE

Severity Count
🔴 CRITICAL 0
🟠 HIGH 0
🟡 MEDIUM 1
🟢 LOW 7

Total: 8 unique findings (after deduplication; raw: 9 across 5 agents)


🟡 Medium Issues (Needs Decision)

M1: Happy-path inline success not tested via postFinding

📍 scripts/lib/confluence-writer.mjs:52-53 / tests/confluence-writer.test.mjs

postFinding has two terminal branches for the inline path: 400 → retry as footer (✅ covered) and 201 success → return {id, type:'inline', reason:null} (❌ not tested). A regression setting the wrong type or reason on a successful inline post would go undetected. The downstream review-spec.md command prints different output per type.

Options: Fix Now | Create Issue | Skip

View suggested test (~10 lines)
it('inline anchor resolves and inline post succeeds — returns type:inline reason:null', async () => {
  const fetch = routingFetch({
    [CONTENT]: PAGE_OK,
    [INLINE]: { status: 201, json: { id: 'inline-1', version: { createdAt: '' } } },
  })
  const result = await postFinding({ pageId: '123', finding: FINDING, creds: CREDS, fetch })
  assert.equal(result.type, 'inline')
  assert.equal(result.reason, null)
  assert.equal(result.id, 'inline-1')
})

Mirrors the existing test-1 pattern exactly. Recommended: fix before merge.


🟢 Low Issues

View 7 low-priority observations
# Issue Location Recommended Action
L1 finding: any — missing FindingInput typedef on exported postFinding (7 accessed properties undocumented) confluence-writer.mjs:37 Add @typedef FindingInput (2 lines, matches established pattern in atlassian-fetch.mjs)
L2 created field silently dropped from CLI stdout — no changelog mention confluence-writer.mjs:123 + CHANGELOG.md Optional one-line note: "CLI stdout no longer includes created"
L3 HTTP 400 from non-inline endpoints maps to kind:'rejected' — classification imprecision atlassian-fetch.mjs:404-406 Leave as-is (reclassification explicitly deferred per scope doc)
L4 Footer retry loses inline-rejection context on secondary failure — logs show only the footer error confluence-writer.mjs:55-58 Leave as-is (fail-loud contract verified by test; wrapping would lose err.kind)
L5 PostFindingResult.id description omits empty-string sentinel (mirrors upstream PostedComment.id) confluence-writer.mjs:24 Add , or '' when absent from the response (1 word change)
L6 AGENTS.md status string omits reactive fallback mention AGENTS.md Leave as-is (ADR-0004 has the full update)
L7 CONTEXT.md status string — same as L6 CONTEXT.md Leave as-is (ADR-0004 is canonical)

✅ What's Good

  • Narrow retry catch: err instanceof FetchError && err.kind === 'rejected' cannot accidentally absorb auth/network/not-found errors. Every non-rejected FetchError rethrows — textbook correct.
  • Five test scenarios: inline-400→footer-success, inline-401 fail-loud, network-throw fail-loud, double-failure, resolver-footer pass. Full required matrix, all behavior-oriented with explicit call-graph guards (footerCalled, inlineCalled).
  • HTTP 400 placement in parseJsonResponse: ordered after 401/403/404, before catch-all !res.ok. Classification hierarchy is readable.
  • HTTP status classification table (atlassian-fetch.test.mjs:909-930): data-driven parametric block pins the 400→rejected contract as a stable, readable spec.
  • postFinding JSDoc: ADR-0004 reference, HTTP 400 → kind mapping, "caller fails loud" summary — everything a future reader needs without looking at the implementation.
  • ADR-0004 already updated: dedicated 2026-06 section with full reactive fallback context.
  • CHANGELOG 0.1.11: detailed, links to issue unic-spec-review: inline-anchor match-count mismatch escapes the ADR-0004 footer fallback #232, explains 'rejected' vs 'unreachable' distinction.

📋 Suggested Follow-up Issues

Title Priority
Add FindingInput typedef to postFinding in confluence-writer P3
Clarify FetchErrorKind taxonomy: 'rejected' vs generic 400 P3

Reviewed by Archon comprehensive-pr-review workflow · 5 agents · 8 unique findings
Artifacts: artifacts/runs/c76d0fb0a61ba0973b9fe72be7a65975/review/

- Add FindingInput typedef to postFinding (replaces `any`)
- Update PostFindingResult.id doc: note empty-string sentinel
- Add happy-path inline-success test (type:inline reason:null)
- Note removal of `created` field from CLI stdout in CHANGELOG

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@orioltf

orioltf commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

⚡ Self-Fix Report (Aggressive)

Status: COMPLETE
Pushed: ✅ Changes pushed to archon/task-feature-unic-spec-review-232-reactive-footer-fallb
Commit: 9c33c4b
Philosophy: Fix everything unless clearly a new concern


Fixes Applied (4 total)

Severity Count
🔴 CRITICAL 0
🟠 HIGH 0
🟡 MEDIUM 1
🟢 LOW 3
View all fixes
  • Happy-path inline success not tested (tests/confluence-writer.test.mjs) — Added test: inline anchor resolves + 201 → asserts type:'inline', reason:null, id matches
  • finding param typed as any (scripts/lib/confluence-writer.mjs:37) — Added @typedef {Object} FindingInput with all 7 properties; updated @param to use it
  • PostFindingResult.id omits empty-string sentinel (scripts/lib/confluence-writer.mjs:24) — Updated description: "Confluence comment id, or '' when absent from the response"
  • created field dropped with no changelog note (CHANGELOG.md) — Added one-line note: "CLI stdout now emits {id, type, reason} (the created field previously present is no longer included)"

Tests Added

  • inline anchor resolves and inline post succeeds — returns type:inline reason:null in tests/confluence-writer.test.mjs

Skipped (4)

All 4 skipped findings were independently recommended "leave as-is" by the reviewing agents:

Finding Reason
HTTP 400 from non-inline endpoints maps to kind:'rejected' Deferred FetchErrorKind reclassification per scope doc; no behavioral impact
Footer retry loses inline-rejection context on secondary failure Wrapping would lose err.kind discrimination; fail-loud contract confirmed by test
AGENTS.md status string omits reactive fallback ADR-0004 is canonical; status string is a high-level summary
CONTEXT.md status string — same incompleteness Same reasoning as AGENTS.md

Suggested Follow-up Issues

(none)


Validation

✅ Type check | ✅ Lint | ✅ Tests (433 passed)


Self-fix by Archon · aggressive mode · fixes pushed to archon/task-feature-unic-spec-review-232-reactive-footer-fallb

…e repetition

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@orioltf orioltf merged commit 97f2306 into develop Jun 9, 2026
9 checks passed
@orioltf orioltf deleted the archon/task-feature-unic-spec-review-232-reactive-footer-fallb branch June 9, 2026 17:21
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.

unic-spec-review: inline-anchor match-count mismatch escapes the ADR-0004 footer fallback

1 participant