Skip to content

Expose durable taxi event-bound repair - #4

Merged
Alex-lop merged 6 commits into
mainfrom
agent/south-start-rework
Aug 10, 2026
Merged

Alex-lop merged 6 commits into
mainfrom
agent/south-start-rework

Conversation

@Alex-lop

@Alex-lop Alex-lop commented Aug 10, 2026 •

Copy link
Copy Markdown
Owner

Outcome

Resolved and merged. The NO_GO note on the intermediate draft applied to a chronology gap that was fixed before merge; the final head passed the required rehearsal and regression checks.

What changed

  • added fixed operator-only taxi injection and bounded watcher polling
  • persisted observed and delivered events and required exact event-bound repair
  • kept the MCP surface at exactly 21 tools
  • updated the 22-step rehearsal, regression coverage, and deterministic Phase 3A artifacts

Validation

  • locked suites: 378 passed, 12 environment-gated skips
  • focused suite: 118 passed, 3 live skips
  • rehearsal: 22/22 offline
  • Phase 3A artifacts: 6/6 byte-exact
  • MCP: 21 tools; no injection tool

@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Alex-lop, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 37 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c0824d3-94a9-4b9b-8bc0-0fa212e0db61

📥 Commits

Reviewing files that changed from the base of the PR and between 8cfff99 and da507f5.

📒 Files selected for processing (3)
  • .github/workflows/tests.yml
  • scripts/rehearse_phase3a_demo.py
  • tests/unit/test_rehearse_phase3a_demo.py
📝 Walkthrough

Walkthrough

The PR adds durable event and observation validation, event-bound repair and publication recovery, a CLI watcher and injector, and expanded integration coverage. Example lineage records now use updated document identifiers and integrity hashes.

Changes

Event-bound repair workflow

