Skip to content

fix(W4 sweep): red suite, an overstated type-safety verdict, and one-command start - #154

Merged
troysatchell merged 27 commits into
mainfrom
fix/w4-sweep-correctness
Aug 8, 2026
Merged

fix(W4 sweep): red suite, an overstated type-safety verdict, and one-command start#154
troysatchell merged 27 commits into
mainfrom
fix/w4-sweep-correctness

Conversation

@troysatchell

@troysatchell troysatchell commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Three items from the W4 requirements sweep's PM scope gate (audit/requirements/pm-triage.md), bundled into one branch and one review. None has a Linear ticket — deliberate PM ruling: they were dispositioned "fix now", and filing tickets for work being done in the same session is overhead rather than coordination.

Gate: pass — all checks green, including tests:not-weakened and regression-test (4 cases in 2 files). Evidence in .factory/gate-result.json.


1. W4-R35 — the suite is red, for one trivial reason (5126a03)

Problem. pnpm test exits 1. Two failures, both in api/src/db/__tests__/migrationRunner.test.ts (lines 167, 184), which is the regression test for DB-1 / TRO-178.

Cause — observed, not inferred. The test compared a Postgres-ordered query (SELECT version FROM schema_migrations ORDER BY version) against a JavaScript [...expected].sort(). Those collations disagree on exactly one pair out of 46 migration names. Verified directly:

SELECT v FROM (VALUES ('020_document_associations'),('020b_sprint_assignee_ids')) t(v) ORDER BY v;
  -> 020b_sprint_assignee_ids, 020_document_associations
['020_document_associations','020b_sprint_assignee_ids'].sort()
  -> 020_document_associations, 020b_sprint_assignee_ids

Nothing was wrong with the migrations. The collision was introduced by 426f0e2 ("fix: resolve migration numbering collisions") — the fix for numbering collisions created an ordering one. It is the only such pair among all 46.

Change. recordedVersions() now sorts in JS after the query, so both sides of every comparison use one collation. The SQL ORDER BY stays for anyone reading the query in isolation.

What was deliberately NOT done. The assertion remains a strict, ordered toEqual on full identity. It was not loosened to a count, a subset check, or expect.arrayContaining — any of which would have gone green by destroying the DB-1 guarantee the test exists to enforce ("the runner must apply every migration or fail"). A runner that skips or duplicates a migration still fails this test.

Why this one first. It repairs the second clause of this project's own definition of done — "the full suite still passes" — which currently fails for all 121 tickets marked Done, and it was blocking the tests:api gate on every other branch.

2. W4-R10(a) — a stated target reported as met (8386f26)

Problem. docs/IMPROVEMENTS.md recorded the type-safety category's verdict as met. The requirement's threshold is defined on the count.sh tracked total, and that total has never measured below baseline.

Measurement — re-run this session with the exact command audit/type-safety/baseline.md:14-18 prescribes:

baseline (076a183) prior recount HEAD
tracked total 1535 1778 1987

+452 / +29%, where W4-R10 requires −25% (target ≤1151). Breakdown: any 50 + as 1882 + non-null 47 + ts-ignore 8, summed by baseline.md:77's own formula.

Precision about the defect. This was not a hidden claim. The document already marked its inference explicitly and printed "1535 → 1778, Up 243" in the open. The narrow, defensible correction is that the per-ticket sum-of-diffs answers a different question than the threshold names. That argument is preserved in the document, demoted from verdict-justification to supporting evidence — the real wins are real (any halved 102 → 50; req.userId!/req.workspaceId! 236 → 0) and are still recorded.

The file grew (+62/−35): nothing honest was removed.

3. W4-R42 — one-command start stopped at the database (5030ad0)

Problem. W4-R42 requires one command to start the full composed system "without any manual setup steps beyond installing dependencies." ./start.sh did everything correctly given a reachable Postgres, but did not start Postgres itself — it failed at api/src/db/ensureDatabase.ts:53-61.

Observed, first-hand: this is what blocked the requirements sweep earlier today on a clean machine — ERROR: Cannot reach PostgreSQL at localhost:5433. Start it, then re-run ./start.sh.

Change. scripts/dev.sh now brings the docker-compose.local.yml Postgres up when unreachable and waits for health, falling back to the original message when Docker is unavailable. New api/src/db/postgresReachable.ts with its own test. ensureDatabase.ts is unchanged — its message is correct and is cited as evidence elsewhere.


Bundled rather than split because CodeRabbit rate-limited this factory once on PR-per-ticket. Dependency pinning (W4-R38) and the Terraform plan annotations (W4-R27) are deliberately separate PRs: each would have swamped review of these three.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Local development can automatically start and connect to the bundled PostgreSQL service when no database is configured.
    • Added database reachability checks with clearer startup feedback and connection handling.
  • Bug Fixes

    • Migration validation now works consistently across database environments while detecting missing migrations.
    • Improved handling of unavailable or invalid database connections.
  • Documentation

    • Added comprehensive requirements-audit reports, gap tracking, verification results, and project improvement guidance.
    • Corrected the documented type-safety status and updated supporting metrics.
    • Added guidance for reproducing and maintaining requirements audits.

troysatchell and others added 18 commits August 8, 2026 11:18
Portable skill: PDF guidelines -> requirements inventory -> three-way
traceability matrix (requirement <-> Linear ticket <-> code file:line),
behavioral verification gating VERIFIED, gaps handoff for PM agent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pm_skill config field, init auto-detection, phase-5 invocation with
passive fallback when no PM skill exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lignment)

10 tasks: skill files with full content (config template, inventory
format, report format, SKILL.md), discoverability check, Ship shakedown
(init/extract/baseline), determinism check, skill-audit pass + memory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review caught the plan's Task 2 content block contradicting the spec's
error-handling rule (stop-and-ask on PDF hash mismatch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…per, quote verification)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
54 requirements extracted from GFA_Week_4_ShipShape_Updated.pdf with stable
W4-R<n> IDs and verbatim quotes. Includes the pdf_tool text cache
(source-W4.md) that every quote was mechanically verified against, plus the
config's filled sha256 and pdf_tool backend.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
54 active requirements traced to code with file:line evidence:
2 VERIFIED, 41 IMPLEMENTED-UNVERIFIED, 10 PARTIAL, 1 ASSUMED, 0 MISSING.

Behavioral verification run by the controller: seed floors met (500 docs /
255 issues / 20 users / 35 sprints, after db:seed + audit/seed-augment.ts);
Render deployment live (HTTP 200); pnpm type-check green.

Two findings the sweep turned up by re-measuring rather than trusting a
claim: docs/IMPROVEMENTS.md records the type-safety target as met, but the
audit's own count.sh gives 1987 tracked violations against a 1535 baseline
(+29%, wrong direction); and pnpm test exits 1 on two migrationRunner
assertions that compare a Postgres-ordered query against a JS sort.

Ticket mapping is BLOCKED — the Linear connector is unauthorized, recorded
as a blocked dimension rather than as absent tickets. Config's code_roots
widened beyond application source: this doc set's acceptance evidence lives
mostly in audit/, terraform/, .github/ and repo-root docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Regenerated REPORT.md/gaps.md/matrix.baseline.json from corrected trace
sources — the artifacts are generated, so every fix landed upstream in the
cluster JSONs, the verification results, or the two render scripts.

I1  W4-R47's evidence had zero valid citations: `.git/refs (branch listing):1`
    is not a file, and neither merge-commit subject appears in CHANGES.md
    (line 10769 is the TRO-248 heading). Replaced with three CHANGES.md lines
    that genuinely name the branch each improvement landed on (10763, 14335,
    14357); the git-history evidence moved into notes with verified SHAs
    (1474cb1, a1ad4f3, b35b817, 1ed03a4, 505e542, 1e4b68f, af056fa, dbae2af)
    and the branch list. Verdict unchanged.
I2  Disclosed that the sweep wrote to the developer's database: W4-R13's seed
    run targeted ship_standup, `pnpm test` then truncated 15 tables via
    api/src/test/setup.ts:93-98, and it was re-seeded afterwards. Added a hard
    rule to the skill requiring a throwaway database or an explicit report.
I4  Summary now leads with the two urgent findings — type-safety target unmet
    (1987 vs 1535, +29%) while docs/IMPROVEMENTS.md records it met, and a red
    suite at HEAD — instead of the blocked ticket dimension.
I5  New "Coverage and limitations" block right after the Summary: e2e never
    ran, ticket mapping blocked, the database mutation, 41/54 rows statically
    traced only, and which dirty paths are actually cited.
I4b "Verification performed" now tables every command run with its real
    result, including pnpm test, pnpm type-check, count.sh, and the e2e suite
    marked NOT RUN. Sourced from a new `commands_run` matrix field.
D1  report-format.md documents ticket_mapping, needs_ruling, commands_run,
    dirty_paths, suggested_scope, and PDF page citations; blocked rows now
    carry tickets:["BLOCKED"] so the matrix distinguishes "never checked" from
    "checked, none found". SKILL.md reconciled to match.
D2  gaps.md restores the documented "Suggested scope" field — all 10 entries,
    derived from each row's notes and pm-triage.md.
M1  api/vitest.config.ts:24 -> :27 (the coverage block). Dropped the
    unreproducible memory-bank/activeContext.md:25 citation from W4-R35 and
    moved the claim into notes with the reason. Matrix records dirty_paths.
M2  Removed both self-citations into the excluded audit/requirements root
    (source-W4.md:332, inventory.md:6); replaced with the source PDF's
    p.12 and p.7.
M3  Rewrote W4-R10/R33/R34/R35/R37/R51 notes into third person, dropping
    "DEVIATING from the dispatch instruction", "this cluster", "the
    controller", and the duplicated 1987 recount. Facts preserved.
M4  Corrected the W4-R10 accusation: docs/IMPROVEMENTS.md:27-28 marks its
    inference explicitly and prints 1535 -> 1778 "Up 243" openly. The verdict
    (PARTIAL) and the number (+29%) stand; the characterisation now says the
    claim is disclosed and it is the literal threshold that is unmet. Same
    correction applied in pm-triage.md.
M6  requirements.config.yaml drops `.` from code_roots (it admitted the whole
    repo and made every other root redundant) in favour of the workspace
    packages plus the seven repo-root files the requirements actually cite.

