Skip to content

chore(requirements): replace nightly sync with a command, reconcile DB with the board - #905

Merged
SarahLittlejohn merged 11 commits into
masterfrom
chore/requirements-sync-rework
Aug 4, 2026
Merged

chore(requirements): replace nightly sync with a command, reconcile DB with the board#905
SarahLittlejohn merged 11 commits into
masterfrom
chore/requirements-sync-rework

Conversation

@SarahLittlejohn

@SarahLittlejohn SarahLittlejohn commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Why

The nightly requirements-sync.yml Action ran with a GitHub App token that cannot read Project #43. HMCTSClaudeCode declares repository_projects (Projects classic) but no organisation Projects permission, and ProjectV2 is an org-scoped resource — so the board query returned FORBIDDEN.

Rather than stopping, the job substituted its own proxy for board status: issue closed + merged closing PR. That proxy can only ever detect verified, and it cannot see open issues at all — i.e. every ticket in Refined Tickets. Migrations 004–010 each shipped carrying HUMAN REVIEW REQUIRED: Board access unavailable, every run exited green, and the database fell 127 requirements behind the board.

Fixing the App would mean widening an org-wide App's permissions across all 36 hmcts boards to serve one unattended job. Moving the sync to an interactive command avoids that entirely — a developer's own token already has read:project.

What changed

1. Nightly Action deleted.github/workflows/requirements-sync.yml (−212). Stale reference in docs/GITHUB_MCP.md updated.

2. Migration 011 reconciles the DB with the board — 127 new requirements (REQ-0160…REQ-0286), 25 updated. The DB is now an exact mirror: 0 status mismatches, 0 board issues missing.

3. /qk-requirements-sync (.claude/commands/multi-agent/), backed by one script:

requirements/scripts/fetch_board.sh pages the board and maps columns to statuses. It hard-fails on GraphQL error, on data.node == null (what a token without projects access actually returns instead of an HTTP error), and on zero items. An unreadable board now stops the run instead of degrading it — that's the property whose absence caused this whole problem.

The migration SQL is written directly by the command rather than by a generator: deltas are normally a handful of rows (002–010 inserted 1–5 requirements each), so the command carries the conventions instead — label mappings, impl_paths exclusions, id/ref arithmetic, one-UPDATE-per-requirement versioning, quote escaping. It also forbids substituting any proxy for board status, and verifies the DB mirrors the board exactly before opening a PR — the check the old Action never had. Migration 011 was generated mechanically as a one-off, being 127 rows with ~1,500 apostrophes to escape.

The command also infers requirement_link rows for new requirements: structural links from sub-issues and references as facts, content-based links as origin='inferred', is_suspect=1, with a written rationale.

4. /qk-tickets — source selection + bug fixes. Takes master or pr <number>; with no argument it lists open sync PRs and asks. For a PR it overlays only the migration files onto the current checkout (no branch switch) and reverts afterwards. Adds parallel-work analysis: tickets are unsafe together if linked by depends_on (incl. transitively), conflicts_with, or a parent/child refines/derives_from.

It was also already broken: the query selected story_points and assigned_to, which are not columns in schema.sql, so it errored out entirely; and it documented blocks/related_to link types absent from the CHECK constraint. Now grouped by priority, using the real types.