Layer / File(s) Summary
Durable monitoring and runtime validation
src/reglineage/monitoring/*, src/reglineage/runtime/store.py, tests/monitoring/test_monitoring.py, tests/capability/test_capability_runtime.py
Monitoring and runtime stores validate persisted event metadata. Delivery becomes durable and idempotent. Observation binding checks event, snapshot, policy, and delivery state.
Durable event-bound repair and publication
src/reglineage/composition.py, src/reglineage/mcp_backend.py, src/reglineage/replay/*, src/reglineage/north_star_console.py, tests/replay/test_repair.py, tests/integration/test_repair_event_contract.py
Repair requires an explicit event ID. Proposals, approvals, candidates, leases, and publication receipts are reconstructed and validated from durable bindings.
Taxi watcher and injection commands
src/reglineage/cli.py, tests/integration/test_taxi_cli.py, tests/mcp_runtime/test_protocol.py
The CLI adds structured watch output, bounded polling, attributed injection, delivery filtering, and event-aware repair dispatch.
Rehearsal and workflow evidence
src/reglineage/rehearsal.py, scripts/benchmark_north_star.py, tests/integration/test_mcp_security_rework.py, tests/integration/test_north_star_*.py
Rehearsal and integration flows record the observed event and reuse it across repair stages, retries, and restarts.
Updated lineage example records
examples/reglineage/*.json
Example publication and execution records contain new document identifiers and integrity hashes.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • Alex-lop/RegLineage#2: Introduced the workflow paths that this PR extends across CLI, composition, monitoring, replay, runtime, and integration code.

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant TaxiCLI
  participant MonitoringStore
  participant TaxiSlice
  participant RepairRegistry
  participant PublicationStore
  Operator->>TaxiCLI: start watch or inject mutation
  TaxiCLI->>MonitoringStore: poll or persist delivered event
  TaxiCLI->>TaxiSlice: pass event_id to repair
  TaxiSlice->>RepairRegistry: validate binding and repair evidence
  TaxiSlice->>PublicationStore: verify publication boundary
  PublicationStore-->>TaxiSlice: return verified receipt
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: exposing durable, event-bound taxi repair functionality.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/south-start-rework

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

❤️ Share

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

@Alex-lop
Alex-lop marked this pull request as ready for review August 10, 2026 17:35

Copy link
Copy Markdown
Owner Author

@coderabbitai resume

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (16)
src/reglineage/cli.py (1)

523-532: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the fixed mutation ID into a module-level constant.

The literal "taxi-pulocation-borough-only" appears here and in _watch_taxi at Line 234. The watcher binds the observation to the same mutation that this command creates. A single constant prevents the two sites from drifting.

♻️ Proposed refactor

Add the constant near the top of the module:

+_TAXI_MUTATION_ID = "taxi-pulocation-borough-only"
+
+
 def _print(value: object) -> None:

Then use it at both sites:

             elif args.command == "taxi-inject":
-                mutation_id = "taxi-pulocation-borough-only"
+                mutation_id = _TAXI_MUTATION_ID
                 service.mutator.bind_observation(  # type: ignore[attr-defined]
-                    "taxi-pulocation-borough-only", event.event_id
+                    _TAXI_MUTATION_ID, event.event_id
                 )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/reglineage/cli.py` around lines 523 - 532, Define a module-level constant
for the fixed “taxi-pulocation-borough-only” mutation ID, then replace the
duplicated literal in the taxi-inject command and _watch_taxi with that constant
so both paths remain synchronized.
tests/integration/test_north_star_benchmark.py (1)

172-180: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Select the evidence step by its step number, not by list index.

item["evidence"]["steps"][8] depends on step 9 staying at index 8. scripts/benchmark_north_star.py Line 827 already maps steps by number for the same evidence structure. Use the same lookup here so an inserted step fails with a clear message instead of asserting against the wrong step.

♻️ Proposed refactor
             for item in second["repair_repetitions"]:
-                observed = item["evidence"]["steps"][8]
+                steps = {int(step["step"]): step for step in item["evidence"]["steps"]}
+                observed = steps[9]
                 self.assertEqual(observed["observation_truth_label"], "OBSERVED")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/test_north_star_benchmark.py` around lines 172 - 180,
Update the evidence lookup in the repair_repetitions assertions to select the
entry whose step number is 9, matching the step-number mapping used by benchmark
evidence handling, instead of indexing steps at position 8. Keep the existing
assertions unchanged after selecting the correct step.
src/reglineage/mcp_backend.py (2)

653-662: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the reused binding local.

Line 653 binds binding to the repair_event_binding record. Line 673 rebinds the same name to a repair_publication_binding record, and line 677 can set it to None. The event-binding checks finish before the rebinding, so behavior is correct today. The shared name still makes any future read after line 673 resolve to the wrong record.

Use event_binding at line 653 and publication_binding at line 673.

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

In `@src/reglineage/mcp_backend.py` around lines 653 - 662, Rename the local
loaded with “repair_event_binding” from binding to event_binding and update its
proposal_hash and binding_hash checks accordingly; rename the later
“repair_publication_binding” local to publication_binding, including its None
assignment and subsequent references.

532-539: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Chain the exception cause.

Line 537 raises inside an except KeyError block without from. Every other handler in this file uses from exc, including _load at line 159 and _event at line 171. The missing chain hides the original lookup failure in tracebacks.

🐛 Proposed fix to restore the cause chain
         try:
             item = self.service.records.load(
                 "repair_proposal", f"borough-repair:{event.event_id}:v1"
             )
-        except KeyError:
-            raise ToolDenied("REPAIR_NOT_PROPOSED")
+        except KeyError as exc:
+            raise ToolDenied("REPAIR_NOT_PROPOSED") from exc
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/reglineage/mcp_backend.py` around lines 532 - 539, Update the KeyError
handler in the repair-proposal loading flow to catch the exception as a named
variable and raise ToolDenied("REPAIR_NOT_PROPOSED") from that original
exception, matching the chaining pattern used by _load and _event.

Source: Linters/SAST tools

src/reglineage/composition.py (5)

628-655: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split the 26-clause predicate into named groups with distinct messages.

Every failure in this condition raises the same message: "Event does not authorize repair of this exact lease". Operators cannot tell whether the observation identity, the policy epoch, the delivery state, or the lease scope caused the rejection. The block also mixes observation checks, event checks, and lease checks in one expression.

Group the clauses into small helpers such as _observation_binds_event, _event_is_fixed_borough, and _lease_admits_repair, and raise a specific message per group. Behavior stays identical and the code stays fail-closed.

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

In `@src/reglineage/composition.py` around lines 628 - 655, Refactor the
authorization predicate around the repair validation into named helpers such as
_observation_binds_event, _event_is_fixed_borough, and _lease_admits_repair,
separating observation, event, delivery, and lease checks while preserving every
existing condition and fail-closed behavior. Update the validation flow to raise
distinct, actionable PermissionError messages for each failed group instead of
one generic message.

1273-1275: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two files hardcode the repair record id version. propose_borough_repair builds proposal_id as f"borough-repair:{event.event_id}:{REPAIR_RECIPE_VERSION}", but both lookup sites spell the version as the literal v1. If REPAIR_RECIPE_VERSION advances, both lookups raise KeyError against durable records that exist under the new id.

  • src/reglineage/composition.py#L1273-L1275: build the repair_event_binding id with REPAIR_RECIPE_VERSION instead of the v1 literal.
  • src/reglineage/mcp_backend.py#L533-L535: build the repair_proposal id with the same imported REPAIR_RECIPE_VERSION instead of the v1 literal.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/reglineage/composition.py` around lines 1273 - 1275, The repair record
lookups must use the shared REPAIR_RECIPE_VERSION instead of hardcoded v1.
Update the repair_event_binding lookup in
src/reglineage/composition.py:1273-1275 and the repair_proposal lookup in
src/reglineage/mcp_backend.py:533-535, ensuring both construct IDs consistently
with propose_borough_repair and import the shared version symbol where needed.

862-871: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Read leases through the CapabilityRuntime API instead of its private table.

_assert_overall_active opens a second sqlite3 connection to self.runtime.db_path and selects from capability_leases. This duplicates the runtime's schema knowledge inside composition.py, bypasses the runtime's own lock, and is the only reason composition.py imports sqlite3 at line 9. A schema change inside reglineage.runtime.store would break this call site with no type or import signal.

Add a narrow accessor to CapabilityRuntime, for example leases_by_tool(tool_id), and call it here.

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

In `@src/reglineage/composition.py` around lines 862 - 871, The
_assert_overall_active method should stop querying the private capability_leases
table through a separate sqlite3 connection. Add a narrow public
leases_by_tool(tool_id) accessor to CapabilityRuntime that performs the
runtime-owned lookup, then update _assert_overall_active to use it for
OVERALL_TOOL_ID and retain the existing ACTIVE-state validation; remove the
now-unused sqlite3 import from composition.py.

858-860: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Name the one-hour approval window.

The one-hour freshness window is a literal here, and timedelta(hours=1) also appears at line 1023 and line 1115 where approval expiry is set. If one value changes, an approval can be issued with an expiry that the freshness check rejects, or the reverse.

Define a module constant such as _APPROVAL_VALIDITY = timedelta(hours=1) and use it at all three sites.

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

In `@src/reglineage/composition.py` around lines 858 - 860, Define a module-level
_APPROVAL_VALIDITY constant set to the one-hour timedelta, then update
_unused_approval_is_current and both approval-expiry assignments to reuse it
instead of constructing separate timedelta(hours=1) values.

57-68: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Promote the borrowed replay.repair helpers to a public surface.

Lines 63-67 import _candidate_content_hash, _governed_content_hash, and _validation_from_json. The leading underscore marks them private to reglineage.replay.repair, but composition.py now depends on all three in _verify_repair_candidate and _resume_activated_repair. A rename or signature change inside replay.repair breaks composition.py with no compile-time signal for the signature case.

Export these three under public names from reglineage.replay.repair, or add a small public verification function in that module and call it from here. This keeps the candidate-verification logic on the side that owns the schema.

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

In `@src/reglineage/composition.py` around lines 57 - 68, Promote the private
replay.repair helpers used by _verify_repair_candidate and
_resume_activated_repair to a stable public API. Export public equivalents for
_candidate_content_hash, _governed_content_hash, and _validation_from_json, or
add one public verification function in reglineage.replay.repair and update
composition.py to call it, keeping schema-specific candidate verification owned
by that module.
src/reglineage/monitoring/service.py (1)

423-463: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Derive the observation stream from the mutation instead of the module constant.

await_observation polls mutation.previous_snapshot.target; if inject_borough_only accepts a non-default target, bind_observation still reads PULOCATION_TARGET.stream_id and PULOCATION_TARGET.identity. Use mutation.previous_snapshot.target for the observation lookup, expected stream_id, current snapshot lookup, and target-identity comparison so mutation observations bind the actual injected target.

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

In `@src/reglineage/monitoring/service.py` around lines 423 - 463, Update
bind_observation to derive the target from mutation.previous_snapshot.target
instead of PULOCATION_TARGET. Use that target for observation lookup,
expected_event_id stream_id, current snapshot retrieval, and target-identity
validation, while preserving the existing mutation binding checks.
tests/integration/test_repair_event_contract.py (6)

675-680: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Silence the S608 finding with a scoped suppression.

Ruff reports S608 on this statement. The interpolated value is the module constant PERSISTED_BOROUGH_RELATION, not caller input, so there is no injection path. A relation name cannot be a bound parameter, so the interpolation must stay. Add a targeted suppression so the lint gate stays clean.

🔧 Proposed fix
         with duckdb.connect(str(self.service.replay_db)) as connection:
-            connection.execute(
+            connection.execute(  # noqa: S608 - relation name is a module constant
                 f"UPDATE {PERSISTED_BOROUGH_RELATION} SET borough = 'Tampered' "
                 "WHERE source_record_id = (SELECT MIN(source_record_id) "
                 f"FROM {PERSISTED_BOROUGH_RELATION})"
             )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/test_repair_event_contract.py` around lines 675 - 680, Add
a narrowly scoped Ruff S608 suppression to the SQL UPDATE statement in the test
using PERSISTED_BOROUGH_RELATION, preserving the constant interpolation and
query behavior while leaving unrelated SQL checks enabled.

Source: Linters/SAST tools


741-746: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the positional index with a named binding.

completed[3] does not state what the fourth element is. A reader cannot tell which lease the assertion checks. Unpack the result, or index by a named constant.

♻️ Proposed refactor
             completed = self.service.repair(
                 self.zone,
                 event_id=event_id,
                 now=publication_approval.approved_at + timedelta(hours=2),
             )
-        self.assertEqual(completed[3].replacement_for, self.zone.lease_id)
+        *_, replacement_lease = completed
+        self.assertEqual(replacement_lease.replacement_for, self.zone.lease_id)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/test_repair_event_contract.py` around lines 741 - 746,
Replace the positional completed[3] access in the repair test with a named
binding by unpacking the repair result or using an existing named index
constant, then assert replacement_for against self.zone.lease_id through that
named reference.

113-118: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Do not re-derive the observation identifier in the test.

The helper reconstructs observation_id as "poll:" + canonical_sha256(observation_values)[:24]. This copies the production derivation rule, including the prefix and the 24-character truncation. If the production derivation changes, this helper produces a value that no longer matches the real format, and the tampering tests can pass for the wrong reason.

Reuse the production helper that builds the observation identifier, and assert the derived value against it.

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

In `@tests/integration/test_repair_event_contract.py` around lines 113 - 118,
Update the changed_observation setup around monitoring_hashed to reuse the
production helper that derives the observation identifier instead of manually
combining "poll:" with canonical_sha256 and truncation. Assert or pass the
resulting identifier from that helper while preserving the tampering test’s
intended behavior.

72-89: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused deliver parameter, or use it in the undelivered test.

No caller passes deliver=False. test_injected_unobserved_and_observed_undelivered_fail_before_proposal re-implements the undelivered flow inline at lines 195-207. This duplicates the inject-and-poll sequence and lets the two paths drift.

Call self._inject(deliver=False) in that test, or drop the parameter from _inject.

Also applies to: 194-213

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

In `@tests/integration/test_repair_event_contract.py` around lines 72 - 89, Update
the undelivered test
test_injected_unobserved_and_observed_undelivered_fail_before_proposal to call
_inject(deliver=False) instead of duplicating the inject-and-poll flow inline.
Preserve the existing assertions and remove the now-redundant setup code; keep
_inject’s deliver branching intact.

29-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Register cleanups instead of relying on tearDown.

setUp creates TemporaryDirectory and TaxiSlice before it finishes. If fixture_cache, TaxiSlice, replay.start, or _lease raises, tearDown does not run and the temporary directory leaks. Use addCleanup so each resource is released as soon as it is created.

Note that self.service is rebound at line 554, so the service cleanup must resolve the current attribute at call time.

♻️ Proposed refactor
     def setUp(self) -> None:
         self.temporary = TemporaryDirectory()
+        self.addCleanup(self.temporary.cleanup)
         root = Path(self.temporary.name)
         _, info = fixture_cache(root)
         self.run_root = root / "run"
         self.stage_path = str(info["stage_path"])
         self.now = datetime.now(timezone.utc) - timedelta(minutes=1)
         self.service = TaxiSlice(
             self.run_root, stage_path=self.stage_path, clock_started_at=NOW
         )
+        self.addCleanup(lambda: self.service.close())
         self.service.replay.start(batch_size=500_000)
         self.service.poller.poll()
         self.zone = self._lease(ZONE_TOOL_ID, ZONE_VERSION, "zone")
         self.overall = self._lease(OVERALL_TOOL_ID, OVERALL_VERSION, "overall")
 
-    def tearDown(self) -> None:
-        self.service.close()
-        self.temporary.cleanup()
-
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/test_repair_event_contract.py` around lines 29 - 46, Update
the test setup in setUp to register cleanup callbacks immediately after creating
TemporaryDirectory and TaxiSlice, so cleanup still runs when later setup steps
fail. Use addCleanup for the temporary directory and a callback that resolves
self.service at cleanup time, preserving the existing tearDown behavior while
handling the later self.service rebinding.

652-657: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use one consistent type for patch_hash.

Line 654 passes manifest["patch_hash"] directly. Line 689 wraps the same value with str(...). Pick one form. If the record value is already a string, remove the str(...) call. If it is not, add the conversion in both places so both tests exercise the same input type.

Also applies to: 687-691

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

In `@tests/integration/test_repair_event_contract.py` around lines 652 - 657, Make
the `patch_hash` argument type consistent in both `publication_receipts`
assertions: update the call at the shown `publication_manifest` lookup and the
corresponding call around the second assertion to either both pass the value
directly or both convert it with `str(...)`, based on the record’s actual type.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/reglineage/cli.py`:
- Around line 227-259: Update _watch_taxi to catch KeyError and
MonitoringConflict from service.mutator.bind_observation for
"taxi-pulocation-borough-only". On failure, emit the function’s structured
terminal JSON record and return the distinct failure exit code used by its other
error paths, ensuring no subsequent unguarded processing can crash the watcher;
import MonitoringConflict alongside the existing monitoring symbols.

In `@src/reglineage/composition.py`:
- Around line 702-722: Extract the shared repair-binding lookup and seven-field
validation into an _existing_repair_binding(zone_lease, event, observation)
helper returning the validated binding or None, while preserving both existing
PermissionError messages. In src/reglineage/composition.py lines 702-722,
replace the inline logic with this helper and retain only proposal construction
and persistence; do the same at lines 775-796, retaining only proposal
re-derivation and durable-record comparison.

In `@src/reglineage/rehearsal.py`:
- Around line 354-373: Update the repair call in the rehearsal flow after event
lookup to compute a timestamp later than the rehearsal timestamp, observed event
timestamp, and polled observation timestamp, then pass that clamped value as now
to service.repair. Preserve the existing zone_lease, event_id, and on_suspended
arguments.

In `@src/reglineage/runtime/store.py`:
- Around line 601-606: Update the event lookup flow around
`_validated_event_row` to explicitly check whether `event_row` is missing and
raise `RuntimeContractError` before invoking the helper. Match the existing
missing-event handling used by `accepted_event` and `_status`, while preserving
validation for present rows.

In `@tests/integration/test_repair_event_contract.py`:
- Around line 35-38: Update the test setup around self.now and the later
self.now advances to derive all timestamps from the fixed NOW constant instead
of datetime.now(timezone.utc). Preserve the existing one-minute offset and
subsequent timedelta progression so chronology assertions remain deterministic.
- Around line 450-460: Update the SQLite connection block around the
change_events UPDATE to explicitly close the connection, using
contextlib.closing (and adding its import) or an equivalent explicit close.
Preserve the existing transaction commit behavior and query parameters while
ensuring the handle is released before teardown cleanup.

In `@tests/integration/test_taxi_cli.py`:
- Around line 295-397: Update
test_separate_processes_ready_inject_and_persist_delivered_history to guarantee
cleanup of both watcher and injector processes on every exit, using a test-class
_reap helper or equivalent registered cleanup that kills running children and
waits for them. Replace the unbounded watcher.stdout.readline readiness wait
with a bounded approach, and ensure timeout paths around injector.wait and
watcher.communicate invoke cleanup before propagating the failure.

---

Nitpick comments:
In `@src/reglineage/cli.py`:
- Around line 523-532: Define a module-level constant for the fixed
“taxi-pulocation-borough-only” mutation ID, then replace the duplicated literal
in the taxi-inject command and _watch_taxi with that constant so both paths
remain synchronized.

In `@src/reglineage/composition.py`:
- Around line 628-655: Refactor the authorization predicate around the repair
validation into named helpers such as _observation_binds_event,
_event_is_fixed_borough, and _lease_admits_repair, separating observation,
event, delivery, and lease checks while preserving every existing condition and
fail-closed behavior. Update the validation flow to raise distinct, actionable
PermissionError messages for each failed group instead of one generic message.
- Around line 1273-1275: The repair record lookups must use the shared
REPAIR_RECIPE_VERSION instead of hardcoded v1. Update the repair_event_binding
lookup in src/reglineage/composition.py:1273-1275 and the repair_proposal lookup
in src/reglineage/mcp_backend.py:533-535, ensuring both construct IDs
consistently with propose_borough_repair and import the shared version symbol
where needed.
- Around line 862-871: The _assert_overall_active method should stop querying
the private capability_leases table through a separate sqlite3 connection. Add a
narrow public leases_by_tool(tool_id) accessor to CapabilityRuntime that
performs the runtime-owned lookup, then update _assert_overall_active to use it
for OVERALL_TOOL_ID and retain the existing ACTIVE-state validation; remove the
now-unused sqlite3 import from composition.py.
- Around line 858-860: Define a module-level _APPROVAL_VALIDITY constant set to
the one-hour timedelta, then update _unused_approval_is_current and both
approval-expiry assignments to reuse it instead of constructing separate
timedelta(hours=1) values.
- Around line 57-68: Promote the private replay.repair helpers used by
_verify_repair_candidate and _resume_activated_repair to a stable public API.
Export public equivalents for _candidate_content_hash, _governed_content_hash,
and _validation_from_json, or add one public verification function in
reglineage.replay.repair and update composition.py to call it, keeping
schema-specific candidate verification owned by that module.

In `@src/reglineage/mcp_backend.py`:
- Around line 653-662: Rename the local loaded with “repair_event_binding” from
binding to event_binding and update its proposal_hash and binding_hash checks
accordingly; rename the later “repair_publication_binding” local to
publication_binding, including its None assignment and subsequent references.
- Around line 532-539: Update the KeyError handler in the repair-proposal
loading flow to catch the exception as a named variable and raise
ToolDenied("REPAIR_NOT_PROPOSED") from that original exception, matching the
chaining pattern used by _load and _event.

In `@src/reglineage/monitoring/service.py`:
- Around line 423-463: Update bind_observation to derive the target from
mutation.previous_snapshot.target instead of PULOCATION_TARGET. Use that target
for observation lookup, expected_event_id stream_id, current snapshot retrieval,
and target-identity validation, while preserving the existing mutation binding
checks.

In `@tests/integration/test_north_star_benchmark.py`:
- Around line 172-180: Update the evidence lookup in the repair_repetitions
assertions to select the entry whose step number is 9, matching the step-number
mapping used by benchmark evidence handling, instead of indexing steps at
position 8. Keep the existing assertions unchanged after selecting the correct
step.

In `@tests/integration/test_repair_event_contract.py`:
- Around line 675-680: Add a narrowly scoped Ruff S608 suppression to the SQL
UPDATE statement in the test using PERSISTED_BOROUGH_RELATION, preserving the
constant interpolation and query behavior while leaving unrelated SQL checks
enabled.
- Around line 741-746: Replace the positional completed[3] access in the repair
test with a named binding by unpacking the repair result or using an existing
named index constant, then assert replacement_for against self.zone.lease_id
through that named reference.
- Around line 113-118: Update the changed_observation setup around
monitoring_hashed to reuse the production helper that derives the observation
identifier instead of manually combining "poll:" with canonical_sha256 and
truncation. Assert or pass the resulting identifier from that helper while
preserving the tampering test’s intended behavior.
- Around line 72-89: Update the undelivered test
test_injected_unobserved_and_observed_undelivered_fail_before_proposal to call
_inject(deliver=False) instead of duplicating the inject-and-poll flow inline.
Preserve the existing assertions and remove the now-redundant setup code; keep
_inject’s deliver branching intact.
- Around line 29-46: Update the test setup in setUp to register cleanup
callbacks immediately after creating TemporaryDirectory and TaxiSlice, so
cleanup still runs when later setup steps fail. Use addCleanup for the temporary
directory and a callback that resolves self.service at cleanup time, preserving
the existing tearDown behavior while handling the later self.service rebinding.
- Around line 652-657: Make the `patch_hash` argument type consistent in both
`publication_receipts` assertions: update the call at the shown
`publication_manifest` lookup and the corresponding call around the second
assertion to either both pass the value directly or both convert it with
`str(...)`, based on the record’s actual type.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8768cb4c-686e-47e3-9e81-11995a2773d2

📥 Commits

Reviewing files that changed from the base of the PR and between 4fdb41c and 8cfff99.

📒 Files selected for processing (24)
  • examples/reglineage/publication-summary.json
  • examples/reglineage/runtime-attestation.json
  • examples/reglineage/verified-execution-receipt.json
  • scripts/benchmark_north_star.py
  • src/reglineage/cli.py
  • src/reglineage/composition.py
  • src/reglineage/mcp_backend.py
  • src/reglineage/monitoring/service.py
  • src/reglineage/monitoring/store.py
  • src/reglineage/north_star_console.py
  • src/reglineage/rehearsal.py
  • src/reglineage/replay/models.py
  • src/reglineage/replay/repair.py
  • src/reglineage/runtime/store.py
  • tests/capability/test_capability_runtime.py
  • tests/integration/test_mcp_security_rework.py
  • tests/integration/test_north_star_benchmark.py
  • tests/integration/test_north_star_console.py
  • tests/integration/test_north_star_vertical_slice.py
  • tests/integration/test_repair_event_contract.py
  • tests/integration/test_taxi_cli.py
  • tests/mcp_runtime/test_protocol.py
  • tests/monitoring/test_monitoring.py
  • tests/replay/test_repair.py

Comment thread src/reglineage/cli.py
Comment on lines +227 to +259
success = False
if (
event is not None
and observation is not None
and service.monitoring.event_delivered(event.event_id) # type: ignore[attr-defined]
):
service.mutator.bind_observation( # type: ignore[attr-defined]
"taxi-pulocation-borough-only", event.event_id
)
success = service.monitoring.event_delivered(event.event_id) # type: ignore[attr-defined]
record.update(
observation_id=observation.observation_id,
observation_hash=observation.observation_hash,
disposition=observation.disposition.value,
current_snapshot_hash=observation.current_snapshot_hash,
changed_aspects=observation.changed_aspects,
event_id=event.event_id,
event_source=event.source,
event_effect=event.effect.value,
event_truth_label=event.truth_label.value,
affected_tools=event.affected_tools,
)
if event is not None:
record["delivered"] = service.monitoring.event_delivered( # type: ignore[attr-defined]
event.event_id
)
record["terminal_state"] = (
"OBSERVED_DELIVERED_BOUND"
if success
else "BOUNDED_TIMEOUT"
if poll_number == max_polls
else None
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Handle bind_observation failures instead of letting the watcher crash.

service.mutator.bind_observation raises KeyError when no durable mutation record exists for "taxi-pulocation-borough-only", and raises MonitoringConflict when the observation does not bind the injected change. Both conditions are reachable here. A delivered fixed-borough event can exist without a matching mutation record, for example when an operator changes governance outside taxi-inject.

In that case _watch_taxi exits with an unhandled traceback. Every other failure path in this function emits one structured JSON record and returns a distinct exit code. Callers that parse the line-oriented record stream then receive no terminal record.

Guard the bind call and report the failure as a terminal record.

🛡️ Proposed fix to report bind failures as a terminal record
             success = False
             if (
                 event is not None
                 and observation is not None
                 and service.monitoring.event_delivered(event.event_id)  # type: ignore[attr-defined]
             ):
-                service.mutator.bind_observation(  # type: ignore[attr-defined]
-                    "taxi-pulocation-borough-only", event.event_id
-                )
-                success = service.monitoring.event_delivered(event.event_id)  # type: ignore[attr-defined]
+                try:
+                    service.mutator.bind_observation(  # type: ignore[attr-defined]
+                        "taxi-pulocation-borough-only", event.event_id
+                    )
+                except (KeyError, MonitoringConflict) as exc:
+                    record["terminal_state"] = "OBSERVATION_NOT_BOUND"
+                    record["reason"] = type(exc).__name__
+                    _print_record(record)
+                    return 4
+                success = service.monitoring.event_delivered(event.event_id)  # type: ignore[attr-defined]
                 record.update(

Import MonitoringConflict with the other monitoring symbols at the top of _watch_taxi:

-    from reglineage.monitoring import PULOCATION_TARGET, PollDisposition
+    from reglineage.monitoring import MonitoringConflict, PULOCATION_TARGET, PollDisposition
📝 Committable suggestion

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

Suggested change
success = False
if (
event is not None
and observation is not None
and service.monitoring.event_delivered(event.event_id) # type: ignore[attr-defined]
):
service.mutator.bind_observation( # type: ignore[attr-defined]
"taxi-pulocation-borough-only", event.event_id
)
success = service.monitoring.event_delivered(event.event_id) # type: ignore[attr-defined]
record.update(
observation_id=observation.observation_id,
observation_hash=observation.observation_hash,
disposition=observation.disposition.value,
current_snapshot_hash=observation.current_snapshot_hash,
changed_aspects=observation.changed_aspects,
event_id=event.event_id,
event_source=event.source,
event_effect=event.effect.value,
event_truth_label=event.truth_label.value,
affected_tools=event.affected_tools,
)
if event is not None:
record["delivered"] = service.monitoring.event_delivered( # type: ignore[attr-defined]
event.event_id
)
record["terminal_state"] = (
"OBSERVED_DELIVERED_BOUND"
if success
else "BOUNDED_TIMEOUT"
if poll_number == max_polls
else None
)
success = False
if (
event is not None
and observation is not None
and service.monitoring.event_delivered(event.event_id) # type: ignore[attr-defined]
):
try:
service.mutator.bind_observation( # type: ignore[attr-defined]
"taxi-pulocation-borough-only", event.event_id
)
except (KeyError, MonitoringConflict) as exc:
record["terminal_state"] = "OBSERVATION_NOT_BOUND"
record["reason"] = type(exc).__name__
_print_record(record)
return 4
success = service.monitoring.event_delivered(event.event_id) # type: ignore[attr-defined]
record.update(
observation_id=observation.observation_id,
observation_hash=observation.observation_hash,
disposition=observation.disposition.value,
current_snapshot_hash=observation.current_snapshot_hash,
changed_aspects=observation.changed_aspects,
event_id=event.event_id,
event_source=event.source,
event_effect=event.effect.value,
event_truth_label=event.truth_label.value,
affected_tools=event.affected_tools,
)
if event is not None:
record["delivered"] = service.monitoring.event_delivered( # type: ignore[attr-defined]
event.event_id
)
record["terminal_state"] = (
"OBSERVED_DELIVERED_BOUND"
if success
else "BOUNDED_TIMEOUT"
if poll_number == max_polls
else None
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/reglineage/cli.py` around lines 227 - 259, Update _watch_taxi to catch
KeyError and MonitoringConflict from service.mutator.bind_observation for
"taxi-pulocation-borough-only". On failure, emit the function’s structured
terminal JSON record and return the distinct failure exit code used by its other
error paths, ensuring no subsequent unguarded processing can crash the watcher;
import MonitoringConflict alongside the existing monitoring symbols.

Comment on lines +702 to +722
existing = tuple(
item
for item in self.records.list("repair_event_binding")
if item["event_id"] == event.event_id
or item["lease_id"] == zone_lease.lease_id
)
if existing:
if len(existing) != 1:
raise PermissionError("Repair binding is ambiguous")
binding = existing[0]
if (
binding["event_id"] != event.event_id
or binding["event_hash"] != event.event_hash
or binding["observation_id"] != observation.observation_id
or binding["observation_hash"] != observation.observation_hash
or binding["current_snapshot_hash"]
!= observation.current_snapshot_hash
or binding["lease_id"] != zone_lease.lease_id
or binding["lease_hash"] != zone_lease.lease_hash
):
raise PermissionError("Event or lease is already bound elsewhere")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Extract the duplicated repair-binding lookup and validation. _bound_repair_proposal and _preflight_repair_binding contain an identical records.list("repair_event_binding") filter and an identical seven-field mismatch predicate, including both error messages. repair calls both in the same invocation, so the scan runs twice and the authorization predicate exists in two copies. A future tightening applied to one copy silently leaves the other weaker.

  • src/reglineage/composition.py#L702-L722: replace the inline filter and mismatch check with a call to a new _existing_repair_binding(zone_lease, event, observation) helper that returns the validated binding or None, and keep only the proposal construction and persistence here.
  • src/reglineage/composition.py#L775-L796: replace the duplicated filter and mismatch check with the same _existing_repair_binding call, and keep only the proposal re-derivation and durable-record comparison here.
♻️ Proposed shared helper
    def _existing_repair_binding(
        self,
        zone_lease: CapabilityLease,
        event: ChangeEvent,
        observation: PollObservation,
    ) -> dict[str, object] | None:
        existing = tuple(
            item
            for item in self.records.list("repair_event_binding")
            if item["event_id"] == event.event_id
            or item["lease_id"] == zone_lease.lease_id
        )
        if not existing:
            return None
        if len(existing) != 1:
            raise PermissionError("Repair binding is ambiguous")
        binding = existing[0]
        if (
            binding["event_id"] != event.event_id
            or binding["event_hash"] != event.event_hash
            or binding["observation_id"] != observation.observation_id
            or binding["observation_hash"] != observation.observation_hash
            or binding["current_snapshot_hash"] != observation.current_snapshot_hash
            or binding["lease_id"] != zone_lease.lease_id
            or binding["lease_hash"] != zone_lease.lease_hash
        ):
            raise PermissionError("Event or lease is already bound elsewhere")
        return binding
📍 Affects 1 file
  • src/reglineage/composition.py#L702-L722 (this comment)
  • src/reglineage/composition.py#L775-L796
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/reglineage/composition.py` around lines 702 - 722, Extract the shared
repair-binding lookup and seven-field validation into an
_existing_repair_binding(zone_lease, event, observation) helper returning the
validated binding or None, while preserving both existing PermissionError
messages. In src/reglineage/composition.py lines 702-722, replace the inline
logic with this helper and retain only proposal construction and persistence; do
the same at lines 775-796, retaining only proposal re-derivation and
durable-record comparison.

Comment on lines +354 to +373
mutation = service.mutator.inject_borough_only(
"taxi-pulocation-borough-only",
actor="external-datahub-admin",
now=_now_after(overall_lease.issued_at),
)
steps.append({"step": 8, "status": "PROVEN", "mutation_hash": mutation.mutation_hash, "evidence_mode": "INJECTED_GOVERNANCE_EVENT"})
polled, delivered = service.poller.poll_and_assess(service.runtime)
if polled.event is None or not service.monitoring.event_delivered(polled.event.event_id):
raise RuntimeError("Separate watcher poll did not durably deliver the injected event")
event_id = service.mutator.bind_observation(
mutation.mutation_id, polled.event.event_id
)
observed_event = service.monitoring.event(event_id)
if observed_event.event_id != event_id or not delivered:
raise RuntimeError("Durable event lookup differs from watcher delivery")
transitions = service.runtime.transitions(zone_lease.lease_id)
steps.append({"step": 9, "status": "PROVEN", "event_id": event_id, "observation_id": polled.observation.observation_id, "observation_truth_label": polled.observation.truth_label.value, "event_truth_label": observed_event.truth_label.value, "event_source": observed_event.source, "event_effect": observed_event.effect.value, "event_delivered": service.monitoring.event_delivered(event_id), "states": tuple(item.to_state.value for item in transitions)})

try:
service.repair(zone_lease, now=_now_after(overall_lease.issued_at), on_suspended=run_fallback)
service.repair(zone_lease, event_id=event_id, now=_now_after(overall_lease.issued_at), on_suspended=run_fallback)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm rehearsal.py does not already clamp repair time past the observation, and compare with the clamps added in tests.
set -euo pipefail

fd -t f 'rehearsal.py' -x rg -n 'observed_at|_now_after|service\.repair' {}

echo '--- explicit clamps added elsewhere in this change set ---'
rg -n -B2 -A4 'observed_at\s*$|observed_at\b.*timedelta\(microseconds=1\)' --glob 'tests/**/*.py' --glob 'src/**/*.py'

