From 3439941881b9bdc822af4e38cd70db3d7c5ea767 Mon Sep 17 00:00:00 2001 From: "jerod.wilkerson" <30474318+jerodw@users.noreply.github.com> Date: Sat, 15 Aug 2026 13:08:01 -0600 Subject: [PATCH] story-040: No target-stack literal in harness source Implemented by the l5 harness story workflow. --- .harness/docs/ARCHITECTURE.md | 20 + orchestration/harness_source.py | 259 +++++++ tests/test_clean_clone_check.py | 17 + .../test_no_target_stack_in_harness_source.py | 682 ++++++++++++++++++ tests/test_stage_output_ownership.py | 19 +- 5 files changed, 995 insertions(+), 2 deletions(-) create mode 100644 orchestration/harness_source.py create mode 100644 tests/test_no_target_stack_in_harness_source.py diff --git a/.harness/docs/ARCHITECTURE.md b/.harness/docs/ARCHITECTURE.md index 836fb7e..7cecadf 100644 --- a/.harness/docs/ARCHITECTURE.md +++ b/.harness/docs/ARCHITECTURE.md @@ -104,6 +104,7 @@ The drift source that paragraph used to name is closed: `planner.md` no longer s The fourth, **`assignment_problems(story, stages)`**, is plan-time-only for exactly the same reason and is otherwise unlike its neighbour — which is why the module docstring now distinguishes the two rather than blending them. It reports one problem per `technical_plan.likely_file_changes` entry whose `file` falls under a prefix the entry's own `stage` is restricted from creating under, unless a grant on that stage covers the file. It is called from `artifact_problems` beside `strictness_problems`, below the `read_story` gate, so the existing order is unchanged. Both halves come off `stage_restrictions` and the grant is decided by `grant_covers`, so the module still names no stage and no prefix; a story with no `technical_plan`, and an entry missing `file` or `stage`, yield nothing rather than raising. It is **structural** — two literals compared, one in the artifact and one in the workflow definition — and inherits none of the third check's hedging: no clause split, no vocabulary of scoping words, no paraphrase that evades it, nothing it declines to decide. See "Work a stage cannot own" below for why the subject is `likely_file_changes` and what the check costs. The fifth, **`naming_problems(story)`**, is plan-time-only for exactly the reason the third and the fourth are — committed artifacts that have already run name their modules for story numbers, and refusing them at pre-flight would make those stories unrunnable. It reports one problem per `technical_plan.likely_file_changes` entry whose `file` has a basename matching `STORY_NUMBERED_MODULE` (`^test_story_\d+`), and the message says what to name the module instead rather than only that the name is wrong. Its subject is `likely_file_changes` for the reason `assignment_problems` takes it: that is where a plan states which files it expects written, so a name is decidable there, before anything exists under it. It is **structural** like its neighbour — one literal in the artifact against one pattern — and inherits none of the third check's hedging. It is the only one of the five that reads no workflow, so it takes `story` alone; a story with no `technical_plan`, and an entry with no `file`, yield nothing rather than raising. See "A test module is named for what it checks" below for the convention it holds and the two other mechanisms holding it. +- `harness_source.py` — a **declaration the suite asserts against**, added by story-040, and the one module here that no run-time path reads. It declares where the harness's own source lives (`HARNESS_SOURCE_DIRS`: orchestration, prompts, workflows, schemas, rules, hooks, scripts, templates), which of those a target reads or describes (`TARGET_FACING_DIRS`: prompts and workflows), the tokens naming a language or toolchain (`STACK_TOKENS`), the directory shapes a target's test layout would use (`TARGET_LAYOUT_PATHS`), the one file exempt from its own scan (`EXEMPT_FILES`), and the directory names that are not source (`SKIP_DIR_NAMES`). `scan(root=None)` walks the declared directories of a repository root — this repository by default, resolved relative to the module the way `schema_validator` resolves `schemas/`, and a throwaway one built by a test otherwise, so both go through one code path — and returns sorted frozen `Finding(path, line_number, line, token, rule)` records. A directory the root does not have is skipped rather than raising, so a throwaway root need only carry what a test is about. Two rules: `STACK_RULE` reads all eight directories, `LAYOUT_RULE` reads `TARGET_FACING_DIRS` only. The coordinator does not call it, nothing routes on it, and no run is refused because of what it reports — see "No target-stack literal in harness source" below for what it is for and what it does not catch. - `run_status.py` — read-only status snapshot backing `l5-status`. Lists every run under the configured runs directory (story id, status, current stage, retry count, sorted by story id) or shows one run's full `RunState` plus the last 10 lines of its `events.log`. Reuses `story_coordinator.load_state` for state parsing (never duplicated); a run with a missing or unparseable `state.json` is flagged `unreadable` in the listing without aborting it, while the detail view fails loudly (stderr, exit 1). Never writes to run directories or anywhere else. ### Tool allowlist @@ -402,6 +403,24 @@ story-031 escalated seventeen minutes into its implementer on a conflict that wa **This story's own plan was not governed by the check it adds**, for the eighth time in this pattern's history, and the cause is a new one worth naming: the artifact was written and committed by an `l5-plan` session that ran before `assignment_problems` existed. It happens to be clean — it assigns its test module to the stage that owns validation, which is the convention this story writes down — but it was not checked. Enforcement begins with the next plan, which is one step earlier than the stale-workflow and stale-import cases, since the artifact rather than the run is what escapes. +## No target-stack literal in harness source + +The harness runs against any repository. Its only tie to a target's language, toolchain or directory layout belongs in `.harness/`, which is that target's own configuration. An audit on 2026-08-15 found five ties outside it: a Python snippet executed as a version probe and a configuration key whose name is a language, both in `orchestration/story_coordinator.py`; the `may_not_create` line of `workflows/story-workflow.json`; and two lines of prose in `prompts/tester.md` naming a pytest layout. + +**The rule is a scan, not a paragraph, and the choice has a record.** Two rules of exactly this shape were written down here before: the `git diff HEAD` baseline rule was documented *and* injected into every stage prompt and shipped five more times, and the git-history-loader rule was documented and shipped three more times. Both stopped when a scan landed. Two of the five ties also sit in a prompt, which no fixture reaches — `prompts/tester.md` is prose an agent reads — so "every configurable value is proven configurable" could vary every key in existence and still not notice a pytest filename sitting in it. Only a reader or a scan sees that, and readers had missed it since the file was written. + +**Where the two halves live.** The declaration and the scan are `orchestration/harness_source.py` (above); the judgement is `tests/test_no_target_stack_in_harness_source.py`. The module reports mentions and judges none of them — the test module holds **two allowlists**, keyed by repository-relative path and the exact text of the matched line rather than by line number, so an unrelated edit above a tie does not churn the list and read as burn-down. `TEMPORARY_TIES` (36 entries) holds the mentions that name or assume a target's stack or layout. `PERMANENT_MENTIONS` (9 entries) holds the mentions that are the opposite of a tie — a docstring saying every scalar `story_parser` produces is a Python `str` is a fact about this code, and the coordinator docstring saying a target's test command need not be a Python interpreter is the harness declining the assumption — and **every entry carries a one-line reason**, so the permanent half is a judgement on the record rather than a suppression list. The two together equal exactly what the scan reports against this repository (45 distinct `(path, line text)` pairs from 46 findings — `prompts/tester.md:47` matches both rules), asserted in both directions and asserted disjoint. The classification rule sits in the module docstring beside the declaration so the next one is not re-argued. + +**Why they are two lists and not one.** `TEMPORARY_TIES` reaching empty is the completion signal for `.harness/requests/the-interpreter-is-not-assumed-to-be-python.md` and `.harness/requests/the-test-location-comes-from-configuration.md` — the two stories queued behind this one, which fix the ties this one grandfathers. The artifact-schema mentions of `python`, `python_version` and `clean_clone_python` in `schemas/clean-clone-result.schema.json`, `schemas/revert-check-result.schema.json` and `schemas/harness-config.schema.json` are on the temporary list for that reason: the interpreter story names those schemas and they burn down with it. Merge the two lists and the signal is gone — a list that stops shrinking cannot be told from work that finished. + +**story-040 fixes none of the five**, deliberately. `orchestration/story_coordinator.py`, `workflows/story-workflow.json` and `prompts/tester.md` carry no edit on its branch, and no run-time behaviour changed. + +**What the scan does not catch, stated where a reader meets it.** `STACK_TOKENS` is a guess about languages nobody has tried and is incomplete by construction — a Ruby `Gemfile` or an Elixir `mix.exs` is reported by nothing here, which the suite demonstrates by planting both and finding them invisible. It is worth having anyway because it catches the *shape* of the mistake, which recurs, rather than every instance of it. The layout half **cannot read `orchestration/` at all**: this repository's own suite is called `tests/`, so in Python source a `tests/` literal cannot be told from an honest reference to it, and the half therefore reads only the target-facing files where a path can only mean a target's. `.harness/` and `tests/` are outside the scan entirely. The module is exempt from its own scan **by name and by nothing else** — the token list would otherwise report itself — and a second file carrying the same tokens is reported. And the scan is not tamper-proof: deleting it alongside a forced repair is not caught here at any granularity, and no assertion in the module implies otherwise. Every one of those limits is asserted by reading the module's text rather than by trusting it to be there. + +**The matcher's boundary is "not a letter or a digit", not `\b`.** That is what makes `clean_clone_python` and `platform.python_version()` visible — `_` and `.` are not alphanumeric — while `pipeline` and `pipe` are not, because `e` is; `\b` inverts both. The alternation is longest-token-first so `python3` claims its own match rather than being read as `python` followed by a digit, which the boundary would then reject and hide entirely. The layout pattern keeps the left boundary and drops the right one, because each path already ends in a slash and requiring a non-alphanumeric after it would hide `tests/conftest.py` — the exact shape being looked for. All four constructed cases are run through `scan` rather than reasoned about. + +**Every absence assertion is paired with a demonstration it can fail.** A tie planted in a file that is clean today is reported and turns the equality assertion red; a list entry left behind after its tie is removed turns it red the other way; the identical text that is invisible under `.harness/`, under `tests/` and in `orchestration/` is reported when planted in `prompts/`; the exemption is paired with a twin file carrying the module's full text, which *is* reported. All of it runs against a throwaway root rather than by editing this repository, in the style `tests/test_schema_inventory_location.py` established. + ## Run directory anatomy .harness/runs/story-001/ @@ -580,5 +599,6 @@ It is narrower than Chapter 18's **checkpoints**, and the difference is the reas - **Two literals, two homes, held equal by a test.** `STORY_NUMBERED_MODULE = re.compile(r"^test_story_\d+")` is defined in both `orchestration/plan_validation.py` and `tests/test_baseline_honesty.py`, because the plan-time refusal ships with the harness and the scan belongs to the suite; neither may import the other. `tests/test_validation_module_naming.py` holds the two to the same verdict over a shared name set, which is what stops the plan-time refusal and the standing scan from drifting into two conventions. If a third reader appears, that is the point at which the pattern should get one home rather than a third copy. - **A reference naming a path at a pinned historical revision keeps its historical spelling.** The rename sweep rewrote only references naming a file *as it exists now* — in `tests/`, `orchestration/`, `scripts/`, `prompts/` and this document. A reference that reads a path's text at a pinned revision was left spelled as it was there, because that is the name the object has at that revision and rewriting it breaks the read; `tests/test_baseline_honesty.py`'s regression set and `STORY_ORIGINS` itself are full of these. Nothing under `.harness/stories/` or `.harness/runs-archive/` was touched: both record what was true when they were written, and history describing the past accurately is not drift. - **A suite count recorded in a story is a measurement of the tree it was taken from, and the tree moves.** story-038's acceptance criterion recorded 2002 tests "before the rename", and reconciling the finished suite against it took two attempts. The number was correct when taken — the plan commit `abce051` collects exactly 2002 — but `426fe1f` ("Grant the verifier a self-route budget of 1") landed between the plan and the work, moving one declaration out of `BUDGETLESS` into `BUDGETED` and shrinking a parametrization by 3, so the branch base collects 1999. Every item of the difference to the finished count is accounted for by module: +3 for the naming scan and its two controls, +109 for this story's own validation. **The reconciliation that settles it is per-module collection counts between the two trees, not a total**, since a total can absorb a lost module against an added one; here 32 of the 34 renamed modules collect an identical count and the two that differ are the two named. Five test *names* disappeared and all five are merge disambiguations rather than deletions — two collisions in the story-008/story-009 merge renamed, three in the story-014/story-033 merge suffixed `_by_story_033`. When a story records a count, record the revision it was measured at with it; when a later stage cannot reproduce it, compare per module before concluding anything was lost. +- **story-040 forced the standing-assertion repair pattern twice more, and the collision was unavoidable rather than incidental.** A module whose deliverable is to declare `tests/` and `pytest` cannot do it without spelling them, so `tests/test_stage_output_ownership.py::test_no_path_prefix_is_named_in_orchestration_code` and `tests/test_clean_clone_check.py::test_no_test_command_string_appears_in_orchestration_code` both went red on `orchestration/harness_source.py` the moment it existed. Each was repaired the way the bullet above prescribes — a by-name exemption held shut from both sides: the exempt module must exist *and* must actually contain the literal, or the exemption is stale and the test goes red; every other module is held to the original assertion unchanged; and neither exemption widens past the one literal it is about, so `harness_source.py` is still held to `may_not_create` in the first and to `-m pytest` and `unittest` in the second. Both repairs were folded into the existing test functions rather than added as new ones, because `tests/test_shared_baseline_resolution.py` pins `test_stage_output_ownership.py`'s test-name set exactly. No assertion was weakened, skipped or deleted and no test function was added or removed — the pre-story total of 2196 was unchanged by the repairs, and the finished suite is 2248 with story-040's own 52 cases. **The general shape worth carrying forward:** a story that adds a *declaration* of forbidden literals will collide with every standing scan that forbids them, and the collision is the declaration working rather than a defect in either. - Verification rules never change between retries; retries narrow scope, they do not restart the workflow. - Capacity exhaustion (rate limits) is a reason to wait, not to fail; budget ceilings are a reason to stop. diff --git a/orchestration/harness_source.py b/orchestration/harness_source.py new file mode 100644 index 0000000..71dd5f9 --- /dev/null +++ b/orchestration/harness_source.py @@ -0,0 +1,259 @@ +"""Declare where the harness's own source lives, and scan it for ties to a target's stack. + +The harness is meant to run against any repository. Its only tie to a +target's language, toolchain or directory layout belongs in `.harness/`, +which is that target's own configuration. A tie anywhere else -- a Python +snippet executed as a probe, a configuration key whose name is a language, +a workflow restriction naming a directory, a sentence of prose naming a +pytest layout -- is the harness assuming what it is running against. + +This module is a **declaration the suite asserts against**. Nothing in +orchestration calls it: the coordinator does not run it, no run-time +behaviour reads it, and no run is refused because of what it reports. It +exists so that `tests/test_no_target_stack_in_harness_source.py` can hold +two allowlists against what the scan actually finds, and so that the list +of known ties burns down measurably rather than by assertion. + +Prose was tried twice for rules of this shape in this repository -- the +`git diff HEAD` baseline rule and the git-history-loader rule were both +written down, injected into prompts, and shipped again five and three more +times respectively. Both stopped when a scan landed. A scan is therefore +the deliverable here, and this docstring is the pointer to the mechanism +rather than the mechanism. + +Two rules +--------- + +`STACK_RULE` looks for a token naming a language or toolchain +(`STACK_TOKENS`) anywhere under `HARNESS_SOURCE_DIRS`. + +`LAYOUT_RULE` looks for a path shaped like a target's test layout +(`TARGET_LAYOUT_PATHS`) under `TARGET_FACING_DIRS` only. + +Classifying what the scan reports +--------------------------------- + +The scan reports mentions; it does not judge them, and the judgement is the +suite's two lists. A mention is a **temporary tie** when it names or +assumes a target's stack or layout -- something a target-agnostic harness +should not be saying. It is a **permanent mention** when it describes the +harness's *own* implementation language, or explicitly says a target need +*not* be Python. A docstring saying every scalar this parser produces is a +Python `str` is a fact about this code; a sentence saying a target's test +command need not be a Python interpreter is the opposite of a tie. The two +lists stay separate because only the temporary one is a burn-down: merged, +a list that stops shrinking cannot be told from work that finished. + +What this does not catch +------------------------ + +`STACK_TOKENS` is **a guess about languages nobody has tried, and it will +be incomplete**. It is written from the toolchains this repository's +authors happen to know, so a tie naming a language absent from the list -- +a Ruby `Gemfile`, an Elixir `mix.exs`, a toolchain invented after this was +written -- is reported by nothing here. The list is worth having anyway +because it catches the *shape* of the mistake, which recurs, rather than +every instance of it. Widen it when a new tie teaches a new token. + +The layout half **cannot read `orchestration/` at all**, and that is a +deliberate limit rather than an oversight. This harness's own suite is +called `tests/`, so in Python source a `tests/` literal cannot be told from +an honest reference to this repository's own test directory. The layout +half therefore reads only the target-facing files -- prompts and workflow +definitions -- where a path can only mean a target's. A `tests/` literal in +`orchestration/` is invisible to this scan and always will be. + +`.harness/` and `tests/` are outside the scan entirely. A target's ties +belong in `.harness/`, and this repository's own suite is legitimately full +of Python. + +This module is **exempt from its own scan by name**: `EXEMPT_FILES` holds +`orchestration/harness_source.py` and nothing else, because the token list +declared here would otherwise report itself on every line. The exemption is +by name and covers no other file -- a second file carrying these same +tokens is reported like any other. + +The scan is not tamper-proof and does not claim to be. Deleting it is not +caught here. +""" +from __future__ import annotations + +import re +from dataclasses import dataclass +from pathlib import Path + +# The scan runs against this repository by default, resolved relative to +# this module the way schema_validator resolves schemas/ -- the harness +# source ships with the harness code, not with a target's .harness/. +HARNESS_ROOT = Path(__file__).resolve().parents[1] + +# The directories holding the harness's own source. Everything the scan +# reads lives under one of these; .harness/ and tests/ are deliberately +# absent, per the module docstring. +HARNESS_SOURCE_DIRS: tuple[str, ...] = ( + "orchestration", + "prompts", + "workflows", + "schemas", + "rules", + "hooks", + "scripts", + "templates", +) + +# The subset of the above that a target reads or that describes a target's +# repository: prompts an agent is given, and the workflow definition whose +# declarations name paths in the target. A path literal here can only mean +# a target's, which is what makes the layout rule decidable in them. +TARGET_FACING_DIRS: tuple[str, ...] = ("prompts", "workflows") + +# Tokens naming a language or toolchain. A guess, and incomplete by +# construction -- see "What this does not catch" above. +STACK_TOKENS: tuple[str, ...] = ( + "python", + "python3", + "pytest", + "venv", + "conftest", + "pip", + "npm", + "jest", + "go.mod", + "cargo", + "gradle", + "tox", +) + +# Directory shapes a target's test layout would use. Each ends in a slash, +# which is what makes it a path rather than a word. +TARGET_LAYOUT_PATHS: tuple[str, ...] = ( + "tests/", + "test/", + "spec/", + "specs/", + "__tests__/", +) + +# Exempt by name, and covering nothing else: this module declares the token +# list, so it would otherwise report itself on every line of it. +EXEMPT_FILES: tuple[str, ...] = ("orchestration/harness_source.py",) + +# Compiled artifacts are not source and carry no judgement. +SKIP_DIR_NAMES: frozenset[str] = frozenset({"__pycache__"}) + +STACK_RULE = "stack-token" +LAYOUT_RULE = "target-layout" + + +@dataclass(frozen=True) +class Finding: + """One matched line. `line` is the text so a caller can key on it. + + Keying on the text rather than on `line_number` is what lets an + allowlist survive an unrelated edit above a match; the line number is + carried anyway so a failure message can name it. + """ + + path: str + line_number: int + line: str + token: str + rule: str + + +def _stack_pattern(tokens: tuple[str, ...]) -> re.Pattern[str]: + """One alternation, longest token first, bounded by a non-alphanumeric. + + Bounding on "not a letter or a digit" rather than on a word boundary is + the choice that makes an identifier-embedded token visible: + `clean_clone_python` and `platform.python_version()` are seen, because + `_` and `.` are not letters or digits, while `pipeline` and `pipe` are + not, because `e` is. `\\b` would do the opposite on both counts. + + Longest first so that `python3` claims its own match rather than being + read as `python` followed by a digit -- which the boundary would then + reject, hiding it entirely. + """ + ordered = sorted(tokens, key=len, reverse=True) + alternation = "|".join(re.escape(token) for token in ordered) + return re.compile(rf"(? re.Pattern[str]: + """The same left boundary, and no right boundary. + + Each path already ends in a slash, so it delimits itself on the right; + requiring a non-alphanumeric after it would hide `tests/conftest.py`, + which is exactly the shape being looked for. + """ + ordered = sorted(paths, key=len, reverse=True) + alternation = "|".join(re.escape(path) for path in ordered) + return re.compile(rf"(? list[Path]: + """Every readable file under the named directories of `root`, sorted.""" + found: list[Path] = [] + for name in directories: + directory = root / name + if not directory.is_dir(): + continue + for path in directory.rglob("*"): + if not path.is_file(): + continue + if SKIP_DIR_NAMES.intersection(path.parts): + continue + found.append(path) + return sorted(found) + + +def _lines(path: Path) -> list[str] | None: + """The file's lines, or None when it is not text this scan can read.""" + try: + return path.read_text(encoding="utf-8").splitlines() + except (UnicodeDecodeError, OSError): + return None + + +def scan(root: Path | None = None) -> list[Finding]: + """Every tie the two rules find under `root`, sorted deterministically. + + `root` is a repository root: this repository by default, and a + throwaway one built by a test otherwise, so the same code path decides + both. A directory the root does not have is skipped rather than raising + -- a throwaway root need only carry what the test is about. + + A line matching both rules yields one finding per rule, each carrying + the token that rule matched; within a rule, a line yields one finding + carrying its first match. + """ + base = (root or HARNESS_ROOT).resolve() + exempt = {base / name for name in EXEMPT_FILES} + + findings: list[Finding] = [] + for path in _scanned_files(base, HARNESS_SOURCE_DIRS): + if path in exempt: + continue + lines = _lines(path) + if lines is None: + continue + relative = path.relative_to(base).as_posix() + target_facing = path.relative_to(base).parts[0] in TARGET_FACING_DIRS + for number, line in enumerate(lines, start=1): + stack = STACK_PATTERN.search(line) + if stack is not None: + findings.append( + Finding(relative, number, line, stack.group(1), STACK_RULE) + ) + if not target_facing: + continue + layout = LAYOUT_PATTERN.search(line) + if layout is not None: + findings.append( + Finding(relative, number, line, layout.group(1), LAYOUT_RULE) + ) + return sorted(findings, key=lambda f: (f.path, f.line_number, f.rule, f.token)) diff --git a/tests/test_clean_clone_check.py b/tests/test_clean_clone_check.py index 1f2026d..2e89e5e 100644 --- a/tests/test_clean_clone_check.py +++ b/tests/test_clean_clone_check.py @@ -397,7 +397,24 @@ def test_a_gitignored_interpreter_directory_is_available_in_the_clone( def test_no_test_command_string_appears_in_orchestration_code(): + """story-040 adds the one module whose deliverable *is* to name a + toolchain: harness_source.py declares the stack tokens its scan looks + for, `pytest` among them, and cannot declare them without spelling + them. It is exempt by name, and the exemption is held shut from both + sides — that module must actually name `pytest`, or the exemption is + stale, and it is still held to the other two fragments — so no other + module gains any latitude.""" + declares_the_tokens = "harness_source.py" + declaring = REPO_ROOT / "orchestration" / declares_the_tokens + assert declaring.is_file(), declares_the_tokens + declared = declaring.read_text(encoding="utf-8") + assert "pytest" in declared + for fragment in ("-m pytest", "unittest"): + assert fragment not in declared, f"{declares_the_tokens} names {fragment}" + for source in (REPO_ROOT / "orchestration").glob("*.py"): + if source.name == declares_the_tokens: + continue text = source.read_text(encoding="utf-8") for fragment in ("pytest", "-m pytest", "unittest"): assert fragment not in text, f"{source.name} names {fragment}" diff --git a/tests/test_no_target_stack_in_harness_source.py b/tests/test_no_target_stack_in_harness_source.py new file mode 100644 index 0000000..b5f4e4d --- /dev/null +++ b/tests/test_no_target_stack_in_harness_source.py @@ -0,0 +1,682 @@ +"""Independent validation for story-040: no target-stack literal in harness +source, held by a scan rather than by a paragraph. + +Written from the story's acceptance criteria rather than from the +implementation. The story fixes no tie. Its deliverable is a scan plus two +lists, so what is validated here is the scan's *reach* and the lists' +*exactness*, not any repair: + + * **the two lists.** `TEMPORARY_TIES` and `PERMANENT_MENTIONS` below are + asserted to equal exactly what `harness_source.scan()` reports against + this repository, in both directions, and to share no entry. Each entry + is keyed by repository-relative path and the exact text of the matched + line rather than by a line number, so an unrelated edit above a tie + does not churn the list and look like the burn-down. + * **the five audited ties.** Asserted present by name, by running the + scan rather than by reading the list — a scan that cannot see the ties + that motivated it has not been shown to work. + * **the matcher.** The four boundary cases the story names are + constructed and run through the real `scan`, not reasoned about. + * **the stated limits.** Read out of `orchestration/harness_source.py`'s + own docstring, and then *exercised* — a language absent from + `STACK_TOKENS` really is invisible, and the layout half really cannot + see `orchestration/` — so the limits are known to match the behaviour + rather than to overstate or undersell it. + +Every absence asserted here carries a demonstration that it can fail, and +every demonstration is built against a throwaway repository root rather +than by editing this one: + + * "the lists equal what the scan reports" sits beside a throwaway root + with a tie planted in a file that is clean today, where the same + comparison reports an unexpected entry, and beside one with a known tie + removed, where it reports a stale entry; + * "nothing under `.harness/` or `tests/` is reported" sits beside ties + planted in both, and beside the same tie planted in a scanned directory, + which is reported; + * "the layout half does not read `orchestration/`" sits beside the same + literal planted in a target-facing file, which is reported; + * "the declaring module is exempt" sits beside a second file carrying its + tokens, which is reported; + * "every permanent mention carries a reason" sits beside an entry with + none, which the same check reports; + * "the module states its limits" sits beside a rendering of that module + with each stated limit stripped out, which the same check reports; + * "no tie was fixed" is resolved through the shared story range in + `tests/conftest.py` and sits beside a synthetic history whose run commit + edits one of the three files, which the same comparison reports. + +Nothing here invokes a model, and nothing here writes to this repository. +""" +import ast +import shutil +from pathlib import Path + +import pytest + +import harness_source +from conftest import story_diff +from test_shared_baseline_resolution import committed_story + +REPO_ROOT = Path(harness_source.__file__).resolve().parents[1] +DECLARING_MODULE = "orchestration/harness_source.py" +VALIDATION_REL = "tests/test_no_target_stack_in_harness_source.py" + +#: The three files the story forbids itself to edit. Every tie in them is +#: grandfathered below and repaired by a later story, not by this one. +UNTOUCHED = ( + "orchestration/story_coordinator.py", + "workflows/story-workflow.json", + "prompts/tester.md", +) + + +# ========================================================================== +# The two lists +# +# An entry is (repository-relative path, exact text of the matched line). +# Keyed on the text rather than the line number so that an unrelated edit +# above a tie does not churn the list and read as burn-down progress. +# +# The split is the whole point. TEMPORARY_TIES is a burn-down: a mention +# that names or assumes a *target's* stack or layout, which the harness has +# no business saying, and which the-interpreter-is-not-assumed-to-be-python +# and the-test-location-comes-from-configuration exist to remove. This list +# reaching empty is their completion signal. PERMANENT_MENTIONS is a +# judgement on the record: a mention that describes *this harness's own* +# implementation language, or that explicitly says a target need not be +# Python, is the opposite of a tie and will never be removed. Merged, a list +# that stops shrinking could not be told from work that finished. +# ========================================================================== + + +TEMPORARY_TIES: frozenset[tuple[str, str]] = frozenset({ + # --- The version probe: a Python snippet the harness executes. ------ + ('orchestration/story_coordinator.py', + '_VERSION_PROBE = "import platform; print(platform.python_version())"'), + + # --- The clean-clone record's own Python-shaped fields. ------------- + ('orchestration/story_coordinator.py', + ' python: str'), + ('orchestration/story_coordinator.py', + ' python_version: str | None = None'), + ('orchestration/story_coordinator.py', + ' record: dict = {"ran": self.ran, "command": self.command, "python": self.python}'), + ('orchestration/story_coordinator.py', + ' "python_version": self.python_version,'), + + # --- The clean_clone_python configuration key, and the prose that + # explains it. Line 1179 is here rather than in the permanent list + # because "the oldest supported Python" is an assumption about the + # target's stack, not a statement about the harness's own. + ('orchestration/story_coordinator.py', + ' *configured* clean_clone_python that does not exist is a different case,'), + ('orchestration/story_coordinator.py', + ' clean_clone_python: str | None,'), + ('orchestration/story_coordinator.py', + ' configuration names a `clean_clone_python`, so the check can exercise the'), + ('orchestration/story_coordinator.py', + ' oldest supported Python rather than whichever one the developer works in.'), + ('orchestration/story_coordinator.py', + ' interpreter = clean_clone_python or argv[0]'), + ('orchestration/story_coordinator.py', + ' if clean_clone_python and resolved is None:'), + ('orchestration/story_coordinator.py', + ' python=interpreter,'), + ('orchestration/story_coordinator.py', + ' f"clean_clone_python names {clean_clone_python}, which is not an "'), + ('orchestration/story_coordinator.py', + ' python=interpreter,'), + ('orchestration/story_coordinator.py', + ' python_version=_interpreter_version(resolved) if resolved else None,'), + ('orchestration/story_coordinator.py', + ' config.get("clean_clone_python"),'), + ('orchestration/story_coordinator.py', + ' python = config.get("clean_clone_python") or shlex.split(command)[0]'), + ('orchestration/story_coordinator.py', + ' python=python,'), + ('orchestration/story_coordinator.py', + ' config.get("clean_clone_python"),'), + ('orchestration/story_coordinator.py', + ' python=python,'), + + # --- Two lines of prose in a prompt naming a pytest layout. --------- + ('prompts/tester.md', + 'New tests belong in tests/ and become permanent repository assets.'), + ('prompts/tester.md', + 'shared resolution in `tests/conftest.py`.'), + + # --- The artifact schemas that burn down with the interpreter story, + # which names these three files by name. + ('schemas/clean-clone-result.schema.json', + ' "required": ["ran", "command", "python"],'), + ('schemas/clean-clone-result.schema.json', + ' "description": "Whether the suite actually ran in the clone. False when the check refused to run, in which case reason says why and exit_code, output_tail and python_version are absent."'), + ('schemas/clean-clone-result.schema.json', + ' "python": {'), + ('schemas/clean-clone-result.schema.json', + ' "description": "The interpreter the run used: .harness/config.yaml\'s clean_clone_python when that key is set, and test_command\'s own interpreter otherwise."'), + ('schemas/clean-clone-result.schema.json', + ' "python_version": {'), + ('schemas/clean-clone-result.schema.json', + ' "description": "The version that interpreter reported, so a reader can tell which Python the check exercised rather than assuming it matched CI. Absent when the interpreter reported no recognizable version, which is what a test command that is not a Python interpreter does."'), + ('schemas/harness-config.schema.json', + ' "clean_clone_python": {'), + ('schemas/revert-check-result.schema.json', + ' "required": ["ran", "paths", "command", "python"],'), + ('schemas/revert-check-result.schema.json', + ' "description": "Whether the suite actually ran in the clone with the edits reverted. False when the check could not run, in which case reason says why and permitted, exit_code, output_tail and python_version are absent."'), + ('schemas/revert-check-result.schema.json', + ' "python": {'), + ('schemas/revert-check-result.schema.json', + ' "description": "The interpreter the run used: .harness/config.yaml\'s clean_clone_python when that key is set, and test_command\'s own interpreter otherwise."'), + ('schemas/revert-check-result.schema.json', + ' "python_version": {'), + ('schemas/revert-check-result.schema.json', + ' "description": "The version that interpreter reported, so a reader can tell which Python the check exercised. Absent when the interpreter reported no recognizable version, which is what a test command that is not a Python interpreter does."'), + + # --- The workflow restriction naming a directory in the target. ----- + ('workflows/story-workflow.json', + ' "may_not_create": ["tests/"],'), +}) + + +#: Each entry carries a one-line reason saying why that mention is not a +#: tie, so the permanent half is a judgement on the record rather than a +#: suppression list. +PERMANENT_MENTIONS: dict[tuple[str, str], str] = { + ('hooks/bash_guard.py', + '#!/usr/bin/env python3'): + "the harness's own hook is a Python program and says so to the kernel", + ('scripts/l5-assist', + '#!/usr/bin/env python3'): + "the harness's own entry point is a Python program and says so to the kernel", + ('scripts/l5-init', + '#!/usr/bin/env python3'): + "the harness's own entry point is a Python program and says so to the kernel", + ('scripts/l5-plan', + '#!/usr/bin/env python3'): + "the harness's own entry point is a Python program and says so to the kernel", + ('scripts/l5-run', + '#!/usr/bin/env python3'): + "the harness's own entry point is a Python program and says so to the kernel", + ('scripts/l5-status', + '#!/usr/bin/env python3'): + "the harness's own entry point is a Python program and says so to the kernel", + ('orchestration/story_coordinator.py', + ' count that cannot be spent, and `True` is not a budget however much Python'): + "names the language this validation itself is written in, explaining why a bool is refused", + ('orchestration/story_coordinator.py', + ' None is not a failure: the configured test command need not be a Python'): + "says outright that a target's test command need not be Python, which is the opposite of a tie", + ('orchestration/story_parser.py', + '- No type coercion. Every scalar parses to a Python ``str``; ``42`` and'): + "a fact about what this parser returns to its own callers, not about any target", +} + + +#: The five ties the 2026-08-15 audit found, each identified by the file it +#: sits in and a fragment of the line, so the scan is asked for them by name +#: rather than being read off the list above. +AUDITED_TIES = ( + ("the version probe", + "orchestration/story_coordinator.py", "_VERSION_PROBE", 945), + ("the clean_clone_python configuration key", + "orchestration/story_coordinator.py", "clean_clone_python", None), + ("the may_not_create restriction naming a directory", + "workflows/story-workflow.json", '"may_not_create": ["tests/"]', 9), + ("prompts/tester.md line 19", + "prompts/tester.md", "New tests belong in tests/", 19), + ("prompts/tester.md line 47", + "prompts/tester.md", "tests/conftest.py", 47), +) + +#: The artifact schemas the-interpreter-is-not-assumed-to-be-python names, +#: whose mentions burn down with it and are therefore temporary. +INTERPRETER_STORY_SCHEMAS = ( + "schemas/clean-clone-result.schema.json", + "schemas/revert-check-result.schema.json", + "schemas/harness-config.schema.json", +) + +#: The mentions that are honest sentences rather than ties. A rule that +#: cannot tell these from a tie gets turned off within two stories. +LEGITIMATE_MENTIONS = ( + ("orchestration/story_parser.py", "Every scalar parses to a Python"), + ("orchestration/story_coordinator.py", + "the configured test command need not be a Python"), +) + + +# ========================================================================== +# The comparison, written once so the controls drive the same code +# ========================================================================== + + +def reported_entries(findings) -> set[tuple[str, str]]: + """What the scan reports, as the (path, line text) pairs the lists use. + + A line matching both rules yields two findings carrying the same pair; + the lists classify a *line*, not a rule, so the pair collapses to one + entry here. + """ + return {(finding.path, finding.line) for finding in findings} + + +def list_problems(findings) -> list[str]: + """Where the two lists and what the scan reports disagree, both ways. + + Returned rather than asserted so that a control can require the same + comparison to report something. The message names the file and line of + every unexpected or stale entry. + """ + reported = reported_entries(findings) + listed = set(TEMPORARY_TIES) | set(PERMANENT_MENTIONS) + numbers = {(f.path, f.line): f.line_number for f in findings} + problems = [] + for entry in sorted(reported - listed): + problems.append( + f"unexpected: {entry[0]}:{numbers[entry]} is a new tie on no list " + f"-- {entry[1].strip()!r}" + ) + for entry in sorted(listed - reported): + problems.append( + f"stale: {entry[0]} no longer carries the listed line, so its " + f"entry must come off the list -- {entry[1].strip()!r}" + ) + return problems + + +def reasonless(mentions: dict) -> list[tuple[str, str]]: + """Every permanent entry whose reason is missing or not one line.""" + return sorted(entry for entry, reason in mentions.items() + if not isinstance(reason, str) or not reason.strip() + or "\n" in reason) + + +def findings_for(findings, path: str, fragment: str) -> list: + return [f for f in findings if f.path == path and fragment in f.line] + + +@pytest.fixture(scope="module") +def here(): + """What the scan reports against this repository. Resolved once.""" + return harness_source.scan() + + +# ========================================================================== +# The throwaway root: the same scan, run somewhere it is safe to break +# ========================================================================== + + +def build_throwaway(root: Path) -> Path: + """A copy of the harness's source directories, plus the two the scan + must leave alone, in a root a test may vandalize. + + The copy is faithful, so the same lists apply to it — which is what lets + a control plant one violation and attribute the resulting failure to + that plant alone. + """ + root.mkdir(parents=True) + for name in harness_source.HARNESS_SOURCE_DIRS: + shutil.copytree(REPO_ROOT / name, root / name, + ignore=shutil.ignore_patterns("__pycache__")) + plant(root, ".harness/docs/ARCHITECTURE.md", + "# A target's own configuration lives here.\n") + plant(root, "tests/test_placeholder.py", "def test_it():\n assert True\n") + return root + + +def plant(root: Path, relative: str, text: str) -> Path: + path = root / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + return path + + +def append(root: Path, relative: str, text: str) -> Path: + path = root / relative + path.write_text(path.read_text(encoding="utf-8") + text, encoding="utf-8") + return path + + +@pytest.fixture(scope="module") +def pristine(tmp_path_factory) -> Path: + return build_throwaway(tmp_path_factory.mktemp("pristine") / "repo") + + +@pytest.fixture +def throwaway(pristine, tmp_path) -> Path: + """A fresh vandalizable copy per test, so one plant is one failure.""" + root = tmp_path / "repo" + shutil.copytree(pristine, root) + return root + + +def test_the_throwaway_root_reports_what_this_repository_does(throwaway, here): + """The controls below are only worth something if the copy is a faithful + stand-in: same code path, same findings, before anything is planted.""" + assert reported_entries(harness_source.scan(throwaway)) \ + == reported_entries(here) + assert not list_problems(harness_source.scan(throwaway)) + + +# ========================================================================== +# The five ties that motivated the story +# ========================================================================== + + +@pytest.mark.parametrize("label,path,fragment,line_number", + AUDITED_TIES, ids=[t[0] for t in AUDITED_TIES]) +def test_the_scan_reports_each_audited_tie(here, label, path, fragment, + line_number): + """Asked of the scan by name rather than read off the list. A scan that + cannot see the ties that motivated it has not been shown to work.""" + matches = findings_for(here, path, fragment) + assert matches, f"{label}: nothing reported in {path} matching {fragment!r}" + if line_number is not None: + assert line_number in {f.line_number for f in matches}, ( + label, sorted(f.line_number for f in matches)) + + +@pytest.mark.parametrize("label,path,fragment,line_number", + AUDITED_TIES, ids=[t[0] for t in AUDITED_TIES]) +def test_each_audited_tie_is_a_temporary_tie(here, label, path, fragment, + line_number): + for finding in findings_for(here, path, fragment): + entry = (finding.path, finding.line) + assert entry in TEMPORARY_TIES, (label, entry) + assert entry not in PERMANENT_MENTIONS, (label, entry) + + +@pytest.mark.parametrize("schema", INTERPRETER_STORY_SCHEMAS) +def test_the_artifact_schema_mentions_are_temporary_ties(here, schema): + """the-interpreter-is-not-assumed-to-be-python names these three files, + so their mentions burn down with it and belong on the temporary half.""" + matches = [f for f in here if f.path == schema] + assert matches, schema + for finding in matches: + assert any(token in finding.line.lower() + for token in ("python", "python_version", + "clean_clone_python")), finding + assert (finding.path, finding.line) in TEMPORARY_TIES, finding + + +@pytest.mark.parametrize("path,fragment", LEGITIMATE_MENTIONS) +def test_a_legitimate_mention_is_permanent_and_never_a_tie(here, path, + fragment): + """The docstring saying this parser's scalars are Python strings, and the + one saying a target's test command need not be a Python interpreter.""" + matches = findings_for(here, path, fragment) + assert matches, (path, fragment) + for finding in matches: + entry = (finding.path, finding.line) + assert entry in PERMANENT_MENTIONS, entry + assert entry not in TEMPORARY_TIES, entry + + +# ========================================================================== +# The two lists, exact in both directions +# ========================================================================== + + +def test_the_two_lists_are_exactly_what_the_scan_reports(here): + """Both directions: no tie is reported that no list claims, and no list + entry survives its line disappearing from the source.""" + assert not list_problems(here), "\n".join(list_problems(here)) + + +def test_the_two_lists_share_no_entry(): + assert not set(TEMPORARY_TIES) & set(PERMANENT_MENTIONS) + + +def test_every_permanent_mention_carries_a_reason(): + assert not reasonless(PERMANENT_MENTIONS) + + +def test_an_entry_with_no_reason_is_reported_by_the_same_check(): + """The control for the assertion above: the check has to be able to see + a missing reason, or it is a claim about what was typed rather than a + check on it.""" + entry = ("orchestration/somewhere.py", "python") + assert reasonless({**PERMANENT_MENTIONS, entry: ""}) == [entry] + assert reasonless({**PERMANENT_MENTIONS, entry: " "}) == [entry] + assert reasonless({**PERMANENT_MENTIONS, entry: "two\nlines"}) == [entry] + + +def test_a_planted_tie_in_a_clean_file_is_reported_and_turns_the_lists_red( + throwaway, +): + """The control for the equality assertion, in the direction that + matters: a tie that lands tomorrow cannot join a list quietly.""" + clean = "prompts/implementer.md" + assert not [f for f in harness_source.scan(throwaway) if f.path == clean], \ + f"{clean} is no longer clean, so it cannot serve as the plant site" + + append(throwaway, clean, "\nRun pytest before you finish.\n") + + planted = [f for f in harness_source.scan(throwaway) if f.path == clean] + assert [f.token.lower() for f in planted] == ["pytest"] + problems = list_problems(harness_source.scan(throwaway)) + assert len(problems) == 1, problems + assert problems[0].startswith("unexpected: prompts/implementer.md:") + + +def test_a_list_entry_left_behind_after_its_tie_is_removed_turns_the_lists_red( + throwaway, +): + """The other direction: a file that stops violating must be taken off + the list, or the burn-down counts work that is already done.""" + removed = ('workflows/story-workflow.json', + ' "may_not_create": ["tests/"],') + assert removed in TEMPORARY_TIES + + text = (throwaway / removed[0]).read_text(encoding="utf-8") + assert removed[1] + "\n" in text + plant(throwaway, removed[0], text.replace(removed[1] + "\n", "")) + + problems = list_problems(harness_source.scan(throwaway)) + assert len(problems) == 1, problems + assert problems[0].startswith("stale: workflows/story-workflow.json") + + +# ========================================================================== +# The matcher, on the four cases the story constructs +# ========================================================================== + + +BOUNDARY_CASES = ( + ("clean_clone_python = config.get('clean_clone_python')", True), + ("version = platform.python_version()", True), + ("results = pipeline(stages)", False), + ("stream = pipe(left, right)", False), +) + + +@pytest.mark.parametrize("line,expected", BOUNDARY_CASES, + ids=[c[0].split(" ")[0] for c in BOUNDARY_CASES]) +def test_a_stack_token_is_bounded_by_a_non_alphanumeric(throwaway, line, + expected): + """Constructed and run, not reasoned about. `_` and `.` are not letters + or digits, so an identifier-embedded token is seen; `e` is, so `pipeline` + and `pipe` are not.""" + plant(throwaway, "orchestration/boundary_case.py", line + "\n") + reported = [f for f in harness_source.scan(throwaway) + if f.path == "orchestration/boundary_case.py"] + assert bool(reported) is expected, (line, reported) + + +# ========================================================================== +# What the scan does not read, and why +# ========================================================================== + + +@pytest.mark.parametrize("relative", ( + ".harness/docs/notes.md", + ".harness/config.yaml", + "tests/test_planted.py", +)) +def test_a_tie_under_harness_or_tests_is_not_reported(throwaway, relative): + """A target's ties belong in `.harness/`, and this repository's own suite + is legitimately full of Python. The positive control is beside it: the + identical text in a scanned directory *is* reported.""" + tie = "run pytest in tests/ with the configured python3\n" + plant(throwaway, relative, tie) + assert not [f for f in harness_source.scan(throwaway) + if f.path == relative] + + plant(throwaway, "prompts/planted.md", tie) + assert [f for f in harness_source.scan(throwaway) + if f.path == "prompts/planted.md"], \ + "the same text is invisible everywhere, so this proves nothing" + + +def test_the_layout_rule_does_not_read_orchestration(throwaway): + """The deliberate limit: this harness's own suite is called `tests/`, so + in Python source a `tests/` literal cannot be told from an honest + reference to it. The same literal in a target-facing file is reported.""" + literal = 'MAY_NOT_CREATE = ["tests/"]\n' + plant(throwaway, "orchestration/layout_case.py", literal) + plant(throwaway, "prompts/layout_case.md", literal) + + found = harness_source.scan(throwaway) + assert not [f for f in found + if f.path == "orchestration/layout_case.py" + and f.rule == harness_source.LAYOUT_RULE] + assert [f for f in found + if f.path == "prompts/layout_case.md" + and f.rule == harness_source.LAYOUT_RULE] + + +@pytest.mark.parametrize("directory", harness_source.HARNESS_SOURCE_DIRS) +def test_every_declared_source_directory_is_actually_read(throwaway, + directory): + """The declaration is only worth what the walk honours: a tie planted in + each of the eight named directories is reported.""" + relative = f"{directory}/planted_tie.md" + plant(throwaway, relative, "the target is built with gradle\n") + assert [f for f in harness_source.scan(throwaway) if f.path == relative] + + +def test_the_exemption_covers_the_declaring_module_alone(throwaway): + """Exempt by name and by nothing else: a second file carrying the same + tokens is reported like any other.""" + assert not [f for f in harness_source.scan(throwaway) + if f.path == DECLARING_MODULE] + + twin = "orchestration/twin_of_harness_source.py" + plant(throwaway, twin, + (REPO_ROOT / DECLARING_MODULE).read_text(encoding="utf-8")) + assert [f for f in harness_source.scan(throwaway) if f.path == twin] + assert not [f for f in harness_source.scan(throwaway) + if f.path == DECLARING_MODULE] + + +# ========================================================================== +# The stated limits: read, then exercised +# ========================================================================== + + +#: Each limit the module must state, as the fragments that have to appear in +#: its docstring. Lowercased before searching, so casing is not the subject. +STATED_LIMITS = { + "the token list is a guess": ("stack_tokens", "guess"), + "the token list is incomplete": ("incomplete",), + "the layout half cannot read orchestration": ("cannot read `orchestration/`",), + "harness and tests are outside the scan": ("`.harness/` and `tests/` are outside",), + "the exemption is by name": ("exempt from its own scan by name",), +} + + +def module_docstring(text: str) -> str: + return ast.get_docstring(ast.parse(text)) or "" + + +def unstated_limits(docstring: str) -> list[str]: + lowered = docstring.lower() + return sorted(label for label, fragments in STATED_LIMITS.items() + if not all(fragment in lowered for fragment in fragments)) + + +def test_the_module_states_what_it_does_not_catch(): + """Read out of the module rather than eyeballed.""" + docstring = module_docstring( + (REPO_ROOT / DECLARING_MODULE).read_text(encoding="utf-8")) + assert docstring + assert not unstated_limits(docstring) + + +@pytest.mark.parametrize("label", sorted(STATED_LIMITS)) +def test_a_missing_limit_is_reported_by_the_same_check(label): + """The control: each stated limit stripped out of a rendering of the + docstring, which the same search has to report as missing.""" + docstring = module_docstring( + (REPO_ROOT / DECLARING_MODULE).read_text(encoding="utf-8")) + stripped = docstring + for fragment in STATED_LIMITS[label]: + stripped = stripped.replace(fragment, "") + stripped = stripped.replace(fragment.upper(), "") + stripped = stripped.replace(fragment.capitalize(), "") + assert label in unstated_limits(stripped) + + +def test_the_stated_incompleteness_is_true_rather_than_modest(throwaway): + """The docstring says the token list is a guess that will miss languages + nobody has tried. Exercised rather than believed: a Ruby and an Elixir + tie, planted in a scanned directory, really are invisible.""" + for token in ("python", "pytest"): + assert token in harness_source.STACK_TOKENS + for token in ("gemfile", "mix.exs", "composer.json"): + assert token not in harness_source.STACK_TOKENS + + plant(throwaway, "prompts/other_stacks.md", + "run the target's suite with Gemfile and mix.exs\n") + assert not [f for f in harness_source.scan(throwaway) + if f.path == "prompts/other_stacks.md"] + + +# ========================================================================== +# The story fixed nothing, and this run changed nothing +# ========================================================================== + + +def test_no_tie_was_fixed_by_this_story(): + """The three files carrying the audited ties are untouched on this + story's branch. Resolved through the shared story range in + `tests/conftest.py`, never as HEAD against this repository.""" + assert story_diff(list(UNTOUCHED), + validation_file=Path(__file__)).strip() == "" + + +@pytest.mark.parametrize("guarded", UNTOUCHED) +def test_the_same_comparison_reports_a_story_that_did_edit_one(tmp_path, + guarded): + """The control for the assertion above, over the shape this repository + cannot be in while these tests run: a story already committed, whose own + run commit rewrote the guarded file.""" + root = committed_story(tmp_path, VALIDATION_REL, guarded, violate="modify", + name=f"violating-{Path(guarded).name}") + assert story_diff([guarded], validation_file=root / VALIDATION_REL, + repo=root).strip() != "" + + +def readers_of_the_scan(root: Path) -> list[str]: + """Every module under `root`'s orchestration/ that names the scan.""" + declaring = Path(DECLARING_MODULE).name + return sorted(module.name + for module in (root / "orchestration").glob("*.py") + if module.name != declaring + and "harness_source" in module.read_text(encoding="utf-8")) + + +def test_the_coordinator_does_not_run_the_scan(): + """A declaration the suite drives, not run-time behaviour. Nothing under + orchestration/ reads it, so no run is decided by what it reports.""" + assert not readers_of_the_scan(REPO_ROOT) + + +def test_a_module_that_did_read_the_scan_is_reported(throwaway): + """The control: the same search over a root where one module imports it.""" + plant(throwaway, "orchestration/would_run_it.py", + "import harness_source\n\nharness_source.scan()\n") + assert readers_of_the_scan(throwaway) == ["would_run_it.py"] diff --git a/tests/test_stage_output_ownership.py b/tests/test_stage_output_ownership.py index 65c26fb..dcba7d2 100644 --- a/tests/test_stage_output_ownership.py +++ b/tests/test_stage_output_ownership.py @@ -392,11 +392,26 @@ def test_no_path_prefix_is_named_in_orchestration_code(): """tests/ is a fact about this workflow, not about the harness. The mechanism key may appear where the declaration is enforced (story_coordinator, this story) and where it is rendered for the planner - (context_assembler, story-009); no module may name the prefix itself.""" + (context_assembler, story-009); no module may name the prefix itself. + + story-040 adds the one module whose deliverable *is* to name it: + harness_source.py declares the target-layout shapes its scan looks for, + and cannot declare them without spelling them. It is exempt by name, + and the exemption is held shut from both sides — that module must exist + and must actually name the prefix, or the exemption is stale, and every + other module is held to the assertion unchanged. It gains no latitude + on the mechanism key, which it still may not name.""" allowed_to_read_the_key = ("story_coordinator.py", "context_assembler.py") + declares_the_prefix = "harness_source.py" + + declaring = ORCHESTRATION / declares_the_prefix + assert declaring.is_file(), declares_the_prefix + assert "tests/" in executable_source(declaring.read_text(encoding="utf-8")) + for module in sorted(ORCHESTRATION.glob("*.py")): body = executable_source(module.read_text(encoding="utf-8")) - assert "tests/" not in body, module.name + if module.name != declares_the_prefix: + assert "tests/" not in body, module.name assert ("may_not_create" not in body or module.name in allowed_to_read_the_key), module.name