5. requirements.db untracked. .gitignore has listed requirements/*.db since #695, but the file was already tracked by then — it was swept into #772 (an unrelated list-types PR) the same day, and gitignore does not apply to tracked files. Verified a fresh clone rebuilds it from SQL alone, identical content (both dumps hash to 07b2f180…).

Status changes

The mapping now covers the whole board, not just Refined Tickets and beyond: Backlogdraft, Prioritised Backlogproposed — matching what seed.sql already did for those columns. A ticket moving backwards is mirrored like any other move and recorded in requirement_change. The old never-down-status rule existed to guard against guessed data; with a readable board it just held the DB out of date.

157 of 159 pre-existing requirements are unchanged. Only two moved:

Ref Before After Board column
REQ-0002 (#213) verified draft Backlog
REQ-0110 (#438) approved implemented Ready For Sign Off

Both recorded as status_changed at version 2 with old/new values.

Status master this PR of which new
draft 1 79 77
proposed 3 8 5
approved 1 25 25
in_progress 0 4 4
implemented 2 7 4
verified 152 163 12
total 159 286 127

schema.sql is unchanged — no new status type. in_progress appears in the data for the first time, but it was always in the CHECK constraint and the old workflow already mapped In Progress=in_progress; it simply never managed to write one.

Other field changes on existing rows: 4 granularity backfills (NULL → story, from type:* labels), 19 impl_paths refreshes from merged-PR file lists. No impl_commit_sha changes.

For a human to look at

Verification

  • yarn requirements:build succeeds; PRAGMA integrity_checkok; PRAGMA foreign_key_check → empty.
  • Mirror parity: 0 mismatched, 0 missing.
  • Both fetch_board.sh failure paths tested: bad PROJECT_ID and empty response each exit 1 with a clear cause and no output.
  • yarn lint passes across all 65 packages.

⚠️ One pre-existing, unrelated test failure: @hmcts/court-of-appeal-civil-daily-cause-list fails with Cannot find package '@hmcts/et-daily-list'. Confirmed identical on clean master with these changes stashed.

Note for reviewers

After merging, run yarn requirements:buildrequirements.db is no longer tracked, so a stale local copy won't be updated by git pull.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a requirements synchronisation command for reconciling the database with project-board data and generating reviewed migrations.
    • Added project-board data retrieval with status validation, issue metadata and merged pull request details.
    • Updated ticket listing to support the main branch or a selected pull request, with dependency, conflict and parallelisation insights.
    • Added a migration importing 127 new requirements and reconciling existing records.
  • Documentation

    • Clarified GitHub MCP Server usage and CI workflow coverage.
  • Chores

    • Removed the former scheduled synchronisation workflow.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds GitHub Project ingestion, transactional requirements reconciliation, and source-aware ticket analysis. Replaces the nightly synchronisation workflow with a Claude command and updates related documentation.

Changes

Requirements tooling

Layer / File(s) Summary
Board ingestion and reconciliation migration
requirements/scripts/fetch_board.sh, requirements/migrations/011_reconcile_board_2026_07_29.sql
Reads Project #43 with strict status mapping and pagination. Inserts 127 requirements and reconciles existing records with change history.
Requirements synchronisation command
.claude/commands/multi-agent/qk-requirements-sync.md, .github/workflows/requirements-sync.yml, docs/GITHUB_MCP.md
Defines board validation, migration generation, integrity and parity checks, inferred-link insertion, concurrent PR checks, and migration-only pull-request handling. Removes the nightly workflow and updates its documentation reference.
Source-aware ticket analysis
.claude/commands/multi-agent/qk-tickets.md
Supports master or pull-request migration sources, dependency-aware ticket analysis, link reporting, and temporary-worktree cleanup.

Sequence Diagram(s)

sequenceDiagram
  participant Developer
  participant qk_requirements_sync
  participant GitHubProject43
  participant requirements_db
  participant GitHubPullRequest
  Developer->>qk_requirements_sync: Run synchronisation command
  qk_requirements_sync->>GitHubProject43: Fetch board items
  GitHubProject43-->>qk_requirements_sync: JSONL requirements data
  qk_requirements_sync->>requirements_db: Rebuild and verify migration
  qk_requirements_sync->>GitHubPullRequest: Commit migration and open pull request
  GitHubPullRequest-->>Developer: Return pull-request URL
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarises the main changes: replacing the nightly sync with a command and reconciling the database with the board.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/requirements-sync-rework

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.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

🎭 Playwright E2E Test Results

82 tests   52 ✅  4m 1s ⏱️
31 suites  30 💤
 1 files     0 ❌

Results for commit 43c52f1.

♻️ This comment has been updated with latest results.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (1)
requirements/scripts/generate_sync_migration.ts (1)

157-171: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Title and statement drift is never reconciled.

Only status, priority, granularity and the two impl fields are diffed, so an issue retitled or rewritten on GitHub leaves the database permanently stale — and the parity check in qk-requirements-sync.md Step 4 compares status only, so it will still report a clean mirror. Either diff title/statement too, or state the exclusion explicitly in the file header alongside the requirement_link carve-out.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2fe0d4b8-1ae3-4b83-8dc3-4cc307ec1719

📥 Commits

Reviewing files that changed from the base of the PR and between d9320ea and 641932d.

⛔ Files ignored due to path filters (1)
  • requirements/requirements.db is excluded by !**/*.db
📒 Files selected for processing (7)
  • .claude/commands/multi-agent/qk-requirements-sync.md
  • .claude/commands/multi-agent/qk-tickets.md
  • .github/workflows/requirements-sync.yml
  • docs/GITHUB_MCP.md
  • requirements/migrations/011_reconcile_board_2026_07_29.sql
  • requirements/scripts/fetch_board.sh
  • requirements/scripts/generate_sync_migration.ts
💤 Files with no reviewable changes (1)
  • .github/workflows/requirements-sync.yml

