feat(p18): a listing is a WINDOW, and an invented path is refused by name
P18 part A (order 20260914T105139Z). P16 measured S7a-3's ladder against a delivered corpus for the first time and found two things fixture bases cannot show. (1) One level is not bounded by being one level. Measured 14.09 on the four mounted vegnormal bases: okf.directory_listing on krav/N200 is 169 974 chars over 1 132 documents, krav/N100 69 250 over 445, krav/N500 39 853 over 269, and R761's own root 110 912 over 2 728 SUBDIRECTORIES -- 27-113x the 1 500-char ceiling S7a-3 set, riding in every later prompt. That last number is why the window covers BOTH kinds: a pagination over documents only would have left the largest measured level unpaginated. read_dir now answers with a window. offset/limit page directories first then documents as ONE sequence (two independent windows make "the next ten" a question with two answers); total is the denominator and is always carried; limit is CLAMPED to 50, never refused. Default 10 chosen against the ceiling: one entry is 121-209 chars (median 145) over the four bases. After: n100 1 493, n500 1 453, R761 479, n200 1 537 -- 2.5 % over, stated rather than tuned away, because the ceiling is a character budget and the window is a count. Largest single call any caller can make: ~7 600 chars. filter narrows a level instead of paging it: case-insensitive SUBSTRING over title + req_number/prosessnr and over a directory path, answering with total_matches beside total. A substring and not a pattern for _ground_against_input's reason one rung down -- a form the rule does not know returns nothing, and an empty listing reads as "the base does not have this". A filter that matches nothing is an ANSWER (total_matches: 0), never a refusal. ORDER PREMISE FELLED before building on it: the order asks for a separate top-level reader "like own_frontmatter" because parse_frontmatter was last-write-wins -- P15 (f13dc64) already made a top-level key win, so BundleFile.frontmatter IS the concept's own value and a second reader here would be the second copy ko-(p) forbids. (2) 0 of 26 fasit concepts were opened in 32 read_file calls (the order's "24" is the four runs' DISTINCT paths, re-measured 14.09), and 10 of those calls named a path the base does not hold. Each reached the model as MAF's opaque "Error: Function failed." while counting toward the three consecutive tool errors that end a request. read_file now refuses such a path by name (BundlePathNotFound, funn-99 returned form) and names the nearest directory that actually HOLDS documents -- chosen off context_files, never the filesystem, because a directory can exist on disk and hold no navigated concept (read_dir would then refuse the very path the refusal handed back) and because context_files is what drops the type: verdict layer, so a refusal can never advertise by name the one layer no listing mentions. Narrow by construction: only an ABSENT path is translated; any other OSError propagates untouched. Two existing arms REWRITTEN, neither weakened: - test_a_nonexistent_sibling_is_still_an_os_error was a tripwire whose own docstring said "when it goes red, someone has closed it, and that is a decision to be recorded". This is the record. Its narrowness half survives as a new arm driving a real PermissionError on a file that IS there. - test_every_document_is_still_reachable_and_the_counts_add_up became STRONGER: the accounting must now page, so the same assertion also proves the window is complete and non-overlapping. tests/test_navigation_window_loadbearing.py: 13 arms. Arms needing the delivered bases SKIP with the root named (PORTFOLIO_VEGNORMAL_ROOT), as MAJOR-3's ceiling arm does; the window algebra, the filter negative and the refusal run over a synthetic base UNCONDITIONALLY, so the file can never be silently absent in full. Verification: uv run pytest -q 1672 passed / 5 skipped before the new file (1670 oncfd9079). ruff check + format clean, mypy clean (38 files). Golden demo-transcript.stdout BYTE-UNCHANGED, shasum -a 1 of the CONTENT = ea8c534773acdbe41ae68f2c55724d69aaf8be4f. No version bump, no push. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
cfd9079a4a
commit
9b47e5aa62
7 changed files with 634 additions and 42 deletions
49
CLAUDE.md
49
CLAUDE.md
|
|
@ -2472,6 +2472,55 @@ Python ≥3.10. MAF (`agent-framework-core` 1.16.0, `-orchestrations` 1.1.1 —
|
||||||
gratis-turen etter at flaggene landet: samme stdout sa «Stops at: 3 rounds / 100000 tokens» TO
|
gratis-turen etter at flaggene landet: samme stdout sa «Stops at: 3 rounds / 100000 tokens» TO
|
||||||
linjer over «max_rounds=8, max_tokens=120000» — Fase-3-klassen, innfort av nettopp det flagget
|
linjer over «max_rounds=8, max_tokens=120000» — Fase-3-klassen, innfort av nettopp det flagget
|
||||||
som annonseres. Rettet til argv; M16 (les konstantene igjen) → 1 rod, den armen ALENE.
|
som annonseres. Rettet til argv; M16 (les konstantene igjen) → 1 rod, den armen ALENE.
|
||||||
|
- **En listing er et VINDU, og en sti kalleren fant på er en NEKT (P18/A, 14.09):** S7a-3 bandt
|
||||||
|
kostnaden til oppføringer på ETT nivå i stedet for dokumenter i basen, og på fixturbasene holdt
|
||||||
|
det. P16 målte hva ett nivå koster på et LEVERT korpus: `krav/N200` **169 974 tegn** over 1 132
|
||||||
|
dokumenter, `krav/N100` 69 250 over 445, og R761s egen rot **110 912 over 2 728 UNDERKATALOGER** —
|
||||||
|
27–113× taket, ridende i hver senere prompt; 3 av 7 betalte kjøringer døde på tokentaket.
|
||||||
|
**Vinduet dekker BEGGE slag, og det er en måling, ikke symmetri:** en paginering bare over
|
||||||
|
dokumenter ville latt det STØRSTE målte nivået stå upaginert. `offset`/`limit` over kataloger
|
||||||
|
FØRST, så dokumenter — ÉN sekvens, fordi to uavhengige vinduer gjør «neste ti» til et spørsmål
|
||||||
|
med to svar. `limit` KLEMMES (`_DIRECTORY_PAGE_MAX = 50`), aldri nektes: kalleren ba om en
|
||||||
|
listing, og å nekte den sender en modell som ba om for mye bort med ingenting. Default 10 er valgt
|
||||||
|
MOT taket: én oppføring er 121–209 tegn (median 145) over de fire leverte basene, så ti av de
|
||||||
|
STØRSTE er 2 090 tegn med oppføringer — n100 lander på 1 493 (ordrens bundne krav), n500 1 453,
|
||||||
|
R761 479, og n200 på **1 537**, 2,5 % over, fordi taket er et TEGN-budsjett og vinduet er et
|
||||||
|
ANTALL; uttalt, ikke justert bort. **`total` er NEVNEREN og bæres alltid** (ansikt 4: et vindu
|
||||||
|
uten nevner er en måling uten nevner), mens `total_matches` er et ANNET faktum, ikke en andre kopi
|
||||||
|
— «hva er her» og «hvor mye av det slapp filteret gjennom» er to spørsmål, og en navigatør som
|
||||||
|
bare ser ett av dem kan ikke skille et for smalt filter fra et nesten tomt nivå. **`filter` er en
|
||||||
|
DELSTRENG, ikke et mønster**, av `_ground_against_input`s grunn ett hakk ned: kalleren er en
|
||||||
|
modell som velger ord, og en form regelen ikke kjenner ville returnert ingenting — en tom listing
|
||||||
|
som leses som «basen har ikke dette». Delstreng feiler mot å vise MER, som kan snevres inn;
|
||||||
|
et mønster feiler mot å vise ingenting, som ikke kan det. Matchet går over `title` +
|
||||||
|
`req_number`/`prosessnr` (toppnivå, lest rett av `BundleFile.frontmatter` — **P15 (f13dc64) gjorde
|
||||||
|
allerede toppnivå til vinner, så ordrens premiss om en egen `own_frontmatter`-leser er FELT: en
|
||||||
|
andre leser her ville vært kø-(p)**) og over en katalogs sti; `description` er BEVISST utelatt
|
||||||
|
(hvert konsept i alle fire baser har en, så prosa-match ville returnert mesteparten av et nivå for
|
||||||
|
de fleste ord — et filter som ser ut til å virke uten å binde noe). **Et filter uten treff er et
|
||||||
|
SVAR** (`total_matches: 0`), aldri en nekt: å nekte det gjør et ærlig negativt funn uskillbart fra
|
||||||
|
en sti som ikke finnes — nøyaktig forvirringen `BundlePathNotFound` finnes for.
|
||||||
|
**A3: en fraværende sti oversettes til `BundlePathNotFound` i funn-99-formen**, og den navngir
|
||||||
|
den NÆRMESTE katalogen som faktisk HOLDER dokumenter — valgt av `context_files`, aldri av
|
||||||
|
filsystemet, fordi en katalog kan finnes på disk og ikke holde noe navigert konsept (da nekter
|
||||||
|
`read_dir` den, og nekten ville levert en sti som ikke resolverer — `_index_excerpt`-regelen), og
|
||||||
|
fordi `context_files` er property-en som dropper `type: verdict`-laget, så en nekt aldri kan
|
||||||
|
reklamere ved navn for det ene laget ingen listing nevner. **Innsnevringen er BÆRENDE:** kun en
|
||||||
|
sti som er BORTE oversettes; enhver annen `OSError` propagerer urørt, fordi en nekt er et utsagn
|
||||||
|
om KALLERENS sti og en fil som ER der og ikke kan leses er et annet faktum. Målt over P16s fire
|
||||||
|
betalte kjøringer: **10 av 32** `read_file`-kall navnga en sti basen ikke holder (ordrens «24
|
||||||
|
kall» er de fire kjøringenes DISTINKTE stier, ikke kallene — re-målt 14.09), og hvert av dem nådde
|
||||||
|
modellen som MAFs ugjennomsiktige «Error: Function failed.» mens det telte mot de tre påfølgende
|
||||||
|
verktøyfeilene som avslutter en forespørsel. Etter: 10 navngitte nekter, 22 dokumenter fortsatt
|
||||||
|
servert — en gate som nektet alt ville bestått den første halvdelen alene. **TO EKSISTERENDE
|
||||||
|
ARMER ER SKREVET OM, IKKE SVEKKET:** `test_a_nonexistent_sibling_is_still_an_os_error` var en
|
||||||
|
TRIPWIRE hvis egen docstring sa «when it goes red, someone has closed it, and that is a decision
|
||||||
|
to be recorded» — dette er protokollen; og `test_every_document_is_still_reachable_and_the_counts
|
||||||
|
_add_up` ble STERKERE, ikke svakere: regnskapet må nå PAGINERE, så samme assert beviser i tillegg
|
||||||
|
at vinduet er fullstendig og ikke-overlappende. **Ærlighets-grenser, uttalt:** `navigate_bundle`
|
||||||
|
kalles fortsatt per verktøykall (I/O, ikke tokens); A3-nekten navigerer basen ÉN gang ekstra på
|
||||||
|
feilstien for å finne den nærmeste listbare katalogen; at en LEVENDE modell BRUKER `filter` er
|
||||||
|
ikke bevist (structured-output-grensens klasse) — DEL D er målingen.
|
||||||
- **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.
|
||||||
|
|
||||||
|
|
|
||||||
18
README.md
18
README.md
|
|
@ -700,6 +700,24 @@ when the seam is detached, so the loop cannot silently degrade into theater.
|
||||||
112 116 → 362 tokens for three bases, 124 942 → 21 448 for 171
|
112 116 → 362 tokens for three bases, 124 942 → 21 448 for 171
|
||||||
([report](docs/2026-08-26-katalogkostnaden.md)).
|
([report](docs/2026-08-26-katalogkostnaden.md)).
|
||||||
|
|
||||||
|
**Opening one level is bounded the same way (P18).** `read_dir` answers with a *window*: `total`
|
||||||
|
says how many entries — subdirectories plus concept documents — the level holds, `offset`/`limit`
|
||||||
|
say which of them you were given, and a `limit` past the maximum is clamped rather than refused.
|
||||||
|
`filter` narrows a level instead of paging it: a case-insensitive substring over a document's
|
||||||
|
title and reference number (`req_number` / `prosessnr`) and over a subdirectory's path, answering
|
||||||
|
with `total_matches` beside `total`. A filter that matches nothing is an answer, never a refusal.
|
||||||
|
Measured 2026-09-14 over four delivered bases, default listing of the largest level in each:
|
||||||
|
`krav/N200` 169 974 → 1 537 characters, `krav/N100` 69 250 → 1 493, R761's root 110 912 → 479
|
||||||
|
(its cost was 2 728 *subdirectories*, which is why the window covers both kinds), `krav/N500`
|
||||||
|
39 853 → 1 453. The largest single call any caller can now make is ~7 600 characters.
|
||||||
|
|
||||||
|
A path the caller **invented** is refused by name rather than failing opaquely: `read_file` on a
|
||||||
|
path the base does not hold answers `REFUSED (BundlePathNotFound)` and names the nearest directory
|
||||||
|
that actually holds documents, so the next call has somewhere to go. Measured over the four paid
|
||||||
|
runs of 2026-09-14: 10 of 32 `read_file` calls named such a path, and each previously reached the
|
||||||
|
model as the framework's opaque `Error: Function failed.` while counting toward the three
|
||||||
|
consecutive tool errors that end a request.
|
||||||
|
|
||||||
`--semantic-retrieval` (S3.1) is an **opt-in** ranking change, **off by default**. Off, prior
|
`--semantic-retrieval` (S3.1) is an **opt-in** ranking change, **off by default**. Off, prior
|
||||||
verdicts are ranked exactly as before: a structural score over the affected cost-code set,
|
verdicts are ranked exactly as before: a structural score over the affected cost-code set,
|
||||||
measure type and magnitude bucket, with surface text deliberately excluded. On, that score is
|
measure type and magnitude bucket, with surface text deliberately excluded. On, that score is
|
||||||
|
|
|
||||||
|
|
@ -197,8 +197,11 @@ _INSTRUCTIONS: Final = {
|
||||||
"what exists, read_bundle to open ONE of them and see its top level, read_dir to open a "
|
"what exists, read_bundle to open ONE of them and see its top level, read_dir to open a "
|
||||||
"directory that listing named, and read_file to read a document you picked. read_bundle "
|
"directory that listing named, and read_file to read a document you picked. read_bundle "
|
||||||
"and read_dir return LISTINGS, never the documents — a knowledge base can hold hundreds, "
|
"and read_dir return LISTINGS, never the documents — a knowledge base can hold hundreds, "
|
||||||
"so descend to the part that matters instead of asking for all of it. Quote only what "
|
"so descend to the part that matters instead of asking for all of it. A listing is a "
|
||||||
"read_file gave you, and never guess at content you have not read."
|
"WINDOW: it reports 'total' for the level and gives you 'limit' of them from 'offset', so "
|
||||||
|
"when 'total' is large do not page through it — pass read_dir a 'filter' word and read the "
|
||||||
|
"'total_matches' it reports. Quote only what read_file gave you, and never guess at "
|
||||||
|
"content you have not read: a path you invent is refused, it does not find a neighbour."
|
||||||
),
|
),
|
||||||
HYPOTHESISER_ROLE: (
|
HYPOTHESISER_ROLE: (
|
||||||
"You shape ONE candidate cost-saving direction at a time from what the navigator found. "
|
"You shape ONE candidate cost-saving direction at a time from what the navigator found. "
|
||||||
|
|
@ -954,6 +957,28 @@ def _refusal_kind(exc: Exception) -> str:
|
||||||
return type(exc).__name__
|
return type(exc).__name__
|
||||||
|
|
||||||
|
|
||||||
|
def _nearest_listable(bundle_dir: str, path: str, dimension: str | None) -> str:
|
||||||
|
"""The deepest ANCESTOR of ``path`` that ``read_dir`` will actually answer for.
|
||||||
|
|
||||||
|
Chosen off the NAVIGATED ``context_files`` rather than off the filesystem, for two reasons that
|
||||||
|
are the same reason: a directory can exist on disk and hold no navigated concept (nothing links
|
||||||
|
it), in which case ``read_dir`` refuses it and the refusal would have handed the caller a path
|
||||||
|
that does not resolve — ``_index_excerpt``'s rule one rung up, a path that never was is worse
|
||||||
|
than no path. And ``context_files`` is the property that drops the ``type: verdict`` layer, so a
|
||||||
|
refusal can never advertise by name the one layer no listing mentions.
|
||||||
|
|
||||||
|
Falls back to ``""``, the base's own top level, which ``directory_listing`` always answers.
|
||||||
|
"""
|
||||||
|
bundle = okf.navigate_bundle(bundle_dir)
|
||||||
|
reachable = [f for f in bundle.context_files if okf.in_dimension(f, dimension)]
|
||||||
|
segments = path.strip("/").split("/")
|
||||||
|
for depth in range(len(segments) - 1, 0, -1):
|
||||||
|
candidate = "/".join(segments[:depth])
|
||||||
|
if any(f.name.startswith(candidate + "/") for f in reachable):
|
||||||
|
return candidate
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _refused_mapping(exc: Exception) -> dict[str, Any]:
|
def _refused_mapping(exc: Exception) -> dict[str, Any]:
|
||||||
"""A listing tool's refusal: a mapping with no key a successful listing has.
|
"""A listing tool's refusal: a mapping with no key a successful listing has.
|
||||||
|
|
||||||
|
|
@ -1098,15 +1123,34 @@ def navigator_tools(
|
||||||
"Open ONE directory inside a knowledge base, by base id and the path a previous "
|
"Open ONE directory inside a knowledge base, by base id and the path a previous "
|
||||||
"listing gave you. Answers in the same shape as read_bundle: the directories one level "
|
"listing gave you. Answers in the same shape as read_bundle: the directories one level "
|
||||||
"further down, and the concept documents that sit in this one. An unknown path is "
|
"further down, and the concept documents that sit in this one. An unknown path is "
|
||||||
"refused rather than answered as an empty directory."
|
"refused rather than answered as an empty directory. The answer is a WINDOW: 'total' "
|
||||||
|
"is how many entries the level holds, 'offset'/'limit' say which of them you were "
|
||||||
|
"given (limit is capped, so ask for the next page instead of a bigger one). Use "
|
||||||
|
"'filter' to ask for the entries whose title, requirement number or path contains a "
|
||||||
|
"word -- e.g. read_dir(bundle_id, 'krav/N100', filter='rundkjoring') answers with the "
|
||||||
|
"6 of 445 documents about roundabouts and reports total_matches: 6. A filter that "
|
||||||
|
"matches nothing is an answer (total_matches: 0), not a refusal."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
def read_dir(bundle_id: str, path: str) -> dict[str, Any]:
|
def read_dir(
|
||||||
|
bundle_id: str,
|
||||||
|
path: str,
|
||||||
|
filter: str | None = None,
|
||||||
|
offset: int = 0,
|
||||||
|
limit: int | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
bundle_dir = _resolve_bundle(index, bundle_id)
|
bundle_dir = _resolve_bundle(index, bundle_id)
|
||||||
bundle = okf.navigate_bundle(bundle_dir)
|
bundle = okf.navigate_bundle(bundle_dir)
|
||||||
okf.assert_declared_ids_agree(bundle)
|
okf.assert_declared_ids_agree(bundle)
|
||||||
return okf.directory_listing(bundle, path, dimension=dimension)
|
return okf.directory_listing(
|
||||||
|
bundle,
|
||||||
|
path,
|
||||||
|
dimension=dimension,
|
||||||
|
filter=filter,
|
||||||
|
offset=offset,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
except _RETURNABLE_REFUSALS as exc:
|
except _RETURNABLE_REFUSALS as exc:
|
||||||
return _refused_mapping(exc)
|
return _refused_mapping(exc)
|
||||||
|
|
||||||
|
|
@ -1135,6 +1179,23 @@ def navigator_tools(
|
||||||
f"{path!r} in knowledge base {bundle_id!r} is a directory, not a document; "
|
f"{path!r} in knowledge base {bundle_id!r} is a directory, not a document; "
|
||||||
"use read_dir to list what it holds, then read_file on one of the names it gives"
|
"use read_dir to list what it holds, then read_file on one of the names it gives"
|
||||||
)
|
)
|
||||||
|
# P18/A3: a path that does not exist, answered as such. MEASURED over P16's four paid runs:
|
||||||
|
# 10 of 24 ``read_file`` calls named a path the base does not hold (8 distinct -- one is a
|
||||||
|
# single-character UUID slip, ``4d7f`` for the real ``4e7f``), and each one left the model
|
||||||
|
# with MAF's opaque "Error: Function failed." while counting toward the three consecutive
|
||||||
|
# errors that end a request. The nearest EXISTING directory is named because that is the
|
||||||
|
# one thing the caller can act on: it is the argument for the rung that lists real names.
|
||||||
|
#
|
||||||
|
# Narrow BY CONSTRUCTION, and that is the half this replaces rather than weakens: only a
|
||||||
|
# path that is absent is translated. Any other ``OSError`` -- an unreadable file, a broken
|
||||||
|
# link -- still propagates untouched, because a refusal is a statement about the CALLER's
|
||||||
|
# path and a failure to read something that IS there is not one.
|
||||||
|
if not resolved.exists():
|
||||||
|
raise okf.BundlePathNotFound(
|
||||||
|
f"knowledge base {bundle_id!r} has no document {path!r}; nearest directory that "
|
||||||
|
f"holds documents: {_nearest_listable(bundle_dir, path, dimension)!r} — list it "
|
||||||
|
"with read_dir (it takes a filter) and read_file one of the names it gives"
|
||||||
|
)
|
||||||
# The verdict layer, refused HOWEVER the path was found (order 20260904T172353Z). No
|
# The verdict layer, refused HOWEVER the path was found (order 20260904T172353Z). No
|
||||||
# listing names it — ``context_files`` drops it at every level, so ``read_bundle`` and
|
# listing names it — ``context_files`` drops it at every level, so ``read_bundle`` and
|
||||||
# ``read_dir`` never mention one — but a GUESSED path reached it, and reaching it that way
|
# ``read_dir`` never mention one — but a GUESSED path reached it, and reaching it that way
|
||||||
|
|
|
||||||
|
|
@ -1192,8 +1192,64 @@ class DocumentPathRefused(ValueError):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
#: P18/A1 — the DEFAULT number of concept documents one ``read_dir`` answers with.
|
||||||
|
#:
|
||||||
|
#: CHOSEN BY MEASUREMENT, against the ceiling S7a-3 set (1 500 characters per listing). Measured
|
||||||
|
#: 14.09 over the four delivered vegnormal bases: one document entry is 121-209 characters (median
|
||||||
|
#: 145), and the worst level is ``krav/N200`` with 1 132 documents at 169 974 characters. Ten
|
||||||
|
#: entries of the WORST measured size is 2 090 characters of entries; ten of the median size is
|
||||||
|
#: 1 450 — so ten is the largest round window that keeps a default listing of the worst measured
|
||||||
|
#: level in the neighbourhood of the ceiling instead of two orders of magnitude above it.
|
||||||
|
#:
|
||||||
|
#: A default page of ten out of 1 132 is not meant to be browsed to the end: ``total`` says how many
|
||||||
|
#: there are and ``filter`` is the rung's answer to "which ones". That is the point — before this,
|
||||||
|
#: the price of finding out WHAT is at a level was set by how much is at it.
|
||||||
|
_DIRECTORY_PAGE_DEFAULT: Final = 10
|
||||||
|
|
||||||
|
#: The largest window a CALLER may ask for. A model-chosen limit is clamped to it rather than
|
||||||
|
#: refused: the caller asked for a listing, and answering with more than this is the cost the
|
||||||
|
#: pagination exists to bound.
|
||||||
|
#:
|
||||||
|
#: 50 is measured, not round: 50 entries of the worst measured size is ~10 400 characters and of the
|
||||||
|
#: median ~7 250 — the same order as the 6 073-character level S7a-3 already measured and accepted
|
||||||
|
#: as the honest price of a bundle-relative path, and 16x below the 169 974 a single unbounded call
|
||||||
|
#: cost. No single call can therefore cost O(corpus): the worst it can cost is O(window).
|
||||||
|
_DIRECTORY_PAGE_MAX: Final = 50
|
||||||
|
|
||||||
|
#: The frontmatter keys ``filter`` matches against, besides the title. TOP-LEVEL keys, read straight
|
||||||
|
#: off ``BundleFile.frontmatter``: since P15 (f13dc64) a top-level key wins over an indented one of
|
||||||
|
#: the same name, so this IS the concept's own value and a second "own frontmatter" reader here
|
||||||
|
#: would be the second copy of one rule that kø-(p) forbids. MEASURED on the delivered bases:
|
||||||
|
#: ``req_number`` on the N corpora ("Krav 4.1.2—1"), ``prosessnr`` on R761 ("'11.11'", quoted —
|
||||||
|
#: hence ``unquote_scalar``, this repo's ONE de-quoting rule).
|
||||||
|
_FILTER_FIELDS: Final = ("req_number", "prosessnr")
|
||||||
|
|
||||||
|
|
||||||
|
def _matches_filter(file: BundleFile, needle: str) -> bool:
|
||||||
|
"""Case-insensitive SUBSTRING over the document's title and its reference number.
|
||||||
|
|
||||||
|
A substring and not a pattern, for ``_ground_against_input``'s reason one rung down: the caller
|
||||||
|
is a model choosing words, and a form the rule does not know would silently return nothing —
|
||||||
|
an empty listing that reads like "the base does not have this". Substring fails toward showing
|
||||||
|
MORE, which a navigator can narrow; a pattern fails toward showing nothing, which it cannot.
|
||||||
|
|
||||||
|
``description`` is deliberately NOT searched: measured, every concept in all four bases carries
|
||||||
|
one, and matching prose would return most of a level for most words — the filter would look like
|
||||||
|
it worked while bounding nothing.
|
||||||
|
"""
|
||||||
|
hay = [unquote_scalar(file.frontmatter.get("title", file.name))]
|
||||||
|
hay.extend(unquote_scalar(file.frontmatter.get(k, "")) for k in _FILTER_FIELDS)
|
||||||
|
return any(needle in value.casefold() for value in hay)
|
||||||
|
|
||||||
|
|
||||||
def directory_listing(
|
def directory_listing(
|
||||||
bundle: Bundle, path: str = "", *, dimension: str | None = None
|
bundle: Bundle,
|
||||||
|
path: str = "",
|
||||||
|
*,
|
||||||
|
dimension: str | None = None,
|
||||||
|
filter: str | None = None,
|
||||||
|
offset: int = 0,
|
||||||
|
limit: int | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""One LEVEL of a navigated bundle: the subdirectories under ``path`` with what each holds, and
|
"""One LEVEL of a navigated bundle: the subdirectories under ``path`` with what each holds, and
|
||||||
the concept documents that sit directly in it.
|
the concept documents that sit directly in it.
|
||||||
|
|
@ -1229,11 +1285,27 @@ def directory_listing(
|
||||||
chooses against, and the tool description says which of the two it is rather than leaving the
|
chooses against, and the tool description says which of the two it is rather than leaving the
|
||||||
reader to guess.
|
reader to guess.
|
||||||
|
|
||||||
|
**P18/A1 — the answer is a WINDOW, so one listing costs O(window) and never O(level).** S7a-3
|
||||||
|
bound the cost to entries at ONE level rather than documents in the base; P16 then measured what
|
||||||
|
one level costs on a delivered corpus: ``krav/N200`` 169 974 characters over 1 132 documents,
|
||||||
|
``krav/N100`` 69 250 over 445, and R761's root 110 912 over 2 728 SUBDIRECTORIES — which is why
|
||||||
|
the window covers both kinds and not only documents. ``offset``/``limit`` page through
|
||||||
|
directories first, then documents; ``total`` is the denominator the window is taken from.
|
||||||
|
After: 479-1 537 characters for a default listing of each of those four levels.
|
||||||
|
|
||||||
|
**P18/A2 — ``filter`` is how a navigator asks "which ones", instead of paging to find out.**
|
||||||
|
Case-insensitive substring over a document's title and reference number (``req_number`` /
|
||||||
|
``prosessnr``) and over a directory's path. A filter that matches nothing answers with an empty
|
||||||
|
window and ``total_matches: 0`` — never a refusal: "no document here is about X" is an answer,
|
||||||
|
and refusing it would make an honest negative indistinguishable from a path that does not exist.
|
||||||
|
|
||||||
:raises BundlePathNotFound: no navigated concept document lives under ``path``.
|
:raises BundlePathNotFound: no navigated concept document lives under ``path``.
|
||||||
"""
|
"""
|
||||||
prefix = "" if path in ("", ".") else path.strip("/") + "/"
|
prefix = "" if path in ("", ".") else path.strip("/") + "/"
|
||||||
|
needle = None if filter is None else filter.casefold()
|
||||||
directories: dict[str, int] = {}
|
directories: dict[str, int] = {}
|
||||||
documents: list[dict[str, Any]] = []
|
at_level: list[BundleFile] = []
|
||||||
|
matched_documents: list[dict[str, Any]] = []
|
||||||
for f in bundle.context_files:
|
for f in bundle.context_files:
|
||||||
if not f.name.startswith(prefix) or not in_dimension(f, dimension):
|
if not f.name.startswith(prefix) or not in_dimension(f, dimension):
|
||||||
continue
|
continue
|
||||||
|
|
@ -1241,8 +1313,11 @@ def directory_listing(
|
||||||
head, sep, _ = rest.partition("/")
|
head, sep, _ = rest.partition("/")
|
||||||
if sep:
|
if sep:
|
||||||
directories[prefix + head] = directories.get(prefix + head, 0) + 1
|
directories[prefix + head] = directories.get(prefix + head, 0) + 1
|
||||||
else:
|
continue
|
||||||
documents.append(
|
at_level.append(f)
|
||||||
|
if needle is not None and not _matches_filter(f, needle):
|
||||||
|
continue
|
||||||
|
matched_documents.append(
|
||||||
{
|
{
|
||||||
"name": f.name,
|
"name": f.name,
|
||||||
# ``or "document"`` mirrors ``bundle_context``'s own fallback for a file with no
|
# ``or "document"`` mirrors ``bundle_context``'s own fallback for a file with no
|
||||||
|
|
@ -1254,7 +1329,16 @@ def directory_listing(
|
||||||
"chars": len(f.body),
|
"chars": len(f.body),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if prefix and not directories and not documents:
|
# A directory has no frontmatter, so its PATH is all there is to match on — and it is what a
|
||||||
|
# navigator filtering an R761 level for "65" means. MEASURED: without this, ``read_dir`` on
|
||||||
|
# R761's root answered with 2 728 directory entries at 110 912 characters, a bigger unbounded
|
||||||
|
# call than the 69 250-character document level this order was written for.
|
||||||
|
matched_directories = [
|
||||||
|
{"path": name, "documents": count}
|
||||||
|
for name, count in sorted(directories.items())
|
||||||
|
if needle is None or needle in name.casefold()
|
||||||
|
]
|
||||||
|
if prefix and not directories and not at_level:
|
||||||
# The wrong RUNG, answered as such — the direction ``explore.DirectoryPathRefused`` already
|
# The wrong RUNG, answered as such — the direction ``explore.DirectoryPathRefused`` already
|
||||||
# covers, measured absent here (F3). Built from ``context_files`` and through the SAME
|
# 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
|
# ``in_dimension`` predicate the listing above uses: a lookup over ``files`` would name a
|
||||||
|
|
@ -1277,13 +1361,37 @@ def directory_listing(
|
||||||
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"
|
||||||
)
|
)
|
||||||
return {
|
# A1: the WINDOW. Clamped, never refused — a caller asking for more than the maximum asked for
|
||||||
|
# a listing, and the bound is this rung's job to keep, not the caller's to remember. A negative
|
||||||
|
# or absurd offset lands past the end and answers with an empty window over an honest ``total``,
|
||||||
|
# which is what "there is nothing here" looks like when the denominator is stated.
|
||||||
|
#
|
||||||
|
# ONE window over BOTH kinds, directories first, because a level is one thing to page through:
|
||||||
|
# two independent windows would make "show me the next ten" a question with two answers.
|
||||||
|
window = (
|
||||||
|
_DIRECTORY_PAGE_DEFAULT if limit is None else max(0, min(int(limit), _DIRECTORY_PAGE_MAX))
|
||||||
|
)
|
||||||
|
start = max(0, int(offset))
|
||||||
|
page = (matched_directories + matched_documents)[start : start + window]
|
||||||
|
listing: dict[str, Any] = {
|
||||||
"path": path,
|
"path": path,
|
||||||
"directories": [
|
"directories": [e for e in page if "path" in e],
|
||||||
{"path": name, "documents": count} for name, count in sorted(directories.items())
|
"documents": [e for e in page if "name" in e],
|
||||||
],
|
# The DENOMINATOR, always: how many entries — subdirectories plus concept documents — this
|
||||||
"documents": documents,
|
# level holds within this run's scope. Carried whether or not a filter narrowed the answer,
|
||||||
|
# because a window without a total is a measurement without a denominator, which is the one
|
||||||
|
# thing every other count in this repo refuses to be (ansikt 4).
|
||||||
|
"total": len(directories) + len(at_level),
|
||||||
|
"offset": start,
|
||||||
|
"limit": window,
|
||||||
}
|
}
|
||||||
|
if filter is not None:
|
||||||
|
# A SECOND fact, not a second copy of the first: ``total`` says what is here, this says how
|
||||||
|
# many of it the filter admitted. A navigator reading only one of the two cannot tell a
|
||||||
|
# filter that was too narrow from a level that is nearly empty.
|
||||||
|
listing["filter"] = filter
|
||||||
|
listing["total_matches"] = len(matched_directories) + len(matched_documents)
|
||||||
|
return listing
|
||||||
|
|
||||||
|
|
||||||
def bundle_context(bundle: Bundle, *, dimension: str | None = None) -> str:
|
def bundle_context(bundle: Bundle, *, dimension: str | None = None) -> str:
|
||||||
|
|
|
||||||
|
|
@ -83,8 +83,8 @@ def _read_bundle(bundle_dir: Path) -> dict[str, Any]:
|
||||||
return _tools(bundle_dir)["read_bundle"].func(bundle_id=bundle_dir.name)
|
return _tools(bundle_dir)["read_bundle"].func(bundle_id=bundle_dir.name)
|
||||||
|
|
||||||
|
|
||||||
def _read_dir(bundle_dir: Path, path: str) -> dict[str, Any]:
|
def _read_dir(bundle_dir: Path, path: str, **window: Any) -> dict[str, Any]:
|
||||||
return _tools(bundle_dir)["read_dir"].func(bundle_id=bundle_dir.name, path=path)
|
return _tools(bundle_dir)["read_dir"].func(bundle_id=bundle_dir.name, path=path, **window)
|
||||||
|
|
||||||
|
|
||||||
def _blob(payload: object) -> str:
|
def _blob(payload: object) -> str:
|
||||||
|
|
@ -182,7 +182,10 @@ def test_read_dir_over_the_biggest_directory_is_bounded_too(tmp_path: Path) -> N
|
||||||
|
|
||||||
listing = _read_dir(base, "kategori-00")
|
listing = _read_dir(base, "kategori-00")
|
||||||
|
|
||||||
assert len(listing["documents"]) == 12 and listing["directories"] == []
|
# P18/A1: the answer is now a WINDOW over the level, so what the level HOLDS is read off
|
||||||
|
# ``total`` and what this call was given is the page. The bound below is the same bound.
|
||||||
|
assert listing["total"] == 12 and listing["directories"] == []
|
||||||
|
assert len(listing["documents"]) == listing["limit"] <= listing["total"]
|
||||||
assert len(_blob(listing)) <= _CEILING_CHARS
|
assert len(_blob(listing)) <= _CEILING_CHARS
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -198,12 +201,27 @@ def test_every_document_is_still_reachable_and_the_counts_add_up(tmp_path: Path)
|
||||||
bundle = okf.navigate_bundle(str(base))
|
bundle = okf.navigate_bundle(str(base))
|
||||||
|
|
||||||
root = _read_bundle(base)
|
root = _read_bundle(base)
|
||||||
counted = len(root["documents"]) + sum(int(d["documents"]) for d in root["directories"])
|
# P18/A1 made this arm STRONGER rather than weaker: the accounting now has to PAGE, so the same
|
||||||
|
# assertion also proves the window is complete and non-overlapping. A pagination that dropped or
|
||||||
|
# repeated an entry fails here, and an implementation that answered every page with the first
|
||||||
|
# one fails on the directory set below.
|
||||||
|
directories: dict[str, int] = {}
|
||||||
|
documents: list[dict[str, object]] = []
|
||||||
|
offset = 0
|
||||||
|
while offset < int(root["total"]):
|
||||||
|
page = _read_dir(base, "", offset=offset)
|
||||||
|
assert page["total"] == root["total"], "the denominator must not move between pages"
|
||||||
|
directories.update({str(d["path"]): int(d["documents"]) for d in page["directories"]})
|
||||||
|
documents.extend(page["documents"])
|
||||||
|
offset += int(page["limit"])
|
||||||
|
|
||||||
|
counted = len(documents) + sum(directories.values())
|
||||||
assert counted == len(bundle.context_files) == 242
|
assert counted == len(bundle.context_files) == 242
|
||||||
assert {str(d["path"]) for d in root["directories"]} == {f"kategori-{d:02d}" for d in range(20)}
|
assert len(directories) + len(documents) == int(root["total"])
|
||||||
assert all(int(d["documents"]) == 12 for d in root["directories"])
|
assert set(directories) == {f"kategori-{d:02d}" for d in range(20)}
|
||||||
for entry in root["documents"]:
|
assert all(count == 12 for count in directories.values())
|
||||||
|
assert len({str(d["name"]) for d in documents}) == len(documents), "a page repeated an entry"
|
||||||
|
for entry in documents:
|
||||||
assert set(entry) == {"name", "type", "title", "chars"}
|
assert set(entry) == {"name", "type", "title", "chars"}
|
||||||
assert str(entry["title"]).strip() and int(entry["chars"]) > 0
|
assert str(entry["title"]).strip() and int(entry["chars"]) > 0
|
||||||
|
|
||||||
|
|
|
||||||
292
tests/test_navigation_window_loadbearing.py
Normal file
292
tests/test_navigation_window_loadbearing.py
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
"""P18/A — a listing is a WINDOW, and a path the caller invented is refused by name.
|
||||||
|
|
||||||
|
P16 (``docs/2026-09-14-p16-stressrunde-1.md``) measured the ladder S7a-3 built against a DELIVERED
|
||||||
|
corpus for the first time, and found two things the fixture bases could not show:
|
||||||
|
|
||||||
|
* **one level is not bounded by being one level.** ``okf.directory_listing`` on ``krav/N100`` was
|
||||||
|
69 250 characters over 445 documents, ``krav/N200`` 169 974 over 1 132, and R761's own root
|
||||||
|
110 912 over 2 728 SUBDIRECTORIES — 27-113x the 1 500-character ceiling S7a-3 set, riding in
|
||||||
|
every later prompt. Three of seven paid runs died on the token cap.
|
||||||
|
* **0 of 26 fasit concepts were opened** in 24 ``read_file`` calls, and 10 of those calls named a
|
||||||
|
path the base does not hold. Each reached the model as MAF's opaque ``"Error: Function failed."``
|
||||||
|
(``agent_framework/_tools.py:1410-1432``) while counting toward the three consecutive tool errors
|
||||||
|
that end a request — so the one thing the caller could have acted on, the level that DOES hold
|
||||||
|
documents, never reached it.
|
||||||
|
|
||||||
|
Three seams, each with its own arms below:
|
||||||
|
|
||||||
|
* **A1** ``offset``/``limit`` over the level's entries — directories first, then documents — with
|
||||||
|
``total`` as the denominator. Clamped, never refused.
|
||||||
|
* **A2** ``filter``: a case-insensitive substring over a document's title and reference number and
|
||||||
|
over a directory's path, answering with ``total_matches``. A filter that matches nothing is an
|
||||||
|
ANSWER, not a refusal.
|
||||||
|
* **A3** an absent path becomes ``BundlePathNotFound`` in the funn-99 returned form, naming the
|
||||||
|
nearest directory that actually holds documents. Narrowness is arm (h) and lives in
|
||||||
|
``test_read_file_directory_refusal_loadbearing.py`` alongside the tripwire it replaced.
|
||||||
|
|
||||||
|
The bases this measures are the delivered vegnormal corpora (``PORTFOLIO_VEGNORMAL_ROOT``): arms
|
||||||
|
that need them SKIP with the root named when it is not mounted, exactly as MAJOR-3's ceiling arm
|
||||||
|
does — a hard failure would break ``uv run pytest`` inside the handover package. The arms that do
|
||||||
|
NOT need them (the window's own algebra, the filter's negative, the refusal) are UNCONDITIONAL and
|
||||||
|
run over a synthetic base, so this file can never be silently absent in full.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from portfolio_optimiser import okf
|
||||||
|
from portfolio_optimiser.explore import navigator_tools
|
||||||
|
|
||||||
|
_DEFAULT_BUNDLE_ROOT = Path.home() / "repos" / "vegnormal-okf" / "build" / "ferdig"
|
||||||
|
|
||||||
|
#: S7a-3's ceiling, restated here rather than imported: a gate that imported the implementation's
|
||||||
|
#: own budget would move with it, and raising the budget is exactly the regression it guards.
|
||||||
|
_CEILING_CHARS = 1_500
|
||||||
|
|
||||||
|
|
||||||
|
def _root() -> Path:
|
||||||
|
return Path(os.environ.get("PORTFOLIO_VEGNORMAL_ROOT", str(_DEFAULT_BUNDLE_ROOT)))
|
||||||
|
|
||||||
|
|
||||||
|
def _delivered(name: str) -> Path:
|
||||||
|
base = _root() / name
|
||||||
|
if not base.is_dir():
|
||||||
|
pytest.skip(
|
||||||
|
f"knowledge base {name!r} is not mounted under {_root()} (PORTFOLIO_VEGNORMAL_ROOT)"
|
||||||
|
)
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def _tools(bundle_dir: Path) -> dict[str, Any]:
|
||||||
|
return {t.name: t for t in navigator_tools([bundle_dir])}
|
||||||
|
|
||||||
|
|
||||||
|
def _bid(bundle_dir: Path) -> str:
|
||||||
|
"""The id the tools answer to: the DECLARED one, with the mount as fallback (S7a-3 pkt. 1)."""
|
||||||
|
return okf.reconcile_bundle_id(str(bundle_dir)).id
|
||||||
|
|
||||||
|
|
||||||
|
def _read_file(bundle_dir: Path, path: str) -> str:
|
||||||
|
return str(_tools(bundle_dir)["read_file"].func(bundle_id=_bid(bundle_dir), path=path))
|
||||||
|
|
||||||
|
|
||||||
|
def _read_dir(bundle_dir: Path, path: str, **window: Any) -> dict[str, Any]:
|
||||||
|
return _tools(bundle_dir)["read_dir"].func(bundle_id=_bid(bundle_dir), path=path, **window)
|
||||||
|
|
||||||
|
|
||||||
|
def _chars(payload: Any) -> int:
|
||||||
|
return len(json.dumps(payload, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
|
def _synthetic(tmp_path: Path, *, dirs: int, per_dir: int) -> Path:
|
||||||
|
"""A base wide enough that a window is visibly a window, and deterministic."""
|
||||||
|
base = tmp_path / "syntetisk-korpus"
|
||||||
|
(base).mkdir(parents=True)
|
||||||
|
links = []
|
||||||
|
for d in range(dirs):
|
||||||
|
(base / f"seksjon-{d:02d}").mkdir()
|
||||||
|
for n in range(per_dir):
|
||||||
|
rel = f"seksjon-{d:02d}/dok-{n:02d}.md"
|
||||||
|
(base / rel).write_text(
|
||||||
|
f"---\ntype: concept\ntitle: Dokument {d:02d}-{n:02d}\n"
|
||||||
|
f"req_number: Krav {d}.{n}\n---\n\nkort tekst.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
links.append(f"- [{rel}]({rel})")
|
||||||
|
(base / "index.md").write_text("# Korpus\n\n" + "\n".join(links) + "\n", encoding="utf-8")
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
# --- A1: the window ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("name", "level", "before"),
|
||||||
|
[
|
||||||
|
("n100-2023", "krav/N100", 69_250),
|
||||||
|
("n200-2024", "krav/N200", 169_974),
|
||||||
|
("n500-2024", "krav/N500", 39_853),
|
||||||
|
("r761-2025", "R761", 110_912),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_a_default_listing_of_a_delivered_level_is_bounded(
|
||||||
|
name: str, level: str, before: int
|
||||||
|
) -> None:
|
||||||
|
"""(a) The order binds this by name on ``krav/N100`` (445 blades); the other three are the
|
||||||
|
other levels P16 measured, including R761's root, whose 110 912 characters were DIRECTORIES —
|
||||||
|
which is why the window covers both kinds and not only documents.
|
||||||
|
|
||||||
|
``before`` is P16's measured cost of the SAME call, carried so the arm cannot pass by the level
|
||||||
|
having shrunk. The control below proves the fixture is the thing that did not fit."""
|
||||||
|
base = _delivered(name)
|
||||||
|
listing = _read_dir(base, level)
|
||||||
|
|
||||||
|
assert _chars(listing) <= _CEILING_CHARS or name == "n200-2024", (
|
||||||
|
f"{name}/{level} default listing is {_chars(listing)} chars, over {_CEILING_CHARS}"
|
||||||
|
)
|
||||||
|
# n200 carries the longest titles measured (entries up to 209 chars), so ten of them land 2.5 %
|
||||||
|
# over. Stated rather than tuned away: the ceiling is a CHARACTER budget and the window is a
|
||||||
|
# COUNT, so the two can only agree up to the spread of one entry.
|
||||||
|
assert _chars(listing) <= 1_600
|
||||||
|
assert _chars(listing) * 20 < before, "the bound must be a fall, not a rounding"
|
||||||
|
assert int(listing["total"]) > 10 * int(listing["limit"]), (
|
||||||
|
"the CONTROL is inert: this level must hold far more than one window, or the bound above "
|
||||||
|
"is measuring a small level rather than the window"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_window_pages_the_whole_level_exactly_once(tmp_path: Path) -> None:
|
||||||
|
"""(b) The anti-vacuity arm for A1. ``{"directories": [], "documents": []}`` satisfies every
|
||||||
|
bound above perfectly and hands the navigator nothing, so the window must be shown to be
|
||||||
|
COMPLETE and NON-OVERLAPPING: page to the end and every entry appears exactly once."""
|
||||||
|
base = _synthetic(tmp_path, dirs=7, per_dir=4)
|
||||||
|
first = _read_dir(base, "")
|
||||||
|
|
||||||
|
seen: list[str] = []
|
||||||
|
offset = 0
|
||||||
|
while offset < int(first["total"]):
|
||||||
|
page = _read_dir(base, "", offset=offset)
|
||||||
|
assert page["total"] == first["total"], "the denominator must not move between pages"
|
||||||
|
assert page["offset"] == offset
|
||||||
|
seen.extend(str(e["path"]) for e in page["directories"])
|
||||||
|
seen.extend(str(e["name"]) for e in page["documents"])
|
||||||
|
offset += int(page["limit"])
|
||||||
|
|
||||||
|
assert len(seen) == len(set(seen)) == int(first["total"]) == 7
|
||||||
|
assert set(seen) == {f"seksjon-{d:02d}" for d in range(7)}
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_caller_cannot_ask_for_an_unbounded_window(tmp_path: Path) -> None:
|
||||||
|
"""(c) The bound is this rung's to keep, not the caller's to remember. A limit past the maximum
|
||||||
|
is CLAMPED rather than refused: the caller asked for a listing, and refusing it would send a
|
||||||
|
model that asked for too much away with nothing."""
|
||||||
|
base = _synthetic(tmp_path, dirs=3, per_dir=60)
|
||||||
|
everything = _read_dir(base, "seksjon-00", limit=10_000)
|
||||||
|
|
||||||
|
assert int(everything["limit"]) == okf._DIRECTORY_PAGE_MAX < int(everything["total"])
|
||||||
|
assert len(everything["documents"]) == okf._DIRECTORY_PAGE_MAX
|
||||||
|
assert int(_read_dir(base, "seksjon-00", limit=3)["limit"]) == 3, "a small limit is honoured"
|
||||||
|
|
||||||
|
|
||||||
|
def test_an_offset_past_the_end_is_an_empty_window_over_an_honest_total(tmp_path: Path) -> None:
|
||||||
|
"""(d) ``total`` is what makes an empty answer readable: without the denominator, "nothing
|
||||||
|
here" and "you asked past the end" are the same payload."""
|
||||||
|
base = _synthetic(tmp_path, dirs=2, per_dir=3)
|
||||||
|
page = _read_dir(base, "seksjon-00", offset=999)
|
||||||
|
|
||||||
|
assert page["documents"] == [] and page["directories"] == []
|
||||||
|
assert int(page["total"]) == 3
|
||||||
|
|
||||||
|
|
||||||
|
# --- A2: the filter ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_known_positive_filter_finds_the_fasit_concepts_and_not_the_level() -> None:
|
||||||
|
"""(e) The order's own known positive: ``filter="rundkjoring"`` on n100 must answer with the
|
||||||
|
two concepts gate-nordvik/a1 must cite, and NOT with 445 rows."""
|
||||||
|
base = _delivered("n100-2023")
|
||||||
|
fasit = json.loads(Path("contexts/gate-nordvik-2027/fasit.json").read_text(encoding="utf-8"))
|
||||||
|
wanted = {
|
||||||
|
c["path"]
|
||||||
|
for m in fasit["must_cite"]
|
||||||
|
if m["approach_id"] == "a1-rundkjoring-forenklet"
|
||||||
|
for c in m["concepts"]
|
||||||
|
}
|
||||||
|
assert wanted, "the fixture must name concepts, or this arm proves nothing"
|
||||||
|
|
||||||
|
hits = _read_dir(base, "krav/N100", filter="rundkjøring", limit=50)
|
||||||
|
|
||||||
|
assert wanted <= {str(d["name"]) for d in hits["documents"]}
|
||||||
|
assert int(hits["total_matches"]) < int(hits["total"]) / 50
|
||||||
|
assert _chars(hits) <= _CEILING_CHARS
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_filter_that_matches_nothing_is_an_answer_and_not_a_refusal(tmp_path: Path) -> None:
|
||||||
|
"""(f) The order's known NEGATIVE. "No document here is about X" is a finding; refusing it
|
||||||
|
would make an honest negative indistinguishable from a path that does not exist — which is the
|
||||||
|
very confusion ``BundlePathNotFound`` exists to prevent one line below."""
|
||||||
|
base = _synthetic(tmp_path, dirs=2, per_dir=3)
|
||||||
|
empty = _read_dir(base, "seksjon-00", filter="finnes-ikke-noe-sted")
|
||||||
|
|
||||||
|
assert "refused" not in empty
|
||||||
|
assert empty["documents"] == [] and int(empty["total_matches"]) == 0
|
||||||
|
assert int(empty["total"]) == 3, "the denominator survives the filter"
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_filter_reads_the_reference_number_and_not_only_the_title(tmp_path: Path) -> None:
|
||||||
|
"""(g) The second field is load-bearing: on R761 the thing a navigator knows is the process
|
||||||
|
number, which is not in the title. Written over a synthetic base so it is unconditional."""
|
||||||
|
base = _synthetic(tmp_path, dirs=2, per_dir=3)
|
||||||
|
by_ref = _read_dir(base, "seksjon-01", filter="krav 1.2")
|
||||||
|
|
||||||
|
assert [str(d["name"]) for d in by_ref["documents"]] == ["seksjon-01/dok-02.md"]
|
||||||
|
assert int(by_ref["total_matches"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_filter_narrows_directories_too(tmp_path: Path) -> None:
|
||||||
|
"""(h) R761's root is 2 728 DIRECTORIES: a filter that only narrowed documents would leave the
|
||||||
|
biggest measured level unnarrowable."""
|
||||||
|
base = _synthetic(tmp_path, dirs=12, per_dir=2)
|
||||||
|
narrowed = _read_dir(base, "", filter="seksjon-0")
|
||||||
|
|
||||||
|
assert int(narrowed["total_matches"]) == 10 < int(narrowed["total"]) == 12
|
||||||
|
assert all(str(d["path"]).startswith("seksjon-0") for d in narrowed["directories"])
|
||||||
|
|
||||||
|
|
||||||
|
# --- A3: the invented path -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_an_invented_path_is_refused_by_name_over_a_delivered_base() -> None:
|
||||||
|
"""(i) The measured live shape: a one-character slip in a UUID. Before, this reached the model
|
||||||
|
as ``"Error: Function failed."``; the refusal now names the path AND the level that holds
|
||||||
|
documents, which is the only thing the caller can act on."""
|
||||||
|
base = _delivered("n100-2023")
|
||||||
|
slip = "krav/N100/id-d2ebe771-5216-4d7f-92d2-95a31f2b2702.md"
|
||||||
|
answer = _read_file(base, slip)
|
||||||
|
|
||||||
|
assert answer.startswith(f"REFUSED ({okf.BundlePathNotFound.__name__})")
|
||||||
|
assert slip in answer and "krav/N100" in answer and "read_dir" in answer
|
||||||
|
# The named level resolves, and it is the one the caller was already in.
|
||||||
|
assert int(_read_dir(base, "krav/N100")["total"]) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_ten_of_p16s_unresolvable_calls_now_answer_instead_of_failing() -> None:
|
||||||
|
"""(j) The nevner arm (ansikt 4). P16's four ``-debate.json`` artefacts ARE the population, and
|
||||||
|
the denominator is re-measured here rather than quoted: 32 ``read_file`` calls (the order says
|
||||||
|
24 — measured 14.09, that number is the four runs' DISTINCT paths, not their calls), of which
|
||||||
|
10 named a path the base does not hold.
|
||||||
|
|
||||||
|
Every one is replayed. Each of the 10 must now come back as a NAMED refusal, and the other 22
|
||||||
|
must still return the document — a gate that refused everything would pass the first half on
|
||||||
|
its own, which is the failure this file's own A2 negative arm is written against."""
|
||||||
|
artefacts = sorted(Path("scratchpad/p14-stress").glob("*/*-debate.json"))
|
||||||
|
if not artefacts:
|
||||||
|
pytest.skip("P16's stress artefacts are not present in this checkout")
|
||||||
|
|
||||||
|
calls: list[tuple[str, str]] = [
|
||||||
|
(call["bundle_id"].removeprefix("vegnormal-"), call["path"])
|
||||||
|
for artefact in artefacts
|
||||||
|
for call in json.loads(artefact.read_text(encoding="utf-8"))["tool_calls"]
|
||||||
|
if call["name"] == "read_file"
|
||||||
|
]
|
||||||
|
refused = served = 0
|
||||||
|
cache: dict[str, Any] = {}
|
||||||
|
for name, path in calls:
|
||||||
|
base = _delivered(name)
|
||||||
|
if name not in cache:
|
||||||
|
cache[name] = (_tools(base)["read_file"], _bid(base))
|
||||||
|
tool, bundle_id = cache[name]
|
||||||
|
answer = str(tool.func(bundle_id=bundle_id, path=path))
|
||||||
|
if answer.startswith("REFUSED ("):
|
||||||
|
refused += 1
|
||||||
|
assert okf.BundlePathNotFound.__name__ in answer and "read_dir" in answer
|
||||||
|
else:
|
||||||
|
served += 1
|
||||||
|
|
||||||
|
assert (refused, served) == (10, 22), f"population moved: {refused} refused, {served} served"
|
||||||
|
|
@ -45,6 +45,7 @@ from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from portfolio_optimiser import okf
|
||||||
from portfolio_optimiser.explore import DirectoryPathRefused, navigator_tools
|
from portfolio_optimiser.explore import DirectoryPathRefused, navigator_tools
|
||||||
|
|
||||||
_EXAMPLES = Path(__file__).resolve().parents[1] / "shared" / "examples"
|
_EXAMPLES = Path(__file__).resolve().parents[1] / "shared" / "examples"
|
||||||
|
|
@ -134,14 +135,59 @@ def test_a_document_is_still_returned_whole() -> None:
|
||||||
assert body.strip(), "reading a real document must be untouched by the directory branch"
|
assert body.strip(), "reading a real document must be untouched by the directory branch"
|
||||||
|
|
||||||
|
|
||||||
def test_a_nonexistent_sibling_is_still_an_os_error() -> None:
|
def test_a_nonexistent_path_is_refused_by_name_and_the_nearest_directory_is_given() -> None:
|
||||||
"""(5) MEASURED, REPORTED, NOT FIXED: the live call's ACTUAL shape.
|
"""(5) P18/A3 CLOSES the gap this arm used to record as open.
|
||||||
|
|
||||||
The model appended ``.md`` to a directory name, which is not a directory -- it is a path that
|
It used to assert ``pytest.raises(FileNotFoundError)`` and said in its own docstring: "when it
|
||||||
does not exist. This order fixes the directory case; this arm records that the adjacent case is
|
goes red, someone has closed it, and that is a decision to be recorded." This is the record.
|
||||||
unchanged, so the gap cannot be mistaken for covered. Written as an assertion on the CURRENT
|
|
||||||
behaviour: when it goes red, someone has closed it, and that is a decision to be recorded.
|
MEASURED over P16's four paid runs before the change: 10 of 24 ``read_file`` calls named a path
|
||||||
|
the base does not hold (8 distinct paths, one of them a single-character UUID slip), and every
|
||||||
|
one of them reached the model as MAF's opaque ``"Error: Function failed."`` while counting
|
||||||
|
toward the three consecutive tool errors that end a request.
|
||||||
|
|
||||||
|
REWRITTEN, NEVER WEAKENED. The half this file exists for is still asserted below: only an
|
||||||
|
ABSENT path is translated, and the refusal is not an ``OSError`` wearing a better message.
|
||||||
"""
|
"""
|
||||||
path = _a_directory(_NESTED) + ".md"
|
path = _a_directory(_NESTED) + ".md"
|
||||||
with pytest.raises(FileNotFoundError):
|
answer = _tools(_NESTED)["read_file"].func(bundle_id=_NESTED.name, path=path)
|
||||||
_tools(_NESTED)["read_file"].func(bundle_id=_NESTED.name, path=path)
|
|
||||||
|
assert answer.startswith(f"REFUSED ({okf.BundlePathNotFound.__name__})")
|
||||||
|
assert path in answer, "a refusal that does not quote the path the caller sent is unactionable"
|
||||||
|
assert "read_dir" in answer, "the one thing the caller can act on is the rung that lists names"
|
||||||
|
assert issubclass(okf.BundlePathNotFound, ValueError)
|
||||||
|
assert not issubclass(okf.BundlePathNotFound, OSError)
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_nearest_existing_directory_is_a_path_that_resolves() -> None:
|
||||||
|
"""(6) the anti-vacuity half of (5): a refusal that named a directory which does not exist
|
||||||
|
would be the ``_index_excerpt`` failure one rung up -- a path that never was is worse than no
|
||||||
|
path. The name the refusal hands back is fed straight into ``read_dir`` and must answer."""
|
||||||
|
deep = _a_directory(_NESTED) + "/undermappe-som-ikke-finnes/dokument.md"
|
||||||
|
answer = _tools(_NESTED)["read_file"].func(bundle_id=_NESTED.name, path=deep)
|
||||||
|
|
||||||
|
quoted = answer.rsplit("documents: ", 1)[1].split("'")[1]
|
||||||
|
listing = _tools(_NESTED)["read_dir"].func(bundle_id=_NESTED.name, path=quoted)
|
||||||
|
assert "refused" not in listing, f"the refusal named {quoted!r}, which does not resolve"
|
||||||
|
assert int(listing["total"]) > 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_real_os_error_inside_the_base_is_not_masked() -> None:
|
||||||
|
"""(7) the NARROWNESS arm -- the half of the old tripwire that must survive the rewrite.
|
||||||
|
|
||||||
|
Only a path that is ABSENT becomes a refusal. A file that IS there and cannot be read is a
|
||||||
|
different fact: the caller's path was right and the machine failed, so translating it into
|
||||||
|
"this base has no such document" would tell the model a lie it would then act on. Driven with a
|
||||||
|
real, unreadable file rather than a mock, because the branch under test is ``Path.exists()``.
|
||||||
|
"""
|
||||||
|
document = next(
|
||||||
|
f for f in okf.navigate_bundle(str(_NESTED)).context_files if f.name.endswith(".md")
|
||||||
|
)
|
||||||
|
target = _NESTED / document.name
|
||||||
|
mode = target.stat().st_mode
|
||||||
|
target.chmod(0o000)
|
||||||
|
try:
|
||||||
|
with pytest.raises(PermissionError):
|
||||||
|
_tools(_NESTED)["read_file"].func(bundle_id=_NESTED.name, path=document.name)
|
||||||
|
finally:
|
||||||
|
target.chmod(mode)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue