{
  "schema_version": 1,
  "title": "Miru author-side findings recorded as fixed before independent review",
  "unit": "Distinct retained finding records; not necessarily distinct underlying defects.",
  "provenance": {
    "applies_to": "Every row in findings.",
    "origin": "Retained private author-side review capture, released here as sanitized evidence excerpts.",
    "source_snapshot_at": "2026-11-01T11:14:50.600925+00:00",
    "capture_completed_at": "2026-11-01T11:30:04.991387+00:00",
    "source_extract_sha256": "57d875de8530ca6756214c704cb1bd0e3cb92e42a2caedd473a96b93cd7bd9ba",
    "selection": "From the merged 103-PR assessment cohort, select author-side plan/build findings whose recorded answer is fixed and whose finding and answer timestamps both precede the first submitted non-pending independent review. Preserve one row per stored finding identity, using the earliest available finding header and latest recorded answer. This includes 99 implementation PRs, 2 design PRs and 2 human preparation/scope PRs; the latter contribute no rows.",
    "record_ids": "Stable public identifiers shared with the source extract; private operational identifiers are omitted.",
    "public_link_scope": "PR links identify public work. Commit links identify the recorded build correction target. Neither link is represented as a public copy of the retained finding or answer.",
    "correction_claim": "Fixed is the author ledger resolution. The 71 distinct build commit objects were located; individual patches and retained behavior were not independently audited for every finding. Plan revisions are private and have no public commit link in these records.",
    "excerpt_policy": "Preserve the substantive finding and recorded correction. Remove issue-quotation footers, suggested-fix repetition, review-process commentary and test execution logs. Expand internal plan-section labels into plain words. The excerpts are edited retained evidence, not raw transcripts; no extra text is invented to complete a source excerpt.",
    "integrity_limits": "Public row and file hashes verify this export. The retained-body and source-extract hashes identify withheld source text but cannot reproduce or independently authenticate it. The offline verifier checks internal consistency, not the truth of the source claims.",
    "scope_limits": "No semantic deduplication, measured causal effect, category assignment or assessment score is established by this bundle."
  },
  "reconciliation": {
    "metric_source": "Counts of the included findings array unless explicitly marked as cohort metadata.",
    "merged_pr_cohort": {
      "total": 103,
      "implementation": 99,
      "design": 2,
      "human_preparation_or_scope": 2,
      "source": "Assessment cohort metadata retained with the source extract; absent-record PRs cannot be reconstructed from the findings array."
    },
    "records": 585,
    "prs_with_records": 87,
    "by_cohort": {
      "merged_implementation": {
        "records": 560,
        "prs_with_records": 85,
        "plan": 397,
        "build": 163
      },
      "merged_design": {
        "records": 25,
        "prs_with_records": 2,
        "plan": 14,
        "build": 11
      }
    },
    "by_stage": {
      "plan": 411,
      "build": 174
    },
    "by_recorded_severity": {
      "blocker": 59,
      "high": 147,
      "medium": 242,
      "low": 137
    },
    "structured_locus": {
      "plan": {
        "records": 411,
        "with_path": 0,
        "with_line": 0
      },
      "build": {
        "records": 174,
        "with_path": 174,
        "with_line": 174
      }
    },
    "correction_targets": {
      "plan_revision_records": 411,
      "code_commit_records": 174,
      "distinct_code_commits": 71
    },
    "timing": {
      "finding_and_answer_before_first_independent_review": 585,
      "finding_and_answer_before_pr_creation": 411,
      "plan_before_pr_creation": 411,
      "build_before_pr_creation": 0
    }
  },
  "findings": [
    {"record_id":"author-pr4-build-001","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Delayed claim replaces the re-added entry's current hold","finding_evidence":"A delayed claim for a removed association counts as another same-invoice replacement and overwrites the re-added association's hold. The current association's token stops matching, its release does nothing, and the entry stays locked. Token checks stop stale releases but not stale claims.","recorded_resolution":"fixed","recorded_correction":"Each claim and release now carries a rising sequence number, the billing outbox row id. A high-water mark kept per invoice and entry rejects a stale claim even after release. A reverse-order claim case is added to the failure table.","correction_target":"code_commit","source_path":"docs/architecture/timekeeping-soa.md","source_line":446,"fix_commit":"c3562e1fe63256373836398887a7fcefb525de30","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/c3562e1fe63256373836398887a7fcefb525de30","finding_recorded_at":"2026-10-24T07:22:46.244Z","answer_recorded_at":"2026-10-24T07:34:05.757Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"8236ab92b4388430375ffe9b9b73897bc995a1166dc1738bdf3096126ebcce8b","record_sha256":"5219d1feffaa735217d0f62507d2ada1e1806613f8543e901b859c663f4fabbe"},
    {"record_id":"author-pr4-build-002","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Locks taken after the backfill never get hold tokens","finding_evidence":"Only the one-time backfill turns locks into holds and writes tokens back. Drafts locked by the old callbacks during P2, or after a rollback, never get a token. After cutover, billing releases only with stored tokens, so discarding such a draft or removing its line items leaves the entries locked. This breaks L2 and L4 for every draft open during cutover.","recorded_resolution":"fixed","recorded_correction":"Cutover now re-runs the lock-to-hold conversion for the company and writes tokens back. Rollback clears the tokens. An end-to-end test covers a draft opened before cutover and discarded after it.","correction_target":"code_commit","source_path":"docs/architecture/timekeeping-soa.md","source_line":604,"fix_commit":"c3562e1fe63256373836398887a7fcefb525de30","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/c3562e1fe63256373836398887a7fcefb525de30","finding_recorded_at":"2026-10-24T07:22:46.244Z","answer_recorded_at":"2026-10-24T07:34:05.757Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"70e0b8dd70b5ec280ad4b5916d8dd4dea7fb8fbe8463bfeba7a7433ce804fdae","record_sha256":"9a069bea953056e82a194eda95c6fdbab7260f6d1af40111723e28a3bce819cf"},
    {"record_id":"author-pr4-build-003","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"P5 stops the wrong sync; AC1 and post-P5 rollback contradict","finding_evidence":"Rollback relies on Timekeeping changes flowing into the monolith until P5, but P5 stops the opposite direction. No step stops the reverse flow, so AC1 ('no writes for 30 days') can never pass. If P5 is read the other way, the 90-day rollback window has no working rollback, because the table is read-only and outbox retention is never stated.","recorded_resolution":"fixed","recorded_correction":"P5 now says the monolith-to-Timekeeping sync stops at each company's cutover. The reverse sync runs for the whole rollback window and stops only when the window ends. The 30-day no-writes criterion now excludes that sync.","correction_target":"code_commit","source_path":"docs/architecture/timekeeping-soa.md","source_line":595,"fix_commit":"c3562e1fe63256373836398887a7fcefb525de30","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/c3562e1fe63256373836398887a7fcefb525de30","finding_recorded_at":"2026-10-24T07:22:46.244Z","answer_recorded_at":"2026-10-24T07:34:05.757Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"57bef46e72d7ddf9764b1f0dbbf2d04ce621948175f9ed546a000ef683194284","record_sha256":"87b8046048f6697462e03c2b29c89af0cf7c90e7349025b23f1eef8d0c699745"},
    {"record_id":"author-pr4-build-004","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Pending claim retry can hold entries of a discarded draft","finding_evidence":"A draft discarded while its claim is still pending has no stored token, so the discard releases nothing. The retry job then places live holds for a discarded invoice. The reconciler never releases them, because a soft-discarded invoice still has its invoice and association rows. This breaks L2, and it cannot be told apart from the preserved L3 quirk.","recorded_resolution":"fixed","recorded_correction":"Discard now moves pending associations to releasing. The retry job re-reads the invoice and skips the claim once it is discarded, and a late token is released right away. The reconciler releases holds of invoices discarded while still drafts.","correction_target":"code_commit","source_path":"docs/architecture/timekeeping-soa.md","source_line":536,"fix_commit":"c3562e1fe63256373836398887a7fcefb525de30","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/c3562e1fe63256373836398887a7fcefb525de30","finding_recorded_at":"2026-10-24T07:22:46.244Z","answer_recorded_at":"2026-10-24T07:34:05.757Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"e5390babc495351e2a9dfc4eb825a4095906439ee51315aad85fd642d280e0e3","record_sha256":"41b077c52740385bb767f17bc5c9889830d64bb263835804888f06a60cbbc54a"},
    {"record_id":"author-pr4-build-005","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Reordered hold events suppress newer report field values","finding_evidence":"HoldClaimed carries the higher shared entry version but no duration. If it arrives before the Updated event that carried the new duration, the highest-version rule drops that update, and reports keep the old duration for good. This breaks the document's out-of-order convergence promise and its report-parity goal.","recorded_resolution":"fixed","recorded_correction":"Hold and billed events now carry the full entry snapshot, as Updated does. A failure-table row covers the hold event arriving before the older update.","correction_target":"code_commit","source_path":"docs/architecture/timekeeping-soa.md","source_line":380,"fix_commit":"c3562e1fe63256373836398887a7fcefb525de30","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/c3562e1fe63256373836398887a7fcefb525de30","finding_recorded_at":"2026-10-24T07:22:46.244Z","answer_recorded_at":"2026-10-24T07:34:05.757Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"7001f16b92f01828e6d181dbb1b5803a314096a217a7fb67c00a252a0d5abe34","record_sha256":"c6f5b414f2aeac4663a7c8c0b54a2b5f1145ed2705f0437ad946ad957111e014"},
    {"record_id":"author-pr4-build-006","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"The 'shadow only' D1 option cannot work with the hold design","finding_evidence":"Shadow mode is only described for P2. For companies moved in P3, the unique held index and the 409 held_by_other rule give no way to accept a second invoice's claim. If owners pick shadow-only, or have not decided D1 when a wave moves, invoice saves for moved companies start failing. That is the customer-visible change the document says needs an owner decision.","recorded_resolution":"fixed","recorded_correction":"Holds are now unique per entry and invoice. A per-company setting, off by default, accepts a second invoice's hold and logs the would-be refusal. Each release clears only its own hold, so moved companies keep today's save behaviour until the owners decide.","correction_target":"code_commit","source_path":"docs/architecture/timekeeping-soa.md","source_line":477,"fix_commit":"c3562e1fe63256373836398887a7fcefb525de30","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/c3562e1fe63256373836398887a7fcefb525de30","finding_recorded_at":"2026-10-24T07:22:46.244Z","answer_recorded_at":"2026-10-24T07:34:05.757Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"65ed206f061dd08266418957329cabd8ab9f3c945d7283984bb0cb0608ac9068","record_sha256":"6edb2790258c184159343ec7e29b71d95603d6f318715751d3dc5749edc39f09"},
    {"record_id":"author-pr4-build-007","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Unrequested behaviour rule: backfill gives a double-locked entry's hold to the oldest invoice","finding_evidence":"Reverse plan check. Neither the plan nor the issue decides which invoice keeps an entry that two invoices lock today. The choice decides which draft's discard unlocks the entry after cutover, and which invoice is left in conflict. Report only; it does not affect the verdict.","recorded_resolution":"fixed","recorded_correction":"I removed the oldest-invoice rule. The backfill now gives an entry locked by two kept invoices one hold per invoice, which copies today's state, so no invoice is picked.","correction_target":"code_commit","source_path":"docs/architecture/timekeeping-soa.md","source_line":606,"fix_commit":"c3562e1fe63256373836398887a7fcefb525de30","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/c3562e1fe63256373836398887a7fcefb525de30","finding_recorded_at":"2026-10-24T07:22:46.244Z","answer_recorded_at":"2026-10-24T07:34:05.757Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"3af576481cb9a7a80e8298aea1ea9d06b00eac75d54023d57953ba5aeea7dd42","record_sha256":"3edd0c2e82016517a7990ca7654127631c2eb87ced02aa237df6967611a902db"},
    {"record_id":"author-pr4-plan-001","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"An invoice identifier alone cannot protect a renewed hold from a delayed release.","finding_evidence":"plan item 4 checks the invoice identifier when releasing a hold, but the same invoice can acquire an entry again. app/models/invoice_line_item.rb releases entries on draft line-item deletion, and app/models/invoice.rb locks entries on draft saves. Consider removing an entry from draft A, delaying its release command, then adding it back to A. The renewed claim succeeds for the same holder, but the delayed release still matches A and removes the current hold. Another invoice can then claim the entry. Deduplication by event identifier does not reject a release arriving for the first time. Specify a hold generation or an enforced ordering rule that rejects releases from an earlier association. Add this same-invoice sequence to the proposed failure table.","recorded_resolution":"fixed","recorded_correction":"Each billing hold now carries its own token, and a release must match that token atomically. The remove-then-re-add-on-the-same-draft case is added to the planned failure table.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-24T06:54:33.130Z","answer_recorded_at":"2026-10-24T06:55:40.848Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"6600ded00fa8ed92900a465c8259e0bcf5815998ccf44d49a432bd7f884460d7","record_sha256":"71f9ff18265eb570a58acd333b52b4168977ddd75cb6799bb69e46955af0c601"},
    {"record_id":"author-pr4-plan-002","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Cutting over individual entry points can create two writers for the same time entry.","finding_evidence":"plan item 8 requires one writer per field but switches writes per entry point. Both app/controllers/api/v1/timesheet_entry_controller.rb and app/controllers/api/v1/cli/timesheet_entries_controller.rb update duration and note on existing entries. After the web path switches, a web edit can reach Timekeeping while a CLI edit reaches the monolith for the same entry. Each database can acknowledge a different value, so billing and reports can disagree with the employee's saved time. Specify how every entry point routes writes to the current owner, including during rollback. Alternatively, switch ownership by tenant and route all its writers together. Include a verification scenario that alternates web and CLI edits during cutover.","recorded_resolution":"fixed","recorded_correction":"Ownership now switches per company, and every entry point routes through the same ownership check, including during rollback. A verification scenario that alternates web and CLI edits during cutover is added.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-24T06:54:33.130Z","answer_recorded_at":"2026-10-24T06:55:40.848Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"ed2017cf473c83150e6744c42aad355385b33fbfe0f0eb0a1c08db38dccaf75a","record_sha256":"e834d3bd3df01289eacf2ca55f038009ff2a028293a6fec9c9124e6b1c44a7c5"},
    {"record_id":"author-pr4-plan-003","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Define how Timekeeping obtains the reference data its rules require.","finding_evidence":"plan item 1 assigns entry ownership, but leaves the service boundary for supporting data undefined. In app/models/timesheet_entry.rb, validation reads project.billable and the agent's company and user. In app/policies/timesheet_entry_policy.rb, edit permission reads company.timesheet_edit_days through project.client.company. The signed role token in plan item 6 does not supply these facts. Specify their authoritative owners and how Timekeeping reads them without database joins across services. Define freshness and failure behavior for remote reads or local copies, including changes to settings and project membership. Otherwise implementers must independently choose interfaces that can preserve different rules or retain the existing database coupling.","recorded_resolution":"fixed","recorded_correction":"Added a decision that names who owns the reference data Timekeeping needs: project billable flag, project membership, company edit window and roles. It also covers how Timekeeping keeps local copies, how fresh they must be, and what fails closed.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-24T06:54:33.130Z","answer_recorded_at":"2026-10-24T06:55:40.848Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"8f6c7144a857a23935da4ffb4d11a16c5c735cb3b6222200da6e3bc7b7a589a4","record_sha256":"fe29d1e45e987b46aa33e1c0509c80c37db01d5324d462dc5df4982982337b32"},
    {"record_id":"author-pr4-plan-004","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Identify each billing hold separately from the invoice that owns it.","finding_evidence":"The release interface in plan item 4 identifies a hold only by company, entry and invoice. That cannot distinguish two holds created by the same invoice. A draft can remove an entry and later add it again. If the first release arrives after the new claim, it can clear the new hold because the invoice still matches. Deduplicating request identifiers does not reject a delayed request that has not run before. Another invoice could then claim the entry. Give each claim a distinct token or revision, and require release to match that value atomically. Include this case in the planned state machine and failure table.","recorded_resolution":"fixed","recorded_correction":"Same fix as the delayed-release finding: each claim gets its own hold token, and a release clears a hold only when that token matches. The case is added to the hold state machine and the failure table.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-24T06:54:33.130Z","answer_recorded_at":"2026-10-24T06:55:40.848Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"bb6ea74e9c3f8e602c1412bba92f767bfd735558650492f1ddc0281c78d2cb19","record_sha256":"47c12448f801a51876d615d1d43c9b3662d0fbae3ef18881fecccdde55cd9b50"},
    {"record_id":"author-pr4-plan-005","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep the requesting user's identity on CLI and MCP calls.","finding_evidence":"plan item 6 groups CLI and MCP calls with calls that have no user, then assigns them a service identity. These paths already carry a user. app/controllers/api/v1/cli/timesheet_entries_controller.rb authorizes current_timesheet_entry and scopes it to current_user. app/controllers/api/v1/mcp_controller.rb supplies user_id, and app/mcp/tools/time_update_tool.rb forwards updates to the CLI endpoint. Treating these paths as background operations could let future callers bypass user-specific ownership, billed-entry and edit-window checks. Separate user-delegated calls from background commands in plan item 6. Require CLI and MCP requests to preserve the user's identity and permissions across the service boundary, even when the calling service authenticates separately.","recorded_resolution":"fixed","recorded_correction":"I checked the CLI timesheet controller and the MCP tools, and both already carry a user. The design now treats CLI, MCP, mobile, timer and agent calls as calls on a user's behalf that keep that user's permissions; only jobs, imports and billing commands use a service identity.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-24T06:54:33.130Z","answer_recorded_at":"2026-10-24T06:55:40.848Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"1b8d976eb71a69a80423b1fa038471886a7df936ccf7d01fca9fb4745c2e0bb2","record_sha256":"8d07ad83b216fcdc1b646ae83615a0e7d6b0526d4ba94341ba9b948d76a0b41a"},
    {"record_id":"author-pr4-plan-006","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Move write ownership before switching individual entry points.","finding_evidence":"plan item 8 promises one writer per field, but switches writes separately for each entry point. Both app/controllers/api/v1/timesheet_entry_controller.rb and app/controllers/api/v1/cli/timesheet_entries_controller.rb update the same entries, including duration, project and billing status. Switching the web endpoint while the command-line endpoint still writes locally leaves two authorities for those fields. An accepted edit can then be overwritten during replication or rollback, changing recorded time and billing inputs. Amend the planned document to establish one authoritative destination for every writer before switching endpoints. For example, route all writes for a selected company to its current owner, reject writes at the former owner, and drain outstanding writes before transferring ownership. Include invoice callbacks, imports and jobs in that prerequisite, and name the verification that","recorded_resolution":"fixed","recorded_correction":"Cutover now moves write ownership per company, not per entry point. Every writer, including invoice callbacks, imports and jobs, routes through one ownership check, the former owner rejects writes, and in-flight writes drain before the switch.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-24T06:54:33.130Z","answer_recorded_at":"2026-10-24T06:55:40.848Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"ce573a60fe070c987982c745da2c03618d9bf1d8240476968198edf0843f5278","record_sha256":"6a176fea44dfb6a2e55abac6762197b699b32c82218acad582f19b4b449e97ba"},
    {"record_id":"author-pr4-plan-007","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The cutover plan does not reconcile per-entry-point flags with one writer per field.","finding_evidence":"plan item 8 requires one writer per field, but also cuts over writes separately for each entry point. Web requests and imports can update the same entry through different paths. The plan does not state whether both paths use one authoritative writer during this phase. One implementer could route both paths to that writer; another could let each path write its local database. Specify the ownership switch and routing rule for paths that have not yet moved. Make the sequence in U3 follow that rule, including rollback.","recorded_resolution":"fixed","recorded_correction":"There are no more per-entry-point flags. Every path, including web and imports, reads one ownership record per company and writes only to the current owner. The cutover and rollback sequence follows that rule.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-24T06:54:33.130Z","answer_recorded_at":"2026-10-24T06:55:40.848Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"e384ee945c2027d2195adb65817fc4e5311e07b54d717acf49d1b29d1330c213","record_sha256":"01af906ce078e25705e613ae129f63d6c86414bf9375a7ef4bcd8c14d0339f50"},
    {"record_id":"author-pr4-plan-008","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The invoice-send failure case assumes a state change that the observed behavior does not establish.","finding_evidence":"plan item 2 says the invoice mailer path that sets `billed` is uncalled and sent invoices typically remain `unbilled`. U2 nevertheless requires recovery for “invoice sent but mark-billed lost.” That scenario implies a reliable mark-billed command after sending, without identifying it as a proposed behavior change. Engineers could preserve today's state or introduce the new transition. State which behavior the target preserves. If the transition is optional future work, label it separately and keep it outside migration acceptance and recovery requirements.","recorded_resolution":"fixed","recorded_correction":"The target keeps today's behaviour: sending an invoice does not mark entries billed. Marking them billed on send is labelled as optional future work, and it is kept out of migration acceptance and the failure table.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-24T06:54:33.130Z","answer_recorded_at":"2026-10-24T06:55:40.848Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"86627584d26046f5fffd736ebab0b9b8cb33db52357ba5071b202f8161814851","record_sha256":"5831bdcdbc7a970b7fe97ce2e7ce66f5aca6fcabe1d15d3878890209832473e5"},
    {"record_id":"author-pr4-plan-009","pr":4,"pr_url":"https://github.com/Homie-Improvement/miru/pull/4","cohort":"merged_design","stage":"plan","review_round":2,"recorded_severity":"high","finding_title":"The plan goes beyond the issue: Replicate monolith-owned reference data into Timekeeping and delay settings and permission changes until events arrive.","finding_evidence":"Defining reference-data ownership and access is required, but accepting delayed enforcement of settings and authorization data explicitly changes the preservation rule. The earlier review asked for freshness and failure semantics, not permission to weaken them; retain authoritative authorization decisions or present delayed enforcement as an unresolved alternative rather than the selected target.","recorded_resolution":"fixed","recorded_correction":"The design no longer accepts delayed enforcement. Authorization facts come fresh from the monolith on every write, and if they cannot be read the write is refused. Event-fed copies are listed only as an alternative for review.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-24T06:56:20.631Z","answer_recorded_at":"2026-10-24T06:57:27.483Z","first_independent_review_at":"2026-10-24T07:39:10Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c9b00dcb8045bf710ba2adeb887981d94d7cb8f66c70b02cd0cd99365780b73e","record_sha256":"9575228b2f1279a1f3344d12f4eed621f44bfefa95770f486f040d614a33ccfa"},
    {"record_id":"author-pr6-build-001","pr":6,"pr_url":"https://github.com/Homie-Improvement/miru/pull/6","cohort":"merged_design","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Aborting before drain strands the outgoing owner's snapshots","finding_evidence":"Reports can stay stale indefinitely after an aborted handoff, even though the original owner is accepting writes again. If snapshots are queued when the handoff starts and the operator aborts before the step-3 drain, abort keeps the raised generation and clears handoff_step. Section 5.4 (line 518) then refuses the queued previous-generation snapshots with 409. The relay delivers in outbox order and marks a row sent only after a 200 (line 512), so it stalls on the first refused row. That contradicts 'Why no write is lost'.","recorded_resolution":"fixed","recorded_correction":"Abort now finishes settle and the bounded drain before the outgoing owner admits writes again. If the drain cannot finish, the handoff stays fenced and resumable. The interrupted-handoff test covers abort before step 3.","correction_target":"code_commit","source_path":"docs/architecture/timekeeping-soa.md","source_line":941,"fix_commit":"ad58d731c0d4e945da9d0eb3508b4065a37854cf","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ad58d731c0d4e945da9d0eb3508b4065a37854cf","finding_recorded_at":"2026-10-25T04:15:59.052Z","answer_recorded_at":"2026-10-25T04:26:48.377Z","first_independent_review_at":"2026-10-25T04:32:51Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"bc2edeec22ab03fad4ad03fbe858565add8f1b303ecbe7a4ed4acf5681cc0fd0","record_sha256":"3640b66eca278e2b88ef48faa676cb479d795e028b4371b286c647f5ea92cb25"},
    {"record_id":"author-pr6-build-002","pr":6,"pr_url":"https://github.com/Homie-Improvement/miru/pull/6","cohort":"merged_design","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Draft line-item destroy does not unlock aggregate-linked entries","finding_evidence":"The revised L4 row and section 1.4 say that destroying a line item on a draft unlocks its entries, including every line item of a draft destroyed by the bulk delete. For a project aggregate line that is not what the code does. The dependent: :destroy on the join rows (line 6) runs before before_destroy :unlock_timesheet_entries (line 10), so by the time associated_timesheet_entry_ids reads linked_timesheet_entry_ids the join rows are gone, and only the direct timesheet_entry_id is unlocked. Aggregate-linked entries are left locked with no invoice referencing them. Section 10.4 does not name this as a source of orphans and S0 does not pin it. This breaks plan R2 (where the document describes today's behaviour, the code wins).","recorded_resolution":"fixed","recorded_correction":"I confirmed it with a small ActiveRecord 8.1 model that mirrors the callback order. The unlock saw no linked ids, and both linked entries stayed locked. L4, section 1.4, the orphan list and the S0 specs now say so.","correction_target":"code_commit","source_path":"docs/architecture/timekeeping-soa.md","source_line":223,"fix_commit":"ad58d731c0d4e945da9d0eb3508b4065a37854cf","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ad58d731c0d4e945da9d0eb3508b4065a37854cf","finding_recorded_at":"2026-10-25T04:15:59.052Z","answer_recorded_at":"2026-10-25T04:26:48.377Z","first_independent_review_at":"2026-10-25T04:32:51Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"e939cc0886b5bf503fe168abf8b12758686de5d7d81ae90bf336ccfd6fef9e1c","record_sha256":"f13263636b797b0aba6f87a5508d17853db9ca3361c33bbca246a10cdc5a9fd2"},
    {"record_id":"author-pr6-build-003","pr":6,"pr_url":"https://github.com/Homie-Improvement/miru/pull/6","cohort":"merged_design","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Failed user deletion leaves a live user permanently retired","finding_evidence":"A user whose hard delete fails keeps their account but can never log time again. Take a user who has an expense and no time entries. The retire command passes and commits in Timekeeping. The monolith's destroy! then fails on the expenses -> users foreign key and rolls back locally, but the retirement mark stays, and section 10.3 carries it through every checkpoint. The design defines no way to reverse it.","recorded_resolution":"fixed","recorded_correction":"User retirement now follows the delete's durable outcome: retiring, then committed or aborted, with recovery. A failed destroy clears the mark. A new test covers the expenses foreign-key failure and a crash between retire and destroy.","correction_target":"code_commit","source_path":"docs/architecture/timekeeping-soa.md","source_line":371,"fix_commit":"ad58d731c0d4e945da9d0eb3508b4065a37854cf","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ad58d731c0d4e945da9d0eb3508b4065a37854cf","finding_recorded_at":"2026-10-25T04:15:59.052Z","answer_recorded_at":"2026-10-25T04:26:48.377Z","first_independent_review_at":"2026-10-25T04:32:51Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"1feab05682812ab1249a573d094c3ef9765af92ad8a35107149b0245f94181eb","record_sha256":"df93a3e6ade8d3907924f03b3674dca820d077af87c16c792430ad53f507f6fb"},
    {"record_id":"author-pr6-build-004","pr":6,"pr_url":"https://github.com/Homie-Improvement/miru/pull/6","cohort":"merged_design","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Unrequested behaviour rule: every billing write is refused with 503 during a handoff","finding_evidence":"The issue asks for one bounded write pause on entry writes. The document widens it to every invoice, line-item, invoice-tax and payment write, including mobile-collection payments, for the whole handoff. Users will see this, and neither the plan nor the issue asks for it. Requirements reverse check; does not affect the verdict.","recorded_resolution":"fixed","recorded_correction":"The billing-wide pause is kept, because the financial comparison needs two states with no billing write between them. It is now listed in section 15.3 as a disclosed availability change.","correction_target":"code_commit","source_path":"docs/architecture/timekeeping-soa.md","source_line":800,"fix_commit":"ad58d731c0d4e945da9d0eb3508b4065a37854cf","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ad58d731c0d4e945da9d0eb3508b4065a37854cf","finding_recorded_at":"2026-10-25T04:15:59.052Z","answer_recorded_at":"2026-10-25T04:26:48.377Z","first_independent_review_at":"2026-10-25T04:32:51Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"7ed0953534b84b280ea8a514514a3094547a66bb98c4ea1962619765a64abfbb","record_sha256":"59f55f0f2a06192d0759721fee4dce1816d89b9428ee40c77215833a60c2277c"},
    {"record_id":"author-pr6-plan-001","pr":6,"pr_url":"https://github.com/Homie-Improvement/miru/pull/6","cohort":"merged_design","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Keeping the user foreign key does not protect entries that have not reached the projection.","finding_evidence":"plan item 6 assumes users cannot be hard-deleted while entries reference them. However, `app/controllers/api/v1/users_controller.rb:82` calls `target_user.destroy!`, and `app/models/user.rb:38` has no dependent-entry check. Pause the relay, create a user's first entry in Timekeeping, then delete that user as a super administrator. The projection has no referencing row, so its foreign key cannot prevent deletion. When delivery resumes, `db/schema.rb:1327` rejects the snapshot because the user is gone. Reports omit accepted time, and the required outbox drain cannot finish. Revise plan item 6 to protect user deletion against authoritative entries, including concurrent entry creation, rather than relying on the lagging projection. Document and test this ordering.","recorded_resolution":"fixed","recorded_correction":"Same fix as the matching finding: user delete is checked against Timekeeping's entries, not the projection, and new entries for that user are refused while the delete runs. The design states this and it is tested.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T03:32:32.047Z","answer_recorded_at":"2026-10-25T03:34:03.047Z","first_independent_review_at":"2026-10-25T04:32:51Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9ee3eaae47ad975e4f6ea23c42f1dfbb90e5df88edb2ec13ae8de8434b721fec","record_sha256":"3ab9334475a7df44c95552d28ec4b437a04709ca1a6c680a0211cc0b95c7a01a"},
    {"record_id":"author-pr6-plan-002","pr":6,"pr_url":"https://github.com/Homie-Improvement/miru/pull/6","cohort":"merged_design","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The generation check needs an explicit exception for the final projection drain.","finding_evidence":"plan item 7 rejects snapshots whose generation differs from the current owner's generation. The handoff raises the global generation before it drains the outgoing owner's outbox. Consider an accepted entry whose snapshot remains queued at that point. Its snapshot carries the previous generation, so the stated endpoint rule rejects the delivery needed to complete the drain. The handoff then cannot finish within its bounded write pause. Define how the consumer accepts the outgoing owner's final snapshots during handoff, while rejecting unrelated stale deliveries. Bound that permission by the frozen outbox position and revoke it at the ownership flip. Include this ordering in the interrupted-handoff verification.","recorded_resolution":"fixed","recorded_correction":"The design now accepts the outgoing owner's queued snapshots only up to the captured final outbox position, and only until ownership flips. The interrupted-handoff check covers this ordering.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T03:32:32.047Z","answer_recorded_at":"2026-10-25T03:34:03.047Z","first_independent_review_at":"2026-10-25T04:32:51Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"793d8e2dcfecdc4118bb0efbdc74edb13da85fe7622e5719acb0acb551954b8c","record_sha256":"8577b846be98193451530560406f88f93941d0375cfe45ff573bf282949e3d1f"},
    {"record_id":"author-pr6-plan-003","pr":6,"pr_url":"https://github.com/Homie-Improvement/miru/pull/6","cohort":"merged_design","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The retained user foreign key does not protect entries that have not reached the projection.","finding_evidence":"plan item 6 assumes users cannot be hard-deleted while entries reference them. But `app/controllers/api/v1/users_controller.rb:51-82` permits a super admin to call `target_user.destroy!`. `app/models/user.rb:38` adds no deletion check; the foreign key currently provides that protection. With the relay paused, create the first entry for a non-owner user, then delete that user. The projection has no entry to block deletion. When delivery resumes, its retained user foreign key rejects the snapshot, preventing reports from receiving the accepted entry. Revise plan item 6 to document this hard-delete path and preserve its rejection when Timekeeping holds entries, including entries absent from the projection. Specify how deletion and entry creation are serialized so a synchronous check cannot race a new write.","recorded_resolution":"fixed","recorded_correction":"Confirmed: the users controller destroy action calls destroy! on the user. The design now has user hard delete ask Timekeeping, which checks for entries and blocks new ones in one transaction.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T03:32:32.047Z","answer_recorded_at":"2026-10-25T03:34:03.047Z","first_independent_review_at":"2026-10-25T04:32:51Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"e35c354916d14d2d05c5f277a978d5c90012889f703dad6ff596bf3f68fd28d0","record_sha256":"258fb7c77dc4b4d2afb383e3ec7029eb51129ae2b19cc42630f4faa88eb8b6e6"},
    {"record_id":"author-pr6-plan-004","pr":6,"pr_url":"https://github.com/Homie-Improvement/miru/pull/6","cohort":"merged_design","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The retired-concept check also rejects mechanisms the plan requires.","finding_evidence":"The billing rewrite requires a search for \"hold, claim, token, floor, seq, shadow and enforced\" to find no live design use. However, the handoff unit requires an ID sequence, and the gateway lane retains the user token and reference claims. An implementer cannot satisfy both instructions literally. Limit this check to the retired billing mechanisms, such as hold tokens, claim floors and per-entry hold sequences. Explicitly allow the allocator sequence and authentication terms.","recorded_resolution":"fixed","recorded_correction":"The search check now targets only the retired billing mechanisms. It explicitly allows the id-sequence handoff, entry versions and the user token used for authentication.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T03:32:32.047Z","answer_recorded_at":"2026-10-25T03:34:03.047Z","first_independent_review_at":"2026-10-25T04:32:51Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"045ec8528ed475827a8e47da66220e55eb827b7ddf363cdfce78ad6727d2e330","record_sha256":"2ad118d3ed867b7b740d23e7f941bec8165cfeb3719acb3bba237ffa1d3c60fa"},
    {"record_id":"author-pr6-plan-005","pr":6,"pr_url":"https://github.com/Homie-Improvement/miru/pull/6","cohort":"merged_design","stage":"plan","review_round":2,"recorded_severity":"high","finding_title":"The plan goes beyond the issue: Exempt seed and demo writers from the gateway and refuse them while Timekeeping owns entries.","finding_evidence":"The issue does not exempt seed or demo writers from its all-writers rule. Replacing those writers with refusal during Timekeeping ownership and limiting the guard to other flows introduces an exception to the mandated boundary; the author must reconcile that exception with the issue before locking the plan.","recorded_resolution":"fixed","recorded_correction":"Seed and demo writers are no longer an exception. While Timekeeping owns the data they go through the gateway like every other writer, and the single-writer guard covers them too.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T03:34:59.205Z","answer_recorded_at":"2026-10-25T03:36:20.050Z","first_independent_review_at":"2026-10-25T04:32:51Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c92b44ca2fcec4507d60b696b7316fe1eb2e88285fefe85235dc96b8d4131f99","record_sha256":"39383799e82e083355b94c062b7fd3240bbf8c3c00914ab15403315630e41c81"},
    {"record_id":"author-pr27-build-001","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Credential proof passes with credential-bearing entry metadata","finding_evidence":"A caller can put its issued bearer token inside source_metadata or proof_metadata, and the unbroken fake publishes it verbatim in the outbox. The example 'carries no credential anywhere in the batch, nested metadata included' seeds only an entry with empty metadata, so it certifies this unsafe producer. The mutation row catches a policy that injects credentials on purpose, not ordinary pass-through. Later consumers can trust a green suite while credentials cross the snapshot boundary, against entry_snapshots.yml rules.credentials and the issue's 'The snapshot carries no credential'.","recorded_resolution":"fixed","recorded_correction":"An entry write whose body holds a token or service identity Timekeeping issued is now refused with 422 credential_in_body, so no snapshot can carry it. New examples cover create and update with nested metadata, and a broken fake that allows it fails them.","correction_target":"code_commit","source_path":"spec/contracts/timekeeping/entry_snapshot_examples.rb","source_line":40,"fix_commit":"08cf7ebb488e7286181bd10ae7d285fab0036b4c","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/08cf7ebb488e7286181bd10ae7d285fab0036b4c","finding_recorded_at":"2026-10-25T09:16:55.124Z","answer_recorded_at":"2026-10-25T09:30:05.884Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"2e97f08a67ed61b281e52682ebc9dc62d1c2a8c1d4346a2b58f8b0297131badb","record_sha256":"b2562260bcb49e5bed1239e5760bc5b76d2131a1ca0f1fdbc056d95ac1e316fa"},
    {"record_id":"author-pr27-build-002","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Duplicated Code: refusal reasons copied from reservations.yml conflict_reasons","finding_evidence":"The intent requires each fact to live once, but the fake classifies prepare conflicts from its own hard-coded copy of reservations.yml conflict_reasons, and writes unresolved_operation's retryable flag as a literal. If the YAML changes, the fake keeps the old set, every group still passes, and later lanes build against the wrong set.","recorded_resolution":"fixed","recorded_correction":"The refusal set is now built from reservations.yml conflict_reasons. The unresolved_operation retryable flag is now read from the contract, so the fake no longer copies either fact.","correction_target":"code_commit","source_path":"spec/support/timekeeping/reservation_rules.rb","source_line":19,"fix_commit":"08cf7ebb488e7286181bd10ae7d285fab0036b4c","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/08cf7ebb488e7286181bd10ae7d285fab0036b4c","finding_recorded_at":"2026-10-25T09:16:55.124Z","answer_recorded_at":"2026-10-25T09:30:05.884Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"cb8b67bed933724f51c5a5dfeb6b08c52d54c0b3a4929d285092b12b0900502d","record_sha256":"525564f269c1905f26c6af06a9344561eb54e9489bd773f3910adebda3ef4a95"},
    {"record_id":"author-pr27-build-003","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Fake and entry contract skip employee scoping on reads and edits (CWE-639)","finding_evidence":"In the fake, an employee token can GET, list, PATCH, DELETE, bulk-move and bulk-delete any colleague's entry in the same company. Only the company is checked, and the acting user only when the body names a user_id. No example or mutant covers this, so a real provider with the same hole passes the entry group, and lanes built against the fake never meet the refusal production will give. The architecture doc's A1/A4 (section 6.3: enforced in Timekeeping, from the token) and the intent's strict actor scoping say otherwise. R4 covers only the body's user_id, so the plan leaves open whether A1/A4 belong in this foundation contract.","recorded_resolution":"fixed","recorded_correction":"I took the finding's second option. The issue asks only for the target user_id rule, so the README now says employee read and edit scoping is not covered by these examples. A provider's own specs must prove it.","correction_target":"code_commit","source_path":"spec/support/timekeeping/fake_timekeeping.rb","source_line":224,"fix_commit":"08cf7ebb488e7286181bd10ae7d285fab0036b4c","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/08cf7ebb488e7286181bd10ae7d285fab0036b4c","finding_recorded_at":"2026-10-25T09:16:55.124Z","answer_recorded_at":"2026-10-25T09:30:05.884Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"2bd78b46f9a302a595105d446774a72952a8c4b9ade721185b3c4a499eaad929","record_sha256":"be45708f370600f157eac29523ae7880782499a3a53b5ea5eb211ecef08448ec"},
    {"record_id":"author-pr27-build-004","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Idempotency contract contradicts terminal-prepare answer for same operation_id","finding_evidence":"A provider that follows idempotency.yml as written (24h receipts of 2xx answers keyed by operation_id) replays the first prepare's {state: prepared} when the same prepare arrives after commit, so it fails the reservations group, which expects 'committed'. The fake settles the order in code alone (prepare answers from the live operation record and bypasses IdempotencyReceipts), so a real provider cannot learn it from the contract files.","recorded_resolution":"fixed","recorded_correction":"idempotency.yml now says prepare answers from the stored operation record, which has no 24-hour limit. A prepare replayed after commit or abort returns that state. The header comment names this exception.","correction_target":"code_commit","source_path":"contracts/timekeeping/idempotency.yml","source_line":19,"fix_commit":"08cf7ebb488e7286181bd10ae7d285fab0036b4c","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/08cf7ebb488e7286181bd10ae7d285fab0036b4c","finding_recorded_at":"2026-10-25T09:16:55.124Z","answer_recorded_at":"2026-10-25T09:30:05.884Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"d6c6077c30a5cb5b7e7eb423c7a5f3199af53e2f407b666a03e70fb89106948c","record_sha256":"d1040cebbd4dcca0bce1af33536729f057f5f74900d9227a86b92932e9ab5c99"},
    {"record_id":"author-pr27-build-005","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"One actor's abort drops another actor's reservations","finding_evidence":"Two services in the same company can both prepare different invoices with the same operation_id, but aborting either operation drops both services' provisional reservations, and committing either commits both entry sets. Operation records are actor-scoped while reservation rows and own_provisional are not. The actor-isolation examples stop after prepare, so the harness passes these corrupt terminal transitions. This breaks reservations.yml rules.abort_scope ('Abort drops only this operation's provisional reservations') and R9's actor isolation.","recorded_resolution":"fixed","recorded_correction":"Each provisional reservation now carries its operation's full identity: company, actor and operation id. A new example shows one service aborting its operation leaves another service's reservation alone. The second service can still commit.","correction_target":"code_commit","source_path":"spec/support/timekeeping/fake_timekeeping.rb","source_line":473,"fix_commit":"08cf7ebb488e7286181bd10ae7d285fab0036b4c","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/08cf7ebb488e7286181bd10ae7d285fab0036b4c","finding_recorded_at":"2026-10-25T09:16:55.124Z","answer_recorded_at":"2026-10-25T09:30:05.884Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"fc4ee71c8ba3171f6be1bd381520f9ad837e6b1eff80dc28348ef5af66671b64","record_sha256":"26e387a1aeaab37f8987b163f520180e38a90bc414766f1eef7b10ac2ac374c1"},
    {"record_id":"author-pr27-build-006","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Rule that only 2xx answers are stored cannot fail any example","finding_evidence":"If IdempotencyReceipts stored every answer, every group would still pass. The one example for the rule sends a stale Ownership-Generation, which FakeTimekeeping#entry_request refuses before it calls @receipts.answer, so no receipt store sees the 409. No example retries a refused snapshot batch with the same key, which is the relay's real retry path. The rule is a hardcoded constant rather than a policy read from idempotency.yml, so mutation_spec cannot break it. A provider that replays a stored 409 would stall the handoff with every group green: the silent pass the intent forbids.","recorded_resolution":"fixed","recorded_correction":"The stored status class is now a receipt policy read from idempotency.yml. A new example retries a refused snapshot batch with the same key and expects 200, and a new broken fake that stores every answer fails it.","correction_target":"code_commit","source_path":"spec/contracts/timekeeping/idempotency_examples.rb","source_line":67,"fix_commit":"08cf7ebb488e7286181bd10ae7d285fab0036b4c","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/08cf7ebb488e7286181bd10ae7d285fab0036b4c","finding_recorded_at":"2026-10-25T09:16:55.124Z","answer_recorded_at":"2026-10-25T09:30:05.884Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"a62dc90112f60ba6fb31098dcc0531e3d564f931a41ca1e55a5fa2470d0809fb","record_sha256":"756aad60d475be9ebc6bdb6f07a358ffb0b26cf283ed7a1670ca8e3ea40bdacd"},
    {"record_id":"author-pr27-build-007","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"conflict and unresolved_operation reason strings absent from contract YAML","finding_evidence":"Every other error in the YAML gives its exact body. The two 409s the monolith must branch on list only body_fields, so 'conflict' and 'unresolved_operation' exist only as literals in the fake and the examples. A provider has to guess them, and a typo shared by fake and examples would pass every group.","recorded_resolution":"fixed","recorded_correction":"reservations.yml now gives the reason strings for both 409s. The fake and the reservation examples read them from the contract, so neither file hard-codes them.","correction_target":"code_commit","source_path":"contracts/timekeeping/reservations.yml","source_line":56,"fix_commit":"08cf7ebb488e7286181bd10ae7d285fab0036b4c","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/08cf7ebb488e7286181bd10ae7d285fab0036b4c","finding_recorded_at":"2026-10-25T09:16:55.124Z","answer_recorded_at":"2026-10-25T09:30:05.884Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"ae65b7c3809ad6c4e61a993d5fb3795c79a0958721d0edcd6d09355322d48296","record_sha256":"2e67ac7ffb46d4086a24040bfec782a143cab50115b9252d13651f9820d634af"},
    {"record_id":"author-pr27-plan-001","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"A refused prepare must preserve existing locks, not make every requested entry unlocked.","finding_evidence":"The planned all-or-nothing example expects every requested entry to read `locked: false` after a conflict. Consider an entry X committed to invoice A and a free entry Y. Invoice B prepares `[Y, X]`. The correct result is a conflict, with Y still unlocked and X still locked by A. The planned assertion rejects that correct implementation and conflicts with the unit's earlier assertion that X stays locked. Compare the state before and after the failed prepare. Assert that free entries gain no reservation and existing reservations remain unchanged.","recorded_resolution":"fixed","recorded_correction":"The refused-prepare example now records each entry's state before the call. Afterwards the free entries stay unlocked and the entry another invoice holds keeps that reservation and stays locked.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T08:14:21.432Z","answer_recorded_at":"2026-10-25T08:16:39.213Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"79d4383d6849adc829d49e98217f2055dc7e5c1a07a64d287f34d59bdf9fbfc3","record_sha256":"aa2da19ae7a62eaa182f6905a9fc0424191f68c50da37870e0d023e8bc48f6b9"},
    {"record_id":"author-pr27-plan-002","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"An exact snapshot key set does not prove that the snapshot contains no credential.","finding_evidence":"plan item 10 treats the exact key-set check as a credential safeguard, but allowed fields can contain credentials. `app/models/timesheet_entry.rb:211-220` preserves arbitrary `proof_metadata` keys and normalized values. A snapshot containing `proof_metadata: { token: \"test-secret\" }` therefore keeps the permitted top-level keys and passes the planned check. This leaves the ticket's credential exclusion unproven for later providers. Define how credential-bearing metadata is prevented or removed at its source. Add a synthetic nested-credential example and a mutant that copies that credential into the snapshot. Do not treat arbitrary names in user-authored notes as identity enrichment.","recorded_resolution":"fixed","recorded_correction":"A new example searches the whole published snapshot, nested metadata included, for the request's own token and the service identity. A new broken fake copies a token into proof_metadata and must fail it.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T08:14:21.432Z","answer_recorded_at":"2026-10-25T08:16:39.213Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"68abf9f657120dc85e7c3a741eb719852aae46c23829d2850403f441c1dd53de","record_sha256":"25cd0720d94e79c41ec2118ad25219ab39712732581e02ac688f5bdc79e5dfd5"},
    {"record_id":"author-pr27-plan-003","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Company-only operation lookup does not prove separation between actors.","finding_evidence":"plan item 6 scopes reservation `operation_id` only to the company. The planned examples test a second company but not a second actor for reservation operations. Let service actor A prepare operation O for an invoice. Then let another authorized service actor B submit O within the same company. A company-only lookup can return A's stored operation to B, yet all listed examples pass. Define actor separation for operation lookup and exercise this case through the injected client. Add a mutant that drops the actor from that lookup, rather than testing actor separation only for entry receipts.","recorded_resolution":"fixed","recorded_correction":"Operations are now looked up by company, calling service and operation id. A new example reuses an operation id from a second service in the same company, and a new broken fake drops the caller from the lookup.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T08:14:21.432Z","answer_recorded_at":"2026-10-25T08:16:39.213Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"46c1732f14eacf437955237b4ca81a2eb1abb6781942d0b5dd06a51683bc841c","record_sha256":"e51b5e76f8f1d6e2ecb641057a5af6c1933f51662839b53eda0d6e8fea8df335"},
    {"record_id":"author-pr27-plan-004","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Deferring save reconciliation leaves a required failure-table row without contract examples.","finding_evidence":"The plan excludes the monolith's save reconciliation and covers only the provider's terminal prepare response. However, docs/architecture/timekeeping-soa.md:766 explicitly marks save reconciliation as Contract. That row requires replaying committed and aborted saves, plus a pending local intent paired with a committed remote operation. Returning the stored prepare state does not prove these outcomes. Add a consumer-side contract seam and fake for those replay decisions, without building production invoice orchestration. Leaving the examples to the invoice lane means this foundation does not cover every row the ticket requires.","recorded_resolution":"fixed","recorded_correction":"The save-reconciliation row is now covered. The plan adds the rule as a table in the contract, a shared group, and a small fake that decides a save's outcome. It builds no invoice code.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T08:14:21.432Z","answer_recorded_at":"2026-10-25T08:16:39.213Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"b388b92939de5e82bd5289c486471413ec9415eb0c4903fa7df25d15af17eb01","record_sha256":"24e605988679293fc3d5ba477afd1aa6696ab9827943ca6f0f23c12c043d70cc"},
    {"record_id":"author-pr27-plan-005","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Exact snapshot keys do not detect credentials inside an allowed metadata field.","finding_evidence":"plan item 10 treats the exact entry key set as a credential guard. A mutant can copy the service bearer token into `entry.proof_metadata[\"authorization\"]` without changing either checked key set. It would pass the listed privacy checks. This is a reachable container: `app/models/timesheet_entry.rb` preserves arbitrary proof metadata keys in `normalized_proof_metadata`, and the agent controller accepts that hash. Add a concrete credential-leak mutant inside an allowed metadata field and define how the producer prevents that leak. Keep the exact-key check, but do not present it as sufficient proof that the payload contains no credential.","recorded_resolution":"fixed","recorded_correction":"The exact field check is no longer described as credential proof. A deep search for the known test tokens covers that, and a broken fake that puts the service token in proof_metadata must fail.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T08:14:21.432Z","answer_recorded_at":"2026-10-25T08:16:39.213Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d48a094086754ed9badee650d2ed6484a4400cc1d0631df93c38fe26a1be8538","record_sha256":"625c5c7374293b8e5b684e1fcdb22101a49e05210e2c112d7ea5ec040421cdb6"},
    {"record_id":"author-pr27-plan-006","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Give entry examples a reservation setup interface that does not require reservation endpoints.","finding_evidence":"The declared setup interface can create entries but cannot establish reservations. U4 nevertheless requires the entry group to check `locked: true` after a commit. If that group calls prepare and commit to arrange its state, a provider cannot run it before implementing the reservation API. This defeats the independence that makes these examples reusable by later lanes. Add a setup operation that establishes a reserved entry through each provider's fixture adapter. Keep prepare and commit behavior in the reservation group. The entry group can then check both values of `locked` without requiring another contract's endpoints.","recorded_resolution":"fixed","recorded_correction":"The setup object now has a hook that creates an entry that already holds a reservation. The entry examples use it to check both values of locked, so they never call the reservation endpoints.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T08:14:21.432Z","answer_recorded_at":"2026-10-25T08:16:39.213Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"80ed82c977161d22cf99ba38d3f7e32aa57ab8fac8834e63e28ba9f58beaba07","record_sha256":"bd973e5c2028f7bf4dffeb43dab267582d3fb2cf26eca9cb8614000f985234b4"},
    {"record_id":"author-pr27-plan-007","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep the event schema version in the transmitted snapshot contract.","finding_evidence":"plan item 10 moves `schema_version` into a YAML file and excludes it from transmitted snapshots. A consumer cannot use that file value to identify the schema of a queued event from an older producer. `docs/architecture/timekeeping-soa.md:407` requires every event to carry `schema_version` and supports parallel event versions during incompatible changes. The ticket requires the listed snapshot fields, but does not prohibit a schema version. Define its location in the transmitted envelope and include it in the producer and consumer examples. Keep the exact entry-field check separate so it still rejects personal and credential fields.","recorded_resolution":"fixed","recorded_correction":"schema_version now travels at the top of each snapshot batch, beside snapshots, and both the producer and endpoint examples check it. The exact field check on each entry is unchanged.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T08:14:21.432Z","answer_recorded_at":"2026-10-25T08:16:39.213Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f800654f7e2d6fd0a06bd12e6353dc3ec7a8b447e71ba422e3b2e096ac7f7d7f","record_sha256":"e73f09f610a2a2cb9706ca1f7e25f3834d179a853996087eedf0e61b4f5dad08"},
    {"record_id":"author-pr27-plan-008","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Reservation operation identifiers lack the required actor scope.","finding_evidence":"The plan scopes reservation operations only to company_id, although the ticket also requires separation by actor. Its examples test another company and another actor for entry keys, but only another company for reservation operation identifiers. Two actors in one company could therefore share a stored operation while the contract suite passes. Define actor separation for operation lookup and add a prepare example that reuses an operation_id under another actor in the same company. This prevents later providers from treating distinct actors' operations as the same request.","recorded_resolution":"fixed","recorded_correction":"Operation lookup now includes the calling actor. A prepare example reuses an operation id under another service actor in the same company and must not get the first actor's stored operation.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T08:14:21.432Z","answer_recorded_at":"2026-10-25T08:16:39.213Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"827e0b07fda28d5ea5dc4c672203269772fb09f438a566421f2cb4092926a4a5","record_sha256":"8f78867cde7c00d135df6cac2a7cb8500e79c6db496a6fc2106f2fc923bea2f9"},
    {"record_id":"author-pr27-plan-009","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Snapshot version checks do not satisfy the promised idempotency contract.","finding_evidence":"The ticket applies keyed replay rules to all three contracts. The plan instead makes snapshot retries depend only on version. Under this design, replaying a batch changes the answer from applied to ignored, and reusing its key with a changed body need not return 422. Later consumers could pass these examples without implementing the promised retry behavior. Distinguish replay of the same keyed request from delivery of an existing version in a new request. Add snapshot examples for the same answer on keyed replay and 422 on changed-body key reuse.","recorded_resolution":"fixed","recorded_correction":"Keyed replay and version checks are now separate. A replayed snapshot batch returns its first applied and ignored counts, and reusing its key with a changed body gets 422.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T08:14:21.432Z","answer_recorded_at":"2026-10-25T08:16:39.213Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"8a331e4b1a5d7afdf192fe2bfff969a2b65e8301e88c82208f5304344aff9487","record_sha256":"8c2a3144c82047112fb374bb067272ea43099ab0816972233a15f3afc50b2200"},
    {"record_id":"author-pr27-plan-010","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The failed-prepare scenario requires the conflicting entry to lose its existing lock.","finding_evidence":"The reservation unit tests a prepare containing one conflicting entry and free entries, then expects every entry GET to show `locked: false`. The conflicting entry must already have a reservation to produce `on_another_invoice` or `provisional_elsewhere`. Requirements R3 and R6 require that reservation to remain and its entry to stay locked. As written, the example would reject a correct provider or endorse releasing another operation's reservation. Assert that the free entries remain unlocked and that the conflicting entry retains its original reservation and lock.","recorded_resolution":"fixed","recorded_correction":"Same fix as for the other lock findings. The example now compares state before and after the call, and the conflicting entry must keep its original reservation and stay locked.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T08:14:21.432Z","answer_recorded_at":"2026-10-25T08:16:39.213Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"edb45c8cef6c2300615942282dc60754ed3c404adcca7a2c5d8604850a024d3d","record_sha256":"be574ee9ae3478b9bd4b36c3dbd114cd108ae2aa4d421f4d435cfb1f0c3ca9dc"},
    {"record_id":"author-pr27-plan-011","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The failed-prepare test must preserve the conflicting entry's existing lock.","finding_evidence":"The reservation unit proposes proving atomic prepare by checking that every requested entry reads `locked: false` after a conflict. That cannot pass against a correct provider when another invoice already reserves the conflicting entry. `docs/architecture/timekeeping-soa.md:588` requires both provisional and committed reservations to produce `locked: true`. This test would reject correct providers or encourage a fake that releases another invoice's reservation. Seed one reserved entry and several free entries. After the refused prepare, assert that the reserved entry remains locked and each previously free entry remains unlocked.","recorded_resolution":"fixed","recorded_correction":"The example now seeds one reserved entry and several free ones. After the refused prepare, the reserved entry stays locked and each free entry stays unlocked.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T08:14:21.432Z","answer_recorded_at":"2026-10-25T08:16:39.213Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"5affcae02b2da9b8865c261254a299b681176159daa705200765d118c5c4710a","record_sha256":"35a5e0f34e50345458d6c5b94daf28e98c78f473f8516f67069192ec8c7856c4"},
    {"record_id":"author-pr27-plan-012","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The idempotency decision contradicts the requirement it claims to implement.","finding_evidence":"Requirement R9 applies the same-answer, changed-body and actor-isolation rules across all three contracts. plan item 6 instead scopes reservation operations only by company and makes repeated snapshots return an ignored count rather than the original answer. It assigns the idempotency group only entry-create and prepare cases. Two implementers could follow R9 or plan item 6 and build incompatible contracts. Reconcile these rules explicitly, including replay responses, changed bodies and actor isolation for each contract. Keep the shared examples consistent with that decision and disclose any departure from the ticket.","recorded_resolution":"fixed","recorded_correction":"The idempotency decision now gives one rule set for all three contracts: same answer on replay, 422 on a changed body, and no match across company or actor. Where the snapshot endpoint differs, the plan says so.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T08:14:21.432Z","answer_recorded_at":"2026-10-25T08:16:39.213Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"7364ca3163d0a28d57846670f2b668f27df5f04b5d99b5f9480c0a7109306761","record_sha256":"fe8b38e18519444e7f3be3ded6274fb16605c967a0af31d7f75abfb2f36e4caa"},
    {"record_id":"author-pr27-plan-013","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The idempotency design drops required snapshot replay and actor-isolation guarantees.","finding_evidence":"plan item 6 replaces snapshot request idempotency with version deduplication and scopes reservation operation ids only by company. U3 tests entry receipts and cross-company prepare, but not snapshot receipts or operation replay by another actor. This narrows the rule that applies across all three contracts. For example, replaying an identical snapshot request would return ignored counts instead of its original applied counts. Add shared examples for same-key replay, changed-body rejection, and company and actor isolation on each contract. Keep version deduplication separate: a duplicate version under a new request key can be ignored. Otherwise later providers can pass these examples while breaking the promised retry behavior.","recorded_resolution":"fixed","recorded_correction":"The snapshot endpoint now takes an Idempotency-Key. Same key and body return the first answer, a changed body gets 422, and a new key carrying an old version is still counted as ignored.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T08:14:21.432Z","answer_recorded_at":"2026-10-25T08:16:39.213Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"80978c66c719e3d96ca8f8ffd2491a923447bd37524cae7a6621ca6cf013c0fc","record_sha256":"69de481794a3598a37db960255a034f1352971e12f27c6f173c6a23c9a4a8596"},
    {"record_id":"author-pr27-plan-014","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The reservation examples do not test the service-only authorization boundary.","finding_evidence":"The planned examples check foreign companies and stale generations, but never reject anonymous callers or user tokens. `docs/architecture/timekeeping-soa.md` section 5.3 requires service identity for these endpoints. Section 6.2 also binds background credentials to a company and named operations. Without these examples, a future provider could pass the shared group while allowing an employee to reserve entries, mark them billed, or detach their agent. Add shared examples for prepare, commit, abort, mark-billed and detach-entries that reject missing credentials, user tokens, and service credentials without the required company or operation permission. Assert that rejection changes nothing, and include a mutant that bypasses this check.","recorded_resolution":"fixed","recorded_correction":"Prepare, commit, abort, mark-billed and detach-entries now get examples that reject a missing credential, a user token and a service identity bound to another company or operation, with no change made.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T08:14:21.432Z","answer_recorded_at":"2026-10-25T08:16:39.213Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"fc1d5f3102cec8260298af129147aae6f80bf7401f6efc15216fe32ddea1868e","record_sha256":"f4a51feb174d05fe8e6da89b894c61e8775c138dbe6a8bcbbce60b288ea554df"},
    {"record_id":"author-pr27-plan-015","pr":27,"pr_url":"https://github.com/Homie-Improvement/miru/pull/27","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"high","finding_title":"The plan goes beyond the issue: Use scoped replay receipts across all contracts but scope snapshot receipts only by service identity rather than company.","finding_evidence":"The snapshot exception overrides the explicit company-isolation rule. With the same service identity and key, a batch for another company would encounter the first company's receipt and receive a replay or changed-body rejection; the absence of company_id in the URL does not remove company_id from the snapshot entries.","recorded_resolution":"fixed","recorded_correction":"Snapshot receipts are now scoped by the set of company ids in the batch, by the calling service and by the key. An example sends the same key and service with another company's batch and expects it to be judged fresh.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T08:18:01.868Z","answer_recorded_at":"2026-10-25T08:19:27.785Z","first_independent_review_at":"2026-10-25T09:39:40Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"020ef8bef621e28b54859eec8b082e305f4fc34caa9f4e7ac983b843b614f430","record_sha256":"f7d40f78bd5c19594a4c1873b73b6c1596ab88914ae682664e74b3e95022c0d3"},
    {"record_id":"author-pr30-build-001","pr":30,"pr_url":"https://github.com/Homie-Improvement/miru/pull/30","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Conditional secret defaults pass the committed-secret gate","finding_evidence":"A committed Ruby initializer containing ENV[\"TIMEKEEPING_SERVICE_TOKEN\"] ||= \"abc123\" passes the new secret scan, even though it installs a literal service credential whenever configuration is absent. That breaks R11 (the scan must fail on a literal value for TIMEKEEPING_SERVICE_TOKEN anywhere in the tree). Conditional ENV defaults are already a repository idiom (config/boot.rb:3), but the assignment matcher recognizes only =>, = and :. Adding ||= to the matcher closes this silent pass without changing runtime authentication.","recorded_resolution":"fixed","recorded_correction":"The secrets scan now also flags a quoted literal set with ||=. A new example checks that it is flagged and that ||= SecureRandom.hex(32) is not, and the spec passes.","correction_target":"code_commit","source_path":"spec/timekeeping/no_committed_secrets_spec.rb","source_line":27,"fix_commit":"2740c58bb5d6a539166448db81a3b0792d4aa0e2","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/2740c58bb5d6a539166448db81a3b0792d4aa0e2","finding_recorded_at":"2026-10-25T11:11:50.046Z","answer_recorded_at":"2026-10-25T11:28:15.736Z","first_independent_review_at":"2026-10-25T11:32:22Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"6eb924e0a48ad4cbf18445e535cb1f80edb71f4fdfcaa4d38703ec6a0903c509","record_sha256":"e50be4de27cbb66e3612c884b72034db8d5852bd8d6de10a627340351b08ca43"},
    {"record_id":"author-pr30-build-002","pr":30,"pr_url":"https://github.com/Homie-Improvement/miru/pull/30","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Duplicate rule: primary-role logic copied from User#primary_role","finding_evidence":"Timekeeping enforces a role in two places: the reference response and, through mint, every user token. That role now comes from a second copy of the rule the monolith uses for its own authorization. The 'employee' default and the 'lowest role id wins' tie-break are both written again, and only a comment ties them to User#primary_role. If User#primary_role changes (an explicit role priority, a new default), the monolith and Timekeeping will disagree about what a user may do, with no error, and no test compares the two. One batched source in the model, with primary_role delegating to it, removes the drift.","recorded_resolution":"fixed","recorded_correction":"The role rule now lives once in User.primary_roles_for, with User::DEFAULT_ROLE. User#primary_role and the reference facts both call it, and the copy in the facts builder is gone.","correction_target":"code_commit","source_path":"app/services/timekeeping/reference_facts.rb","source_line":57,"fix_commit":"2740c58bb5d6a539166448db81a3b0792d4aa0e2","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/2740c58bb5d6a539166448db81a3b0792d4aa0e2","finding_recorded_at":"2026-10-25T11:11:50.046Z","answer_recorded_at":"2026-10-25T11:28:15.736Z","first_independent_review_at":"2026-10-25T11:32:22Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"77d6c7dec43cd9ca839f2f1de3b88de36d6119010a5233b566abd4ff15893454","record_sha256":"724e938b6df04f433ea584bb3c9d88072c329326fbdcbd1bec58c9eb6b04557a"},
    {"record_id":"author-pr30-plan-001","pr":30,"pr_url":"https://github.com/Homie-Improvement/miru/pull/30","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Bind an agent token to the agent's own user.","finding_evidence":"plan item 2 checks an agent's company but not its user. Call `mint(user: admin, company: company, agent: employee_agent)` where both users belong to that company. The plan signs the admin's `sub` and `role` with the employee's `agent_id`. This contradicts the agent identity that Timekeeping must use for authorization and audit records. `docs/architecture/timekeeping-soa.md:530` requires the token to represent the agent's user. The current controller enforces that identity by assigning `timesheet_entry.user = current_agent.user` in `app/controllers/api/v1/agent/timesheet_entries_controller.rb:16`. Derive the user from the agent, or reject a different `user.id` before signing. Add a same-company, different-user refusal test.","recorded_resolution":"fixed","recorded_correction":"Minting with an agent now refuses unless the agent belongs to the same user and company, matching the check in app/models/timesheet_entry.rb; a spec covers a same-company agent of another user.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T10:16:27.681Z","answer_recorded_at":"2026-10-25T10:18:03.045Z","first_independent_review_at":"2026-10-25T11:32:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f0be508136b8de181a8396fa03064c38c7f80f91d74d686232047bdfcf76c861","record_sha256":"eb3110b7f7f9ed7971d3f6ebc101ac391057050675453c8c1160a00f504c02d9"},
    {"record_id":"author-pr30-plan-002","pr":30,"pr_url":"https://github.com/Homie-Improvement/miru/pull/30","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Do not let file size or encoding bypass the secrets check.","finding_evidence":"plan item 8 silently skips files of at least 1 MiB and files that are not valid UTF-8. A tracked deployment file padded beyond that limit can contain `TIMEKEEPING_SERVICE_TOKEN=abc123` and still pass. A private-key file with an invalid UTF-8 byte also escapes detection. These inputs defeat the promised check anywhere in the tree and leave future credential leaks undetected. Scan tracked file contents as bytes, using bounded reads if needed, rather than excluding files by size or encoding. Add tests with a generated secret beyond the size limit and a generated private key beside an invalid UTF-8 byte.","recorded_resolution":"fixed","recorded_correction":"Same fix: no file is skipped by size or encoding, and a padded sample past 1 MiB and an invalid-byte sample must both be flagged.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T10:16:27.681Z","answer_recorded_at":"2026-10-25T10:18:03.045Z","first_independent_review_at":"2026-10-25T11:32:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"2fce391ed4acd78cfec2e97e86d1a13660fab2e263e408e5f92ff325e6100654","record_sha256":"26f8e210f72f7dae01441f3aaa95c9c05af2effadd389d5a4be8cd3e8044200c"},
    {"record_id":"author-pr30-plan-003","pr":30,"pr_url":"https://github.com/Homie-Improvement/miru/pull/30","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The planned route points to the wrong controller.","finding_evidence":"The route in plan item 7 resolves to `Internal::V1::ReferencesController#show`, but U3 creates `Internal::V1::Companies::ReferencesController`. Rails resource nesting adds the company path and parameter, not a controller module. Every reference request would fail before the planned controller can authenticate it or return facts. The repository handles this explicitly in `config/routes/api.rb:179-180`, where nested company resources specify `controller: \"companies/...\"`. Add `controller: \"companies/references\"` to the reference resource, or an equivalent module scope. Verify that route recognition selects `internal/v1/companies/references#show`, rather than only checking that the URL appears in the route list.","recorded_resolution":"fixed","recorded_correction":"The reference route now names `controller: \"companies/references\"`, as the nested company routes in config/routes/api.rb do, and a routing spec checks it resolves to internal/v1/companies/references#show.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T10:16:27.681Z","answer_recorded_at":"2026-10-25T10:18:03.045Z","first_independent_review_at":"2026-10-25T11:32:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"5a4d959743193090a3bf821ee217ea484c3a93421c637b58a76ea7b0759f2877","record_sha256":"b55a25eaa745d0da1c5f3100c516b4c5b329ac888b13851ec2c5015f59a6f481"},
    {"record_id":"author-pr30-plan-004","pr":30,"pr_url":"https://github.com/Homie-Improvement/miru/pull/30","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The scan excludes files that the stated secret-checking requirement includes.","finding_evidence":"The requirement says the spec fails on credentials \"anywhere in the tree\", but the scan decision skips files at least 1 MiB and files that are not valid UTF-8. A tracked file in either group can contain a PEM private key or a literal service token without failing the check. This leaves future credential leaks undetected. Remove these exclusions, or scan the relevant byte sequences without decoding the whole file. Add samples that prove the check still detects secrets in both groups.","recorded_resolution":"fixed","recorded_correction":"The secrets scan now reads every tracked file as raw bytes with no size or encoding exemption, and samples past 1 MiB and beside an invalid UTF-8 byte prove it still flags them.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T10:16:27.681Z","answer_recorded_at":"2026-10-25T10:18:03.045Z","first_independent_review_at":"2026-10-25T11:32:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"1e6c223dd326259f3897d9344af8668c501487d812a848b446df127c88b7bc9d","record_sha256":"91fe03b42785a1ee7a91bdff46a0b5884dbe00f73cd1fac6a71b2d8dca1ba273"},
    {"record_id":"author-pr30-plan-005","pr":30,"pr_url":"https://github.com/Homie-Improvement/miru/pull/30","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The secrets check must not silently exclude tracked files.","finding_evidence":"plan item 8 skips files at least 1 MiB and files that are not valid UTF-8. A private key appended to a large fixture or a file containing an invalid byte will therefore pass the check. This weakens the promised regression guard, even though no current credential leak is established. Scan tracked files as bytes, using bounded reads when needed, rather than treating size or encoding as an exemption. Add samples that place a generated private key after the size threshold and beside an invalid UTF-8 byte.","recorded_resolution":"fixed","recorded_correction":"Same fix: the scan treats size and encoding as no exemption, and the spec plants a generated key past the size threshold and beside an invalid byte.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T10:16:27.681Z","answer_recorded_at":"2026-10-25T10:18:03.045Z","first_independent_review_at":"2026-10-25T11:32:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"84db72dcb7df7d7dafe8992f8a1c3e6b54f695068c075e6f666d4779f9fe3ed5","record_sha256":"c6de4b9a93d3c4ffbb971f05e99d5dd0343dd5d75e5094e945707d68cf848bf8"},
    {"record_id":"author-pr30-plan-006","pr":30,"pr_url":"https://github.com/Homie-Improvement/miru/pull/30","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The secrets scan excludes files the ticket requires it to check.","finding_evidence":"The ticket requires the spec to detect secrets \"anywhere in the tree\". plan item 8 silently skips tracked files at least 1 MiB in size and files that are not valid UTF-8. A PEM private key or literal service token in either file class would pass the check. These exclusions narrow the promised regression guard and can hide future credential leaks. Scan every tracked file for the required byte patterns, using bounded reads if needed. Add samples with a secret beyond 1 MiB and a secret beside invalid UTF-8 bytes to prove neither exclusion remains.","recorded_resolution":"fixed","recorded_correction":"Same fix as the other scan findings: every tracked file is scanned as bytes, and the two bypass samples are part of the spec.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T10:16:27.681Z","answer_recorded_at":"2026-10-25T10:18:03.045Z","first_independent_review_at":"2026-10-25T11:32:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"3f42a99af3b3bab60fb90c74986bc976a9b604a84b1589341c55b5e9d46dbb1f","record_sha256":"24c5040987085d98e1f59415f26c3552a875cbb54ce1b95ca091936162b36e81"},
    {"record_id":"author-pr30-plan-007","pr":30,"pr_url":"https://github.com/Homie-Improvement/miru/pull/30","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The token logging test must enable normal request logging.","finding_evidence":"The planned test captures `Rails.logger`, but `config/environments/test.rb:14-15` sets its underlying logger to ERROR. Lines 34-35 also set the Rails log level to ERROR. Production defaults to INFO in `config/environments/production.rb:52`. Unless the test changes both thresholds, an accidental INFO-level token log can remain invisible and the test will pass. Install an INFO-enabled capture logger for these examples and restore it afterward. Prove that the assertion catches an intentionally emitted token at that level.","recorded_resolution":"fixed","recorded_correction":"The logging spec now installs a DEBUG-level capture logger for Rails and Action Controller and restores it after, and a control example proves an INFO line holding a token is caught.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T10:16:27.681Z","answer_recorded_at":"2026-10-25T10:18:03.045Z","first_independent_review_at":"2026-10-25T11:32:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"85ebd6dfb0be1531ef56db6bc5019b85911fd9d013c2054632a8b56039083815","record_sha256":"2a073be81b5836b3b1cb5d02046b0e99352aa2524b639c86d3c2354f341f0d5a"},
    {"record_id":"author-pr30-plan-008","pr":30,"pr_url":"https://github.com/Homie-Improvement/miru/pull/30","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The verification signature exposes a clock that the decoding decision does not use.","finding_evidence":"The declared `verify(token, public_key:, now: Time.current)` signature accepts a verification time. The decoding decision enables expiration checks but never uses `now`. One implementer could compare expiry against that argument; another could leave the comparison to the library's current clock. The same token could then pass or fail for the same declared inputs. Either remove `now:` from verification and use the clock advanced by `travel`, or state how verification uses it and add a test with an explicit time.","recorded_resolution":"fixed","recorded_correction":"Verification no longer takes a clock argument; expiry is checked by the library against the current time, and the expiry spec moves that clock with `travel`.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T10:16:27.681Z","answer_recorded_at":"2026-10-25T10:18:03.045Z","first_independent_review_at":"2026-10-25T11:32:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"71af23084b9efab324a9996dc5a2d3be1a09e77643f1e5150dff8f8634785f7b","record_sha256":"7b704fa22d0a30b1e7141d5394ddbf90a562279403d1768b16994aafa6521a20"},
    {"record_id":"author-pr30-plan-009","pr":30,"pr_url":"https://github.com/Homie-Improvement/miru/pull/30","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Token minting must bind an agent to its own user.","finding_evidence":"plan item 2 checks an agent's company but does not check `agent.user_id == user.id`. Two users in the same company therefore pass the planned checks even when the agent belongs to the other user. The resulting signed token certifies an inconsistent actor identity. Today's model rejects this combination in `app/models/timesheet_entry.rb:170-177`, and the agent controller derives the user from the authenticated agent. Require the same relationship before signing. Add tests for an unrelated user in the same company and for the correct agent-user pair.","recorded_resolution":"fixed","recorded_correction":"Minting now requires the agent's user to be the acting user before it signs, with specs for an unrelated same-company user and for the matching pair.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T10:16:27.681Z","answer_recorded_at":"2026-10-25T10:18:03.045Z","first_independent_review_at":"2026-10-25T11:32:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"8b93c88bf5cd5b690662cfdc8fae675133d9223d37db2d2e4854b5750e5e319a","record_sha256":"3c93e1070fd4e253bff52d3e989ab3972f62b6c415495a7d15c717e89959576f"},
    {"record_id":"author-pr30-plan-010","pr":30,"pr_url":"https://github.com/Homie-Improvement/miru/pull/30","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"medium","finding_title":"The plan goes beyond the issue: Implement the authenticated company reference endpoint and its routing and request tests.","finding_evidence":"The endpoint and its tests are required, but the unit additionally changes every unknown path in the internal API namespace to an authenticated JSON response. That namespace-wide behavior is beyond the named endpoint; remove that addition while retaining the endpoint and its route-recognition test.","recorded_resolution":"fixed","recorded_correction":"Removed the namespace-wide fallback and the change to the app's catch-all route; the endpoint, its request specs and its route-recognition spec stay.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T10:19:00.343Z","answer_recorded_at":"2026-10-25T10:20:32.180Z","first_independent_review_at":"2026-10-25T11:32:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"4d0c45914140f44cd84ba181ef5e943cc9e6cbc802e2899af57de3edd404c1ca","record_sha256":"1cf8cfb6a9198704d5f8380c8e7d68e370cc0faef74cce777fc8886feafa8619"},
    {"record_id":"author-pr30-plan-011","pr":30,"pr_url":"https://github.com/Homie-Improvement/miru/pull/30","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"medium","finding_title":"The plan goes beyond the issue: Route the reference endpoint to its company controller and add an authenticated fallback for unknown internal paths.","finding_evidence":"Explicit controller routing and preserving existing routes are in scope, including the reviewed routing fix. Adding a wildcard that changes responses and authentication for unrelated unknown internal paths extends the change beyond the endpoint the issue names.","recorded_resolution":"fixed","recorded_correction":"The catch-all for unknown internal paths and its test are removed; only the named reference route is added, with its explicit companies controller and route-recognition test.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T10:19:00.343Z","answer_recorded_at":"2026-10-25T10:20:32.180Z","first_independent_review_at":"2026-10-25T11:32:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"4939b159bd4bdb1ef39b85f809816562f6ebae8a2c82bb74850ace8faa6945c8","record_sha256":"af1e2ae801024fd6394880776183c633579c8d6acec781503b82248bd1c92473"},
    {"record_id":"author-pr31-build-001","pr":31,"pr_url":"https://github.com/Homie-Improvement/miru/pull/31","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"X1 header claims destroy unlocks ids it does not","finding_evidence":"This file documents the read paths the Timekeeping extraction must keep. Its header calls the X1 ids \"the entry ids a draft line item unlocks on destroy\", but the example computes them with `associated_timesheet_entry_ids` while the line still exists. On a freshly loaded aggregate line, `has_many :invoice_line_item_time_entries, dependent: :destroy` (invoice_line_item.rb:6) runs before `before_destroy :unlock_timesheet_entries` (line 10). So destroy unlocks only the direct entry, which the sibling L4 example pins (invoice_entry_locking_spec.rb:103). Someone doing the extraction who trusts this header would treat X1 as the unlock set, and that contradicts the baseline in the next file.","recorded_resolution":"fixed","recorded_correction":"The header now says the lookup is read while the line still exists. On a freshly loaded aggregate line, destroy unlocks only the direct entry. The characterization specs still pass.","correction_target":"code_commit","source_path":"spec/characterization/entry_lookups_and_pdf_totals_spec.rb","source_line":6,"fix_commit":"19b099611eb9fc13c9a3d1386c464b34c5dc1626","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/19b099611eb9fc13c9a3d1386c464b34c5dc1626","finding_recorded_at":"2026-10-25T12:30:38.712Z","answer_recorded_at":"2026-10-25T12:43:19.104Z","first_independent_review_at":"2026-10-25T12:46:23Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"17625ec7b9d2ec3247e0b4480e183d96573589dbca2e8644ca1e760cef98a456","record_sha256":"796f3d36c716b6c49135d46062688e34892d704da991af6e1c70ed52b7c56808"},
    {"record_id":"author-pr31-plan-001","pr":31,"pr_url":"https://github.com/Homie-Improvement/miru/pull/31","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Characterize agent ownership without adding a request-rejection requirement.","finding_evidence":"The entry-rules unit says an agent request targeting another user must be rejected. The ticket requires the saved entry to belong to the agent's user, not rejection of that request. In app/controllers/api/v1/agent/timesheet_entries_controller.rb, agent_timesheet_entry_params excludes user_id, and create assigns current_agent.user. A supplied user_id therefore does not produce the planned rejection. Pin that the saved entry still belongs to the agent's user. If the rejection example is meant to pin the model validation instead, declare a model example that supplies a mismatched user directly. app/models/timesheet_entry.rb implements that rejection in validate_agent_belongs_to_workspace. This distinction keeps the baseline faithful to main and prevents the extraction from treating a new request-rejection rule as existing behavior.","recorded_resolution":"fixed","recorded_correction":"The agent request case now pins that a supplied user_id is ignored and the saved entry belongs to the agent's user. A separate model example pins the mismatched-user validation.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T11:59:52.372Z","answer_recorded_at":"2026-10-25T12:00:48.652Z","first_independent_review_at":"2026-10-25T12:46:23Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"0e9ca1a21566507bbda24602202b4a6918f2edbac000c182d07a2ff24632306b","record_sha256":"16e8cf7a5d220ab437c6b7eb711d0d48a8f40f3a490fb8577920868078a2497b"},
    {"record_id":"author-pr31-plan-002","pr":31,"pr_url":"https://github.com/Homie-Improvement/miru/pull/31","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The agent endpoint ignores a supplied user ID rather than rejecting it.","finding_evidence":"The entry-rules unit expects an agent request targeting another user to be rejected. Send a valid agent token, an allowed project, and another user's `user_id`: the controller excludes `user_id` from permitted parameters, assigns `current_agent.user`, and returns 200. See app/controllers/api/v1/agent/timesheet_entries_controller.rb:17-23 and 34-45. The planned rejection assertion would fail on unchanged main and misstate the ownership rule that future work must preserve. Assert that the request succeeds and the reloaded entry belongs to the authenticated agent's user, not the supplied user. Keep rejection of another company's project as a separate case.","recorded_resolution":"fixed","recorded_correction":"Confirmed in the agent controller: user_id is not permitted and the user is always the agent's. The spec now expects 200 with the entry owned by the agent's user.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T11:59:52.372Z","answer_recorded_at":"2026-10-25T12:00:48.652Z","first_independent_review_at":"2026-10-25T12:46:23Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"edbc31d697aee0b6b0a06240f12392e9936764dc840a6025410bf9048270e4ae","record_sha256":"a05df5a67cb725e57b14c447f0ee3bf5b49c07c221640ad3dd2fd5baeebd3c19"},
    {"record_id":"author-pr31-plan-003","pr":31,"pr_url":"https://github.com/Homie-Improvement/miru/pull/31","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The agent request cannot reject a supplied user ID as planned.","finding_evidence":"The entry-rules unit expects an agent request targeting another user to be rejected. However, app/controllers/api/v1/agent/timesheet_entries_controller.rb:17 always assigns current_agent.user, and agent_timesheet_entry_params does not permit user_id. With an otherwise valid request, the controller ignores the supplied user ID and creates an entry for the agent's user. The planned rejection assertion cannot pass on unchanged main. Assert that the request cannot change the entry's owner instead. If the spec must also pin rejection of a mismatched agent and user, exercise validate_agent_belongs_to_workspace in a model example. Keep the foreign-company project rejection as a separate request case.","recorded_resolution":"fixed","recorded_correction":"The request example now asserts that the owner cannot be overridden. The mismatched agent and user rejection moved to a model example on validate_agent_belongs_to_workspace.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T11:59:52.372Z","answer_recorded_at":"2026-10-25T12:00:48.652Z","first_independent_review_at":"2026-10-25T12:46:23Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"75a007e259e42e732a7dc4d0fbbd80857201159c311c198cbef88dd6d74aebd5","record_sha256":"c4c4bbdd964181d2d5c10c3aad8fe949f9f759033f2c6db8f04b37b0ec525a18"},
    {"record_id":"author-pr31-plan-004","pr":31,"pr_url":"https://github.com/Homie-Improvement/miru/pull/31","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The agent request must pin ignored user overrides, not request rejection.","finding_evidence":"The entry-rules unit says an agent request targeting another user is rejected. However, app/controllers/api/v1/agent/timesheet_entries_controller.rb:17 assigns current_agent.user, and agent_timesheet_entry_params does not permit user_id. With a valid project and entry data, a supplied user_id is ignored; it does not cause rejection. Pin that successful request and reload the entry to prove that the authenticated agent still owns it. Keep rejection for a project outside the agent's company or membership scope. Otherwise the suite cannot pass unchanged and misstates how the application prevents user impersonation during the later extraction.","recorded_resolution":"fixed","recorded_correction":"The agent request now pins the ignored override, reloading the entry to show the agent's user owns it. Rejection is kept only for a project outside the agent's company.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T11:59:52.372Z","answer_recorded_at":"2026-10-25T12:00:48.652Z","first_independent_review_at":"2026-10-25T12:46:23Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"5ab4917785170820558962a9919f077c7837d2d296550cb3f6e8228388235b8d","record_sha256":"fab851c249bab11149edfa063e9cc3e378bbae56cd7df26667e90eb0b42253e1"},
    {"record_id":"author-pr31-plan-005","pr":31,"pr_url":"https://github.com/Homie-Improvement/miru/pull/31","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The draft re-save case has two conflicting rules for its expected result.","finding_evidence":"plan item 5 calls the draft re-lock uncertain, assigns it to R7, and says to pin whatever value main produces. plan item 7 instead requires R7 to finish with `locked: true`, as does the invoice-locking implementation unit. plan item 5 also attributes the uncertainty to `after_destroy`, while R7 edits a line and triggers `after_save`. An implementer could therefore accept `locked: false` as the baseline or treat it as evidence that the test missed the intended re-save. Confine the observed-value rule to the uncertain destroy cases. Keep the draft-edit case separate, with its amount-change assertion and explicit locked-state expectation.","recorded_resolution":"fixed","recorded_correction":"The rule to pin whatever main produces now covers only the uncertain destroy cases. The draft line edit stays separate: it must change the amount and end with entries locked true.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T11:59:52.372Z","answer_recorded_at":"2026-10-25T12:00:48.652Z","first_independent_review_at":"2026-10-25T12:46:23Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"01c8d3e5a09c64253443c22ff40618d39593f4b88e3493026ee8ddbab6e4d28a","record_sha256":"131d1e32d651a33349fac4b669483614946de113456cf517a393c6e39159764f"},
    {"record_id":"author-pr36-plan-001","pr":36,"pr_url":"https://github.com/Homie-Improvement/miru/pull/36","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Background exemptions also permit deletion of billed entries.","finding_evidence":"plan item 4 makes every named background caller bypass A2, including for deletion. For example, change_decision with caller: \"agents.detach_entries\", change: { action: :delete }, and a billed entry returns :ok. bulk_decision likewise returns :apply for that entry. A consumer trusting these decisions could discard billed records that should remain protected. app/policies/timesheet_entry_policy.rb rejects billed records in both update? and destroy?. docs/architecture/timekeeping-soa.md section 6.2 grants background callers only named operations; none authorizes entry deletion. Keep billed-entry refusal for single and bulk deletion, and test both with background callers. The required A7 background example concerns changing bill status, so this restriction does not prevent that example.","recorded_resolution":"fixed","recorded_correction":"A background delete of a billed entry, for example by agents.detach_entries, is now refused by both decision functions, and the plan adds a test for each.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T13:43:54.355Z","answer_recorded_at":"2026-10-25T13:45:12.830Z","first_independent_review_at":"2026-10-25T14:11:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"478a080ff865a9928e92fc4ab4b58604aca8563f80d5b56f584bf3b28b3c159f","record_sha256":"90ce3ab8883d89df32b943dbf0b1139d29ee03896669b556481e3b222ebd5c6f"},
    {"record_id":"author-pr36-plan-002","pr":36,"pr_url":"https://github.com/Homie-Improvement/miru/pull/36","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Do not grant background callers permission to delete billed entries.","finding_evidence":"plan item 4 disables A2 for every background operation, including deletes. With caller: \"entries.import\", change: { action: :delete }, and a billed entry, change_decision returns :ok; bulk_decision likewise returns :apply. A service caller can therefore receive permission to remove billed records through an operation intended only for imports. docs/architecture/timekeeping-soa.md:530 grants permission for named operations, not unrestricted entry changes. Its section 7.3 also preserves billed-entry deletion refusal. The required background exception for A7 concerns status changes and does not require allowing deletes. Keep billed deletes forbidden in both decision functions, including for background callers, and add regression examples. This preserves the required background update examples without widening delete permission.","recorded_resolution":"fixed","recorded_correction":"A billed entry is now refused for every caller, background included, in both the single and the bulk decision. Regression examples cover a background delete of a billed entry.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T13:43:54.355Z","answer_recorded_at":"2026-10-25T13:45:12.830Z","first_independent_review_at":"2026-10-25T14:11:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"fb71e4490f3cf2f9228597d1f11debb823bd04524f2b18ede30f4702031d3671","record_sha256":"45ea11a48e546232541067a9f24ec5e2e170e9bf3745b49cb382f21721758105"},
    {"record_id":"author-pr36-plan-003","pr":36,"pr_url":"https://github.com/Homie-Improvement/miru/pull/36","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The background exception also permits billed deletes and bulk moves that the ticket requires refusing.","finding_evidence":"plan item 4 exempts background callers from A2–A4 and returns :ok or :apply for every entry. This exceeds the requested A6/A7 skip. For example, caller: \"entries.import\" with action: :delete and a billed entry is allowed, as is a bulk move containing a billed entry. The A7 background example needs neither permission. Such results could let a future caller remove or move invoiced work instead of receiving the required refusal. TimesheetEntryPolicy#update? rejects billed entries, and BulkActionController#entry_locked? rejects them before either bulk action. Preserve billed-delete and billed-bulk refusal, and limit any exception needed for the A7 background example to that case. Add background-caller tests for these refusals.","recorded_resolution":"fixed","recorded_correction":"Background callers now stay bound by the billed refusal for update, delete and bulk. They skip only the locked and away-from-billed checks, and tests pin billed delete and billed bulk as refused.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T13:43:54.355Z","answer_recorded_at":"2026-10-25T13:45:12.830Z","first_independent_review_at":"2026-10-25T14:11:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f680baf422ccb3145c4d728a5bcdc2d9246951c5b36b132968175d74067bb049","record_sha256":"cff34448d858c9db9512d7a09dc956d051333736ca3355e3ea7474f9d6387053"},
    {"record_id":"author-pr36-plan-004","pr":36,"pr_url":"https://github.com/Homie-Improvement/miru/pull/36","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The background exemption contradicts the billed-entry guarantees.","finding_evidence":"Requirements R9 and R16 require refusal of billed-entry changes and bulk requests containing billed entries. However, plan item 4 promises background callers :ok and :apply even for billed entries, and U2 tests that exemption. A background bulk delete would therefore authorize deletion of billed records despite the stated guarantee. The rationale also conflicts with plan item 2: refusing_rules can prove that background callers skip A7 while A2 still refuses the overall change. Keep the A7 exemption distinct from the final decision, and reconcile plan item 4 and its tests with the billed-entry guarantees.","recorded_resolution":"fixed","recorded_correction":"The background reading is rewritten so the billed guarantees hold for every caller. The A7 skip is proved by the rule list, not by an overall approval, and the tests changed to match.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T13:43:54.355Z","answer_recorded_at":"2026-10-25T13:45:12.830Z","first_independent_review_at":"2026-10-25T14:11:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"2621de53cafbb71552ed4e03dd0c87bccbbf522bb9b858055b025b314462614e","record_sha256":"6ec1e7e84b2528db5177103fff112b04d01103b8565514b5b496c8da3c979a61"},
    {"record_id":"author-pr36-plan-005","pr":36,"pr_url":"https://github.com/Homie-Improvement/miru/pull/36","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The background exemption permits deletion of billed entries.","finding_evidence":"plan item 4 returns :ok and per-entry :apply for every named background caller, even when deleting billed entries. A background bulk delete would therefore receive permission to erase time records that support an invoice. The ticket grants a background skip for A6 and A7, but also requires whole-request refusal for billed entries. Preserve that refusal in bulk decisions and the A2 refusal for deletion rather than exempting background callers from both. The A7 background example does not require removing these protections: the planned refusing_rules function can show that A7 was skipped while A2 still refuses the change. Update the background tests so they do not require unconditional approval of billed entries.","recorded_resolution":"fixed","recorded_correction":"The background skip now covers only the locked and away-from-billed checks. The billed refusal still applies, and the A7 skip is shown through refusing_rules, which lists the billed check but not A7.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T13:43:54.355Z","answer_recorded_at":"2026-10-25T13:45:12.830Z","first_independent_review_at":"2026-10-25T14:11:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"e94f5941325303d9fa387f00b100697e35810c8f2987d56e808e85d2c8d4a3d8","record_sha256":"5978af74a4c2237ca682757294e8fa37ba33b91f093cda59536af4a1aef7760f"},
    {"record_id":"author-pr36-plan-006","pr":36,"pr_url":"https://github.com/Homie-Improvement/miru/pull/36","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The rule-removal requirement cannot verify permissive rules as written.","finding_evidence":"The completion criteria require every refused example to fail when its own rule is removed. However, plan item 6 implements A11 by having no guard against setting billed status. U2's refused A11 example rejects another user's entry through A4, so removing A11 cannot make that example fail. A10 likewise grants an exception rather than causing a refusal. Distinguish refusal rules from permissions in the verification criteria. Test refusal rules by removing their checks, and test permissions by removing their exceptions or introducing the forbidden restriction.","recorded_resolution":"fixed","recorded_correction":"The plan now separates refusal rules (A2, A3, A4, A6, A7), proved by deleting their check, from the A10 and A11 permissions, proved by adding the forbidden guard and watching the allowed example fail.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T13:43:54.355Z","answer_recorded_at":"2026-10-25T13:45:12.830Z","first_independent_review_at":"2026-10-25T14:11:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d7cd05e212cb201fad952fe105d55660cfd60936ed95e5affb705442bbca7bb3","record_sha256":"a54e86d02c7397d572678597b3e8c5a512bdbc8d624e62753030955ff27427e6"},
    {"record_id":"author-pr38-build-001","pr":38,"pr_url":"https://github.com/Homie-Improvement/miru/pull/38","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Locked EXISTS subquery cannot use reservations' partial indexes","finding_evidence":"Every show and list request works out locked by probing reservations on (company_id, entry_id) with no state predicate. PostgreSQL uses a partial index only when the query implies its predicate, so both partial indexes are skipped and every read scans the whole reservations table. Committed and retained rows stay for every invoiced entry, so read time grows with the billing history of all tenants, not with page size.","recorded_resolution":"fixed","recorded_correction":"I added a plain index on reservations (company_id, entry_id) in the first migration and in schema.rb. The locked lookup and the foreign key's referencing side can now use it. The partial unique indexes stay as the uniqueness rule.","correction_target":"code_commit","source_path":"timekeeping/app/queries/timekeeping/entries_query.rb","source_line":22,"fix_commit":"ed98f3e10015c3b2cfa9e245c185261cad650c7e","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ed98f3e10015c3b2cfa9e245c185261cad650c7e","finding_recorded_at":"2026-10-25T15:18:51.335Z","answer_recorded_at":"2026-10-25T15:34:10.220Z","first_independent_review_at":"2026-10-25T15:38:41Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"31bfdcbf08bf72e47c8b70305659b3e48e7b70b955bc9ca8fd47b7fb00d50629","record_sha256":"986803a7e7592d8ccf66920f01c1430e6599ac126fd1102987206d5f23bda658"},
    {"record_id":"author-pr38-build-002","pr":38,"pr_url":"https://github.com/Homie-Improvement/miru/pull/38","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"Token in a URL path segment is logged in clear","finding_evidence":"The intent and R12 say no log line ever carries a token. A token sent as a path segment, as in GET /v1/companies/<service-token>/entries or /v1/companies/1/entries/<token>, is written in clear on the INFO 'Started GET' line before the company guard returns 404. The cause is that Rails::Rack::Logger logs request.filtered_path, which filters only the query string, never the path. A JWT contains dots, so it also fails routing and is logged again on the production 'No route matches' line. The comment at line 5, and plan plan item 9, both assume the whole start line is masked. The logging spec plants tokens only in query values and X-Request-Id, so CI stays green while the long-lived service credential reaches every log reader.","recorded_resolution":"fixed","recorded_correction":"The request start line now logs a path in which every segment that is not a route word, id or date is masked. Routing-error lines are no longer logged. Specs plant the service token and a JWT in path segments.","correction_target":"code_commit","source_path":"timekeeping/config/initializers/filter_parameter_logging.rb","source_line":5,"fix_commit":"ed98f3e10015c3b2cfa9e245c185261cad650c7e","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ed98f3e10015c3b2cfa9e245c185261cad650c7e","finding_recorded_at":"2026-10-25T15:18:51.335Z","answer_recorded_at":"2026-10-25T15:34:10.220Z","first_independent_review_at":"2026-10-25T15:38:41Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"93dc57afafe3a2815e45d77e15139df1087ca4d86d090cb0c64f95ebd80b161c","record_sha256":"17103300725b87c2c1ec40b6cc746f6978c4be87745f020160943935ba166087"},
    {"record_id":"author-pr38-plan-001","pr":38,"pr_url":"https://github.com/Homie-Improvement/miru/pull/38","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Allowed parameter names still let credentials reach the log.","finding_evidence":"plan item 9 filters by parameter name alone. Send GET /v1/companies/1/entries?project_id=<service-token>, using the configured service token as the value. Because project_id is allowed, both the filtered request path and controller parameters retain the credential. plan item 6 returns an empty list for this invalid integer, but that happens after request logging. The service credential remains usable by anyone who obtains the log until it is rotated. Preserve clear logging for valid identifiers and dates, but mask invalid values before the request logger runs. Add a DEBUG capture test with the generated service token in project_id. The existing note and proof_url tests do not reach this case.","recorded_resolution":"fixed","recorded_correction":"A project_id that is not a plain integer is now masked by the parameter filter itself, so the request start line never shows it. A capture test sends the service token as project_id.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T14:41:14.054Z","answer_recorded_at":"2026-10-25T14:43:12.230Z","first_independent_review_at":"2026-10-25T15:38:41Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"669009b4ea1143788eb67becbeaf0f8c8f5e69fe4f2c5ff77e6265ece2a483f0","record_sha256":"0b71a226e8a2c26c2ded1789ae9cfada541920e58c932b9ff57c2f83289e6b48"},
    {"record_id":"author-pr38-plan-002","pr":38,"pr_url":"https://github.com/Homie-Improvement/miru/pull/38","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The logging design still exposes credentials placed in allowed values or request tags.","finding_evidence":"The key allow-list in plan item 9 does not prevent a bearer token in `project_id` from reaching request logs. plan item 4 also logs `X-Request-Id` after character cleaning, which does not remove credentials. For example, the hexadecimal service token generated in `spec/support/timekeeping/identity_helpers.rb` remains unchanged when used as a request id. These paths copy a usable credential into logs, where readers could replay it until expiry or rotation. Input checks in the controller run too late to protect the request-start line. Add a decision that prevents credentials from reaching these log values before request logging begins. Extend the DEBUG capture tests to cover a bearer token in `project_id` and a service token in `X-Request-Id`, while keeping ordinary ids visible.","recorded_resolution":"fixed","recorded_correction":"Allowed parameters are now shown in clear only when the value has the expected id or date shape. A request id is shown in clear only as a UUID, and as a short digest otherwise. Both checks run inside the logger's own filter and tag code.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T14:41:14.054Z","answer_recorded_at":"2026-10-25T14:43:12.230Z","first_independent_review_at":"2026-10-25T15:38:41Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9a194820877a8364b32d69f6438936e76dacd80f0a8e52b04d0ab5124a1a4aaf","record_sha256":"5eae1219a4464bd775ed03cea383bff7ad6a646be7094e220f4fe9d6d0b4e2cb"},
    {"record_id":"author-pr38-plan-003","pr":38,"pr_url":"https://github.com/Homie-Improvement/miru/pull/38","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The logging unit requires a controller and routes that the next unit creates.","finding_evidence":"The logging unit must pass request specs and a control example that calls Timekeeping::V1::EntriesController.logger. The entry-read unit creates that controller and both routes, but depends on the logging unit. The existing example in spec/requests/internal/v1/companies/references_spec.rb also requires its concrete controller for the control check. The logging unit therefore cannot pass its stated verification in the declared order. Keep middleware checks in the logging unit, and move the route-dependent logging specs and their verification into the entry-read unit.","recorded_resolution":"fixed","recorded_correction":"I moved the route-dependent logging specs and the controller-logger control example into the entry-read unit, which creates the controller and routes.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T14:41:14.054Z","answer_recorded_at":"2026-10-25T14:43:12.230Z","first_independent_review_at":"2026-10-25T15:38:41Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d4f863046ca236062b835b532e25d107a2ef2d11e2b58cdfc61787f174a4064d","record_sha256":"c926dcb7f55baefc1c25aafe8f1b6c6914058dad517954e04cde4d871b17d2b1"},
    {"record_id":"author-pr38-plan-004","pr":38,"pr_url":"https://github.com/Homie-Improvement/miru/pull/38","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The logging unit requires routes and a controller that the next unit creates.","finding_evidence":"The logging unit must pass request specs and capture Timekeeping::V1::EntriesController.logger. The next unit creates that controller and both routes, but depends on the logging unit. The stated sequence therefore cannot complete the logging unit's verification independently. Move the route-dependent logging specs and their verification into the entry-read unit, or create the required controller and routes earlier.","recorded_resolution":"fixed","recorded_correction":"The request-level logging spec and its control example now sit in the entry-read unit. The logging unit keeps only middleware and filter checks that need no route.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T14:41:14.054Z","answer_recorded_at":"2026-10-25T14:43:12.230Z","first_independent_review_at":"2026-10-25T15:38:41Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"539cfdcb6747c3fbc0ca986bb13f7552e19d8495888bfccc2b07a2bf92879d0e","record_sha256":"9d77a760882e3140193a6df938bc009e840c22ed182e2e5ea62d03c6da778cc6"},
    {"record_id":"author-pr38-plan-005","pr":38,"pr_url":"https://github.com/Homie-Improvement/miru/pull/38","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The schema test excludes the additional version column that the migration must create.","finding_evidence":"The migration unit says entries columns must equal the snapshot entry_fields. The schema requirement separately requires those fields plus version. A literal equality assertion would reject the required schema. State that the expected columns include version alongside the snapshot fields, and use that expectation in the schema test.","recorded_resolution":"fixed","recorded_correction":"The snapshot entry_fields list in the contract already includes version. The plan now says the expected columns are that list, with version checked by name.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T14:41:14.054Z","answer_recorded_at":"2026-10-25T14:43:12.230Z","first_independent_review_at":"2026-10-25T15:38:41Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"aeedcd30639b6f15a432a4e7c022bcc7989da460a0e97f480f74d2acf7e06ae2","record_sha256":"4d9420c2e1e63ce0a4f39812fedafc9b5bb1fe14a2589863d310a8b3c7e4d2d1"},
    {"record_id":"author-pr40-build-001","pr":40,"pr_url":"https://github.com/Homie-Improvement/miru/pull/40","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"Credential scan grows quadratically on repeated eyJ in one body string (CWE-400)","finding_evidence":"Any signed-in user, including an employee, can exhaust a worker's memory and CPU with one write body, and every tenant on that process is hit. CredentialScan.found? runs before any field check and before the reference read. token_candidates builds head[start..] for every \"eyJ\" offset and keeps every candidate in one Set before it verifies any, so a 150 KB string makes about 50k candidates of up to 150 KB each. Each candidate then gets a JWT parse and a PEM re-parse. Cost grows with the square of the segment length, and the app sets no request-body size limit.","recorded_resolution":"fixed","recorded_correction":"Header starts are now looked for only in the last 512 characters of a segment, candidates are checked one at a time, and the key is parsed once. A 200 KB eyJ run now scans quickly, and took about 60 seconds without the bound.","correction_target":"code_commit","source_path":"timekeeping/app/services/timekeeping/credential_scan.rb","source_line":65,"fix_commit":"068ffa88d20426c0f2ca30b6a39ff4de17fbf839","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/068ffa88d20426c0f2ca30b6a39ff4de17fbf839","finding_recorded_at":"2026-10-25T17:41:25.839Z","answer_recorded_at":"2026-10-25T17:59:04.959Z","first_independent_review_at":"2026-10-25T18:05:07Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"eb38214adaffb067303df3f3a12a5f6fcc0a75336407ed232afc1b1c9ae50930","record_sha256":"966f1acfce12197468e4113606866f3a1c5c3030549257fac3b57d89afb60937"},
    {"record_id":"author-pr40-build-002","pr":40,"pr_url":"https://github.com/Homie-Improvement/miru/pull/40","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Malformed-body 400s bypass the {reason:} error shape","finding_evidence":"A wrong-shaped or non-JSON write body gets Rails' default 400 rendering, not the {reason: ...} body every other answer on these routes uses. ApplicationController promises that error bodies hold only the contract's reason. A consumer that reads response.reason to decide whether to retry gets nil or a parse error.","recorded_resolution":"fixed","recorded_correction":"A write body that is not JSON, or holds a wrong-shaped value, now gets 400 with {\"reason\":\"invalid_request\"}. The status map and its spec carry the new reason, and a request spec covers both cases.","correction_target":"code_commit","source_path":"timekeeping/app/services/timekeeping/entry_command.rb","source_line":132,"fix_commit":"068ffa88d20426c0f2ca30b6a39ff4de17fbf839","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/068ffa88d20426c0f2ca30b6a39ff4de17fbf839","finding_recorded_at":"2026-10-25T17:41:25.839Z","answer_recorded_at":"2026-10-25T17:59:04.959Z","first_independent_review_at":"2026-10-25T18:05:07Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"ffe1bf06773c163edfec5e2fa76303193f59c277d12b58972c5e7d0cf232277f","record_sha256":"dd28cf15e97f96a955f9009f2befb08f344e1111e53bb2b1afad12bb2bf8db19"},
    {"record_id":"author-pr40-build-003","pr":40,"pr_url":"https://github.com/Homie-Improvement/miru/pull/40","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Retry after project removal answers 404 instead of replaying the stored success","finding_evidence":"Suppose a client's create, update or bulk-move committed, the response was lost, and the project was removed before the retry. The retry gets 404 not_found although the write exists, because the project check runs before the receipt is read. That breaks idempotency.yml's 'Same key and body returns the same answer'. plan item 3 discloses only the 503-during-outage case.","recorded_resolution":"fixed","recorded_correction":"The missing-project check now runs in the writer after the receipt is re-read, so a committed write replays its stored 201. A new request with that project still gets 404.","correction_target":"code_commit","source_path":"timekeeping/app/services/timekeeping/entry_command.rb","source_line":72,"fix_commit":"068ffa88d20426c0f2ca30b6a39ff4de17fbf839","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/068ffa88d20426c0f2ca30b6a39ff4de17fbf839","finding_recorded_at":"2026-10-25T17:41:25.839Z","answer_recorded_at":"2026-10-25T17:59:04.959Z","first_independent_review_at":"2026-10-25T18:05:07Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"ad6997874f1c80ddab435b84478ed8a9ff98b861f78c8e7e42103af572370f63","record_sha256":"bbaff9bb6d269b0677c69a5359a506c730c432a41fa111bc3729b630188bd5dd"},
    {"record_id":"author-pr40-build-004","pr":40,"pr_url":"https://github.com/Homie-Improvement/miru/pull/40","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"Single-entry PATCH/DELETE rule refusals never exercised through the writer","finding_evidence":"PATCH and DELETE lock the target entry with no visibility scope (locked_entries without visible_only). That leaves change_refusal as the only check stopping an employee from editing or discarding a colleague's, billed, reserved or out-of-window entry. No writer, request or contract spec sends such a single-entry request, so dropping or miswiring change_refusal would leave every spec green.","recorded_resolution":"fixed","recorded_correction":"Added request specs where an employee updates or deletes a colleague's, billed or out-of-window entry, or updates a reserved one. Each gets 403 with nothing written, and 7 of them fail when the rule check is removed.","correction_target":"code_commit","source_path":"timekeeping/app/services/timekeeping/entry_writer.rb","source_line":157,"fix_commit":"068ffa88d20426c0f2ca30b6a39ff4de17fbf839","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/068ffa88d20426c0f2ca30b6a39ff4de17fbf839","finding_recorded_at":"2026-10-25T17:41:25.839Z","answer_recorded_at":"2026-10-25T17:59:04.959Z","first_independent_review_at":"2026-10-25T18:05:07Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"d9bd708dcc25986d41ab339c268a216dc7b1e9a39d20c54d6e11cd546cdfc045","record_sha256":"88d24451587a57fbb23deec9db24a64236dbb0d02b65e67a4dff918bac81abed"},
    {"record_id":"author-pr40-build-005","pr":40,"pr_url":"https://github.com/Homie-Improvement/miru/pull/40","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Writer step comment says row-lock writers never wait","finding_evidence":"FOR UPDATE makes a second writer to the same entry wait until the first commits, and the race spec depends on that wait. Id ordering prevents deadlock, not waiting. The wrong comment invites a later change that relies on writers never blocking.","recorded_resolution":"fixed","recorded_correction":"The step comment now says that writers on the same entries wait in turn, and that locking in id order prevents deadlock.","correction_target":"code_commit","source_path":"timekeeping/app/services/timekeeping/entry_writer.rb","source_line":15,"fix_commit":"068ffa88d20426c0f2ca30b6a39ff4de17fbf839","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/068ffa88d20426c0f2ca30b6a39ff4de17fbf839","finding_recorded_at":"2026-10-25T17:41:25.839Z","answer_recorded_at":"2026-10-25T17:59:04.959Z","first_independent_review_at":"2026-10-25T18:05:07Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"323e92f431ad748be8073e037c116815a2fb9fd747607b9f209701a106da1ea8","record_sha256":"26a4997cd5f3d12bf514107fabd834604101d88e601b27145a305cf2ce9a0413"},
    {"record_id":"author-pr40-plan-001","pr":40,"pr_url":"https://github.com/Homie-Improvement/miru/pull/40","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Scan the decoded body so JSON escapes cannot hide credentials.","finding_evidence":"plan item 6 and the CredentialScan interface inspect raw JSON, but the writer stores decoded values. A caller can replace a JWT's initial `e` with `\\u0065` inside `proof_metadata`. The raw text contains neither the bearer token nor a verifiable JWT, while JSON parsing restores the complete credential. The write can then persist that credential in the entry, audit and outbox. The existing policy in `spec/support/timekeeping/fake_timekeeping.rb:59-61` instead scans `JSON.generate(body)` over decoded values. Parse once in EntryCommand and pass the decoded body, or its serialization, to CredentialScan. Use that same body for persistence and fingerprinting. Add an escaped-token test that expects 422 and no writes.","recorded_resolution":"fixed","recorded_correction":"The command parses the body once. That parsed body feeds the credential scan, the fingerprint and the stored columns, and an escaped-token test expects 422 with no writes.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T16:43:20.325Z","answer_recorded_at":"2026-10-25T16:45:19.433Z","first_independent_review_at":"2026-10-25T18:05:07Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"51b9cf2b62d4e0ab6df5fc8beaeac556a1e8b86fc4348f05c5588bd2bc00f75a","record_sha256":"e3cc82d1a48bd55a6f3a5740753cbde5f10b817461e68ed3b18a1d025755cd87"},
    {"record_id":"author-pr40-plan-002","pr":40,"pr_url":"https://github.com/Homie-Improvement/miru/pull/40","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Scanning only raw JSON lets escaped credentials enter stored entries and snapshots.","finding_evidence":"plan item 6 scans the raw request string, but JSON decoding can restore a credential that the scan never sees. With TIMEKEEPING_SERVICE_TOKEN set to service-secret, a valid PATCH containing {\"proof_metadata\":{\"token\":\"\\u0073ervice-secret\"}} contains no literal service-secret on the wire. Decoding restores that credential before storage. The existing migration stores proof_metadata as jsonb, and contracts/timekeeping/entry_snapshots.yml includes it in every full snapshot. The planned writer would therefore copy the secret into the entry, audit, receipt and outbox. Unicode escapes can also hide a signed user token from the planned substring verification. Scan decoded string keys and values before persistence, using the same parsed body that the command writes. Add rejection tests for escaped service credentials, the caller's token and another valid user token.","recorded_resolution":"fixed","recorded_correction":"The scan now reads decoded keys and values from the same parsed body the command writes. Rejection tests cover an escaped service credential, the caller's token and another user token.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T16:43:20.325Z","answer_recorded_at":"2026-10-25T16:45:19.433Z","first_independent_review_at":"2026-10-25T18:05:07Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"b6eefe8f7cd7ec73cf9f8e31ea7cf31597085fbcbb0bb4d8adb517d2decb19ee","record_sha256":"dfdd5c55d373e5f0653442aed005fed3555c66167f5e8be99add87122a9ce406"},
    {"record_id":"author-pr40-plan-003","pr":40,"pr_url":"https://github.com/Homie-Improvement/miru/pull/40","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Scanning only raw JSON lets escaped credentials reach stored entries and snapshots.","finding_evidence":"plan item 6 scans the raw request body, but JSON escapes can conceal a credential until parsing. For example, replace the first `e` of a valid user token with the literal JSON escape `\\u0065` inside `proof_metadata`. The raw body contains neither the complete token nor a substring that verifies as that token. Parsing restores the credential. plan item 12 then copies that metadata into the outbox, exposing the credential to snapshot consumers. `spec/support/timekeeping/fake_timekeeping.rb` instead scans `JSON.generate(body)` after parsing, and `contracts/timekeeping/entry_snapshots.yml` includes `proof_metadata`. Scan the parsed body's serialized JSON, including keys and nested values. Add request tests that send escaped caller tokens, other valid user tokens and held service credentials, and assert 422 with no writes.","recorded_resolution":"fixed","recorded_correction":"The body is parsed once, and the scan checks the parsed form, so a token with a \\u0065 escape is found. Request tests cover escaped caller tokens, other user tokens and held credentials, with 422 and no rows.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T16:43:20.325Z","answer_recorded_at":"2026-10-25T16:45:19.433Z","first_independent_review_at":"2026-10-25T18:05:07Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d0c94aea584b70b4f7a8d6aa752b30f5305b17c264a4d89357679b787e111732","record_sha256":"5c981abbb403166e00b47bb6fa1f28b22d289dcb141957212d6f504e7fe778ff"},
    {"record_id":"author-pr40-plan-004","pr":40,"pr_url":"https://github.com/Homie-Improvement/miru/pull/40","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Scanning only the raw request permits credentials in stored metadata.","finding_evidence":"plan item 6 checks the raw request string, but R14 requires checking the body's serialized JSON. These differ when JSON escapes encode credential characters. For a configured credential `service-secret`, metadata containing `\"\\u0073ervice-secret\"` passes the proposed substring check. JSON parsing restores `service-secret`, which the write then stores and copies into audit and outbox rows. The same bypass applies to escaped user tokens. The existing policy in `spec/support/timekeeping/fake_timekeeping.rb` checks `JSON.generate(body)`, and `contracts/timekeeping/entry_api.yml` forbids credentials anywhere in the body. Scan the parsed body's serialized form before any write, and test escaped credentials inside nested values and keys.","recorded_resolution":"fixed","recorded_correction":"The scan now runs on the parsed body: it checks JSON.generate of that body and each decoded key and string value. Escaped credentials are tested on nested values and keys.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T16:43:20.325Z","answer_recorded_at":"2026-10-25T16:45:19.433Z","first_independent_review_at":"2026-10-25T18:05:07Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9c6ba7774c41be91aa31c1d4cf5b7fb29503034e18bbe0840c497811221a4eb3","record_sha256":"88858916367db5f23632376c4355775f85c0c72e443cbde36de7b7a2a55a7be2"},
    {"record_id":"author-pr40-plan-005","pr":40,"pr_url":"https://github.com/Homie-Improvement/miru/pull/40","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The create path needs an explicit default for bill_status.","finding_evidence":"Normal contract requests omit bill_status, but the existing database cannot insert those requests unchanged. timekeeping/db/migrate/20260101000000_create_entries_and_reservations.rb declares bill_status NOT NULL without a default. timekeeping/app/models/entry.rb adds no default either. The shared helper entry_body omits that field, while FakeTimekeeping#new_entry supplies \"unbilled\". The planned create work names target-user checks, project checks and version initialization, but does not supply this required value. Add the \"unbilled\" default to the create decision or attribute builder, preserving an explicitly supplied bill_status. Test a minimal contract create directly so ordinary requests return 201 rather than a database error.","recorded_resolution":"fixed","recorded_correction":"Confirmed: timekeeping/db/schema.rb has bill_status NOT NULL with no default. A create now sets \"unbilled\" when the body leaves it out, and a minimal create test expects 201.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T16:43:20.325Z","answer_recorded_at":"2026-10-25T16:45:19.433Z","first_independent_review_at":"2026-10-25T18:05:07Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"3d0c42e06131f7e1176d51e4a9f955346823af76db910a8c8da8fff7b2498b22","record_sha256":"80ab33e99a98f413ee7f6a641c38e1ff4706d35a9803c8077ccb4db47e75d65e"},
    {"record_id":"author-pr40-plan-006","pr":40,"pr_url":"https://github.com/Homie-Improvement/miru/pull/40","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The credential scan checks a different body representation from the write.","finding_evidence":"R14 requires credential rejection in serialized JSON, but plan item 6 scans only the raw HTTP body. plan item 9 parses that body before generating JSON for the fingerprint. These representations differ: replacing a token's first `e` with the JSON escape `\\u0065` hides it from the raw substring and token-pattern checks. Parsing restores the complete token, which can then enter stored metadata, audits and outbox snapshots. Define one parsed body as the source for both scanning and writing. Check its decoded strings and serialized form before persistence. Add a test that submits an escaped credential and expects 422 with no rows written.","recorded_resolution":"fixed","recorded_correction":"The scan, the fingerprint and the write now all use one parsed body. The scan reads its decoded strings and its serialized form, and an escaped-credential test expects 422 with no rows.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T16:43:20.325Z","answer_recorded_at":"2026-10-25T16:45:19.433Z","first_independent_review_at":"2026-10-25T18:05:07Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"704b6d74d03554de74f14dbfb534094f4effc5f97494efb35b4dde42fa505a29","record_sha256":"7e3bc5ccadbfdb860f9a1b597fa384c47ebca6ece1c35d4d84e1a59deeeba3e6"},
    {"record_id":"author-pr40-plan-007","pr":40,"pr_url":"https://github.com/Homie-Improvement/miru/pull/40","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The missing-row race cannot prove that the generation share lock protects writes.","finding_evidence":"U8 requires the generation race to fail when `FOR SHARE` is removed. Its only generation scenario starts without a row. Under plan item 13, the writer first inserts generation 1 and retains that uncommitted insertion until commit. A concurrent `OwnershipGeneration.advance` must wait for that insertion even without `FOR SHARE`. Holding the writer at `:generation_checked` therefore cannot distinguish the correct implementation from one missing the share lock. The stated stop condition can halt implementation, or the test can leave the existing-row path unprotected. Keep the missing-row scenario and add a race with an already committed generation row. Show that advancing that row waits for the writer, and that removing `FOR SHARE` breaks this assertion.","recorded_resolution":"fixed","recorded_correction":"Added a race on an already committed generation row, which must fail when FOR SHARE is removed. The missing-row case now advances while the write is held before its seed insert and expects 409.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T16:43:20.325Z","answer_recorded_at":"2026-10-25T16:45:19.433Z","first_independent_review_at":"2026-10-25T18:05:07Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"b7480057176e835783643131f049b88b9668be9a54ac53a1e82fbe83ecf29486","record_sha256":"caaad4af3b91bc98e5158fd23daf8c6f0404a12a803dee9c27e11c040ad14824"},
    {"record_id":"author-pr40-plan-008","pr":40,"pr_url":"https://github.com/Homie-Improvement/miru/pull/40","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The proposed race cases cannot all fail when their locks are removed.","finding_evidence":"The plan makes lock-removal failures a completion condition, but some listed cases remain safe through other required protections. In the missing-generation-row case, the writer's INSERT holds its uncommitted row until commit. OwnershipGeneration.advance must wait even without the later FOR SHARE lock. Likewise, concurrent creates can still return one 201 and one 422 without the receipt advisory lock, because the unique index and rollback-and-replay backstop preserve that result. Use an already committed generation row to isolate FOR SHARE. Use the PATCH and DELETE cases to isolate the receipt lock, and test the unique-index backstop separately. Keep the missing-row and concurrent-create cases as correctness tests rather than requiring them to detect removal of those locks.","recorded_resolution":"fixed","recorded_correction":"Only the PATCH and DELETE races and the committed-row generation race must fail when a lock is removed. The concurrent-create and missing-row cases stay as correctness tests, and the backstop has its own test.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T16:43:20.325Z","answer_recorded_at":"2026-10-25T16:45:19.433Z","first_independent_review_at":"2026-10-25T18:05:07Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"830f08ad2d95b83e7066af3f08cc89167d9ae76ca30ce209d76997ac9031395d","record_sha256":"e6141fcc2f8eede4bbf688f6dd5d80c0fb9022900991c6ba0e1a049528460f46"},
    {"record_id":"author-pr40-plan-009","pr":40,"pr_url":"https://github.com/Homie-Improvement/miru/pull/40","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"high","finding_title":"The plan goes beyond the issue: Accept writes without an idempotency key and persist no receipt for them.","finding_evidence":"Accepting a mutation without any receipt creates an exception to the issue's explicit per-change receipt guarantee and allows duplicate creates after a lost response. Citing a fake helper and disclosing that risk do not resolve this departure; establish an authorized keyless policy that reconciles the guarantee before locking the plan.","recorded_resolution":"fixed","recorded_correction":"Writes without an Idempotency-Key are now refused with 422 idempotency_key_required before any read or write, so every committed change has its receipt. No shared example sends a keyless write with a valid token.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T16:46:45.239Z","answer_recorded_at":"2026-10-25T16:48:40.905Z","first_independent_review_at":"2026-10-25T18:05:07Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"b658c147989157ed55257d6fc4a25bbe4373523f35937d3add3847e3d6bf1400","record_sha256":"b3568a4ed5e414556d5555fbce66ca34679d4cb11e251b6eac66d3f38701826a"},
    {"record_id":"author-pr42-plan-001","pr":42,"pr_url":"https://github.com/Homie-Improvement/miru/pull/42","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"An unreadable baseline can hide the writes this test must detect.","finding_evidence":"plan item 1 says including the read status makes a failed read fail an expectation. U1 only compares `[status, body.slice(\"bill_status\", \"agent_id\")]` with its baseline. If the seeded entries return 404 before and after each command, both values are `[404, {}]`, and a forbidden write remains invisible. A 200 response that omits these fields has the same gap. `spec/contracts/timekeeping/contract_example_helpers.rb` returns the response without validating it, and `contracts/timekeeping/entry_api.yml` requires both fields. Validate the baseline before issuing commands: require status 200, both field keys, and the intended starting values, including `bill_status` \"unbilled\" and `agent_id` 77 on the affected entries. Keep the per-command comparisons. This makes failed observation fail the example instead of passing as unchanged state.","recorded_resolution":"fixed","recorded_correction":"The example now asserts the baseline before sending any command: the billable entry reads bill_status \"unbilled\" and the agent entry reads agent_id 77, so an unreadable or empty baseline fails.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:39:19.332Z","answer_recorded_at":"2026-10-25T18:39:58.109Z","first_independent_review_at":"2026-10-25T19:23:08Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"83e552e09e8b07e4c2b30e7cad6eddba8034d0b8ccdefe156d2f0a02c99fbbca","record_sha256":"a2e40b9c3b71706bc14324ccf732ef91e502e72c3893d4080aa5e4f92adad3a6"},
    {"record_id":"author-pr42-plan-002","pr":42,"pr_url":"https://github.com/Homie-Improvement/miru/pull/42","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The entry-read approach assumes a route that reservation-only providers need not implement.","finding_evidence":"The first decision says every provider already serves the entry show route. However, contracts/timekeeping/README.md allows providers to run each group independently, implementing only that group's routes. A reservation-only provider can return 404 for every new entry read. The proposed comparison then accepts identical [404, {}] values before and after an unauthorized write, so it still misses the defect. Use an arrangement reader to preserve independent groups, or explicitly establish and document the new route prerequisite. If route reads remain, require a successful baseline containing both fields before sending rejected commands; comparing response statuses alone does not establish that the fields were observed.","recorded_resolution":"fixed","recorded_correction":"Agreed: the contract README lets a provider run the reservations group without the entry API. The plan now reads the fields through a new timekeeping_arrange reader instead of the show route.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:39:19.332Z","answer_recorded_at":"2026-10-25T18:39:58.109Z","first_independent_review_at":"2026-10-25T19:23:08Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f45a5af12cf77597bdf6a317069f7ebced4e56bd352c1e561d60c97f3a9d6839","record_sha256":"cc77eac2b5488e3046cdccbce6973ebd503c3d82a784f898f7715b591fc1e3c1"},
    {"record_id":"author-pr43-build-001","pr":43,"pr_url":"https://github.com/Homie-Improvement/miru/pull/43","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"CLASSIFICATION_TABLE freezes only the outer array, leaving its rows writable","finding_evidence":"Each CLASSIFICATION_TABLE row is a [lambda, list] array, and `freeze` is shallow, so code like `CLASSIFICATION_TABLE[0][0] = ->(_) { false }` still works without an error. Such a write would change how every later classification in the process sorts entries into conflicts, orphans, retained, relock_releases and unlocked_links. That would also change what the recorder refuses and what it writes to the checked-in disposition. The repository's convention for constant decision tables of lambda rows (docs/solutions/conventions/freeze-rule-table-rows-not-only-the-outer-array.md) requires freezing each row and checking it with be_frozen / all(be_frozen). This new table does neither.","recorded_resolution":"fixed","recorded_correction":"Each row of the classification table is now frozen, as well as the table itself. A new spec checks both, and a write into a row now raises FrozenError.","correction_target":"code_commit","source_path":"lib/timekeeping/preflight.rb","source_line":33,"fix_commit":"8932698bd2d55861d30ea60178a6442eb29d1df7","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/8932698bd2d55861d30ea60178a6442eb29d1df7","finding_recorded_at":"2026-10-25T19:20:01.269Z","answer_recorded_at":"2026-10-25T19:33:54.356Z","first_independent_review_at":"2026-10-25T19:43:56Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"54bc45dc51c99fb283a05ec5d9a6c0ee4b6e019d9c10677919db4797e318396e","record_sha256":"3448ae7f1990359820e75f5a2d060324d05e37163e04bb33f0f58a298b56f282"},
    {"record_id":"author-pr43-plan-001","pr":43,"pr_url":"https://github.com/Homie-Improvement/miru/pull/43","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Generating a file from the shared fixture always triggers the reseed guard.","finding_evidence":"After the shared context loads both companies, U4 calls generate! into a temporary path on the same load. plan item 6 defines generate! as loading the seed first, and plan item 3 rejects a load whenever a Company exists. This verification therefore fails before writing anything. Keep the reseed guard. Test file generation on its own fresh database, or verify serialization from the existing snapshot without calling the seed-loading entry point.","recorded_resolution":"fixed","recorded_correction":"The reseed guard is kept. The spec writes the file from the existing load through a step that never calls the seed loader.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:41:02.672Z","answer_recorded_at":"2026-10-25T18:42:13.549Z","first_independent_review_at":"2026-10-25T19:43:56Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"15c5bd1910a34fb14b7f0e72fd8dd6d4f07460f85dca026fa564890633755b19","record_sha256":"02a9e52c128aedc7bbe1fb12cfbf94a95854170c11fd6064ce490a2b213c3adf"},
    {"record_id":"author-pr43-plan-002","pr":43,"pr_url":"https://github.com/Homie-Improvement/miru/pull/43","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The empty-database check permits the seed to overwrite existing users.","finding_evidence":"With no Company rows, an existing User with email dana@forkandleaf.example passes the check in plan item 3. The database is not empty. db/seeds.rb:40-50 finds that user by email and overwrites their name, password and workspace. app/models/user.rb permits users without a company, so this state is valid. The production restriction does not protect existing development accounts. Check that all application tables are empty before resetting sequences or loading the seed, excluding database metadata. Add a test with an existing user and no companies that proves rejection leaves the user unchanged.","recorded_resolution":"fixed","recorded_correction":"The loader now refuses when any application table has rows, not only companies. A test with a standalone seed-email user and no company proves refusal leaves that user unchanged.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:41:02.672Z","answer_recorded_at":"2026-10-25T18:42:13.549Z","first_independent_review_at":"2026-10-25T19:43:56Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f52560f90ab9478c83efc330d145fe611aa718539a77c23138e1fb4e352290c4","record_sha256":"d8ab032e80d6f4843e7566a09631ee0bee70d6b3804a9734f81f7668a9994b5d"},
    {"record_id":"author-pr43-plan-003","pr":43,"pr_url":"https://github.com/Homie-Improvement/miru/pull/43","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The empty-database guard permits existing accounts to be overwritten.","finding_evidence":"plan item 3 treats the absence of Company rows as proof that every table is empty. Users can exist without companies: db/schema.rb:1150 allows a null current_workspace_id, and app/models/user.rb:166-172 explicitly supports this state. An existing dana@forkandleaf.example account therefore passes the guard. db/seeds.rb:39-49 finds that account by email and replaces its password and profile; line 12 defaults the password to \"password\". The authentication callbacks also revoke its command-line sessions. The task's production refusal does not protect existing accounts in development databases. Before resetting sequences or loading seeds, reject any existing application data, excluding Rails metadata tables. Test that a standalone user causes refusal and retains its credentials and sessions.","recorded_resolution":"fixed","recorded_correction":"The guard now checks every application table except Rails metadata, so an existing account can no longer be overwritten. A test proves a standalone user keeps its password digest after refusal.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:41:02.672Z","answer_recorded_at":"2026-10-25T18:42:13.549Z","first_independent_review_at":"2026-10-25T19:43:56Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"52bebf8d70af852183dc9567b22aba54f8176b12024008dfdedec9ab46a649a3","record_sha256":"e1cee76c66b79e25e9b97aed11ec0f47b1b1ee1aa69322ecfd298f28fbb6d4e2"},
    {"record_id":"author-pr43-plan-004","pr":43,"pr_url":"https://github.com/Homie-Improvement/miru/pull/43","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The fixture cannot call the generator against the seed it already loaded.","finding_evidence":"The shared context in the fourth unit loads both companies before its examples. That unit then requires generate! to write a temporary file from the same load. However, the generator always calls load_fresh_seed!, which must reject those existing companies. The required example cannot pass with the declared interfaces. Compare the pure record result from the existing snapshot with the checked-in file, and test generate! separately on an empty database. This keeps the refusal intact without adding a second loading mode.","recorded_resolution":"fixed","recorded_correction":"The fixture spec now calls the write step on the loaded seed and compares its file with the checked-in one. The full rake run on an empty database proves the load-and-write path.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:41:02.672Z","answer_recorded_at":"2026-10-25T18:42:13.549Z","first_independent_review_at":"2026-10-25T19:43:56Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"65e3d7249f078f105b182b66bfbe82d0190ef84bb360c2028fe767fcfd867d7a","record_sha256":"bc9fe8cab8cbf7ed0be35e0e21cca00f316053b76947b5882284505746afeb69"},
    {"record_id":"author-pr43-plan-005","pr":43,"pr_url":"https://github.com/Homie-Improvement/miru/pull/43","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The fixture spec calls the generator after the loader has made its database guard fail.","finding_evidence":"The shared context loads both companies before the fixture spec runs. The fixture spec then requires `generate!` to write a temporary file \"on the same load\". However, the design says `generate!` calls `load_fresh_seed!`, which must reject any database containing a company. These requirements make that integration check fail before it writes anything. Keep the generator's refusal to reseed. Compare the already-loaded snapshot through `SeedDisposition.record`, and test the full generator separately on an empty database. State which check proves file generation without requiring a second load over existing data.","recorded_resolution":"fixed","recorded_correction":"File writing is now its own step that reads the already-loaded data. The fixture spec calls that step on its load, and the full generator keeps its refusal to reseed.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:41:02.672Z","answer_recorded_at":"2026-10-25T18:42:13.549Z","first_independent_review_at":"2026-10-25T19:43:56Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9e4b446b3aeab56fd289954cabc2cf6362aa4085fdcf53321b8c35c03c2ebf18","record_sha256":"a01cac3a364c22623abef5e41e5f4de0bcb16a3eb0c9d58cd56f3e36d2836404"},
    {"record_id":"author-pr43-plan-006","pr":43,"pr_url":"https://github.com/Homie-Improvement/miru/pull/43","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The generator cannot write from the seed already loaded by the shared fixture.","finding_evidence":"The fixture cannot exercise the planned file-writing path without loading the seed again. The shared context loads both companies before the examples. However, generate!(path:, as_of:) always calls load_fresh_seed!, which rejects any existing Company. The test in U4 that calls generate! “on the same load” therefore must fail. Keep fresh loading in the task's orchestration, and let file generation consume the measured snapshot or recorded hash. Alternatively, change U4 to test the existing pure record function and verify file generation separately on an empty database. Do not weaken the empty-database check to make this test pass.","recorded_resolution":"fixed","recorded_correction":"The generator is now two steps: a fresh load, then a write that reads the current data. The fixture spec drives the write step on its existing load, with no second load.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:41:02.672Z","answer_recorded_at":"2026-10-25T18:42:13.549Z","first_independent_review_at":"2026-10-25T19:43:56Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"99eeb93a45c3879fb1012cc63ef324c12cf1225c8f418c872d15be28e8f24041","record_sha256":"a2cb4019f3f527ecb0e801d0857d3e8143c77c1257c9145937b0ff7249d41e8b"},
    {"record_id":"author-pr43-plan-007","pr":43,"pr_url":"https://github.com/Homie-Improvement/miru/pull/43","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The loader checks for companies, not the empty database the ticket requires.","finding_evidence":"The ticket requires an empty database, but plan item 3 rejects only an existing Company row. Users can exist without a company: db/schema.rb:1141-1150 makes current_workspace_id nullable, and app/models/user.rb:36-37 associates companies through employments. The seed then finds users by email instead of always creating them (db/seeds.rb:40-51). This allows the recorder to reuse existing users and produce a baseline that depends on prior data rather than a fresh seed. Check that application tables are empty before resetting sequences or loading the seed, with explicit exceptions for database metadata. Add a refusal test with an existing User and no Company, and assert that it changes nothing.","recorded_resolution":"fixed","recorded_correction":"Same fix as the other empty-database findings: all application tables must be empty before the load, and a refusal test with an existing user and no company asserts nothing changed.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:41:02.672Z","answer_recorded_at":"2026-10-25T18:42:13.549Z","first_independent_review_at":"2026-10-25T19:43:56Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"e7bea0e5c3ad5de0f4090b1da7ab0a71e194e7ce235f71376cfa08d214bd1acc","record_sha256":"dee4ee6cc6b30e6ceb1983923cda51db5073458e9df7368af42229eaa24fef63"},
    {"record_id":"author-pr43-plan-008","pr":43,"pr_url":"https://github.com/Homie-Improvement/miru/pull/43","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The loader treats an empty Company table as proof that the whole database is empty.","finding_evidence":"The loader decision says it resets every table's sequence, \"safe because the guard proved them empty\", but its guard checks only for `Company` rows. The generator requirement instead requires an empty database. A database with no companies but rows in another table passes the stated guard. The plan supplies no invariant that excludes this state. Define which tables must be empty and check them before resetting sequences or loading the seed. Add a test with an empty Company table and existing rows elsewhere, so the recorded fixture cannot silently come from a mixed database.","recorded_resolution":"fixed","recorded_correction":"The plan now defines the empty database as every table except ar_internal_metadata and schema_migrations having no rows, checked before sequences are reset. A test covers rows outside the Company table.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:41:02.672Z","answer_recorded_at":"2026-10-25T18:42:13.549Z","first_independent_review_at":"2026-10-25T19:43:56Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"8539aeeef449c2388a6746d0c6fe2aa1bf883657d06476bda103b93344bae82a","record_sha256":"09c9bf806db552c79249fbb06f0c3b5052cd093c92489593d03c77fa862b5833"},
    {"record_id":"author-pr43-plan-009","pr":43,"pr_url":"https://github.com/Homie-Improvement/miru/pull/43","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The loader's company check does not establish an empty database.","finding_evidence":"The planned loader permits existing users whenever Company has no rows. db/schema.rb:1150 allows a null current_workspace_id, and app/models/company.rb nullifies that field when a company is destroyed without deleting its users. db/seeds.rb:41-51 finds users by email and overwrites their names, password and workspace. A remaining user with a seed email therefore has account data changed despite the promised refusal to seed over existing data. Other existing users also change the seeded user ids and entry selection. Before resetting sequences or loading the seed, check all application tables that must be empty, with explicit exceptions for framework metadata. Add a refusal test with an existing user and no company, and assert that the user remains unchanged.","recorded_resolution":"fixed","recorded_correction":"The guard now checks all application tables, so leftover users from a destroyed company block the load. A refusal test asserts that user's attributes are unchanged.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:41:02.672Z","answer_recorded_at":"2026-10-25T18:42:13.549Z","first_independent_review_at":"2026-10-25T19:43:56Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9d398533567ce867319c6b51fed37dc3408bac4c8e993b032fb257d92cf259c3","record_sha256":"ffe4fa9e35a1734b16d5f622c0325d570c78b8c8b0d1b09a00d543d7baa06b43"},
    {"record_id":"author-pr43-plan-010","pr":43,"pr_url":"https://github.com/Homie-Improvement/miru/pull/43","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Unlocked entries with multiple historical owners escape the conflict list.","finding_evidence":"Consider an unlocked entry referenced by two distinct discarded non-draft invoices and no kept invoice. plan item 1 returns no bucket because its historical-conflict rule requires locked, and U1 explicitly expects unlocked entries with only discarded links to appear in no list. Neither the ticket nor docs/architecture/timekeeping-soa.md:873 limits historical conflicts to locked entries. A future caller could therefore receive a clean conflict report despite ambiguous ownership. Remove the locked condition from this conflict rule and test this input. This does not require resolving precedence between overlapping lists: no other listed category covers this input.","recorded_resolution":"fixed","recorded_correction":"The rule for more than one plausible historical owner no longer requires the entry to be locked. An unlocked entry with two discarded non-draft owners is now a conflict, with a test for that input.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:41:02.672Z","answer_recorded_at":"2026-10-25T18:42:13.549Z","first_independent_review_at":"2026-10-25T19:43:56Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"506348cdabeb1bae645cb10a50629abdde0b100d4145f2d4398357e2ae86550b","record_sha256":"1fdf44fe8a4248e43236babd16772a39c6ea01bfb0fcb57fc865d8d68dabf627"},
    {"record_id":"author-pr43-plan-011","pr":43,"pr_url":"https://github.com/Homie-Improvement/miru/pull/43","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"medium","finding_title":"The plan goes beyond the issue: Classify separately by tenant and treat every cross-company link as a classifier conflict.","finding_evidence":"Tenant-keyed recording is required, but adding a conflict solely because one invoice belongs to another company extends the classifier's stated definition. Retain tenant-integrity assertions or refusal of unsafe snapshots without making this additional public classification behavior mandatory.","recorded_resolution":"fixed","recorded_correction":"The classifier now uses only the issue's two conflict conditions. Tenant safety moved to the recorder: it refuses to write when any invoice links an entry from another company, with a test.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:43:06.677Z","answer_recorded_at":"2026-10-25T18:44:07.941Z","first_independent_review_at":"2026-10-25T19:43:56Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f309d3758e360cfc3128057f5be2c6c8a95cf9200474442d588699d7d2e1ee1b","record_sha256":"5685ab79199e07354cd9f6fc4206aa2272cc8d7fdef5db0bc4e1ac7f1a9ad067"},
    {"record_id":"author-pr43-plan-012","pr":43,"pr_url":"https://github.com/Homie-Improvement/miru/pull/43","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"medium","finding_title":"The plan goes beyond the issue: Use an exclusive decision table for classification, including a new cross-company conflict category.","finding_evidence":"The decision-table structure and unlocked historical-owner correction implement the requested classifier. However, categorizing every cross-company link as a conflict adds a third conflict condition not stated by the issue; tenant-isolation validation can remain without changing the classifier's specified categories.","recorded_resolution":"fixed","recorded_correction":"The cross-company row is gone from the classifier's decision table. Tenant isolation is now checked by the recorder and the fixture spec, so classify keeps the categories the issue names.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:43:06.677Z","answer_recorded_at":"2026-10-25T18:44:07.941Z","first_independent_review_at":"2026-10-25T19:43:56Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"fa6f8c3bd396f409bc15494b0a66747ab93cd3c17a296b338710a583b27bee59","record_sha256":"8c6d4f7de25ecefca6ff378ce88780baba1d447c634cfa88f63b50382aa7f30e"},
    {"record_id":"author-pr44-build-001","pr":44,"pr_url":"https://github.com/Homie-Improvement/miru/pull/44","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"DROPPED_KEYS rows left mutable; shallow freeze only","finding_evidence":"up, down and the dangling-reference precheck all read DROPPED_KEYS. `.freeze` on the outer array is shallow, so code that loads the migration can write into a row hash and silently change which key is restored or checked. The repository learning 'freeze rule table rows, not only the outer array' applies here. Impact is low: nothing writes to it today.","recorded_resolution":"fixed","recorded_correction":"Each row in the list of dropped keys is now frozen, as well as the list itself.","correction_target":"code_commit","source_path":"db/migrate/20261020120000_drop_timesheet_entry_foreign_keys.rb","source_line":13,"fix_commit":"a97027a7179a56c3cb4bf2f4196bbb48562092e2","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/a97027a7179a56c3cb4bf2f4196bbb48562092e2","finding_recorded_at":"2026-10-25T19:44:11.340Z","answer_recorded_at":"2026-10-25T20:03:31.120Z","first_independent_review_at":"2026-10-25T20:31:02Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"be921ccc84061e7e2626d30e9b6d7c43d21b05bf1581a8cd223267e2adcd6828","record_sha256":"03ef79832cfe9594e766f07c6e9f1d50b127aeb63ae94b660fee45e5b37638ec"},
    {"record_id":"author-pr44-build-002","pr":44,"pr_url":"https://github.com/Homie-Improvement/miru/pull/44","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Rollback-refusal test checks only the last key, not 'before any change'","finding_evidence":"The example says the rollback stops before any change, but it checks only the last key in DROPPED_KEYS and never the starting state. If a later edit made down restore keys one at a time and then fail, the first key would come back, the test would still pass, and under disable_ddl_transaction! the schema would be left half rolled back.","recorded_resolution":"fixed","recorded_correction":"The refused-rollback test now checks that none of the three dropped keys exist, both before and after the refused rollback.","correction_target":"code_commit","source_path":"spec/db/migrations/drop_timesheet_entry_foreign_keys_spec.rb","source_line":32,"fix_commit":"a97027a7179a56c3cb4bf2f4196bbb48562092e2","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/a97027a7179a56c3cb4bf2f4196bbb48562092e2","finding_recorded_at":"2026-10-25T19:44:11.340Z","answer_recorded_at":"2026-10-25T20:03:31.120Z","first_independent_review_at":"2026-10-25T20:31:02Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"476153ec6f3456e1cad2c88151e4ac197429e40ca5ed5bc12b25510e508ee079","record_sha256":"bcd083f6742d5e27b336a868cc0eb6b7990045abfe8d6cfbe1d8d6ce30020175"},
    {"record_id":"author-pr44-build-003","pr":44,"pr_url":"https://github.com/Homie-Improvement/miru/pull/44","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"S2 backfill runs inside add_column's transaction, holding ACCESS EXCLUSIVE on timesheet_entries","finding_evidence":"Every read and write of timesheet_entries waits until S2 commits. Timers, the timesheet page, reports and invoice generation queue up, then time out or exhaust connections. The migration has no disable_ddl_transaction!, so add_column (line 27) takes an ACCESS EXCLUSIVE lock and keeps it until COMMIT, while EntryCompanyBackfill rewrites every row in one unbatched UPDATE in the same transaction. lock_timeout (10s) limits only how long the migration waits to get the lock; statement_timeout (1h) caps the outage but does not prevent it. The outage grows with the table's size, which the checkout does not show. The existing rake task already runs the same idempotent service.","recorded_resolution":"fixed","recorded_correction":"The backfill now runs in its own migration, outside the transaction that adds the column. It updates one committed batch of 10,000 ids at a time. Rollback and re-migrate left db/schema.rb unchanged.","correction_target":"code_commit","source_path":"db/migrate/20261025120000_prepare_monolith_for_timekeeping.rb","source_line":31,"fix_commit":"a97027a7179a56c3cb4bf2f4196bbb48562092e2","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/a97027a7179a56c3cb4bf2f4196bbb48562092e2","finding_recorded_at":"2026-10-25T19:44:11.340Z","answer_recorded_at":"2026-10-25T20:03:31.120Z","first_independent_review_at":"2026-10-25T20:31:02Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"a34f15233c309a2d3e84efa01b8ce239e488557fbb727d1b1572bc03cf93cf8a","record_sha256":"1d58cb8681ce4f4b5055181b64d0f14595561a6053decbe0a88959a259e468b5"},
    {"record_id":"author-pr44-plan-001","pr":44,"pr_url":"https://github.com/Homie-Improvement/miru/pull/44","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"An old application process can leave company_id empty after the backfill completes.","finding_evidence":"plan item 3 backfills once, but plan item 5 only stamps writes made by the new application code. config/deploy.yml:109 configures rolling deployment. Consider an old process handling an entry creation after S2 commits but before that process stops. app/controllers/api/v1/timesheet_entry_controller.rb:31-38 saves through the current model, which has no company_id callback. The nullable column accepts NULL. The new callback never runs for that row unless it is saved again. Thus the completed deployment can violate the required company match even when every writer uses the model. Specify a rollout order that closes this interval, or reconcile company_id after old writers stop. Test a write between the initial backfill and activation of the callback, including an old writer moving an existing entry.","recorded_resolution":"fixed","recorded_correction":"The backfill SQL now lives in one service that both the migration and a rake task call. The rollout runs the task once after old containers stop, which repairs rows written in that window.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:43:46.539Z","answer_recorded_at":"2026-10-25T18:46:03.976Z","first_independent_review_at":"2026-10-25T20:31:02Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"dc3ee5cca3673a53abcf0d7601d557adbb31a5c6bbfb888af72ef150029ccf97","record_sha256":"10421c847aa213bcc84b7f630341735f827c9508b5b9a542b6fd052deb925791"},
    {"record_id":"author-pr44-plan-002","pr":44,"pr_url":"https://github.com/Homie-Improvement/miru/pull/44","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Dropping the direct billing foreign key allows a missing entry reference to be saved.","finding_evidence":"plan item 2 does not replace every reference check that S1 removes. Submit an otherwise valid invoice with a line item's timesheet_entry_id set to a nonexistent ID. app/policies/invoice_policy.rb:58 permits that field. app/models/invoice_line_item.rb:5 makes the association optional, and its validator at line 57 returns when the entry is absent. After S1, the save persists the dangling billing reference; Invoice#lock_timesheet_entries merely updates no matching entry. Today the foreign key rejects this save. The dangling reference also prevents S1 rollback from restoring that key without changing billing data. Before dropping the key, reject a supplied timesheet_entry_id whose entry does not exist, while keeping NULL valid for manual lines. Add this case to the S1 tests.","recorded_resolution":"fixed","recorded_correction":"Confirmed in app/models/invoice_line_item.rb: the association is optional and the check skips a missing entry. The plan adds existence checks for line-item entry ids and entry agent ids before the keys drop.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:43:46.539Z","answer_recorded_at":"2026-10-25T18:46:03.976Z","first_independent_review_at":"2026-10-25T20:31:02Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c25865ed1b440e8eb1d454c72bf760a1ae5b24c4735c54d052602ed3bed1d2bb","record_sha256":"00b1f42f09d20c9c3e2c854f423652ed711ea3762f7b06a32897c1279034f6c8"},
    {"record_id":"author-pr44-plan-003","pr":44,"pr_url":"https://github.com/Homie-Improvement/miru/pull/44","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Fresh databases do not receive the required ownership row.","finding_evidence":"plan item 6 states that loading db/schema.rb leaves timekeeping_ownership empty. It repairs this only when TimekeepingOwnership.current runs, but U3 states that nothing in app/ calls the model. Fresh installations therefore finish setup without the ownership record, unlike databases upgraded by migration. A later consumer cannot rely on the promised initial state. Initialize the row during database setup as well as migration, using the same defaults. Test that setup produces generation 1 and owner 'monolith' before calling current.","recorded_resolution":"fixed","recorded_correction":"db/seeds.rb now creates the ownership row, and db:prepare seeds a newly created database, so fresh setups get generation 1 owned by the monolith. A spec checks the setup call.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:43:46.539Z","answer_recorded_at":"2026-10-25T18:46:03.976Z","first_independent_review_at":"2026-10-25T20:31:02Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"448847fb9ca551d4a2aced52291ee1f816d485f7bcc7c2545c046799ede4ae5f","record_sha256":"31195b0ac5428607446ee4e7c21c4e3d524eef0515a7f7a7f80f4f0b7a75d985"},
    {"record_id":"author-pr44-plan-004","pr":44,"pr_url":"https://github.com/Homie-Improvement/miru/pull/44","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Lazy initialization does not establish the promised ownership row after schema loading.","finding_evidence":"plan item 6 says a database loaded from db/schema.rb has no ownership row. It relies on TimekeepingOwnership.current to create that row, but U3 explicitly says nothing in app/ calls the model yet. The id = 1 constraint prevents a second row; it does not ensure the first exists. Thus schema loading leaves zero rows despite R4 and the completion criteria requiring one. Specify when initialization runs for a schema-loaded database and verify that the row exists before any explicit call to current.","recorded_resolution":"fixed","recorded_correction":"The ownership row is now created explicitly: by the migration, by db/seeds.rb (which db:prepare runs on a fresh database), and by the test suite right after it empties tables.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:43:46.539Z","answer_recorded_at":"2026-10-25T18:46:03.976Z","first_independent_review_at":"2026-10-25T20:31:02Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"abd3170e82041bfa2658d5a3e64a5151f2042a9ec87877ca1531c532bb9df38b","record_sha256":"452194790f60b07bcfee5767a290fb24bac218282935faeb9bb33e7534bb8d58"},
    {"record_id":"author-pr44-plan-005","pr":44,"pr_url":"https://github.com/Homie-Improvement/miru/pull/44","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Separate ownership initialization from reading the current owner.","finding_evidence":"plan item 6 makes `TimekeepingOwnership.current` recreate a missing record as generation 1 with owner `monolith`. This treats a fresh database and lost ownership state as the same condition. `docs/architecture/timekeeping-soa.md:806-808` requires generations to rise and describes reading this record to decide whether writes are allowed. Future callers could therefore receive invented authority instead of detecting missing state. No application code calls this model yet, so the immediate cost is an unsafe interface for later work. Make `current` require the existing row. Initialize it explicitly during database setup and test preparation, using the column defaults rather than copying them.","recorded_resolution":"fixed","recorded_correction":"Reading the owner now requires the existing row and raises if it is missing. Creating the first row is a separate setup call that only inserts when the table is empty.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:43:46.539Z","answer_recorded_at":"2026-10-25T18:46:03.976Z","first_independent_review_at":"2026-10-25T20:31:02Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"04fd2ee74565e56816ed534b31795192fa8d60e2cbfa6ca073d497d4590348b6","record_sha256":"e918b70476b89b120f23273256ccb8d5cb2086ed438050b1ddf300eb8309390f"},
    {"record_id":"author-pr44-plan-006","pr":44,"pr_url":"https://github.com/Homie-Improvement/miru/pull/44","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The rollback decision promises less than the rollback requirement.","finding_evidence":"The requirements and completion criteria promise clean rollback. plan item 1 instead permits rollback to stop when orphaned agent_id values exist. The stop condition protects existing data, but it does not establish R8. State the conditions under which rollback succeeds and show how the retained write paths preserve them. If clean rollback cannot be guaranteed without changing rows, identify that as an unmet requirement rather than describing the migration as unconditionally reversible.","recorded_resolution":"fixed","recorded_correction":"The plan now says rollback succeeds when no row points at a missing entry or agent, shows how the kept write paths prevent such rows, and has the rollback count them first and stop with a clear message.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T18:43:46.539Z","answer_recorded_at":"2026-10-25T18:46:03.976Z","first_independent_review_at":"2026-10-25T20:31:02Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"0601b8a065914108c1d73635ce02cdfc6b1b1e6d49ae4dbc71e543effa5813c6","record_sha256":"8250721162eb6f4e0f6433c99f8a8bb80ad8b52a7a072a9c640d4c66fa007286"},
    {"record_id":"author-pr45-build-001","pr":45,"pr_url":"https://github.com/Homie-Improvement/miru/pull/45","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"OUTCOME_COUNTS freezes only the outer hash; its increment rows stay writable","finding_evidence":"OUTCOME_COUNTS is the decision table that maps each outcome to its [writes, reference_unavailable] increments, and every record reads its increments from it (write_metrics.rb:47). The outer hash is frozen, but the inner arrays are not, because frozen_string_literal covers only string literals. Any code that loads the class can run OUTCOME_COUNTS[:write][0] = 0, and every later record in that process then stops counting writes with no error, which skews the alert evaluator's rate and p95 inputs. The repository's convention freeze-rule-table-rows-not-only-the-outer-array (PR #41) says the shallow-freeze rule applies to any constant that holds nested arrays or hashes, and the new write_metrics_spec.rb has no frozen check.","recorded_resolution":"fixed","recorded_correction":"Each increment row is now frozen along with the outer hash, and the spec checks both. A plain Ruby check showed the old rows were writable and the new ones raise FrozenError.","correction_target":"code_commit","source_path":"timekeeping/app/services/timekeeping/write_metrics.rb","source_line":19,"fix_commit":"9b00d182e43ce210a58d6d052bd0418ff40e48ed","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/9b00d182e43ce210a58d6d052bd0418ff40e48ed","finding_recorded_at":"2026-10-25T19:53:28.697Z","answer_recorded_at":"2026-10-25T20:06:23.939Z","first_independent_review_at":"2026-10-25T20:16:49Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"79d3bdeabc669b075da2a42642f7ec5c92d753e0e3a524036212a4b83f0e5444","record_sha256":"5f6a8193fed90464de7c961fa4ee23ee753b5f506c19875302774122ab36f2ce"},
    {"record_id":"author-pr45-plan-001","pr":45,"pr_url":"https://github.com/Homie-Improvement/miru/pull/45","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Count expired-token refusals when their company can be verified from the signature.","finding_evidence":"The decision to omit expired-token refusals narrows the required 503 count. In timekeeping/lib/timekeeping/token_verifier.rb, ExpiredToken is raised only after signature verification. WriteAccess.decide then returns :reference_unavailable, but Identity.resolve discards the company. The ticket requires a valid signature for metric attribution; it does not require an unexpired token. Excluding these requests can hide a rise in refusals from the alert evaluator. Preserve a separately validated company for metrics after verifying the signature and claim types, without granting the expired caller access. Extend the planned files and tests to cover that context. Keep invalid-signature requests uncounted and keep the existing expired-token response unchanged.","recorded_resolution":"fixed","recorded_correction":"TokenVerifier now keeps the company of an expired token once the signature and claim types pass. Identity carries it in a separate field that is used only for metrics. Forged tokens stay uncounted, and the 503 answer is unchanged.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T19:26:39.259Z","answer_recorded_at":"2026-10-25T19:28:50.250Z","first_independent_review_at":"2026-10-25T20:16:49Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"4ea232a77c89050d2115c10d71069b6d2b5ce2e3d8ce807cb21bcb0be8ae949f","record_sha256":"20b5039a516f64679063c18eecb997174c737d5047ce2538f75b027cb5a5998f"},
    {"record_id":"author-pr45-plan-002","pr":45,"pr_url":"https://github.com/Homie-Improvement/miru/pull/45","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Count reference_unavailable refusals from expired tokens whose signatures are valid.","finding_evidence":"When a company's signed tokens expire, every write can return 503 while its metric rows remain unchanged. plan item 9 explicitly excludes these refusals. In timekeeping/lib/timekeeping/token_verifier.rb, ExpiredToken occurs only after signature verification. timekeeping/lib/timekeeping/identity.rb then discards the claims, and timekeeping/app/services/timekeeping/write_access.rb returns reference_unavailable. That discarded attribution is an implementation choice, not the ticket's prohibition on unverified keys. Preserve signature-verified, type-checked company attribution separately from access authorization, while keeping expired callers unauthorized. Replace U2's empty-table expectation with a test that these refusals increment only the signed company's counter. Otherwise t","recorded_resolution":"fixed","recorded_correction":"The empty-table check for expired tokens is gone. The new test expects the signed company's reference_unavailable count to rise by 1 while every other company stays unchanged.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T19:26:39.259Z","answer_recorded_at":"2026-10-25T19:28:50.250Z","first_independent_review_at":"2026-10-25T20:16:49Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"7ad78704267f11b7b68735a89ddc75e96094be70e7a8977c025a115f39eabbea","record_sha256":"22841445622d270f48c1517a3949001d4d72e58a3c1992c690e02791f8396a96"},
    {"record_id":"author-pr45-plan-003","pr":45,"pr_url":"https://github.com/Homie-Improvement/miru/pull/45","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Prevent a blocked metric update from delaying a committed write response.","finding_evidence":"Consider another connection holding the company's current metric row lock. plan item 2 makes the next upsert wait for that lock. plan item 6 runs this SQL synchronously in an around_action ensure, after rendering but before the controller returns the response. The entry transaction has already committed, yet the caller can time out waiting for its answer. The recorded latency also excludes this wait because the hook measures the yield before recording. Logging an exception does not handle a statement that is still waiting. Make persistence occur after response delivery, or explicitly bound this wait and account for the resulting request delay. Add a separate-connection test that holds the metric row lock and verifies the chosen response and latency guarantees.","recorded_resolution":"fixed","recorded_correction":"Each metric write now runs under a short PostgreSQL lock_timeout. If the row stays locked, the metric is dropped and logged, and the write's answer goes out on time. A separate-connection test checks this.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T19:26:39.259Z","answer_recorded_at":"2026-10-25T19:28:50.250Z","first_independent_review_at":"2026-10-25T20:16:49Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"b7f8b199760dd14580b4772e3479f570c7312599e8bbbb0aa3faa23e2ca60672","record_sha256":"bc83d08e76325010cf5ce9b42eb031a5664132a51564b76c3b9fcd72071cedeb"},
    {"record_id":"author-pr45-plan-004","pr":45,"pr_url":"https://github.com/Homie-Improvement/miru/pull/45","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Signed but expired tokens leave reference-unavailable refusals invisible to operators.","finding_evidence":"plan item 9 deliberately omits the 503 responses returned for validly signed but expired tokens. If these refusals dominate a company's requests, the evaluator can see no failures even though callers cannot write. The ticket requires counting these refusals and restricts company keys to tokens with valid signatures; it does not require an unexpired token for metric attribution. Preserve the verified company separately from authorization eligibility, without allowing the expired token to write. Record these refusals under that company and test that they appear at GET /internal/v1/metrics. Disclosing their omission does not make the failure rate observable.","recorded_resolution":"fixed","recorded_correction":"The plan now counts the 503 sent to a validly signed but expired token under the company in that token. The expired token still cannot write, and a request spec checks the count.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T19:26:39.259Z","answer_recorded_at":"2026-10-25T19:28:50.250Z","first_independent_review_at":"2026-10-25T20:16:49Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"eb782ab70c91aacbbc7886b6929949f41bd90a0e69bd24e96ce50144912864ae","record_sha256":"bebc592746d811fa83d14620de46881c63a154bd88fe5066bc27d58c439ef71e"},
    {"record_id":"author-pr45-plan-005","pr":45,"pr_url":"https://github.com/Homie-Improvement/miru/pull/45","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"high","finding_title":"The plan goes beyond the issue: Abort a metric update after a short lock wait and discard the event while returning the entry response.","finding_evidence":"Bounding bookkeeping delays protects existing writes, but deliberately dropping a verified company's event after ordinary lock contention overrides the requirement to record each write and can hide refusals from the evaluator. The earlier request to bound waiting does not itself authorize this loss of required counts.","recorded_resolution":"fixed","recorded_correction":"The lock timeout and the dropped-metric path are gone. Each write is now recorded by one plain upsert, which waits only for other single-statement upserts on the same row.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T19:29:44.496Z","answer_recorded_at":"2026-10-25T19:30:48.050Z","first_independent_review_at":"2026-10-25T20:16:49Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"a282953c9305285c23bbb2e2772305be71699ca05c478a8743088e88bfb3fc9b","record_sha256":"9ed2782911c4bc49b2228582c06b09052b6ad093c86e709468eb4dacf67db6d2"},
    {"record_id":"author-pr46-plan-001","pr":46,"pr_url":"https://github.com/Homie-Improvement/miru/pull/46","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The billing controls do not distinguish provisional reservations from committed reservations.","finding_evidence":"The proposed controls catch writes to unreserved entries and another invoice's entries, but not provisional entries for the requested invoice. A provider that removes the `state == \"committed\"` filter from `mark_billed` would still pass every planned billing assertion. That filter exists in spec/support/timekeeping/fake_timekeeping.rb, and docs/architecture/timekeeping-soa.md:476 limits billing to committed reservations. Add an unbilled entry provisionally reserved for `invoice_a` to the billing example. Assert its baseline and verify that it stays unbilled while x and y become billed. This makes the committed-only selection observable and prevents the strengthened contract from hiding that future defect.","recorded_resolution":"fixed","recorded_correction":"The billing example now also seeds an entry held provisionally for the same invoice, checks it starts unbilled, and checks it stays unbilled while the committed entries become billed.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-25T19:37:46.817Z","answer_recorded_at":"2026-10-25T19:38:22.168Z","first_independent_review_at":"2026-10-25T20:05:31Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"13312bb25708d8f5b56d5da375d4a4ad16e3babfe9bf68e3904c11940ed47630","record_sha256":"5e3f7ef5a765d28a202e56f30560b34499d496432291acb8bdd5ea925d0dbeca"},
    {"record_id":"author-pr51-build-001","pr":51,"pr_url":"https://github.com/Homie-Improvement/miru/pull/51","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Migration spec repeats several contracts; one example now vacuous","finding_evidence":"db/schema.rb declares the three keys again, so the test database starts with them. The plain rollback example hits `if_not_exists: true` and adds nothing, so it would still pass if down's add_foreign_key call were deleted. Several other examples and two helpers overlap, so a future change to the rollback or the key list has to be made in near-copies, and an example that looks like restore coverage proves nothing.","recorded_resolution":"fixed","recorded_correction":"The rollback example now removes the keys first, checks all three validated, then runs up. With down's add_foreign_key removed, that example fails. Overlapping examples and the table-only helper are folded.","correction_target":"code_commit","source_path":"spec/db/migrations/drop_timesheet_entry_foreign_keys_spec.rb","source_line":51,"fix_commit":"d3efb92d8aa3a6a57cf8a0f37ce3dbd37d3a4b4d","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/d3efb92d8aa3a6a57cf8a0f37ce3dbd37d3a4b4d","finding_recorded_at":"2026-10-26T00:59:27.963Z","answer_recorded_at":"2026-10-26T01:12:37.303Z","first_independent_review_at":"2026-10-26T01:15:00Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"1c64939a28cd5343d948098ce7c2660db7be44051ca5569ba761942227883c96","record_sha256":"aab3c99a662bd47e3c449a0d5d27ae5dba563e2f982fa130d2335c5e24c0a2a5"},
    {"record_id":"author-pr51-build-002","pr":51,"pr_url":"https://github.com/Homie-Improvement/miru/pull/51","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Restore runbook step 1 cannot run in its own scenario","finding_evidence":"The runbook covers a database that lost the keys while a running process lacks the validations, and gives an app-only rollback as the example. PR #44 shipped the migration file and the validations together, so a release without the validations also lacks the migration file. In that scenario every running container is the old release, so running the down 'from one container' stops with UnknownMigrationVersionError. No data changes, but the operator has no working way to restore the keys before the rolling deploy this section exists to protect. The db-migrate alias uses `app exec --reuse`, which also runs inside a running (old) container.","recorded_resolution":"fixed","recorded_correction":"Step 1 now runs the down in a one-off container of the release being deployed, never a running one or the --reuse alias. The pre-rollback case may still use a running container.","correction_target":"code_commit","source_path":"docs/architecture/timekeeping-soa.md","source_line":841,"fix_commit":"d3efb92d8aa3a6a57cf8a0f37ce3dbd37d3a4b4d","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/d3efb92d8aa3a6a57cf8a0f37ce3dbd37d3a4b4d","finding_recorded_at":"2026-10-26T00:59:27.963Z","answer_recorded_at":"2026-10-26T01:12:37.303Z","first_independent_review_at":"2026-10-26T01:15:00Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"2ac779ef5bc45107358c86c6bab7a48794577ef499cd8ca5e4aed87a14087aa6","record_sha256":"90f602d13d476f8cf779dd7f5957ac4d6cf09db3348f5b8d3887b8774ac43c16"},
    {"record_id":"author-pr51-plan-001","pr":51,"pr_url":"https://github.com/Homie-Improvement/miru/pull/51","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Require a safe deployment path when the database has already lost its keys.","finding_evidence":"Consider a database that recorded the original migration, followed by an app-only rollback to code without the validations. Deploying this change still leaves old writers unprotected during replacement: Rails skips the recorded migration, and the planned no-op cannot restore its keys. This is a new exposure during deployment, not only the historical window acknowledged in plan item 2. `bin/docker-entrypoint:10-14` runs `db:prepare` and starts the application; neither deploy configuration requires restoration first. Make restoration a prerequisite for this case, using the existing `down` pre-check before replacement starts, or require stopping old writers until validated processes replace them. If restoration finds dangling rows, stop rather than continue the rolling deployment. Document the same prerequisite for rollback to code without validations; plan item 1 cannot assum","recorded_resolution":"fixed","recorded_correction":"The plan now makes restoring the keys a release prerequisite. It uses the migration's existing rollback check before a rolling deploy. If that check finds dangling rows, the rollout stops and old writers must be stopped.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:14:22.781Z","answer_recorded_at":"2026-10-26T00:15:11.317Z","first_independent_review_at":"2026-10-26T01:15:00Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"b8edff7dcddafc174d1b9c1fc33cef453e4f490e0a7a59888bc381965c643a40","record_sha256":"501d15f5b4b97b5e1ced44c6b66281331e268de876404497623e435a37fc9ed1"},
    {"record_id":"author-pr51-plan-002","pr":51,"pr_url":"https://github.com/Homie-Improvement/miru/pull/51","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The safety guarantee excludes databases that already ran the migration without stating a deployment prerequisite.","finding_evidence":"The stated goal promises database protection until every running process has the validations. plan item 1 also says the release sequence makes every deploy route safe. But plan item 2 deliberately leaves the keys absent on databases that ran the original migration. Consider such a database after an app-only rollback to code without the validations. Deploying this release leaves those old processes writing without either protection until replacement finishes. This is a new exposure during this rollout, not only the past window that plan item 2 acknowledges. Make the guarantee consistent with this supported starting state. Require operators to restore the keys successfully or stop old writers before rollout on these databases, and include that prerequisite in the release instructions.","recorded_resolution":"fixed","recorded_correction":"The plan now limits the guarantee to databases that still have the keys. Where the keys are already gone and old code is running, the release steps require restoring them first, or a full stop of old writers.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:14:22.781Z","answer_recorded_at":"2026-10-26T00:15:11.317Z","first_independent_review_at":"2026-10-26T01:15:00Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"5c1d4bf2d7d7938a13d35ca86a7c244d3789f21cf6004fcab8750bb72971f9b3","record_sha256":"04e02b2a92702f8470da2f57b450724d57ad5f64a6d61732799c4fed8ff85af1"},
    {"record_id":"author-pr52-build-001","pr":52,"pr_url":"https://github.com/Homie-Improvement/miru/pull/52","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Controller comment promises count on first read; timeouts defer it","finding_evidence":"The controller comment tells maintainers and alert-evaluator authors that every write is counted on the first metrics read after it. The code does not guarantee that. If the company's window row is held, the fold's lock_timeout rolls that company back and returns 0. If another read is already folding, SKIP LOCKED makes this read skip those events. Either way the write is missing from that read. The specs in this change assert exactly that.","recorded_resolution":"fixed","recorded_correction":"The controller comment now says each read adds the pending events it can. Events of a company whose row is held, or that another read is already adding, stay pending until a later read counts them.","correction_target":"code_commit","source_path":"timekeeping/app/controllers/timekeeping/internal/v1/metrics_controller.rb","source_line":5,"fix_commit":"14e4ceadf52609eb978829c1a75ab420554f7667","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/14e4ceadf52609eb978829c1a75ab420554f7667","finding_recorded_at":"2026-10-26T01:02:30.310Z","answer_recorded_at":"2026-10-26T01:19:07.437Z","first_independent_review_at":"2026-10-26T01:21:03Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"d71673310251dc97cb5d1abd9b2413b8c2df6985f7ee1dcc8a1c78b7120fac1b","record_sha256":"735dc965ddbd300f285d00b7b65be3188e8724549d7812bcf55e2ed1cebdf0c0"},
    {"record_id":"author-pr52-build-002","pr":52,"pr_url":"https://github.com/Homie-Improvement/miru/pull/52","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Held-row harness and fold-while-held example duplicated across three specs","finding_evidence":"The row-holder thread is written three times and the pg_blocking_pids poll twice, so a change to how the lock is held or detected must be made in every copy, and the copies can drift. The folded_windows service example adds no coverage and never checks that the fold actually waited, so it would pass even if the fold never touched the held row.","recorded_resolution":"fixed","recorded_correction":"The row holder and the lock-wait poll now live once, in spec/support/entry_write_context.rb, and all three specs use them. The folded_windows service example without a blocked check is gone.","correction_target":"code_commit","source_path":"timekeeping/spec/services/timekeeping/write_metrics_spec.rb","source_line":225,"fix_commit":"14e4ceadf52609eb978829c1a75ab420554f7667","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/14e4ceadf52609eb978829c1a75ab420554f7667","finding_recorded_at":"2026-10-26T01:02:30.310Z","answer_recorded_at":"2026-10-26T01:19:07.437Z","first_independent_review_at":"2026-10-26T01:21:03Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"11d3beb59a481fae538e9a349e10783c195ec6d336475953d486fff73733380f","record_sha256":"97637ffdb8d603179028e90cc91a9e7e25a5dcb661c3d543c65afadd536de052"},
    {"record_id":"author-pr52-build-003","pr":52,"pr_url":"https://github.com/Homie-Improvement/miru/pull/52","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"fold_company's SET LOCAL lock_timeout outlives its savepoint and leaks into the caller's transaction","finding_evidence":"When a caller folds pending events inside a transaction it already has open, the rest of that transaction runs with the fold's 100 ms lock timeout. PostgreSQL's RELEASE SAVEPOINT does not undo SET LOCAL; the setting lasts until the outer transaction ends. So a later, unrelated write that waits more than 100 ms fails and can abort the caller's transaction. The comment at write_metrics.rb:111-112 says the opposite. No production caller nests today, but every transactional-fixture spec that folds now runs its later statements under a 100 ms lock timeout. The only lock_timeout isolation test turns transactional fixtures off, so the savepoint path is never tested.","recorded_resolution":"fixed","recorded_correction":"Confirmed by a run: after a released savepoint, SHOW lock_timeout gave 100ms in the outer transaction. The fold now puts the caller's value back after a successful fold, and a new test checks it.","correction_target":"code_commit","source_path":"timekeeping/app/services/timekeeping/write_metrics.rb","source_line":115,"fix_commit":"14e4ceadf52609eb978829c1a75ab420554f7667","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/14e4ceadf52609eb978829c1a75ab420554f7667","finding_recorded_at":"2026-10-26T01:02:30.310Z","answer_recorded_at":"2026-10-26T01:19:07.437Z","first_independent_review_at":"2026-10-26T01:21:03Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"7d89f9f310ae7e8897dde9448e0948b29c2fb0b9590ede93ffd5289f5ea216dc","record_sha256":"2ff98a3f52ae8e46c850a0da4f1ff448a17ce341b12ba61c1093f8cd7da74c46"},
    {"record_id":"author-pr52-plan-001","pr":52,"pr_url":"https://github.com/Homie-Improvement/miru/pull/52","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A timeout for each lock wait cannot enforce the stated response limit.","finding_evidence":"The fold can delay a committed entry response beyond the promised two lock waits. plan item 2 allows ten event transactions and stops only when one times out. Each successful upsert can wait just below LOCK_TIMEOUT_MS before another session releases its lock. Those waits accumulate without raising ActiveRecord::LockWaitTimeout. The existing concern calls record synchronously in timekeeping/app/controllers/concerns/timekeeping/write_metrics_recording.rb:34, so the caller bears the entire delay. plan item 4, plan item 7 and the planned concern comment would therefore promise a limit the implementation cannot enforce. Give the whole fold a shared time budget, or state and justify a bound that includes every permitted wait. Add a contention test with several successful waits; the single held-row test only checks the first timeout.","recorded_resolution":"fixed","recorded_correction":"The response path now runs no fold transactions, only one insert under one lock timeout, so successful waits can no longer pile up. The bound is one lock wait.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:15:06.571Z","answer_recorded_at":"2026-10-26T00:16:56.348Z","first_independent_review_at":"2026-10-26T01:21:03Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"52ce4e3949b7848173c91dfcd9f9ea116472769be76fec81da3273264756de3b","record_sha256":"b200348e8689ec98d4e5c89bba36658dd295f2a862a887c31be1afcb516d6e8f"},
    {"record_id":"author-pr52-plan-002","pr":52,"pr_url":"https://github.com/Homie-Improvement/miru/pull/52","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Pending counts can remain invisible after the lock clears.","finding_evidence":"Let a company's final request append its event and time out during the fold. Release the lock, then send no more writes for that company. plan item 3 provides no further fold trigger, so the event stays pending indefinitely. timekeeping/app/controllers/timekeeping/internal/v1/metrics_controller.rb reads only WriteMetricWindow, so operators never see that request in the counts used for alerts. A backlog larger than FOLD_LIMIT can also remain after the final successful recording call. Provide a retry trigger that does not depend on another company write. Test recovery after lock release without sending another request or manually invoking fold_pending.","recorded_resolution":"fixed","recorded_correction":"Folding now happens on each metrics read and covers every pending event of every company. A company's last blocked write shows up on the first read after the lock clears, with no further entry write and no manual call.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:15:06.571Z","answer_recorded_at":"2026-10-26T00:16:56.348Z","first_independent_review_at":"2026-10-26T01:21:03Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"143d8900c13cac2b51cab322184aa7906e3a5d83a4278d36192af83158c30103","record_sha256":"ec78d37ee579c082f67c04190bd6a0b48bcc1fe73b84b1b015c4ad89e6abfda9"},
    {"record_id":"author-pr52-plan-003","pr":52,"pr_url":"https://github.com/Homie-Improvement/miru/pull/52","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Pending counts can remain invisible after the metric lock clears.","finding_evidence":"plan item 3 retries pending events only when the same company writes again. If its last request meets the lock, releasing the lock never makes that event visible to the metrics reader. An operator can therefore keep seeing an understated write or failure count after contention ends. This matters especially when a company stops retrying after failures. The request test hides this case by sending another write after release. Add a retry path that does not require another company write, and test that the pending event reaches write_metric_windows after release without another entry request.","recorded_resolution":"fixed","recorded_correction":"Pending events reach the counter table on the next metrics read, not on the company's next write. A spec releases the lock and reads metrics without sending another entry request.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:15:06.571Z","answer_recorded_at":"2026-10-26T00:16:56.348Z","first_independent_review_at":"2026-10-26T01:21:03Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"29e984733f62d5a92a0ef175d5489d986704dc1befcec37c4bfacd7140b63e72","record_sha256":"a5cebd26ffcb95432791a211587ab38b6b153408cf03614a53fe266180b5cbed"},
    {"record_id":"author-pr52-plan-004","pr":52,"pr_url":"https://github.com/Homie-Improvement/miru/pull/52","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Pending events need a drain trigger that does not depend on another company write.","finding_evidence":"The proposed queue can leave counts invisible indefinitely after the lock clears. plan item 3 makes another write by the same company the only retry trigger. A public `fold_pending` method does not provide a caller that guarantees progress. The existing concern calls the recorder only during entry requests (`timekeeping/app/controllers/concerns/timekeeping/write_metrics_recording.rb:33`). The metrics endpoint reads only `WriteMetricWindow` (`timekeeping/app/controllers/timekeeping/internal/v1/metrics_controller.rb:10`), and the plan preserves that behavior. If the blocked request is the company's last write, operators keep seeing incomplete counts even after the database recovers. Give pending events an independent drain trigger, while retaining atomic delete-and-count transactions. Test recovery after lock release with","recorded_resolution":"fixed","recorded_correction":"Pending events are now folded every time the internal metrics endpoint is read, which the alert evaluator polls. Counts no longer depend on another write from the same company, and a spec checks recovery after the lock is released.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:15:06.571Z","answer_recorded_at":"2026-10-26T00:16:56.348Z","first_independent_review_at":"2026-10-26T01:21:03Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"70173849d1410d7587a35546e432bbfb3f4783598fc4b1c480ac3b5c11830033","record_sha256":"fd89005617a04903fdbd15daa6588d29fd9a120ca9359f900feacf3658c7d680"},
    {"record_id":"author-pr52-plan-005","pr":52,"pr_url":"https://github.com/Homie-Improvement/miru/pull/52","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Preserved events need a retry path that does not require another company write.","finding_evidence":"plan item 3 leaves an event pending until the same company writes again. Consider its final write timing out against a locked metric row. Releasing the lock triggers no fold, and other companies cannot drain that event. The metrics endpoint reads only `WriteMetricWindow` in `timekeeping/app/controllers/timekeeping/internal/v1/metrics_controller.rb:10`, so the preserved count remains invisible indefinitely. This does not deliver the stated goal's promise that the write is counted when the lock clears. Operators receive incomplete totals until another write or manual intervention. Add a retry path independent of new company traffic, and test lock release without sending another entry write.","recorded_resolution":"fixed","recorded_correction":"The retry path is now the metrics read, which runs whether or not the company writes again. It is tested by releasing the lock and reading metrics, with no further entry write.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:15:06.571Z","answer_recorded_at":"2026-10-26T00:16:56.348Z","first_independent_review_at":"2026-10-26T01:21:03Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"35f7b4df7ace467bca103220a5be4a2409b7d25d08946a2a68347370c524057d","record_sha256":"3300cf0226faf809f0aa7ad4f4448901564432ee672d3056c2cc0817157abbaf"},
    {"record_id":"author-pr52-plan-006","pr":52,"pr_url":"https://github.com/Homie-Improvement/miru/pull/52","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Releasing the lock does not trigger the promised eventual counting.","finding_evidence":"The stated goal and completion criteria promise that a blocked event is counted once the lock clears. plan item 3 leaves it pending until another write from the same company, and no unit supplies another trigger. If that company makes no further writes, the metrics reader continues to omit its committed event after contention ends. Both lock-hold tests hide this gap by explicitly calling the fold or sending another write. Provide a drain trigger independent of another entry write, and verify that a pending event becomes visible after lock release without further company traffic.","recorded_resolution":"fixed","recorded_correction":"The promise now matches the design: a blocked event is counted on the first metrics read after the lock clears. The request spec checks this through the metrics endpoint alone, with no extra write.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:15:06.571Z","answer_recorded_at":"2026-10-26T00:16:56.348Z","first_independent_review_at":"2026-10-26T01:21:03Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"6855b2827c3a26fd8e0d6b4e2c0912cacbb878d5fc319abaa3aeeb7ed2079ef1","record_sha256":"a36fd318b71a894d9c2b94751637d87cf9743bcf3f620d7bc73af601de2c2fad"},
    {"record_id":"author-pr52-plan-007","pr":52,"pr_url":"https://github.com/Homie-Improvement/miru/pull/52","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Successful lock waits can exceed the promised response limit.","finding_evidence":"With ten pending events, competing writers can make each fold wait just under LOCK_TIMEOUT_MS before acquiring the window row. Every fold succeeds, so the stop-on-timeout rule never applies. The request accumulates ten waits, not the two promised by plan item 4 and plan item 7. Because recording remains synchronous in timekeeping/app/controllers/concerns/timekeeping/write_metrics_recording.rb, users wait for all ten folds before receiving their committed response. Enforce one total lock-wait budget across the recording call, or move backlog processing outside response delivery. Add a test with repeated successful waits; the proposed continuously held lock tests only the first timeout.","recorded_resolution":"fixed","recorded_correction":"Entry responses no longer fold at all. They do one insert of a new pending row, which never waits on the shared counter row, so at most one lock wait is added to a response.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:15:06.571Z","answer_recorded_at":"2026-10-26T00:16:56.348Z","first_independent_review_at":"2026-10-26T01:21:03Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"3bfe143c3293c8b3ff2ca4be48437ced7fd2f9bf4af5172043055b5fcb29a58f","record_sha256":"e94d5585e66a86aaa7ff03c19087cd993aa9bcbad1b33cd5770f2081f819c03b"},
    {"record_id":"author-pr52-plan-008","pr":52,"pr_url":"https://github.com/Homie-Improvement/miru/pull/52","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The per-event timeout does not enforce the promised response limit.","finding_evidence":"plan item 4 and the completion criteria promise at most two lock waits per response. plan item 2 instead permits ten separate fold transactions, each with its own lock timeout. With a backlog, several folds can each wait just below the timeout and succeed. The request then exceeds the promised bound without any timeout stopping the loop. Operators also cannot see this added delay in the recorded latency. Define and enforce one cumulative lock-wait budget for the recording call, or limit synchronous folding so the stated bound holds. Add a test with several pending events and repeated waits that succeed before each timeout.","recorded_resolution":"fixed","recorded_correction":"The fold loop has left the entry response path. Each response makes one insert under one lock timeout, so the stated limit of one wait holds by construction.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:15:06.571Z","answer_recorded_at":"2026-10-26T00:16:56.348Z","first_independent_review_at":"2026-10-26T01:21:03Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"689a8f9b0117b9729c1ca56392c7077a7dfae07ac2ff9a7b2fdcef06ed6f974d","record_sha256":"f80d91a874e718fb3f042df12058cedf19cdb08d021843ad71e82be5ecaf2e96"},
    {"record_id":"author-pr52-plan-009","pr":52,"pr_url":"https://github.com/Homie-Improvement/miru/pull/52","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The plan replaces independent response delivery with bounded synchronous persistence.","finding_evidence":"R2 promises independent response delivery, but plan item 4 explicitly says the design does not provide it. plan item 5 and U3 retain the synchronous call in `timekeeping/app/controllers/concerns/timekeeping/write_metrics_recording.rb:33`. A committed entry response must still wait for the pending-event insert and counter updates. The lock timeout limits individual lock waits, not statement execution or the entire recording call. This leaves response delivery dependent on metric persistence during database contention. Plan a delivery boundary that removes counter updates from the response path while retaining durable events. Rejecting `rack.after_reply` explains why one mechanism is unsuitable; it does not justify dropping the ticket's requested separation.","recorded_resolution":"fixed","recorded_correction":"Counter updates are off the entry response path. The response saves only a durable pending event, and the metrics endpoint folds pending events into the counters, so entry responses never wait on counter rows.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:15:06.571Z","answer_recorded_at":"2026-10-26T00:16:56.348Z","first_independent_review_at":"2026-10-26T01:21:03Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c28af9fcf44a037095ccf1c5e527e76a983df200f4613911d772462ec393786a","record_sha256":"1a3fcf881fa2e2a9eb2941d950fe6899c3f9631d2b2e9fd6e292159e58bae0d1"},
    {"record_id":"author-pr52-plan-010","pr":52,"pr_url":"https://github.com/Homie-Improvement/miru/pull/52","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"high","finding_title":"The plan goes beyond the issue: Allow a pending-event insert timeout or database failure to lose the metric event after logging it.","finding_evidence":"Retaining the existing outage backstop is not itself extra scope, but explicitly permitting the newly introduced append lock timeout to discard an event contradicts the requested timeout preservation. Disclosure does not establish an exception; the author must address this loss path or obtain agreement to narrow the preservation promise.","recorded_resolution":"fixed","recorded_correction":"The append no longer has a lock timeout, so a timeout can no longer discard an event. Only an outright database failure is still logged and lost, as the existing backstop does today.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:17:29.320Z","answer_recorded_at":"2026-10-26T00:18:20.012Z","first_independent_review_at":"2026-10-26T01:21:03Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"b90e0a40c12ba41bc48df75142a2cdadc8b43fa0d0c24822082dc174c8449833","record_sha256":"b0a7dc523e4c578385ce0871f62db263eeb2ee67b6cb09bb2a1a57d3bea86dac"},
    {"record_id":"author-pr53-build-001","pr":53,"pr_url":"https://github.com/Homie-Improvement/miru/pull/53","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Snapshot timestamps go on the wire as Time#to_s, not ISO8601","finding_evidence":"If the json gem's generic Object#to_json applies on the JSON.generate path, created_at, updated_at and discarded_at reach the projection as \"2026-03-02 09:00:00 UTC\". That is not ISO 8601, it drops sub-second precision, and it differs from the stored entry and from the iso8601 the service uses elsewhere. No spec asserts the wire format.","recorded_resolution":"fixed","recorded_correction":"Under the locked gems, JSON.generate wrote updated_at as 2026-03-02 09:00:00 UTC and lost its microseconds. The source now sends timestamps as ISO 8601 UTC with microseconds, and a new spec pins that.","correction_target":"code_commit","source_path":"timekeeping/app/services/timekeeping/relay_outbox_source.rb","source_line":32,"fix_commit":"2616c3176793ea2756516398ad938192396f2fce","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/2616c3176793ea2756516398ad938192396f2fce","finding_recorded_at":"2026-10-26T01:27:37.550Z","answer_recorded_at":"2026-10-26T01:59:28.434Z","first_independent_review_at":"2026-10-26T03:27:54Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"cc22d37c5940869a4e8fde9e1596277fa660a22e6c1f4ef8447337b97937d13e","record_sha256":"2831c4edadeb5223b8ed9f62c2e82f420cdf004001256767fc7fc8d5111ce173"},
    {"record_id":"author-pr53-build-002","pr":53,"pr_url":"https://github.com/Homie-Improvement/miru/pull/53","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Undelivered 200 answers halt the relay with no log line","finding_evidence":"A 200 that is not a delivery (a proxy page, a truncated body, a count mismatch) stops every 10-second run at the same batch and marks nothing. SnapshotEndpoint logs only non-200 statuses and RelayJob discards the result, so lag grows for every company with no log line until the 5-minute lag alert fires.","recorded_resolution":"fixed","recorded_correction":"The endpoint wrapper now logs every answer the relay will not count as delivered, not only non-200s. A new test shows a proxy page or wrong-count 200 logs status=200 reason=unrecognized, while a real delivery logs nothing.","correction_target":"code_commit","source_path":"timekeeping/app/services/timekeeping/snapshot_endpoint.rb","source_line":25,"fix_commit":"2616c3176793ea2756516398ad938192396f2fce","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/2616c3176793ea2756516398ad938192396f2fce","finding_recorded_at":"2026-10-26T01:27:37.550Z","answer_recorded_at":"2026-10-26T01:59:28.434Z","first_independent_review_at":"2026-10-26T03:27:54Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"bda6c76348eea4e7d4dd8a694b5231d1da7436e46f4bb59f21a08486ec3c3ce1","record_sha256":"bce7b45e827204bb36dc277f1078e247e5006b81b49ca48ae73be2edbf51c71c"},
    {"record_id":"author-pr53-plan-001","pr":53,"pr_url":"https://github.com/Homie-Improvement/miru/pull/53","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Derive retry keys from the snapshot content, not positions alone.","finding_evidence":"plan item 5 gives different outboxes the same retry key when their local positions match. Consider a delivered batch at position 1 for one company under generation 2. After ownership changes, another application's outbox delivers position 1 for that company under generation 3, using the same service identity. Both batches get the same key despite different bodies. spec/support/timekeeping/idempotency_receipts.rb scopes receipts by company, actor, operation and key; it rejects a changed body with key_reused. The new owner's relay then stops without marking anything, and later snapshots cannot reach the projection. docs/architecture/timekeeping-soa.md sections 5.4 and 11.2 describe separate owner outboxes and a shared service credential. Derive the UUID from a canonical representation of the complete batch,","recorded_resolution":"fixed","recorded_correction":"The delivery key is now a name-based UUID over the canonical JSON of the whole batch, so two batches with different content can never share a key.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:16:11.363Z","answer_recorded_at":"2026-10-26T00:17:44.334Z","first_independent_review_at":"2026-10-26T03:27:54Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"02d2e9f7feb6be45651587f84a135b012936e6ce2ff0cf9f36ab0dd71d5fea16","record_sha256":"9b53ed7b8847fbb0f4624805c784c16502111da7414f4f73a41d106c48cbff07"},
    {"record_id":"author-pr53-plan-002","pr":53,"pr_url":"https://github.com/Homie-Improvement/miru/pull/53","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Expect the stored response when retrying with the same key.","finding_evidence":"The lost-mark test expects FakeRelay to answer with all rows ignored on retry. plan item 5 reuses the same key and body, so that is not the response this fake returns. FakeRelay#call invokes IdempotencyReceipts#answer, which replays the original stored 200 before calling apply again. If the first response reported applied: 1 and ignored: 0, the retry reports those same counts. Update this scenario to assert the stored response, unchanged projection and successful sent marking. Use a different key in a separate scenario to exercise the consumer's version-based duplicate rejection.","recorded_resolution":"fixed","recorded_correction":"A same-key retry now expects the stored response, an unchanged projection and successful marking. A separate scenario with a different batch body checks the consumer's version-based duplicate handling.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:16:11.363Z","answer_recorded_at":"2026-10-26T00:17:44.334Z","first_independent_review_at":"2026-10-26T03:27:54Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f70fb817827d0308b68042262a9d17e45257f3c1b52b16274a1334f0aa24af53","record_sha256":"c1f9cab1d249ba86070b9ffb6aea6032c52bd384a79f41aa7f173636237ebfe8"},
    {"record_id":"author-pr53-plan-003","pr":53,"pr_url":"https://github.com/Homie-Improvement/miru/pull/53","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Keep the outbox lag calculation out of the metrics controller.","finding_evidence":"plan item 11 makes the controller select unsent rows and group their oldest enqueue times by company. This places the rule that defines outbox lag in the HTTP layer. The existing controller at timekeeping/app/controllers/timekeeping/internal/v1/metrics_controller.rb only retrieves ordered windows and hands them to a presenter. Put the new selection and aggregation behind a named query on OutboxSnapshot, and pass its result to the presenter. This keeps the controller as a thin handoff without adding another service or framework.","recorded_resolution":"fixed","recorded_correction":"The lag selection and grouping move to a named query on OutboxSnapshot. The controller passes the query's result to the presenter, as it already does for the metric windows.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:16:11.363Z","answer_recorded_at":"2026-10-26T00:17:44.334Z","first_independent_review_at":"2026-10-26T03:27:54Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"84145557cf444a766493e6188c6058bc64d5de075b815c43f22c69ff80c4fd68","record_sha256":"8677f86926d936cb5a26817192f0a9c53cd7e35836de30c834c13a25eba40e37"},
    {"record_id":"author-pr53-plan-004","pr":53,"pr_url":"https://github.com/Homie-Improvement/miru/pull/53","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Map response reasons to fixed local values before logging them.","finding_evidence":"plan item 12 logs the endpoint's `reason` value, but plan item 3 does not restrict that value to known contract errors. A failed response such as `401 {\"reason\":\"Bearer <service token>\"}` would therefore copy a credential into the worker log. Omitting the rest of the response body does not prevent this leak. The existing `timekeeping/app/services/timekeeping/reference_client.rb` reports HTTP failures using only their status. Preserve that boundary: map recognized status-and-reason pairs to fixed local values and use a fixed fallback for other responses. Add a test whose response reason contains the configured credential and a newline, and prove neither reaches the log.","recorded_resolution":"fixed","recorded_correction":"The relay maps each response to a fixed local reason from the contract's error list, or to unrecognized. A new test proves a credential with a newline in the response reason never reaches the log.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:16:11.363Z","answer_recorded_at":"2026-10-26T00:17:44.334Z","first_independent_review_at":"2026-10-26T03:27:54Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"dd2a1e9115ae7cf326ba05f28219b715b58b3dbcea9ccee3d91f7c48dc6163e2","record_sha256":"4f1d5dfbd16c440d53066e4f383a683fdef3e2c5a4e20d722b7bea7a60167fe6"},
    {"record_id":"author-pr53-plan-005","pr":53,"pr_url":"https://github.com/Homie-Improvement/miru/pull/53","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The fake returns the original counts when a retry uses the same idempotency key.","finding_evidence":"The lost-mark test cannot produce the planned response with all rows counted as ignored. plan item 5 gives the retry the same key and body. In spec/support/timekeeping/fake_relay.rb, FakeRelay#call passes that request through IdempotencyReceipts#answer. In spec/support/timekeeping/idempotency_receipts.rb, that method returns the stored response before calling apply again. If the first response counted rows as applied, the retry returns those same counts. Test that this replay leaves the projection unchanged and lets the relay mark the rows sent. Use a separate request with a fresh key to test that duplicate snapshots are counted as ignored.","recorded_resolution":"fixed","recorded_correction":"The lost-mark retry test now expects the fake's stored first answer, an unchanged projection and the rows marked sent. A separate test resends the same rows in a new batch and expects them counted ignored.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:16:11.363Z","answer_recorded_at":"2026-10-26T00:17:44.334Z","first_independent_review_at":"2026-10-26T03:27:54Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f330f0ef510520f913f791c1b3d43ce13fc81a41b2c4a0c10e16d23e4b68a029","record_sha256":"89f425e481d7199e464007e6cd24fca10d8dd0bd370dd8c70eff56211d4b6a2b"},
    {"record_id":"author-pr53-plan-006","pr":53,"pr_url":"https://github.com/Homie-Improvement/miru/pull/53","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The plan gives two different responses for the same retry.","finding_evidence":"plan item 5 describes the consumer replaying a stored 200 for an existing Idempotency-Key. However, plan item 3 and the U2 duplicate test expect a retry after a lost mark to return all rows as ignored. A stored response from the first successful delivery can instead report rows as applied. Both responses satisfy plan item 3's count check, but they require different fake behavior and test expectations. State which response the existing endpoint contract requires. Align the retry tests with that response, and distinguish replaying the same key from submitting duplicate rows under a new key.","recorded_resolution":"fixed","recorded_correction":"The plan now separates the two cases. A retry with the same key replays the stored 200. A duplicate row sent in a different batch is counted ignored. The tests match each case.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:16:11.363Z","answer_recorded_at":"2026-10-26T00:17:44.334Z","first_independent_review_at":"2026-10-26T03:27:54Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f0c259472802cf283513f9f85a21004041e7de32ac45f6975152b8dd5d3eaf1b","record_sha256":"a8e9a3b1d7c5327ea5ade4415b54627113402ece42d38c511c1e13dd26818c4c"},
    {"record_id":"author-pr53-plan-007","pr":53,"pr_url":"https://github.com/Homie-Improvement/miru/pull/53","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Two outbox sources can share a delivery key and leave entries undelivered.","finding_evidence":"plan item 5 derives the delivery key only from positions, but positions belong to each application's database. A Timekeeping batch at position 1 and a monolith batch at position 1 therefore receive the same key despite containing different entries. With the stored-response replay described in plan item 5, the second batch can receive the first batch's 200 and be marked sent without updating the projection. The plan establishes no source-specific scope for that stored response. Derive the key from the complete immutable batch, or establish a source-specific scope that prevents this collision. Add a test with two sources that share positions but contain different entries, and prove both reach the projection.","recorded_resolution":"fixed","recorded_correction":"The key now comes from the full batch content, not positions alone. A new gem test delivers two sources with matching positions but different entries and proves both reach the projection.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:16:11.363Z","answer_recorded_at":"2026-10-26T00:17:44.334Z","first_independent_review_at":"2026-10-26T03:27:54Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c36973b5ae3923c79ca359319bf7ccfd6cee31e7e8919fae558179f2fd0d18b5","record_sha256":"01fea65d23b0ab1ba2bc21f0178679852feaba8e8ad3cd08741e0bfaa856f6ed"},
    {"record_id":"author-pr53-plan-008","pr":53,"pr_url":"https://github.com/Homie-Improvement/miru/pull/53","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"medium","finding_title":"The plan goes beyond the issue: Drain ordered batches and add a failure field to the relay's public result.","finding_evidence":"Batch traversal and input validation implement delivery, but the additional failure field changes the explicitly specified public result for logging that the issue does not request. Keep the stated result shape or obtain agreement for the interface extension.","recorded_resolution":"fixed","recorded_correction":"The relay's result is back to the issue's shape, {sent:, last_position:}. A Timekeeping endpoint wrapper now logs a failed delivery's status and fixed reason, so the relay's return value carries no extra key.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:18:31.462Z","answer_recorded_at":"2026-10-26T00:19:59.032Z","first_independent_review_at":"2026-10-26T03:27:54Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"b0dee85d75c952761b766696337a1b14b2c78fde2e0aa17207a2f57131e1a879","record_sha256":"b56ed3cb51f0cc6a8f49079d654c7cfd41012ff3e3b34aff22ce9dd3c22e62a1"},
    {"record_id":"author-pr55-build-001","pr":55,"pr_url":"https://github.com/Homie-Improvement/miru/pull/55","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Expired snapshot receipts are never purged; table grows unbounded","finding_evidence":"Every keyed 200 delivery inserts a receipt row. The only delete runs when the same scope and key arrive again after expiry, and the relay sends a fresh key with every batch. So the table and its unique array index grow by one row per batch for as long as the projection runs, while the contract retains receipts for only 24 hours. Nothing queries the expires_at index the migration adds.","recorded_resolution":"fixed","recorded_correction":"Each keyed delivery now deletes expired receipts through the expires_at index, under the receipt lock. A new spec shows an expired receipt under another key is removed and a live one is kept.","correction_target":"code_commit","source_path":"app/services/timekeeping/snapshot_delivery_receipts.rb","source_line":43,"fix_commit":"d2802cbd654749e85eff4ffa5564d16fae671888","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/d2802cbd654749e85eff4ffa5564d16fae671888","finding_recorded_at":"2026-10-26T01:40:11.823Z","answer_recorded_at":"2026-10-26T02:09:34.421Z","first_independent_review_at":"2026-10-26T02:25:29Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"eb0f0ea31af2c6e946f7be4eec098c6c6b4be3365eafb60f328049d88af96311","record_sha256":"d585d337765c2b5fa48b623f727398cd68d7ed0c4ca17349969428b395bde967"},
    {"record_id":"author-pr55-build-002","pr":55,"pr_url":"https://github.com/Homie-Improvement/miru/pull/55","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Offboarding an employee breaks administrators' historical entry reads","finding_evidence":"Once Timekeeping owns entries, an administrator who opens a former employee's historical entries gets a generic 500 on both the time-tracking index and the entry list. In the legacy state the same request lists them. company.users still finds a kept user whose employment was discarded, but UserToken.mint accepts only users with a kept employment and raises UserToken::NotInCompanyError. Neither EntryApiClient nor ErrorHandler rescues that error. The fake entry client the request specs use does not mint a token, so no test reaches this path.","recorded_resolution":"fixed","recorded_correction":"The read token is now minted for the signed-in reader, and the selected user only filters the list. A new spec reads a user whose employment was discarded, as an admin, and finds the token names the admin.","correction_target":"code_commit","source_path":"app/services/timekeeping/entry_api_client.rb","source_line":31,"fix_commit":"d2802cbd654749e85eff4ffa5564d16fae671888","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/d2802cbd654749e85eff4ffa5564d16fae671888","finding_recorded_at":"2026-10-26T01:40:11.823Z","answer_recorded_at":"2026-10-26T02:09:34.421Z","first_independent_review_at":"2026-10-26T02:25:29Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"34cfe4009a6d5513becd5efb8f3d5843c7cb464823af00963b51a93b0bf0cede","record_sha256":"1426fa4d32ec4034e57c4b206dfddc0a94fc595223d5663ebb0c5d358b580c50"},
    {"record_id":"author-pr55-build-003","pr":55,"pr_url":"https://github.com/Homie-Improvement/miru/pull/55","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Owner read failure returns an unhandled 500, not the API's JSON error","finding_evidence":"When Timekeeping owns entries, any timeout, non-200 answer or bad JSON from its entry API raises EntryApiClient::Error out of both time-tracking read routes. Nothing rescues it, so the frontend gets Rails' default 500 page instead of the `{ errors: message }` JSON that every other API failure returns. Before this change these reads could not fail this way.","recorded_resolution":"fixed","recorded_correction":"An unreadable Timekeeping now answers both read routes with a JSON 503 `{ errors: message }`. The client also wraps socket, TLS and protocol errors, and the reads still never fall back to the projection.","correction_target":"code_commit","source_path":"app/services/timekeeping/entry_api_client.rb","source_line":34,"fix_commit":"d2802cbd654749e85eff4ffa5564d16fae671888","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/d2802cbd654749e85eff4ffa5564d16fae671888","finding_recorded_at":"2026-10-26T01:40:11.823Z","answer_recorded_at":"2026-10-26T02:09:34.421Z","first_independent_review_at":"2026-10-26T02:25:29Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"eeced230cbc12cf84a7b75f0c6f3f274d8b3501e1814f6e6239bba1b9e893d40","record_sha256":"8db23328634274df04d838fe7f7169e62ccaccde958a48cf4c33f1254788d8d0"},
    {"record_id":"author-pr55-plan-001","pr":55,"pr_url":"https://github.com/Homie-Improvement/miru/pull/55","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A second cutover attempt sends legacy reads to the prepared tables.","finding_evidence":"Abort the first cutover after generation rises from 1 to 2. Start another cutover, which raises it to 3 while the monolith remains the owner. plan item 6 now selects :prepared, although no successful flip has moved reads away from timesheet_entries. The entries table can be empty or contain an abandoned checkpoint, so users see missing or stale entries. docs/architecture/timekeeping-soa.md:806 says aborted handoffs retain the raised generation. Its abort procedure at line 949 keeps the outgoing data source unchanged. Select the monolith's storage source from state that records a completed ownership transition, not a generation threshold. Test repeated aborted attempts and reads during the next attempt.","recorded_resolution":"fixed","recorded_correction":"Agreed. The ownership row now stores which monolith tables are live, legacy or prepared. An aborted handoff leaves it unchanged, and a test covers repeated aborted attempts.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:17:41.441Z","answer_recorded_at":"2026-10-26T00:21:37.436Z","first_independent_review_at":"2026-10-26T02:25:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"235a99e8a2c3c01079138d80637513d1ac981e7588dae826a305a601cf86cdbd","record_sha256":"390d2077d844abb90007b26e83c47ad0bdca200cb16860ecb687aff753aac5e0"},
    {"record_id":"author-pr55-plan-002","pr":55,"pr_url":"https://github.com/Homie-Improvement/miru/pull/55","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"An inconsistent project id exposes projected data to another company.","finding_evidence":"Deliver a new id with company_id A, project_id belonging to B, and A's private note. plan item 4 explicitly omits the project-company check and skips model callbacks. Both foreign keys can pass, so the row is stored. Company#timesheet_entries follows clients and projects, not the stored company_id. Reports::TimeEntries::ReportService#search_timesheet_entries and #group_by_total_duration use that association, so B receives A's entry and duration. The scoped hydration in plan item 7 cannot protect these unchanged report reads. TimesheetEntry#assign_company_from_project currently prevents this disagreement on model saves. Validate the project-company relationship before projection writes, reject the whole inconsistent batch, and test that B's reports never include it. Avoiding a stalled relay does not justify exposing another company's data.","recorded_resolution":"fixed","recorded_correction":"Agreed. Before any write, each snapshot's project must belong to its company_id. Otherwise the whole batch is refused, and a test shows the other company's report never shows the entry.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:17:41.441Z","answer_recorded_at":"2026-10-26T00:21:37.436Z","first_independent_review_at":"2026-10-26T02:25:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"efcadc301e7c79071bad16635ae0d0277465fb2efdbf05959a3b5ee56ec7f496","record_sha256":"6c35e6ef21afa89202fd581c43e4ddfb99a7f2c45f0f11b6503fbe5cb0d9faaa"},
    {"record_id":"author-pr55-plan-003","pr":55,"pr_url":"https://github.com/Homie-Improvement/miru/pull/55","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Do not defer the owner's review status while returning a fabricated value.","finding_evidence":"plan item 7 substitutes `not_required` for every Timekeeping entry because contracts/timekeeping/entry_api.yml omits `review_status`. This drops an existing response value rather than preserving the owner's record. app/models/timesheet_entry.rb includes `review_status` in `snippet` and sets agent entries to `pending_review`. After ownership moves, those entries would falsely tell users that review is not required; approved and rejected entries also lose their actual status. Include the contract change and owner-side status in this work, or establish another authoritative way to obtain it. Test actual status values across owner states, not only JSON keys and value types.","recorded_resolution":"fixed","recorded_correction":"Agreed. Timekeeping already stores review_status, so the plan adds it to the entry list contract and to its presenter. The reader shows the owner's real value.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:17:41.441Z","answer_recorded_at":"2026-10-26T00:21:37.436Z","first_independent_review_at":"2026-10-26T02:25:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"63eb61f6915c6cc9fd5d4dc52cae92f29ed51298e207dd26b2fb98dca5ed2fd7","record_sha256":"919976f7b4c8db487f2418a7923d616cec79d28a08daeb6f1d42d9619c9b9c5b"},
    {"record_id":"author-pr55-plan-004","pr":55,"pr_url":"https://github.com/Homie-Improvement/miru/pull/55","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Generation alone cannot identify the prepared monolith after repeated aborted cutovers.","finding_evidence":"plan item 6 assumes that monolith ownership at generation 3 or higher proves a completed rollback. `docs/architecture/timekeeping-soa.md` section 10.1 says every handoff raises the generation, and section 10.5 says abort keeps that raised generation. Start at generation 1 and abort two first-cutover attempts: the legacy monolith still owns the data at generation 3. The proposed reader then selects `entries`, which remains empty, rather than `timesheet_entries`. Users lose sight of their existing time entries until the state is corrected. Establish a durable distinction between legacy and prepared ownership rather than inferring it solely from generation. Test two aborted first-cutover attempts before any successful flip.","recorded_resolution":"fixed","recorded_correction":"Agreed. The legacy or prepared choice now comes from a stored column on the ownership row, not from the generation. Two aborted first cutovers still read timesheet_entries.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:17:41.441Z","answer_recorded_at":"2026-10-26T00:21:37.436Z","first_independent_review_at":"2026-10-26T02:25:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"7a63fe4559766aff3393998d5155a4710e5e16ff422021df930d73097df4da38","record_sha256":"7baff79484a839f24cac927801f326132ecf8ed54585e734e9d8e3dfdc1c7c51"},
    {"record_id":"author-pr55-plan-005","pr":55,"pr_url":"https://github.com/Homie-Improvement/miru/pull/55","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Reading from Timekeeping must preserve each entry's review status.","finding_evidence":"plan item 7 gives every entry read from Timekeeping the status `not_required`, because its list response omits `review_status`. An entry awaiting review, approved, or rejected would therefore appear to require no review. Users could act on that false status during normal use. Matching response keys and types in U5 will not catch this change in meaning. Supply the actual status from the current owner before enabling this read path, rather than deferring the contract change. Test entries with distinct review statuses through both index routes and assert their values, not only their response shapes.","recorded_resolution":"fixed","recorded_correction":"Agreed. The list response now carries review_status from Timekeeping's stored entry, and the request spec checks pending_review, approved and rejected values through both index routes.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:17:41.441Z","answer_recorded_at":"2026-10-26T00:21:37.436Z","first_independent_review_at":"2026-10-26T02:25:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"fb34e16ab9514440f47cd9761799a9ab34f4001cc09fe1dbf79920c5309ff9e2","record_sha256":"b84d9b9b95a8d973caa89eb77287966d3a02eb83873f31b81136a5baffa75598"},
    {"record_id":"author-pr55-plan-006","pr":55,"pr_url":"https://github.com/Homie-Improvement/miru/pull/55","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The endpoint must implement the existing snapshot replay contract.","finding_evidence":"Deferring receipts drops behavior already required by contracts/timekeeping/idempotency.yml and spec/contracts/timekeeping/idempotency_examples.rb. The snapshot examples require the first answer on replay and reject a changed batch under the same key. Under the plan, a reused key with version 2 updates a row that the contract requires to remain at version 1. A successful delivery retried after an ownership flip can also return 409 instead of its original 200, preventing the relay from advancing after a lost response. Include scoped receipts and run the snapshot idempotency examples against the real endpoint. Disclosure does not satisfy this checked-in contract.","recorded_resolution":"fixed","recorded_correction":"Agreed, contracts/timekeeping/idempotency.yml covers snapshot deliveries. The plan now adds scoped receipts and runs the shared snapshot idempotency examples against the real endpoint.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:17:41.441Z","answer_recorded_at":"2026-10-26T00:21:37.436Z","first_independent_review_at":"2026-10-26T02:25:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"b4c019cc1a6f04e9b20d9bd2a9b662a4c341d519c8220e88fc3b7eab0dd5c94e","record_sha256":"46609bbf9b5db2eb975dea3825b0c81af0b891d180d37e3be4b82958cda0c3ee"},
    {"record_id":"author-pr55-plan-007","pr":55,"pr_url":"https://github.com/Homie-Improvement/miru/pull/55","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The legacy paused-delivery test requires an impossible database state.","finding_evidence":"U5 requires both reads to show a newly created entry while `timesheet_entries` has no corresponding row in every owner state. plan item 8 creates the legacy entry through the existing POST route. `app/controllers/api/v1/timesheet_entry_controller.rb` builds and saves a `TimesheetEntry` directly, so that successful request necessarily creates the row U5 requires to be absent. The specified verification cannot pass. Limit the absent-projection assertion to Timekeeping and prepared ownership. In the legacy case, assert that the directly stored row appears in both reads without relay delivery.","recorded_resolution":"fixed","recorded_correction":"Agreed, the legacy POST stores the row directly. The legacy case now asserts that the stored row shows in both reads, and the absent-row check stays on the other two owner states.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:17:41.441Z","answer_recorded_at":"2026-10-26T00:21:37.436Z","first_independent_review_at":"2026-10-26T02:25:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"2616a70733b07002ece8a0adb4afb9ca70e698cec74a576fb90e4545f34e4c27","record_sha256":"dd82e621400fdcc5025e04c8701ff6f2285fe65b20e8d0f9818debaadd25bcef"},
    {"record_id":"author-pr55-plan-008","pr":55,"pr_url":"https://github.com/Homie-Improvement/miru/pull/55","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The legacy paused-delivery test requires the created row to be both present and absent.","finding_evidence":"The legacy branch cannot satisfy the test declared in U5. plan item 8 creates its entry through the monolith's POST route, and Scope Boundaries says writers still write directly to `timesheet_entries`. U5 nevertheless requires both reads to show that entry while `timesheet_entries` has no corresponding row, in every owner state. Keep the absent-projection assertion for the prepared and Timekeeping states only. For the legacy state, assert that POST creates the local row and both reads return it. This makes the verification consistent with the three storage paths it must prove.","recorded_resolution":"fixed","recorded_correction":"Agreed. The no-projection-row check now applies only to the prepared and Timekeeping states. In the legacy state, the test checks that the created row shows in both reads.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:17:41.441Z","answer_recorded_at":"2026-10-26T00:21:37.436Z","first_independent_review_at":"2026-10-26T02:25:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"6e37ddb005297acf3530b3eb162be9cf9f8758e4cc9ca3eb21e39edd97a9c268","record_sha256":"e5ea26deeb78e2e8a5c67aef1cec6ff2c76470968b8e06184a1e79e5b92cdac0"},
    {"record_id":"author-pr55-plan-009","pr":55,"pr_url":"https://github.com/Homie-Improvement/miru/pull/55","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The single upsert cannot accept multiple snapshots for the same entry.","finding_evidence":"Pause delivery, create entry 42, then update it before resuming delivery. In spec/support/timekeeping/fake_timekeeping.rb, write_entry appends both versions to the outbox. FakeRelay#next_batch sends all unsent snapshots together. plan item 4 puts both snapshots into one INSERT ... ON CONFLICT DO UPDATE statement. PostgreSQL rejects a statement that affects the same conflict row twice, so this ordinary batch fails and the relay cannot advance. Define how repeated ids are processed in outbox order while preserving applied and ignored counts. Do not pass duplicate conflict keys into the same upsert. Add a test that delivers a create and update together through the fake relay.","recorded_resolution":"fixed","recorded_correction":"Agreed. Snapshots run one by one in outbox position order, each with its own guarded upsert, so a repeated entry id is applied in order. The counts stay exact.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:17:41.441Z","answer_recorded_at":"2026-10-26T00:21:37.436Z","first_independent_review_at":"2026-10-26T02:25:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"4d19ec18c8b860f9c5a2b7814bee2b469efa95364afaaf4c22c21a5ad11c46e4","record_sha256":"6c76af87932afa807293dca7de53991187850cd7c337af968e33dac3907484d8"},
    {"record_id":"author-pr55-plan-010","pr":55,"pr_url":"https://github.com/Homie-Improvement/miru/pull/55","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The single-upsert design cannot consume a normal batch with repeated entry IDs.","finding_evidence":"Snapshot delivery must handle several changes to one entry before delivery resumes. `spec/support/timekeeping/fake_timekeeping.rb` appends a snapshot for every change, and `FakeRelay#next_batch` returns every unsent snapshot. A create followed by an update therefore puts the same entry ID into one batch. The single `INSERT ... ON CONFLICT DO UPDATE` proposed in plan item 4 cannot affect the same row twice: PostgreSQL rejects that statement. The transaction rolls back, and the relay cannot advance past this ordinary batch, leaving reports stale. Specify how repeated IDs are processed within the transaction, while preserving company checks, version rules and accurate counts. Add a test that creates and updates one entry before delivering its pending batch.","recorded_resolution":"fixed","recorded_correction":"Agreed. The consumer now runs one version-guarded upsert per snapshot, in outbox position order, inside the batch transaction. A test delivers a create and an update of one entry together.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:17:41.441Z","answer_recorded_at":"2026-10-26T00:21:37.436Z","first_independent_review_at":"2026-10-26T02:25:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"b879da5eb963af8f9be2f699069a350cf0cc0b77d8ed89be8391314cb7b1f6bc","record_sha256":"1bbb2a1a986282f93bf8133ebb4a3f0b5657aa204f160baf62fe20c2d244e516"},
    {"record_id":"author-pr55-plan-011","pr":55,"pr_url":"https://github.com/Homie-Improvement/miru/pull/55","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Track the monolith's read source separately from its generation.","finding_evidence":"The generation identifies a write epoch, not whether the monolith uses legacy or prepared tables. plan item 6 treats every monolith-owned generation of 3 or higher as prepared. However, docs/architecture/timekeeping-soa.md:905-906 raises the generation when each handoff starts, and its Abort rule preserves that increase without flipping ownership. Two aborted first-cutover attempts therefore leave the legacy monolith at generation 3. The proposed reader then selects the prepared entries table, which is not authoritative, and employees lose their time entries from both lists. Store the legacy/prepared source explicitly in the ownership state instead of deriving it from the generation. Preserve it on abort and define its transition on a completed flip. Test two aborted attempts before the first succ","recorded_resolution":"fixed","recorded_correction":"Agreed. A new monolith_store column (legacy or prepared) holds the read source. An abort keeps it, and a later completed flip back to the monolith sets it to prepared.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:17:41.441Z","answer_recorded_at":"2026-10-26T00:21:37.436Z","first_independent_review_at":"2026-10-26T02:25:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f7a8f184187b7f77f2e3617e62511bb84ab6772545b033c8c1c80d180aeee727","record_sha256":"b8e2fc255b6b2c80ccb4cda7b562c3d068caccbc1989a925de07cb9e2061af46"},
    {"record_id":"author-pr55-plan-012","pr":55,"pr_url":"https://github.com/Homie-Improvement/miru/pull/55","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Validate project ownership before accepting a snapshot.","finding_evidence":"plan item 4 explicitly omits the project-company check while bypassing model callbacks. A snapshot for company A can therefore name company B's existing project, even when its stored company_id remains A. Reports::TimeEntries::ReportService#search_timesheet_entries scopes through current_company.timesheet_entries and project_id, not the entry's company_id. app/models/company.rb and app/models/client.rb define that association through projects. app/views/api/v1/reports/time_entries/index.json.jbuilder then renders the entry's note and user name to company B. The reader's separate company checks cannot protect reports. Timekeeping's authority over company_id does not make these existing joins safe. Reject a snapshot whose project belongs to another company before writing it. Test both insertion and a higher-version project change, with no batch writes on refusal. A stalled delivery is recoverable; disclosure t","recorded_resolution":"fixed","recorded_correction":"Agreed. Company#timesheet_entries goes through clients and projects, so a cross-company project would leak into reports. A batch with such a project is now refused and writes nothing.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:17:41.441Z","answer_recorded_at":"2026-10-26T00:21:37.436Z","first_independent_review_at":"2026-10-26T02:25:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"4e6339fdd675ce469b2223ef5e9dd088cc949964c9491c2388c6d5896edbac1b","record_sha256":"410d3653e52f91f7091a69aba27504d3bebbcbc01e66254e131af399ed4f4c55"},
    {"record_id":"author-pr63-build-001","pr":63,"pr_url":"https://github.com/Homie-Improvement/miru/pull/63","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Command specs check the user receipt table, not the service one","finding_evidence":"The reservation and service commands write ServiceIdempotencyReceipt, so these 'writes nothing' receipt checks always pass and would miss a leaked receipt. This violates docs/solutions/conventions/assert-the-baseline-before-proving-a-refused-command-changed-nothing.md.","recorded_resolution":"fixed","recorded_correction":"The no-write checks in the reservation request specs now count ServiceIdempotencyReceipt, which is what these routes write. All of those specs pass.","correction_target":"code_commit","source_path":"timekeeping/spec/requests/v1/reservations_spec.rb","source_line":287,"fix_commit":"f0bbb51a2e3b2d3199d4ce944c1981f70d9995af","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/f0bbb51a2e3b2d3199d4ce944c1981f70d9995af","finding_recorded_at":"2026-10-26T02:16:13.306Z","answer_recorded_at":"2026-10-26T02:22:47.403Z","first_independent_review_at":"2026-10-26T03:23:16Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"d5aa3e94075373c92493f5ed2ffa4de51f3e8ecefaafe48c2c0993d12e1121cf","record_sha256":"29feda0c0a5fa0ee4abacd6a4fcb55b149309784bf378390dc2dbfb43292807b"},
    {"record_id":"author-pr63-build-002","pr":63,"pr_url":"https://github.com/Homie-Improvement/miru/pull/63","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Migration rollback fails once any service audit row exists","finding_evidence":"Service audit rows carry actor_id NULL. On rollback the reversible change_column_null replays as SET NOT NULL, which PostgreSQL refuses once such rows exist. The down migration then fails unless someone deletes audit history by hand.","recorded_resolution":"fixed","recorded_correction":"Rolling back now stops with an IrreversibleMigration error, and leaves the migration applied, when any audit row has no actor_id. Otherwise it restores NOT NULL. I checked both paths on a test database.","correction_target":"code_commit","source_path":"timekeeping/db/migrate/20260401000000_create_invoice_operations.rb","source_line":62,"fix_commit":"f0bbb51a2e3b2d3199d4ce944c1981f70d9995af","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/f0bbb51a2e3b2d3199d4ce944c1981f70d9995af","finding_recorded_at":"2026-10-26T02:16:13.306Z","answer_recorded_at":"2026-10-26T02:22:47.403Z","first_independent_review_at":"2026-10-26T03:23:16Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"ba321d06e3f4c226fca4065a08a30f1dc2a082b8141d7b4ba751008b8c850fda","record_sha256":"d0b1558189ae05dd903b5e82a31237c24d0a3af9baac7d9c05a09c62c869ec8a"},
    {"record_id":"author-pr63-build-003","pr":63,"pr_url":"https://github.com/Homie-Improvement/miru/pull/63","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"Stale reserved flag read before the entry lock skips the relock version, audit and outbox","finding_evidence":"If an abort or releasing commit on invoice A runs at the same time as a prepare on invoice B for the same entry, the waiting prepare reads reserved:true from its locking statement's READ COMMITTED snapshot. The fresh decision query then reserves the entry again, but record_locked_flips compares true with true and skips the version bump, the audit row and the relock outbox snapshot. Downstream consumers keep A's locked:false while the entry is reserved, which breaks R10.","recorded_resolution":"fixed","recorded_correction":"Entries are now locked first, then their reserved flags are read in a separate query. A new two-connection test (abort held, prepare waiting) failed before the fix (version 3, not 4) and passes now.","correction_target":"code_commit","source_path":"timekeeping/app/services/timekeeping/reservation_operations.rb","source_line":200,"fix_commit":"f0bbb51a2e3b2d3199d4ce944c1981f70d9995af","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/f0bbb51a2e3b2d3199d4ce944c1981f70d9995af","finding_recorded_at":"2026-10-26T02:16:13.306Z","answer_recorded_at":"2026-10-26T02:22:47.403Z","first_independent_review_at":"2026-10-26T03:23:16Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"e63d221db29b0e2f956f499013fba8139a61c01f55b00a53b1dd8e59d5090456","record_sha256":"81477907162d6d872f642ff0b3159a718f30a9b2108b1a00fe537e94a3b9141f"},
    {"record_id":"author-pr63-plan-001","pr":63,"pr_url":"https://github.com/Homie-Improvement/miru/pull/63","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep one authoritative operation identity on each reservation.","finding_evidence":"The existing `reservations.operation_id` column is a required UUID in `timekeeping/db/schema.rb`. Adding `invoice_operation_id` creates two stored references to the operation that owns a reservation. The plan makes the new reference authoritative for commit and abort, but does not remove the old value or constrain the two references to agree. Future callers could select reservations through the old value and bypass the service-scoped identity this change introduces. Replace the redundant UUID with the owning operation reference, or specify an enforced consistency rule and document which reference callers must use.","recorded_resolution":"fixed","recorded_correction":"Reservations keep the required operation_id, and a composite foreign key now forces it to match the owning operation row. Code selects owned rows only through the operation reference.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:18:05.901Z","answer_recorded_at":"2026-10-26T00:19:53.421Z","first_independent_review_at":"2026-10-26T03:23:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"43e247794701f929dd0307c999a45623fc2cbe764bac6be126bc4edb466b3ac8","record_sha256":"1d97809363983bde0155d46330f4869fd015975004cd1fc044a070fde656990a"},
    {"record_id":"author-pr63-plan-002","pr":63,"pr_url":"https://github.com/Homie-Improvement/miru/pull/63","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"New signed service tokens bypass the existing check that prevents credentials from being stored.","finding_evidence":"A user can save a note containing a valid service token other than the request's bearer token. plan item 3 introduces HS256 tokens, but timekeeping/app/services/timekeeping/credential_scan.rb recognizes embedded signed tokens only through an RSA signature. Config.service_credentials contains the signing secret and configured literals, not every token minted from that secret. EntryCommand.build therefore accepts this note. EntryWriter then copies it into the entry, audit record and outbox through OutboxFields.row. Readers can obtain the token and execute its allowed background commands until it expires. U8 registers minted tokens as held credentials, which hides this production case. Extend credential detection to recognize tokens signed with the service secret. Add a regression test using a user bearer token and a different, unregistered service token inside the note; assert refusal before any row","recorded_resolution":"fixed","recorded_correction":"Same fix as the first finding: detection works from the signing secret, not from a list of minted tokens, and the regression test uses a token the test setup never registers.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:18:05.901Z","answer_recorded_at":"2026-10-26T00:19:53.421Z","first_independent_review_at":"2026-10-26T03:23:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d33feab8c6797863cb1f41a8b8d1094847144915cbeff7bbe82399fc3a7c9a1a","record_sha256":"89445bda11e1b4a2c1240bb0d500fb380b22dde97b4d8ce919b5d88eb4a838b1"},
    {"record_id":"author-pr63-plan-003","pr":63,"pr_url":"https://github.com/Homie-Improvement/miru/pull/63","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The new service tokens bypass the existing check that prevents credentials from entering stored entries.","finding_evidence":"plan item 3 introduces HS256 service tokens, but the plan leaves CredentialScan unchanged. In timekeeping/app/services/timekeeping/credential_scan.rb:22-29, it detects only literal held credentials, the current bearer, or RSA-signed tokens. A user write with a different, valid service token in its note passes this check. EntryWriter then copies that token into the entry, audit, receipt and outbox; readers can obtain it and use its service permissions until expiry. This also breaks contracts/timekeeping/entry_api.yml:81-84, which forbids storing service identities. Extend the credential check when introducing the token format. Test an entry write authenticated with a user token whose note contains an unregistered HS256 service token, including JSON-escaped text, and assert refusal before any stored change.","recorded_resolution":"fixed","recorded_correction":"The credential check now also finds any token signed with the service secret, registered or not, in entry and reservation bodies, with a regression test that uses a user bearer and an escaped token.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T00:18:05.901Z","answer_recorded_at":"2026-10-26T00:19:53.421Z","first_independent_review_at":"2026-10-26T03:23:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"93feda513e4497f6ab79b41e4dc8967ed87e32c68eae90339482af52e9699a4f","record_sha256":"581b5feb76c8be7893d818375c686d03fac09d0c8cd93be209071132c54fcdc1"},
    {"record_id":"author-pr64-build-001","pr":64,"pr_url":"https://github.com/Homie-Improvement/miru/pull/64","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Duplicate Code: new fence describe reimplements race helpers and test","finding_evidence":"The appended \"fencing behind an admitted write\" describe block rebuilds by hand what the file's own race, on_own_connection and blocked_by? helpers already do: the checkpoint queues, threads on their own connections, backend pid capture and the pg_blocking_pids polling loop. Its example 'keeps the generation admitted while a fence waits ... then fences once it commits' checks the same rule as the existing 'makes a fence wait until the write that read the admitted generation commits'. The only extra assertion is that the generation is still 1 while the fence waits. The copy has already drifted: it has no begin/ensure, so if `expect(Timekeeping::Admission.current.generation).to eq(1)` fails before `release << true`, the writer thread waits on release.pop for ever while holding its row locks, and the non-transactional after hook's TRUNCATE blocks on those locks. One failed assertion then hangs the whole suite instead of reporting a failure. Any change to how races are set up or probed must also be made in two places.","recorded_resolution":"fixed","recorded_correction":"The duplicate fence test is gone. The existing fence-wait test now checks the generation through race's new while_blocked hook, and race always releases the held write. The timeout test now uses the shared helpers.","correction_target":"code_commit","source_path":"timekeeping/spec/services/timekeeping/entry_writer_race_spec.rb","source_line":155,"fix_commit":"1c04b47d2fea90148d7e87e417d22ad22299134d","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/1c04b47d2fea90148d7e87e417d22ad22299134d","finding_recorded_at":"2026-10-26T02:31:33.845Z","answer_recorded_at":"2026-10-26T02:37:57.535Z","first_independent_review_at":"2026-10-26T03:15:28Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"9c839237d3a37c8b91eaec20a0a2d962b027f42c8f84bfafebf7d2712a0a427b","record_sha256":"4cf803bfea13c39f32f279620b7fbfe635cfd66a50259522c33c68f60338f6ca"},
    {"record_id":"author-pr64-plan-001","pr":64,"pr_url":"https://github.com/Homie-Improvement/miru/pull/64","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Exclude historical migrations from the reference-removal check.","finding_evidence":"The completion rule requires no reference to `ownership_generations` outside the new migration. However, `timekeeping/db/migrate/20260201000000_create_write_tables.rb:57` creates that table. That historical migration must remain intact so migration-based setup can create the source table before the replacement migration reads it. Limit the removal check to application code, specs and the current schema, or explicitly exempt historical migrations. The existing verification command already limits its search to `timekeeping/app` and `timekeeping/spec`.","recorded_resolution":"fixed","recorded_correction":"The completion rule now exempts the earlier migrations, including the one that creates the old table, and matches the search limited to the app and spec folders.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T01:43:12.198Z","answer_recorded_at":"2026-10-26T01:44:10.416Z","first_independent_review_at":"2026-10-26T03:15:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"b823cdccd7ddf10928266c6af4a9d7cc26b5d3ef14d30ebf8f297b24c6d6a5b4","record_sha256":"35e2c60fb4b3aaa5d6960d7e8f7308cd323f0808ab8ff7e6300772e141c401d5"},
    {"record_id":"author-pr64-plan-002","pr":64,"pr_url":"https://github.com/Homie-Improvement/miru/pull/64","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The cleanup rule includes historical migrations that the implementation must preserve.","finding_evidence":"The completion rule bans every reference to `ownership_generations` outside the new migration. However, the migration verification requires building an older database that still contains that table. Its historical migrations must retain the old table definition. The units only remove application and spec references, so their verification does not establish this broader completion rule. Limit the cleanup rule to current application code, specs and schema, and explicitly allow historical migrations.","recorded_resolution":"fixed","recorded_correction":"The cleanup rule now covers only application code, specs and the current schema, and says the earlier migrations that create the old table stay unchanged.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T01:43:12.198Z","answer_recorded_at":"2026-10-26T01:44:10.416Z","first_independent_review_at":"2026-10-26T03:15:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"bfbf7e065b351c2904074836859e0670b8a254afce8764b697ccc7686843e468","record_sha256":"f35412e0bf52fdee3db93620376dbfa8f01d2f36240427a584cc706c7c1616df"},
    {"record_id":"author-pr65-build-001","pr":65,"pr_url":"https://github.com/Homie-Improvement/miru/pull/65","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Decimal metadata is rounded before verification can detect it","finding_evidence":"An export and load can change a JSONB metadata or audit number without any error and still report matching checksums. That breaks the intent's 'never drop or coerce a value' and R5's 'every column as stored'. The JSONB cast in read_rows decodes a high-precision decimal into a Ruby Float before Checkpoint.build computes the digest. load_checkpoint deletes the stored row and writes the rounded Float back, and the read-back verify passes because it compares against the already-rounded checkpoint. The same path covers proof_metadata and the before/after audit cells.","recorded_resolution":"fixed","recorded_correction":"Export now parses jsonb cells as BigDecimal, the checkpoint encodes JSON numbers as exact decimals, and load writes them back as bare numbers. A real Postgres test shows both stored texts unchanged.","correction_target":"code_commit","source_path":"lib/timekeeping/handoff/monolith_store.rb","source_line":110,"fix_commit":"b010cc0574a84873d378b7326c025d15397341fa","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/b010cc0574a84873d378b7326c025d15397341fa","finding_recorded_at":"2026-10-26T03:09:29.961Z","answer_recorded_at":"2026-10-26T03:22:26.105Z","first_independent_review_at":"2026-10-26T03:39:25Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"4870334168d60c5c1e08fde0d0ed84c8a545adc28bf1023c3ba5a07c29a188ca","record_sha256":"e8abcb7b5f912b9dd1f7b166beff47b7484dfb8af3e1b601a066058ad81600e6"},
    {"record_id":"author-pr65-plan-001","pr":65,"pr_url":"https://github.com/Homie-Improvement/miru/pull/65","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Choose one response when the text read path cannot preserve float bits.","finding_evidence":"The stop conditions require work to stop if Rails' text read path cannot preserve float bits. The Open Questions instead leave `float8send` as an implementation option, with the round-trip test deciding. These instructions give implementers different responses to the same failed test. State whether the implementation must stop or try the binary read path before stopping, and make both sections agree.","recorded_resolution":"fixed","recorded_correction":"The plan now gives one answer in both places. If the round-trip test shows the text read loses float bits, the build switches to reading float8send. It stops only if that binary read also fails.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T01:44:56.032Z","answer_recorded_at":"2026-10-26T01:46:08.452Z","first_independent_review_at":"2026-10-26T03:39:25Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"3fcbb40e9410c5c492b39f95ceafe9873f4f87ba2439f3a4ee5670e187abcb02","record_sha256":"54d4a090c6f13f390c498fd0fc96c6ef69ff76c59a686b93ce0c4ffe10f9f292"},
    {"record_id":"author-pr65-plan-002","pr":65,"pr_url":"https://github.com/Homie-Improvement/miru/pull/65","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Give load its own rollback boundary when a caller already has a transaction.","finding_evidence":"plan item 6 promises that any raise rolls back the load, but it does not define behavior inside an existing transaction. Rails 8.1 transactions join the caller's transaction by default; Gemfile selects Rails 8.1.2. Consider a caller that rescues the Ruby exception from the read-back checksum check, then commits its outer transaction. Without a savepoint, the replacement rows commit despite the reported failure. plan item 5 explicitly rejects an existing transaction for export, but load has no equivalent guard. Reject an existing transaction before load writes, or use a savepoint that rolls back before the exception reaches the caller. Add a test that rescues a read-back mismatch inside an outer transaction, commits that transaction, and checks that all original rows remain. The current tests deliberately run outside a t","recorded_resolution":"fixed","recorded_correction":"Load now refuses, before any delete, when the caller already has a transaction open. So a rescued failure can never commit replaced rows. A test commits the outer transaction and checks every original row remains.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T01:44:56.032Z","answer_recorded_at":"2026-10-26T01:46:08.452Z","first_independent_review_at":"2026-10-26T03:39:25Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9e5f3d5ec7924302906a668388fcb18beab38f6cd35eb31a9c0033dfc2b72bba","record_sha256":"15f342f95760f84b9af04a039c68f36f8c2a8293995296c80e51eee2aabd6401"},
    {"record_id":"author-pr65-plan-003","pr":65,"pr_url":"https://github.com/Homie-Improvement/miru/pull/65","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Load must establish its transaction boundary before replacing rows.","finding_evidence":"plan item 6 promises that any exception rolls back the replacement, but it does not exclude an existing transaction. Rails joins an existing transaction by default. If the read-back digest raises a Ruby exception and the caller rescues it, the caller can commit the replacement despite the failed verification. Operators would then retain data that the load reported as refused. plan item 9 runs store tests outside transactions, so those tests do not cover this case; spec/support/database_cleaner.rb normally starts an outer transaction. Reject an already-open transaction before load makes any changes, as export already does, or explicitly isolate load with a savepoint. Add a test that rescues the load failure inside an outer transaction and proves that committing it preserves the original rows.","recorded_resolution":"fixed","recorded_correction":"Load now refuses before any write if a transaction is already open, the same way export does. A new test rescues that refusal inside an outer transaction, commits it, and checks the original rows remain.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T01:44:56.032Z","answer_recorded_at":"2026-10-26T01:46:08.452Z","first_independent_review_at":"2026-10-26T03:39:25Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"e6b1cf17d74424b483945269abc131d9c478f4eddb33c2815646ed550d1b8752","record_sha256":"81783141b10200664dbd709df21caf3440286c92da2640bcc78588c802a24301"},
    {"record_id":"author-pr71-build-001","pr":71,"pr_url":"https://github.com/Homie-Improvement/miru/pull/71","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Meta spec pins example titles and code text, not the one-file layout","finding_evidence":"The intent asks the meta spec only to guard the one-file layout, and the fourth example (lines 44-53) already does that. The first three examples (lines 7-40) grep the source text of other spec files instead: exact example titles, `company_b.timesheet_entries.where(id: stored.id)` counted exactly once, `final_outbox_position: 10` counted exactly once, and shared-contract wording. Rewording a title or refactoring a query fails the suite even though behaviour has not changed. Deleting the real assertions while leaving the strings in place still passes, so the checks give false confidence.","recorded_resolution":"fixed","recorded_correction":"I removed the three examples that string-matched example titles, code fragments and contract wording. Only the one-file, no-401-or-403 layout check remains, with a comment listing what the folded file holds. The meta and request specs pass, 8 examples.","correction_target":"code_commit","source_path":"spec/meta/timekeeping_entry_snapshot_request_specs_spec.rb","source_line":23,"fix_commit":"09465d6eb32db03ddc7e9e0c87b2516d2cb759d0","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/09465d6eb32db03ddc7e9e0c87b2516d2cb759d0","finding_recorded_at":"2026-10-26T03:35:27.434Z","answer_recorded_at":"2026-10-26T04:17:46.045Z","first_independent_review_at":"2026-10-26T04:28:13Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"81a444620412f6e1f88089c7151f3b0f18eb371ce04542fa3f4267a34bdf1187","record_sha256":"eac9983dfd5b290fb8515144346e802fb7a71527ad7e2f5f799c7a184b192525"},
    {"record_id":"author-pr78-build-001","pr":78,"pr_url":"https://github.com/Homie-Improvement/miru/pull/78","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Generated acceptance specs repeat the same snapshot and printed_totals contracts across five files and six describe blocks","finding_evidence":"The same snapshot contracts (mixed-company refusal, no billing write, discarded invoice, deleted line item, identifiers-only payload) are asserted in two to five spec files that plan units U2 and U3 do not name. The no-billing-write subscribers already use different regexes. Six overlapping printed_totals describes are appended to invoice_service_spec.rb (lines 339-491). Every contract change has to be edited in each copy, and a missed copy gives false confidence. Each copy also re-runs the two-tenant seed.","recorded_resolution":"fixed","recorded_correction":"Merged the five snapshot spec files into one financial_snapshot_spec.rb, one copy of each check, and folded the six printed_totals blocks into one describe. The seed now loads once.","correction_target":"code_commit","source_path":"spec/services/timekeeping/financial_snapshot_4_spec.rb","source_line":11,"fix_commit":"6741b3aed703cd4ffbe8e6afa22d69c9d79bed1e","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/6741b3aed703cd4ffbe8e6afa22d69c9d79bed1e","finding_recorded_at":"2026-10-26T03:53:07.123Z","answer_recorded_at":"2026-10-26T04:35:07.674Z","first_independent_review_at":"2026-10-26T04:47:10Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"9e56f969220edfaf27418b386d54cc7759e18aa41d33216fc48b7307d7149a0e","record_sha256":"8d1ce1c953256505997f37683c64d0de0be601338f4f938774c84414f266fffe"},
    {"record_id":"author-pr78-plan-001","pr":78,"pr_url":"https://github.com/Homie-Improvement/miru/pull/78","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The mutation tests contradict the shared PDF totals.","finding_evidence":"The tests require exactly one difference for every mutation except quantity. However, the snapshot also includes formatted amounts and computed PDF totals. Changing invoice tax must change the stored tax, the formatted invoice tax, and the printed total. Changing discount or line item rate also changes several captured values. `update_column` prevents callbacks, but it does not prevent the PDF calculations from reading the changed value. Requiring one difference would reject correct snapshots or encourage incomplete capture. Specify the complete expected difference set for each mutation, while retaining the check that removing its stored field makes the test fail.","recorded_resolution":"fixed","recorded_correction":"Agreed. The one-difference rule is gone. The plan now lists the complete expected set for each mutation, and each test still fails if its stored field is left out of the snapshot.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T01:42:39.863Z","answer_recorded_at":"2026-10-26T01:43:42.972Z","first_independent_review_at":"2026-10-26T04:47:10Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f713224a5acde704ab500151330664960095a962b55d10381277dbad406b7518","record_sha256":"8b01a1f5e17737b27ce5aa7f94147020795f9838d0360354080173769aa5fadd"},
    {"record_id":"author-pr78-plan-002","pr":78,"pr_url":"https://github.com/Homie-Improvement/miru/pull/78","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The mutation tests require fewer differences than the PDF calculations produce.","finding_evidence":"The snapshot tests require exactly one difference for each mutation except quantity. That cannot hold for several required cases. In app/services/pdf_generation/invoice_service.rb, formatted_invoice_amounts reads amount, tax and discount directly, while calculate_total also uses tax and discount. Changing amount by one currency unit therefore changes both the stored amount and printed invoice_amount. Changing tax or discount also changes the printed total. Changing a line item's rate affects the subtotal through app/models/invoice_line_item.rb#total_cost_of_all_line_items. update_column skips callbacks, but it does not stop these read-time calculations. Specify the full expected difference set for each mutation, including derived PDF values. Keep the assertion that each changed source field appears, so a derived difference cannot conceal a","recorded_resolution":"fixed","recorded_correction":"Agreed. The plan now lists the derived PDF values for amount, tax, discount, rate, quantity and invoice tax amount. Each mutation moves the value by 10.00 so the printed strings change too.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T01:42:39.863Z","answer_recorded_at":"2026-10-26T01:43:42.972Z","first_independent_review_at":"2026-10-26T04:47:10Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d876712f771876c7f5040aea8e669d5dbbdecb5e79db9d75513e8457c337fa5f","record_sha256":"221c83d8b107711f55b8fe72904db06ef727d48382ffbf85f5d350d97cb20e21"},
    {"record_id":"author-pr78-plan-003","pr":78,"pr_url":"https://github.com/Homie-Improvement/miru/pull/78","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The mutation tests would reject correct reports of dependent PDF changes.","finding_evidence":"The snapshot tests require exactly one difference for each mutation except quantity. Consider `update_column(:tax, ...)` with a change large enough to alter the printed amount. `app/services/pdf_generation/invoice_service.rb` reads tax in both `calculate_total` and `formatted_invoice_amounts`. A correct comparison must therefore report the stored tax, printed invoice tax, and printed total. Changing amount or discount also changes a formatted value; changing rate can change subtotal and total. Bypassing model callbacks does not remove these dependencies. Replace the single-difference rule with explicit expected sets for each mutation. Keep a separate assertion for each stored field so a dependent PDF difference cannot hide its omission.","recorded_resolution":"fixed","recorded_correction":"Agreed. Each mutation test now asserts the full expected set of differences, including the printed PDF values that depend on the changed field, and separately checks the changed stored field itself.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T01:42:39.863Z","answer_recorded_at":"2026-10-26T01:43:42.972Z","first_independent_review_at":"2026-10-26T04:47:10Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"a95973b91e53d841124aa00d86ccd2938bb15d455c82dd44a2959d35ca24c87f","record_sha256":"ec6fc9cafa670b129dee2a8658a07b6485fb5e30c9bbfaa90d2a803cd7be7983"},
    {"record_id":"author-pr79-plan-001","pr":79,"pr_url":"https://github.com/Homie-Improvement/miru/pull/79","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A token minted for a read still grants elevated write access.","finding_evidence":"plan item 2 changes token creation, but does not constrain where the token works. Mint a read token for an employee-first admin, then replay it before expiry on PATCH for a colleague's unbilled entry, with valid write headers. timekeeping/app/services/timekeeping/write_access.rb accepts any verified user token. timekeeping/app/services/timekeeping/entry_command.rb copies identity.role into the write command; it does not restore the reference role. The writer therefore applies admin permissions and accepts a change that the previous employee token refused. A holder of the read token can also bypass employee checks on reserved entries. The existing mint keyword cannot prevent this because the provider never receives or checks it. Revise plan item 2 and the no-provider-change boundary to enforce the read/write distinction at the receiving service, and test th","recorded_resolution":"fixed","recorded_correction":"Agreed, and fixed with the same change: the read/write split is now enforced where Timekeeping accepts the token. A replayed read token on a write route gets forbidden and changes nothing.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:07:23.459Z","answer_recorded_at":"2026-10-26T03:09:39.108Z","first_independent_review_at":"2026-10-26T04:22:36Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"b4d7ae51be961d707473af2cf37e234b0539f35ee8021f1f61a4932dd83c4c7c","record_sha256":"0110598f01f55d59fd463c81ff49e15392bb755131191479946890547ef08fbe"},
    {"record_id":"author-pr79-plan-002","pr":79,"pr_url":"https://github.com/Homie-Improvement/miru/pull/79","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The read purpose does not prevent the new privileged token from authorizing writes.","finding_evidence":"plan item 2 says write authority stays unchanged, but `purpose: :read` changes only token minting. `timekeeping/lib/timekeeping/identity.rb` reads the same role for every route. `WriteAccess.decide` accepts that identity, and `EntryCommand.build` copies `identity.role` rather than the reference endpoint's role. A bearer token captured from this read can therefore authorize writes as admin, although the same user's primary role remains employee. This bypasses the employee restrictions in `TimekeepingRules::EntryRules`, including restrictions on changing colleagues' entries and locked entries. Enforce the read/write distinction where the provider accepts the credential, or preserve the primary role for writes through a separate read authorization fact. Add a provider regression that submits the multi-role user's read token to a write route and proves","recorded_resolution":"fixed","recorded_correction":"Confirmed in the provider: write routes take the role from the same token. The plan now signs a purpose into every token, and Timekeeping refuses write routes for a read token, with a provider test.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:07:23.459Z","answer_recorded_at":"2026-10-26T03:09:39.108Z","first_independent_review_at":"2026-10-26T04:22:36Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"697dc87c589d9dc78d428ffe36a1001904823779fc11f52ff342e34f9aafda41","record_sha256":"e7313248c2ba67ddf98fe1dedfe100b6a3da5b9c74351c5a57ffe74b8315627f"},
    {"record_id":"author-pr80-build-001","pr":80,"pr_url":"https://github.com/Homie-Improvement/miru/pull/80","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"prepare_table_policy_spec duplicates a mutation_spec mutant and its runner","finding_evidence":"The new acceptance spec builds the same changed prepare table (on_another_invoice answered as add_provisional) that the mutation_spec row \"reserves an entry committed to another active invoice\" already builds. It runs that table through a hand-copied second copy of the hidden-group reporter/listener/current_example-restore harness. It adds no coverage: its one extra assertion, that the shared PREPARE_TABLE outcomes are unchanged, is already at reservation_rules_spec.rb:68-69. The cost is a second harness that must be kept in step with run_contract_group, and a second full run of the reservations contract group on every suite run.","recorded_resolution":"fixed","recorded_correction":"The duplicate spec file is deleted. The contract mutation spec still builds the same changed table and fails the reservations group, and the rules spec still checks the shared table is unchanged.","correction_target":"code_commit","source_path":"spec/contracts/timekeeping/prepare_table_policy_spec.rb","source_line":10,"fix_commit":"ce5a1232baa1375213508491f43e238b222cd326","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ce5a1232baa1375213508491f43e238b222cd326","finding_recorded_at":"2026-10-26T04:37:13.449Z","answer_recorded_at":"2026-10-26T04:42:14.164Z","first_independent_review_at":"2026-10-26T07:01:49Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"9bd3db03ad6042c7f51c52e84a6a5597061b8ff6cc80b89190a49ee6d2a3afab","record_sha256":"845af8a41b4523957a335baa8ef2727081142cdbb13234ca2cbbeed32fa5fcea"},
    {"record_id":"author-pr80-plan-001","pr":80,"pr_url":"https://github.com/Homie-Improvement/miru/pull/80","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The plan gives conflicting rules for preserving request entries.","finding_evidence":"plan item 5 says entries are canonical everywhere: integers, unique, and sorted. The port-value unit instead requires the request to keep all six fields unchanged. For entries [3, 1, 3], these rules require [1, 3] and [3, 1, 3], respectively. Future adapters will depend on this public value type, so its behavior must be clear. State whether the request normalizes entries or preserves them, then align plan item 5 and the request tests with that choice.","recorded_resolution":"fixed","recorded_correction":"The request now normalizes entries to unique integers in ascending order, the same form the decision returns. Its test checks that [3, 1, 3] is stored as [1, 3].","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:37:59.709Z","answer_recorded_at":"2026-10-26T03:39:32.629Z","first_independent_review_at":"2026-10-26T07:01:49Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f82b83270dff97a62b3b5509daee19a889780e89aa4286b95358fce6bd78d421","record_sha256":"708e1b255abd503ec7da9b7b5208416bf3622ad6db3fa6bc5215f97ae183b79b"},
    {"record_id":"author-pr80-plan-002","pr":80,"pr_url":"https://github.com/Homie-Improvement/miru/pull/80","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The planned value objects do not define an immutable boundary for nested values.","finding_evidence":"The unit promises immutable values, but it keeps counts as given. The design freezes arrays and conflict hashes, without defining ownership of mutable strings or the counts container. Ruby's Data.define freezes the record, not its referenced objects; the cited ProjectionConsumer::Result pattern adds no recursive freezing. A caller could change a counts hash after constructing an outcome, so adapters and their callers could observe different results from the same value. Define which nested values the constructors copy and freeze, and test that changing caller-owned inputs cannot change a constructed value. Do not freeze caller-owned containers in place. The ticket asks for value types, not deep immutability; this finding concerns the immutable interface the plan chooses.","recorded_resolution":"fixed","recorded_correction":"Both constructors now copy and freeze entries, conflicts, counts and their strings, and leave the caller's objects unfrozen. A test shows that changing a caller's input leaves the built value unchanged.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:37:59.709Z","answer_recorded_at":"2026-10-26T03:39:32.629Z","first_independent_review_at":"2026-10-26T07:01:49Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"90693a4e01a920649d494a4a50e7d11aa7878cedddd8e2c646bb800c444e2ab6","record_sha256":"aa0c3f94c75a22bb7b183fdf513e4d3b31032b1013b018bee34b8b307d611c1f"},
    {"record_id":"author-pr80-plan-003","pr":80,"pr_url":"https://github.com/Homie-Improvement/miru/pull/80","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The stated work order contradicts the decision unit's dependency.","finding_evidence":"The work order allows the reservation decision and port value types to land in either order. However, the decision unit declares a dependency on the port-value unit, and plan item 6 makes it use ReservationRequest::DISPOSITIONS. Build the port value types before the reservation decision so each unit can pass its own verification when committed.","recorded_resolution":"fixed","recorded_correction":"The work order now builds the port value types before the reservation decision, which matches that unit's dependency.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:37:59.709Z","answer_recorded_at":"2026-10-26T03:39:32.629Z","first_independent_review_at":"2026-10-26T07:01:49Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d98d19ea8d6981cd5752bdddd1d0d382c0f0e1d59d21328d48df4a5db02160c1","record_sha256":"2bb4575107c70b4de1a8e1eb76760eda1dffdbeb91fd9964ec27248589878eea"},
    {"record_id":"author-pr80-plan-004","pr":80,"pr_url":"https://github.com/Homie-Improvement/miru/pull/80","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The test-mode work omits existing tests that use separate database connections.","finding_evidence":"The ticket says, \"Use it for every test that runs separate connections or processes.\" The test-mode unit changes only the cleaner and a new spec. Existing concurrency tests in spec/services/timekeeping/projection_consumer_spec.rb:163-206 use separate connections through rules_thread. They still manage transactions through rules_without_cleaner_transaction at lines 125-132. The threaded example in spec/mcp/idempotency_store_spec.rb:83-100 also acquires a separate database connection through app/mcp/idempotency_store.rb. Add these files to the unit, apply committed_data: true to the relevant examples, and replace the projection specs' manual cleaner wrapper with the shared mode. Otherwise existing tests retain a second cleanup mechanism that can drift from the new one. Keep the proposed untagged visibility test as an explicit negative control.","recorded_resolution":"fixed","recorded_correction":"The projection consumer's committed-transaction examples now use the shared tag in place of their own cleaner wrapper, and the threaded idempotency store example is tagged too.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:37:59.709Z","answer_recorded_at":"2026-10-26T03:39:32.629Z","first_independent_review_at":"2026-10-26T07:01:49Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"11025a105a6aec84e0200239bf99c21b3a197eb0620f2b48308b37d474298205","record_sha256":"5caf3978cf2698c270b5131c5eaf0607cb67c71340977ab5dee9a49ab16c5a4d"},
    {"record_id":"author-pr80-plan-005","pr":80,"pr_url":"https://github.com/Homie-Improvement/miru/pull/80","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"low","finding_title":"The plan goes beyond the issue: Implement the ledger migration and models, regenerate the schema and revise the architecture document.","finding_evidence":"The migration, models, schema dump and their tests implement the requested work. Revising the architecture document is additional documentation work the issue references for context but does not request.","recorded_resolution":"fixed","recorded_correction":"The architecture document edit is gone from the migration unit. The plan now lists it as follow-up work, because the issue cites that document only for context.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:40:43.910Z","answer_recorded_at":"2026-10-26T03:42:08.263Z","first_independent_review_at":"2026-10-26T07:01:49Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f85d109f209169060bad6b6592ae6afd8154038025811d4541e3c267b08f5cc2","record_sha256":"a01660b00d1c5b9a5d2d3a13198a62ada0de5e0730056ceda1f0bc917121efb2"},
    {"record_id":"author-pr80-plan-006","pr":80,"pr_url":"https://github.com/Homie-Improvement/miru/pull/80","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"medium","finding_title":"The plan goes beyond the issue: Test visibility through a second connection, including an untagged transactional negative control.","finding_evidence":"The tagged visibility test is requested, but the untagged negative control deliberately exempts a separate-connection test from the universal tagging rule. Although the earlier review retained that useful control, preserve the regression check without this exemption or clarify the exception with the issue author.","recorded_resolution":"fixed","recorded_correction":"The untagged negative-control example is removed. Instead, a manual check confirms that taking the tag off the visibility example makes it fail, and the tag is restored afterwards.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:40:43.910Z","answer_recorded_at":"2026-10-26T03:42:08.263Z","first_independent_review_at":"2026-10-26T07:01:49Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"3ba3a2b097ee53909767afbe0273c55707008cfa9e3457cf31badeba89995457","record_sha256":"c8f83ebfdd00d367e17e76a6e43a94b8b381b9799c705fb1d14c879aa147a7c9"},
    {"record_id":"author-pr80-plan-007","pr":80,"pr_url":"https://github.com/Homie-Improvement/miru/pull/80","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"low","finding_title":"The plan goes beyond the issue: Update the architecture document to describe the new intent columns and ledger tables.","finding_evidence":"This is useful, narrowly related documentation maintenance, but the issue does not request changes to the architecture document it cites.\n\nThe issue asks for nothing like it.\n\nAnswer by removing it from the plan or deferring it to follow-up work the plan names as out of scope; or decline with your reason.","recorded_resolution":"fixed","recorded_correction":"The design document update is removed from this change and listed as follow-up work. This is the same edit as the migration unit finding.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:40:43.910Z","answer_recorded_at":"2026-10-26T03:42:08.263Z","first_independent_review_at":"2026-10-26T07:01:49Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"bf4d27ea5a3caa28e2bbf7a1dc722c5f3d0705bc786443ad6ffe6b612149752d","record_sha256":"0b06937c0d9b21d1128495312c0033a474048aef3fb73eca673353b71ef49502"},
    {"record_id":"author-pr83-build-001","pr":83,"pr_url":"https://github.com/Homie-Improvement/miru/pull/83","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Concurrent invoice creates deadlock while locking the same entry","finding_evidence":"Two draft-invoice creates that run at the same time and name the same entry can now deadlock, and one of them fails with a 500. Both validations take compatible FOR SHARE locks. Each invoice's after_save lock_timesheet_entries then UPDATEs that entry's locked flag, and each UPDATE waits on the other transaction's share lock. PostgreSQL aborts one with ActiveRecord::Deadlocked, which ErrorHandler does not rescue. The base had no row lock here, and the remaining foreign key's FOR KEY SHARE never conflicted with that UPDATE.","recorded_resolution":"fixed","recorded_correction":"The existence check now takes FOR KEY SHARE. A two-connection probe deadlocked under FOR SHARE but not under FOR KEY SHARE, and FOR KEY SHARE still blocked a delete. The invoice and model specs pass.","correction_target":"code_commit","source_path":"app/models/invoice_line_item.rb","source_line":105,"fix_commit":"8d00214acf0e586e8a17084bf110ae7b3f5075ff","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/8d00214acf0e586e8a17084bf110ae7b3f5075ff","finding_recorded_at":"2026-10-26T04:43:05.612Z","answer_recorded_at":"2026-10-26T04:56:59.863Z","first_independent_review_at":"2026-10-26T05:11:17Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"1eb3fe40840fd5ad776a21ac195dd02e4b5701d8889d02a5c467f5dd8c5d999a","record_sha256":"99d76b11daf75719ae7b2c5b038407af54549a1c8f6a81fde9024a3890eb8b54"},
    {"record_id":"author-pr83-plan-001","pr":83,"pr_url":"https://github.com/Homie-Improvement/miru/pull/83","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Make deferred linked ids visible to Rails change tracking before parent validation.","finding_evidence":"The deferred writer in plan item 2 stores ids without changing an attribute or association. Rails therefore considers an existing line item unchanged when only its linked ids change. Invoice uses nested autosave in app/models/invoice.rb, and Api::V1::InvoicesController#update saves through the parent invoice. Autosave can skip the unchanged child, so its before_validation resolver never runs. A valid link replacement or clearing request then returns success without saving the requested links. Foreign or missing ids can also return success instead of not_found. Make pending ids participate in the child's autosave change detection, or resolve them before that decision. Add PATCH tests that change only linked_timesheet_entry_ids on an existing line, leaving all scalar attributes unchanged. Cover valid replacement, clearing, and foreign and missing ids.","recorded_resolution":"fixed","recorded_correction":"Pending linked ids now make the line item report itself as changed for autosave, so its resolver runs during the invoice's validation. PATCH tests change only the linked ids on an existing line.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:37:20.342Z","answer_recorded_at":"2026-10-26T03:38:46.264Z","first_independent_review_at":"2026-10-26T05:11:17Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"e2e66fe045aeacae302581fd24f6cbee8dfdcf8bf854d538a6584be366bfb11f","record_sha256":"035c633b98a5808f694a39e00d31f2787e2bfc3e7300e098c9d185c55cbc9159"},
    {"record_id":"author-pr83-plan-002","pr":83,"pr_url":"https://github.com/Homie-Improvement/miru/pull/83","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Make pending linked ids trigger validation when an existing line has no other changes.","finding_evidence":"The deferred writer in plan item 2 only stores requested ids in memory. `Invoice` uses `accepts_nested_attributes_for :invoice_line_items`, and Rails autosave selects changed persisted children for validation. A PATCH containing an existing line's `id` and only `linked_timesheet_entry_ids` does not change a database attribute or association yet. Its `before_validation` callback therefore never resolves those ids. The endpoint can return success while ignoring valid replacements, empty-array clears, and foreign ids that should produce 422. Make pending assignments participate in Rails change detection, or resolve them before autosave selects children. Add request tests for replacement, clearing, and rejection on an existing line with every other attribute unchanged.","recorded_resolution":"fixed","recorded_correction":"I checked `app/models/invoice.rb`: autosave picks children by `changed_for_autosave?`. The line item now counts pending linked ids as a change, and PATCH tests cover replace, clear, foreign and missing ids.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:37:20.342Z","answer_recorded_at":"2026-10-26T03:38:46.264Z","first_independent_review_at":"2026-10-26T05:11:17Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"53ddd3bcf47c82a66acd99d9af648474c8d8a53b439ea67c8e43673435556365","record_sha256":"ae92ac8e1ab2d49324394c647f94859ebd1ba525972dbacf4893bf27e42aa815"},
    {"record_id":"author-pr83-plan-003","pr":83,"pr_url":"https://github.com/Homie-Improvement/miru/pull/83","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Pending linked ids must make an existing line item eligible for validation and saving.","finding_evidence":"The deferred writer only stores requested ids in memory. Consider PATCH with an existing line's id and linked_timesheet_entry_ids, but no changed database attributes. app/models/invoice.rb uses accepts_nested_attributes_for :invoice_line_items; Rails autosave validates changed children. An instance variable alone does not make that child changed, so its before_validation resolver never runs. A valid replacement or empty array silently leaves the old links, while a foreign id returns success instead of not_found. Make pending assignment participate in autosave change detection, or resolve it before that decision. Test existing-line updates that change only linked ids: valid replacement, clearing, foreign id, and missing id. Adding a new line does not exercise this failure.","recorded_resolution":"fixed","recorded_correction":"Fixed the same way: the line item counts pending linked ids as a change for autosave. Tests on an existing line with only linked ids changed cover replacement, clearing, foreign and missing ids.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:37:20.342Z","answer_recorded_at":"2026-10-26T03:38:46.264Z","first_independent_review_at":"2026-10-26T05:11:17Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"280a7799cbe5457a81d96d7156c3321dbb0dbd0652c0df131dd27eb491247db7","record_sha256":"347581214ff03c07cda2eaa66e4003817a5f435d6e3326daf17059d4724f2491"},
    {"record_id":"author-pr83-plan-004","pr":83,"pr_url":"https://github.com/Homie-Improvement/miru/pull/83","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Scope the returned candidate entries, not only the entries used to exclude candidates.","finding_evidence":"The candidate decision adds company_id only to the two exclusion queries. In app/services/generate_invoice/new_line_items_service.rb, where_clause limits returned entries by project_id, and search_timesheet_entries applies no stored company_id filter. Create an unbilled entry in this client's project, then set company_id to NULL as the planned validation test does. The picker still offers that entry, although the new validator refuses it. A foreign company_id on the same project also passes the picker. Add client.company_id to the candidate relation and the team-member entry lookup, so both read only the company's entries. Add a service test proving NULL-company entries are absent from the returned candidates.","recorded_resolution":"fixed","recorded_correction":"The returned candidates and the team-member entry lookup now also filter on the stored company column, and a service test proves a NULL-company entry in the client's project is not offered.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:37:20.342Z","answer_recorded_at":"2026-10-26T03:38:46.264Z","first_independent_review_at":"2026-10-26T05:11:17Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d40024b0982f54c4c83f70c30859f651c111de82b0d912da3d6858365b2e2cab","record_sha256":"1914e6acab53a89e51296ef3cfc6f4bd3cfd64f8fad84419a76b1570b516cf6f"},
    {"record_id":"author-pr83-plan-005","pr":83,"pr_url":"https://github.com/Homie-Improvement/miru/pull/83","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Scope the returned candidate entries, not only the exclusion queries.","finding_evidence":"The candidate changes in plan item 4 cover only entries already referenced by invoices. In app/services/generate_invoice/new_line_items_service.rb, where_clause limits candidates by project_id, and search_timesheet_entries applies no company_id condition. TimeEntries::Filters adds none either. An unbilled entry in the client's project with company_id set to NULL therefore remains selectable, although the planned validator rejects it as not_found. The customer can select an offered entry and then cannot create the invoice. This omits the ticket's requirement to read only that company's entries. Add the stored company_id scope to the candidate relation and the entry lookup for team-member options. Add a service regression proving that a NULL-company entry in an otherwise eligible project is not offered.","recorded_resolution":"fixed","recorded_correction":"The candidate list and team-member options now read only entries whose stored company is the client's company, so an offered entry is always one the validator accepts.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:37:20.342Z","answer_recorded_at":"2026-10-26T03:38:46.264Z","first_independent_review_at":"2026-10-26T05:11:17Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"4a3dadd9b014746d5bb9dafd6912af9feaf809dff741b4da08d111b0006d16da","record_sha256":"eace344586b4ac76b356fe497ffed21d55de7698dc5a54c55dabc3df4dfe6140"},
    {"record_id":"author-pr83-plan-006","pr":83,"pr_url":"https://github.com/Homie-Improvement/miru/pull/83","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Scope the returned candidates as well as the queries that exclude allocated entries.","finding_evidence":"plan item 4 adds `company_id` only to the direct and linked exclusion queries. In `app/services/generate_invoice/new_line_items_service.rb`, `where_clause` and `search_timesheet_entries` still select candidates by project without checking the stored company column. An unbilled entry in the client's project with NULL `company_id` therefore remains selectable, although the planned validator rejects it on invoice creation. Add the company constraint to candidate retrieval, and test that a NULL-company entry is not offered. Check `team_members_of_projects` too, since its entry query also lacks that constraint.","recorded_resolution":"fixed","recorded_correction":"Candidate retrieval and the team-member lookup now carry the client's company on the stored column, with a test that a NULL-company entry is not offered.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:37:20.342Z","answer_recorded_at":"2026-10-26T03:38:46.264Z","first_independent_review_at":"2026-10-26T05:11:17Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"69d85c6801c67e666759e282462ac194b9515de3c93ded81cf3d92f603c8cd96","record_sha256":"a21d7f24f7c9591adc4e8e51d9af3f42d75555e9596ffad33bc48e12f9d3e6f5"},
    {"record_id":"author-pr83-plan-007","pr":83,"pr_url":"https://github.com/Homie-Improvement/miru/pull/83","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The update tests do not establish the response guarantee stated in the completion criteria.","finding_evidence":"The completion criteria require equal responses for foreign and missing ids for both direct and linked ids on create and update. The request-test unit lists only linked ids for PATCH; its direct-id comparisons cover POST alone. Passing the listed tests therefore leaves the direct-id update guarantee untested. Add a PATCH comparison for `timesheet_entry_id`, checking equal status and full response body for foreign and nonexistent ids.","recorded_resolution":"fixed","recorded_correction":"The update tests now include a PATCH that compares status and full body for a foreign and a nonexistent direct timesheet entry id.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:37:20.342Z","answer_recorded_at":"2026-10-26T03:38:46.264Z","first_independent_review_at":"2026-10-26T05:11:17Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"4336dbc66cb885de59ecf260565bd974aab83c591f45b891be0d8bc51d52485b","record_sha256":"67a345f2ef4a707f40318e6d6e2c9217deb0b89ae52cbc6a1d4cfd5faa338637"},
    {"record_id":"author-pr85-build-001","pr":85,"pr_url":"https://github.com/Homie-Improvement/miru/pull/85","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Midway-rollback example no longer proves the load fails after entries were replaced","finding_evidence":"The coverage map names 'leaves every checkpoint table as it was when a load fails at the audits insert' as the kept example for the midway-failure rollback contract (plan R3). It accepts any StandardError, and it no longer asserts that the built checkpoint verifies, which the deleted monolith_store_4_spec.rb did and plan item 4 requires the kept example to carry. load_checkpoint runs Checkpoint.verify, the column check and the version check (monolith_store.rb:58-63) before it opens the transaction or deletes anything. If one of those fires, nothing is written, the after-snapshot equals the baseline, and the example passes without ever exercising the rollback. The audit-insert failure path is then unguarded. The second-batch and read-back-mismatch examples still prove rollback after the DELETEs, so the regression risk is narrower than losing all rollback coverage. But this example, the one the plan maps the contract to, can pass vacuously, which is the silent false pass the merge was meant to avoid.","recorded_resolution":"fixed","recorded_correction":"Both audit-failure examples now check the checkpoint verifies, and they expect the Store::Error that names entry_audits. A store forced to refuse before deleting rows now fails both.","correction_target":"code_commit","source_path":"spec/lib/timekeeping/handoff/monolith_store_spec.rb","source_line":270,"fix_commit":"3ae00a5d6cb523dbe059c0cdc35fb23fff6b6c8d","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/3ae00a5d6cb523dbe059c0cdc35fb23fff6b6c8d","finding_recorded_at":"2026-10-26T05:07:01.348Z","answer_recorded_at":"2026-10-26T05:21:32.481Z","first_independent_review_at":"2026-10-26T05:31:46Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"cf0793f29dbb231acff09300b863193cbb0d9ba60e343ccd2d96ff85ba514cb7","record_sha256":"b15cd9492f981adfea59f2cdb3c1c060beadae32129282da13309237a728fbbc"},
    {"record_id":"author-pr85-plan-001","pr":85,"pr_url":"https://github.com/Homie-Improvement/miru/pull/85","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep the check of the isolation settings that apply when export reads its rows.","finding_evidence":"The export-isolation merge can discard a stronger assertion. In spec/lib/timekeeping/handoff/monolith_store_4_spec.rb, the example only searches captured SQL for `REPEATABLE READ` and `READ ONLY`. The original monolith_store_spec.rb example reads `current_setting('transaction_isolation')` and `current_setting('transaction_read_only')` during a table read. If export starts correctly but later switches to `READ COMMITTED`, both SQL searches still pass. Concurrent writes could then produce a checkpoint assembled from different database snapshots. The Open Questions condition permits dropping the settings check because the retained example already asserts both phrases. Require the merged example to retain the actual settings check as well as refusal inside an open transaction.","recorded_resolution":"fixed","recorded_correction":"The merged export example now always keeps the live check of the isolation and read-only settings during a table read, as well as the refusal inside an open transaction.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T04:35:25.344Z","answer_recorded_at":"2026-10-26T04:36:16.818Z","first_independent_review_at":"2026-10-26T05:31:46Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"affe05b8cf57a43ecd872d61bf629337dd9863cd896fba6c45c8486d88ba5270","record_sha256":"00c5f495eb0ee46d939ed420f56cd840a2941ca168478d8ebc75bc811ff3ae1f"},
    {"record_id":"author-pr86-build-001","pr":86,"pr_url":"https://github.com/Homie-Improvement/miru/pull/86","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Ownership refusal renders JSON on HTML payer pages","finding_evidence":"During a handoff, a payer who clicks Pay, or comes back from PayPal or Razorpay, sees a raw JSON page ({\"errors\":...,\"reason\":\"ownership_moving\"}) instead of an HTML page. Invoices::PaymentsController#new, #paypal_return and #razorpay_success are browser routes on ApplicationController, and render_ownership_moving always renders JSON. Every other ErrorHandler response uses respond_to with an HTML branch.","recorded_resolution":"fixed","recorded_correction":"A browser GET now gets the HTML error page with status 503 and Retry-After. JSON callers and webhook POSTs, which send no Accept header, still get the JSON refusal.","correction_target":"code_commit","source_path":"app/controllers/concerns/ownership_moving_response.rb","source_line":19,"fix_commit":"cc3bb893fffc0f961e0a983d8541401b0d06ce02","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/cc3bb893fffc0f961e0a983d8541401b0d06ce02","finding_recorded_at":"2026-10-26T05:24:47.056Z","answer_recorded_at":"2026-10-26T06:05:13.910Z","first_independent_review_at":"2026-10-26T06:22:22Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"0c09e5b2a4c7128b4181377b0d1c5cfbee40a0a9906b317c07a501c0fd6f9641","record_sha256":"471ed13aa9406e7b01572da6f5c1c83e5459265569f14dea15e21bb1ffc6eb87"},
    {"record_id":"author-pr86-build-002","pr":86,"pr_url":"https://github.com/Homie-Improvement/miru/pull/86","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"PayPal return captures funds before the fence refuses","finding_evidence":"During a handoff, a payer who returns from PayPal has the order captured, so the money moves. Only after that does InvoicePayment::Settle reach the fence and raise OwnershipMoving. The transaction rolls back, and the payer sees a 503 with no record that the payment happened, so settlement depends entirely on PayPal redelivering the webhook. This path also takes the invoice row lock before the ownership share lock, the reverse of the order the other paths' comments promise.","recorded_resolution":"fixed","recorded_correction":"The PayPal return now takes the fence before the invoice lock. During a handoff it is refused before PayPal is asked to show or capture the order, and no payment row is written.","correction_target":"code_commit","source_path":"app/services/invoice_payment/paypal_capture_fulfillment.rb","source_line":48,"fix_commit":"cc3bb893fffc0f961e0a983d8541401b0d06ce02","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/cc3bb893fffc0f961e0a983d8541401b0d06ce02","finding_recorded_at":"2026-10-26T05:24:47.056Z","answer_recorded_at":"2026-10-26T06:05:13.910Z","first_independent_review_at":"2026-10-26T06:22:22Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"4643618a3e3e404bf39d74850ec07faa7ed27a9faf55ee75767abe2581b51e8d","record_sha256":"43a08e6a72d2175830b347d40f192f0adc94689e1760f492b07cb5ede85b0c6c"},
    {"record_id":"author-pr86-build-003","pr":86,"pr_url":"https://github.com/Homie-Improvement/miru/pull/86","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"admit!-outside-transaction specs accept any StandardError","finding_evidence":"The guard under test raises Timekeeping::BillingFence::TransactionRequired, but both tests accept any StandardError. OwnershipMoving is also a StandardError, so if the ownership row were missing, or committed-data state left a handoff_step set, the example would pass on the handoff refusal instead of the transaction guard. A NoMethodError or a rename would pass it too.","recorded_resolution":"fixed","recorded_correction":"Both specs that call admit! outside a transaction now expect TransactionRequired, so a handoff refusal or an unrelated error no longer satisfies them.","correction_target":"code_commit","source_path":"spec/services/timekeeping/billing_fence_spec.rb","source_line":64,"fix_commit":"cc3bb893fffc0f961e0a983d8541401b0d06ce02","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/cc3bb893fffc0f961e0a983d8541401b0d06ce02","finding_recorded_at":"2026-10-26T05:24:47.056Z","answer_recorded_at":"2026-10-26T06:05:13.910Z","first_independent_review_at":"2026-10-26T06:22:22Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"74ae4f602643176800d2c17c22c329e0d48ddf6cecbfe6e365d1dd26f7185d3b","record_sha256":"157d99c6d04c1ad6380a88370dd75a3a50a6f3d614cc0f8a2627558da4349387"},
    {"record_id":"author-pr86-build-004","pr":86,"pr_url":"https://github.com/Homie-Improvement/miru/pull/86","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"send_invoice queues client mail before its fenced status write","finding_evidence":"During a handoff, sending a draft or never-sent invoice queues the client email first. The model fence then refuses `invoice.record_send!`, and the user gets a 503 that says to try again. The email still goes out, because the `send_invoice` mailer action writes nothing the fence can refuse. The invoice stays a draft with no sent_at and no Invoices::Sent event, and when the user retries as told, the client gets a second email. A rollback cannot undo an email, so a refused request has still had an effect. Before this change record_send! could not fail here; the diff is what makes this ordering unsafe.","recorded_resolution":"fixed","recorded_correction":"send_invoice now records the send before it queues the client email. A handoff refusal therefore answers 503 with no email queued and the invoice left as a draft.","correction_target":"code_commit","source_path":"app/controllers/api/v1/invoices_controller.rb","source_line":98,"fix_commit":"cc3bb893fffc0f961e0a983d8541401b0d06ce02","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/cc3bb893fffc0f961e0a983d8541401b0d06ce02","finding_recorded_at":"2026-10-26T05:24:47.056Z","answer_recorded_at":"2026-10-26T06:05:13.910Z","first_independent_review_at":"2026-10-26T06:22:22Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"f192f91931839d02563c9617e5d85989bdafd70eac9b54c5a583312925559fb3","record_sha256":"5018dee6ecee4c378c610057f507e61392ed7f20709d9e5f6123ce04aba8bce1"},
    {"record_id":"author-pr86-plan-001","pr":86,"pr_url":"https://github.com/Homie-Improvement/miru/pull/86","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"A failed bulk deletion can commit changes to an invoice that must remain intact.","finding_evidence":"plan item 8 assumes that returning false from destroy preserves today's behavior inside the new outer transaction. Consider an invoice with payments and line items. app/models/invoice.rb:40-42 destroys its line items and taxes before checking dependent payments. app/models/invoice_line_item.rb:12 then recalculates invoice totals after each deletion. Today, the failed destroy rolls these changes back. Inside the proposed joined transaction, the failed destroy has no independent rollback boundary, so the outer transaction can commit deleted children and changed totals while returning 422. Keep the shared ownership lock for the whole batch, but isolate each destroy with a savepoint. Test a payment-bearing invoice with line items and taxes, and assert that its children and money fields remain unchanged.","recorded_resolution":"fixed","recorded_correction":"Same fix as the other bulk deletion finding: a savepoint around each destroy inside the shared fence. A test asserts that a payment-bearing invoice keeps its children and money fields.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:40:22.813Z","answer_recorded_at":"2026-10-26T03:43:07.056Z","first_independent_review_at":"2026-10-26T06:22:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"26bfe117267d06069b48d956fa2693873e2353571405784a7bf3cf7b8ed2325d","record_sha256":"35d747e8ec8e7ddf0b0da25aceb5466275ea18d3a1f10894fe0a1a4825bdfa69"},
    {"record_id":"author-pr86-plan-002","pr":86,"pr_url":"https://github.com/Homie-Improvement/miru/pull/86","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A refused payment-link update can leave a payable link with a customer and lose its payment record.","finding_evidence":"plan item 7 calls an orphaned Razorpay link unpaid, but app/services/payment_providers/razorpay_payment_link_service.rb sends notify.sms in the creation request before invoice.update! stores the link. A handoff can refuse that update after the customer receives the link. After the handoff, an operator retry can create and store another link. If the customer pays the first link, superseded_payment_link? in app/services/invoice_payment/razorpay_payment_link_webhook_fulfillment.rb makes its webhook return true without recording the payment. The customer is charged while the invoice remains unpaid. Replace the assumption with a recovery design that retains and reconciles created links or prevents their delivery before durable association. Test this ordering, including the retry and the old link's paid","recorded_resolution":"fixed","recorded_correction":"Payment link creation and storage now run in one fenced transaction. During a handoff the request is refused before any link is created, so no customer gets a link that is not stored.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:40:22.813Z","answer_recorded_at":"2026-10-26T03:43:07.056Z","first_independent_review_at":"2026-10-26T06:22:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"e6cb4c66b0b5eaa55a9882fc36528e138a39a9651ca31875622f6f56f224b11d","record_sha256":"52b68bba2118e225435275868bb960f20a6df7739f113a7b10cd00d00632fa44"},
    {"record_id":"author-pr86-plan-003","pr":86,"pr_url":"https://github.com/Homie-Improvement/miru/pull/86","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A refused payment-link write can leave customers with a payable, untracked link.","finding_evidence":"plan item 7 assumes an orphaned Razorpay link remains unpaid. app/services/payment_providers/razorpay_payment_link_service.rb:18 creates the link before saving its identifier, and payment_link_payload enables SMS notification. During a handoff, the callback refuses storage after Razorpay can send the link. A retry can store a second link. If the customer pays the first link, RazorpayPaymentLinkWebhookFulfillment#superseded_payment_link? causes its webhook to return success without recording the payment. The merchant can then see an unpaid invoice despite receiving money. Separate creation, storage and notification so notifi","recorded_resolution":"fixed","recorded_correction":"The payment-link service now holds the fence across creation and storage, so a link is never sent and then left unstored. The cost is that a handoff can wait for one provider call.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:40:22.813Z","answer_recorded_at":"2026-10-26T03:43:07.056Z","first_independent_review_at":"2026-10-26T06:22:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"7010f5da9bd05cdb16b87f297e9146242bd9c1d97b18057a91d76e14d1c8929a","record_sha256":"670b9437dae54fbd76afad88e550701d33326386949091cc5a967597f8d3010f"},
    {"record_id":"author-pr86-plan-004","pr":86,"pr_url":"https://github.com/Homie-Improvement/miru/pull/86","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Existing mailer writes already bypass the proposed model guard.","finding_evidence":"The deferred audit is not merely protection against hypothetical future writers. app/mailers/client_payment_mailer.rb:19 and app/mailers/payment_mailer.rb:22 call @invoice.update_columns. app/mailers/invoice_mailer.rb:19 does the same before its later status callback. These calls skip before_save, so queued mail can update invoice timestamps during a handoff without taking the ownership lock. A later guarded callback cannot undo the earlier committed update. Audit and fence these existing writes in this change, while keeping email delivery outside the transaction. Add a test that runs queued mail during a handoff and confirms that no invoice column changes.","recorded_resolution":"fixed","recorded_correction":"A new unit wraps each mailer's timestamp write in the fence, keeps delivery out of the lock, and adds tests that run each mailer during a handoff and check that no invoice column changes.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:40:22.813Z","answer_recorded_at":"2026-10-26T03:43:07.056Z","first_independent_review_at":"2026-10-26T06:22:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"4daceb345448d621e52041dca7b36e3086f6e0cefbdf7b71a565e2b53310fb95","record_sha256":"501f4f92d72f26bb54ac6ee0e59952e7609129f5b2c0c25827b97529fff693ee"},
    {"record_id":"author-pr86-plan-005","pr":86,"pr_url":"https://github.com/Homie-Improvement/miru/pull/86","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Include existing callback-bypassing invoice writes in this change.","finding_evidence":"The deferred audit excludes existing writers from the required fence. app/mailers/invoice_mailer.rb:19, app/mailers/payment_mailer.rb:22 and app/mailers/client_payment_mailer.rb:19 call `@invoice.update_columns`, which skips the proposed callbacks. These mailers run separately from the controller transaction, so its ownership lock cannot protect them. During a handoff, they can commit invoice changes without checking ownership. InvoiceMailer then calls `sent!` in its after_action, where the proposed guard can refuse the status change after `sent_at` has already committed. This leaves a partially updated invoice. Include these writers in the current audit and implementation, rather than deferring them. Add tests that run the mailer paths during a handoff and verify that invoice fields remain","recorded_resolution":"fixed","recorded_correction":"InvoiceMailer now writes sent_at and the sent status in one fenced transaction, so a refusal cannot leave sent_at committed without the status change. A test covers this.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:40:22.813Z","answer_recorded_at":"2026-10-26T03:43:07.056Z","first_independent_review_at":"2026-10-26T06:22:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"5d21d12d660e0c56be9f66b4234afb3d379b032ddea527d833ce58bc1a63ffcb","record_sha256":"c3603f5e63342174a0319de355ef6d4cbecf3c7423dc328174840215ed4ef456"},
    {"record_id":"author-pr86-plan-006","pr":86,"pr_url":"https://github.com/Homie-Improvement/miru/pull/86","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The PayPal webhook does not inherit the planned error handler.","finding_evidence":"plan item 5 assumes all webhook controllers inherit ErrorHandler through ApplicationController. However, app/controllers/webhooks/paypal_controller.rb:3 inherits directly from ActionController::API and does not include ErrorHandler. Adding the earlier re-raise required by plan item 9 bypasses its existing 500 response, but cannot reach the proposed shared 503 renderer. Explicitly connect this controller to the common refusal handler, or share a narrow handler that works with ActionController::API. Test a signed PayPal event against the actual events endpoint and assert status 503, reason ownership_moving and Retry-After.","recorded_resolution":"fixed","recorded_correction":"Confirmed: the PayPal controller inherits ActionController::API. It now uses a small shared renderer for the refusal, and a test checks for 503, ownership_moving and Retry-After.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:40:22.813Z","answer_recorded_at":"2026-10-26T03:43:07.056Z","first_independent_review_at":"2026-10-26T06:22:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"1b4bc982c718f1d16da8cc686269f97a4882e05319493bb421058118fe862d0b","record_sha256":"1a8d6e4ba9042503a45ac4507fcd7f7d17b3ce65daca0f76250b109bf0efde23"},
    {"record_id":"author-pr86-plan-007","pr":86,"pr_url":"https://github.com/Homie-Improvement/miru/pull/86","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The deferred callback audit already has confirmed invoice writers that bypass the fence.","finding_evidence":"A queued payment email can run after handoff_step is set. app/mailers/payment_mailer.rb:22 then writes payment_sent_at through update_columns without running the proposed callbacks. app/mailers/client_payment_mailer.rb:19 and app/mailers/invoice_mailer.rb:19 do the same for other invoice timestamps. None appears in the units' file lists. Deferring this audit therefore leaves known invoice writes outside the shared lock, rather than covering every writer as plan item 4 claims. Route these database writes through the fence in this change. Test a mail job that was queued before the handoff but executes during it, and define how the job retries a refusal without holding the database lock during email delivery.","recorded_resolution":"fixed","recorded_correction":"Mailers now raise the refusal before delivery. A mail delivery job set on ApplicationMailer retries it after a wait, and the database lock covers only the timestamp write.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:40:22.813Z","answer_recorded_at":"2026-10-26T03:43:07.056Z","first_independent_review_at":"2026-10-26T06:22:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"20451106546c6f6a39e490c6eff7406ddf70f41762b16cd5156b98ff287832a0","record_sha256":"6bac211c97a6c7e5c9c5f1c00c9352861f5df09ef92300c12a34f3a6b329606d"},
    {"record_id":"author-pr86-plan-008","pr":86,"pr_url":"https://github.com/Homie-Improvement/miru/pull/86","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The deferred write audit leaves the promised coverage unresolved.","finding_evidence":"The stated goal says the model guard enforces the fence on every other writer. However, deferred work excludes the audit of writes that bypass that guard. The plan also leaves callback use by `record_send!` and `viewed!` unresolved, while requiring explicit wrappers if they bypass callbacks. These instructions leave two possible completion criteria: verify every writer now, or finish with coverage unknown. Move the audit into this change and require any existing bypassing writers to use the fence. Defer only protection against bypasses introduced by future changes.","recorded_resolution":"fixed","recorded_correction":"The audit is now done in the plan. A search of app/ for update_columns, update_all, delete_all and insert_all found only the three mailers on billing tables, and they are now fenced. The deferred item is removed.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:40:22.813Z","answer_recorded_at":"2026-10-26T03:43:07.056Z","first_independent_review_at":"2026-10-26T06:22:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"a0dd2386c4b6250e0aee750789ca486130d985f729d01c5b254ba068a72c8d1f","record_sha256":"9f2dd5cf80a7be587abe379a1a6bf20dfe59ec79a17f8264d8f3c1728f13af95"},
    {"record_id":"author-pr86-plan-009","pr":86,"pr_url":"https://github.com/Homie-Improvement/miru/pull/86","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The model callbacks leave existing invoice writers outside the fence.","finding_evidence":"The callback concern cannot provide the complete write boundary claimed by plan item 4. Existing mailers bypass it: app/mailers/invoice_mailer.rb:19 writes sent_at with update_columns; app/mailers/payment_mailer.rb:22 and app/mailers/client_payment_mailer.rb:19 do the same for payment timestamps. These jobs can change invoices during a handoff without acquiring the ownership lock. Deferring the audit therefore leaves known writers outside the required guarantee. Route these writes through BillingFence.transaction in this change, and audit the other callback-bypassing operations before treating the concern as a backstop.","recorded_resolution":"fixed","recorded_correction":"The mailer writes now go through the fence in this change. The callback concern stays a backstop only for writes that run callbacks, and the audit found no other bypassing writer.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:40:22.813Z","answer_recorded_at":"2026-10-26T03:43:07.056Z","first_independent_review_at":"2026-10-26T06:22:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"77537e588a446c1c24eb9d0bb872f9dc86e07b1a883eddb88a4cf8ba5cbe543e","record_sha256":"6bf5cc6966a5ff89eaba3d04e95e0c10465a6810e19bc65ffe08be704322843a"},
    {"record_id":"author-pr86-plan-010","pr":86,"pr_url":"https://github.com/Homie-Improvement/miru/pull/86","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The shared bulk-delete transaction can commit child deletions from a failed invoice destroy.","finding_evidence":"plan item 8 preserves the false return from destroy, but not its rollback behavior. In app/models/invoice.rb:40-42, dependent destruction of line items and taxes precedes the payment restriction. InvoiceLineItem's after_destroy callback also saves the invoice totals. Today, each destroy has its own transaction. Inside the proposed outer transaction, destroy joins it, and its ActiveRecord::Rollback does not roll back that outer transaction. A rejected invoice can therefore retain its payment while losing its lines, taxes and original totals. Keep one outer fence, but isolate each destroy in a requires_new savepoint so a rejected destroy rolls back its own changes. Test a mixed batch with a payment-bearing invoice that has lines and taxes. Assert that the rejected invoice and all its children and totals remain unchanged.","recorded_resolution":"fixed","recorded_correction":"Each invoice destroy in bulk deletion now runs in its own savepoint inside the one fenced transaction, and a mixed-batch test checks that a refused invoice keeps its lines, taxes and totals.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T03:40:22.813Z","answer_recorded_at":"2026-10-26T03:43:07.056Z","first_independent_review_at":"2026-10-26T06:22:22Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"2cf4e49740092d5ef81f4794f229c9912d2b9fe491f74ee985822703ff5cd82c","record_sha256":"36791a1646655dcc36362ca2e8ea5ea112c2307e167fe0cdb005e5cb170eeeda"},
    {"record_id":"author-pr87-plan-001","pr":87,"pr_url":"https://github.com/Homie-Improvement/miru/pull/87","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Rejecting unknown override keys adds behavior beyond the requested extraction.","finding_evidence":"The ticket asks to share the builder and says the move \"changes no behaviour\". It does not ask for stricter fixture validation. In spec/contracts/timekeeping/contract_example_helpers.rb, snapshot_item currently accepts extra fields through merge(fields.transform_keys(&:to_s)). The new ArgumentError rule changes that behavior and requires special handling for intentionally malformed fixtures. Keep the shared builder's existing merge behavior and omit the new rejection test. Unknown-key rejection can be considered separately; it is not needed to remove the duplicated snapshot defaults.","recorded_resolution":"fixed","recorded_correction":"Agreed. The shared builder now merges overrides exactly as snapshot_item does today, with no new rejection. Refusing unknown keys is listed as separate follow-up work.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T04:57:03.138Z","answer_recorded_at":"2026-10-26T04:57:43.076Z","first_independent_review_at":"2026-10-26T05:50:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"87a669a3b0de116544105e0b72c72ecb04b85a171bf6cbd628f50f0690d44ada","record_sha256":"b62e4fef029ed3c24738645bb81edb748c582d11946fe97fd5d6ad4b61d7b9c9"},
    {"record_id":"author-pr90-build-001","pr":90,"pr_url":"https://github.com/Homie-Improvement/miru/pull/90","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Caller-transaction refusal test accepts any StandardError","finding_evidence":"This example should prove that export refuses to run inside a caller's transaction (plan item 2). Because it matches any StandardError, it also passes on unrelated failures: a NoMethodError, a SQL error, a ShapeError, or a Refused from the seed. If the isolation: argument were dropped and export quietly joined the caller's snapshot, the test could still pass.","recorded_resolution":"fixed","recorded_correction":"The open-transaction test now expects ActiveRecord::TransactionIsolationError, then checks the same source exports normally outside the transaction. It passed when run.","correction_target":"code_commit","source_path":"spec/lib/timekeeping/handoff/legacy_source_5_spec.rb","source_line":78,"fix_commit":"e51a68df2b31abb07f6e1c3669f47bee8d19c6eb","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/e51a68df2b31abb07f6e1c3669f47bee8d19c6eb","finding_recorded_at":"2026-10-26T06:04:18.433Z","answer_recorded_at":"2026-10-26T06:35:56.550Z","first_independent_review_at":"2026-10-26T06:50:39Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"969a1ad2c15d93c9ee34611e080faef623854914c4e05d0951451b920d46a7a3","record_sha256":"f1ce76fcc66c611ca7128483fb4befba364ca2ed39b7034c57910aba9240f602"},
    {"record_id":"author-pr90-build-002","pr":90,"pr_url":"https://github.com/Homie-Improvement/miru/pull/90","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"LegacySource contracts duplicated across five spec files, seed reloaded per group","finding_evidence":"One unit's acceptance tests are split over legacy_source_spec.rb and legacy_source_2..5_spec.rb, and the same contracts are asserted in up to four places: the foreign invoice number refusal (spec, _3, _5), the company_id mismatch (spec, _3), the repeat export (_2, _4, _5) and the self-created audit export (spec, _2, _4). Each example sits in its own top-level RSpec.describe, so two_tenant_seed's before(:all) truncates and reloads the seed of thousands of rows for each one. That defeats the new :seeded_without_transaction tag, whose purpose is to keep the seed loaded between examples. Several files also repeat the frozen_string_literal magic comment partway through, where Ruby ignores it.","recorded_resolution":"fixed","recorded_correction":"The five LegacySource spec files are now one, with each distinct assertion kept once. All read-only seeded examples share one group, so the seed loads once for them. Each example that writes rows has its own group.","correction_target":"code_commit","source_path":"spec/lib/timekeeping/handoff/legacy_source_3_spec.rb","source_line":7,"fix_commit":"e51a68df2b31abb07f6e1c3669f47bee8d19c6eb","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/e51a68df2b31abb07f6e1c3669f47bee8d19c6eb","finding_recorded_at":"2026-10-26T06:04:18.433Z","answer_recorded_at":"2026-10-26T06:35:56.550Z","first_independent_review_at":"2026-10-26T06:50:39Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"0dd883fb1b80cfe33df35881f1ae49921c4747afbcc2e5804ccb182bb0663e95","record_sha256":"3453f0bdb0e93313079ec0a92d66fd5c151a886f8194bf02a6fc43b2cd530349"},
    {"record_id":"author-pr90-build-003","pr":90,"pr_url":"https://github.com/Homie-Improvement/miru/pull/90","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Live unlocked-link listing re-implements SeedDisposition helper","finding_evidence":"Plan plan item 5 names SeedDisposition.invoices_with_unlocked_links as the live listing, but the derivation rebuilds that listing inline. The listing's shape is now defined twice: in the helper that writes the checked-in record and in the derivation that compares live data against it. If either changes (sort order, an extra key, another disposition), the cutover's comparison drifts from the recorder without any error and either refuses a good record or accepts a wrong one. The derivation also wraps both sides in a made-up {\"companies\" => {\"this company\" => ...}} envelope only so it can call the whole-record comparators.","recorded_resolution":"fixed","recorded_correction":"The live listing now comes from SeedDisposition.invoices_with_unlocked_links, so the recorder and the cutover share one definition. The comparison wrapper stays because the existing whole-record comparators need it.","correction_target":"code_commit","source_path":"lib/timekeeping/reservation_derivation.rb","source_line":99,"fix_commit":"e51a68df2b31abb07f6e1c3669f47bee8d19c6eb","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/e51a68df2b31abb07f6e1c3669f47bee8d19c6eb","finding_recorded_at":"2026-10-26T06:04:18.433Z","answer_recorded_at":"2026-10-26T06:35:56.550Z","first_independent_review_at":"2026-10-26T06:50:39Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"72014a0bacc01a7b07093483b111872325e5b0ab85c7dd80dee14540d625f407","record_sha256":"5835737883a51e88859d826071e1d7561d1b7271bd09ac721ef1bbdb04b2123f"},
    {"record_id":"author-pr90-build-004","pr":90,"pr_url":"https://github.com/Homie-Improvement/miru/pull/90","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"OWNER_TABLE rows left mutable; only outer hash frozen","finding_evidence":"OWNER_TABLE is the single source of truth for which pre-flight list maps to which owner invoice and disposition. The shallow .freeze leaves each row writable, so a stray mutation anywhere in the process would change every derived reservation and nothing would raise. This contradicts docs/solutions/conventions/freeze-rule-table-rows-not-only-the-outer-array.md.","recorded_resolution":"fixed","recorded_correction":"Each OWNER_TABLE row is now frozen as well as the table itself. A spec asserts both without attempting a write.","correction_target":"code_commit","source_path":"lib/timekeeping/reservation_derivation.rb","source_line":29,"fix_commit":"e51a68df2b31abb07f6e1c3669f47bee8d19c6eb","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/e51a68df2b31abb07f6e1c3669f47bee8d19c6eb","finding_recorded_at":"2026-10-26T06:04:18.433Z","answer_recorded_at":"2026-10-26T06:35:56.550Z","first_independent_review_at":"2026-10-26T06:50:39Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"2caf45bf26a0a737500cdaf978089b3d4a3e0d0190b11933ff380fa3e730460e","record_sha256":"c8133d1e5c700015b56e30ea5400f0659d28c93f1949bd26eeb7720f744e6648"},
    {"record_id":"author-pr90-build-005","pr":90,"pr_url":"https://github.com/Homie-Improvement/miru/pull/90","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Recorded company name matching zero or several companies untested","finding_evidence":"recorded_by_company stops a recorded listing from being applied to the wrong company when its name is missing or shared. No example passes a record whose company name matches no company or two companies, so this refusal path, keyed by name rather than company id, never runs in a test. Plan U3 lists \"unknown record company refuses\" and plan item 3 makes the behaviour part of the contract. If a regression took the first matching id, one tenant's listing would be applied to another tenant and no test would fail.","recorded_resolution":"fixed","recorded_correction":"Added a test that creates two companies with one shared name and records a listing under that name and under a name no company has. Export refuses, keyed by each name, with \"matches 2 companies\" and \"matches 0 companies\".","correction_target":"code_commit","source_path":"lib/timekeeping/handoff/legacy_source.rb","source_line":133,"fix_commit":"e51a68df2b31abb07f6e1c3669f47bee8d19c6eb","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/e51a68df2b31abb07f6e1c3669f47bee8d19c6eb","finding_recorded_at":"2026-10-26T06:04:18.433Z","answer_recorded_at":"2026-10-26T06:35:56.550Z","first_independent_review_at":"2026-10-26T06:50:39Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"a027f31d08374bdc4eab9278bd026dd8981e73507fe40716ec4658bc10d07f8a","record_sha256":"20e998c9dc65691bf20fcf72bf6acc34643bd20ed5bdabc75dd8b338de00d24b"},
    {"record_id":"author-pr90-plan-001","pr":90,"pr_url":"https://github.com/Homie-Improvement/miru/pull/90","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A named legacy author must not become an anonymous system actor.","finding_evidence":"Consider a TimesheetEntry audit with nil user_id and user_type, but username \"legacy-importer\". The audits table has a username column in db/schema.rb. app/controllers/api/v1/audit_logs_controller.rb:63–67 preserves that author when no user record exists. plan item 12 instead assigns system 0, and the audit input in Seams omits username, so conversion cannot distinguish this row from an anonymous system change. The checkpoint would misattribute the change and discard its recorded author. Read username and reserve the system stand-in for genuinely anonymous rows. Preserve named authors through an explicit supported mapping, or refuse these rows with a per-company explanation when the canonical shape cannot represent them. Add this case to the converter tests.","recorded_resolution":"fixed","recorded_correction":"Agreed. The converter now reads the stored username. The system stand-in is used only when both user and username are empty. A row with a username but no user is refused for its company with that reason, because the audit row's shape has no field for a name.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T04:37:58.953Z","answer_recorded_at":"2026-10-26T04:39:28.433Z","first_independent_review_at":"2026-10-26T06:50:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"1ee6643fe813dbade050aabaab0d4c2f275d6f6bfddadf48fcdab15299652066","record_sha256":"863b0c0804eff252ae4cd43c6a195b11e9d3bb98118bf8c2cc7ff3585e789e8e"},
    {"record_id":"author-pr90-plan-002","pr":90,"pr_url":"https://github.com/Homie-Improvement/miru/pull/90","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The audit conversion tests do not cover deletion history.","finding_evidence":"The ticket requires every TimesheetEntry audit row, but the conversion rules and tests cover only create and update changes. A legacy destroy audit also needs its original change values preserved. Copying its action alone does not define its before and after values. Add an explicit destroy conversion rule and test. Also test the planned refusal when its entry no longer exists, so operators receive a clear reason rather than an incomplete checkpoint. Without these cases, the tests cannot establish the promised coverage of legacy history.","recorded_resolution":"fixed","recorded_correction":"Added a rule for destroy audits: before holds the stored values and after is an empty object. Tests now cover that mapping and the refusal when the destroyed entry is no longer in the checkpoint.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T04:37:58.953Z","answer_recorded_at":"2026-10-26T04:39:28.433Z","first_independent_review_at":"2026-10-26T06:50:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"545a9b71551f357a1e53b060100b4b846fc18c632b810891142b256e9ed0ffd2","record_sha256":"a4f3a61e5a800fb165431889e7877de49a500f25ec6e00ca3b29959e75d89afc"},
    {"record_id":"author-pr90-plan-003","pr":90,"pr_url":"https://github.com/Homie-Improvement/miru/pull/90","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The correction snapshot has two conflicting timestamp rules.","finding_evidence":"plan item 8 defines a correction snapshot as the copied entry row with only `locked` and `version` changed. plan item 9 instead assigns “every correction time” to `captured_at`. The correction shape has no separate capture timestamp, so implementers could either preserve or replace the entry's `created_at` and `updated_at`. These choices produce different correction snapshots. State which fields use `captured_at` and which retain their stored values, and test that distinction. If capture time belongs outside the snapshot, declare its field in the correction shape.","recorded_resolution":"fixed","recorded_correction":"A correction snapshot now keeps the entry's stored created_at and updated_at. The capture time goes in its own captured_at field on the correction item, and a test checks both.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T04:37:58.953Z","answer_recorded_at":"2026-10-26T04:39:28.433Z","first_independent_review_at":"2026-10-26T06:50:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"8640b29c6934b8cbf9fa0e5241130cc223e632d1e85dc4723645860f9e079f44","record_sha256":"4f9ff7b1a25ffec4e8b400d3e87e1694ae780cb9f464072c5269b5ac2ef2cf9b"},
    {"record_id":"author-pr90-plan-004","pr":90,"pr_url":"https://github.com/Homie-Improvement/miru/pull/90","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The seeded export tests need a setup that runs outside the test transaction without deleting the seed.","finding_evidence":"The planned integration tests cannot combine the existing seed context with the transaction setup used by MonolithStore tests. spec/support/two_tenant_seed.rb loads the seed once in before(:all). spec/support/database_cleaner.rb then opens a transaction around each ordinary example, which prevents export from setting repeatable-read isolation. Adding :without_transaction instead truncates every table before each example and deletes that seed. Specify a setup that loads the seed after truncation and runs export outside a transaction. Include any support-file changes in the unit's Files list. Otherwise these tests fail before they exercise the source, leaving the tenant and audit checks unproved.","recorded_resolution":"fixed","recorded_correction":"Confirmed in the spec support files: the seed loads once per group and ordinary examples run in a transaction. The plan now adds a tag that skips that transaction without truncating, and gives each writing test its own seeded group.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T04:37:58.953Z","answer_recorded_at":"2026-10-26T04:39:28.433Z","first_independent_review_at":"2026-10-26T06:50:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"146b43e999112c46b52d25e3d1b2a77be61ae84a930416e902b275f42b50949d","record_sha256":"76066bfd121d8911889964171f6e66b36e86691d4fb25bab15f1a0e292d8d6a9"},
    {"record_id":"author-pr91-build-001","pr":91,"pr_url":"https://github.com/Homie-Improvement/miru/pull/91","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Net::HTTP silently retries timed-out checkpoint GET/PUT once","finding_evidence":"The class comment (line 14) and plan plan item 9 say the client never retries, but Net::HTTP's default max_retries of 1 re-sends GET and PUT after Net::ReadTimeout or a connection reset. A load that runs past 300 s is sent again, waits behind the first load's EXCLUSIVE table locks, holds a second thread and connection, and replaces the tables twice. The operator sees Store::Error only after up to 600 s. WebMock's to_timeout raises Net::OpenTimeout, which is never retried, so the specs cannot catch this. Reloading the same checkpoint is safe, so the cost is delay, not lost data.","recorded_resolution":"fixed","recorded_correction":"Confirmed with a local socket: by default Net::HTTP sent a timed-out PUT twice, and with max_retries: 0 it sends it once. The client now passes max_retries: 0, and a new spec fails without that setting.","correction_target":"code_commit","source_path":"lib/timekeeping/handoff/timekeeping_store.rb","source_line":89,"fix_commit":"37aeebafb1b6bf0e7d955c875e6a59f5490a4164","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/37aeebafb1b6bf0e7d955c875e6a59f5490a4164","finding_recorded_at":"2026-10-26T06:25:17.742Z","answer_recorded_at":"2026-10-26T06:41:15.105Z","first_independent_review_at":"2026-10-26T06:58:16Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"f6557ce406bf959bd0001bbcdd9bff20215265b75c68a8c72fbf043e7faf7583","record_sha256":"889b4a2ca1cfcfc1927a7bc9b7b581ff9523c2ba47d306e1781aaac532e9135b"},
    {"record_id":"author-pr91-plan-001","pr":91,"pr_url":"https://github.com/Homie-Improvement/miru/pull/91","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Copy the new path gem before Docker installs the monolith bundle.","finding_evidence":"The shared-gem extraction will break the configured production image builds. Dockerfile.production:40-43 copies only Gemfile and Gemfile.lock before running bundle install; it copies gems/ later. Dockerfile.kamal uses the same order. config/deploy.yml and config/deploy.kamal.yml select these files. Once the root Gemfile requires gems/timekeeping_handoff, Bundler cannot find that directory during installation. The build-context assumption does not establish that the gem exists in this earlier layer. Add the affected Dockerfiles to this unit, copy the gem before bundle install, and verify the configured image builds.","recorded_resolution":"fixed","recorded_correction":"Confirmed: Dockerfile, Dockerfile.production and Dockerfile.kamal copy only Gemfile and Gemfile.lock before bundle install. The extraction unit now copies the gem directory before that step in all three.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T04:40:41.456Z","answer_recorded_at":"2026-10-26T04:42:40.978Z","first_independent_review_at":"2026-10-26T06:58:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"272a611a53ee0415d4c40837f4061f8924e2d83b3956619724cd705678b11450","record_sha256":"55b00d2d99d372257e173edb4b6374cad571ebdf97a4ab4229c9449619d4c62d"},
    {"record_id":"author-pr91-plan-002","pr":91,"pr_url":"https://github.com/Homie-Improvement/miru/pull/91","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Correction snapshots consume a version that the next entry write will reuse.","finding_evidence":"plan item 7 and U4 change only the outbox, leaving a loaded version-0 entry unchanged. Deliver its version-1 correction, then admit writes and edit that entry. timekeeping/app/services/timekeeping/entry_writer.rb:227 increments the stored version from 0 to 1. app/services/timekeeping/projection_consumer.rb:188 ignores that edit because the projection already holds version 1. Reports therefore miss the employee's accepted edit until another write happens. Make the correction transaction also establish an entry version that makes subsequent writes strictly newer than the correction. Repeating the correction must not increment it again. Add a regression that delivers the correction, performs the first normal edit, and verifies that the projection applies it.","recorded_resolution":"fixed","recorded_correction":"Confirmed: EntryWriter sets the version to the stored version plus one, and the projection ignores a version it already holds. The correction now raises the stored version to the correction's, never lowering it.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T04:40:41.456Z","answer_recorded_at":"2026-10-26T04:42:40.978Z","first_independent_review_at":"2026-10-26T06:58:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"97ffc6157d86f9920f8bbd1ce67e36c263253b7e8be2774cea3e01fa3882aa84","record_sha256":"f353427314d76db218b43e405e740addf96b999f59eee4299c27c42e71952729"},
    {"record_id":"author-pr91-plan-003","pr":91,"pr_url":"https://github.com/Homie-Improvement/miru/pull/91","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Extend test cleanup to remove committed invoice operation rows.","finding_evidence":"This unit requires tests outside transactional fixtures and seeds populated invoice_operations. The shared context it selects truncates tables in timekeeping/spec/support/entry_write_context.rb:46-56, but that list excludes invoice_operations. Those committed rows will survive examples, affect later exports, and make prepared-operation tests contaminate later tests. Add this support file to the unit and include invoice_operations in its cleanup. Verify that the new store tests and the existing suite pass together in different example orders.","recorded_resolution":"fixed","recorded_correction":"The cleanup in timekeeping/spec/support/entry_write_context.rb does leave out invoice_operations. The store unit now adds that table to the TRUNCATE list and runs the suite in random order.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T04:40:41.456Z","answer_recorded_at":"2026-10-26T04:42:40.978Z","first_independent_review_at":"2026-10-26T06:58:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"e4452d8c7e27169690aa71fda69485f04183a130b92cc5df603e84aae733336a","record_sha256":"5a095971d1787e847050688c9e98c19feb537fbd8fd4ee40850d93ebc82967ea"},
    {"record_id":"author-pr91-plan-004","pr":91,"pr_url":"https://github.com/Homie-Improvement/miru/pull/91","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep handoff error handling out of the shared service controller.","finding_evidence":"plan item 4 puts handoff-specific error mappings in the authentication base that MetricsController will also inherit. timekeeping/app/controllers/timekeeping/internal/v1/metrics_controller.rb calls WriteMetrics.folded_windows, not Admission. A database lock timeout on that route would therefore become fence_timeout even though no fence ran. Mapping every ActiveRecord::RecordNotFound to admission_missing also gives later service endpoints the wrong failure meaning. Keep the shared authentication check, but scope these mappings to the handoff operations that establish their meaning. This prevents unrelated endpoints from silently inheriting the handoff error contract.","recorded_resolution":"fixed","recorded_correction":"The shared service controller now holds only the credential check. Handoff error mapping sits in a handoff base controller, and the admission errors are mapped only in the admission controller.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T04:40:41.456Z","answer_recorded_at":"2026-10-26T04:42:40.978Z","first_independent_review_at":"2026-10-26T06:58:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9a3ee945b806397dfb9f1a51685f9553989e17f4951c2318269423bd6fcd76c9","record_sha256":"ad636f1200144e5bd9a06f88af64145280ede79382cfbbbf2e4ab07fb3670e80"},
    {"record_id":"author-pr91-plan-005","pr":91,"pr_url":"https://github.com/Homie-Improvement/miru/pull/91","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The outbox lock does not prevent duplicate corrections within one request.","finding_evidence":"plan item 7 serializes concurrent requests but deletes matching rows before inserting the supplied snapshots. A single request can contain two snapshots with the same company_id and id. Both pass plan item 8's field checks and the entry-existence check. timekeeping/db/schema.rb keys outbox_snapshots by outbox_position, so both inserts succeed for the same generation. With different locked values at version 1, the projection accepts the first and ignores the second rather than replacing it. Validate uniqueness of (company_id, id) before deleting anything, or define one deterministic replacement per key. Add a same-request duplicate test as well as repeated-request tests.","recorded_resolution":"fixed","recorded_correction":"A request that names the same company and entry twice is now refused before anything is deleted, and a test covers that case.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T04:40:41.456Z","answer_recorded_at":"2026-10-26T04:42:40.978Z","first_independent_review_at":"2026-10-26T06:58:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"3c9fac164791a4ec54ba531b786be2165abd0f3751a1330fc0b5f6b0b6d91856","record_sha256":"06739572b572a29d3e25e95127d23b70027931564b0914293614292ab5868bd7"},
    {"record_id":"author-pr91-plan-006","pr":91,"pr_url":"https://github.com/Homie-Improvement/miru/pull/91","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The plan both fixes and defers the gem's dependency declaration.","finding_evidence":"The first technical decision says, \"The gem declares no runtime dependency.\" The open questions later leave declaring `activesupport` to implementation. These prescribe different gemspecs, so an implementer cannot tell which decision governs. Choose one dependency policy and make both sections agree.","recorded_resolution":"fixed","recorded_correction":"The plan now settles on one rule: the gem declares no runtime dependency and relies on the host app loading ActiveSupport. The open question about this is removed.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T04:40:41.456Z","answer_recorded_at":"2026-10-26T04:42:40.978Z","first_independent_review_at":"2026-10-26T06:58:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"7f1c8100287de77c31eb118f4231b8ced37f3423955da8c1360f086fd604ad7c","record_sha256":"f3b12305a8e748e4da04f6151f9979ade8011d4ed2634ef58097617e1bcb7357"},
    {"record_id":"author-pr91-plan-007","pr":91,"pr_url":"https://github.com/Homie-Improvement/miru/pull/91","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The planned tests never load a monolith export into Timekeeping's database.","finding_evidence":"The ticket requires a cross-store round trip, but the planned tests prove two different paths. Timekeeping reloads its own export, while the monolith sends its export to a stub that echoes the request. Neither test establishes that Timekeeping preserves populated monolith audits and receipts through its column types and table mappings. Shared SQL and checksum code do not prove that compatibility. Add a portable checkpoint fixture produced by MonolithStore, including populated audits, idempotency receipts and operation receipts. Load it through Timekeeping's real endpoint, export it again, and compare rows, counts and checksums. This needs no process with credentials for both databases.","recorded_resolution":"fixed","recorded_correction":"The same checked-in checkpoint file, produced from populated monolith tables, is loaded through Timekeeping's real load endpoint and exported again. The test compares rows, counts and checksums.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T04:40:41.456Z","answer_recorded_at":"2026-10-26T04:42:40.978Z","first_independent_review_at":"2026-10-26T06:58:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"136d5433aafc49a7c3bdfb209098f5ff74b33a26df11d1a72ef9cbfa1bd70968","record_sha256":"2342895c912593a91aebb97351954fffa85baf9fdcc52608dc02c2c37edb2915"},
    {"record_id":"author-pr91-plan-008","pr":91,"pr_url":"https://github.com/Homie-Improvement/miru/pull/91","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The planned tests omit the required round trip between the two real stores.","finding_evidence":"plan item 11 substitutes a Timekeeping self-round-trip and a monolith HTTP stub that echoes its input. Neither test loads a checkpoint exported by MonolithStore into Timekeeping's real tables. Shared SQL does not prove compatibility between the different table mappings and database schemas. The existing MonolithStore specs exercise only the monolith database. Add a serialized checkpoint fixture exported from populated monolith tables, verify that fixture against MonolithStore, then load it through Timekeeping's endpoint and compare the re-exported rows, counts and checksums. This meets the required test without giving either application credentials for both databases.","recorded_resolution":"fixed","recorded_correction":"The plan now adds a checked-in checkpoint file. A monolith spec proves the file matches a MonolithStore export of populated tables, and Timekeeping loads it through its real endpoint and re-exports it.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T04:40:41.456Z","answer_recorded_at":"2026-10-26T04:42:40.978Z","first_independent_review_at":"2026-10-26T06:58:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d5f132e9e1a8ae82617b992490a92538748b75997535d86dd5126ed248c28124","record_sha256":"9f7a85a3c56d8308dc6138d2b288495fcc2e398ea5bf77e7146dd59960f06fd9"},
    {"record_id":"author-pr92-build-001","pr":92,"pr_url":"https://github.com/Homie-Improvement/miru/pull/92","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Misfiled preflight rows certify another company's reservations","finding_evidence":"check_reservation_classification picks expected rows by the outer map key (line 368). It matches them on entry_id/state and compares only invoice_id/disposition (line 370), so the row's own company_id is never read. check_inputs! (lines 219-237) refuses a money snapshot filed under the wrong company but never looks at expected_reservations. So a row that says it belongs to company 202, filed under 101, produces an empty classification check and a passing verdict for 101 when it should be refused. That breaks R13/R14 and the intent's rule that bad input must not pass silently.","recorded_resolution":"fixed","recorded_correction":"The run now refuses an expected reservation row whose own company_id differs from the company it is filed under, the same way it refuses a misfiled money snapshot. Rows without a company_id are still accepted.","correction_target":"code_commit","source_path":"lib/timekeeping/handoff/reconciliation.rb","source_line":370,"fix_commit":"228808825246cac65d44c8aaaa790ed8d7c210d8","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/228808825246cac65d44c8aaaa790ed8d7c210d8","finding_recorded_at":"2026-10-26T06:32:19.128Z","answer_recorded_at":"2026-10-26T06:48:22.870Z","first_independent_review_at":"2026-10-26T07:02:14Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"265e66880b02e64e5b7f7424ff9a98846f6cd6f39e847b90d76c209fd1b80e49","record_sha256":"d8dc21a7be4a9efe9e46e2ccf143c5b371a3724637d4975a945a180eb7a269ac"},
    {"record_id":"author-pr92-build-002","pr":92,"pr_url":"https://github.com/Homie-Improvement/miru/pull/92","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"report comment says money values are passed; they are nil","finding_evidence":"The public comment says FinancialSnapshot.report gets the changed money values. financial_changes, just below it, deliberately sets old_value and new_value to nil (line 207), and its own comment says so. A later subscriber that relies on the public comment and reads those values will get nil. The plan's Verification Contract asks for every comment the code contradicts to be reported.","recorded_resolution":"fixed","recorded_correction":"The comment on report now says it passes the identifiers of each changed money value (kind, record id and field) without the values. That matches what the code sends.","correction_target":"code_commit","source_path":"lib/timekeeping/handoff/reconciliation.rb","source_line":175,"fix_commit":"228808825246cac65d44c8aaaa790ed8d7c210d8","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/228808825246cac65d44c8aaaa790ed8d7c210d8","finding_recorded_at":"2026-10-26T06:32:19.128Z","answer_recorded_at":"2026-10-26T06:48:22.870Z","first_independent_review_at":"2026-10-26T07:02:14Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"3ed061ee51b6bc97d70529bfbdeac452418898a7b692dcdac62bf53b0f6f9d3b","record_sha256":"d36adb5e196cda9a36c0d0e03889230d649ba91fefe217cf71f2c1abacd58e0f"},
    {"record_id":"author-pr92-plan-001","pr":92,"pr_url":"https://github.com/Homie-Improvement/miru/pull/92","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"A listed correction can hide an unrelated change to the same row.","finding_evidence":"plan item 10 matches corrections using only company_id, check and row_key. It cannot distinguish the expected correction from another difference in that row. For example, a listed projection correction for locked also excuses a changed duration in that projection row. The operator can approve a handoff with an unexplained change to billable time. Represent the specific difference that the pre-flight approved, and match against it rather than allowing every difference for that row and check. Test a listed locked correction together with an unlisted duration change on the same row; the duration change must still abort.","recorded_resolution":"fixed","recorded_correction":"A listed correction now names the check, the row and the field it explains, and it excuses only that field. A test lists a locked correction and changes the duration too, and the duration change aborts.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T05:35:53.156Z","answer_recorded_at":"2026-10-26T05:37:14.546Z","first_independent_review_at":"2026-10-26T07:02:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"e66523cd7158973f22237b416fa7409c6dda78320dc48eb94dabf3da675b459b","record_sha256":"69a03ca0e61e225c16be1850839668eedea5bbc977e2fbc40ad63d23afaed274"},
    {"record_id":"author-pr92-plan-002","pr":92,"pr_url":"https://github.com/Homie-Improvement/miru/pull/92","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"A listed correction must not excuse unrelated changes to the same row.","finding_evidence":"plan item 10 matches corrections only by company, check and row_key. Consider a first-cutover entry whose listed correction changes locked. If copying also changes its note, an entries correction for that row excuses both differences. Totals and money remain equal, so the result can pass with corrupted entry data. lib/timekeeping/preflight.rb classifies lock and ownership conditions; it does not authorize note changes. Identify the expected change within each correction, and match each emitted difference against that change. Add a case where the listed lock correction passes but an additional note change on the same entry aborts.","recorded_resolution":"fixed","recorded_correction":"Corrections now match by field as well as check and row. A test lists a correction for one field and also changes the note on the same entry, and the note change aborts.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T05:35:53.156Z","answer_recorded_at":"2026-10-26T05:37:14.546Z","first_independent_review_at":"2026-10-26T07:02:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9dd73e6bc0542e09fbdc6668d35a129e7ff1e50f280a011790e1083bfefab278","record_sha256":"3d0a06c76b39619b2daed01036feaee40867328b5e75ca07389de25cb8f5853c"},
    {"record_id":"author-pr92-plan-003","pr":92,"pr_url":"https://github.com/Homie-Improvement/miru/pull/92","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Missing both financial snapshots can leave a company's money check passing.","finding_evidence":"plan item 9 detects a missing snapshot only when the company appears in one of the two money maps. plan item 13 also creates company reports from other inputs. If entries identify company A but both money maps omit A, the plan defines no missing-snapshot difference for A. An empty money result then looks like a successful check, although the operator has no proof that invoice values stayed unchanged. Require both snapshots for each company being reconciled, or reject incomplete inputs before producing a report. Add a test with company A's entries present and both snapshots absent.","recorded_resolution":"fixed","recorded_correction":"Same fix as the previous finding. A test gives company A entries and no snapshots, and A's money check aborts.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T05:35:53.156Z","answer_recorded_at":"2026-10-26T05:37:14.546Z","first_independent_review_at":"2026-10-26T07:02:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"7c7e3db9cad9b012675d513b122b57b41892b7c77f7cda6f15d40113e537721d","record_sha256":"8d59b2a55427b26b95ee775d72f21164b1499037a8b59d11f9082a49683e31bd"},
    {"record_id":"author-pr92-plan-004","pr":92,"pr_url":"https://github.com/Homie-Improvement/miru/pull/92","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Missing both financial snapshots can produce a passing money check.","finding_evidence":"plan item 9 checks only the union of money_before and money_after keys. Give run valid checkpoint rows for company 7 but omit company 7 from both snapshot maps. plan item 13 still creates its company report, yet the money check has no pair to compare and no one-sided absence to report. The empty differences list then certifies an unchecked company. app/services/timekeeping/financial_snapshot.rb compares only the supplied pair; it cannot detect an omitted company. Require both snapshots for every company in the reconciliation inputs, and report their absence as a money failure. Test omission from both maps, not only omission from one.","recorded_resolution":"fixed","recorded_correction":"The money check now runs for every company found in any input, and a company with no before snapshot, no after snapshot, or neither gets a missing-snapshot difference that aborts.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T05:35:53.156Z","answer_recorded_at":"2026-10-26T05:37:14.546Z","first_independent_review_at":"2026-10-26T07:02:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"71c769ac8b84bda55398179462a95d7d46c078afd2a3609de7452bc3b97a6408","record_sha256":"20db57178ae95e565f9e81aea6dc7208e3dc86b61e20d88bd67628bb4334b92a"},
    {"record_id":"author-pr92-plan-005","pr":92,"pr_url":"https://github.com/Homie-Improvement/miru/pull/92","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Projection matching by entry id conflicts with the required company-scoped identity.","finding_evidence":"The projection rule in plan item 7 matches rows by entry id alone. plan item 4 instead requires matching by `(company_id, key)`, and U2 explicitly tests companies sharing entry ids. With entry id 7 in companies A and B, plan item 7 cannot identify which checkpoint entry owns either projection row. An implementation could report a difference under the wrong company or reject clean rows. Define how projection matching preserves company identity when ids repeat. Also specify how a projection row with nil or incorrect company_id is attributed without selecting another company's entry. Add a projection test with shared entry ids that establishes this rule.","recorded_resolution":"fixed","recorded_correction":"Projection rows now match checkpoint entries by company and id together. A row with a nil or wrong company is reported under the one company whose entry has that id, or as unassigned when none or several do.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T05:35:53.156Z","answer_recorded_at":"2026-10-26T05:37:14.546Z","first_independent_review_at":"2026-10-26T07:02:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"bdfe55281f3546320179d23e30f58ee6b3791b0af81012d2f7336e47624453f2","record_sha256":"9e8d81d24e5f262fc9d2415ce2f3cce4b25f0fb88f91f4d331fa298a75151241"},
    {"record_id":"author-pr92-plan-006","pr":92,"pr_url":"https://github.com/Homie-Improvement/miru/pull/92","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The money event needs an explicit adapter for reconciliation differences.","finding_evidence":"After a money change, the planned event call cannot consume the result's difference type. plan item 9 converts each financial difference into a reconciliation Difference. plan item 13 retains only those converted objects. However, FinancialSnapshot.report in app/services/timekeeping/financial_snapshot.rb passes its objects to FinancialSnapshotChanged. That event reads record.kind and record.record_id in app/events/timekeeping/financial_snapshot_changed.rb. The planned Difference exposes neither method; it puts these identifiers in row_key instead. Passing it directly raises NoMethodError during a failed handoff and prevents the required financial signal. Specify an adapter that restores the event's required interface from the stored identifiers, or retain a separate compatible value in the result. Extend the changed-discount test to exercise the real FinancialSnapshot.report an","recorded_resolution":"fixed","recorded_correction":"Same fix as the first money finding: the report step builds the shape the money event reads, with no money values, and the changed-discount test runs the real money reporter and serializes its event.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T05:35:53.156Z","answer_recorded_at":"2026-10-26T05:37:14.546Z","first_independent_review_at":"2026-10-26T07:02:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9867e3dccec565b52f1b17ba44c9d9b5ea3cdfe89316ad5fe79a2f290f350b18","record_sha256":"5c407601585700ab4b4cbbe8df63036ae10df8ab9b730d9ef6091eee958befb2"},
    {"record_id":"author-pr92-plan-007","pr":92,"pr_url":"https://github.com/Homie-Improvement/miru/pull/92","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The plan drops equality checks for invoice operation intents.","finding_evidence":"R8 requires equality only for `invoice_operations`, while the ticket also requires equality for every `invoice_operation_intents` row. The declared `run` signature accepts only one `intents` collection, and U2 checks its states and operation links, not unchanged contents. `Checkpoint::TABLES` in `lib/timekeeping/handoff/checkpoint.rb` does not include intents, so checkpoint comparison cannot cover this omission. For example, changing an intent's `invoice_id` or `generation` while retaining its committed state and operation link can produce a passing report. Both fields exist in `db/schema.rb:644-654`. Restore intent-row equality to R8, accept frozen and post-handoff intent rows, and compare them per company. Add tests for changed, missing and extra intent rows so operators cannot approve a handoff with altered receipts.","recorded_resolution":"fixed","recorded_correction":"Intent rows are now compared in full: the run takes the frozen intent rows and the intent rows read after the handoff, and compares every column per company. Tests cover changed, missing and extra intents.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T05:35:53.156Z","answer_recorded_at":"2026-10-26T05:37:14.546Z","first_independent_review_at":"2026-10-26T07:02:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"5dbbbc3dd67bffb1207829676ff68a19e2599cd0552213d990c8bb0cd3b8ac36","record_sha256":"559f41ffe3fc1985969650b0d0438a3eb570d8b22089c956a2904e418ddf35ac"},
    {"record_id":"author-pr92-plan-008","pr":92,"pr_url":"https://github.com/Homie-Improvement/miru/pull/92","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The reduced money differences do not satisfy the existing financial event interface.","finding_evidence":"Define an explicit adapter before passing money differences from the result to `FinancialSnapshot.report`. plan item 3 and plan item 9 replace each financial difference with `Difference(company_id, check, table, row_key, field, reason)`. plan item 13 retains only these differences, but U3 passes them to the existing reporter. In `app/services/timekeeping/financial_snapshot.rb`, that reporter constructs `FinancialSnapshotChanged`. Its `event_data` calls `record.kind` and `record.record_id`, which the replacement type does not expose. A changed discount can therefore raise during event serialization instead of emitting the financial alert. Keep the result free of money values, but specify how its identifiers become the interface that the existing reporter requires. Add a test that serializes the financial event produced by `Reconciliation.report(result)`.","recorded_resolution":"fixed","recorded_correction":"The report step now rebuilds the financial difference shape from the stored identifiers before calling the existing money reporter, and a test serializes the money event it produces.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T05:35:53.156Z","answer_recorded_at":"2026-10-26T05:37:14.546Z","first_independent_review_at":"2026-10-26T07:02:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"538a4332879f4c8a4f641c40069b0d814afdb45c10378c317df9b791ddaa5db2","record_sha256":"e73e89d0dc3ef47212866c93779dc85b176140f604920815d030729115ba334b"},
    {"record_id":"author-pr92-plan-009","pr":92,"pr_url":"https://github.com/Homie-Improvement/miru/pull/92","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The single intent input cannot detect lost or changed intent receipts.","finding_evidence":"The run signature supplies only one intents collection, and plan item 12 requires only company_id, operation_id and state. Remove a settled intent before reconciliation while leaving its committed operation unchanged. All remaining intent-state checks pass because no reference collection proves that the removed receipt existed. A changed generation or invoice_id also escapes these checks. db/migrate/20261025120000_prepare_monolith_for_timekeeping.rb defines these additional intent fields, while Checkpoint::TABLES contains no intent rows. Supply frozen-source and current intent rows, compare their full contents by company, and then apply the terminal-state rules. Test a missing settled intent and a changed non-state field.","recorded_resolution":"fixed","recorded_correction":"The run now takes frozen and current intent rows, compares every intent column per company, and then applies the state rules. Tests cover a missing settled intent and a changed generation.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T05:35:53.156Z","answer_recorded_at":"2026-10-26T05:37:14.546Z","first_independent_review_at":"2026-10-26T07:02:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"efc2480c949f27f672e5136f8007438c65b7116df1e014760974cd0020718807","record_sha256":"3cb71541fbea565ba80bd461de620968453083398895ebdc61e41e4a2972b88c"},
    {"record_id":"author-pr93-plan-001","pr":93,"pr_url":"https://github.com/Homie-Improvement/miru/pull/93","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Limit the scope-removal stop condition to examples that test company rejection.","finding_evidence":"The plan says to stop if any surviving example cannot fail when the company scope is removed. Several required survivors do not depend on that scope. For example, `changed_for_autosave?` in `app/models/invoice_line_item.rb:68-70` checks dirty state and pending ids, not company membership. Its retained example should still pass after company filtering is removed. The current stop condition therefore blocks an otherwise correct consolidation. Limit it to the two merged company-rejection examples, as the implementation unit's verification already does. Keep the other examples as guards for their separate behaviors.","recorded_resolution":"fixed","recorded_correction":"Same fix as the first finding: only the two merged company-rejection examples must fail without the company scope; the other kept examples guard their own behaviours.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T05:48:05.203Z","answer_recorded_at":"2026-10-26T05:48:44.297Z","first_independent_review_at":"2026-10-26T06:51:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"fe1e01b733e66b720bfcea843972a2212764afae0b677cb7ed1026160bf7ca60","record_sha256":"b45b39fc89d94ed7d9bf43dd8e6aefdbb69a862feb970378bd6bc45acbbccdf8"},
    {"record_id":"author-pr93-plan-002","pr":93,"pr_url":"https://github.com/Homie-Improvement/miru/pull/93","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The linked-entry assertions do not establish that nothing is linked.","finding_evidence":"The requirements retain \"linked identical errors and nothing linked\", but the planned merged example only excludes the foreign or NULL-company entry from `linked_timesheet_entries`. Its input also contains `same_company_entry.id`. A regression that links that permitted entry despite the invalid mixed selection would pass the stated exclusion check. Saving a separate valid line does not close this gap. Make the planned assertions establish that the invalid mixed selection creates no links, while keeping the separate successful-save check.","recorded_resolution":"fixed","recorded_correction":"The merged linked example now asserts the invalid mixed selection links nothing at all and refuses to save, and keeps the separate successful save as the baseline.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T05:48:05.203Z","answer_recorded_at":"2026-10-26T05:48:44.297Z","first_independent_review_at":"2026-10-26T06:51:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"a93219d81b1bdbc7a89d073d8e6c48f7b2755227ed52ad54329377c298f27afa","record_sha256":"d86fccb7c3460c9eac657ca71ee9793de9a85d50e566a72cbffa17ee334a00a7"},
    {"record_id":"author-pr93-plan-003","pr":93,"pr_url":"https://github.com/Homie-Improvement/miru/pull/93","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The stop condition requires failures from examples unrelated to company filtering.","finding_evidence":"The stated goal says to stop if any surviving example cannot fail when the company scope is removed. The implementation and verification sections require this check only for the two merged examples. Preserved examples such as \"reads zero-padded linked ids as decimal, the way the id column casts them\" need not fail when company filtering is removed. The broader condition could therefore stop a correct cleanup. Limit it to the merged not-found examples, consistent with the implementation instructions.","recorded_resolution":"fixed","recorded_correction":"The stop condition now applies only to the two merged not-found examples, matching the implementation unit's check.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T05:48:05.203Z","answer_recorded_at":"2026-10-26T05:48:44.297Z","first_independent_review_at":"2026-10-26T06:51:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"ca79e562712c6be5daabdcb07a16a2df1de740eeb6da05e3c907e3bd06dd028c","record_sha256":"a5d6cbabfb5fdbb83304a34a60e0238e46edef227e4c46c58d4140b8dba3d58c"},
    {"record_id":"author-pr94-plan-001","pr":94,"pr_url":"https://github.com/Homie-Improvement/miru/pull/94","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The required example count uses an incorrect baseline.","finding_evidence":"The six current files contain 34 examples, not 35: the base file has 12, and the others have 6, 2, 4, 7 and 3. Removing the 12 duplicates listed in the plan leaves 22 examples. The verification for the consolidation instead requires 23, so the stated merge cannot pass that check without adding or retaining an extra example. Change the baseline to 34 and the expected merged count to 22, or identify the additional distinct example the plan intends to add.","recorded_resolution":"fixed","recorded_correction":"I recounted: the base file has 12 examples, so the six files hold 34. Removing the 12 duplicates leaves 22, and the check for the merged file now expects 34 and 22.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T06:32:21.292Z","answer_recorded_at":"2026-10-26T06:33:04.221Z","first_independent_review_at":"2026-10-26T07:10:12Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"26c9ad5357bf9329549a3e76f787eeaa223be27dce10f897246ff058036e4111","record_sha256":"62e73339b60856e1adf17f698d5146ff1c468145a0ef8c87265a0dbe193e22e7"},
    {"record_id":"author-pr104-build-001","pr":104,"pr_url":"https://github.com/Homie-Improvement/miru/pull/104","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Meta spec restates the request-spec assertions as source-text regexes","finding_evidence":"None of the plan's requirements or implementation units asks for this meta spec. It copies the folded request specs' assertions (no echoed id, no writes, locked baseline, repeated refusal, replace-twice-then-clear) and their helper and let counts as regexes over spec source, so the same facts now live in two places. Changes that alter no behaviour turn it red: renaming patch_company_scope_line, rewrapping a line inside `2.times do`, or adding a second helper method anywhere after the describe (the section runs to end of file). It also cannot show that any assertion runs or can fail. Only the request specs and the plan's scope-removal check can show that.","recorded_resolution":"fixed","recorded_correction":"I removed the two examples that restated the refusal assertions as regexes, along with the lock-baseline pattern. The describe-name, one-describe-with-shared-lets and one-helper checks stay, because the issue asks for that layout.","correction_target":"code_commit","source_path":"spec/meta/invoice_entry_company_scope_request_specs_spec.rb","source_line":64,"fix_commit":"ead4a5b67c9dcc90ca4bb0b8412fb2f8c95262df","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ead4a5b67c9dcc90ca4bb0b8412fb2f8c95262df","finding_recorded_at":"2026-10-26T07:36:03.255Z","answer_recorded_at":"2026-10-26T07:51:16.379Z","first_independent_review_at":"2026-10-26T08:03:02Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"eab52619f9d0dd62537d9d2cbd8bd950f4f19386c7915ddc0664d0cbb6b06119","record_sha256":"2013c7efa085b8f2619c0a7eef2700aeecd37e6a1b04918ea9d180a412661c87"},
    {"record_id":"author-pr104-plan-001","pr":104,"pr_url":"https://github.com/Homie-Improvement/miru/pull/104","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep the no-echo assertion in the repeated linked refusal example.","finding_evidence":"The assertion map omits an existing check from the repeated linked refusal example: spec/requests/api/v1/invoices/create_spec.rb:783 excludes foreign_entry.id from the response. Consider a regression that echoes the submitted id only when a linked list contains one entry. The mixed own-plus-foreign example would not catch it. The repeated example could still return identical 422 bodies, write nothing, and preserve locks while exposing the id. Keep the no-echo check in that example's mapped assertions.","recorded_resolution":"fixed","recorded_correction":"The repeated linked refusal example now also checks that the response never includes the foreign entry's id, as create_spec.rb line 783 does today.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T06:59:06.144Z","answer_recorded_at":"2026-10-26T06:59:50.699Z","first_independent_review_at":"2026-10-26T08:03:02Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"2acd56d530dae7015b3b3c796ada90ce2f6a6b0bcc4e6f2dd8f836876f606961","record_sha256":"aab8e93af60e0fb1bb2dd41c3a749467da27ecffbc7e86a4a6c10a29b4886a19"},
    {"record_id":"author-pr104-plan-002","pr":104,"pr_url":"https://github.com/Homie-Improvement/miru/pull/104","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Limit the scope-removal stop condition to refusal examples.","finding_evidence":"The stop condition requires stopping if any merged example passes after removing the company filter. The update suite also retains a successful same-company replacement-and-clear example. That example should still pass: app/models/invoice_line_item.rb resolves the requested ids through entries_in_invoice_company, and removing its company filter does not exclude valid same-company entries. The current condition would therefore stop a correct implementation. Limit it to the foreign-company and NULL-company refusal examples, as the implementation units already describe.","recorded_resolution":"fixed","recorded_correction":"The stop condition now covers only the foreign and NULL-company refusal examples. It also says the same-company replace-then-clear example should stay green when the company filter is removed.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T06:59:06.144Z","answer_recorded_at":"2026-10-26T06:59:50.699Z","first_independent_review_at":"2026-10-26T08:03:02Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"778be0b1321d6e3f8e17dd43ced4b0e7c11ef6c2ac9a436fc53864b6b435fa5f","record_sha256":"bc6eb246180b9ce78fad165c5daffe205d1322ba0f0e8714b6600fe3b5d8f5bd"},
    {"record_id":"author-pr104-plan-003","pr":104,"pr_url":"https://github.com/Homie-Improvement/miru/pull/104","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The assertion map weakens the existing checks of the 422 error shape.","finding_evidence":"The ticket requires every distinct assertion to survive. The assertion map only requires `JSON.parse(body)[\"errors\"]` to be present for linked create requests. The existing check at spec/requests/api/v1/invoices/create_spec.rb:1035 requires that field to include \"not found\". Equivalent checks exist for direct and NULL-company create requests at :980 and :1084, and linked and direct update requests at spec/requests/api/v1/invoices/update_spec.rb:630 and :728. A raw-body substring check does not preserve this contract: \"not found\" could move outside `errors` while these replacement checks pass. Keep the parsed `errors` content checks in the corresponding consolidated cases. This preserves coverage of the response shape without retaining duplicate exam","recorded_resolution":"fixed","recorded_correction":"I confirmed parsed `errors` include \"not found\" checks at create_spec.rb lines 980, 1035 and 1084 and at update_spec.rb lines 630 and 728. Each merged refusal example now keeps that check.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T06:59:06.144Z","answer_recorded_at":"2026-10-26T06:59:50.699Z","first_independent_review_at":"2026-10-26T08:03:02Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"09bf39497590ef0e57f658e21ab84528b8391ee794d92342add5e3e7d64f993b","record_sha256":"ee5267e527128bedf698af10ff1bc75ca25d9a39b25ebd755cdbd2750e658eac"},
    {"record_id":"author-pr104-plan-004","pr":104,"pr_url":"https://github.com/Homie-Improvement/miru/pull/104","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The stop condition also rejects the planned successful update example.","finding_evidence":"The stop condition says to stop if \"any merged example passes\" after removing company scoping. The update example \"link-only replace is idempotent on repeat, then clear\" uses valid entries and should still pass without that scope. The implementation units instead require only foreign and NULL-company refusal examples to fail. Restrict the stop condition to those refusal examples, and state that the valid replacement-and-clear example should remain green. Otherwise an implementer can stop on the expected result of a correct verification run.","recorded_resolution":"fixed","recorded_correction":"Same fix as the first stop-condition finding. Only the refusal examples must fail when the scope is removed, and the valid replace-then-clear example is expected to stay green.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T06:59:06.144Z","answer_recorded_at":"2026-10-26T06:59:50.699Z","first_independent_review_at":"2026-10-26T08:03:02Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"3551e5c8d1252e302607ce0cb99bd71ea43b3c29f5d1f5c028f83721d1201c32","record_sha256":"121f05f5a86a28de9d3b5af202c4dc7c07639ffe22516e60eb8231003fa37fe6"},
    {"record_id":"author-pr111-build-001","pr":111,"pr_url":"https://github.com/Homie-Improvement/miru/pull/111","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Pause-file leak assertion is vacuous after ensure deletes it","finding_evidence":"The assertion looks like proof that the example leaves no pause file behind, but it can never fail. The ensure block calls resume_relay! (FileUtils.rm_f on the same path) just before it. The cleanup guarantees the outcome, so the assertion proves nothing.","recorded_resolution":"fixed","recorded_correction":"I removed the pause-file assertion: the ensure block deletes that file just before, so the check could never fail. The comment now covers the queue process only, and the process-group check stays.","correction_target":"code_commit","source_path":"timekeeping/spec/jobs/relay_job_worker_spec.rb","source_line":149,"fix_commit":"78deb8bc26283640c5eced4aef15a90d366c8e54","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/78deb8bc26283640c5eced4aef15a90d366c8e54","finding_recorded_at":"2026-10-26T07:42:26.843Z","answer_recorded_at":"2026-10-26T09:11:38.830Z","first_independent_review_at":"2026-10-26T09:22:48Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"a513603a8bf53096762ee9ba8e1605c639677ac2e6e5a7c1af5ec6f4f85cc18e","record_sha256":"616cce06026e32cc4f66d3a75783c4be8c3c958dc55244757c09c8bf58a9f021"},
    {"record_id":"author-pr111-build-002","pr":111,"pr_url":"https://github.com/Homie-Improvement/miru/pull/111","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Wrong-method delivery still passes the real-socket acceptance test","finding_evidence":"The listener always passes \"POST\" to FakeRelay#call. If the worker sent the snapshot with a different method, such as PUT, FakeRelay would still apply it, the row would be marked sent, and every delivery assertion would pass. The test boundary quietly fixes the request instead of checking it. plan item 2 says the listener should forward each POST the way the POST-only WebMock stub in relay_job_spec.rb does.","recorded_resolution":"fixed","recorded_correction":"The fake listener now passes the request's real method to FakeRelay. I called FakeRelay directly with the same payload: PUT got a 404 and projected nothing, POST got a 200. The worker spec still passes.","correction_target":"code_commit","source_path":"timekeeping/spec/jobs/relay_job_worker_spec.rb","source_line":38,"fix_commit":"78deb8bc26283640c5eced4aef15a90d366c8e54","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/78deb8bc26283640c5eced4aef15a90d366c8e54","finding_recorded_at":"2026-10-26T07:42:26.843Z","answer_recorded_at":"2026-10-26T09:11:38.830Z","first_independent_review_at":"2026-10-26T09:22:48Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"0c77682da35493bba54cff8b58afb7b1a27ec9943e6085067cf44653c9af8924","record_sha256":"7edfe2cad2df903fc63c35088af41b9a10e2bc4d8539a8c974fc1f717a57b939"},
    {"record_id":"author-pr112-plan-001","pr":112,"pr_url":"https://github.com/Homie-Improvement/miru/pull/112","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The delete race can pass with a snapshot that still shows an undeleted entry.","finding_evidence":"The new example checks snapshot locked values only. Two snapshots containing locked: true would pass even if the delete snapshot retained discarded_at: nil and the prepare's version. The stored entry and audit assertions would not catch that wrong published state. timekeeping/app/services/timekeeping/outbox_fields.rb includes discarded_at and version because snapshots carry the entry state. Extend the planned assertions to compare each snapshot's version and discarded_at with the expected prepare and delete states. This closes a gap where a future defect could leave readers of snapshots showing an entry that Timekeeping deleted.","recorded_resolution":"fixed","recorded_correction":"The new example now checks each snapshot's version, discarded_at and locked flag in order: the prepare snapshot shows the entry kept at version 2, and the delete snapshot shows it discarded at version 3.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:24:26.161Z","answer_recorded_at":"2026-10-26T07:25:09.652Z","first_independent_review_at":"2026-10-26T07:58:11Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"ffea7cee1244d4b78b002ff086c4d22d47c8f6ea68fdae83a51bda5cfc6897fe","record_sha256":"cf5b25f2ff2ba23a20f3696b917793c5871f6eb3a9c0b1a15d8b5b6aae8d498e"},
    {"record_id":"author-pr112-plan-002","pr":112,"pr_url":"https://github.com/Homie-Improvement/miru/pull/112","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The delete snapshot cannot prove that the rule decision read the new reservation.","finding_evidence":"The proposed evidence does not establish the post-wait read claimed by plan item 4. In timekeeping/app/services/timekeeping/entry_writer.rb:88, commit_change reads reservations again after applying the delete. This independent read supplies both the audit and snapshot rows. If the bulk-delete decision received locked: false, deletion would still succeed because the delete rule ignores that flag. The later read would still produce locked: true, so all proposed expectations would pass. Describe this example as proof of the serialized stored outcome, not proof of the decision's reservation read. Keep the existing refused-update example as the check that distinguishes stale reservation facts.","recorded_resolution":"fixed","recorded_correction":"Agreed. The audit and snapshot rows come from a fresh read after the delete, so the new example proves the serialized stored outcome only. The refused-update example stays the check for stale reservation facts.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:24:26.161Z","answer_recorded_at":"2026-10-26T07:25:09.652Z","first_independent_review_at":"2026-10-26T07:58:11Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"e060dcbd0874e8bf854a26ed0ceffdfd789bbf2d42a27dfeee5ebe76b6a367a5","record_sha256":"e87c7e22b9c0e48d0f590cef1f4ff10506d525bc2951bba4d0f1f897a1b37610"},
    {"record_id":"author-pr112-plan-003","pr":112,"pr_url":"https://github.com/Homie-Improvement/miru/pull/112","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The release order relative to thread joins is ambiguous.","finding_evidence":"The first unit says to wrap the wait check and joins so `release << true` runs in `ensure`. Read literally, this permits joining the held thread before releasing it, which would hang the test. The planning decision's statement that releasing twice is harmless instead implies keeping the normal release before the joins. State explicitly that the normal path releases before joining, while `ensure` provides a backstop when an earlier step raises.","recorded_resolution":"fixed","recorded_correction":"The helper change now says the normal path releases the held operation before joining either thread, and the ensure block only releases again as a backstop when an earlier step raises.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:24:26.161Z","answer_recorded_at":"2026-10-26T07:25:09.652Z","first_independent_review_at":"2026-10-26T07:58:11Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"758f101f305d8ff114b458cfce45c61c0a923ee13019a192b00dd10c65ccdfeb","record_sha256":"b570afb71800532053c8e1c27a00614fc71b9f50aac682db2fff1c3992b610db"},
    {"record_id":"author-pr113-build-001","pr":113,"pr_url":"https://github.com/Homie-Improvement/miru/pull/113","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Tax-row reader and invoice-update setup copied across five examples","finding_evidence":"The rule for what counts as an unchanged tax row (id plus INVOICE_TAX_FIELDS, ordered by id) is written out five times: a `stored_tax_rows` method at billing_fence_2_spec.rb:73, three inline lambdas at :212, :267 and :317, and another lambda at billing_fence_4_spec.rb:70. The three new top-level describes (retried update, destroy-all, foreign tax row) also each repeat by hand the company/client/user/employment/admin/sign_in setup, the GST and Service tax configurations and the two seeded tax rows that the 'Billing fence on invoice update' describe in the same file already provides through let/let!/before. If the comparison has to change (a field added or ignored, a different ordering or scope), every copy must be edited the same way, and a missed copy quietly weakens one refusal check. The retried example also sends almost the same update/add/destroy payload as the existing update example; its only distinct case is the second attempt.","recorded_resolution":"fixed","recorded_correction":"One helper in spec/support now reads tax rows for both files. The retry, destroy-all and foreign-row examples now reuse the update block's setup. Both files pass: 10 examples, 0 failures.","correction_target":"code_commit","source_path":"spec/requests/api/v1/billing_fence_2_spec.rb","source_line":212,"fix_commit":"3d0f44ed87997ef4e0b7064ff7a21039e377e7b7","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/3d0f44ed87997ef4e0b7064ff7a21039e377e7b7","finding_recorded_at":"2026-10-26T08:05:33.329Z","answer_recorded_at":"2026-10-26T08:22:20.920Z","first_independent_review_at":"2026-10-26T08:32:11Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"0506857c5b6c06bfd210565f2f625671b17b668157a1e2ca3df86ba882c4d013","record_sha256":"dbf65bbec241907f3569f3ad3f85bc69eb43d2ddc66e16f277a425084e8a3733"},
    {"record_id":"author-pr113-plan-001","pr":113,"pr_url":"https://github.com/Homie-Improvement/miru/pull/113","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The mutation check does not establish that the nested tax changes reach storage.","finding_evidence":"Leaving `handoff_step` unset stops both examples before the tax comparison: file 2 first checks unchanged line-item count, and file 4 first expects 503. Even if those assertions are temporarily bypassed, any tax-row difference is insufficient. Consider a request that loses `invoice_taxes_attributes` during serialization but retains the line-item changes. `app/models/invoice.rb:381` recalculates percentage-tax amounts from the changed subtotal, so the tax comparison can fail without any requested tax update, addition, or deletion reaching storage. This defeats the stated proof that `_destroy` works. For the temporary admitted run, adjust the earlier refusal expectations and assert a successful response, the requested tax value change, the new row, and the deleted row's absence. Then restore the refusal test and record tho","recorded_resolution":"fixed","recorded_correction":"Agreed: draft invoices recalculate tax amounts from the subtotal, so the admitted run now checks the changed value, the new row and the deleted row's absence, not just any difference.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:34:50.581Z","answer_recorded_at":"2026-10-26T07:35:37.889Z","first_independent_review_at":"2026-10-26T08:32:11Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"069a480a480e2bf9137f3af5ef36d648765bff57fd9e79a8ac34b4cc19b8cb2a","record_sha256":"1cd4d56f90bb8803c36e7b3429cf5a4ef9d431748854bb58f94bfc5dac243b3a"},
    {"record_id":"author-pr113-plan-002","pr":113,"pr_url":"https://github.com/Homie-Improvement/miru/pull/113","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The mutation check must reach the tax-row comparison after the request succeeds.","finding_evidence":"The mutation procedure says only to leave `handoff_step` unset, while the plan retains the 503 response and unchanged-count expectations. An admitted request can fail those earlier expectations before reaching the tax-row comparison. That failure would not prove that the new comparison detects changed tax rows. Specify that the temporary run expects a successful response and bypasses unrelated refusal assertions, then require a failure at the stored tax-row comparison. Restore all refusal assertions afterward.","recorded_resolution":"fixed","recorded_correction":"The temporary check now asserts a successful response and skips the refusal-only checks so it reaches the stored tax-row comparison; all refusal checks are restored afterward.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:34:50.581Z","answer_recorded_at":"2026-10-26T07:35:37.889Z","first_independent_review_at":"2026-10-26T08:32:11Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"cd49f099a9034e3b18023cd205efe8687d6aa21823fb4c30a17bf59857b97989","record_sha256":"3a54eb6d595e3e4b91aa8e51820d0ad4be415f43a34d53693474388a090c700c"},
    {"record_id":"author-pr113-plan-003","pr":113,"pr_url":"https://github.com/Homie-Improvement/miru/pull/113","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The temporary admitted-request check must reach the tax-row comparison.","finding_evidence":"Skipping only the handoff assignment cannot produce the promised tax-row expectation failure. The admitted payload adds a line item, so the unchanged-count assertion in `spec/requests/api/v1/billing_fence_2_spec.rb:88` fails first. In `spec/requests/api/v1/billing_fence_4_spec.rb:84`, the expected 503 fails before any stored-row comparison. Neither example enables failure aggregation. Specify a temporary admitted-request check that expects success, removes the refusal-only count assertions, and directly verifies the stored tax changes. Revert all temporary changes afterward. Otherwise a failed mutation run can establish only that admission differs from refusal, not that the tax-row comparison detects a write.","recorded_resolution":"fixed","recorded_correction":"The temporary admitted run now expects success, drops the refusal-only count and 503 checks, asserts the requested tax changes directly, then confirms the row comparison fails, and is fully reverted.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:34:50.581Z","answer_recorded_at":"2026-10-26T07:35:37.889Z","first_independent_review_at":"2026-10-26T08:32:11Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"ec2309e4f9473db14ae3c72ae6cd9f33bb0a268972bfadc9e6319b0c15ce2698","record_sha256":"902c1b6259ddde3ae3c6e600dd6311fc5740b80767e28dbf6810fcbc98967cac"},
    {"record_id":"author-pr113-plan-004","pr":113,"pr_url":"https://github.com/Homie-Improvement/miru/pull/113","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Use positive composition for the two unchanged-count assertions.","finding_evidence":"The planned `.and not_change(InvoiceTax, :count)` cannot extend the existing `end.not_to change(InvoiceLineItem, :count)` in `spec/requests/api/v1/billing_fence_2_spec.rb:88`. RSpec does not support negating compound matchers. `spec/support/negated_matchers.rb:3-5` explicitly provides `not_change` for this case. Replace the whole assertion with `.to not_change(InvoiceLineItem, :count).and not_change(InvoiceTax, :count)`, as the cited create example does. Otherwise the focused test fails before it can establish that refusal preserves the rows.","recorded_resolution":"fixed","recorded_correction":"Confirmed spec/support/negated_matchers.rb defines not_change for this case; the plan now replaces the block with a positive not_change(...).and not_change(InvoiceTax, :count) chain.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:34:50.581Z","answer_recorded_at":"2026-10-26T07:35:37.889Z","first_independent_review_at":"2026-10-26T08:32:11Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"5991b55ec4b2d0b3713ac4cb6675f5cad6b309b499ef1d528a3852be5d531771","record_sha256":"78f7e98e5f9461012316affe70a4ee37e6f2e5862550a709fe3febe63fd990d2"},
    {"record_id":"author-pr114-plan-001","pr":114,"pr_url":"https://github.com/Homie-Improvement/miru/pull/114","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep the check that marks several selected positions while leaving other rows unsent.","finding_evidence":"The inventory maps the marking checks in `relay_outbox_source_4_spec.rb` to an example that marks only `[p1]`. The existing numbered example calls `mark_sent([first, third])` and expects exactly `[second, fourth]` to remain. This checks that one call marks every selected position without marking intervening rows. The planned single-position examples do not preserve that check. Add this input and its exact remaining-position assertion to the merged spec and name its owner in the inventory.","recorded_resolution":"fixed","recorded_correction":"Confirmed in relay_outbox_source_4_spec.rb. The merged marking example now marks the first and third of four rows and expects exactly the second and fourth to remain unsent.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:09:57.336Z","answer_recorded_at":"2026-10-26T07:11:04.284Z","first_independent_review_at":"2026-10-26T08:24:06Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"1d102530970402d8e378856024668de192954d6fdecf83fd0094fd56b7c933d3","record_sha256":"a1dd1769815642e09d67861b51dfed98cbf10c3c0fb7871b8654b75f27e040ff"},
    {"record_id":"author-pr114-plan-002","pr":114,"pr_url":"https://github.com/Homie-Improvement/miru/pull/114","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep the pause request with an incorrect service token as a separate refusal input.","finding_evidence":"The inventory marks the incorrect-token pause check as already covered, but the surviving example tests a different input. `timekeeping/spec/requests/internal/v1/test_hooks_3_spec.rb:51` sends `Bearer not-the-service-token` to the pause route. The base file sends no header or a signed user token to pause; its incorrect service token goes to resume. The merge explicitly adds only the missing no-header resume input. Keep the incorrect-token pause request too, with its 401 assertion. Otherwise, the refactor removes a distinct authorization check and could hide a future regression that accepts arbitrary bearer strings on pause while still rejecting user tokens.","recorded_resolution":"fixed","recorded_correction":"The pause request with a wrong service token now stays as its own input, with its 401 check, in the merged refusal example.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:09:57.336Z","answer_recorded_at":"2026-10-26T07:11:04.284Z","first_independent_review_at":"2026-10-26T08:24:06Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"4187de15785c71b0fc492d4c33079414e890ad8002268475172cb13143172fbb","record_sha256":"73c222031c961e97c41345ba9ed48cb4aa9928cbf83ec67ed3e09ae0199cb8a4"},
    {"record_id":"author-pr114-plan-003","pr":114,"pr_url":"https://github.com/Homie-Improvement/miru/pull/114","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep the wrong-bearer pause request when merging the hook specs.","finding_evidence":"The check inventory marks the wrong-bearer pause check as covered by the surviving refusal example. However, `timekeeping/spec/requests/internal/v1/test_hooks_spec.rb` sends its wrong bearer to `/internal/v1/test-hooks/relay/resume`, not `/internal/v1/test-hooks/relay/pause`. The pause requests there use no header or a signed user token. `test_hooks_3_spec.rb` separately checks pause with `Bearer not-the-service-token`. Deleting that example as covered would let a future pause-specific defect accept an arbitrary opaque bearer without this check failing. Add that pause request explicitly to the surviving refusal example and correct its inventory entry. The merge must preserve this distinct input, not just the wrong-bearer resume input.","recorded_resolution":"fixed","recorded_correction":"The merged refusal example keeps a pause request with `Bearer not-the-service-token` beside the wrong-token resume request, and the inventory row is corrected.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:09:57.336Z","answer_recorded_at":"2026-10-26T07:11:04.284Z","first_independent_review_at":"2026-10-26T08:24:06Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"ba4fa40348002819623766ff1c6ffbcf40e905fbe07ffd45e05fbdfa97679507","record_sha256":"0740a2a5e6c68ac074483537a4dc3db36fdf56dda595eea7b5240f93178b91bb"},
    {"record_id":"author-pr114-plan-004","pr":114,"pr_url":"https://github.com/Homie-Improvement/miru/pull/114","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep the wrong-bearer refusal check on the pause route.","finding_evidence":"The inventory says the base refusal example already covers a pause request with a wrong bearer. In `timekeeping/spec/requests/internal/v1/test_hooks_spec.rb`, pause receives no header or a user token; the wrong bearer is sent only to resume. `test_hooks_3_spec.rb` separately checks pause with `Bearer not-the-service-token`. Deleting it and adding only resume without a header loses that distinct route-and-input check. Carry the wrong-bearer pause request into the merged refusal example and correct the inventory mapping.","recorded_resolution":"fixed","recorded_correction":"Confirmed: the base file sends the wrong service token only to resume. The merged refusal example now also sends a wrong bearer to pause and expects 401, and the inventory says so.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:09:57.336Z","answer_recorded_at":"2026-10-26T07:11:04.284Z","first_independent_review_at":"2026-10-26T08:24:06Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"86a5c8c6532c70605e9cacb6f53c4f2d0475d84c7f0ec1d91180d4c5ba07fcae","record_sha256":"83dcbf19ecd688726d096b2534f73fa1d85619f3658dc8827e176179853e0a2d"},
    {"record_id":"author-pr114-plan-005","pr":114,"pr_url":"https://github.com/Homie-Improvement/miru/pull/114","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The sequencing rule contradicts the endpoint unit's declared independence.","finding_evidence":"Sequencing says every other unit uses the shared row builder and must follow its creation. However, the endpoint decision says those specs build no outbox rows, and the endpoint unit declares no dependencies. This gives implementers two different execution orders and needlessly delays independent work. State that the relay-source, relay-job/test-hooks, and metrics units depend on the builder, while the endpoint merge can proceed independently.","recorded_resolution":"fixed","recorded_correction":"Sequencing now says the relay source, relay job and test hooks, and metrics merges follow the builder, and the snapshot endpoint merge can go first or in parallel.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:09:57.336Z","answer_recorded_at":"2026-10-26T07:11:04.284Z","first_independent_review_at":"2026-10-26T08:24:06Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c8d0ef13b398a93fa17284e395525942798a018458d748a86925d1084d71b157","record_sha256":"9d07b9c2276f3a350a89940c592095046556b9e9d76f0673d4a6825d66d6f891"},
    {"record_id":"author-pr114-plan-006","pr":114,"pr_url":"https://github.com/Homie-Improvement/miru/pull/114","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The surviving refusal example does not cover a pause request with a wrong bearer token.","finding_evidence":"The inventory marks the wrong-bearer pause check as already covered. In `timekeeping/spec/requests/internal/v1/test_hooks_spec.rb`, the pause requests use no header or a user token. Only the resume request uses a wrong service token. The distinct pause check exists in `timekeeping/spec/requests/internal/v1/test_hooks_3_spec.rb`, which the plan deletes. Add that input to the surviving refusal example and to the test scenarios for the test-hooks merge. Otherwise a future change that accepts a wrong bearer token on the pause route could pass the merged tests.","recorded_resolution":"fixed","recorded_correction":"The wrong-bearer pause input is now in the merged refusal example and in the test hooks test scenarios; the inventory no longer calls it covered.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:09:57.336Z","answer_recorded_at":"2026-10-26T07:11:04.284Z","first_independent_review_at":"2026-10-26T08:24:06Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f7b704f99096d2d35c2a3d9109d381d0c02f48f899c7cc5fa8fd42af6001f2a2","record_sha256":"9f21693d8b2f8892fe114821e6d0a5438a4aecda938ba6bb13f8b96a0d79de28"},
    {"record_id":"author-pr116-build-001","pr":116,"pr_url":"https://github.com/Homie-Improvement/miru/pull/116","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Razorpay provider and client-stub setup copied three times","finding_evidence":"The 11-line payments_provider build/key_secret/save! block and the RazorpayClient instance_double stub are copied word for word into the examples at lines 95, 163 and 247, and the invoice snapshot lambda is copied in two shapes. A change to Razorpay provider setup has to be made in three places, and one copy's 'unchanged' check can silently stop checking a field. create_spec.rb in the same directory already has create_razorpay_provider!.","recorded_resolution":"fixed","recorded_correction":"Provider setup, the client stub, invoice creation and the snapshot are now one file-local helper each, shared by every payment example. Payment counts stay per invoice. The file ran with 5 examples and 0 failures.","correction_target":"code_commit","source_path":"spec/requests/api/v1/mobile/collections/billing_fence_spec.rb","source_line":180,"fix_commit":"81a091286c4da7a2fc7254d5b260386b65bf94e9","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/81a091286c4da7a2fc7254d5b260386b65bf94e9","finding_recorded_at":"2026-10-26T08:21:07.172Z","answer_recorded_at":"2026-10-26T08:36:13.190Z","first_independent_review_at":"2026-10-26T08:47:34Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"88405b0d5a878ea7fe1e308538af889dcc414b5555abb282bc9b6ddb7acf3be6","record_sha256":"a870572e875d15968d3d184ead5d7b042aaefb8c5af83657939716a20281e98b"},
    {"record_id":"author-pr116-build-002","pr":116,"pr_url":"https://github.com/Homie-Improvement/miru/pull/116","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Two payment-link examples assert the same fenced new-link path","finding_evidence":"The examples at lines 95 and 247 test the same new-link fence path: 503 ownership_moving, no create_payment_link call, an unchanged snapshot, then exactly one link after the handoff. RazorpayPaymentLinkService#process enters the fence the same way whatever notify_sms is, so the only difference is the success status (:accepted vs :ok). The plan asked for one payment_link example. The two copies have already drifted (global vs invoice-scoped Payment.count, link tuple vs separate keys), so any change to the fence contract has to be made twice.","recorded_resolution":"fixed","recorded_correction":"The two payment-link examples are now one scenario run twice: with SMS it expects 202, without SMS it expects 200. All earlier assertions and the comment on the uncovered stored-link reuse path are kept.","correction_target":"code_commit","source_path":"spec/requests/api/v1/mobile/collections/billing_fence_spec.rb","source_line":247,"fix_commit":"81a091286c4da7a2fc7254d5b260386b65bf94e9","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/81a091286c4da7a2fc7254d5b260386b65bf94e9","finding_recorded_at":"2026-10-26T08:21:07.172Z","answer_recorded_at":"2026-10-26T08:36:13.190Z","first_independent_review_at":"2026-10-26T08:47:34Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"25368f80d786932ca87b1090519448fb756fb6f6ddf5e5a8c18e6bd20f90f28c","record_sha256":"9a6acd7e00ebfec4af0a2e233fc75517318543df005bbb5eec709e58d55e1920"},
    {"record_id":"author-pr116-plan-001","pr":116,"pr_url":"https://github.com/Homie-Improvement/miru/pull/116","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The payment-link assertion list does not match the shared baseline rule.","finding_evidence":"plan item 3 requires each refusal check to compare Payment.count, invoice status, amount_paid, amount_due, payment_infos, and stored link fields. plan item 7 calls the unit lists the adopted assertion lists. However, U1 lists only stored link fields and payment_infos for payment_link. An implementer following that list could omit the payment and balance checks while another follows plan item 3. The resulting test could pass even if a refused request creates a payment or changes the invoice balance. Make U1 explicitly use the full plan item 3 baseline before clearing handoff_step and sending the successful control request.","recorded_resolution":"fixed","recorded_correction":"Both refusal cases in the first unit now compare the same full baseline before the request is retried: payment count, invoice status, amount paid, amount due, payment_infos and the stored link fields.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:35:38.465Z","answer_recorded_at":"2026-10-26T07:36:18.956Z","first_independent_review_at":"2026-10-26T08:47:34Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"86298218239ea5acd64d389bda13fb59cb9698ee9ba2f410781475a2ab5da793","record_sha256":"c4e82f262b3125d0310b71a3f52f65391210ac4c90cad47fda502f8c935b0876"},
    {"record_id":"author-pr117-build-001","pr":117,"pr_url":"https://github.com/Homie-Improvement/miru/pull/117","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Agent refusal cases asserted alike in request, writer and shared contract specs","finding_evidence":"The same three S7 refusals (foreign agent answered like a missing one, colleague's agent on create, update moving an agent entry to another user) are asserted in the shared provider contract, which already runs against the real Timekeeping provider, and again in the request spec and the writer spec. Changing the refusal shape means editing up to three copies, and no copy catches a risk the others miss.","recorded_resolution":"fixed","recorded_correction":"The request spec no longer repeats three refusals: foreign versus missing agent, colleague agent on create, and the user-only update. The shared contract examples own those, and the writer spec keeps the row counts and replay.","correction_target":"code_commit","source_path":"timekeeping/spec/requests/v1/entry_writes_spec.rb","source_line":359,"fix_commit":"094978a4828981f1e02f3f907d3518d86adb7c0f","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/094978a4828981f1e02f3f907d3518d86adb7c0f","finding_recorded_at":"2026-10-26T08:25:49.324Z","answer_recorded_at":"2026-10-26T08:31:37.734Z","first_independent_review_at":"2026-10-26T08:45:59Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"aae4c952ab4dae12c1b40fb831e735ce746a1b2dbe7135b905f4022e754068a6","record_sha256":"2e2608f7e1ca298dab1aa84d4b1e272de4e81a921231732fc5c07c074e155221"},
    {"record_id":"author-pr117-build-002","pr":117,"pr_url":"https://github.com/Homie-Improvement/miru/pull/117","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"EntryDecision.agent_owner spec repeats the gem's rule table cases","finding_evidence":"EntryDecision.agent_owner passes its arguments straight to TimekeepingRules::EntryRules.agent_owner with no conversion. The wrapper spec repeats the gem spec's truth table with the same company_agents { 40 => 7, 41 => 8 } fixture (unknown agent, other user's agent, empty list, own agent, nil). A change to the S7 table must be made in two files, and the copy catches nothing the gem spec misses.","recorded_resolution":"fixed","recorded_correction":"The EntryDecision example now makes one pass-through check: the entry user's agent is ok and another user's agent is not_found. The full rule table stays in the gem's agent_owner_spec.rb.","correction_target":"code_commit","source_path":"timekeeping/spec/services/timekeeping/entry_decision_spec.rb","source_line":121,"fix_commit":"094978a4828981f1e02f3f907d3518d86adb7c0f","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/094978a4828981f1e02f3f907d3518d86adb7c0f","finding_recorded_at":"2026-10-26T08:25:49.324Z","answer_recorded_at":"2026-10-26T08:31:37.734Z","first_independent_review_at":"2026-10-26T08:45:59Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"29840e4df44383c044e2e04a0d909ad7eacf8e9ff9b25d65395cefacf4ba04ac","record_sha256":"4c8d708693b3ee68e0953616832facab53e7a2299fb49c0547cc5b11547eac38"},
    {"record_id":"author-pr118-build-001","pr":118,"pr_url":"https://github.com/Homie-Improvement/miru/pull/118","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Bystander example needs four pooled connections; test pool is three","finding_evidence":"The 'counts a lock wait only when the named holder is the one blocking the named waiter' example needs four pooled connections at once: the holder, the waiter, the bystander and the main thread. The main thread already holds a lease from DatabaseCleaner.clean_with and TimekeepingOwnership.ensure_initial!. The test pool is 3: config/database.yml defaults to RAILS_MAX_THREADS 3, and CI sets neither DB_POOL nor RAILS_MAX_THREADS. So the bystander thread waits out the default 5s checkout_timeout and raises ActiveRecord::ConnectionTimeoutError. bystander_pids.pop(timeout: 5) then returns nil, and both bystander checks come out false only because of SQL NULL. The ensure's join re-raises the thread's error, so the example is likely to error in CI. Where the pool happens to be larger, the example passes without ever testing a real bystander, because nothing asserts that bystander_pid is present.","recorded_resolution":"fixed","recorded_correction":"The bystander session is now opened outside the pool and closed in ensure. The example also asserts that the holder, waiter and bystander pids are all integers, so a missing pid cannot make the negative checks pass. With DB_POOL=3 the spec files pass.","correction_target":"code_commit","source_path":"spec/db/committed_data_mode_spec.rb","source_line":100,"fix_commit":"6db7ade2ab60f4bbd5cfd30ea3810ad45bc73faf","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/6db7ade2ab60f4bbd5cfd30ea3810ad45bc73faf","finding_recorded_at":"2026-10-26T08:26:51.692Z","answer_recorded_at":"2026-10-26T08:43:01.129Z","first_independent_review_at":"2026-10-26T08:54:38Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"aba63fd88932ea2c51ae2925212d2e23845688aaba90d7c0a1fa12517ad99492","record_sha256":"7bceb8ba9018b9fa075c421cec0ea19a3f98b50aebec618373a122d05e7d18e4"},
    {"record_id":"author-pr119-build-001","pr":119,"pr_url":"https://github.com/Homie-Improvement/miru/pull/119","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Admin-role user token refusal on commit dropped from matrix","finding_evidence":"The deleted 'Reservation commit access' case refused an admin-role user token. The matrix now builds only owner-role tokens, so no test shows an admin delegation being refused on reservation routes. The plan's Assumptions section says admin tokens hit the same 403 branch, but the issue asks to keep every distinct assertion.","recorded_resolution":"fixed","recorded_correction":"The access matrix now has an admin user token row. On every route it must get 403 forbidden and change nothing.","correction_target":"code_commit","source_path":"timekeeping/spec/requests/v1/reservations_auth_3_spec.rb","source_line":48,"fix_commit":"f00bfab8046a3ab963771777cc93d926d8865120","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/f00bfab8046a3ab963771777cc93d926d8865120","finding_recorded_at":"2026-10-26T08:30:18.797Z","answer_recorded_at":"2026-10-26T08:45:04.543Z","first_independent_review_at":"2026-10-26T08:57:58Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"bc27ce8a5ffa7d6f25601186f1ecdc231bba8f0599ecf6d3ff6e253596a46aa1","record_sha256":"428be2611b92326aa406532670e80bdba4b7bb705c5dd549899edfc306c0222c"},
    {"record_id":"author-pr119-build-002","pr":119,"pr_url":"https://github.com/Homie-Improvement/miru/pull/119","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Helper spec repeats matrix and mark-billed replay cases","finding_evidence":"The new helper spec repeats the auth_3 'no token' and 'other company' rows and the reservation_writes_spec mark-billed replay, so a behaviour change again needs edits in several files. That is the drift this refactor set out to remove. The plan did not ask for this file; U1 says the helper is proven through U2 to U4.","recorded_resolution":"fixed","recorded_correction":"I cut the helper spec down to what the helper itself does: no header for a nil credential, paths built from the contract, generation added, required keywords. The 401, no-write, replay and row-count checks now live only in their owner files.","correction_target":"code_commit","source_path":"timekeeping/spec/requests/v1/reservation_request_helpers_spec.rb","source_line":39,"fix_commit":"f00bfab8046a3ab963771777cc93d926d8865120","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/f00bfab8046a3ab963771777cc93d926d8865120","finding_recorded_at":"2026-10-26T08:30:18.797Z","answer_recorded_at":"2026-10-26T08:45:04.543Z","first_independent_review_at":"2026-10-26T08:57:58Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"ec8ecea27662dc3fef69dfd8a8ae933104aa54989b1d908382ad7d92d02c4804","record_sha256":"9a293838bb9d35e142f0ac79c17503a6692f2c4814b6db83f024d9e38d124123"},
    {"record_id":"author-pr119-build-003","pr":119,"pr_url":"https://github.com/Homie-Improvement/miru/pull/119","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Outbox-rollback retry no longer checks the reservation's invoice and state","finding_evidence":"The deleted copy checked that the retried prepare left exactly one provisional reservation for invoice 501. The merged case checks only response state 'prepared' and +1 row counts, so a retry that reserved the wrong invoice or left the reservation in another state would pass. The issue and plan R2 require every distinct assertion to survive.","recorded_resolution":"fixed","recorded_correction":"After the retry, the outbox-rollback case now asserts exactly one provisional reservation for invoice 501. The file's tests pass.","correction_target":"code_commit","source_path":"timekeeping/spec/requests/v1/reservation_writes_spec.rb","source_line":68,"fix_commit":"f00bfab8046a3ab963771777cc93d926d8865120","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/f00bfab8046a3ab963771777cc93d926d8865120","finding_recorded_at":"2026-10-26T08:30:18.797Z","answer_recorded_at":"2026-10-26T08:45:04.543Z","first_independent_review_at":"2026-10-26T08:57:58Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"51f26db1b8b183e04dc96e0380c22a7f2fcfb0adea39a924928f8636cb0e40c2","record_sha256":"40e7ea3d81ff40987e070f22fd6789e4dcfd583233a2f860f8e54686faa8a541"},
    {"record_id":"author-pr119-build-004","pr":119,"pr_url":"https://github.com/Homie-Improvement/miru/pull/119","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Unknown route fails by nil NoMethodError; test accepts any error","finding_evidence":"dig returns nil for an unknown route and the call fails on nil.gsub. The spec accepts any StandardError, so it would pass on an unrelated helper bug and never shows the misspelt route was the cause.","recorded_resolution":"fixed","recorded_correction":"The path lookup now uses fetch at each step, so an unknown route raises KeyError naming it. The spec now expects KeyError matching /refund/. In Ruby the message reads key not found: \"refund\".","correction_target":"code_commit","source_path":"timekeeping/spec/support/reservation_requests.rb","source_line":30,"fix_commit":"f00bfab8046a3ab963771777cc93d926d8865120","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/f00bfab8046a3ab963771777cc93d926d8865120","finding_recorded_at":"2026-10-26T08:30:18.797Z","answer_recorded_at":"2026-10-26T08:45:04.543Z","first_independent_review_at":"2026-10-26T08:57:58Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"237848a5053674de448491941d82d2452c913467c3bb48eba2c2f416c92cc996","record_sha256":"d81b4ec207f0ea9699f066dd7365387f5e63ce4c18c97c6c20ba14895c5bb191"},
    {"record_id":"author-pr119-plan-001","pr":119,"pr_url":"https://github.com/Homie-Improvement/miru/pull/119","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep the owner's successful commit after a foreign credential targets the owner's path.","finding_evidence":"The deleted `reservations_tenant_abort_spec.rb` sends company B's credential to company A's abort path, then proves A can still commit. The planned replacement sends B's credential to B's path. The access matrix retains the first refusal, but checks operation counts, not operation contents or subsequent completion. Consider a regression that returns 404 but changes A's `InvoiceOperation.state` to `aborted`, leaving reservations and counts unchanged. Both planned owners pass. The deleted test fails because `ReservationOperations#commit` returns zero reserved entries for a terminal operation. Keep the owner commit after the mismatched-credential refusal, or assert the operation's full stored state after each refusal. These requests test different company boundaries and are not interchangeable.","recorded_resolution":"fixed","recorded_correction":"The matrix baseline now records each invoice operation's company, id, state and stored entry set. After the matrix, the owner commits the seeded operation and must get one reserved entry.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:25:42.860Z","answer_recorded_at":"2026-10-26T07:26:46.883Z","first_independent_review_at":"2026-10-26T08:57:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"a208beaddae6304ce33b3a0913d420531986b6bc0f55406d9e1d6421f1813ee4","record_sha256":"5651a5bb73686c0a762114a0216981b3c1da6458c331a99453f866ce8f78ed0f"},
    {"record_id":"author-pr119-plan-002","pr":119,"pr_url":"https://github.com/Homie-Improvement/miru/pull/119","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The access-matrix unit delays checks that the design requires after every refused request.","finding_evidence":"The design says to compare stored state after every refused call. The access-matrix unit instead says to compare it after all 25 calls. These instructions permit different tests. An end-only comparison can miss a mutation that a later call reverses, and it cannot identify which refusal changed state. Require the baseline comparison after each request, as the design already specifies.","recorded_resolution":"fixed","recorded_correction":"The access-matrix unit now compares the stored state with the one baseline after each refused request, labelled by route and credential. It no longer waits until all 25 calls are done.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:25:42.860Z","answer_recorded_at":"2026-10-26T07:26:46.883Z","first_independent_review_at":"2026-10-26T08:57:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"413b5eba13fae44f887203653f69c3b6710693f0c938054c9e2646049004f4fa","record_sha256":"73218a2428d107a190f6853ad28836cf7ba5872fc9d0762913b22e605f8a5ab5"},
    {"record_id":"author-pr119-plan-003","pr":119,"pr_url":"https://github.com/Homie-Improvement/miru/pull/119","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The helper requirement includes requests that the plan explicitly leaves unchanged.","finding_evidence":"The requirement says one helper sends every reservation request in the owner files. The completion criteria also prohibit pasted request lambdas in those files. However, the deferred work leaves other describes in `reservations_auth_2_spec.rb` on `TimekeepingRackClient`, and the scope boundaries leave the distinct two-company case in `reservation_writes_spec.rb` unchanged. Limit the helper requirement and completion criteria to the consolidated contract cases, or remove those exclusions. The narrower wording matches the declared scope without adding work.","recorded_resolution":"fixed","recorded_correction":"The helper requirement and the done criteria now cover only the merged contract cases. The distinct cases left outside the merge are named as untouched.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:25:42.860Z","answer_recorded_at":"2026-10-26T07:26:46.883Z","first_independent_review_at":"2026-10-26T08:57:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"33e251d1fcb3ed8c4911049e2097911cbd0da84ba08d3017d1ec355aeb5dc24f","record_sha256":"a913cc3c90a39db6cfc8841d991a2a8a59b6efdfdce2460d6b551f228f4af51d"},
    {"record_id":"author-pr120-build-001","pr":120,"pr_url":"https://github.com/Homie-Improvement/miru/pull/120","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Stripe payment_intent.succeeded refusal example duplicates the replay example's refused phase","finding_evidence":"The standalone payment_intent.succeeded refusal example tests the same contract as the first phase of the replay example: same kind of invoice and signed payload, a POST during close_admission, and the same 503, Retry-After 5, ownership_moving, zero-Payment and unchanged-invoice assertions. The only check it adds is that the body does not include \"close_admission\". The two copies can drift apart unnoticed when only one is edited. The PayPal refusal example is not redundant in this way, because it asserts show_order, capture_order and PaymentMailer were not called.","recorded_resolution":"fixed","recorded_correction":"I moved the only extra check, that the body does not mention close_admission, into the replay example's refused request, then deleted the standalone example. billing_fence_spec.rb passes: 6 examples, 0 failures.","correction_target":"code_commit","source_path":"spec/requests/webhooks/billing_fence_spec.rb","source_line":155,"fix_commit":"1696c52720fd0f07057002e788fcd4d0d2a5e3ad","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/1696c52720fd0f07057002e788fcd4d0d2a5e3ad","finding_recorded_at":"2026-10-26T08:37:19.122Z","answer_recorded_at":"2026-10-26T08:51:30.740Z","first_independent_review_at":"2026-10-26T09:04:06Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"57c038074ca4f099fe34fe2550ae66f2cbd82010f835a888eb5deb6256313f57","record_sha256":"95102d7e14fa0298fc0303b4720c038527b37814d1fa1d88f56793f7fc4d9948"},
    {"record_id":"author-pr120-plan-001","pr":120,"pr_url":"https://github.com/Homie-Improvement/miru/pull/120","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The excluded Stripe checkout-completed path has no refusal coverage.","finding_evidence":"The assumptions exclude `checkout.session.completed` because it supposedly needs a session fetch and already has a service-level fence test. Neither premise holds. `app/services/invoice_payment/stripe_checkout_fulfillment.rb` reads the event directly; its `card_name` method skips provider calls without a connected account. The existing checkout fulfillment specs do not exercise a handoff. A signed event with `mode: \"payment\"`, `status: \"complete\"`, `payment_status: \"paid\"`, and matching invoice metadata takes a separate dispatch branch. If that branch swallowed `OwnershipMoving`, every planned request test could pass while this event received the wrong response. Add a signed refusal request for this event, using the re","recorded_resolution":"fixed","recorded_correction":"Agreed. The checkout fulfillment service reads the event directly and no existing spec runs it during a handoff. The plan now adds a signed checkout.session.completed payment-mode refusal request test.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:36:15.525Z","answer_recorded_at":"2026-10-26T07:37:02.123Z","first_independent_review_at":"2026-10-26T09:04:06Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"261acbb6d5ddcd4865f3c03aad2cbdf3552e3239447e003aba4d768184e6c422","record_sha256":"3c7188d5a5de7f53f8add6d64f54521f1252cac3cb7ddb1fdd91fb75965bcf11"},
    {"record_id":"author-pr121-build-001","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Handoff transitions misclassify refusals after rollback","finding_evidence":"A handoff that starts between rollback and insert causes an outside-handoff refusal to be recorded with a step, so it never alerts. A flip that clears the step in that gap causes a freeze-time refusal to raise a false outside-handoff alert. The decision reads a locked row, but the recorder rereads ownership after the lock is released. Carrying the decided step fixes the attribution and leaves the 409 unchanged.","recorded_resolution":"fixed","recorded_correction":"The consumer now records the handoff step it read under its FOR SHARE lock. Two specs change ownership after rollback, one in each direction, and both record the step that was decided.","correction_target":"code_commit","source_path":"app/services/timekeeping/projection_consumer.rb","source_line":70,"fix_commit":"e73d562ea2b5fa187a01a016d8bf764d6713e4de","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/e73d562ea2b5fa187a01a016d8bf764d6713e4de","finding_recorded_at":"2026-10-26T09:03:57.092Z","answer_recorded_at":"2026-10-26T09:19:02.763Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"348e13d7940bd230ad90361ed2719953d169a400eb7e9d085027278c6b17ab79","record_sha256":"d05acda74dc9fb3176721fc5f746b5e9006b1b81a871309ea58069c1d3965d70"},
    {"record_id":"author-pr121-build-002","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Metrics spec loses the per-company outbox_lag tests the new alert relies on","finding_evidence":"The author says this edit only removed a duplicate require, but it deletes the four acceptance tests for outbox_lag. The new outbox_lag alert reads metrics.fetch(\"outbox_lag\") directly, and every monolith-side test of it uses a hand-written body. Nothing now checks that Timekeeping reports the correct per-company lag. A lag under the wrong company, or one that counts sent rows, would raise or hide the alert with no test failing.","recorded_resolution":"fixed","recorded_correction":"Restored the outbox_lag request specs beside the new window_seconds examples: per-company age, sent rows left out, the empty list, and 401 without the service token. The Timekeeping suite passes.","correction_target":"code_commit","source_path":"timekeeping/spec/requests/internal/v1/metrics_spec.rb","source_line":145,"fix_commit":"e73d562ea2b5fa187a01a016d8bf764d6713e4de","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/e73d562ea2b5fa187a01a016d8bf764d6713e4de","finding_recorded_at":"2026-10-26T09:03:57.092Z","answer_recorded_at":"2026-10-26T09:19:02.763Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"1cf8b973482eff33b051a02e7ebb0d1e44b30aa5bcc59474d8ed6e7943bc7d98","record_sha256":"c4705b3c58ffbd8867f8c67302048ef61f45f259d4af70203214a0623e6e3a76"},
    {"record_id":"author-pr121-build-003","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"Projection consumer spec rewrite deletes version, race and flip tests","finding_evidence":"The author calls this edit lint-only, but the diff replaces the whole 240-line file (@@ -1,240 +1,71 @@). It deletes the consumer's own rule tests: refusing a company_id mismatch, ignoring an older or equal version, mapping stored values and enums, invalid_snapshot for unknown keys and users, rolling back the batch, clearing miru.writer after commit, the v2-after-v3 concurrency race, the ownership flip waiting for an accepted previous-generation batch, and the agent and deleted-project reference checks. This change edits the rescue path of ProjectionConsumer#call, and these were the tests that would catch a regression there. The file also lost `require \"rails_helper\"`, so it fails when run alone.","recorded_resolution":"fixed","recorded_correction":"projection_consumer_spec.rb now matches the base file exactly, rails_helper included. The three refusal-record examples moved to projection_consumer_refusals_spec.rb.","correction_target":"code_commit","source_path":"spec/services/timekeeping/projection_consumer_spec.rb","source_line":4,"fix_commit":"e73d562ea2b5fa187a01a016d8bf764d6713e4de","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/e73d562ea2b5fa187a01a016d8bf764d6713e4de","finding_recorded_at":"2026-10-26T09:03:57.092Z","answer_recorded_at":"2026-10-26T09:19:02.763Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"316842b7064ce24918a75cdb28da03afcd6974e9ff4b140cd25767a4821339b7","record_sha256":"53f46a7929f15a607f1fadbdda510bb198c74c689c407e0578a2592ae2d4fb9b"},
    {"record_id":"author-pr121-build-004","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"Snapshot endpoint spec rewrite deletes its existing contract coverage","finding_evidence":"The author calls this edit lint-only plus added examples, but the diff replaces the whole 327-line file (@@ -1,327 +1,92 @@). Every test the endpoint had before is gone: 401 for employee and owner user tokens and for an unset token, project tenancy, company mismatch, unknown columns or bill_status, accepting 99 levels of nesting, idempotency_key_reused, and the handoff window (accept up to the final outbox position during freeze, 409 after the flip). The handoff-window test guards the issue's rule that the 409 stale_generation answer must never change, so a regression there would now pass CI. The file also lost `require \"rails_helper\"`, and .rspec loads only spec_helper, so running this file alone fails on `create` and `type: :request`.","recorded_resolution":"fixed","recorded_correction":"Restored all eight base describes in entry_snapshots_spec.rb with rails_helper. The refusal-record example moved to entry_snapshots_2_spec.rb, and the 401/422 cases I had added were dropped as repeats.","correction_target":"code_commit","source_path":"spec/requests/internal/v1/timekeeping/entry_snapshots_spec.rb","source_line":1,"fix_commit":"e73d562ea2b5fa187a01a016d8bf764d6713e4de","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/e73d562ea2b5fa187a01a016d8bf764d6713e4de","finding_recorded_at":"2026-10-26T09:03:57.092Z","answer_recorded_at":"2026-10-26T09:19:02.763Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"28c965387739c5b2c3131674d65d5edc7fa898a0899ad5db88dbcd71577326b9","record_sha256":"aa8d1751f4736dfac570fbecd7beb3a16f142436b40f67a7d28badccab9ab113"},
    {"record_id":"author-pr121-build-005","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Test-hooks boot spec dropped its positive control, relay-pause guard and requires","finding_evidence":"The rewrite removes the TIMEKEEPING_TEST_HOOKS=1 boot, which proved the production probe can see a loaded hook. Without it, the 'absent 404' expectations cannot tell a working gate from a broken probe. The relay-pause route lost its production check, and no other spec covers it in production. The rewrite also removes the rails_helper and open3 requires, so the file raises NameError when run alone. The author's stated reason for this edit was 'lint only'.","recorded_resolution":"fixed","recorded_correction":"Restored the rails_helper and open3 requires and the TIMEKEEPING_TEST_HOOKS=1 boot as a positive control. Each boot now probes both relay pause and writes/delay, and the production flag files are cleaned up in ensure.","correction_target":"code_commit","source_path":"timekeeping/spec/lib/timekeeping/test_hooks_spec.rb","source_line":4,"fix_commit":"e73d562ea2b5fa187a01a016d8bf764d6713e4de","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/e73d562ea2b5fa187a01a016d8bf764d6713e4de","finding_recorded_at":"2026-10-26T09:03:57.092Z","answer_recorded_at":"2026-10-26T09:19:02.763Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"bf2f04477ab0e155b8b184cb7019f73336fd1d44cd6ead324cc9e7ab4e615b4f","record_sha256":"74df6320ab8eee5d1de57cfd8d606b75bf3ac8862e5185f55b50d7c1d00dd6c2"},
    {"record_id":"author-pr121-build-006","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Unexpected metrics body or bad response crashes alerts task","finding_evidence":"When the metrics read fails, the task should still report the monolith's own alerts and then exit non-zero, as the AlertSignals comment says. Two failures get past MetricsClient::Error. A 200 body with the wrong shape (for example from a pre-change Timekeeping image without window_seconds) raises KeyError later. A garbled HTTP answer raises Net::HTTPBadResponse, which the rescue list misses. Either way the rake task raises before printing anything, and every monolith alert in that run is lost. Checking the body shape in the client stops the failure where it starts.","recorded_resolution":"fixed","recorded_correction":"The client now rescues Net::HTTPBadResponse and raises its own Error for a body missing a field the evaluator reads. A task spec shows the pending-intent alert still prints before the non-zero exit.","correction_target":"code_commit","source_path":"app/services/timekeeping/metrics_client.rb","source_line":33,"fix_commit":"e73d562ea2b5fa187a01a016d8bf764d6713e4de","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/e73d562ea2b5fa187a01a016d8bf764d6713e4de","finding_recorded_at":"2026-10-26T09:03:57.092Z","answer_recorded_at":"2026-10-26T09:19:02.763Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"be626159be23b1a0304f29d8f63bff75927eb3f0582c03652b3d65465db063ca","record_sha256":"2f92359f53fedb4c83de06a3b3c1c80d5b61824c8156419a8787bdc551506a1c"},
    {"record_id":"author-pr121-plan-001","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Deferring stale-generation counts leaves an existing failure path without its required alert.","finding_evidence":"The scope boundary and plan item 6 permanently mark `stale_generation` unavailable, although this failure already occurs in the target code. `app/services/timekeeping/projection_consumer.rb:88-94` rejects an unaccepted generation, and `app/controllers/internal/v1/timekeeping/entry_snapshots_controller.rb:9-10` returns that result to the relay. With no handoff active, a stale snapshot can therefore receive 409 while `timekeeping:alerts` prints only an unavailable notice. Operators miss the specific alert required for this degraded state. Include collection of real refusals from this existing path, with company attribution and handoff context. Verify that a real refusal outside a handoff reaches the task. A pure evaluator test with invented counts does not cover that requirement.","recorded_resolution":"fixed","recorded_correction":"The plan now records every 409 stale_generation answer from the projection consumer, with the company and the handoff step at that moment, and the alert task reads those records.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:22:03.542Z","answer_recorded_at":"2026-10-26T07:25:52.121Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"bcf1d5b434f91201363ae73c27738f34c75c4d911f7d39fb414300390f4a833e","record_sha256":"6ef3b6c23ae0df421c1c2d9aae56cbebaa78366d1197e1c7d073531be7b16308"},
    {"record_id":"author-pr121-plan-002","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Exclude local Bundler configuration from the image context.","finding_evidence":"plan item 1 re-includes all of `timekeeping/` but does not exclude `timekeeping/.bundle/`. Both `timekeeping/.gitignore:1` and the root `.dockerignore` exclude local Bundler configuration today. Bundler configuration can contain private gem registry credentials. A developer's local build would copy that configuration into the image and could export it through the configured build cache. The per-Dockerfile ignore file replaces the root rules, so the existing exclusion no longer protects this build. Exclude `**/.bundle` before either COPY or bundle installation. Verify this with a harmless sentinel in `timekeeping/.bundle/config`, and confirm it cannot enter image layers or the exported cache.","recorded_resolution":"fixed","recorded_correction":"The image's ignore file now excludes every .bundle directory before anything is copied, and the build job checks that a sentinel Bundler config never reaches the image.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:22:03.542Z","answer_recorded_at":"2026-10-26T07:25:52.121Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"ad6abbe9132d4db85d007c84fcd9917c0cc93a95b7ab957ac56dfb387f403ad1","record_sha256":"0cc142ca0944e4721c125f14c40855f5df144323e695952be102ac10f12a144a"},
    {"record_id":"author-pr121-plan-003","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Judging only the newest window can raise premature alerts and hide a full window of failures.","finding_evidence":"plan item 5 judges the newest window even when it has just started. In timekeeping/app/services/timekeeping/write_metrics.rb, window_start assigns each request to a fixed five-minute interval; it does not establish five minutes of observations. One failed write immediately after a boundary therefore raises the reference_unavailable alert before the required duration. Conversely, a company can have failures throughout one interval, then make one healthy write in the next. A task run after that healthy write ignores the failed interval entirely. Operators receive a false alert in the first case and miss the outage in the second. Define which completed observation interval satisfies “for 5 minutes,” and do not let a newer partial interval hide it. Add tests for both sequences with an injected clock; the live test can wait or use a controlled clock rather than weaken the duration rule.","recorded_resolution":"fixed","recorded_correction":"The evaluator now judges the latest completed window that ended within the last five minutes, and a newer partial window cannot hide it. Tests with a set clock cover both sequences.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:22:03.542Z","answer_recorded_at":"2026-10-26T07:25:52.121Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"01e9ea071f2d031b64827328e9111cfe4d8c7e02ec7e7d665953c8e88e79a46c","record_sha256":"d6def73f79d9265a492ae8932516d3a1d726f6a4d14c2993b014fb245043b345"},
    {"record_id":"author-pr121-plan-004","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The custom latency-bucket test changes the alert threshold without defining how.","finding_evidence":"The evaluator must alert above 300 ms, but the custom-bucket test says bounds [10,20,40] must produce an alert above 20 ms. The frozen rule table and declared evaluator interface provide no way to replace the 300 ms threshold. One implementer could change the threshold for this test; another could incorrectly derive it from the bucket bounds. Keep the threshold at 300 ms and use custom bounds such as [100,300,500]. Test distributions on both sides of 300 ms to prove that the evaluator reads the supplied bounds without changing the alert rule.","recorded_resolution":"fixed","recorded_correction":"The custom-bounds test now uses [100,300,500] with the threshold fixed at 300 ms, and it checks distributions on both sides of 300.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:22:03.542Z","answer_recorded_at":"2026-10-26T07:25:52.121Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"19c7d6012fdd04bdc742ccaec122b7e424b45e282918b62ad34b872cb4b46798","record_sha256":"854107d9b8d25194d408cf27edd29b7a8d9fa433ccd384e7fce592f1d046ada6"},
    {"record_id":"author-pr121-plan-005","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The live server must start outside the monolith's Bundler environment.","finding_evidence":"plan item 10 launches `timekeeping/bin/rails server -e test` from a spec run through the monolith's `bundle exec rspec`. That child inherits the monolith's `BUNDLE_GEMFILE` and Bundler settings. `timekeeping/config/boot.rb:3` uses `ENV[\"BUNDLE_GEMFILE\"] ||= ...`, so it does not replace the inherited Gemfile. The child then loads the monolith's bundle rather than the service's independent bundle. The root Gemfile does not declare `timekeeping_rules` or `timekeeping_relay`, which the service needs. Installing both bundles does not fix their selection at process launch. Specify that the live helper starts the child within `Bundler.with_unbundled_env`, sets `BUNDLE_GEMFILE` to the absolute service Gemfile path, and uses the service working directory. Without this step, the required live test cannot reliably boot the service and verify either alert.","recorded_resolution":"fixed","recorded_correction":"The live helper now starts the Timekeeping server outside the monolith's Bundler environment, with BUNDLE_GEMFILE set to the service's own Gemfile and the service directory as its working directory.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:22:03.542Z","answer_recorded_at":"2026-10-26T07:25:52.121Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"884d310ee438c8fec7442b4e54aca1843390e37ee63414d8d6bf255f8b6cdb10","record_sha256":"86a5c06d1d9fda612296fdee9f25ee995bb0144769365daa643648cafa22c0d4"},
    {"record_id":"author-pr121-plan-006","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The proposed p95 algorithm has no result when slow writes occupy the overflow bucket.","finding_evidence":"plan item 5 selects the smallest finite bucket bound whose cumulative count reaches 95%. In timekeeping/app/services/timekeeping/write_metrics.rb, latencies above 5000 ms enter the separate \"inf\" bucket. timekeeping/app/presenters/timekeeping/write_metrics_presenter.rb exposes that bucket as overflow_bucket, outside latency_bucket_bounds_ms. If every write takes 6000 ms, no finite bound reaches 95%, so the stated algorithm cannot return a p95 or raise the required latency alert reliably. This hides the alert during severe slowness or makes evaluation fail. Handle overflow explicitly as exceeding the latency threshold, and test windows whose 95th percentile falls in that bucket. Keep the overflow key sourced from the metrics response.","recorded_resolution":"fixed","recorded_correction":"Requests in the overflow bucket now count as slower than every bound, so a window whose 95th percentile lands there raises the latency alert. The overflow key still comes from the metrics response.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:22:03.542Z","answer_recorded_at":"2026-10-26T07:25:52.121Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f49d1f450a94feb6f75513aeee2e1bd2c40e3d36ce5cd0483cb36d0b6bcae6f7","record_sha256":"8dac0a39a41281e8784379b4b6eb952f6db2e1d7602e6d32d7a2d00a2d9be46d"},
    {"record_id":"author-pr121-plan-007","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The reference failure alert does not enforce the five-minute condition.","finding_evidence":"plan item 5 evaluates an unfinished window immediately. One failed write at the start of a window can therefore raise an alert within seconds. Operators would receive an alert for a brief interruption, although the ticket requires failures over 1% for five minutes. Selecting only the newest window also discards the previous window as soon as a new write opens another one. A healthy write can then hide a qualifying failure period. Define which five-minute period the task evaluates and test both an incomplete period and a transition between windows. Keep the evaluator's clock injectable so the live test need not wait five minutes. Test duration is not a reason to change the operator-visible condition.","recorded_resolution":"fixed","recorded_correction":"The evaluated period is now the latest completed five-minute window. The live test runs the task with the monolith's clock moved past that window's end, so it does not sleep.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:22:03.542Z","answer_recorded_at":"2026-10-26T07:25:52.121Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d089d67fbc87537571ab040a7d387d0bc882cfc042e30f7653d39fac715428ac","record_sha256":"184c977d758a88eb97ac830a09040b107a1f122f3a921a33d18354410b5b0770"},
    {"record_id":"author-pr121-plan-008","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The reference failure rule drops the required five-minute duration.","finding_evidence":"plan item 5 evaluates incomplete windows and explicitly rejects waiting for five minutes of evidence. A new window containing one failed write can therefore raise an alert immediately. Operators would receive an outage alert for a brief failure that the ticket says must last five minutes. `timekeeping/app/services/timekeeping/write_metrics.rb` groups events into fixed windows, but does not establish that an incomplete window satisfies that duration. Section 11.3 retains the words \"for 5 minutes\". Preserve that duration in the evaluator and add a quiet case before it elapses. Use controlled timestamps or a supplied clock to test this without making the live test sleep five minutes.","recorded_resolution":"fixed","recorded_correction":"The rate is now judged only over the latest completed five-minute window. An unfinished window never fires, and a test covers that quiet case.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:22:03.542Z","answer_recorded_at":"2026-10-26T07:25:52.121Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f97c6c946c30cbf9fbe6899a4b68d4ff354a5e0395ef0e94db5fb9e7ae31f2b1","record_sha256":"fc4ef39753ee32a8c57305f143ecbe9e1f9403a7262f47bd3c21c7acd66a4d1c"},
    {"record_id":"author-pr121-plan-009","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The signal gatherer needs an explicit result for a failed metrics read.","finding_evidence":"The declared `AlertSignals.gather` result contains only `signals` and `unavailable`. However, plan item 6 always marks five signals unavailable, while plan item 8 requires a non-zero exit specifically when the metrics read fails. The task cannot distinguish these cases from the declared result without interpreting signal names or adding an undocumented convention. Raising instead would prevent the task from evaluating the returned monolith signals. Add an explicit metrics-read failure field to the result, or declare another structured outcome that preserves those signals. Keep the exit decision in the rake task, and test expected missing sources separately from a failed read.","recorded_resolution":"fixed","recorded_correction":"The gather result now carries a separate metrics_error field, so a failed metrics read is never confused with a signal that has no source yet. The rake task decides the exit code.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:22:03.542Z","answer_recorded_at":"2026-10-26T07:25:52.121Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c3005c43f835a6afeb35e21b8c405e11efc577b315b324e002f8b350bb4c3353","record_sha256":"9fd5601a457994213be57f0f0528c94907b23739e84738393dd7598bfec4fcb9"},
    {"record_id":"author-pr121-plan-010","pr":121,"pr_url":"https://github.com/Homie-Improvement/miru/pull/121","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"high","finding_title":"The plan goes beyond the issue: Collect available signals but leave four requested alert sources unavailable indefinitely.","finding_evidence":"Distinguishing missing data from healthy data and preserving alerts after a metrics failure are appropriate, but declaring four named alert sources unavailable substitutes a reduced delivery for the requested local alerts. The author's report that producers do not yet exist establishes unresolved dependencies, not permission to drop those alerts; identify those delivery blockers or provide the required sources before claiming completion.","recorded_resolution":"fixed","recorded_correction":"No code records ownership pauses, prepare refusals, reconciliation results or a snapshot baseline. The plan now names these four as delivery blockers and ships as partial delivery.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:26:41.990Z","answer_recorded_at":"2026-10-26T07:29:33.280Z","first_independent_review_at":"2026-10-26T16:35:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"a2796fa3f52526b817d172d962f6c78f377610a6571636459e816a2a05557d55","record_sha256":"4a67508b2efc8a835b073ced9cff9f58d0daa5ccbbf819061d96c9f76ce9d2e9"},
    {"record_id":"author-pr127-plan-001","pr":127,"pr_url":"https://github.com/Homie-Improvement/miru/pull/127","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Construct the missing-URL client without calling the already stubbed constructor.","finding_evidence":"The planned 503 example would keep returning the fake client. The group's before hook already stubs `Timekeeping::EntryApiClient.new` in `spec/requests/api/v1/time_tracking/owner_reads_spec.rb:90`. Therefore, evaluating `Timekeeping::EntryApiClient.new(base_url: nil)` inside `and_return(...)` returns that fake before the new stub is installed. The later stub replaces its return value with the same fake. That fake reads successfully rather than reaching the missing-URL error in `app/services/timekeeping/entry_api_client.rb:43`, so the 503 assertions fail. Restore the original constructor before constructing the missing-URL client, then install the example's override. Keep the planned discrimination check to verify that removing this override makes the example fail.","recorded_resolution":"fixed","recorded_correction":"Agreed. The example builds the real client after restoring the original constructor and before its own stub, and the check that removing this override turns the example red stays in the plan.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:55:16.487Z","answer_recorded_at":"2026-10-26T07:56:00.560Z","first_independent_review_at":"2026-10-26T10:02:19Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"8b982337f5b27fe4cb5e9f9a6febf1b452532705639304c9e5350d19df8810dc","record_sha256":"c4c4da4ccd8afc53ddb90c2e732aa091d59e39c7a1208b9776a8ad043daf74ec"},
    {"record_id":"author-pr127-plan-002","pr":127,"pr_url":"https://github.com/Homie-Improvement/miru/pull/127","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The 503 example must bypass the constructor stub when it creates the real client.","finding_evidence":"plan item 4 cannot create the missing-URL client as written. In spec/requests/api/v1/time_tracking/owner_reads_spec.rb, the group's before hook already makes Timekeeping::EntryApiClient.new return FakeEntryApiClient. The example's Timekeeping::EntryApiClient.new(base_url: nil) therefore returns that fake, not a real client. Replacing the stub afterward only installs the same fake again. The missing-URL error in app/services/timekeeping/entry_api_client.rb never runs, so the merged 503 example fails. Restore the original constructor before creating the client, or use and_wrap_original to call the original constructor with base_url: nil. Keep both routes' status and error-body assertions.","recorded_resolution":"fixed","recorded_correction":"Agreed. The 503 example now restores the real constructor with and_call_original, builds the missing-URL client, and only then stubs new to return that client.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T07:55:16.487Z","answer_recorded_at":"2026-10-26T07:56:00.560Z","first_independent_review_at":"2026-10-26T10:02:19Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"72554b657c3dcbb675932a6c0ea60d6f469f8fd6111ef7a6fd22282f8bf254a9","record_sha256":"3e8f67a4b4c6f56addf7e9a9a12036a80ec08ebef8479c32018677ab99648812"},
    {"record_id":"author-pr129-build-001","pr":129,"pr_url":"https://github.com/Homie-Improvement/miru/pull/129","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Projection-plus-credential setup and the 403 pass-through example duplicated across spec files","finding_evidence":"The point of this change is to make the relay specs share their doubles, but it copies the 'build a FakeTimekeeping, add a deliver credential, wrap it in FakeRelay' setup into four places: support_doubles_spec.rb:50, fake_relay_delivery_spec.rb:14, relay_spec.rb:18-23 and batch_json_spec.rb:11-13. If the consumer fake's credential setup changes, all four copies must change together, and a copy someone misses drifts without any test noticing. That is the drift risk the issue was filed to remove. The 403 case is also asserted twice (support_doubles_spec.rb:108 and fake_relay_delivery_spec.rb:121); only Response#delivered?(1) is distinct.","recorded_resolution":"fixed","recorded_correction":"The four specs now build the consumer fake through one FakeRelayEndpoint.over_projection helper. The duplicate 403 example is gone, and the relay-level 403 test now also checks delivered?(1).","correction_target":"code_commit","source_path":"gems/timekeeping_relay/spec/timekeeping_relay/support_doubles_spec.rb","source_line":50,"fix_commit":"68c3a9aada857a347080dfca48106368a257bbe5","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/68c3a9aada857a347080dfca48106368a257bbe5","finding_recorded_at":"2026-10-26T09:30:12.935Z","answer_recorded_at":"2026-10-26T09:55:28.993Z","first_independent_review_at":"2026-10-26T10:06:53Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"35e51dad3f003d84a9ee94c9fd6e1eff80103cef6283a12b5cee56bd3d477e11","record_sha256":"7f480c215b225edff452a5aa0ad69c86d1346f2b7a48d51b93573a2b9ddec5b8"},
    {"record_id":"author-pr129-plan-001","pr":129,"pr_url":"https://github.com/Homie-Improvement/miru/pull/129","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The plan both forbids and requires scripted consumer-status responses.","finding_evidence":"plan item 2 says replay, ignored, 403 and stale_generation answers always come from the consumer fake, never from scripts. However, plan item 3 permits scripted relay-control tests, and U2 requires a table containing 403 and 409 stale_generation plus an all-ignored success case. Two implementers could enforce different rules about which tests may use scripts. Limit plan item 2's prohibition to examples that verify consumer behavior in fake_relay_delivery_spec.rb. Explicitly permit scripted responses for relay-control examples in relay_spec.rb, while retaining consumer-backed checks for replay, ignored counts, authorization and generation rejection. This distinction prevents scripts from replacing the checks that detect incorrect requests.","recorded_resolution":"fixed","recorded_correction":"The rule now applies only to the consumer-backed examples. Relay-control examples may script any status, 403 and 409 included, but the consumer checks still use the real fake.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T08:33:00.794Z","answer_recorded_at":"2026-10-26T08:33:57.759Z","first_independent_review_at":"2026-10-26T10:06:53Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"aa1da3a61f6f97d77901a586c09224beaa394356c49a76a6942f5289c3fe5403","record_sha256":"db1e41ed2fa85cf58c1a242f002c9b4efa982045a830ec8d16a38a16e8c0bdfc"},
    {"record_id":"author-pr138-build-001","pr":138,"pr_url":"https://github.com/Homie-Improvement/miru/pull/138","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"U1/plan item 1 grouped-line comment was not added when the _2 grouped-line example was dropped","finding_evidence":"The deleted _2 example was the only place that said, in words, that a grouped line keeps an entry even after that entry's own direct line is removed. plan item 1 dropped that example as a duplicate on the condition that this comment replaces it. The assertion is still covered, because the decision only sees `desired`, but the stated rule is gone. A maintainer reading the draft example now sees only 'Entry 4's line was removed' and nothing about the removed-direct-line case the issue asked to keep.","recorded_resolution":"fixed","recorded_correction":"Added a comment to the draft example. The decision only sees the remaining lines, so an entry whose own direct line was removed stays held while a grouped line still lists it.","correction_target":"code_commit","source_path":"gems/timekeeping_rules/spec/timekeeping_rules/reservation_decision_spec.rb","source_line":33,"fix_commit":"e9b19890ce7fdfb99960e097e5a6bfbeb7067d85","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/e9b19890ce7fdfb99960e097e5a6bfbeb7067d85","finding_recorded_at":"2026-10-26T09:47:47.400Z","answer_recorded_at":"2026-10-26T10:03:57.479Z","first_independent_review_at":"2026-10-26T10:14:27Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"74914420439a3ebd0242c4a96cb36c51468040495dae80afebbbdab55dca477c","record_sha256":"79333429fa844f5ef55773422b794640da18c802b7baa9ec883dfc5655f48635"},
    {"record_id":"author-pr138-plan-001","pr":138,"pr_url":"https://github.com/Homie-Improvement/miru/pull/138","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep the allowed-operation assertion for a token that also lists a disallowed operation.","finding_evidence":"The reservation_access merge map drops the own-company `:allowed` assertion as a duplicate, but its fixture checks a distinct case. In `timekeeping/spec/services/timekeeping/reservation_access_2_spec.rb`, the token lists both `reservations.prepare` and `entries.delete`, and prepare must still be allowed. The sibling checks prepare with only `reservations.prepare`. A regression that discards all grants whenever any listed operation is outside the allowlist would pass the planned assertions, including the added `entries.delete` denial. It fails the original mixed-list prepare assertion. `ReservationAccess.granted_operations` currently intersects the two lists, so an extra name does not cancel a valid grant. Keep the `reservations.prepare` allowed assertion on the same mixed-list identity used for the added denial.","recorded_resolution":"fixed","recorded_correction":"Agreed. The merged spec now keeps the prepare-allowed check on the token that also lists entries.delete, plus the company 2 and unlisted-commit checks on that same token.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:28:53.058Z","answer_recorded_at":"2026-10-26T09:29:36.038Z","first_independent_review_at":"2026-10-26T10:14:27Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c98d9a938ab8fc1afc5340823cc2606acb82cf24e4c511ecba88b9e2c2615893","record_sha256":"77aa62e187c909f5903876cdd384ff572fa6e2f0b25f173304e52e654fd57b13"},
    {"record_id":"author-pr138-plan-002","pr":138,"pr_url":"https://github.com/Homie-Improvement/miru/pull/138","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep the allowed-operation assertion for a token that also lists a forbidden operation.","finding_evidence":"The reservation_access_2_spec.rb example checks that one token listing both `reservations.prepare` and `entries.delete` still allows `reservations.prepare`. The sibling checks allowance only for a token listing `reservations.prepare` alone. The merge map drops the former assertion as a duplicate and keeps only the mixed token's denial for `entries.delete`. These cases differ: `timekeeping/app/services/timekeeping/reservation_access.rb` intersects the token's operations with the background allowlist in `granted_operations`. Rejecting every operation when a token lists an unsupported name would pass the planned assertions but fail the deleted example. Keep the `:allowed` assertion for `reservations.prepare` on the mixed token, or use that token for the sibling's existing positive assertion. This preserves a case only the deleted file covers.","recorded_resolution":"fixed","recorded_correction":"Agreed. The token listing both reservations.prepare and entries.delete keeps its allowed check for prepare in the merged example, so a rule that drops every grant on one bad name would fail.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:28:53.058Z","answer_recorded_at":"2026-10-26T09:29:36.038Z","first_independent_review_at":"2026-10-26T10:14:27Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"52aa631cbb90c903da05c320cc65bda4e3804bc91a6b4f107c399dbcf516f2c3","record_sha256":"b18457a18438786494be6b567bd298353744619d9322f44256d11505d6d78ce2"},
    {"record_id":"author-pr141-build-001","pr":141,"pr_url":"https://github.com/Homie-Improvement/miru/pull/141","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Endless default arguments swallow later stray requests","finding_evidence":"An endless builder with a parenthesized default expression lets the gate count requests in later examples as part of that builder. For `def post_company_scope_invoice(ref = missing_id.to_s()) = send_request(:post, api_v1_invoices_path, params: {})`, the signature matcher stops at the inner closing parenthesis and does not recognize the endless method. That method has no closing end, so the scan takes the next same-indent `end` (or, if there is none, the rest of the section) as the method's end, which hides every later stray request. This breaks the intent's contract that the spec must fail when any send_request call sits outside the one builder. It also contradicts the module's own comment that an endless method sits on its def line.","recorded_resolution":"fixed","recorded_correction":"The endless-method check now matches balanced parentheses, so the given builder ends on its def line and the later request counts as stray. A method with no closing end now stops the scan instead of running to the end.","correction_target":"code_commit","source_path":"spec/support/company_scope_request_spec_scan.rb","source_line":12,"fix_commit":"0e609bf9db1db90ad8b015a01a095e38e4a50691","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/0e609bf9db1db90ad8b015a01a095e38e4a50691","finding_recorded_at":"2026-10-26T10:00:29.952Z","answer_recorded_at":"2026-10-26T10:14:55.868Z","first_independent_review_at":"2026-10-26T10:28:09Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"385cddfa6bcf1bbb6570a91ee87280d4a11cce187e015b106e162978a9ea8dca","record_sha256":"495418a24697ebabb0213588265d4a7873ed2b1419c15d3dd1a28aa181c89622"},
    {"record_id":"author-pr141-plan-001","pr":141,"pr_url":"https://github.com/Homie-Improvement/miru/pull/141","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Counting comments as request calls still rejects valid assertion helpers.","finding_evidence":"The decisions count every `send_request` occurrence, rather than calls. Add an assertion-only method containing `# Check the send_request response.` beside the existing builder in `spec/requests/api/v1/invoices/create_spec.rb:576`. The planned scan reports two builders, although the added method sends no request. The same comment outside either method becomes a stray request. This defeats the promised allowance for assertion helpers. Exclude comment-only lines before classifying methods and counting stray calls, and add a fixture proving that these comments do not change the result.","recorded_resolution":"fixed","recorded_correction":"The scan now drops comment text before it looks for `send_request`, so a comment that names it neither makes a builder nor counts as a stray request. A fixture covers both cases.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:10:13.683Z","answer_recorded_at":"2026-10-26T09:10:45.610Z","first_independent_review_at":"2026-10-26T10:28:09Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"aac483c256a1090a9881e7f2c9870e7a90104a67ec42367fbb2c3155e178f76b","record_sha256":"31f177d0eb0995544ae8d4481207658603c6daaf2a40de3a0fff2b91d1560b73"},
    {"record_id":"author-pr148-build-001","pr":148,"pr_url":"https://github.com/Homie-Improvement/miru/pull/148","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"Local owner create and update accept an agent_id of another company or user, committing a snapshot the projection refuses","finding_evidence":"An otherwise valid create naming another company's or another user's agent gets 201 and commits the entry, audit, receipt and outbox snapshot. ProjectionConsumer#refuse_foreign_agents! refuses that snapshot and the relay re-sends it, so every later entry is held back. An update that changes only user_id on an agent-backed entry does the same. This breaks LocalOwner's own promise (local_owner.rb:57-59) that no committed snapshot can be one the projection refuses.","recorded_resolution":"fixed","recorded_correction":"Create and update now answer not_found and write nothing when the entry's agent is not one of the company's agents belonging to the entry's user. New tests cover a foreign agent, a colleague's agent and a user-only move.","correction_target":"code_commit","source_path":"app/services/timekeeping/local_owner.rb","source_line":338,"fix_commit":"24df29f06283251150d48ec0be5216296ec5ed71","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/24df29f06283251150d48ec0be5216296ec5ed71","finding_recorded_at":"2026-10-26T16:42:16.297Z","answer_recorded_at":"2026-10-26T16:59:40.382Z","first_independent_review_at":"2026-10-26T17:29:50Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"630fa0522d0380d00c0a2c7409cd1ad4aec1e6a814a69bed8d9f9d0c1a713f56","record_sha256":"947770974b479b67d52f54c735e134ee5b0fbf25afb272c8a9154363747ae6c1"},
    {"record_id":"author-pr148-build-002","pr":148,"pr_url":"https://github.com/Homie-Improvement/miru/pull/148","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"LocalOwner's work_date check admits ISO 8601 forms the service refuses with 400: compact and datetime forms are stored, week and ordinal forms raise at insert","finding_evidence":"After a rollback, the same entry API answers 201 here and 400 from the service for \"20260302\" or a datetime string, so clients can come to rely on input that breaks when ownership flips back. Week and ordinal forms may also reach the NOT NULL work_date column as an unhandled error. The field-shape rule lives in two places, although every rule is meant to come from the shared gem.","recorded_resolution":"fixed","recorded_correction":"The YYYY-MM-DD work_date form now lives once in the rules gem. The service's entry command and the local owner both use it, so compact, date-time, week and ordinal forms get 400 invalid_request.","correction_target":"code_commit","source_path":"app/services/timekeeping/local_owner.rb","source_line":289,"fix_commit":"24df29f06283251150d48ec0be5216296ec5ed71","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/24df29f06283251150d48ec0be5216296ec5ed71","finding_recorded_at":"2026-10-26T16:42:16.297Z","answer_recorded_at":"2026-10-26T16:59:40.382Z","first_independent_review_at":"2026-10-26T17:29:50Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"678b8b76a1e229381cd8f2cf147ecc1ec4a6d810a94d1550c9303d683ecab829","record_sha256":"a1752fe976f74d2a049f24d7e8b4582764d9cccc4fa510ab1b085f1e3ef1f74f"},
    {"record_id":"author-pr148-build-003","pr":148,"pr_url":"https://github.com/Homie-Improvement/miru/pull/148","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"entry_reader_spec rewrite deletes five existing EntryReader tests","finding_evidence":"The author says the edit only added rails_helper and a frozen-string comment. In fact the base examples were replaced by one new prepared-branch example. Five reader behaviours lost their only regression coverage: legacy company scoping, dropping foreign entries and projects, hiding discarded entries, ordering with read-only records and review_status, and raising the client error without falling back to timesheet_entries. EntryReader's only production change is the prepared branch's model name, so those tests still apply, and a regression in its tenancy filter, discarded_at filter or error propagation would now pass CI.","recorded_resolution":"fixed","recorded_correction":"I restored all six base examples word for word: a diff of the file's first 145 lines against the base shows no difference. The new LocalEntry example follows them, and the file passes.","correction_target":"code_commit","source_path":"spec/services/timekeeping/entry_reader_spec.rb","source_line":8,"fix_commit":"24df29f06283251150d48ec0be5216296ec5ed71","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/24df29f06283251150d48ec0be5216296ec5ed71","finding_recorded_at":"2026-10-26T16:42:16.297Z","answer_recorded_at":"2026-10-26T16:59:40.382Z","first_independent_review_at":"2026-10-26T17:29:50Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"1d25b8101c5554644facc00b70d067c354e4f67a1c207e004236d60321f82634","record_sha256":"06a986c4fa46941bffa800139f2162637d8869b07d0d2dd64ce6acc31c7e8ace"},
    {"record_id":"author-pr148-plan-001","pr":148,"pr_url":"https://github.com/Homie-Improvement/miru/pull/148","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Check that a user actor belongs to the requested company before applying entry rules.","finding_evidence":"plan item 5 scopes service actors but gives user actors only user_id and role. The planned checks never establish the user's membership in the keyword company. Consider Actor.user(user_id: user_a, role: \"owner\") with company B, a valid B project, and no body user_id. In gems/timekeeping_rules/lib/timekeeping_rules/entry_rules.rb, TARGET_USER_TABLE returns :acting before checking company_user_ids. The plan can therefore create a B entry assigned to a user who belongs only to A. Checking requested target users does not prevent this path. Read the acting user's record from ReferenceFacts for the keyword company, refuse an absent user, and use that record's role before calling the shared rules. Add this mismatched actor/company case to the provider tests.","recorded_resolution":"fixed","recorded_correction":"Before any rule runs, the acting user must be one of the keyword company's users in ReferenceFacts. Otherwise the request answers not_found and writes nothing. A provider test covers a user from another company.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:05:49.109Z","answer_recorded_at":"2026-10-26T09:07:44.532Z","first_independent_review_at":"2026-10-26T17:29:50Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"ea27c4be6953dcc059507721140ac07ffaa2cc33dc1caa34bf9380c6cbeb029d","record_sha256":"537d869d1b2f85e80234fcd0751466b047890b908a17f25f1e063d23166ac3d9"},
    {"record_id":"author-pr148-plan-002","pr":148,"pr_url":"https://github.com/Homie-Improvement/miru/pull/148","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Remove the source-text dependency between independent outbox locks.","finding_evidence":"plan item 6 makes monolith tests parse the service's Ruby source to enforce equal advisory-lock keys. These locks protect different databases: config/database.yml uses DATABASE_URL, while timekeeping/config/database.yml uses TIMEKEEPING_DATABASE_URL. Equal keys do not coordinate those databases. The check makes an independent service lock change or equivalent constant expression break monolith tests without any ordering defect. Keep the monolith's lock key defined once for its local outbox writers, and remove the cross-application source-text comparison. The ticket requires ordered commits, not equal lock keys across applications.","recorded_resolution":"fixed","recorded_correction":"The monolith defines its outbox lock key once, for its own writers on its own database. The test that compared it with the service's source text is removed.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:05:49.109Z","answer_recorded_at":"2026-10-26T09:07:44.532Z","first_independent_review_at":"2026-10-26T17:29:50Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"3d01a84f2ede9097357be28fec8e6dcb167fe7f991a367c5216ad00e20c66a68","record_sha256":"81f685ec55fc5d1a155fcd50991e66c2f1127f67c95de5d580ca5c690c608dbf"},
    {"record_id":"author-pr148-plan-003","pr":148,"pr_url":"https://github.com/Homie-Improvement/miru/pull/148","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Resolve the acting user's membership and role from the company's current reference facts.","finding_evidence":"plan item 5 gives a user actor a user_id and role, but the write design never resolves that actor against ReferenceFacts. The shared rule does not supply this check: EntryRules::TARGET_USER_TABLE returns :acting for an omitted user_id before checking company_user_ids. EntryDecision.target_user preserves that behavior. A user actor paired with another company could therefore create an entry there, despite company-scoped row queries. A stale privileged actor role could also authorize changes that current company facts forbid. Before applying the rules, require the acting user to appear in ReferenceFacts for the keyword company_id, and use that user's reference role. Add provider tests for an acting user outside the company and an actor role that differs from the reference role.","recorded_resolution":"fixed","recorded_correction":"Same fix as the two findings above: the acting user's membership and role come from ReferenceFacts for the keyword company, and provider tests cover both cases.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:05:49.109Z","answer_recorded_at":"2026-10-26T09:07:44.532Z","first_independent_review_at":"2026-10-26T17:29:50Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"a1d6b94bbbc0f02d14f0dbe81f96c5b9c418d0e011abb3e7fb9ed7d48695522f","record_sha256":"b7ff6770d94c38a48c0264cddcd0c65a84c124af9f034f059e75d386e2b97150"},
    {"record_id":"author-pr148-plan-004","pr":148,"pr_url":"https://github.com/Homie-Improvement/miru/pull/148","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Resolve the acting user's role from the company's reference facts.","finding_evidence":"R11 retains the target-user rule but omits the required source of its role. The actor's supplied role is the plan's only declared role source. `app/services/timekeeping/reference_facts.rb` returns each company user's role, and `app/services/timekeeping/user_token.rb` already uses that role for write authority. Without that step, a stale admin role, or one from another company, can authorize changes to colleagues' entries. Add this requirement to R11 and implement it in U3: resolve the acting user and role from the company's reference facts before applying entry rules. Add a provider test where `Actor.user` supplies `admin` but those facts identify the user as an employee.","recorded_resolution":"fixed","recorded_correction":"The role passed to the shared rules now comes from ReferenceFacts, never from the actor. A provider test has the actor claim admin while the facts say employee.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:05:49.109Z","answer_recorded_at":"2026-10-26T09:07:44.532Z","first_independent_review_at":"2026-10-26T17:29:50Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"88611d7a5d6424e08e12d272e3ed1c479dc6cbc82c893904a87feca54a7b11ea","record_sha256":"c4ecd6706b468b9d2e23cadce7d4afdfa3a99f8265b48dd59e7851c0f63ac3d4"},
    {"record_id":"author-pr148-plan-005","pr":148,"pr_url":"https://github.com/Homie-Improvement/miru/pull/148","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The API response and the outbox snapshot require different field lists.","finding_evidence":"The assumption that both use the same fields is false. contracts/timekeeping/entry_api.yml defines response.entry_fields without external_run_id, external_session_id, discarded_at, created_at or updated_at. Those fields belong to the snapshot. spec/contracts/timekeeping/entry_api_examples.rb:129 checks the response keys exactly, so returning the snapshot would fail the required contract run. Resolve this before implementation: use the API contract for responses and SnapshotBatch::ENTRY_FIELDS for outbox payloads. timekeeping/app/presenters/timekeeping/entry_presenter.rb already shows the separate response shape.","recorded_resolution":"fixed","recorded_correction":"Responses now use response.entry_fields from contracts/timekeeping/entry_api.yml, and outbox payloads use SnapshotBatch::ENTRY_FIELDS. The same-fields assumption is gone.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:05:49.109Z","answer_recorded_at":"2026-10-26T09:07:44.532Z","first_independent_review_at":"2026-10-26T17:29:50Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"068d84d500c85e97036ed7960cc62d3a320be874bfa0b467b5b9ee3efcdff4fb","record_sha256":"cce164409a2962c460e94affdc71d622c09ed431260b9e8da15a4d5febc1e57b"},
    {"record_id":"author-pr148-plan-006","pr":148,"pr_url":"https://github.com/Homie-Improvement/miru/pull/148","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The checkpoint list omits the step that checks versions and rules.","finding_evidence":"R2 makes checking If-Match and the rules a separate transaction step. R4 promises a checkpoint after each step. However, the declared STEPS list goes from rows_locked directly to entry_written. An implementer following that list cannot expose the promised checkpoint after validation and before the entry changes. Add a named checkpoint for that step and include it in the sequence test.","recorded_resolution":"fixed","recorded_correction":"A rules_checked step now sits between rows_locked and entry_written, and the sequence test reads it.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:05:49.109Z","answer_recorded_at":"2026-10-26T09:07:44.532Z","first_independent_review_at":"2026-10-26T17:29:50Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"88f7bee8f6c9a8cba869ed7f5efcde44b6fd1094bc04fda6dd8de5db7fd5ffee","record_sha256":"9b2bc931c4555ad7376d347ccd1c2868109d2ce672811bc7c541175d3a332521"},
    {"record_id":"author-pr148-plan-007","pr":148,"pr_url":"https://github.com/Homie-Improvement/miru/pull/148","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The preflight refusals contradict the unconditional stale-generation response.","finding_evidence":"R7 requires 409 stale_generation for any generation when ownership is not prepared or a handoff is active. The design instead checks the actor, body, credentials and snapshot values before reading ownership. plan item 5 explicitly returns 403 for a service actor before that read. Thus a request during handoff can return 400 or 403 rather than the promised 409. State one consistent precedence across R7, plan item 5 and the step sequence. Keep the ownership check before these refusals if R7 remains unconditional, and test a stale request with an otherwise invalid body.","recorded_resolution":"fixed","recorded_correction":"Ownership is now read first, inside the transaction, before every other refusal. A request while fenced answers 409 even with a bad body or a service actor, and a test covers this.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:05:49.109Z","answer_recorded_at":"2026-10-26T09:07:44.532Z","first_independent_review_at":"2026-10-26T17:29:50Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"3647c637faf4a2db71d638dda283b26f898841764572bae05ce4cff1e59a1217","record_sha256":"8783794847ea39974f68b37718fc00bf8c3b8498c1d3b91abae6e1ba6b42c2dd"},
    {"record_id":"author-pr148-plan-008","pr":148,"pr_url":"https://github.com/Homie-Improvement/miru/pull/148","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The tenancy fixtures cannot give two entries the same primary key.","finding_evidence":"U4 requires two companies with overlapping entry ids. db/schema.rb:414 creates entries with the default global id primary key. Its additional company_id/id index does not allow duplicate ids. The proposed fixture would fail before exercising company isolation. Keep the existing schema and specify feasible collisions: reuse receipt keys across companies, pass foreign entry ids, and create foreign-company reservation rows that reference the tested entry id. Assert that these rows neither authorize a write nor set its reservation flag. Disclose how these cases interpret the ticket's overlapping-id requirement.","recorded_resolution":"fixed","recorded_correction":"Entry ids and outbox positions are global keys, so they cannot overlap. The tenancy spec now reuses receipt keys, actors and operations across companies and sends foreign ids; the pull request explains this reading.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:05:49.109Z","answer_recorded_at":"2026-10-26T09:07:44.532Z","first_independent_review_at":"2026-10-26T17:29:50Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"02f0df6a9b3032ef9456ee6753a58a051d95432ac1e6e1f2929c7e2a80895f7a","record_sha256":"1a971a25fc1404a9e1be8624b368b610668ea44daab7537e130f409be4610f25"},
    {"record_id":"author-pr148-plan-009","pr":148,"pr_url":"https://github.com/Homie-Improvement/miru/pull/148","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Validate source and bill_status only when the request supplies them.","finding_evidence":"plan item 6 sends body values through both snapshot casts before writing. A valid bulk_delete body contains only entry_ids; DELETE can have an empty body. Both therefore supply nil to these casts. In app/services/timekeeping/snapshot_batch.rb, FIELD_CASTS rejects nil for both fields, so the planned check returns 400 instead of deleting entries. Updates that change only note fail too. The service avoids this in EntryCommand.check_fields! by checking body.key?(field) before validating a value. Specify that same presence check here. Validate supplied values, but preserve stored values or apply create defaults when fields are absent. Include empty-body DELETE and bulk_delete in the provider tests.","recorded_resolution":"fixed","recorded_correction":"Source and bill_status are checked only when the body carries the key. Absent fields keep their stored values or the create defaults. Tests cover an empty-body delete, bulk_delete and a note-only update.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:05:49.109Z","answer_recorded_at":"2026-10-26T09:07:44.532Z","first_independent_review_at":"2026-10-26T17:29:50Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"553d979b32d525112d169b1eee1ff1ae03da8b6ff3cb80555c5a0b52f2bec509","record_sha256":"f8561d312e08fafe7660755d8aa6aef9105436c670e233c1550e02629c24c97a"},
    {"record_id":"author-pr148-plan-010","pr":148,"pr_url":"https://github.com/Homie-Improvement/miru/pull/148","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"medium","finding_title":"The plan goes beyond the issue: Reject service actors for entry operations and add an unused service-permission helper for a later reservation feature.","finding_evidence":"Representing the requested service actor and preserving the entry API's user-only boundary are within this change. Defining and testing a permission helper solely for the later reservation leaf adds unused authorization behavior; defer that helper until its consumer is implemented.","recorded_resolution":"fixed","recorded_correction":"I removed the service-permission helper and its tests. Entry operations still refuse service actors. The helper is listed as follow-up work for the reservation operations, which will use it.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:08:57.366Z","answer_recorded_at":"2026-10-26T09:10:14.460Z","first_independent_review_at":"2026-10-26T17:29:50Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"55c180d086203544e44be2f478a2769269863857c9711c0c71c456b4075a26d5","record_sha256":"a39b5816f7f5c3005827605802a8ef37a951de7e649ede74bf647c037f33acbb"},
    {"record_id":"author-pr149-build-001","pr":149,"pr_url":"https://github.com/Homie-Improvement/miru/pull/149","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Delayed workers let lock regression tests pass without locks","finding_evidence":"These two specs are the only proof that the FOR SHARE lock in unless_handoff_begun closes the race between the hard-delete check and a handoff beginning. Each shows the wait only by `thread.join(0.5)` returning nil (lines 33 and 64). A worker that has not yet checked out a connection or sent its statement also returns nil there. So on a slow runner, both examples pass with `lock(\"FOR SHARE\")` removed, and the irreversible-delete race could reopen unnoticed. The trusted convention docs/solutions/conventions/prove-a-lock-wait-with-blocked-by-backend-pids-not-a-join-timeout.md names this exact pattern.","recorded_resolution":"fixed","recorded_correction":"Both examples now record each connection's backend pid and ask Postgres, while the holder is paused, whether the waiter is blocked by it. With FOR SHARE removed, both examples failed; with it restored, both passed.","correction_target":"code_commit","source_path":"spec/models/timekeeping_ownership_concurrency_spec.rb","source_line":64,"fix_commit":"ab77d52e0ed750dc7721cf707fd6c52118c3bc77","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ab77d52e0ed750dc7721cf707fd6c52118c3bc77","finding_recorded_at":"2026-10-26T16:40:33.073Z","answer_recorded_at":"2026-10-26T16:55:06.419Z","first_independent_review_at":"2026-10-26T17:09:28Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"f36699676e8587588ead4ea6e20a35bcf006b25e8e3f6035b74ddc63f1afc1c1","record_sha256":"951c3bbedc3a03ac917f7db2ba0ed9d1e03c4ab523245ff6c09223e47af88a82"},
    {"record_id":"author-pr149-build-002","pr":149,"pr_url":"https://github.com/Homie-Improvement/miru/pull/149","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Destroy spec repeats the refusal-body and 403 cases","finding_evidence":"Two top-level describes share the name 'Api::V1::Users#destroy handoff refusal body' (lines 148 and 342) and overlap heavily: 422, keys == ['errors'], the fixed message, changed_by absent. The admin-during-handoff 403 is asserted at line 125 and again at line 415. A change to the refusal body or the refusal order has to be edited in two places, and the duplicate group name makes failure output harder to read. Commit 52a50a7 ('fold duplicated handoff refusal specs') left these in place. The two body examples use different begun states and different leak probes, so the merge must keep both.","recorded_resolution":"fixed","recorded_correction":"Both refusal-body examples now sit in one group, with every assertion and both leak checks kept. The 403-during-handoff example moved into the refusal-order group with its audit-count and message checks.","correction_target":"code_commit","source_path":"spec/requests/api/v1/users/destroy_spec.rb","source_line":148,"fix_commit":"ab77d52e0ed750dc7721cf707fd6c52118c3bc77","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ab77d52e0ed750dc7721cf707fd6c52118c3bc77","finding_recorded_at":"2026-10-26T16:40:33.073Z","answer_recorded_at":"2026-10-26T16:55:06.419Z","first_independent_review_at":"2026-10-26T17:09:28Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"89b91d2e7a875a9b95afacfe831eb6e412db45a8ac9fde752fae6ae0510f7a01","record_sha256":"1ff3bcceae4385ae31f8e1c35979500ea1dcccbcbb0fc9383471aca918bd8a79"},
    {"record_id":"author-pr149-build-003","pr":149,"pr_url":"https://github.com/Homie-Improvement/miru/pull/149","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Restored snapshot test drops newer-generation and nil-position checks","finding_evidence":"Commit eec5dd4 says it restored the base's previous-generation snapshot example. The rebuilt example dropped two refusals: a generation newer than the current one (3 while the row is at 2), and the previous generation while final_outbox_position is nil during a freeze. No other example in the file covers either case. The guards at app/models/timekeeping_ownership.rb:101-102 could then be weakened or removed with the suite still green, and the projection consumer would accept snapshots it must refuse. The issue did not ask for this edit, and the commit message claims a restore that did not fully happen.","recorded_resolution":"fixed","recorded_correction":"I copied the base branch's example back word for word. That brings back the newer-generation refusal, the generation 0 refusal, and the nil final_outbox_position refusal during a freeze.","correction_target":"code_commit","source_path":"spec/models/timekeeping_ownership_spec.rb","source_line":100,"fix_commit":"ab77d52e0ed750dc7721cf707fd6c52118c3bc77","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ab77d52e0ed750dc7721cf707fd6c52118c3bc77","finding_recorded_at":"2026-10-26T16:40:33.073Z","answer_recorded_at":"2026-10-26T16:55:06.419Z","first_independent_review_at":"2026-10-26T17:09:28Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"af8e535732aa5b662d025d2e0458982daa9d9c93c983e9a5c2f28c00a9637819","record_sha256":"4e22be4e6b86fc137b13b75082b3d94c5ed3a6f1da5cedd73a7f61676f41e310"},
    {"record_id":"author-pr149-build-004","pr":149,"pr_url":"https://github.com/Homie-Improvement/miru/pull/149","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Tautological assertions that cannot fail","finding_evidence":"Each of these lines compares a value with a fresh read of the same value taken just before, so it can never fail. They look like they follow the trusted 'assert the baseline' convention but check nothing, which misleads a reader about what the refusal specs prove. The post-action comparisons still prove each refusal, so this is misleading dead weight, not a coverage hole.","recorded_resolution":"fixed","recorded_correction":"Removed the two self-comparisons in the demo seeder spec. In the seeds spec, the self-comparison now checks the seeded business_phone value instead. The checks after each refusal are unchanged.","correction_target":"code_commit","source_path":"spec/db/seeds_handoff_refusal_spec.rb","source_line":29,"fix_commit":"ab77d52e0ed750dc7721cf707fd6c52118c3bc77","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ab77d52e0ed750dc7721cf707fd6c52118c3bc77","finding_recorded_at":"2026-10-26T16:40:33.073Z","answer_recorded_at":"2026-10-26T16:55:06.419Z","first_independent_review_at":"2026-10-26T17:09:28Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"056ed1a33d93ccb91f802ecb101bc027c6eac231860ec8c792490c31049499bd","record_sha256":"6a404274e8d3389bb6873c0685cfc66f15a5191afdfafab2c0f721d49c17e80a"},
    {"record_id":"author-pr149-plan-001","pr":149,"pr_url":"https://github.com/Homie-Improvement/miru/pull/149","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Deferring synchronization permits permanent user deletion after a handoff starts.","finding_evidence":"A delete request can read the initial state, pause, then resume after another connection commits handoff_step = 'close_admission'. The planned check has already passed, so app/controllers/api/v1/users_controller.rb:82 permanently deletes the user. User also destroys employments, project memberships and credentials through dependent associations in app/models/user.rb:36-57. Choose a non-owner target with no timesheet entries so no entry foreign key stops the deletion. This is irreversible deletion after the promised cutoff, not merely a different audit result. Protect the ownership read and deletion with one transaction and a shared row lock, as app/services/timekeeping/billing_fence.rb already does for billing writes. Add a two-connection ordering test. Operator rarity does not prevent this sequence.","recorded_resolution":"fixed","recorded_correction":"The delete now reads the ownership row with a shared lock in one transaction with the audit row and the delete, as the billing fence does, and a two-connection test pins the ordering.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:34:57.711Z","answer_recorded_at":"2026-10-26T09:36:19.026Z","first_independent_review_at":"2026-10-26T17:09:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"182332d93990de3f35b89c7608f3dad2a17b4d436392cd4784e2229c7b2377bd","record_sha256":"c77f5c3c5da7093feb25c395467221bb30d97aad9499b9b6a00491ad4bc0676c"},
    {"record_id":"author-pr149-plan-002","pr":149,"pr_url":"https://github.com/Homie-Improvement/miru/pull/149","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Keep ownership initialization out of the timesheet data writer.","finding_evidence":"plan item 4 gives `db/seeds/timesheet_data.rb` responsibility for recreating the ownership record before checking it. `TimekeepingOwnership.ensure_initial!` in `app/models/timekeeping_ownership.rb:32–33` inserts an absent row with initial defaults. A missing record after a handoff would therefore become permission to seed, rather than a refusal. This bypasses the shared predicate and allows demo writes when ownership is unknown. The timesheet script currently has no initialization call; its first write is `Company.find_or_create_by!`. Keep initialization in explicit database setup, and let this script use the read-only refusal helper before any write. Replace the proposed missing-row success test with a refusal test that also proves the ownership table stays empty. An existing initial legacy row should still permit seeding.","recorded_resolution":"fixed","recorded_correction":"Initialization stays in database setup and db/seeds.rb only. The timesheet script uses the read-only refusal helper, and the missing-row success test is replaced by a refusal test.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:34:57.711Z","answer_recorded_at":"2026-10-26T09:36:19.026Z","first_independent_review_at":"2026-10-26T17:09:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"b1d5083852e6dfb131a82fef8d83bcd60e6afc165bed87b466deb64f15197a06","record_sha256":"cc178f53b6be3d441880edb731734897bfbed6202961cefce402e27da3f86ae2"},
    {"record_id":"author-pr149-plan-003","pr":149,"pr_url":"https://github.com/Homie-Improvement/miru/pull/149","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The new bootstrap call lets the timesheet seeder bypass refusal when ownership state is missing.","finding_evidence":"Under plan item 4, run db/seeds/timesheet_data.rb after the ownership row disappears during a handoff. TimekeepingOwnership.ensure_initial! inserts a generation-1 legacy row at app/models/timekeeping_ownership.rb:33. The subsequent check admits the seeder, which creates users, projects and timesheet entries. It also leaves every other guard reading a false initial state. The ticket explicitly places bootstrap before refusal in db/seeds.rb, but gives no such exception to timesheet_data.rb. Remove the new bootstrap call from that writer. Test that a missing row causes refusal and remains missing. Keep schema initialization separate from a writer that must treat lost ownership state as unsafe.","recorded_resolution":"fixed","recorded_correction":"The timesheet seed file no longer calls ensure_initial!. A missing ownership row now refuses it, and a test proves the row stays missing and no company is written.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:34:57.711Z","answer_recorded_at":"2026-10-26T09:36:19.026Z","first_independent_review_at":"2026-10-26T17:09:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"3c7a8b5ff7a6eccb688a4009718f038cdd3d2299fc4f6b401688493740023610","record_sha256":"5c080c122c4f7536f23b7c2d92a2f9dd51c3df1f799a2b7c44446a5181548bea"},
    {"record_id":"author-pr149-plan-004","pr":149,"pr_url":"https://github.com/Homie-Improvement/miru/pull/149","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The new seed bootstrap bypasses the missing-row refusal.","finding_evidence":"plan item 4 and U4 make `db/seeds/timesheet_data.rb` create an initial ownership row before checking it. With a missing row, the builder therefore writes and proceeds instead of refusing. This contradicts R2 and R3, and U4 explicitly tests the opposite outcome. R7 preserves an existing initial legacy state; it does not establish that a missing row is that state. The ticket specifies initialization before checking only for `db/seeds.rb`. Remove the new initialization from `db/seeds/timesheet_data.rb` and test that a missing row causes refusal without creating ownership or company records. Keep fresh-database initialization separate from this builder so missing handoff state cannot silently authorize writes.","recorded_resolution":"fixed","recorded_correction":"The plan now refuses the timesheet script on a missing row, matching the missing-row rule, and the test checks that no ownership or company record is created.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:34:57.711Z","answer_recorded_at":"2026-10-26T09:36:19.026Z","first_independent_review_at":"2026-10-26T17:09:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"19d9daa2d3d57f1b62b5e79ac9aeefea70224818fe05e131baeb9e6c6ca4dfaf","record_sha256":"801eccfc340a96e23c430c239b3236850ff8b4f2cd9957d71f02572830326083"},
    {"record_id":"author-pr149-plan-005","pr":149,"pr_url":"https://github.com/Homie-Improvement/miru/pull/149","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The planned fresh-database seed test lacks a valid company fixture.","finding_evidence":"U4 expects timesheet_data.rb to seed \"Test Company\" when the ownership row is absent. That script cannot create the company on a fresh database: db/seeds/timesheet_data.rb:11 supplies only its name and business_phone, but app/models/company.rb:76 requires country. db/schema.rb defines country without a default. Adding ensure_initial! does not satisfy that prerequisite, so the proposed successful seed test will fail after creating the ownership row. Define the valid fixtures needed to exercise the initial-state path, or explicitly account for the existing builder failure. Do not describe ownership initialization alone as making this script work on a schema-loaded database.","recorded_resolution":"fixed","recorded_correction":"Confirmed in app/models/company.rb:76 that country is required. The legacy-state test now pre-creates a valid Test Company and checks only that the handoff check lets the script through.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:34:57.711Z","answer_recorded_at":"2026-10-26T09:36:19.026Z","first_independent_review_at":"2026-10-26T17:09:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"872ee00927045b842624e92c269e51052953370293a185349e1b1713c2a8f748","record_sha256":"1c668e135601e24721a86559f13c34d6de5b434fba55e51ea1a749c8656460de"},
    {"record_id":"author-pr149-plan-006","pr":149,"pr_url":"https://github.com/Homie-Improvement/miru/pull/149","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The timesheet seed builder must refuse when the ownership row is missing.","finding_evidence":"plan item 4 adds `ensure_initial!` to `db/seeds/timesheet_data.rb`, turning a missing ownership row into permission to seed. In `app/models/timekeeping_ownership.rb:33-34`, that method inserts a generation-1 row when none exists. The proposed check then passes, allowing the company write at `db/seeds/timesheet_data.rb:11` and subsequent demo writes despite lost ownership state. The ticket explicitly places bootstrap before the check in `db/seeds.rb`, but gives no such exception for this builder. Remove the added bootstrap call from the timesheet builder. Check the existing row before any write, and test that a missing row raises without creating ownership or demo records. Preserve successful seeding when the initial legacy row exists.","recorded_resolution":"fixed","recorded_correction":"The added bootstrap call is gone. The timesheet script checks the existing row before its first write, and still seeds past the check when the initial legacy row exists.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:34:57.711Z","answer_recorded_at":"2026-10-26T09:36:19.026Z","first_independent_review_at":"2026-10-26T17:09:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"5c9d26a3335a36d9ac1a83d19662632aef5aa87854138dfadf85316bbdf3b9f6","record_sha256":"0aef646ba3db4737fd819001685d48ae2a1872782608f44b9d1f7dd7d5444798"},
    {"record_id":"author-pr149-plan-007","pr":149,"pr_url":"https://github.com/Homie-Improvement/miru/pull/149","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The timesheet seeder must not recreate a missing ownership row before checking it.","finding_evidence":"plan item 4 makes a missing safety record enable writes instead of refusing them. In app/models/timekeeping_ownership.rb, ensure_initial! inserts id 1 with the initial defaults whenever the row is absent. Calling it before the new check therefore clears the only evidence available to distinguish a fresh database from lost handoff state. The script then creates users and grants roles, as db/seeds/timesheet_data.rb shows. It also leaves every other guarded writer seeing an initial state. Keep initialization in database setup and check the existing row before this script writes anything. Test that a missing row raises and remains absent. The ticket explicitly places initialization before the check in db/seeds.rb; it does not extend that exception to db/seeds/timesheet_data.rb.","recorded_resolution":"fixed","recorded_correction":"Removed the planned ensure_initial! call from db/seeds/timesheet_data.rb. The script only reads the row, so a missing row raises and stays absent, and a test checks both.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:34:57.711Z","answer_recorded_at":"2026-10-26T09:36:19.026Z","first_independent_review_at":"2026-10-26T17:09:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"7b12263bbe1389bcb7dba6dc820cb03a4dc32b11051af2c9c925278f0d9c8fa6","record_sha256":"b1d6a944137b78aaf516dfddde99b4af927858685e21105c960e47890f639fbd"},
    {"record_id":"author-pr150-build-001","pr":150,"pr_url":"https://github.com/Homie-Improvement/miru/pull/150","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Refusal matrix comment says snapshot raises entry version; it cannot","finding_evidence":"The seeded entry takes the column default, version 1, and the refused correction body also sends version 1. CorrectionSnapshots.raise_stored_versions! only updates entries whose version is lower than the correction's. So the comment is false, and the fenced example's closing `expect(entry.reload.version).to eq(1)` passes whether or not a correction leaked. Only the outbox_snapshots baseline catches a leak. Sending version 2 makes both the comment and the version check real.","recorded_resolution":"fixed","recorded_correction":"The refused correction body now sends version 2, so a correction that got past the auth check would raise the version-1 entry to 2. The comment and the final version check now hold.","correction_target":"code_commit","source_path":"timekeeping/spec/requests/internal/v1/handoff_refusals_spec.rb","source_line":37,"fix_commit":"8902b9ffa0801afa34eac9d4fd994ee56a99471f","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/8902b9ffa0801afa34eac9d4fd994ee56a99471f","finding_recorded_at":"2026-10-26T16:41:34.222Z","answer_recorded_at":"2026-10-26T16:59:58.439Z","first_independent_review_at":"2026-10-26T17:13:57Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"80048b41bdb6e59f758f4720eceebc168dd876bcc723be4aa2e59b40f6bc1058","record_sha256":"43675e9a713c8441fbc53fe2e9acb4638c8bd458a194752215bc5d02975ceb87"},
    {"record_id":"author-pr150-build-002","pr":150,"pr_url":"https://github.com/Homie-Improvement/miru/pull/150","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Whole-store baseline reader still copied per spec (Duplicated Code)","finding_evidence":"The refused-caller matrix and the load-refusal table exist to prove that nothing changed, yet each keeps its own copy of the list of tables a handoff can write and of the same SELECT * ORDER BY 1, 2 reader. The next-entry-id SQL is also written out twice, in the refusal spec and in service_store_spec. Plan plan item 2 puts a 'handoff row-count helper' in spec/support, and that helper does not exist. If a seventh writable table is added to only one list, the other refusal table keeps passing even when a refused call writes that table, which is the regression these tables exist to catch.","recorded_resolution":"fixed","recorded_correction":"A new HandoffStoredState support module holds the one list of stored tables, the whole-row reader and the next-id reader. The refusal matrix, the load-refusal table and service_store_spec all use it now.","correction_target":"code_commit","source_path":"timekeeping/spec/requests/internal/v1/handoff_refusals_spec.rb","source_line":16,"fix_commit":"8902b9ffa0801afa34eac9d4fd994ee56a99471f","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/8902b9ffa0801afa34eac9d4fd994ee56a99471f","finding_recorded_at":"2026-10-26T16:41:34.222Z","answer_recorded_at":"2026-10-26T16:59:58.439Z","first_independent_review_at":"2026-10-26T17:13:57Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"d802583a5fa706898ad01e91092f4f88999dd21274d9fd09d2c90c3838b9fd44","record_sha256":"556bb29db71b40f0135dec581e8112b4d3b910c64c454a8a38982cf06259a670"},
    {"record_id":"author-pr150-build-003","pr":150,"pr_url":"https://github.com/Homie-Improvement/miru/pull/150","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"service_store_spec keeps a second literal checkpoint-row builder","finding_evidence":"HandoffRows says it is the one place that builds checkpoint rows, and plan R5 requires row builders to be shared through spec/support. In service_store_spec, though, the refusal table, load-twice, outbox and both sequence examples still use service_store_entry_row and service_store_tables, a hand-written literal for every table, while the rollback example uses handoff_checkpoint_tables. A column added to Checkpoint::TABLES therefore still has to be added to the literal by hand, and the file holds two builders for one shape.","recorded_resolution":"fixed","recorded_correction":"service_store_spec now builds its rows with the shared handoff_checkpoint_tables builder, with only its own values passed as overrides. A side-by-side check showed the new tables equal the old literal exactly.","correction_target":"code_commit","source_path":"timekeeping/spec/services/timekeeping/handoff/service_store_spec.rb","source_line":28,"fix_commit":"8902b9ffa0801afa34eac9d4fd994ee56a99471f","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/8902b9ffa0801afa34eac9d4fd994ee56a99471f","finding_recorded_at":"2026-10-26T16:41:34.222Z","answer_recorded_at":"2026-10-26T16:59:58.439Z","first_independent_review_at":"2026-10-26T17:13:57Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"dcc8757487ea229aaa5d1a080f03a99005527c6d0ec8fc9a70fba3185c31d0e3","record_sha256":"b5d96a36a8337af2c298aae96660a45c0a24136aef712e818299bea7a31041c9"},
    {"record_id":"author-pr150-plan-001","pr":150,"pr_url":"https://github.com/Homie-Improvement/miru/pull/150","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The admission refusal table omits the existing missing-generation case.","finding_evidence":"The admission unit lists generations \"2\", 0 and -1, but does not include an absent generation. timekeeping/spec/requests/internal/v1/handoff_spec.rb currently sends {} and expects 400 invalid_request while admission stays fenced. The controller reads json_body[\"generation\"], so {} exercises nil rather than any listed value. A future change that defaults an omitted generation could pass the planned table. Add {} to the merged admission table and preserve both the response and fenced-state assertions.","recorded_resolution":"fixed","recorded_correction":"The merged admit table now sends the same bodies the current specs send, including an empty body with no generation, and keeps both the 400 invalid_request answer and the still-fenced check.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T10:28:26.955Z","answer_recorded_at":"2026-10-26T10:29:40.840Z","first_independent_review_at":"2026-10-26T17:13:57Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"325b82e1418401fd35c51103c815fc48127d217f52a02c863f8abb92fd495e0e","record_sha256":"1369eed688eccffc8a9dc4d84cdf767867fb5fb6a4e346703130df909c8b28d2"},
    {"record_id":"author-pr150-plan-002","pr":150,"pr_url":"https://github.com/Homie-Improvement/miru/pull/150","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The duplicate-correction refusal must preserve snapshots that already exist.","finding_evidence":"The corrections unit describes a duplicate-entry request that is refused and writes no snapshot. An empty starting state would let a delete-before-validation defect pass. In timekeeping/spec/requests/internal/v1/handoff_4_spec.rb, the existing example first writes corrections, then sends duplicate entries and checks that correction_rows_for_spec still equals its saved value. Preserve that setup in the merged example: seed existing snapshots, send the duplicate request, and compare their full rows afterward. Otherwise the consolidation can lose the check that a refused request does not delete valid corrections.","recorded_resolution":"fixed","recorded_correction":"The merged duplicate-entry refusal now first writes valid corrections, then sends the duplicate request and checks the full stored rows are unchanged, as the current example does.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T10:28:26.955Z","answer_recorded_at":"2026-10-26T10:29:40.840Z","first_independent_review_at":"2026-10-26T17:13:57Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"dd500234a3f94d5e0b25fb9446d9383c0202c3f2390f94630faa465043db514f","record_sha256":"f716d75b7bd0b21eb5633ef50bfaf9678d8e7dc5414de22caa5b8fa63b831a44"},
    {"record_id":"author-pr150-plan-003","pr":150,"pr_url":"https://github.com/Homie-Improvement/miru/pull/150","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The refused-caller unit checks side effects later than the shared table rule requires.","finding_evidence":"The plan requires each table row to compare its after-state with one seeded baseline. The refused-caller unit instead checks counts, entries_id_seq, outbox rows and admission generation \"after the matrix.\" These instructions allow different implementations. A final comparison can miss an unauthorized change that a later request reverses, such as a fence followed by an admit. Require the state comparison after each route-and-caller request, using the same baseline throughout. Keep separate baselines where the refused fence and refused admit cases require different starting states.","recorded_resolution":"fixed","recorded_correction":"The refused-caller matrix now compares state to one seeded baseline after every route-and-caller request, labelled with the route and caller; the refused fence and admit cases keep their own baselines.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T10:28:26.955Z","answer_recorded_at":"2026-10-26T10:29:40.840Z","first_independent_review_at":"2026-10-26T17:13:57Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"e732871e5e6052327c6c6d641c24a259dae42e0e630cca67c35c85efc42b2d5c","record_sha256":"4df04012b6d717ffa90944f1d8f61024fc2e69430f2676c0e776940734fc420a"},
    {"record_id":"author-pr150-plan-004","pr":150,"pr_url":"https://github.com/Homie-Improvement/miru/pull/150","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The requirement adds all four example types to every surface instead of assigning each to its owner.","finding_evidence":"R3 says, \"Each surface has one round-trip example, one load-twice example, one load-refusal table and one sequence table.\" The ticket asks to consolidate those examples, not add all four to checkpoint, corrections and admission. The existing correction specs exercise write! rather than checkpoint loads, and the corrections unit correctly plans correction-specific cases. Change R3 to assign each example type to its relevant owner per layer. This keeps the completion requirement from demanding new, unrelated tests that the ticket does not ask for.","recorded_resolution":"fixed","recorded_correction":"The requirement now carries the issue's own words, and a design decision assigns each of the four forms to one owner per layer instead of demanding all four on every surface.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T10:28:26.955Z","answer_recorded_at":"2026-10-26T10:29:40.840Z","first_independent_review_at":"2026-10-26T17:13:57Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"5f576f424a62e61565a11c38121d8aad62b4dc962201efe42716dcb0d3cfd84c","record_sha256":"904cefb35545ca83f659e0461986219d595377e0517e4309898f4de07974b4e7"},
    {"record_id":"author-pr150-plan-005","pr":150,"pr_url":"https://github.com/Homie-Improvement/miru/pull/150","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The requirement for every surface to contain all four test forms contradicts the planned owners.","finding_evidence":"R3 says, \"Each surface has one round-trip example, one load-twice example, one load-refusal table and one sequence table.\" The implementation instead assigns checkpoint tests to the checkpoint owner and the sequence table to the admission owner. It does not give corrections all four forms. State which owner needs each form, rather than requiring every form on every surface. This removes an ambiguity that could lead an implementer to recreate the duplication this change removes.","recorded_resolution":"fixed","recorded_correction":"Same fix as the scope finding: the requirement uses the issue's words, and the plan states which owner holds the round trip, load-twice, load-refusal table and sequence table.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T10:28:26.955Z","answer_recorded_at":"2026-10-26T10:29:40.840Z","first_independent_review_at":"2026-10-26T17:13:57Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"6141aa42fc011a7e67707406a6482be6d97f2f6e911784b1bc67188655672bdc","record_sha256":"3b7aaec95ab611a168145f5f3ddbde1275eb366e5d0fd76d1b1b16146aa88d38"},
    {"record_id":"author-pr151-build-001","pr":151,"pr_url":"https://github.com/Homie-Improvement/miru/pull/151","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Generated acceptance specs duplicate the same examples across files","finding_evidence":"The same A6 contract is asserted five times for change_decision, four times in bulk_decision_spec.rb, three times in identity_spec.rb, and the outside-company write-role check four times in user_token_spec.rb. Any change to A6/A7 or to the rule signature means editing every copy, a copy left stale will mislead, and the repeats make it hard to tell which example is the real check.","recorded_resolution":"fixed","recorded_correction":"I deleted change_decision_2_spec.rb and folded the repeated primary-role examples into one block each in the change, bulk and identity specs, adding the unlocked-entry check. The gem suite passes: 98 examples.","correction_target":"code_commit","source_path":"gems/timekeeping_rules/spec/timekeeping_rules/entry_rules/change_decision_2_spec.rb","source_line":5,"fix_commit":"f27d4f5c6e82d591c11b20714bd5b3c12969a135","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/f27d4f5c6e82d591c11b20714bd5b3c12969a135","finding_recorded_at":"2026-10-26T16:41:51.030Z","answer_recorded_at":"2026-10-26T16:52:42.149Z","first_independent_review_at":"2026-10-26T17:08:47Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"ad3b6f078472a28622273636982f746affa8cbce64e5a19779b6c2a78957f28a","record_sha256":"697003fbc2521becd5a5aca8fb2935451dbea4947dc34f4b863304f90fc3d1fd"},
    {"record_id":"author-pr151-build-002","pr":151,"pr_url":"https://github.com/Homie-Improvement/miru/pull/151","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Read-tokens-on client path lost its only tests","finding_evidence":"The change removed the only examples that ran EntryApiClient with TIMEKEEPING_READ_TOKENS_ENABLED=\"true\". If token_purpose (entry_api_client.rb:56) broke and always returned :write, every spec would still pass, and delegated reads would go out on write tokens carrying the raised admin role. The plan's U2 test scenarios name the flag-\"true\" case.","recorded_resolution":"fixed","recorded_correction":"The table example now also runs with the flag set to \"true\" and expects a read purpose. I restored the flag-on tests for reading a colleague and for plain or other-company users. Forcing token_purpose to :write now fails 3 examples.","correction_target":"code_commit","source_path":"spec/services/timekeeping/entry_api_client_spec.rb","source_line":140,"fix_commit":"f27d4f5c6e82d591c11b20714bd5b3c12969a135","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/f27d4f5c6e82d591c11b20714bd5b3c12969a135","finding_recorded_at":"2026-10-26T16:41:51.030Z","answer_recorded_at":"2026-10-26T16:52:42.149Z","first_independent_review_at":"2026-10-26T17:08:47Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"16b2ce98cd5b8a0a94d046d01a67220faa5d1dc6d475a27669cff251066a4e7f","record_sha256":"19464ca499dad4c94b5b23bce7ec6fc916864ed2651f7e0aac85ceb95c388e0f"},
    {"record_id":"author-pr151-build-003","pr":151,"pr_url":"https://github.com/Homie-Improvement/miru/pull/151","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Rollback deploys the incompatible receiver before tokens expire","finding_evidence":"As the doc states it, an operator can roll back the monolith, roll back Timekeeping straight away, and only then wait for tokens to expire. A write token minted just before the monolith rollback is still live for up to 60s and carries role admin with primary_role employee. The older Timekeeping decides A6 on role, so it accepts an update to a locked or reserved entry. Nothing in code gates the raised role, so this sentence is the only rollback guard for the intent's rule that a raised role must not edit locked or reserved time.","recorded_resolution":"fixed","recorded_correction":"The rollback text now gives the order step by step: roll back every monolith process, wait at least the 60-second token lifetime after the last one stops, and only then deploy the older Timekeeping.","correction_target":"code_commit","source_path":"docs/architecture/timekeeping-soa.md","source_line":528,"fix_commit":"f27d4f5c6e82d591c11b20714bd5b3c12969a135","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/f27d4f5c6e82d591c11b20714bd5b3c12969a135","finding_recorded_at":"2026-10-26T16:41:51.030Z","answer_recorded_at":"2026-10-26T16:52:42.149Z","first_independent_review_at":"2026-10-26T17:08:47Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"4f84acaa211645cb55a7ced6456785aace32850f3c561a6f41dcbd935ee77f21","record_sha256":"1fba77cd74d34ea93cc57eb3e3777f45345ba65915698c1beb849e6a2bb768cf"},
    {"record_id":"author-pr151-plan-001","pr":151,"pr_url":"https://github.com/Homie-Improvement/miru/pull/151","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Correct the claim that the new role permits changes away from billed.","finding_evidence":"plan item 6 and the planned pull request disclosure say the raised role also lifts the billed-change restriction. Try updating an entry from billed to unbilled with the planned admin token. In gems/timekeeping_rules/lib/timekeeping_rules/entry_rules.rb, CHANGE_RULES :a2 refuses every billed entry regardless of role. EntryWriter#change_refusal therefore stops the update even though :a7 no longer refuses it. Correct plan item 6 and the disclosure to distinguish the reachable locked-entry change from the billed transition that remains forbidden.","recorded_resolution":"fixed","recorded_correction":"Corrected. Rule A2 refuses every change to a billed entry for every caller, so the billed rule A7 was never reachable; the plan no longer claims the raised role lifts it.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:34:53.519Z","answer_recorded_at":"2026-10-26T09:36:54.891Z","first_independent_review_at":"2026-10-26T17:08:47Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"87b5f9a447d7fc698306cd5dc8427d9c23f6db27f0bb5d1e2edd28bc12492a21","record_sha256":"e4ffd5705d1d7559db7e69f7071829f9154e7755c5bac4eb4c429651bfac53db"},
    {"record_id":"author-pr151-plan-002","pr":151,"pr_url":"https://github.com/Homie-Improvement/miru/pull/151","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Preserve locked-entry restrictions when raising the signed role.","finding_evidence":"plan item 6 accepts more write authority than the ticket permits. An employee-first admin can now update a colleague's reserved, unbilled entry, although the monolith refuses that update. app/models/timesheet_entry.rb:199 checks the acting user's primary role. In contrast, CHANGE_RULES in gems/timekeeping_rules/lib/timekeeping_rules/entry_rules.rb exempts the newly signed admin role from the locked-entry restriction. timekeeping/app/services/timekeeping/entry_writer.rb then saves the change through apply_change; its Entry model has no further validations. The company and colleague bear changes to their reserved time, so plan item 6's claim that no other person bears harm does not justify this expansion. Preserve the existing locked-entry restriction while delivering the requested role claim and colleague targeting. Add a regression test at the write boundary rather than defe","recorded_resolution":"fixed","recorded_correction":"Fixed as for the other locked-entry findings, with the regression test at the entry writer boundary rather than deferred.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:34:53.519Z","answer_recorded_at":"2026-10-26T09:36:54.891Z","first_independent_review_at":"2026-10-26T17:08:47Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f9caf988e50c687a8afeea92c95250a47863f1a4f13b78f0f13980e71b7a2357","record_sha256":"2bec99f7b8733f72de6d4bbe5a47e6ab9d1ec47b9d5c1dc3634f90816ba7c366"},
    {"record_id":"author-pr151-plan-003","pr":151,"pr_url":"https://github.com/Homie-Improvement/miru/pull/151","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Preserve the existing restriction on edits to reserved time entries.","finding_evidence":"plan item 6 wrongly concludes that raising an existing admin's token role cannot harm another person. An employee-first admin can now change a colleague's reserved, unbilled entry, including its duration. In gems/timekeeping_rules/lib/timekeeping_rules/entry_rules.rb, rule :a6 blocks that edit only when the signed role is \"employee\". timekeeping/app/services/timekeeping/entry_writer.rb:142-157 then saves the change; timekeeping/app/models/entry.rb adds no validation. The monolith still refuses this user's locked-entry edit in app/models/timesheet_entry.rb:199-205. These reservations belong to invoices, so the changed authority affects company billing records, not just the caller. Disclosure does not preserve that protection. Keep the existing locked-entry restriction separate from the raised role used for colleague targeting, and test an employee-first admin editing a","recorded_resolution":"fixed","recorded_correction":"Same fix: the locked-entry rule now decides on a separate signed primary_role claim, not the raised role, and a writer test edits a reserved colleague entry with an employee-first admin token.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:34:53.519Z","answer_recorded_at":"2026-10-26T09:36:54.891Z","first_independent_review_at":"2026-10-26T17:08:47Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"ab0be7ca71c127325525a9e244f02adf0c065e1a4dc85d01e9ad01596ec7e609","record_sha256":"ca8455a47e564b7936f66ad35a29a7f5ef4276e91ed9a460322ba0f73317a9b4"},
    {"record_id":"author-pr151-plan-004","pr":151,"pr_url":"https://github.com/Homie-Improvement/miru/pull/151","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Preserve the existing restriction on edits to reserved time.","finding_evidence":"plan item 6 treats holding admin as proof that nobody else bears harm. Consider an employee-first admin updating a colleague's current, unbilled entry while an invoice reserves it. The old write token and app/models/timesheet_entry.rb:199 reject the locked edit. The planned admin token passes CHANGE_RULES :a6 because employee is false. EntryWriter#update_entry then persists the change through apply_change, without another authorization check. This lets the user change reserved company time that the monolith refuses, affecting the company and the colleague, not just the caller. Revise plan item 6 to preserve this restriction while allowing colleague targets, using a separate authorization fact if needed. Test the reserved-entry update through the writer. Disclosure alone does not prevent the new authority.","recorded_resolution":"fixed","recorded_correction":"The plan now signs the user's oldest company role in a separate primary_role claim. Timekeeping's locked-entry rule reads that claim, so an employee-first admin is still refused, as in the monolith.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:34:53.519Z","answer_recorded_at":"2026-10-26T09:36:54.891Z","first_independent_review_at":"2026-10-26T17:08:47Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"cb2d742cc5a03aad6f733aa0863a8a485817df3135d7fd0846b4a7ecd897fb60","record_sha256":"10341bae3f0b00f6d8614b59db18921bab7aa57314230761fce390c9ad35e0a0"},
    {"record_id":"author-pr151-plan-005","pr":151,"pr_url":"https://github.com/Homie-Improvement/miru/pull/151","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The raised role also permits changes to locked and billed time that the monolith rejects.","finding_evidence":"plan item 6 says an employee-first admin gains permission to change locked entries and billed status. It dismisses harm because that user already holds an admin role. That does not establish consent from colleagues or people who rely on those protected records. Allowing colleague time entry does not require removing these protections. Revise this decision to preserve the existing locked-entry and billed-status restrictions while still signing the requested company role, or establish an authoritative policy that permits those changes. Add tests that exercise these writes with an employee-first admin token. A disclosure alone does not protect the affected records.","recorded_resolution":"fixed","recorded_correction":"The plan now keeps the locked and billed protections on the oldest company role through a separate signed claim, and tests both with an employee-first admin token.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:34:53.519Z","answer_recorded_at":"2026-10-26T09:36:54.891Z","first_independent_review_at":"2026-10-26T17:08:47Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"7d45f37f4808cb1bb534359a9a96d19db9159e94e7ed9b0a8775c00ec47c653c","record_sha256":"6bb9c80036ea6c49c33a13385f06017b16798da36f0d4fd7c8978c1e48e2224c"},
    {"record_id":"author-pr151-plan-006","pr":151,"pr_url":"https://github.com/Homie-Improvement/miru/pull/151","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The role-selection rule leaves the order of owner and admin unclear.","finding_evidence":"plan item 1 selects the \"first role named in `PRIVILEGED_ROLES`\" but also says roles are considered \"oldest first\". These orders can select different roles for a user who holds both owner and admin. One implementer could use the contract's role order; another could use `roles.id`. Both would satisfy the listed tests, which cover each privileged role separately. State which order preserves the existing read-token rule, and add a case where the two orders differ. Otherwise the signed and served role can change without a test detecting the departure.","recorded_resolution":"fixed","recorded_correction":"The rule is now stated as the read token's: the oldest owner or admin role by roles.id wins. Tests cover owner-before-admin and admin-before-owner users.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T09:34:53.519Z","answer_recorded_at":"2026-10-26T09:36:54.891Z","first_independent_review_at":"2026-10-26T17:08:47Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"7e0e4392185673a487a960104292e2cf35e851c17efe23de0fc9244e55d9febd","record_sha256":"c63ed39ca4fa9367375ceb06efb85553f5b2a8b251e2f33bfa00e125217a69b5"},
    {"record_id":"author-pr152-build-001","pr":152,"pr_url":"https://github.com/Homie-Improvement/miru/pull/152","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Generated malformed body makes the freeze guard fail","finding_evidence":"The folded spec fails its own new frozen-rows example even when the client is correct, so the suite goes red on this change and the plan's verification (the focused spec passes under random order) cannot hold. JSON.generate builds a new, unfrozen String at runtime. `# frozen_string_literal: true` freezes only the two literal entries, and `.freeze` on the array goes one level deep. So `expect(malformed_answers).to all(be_frozen)` at line 154 rejects the third element on every run, in any example order.","recorded_resolution":"fixed","recorded_correction":"Each malformed answer is now frozen before the list is, per the repository's freeze-rows learning. With the old code the third string was not frozen. All three are frozen now, and the spec passes (89 examples).","correction_target":"code_commit","source_path":"spec/lib/timekeeping/handoff/timekeeping_store_spec.rb","source_line":128,"fix_commit":"6b2bf40f5df6f52ad50587104ec66b3fdea46456","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/6b2bf40f5df6f52ad50587104ec66b3fdea46456","finding_recorded_at":"2026-10-26T16:46:49.222Z","answer_recorded_at":"2026-10-26T17:02:23.091Z","first_independent_review_at":"2026-10-26T17:14:00Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"6516839a065bd73599feb281b766e9b7aa92911c820bb0bad4bf0cb963286011","record_sha256":"076675c9c6765550161ded392c4b73aa934f848d69be949dda22ea329a1550a4"},
    {"record_id":"author-pr152-plan-001","pr":152,"pr_url":"https://github.com/Homie-Improvement/miru/pull/152","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The route table cannot call described_class while the example group is being defined.","finding_evidence":"plan item 2 places the route table directly inside the outer `RSpec.describe` block and uses `described_class::CHECKPOINT_PATH` for its paths. In RSpec 3.13, which Gemfile.lock pins, `described_class` is available inside examples, not while defining their group. Building these rows as specified would stop the file from loading before any example runs. The existing specs use `described_class` inside examples or `let` blocks; the shared-example pattern in spec/requests/rack_attack_spec.rb passes literal paths during group definition. Use fully qualified constants such as `Timekeeping::Handoff::TimekeepingStore::CHECKPOINT_PATH` when building the table. This retains the client's constants as the source of truth without adding deferred lookup.","recorded_resolution":"fixed","recorded_correction":"rspec-core does define described_class on the group class too, but the point is now moot: the table holds literal paths, so it no longer reads described_class or the client constants.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T10:27:06.169Z","answer_recorded_at":"2026-10-26T10:27:58.421Z","first_independent_review_at":"2026-10-26T17:14:00Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"38934211a44588f803dcaad10abf6693d140b2d558675bca645e180f6bdc2a5a","record_sha256":"43e22fe1654f0f7963d24c63823831cb372b76147068135cff962c8d8e8672fb"},
    {"record_id":"author-pr152-plan-002","pr":152,"pr_url":"https://github.com/Homie-Improvement/miru/pull/152","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The tables are row-frozen, not deeply frozen.","finding_evidence":"The table-freezing decision says the tables are “deeply frozen,” but `.map(&:freeze).freeze` freezes only each row and the outer list. The route rows also contain request and response bodies, which can contain mutable hashes and arrays. The implementation unit checks only the rows and outer list, so it does not establish the stated protection against mutation between randomly ordered tests. Describe the guarantee as freezing the rows and outer list. If nested values must also remain immutable, specify how to freeze and verify them.","recorded_resolution":"fixed","recorded_correction":"The plan now says rows and the list are frozen, not deeply frozen. Row bodies are built by procs that return fresh objects per example, so no example can change a shared body.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T10:27:06.169Z","answer_recorded_at":"2026-10-26T10:27:58.421Z","first_independent_review_at":"2026-10-26T17:14:00Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"958d16747d3eedd6c9edc5a3bee5ac1597d48f4747440efa28732a6ce9dfede9","record_sha256":"0f5c5a54409dd3fe249e46c8769e13ce5978efcdda44dda314d065a8ea02fe54"},
    {"record_id":"author-pr152-plan-003","pr":152,"pr_url":"https://github.com/Homie-Improvement/miru/pull/152","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Using client path constants as expected URLs removes an existing route-contract check.","finding_evidence":"plan item 2 makes the stubs derive their expected paths from the same constants that send each request. If FENCE_PATH changes to \"/internal/v1/handoff/admission/fnce\", both the request and stub follow it, so the folded examples still pass. The service rejects that path: timekeeping/config/routes.rb:28 declares \"admission/fence\". The current timekeeping_store_2_spec.rb and timekeeping_store_auth_spec.rb use literal URLs and would catch this change. Keep the expected endpoint paths independently in the single route table, or retain explicit assertions that the client constants match those paths. This preserves existing coverage without repeating paths across examples.","recorded_resolution":"fixed","recorded_correction":"Agreed. The route table now holds each endpoint path as a literal string, checked against the service routes, so a typo in a client constant fails the spec as the old files did.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T10:27:06.169Z","answer_recorded_at":"2026-10-26T10:27:58.421Z","first_independent_review_at":"2026-10-26T17:14:00Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"975f566981c311b1ca96b536ff546eb6da37d4b7802c16d6c0cb5e9e087f8cb7","record_sha256":"b33945dad483bcd559449ab0abac34adcdf5bbcd0ebe56ac8c6f5e51d4572b06"},
    {"record_id":"author-pr153-build-001","pr":153,"pr_url":"https://github.com/Homie-Improvement/miru/pull/153","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Handoff request specs lost their non-empty baseline guards","finding_evidence":"Without these guards, an empty fixture would let `match_array([])` and `eq([])` pass, and the 503 handoff examples would stop proving that a refused handoff keeps the child rows. Neither the issue nor the plan asks for this removal. It breaks the learning 'assert the baseline before proving a refused command changed nothing' and the plan's completion criteria (existing endpoint expectations unchanged).","recorded_resolution":"fixed","recorded_correction":"I restored the baseline guards in both handoff examples: three line items and three taxes, one own-invoice line item and a positive foreign count. Both request and service specs pass.","correction_target":"code_commit","source_path":"spec/requests/api/v1/invoices/bulk_deletion_spec.rb","source_line":169,"fix_commit":"fd5baa65003b6227017f92bb73f3c3d7d7fae6b7","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/fd5baa65003b6227017f92bb73f3c3d7d7fae6b7","finding_recorded_at":"2026-10-26T16:55:47.019Z","answer_recorded_at":"2026-10-26T17:11:26.963Z","first_independent_review_at":"2026-10-26T17:24:23Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"aab6c5b33b53a1f79f0d5688a08d23e034b23f7c7566769056615ba582867d36","record_sha256":"8042adbf7c6bc2ea314a2fc69e9dca72d42168ec536e3b4ba8d1da7b03ef3c12"},
    {"record_id":"author-pr153-build-002","pr":153,"pr_url":"https://github.com/Homie-Improvement/miru/pull/153","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Learning still says request spec holds the mixed-batch baseline","finding_evidence":"Line 65 still says, in the present tense, that the request spec's mixed-batch baseline compares discount. This change deletes that example, so the sentence is now false. The paragraph appended at line 68 names the service spec instead, so the learning gives two different locations for the guard. Plan plan item 5 named lines 63-65 for this update.","recorded_resolution":"fixed","recorded_correction":"The learning now says the duplicate was folded into the request spec, then moved with its discount comparison to the service spec. The current location is stated only once.","correction_target":"code_commit","source_path":"docs/solutions/conventions/give-each-destroy-a-savepoint-inside-a-shared-transaction.md","source_line":65,"fix_commit":"fd5baa65003b6227017f92bb73f3c3d7d7fae6b7","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/fd5baa65003b6227017f92bb73f3c3d7d7fae6b7","finding_recorded_at":"2026-10-26T16:55:47.019Z","answer_recorded_at":"2026-10-26T17:11:26.963Z","first_independent_review_at":"2026-10-26T17:24:23Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"8b9092a4ccf8009912e05c48fec93dde35df413da1cf04911e60ec975d8f61e5","record_sha256":"d44a85d2adbd88124264c08fbb4178e6ab844bbaa9ceacb8447b5f4ebac65600"},
    {"record_id":"author-pr153-build-003","pr":153,"pr_url":"https://github.com/Homie-Improvement/miru/pull/153","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"New 422/204 contract example duplicates existing admin examples","finding_evidence":"The 204 and 422 responses are now pinned twice. The new block copies the admin setup again and opens with `expect(Invoices::BulkDeletionService).to be_a(Class)`, which tests no behaviour. Any future response change has to be edited in both places. The block's only new checks (exact error message, empty 204 body) belong in the existing examples that plan item 4 kept to pin the HTTP contract.","recorded_resolution":"fixed","recorded_correction":"I removed the separate response-contract block and its no-op class check. Its exact 422 body check and empty 204 body check now sit in the existing admin examples. All 14 examples pass.","correction_target":"code_commit","source_path":"spec/requests/api/v1/invoices/bulk_deletion_spec.rb","source_line":114,"fix_commit":"fd5baa65003b6227017f92bb73f3c3d7d7fae6b7","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/fd5baa65003b6227017f92bb73f3c3d7d7fae6b7","finding_recorded_at":"2026-10-26T16:55:47.019Z","answer_recorded_at":"2026-10-26T17:11:26.963Z","first_independent_review_at":"2026-10-26T17:24:23Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"615d60397cad27abd62d066554ceac6f094e3e3a89f7157bc594c7d292b21784","record_sha256":"57c7464404a2967102ad244e5bcc030e807a1496ee9d15ad1e7e3d19b963c54a"},
    {"record_id":"author-pr154-build-001","pr":154,"pr_url":"https://github.com/Homie-Improvement/miru/pull/154","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Correction value request spec repeats the source-agent refusal four times","finding_evidence":"The new file has eight describe blocks, and three share the name 'Timekeeping handoff correction snapshot value refusals'. Four examples send a source 'agent' correction and assert the same 400 with no outbox row, and two near-identical examples write one valid decimal-duration correction. Each block copies the same token let and around hook. A later change to the refusal rule or the route means editing every copy, the repeated names make failure output ambiguous, and no copy covers a risk the others miss.","recorded_resolution":"fixed","recorded_correction":"The request value spec is now one describe block with one token and one setup. It keeps four tests: the agent refusal then valid retry, non-text sources, the 97-level metadata refusal, and one valid decimal write sent twice.","correction_target":"code_commit","source_path":"timekeeping/spec/requests/internal/v1/handoff_correction_values_spec.rb","source_line":19,"fix_commit":"3e3ebc503382aba1ef221124550b97b05f0df194","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/3e3ebc503382aba1ef221124550b97b05f0df194","finding_recorded_at":"2026-10-26T17:06:41.789Z","answer_recorded_at":"2026-10-26T17:45:46.370Z","first_independent_review_at":"2026-10-26T18:02:20Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"1f5f0d2e6b51c0e1053681403eabbc70ec0f06ce0fe26bd8b36e2a3482e993c4","record_sha256":"b356d522d4a0ad550b3004458c9a86a1db5f25c9a0cefc502c35318faab917ec"},
    {"record_id":"author-pr154-build-002","pr":154,"pr_url":"https://github.com/Homie-Improvement/miru/pull/154","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Refusals spec rewrite drops the sequence and fenced-admit checks","finding_evidence":"The rewritten 401 suite would still pass if a refused caller advanced the entry-id sequence, wrote a correction, or admitted writes into a fenced store before answering 401. refusal_state compares only row counts and the admission generation. Advancing the sequence (table_store.rb:100-106) changes neither, and the base check that a later entry's id stays below 9_000_000_000 is gone. The correction case now runs while admitted, so FenceCheck.require_fenced! would refuse the write even if auth were bypassed. The admit-from-fenced example and the admin-role caller were dropped. The author called the edit lint-only and said the original could not be recovered, but the base file is at 17de15e, and the rewrite falls outside this change's intent.","recorded_resolution":"fixed","recorded_correction":"handoff_refusals_spec.rb is now restored word for word from the base branch. That brings back the sequence id check, the fenced correction, admit-from-fenced and the admin caller, and the full timekeeping suite passes.","correction_target":"code_commit","source_path":"timekeeping/spec/requests/internal/v1/handoff_refusals_spec.rb","source_line":46,"fix_commit":"3e3ebc503382aba1ef221124550b97b05f0df194","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/3e3ebc503382aba1ef221124550b97b05f0df194","finding_recorded_at":"2026-10-26T17:06:41.789Z","answer_recorded_at":"2026-10-26T17:45:46.370Z","first_independent_review_at":"2026-10-26T18:02:20Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"c795dacb50d380e861828985f52b397a6f1ed370fc38895dfc1a4dbe69fe8de6","record_sha256":"e91064e79d5bc5f93d750bef77260bb45b49d1fef618fc6833091b650d5b92b8"},
    {"record_id":"author-pr154-plan-001","pr":154,"pr_url":"https://github.com/Homie-Improvement/miru/pull/154","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A deeply nested metadata object can still stop the relay after passing validation.","finding_evidence":"plan item 1 and plan item 4 permit Hash metadata without bounding its depth. An in-process correction can supply source_metadata containing 110 nested objects while every other field is valid. LosslessSql.json_text recursively serializes this object without a depth limit, so the correction commits. Later, exact_json in timekeeping/app/services/timekeeping/relay_outbox_source.rb calls JSON.parse with its default nesting limit and raises before offering the row for delivery. Every later snapshot remains blocked. Enforce a delivery-safe metadata depth before the correction transaction, and test both the largest accepted depth and a refused deeper value through the relay read. The monolith parser also has a separate limit of 103 in app/controllers/internal/v1/timekeeping/entry_snapshots_controller.rb; the accepted bound must fit the whole delivery path.","recorded_resolution":"fixed","recorded_correction":"Metadata rules now refuse objects nested deeper than 96 levels, the depth the entry API already allows, and tests deliver the deepest accepted value through the relay read.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T10:28:48.158Z","answer_recorded_at":"2026-10-26T10:29:52.804Z","first_independent_review_at":"2026-10-26T18:02:20Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"23dda45e031ef2e9b11b93dea68615a6e0f34d079211297335a26bebe31cd24a","record_sha256":"d6020daa1cddc14156f829d1f9dfe90d01c3da26fdb327f8c4f8fb208ee2570a"},
    {"record_id":"author-pr154-plan-002","pr":154,"pr_url":"https://github.com/Homie-Improvement/miru/pull/154","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Keep parsed project identifiers out of the checks on raw entry fields.","finding_evidence":"plan item 2 slices the shared rules by writable fields, but WRITABLE_FIELDS includes project_id. The new project_id rule requires a positive Integer. In timekeeping/app/services/timekeeping/entry_command.rb:131-139, parse_project_id also accepts a decimal string such as \"11\". It returns the Integer without changing body. Then check_fields! validates the raw body at lines 141-151. The planned slice would therefore reject previously valid create, update and bulk_move requests with project_id: \"11\". Keep project_id under its existing parser rather than adding it to FIELD_SHAPES, or validate the parsed value without changing the request fingerprint. Add a regression test for a string project identifier.","recorded_resolution":"fixed","recorded_correction":"Same fix: the shared rules are sliced by the field list the entry API already checks, which leaves out project_id, and a test sends project_id \"11\" on create, update and bulk move.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T10:28:48.158Z","answer_recorded_at":"2026-10-26T10:29:52.804Z","first_independent_review_at":"2026-10-26T18:02:20Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"efcb42131454e0de60d82637f84abccbfb6e7af20cb306fb2af5fc61eb388a74","record_sha256":"2a39fdf23b69f6ea11dc02a47c63cfd5653c4e270898ff2fb13682153440e197"},
    {"record_id":"author-pr154-plan-003","pr":154,"pr_url":"https://github.com/Homie-Improvement/miru/pull/154","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Keep the entry API's existing support for string project identifiers.","finding_evidence":"plan item 2 adds project_id to EntryCommand::FIELD_SHAPES by slicing the shared rules with WRITABLE_FIELDS. That changes the entry API, not just correction validation. In timekeeping/app/services/timekeeping/entry_command.rb, parse_project_id accepts \"11\" and returns Integer 11, but check_fields! then reads the unchanged request body. The shared snapshot rule requires a positive Integer, so a previously accepted create, update or bulk move with project_id: \"11\" now raises ActionController::BadRequest. Preserve the existing field selection when sharing predicates, leaving project_id validation with parse_project_id. Add a regression test for a string project identifier so correction validation does not restrict existing entry callers.","recorded_resolution":"fixed","recorded_correction":"The entry API now shares rules only for the nine fields it already checks; project_id stays with its existing parser, and a regression test keeps project_id \"11\" accepted.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T10:28:48.158Z","answer_recorded_at":"2026-10-26T10:29:52.804Z","first_independent_review_at":"2026-10-26T18:02:20Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"e301bbbed1f8c4c0c1d36a5b7b84ac897ed7c1d9dff0a3140ef8b928c29b7ae4","record_sha256":"3fdd671843b27da9824989b57ef49db306dae12f471fcaae5aaa2bcd1e8a0169"},
    {"record_id":"author-pr154-plan-004","pr":154,"pr_url":"https://github.com/Homie-Improvement/miru/pull/154","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Preserve the distinction between request fields and stored snapshot fields.","finding_evidence":"plan item 2 slices the snapshot rules by EntryCommand's writable fields, which include project_id. That adds the snapshot's positive-Integer rule to raw request validation. In timekeeping/app/services/timekeeping/entry_command.rb, parse_project_id accepts \"11\" and returns 11, but build leaves body unchanged before calling check_fields!. The proposed rule therefore rejects a previously accepted request with project_id: \"11\". This affects create, update and bulk_move. Share predicates for the fields FIELD_SHAPES already checks, rather than every writable field. Keep parse_project_id responsible for request identifiers, and add a regression test for string project identifiers.","recorded_resolution":"fixed","recorded_correction":"Request fields and snapshot fields stay distinct: the entry API reuses only the shared rules for fields it already checks, and parse_project_id keeps owning request ids.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T10:28:48.158Z","answer_recorded_at":"2026-10-26T10:29:52.804Z","first_independent_review_at":"2026-10-26T18:02:20Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"63509f66f061e5fb1a58282a7be64e9619589e52314a3a6705219807ff22e8e9","record_sha256":"4ccf5a2807ff57f8c05c1ce9f246dfaa1befdde550076a406c0229b354d1f9bb"},
    {"record_id":"author-pr154-plan-005","pr":154,"pr_url":"https://github.com/Homie-Improvement/miru/pull/154","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Sharing the project_id predicate would reject entry requests that currently succeed.","finding_evidence":"plan item 2 slices the shared rules by WRITABLE_FIELDS, which includes project_id. Consider a create body with project_id \"11\", a valid work_date and duration. In timekeeping/app/services/timekeeping/entry_command.rb, parse_project_id accepts \"11\" and returns Integer 11. However, check_fields! then checks the original body. The new positive-Integer predicate would reject \"11\", although entry_fields already replaces it with the parsed Integer before writing. This also affects updates and bulk moves. Preserve the existing separation: leave project_id validation with parse_project_id, and share predicates only for fields currently checked by FIELD_SHAPES. Add regression tests for decimal-string project identifiers.","recorded_resolution":"fixed","recorded_correction":"Fixed as suggested: project_id validation stays with parse_project_id, only the nine fields checked today share rules, and decimal-string project ids get regression tests.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T10:28:48.158Z","answer_recorded_at":"2026-10-26T10:29:52.804Z","first_independent_review_at":"2026-10-26T18:02:20Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9795017971727af9f60658770b0fb73758efb0ffd0d8e1c60a3fca9d8ce7cc4b","record_sha256":"19b063440044fe5121a07414ab7bfbd73290444b68b222cee916a37979760d2f"},
    {"record_id":"author-pr156-build-001","pr":156,"pr_url":"https://github.com/Homie-Improvement/miru/pull/156","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Inner-example runner and count helper copied three times across two spec files","finding_evidence":"The routine that runs an inner example through the hooks (about 20 lines, with hand-reset RSpec.current_example and current_scope) exists three times and differs only in metadata and label, so a fix to one copy will likely miss the others. ledger_cleanup_count_elsewhere is a word-for-word copy of committed_data_count_elsewhere.","recorded_resolution":"fixed","recorded_correction":"There is now one inner-example runner in spec/support. Both spec files call it, and its duplicate copies and the copied count helper are deleted. Rubocop is clean and both files pass.","correction_target":"code_commit","source_path":"spec/db/committed_data_mode_spec.rb","source_line":273,"fix_commit":"623a48f69adffb5cc8fb162b7388c16a2ad0e9d7","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/623a48f69adffb5cc8fb162b7388c16a2ad0e9d7","finding_recorded_at":"2026-10-26T17:37:56.739Z","answer_recorded_at":"2026-10-26T18:00:39.683Z","first_independent_review_at":"2026-10-26T18:15:23Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"1c926adc078116eaeb79cb32344cc2f3d8984067328a612e9e01e9429b284939","record_sha256":"0260713ee3d32e191997300fb038536eb0ab07b31bb75fd3136a6d993defc199"},
    {"record_id":"author-pr156-build-002","pr":156,"pr_url":"https://github.com/Homie-Improvement/miru/pull/156","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Lock-wait helper specs rewritten unasked, dropping the bystander, nil-contender, not-yet-sent and pid-ordering checks","finding_evidence":"Neither the issue nor the plan asks to change this group. The rewrite removes tests for behaviour that the unchanged helpers in spec/support/committed_data.rb promise, so these regressions would now pass: blocked_by? ignoring the named holder; blocked_by? returning true for a nil contender; blocked_by? reporting a thread that has not sent its query as blocked, or polling past its deadline; committed_data_thread pushing its pid after the block runs. billing_fence_concurrency_spec.rb and projection_consumer_spec.rb rely on blocked_by? with specific pids to prove production lock ordering.","recorded_resolution":"fixed","recorded_correction":"The lock-wait helper group is back exactly as it is on the base branch; a diff against the base file shows no difference. The advisory-lock rewrite is gone, and the spec file passes.","correction_target":"code_commit","source_path":"spec/db/committed_data_mode_spec.rb","source_line":170,"fix_commit":"623a48f69adffb5cc8fb162b7388c16a2ad0e9d7","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/623a48f69adffb5cc8fb162b7388c16a2ad0e9d7","finding_recorded_at":"2026-10-26T17:37:56.739Z","answer_recorded_at":"2026-10-26T18:00:39.683Z","first_independent_review_at":"2026-10-26T18:15:23Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"a81622501d9dcd90bc9043e93db11c57f990522d97e37ee5d0faa5c70b1b56ec","record_sha256":"769347c7456b7300f6d38e7ecd09622a571d8de9260a8caf7eb772bf9770089b"},
    {"record_id":"author-pr156-build-003","pr":156,"pr_url":"https://github.com/Homie-Improvement/miru/pull/156","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"New ledger-cleanup groups repeat existing checks and add a name-absence test that proves nothing","finding_evidence":"The third group repeats the line-103 example, and the notification test at lines 135-138 already asserts the delete order and the pooled connection. The name-absence example passes even when a second connection is built under another name or inline, so it gives false confidence.","recorded_resolution":"fixed","recorded_correction":"Removed the name-absence example. The after-hook statement test now sits in the first committed-data group and uses its existing count helper, and the third group is deleted. Both spec files pass.","correction_target":"code_commit","source_path":"spec/db/committed_data_mode_spec.rb","source_line":264,"fix_commit":"623a48f69adffb5cc8fb162b7388c16a2ad0e9d7","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/623a48f69adffb5cc8fb162b7388c16a2ad0e9d7","finding_recorded_at":"2026-10-26T17:37:56.739Z","answer_recorded_at":"2026-10-26T18:00:39.683Z","first_independent_review_at":"2026-10-26T18:15:23Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"743b9dffa3bce74e1069f76188ce395de322866fb1e2229ad8e4000fd9793c33","record_sha256":"f3e1d541e1066d7071911e17cf13a01d195bc8b1cbea7a0f3f2a65fb6e198615"},
    {"record_id":"author-pr156-plan-001","pr":156,"pr_url":"https://github.com/Homie-Improvement/miru/pull/156","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The notification fallback cannot satisfy the required negative-control test.","finding_evidence":"The open question permits replacing the connection stub with a `sql.active_record` notification check. plan item 3 explicitly says that check misses connection access without SQL. However, plan item 4 and the completion criteria require the example to fail when a temporary edit calls `ActiveRecord::Base.connection`. Under the fallback, that edit can pass without emitting SQL. This leaves conflicting instructions about whether the weaker test is acceptable and could hide a future database dependency. Remove the fallback, or specify another scoped connection check that still detects the required temporary break.","recorded_resolution":"fixed","recorded_correction":"The SQL-notification fallback is removed. The preflight example keeps its connection stub inside a temporary mock scope; if that scope cannot work, the plan stops instead of using a weaker check.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T17:02:11.978Z","answer_recorded_at":"2026-10-26T17:02:53.481Z","first_independent_review_at":"2026-10-26T18:15:23Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d565044b4d41ee60e21dc0893fb2cb61efa61bfd39f608d2f76466aa108ea502","record_sha256":"ef7ea4eb7b12bbd115c1369bb1a85e73250545bcb02c3f316e06109f289310c6"},
    {"record_id":"author-pr156-plan-002","pr":156,"pr_url":"https://github.com/Homie-Improvement/miru/pull/156","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"low","finding_title":"The plan goes beyond the issue: Revise the learning document's description of the private adapter while retaining its warning about connection stubs.","finding_evidence":"This is the documentation maintenance described by the separate learning-update work, rather than something the issue requests. Its narrow size and direct relevance make it a low-severity addition.\n\nThe issue asks for nothing like it.\n\nAnswer by removing it from the plan or deferring it to follow-up work the plan names as out of scope; or decline with your reason.","recorded_resolution":"fixed","recorded_correction":"Same removal: the plan no longer edits the learning document. Its unit and decision are retired, and the plan names the note as outside this change.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T17:03:15.639Z","answer_recorded_at":"2026-10-26T17:03:50.445Z","first_independent_review_at":"2026-10-26T18:15:23Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d2c8930174d9ac48ac4aabf299c31dd3b59b09ad3fe46fafde18e8a2b0a3f4e4","record_sha256":"97a7520146eea43a70ec7e4dbe319408549f8135e1b54d7fd280360d1f8389d9"},
    {"record_id":"author-pr156-plan-003","pr":156,"pr_url":"https://github.com/Homie-Improvement/miru/pull/156","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"low","finding_title":"The plan goes beyond the issue: Rewrite the captured cleanup learning to describe the pooled connection and narrowed stubs.","finding_evidence":"Updating this separate learning document is useful maintenance, but the issue asks for changes to the cleanup hook and two specs, not a rewrite of the captured learning.\n\nThe issue asks for nothing like it.\n\nAnswer by removing it from the plan or deferring it to follow-up work the plan names as out of scope; or decline with your reason.","recorded_resolution":"fixed","recorded_correction":"The rewrite of the captured cleanup learning is removed from the plan. The issue asks only for the hook and the two specs; the later documentation refresh can update that note.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T17:03:15.639Z","answer_recorded_at":"2026-10-26T17:03:50.445Z","first_independent_review_at":"2026-10-26T18:15:23Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"08d2478a0462659a4a93a4c903170ba0ae0cb7e57f3e939ba91d3d089476097a","record_sha256":"f50a2cb8a8ae439412188213772364b75b304e9da2326d3333006c886903b8a7"},
    {"record_id":"author-pr160-build-001","pr":160,"pr_url":"https://github.com/Homie-Improvement/miru/pull/160","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Generated specs repeat the same contracts across four spec files","finding_evidence":"The same scenarios are written two to four times, each with copied setup and helpers, across outbox_delivery_spec, deliver_invoice_operation_job_spec, operation_coordinator_spec, recover_invoice_operation_intents_job_spec and operation_lock_spec. When the save order changes every copy must be edited alike, and a missed copy fails for no reason or keeps checking old behaviour; the four stale-signal copies even read the event payload four different ways. One cross-company copy (:195) gives the foreign intent a different invoice_id as well, so the invoice filter alone refuses it and the example would still pass with the company scope deleted (docs/solutions/conventions/arrange-foreign-tenant-fixtures-that-differ-only-in-tenant.md).","recorded_resolution":"fixed","recorded_correction":"I folded the duplicate outbox and lock examples. That includes removing the cross-company example whose foreign intent also had a different invoice id. The coordinator and recovery examples stay, since each checks something the others do not.","correction_target":"code_commit","source_path":"spec/services/invoices/outbox_delivery_spec.rb","source_line":10,"fix_commit":"4ef51452cedad4040fc63ad6e6d08ad5776f6a47","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/4ef51452cedad4040fc63ad6e6d08ad5776f6a47","finding_recorded_at":"2026-10-26T18:15:19.434Z","answer_recorded_at":"2026-10-26T18:52:08.632Z","first_independent_review_at":"2026-10-26T19:10:13Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"73ca818027758b3414bd91b7f9dba01d8cbd68c8e1cc93aa5718664695980962","record_sha256":"49231447028309a9d095a1d4bf432e95bc4f0f9fc4bf23cba3eab36e98b4f762"},
    {"record_id":"author-pr160-build-002","pr":160,"pr_url":"https://github.com/Homie-Improvement/miru/pull/160","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Intent stores re-read generation, not the prepared request's","finding_evidence":"Prepare is sent with request.generation, but the intent and both outbox rows store TimekeepingOwnership.current.generation, read again inside the coordinator, and OutboxDelivery rebuilds commit/abort from intent.generation. If ownership flips between building the request and the call, the commit or abort the store receives carries a different generation than the prepare it settles; a store that checks generations would refuse it and the row would retry every minute forever. It also breaks the ReservationRequest contract ('The intent row stores this request so a retry resends it unchanged') and section 9.2 ('a retry or recovery resends the stored request and never recomputes it'). The request should be the one source of the generation.","recorded_resolution":"fixed","recorded_correction":"The issue says the intent stores \"the generation from the single global `TimekeepingOwnership` row\". So the coordinator now refuses a request built for another generation with the existing OwnershipMoving error, before writing an intent.","correction_target":"code_commit","source_path":"app/services/invoices/operation_coordinator.rb","source_line":65,"fix_commit":"4ef51452cedad4040fc63ad6e6d08ad5776f6a47","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/4ef51452cedad4040fc63ad6e6d08ad5776f6a47","finding_recorded_at":"2026-10-26T18:15:19.434Z","answer_recorded_at":"2026-10-26T18:52:08.632Z","first_independent_review_at":"2026-10-26T19:10:13Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"36c230f53ac2239907cfe308d9de2a580b9837320607e6fc92e099e4c7970909","record_sha256":"c2b7f9f6c509a2a889f92fcc527a64963e76167d29b2f624e7ef66508109ada1"},
    {"record_id":"author-pr160-build-003","pr":160,"pr_url":"https://github.com/Homie-Improvement/miru/pull/160","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"One stuck save's lock stalls the whole recovery sweep","finding_evidence":"If a save hangs while it holds its (company_id, invoice_id) advisory lock, for example in a prepare call with no timeout, the sweep waits on that lock forever (pg_advisory_xact_lock, and no lock_timeout is configured). Intents are resolved one at a time and outbox rows are delivered only afterwards, so no other company's crashed intent is failed and no undelivered row is delivered until that save ends. The recurring task runs every minute on a single-thread invoice_operations worker, so later sweeps queue behind the blocked one. The comment above resolve promises the opposite ('one company's live save ... never holds up the others'). A bounded wait keeps the required behaviour: the sweep still never fails a live save's intent.","recorded_resolution":"fixed","recorded_correction":"Each recovery transaction now sets a 5-second lock_timeout before it takes the advisory lock. A stuck save leaves only its own intent pending, and other companies' intents and undelivered rows still settle.","correction_target":"code_commit","source_path":"app/jobs/recover_invoice_operation_intents_job.rb","source_line":51,"fix_commit":"4ef51452cedad4040fc63ad6e6d08ad5776f6a47","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/4ef51452cedad4040fc63ad6e6d08ad5776f6a47","finding_recorded_at":"2026-10-26T18:15:19.434Z","answer_recorded_at":"2026-10-26T18:52:08.632Z","first_independent_review_at":"2026-10-26T19:10:13Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"c9546534de2ed7d730f82ddd8806f541d0a66eb5025a5756440afe2b042ddabc","record_sha256":"7fae2081e26c59120e1ca168f2a4bf791a53184149e4280b0183612844ba73a2"},
    {"record_id":"author-pr160-build-004","pr":160,"pr_url":"https://github.com/Homie-Improvement/miru/pull/160","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Savepoint rollback delivers abort before failure is durable","finding_evidence":"When the coordinator is called inside a requires_new savepoint, connection.current_transaction is the savepoint, and the after_rollback hook registered on it may run when the savepoint rolls back while the outer transaction is still open. record_failure's ApplicationRecord.transaction would then join the outer transaction and deliver the abort at once, before the failed intent and abort row are committed. A later outer rollback would erase both, leaving the separately committed intent pending even though abort was sent. Section 9.2 and 9.3 of docs/architecture/timekeeping-soa.md require the failure to be recorded before abort. Plan plan item 5 assumes these hooks fire only on the outermost real transaction. The repository already nests invoice savepoints inside the billing transaction (bulk deletion), which section 9.2 names as a caller. Recovery would eventually see the intent and send a second abort, so the store is not left holding entries, but the 'failure recorded first' order is broken.","recorded_resolution":"fixed","recorded_correction":"Confirmed: Rails 8.1.3.1 runs after_rollback when a savepoint rolls back. The failure write now waits until no transaction is open. A new nested-savepoint spec passes, and it fails without the fix.","correction_target":"code_commit","source_path":"app/services/invoices/operation_coordinator.rb","source_line":102,"fix_commit":"4ef51452cedad4040fc63ad6e6d08ad5776f6a47","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/4ef51452cedad4040fc63ad6e6d08ad5776f6a47","finding_recorded_at":"2026-10-26T18:15:19.434Z","answer_recorded_at":"2026-10-26T18:52:08.632Z","first_independent_review_at":"2026-10-26T19:10:13Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"c2aff1635bbd4ffc1f8f09621867bdef8558693b519e9de36231f91c227ac19a","record_sha256":"c1f865d5b5ebfc761e68901a5c94236419c61f2d82b61808c0334f7cb1c3fb03"},
    {"record_id":"author-pr160-plan-001","pr":160,"pr_url":"https://github.com/Homie-Improvement/miru/pull/160","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Adapter selection fails before delivery can record the failed attempt.","finding_evidence":"plan item 9 resolves the default adapter in DeliverInvoiceOperationJob before constructing OutboxDelivery. plan item 7 and the assumptions make every default adapter lookup raise AdapterNotAvailable. A scheduled delivery without the injected test adapter therefore exits before the service can lock the row and increment attempts. The rescue around the adapter call in plan item 10 cannot catch an earlier lookup failure. app/jobs/application_job.rb has no handler for this new error. Move adapter resolution into the delivery operation's protected attempt boundary, or explicitly record lookup failures there. Prove that the non-injected job leaves delivered_at unset and increments attempts, as the delivery unit requires.","recorded_resolution":"fixed","recorded_correction":"Adapter lookup now happens inside the locked attempt step, and the job spec without an injected adapter checks that delivered_at stays unset and attempts goes up by one.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T17:05:25.132Z","answer_recorded_at":"2026-10-26T17:06:51.376Z","first_independent_review_at":"2026-10-26T19:10:13Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"6c785dd1205e1af8c76437e7046a13c7861678390cdfae1745fc3007e6e1453b","record_sha256":"a59a0da15a573b47798b4a4bb792087f1de2a33e5f16f11bd1405b5f3bd87b5d"},
    {"record_id":"author-pr160-plan-002","pr":160,"pr_url":"https://github.com/Homie-Improvement/miru/pull/160","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Adapter selection raises outside the service that must count the failed attempt.","finding_evidence":"plan item 9 makes the job select its default adapter before constructing OutboxDelivery. The assumptions say that selection raises AdapterNotAvailable. plan item 10 puts exception handling and attempt counting inside OutboxDelivery, which therefore never receives this failure. This contradicts the U2 test requiring a job without reservations to increment attempts. Establish one error boundary that includes adapter selection and records its failure against the outbox row. Keep the job and service contracts consistent with that boundary.","recorded_resolution":"fixed","recorded_correction":"Delivery now has one error boundary that covers adapter selection and the adapter call, and records either failure against the outbox row.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T17:05:25.132Z","answer_recorded_at":"2026-10-26T17:06:51.376Z","first_independent_review_at":"2026-10-26T19:10:13Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"69a315fc535f21ae0bfbb9f2a2f3e9c6aa123c8152fabe3adca8af530a641704","record_sha256":"83bffef585049588f6aaef8c675186cf76d089f878550cf46fc77c087f961025"},
    {"record_id":"author-pr160-plan-003","pr":160,"pr_url":"https://github.com/Homie-Improvement/miru/pull/160","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Default adapter selection raises before delivery can count the attempt.","finding_evidence":"plan item 9 resolves ReservationAdapters.for(TimekeepingOwnership.current) in the job before constructing OutboxDelivery. R7 and the Assumptions section require that lookup to raise AdapterNotAvailable for every current owner. The attempt counter and exception handling belong to OutboxDelivery under plan item 10, so they cannot catch this earlier failure. ApplicationJob has no handler for this error in app/jobs/application_job.rb. Consequently, the default job raises instead of preserving the row with an incremented attempt, and the required uninjected job test cannot pass as described. Move default adapter selection inside the delivery failure boundary, after loading the row, so lookup failures follow the same counted retry path as adapter failures.","recorded_resolution":"fixed","recorded_correction":"Adapter selection now runs inside the delivery step, after the row is locked, so an 'adapter not available' error is counted as an attempt like any other adapter failure.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T17:05:25.132Z","answer_recorded_at":"2026-10-26T17:06:51.376Z","first_independent_review_at":"2026-10-26T19:10:13Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"4fbd837d87d7c89e1050d19111008bebc87b5ca87117edff30f7026bac8793f7","record_sha256":"f06c0b5b7e7e812ea37b53e3707d0b4631b530768a5269cdf72c7b262981bab4"},
    {"record_id":"author-pr160-plan-004","pr":160,"pr_url":"https://github.com/Homie-Improvement/miru/pull/160","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Make a refused reservation force the invoice transaction to roll back.","finding_evidence":"The public coordinator contract lets a refused outcome return normally so the caller can choose whether to roll back. This contradicts the failure mapping in plan item 6 and leaves transaction safety outside the coordinator. If the caller commits after that return, the invoice change commits without a holding or commit instruction. Recovery later fails the pending intent and sends an abort, leaving a saved invoice without its reservation. Define one failure contract that forces rollback, such as raising a refusal exception that preserves the outcome's conflicts. Test a refused prepare inside an otherwise normally completing caller transaction, without a caller-supplied rollback.","recorded_resolution":"fixed","recorded_correction":"There is now one failure contract: every non-prepared answer raises, and a refusal raises an error that keeps the outcome's conflicts for the caller to read.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T17:05:25.132Z","answer_recorded_at":"2026-10-26T17:06:51.376Z","first_independent_review_at":"2026-10-26T19:10:13Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"5f2b4e91dd805a52b6475958d1163956085b175e27de96edeae77ae5ea1cd606","record_sha256":"e28dcf6548437b4d6109958639945f9168795e26cb88a1f755f0392650b83b7f"},
    {"record_id":"author-pr160-plan-005","pr":160,"pr_url":"https://github.com/Homie-Improvement/miru/pull/160","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Resolve the delivery adapter inside the boundary that records failed attempts.","finding_evidence":"plan item 9 resolves the default adapter in DeliverInvoiceOperationJob before entering OutboxDelivery. plan item 10 puts error handling and attempt counting inside that service. Since every adapter table entry raises AdapterNotAvailable, a normal job without an injected adapter fails before it can increment attempts. The U2 test expecting that increment cannot pass with these boundaries. Keep the job thin, but pass an adapter resolver into the delivery service and invoke it inside the error-handling boundary. This keeps one owner for delivery failures and preserves the planned injectable adapter.","recorded_resolution":"fixed","recorded_correction":"The delivery service now takes an adapter source it calls inside its error boundary; the job stays thin and specs still inject the test adapter.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T17:05:25.132Z","answer_recorded_at":"2026-10-26T17:06:51.376Z","first_independent_review_at":"2026-10-26T19:10:13Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"0380eb5c3ff83cfab9d471ed960f98080cd68b251ebf8158103af87daca31db8","record_sha256":"f3ea457881e668a9f0e1ecc5c2d4915245fd528512d141813bbb373cca5d7079"},
    {"record_id":"author-pr160-plan-006","pr":160,"pr_url":"https://github.com/Homie-Improvement/miru/pull/160","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Returning a refusal can commit an invoice change without its reservations.","finding_evidence":"The assumptions explicitly return a refused outcome without raising. Consider a caller that changes an invoice inside a transaction, calls the coordinator, and reaches the transaction's end. Returning normally commits the invoice change. The rollback hook in plan item 5 never runs, so the intent remains pending without an abort row. Recovery later marks that intent failed, although the invoice change already committed. The invoice can then claim entries that the reservation adapter refused. Require the refusal path to force rollback, rather than depend on an unstated caller obligation. Add a test where the caller does not explicitly roll back after receiving a refusal.","recorded_resolution":"fixed","recorded_correction":"Refusal now raises through the invoice transaction, so the invoice change cannot commit after a refusal unless a caller rescues the error inside it; that misuse is named in the plan.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T17:05:25.132Z","answer_recorded_at":"2026-10-26T17:06:51.376Z","first_independent_review_at":"2026-10-26T19:10:13Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"84556cde9fba814e092218482ae7980952ceee835ac7a9efb799b0916544772f","record_sha256":"015a1ec7b2677273aa6e840787fa1f600e40d4e409e0de1e8528916383e8d013"},
    {"record_id":"author-pr160-plan-007","pr":160,"pr_url":"https://github.com/Homie-Improvement/miru/pull/160","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Returning a refused outcome does not roll back the caller's invoice transaction.","finding_evidence":"The Assumptions section says a refused outcome is returned so the caller can roll back. Returning does not trigger the rollback hook selected in plan item 5. For example, a caller can change an invoice, call the coordinator, receive refused, and leave its transaction normally. The invoice change then commits without a holding or commit instruction; recovery later fails the pending intent and sends abort. The existing transaction wrapper in app/services/timekeeping/billing_fence.rb commits when its block returns normally and does not interpret reservation outcomes. Make refusal force rollback, such as by raising an error through the invoice transaction. Specify one consistent failure con","recorded_resolution":"fixed","recorded_correction":"A refused prepare now raises an error that carries the outcome, so the invoice transaction rolls back and the failure path writes the failed intent and abort row. A spec covers a caller that does nothing.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T17:05:25.132Z","answer_recorded_at":"2026-10-26T17:06:51.376Z","first_independent_review_at":"2026-10-26T19:10:13Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"238340858fe34fe03ee495c99cd9563de7fb26cc1fede05dd5a763a658e53934","record_sha256":"933d763d60bbc3a950dead2810ea7e24779d5d28df8b1865dbd4eca4e7d603ca"},
    {"record_id":"author-pr160-plan-008","pr":160,"pr_url":"https://github.com/Homie-Improvement/miru/pull/160","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Returning a refused outcome lets the invoice commit without a reservation.","finding_evidence":"The plan returns `refused` and leaves rollback to the caller. If the caller exits the transaction normally, the invoice commits without a reservation or holding. The rollback hook never runs, so the intent stays pending until recovery aborts it. Recovery cannot undo that committed invoice. Make refusal force the invoice transaction to roll back, rather than depend on an unstated caller obligation. Add a test where the caller does nothing after the coordinator call: refusal must leave no invoice change, a failed intent and one abort row.","recorded_resolution":"fixed","recorded_correction":"A refusal now raises, so the invoice transaction rolls back. A spec checks that a caller doing nothing after the call ends with no invoice change, a failed intent and one abort row.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T17:05:25.132Z","answer_recorded_at":"2026-10-26T17:06:51.376Z","first_independent_review_at":"2026-10-26T19:10:13Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c837c3a799d6d8a5847920210567df79c8971f6d5e7d799ed49f7af1336ce95c","record_sha256":"40e3a209d9b9c7e24fb16bccc1656dc76759bb33b1d75e5009a7485f23e275ad"},
    {"record_id":"author-pr160-plan-009","pr":160,"pr_url":"https://github.com/Homie-Improvement/miru/pull/160","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The coordinator must enforce rollback when prepare refuses the save.","finding_evidence":"The Assumptions section says a refused outcome returns so the caller can roll back on its own terms. This transfers required failure handling out of the coordinator. A caller that commits its transaction after that return can persist the invoice change without a committed intent, holding, or commit message. Recovery can later abort the reservation, but cannot undo that invoice change. Require the coordinator to force rollback on refusal, and resolve the conflicting statement that refused outcomes raise. Add a spec where the caller does not explicitly request rollback; the invoice change must still be absent, with one failed intent and one abort row.","recorded_resolution":"fixed","recorded_correction":"The coordinator now forces the rollback itself by raising on refusal; the contradictory 'returned to the caller' assumption is gone, and a spec checks a caller that never asks for rollback.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T17:05:25.132Z","answer_recorded_at":"2026-10-26T17:06:51.376Z","first_independent_review_at":"2026-10-26T19:10:13Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"933e1e35446a8330ac36a0d6287384881c76619100aeface4d198cfcba0a5d7c","record_sha256":"945a581105b32cfa3cdfc6d111438edcdad860108295911baeefba5e2139c454"},
    {"record_id":"author-pr160-plan-010","pr":160,"pr_url":"https://github.com/Homie-Improvement/miru/pull/160","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The plan both raises and returns a refused prepare outcome.","finding_evidence":"The same assumption says refused outcomes raise a retryable error and are returned so the caller can choose to roll back. plan item 6 instead routes every refusal through failure handling after rollback. Returning normally lets the caller commit an invoice change without a prepared reservation. Its intent can then remain pending until recovery aborts it. Choose one contract that guarantees rollback before recording failure. Update the declared return behavior and add a test where the caller does not explicitly roll back after refusal.","recorded_resolution":"fixed","recorded_correction":"The plan no longer both returns and raises a refusal. Every failure raises, and the coordinator returns an outcome only on the prepared path.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T17:05:25.132Z","answer_recorded_at":"2026-10-26T17:06:51.376Z","first_independent_review_at":"2026-10-26T19:10:13Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"41c6937a1f06164ea016fd27096d224cd4ec21fd469ffde5498042d611c51ad2","record_sha256":"cbdab684a3ebfd7a7b40ca81bcd580d6357832e094132e763e4aa2ee83b0fee6"},
    {"record_id":"author-pr161-plan-001","pr":161,"pr_url":"https://github.com/Homie-Improvement/miru/pull/161","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The planned authorization mutation stops before it can prove the state check fails.","finding_evidence":"The execution note requires a failure on stored state, not just response status. In timekeeping/spec/requests/internal/v1/handoff_refusals_spec.rb, expect_every_refused_caller_to_change_nothing checks status before state. Its first route is checkpoint_export, as ordered in timekeeping/spec/support/handoff_requests.rb. Skipping authorization makes that request fail the 401 expectation and stops the example before any destructive request or state comparison. Adding only a callers parameter does not change this. Specify a mutation that performs a write but still returns 401, or a focused mutation check that reaches a destructive request and evaluates stored state despite the changed response. This makes the required proof executable without changing production code in the final patch.","recorded_resolution":"fixed","recorded_correction":"The temporary break now deletes the seeded rows inside the refusal path and still answers 401. So the status check passes, and only the state comparison can catch it. The controller is then restored.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T17:54:21.425Z","answer_recorded_at":"2026-10-26T17:55:00.443Z","first_independent_review_at":"2026-10-26T18:33:51Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d7947fc3c4bda390753bfc9a31baedaac17b5c9fbc105094bb783c27998c3b9d","record_sha256":"3b1ee40f9f7960b3f7788903409bde1cda0badca395618c7a7a71a07935dbd57"},
    {"record_id":"author-pr161-plan-002","pr":161,"pr_url":"https://github.com/Homie-Improvement/miru/pull/161","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The proposed authorization mutation stops before the state check it must prove.","finding_evidence":"The execution note requires a failure on stored state, not just status. However, `expect_every_refused_caller_to_change_nothing` asserts status before comparing state. `HANDOFF_ROUTES` starts with `checkpoint_export`, whose controller renders a successful response when authorization is skipped. That first status assertion stops the example before any state comparison or destructive request. Adding a callers parameter does not change this. Use a controlled mutation that changes stored state before returning the normal 401 response, then confirm the state assertion catches it. This proves the intended protection against a request that writes before refusing access.","recorded_resolution":"fixed","recorded_correction":"Same fix: the temporary break writes before it answers the usual 401, so the example runs to the state comparison. That comparison must fail on the first route, and then the break is reverted.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T17:54:21.425Z","answer_recorded_at":"2026-10-26T17:55:00.443Z","first_independent_review_at":"2026-10-26T18:33:51Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"8e4c95a3de252f8a2db335b1f62322e086e5985fa91f2ba8b06c14ac03767978","record_sha256":"0aacb85fedbeb48ed72d37491adff50185e27c19771dea62ecd81285349e6b96"},
    {"record_id":"author-pr170-build-001","pr":170,"pr_url":"https://github.com/Homie-Improvement/miru/pull/170","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Form-encoded writes fail after switching owners","finding_evidence":"A valid form-encoded create or update with duration=30 works while the legacy store owns entries, then gets 422 once Timekeeping owns them. Rails passes form values as strings. create_body and the update body copy that string into the JSON unchanged, but Timekeeping::EntryCommand only accepts a Numeric duration, and the contract requires a JSON number. The new forwarded lifecycle specs send form parameters and still pass, because FakeTimekeeping stores the string without the real provider's type checks.","recorded_resolution":"fixed","recorded_correction":"Forwarded create and update bodies now send a numeric duration string as a JSON number, and leave other values unchanged. The token-mode fake now refuses a non-number duration, as the real service does, so the form-posted lifecycle specs cover this.","correction_target":"code_commit","source_path":"app/services/timekeeping/entry_write_client.rb","source_line":75,"fix_commit":"22bd547ef7a26e9e06e3d8bc6b36e11aa5723371","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/22bd547ef7a26e9e06e3d8bc6b36e11aa5723371","finding_recorded_at":"2026-10-26T19:55:07.631Z","answer_recorded_at":"2026-10-26T20:19:16.651Z","first_independent_review_at":"2026-10-26T20:35:43Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"4421453f7373dd7fb83f49d5ba68ecaee3fd0645e76a15243cba56d1517aa67d","record_sha256":"bd0a34c58d6f5e520c7e88d050942ca7455f0766a4b64c45fb7a730ac200f7d0"},
    {"record_id":"author-pr170-build-002","pr":170,"pr_url":"https://github.com/Homie-Improvement/miru/pull/170","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Spec file under spec/support is auto-required into every run","finding_evidence":"rails_helper requires every spec/support file, so this example is registered in every rspec invocation, including focused runs of unrelated specs. A full run also loads it through the default *_spec.rb pattern, so it runs twice, and its own require of rails_helper is circular.","recorded_resolution":"fixed","recorded_correction":"The stale If-Match delete example moved into the token-mode group of the fake's contract spec, with the same 409, version_conflict and kept-entry checks. The file under spec/support is deleted.","correction_target":"code_commit","source_path":"spec/support/timekeeping/fake_timekeeping_jwt_spec.rb","source_line":7,"fix_commit":"22bd547ef7a26e9e06e3d8bc6b36e11aa5723371","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/22bd547ef7a26e9e06e3d8bc6b36e11aa5723371","finding_recorded_at":"2026-10-26T19:55:07.631Z","answer_recorded_at":"2026-10-26T20:19:16.651Z","first_independent_review_at":"2026-10-26T20:35:43Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"e5fdad2b8da8029c4522cc25bc5e17aa8b073d5ae99b217c7511c8edd19e8619","record_sha256":"f0dd0eaa27f2cd2743d3fcd1a01ea3d7926c94937b4652c39e790f1a4ade0869"},
    {"record_id":"author-pr170-build-003","pr":170,"pr_url":"https://github.com/Homie-Improvement/miru/pull/170","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Version-conflict destroy test injects its own stale version","finding_evidence":"This is the only test of If-Match forwarding on a forwarded destroy, and it passes whatever version Gateway#destroy sends. The stub supplies its own `stale[\"version\"] + 1`, the lines that should change the entry and the final assertion all end in `if false`, and `stale` merges the version with itself. Dropping found.version would go unnoticed. The dead code also breaks the plan's completion criteria ban on dead-end code.","recorded_resolution":"fixed","recorded_correction":"The test no longer overrides the version. Another writer now changes the entry between the lookup and the real DELETE. The test checks the 409 version_conflict, that the entry is kept, and that the If-Match sent equals the version the lookup returned.","correction_target":"code_commit","source_path":"spec/requests/api/v1/timesheet_entries/gateway_spec.rb","source_line":480,"fix_commit":"22bd547ef7a26e9e06e3d8bc6b36e11aa5723371","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/22bd547ef7a26e9e06e3d8bc6b36e11aa5723371","finding_recorded_at":"2026-10-26T19:55:07.631Z","answer_recorded_at":"2026-10-26T20:19:16.651Z","first_independent_review_at":"2026-10-26T20:35:43Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"9a3356609c585881e515a792d420c85679eafebc6d6cf2dc11673d0ef9baf52e","record_sha256":"e4aec7bc50bcd6c2b3b5cac6b066d8e6a08454f89f3c41432670faf22009e0f4"},
    {"record_id":"author-pr170-plan-001","pr":170,"pr_url":"https://github.com/Homie-Improvement/miru/pull/170","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Pass the request identifier through the owner-aware lookup client.","finding_evidence":"The planned `EntryApiClient#show_entry(company_id:, entry_id:, acting_user_id:)` cannot receive the request identifier. Update and destroy call this method before forwarding their writes. The existing `EntryApiClient#get` in `app/services/timekeeping/entry_api_client.rb` sends only Authorization, and the planned default client receives no request context. Thus these lookup requests cannot carry `X-Request-Id`, which prevents tracing the complete write across services. Add an explicit request identifier argument to `show_entry`, pass `context.request_id` from `EntryLookup`, and test the header through the HTTP bridge.","recorded_resolution":"fixed","recorded_correction":"show_entry gains a request_id argument, EntryLookup passes the context's request id, and the HTTP bridge test checks the header on the lookup call.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T18:29:35.056Z","answer_recorded_at":"2026-10-26T18:31:54.238Z","first_independent_review_at":"2026-10-26T20:35:43Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"0dba9b7f4adc68053d38c9571fd21851051fff33d7769294368221f78200ac4e","record_sha256":"7e1b05ba7d2e9af8c569e6781421b5f35cf127b48cef2064ee9794690c554059"},
    {"record_id":"author-pr170-plan-002","pr":170,"pr_url":"https://github.com/Homie-Improvement/miru/pull/170","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Sending a version on delete does not yet prevent the billing race.","finding_evidence":"plan item 10 says `If-Match: version` closes the race where an entry becomes billed after lookup. Consider a delete that reads an unbilled entry, then billing changes it before DELETE reaches Timekeeping. In `spec/support/timekeeping/fake_timekeeping.rb`, `update_entry` checks `If-Match`, but `destroy_entry` receives no headers and discards the entry without checking its version. The planned tests only check that the client sends the header. They can pass while a concurrent delete removes an entry that the policy would now refuse. Establish the provider's version check for DELETE in the contract and fake, and add a test that changes the version between lookup and delete. The stale delete must return a conflict and leave the entry unchanged.","recorded_resolution":"fixed","recorded_correction":"The real Timekeeping service already checks If-Match on delete. The fake will now check it too, and a test changes the version between lookup and delete and expects 409 with the entry kept.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T18:29:35.056Z","answer_recorded_at":"2026-10-26T18:31:54.238Z","first_independent_review_at":"2026-10-26T20:35:43Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d6039911d0cee7c583ac236bb3d125e030c04628baf9548262d3c8030bef22be","record_sha256":"a54d3e76053667d2bd7d4acaec21789c70d9018778facdb0a64bde4c809adc25"},
    {"record_id":"author-pr170-plan-003","pr":170,"pr_url":"https://github.com/Homie-Improvement/miru/pull/170","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The forwarded update body can erase an entry's recorded source.","finding_evidence":"plan item 7 adds `source: \"manual\"` and `source_metadata: {}` to the forwarded body without limiting these defaults to create. Consider a user editing the note on an imported entry or an entry that records an automation tool. In `spec/support/timekeeping/fake_timekeeping.rb`, `update_entry` merges supplied source fields into the stored entry, so this edit erases its origin. The existing controller does not permit either source field, and the model preserves existing source metadata during ordinary edits. Apply these defaults only on create. For update, omit source fields and preserve the stored target user unless the existing action permits changing them. Add a forwarded note-only update test that checks the original source and metadata remain unchanged.","recorded_resolution":"fixed","recorded_correction":"Agreed. The source defaults now go on create only. A forwarded update sends only the permitted edit fields, with no source fields and no user_id, and a test checks that a note-only edit keeps the source.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T18:29:35.056Z","answer_recorded_at":"2026-10-26T18:31:54.238Z","first_independent_review_at":"2026-10-26T20:35:43Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"e44d82b3372f7fd4086d78783969a7ed88388dd590d36cf259cf7dd89a56abb2","record_sha256":"fb84593e4bc71503f0a25b9a381a05eca484c98a7f5b055c546485db2fe46ff8"},
    {"record_id":"author-pr170-plan-004","pr":170,"pr_url":"https://github.com/Homie-Improvement/miru/pull/170","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The lookup client cannot receive the request identifier required for every forwarded call.","finding_evidence":"R8 requires every forwarded call to carry `X-Request-Id`. However, `EntryApiClient#show_entry(company_id:, entry_id:, acting_user_id:)` has no request identifier or context argument. The update and destroy paths call this method before forwarding their writes. U3 tests the header only on create, update and destroy calls, so the lookup request can remain uncorrelated without failing those tests. Clarify the header requirement for this read call. To apply it consistently, pass the context's request identifier through `show_entry` and test the header on that request.","recorded_resolution":"fixed","recorded_correction":"show_entry now takes the request id and sends it as X-Request-Id. A request spec checks the header on both the lookup call and the write call for update and destroy.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T18:29:35.056Z","answer_recorded_at":"2026-10-26T18:31:54.238Z","first_independent_review_at":"2026-10-26T20:35:43Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"17cc5ab0a471447500e11f4b97908fde65b873dc3325b9f2dd711266ae5cfd3c","record_sha256":"6593137e080b51a8d6d901a2213d684faf3f0006043a061d22184b96e6d7cd0c"},
    {"record_id":"author-pr170-plan-005","pr":170,"pr_url":"https://github.com/Homie-Improvement/miru/pull/170","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The new client errors need an explicit path to the promised HTTP responses.","finding_evidence":"plan item 9 introduces `EntryWriteClient::Error`, `Forbidden`, `Conflict` and `Rejected`, but does not define their inheritance or controller handlers. `app/controllers/concerns/error_handler.rb` currently handles `EntryApiClient::Error`, Pundit errors and record validation errors, not these new types. U1 adds only the stale-generation handler. The status table alone does not make the new exceptions render as 503, 403, 409 or 422. Specify compatible inheritance where appropriate and add handlers for the remaining types. Verify the mappings through requests, because the U3 exception tests cannot prove the rendered status and message.","recorded_resolution":"fixed","recorded_correction":"The plan now gives each new client error its parent class and handler. Transport errors reuse the existing 503 handler; 403, 409 and 422 get handlers, and request specs check each status and message.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T18:29:35.056Z","answer_recorded_at":"2026-10-26T18:31:54.238Z","first_independent_review_at":"2026-10-26T20:35:43Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"fbdddff8eef45e59a5e9b6bdf143d6ff2ae9fe6041925531f160a1ed87a42756","record_sha256":"7944563823f8db1a09c816b087808f145efc710f951b886791339335b77110d8"},
    {"record_id":"author-pr170-plan-006","pr":170,"pr_url":"https://github.com/Homie-Improvement/miru/pull/170","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The owner-aware lookup does not carry the request identifier to Timekeeping.","finding_evidence":"plan item 10 adds a remote show call before forwarded updates and deletes. However, the declared `EntryApiClient#show_entry(company_id:, entry_id:, acting_user_id:)` receives no request identifier. The existing client's `get` method sets only `Authorization`. U3 tests `X-Request-Id` on the three write calls, but not on this new lookup call. This narrows the ticket's requirement to the final write and leaves lookup failures disconnected from the originating request. Pass the context's request identifier to `show_entry`, send it as `X-Request-Id`, and verify both remote calls in an update or delete request.","recorded_resolution":"fixed","recorded_correction":"The lookup call now carries X-Request-Id too, and an update or delete request spec checks both calls to Timekeeping.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T18:29:35.056Z","answer_recorded_at":"2026-10-26T18:31:54.238Z","first_independent_review_at":"2026-10-26T20:35:43Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"97897a4f6bf1b8ec0339415bc1e1894c34629abeafd616edde6efbad2bae8e78","record_sha256":"911e83cdd9d43089fc73c1e645a6f3db18299cdaaf9378eaf938460e089c103b"},
    {"record_id":"author-pr170-plan-007","pr":170,"pr_url":"https://github.com/Homie-Improvement/miru/pull/170","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The planned tests do not verify the delegated locked-entry restriction.","finding_evidence":"plan item 12 delegates locked-entry checks to Timekeeping, but the request tests use a fake that does not enforce them. In spec/support/timekeeping/fake_timekeeping.rb, update_entry checks the version and ownership of related records, then writes without checking locked or primary_role. plan item 14 also builds the fake actor without primary_role. An employee-first admin can therefore edit a reserved entry in these tests, although timekeeping/app/services/timekeeping/entry_writer.rb passes primary_role to EntryDecision.change and the shared rules refuse it. Add a negative gateway test against the real provider, or preserve primary_role and apply the existing shared rule in the fake. This verifies that forwarding does not bypass the restriction when the actor also holds an admin role.","recorded_resolution":"fixed","recorded_correction":"The fake's token mode now keeps primary_role and applies the shared timekeeping_rules change decision on update and delete. A test shows an employee-first admin cannot edit a reserved entry.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T18:29:35.056Z","answer_recorded_at":"2026-10-26T18:31:54.238Z","first_independent_review_at":"2026-10-26T20:35:43Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c1ca2135e66046cb16d278f297b2878720889a09c94c616751070f422e7cdb34","record_sha256":"23f382c3d3125f6bd3875c54a608dc19f7f6cedb578b3b11208fe64a1063bd75"},
    {"record_id":"author-pr170-plan-008","pr":170,"pr_url":"https://github.com/Homie-Improvement/miru/pull/170","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The prepared-state test expects fewer calls than the declared update and destroy paths require.","finding_evidence":"U5 says the prepared state makes one local-owner call. plan item 2 first performs lookup, and plan item 10 routes prepared lookup through `local_owner.find_entry`. plan item 13 then requires `update_entry` or `destroy_entry` for the write. Thus, create needs one call, but update and destroy need a lookup call followed by a write call. Scope the one-call assertion to create and state the two-call expectations for update and destroy. Otherwise, the test description conflicts with the interface it is meant to verify.","recorded_resolution":"fixed","recorded_correction":"The prepared-state test now expects one local-owner call for create. For update and destroy it expects a lookup call and then a write call, and nothing else.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T18:29:35.056Z","answer_recorded_at":"2026-10-26T18:31:54.238Z","first_independent_review_at":"2026-10-26T20:35:43Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9f691bb86d01f87ce804d4a44d56bf56e3f806b46f79ae639f3c7fdc20a82ea0","record_sha256":"329a10199328d5f6203ae7d91e5aae689dcae429906a475d2e15fa18cce9cc5d"},
    {"record_id":"author-pr170-plan-009","pr":170,"pr_url":"https://github.com/Homie-Improvement/miru/pull/170","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The unknown-owner test cannot reach the gateway through update_column.","finding_evidence":"The gateway test in U5 proposes storing an unknown owner with `update_column`. PostgreSQL rejects that write: `db/schema.rb:1248` defines `timekeeping_ownership_owner_check`, which permits only `monolith` and `timekeeping`. Skipping model validation does not skip this constraint. The test therefore fails during setup and never checks the gateway's refusal. Inject or stub an ownership record with the unknown value at the locked-read boundary. Keep real database reads for the locking tests.","recorded_resolution":"fixed","recorded_correction":"Agreed: the database check constraint rejects an unknown owner. The gateway now reads ownership through an injectable reader. The unknown-value tests inject a record; the lock tests keep real reads.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T18:29:35.056Z","answer_recorded_at":"2026-10-26T18:31:54.238Z","first_independent_review_at":"2026-10-26T20:35:43Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"6ead3b67b216719aff97ca879511376d2375edb4cc2322fb8f9638c36f3bd33c","record_sha256":"bdf4039356bb289cb3f415c51d436c1c89fbc9fc765a079e66a4a30ea7169b7e"},
    {"record_id":"author-pr174-build-001","pr":174,"pr_url":"https://github.com/Homie-Improvement/miru/pull/174","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Local employee fixtures hide outside-role fallback regressions","finding_evidence":"The deleted base group 'roles agreeing with the write token' built global_admin and other_company_admin with no role in the company, so role_from had to fall back to employee while a global or foreign admin role existed. Every replacement row (user_token_spec.rb:248 and :250, reference_facts_spec.rb:188) adds a local employee role first, and no spec at head builds an employed user whose only privileged role is global. Suppose a regression consults global or foreign roles only when the company-scoped role list is empty. It would raise both the served role and the signed token role, a privilege boundary, and every row would still pass. This breaks the issue's 'without reducing coverage' and plan R5.","recorded_resolution":"fixed","recorded_correction":"I added two agreement rows at the end of the token table: a global admin and another company's admin, neither holding any role in the company. Both sign and serve employee. The existing rows are unchanged.","correction_target":"code_commit","source_path":"spec/services/timekeeping/user_token_spec.rb","source_line":248,"fix_commit":"eac5d94a5bc2dbbd6396f8a0a42717b8ff5b7bcf","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/eac5d94a5bc2dbbd6396f8a0a42717b8ff5b7bcf","finding_recorded_at":"2026-10-26T19:56:07.549Z","answer_recorded_at":"2026-10-26T20:16:53.810Z","first_independent_review_at":"2026-10-26T20:30:39Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"c0186e896a2092f4eb393166702a53f03635ce1121dbd90153138f959b891c1d","record_sha256":"4770c725a660d46334935bf86ebe8c214aeb3de9f97c51a3135b7f257e665d60"},
    {"record_id":"author-pr174-plan-001","pr":174,"pr_url":"https://github.com/Homie-Improvement/miru/pull/174","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep different privileged roles together in the batch isolation example.","finding_evidence":"The existing \"roles agreeing with the write token\" example in spec/services/timekeeping/reference_facts_spec.rb lists an employee-first admin and an employee-first owner in one ReferenceFacts.call. The plan replaces them with separate table examples. Its remaining batch example has only one privileged holder: the admin. A regression that chooses one privileged role per company and assigns it to every privileged holder would pass these planned cases, but fail the existing mixed admin/owner example. This matters because held_role_names_by_user_id groups roles for multiple users in app/services/timekeeping/reference_facts.rb. Add an employee-first owner to the retained batch example and expect both \"admin\" and \"owner\" for their respective holders. Separate companies are needed for opposite role creation orders, not for users who each hold only one privileged role.","recorded_resolution":"fixed","recorded_correction":"The kept batch example now also holds an employee-first owner in the same company and one call, and expects \"admin\" for the admin and \"owner\" for the owner, so a per-company role mix-up fails it.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T19:35:00.267Z","answer_recorded_at":"2026-10-26T19:35:43.150Z","first_independent_review_at":"2026-10-26T20:30:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"5a870fc2ec60d19dabb8ee8757d23ea8e91462d51fe001aaf19f82991803967a","record_sha256":"546dbbcfcb7525f3700d3edbb7c8c5c20688c0e199185be472cd9045741221b0"},
    {"record_id":"author-pr179-build-001","pr":179,"pr_url":"https://github.com/Homie-Improvement/miru/pull/179","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Duplicated generated examples across ServiceStore and request specs","finding_evidence":"service_store_spec has twelve top-level describe blocks that assert the same scenarios over and over: NULL locked (119, 193), version below 1 (97, 170, 273), refused-then-corrected (68, 140, 242), source agent (37, 140, 215) and message leak (11, 293). The request spec repeats its one scenario three times. Plan U2 asked for one more row in the existing refusal tables. A change to the message or fixtures now needs about a dozen edits, and the copies can drift apart.","recorded_resolution":"fixed","recorded_correction":"Each spec file now has one describe block with no mid-file requires. The invalid-value case is one more row in the request refusal table, and the store spec keeps one example per distinct check.","correction_target":"code_commit","source_path":"timekeeping/spec/services/timekeeping/handoff/service_store_spec.rb","source_line":37,"fix_commit":"dfc39c9692992e5c37c1956fa6d0ae29da28ba00","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/dfc39c9692992e5c37c1956fa6d0ae29da28ba00","finding_recorded_at":"2026-10-26T20:26:24.291Z","answer_recorded_at":"2026-10-26T21:10:08.998Z","first_independent_review_at":"2026-10-26T21:25:37Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"a2508418adf35aa5c2c82175a7fed4ceebbdc314908667a959d4122bb13a16da","record_sha256":"1ca44e6ffc91c60f12a3f2d312ed73ef89ab568293d86e4e4968d559a6fb9014"},
    {"record_id":"author-pr179-build-002","pr":179,"pr_url":"https://github.com/Homie-Improvement/miru/pull/179","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"Existing ServiceStore specs deleted under a 'require only' edit","finding_evidence":"Every ServiceStore test is gone: the forward-only entry id sequence; refusals while admitted, with an edited digest and with an older version; rollback; export of all five tables; the two-company round trip; outbox and admission left untouched; audit/receipt/operation sequences never moving back; and refusing export while a prepared operation remains. Production still calls advance_entry_id_sequence (entry_id_sequences_controller.rb:12), and no spec covers it now. These are exactly the behaviours the new override in front of TableStore#load_checkpoint could break. A sequence that moves backwards and causes id collisions after handoff, or a broken rollback, would go uncaught.","recorded_resolution":"fixed","recorded_correction":"I restored the base service store spec in full and added the value-check examples inside its export and load block. Base and new examples all pass, and the full Timekeeping suite passes.","correction_target":"code_commit","source_path":"timekeeping/spec/services/timekeeping/handoff/service_store_spec.rb","source_line":1,"fix_commit":"dfc39c9692992e5c37c1956fa6d0ae29da28ba00","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/dfc39c9692992e5c37c1956fa6d0ae29da28ba00","finding_recorded_at":"2026-10-26T20:26:24.291Z","answer_recorded_at":"2026-10-26T21:10:08.998Z","first_independent_review_at":"2026-10-26T21:25:37Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"5bcb0a40b37d498e1adb2ea8eefbd52ced8ce9600e15ff681226df4c2452b6ca","record_sha256":"483c937d420910b586ad7c74f59cdc0bbb0ee59f91eabbf4f7a68ba98d50a8ba"},
    {"record_id":"author-pr179-build-003","pr":179,"pr_url":"https://github.com/Homie-Improvement/miru/pull/179","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"Existing checkpoint request specs deleted, not just require added","finding_evidence":"Every test this file held before is gone: the export shape, loading twice with decimal precision kept, the monolith round trip, keeping company ids apart, rollback on a failed insert, and the load refusal table (tampered digest 409, older version 409, unknown table 400, unparseable body 400, admission open 409). The stated reason was only 'added require rails_helper'. A regression in export, digest checking, the admission fence or rollback would now pass CI. Nothing shows a valid checkpoint still loads as before, which the intent requires. The replacement examples all refuse before the transaction starts, so none of them reaches it.","recorded_resolution":"fixed","recorded_correction":"I restored the base request spec. The restored round-trip test then showed the shared monolith fixture carries source \"agent\". The fixture now uses \"cli\", which is valid in both apps, with its digest recomputed.","correction_target":"code_commit","source_path":"timekeeping/spec/requests/internal/v1/handoff_checkpoint_spec.rb","source_line":1,"fix_commit":"dfc39c9692992e5c37c1956fa6d0ae29da28ba00","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/dfc39c9692992e5c37c1956fa6d0ae29da28ba00","finding_recorded_at":"2026-10-26T20:26:24.291Z","answer_recorded_at":"2026-10-26T21:10:08.998Z","first_independent_review_at":"2026-10-26T21:25:37Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"f39cfbad6dd552c511e6c9dd26c47ecc3ecbd6c73687a2ac892f0c264b5a990a","record_sha256":"f206cd372fbb9c7b2f3fae92688d73c6dbf1a2747210391915ad9b97ae378506"},
    {"record_id":"author-pr179-plan-001","pr":179,"pr_url":"https://github.com/Homie-Improvement/miru/pull/179","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The deep-metadata store tests cannot complete through the unchanged checkpoint encoder.","finding_evidence":"The store tests require metadata nested 96 levels to load and 97 levels to reach the new refusal check. However, gems/timekeeping_handoff/lib/timekeeping/handoff/checkpoint.rb:149 computes each checksum with JSON.generate using its default nesting limit. The encoding at line 166 wraps each metadata object in additional arrays. These fixtures exceed that limit while Checkpoint.build computes their digests, before ServiceStore runs. Constructing a checkpoint directly can reach the new refusal check, but the valid 96-level case still fails when TableStore calls Checkpoint.verify. Keep the 96/97 boundary tests in the pure validator and use transportable metadata in successful store tests. If loading 96 levels remains required, explicitly plan the encoder prerequisite rather than promise that test with the shared gem unchanged.","recorded_resolution":"fixed","recorded_correction":"Agreed. The checkpoint's checksum wraps each metadata level in several JSON arrays, so deep metadata fails at build. The 96/97 tests now run against the pure check only, and store tests use shallow metadata.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T19:51:13.802Z","answer_recorded_at":"2026-10-26T19:52:06.937Z","first_independent_review_at":"2026-10-26T21:25:37Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"eaf6dc9dd01b3695034700791c6672e62cc7e9c8292dc48dfb2f75045069a2fa","record_sha256":"4539969f37031d761ea814c0eea3db583e89593dba217b18c3760e1e86d0df02"},
    {"record_id":"author-pr179-plan-002","pr":179,"pr_url":"https://github.com/Homie-Improvement/miru/pull/179","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The existing version check does not reject non-positive checkpoint versions.","finding_evidence":"A checkpoint can contain an otherwise valid entry with version -1 and a matching digest. On an empty destination, TableStore#refuse_older_checkpoint_versions! checks no matching stored entry, so it accepts this version. timekeeping/db/schema.rb has no positive-version constraint. plan item 2 excludes version, and the assumption incorrectly treats the existing comparison as value validation. A later service command increments the version to 0 and publishes it through WriteSteps.write_outbox. app/services/timekeeping/snapshot_batch.rb requires a positive version, so delivery stops again. Validate checkpoint versions as positive integers before loading, while retaining the separate comparison against stored versions. Add an empty-destination refusal test for a non-positive version.","recorded_resolution":"fixed","recorded_correction":"Agreed. The projection requires a positive version and the load only compares versions against stored rows, so the check now also refuses a non-positive version, tested on an empty store.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T19:51:13.802Z","answer_recorded_at":"2026-10-26T19:52:06.937Z","first_independent_review_at":"2026-10-26T21:25:37Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"911959d9f369973f290c5702f8b553dd2b45f3cf89f73acf4da95939a292ff18","record_sha256":"142143b88055b65821ecadc4d9fecdcac10dc007494976b1e8724f6e249c7685"},
    {"record_id":"author-pr179-plan-003","pr":179,"pr_url":"https://github.com/Homie-Improvement/miru/pull/179","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The planned metadata boundary tests exceed the checkpoint format's own nesting limit.","finding_evidence":"The proposed successful load with metadata nested 96 levels cannot pass the shared loader. In gems/timekeeping_handoff/lib/timekeeping/handoff/checkpoint.rb, encode_value and encode_hash wrap each nested object in additional arrays. checksum then calls JSON.generate with its default nesting limit. TableStore#load_checkpoint calls Checkpoint.verify before inserting rows, so the 96-level case fails there even if the new validator accepts it. Building the proposed 97-level rejection fixture through Checkpoint.build also fails before the new check runs. Keep the shared gem unchanged as planned. Test 96/97 levels directly against the pure validator, construct rejection fixtures without computing their digest if necessary, and use transport-representable metadata for successful store and request tests.","recorded_resolution":"fixed","recorded_correction":"Agreed, same cause as the encoder limit. The shared gem stays unchanged, the nesting edges are tested on the pure check, and the store and request tests use metadata the checkpoint can carry.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T19:51:13.802Z","answer_recorded_at":"2026-10-26T19:52:06.937Z","first_independent_review_at":"2026-10-26T21:25:37Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"008e30a805fe8bae97c4792b62f3be33ebf91b425505c1ae443210765a07b1d7","record_sha256":"64b57b84c2101f7a3db2f3b24db9c6fedef2090e44506abe86fb98cfde43282a"},
    {"record_id":"author-pr180-build-001","pr":180,"pr_url":"https://github.com/Homie-Improvement/miru/pull/180","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Rolled-back creates no longer exercise the requested invoice","finding_evidence":"The merged refusal, guard-miss and post-prepare rollback examples would still pass if the coordinator stopped recording failure for an invoice whose creation rolled back. The removed copies built the ReservationRequest from the invoice created inside the transaction. Their pending intent pointed at an invoice that never committed, and it still had to end failed with one abort row. Now every request targets an invoice committed beforehand, and the invoice created in the transaction is an unrelated row that is only checked for disappearance. That proves Rails rolls back an insert. It does not prove anything about the coordinator. Suppose an invoice-existence prerequisite were added to record_failure or to the intent write (no foreign key guards this today). A rolled-back target's intent would then stay pending, and no coordinator example would catch it. The intent lists rollback of created invoices among the checks that must survive.","recorded_resolution":"fixed","recorded_correction":"The refused, guard-miss and rolled-back-after-prepared examples now build the request from the invoice created in the transaction, then check that the target is gone and its intent failed. The refused and guard-miss examples still roll back updates to an existing invoice.","correction_target":"code_commit","source_path":"spec/services/invoices/operation_coordinator_spec.rb","source_line":119,"fix_commit":"16bc76ea404bf473b594acba6357485227e65e26","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/16bc76ea404bf473b594acba6357485227e65e26","finding_recorded_at":"2026-10-26T20:29:16.853Z","answer_recorded_at":"2026-10-26T20:46:06.057Z","first_independent_review_at":"2026-10-26T21:00:23Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"4aed83db32bfa617d3b2a8760cac128eadfb998c3e76bc8b9857c49307910092","record_sha256":"7264f66a49d9eb5e83fff0f2b0cd8294d2d88b721108dbfd76de97931b05eedc"},
    {"record_id":"author-pr180-build-002","pr":180,"pr_url":"https://github.com/Homie-Improvement/miru/pull/180","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Stale-signal event name no longer pinned to its literal","finding_evidence":"The published event name is fixed as `invoices.stale_operation_intents`. Before this change, four recovery examples filtered on that exact string. The merged collector now filters on Invoices::StaleOperationIntents::NAME, and no spec checks that constant's value. If someone renames the constant's value, every stale-signal example still passes, because each one takes the expected name from the code it is meant to check. Subscribers and alerts keyed on the published name would then stop receiving the signal without any test failing. The intent says every distinct check from a removed copy must still be asserted somewhere, and this one no longer is.","recorded_resolution":"fixed","recorded_correction":"The cross-company stale-signal example now asserts the constant equals \"invoices.stale_operation_intents\", so renaming the published name fails a test. The shared collector still filters on the constant.","correction_target":"code_commit","source_path":"spec/jobs/recover_invoice_operation_intents_job_spec.rb","source_line":31,"fix_commit":"16bc76ea404bf473b594acba6357485227e65e26","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/16bc76ea404bf473b594acba6357485227e65e26","finding_recorded_at":"2026-10-26T20:29:16.853Z","answer_recorded_at":"2026-10-26T20:46:06.057Z","first_independent_review_at":"2026-10-26T21:00:23Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"8c4db6a35c7a47a9aa3992e03b24dec4daf4045a1af755193a3253518418910e","record_sha256":"bd8f4e912eaed3ee39d8745e996307cfcd20dff66bf17b538f18a56af25709a8"},
    {"record_id":"author-pr180-plan-001","pr":180,"pr_url":"https://github.com/Homie-Improvement/miru/pull/180","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The collector-removal check needs a recursive search.","finding_evidence":"The recovery-spec unit verifies removal with `grep StaleIntentEventCollector spec`, but `spec` is a directory. Without recursion, this command does not search the spec files, so it cannot establish that the constant is gone. Use `grep -R -n --include='*.rb' StaleIntentEventCollector spec` and require no matches, while distinguishing the expected exit status 1 from a search error.","recorded_resolution":"fixed","recorded_correction":"The recovery-spec check now runs a recursive search: `grep -R -n --include='*.rb' StaleIntentEventCollector spec`. It must exit 1 (no match); exit 2 is a search error, not a pass.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:04:36.158Z","answer_recorded_at":"2026-10-26T20:05:25.460Z","first_independent_review_at":"2026-10-26T21:00:23Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"44c8b35c28dc2f51931a455add3bc2cb95f244535ffe0ebce77eafba7ac8bb18","record_sha256":"b71bf59ad3d48a955915cdad8aec7a21c414623272396cb8f9c5cc8fb56a378d"},
    {"record_id":"author-pr180-plan-002","pr":180,"pr_url":"https://github.com/Homie-Improvement/miru/pull/180","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The job-spec requirement adds an exclusion that the ticket does not state.","finding_evidence":"R2 adds “and nothing beyond that” to the ticket's request for a small forwarding and queue example. The ticket does not exclude other distinct job checks. The current job spec also checks attempts when no adapter is registered, and the plan correctly retains that check in its delivery unit. Remove the exclusive phrase and explicitly allow this retained check. Otherwise, an implementer could delete it to satisfy the stated requirement.","recorded_resolution":"fixed","recorded_correction":"Same fix as the matching finding. The ticket's words no longer carry an added exclusion, and the plan now names the existing no-adapter check as one the job spec keeps.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:04:36.158Z","answer_recorded_at":"2026-10-26T20:05:25.460Z","first_independent_review_at":"2026-10-26T21:00:23Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c3f1b77751b28eafe77a64f7f25d882b9c4a2219b59618f6909bbf48ce9e011a","record_sha256":"33bc10747696b7aba8dec35e35c6e997d7dffff0e9d09c82c7ccf73b3b08f137"},
    {"record_id":"author-pr180-plan-003","pr":180,"pr_url":"https://github.com/Homie-Improvement/miru/pull/180","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The job-spec requirement excludes a check that the implementation explicitly keeps.","finding_evidence":"R2 permits only adapter forwarding and queue selection, adding “and nothing beyond that.” The stated goal, plan item 4 and U1 also require a no-adapter example that checks `attempts == 1`. An implementer cannot satisfy both instructions. Remove the exclusive wording from R2 and explicitly retain the no-adapter check. The ticket says to keep a small job example; it does not prohibit preserving this existing check.","recorded_resolution":"fixed","recorded_correction":"I removed \"and nothing beyond that\" from the job-spec requirement. It now says the no-adapter attempt-count check stays, beside forwarding and queue selection.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:04:36.158Z","answer_recorded_at":"2026-10-26T20:05:25.460Z","first_independent_review_at":"2026-10-26T21:00:23Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"4ef999e348b9f824efc7dc8f05df9b67586e4f18310f3fbe5b2f814a2a2a8aeb","record_sha256":"d8c0918fe837062b14738579a3f6d650552b39c3d94a4f1ecdf1b599c682da47"},
    {"record_id":"author-pr181-build-001","pr":181,"pr_url":"https://github.com/Homie-Improvement/miru/pull/181","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Agent-ownership spec rewrite drops the replay-ordering and valid-pair specs","finding_evidence":"When the entry writer spec was rebuilt, its agent-ownership block lost coverage that has nothing to do with this issue, and no other spec picks it up. Three base specs are gone: (1) a committed agent create retried after the agent leaves the reference facts must replay, not answer a false 404. That spec pins the agent check after the receipt read. (2) An update that keeps the stored agent must commit and republish agent_id/user_id. (3) An update that changes user_id and agent_id together to another valid pair must commit. The own-agent create spec also no longer checks agent_id/user_id on the entry or the outbox snapshot. If the agent check is moved ahead of the receipt read, or turns down a valid pair, every remaining spec still passes. The issue asks for none of this to change.","recorded_resolution":"fixed","recorded_correction":"I restored the base agent ownership block word for word, including the replay, keep-agent and valid-pair specs and the outbox agent checks. A diff against the base shows it is identical, and the full suite passes.","correction_target":"code_commit","source_path":"timekeeping/spec/services/timekeeping/entry_writer_spec.rb","source_line":221,"fix_commit":"baa2778d2c64928fd21fc001156822a54f68c0e3","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/baa2778d2c64928fd21fc001156822a54f68c0e3","finding_recorded_at":"2026-10-26T20:33:40.418Z","answer_recorded_at":"2026-10-26T21:05:12.173Z","first_independent_review_at":"2026-10-26T21:19:08Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"1421815f372c8d93392085f5bb7a28bb5c8e8f7c203264f45119a3b9fc3be2cb","record_sha256":"1e44791a6095be2a3c0ce8472cfe30e1687099e05f42b4ebd2ccb9b4d63930b6"},
    {"record_id":"author-pr181-plan-001","pr":181,"pr_url":"https://github.com/Homie-Improvement/miru/pull/181","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Refusing both commit and abort can leave reserved entries without a recovery path.","finding_evidence":"plan item 4 refuses both commit and abort when a reserved entry contains a bad stored value. The deferred-work section also says that the lock prevents repair through the entry API. For example, an entry whose source becomes \"legacy\" after preparation cannot be committed, released or corrected through those paths. This protects delivery but leaves the affected user unable to complete or cancel the reservation. Define and test a safe recovery path before deferring repair, using an existing operator mechanism if one exists. Recovery must still prevent invalid snapshots from entering the outbox. Explain who can perform it and how the entry becomes usable again.","recorded_resolution":"fixed","recorded_correction":"The plan now states the recovery route and tests it: abort still works when only a released held entry is bad, and checkpoints cannot carry prepared operations. A dedicated repair tool stays follow-up.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T19:51:37.800Z","answer_recorded_at":"2026-10-26T19:53:04.806Z","first_independent_review_at":"2026-10-26T21:19:08Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"5bedcead13999e12cf583a81ad320579bcacf597e44642b19cd0bb720b0d5400","record_sha256":"c8f0de8fb30bd0949fd8a8ced6e86418736c5e8c24b8eae27f5f990f4ccb007b"},
    {"record_id":"author-pr181-plan-002","pr":181,"pr_url":"https://github.com/Homie-Improvement/miru/pull/181","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The commit refusal test needs an entry whose reservation is released.","finding_evidence":"The reservation test in U3 expects refusal after prepare succeeds and an entry acquires a bad source. A normal commit only changes its reservation from provisional to committed. In timekeeping/app/services/timekeeping/reservation_operations.rb, record_locked_flips returns before write_outbox when the entry stays locked. The existing reservation_operations_spec.rb explicitly tests this behavior. That setup therefore never reaches the planned check. Specify a commit that releases a previously committed entry omitted from the new desired set, then corrupt that entry before commit. Assert refusal and rollback there. Keep a separate assertion that a commit without a locked-flag change still succeeds without publishing.","recorded_resolution":"fixed","recorded_correction":"The commit refusal test now releases a previously committed entry left out of the new desired set, and a separate test keeps a commit with no locked-flag change succeeding without publishing.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T19:51:37.800Z","answer_recorded_at":"2026-10-26T19:53:04.806Z","first_independent_review_at":"2026-10-26T21:19:08Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"94bfb26bf993cbd891bb083b115efebb8bf373df90edc03163432eef082ace23","record_sha256":"682105caaf9f122c23d916ca25cc339d60fcd8dddf5d95ba994fb6de6e1241d0"},
    {"record_id":"author-pr181-plan-003","pr":181,"pr_url":"https://github.com/Homie-Improvement/miru/pull/181","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The reservation tests leave the abort rollback path untested.","finding_evidence":"The reservation unit promises refusal coverage for prepare, commit and abort, but lists no abort refusal test. A concrete case is a prepared reservation whose entry acquires source \"legacy\" before abort. In timekeeping/app/services/timekeeping/reservation_operations.rb, abort_operation deletes provisional reservations before record_locked_flips publishes the unlocked entry. Test that the guard refuses this snapshot and restores the reservations, prepared operation, version and audit rows, with no receipt or outbox insert. Also make the commit test explicitly release a bad previously held entry. A normal provisional-to-committed transition leaves locked unchanged, so record_locked_flips returns before write_outbox; corrupting only that entry would not exercise the planned rescue. The existing spec \"writes no audit, outbox or version change when a provisional reservation becomes committed\" co","recorded_resolution":"fixed","recorded_correction":"The reservation tests now include an abort whose unlocked entry holds a bad source, with full rollback checks, and a commit that releases a bad previously held entry.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T19:51:37.800Z","answer_recorded_at":"2026-10-26T19:53:04.806Z","first_independent_review_at":"2026-10-26T21:19:08Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f00f0ce12f9039bacfc470482ed819bafeb9a87d9531f48522fd5bd9b76ae84b","record_sha256":"928f409e5084f0643fc74c656995a2ee68cffe88c4ec70d258e886674b76c60f"},
    {"record_id":"author-pr182-build-001","pr":182,"pr_url":"https://github.com/Homie-Improvement/miru/pull/182","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Cross-company note correction rule tested twice with identical setup","finding_evidence":"The examples at lines 666 and 722 build the same fixture and open with the same assertions, and line 641 also covers cross-company scoping. That goes against the plan's one test per rule (R3/U3) and means three edits for one change to correction scoping.","recorded_resolution":"fixed","recorded_correction":"I removed the duplicate cross-company note-correction example. Its two assertions about company 202 moved into the example that checks the result is returned unrepaired.","correction_target":"code_commit","source_path":"spec/lib/timekeeping/handoff/reconciliation_spec.rb","source_line":666,"fix_commit":"ca8927829340ed46f78dd0b85b79be136ca48aaa","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ca8927829340ed46f78dd0b85b79be136ca48aaa","finding_recorded_at":"2026-10-26T20:53:02.526Z","answer_recorded_at":"2026-10-26T21:13:49.524Z","first_independent_review_at":"2026-10-26T21:27:13Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"667ed61596032866e2416d7b601e0b324f21084ef22e37eb8c30ab857ef59dc9","record_sha256":"6608c758b750957a15aae2adb9c7ffd61745cba1351747ee93564b444cb98709"},
    {"record_id":"author-pr182-build-002","pr":182,"pr_url":"https://github.com/Homie-Improvement/miru/pull/182","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Entry corrections lose their second-field rejection scenario","finding_evidence":"No remaining example puts a listed :entries field correction next to an unlisted second change on the same row. The replacement takes its correction from a projection difference, so it covers only :projection. An entries-only over-broad match in Correction#explains? would go unnoticed, contrary to R3 and plan item 5 (keep the strictest correction variant). This is lost coverage, not a present defect.","recorded_resolution":"fixed","recorded_correction":"I added an entries example where a listed locked correction sits beside an unlisted note change on the same row, and cited it in the learning. When I made explains? ignore the field for entries, only this example failed.","correction_target":"code_commit","source_path":"spec/lib/timekeeping/handoff/reconciliation_spec.rb","source_line":659,"fix_commit":"ca8927829340ed46f78dd0b85b79be136ca48aaa","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ca8927829340ed46f78dd0b85b79be136ca48aaa","finding_recorded_at":"2026-10-26T20:53:02.526Z","answer_recorded_at":"2026-10-26T21:13:49.524Z","first_independent_review_at":"2026-10-26T21:27:13Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"052d973a83b62fcc719d830ee86ef4f589416d53d0776b77a91f46829db0499b","record_sha256":"0a2ce46308a4e2b936ac64fc082b85f243455935fa5c53ad1592132911c17e63"},
    {"record_id":"author-pr182-build-003","pr":182,"pr_url":"https://github.com/Homie-Improvement/miru/pull/182","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"No-repair assertion checks a hash run never receives","finding_evidence":"The assertion can never fail: recon_checks_run copies `changed` through recon_checks_canonical's reverse_merge before Checkpoint.build and the projection mapper see it, so run never holds the inspected objects. The listed-difference path has no other no-repair check on the projection rows run receives; the deleted reconciliation_2_spec examples asserted on the objects run actually got.","recorded_resolution":"fixed","recorded_correction":"The unrepaired example now builds the checkpoint and projection itself and passes them to run. Afterwards it asserts the notes on the rows run actually received, not on the copied hash.","correction_target":"code_commit","source_path":"spec/lib/timekeeping/handoff/reconciliation_spec.rb","source_line":742,"fix_commit":"ca8927829340ed46f78dd0b85b79be136ca48aaa","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ca8927829340ed46f78dd0b85b79be136ca48aaa","finding_recorded_at":"2026-10-26T20:53:02.526Z","answer_recorded_at":"2026-10-26T21:13:49.524Z","first_independent_review_at":"2026-10-26T21:27:13Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"49a3599681b4e4045f8f992744eadd42fb42f60269b9485c99b091856cd8aaf7","record_sha256":"aaf78b911764a0512504563a1d2eaa4270c714d87709cffb8c33e2f2a73d6ed4"},
    {"record_id":"author-pr182-build-004","pr":182,"pr_url":"https://github.com/Homie-Improvement/miru/pull/182","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"No-write example no longer guards intents and money snapshots","finding_evidence":"If run changed an intent row or a money snapshot in place, no test would fail, although the example is titled 'leaves every input unchanged' and reconciliation.rb:11 says run changes no input. The deleted reconciliation_4_spec example deep-compared intents, next_entry_id and both money snapshots with a discount change; the merged example copies only source.rows, checkpoint.rows and projection with difference-free default snapshots. The merge weakened a case.","recorded_resolution":"fixed","recorded_correction":"The no-write example now builds intents and money snapshots, with company 101's discount going from 5 to 15. It deep-copies them beside the checkpoint rows and projection and compares all of them after the run.","correction_target":"code_commit","source_path":"spec/lib/timekeeping/handoff/reconciliation_spec.rb","source_line":697,"fix_commit":"ca8927829340ed46f78dd0b85b79be136ca48aaa","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ca8927829340ed46f78dd0b85b79be136ca48aaa","finding_recorded_at":"2026-10-26T20:53:02.526Z","answer_recorded_at":"2026-10-26T21:13:49.524Z","first_independent_review_at":"2026-10-26T21:27:13Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"22507da279c121b82285a9d50056ae387b47d7a6f6caad241bf57bd3890bd03a","record_sha256":"8bb5211afd937e19c5f646691dfe81fa53ee2b797a0a2fca431dbbde36f341a6"},
    {"record_id":"author-pr189-build-001","pr":189,"pr_url":"https://github.com/Homie-Improvement/miru/pull/189","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Actor-keyword and response-field examples drop the audit actor_id and created_at checks","finding_evidence":"No LocalOwner spec now checks the audit row's actor against a body role claim, and created_at precision is no longer checked. These are distinct checks the merge dropped (R4).","recorded_resolution":"fixed","recorded_correction":"The actor-keyword example now uses an employee actor and sends role owner and acting_user_id in the body. It checks that the audit row's actor_id is the keyword actor. The response-fields example checks created_at to the microsecond in UTC again.","correction_target":"code_commit","source_path":"spec/services/timekeeping/local_owner_spec.rb","source_line":375,"fix_commit":"734f872defad4047c98cbf7323524e9a8ef51b73","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/734f872defad4047c98cbf7323524e9a8ef51b73","finding_recorded_at":"2026-10-26T22:11:58.420Z","answer_recorded_at":"2026-10-26T22:40:15.690Z","first_independent_review_at":"2026-10-26T22:59:00Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"e086a120ef81d1fb28be9ea3ce5fd21679f0ba5b4101998c30762d36ba02e03b","record_sha256":"961ec4650a1440a58cc0d05c3f0941e1e33abd356b12776dbdba9ea5f1bd68ba"},
    {"record_id":"author-pr189-build-002","pr":189,"pr_url":"https://github.com/Homie-Improvement/miru/pull/189","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Foreign-id case still has two homes (helpers spec copy)","finding_evidence":"R5 gives the foreign-id case one home. This weaker near-copy of the tenancy example means a write-path change needs two edits, and the copy can drift without anyone noticing.","recorded_resolution":"fixed","recorded_correction":"I deleted the foreign-id copy from the helpers spec. The tenancy example makes every assertion it made, plus the id-masked JSON comparison.","correction_target":"code_commit","source_path":"spec/contracts/timekeeping/local_owner_spec_helpers_spec.rb","source_line":259,"fix_commit":"734f872defad4047c98cbf7323524e9a8ef51b73","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/734f872defad4047c98cbf7323524e9a8ef51b73","finding_recorded_at":"2026-10-26T22:11:58.420Z","answer_recorded_at":"2026-10-26T22:40:15.690Z","first_independent_review_at":"2026-10-26T22:59:00Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"04a356d1f4b51e56f98774458b3907c54999e7139e738514a320a2334cfdda53","record_sha256":"ea6dbf0425e8f140523f0982096b67da67fae05fafe0b2f3767439b16c7fa24e"},
    {"record_id":"author-pr189-build-003","pr":189,"pr_url":"https://github.com/Homie-Improvement/miru/pull/189","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Helpers spec repeats each helper example two or three times","finding_evidence":"The file is eleven pasted describe blocks with mid-file headers that have no effect. Row counts are checked at lines 8, 110 and 190, the table list at 46 and 134, prepare_local_ownership! at 66, 147 and 222, and timekeeping_member at 90 and 165. This rebuilds, in the helper's own spec, the duplication the change removes elsewhere.","recorded_resolution":"fixed","recorded_correction":"The helpers spec is now one describe with one example per helper, each holding the union of the old copies' assertions. Its 4 examples pass.","correction_target":"code_commit","source_path":"spec/contracts/timekeeping/local_owner_spec_helpers_spec.rb","source_line":110,"fix_commit":"734f872defad4047c98cbf7323524e9a8ef51b73","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/734f872defad4047c98cbf7323524e9a8ef51b73","finding_recorded_at":"2026-10-26T22:11:58.420Z","answer_recorded_at":"2026-10-26T22:40:15.690Z","first_independent_review_at":"2026-10-26T22:59:00Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"d809602d07c9200a99fc25ea3e3dc822a07402817054a75de3ceea877a38fa6e","record_sha256":"588b3816ed36fe717649cee227cc950da5ce9559a0212972409a7d63913a9642"},
    {"record_id":"author-pr189-build-004","pr":189,"pr_url":"https://github.com/Homie-Improvement/miru/pull/189","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Merged bulk_move example drops audit, outbox_position and receipt checks","finding_evidence":"Snapshot batches sort by outbox_position (snapshot_batch.rb:88). Positions out of order, skipped audits or one receipt per entry would all pass, while the example's title still claims position order.","recorded_resolution":"fixed","recorded_correction":"The bulk_move example now orders by outbox_position and checks the positions rise without repeats in entry-id order. It also checks one audit per moved entry and one receipt.","correction_target":"code_commit","source_path":"spec/services/timekeeping/local_owner_spec.rb","source_line":453,"fix_commit":"734f872defad4047c98cbf7323524e9a8ef51b73","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/734f872defad4047c98cbf7323524e9a8ef51b73","finding_recorded_at":"2026-10-26T22:11:58.420Z","answer_recorded_at":"2026-10-26T22:40:15.690Z","first_independent_review_at":"2026-10-26T22:59:00Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"afbad16804aa7f00fd6d58b02ea25619b0e2c3d6c4e1cc777ed0daf830e6bcee","record_sha256":"6a85b43c6b061000a769f2937a77ae8c48df9b38df75758721700ad2503bba7e"},
    {"record_id":"author-pr189-build-005","pr":189,"pr_url":"https://github.com/Homie-Improvement/miru/pull/189","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Replay-steps example drops the reused-key branch","finding_evidence":"A reused-key path that takes locks or writes before it refuses would go unnoticed.","recorded_resolution":"fixed","recorded_correction":"The replay-steps example now also sends the same key with a changed note. It checks for 422 and that the same four steps run before the receipt check, matching the original's STEPS.take(4).","correction_target":"code_commit","source_path":"spec/services/timekeeping/local_owner_spec.rb","source_line":431,"fix_commit":"734f872defad4047c98cbf7323524e9a8ef51b73","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/734f872defad4047c98cbf7323524e9a8ef51b73","finding_recorded_at":"2026-10-26T22:11:58.420Z","answer_recorded_at":"2026-10-26T22:40:15.690Z","first_independent_review_at":"2026-10-26T22:59:00Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"7b706741824eea41f3fe06e1c1d53148aaee6b39c47fa092546a28dc9200a24b","record_sha256":"c6615863a2e1c6814ae4b4deaa4b481f8f7d833933e8fe56a6d31829d6bde274"},
    {"record_id":"author-pr189-build-006","pr":189,"pr_url":"https://github.com/Homie-Improvement/miru/pull/189","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Stale retry split into near-identical employee and owner copies","finding_evidence":"R5 and plan item 1 give the stale retry one home. The two copies differ only in role, which the fence does not read, and each holds checks the other lacks.","recorded_resolution":"fixed","recorded_correction":"The employee and owner stale retries are now one table with a row per role. Each row holds both copies' assertions.","correction_target":"code_commit","source_path":"spec/services/timekeeping/local_owner_fence_spec.rb","source_line":68,"fix_commit":"734f872defad4047c98cbf7323524e9a8ef51b73","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/734f872defad4047c98cbf7323524e9a8ef51b73","finding_recorded_at":"2026-10-26T22:11:58.420Z","answer_recorded_at":"2026-10-26T22:40:15.690Z","first_independent_review_at":"2026-10-26T22:59:00Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"8aeff1e9982ce8e699e0b31a5a703517bfd12e7783c3591b11ea00f68a6a7e6f","record_sha256":"a8bde6ea07547c356eec01f050bdb86ea46bee1a712974384b1298c993fc19ea"},
    {"record_id":"author-pr189-build-007","pr":189,"pr_url":"https://github.com/Homie-Improvement/miru/pull/189","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"Tenancy 'colleague access' block deleted; colleague and agent cases have no new home","finding_evidence":"The diff deletes all 11 examples of the base tenancy file's 'colleague access' block, and nothing at head replaces them. Gone are the checks on employee reassignment to a colleague, editing a colleague's entry, bulk routes listing a colleague's entry, bulk_delete scoped by the role the actor claims, an admin naming another company's user, and all five agent-ownership cases. A regression in update, bulk or agent authorization now passes the LocalOwner suite. That breaks R4, and the change does not disclose the deletion.","recorded_resolution":"fixed","recorded_correction":"I restored the colleague access block from the base, using the shared helpers: reassignment, colleague edit, bulk routes, the claimed-admin bulk_delete, a foreign user, and the agent cases. I dropped only the two create-for-colleague cases core covers.","correction_target":"code_commit","source_path":"spec/services/timekeeping/local_owner_tenancy_spec.rb","source_line":82,"fix_commit":"734f872defad4047c98cbf7323524e9a8ef51b73","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/734f872defad4047c98cbf7323524e9a8ef51b73","finding_recorded_at":"2026-10-26T22:11:58.420Z","answer_recorded_at":"2026-10-26T22:40:15.690Z","first_independent_review_at":"2026-10-26T22:59:00Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"d02609d2f70fa4194375bf0bdf75113270ae052e036a2a2a87e4041409ff5905","record_sha256":"72543b20ccf2959aa3f9d73d38b4d543b377b117b675240f8ccf7414ded77ec0"},
    {"record_id":"author-pr189-build-008","pr":189,"pr_url":"https://github.com/Homie-Improvement/miru/pull/189","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"credential_in_body keeps a weaker standalone copy beside its table","finding_evidence":"credential_in_body keeps two homes (R5). The standalone copy checks less, and the table's comment at line 45 is false while it stays outside the table.","recorded_resolution":"fixed","recorded_correction":"The standalone array example is now the last row of the credential table. It also gains the 422, seeded-baseline and unchanged-attributes checks the table makes.","correction_target":"code_commit","source_path":"spec/services/timekeeping/local_owner_spec.rb","source_line":8,"fix_commit":"734f872defad4047c98cbf7323524e9a8ef51b73","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/734f872defad4047c98cbf7323524e9a8ef51b73","finding_recorded_at":"2026-10-26T22:11:58.420Z","answer_recorded_at":"2026-10-26T22:40:15.690Z","first_independent_review_at":"2026-10-26T22:59:00Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"cfc5e12d40a04183b440b2f477d64c770631bf26066f528643db7dbf429e168e","record_sha256":"a688f3a2865d870216f8caea0dbbaa241aefe56a1c8f5bfbd18d4fc26dd58d36"},
    {"record_id":"author-pr189-build-009","pr":189,"pr_url":"https://github.com/Homie-Improvement/miru/pull/189","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"work_date check loses the update path and two refused forms","finding_evidence":"The base sent four forms to both create and update. The new table sends two forms to create only, so an update path that stopped validating work_date, or accepted week or ordinal dates, would pass.","recorded_resolution":"fixed","recorded_correction":"Each of the five refused work_date forms now goes to both a create and an update. Each row checks for 400 invalid_request, unchanged attributes and unchanged row counts.","correction_target":"code_commit","source_path":"spec/services/timekeeping/local_owner_spec.rb","source_line":303,"fix_commit":"734f872defad4047c98cbf7323524e9a8ef51b73","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/734f872defad4047c98cbf7323524e9a8ef51b73","finding_recorded_at":"2026-10-26T22:11:58.420Z","answer_recorded_at":"2026-10-26T22:40:15.690Z","first_independent_review_at":"2026-10-26T22:59:00Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"63d3fe0d9971a7aa1ea05d6a70c2beaab4c1693d872412b67fcf6894c50ff85e","record_sha256":"45598fdc71e0c11599cee42ffda7aef0c6eb8209a851bd34b8e648f668a6c370"},
    {"record_id":"author-pr189-plan-001","pr":189,"pr_url":"https://github.com/Homie-Improvement/miru/pull/189","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Removing one company filter does not defeat the foreign-entry refusal.","finding_evidence":"The planned foreign-id mutation can leave a correct merged example green. In app/services/timekeeping/local_owner.rb:486-499, removing company_id from locked_entries still leaves reserved_rows filtering by company_id. The foreign id then maps to nil, and update_entry returns not_found without changing rows. This defeats the stated requirement to stop and fix the example whenever that mutation survives. Define a mutation that actually permits a foreign entry through both reads, or record this surviving mutation as redundant protection rather than lost coverage. Do not change the test merely to detect removal of one filter when another still enforces the same refusal.","recorded_resolution":"fixed","recorded_correction":"Same fix as the company-filter finding: the fault opens both reads. Removing only one filter is now recorded as backup protection, and that alone never triggers a test change.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:07:47.017Z","answer_recorded_at":"2026-10-26T21:08:57.040Z","first_independent_review_at":"2026-10-26T22:59:00Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"cb6538586f5babd2fd517298a1a81c5e1924e97b1fb6d8b7f7377c1ba5a5b22c","record_sha256":"956f35f0d8acc7b58a55588b1b9aa07d31d284234518cf8fb7d7516e3c80eaff"},
    {"record_id":"author-pr189-plan-002","pr":189,"pr_url":"https://github.com/Homie-Improvement/miru/pull/189","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The allowed unit order can delete tests before their replacements exist.","finding_evidence":"The plan permits the core, fence, and tenancy merges in any order after the helper is added. However, the core merge deletes `_2`, `_3`, and `_4`, while the fence and tenancy merges add replacements for some examples in those files. Landing the core merge first would temporarily remove those checks, contrary to the goal of preserving every assertion. Move the destination examples before deleting their source files, and make that dependency explicit. Alternatively, keep each source deletion and all its replacement examples in one commit.","recorded_resolution":"fixed","recorded_correction":"The core merge now runs last among the merges: it depends on the fence and tenancy merges, so every copy has its new home before _2, _3 and _4 are deleted.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:07:47.017Z","answer_recorded_at":"2026-10-26T21:08:57.040Z","first_independent_review_at":"2026-10-26T22:59:00Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9b0ea1e90ef3600c38122d6acd4eb9ec2a0ed14615f49fca6ea72dbbf0dfb88f","record_sha256":"2b665674607192b0782afe823a74fff25260504e2b5c29167826bc73167a59b8"},
    {"record_id":"author-pr189-plan-003","pr":189,"pr_url":"https://github.com/Homie-Improvement/miru/pull/189","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The foreign-id fault check must account for both company filters.","finding_evidence":"Removing only the filter on the entry lookup will not make the foreign-id example fail. app/services/timekeeping/local_owner.rb:487 filters the locked entries by company. The subsequent reserved_rows call filters them again at :499. With only the first filter removed, the second read omits the foreign entry, and update_entry still answers not_found. This is correct behavior, not a weak example. Revise the temporary fault to bypass both filters for this path, and make the stop condition require failure only when the mutation actually permits foreign access. Otherwise the planned verification cannot pass as described and could prompt unnecessary test changes.","recorded_resolution":"fixed","recorded_correction":"Confirmed in local_owner.rb: locked_entries and reserved_rows both filter by company_id. The fault check now removes both filters, and a surviving one-filter break counts as backup protection.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:07:47.017Z","answer_recorded_at":"2026-10-26T21:08:57.040Z","first_independent_review_at":"2026-10-26T22:59:00Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"6c61f19e473b8f9e9656f1c9f1e4eec690123b36852edd78e407e23c1dceeb06","record_sha256":"9a81e3056934b0866ba34d0647522abcfb2932fea875582f79a9e9c55fa98920"},
    {"record_id":"author-pr192-build-001","pr":192,"pr_url":"https://github.com/Homie-Improvement/miru/pull/192","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Meta spec distinct-case markers pass vacuously on unrelated text","finding_evidence":"The meta spec says it guards that each distinct case is kept (plan item 5 / R6), but its bare-substring markers are already matched by unrelated text. \"500\" (line 58) also matches the latency_bucket_bounds_ms lists (spec/tasks/timekeeping_alerts_spec.rb:90, 116, 153), and \"600\" (line 61) is matched by the committed intent's 3600 (line 233). Removing the 500-status case or the exact-10-minute boundary row would still pass, and those are the cases the intent says must stay. The line 96 regex fails on harmless rewrites, and the file opens the top-level describe three times, the repeated-describe shape this change removes elsewhere.","recorded_resolution":"fixed","recorded_correction":"The meta spec now uses markers that only the guarded example holds, and each must appear exactly once. All checks share one describe. Removing the 500 status or the 600-second row now fails it.","correction_target":"code_commit","source_path":"spec/meta/timekeeping_alerts_specs_spec.rb","source_line":58,"fix_commit":"0eda2e5b4ff9e3c43cefb117dae8e36f8a83a0c2","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/0eda2e5b4ff9e3c43cefb117dae8e36f8a83a0c2","finding_recorded_at":"2026-10-26T22:15:30.511Z","answer_recorded_at":"2026-10-26T22:39:25.108Z","first_independent_review_at":"2026-10-26T22:57:29Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"8967fba5d6c440a92a82448ac843917b8c4fce9cdc52a9ef2042f71b0cf5b7b0","record_sha256":"251419a3f402eef4ac0f32ddfe5840aa4bead9b5551f669950905967bc4a3c1a"},
    {"record_id":"author-pr192-plan-001","pr":192,"pr_url":"https://github.com/Homie-Improvement/miru/pull/192","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep the delay check after each refused credential before deleting the numbered spec.","finding_evidence":"The deletion drops a stronger assertion. In timekeeping/spec/requests/internal/v1/write_delay_hook_2_spec.rb, each rejected request must leave write_delay_seconds at zero. The retained write_delay_hook_spec.rb checks zero only after all three requests. A regression that sets a delay for the missing credential, then clears it on a later rejection, would pass the retained example. The plan incorrectly treats these checks as equivalent. Move the per-request zero-delay assertions into the retained example before deleting the numbered file, and include that file in the unit's file list.","recorded_resolution":"fixed","recorded_correction":"Same fix as the other write-delay findings: the kept example asserts a zero delay after each refused credential, and write_delay_hook_spec.rb is in the unit's file list.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:35:46.979Z","answer_recorded_at":"2026-10-26T21:36:51.622Z","first_independent_review_at":"2026-10-26T22:57:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"69de6488ca26cfd4dc41074041718ee99d319f2df44aa4e9a6a575239875f42a","record_sha256":"d496704c65104fd126965eedd492beb08ba1425815623bc481e06e3af8b30879"},
    {"record_id":"author-pr192-plan-002","pr":192,"pr_url":"https://github.com/Homie-Improvement/miru/pull/192","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep the zero-delay assertion after each refused request.","finding_evidence":"plan item 4 treats the write-delay examples as equivalent, but their assertions differ. In timekeeping/spec/requests/internal/v1/write_delay_hook_2_spec.rb, the example checks write_delay_seconds after each refused request. The surviving example in write_delay_hook_spec.rb checks it only after all three requests. A regression that sets a delay on the first refusal and clears it on a later refusal would pass the surviving example but fail the deleted one. The delay persists in a file, as timekeeping/lib/timekeeping/test_hooks.rb shows, so the intermediate state matters. Before deleting the numbered file, retain its zero-delay check after each request in the surviving example. This preserves the stronger assertion instead of weakening coverage.","recorded_resolution":"fixed","recorded_correction":"Agreed: the deleted file checks the delay after each refused request. The kept example in write_delay_hook_spec.rb now gains that per-request check before the numbered file is deleted.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:35:46.979Z","answer_recorded_at":"2026-10-26T21:36:51.622Z","first_independent_review_at":"2026-10-26T22:57:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c803b8dddbaf69aa88ea655bac576b0f902715f3c1484332bad9787e5b613a36","record_sha256":"d662a1446260ac666db7064e18d58d17744605ac919f9bd5275d0b95ee97f7df"},
    {"record_id":"author-pr192-plan-003","pr":192,"pr_url":"https://github.com/Homie-Improvement/miru/pull/192","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Preserve the delay-state check after each refused request.","finding_evidence":"The write-delay examples are not equally strong. In timekeeping/spec/requests/internal/v1/write_delay_hook_2_spec.rb, each request with missing, user or wrong service credentials must leave the delay at zero. The retained write_delay_hook_spec.rb checks the delay only after all three requests. Deleting the numbered example therefore removes the check immediately after the first two refusals. A future defect could change the delay on an earlier refusal and reset it on a later request without failing the retained test. Move the per-request zero-delay assertions into write_delay_hook_spec.rb before deleting the numbered file, and include that retained file in the implementation unit's files.","recorded_resolution":"fixed","recorded_correction":"The plan now moves the zero-delay check after each refused request into write_delay_hook_spec.rb, and that file is listed in the unit's files.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:35:46.979Z","answer_recorded_at":"2026-10-26T21:36:51.622Z","first_independent_review_at":"2026-10-26T22:57:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"175b2594fb086e299bb9aa04be173cb382219a1b48a95febda26edd99ae09caf","record_sha256":"d1670721d8b1ed83f32744581f96595f0cf86e185e97edc985ff1068a0ecfa0e"},
    {"record_id":"author-pr192-plan-004","pr":192,"pr_url":"https://github.com/Homie-Improvement/miru/pull/192","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The stop condition conflicts with the required temporary task edits.","finding_evidence":"The stated goal says to stop rather than edit anything under `lib/`. However, the execution note in “U2. One alert task spec file” requires two temporary edits to `lib/tasks/timekeeping_alerts.rake` to prove that assertions detect missing output and leaked tokens. Reverting those edits satisfies the final file restriction, but not the stated stop condition. Clarify that the stop condition prohibits retained production changes and permits these temporary verification edits, provided they are reverted.","recorded_resolution":"fixed","recorded_correction":"The stop condition now forbids kept production changes and allows the two temporary rake task edits used to prove the assertions can fail, provided they are reverted.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:35:46.979Z","answer_recorded_at":"2026-10-26T21:36:51.622Z","first_independent_review_at":"2026-10-26T22:57:29Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"a338d71d3c6a5eeefcb220f61608ed865c1e66abfaa8f4cb142ddd9b322eb40f","record_sha256":"443e1d01a6af90257f57a252b700a0e31187a90dec44a2f5940048e28f70b06e"},
    {"record_id":"author-pr194-build-001","pr":194,"pr_url":"https://github.com/Homie-Improvement/miru/pull/194","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"LocalOutboxSource spec repeats its cross-company and re-mark mark_sent cases","finding_evidence":"The example at line 144 checks nothing that the examples at lines 79, 101 and 126 do not, apart from an exact travel_to time. The mark_sent contract is checked in up to four copies that can drift apart, and a reader cannot tell which example is the real one.","recorded_resolution":"fixed","recorded_correction":"I removed the repeated cross-company re-mark example and the mark half of the interleaved read example. The re-mark example now asserts sent_at equals the first travel time exactly.","correction_target":"code_commit","source_path":"spec/services/timekeeping/local_outbox_source_spec.rb","source_line":144,"fix_commit":"11104b73ef5e384ad2c7bc341d0c8d3f9fc86bed","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/11104b73ef5e384ad2c7bc341d0c8d3f9fc86bed","finding_recorded_at":"2026-10-26T22:28:27.219Z","answer_recorded_at":"2026-10-26T22:36:31.736Z","first_independent_review_at":"2026-10-26T22:55:37Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"e516cce4173687c4cfd6da8556811da97c117bb723ca4e0fc415e9f4f8e60795","record_sha256":"b5ebf018876ba8622371a2dc315eb0aea197c44ea9e3a0414a459354bdd8b84e"},
    {"record_id":"author-pr194-plan-001","pr":194,"pr_url":"https://github.com/Homie-Improvement/miru/pull/194","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Clear-text delivery exposes a credential that can change other companies' projections.","finding_evidence":"With `TIMEKEEPING_PROJECTION_URL=http://<remote host>`, `HttpEndpoint#post` sends the bearer token and entry payload without encryption. A network observer can capture the token and submit forged snapshots. `Internal::V1::BaseController` accepts this shared token, and `ProjectionConsumer` permits updates when their generation, company references and version pass its checks. plan item 9 assigns this risk to the operator, but company users bear the risk of exposed entries and altered reports. The ticket requires service authentication, not clear-text transport. Replace disclosure alone with an enforced protected transport policy for remote delivery, and test that an unprotected remote URL sends no token or snapshot.","recorded_resolution":"fixed","recorded_correction":"The endpoint now refuses a plain-http URL unless the host is loopback, so no token or snapshot goes out. A spec checks that no request is made for such a URL.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:10:37.435Z","answer_recorded_at":"2026-10-26T21:12:37.799Z","first_independent_review_at":"2026-10-26T22:55:37Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"ba758a8adb562a0adc5c465ecb9b88f480afd247279dee92a18097be99aa048e","record_sha256":"8aba70e6b015a4e88b8f2c0206ede8e7034849e4235533a48e1e3e1709f95faf"},
    {"record_id":"author-pr194-plan-002","pr":194,"pr_url":"https://github.com/Homie-Improvement/miru/pull/194","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Protect customer snapshots and the service token before allowing network delivery.","finding_evidence":"R10 sends snapshots with `TIMEKEEPING_SERVICE_TOKEN`, but plan item 9 permits clear-text HTTP and assigns the risk to the operator. Customers bear the exposure: snapshots contain their entry notes and metadata. In `gems/timekeeping_relay/lib/timekeeping_relay/http_endpoint.rb`, `post` sends the bearer token and batch with TLS only when the scheme is `https`. Anyone who intercepts HTTP can retain those records and reuse the token against the internal controllers. `Internal::V1::BaseController` accepts that token without a company restriction. Disclosure and a private-network recommendation do not prevent this exposure. The ticket requires service identity, not unencrypted transport. Require HTTPS for non-local delivery in the new wrapper, reject unsafe URLs before sending, and test that rejection. Keep a","recorded_resolution":"fixed","recorded_correction":"Customers would bear the exposure, so a doc note is not enough. The new endpoint requires https for any host that is not loopback and refuses other URLs before sending; a spec proves it.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:10:37.435Z","answer_recorded_at":"2026-10-26T21:12:37.799Z","first_independent_review_at":"2026-10-26T22:55:37Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"3a3f8a1d3f0cc9561d31e454ff23c6c831930993acb00ab1564548b59070c387","record_sha256":"a80febdd623c18fb668dc28858fe5556043586ad5e13d86117dd609b6224669e"},
    {"record_id":"author-pr194-plan-003","pr":194,"pr_url":"https://github.com/Homie-Improvement/miru/pull/194","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The invalid-token test needs separate sender and receiver credentials.","finding_evidence":"The end-to-end test sends requests into the same Rails application through WebMock, as plan item 10 specifies. However, U5 describes changing TIMEKEEPING_SERVICE_TOKEN only in a separate relay process. This test has no separate process, and plan item 4 makes the sender read the same token setting that the controller accepts. Changing that setting for both sides would test matching credentials, not rejection. Specify how the test gives the endpoint wrapper a wrong token while keeping the controller's expected token unchanged. Keep this separation in test code so the test proves that rejected requests leave the outbox unsent.","recorded_resolution":"fixed","recorded_correction":"The test now separates the two sides inside one process. The controller keeps the token from the environment, and only the stubbed sender endpoint gets a wrong token.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:10:37.435Z","answer_recorded_at":"2026-10-26T21:12:37.799Z","first_independent_review_at":"2026-10-26T22:55:37Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9d392ee76dcaab625da21a8eda542b6c5b927e8218d7d1046a29a21d1d9d1fef","record_sha256":"b00482a35e4542b0102e598ca31c4a346bc29d824102ea110e3e64d63884e94a"},
    {"record_id":"author-pr194-plan-004","pr":194,"pr_url":"https://github.com/Homie-Improvement/miru/pull/194","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The negative-zero source test asks the reader to recover information absent from jsonb.","finding_evidence":"U2 requires -0.0 to retain identical bits after storage through LocalOutboxEntry. Its payload column is jsonb, which stores numeric zero without the negative sign. app/services/timekeeping/local_owner.rb writes duration as a JSON number in write_outbox and json_value. Reading the raw payload text cannot recover that lost sign. Separate preservation of stored numeric values from preservation of the original Float bits. Resolve negative zero at the writer and storage boundary if it must be preserved, or document this edge case and remove the impossible source-only assertion.","recorded_resolution":"fixed","recorded_correction":"Agreed: Postgres numeric drops the sign of -0.0, so the reader cannot get it back. The plan removes that assertion and records the edge case.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:10:37.435Z","answer_recorded_at":"2026-10-26T21:12:37.799Z","first_independent_review_at":"2026-10-26T22:55:37Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"141bc5e158208e6e22f76e5754bfc1b9d500cfbcf1acf68067154a19d27204b1","record_sha256":"fe2f8eb603e6f9313744943d99639f314d07c93cd29a68d1dcb1b1eecf08f7be"},
    {"record_id":"author-pr194-plan-005","pr":194,"pr_url":"https://github.com/Homie-Improvement/miru/pull/194","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The receiver still converts the smallest duration to zero.","finding_evidence":"For a duration of `5e-324`, plan item 3 avoids `BigDecimal#to_f` because it can return zero. The HTTP receiver recreates that conversion: `app/controllers/internal/v1/timekeeping/entry_snapshots_controller.rb` parses decimals as BigDecimal, then `app/services/timekeeping/snapshot_batch.rb:58` calls `value.to_f`. `ProjectionConsumer` writes that value and returns 200, so the relay marks the original snapshot sent despite the changed duration. Keeping the consumer unchanged leaves this failure in the new delivery path. Preserve the duration at the receiver's conversion step and add an HTTP end-to-end bit-pattern assertion for `5e-324`, not only a source assertion.","recorded_resolution":"fixed","recorded_correction":"Same cause as the first finding. The receiver's duration cast now converts from the decimal text, and the end-to-end test checks the bits of 5e-324 stored in timesheet_entries.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:10:37.435Z","answer_recorded_at":"2026-10-26T21:12:37.799Z","first_independent_review_at":"2026-10-26T22:55:37Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"001ad81f40153f7699fd2a6b1dcb873bbb557278611fe2d2025192742f2b010c","record_sha256":"64935bb317db261ecac49f0f5439980b854d173fbe1439b1b09c41cba3bf0db2"},
    {"record_id":"author-pr194-plan-006","pr":194,"pr_url":"https://github.com/Homie-Improvement/miru/pull/194","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The unchanged projection parser loses the subnormal duration that the source preserves.","finding_evidence":"plan item 3 avoids BigDecimal#to_f because it can turn 5e-324 into zero, but the receiving path still uses that conversion. app/controllers/internal/v1/timekeeping/entry_snapshots_controller.rb parses numbers as BigDecimal. app/services/timekeeping/snapshot_batch.rb then converts duration with value.to_f. ProjectionConsumer writes that converted value and returns 200, so the relay marks the snapshot sent although the projection contains zero. Preserving bits only in LocalOutboxSource cannot prevent this outcome. Revise the unchanged-consumer boundary to address the receiving conversion, and test 5e-324 through the real endpoint into timesheet_entries.","recorded_resolution":"fixed","recorded_correction":"Confirmed: the receiver parses decimals as BigDecimal and the duration cast calls to_f. The plan now fixes that cast and tests 5e-324 through the real endpoint into timesheet_entries.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:10:37.435Z","answer_recorded_at":"2026-10-26T21:12:37.799Z","first_independent_review_at":"2026-10-26T22:55:37Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d14465d5f1be2774fa81b845b9f4c96ffdc41994feef1732a5858e349cad3783","record_sha256":"e1358ea7c95625aec5392420d81e3a4f6cf86cf9b51bcc10b3df2c1b446cf7bc"},
    {"record_id":"author-pr194-plan-007","pr":194,"pr_url":"https://github.com/Homie-Improvement/miru/pull/194","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The wrong-token integration test needs separate sender and receiver credentials.","finding_evidence":"The end-to-end test routes HTTP into Rails.application within the same process. U5 proposes changing TIMEKEEPING_SERVICE_TOKEN in that process to test refusal. However, LocalSnapshotEndpoint.configured reads that environment value, and app/controllers/internal/v1/base_controller.rb reads the same value when authenticating the request. Changing it before running the job changes both credentials, so the request succeeds. Construct the endpoint with an incorrect token while keeping the controller's environment token unchanged, then inject that endpoint into the job. This tests refusal without bypassing authentication.","recorded_resolution":"fixed","recorded_correction":"The refusal test now stubs the endpoint factory to return an endpoint with an incorrect token, while the controller reads the unchanged environment token.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:10:37.435Z","answer_recorded_at":"2026-10-26T21:12:37.799Z","first_independent_review_at":"2026-10-26T22:55:37Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9d427bb64159c0f7d36fcdedd6899550b0a1a6a7a5128a4c58f11b77c420f774","record_sha256":"338d3a7a9c52513d4c47a0f4362d0ed9063c742ab7cdff9e69303ae7107a1e63"},
    {"record_id":"author-pr194-plan-008","pr":194,"pr_url":"https://github.com/Homie-Improvement/miru/pull/194","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The wrong-token test changes the receiver's expected token too.","finding_evidence":"The proposed in-process HTTP test shares ENV with the controller. If U5 changes `TIMEKEEPING_SERVICE_TOKEN` before running the job, both `LocalSnapshotEndpoint.configured` and `Internal::V1::BaseController#authenticate_timekeeping_service!` read that changed value. The supposedly wrong token therefore authenticates, and the snapshot can be applied. Keep the receiver's expected token fixed and inject a differently configured sender endpoint for this case. Assert a 401 response, an unchanged projection, and an unsent outbox row. This makes the test exercise rejected authentication rather than changing both sides of the credential comparison.","recorded_resolution":"fixed","recorded_correction":"The wrong-token test now keeps the controller's token setting fixed and gives the job an endpoint built with a different token. It asserts a 401, an unchanged projection and an unsent row.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:10:37.435Z","answer_recorded_at":"2026-10-26T21:12:37.799Z","first_independent_review_at":"2026-10-26T22:55:37Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"410a152b5331896930387ba6a56034917cec355ba3b60fc0daaa2749fd5bcc96","record_sha256":"2f6d0bf8ed2a00cfebbe9f13c6bd0aefd668023b3d7e4aae5587c0efb57de941"},
    {"record_id":"author-pr196-build-001","pr":196,"pr_url":"https://github.com/Homie-Improvement/miru/pull/196","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Local signal read failures now exit zero instead of failing the task","finding_evidence":"Before this change, a database fault while reading stale_generation made timekeeping:alerts exit non-zero, so the scheduler saw the failure. Now read_local_signals rescues the error for all four local signals, prints unavailable lines and exits 0. A database outage that blinds every local alert looks like a successful run. This also departs from the repository's own pattern: the metrics path, and the learning behind it, print what they could evaluate and then exit non-zero.","recorded_resolution":"fixed","recorded_correction":"gather now returns failed_local_reads. The task prints every line it can, then exits non-zero and names the failed reads, the same way it handles a failed metrics read. The unreadable-history task spec checks the exit status.","correction_target":"code_commit","source_path":"app/services/timekeeping/alert_signals.rb","source_line":66,"fix_commit":"4f91cb0176a43624c51a36cbb3d22232e6745e83","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/4f91cb0176a43624c51a36cbb3d22232e6745e83","finding_recorded_at":"2026-10-26T22:33:18.821Z","answer_recorded_at":"2026-10-26T23:03:48.784Z","first_independent_review_at":"2026-10-27T00:17:38Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"da1dcf6bb574ee3f25e4d80762fa9eceaf8070ae1db40fef44e98295c00f679c","record_sha256":"4d7f27e89c5ddcb95b79b1aa9c594b8a2dd2391d533e61b6118d472bad4a758e"},
    {"record_id":"author-pr196-build-002","pr":196,"pr_url":"https://github.com/Homie-Improvement/miru/pull/196","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Lost refusal record only logged, not reported to error tracking","finding_evidence":"The timekeeping_prepare_refusals row is the only source for the prepare_refusal alert. When insert_all! fails, the refusal disappears from the alert and only a log line records it; error tracking never hears of it, unlike with the sibling record_failure. The StandardError rescue would also hide a programming error, such as a KeyError from conflict.fetch(:reason).","recorded_resolution":"fixed","recorded_correction":"A failed refusal-record write is now reported through Rails.error.report with the company and operation ids, the same way record_failure reports. The write-failure spec checks that report.","correction_target":"code_commit","source_path":"app/services/invoices/operation_coordinator.rb","source_line":163,"fix_commit":"4f91cb0176a43624c51a36cbb3d22232e6745e83","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/4f91cb0176a43624c51a36cbb3d22232e6745e83","finding_recorded_at":"2026-10-26T22:33:18.821Z","answer_recorded_at":"2026-10-26T23:03:48.784Z","first_independent_review_at":"2026-10-27T00:17:38Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"e007132403daac007997e6792ab025d5815de3210067fcc033e99c67360e9758","record_sha256":"12f2508c64fdb2a40ce1286071fa644307d0170200f30fa1f413212ae710eec8"},
    {"record_id":"author-pr196-build-003","pr":196,"pr_url":"https://github.com/Homie-Improvement/miru/pull/196","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Three coordinator refusal-record tests are pasted in twice","finding_evidence":"Two describe blocks share the name \"prepare refusal record\", and each redefines the same helpers. Three cases are written twice: rescue-and-commit (369/560), savepoint then outer commit (509/597), and write failure (478/620). A change to the refusal-timing contract has to be made in every copy, and a copy that gets missed drifts unnoticed. Some pairs assert slightly different things, so a careless merge could lose a real assertion.","recorded_resolution":"fixed","recorded_correction":"I removed the three later copies and kept the versions with the stricter checks (created_at, and the count read while the outer transaction is open). The leftover savepoint example now has its own name.","correction_target":"code_commit","source_path":"spec/services/invoices/operation_coordinator_spec.rb","source_line":560,"fix_commit":"4f91cb0176a43624c51a36cbb3d22232e6745e83","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/4f91cb0176a43624c51a36cbb3d22232e6745e83","finding_recorded_at":"2026-10-26T22:33:18.821Z","answer_recorded_at":"2026-10-26T23:03:48.784Z","first_independent_review_at":"2026-10-27T00:17:38Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"09f879715bbb62d814929f99349f9ede98629ac100692d0f314e6a625f2f8d7b","record_sha256":"1739fe14894656bf77ec595b705f7b78a80fb76387fe4a0e7eb203d94ef26d32"},
    {"record_id":"author-pr196-plan-001","pr":196,"pr_url":"https://github.com/Homie-Improvement/miru/pull/196","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A refused prepare can remain invisible when its caller catches the error and commits.","finding_evidence":"The plan records refusals only after rollback and leaves the commit case as an open question. If a caller catches `OperationErrors::Refused` and commits its surrounding transaction, the refusal leaves no record. Operators then receive no `prepare_refusal` alert despite an `on_another_invoice` refusal. Matching the existing `record_failure` behavior does not satisfy the alert requirement: the signal describes a refused prepare, not a rolled-back invoice transaction. Define recording for both transaction outcomes without changing the caller's result. Add a command-level test that catches the refusal, commits, and verifies the company's alert.","recorded_resolution":"fixed","recorded_correction":"Recording now covers both transaction outcomes without changing the caller's result, and a command-level test catches the refusal, commits, and checks the company's alert.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:36:44.983Z","answer_recorded_at":"2026-10-26T21:37:38.060Z","first_independent_review_at":"2026-10-27T00:17:38Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"86441bac9bf4308fcd9b1479ba7abb871f638ed924201d86fc36e3d8715b99ea","record_sha256":"24f76e9e9c72123cba495a8c2dd56f98a0fe6737ac11bce046dab8cf70bc18d7"},
    {"record_id":"author-pr196-plan-002","pr":196,"pr_url":"https://github.com/Homie-Improvement/miru/pull/196","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Record a prepare refusal independently of whether the caller rolls back.","finding_evidence":"plan item 1 ties refusal recording to invoice rollback, but a refused prepare has already occurred regardless of the transaction's final outcome. In app/services/invoices/operation_coordinator.rb:71-72, commit only delivers a commit item; only rollback starts the failure callback. If a caller rescues OperationErrors::Refused inside its transaction and commits, the planned record is never written. Operators then receive no prepare_refusal alert for a real on_another_invoice refusal. The Open Questions section acknowledges this case but leaves the recording contract unresolved. Keep invoice failure settlement unchanged, but schedule the captured refusal for recording once after the outermost transaction ends, on either commit or rollback. Add a test that rescues the refusal, commits, and verifies the command reports it.","recorded_resolution":"fixed","recorded_correction":"Failure settlement is unchanged; the captured refusal is recorded once after the outermost transaction ends on either outcome, and the open question about this case is removed.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:36:44.983Z","answer_recorded_at":"2026-10-26T21:37:38.060Z","first_independent_review_at":"2026-10-27T00:17:38Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9c04103f7d82fa916343929779c0c2fc4b031172b33b449a3b284e42b2727a4c","record_sha256":"5e4f8f1135741e998dcb8a1ed8da5bd98a5f3dbd675b35af3c0da8312f2acb59"},
    {"record_id":"author-pr196-plan-003","pr":196,"pr_url":"https://github.com/Homie-Improvement/miru/pull/196","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Recording only after rollback loses refusals when the caller rescues the error and commits.","finding_evidence":"plan item 1 ties the refusal record to rollback, although the refusal occurs when prepare answers. Consider a caller that catches `Invoices::OperationErrors::Refused` inside its transaction and then commits. In app/services/invoices/operation_coordinator.rb:69-70, commit only delivers a commit item; only rollback starts `after_outermost_transaction`. Under this plan, neither path writes the refusal in that case. The command therefore reports no prepare-refusal alert despite receiving `on_another_invoice`. The Open Questions section acknowledges this case but leaves it unresolved. Record the captured refusal after the outermost transaction ends on either commit or rollback, independently of intent settlement. Add a command-level test for a rescued refusal followed by commit.","recorded_resolution":"fixed","recorded_correction":"The refusal record is now written once after the outermost transaction ends, on commit or rollback, apart from intent settlement, and a command test covers a rescued refusal that commits.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:36:44.983Z","answer_recorded_at":"2026-10-26T21:37:38.060Z","first_independent_review_at":"2026-10-27T00:17:38Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"6be7b9f347704a7e138ba515c5e9bdd05df98696ff66add83dd24e0ce7d1f609","record_sha256":"af9e42c5ee17c762d9e4916cddddbf0912b5397556f5d7d7f131cdf175f9e0a3"},
    {"record_id":"author-pr196-plan-004","pr":196,"pr_url":"https://github.com/Homie-Improvement/miru/pull/196","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The first unit requires a test result that only the second unit can deliver.","finding_evidence":"The first unit requires `gather` to list only `ownership_moving` from `NO_SOURCE`. However, app/services/timekeeping/alert_signals.rb currently lists four names. This unit removes only the two event-history signals; `prepare_refusal` remains unavailable until the second unit adds its table and reader. The first unit therefore cannot pass its stated verification on its own. Keep both `ownership_moving` and `prepare_refusal` in its expected result, then move the ownership-only expectation to the second unit.","recorded_resolution":"fixed","recorded_correction":"The first unit now expects ownership_moving and prepare_refusal to stay unavailable; the ownership-only expectation moved to the second unit.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:36:44.983Z","answer_recorded_at":"2026-10-26T21:37:38.060Z","first_independent_review_at":"2026-10-27T00:17:38Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"db899a2a72f698c7b9e766f04bfe6c0dbd5022da1e423e9710c20f5d8c55c374","record_sha256":"e4afbdce4747d6033e9c86c49f47c567d0db36ef3789747d3f17f70b5a4d1953"},
    {"record_id":"author-pr196-plan-005","pr":196,"pr_url":"https://github.com/Homie-Improvement/miru/pull/196","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The first unit requires the unavailable-signal state that only the second unit creates.","finding_evidence":"The first unit removes only reconciliation and snapshot signals from `NO_SOURCE`, so `prepare_refusal` must remain unavailable until the second unit adds its producer. However, the first unit's tests and verification already require only `ownership_moving` to remain unavailable. Its standalone verification therefore cannot pass while preserving honest reporting of prepare refusals. Keep both names in the first unit's expected unavailable list, then move the ownership-only expectation into the second unit.","recorded_resolution":"fixed","recorded_correction":"Same correction: prepare_refusal stays reported as unavailable until the second unit adds its record and reader, which then owns the ownership-only expectation.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T21:36:44.983Z","answer_recorded_at":"2026-10-26T21:37:38.060Z","first_independent_review_at":"2026-10-27T00:17:38Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"102629aeddc74cd43166d3e4db712a9a301551ba3a155acfae5fd16097b53a7a","record_sha256":"f9bb8288d6c1d381f5ccaf61ced855fd11d8e859a02cc548be99b65d4771ccaa"},
    {"record_id":"author-pr197-build-001","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Forward rake tasks refuse a handoff in progress (plan said resume: true)","finding_evidence":"This is an unrequested behaviour rule from the reverse requirements check. Plan U5 has handoff_cutover call Runner.run(direction: :cutover, resume: true, abort: false). All three forward tasks pass resume: false instead, so re-running one mid-handoff is refused and the operator must use handoff_resume with DIRECTION. No spec pins either value.","recorded_resolution":"fixed","recorded_correction":"I kept resume: false on the forward tasks, so only handoff_resume continues a handoff. A new rake example now pins resume: false on the forward tasks and resume: true on handoff_resume.","correction_target":"code_commit","source_path":"lib/tasks/timekeeping.rake","source_line":48,"fix_commit":"426a4efbe8c0752729232839b8d8d037fc347c41","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/426a4efbe8c0752729232839b8d8d037fc347c41","finding_recorded_at":"2026-10-26T22:41:37.448Z","answer_recorded_at":"2026-10-27T00:08:58.055Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"fc88bb17acee7d3983999667ec0a1deeb535c5f59126ee2b15e608305ef1462b","record_sha256":"8bbc6fa956f15c0ccc15049fec8ccd911ebf1534ed9923f185e61d614b546bda"},
    {"record_id":"author-pr197-build-002","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"Model spec deletes six handoff_begun tests without stated reason","finding_evidence":"The model code did not change, but nothing in the suite now pins handoff_begun? for a missing row or for each column on its own. Narrowing it to generation > 1 || !handoff_step.nil? would pass every spec, and seed and demo guards could then run against a handoff that has begun, which is the state the new runner's abort creates.","recorded_resolution":"fixed","recorded_correction":"I put back the six earlier handoff_begun?, unless_handoff_begun and refuse_if_handoff_begun! examples beside the new ones. The model spec passes with them.","correction_target":"code_commit","source_path":"spec/models/timekeeping_ownership_spec.rb","source_line":163,"fix_commit":"426a4efbe8c0752729232839b8d8d037fc347c41","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/426a4efbe8c0752729232839b8d8d037fc347c41","finding_recorded_at":"2026-10-26T22:41:37.448Z","answer_recorded_at":"2026-10-27T00:08:58.055Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"41c87ff3a0a1f364cc6bf686d72c4666921cd906314641d37a77d93b8096960c","record_sha256":"a0de24c668b444c62b4c811c1a93df53e72f1d4f0c4e904e46016f648d523693"},
    {"record_id":"author-pr197-build-003","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Resume state stored under ephemeral log/ directory","finding_evidence":"/app/log is not a mounted volume in config/deploy.yml. A handoff_resume run in a fresh Kamal container finds no money_before.json or checkpoint.json and fails with Unreadable, so the operator can only abort and restart, which defeats R8/R20.","recorded_resolution":"fixed","recorded_correction":"The default report directory is now storage/timekeeping_handoff, the persisted volume, and the resume task's description says it needs those files.","correction_target":"code_commit","source_path":"lib/timekeeping/handoff/runner.rb","source_line":41,"fix_commit":"426a4efbe8c0752729232839b8d8d037fc347c41","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/426a4efbe8c0752729232839b8d8d037fc347c41","finding_recorded_at":"2026-10-26T22:41:37.448Z","answer_recorded_at":"2026-10-27T00:08:58.055Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"e0979d99f2c0c32b672d618bd691234befce273aa4750bcd33007c55e2a9b7d4","record_sha256":"26ee2188a976830cc3c77e163aa2c38e9afbe81c71029f27de1994aca4ca6bbd"},
    {"record_id":"author-pr197-plan-001","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A failed admission call after flip leaves the new owner fenced without a resumable step.","finding_evidence":"plan item 11 commits the owner change and clears `handoff_step` before calling the incoming adapter's `admit`. If that call fails during rollback, the monolith owns the entries but still rejects writes. A retry with `direction: :rollback` fails plan item 2's direction check, and plan item 9 refuses an idle abort with monolith ownership. Thus a temporary integration failure becomes an outage that the rake commands cannot repair. Preserve a way to retry admission for the committed owner without starting another handoff or raising the generation. Test a failed admission call after the flip commits, followed by recovery through a fresh runner.","recorded_resolution":"fixed","recorded_correction":"A resume naming the direction that just flipped now re-sends the admit call to the current owner at the current generation. It writes nothing to the row and is tested after a failed admit.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:23:44.456Z","answer_recorded_at":"2026-10-26T20:26:44.831Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f201b793280ac84ef5d13441f7e777c403182d8fd0c970f299e1c824eae61aac","record_sha256":"ea67c1579a049647907b81493511028a00b521086362e12fc1cadb4dcb11a4e6"},
    {"record_id":"author-pr197-plan-002","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A failed admission call after the flip leaves no resumable recovery step.","finding_evidence":"Under plan item 11, the ownership transaction can commit and the incoming owner's admit request can fail before it reaches the service. The new owner then remains fenced, but handoff_step is null. plan item 2 rejects resuming the original direction because the owner has changed. plan item 9 only repairs idle Timekeeping through abort; it does not repair a monolith owner after rollback. Operators can therefore finish rollback with neither owner admitting writes and no runner action that retries its final admission call. Retain or derive a recoverable admission obligation after the flip, and retry it without another generation increase or reverse handoff. Test a failed admit request and a process restart immediately after the flip commits.","recorded_resolution":"fixed","recorded_correction":"Same fix as the other admit-failure finding. A resume of the completed direction retries the admit call without raising the generation, and a test covers a restart right after the flip.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:23:44.456Z","answer_recorded_at":"2026-10-26T20:26:44.831Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"50a1385053679fa52cb4e2cec5dac1c46c45689bb1582abf335e7ef13c95b86e","record_sha256":"8e3f471d3b23074f641ed03f3f0120e2c34a607d3f09d447a2743d177099a1ed"},
    {"record_id":"author-pr197-plan-003","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A timed-out cutover can leave monolith writes fenced with no usable abort.","finding_evidence":"plan item 5 fences the outgoing monolith before the ownership transaction. If that transaction times out, the monolith remains fenced but `handoff_step` remains null. plan item 9 then refuses abort because the owner is monolith. Operators cannot restore writes through the promised abort command while the lock contention persists. Extend the timeout recovery decision to cover the outgoing monolith, not only Timekeeping. Test a cutover timeout followed by abort, and prove that the monolith admits writes at the unchanged generation without admitting Timekeeping.","recorded_resolution":"fixed","recorded_correction":"Same fix as the earlier timeout finding. A test aborts after a cutover timeout and shows the monolith admits writes at the unchanged generation while Timekeeping is not admitted.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:23:44.456Z","answer_recorded_at":"2026-10-26T20:26:44.831Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"ee38a6e0cb31f424bb4a9e2c1b465fe6c720646eb83539fb881eb253bb37ee94","record_sha256":"3b2671acbd85c8f5f8a6d0e890cc4e9c8a70bd64b5c20136469e44318eeeda2a"},
    {"record_id":"author-pr197-plan-004","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Correction writes make the planned legacy checkpoint reload fail.","finding_evidence":"plan item 8 assumes that repeating the checkpoint load and correction writes is harmless. The existing stores reject that sequence. LegacySource copies version 0 unchanged. The correction writer then raises corrected entries to version 1 in timekeeping/app/services/timekeeping/handoff/correction_snapshots.rb:101. A later load rejects the original version-0 checkpoint in gems/timekeeping_handoff/lib/timekeeping/handoff/table_store.rb:201. This blocks recovery after corrections commit but before checkpoint completion is recorded. It also blocks the required retry after an aborted first cutover, because the incoming store retains those version-1 entries. Define a recovery protocol that accounts for correction-induced versions without weakening protection against overwriting newer user edits. Prove both cases through","recorded_resolution":"fixed","recorded_correction":"Same fix as the correction-version finding. The legacy load copy raises versions to the highest stored or correction version, and both retry cases are tested.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:23:44.456Z","answer_recorded_at":"2026-10-26T20:26:44.831Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"16bb664a911b813e4105e01e856cc7b7eb078609728153c2fce7242982fcf2f8","record_sha256":"fe89dc39490583019d4d2fd68641535c43266abb07e13b3d5bb8ad8c3f31ea65"},
    {"record_id":"author-pr197-plan-005","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Put completion checks in the declarative step table.","finding_evidence":"plan item 1 defines rows as `{name:, action:, recorded_as:}`, but none identifies the check that establishes completion. A progress label does not establish that a step finished. This leaves the interpreter's central decision implicit in individual action methods, making the table incomplete as the source of step rules. Replace redundant metadata where possible with an explicit completion-check reference, and state that the interpreter records progress only after that check passes.","recorded_resolution":"fixed","recorded_correction":"Each table row now names its completion check. The interpreter records a step only after that check passes.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:23:44.456Z","answer_recorded_at":"2026-10-26T20:26:44.831Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"84ce9a73308d49ebf556eb7fe72db00612bb4c6c378b1688b0f65fc54194f445","record_sha256":"a82b35507b46c36441db9f2469ce015db0c5331a02536e6b93247a4785b021d5"},
    {"record_id":"author-pr197-plan-006","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Separate the persisted freeze boundary from completion of the drain.","finding_evidence":"plan item 6 drains through P before persisting P and `handoff_step = 'freeze'`. However, `app/models/timekeeping_ownership.rb:99-102` rejects previous-generation snapshots until that state is persisted. A drain that follows this contract cannot apply queued snapshots during rollback or re-cutover. The operator remains fenced without completing the handoff. Define how the runner publishes the frozen source and its position before the consumer drains, while recording step completion only afterward. Resolve this state-model boundary against the existing acceptance predicate rather than leaving the drain adapter to bypass it. Add a test whose drain uses `snapshot_generation_accepted?` with a queued previous-generation snapshot.","recorded_resolution":"fixed","recorded_correction":"The captured position is now stored in its own transaction before the drain. The freeze step is recorded only after the drain is done, and a test drains through the existing acceptance check.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:23:44.456Z","answer_recorded_at":"2026-10-26T20:26:44.831Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"ea3fcb3ec33a9787c41685079fdbd71c1f97365c7af95e66484be2e132cda06a","record_sha256":"eed79ab844ff775acf54a629f6a3c1ca1db5e4a85d0d9a57e1e9c8b5409c4534"},
    {"record_id":"author-pr197-plan-007","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The freeze step omits the call that stops commit and abort delivery.","finding_evidence":"plan item 6 captures the outgoing store's highest outbox position and drains through it, but never stops commit and abort delivery. plan item 4 supplies only admission, settle and drain adapters. The existing TimekeepingStore exposes admission fencing, not a separate operation to end settlement. Closing admission cannot substitute for this action: settlement must remain possible during step 2. Specify which integration stops commit and abort before capturing the final position, including on resume and abort. Test that order with the injected integrations. Otherwise the runner can record a frozen source without enforcing the boundary that protects the copied data.","recorded_resolution":"fixed","recorded_correction":"The settle integration now has a call that stops applying commit and abort. Freeze calls it before it captures the position, and a test checks the order.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:23:44.456Z","answer_recorded_at":"2026-10-26T20:26:44.831Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"289ed5ec16fa2a387329c82d4cd462e9ecdcb5a0a6e6c4ccede75a087f8312dd","record_sha256":"84f16a50ef04881141d1096ad8a1c1e69419446d74942930482f97d363f673d0"},
    {"record_id":"author-pr197-plan-008","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The planned freeze order prevents the final snapshots from reaching the projection.","finding_evidence":"Consider rollback from generation 2 with an undelivered generation-2 snapshot. Step 1 raises ownership to generation 3. plan item 6 drains before recording 'freeze' and P. However, app/models/timekeeping_ownership.rb:99 accepts the previous generation only after 'freeze' is recorded. app/services/timekeeping/projection_consumer.rb:91 applies that check and returns 409 otherwise. The drain cannot finish, so neither resume nor abort can reopen writes. Define how the runner publishes permission to drain through P before delivery, without falsely recording a completed drain. Test with the real projection consumer and an outstanding previous-generation snapshot, rather than only a fake drain.","recorded_resolution":"fixed","recorded_correction":"Freeze now stores the outbox position before draining, and the acceptance check reads that position. A test drains an outstanding previous-generation snapshot through the real consumer's write path.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:23:44.456Z","answer_recorded_at":"2026-10-26T20:26:44.831Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"47e1fd051bb45c5be9620938e3a64eaf70ed83dd6f78d5277cf271b791768973","record_sha256":"0d3f63ac598efb84a690aa1d1063238aab7063040f93539bbe8a1216a1ba9ad1"},
    {"record_id":"author-pr197-plan-009","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The planned freeze sequence cannot drain previous-generation snapshots through the existing consumer.","finding_evidence":"plan item 6 drains through the captured outbox position before recording that position and handoff_step = 'freeze'. During that drain, the row still records 'settle'. app/models/timekeeping_ownership.rb:102 rejects previous-generation snapshots unless freeze is recorded and final_outbox_position is present. app/services/timekeeping/projection_consumer.rb:91 uses that check and returns 409 for a refused batch. After step 1 raises the generation, an outgoing owner's undelivered snapshots therefore cannot drain through this consumer. The handoff remains fenced instead of completing. Resolve how captured-position eligibility becomes available before draining while completion remains recorded only after the drain. The stated goal currently forbids changing the eligibility predicate, so the plan must settle this prerequisite rather than","recorded_resolution":"fixed","recorded_correction":"Confirmed in the ownership model and projection consumer. Step 1 now clears the stored position, freeze writes it before draining, and the acceptance check keys on that captured position.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:23:44.456Z","answer_recorded_at":"2026-10-26T20:26:44.831Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"1bba5a7a49734a89a0227cbc1e0618361403f3c00a4c2bfff38def48135bbe23","record_sha256":"6802639291578f10273bc1b85313fa220ac79251e7d6cf5416d20f96b1ac558d"},
    {"record_id":"author-pr197-plan-010","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The resume rules require checkpoint metadata before the checkpoint step has run.","finding_evidence":"A run interrupted after step 3 has no `checkpoint.json` because plan item 8 creates it during step 4. However, plan item 8 says that on resume the runner re-exports with the stored capture time, and plan item 7 refuses when a required file is missing. Applied together, these rules stop the required resume after step 3 and leave writes fenced. Distinguish a checkpoint step that has not started from one whose metadata was already persisted. Permit the former to capture and persist its first timestamp; require the latter to reuse it. State how the runner distinguishes those states, and test both paths.","recorded_resolution":"fixed","recorded_correction":"A missing checkpoint metadata file now means the checkpoint step has not started, so the runner captures and stores a fresh time. A file that is present must be reused. Both paths are tested.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:23:44.456Z","answer_recorded_at":"2026-10-26T20:26:44.831Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9d76c241cf0c75357fe77a8427ae29f2edb13681172505bf92198fe32e4ac738","record_sha256":"a8585bcfb305e6b3ce147dbd914b5d23214cf964df9bf157e387d8cb369944b1"},
    {"record_id":"author-pr197-plan-011","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The step table drops the required completion checks.","finding_evidence":"R3 retains the ticket's requirement for a completion check in each table row. plan item 1 instead defines rows as `{name:, action:, recorded_as:}` and says the interpreter calls each action. A progress label does not establish that a step completed. Add the completion predicate to each row and specify how the interpreter uses it before recording progress. Include tests that a failed completion check leaves the step unrecorded. This keeps the required checks explicit rather than leaving future actions to decide independently what completion means.","recorded_resolution":"fixed","recorded_correction":"Table rows now carry a completion check. A test shows that a failed check leaves the step unrecorded and resumable.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:23:44.456Z","answer_recorded_at":"2026-10-26T20:26:44.831Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"a064f5e5e1e5e971e8a535701621496b6fb73d1467d128cbc7acd1603f397ce1","record_sha256":"89217e891341bd80c8eedf2d0256e2e66584176c5506520755ac827cd15d2084"},
    {"record_id":"author-pr197-plan-012","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The timeout abort rule overlooks the monolith fence applied before the transaction.","finding_evidence":"A timed-out cutover can leave the current owner unable to accept writes, while abort refuses to restore admission. plan item 5 fences the outgoing monolith before the ownership transaction. If that transaction times out, `handoff_step` remains null, but the separate fence remains active. plan item 9 refuses abort when the monolith owns the entries because it considers only the incoming Timekeeping fence. This contradicts the promised abortable timeout state. Define how abort restores the outgoing monolith in this state, or make its fence atomic with the ownership update. Add a test that times out after fencing the monolith, then aborts and verifies that the monolith accepts writes again.","recorded_resolution":"fixed","recorded_correction":"With no step recorded, abort now re-admits the outgoing owner, monolith or Timekeeping, at the unchanged generation. This is tested after a timeout once the monolith is fenced.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:23:44.456Z","answer_recorded_at":"2026-10-26T20:26:44.831Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9b9316cf5337f96dd2ab1164b8f1c7ca6daeab533fa34fae7c51294a1dee70ba","record_sha256":"b53cf2e847f516e94f61119d49c09c3e140c3bc1436a7f9bbdb02ce7fefb2c6a"},
    {"record_id":"author-pr197-plan-013","pr":197,"pr_url":"https://github.com/Homie-Improvement/miru/pull/197","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Writing corrections makes a repeated legacy checkpoint load fail.","finding_evidence":"Take a legacy entry at version 0 with a recorded unlocked invoice link. plan item 8 loads its checkpoint, then writes a correction at version 1. timekeeping/app/services/timekeeping/handoff/correction_snapshots.rb raises the stored entry to version 1. If the process stops before recording checkpoint completion, resume loads the version-0 checkpoint again. gems/timekeeping_handoff/lib/timekeeping/handoff/table_store.rb rejects that load because the target holds a newer version. Abort followed by another first-cutover attempt hits the same refusal. Define compatible versions for checkpoint loading and correction writing that preserve monotonic versions and allow both retries. Add interruption coverage after corrections commit but before the progress write, and the required abort-after-load retry.","recorded_resolution":"fixed","recorded_correction":"Confirmed: the correction writer raises versions and the table store refuses older loads. On a legacy source the load copy now carries each entry's highest known version.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:23:44.456Z","answer_recorded_at":"2026-10-26T20:26:44.831Z","first_independent_review_at":"2026-10-27T00:43:39Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"bca0136b064ef6d39d3362d547cc7ea21a1504125a519df096a7baff4e8fe60b","record_sha256":"4f8ed86c4711b0ad7648ca1e7b8f0130c3685b1855e0bad69c3ffa81df144804"},
    {"record_id":"author-pr198-build-001","pr":198,"pr_url":"https://github.com/Homie-Improvement/miru/pull/198","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Appended acceptance describe blocks duplicate existing setup and examples","finding_evidence":"handle_spec.rb now holds two top-level describe blocks for the same endpoint. The second re-declares the company/user/cli_token lets and the before block, writes out headers and payloads four times although the private mcp_headers, initialize_payload and tool_call_payload helpers already build them, and its initialize, whoami Host-forwarding and disallowed-origin examples overlap existing ones. client_spec.rb and server_factory_spec.rb repeat the pattern. A change to MCP headers, protocol version or setup now has to be made in two places per file, the copies can drift, and readers of one unit's tests miss half of them.","recorded_resolution":"fixed","recorded_correction":"I removed the appended describe blocks. Their distinct checks now sit in the existing groups, which build requests with the shared helpers. That applies in the request, client and server factory specs.","correction_target":"code_commit","source_path":"spec/requests/api/v1/mcp/handle_spec.rb","source_line":216,"fix_commit":"ae3e17cbef0ab1cd6c8c5b2ddd6a6c9a1562c035","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ae3e17cbef0ab1cd6c8c5b2ddd6a6c9a1562c035","finding_recorded_at":"2026-10-26T22:42:05.133Z","answer_recorded_at":"2026-10-26T23:05:08.038Z","first_independent_review_at":"2026-10-26T23:19:35Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"b47a994587b95f332b70a0e37ca5d73c3b2ced7fefa73a4dd3e3c795766217fe","record_sha256":"5215066752a10eca9d4fafce0efda0ed16bab1ebb36b2e4d67b71d72bdac04c6"},
    {"record_id":"author-pr198-build-002","pr":198,"pr_url":"https://github.com/Homie-Improvement/miru/pull/198","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Exception reporter specs bypass the SDK, so they don't test the 1.x contract","finding_evidence":"The intent says the exception reporter must keep working on mcp 1.x. The risk the upgrade adds is that 1.x calls exception_reporter at a different point or with different arguments. All three examples call the app's unchanged lambda directly, so they pass the same way on 0.25 and 1.8.1: if the SDK stopped calling the reporter or changed its arity, nothing would fail. The nil-context example proves little, since the lambda never reads server_context. The plan's completion criteria asks for specs that exercise the real calls and fail if the 1.x API breaks.","recorded_resolution":"fixed","recorded_correction":"The reporter spec now makes a tool raise, sends tools/call through the real SDK server, and expects the reporter's log line once with no token. The nil-context example is gone: mcp 1.8.1 always passes a hash.","correction_target":"code_commit","source_path":"spec/initializers/mcp_spec.rb","source_line":14,"fix_commit":"ae3e17cbef0ab1cd6c8c5b2ddd6a6c9a1562c035","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ae3e17cbef0ab1cd6c8c5b2ddd6a6c9a1562c035","finding_recorded_at":"2026-10-26T22:42:05.133Z","answer_recorded_at":"2026-10-26T23:05:08.038Z","first_independent_review_at":"2026-10-26T23:19:35Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"67c3158c2625d515f0367dd985e055f8544e33723030a510e4b1ec1813b31e03","record_sha256":"96dcd2f2ff43cfb3b140da697e8e73442d89fac13c985516bccc38a97ef2fab2"},
    {"record_id":"author-pr198-build-003","pr":198,"pr_url":"https://github.com/Homie-Improvement/miru/pull/198","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Stdio and initializer specs repeat setup and assertions across examples","finding_evidence":"Both stdio examples copy the same block that saves $stdin/$stdout, swaps them, runs the transport under a timeout and restores them, plus the initialize request. The first two initializer examples assert the same '[MCP] RuntimeError: tool exploded' log line. A fix to the global swap, such as also restoring $stderr, would have to be made twice.","recorded_resolution":"fixed","recorded_correction":"The stdio spec now uses one helper for the stdin and stdout swap, the timeout and restoring the streams, and another for the initialize request. Both examples stay. The duplicate initializer examples were folded into one.","correction_target":"code_commit","source_path":"spec/mcp/stdio_transport_spec.rb","source_line":51,"fix_commit":"ae3e17cbef0ab1cd6c8c5b2ddd6a6c9a1562c035","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ae3e17cbef0ab1cd6c8c5b2ddd6a6c9a1562c035","finding_recorded_at":"2026-10-26T22:42:05.133Z","answer_recorded_at":"2026-10-26T23:05:08.038Z","first_independent_review_at":"2026-10-26T23:19:35Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"b1c18a2951ba84e1220328349126e73ac15a3dce05f246a63409e19829a80a04","record_sha256":"3c8f8064694e7c03ab2946d79823dd29b9f7eb0c230db6cf49cc923232d44fa2"},
    {"record_id":"author-pr198-build-004","pr":198,"pr_url":"https://github.com/Homie-Improvement/miru/pull/198","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"The mcp >= 1.2.0 version floor is copied into four example assertions","finding_evidence":"The 'mcp >= 1.2.0' rule lives in the Gemfile pin (~> 1.2) and Gemfile.lock, and bundler-audit enforces the advisory. The diff also hard-codes it in four example assertions with two spellings (Gem.loaded_specs and MCP::VERSION). When the floor next moves for a later advisory, all four copies must be found and changed, and an example would then fail with a version message instead of the behaviour it is named for; inside a mocked client unit test the check has nothing to do with the behaviour under test.","recorded_resolution":"fixed","recorded_correction":"The version floor check now lives only in the server factory example named for the advisory. I removed the copies from the request and client specs.","correction_target":"code_commit","source_path":"spec/services/mcp/client_spec.rb","source_line":110,"fix_commit":"ae3e17cbef0ab1cd6c8c5b2ddd6a6c9a1562c035","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ae3e17cbef0ab1cd6c8c5b2ddd6a6c9a1562c035","finding_recorded_at":"2026-10-26T22:42:05.133Z","answer_recorded_at":"2026-10-26T23:05:08.038Z","first_independent_review_at":"2026-10-26T23:19:35Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"a27452375986a74f56ce5cd406059c81598cb4c967c5d1e187ab19deff300012","record_sha256":"ee99cf4db6d919df4f96cf24915a35136e012a47bdd322c0941ffd6ff6ef85b7"},
    {"record_id":"author-pr198-plan-001","pr":198,"pr_url":"https://github.com/Homie-Improvement/miru/pull/198","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The declared client interface confuses the application wrapper with the SDK client.","finding_evidence":"The plan declares `MCP::Miru::Client#call_tool(tool:, arguments:)`, but `app/services/mcp/miru/client.rb:28` exposes `call_tool(name:, arguments: {})`. The wrapper finds the tool by name, then passes `tool:` to the SDK client internally. `spec/services/mcp/client_spec.rb` tests that distinction, and the document's client examples use `name:`. Declare the wrapper seam with `name:` and keep any required SDK argument changes inside the wrapper. This prevents the upgrade plan from implying an unnecessary change to the application's public interface.","recorded_resolution":"fixed","recorded_correction":"Confirmed in app/services/mcp/miru/client.rb: the wrapper takes `name:` and passes `tool:` to the SDK. The seam now uses `name:`, and SDK argument changes stay inside the wrapper.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T22:00:45.583Z","answer_recorded_at":"2026-10-26T22:01:34.446Z","first_independent_review_at":"2026-10-26T23:19:35Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"613c6a83d528d1ab74610cacc9fd7b225b75eda96276acbba1f40d9a015f004d","record_sha256":"0e7fbf5e3d6348c6c22850e437f8ec648f2fb20877e8d8968847170c23dad89d"},
    {"record_id":"author-pr198-plan-002","pr":198,"pr_url":"https://github.com/Homie-Improvement/miru/pull/198","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The stdio test cannot detect a changed argument requirement on `open`.","finding_evidence":"The planned test passes if `open` exists but requires an argument. Yet `bin/miru-mcp:23` calls `transport.open` without arguments, so that change would break the binary without failing this test. This defeats the stated verification that changing an API's argument requirements makes the new spec fail. Exercise the real `open` call with controlled input and captured output, using end-of-input and a timeout to prevent blocking. Check that an initialize request produces a response. This tests the call that could fail, rather than only the method's existence.","recorded_resolution":"fixed","recorded_correction":"The stdio spec now calls the real `open` with no arguments, as bin/miru-mcp does. It feeds one initialize request then end-of-input, under a timeout, and checks a response is written.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T22:00:45.583Z","answer_recorded_at":"2026-10-26T22:01:34.446Z","first_independent_review_at":"2026-10-26T23:19:35Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c7ca534270ae9b1b7aae2e93562a53fb108c6d5defc76c6c0d2048aa36062fb7","record_sha256":"7192c16304c955b4ec092a58681bd3448a597d67e2f9801bc59154faf7dbdbd6"},
    {"record_id":"author-pr198-plan-003","pr":198,"pr_url":"https://github.com/Homie-Improvement/miru/pull/198","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The stdio test cannot establish the API compatibility that its verification promises.","finding_evidence":"The stdio test only constructs the transport and checks that it responds to `open`. The same unit promises that the test fails when the method's arity changes, but explicitly forbids calling it. A method that now requires an argument would pass this test while `bin/miru-mcp` still calls it without arguments. Exercise the actual call with controlled input that reaches end-of-file, or check the call signature explicitly and narrow the verification claim to what that check proves.","recorded_resolution":"fixed","recorded_correction":"The stdio spec now runs the actual no-argument `open` call with controlled input that ends in end-of-input. The verification claim now matches what the spec proves.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T22:00:45.583Z","answer_recorded_at":"2026-10-26T22:01:34.446Z","first_independent_review_at":"2026-10-26T23:19:35Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"8e7b068d6624e590de75e97778d98a843ec809feb6237ea2f9bd4acdf53fde33","record_sha256":"5ced3e69cf1dccc18e5d7302ec199f20d8d74c797c12f9aada610a49a4ac9551"},
    {"record_id":"author-pr198-plan-004","pr":198,"pr_url":"https://github.com/Homie-Improvement/miru/pull/198","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"low","finding_title":"The plan goes beyond the issue: Refresh stale MCP version references and record API changes in the MCP documentation.","finding_evidence":"The issue requests a dependency upgrade, application adaptations, and verification, but does not request repairing documentation references that were already stale. This is small, closely related documentation housekeeping and need not hold the plan lock.\n\nThe issue asks for nothing like it.\n\nAnswer by removing it from the plan or deferring it to follow-up work the plan names as out of scope; or decline with your reason.","recorded_resolution":"fixed","recorded_correction":"The fix for the stale version references in docs/miru-mcp.md is removed from this change. It is now one follow-up item, since those references were already stale.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T22:02:08.015Z","answer_recorded_at":"2026-10-26T22:03:28.695Z","first_independent_review_at":"2026-10-26T23:19:35Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"5c20b163a8a22d4a132e5680038656b9c440da066f926096a411b02bc8ef65fb","record_sha256":"f8afb10976dd1d317cb87b0ee85594ff4a458c1b9f3c41bd447f11d2e73aca77"},
    {"record_id":"author-pr198-plan-005","pr":198,"pr_url":"https://github.com/Homie-Improvement/miru/pull/198","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"low","finding_title":"The plan goes beyond the issue: Require a same-change documentation refresh for the MCP security upgrade.","finding_evidence":"The stated documentation convention is not a requirement expressed by the issue. Refreshing those references is related, low-cost housekeeping, but it adds work beyond the issue's dependency, caller-adaptation, and verification requests.\n\nThe issue asks for nothing like it.\n\nAnswer by removing it from the plan or deferring it to follow-up work the plan names as out of scope; or decline with your reason.","recorded_resolution":"fixed","recorded_correction":"The document refresh is removed from this change and listed as follow-up work, because the issue asks only for the gem bump, caller fixes and passing checks.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T22:02:08.015Z","answer_recorded_at":"2026-10-26T22:03:28.695Z","first_independent_review_at":"2026-10-26T23:19:35Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"0cb1ccb5a22fefcdf0912c8f1235bea33aeae07798de0c68b7ae1ec40ddec0ba","record_sha256":"c1b725230ee2d2253290f1f8698db7c647d9ef8f70af7c645dc944eac0bbca39"},
    {"record_id":"author-pr201-build-001","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"build","review_round":4,"recorded_severity":"medium","finding_title":"Concurrent successors resurrect a discarded invoice's retained claims","finding_evidence":"Two saves taking different entries from one retained holding read the array without locking it. The second write puts back the entry the first removed, so a later discard leaves that entry locked.","recorded_resolution":"fixed","recorded_correction":"A takeover now removes taken entries from the holding row's current value in one company-scoped UPDATE. A new two-connection test passes with this change and failed on the old code with \"Expected [1] to eq []\".","correction_target":"code_commit","source_path":"app/services/invoices/legacy_reservations.rb","source_line":112,"fix_commit":"03b4d0b0859f0fedd429b18b8218412da93a5d52","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/03b4d0b0859f0fedd429b18b8218412da93a5d52","finding_recorded_at":"2026-10-27T00:19:05.738Z","answer_recorded_at":"2026-10-27T00:40:33.241Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"881665dcf20eef44d0cbf2c28a23717c85ba56c470b86ecc3008707012be6e0c","record_sha256":"bc7d77976caee09f0ac8d22c2c55291993b55115b7b614e6248c8c09f9c203d3"},
    {"record_id":"author-pr201-build-002","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"build","review_round":4,"recorded_severity":"high","finding_title":"Cross-company entry not_found request specs deleted","finding_evidence":"If a regression lets callers tell a foreign or NULL-company entry apart from a missing id, no request test fails. The new before_save ahead of link_pending_linked_timesheet_entries and the new 'entry not found' refusal path add exactly that risk.","recorded_resolution":"fixed","recorded_correction":"Both company-scope describes are restored word for word from the base: five examples in create_spec and four in update_spec. They pass unchanged beside the new reservation examples.","correction_target":"code_commit","source_path":"spec/requests/api/v1/invoices/create_spec.rb","source_line":530,"fix_commit":"03b4d0b0859f0fedd429b18b8218412da93a5d52","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/03b4d0b0859f0fedd429b18b8218412da93a5d52","finding_recorded_at":"2026-10-27T00:19:05.738Z","answer_recorded_at":"2026-10-27T00:40:33.241Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"b5dac36dfbdf7d8362af9dc24887a58e7f8559920b93f08c91802f8a034613f3","record_sha256":"5ed9f7d87e015bfa22157fa5d4517a68b397f7b3979d2b24024e17498c65ebf2"},
    {"record_id":"author-pr201-build-003","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"build","review_round":4,"recorded_severity":"high","finding_title":"Handoff-fence and bulk-deletion acceptance tests deleted","finding_evidence":"If the billing fence regresses, InvoiceMailer could write sent_at or the sent status during an ownership handoff, and bulk deletion could remove invoices during a handoff or act on another company's invoice id. No test would catch it. The generated reservation tests replaced the base handoff-refusal and company-scoped delegation examples with success-only cases, and the author gave no reason.","recorded_resolution":"fixed","recorded_correction":"The mailer handoff example and both bulk-deletion describes (service delegation and the two handoff refusals) are restored word for word. The new mark_billed and draft-release examples sit beside them under distinct describe names.","correction_target":"code_commit","source_path":"spec/mailers/invoice_mailer_spec.rb","source_line":143,"fix_commit":"03b4d0b0859f0fedd429b18b8218412da93a5d52","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/03b4d0b0859f0fedd429b18b8218412da93a5d52","finding_recorded_at":"2026-10-27T00:19:05.738Z","answer_recorded_at":"2026-10-27T00:40:33.241Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"b008dec44f8f5ce5b74b17816add53021fd42fa9c495d59fec6bf8d60a3257c8","record_sha256":"4460f7364e8377a395af33c07d1358a55fadfff3f324cbce0f32f2e3af493725"},
    {"record_id":"author-pr201-build-004","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"build","review_round":4,"recorded_severity":"high","finding_title":"ReservationDecision spec drops examples for unchanged rows","finding_evidence":"R2's exactly-once guarantee (reservation_decision.rb:49) and R8's retained non-draft entries (:34-35) are no longer tested. The file went from 11 examples to 3, but the author said only that examples were added.","recorded_resolution":"fixed","recorded_correction":"Every base decision example is restored except \"sends held union lines for a non-draft addition\", which the live non-draft rule replaces. In \"reads a nil holding\", the live sent case now expects nil. A new example covers a live non-draft sending nothing.","correction_target":"code_commit","source_path":"spec/services/invoices/reservation_decision_spec.rb","source_line":7,"fix_commit":"03b4d0b0859f0fedd429b18b8218412da93a5d52","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/03b4d0b0859f0fedd429b18b8218412da93a5d52","finding_recorded_at":"2026-10-27T00:19:05.738Z","answer_recorded_at":"2026-10-27T00:40:33.241Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"85e9064aff8507a7f8f5300ef67929cb9e68656fc6ebbbef3be100200bef6769","record_sha256":"c18baa92d6a2fe59298fc82c3bc95fd861c12aff24d8ffeb97e58bff6c19c2ba"},
    {"record_id":"author-pr201-build-005","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"build","review_round":4,"recorded_severity":"high","finding_title":"Rewritten request specs fail the company-scope meta spec","finding_evidence":"Describe names repeat (create_spec :5, :530 and 6 more; update_spec :5, :351 and 3 more), so spec/meta/invoice_entry_company_scope_request_specs_spec.rb:25 and :38 fail every time and the suite is red.","recorded_resolution":"fixed","recorded_correction":"After the marker, create_spec and update_spec each hold exactly one describe, the company-scope one. The reservation examples sit above the marker under distinct names, and the meta spec now passes.","correction_target":"code_commit","source_path":"spec/requests/api/v1/invoices/create_spec.rb","source_line":530,"fix_commit":"03b4d0b0859f0fedd429b18b8218412da93a5d52","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/03b4d0b0859f0fedd429b18b8218412da93a5d52","finding_recorded_at":"2026-10-27T00:19:05.738Z","answer_recorded_at":"2026-10-27T00:40:33.241Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"4dff3d760b03bcff8652278f2f1f2dc60c180a0934d3508b101f6f0d5f51704e","record_sha256":"61b5715bae9ae4c46306901f5077f7ccca549cf798cfb8ecb08e8dce8c269d99"},
    {"record_id":"author-pr201-build-006","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"build","review_round":4,"recorded_severity":"low","finding_title":"Same refusal and tenant examples repeated across new specs","finding_evidence":"One rule is tested in three to five places, so copies fall out of step.","recorded_resolution":"fixed","recorded_correction":"The three create-spec copies of the sent-over-draft refusal are now one example, and so is the update-spec pair. The three not_found copies in legacy_reservations_spec are one example and the two mark_billed copies are one; each kept example now carries every distinct assertion of the ones removed.","correction_target":"code_commit","source_path":"spec/requests/api/v1/invoices/create_spec.rb","source_line":543,"fix_commit":"03b4d0b0859f0fedd429b18b8218412da93a5d52","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/03b4d0b0859f0fedd429b18b8218412da93a5d52","finding_recorded_at":"2026-10-27T00:19:05.738Z","answer_recorded_at":"2026-10-27T00:40:33.241Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"d2b4ac11fa06f7b74775390b03a3bb9d99b673a49c85d8fd257de3d48a4356cd","record_sha256":"4809a50bfb182dc15f81b9a90265480ddbc74312104239bb5c4204c312002c0c"},
    {"record_id":"author-pr201-build-007","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"build","review_round":4,"recorded_severity":"medium","finding_title":"Seed recorder spec fixture changed despite 'seed specs unchanged'","finding_evidence":"The fixture no longer builds two kept owners on one entry, which suggests real legacy data in that state would be refused on its next save. The plan disclosed this change under Assumptions, but the intent says the seed specs stay unchanged.","recorded_resolution":"fixed","recorded_correction":"The recorder example again builds two kept invoices that link one entry and then discards both, as on base. The second link is now written with insert_all!, because invoice saves now refuse that legacy state. Its assertions are unchanged.","correction_target":"code_commit","source_path":"spec/lib/timekeeping/seed_disposition_recorder_spec.rb","source_line":140,"fix_commit":"03b4d0b0859f0fedd429b18b8218412da93a5d52","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/03b4d0b0859f0fedd429b18b8218412da93a5d52","finding_recorded_at":"2026-10-27T00:19:05.738Z","answer_recorded_at":"2026-10-27T00:40:33.241Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"87a0a014cfcf22962c58495fbc2c75db59bebd371b3f9335d527fece525f2d3b","record_sha256":"e247f39b2a5fb1987d7870c31d2416bb5bf7e41c99f235ca5bcdb0f1c3c9b24d"},
    {"record_id":"author-pr201-plan-001","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"A successor draft cannot release an entry while the old retained holding still lists it.","finding_evidence":"plan item 6 prevents unlocking whenever any holding still lists an entry. Consider a sent invoice A that holds entry E, then is discarded. Draft B takes E and later removes its line or is discarded. The coordinator only upserts B's holding; neither plan item 6 nor plan item 7 removes E from A's retained holding. B's release therefore leaves E locked, blocking edits after the draft should have released it. docs/architecture/timekeeping-soa.md:606 requires a successor's commit to drop the previous retained reservation. Record that transfer under the entry lock, and ensure release checks ignore superseded retained claims. Add a test covering discard A, commit B, then release B.","recorded_resolution":"fixed","recorded_correction":"When a successor invoice takes a retained entry, prepare now removes that entry from the other invoice's retained holding. This runs in the same transaction, under the entry lock, so a rollback restores it. A test now covers discard A, take by B, then release by B.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:09:36.281Z","answer_recorded_at":"2026-10-26T20:11:35.009Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"0e8cbb4963e0da24d8d46604db00adbb690f155cfa1ab1844516f4592f233e6a","record_sha256":"b1d57f7903c935710f6a034237d941f2dfccb08bcc36d2b4aef4f2d2626fd0a6"},
    {"record_id":"author-pr201-plan-002","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Concurrent invoice creates can deadlock before the adapter can return the required refusal.","finding_evidence":"plan item 7 takes FOR UPDATE after line validation. app/models/invoice_line_item.rb takes FOR KEY SHARE during both direct-entry and pending grouped-entry validation. If two transactions validate the same entry before either flushes, both hold FOR KEY SHARE. Each then requests FOR UPDATE, which conflicts with the other transaction's existing lock. PostgreSQL must abort one transaction instead of returning the planned 422 refusal. This affects a single shared entry, not just the deferred multi-invoice lock-order case. Move the required entry locking ahead of the conflicting validation locks, or define another safe arrangement. Add a two-connection test that pauses both transactions after validation and verifies one success and one refusal.","recorded_resolution":"fixed","recorded_correction":"The adapter now takes FOR NO KEY UPDATE, which serializes writers without conflicting with the validation's FOR KEY SHARE. This is a disclosed departure from the issue's \"FOR UPDATE\". A two-connection test is added.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:09:36.281Z","answer_recorded_at":"2026-10-26T20:11:35.009Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"39e0cc65652c776aab33ba7890b933169f5a8d334a6f39b0a7432fd4a4770dc1","record_sha256":"cf80da17491433764cd3fee4a8465b72d3c0edb2b554111c9d89778744b41635"},
    {"record_id":"author-pr201-plan-003","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The adapter must transfer retained entries when a successor invoice commits.","finding_evidence":"plan item 6 prevents release while any holding still lists the entry, but no step removes a previous invoice's retained ownership. The existing coordinator only updates the requesting invoice's holding in app/services/invoices/operation_coordinator.rb:115-120. Consider a sent invoice that is discarded, then a new draft that takes its entry. When that draft is discarded, the old retained holding still lists the entry, so plan item 6 leaves it locked. Users cannot edit the entry even though the successor released it. docs/architecture/timekeeping-soa.md section 7.2 requires successor commit to drop the previous retained reservation. Add that transfer to the legacy adapter, preserve the old ownership if the successor rolls back, and test takeover followed by draft discard.","recorded_resolution":"fixed","recorded_correction":"This is the same fix as the retained-takeover finding. The successor's prepare drops the entry from the old retained holding inside the invoice transaction, and the takeover-then-discard test proves the release.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:09:36.281Z","answer_recorded_at":"2026-10-26T20:11:35.009Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"2465ad5f98af7a06c104fc80438d459c047a901dd478bd0aff44253ce8447f8f","record_sha256":"8cd77f5e25dc663107106eb52ca66d6b6ed0f3813f5d572de77e331fa8662db6"},
    {"record_id":"author-pr201-plan-004","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The boundary can overwrite a holding with a set computed before another transaction commits.","finding_evidence":"plan item 3 reads holdings and lines before calling the coordinator. app/services/invoices/operation_coordinator.rb acquires the invoice's advisory lock only inside call. Start with two lines referencing E and F. Concurrent transactions change separate lines to X and Y without changing totals. app/models/invoice_line_item.rb does not re-save the invoice for entry-only changes. Both flushes can compute their sets before either obtains the advisory lock. The second transaction then settles its stale set, such as {E,Y}, after the first commits {X,F}. The final lines reference {X,Y}, but the holding omits X. plan item 7 can then allow another invoice to take X. Acquire the invoice lock before reading the facts used for the decision, and add a two-connection test for equal-total edits to separate lines.","recorded_resolution":"fixed","recorded_correction":"The flush now takes the invoice's advisory lock before it reads holdings and lines. The coordinator takes the same lock again, which PostgreSQL allows in one session. A two-connection test is added.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:09:36.281Z","answer_recorded_at":"2026-10-26T20:11:35.009Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"dd7e7f93eac57554f978b71fb6475b01f151d720eda5365defbf07e8cabe7b71","record_sha256":"5bb1e74e08fa07fcb5d34688da7dd619360e76630bc66f5d3dc66a5dde808d65"},
    {"record_id":"author-pr201-plan-005","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The conflict check excludes untouched invoices created before the boundary.","finding_evidence":"plan item 7 checks holdings, while the deferred backfill leaves existing invoices without them. An entry on an untouched kept invoice therefore counts as retained, and a second invoice can claim it. This permits duplicate billing during normal use. Today, app/models/invoice.rb locks entries without writing holdings; app/services/invoices/reservation_adapters.rb registers no working adapter. The architecture document defines orphans as entries with no referencing invoice, not merely entries without holdings (docs/architecture/timekeeping-soa.md:891). Distinguish those orphans from entries still referenced by kept invoices. A company-scoped lookup of direct and grouped lines can enforce the conflict rule without requiring a full backfill. Add a test that refuses a second invoice while the original pre-boundary invoice remains untouched.","recorded_resolution":"fixed","recorded_correction":"An entry with no holding that a line of another kept invoice still references now counts as on another invoice. Only entries no kept invoice references count as legacy orphans. This matches the issue's own label for them.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:09:36.281Z","answer_recorded_at":"2026-10-26T20:11:35.009Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d5594926eaac5987c6fd2cdaaeb49df480f5e3a990d6c09a6c1ee5ba1cc69665","record_sha256":"2fa2f757966b44e2d15c3269924f514c054f5caf36f03e684e4f7af9400878e0"},
    {"record_id":"author-pr201-plan-006","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The discard fix cannot restore the whole decision spec to main before the line-removal fix.","finding_evidence":"The verification for the discarded-draft fix says the decision spec matches main again. plan item 8 says the first boundary commit changes both the discard rule and the draft-live rule. The line-removal fix restores the draft-live rule only in the next commit. Change the earlier verification to require that only the discard rule matches main. Keep the full comparison with main in the final unit.","recorded_resolution":"fixed","recorded_correction":"The discard fix now checks only that the discard rule matches main. The full match with main is checked once, in the final line-removal fix.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:09:36.281Z","answer_recorded_at":"2026-10-26T20:11:35.009Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"af3257e23f3cbb11f44d5ea68e6a5a41611dd7cf5e38c184953696b0d6b0b560","record_sha256":"d810a888ae1e2e79439630ede5632979a4208c22e4578550b1c9d335c4f3d15f"},
    {"record_id":"author-pr201-plan-007","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The initial boundary commit releases destroyed drafts before the labelled fix.","finding_evidence":"U2 requires every characterization example except the re-lock example to pass. However, plan item 8 changes only discarded-draft and live-draft decisions. app/services/invoices/reservation_decision.rb already maps destroyed drafts to RELEASE_ALL. BulkDeletionService destroys invoices inside savepoints, so U2 will send an empty desired set and its adapter will unlock their entries. This fails the unchanged example \"leaves the linked entries locked when the draft is bulk deleted\" before U6 moves it. Include the destroyed-draft decision in the temporary compatibility behavior, then restore it and move its example in the labelled draft-removal fix.","recorded_resolution":"fixed","recorded_correction":"The first commit now also gives the destroyed-draft rule a temporary nil result. The line-removal fix restores it and moves the bulk-delete example.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:09:36.281Z","answer_recorded_at":"2026-10-26T20:11:35.009Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"e8f6a98737553de86c13f6f9d47d5c1db4921098cdd28f9aae177c7c2b63bc6e","record_sha256":"8329d573efbd5d7ece54b01e62cf15b37d9dcc8838a3e29640bdb47ee197f970"},
    {"record_id":"author-pr201-plan-008","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The registry needs a stable identity before a new invoice has a database id.","finding_evidence":"plan item 1 keys registrations by invoice id, but plan item 2 first registers an invoice in before_save, before its insert assigns that id. Later line callbacks register the same invoice with its assigned id. The plan does not say how these registrations become one registry entry. Two new invoices in one transaction also initially share the nil key. Implementers could either merge distinct invoices or flush one invoice twice, producing missing or duplicate reservation operations. Define how new invoices get distinct registry identities and how later registrations reuse them after insertion. Add tests for repeated registration during create and two invoice creates in one transaction.","recorded_resolution":"fixed","recorded_correction":"A new invoice now registers in after_create, keyed by its new id, with an empty captured set. Only persisted invoices register in before_save. Tests cover repeated registration during create and two creates in one transaction.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:09:36.281Z","answer_recorded_at":"2026-10-26T20:11:35.009Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"1493b1034d3cc0186b099ff9287c9fc3dcec6b1c2f5b76f0d80212327c32ed98","record_sha256":"3c0d12d0683e7ac0483792f911199a26bf7a5e2fd12396abaf8e736333191899"},
    {"record_id":"author-pr201-plan-009","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"medium","finding_title":"The plan goes beyond the issue: Register new invoices after insertion while registering existing invoices before writes.","finding_evidence":"For new invoices, this explicitly replaces the issue's before-any-write registration and capture timing with registration after insertion. An empty historical set explains the intended equivalence, but the decision still changes an expressly stated timing rule rather than merely choosing how to implement it.","recorded_resolution":"fixed","recorded_correction":"A new invoice now registers before its insert, in before_save, capturing the empty set as the issue's timing requires. Its entry is keyed by the record itself until after_create moves it to the new id.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T20:13:02.866Z","answer_recorded_at":"2026-10-26T20:14:18.778Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"a0cbb175b9cabefa87c92f83dee5a69906b4faa982f7c8840f21aeda0ce8d921","record_sha256":"d8b1b4b1c9ba2d9f9a951a514db2459cd14d69efab008c0b1de2e23a15537745"},
    {"record_id":"author-pr201-plan-010","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":3,"recorded_severity":"blocker","finding_title":"A caller can bypass invoice exclusivity by submitting a non-draft status.","finding_evidence":"plan item 13 skips prepare for every live non-draft save, including entry additions. However, app/policies/invoice_policy.rb permits caller-supplied `status`, and Api::V1::InvoicesController#create passes it directly to invoice creation. A caller can therefore submit `status: \"sent\"` with an entry already on another kept invoice. The new conflict check never runs, allowing duplicate billing. Preserving unlocked seed entries does not require accepting conflicting references. docs/architecture/timekeeping-soa.md section 7.4 expressly includes exclusivity in this migration. Preserve non-draft lock behavior, but enforce exclusivity when their direct or grouped references change. Add API tests that create and update a sent invoice with an already-invoiced entry and expect the specified refusal.","recorded_resolution":"fixed","recorded_correction":"A sent invoice whose lines gain an entry now goes through an admission check under entry row locks. An entry on another kept invoice is refused with 422, and no lock or intent is written. Create and update tests are added.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T22:11:50.667Z","answer_recorded_at":"2026-10-26T22:13:25.347Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"3b660524516b20adf7ed6acd00cde3dc1dc3d3d101b188053ac98c4931dedacf","record_sha256":"429a9c431baba835cbc081e8f0096be9fdd14e9ca4e33dde1cb71cb860aee583"},
    {"record_id":"author-pr201-plan-011","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":3,"recorded_severity":"blocker","finding_title":"A second sent invoice can still bill an entry already on another kept invoice.","finding_evidence":"The disclosed gap allows duplicate billing: invoice A holds entry E, then a user creates or edits sent invoice B to reference E. plan item 13 skips prepare for B, so plan item 14's conflict check never runs. Both invoices remain kept and can charge for the same work. Preserving unlocked seed entries does not require accepting conflicting line additions. Keep the existing non-draft lock behavior, but enforce the kept-invoice conflict rule when a non-draft gains an entry. Add create and update tests that expect 422 \"entry is on another invoice\" and leave the original invoice unchanged. This belongs to the requested conflict fix, not a fourth behavior change.","recorded_resolution":"fixed","recorded_correction":"Fixed with the same admission check. Creating or editing a sent invoice to add an entry another kept invoice holds now answers 422 \"entry is on another invoice\", and the original invoice is left unchanged.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T22:11:50.667Z","answer_recorded_at":"2026-10-26T22:13:25.347Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"bd9d9329ba430d7d073edd38601072323533ae5feea5d653a6927f304f61d473","record_sha256":"b7cf51487e2122d84e126ba794f85a0f91823c236a832357aa755085a9e003fe"},
    {"record_id":"author-pr201-plan-012","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":3,"recorded_severity":"blocker","finding_title":"Live non-draft saves bypass the conflict check and permit duplicate billing.","finding_evidence":"Create draft A holding entry E, then POST invoice B with status `sent` and a line referencing E. `InvoicePolicy#permitted_attributes` permits both fields, and `Api::V1::InvoicesController#create` saves them without a conflict check. Under plan item 13, B sends no operation, so plan item 14's prepare classification never runs. Both kept invoices can bill E. Updating a sent invoice to add E has the same result. The disclosed gap therefore reaches the ordinary API, not just seed construction. `docs/architecture/timekeeping-soa.md` section 7.4 explicitly says exclusivity is not future work. Preserving unlocked seed links does not require accepting another invoice's entries. Keep the no-new-lock behavior, but enforce exclusivity for live non-draft entry changes under entry-row serialization. Add API create and update tests that expect 422 while the seed guards remain unchanged.","recorded_resolution":"fixed","recorded_correction":"Non-draft entry additions are now checked under the same id-ordered entry row locks as prepare, with no intent and no lock write. API create and update tests cover the refusal.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T22:11:50.667Z","answer_recorded_at":"2026-10-26T22:13:25.347Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"7290e2f786ae1f63ec9eb98cdf2b0f0844d42e8603c27eaa58b2ed7df83dde33","record_sha256":"451f21909080006e13ac431fa887b4b317f1acc6b398e48ec96cd9b65171c952"},
    {"record_id":"author-pr201-plan-013","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":3,"recorded_severity":"blocker","finding_title":"Sent-invoice saves bypass the only planned conflict check.","finding_evidence":"The required refusal cannot run when the live non-draft decision returns nil. app/controllers/api/v1/invoices_controller.rb permits creates and updates through invoice_params; app/policies/invoice_policy.rb permits :status and entry-bearing lines. A caller can therefore submit a sent invoice containing an entry already on a kept draft. The boundary skips the coordinator, so the adapter never applies the conflict rules. Both invoices survive and can bill the same work. The disclosed gap does not resolve this missing enforcement step. Preserve unlocked seed links, but provide a serialized conflict check for non-draft creates and entry-set edits without newly setting their locked flags. Add request tests proving that these saves return the required 422 while unclaimed sent-invoice entries stay unlocked.","recorded_resolution":"fixed","recorded_correction":"The flush now has an enforcement step for non-draft creates and edits that add entries. Request tests check the 422, and check that sent-invoice entries no other invoice claims stay unlocked.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T22:11:50.667Z","answer_recorded_at":"2026-10-26T22:13:25.347Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"3b5590c46a6e26ee9482317edf7d8f666233dd8abe5e090d819606d08d9214ed","record_sha256":"b1001d5600870992cb3c90ddabf117f5644dad2d2093125e3536746697cf9bbb"},
    {"record_id":"author-pr201-plan-014","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":3,"recorded_severity":"blocker","finding_title":"Skipping lock changes must not skip conflict checks.","finding_evidence":"The design couples conflict checks to operations that change locks. plan item 13 returns nil for every live non-draft save, so the boundary never reaches the checks in plan item 14. The normal API accepts both status and entry-bearing lines through app/policies/invoice_policy.rb and app/controllers/api/v1/invoices_controller.rb. A caller can therefore create a sent invoice for an entry already on another kept invoice, allowing duplicate billing. Preserving unlocked seed links does not require accepting that conflict: spec/timekeeping/seed_disposition_spec.rb explicitly requires seeds without conflicts. Separate admission checks from lock-changing decisions. Reuse Timekeeping::ReservationRules under entry-row serialization when a non-draft changes its referenced entries, while preserving the required absence of new locks and intents. Test that a second sent invoice is refused and that an","recorded_resolution":"fixed","recorded_correction":"The admission check is now its own adapter step, reusing Timekeeping::ReservationRules. It runs whenever a live non-draft gains entries and writes no lock, intent or holding.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T22:11:50.667Z","answer_recorded_at":"2026-10-26T22:13:25.347Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"697d573901d45dba5700ea39a79f4f8af11a0a3860553ba1ffc6e55968591cf7","record_sha256":"ae7ba2479f2d1028b4474c0f9cda643335bbcef604f3d6efa677cbd882f1a3c3"},
    {"record_id":"author-pr201-plan-015","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":3,"recorded_severity":"blocker","finding_title":"Skipping preparation for non-drafts leaves the required duplicate-billing refusal unenforced.","finding_evidence":"The plan expressly leaves sent-invoice saves exempt from the conflict check. This drops part of the deliberate fix, not just reservation writes. InvoicePolicy#permitted_attributes accepts :status and nested entry ids. Api::V1::InvoicesController#create and #update save those values directly. A caller can therefore create a sent invoice referencing an entry already on another kept invoice. The company validations pass, and the planned nil decision bypasses prepare and its refusal. Both invoices can bill the same time. docs/architecture/timekeeping-soa.md section 2.3 explicitly requires refusal for a second kept invoice; section 9.2's operation list does not exempt non-drafts from that fix. Preserve the seeded entries' unlocked state, but enforce the conflict rule on non-draft entry additions too. Add create and edit tests that expect the specified 422 without newly locking","recorded_resolution":"fixed","recorded_correction":"The conflict check is now separate from lock changes. Non-draft entry additions are refused on conflict without newly locking, so the seeded unlocked links stay as they are.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T22:11:50.667Z","answer_recorded_at":"2026-10-26T22:13:25.347Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"eb3e76acdb02446fc23587377fd5336fe97dc23afb46e4ababb225d8a77f716b","record_sha256":"4e4d72f8f1635a9bb3b406fee4e65b95aea45ade57d62eab73d8c0f4e9204475"},
    {"record_id":"author-pr201-plan-016","pr":201,"pr_url":"https://github.com/Homie-Improvement/miru/pull/201","cohort":"merged_implementation","stage":"plan","review_round":3,"recorded_severity":"blocker","finding_title":"The non-draft exception defeats the promised protection against duplicate billing.","finding_evidence":"The stated goal and R13 promise to refuse a second kept invoice for the same entry. However, plan item 13 skips prepare for every live non-draft save, and “listed exceptions” explicitly says those saves can accept an entry another kept invoice holds. Create draft A with entry E, then create sent invoice B with E. B bypasses plan item 14's conflict check, leaving two kept invoices billing E. Preserving unlocked seed entries does not require accepting conflicting invoice lines. Keep the no-new-lock behavior for sent invoices, but define how their entry changes enforce the conflict rule. Test both creation orders: draft then sent, and sent then draft.","recorded_resolution":"fixed","recorded_correction":"The disclosed gap is removed. Tests now cover both orders, a draft then a sent invoice and a sent invoice then a draft, and expect a refusal each time.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-26T22:11:50.667Z","answer_recorded_at":"2026-10-26T22:13:25.347Z","first_independent_review_at":"2026-10-27T01:07:58Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"719900834344202a1199ad79c6d5360d07363dfe2c222da1688b166d988b9b1b","record_sha256":"9e2b4b17718bbd983161fb304fa7033b14f9522d0a9e48ee97bda80b68b3884e"},
    {"record_id":"author-pr207-build-001","pr":207,"pr_url":"https://github.com/Homie-Improvement/miru/pull/207","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Generated detach specs repeat the same cases across five spec files","finding_evidence":"Several detach behaviours are each asserted in more than one file, so changing one means editing three or four near-identical examples, and the suite gets slower with no added coverage. The author's note says the duplicates were removed, but the diff still contains them: legacy scope twice in agent_spec.rb (:213, :304); lock order plus the legacy_gateway mark in agent_spec.rb :325 and two gateway_spec examples; company-bound token claims in agent_spec, gateway_spec and service_command_client_spec; the bare-operation 403 and other-company 404 in four fake_timekeeping_spec groups (:193, :232, :258, :280) and service_command_client_spec; the user-destroy handoff refusal in agent_spec and gateway_spec; the mint claims at service_token_spec.rb:10 and :51.","recorded_resolution":"fixed","recorded_correction":"Each case now has one owner. Merges: the fake token-mode groups, the two mint examples, the two legacy-scope examples. Dropped: gateway copies of credentials, user-destroy refusal and lock order, and the client's cross-company check.","correction_target":"code_commit","source_path":"spec/models/agent_spec.rb","source_line":213,"fix_commit":"5bb7875e8376b8ed2a973e99f2642da88b69d1d8","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/5bb7875e8376b8ed2a973e99f2642da88b69d1d8","finding_recorded_at":"2026-10-27T01:38:18.879Z","answer_recorded_at":"2026-10-27T02:26:25.529Z","first_independent_review_at":"2026-10-27T02:44:20Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"efe9f2ba09d12a0c3c6f28414e88600a9ee9dde227e853b721d7139cff43cef9","record_sha256":"ba809ca534aef1a99a2c3fb598446dfbd29407d16381d8e14f7da843e6e9cf02"},
    {"record_id":"author-pr207-build-002","pr":207,"pr_url":"https://github.com/Homie-Improvement/miru/pull/207","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Malformed service claims pass fake but fail production","finding_evidence":"A correctly RS256-signed token whose operations claim is the string agents.detach_entries is accepted by the new token-mode fake, but production refuses the identical token. The fake verifies through ServiceToken.verify, which turns that string into an array; the production ServiceTokenVerifier requires a real array of strings and also checks the types of the sub and company_id claims. Authentication integration specs can therefore pass for a sender whose claims production rejects. That breaks the fake's own promise at fake_timekeeping.rb:27 and the plan's dig-here asking token mode to check service tokens as strictly as the real ReservationAccess. Today's minter always emits valid shapes, so the gap stays hidden until a sender's claim shape drifts.","recorded_resolution":"fixed","recorded_correction":"Service token checks now apply the same claim-shape rules as Timekeeping's own verifier, and the list coercion is gone. The fake now refuses a signed token with a single-string operations claim (401) and detaches nothing.","correction_target":"code_commit","source_path":"app/services/timekeeping/service_token.rb","source_line":77,"fix_commit":"5bb7875e8376b8ed2a973e99f2642da88b69d1d8","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/5bb7875e8376b8ed2a973e99f2642da88b69d1d8","finding_recorded_at":"2026-10-27T01:38:18.879Z","answer_recorded_at":"2026-10-27T02:26:25.529Z","first_independent_review_at":"2026-10-27T02:44:20Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"aaab6b0777cf05c633a2e714ffc52fa9308ae863e2fc7ebbd7db517101dc8e6f","record_sha256":"1be8743add33327df01e31a18bb5dc558bb17a28ba112e82b5b0e3d95b4bb14c"},
    {"record_id":"author-pr207-plan-001","pr":207,"pr_url":"https://github.com/Homie-Improvement/miru/pull/207","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A new destroy attempt must not replay a detach that predates newly created entries.","finding_evidence":"plan item 6 assigns one operation_id to an agent for an entire ownership generation. However, timekeeping/app/services/timekeeping/service_commands.rb returns a stored receipt before selecting entries again. After a remote detach succeeds but the local destroy rolls back, the agent remains available. If another entry names that agent before the next destroy attempt, the repeated operation_id returns the earlier success without detaching that entry. The callback then deletes the agent and leaves the new entry pointing to a missing agent. Give each new destroy transaction a fresh operation_id, and reuse it only when resending that command. Add a regression that creates an entry between the rolled-back destroy and its retry. Repeating the nullification with a fresh command is safe and avoids treating all future detach attempts as completed work.","recorded_resolution":"fixed","recorded_correction":"The operation id now comes from the attempt's fresh request id and the agent id, not from the generation, so a later destroy never replays an earlier receipt. A regression test adds an entry between attempts.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T00:42:48.438Z","answer_recorded_at":"2026-10-27T00:43:58.361Z","first_independent_review_at":"2026-10-27T02:44:20Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c14ecbb91dde4e142c6a91550cc13146d76b8a95ae215cd9d802e38b3d17fc57","record_sha256":"a00e4330471678dbe39bf923cc682afe82bb8e3b04dab5e119165621bdbd2ab6"},
    {"record_id":"author-pr207-plan-002","pr":207,"pr_url":"https://github.com/Homie-Improvement/miru/pull/207","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A retried destroy can delete an agent without detaching its new entries.","finding_evidence":"plan item 6 derives operation_id only from agent id and ownership generation. Consider the rollback plan item 7 explicitly permits: the service detaches entries, but the local destroy fails. The agent remains available, so users can add entries before retrying the destroy. At the same generation, that retry reuses the completed receipt. The service returns the earlier result without detaching the new entries, and the local destroy deletes their agent. Those entries now reference a missing agent. Revise the retry design to distinguish a resumed detach from a later destroy after intervening writes, while retaining safe retries after an uncertain response. Add a test that commits the first remote detach, rolls back the local destroy, adds an entry, and retries. The retry must detach that entry or refuse the destroy.","recorded_resolution":"fixed","recorded_correction":"A retried destroy now sends a new operation id, so the service selects entries again and detaches the new one before the agent is deleted. The plan adds a test for this.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T00:42:48.438Z","answer_recorded_at":"2026-10-27T00:43:58.361Z","first_independent_review_at":"2026-10-27T02:44:20Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"a04b0668b5bc311c548d943b0ba8ef61a9d573c4443025c55bec399109ca00d0","record_sha256":"c5b8191b8eed005c36e8746eabd6139856f32892f9156988db75b8aac079a251"},
    {"record_id":"author-pr207-plan-003","pr":207,"pr_url":"https://github.com/Homie-Improvement/miru/pull/207","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A retried destroy can replay an old detach and leave new entries linked to a deleted agent.","finding_evidence":"plan item 6 gives every destroy attempt for one agent and generation the same operation_id. Consider a remote detach that commits before the local destroy rolls back. The agent remains available, so another entry can name it before deletion is retried. app/services/timekeeping/reference_facts.rb still lists that agent, and timekeeping/app/services/timekeeping/entry_writer.rb accepts its entries. The retry then replays the old receipt without detaching the new entry: timekeeping/app/services/timekeeping/service_commands.rb returns replay(receipt) before selecting targets. Local deletion succeeds, leaving an entry linked to a missing agent. Use a fresh operation_id for each local destroy attempt, retaining it only for resends within that attempt. Repeating the detach with a fresh key safely clears remaining entries. Add a test covering remote success, local rollback, a new","recorded_resolution":"fixed","recorded_correction":"Each destroy attempt now gets its own operation id, reused only for resends inside that attempt, so a retry after a local rollback detaches entries created in between. A test covers that sequence.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T00:42:48.438Z","answer_recorded_at":"2026-10-27T00:43:58.361Z","first_independent_review_at":"2026-10-27T02:44:20Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"638164d2b32513037aa20515021c276304c9d93f0e36647baa07d3714685a1f8","record_sha256":"12496dbc18fdfb8728155750c8f7a9f9cf8fe742c8d041eff5dc6e8b42a2a961"},
    {"record_id":"author-pr207-plan-004","pr":207,"pr_url":"https://github.com/Homie-Improvement/miru/pull/207","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Give each destroy attempt its own command identity so a prior receipt cannot skip new entries.","finding_evidence":"plan item 6 makes every destroy attempt for one agent and generation share an operation_id. plan item 7 allows the local destroy to roll back after the remote detach commits. The surviving agent can then acquire new entries. A later destroy sends the old operation_id, so timekeeping/app/services/timekeeping/service_commands.rb returns the stored receipt before selecting entries. The local destroy succeeds but leaves those new entries linked to a deleted agent. The command identity must distinguish a new destroy attempt from a resend of the same command. Prefer a fresh UUID for each callback invocation, reused only for resends within that invocation, rather than deriving it from agent id and generation. Add a test that commits a remote detach, rolls back the local destroy, adds an entry, then retries destruction at the same generation.","recorded_resolution":"fixed","recorded_correction":"The plan now gives every callback invocation its own command identity and keeps it only for resends within that invocation. The retry test commits remotely, rolls back locally, adds an entry and retries.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T00:42:48.438Z","answer_recorded_at":"2026-10-27T00:43:58.361Z","first_independent_review_at":"2026-10-27T02:44:20Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"39feac5f4e4b1a92fc0120a2f8ed783b0071f2d50128384359ab26e49d1c4e3c","record_sha256":"7f864908732d6dc39878bd2f75405f83905a9adf0b758e0e67ab23f16f2a9b19"},
    {"record_id":"author-pr208-build-001","pr":208,"pr_url":"https://github.com/Homie-Improvement/miru/pull/208","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"A rerun that fails again drops recovery mode, so the next rerun skips unwritten identical rows","finding_evidence":"Every run resets imported_rows to 0 (line 63), and fail_import stores only the rows that run wrote (line 95). Suppose a rerun skips an already-stored row and then fails before writing anything. It stores imported_rows=0, so the next rerun goes back to skipping every match and silently drops an identical row that was never written. A worker crash that leaves status 'processing' has the same effect. This breaks the intent's rule that a rerun must not skip unwritten rows.","recorded_resolution":"fixed","recorded_correction":"Rerun handling no longer depends on the per-run imported_rows counter. Stored entries are split by whether they were created before or after this data import, so a second failed run or a crash cannot switch it off.","correction_target":"code_commit","source_path":"app/services/imports/harvest_time_entries_importer.rb","source_line":101,"fix_commit":"707cb685d17162398ab63d8b9ebb4539a63c513b","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/707cb685d17162398ab63d8b9ebb4539a63c513b","finding_recorded_at":"2026-10-27T01:48:25.533Z","answer_recorded_at":"2026-10-27T02:20:28.580Z","first_independent_review_at":"2026-10-27T02:37:55Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"79d2e2570a6910d99e3b16849f236485eae5f24ed7e69fb6f4e7b4c3f61c7e83","record_sha256":"81c586141973a75fccaf61a4ac7fc491990e924f47be2085c19422f9c6992756"},
    {"record_id":"author-pr208-build-002","pr":208,"pr_url":"https://github.com/Homie-Improvement/miru/pull/208","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Idempotency key hashes the workspace base currency, so a retried row can get a new key","finding_evidence":"When the CSV has no Currency column, normalized_row fills in company.base_currency (line 133), and that value goes into the key, although currency is not sent in the create body. The comment at lines 24-28 says workspace values are kept out of the key for exactly this reason. If base_currency changes before a rerun and the forwarded row is not yet projected locally, the rerun sends it under a new key and Timekeeping stores the entry twice.","recorded_resolution":"fixed","recorded_correction":"Currency is removed from the values the row key is built from, because it falls back to the company's base currency. A new spec shows that a forwarded rerun after a currency change sends the same key and stores no second entry.","correction_target":"code_commit","source_path":"app/services/imports/harvest_time_entries_importer.rb","source_line":30,"fix_commit":"707cb685d17162398ab63d8b9ebb4539a63c513b","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/707cb685d17162398ab63d8b9ebb4539a63c513b","finding_recorded_at":"2026-10-27T01:48:25.533Z","answer_recorded_at":"2026-10-27T02:20:28.580Z","first_independent_review_at":"2026-10-27T02:37:55Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"5922d3d84d37be3aa0a61f671fceb3280ce18994c4dcbe9630b72f2d451bad6e","record_sha256":"82b94d5a3ffc6293868a9f78cb0d1965133a53f7207e0db0289b07120e6e97b9"},
    {"record_id":"author-pr208-build-003","pr":208,"pr_url":"https://github.com/Homie-Improvement/miru/pull/208","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Recovery recreates rows the first attempt deliberately skipped as duplicates","finding_evidence":"load_database_duplicates counts every source=import entry without knowing which import wrote it. A recovery run therefore uses up a match that belongs to an earlier import and creates a second copy of a row the first run deliberately skipped as a duplicate. This breaks the rule that a rerun must not duplicate stored rows.","recorded_resolution":"fixed","recorded_correction":"Matches from entries stored before this data import existed now skip every matching row, on any run. Only entries stored after the import was created are used up one per row, so a rerun no longer recreates an earlier import's row.","correction_target":"code_commit","source_path":"app/services/imports/harvest_time_entries_importer.rb","source_line":319,"fix_commit":"707cb685d17162398ab63d8b9ebb4539a63c513b","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/707cb685d17162398ab63d8b9ebb4539a63c513b","finding_recorded_at":"2026-10-27T01:48:25.533Z","answer_recorded_at":"2026-10-27T02:20:28.580Z","first_independent_review_at":"2026-10-27T02:37:55Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"aadae4378445d44347388e5af27221aa177d9a80ff67958850aee18b9bca9c05","record_sha256":"580e7e21d1b33c54c96e0b2a3b5b212381285ac68dc7ceb8876597dad14ee247"},
    {"record_id":"author-pr208-plan-001","pr":208,"pr_url":"https://github.com/Homie-Improvement/miru/pull/208","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A failed import can report zero entries after committing rows.","finding_evidence":"plan item 5 makes earlier rows commit independently, but the design still persists progress only every 200 rows. In `app/services/imports/harvest_time_entries_importer.rb`, `process` resets `imported_rows` to zero, and `fail_import` updates only status, message and finish time. If row one commits and row two raises StaleGeneration, the failed import therefore reports zero imported rows despite storing row one. Users cannot tell how much of the failed import took effect. Persist the actual committed-row count when recording failure, without counting the refused row as failed or skipped. Extend the generation-change test to check the stored imported count after fewer than 200 writes.","recorded_resolution":"fixed","recorded_correction":"A failed import now stores the number of rows that actually committed before the failure. The refused row is not counted as failed or skipped, and the generation test checks the stored count.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T00:42:09.052Z","answer_recorded_at":"2026-10-27T00:43:24.750Z","first_independent_review_at":"2026-10-27T02:37:55Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f643bbf8a71b23b6e6828e4a742dc2fb397b1fdbf2800317a79c0971168e7142","record_sha256":"a2f9fdf39476c51dcab56415e755233a7237422d0ea2fd570eee8cfa7571c00d"},
    {"record_id":"author-pr208-plan-002","pr":208,"pr_url":"https://github.com/Homie-Improvement/miru/pull/208","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A project change can give an already committed row a new retry key.","finding_evidence":"plan item 4 hashes the resolved final bill status, but that value is not stable across retries. In `app/services/imports/harvest_time_entries_importer.rb`, `bill_status` reads the project's current `billable` flag. Suppose a billable row commits remotely, the import fails, and projection delivery is delayed. If the project becomes non-billable before retry, the same uploaded row now hashes `non_billable` instead of `unbilled`. The duplicate query finds no projection, and Timekeeping receives a new key and creates a second entry. Derive row identity from immutable file content, its line number and the import id. Define how a changed request body is refused without creating another entry. Test a retry after the project's billable flag changes while the projection is absent.","recorded_resolution":"fixed","recorded_correction":"The retry key now comes only from the import id, the row's CSV line number and its raw file values. A rerun whose body changed is refused by Timekeeping and fails the job instead of creating a second entry.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T00:42:09.052Z","answer_recorded_at":"2026-10-27T00:43:24.750Z","first_independent_review_at":"2026-10-27T02:37:55Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"b39b565946f6252b4444ad562ad286c87e29d535cb0638f6b9df7ec7b119fa83","record_sha256":"f474a6470842b0c4068da04f16892d4e269a7e050e2cc2e1152ac3824ff686db"},
    {"record_id":"author-pr208-plan-003","pr":208,"pr_url":"https://github.com/Homie-Improvement/miru/pull/208","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A retry can skip an identical row that was never written.","finding_evidence":"plan item 4 preserves two identical CSV rows by including their line numbers in their keys. plan item 5 still relies on the existing duplicate filter before forwarding. Consider two identical rows: the first commits and reaches the projection, then the job fails before the second write. On retry, `load_database_duplicates` and `duplicate_row?` in `app/services/imports/harvest_time_entries_importer.rb` match both rows by user, project, date, duration and note. Both disappear from the write plan, so the second entry is never created, yet the job completes. Make same-import recovery distinguish row occurrences instead of treating one projected match as proof that every matching row committed. Test this ordering with projection delivery between failure and retry.","recorded_resolution":"fixed","recorded_correction":"Duplicate matching now counts occurrences: one stored entry skips one matching row, not all of them. A retry then writes the identical row that never committed.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T00:42:09.052Z","answer_recorded_at":"2026-10-27T00:43:24.750Z","first_independent_review_at":"2026-10-27T02:37:55Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c65ce2b3e33ac97885c6071887f05cc93bde415e0aa4e3a1e9619bc8f569b08e","record_sha256":"aafac7217328a16978ffabb7eb669c95e9234f8b5b896c6ed5d276085bf8cf7f"},
    {"record_id":"author-pr208-plan-004","pr":208,"pr_url":"https://github.com/Homie-Improvement/miru/pull/208","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The plan gives two different points for reading the expected generation.","finding_evidence":"plan item 3 requires reading the generation at the start of `persist_entries`. The High-Level Design instead reads it after creating clients, projects, and members. If ownership changes during that setup, the first ordering retains the old generation and refuses the first entry; the second accepts the new generation and continues. Move the generation read before that setup in the sketch, and make the implementation order explicit so both sections describe the same behavior.","recorded_resolution":"fixed","recorded_correction":"The design sketch now reads the generation first in persist_entries, before clients, projects and members are created, matching the decision text.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T00:42:09.052Z","answer_recorded_at":"2026-10-27T00:43:24.750Z","first_independent_review_at":"2026-10-27T02:37:55Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"6584538f7267f2e3b133d41fbe26a4674ea5ee955fe6aa6776779784462d0e89","record_sha256":"23417b1d88d7e780813b607644640a2b579e55355821571869957bd5c871bab5"},
    {"record_id":"author-pr208-plan-005","pr":208,"pr_url":"https://github.com/Homie-Improvement/miru/pull/208","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"high","finding_title":"The plan goes beyond the issue: Integrate gateway writes into the importer, including retry keys, per-row transactions, failure counts, and occurrence-counted duplicate matching.","finding_evidence":"The routing, retry recovery, and accurate committed counts serve the issue, but globally changing duplicate matching also changes which rows fresh and legacy imports create. Limit the duplicate-policy change to the same-import recovery problem rather than changing unrelated fresh-import behavior.","recorded_resolution":"fixed","recorded_correction":"Occurrence counting now applies only when the same data import is rerun after a failure that already stored rows. Fresh imports keep today's duplicate matching unchanged.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T00:44:20.513Z","answer_recorded_at":"2026-10-27T00:45:23.019Z","first_independent_review_at":"2026-10-27T02:37:55Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9f801ebc73b3a105e67309d8b1e8f15c418c3a9a5692f3290288784f0aa0824f","record_sha256":"8bb5a20221c53743da93d772976e625cf90acd1a4f9eed8d84b402d68b6a9c9e"},
    {"record_id":"author-pr208-plan-006","pr":208,"pr_url":"https://github.com/Homie-Improvement/miru/pull/208","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"high","finding_title":"The plan goes beyond the issue: Replace duplicate-set matching with occurrence counts for all imports, including fresh imports containing more matching rows than the workspace stores.","finding_evidence":"Preventing an unwritten identical row from being lost during same-import recovery serves the issue and the review finding. Applying the new policy to fresh imports also creates rows that existing duplicate detection intentionally skipped; the issue does not request that broader behavior change, so constrain the recovery fix or obtain explicit authorization for the expanded duplicate policy.","recorded_resolution":"fixed","recorded_correction":"The broader duplicate policy is removed. Only a rerun of a failed import that stored rows counts occurrences, and a test pins that a fresh import still skips every matching row.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T00:44:20.513Z","answer_recorded_at":"2026-10-27T00:45:23.019Z","first_independent_review_at":"2026-10-27T02:37:55Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"29a67728b7cbf63951fa4e4a461b706a79b0697f20f67010072a2f34a6ac0102","record_sha256":"9e3668f16bbbda0b56fc00f6cf655aca73bedf9c4bf10ce9a5b23f384c696f8c"},
    {"record_id":"author-pr210-build-001","pr":210,"pr_url":"https://github.com/Homie-Improvement/miru/pull/210","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"Unrelated agent-ownership and service-token request specs deleted and rewritten weaker","finding_evidence":"The replay issue never asks to touch two existing request-spec groups, 'Entry writes carrying a service token' and 'Entry write routes', 'agent ownership'. The change deletes both and puts three weaker tests in their place. The last commit (fcc9ca1) is titled as a restore, but it rewrote the specs instead of bringing back the base text. With these checks gone, CI would pass a regression in agent ownership (S7) or in the credential_in_body refusal. No request-level test now covers: an unregistered signed timekeeping_service JWT in a note, including the glued 'glued<token>tail' form; an owner creating an entry for an employee with the owner's own agent; an update that sets a colleague's agent while the stored user stays the same; an update that sets another company's agent; a zero or negative agent_id answering 400; or clearing an agent with null and bulk-moving or deleting agent entries.","recorded_resolution":"fixed","recorded_correction":"I restored both base groups word for word and removed the rewritten group. The file's first 476 lines now diff clean against the base, and the timekeeping entry-write, command, provider and entries specs pass: 126 examples, 0 failures.","correction_target":"code_commit","source_path":"timekeeping/spec/requests/v1/entry_writes_spec.rb","source_line":355,"fix_commit":"2a98fde48f56fa5dad1d91e914642e6e960d321c","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/2a98fde48f56fa5dad1d91e914642e6e960d321c","finding_recorded_at":"2026-10-27T02:09:38.935Z","answer_recorded_at":"2026-10-27T02:51:42.657Z","first_independent_review_at":"2026-10-27T03:05:14Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"5d7fd75729907c0100085fc80e6a1a0c1105f6e53cb62a9ed3c19dd2157e89bd","record_sha256":"6305c1d1a2b5f93b59365c7550a8b79fcc2c4e918d89e0ca32fc2919b220bbb8"},
    {"record_id":"author-pr210-plan-001","pr":210,"pr_url":"https://github.com/Homie-Improvement/miru/pull/210","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"An older service can treat the replay-only request as a real delete.","finding_evidence":"The no-write guarantee fails during a mixed-version rollout or service rollback. An admin can call destroy with scope: :actor for a colleague's kept entry. EntryLookup rejects that target, so the planned fallback sends DELETE without If-Match. The current service ignores the new header: timekeeping/app/services/timekeeping/entry_command.rb:73-80 constructs a normal command, and EntryWriter permits an admin to delete that entry. The gateway checks scope and authorization only after the deletion. Ordering implementation units does not prevent requests from reaching older service instances. Make unsupported replay requests fail without writing, or gate the fallback until every service instance supports it. Specify safe rollout and rollback ordering, and test an unsupported provider.","recorded_resolution":"fixed","recorded_correction":"Agreed. The replay request now also sends an If-Match an older service always refuses as a version conflict. An older service therefore replays a receipt or writes nothing, and a spec proves this.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T01:05:50.440Z","answer_recorded_at":"2026-10-27T01:07:12.773Z","first_independent_review_at":"2026-10-27T03:05:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"15ff23df31d26a1b8337cf7bcb12b16d46397e646cac2da413d346bc136f24db","record_sha256":"b9407ac7a0461a9888e1f430dd347b1a1ba2a7677dcc9a1af14488d916d5d641"},
    {"record_id":"author-pr210-plan-002","pr":210,"pr_url":"https://github.com/Homie-Improvement/miru/pull/210","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The planned disclosure promises an expiry that the receipt lookup does not enforce.","finding_evidence":"The plan says receipts expire after 24 hours and later retries return not found. However, timekeeping/app/services/timekeeping/entry_writer.rb:107 finds receipts without checking created_at. Its answer_from method then returns the stored answer without an age check. The fake's IdempotencyReceipts also retains answers without expiry. None of the implementation units changes that behavior. Correct the scope statement and required pull request disclosure to distinguish the contract's retention_hours: 24 from an enforced expiry. The ticket does not ask for a new expiry mechanism, so do not add one solely to make this statement true.","recorded_resolution":"fixed","recorded_correction":"Confirmed: the writer finds receipts with no age check, and the fake never expires them. The plan now says 24 hours is a minimum retention, not an enforced expiry, and adds no expiry.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T01:05:50.440Z","answer_recorded_at":"2026-10-27T01:07:12.773Z","first_independent_review_at":"2026-10-27T03:05:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"10a88f69ec2e0b97a1d66c32b4ff1da65b3e2b97adecfb470def523f2b22dcdd","record_sha256":"b930b96af0f4218c97a406715920bd05e0398261e14776b70583a84cd6b7b97b"},
    {"record_id":"author-pr210-plan-003","pr":210,"pr_url":"https://github.com/Homie-Improvement/miru/pull/210","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"low","finding_title":"The plan goes beyond the issue: Answer replay-only requests from receipts and apply the new header rule to every write route.","finding_evidence":"Receipt-only handling for DELETE serves the issue, but specifying new header behavior on every write route extends the public behavior to operations the issue does not name. A shared implementation is reasonable, but that alone does not require exposing replay-only behavior on other operations.","recorded_resolution":"fixed","recorded_correction":"Agreed. The replay-only header is now read only on the single-entry DELETE route. Every other write route ignores it and behaves exactly as before, and a spec pins that.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T01:07:58.174Z","answer_recorded_at":"2026-10-27T01:08:59.195Z","first_independent_review_at":"2026-10-27T03:05:14Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"551c956b91f3be9bc4a0ee361323a02f553be6515061944067387fe29db867b1","record_sha256":"51820df87c6e5c1ca4d13bf5b7679a00143b38c203a574bfdd3c1dd1d65735e5"},
    {"record_id":"author-pr218-build-001","pr":218,"pr_url":"https://github.com/Homie-Improvement/miru/pull/218","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"medium","finding_title":"Ten overlapping one-example runner describes repeat the split this change removes","finding_evidence":"This change exists to remove duplication from the suite, and it merges the service spec's split describe blocks. It then adds ten one-example describe blocks to the task spec (lines 499-743), and several of them check the same runner contract. 665 and 726 both unit-test the alert_company_ids parser. 576 and 683 both run the failing body twice and expect ['101'] on the first run. 554 and 616 both check that now: is honoured and the clock is put back. The token-absence check appears at 499, 520 and 598, and the failing_window literal is pasted three times. The file already has a 'shared task runner' describe at line 9. To learn what the runner promises, a reader has to compare ten blocks, and a change to the runner or to the alert-line format means editing several near-copies. That is the cost this refactor set out to remove. Each merge must keep every distinct check (learning merge-duplicate-tests-only-onto-same-or-stricter-input-and-share-aggregate-verdicts).","recorded_resolution":"fixed","recorded_correction":"The ten runner describes are now one block. The overlapping examples are merged with every distinct check kept, and the money example is back in the real-records block. The four alert spec files pass.","correction_target":"code_commit","source_path":"spec/tasks/timekeeping_alerts_spec.rb","source_line":499,"fix_commit":"ad3d80b045f311b7dd4c3346d1bc3f0710fc24d8","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ad3d80b045f311b7dd4c3346d1bc3f0710fc24d8","finding_recorded_at":"2026-10-27T03:27:30.371Z","answer_recorded_at":"2026-10-27T03:48:01.745Z","first_independent_review_at":"2026-10-27T04:03:28Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"e4e2a4be87fffa4801cc31d9a70aa86c8ed34be15c0e17ac62a1dea71dd03676","record_sha256":"1fa33f45d23f36dd0c9262f03b5f6feb8372fa663768f96852e548f4fc1b3ec0"},
    {"record_id":"author-pr218-plan-001","pr":218,"pr_url":"https://github.com/Homie-Improvement/miru/pull/218","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The completion rule forbids the live wrapper that the implementation retains.","finding_evidence":"The completion rule requires that no describe-local run wrapper remain in the three spec files. However, the live-spec decision and implementation unit retain `run_alerts_task` to assert a nil exit status and return stdout. An implementer cannot satisfy both instructions. State that thin adapters may remain when they preserve existing failure checks, or specify how callers replace this wrapper without losing that check.","recorded_resolution":"fixed","recorded_correction":"The completion rule now allows the live spec's thin wrapper, which only calls the shared runner, keeps the nil exit check and returns stdout.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T02:52:06.919Z","answer_recorded_at":"2026-10-27T02:53:07.037Z","first_independent_review_at":"2026-10-27T04:03:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"ebb91e0b67ad36bcb199e987778361d27275be35f40ebfe6fe5d119407efdac0","record_sha256":"abb81feb2371dd5a39dbff2ffdd57d1d445008ff25217973b094ba61dad74934"},
    {"record_id":"author-pr218-plan-002","pr":218,"pr_url":"https://github.com/Homie-Improvement/miru/pull/218","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The proposed grep check rejects the shared client helper that the plan keeps.","finding_evidence":"The verification requires no Object.new client to remain in the service spec. However, spec/services/timekeeping/alert_signals_spec.rb:13-16 constructs that client inside alert_signals_client. The unit explicitly keeps this helper and migrates examples to it. Restrict the check to duplicate client construction inside examples, allowing the one construction in alert_signals_client. Otherwise a correct implementation cannot satisfy the stated check.","recorded_resolution":"fixed","recorded_correction":"The check now allows the single client built inside alert_signals_client and forbids only clients built inline in examples.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T02:52:06.919Z","answer_recorded_at":"2026-10-27T02:53:07.037Z","first_independent_review_at":"2026-10-27T04:03:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"54a0d392d50e7d7d8823093e634bc260a88238faf14853a44371316526c9c8cb","record_sha256":"39bf2509709bc4d31a973b9e09387eed66182a826072f09ba433a33de0a8d351"},
    {"record_id":"author-pr218-plan-003","pr":218,"pr_url":"https://github.com/Homie-Improvement/miru/pull/218","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The verification command skips every example that exercises the changed live wrapper.","finding_evidence":"The command cannot prove the live-spec migration as claimed. In spec/integration/timekeeping/alerts_live_spec.rb:20-21, before(:all) skips all three examples unless TIMEKEEPING_LIVE=1. The plan explicitly runs without that flag. None of those examples will call the new wrapper or its exit-status assertion. Add an enabled live run with its service and database prerequisites, or a focused test that exercises the wrapper without starting the server. Keep the disabled run as a separate check of the skip behavior.","recorded_resolution":"fixed","recorded_correction":"Added a live run with its service and database prerequisites as the check for the live wrapper. The run without the flag stays as a separate check that the examples are still skipped.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T02:52:06.919Z","answer_recorded_at":"2026-10-27T02:53:07.037Z","first_independent_review_at":"2026-10-27T04:03:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d552ac706781266c8b322b4a05b29c4b09514785046f5a5e4940ac045c25b6a4","record_sha256":"0cf70677c0b7ff00c494a89ff097ab89510f7b60e937b376453f98c8b5a23171"},
    {"record_id":"author-pr218-plan-004","pr":218,"pr_url":"https://github.com/Homie-Improvement/miru/pull/218","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The verification command skips the live behavior it claims to prove.","finding_evidence":"The verification contract says its command proves all four implementation units, but it explicitly leaves all three live examples pending. The live-spec unit changes output capture, clock handling, and how task exits fail an example. Its stated live scenarios therefore remain unverified by the prescribed run. Add an enabled live run, or a runnable test that exercises the adapter without a live server. Distinguish checks for skipped examples from checks that establish unchanged behavior.","recorded_resolution":"fixed","recorded_correction":"Verification now separates the skip check from a live run with TIMEKEEPING_LIVE=1. If the environment cannot boot the server, the pull request says the live examples were not run.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T02:52:06.919Z","answer_recorded_at":"2026-10-27T02:53:07.037Z","first_independent_review_at":"2026-10-27T04:03:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"1e14ffda130c06cb976f4cd6a598cabfccf04e9c9a1a215f4519556a3b588407","record_sha256":"b8992723a059143f9d272f0ba1b083b90111de995a67fb392fc41a662ff1fea2"},
    {"record_id":"author-pr218-plan-005","pr":218,"pr_url":"https://github.com/Homie-Improvement/miru/pull/218","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Treat the proposed helper name as a design choice, not a missing existing symbol.","finding_evidence":"The ticket explicitly asks to add `run_timekeeping_alerts(now:, metrics:)`. It does not claim that helper already exists. Remove this entry from `Names not in the repository` and retain the decision to extend `run_timekeeping_alerts_task` in the design rationale. That reuse is supported by `spec/support/timekeeping/alerts_task_helpers.rb` and the runner-name check in `spec/meta/timekeeping_alerts_specs_spec.rb`. The current classification makes a deliberate naming departure look like a stale ticket reference.","recorded_resolution":"fixed","recorded_correction":"Same fix as the matching finding: the helper name is now treated as a naming choice in the design decisions, not as a stale reference.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T02:52:06.919Z","answer_recorded_at":"2026-10-27T02:53:07.037Z","first_independent_review_at":"2026-10-27T04:03:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"fe1f8da2c91723eb0f8e7c9c9e3376141e2f5f18abfb34f4aa04985aedc48ccc","record_sha256":"ef7667341b61e470abb8aa5aea737f6ec83343852ccc4978c64ef704a81caace"},
    {"record_id":"author-pr218-plan-006","pr":218,"pr_url":"https://github.com/Homie-Improvement/miru/pull/218","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Treat the requested helper name as a design choice, not a missing repository symbol.","finding_evidence":"The ticket explicitly asks to add `run_timekeeping_alerts(now:, metrics:)`; it does not claim that helper exists. Remove that entry from `Names not in the repository`. Keep the decision to extend `run_timekeeping_alerts_task` in plan item 1, where the plan already explains why reusing the existing runner avoids duplication. This separates a deliberate interface choice from the obsolete file paths the ticket assumes still exist.","recorded_resolution":"fixed","recorded_correction":"The requested helper name is no longer listed as a missing symbol. The decision to extend the existing runner instead, and why, stays in the design decisions as a disclosed naming departure.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T02:52:06.919Z","answer_recorded_at":"2026-10-27T02:53:07.037Z","first_independent_review_at":"2026-10-27T04:03:28Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"9c6e8fa465495aa23a87302a1933f1e8836d9f71a7fbe3961be7d3ae773ad6ad","record_sha256":"371cc0cbb940a9ce7d8a11ead51ce50ff5d34c99ffb28d369979a490263aa5d3"},
    {"record_id":"author-pr219-build-001","pr":219,"pr_url":"https://github.com/Homie-Improvement/miru/pull/219","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"ReservationOutcome spec deletes its state and frozen-copy tests","finding_evidence":"The change rewrites ReservationOutcome#initialize and, in the same change, deletes the only two tests of that constructor. A regression in state validation or in the frozen copies would now pass. The stated reason for the edit (a missing rails_helper require) does not match the base, which already has the require on line 5. The learning copy-and-freeze-nested-inputs-in-data-define-value-types cites this spec as its evidence.","recorded_resolution":"fixed","recorded_correction":"I restored both original examples word for word next to the new reason example. The frozen-copy example now also passes a mutable reason, changes it afterwards, and checks that the outcome keeps the original. All 3 examples pass.","correction_target":"code_commit","source_path":"spec/services/invoices/reservation_outcome_spec.rb","source_line":9,"fix_commit":"1b9baa63df9dbce284d62a98ce9a3a16d81615a7","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/1b9baa63df9dbce284d62a98ce9a3a16d81615a7","finding_recorded_at":"2026-10-27T03:58:59.640Z","answer_recorded_at":"2026-10-27T04:21:14.359Z","first_independent_review_at":"2026-10-27T04:41:20Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"6e829d24c1b3303a2864632c9391024e2ac899f16ab4a47d9ec3c0ac245fabef","record_sha256":"035c7192d20af99c391ac1768a6ef86ccb3ac9ffbdedeabe97e64d7bee6625b5"},
    {"record_id":"author-pr219-build-002","pr":219,"pr_url":"https://github.com/Homie-Improvement/miru/pull/219","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"Retained reservation hides another kept invoice's line claim, so two kept invoices can hold one entry","finding_evidence":"Under the prepared owner, one entry can end up on two kept invoices, which the issue's acceptance bar forbids. Discarded sent invoice A keeps entry X as a committed retained row. Live sent invoice B adds a line for X; admit lets it through and writes nothing. Draft C then adds X. decide hands A's retained row to the rules and only looks at other_active_claim when there is no committed row, so it never sees B's line claim. The rules answer add_provisional, C commits, and commit deletes A's retained row. B and C now both hold X. The legacy adapter ranks another kept invoice's line above a retain-only claim and refuses C. The plan's plan item 13 wording causes this gap, so that wording cannot meet R7.","recorded_resolution":"fixed","recorded_correction":"Prepare and admit now check another kept invoice's line or holding claim before falling back to a retained row, in the legacy adapter's order. A new test fails without this change and passes with it.","correction_target":"code_commit","source_path":"app/services/invoices/prepared_reservations.rb","source_line":201,"fix_commit":"1b9baa63df9dbce284d62a98ce9a3a16d81615a7","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/1b9baa63df9dbce284d62a98ce9a3a16d81615a7","finding_recorded_at":"2026-10-27T03:58:59.640Z","answer_recorded_at":"2026-10-27T04:21:14.359Z","first_independent_review_at":"2026-10-27T04:41:20Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"914fe9a2c7da09bac05673f8b436ded229c730c0c9ba99320d92293b7e72d621","record_sha256":"662748c8157e008abdef437b40c6d28c1dd0c96890f4bf02eadfbe4f55ea50e0"},
    {"record_id":"author-pr219-plan-001","pr":219,"pr_url":"https://github.com/Homie-Improvement/miru/pull/219","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Commit must remove this invoice's own retained row before promoting its replacement.","finding_evidence":"plan item 2 creates provisional rows for desired entries already retained by this invoice. The commit design deletes only other invoices' retained rows before promoting those provisional rows. Restore a discarded sent invoice, then change it to draft while keeping its entry. Prepare creates a provisional row beside its own retained committed row. Promotion violates index_reservations_one_committed_per_entry in db/schema.rb. The invoice save has already committed, but delivery keeps failing and the prepared operation blocks later changes. Delete retained committed rows on claimed entries even when they belong to this invoice. timekeeping/app/services/timekeeping/reservation_operations.rb already does this in make_desired_set","recorded_resolution":"fixed","recorded_correction":"Same fix as the matching finding: retained rows on claimed entries are deleted whatever invoice holds them, and a test restores a retained invoice to draft and commits it.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T02:39:48.522Z","answer_recorded_at":"2026-10-27T02:41:32.269Z","first_independent_review_at":"2026-10-27T04:41:20Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"7aaff5ac305b1e7a5610a8e95ff81ef591bda638651183be259b04cd495ba246","record_sha256":"97c0b0899141b053b0c7621677d2e489a578f4fdd0b50aef68737781bb7d543e"},
    {"record_id":"author-pr219-plan-002","pr":219,"pr_url":"https://github.com/Homie-Improvement/miru/pull/219","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Commit must remove this invoice's retained row before promoting its replacement.","finding_evidence":"An operation that reclaims its own retained entry cannot commit as planned. plan item 2 creates a provisional row for that entry, but High-Level Design deletes only other invoices' retained rows. Promoting the provisional row then violates `index_reservations_one_committed_per_entry` in db/schema.rb:1080. Delivery rolls back and retries indefinitely, leaving the operation prepared and blocking later changes to that invoice. The referenced service avoids this: timekeeping/app/services/timekeeping/reservation_operations.rb:195-197 removes retained rows on claimed entries regardless of invoice identity. Follow that deletion scope before promotion, and add a test that prepares and commits an entry already retained by the same i","recorded_resolution":"fixed","recorded_correction":"Commit now deletes every retained committed row on a claimed entry, this invoice's own included, before promoting, as the service does. A test commits an entry the same invoice already retained.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T02:39:48.522Z","answer_recorded_at":"2026-10-27T02:41:32.269Z","first_independent_review_at":"2026-10-27T04:41:20Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"eb116d992935c06da9520e367ab14667700f348b6ebc8107f10e5d44c4041e61","record_sha256":"f696410d2e4474b322ca599c34e91bba9196fb9f6c9dbe34944a236ee73330bf"},
    {"record_id":"author-pr219-plan-003","pr":219,"pr_url":"https://github.com/Homie-Improvement/miru/pull/219","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep the required unresolved_operation reason observable instead of deferring it.","finding_evidence":"R6 promises a retryable answer named `unresolved_operation`, but deferred work and plan item 6 remove that distinction. `app/services/invoices/reservation_outcome.rb` has only state, conflicts and counts. `OperationCoordinator#raise_unless_prepared!` converts every retryable answer into `PrepareRetryable` with the message `prepare_retryable`. The planned empty conflicts therefore leave no observable `unresolved_operation` reason. This also differs from `contracts/timekeeping/reservations.yml:59-63`, which names that reason. Include a bounded way to preserve the reason across the adapter port and test it for a second prepare. The existing port's limitation explains the required work; it does not justify deferring an explicit ticket requirement.","recorded_resolution":"fixed","recorded_correction":"The outcome now carries an optional reason, and a second prepare answers retryable with reason unresolved_operation, taken from the reservations contract. The coordinator passes it into the raised error, and tests check both.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T02:39:48.522Z","answer_recorded_at":"2026-10-27T02:41:32.269Z","first_independent_review_at":"2026-10-27T04:41:20Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"ad0b87405bfa657898e867966c80a6b8b377fa6269434b3e9413191d2b1e1506","record_sha256":"ce1c781e48ac0287d6ea7f29bcaac3286f1f230cbfb4d7d140072e2864209373"},
    {"record_id":"author-pr219-plan-004","pr":219,"pr_url":"https://github.com/Homie-Improvement/miru/pull/219","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Reservation checks must include entries claimed only through a kept invoice's lines.","finding_evidence":"plan item 10 admits new lines on a sent invoice without reserving them. plan item 2 then checks only reservation rows. Create a kept sent invoice A, add entry E, and add E to draft invoice B. A's admission writes no reservation, so B's prepare sees E as free and succeeds. Both kept invoices now charge for the same entry. app/services/invoices/reservation_boundary.rb confirms that live non-drafts send no reservation operation. app/services/invoices/legacy_reservations.rb prevents this through EntryClaims, which also reads kept invoices' lines and holdings. Include these claims when building facts for ReservationRules, without changing the preserved no-lock behavior for sent saves. Add prepared-owner tests for a second invoice referencing a sent invoice's entry, including grouped links.","recorded_resolution":"fixed","recorded_correction":"Agreed. Prepare and admit now also count entries claimed by another kept invoice's lines or holdings, reusing the legacy claim reader moved to its own file. Tests cover direct and grouped lines.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T02:39:48.522Z","answer_recorded_at":"2026-10-27T02:41:32.269Z","first_independent_review_at":"2026-10-27T04:41:20Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"2a31ba69c38999104390c920c0fd65fd6f38f4962ef512c9943c7e6f15337d6e","record_sha256":"03c9e2fdd0a9f3b322736c32a9df0733cd73220a878c2282611611665ede42cf"},
    {"record_id":"author-pr219-plan-005","pr":219,"pr_url":"https://github.com/Homie-Improvement/miru/pull/219","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The abort-after-commit test expects a different outcome from the design.","finding_evidence":"The adapter test says that abort after commit must answer `committed`. plan item 4 instead requires abort to answer `aborted` when the stored operation is terminal, so its outbox row can settle. These expectations cannot both pass. Change the test to expect `committed` from repeated commit calls and `aborted` from abort after commit. Both must leave reservations, versions and outbox snapshots unchanged.","recorded_resolution":"fixed","recorded_correction":"The test now expects committed from a repeated commit and aborted from an abort after commit, with no row, version or outbox change in either case.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T02:39:48.522Z","answer_recorded_at":"2026-10-27T02:41:32.269Z","first_independent_review_at":"2026-10-27T04:41:20Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f11694a6bcd43451a481779de0e287fc55f1ba26318c95589304c1a35b434198","record_sha256":"58b88f664718116bacad8bcdc759ae4ddaca28fd48a592f48ec08e3eafb09b5b"},
    {"record_id":"author-pr219-plan-006","pr":219,"pr_url":"https://github.com/Homie-Improvement/miru/pull/219","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The refusal test requires zero reservations despite seeding an existing reservation.","finding_evidence":"The adapter test seeds an entry actively committed to another invoice, then requires refusal to leave zero reservations. That contradicts the setup and the requirement that refusal write nothing. Assert that the existing reservation remains unchanged and that the refused operation adds no reservations, operation rows or outbox snapshots. Apply the same distinction when the end-to-end test refuses a second invoice.","recorded_resolution":"fixed","recorded_correction":"The refusal tests now check that the seeded reservation is unchanged and that the refused save adds no reservation, operation or outbox rows.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T02:39:48.522Z","answer_recorded_at":"2026-10-27T02:41:32.269Z","first_independent_review_at":"2026-10-27T04:41:20Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c5673ea33f3bf00a914033e7546a0b29cf2f309da326bbecfd874e51e2b3f50b","record_sha256":"eac451e28e7cba0cc59941edd076a0d4dad2345a6473af08cdf535f06c6e6692"},
    {"record_id":"author-pr221-plan-001","pr":221,"pr_url":"https://github.com/Homie-Improvement/miru/pull/221","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Let the merged request helper select the company in the URL.","finding_evidence":"plan item 7 gives the merged helper entry, token, key and header inputs, but no company input. The existing helper in timekeeping/spec/requests/v1/entry_writes_spec.rb:588 takes company_id so the foreign-company example sends a matching foreign token and URL while reusing another company's entry ID and key. Keeping the URL fixed to the shared company would instead trigger the earlier token-versus-URL refusal. timekeeping/app/controllers/timekeeping/v1/entries_controller.rb:43-45 performs that check before building the command, so the example could pass without testing receipt isolation between companies. Add an explicit company_id argument, defaulting to the shared company, and retain the foreign-company URL in that example.","recorded_resolution":"fixed","recorded_correction":"The merged request helper now takes a company id that defaults to the shared company, and the foreign-company example passes the foreign company so its token and URL still match.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T03:57:04.362Z","answer_recorded_at":"2026-10-27T03:58:00.126Z","first_independent_review_at":"2026-10-27T04:52:34Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d66e43bec52c27b0087b0ef492e460280d99d83119af6f7fcb448413a1a27cc2","record_sha256":"b5d79b43fc004c0f0450f0f620595809d3c90e11abbff7e700b1304c3e16258e"},
    {"record_id":"author-pr221-plan-002","pr":221,"pr_url":"https://github.com/Homie-Improvement/miru/pull/221","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The merged request helper cannot preserve the foreign-company replay scenario.","finding_evidence":"plan item 7 gives the helper an entry id and token, but no company id for the request path. The existing foreign-company example in timekeeping/spec/requests/v1/entry_writes_spec.rb sends both the foreign owner's token and the foreign company's path, reusing the original entry id and key. Keeping the shared company's path instead would produce a misleading pass: timekeeping/app/services/timekeeping/read_access.rb:16 rejects that token/path mismatch before the receipt lookup runs. The test would no longer detect a receipt lookup that ignores company boundaries. Add a company_id: argument defaulting to the shared company, and pass the foreign company explicitly in this example.","recorded_resolution":"fixed","recorded_correction":"Fixed: the foreign-company replay keeps a matching foreign token and URL, so the request reaches the receipt lookup instead of stopping at the earlier token-versus-URL check.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T03:57:04.362Z","answer_recorded_at":"2026-10-27T03:58:00.126Z","first_independent_review_at":"2026-10-27T04:52:34Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"24a5fa26f0f5e3c9e7944cfdd852b67d77cb36f3193a6835d0dec2c883892acd","record_sha256":"1d529d220312a01791e1313953a5df9d6529c1745cd9fe338bec54a187929a26"},
    {"record_id":"author-pr221-plan-003","pr":221,"pr_url":"https://github.com/Homie-Improvement/miru/pull/221","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The merged request helper needs a company argument to preserve the cross-company replay test.","finding_evidence":"plan item 7 gives the request helper an entry id, token, key and header options, but no company for the URL. The existing cross-company example in timekeeping/spec/requests/v1/entry_writes_spec.rb:589-612 passes foreign_company to replay_delete while reusing the deleted entry id and key. Its foreign token matches that foreign URL. A helper that always uses the shared company cannot reproduce this request. Using the foreign token with the original company's URL instead could pass through an earlier company-mismatch refusal, hiding a defect in receipt isolation. Add a company_id argument, defaulting to the shared company, and pass foreign_company explicitly in this example.","recorded_resolution":"fixed","recorded_correction":"Fixed the same way: the helper takes a company id, defaulting to the shared company, and the cross-company example keeps the foreign company's URL with the reused entry id and key.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T03:57:04.362Z","answer_recorded_at":"2026-10-27T03:58:00.126Z","first_independent_review_at":"2026-10-27T04:52:34Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"86946282f44f6c556a8a72c799bd0c6cb7e6398dfb7b8b1e936858778de0900a","record_sha256":"636638b3b11310ed6d004323bec4ebcb647177e76651a3628f12a55cf6c9528f"},
    {"record_id":"author-pr221-plan-004","pr":221,"pr_url":"https://github.com/Homie-Improvement/miru/pull/221","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The token-mode consolidation conflicts with the rule for preserving checks on the same code path.","finding_evidence":"The assertion-ownership rule requires each removed check to survive on the same code path. Yet the plan moves stored-entry and outbox checks from token-mode admin examples into a shared contract that uses a non-token owner. The retained token-mode example checks responses, but not stored state or outbox size. A token-mode regression could therefore return the expected response while changing stored data, without failing those checks. Keep the state checks in the surviving token-mode example, or run the shared checks through that mode before deleting their original owners.","recorded_resolution":"fixed","recorded_correction":"The surviving token-mode admin example now also checks the outbox size and the whole stored entry after each replay, alongside the same checks added to the shared contract.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T03:57:04.362Z","answer_recorded_at":"2026-10-27T03:58:00.126Z","first_independent_review_at":"2026-10-27T04:52:34Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"0460bb4f9cf31f99f07b75eec236b9dbffe902e570eae21487dd6ea4c5cb741e","record_sha256":"d82f44a48611b2ac6ce9818d26340a0108ed4145a4f90cacf8c19d02a5834e29"},
    {"record_id":"author-pr223-build-001","pr":223,"pr_url":"https://github.com/Homie-Improvement/miru/pull/223","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Duplicate external_run_id shape tests with copied helper in entry_command_spec","finding_evidence":"The same String-or-nil rule is asserted twice through copied helpers, so a change to the rule must be made twice.","recorded_resolution":"fixed","recorded_correction":"Folded the second run-field example into the first. Every case survives: one String with one nil, both Strings, both nil, and 1, 123, an array, a hash and true each refused with 400.","correction_target":"code_commit","source_path":"timekeeping/spec/services/timekeeping/entry_command_spec.rb","source_line":184,"fix_commit":"791dd9a95fc92d498d21e3c6f0edb0e959bbf21e","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/791dd9a95fc92d498d21e3c6f0edb0e959bbf21e","finding_recorded_at":"2026-10-27T06:07:08.162Z","answer_recorded_at":"2026-10-27T06:21:52.011Z","first_independent_review_at":"2026-10-27T07:43:04Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"2aff4bba70b64553926f7b326faac727174ccbb45e87ff550b978f80a000eaf4","record_sha256":"d3792d5bf6f7442e5475abf9d57e522cb286d1ef8a9d44c4633d3cef0c5e3366"},
    {"record_id":"author-pr223-build-002","pr":223,"pr_url":"https://github.com/Homie-Improvement/miru/pull/223","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"Monolith gateway and write-client specs for destroy replay and agent detach deleted without replacement","finding_evidence":"Gateway#destroy was rewritten (scope check before replay, no replay for an out-of-scope entry) and the same edit deleted every monolith test of destroy replay, EntryWriteClient#replay_destroy_entry (what stops a retried CLI/MCP/web delete from deleting twice) and Gateway#detach_agent_entries routing (which R9 depends on). No spec calls replay_destroy_entry or checks detach_operation_id any more, and no reason was given. A regression there would now pass CI.","recorded_resolution":"fixed","recorded_correction":"Restored the removed gateway detach and destroy-replay examples and the write-client replay-only examples. The scope-refused colleague example now expects RecordNotFound with no DELETE, matching the new destroy order.","correction_target":"code_commit","source_path":"spec/services/timekeeping/gateway_spec.rb","source_line":400,"fix_commit":"791dd9a95fc92d498d21e3c6f0edb0e959bbf21e","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/791dd9a95fc92d498d21e3c6f0edb0e959bbf21e","finding_recorded_at":"2026-10-27T06:07:08.162Z","answer_recorded_at":"2026-10-27T06:21:52.011Z","first_independent_review_at":"2026-10-27T07:43:04Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"7d8cca949b6831599b95f5b2a41bb6f701fcc480808e25fd25a4887abdb18a6a","record_sha256":"f1c170f4690f34db4e7991badb39595bc6131af4fee95873a6f9b3f516ca61da"},
    {"record_id":"author-pr223-build-003","pr":223,"pr_url":"https://github.com/Homie-Improvement/miru/pull/223","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"high","finding_title":"Provider specs for agent ownership, service-token notes and replay-only DELETE deleted without replacement","finding_evidence":"About 320 lines of provider authorization and replay examples were removed without a reason. A provider change that attaches another user's or company's agent to an entry, or replays another company's stored DELETE answer, would now pass CI (CWE-639). This change edits EntryWriter's agent path, so the lost coverage matters more.","recorded_resolution":"fixed","recorded_correction":"Restored the removed provider examples: service-token note refusal, agent ownership including the 400 for zero or negative agent_id, and the replay-only DELETE examples, beside the new run-id test.","correction_target":"code_commit","source_path":"timekeeping/spec/requests/v1/entry_writes_spec.rb","source_line":326,"fix_commit":"791dd9a95fc92d498d21e3c6f0edb0e959bbf21e","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/791dd9a95fc92d498d21e3c6f0edb0e959bbf21e","finding_recorded_at":"2026-10-27T06:07:08.162Z","answer_recorded_at":"2026-10-27T06:21:52.011Z","first_independent_review_at":"2026-10-27T07:43:04Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"c4b0bf41368c11df010127213b2f797c0fdeabd2b2bf2ca4cb9b3baaf6e74c1e","record_sha256":"2cf41c5b12030e7d9749aaf4c1d18b97ae0d59099d5b5199133679f3b3041739"},
    {"record_id":"author-pr223-plan-001","pr":223,"pr_url":"https://github.com/Homie-Improvement/miru/pull/223","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A timed-out create can commit after the agent is deleted.","finding_evidence":"The deferred late-commit window breaks the deletion guarantee. Pause a provider create after EntryCommand.build reads agent facts but before it inserts the entry. Let EntryWriteClient time out, which releases the monolith's agent lock. Then destroy the agent: ServiceCommands selects no matching entry, finishes detach, and permits deletion. Resume the provider create. EntryWriter checks only the earlier facts and commits an entry naming the deleted agent. plan item 5 and the two successful-call orderings in U4 do not prevent this sequence. Add coordination between provider create and detach that also covers uncertain remote completion, and test this ordering. A one-time existence check or an immediate detach retry alone cannot exclude a later commit.","recorded_resolution":"fixed","recorded_correction":"Fixed in Timekeeping itself: create and detach-entries now share one per-agent lock, and create refuses once a detach receipt exists, which covers a caller that timed out.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T04:25:26.450Z","answer_recorded_at":"2026-10-27T04:28:07.925Z","first_independent_review_at":"2026-10-27T07:43:04Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"8a69b41688733e5c569b4fff0dafa4cde7964285a8babc91c83f6f17f0e7546e","record_sha256":"e76dc1f2ae7f3ad01b3be9acf2860eb0d70db667c8fa1af6dffef47cd07d7e48"},
    {"record_id":"author-pr223-plan-002","pr":223,"pr_url":"https://github.com/Homie-Improvement/miru/pull/223","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"A timed-out forwarded create can still leave an entry naming a deleted agent.","finding_evidence":"The deferred late-commit window defeats the ticket's explicit deletion guarantee. A provider create can remain in flight after the gateway times out and releases its agent lock. Agent deletion can then detach existing entries before the provider commits the new entry. That entry retains the deleted agent's identifier. The planned tests cover successful completion and deletion-first ordering, but not this failure sequence. Resolve this window before moving agent writes onto the gateway, and add a test that delays the provider commit beyond the caller's timeout. The solution must prevent or reconcile the late write; documenting the window does not satisfy the guarantee.","recorded_resolution":"fixed","recorded_correction":"Added a provider-side ordering between create and detach, plus a test that holds a create past its caller's timeout, completes the detach, then resumes the create and sees it refused.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T04:25:26.450Z","answer_recorded_at":"2026-10-27T04:28:07.925Z","first_independent_review_at":"2026-10-27T07:43:04Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"8495ff5f177f3077b33d04e2ebc46e180b3d7d11ee699fab7a427cd47f8b5d95","record_sha256":"6f7e3443368877b740fff35fb6759822d73cfdaaa69614e4eda9f5948b5f747f"},
    {"record_id":"author-pr223-plan-003","pr":223,"pr_url":"https://github.com/Homie-Improvement/miru/pull/223","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"Do not defer the race between a timed-out create and agent deletion.","finding_evidence":"The deferred late-commit window is part of the required create-versus-destroy guarantee. EntryWriteClient#forward converts a timeout into an error, which releases the gateway transaction's agent lock. The remote request can still run. EntryCommand.build reads agent facts before EntryWriter opens its transaction, so that request can retain valid facts while Agent#detach_timesheet_entries detaches existing entries and deletes the agent. A later remote commit can then store an entry naming the deleted agent. The two successful-call orderings in the planned race test do not cover this case. Include a protocol that prevents deletion from overtaking an unresolved create, or otherwise prevents that create from committing afterward. Add a test that times out the caller, completes deletion, and then resumes the remote create. Disclosure alone cannot satisfy the guarant","recorded_resolution":"fixed","recorded_correction":"The timeout ordering is no longer deferred. A provider spec times out the caller, completes the deletion, then resumes the create and sees it refused.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T04:25:26.450Z","answer_recorded_at":"2026-10-27T04:28:07.925Z","first_independent_review_at":"2026-10-27T07:43:04Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"156f84ea1011f813263b24f21f0910ecf7d1de4016dee3a6b49ab7a541a257a3","record_sha256":"002dd418917b972d3307c5d1c225b7fb5d7f6b4e40f0362c36897a9740e5d5b6"},
    {"record_id":"author-pr223-plan-004","pr":223,"pr_url":"https://github.com/Homie-Improvement/miru/pull/223","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Forwarded agent entries lose their pending-review status.","finding_evidence":"An ordinary agent create changes its stored status and response from pending_review to not_required when Timekeeping owns entries. TimesheetEntry#ensure_review_status_is_set assigns pending_review locally. However, plan item 3 excludes review_status from the forwarded body, EntryWriter#create_entry supplies no derived status, and timekeeping/db/schema.rb defaults it to not_required. Gateway hydrates that answer without running validation, so the agent receives not_required and readers see an entry that does not require review. The existing agent create spec explicitly expects pending_review in storage and JSON. Disclosure does not preserve this behavior. Derive pending_review from the trusted agent association in each owning store and the fake, without allowing callers to choose approval status. Add a forwarded-create assertion for both storage and response.","recorded_resolution":"fixed","recorded_correction":"Each owning store now derives pending_review from the agent_id the server sends; callers cannot choose it. The tests check both the stored value and the response.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T04:25:26.450Z","answer_recorded_at":"2026-10-27T04:28:07.925Z","first_independent_review_at":"2026-10-27T07:43:04Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"3ccd26a04f6f59f6e52ca6ad1447652758e0f8e20d7ba6c8e6793336c794732e","record_sha256":"144b602f524ec4340c5bb72353bc9c7288fdbe802846a2bf149c2bd3903c5444"},
    {"record_id":"author-pr223-plan-005","pr":223,"pr_url":"https://github.com/Homie-Improvement/miru/pull/223","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Forwarded agent entries must retain the existing review requirement.","finding_evidence":"The plan explicitly defers changing forwarded agent entries from `not_required` to `pending_review`, although local agent entries already require review. Once these writes move to Timekeeping, ordinary agent creates will bypass that existing review requirement. Users will see entries as not requiring review solely because storage ownership changed. Preserve the model's review default in the provider and test it through the agent route, including the returned JSON. Disclosure in the pull request does not preserve the review workflow.","recorded_resolution":"fixed","recorded_correction":"Forwarded agent entries now keep pending_review, tested through the agent route including the returned JSON.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T04:25:26.450Z","answer_recorded_at":"2026-10-27T04:28:07.925Z","first_independent_review_at":"2026-10-27T07:43:04Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"bb8f2154f81d1b7c154530ed855158d4ccb2bacc05101370a810d0d7faa8ef1f","record_sha256":"bcb789f64900ce99a115d2af210338825e5cfb0701445a6b0ded932dd4607d7a"},
    {"record_id":"author-pr223-plan-006","pr":223,"pr_url":"https://github.com/Homie-Improvement/miru/pull/223","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Keep agent entries pending review when forwarding them.","finding_evidence":"Deferring the review_status difference changes the agent response during ordinary use. app/models/timesheet_entry.rb sets agent entries to pending_review, and snippet exposes that value. spec/requests/api/v1/agent/timesheet_entries/create_spec.rb:48 and :59 require it in storage and JSON. The provider instead defaults review_status to not_required in timekeeping/db/schema.rb:47, and EntryWriter#create_entry does not override it. Moving the controller therefore tells callers that an agent entry needs no review. Disclosure does not preserve the promised JSON. Include server-derived pending_review behavior in this change, with equivalent behavior in LocalOwner and the fake. Test the forwarded response value, not only its keys, without allowing callers to set review_status.","recorded_resolution":"fixed","recorded_correction":"Agreed. Timekeeping, the prepared local store and the test fake now set pending_review on create when the entry names an agent, from one shared rule; callers still cannot set it.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T04:25:26.450Z","answer_recorded_at":"2026-10-27T04:28:07.925Z","first_independent_review_at":"2026-10-27T07:43:04Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"497034f1ff6ef3790c06e024f40f4282d9f6daf25e6cf6a0c0a9a80fa9f2a3fa","record_sha256":"2c07144dc878edd3a1ccb87e89cac4c0ab953a2c50a569f79440ec63ce34d7af"},
    {"record_id":"author-pr223-plan-007","pr":223,"pr_url":"https://github.com/Homie-Improvement/miru/pull/223","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"Keep the agent review default in the owning writer.","finding_evidence":"The plan moves agent creation away from the model that decides whether an entry needs review, but leaves that decision behind. app/models/timesheet_entry.rb:161 sets agent entries to pending_review. timekeeping/app/services/timekeeping/entry_writer.rb:create_entry copies command.entry_fields without deriving review_status; timekeeping/app/models/entry.rb has no callbacks, and timekeeping/db/schema.rb:47 defaults it to not_required. Thus ordinary forwarded agent creates tell users that review is unnecessary, unlike today's response. Disclosure does not preserve this behavior. Give the owning writer responsibility for this default, using one pure rule shared with the prepared owner and fake. Keep review_status out of caller-writable fields. Add tests that compare review_status across local and forwarded agent creates instead of deferring this rule.","recorded_resolution":"fixed","recorded_correction":"The review default now lives in one shared rule in the timekeeping_rules gem, used by Timekeeping's writer, the prepared local store and the fake. review_status stays out of writable fields.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T04:25:26.450Z","answer_recorded_at":"2026-10-27T04:28:07.925Z","first_independent_review_at":"2026-10-27T07:43:04Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"8b3e361b49f370f60e8eaf280126b577ba85034348d2f2d630aaf561ac05223b","record_sha256":"09795a1b686b21f0dc4701630ce24fb0450b6d71c5d952544dfd5b1a89ab12cd"},
    {"record_id":"author-pr223-plan-008","pr":223,"pr_url":"https://github.com/Homie-Improvement/miru/pull/223","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The deferred late-commit race contradicts the promised agent-deletion guarantee.","finding_evidence":"R9 promises that a forwarded create never leaves an entry naming a deleted agent. However, the assumptions defer the late-commit window described under Scope Boundaries. If the gateway times out while the provider still processes the create, its transaction releases the agent lock. Agent deletion can then detach existing entries before the provider commits the new entry. The resulting entry still names the deleted agent. Holding the lock until the forwarded call returns, as plan item 5 specifies, does not cover this ordering. Resolve this contradiction by including a mechanism that prevents or reconciles late commits, and test this timeout ordering alongside the two successful-call orderings. Disclosure alone cannot establish R9.","recorded_resolution":"fixed","recorded_correction":"Agreed. Timekeeping's create now takes the same per-agent lock detach takes and refuses an agent that already has a stored detach, so a late create cannot land after the detach.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T04:25:26.450Z","answer_recorded_at":"2026-10-27T04:28:07.925Z","first_independent_review_at":"2026-10-27T07:43:04Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"f59e72726b107aa74f464b4a99d5ad143c100122cc6fa3da949633d7769dddb2","record_sha256":"14c6d4f8c3142cc6802daf558fecc5bd2e720bf12e10f46d5954f719c91cb52f"},
    {"record_id":"author-pr223-plan-009","pr":223,"pr_url":"https://github.com/Homie-Improvement/miru/pull/223","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"blocker","finding_title":"The implementation omits the provider change needed to keep agent entries pending review.","finding_evidence":"The assumption that review_status can wait for follow-up prevents the plan from keeping the agent response unchanged. TimesheetEntry#ensure_review_status_is_set assigns pending_review when an agent is present. The existing agent create spec checks that value in the response. In contrast, timekeeping/app/services/timekeeping/entry_writer.rb creates an Entry without deriving review_status, and timekeeping/db/schema.rb:47 defaults it to not_required. EntryPresenter returns that value, and EntryReader preserves it when building the response. Each normal forwarded agent create therefore reports that review is unnecessary. Add the provider-side derivation to an implementation unit, keep the fake consistent, and test the forwarded response against the legacy response. Disclosure alone does not preserve this behavior.","recorded_resolution":"fixed","recorded_correction":"A new unit adds the provider-side review default, keeps the fake consistent, and compares the forwarded response's review_status with the legacy one.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T04:25:26.450Z","answer_recorded_at":"2026-10-27T04:28:07.925Z","first_independent_review_at":"2026-10-27T07:43:04Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"db4a30cfe238f65be4531c8abbcd61366079340a120660119ab7ed5d1418cef0","record_sha256":"4947c81ecc287204d0e413ab35e136ca32c3cfab7669a68ba601f49d9a351c84"},
    {"record_id":"author-pr223-plan-010","pr":223,"pr_url":"https://github.com/Homie-Improvement/miru/pull/223","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"high","finding_title":"The planned locks cannot protect agent deletion after a forwarded request times out.","finding_evidence":"The deferred late-commit window leaves the promised race guarantee unbuilt. EntryCommand.build reads agent facts before the provider transaction. EntryWriter then checks those saved facts, while ServiceCommands detaches only entries already present. A provider create can pause before insertion until the HTTP client times out. Gateway then rolls back and releases the agent lock. Agent destruction can detach zero entries and commit before the provider resumes and inserts an entry naming the deleted agent. The proposed two-connection success tests do not cover this ordering. Add an implementation step that orders provider creates against detach commands, or a durable recovery mechanism that prevents deletion from completing with an unresolved create. Test the timeout ordering with a delayed provider write. Do not defer this part of the stated guarantee.","recorded_resolution":"fixed","recorded_correction":"Timekeeping now orders creates against detach-entries with the advisory lock detach already takes, and the stored detach receipt refuses any create that arrives later.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T04:25:26.450Z","answer_recorded_at":"2026-10-27T04:28:07.925Z","first_independent_review_at":"2026-10-27T07:43:04Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"67602a1e0350da8eabe889196fa0a6922280b7e2ddd0cd7dc3cfedf1a338bcf8","record_sha256":"f470df2295de2c851c69b885e161741491493296cf72b8e448277a6bd8b9a51a"},
    {"record_id":"author-pr224-build-001","pr":224,"pr_url":"https://github.com/Homie-Improvement/miru/pull/224","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Helper contract spec repeats the forwarded-import owner example","finding_evidence":"The change exists to remove near-duplicate forwarded-import examples, but the new helper spec adds one back. Its first example runs a full Harvest import through the fake and checks what the DataImportJob tenant owner checks: no local TimesheetEntry, two POSTs to /v1/companies/<id>/entries, user_id and project_id on every create, and status completed with 2 imported and 0 failed. A change to the importer's forwarding contract then needs edits in both files, which is the drift this refactor was meant to remove.","recorded_resolution":"fixed","recorded_correction":"The helper's first example now checks only what the helper does: the ownership, the fake's generation, its registrations, and that creates reach the fake. What a forwarded import sends stays in the importer and job owners.","correction_target":"code_commit","source_path":"spec/contracts/timekeeping/forwarded_timekeeping_helpers_spec.rb","source_line":44,"fix_commit":"9d2513014b4f70729557ead6f077b305dd8e0794","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/9d2513014b4f70729557ead6f077b305dd8e0794","finding_recorded_at":"2026-10-27T06:00:49.273Z","answer_recorded_at":"2026-10-27T06:24:09.373Z","first_independent_review_at":"2026-10-27T06:27:13Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"72c6db31bc85b8ae77c3083142cc15b8fdda943badd45a8c1c4c1ee5ac091af3","record_sha256":"8fcd5a30ccb9b733568469754b3d5314ae68de745209ebfe10285fec64cc55b0"},
    {"record_id":"author-pr224-build-002","pr":224,"pr_url":"https://github.com/Homie-Improvement/miru/pull/224","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Job spec spies on helper internals instead of the job","finding_evidence":"The left-company DataImportJob example now spies on the fake's register_user and counts how often the shared arrange_forwarded_timekeeping helper calls it. That tests the helper, not the job: it can fail only when the helper changes, never because of DataImportJob. The helper's contract ends up split between its own spec and a job spec, so a later change to how the helper registers users breaks an unrelated job example. The job behaviour that matters is already asserted further down in the same example.","recorded_resolution":"fixed","recorded_correction":"The register_user spy is gone from the job example. The check that the helper registers exactly the users it is given now sits in the helper spec's reset example.","correction_target":"code_commit","source_path":"spec/jobs/data_import_job_spec.rb","source_line":156,"fix_commit":"9d2513014b4f70729557ead6f077b305dd8e0794","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/9d2513014b4f70729557ead6f077b305dd8e0794","finding_recorded_at":"2026-10-27T06:00:49.273Z","answer_recorded_at":"2026-10-27T06:24:09.373Z","first_independent_review_at":"2026-10-27T06:27:13Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"5ca3d06331572d8cf196a1eabdecc6e5592c8c467839e695f689bdac04a6a848","record_sha256":"d067b8a06e5085a7d6e0f2f6f49284fca497db606ae401b8f0248b21f386192e"},
    {"record_id":"author-pr224-plan-001","pr":224,"pr_url":"https://github.com/Homie-Improvement/miru/pull/224","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Move the zero-copy search until after the duplicate examples are deleted.","finding_evidence":"The first unit cannot meet its zero-copy verification using the call sites listed in the planning decisions. The duplicate groups still contain `update_columns(owner: \"timekeeping\", generation: 2)` at spec/services/imports/harvest_time_entries_importer_spec.rb:692 and spec/jobs/data_import_job_spec.rb:200. The later units delete those groups, but both depend on the first unit. Move the zero-copy search to the final verification rather than refactoring setup that the next units will delete.","recorded_resolution":"fixed","recorded_correction":"The search for leftover copied ownership setup now runs in the final verification, after the later units delete the duplicate groups, so the groups being deleted are not refactored first.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T05:21:29.880Z","answer_recorded_at":"2026-10-27T05:22:21.545Z","first_independent_review_at":"2026-10-27T06:27:13Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"2ad45c547bf15932639d5766cb9760147aea0ad7ffa45707025d83c57aa62e95","record_sha256":"bb3ee9f2406a5b09b6e9cceb2090920c113d1edebb34111964289b0072d1eb4a"},
    {"record_id":"author-pr224-plan-002","pr":224,"pr_url":"https://github.com/Homie-Improvement/miru/pull/224","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The helper migration and its verification disagree about when duplicate setup disappears.","finding_evidence":"The first unit must leave no copied `update_columns(owner: \"timekeeping\", generation: 2)` setup in either spec file. However, its approach follows plan item 4, which lists only the surviving groups. The forwarded rerun and job groups scheduled for deletion still exist until the second and third units. An implementer cannot follow that call-site list and satisfy the first unit's verification. Either migrate those temporary groups too, or move the no-copies check after consolidation and limit the first unit's stated scope to the listed groups.","recorded_resolution":"fixed","recorded_correction":"The first unit now checks only the groups it migrates; the search for leftover copied setup moved to the final verification, after the duplicate groups are deleted.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T05:21:29.880Z","answer_recorded_at":"2026-10-27T05:22:21.545Z","first_independent_review_at":"2026-10-27T06:27:13Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"03934b100ca40cee6640fb71ca58c462c386d4ef683ae3c3dd0d471870b28a45","record_sha256":"1d13a963648b950f6566710b7543c561f203bfb0390fdeb256739e5d3d42832f"},
    {"record_id":"author-pr224-plan-003","pr":224,"pr_url":"https://github.com/Homie-Improvement/miru/pull/224","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The rerun merge drops the assertion that the first row is retried first.","finding_evidence":"The deleted rerun example checks `expect(rerun_keys.first).to eq(first_run_keys.first)` in spec/services/imports/harvest_time_entries_importer_spec.rb. The surviving example groups requests by note, so it cannot check request order. Moving only the size and uniqueness assertions, as specified in plan item 1, loses this check. For example, sorting rows by descending duration on retries would reverse the distinct rows but leave the identical-row example unchanged. The surviving example would still pass. The duplicate-create break in plan item 2 would not expose this loss. Move the first-key assertion too, and verify that reversing the distinct rows on retry makes the surviving example fail.","recorded_resolution":"fixed","recorded_correction":"The surviving forwarded rerun example now also checks that the rerun's first create reuses the first run's key, and its break check reverses the row order on retry to prove it fails.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T05:21:29.880Z","answer_recorded_at":"2026-10-27T05:22:21.545Z","first_independent_review_at":"2026-10-27T06:27:13Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"ba24e28172a65c0c6f68560d5e501125e3656cd2eb92252501d95c042808ad7a","record_sha256":"ff5874b82b1516033faa60a1cf0254e9b062711bff779f95ce970e6fb4a853ac"},
    {"record_id":"author-pr230-plan-001","pr":230,"pr_url":"https://github.com/Homie-Improvement/miru/pull/230","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The index discrimination check does not name a distinct mutation.","finding_evidence":"The same-invoice example already expects the unique-index recovery path to return `retryable` with `unresolved_operation`. However, plan item 7 describes its temporary change as \"change the index test to rely on the rescue.\" That describes the intended path rather than a distinct change that should make the example fail. The implementation unit and completion criteria nevertheless require this failure. Name the exact temporary change and the assertion it must break, so the check can distinguish working index enforcement or recovery from a defect.","recorded_resolution":"fixed","recorded_correction":"The check now names two concrete temporary changes. Removing the RecordNotUnique rescue must break the retryable answer. Dropping the index must break the wait and one-operation checks.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T06:35:33.268Z","answer_recorded_at":"2026-10-27T06:36:28.644Z","first_independent_review_at":"2026-10-27T07:37:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"413701a48906c8da5d49a89e781b2446b5df152e63bc0308c6b025e774427536","record_sha256":"258b5b938b0df964dfe1c6cdefaae3f9941ed04e9fedae0869e7d5b31a434f4b"},
    {"record_id":"author-pr230-plan-002","pr":230,"pr_url":"https://github.com/Homie-Improvement/miru/pull/230","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The planned races do not detect a reservation flag read before the entry lock wait.","finding_evidence":"The rollback case in plan item 4 starts with no committed reservation. The holder's uncommitted insert stays invisible, so the waiter reads reserved=false both before the wait and after rollback. If the holder commits instead, prepare refuses before record_locked_flips uses that flag. The finish races wait on the operation lock before reading entries. Thus all five cases can pass if lock_entries reads reserved_rows before taking the entry lock. app/services/invoices/prepared_reservations.rb:205-213 separates those reads; lines 313-320 use the flag to decide version and snapshot changes. Add a schedule where a committed reservation disappears while prepare waits, such as prepare behind an abort. Assert exact changes and verify that moving the reservation read before the entry lock makes the test fail.","recorded_resolution":"fixed","recorded_correction":"Added a prepare-behind-abort example: a committed provisional row disappears while the prepare waits, so a flag read before the lock skips the version bump and snapshot and the exact counts fail.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T06:35:33.268Z","answer_recorded_at":"2026-10-27T06:36:28.644Z","first_independent_review_at":"2026-10-27T07:37:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"bad86b818fc6788c5b6c7ceaf78ffa6ba4127705d8800b0fead02da6f4975f74","record_sha256":"b6d10366beae5f7072a6bd62de628ad327f840ce5d8de0479d9df7abbae776b4"},
    {"record_id":"author-pr230-plan-003","pr":230,"pr_url":"https://github.com/Homie-Improvement/miru/pull/230","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The rollback control can pass with its named entry lock removed.","finding_evidence":"plan item 7 requires every example to fail when its named lock is removed, and the stated goal requires stopping otherwise. In the same-entry rollback case, removing .lock(ROW_LOCK) lets the waiter reach the provisional reservation insert. That insert waits on the holder's unique-index conflict. After the holder rolls back, the waiter prepares successfully with exactly one version bump and snapshot. blocked_by? still identifies the same holder, and every planned assertion passes. Treat this example as a rollback control rather than requiring it to distinguish the entry lock, or choose a schedule whose outcome changes without that lock. Otherwise a valid control forces an unnecessary implementation stop.","recorded_resolution":"fixed","recorded_correction":"Agreed. The rollback example is now labelled a control for the first-writer-rolled-back outcome and is exempt from the lock-removal check; the new prepare-behind-abort example proves the entry lock instead.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T06:35:33.268Z","answer_recorded_at":"2026-10-27T06:36:28.644Z","first_independent_review_at":"2026-10-27T07:37:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"eda6e552a820fbb4f30f79dbd86025bf092f41f4344c9031826c9c410f15475e","record_sha256":"75e5c426efcbecfff0f1f25d613d62263f13dea209dfa424a5440e6286b135cb"},
    {"record_id":"author-pr230-plan-004","pr":230,"pr_url":"https://github.com/Homie-Improvement/miru/pull/230","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The rollback scenario cannot prove the post-lock reservation read or detect removal of the entry lock.","finding_evidence":"The holder's new reservation is uncommitted, so the waiter sees reserved=false before the wait and after rollback. Moving the flag read before the wait therefore leaves this example passing. The holder-commits case refuses before comparing flags, and the finish races return after locking the settled operation. None detects the stale flag described in plan item 4. Also, removing the entry lock lets the rollback waiter block at Reservation.insert_all! on index_reservations_one_provisional_per_entry (db/schema.rb:1081), then succeed with the expected version and snapshot changes. That contradicts plan item 7's required failure. app/services/invoices/prepared_reservations.rb:208-209 separates locking from reading precisely to refresh this flag. Add a schedule where a committed reservation is visible before the wait but disappears afterward, such as abort against prepare. Check t","recorded_resolution":"fixed","recorded_correction":"Same fix as the flag-read finding: the prepare-behind-abort example covers the post-lock read, and the rollback example is now a labelled control, not a proof of the entry lock.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T06:35:33.268Z","answer_recorded_at":"2026-10-27T06:36:28.644Z","first_independent_review_at":"2026-10-27T07:37:16Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"7d7a8c812c7c3de76157eb7e04077c58cdf5c69e063d2a6e8a6eebd6714b991d","record_sha256":"0c0819876cdbeaed63ce5d022b24d8bc60214fc97d6851737c366a3011f20fa0"},
    {"record_id":"author-pr238-build-001","pr":238,"pr_url":"https://github.com/Homie-Improvement/miru/pull/238","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Exactly-one-correction tally and its comments claim duplicate detection the replacing fake cannot give","finding_evidence":"The comment at line 441 says this check proves each corrected entry keeps exactly one correction after the checkpoint step runs twice. The double-crash group's header comment at line 489 also says a restart that duplicated corrections would show. But the in-memory store deletes any earlier correction for the same entry before it appends the new one (line 59). A runner that wrote the snapshots twice, or ten times, still leaves one per entry, so the tally passes either way and claims a duplication guard it does not provide. It also repeats the match_array on line 444. The runner has no defect here, because rewriting corrections is harmless under the store's replace-per-entry contract. The problem is that the test claims more detection than it has. The checks that do catch a harmful rewrite are the version-1 assertion and the digest comparison.","recorded_resolution":"fixed","recorded_correction":"Removed the tally check and its comment. I reworded the double-crash comment: the store keeps one correction per entry, so the test checks capture-time reuse and the digest. The spec passes, 11 examples.","correction_target":"code_commit","source_path":"spec/lib/timekeeping/handoff/runner_legacy_handoff_spec.rb","source_line":445,"fix_commit":"e7b294366fd6a9d8e6e55359bb91ed8f5a521393","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/e7b294366fd6a9d8e6e55359bb91ed8f5a521393","finding_recorded_at":"2026-10-27T08:02:13.025Z","answer_recorded_at":"2026-10-27T08:44:37.480Z","first_independent_review_at":"2026-10-27T09:13:44Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"93ff148cc33bdea65747803db9c56870f8ff4239858f7ee9a252a10ee791e1e7","record_sha256":"4f1a2297dcee89d0929ca0cc71ec9346a5916ccc518c55ea0018455316da4c74"},
    {"record_id":"author-pr238-plan-001","pr":238,"pr_url":"https://github.com/Homie-Improvement/miru/pull/238","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Skipping correction writes only on resume cannot make these examples fail.","finding_evidence":"The required discrimination check skips write_correction_snapshots on resume, but both interruptions happen after the first attempt has saved corrections. The fake retains them across runner calls in spec/lib/timekeeping/handoff/runner_legacy_handoff_spec.rb:56. After the recorded checkpoint, Steps.decide resumes at sequence, so it never calls the correction writer again (lib/timekeeping/handoff/steps.rb:83). After corrections, checkpoint runs again, but omitting its correction write leaves the previously saved corrections intact. The planned assertions therefore cannot detect this mutation. Instead, temporarily omit correction writes on the initial attempt as well, and require both examples to reject the missing corrections. Update the completion criteria to match that check.","recorded_resolution":"fixed","recorded_correction":"Agreed. The correction-loss check now drops correction writes on every attempt, including the first, and both examples must go red under it.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T07:12:06.333Z","answer_recorded_at":"2026-10-27T07:12:56.474Z","first_independent_review_at":"2026-10-27T09:13:44Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"50ed4ec1e1405d1d7da12ed14bfa90f57a5c5f17fd2368bdb98bc0a493b99f8d","record_sha256":"9b46067ac463f89e91d1f91945d9ade718ceb8c757cd71060c959f485bea8291"},
    {"record_id":"author-pr238-plan-002","pr":238,"pr_url":"https://github.com/Homie-Improvement/miru/pull/238","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Skipping correction writes only on resume need not make either example fail.","finding_evidence":"Both interruption points occur after the first attempt has written corrections. The fake's writer at `spec/lib/timekeeping/handoff/runner_legacy_handoff_spec.rb:56` replaces corrections by entry ID, so repeating those writes preserves the same state. After recorded checkpoint, recovery skips the writer entirely. Therefore, the proposed mutation that skips `write_correction_snapshots` only on resume leaves the expected corrections and digest intact. Remove the requirement that both examples reject this mutation. Instead, validate their assertions with a mutation that changes the final correction state, such as omitting correction writes on the first attempt.","recorded_resolution":"fixed","recorded_correction":"Same fix as the other correction finding: the mutation now omits correction writes on the first attempt too, so the final correction state really changes.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T07:12:06.333Z","answer_recorded_at":"2026-10-27T07:12:56.474Z","first_independent_review_at":"2026-10-27T09:13:44Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"7fa8aaf5b40b74337e1939f9cc9907101d3adf379ce1a30990605473f14150c0","record_sha256":"0f225121d9714f4361099df2515b0f54265586fa7233599544e6071a7115bbaf"},
    {"record_id":"author-pr238-plan-003","pr":238,"pr_url":"https://github.com/Homie-Improvement/miru/pull/238","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"The derived control does not implement the promised comparison with an uninterrupted run.","finding_evidence":"Requirement R4 promises equality with an uninterrupted control run, but plan item 1 explicitly says the control is never run. The planned helper computes an expected load instead. The runner's reconciliation check explains why that load should match a successful run, but the new tests never establish that an uninterrupted run actually produces it. This leaves the promised comparison untested and could hide a difference between uninterrupted and resumed execution. Include an executed uninterrupted control, with isolated ownership state and comparable source data, and compare its complete digest with each resumed result.","recorded_resolution":"fixed","recorded_correction":"Added a third group that runs a real uninterrupted cutover at the same fixed time and asserts its full digest and corrections equal the derived control, so the resumed runs are tied to an executed run.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T07:12:06.333Z","answer_recorded_at":"2026-10-27T07:12:56.474Z","first_independent_review_at":"2026-10-27T09:13:44Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d52643a328102896695ef76914f834e387785d3fdec78b4ee6089bc0a4e6d91b","record_sha256":"c55e009eca838bc9c3c74b15a6d4ccdc51647002275f075f84a1e77929266cd7"},
    {"record_id":"author-pr238-plan-004","pr":238,"pr_url":"https://github.com/Homie-Improvement/miru/pull/238","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The non-empty company check fails for valid empty tables.","finding_evidence":"The planned check requires seeded companies in every table's digest. However, `lib/timekeeping/handoff/legacy_source.rb:102-104` always exports empty `invoice_operations` and `idempotency_records` tables. `gems/timekeeping_handoff/lib/timekeeping/handoff/checkpoint.rb:138` builds an empty company map for each empty table. Both new examples would therefore fail even when recovery works correctly. Keep the whole-digest comparison, but check company coverage on populated tables such as `entries` and `reservations`. Assert that the other tables match the control, including their empty maps.","recorded_resolution":"fixed","recorded_correction":"Same fix as the other empty-table finding: coverage is asserted on entries, reservations and entry_audits, and the two always-empty tables are expected to have empty company maps.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T07:12:06.333Z","answer_recorded_at":"2026-10-27T07:12:56.474Z","first_independent_review_at":"2026-10-27T09:13:44Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"49028742465a1002ab739b001dcf6a05e145392321ceafecc2d52489a6bb80af","record_sha256":"266692f02fa39d01f4a6b4bdba27b0672f769fc15a2bc4aa583cbc6e0b5aad22"},
    {"record_id":"author-pr238-plan-005","pr":238,"pr_url":"https://github.com/Homie-Improvement/miru/pull/238","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The planned company-set assertion cannot pass for tables that the legacy source always leaves empty.","finding_evidence":"The planned assertion requires every table's digest to contain all seeded companies. However, lib/timekeeping/handoff/legacy_source.rb:102–104 always exports empty invoice_operations and idempotency_records tables. In gems/timekeeping_handoff/lib/timekeeping/handoff/checkpoint.rb:138, digest_of groups existing rows by company, so these tables have empty company maps. Both new examples would fail against the unchanged runner. Keep the whole-digest comparison, but check each table's company set against the independently built control. Assert seeded-company coverage on populated tables and explicitly check the expected empty tables.","recorded_resolution":"fixed","recorded_correction":"Confirmed in legacy_source.rb: invoice_operations and idempotency_records are always empty. Seeded-company coverage is now asserted on the populated tables only; the empty ones are checked as empty.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T07:12:06.333Z","answer_recorded_at":"2026-10-27T07:12:56.474Z","first_independent_review_at":"2026-10-27T09:13:44Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"72153ce4b5e8f323a2b238c9a58f492efcb74c321a9452c50af4479486c69c57","record_sha256":"ac502af254d0e981e542c19f0318338795744e2212bcef1fc18679344b3bbf2b"},
    {"record_id":"author-pr239-build-001","pr":239,"pr_url":"https://github.com/Homie-Improvement/miru/pull/239","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Context comment says later groups set ownership; admit group does not","finding_evidence":"The new comment tells a reader that every group after the shared-ownership context sets prepared ownership itself. The \"admit\" group never sets it, and it ran without ownership before this change too. A maintainer who trusts the comment may assume admit runs under prepared ownership, or may move groups around on the belief that each group configures itself. Rewording the comment to say what is true keeps the one place that explains the layout accurate.","recorded_resolution":"fixed","recorded_correction":"Reworded the comment. Later groups set ownership inline where they need it, the line-claim groups only after their legacy fixtures, and admit needs none. The spec and layout check pass.","correction_target":"code_commit","source_path":"spec/services/invoices/prepared_reservations_spec.rb","source_line":12,"fix_commit":"ba4a6fb5fe7415fc894cd62365271233c2f4ccac","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/ba4a6fb5fe7415fc894cd62365271233c2f4ccac","finding_recorded_at":"2026-10-27T08:04:29.179Z","answer_recorded_at":"2026-10-27T08:41:08.521Z","first_independent_review_at":"2026-10-27T09:11:54Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"e066279a9099c582e9bc05f43c034ef3a334c1a83c9b713984905d3d22023c02","record_sha256":"15987b7e5376e093e515a8830fd0fbddbb54307c9b68b1bfd0f580b138bc8ca9"},
    {"record_id":"author-pr239-plan-001","pr":239,"pr_url":"https://github.com/Homie-Improvement/miru/pull/239","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"Keeping an unmatched copy conflicts with the required single example per contract.","finding_evidence":"The stop condition, merge rule and second implementation unit allow an unmatched copy to remain as a separate example. However, the requirements and that unit's verification require exactly one example per listed contract. Naming the retained copy in the pull request does not resolve this contradiction. State that an unmatched setup must be incorporated into the surviving example, or stop and revise the merge before declaring the work complete. A separate example can remain only when it tests a distinct contract outside the listed set.","recorded_resolution":"fixed","recorded_correction":"The stop condition, merge rule, second unit and done list now agree. A setup that does not fit is merged into the surviving example, or the work stops. No second example of a listed contract stays.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T07:19:11.667Z","answer_recorded_at":"2026-10-27T07:20:18.302Z","first_independent_review_at":"2026-10-27T09:11:54Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"1c9f30764776ff6ca2ede7d7692f7004962887d1e0b54202fd80074e8a8913ac","record_sha256":"fb3be430c4e0dc023864952064719d6affac600d5c4a882e7743463cd314f374"},
    {"record_id":"author-pr239-plan-002","pr":239,"pr_url":"https://github.com/Homie-Improvement/miru/pull/239","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The combined ordering example needs an explicit scope for its final state assertions.","finding_evidence":"The merge rule runs the committed ordering and aborted ordering on two separate operations in one example. It also retains the expectation that reservations equal zero and operation states equal ['aborted']. Those expectations cannot describe all rows after the first operation has committed. The second implementation unit repeats this ambiguous final state. Specify that these expectations select only the second operation and its reservations. Keep separate state assertions for the first operation, so the combined example does not fail because it includes the committed operation.","recorded_resolution":"fixed","recorded_correction":"The combined ordering example now checks reservations=0 and operations ['aborted'] on the second operation and its invoice only. The first operation keeps its own committed-state checks.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T07:19:11.667Z","answer_recorded_at":"2026-10-27T07:20:18.302Z","first_independent_review_at":"2026-10-27T09:11:54Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"66e61bf716fbdd590e82e1c4844d6af688809149d589e29fd94b63057cb512bd","record_sha256":"fb1d6a00465d5b5b1ae0aae437dca940a7954fd31708d2202b128756c9d1447a"},
    {"record_id":"author-pr239-plan-003","pr":239,"pr_url":"https://github.com/Homie-Improvement/miru/pull/239","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The fallback permits duplicate examples that the acceptance criterion requires removing.","finding_evidence":"The merge rule allows an unmatched copy to remain as a separate example, and the completion criteria accept it when the pull request names it. That disclosure does not satisfy the ticket's requirement for one example per listed contract. Keeping distinct coverage is necessary, but it need not mean keeping a second example: the plan already combines both delivery orderings within one example. Require each listed contract's example to exercise any distinct setups that cannot share one input. Keep a separate example only when the comparison establishes that it tests a different contract, not merely another input for a listed contract.","recorded_resolution":"fixed","recorded_correction":"The fallback is gone. A copy with a distinct setup is folded into its contract's single example as a further scenario. Only a copy shown to test a different contract stays separate.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T07:19:11.667Z","answer_recorded_at":"2026-10-27T07:20:18.302Z","first_independent_review_at":"2026-10-27T09:11:54Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"a60ba83d58b4b80ab63b662e09680daf3b187f447ae93315a0100bf87ec5530c","record_sha256":"21892132e640ef5d94c2034251ae133ccc65afaa1a06158144a6a8e7207aac5f"},
    {"record_id":"author-pr239-plan-004","pr":239,"pr_url":"https://github.com/Homie-Improvement/miru/pull/239","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The planned fault cannot make the cross-company mark_billed example fail.","finding_evidence":"The break-and-watch check targets the company check in `decide` for both cross-company examples. However, `app/services/invoices/prepared_reservations.rb:159` defines `mark_billed`, which never calls `decide`. Its entry selection uses `lock_entries`, defined at line 205, and its update also filters by company. Changing `decide` cannot exercise the billing example's protection against foreign entries. The plan would therefore stop despite a correct consolidation. Keep the `decide` fault for the prepare example, but give the billing example a separate fault in the company-scoped entry selection that it actually calls.","recorded_resolution":"fixed","recorded_correction":"Confirmed: `mark_billed` never calls `decide`. The billing example's planned fault now removes the company filter from `lock_entries`, `reserved_rows` and the `mark_billed` update.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T07:19:11.667Z","answer_recorded_at":"2026-10-27T07:20:18.302Z","first_independent_review_at":"2026-10-27T09:11:54Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d78d3a87f6107e1f0498e68978f23e7b542e5b22b4dd1c421edb02fb2e73a9ad","record_sha256":"b88b4099461c39b10804ca8df2edcce521433bb4cfdec8a5adaa33df84c84071"},
    {"record_id":"author-pr239-plan-005","pr":239,"pr_url":"https://github.com/Homie-Improvement/miru/pull/239","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"low","finding_title":"The plan goes beyond the issue: Keep cited example descriptions and rewrite learning-document references when a cited example is removed.","finding_evidence":"Keeping existing descriptions is a permissible preservation measure, but requiring edits to learning-document evidence lines adds documentation work the issue does not request.\n\nThe issue asks for nothing like it.\n\nAnswer by removing it from the plan or deferring it to follow-up work the plan names as out of scope; or decline with your reason.","recorded_resolution":"fixed","recorded_correction":"The plan still keeps the example descriptions the learning documents cite, but it no longer edits those documents. The one stale citation is listed as follow-up work.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T07:20:52.195Z","answer_recorded_at":"2026-10-27T07:22:23.609Z","first_independent_review_at":"2026-10-27T09:11:54Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"72f0abdf4f5c06bca885f4e55429f544c81b760c5cf94c2b24f1c6337b19c45d","record_sha256":"e3ac876b527f1a5a67fc4aea4cadf1c8219cd8a18a79beb106f10d19a0599a40"},
    {"record_id":"author-pr239-plan-006","pr":239,"pr_url":"https://github.com/Homie-Improvement/miru/pull/239","cohort":"merged_implementation","stage":"plan","review_round":2,"recorded_severity":"low","finding_title":"The plan goes beyond the issue: Rewrite learning-document citations that name examples removed by the consolidation.","finding_evidence":"The issue requests a spec refactor, not maintenance of learning-document evidence lines. This is a small, related documentation addition, but it extends the required work beyond the stated contract.\n\nThe issue asks for nothing like it.\n\nAnswer by removing it from the plan or deferring it to follow-up work the plan names as out of scope; or decline with your reason.","recorded_resolution":"fixed","recorded_correction":"The learning-document edit unit is removed from this change. Updating the one citation that now names a merged-away example is listed as follow-up work.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T07:20:52.195Z","answer_recorded_at":"2026-10-27T07:22:23.609Z","first_independent_review_at":"2026-10-27T09:11:54Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"d2b395144c8bf43bcdcba3b16f4fd98e5cb3e1c9907c6b05bb42322ab9284fba","record_sha256":"459be3da44c42453e36fb3e4234722f55e772b5059821d742fda94fe4e2900ab"},
    {"record_id":"author-pr240-plan-001","pr":240,"pr_url":"https://github.com/Homie-Improvement/miru/pull/240","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The failure injection does not specify the logging required by the call assertions.","finding_evidence":"The one-shot failure decision requires later calls to log, but does not require the failed call to log. The next decision expects two logged admit attempts, including that failure. A wrapper that raises before calling the original fake follows the injection instructions but produces only one entry. Specify that the failed attempt is logged before raising, and that each successful attempt is logged exactly once.","recorded_resolution":"fixed","recorded_correction":"The injection rule now says the failing call logs its admit entry first and then raises. Each later call logs exactly once, so the two expected monolith admit entries are fully specified.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T07:10:54.880Z","answer_recorded_at":"2026-10-27T07:11:42.229Z","first_independent_review_at":"2026-10-27T09:00:18Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"7aa51f3e43a08d540ed3883fa3a67de3f7d3463bef2b4644d2bc9c851042c8dc","record_sha256":"f2c4583bd4e931ca0cfb3f614e845fd551a762f6e5500c0199fe09e0bea80c41"},
    {"record_id":"author-pr240-plan-002","pr":240,"pr_url":"https://github.com/Homie-Improvement/miru/pull/240","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The plan gives conflicting instructions when the runner wraps the injected error.","finding_evidence":"The stop conditions require stopping if the runner does not propagate the injected error. The later assumption instead says to assert the wrapped error class and continue. These instructions give different outcomes for the same behavior. State one rule in both places: either stop when the error is wrapped, or allow wrapping and require the assertion to verify the injected failure remains its cause.","recorded_resolution":"fixed","recorded_correction":"I read runner.rb: its only rescue is for the lock timeout, so a failed admit passes out unchanged. The plan now has one rule: assert the exact injected error, and stop if it arrives wrapped.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T07:10:54.880Z","answer_recorded_at":"2026-10-27T07:11:42.229Z","first_independent_review_at":"2026-10-27T09:00:18Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"c1c1d5fe3e1072c9a3883fcb55574d7a260f5354e205a204cbc81b084c875933","record_sha256":"8d306b15d47109ed61cc4b600da3fba735b80fd26e5cf30cba93eefdf8aae329"},
    {"record_id":"author-pr243-build-001","pr":243,"pr_url":"https://github.com/Homie-Improvement/miru/pull/243","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"Runner fakes and row-state helpers copied into all four groups","finding_evidence":"All four top-level groups rebuild the same anonymous Timekeeping store fake, monolith admission fake and [generation, owner, handoff_step] row-state/admits helpers, each under a different name. If the runner's store or admission contract changes, every copy has to change the same way, and a missed copy fails in only one group or quietly stops matching the real store. The copies have already drifted: in the legacy groups advance_entry_id_sequence returns above + 1, while the rollback groups track @next_id. The rubocop:disable Lint/DuplicateMethods exists only because of this copying.","recorded_resolution":"fixed","recorded_correction":"The four groups now share one context at the top of the spec file. It holds the Timekeeping store fake, now max-based in every group, the monolith admission fake, and the row-state and admits helpers. The rubocop disable is gone.","correction_target":"code_commit","source_path":"spec/lib/timekeeping/handoff/runner_incomplete_checks_spec.rb","source_line":18,"fix_commit":"488521a08f93cd72c17aa9397c90379111b58039","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/488521a08f93cd72c17aa9397c90379111b58039","finding_recorded_at":"2026-10-27T08:54:38.943Z","answer_recorded_at":"2026-10-27T09:37:00.737Z","first_independent_review_at":"2026-10-27T10:06:18Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"818db72f0720be9ee971b49d488faefced224a24b3d0c96c2e2fbeb8227a7eb0","record_sha256":"0460ed055af1db5e4cf3f31cef52ccf8157364df7a530c3cf69cae030035710f"},
    {"record_id":"author-pr244-build-001","pr":244,"pr_url":"https://github.com/Homie-Improvement/miru/pull/244","cohort":"merged_implementation","stage":"build","review_round":2,"recorded_severity":"low","finding_title":"New runner specs re-inline intent INSERT and redundant cleanup","finding_evidence":"The file adds insert_committed_intent_for_source_intents_spec, but the last three new groups (lines 508, 540, 585) still copy the same INSERT SQL by hand. Four examples also end in ensure DELETE blocks (lines 385, 522, 565, 605), even though TestDatabaseCleanup.delete_operation_ledger_rows! already clears invoice_operation_intents after every example. The repo learning clean-cross-connection-rows-in-a-last-declared-hook-on-its-own-connection makes that global hook the one place for this cleanup. The file now has two cleanup styles, and any change to the intent columns has to be made in four places.","recorded_resolution":"fixed","recorded_correction":"The last three runner groups now use the shared intent insert helper, and their per-example DELETE blocks are gone. The global after-hook already clears invoice_operation_intents after every example.","correction_target":"code_commit","source_path":"spec/lib/timekeeping/handoff/runner_legacy_handoff_spec.rb","source_line":540,"fix_commit":"af41088add7e004a12e11062b03f93196d4b809d","fix_commit_url":"https://github.com/Homie-Improvement/miru/commit/af41088add7e004a12e11062b03f93196d4b809d","finding_recorded_at":"2026-10-27T09:14:42.261Z","answer_recorded_at":"2026-10-27T09:30:14.381Z","first_independent_review_at":"2026-10-27T10:11:12Z","finding_and_answer_before_pr_creation":false,"retained_finding_body_sha256":"83fe9cf6ee0190edc7be27e3a2330a22f71ccdd9dabd2187c908372c22b1ccfe","record_sha256":"b9c6b3b1e7125b638ecf52109d23ffbd02f39daca683af4ccd686e45fbdfe0ee"},
    {"record_id":"author-pr247-plan-001","pr":247,"pr_url":"https://github.com/Homie-Improvement/miru/pull/247","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"medium","finding_title":"Keep the MCP shared setup on the legacy store.","finding_evidence":"plan item 1 applies `arrange_forwarded_timekeeping` to all three files, but the MCP retry assertions require local persistence. That helper sets owner to `timekeeping` at spec/support/timekeeping/forwarded_timekeeping_helpers.rb:15. After the handoff clears, app/services/timekeeping/gateway.rb:141 forwards creates and returns an unsaved entry instead of inserting a local row. The existing assertions at spec/mcp/tools/write_tools_gateway_spec.rb:48 and :149 therefore cannot pass under this setup. Checking the fake instead would remove the existing coverage of local persistence. Limit the forwarding setup to the agent and CLI contexts that already use it. Keep one shared legacy-store setup and the existing cache cleanup for MCP.","recorded_resolution":"fixed","recorded_correction":"Agreed: the forwarding setup moves the owner to Timekeeping, so local rows would never be written. The MCP file now keeps a legacy-store setup with its cache clearing, and forwarding stays in agent and CLI.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T10:33:32.471Z","answer_recorded_at":"2026-10-27T10:34:28.018Z","first_independent_review_at":"2026-10-27T11:51:10Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"31e129945f988c9b811e633623ea97fedf84e064dfee54f0db69f8c3fb9bf0f1","record_sha256":"d7cc1181f00f398a9954d4851d2026ff3b52f9e2bea22844eb5df1f939443ec3"},
    {"record_id":"author-pr247-plan-002","pr":247,"pr_url":"https://github.com/Homie-Improvement/miru/pull/247","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The stop condition conflicts with the planned role and credential tables.","finding_evidence":"The stop condition says to keep a copy as its own named case when caller role or credential shape changes a code branch. The plan instead requires those differences to become rows inside one example. This affects both the CLI role table and the MCP credential table. One implementer could retain separate examples, while another could merge them as planned. Clarify that branch differences must survive as table rows, and reserve separate examples for differences that cannot share one behaviour example.","recorded_resolution":"fixed","recorded_correction":"The stop condition now says a role or credential difference the code branches on survives as a row of the one example's table; a separate example is kept only for a different behaviour.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T10:33:32.471Z","answer_recorded_at":"2026-10-27T10:34:28.018Z","first_independent_review_at":"2026-10-27T11:51:10Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"31abd4760efe040b4de95240d79cb34822f92165b65e4f94fba9ace192bd2fd3","record_sha256":"5174523a84e68d06ff7df8dc7231b8bfe7eee08abebfa43cd81498e0e727edcb"},
    {"record_id":"author-pr250-plan-001","pr":250,"pr_url":"https://github.com/Homie-Improvement/miru/pull/250","cohort":"merged_implementation","stage":"plan","review_round":1,"recorded_severity":"low","finding_title":"The stop condition forbids the layout expectation change that the plan requires.","finding_evidence":"The stop condition says to stop if an example can pass only after an assertion changes. The layout example needs exactly that change: the shared request helper removes `entries: [`, so the existing source-pattern assertion must change as described in the plan. One implementer could stop while another makes the required edit. Clarify that equivalent source-layout checks may change while every distinct behavioral assertion must remain. Keep the planned replacement pattern and its check that one request includes both entries.","recorded_resolution":"fixed","recorded_correction":"The stop condition now exempts the one planned layout-spec pattern change, which keeps its check that one request carries both entries; every behavioural assertion must still stay as it is.","correction_target":"plan_revision","source_path":null,"source_line":null,"fix_commit":null,"fix_commit_url":null,"finding_recorded_at":"2026-10-27T11:02:33.027Z","answer_recorded_at":"2026-10-27T11:03:24.687Z","first_independent_review_at":"2026-10-27T12:30:46Z","finding_and_answer_before_pr_creation":true,"retained_finding_body_sha256":"4cb22e2bede5e41f2609c2941b9ae1220a494a092e7c7c45cd7b085e9c99850d","record_sha256":"7b291929b457b6ea1ebbd1a572fc18fbe36a2bf5fb6350b6e332aaf18ce48606"}
  ]
}