Comment thread .claude/commands/multi-agent/qk-requirements-sync.md Outdated
Comment thread .claude/commands/multi-agent/qk-requirements-sync.md
Comment thread .claude/commands/multi-agent/qk-tickets.md
Comment thread .claude/commands/multi-agent/qk-tickets.md Outdated
Comment thread .claude/commands/multi-agent/qk-tickets.md Outdated
Comment on lines +1599 to +1604
'2025-10-10T11:12:59Z', '2025-10-10T11:12:59Z', 'github-actions[bot]', 'github-actions[bot]');

INSERT INTO requirement_change
(requirement_id, version, change_type, change_summary, changed_by, changed_at)
VALUES
(160, 1, 'created', 'imported from GitHub issue', 'github-actions[bot]', '2025-10-10T11:12:59Z');

Copy link
Copy Markdown
Contributor

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

Provenance is recorded as github-actions[bot], but this migration is now produced by an interactive command.

The nightly Action is gone; created_by/updated_by/changed_by should reflect the actual origin (e.g. qk-requirements-sync) so the audit trail does not point at a workflow that no longer exists. Root cause is the CHANGED_BY constant in requirements/scripts/generate_sync_migration.ts (line 17) — changing it there regenerates this file correctly.

Comment thread requirements/scripts/fetch_board.sh Outdated
Comment thread requirements/scripts/fetch_board.sh Outdated
Comment on lines +1 to +14
// Generate a migration that reconciles the requirements database with the board.
//
// Reads board state (JSON Lines from fetch_board.sh) plus the built database, and
// emits SQL for the delta on stdout. Emits nothing and exits 1 when there is no
// drift, so a caller can distinguish "nothing to do" from "something to write".
//
// Everything here is mechanical: status comes from the board column, priority and
// granularity from labels, impl fields from merged closing PRs. Inferred
// requirement_link rows are deliberately NOT generated — those are judgement and
// are added as a separate reviewable step.
//
// Usage: tsx requirements/scripts/generate_sync_migration.ts <board.jsonl> <db> <today-iso>

import { execFileSync } from "node:child_process";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

File name should be kebab-case.

generate_sync_migration.ts uses snake_case; rename to generate-sync-migration.ts (and update the usage comment plus the npx tsx … invocation in .claude/commands/multi-agent/qk-requirements-sync.md Step 3). As per coding guidelines: "Use kebab-case for file and directory names."

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 13-13: Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFileSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

Source: Coding guidelines