Also found and fixed while re-verifying every citation: CHANGES.md:9765 ->
:9785 with the quote made verbatim (W4-R10), and an off-by-one line reference
in a W4-R50 note. No verdict changed; 54 rows, acceptance check green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ruling I-01 (Troy, 2026-08-08): W4-R26's "navigate to terraform/" means the
local-provider and Render exercise directories the same passage asks you to
author, not the pre-existing AWS root. W4-R26 moves ASSUMED ->
IMPLEMENTED-UNVERIFIED with interpretation I-01; needs_ruling is now empty and
no verdict in the matrix is contingent. REPORT.md gains an "Interpretation
rulings applied" section so a reader can see which verdicts rest on a
judgement call.

The review flagged that the sweep skipped its own ambiguity protocol here: only
one ambiguity surfaced, far under the ~5 flood cap, so the skill required
asking rather than emitting ASSUMED. Asked and settled.

Also moves the generator into audit/requirements/pipeline/ (cluster traces,
verification results, three scripts, README). REPORT.md and gaps.md were
previously derivable only from a session scratch directory, so the baseline
stopped being reproducible once that session ended — which an audit demanding
reproducible before/after measurement should not ship. Verified: re-running the
committed pipeline reproduces all 54 verdicts identically.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pm-triage.md said "the 41 IMPLEMENTED-UNVERIFIED requirements"; applying
ruling I-01 moved W4-R26 into that tier, so it is 42. A wrong count in a
deliverable is the same defect class this sweep exists to find.

Also rewrites a W4-R33 evidence note that still addressed the fan-out rather
than the reader ("not independently deep-traced by this cluster").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d, scoped

Phase 2 was BLOCKED on an unauthorized Linear connector; it is authorized now,
so the sweep's third dimension is live. 33 of 54 requirements have covering
tickets, 21 have none, and 9 in-scope tickets map to no requirement.

Scoped by Linear PROJECT, not by issue-number range, and that distinction is
the finding. The TRO team is a personal catch-all spanning six projects whose
issue numbers interleave: TRO-250..275 are Clavira Pilot Readiness, TRO-312..365
mostly FleetGraph (Week 5 — same repo, different assignment). Sweeping the team
unscoped produced 88 orphans, ~80 of them simply other products' work. Scoped to
"ShipShape Audit Remediation" (123 issues) it is 9, and those 9 are real
signal: DB-11/DB-12, the CodeQL security tickets, and two TF-7 follow-ups did
work this brief never asked for.

config's tickets.project was null and is now set — the field existed for exactly
this and going without it would have made orphan detection worse than useless.
The 21 unticketed requirements are mostly process deliverables (the audit report
itself, commit discipline, the submission artifacts) done without tickets rather
than left undone; W4-R38 (dependency pinning) is the one that looks like a
genuine uncovered gap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The triage was written while Linear was unauthorized and said so. Records what
the live mapping adds: W4-R38 is the only gap with no ticket covering it, and
W4-R10/R33/R35 have tickets that name their own gaps (TRO-304, TRO-305). No
disposition changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 2 was blocked when the first triage ran, so ticket coverage never went
through the scope gate. It has now.

Rulings: none of the 21 unticketed requirements and none of the 9 orphan
tickets imply work. "No ticket" is a defect only when the requirement is
unsatisfied AND untracked — which is true of exactly two (W4-R35, W4-R38), and
both are already "fix now", so a tracker entry would be overhead rather than
coordination. Conditional: ticket them if they are not fixed before submission.

The gate did surface something bigger than either list. The project defines
done as "compare-mode measurement proves the delta AND the full suite still
passes", and at HEAD pnpm test exits 1 while only 3 of 8 categories have a
compare-* artifact. So 121 tickets are marked Done against a bar that does not
currently hold — the same defect class as the IMPROVEMENTS.md claim, and
invisible until the ticket dimension existed. Fixing the red suite repairs the
clause for all 121 at once.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-178)

recordedVersions() compared a Postgres-ORDER-BY'd list against a
JS-.sort()'d expectation. The two collations disagree on
020_document_associations vs 020b_sprint_assignee_ids (Postgres orders
020b first, JS orders 020_ first), so both real-migration-set assertions
failed against a runner that had actually applied all 46 migrations
correctly.

Fix: sort recordedVersions()'s own result with JS .sort() before
returning it, so both sides of every comparison use the same collation.
The comparison itself is unchanged — still a strict, ordered toEqual,
not a count or arrayContaining — preserving the DB-1 regression
guarantee (the runner must apply every migration or fail).

Proof: red on unfixed code (observed AssertionError with the 020/020b
pair transposed), green after the fix (7/7, isolated), then re-proved
red by temporarily filtering 020b_sprint_assignee_ids.sql out of
runPendingMigrations's file list — both assertions failed correctly
(44 vs 45 versions, 020b missing) — before reverting the sabotage back
to a clean diff and confirming green again.
…ed total

docs/IMPROVEMENTS.md §1 recorded W4-R10 (eliminate 25% of the 1535
tracked type-safety violations) as met, via a sum of controlled
per-ticket diffs. Re-running the audit's own instrument at HEAD
(bash ~/.claude/skills/type-safety-audit/scripts/count.sh web api
shared) gives 1987 tracked violations (any 50 + as 1882 + non-null 47
+ ts-ignore 8) against the 1535 baseline: +452 / +29%, where the
requirement needs -25% (~-384). The tracked total has never measured
below baseline at any point this sprint (1535 -> 1778 -> 1987).

This was not a concealed claim — the document's own lines 27-28
already disclosed the sum-of-diffs method and the adjacent table
already showed the total rising. The defect was narrower: the verdict
field said "met" against a requirement whose literal threshold is the
tracked total, not the per-ticket sum. Restated the verdict as not met
against that threshold, kept the genuine per-ticket wins (any 102->50,
req.userId!/req.workspaceId! 236->0, both reconfirmed this session),
and added this session's full recount with its formula and source.

No regression test: a verdict correction to a static-analysis doc has
no observable behavior to assert on.
…able (W4-R42)

./start.sh and scripts/dev.sh stopped at ensureDatabase.ts's actionable
"Postgres unreachable" message, requiring the user to start Postgres
by hand before retrying. W4-R42 requires one command to bring up the
full composed system with no manual setup step beyond installing
dependencies.

scripts/dev.sh (both start.sh and `pnpm dev` funnel through it) now
checks reachability before resolving api/.env.local, and — only when
neither an explicit DATABASE_URL nor an existing api/.env.local pinned
a database, so nothing configured is ever silently redirected — brings
up docker-compose.local.yml's postgres service, waits for it to report
healthy, and repoints DATABASE_URL at it before continuing into the
existing ensureDatabase/migrate/verify/seed flow unchanged. The
compose project name is pinned to `-p ship` rather than left
directory-derived, so every worktree reuses the one shared Postgres
container this repo's factory tooling already runs everything
against, instead of colliding on docker-compose.local.yml's fixed
5433:5432 host port mapping. Falls through unchanged to
ensureDatabase.ts's own message when Docker is unavailable, the
compose file is missing, or the compose command itself fails --
api/src/db/ensureDatabase.ts is untouched.

Extracted the reachability check into api/src/db/postgresReachable.ts
(typed, tested) rather than an inline node -e snippet, matching the
ensureDatabase.ts/migrate.ts pattern already used by this script.

Verified twice, genuinely end-to-end: stopped the real Postgres
container, removed api/.env.local, unset DATABASE_URL, ran the real
./scripts/dev.sh -- it bootstrapped Postgres, created the database,
applied all 46 migrations, seeded, and brought up the API/web/agent
dev servers successfully. Cleaned up afterward (throwaway database
dropped, real api/.env.local and ship_wt_w4_sweepa confirmed intact).
The test exercises isPostgresReachable() with real sockets (genuine
coverage of the probe), but not postgresReachable.ts's CLI wrapper
(main(), the argv/exit-code mapping scripts/dev.sh actually invokes
via `npx tsx ... | $?`). An inverted exit code there would stay green
under this suite. Flagged in response to a direct question about it
rather than left implicit in the original entry.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

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

This PR adds a W4 requirements-audit system with configuration, evidence, generation pipelines, reports, and triage records. It also adds TCP-based Postgres reachability checks, Docker-based local startup, socket tests, and migration-version sorting.

Changes

W4 audit and database workflow

