Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions project/ticket-466/README.md
Original file line number Diff line number Diff line change
@@ -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.
76 changes: 76 additions & 0 deletions project/ticket-466/intent.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
}
32 changes: 32 additions & 0 deletions src/koru/queue/transaction/preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
*,
Expand Down Expand Up @@ -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(
Expand Down
41 changes: 41 additions & 0 deletions tests/test_strict_files.py
Original file line number Diff line number Diff line change
@@ -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
Loading