Repository: Alex-lop/RegLineage

Length of output: 1840


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- rehearsal.py relevant outline ---'
ast-grep outline src/reglineage/rehearsal.py --match 'Rehearsal' --view expanded || true

echo '--- rehearsal.py clock and repair lines ---'
sed -n '1,90p' src/reglineage/rehearsal.py
sed -n '300,390p' src/reglineage/rehearsal.py

echo '--- locate TaxiSlice.repair definitions/usages ---'
rg -n 'def repair|observed_at|Repair action must follow the exact observed event|insert_event|insert_observation|bind_observation|event_delivered' --glob 'src/**/*.py' --glob 'tests/**/*.py'

Repository: Alex-lop/RegLineage

Length of output: 11429


🏁 Script executed:

Compositorically locate relevant source files and inspect only changed/relevant definitions

Repository: Alex-lop/RegLineage

Length of output: 211


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- repository files containing TaxiSlice and repair ---'
rg -n 'class TaxiSlice|def repair\(self|Repair action must follow the exact observed event|observed_at' src --glob '*.py'

echo '--- composition relevant section ---'
sed -n '1,220p' src/reglineage/composition.py

echo '--- runtime/mutator relevant sections if present ---'
sed -n '1,260p' src/reglineage/runtime.py
sed -n '1,260p' src/reglineage/mutator.py 2>/dev/null || true
sed -n '1,260p' src/reglineage/monitoring.py 2>/dev/null || true

