Skip to content

feat(query): pending queue briefings and session continuity - #574

Merged
George-RD merged 2 commits into
mainfrom
feat/pending-queue-briefing
Aug 3, 2026
Merged

feat(query): pending queue briefings and session continuity#574
George-RD merged 2 commits into
mainfrom
feat/pending-queue-briefing

Conversation

@George-RD

Copy link
Copy Markdown
Collaborator

Campaign step 5: todo.pending-queue-briefing. The pending queue now briefs the maintainer in place and a session opens where the last one stopped.

Surfaces

  • Wire (SCHEMA_VERSION 10): PendingDecision gains ruling_summary, rubric (tier/unblocks/alignment/options), local-tier evidence receipts with tri-state subject_hash_matches (required, nullable), changed_since_review, ruling_prompt, and reopen_command.
  • CLI: cairn pending <id> renders one briefing on the human and --json paths (unknown id fails with pending.not-found); cairn context opens with Waiting on you (full briefings), Where work was left (in-progress todos, active changes, in-progress beads under dec.beads-task-layer's read-only authority), and the shared next-recommended selection; pending errors propagate instead of rendering an honest-empty queue.
  • MCP: PendingRequest exposes optional id; registry description names the briefing form.
  • Webui: pending channel renders the same briefing read-only; lane-scoped landmarks (pendingPrompt/pendingRubric/pendingEvidence/pendingReopen) added to the visual harness.
  • Evidence correctness: receipts join through the canonical manifest::review_path_covered (canonicalise, strip root, slash-normalised for Windows, directory rules); dangling exact affects: review pointers seed unverified rows; any comparable stale receipt keeps the changed marker while uncomparable hashes render "could not be checked" and never claim change.
  • Parser hardening: level-aware section finding (H1 titles never win over ## Decision), logical rubric-bullet accumulation, terminator counting immune to dec.foo/path tokens and closing quotes.
  • Live rubrics: both queued decisions rewritten with plain authored leads and rubric prose; the two live bodies are byte fixtures in tests.

Review

Two-lens sequence per repo convention, iterated to zero findings: reforge lens (14 findings, all applied), adversarial lens (REJECT cycles driving 20+ fixes: parser bugs proven on the committed fixture, evidence-path normalisation, continuity gaps, module splits per the 300-line seam rule), final reforge on the shipped tree (10 cleanup findings applied). Both lenses APPROVE 3bd3c9a.

Deliberate boundaries (recorded per review)

  • Rubric prose keeps authority ids (`dec.cairn-mission": the alignment convention requires naming the authority) and the real names of real artefacts (spec, registry, schema); only project-insider vocabulary was rewritten.
  • next_action was split into ruling_prompt (chat-first instruction) + reopen_command (exact command) rather than carrying either alone; schema v10 is introduced by this PR so the rename is compatibility-free.

Gates: 55 test suites, pre-archive, dogfood Decision pass, scan --strict, biome CI-exact, eval 16/16 scenarios ux_defect_score=0.

The read-only console shipped in PR #572 with dec.control-plane-programme
unsigned, which Task 1 defines as the stop line; post-signature widening
and the intake lane move to the new todo.console-signed-widening (gated
on the maintainer signature, undeclarable per ruling 4), and
ui-asset-refresh re-points at the two remaining webui-changing units.
Implements todo.pending-queue-briefing (campaign step 5): the pending
queue renders the maintainer's rubric briefing in plain language, and a
session opens with what is waiting and the recommended next action.

- pending wire: ruling_summary, rubric (tier/unblocks/alignment/options),
  local-tier evidence receipts, changed_since_review; SCHEMA_VERSION 10.
- receipt hash comparison is tri-state: a receipt with no comparable
  hash renders as unverified and never claims the text changed; any
  known-stale receipt keeps the changed marker (affects-linked reviews
  included).
- cairn pending <id> renders one briefing on both the human and --json
  paths (CAIRN_COMMAND_FAILED on an unknown id); MCP PendingRequest
  exposes the optional id.
- cairn context opens with Waiting on you: briefings plus the shared
  next-recommended selection; pending errors propagate instead of
  rendering an honest-empty queue; the human path honours --changes-dir.
- webui pending channel shows the same briefing read-only with
  lane-scoped landmarks in the visual harness.
- renderers split per the 300-line seam convention: cli/render/pending.rs
  and handlers/pending_evidence.rs with sibling test modules.
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • View detailed pending-decision briefings, including rulings, rubrics, review evidence, status changes, and recommended next actions.
    • Open a specific pending decision by ID from the command line or query interface.
    • Expand pending items in the web interface and access reopening commands.
    • Context views now summarize unfinished work, active changes, backlog items, and recommended next steps.
  • Documentation

    • Updated command and integration references for pending-decision details and optional IDs.
  • Bug Fixes

    • Improved review-evidence matching and stale-review status reporting.

Walkthrough

The pending queue now supports decision-specific briefings. Query and CLI responses include ruling, rubric, review evidence, change status, and reopen commands. The context view exposes continuity data. The web UI expands pending rows into detailed briefings. The API schema version is 10.

Changes

Pending decision briefing flow

Layer / File(s) Summary
Briefing data and parsing
src/query_api/handlers/pending_*.rs, schemas/PendingResponse.schema.json
Pending decisions now include ruling summaries, rubrics, review receipts, subject-hash status, prompts, and reopen commands.
Queue, context, and CLI integration
src/query_api/handlers/pending.rs, src/query_api/handlers/context.rs, src/cli/..., tests/pending_queue.rs
The queue supports optional ID filtering. Context output includes continuity data. CLI output renders detail views and changed-review status.
Pending web UI and harness checks
src/ui_assets/*, harness/*
Pending rows expand into briefing details. Harness checks validate the channel, detail panel, rubric, evidence, prompt, and reopen control.
Contracts and repository records
schemas/*, src/mcp/mod.rs, src/query_api/mod.rs, docs/*, meta/*, map.json
The response contract and API version advance to 10. Documentation, fixtures, decision records, todo metadata, localization, and source mapping are updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

Poem

A rabbit reads the pending queue,
Finds rubrics, receipts, and rulings too.
Rows unfold with a gentle click,
Hashes say what changed or stuck.
“Reopen here!” the carrots cheer—
Clear briefings make the path appear.


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Docstring Coverage ❌ Error Docstring coverage is 61.34% 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
Title check ✅ Passed The title clearly summarizes the main changes: pending queue briefings and session continuity.
Description check ✅ Passed The description directly explains the pending briefings, session continuity, schema changes, and affected interfaces.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pending-queue-briefing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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: 6

🤖 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 `@docs/design-system/copy.toml`:
- Around line 426-428: Update the evidence-verdict-unknown value used by
render_pending_evidence to “not recorded” so evidence-receipt supplies the only
literal “verdict” prefix and the rendered fallback is not duplicated.

In `@meta/todos/todo.overharness-console-ux.md`:
- Line 3: Update the todo’s status and acceptance criteria consistently: either
change the retained criterion to explicitly allow the documented unsigned
fallback as an alternative, or remove status: done and keep the todo open until
the signed programme decision is recorded. Align the decision with the
acceptance-criteria text around the signed decision and fallback entries.

In `@src/query_api/handlers/pending_brief.rs`:
- Around line 62-73: Update first_ruling_paragraph so
first_body_paragraph(&lines) is used only when find_section finds no
ruling/decision heading; when a section is found, scope any fallback paragraph
search to lines[start..end] and do not fall back to unrelated document-level
prose.

In `@src/query_api/handlers/pending_evidence.rs`:
- Around line 80-98: The assemble loading path must produce deterministic
receipt ordering. Update the final stems or receipts construction in assemble so
the returned receipts Vec is sorted, while preserving the existing review
matching and PendingReceipt field population.

In `@src/ui_assets/channel-bar.js`:
- Around line 134-155: Update the evidence rendering block in the pending-detail
template to render the evidence section unconditionally, removing the outer
evidence truthiness guard. Keep the existing receipts.length conditional so
populated evidence shows the receipt list and absent or empty evidence shows the
pending-no-evidence fallback, while preserving the Evidence label.
- Around line 163-170: The pending ChannelItem lists lack stable keys, allowing
expanded state to move between decisions when rows reorder. Update every
pendingRows ChannelItem call site, including the console mapping, to pass
key={item.id}; keep the existing item prop and rendering behavior unchanged.
🪄 Autofix (Beta)

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: 26ac701f-aea1-4a47-924a-edde369dd277

📥 Commits

Reviewing files that changed from the base of the PR and between cb34935 and 3bd3c9a.

⛔ Files ignored due to path filters (17)
  • tests/snapshots/wire_format_snapshots__api_blueprint.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_dependents_app_api.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_depends_app_api.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_frontier_cycle_error.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_graph.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_lint.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_meta.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_node_app_api.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_node_app_api_contract.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_node_app_api_decisions.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_node_app_api_rationale.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_node_app_api_research.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_node_app_api_sources.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_node_app_api_todos.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_pending.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_roadmap.snap is excluded by !**/*.snap
  • tests/snapshots/wire_format_snapshots__api_status.snap is excluded by !**/*.snap
📒 Files selected for processing (45)
  • docs/commands.md
  • docs/design-system/copy.toml
  • docs/integration-contract.md
  • harness/eval.mjs
  • harness/fixtures/api/pending
  • harness/fixtures/assets/copy.json
  • harness/lib/audit.mjs
  • map.json
  • meta/decisions/bootstrap-fixture-corpus-split.md
  • meta/decisions/control-plane-programme.md
  • meta/todos/todo.console-signed-widening.md
  • meta/todos/todo.overharness-console-ux.md
  • meta/todos/todo.pending-queue-briefing.md
  • meta/todos/todo.ui-asset-refresh.md
  • schemas/PendingResponse.schema.json
  • src/artefacts/registry/manifest.rs
  • src/artefacts/registry/manifest/review_coverage.rs
  • src/cli/mod.rs
  • src/cli/render/mod.rs
  • src/cli/render/pending.rs
  • src/cli/render/project.rs
  • src/cli/render/project/tests.rs
  • src/cli/render/session.rs
  • src/cli/render/session/tests.rs
  • src/mcp/mod.rs
  • src/query_api/handlers/context.rs
  • src/query_api/handlers/mod.rs
  • src/query_api/handlers/pending.rs
  • src/query_api/handlers/pending_brief.rs
  • src/query_api/handlers/pending_brief_live_tests.rs
  • src/query_api/handlers/pending_brief_tests.rs
  • src/query_api/handlers/pending_evidence.rs
  • src/query_api/handlers/pending_evidence_tests.rs
  • src/query_api/handlers/pending_rubric.rs
  • src/query_api/handlers/pending_tests.rs
  • src/query_api/handlers/project.rs
  • src/query_api/mod.rs
  • src/query_api/registry.rs
  • src/scanner/ratification/convergence.rs
  • src/ui/mod.rs
  • src/ui_assets/channel-bar.js
  • src/ui_assets/style.css
  • tests/graph_explorer.rs
  • tests/pending_queue.rs
  • tests/wire_format_snapshots.rs

Comment on lines +426 to +428
evidence-receipt = " Review {stem}: {reviewer}; verdict {verdict}; {match}"
evidence-reviewer-unknown = "reviewer unknown"
evidence-verdict-unknown = "verdict not recorded"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect how evidence-receipt placeholders are filled for the unknown-verdict case.
rg -n 'evidence-receipt|evidence-verdict-unknown|evidence-reviewer-unknown' --type=rust -B3 -A6

Repository: cairn-framework/cairn

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -u

echo "Tracked files around render/pending.rs:"
git ls-files | rg '(^|/)pending\.rs$|docs/design-system/copy\.toml|cairn\.blueprint' || true

echo
echo "Search evidence placeholders across repo:"
rg -n 'evidence-receipt|evidence-verdict-unknown|evidence-reviewer-unknown|{reviewer}|{verdict}' . || true

Repository: cairn-framework/cairn

Length of output: 698


🏁 Script executed:

#!/bin/bash
set -u

echo "Search evidence placeholders across repo (safe regex):"
rg -n 'evidence-receipt|evidence-verdict-unknown|evidence-reviewer-unknown' . || true
echo
rg -n '\{reviewer\}|\{verdict\}' . || true

echo
echo "Inspect src/cli/render/pending.rs outline and relevant sections:"
wc -l src/cli/render/pending.rs
ast-grep outline src/cli/render/pending.rs || true
rg -n 'evidence|receipt|reviewer|verdict|where_left|fill|replace' src/cli/render/pending.rs -C 4 || true

Repository: cairn-framework/cairn

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -u

echo "src/cli/render/pending.rs size:"
wc -l src/cli/render/pending.rs

echo
echo "Lines 1-260 of src/cli/render/pending.rs:"
sed -n '1,260p' src/cli/render/pending.rs | cat -n

echo
echo "Targeted substitution/rendering references in src/cli/render/pending.rs:"
rg -n 'evidence|receipt|where_left|render|replace|pending::|copy|templates|Evidence|Verdict|Reviewer' src/cli/render/pending.rs -C 3 || true

echo
echo "Parse copy entries into a deterministic substitution verifier (does not run repo code):"
python3 - <<'PY'
from pathlib import Path
import re
text = Path('docs/design-system/copy.toml').read_text()
template = None
reviewer = None
verdict = None
for line in text.splitlines():
    line = line.strip()
    if line.startswith('evidence-receipt = ') and template is None:
        m = re.match(r'^evidence-receipt\s*=\s*"(.*)"$', line)
        if m:
            template = m.group(1).encode('unicode_escape').decode('ascii').encode().decode('unicode_escape')
    if line == 'evidence-reviewer-unknown = "reviewer unknown"':
        reviewer = "reviewer unknown"
    if line == 'evidence-verdict-unknown = "verdict not recorded"':
        verdict = "verdict not recorded"
print({"template": template, "reviewer": reviewer, "verdict": verdict})
if template and reviewer and verdict:
    sentence = template.replace("{reviewer}", reviewer).replace("{verdict}", verdict)
    print({"rendered": sentence, "duplicate_verdict": sentence.count("verdict verdict") > 0})
PY

Repository: cairn-framework/cairn

Length of output: 13182


Remove the redundant “verdict” from the unknown verdict fallback.

render_pending_evidence replaces {verdict} directly with pending.evidence-verdict-unknown, and pending.evidence-receipt already contains the literal verdict prefix. This makes the fallback render as verdict verdict not recorded. Use not recorded for the fallback instead.

✏️ Proposed fix
-evidence-verdict-unknown = "verdict not recorded"
+evidence-verdict-unknown = "not recorded"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
evidence-receipt = " Review {stem}: {reviewer}; verdict {verdict}; {match}"
evidence-reviewer-unknown = "reviewer unknown"
evidence-verdict-unknown = "verdict not recorded"
evidence-receipt = " Review {stem}: {reviewer}; verdict {verdict}; {match}"
evidence-reviewer-unknown = "reviewer unknown"
evidence-verdict-unknown = "not recorded"
🤖 Prompt for 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.

In `@docs/design-system/copy.toml` around lines 426 - 428, Update the
evidence-verdict-unknown value used by render_pending_evidence to “not recorded”
so evidence-receipt supplies the only literal “verdict” prefix and the rendered
fallback is not duplicated.

---
node: cairn.ui
status: in_progress
status: done

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Align the terminal status with the acceptance criteria.

status: done marks this work complete. The retained acceptance criterion at Line 49 still requires a signed programme decision, while Lines 61-66 record only the unsigned fallback. Define the fallback as an alternative acceptance criterion, or keep this todo open until the decision is signed.

Also applies to: 61-66

🤖 Prompt for 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.

In `@meta/todos/todo.overharness-console-ux.md` at line 3, Update the todo’s
status and acceptance criteria consistently: either change the retained
criterion to explicitly allow the documented unsigned fallback as an
alternative, or remove status: done and keep the todo open until the signed
programme decision is recorded. Align the decision with the acceptance-criteria
text around the signed decision and fallback entries.

Comment on lines +62 to +73
fn first_ruling_paragraph(body: &str) -> Option<String> {
let lines: Vec<&str> = body.lines().collect();
find_section(&lines, |level, heading| {
// Level 2+ only: an H1 title mentioning "decision" is the document
// name, never the ruling section.
level >= 2 && (heading.starts_with("ruling") || heading.starts_with("decision"))
})
.and_then(|(start, end)| first_paragraph(&lines[start..end]))
.or_else(|| first_body_paragraph(&lines))
.map(|paragraph| clean_markdown(&paragraph))
.filter(|paragraph| !paragraph.is_empty())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Scope the ruling-paragraph fallback to inside the found section.

first_ruling_paragraph falls back to first_body_paragraph(&lines) whenever first_paragraph returns None for the found "ruling"/"decision" section. first_body_paragraph searches the whole document from the top, not the found section. If the section's first line is itself a heading (no lead-in paragraph before a subheading), the function silently picks up unrelated prose from earlier in the document instead of the ruling text.

Only use the whole-document fallback when no "ruling"/"decision" heading is found at all.

🐛 Proposed fix to scope the fallback correctly
 fn first_ruling_paragraph(body: &str) -> Option<String> {
     let lines: Vec<&str> = body.lines().collect();
-    find_section(&lines, |level, heading| {
+    let section = find_section(&lines, |level, heading| {
         // Level 2+ only: an H1 title mentioning "decision" is the document
         // name, never the ruling section.
         level >= 2 && (heading.starts_with("ruling") || heading.starts_with("decision"))
-    })
-    .and_then(|(start, end)| first_paragraph(&lines[start..end]))
-    .or_else(|| first_body_paragraph(&lines))
-    .map(|paragraph| clean_markdown(&paragraph))
-    .filter(|paragraph| !paragraph.is_empty())
+    });
+    let paragraph = match section {
+        Some((start, end)) => first_paragraph(&lines[start..end]),
+        None => first_body_paragraph(&lines),
+    };
+    paragraph
+        .map(|paragraph| clean_markdown(&paragraph))
+        .filter(|paragraph| !paragraph.is_empty())
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn first_ruling_paragraph(body: &str) -> Option<String> {
let lines: Vec<&str> = body.lines().collect();
find_section(&lines, |level, heading| {
// Level 2+ only: an H1 title mentioning "decision" is the document
// name, never the ruling section.
level >= 2 && (heading.starts_with("ruling") || heading.starts_with("decision"))
})
.and_then(|(start, end)| first_paragraph(&lines[start..end]))
.or_else(|| first_body_paragraph(&lines))
.map(|paragraph| clean_markdown(&paragraph))
.filter(|paragraph| !paragraph.is_empty())
}
fn first_ruling_paragraph(body: &str) -> Option<String> {
let lines: Vec<&str> = body.lines().collect();
let section = find_section(&lines, |level, heading| {
// Level 2+ only: an H1 title mentioning "decision" is the document
// name, never the ruling section.
level >= 2 && (heading.starts_with("ruling") || heading.starts_with("decision"))
});
let paragraph = match section {
Some((start, end)) => first_paragraph(&lines[start..end]),
None => first_body_paragraph(&lines),
};
paragraph
.map(|paragraph| clean_markdown(&paragraph))
.filter(|paragraph| !paragraph.is_empty())
}
🤖 Prompt for 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.

In `@src/query_api/handlers/pending_brief.rs` around lines 62 - 73, Update
first_ruling_paragraph so first_body_paragraph(&lines) is used only when
find_section finds no ruling/decision heading; when a section is found, scope
any fallback paragraph search to lines[start..end] and do not fall back to
unrelated document-level prose.

Comment on lines +80 to +98
let receipts = stems
.iter()
.map(|stem| {
let review = reviews.iter().find(|review| review_stem(review) == *stem);
let subject_hash_matches = match (
review.and_then(|review| review.subject_hash.as_deref()),
current_subject_hash,
) {
(Some(reviewed), Some(current)) => Some(reviewed == current),
_ => None,
};
PendingReceipt {
stem: stem.clone(),
reviewer: review.map(|review| review.reviewer.clone()),
verdict: review.and_then(|review| first_verdict_line(&review.body)),
subject_hash_matches,
}
})
.collect::<Vec<_>>();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether scan_result.artefacts.reviews is populated in a stable sorted order.
rg -n 'reviews\s*:\s*Vec<Review>|reviews\.sort|fn load_reviews|reviews\.push' -g '*.rs' src/artefacts src/scanner

Repository: cairn-framework/cairn

Length of output: 291


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg '(^|/)pending_evidence\.rs$|(^|/)pending\.rs$|artefacts/registry/types.rs$|artefacts/registry/kinds.rs$|cairn.blueprint' || true

echo "== outline pending_evidence.rs =="
ast-grep outline src/query_api/handlers/pending_evidence.rs --view expanded || true

echo "== relevant lines pending_evidence.rs =="
sed -n '1,160p' src/query_api/handlers/pending_evidence.rs

echo "== relevant types/kinds for Review =="
sed -n '300,370p' src/artefacts/registry/types.rs
sed -n '220,270p' src/artefacts/registry/kinds.rs

echo "== search all reviews construction/usages =="
rg -n 'reviews|Review|first_verdict_line|review_stem|artifact_sets|receipts|affects' -g '*.rs' src | sed -n '1,240p'

Repository: cairn-framework/cairn

Length of output: 32835


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== scanner artifact loading =="
rg -n 'fn load|scan_result|ArtefactSet|reviews|decisions|affects' -g '*.rs' src/scanner src/artefacts/registry | sed -n '1,260p'

echo "== pending.rs evidence calls =="
sed -n '1,180p' src/query_api/handlers/pending.rs

echo "== review coverage helper =="
sed -n '1,80p' src/artefacts/registry/manifest/review_coverage.rs
rg -n 'load_reviews|find.*review|meta/reviews|rev.*sort|sort_by|DirEntry|WalkDir' -g '*.rs' src/artefacts src/scanner | sed -n '1,220p'

Repository: cairn-framework/cairn

Length of output: 38704


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== artefact kinds implementation =="
sed -n '1,115p' src/artefacts/registry/kinds.rs
sed -n '203,255p' src/artefacts/registry/kinds.rs

echo "== registry load order implementation =="
sed -n '1,65p' src/artefacts/registry/mod.rs
sed -n '1,70p' src/artefacts/registry/io.rs

echo "== deterministic path-order behaviour probe =="
python3 - <<'PY'
import os, tempfile, pathlib

root = pathlib.Path(tempfile.mkdtemp())
root.joinpath("blueprint.toml").write_bytes(b'')
root.joinpath("meta/reviews").mkdir(parents=True)

paths = (
    "a.md", "b.md", "m1/a.md", "m2/a.md", "z.md", "A.md"
)
# create filesystem directory entries in reverse order; os.scandir order is filesystem-dependent.
for name in reversed(paths):
    p = root.joinpath("meta/reviews", name)
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_bytes(b"")

files = tuple(p.name for p in os.scandir(root.joinpath("meta/reviews")) if p.name.endswith(".md"))
print("scandir_order=", files)
print("sorted_orders_match=", tuple(sorted(files)) == (files if False else None))
print("lexicographic=", tuple(sorted(paths)))
PY

Repository: cairn-framework/cairn

Length of output: 10495


Sort the loading path before returning receipts.

Reviews are pushed in filesystem-entry order, and stems are then built from decision.receipts, decision.affects, then reviews. pending_response sorts the final decision rows, but assemble does not sort the resulting receipts Vec. Sort the final stems or receipts before returning to keep the wire payload deterministic.

🤖 Prompt for 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.

In `@src/query_api/handlers/pending_evidence.rs` around lines 80 - 98, The
assemble loading path must produce deterministic receipt ordering. Update the
final stems or receipts construction in assemble so the returned receipts Vec is
sorted, while preserving the existing review matching and PendingReceipt field
population.

Comment on lines +134 to +155
${
evidence
? html`<section class="pending-detail-section pending-detail-evidence">
<p class="plate-meta pending-detail-label">${copy("webui.channel.pending-evidence")}</p>
${
receipts.length
? html`<ul class="pending-detail-list">
${receipts.map(
(receipt) => html`
<li>
<span>${receipt.stem}</span>
<span>${copy("webui.channel.pending-reviewer")}: ${receipt.reviewer || copy("pending.evidence-reviewer-unknown")}</span>
<span>${copy("webui.channel.pending-verdict")}: ${receipt.verdict || copy("pending.evidence-verdict-unknown")}</span>
<span>${receipt.subject_hash_matches === true ? copy("pending.evidence-match") : receipt.subject_hash_matches === false ? copy("pending.evidence-mismatch") : copy("pending.evidence-unverified")}</span>
</li>
`,
)}
</ul>`
: html`<p class="plate-meta">${copy("webui.channel.pending-no-evidence")}</p>`
}
</section>`
: null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Render the empty evidence state when evidence is absent.

Lines 134-155 omit the full evidence section when item.evidence is absent. dec.control-plane-programme in harness/fixtures/api/pending has no evidence object. Its expanded row therefore shows neither the Evidence label nor the pending-no-evidence fallback.

Render the section unconditionally. Use receipts.length to select the receipt list or the empty state.

Proposed fix
-      ${
-        evidence
-          ? html`<section class="pending-detail-section pending-detail-evidence">
+      <section class="pending-detail-section pending-detail-evidence">
               <p class="plate-meta pending-detail-label">${copy("webui.channel.pending-evidence")}</p>
               ${
                 receipts.length
...
                   : html`<p class="plate-meta">${copy("webui.channel.pending-no-evidence")}</p>`
               }
-            </section>`
-          : null
-      }
+            </section>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
${
evidence
? html`<section class="pending-detail-section pending-detail-evidence">
<p class="plate-meta pending-detail-label">${copy("webui.channel.pending-evidence")}</p>
${
receipts.length
? html`<ul class="pending-detail-list">
${receipts.map(
(receipt) => html`
<li>
<span>${receipt.stem}</span>
<span>${copy("webui.channel.pending-reviewer")}: ${receipt.reviewer || copy("pending.evidence-reviewer-unknown")}</span>
<span>${copy("webui.channel.pending-verdict")}: ${receipt.verdict || copy("pending.evidence-verdict-unknown")}</span>
<span>${receipt.subject_hash_matches === true ? copy("pending.evidence-match") : receipt.subject_hash_matches === false ? copy("pending.evidence-mismatch") : copy("pending.evidence-unverified")}</span>
</li>
`,
)}
</ul>`
: html`<p class="plate-meta">${copy("webui.channel.pending-no-evidence")}</p>`
}
</section>`
: null
<section class="pending-detail-section pending-detail-evidence">
<p class="plate-meta pending-detail-label">${copy("webui.channel.pending-evidence")}</p>
${
receipts.length
? html`<ul class="pending-detail-list">
${receipts.map(
(receipt) => html`
<li>
<span>${receipt.stem}</span>
<span>${copy("webui.channel.pending-reviewer")}: ${receipt.reviewer || copy("pending.evidence-reviewer-unknown")}</span>
<span>${copy("webui.channel.pending-verdict")}: ${receipt.verdict || copy("pending.evidence-verdict-unknown")}</span>
<span>${receipt.subject_hash_matches === true ? copy("pending.evidence-match") : receipt.subject_hash_matches === false ? copy("pending.evidence-mismatch") : copy("pending.evidence-unverified")}</span>
</li>
`,
)}
</ul>`
: html`<p class="plate-meta">${copy("webui.channel.pending-no-evidence")}</p>`
}
</section>
🤖 Prompt for 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.

In `@src/ui_assets/channel-bar.js` around lines 134 - 155, Update the evidence
rendering block in the pending-detail template to render the evidence section
unconditionally, removing the outer evidence truthiness guard. Keep the existing
receipts.length conditional so populated evidence shows the receipt list and
absent or empty evidence shows the pending-no-evidence fallback, while
preserving the Evidence label.

Comment on lines 163 to 170
function ChannelItem({ item, kind, onFocus }) {
const label = itemLabel(item, kind);
const nodeId = item?.node || (Array.isArray(item?.nodes) ? item.nodes[0] : undefined);
const showFocus = Boolean(nodeId);
const pending = kind === "pending";
const [expanded, setExpanded] = useState(false);
const toggle = () => setExpanded((value) => !value);
const fullText = [label.title, label.body, label.meta].filter(Boolean).join(" · ");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file overview =="
wc -l src/ui_assets/channel-bar.js
echo

echo "== ChannelBar and ChannelItem contexts =="
sed -n '1,240p' src/ui_assets/channel-bar.js
echo

echo "== ChannelItem usages =="
rg -n "ChannelItem|ChannelBar|items\.map" src/ui_assets -S || true

echo "== git diff stat/name/status if available =="
git diff --stat -- src/ui_assets/channel-bar.js || true
git diff -- src/ui_assets/channel-bar.js | sed -n '1,220p' || true

Repository: cairn-framework/cairn

Length of output: 11148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== deterministic React/Preact key behavior probe (conceptual via source) =="
python3 - <<'PY'
from pathlib import Path
p = Path("src/ui_assets/channel-bar.js")
text = p.read_text()
checks = {
    "ChannelItem has useState expanded": 'const [expanded, setExpanded] = useState(false)' in text,
    "ChannelItem has useId expanded": "expanded" in text and ("useId" in text or "useId(" in text) for _ in [1],
    "ChannelBar maps no key": "items.map((item) => html`<${ChannelItem}" in text and "key=${item.id}" not in text,
    "ChannelBar maps with key": "items.map((item) => html`<${ChannelItem} key=${item.id}" in text,
}
print(checks)
PY

Repository: cairn-framework/cairn

Length of output: 502


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== pending rows construction in src/ui_assets/console.js =="
sed -n '1,130p' src/ui_assets/console.js

echo "== app ChannelBar call =="
sed -n '440,470p' src/ui_assets/app.js

echo "== package/framework snippets mentioning preact/react/jsx =="
fd -a 'package.json|README.md|*.blueprint|blueprint.*' . | sed 's#^\./##' | head -50
for f in package.json README.md docs/design-system; do
  if [ -f "$f" ]; then echo "--- $f"; sed -n '1,220p' "$f"; fi
done

echo "== toolchain declarations =="
find . -maxdepth 2 -type f \( -name 'package.json' -o -name '*.lock' -o -name 'pnpm-lock.yaml' -o -name 'yarn.lock' -o -name 'cairn.blueprint' \) -print | sort | while read -r f; do
  echo "--- $f"
  sed -n '1,220p' "$f"
done

Repository: cairn-framework/cairn

Length of output: 5395


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== framework/tool declarations =="
git ls-files | grep -E '(^|/)(package\.json|pnpm-lock\.yaml|yarn\.lock|package-lock\.json|cairn\.blueprint|\.blueprint|README\.md)$' | sort | readarray -t files
printf '%s\n' "${files[@]}" | while read -r f; do
  echo "--- $f"
  sed -n '1,260p' "$f"
done

echo "== HTML helper implementation if relevant =="
cat -n src/ui_assets/utils.js | sed -n '1,220p'

echo "== behavioral check: current ChannelItem call sites without key =="
python3 - <<'PY'
from pathlib import Path
import re
paths = ["src/ui_assets/channel-bar.js", "src/ui_assets/console.js", "src/ui_assets/app.js"]
for p in paths:
    text = Path(p).read_text()
    m = re.findall(r'<\$\{ChannelItem\}([^>]+)>', text)
    print(p + ":", m)
print("has ChannelItem call with key parameter:", bool(re.search(r'<\$\{ChannelItem\}\s+key=', Path("src/ui_assets/channel-bar.js").read_text() + Path("src/ui_assets/console.js").read_text() + Path("src/ui_assets/app.js").read_text())))
PY

Repository: cairn-framework/cairn

Length of output: 242


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files to inspect =="
git ls-files | grep -E '(^package\.json$|pnpm-lock\.yaml$|yarn\.lock$|package-lock\.json$|cairn\.blueprint$|\.blueprint$|README\.md$)' | sort

echo "== relevant declarations =="
find . -maxdepth 2 -type d \( -name .git -prune \) -o -type f \( -name package.json -o -name pn*lock*.yaml -o -name y*lock -o -name cairn.blueprint -o -name .blueprint -o -name README.md \) -print | sort | while read -r f; do
  echo "--- $f"
  sed -n '1,260p' "$f"
done

echo "== utils.js snippets =="
cat -n src/ui_assets/utils.js | sed -n '1,220p'

echo "== behavioral check: current ChannelItem call sites without key =="
python3 - <<'PY'
from pathlib import Path
import re
files = ["src/ui_assets/channel-bar.js", "src/ui_assets/console.js", "src/ui_assets/app.js"]
all = "".join(Path(f).read_text() for f in files)
print("ChannelItem calls:")
for m in re.findall(r'<\$\{ChannelItem\}([^>]+)>', all):
    print("  ->", m.strip())
print("has ChannelItem call with key=", bool(re.search(r'<\$\{ChannelItem\}\s+key=', all)))
print("ChannelItem declaration has useState expanded:", "const [expanded, setExpanded] = useState(false)" in Path("src/ui_assets/channel-bar.js").read_text())
PY

Repository: cairn-framework/cairn

Length of output: 40556


Use a stable key for each pending ChannelItem.

ChannelItem stores expanded state. The pending rows mapping passes item as a prop but not key, so if a pending refresh changes row order, a component instance can keep the expansion state of another decision. Add key=${item.id} on the ChannelItem call sites, including the console pendingRows mapping.

🤖 Prompt for 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.

In `@src/ui_assets/channel-bar.js` around lines 163 - 170, The pending ChannelItem
lists lack stable keys, allowing expanded state to move between decisions when
rows reorder. Update every pendingRows ChannelItem call site, including the
console mapping, to pass key={item.id}; keep the existing item prop and
rendering behavior unchanged.

@George-RD
George-RD merged commit 51ea73f into main Aug 3, 2026
21 checks passed
@George-RD
George-RD deleted the feat/pending-queue-briefing branch August 3, 2026 05:23
George-RD added a commit that referenced this pull request Aug 3, 2026
* chore(todos): close the pending briefing unit (#574)

* feat(query): surface reverse provenance edges on decisions
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.

1 participant