diff --git a/backend/tests/unit/scripts/test_dashboard_expand_transitive_deps.py b/backend/tests/unit/scripts/test_dashboard_expand_transitive_deps.py new file mode 100644 index 00000000..1674a752 --- /dev/null +++ b/backend/tests/unit/scripts/test_dashboard_expand_transitive_deps.py @@ -0,0 +1,197 @@ +"""Tests for the DEPS_ALL_BACKEND time-ordered expansion in +``scripts/build_mvp1_dashboard.py``. + +Covers the fix for +[bug_dashboard_depends_on_column_bloat](../../../../docs/02_product/planned_features/bug_dashboard_depends_on_column_bloat/idea.md): +shipped features that use the "ALL prior backend features" prose marker +must inherit only backend peers that merged on or before them — not the +full current-snapshot roster (which historically included features +shipped weeks later AND still-planned ideas). +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +# scripts/build_mvp1_dashboard.py is at the repo root, not on sys.path. +_REPO_ROOT = Path(__file__).resolve().parents[4] +sys.path.insert(0, str(_REPO_ROOT)) + +from scripts.build_mvp1_dashboard import ( # noqa: E402 + DEPS_ALL_BACKEND, + Feature, + _expand_transitive_deps, + _merge_order_key, +) + + +def _feat( + folder: str, + *, + prefix: str, + merged_date: str | None, + pr_number: int | None = None, + depends_on: list[str] | None = None, +) -> Feature: + """Minimal Feature factory — only fields the expansion cares about.""" + return Feature( + folder=folder, + prefix=prefix, + short_name=folder.split("_", 1)[1] if "_" in folder else folder, + path=Path("/tmp") / folder, + location="implemented" if merged_date else "planned", + stage="done" if merged_date else "idea", + status_line="Complete" if merged_date else "Idea", + one_liner="test fixture", + depends_on=list(depends_on) if depends_on else [], + pr_number=pr_number, + merged_date=merged_date, + ) + + +class TestExpandTransitiveDeps: + def test_shipped_feature_only_inherits_earlier_backend_peers(self) -> None: + """The canonical bloat case: feat_chat_agent shipped 2026-05-12 and + must NOT inherit features that shipped after it (or planned ideas). + """ + before = _feat("infra_foundation", prefix="infra", merged_date="2026-05-09", pr_number=4) + same_day_earlier_pr = _feat( + "feat_studies_ui", prefix="feat", merged_date="2026-05-12", pr_number=50 + ) + chat_agent = _feat( + "feat_chat_agent", + prefix="feat", + merged_date="2026-05-12", + pr_number=60, + depends_on=[DEPS_ALL_BACKEND], + ) + after = _feat( + "feat_pr_metric_confidence", prefix="feat", merged_date="2026-05-21", pr_number=180 + ) + planned = _feat("feat_ubi_judgments", prefix="feat", merged_date=None) + + _expand_transitive_deps([before, same_day_earlier_pr, chat_agent, after, planned]) + + assert chat_agent.depends_on == ["feat_studies_ui", "infra_foundation"] + + def test_planned_feature_inherits_full_backend_snapshot(self) -> None: + """Planned features with the transitive marker keep current behavior — + they genuinely depend on every backend sibling in the queue. + """ + before = _feat("infra_foundation", prefix="infra", merged_date="2026-05-09", pr_number=4) + shipped = _feat("feat_studies_ui", prefix="feat", merged_date="2026-05-12", pr_number=50) + future = _feat( + "feat_pr_metric_confidence", prefix="feat", merged_date="2026-05-21", pr_number=180 + ) + planned_target = _feat( + "feat_some_planned_thing", + prefix="feat", + merged_date=None, + depends_on=[DEPS_ALL_BACKEND], + ) + + _expand_transitive_deps([before, shipped, future, planned_target]) + + assert planned_target.depends_on == [ + "feat_pr_metric_confidence", + "feat_studies_ui", + "infra_foundation", + ] + + def test_explicit_deps_alongside_sentinel_are_preserved(self) -> None: + """If a feature declares both explicit deps and the transitive marker, + the explicit ones are unioned with the time-scoped expansion. + """ + a = _feat("infra_foundation", prefix="infra", merged_date="2026-05-09", pr_number=4) + b = _feat("feat_extra", prefix="feat", merged_date="2026-05-10", pr_number=10) + target = _feat( + "feat_target", + prefix="feat", + merged_date="2026-05-12", + pr_number=20, + depends_on=["infra_external_dep", DEPS_ALL_BACKEND], + ) + + _expand_transitive_deps([a, b, target]) + + assert target.depends_on == ["feat_extra", "infra_external_dep", "infra_foundation"] + + def test_feature_without_sentinel_is_unchanged(self) -> None: + """No DEPS_ALL_BACKEND → no expansion path runs; depends_on stays as-is.""" + a = _feat("infra_foundation", prefix="infra", merged_date="2026-05-09", pr_number=4) + target = _feat( + "feat_target", + prefix="feat", + merged_date="2026-05-12", + pr_number=20, + depends_on=["infra_foundation"], + ) + + _expand_transitive_deps([a, target]) + + assert target.depends_on == ["infra_foundation"] + + def test_non_backend_prefixes_excluded_from_expansion(self) -> None: + """The expansion only includes infra_*/feat_* peers — chore_/bug_/epic_ + are not part of the backend dependency surface. + """ + infra = _feat("infra_foundation", prefix="infra", merged_date="2026-05-09", pr_number=4) + chore = _feat( + "chore_tutorial_polish", prefix="chore", merged_date="2026-05-12", pr_number=64 + ) + bug = _feat("bug_some_fix", prefix="bug", merged_date="2026-05-11", pr_number=55) + target = _feat( + "feat_chat_agent", + prefix="feat", + merged_date="2026-05-12", + pr_number=60, + depends_on=[DEPS_ALL_BACKEND], + ) + + _expand_transitive_deps([infra, chore, bug, target]) + + # Only infra_foundation is included — bug_ and chore_ are excluded + # by prefix even though they shipped first. + assert target.depends_on == ["infra_foundation"] + + def test_self_dep_dropped_from_both_explicit_and_expansion(self) -> None: + """``f.folder`` is removed from the final union — whether it slipped + in via the explicit ``depends_on`` list or via the sentinel + expansion. (Gemini PR #208 cycle-1 finding hardened the guard.) + """ + target = _feat( + "feat_self", + prefix="feat", + merged_date="2026-05-12", + pr_number=20, + depends_on=["feat_self", DEPS_ALL_BACKEND], + ) + + _expand_transitive_deps([target]) + + # Both the explicit "feat_self" AND any self-reference in the + # expansion are removed. + assert target.depends_on == [] + + +class TestMergeOrderKey: + def test_earlier_date_sorts_first(self) -> None: + a = _feat("a", prefix="feat", merged_date="2026-05-09", pr_number=4) + b = _feat("b", prefix="feat", merged_date="2026-05-12", pr_number=60) + assert _merge_order_key(a) < _merge_order_key(b) + + def test_same_date_lower_pr_sorts_first(self) -> None: + a = _feat("a", prefix="feat", merged_date="2026-05-12", pr_number=50) + b = _feat("b", prefix="feat", merged_date="2026-05-12", pr_number=60) + assert _merge_order_key(a) < _merge_order_key(b) + + def test_missing_merge_date_sorts_after_shipped(self) -> None: + shipped = _feat("a", prefix="feat", merged_date="2026-05-12", pr_number=60) + planned = _feat("b", prefix="feat", merged_date=None) + assert _merge_order_key(shipped) < _merge_order_key(planned) + + def test_missing_pr_sorts_after_shipped_same_day(self) -> None: + with_pr = _feat("a", prefix="feat", merged_date="2026-05-12", pr_number=60) + no_pr = _feat("b", prefix="feat", merged_date="2026-05-12", pr_number=None) + assert _merge_order_key(with_pr) < _merge_order_key(no_pr) diff --git a/docs/00_overview/DASHBOARD.md b/docs/00_overview/DASHBOARD.md index dd6e8245..4029e58b 100644 --- a/docs/00_overview/DASHBOARD.md +++ b/docs/00_overview/DASHBOARD.md @@ -6,7 +6,7 @@ _Top-level index across MVP1 → GA v1+ as of **2026-05-23**. Click a release na | Release | Theme | Progress | Status | |---|---|---|---| -| [MVP1 / v0.1](MVP1_DASHBOARD.md) | The Loop | 67 / 67 scoped done · 7 remaining | **In progress** | +| [MVP1 / v0.1](MVP1_DASHBOARD.md) | The Loop | 67 / 67 scoped done · 8 remaining | **In progress** | | [MVP2 / v0.2](MVP2_DASHBOARD.md) | Observable | 1 / 1 scoped done · 1 remaining | **In progress** | | MVP3 / v0.3 | Production Stacks | — | **Not yet scoped** | | MVP4 / v0.4 | Multi-tenant, Multi-LLM | — | **Not yet scoped** | diff --git a/docs/00_overview/MVP1_DASHBOARD.md b/docs/00_overview/MVP1_DASHBOARD.md index 26c6ee38..1a322491 100644 --- a/docs/00_overview/MVP1_DASHBOARD.md +++ b/docs/00_overview/MVP1_DASHBOARD.md @@ -15,13 +15,13 @@ Pull from the Idea backlog or capture a new feature spec. | Metric | Value | |---|---| | Scoped items done | **67 / 67** (100%) — feat_/infra_/chore_/epic_ past idea stage | -| Pending work | **14** items (every not-done feat/infra/chore/bug across all priorities) | +| Pending work | **15** items (every not-done feat/infra/chore/bug across all priorities) | | → P0 — do next | **0** unblocking / paying daily cost | | → P1 | **1** high-value, ready when P0 clears | -| → P2 (default) | 12 important to file, not blocking | +| → P2 (default) | 13 important to file, not blocking | | → Backlog | 1 captured for record, not planned | | Open bugs | 2 | -| Legacy "Path to MVP1" | 7 items — scoped-not-done + bugs + chore-ideas only (excludes feat/infra ideas) | +| Legacy "Path to MVP1" | 8 items — scoped-not-done + bugs + chore-ideas only (excludes feat/infra ideas) | | Backlog ideas | 7 idea-only feat/infra (not yet scoped into MVP1) | | In flight | 0 feature(s) actively shipping | @@ -32,7 +32,7 @@ Pull from the Idea backlog or capture a new feature spec. | Feature | Type | One-liner | Depends on | Status | |---|---|---|---|---| | [feat_agent_propose_search_space](implemented_features/2026_05_21_feat_agent_propose_search_space/feature_spec.md) | Feature | A new read-only agent tool `propose_search_space(template_id, cluster_id, judgment_list_id?, prior_study_id?) → SearchSpace JSON` that emits a deterministic, code-generated search space using the same | — | [PR #175](https://github.com/SoundMindsAI/relyloop/pull/175) merged 2026-05-21 | -| [feat_chat_agent](implemented_features/2026_05_12_feat_chat_agent/feature_spec.md) | Feature | A chat surface at `/chat/{conversation_id}` streams OpenAI completions via SSE. | `feat_agent_propose_search_space` `feat_auto_followup_studies` `feat_chat_last_message_preview` `feat_cluster_target_filter` `feat_config_repo_baseline_tracking` `feat_contextual_help` `feat_contextual_help_mvp2` `feat_create_study_search_space_builder` `feat_create_study_target_autocomplete` `feat_data_table_primitive` `feat_digest_executable_followups` `feat_digest_proposal` `feat_fts_rank_ordering_mvp2` `feat_github_pr_worker` `feat_github_webhook` `feat_home_demo_reseed_endpoint` `feat_home_first_run_demo_nudge` `feat_judgments_periodic_resume_sweep` `feat_llm_judgments` `feat_orchestrator_zero_streak_abort` `feat_pr_metric_confidence` `feat_proposals_ui` `feat_query_inline_crud` `feat_studies_ui` `feat_study_baseline_trial` `feat_study_clone_from_previous` `feat_study_lifecycle` `feat_study_preflight_overlap_probe` `feat_study_target_judgment_mismatch_guard` `feat_ubi_judgments` `infra_adapter_elastic` `infra_arq_subprocess_test_mvp2` `infra_ci_smoke_makeup` `infra_dashboard_regen_pre_commit_conflict` `infra_e2e_seed_completed_study` `infra_e2e_wire_seed_helper_into_studies_spec` `infra_foundation` `infra_frontend_stack_refresh` `infra_ir_measures_migration` `infra_make_targets_split_backend_only` `infra_nvmrc` `infra_optuna_eval` `infra_per_trial_timeout` `infra_structlog_test_helpers` `infra_study_preflight_real_engine_integration` `infra_uv_sync_drops_precommit` | [PR #60](https://github.com/SoundMindsAI/relyloop/pull/60) merged 2026-05-12 | +| [feat_chat_agent](implemented_features/2026_05_12_feat_chat_agent/feature_spec.md) | Feature | A chat surface at `/chat/{conversation_id}` streams OpenAI completions via SSE. | `feat_digest_proposal` `feat_github_pr_worker` `feat_github_webhook` `feat_llm_judgments` `feat_proposals_ui` `feat_studies_ui` `feat_study_lifecycle` `infra_adapter_elastic` `infra_foundation` `infra_optuna_eval` | [PR #60](https://github.com/SoundMindsAI/relyloop/pull/60) merged 2026-05-12 | | [feat_cluster_target_filter](implemented_features/2026_05_20_feat_cluster_target_filter/feature_spec.md) | Feature | Each registered cluster can optionally carry a glob pattern (`products*`, `team-a-*`, `docs-[ef][nr]-*`) that scopes `list_targets()` to the matching subset. | — | [PR #168](https://github.com/SoundMindsAI/relyloop/pull/168) merged 2026-05-20 | | [feat_config_repo_baseline_tracking](implemented_features/2026_05_23_feat_config_repo_baseline_tracking/feature_spec.md) | Feature | A single denormalized FK `config_repos.last_merged_proposal_id` points at the most recently merged proposal for each config repo. | — | [PR #202](https://github.com/SoundMindsAI/relyloop/pull/202) merged 2026-05-23 | | [feat_contextual_help](implemented_features/2026_05_15_feat_contextual_help/feature_spec.md) | Feature | a relevance engineer can launch their second study and interpret its digest without re-reading the tutorial, because every domain-jargon label has a one-click contextual definition grounded in the sam | — | [PR #122](https://github.com/SoundMindsAI/relyloop/pull/122) merged 2026-05-15 | @@ -97,7 +97,7 @@ Pull from the Idea backlog or capture a new feature spec. | [chore_starlette_422_deprecation](implemented_features/2026_05_13_chore_starlette_422_deprecation/idea.md) | Chore | Complete | — | Complete | | [chore_test_both_engines](implemented_features/2026_05_13_chore_test_both_engines/idea.md) | Chore | Complete | — | Complete | | [chore_trial_summary_single_query](implemented_features/2026_05_13_chore_trial_summary_single_query/idea.md) | Chore | Complete | — | Complete | -| [chore_tutorial_polish](implemented_features/2026_05_12_chore_tutorial_polish/feature_spec.md) | Chore | The release tag `v0.1.0` is pushed with: a worked tutorial at `docs/08_guides/tutorial-first-study.md`, sample data (50-query set + sample ES index of ~1,000 docs from the Amazon ESCI subset), README | `feat_agent_propose_search_space` `feat_auto_followup_studies` `feat_chat_agent` `feat_chat_last_message_preview` `feat_cluster_target_filter` `feat_config_repo_baseline_tracking` `feat_contextual_help` `feat_contextual_help_mvp2` `feat_create_study_search_space_builder` `feat_create_study_target_autocomplete` `feat_data_table_primitive` `feat_digest_executable_followups` `feat_digest_proposal` `feat_fts_rank_ordering_mvp2` `feat_github_pr_worker` `feat_github_webhook` `feat_home_demo_reseed_endpoint` `feat_home_first_run_demo_nudge` `feat_judgments_periodic_resume_sweep` `feat_llm_judgments` `feat_orchestrator_zero_streak_abort` `feat_pr_metric_confidence` `feat_proposals_ui` `feat_query_inline_crud` `feat_studies_ui` `feat_study_baseline_trial` `feat_study_clone_from_previous` `feat_study_lifecycle` `feat_study_preflight_overlap_probe` `feat_study_target_judgment_mismatch_guard` `feat_ubi_judgments` `infra_adapter_elastic` `infra_arq_subprocess_test_mvp2` `infra_ci_smoke_makeup` `infra_dashboard_regen_pre_commit_conflict` `infra_e2e_seed_completed_study` `infra_e2e_wire_seed_helper_into_studies_spec` `infra_foundation` `infra_frontend_stack_refresh` `infra_ir_measures_migration` `infra_make_targets_split_backend_only` `infra_nvmrc` `infra_optuna_eval` `infra_per_trial_timeout` `infra_structlog_test_helpers` `infra_study_preflight_real_engine_integration` `infra_uv_sync_drops_precommit` | [PR #64](https://github.com/SoundMindsAI/relyloop/pull/64) merged 2026-05-12 | +| [chore_tutorial_polish](implemented_features/2026_05_12_chore_tutorial_polish/feature_spec.md) | Chore | The release tag `v0.1.0` is pushed with: a worked tutorial at `docs/08_guides/tutorial-first-study.md`, sample data (50-query set + sample ES index of ~1,000 docs from the Amazon ESCI subset), README | `feat_chat_agent` `feat_digest_proposal` `feat_github_pr_worker` `feat_github_webhook` `feat_llm_judgments` `feat_proposals_ui` `feat_studies_ui` `feat_study_lifecycle` `infra_adapter_elastic` `infra_foundation` `infra_optuna_eval` | [PR #64](https://github.com/SoundMindsAI/relyloop/pull/64) merged 2026-05-12 | | [bug_capability_check_test_isolation](implemented_features/2026_05_12_bug_capability_check_test_isolation/idea.md) | Bug | Complete | — | Complete | | [bug_contract_test_stub_missing_target_filter_kwarg](implemented_features/2026_05_23_bug_contract_test_stub_missing_target_filter_kwarg/idea.md) | Bug | Complete | — | Complete | | [bug_cursor_decode_value_validation](implemented_features/2026_05_17_bug_cursor_decode_value_validation/idea.md) | Bug | Complete | — | Complete | @@ -125,7 +125,7 @@ _None._ _None._ -### Idea (14) +### Idea (15) | Priority | Feature | Type | One-liner | Depends on | Status | |---|---|---|---|---|---| @@ -136,12 +136,13 @@ _None._ | P2 | [feat_study_baseline_trial](../02_product/planned_features/feat_study_baseline_trial/idea.md) | Feature | `studies.baseline_metric` exists as a column on the `studies` table (declared in `feat_study_lifecycle` Phase 1, [`backend/app/db/models/study.py:76`](../../backend/app/db/models/study.py#L76)) with t | — | Idea — deferred Phase 2 work from `feat_pr_metric_confidence` (Phase 1 merged 2026-05-21 as PR #180 squash `d0a8358`). | | P2 | [feat_study_clone_from_previous](../02_product/planned_features/feat_study_clone_from_previous/idea.md) | Feature | A relevance engineer's normal workflow after the first study completes: | — | Idea — surfaced during a UX review of parameter-tuning ergonomics on 2026-05-19. | | P2 | [infra_study_preflight_real_engine_integration](../02_product/planned_features/infra_study_preflight_real_engine_integration/idea.md) | Infra | `feat_study_preflight_overlap_probe`'s integration tests (AC-1 through AC-4b in [`backend/tests/integration/test_studies_api.py`](../../backend/tests/integration/test_studies_api.py)) use… | — | Idea — surfaced during `feat_study_preflight_overlap_probe` (PR ___) phase-gate review | +| P2 | [chore_dashboard_pr_extraction_from_idea](../02_product/planned_features/chore_dashboard_pr_extraction_from_idea/idea.md) | Chore | Several early MVP1 features shipped before the `/pipeline` ceremony solidified, leaving them with only an `idea.md` in `implemented_features/_/`. Examples (as of 2026-05-23): | — | Idea — surfaced during the tangential-observations sweep of `bug_dashboard_depends_on_column_bloat` (PR pending) | | P2 | [chore_reconciler_terminal_closed_no_poll](../02_product/planned_features/chore_reconciler_terminal_closed_no_poll/idea.md) | Chore | After `bug_pr_reconciler_blocked_by_closed_fallback` ships, the reconciler's candidate query at [`backend/app/db/repo/proposal.py:455-475`](../../backend/app/db/repo/proposal.py#L455-L475) returns BOT | — | Idea — surfaced during the ad-hoc tangential-observations sweep of `bug_pr_reconciler_blocked_by_closed_fallback` (PR pending) | | P2 | [chore_studies_post_arq_spy_fixture](../02_product/planned_features/chore_studies_post_arq_spy_fixture/idea.md) | Chore | The studies POST handler at [`backend/app/api/v1/studies.py:307`](../../backend/app/api/v1/studies.py#L307) calls `await _enqueue_start_study(request, study_id)` after a successful create. The helper | — | Idea — surfaced during `feat_study_preflight_overlap_probe` (PR ___) phase-gate review | | P2 | [chore_study_default_stop_conditions](../02_product/planned_features/chore_study_default_stop_conditions/idea.md) | Chore | The server-side `StudyConfigSpec` validator at [`backend/app/api/v1/schemas.py:572-580`](../../backend/app/api/v1/schemas.py) correctly **requires** at least one of `max_trials` or `time_budget_min` — | — | Idea — surfaced during the 2026-05-21 Karpathy-loop audit; recommendation grounded in measured per-trial cost from the local dev DB. | | P2 | [chore_template_library_expansion](../02_product/planned_features/chore_template_library_expansion/idea.md) | Chore | Three connected gaps: | — | Idea — surfaced during a UX review of parameter-tuning ergonomics on 2026-05-19. | | P2 | [bug_dashboard_banner_dismiss_persistence_flake](../02_product/planned_features/bug_dashboard_banner_dismiss_persistence_flake/idea.md) | Bug | The test, introduced by [PR #188 (`feat_home_first_run_demo_nudge`)](implemented_features/2026_05_22_feat_home_first_run_demo_nudge), is: | — | Idea — surfaced during `feat_study_preflight_overlap_probe` (PR #193) smoke CI | -| P2 | [bug_dashboard_depends_on_column_bloat](../02_product/planned_features/bug_dashboard_depends_on_column_bloat/idea.md) | Bug | [`scripts/build_mvp1_dashboard.py`](../../scripts/build_mvp1_dashboard.py) (2,084 lines) generates the "Depends on" column for each planned-feature row in [`MVP1_DASHBOARD.md`](MVP1_DASHBOARD.md) and | — | Idea — surfaced by Gemini Code Assist review on PR #200 (2026-05-22). Pre-existing bug; this PR only made one more entry visible. | +| P2 | [bug_dashboard_depends_on_column_bloat](../02_product/planned_features/bug_dashboard_depends_on_column_bloat/idea.md) | Bug | [`scripts/build_mvp1_dashboard.py`](../../scripts/build_mvp1_dashboard.py) (2,084 lines) generates the "Depends on" column for each row in [`MVP1_DASHBOARD.md`](MVP1_DASHBOARD.md) and `mvp1_dashboard. | — | Idea — surfaced by Gemini Code Assist review on PR #200 (2026-05-22). Pre-existing bug; this PR only made one more entry visible. | | Backlog | [chore_e2e_seed_acme_helper_dead](../02_product/planned_features/chore_e2e_seed_acme_helper_dead/idea.md) | Chore | `seedAcmeProductsChain` is a 140-line helper that constructs a cluster + query_set + template + judgment_list + study + optional proposal/digest chain "Acme Products" demo scenario. The function is co | — | Idea — surfaced during `chore_e2e_test_rows_isolation` Story 1.2 coverage audit | ## Dependency graph @@ -294,77 +295,27 @@ graph LR infra_foundation --> feat_llm_judgments infra_adapter_elastic --> feat_llm_judgments feat_study_lifecycle --> feat_llm_judgments - feat_agent_propose_search_space --> chore_tutorial_polish feat_chat_agent --> chore_tutorial_polish - feat_cluster_target_filter --> chore_tutorial_polish - feat_config_repo_baseline_tracking --> chore_tutorial_polish - feat_contextual_help --> chore_tutorial_polish - feat_create_study_search_space_builder --> chore_tutorial_polish - feat_create_study_target_autocomplete --> chore_tutorial_polish - feat_data_table_primitive --> chore_tutorial_polish feat_digest_proposal --> chore_tutorial_polish feat_github_pr_worker --> chore_tutorial_polish feat_github_webhook --> chore_tutorial_polish - feat_home_first_run_demo_nudge --> chore_tutorial_polish - feat_judgments_periodic_resume_sweep --> chore_tutorial_polish feat_llm_judgments --> chore_tutorial_polish - feat_orchestrator_zero_streak_abort --> chore_tutorial_polish - feat_pr_metric_confidence --> chore_tutorial_polish feat_proposals_ui --> chore_tutorial_polish - feat_query_inline_crud --> chore_tutorial_polish feat_studies_ui --> chore_tutorial_polish feat_study_lifecycle --> chore_tutorial_polish - feat_study_preflight_overlap_probe --> chore_tutorial_polish - feat_study_target_judgment_mismatch_guard --> chore_tutorial_polish infra_adapter_elastic --> chore_tutorial_polish - infra_ci_smoke_makeup --> chore_tutorial_polish - infra_dashboard_regen_pre_commit_conflict --> chore_tutorial_polish - infra_e2e_seed_completed_study --> chore_tutorial_polish - infra_e2e_wire_seed_helper_into_studies_spec --> chore_tutorial_polish infra_foundation --> chore_tutorial_polish - infra_frontend_stack_refresh --> chore_tutorial_polish - infra_ir_measures_migration --> chore_tutorial_polish - infra_make_targets_split_backend_only --> chore_tutorial_polish - infra_nvmrc --> chore_tutorial_polish infra_optuna_eval --> chore_tutorial_polish - infra_per_trial_timeout --> chore_tutorial_polish - infra_structlog_test_helpers --> chore_tutorial_polish - infra_uv_sync_drops_precommit --> chore_tutorial_polish - feat_agent_propose_search_space --> feat_chat_agent - feat_cluster_target_filter --> feat_chat_agent - feat_config_repo_baseline_tracking --> feat_chat_agent - feat_contextual_help --> feat_chat_agent - feat_create_study_search_space_builder --> feat_chat_agent - feat_create_study_target_autocomplete --> feat_chat_agent - feat_data_table_primitive --> feat_chat_agent feat_digest_proposal --> feat_chat_agent feat_github_pr_worker --> feat_chat_agent feat_github_webhook --> feat_chat_agent - feat_home_first_run_demo_nudge --> feat_chat_agent - feat_judgments_periodic_resume_sweep --> feat_chat_agent feat_llm_judgments --> feat_chat_agent - feat_orchestrator_zero_streak_abort --> feat_chat_agent - feat_pr_metric_confidence --> feat_chat_agent feat_proposals_ui --> feat_chat_agent - feat_query_inline_crud --> feat_chat_agent feat_studies_ui --> feat_chat_agent feat_study_lifecycle --> feat_chat_agent - feat_study_preflight_overlap_probe --> feat_chat_agent - feat_study_target_judgment_mismatch_guard --> feat_chat_agent infra_adapter_elastic --> feat_chat_agent - infra_ci_smoke_makeup --> feat_chat_agent - infra_dashboard_regen_pre_commit_conflict --> feat_chat_agent - infra_e2e_seed_completed_study --> feat_chat_agent - infra_e2e_wire_seed_helper_into_studies_spec --> feat_chat_agent infra_foundation --> feat_chat_agent - infra_frontend_stack_refresh --> feat_chat_agent - infra_ir_measures_migration --> feat_chat_agent - infra_make_targets_split_backend_only --> feat_chat_agent - infra_nvmrc --> feat_chat_agent infra_optuna_eval --> feat_chat_agent - infra_per_trial_timeout --> feat_chat_agent - infra_structlog_test_helpers --> feat_chat_agent - infra_uv_sync_drops_precommit --> feat_chat_agent infra_foundation --> feat_github_pr_worker infra_adapter_elastic --> feat_github_pr_worker feat_study_lifecycle --> feat_github_pr_worker diff --git a/docs/00_overview/dashboard.html b/docs/00_overview/dashboard.html index e2177599..d75bc662 100644 --- a/docs/00_overview/dashboard.html +++ b/docs/00_overview/dashboard.html @@ -384,7 +384,7 @@

Releases

The Loop
-
67 / 67 scoped done · 7 remaining
+
67 / 67 scoped done · 8 remaining
In progress
diff --git a/docs/00_overview/mvp1_dashboard.html b/docs/00_overview/mvp1_dashboard.html index 91f58ef4..fd321360 100644 --- a/docs/00_overview/mvp1_dashboard.html +++ b/docs/00_overview/mvp1_dashboard.html @@ -403,7 +403,7 @@

MVP1 Progress

Pending work
-
14
+
15
every not-done feat/infra/chore/bug across all priorities
@@ -425,7 +425,7 @@

MVP1 Progress

P2 (default)
-
12
+
13
important to file, not blocking
@@ -435,7 +435,7 @@

MVP1 Progress

Legacy "Path to MVP1"
-
7
+
8
scoped not-done + bugs + chore-ideas only (excludes feat/infra ideas)
@@ -463,7 +463,7 @@

Pipeline

-

Idea 14

+

Idea 15

@@ -556,6 +556,19 @@

Idea 14

+
+ +
+ Chore + P2 + +
+
Several early MVP1 features shipped before the `/pipeline` ceremony solidified, leaving them with only an `idea.md` in `implemented_features/<date>_<slug>/`. Examples (as of 2026-05-23):
+ + +
+ +
@@ -628,7 +641,7 @@

Idea 14

P2
-
[`scripts/build_mvp1_dashboard.py`](../../scripts/build_mvp1_dashboard.py) (2,084 lines) generates the "Depends on" column for each planned-feature row in [`MVP1_DASHBOARD.md`](MVP1_DASHBOARD.md) and
+
[`scripts/build_mvp1_dashboard.py`](../../scripts/build_mvp1_dashboard.py) (2,084 lines) generates the "Depends on" column for each row in [`MVP1_DASHBOARD.md`](MVP1_DASHBOARD.md) and `mvp1_dashboard.
@@ -688,7 +701,7 @@

Done 81

A chat surface at `/chat/{conversation_id}` streams OpenAI completions via SSE.
-
depends on: feat_agent_propose_search_spacefeat_auto_followup_studiesfeat_chat_last_message_previewfeat_cluster_target_filterfeat_config_repo_baseline_trackingfeat_contextual_helpfeat_contextual_help_mvp2feat_create_study_search_space_builderfeat_create_study_target_autocompletefeat_data_table_primitivefeat_digest_executable_followupsfeat_digest_proposalfeat_fts_rank_ordering_mvp2feat_github_pr_workerfeat_github_webhookfeat_home_demo_reseed_endpointfeat_home_first_run_demo_nudgefeat_judgments_periodic_resume_sweepfeat_llm_judgmentsfeat_orchestrator_zero_streak_abortfeat_pr_metric_confidencefeat_proposals_uifeat_query_inline_crudfeat_studies_uifeat_study_baseline_trialfeat_study_clone_from_previousfeat_study_lifecyclefeat_study_preflight_overlap_probefeat_study_target_judgment_mismatch_guardfeat_ubi_judgmentsinfra_adapter_elasticinfra_arq_subprocess_test_mvp2infra_ci_smoke_makeupinfra_dashboard_regen_pre_commit_conflictinfra_e2e_seed_completed_studyinfra_e2e_wire_seed_helper_into_studies_specinfra_foundationinfra_frontend_stack_refreshinfra_ir_measures_migrationinfra_make_targets_split_backend_onlyinfra_nvmrcinfra_optuna_evalinfra_per_trial_timeoutinfra_structlog_test_helpersinfra_study_preflight_real_engine_integrationinfra_uv_sync_drops_precommit
+
depends on: feat_digest_proposalfeat_github_pr_workerfeat_github_webhookfeat_llm_judgmentsfeat_proposals_uifeat_studies_uifeat_study_lifecycleinfra_adapter_elasticinfra_foundationinfra_optuna_eval
@@ -1533,7 +1546,7 @@

Done 81

The release tag `v0.1.0` is pushed with: a worked tutorial at `docs/08_guides/tutorial-first-study.md`, sample data (50-query set + sample ES index of ~1,000 docs from the Amazon ESCI subset), README
-
depends on: feat_agent_propose_search_spacefeat_auto_followup_studiesfeat_chat_agentfeat_chat_last_message_previewfeat_cluster_target_filterfeat_config_repo_baseline_trackingfeat_contextual_helpfeat_contextual_help_mvp2feat_create_study_search_space_builderfeat_create_study_target_autocompletefeat_data_table_primitivefeat_digest_executable_followupsfeat_digest_proposalfeat_fts_rank_ordering_mvp2feat_github_pr_workerfeat_github_webhookfeat_home_demo_reseed_endpointfeat_home_first_run_demo_nudgefeat_judgments_periodic_resume_sweepfeat_llm_judgmentsfeat_orchestrator_zero_streak_abortfeat_pr_metric_confidencefeat_proposals_uifeat_query_inline_crudfeat_studies_uifeat_study_baseline_trialfeat_study_clone_from_previousfeat_study_lifecyclefeat_study_preflight_overlap_probefeat_study_target_judgment_mismatch_guardfeat_ubi_judgmentsinfra_adapter_elasticinfra_arq_subprocess_test_mvp2infra_ci_smoke_makeupinfra_dashboard_regen_pre_commit_conflictinfra_e2e_seed_completed_studyinfra_e2e_wire_seed_helper_into_studies_specinfra_foundationinfra_frontend_stack_refreshinfra_ir_measures_migrationinfra_make_targets_split_backend_onlyinfra_nvmrcinfra_optuna_evalinfra_per_trial_timeoutinfra_structlog_test_helpersinfra_study_preflight_real_engine_integrationinfra_uv_sync_drops_precommit
+
depends on: feat_chat_agentfeat_digest_proposalfeat_github_pr_workerfeat_github_webhookfeat_llm_judgmentsfeat_proposals_uifeat_studies_uifeat_study_lifecycleinfra_adapter_elasticinfra_foundationinfra_optuna_eval
@@ -1870,77 +1883,27 @@

Dependency graph (feat_ + infra_)

infra_foundation --> feat_llm_judgments infra_adapter_elastic --> feat_llm_judgments feat_study_lifecycle --> feat_llm_judgments - feat_agent_propose_search_space --> chore_tutorial_polish feat_chat_agent --> chore_tutorial_polish - feat_cluster_target_filter --> chore_tutorial_polish - feat_config_repo_baseline_tracking --> chore_tutorial_polish - feat_contextual_help --> chore_tutorial_polish - feat_create_study_search_space_builder --> chore_tutorial_polish - feat_create_study_target_autocomplete --> chore_tutorial_polish - feat_data_table_primitive --> chore_tutorial_polish feat_digest_proposal --> chore_tutorial_polish feat_github_pr_worker --> chore_tutorial_polish feat_github_webhook --> chore_tutorial_polish - feat_home_first_run_demo_nudge --> chore_tutorial_polish - feat_judgments_periodic_resume_sweep --> chore_tutorial_polish feat_llm_judgments --> chore_tutorial_polish - feat_orchestrator_zero_streak_abort --> chore_tutorial_polish - feat_pr_metric_confidence --> chore_tutorial_polish feat_proposals_ui --> chore_tutorial_polish - feat_query_inline_crud --> chore_tutorial_polish feat_studies_ui --> chore_tutorial_polish feat_study_lifecycle --> chore_tutorial_polish - feat_study_preflight_overlap_probe --> chore_tutorial_polish - feat_study_target_judgment_mismatch_guard --> chore_tutorial_polish infra_adapter_elastic --> chore_tutorial_polish - infra_ci_smoke_makeup --> chore_tutorial_polish - infra_dashboard_regen_pre_commit_conflict --> chore_tutorial_polish - infra_e2e_seed_completed_study --> chore_tutorial_polish - infra_e2e_wire_seed_helper_into_studies_spec --> chore_tutorial_polish infra_foundation --> chore_tutorial_polish - infra_frontend_stack_refresh --> chore_tutorial_polish - infra_ir_measures_migration --> chore_tutorial_polish - infra_make_targets_split_backend_only --> chore_tutorial_polish - infra_nvmrc --> chore_tutorial_polish infra_optuna_eval --> chore_tutorial_polish - infra_per_trial_timeout --> chore_tutorial_polish - infra_structlog_test_helpers --> chore_tutorial_polish - infra_uv_sync_drops_precommit --> chore_tutorial_polish - feat_agent_propose_search_space --> feat_chat_agent - feat_cluster_target_filter --> feat_chat_agent - feat_config_repo_baseline_tracking --> feat_chat_agent - feat_contextual_help --> feat_chat_agent - feat_create_study_search_space_builder --> feat_chat_agent - feat_create_study_target_autocomplete --> feat_chat_agent - feat_data_table_primitive --> feat_chat_agent feat_digest_proposal --> feat_chat_agent feat_github_pr_worker --> feat_chat_agent feat_github_webhook --> feat_chat_agent - feat_home_first_run_demo_nudge --> feat_chat_agent - feat_judgments_periodic_resume_sweep --> feat_chat_agent feat_llm_judgments --> feat_chat_agent - feat_orchestrator_zero_streak_abort --> feat_chat_agent - feat_pr_metric_confidence --> feat_chat_agent feat_proposals_ui --> feat_chat_agent - feat_query_inline_crud --> feat_chat_agent feat_studies_ui --> feat_chat_agent feat_study_lifecycle --> feat_chat_agent - feat_study_preflight_overlap_probe --> feat_chat_agent - feat_study_target_judgment_mismatch_guard --> feat_chat_agent infra_adapter_elastic --> feat_chat_agent - infra_ci_smoke_makeup --> feat_chat_agent - infra_dashboard_regen_pre_commit_conflict --> feat_chat_agent - infra_e2e_seed_completed_study --> feat_chat_agent - infra_e2e_wire_seed_helper_into_studies_spec --> feat_chat_agent infra_foundation --> feat_chat_agent - infra_frontend_stack_refresh --> feat_chat_agent - infra_ir_measures_migration --> feat_chat_agent - infra_make_targets_split_backend_only --> feat_chat_agent - infra_nvmrc --> feat_chat_agent infra_optuna_eval --> feat_chat_agent - infra_per_trial_timeout --> feat_chat_agent - infra_structlog_test_helpers --> feat_chat_agent - infra_uv_sync_drops_precommit --> feat_chat_agent infra_foundation --> feat_github_pr_worker infra_adapter_elastic --> feat_github_pr_worker feat_study_lifecycle --> feat_github_pr_worker @@ -2107,77 +2070,27 @@

Dependency graph (feat_ + infra_)

infra_foundation --> feat_llm_judgments infra_adapter_elastic --> feat_llm_judgments feat_study_lifecycle --> feat_llm_judgments - feat_agent_propose_search_space --> chore_tutorial_polish feat_chat_agent --> chore_tutorial_polish - feat_cluster_target_filter --> chore_tutorial_polish - feat_config_repo_baseline_tracking --> chore_tutorial_polish - feat_contextual_help --> chore_tutorial_polish - feat_create_study_search_space_builder --> chore_tutorial_polish - feat_create_study_target_autocomplete --> chore_tutorial_polish - feat_data_table_primitive --> chore_tutorial_polish feat_digest_proposal --> chore_tutorial_polish feat_github_pr_worker --> chore_tutorial_polish feat_github_webhook --> chore_tutorial_polish - feat_home_first_run_demo_nudge --> chore_tutorial_polish - feat_judgments_periodic_resume_sweep --> chore_tutorial_polish feat_llm_judgments --> chore_tutorial_polish - feat_orchestrator_zero_streak_abort --> chore_tutorial_polish - feat_pr_metric_confidence --> chore_tutorial_polish feat_proposals_ui --> chore_tutorial_polish - feat_query_inline_crud --> chore_tutorial_polish feat_studies_ui --> chore_tutorial_polish feat_study_lifecycle --> chore_tutorial_polish - feat_study_preflight_overlap_probe --> chore_tutorial_polish - feat_study_target_judgment_mismatch_guard --> chore_tutorial_polish infra_adapter_elastic --> chore_tutorial_polish - infra_ci_smoke_makeup --> chore_tutorial_polish - infra_dashboard_regen_pre_commit_conflict --> chore_tutorial_polish - infra_e2e_seed_completed_study --> chore_tutorial_polish - infra_e2e_wire_seed_helper_into_studies_spec --> chore_tutorial_polish infra_foundation --> chore_tutorial_polish - infra_frontend_stack_refresh --> chore_tutorial_polish - infra_ir_measures_migration --> chore_tutorial_polish - infra_make_targets_split_backend_only --> chore_tutorial_polish - infra_nvmrc --> chore_tutorial_polish infra_optuna_eval --> chore_tutorial_polish - infra_per_trial_timeout --> chore_tutorial_polish - infra_structlog_test_helpers --> chore_tutorial_polish - infra_uv_sync_drops_precommit --> chore_tutorial_polish - feat_agent_propose_search_space --> feat_chat_agent - feat_cluster_target_filter --> feat_chat_agent - feat_config_repo_baseline_tracking --> feat_chat_agent - feat_contextual_help --> feat_chat_agent - feat_create_study_search_space_builder --> feat_chat_agent - feat_create_study_target_autocomplete --> feat_chat_agent - feat_data_table_primitive --> feat_chat_agent feat_digest_proposal --> feat_chat_agent feat_github_pr_worker --> feat_chat_agent feat_github_webhook --> feat_chat_agent - feat_home_first_run_demo_nudge --> feat_chat_agent - feat_judgments_periodic_resume_sweep --> feat_chat_agent feat_llm_judgments --> feat_chat_agent - feat_orchestrator_zero_streak_abort --> feat_chat_agent - feat_pr_metric_confidence --> feat_chat_agent feat_proposals_ui --> feat_chat_agent - feat_query_inline_crud --> feat_chat_agent feat_studies_ui --> feat_chat_agent feat_study_lifecycle --> feat_chat_agent - feat_study_preflight_overlap_probe --> feat_chat_agent - feat_study_target_judgment_mismatch_guard --> feat_chat_agent infra_adapter_elastic --> feat_chat_agent - infra_ci_smoke_makeup --> feat_chat_agent - infra_dashboard_regen_pre_commit_conflict --> feat_chat_agent - infra_e2e_seed_completed_study --> feat_chat_agent - infra_e2e_wire_seed_helper_into_studies_spec --> feat_chat_agent infra_foundation --> feat_chat_agent - infra_frontend_stack_refresh --> feat_chat_agent - infra_ir_measures_migration --> feat_chat_agent - infra_make_targets_split_backend_only --> feat_chat_agent - infra_nvmrc --> feat_chat_agent infra_optuna_eval --> feat_chat_agent - infra_per_trial_timeout --> feat_chat_agent - infra_structlog_test_helpers --> feat_chat_agent - infra_uv_sync_drops_precommit --> feat_chat_agent infra_foundation --> feat_github_pr_worker infra_adapter_elastic --> feat_github_pr_worker feat_study_lifecycle --> feat_github_pr_worker diff --git a/docs/02_product/planned_features/bug_dashboard_depends_on_column_bloat/bug_fix.md b/docs/02_product/planned_features/bug_dashboard_depends_on_column_bloat/bug_fix.md new file mode 100644 index 00000000..df45c86d --- /dev/null +++ b/docs/02_product/planned_features/bug_dashboard_depends_on_column_bloat/bug_fix.md @@ -0,0 +1,70 @@ +# Bug fix — dashboard_depends_on_column_bloat + +**Source idea:** [idea.md](./idea.md) +**Branch:** `bug/dashboard-depends-on-column-bloat` +**Type:** bug fix — medium (this skill's scope; ~100 LOC across script + new test file) +**Date:** 2026-05-23 + +## Problem + +The MVP1 dashboard's "Depends on" column rendered 41-42 backtick'd entries for two shipped features (`feat_chat_agent` and `chore_tutorial_polish`, both 2026-05-12), including features that shipped weeks later (e.g., `feat_pr_metric_confidence` 2026-05-21) and still-planned ideas (e.g., `feat_ubi_judgments`). A shipped feature can't depend on something that didn't exist yet — the column was bloated and operator-misleading. Pre-existing on `main`; only Gemini Code Assist's review of PR #200 surfaced it. + +## Reproduction + +```bash +# Pre-fix on main: bloated rows +git checkout main +grep -m1 "feat_chat_agent" docs/00_overview/MVP1_DASHBOARD.md | grep -oE '\`[a-z_]+\`' | sort -u | wc -l +# → 41 entries (should be ~10 — only the features that shipped on or before 2026-05-12) +``` + +Regression test (in [`backend/tests/unit/scripts/test_dashboard_expand_transitive_deps.py`](../../../../backend/tests/unit/scripts/test_dashboard_expand_transitive_deps.py)) fails on `main` with `ImportError: cannot import name '_expand_transitive_deps'` (the helper doesn't exist there); passes on this branch. + +```bash +.venv/bin/python -m pytest backend/tests/unit/scripts/test_dashboard_expand_transitive_deps.py -v +``` + +## Root cause + +The diagnosis in the original `idea.md` was wrong (claimed the parser scanned the whole document); the actual cause is in the sentinel-expansion logic, not the parser. + +- **Owning layer:** scripts (regen script — not application code) +- **Parser (already correct):** [`scripts/build_mvp1_dashboard.py:445`](../../../../scripts/build_mvp1_dashboard.py#L445) — `re.search(r"^-\s+Depends on:\s*(.+)$", ..., re.MULTILINE)` is already scoped to the `- Depends on:` bullet line. +- **Bug site:** [`scripts/build_mvp1_dashboard.py:707-714`](../../../../scripts/build_mvp1_dashboard.py) (pre-fix) — the `DEPS_ALL_BACKEND` sentinel was expanded against the **current snapshot** of `infra_*`/`feat_*` folders with no time-ordering filter. Two features use the transitive marker (`_TRANSITIVE_DEP_PHRASES` at [line 413](../../../../scripts/build_mvp1_dashboard.py#L413)): `feat_chat_agent` says `- Depends on: ALL prior backend features`, `chore_tutorial_polish` says `ALL prior MVP1 features`. Both inherited today's full backend roster. + +## Fix design (locked decisions) + +1. **Extract `_expand_transitive_deps(features)` as a module-level helper.** Cites: standard testable-unit refactor pattern; the existing `_extract_*` helpers in the same file (e.g., `_extract_pr_number`, `_extract_merged_date`) are module-level and unit-tested similarly. +2. **Time-order the expansion via `(merged_date, pr_number, folder)` sort key.** For a shipped feature `f` using the sentinel, include only backend peers `g` where `_merge_order_key(g) < _merge_order_key(f)`. For a planned feature (no `merged_date`), keep the full-snapshot expansion (planned features genuinely depend on every backend sibling in the queue). Cites: idea.md's Phase 4 lock — folder date prefix is the canonical merge date; PR# is the same-day tiebreaker. +3. **Sort key — `("9999-99-99", 999999, folder)` for missing fields.** Anything without a `merged_date` sorts to end-of-time; anything without a `pr_number` sorts to end-of-day. Cites: preserves the conservative-exclusion property — when merge order is ambiguous, the helper excludes the ambiguous peer rather than risk including a post-shipment one. +4. **Preserve the pre-existing self-dep guard.** `f.folder` is still subtracted from the scoped expansion. The explicit-side self-reference (rare; would mean the spec author wrote their own folder name in `- Depends on:`) is left alone — out of scope for this bug. Cites: minimal-change rule from CLAUDE.md Bug Fix Protocol Step 3. + +### Open questions + +None — every fork was an engineering judgment call; all locked above. + +## Regression test plan + +| Layer | Path | What it asserts | +|---|---|---| +| unit | `backend/tests/unit/scripts/test_dashboard_expand_transitive_deps.py` | 10 cases: shipped-feature time-scoped expansion (the canonical bloat case), planned-feature full-snapshot expansion, explicit-deps union with sentinel, no-sentinel pass-through, non-backend prefixes excluded, self-dep guard preserved, and 4 `_merge_order_key` cases locking date / PR# / missing-fields tiebreakers. Verified to fail on `main` with `ImportError` (the helper doesn't exist there). | + +End-to-end verification on the live filesystem: + +- `feat_chat_agent` row: **41 → 10** backtick'd entries. +- `chore_tutorial_polish` row: **42 → 11** backtick'd entries. +- All other dashboard rows scoped to existing folders: byte-identical (no collateral churn from the bug-fix logic). +- The dashboard's Idea section gains one new row for the tangential follow-up `chore_dashboard_pr_extraction_from_idea` (filed in this PR per CLAUDE.md tangential-discoveries protocol). Idea count moves from 14 → 15 / 7-remaining → 8-remaining; that's the only non-bloat-related dashboard delta. +- 10 entries match exactly the set of `infra_*`/`feat_*` folders shipped on or before 2026-05-12 minus `feat_chat_agent` itself (verified by `ls docs/00_overview/implemented_features/`). + +## Rollout + +None — code-only change to the regen script. + +- No schema, no API, no migration. +- The pre-commit `mvp1-dashboard-regen` hook regenerates [`MVP1_DASHBOARD.md`](../../../00_overview/MVP1_DASHBOARD.md), [`DASHBOARD.md`](../../../00_overview/DASHBOARD.md), and the `.html` siblings automatically on commit. Those files travel with the fix. +- No operator action required. + +## Tangential observations + +- [`chore_dashboard_pr_extraction_from_idea`](../chore_dashboard_pr_extraction_from_idea/idea.md) — `_extract_pr_number` only reads `pipeline_status.md` / `implementation_plan.md` / `feature_spec.md`, not `idea.md`. Legacy implemented features that shipped before the `/pipeline` ceremony (e.g., `infra_frontend_stack_refresh`) only have `idea.md`, so their PR# is `None` and they sort to end-of-day in `_merge_order_key`. Net effect: ~1 missing edge per legacy feature in same-day peers' deps. Operator-cosmetic, not a correctness regression. Worth a polish PR next time someone is touching the regen script. diff --git a/docs/02_product/planned_features/bug_dashboard_depends_on_column_bloat/idea.md b/docs/02_product/planned_features/bug_dashboard_depends_on_column_bloat/idea.md index fbee508f..5cf8e787 100644 --- a/docs/02_product/planned_features/bug_dashboard_depends_on_column_bloat/idea.md +++ b/docs/02_product/planned_features/bug_dashboard_depends_on_column_bloat/idea.md @@ -8,46 +8,80 @@ ## Problem -[`scripts/build_mvp1_dashboard.py`](../../../../scripts/build_mvp1_dashboard.py) (2,084 lines) generates the "Depends on" column for each planned-feature row in [`MVP1_DASHBOARD.md`](../../../00_overview/MVP1_DASHBOARD.md) and `mvp1_dashboard.html`. The current output produces logical impossibilities: +[`scripts/build_mvp1_dashboard.py`](../../../../scripts/build_mvp1_dashboard.py) (2,084 lines) generates the "Depends on" column for each row in [`MVP1_DASHBOARD.md`](../../../00_overview/MVP1_DASHBOARD.md) and `mvp1_dashboard.html`. Two shipped features render impossibly-large dependency lists: -- **`feat_chat_agent`** shipped 2026-05-12 (PR #60). Its "Depends on" column lists 46 features, including `feat_pr_metric_confidence` (shipped 2026-05-21), `feat_study_clone_from_previous` (idea only), `feat_ubi_judgments` (idea only, dated 2026-05-22). A merged feature can't depend on ideas that didn't exist yet. -- **`chore_tutorial_polish`** shipped 2026-05-12 (PR #64). Same pattern — lists 46 features, most shipped weeks later or still unscoped. -- The shape suggests the regen script is treating **every backtick'd feature-name reference in a spec/idea body** as a forward "depends on" relationship — including cases where the reference is in a "Relationship to other work" section, a "Future extensions" paragraph, or a comparison to a sibling feature. +- **`feat_chat_agent`** shipped 2026-05-12 (PR #60). Its "Depends on" column lists 46 entries, including `feat_pr_metric_confidence` (shipped 2026-05-21), `feat_study_clone_from_previous` (idea only), `feat_ubi_judgments` (idea only, dated 2026-05-22). A merged feature can't depend on features that didn't exist yet. +- **`chore_tutorial_polish`** shipped 2026-05-12 (PR #64). Same shape — 42 backtick'd entries, most shipped weeks later or still unscoped. -The "Depends on" column should reflect the **forward dependency graph** — i.e., what each planned feature needs to ship *before* it. The canonical source is the `**Depends on:**` line in each `idea.md` / `feature_spec.md` ([`feature_templates/idea-template.md`](../feature_templates/idea-template.md) requires it). Parsing that line directly (instead of grep'ing the whole document for backtick'd feature names) would fix the bloat. +The **actual root cause** (confirmed by reading the code, not by inference from symptoms — the prior diagnosis in this idea was wrong): + +1. The parser at [`build_mvp1_dashboard.py:435-456`](../../../../scripts/build_mvp1_dashboard.py) is **already correctly scoped** to the `- Depends on:` line via `re.search(r"^-\s+Depends on:\s*(.+)$", ..., re.MULTILINE)`. It does not scan the whole document; it correctly extracts only the canonical bullet line. +2. The bloat lives in the **sentinel-expansion logic** at [`build_mvp1_dashboard.py:707-714`](../../../../scripts/build_mvp1_dashboard.py). The parser recognizes the prose markers `"all prior backend features"` / `"all prior mvp1 features"` (defined as `_TRANSITIVE_DEP_PHRASES` at line 413) and adds the `DEPS_ALL_BACKEND` sentinel. The expansion block then replaces that sentinel with **every `infra_*` and `feat_*` folder in the current snapshot**, with no time-ordering filter: + +```python +backend_folders = sorted(f.folder for f in features if f.prefix in ("infra", "feat")) +for f in features: + if DEPS_ALL_BACKEND not in f.depends_on: + continue + explicit = [d for d in f.depends_on if d != DEPS_ALL_BACKEND] + merged = sorted(set(explicit) | set(backend_folders) - {f.folder}) + f.depends_on = merged +``` + +So a feature like `feat_chat_agent` that genuinely meant "everything merged before me on 2026-05-12" inherits today's full backend roster — including 30+ features that shipped *after* it and a handful of planned ideas that don't exist on disk as code yet. Only **two** features use the transitive phrase (verified by `grep -rlE "^- Depends on:.*(ALL prior|all backend|all MVP)" docs/`), so the fix surface is narrow. + +The fix is to time-order the sentinel expansion — for shipped features, restrict the expansion to backend folders that merged on or before this feature's merge date. For planned features that still use the transitive phrase (none today, but the planned `feat_ubi_judgments` etc. don't use it), the current-snapshot expansion remains correct (a planned feature genuinely depends on everything in the queue). ## Proposed capabilities -Single tier — fix the parser; no schema or UI change. +Single tier — time-order the sentinel expansion; no schema, no UI, no parser change. + +### Sentinel-expansion correction + +**Scope:** [`scripts/build_mvp1_dashboard.py:703-714`](../../../../scripts/build_mvp1_dashboard.py) — the block that resolves `DEPS_ALL_BACKEND` against `backend_folders`. + +**Fix design (recommended default — lock during /bug-fix or /spec-gen):** + +1. **Derive a merge-order key** for every feature in the loaded set. Shipped features have a `pr_number` (already parsed by `_extract_pr_number` at line 476+) and/or a folder prefix `YYYY_MM_DD_` from their `implemented_features/` path. Planned features have neither — they're "post-everything-shipped" in dependency terms. +2. **Filter the expansion per-feature:** + - For a **shipped feature** `f` using the transitive marker, expand `DEPS_ALL_BACKEND` to only those backend folders whose merge order is strictly less than `f`'s merge order. Use the folder date prefix (`YYYY_MM_DD_`) for shipped peers and treat planned features as having infinite merge order (i.e., never included). + - For a **planned feature** using the transitive marker (none today; defensive only), keep the current behavior — expand to every backend folder in the snapshot, since a planned feature genuinely depends on everything queued. +3. **Tiebreaker** when two shipped features share the same date prefix (e.g., both 2026-05-12): use PR number ascending; if PR numbers are equal or absent, fall back to lexicographic folder name. This is a rare case but worth pinning. + +**Expected post-fix outcomes:** + +- `feat_chat_agent` ([`implemented_features/2026_05_12_feat_chat_agent/`](../../../00_overview/implemented_features/2026_05_12_feat_chat_agent/)) "Depends on" column drops from 46 entries to the count of `infra_*`/`feat_*` folders that shipped on or before 2026-05-12: `infra_foundation` (2026-05-09), `infra_adapter_elastic` (2026-05-10), `infra_optuna_eval` (2026-05-10), `feat_study_lifecycle` (2026-05-10), `feat_llm_judgments` (2026-05-11), `feat_digest_proposal` (2026-05-11), `feat_github_pr_worker` (2026-05-12), `feat_github_webhook` (2026-05-12), `feat_studies_ui` (2026-05-12), `feat_proposals_ui` (2026-05-12). That's roughly 10 entries — coherent and time-consistent. +- `chore_tutorial_polish` (also 2026-05-12, PR #64) drops from 42 entries to the same set (it's the release-readiness chore; "ALL prior MVP1 features" really does mean "everything up to PR #64"). +- All other shipped features that DON'T use the transitive phrase are unchanged (their `- Depends on:` lines list explicit folders; no sentinel involved). + +**Regression test plan:** + +Add a unit test alongside the existing test fixtures for the regen script. Two cases: -### Parser correction +1. Lock the fix: a `feat_chat_agent`-shaped fixture (folder prefixed `2026_05_12_*`, spec body says `- Depends on: ALL prior backend features`) gets expanded against a feature set that includes one before (`2026_05_09_*`), one same-day (`2026_05_12_*` with lower PR#), and one after (`2026_05_21_*`). Expected: only the before + same-day-lower-PR neighbors appear, not the after. +2. Lock the unchanged-behavior path: a planned-feature fixture using the transitive phrase gets expanded against the full snapshot (current behavior preserved). -- **Locate the "Depends on" extraction logic** in [`scripts/build_mvp1_dashboard.py`](../../../../scripts/build_mvp1_dashboard.py). Likely a regex sweep over the whole document body; needs to be scoped to the `**Depends on:**` line only. -- **Spec format:** `**Depends on:** `. The line lives near the top of every idea.md (per the template) and every implemented feature_spec.md. -- **Edge cases:** - - `Depends on: None` → empty list (rendered as `—` in the markdown). - - `Depends on:` followed by a paragraph of prose with multiple backtick'd names → parse all backtick'd names on that line only. - - Multiple "Depends on:" lines (shouldn't exist, but defensive) → use the first. - - Implemented features whose canonical `feature_spec.md` predates the convention → fall back to scanning the first 30 lines for an explicit "Depends on" mention; if none, emit `—`. -- **Add a regression test:** new unit test in `backend/tests/unit/scripts/test_dashboard_depends_on.py` (or wherever existing tests for the regen script live) asserts that for a fixture set of idea.md files, only the `**Depends on:**` line is parsed — not body-level backtick references. +Test file location: `scripts/tests/test_dashboard_depends_on_expansion.py` if a scripts test dir exists; otherwise `backend/tests/unit/scripts/test_dashboard_depends_on_expansion.py`. Verify the location during /bug-fix. ### Verify the bloated rows shrink -After the fix, re-run `python scripts/build_mvp1_dashboard.py`. Expected outcomes: +After the fix, re-run `python scripts/build_mvp1_dashboard.py`. Confirm: -- `feat_chat_agent` "Depends on" column drops from 46 entries to whatever its actual spec lists (likely 1-3: `infra_foundation`, `infra_adapter_elastic`, possibly `feat_study_lifecycle`). -- Every implemented feature's "Depends on" column drops to its real forward dependency graph. -- Spot-check 3-5 rows against the source `feature_spec.md` "Depends on:" line to confirm parity. +- `feat_chat_agent` row backtick'd-entry count drops from 46 to ~10. +- `chore_tutorial_polish` row drops from 42 to ~10. +- Every other shipped feature's row is byte-identical to its pre-fix state (no collateral churn). +- Spot-check both shrunk rows against the time-ordered subset by hand. ### Out of scope +- Changing the parser (already correctly scoped — see Problem section). - Adding a "Depended on by" (reverse-dependency) column. The current dashboard has no such surface; reverse lookups can come later if useful. - UI / HTML styling changes. The bug is purely in the data layer. ## Scope signals - **Backend:** 0 LOC (no API change). -- **Scripts:** ~30–80 LOC in `scripts/build_mvp1_dashboard.py` — narrowing the parser. Plus ~80 LOC test coverage. +- **Scripts:** ~20–40 LOC in `scripts/build_mvp1_dashboard.py` — adding a per-feature merge-order filter to the `DEPS_ALL_BACKEND` expansion block at lines 703-714. Plus ~80 LOC test coverage. - **Frontend:** 0 LOC. - **Migration:** None. - **Config:** None. @@ -63,5 +97,5 @@ The fix is bounded enough to ship in a follow-up PR with no further design work. ## Relationship to other work - **Surfaced by [PR #200](https://github.com/SoundMindsAI/relyloop/pull/200)** — Gemini Code Assist flagged 4 instances when `feat_ubi_judgments` got added to the bloated lists. The bug pre-exists PR #200; this idea is the deferred-fix capture. -- **Adjacent to [`infra_dashboard_regen_pre_commit_conflict`](../infra_dashboard_regen_pre_commit_conflict/)** (status: TBD — also a dashboard-regen issue, but about pre-commit hook conflicts rather than "Depends on" parsing). May be worth bundling into a single dashboard-regen-quality PR if both are tackled together. +- **Adjacent to [`infra_dashboard_regen_pre_commit_conflict`](../../../00_overview/implemented_features/2026_05_14_infra_dashboard_regen_pre_commit_conflict/)** (shipped 2026-05-14 — pre-commit hook conflicts on idempotent regen + relative-link rewriting). Touches the same script but at a different layer (pre-commit hook contract), so no bundling needed; cited only for context. - **Does NOT block any planned feature.** The dashboard is internal planning surface; the bloated column doesn't break navigation or block decisions, it just makes "Depends on" non-actionable. diff --git a/docs/02_product/planned_features/chore_dashboard_pr_extraction_from_idea/idea.md b/docs/02_product/planned_features/chore_dashboard_pr_extraction_from_idea/idea.md new file mode 100644 index 00000000..b850f09a --- /dev/null +++ b/docs/02_product/planned_features/chore_dashboard_pr_extraction_from_idea/idea.md @@ -0,0 +1,52 @@ +# Extend `_extract_pr_number` to read PR# from idea.md for legacy implemented features + +**Date:** 2026-05-23 +**Status:** Idea — surfaced during the tangential-observations sweep of `bug_dashboard_depends_on_column_bloat` (PR pending) +**Priority:** P2 — minor data gap; affects only same-day tiebreakers for early-shipped features without a `feature_spec.md` or `pipeline_status.md`. Practical impact is one or two missing edges in the dependency graph; not a correctness regression. +**Origin:** Investigating the post-fix dependency list for `feat_chat_agent` (10 entries down from 41), I noticed `infra_frontend_stack_refresh` (shipped 2026-05-12) was excluded. Root cause: it has no `feature_spec.md` / `pipeline_status.md` / `implementation_plan.md` — only `idea.md`. The regen script's `_extract_pr_number` at [`scripts/build_mvp1_dashboard.py:476`](../../../../scripts/build_mvp1_dashboard.py#L476) reads `pipe + plan + spec` looking for the PR#, so for idea-only features it returns `None`. With `pr_number=None`, the new `_merge_order_key` helper sorts the feature to end-of-day (key tuple `(date, 999999, folder)`), placing it AFTER same-day peers with concrete PR numbers. The fix's time-order filter then excludes it from those peers' `DEPS_ALL_BACKEND` expansion. +**Depends on:** [`bug_dashboard_depends_on_column_bloat`](../bug_dashboard_depends_on_column_bloat/idea.md) (must merge first — this is a polish layer on top of that fix). + +## Problem + +Several early MVP1 features shipped before the `/pipeline` ceremony solidified, leaving them with only an `idea.md` in `implemented_features/_/`. Examples (as of 2026-05-23): + +- `2026_05_12_infra_frontend_stack_refresh` — idea.md only +- `2026_05_13_chore_ci_gitleaks_workflow_step` — idea.md only +- `2026_05_13_chore_ci_gitignore_paths_ignore_gap` — idea.md only +- `2026_05_13_chore_cluster_delete_ui` — idea.md only +- `2026_05_13_infra_per_trial_timeout` — idea.md only +- `2026_05_13_infra_nvmrc` — idea.md only + +(Plus several more from the same period — `find docs/00_overview/implemented_features/ -maxdepth 1 -type d -exec test '!' -f {}/feature_spec.md ';' -print`.) + +For each of these, `_extract_pr_number(pipe, plan, spec)` is called with `pipe == plan == spec == ""` because the script never reads `idea.md`. The function returns `None`. Downstream effects: + +1. **Dashboard "Status" column** — these features render as `Complete` (the fallback at line 652) instead of `[PR #N](url) merged YYYY-MM-DD`. Minor cosmetic loss. +2. **`DEPS_ALL_BACKEND` expansion tiebreaker** — these features sort to end-of-day in `_merge_order_key`. Same-day peers with PR numbers (e.g., `feat_chat_agent` PR #60, `chore_tutorial_polish` PR #64) exclude them from the transitive-deps expansion. Net effect on the dashboard: ~1 missing edge per affected legacy feature in the canonical `feat_chat_agent` / `chore_tutorial_polish` rows. + +## Proposed fix + +Extend `_extract_pr_number` to also accept an `idea` text argument (or add `idea` to the existing concat at the call site). Most legacy idea.md files cite their own PR# in the format `merged via PR #N` or `(PR #N)` somewhere in the body — the existing regex `r"PR[^a-zA-Z\n]{0,5}#(\d+)[^.\n]{0,80}merged"` would match if applied to the idea body. + +- **Scope:** ~10 LOC in [`scripts/build_mvp1_dashboard.py`](../../../../scripts/build_mvp1_dashboard.py) — read `idea.md` in `_load_implemented`, pass it to `_extract_pr_number`. Update the function signature accordingly. +- **Tests:** 1 new test case in [`backend/tests/unit/scripts/test_dashboard_expand_transitive_deps.py`](../../../../backend/tests/unit/scripts/test_dashboard_expand_transitive_deps.py) (or a new test file for the loader) asserting an idea-only implemented feature gets its PR# extracted. +- **Verification:** after the fix, `infra_frontend_stack_refresh` should appear in `feat_chat_agent`'s `Depends on` column (it shipped before via PR earlier than #60, assuming the idea cites it). + +## Why deferred + +Not a correctness regression — the `bug_dashboard_depends_on_column_bloat` fix dramatically improves the dependency surface (41→10, 42→11 entries). The remaining ~1-edge data gap per legacy feature is operator-cosmetic, not a blocker for any planning workflow. Worth picking up when the next dashboard-quality sweep lands. + +## Scope signals + +- **Backend:** 0 LOC. +- **Scripts:** ~10 LOC in `scripts/build_mvp1_dashboard.py`. +- **Frontend:** 0 LOC. +- **Migration:** None. +- **Config:** None. +- **Audit events:** N/A. +- **Tests:** ~20 LOC test coverage. + +## Relationship to other work + +- **Predicated on [`bug_dashboard_depends_on_column_bloat`](../bug_dashboard_depends_on_column_bloat/idea.md)** — the time-order filter created the regression-surface for missing PR numbers. Without the bloat fix, the missing PR# had no visible effect (everything got expanded anyway). +- **Touches the same `_extract_pr_number` surface as the early dashboard work**. No conflicts; purely additive. diff --git a/scripts/build_mvp1_dashboard.py b/scripts/build_mvp1_dashboard.py index 5639438f..e583889d 100755 --- a/scripts/build_mvp1_dashboard.py +++ b/scripts/build_mvp1_dashboard.py @@ -684,6 +684,59 @@ def _data_freshness() -> dt.datetime: return dt.datetime.fromtimestamp(max(candidates), tz=dt.UTC) +def _merge_order_key(f: Feature) -> tuple[str, int, str]: + """Sort key approximating merge order across the feature set. + + Used by :func:`_expand_transitive_deps` to scope a shipped feature's + ``DEPS_ALL_BACKEND`` expansion to peers that merged on or before it + (bug_dashboard_depends_on_column_bloat). Tuple components: + + 1. ``merged_date`` (YYYY-MM-DD lexicographic) — primary order. + Planned features (no merge date) sort to "9999-99-99", placing + them strictly after every shipped feature. + 2. ``pr_number`` — same-day tiebreaker. Missing PR# sorts last + within the date. + 3. ``folder`` — final stable tiebreaker (rare path). + """ + return ( + f.merged_date or "9999-99-99", + f.pr_number if f.pr_number is not None else 999_999, + f.folder, + ) + + +def _expand_transitive_deps(features: list[Feature]) -> None: + """Expand each feature's ``DEPS_ALL_BACKEND`` sentinel in place. + + For SHIPPED features (those with a ``merged_date``), the expansion + is filtered to backend peers whose merge order is strictly less + than this feature's — i.e., everything actually merged before it. + This is the bug_dashboard_depends_on_column_bloat fix: under the + prior implementation, ``feat_chat_agent`` (shipped 2026-05-12) + inherited today's full backend roster, including features that + shipped weeks later and planned ideas not yet specced. A shipped + feature can't depend on something that didn't exist yet. + + For PLANNED features that use the transitive marker (defensive — + none today), the expansion remains the current full snapshot, + since a planned feature genuinely depends on every backend sibling + in the queue. + """ + backend = [f for f in features if f.prefix in ("infra", "feat")] + for f in features: + if DEPS_ALL_BACKEND not in f.depends_on: + continue + explicit = [d for d in f.depends_on if d != DEPS_ALL_BACKEND] + if f.merged_date is not None: + self_key = _merge_order_key(f) + scoped = {g.folder for g in backend if _merge_order_key(g) < self_key} + else: + scoped = {g.folder for g in backend} + # Self-deps don't make sense; drop f.folder if it slipped in via + # either the explicit list or the sentinel expansion. + f.depends_on = sorted((set(explicit) | scoped) - {f.folder}) + + def load_all() -> list[Feature]: features: list[Feature] = [] if PLANNED_DIR.exists(): @@ -703,15 +756,9 @@ def load_all() -> list[Feature]: # Expand the DEPS_ALL_BACKEND sentinel against the live feature set # (per the pipeline-skill algorithm). Resolved AFTER both planned + # implemented are loaded so transitive deps see every backend - # sibling regardless of which folder they live in. - backend_folders = sorted(f.folder for f in features if f.prefix in ("infra", "feat")) - for f in features: - if DEPS_ALL_BACKEND not in f.depends_on: - continue - explicit = [d for d in f.depends_on if d != DEPS_ALL_BACKEND] - # Self-deps don't make sense; drop f.folder if it slipped in. - merged = sorted(set(explicit) | set(backend_folders) - {f.folder}) - f.depends_on = merged + # sibling regardless of which folder they live in. Time-ordered for + # shipped features (bug_dashboard_depends_on_column_bloat). + _expand_transitive_deps(features) return features