Repository: Alex-lop/RegLineage

Length of output: 15203


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo '--- composition.py repair implementation ---'
sed -n '800,930p' src/reglineage/composition.py

echo '--- monitoring mutator/poller event injection and observed timestamp implementation ---'
rg -n 'inject_borough_only|bind_observation|poll_and_assess|def _clock|observed_at' src/reglineage/monitoring src/reglineage/datahub src/reglineage --glob '*.py'
sed -n '760,855p' src/reglineage/monitoring/service.py 2>/dev/null || true
sed -n '1,90p' src/reglineage/monitoring/store.py 2>/dev/null || true

Repository: Alex-lop/RegLineage

Length of output: 25742


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo '--- monitoring/service.py relevant implementation ---'
sed -n '260,430p' src/reglineage/monitoring/service.py

echo '--- datahub/replay configuration snapshot references ---'
rg -n 'clock_started_at|source_watermark|datetime.now|observed_at=|snapshot_time=' src/reglineage/replay src/reglineage/taxi_datahub.py src/reglineage/datahub src/reglineage/monitoring --glob '*.py'
sed -n '1,130p' src/reglineage/replay.py 2>/dev/null || true
sed -n '260,310p' src/reglineage/taxi_datahub.py

Repository: Alex-lop/RegLineage

