From cbbed844a42004d838f063491ee5d221c1926146 Mon Sep 17 00:00:00 2001 From: Paulo Date: Sat, 1 Aug 2026 23:14:37 +0200 Subject: [PATCH] Verification commands carry the CI check that proves them --- backend/druks/contrib/ship/contracts.py | 26 ++++-- backend/druks/contrib/ship/policy.py | 11 ++- .../build/evaluate_implementation.md | 14 +-- .../ship/templates/profile/repo_profiler.md | 13 +-- .../ship/templates/verification_block.md | 6 +- backend/druks/contrib/ship/workflows.py | 15 +++- ...2_verification_commands_carry_ci_checks.py | 87 +++++++++++++++++++ backend/tests/ship/test_build_prompts.py | 23 +++++ backend/tests/ship/test_contracts.py | 28 ++++++ backend/tests/ship/test_profiling.py | 42 ++++----- .../extensions/ship/projects/ProjectsPage.tsx | 6 +- .../src/extensions/ship/projects/types.ts | 6 +- 12 files changed, 221 insertions(+), 56 deletions(-) create mode 100644 backend/migrations/versions/d9f3a7c1e5b2_verification_commands_carry_ci_checks.py diff --git a/backend/druks/contrib/ship/contracts.py b/backend/druks/contrib/ship/contracts.py index ee0c1619..bb2eb095 100644 --- a/backend/druks/contrib/ship/contracts.py +++ b/backend/druks/contrib/ship/contracts.py @@ -30,14 +30,19 @@ async def on_wait(cls, workflow: Workflow) -> None: await build.request_assignee_review() +class VerificationCommandOutput(AgentOutput): + command: str + ci_check: str | None + + class RepoProfilerOutput(AgentOutput): languages: list[str] frameworks: list[str] package_managers: list[str] stack_summary: str - test_commands: list[str] - lint_commands: list[str] - typecheck_commands: list[str] + test_commands: list[VerificationCommandOutput] + lint_commands: list[VerificationCommandOutput] + typecheck_commands: list[VerificationCommandOutput] # Skills the profiler judges an implementer will need to build here — not # skills bundled in the repo. recommended_skills: list[str] @@ -51,9 +56,18 @@ def to_result(self) -> dict[str, Any]: "package_managers": self.package_managers, "stack_summary": self.stack_summary, "verification": { - "test_commands": self.test_commands, - "lint_commands": self.lint_commands, - "typecheck_commands": self.typecheck_commands, + "test_commands": [ + {"command": entry.command, "ci_check": entry.ci_check} + for entry in self.test_commands + ], + "lint_commands": [ + {"command": entry.command, "ci_check": entry.ci_check} + for entry in self.lint_commands + ], + "typecheck_commands": [ + {"command": entry.command, "ci_check": entry.ci_check} + for entry in self.typecheck_commands + ], }, "recommended_skills": self.recommended_skills, } diff --git a/backend/druks/contrib/ship/policy.py b/backend/druks/contrib/ship/policy.py index 92e2e3f7..4cd4bd97 100644 --- a/backend/druks/contrib/ship/policy.py +++ b/backend/druks/contrib/ship/policy.py @@ -58,15 +58,18 @@ async def verification_block(self, *, profile: dict[str, Any], repo: str | None) # invent verification commands" guardrail. verification = profile.get("verification") or {} sections = [ - {"label": "Lint", "commands": verification.get("lint_commands", [])}, - {"label": "Typecheck", "commands": verification.get("typecheck_commands", [])}, - {"label": "Tests", "commands": verification.get("test_commands", [])}, + {"label": "Lint", "command_entries": verification.get("lint_commands", [])}, + { + "label": "Typecheck", + "command_entries": verification.get("typecheck_commands", []), + }, + {"label": "Tests", "command_entries": verification.get("test_commands", [])}, ] body = await render_prompt( "ship/verification_block.md", repo=repo, sections=sections, - has_commands=any(section["commands"] for section in sections), + has_commands=any(section["command_entries"] for section in sections), sandbox_env_keys=sorted(self.sandbox.env), ) return body.rstrip() + "\n" diff --git a/backend/druks/contrib/ship/templates/build/evaluate_implementation.md b/backend/druks/contrib/ship/templates/build/evaluate_implementation.md index 9caddff9..8600aebd 100644 --- a/backend/druks/contrib/ship/templates/build/evaluate_implementation.md +++ b/backend/druks/contrib/ship/templates/build/evaluate_implementation.md @@ -34,7 +34,7 @@ plan should have asked for. You verify what it did ask for, exhaustively, in a s {% include "ship/build/_contract.md" %} {% include "ship/build/_related_repos.md" %} {% include "ship/build/_skills.md" %} -Evaluate the implementation against the **Current plan** above, the issue, and the current PR diff. When `pr_base_sha` and `head_sha` are listed in the **Workflow context** section above, use them as the authoritative PR diff range and evaluate `head_sha` against `pr_base_sha`. If branch names disagree with those SHAs, trust the SHAs and mention metadata drift only when it affects the PR. Return blocked if an authoritative SHA is unavailable locally after fetching. Evaluate every acceptance criterion from the PR state and report one result per criterion. Inspection commands such as git diff/show, rg, and sed are allowed for review. Verification commands are different: report a result for every configured verification profile command, taking that result from CI wherever CI already ran the command (see GITHUB CHECKS below) and running the command yourself otherwise. Do not invent repo-specific smoke tests or package install commands. Return exactly one final result object. Return pass only when the work is ready for a human final PR review. Return fail for actionable implementation changes. +Evaluate the implementation against the **Current plan** above, the issue, and the current PR diff. When `pr_base_sha` and `head_sha` are listed in the **Workflow context** section above, use them as the authoritative PR diff range and evaluate `head_sha` against `pr_base_sha`. If branch names disagree with those SHAs, trust the SHAs and mention metadata drift only when it affects the PR. Return blocked if an authoritative SHA is unavailable locally after fetching. Evaluate every acceptance criterion from the PR state and report one result per criterion. Inspection commands such as git diff/show, rg, and sed are allowed for review. Verification commands are different: report a result for every configured verification profile command, reading its named CI check when it has one (see GITHUB CHECKS below) and running it yourself when it does not. Do not invent repo-specific smoke tests or package install commands. Return exactly one final result object. Return pass only when the work is ready for a human final PR review. Return fail for actionable implementation changes. EXHAUSTIVE ENUMERATION — this is the single most important rule. Subsequent rounds will not retry, and findings you omit now cost an entire extra implementation loop to surface next round. Walk through these sweeps and list every blocker you find in a single response: 1. Each acceptance criterion explicitly — does the diff satisfy it? @@ -48,9 +48,9 @@ Do not return until you have collected every finding you can identify across all UNFULFILLABLE-AC GATE — before scoring any finding against an acceptance criterion, check whether the criterion is **code-verifiable** by you (reading the diff, inspecting tests, running the configured verification profile). If a criterion requires manual operator action — "manually smoke X", "load the app locally", "verify visually in the browser", "click through Y", "confirm against the live N integration", "screenshot the rendered output", etc. — it is **not satisfiable by the implementer** through any code change. Mark its `acceptance_results` entry as `not_run` with a one-line reason ("requires operator-driven manual smoke") and do NOT emit a blocking finding against it. The planner is supposed to keep these out of binding AC, but if one slips through, the evaluator must not loop the implementer over it forever. Report once per round at most, as a `low`-severity note recommending the operator smoke post-merge — never as `high` or `medium`. INFEASIBLE-BLOCKER GATE — return `blocked`, NOT `fail`, when the only thing keeping this PR from `pass` is something **no in-scope code change by the implementer can fix**. `fail` re-runs the implementer (a full ~10-minute round); if the blocker is unfixable, every round makes identical non-progress until the revision cap escalates to a human anyway — so escalate now instead of burning the rounds. `blocked` routes straight to the operator. Three shapes qualify, and you must name the specifics in `body`: -1. **Environmental** — a mandatory verification command cannot run in this sandbox because the runtime or toolchain is wrong/missing, not because the code is wrong. Examples: "the production build needs Node >=20.9.0 but the sandbox has 18.x", "the typecheck binary exits printing its help instead of running", "the test interpreter/deps aren't installed". Report the exact command and blocker, mark the check `not_run`, and return `blocked` — unless green GitHub checks cover the same ground (see GITHUB CHECKS below), which turns it into a pass. Do not fail the implementer for a check the box physically cannot execute. +1. **Environmental** — a mandatory verification command cannot run in this sandbox because the runtime or toolchain is wrong/missing, not because the code is wrong. Examples: "the production build needs Node >=20.9.0 but the sandbox has 18.x", "the typecheck binary exits printing its help instead of running", "the test interpreter/deps aren't installed". Report the exact command and blocker, mark the check `not_run`, and return `blocked` — unless the command entry names a GitHub check that is green for `head_sha`, which turns it into a pass. Do not fail the implementer for a check the box physically cannot execute. 2. **Contradictory / forbidden** — satisfying one binding requirement would require a change another binding requirement (or the PR's out-of-scope guard) explicitly forbids. Example: one requirement makes the frontend build mandatory while another forbids touching dependencies or the runtime. The contract is unsatisfiable as written; only a human can relax it. Quote both requirements and return `blocked`. -3. **Pre-existing baseline failure** — the failing check also fails on `pr_base_sha` / in code the diff did not touch (confirm before claiming it). The diff didn't introduce it, so it isn't this PR's regression. Mark it `not_run`/baseline; if it is the ONLY blocker, return `blocked` with that note rather than `fail`. +3. **Pre-existing baseline failure** — the failing check is already failing on the default branch / in code the diff did not touch (confirm before claiming it). For a configured check with a name, read that exact check's recent conclusions on the default branch with `gh` (`gh run list` or the check-runs API); do not check out `pr_base_sha` and re-run the command locally to answer the baseline question. The diff didn't introduce it, so it isn't this PR's regression. Mark it `not_run`/baseline; if it is the ONLY blocker, return `blocked` with that note rather than `fail`. The test is strict and binary: *can a code change the implementer is allowed to make resolve this blocker?* Yes → `fail` with an actionable finding (a test its own diff broke, a missed AC, a real in-scope code defect). No → `blocked`. Never loop the implementer on a blocker no code change can clear. SEVERITY CALIBRATION — assign severity per finding: @@ -80,9 +80,9 @@ Why the strictness: the system has already spent multiple rounds inspecting this Comment form rules apply to every review note that requests a code change, whether it surfaces as a per-criterion result, a check note, or a line comment on the diff. Describe the constraint, not the prescription: when two or more reasonable approaches satisfy the constraint, list them with trade-offs and let the implementer choose; prescribe a specific implementation only when one is clearly dominant, and say why. Name the test that should exist after the fix lands — either an existing test to extend or a new one to add — because a code-change request without a test note is incomplete. When you are enforcing a previously-flagged requirement, quote or link the original ask; mark unaddressed prior items explicitly so silently-dropped feedback gets surfaced rather than restated from memory. Leave room for disagreement: end with explicit permission to push back so the implementer can engage rather than just comply. Write in active voice with one subject per sentence; avoid stacked qualifiers and noun-chain phrasing. GITHUB CHECKS — the PR's CI is your primary verification evidence; consult it yourself (`gh` is authenticated). Read the checks for exactly `head_sha`; a result from another commit is not evidence. -- Where a green check already ran a configured verification command, record that command as passing, name the check, and do not run it yourself. Rerunning what CI just ran spends a full suite for no new information. -- A failing check this diff caused is a high finding and a fail verdict. Inspect its log and reproduce only the single failing target it names, never the whole suite. Confirm it also fails on `pr_base_sha` before claiming it is a pre-existing baseline failure (shape 3 of the INFEASIBLE-BLOCKER GATE). -- Run a configured command yourself whenever CI does not clearly cover it, including when its check has not settled after you finish everything else and wait a few minutes. Never record a command as passing on a check you cannot point at. Unsettled checks that cover no configured command are not_run and do not block. -- If a command you must run yourself cannot run because repo dependencies, private indexes, or credentials are unavailable, report that check as not_run. Green GitHub checks covering the same ground make that gap non-blocking: return pass, not blocked, and name in `body` which GitHub checks stood in. +- A configured command with a CI check name is covered by exactly that named check. Read its conclusion for `head_sha`; when it is green, record the command as passing, name the check, and do not run the command yourself. +- A failing named check this diff caused is a high finding and a fail verdict. Inspect its log and reproduce only the single failing target it names, never the whole suite. Before claiming a pre-existing baseline failure, read that same check's recent conclusions on the default branch with `gh` (shape 3 of the INFEASIBLE-BLOCKER GATE); do not check out `pr_base_sha` and re-run the suite locally. Keep the local single-target reproduction for a diff-caused failure. +- A configured command with no CI check name runs locally. Never infer that another check covers it. If a named check has not settled after you finish everything else and wait a few minutes, mark its command `not_run`; do not substitute a local run for the missing conclusion. Unsettled checks that cover no configured command are `not_run` and do not block. +- If a command you must run locally cannot run because repo dependencies, private indexes, or credentials are unavailable, report that check as `not_run`. A different green GitHub check cannot stand in for it. {% include "ship/build/_github_review.md" %} diff --git a/backend/druks/contrib/ship/templates/profile/repo_profiler.md b/backend/druks/contrib/ship/templates/profile/repo_profiler.md index 64548d08..10ffabe5 100644 --- a/backend/druks/contrib/ship/templates/profile/repo_profiler.md +++ b/backend/druks/contrib/ship/templates/profile/repo_profiler.md @@ -22,11 +22,14 @@ its name or a language's usual defaults. required checks and commands the repo's CI or docs treat as must-pass. Confirm each candidate is actually passing on the default branch — check its latest run or commit status with `gh` (authenticated here); never assume a configured or required check is - green. Report the exact configured command; never invent one. Leave a category empty when - no command qualifies — empty test, lint, and typecheck categories are correct and common. - Put editor-only or advisory tools, optional linters, and known-red or flaky suites in - `stack_summary` as context, never in verification. Do not list a command that - `stack_summary` describes as not a CI gate, red, or flaky. + green. Report the exact configured command; never invent one. Beside each command, record + `ci_check` as the exact CI check name whose green run confirmed it. Set `ci_check` to null + when no CI check runs that command; never guess or approximate a check name — a wrong name + is worse than null. Leave a category empty when no command qualifies — empty test, lint, + and typecheck categories are correct and common. Put editor-only or advisory tools, + optional linters, and known-red or flaky suites in `stack_summary` as context, never in + verification. Do not list a command that `stack_summary` describes as not a CI gate, red, + or flaky. 3. **Recommend the skills an implementer will need to build here.** Pick from the catalog below — do not invent skill names. A skill belongs in `recommended_skills` only when its diff --git a/backend/druks/contrib/ship/templates/verification_block.md b/backend/druks/contrib/ship/templates/verification_block.md index f120be59..c668e04a 100644 --- a/backend/druks/contrib/ship/templates/verification_block.md +++ b/backend/druks/contrib/ship/templates/verification_block.md @@ -9,10 +9,10 @@ {% if not has_commands -%} No lint / typecheck / test / smoke commands are configured for this repo. Take verification conventions from the repo's AGENTS.md if it has one; otherwise the evaluator is acceptance-criteria-driven. Never invent commands (pytest, npm test, …) the project doesn't actually use. {% else -%} -{% for section in sections if section.commands -%} +{% for section in sections if section.command_entries -%} **{{ section.label }}:** -{% for command in section.commands -%} -- `{{ command }}` +{% for entry in section.command_entries -%} +- `{{ entry.command }}`{{ " — CI: `" ~ entry.ci_check ~ "`" if entry.ci_check else "" }} {% endfor %} {% endfor -%} {% endif -%} diff --git a/backend/druks/contrib/ship/workflows.py b/backend/druks/contrib/ship/workflows.py index 2497ca2d..dd978a94 100644 --- a/backend/druks/contrib/ship/workflows.py +++ b/backend/druks/contrib/ship/workflows.py @@ -427,7 +427,20 @@ async def run(self, repo_id: int, refresh_only: bool = False) -> None: policy = await RepoPolicy.resolve(project_repo.full_name) effective = dict(baseline) if policy.verification: - effective["verification"] = policy.verification.model_dump(mode="json") + effective["verification"] = { + "test_commands": [ + {"command": command, "ci_check": None} + for command in policy.verification.test_commands + ], + "lint_commands": [ + {"command": command, "ci_check": None} + for command in policy.verification.lint_commands + ], + "typecheck_commands": [ + {"command": command, "ci_check": None} + for command in policy.verification.typecheck_commands + ], + } project_repo.set_profile(baseline=baseline, effective=effective) async def get_workspace_kwargs(self, sandbox: "Sandbox") -> dict[str, Any]: diff --git a/backend/migrations/versions/d9f3a7c1e5b2_verification_commands_carry_ci_checks.py b/backend/migrations/versions/d9f3a7c1e5b2_verification_commands_carry_ci_checks.py new file mode 100644 index 00000000..21f2292e --- /dev/null +++ b/backend/migrations/versions/d9f3a7c1e5b2_verification_commands_carry_ci_checks.py @@ -0,0 +1,87 @@ +"""verification commands carry CI checks + +Revision ID: d9f3a7c1e5b2 +Revises: b6d9e2c4f8a1 +Create Date: 2026-08-01 00:00:00.000000 + +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "d9f3a7c1e5b2" +down_revision: str | Sequence[str] | None = "b6d9e2c4f8a1" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + for profile_name in ("baseline", "effective"): + for command_group in ("test_commands", "lint_commands", "typecheck_commands"): + path = f"{{{profile_name},verification,{command_group}}}" + op.execute( + sa.text( + f""" + UPDATE project_repos + SET profile = jsonb_set( + profile, + '{path}', + ( + SELECT jsonb_agg( + CASE jsonb_typeof(entry) + WHEN 'string' THEN jsonb_build_object( + 'command', entry #>> '{{}}', + 'ci_check', NULL + ) + ELSE entry + END + ORDER BY position + ) + FROM jsonb_array_elements(profile #> '{path}') + WITH ORDINALITY AS commands(entry, position) + ) + ) + WHERE EXISTS ( + SELECT 1 + FROM jsonb_array_elements(profile #> '{path}') AS commands(entry) + WHERE jsonb_typeof(entry) = 'string' + ) + """ + ) + ) + + +def downgrade() -> None: + for profile_name in ("baseline", "effective"): + for command_group in ("test_commands", "lint_commands", "typecheck_commands"): + path = f"{{{profile_name},verification,{command_group}}}" + op.execute( + sa.text( + f""" + UPDATE project_repos + SET profile = jsonb_set( + profile, + '{path}', + ( + SELECT jsonb_agg( + CASE jsonb_typeof(entry) + WHEN 'object' THEN entry -> 'command' + ELSE entry + END + ORDER BY position + ) + FROM jsonb_array_elements(profile #> '{path}') + WITH ORDINALITY AS commands(entry, position) + ) + ) + WHERE EXISTS ( + SELECT 1 + FROM jsonb_array_elements(profile #> '{path}') AS commands(entry) + WHERE jsonb_typeof(entry) = 'object' + ) + """ + ) + ) diff --git a/backend/tests/ship/test_build_prompts.py b/backend/tests/ship/test_build_prompts.py index 97cb64bf..4ede1df7 100644 --- a/backend/tests/ship/test_build_prompts.py +++ b/backend/tests/ship/test_build_prompts.py @@ -6,6 +6,7 @@ from druks.contrib import ship from druks.contrib.ship.journal import BuildJournal from druks.contrib.ship.models import Project, ProjectRepo +from druks.contrib.ship.policy import RepoPolicy from druks.contrib.ship.prompt_context import BuildPromptContext from druks.prompts import render_prompt from druks.workflows import FatalError @@ -68,6 +69,28 @@ async def test_build_operation_prompt_renders(template): ) +async def test_verification_profile_renders_ci_provenance_per_command(): + block = await RepoPolicy().verification_block( + profile={ + "verification": { + "lint_commands": [{"command": "ruff check .", "ci_check": "Backend / lint"}], + "typecheck_commands": [], + "test_commands": [{"command": "pytest", "ci_check": None}], + } + }, + repo=None, + ) + + assert block.splitlines() == [ + "## Verification profile", + "", + "**Lint:**", + "- `ruff check .` — CI: `Backend / lint`", + "**Tests:**", + "- `pytest`", + ] + + def test_build_prompt_context_covers_template_attrs(): # Every build prompt reads build.; assert BuildPromptContext carries them # all, so a template ref can never outrun the context contract. diff --git a/backend/tests/ship/test_contracts.py b/backend/tests/ship/test_contracts.py index d5020d67..142e2fe6 100644 --- a/backend/tests/ship/test_contracts.py +++ b/backend/tests/ship/test_contracts.py @@ -47,6 +47,34 @@ def test_output_contract_is_strict(model): assert not missing, f"{model.__name__}: non-required properties {missing} break strict mode" +def test_repo_profiler_output_maps_verification_commands_to_plain_dicts(): + output = O.RepoProfilerOutput.model_validate( + { + "languages": ["python"], + "frameworks": ["django"], + "package_managers": ["uv"], + "stack_summary": "A Django backend.", + "test_commands": [{"command": "pytest", "ci_check": "Backend / tests"}], + "lint_commands": [{"command": "ruff check .", "ci_check": None}], + "typecheck_commands": [], + "recommended_skills": ["django-patterns"], + } + ) + + assert output.to_result() == { + "languages": ["python"], + "frameworks": ["django"], + "package_managers": ["uv"], + "stack_summary": "A Django backend.", + "verification": { + "test_commands": [{"command": "pytest", "ci_check": "Backend / tests"}], + "lint_commands": [{"command": "ruff check .", "ci_check": None}], + "typecheck_commands": [], + }, + "recommended_skills": ["django-patterns"], + } + + def _implementation(**overrides): fields = { "type": "result", diff --git a/backend/tests/ship/test_profiling.py b/backend/tests/ship/test_profiling.py index cfd9c667..ef597e3f 100644 --- a/backend/tests/ship/test_profiling.py +++ b/backend/tests/ship/test_profiling.py @@ -1,9 +1,9 @@ import pytest -from druks.contrib.ship.contracts import RepoProfilerOutput from druks.contrib.ship.extension import Ship from druks.contrib.ship.models import Project, ProjectRepo from druks.contrib.ship.policy import RepoPolicy, VerificationProfile from druks.contrib.ship.workflows import Profile +from druks.durable.engine import configure_engine from druks.skills.datastructures import InstalledSkill from druks.skills.models import SkillCollection @@ -12,8 +12,6 @@ def _passthrough_step(monkeypatch, druks_db): # run() is itself a durable step (single-operation workflow) — route it # straight through so the test needs no live DBOS runtime. - from druks.durable.engine import configure_engine - configure_engine(druks_db.connection()) async def _run_step(_options, fn): @@ -50,8 +48,8 @@ def _profiled(**overrides) -> dict: "package_managers": ["uv"], "stack_summary": "A Django backend.", "verification": { - "test_commands": ["pytest"], - "lint_commands": ["ruff check ."], + "test_commands": [{"command": "pytest", "ci_check": "Backend / tests"}], + "lint_commands": [{"command": "ruff check .", "ci_check": "Backend / lint"}], "typecheck_commands": [], }, "recommended_skills": ["django-patterns"], @@ -64,20 +62,6 @@ async def _no_policy(repo): return RepoPolicy() -def test_profiler_output_maps_onto_the_stored_shape(): - output = RepoProfilerOutput( - languages=["python"], - frameworks=["django"], - package_managers=["uv"], - stack_summary="A Django backend.", - test_commands=["pytest"], - lint_commands=["ruff check ."], - typecheck_commands=[], - recommended_skills=["django-patterns"], - ) - assert output.to_result() == _profiled() - - @pytest.mark.parametrize("refresh_only", [False, True]) async def test_dispatch_shapes_the_profile_start(druks_db, monkeypatch, refresh_only): repo = _seed_repo() @@ -112,7 +96,9 @@ async def _profiler(*, repo: str): repo = ProjectRepo.get(repo.id) assert repo.profile["baseline"]["languages"] == ["python"] - assert repo.effective_profile["verification"]["lint_commands"] == ["ruff check ."] + assert repo.effective_profile["verification"]["lint_commands"] == [ + {"command": "ruff check .", "ci_check": "Backend / lint"} + ] async def test_drops_skills_that_are_not_enabled(self, druks_db, monkeypatch): _seed_skills("django-patterns", "retired-skill", disabled=("retired-skill",)) @@ -148,10 +134,14 @@ async def _pinning_policy(repo): repo = ProjectRepo.get(repo.id) # The pin replaces the whole verification section on the effective profile... - assert repo.effective_profile["verification"]["test_commands"] == ["make test"] + assert repo.effective_profile["verification"]["test_commands"] == [ + {"command": "make test", "ci_check": None} + ] assert repo.effective_profile["verification"]["lint_commands"] == [] # ...but the detected baseline is preserved underneath it. - assert repo.profile["baseline"]["verification"]["lint_commands"] == ["ruff check ."] + assert repo.profile["baseline"]["verification"]["lint_commands"] == [ + {"command": "ruff check .", "ci_check": "Backend / lint"} + ] class TestRefreshOnly: @@ -173,5 +163,9 @@ async def _pinning_policy(repo): repo = ProjectRepo.get(repo.id) # Baseline untouched — only the pin re-applies. - assert repo.profile["baseline"]["verification"]["test_commands"] == ["pytest"] - assert repo.effective_profile["verification"]["test_commands"] == ["make test"] + assert repo.profile["baseline"]["verification"]["test_commands"] == [ + {"command": "pytest", "ci_check": "Backend / tests"} + ] + assert repo.effective_profile["verification"]["test_commands"] == [ + {"command": "make test", "ci_check": None} + ] diff --git a/frontend/src/extensions/ship/projects/ProjectsPage.tsx b/frontend/src/extensions/ship/projects/ProjectsPage.tsx index 8363b94d..8692cc6d 100644 --- a/frontend/src/extensions/ship/projects/ProjectsPage.tsx +++ b/frontend/src/extensions/ship/projects/ProjectsPage.tsx @@ -447,9 +447,9 @@ function ProfilePanel({
verification
- {verification.map((command) => ( - - {command} + {verification.map((entry) => ( + + {entry.command} ))}
diff --git a/frontend/src/extensions/ship/projects/types.ts b/frontend/src/extensions/ship/projects/types.ts index e1d43656..337e770f 100644 --- a/frontend/src/extensions/ship/projects/types.ts +++ b/frontend/src/extensions/ship/projects/types.ts @@ -8,9 +8,9 @@ export interface RepoProfile { frameworks?: string[] package_managers?: string[] verification?: { - test_commands?: string[] - lint_commands?: string[] - typecheck_commands?: string[] + test_commands?: { command: string; ci_check: string | null }[] + lint_commands?: { command: string; ci_check: string | null }[] + typecheck_commands?: { command: string; ci_check: string | null }[] } recommended_skills?: string[] }