Skip to content

Commit ac39531

Browse files
Phase 87: Operator UI — Stage Matrix, Failure Retry, Eligibility Trace & Priority (#233)
* docs(87): capture phase context * docs(state): record phase 87 context session * docs(87): UI design contract Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4M6HDCD4UpPrMuCyn1Q9H * docs(87): reconcile UI-SPEC typography + spacing to shipped design system Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4M6HDCD4UpPrMuCyn1Q9H * docs(state): record phase 87 UI-SPEC session * docs(phase-87): add validation strategy * docs(87): create phase plan (8 plans, 6 waves) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4M6HDCD4UpPrMuCyn1Q9H * docs(87): pattern map + plan-checker warning fixes + planning state - Add 87-PATTERNS.md (24/24 analogs, 4 PR seams) - Mark RESEARCH open questions RESOLVED (OQ-1 scope-minimal, OQ-2 recovery-parity, OQ-3 CHECK) - Cite D-02 in 87-04/87-06 must_haves (files-table + right-pane dual home) - Correct VALIDATION behavior #3 (enrich ELIGIBILITY_DAG is empty) + fill coverage map, approve - Record planning complete in STATE.md; ROADMAP wave-dep annotation Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4M6HDCD4UpPrMuCyn1Q9H * docs(phase-87): mark phase start * feat(87-01): add StageSkip ORM sidecar model + registry - StageSkip(TimestampMixin, Base) on stage_skip with UUID PK, FK->files.id - UNIQUE(file_id, stage) uq_stage_skip_file_stage (<=1 skip per file/stage, D-13a) - CHECK stage IN enrich values ck_stage_skip_enrich_only (D-10, enrich-only) - reason NOT NULL (D-09); register StageSkip on Base.metadata + __all__ * feat(87-01): additive migration 037 create stage_skip - revision 037, down_revision 036 (bare-number strings) - create_table stage_skip: id PK, file_id FK->files.id, stage, reason, skipped_at - UNIQUE(file_id, stage) + enrich-only CHECK via op.f() bare names - mirrored downgrade drop_table; no backfill (greenfield); no SAQ jobs table * test(87-01): migration 037 up/down + constraints + empty-diff + no-saq - bare-number revision assert (037/036) + no-saq_jobs grep guard - upgrade 036->037: stage_skip exists; valid enrich skip inserts - UNIQUE(file_id,stage) rejects duplicate; enrich-only CHECK rejects 'propose' - empty autogenerate diff for stage_skip (ORM __table_args__ mirror parity) - mirrored downgrade drops the table * docs(87-01): complete stage_skip marker sidecar plan * docs(phase-87): update tracking after wave 1 * feat(87-02): Python twin — Status.SKIPPED + precedence + eligible/domain_completed threading - Add SKIPPED member to Status (5-way: in_flight ≻ done ≻ skipped ≻ failed ≻ not_started) - Thread skipped scalar into _analyze/_metadata/_fingerprint (after done, before failed) - resolve_status passes skipped to the three enrich branches only; downstream ignores it - eligible: SKIPPED enrich stage leaves the pending set; domain_completed: SKIPPED is complete - Extend tests/shared/test_stage_resolver.py with the six D-08 precedence/eligibility behaviors * feat(87-02): SQL twin — skipped_clause builder threaded into the three composers - Import StageSkip; add skipped_clause(stage) correlated-exists builder (enrich-only ValueError guard) - stage_status_case: 5-way for enrich (done ≻ skipped ≻ failed), downstream stays 4-way - eligible_clause: append not_(skipped_clause) so a skipped stage leaves all 3 enrich pending sets - domain_completed_clause: skipped disjunct so recovery treats a skipped stage as complete - Add shared guard tests: skipped_clause builds for enrich, raises ValueError on downstream (D-10) * chore(87-02): log out-of-scope drain psycopg2 fixture gap to deferred-items * docs(87-02): complete skipped-marker derivation-threading plan * docs(phase-87): update tracking after wave 2 * fix(test): coerce bare postgresql:// TEST_DATABASE_URL to asyncpg driver The async_engine fixture feeds TEST_DATABASE_URL straight to create_async_engine. A bare postgresql:// (or postgresql+psycopg2://) DSN — the natural form operators export, matching PHAZE_QUEUE_URL — resolves SQLAlchemy's default psycopg2 sync dialect, which the async-only stack does not install, so every DB-fixture test dies at setup with "No module named 'psycopg2'" (surfaced by test_drain_double_dispatch.py). _coerce_async_dsn() normalizes sync driver prefixes to +asyncpg, rewriting only the leading token. Adds a regression guard and resolves the Phase 87 Wave 2 deferred item (rejecting the psycopg2-binary option per the project's async-only driver rule). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4M6HDCD4UpPrMuCyn1Q9H * test(87-03): extend DERIV-04 harness with skipped cells on all 3 axes - Add seed_analysis_skipped_over_failed (skipped ≻ failed precedence cell), seed_metadata_skipped, seed_fp_skipped mirroring the existing seed idiom. - Add skipped cells to CASES (expect 'skipped'), ELIGIBLE_CASES (False -- leaves the pending set), DOMAIN_COMPLETED_CASES (True -- recovery treats it complete). - Extend load_scalars to read a skipped bool from stage_skip for each enrich stage so the Python twin sees it; three parametrized tests cover the new cells with zero test-body edits. - Mutation-verified: dropping the skipped branch from stage_status_case turns the three skipped CASES cells RED; restored to green (59 passed). * feat(87-04): 5-bucket stage pill + 6-pill matrix with 7→6 remap - _stage_pill.html: done/in_flight/not_started/failed/skipped tokens, each with glyph + word + aria-label + dark: pair (WCAG 1.4.1) - _stage_matrix.html: 6 pills Meta·FP·Analyze·Prop·Appr·Exec (Appr→review, Exec→apply, tracklist omitted) + one-legend-per-surface - tests/shared/test_stage_pill_render.py: buckets, order, remap landmine, legend * test(87-03): skipped leaves all 3 enrich pending sets + recovery guards - Add per-enrich-stage pending-set-drop tests (metadata/fingerprint/analyze): a stage_skip marker removes a would-be-pending file from get_metadata_pending_files / get_fingerprint_pending_files / get_discovered_files_with_duration, proving the ~skipped conjunct in eligible_clause propagates zero-edit. Positive controls assert the seed WOULD be pending. Mutation-verified: dropping ~skipped -> all 3 RED. - Recovery guards (behavior 5): a force-skipped analyze/metadata file is domain-complete and NOT re-enqueued, on both the automatic and manual (force=True) paths. Mutation-verified: dropping the skipped_clause disjunct from domain_completed_clause -> all 3 RED. - Track a fingerprint recovery GAP with a strict-xfail regression guard: _build_done_sets reads done_clause(FINGERPRINT) only (never skipped_clause), so a force-skipped fingerprint with a surviving ledger row IS re-enqueued. Documented in deferred-items.md (source fix is out of this tests-only parallel plan's ownership). * test(87-03): shadow-compare stays green post-skip (additive-writer proof) - Seed an analysis_failed file with analysis.failed_at set AND an additive stage_skip(analyze) marker, failed_at left in place (mirrors the real write path). - Assert run_shadow_compare reports zero hard divergences (the analysis_failed invariant state='analysis_failed' => failed_clause(ANALYZE) still holds) and the soft allowlist is unchanged ({fingerprinted, local_analyzing}) -- the skip needs NO new allowlist entry (D-13d). - Assert failed_clause(ANALYZE) is still True for the skipped-over-failed file. - Mutation-verified teeth: simulating a non-additive writer that clears failed_at turns all 3 tests RED -- so a later writer that tidies failed_at regresses the Phase-79 gate loudly. * feat(87-04): paginated, per-row-derived, degrade-safe files table - get_files_page + _files_page_stmt: bounded (LIMIT+1 sentinel, no whole-corpus COUNT), correlated per-page stage_status_case derivation, begin_nested SAVEPOINT degrade to a safe empty page (T-87-11/T-87-12) - GET /pipeline/files: stage/bucket validated against Stage/Status allowlists and plumbed through for Plan 05 (T-87-14) - files_table_view.html: 6-pill matrix per row, one legend per surface, record slide-in binding, autoescaped path cell, cursor Prev/Next (no COUNT) - test_files_page.py: bounded/no-COUNT, per-row derivation, EXPLAIN partial-index usage, SAVEPOINT degrade * docs(87-04): complete stage-matrix pill + paginated derived files table plan * docs(87-03): complete skipped-marker drift-lock + behavior-guard plan Flags the fingerprint recovery-skip gap (deferred, out of ownership) at the top of the summary; tracked by a strict-xfail guard + deferred-items.md. * fix(87): exclude force-skipped fingerprint from recovery re-enqueue reenqueue._build_done_sets derived fingerprint_done from done_clause(FINGERPRINT) only, never skipped_clause. A file the operator force-skipped for fingerprint — domain-complete everywhere else via Plan 02's skipped_clause disjunct — was therefore NOT in fingerprint_done, so recover_orphaned_work re-enqueued it if its fingerprint_file ledger row survived a restart, re-driving a stage the operator explicitly skipped (phase-87 behavior 5 gap surfaced by plan 87-03). Derive fingerprint_done from or_(done_clause, skipped_clause): a FAILED-but-not- skipped fingerprint still auto-retries (FAILURE_IS_TERMINAL[fingerprint] is False), a force-SKIPPED one is excluded. Not domain_completed_clause — that couples recovery to the terminality axis and obscures the FAIL-04 auto-retry intent. Converts the 87-03 strict-xfail tripwire to a passing guard (mutation-verified); resolves the Wave 3 deferred item. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4M6HDCD4UpPrMuCyn1Q9H * docs(phase-87): update tracking after wave 3 * feat(87-05): per-stage status filter lens over the files table (UI-02/D-03) - _status_filter_bar.html: URL-carried stage x bucket filter (hx-push-url), failure-inclusive, one canonical surface (no separate failures page) - files_table_view.html: include the filter bar above the table + filter-aware empty states (unfiltered / failed-filter / other-filter copy per Copywriting Contract) - tests/integration/test_files_filter.py: failed-filter returns only failed rows, empty failed-filter renders the failed-filter copy, filter state is URL-carried * feat(87-05): retire raw-enum State render + mutation-tested grep guard (UI-01) - metadata_workspace.html: remove the {'text': f.state} raw-enum State column/cell (the one genuine raw-internal-status render); per-stage status now lives only in the derived _stage_matrix pill row on the files table (anti-feature table cutover) - analyze_workspace.html: rename the derived window-progress column State -> Progress (it never rendered a raw enum; the f.state == '...' lines are pure comparisons that drive the load-bearing WORK-04 mid-flight signal) - tests/shared/test_no_raw_state_render.py: grep guard scanning pipeline/partials + record templates for {{ f.state }} / {'text': f.state} render forms; strips Jinja comments so prose mentions don't false-positive; comparisons/sets are filtered out. Mutation-tested: re-adding a raw f.state cell turns it RED (observation in SUMMARY) * feat(87-06): force-skip writer endpoint (enrich-only, additive, sanitized, committed) - POST /pipeline/files/{file_id}/skip/{stage} writes a stage_skip marker (D-08/UI-04) - enrich-only 422 guard (D-10, T-87-18) mirrors pipeline_stages._validate_stage - required reason (D-09, T-87-22) + sanitize_pg_text before persist (T-87-19) - additive-only: never clears analysis.failed_at (T-87-20, shadow-compare stays green) - commits itself (get_session never auto-commits); tests read from an INDEPENDENT session * docs(87-05): complete failure-filter-lens + raw-state-retirement plan * feat(87-06): single-row eligibility trace endpoint + _eligibility_trace.html - GET /pipeline/files/{file_id}/trace/{stage} evaluates resolve_status + the REAL eligible() for ONE file and names the single unmet blocker (UI-03, behavior 10) - single-row file_id-scoped reads, never a corpus scan (T-87-23); degrade-safe - verdict is authoritative eligible(); a SKIPPED upstream stays gating under the OQ-1 scope-minimal resolution (Phase 90) -- rendered honestly, not as counterfeit 'satisfied' - _eligibility_trace.html: verdict + 4 named conjuncts, blocker highlighted, autoescaped * feat(87-06): right-pane expanded matrix (trace triggers) + enrich-only force-skip dialog - record_body.html: 6 keyboard-accessible stage trace triggers (hx-get the trace endpoint, reveal _eligibility_trace.html beneath the clicked pill) — the right-pane half of D-02 - _force_skip_dialog.html: Alpine x-trap focus-trap confirm dialog with a REQUIRED reason textarea, accent-cyan confirm, Cancel default focus; posts to the Task-1 writer - force-skip rendered for enrich stages ONLY (metadata/fingerprint/analyze); propose/review/ apply carry NO skip affordance (approval-bypass hazard, D-10) — render-asserted with teeth - verbatim UI-SPEC copy; 422 validation force-swapped inline; success closes + aria-live toast * docs(87-06): complete right-pane eligibility trace + force-skip plan * docs(phase-87): update tracking after wave 4 * feat(87-07): per-file scoped retry variants (analyze + metadata), manual-analyze preserved - Add POST /pipeline/files/{id}/analysis-failed/retry and /pipeline/files/{id}/metadata-failed/retry: scoped twins of the bulk endpoints, filtered to one file through the SAME Phase-30-hardened guarded funnel (resolve_queue_for_task -> NoActiveAgentError guard -> enqueue). - Analyze per-file retry stays manual-only: flips ANALYSIS_FAILED -> FINGERPRINTED, clears analysis.failed_at in one txn, commits BEFORE enqueue (Phase-81 CR-01); no auto-loop (ELIGIBLE_AFTER_FAILURE[ANALYZE]=False, behavior 8 / T-87-24). - Metadata per-file retry leaves the failure row (D-11); reuses response partials verbatim. - Scoped by state/EXISTS so a non-failed/unknown id is a safe no-op (T-87-27). - tests: per-file+bulk route through the guarded funnel; no-agent -> amber, no mutation; behavior-8 no-auto-loop predicate; render assertions for Task 2 (-k render). * feat(87-07): wire per-row Retry (failed cells) + bulk Retry all failed · {stage} - files_table_view.html: per-row Retry renders ONLY on a failed metadata/analyze cell (HTMX POST to the per-file scoped variant; @click.stop keeps it off the row record slide-in; ack swaps into the cell result span; aria-label per the Copywriting Contract). Bulk primary Retry all failed · {stage} on the failed-filter view of an enrich stage, posting to the live bulk endpoint. Failed pill clears on the NEXT poll tick. - fingerprint gets NO manual retry control (per-row or bulk): it self-retries via the eligible_clause pending set (RESEARCH / ELIG-04). - _stage_matrix.html: doc note pointing to the cell-level retry home (pure token, no file_id). * docs(87-07): complete per-file + bulk retry affordances plan * docs(phase-87): update tracking after wave 5 * feat(87-08): per-stage orphan-count helper (recovery-candidate parity, degrade-safe) + #pipeline-stats seed - get_stage_orphan_counts: per-enrich-stage recovery-candidate count (ledger minus live minus domain-completed minus in-flight-cloud), reusing recover_orphaned_work's own predicate so the badge cannot drift - SAVEPOINT degrade to all-zeros; function-local reenqueue import breaks the cycle + preserves the control-only agent-worker boundary - seed metadataOrphan/analyzeOrphan/fingerprintOrphan onto the dag OOB fanout in _build_dag_context (no self-poll); base.html store defaults - test_orphan_count.py: parity vs recovery-candidate set + exclusions (domain-completed, force-skip, live key, in-flight cloud) + degrade * feat(87-08): rail — amber orphan badge + priority stepper + pause/resume re-wire - amber orphaned/stuck (recovery-candidate) badge per enrich node, bound to $store.pipeline.{metadata,fingerprint,analyze}Orphan, role=status, hidden at 0; rides the #pipeline-stats OOB fanout (no self-poll) - per-enrich-stage ▲/▼ priority stepper (delta ∓10) + pause/resume toggle re-wired to the LIVE POST /pipeline/stages/{stage}/{priority,pause,resume} endpoints (hx-swap=none; label reads the store, refreshed each 5s poll) - fresh markup against the live endpoints (no Phase-38 template resurrected); interactive controls in a sub-row OUTSIDE the nav <button> (no nesting); explicit aria-labels + the D-11 clarifying tooltip; collapsed-rail hidden - test_rail_priority_controls.py: endpoints/deltas, aria-labels, D-11 tooltip, orphan-badge store binding + hidden-at-0 + amber/role=status, no-nesting * chore(87-08): log pre-existing inert x-cloak (no CSS rule) to deferred-items * docs(87-08): complete DAG-rail orphan badge + priority/pause re-wire plan * fix(css): define [x-cloak] display:none so cloaked elements stop flashing Alpine v3 does not inject the [x-cloak]{display:none} rule — the app must define it. Neither assets/src/app.css nor the compiled output defined it, so every x-cloak in the templates (theme toggle, header, cmdk modal, record host, agents table, and the Phase-87 rail orphan badge / Resume / Paused caption) was inert and flashed fallback content for a few ms before x-show initialized. Add [x-cloak] { display: none !important; } to the source CSS (compiled output is gitignored, rebuilt via `just tailwind`). Regression guard asserts the source rule exists. App-wide pre-existing fix surfaced by plan 87-08; resolves the Wave 6 deferred item. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4M6HDCD4UpPrMuCyn1Q9H * docs(phase-87): update tracking after wave 6 * fix(87): register stage_skip in table guard + de-trip dead-template guard Two post-merge integration failures the per-plan gates could not see: - tests/shared/models/test_core_models.py::test_all_tables_defined asserts an exhaustive table set; migration 037's stage_skip (87-01) was never added. - tests/shared/core/test_dead_template_guard.py::test_entry_literals_resolve_to_templates false-tripped on a _STAGE_TRACE_LABELS comment: `_HTML_LITERAL` captures ANY quoted *.html in router source, and `..."Exec"). Mirrors _stage_matrix.html's pill order.` accidentally forms a quoted literal (closing " of "Exec" opens it, the apostrophe in html's closes it). Reworded the comment to drop the quotes and the .html token — did NOT relax the guard assertion or _HTML_LITERAL capture, per the guard's own WR-03 contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4M6HDCD4UpPrMuCyn1Q9H * fix(87): force-skip writer idempotent + validate reason after sanitize Code-review findings on the Phase-87 force_skip_stage writer: - CR-01 (blocker): a bare session.add(StageSkip)+commit hit UNIQUE(file_id, stage) with an unhandled IntegrityError -> HTTP 500 on any re-submit (a normal path: _force_skip_dialog.html is not hidden after success). Switch to pg_insert(...).on_conflict_do_nothing(index_elements=["file_id","stage"]), mirroring insert_ledger_if_absent — the marker's existence is the desired end state, so a duplicate is a no-op success. Mutation-verified. - WR-01: the D-09 "reason required" blank check ran on raw input before sanitize_pg_text, so a NUL-only reason (str.strip() does not remove NUL) slipped through and persisted as "". Sanitize first, validate the sanitized value. Adds two regression guards (idempotent-duplicate, NUL-only-reason). WR-02 (hot-poll orphan-count cost) tracked as a milestone backlog item. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4M6HDCD4UpPrMuCyn1Q9H * docs(87): backlog WR-02 orphan-count hot-poll cost with a 2026.7.5 close gate Phase 87 code review flagged get_stage_orphan_counts running recovery's full ledger derivation on every 5s /pipeline/stats poll (degrade-safe but can block on a large ledger). Genuinely a read-path perf redesign, so per the "fix inline or backlog with a close-out gate" rule it is recorded in the ROADMAP backlog as a MILESTONE-CLOSE GATE that must be resolved before completing 2026.7.5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4M6HDCD4UpPrMuCyn1Q9H * feat(87-09): surface derived files stage-matrix as a reachable rail workspace - shell.py: add STAGE_PARTIALS['files'] static literal + a _render_stage 'files' branch mirroring pipeline_files() context (get_files_page, page=1/size=25, no filters); fragment/full fork inherited from _render_stage - rail.html: add a keyboard-accessible 'Files' nav node (hx-get=/s/files) after Summary — max-lg:sr-only label, aria-hidden glyph, title, aria-current binding - test_shell_routes.py: add 'files' to _RAIL_STAGES (14 nodes) + fork test + reachability/a11y test (mutation-checked) Closes UI-01/UI-02 reachability gap: the built+tested files matrix was unreachable (no nav entry, chrome-less on direct nav). * docs(87): code review + verification artifacts, IN-01 doc fix, dispositions - 87-REVIEW.md (standard-depth review) + 87-VERIFICATION.md (passed, 5/5). - IN-01: correct the _safe_bucket_counts / get_stage_progress docstrings from FOUR-BUCKET to FIVE-BUCKET — the code already returns the skipped bucket (Status.SKIPPED zero-fills the count dict since 87-02); doc-only. - deferred-items.md: record CR-01/WR-01 fixed, WR-02 backlogged (milestone gate), IN-01 fixed, IN-02 accepted (degrades to a clear empty state). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4M6HDCD4UpPrMuCyn1Q9H * docs(phase-87): complete phase execution * docs(phase-87): evolve PROJECT.md after phase completion * docs(phase-87): add security threat verification (31/31 closed, threats_open: 0) * docs(phase-87): update validation strategy — re-audited, 10/10 covered, wave 0 complete * fix(87): host OOB poll-seed targets on the files workspace + add orphan seeds Two integration defects surfaced by Phase-87 live UAT (Playwright against a seeded local boot), both breaking the /pipeline/stats OOB seed round-trip that drives the chrome-polled rail badges: - The /s/files workspace (added by 87-09) reused files_table_view.html, which never composed the shared _workspace_poll_seeds.html host that every other workspace gets via _workspace_scaffold. So every 5s poll on /s/files logged htmx:oobErrorNoTarget for all seeds and none of the rail counts updated. Fix: shell._render_stage("files") sets include_poll_seeds, and files_table_view.html includes the seed host as a SIBLING of #files-table-view (so the inner filter/pagination swaps that target #files-table-view never duplicate the seed ids; the pipeline_files() filter fragment omits the flag). - _workspace_poll_seeds.html was missing the three UI-05 orphan seed targets (dag-seed-{metadata,fingerprint,analyze}Orphan). 87-08 added them to base.html's store + the stats-poll fanout but not to the seed host, so the amber rail orphan badge never received the poll's OOB seed on ANY workspace (stuck at base.html 0). Adds a durable guard (test_workspace_poll_seeds.py): every $store.pipeline key must have a dag-seed-<key> target, and the /s/files workspace hosts the seeds while the filter fragment does not. Mutation-verified (removing the orphan targets re-RED's it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C4M6HDCD4UpPrMuCyn1Q9H * test(87): persist live-UAT results + 2 fixes found via Playwright * docs(87): ship phase 87 — PR #233 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0717ffa commit ac39531

78 files changed

Lines changed: 10211 additions & 105 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.planning/PROJECT.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ Get 200K messy music and concert files properly named, organized into logical fo
112112

113113
**Phase-by-phase execution history (2026.7.5):**
114114

115+
**2026.7.5 Parallel Enrich DAG — Phase 87 (Operator UI — Stage Matrix, Failure Retry, Eligibility Trace & Priority) COMPLETE + verified 2026-07-11 (5/5 must-haves).** Replaces the raw-enum "State" column with a derived per-file stage matrix and gives the operator failure visibility+retry, a "why not eligible?" trace, force-skip, an orphaned-work count, and the restored per-stage priority stepper (UI-01..05, PRIO-01). Shipped across 9 plans / 6 waves + a gap-closure plan: 87-01 the `stage_skip` `(file_id,stage)` sidecar (migration `037`); 87-02 threaded a `skipped` marker (`Status.SKIPPED` + `skipped_clause`) through the single-source derivation with the precedence `in_flight ≻ done ≻ skipped ≻ failed ≻ not_started`; 87-03 locked the marker against SQL⇔Python drift; 87-04 the 5-bucket pill + 6-pill matrix + paginated, per-row-derived, degrade-safe `GET /pipeline/files` (no whole-corpus scan; all four Phase-77 partial indexes back the probes); 87-05 the URL-carried status/failure filter lens + retired the raw-enum column; 87-06 the single-row eligibility trace (honest — a skipped upstream renders as a gating blocker deferred to Phase 90, not "satisfied") + the enrich-only, sanitized, additive force-skip writer; 87-07 per-file + bulk retry affordances (analyze stays manual, terminal-guard preserved; fingerprint self-retries); 87-08 the recovery-parity orphan-count rail badge + the priority/pause/resume re-wire. **Six issues found and fixed inline during execution** (per the never-skip rule), each with a mutation-verified regression guard: a pre-existing `psycopg2` DSN footgun in `tests/conftest.py` (bare `postgresql://` → sync dialect; `_coerce_async_dsn`); a **behavior-5 recovery gap** where `recover_orphaned_work` re-enqueued force-SKIPPED fingerprint files (`reenqueue.py` `fingerprint_done` now `or_(done_clause, skipped_clause)`); two post-merge guard failures (table registry missing `stage_skip`; dead-template guard false-trip on a comment); and code-review **CR-01** (force-skip 500 on duplicate → `on_conflict_do_nothing`) + **WR-01** (validate the reason after sanitize). An app-wide `[x-cloak]` CSS rule (Alpine never injected it) was also fixed. **UI-01/UI-02 initially verified as a gap** (3/5) — the derived files matrix was built+tested but had no shell nav entry — closed by gap-closure plan 87-09 (a first-class "Files" rail node → `/s/files` with the inherited fragment/full-page fork), re-verified 5/5. Backlogged with a 2026.7.5 close-out gate: **WR-02** (the orphan-count badge runs recovery's full ledger derivation on every 5s poll — a read-path perf bound). Ships on branch `SimplicityGuy/phase-87`. **Next: Phase 88 (Lane / Agent Drill-In).**
116+
115117
**2026.7.5 Parallel Enrich DAG — Phase 81 (Per-Stage Failure Persistence & Retry Paths) COMPLETE + verified 2026-07-09 (4/4 success criteria).** Every enrich stage now has a durable failure marker and a retry story, closing the latent bug where a failed metadata extraction recorded *nothing* and became invisible-and-permanently-ineligible. Shipped across 6 plans / 2 waves: (1) `enums/stage.py` gains the two orthogonal DB-free tables `FAILURE_IS_TERMINAL` {analyze:True, metadata:True, fingerprint:False} and `ELIGIBLE_AFTER_FAILURE` {analyze:False, metadata:True, fingerprint:True} — `FAILURE_IS_TERMINAL` was cited by 80-CONTEXT D-01 but had existed in no `.py` until now — plus the pure `domain_completed()` and its SQL twin `domain_completed_clause()` in `services/stage_status.py`, drift-locked by the extended Phase-78 equivalence matrix. Both twins are deliberately **ledger-agnostic** (they do not model `in_flight`); Phase 80's recovery callers must apply the ledger check separately. (2) Migration `033` runs a mixed-row cleanup UPDATE (done wins — `failed_at` cleared, `analysis_completed_at` retained, D-09/D-04) BEFORE `create_check_constraint`, since `032`'s unguarded backfill had already produced mixed rows in the live corpus; the `analysis_completed_at` XOR `failed_at` CHECK is mirrored into the ORM `__table_args__` so autogenerate still yields an empty diff. The *destructive* Phase-90 migration is renumbered `033 → 034` across all forward-looking planning docs (D-08); dated historical records consciously keep `033`. (3) FAIL-02: `report_metadata_failed` persists a `metadata` row (`failed_at` + `error_message`) via `pg_insert(...).on_conflict_do_update` instead of writing nothing; `put_metadata` clears the marker on success; the request body is **optional** (`MetadataFailurePayload | None`, no `Body()` wrapper) so a bodyless POST from an older agent still 200s and clears the ledger (CR-02 version-skew guard, D-10). (4) FAIL-01: `report_analysis_failed` dual-writes `analysis.failed_at` + `error_message` alongside the still-live `state = ANALYSIS_FAILED`, clearing `analysis_completed_at` to satisfy the new CHECK; `put_analysis` clears the marker on success. The SC#1 backfill itself lives in migration `032` (Phase 77, `faee8b8a`) — SC#1 holds on the 77+81 combination, verified explicitly rather than assumed. (5) FAIL-03: `POST /pipeline/metadata-failed/retry` bulk operator retry reuses `failed_clause(Stage.METADATA)` and mirrors `retry_analysis_failed`'s Phase-30 guard (resolve the per-agent `meta` lane once, catch `NoActiveAgentError`, never fall through to the consumer-less default queue); per D-11 it leaves the failure row in place — only a successful re-run clears it. (6) FAIL-04 ships **no new writer** by design: characterization tests prove `report_fingerprint_failed` persists no `fingerprint_results` row (and writes no `engine='_task'` sentinel that would poison the per-engine badge joins); the durable, auto-retryable marker is the existing `status='failed'` row from `put_fingerprint`, and a failed fingerprint stays eligible (ELIG-04). Verifier 4/4 (independent codebase read + its own test re-runs). Post-merge gates: mypy clean (203 files), all nine buckets green (3,176 tests), `just docs-drift` green. Code-review gate: 2 blockers + 6 warnings + 4 info — **both blockers FIXED before phase close**, each proven non-vacuous by reverting the fix and watching the new tests go red: CR-01 (`1ff92265`) the pre-existing `retry_analysis_failed` flipped only `files.state` and left `analysis.failed_at` set, stranding every retried file as `fingerprinted`-yet-`failed` — a row the Phase 79 shadow gate reads as divergent and `domain_completed(ANALYZE)` reads as "un-processable", so Phase 80 recovery would have skipped exactly the files the operator just re-enqueued; CR-02 (`a6398d33`) `domain_completed_clause` raised a bare `KeyError` on the four downstream stages while the Python twin short-circuited on `DONE` and returned `True` — a silent twin divergence on every non-failed downstream row, now a symmetric fail-loud `ValueError` matching `reenqueue.py`'s existing "live-keys-only … no domain predicate" classification. Two warnings remain OPEN in `deferred-items.md`: **WR-02** — the `domain_completed` drift-lock excludes the `in_flight ∧ failed` seeds, a cell FAIL-03's retry now *routinely* produces (Phase 80 must add an `inflight` term to the SQL twin or gate at the call site); **WR-01** — the metadata failure upsert can mark a row that already carries real tags as failed. Ships as its own PR on a worktree branch. **Next: Phase 82 (Counts & Pending-Set Cutover).**
116118

117119
**2026.7.5 Parallel Enrich DAG — Phase 79 (Shadow-Compare Gate, live corpus) COMPLETE + verified 2026-07-08 (11/11 code-verifiable truths; 1 human item deferred per D-02).** The standing state↔derived implication gate that phases 80–90 keep green and the hard precondition for the destructive `033` (Phase 90). Shipped across 2 plans / 2 waves over the ONE shared assertion core (D-01): (1) `src/phaze/services/shadow_compare.py` — an `INVARIANTS` registry with one `state ⇒ derived-condition` implication per `FileState` value (16 entries; `DISCOVERED` documented-vacuous), `Invariant`/`InvariantResult`/`Report` dataclasses, and async `run_shadow_compare` running a per-invariant corpus-wide anti-join (`state=X AND NOT <derived>`) returning counts + capped `file_id` samples + `hard_fail_total`; reuses Phase-78 `done_clause`/`failed_clause` (D-03, never `stage_status_case`), asserts implication-not-equality, PUSHING/PUSHED loosen to `cloud_job` row-existence (RESEARCH A3/OQ1) while AWAITING_CLOUD keeps `status='awaiting'`, apply-outcome states assert `proposals.status` (not `execution_log`), and the `{fingerprinted, local_analyzing}` soft allowlist is counted-but-never-gated (D-06). (2) `src/phaze/cli/shadow_compare.py` — a thin `python -m phaze.cli.shadow_compare` argparse runner + `[group('db')] shadow-compare *ARGS` justfile recipe driving the SAME core (no duplicated logic), exit 1 iff any HARD invariant diverges (D-05), `--sample-cap`/`--verbose`/`--database-url` flags, DSN never leaked. Hermetic fixture-corpus gate `tests/integration/test_shadow_compare.py` — non-vacuous divergent RED cell + consistent GREEN cell per HARD invariant, implication cell, soft-allowlist cell, DB-free registry cell locking D-04 coverage + D-06 allowlist, and CLI-exit cells; `just test-bucket integration` green in isolation (130 passed, 100% core coverage). Verifier 11/11 (independent codebase read + its own 130/130 suite re-run). Code-review gate: 1 blocker + 2 warnings + 1 info — ALL fixed inline (`ba489b62`): CR-01 (a data-loss footgun — the CLI cells' committed `TRUNCATE agents CASCADE` would wipe the DEV DB on a bare `pytest` with `TEST_DATABASE_URL` unset; now a module-level `_test`-DB guard refuses any non-`_test` target), WR-01 (DSN parse-error redaction + password-masking `URL` threaded to the engine), WR-02 (`--sample-cap` rejects negatives), IN-01 (`ORDER BY id` for deterministic samples). One human-verification item deferred by design: the live ~200K-corpus run (SC-3) is explicitly held to the next homelab rollout per D-02, tracked as the sole Manual-Only check in `79-VALIDATION.md`/`79-HUMAN-UAT.md` and must be recorded before Phase 90's `033`. Ships as its own PR on a worktree branch. **Next: Phase 80 (Recovery / Re-enqueue Cutover) — the first reader cut over off `FileRecord.state`, deliberately before the pending-set/counts readers (READ-03).**

.planning/REQUIREMENTS.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@
5757

5858
### Operator UI (UI)
5959

60-
- [ ] **UI-01**: The file-row "State" display is derived from `stage_status` per stage (a per-file stage matrix), replacing the raw-enum-string column; the list is paginated and never renders a query that scans the whole 200K corpus per poll.
61-
- [ ] **UI-02**: The operator can see failed files per enrich stage and trigger a retry from the console (fingerprint/metadata retry + the existing manual analyze retry). *(failure visibility + retry affordance)*
62-
- [ ] **UI-03**: For any file not in a stage's pending set, the operator can see **why it is not eligible** — an eligibility trace over the pure `eligible()` conjuncts (done? in-flight? upstream unmet? terminally failed?). *(closes gap G-04; the diagnostic whose absence hid the current deadlock)*
63-
- [ ] **UI-04**: The operator can force a stage to done / skip a stage for a specific file, so the `failed` bucket for genuinely-unprocessable files can converge rather than accumulate permanently. *(gap G-03)*
64-
- [ ] **UI-05**: An orphaned/stuck-work count is surfaced (files with an in-flight marker but no progress), derived for free from the chosen `in_flight` source.
60+
- [x] **UI-01**: The file-row "State" display is derived from `stage_status` per stage (a per-file stage matrix), replacing the raw-enum-string column; the list is paginated and never renders a query that scans the whole 200K corpus per poll.
61+
- [x] **UI-02**: The operator can see failed files per enrich stage and trigger a retry from the console (fingerprint/metadata retry + the existing manual analyze retry). *(failure visibility + retry affordance)*
62+
- [x] **UI-03**: For any file not in a stage's pending set, the operator can see **why it is not eligible** — an eligibility trace over the pure `eligible()` conjuncts (done? in-flight? upstream unmet? terminally failed?). *(closes gap G-04; the diagnostic whose absence hid the current deadlock)*
63+
- [x] **UI-04**: The operator can force a stage to done / skip a stage for a specific file, so the `failed` bucket for genuinely-unprocessable files can converge rather than accumulate permanently. *(gap G-03)*
64+
- [x] **UI-05**: An orphaned/stuck-work count is surfaced (files with an in-flight marker but no progress), derived for free from the chosen `in_flight` source.
6565

6666
### Performance (PERF)
6767

@@ -80,7 +80,7 @@
8080

8181
*Folded in from the #222 post-deploy backlog. Backend is live end-to-end; only the v7.0-deleted UI control is missing.*
8282

83-
- [ ] **PRIO-01**: The operator can change a per-stage job priority from the shell (a stepper wired to the existing `POST /pipeline/stages/{stage}/priority` endpoint — ▲ raises priority / lowers the number), re-connecting the orphaned setter; pause/resume controls are surfaced too if that endpoint is likewise orphaned.
83+
- [x] **PRIO-01**: The operator can change a per-stage job priority from the shell (a stepper wired to the existing `POST /pipeline/stages/{stage}/priority` endpoint — ▲ raises priority / lowers the number), re-connecting the orphaned setter; pause/resume controls are surfaced too if that endpoint is likewise orphaned.
8484

8585
### Lane / Agent Drill-In (DRILL)
8686

@@ -149,11 +149,11 @@
149149
| SIDECAR-01 | Phase 83 | Complete |
150150
| SIDECAR-02 | Phase 84 | Complete |
151151
| SIDECAR-03 | Phase 86 | Complete |
152-
| UI-01 | Phase 87 | Pending |
153-
| UI-02 | Phase 87 | Pending |
154-
| UI-03 | Phase 87 | Pending |
155-
| UI-04 | Phase 87 | Pending |
156-
| UI-05 | Phase 87 | Pending |
152+
| UI-01 | Phase 87 | Complete |
153+
| UI-02 | Phase 87 | Complete |
154+
| UI-03 | Phase 87 | Complete |
155+
| UI-04 | Phase 87 | Complete |
156+
| UI-05 | Phase 87 | Complete |
157157
| PERF-01 | Phase 77 | Complete |
158158
| PERF-02 | Phase 82 | Complete |
159159
| MIG-01 | Phase 77 | Complete |
@@ -163,7 +163,7 @@
163163
| LEGACY-01 | Phase 89 | Pending |
164164
| LEGACY-02 | Phase 89 | Pending |
165165
| LEGACY-03 | Phase 89 | Pending |
166-
| PRIO-01 | Phase 87 | Pending |
166+
| PRIO-01 | Phase 87 | Complete |
167167
| DRILL-01 | Phase 88 | Pending |
168168
| DRILL-02 | Phase 88 | Pending |
169169
| DRILL-03 | Phase 88 | Pending |

0 commit comments

Comments
 (0)