fix(b-gate): a probe is bound to its step, a reason cannot be empty, and the row's limit is what was measured [skip-docs]
Row 1 goes 3 of 17 -> 0 of 17. Nothing was removed from the product and no row
changed colour; the three that counted stopped counting because the contract they
satisfied was satisfiable without capability.
BEARING 1 -- the probe is bound to the step, and the binding is MEASURED in the
probe's own source. Chosen: read the probe (ast) rather than demand it live in a
contract-named file, because a file name is a convention a stub meets as easily as a
real probe. Three traits, each measured: it drives the DOOR (the dotted module or the
registered command name appears as a string it uses -- anywhere but a docstring,
because the honest form assembles argv in a variable first), it names the STEP (the
symbol, id or subcommand as a whole WORD in what it passes INTO a call or calls), and
it asserts at all. A probe claimed by two steps proves at most one and the gate cannot
tell which -- so neither.
Two traps found while measuring, both closed:
- substring: "gate" is not named by portfolio_optimiser.evals.v1_gate
- local alias: the first cut accepted step `gate` because the probe file imports the
module AS `gate`. Names are therefore read only where they are sent or called.
What that costs, measured against the contract that stands:
rundebinding drives the door, names no step (was green)
rapport never goes through the door at all (was green)
gate drives the door, names no step (was green)
BEARING 2 -- the limit statement said exactly TWO ways remained; the checkpoint
planted 21 call forms and measured SIX. Four are closed with a guard each (the
official Python SDK in both spellings, the node and uv runners, a dynamic import);
three remain and are now named: a runtime-composed name, a name from an environment
variable, a base64-decoded name. Left open deliberately -- the encodings are not
enumerable and our own contract stores base64 by design. Row 3: 9 of 9 -> 12 of 12,
still GREEN, 0 hits over 512 files. One of the three caught a command written in this
round's own test docstring; it was rewritten, not exempted.
BEARING 3 -- held_out accepted an EMPTY reason and shrank the denominator, while the
summary said "held out with a reason" either way. A blank reason is no reason: the
symbol stays in the denominator as a call without a door, the summary counts reasons,
and the four steps declared OUTSIDE the run path are now named one by one as having no
derived source instead of being counted in silence.
Five small rests, closed: a pruned manifest (451 of 512 was still GREEN) is now NOT
MEASURED, one sentinel per area the old handlist missed; a non-UTF-8 file is read as
byte text instead of counted and skipped; a symlink out of the tree is named and
fails the row; a runbook whose whole content is "x" no longer passes, the contract
names its sections; and the row states that its ratio is not a coverage measure.
Two stated, not closed, each with its reason in the row's own attestation: a po call
moved one floor down into a helper leaves the denominator (following helpers would
pull private ones in and make the denominator the curated list this row exists to
avoid), and row 3's k/n can still be padded by a guard with no measured escape behind
it. The ledger's two precision errors are corrected: "no row became greener" is true
of colour, not of numbers, and ENTRY_KINDS has three arts, not four.
Suite: 2172 passed, 5 skipped, 5 xfailed in 645 s. ruff and mypy clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
40869e272f
commit
9eec31b7da
4 changed files with 441 additions and 73 deletions
|
|
@ -3268,9 +3268,13 @@
|
||||||
ordre `20260919T082156Z-338455001`):** operatørbeslutningen er at Claude Code LEDER i utvikling
|
ordre `20260919T082156Z-338455001`):** operatørbeslutningen er at Claude Code LEDER i utvikling
|
||||||
og test, at po er verktøykassen, og at produksjon kjører Foundry uten Claude i det hele tatt.
|
og test, at po er verktøykassen, og at produksjon kjører Foundry uten Claude i det hele tatt.
|
||||||
`python -m portfolio_optimiser.evals.b_gate` er kontrakten den kapabiliteten skal leveres inn i,
|
`python -m portfolio_optimiser.evals.b_gate` er kontrakten den kapabiliteten skal leveres inn i,
|
||||||
skrevet RØD før noe bygges: **3 av 17 · 0 av 2 · 9 av 9 · 0 av 3 · 4 av 5 · IKKE MÅLT**, exit 1.
|
skrevet RØD før noe bygges: **0 av 17 · 0 av 2 · 12 av 12 · 0 av 3 · 4 av 5 · IKKE MÅLT**, exit 1.
|
||||||
(Første utgave sa `3 av 13 · … · 6 av 6 · … · 1 av 2`; sjekkpunktet viste at tre av de nevnerne
|
(Utgave 1 sa `3 av 13 · … · 6 av 6 · … · 1 av 2`, utgave 2 `3 av 17 · … · 9 av 9 · … · 4 av 5`;
|
||||||
var gatens egne tall, ikke kildens. Ingen rad ble GRØNNERE av reparasjonen — nevnerne ble sanne.)
|
begge sjekkpunktene viste at nevnere var gatens egne tall. **Ingen rad har skiftet FARGE i noen av
|
||||||
|
de to reparasjonene — men tall har flyttet seg i begge retninger, og det skal sies presist:** rad
|
||||||
|
5 gikk `1 av 2` → `4 av 5` fordi ENHETEN ble delt opp (0,50 → 0,80 — grønnere i tall, samme
|
||||||
|
farge), rad 3 `6 av 6` → `9 av 9` → `12 av 12` fordi flaten og vaktene vokste, og rad 1 `3 av 17`
|
||||||
|
→ `0 av 17` fordi proben ble bundet til steget. Ingen rad har blitt grønnere av en svakere prøve.)
|
||||||
**Rad 1s nevner UTLEDES av kjørestien** (`run_path_calls`): 41 po-funksjoner kalles i
|
**Rad 1s nevner UTLEDES av kjørestien** (`run_path_calls`): 41 po-funksjoner kalles i
|
||||||
`run.py::run_project` (PMs 39 deterministiske + `generate_via_llm`/`fresh_workflow`, som holdes
|
`run.py::run_project` (PMs 39 deterministiske + `generate_via_llm`/`fresh_workflow`, som holdes
|
||||||
utenfor fordi de krever chatklient). Et kall som verken er erklært som steg eller navngitt som
|
utenfor fordi de krever chatklient). Et kall som verken er erklært som steg eller navngitt som
|
||||||
|
|
@ -3283,20 +3287,21 @@
|
||||||
`entry["kind"]` leses (den ble lest 0 ganger før) og må være én av tre arter gaten har kode for å
|
`entry["kind"]` leses (den ble lest 0 ganger før) og må være én av tre arter gaten har kode for å
|
||||||
etterprøve — `console-script` i pyproject, `module-main` med sin egen `__main__`-vakt,
|
etterprøve — `console-script` i pyproject, `module-main` med sin egen `__main__`-vakt,
|
||||||
`subcommand` registrert i modulens egen argparse — og hvert steg må dessuten ha en navngitt probe
|
`subcommand` registrert i modulens egen argparse — og hvert steg må dessuten ha en navngitt probe
|
||||||
som kaller døren og leser artefaktet. Målt: en modul med bare `def main(): return <symbol>()` tok
|
som er BUNDET til steget (avsnittet under). Målt: en modul med bare `def main(): return
|
||||||
rad 1 fra 3 til 4 av 13 før; nå avvises den under alle fire arter, med grunn.
|
<symbol>()` tok rad 1 fra 3 til 4 av 13 før; nå avvises den under alle tre artene (`ENTRY_KINDS`
|
||||||
|
har TRE arter, ikke fire), med grunn.
|
||||||
**«Kallbar utenfra» betyr uten chatklient:** en inngang som når steget via en vei der
|
**«Kallbar utenfra» betyr uten chatklient:** en inngang som når steget via en vei der
|
||||||
`create_chat_client`/`client_factory`/`_default_factory` nevnes, teller IKKE (`entry_reaches`) —
|
`create_chat_client`/`client_factory`/`_default_factory` nevnes, teller IKKE (`entry_reaches`) —
|
||||||
det er hele grunnen til at `run.py`-stegene er røde mens rundebinderen og v1-gaten er grønne.
|
det er hele grunnen til at `run.py`-stegene er røde mens rundebinderen og v1-gaten er grønne.
|
||||||
Sjekken er med vilje strengere enn nødvendig: den leser hele det `main` når i sin egen modul,
|
Sjekken er med vilje strengere enn nødvendig: den leser hele det `main` når i sin egen modul,
|
||||||
ikke bare den ene veien ned. **Rad 3 er den eneste raden som er grønn, og den er grønn som en
|
ikke bare den ene veien ned. **Rad 3 er den eneste raden som er grønn, og den er grønn som en
|
||||||
MÅLING:** ni vakter, hver med sin kjent-positive OG kjent-negative prøve, over **512 publiserte
|
MÅLING:** tolv vakter, hver med sin kjent-positive OG kjent-negative prøve, over **512 publiserte
|
||||||
filer lest av REPO-MANIFESTET** (`git ls-files`, eller filtreet selv i et rent uttrekk — som ER
|
filer lest av REPO-MANIFESTET** (`git ls-files`, eller filtreet selv i et rent uttrekk — som ER
|
||||||
det publiserte). Håndlista på elleve `roots` den erstattet så 433 av dem: `main.py`,
|
det publiserte). Håndlista på elleve `roots` den erstattet så 433 av dem: `main.py`,
|
||||||
`examples/`, `spikes/`, `contexts/`, `CLAUDE.md` og `llms.txt` lå utenfor, og alle seks
|
`examples/`, `spikes/`, `contexts/`, `CLAUDE.md` og `llms.txt` lå utenfor, og alle seks
|
||||||
kjent-positive kunne plantes i `main.py` uten at raden merket det. Manifestet gjør tallet
|
kjent-positive kunne plantes i `main.py` uten at raden merket det. Manifestet gjør tallet
|
||||||
reproduserbart: 433 i uttrekk og 435 i arbeidstreet var de to gitignorerte `.local.md`-filene
|
reproduserbart: 433 i uttrekk og 435 i arbeidstreet var de to gitignorerte `.local.md`-filene
|
||||||
under `docs/plan/`. **Tre av de ni vaktene feller INDIREKTE kall** — absolutt sti, liste lagt i en
|
under `docs/plan/`. **Tre av de tolv vaktene feller INDIREKTE kall** — absolutt sti, liste lagt i en
|
||||||
variabel, konstant, shell-streng — så 5 av 5 av sjekkpunktets varianter avvises, med 0 falske
|
variabel, konstant, shell-streng — så 5 av 5 av sjekkpunktets varianter avvises, med 0 falske
|
||||||
positive målt over hele flaten. **En tom flate er `IKKE MÅLT`, aldri grønn:** raden krever en
|
positive målt over hele flaten. **En tom flate er `IKKE MÅLT`, aldri grønn:** raden krever en
|
||||||
sentinel-fil og skriver både filtallet, hvilket manifest den leste og hvor mange filer som ikke
|
sentinel-fil og skriver både filtallet, hvilket manifest den leste og hvor mange filer som ikke
|
||||||
|
|
|
||||||
|
|
@ -417,7 +417,13 @@
|
||||||
},
|
},
|
||||||
"no_claude_path": {
|
"no_claude_path": {
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"sentinel": "src/portfolio_optimiser/run.py",
|
"sentinels": [
|
||||||
|
"src/portfolio_optimiser/run.py",
|
||||||
|
"main.py",
|
||||||
|
"examples/ingest-golden-file/expected-bundle/index.md",
|
||||||
|
"spikes/__init__.py",
|
||||||
|
"contexts/dekke-og-kontrakt-lindaas-2027/bundle.txt"
|
||||||
|
],
|
||||||
"skip_dirs": [
|
"skip_dirs": [
|
||||||
".git",
|
".git",
|
||||||
"__pycache__",
|
"__pycache__",
|
||||||
|
|
@ -493,6 +499,27 @@
|
||||||
"pattern_b64": "PVxzKlsiJ10oPzpbXiInXHNdKi8pP2NsYXVkZVsiJ10=",
|
"pattern_b64": "PVxzKlsiJ10oPzpbXiInXHNdKi8pP2NsYXVkZVsiJ10=",
|
||||||
"known_positive_b64": "Q0xBVURFX0JJTiA9ICIvdXNyL2xvY2FsL2Jpbi9jbGF1ZGUi",
|
"known_positive_b64": "Q0xBVURFX0JJTiA9ICIvdXNyL2xvY2FsL2Jpbi9jbGF1ZGUi",
|
||||||
"known_negative_b64": "TU9ERUxMID0gImNsYXVkZS1vcHVzLTUi"
|
"known_negative_b64": "TU9ERUxMID0gImNsYXVkZS1vcHVzLTUi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sdk-anthropic",
|
||||||
|
"label": "import av den offisielle Anthropic-SDK-en",
|
||||||
|
"pattern_b64": "XlxzKihmcm9tfGltcG9ydClccythbnRocm9waWNcYg==",
|
||||||
|
"known_positive_b64": "aW1wb3J0IGFudGhyb3BpYw==",
|
||||||
|
"known_negative_b64": "aW1wb3J0IGFudGhyb3BpY19ub3Rlcw=="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "node-runner",
|
||||||
|
"label": "claude-code startet av en node- eller uv-kjoerer",
|
||||||
|
"pattern_b64": "KG5weHx1dnh8YnVueHxwbnBtXHMrZGx4KVxiWyInXHMsXSsoLXsxLDJ9XHdbXHctXSpbIidccyxdKykqWyInXT8oQGFudGhyb3BpYy1haS8pP2NsYXVkZS1jb2RlXGI=",
|
||||||
|
"known_positive_b64": "bnB4IEBhbnRocm9waWMtYWkvY2xhdWRlLWNvZGUgLXAgImFuYWx5c2VyIg==",
|
||||||
|
"known_negative_b64": "bnB4IHByZXR0aWVyIC0td3JpdGUgLg=="
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dynamisk-import",
|
||||||
|
"label": "dynamisk import av en Claude-klient",
|
||||||
|
"pattern_b64": "X19pbXBvcnRfX1woXHMqWyInXShjbGF1ZGVfYWdlbnRfc2RrfGNsYXVkZV9jb2RlX3Nka3xhbnRocm9waWMpWyInXQ==",
|
||||||
|
"known_positive_b64": "X19pbXBvcnRfXygiY2xhdWRlX2FnZW50X3NkayIp",
|
||||||
|
"known_negative_b64": "X19pbXBvcnRfXygiYWdlbnRfZnJhbWV3b3JrIik="
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -557,6 +584,13 @@
|
||||||
"attestering"
|
"attestering"
|
||||||
],
|
],
|
||||||
"path": "docs/kjoerebok-verktoeykassen.md",
|
"path": "docs/kjoerebok-verktoeykassen.md",
|
||||||
|
"sections": [
|
||||||
|
"Forutsetninger",
|
||||||
|
"Kommandoene",
|
||||||
|
"En hel analyse, steg for steg",
|
||||||
|
"Naar noe feiler",
|
||||||
|
"Hva gaten ikke beviser"
|
||||||
|
],
|
||||||
"attestation": "v1-rounds/kjoerebok-attestering.txt",
|
"attestation": "v1-rounds/kjoerebok-attestering.txt",
|
||||||
"keys": [
|
"keys": [
|
||||||
"kjørebok",
|
"kjørebok",
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,25 @@ EXTERNAL_DOOR = (
|
||||||
"tools Claude Code kan bruke» — en kommando er nettopp det. Gaten teller tre arter den kan "
|
"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 "
|
"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 "
|
"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 "
|
"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"
|
"nok: PROBENS EGEN KILDE leses, og tre trekk måles i den — at proben rører DØREN (dens "
|
||||||
|
"dotted modulnavn eller dens registrerte kommandonavn står i et kall der), at den navngir "
|
||||||
|
"STEGET (symbolet, id-en eller underkommandoen som et eget ord), og at den asserter i det "
|
||||||
|
"hele tatt. En probe som er DELT mellom to steg beviser høyst ett av dem, og gaten vet ikke "
|
||||||
|
"hvilket — altså ingen. Valgt å lese probens kilde framfor å kreve at den ligger i en "
|
||||||
|
"kontrakt-navngitt fil, fordi et filnavn er en konvensjon en stub oppfyller like lett som en "
|
||||||
|
"ekte probe; målt 19.09 tok fjorten stubber og ÉN urelatert bestått test raden til 17 av 17"
|
||||||
|
)
|
||||||
|
|
||||||
|
#: Rad 1: grensen for hvordan nevneren leses, sagt av raden selv.
|
||||||
|
RUN_PATH_LIMIT = (
|
||||||
|
"rad 1s nevner leses LEKSIKALSK i run_project selv, ikke gjennom hjelperne den kaller: et "
|
||||||
|
"po-kall flyttet én etasje ned, til en hjelper i samme modul, faller ut av nevneren (målt "
|
||||||
|
"19.09: 41 → 40). Gaten NAVNGIR at symbolet ikke lenger kalles i kjørestien, men feller det "
|
||||||
|
"ikke. Valgt å uttale grensen framfor å følge hjelpere transitivt, fordi et kallgraf-søk "
|
||||||
|
"ville trukket private hjelpere inn i nevneren og gjort den til en ny kuratert liste — den "
|
||||||
|
"ene tingen raden finnes for å unngå. Stegene som er erklært UTENFOR kjørestien har ingen "
|
||||||
|
"utledet nevner i det hele tatt; de står navngitt under raden, ett for ett"
|
||||||
)
|
)
|
||||||
|
|
||||||
#: Inngangsartene rad 1 kan VERIFISERE mot kilden. En art utenfor lista teller aldri som en dør:
|
#: Inngangsartene rad 1 kan VERIFISERE mot kilden. En art utenfor lista teller aldri som en dør:
|
||||||
|
|
@ -356,6 +373,200 @@ def registered_entry(
|
||||||
return True, ""
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
_TOKEN = re.compile(r"[\w-]+")
|
||||||
|
|
||||||
|
|
||||||
|
def _meaningful(node: ast.AST) -> list[ast.AST]:
|
||||||
|
"""Hver node i kroppen UNNTATT strenger som står alene — docstringen er ikke en atferd."""
|
||||||
|
stack = [node]
|
||||||
|
out: list[ast.AST] = []
|
||||||
|
while stack:
|
||||||
|
current = stack.pop()
|
||||||
|
if (
|
||||||
|
isinstance(current, ast.Expr)
|
||||||
|
and isinstance(current.value, ast.Constant)
|
||||||
|
and isinstance(current.value.value, str)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
out.append(current)
|
||||||
|
stack += list(ast.iter_child_nodes(current))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def door_strings(nodes: Sequence[ast.AST]) -> set[str]:
|
||||||
|
"""Strengene proben bruker, docstringen unntatt.
|
||||||
|
|
||||||
|
Videre enn ``call_payload`` med vilje, og grunnen er målt: den ekte probe-formen bygger
|
||||||
|
kommandoen i en variabel først (``base = [sys.executable, "-m", "<modul>"]``) og sender den
|
||||||
|
inn etterpå. Et dotted modulnavn er så spesifikt at det ER døren uansett hvor det settes
|
||||||
|
sammen — det er nettopp den formen rad 3 selv har en egen vakt for."""
|
||||||
|
found: set[str] = set()
|
||||||
|
for node in nodes:
|
||||||
|
for child in _meaningful(node):
|
||||||
|
if isinstance(child, ast.Constant) and isinstance(child.value, str):
|
||||||
|
found.add(child.value)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def call_payload(nodes: Sequence[ast.AST]) -> tuple[set[str], set[str]]:
|
||||||
|
"""(ord, hele strenger) proben sender INN i et kall, pluss navnene den kaller.
|
||||||
|
|
||||||
|
To avgrensninger, begge målt fram:
|
||||||
|
|
||||||
|
* **Bare det som går inn i et kall.** En docstring som nevner symbolet, eller en kommentar
|
||||||
|
som navngir modulen, er ikke en atferd. Leses hele funksjonskroppen, er «nevner» nok — og
|
||||||
|
da er et navn en dør igjen, bare ett hakk lenger inne.
|
||||||
|
* **Delt i ORD, aldri delstreng.** «gate» er ikke nevnt av
|
||||||
|
``portfolio_optimiser.evals.v1_gate``. Målt 19.09: en tidligere utgave av denne regelen
|
||||||
|
godtok steget «gate» fordi probefila importerte modulen som ``gate`` — den bandt til et
|
||||||
|
lokalt alias, ikke til steget."""
|
||||||
|
words: set[str] = set()
|
||||||
|
constants: set[str] = set()
|
||||||
|
for node in nodes:
|
||||||
|
for call in ast.walk(node):
|
||||||
|
if not isinstance(call, ast.Call):
|
||||||
|
continue
|
||||||
|
func = call.func
|
||||||
|
if isinstance(func, ast.Name):
|
||||||
|
words.add(func.id)
|
||||||
|
elif isinstance(func, ast.Attribute):
|
||||||
|
words.add(func.attr)
|
||||||
|
for argument in [*call.args, *[k.value for k in call.keywords]]:
|
||||||
|
for child in ast.walk(argument):
|
||||||
|
if isinstance(child, ast.Constant) and isinstance(child.value, str):
|
||||||
|
constants.add(child.value)
|
||||||
|
words |= set(_TOKEN.findall(child.value))
|
||||||
|
elif isinstance(child, ast.Name):
|
||||||
|
words.add(child.id)
|
||||||
|
elif isinstance(child, ast.Attribute):
|
||||||
|
words.add(child.attr)
|
||||||
|
return words, constants
|
||||||
|
|
||||||
|
|
||||||
|
def step_needles(step: Mapping[str, Any]) -> set[str]:
|
||||||
|
"""Ordene som skiller DETTE steget fra de andre bak samme dør."""
|
||||||
|
needles = {str(step["symbol"]), str(step["id"])}
|
||||||
|
command = str(dict(step.get("entry", {})).get("command", ""))
|
||||||
|
if command:
|
||||||
|
needles.add(command)
|
||||||
|
return needles
|
||||||
|
|
||||||
|
|
||||||
|
def drives_door(
|
||||||
|
tree: ast.AST,
|
||||||
|
nodes: Sequence[ast.AST],
|
||||||
|
entry: Mapping[str, Any],
|
||||||
|
dotted: str,
|
||||||
|
repo_root: Path,
|
||||||
|
) -> bool:
|
||||||
|
"""Proben rører døren — MÅLT i probens egen kilde, på én av to måter.
|
||||||
|
|
||||||
|
Som subprosess: dørens dotted modulnavn (``python -m <modul>``) eller dens registrerte
|
||||||
|
kommandonavn står som en streng et kall bruker. Eller i prosess: inngangen er importert fra
|
||||||
|
dørens egen modul og kalt. Alt annet er en test som består et annet sted."""
|
||||||
|
scope = str(entry.get("scope", "main"))
|
||||||
|
names = {dotted}
|
||||||
|
if str(entry.get("kind", "")) == "console-script":
|
||||||
|
target = f"{dotted}:{scope}"
|
||||||
|
names |= {name for name, value in console_scripts(repo_root).items() if value == target}
|
||||||
|
if names & door_strings(nodes):
|
||||||
|
return True
|
||||||
|
parent, _, last = dotted.rpartition(".")
|
||||||
|
aliases: set[str] = set()
|
||||||
|
imported = False
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.ImportFrom):
|
||||||
|
module = node.module or ""
|
||||||
|
if module == parent:
|
||||||
|
aliases |= {a.asname or a.name for a in node.names if a.name == last}
|
||||||
|
elif module == dotted:
|
||||||
|
imported = imported or any(a.name == scope for a in node.names)
|
||||||
|
elif isinstance(node, ast.Import):
|
||||||
|
aliases |= {
|
||||||
|
a.asname or a.name.rpartition(".")[2] for a in node.names if a.name == dotted
|
||||||
|
}
|
||||||
|
for node in nodes:
|
||||||
|
for call in ast.walk(node):
|
||||||
|
if not isinstance(call, ast.Call):
|
||||||
|
continue
|
||||||
|
func = call.func
|
||||||
|
if (
|
||||||
|
isinstance(func, ast.Attribute)
|
||||||
|
and func.attr == scope
|
||||||
|
and isinstance(func.value, ast.Name)
|
||||||
|
and func.value.id in aliases
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
if isinstance(func, ast.Name) and func.id == scope and imported:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def probe_binds(
|
||||||
|
repo_root: Path,
|
||||||
|
step: Mapping[str, Any],
|
||||||
|
nodeid: str,
|
||||||
|
package: str = "portfolio_optimiser",
|
||||||
|
) -> tuple[bool, str]:
|
||||||
|
"""(proben beviser DETTE steget, grunnen når den ikke gjør det).
|
||||||
|
|
||||||
|
Målt 19.09.2026 (N2/N2b): gaten slo opp om nodeid-en var ``passed`` og godtok den. En
|
||||||
|
registrert stub-dør med en probe pekt på en vilkårlig grønn test tok rad 1 fra 3 til 4 av 17
|
||||||
|
— og gjort for alle fjorten røde steg: 17 av 17 GRØNN, uten én linje kapabilitet. Bindingen
|
||||||
|
er derfor en MÅLING i probens kilde, ikke en erklæring i kontrakten."""
|
||||||
|
file_part, separator, rest = nodeid.partition("::")
|
||||||
|
if not separator or not rest:
|
||||||
|
return False, f"proben {nodeid} navngir ingen test"
|
||||||
|
path = repo_root / file_part
|
||||||
|
if not path.is_file():
|
||||||
|
return False, f"probefila {file_part} finnes ikke"
|
||||||
|
try:
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, SyntaxError, UnicodeDecodeError):
|
||||||
|
return False, f"probefila {file_part} lar seg ikke lese som kilde"
|
||||||
|
name = rest.rpartition("::")[2]
|
||||||
|
functions = _functions(tree)
|
||||||
|
if name not in functions:
|
||||||
|
return False, f"proben {name} finnes ikke i {file_part}"
|
||||||
|
nodes = [functions[n] for n in sorted(_reachable(functions, name))]
|
||||||
|
if not any(isinstance(x, ast.Assert) for node in nodes for x in ast.walk(node)):
|
||||||
|
return False, f"proben {name} asserter ingenting — da måler den ikke"
|
||||||
|
dotted = _dotted(package, str(step["entry"]["module"]))
|
||||||
|
if not drives_door(tree, nodes, step["entry"], dotted, repo_root):
|
||||||
|
return False, f"proben {name} rører ikke døren ({dotted})"
|
||||||
|
needles = step_needles(step)
|
||||||
|
words, _constants = call_payload(nodes)
|
||||||
|
if not needles & words:
|
||||||
|
return False, f"proben {name} navngir ikke steget ({', '.join(sorted(needles))})"
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
def shared_probes(steps: Sequence[Mapping[str, Any]]) -> dict[str, list[str]]:
|
||||||
|
"""Nodeid-er mer enn ett steg gjør krav på. En probe beviser ett steg, eller ingen."""
|
||||||
|
claimed: dict[str, set[str]] = {}
|
||||||
|
for step in steps:
|
||||||
|
for nodeid in step.get("probe", ()):
|
||||||
|
claimed.setdefault(str(nodeid), set()).add(str(step["id"]))
|
||||||
|
return {nodeid: sorted(ids) for nodeid, ids in claimed.items() if len(ids) > 1}
|
||||||
|
|
||||||
|
|
||||||
|
def _binding_why(
|
||||||
|
step: Mapping[str, Any],
|
||||||
|
probes: Sequence[str],
|
||||||
|
repo_root: Path,
|
||||||
|
package: str,
|
||||||
|
shared: Mapping[str, Sequence[str]],
|
||||||
|
) -> str:
|
||||||
|
for nodeid in probes:
|
||||||
|
if nodeid in shared:
|
||||||
|
others = ", ".join(i for i in shared[nodeid] if i != str(step["id"]))
|
||||||
|
return f"atferdsprobe: delt med {others} — en probe beviser ett steg, eller ingen"
|
||||||
|
ok, why = probe_binds(repo_root, step, nodeid, package)
|
||||||
|
if not ok:
|
||||||
|
return f"atferdsprobe: {why}"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def run_path_calls(src: Path, spec: Mapping[str, Any]) -> dict[str, str]:
|
def run_path_calls(src: Path, spec: Mapping[str, Any]) -> dict[str, str]:
|
||||||
"""``symbol -> modulen det kom fra``: po-funksjonene kjørestien FAKTISK kaller.
|
"""``symbol -> modulen det kom fra``: po-funksjonene kjørestien FAKTISK kaller.
|
||||||
|
|
||||||
|
|
@ -406,6 +617,7 @@ def measure_steps(
|
||||||
package: str = "portfolio_optimiser",
|
package: str = "portfolio_optimiser",
|
||||||
) -> list[Step]:
|
) -> list[Step]:
|
||||||
measured: list[Step] = []
|
measured: list[Step] = []
|
||||||
|
shared = shared_probes(steps)
|
||||||
for step in steps:
|
for step in steps:
|
||||||
tree = _tree(src, str(step["module"]))
|
tree = _tree(src, str(step["module"]))
|
||||||
resolved = tree is not None and _defined(tree, str(step["symbol"]))
|
resolved = tree is not None and _defined(tree, str(step["symbol"]))
|
||||||
|
|
@ -421,13 +633,14 @@ def measure_steps(
|
||||||
if ok:
|
if ok:
|
||||||
ok, why = registered_entry(src, repo_root, package, step["entry"])
|
ok, why = registered_entry(src, repo_root, package, step["entry"])
|
||||||
if ok:
|
if ok:
|
||||||
probes = list(step.get("probe", ()))
|
probes = [str(n) for n in step.get("probe", ())]
|
||||||
verdict = _probe_verdict(probes, outcomes or {})
|
verdict = _probe_verdict(probes, outcomes or {})
|
||||||
why = (
|
if not probes:
|
||||||
"ingen atferdsprobe registrert — et navn er ikke en atferd"
|
why = "ingen atferdsprobe registrert — et navn er ikke en atferd"
|
||||||
if not probes
|
elif verdict:
|
||||||
else (f"atferdsprobe: {verdict}" if verdict else "")
|
why = f"atferdsprobe: {verdict}"
|
||||||
)
|
else:
|
||||||
|
why = _binding_why(step, probes, repo_root, package, shared)
|
||||||
external = resolved and called and not why
|
external = resolved and called and not why
|
||||||
measured.append(Step(str(step["id"]), str(step["label"]), resolved, called, external, why))
|
measured.append(Step(str(step["id"]), str(step["label"]), resolved, called, external, why))
|
||||||
return measured
|
return measured
|
||||||
|
|
@ -444,25 +657,41 @@ def score_toolbox(
|
||||||
measured = measure_steps(steps, src, outcomes, repo_root, package)
|
measured = measure_steps(steps, src, outcomes, repo_root, package)
|
||||||
in_path = [s for s in measured if s.resolved and s.called]
|
in_path = [s for s in measured if s.resolved and s.called]
|
||||||
declared = {str(s["symbol"]) for s in steps}
|
declared = {str(s["symbol"]) for s in steps}
|
||||||
held = {
|
claimed = {
|
||||||
str(h["symbol"]): str(h.get("reason", "")) for h in (run_path or {}).get("held_out", ())
|
str(h["symbol"]): str(h.get("reason", "")) for h in (run_path or {}).get("held_out", ())
|
||||||
}
|
}
|
||||||
|
#: Målt 19.09 (N4/N5): et steg flyttet til «held_out» tok nevneren 17 → 16 MED ELLER UTEN
|
||||||
|
#: grunn, og sammendraget sa «med grunn» uansett. En tom grunn er ingen grunn: symbolet blir
|
||||||
|
#: stående i nevneren som et kall uten dør, nøyaktig som et udeklarert kall.
|
||||||
|
held = {symbol: reason for symbol, reason in claimed.items() if reason.strip()}
|
||||||
calls = run_path_calls(src, run_path) if run_path else {}
|
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)
|
undeclared = sorted(name for name in calls if name not in declared and name not in held)
|
||||||
n = len(in_path) + len(undeclared)
|
n = len(in_path) + len(undeclared)
|
||||||
k = sum(1 for s in in_path if s.external)
|
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(
|
exceptions = tuple(f"{s.id}: {s.why}" for s in measured if not s.external) + tuple(
|
||||||
f"udeklarert: {name} kalles i {calls[name]} fra kjørestien uten å være et steg"
|
(
|
||||||
|
f"holdt utenfor UTEN grunn: {name} kalles i {calls[name]} fra kjørestien og teller "
|
||||||
|
"i nevneren"
|
||||||
|
if name in claimed
|
||||||
|
else f"udeklarert: {name} kalles i {calls[name]} fra kjørestien uten å være et steg"
|
||||||
|
)
|
||||||
for name in undeclared
|
for name in undeclared
|
||||||
)
|
)
|
||||||
diagnostics = tuple(
|
diagnostics = tuple(
|
||||||
f"holdt utenfor: {symbol} — {reason or 'UTEN GRUNN'}"
|
f"holdt utenfor: {symbol} — {reason or 'UTEN GRUNN'}"
|
||||||
for symbol, reason in sorted(held.items())
|
for symbol, reason in sorted(claimed.items())
|
||||||
) + tuple(
|
) + tuple(
|
||||||
f"holdt utenfor: {symbol} kalles ikke lenger i kjørestien (foreldet unntak)"
|
f"holdt utenfor: {symbol} kalles ikke lenger i kjørestien (foreldet unntak)"
|
||||||
for symbol in sorted(held)
|
for symbol in sorted(held)
|
||||||
if calls and symbol not in calls
|
if calls and symbol not in calls
|
||||||
)
|
)
|
||||||
|
driver = (str((run_path or {}).get("module", "")), str((run_path or {}).get("scope", "")))
|
||||||
|
off_path = tuple(
|
||||||
|
f"utenfor kjørestien: {s['id']} (driver {s['driver']['module']}::{s['driver']['scope']}) "
|
||||||
|
"— uten utledet kilde, kuratert i kontrakten"
|
||||||
|
for s in steps
|
||||||
|
if run_path and (str(s["driver"]["module"]), str(s["driver"]["scope"])) != driver
|
||||||
|
)
|
||||||
return Row(
|
return Row(
|
||||||
"verktøykasse",
|
"verktøykasse",
|
||||||
"1 steg i kjørestien kallbare utenfra",
|
"1 steg i kjørestien kallbare utenfra",
|
||||||
|
|
@ -470,10 +699,10 @@ def score_toolbox(
|
||||||
n,
|
n,
|
||||||
GREEN if n and k == n else RED,
|
GREEN if n and k == n else RED,
|
||||||
f"{k} av {n} steg har en dør ut; {len(held)} kall holdt utenfor med grunn, "
|
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",
|
f"{len(off_path)} erklærte steg er utenfor kjørestien",
|
||||||
exceptions=exceptions,
|
exceptions=exceptions,
|
||||||
diagnostics=diagnostics,
|
diagnostics=diagnostics + off_path,
|
||||||
attests=(EXTERNAL_DOOR,),
|
attests=(EXTERNAL_DOOR, RUN_PATH_LIMIT),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -539,19 +768,73 @@ def score_roles(roles: Mapping[str, Any], outcomes: Mapping[str, str], src: Path
|
||||||
|
|
||||||
_FENCE = re.compile(r"^\s*(```|~~~)")
|
_FENCE = re.compile(r"^\s*(```|~~~)")
|
||||||
|
|
||||||
#: Rad 3: hva vakten IKKE rekker, sagt av raden selv. Fire av PMs fem indirekte kall-varianter
|
#: Rad 3: hva vakten IKKE rekker, sagt av raden selv — og ETTERPRØVD. Erklæringen oppga to
|
||||||
#: slapp forbi før 19.09; disse to gjør det fortsatt, og de er linje-lokale grenser i en
|
#: gjenstående veier; PM plantet 21 kallformer 19.09 og MÅLTE at seks slapp. De fire billigste
|
||||||
#: tekstvakt, ikke noe en regex kan lukke.
|
#: (den offisielle SDK-en i to skriveformer, node- og uv-kjørerne, det dynamiske import-kallet)
|
||||||
|
#: er lukket med hver sin vakt; de tre som står igjen står her, navngitt.
|
||||||
NO_CLAUDE_LIMIT = (
|
NO_CLAUDE_LIMIT = (
|
||||||
"rad 3 er en TEKSTVAKT over hele det publiserte manifestet, ikke en dataflyt-analyse: den "
|
"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 "
|
"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 "
|
"konstant, en shell-streng, den offisielle Python-SDK-en, en node- eller uv-kjører som "
|
||||||
"kommandonavn satt sammen av deler ved kjøretid, og et navn lest ut av en miljøvariabel. "
|
"starter kommandoen, og et dynamisk import-kall. TRE veier står igjen og kan ikke felles "
|
||||||
"Raden påstår derfor ikke at ingen vei finnes; den påstår at ingen SKREVET vei finnes, over "
|
"linje for linje: et kommandonavn satt sammen av deler ved kjøretid, et navn lest ut av en "
|
||||||
"en nevner den oppgir i filer"
|
"miljøvariabel, og et navn som er base64-kodet eller på annen måte avkodet først. 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. k av n er ikke et dekningsmål: det teller vakter som MÅLER "
|
||||||
|
"(hver med kjent-positiv og kjent-negativ), aldri hvor stor del av angrepsflaten de dekker "
|
||||||
|
"— en vakt lagt til uten en målt omgåelse bak seg hever både k og n og beviser ingenting"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _published_paths(root: Path, manifest: Mapping[str, Any]) -> tuple[list[Path], str]:
|
||||||
|
"""(hver publisert sti, symlenker inkludert, hvor lista kom fra)."""
|
||||||
|
skip = set(manifest.get("skip_dirs", (".git", "__pycache__")))
|
||||||
|
if (root / ".git").exists():
|
||||||
|
try:
|
||||||
|
listing: str | None = 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_symlink() or p.is_file()], "git-manifestet"
|
||||||
|
walked = [
|
||||||
|
path
|
||||||
|
for path in sorted(root.rglob("*"))
|
||||||
|
if (path.is_symlink() or path.is_file()) and not (skip & set(path.relative_to(root).parts))
|
||||||
|
]
|
||||||
|
return walked, "filtreet (uttrekk uten git-metadata)"
|
||||||
|
|
||||||
|
|
||||||
|
def escaping_symlinks(root: Path, manifest: Mapping[str, Any]) -> tuple[str, ...]:
|
||||||
|
"""Symlenker i den publiserte flaten som peker UT av treet.
|
||||||
|
|
||||||
|
Målt 19.09 (N10b): symlenker ble filtrert bort. Peker lenka INN i treet er målet skannet
|
||||||
|
selv, og lenka er en dublett; peker den UT, bar den en kommando ingen leste. Den blir ikke
|
||||||
|
skannet (innholdet publiseres ikke), men den KAN ikke passere i stillhet."""
|
||||||
|
paths, _source = _published_paths(root, manifest)
|
||||||
|
anchor = root.resolve()
|
||||||
|
escaping: list[str] = []
|
||||||
|
for path in paths:
|
||||||
|
if not path.is_symlink():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
target = path.resolve()
|
||||||
|
except OSError: # pragma: no cover - en brutt lenke er også en lenke ut
|
||||||
|
escaping.append(str(path.relative_to(root)))
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
target.relative_to(anchor)
|
||||||
|
except ValueError:
|
||||||
|
escaping.append(str(path.relative_to(root)))
|
||||||
|
return tuple(sorted(escaping))
|
||||||
|
|
||||||
|
|
||||||
def published_files(root: Path, manifest: Mapping[str, Any]) -> tuple[list[Path], str]:
|
def published_files(root: Path, manifest: Mapping[str, Any]) -> tuple[list[Path], str]:
|
||||||
"""(filene som PUBLISERES, hvor lista kom fra).
|
"""(filene som PUBLISERES, hvor lista kom fra).
|
||||||
|
|
||||||
|
|
@ -563,29 +846,8 @@ def published_files(root: Path, manifest: Mapping[str, Any]) -> tuple[list[Path]
|
||||||
|
|
||||||
Ingen suffiks-allowlist heller: en fil som lar seg lese som tekst kan bære en kommandolinje
|
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."""
|
uansett hva den heter. Det som ikke lar seg avkode, telles og oppgis."""
|
||||||
skip = set(manifest.get("skip_dirs", (".git", "__pycache__")))
|
paths, source = _published_paths(root, manifest)
|
||||||
if (root / ".git").exists():
|
return [p for p in paths if p.is_file() and not p.is_symlink()], source
|
||||||
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]]:
|
def scannable_lines(path: Path, text: str) -> list[tuple[int, str]]:
|
||||||
|
|
@ -622,18 +884,42 @@ class Surface:
|
||||||
unreadable: int
|
unreadable: int
|
||||||
source: str
|
source: str
|
||||||
sentinel: bool
|
sentinel: bool
|
||||||
|
missing: tuple[str, ...] = ()
|
||||||
|
escaping: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
def sentinels_of(manifest: Mapping[str, Any]) -> list[str]:
|
||||||
|
"""Filene som MÅ ligge i flaten for at et fravær skal bety noe.
|
||||||
|
|
||||||
|
Målt 19.09 (N6): ÉN sentinel stopper en TOM flate, ikke en BESKÅRET. Manifestet filtrert fra
|
||||||
|
512 til 451 filer — hele ``examples/``, ``spikes/``, ``contexts/`` og ``main.py`` — ga
|
||||||
|
fortsatt GRØNN, og det var nøyaktig de områdene den gamle håndlista også utelot. Kontrakten
|
||||||
|
planter derfor én sentinel per område; forsvinner ett av dem, er raden IKKE MÅLT."""
|
||||||
|
listed = [str(s) for s in manifest.get("sentinels", ())]
|
||||||
|
single = str(manifest.get("sentinel", ""))
|
||||||
|
if single and single not in listed:
|
||||||
|
listed.append(single)
|
||||||
|
return listed
|
||||||
|
|
||||||
|
|
||||||
def measure_patterns(config: Mapping[str, Any], root: Path) -> tuple[list[Pattern], Surface]:
|
def measure_patterns(config: Mapping[str, Any], root: Path) -> tuple[list[Pattern], Surface]:
|
||||||
manifest = config.get("manifest", {})
|
manifest = config.get("manifest", {})
|
||||||
files, source = published_files(root, manifest)
|
files, source = published_files(root, manifest)
|
||||||
sentinel = str(manifest.get("sentinel", ""))
|
|
||||||
scanned: list[tuple[Path, list[tuple[int, str]]]] = []
|
scanned: list[tuple[Path, list[tuple[int, str]]]] = []
|
||||||
unreadable = 0
|
unreadable = 0
|
||||||
for path in files:
|
for path in files:
|
||||||
try:
|
try:
|
||||||
text = path.read_text(encoding="utf-8")
|
text = path.read_text(encoding="utf-8")
|
||||||
except (UnicodeDecodeError, OSError):
|
except UnicodeDecodeError:
|
||||||
|
#: Målt 19.09 (N9): «\xff\xfe» + kommandoen ga GRØNN, og fila ble bare TELT som
|
||||||
|
#: ulesbar. Et fravær som er målt bort er ikke et fravær. latin-1 kan ikke feile og
|
||||||
|
#: bevarer hver ASCII-byte, så en kommandolinje i en ellers binær fil leses nå.
|
||||||
|
unreadable += 1
|
||||||
|
try:
|
||||||
|
text = path.read_bytes().decode("latin-1")
|
||||||
|
except OSError: # pragma: no cover - fila forsvant mellom to lesninger
|
||||||
|
continue
|
||||||
|
except OSError: # pragma: no cover - en fil som ikke kan åpnes publiseres ikke
|
||||||
unreadable += 1
|
unreadable += 1
|
||||||
continue
|
continue
|
||||||
scanned.append((path, scannable_lines(path, text)))
|
scanned.append((path, scannable_lines(path, text)))
|
||||||
|
|
@ -655,8 +941,17 @@ def measure_patterns(config: Mapping[str, Any], root: Path) -> tuple[list[Patter
|
||||||
if rx.search(line)
|
if rx.search(line)
|
||||||
)
|
)
|
||||||
measured.append(Pattern(str(spec["id"]), str(spec["label"]), not why, why, hits))
|
measured.append(Pattern(str(spec["id"]), str(spec["label"]), not why, why, hits))
|
||||||
present = bool(sentinel) and any(str(p.relative_to(root)) == sentinel for p in files)
|
names = {str(p.relative_to(root)) for p in files}
|
||||||
return measured, Surface(len(files), unreadable, source, present)
|
wanted = sentinels_of(manifest)
|
||||||
|
missing = tuple(s for s in wanted if s not in names)
|
||||||
|
return measured, Surface(
|
||||||
|
len(files),
|
||||||
|
unreadable,
|
||||||
|
source,
|
||||||
|
bool(wanted) and not missing,
|
||||||
|
missing or (("(ingen sentinel erklært)",) if not wanted else ()),
|
||||||
|
escaping_symlinks(root, manifest),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def score_no_claude_path(config: Mapping[str, Any], root: Path) -> Row:
|
def score_no_claude_path(config: Mapping[str, Any], root: Path) -> Row:
|
||||||
|
|
@ -670,13 +965,23 @@ def score_no_claude_path(config: Mapping[str, Any], root: Path) -> Row:
|
||||||
elif pattern.hits:
|
elif pattern.hits:
|
||||||
shown = ", ".join(pattern.hits[:3])
|
shown = ", ".join(pattern.hits[:3])
|
||||||
exceptions.append(f"{pattern.id}: {len(pattern.hits)} treff — {shown}")
|
exceptions.append(f"{pattern.id}: {len(pattern.hits)} treff — {shown}")
|
||||||
sentinel = str(config.get("manifest", {}).get("sentinel", ""))
|
|
||||||
if not surface.sentinel:
|
if not surface.sentinel:
|
||||||
exceptions.append(
|
exceptions.append(
|
||||||
f"flaten mangler sin sentinel ({sentinel}) — {surface.files} filer sett, "
|
f"flaten mangler sin sentinel ({', '.join(surface.missing)}) — {surface.files} "
|
||||||
"et fravær uten nevner er ikke et bevis"
|
"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
|
for link in surface.escaping:
|
||||||
|
exceptions.append(
|
||||||
|
f"symlenke ut av treet: {link} — målet publiseres ikke og skannes ikke, "
|
||||||
|
"og lenka kan bære en kommando raden aldri leser"
|
||||||
|
)
|
||||||
|
status = (
|
||||||
|
NOT_MEASURED
|
||||||
|
if not surface.sentinel
|
||||||
|
else GREEN
|
||||||
|
if n and k == n and not surface.escaping
|
||||||
|
else RED
|
||||||
|
)
|
||||||
return Row(
|
return Row(
|
||||||
"ingen-claude-vei",
|
"ingen-claude-vei",
|
||||||
"3 vakter mot en vei fra po til Claude",
|
"3 vakter mot en vei fra po til Claude",
|
||||||
|
|
@ -684,7 +989,7 @@ def score_no_claude_path(config: Mapping[str, Any], root: Path) -> Row:
|
||||||
n,
|
n,
|
||||||
status,
|
status,
|
||||||
f"{k} av {n} vakter måler og gir null treff over {surface.files} publiserte filer "
|
f"{k} av {n} vakter måler og gir null treff over {surface.files} publiserte filer "
|
||||||
f"({surface.source}; {surface.unreadable} ulesbare)",
|
f"({surface.source}; {surface.unreadable} ikke-UTF-8, lest som byte-tekst)",
|
||||||
exceptions=tuple(exceptions),
|
exceptions=tuple(exceptions),
|
||||||
attests=(NO_CLAUDE_LIMIT,),
|
attests=(NO_CLAUDE_LIMIT,),
|
||||||
)
|
)
|
||||||
|
|
@ -897,7 +1202,16 @@ def score_runbook(
|
||||||
now: datetime | None = None,
|
now: datetime | None = None,
|
||||||
) -> Row:
|
) -> Row:
|
||||||
runbook = repo_root / str(config["path"])
|
runbook = repo_root / str(config["path"])
|
||||||
has_runbook = runbook.is_file() and bool(runbook.read_text(encoding="utf-8").strip())
|
try:
|
||||||
|
text = runbook.read_text(encoding="utf-8") if runbook.is_file() else ""
|
||||||
|
except (OSError, UnicodeDecodeError): # pragma: no cover - en ulesbar kjørebok er ingen bok
|
||||||
|
text = ""
|
||||||
|
#: Målt 19.09 (N3): en kjørebok hvis HELE innhold er «x», med en korrekt sjekksummet
|
||||||
|
#: attestering, ga «2 av 2 GRØNN». Tom fil var alt som var avvist. Kontrakten navngir derfor
|
||||||
|
#: seksjonene en kjørebok må bære — det gjør den ikke sann, men det gjør «x» umulig.
|
||||||
|
sections = [str(s) for s in config.get("sections", ())]
|
||||||
|
missing_sections = [s for s in sections if s not in text]
|
||||||
|
has_runbook = bool(text.strip()) and not missing_sections
|
||||||
attest_path = attest if attest is not None else repo_root / str(config["attestation"])
|
attest_path = attest if attest is not None else repo_root / str(config["attestation"])
|
||||||
attestation = read_runbook_attestation(
|
attestation = read_runbook_attestation(
|
||||||
attest_path, list(config["keys"]), runbook, str(config["path"]), now
|
attest_path, list(config["keys"]), runbook, str(config["path"]), now
|
||||||
|
|
@ -905,8 +1219,13 @@ def score_runbook(
|
||||||
k = int(has_runbook) + int(attestation.ok)
|
k = int(has_runbook) + int(attestation.ok)
|
||||||
n = len(config["artefacts"])
|
n = len(config["artefacts"])
|
||||||
exceptions: list[str] = []
|
exceptions: list[str] = []
|
||||||
if not has_runbook:
|
if not text.strip():
|
||||||
exceptions.append(f"kjørebok: {config['path']} finnes ikke")
|
exceptions.append(f"kjørebok: {config['path']} finnes ikke")
|
||||||
|
elif missing_sections:
|
||||||
|
exceptions.append(
|
||||||
|
f"kjørebok: {config['path']} mangler seksjonene kontrakten navngir "
|
||||||
|
f"({', '.join(missing_sections)})"
|
||||||
|
)
|
||||||
if not attestation.ok:
|
if not attestation.ok:
|
||||||
exceptions.append(f"attestering: {attestation.why}")
|
exceptions.append(f"attestering: {attestation.why}")
|
||||||
return Row(
|
return Row(
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,9 @@ _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
|
#: 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.
|
#: at en stille endring i kjørestien skal vise seg som en rød test og ikke som et nytt tall.
|
||||||
_STEPS_TODAY = 17
|
_STEPS_TODAY = 17
|
||||||
_EXTERNAL_TODAY = 3
|
_EXTERNAL_TODAY = 0
|
||||||
_ROLES_TODAY = ("proposer", "checker")
|
_ROLES_TODAY = ("proposer", "checker")
|
||||||
_PATTERNS_TODAY = 9
|
_PATTERNS_TODAY = 12
|
||||||
|
|
||||||
|
|
||||||
def _write(path: Path, text: str) -> None:
|
def _write(path: Path, text: str) -> None:
|
||||||
|
|
@ -106,14 +106,18 @@ def test_the_step_denominator_equals_an_independent_count_of_the_source() -> Non
|
||||||
assert row.n == _STEPS_TODAY
|
assert row.n == _STEPS_TODAY
|
||||||
|
|
||||||
|
|
||||||
def test_row1_today_is_red_with_three_of_seventeen_steps_callable_from_outside() -> None:
|
def test_row1_today_is_red_with_no_step_callable_from_outside() -> None:
|
||||||
|
"""Var «3 av 17» til 19.09. De tre falt da proben ble BUNDET til steget: to av dem driver
|
||||||
|
riktig dør uten å navngi hvilket steg de beviser, den tredje går ikke gjennom døren i det
|
||||||
|
hele tatt. Ingen av dem var en kodefeil — det var kontrakten som godtok dem."""
|
||||||
passing = {n: "passed" for step in _CONFIG["steps"] for n in step.get("probe", ())}
|
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)
|
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)
|
assert (row.k, row.n, row.status) == (_EXTERNAL_TODAY, _STEPS_TODAY, gate.RED)
|
||||||
named = {x.split(":")[0] for x in row.exceptions}
|
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 "validering" in named and "utboks" in named
|
||||||
assert "prepass-artefakt" in named and "parse-feil" in named
|
assert "prepass-artefakt" in named and "parse-feil" in named
|
||||||
|
bound = {x.split(":")[0] for x in row.exceptions if "atferdsprobe" in x}
|
||||||
|
assert bound == {"rundebinding", "rapport", "gate"}, row.exceptions
|
||||||
|
|
||||||
|
|
||||||
def _fake_src(root: Path, *, chat_client: bool, symbol: str = "gjoer_noe") -> Path:
|
def _fake_src(root: Path, *, chat_client: bool, symbol: str = "gjoer_noe") -> Path:
|
||||||
|
|
@ -441,7 +445,7 @@ def test_row6_is_not_measured_while_the_runbook_is_missing(tmp_path: Path) -> No
|
||||||
|
|
||||||
|
|
||||||
def test_row6_is_still_not_measured_without_the_operators_attestation(tmp_path: Path) -> None:
|
def test_row6_is_still_not_measured_without_the_operators_attestation(tmp_path: Path) -> None:
|
||||||
_write(tmp_path / _CONFIG["runbook"]["path"], "# kjørebok\n\nsteg 1 …\n")
|
_runbook(tmp_path)
|
||||||
row = gate.score_runbook(_CONFIG["runbook"], tmp_path)
|
row = gate.score_runbook(_CONFIG["runbook"], tmp_path)
|
||||||
assert (row.k, row.n, row.status) == (1, 2, gate.NOT_MEASURED)
|
assert (row.k, row.n, row.status) == (1, 2, gate.NOT_MEASURED)
|
||||||
|
|
||||||
|
|
@ -840,9 +844,7 @@ def test_the_surface_count_matches_an_independent_count_of_the_manifest() -> Non
|
||||||
repository») finnes ikke manifestet å telle mot, og armen HOPPER OVER seg selv i stedet for å
|
repository») finnes ikke manifestet å telle mot, og armen HOPPER OVER seg selv i stedet for å
|
||||||
felle: ellers vokser «røde i uttrekket» med hver ny git-avhengig arm, og tallet slutter å
|
felle: ellers vokser «røde i uttrekket» med hver ny git-avhengig arm, og tallet slutter å
|
||||||
være et funn. Nevneren pinnes HER, ikke i armen over."""
|
være et funn. Nevneren pinnes HER, ikke i armen over."""
|
||||||
listing = subprocess.run(
|
listing = subprocess.run(["git", "ls-files", "-z"], cwd=_REPO, capture_output=True, text=True)
|
||||||
["git", "ls-files", "-z"], cwd=_REPO, capture_output=True, text=True
|
|
||||||
)
|
|
||||||
if listing.returncode != 0:
|
if listing.returncode != 0:
|
||||||
pytest.skip("uttrekk uten git-metadata: manifestet finnes ikke å telle mot")
|
pytest.skip("uttrekk uten git-metadata: manifestet finnes ikke å telle mot")
|
||||||
mine = {t for t in listing.stdout.split("\0") if t}
|
mine = {t for t in listing.stdout.split("\0") if t}
|
||||||
|
|
@ -870,7 +872,7 @@ def test_the_row_names_which_manifest_it_read() -> None:
|
||||||
files, source = gate.published_files(_REPO, _manifest())
|
files, source = gate.published_files(_REPO, _manifest())
|
||||||
assert f"over {len(files)} publiserte filer" in row.reason
|
assert f"over {len(files)} publiserte filer" in row.reason
|
||||||
assert source in row.reason
|
assert source in row.reason
|
||||||
assert f"{_UNDECODABLE_TODAY} ulesbare" in row.reason
|
assert f"{_UNDECODABLE_TODAY} ikke-UTF-8, lest som byte-tekst" in row.reason
|
||||||
|
|
||||||
|
|
||||||
def test_the_surface_falls_back_to_the_tree_when_there_is_no_git_metadata(tmp_path: Path) -> None:
|
def test_the_surface_falls_back_to_the_tree_when_there_is_no_git_metadata(tmp_path: Path) -> None:
|
||||||
|
|
@ -982,7 +984,11 @@ def test_row6_denominator_comes_from_the_artefacts_the_contract_names(tmp_path:
|
||||||
|
|
||||||
|
|
||||||
def _runbook(root: Path) -> str:
|
def _runbook(root: Path) -> str:
|
||||||
text = "# kjørebok\n\nsteg 1: naviger pakken\n"
|
"""En kjørebok som bærer seksjonene kontrakten navngir — ellers er «x» en kjørebok (N3)."""
|
||||||
|
sections = "\n".join(
|
||||||
|
f"## {s}\n\nsteg 1: naviger pakken\n" for s in _CONFIG["runbook"]["sections"]
|
||||||
|
)
|
||||||
|
text = f"# kjørebok\n\n{sections}"
|
||||||
_write(root / _CONFIG["runbook"]["path"], text)
|
_write(root / _CONFIG["runbook"]["path"], text)
|
||||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
@ -1171,9 +1177,7 @@ def test_one_probe_cannot_prove_two_steps(tmp_path: Path) -> None:
|
||||||
a = _write_probe(tmp_path, needle="gjoer_noe", nodeid="tests/test_a.py::test_a")
|
a = _write_probe(tmp_path, needle="gjoer_noe", nodeid="tests/test_a.py::test_a")
|
||||||
b = _write_probe(tmp_path, needle="gjoer_noe_annet", nodeid="tests/test_b.py::test_b")
|
b = _write_probe(tmp_path, needle="gjoer_noe_annet", nodeid="tests/test_b.py::test_b")
|
||||||
outcomes = {a: "passed", b: "passed"}
|
outcomes = {a: "passed", b: "passed"}
|
||||||
row = gate.score_toolbox(
|
row = gate.score_toolbox(_two_steps(a, b), src, _fake_run_path(), outcomes, tmp_path)
|
||||||
_two_steps(a, b), src, _fake_run_path(), outcomes, tmp_path
|
|
||||||
)
|
|
||||||
assert (row.k, row.n, row.status) == (2, 2, gate.GREEN), row.exceptions
|
assert (row.k, row.n, row.status) == (2, 2, gate.GREEN), row.exceptions
|
||||||
|
|
||||||
shared = _write_probe(
|
shared = _write_probe(
|
||||||
|
|
@ -1188,7 +1192,9 @@ def test_one_probe_cannot_prove_two_steps(tmp_path: Path) -> None:
|
||||||
assert all("delt" in x for x in row.exceptions), row.exceptions
|
assert all("delt" in x for x in row.exceptions), row.exceptions
|
||||||
|
|
||||||
|
|
||||||
def test_the_same_unrelated_green_test_cannot_turn_the_real_row1_into_a_door(tmp_path: Path) -> None:
|
def test_the_same_unrelated_green_test_cannot_turn_the_real_row1_into_a_door(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
"""N2b mot KONTRAKTEN som står: hver av de 17 stegenes probe byttet ut med én urelatert
|
"""N2b mot KONTRAKTEN som står: hver av de 17 stegenes probe byttet ut med én urelatert
|
||||||
bestått test. Målt 19.09 ga det «17 av 17 GRØNN» med fjorten stubber; her er nevneren
|
bestått test. Målt 19.09 ga det «17 av 17 GRØNN» med fjorten stubber; her er nevneren
|
||||||
repoets egen kontrakt, og ingen av stegene skal telle."""
|
repoets egen kontrakt, og ingen av stegene skal telle."""
|
||||||
|
|
@ -1297,9 +1303,13 @@ _MEASURED_ESCAPES = (
|
||||||
def test_every_way_the_checkpoint_measured_as_open_and_cheap_to_close_is_caught(
|
def test_every_way_the_checkpoint_measured_as_open_and_cheap_to_close_is_caught(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""De seks formene PM plantet og målte som USETTE 19.09: den offisielle ``anthropic``-SDK-en
|
"""De seks formene PM plantet og målte som USETTE 19.09: den offisielle Python-SDK-en (to
|
||||||
(to skriveformer), ``npx @anthropic-ai/claude-code`` (to former), ``uvx claude-code`` og et
|
skriveformer), node-kjøreren (to former), uv-kjøreren og et dynamisk import-kall. Hver
|
||||||
dynamisk import-kall. Hver plantes én om gangen, med rc-0-kontroll først og etter."""
|
plantes én om gangen, med rc-0-kontroll først og etter.
|
||||||
|
|
||||||
|
Formene STÅR IKKE skrevet her, bare base64-kodet — denne fila er selv en del av flaten raden
|
||||||
|
skanner, og en klartekst-variant ville vært sitt eget funn. Målt: den var det, i første
|
||||||
|
utkast av denne docstringen, og vakten fant den."""
|
||||||
cfg = json.loads(json.dumps(_CONFIG["no_claude_path"]))
|
cfg = json.loads(json.dumps(_CONFIG["no_claude_path"]))
|
||||||
cfg["manifest"] = {"sentinel": "src/run.py", "sentinels": ["src/run.py"]}
|
cfg["manifest"] = {"sentinel": "src/run.py", "sentinels": ["src/run.py"]}
|
||||||
_write(tmp_path / "src" / "run.py", "x = 1\n")
|
_write(tmp_path / "src" / "run.py", "x = 1\n")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue