feat(navigation,validator): read_dir names the rung that reads a document, and a run can require its anchoring
F3 and F4, the two findings the S7 acid test (session 98) reported and left. The order required both descriptions to be treated as PREMISES. One held; the other was felled before anything was built on it. F3 -- premise FELLED, asymmetry real. The order read arm C's two refused calls as "the path names a document that EXISTS". Measured against the base that ran: its root holds 27 directories named del-ii-bilag-N-... and 12 documents named inbox-del-ii-bilag-N-....md, and the requested path matches NEITHER -- it is the directory naming convention applied to a document whose real name carries an inbox- prefix. So the two live rounds were the UNKNOWN-path class, and this delivery does NOT recover them (gated). What IS real: read_file on a directory has named read_dir since session 95, while read_dir on a document named neither the rung nor the path. okf.DocumentPathRefused closes that one direction -- a ValueError, a SIBLING of BundlePathNotFound rather than a subclass, built from context_files (never files) and through the same in_dimension predicate the listing uses, quoting the document's REAL name so what it hands back resolves. F4 -- premise HELD, option (c) felled by measurement. All four live artefacts stamped cost_baseline_anchored: False and each arm invented its cost codes. derive_cost_baseline refuses against the delivered base: K2's price schedule is a pandoc SIMPLE table with ONE column header, so making --derive-cost-baseline reachable there would mean inventing a rule for an unmeasured form -- MAJOR-4's own honesty limit. Chose (b) over (a): --require-cost-baseline / run_project(require_cost_baseline=...), OPT-IN and never default, so every bundle without a cost-baseline.json runs unchanged. The gate sits where both branches have bound baseline and ABOVE the dry-run cut, so it fires on the free trip too and, on the paid one, before the first model call. Three CLI refusals by name, each with an rc-0 control. 12 mutations, all red against the WHOLE suite. Green control 1493/5 -> 1511/5 (+18 node-ids, 0 removed); golden demo-transcript.stdout BYTE-UNCHANGED (shasum -a 1 of the CONTENT = ea8c534773acdbe41ae68f2c55724d69aaf8be4f). No paid run: both findings measured offline. Measurement: docs/2026-09-08-f3-f4-nekten-og-forankringen.md Order: 20260908T020419Z-5837110336-from-portfolio-optimiser Co-Authored-By: Claude <Opus 5>
This commit is contained in:
parent
76b939b3b8
commit
9232f94041
8 changed files with 759 additions and 2 deletions
69
CLAUDE.md
69
CLAUDE.md
|
|
@ -2102,6 +2102,75 @@ Python ≥3.10. MAF (`agent-framework-core` 1.16.0, `-orchestrations` 1.1.1 —
|
||||||
`ChatClientException` (Azure 400 etter tre `quick_validate`-nekter på rad) er RAPPORTERT, ikke
|
`ChatClientException` (Azure 400 etter tre `quick_validate`-nekter på rad) er RAPPORTERT, ikke
|
||||||
fikset — den ligger utenfor `main()`s nekt-tuppel og forlater CLI-en som traceback. Måling:
|
fikset — den ligger utenfor `main()`s nekt-tuppel og forlater CLI-en som traceback. Måling:
|
||||||
`docs/2026-09-07-prepass-mater-q5b-k2.md`.
|
`docs/2026-09-07-prepass-mater-q5b-k2.md`.
|
||||||
|
- **`read_dir` på et DOKUMENT navngir rungen som leser det — og ordrens premiss ble FELT før noe
|
||||||
|
ble bygget på det (F3, 08.09, ordre `20260908T020419Z`):** ordren leste de to nektede live-kallene
|
||||||
|
(`read_dir('del-ii-bilag-6-teknisk-oppsett')` + `…-oppsett.md`, § 4 i syretesten) som «stien
|
||||||
|
navngir et dokument som FINNES». **MÅLT mot basen som kjørte** holder rotnivået 27 kataloger
|
||||||
|
`del-ii-bilag-N-…` og 12 dokumenter `inbox-del-ii-bilag-N-….md`; stien matcher **verken** — den er
|
||||||
|
katalog-navnekonvensjonen anvendt på et dokument hvis ekte navn bærer et `inbox-`-prefiks. De to
|
||||||
|
rundene var altså UKJENT-sti-klassen, og **denne leveransen gjenoppretter dem IKKE** (gatet av
|
||||||
|
`test_the_measured_live_path_is_still_the_unknown_class`); å resolvere dem ville vært å gjette
|
||||||
|
hvilket dokument en kaller MENTE — invensjon, ikke validering. **Det som ER ekte er asymmetrien:**
|
||||||
|
`read_file` på en katalog har siden økt 95 svart `DirectoryPathRefused` som navngir `read_dir`,
|
||||||
|
mens `read_dir` på et dokument svarte den generiske «has no directory» og navnga verken rungen
|
||||||
|
eller stien. `okf.DocumentPathRefused` lukker den ene retningen: en `ValueError`
|
||||||
|
(`BundlePathNotFound`/`DimensionScopeRefused`-presedensen) og en **SØSKEN av `BundlePathNotFound`,
|
||||||
|
aldri en subklasse** — «dette er et dokument» og «dette er ingenting» er ulike fakta, og en kaller
|
||||||
|
som switcher på det første skal ikke besvares av det andre. Oppslaget bygges av `context_files`,
|
||||||
|
ALDRI `files` (M2 → 1 rød: en suggestion fra vandringen ville navngitt et `type: verdict`-dokument
|
||||||
|
ved sti — i en NEKT — altså det ene laget ingen listing nevner og `read_file` avviser blankt), og
|
||||||
|
gjennom SAMME `in_dimension`-predikat listingen bruker (M3 → 1 rød: §4.1a invertert, å navngi et
|
||||||
|
dokument kjøringen straks ville nektet å åpne er S2cs «filter i navnet alene»). Dokumentets EKTE
|
||||||
|
navn siteres, aldri kallerens sti (M5 → 2 røde — `_index_excerpt`-regelen: en sti som aldri fantes
|
||||||
|
er verre enn ingen sti; armen mater den navngitte stien tilbake til `read_file`). De to grenene
|
||||||
|
deler BEVISST ingen ordlyd (M4 → 4 røde), og arm (g) i
|
||||||
|
`test_hierarchical_navigation_loadbearing` er SKREVET OM — ikke svekket — fra `ValueError` til
|
||||||
|
`BundlePathNotFound` ved navn, fordi begge nekter siterer kallerens sti og et treff på stien alene
|
||||||
|
ikke lenger kan skille dem. Load-bearing MÅLT (`tests/test_read_dir_wrong_rung_loadbearing.py`,
|
||||||
|
8 armer), **fem mutasjoner alle røde mot HELE suiten** + grønn kontroll **1511/5** og golden
|
||||||
|
BYTE-UENDRET (`shasum -a 1` av INNHOLDET = `ea8c534773acdbe41ae68f2c55724d69aaf8be4f`): M1 detach
|
||||||
|
dokument-grenen (4) · M2 bygg fra `files` (1) · M3 ignorer dimensjonen (1) · M4 kollaps de to
|
||||||
|
grenenes ordlyd (4) · M5 ekko kallerens sti (2). **Ærlighets-grenser, uttalt:** ingen LEVENDE
|
||||||
|
modell har lest den nye nekten, så at den endrer neste trekk er ikke bevist
|
||||||
|
(structured-output-grensens klasse); `read_dir`s verktøybeskrivelse er URØRT — den påstår
|
||||||
|
ingenting usant, og en utvidelse ville vært prosa uten en gate. Måling:
|
||||||
|
`docs/2026-09-08-f3-f4-nekten-og-forankringen.md` § 1.
|
||||||
|
- **En kjøring KAN kreve at gaten er forankret, og nekten koster ingenting (F4, 08.09, samme
|
||||||
|
ordre):** MÅLT på nytt mot de fire artefaktene den betalte S7-kjøringen etterlot: alle armer
|
||||||
|
stemplet `cost_baseline_anchored: False`, stage 0 ble hoppet over, og hver arm fant på kodene sine
|
||||||
|
(`ENGRAVE_MARK` · `RITB-HOURS`/`SYSINT-HOURS` · `RITB-consultancy`/`system-integrator` ·
|
||||||
|
`Material_Cost_Concrete`/`…_Steel`/`Construction_Heating_Fuel`). **Ordrens opsjon (c) er FELT AV
|
||||||
|
MÅLING:** `derive_cost_baseline` mot den leverte basen nekter — K2s prisskjema er en pandoc SIMPLE
|
||||||
|
table med ÉN kolonneoverskrift (`Prisskjema`) og hver verdi kollapset i den, så å gjøre
|
||||||
|
`--derive-cost-baseline` nåbart der ville krevd en regel for en form ingen har målt, som er
|
||||||
|
MAJOR-4s egen ærlighets-grense. **Valgt (b), ikke (a):** synligheten (`cost_baseline_notice`)
|
||||||
|
finnes og er ærlig, men den kan ikke stoppe et maskinlesbart artefakt som sier
|
||||||
|
`validator_decision: validated` over linjer ingenting forankret. `--require-cost-baseline` /
|
||||||
|
`run_project(require_cost_baseline=…)` er **OPT-IN, aldri default** — hver base uten
|
||||||
|
`cost-baseline.json`, altså hver commons-eid golden, kjører uendret, som er dét (a) beskyttet
|
||||||
|
(M12, gaten fyrer uansett flagg → **171 røde**). `UnanchoredRunRefused` er en `ValueError`
|
||||||
|
(`BundleIdMismatch`/`CostBaselineDerivationError`-presedensen): en argv som tar feil om hva denne
|
||||||
|
basen kan tilby hører på CLI-ens nekt-tuppel og hostings 400-arm, aldri krasj-kanalen. **Gaten bor
|
||||||
|
ÉTT sted — der BEGGE grener har bundet `baseline`, og OVER dry-run-kuttet** (M7 → 1 rød): den
|
||||||
|
frie turen er den billigste å lære det på, og på den betalte fyrer den før første modellkall ved
|
||||||
|
konstruksjon — armen asserterer NULL kall, aldri bare unntaket, fordi en nekt etter forbruket ser
|
||||||
|
identisk ut ved exit-koden (økt 57s regel). Tre CLI-nekter, hver med en rc-0-kontroll på en argv
|
||||||
|
som ellers ville blitt AKSEPTERT: krever `--bundle-dir` (veg-stien er forankret ved konstruksjon,
|
||||||
|
så der kunne flagget aldri fyre — et flagg som ikke kan fyre er en påstand flaten gjør om seg
|
||||||
|
selv, Fase-3-klassen), refusert i `--portfolio` VED NAVN (M9 → 1 rød med egen signatur: uten den
|
||||||
|
STARTER mutanten et porteføljepass og når modellen med `ChatClientException`, altså er nekten dét
|
||||||
|
som holder argv-en fra å koste noe) og i `report_forbidden` (report-modus returnerer OVER hver
|
||||||
|
dispatch, så en utelatelse er et stille DROPP — F4-gapets egen klasse). Load-bearing MÅLT
|
||||||
|
(`tests/test_require_cost_baseline_loadbearing.py`, 10 armer), **sju mutasjoner alle røde mot HELE
|
||||||
|
suiten** + samme grønne kontroll og golden: M6 detach gaten (3) · M7 gaten under dry-run-kuttet
|
||||||
|
(1) · M8 dropp fra `report_forbidden` (1) · M9 dropp fra portefølje-partisjonen (1) · M10 detach
|
||||||
|
`--bundle-dir`-kravet (1) · M11 detach fullkjørings-wiringen (1) · M12 gaten fyrer uansett flagg
|
||||||
|
(171). **Ærlighets-grenser, uttalt:** den hostede flaten er BEVISST urørt (feltet er i ingen av
|
||||||
|
hostings tre sett, så den generiske 400-en svarer og Fase 4es to halvdeler står — MAJOR-4s eget
|
||||||
|
valg gjentatt); stempelet sier fortsatt AT en kjøring var forankret, aldri HVILKEN av de tre
|
||||||
|
projeksjonene som forankret den; og ingen betalt kjøring er gjort — begge funn er målt offline mot
|
||||||
|
basen og artefaktene økt 98 etterlot. Måling:
|
||||||
|
`docs/2026-09-08-f3-f4-nekten-og-forankringen.md` § 2.
|
||||||
- **STATE.md er local-only** (gitignored). Voyage session-state er efemert; STATE.md er kanonisk kontinuitet.
|
- **STATE.md er local-only** (gitignored). Voyage session-state er efemert; STATE.md er kanonisk kontinuitet.
|
||||||
- Prosess: Voyage-plugin (`/trekbrief → /trekplan → /trekexecute → /trekreview`) per større fase.
|
- Prosess: Voyage-plugin (`/trekbrief → /trekplan → /trekexecute → /trekreview`) per større fase.
|
||||||
|
|
||||||
|
|
|
||||||
14
README.md
14
README.md
|
|
@ -626,6 +626,20 @@ when the seam is detached, so the loop cannot silently degrade into theater.
|
||||||
--derive-cost-baseline
|
--derive-cost-baseline
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **Requiring the run to be anchored** — `--require-cost-baseline` (opt-in, requires
|
||||||
|
`--bundle-dir`). Without a baseline the validator's stage 0 is skipped, and the run says so on
|
||||||
|
stdout — but it still finishes and still stamps `validator_decision: validated` over cost lines
|
||||||
|
nothing tied to the project. Measured on a live tender run, every arm invented its codes. This
|
||||||
|
flag turns that visibility into a refusal: no baseline, no run, and the refusal fires before the
|
||||||
|
first model call, on `--live-dry-run` as well. It stays opt-in because a bundle that ships no
|
||||||
|
`cost-baseline.json` is legitimately un-anchored — combine it with `--derive-cost-baseline` when
|
||||||
|
the base carries a priced schedule.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python -m portfolio_optimiser.run PROSJEKT-1 --docs-dir <bundle> --bundle-dir <bundle> \
|
||||||
|
--derive-cost-baseline --require-cost-baseline
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
`--explore` is refused together with `--mandate` — they are two sources of one mandate, and
|
`--explore` is refused together with `--mandate` — they are two sources of one mandate, and
|
||||||
merging would silently overwrite what you wrote. To seed an exploration with a domain expert's
|
merging would silently overwrite what you wrote. To seed an exploration with a domain expert's
|
||||||
|
|
|
||||||
102
docs/2026-09-08-f3-f4-nekten-og-forankringen.md
Normal file
102
docs/2026-09-08-f3-f4-nekten-og-forankringen.md
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
# F3 og F4 — måling før bygging (økt 100, 08.09.2026)
|
||||||
|
|
||||||
|
Ordre `20260908T020419Z-5837110336`. De to funnene S7-syretesten (økt 98) rapporterte og ikke
|
||||||
|
fikset. Ordren krevde at begge beskrivelser ble behandlet som **premisser**, ikke fakta. Det ene
|
||||||
|
holdt; det andre ble felt.
|
||||||
|
|
||||||
|
## 1. F3 — premisset er FELT, asymmetrien er ekte
|
||||||
|
|
||||||
|
**Ordrens premiss:** «når stien navngir et dokument som FINNES (med eller uten `.md`)» — altså at
|
||||||
|
de to nektede kallene i arm C var *feil rung*, ikke *ukjent sti*.
|
||||||
|
|
||||||
|
**Målt mot basen som faktisk kjørte** (`scratchpad/s7-prepass/k2-bundle-s7`, den samme
|
||||||
|
`{run_id}-debate.json` § 4 siterer):
|
||||||
|
|
||||||
|
```
|
||||||
|
read_dir(del-ii-bilag-6-teknisk-oppsett) NEKTET
|
||||||
|
read_dir(del-ii-bilag-6-teknisk-oppsett.md) NEKTET
|
||||||
|
```
|
||||||
|
|
||||||
|
Basens rotnivå holder **27 kataloger** navngitt `del-ii-bilag-N-…` og **12 rotdokumenter**
|
||||||
|
navngitt `inbox-del-ii-bilag-N-….md`. Det eneste dokumentet i nærheten heter
|
||||||
|
`inbox-del-ii-bilag-6-teknisk-oppsett.md`. Stien modellen ba om matcher **verken** en katalog
|
||||||
|
**eller** et dokument — den er katalog-navnekonvensjonen anvendt på et dokument hvis ekte navn
|
||||||
|
bærer et `inbox-`-prefiks.
|
||||||
|
|
||||||
|
**Følgen:** de to live-rundene var UKJENT-sti-klassen, ikke feil-rung-klassen. Ingenting i denne
|
||||||
|
leveransen ville reddet dem, og det står gatet i `test_the_measured_live_path_is_still_the_unknown_class`
|
||||||
|
så ingen senere leser kan lese dette som en fiks av den målte kostnaden. Å resolvere den ville
|
||||||
|
vært å gjette hvilket dokument en kaller MENTE — invensjon, ikke validering.
|
||||||
|
|
||||||
|
**Det som ER ekte:** symmetrien ordren pekte på finnes i koden i nøyaktig ÉN retning.
|
||||||
|
`read_file` på en katalog → `explore.DirectoryPathRefused`, som navngir `read_dir`. `read_dir` på
|
||||||
|
et dokument → den generiske «has no directory», som navngir verken rungen som leser det eller
|
||||||
|
stien den ville tatt. Det gapet er lukket (`okf.DocumentPathRefused`).
|
||||||
|
|
||||||
|
## 2. F4 — premisset HOLDER, og (c) er felt av måling
|
||||||
|
|
||||||
|
**Målt på nytt mot de fire artefaktene den betalte kjøringen etterlot:**
|
||||||
|
|
||||||
|
| arm | `cost_baseline_anchored` | kostkoder i forslaget |
|
||||||
|
|---|---|---|
|
||||||
|
| A | `False` | `ENGRAVE_MARK` |
|
||||||
|
| B | `False` | `RITB-HOURS`, `SYSINT-HOURS` |
|
||||||
|
| B1 | `False` | `RITB-consultancy`, `system-integrator` |
|
||||||
|
| C | `False` | `Material_Cost_Concrete`, `Material_Cost_Steel`, `Construction_Heating_Fuel` |
|
||||||
|
|
||||||
|
Stage 0 hoppes over når `baseline is None` (`validator.validate_proposal`), og ingen av linjene er
|
||||||
|
knyttet til K2.
|
||||||
|
|
||||||
|
**Opsjon (c) er felt, med måling.** `okf.derive_cost_baseline` mot den leverte basen:
|
||||||
|
|
||||||
|
```
|
||||||
|
REFUSED: no cost table found in bundle '…/k2-bundle-s7': no concept file carries a markdown table
|
||||||
|
whose header names all three of ['code', 'quantity', 'unit_cost']
|
||||||
|
```
|
||||||
|
|
||||||
|
K2s leverte prisskjema (`del-ii-bilag-7-prisskjema/prissammenstilling-sheet-1.md`) er en pandoc
|
||||||
|
**simple table med ÉN kolonneoverskrift** (`Prisskjema`) og hver verdi kollapset i den. Å gjøre
|
||||||
|
`--derive-cost-baseline` nåbart der ville krevd en regel for en form ingen har målt — nøyaktig den
|
||||||
|
ærlighets-grensen MAJOR-4 skrev ned for seg selv.
|
||||||
|
|
||||||
|
**Valgt: (b), og hvorfor ikke (a).** Synligheten (`cost_baseline_notice`) finnes og er ærlig; det
|
||||||
|
den ikke kan er å stoppe et maskinlesbart artefakt som sier `validator_decision: validated` over
|
||||||
|
linjer ingenting forankret. Et **opt-in**-flagg lar hver base uten `cost-baseline.json` — hver
|
||||||
|
commons-eid golden — kjøre nøyaktig som før, som er dét (a) beskyttet, mens en kaller som trenger
|
||||||
|
garantien ber om den ved navn. Målt: mutasjonen som lar gaten fyre uten flagget gir **171 røde**.
|
||||||
|
|
||||||
|
## 3. Battteriet
|
||||||
|
|
||||||
|
Grønn kontroll **1493/5** ved øktstart → **1511/5** etter (**+18 node-ider, 0 fjernet**), golden
|
||||||
|
`demo-transcript.stdout` BYTE-UENDRET (`shasum -a 1` av INNHOLDET =
|
||||||
|
`ea8c534773acdbe41ae68f2c55724d69aaf8be4f`). **12 mutasjoner, alle røde mot HELE suiten:**
|
||||||
|
|
||||||
|
| # | mutasjon | røde |
|
||||||
|
|---|---|---|
|
||||||
|
| M1 | detach dokument-grenen | 4 |
|
||||||
|
| M2 | bygg oppslaget fra `files` | 1 |
|
||||||
|
| M3 | ignorer dimensjonen i oppslaget | 1 |
|
||||||
|
| M4 | navngi `read_file` i ukjent-sti-meldingen òg | 4 |
|
||||||
|
| M5 | ekko kallerens sti i stedet for det ekte navnet | 2 |
|
||||||
|
| M6 | detach forankrings-gaten | 3 |
|
||||||
|
| M7 | flytt gaten UNDER dry-run-kuttet | 1 |
|
||||||
|
| M8 | dropp fra `report_forbidden` | 1 |
|
||||||
|
| M9 | dropp fra portefølje-partisjonen | 1 |
|
||||||
|
| M10 | detach `--bundle-dir`-kravet | 1 |
|
||||||
|
| M11 | detach fullkjørings-wiringen | 1 |
|
||||||
|
| M12 | gaten fyrer uansett flagg | 171 |
|
||||||
|
|
||||||
|
M9s signatur er egen: uten nekten ved navn STARTER mutanten et porteføljepass og når modellen
|
||||||
|
(`ChatClientException`) — altså er nekten dét som holder en argv fra å koste noe.
|
||||||
|
|
||||||
|
**Ingen betalt kjøring.** Begge funn er målt offline mot artefaktene og basen økt 98 etterlot.
|
||||||
|
|
||||||
|
## 4. Ærlighets-grenser, uttalt
|
||||||
|
|
||||||
|
- F3 gjenoppretter **ikke** de to målte rundene (§ 1); den lukker asymmetrien.
|
||||||
|
- Ingen LEVENDE modell har lest den nye nekten — at den faktisk endrer neste trekk er ikke bevist
|
||||||
|
(structured-output-grensens klasse).
|
||||||
|
- Den hostede flaten er **bevisst urørt** for `--require-cost-baseline` (feltet er i ingen av
|
||||||
|
hostings tre sett, så den generiske 400-en svarer og Fase 4es to halvdeler står) — MAJOR-4s eget
|
||||||
|
valg, gjentatt.
|
||||||
|
- Stempelet sier fortsatt AT en kjøring var forankret, aldri HVILKEN projeksjon som forankret den.
|
||||||
|
|
@ -1067,6 +1067,27 @@ class BundlePathNotFound(ValueError):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentPathRefused(ValueError):
|
||||||
|
"""``read_dir`` was asked for a DOCUMENT — the wrong rung of the navigation ladder.
|
||||||
|
|
||||||
|
The symmetry of ``explore.DirectoryPathRefused``, which has answered the other direction since
|
||||||
|
the live K2 run of session 95: ``read_file`` on a directory names ``read_dir``, while
|
||||||
|
``read_dir`` on a document named neither the rung that reads it nor the path it would take. A
|
||||||
|
refusal that only says "no" leaves the caller — a model choosing a path — with the same next
|
||||||
|
move it just made.
|
||||||
|
|
||||||
|
A ``ValueError``, the ``BundlePathNotFound``/``DimensionScopeRefused`` precedent, and a SIBLING
|
||||||
|
of ``BundlePathNotFound`` rather than a subclass: "this path is a document" and "this path is
|
||||||
|
nothing" are different facts, and a caller switching on the first must not be answered by the
|
||||||
|
second.
|
||||||
|
|
||||||
|
**Not the class the live two rounds fell into** (``docs/2026-09-07-syretest-s7-prepass-k2.md``
|
||||||
|
§ 4): there the path named no document at all — it was a document's name with its ``inbox-``
|
||||||
|
prefix dropped — and it is still refused as unknown. Naming the nearest look-alike would be
|
||||||
|
guessing what a caller meant, which is invention rather than validation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def directory_listing(
|
def directory_listing(
|
||||||
bundle: Bundle, path: str = "", *, dimension: str | None = None
|
bundle: Bundle, path: str = "", *, dimension: str | None = None
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
|
@ -1130,6 +1151,24 @@ def directory_listing(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if prefix and not directories and not documents:
|
if prefix and not directories and not documents:
|
||||||
|
# The wrong RUNG, answered as such — the direction ``explore.DirectoryPathRefused`` already
|
||||||
|
# covers, measured absent here (F3). Built from ``context_files`` and through the SAME
|
||||||
|
# ``in_dimension`` predicate the listing above uses: a lookup over ``files`` would name a
|
||||||
|
# ``type: verdict`` document by path, advertising in a refusal the one layer no listing
|
||||||
|
# mentions, and one that ignored the scope would name a document this run would then refuse
|
||||||
|
# to open. The document's REAL name is quoted, never the caller's path, so what the refusal
|
||||||
|
# hands back resolves.
|
||||||
|
stem = path.strip("/")
|
||||||
|
candidate = stem if stem.endswith(".md") else stem + ".md"
|
||||||
|
named = next(
|
||||||
|
(f for f in bundle.context_files if f.name == candidate and in_dimension(f, dimension)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if named is not None:
|
||||||
|
raise DocumentPathRefused(
|
||||||
|
f"{path!r} in knowledge base {bundle.dir!r} is a document, not a directory; "
|
||||||
|
f"use read_file to read {named.name!r} whole"
|
||||||
|
)
|
||||||
raise BundlePathNotFound(
|
raise BundlePathNotFound(
|
||||||
f"knowledge base {bundle.dir!r} has no directory {path!r}; it holds no concept "
|
f"knowledge base {bundle.dir!r} has no directory {path!r}; it holds no concept "
|
||||||
"document under that path"
|
"document under that path"
|
||||||
|
|
|
||||||
|
|
@ -727,6 +727,27 @@ def _default_factory(profile: Profile | str) -> Callable[[str], BaseChatClient]:
|
||||||
return factory
|
return factory
|
||||||
|
|
||||||
|
|
||||||
|
class UnanchoredRunRefused(ValueError):
|
||||||
|
"""A run was required to be anchored (F4) and the bundle offered no cost baseline.
|
||||||
|
|
||||||
|
Measured live (``docs/2026-09-07-syretest-s7-prepass-k2.md`` § 8): all three paid arms stamped
|
||||||
|
``cost_baseline_anchored: False``, so the validator's stage 0 — the one stage that tells a
|
||||||
|
fabricated cost line from a real one — was skipped, and each arm invented its codes
|
||||||
|
(``ENGRAVE_MARK``, ``RITB-HOURS``, ``Material_Cost_Concrete``). The run SAID so on stdout
|
||||||
|
(``cost_baseline_notice``), so this was never a silence; what visibility cannot do is stop a
|
||||||
|
machine-readable artefact reading ``validator_decision: validated`` over lines nothing anchored.
|
||||||
|
|
||||||
|
OPT-IN, never a default: a bundle written before the S4.0 amendment — every commons-owned
|
||||||
|
golden — is legitimately un-anchored, and making the requirement the default would refuse them
|
||||||
|
all. A caller who needs the guarantee asks for it by name and composes it with
|
||||||
|
``--derive-cost-baseline`` when the base carries a priced schedule instead of the file.
|
||||||
|
|
||||||
|
A ``ValueError``, the ``BundleIdMismatch``/``CostBaselineDerivationError`` precedent: an argv
|
||||||
|
that is wrong about what this base can offer belongs on the CLI's refusal tuple and hosting's
|
||||||
|
400 arm, never on the crash channel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
#: The one line a run prints about its own anchoring. Rendered ONLY when the run is un-anchored:
|
#: The one line a run prints about its own anchoring. Rendered ONLY when the run is un-anchored:
|
||||||
#: an anchored run has nothing to warn about, and ``mandate.announce``'s rule is that a line for
|
#: an anchored run has nothing to warn about, and ``mandate.announce``'s rule is that a line for
|
||||||
#: something the run does not have is OMITTED rather than rendered empty.
|
#: something the run does not have is OMITTED rather than rendered empty.
|
||||||
|
|
@ -923,6 +944,7 @@ async def run_project(
|
||||||
#: Bundle path only, and OPT-IN by construction: the default leaves every existing run on the
|
#: Bundle path only, and OPT-IN by construction: the default leaves every existing run on the
|
||||||
#: file loader, byte-identically.
|
#: file loader, byte-identically.
|
||||||
derive_cost_baseline: bool = False,
|
derive_cost_baseline: bool = False,
|
||||||
|
require_cost_baseline: bool = False,
|
||||||
dimension: Dimension | None = None,
|
dimension: Dimension | None = None,
|
||||||
store: VerdictStore | None = None,
|
store: VerdictStore | None = None,
|
||||||
verdict_dir: str | None = None,
|
verdict_dir: str | None = None,
|
||||||
|
|
@ -1122,6 +1144,19 @@ async def run_project(
|
||||||
prepass_declaration = None
|
prepass_declaration = None
|
||||||
debate_tools = [make_retrieval_tool(docs_dir, top_k=top_k)]
|
debate_tools = [make_retrieval_tool(docs_dir, top_k=top_k)]
|
||||||
|
|
||||||
|
# F4: the anchoring REQUIREMENT, opt-in and checked here — the one point at which both
|
||||||
|
# branches have bound ``baseline``, and above the dry-run cut below, so the FREE trip refuses
|
||||||
|
# too. Before the first model call by construction: at the exit code a refusal after the spend
|
||||||
|
# is indistinguishable from one before it (session 57's rule). The road path is anchored by
|
||||||
|
# construction, so on it this can only pass.
|
||||||
|
if require_cost_baseline and baseline is None:
|
||||||
|
raise UnanchoredRunRefused(
|
||||||
|
"this run was required to be anchored, but the knowledge base offers no cost "
|
||||||
|
"baseline: without one the validator's stage 0 is skipped and nothing ties a proposed "
|
||||||
|
"cost line to this project. Ship a cost-baseline.json, or pass "
|
||||||
|
"--derive-cost-baseline when the base carries a priced schedule"
|
||||||
|
)
|
||||||
|
|
||||||
# Trekk B2 (krav 3): configured MCP servers become tools the AGENTS can call during the debate.
|
# Trekk B2 (krav 3): configured MCP servers become tools the AGENTS can call during the debate.
|
||||||
# Appended to BOTH paths — on the bundle path they are the first tools that path has ever had.
|
# Appended to BOTH paths — on the bundle path they are the first tools that path has ever had.
|
||||||
# Constructed here but NOT connected: an ``MCPTool`` is an async context manager, so the run
|
# Constructed here but NOT connected: an ``MCPTool`` is an async context manager, so the run
|
||||||
|
|
@ -2554,6 +2589,16 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
"guesses: an unpriced or ambiguous schedule stops the run"
|
"guesses: an unpriced or ambiguous schedule stops the run"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--require-cost-baseline",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"REFUSE the run unless the validator's stage 0 has a cost baseline to reconcile "
|
||||||
|
"against (F4). Opt-in: a bundle that ships none is legitimately un-anchored and runs "
|
||||||
|
"unchanged without this flag. Combine with --derive-cost-baseline to satisfy it from "
|
||||||
|
"a priced schedule inside the base"
|
||||||
|
),
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--proposals-from-mandate",
|
"--proposals-from-mandate",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
|
|
@ -2646,6 +2691,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
# Report mode returns before the run dispatch, so an omission here is a SILENT DROP,
|
# Report mode returns before the run dispatch, so an omission here is a SILENT DROP,
|
||||||
# not a refusal — the gap F4 measured on --plan-review.
|
# not a refusal — the gap F4 measured on --plan-review.
|
||||||
"--derive-cost-baseline": args.derive_cost_baseline,
|
"--derive-cost-baseline": args.derive_cost_baseline,
|
||||||
|
# Same reason, same rung: report mode returns above every run dispatch, so leaving it
|
||||||
|
# out is a SILENT DROP of a guarantee the operator asked for by name.
|
||||||
|
"--require-cost-baseline": args.require_cost_baseline,
|
||||||
# Same reason, one flag later: report mode returns above the S7b dispatch too.
|
# Same reason, one flag later: report mode returns above the S7b dispatch too.
|
||||||
"--proposals-from-mandate": args.proposals_from_mandate,
|
"--proposals-from-mandate": args.proposals_from_mandate,
|
||||||
"PROJECT_ID": args.project_id is not None,
|
"PROJECT_ID": args.project_id is not None,
|
||||||
|
|
@ -2737,6 +2785,10 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
# who wrote --portfolio --derive-cost-baseline to add the one flag this mode also
|
# who wrote --portfolio --derive-cost-baseline to add the one flag this mode also
|
||||||
# refuses. Same reason --explore is listed here rather than left to fall through.
|
# refuses. Same reason --explore is listed here rather than left to fall through.
|
||||||
"--derive-cost-baseline": args.derive_cost_baseline,
|
"--derive-cost-baseline": args.derive_cost_baseline,
|
||||||
|
# It guards ONE base's anchoring, and the portfolio pass takes the road path, which is
|
||||||
|
# anchored by construction — so here the flag could only ever pass. BY NAME rather
|
||||||
|
# than falling through to the --bundle-dir requirement, its neighbours' reason.
|
||||||
|
"--require-cost-baseline": args.require_cost_baseline,
|
||||||
# It reads ONE base's schedule and settles ONE commission against it, so it sits on the
|
# It reads ONE base's schedule and settles ONE commission against it, so it sits on the
|
||||||
# same side of the partition as the flag it requires. BY NAME rather than falling
|
# same side of the partition as the flag it requires. BY NAME rather than falling
|
||||||
# through to "requires --derive-cost-baseline": an operator who wrote --portfolio
|
# through to "requires --derive-cost-baseline": an operator who wrote --portfolio
|
||||||
|
|
@ -2818,6 +2870,17 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
)
|
)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
# The road path's baseline comes from ``Project.cost_items`` and is anchored by construction,
|
||||||
|
# so without a bundle this flag could never fire — and a flag that cannot fire is a claim the
|
||||||
|
# surface makes about itself (the Fase-3 class). Refused BY NAME, its neighbour's reason.
|
||||||
|
if not args.portfolio and args.require_cost_baseline and args.bundle_dir is None:
|
||||||
|
print(
|
||||||
|
"run refused: --require-cost-baseline requires --bundle-dir (the road path is already "
|
||||||
|
"anchored by its own cost_items, so on it the requirement could never fire)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
# The SEEDING arm's three refusals, placed ABOVE the replacing arm's block on purpose: given
|
# The SEEDING arm's three refusals, placed ABOVE the replacing arm's block on purpose: given
|
||||||
# both flags, the block below would answer with "--prepass-payload and --explore cannot be
|
# both flags, the block below would answer with "--prepass-payload and --explore cannot be
|
||||||
# combined", which names neither of the two flags the operator actually put in conflict. At
|
# combined", which names neither of the two flags the operator actually put in conflict. At
|
||||||
|
|
@ -3711,6 +3774,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
prepass_payload=prepass_payload,
|
prepass_payload=prepass_payload,
|
||||||
verdict_input=_verdict_input_from_args(args),
|
verdict_input=_verdict_input_from_args(args),
|
||||||
derive_cost_baseline=args.derive_cost_baseline,
|
derive_cost_baseline=args.derive_cost_baseline,
|
||||||
|
require_cost_baseline=args.require_cost_baseline,
|
||||||
mcp_servers=mcp_servers,
|
mcp_servers=mcp_servers,
|
||||||
live_dry_run=True,
|
live_dry_run=True,
|
||||||
)
|
)
|
||||||
|
|
@ -3781,6 +3845,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
verdict_input=_verdict_input_from_args(args),
|
verdict_input=_verdict_input_from_args(args),
|
||||||
semantic_retrieval=args.semantic_retrieval,
|
semantic_retrieval=args.semantic_retrieval,
|
||||||
derive_cost_baseline=args.derive_cost_baseline,
|
derive_cost_baseline=args.derive_cost_baseline,
|
||||||
|
require_cost_baseline=args.require_cost_baseline,
|
||||||
client_factory=scripted_client_factory,
|
client_factory=scripted_client_factory,
|
||||||
mandate=mandate,
|
mandate=mandate,
|
||||||
mcp_servers=mcp_servers,
|
mcp_servers=mcp_servers,
|
||||||
|
|
|
||||||
|
|
@ -265,10 +265,14 @@ def test_a_nested_document_is_reachable_through_the_rung_below() -> None:
|
||||||
def test_an_unknown_directory_is_refused_by_name(tmp_path: Path) -> None:
|
def test_an_unknown_directory_is_refused_by_name(tmp_path: Path) -> None:
|
||||||
"""(g) Fail-closed, and it is the vacuity trap in its own right: an unknown path rendered as an
|
"""(g) Fail-closed, and it is the vacuity trap in its own right: an unknown path rendered as an
|
||||||
empty listing is indistinguishable from a directory that exists and holds nothing. Validation,
|
empty listing is indistinguishable from a directory that exists and holds nothing. Validation,
|
||||||
never invention (``write_concept_file``'s rule)."""
|
never invention (``write_concept_file``'s rule).
|
||||||
|
|
||||||
|
Pinned to ``BundlePathNotFound`` by NAME since F3 added its sibling: both refusals quote the
|
||||||
|
caller's path, so a match on the path alone could no longer tell "this is nothing" from "this
|
||||||
|
is a document" - the substring two branches share."""
|
||||||
base = _write_tree(tmp_path, "korpus", dirs=2, per_dir=2)
|
base = _write_tree(tmp_path, "korpus", dirs=2, per_dir=2)
|
||||||
|
|
||||||
with pytest.raises(ValueError, match="kategori-99"):
|
with pytest.raises(okf.BundlePathNotFound, match="kategori-99"):
|
||||||
_read_dir(base, "kategori-99")
|
_read_dir(base, "kategori-99")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
224
tests/test_read_dir_wrong_rung_loadbearing.py
Normal file
224
tests/test_read_dir_wrong_rung_loadbearing.py
Normal file
|
|
@ -0,0 +1,224 @@
|
||||||
|
"""F3 - ``read_dir`` on a path that names a DOCUMENT answers with the rung that reads it.
|
||||||
|
|
||||||
|
**The measurement, and the premise it fells.** The live S7 acid test
|
||||||
|
(``docs/2026-09-07-syretest-s7-prepass-k2.md`` § 4) recorded arm C calling
|
||||||
|
``read_dir('del-ii-bilag-6-teknisk-oppsett')`` and then
|
||||||
|
``read_dir('del-ii-bilag-6-teknisk-oppsett.md')``; both were refused, and the two rounds came out
|
||||||
|
of a bounded round cap. The order reads that as "the path named a document that EXISTS (with or
|
||||||
|
without ``.md``)" and asks whether the refusal can name ``read_file`` and the real path, the way
|
||||||
|
``explore.DirectoryPathRefused`` already does in the other direction.
|
||||||
|
|
||||||
|
**Measured against the base that ran it, the premise is false.** That base holds 27 directories
|
||||||
|
named ``del-ii-bilag-N-...`` and 12 root documents named ``inbox-del-ii-bilag-N-....md``. The path
|
||||||
|
the model asked for matches NEITHER: it is the directory-naming convention applied to a document
|
||||||
|
whose real name carries an ``inbox-`` prefix. So the live two rounds were the UNKNOWN-path class,
|
||||||
|
not the wrong-RUNG class, and nothing in this file would have saved them - arm (g) is that fact,
|
||||||
|
gated, so no later reader can mistake this delivery for a fix of the measured cost.
|
||||||
|
|
||||||
|
**What IS real is the asymmetry.** ``read_file`` on a directory raises ``DirectoryPathRefused`` and
|
||||||
|
names ``read_dir``; ``read_dir`` on a document raised the generic "no such directory" and named
|
||||||
|
neither the rung that reads it nor the path it would take. The symmetry the order asked about is
|
||||||
|
absent in exactly one direction, and that is what this file closes.
|
||||||
|
|
||||||
|
The lookup is built from ``context_files``, NEVER ``files`` (arm (e)): a suggestion built from the
|
||||||
|
walk would name a ``type: verdict`` document by path - advertising, in a refusal, the one layer no
|
||||||
|
listing mentions and ``read_file`` refuses outright. It honours the ``dimension`` scope for the
|
||||||
|
same reason one rung up (arm (f)): naming a document the run would then refuse to open is the
|
||||||
|
"filter in name only" S2c measured, inverted.
|
||||||
|
|
||||||
|
Arms: (a) exact name * (b) the same name without ``.md`` * (c) an unknown path keeps its own
|
||||||
|
wording and never names ``read_file`` * (d) anti-vacuity: the named path is usable VERBATIM *
|
||||||
|
(e) the verdict layer is never named * (f) a foreign-dimension document is never named * (g) the
|
||||||
|
LIVE case is not this class * (h) the refusal is a ``ValueError``, not the crash channel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from portfolio_optimiser import okf
|
||||||
|
from portfolio_optimiser.explore import navigator_tools
|
||||||
|
|
||||||
|
#: ASCII-clean throughout: a tool result is serialised with ``\uXXXX`` escapes, and a probe with
|
||||||
|
#: Norwegian characters was red against a working implementation in session 99.
|
||||||
|
_ALPHA = "notat-alpha"
|
||||||
|
_GAMMA = "inbox-notat-gamma"
|
||||||
|
|
||||||
|
|
||||||
|
def _tools(bundle_dir: Path, *, dimension: str | None = None) -> dict[str, Any]:
|
||||||
|
return {t.name: t for t in navigator_tools((str(bundle_dir),), dimension=dimension)}
|
||||||
|
|
||||||
|
|
||||||
|
def _read_dir(bundle_dir: Path, path: str, *, dimension: str | None = None) -> dict[str, Any]:
|
||||||
|
tools = _tools(bundle_dir, dimension=dimension)
|
||||||
|
return tools["read_dir"].func(bundle_id=bundle_dir.name, path=path)
|
||||||
|
|
||||||
|
|
||||||
|
def _read_file(bundle_dir: Path, path: str) -> str:
|
||||||
|
return _tools(bundle_dir)["read_file"].func(bundle_id=bundle_dir.name, path=path)
|
||||||
|
|
||||||
|
|
||||||
|
def _base(root: Path) -> Path:
|
||||||
|
"""A base carrying every class the refusal must tell apart: a plain concept document at the
|
||||||
|
top, a real directory, a ``type: verdict`` document, a foreign-dimension document, and a
|
||||||
|
document whose real name carries the ``inbox-`` prefix the live model dropped."""
|
||||||
|
base = root / "korpus"
|
||||||
|
(base / "arkiv").mkdir(parents=True)
|
||||||
|
(base / f"{_ALPHA}.md").write_text(
|
||||||
|
"---\ntype: concept\ntitle: Notat alpha\n---\n\nalpha body.\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
(base / f"{_GAMMA}.md").write_text(
|
||||||
|
"---\ntype: concept\ntitle: Notat gamma\n---\n\ngamma body.\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
(base / "dom-beta.md").write_text(
|
||||||
|
"---\ntype: verdict\ntitle: Dom beta\n---\n\nverdict body.\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
(base / "energi-notat.md").write_text(
|
||||||
|
"---\ntype: concept\ntitle: Energi\ndimension: energi\n---\n\nenergi body.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(base / "arkiv" / "dok-a.md").write_text(
|
||||||
|
"---\ntype: concept\ntitle: Dokument A\n---\n\narkiv body.\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
(base / "arkiv" / "index.md").write_text(
|
||||||
|
"---\ntype: index\n---\n\n- [Dokument A](dok-a.md)\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
(base / "index.md").write_text(
|
||||||
|
"---\ntype: index\n---\n\n"
|
||||||
|
f"- [Notat alpha]({_ALPHA}.md)\n"
|
||||||
|
f"- [Notat gamma]({_GAMMA}.md)\n"
|
||||||
|
"- [Dom beta](dom-beta.md)\n"
|
||||||
|
"- [Energi](energi-notat.md)\n"
|
||||||
|
"- [arkiv](arkiv/index.md)\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
# --- (a)/(b) the wrong rung, answered as such -----------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_dir_on_a_documents_exact_name_names_read_file_and_the_path(tmp_path: Path) -> None:
|
||||||
|
"""(a) The symmetry ``explore.DirectoryPathRefused`` already has in the other direction. A
|
||||||
|
refusal that only says "no" leaves the caller with the same next move it just made."""
|
||||||
|
base = _base(tmp_path)
|
||||||
|
|
||||||
|
with pytest.raises(okf.DocumentPathRefused) as excinfo:
|
||||||
|
_read_dir(base, f"{_ALPHA}.md")
|
||||||
|
|
||||||
|
message = str(excinfo.value)
|
||||||
|
assert "read_file" in message, "the refusal does not name the rung that reads a document"
|
||||||
|
assert f"{_ALPHA}.md" in message, "the refusal does not name the path read_file would take"
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_suffix_is_not_what_makes_it_a_document(tmp_path: Path) -> None:
|
||||||
|
"""(b) The live model asked BOTH ways in two consecutive rounds. A refusal that only knew the
|
||||||
|
``.md`` form would answer one of them and not the other."""
|
||||||
|
base = _base(tmp_path)
|
||||||
|
|
||||||
|
with pytest.raises(okf.DocumentPathRefused) as excinfo:
|
||||||
|
_read_dir(base, _ALPHA)
|
||||||
|
|
||||||
|
message = str(excinfo.value)
|
||||||
|
assert "read_file" in message
|
||||||
|
assert f"{_ALPHA}.md" in message, (
|
||||||
|
"the refusal echoed the caller's path instead of the document's real name; a path that "
|
||||||
|
"never existed is worse than no path"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- (c) the two branches must not share their wording --------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_an_unknown_path_keeps_its_own_wording(tmp_path: Path) -> None:
|
||||||
|
"""(c) Two refusals that share a substring cannot be told apart by a test OR by a model. The
|
||||||
|
unknown-path branch is unchanged and must stay unable to claim a document exists."""
|
||||||
|
base = _base(tmp_path)
|
||||||
|
|
||||||
|
with pytest.raises(okf.BundlePathNotFound) as excinfo:
|
||||||
|
_read_dir(base, "kategori-99")
|
||||||
|
|
||||||
|
message = str(excinfo.value)
|
||||||
|
assert "read_file" not in message, (
|
||||||
|
"the unknown-path refusal names read_file, so the two branches say the same thing about "
|
||||||
|
"two different facts"
|
||||||
|
)
|
||||||
|
assert not isinstance(excinfo.value, okf.DocumentPathRefused)
|
||||||
|
|
||||||
|
|
||||||
|
# --- (d) anti-vacuity: the named path must WORK ---------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_named_path_is_usable_verbatim(tmp_path: Path) -> None:
|
||||||
|
"""(d) The ``_index_excerpt`` rule, one rung down: the caller is a model, so a suggested path
|
||||||
|
that does not resolve is worse than none. Proven by feeding it back."""
|
||||||
|
base = _base(tmp_path)
|
||||||
|
|
||||||
|
with pytest.raises(okf.DocumentPathRefused) as excinfo:
|
||||||
|
_read_dir(base, _ALPHA)
|
||||||
|
named = str(excinfo.value).split("'")[-2]
|
||||||
|
|
||||||
|
assert "alpha body." in _read_file(base, named)
|
||||||
|
|
||||||
|
|
||||||
|
# --- (e)/(f) the two gates the suggestion must not walk around ------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_verdict_layer_is_never_named(tmp_path: Path) -> None:
|
||||||
|
"""(e) Built from ``context_files``, never ``files``. A suggestion built from the walk would
|
||||||
|
hand a navigator the path of a prior verdict - the one layer no listing mentions and
|
||||||
|
``read_file`` refuses outright (order 20260904T172353Z)."""
|
||||||
|
base = _base(tmp_path)
|
||||||
|
|
||||||
|
with pytest.raises(okf.BundlePathNotFound) as excinfo:
|
||||||
|
_read_dir(base, "dom-beta.md")
|
||||||
|
|
||||||
|
assert "read_file" not in str(excinfo.value)
|
||||||
|
assert not isinstance(excinfo.value, okf.DocumentPathRefused)
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_foreign_dimension_document_is_never_named(tmp_path: Path) -> None:
|
||||||
|
"""(f) §4.1a, inverted: naming a document the run would then refuse to open is the "filter in
|
||||||
|
name only" S2c measured. ONE predicate (``in_dimension``) serves the listing and this."""
|
||||||
|
base = _base(tmp_path)
|
||||||
|
|
||||||
|
with pytest.raises(okf.BundlePathNotFound) as excinfo:
|
||||||
|
_read_dir(base, "energi-notat.md", dimension="tunnel")
|
||||||
|
|
||||||
|
assert "read_file" not in str(excinfo.value)
|
||||||
|
# The control: without a scope the SAME path is the wrong-rung class, so the arm above is the
|
||||||
|
# dimension deciding rather than the document being invisible.
|
||||||
|
with pytest.raises(okf.DocumentPathRefused):
|
||||||
|
_read_dir(base, "energi-notat.md")
|
||||||
|
|
||||||
|
|
||||||
|
# --- (g) the LIVE case is not this class ----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_measured_live_path_is_still_the_unknown_class(tmp_path: Path) -> None:
|
||||||
|
"""(g) The felled premise, gated. The live base holds ``inbox-<name>.md`` documents beside
|
||||||
|
``<name>``-shaped directories, and the model asked for the un-prefixed form. That names no
|
||||||
|
document, so it is refused as unknown - and this delivery does NOT recover the two rounds the
|
||||||
|
order measured. Resolving it would mean guessing which document a caller meant, which is
|
||||||
|
invention rather than validation (``write_concept_file``'s rule)."""
|
||||||
|
base = _base(tmp_path)
|
||||||
|
assert (base / f"{_GAMMA}.md").exists(), "the fixture must hold the prefixed document"
|
||||||
|
|
||||||
|
with pytest.raises(okf.BundlePathNotFound) as excinfo:
|
||||||
|
_read_dir(base, _GAMMA.removeprefix("inbox-"))
|
||||||
|
|
||||||
|
assert "read_file" not in str(excinfo.value)
|
||||||
|
assert not isinstance(excinfo.value, okf.DocumentPathRefused)
|
||||||
|
|
||||||
|
|
||||||
|
# --- (h) the channel ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_refusal_lands_on_the_refusal_tuple(tmp_path: Path) -> None:
|
||||||
|
"""(h) The ``BundlePathNotFound``/``DimensionScopeRefused`` precedent: the caller is a model
|
||||||
|
choosing a path, so this belongs on the CLI's refusal tuple and hosting's 400 arm rather than
|
||||||
|
the crash channel."""
|
||||||
|
assert issubclass(okf.DocumentPathRefused, ValueError)
|
||||||
240
tests/test_require_cost_baseline_loadbearing.py
Normal file
240
tests/test_require_cost_baseline_loadbearing.py
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
"""F4 - a run that must be anchored can say so, and is refused before it costs anything.
|
||||||
|
|
||||||
|
**The measurement** (``docs/2026-09-07-syretest-s7-prepass-k2.md`` § 8, re-measured here against
|
||||||
|
the four artefacts the live run left). All three paid arms stamped
|
||||||
|
``cost_baseline_anchored: False``, so the validator's stage 0 - the ONE stage that tells a
|
||||||
|
fabricated cost line from a real one - was skipped, and every arm invented its codes:
|
||||||
|
``ENGRAVE_MARK``, ``RITB-HOURS``/``SYSINT-HOURS``, ``Material_Cost_Concrete``. Stages 2/4/5 judged
|
||||||
|
those numbers against each other and did their job; nothing tied a single line to K2.
|
||||||
|
|
||||||
|
**Option (c) is felled by measurement, not by preference.** The order offers "make
|
||||||
|
``--derive-cost-baseline`` reachable where it is not today". Measured against the base that ran:
|
||||||
|
``derive_cost_baseline`` refuses with *no concept file carries a markdown table whose header names
|
||||||
|
all three of code/quantity/unit_cost* - K2's delivered price schedule is a pandoc SIMPLE table with
|
||||||
|
ONE column header (``Prisskjema``) and every value collapsed into it. Reaching it would mean
|
||||||
|
inventing a rule for a form nobody has measured, which is precisely the honesty limit MAJOR-4
|
||||||
|
wrote down for itself.
|
||||||
|
|
||||||
|
**Option (b), and why not (a).** The visibility exists (``cost_baseline_notice``) and is honest;
|
||||||
|
what it cannot do is stop a machine-readable artefact that says ``validator_decision: validated``
|
||||||
|
over lines nothing anchored. So: an OPT-IN flag, never a default. Every bundle without a
|
||||||
|
``cost-baseline.json`` - every commons-owned golden - runs exactly as before, which is what (a)
|
||||||
|
was protecting; a caller who needs the guarantee asks for it by name.
|
||||||
|
|
||||||
|
The gate sits at the ONE place both branches have bound ``baseline`` and BEFORE the dry-run cut, so
|
||||||
|
it fires on the free trip too (arm (d)) and, on the paid one, before the first model call (arm (a)
|
||||||
|
asserts NULL calls, never merely the exception - at the exit code a refusal after the spend looks
|
||||||
|
identical to one before).
|
||||||
|
|
||||||
|
Arms: (a) refused, zero model calls * (b) control: without the flag the same base runs *
|
||||||
|
(c) an anchored base is untouched * (d) the dry run refuses too, with its own control *
|
||||||
|
(e) it composes with ``--derive-cost-baseline`` * (f)(g)(h) three CLI refusals BY NAME, each with
|
||||||
|
an rc-0 control on an argv that would otherwise be accepted * (i) the CLI wiring, measured on
|
||||||
|
calls * (j) the channel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from portfolio_optimiser import run
|
||||||
|
from portfolio_optimiser.simulation import ScriptedChatClient
|
||||||
|
|
||||||
|
_FIXTURES = Path(__file__).parent / "fixtures"
|
||||||
|
_PRICED = str(_FIXTURES / "k2-prisskjema-SYNTETISK")
|
||||||
|
_EXAMPLES = Path(__file__).resolve().parents[1] / "shared" / "examples"
|
||||||
|
#: A base that SHIPS a hand-written ``cost-baseline.json`` - the anchored control.
|
||||||
|
_ANCHORED_SOURCE = _EXAMPLES / "tunnel-hauglia"
|
||||||
|
|
||||||
|
_IR_PROJECTION = {
|
||||||
|
"project_id": "K2",
|
||||||
|
"measure": "PLACEHOLDER - authored by this test to satisfy the bundle contract",
|
||||||
|
"affected_items": [{"code": "21.1", "quantity": 1250, "unit_cost": 850.0}],
|
||||||
|
"claimed_saving_nok": 1000.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _runnable(source: str, tmp_path: Path, *, name: str = "runnable") -> str:
|
||||||
|
root = tmp_path / name
|
||||||
|
shutil.copytree(source, root)
|
||||||
|
(root / "validator-input.json").write_text(json.dumps(_IR_PROJECTION), encoding="utf-8")
|
||||||
|
return str(root)
|
||||||
|
|
||||||
|
|
||||||
|
def _scripted(sink: list[str] | None = None) -> Any:
|
||||||
|
return lambda role: ScriptedChatClient(sink=sink, role=role, default_reply="ok")
|
||||||
|
|
||||||
|
|
||||||
|
# --- (a)/(b)/(c) the library seam -----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_an_unanchored_run_is_refused_before_it_costs_anything(tmp_path: Path) -> None:
|
||||||
|
"""(a) The refusal, and the assert that makes it worth having: NULL model calls. Measured on
|
||||||
|
calls rather than on the exception, because a refusal placed after the spend raises the same
|
||||||
|
exception (session 57's rule, session 82's arm)."""
|
||||||
|
bundle_dir = _runnable(_PRICED, tmp_path)
|
||||||
|
sink: list[str] = []
|
||||||
|
|
||||||
|
with pytest.raises(run.UnanchoredRunRefused):
|
||||||
|
await run.run_project(
|
||||||
|
"K2",
|
||||||
|
"local",
|
||||||
|
docs_dir=bundle_dir,
|
||||||
|
bundle_dir=bundle_dir,
|
||||||
|
require_cost_baseline=True,
|
||||||
|
client_factory=_scripted(sink),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert sink == [], f"the run reached the model before it was refused ({len(sink)} calls)"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_control_without_the_flag_the_same_base_runs(tmp_path: Path) -> None:
|
||||||
|
"""(b) The default is byte-for-byte the path every existing caller takes - which is what
|
||||||
|
option (a) of the order was protecting, kept rather than argued away."""
|
||||||
|
bundle_dir = _runnable(_PRICED, tmp_path)
|
||||||
|
|
||||||
|
report = await run.run_project(
|
||||||
|
"K2", "local", docs_dir=bundle_dir, bundle_dir=bundle_dir, live_dry_run=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(report, run.DryRunReport)
|
||||||
|
assert report.cost_baseline_anchored is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_an_anchored_base_is_untouched_by_the_flag(tmp_path: Path) -> None:
|
||||||
|
"""(c) The gate reads the SAME ``baseline`` the validator is handed, so a base that ships the
|
||||||
|
file passes it. Without this arm the flag could be a refusal that always fires."""
|
||||||
|
bundle_dir = _runnable(str(_ANCHORED_SOURCE), tmp_path, name="anchored")
|
||||||
|
|
||||||
|
report = await run.run_project(
|
||||||
|
"K2",
|
||||||
|
"local",
|
||||||
|
docs_dir=bundle_dir,
|
||||||
|
bundle_dir=bundle_dir,
|
||||||
|
require_cost_baseline=True,
|
||||||
|
live_dry_run=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(report, run.DryRunReport)
|
||||||
|
assert report.cost_baseline_anchored is True
|
||||||
|
|
||||||
|
|
||||||
|
# --- (d) the free trip refuses too ----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_the_dry_run_refuses_rather_than_reporting_an_unanchored_run(tmp_path: Path) -> None:
|
||||||
|
"""(d) The gate is ABOVE the dry-run cut on purpose: the dry run's whole job is to say what a
|
||||||
|
real run would do, and it is the cheapest place to learn that this one may not run at all."""
|
||||||
|
bundle_dir = _runnable(_PRICED, tmp_path)
|
||||||
|
|
||||||
|
with pytest.raises(run.UnanchoredRunRefused):
|
||||||
|
await run.run_project(
|
||||||
|
"K2",
|
||||||
|
"local",
|
||||||
|
docs_dir=bundle_dir,
|
||||||
|
bundle_dir=bundle_dir,
|
||||||
|
require_cost_baseline=True,
|
||||||
|
live_dry_run=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- (e) it composes with the derivation ----------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
async def test_it_composes_with_the_derivation(tmp_path: Path) -> None:
|
||||||
|
"""(e) ``--derive-cost-baseline`` is the OTHER half of the answer: derive, and the requirement
|
||||||
|
is met. Two flags that could not be combined would leave the guarantee unreachable on exactly
|
||||||
|
the bases MAJOR-4 was built for."""
|
||||||
|
bundle_dir = _runnable(_PRICED, tmp_path)
|
||||||
|
|
||||||
|
report = await run.run_project(
|
||||||
|
"K2",
|
||||||
|
"local",
|
||||||
|
docs_dir=bundle_dir,
|
||||||
|
bundle_dir=bundle_dir,
|
||||||
|
derive_cost_baseline=True,
|
||||||
|
require_cost_baseline=True,
|
||||||
|
live_dry_run=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(report, run.DryRunReport)
|
||||||
|
assert report.cost_baseline_anchored is True
|
||||||
|
|
||||||
|
|
||||||
|
# --- (f)/(g)/(h) three CLI refusals, each with an rc-0 control -------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_requires_bundle_dir(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
|
"""(f) The road path is anchored by construction, so on it the flag could never fire. A flag
|
||||||
|
that cannot fire is a claim the surface makes about itself (the Fase-3 class)."""
|
||||||
|
rc = run.main(["P1", "--docs-dir", "docs", "--require-cost-baseline"])
|
||||||
|
|
||||||
|
assert rc == 1
|
||||||
|
assert "--require-cost-baseline" in capsys.readouterr().err
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_is_refused_in_portfolio_mode(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
|
"""(g) BY NAME, never by falling through to the ``--bundle-dir`` requirement: that message
|
||||||
|
would tell an operator who wrote ``--portfolio --require-cost-baseline`` to add the one flag
|
||||||
|
this mode also refuses (the ``--derive-cost-baseline`` precedent, and session 57's)."""
|
||||||
|
rc = run.main(["--portfolio", "--require-cost-baseline"])
|
||||||
|
|
||||||
|
assert rc == 1
|
||||||
|
err = capsys.readouterr().err
|
||||||
|
assert "--portfolio" in err, "the refusal never names the mode that refused it"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_is_refused_in_report_mode(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
|
"""(h) Report mode returns ABOVE every run dispatch, so an omission from this allowlist is a
|
||||||
|
SILENT DROP rather than a refusal - the gap F4 itself measured on ``--plan-review``."""
|
||||||
|
ledger = tmp_path / "ledger.json"
|
||||||
|
ledger.write_text("[]", encoding="utf-8")
|
||||||
|
|
||||||
|
assert run.main(["--report", "--ledger", str(ledger)]) == 0, "the control argv must be ACCEPTED"
|
||||||
|
capsys.readouterr()
|
||||||
|
|
||||||
|
rc = run.main(["--report", "--ledger", str(ledger), "--require-cost-baseline"])
|
||||||
|
|
||||||
|
assert rc == 1
|
||||||
|
# The allowlist's own wording, not the flag name: this partition refuses generically, and the
|
||||||
|
# discriminator is the rc-0 control above -- the same argv without the flag is ACCEPTED.
|
||||||
|
assert "mode-exclusive" in capsys.readouterr().err
|
||||||
|
|
||||||
|
|
||||||
|
# --- (i) the CLI wiring ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_wiring_refuses_the_unanchored_run_without_paying(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||||
|
) -> None:
|
||||||
|
"""(i) The flag must REACH ``run_project``. rc 1 alone would also be a parse error, so the
|
||||||
|
discriminator is that no model call happened - and the control proves the same argv without
|
||||||
|
the flag is accepted and DOES call."""
|
||||||
|
monkeypatch.delenv("PORTFOLIO_MODEL_MAP", raising=False)
|
||||||
|
bundle_dir = _runnable(_PRICED, tmp_path)
|
||||||
|
sink: list[str] = []
|
||||||
|
monkeypatch.setattr("portfolio_optimiser.run._default_factory", lambda profile: _scripted(sink))
|
||||||
|
|
||||||
|
argv = ["K2", "--docs-dir", bundle_dir, "--bundle-dir", bundle_dir]
|
||||||
|
assert run.main([*argv, "--live-dry-run"]) == 0, "the control argv must be ACCEPTED"
|
||||||
|
|
||||||
|
rc = run.main([*argv, "--require-cost-baseline"])
|
||||||
|
|
||||||
|
assert rc == 1
|
||||||
|
assert sink == [], f"the run was refused only after paying for it ({len(sink)} calls)"
|
||||||
|
assert "run refused" in capsys.readouterr().err
|
||||||
|
|
||||||
|
|
||||||
|
# --- (j) the channel ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_refusal_lands_on_the_refusal_tuple() -> None:
|
||||||
|
"""(j) A ``ValueError``, the ``BundleIdMismatch``/``CostBaselineDerivationError`` precedent: an
|
||||||
|
argv that is wrong about what this base can offer belongs on the CLI's refusal tuple, never on
|
||||||
|
the crash channel."""
|
||||||
|
assert issubclass(run.UnanchoredRunRefused, ValueError)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue