fix(b-gate): every denominator answers to a source outside the gate, and a name is no longer a door [skip-docs]

Row 1's M was a curated list of 13 in the gate's OWN b_gate.json. It is now DERIVED: run_path_calls
reads what run.py::run_project actually calls (41 po-functions, re-counted here against PM's 39 —
the two that differ are generate_via_llm and fresh_workflow, held out because they need a chat
client). A call that is neither a declared step nor named-with-a-reason counts in the denominator
WITHOUT a door, so the number cannot shrink in silence: removing a step from the contract now
leaves N unchanged and names the orphan. The four outbox writers the run path uses and nobody had
declared (write_prepass, write_parse_failures, write_proposal_reviews, write_debate_tools) are
steps now; 28 calls are held out, each with its reason printed under the row.

A door must be REGISTERED and PROVEN. entry["kind"] is read (it was read 0 times before) and must
be one of three kinds the gate has code to verify: console-script in pyproject, module-main with
its own __main__ guard, subcommand registered in the module's argparse. On top of that every step
needs a named probe that calls the door and reads the artefact. The MCP sentence is struck from
EXTERNAL_DOOR: it claimed a capability with no code behind it.

Row 3 now scans the repo manifest (git ls-files, or the tree itself in an extract), not a hand
list of 11 roots: 512 published files instead of 433, so main.py, examples/, spikes/, contexts/,
CLAUDE.md and llms.txt are inside the guard for the first time. Three new patterns catch the
indirect invocations that walked past the old six — absolute path, list in a variable, constant,
shell string — 5 of 5 of PM's variants are refused now, with 0 false positives measured over the
whole surface. An empty surface is IKKE MAALT, not GREEN: the row demands a sentinel file and
prints the file count and the manifest it read.

Rows 4, 5 and 6 get sources for their denominators. Row 4 counts only checks whose named source
symbol resolves in the code. Row 5 counts each structural requirement on its own (2 profile
members + factory + seam + probe = 5) instead of collapsing three into one unit. Row 6's N comes
from the artefacts the contract names, and the attestation is VALIDATED: it must name the
contract's runbook, carry its sha256, say who ran it, and bear a real ISO date that is not in the
future — v1_gate's own date rule, reused, BOM tolerated as there.

Measured in a scratch clone (/tmp/claude-po/bgate-mut): 12 of 12 mutants felled, control 65 of 65.
All six of PM's broken attacks reproduced as refused, with the rc-0 control green.
No row got greener: 3 of 17 (was 3 of 13), 0 of 2, GREEN, 0 of 3, 4 of 5 (was 1 of 2), IKKE MAALT.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-19 19:40:42 +02:00
commit 59f35fde22
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
3 changed files with 734 additions and 221 deletions

View file

@ -1,5 +1,124 @@
{
"outcome": "En Claude Code-økt driver en hel analyse med po som verktøykasse, uten at po gjør ett eneste modellkall.",
"run_path": {
"module": "run.py",
"scope": "run_project",
"package": "portfolio_optimiser",
"held_out": [
{
"symbol": "admits",
"reason": "dimensjonsfilteret inne i mandat-vurderingen; skriver ingen egen artefakt"
},
{
"symbol": "baseline_from_project",
"reason": "utleder grunnlaget fra prosjektobjektet inne i «kostnadsgrunnlag»"
},
{
"symbol": "build_mcp_tools",
"reason": "bygger verktøylista debatten kaller; hører til modellveien"
},
{
"symbol": "bundle_citations",
"reason": "siteringshjelper inne i «kontekst»"
},
{
"symbol": "bundle_excerpt_citations",
"reason": "siteringshjelper inne i «kontekst»"
},
{
"symbol": "chunk_dict_to_citation",
"reason": "formkonvertering inne i «kontekst»"
},
{
"symbol": "classify_codes",
"reason": "klassifiserer avvisningskodene inne i «validering»"
},
{
"symbol": "criteria_block",
"reason": "renderer mandatkriteriene inn i prompten; modellveien"
},
{
"symbol": "fresh_workflow",
"reason": "krever chatklient: bygger MAF-debatten"
},
{
"symbol": "generate_via_llm",
"reason": "krever chatklient: ER modellkallet"
},
{
"symbol": "grounding_offer",
"reason": "bygger grunnlagstilbudet til prompten; modellveien"
},
{
"symbol": "load_contracts",
"reason": "leser kontraktsfilene før kjøringen; egen dør hører i preflight"
},
{
"symbol": "load_verdicts_from_dir",
"reason": "leser tidligere dommer inn i ExpeL-konteksten; inne i «dom»"
},
{
"symbol": "make_retrieval_tool",
"reason": "pakker «kontekst» som FunctionTool for agentene; modellveien"
},
{
"symbol": "navigator_tools",
"reason": "explore-modusens verktøy; rad 2 avgrenser B til proposer og checker"
},
{
"symbol": "optional_bundle_candidate_features",
"reason": "henter valgfrie trekk inne i «verdict-id»"
},
{
"symbol": "proposal_reviews_payload",
"reason": "bygger nyttelasten «forslagsvurderinger» skriver"
},
{
"symbol": "requirement_payload",
"reason": "explore-modus; utenfor B slik rad 2 avgrenser den"
},
{
"symbol": "resolve_model",
"reason": "slår opp deployment-navnet for chatklienten; modellveien"
},
{
"symbol": "tool_call_payload",
"reason": "bygger nyttelasten «debatt-verktøy» skriver"
},
{
"symbol": "tool_server_index",
"reason": "indekserer MCP-serverne for debatten; modellveien"
},
{
"symbol": "assert_declared_ids_agree",
"reason": "konsistenssjekk inne i «pakke»"
},
{
"symbol": "declared_reference_numbers",
"reason": "leser de erklærte referansetallene inne i «pakke»"
},
{
"symbol": "load_optional_cost_baseline",
"reason": "valgfri overstyring inne i «kostnadsgrunnlag»"
},
{
"symbol": "reconcile_bundle_id",
"reason": "binder bundle-id-en inne i «pakke»"
},
{
"symbol": "declaration_of",
"reason": "leser prepass-erklæringen inne i «prepass»"
},
{
"symbol": "declaration_payload",
"reason": "bygger erklæringens nyttelast inne i «prepass»"
},
{
"symbol": "render_context",
"reason": "renderer prepass-konteksten inn i prompten; modellveien"
}
]
},
"steps": [
{
"id": "pakke",
@ -11,10 +130,11 @@
"scope": "run_project"
},
"entry": {
"kind": "cli",
"kind": "console-script",
"module": "run.py",
"scope": "main"
}
},
"probe": []
},
{
"id": "kostnadsgrunnlag",
@ -26,10 +146,11 @@
"scope": "run_project"
},
"entry": {
"kind": "cli",
"kind": "console-script",
"module": "run.py",
"scope": "main"
}
},
"probe": []
},
{
"id": "kontekst",
@ -41,10 +162,11 @@
"scope": "run_project"
},
"entry": {
"kind": "cli",
"kind": "console-script",
"module": "run.py",
"scope": "main"
}
},
"probe": []
},
{
"id": "prepass",
@ -56,10 +178,11 @@
"scope": "run_project"
},
"entry": {
"kind": "cli",
"kind": "console-script",
"module": "run.py",
"scope": "main"
}
},
"probe": []
},
{
"id": "validering",
@ -71,10 +194,11 @@
"scope": "evaluate_mandate_candidates"
},
"entry": {
"kind": "cli",
"kind": "console-script",
"module": "run.py",
"scope": "main"
}
},
"probe": []
},
{
"id": "verdict-id",
@ -86,10 +210,11 @@
"scope": "run_project"
},
"entry": {
"kind": "cli",
"kind": "console-script",
"module": "run.py",
"scope": "main"
}
},
"probe": []
},
{
"id": "dom",
@ -101,10 +226,11 @@
"scope": "run_project"
},
"entry": {
"kind": "cli",
"kind": "console-script",
"module": "run.py",
"scope": "main"
}
},
"probe": []
},
{
"id": "kjørekonfig",
@ -116,10 +242,11 @@
"scope": "run_project"
},
"entry": {
"kind": "cli",
"kind": "console-script",
"module": "run.py",
"scope": "main"
}
},
"probe": []
},
{
"id": "coverage",
@ -131,10 +258,11 @@
"scope": "run_project"
},
"entry": {
"kind": "cli",
"kind": "console-script",
"module": "run.py",
"scope": "main"
}
},
"probe": []
},
{
"id": "utboks",
@ -146,10 +274,11 @@
"scope": "run_project"
},
"entry": {
"kind": "cli",
"kind": "console-script",
"module": "run.py",
"scope": "main"
}
},
"probe": []
},
{
"id": "rundebinding",
@ -161,10 +290,13 @@
"scope": "main"
},
"entry": {
"kind": "cli",
"kind": "module-main",
"module": "evals/round_builder.py",
"scope": "main"
}
},
"probe": [
"tests/test_round_builder_loadbearing.py::test_the_command_line_builds_a_round_and_refuses_one_it_cannot"
]
},
{
"id": "rapport",
@ -176,10 +308,13 @@
"scope": "build_round"
},
"entry": {
"kind": "cli",
"kind": "module-main",
"module": "evals/round_builder.py",
"scope": "main"
}
},
"probe": [
"tests/test_round_builder_loadbearing.py::test_the_report_names_every_approach_that_was_considered"
]
},
{
"id": "gate",
@ -191,10 +326,77 @@
"scope": "main"
},
"entry": {
"kind": "cli",
"kind": "module-main",
"module": "evals/v1_gate.py",
"scope": "main"
}
},
"probe": [
"tests/test_v1_gate.py::test_the_command_is_red_today_with_every_row_in_its_output"
]
},
{
"id": "prepass-artefakt",
"label": "skriv prepassets egen artefakt",
"module": "outbox.py",
"symbol": "write_prepass",
"driver": {
"module": "run.py",
"scope": "run_project"
},
"entry": {
"kind": "console-script",
"module": "run.py",
"scope": "main"
},
"probe": []
},
{
"id": "parse-feil",
"label": "skriv det som ikke lot seg lese",
"module": "outbox.py",
"symbol": "write_parse_failures",
"driver": {
"module": "run.py",
"scope": "run_project"
},
"entry": {
"kind": "console-script",
"module": "run.py",
"scope": "main"
},
"probe": []
},
{
"id": "forslagsvurderinger",
"label": "skriv vurderingene av hvert forslag",
"module": "outbox.py",
"symbol": "write_proposal_reviews",
"driver": {
"module": "run.py",
"scope": "run_project"
},
"entry": {
"kind": "console-script",
"module": "run.py",
"scope": "main"
},
"probe": []
},
{
"id": "debatt-verktøy",
"label": "skriv verktøyene debatten kalte",
"module": "outbox.py",
"symbol": "write_debate_tools",
"driver": {
"module": "run.py",
"scope": "run_project"
},
"entry": {
"kind": "console-script",
"module": "run.py",
"scope": "main"
},
"probe": []
}
],
"roles": {
@ -214,19 +416,20 @@
}
},
"no_claude_path": {
"roots": [
"src",
"scripts",
"shared",
"docs",
"tests",
"pyproject.toml",
"README.md",
"CHANGELOG.md",
"DEPLOY.md",
"CONTRIBUTING.md",
"SECURITY.md"
],
"manifest": {
"sentinel": "src/portfolio_optimiser/run.py",
"skip_dirs": [
".git",
"__pycache__",
".venv",
".pytest_cache",
".ruff_cache",
".mypy_cache",
"node_modules",
".idea",
".vscode"
]
},
"patterns": [
{
"id": "prosess",
@ -269,20 +472,59 @@
"pattern_b64": "KEFOVEhST1BJQ19CQVNFX1VSTHxBTlRIUk9QSUNfQVBJX0tFWXxhcGlcLmFudGhyb3BpY1wuY29tKQ==",
"known_positive_b64": "b3MuZW52aXJvblsiQU5USFJPUElDX0JBU0VfVVJMIl0gPSBlbmRwb2ludA==",
"known_negative_b64": "b3MuZW52aXJvblsiQVpVUkVfT1BFTkFJX0VORFBPSU5UIl0gPSBlbmRwb2ludA=="
},
{
"id": "argv-literal",
"label": "claude som programnavn i en argumentliste",
"pattern_b64": "W1xbKCxdXHMqWyInXSg/OlteIidcc10qLyk/Y2xhdWRlWyInXQ==",
"known_positive_b64": "Y21kID0gWyJjbGF1ZGUiLCAiLXAiLCAiYW5hbHlzZXIgcHJvc2pla3RldCJd",
"known_negative_b64": "bW9kZWxsZXIgPSBbImNsYXVkZS1vcHVzLTUiLCAiZ3B0LTUiXQ=="
},
{
"id": "kommando-streng",
"label": "claude med flagg inne i en streng",
"pattern_b64": "WyInXSg/OlteIidcc10qLyk/Y2xhdWRlXHMrLXsxLDJ9W0EtWmEtel0=",
"known_positive_b64": "c3VicHJvY2Vzcy5ydW4oc2hsZXguc3BsaXQoImNsYXVkZSAtcCBhbmFseXNlciIpKQ==",
"known_negative_b64": "c3VicHJvY2Vzcy5ydW4oc2hsZXguc3BsaXQoInB5dGhvbiAtbSBwb3J0Zm9saW9fb3B0aW1pc2VyLnJ1biBQMSIpKQ=="
},
{
"id": "binaersti",
"label": "en konstant som navngir claude-binaeren",
"pattern_b64": "PVxzKlsiJ10oPzpbXiInXHNdKi8pP2NsYXVkZVsiJ10=",
"known_positive_b64": "Q0xBVURFX0JJTiA9ICIvdXNyL2xvY2FsL2Jpbi9jbGF1ZGUi",
"known_negative_b64": "TU9ERUxMID0gImNsYXVkZS1vcHVzLTUi"
}
]
},
"no_model_calls": {
"checks": {
"fullfører": [
"tests/test_b_toolbox_probes.py::test_the_whole_run_completes_with_the_chat_client_raising"
],
"rundekatalog": [
"tests/test_b_toolbox_probes.py::test_the_toolbox_outbox_binds_to_a_scratch_round_directory"
],
"gate-form-ok": [
"tests/test_b_toolbox_probes.py::test_v1_gate_reads_that_round_as_form_ok_not_proven"
]
"fullfører": {
"source": {
"module": "run.py",
"symbol": "run_project"
},
"probe": [
"tests/test_b_toolbox_probes.py::test_the_whole_run_completes_with_the_chat_client_raising"
]
},
"rundekatalog": {
"source": {
"module": "evals/round_builder.py",
"symbol": "build_round"
},
"probe": [
"tests/test_b_toolbox_probes.py::test_the_toolbox_outbox_binds_to_a_scratch_round_directory"
]
},
"gate-form-ok": {
"source": {
"module": "evals/v1_gate.py",
"symbol": "FORM_OK"
},
"probe": [
"tests/test_b_toolbox_probes.py::test_v1_gate_reads_that_round_as_form_ok_not_proven"
]
}
}
},
"foundry": {
@ -310,10 +552,15 @@
}
},
"runbook": {
"artefacts": [
"kjørebok",
"attestering"
],
"path": "docs/kjoerebok-verktoeykassen.md",
"attestation": "v1-rounds/kjoerebok-attestering.txt",
"keys": [
"kjørebok",
"sjekksum",
"kjørt",
"dato"
]

View file

@ -27,9 +27,12 @@ import argparse
import ast
import base64
import binascii
import hashlib
import json
import re
import subprocess
from collections.abc import Callable, Mapping, Sequence
from datetime import datetime, timezone
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any
@ -39,6 +42,7 @@ from portfolio_optimiser.evals.v1_gate import (
NOT_MEASURED,
RED,
Row,
_parse_given,
exit_code,
run_probes,
)
@ -75,13 +79,18 @@ CHAT_CLIENT_NAMES = frozenset(
#: Rad 1: hvilken dør som teller, og hvorfor den ble valgt framfor den andre.
EXTERNAL_DOOR = (
"valgt CLI-underkommando som dør UT av verktøykassen, fordi po allerede har fem main() og to "
"konsoll-kommandoer, og operatørens ord er «som alle andre tools Claude Code kan bruke» — en "
"kommando er nettopp det. Grunnen til å velge bort MCP her er at MCP ville krevd en server po "
"ikke har i kjørestien i dag; gaten teller likevel en MCP-registrert dør som kallbar, så "
"valget binder ikke neste ordre"
"valgt kommandolinja som dør UT av verktøykassen, fordi operatørens ord er «som alle andre "
"tools Claude Code kan bruke» — en kommando er nettopp det. Gaten teller tre arter den kan "
"VERIFISERE mot kilden: console-script (registrert i pyproject), module-main (kjørbar med "
"python -m, med sin egen __main__-vakt) og subcommand (et navn registrert i modulens egen "
"argparse). En art gaten ikke kan verifisere er ikke en dør her — og et navn er uansett ikke "
"nok: hvert steg må ha en navngitt probe som kaller døren og leser artefaktet den skriver"
)
#: Inngangsartene rad 1 kan VERIFISERE mot kilden. En art utenfor lista teller aldri som en dør:
#: gaten skal ikke kunne bli grønn av en påstand den ikke har kode for å etterprøve.
ENTRY_KINDS: tuple[str, ...] = ("console-script", "module-main", "subcommand")
#: Rad 2: det ene kravet som skiller en ekte felles vei fra to veier som ser like ut.
BYTE_IDENTICAL_RULE = (
"et forslag gitt UTENFRA skal gjennom NØYAKTIG samme validator, samme avvisningsstadier og "
@ -123,37 +132,6 @@ finnes er RØD, aldri grønn og aldri hoppet over.
"""
# ---------------------------------------------------------------------------------------------
# STUBBER (rød-først). Navnene finnes her for at armene under skal felle på en ASSERT om ATFERD
# og ikke ved innsamling; verdiene er MED VILJE gale og erstattes i reparasjons-commiten.
# ---------------------------------------------------------------------------------------------
#: Inngangsartene gaten kan VERIFISERE. Tom her med vilje: stubben kjenner ingen.
ENTRY_KINDS: tuple[str, ...] = ()
def run_path_calls(src: Path, spec: Mapping[str, Any]) -> dict[str, str]:
"""STUB: skal lese kjørestiens faktiske kall av KILDEN. Returnerer ingenting ennå."""
return {}
def console_scripts(repo_root: Path) -> dict[str, str]:
"""STUB: skal lese [project.scripts] fra pyproject.toml."""
return {}
def registered_entry(
src: Path, repo_root: Path, package: str, entry: Mapping[str, Any]
) -> tuple[bool, str]:
"""STUB: godtar ALT som dør — nøyaktig feilen reparasjonen skal fjerne."""
return True, ""
def published_files(root: Path, manifest: Mapping[str, Any]) -> tuple[list[Path], str]:
"""STUB: skal utlede den publiserte flaten av repo-manifestet."""
return [], "stub"
def load_config(path: Path = _DATA) -> dict[str, Any]:
data: dict[str, Any] = json.loads(path.read_text(encoding="utf-8"))
return data
@ -285,7 +263,148 @@ def entry_reaches(src: Path, entry: Mapping[str, Any], symbol: str) -> tuple[boo
return True, ""
def measure_steps(steps: Sequence[Mapping[str, Any]], src: Path) -> list[Step]:
def console_scripts(repo_root: Path) -> dict[str, str]:
"""``[project.scripts]`` i pyproject.toml, lest som tekst.
Lest som tekst og ikke med ``tomllib``, fordi pakken støtter Python 3.10, der ``tomllib``
ikke finnes og en gate som krever en nyere tolk enn produktet er en gate som ikke kjører
der produktet gjør."""
path = repo_root / "pyproject.toml"
if not path.is_file():
return {}
try:
text = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError): # pragma: no cover - et ulesbart manifest er ingen dør
return {}
scripts: dict[str, str] = {}
inside = False
for line in text.splitlines():
stripped = line.strip()
if stripped.startswith("["):
inside = stripped == "[project.scripts]"
continue
if not inside or "=" not in stripped or stripped.startswith("#"):
continue
name, _, target = stripped.partition("=")
scripts[name.strip().strip("\"'")] = target.strip().strip("\"'")
return scripts
def _dotted(package: str, module: str) -> str:
return f"{package}." + str(module).removesuffix(".py").replace("/", ".")
def _has_dunder_main(tree: ast.AST, scope: str) -> bool:
"""``if __name__ == "__main__":`` som faktisk kaller ``scope`` — en modul kjørbar med -m."""
for node in ast.walk(tree):
if not isinstance(node, ast.If):
continue
test = node.test
if not isinstance(test, ast.Compare) or not isinstance(test.left, ast.Name):
continue
if test.left.id != "__name__":
continue
if not any(isinstance(c, ast.Constant) and c.value == "__main__" for c in test.comparators):
continue
if any(scope in _call_names(child) for child in node.body):
return True
return False
def _registers_subcommand(tree: ast.AST, command: str) -> bool:
for node in ast.walk(tree):
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
continue
if node.func.attr != "add_parser":
continue
if any(isinstance(a, ast.Constant) and a.value == command for a in node.args):
return True
return False
def registered_entry(
src: Path, repo_root: Path, package: str, entry: Mapping[str, Any]
) -> tuple[bool, str]:
"""(inngangen er REGISTRERT, grunnen når den ikke er det).
Målt 19.09.2026: en modul med ``def main(): return <symbol>()`` og ingenting annet tok rad 1
fra 3 til 4 av 13. Et navn er ikke en dør. Døren være registrert et sted utenfor modulen
selv i pakkemanifestet, i en ``__main__``-vakt, eller i en argparse-underkommando og
arten være en gaten har kode for å etterprøve."""
kind = str(entry.get("kind", ""))
if kind not in ENTRY_KINDS:
return False, f"inngangsarten {kind!r} er ikke en gaten kan verifisere"
module = str(entry["module"])
scope = str(entry.get("scope", "main"))
tree = _tree(src, module)
if tree is None:
return False, f"inngangsmodulen {module} finnes ikke"
if kind == "console-script":
target = f"{_dotted(package, module)}:{scope}"
if target not in console_scripts(repo_root).values():
return False, f"{target} er ikke registrert i pyproject [project.scripts]"
return True, ""
if kind == "module-main":
if not _has_dunder_main(tree, scope):
return False, f"{module} har ingen __main__-vakt som kaller {scope}()"
return True, ""
command = str(entry.get("command", ""))
if not command:
return False, f"{module} navngir ingen underkommando"
if not _registers_subcommand(tree, command):
return False, f"{module} registrerer ingen underkommando {command!r}"
return True, ""
def run_path_calls(src: Path, spec: Mapping[str, Any]) -> dict[str, str]:
"""``symbol -> modulen det kom fra``: po-funksjonene kjørestien FAKTISK kaller.
Dette er rad 1s nevner, og den leses av KILDEN. Regelen: et navn importert fra en
undermodul av pakken og kalt i scopet, eller et attributt en modul importert som
``from <pakke> import X``. Klasser og unntak (stor forbokstav) er ikke steg, og private
hjelpere i driveren heller ikke. Et kall som verken er erklært som steg eller navngitt som
holdt utenfor, teller med i nevneren UTEN en dør nevneren kan derfor ikke krympe stille,
som en kuratert liste i gatens egen datafil kunne."""
tree = _tree(src, str(spec["module"]))
if tree is None:
return {}
package = str(spec.get("package", "portfolio_optimiser"))
symbols: dict[str, str] = {}
aliases: set[str] = set()
for node in ast.walk(tree):
if not isinstance(node, ast.ImportFrom):
continue
module = node.module or ""
if module == package:
aliases |= {a.asname or a.name for a in node.names}
elif module.startswith(f"{package}."):
for alias in node.names:
symbols[alias.asname or alias.name] = module.split(".")[-1] + ".py"
found: dict[str, str] = {}
for scope in _scopes(tree, str(spec["scope"])):
for call in ast.walk(scope):
if not isinstance(call, ast.Call):
continue
func = call.func
if isinstance(func, ast.Name) and func.id in symbols and func.id[:1].islower():
found[func.id] = symbols[func.id]
elif (
isinstance(func, ast.Attribute)
and isinstance(func.value, ast.Name)
and func.value.id in aliases
and func.attr[:1].islower()
):
found[func.attr] = f"{func.value.id}.py"
return found
def measure_steps(
steps: Sequence[Mapping[str, Any]],
src: Path,
outcomes: Mapping[str, str] | None = None,
repo_root: Path = _REPO_ROOT,
package: str = "portfolio_optimiser",
) -> list[Step]:
measured: list[Step] = []
for step in steps:
tree = _tree(src, str(step["module"]))
@ -299,7 +418,16 @@ def measure_steps(steps: Sequence[Mapping[str, Any]], src: Path) -> list[Step]:
why = f"{step['driver']['module']}::{step['driver']['scope']} kaller ikke {step['symbol']}"
else:
ok, why = entry_reaches(src, step["entry"], str(step["symbol"]))
why = "" if ok else why
if ok:
ok, why = registered_entry(src, repo_root, package, step["entry"])
if ok:
probes = list(step.get("probe", ()))
verdict = _probe_verdict(probes, outcomes or {})
why = (
"ingen atferdsprobe registrert — et navn er ikke en atferd"
if not probes
else (f"atferdsprobe: {verdict}" if verdict else "")
)
external = resolved and called and not why
measured.append(Step(str(step["id"]), str(step["label"]), resolved, called, external, why))
return measured
@ -312,18 +440,28 @@ def score_toolbox(
outcomes: Mapping[str, str] | None = None,
repo_root: Path = _REPO_ROOT,
) -> Row:
measured = measure_steps(steps, src)
package = str((run_path or {}).get("package", "portfolio_optimiser"))
measured = measure_steps(steps, src, outcomes, repo_root, package)
in_path = [s for s in measured if s.resolved and s.called]
undeclared = sorted(
name
for name in (run_path_calls(src, run_path) if run_path else {})
if name not in {str(s["symbol"]) for s in steps}
and name not in {str(h["symbol"]) for h in (run_path or {}).get("held_out", ())}
)
declared = {str(s["symbol"]) for s in steps}
held = {
str(h["symbol"]): str(h.get("reason", "")) for h in (run_path or {}).get("held_out", ())
}
calls = run_path_calls(src, run_path) if run_path else {}
undeclared = sorted(name for name in calls if name not in declared and name not in held)
n = len(in_path) + len(undeclared)
k = sum(1 for s in in_path if s.external)
exceptions = tuple(f"{s.id}: {s.why}" for s in measured if not s.external) + tuple(
f"udeklarert: {name} kalles i kjørestien uten å være et steg" for name in undeclared
f"udeklarert: {name} kalles i {calls[name]} fra kjørestien uten å være et steg"
for name in undeclared
)
diagnostics = tuple(
f"holdt utenfor: {symbol}{reason or 'UTEN GRUNN'}"
for symbol, reason in sorted(held.items())
) + tuple(
f"holdt utenfor: {symbol} kalles ikke lenger i kjørestien (foreldet unntak)"
for symbol in sorted(held)
if calls and symbol not in calls
)
return Row(
"verktøykasse",
@ -331,8 +469,10 @@ def score_toolbox(
k,
n,
GREEN if n and k == n else RED,
f"{k} av {n} steg har en dør ut; {len(measured) - n} erklærte steg er ikke i kjørestien",
f"{k} av {n} steg har en dør ut; {len(held)} kall holdt utenfor med grunn, "
f"{len(measured) - len(in_path)} erklærte steg er ikke i kjørestien",
exceptions=exceptions,
diagnostics=diagnostics,
attests=(EXTERNAL_DOOR,),
)
@ -397,30 +537,55 @@ def score_roles(roles: Mapping[str, Any], outcomes: Mapping[str, str], src: Path
# Rad 3 — po har ingen vei til Claude
# ---------------------------------------------------------------------------------------------
#: Flatene som publiseres, lest som tekst. Alt annet er binært og bærer ingen kjørbar linje.
TEXT_SUFFIXES = frozenset(
{".py", ".sh", ".toml", ".json", ".md", ".txt", ".cfg", ".ini", ".yaml", ".yml", ".template"}
)
_FENCE = re.compile(r"^\s*(```|~~~)")
#: Rad 3: hva vakten IKKE rekker, sagt av raden selv. Fire av PMs fem indirekte kall-varianter
#: slapp forbi før 19.09; disse to gjør det fortsatt, og de er linje-lokale grenser i en
#: tekstvakt, ikke noe en regex kan lukke.
NO_CLAUDE_LIMIT = (
"rad 3 er en TEKSTVAKT over hele det publiserte manifestet, ikke en dataflyt-analyse: den "
"feller et programnavn skrevet ut, en absolutt sti, en liste lagt i en variabel, en "
"konstant og en shell-streng. To veier står igjen og kan ikke felles linje for linje — et "
"kommandonavn satt sammen av deler ved kjøretid, og et navn lest ut av en miljøvariabel. "
"Raden påstår derfor ikke at ingen vei finnes; den påstår at ingen SKREVET vei finnes, over "
"en nevner den oppgir i filer"
)
def surface_files(root: Path, roots: Sequence[str]) -> list[Path]:
found: list[Path] = []
for rel in roots:
base = root / rel
if base.is_file():
found.append(base)
continue
if not base.is_dir():
continue
for path in sorted(base.rglob("*")):
if not path.is_file() or path.is_symlink():
continue
if "__pycache__" in path.parts or ".git" in path.parts:
continue
if path.suffix in TEXT_SUFFIXES:
found.append(path)
return found
def published_files(root: Path, manifest: Mapping[str, Any]) -> tuple[list[Path], str]:
"""(filene som PUBLISERES, hvor lista kom fra).
Kilden er repo-manifestet, ikke en håndliste: ``git ls-files`` når det finnes en ``.git``,
og ellers filtreet selv et rent uttrekk (``git archive | tar -x``) ER nøyaktig det
publiserte, og har ingen git-metadata å spørre. Håndlista den erstattet utelot ``main.py``,
``examples/``, ``spikes/`` og ``contexts/``: 433 av 512 filer ble skannet, og alle seks
vaktenes kjent-positive kunne plantes i ``main.py`` uten at raden merket det.
Ingen suffiks-allowlist heller: en fil som lar seg lese som tekst kan bære en kommandolinje
uansett hva den heter. Det som ikke lar seg avkode, telles og oppgis."""
skip = set(manifest.get("skip_dirs", (".git", "__pycache__")))
if (root / ".git").exists():
try:
listing = subprocess.run(
["git", "ls-files", "-z"],
cwd=root,
capture_output=True,
text=True,
check=True,
).stdout
except (OSError, subprocess.CalledProcessError): # pragma: no cover - git kan mangle
listing = None
if listing is not None:
found = [root / rel for rel in listing.split("\0") if rel]
return [p for p in found if p.is_file() and not p.is_symlink()], "git-manifestet"
walked = [
path
for path in sorted(root.rglob("*"))
if path.is_file()
and not path.is_symlink()
and not (skip & set(path.relative_to(root).parts))
]
return walked, "filtreet (uttrekk uten git-metadata)"
def scannable_lines(path: Path, text: str) -> list[tuple[int, str]]:
@ -451,17 +616,25 @@ class Pattern:
hits: tuple[str, ...]
def measure_patterns(config: Mapping[str, Any], root: Path) -> list[Pattern]:
@dataclass(frozen=True)
class Surface:
files: int
unreadable: int
source: str
sentinel: bool
def measure_patterns(config: Mapping[str, Any], root: Path) -> tuple[list[Pattern], Surface]:
manifest = config.get("manifest", {})
files, source = published_files(root, manifest)
sentinel = str(manifest.get("sentinel", ""))
scanned: list[tuple[Path, list[tuple[int, str]]]] = []
surface = (
surface_files(root, list(config["roots"]))
if "roots" in config
else published_files(root, config.get("manifest", {}))[0]
)
for path in surface:
unreadable = 0
for path in files:
try:
text = path.read_text(encoding="utf-8")
except (UnicodeDecodeError, OSError):
unreadable += 1
continue
scanned.append((path, scannable_lines(path, text)))
measured: list[Pattern] = []
@ -482,18 +655,14 @@ def measure_patterns(config: Mapping[str, Any], root: Path) -> list[Pattern]:
if rx.search(line)
)
measured.append(Pattern(str(spec["id"]), str(spec["label"]), not why, why, hits))
return measured
present = bool(sentinel) and any(str(p.relative_to(root)) == sentinel for p in files)
return measured, Surface(len(files), unreadable, source, present)
def score_no_claude_path(config: Mapping[str, Any], root: Path) -> Row:
measured = measure_patterns(config, root)
measured, surface = measure_patterns(config, root)
n = len(measured)
k = sum(1 for p in measured if p.valid and not p.hits)
files = (
len(surface_files(root, list(config["roots"])))
if "roots" in config
else len(published_files(root, config.get("manifest", {}))[0])
)
exceptions: list[str] = []
for pattern in measured:
if not pattern.valid:
@ -501,14 +670,23 @@ def score_no_claude_path(config: Mapping[str, Any], root: Path) -> Row:
elif pattern.hits:
shown = ", ".join(pattern.hits[:3])
exceptions.append(f"{pattern.id}: {len(pattern.hits)} treff — {shown}")
sentinel = str(config.get("manifest", {}).get("sentinel", ""))
if not surface.sentinel:
exceptions.append(
f"flaten mangler sin sentinel ({sentinel}) — {surface.files} filer sett, "
"et fravær uten nevner er ikke et bevis"
)
status = NOT_MEASURED if not surface.sentinel else GREEN if n and k == n else RED
return Row(
"ingen-claude-vei",
"3 vakter mot en vei fra po til Claude",
k,
n,
GREEN if n and k == n else RED,
f"{k} av {n} vakter måler og gir null treff over {files} publiserte filer",
status,
f"{k} av {n} vakter måler og gir null treff over {surface.files} publiserte filer "
f"({surface.source}; {surface.unreadable} ulesbare)",
exceptions=tuple(exceptions),
attests=(NO_CLAUDE_LIMIT,),
)
@ -520,18 +698,35 @@ def score_no_claude_path(config: Mapping[str, Any], root: Path) -> Row:
def score_no_model_calls(
config: Mapping[str, Any], outcomes: Mapping[str, str], src: Path | None = None
) -> Row:
checks = config["checks"]
verdicts = {name: _probe_verdict(list(ids), outcomes) for name, ids in checks.items()}
k = sum(1 for why in verdicts.values() if not why)
n = len(verdicts)
"""Rad 4s nevner er sjekkene hvis NAVNGITTE kilde faktisk finnes i koden.
M = 3 var en konstant i gatens egen konfig. bærer hver sjekk et symbol i kilden den
handler om kjøringen selv, rundebinderen, og v1-gatens ``FORM_OK``. Forsvinner symbolet,
faller sjekken ut av nevneren OG navngis, i stedet for å bli en grønn rad over ingenting."""
source_root = src if src is not None else _PACKAGE_SRC
counted: dict[str, str] = {}
exceptions: list[str] = []
for name, check in config["checks"].items():
source = check["source"]
tree = _tree(source_root, str(source["module"]))
if tree is None or not _defined(tree, str(source["symbol"])):
exceptions.append(
f"{name}: kilden {source['module']}::{source['symbol']} finnes ikke — "
"sjekken teller ikke med i nevneren"
)
continue
counted[name] = _probe_verdict(list(check.get("probe", ())), outcomes)
k = sum(1 for why in counted.values() if not why)
n = len(counted)
exceptions += [f"{name}: {why}" for name, why in counted.items() if why]
return Row(
"ingen-modellkall",
"4 løpet drevet uten et eneste modellkall",
k,
n,
GREEN if n and k == n else RED,
f"{k} av {n} sjekker har en bestått probe",
exceptions=tuple(f"{name}: {why}" for name, why in verdicts.items() if why),
f"{k} av {n} sjekker har en bestått probe (nevneren er sjekker med en kilde i koden)",
exceptions=tuple(exceptions),
attests=(BUDGET_IN_B,),
)
@ -582,12 +777,18 @@ def foundry_intact(config: Mapping[str, Any], src: Path) -> tuple[bool, tuple[st
def score_foundry(config: Mapping[str, Any], outcomes: Mapping[str, str], src: Path) -> Row:
intact, missing = foundry_intact(config, src)
"""Hvert strukturelle krav telles for seg, og hvert av dem er et symbol i kilden.
M = 2 blandet profil-medlemmene, fabrikken og injeksjonssømmen til ÉN tellende enhet: tre
krav kunne falle uten at tallet endret seg med mer enn én. Nevneren er medlemmene
kontrakten navngir, pluss fabrikken, pluss sømmen, pluss probene."""
_intact, missing = foundry_intact(config, src)
structural = len(config["profile"]["members"]) + 2
verdicts = {
name: _probe_verdict(list(ids), outcomes) for name, ids in config["evidence"].items()
}
k = int(intact) + sum(1 for why in verdicts.values() if not why)
n = 1 + len(verdicts)
k = structural - len(missing) + sum(1 for why in verdicts.values() if not why)
n = structural + len(verdicts)
exceptions = [f"urørt: {m}" for m in missing]
exceptions += [f"{name}: {why}" for name, why in verdicts.items() if why]
return Row(
@ -596,7 +797,8 @@ def score_foundry(config: Mapping[str, Any], outcomes: Mapping[str, str], src: P
k,
n,
GREEN if k == n else RED,
"profil, fabrikk og injeksjonssøm står" if intact else "Foundry-veien er rørt",
f"{structural - len(missing)} av {structural} strukturkrav står"
+ ("" if not missing else " — Foundry-veien er rørt"),
exceptions=tuple(exceptions),
)
@ -613,7 +815,23 @@ class RunbookAttestation:
why: str
def read_runbook_attestation(path: Path, keys: Sequence[str]) -> RunbookAttestation:
def read_runbook_attestation(
path: Path,
keys: Sequence[str],
runbook: Path,
runbook_name: str,
now: datetime | None = None,
) -> RunbookAttestation:
"""Operatørens bekreftelse på kjøreboka — VALIDERT, ikke bare til stede.
Målt 19.09.2026: den gamle parseren godtok ``kjørebok: x``, datoen ``x``, en framtidsdato og
en peker til en helt annen fil alle tre ga 2 av 2 GRØNN. Fire ting stemme : fila
navngir kontraktens kjørebok, sjekksummen er kjørebokas egen (en attestering av en annen
versjon er ikke en attestering av denne), noen står som den som kjørte den, og datoen er en
ekte ISO-dato som ikke ligger i framtiden. Datoregelen er v1-gatens egen (``_parse_given``),
gjenbrukt og ikke skrevet om: en dato med klokkeslett ha tidssone.
BOM tåles (``utf-8-sig``), som v1-gaten: en editor som skriver den er ikke personens feil."""
if path.is_symlink():
return RunbookAttestation(False, False, f"{path.name} er en symlenke, ikke en fil")
if path.is_dir():
@ -621,7 +839,7 @@ def read_runbook_attestation(path: Path, keys: Sequence[str]) -> RunbookAttestat
if not path.is_file():
return RunbookAttestation(False, False, f"{path} finnes ikke")
try:
text = path.read_text(encoding="utf-8")
text = path.read_text(encoding="utf-8-sig")
except (UnicodeDecodeError, OSError):
return RunbookAttestation(True, False, f"{path.name} er ikke UTF-8")
seen: dict[str, str] = {}
@ -638,6 +856,37 @@ def read_runbook_attestation(path: Path, keys: Sequence[str]) -> RunbookAttestat
missing = [k for k in keys if not seen.get(k)]
if missing:
return RunbookAttestation(True, False, f"mangler {', '.join(missing)}")
if seen["kjørebok"] != runbook_name:
return RunbookAttestation(
True,
False,
f"attesterer kjørebok {seen['kjørebok']!r}, ikke kontraktens {runbook_name!r}",
)
if not runbook.is_file():
return RunbookAttestation(True, False, f"kjørebok {runbook_name} finnes ikke å sjekksumme")
digest = hashlib.sha256(runbook.read_bytes()).hexdigest()
if seen["sjekksum"].casefold() != digest:
return RunbookAttestation(
True,
False,
f"sjekksum {seen['sjekksum']!r} er ikke kjørebokas ({digest[:12]}…) — attesteringen "
"gjelder en annen versjon",
)
given, why = _parse_given(seen["dato"])
if given is None:
return RunbookAttestation(True, False, why)
clock = now if now is not None else datetime.now(tz=timezone.utc)
future = (
given > clock
if isinstance(given, datetime)
else given > clock.astimezone(timezone.utc).date()
)
if future:
return RunbookAttestation(
True,
False,
f"attestert {given.isoformat()}, i framtiden (nå: {clock.isoformat(timespec='seconds')})",
)
return RunbookAttestation(True, True, "")
@ -645,13 +894,16 @@ def score_runbook(
config: Mapping[str, Any],
repo_root: Path,
attest: Path | None = None,
now: Any | None = None,
now: datetime | None = None,
) -> Row:
runbook = repo_root / str(config["path"])
has_runbook = runbook.is_file() and bool(runbook.read_text(encoding="utf-8").strip())
attest_path = attest if attest is not None else repo_root / str(config["attestation"])
attestation = read_runbook_attestation(attest_path, list(config["keys"]))
attestation = read_runbook_attestation(
attest_path, list(config["keys"]), runbook, str(config["path"]), now
)
k = int(has_runbook) + int(attestation.ok)
n = len(config["artefacts"])
exceptions: list[str] = []
if not has_runbook:
exceptions.append(f"kjørebok: {config['path']} finnes ikke")
@ -661,9 +913,9 @@ def score_runbook(
"kjørebok",
"6 kjøreboka finnes og er kjørt",
k,
2,
GREEN if k == 2 else NOT_MEASURED,
"kjøreboka og operatørens bekreftelse står" if k == 2 else "venter på operatøren",
n,
GREEN if k == n else NOT_MEASURED,
"kjøreboka og operatørens bekreftelse står" if k == n else "venter på operatøren",
exceptions=tuple(exceptions),
attests=(RUNBOOK_RULE,),
)
@ -675,13 +927,12 @@ def score_runbook(
def probe_nodeids(config: Mapping[str, Any]) -> list[str]:
ids: list[str] = []
for group in (
config["roles"]["evidence"],
config["no_model_calls"]["checks"],
config["foundry"]["evidence"],
):
ids += [n for nodeids in group.values() for n in nodeids]
ids: list[str] = [n for step in config["steps"] for n in step.get("probe", ())]
ids += [n for nodeids in config["roles"]["evidence"].values() for n in nodeids]
ids += [
n for check in config["no_model_calls"]["checks"].values() for n in check.get("probe", ())
]
ids += [n for nodeids in config["foundry"]["evidence"].values() for n in nodeids]
return ids
@ -692,16 +943,17 @@ def evaluate(
src: Path = _PACKAGE_SRC,
probe_runner: ProbeRunner | None = None,
attest: Path | None = None,
now: datetime | None = None,
) -> list[Row]:
runner = probe_runner or (lambda ids: run_probes(ids, repo_root))
outcomes = runner(probe_nodeids(config))
return [
score_toolbox(config["steps"], src),
score_toolbox(config["steps"], src, config.get("run_path"), outcomes, repo_root),
score_roles(config["roles"], outcomes, src),
score_no_claude_path(config["no_claude_path"], repo_root),
score_no_model_calls(config["no_model_calls"], outcomes),
score_no_model_calls(config["no_model_calls"], outcomes, src),
score_foundry(config["foundry"], outcomes, src),
score_runbook(config["runbook"], repo_root, attest),
score_runbook(config["runbook"], repo_root, attest, now),
]
@ -713,6 +965,11 @@ def render(rows: Sequence[Row]) -> str:
for attest in row.attests:
out.append(f" [{row.title.split()[0]}] {attest}.")
out.append("")
for row in rows:
for note in row.diagnostics:
out.append(f" [{row.title.split()[0]}] {note}")
if any(r.diagnostics for r in rows):
out.append("")
out.append("Unntak fra 100 %:")
for row in rows:
for exception in row.exceptions:

View file

@ -41,10 +41,10 @@ _NOW = datetime(2026, 9, 19, 12, 0, tzinfo=timezone.utc)
#: Repoets egne tall ved skriving (19.09.2026), talt av armene under mot KILDEN. De står her for
#: at en stille endring i kjørestien skal vise seg som en rød test og ikke som et nytt tall.
_STEPS_TODAY = 13
_STEPS_TODAY = 17
_EXTERNAL_TODAY = 3
_ROLES_TODAY = ("proposer", "checker")
_PATTERNS_TODAY = 6
_PATTERNS_TODAY = 9
def _write(path: Path, text: str) -> None:
@ -100,16 +100,18 @@ def _independently_counted_steps() -> tuple[int, int]:
def test_the_step_denominator_equals_an_independent_count_of_the_source() -> None:
resolved, called = _independently_counted_steps()
assert resolved == called == _STEPS_TODAY, "kjørestien har endret seg — oppdater nevneren"
row = gate.score_toolbox(_CONFIG["steps"], _SRC)
row = gate.score_toolbox(_CONFIG["steps"], _SRC, _CONFIG["run_path"], {}, _REPO)
assert row.n == _STEPS_TODAY
def test_row1_today_is_red_with_three_of_thirteen_steps_callable_from_outside() -> None:
row = gate.score_toolbox(_CONFIG["steps"], _SRC)
def test_row1_today_is_red_with_three_of_seventeen_steps_callable_from_outside() -> None:
passing = {n: "passed" for step in _CONFIG["steps"] for n in step.get("probe", ())}
row = gate.score_toolbox(_CONFIG["steps"], _SRC, _CONFIG["run_path"], passing, _REPO)
assert (row.k, row.n, row.status) == (_EXTERNAL_TODAY, _STEPS_TODAY, gate.RED)
named = {x.split(":")[0] for x in row.exceptions}
assert "rundebinding" not in named and "gate" not in named
assert "validering" in named and "utboks" in named
assert "prepass-artefakt" in named and "parse-feil" in named
def _fake_src(root: Path, *, chat_client: bool, symbol: str = "gjoer_noe") -> Path:
@ -126,11 +128,17 @@ def _fake_src(root: Path, *, chat_client: bool, symbol: str = "gjoer_noe") -> Pa
"def _hjelper():\n" + body + "\n\n"
"def kjor():\n"
f" _hjelper()\n return {symbol}()\n\n\n"
"def main(argv=None):\n return kjor()\n",
"def main(argv=None):\n return kjor()\n\n\n"
'if __name__ == "__main__":\n raise SystemExit(main())\n',
)
return src
#: En dør er registrert OG bevist med atferd. De konstruerte kildene under bærer begge deler.
_FAKE_PROBE = "tests/test_konstruert.py::test_doeren_skriver_artefaktet"
_FAKE_PASSED = {_FAKE_PROBE: "passed"}
def _one_step(symbol: str = "gjoer_noe") -> list[dict[str, Any]]:
return [
{
@ -139,21 +147,22 @@ def _one_step(symbol: str = "gjoer_noe") -> list[dict[str, Any]]:
"module": "steg.py",
"symbol": symbol,
"driver": {"module": "driver.py", "scope": "kjor"},
"entry": {"kind": "cli", "module": "driver.py", "scope": "main"},
"entry": {"kind": "module-main", "module": "driver.py", "scope": "main"},
"probe": [_FAKE_PROBE],
}
]
def test_a_step_reachable_from_a_cli_entry_without_a_chat_client_counts(tmp_path: Path) -> None:
src = _fake_src(tmp_path, chat_client=False)
row = gate.score_toolbox(_one_step(), src)
row = gate.score_toolbox(_one_step(), src, None, _FAKE_PASSED, tmp_path)
assert (row.k, row.n, row.status) == (1, 1, gate.GREEN)
def test_m2_a_step_that_needs_a_chat_client_is_not_callable_from_outside(tmp_path: Path) -> None:
"""M-2: fjernes chatklient-sjekken, hopper k fra 0 til 1 og denne armen feller mutanten."""
src = _fake_src(tmp_path, chat_client=True)
row = gate.score_toolbox(_one_step(), src)
row = gate.score_toolbox(_one_step(), src, None, _FAKE_PASSED, tmp_path)
assert (row.k, row.n, row.status) == (0, 1, gate.RED)
assert any("chatklient" in x for x in row.exceptions)
@ -170,10 +179,10 @@ def test_m1_the_denominator_follows_the_source_and_is_not_the_config_length(
"module": "steg.py",
"symbol": "finnes_ikke",
"driver": {"module": "driver.py", "scope": "kjor"},
"entry": {"kind": "cli", "module": "driver.py", "scope": "main"},
"entry": {"kind": "module-main", "module": "driver.py", "scope": "main"},
}
]
row = gate.score_toolbox(steps, src)
row = gate.score_toolbox(steps, src, None, _FAKE_PASSED, tmp_path)
assert row.n == 1, "et steg uten symbol i kilden er ikke et steg"
assert any("borte" in x for x in row.exceptions)
@ -188,10 +197,10 @@ def test_a_step_the_driver_no_longer_calls_leaves_the_denominator(tmp_path: Path
"module": "steg.py",
"symbol": "ubrukt",
"driver": {"module": "driver.py", "scope": "kjor"},
"entry": {"kind": "cli", "module": "driver.py", "scope": "main"},
"entry": {"kind": "module-main", "module": "driver.py", "scope": "main"},
}
]
row = gate.score_toolbox(steps, src)
row = gate.score_toolbox(steps, src, None, _FAKE_PASSED, tmp_path)
assert row.n == 1
@ -199,7 +208,7 @@ def test_an_entry_without_a_main_is_no_entry(tmp_path: Path) -> None:
src = _fake_src(tmp_path, chat_client=False)
text = (src / "driver.py").read_text(encoding="utf-8").replace("def main(", "def _main(")
_write(src / "driver.py", text)
row = gate.score_toolbox(_one_step(), src)
row = gate.score_toolbox(_one_step(), src, None, _FAKE_PASSED, tmp_path)
assert (row.k, row.n) == (0, 1)
assert any("main" in x for x in row.exceptions)
@ -263,12 +272,18 @@ def test_every_pattern_hits_its_own_known_positive_and_misses_its_known_negative
assert not rx.search(gate.decode(spec["known_negative_b64"])), spec["id"]
def _clean_surface(tmp_path: Path) -> dict[str, Any]:
"""En ren, konstruert flate med sin egen sentinel — nevneren er aldri 0 her."""
cfg = json.loads(json.dumps(_CONFIG["no_claude_path"]))
cfg["manifest"] = {"sentinel": "src/rent.py", "skip_dirs": [".git", "__pycache__"]}
_write(tmp_path / "src" / "rent.py", "print('ingenting her')\n")
return cfg
def test_m3_the_denominator_is_the_number_of_patterns_that_can_measure(tmp_path: Path) -> None:
"""M-3: fjernes et mønster, faller nevneren — og et mønster som ikke treffer sin egen
kjent-positive telles ikke som en vakt."""
cfg = json.loads(json.dumps(_CONFIG["no_claude_path"]))
cfg["roots"] = ["src"]
_write(tmp_path / "src" / "rent.py", "print('ingenting her')\n")
cfg = _clean_surface(tmp_path)
row = gate.score_no_claude_path(cfg, tmp_path)
assert (row.k, row.n, row.status) == (_PATTERNS_TODAY, _PATTERNS_TODAY, gate.GREEN)
@ -283,8 +298,8 @@ def test_m3_the_denominator_is_the_number_of_patterns_that_can_measure(tmp_path:
def test_a_planted_path_to_claude_turns_the_row_red_for_that_pattern(tmp_path: Path) -> None:
cfg = json.loads(json.dumps(_CONFIG["no_claude_path"]))
cfg["roots"] = ["src"]
cfg = _clean_surface(tmp_path)
assert gate.score_no_claude_path(cfg, tmp_path).status == gate.GREEN, "rc-0-kontrollen først"
for spec in cfg["patterns"]:
_write(
tmp_path / "src" / f"{spec['id']}.py", gate.decode(spec["known_positive_b64"]) + "\n"
@ -297,8 +312,7 @@ def test_a_planted_path_to_claude_turns_the_row_red_for_that_pattern(tmp_path: P
def test_markdown_counts_only_inside_fenced_blocks(tmp_path: Path) -> None:
"""«docs med kjørbare kommandoer»: en pakkenavn-omtale i brødtekst er ikke en vei til Claude;
den samme linja i en kodeblokk er en kommando noen kan kjøre."""
cfg = json.loads(json.dumps(_CONFIG["no_claude_path"]))
cfg["roots"] = ["docs"]
cfg = _clean_surface(tmp_path)
spec = next(s for s in cfg["patterns"] if s["id"] == "maf-anthropic")
line = gate.decode(spec["known_positive_b64"])
_write(tmp_path / "docs" / "prosa.md", f"- vurdert og forkastet: `{line}` (aldri tatt inn)\n")
@ -317,7 +331,7 @@ def test_row3_on_the_real_published_surface_is_green_today() -> None:
def test_the_surface_names_its_denominator_in_files_not_in_prose() -> None:
files = gate.surface_files(_REPO, _CONFIG["no_claude_path"]["roots"])
files, _ = gate.published_files(_REPO, _manifest())
assert len(files) > 100, "en tom flate ville gitt 0 treff uten å måle noe"
assert any(f.name == "run.py" for f in files)
@ -328,22 +342,23 @@ def test_the_surface_names_its_denominator_in_files_not_in_prose() -> None:
def test_row4_is_red_today_and_names_all_three_checks() -> None:
row = gate.score_no_model_calls(_CONFIG["no_model_calls"], {})
row = gate.score_no_model_calls(_CONFIG["no_model_calls"], {}, _SRC)
assert (row.k, row.n, row.status) == (0, 3, gate.RED)
assert len(row.exceptions) == 3
def test_row4_counts_only_checks_whose_probes_pass() -> None:
checks = _CONFIG["no_model_calls"]["checks"]
outcomes = {n: "passed" for ns in checks.values() for n in ns}
assert gate.score_no_model_calls(_CONFIG["no_model_calls"], outcomes).status == gate.GREEN
outcomes[checks["fullfører"][0]] = "skipped"
row = gate.score_no_model_calls(_CONFIG["no_model_calls"], outcomes)
outcomes = {n: "passed" for c in checks.values() for n in c["probe"]}
row = gate.score_no_model_calls(_CONFIG["no_model_calls"], outcomes, _SRC)
assert row.status == gate.GREEN
outcomes[checks["fullfører"]["probe"][0]] = "skipped"
row = gate.score_no_model_calls(_CONFIG["no_model_calls"], outcomes, _SRC)
assert (row.k, row.status) == (2, gate.RED)
def test_row4_states_what_the_budget_guard_is_in_b_instead_of_pretending() -> None:
row = gate.score_no_model_calls(_CONFIG["no_model_calls"], {})
row = gate.score_no_model_calls(_CONFIG["no_model_calls"], {}, _SRC)
assert any("budsjett" in a.casefold() for a in row.attests)
assert "valgt" in gate.BUDGET_IN_B.casefold()
@ -368,11 +383,12 @@ def test_the_foundry_check_fails_when_the_azure_profile_is_gone(tmp_path: Path)
def test_row5_is_red_until_the_schema_comparison_probe_passes() -> None:
structural = len(_CONFIG["foundry"]["profile"]["members"]) + 2
row = gate.score_foundry(_CONFIG["foundry"], {}, _SRC)
assert (row.k, row.n, row.status) == (1, 2, gate.RED)
assert (row.k, row.n, row.status) == (structural, structural + 1, gate.RED)
nodeid = _CONFIG["foundry"]["evidence"]["samme-skjema"][0]
row = gate.score_foundry(_CONFIG["foundry"], {nodeid: "passed"}, _SRC)
assert (row.k, row.n, row.status) == (2, 2, gate.GREEN)
assert (row.k, row.n, row.status) == (structural + 1, structural + 1, gate.GREEN)
# ---------------------------------------------------------------------------------------------
@ -394,20 +410,26 @@ def test_row6_is_still_not_measured_without_the_operators_attestation(tmp_path:
def test_row6_goes_green_only_with_every_required_key(tmp_path: Path) -> None:
cfg = _CONFIG["runbook"]
_write(tmp_path / cfg["path"], "# kjørebok\n")
digest = _runbook(tmp_path)
attest = tmp_path / cfg["attestation"]
_write(attest, "kjørebok: docs/kjoerebok-verktoeykassen.md\nkjørt: operatøren\n")
assert gate.score_runbook(cfg, tmp_path).status == gate.NOT_MEASURED
_write(attest, "kjørebok: x\nkjørt: operatøren\ndato: 2026-09-20\n")
row = gate.score_runbook(cfg, tmp_path)
_write(attest, f"kjørebok: {cfg['path']}\nsjekksum: {digest}\nkjørt: operatøren\n")
row = gate.score_runbook(cfg, tmp_path, now=_NOW)
assert row.status == gate.NOT_MEASURED
assert any("dato" in x for x in row.exceptions)
_attestation_lines(tmp_path, digest)
row = gate.score_runbook(cfg, tmp_path, now=_NOW)
assert (row.k, row.n, row.status) == (2, 2, gate.GREEN)
def test_a_duplicated_key_in_the_attestation_is_red(tmp_path: Path) -> None:
cfg = _CONFIG["runbook"]
_write(tmp_path / cfg["path"], "# kjørebok\n")
_write(tmp_path / cfg["attestation"], "kjørebok: x\nkjørt: a\nkjørt: b\ndato: 2026-09-20\n")
assert gate.score_runbook(cfg, tmp_path).status == gate.NOT_MEASURED
digest = _runbook(tmp_path)
_attestation_lines(tmp_path, digest)
path = tmp_path / cfg["attestation"]
path.write_text(path.read_text(encoding="utf-8") + "kjørt: en annen\n", encoding="utf-8")
row = gate.score_runbook(cfg, tmp_path, now=_NOW)
assert row.status == gate.NOT_MEASURED
assert any("to ganger" in x for x in row.exceptions)
def test_m5_the_gate_never_writes_the_attestation_itself(tmp_path: Path) -> None:
@ -686,13 +708,11 @@ def test_a_console_script_is_a_door_only_when_the_manifest_registers_it(tmp_path
def test_a_module_main_is_a_door_only_with_the_dunder_guard(tmp_path: Path) -> None:
src = _fake_src(tmp_path, chat_client=False)
entry = {"kind": "module-main", "module": "driver.py", "scope": "main"}
assert gate.registered_entry(src, tmp_path, "fakesrc", entry)[0] is False
_write(
src / "driver.py",
(src / "driver.py").read_text(encoding="utf-8")
+ '\n\nif __name__ == "__main__":\n raise SystemExit(main())\n',
)
assert gate.registered_entry(src, tmp_path, "fakesrc", entry) == (True, "")
stripped = (src / "driver.py").read_text(encoding="utf-8")
_write(src / "driver.py", stripped[: stripped.index("if __name__")])
ok, why = gate.registered_entry(src, tmp_path, "fakesrc", entry)
assert ok is False and "__main__" in why
def test_a_subcommand_is_a_door_only_when_the_parser_registers_that_name(tmp_path: Path) -> None:
@ -719,7 +739,7 @@ def test_a_stub_main_is_not_a_door_without_a_registered_entry(tmp_path: Path) ->
_fake_pyproject(tmp_path, {})
steps = _one_step()
steps[0]["entry"] = {"kind": "console-script", "module": "driver.py", "scope": "main"}
row = gate.score_toolbox(steps, src, None, {}, tmp_path)
row = gate.score_toolbox(steps, src, None, _FAKE_PASSED, tmp_path)
assert (row.k, row.n) == (0, 1)
assert any("pyproject" in x for x in row.exceptions), row.exceptions
@ -729,32 +749,21 @@ def test_a_registered_door_without_a_passing_behaviour_probe_does_not_count(
) -> None:
"""«Kallet med en fixture gir det dokumenterte artefaktet» — et navn er ikke en atferd."""
src = _fake_src(tmp_path, chat_client=False)
_write(
src / "driver.py",
(src / "driver.py").read_text(encoding="utf-8")
+ '\n\nif __name__ == "__main__":\n raise SystemExit(main())\n',
)
steps = _one_step()
steps[0]["entry"] = {"kind": "module-main", "module": "driver.py", "scope": "main"}
steps[0]["probe"] = ["tests/test_noe.py::test_doeren_skriver_artefaktet"]
row = gate.score_toolbox(steps, src, None, {}, tmp_path)
assert (row.k, row.n) == (0, 1)
assert any("probe" in x for x in row.exceptions), row.exceptions
passed = {"tests/test_noe.py::test_doeren_skriver_artefaktet": "passed"}
row = gate.score_toolbox(steps, src, None, passed, tmp_path)
assert any("atferdsprobe" in x for x in row.exceptions), row.exceptions
row = gate.score_toolbox(steps, src, None, {_FAKE_PROBE: "skipped"}, tmp_path)
assert row.k == 0, "hoppet over er ikke bestått"
row = gate.score_toolbox(steps, src, None, _FAKE_PASSED, tmp_path)
assert (row.k, row.n, row.status) == (1, 1, gate.GREEN)
def test_a_step_without_any_probe_is_never_a_door(tmp_path: Path) -> None:
src = _fake_src(tmp_path, chat_client=False)
_write(
src / "driver.py",
(src / "driver.py").read_text(encoding="utf-8")
+ '\n\nif __name__ == "__main__":\n raise SystemExit(main())\n',
)
steps = _one_step()
steps[0]["entry"] = {"kind": "module-main", "module": "driver.py", "scope": "main"}
row = gate.score_toolbox(steps, src, None, {}, tmp_path)
steps[0].pop("probe")
row = gate.score_toolbox(steps, src, None, _FAKE_PASSED, tmp_path)
assert (row.k, row.n) == (0, 1)
assert any("ingen atferdsprobe" in x for x in row.exceptions), row.exceptions