Comment on lines +82 to +85
function queryDb(dbPath: string, sql: string): DbRow[] {
const out = execFileSync("sqlite3", ["-json", dbPath, sql], { encoding: "utf8" }).trim();
return out === "" ? [] : JSON.parse(out);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Node.js child_process execFileSync default maxBuffer size

💡 Result:

The default maxBuffer size for child_process.execFileSync in Node.js is 1024 * 1024 bytes (1 MiB) [1][2]. While older versions of Node.js previously used a default of 200 * 1024 bytes (200 KiB) following a change in 2019 [3][4], the current standard documented in recent versions of Node.js is 1024 * 1024 bytes [1][2]. The maxBuffer option specifies the largest amount of data in bytes allowed on stdout or stderr; if this limit is exceeded, the child process is terminated [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file outline =="
ast-grep outline requirements/scripts/generate_sync_migration.ts --view expanded || true

echo "== relevant queryDb/query usage =="
sed -n '1,140p' requirements/scripts/generate_sync_migration.ts

echo "== migrate requirements migration 011 and impl_paths occurrences in repository =="
git ls-files | rg '011|migration|requirements' | head -n 120
rg -n "impl_paths|migration 011|migration/011|011" . --glob '!node_modules' --glob '!dist' --glob '!build' | head -n 200

echo "== read package/engine context =="
for f in package.json requirements/prisma/schema.prisma; do
  [ -f "$f" ] && { echo "--- $f"; sed -n '1,220p' "$f"; }
done

Repository: hmcts/cath-service

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json, re, pathlib

migration = pathlib.Path("requirements/migrations/010_sync_github_2026_07_26.sql").read_text()
impl_paths = re.findall(r"impl_paths\s*=\.{0,1}\(\d, 1, 'impl_paths', NULL, '(\[.*?\])'", migration, re.S)
if not impl_paths:
    impl_paths = re.findall(r"impl_paths\s*,\s*NULL,\s*'(\[.*?\])'", migration, re.S)

print("impl_paths occurrences:", len(impl_paths))
sizes = []
paths = []
for s in impl_paths:
    sizes.append(len(s.encode("utf-8")))
    paths.append(json.loads(s))

print("total bytes:", sum(sizes))
print("average bytes:", sum(sizes) / len(sizes))
print("max rows:", len(sizes))
print("max entry bytes:", max(sizes))
print("max entry paths:", max(paths, key=len))
print("node default maxBuffer byte:", 1024*1024)
print("rows_needed_to_exceed_1mb_5k_rows:", 1024*1024 // 5000 + 1)
print("rows_needed_to_exceed_1mb_2_6mb_rows:", 1024*1024 // 2600000 + 1)
PY

echo "== current queryDb =="
sed -n '80,88p' requirements/scripts/generate_sync_migration.ts

Repository: hmcts/cath-service

Length of output: 307


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import pathlib

migration = pathlib.Path("requirements/migrations/010_sync_github_2026_07_26.sql").read_text()
print("migration size:", len(migration.encode("utf-8")))
print("contains 367:", "367:" in migration)
print("contains REQ-0099:", "REQ-0099" in migration)
candidates = migration[migation.index("367"):migation.index("367") + 1000] if "367:" in migration else migration[:2000]
print("first 2000 chars with line numbers:")
for i, line in enumerate(migration.splitlines()[:25], 1):
    if "impl_paths" in line or "REQ-0099" in line or "created_at, updated_at" in line or "367:" in line:
        print(i, line[:280])
PY

# Minimal behaviour check for execFileSync maxBuffer option shape.
node - <<'JS'
const { execFileSync } = require('node:child_process');
const out = execFileSync("node", ["-e", "console.log('x');"], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
console.log("maxBuffer option accepted:", out.trim() === "x");
JS

echo "== current queryDb =="
sed -n '80,88p' requirements/scripts/generate_sync_migration.ts

Repository: hmcts/cath-service

Length of output: 731


🏁 Script executed:

#!/bin/bash
set -euo pipefail

migration=$(awk '1==1 && /UPDATE "requirement"/{flag=1} flag<=4{print} /WHERE id = 99/{flag2=1} flag2{print; flag2=0} END{print "END"}' requirements/migrations/010_sync_github_2026_07_26.sql | sed -n '320,390p')
echo "$migration"
python3 - <<'PY'
import pathlib, re, json
migration = pathlib.Path("requirements/migrations/010_sync_github_2026_07_26.sql").read_text()
paths = re.findall(r"'(\[.*?\])'", migration)
impl_paths = [p for p in paths if '"/' in p or ".'" in p]
print("string-array candidates:", len(paths))
print("impl_paths-like candidates:", len(impl_paths))
for p in impl_paths[:5]:
    print("length:", len(p.encode(), "utf-8"))
    data = json.loads(p)
    print("paths:", data[:3] if data else [])
# Also parse line-by-line UPDATE format because the JSON regex depends on implementation detail.
for line in migration.splitlines():
    if "UPDATE \"requirement\"" in line or "impl_paths" in line or "WHERE id = 99" in line:
        print(line[:280])
PY

Repository: hmcts/cath-service

Length of output: 5471


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== all lines containing id 859 and impl_paths assignments with adjacent context =="
rg -n -C 3 "id = 859|859,'|impl_paths" requirements/migrations/010_sync_github_2026_07_26.sql requirements/migrations/011_reconcile_board_2026_07_29.sql requirements/schema.sql

echo "== migration line counts =="
wc -l requirements/migrations/010_sync_github_2026_07_26.sql requirements/migrations/011_reconcile_board_2026_07_29.sql requirements/schema.sql

Repository: hmcts/cath-service

Length of output: 50375


Set an explicit maxBuffer for the sqlite3 query.

The first queryDb() returns every requirement with its impl_paths JSON, and existing migrations already show multi-KB path arrays. As the requirement set grows, this output can exceed execFileSync’s default 1 MiB buffer and fail with ENOBUFS before drift is reported. Set a generous explicit maxBuffer.

@SarahLittlejohn

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7456424e-7ea9-4c3f-9cd7-17fe8ec6617e

📥 Commits

Reviewing files that changed from the base of the PR and between 641932d and ab38c3e.

📒 Files selected for processing (4)
  • .claude/commands/multi-agent/qk-requirements-sync.md
  • .claude/commands/multi-agent/qk-tickets.md
  • requirements/migrations/011_reconcile_board_2026_07_29.sql
  • requirements/scripts/fetch_board.sh
🚧 Files skipped from review as they are similar to previous changes (2)
  • requirements/scripts/fetch_board.sh
  • requirements/migrations/011_reconcile_board_2026_07_29.sql

Comment thread .claude/commands/multi-agent/qk-requirements-sync.md Outdated
Comment thread .claude/commands/multi-agent/qk-tickets.md
@SarahLittlejohn

Copy link
Copy Markdown
Collaborator Author

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.claude/commands/multi-agent/qk-requirements-sync.md (2)

200-212: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fail when SQLite verification reports a failure.

set -e only checks the sqlite3 process exit code. PRAGMA integrity_check can print not ok, and PRAGMA foreign_key_check can print violations while sqlite3 still exits successfully. The command can therefore continue with an invalid database.

Capture both outputs and exit unless the integrity result is exactly ok and the foreign-key result is empty.


214-232: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make the parity check complete and enforce its result.

The query compares only issue_number and status, although Step 4 also reconciles priority, granularity, impl_commit_sha, and impl_paths. Incorrect values in those fields can pass verification. The block also prints non-empty arrays without failing.

Compare every reconciled field and use jq -e or an equivalent shell assertion before opening the PR.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ce771d0-b2c6-4516-a05e-1be6186b9eb5

📥 Commits

Reviewing files that changed from the base of the PR and between ab38c3e and ea56fe6.

📒 Files selected for processing (2)
  • .claude/commands/multi-agent/qk-requirements-sync.md
  • .claude/commands/multi-agent/qk-tickets.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • .claude/commands/multi-agent/qk-tickets.md

Comment thread .claude/commands/multi-agent/qk-requirements-sync.md
Comment thread .claude/commands/multi-agent/qk-requirements-sync.md Outdated
Comment thread .claude/commands/multi-agent/qk-requirements-sync.md Outdated
SarahLittlejohn and others added 7 commits August 4, 2026 12:53
…ith board

The nightly requirements-sync Action ran with a GitHub App token that cannot
read Project #43: HMCTSClaudeCode declares repository_projects (Projects
classic) but no organisation Projects permission, and ProjectV2 is an
org-scoped resource. The ProjectV2 query returned FORBIDDEN, and rather than
stopping, the job substituted "issue closed + merged closing PR" as a proxy for
board status. That proxy cannot see open issues, so every ticket in Refined
Tickets was invisible to it. Migrations 004-010 each shipped carrying "Board
access unavailable / HUMAN REVIEW REQUIRED", the run exited green every night,
and the database fell 126 requirements behind the board.

Rather than widen an org-wide App's permissions to fix an unattended job, move
the sync to an interactive command where a developer's own token already has
read:project.

- Delete .github/workflows/requirements-sync.yml.
- Add requirements/scripts/fetch_board.sh: pages the board and maps each column
  to a status. Exits non-zero if the board is unreadable, including the
  data.node == null case a token without projects access actually returns
  instead of an HTTP error. An unreadable board now stops the run.
- Add requirements/scripts/generate_sync_migration.ts: emits the delta as SQL,
  exits 1 when there is none. Output is deterministic, so a rerun on unchanged
  input regenerates byte-identical SQL.
- Add /qk-requirements-sync, which fetches the board, generates the migration,
  infers requirement_link rows for new requirements, verifies the mirror is
  exact, and opens a PR.
- Add migration 011: 127 new requirements (REQ-0160..REQ-0286), 25 updated.
  Every board issue now has a matching row with a matching status.

The column mapping covers the whole board, not just Refined Tickets and beyond.
Backlog maps to draft and Prioritised Backlog to proposed, matching what
seed.sql already did for those columns, so a ticket moving backwards is
mirrored like any other move and recorded in requirement_change. The old
never-down-status rule existed to protect against guessed data; with a
readable board it just held the database out of date.

/qk-tickets: its query referenced story_points and assigned_to, which are not
columns in schema.sql, so it errored out entirely; it also described 'blocks'
and 'related_to' link types that are not in the CHECK constraint. Group by
priority instead, use the real link types, and add a source argument so it can
read either master or an open sync PR's migration. It now also works out which
approved tickets are safe to run in parallel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Biome lint/style/useTemplate. Generated SQL is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The database is derived entirely from schema.sql + seed.sql + migrations/*.sql,
which init_db.sh applies in order. The migrations describe what is in it, so the
binary adds nothing a clone cannot rebuild — and committing it means every sync
PR carries a ~2MB binary diff that no reviewer can read.

.gitignore has listed requirements/*.db since #695, but the file was already
tracked by then: it was swept into #772 (an unrelated list-types PR) the same
day, and gitignore does not apply to tracked files. Remove it from the index;
it stays on disk and is now correctly ignored.

Rebuild with: yarn requirements:build

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fetch_board.sh
- Fail on any board column with no status mapping, naming the column and the
  offending issues. Previously such items were filtered out silently, which is
  the same "quietly incomplete" failure the script exists to prevent: a renamed
  column would drop items from the feed, and the Step 4 parity check only
  compares issues that are in the feed, so it would still pass.
- Define the column mapping once and share it between the guard and the emit
  step so the two cannot drift apart.
- Use mktemp plus a trap for the stderr capture instead of a predictable
  /tmp/...$$ path that leaked on the early-exit branches.
- Count only issue-bearing items, and report how many were mapped.

qk-tickets
- Build in a temporary worktree at origin/master. Previously `master` built from
  whichever branch was checked out, and the PR path overlaid onto that, so a
  feature branch could produce a database that was neither master nor master
  plus the PR.
- This also removes the dangerous cleanup: `git checkout -- requirements/`
  would have discarded a user's own tracked edits while failing to delete an
  untracked overlaid migration. Cleanup is now removing the worktree, and it
  asserts the main checkout is untouched.
- Normalise `pr <number>` to a bare number before use; `$ARGUMENTS` is not
  positional, so `$1` was never set.
- Separate `gh pr diff`'s exit status from grep's no-match, so an auth or
  network failure is not reported as "this PR has no migrations".
- Compute the transitive closure with a recursive CTE. The adjacent-links
  queries missed paths through non-approved requirements, so two tickets linked
  via an intermediate could be called safe to parallelise.
- Point the agent at the worktree database, not the main checkout's.
- Fix a reference to "Step 6"; the restore step is Step 4.

qk-requirements-sync
- Fetch the remote branch and branch from origin/ when reusing an open sync PR;
  the previous checkout assumed a local ref that may not exist.

Migration 011 and the command now record provenance as 'qk-requirements-sync'
rather than 'github-actions[bot]', which pointed at a workflow this PR deletes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two fixes from the second review pass.

qk-tickets: the recursive CTE walked through requirements of any status, which
contradicted the rule stated a few lines below it — that a dependency on a
verified requirement is already satisfied and does not block. So approved
A -> verified X -> approved B reported A and B as mutually blocking when the
only thing between them was finished work. On the current link graph 92 of 187
transitive paths run through a verified node, so this materially
over-constrained the parallel set.

Traversal now stops AT a verified requirement instead of through it. Verified
nodes still appear as directly-reached, which is what lets Step 3 report the
dependency as satisfied. Verified against a synthetic graph: A -> verified X ->
B does not link A to B, while A -> draft Y -> C still does.

qk-requirements-sync: stage the one migration path this run produced rather than
`git add requirements/migrations/`, which would sweep in any other edit or stray
file in that directory, and refuse to commit if anything else is staged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An unmerged sync PR is not in master, so the next run rebuilt from master,
re-detected everything that PR had already added as still missing, and handed
out the same ids and REQ-NNNN refs a second time. id is the primary key and ref
is UNIQUE, so both migrations could never apply together: init_db.sh fails and
the database stops building. The old Step 6 branch-reuse looked like it covered
this, but by then the SQL was already numbered against master.

Move the check to Step 1, before the board fetch and the delta, and stop rather
than try to extend the open PR — keeping ref allocation single-writer is the
simplest way to guarantee a ref is never issued twice. Step 6 re-checks, since a
PR can be opened while a run is in progress.

Match the sync branch shape exactly (chore/requirements-sync-<YYYY-MM-DD>), not
just its prefix: a hand-made branch like chore/requirements-sync-rework would
otherwise block every sync. Same pattern applied to the qk-tickets PR list.

Step 2b also now requires requirements/ to be clean, since a stray local
migration would shift MAX(id) and the ref sequence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ongoing deltas are a few rows at a time, so a 242-line generator to emit
them was more machinery than the job needs. Delete generate_sync_migration.ts
and update migration 011's header to say it was machine-generated rather
than pointing at a deleted script.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@SarahLittlejohn
SarahLittlejohn force-pushed the chore/requirements-sync-rework branch from 43c52f1 to 9992679 Compare August 4, 2026 12:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a574499-8e56-48bd-a887-e3247da41f29

📥 Commits

Reviewing files that changed from the base of the PR and between 43c52f1 and 9992679.

⛔ Files ignored due to path filters (1)
  • requirements/requirements.db is excluded by !**/*.db
📒 Files selected for processing (6)
  • .claude/commands/multi-agent/qk-requirements-sync.md
  • .claude/commands/multi-agent/qk-tickets.md
  • .github/workflows/requirements-sync.yml
  • docs/GITHUB_MCP.md
  • requirements/migrations/011_reconcile_board_2026_07_29.sql
  • requirements/scripts/fetch_board.sh
💤 Files with no reviewable changes (1)
  • .github/workflows/requirements-sync.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/GITHUB_MCP.md

Comment thread .claude/commands/multi-agent/qk-requirements-sync.md
Comment thread .claude/commands/multi-agent/qk-requirements-sync.md
Comment on lines +375 to +391
# Re-check: the guard ran before the board fetch and delta, so a PR could have been
# opened in between. Cheap to repeat, and the alternative is colliding refs.
# Use the same date-shaped regex as Step 1 so rework/manual branches are not mistaken
# for a sync in flight.
RECHECK=$(gh pr list --state open --search "head:chore/requirements-sync-" \
--json number,headRefName \
--jq '.[] | select(.headRefName | test("^chore/requirements-sync-[0-9]{4}-[0-9]{2}-[0-9]{2}$")) | .number' \
| head -1)
if [ -n "$RECHECK" ]; then
echo "A sync PR was opened while this run was in progress — discarding this migration."
echo "Remove ${MIGRATION}, then re-run once that PR is merged or closed."
exit 1
fi

# The migration is untracked, so it survives the branch switch.
git fetch --quiet origin master
git checkout -b "$BRANCH" origin/master

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Serialise concurrent sync runs before allocating IDs.

The open-PR checks are not a lock. Two invocations can pass both checks before either PR is visible, build the same origin/master baseline, allocate the same IDs and REQ references, and race on the date branch. Add an atomic local or remote lock before generating the migration. Do not rely on the recheck.

Comment thread .claude/commands/multi-agent/qk-tickets.md
Comment thread .claude/commands/multi-agent/qk-tickets.md
Comment on lines +200 to +212
WITH RECURSIVE reachable(root_id, id, depth) AS (
SELECT r.id, r.id, 0
FROM requirement r
WHERE r.status = 'approved' AND r.issue_number IS NOT NULL
UNION
SELECT rc.root_id, rl.target_id, rc.depth + 1
FROM reachable rc
JOIN requirement mid ON mid.id = rc.id
JOIN requirement_link rl ON rl.source_id = rc.id
WHERE rl.type IN ('depends_on', 'derives_from', 'refines')
AND rc.depth < 20
AND (rc.depth = 0 OR mid.status <> 'verified')
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve dependency paths deeper than 20.

depth is part of the recursive row, so UNION does not deduplicate a node revisited at a new depth. The depth limit prevents non-termination, but it also hides valid dependency paths longer than 20. Those tickets can then be reported as safe to parallelise. Track visited node IDs in the recursive state. If traversal is truncated, mark the result incomplete and do not report the pair as safe.

Also applies to: 230-233

Comment on lines +8828 to +8835
INSERT INTO requirement
(id, ref, title, statement, kind, status, priority, granularity,
issue_number, issue_url, impl_commit_sha, impl_paths,
created_at, updated_at, created_by, updated_by)
VALUES
(221, 'REQ-0221', 'Implement Third-Party Inbound Publication API', '## User Story

As a platform engineer, I want cath-service to expose a standardised inbound REST API so that CaTH can push new, updat

Copy link
Copy Markdown
Contributor

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: Rebuild the requirements database from SQL sources and check version literals in migration 011.
set -euo pipefail

fd -t f . requirements --extension sql --extension md | sort

python3 - <<'PY'
import pathlib, re, collections

mig = pathlib.Path("requirements/migrations/011_reconcile_board_2026_07_29.sql")
text = mig.read_text(encoding="utf-8", errors="replace")

# Target version and id per UPDATE block.
updates = re.findall(
    r"UPDATE requirement SET.*?version\s*=\s*(\d+).*?WHERE id\s*=\s*(\d+);",
    text, re.S)
print("UPDATE (id -> new version):")
for ver, rid in updates:
    print(f"  id={rid} -> version={ver}")

# Versions asserted in the paired change rows.
changes = re.findall(
    r"INSERT INTO requirement_change\s*\([^)]*\)\s*VALUES\s*\(\s*(\d+)\s*,\s*(\d+)\s*,",
    text)
pairs = collections.Counter(changes)
dupes = {k: v for k, v in pairs.items() if v > 1}
print("\nDuplicate (requirement_id, version) audit pairs inside this migration:",
      dupes or "none")

# Cross-check that each UPDATE has a matching change row at the same version.
upd = {(rid, ver) for ver, rid in updates}
chg = set(changes)
print("\nUPDATE rows with no matching change row:", sorted(upd - chg) or "none")
PY

# Show the highest version each requirement reaches in migrations 001-010.
fd -e sql . requirements/migrations --exec rg -n --with-filename 'version\s*=\s*[0-9]+' {} \
  | rg -v '011_reconcile_board' || true

Repository: hmcts/cath-service

Length of output: 2980


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import pathlib, re, sqlite3

schema = pathlib.Path("requirements/schema.sql").read_text()
seed = pathlib.Path("requirements/seed.sql").read_text()

conn = sqlite3.connect(":memory:")
conn.executescript(schema)
conn.executescript(seed)

for mig in sorted(pathlib.Path("requirements/migrations")
                   .glob("00*.sql"))[:11]:
    text2 = mig.read_text(errors="replace").replace("CREATE OR REPLACE FUNCTION", "CREATE FUNCTION")
    conn.executescript(text2)

rows = conn.execute("SELECT id, version FROM requirement ORDER BY id").fetchall()
print("requirement id -> version after migrations 001-011:")
for r in rows:
    print(f"  id={r[0]} version={r[1]}")

required = {2,7,78,99,100,101,102,103,104,105,106,107,108,109,110,112,124,126,129,130,131,135,157,158,159}
actual = {r[0] for r in rows}
print("\nIDs updated in 011 present after migration replay:", required <= actual)

mig11 = pathlib.Path("requirements/migrations/011_reconcile_board_2026_07_29.sql").read_text()
updates = re.findall(r"UPDATE requirement SET.*?id\s*=\s*(\d+);", mig11, re.S)
versions_on_011 = re.findall(r"version\s*=\s*(\d+).*?WHERE id\s*=\s*(\d+);", mig11, re.S)
for version, rid in versions_on_011:
    current = next((r[1] for r in rows if r[0] == int(rid)), None)
    print(f"id={rid}: 011 sets version={version}; replayed version={current}; match={version == str(current)}")

# Full replay of migrations 001-011.
conn2 = sqlite3.connect(":memory:")
conn2.executescript(schema)
conn2.executescript(seed)
for mig in sorted(pathlib.Path("requirements/migrations").glob("0*.*sql"))[:11]:
    text = mig.read_text(errors="replace").replace("CREATE OR REPLACE FUNCTION", "CREATE FUNCTION")
    conn2.executescript(text)
print("\nAll updates in 011 match final replayed versions:", all(
    row[1] == target_new for row in conn2.execute("SELECT id, version FROM requirement WHERE id IN (" + ",".join(updates or ["0"]) +")")
    for target_new, row in [(next(v for v,_ in versions_on_011 if _==u), row)]
    for u in [updates[versions_on_011.index((target_new, row[0]))]]
))
PY

Repository: hmcts/cath-service

Length of output: 4856


Base migration 011 on the current requirement versions.

The hardcoded target versions in requirements/migrations/011_reconcile_board_2026_07_29.sql do not match the versions produced by replaying migrations 001-011. Several target rows update non-existent requirements, and the update for id=7 can regress requirement.version from 2 to 2 before later migrations expect subsequent increments. Derive these literals from current audit/version state instead of a stale snapshot, and consider version = version + 1 for future reconciliations.

Comment on lines +61 to +69
labels(first: 50) { nodes { name } }
closedByPullRequestsReferences(first: 20, includeClosedPrs: true) {
nodes {
number
state
mergeCommit { oid }
files(first: 100) { nodes { path } }
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Paginate every nested Project connection.

labels(first: 50), closedByPullRequestsReferences(first: 20), and files(first: 100) fetch only their first page. The query does not inspect pageInfo. The synchronisation command can therefore derive incorrect priority, granularity, impl_commit_sha, or impl_paths while the fetch still succeeds. Add nested pagination or fail when hasNextPage is true.

129 new requirements (REQ-0160–REQ-0288), 17 status/field changes, 110
inferred dependency links. Parity check passed against board.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SarahLittlejohn and others added 3 commits August 4, 2026 13:37
011 is already taken by the July 29 catch-up.

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

Migration 012 was generated from master without 011 applied, so it
assigned ids 160-288 which 011 had already claimed. Shifted all new
requirement ids and refs by +129 (289-417, REQ-0289 to REQ-0417) and
updated the corresponding requirement_change and requirement_link
references. Build now passes: 415 requirements, integrity_check ok.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The unconfirmed-link display section references both fields but they
were missing from the SELECT lists.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@SarahLittlejohn
SarahLittlejohn merged commit 38d5541 into master Aug 4, 2026
10 checks passed
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.

2 participants