Length of output: 15078


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo '--- replay.py probe clock_started_at and source_watermark semantics ---'
sed -n '20,180p' src/reglineage/replay.py

echo '--- source_gateway clock semantics ---'
sed -n '150,210p' src/reglineage/analytics/source_gateway.py
sed -n '330,355p' src/reglineage/analytics/source_gateway.py

echo '--- deterministic behavioral probe of timestamp ordering requirements ---'
python3 - <<'PY'
from datetime import datetime, timedelta, timezone

START = datetime(2026, 8, 9, 12, tzinfo=timezone.utc)
overall_lease_issued_at = START + timedelta(seconds=80)
# Simulate observed timestamps at replay clock (not guaranteed strictly before
# the user-derived repair clock because clock_started_at <= clock if speed has advanced).
observed_at = overall_lease_issued_at + timedelta(milliseconds=1)
now = _now_after = lambda latest: (
    datetime.now(timezone.utc)
    if datetime.now(timezone.utc) > latest
    else latest + timedelta(microseconds=1)
)
repair_now = (lambda _: max(_, _ + timedelta(microseconds=1)))(overall_lease_issued_at)
print({
    "replay_clock_before_repair_now": observed_at < repair_now,
    "current-rehearsal-now": repair_now.isoformat(),
    "sample-observed_at": observed_at.isoformat(),
    "repair-reason": "TaxiSlice.repair raises PermissionError when event.observed_at >= now or observation.observed_at >= now",
})
PY

