From 7c536b2e7b803f5c18dfa81eb3334d285581abab Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Mon, 19 Oct 2026 15:51:33 +0200 Subject: [PATCH] feat(queue): opt-in strict_files refuses patches outside declared files (ticket-466) Refs #726. Co-Authored-By: Claude Opus 5.5 --- project/ticket-466/README.md | 40 +++++++++++++ project/ticket-466/intent.json | 76 +++++++++++++++++++++++++ src/koru/queue/transaction/preflight.py | 32 +++++++++++ tests/test_strict_files.py | 41 +++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 project/ticket-466/README.md create mode 100644 project/ticket-466/intent.json create mode 100644 tests/test_strict_files.py diff --git a/project/ticket-466/README.md b/project/ticket-466/README.md new file mode 100644 index 00000000..8a2be2d6 --- /dev/null +++ b/project/ticket-466/README.md @@ -0,0 +1,40 @@ +# Ticket 466: Opt-in strict_files: refuse promotion of patches outside declared ticket files (#726) + +- **ID**: ticket-466 +- **Owner**: agent:claude +- **Status**: IN_PROGRESS +- **Workflow state**: EDIT +- **Created**: 2026-10-19 + +## Goal and scope + +Part 1 of semcod/koru#726. A run for subactor/platform ticket-455 promoted a +"verified patch" that created an undeclared, truncated `tests/` file because +the verify command passed vacuously. + +`src/koru/queue/transaction/preflight.py`: when a ticket sets +`inputs.strict_files: true` and declares `files`, `screen_promotion_preconditions` +refuses promotion with `patch_targets_outside_declared_files` and names the +undeclared targets (`undeclared_targets(plan)`). Without the opt-in nothing +changes, so existing lanes that add files keep working. +`tests/test_strict_files.py` covers refusal, `./` normalisation and the +default. + +Implemented by willman (`koru.develop_ticket`, zai/glm-5.3). Both attempts +were correct but Koru refused them: the repository's pytest `addopts` load +`wellmanifest_governance`, whose session gate fails inside Koru's nested +staging worktree (GOV-PACKAGING-003), so any pytest verify command fails there. +Patch taken from the staging worktree after review. + +Not in scope (rest of #726): `verify_not_discriminating` when the baseline +already passes; parse checks for new files. + +## Acceptance criteria + +- [x] AC-01: `tests/test_strict_files.py` → 3 passed. +- [x] AC-02: `tests/test_queue_transaction_phases.py` still passes (47 passed together). + +## Tracking boundary + +This directory contains the minimal reviewed intent. Optional participant prose +and raw command logs are not required delivery output. diff --git a/project/ticket-466/intent.json b/project/ticket-466/intent.json new file mode 100644 index 00000000..ed03f3da --- /dev/null +++ b/project/ticket-466/intent.json @@ -0,0 +1,76 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-466", + "summary": "Opt-in strict_files: refuse promotion of patches outside declared ticket files (#726)", + "workstream": "application", + "classification": { + "kind": "SERVICE", + "priority": "P2", + "origin": "health" + }, + "allowedPaths": [ + "project/ticket-466/**", + "TODO.md", + "project/TICKETS.md", + "src/koru/queue/transaction/preflight.py", + "tests/test_strict_files.py" + ], + "forbiddenPaths": [ + "project/ticket-*/user-*.md" + ], + "stacks": [], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "5958f94d3692f250318080f5479dc27d092db443", + "targetBranch": "main", + "outcome": "Tickets opting into inputs.strict_files cannot promote patches that touch undeclared files.", + "nonGoals": [ + "No default behaviour change for tickets without strict_files", + "No verify_not_discriminating outcome yet" + ], + "complexity": "S", + "estimatedMinutes": 25, + "budgets": { + "maxImplementationFiles": 2, + "maxAffectedComponents": 1, + "maxPublicInterfaceChanges": 1, + "maxRuntimeDependencies": 0 + }, + "architecture": { + "status": "accepted", + "decision": "Screen undeclared targets in screen_promotion_preconditions when inputs.strict_files is true.", + "components": [ + { + "name": "queue-preflight", + "paths": [ + "src/koru/queue/transaction/preflight.py", + "tests/test_strict_files.py" + ] + } + ], + "responsibilityChanges": false, + "interfaceChanges": [ + "New ticket input inputs.strict_files and outcome code patch_targets_outside_declared_files" + ], + "dataChanges": [], + "ui": { + "impact": "none", + "states": [], + "evidence": [] + }, + "rollback": "Revert the commit." + }, + "runtimeDependencies": [], + "validation": [ + { + "criterion": "AC-01", + "commands": [ + "python3 -m pytest -q tests/test_strict_files.py tests/test_queue_transaction_phases.py" + ], + "evidence": "project/ticket-466/README.md" + } + ] + } +} diff --git a/src/koru/queue/transaction/preflight.py b/src/koru/queue/transaction/preflight.py index c88b2795..8295844f 100644 --- a/src/koru/queue/transaction/preflight.py +++ b/src/koru/queue/transaction/preflight.py @@ -40,6 +40,10 @@ from koru.queue.types import CommandResult +#: A ticket opted into ``inputs.strict_files`` and the patch touches files it did not declare. +PATCH_TARGETS_OUTSIDE_DECLARED_FILES = "patch_targets_outside_declared_files" + + def extract_patch( result: CommandResult, *, @@ -334,8 +338,36 @@ def run_patch_preflight( return build_patch_plan(project, ticket, diff, manifest, proposal=proposal), None +def undeclared_targets(plan: PatchPlan) -> tuple[str, ...]: + """Targets outside ``ticket.files`` when the ticket opted into strict files.""" + ticket = plan.ticket if isinstance(plan.ticket, dict) else {} + inputs = ticket.get("inputs") + if not isinstance(inputs, dict) or inputs.get("strict_files") is not True: + return () + declared = { + str(rel).replace("\\", "/").removeprefix("./") for rel in (ticket.get("files") or []) + } + if not declared: + return () + return tuple( + sorted( + t for t in plan.targets if t.replace("\\", "/").removeprefix("./") not in declared + ) + ) + + def screen_promotion_preconditions(plan: PatchPlan) -> PatchOutcome | None: """Refuse a promotion the workspace cannot honour cleanly.""" + outside = undeclared_targets(plan) + if outside: + return PatchOutcome( + code=PATCH_TARGETS_OUTSIDE_DECLARED_FILES, + message=( + "inputs.strict_files is set and the patch touches files the ticket " + f"does not declare: {', '.join(outside)}. Declare them in ticket.files " + "or drop them from the patch." + ), + ) if plan.mode != PROMOTION_COMMIT or repository_is_clean(plan.project): return None return PatchOutcome( diff --git a/tests/test_strict_files.py b/tests/test_strict_files.py new file mode 100644 index 00000000..afdea6c4 --- /dev/null +++ b/tests/test_strict_files.py @@ -0,0 +1,41 @@ +"""inputs.strict_files refuses promotion of patches outside ticket.files (semcod/koru#726).""" +from __future__ import annotations + +from pathlib import Path +from types import SimpleNamespace + +from koru.queue.transaction.preflight import ( + PATCH_TARGETS_OUTSIDE_DECLARED_FILES, + screen_promotion_preconditions, + undeclared_targets, +) + + +def _plan(ticket: dict, targets: tuple[str, ...]): + return SimpleNamespace( + project=Path("/nonexistent"), ticket=ticket, targets=targets, mode="branch" + ) + + +def test_strict_files_refuses_undeclared_target() -> None: + plan = _plan( + {"files": ["test/a.mjs"], "inputs": {"strict_files": True}}, + ("test/a.mjs", "tests/a.mjs"), + ) + assert undeclared_targets(plan) == ("tests/a.mjs",) + refusal = screen_promotion_preconditions(plan) + assert refusal is not None + assert refusal.code == PATCH_TARGETS_OUTSIDE_DECLARED_FILES + assert "tests/a.mjs" in refusal.message + + +def test_strict_files_accepts_declared_targets_and_dot_prefix() -> None: + plan = _plan({"files": ["./src/x.py"], "inputs": {"strict_files": True}}, ("src/x.py",)) + assert undeclared_targets(plan) == () + assert screen_promotion_preconditions(plan) is None + + +def test_without_opt_in_extra_targets_are_not_refused() -> None: + plan = _plan({"files": ["src/x.py"], "inputs": {}}, ("src/x.py", "src/y.py")) + assert undeclared_targets(plan) == () + assert screen_promotion_preconditions(plan) is None