docs(v1.1.2): reviewer audit report — APPROVE verdict (iteration 2)
Independent audit of feat/v1.1.2-documents over two iterations. Iteration 1 returned for a single-line cargo-fmt fix that HANDBACK had falsely claimed was green. Iteration 2 (bf26a25+fedc63b) applied the fix, re-verified all three gates on the new HEAD, and recorded the discipline lesson in HANDBACK §1 for the v1.1.3 retro. Re-audit on iteration-2 HEAD: fmt + clippy + 320-test workspace all green. SQL builder is parameter-bound end-to-end (audited line-by-line in docs_repo.rs:319-420 with adversarial-input tests). Layout E extension for docs is mechanically clean. Query DSL operator set is correct precedent for v1.2's advanced-query expansion. Branch ready to merge as v1.1.2.
This commit is contained in:
201
REVIEW.md
201
REVIEW.md
@@ -1,151 +1,140 @@
|
|||||||
# v1.1.1 Audit & Review
|
# v1.1.2 Audit & Review
|
||||||
|
|
||||||
**Branch:** `feat/v1.1.1-storage-and-events`
|
**Branch:** `feat/v1.1.2-documents`
|
||||||
**Base:** `main` (v1.1.0)
|
**Base:** `main` (v1.1.1 head)
|
||||||
**Commits ahead:** 12
|
**Commits ahead:** 9 (7 substantive + 2 from iteration 2)
|
||||||
**Audited by:** reviewer (this report)
|
**Audited by:** reviewer (this report)
|
||||||
**Audited against:** `docs/v1.1.x-design-notes.md` §1–4 (Decided 2026-06-01) + the original v1.1.1 dispatch prompt
|
**Audited against:** the v1.1.2 dispatch prompt + the v1.1.1-shipped patterns the prompt mandated
|
||||||
|
**Iterations:** 2 (iteration 1 returned for a format fix; iteration 2 fixed it cleanly)
|
||||||
|
|
||||||
## Verdict
|
## Verdict
|
||||||
|
|
||||||
**APPROVE — ready to merge to `main` as v1.1.1.**
|
**APPROVE — ready to merge to `main` as v1.1.2.**
|
||||||
|
|
||||||
The implementation is faithful to every load-bearing decision in the design notes. Static checks are green, the workspace test suite passes (243 tests pass, 132 properly-ignored DB-backed cases, 0 failures), the schema matches Layout E exactly, and the documented deviations are all defensible. There is one ambient concern about a cross-crate dependency that should be reflected in `CLAUDE.md` after the merge, but it is not a merge blocker.
|
Substantive work was excellent on iteration 1; the only blocker was a single autoformatter diff at `docs_service.rs:456-457` that the iteration-1 HANDBACK incorrectly claimed was clean. Iteration 2 fixed the line (`bf26a25 chore: cargo fmt`), re-verified all three gates fresh on the new HEAD, replaced HANDBACK §8 with an honest attestation table, and explicitly recorded the discipline lesson in HANDBACK §1 for the v1.1.3 retro. Re-audit on the new HEAD is clean.
|
||||||
|
|
||||||
|
The 9-commit branch reads as a coherent release. Nothing else in the implementation needed changes between iterations.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Static checks reproduced
|
## 1. Static checks reproduced (iteration 2 HEAD: `fedc63b`)
|
||||||
|
|
||||||
```
|
```
|
||||||
cargo fmt --all -- --check ✅ clean
|
cargo fmt --all -- --check ✅ exit 0 (no diff)
|
||||||
cargo clippy --all-targets --all-features -- -D warnings ✅ no findings
|
cargo clippy --all-targets --all-features -- -D warnings ✅ exit 0 (no warnings)
|
||||||
cargo test --workspace ✅ 243 passed / 0 failed
|
cargo test --workspace ✅ 320 passed / 0 failed
|
||||||
(132 ignored — DB-backed integration tests,
|
+ 132 properly-ignored DB-backed
|
||||||
same convention as v1.1.0; documented in HANDBACK §4)
|
integration tests
|
||||||
```
|
```
|
||||||
|
|
||||||
Test distribution per crate matches HANDBACK §4:
|
Per-crate test breakdown:
|
||||||
- manager-core: 63
|
- manager-core: 125 (62 new for v1.1.2: 26 docs_filter + 10 docs_repo sql-shape + 23 docs_service + 3 triggers_api docs)
|
||||||
- orchestrator-core: 56
|
- orchestrator-core: 56 (unchanged from v1.1.1)
|
||||||
- stdlib: 43
|
- stdlib: 43 (unchanged)
|
||||||
- sdk_contract: 30
|
- sdk_contract: 30 (unchanged)
|
||||||
- picloud: 21
|
- picloud: 21 (unchanged)
|
||||||
- executor-core (engine): 17
|
- executor-core engine: 17 (unchanged)
|
||||||
- sdk_kv: 7
|
- sdk_kv: 7 (unchanged)
|
||||||
- shared: 6
|
- sdk_docs: 15 (new in v1.1.2)
|
||||||
|
- shared: 6 (unchanged)
|
||||||
|
|
||||||
47 of these are new in v1.1.1; the rest are v1.1.0's existing suite still passing.
|
77 new tests — comfortably above the prompt's "30-50 new tests" target.
|
||||||
|
|
||||||
## 2. Design-notes conformance (spot-checks)
|
## 2. Design conformance (spot-checks)
|
||||||
|
|
||||||
| Decision | Where it lives | Verdict |
|
All items below were verified on iteration 1 and remain unchanged on iteration 2's HEAD (the format fix touched only whitespace).
|
||||||
|
|
||||||
|
| Decision / requirement | Where it lives | Verdict |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Layout E trigger storage (parent + per-kind detail) | [0008_triggers.sql:22-72](crates/manager-core/migrations/0008_triggers.sql#L22-L72) | ✅ matches exactly; parent has common columns + the four retry/dispatch knobs + `registered_by_principal`; per-kind detail tables for `kv` and `dead_letter` only |
|
| `docs::collection(name)` handle pattern + `::` namespace | [crates/executor-core/src/sdk/docs.rs](crates/executor-core/src/sdk/docs.rs) | ✅ Mirrors KV's shape exactly |
|
||||||
| `routes` stays separate from `triggers` parent | [0012_routes_dispatch_mode.sql](crates/manager-core/migrations/0012_routes_dispatch_mode.sql), [0009_outbox.sql:13-18](crates/manager-core/migrations/0009_outbox.sql#L13-L18) | ✅ HTTP rows use `source_kind = 'http'` and `trigger_id` references `routes.id`; non-HTTP references `triggers.id`; polymorphism in Rust per the design-notes deferral of the column-set refinement |
|
| Identity tuple `(app_id, collection, id)`, server-generated UUID | [0013_docs.sql:18-26](crates/manager-core/migrations/0013_docs.sql#L18-L26) | ✅ Primary key + server-generated id |
|
||||||
| Sync HTTP via outbox + NATS-style inbox | [inbox.rs:30-89](crates/orchestrator-core/src/inbox.rs#L30-L89), [dispatcher.rs:359-394](crates/manager-core/src/dispatcher.rs#L359-L394) | ✅ `oneshot::Sender<InboxResult>` keyed by inbox_id; `deliver()` returns `Delivered` or `Abandoned` exactly per the design-notes failure-mode table |
|
| Error convention (throw on failure, `()` for absent, `bool` for predicates) | [docs_service.rs](crates/manager-core/src/docs_service.rs), [sdk/docs.rs](crates/executor-core/src/sdk/docs.rs) | ✅ |
|
||||||
| `reply_to.is_some()` never retries | [dispatcher.rs:376-394](crates/manager-core/src/dispatcher.rs#L376-L394) | ✅ failure path checks `reply_to` first; delivers single outcome to inbox; deletes outbox row regardless of error |
|
| `app_id` from `cx.app_id`, never from script args | Service layer + SQL builder | ✅ Cross-app isolation test covers service; `every_query_starts_with_app_id_and_collection_predicate` pins it at the builder |
|
||||||
| Status code table (422/502/503/504/507/500) | [dispatcher.rs:555-564](crates/manager-core/src/dispatcher.rs#L555-L564), test [`failure_kind_status_codes_match_design_notes`](crates/manager-core/src/dispatcher.rs#L674) | ✅ exact mapping; covered by a dedicated test |
|
| Query DSL: 7 operators only (`$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`) | [docs_filter.rs ComparisonOp](crates/manager-core/src/docs_filter.rs) | ✅ Enum has exactly 7 variants |
|
||||||
| `dispatch_mode = async` returns `202 Accepted` + JSON body | [api.rs:325-332](crates/orchestrator-core/src/api.rs#L325-L332) | ✅ body shape is `{"accepted_at": rfc3339, "execution_id": uuid}` — matches design notes §2 verbatim |
|
| Unsupported operators throw with v1.2 pointer | docs_filter parser + 3 snapshot tests | ✅ Snapshot tests pin the error wording |
|
||||||
| Default retry: 3/exp/1000ms/±20% jitter | [trigger_config.rs](crates/manager-core/src/trigger_config.rs), tests [`exponential_backoff_doubles_per_attempt`](crates/manager-core/src/dispatcher.rs#L621), [`jitter_within_pct_of_base`](crates/manager-core/src/dispatcher.rs#L647) | ✅ env-overridable; jitter test exercises the ±20% bound across 100 samples |
|
| Dot-path field paths to depth 5 | [docs_filter.rs FieldPath::parse](crates/manager-core/src/docs_filter.rs) | ✅ Depth-limit + segment-validation tests |
|
||||||
| `abandoned_executions` written on dropped receiver | [dispatcher.rs:480-509](crates/manager-core/src/dispatcher.rs#L480-L509) | ✅ written only when `InboxDeliveryOutcome::Abandoned` returns; ordinary timeout-with-receiver-still-alive does not write a row |
|
| `$sort` single-field, `$limit` clamped | docs_filter parser | ✅ Multi-field-sort snapshot test; limit-clamp + negative-rejection tests |
|
||||||
| Dead-letter recursion stop (flag on execution) | [dispatcher.rs:396-425](crates/manager-core/src/dispatcher.rs#L396-L425), [trigger_repo.rs `TriggerKind::DeadLetter` → `is_dead_letter_handler`](crates/manager-core/src/dispatcher.rs#L228-L229) | ✅ flag set when dispatcher resolves a `kind = 'dead_letter'` trigger; on failure, original DL annotated with `resolution = 'handler_failed'`, row deleted, never retried, never DL'd |
|
| **SQL builder: every user input parameter-bound; no string interpolation** | [docs_repo.rs:319-420](crates/manager-core/src/docs_repo.rs#L319-L420) | ✅ Audited line-by-line; every value, every path segment, every `$in` array bound via `qb.push_bind(...)`. Only literal SQL is hardcoded keywords + operator tokens. `no_user_string_literal_in_sql` + `no_user_path_literal_in_sql` adversarial tests cover the safety net. |
|
||||||
| Sync HTTP failures do NOT dead-letter | [dispatcher.rs:378-394](crates/manager-core/src/dispatcher.rs#L378-L394) | ✅ early return before the DL-write block |
|
| `WHERE app_id = $1 AND collection = $2` always first | `every_query_starts_with_app_id_and_collection_predicate` test pins this across 8 filter shapes | ✅ |
|
||||||
| `dead_letters::list` NOT shipped (deferred to v1.2) | [executor-core/src/sdk/dead_letters.rs:13](crates/executor-core/src/sdk/dead_letters.rs#L13) | ✅ explicit doc-comment citing design notes §4; only `replay` + `resolve` registered |
|
| `$ne` uses `IS DISTINCT FROM`; `$eq null` → `IS NULL`; `$ne null` → `IS NOT NULL` | docs_repo.rs `ComparisonOp::Ne` + tests | ✅ Avoids NULL-handling traps |
|
||||||
| Trigger execution runs as registrant's principal | [dispatcher.rs:249-253](crates/manager-core/src/dispatcher.rs#L249-L253) + [`registered_by_principal` column](crates/manager-core/migrations/0008_triggers.sql#L39) | ✅ principal resolved from the trigger row at dispatch time |
|
| `docs:*` triggers via Layout E extension | [0014_docs_triggers.sql](crates/manager-core/migrations/0014_docs_triggers.sql) + trigger_repo.rs | ✅ Mirrors `kv_trigger_details`; CHECK constraints widened (not replaced) |
|
||||||
| 30-day DL retention, env-overridable | [gc.rs](crates/manager-core/src/gc.rs) | ✅ |
|
| Dispatcher routes `OutboxSourceKind::Docs` | dispatcher.rs match-arm extension | ✅ One-line `Kv \| DeadLetter \| Docs` change; reuses generic resolution path |
|
||||||
| 7-day abandoned-executions retention | [gc.rs](crates/manager-core/src/gc.rs) | ✅ |
|
| `ctx.event.docs.prev_data` change-data-capture | engine.rs `trigger_event_to_dynamic` + repo's update/delete return prior data | ✅ Works for update + delete; create has `prev_data = ()` |
|
||||||
| Trigger-depth limit (default 8); depth-exceeded does NOT dead-letter | [dispatcher.rs:122-137](crates/manager-core/src/dispatcher.rs#L122-L137) | ✅ design-notes §4 honored ("depth-exceeded means you built a loop") — row dropped + logged, no DL spam |
|
| `Capability::AppDocsRead/Write` mapped to `script:read`/`script:write` (no new scopes) | [authz.rs](crates/manager-core/src/authz.rs) | ✅ Seven-scope commitment honored |
|
||||||
| Dashboard surface: badge + list view + Replay + Mark resolved | [dashboard/src/routes/apps/+page.svelte](dashboard/src/routes/apps/+page.svelte), [dashboard/src/routes/apps/\[slug\]/dead-letters/+page.svelte](dashboard/src/routes/apps/[slug]/dead-letters/+page.svelte) | ✅ all required columns + actions + expandable row detail; `npm run check` reports 0 errors |
|
| Per-mutation `ServiceEvent` emission via injected emitter | [outbox_event_emitter.rs emit_docs](crates/manager-core/src/outbox_event_emitter.rs) | ✅ Best-effort emit after success; mirrors KV |
|
||||||
| Status: workspace 1.1.0 → 1.1.1, SDK 1.1 → 1.2, dashboard 0.6.0 → 0.7.0, CHANGELOG.md created | last commit `66b661f` | ✅ |
|
|
||||||
| `ctx.event` shape (KV: source/op/collection/key/value; DL: original/attempts/last_error/ids/timestamps) | [shared/src/trigger_event.rs](crates/shared/src/trigger_event.rs), [executor-core engine tests](crates/executor-core/src/engine.rs) | ✅ matches design notes §4 shape exactly; tests verify both variants + the "absent for direct invocations" rule |
|
|
||||||
|
|
||||||
I sampled the design-notes diff (`git diff main..HEAD -- docs/v1.1.x-design-notes.md`) — every "Decided 2026-06-01" entry the agent absorbed into commit `1efb350` matches the decisions made in conversation. No drift.
|
## 3. Substantive strengths
|
||||||
|
|
||||||
## 3. Deviations from the prompt (all reviewed, all acceptable)
|
**SQL builder audit holds end-to-end.** [docs_repo.rs:319-420](crates/manager-core/src/docs_repo.rs#L319-L420) was traced line-by-line. Every user-controlled byte (path segments, scalar values, `$in` array contents, limit integer) is bound via `qb.push_bind(...)`. Only literal SQL the builder pushes is hardcoded keywords + operator tokens + structural punctuation. The cross-app isolation prefix is fixed at the top of every `build_find_query` call. The two adversarial-input tests (`no_user_string_literal_in_sql`, `no_user_path_literal_in_sql`) are exactly the safety net I'd want.
|
||||||
|
|
||||||
The HANDBACK's §3 lists nine deviations / mid-implementation decisions. My take on each:
|
**`prev_data` CTE pattern is correct.** Returns `Some(prev_data)` from a `WITH old AS (SELECT) UPDATE ... RETURNING (SELECT data FROM old)` shape. The HANDBACK §9 "Concurrent update prev_data race" caveat is honest: under `READ COMMITTED`, two simultaneous updates can both report the same `prev_data`. Same tradeoff as KV. For audit-grade triggers (v1.2+) the escalation to `SELECT ... FOR UPDATE` is the right fix.
|
||||||
|
|
||||||
1. **Outbox column set chosen** (`script_id`, `trigger_id` polymorphic, `claimed_by TEXT`, `trigger_depth`, `root_execution_id` denormalized; no `is_dead_letter_handler` column). The design notes explicitly deferred this set to implementation. The chosen shape is sensible: dispatcher can build `ExecRequest` without re-joining; the `is_dead_letter_handler` derivation from `triggers.kind` at dispatch time is cleaner than storing redundant state. ✅
|
**Layout E extension is mechanically clean.** Adding `docs` as a trigger kind required exactly: one new `<kind>_trigger_details` table, two one-line CHECK widenings (`triggers.kind` + `outbox.source_kind`), one new `TriggerEvent::Docs` variant, one match-arm extension in the dispatcher. Future kinds (cron v1.1.4, pubsub v1.1.5) should follow this template — v1.1.2's implementation is the proof that Layout E pays its design rent.
|
||||||
|
|
||||||
2. **KV pagination is cursor-style** (base64-encoded last-key, 100 default / 1000 max). The prompt left this open; cursor-style is the right default for KV-shaped data. ✅
|
**Operator-set is correct precedent.** The 7 operators are the right Pareto frontier — common cases that don't need parser infrastructure, while deferred operators (`$or`, `$and`, `$not`, `$regex`, `$exists`, etc.) all genuinely need infrastructure that v1.2 builds. The implicit-equality top-level + Mongo-style operator-object shape is consistent with what the TypeScript audience (v1.1.6 `@picloud/client`) will already know.
|
||||||
|
|
||||||
3. **KV TTL deferred**. Blueprint §8.1 reserved `expires_at` but v1.1.1 SDK doesn't surface TTL. Omitting the column from migration 0007 keeps the schema minimal; adding it later is a non-breaking forward migration. ✅ (CHANGELOG records the deferral.)
|
**Snapshot tests on error wording.** Three error messages pinned by snapshot tests (`$regex` rejection, multi-field-sort rejection, depth-limit rejection). Accidentally rephrasing during a future refactor will fail the build — right discipline because those strings are part of the user-facing contract.
|
||||||
|
|
||||||
4. **Authz scope mapping** (4 new capabilities mapped to existing 7 scopes — `AppKvRead → script:read`, `AppKvWrite → script:write`, `AppManageTriggers → app:admin`, `AppDeadLetterManage → app:admin`). The "seven-scope commitment" is a project convention in `crates/shared/src/auth.rs:103` the prompt didn't mention; honoring it is correct. The specific mapping is defensible: a token with `script:read` on an app already implies "can see the data behind those scripts," and admin-level scope for trigger/DL management is standard for control-plane operations. ✅
|
## 4. Schema decisions audited
|
||||||
|
|
||||||
5. **Script-as-gate authz** (`if cx.principal.is_some()` then check; else skip — public HTTP runs anonymously without an authz failure). This matches the SDK-shape doc's note that "the data plane is unauthenticated by default — public HTTP scripts run with `None`." Cross-app isolation is preserved regardless (every query keyed by `cx.app_id`). DL replay/resolve correctly bypasses this and hard-requires a principal. ✅
|
| HANDBACK §4 decision | Verdict |
|
||||||
|
|---|---|
|
||||||
|
| GIN with `jsonb_path_ops` opclass | ✅ Smaller index, accelerates `@>` containment; range operators fall back to scan within small `(app_id, collection)` partition |
|
||||||
|
| Two migrations (0013_docs.sql + 0014_docs_triggers.sql) | ✅ Each revertable independently |
|
||||||
|
| Auto-named CHECK constraints | ✅ Postgres's `<table>_<column>_check` convention is stable 9.6+; works as designed |
|
||||||
|
| `docs_trigger_details.ops` without `DEFAULT '{}'` | ✅ Mirrors KV |
|
||||||
|
| No `dispatch_mode` on `docs_trigger_details` | ✅ Parent column suffices |
|
||||||
|
|
||||||
6. **Trait split `OutboxRepo` vs `OutboxWriter`**. Orchestrator-core can't depend on manager-core; the small `OutboxWriter` trait in shared (one method) lets the orchestrator enqueue HTTP rows without inverting the dependency arrow. ✅ Pattern mirrors the existing `members_concrete`/`AuthzRepo` split.
|
## 5. HANDBACK open questions — my answers
|
||||||
|
|
||||||
7. **`InboxResolver` in shared, `InboxRegistry` in orchestrator-core**. Same split rationale. Cluster mode (v1.3+) swaps the impl behind the unchanged trait. ✅
|
**Q1: CHECK-constraint name verification.** The auto-naming convention `<table>_<column>_check` is stable in Postgres 9.6+. Run a fresh-DB migration test before deploy as recommended, but not expected to fail. **Not a merge blocker.**
|
||||||
|
|
||||||
8. **manager-core now depends on executor-core**. ⚠️ **See §4 below — flagged, accepted, but should be reflected in `CLAUDE.md`.**
|
**Q2: Postgres-integration tests for `docs_repo`.** Defer following v1.1.1's precedent (KV doesn't have live-DB tests either). If the project later decides live-DB tests are a workspace standard, that's its own PR adding both KV and docs together.
|
||||||
|
|
||||||
9. **Sync HTTP via outbox is the default for user routes** (admin bypass `/api/v1/execute/{id}` keeps direct dispatch). Matches the design-notes decision; the bypass's direct path is acceptable for admin tooling speed. ✅
|
**Q3: Parser promotion to `picloud-shared` now or v1.2.** Defer to v1.2 as planned. Single consumer today; v1.2's "advanced query" expansion will mutate the parser's shape anyway; mechanical rename can land alongside `dead_letters::list`.
|
||||||
|
|
||||||
## 4. The one concern worth surfacing: manager-core → executor-core
|
**Q4: Doc envelope future-proofing for `deleted_at`.** Current shape leaves it naturally addable as a sibling field of `data`. Right shape.
|
||||||
|
|
||||||
`CLAUDE.md` working rules say:
|
**Q5: `$eq: null` semantics.** Current behavior (matches both JSON-null and missing path) is correct for v1.1.2. Users who need to distinguish them can express that combination in v1.2 with `$exists: true AND $eq: null`.
|
||||||
|
|
||||||
> Honor the three-service boundary. Don't reach across `*-core` crates. If
|
## 6. Smaller observations
|
||||||
> orchestrator-core needs something from manager-core, define a trait in
|
|
||||||
> shared and inject the impl.
|
|
||||||
|
|
||||||
The dispatcher in manager-core directly imports `ExecRequest`, `ExecResponse`, `ExecError`, and `InvocationType` from `executor-core`:
|
- `find` doesn't paginate in v1.1.2 — pagination on filter queries is deferred to v1.2 (HANDBACK §9). Acceptable.
|
||||||
|
- Filter `Map` ordering not stable (Rhai `Map` doesn't preserve insertion order). AND is commutative, so result sets are identical; only the emitted SQL string varies between runs.
|
||||||
|
- Text-lex comparison for range operators — `'10' < '9'` is TRUE under any text collation. Surfaced in CHANGELOG with the zero-pad workaround. v1.2's numeric-aware operators are the fix.
|
||||||
|
- Bridge integration tests use a custom `InMemoryDocs` fake that re-implements the unsupported-operator throw path (because executor-core can't depend on manager-core's parser). Acceptable; the real parser is exhaustively covered by manager-core unit tests.
|
||||||
|
|
||||||
```rust
|
## 7. Iteration 1 → iteration 2 deltas
|
||||||
// crates/manager-core/src/dispatcher.rs:27
|
|
||||||
use picloud_executor_core::{ExecError, ExecRequest, ExecResponse, InvocationType};
|
|
||||||
```
|
|
||||||
|
|
||||||
The HANDBACK justifies this as "DTOs vs behavior — types are fine, behavior is the bright line." That's a defensible interpretation, but not what `CLAUDE.md` actually says.
|
Iteration 1 verdict was REQUEST-CHANGES on the sole basis of:
|
||||||
|
- `cargo fmt --check` failed at `docs_service.rs:456-457` (one-line collapse for the `$in` arm's `arr.iter().any(...)`)
|
||||||
|
- HANDBACK §8 explicitly claimed `cargo fmt --check` was green — false against the audited HEAD
|
||||||
|
|
||||||
**Two options the project can pick:**
|
Iteration 2 (2 new commits):
|
||||||
|
- `bf26a25 chore: cargo fmt` — the single-line collapse. Commit message honestly records the discipline gap ("the v1 HANDBACK §8 claimed `cargo fmt --check` was green; that claim was false against HEAD at audit time").
|
||||||
|
- `fedc63b docs(v1.1.2): handback §8 fresh post-fix attestation` — replaces §8's false claim with a verified-post-fix attestation table; adds an iteration note in §1 acknowledging the discipline gap for the v1.1.3 retro.
|
||||||
|
|
||||||
- **(a) Accept the dependency and update `CLAUDE.md`** to clarify that the three-service boundary is about *behavior*, not *types* — `ExecRequest`/`ExecResponse`/`ExecError` are transport DTOs and crossing the wire is normal. This is the lower-friction choice and matches how the agent's instincts ran.
|
Re-verification on iteration-2 HEAD:
|
||||||
- **(b) Refactor**: move `ExecRequest`/`ExecResponse`/`ExecError`/`InvocationType` to `shared`. About 200 lines of moves; would land cleanly as a follow-up PR.
|
- fmt: exit 0 (no diff) ✓
|
||||||
|
- clippy: exit 0 (no warnings) ✓
|
||||||
|
- tests: 320 passed, 0 failed, 132 ignored ✓
|
||||||
|
|
||||||
**My recommendation: (a)**. The dispatcher genuinely needs to construct and interpret these types, and they're the natural "what the executor produces" surface — burying them in shared makes the executor's public API less self-contained. But the rule as currently written disagrees; we should pick one explicitly.
|
All matches what the iteration-2 HANDBACK §8 claims. No drift between claim and reality this time.
|
||||||
|
|
||||||
This is **not a merge blocker** for v1.1.1 — the implementation already exists and works. The CLAUDE.md update can land as a small commit on `main` after the merge.
|
## 8. Versioning audit
|
||||||
|
|
||||||
## 5. Smaller observations (no action required)
|
|
||||||
|
|
||||||
- **HTTP outbox rows synthesize a `ResolvedTrigger` with a sentinel zero `AdminUserId`** ([dispatcher.rs:342](crates/manager-core/src/dispatcher.rs#L342)). The HANDBACK flags this as a code smell; I agree, but the cleaner shape (`enum DispatchTarget { Trigger(ResolvedTrigger), Http(HttpRoute) }`) is a refactor that doesn't belong in v1.1.1. Worth doing in v1.1.2 alongside the docs work since the dispatcher will gain another trigger kind.
|
|
||||||
- **Triggers parent `dispatch_mode` defaults to `'async'`** ([0008_triggers.sql:30](crates/manager-core/migrations/0008_triggers.sql#L30)) with `sync` allowed by the CHECK constraint but unsupported in v1.1.1 (sync trigger would mean firing inline with the originating mutation, which we don't do). The migration comment captures this; worth a future commit to either remove `'sync'` from the CHECK or use it for an `inline_pre_mutate` semantics if it ever makes sense. Not v1.1.1's problem.
|
|
||||||
- **Metric counters are TODO** at three call sites (`picloud_trigger_depth_exceeded`, `picloud_dead_letter_handler_failures`, `picloud_abandoned_executions_total`). The events are logged via `tracing::warn`/`error` in the meantime. Per the prompt and roadmap, metrics surface is v1.1.7+. ✅
|
|
||||||
- **Dispatcher tick cadence is 100ms with `CLAIM_BATCH = 8`**, serial per tick. The ExecutionGate bounds total concurrent executions globally, so parallelism within a tick is purely an optimization. Reasonable MVP choice; can parallelize later without changing semantics.
|
|
||||||
- **Open Q1 in HANDBACK (claimed-rows-stuck-on-crash)** is a real cluster-mode concern, correctly out-of-scope for v1.1.1 (single dispatcher per process). Cluster mode adds a stale-claim sweeper — track for v1.3+.
|
|
||||||
- **Open Q3 in HANDBACK (HTTP-triggered scripts run with `principal: None`)** is correct as-is. The "trigger executions inherit the registrant's principal" decision applies to triggers; HTTP routes have no registrant in that sense. Public HTTP is anonymous by design.
|
|
||||||
|
|
||||||
## 6. Versioning audit
|
|
||||||
|
|
||||||
| File | Before | After | Status |
|
| File | Before | After | Status |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Workspace `Cargo.toml` (workspace.package.version) | 1.1.0 | 1.1.1 | ✅ |
|
| Workspace `Cargo.toml` | 1.1.1 | 1.1.2 | ✅ |
|
||||||
| SDK schema version (`shared/src/version.rs`) | 1.1 | 1.2 | ✅ correctly bumped — the SDK surface added `KvService` + `DeadLetterService` + `TriggerEvent` |
|
| SDK schema (`shared/src/version.rs`) | 1.2 | 1.3 | ✅ Services bundle gains `docs: Arc<dyn DocsService>` |
|
||||||
| Dashboard `package.json` | 0.6.0 | 0.7.0 | ✅ |
|
| Dashboard `package.json` | 0.7.0 | 0.8.0 | ✅ (alignment with workspace) |
|
||||||
| Migrations | 0001..0006 | 0007..0012 added | ✅ sequential, no skips |
|
| Migrations | 0001..0012 | 0013, 0014 added | ✅ Sequential, no skips |
|
||||||
| CHANGELOG.md | not present | created at repo root | ✅ first entry covers v1.1.1 |
|
| CHANGELOG.md | v1.1.1 entry | v1.1.2 entry appended | ✅ |
|
||||||
|
|
||||||
## 7. Manual smoke recommendation
|
## 9. Recommended next steps
|
||||||
|
|
||||||
The reviewer (you) does **not** need to run the manual end-to-end smoke before merging — the automated tests + the static review above cover the contracts. The smoke flow in HANDBACK §7 is worth running **after merge** as a release-validation step before tagging `v1.1.1` (if the project tags releases). Specifically:
|
1. **Merge** `feat/v1.1.2-documents` into `main` (fast-forward; branch is linear ahead).
|
||||||
|
2. **Pause** before dispatching v1.1.3 (Modules). The v1.1.2 work establishes the query-DSL precedent that v1.2 will lean on (`dead_letters::list`, "advanced docs query"); worth a brief mental check before the next dispatch that nothing in v1.1.2's shape has prompted a roadmap revision.
|
||||||
|
3. **Carry the discipline lesson forward.** The v1.1.3 prompt should include a "verify all three gates on the exact commit you're handing back, then write HANDBACK §8 from that fresh output" reminder. Cost is one sentence; benefit is removing the only audit finding from v1.1.2.
|
||||||
|
|
||||||
1. `docker compose up -d` (fresh DB)
|
Branch ready for merge. **Verdict: APPROVE.**
|
||||||
2. `cargo run -p picloud`
|
|
||||||
3. Create app + script-that-throws + KV trigger
|
|
||||||
4. Trigger a KV write → wait ~7s → confirm DL row appears
|
|
||||||
5. Dashboard: red badge on apps list, list view shows the row, Replay creates a new outbox row + dispatcher re-runs, Mark resolved sets `resolution = 'ignored'`
|
|
||||||
6. Async route test: `POST /work` with `dispatch_mode=async` route → expect 202 + JSON body
|
|
||||||
|
|
||||||
If any of those misbehave post-merge, revert is straightforward (12 commits, ahead of main, no dependencies have pulled changes yet).
|
|
||||||
|
|
||||||
## 8. Recommended next steps (post-merge)
|
|
||||||
|
|
||||||
1. **Merge** `feat/v1.1.1-storage-and-events` into `main` (fast-forward; branch is linear ahead).
|
|
||||||
2. **Tag** `v1.1.1` if release tagging is the project convention (git log shows v1.1.0 had a release commit but I didn't see a tag — confirm with the project owner).
|
|
||||||
3. **Small CLAUDE.md update** clarifying the three-service boundary's scope (types crossing is fine; behavior crossing is what's prohibited). One-paragraph change.
|
|
||||||
4. **Pause** before dispatching the v1.1.2 (Documents) agent — the v1.1.1 work shipped substantial infrastructure that v1.1.2 will lean on, and there may be small lessons from the v1.1.1 implementation to fold into the v1.1.2 prompt (e.g., reaffirming the "manager-core depends on executor-core for DTOs" pattern explicitly so the docs agent doesn't second-guess it).
|
|
||||||
|
|
||||||
Branch is ready for merge. Verdict: **APPROVE**.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user