Repository: Alex-lop/RegLineage

Length of output: 297


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo '--- locate replay and composition files ---'
git ls-files | grep -E '(^|/)replay\.py$|(^|/)rehearsal\.py$|(^|/)composition\.py$|(^|/)source_gateway\.py$|(^|/)replay/(replay|repair)\.py$' || true

echo '--- source files that exist ---'
for f in src/reglineage/replay.py src/reglineage/rehearsal.py src/reglineage/composition.py src/reglineage/replay/replay.py src/reglineage/monitoring/service.py src/reglineage/monitoring/store.py src/reglineage/analytics/source_gateway.py; do
  if [ -f "$f" ]; then
    echo "FOUND $f"
  else
    echo "MISSING $f"
  fi
done

echo '--- source_gateway implementation if present ---'
if [ -f src/reglineage/analytics/source_gateway.py ]; then
  sed -n '150,215p' src/reglineage/analytics/source_gateway.py
  sed -n '330,395p' src/reglineage/analytics/source_gateway.py
fi

echo '--- composition repair implementation if present ---'
if [ -f src/reglineage/composition.py ]; then
  sed -n '870,910p' src/reglineage/composition.py
fi

Repository: Alex-lop/RegLineage

Length of output: 6596


Clamp the repair timestamp past the observation timestamps.