Layer / File(s) Summary
Audit configuration and requirement contracts
audit/requirements.config.yaml, audit/requirements/source-W4.md, audit/requirements/inventory.md, docs/superpowers/*
Defines W4 requirements, audit configuration, inventory formats, verdicts, operating modes, and portable skill design.
Audit evidence and trace data
audit/requirements/pipeline/cluster-*.json, audit/requirements/pipeline/tickets-*.json, audit/requirements/pipeline/verification-results.json
Adds requirement evidence, ticket mappings, ticket records, and verification results.
Audit validation and report generation
audit/requirements/pipeline/*.py, audit/requirements/REPORT.md, audit/requirements/gaps.md, audit/requirements/pipeline/test_acceptance_check.py
Adds acceptance checks, matrix merging, report generation, gap reporting, orphan-ticket reporting, and regression tests.
Audit handoff and correction records
audit/requirements/pm-triage.md, audit/requirements/interpretations.md, docs/IMPROVEMENTS.md, CHANGES.md
Records triage decisions, the W4-R26 interpretation, the corrected 1,987 type-safety count, and migration documentation.
Database reachability and startup flow
api/src/db/postgresReachable.ts, api/src/db/__tests__/*, scripts/dev.sh
Adds timed TCP checks, CLI handling, socket tests, migration sorting, and Docker Postgres bootstrap with health polling and fallback behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant DevScript
  participant ReachabilityCheck
  participant DockerCompose
  participant Postgres
  DevScript->>ReachabilityCheck: Check the default database URL
  ReachabilityCheck->>Postgres: Open a timed TCP socket
  Postgres-->>ReachabilityCheck: Return connection result
  DevScript->>DockerCompose: Start postgres when unreachable
  DockerCompose->>Postgres: Wait for healthy status
  DevScript->>Postgres: Use localhost:5433 when healthy
Loading

Possibly related PRs

  • troysatchell/ship#27: Addresses the related DATABASE_URL and sslmode=disable startup guard concern.
  • troysatchell/ship#155: Directly relates to the requirements-audit artifacts and pipeline outputs updated by this PR.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: fixing the test suite, correcting the type-safety verdict, and enabling one-command local startup.
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 fix/w4-sweep-correctness

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

The prior test covered isPostgresReachable() but not main()'s CLI
wrapper -- the actual contract scripts/dev.sh depends on
(`npx tsx src/db/postgresReachable.ts "$URL"`, branching on `$?`). An
inverted exit-code mapping there would have stayed green under the
existing suite while one-command start silently broke: the "test that
cannot fail when the thing breaks" shape this repo's own audit found
68 times.

Added a second describe block that spawns the real CLI as a
subprocess (execFile, from api/, matching scripts/dev.sh's exact
invocation and cwd) and asserts on the numeric exit code recovered
from the rejection -- 0 when reachable, non-zero when not -- not on
stdout text.

Confirmed red first, on the specific regression this closes:
temporarily inverted process.exit(reachable ? 0 : 1) to
process.exit(reachable ? 1 : 0) in main(). The two new CLI cases
failed (AssertionError: expected 1 to be +0 / expected +0 not to be
+0) while the four isPostgresReachable() cases stayed green --
confirming the failure was isolated to main()'s mapping, not the
underlying probe. Reverted (git diff --stat back to empty) and
re-ran: 6/6 green.

Updates the CHANGES.md caveat from the previous commit to describe
what's now covered, rather than leaving a stale "not covered" note.

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

🤖 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 `@api/src/db/__tests__/postgresReachable.test.ts`:
- Around line 53-60: Update the default-port test for isPostgresReachable to
mock net.createConnection instead of making a real connection to 127.0.0.1.
Configure the mock to exercise the refused-connection result, assert that it was
called with options containing port: 5432, and preserve the expected false
outcome.

In `@audit/requirements/gaps.md`:
- Around line 7-14: Regenerate the W4-R10 handoff to reflect that
docs/IMPROVEMENTS.md already records the tracked-total result as NOT MET. In
audit/requirements/gaps.md:7-14, update the missing-work description and
suggested scope while keeping W4-R10 PARTIAL if the tracked-total gap remains;
in audit/requirements/pm-triage.md:16-56, remove the completed documentation
correction from mandatory disposition and retain only open remediation work.

In `@audit/requirements/pipeline/acceptance-check.py`:
- Around line 2-4: Update the module-level input loading around m, inv, and gaps
to derive the repository root from Path(__file__) rather than the current
working directory, then construct each artifact path relative to that root so
the script works when launched from any directory.
- Around line 5-9: Update the requirement parsing around active, retired, and
missing_rows to inspect each requirement section, recognize both plain and
Markdown-formatted retired statuses, and exclude only IDs explicitly marked
retired from active. Remove the retired-count fallback so the assertion always
fails whenever missing_rows contains any active requirement ID.
- Around line 9-16: Replace every acceptance check assert in the requirements
validation loop—including the missing_rows, VERIFIED, MISSING, and ASSUMED
checks—with explicit failure handling that remains active under python -O. Raise
an exception or aggregate violations and terminate with a nonzero exit status,
while preserving the existing validation messages and successful OK path.

In `@audit/requirements/pipeline/cluster-b.json`:
- Around line 14-47: The audit pipeline inputs still contain pre-fix evidence.
In audit/requirements/pipeline/cluster-b.json lines 14-47, replace the “met” and
1,778-violation references with the corrected “not met” verdict and 1,987
recount, while preserving accurate per-ticket evidence. In lines 371-374, remove
the completed migration-sort work from suggested_scope and update the
verification status accordingly.

In `@audit/requirements/pipeline/cluster-e.json`:
- Around line 5-47: Synchronize the audit outputs with the R35 and R42 fixes:
verify the migration test’s JavaScript sorting and scripts/dev.sh’s local
Postgres startup/wait behavior, then update
audit/requirements/pipeline/cluster-e.json (lines 5-47) to reflect the resolved
requirements. Remove the stale hard-coded red-suite text and count adjustment in
audit/requirements/pipeline/write-report.py (lines 83-96), regenerate
audit/requirements/gaps.md (lines 43-77), and retire the W4-R35/W4-R42 actions
and dispositions in audit/requirements/pm-triage.md (lines 63-94 and 159-172).

In `@audit/requirements/pipeline/verification-results.json`:
- Around line 11-13: Regenerate the audit from corrected implementation-check
evidence: update audit/requirements/pipeline/verification-results.json lines
11-13 with the current W4-R35 passing migration-test result, then regenerate
audit/requirements/REPORT.md lines 11 and 104-107. Also update W4-R42 source
evidence and regenerate audit/requirements/REPORT.md lines 119-122 to reflect
Dockerized Postgres bootstrap behavior, removing superseded claims about the
type-safety target and local startup.

In `@docs/superpowers/specs/2026-08-08-requirements-audit-skill-design.md`:
- Around line 79-83: Update the verification configuration example to move the
application URL from verify.app_url to the active verify_urls.app contract,
while keeping the test and e2e command entries under verify unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ee67b362-be42-42dd-a897-e71d7f44b200

📥 Commits

Reviewing files that changed from the base of the PR and between 32e54ba and 1a00384.

📒 Files selected for processing (30)
  • CHANGES.md
  • api/src/db/__tests__/migrationRunner.test.ts
  • api/src/db/__tests__/postgresReachable.test.ts
  • api/src/db/postgresReachable.ts
  • audit/requirements.config.yaml
  • audit/requirements/REPORT.md
  • audit/requirements/gaps.md
  • audit/requirements/interpretations.md
  • audit/requirements/inventory.md
  • audit/requirements/matrix.baseline.json
  • audit/requirements/pipeline/README.md
  • audit/requirements/pipeline/acceptance-check.py
  • audit/requirements/pipeline/cluster-a.json
  • audit/requirements/pipeline/cluster-b.json
  • audit/requirements/pipeline/cluster-c.json
  • audit/requirements/pipeline/cluster-d.json
  • audit/requirements/pipeline/cluster-e.json
  • audit/requirements/pipeline/cluster-f.json
  • audit/requirements/pipeline/merge-matrix.py
  • audit/requirements/pipeline/tickets-map-1.json
  • audit/requirements/pipeline/tickets-map-2.json
  • audit/requirements/pipeline/tickets-ship.json
  • audit/requirements/pipeline/verification-results.json
  • audit/requirements/pipeline/write-report.py
  • audit/requirements/pm-triage.md
  • audit/requirements/source-W4.md
  • docs/IMPROVEMENTS.md
  • docs/superpowers/plans/2026-08-08-requirements-audit-skill.md
  • docs/superpowers/specs/2026-08-08-requirements-audit-skill-design.md
  • scripts/dev.sh

Comment thread api/src/db/__tests__/postgresReachable.test.ts Outdated
Comment on lines +7 to +14
### W4-R10 — PARTIAL
- **Quote:** "Eliminate 25% of type safety violations. Every fix must preserve existing functionality (all tests still pass). Superficial fixes do not count. Replacing any with unknown without proper type narrowing is not an improvement."
- **Source:** GFA_Week_4_ShipShape_Updated.pdf, p.3
- **Meaning in code:** Post-improvement violation count ≤ 75% of baseline with meaningful types; suite green.
- **Tickets:** TRO-167, TRO-206, TRO-207, TRO-208, TRO-209, TRO-210, TRO-211, TRO-212, TRO-213, TRO-214, TRO-297, TRO-306
- **What is missing:** docs/IMPROVEMENTS.md:27-28 records this category as "Verdict: met, by the sum of controlled per-ticket diffs — not by a live recount, which the tracked metric cannot support today", and the table directly beneath it prints the tracked total as 1535 -> 1778, "Up 243", openly. So the inference behind "met" is marked, not hidden; what fails is narrower and harder to argue away — the requirement's literal threshold ("post-improvement violation count <= 75% of baseline") is defined on the tracked total, and the tracked total has not been below 1535 at any point measured. The alternative accounting the document uses to reach "met" sums controlled per-ticket diffs: TS-1 (156) + TS-3 (19) + TS-4 (236 raw) = 411 >= 384 (IMPROVEMENTS.md:57). The underlying work is real and well-evidenced rather than fabricated — TS-1..TS-10 make narrowly scoped fixes with genuine before/after diffs (api explicit_any 78 -> 36; req.userId! / req.workspaceId! 236 -> 0). Two further records from the repo's own history point the same way: CHANGES.md:9785-9786 is the TS-4 ticket's admission that "a live \"current total vs. 1535\" snapshot cannot cleanly demonstrate the category's cumulative progress - unrelated development moves it in both directions", and CHANGES.md:9780 records that ticket's own live re-run at commit 42e60d9 already finding a tracked total of 1747 — above baseline — before its fix was applied. No compare artifact exists under audit/type-safety/ at all; only baseline.json and baseline.md. Re-measured at HEAD rather than accepted from the repo's own record. `bash ~/.claude/skills/type-safety-audit/scripts/count.sh <web> <api> <shared>` — the exact instrument audit/type-safety/baseline.md:14-18 prescribes — returned web 23 any / 659 as / 5 non-null / 3 ts-ignore, api 27 / 1212 / 42 / 5, shared 0 / 11 / 0 / 0. Summed by baseline.md:77's own formula (any + as + ! + ts-ignore, as-any not double-counted): any 50, as 1882, non-null 47, ts-ignore 8 = 1987 tracked violations, against the 1535 baseline recorded in audit/type-safety/baseline.json (metrics.violationsTotal). That is +452 (+29%) where the requirement asks for -25% (about -384 sites), so the literal threshold is not met at HEAD on 2026-08-08. The nuance the headline number hides: explicit `any` halved (102 -> 50), a real gain in the most meaningful sub-metric, and the rise is driven almost entirely by `as` assertions (1385 -> 1882) — a pattern baseline.md:62 itself documents as over-counting by 15-20% because it catches import/export aliases, comments and `as const`. The direction of the tracked total is not in doubt, but the tracked total is a noisy proxy. The other half of this requirement's acceptance evidence: `pnpm type-check` is green (exit 0, all 4 packages); `pnpm test` exits 1 with 2 failures (see W4-R35).
- **Suggested scope:** Two separable changes, and only the first is small. (a) Restate the verdict at docs/IMPROVEMENTS.md:24-28 as not met against the tracked total, keeping the real per-ticket wins and printing 1987 vs 1535 plainly — minutes of documentation work, and it stops a grader re-running count.sh from finding a success claim that the instrument contradicts. (b) Closing the gap itself is not small: it needs the `as` assertion growth (1385 -> 1882) genuinely reduced, or the baseline re-measured with the corrected pattern audit/type-safety/baseline.md:62 already documents — applied to the baseline as well as to HEAD, with both numbers published.
- **Existing partial evidence:** `audit/type-safety/baseline.json:32`, `docs/IMPROVEMENTS.md:24`, `docs/IMPROVEMENTS.md:35`, `docs/IMPROVEMENTS.md:28`

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 | 🟠 Major | ⚡ Quick win

Regenerate the W4-R10 handoff after the documentation correction.

docs/IMPROVEMENTS.md now states NOT MET at lines 28-36. These handoff records still state that it claims met and direct work to correct that claim. This sends already-completed work back into triage and makes the audit artifacts disagree.

  • audit/requirements/gaps.md#L7-L14: update the missing-work description and suggested scope. Keep W4-R10 PARTIAL if the tracked-total gap remains.
  • audit/requirements/pm-triage.md#L16-L56: remove the completed documentation correction from the mandatory disposition. Retain only open remediation work.
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 7-7: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

📍 Affects 2 files
  • audit/requirements/gaps.md#L7-L14 (this comment)
  • audit/requirements/pm-triage.md#L16-L56
🤖 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 `@audit/requirements/gaps.md` around lines 7 - 14, Regenerate the W4-R10
handoff to reflect that docs/IMPROVEMENTS.md already records the tracked-total
result as NOT MET. In audit/requirements/gaps.md:7-14, update the missing-work
description and suggested scope while keeping W4-R10 PARTIAL if the
tracked-total gap remains; in audit/requirements/pm-triage.md:16-56, remove the
completed documentation correction from mandatory disposition and retain only
open remediation work.

Comment thread audit/requirements/pipeline/acceptance-check.py Outdated
Comment thread audit/requirements/pipeline/acceptance-check.py Outdated
Comment thread audit/requirements/pipeline/acceptance-check.py Outdated
Comment on lines +14 to +47
"file": "docs/IMPROVEMENTS.md",
"line": 24,
"note": "Category 1 target quoted verbatim: eliminate 25% of 1535 tracked violations ~= 384 sites"
},
{
"file": "docs/IMPROVEMENTS.md",
"line": 35,
"note": "repo's own before/after table: tracked total (count.sh, web+api+shared) baseline 1535 -> 1778 on commit 09a6895 (2026-07-31) -- an INCREASE, not the required decrease"
},
{
"file": "docs/IMPROVEMENTS.md",
"line": 28,
"note": "repo claims 'Verdict: met, by the sum of controlled per-ticket diffs -- not by a live recount, which the tracked metric cannot support today'"
},
{
"file": "docs/IMPROVEMENTS.md",
"line": 57,
"note": "alternative accounting: TS-1(156)+TS-3(19)+TS-4(236 raw)=411 >= 384, used to justify 'met' despite the aggregate not moving"
},
{
"file": "CHANGES.md",
"line": 9785,
"note": "TS-4 ticket's own admission, verbatim: \"This means a live \\\"current total vs. 1535\\\" snapshot cannot cleanly demonstrate the category's cumulative progress - unrelated development moves it in both directions\" (sentence runs 9785-9786)"
},
{
"file": "CHANGES.md",
"line": 9780,
"note": "TS-4 ticket's own live re-run at an earlier commit (42e60d9) already found tracked total 1747, higher than the 1535 baseline, before this ticket's fix was even applied"
}
],
"suggested_verification": "pnpm --filter @ship/api exec tsc --noEmit && pnpm --filter @ship/web type-check && pnpm test -- confirms suite-green component only; does not change the numeric-target finding below",
"assumption": null,
"notes": "docs/IMPROVEMENTS.md:27-28 records this category as \"Verdict: met, by the sum of controlled per-ticket diffs \u2014 not by a live recount, which the tracked metric cannot support today\", and the table directly beneath it prints the tracked total as 1535 -> 1778, \"Up 243\", openly. So the inference behind \"met\" is marked, not hidden; what fails is narrower and harder to argue away \u2014 the requirement's literal threshold (\"post-improvement violation count <= 75% of baseline\") is defined on the tracked total, and the tracked total has not been below 1535 at any point measured. The alternative accounting the document uses to reach \"met\" sums controlled per-ticket diffs: TS-1 (156) + TS-3 (19) + TS-4 (236 raw) = 411 >= 384 (IMPROVEMENTS.md:57). The underlying work is real and well-evidenced rather than fabricated \u2014 TS-1..TS-10 make narrowly scoped fixes with genuine before/after diffs (api explicit_any 78 -> 36; req.userId! / req.workspaceId! 236 -> 0). Two further records from the repo's own history point the same way: CHANGES.md:9785-9786 is the TS-4 ticket's admission that \"a live \\\"current total vs. 1535\\\" snapshot cannot cleanly demonstrate the category's cumulative progress - unrelated development moves it in both directions\", and CHANGES.md:9780 records that ticket's own live re-run at commit 42e60d9 already finding a tracked total of 1747 \u2014 above baseline \u2014 before its fix was applied. No compare artifact exists under audit/type-safety/ at all; only baseline.json and baseline.md.",
"suggested_scope": "Two separable changes, and only the first is small. (a) Restate the verdict at docs/IMPROVEMENTS.md:24-28 as not met against the tracked total, keeping the real per-ticket wins and printing 1987 vs 1535 plainly \u2014 minutes of documentation work, and it stops a grader re-running count.sh from finding a success claim that the instrument contradicts. (b) Closing the gap itself is not small: it needs the `as` assertion growth (1385 -> 1882) genuinely reduced, or the baseline re-measured with the corrected pattern audit/type-safety/baseline.md:62 already documents \u2014 applied to the baseline as well as to HEAD, with both numbers published."

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 | 🟠 Major | ⚡ Quick win

Regenerate the audit pipeline inputs after the W4-SWEEPA corrections.

The pipeline still contains pre-fix evidence. This can produce a report and PM handoff that contradict the corrected documentation and completed migration fix.

  • audit/requirements/pipeline/cluster-b.json#L14-L47: replace the old “met” and 1,778-violation citations with the corrected “not met” verdict and 1,987 recount.
  • audit/requirements/pipeline/cluster-b.json#L371-L374: remove the completed migration-sort change from suggested_scope and update the verification status.
📍 Affects 1 file
  • audit/requirements/pipeline/cluster-b.json#L14-L47 (this comment)
  • audit/requirements/pipeline/cluster-b.json#L371-L374
🤖 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 `@audit/requirements/pipeline/cluster-b.json` around lines 14 - 47, The audit
pipeline inputs still contain pre-fix evidence. In
audit/requirements/pipeline/cluster-b.json lines 14-47, replace the “met” and
1,778-violation references with the corrected “not met” verdict and 1,987
recount, while preserving accurate per-ticket evidence. In lines 371-374, remove
the completed migration-sort work from suggested_scope and update the
verification status accordingly.

Comment on lines +5 to +47
"id": "W4-R42",
"verdict": "PARTIAL",
"evidence": [
{
"file": "start.sh",
"line": 68,
"note": "start.sh execs scripts/dev.sh unchanged after a Node/pnpm preflight; it is the documented single entry point"
},
{
"file": "scripts/dev.sh",
"line": 78,
"note": "dev.sh runs ensureDatabase.ts, migrate.ts, verifyMigrations.ts, seed.ts on every invocation (idempotent, self-healing)"
},
{
"file": "api/src/db/ensureDatabase.ts",
"line": 53,
"note": "unreachableMessage() builds the exact text 'ERROR: Cannot reach PostgreSQL at HOST:PORT. Start it, then re-run ./start.sh...' — thrown from ensureDatabase() at line 93 when the initial client.connect() gets ECONNREFUSED/ENOTFOUND/ETIMEDOUT"
},
{
"file": "api/src/db/ensureDatabase.ts",
"line": 93,
"note": "connection-error branch converts a raw pg connect failure into the actionable unreachableMessage and throws — set -e in dev.sh then stops the script before servers start"
},
{
"file": "README.md",
"line": 37,
"note": "README has a 'Cold start (one command)' section documenting ./start.sh from a clean checkout"
},
{
"file": "README.md",
"line": 55,
"note": "README explicitly says: 'No native Postgres installed? Bring up one of the two bundled Docker options first, then point start.sh at it with DATABASE_URL' — a manual pre-step, not part of the one command"
},
{
"file": "README.md",
"line": 69,
"note": "'If Postgres isn't reachable at all, ./start.sh fails immediately and says exactly what to start' — the README itself documents that the single command does not start the database server"
}
],
"suggested_verification": "On a machine with no Postgres server running anywhere (no native service, no docker-compose container), run `./start.sh` from a clean checkout and observe whether it succeeds unaided or requires a manual `docker compose ... up`/`brew services start postgresql` first.",
"assumption": null,
"notes": "PARTIAL. Everything downstream of an already-reachable Postgres server is genuinely one-command and self-healing: dependency install, shared build, database creation, migration, migration verification, seeding, dynamic port selection, and starting both servers — all idempotent, all in scripts/dev.sh, all documented in README's 'Cold start' section. But the script only creates/migrates/seeds a database ON a Postgres SERVER that is already running; it does not start the Postgres server process itself. From a truly clean checkout on a machine with no Postgres running (native service stopped, no Docker container up), `./start.sh` fails immediately with the exact message observed today ('ERROR: Cannot reach PostgreSQL at localhost:5433. Start it, then re-run ./start.sh' — produced by api/src/db/ensureDatabase.ts:53-61's unreachableMessage(), thrown at line 93) and requires a manual step (start a local Postgres service, or `docker compose -f docker-compose.local.yml up -d postgres`) before re-running — which is manual setup beyond 'installing dependencies' under a strict reading of the quote. The README is honest about this (README.md:55-70) rather than hiding it, so the documentation half of R42 is fully met; the 'single command boots app + database... without manual setup beyond installing dependencies' half is not, for the database-server-startup step specifically. No mock external services were found to be needed for local dev (the only external call, AWS SSM, is gated behind NODE_ENV=production in api/src/index.ts:15-17), so that clause of the quote is not in play.",
"suggested_scope": "About 20 lines in start.sh: when Postgres is unreachable and docker-compose.local.yml is present, offer to start that container and wait for it to become healthy, keeping the current message from api/src/db/ensureDatabase.ts:53-61 as the fallback. No application code, and the README already documents the limitation honestly, so only the script changes."

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
set -euo pipefail

fd -a 'migrationRunner.test.ts|postgresReachable.ts|dev.sh|start.sh' .

rg -n -C 4 \
  'postgresReachable|docker compose|docker-compose|Cannot reach PostgreSQL|wait' \
  start.sh scripts/dev.sh api/src/db 2>/dev/null || true

rg -n -C 4 \
  'schema_migrations|ORDER BY version|expected.*sort|sort\\(' \
  api/src/db/__tests__/migrationRunner.test.ts 2>/dev/null || true

rg -n -C 3 \
  'W4-R35|W4-R42|suite is red|one-command start' \
  audit/requirements docs/IMPROVEMENTS.md

Repository: troysatchell/ship

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- migration test ---'
sed -n '90,205p' api/src/db/__tests__/migrationRunner.test.ts

printf '%s\n' '--- startup implementation ---'
sed -n '1,155p' scripts/dev.sh
sed -n '1,110p' api/src/db/postgresReachable.ts

printf '%s\n' '--- audit generator and records ---'
sed -n '75,105p' audit/requirements/pipeline/write-report.py
sed -n '1,65p' audit/requirements/pipeline/cluster-e.json
sed -n '35,85p' audit/requirements/gaps.md
sed -n '55,100p' audit/requirements/pm-triage.md
sed -n '155,175p' audit/requirements/pm-triage.md

printf '%s\n' '--- relevant worktree changes ---'
git status --short
git diff --stat -- audit/requirements api/src/db scripts

Repository: troysatchell/ship

Length of output: 38128


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import json

def text(path):
    return Path(path).read_text()

dev = text("scripts/dev.sh")
migration = text("api/src/db/__tests__/migrationRunner.test.ts")
report = text("audit/requirements/pipeline/write-report.py")
cluster = json.loads(text("audit/requirements/pipeline/cluster-e.json"))
gaps = text("audit/requirements/gaps.md")
triage = text("audit/requirements/pm-triage.md")

checks = {
    "startup probes reachability": "postgresReachable.ts" in dev,
    "startup starts compose postgres": "docker compose -p ship -f docker-compose.local.yml up -d postgres" in dev,
    "startup waits for health": "HEALTH" in dev and 'healthy' in dev,
    "startup redirects to compose port": "localhost:5433" in dev,
    "migration result uses JS sort": "return res.rows.map(r => r.version).sort();" in migration,
    "report still hard-codes red suite": "The test suite is red at HEAD" in report,
    "cluster still marks R42 partial": next(r["verdict"] for r in cluster["requirements"] if r["id"] == "W4-R42") == "PARTIAL",
    "gaps still contain stale R35": "### W4-R35 — PARTIAL" in gaps,
    "gaps still contain stale R42": "### W4-R42 — PARTIAL" in gaps,
    "triage still lists R35 fix now": "| W4-R35 | **Fix now** |" in triage,
    "triage still lists R42 fix now": "| W4-R42 | **Fix now** |" in triage,
}

for name, passed in checks.items():
    print(f"{'PASS' if passed else 'FAIL'}: {name}")

if not all(checks.values()):
    raise SystemExit(1)
PY

Repository: troysatchell/ship

Length of output: 556


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import json

def text(path):
    return Path(path).read_text()

dev = text("scripts/dev.sh")
migration = text("api/src/db/__tests__/migrationRunner.test.ts")
report = text("audit/requirements/pipeline/write-report.py")
cluster = json.loads(text("audit/requirements/pipeline/cluster-e.json"))
gaps = text("audit/requirements/gaps.md")
triage = text("audit/requirements/pm-triage.md")

checks = {
    "startup probes reachability": "postgresReachable.ts" in dev,
    "startup starts compose postgres": "docker compose -p ship -f docker-compose.local.yml up -d postgres" in dev,
    "startup waits for health": "HEALTH" in dev and "healthy" in dev,
    "startup redirects to compose port": "localhost:5433" in dev,
    "migration result uses JS sort": "return res.rows.map(r => r.version).sort();" in migration,
    "report still hard-codes red suite": "The test suite is red at HEAD" in report,
    "cluster still marks R42 partial": next(r["verdict"] for r in cluster["requirements"] if r["id"] == "W4-R42") == "PARTIAL",
    "gaps still contain stale R35": "### W4-R35 — PARTIAL" in gaps,
    "gaps still contain stale R42": "### W4-R42 — PARTIAL" in gaps,
    "triage still lists R35 fix now": "| W4-R35 | **Fix now** |" in triage,
    "triage still lists R42 fix now": "| W4-R42 | **Fix now** |" in triage,
}

for name, passed in checks.items():
    print(f"{'PASS' if passed else 'FAIL'}: {name}")

if not all(checks.values()):
    raise SystemExit(1)
PY

Repository: troysatchell/ship

Length of output: 556


Synchronize the audit with the R35 and R42 fixes.

The migration test now applies JavaScript sorting, and scripts/dev.sh now starts and waits for the local Postgres container. Run the relevant checks, then update cluster-e.json, remove the stale hard-coded red-suite text and count adjustment in write-report.py, regenerate gaps.md, and retire the W4-R35/W4-R42 actions and dispositions in pm-triage.md.

📍 Affects 4 files
  • audit/requirements/pipeline/cluster-e.json#L5-L47 (this comment)
  • audit/requirements/pipeline/write-report.py#L83-L96
  • audit/requirements/gaps.md#L43-L77
  • audit/requirements/pm-triage.md#L63-L94
  • audit/requirements/pm-triage.md#L159-L172
🤖 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 `@audit/requirements/pipeline/cluster-e.json` around lines 5 - 47, Synchronize
the audit outputs with the R35 and R42 fixes: verify the migration test’s
JavaScript sorting and scripts/dev.sh’s local Postgres startup/wait behavior,
then update audit/requirements/pipeline/cluster-e.json (lines 5-47) to reflect
the resolved requirements. Remove the stale hard-coded red-suite text and count
adjustment in audit/requirements/pipeline/write-report.py (lines 83-96),
regenerate audit/requirements/gaps.md (lines 43-77), and retire the
W4-R35/W4-R42 actions and dispositions in audit/requirements/pm-triage.md (lines
63-94 and 159-172).

Comment on lines +11 to +13
"W4-R35": {
"verdict": "PARTIAL",
"notes": "Suite is NOT green at HEAD: `pnpm test` exits 1 with 830/832 tests passing and 2 failures, both in api/src/db/__tests__/migrationRunner.test.ts (lines 167 and 184). The cause was confirmed by direct observation rather than inferred: the test compares a Postgres-ordered result (`SELECT version FROM schema_migrations ORDER BY version`, line 115) against a JavaScript `[...expected].sort()`, and the two orderings genuinely disagree on the pair 020_document_associations / 020b_sprint_assignee_ids — Postgres returns 020b first, JavaScript `.sort()` returns 020_ first. An environment caveat that must not be dropped: an earlier run of the same command failed all 77 files with \"DATABASE_URL must be set\" because the shell lacked the env var; the 830/832 figure comes from the re-run with api/.env.local loaded. `pnpm type-check` is separately green (exit 0, all 4 packages). E2E (`pnpm test:e2e`) was NOT run this sweep — 600+ Playwright tests requiring the /e2e-test-runner protocol — so no claim is made about it either way."

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 | 🟠 Major | ⚡ Quick win

Regenerate the audit after the corrected implementation checks.

The committed audit still describes superseded behavior. Line 13 records the migration sort failure as current, and merge-matrix.py applies this override to generated output. The PR objective states that the sort fix now passes. The report also still says docs/IMPROVEMENTS.md declares the type-safety target met and that local startup cannot boot Dockerized Postgres.

  • audit/requirements/pipeline/verification-results.json#L11-L13: replace the stale W4-R35 result with fresh test evidence.
  • audit/requirements/REPORT.md#L11-L11: regenerate the summary from corrected source evidence.
  • audit/requirements/REPORT.md#L104-L107: regenerate W4-R35 after updating its source verdict and evidence.
  • audit/requirements/REPORT.md#L119-L122: update the W4-R42 source evidence and regenerate the report to reflect Docker Postgres bootstrap behavior.
📍 Affects 2 files
  • audit/requirements/pipeline/verification-results.json#L11-L13 (this comment)
  • audit/requirements/REPORT.md#L11-L11
  • audit/requirements/REPORT.md#L104-L107
  • audit/requirements/REPORT.md#L119-L122
🤖 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 `@audit/requirements/pipeline/verification-results.json` around lines 11 - 13,
Regenerate the audit from corrected implementation-check evidence: update
audit/requirements/pipeline/verification-results.json lines 11-13 with the
current W4-R35 passing migration-test result, then regenerate
audit/requirements/REPORT.md lines 11 and 104-107. Also update W4-R42 source
evidence and regenerate audit/requirements/REPORT.md lines 119-122 to reflect
Dockerized Postgres bootstrap behavior, removing superseded claims about the
type-safety target and local startup.

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

🤖 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 `@api/src/db/__tests__/postgresReachable.test.ts`:
- Around line 50-55: Update the catch block in the postgres reachability test to
verify error is non-null and an object before reading its code property, then
return only when code is numeric; otherwise rethrow the original error
unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 67e5b75f-50aa-49ad-96f8-df3ba69ebb66

📥 Commits

Reviewing files that changed from the base of the PR and between 1a00384 and a61b483.

📒 Files selected for processing (2)
  • CHANGES.md
  • api/src/db/__tests__/postgresReachable.test.ts

Comment thread api/src/db/__tests__/postgresReachable.test.ts
…e host

CI (PR #154) failed with exactly one new case:
postgresReachable.test.ts :: isPostgresReachable "defaults to port
5432 when the URL specifies none". It asserted
isPostgresReachable('postgresql://127.0.0.1/whatever') resolved
false, inferring "the default is 5432" from "nothing answered on
5432" -- true where this was written (local Postgres is on 5433),
false on GitHub Actions, which runs its own Postgres service on 5432.
The probe correctly returned true; the test failed because the
assertion depended on the environment, not the code. Same defect
class as item 1 (migrationRunner.test.ts's collation assumption) --
and the local gate could never have caught it, since it passed
specifically because it ran somewhere the assumption held.

Extracted the URL parsing isPostgresReachable did inline into a pure
resolveHostPort(url): { host, port } | null (same default-port rule,
no socket). Replaced the socket-probe "defaults to 5432" case with a
new `describe('resolveHostPort', ...)` block that asserts directly on
the parsed value -- deterministic on every machine, since it never
opens a socket.

Audited the rest of the file for the same shape: every other
socket-touching case is either self-consistent (an ephemeral
OS-assigned server + connecting to that exact port) or uses port 1,
a guaranteed-closed well-known port and the same pattern
ensureDatabase.test.ts already uses -- no other instance found.

Reproduced the actual CI condition to confirm the fix, not just a
plausible one: started a real postgres:16 container on host port
5432, confirmed the pre-fix isPostgresReachable logic returns true
against it (reproducing the exact failure), then ran the new test
file in the same environment -- 8/8 green, since nothing in it probes
5432 anymore. Also re-proved ordinary red-then-green on the new
resolveHostPort assertions (temporarily defaulted to 5433, saw the
one expected failure, reverted, saw 8/8 again). Stopped the test
container afterward.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@api/src/db/postgresReachable.ts`:
- Around line 51-54: Update resolveHostPort so an explicitly provided port of
"0" is preserved, using the default 5432 only when url.port is empty. Add a
regression test covering postgresql://127.0.0.1:0/whatever and assert that the
resolved port is 0.

In `@CHANGES.md`:
- Around line 58-61: Update the documented “Guaranteed-closed” claim in
CHANGES.md to remove the assertion that 127.0.0.1:1 is guaranteed unreachable,
and describe the tests as using a deterministic connection-failure seam instead
of an unused local port.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dcd0d1e1-0c41-4ea8-9b8d-4a03f006831b

📥 Commits

Reviewing files that changed from the base of the PR and between a61b483 and 10be462.

📒 Files selected for processing (3)
  • CHANGES.md
  • api/src/db/__tests__/postgresReachable.test.ts
  • api/src/db/postgresReachable.ts

Comment thread api/src/db/postgresReachable.ts Outdated
Comment thread CHANGES.md Outdated
Comment on lines +58 to +61
- **Guaranteed-closed, not "probably closed":** the "resolves false"/"exits non-zero" cases both use
`127.0.0.1:1` — port 1 is a reserved well-known port nothing binds to as a listener; this is the
pre-existing pattern `ensureDatabase.test.ts` already uses for its identical "unreachable" case,
not something new introduced here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Remove the guaranteed-closed claim for port 1.

127.0.0.1:1 is not guaranteed to refuse connections. A privileged local process can listen on port 1, so the test can still depend on host state. Use a deterministic connection-failure seam instead of relying on an unused port.

🤖 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 `@CHANGES.md` around lines 58 - 61, Update the documented “Guaranteed-closed”
claim in CHANGES.md to remove the assertion that 127.0.0.1:1 is guaranteed
unreachable, and describe the tests as using a deterministic connection-failure
seam instead of an unused local port.

troysatchell and others added 3 commits August 8, 2026 15:48
… by identity, run from anywhere

CodeRabbit findings on audit/requirements/pipeline/acceptance-check.py:

- Line 9 (Major): `active` was built from every `## W4-R` heading, retired
  ones included, and the retired *count* came from a literal substring match
  (`inv.count("Status: retired")`) that never matches this repo's actual
  Markdown-bolded status line (`- **Status:** retired`) — so it was always 0.
  The fallback then accepted any missing rows whenever their count happened
  to equal the retired count, which is a match on quantity, not identity: N
  requirements missing and N requirements retired does not mean they're the
  same N. Rewrote to parse each requirement's own section (mirroring
  merge-matrix.py's already-correct `active_ids()`), exclude only IDs whose
  own section states retired, and fail on every missing active ID with the
  IDs named.
- Line 16 (Major): every check was a bare `assert`, which `python -O`
  compiles out entirely — a gate whose checks vanish under an interpreter
  flag is not a gate. Replaced with explicit conditionals that call a
  `fail()` helper (`sys.exit(1)` after printing to stderr).
- Line 4 (Major) + Ruff SIM115: paths were relative, requiring cwd == repo
  root, contradicting pipeline/README.md ("run from anywhere") and the
  sibling scripts, which already derive the repo root from `__file__`.
  Switched to `Path(__file__).resolve().parents[3]` (same derivation
  merge-matrix.py uses) and `with open(...) as f:` context managers.
  Also fixes Ruff E741 (ambiguous variable name `l`).

Verified against copies in a scratch dir (real audit artifacts untouched):
constructed a case with one requirement retired (plain-format status, which
the old script's naive check *does* recognize) and a different, genuinely
active requirement dropped from the matrix — count-equal (1 retired, 1
missing) but wrong identity. Old script: `OK — 53 rows, verdicts sound`,
exit 0 — silently wrong. New script:
`FAIL: 1 active requirement(s) missing from the matrix: W4-R1`, exit 1.
Also confirmed under `python -O` (still fails — no assert to compile out)
and from `/tmp` as cwd (still finds its inputs; the old script raises
FileNotFoundError there). Re-run against the actual, unmodified
audit/requirements/ tree: `OK — 54 rows, verdicts sound`, exit 0, from both
the repo root and an unrelated cwd.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…error before reading .code

Two CodeRabbit findings I judged (bodies were truncated in the fetch, read
in full via the GitHub API):

- postgresReachable.ts:54 (Minor, correctness): `resolveHostPort` computed
  `Number(url.port) || 5432`. `Number('0')` is `0`, which is falsy, so an
  explicit `postgresql://host:0/db` silently resolved to port 5432 instead
  of the port the URL actually specified. Real bug — confirmed `url.port`
  is genuinely `'0'` (not `''`) for that input. Changed to
  `url.port === '' ? 5432 : Number(url.port)`, which only defaults on a
  truly absent port, and added a regression test asserting
  `resolveHostPort('postgresql://127.0.0.1:0/whatever')` resolves to
  `{ host: '127.0.0.1', port: 0 }`.
- postgresReachable.test.ts:63 (Minor, maintainability): `runCli`'s catch
  block read `(error as { code?: unknown }).code` — an unchecked assertion
  on a catch variable typed `unknown`. If `error` were ever null or a
  primitive, that read throws its own TypeError and masks the original
  failure instead of rethrowing it. Low-probability for `execFile`'s actual
  rejection shape, but the check is genuinely free: added
  `isErrorWithNumericCode`, a real type guard (`typeof error === 'object' &&
  error !== null && 'code' in error && typeof ...code === 'number'`) in
  place of the cast, matching this repo's stated preference for typing
  `unknown` boundaries explicitly instead of asserting through them.

`npx vitest run postgresReachable.test.ts`: 9/9 passing (was 8, +1 for the
new port-0 case). `pnpm --filter @ship/api type-check`: clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ments snapshot as frozen

Three more CodeRabbit findings from PR #154, none touching application code:

- CHANGES.md:61: "port 1 is a reserved well-known port nothing binds to as a
  listener" was stated as a guarantee. It isn't — a privileged (root or
  admin-equivalent) process can legally listen on port 1, so the test's
  "unreachable" cases still depend on host state, just far less likely to
  matter than the port-5432 case this same entry documents fixing. Reworded
  to "conventionally unused, not guaranteed-closed" and named the residual
  risk explicitly, per this repo's own rule against stating an environment
  assumption as a guarantee.
- docs/superpowers/specs/2026-08-08-requirements-audit-skill-design.md:83:
  the config schema example nested the app URL as `verify.app_url`. Both the
  real `audit/requirements.config.yaml` and the implementation plan
  (`docs/superpowers/plans/2026-08-08-requirements-audit-skill.md:32`) use a
  separate top-level `verify_urls.app`. A skill built from this spec would
  look up a field that doesn't exist. Split the example into `verify:` +
  `verify_urls:` to match.
- REPORT.md / gaps.md / pm-triage.md (the stale-audit-artifact family,
  4 findings total): CodeRabbit is right that these now contradict
  docs/IMPROVEMENTS.md's corrected type-safety verdict and the fixed
  migration test — both landed on this branch (8386f26, 5126a03) after
  these artifacts were last generated at commit ef87839, along with the
  W4-R42 Postgres-bootstrap fix (5030ad0). NOT regenerating: these three
  files plus matrix.baseline.json are the frozen "before" state W4-R34's
  before/after proof depends on, and the cluster-*.json / verification-
  results.json trace evidence (also flagged, left untouched) is recorded
  evidence of what a sweep agent actually saw — rewriting it to agree with
  a later world defeats the purpose of keeping it. Instead added a short
  header to each of the three human-facing docs stating plainly: this
  describes commit ef87839, is a frozen baseline, W4-R35/W4-R10(a)/W4-R42
  fixes have since landed, and a compare-mode run — not an edit to this
  file — is what will show the delta.

`node scripts/factory/merge-changes.mjs --check CHANGES.md`: OK, 144
entries, 230 fences (pre-existing warnings only, unrelated to this diff).

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

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

Caution

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

⚠️ Outside diff range comments (2)
CHANGES.md (1)

42-45: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update the CHANGES.md port rule. resolveHostPort defaults to 5432 only when no port is specified. An explicit :0 resolves to port 0. Replace Number(url.port) || 5432 and qualify “behavior unchanged” to include the port-0 fix.

🤖 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 `@CHANGES.md` around lines 42 - 45, Update the CHANGES.md entry for
resolveHostPort to document that port 5432 is used only when no port is
specified, while an explicit :0 resolves to port 0; replace the Number(url.port)
|| 5432 wording and qualify “behavior unchanged” to note this port-0 correction.
audit/requirements/gaps.md (1)

61-68: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use one dependency-count definition for W4-R38.

audit/requirements/gaps.md correctly reports 142 caret ranges, 9 exact pins, and 2 workspace:* references. Update audit/requirements/pm-triage.md, which incorrectly describes all 144 non-exact entries as caret ranges. Treat the 2 internal workspace references separately and limit pinning changes to the 142 caret ranges unless repository policy requires otherwise.

🤖 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 `@audit/requirements/gaps.md` around lines 61 - 68, The dependency count for
W4-R38 must distinguish 142 caret ranges from 2 internal workspace:* references:
update audit/requirements/pm-triage.md lines 105-116 to describe the two
categories separately and limit pinning work to the 142 caret entries unless
repository policy requires otherwise; audit/requirements/gaps.md lines 61-68 is
the anchor and requires no direct change because it already reports the correct
breakdown.
🤖 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 `@api/src/db/postgresReachable.ts`:
- Line 59: Update the host construction in the PostgreSQL URL parsing logic to
remove outer brackets from IPv6 literals before returning the host, while
preserving ordinary hostnames and the localhost fallback. Add a regression test
covering postgresql://[::1]:5432/whatever and assert that the returned host is
::1.

In `@audit/requirements/pipeline/acceptance-check.py`:
- Around line 90-92: Update the gap-loading and validation logic around the
MISSING verdict check to parse Markdown gap headings into a set of exact
requirement IDs, then test membership against that set instead of
substring-searching the full file. Ensure W4-R1 does not match W4-R10 or
unrelated prose, and add a regression case covering both prefix IDs.
- Around line 86-95: Update the matrix validation loop in the acceptance-check
logic to reject verdicts outside VERIFIED, MISSING, and ASSUMED before returning
OK. For VERIFIED result_excerpt and ASSUMED assumption, require trimmed,
non-empty string values rather than merely truthy values, while preserving the
existing gaps check for MISSING. Add regression cases covering an unknown
verdict and whitespace-only evidence or assumption.

---

Outside diff comments:
In `@audit/requirements/gaps.md`:
- Around line 61-68: The dependency count for W4-R38 must distinguish 142 caret
ranges from 2 internal workspace:* references: update
audit/requirements/pm-triage.md lines 105-116 to describe the two categories
separately and limit pinning work to the 142 caret entries unless repository
policy requires otherwise; audit/requirements/gaps.md lines 61-68 is the anchor
and requires no direct change because it already reports the correct breakdown.

In `@CHANGES.md`:
- Around line 42-45: Update the CHANGES.md entry for resolveHostPort to document
that port 5432 is used only when no port is specified, while an explicit :0
resolves to port 0; replace the Number(url.port) || 5432 wording and qualify
“behavior unchanged” to note this port-0 correction.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dba02a91-e684-4170-aed2-865f194912b3

📥 Commits

Reviewing files that changed from the base of the PR and between 10be462 and eb7c687.

📒 Files selected for processing (8)
  • CHANGES.md
  • api/src/db/__tests__/postgresReachable.test.ts
  • api/src/db/postgresReachable.ts
  • audit/requirements/REPORT.md
  • audit/requirements/gaps.md
  • audit/requirements/pipeline/acceptance-check.py
  • audit/requirements/pm-triage.md
  • docs/superpowers/specs/2026-08-08-requirements-audit-skill-design.md

Comment thread api/src/db/postgresReachable.ts Outdated
Comment thread audit/requirements/pipeline/acceptance-check.py Outdated
Comment thread audit/requirements/pipeline/acceptance-check.py Outdated
…ost to net.createConnection

CodeRabbit re-review, postgresReachable.ts:59 (Minor, functional correctness).
I fetched and read the full analysis chain, then reproduced the failure
directly rather than reasoning about it:

  node -e "console.log(new URL('postgresql://[::1]:5432/x').hostname)"
  // -> '[::1]'  (brackets included)

  net.isIP('[::1]')  // -> 0  (not recognized as an IP at all)
  net.isIP('::1')    // -> 6  (correctly IPv6)

  dns.lookup('[::1]', ...)  // ENOTFOUND
  dns.lookup('::1', ...)    // resolves

Then reproduced it end-to-end against a real listener: a `net.Server` bound
to `::1`, connected to with `{ host: '[::1]', port }` (what `resolveHostPort`
returned before this fix) fails with `ENOTFOUND`; the identical connection
with `{ host: '::1', port }` succeeds. So every IPv6 `DATABASE_URL` was
previously read as unreachable regardless of whether Postgres was actually
listening — a real bug, not a hypothetical one.

**What changed.** `resolveHostPort` now strips a leading `[` and trailing
`]` from `url.hostname` before returning it, only when both are present
(ordinary hostnames and the empty-hostname → 'localhost' fallback are
unaffected — neither starts with `[`).

**Tests added**, matching the file's stated no-mocked-sockets philosophy:
- `resolveHostPort('postgresql://[::1]:5432/whatever')` → `{ host: '::1', port: 5432 }`
- `resolveHostPort('postgresql://[2001:db8::1]:5432/whatever')` → unbracketed host
- End-to-end: `isPostgresReachable` resolves `true` against a real listener
  bound to `::1`, addressed via the bracketed URL form Postgres connection
  strings actually use — this is the case that was silently broken before
  the fix (always resolved `false`).

`npx vitest run postgresReachable.test.ts`: 12/12 passing (was 9; +3 new
IPv6 cases, including the loopback-listener bind, which this sandbox
supports). `pnpm --filter @ship/api type-check`: clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…h, unknown-verdict passthrough

Second round of CodeRabbit review on the same gate this branch already
hardened once (575c2fa). Two real findings, same false-pass class:

1. **Line 92 (Major): MISSING rows matched by substring.** `r["id"] not in
   gaps` searched gaps.md's raw text, so `"W4-R1" in gaps_text` was
   satisfied by a `### W4-R10` heading (or any prose mention) — a
   genuinely MISSING W4-R1 with no write-up of its own would pass.
   Added `parse_gap_ids()`, anchored to `^### (W4-R\d+)(?:\s|$)`
   (MULTILINE), and check membership against that set instead.

   Demonstrated in a scratch copy (real audit/requirements/ untouched,
   confirmed via `git status`): W4-R1 set to MISSING in the matrix, gaps.md
   left with only its pre-existing `### W4-R10` heading and no `### W4-R1`
   heading.
   - Old script (575c2fa): `OK — 54 rows, verdicts sound`, exit 0.
   - New script: `FAIL: W4-R1: MISSING but absent from gaps.md`, exit 1.

2. **Line 95 (Major): unrecognized verdicts skip every branch.** The loop
   only branched on `VERIFIED`/`MISSING`/`ASSUMED`; a typo (`VERIFED`) or
   any out-of-vocabulary value matched none of them and fell straight
   through to `OK`. Added `VALID_VERDICTS` — the exact 7-value table from
   `report-format.md` — and fail immediately on anything outside it, naming
   the row and the bad value. Also tightened the truthiness checks:
   `result_excerpt`/`assumption` accepted a whitespace-only string as
   "evidence" under plain `if value:`; added `has_content()`
   (`isinstance(str) and value.strip() != ""`).

   Demonstrated the same way: W4-R13's verdict corrupted to `'VERIFED'`
   with whitespace-only evidence.
   - Old script: `OK — 54 rows, verdicts sound`, exit 0.
   - New script: `FAIL: W4-R13: unrecognized verdict 'VERIFED'`, exit 1.
   Isolated the whitespace-truthiness half separately (verdict left as the
   valid `VERIFIED`, evidence still whitespace-only): old script still
   prints `OK` (exit 0); new script: `FAIL: W4-R13: VERIFIED without
   evidence` (exit 1).

**A fourth issue, found on the requested full re-read, not from CodeRabbit:**
report-format.md's field rules state `suggested_scope` is "non-null for
every MISSING and PARTIAL row ... an unset value silently drops a required
field from the PM handoff" — the acceptance gate never checked this at all.
Added the same class of check: any MISSING/PARTIAL row with a blank
`suggested_scope` now fails, naming the row. Verified the real matrix's 10
PARTIAL rows all already carry non-empty `suggested_scope` (this addition
changes nothing about the current `OK`).

Considered and deliberately NOT added: verifying `verification`/`assumption`
are null when the verdict doesn't require them, and that BLOCKED rows carry
a non-blank `notes`. Both are already enforced at write time in
`merge-matrix.py` (`if verdict != 'VERIFIED': verification = None`, etc.),
so a reader-side check would be pure defense-in-depth rather than closing a
live false-pass path, and report-format.md doesn't use the same "silently
drops a required field" language for them that motivated the
suggested_scope check. No BLOCKED rows exist in the current 54-row matrix
either way.

Refactored the checks into a pure `find_first_violation(matrix, active_ids,
gap_ids)` (no file I/O) so they're testable directly. Added
`test_acceptance_check.py` (stdlib `unittest`, loaded via `importlib` since
the module under test has a hyphenated filename): 14 cases, including the
exact W4-R1/W4-R10 prefix collision and the corrupted-verdict/whitespace-
evidence cases above. `python3 audit/requirements/pipeline/
test_acceptance_check.py`: 14/14 passing. Re-ran the real, unmodified
pipeline: `OK — 54 rows, verdicts sound`, exit 0, from both the repo root
and an unrelated cwd. Updated pipeline/README.md's file table (the
"verbatim" claim about the plan doc's snippet is now stale — this script
has diverged from it on purpose) and added the new test file's row.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@api/src/db/__tests__/postgresReachable.test.ts`:
- Around line 127-137: Replace the IPv6-dependent server setup in the postgres
reachability test with a mock of net.createConnection that emits connect, and
assert the connection options specify host: '::1'. Remove the server.listen/port
allocation path while preserving the existing real IPv4 reachability test as the
TCP integration coverage.

In `@audit/requirements/pipeline/acceptance-check.py`:
- Around line 127-134: Update the requirement validation around
matrix["requirements"] and matrix_ids to detect duplicate IDs before converting
or relying on the set, and reject any IDs not present in active_ids. Return a
validation error for either condition before the existing missing_active check,
and add regression tests covering duplicate and unknown requirement rows.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c0efa7cc-0fc4-4d52-acff-f590ff034fb6

📥 Commits

Reviewing files that changed from the base of the PR and between eb7c687 and def242f.

📒 Files selected for processing (5)
  • api/src/db/__tests__/postgresReachable.test.ts
  • api/src/db/postgresReachable.ts
  • audit/requirements/pipeline/README.md
  • audit/requirements/pipeline/acceptance-check.py
  • audit/requirements/pipeline/test_acceptance_check.py

Comment on lines +127 to +137
server = net.createServer();
const port = await new Promise<number>((resolve, reject) => {
server?.listen(0, '::1', () => {
const address = server?.address();
if (address && typeof address === 'object') {
resolve(address.port);
} else {
reject(new Error('server did not report a port'));
}
});
});

Copy link
Copy Markdown

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

🏁 Script executed:

#!/bin/bash
set -euo pipefail

node <<'NODE'
const net = require('node:net');

const server = net.createServer();
server.once('error', error => {
  console.error(`IPv6 loopback is unavailable: ${error.code ?? error.message}`);
  process.exitCode = 1;
});
server.listen(0, '::1', () => {
  console.log('IPv6 loopback is available');
  server.close();
});
NODE

Repository: troysatchell/ship

Length of output: 182


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="api/src/db/__tests__/postgresReachable.test.ts"

printf '%s\n' '--- test structure ---'
sed -n '1,190p' "$file"

printf '%s\n' '--- relevant symbols and scripts ---'
rg -n -C 3 "createServer|listen\\(|createConnection|postgresReachable|IPv6|::1|describe\\(|it\\(|test\\(" \
  "$file" package.json api/package.json 2>/dev/null || true

printf '%s\n' '--- deterministic IPv6 listener probe ---'
node <<'NODE'
const net = require('node:net');

const server = net.createServer();
let callbackCalled = false;
let errorCode = null;

server.once('error', error => {
  errorCode = error.code ?? error.message;
  console.log(JSON.stringify({
    ipv6LoopbackAvailable: false,
    error: errorCode,
    callbackCalled,
  }));
});

server.listen(0, '::1', () => {
  callbackCalled = true;
  const address = server.address();
  console.log(JSON.stringify({
    ipv6LoopbackAvailable: true,
    address,
    callbackCalled,
    hasErrorListener: server.listenerCount('error') > 0,
  }));
  server.close();
});
NODE

Repository: troysatchell/ship

Length of output: 23022


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- reachability implementation ---'
fd -i 'postgresReachable' .
for file in $(fd -i 'postgresReachable' . | head -20); do
  if [ -f "$file" ]; then
    printf '\n--- %s ---\n' "$file"
    wc -l "$file"
    sed -n '1,220p' "$file"
  fi
done

printf '%s\n' '--- existing net mocking patterns ---'
rg -n -C 4 "vi\.(mock|spyOn)|mock\(.*node:net|createConnection|listen\\(" \
  --glob '*.{ts,tsx}' api/src | head -300

printf '%s\n' '--- failure behavior without an error listener ---'
node <<'NODE'
const net = require('node:net');

const server = net.createServer();
process.once('uncaughtException', error => {
  console.log(JSON.stringify({
    uncaughtException: true,
    code: error.code ?? null,
    message: error.message,
  }));
  process.exitCode = 0;
});
server.listen(0, '2001:db8::1', () => {
  console.log(JSON.stringify({ unexpectedSuccess: true }));
  server.close();
});
NODE

Repository: troysatchell/ship

Length of output: 41396


Remove the IPv6 host dependency from this test.

When the CI executor has IPv6 disabled, server.listen(0, '::1') emits EADDRNOTAVAIL. The promise has no error listener, so the test can fail with an uncaught exception.

Mock net.createConnection, emit connect, and assert that the options use host: '::1'. Keep the real IPv4 reachability test as the TCP integration test.

🤖 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 `@api/src/db/__tests__/postgresReachable.test.ts` around lines 127 - 137,
Replace the IPv6-dependent server setup in the postgres reachability test with a
mock of net.createConnection that emits connect, and assert the connection
options specify host: '::1'. Remove the server.listen/port allocation path while
preserving the existing real IPv4 reachability test as the TCP integration
coverage.

Comment thread audit/requirements/pipeline/acceptance-check.py Outdated
troysatchell and others added 2 commits August 8, 2026 16:36
…nown IDs pass

Third round of CodeRabbit review on this gate. Both findings real, same
false-pass class as rounds 1 and 2:

**Line 134 (Major): duplicates and unknown IDs both pass.**
`matrix_ids = {r["id"] for r in matrix["requirements"]}` silently collapses
a repeated ID to one set member, and the missing-row check only ever asks
"is every active ID present in matrix_ids" — never the other direction, "is
every matrix ID actually active". So a matrix containing every active ID
PLUS a duplicate row, or PLUS an invented ID, reached every check unchanged
and printed OK.

Added two checks before the existing missing-row check: list-length vs
set-length catches duplicates (names the repeated IDs); `matrix_ids -
set(active_ids)` catches unknown IDs (names them). Confirmed against
merge-matrix.py's own reqs loop (`for rid in ids:`, where `ids` is the
active-only list) that a legitimate matrix never contains a row outside
active_ids — so this isn't a new constraint, it's enforcing one the pipeline
already assumes but never checked on the read side.

Demonstrated in a scratch copy (real audit/requirements/ confirmed
untouched via `git status`), against the previous commit's version (def242f):

  Duplicate-row scenario (every active ID present + a second W4-R1 row):
    Old: `OK — 55 rows, verdicts sound`, exit 0.
    New: `FAIL: 1 requirement ID(s) appear more than once in the matrix: W4-R1`, exit 1.

  Unknown-ID scenario (every active ID present + an invented W4-R99 row):
    Old: `OK — 55 rows, verdicts sound`, exit 0.
    New: `FAIL: 1 requirement ID(s) in the matrix are not active (unknown or
    should-be-excluded-retired): W4-R99`, exit 1.

**The inverted-question pass, as asked.** Enumerated the matrices that
SHOULD fail the gate — missing row, duplicate row, unknown row, bad verdict,
empty evidence, VERIFIED without verification, MISSING absent from gaps,
ASSUMED without assumption, PARTIAL without suggested_scope (9 categories,
9b added: MISSING without suggested_scope, since the same check covers both
verdicts but only PARTIAL had a case) — and checked test_acceptance_check.py
against it. Full result in FindFirstViolationTests' new class docstring:

  Already existed: missing row, bad verdict, empty evidence (whitespace
  variant), MISSING-absent-from-gaps, ASSUMED-without-assumption (whitespace
  variant), PARTIAL-without-suggested_scope. 6 of 9(b).

  Added this round: duplicate row, unknown row, VERIFIED-without-
  verification (verification key absent, distinct from present-but-blank),
  ASSUMED-without-assumption (assumption key absent, same distinction),
  MISSING-without-suggested_scope. 5 new cases.

`python3 audit/requirements/pipeline/test_acceptance_check.py`: 19/19
passing (was 14, +5). Re-ran the real, unmodified pipeline: `OK — 54 rows,
verdicts sound`, exit 0, from repo root and an unrelated cwd.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…obe, not an assumption

CodeRabbit, postgresReachable.test.ts:137 (Major). The "resolves true
against an IPv6 listener" test unconditionally called
`server.listen(0, '::1', ...)`. On a host with IPv6 loopback disabled that
emits `EADDRNOTAVAIL`/`EAFNOSUPPORT` on the server's 'error' event, which
this Promise-wrapped listen had no listener for — an uncaught exception, and
one that looks exactly like a code defect rather than what it actually is:
an environment assumption. Third instance of this class on this branch,
after the port-1 "guaranteed closed" claim and the port-5432 default-port
test, both already documented in CHANGES.md.

**The fix that was explicitly wrong, and why:** blanket-skipping or
mocking-away the IPv6 case entirely would make this the same defect as the
68 e2e specs that pass without asserting anything — a green result that
means nothing. Also wrong: gating on `os.networkInterfaces()` or a similar
heuristic, since the test is about to attempt a bind and the heuristic could
disagree with the real bind's own failure mode.

**What changed.** Added `ipv6LoopbackAvailable()`: attempts the real bind,
resolves `true`/`false` for the two specific error codes that mean "not
available here" (`EADDRNOTAVAIL`, `EAFNOSUPPORT`), and rethrows anything
else — an unexpected bind failure should fail loudly, not be silently
absorbed into "must be environmental". Decided once via top-level await
(this file is ESM) before `describe`/`it` register, so every test in the
file sees the same answer. The IPv6 end-to-end test is now registered with
`(IPV6_LOOPBACK_AVAILABLE ? it : it.skip)(...)` — a real Vitest skip, which
shows as `↓ ... 1 skipped` in the report, not silently folded into "passed".
A `console.warn` fires unconditionally when it skips, naming exactly which
case and why. The pure `resolveHostPort` IPv6 bracket-stripping assertions
(added in 2531ca1) are untouched: no socket, no environment dependency,
still run unconditionally — those are the actual guarantee against the
bracket-stripping regression; the end-to-end case is corroborating evidence
on top, not the only place it could be caught.

**Verified both branches, not just reasoned about them.** This sandbox has
IPv6 loopback available (confirmed directly: `node -e "..."` binds `::1`
successfully), so the normal run exercises the real `it` path: 12/12
passing, all named tests including the IPv6 one visibly ran (`--reporter=
verbose`). Then temporarily forced `IPV6_LOOPBACK_AVAILABLE = false` to
prove the *other* branch actually works — confirmed the `console.warn`
printed, the test showed as `↓ ... skipped` (11 passed | 1 skipped, not
12 passed), and the other 11 cases including all 5 pure `resolveHostPort`
IPv6/non-IPv6 cases still ran and passed — then reverted (`diff` against a
backup confirmed a byte-for-byte clean revert before committing).

`npx vitest run postgresReachable.test.ts`: 12/12 passing.
`pnpm --filter @ship/api type-check`: clean. `npx eslint
api/src/db/__tests__/postgresReachable.test.ts`: clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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