TaxiSlice.repair raises PermissionError("Repair action must follow the exact observed event") when event.observed_at >= now or observation.observed_at >= now. src/reglineage/rehearsal.py:373 passes _now_after(overall_lease.issued_at) as now, while the governance event/observation timestamps come from DataHub/polling and can be equal to a later clock value. Compute now as the latest of the rehearsal timestamp and the observed event/observation timestamps before calling service.repair.

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

In `@src/reglineage/rehearsal.py` around lines 354 - 373, Update the repair call
in the rehearsal flow after event lookup to compute a timestamp later than the
rehearsal timestamp, observed event timestamp, and polled observation timestamp,
then pass that clamped value as now to service.repair. Preserve the existing
zone_lease, event_id, and on_suspended arguments.

Comment on lines 601 to +606
event_row = conn.execute(
"SELECT payload_json FROM change_events WHERE event_id = ?", (event_id,)
"SELECT event_id, event_hash, stream_id, policy_epoch, observed_at, "
"payload_json FROM change_events WHERE event_id = ?",
(event_id,),
).fetchone()
event = _event(event_row["payload_json"])
event = _validated_event_row(event_row)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle a missing change_events row explicitly.

Line 606 passes event_row straight into _validated_event_row. If the row is absent, the helper raises TypeError: 'NoneType' object is not subscriptable instead of RuntimeContractError. The relevant-assessment check above does not guarantee the event row still exists, because a deleted change_events row leaves the impact_assessments row in place. accepted_event at line 443 and _status at line 983 both guard this case.

🛡️ Proposed fix to preserve the fail-closed contract
             event_row = conn.execute(
                 "SELECT event_id, event_hash, stream_id, policy_epoch, observed_at, "
                 "payload_json FROM change_events WHERE event_id = ?",
                 (event_id,),
             ).fetchone()
+            if event_row is None:
+                raise RuntimeContractError(
+                    "Revalidation requires a durable accepted event"
+                )
             event = _validated_event_row(event_row)
📝 Committable suggestion

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

Suggested change
event_row = conn.execute(
"SELECT payload_json FROM change_events WHERE event_id = ?", (event_id,)
"SELECT event_id, event_hash, stream_id, policy_epoch, observed_at, "
"payload_json FROM change_events WHERE event_id = ?",
(event_id,),
).fetchone()
event = _event(event_row["payload_json"])
event = _validated_event_row(event_row)
event_row = conn.execute(
"SELECT event_id, event_hash, stream_id, policy_epoch, observed_at, "
"payload_json FROM change_events WHERE event_id = ?",
(event_id,),
).fetchone()
if event_row is None:
raise RuntimeContractError(
"Revalidation requires a durable accepted event"
)
event = _validated_event_row(event_row)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/reglineage/runtime/store.py` around lines 601 - 606, Update the event
lookup flow around `_validated_event_row` to explicitly check whether
`event_row` is missing and raise `RuntimeContractError` before invoking the
helper. Match the existing missing-event handling used by `accepted_event` and
`_status`, while preserving validation for present rows.

Comment on lines +35 to +38
self.now = datetime.now(timezone.utc) - timedelta(minutes=1)
self.service = TaxiSlice(
self.run_root, stage_path=self.stage_path, clock_started_at=NOW
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not mix wall-clock time with the fixed test clock.

self.now uses datetime.now(timezone.utc), but clock_started_at uses the fixed NOW constant at line 24. The test then advances self.now again from wall clock at lines 84-88 and 204-207. This couples assertions about chronology ordering to the machine clock and to how far NOW is from the present. When NOW moves into the past relative to real time, the relative ordering between service-clock timestamps and self.now can change, which makes the ordering tests non-deterministic over time.

Derive self.now from NOW so the whole test is deterministic.

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

In `@tests/integration/test_repair_event_contract.py` around lines 35 - 38, Update
the test setup around self.now and the later self.now advances to derive all
timestamps from the fixed NOW constant instead of datetime.now(timezone.utc).
Preserve the existing one-minute offset and subsequent timedelta progression so
chronology assertions remain deterministic.

Comment on lines +450 to +460
with sqlite3.connect(self.service.runtime.db_path) as connection:
connection.execute(
"UPDATE change_events SET event_hash = ?, observed_at = ?, "
"payload_json = ? WHERE event_id = ?",
(
authoritative_event.event_hash,
canonical_data(authoritative_time),
canonical_json(authoritative_event),
event_id,
),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Close the SQLite connection.

with sqlite3.connect(...) commits the transaction on exit, but it does not close the connection. The connection to the runtime database stays open until garbage collection. tearDown then calls self.service.close() and self.temporary.cleanup() while a handle on the database file is still open, which can fail the directory cleanup on Windows.

Wrap the connection with contextlib.closing, or close it explicitly.

🐛 Proposed fix
-        with sqlite3.connect(self.service.runtime.db_path) as connection:
-            connection.execute(
-                "UPDATE change_events SET event_hash = ?, observed_at = ?, "
-                "payload_json = ? WHERE event_id = ?",
-                (
-                    authoritative_event.event_hash,
-                    canonical_data(authoritative_time),
-                    canonical_json(authoritative_event),
-                    event_id,
-                ),
-            )
+        with closing(sqlite3.connect(self.service.runtime.db_path)) as connection:
+            with connection:
+                connection.execute(
+                    "UPDATE change_events SET event_hash = ?, observed_at = ?, "
+                    "payload_json = ? WHERE event_id = ?",
+                    (
+                        authoritative_event.event_hash,
+                        canonical_data(authoritative_time),
+                        canonical_json(authoritative_event),
+                        event_id,
+                    ),
+                )

Add the import:

-from contextlib import ExitStack
+from contextlib import ExitStack, closing
📝 Committable suggestion

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

Suggested change
with sqlite3.connect(self.service.runtime.db_path) as connection:
connection.execute(
"UPDATE change_events SET event_hash = ?, observed_at = ?, "
"payload_json = ? WHERE event_id = ?",
(
authoritative_event.event_hash,
canonical_data(authoritative_time),
canonical_json(authoritative_event),
event_id,
),
)
with closing(sqlite3.connect(self.service.runtime.db_path)) as connection:
with connection:
connection.execute(
"UPDATE change_events SET event_hash = ?, observed_at = ?, "
"payload_json = ? WHERE event_id = ?",
(
authoritative_event.event_hash,
canonical_data(authoritative_time),
canonical_json(authoritative_event),
event_id,
),
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/test_repair_event_contract.py` around lines 450 - 460,
Update the SQLite connection block around the change_events UPDATE to explicitly
close the connection, using contextlib.closing (and adding its import) or an
equivalent explicit close. Preserve the existing transaction commit behavior and
query parameters while ensuring the handle is released before teardown cleanup.

Comment on lines +295 to +397
def test_separate_processes_ready_inject_and_persist_delivered_history(self) -> None:
self.service.close()
self.service = None
environment = self._environment()
watcher = subprocess.Popen(
[
sys.executable,
"-m",
"reglineage",
"taxi-watcher",
"watch",
"--interval",
"0.05",
"--max-polls",
"40",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env=environment,
)
assert watcher.stdout is not None
ready_line = watcher.stdout.readline()
if not ready_line:
_, stderr = watcher.communicate(timeout=5)
self.fail(f"watcher exited before readiness: {stderr}")
ready = json.loads(ready_line)
self.assertEqual(ready["record_type"], "BASELINE_READY")

master, slave = pty.openpty()
injector = subprocess.Popen(
[
sys.executable,
"-m",
"reglineage",
"taxi-inject",
"--actor",
"submitted-operator-label",
],
stdin=slave,
stdout=slave,
stderr=slave,
env=environment,
close_fds=True,
)
os.close(slave)
os.write(master, f"INJECT GOVERNANCE {MUTATION_ID}\n".encode())
injector_output = bytearray()
deadline = time.monotonic() + 15
while injector.poll() is None and time.monotonic() < deadline:
readable, _, _ = select.select((master,), (), (), 0.1)
if readable:
try:
injector_output.extend(os.read(master, 4096))
except OSError:
break
injector.wait(timeout=5)
try:
while chunk := os.read(master, 4096):
injector_output.extend(chunk)
except OSError:
pass
finally:
os.close(master)
self.assertEqual(injector.returncode, 0, injector_output.decode(errors="replace"))
injector_text = injector_output.decode(errors="replace")
payload = json.loads(
injector_text[injector_text.index("{") : injector_text.rindex("}") + 1]
)
self.assertEqual(
set(payload),
{
"mutation_id",
"mutation_hash",
"target_identity",
"injected_snapshot_hash",
"actor_attribution",
"truth_label",
"injected_at",
"identity_authenticated",
},
)
self.assertEqual(payload["actor_attribution"], "submitted-operator-label")
self.assertFalse(payload["identity_authenticated"])
self.assertNotIn("previous_snapshot", injector_text)

remaining_stdout, stderr = watcher.communicate(timeout=15)
self.assertEqual(watcher.returncode, 0, stderr)
records = (ready,) + self._records(remaining_stdout)
terminal = records[-1]
self.assertEqual(terminal["terminal_state"], "OBSERVED_DELIVERED_BOUND")

reopened = TaxiSlice(self.run, stage_path=self.stage, clock_started_at=NOW)
try:
event_id = str(terminal["event_id"])
self.assertTrue(reopened.monitoring.event_delivered(event_id))
self.assertEqual(reopened.monitoring.mutation_event(MUTATION_ID), event_id)
self.assertIn(
(event_id, True),
tuple((item.event_id, delivered) for item, delivered in reopened.monitoring.events()),
)
finally:
reopened.close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Terminate both child processes when the test times out.

injector.wait(timeout=5) at Line 351 and watcher.communicate(timeout=15) at Line 381 raise subprocess.TimeoutExpired. Neither call kills the child on timeout. tearDown then runs self.temporary.cleanup() while the orphaned reglineage processes still hold the SQLite databases open. One timeout then produces cascading failures and leaves processes running in CI.

watcher.stdout.readline() at Line 317 also has no timeout. If the watcher hangs before it prints the readiness record, the test blocks without bound instead of failing.

Register cleanup for both processes so a timeout fails one test and releases the resources.

🛡️ Proposed fix to guarantee child-process cleanup
     def test_separate_processes_ready_inject_and_persist_delivered_history(self) -> None:
         self.service.close()
         self.service = None
         environment = self._environment()
         watcher = subprocess.Popen(
             [
                 sys.executable,
                 "-m",
                 "reglineage",
                 "taxi-watcher",
                 "watch",
                 "--interval",
                 "0.05",
                 "--max-polls",
                 "40",
             ],
             stdout=subprocess.PIPE,
             stderr=subprocess.PIPE,
             text=True,
             env=environment,
         )
+        self.addCleanup(self._reap, watcher)
         assert watcher.stdout is not None
             env=environment,
             close_fds=True,
         )
+        self.addCleanup(self._reap, injector)
         os.close(slave)

Add the helper to the test class:

    `@staticmethod`
    def _reap(process: subprocess.Popen) -> None:
        if process.poll() is None:
            process.kill()
        process.wait(timeout=5)
🧰 Tools
🪛 ast-grep (0.45.0)

[error] 298-314: Command coming from incoming request
Context: subprocess.Popen(
[
sys.executable,
"-m",
"reglineage",
"taxi-watcher",
"watch",
"--interval",
"0.05",
"--max-polls",
"40",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
env=environment,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)


[error] 324-338: Command coming from incoming request
Context: subprocess.Popen(
[
sys.executable,
"-m",
"reglineage",
"taxi-inject",
"--actor",
"submitted-operator-label",
],
stdin=slave,
stdout=slave,
stderr=slave,
env=environment,
close_fds=True,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🪛 Ruff (0.16.1)

[warning] 383-383: Consider (ready, *self._records(remaining_stdout)) instead of concatenation

Replace with (ready, *self._records(remaining_stdout))

(RUF005)

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

In `@tests/integration/test_taxi_cli.py` around lines 295 - 397, Update
test_separate_processes_ready_inject_and_persist_delivered_history to guarantee
cleanup of both watcher and injector processes on every exit, using a test-class
_reap helper or equivalent registered cleanup that kills running children and
waits for them. Replace the unbounded watcher.stdout.readline readiness wait
with a bounded approach, and ensure timeout paths around injector.wait and
watcher.communicate invoke cleanup before propagating the failure.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews resumed.

@Alex-lop
Alex-lop merged commit 9324bb1 into main Aug 10, 2026
5 checks passed
@Alex-lop
Alex-lop deleted the agent/south-start-rework branch August 10, 2026 18:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant