refactor(examples): replace sector-specific example material with generic, fictitious examples

The context sets, the packaged knowledge bases and the example bundles are
replaced by one fictitious example set about IT operations in an invented
organisation: three context sets (serverrom-2027, driftsavtale-2027 and the
two-base drift-og-avtale-2027), two synthetic knowledge bases under
src/portfolio_optimiser/data/kunnskapsbaser and two example bundles under
src/portfolio_optimiser/data/bundles. Numbers, codes and structural values in
tests and fixtures are kept; names, ids and wording change. Dated measurement
documents that only recorded runs on the replaced material are deleted.

Gate figures measured on the new set are not comparable with earlier ones.
The exclusion gate from the previous commit is green: 0 tracked files hit
outside the shared/ subtree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-23 15:04:21 +02:00
commit 37547fe292
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
1147 changed files with 24138 additions and 9503 deletions

View file

@ -7,24 +7,23 @@ in the set is constructed rather than real).
**Five arms, and the split between them is a measurement rather than a taste.** Two are
unconditional and can never be silently absent — a mandate that does not load, and a mandate routed
at a base the set is not for. Three need the base itself, which lives OUTSIDE this repository
(``PORTFOLIO_VEGNORMAL_ROOT``): they SKIP when the root is missing, exactly as MAJOR-3's ceiling
gate could not take K2 as a test dependency, and for the same published-package reason — a hard
failure would break ``uv run pytest`` for any external recipient of the ``git archive HEAD``
handover. The skip NAMES the root it looked for.
at a base the set is not for. Three need the base itself. The two example bases ship WITH the
package (``data/kunnskapsbaser/``, pinned in ``frozen_bundles.json``), so those arms run in every
checkout and every installed wheel; they SKIP only when a user has pointed the store elsewhere
(``PORTFOLIO_FROZEN_BUNDLES``) and the pinned copy is not there, and the skip NAMES the store it
looked in.
**Every bundle-reading arm carries its own denominator.** A scan that sees zero concepts is RED
rather than vacuously green: "the anchor was not found" is equally true of a base that was never
read (Verifiseringsloven, ansikt 4).
**Rule U** — the measurable form of "the base cannot answer this" (documented in
``docs/2026-09-12-p14-kontekstsett.md § 2.3``): each ``must_refuse`` row declares >= 1 ``anchor``,
``docs/invarianter.md``): each ``must_refuse`` row declares >= 1 ``anchor``,
a lowercase word of >= 4 characters, and is admitted **iff every anchor is absent — case-insensitive
substring — from the WHOLE text (frontmatter + body) of EVERY concept document in the base**. Not
"shares no keyword with any title": a tunnel question shares "tunnel" with hundreds of titles and
"shares no keyword with any title": a cooling question shares "kjøling" with dozens of titles and
that proves nothing. What makes a question unanswerable is that the base lacks the SUBJECT, and the
anchor is that subject. Titles alone would be a proxy the full text costs nothing more to replace
(measured: 0.77 s for r761-2025, the largest base).
anchor is that subject. Titles alone would be a proxy the full text costs nothing more to replace.
**P16 A2 moved rule U from ``unanswerable`` to ``must_refuse``, and that is ONE form rather than
two.** ``po`` is not a lookup tool (D-1), so an "unanswerable question" had no runnable form: no
@ -56,9 +55,9 @@ _CONTEXT_ROOT = _REPO_ROOT / "contexts"
#: in the set: this repository is published, and an absolute path would pin a set to one machine's
#: home directory and ride out in the handover archive.
#: The concept types the four bases declare. ``index.md`` carries none of them — it is navigation,
#: not content — which is why the file count and the concept count differ.
_CONCEPT_TYPES = {"Krav", "Prosess", "Kapittel", "Normal", "Håndbok"}
#: The concept types the two example bases declare. ``index.md`` carries none of them — it is
#: navigation, not content — which is why the file count and the concept count differ.
_CONCEPT_TYPES = {"Krav", "Prosess", "Standard", "Katalog"}
_MIN_ANCHOR_CHARS = 4
@ -68,29 +67,30 @@ def own_frontmatter(path: Path) -> dict[str, str]:
**P15 (2026-09-13) fixed the finding this helper was written against.** Before P15,
``okf.parse_frontmatter`` was linewise and last-write-wins over EVERY line regardless of
indentation, so a nested block overwrote a top-level key of the same name. Every vegnormal
concept ends its frontmatter with
indentation, so a nested block overwrote a top-level key of the same name. Every concept of a
requirements base ends its frontmatter with
sources:
- resource: https://…
title: N500:2024
title: D200:2027
and the indented ``title`` used to replace the concept's own. MEASURED on n500-2024 before the
fix: ``okf.navigate_bundle`` yielded 270 concept files carrying **1 distinct title**
(``N500:2024``, 270 times). ``okf.parse_frontmatter`` now makes indentation load-bearing —
a top-level (unindented) key always wins over a nested one of the same name — and re-measured
AFTER the fix, the same base's 269 ``krav/N500`` documents carry **269 distinct titles**.
and the indented ``title`` used to replace the concept's own. MEASURED on a delivered
requirements corpus during development, before the fix: ``okf.navigate_bundle`` yielded 270
concept files carrying **1 distinct title** (the sources title, 270 times).
``okf.parse_frontmatter`` now makes indentation load-bearing — a top-level (unindented) key
always wins over a nested one of the same name — and re-measured AFTER the fix, the same base's
269 requirement documents carried **269 distinct titles**.
**This helper still isn't a plain call to ``okf.parse_frontmatter``, and that remains
measured rather than assumed:** ``own_frontmatter`` also strips one layer of enclosing
``'`` quotes (``.strip("'")``) so a value matches the fasit's stored plain-text title
verbatim, while ``okf.parse_frontmatter`` deliberately leaves scalars quoted — unquoting is
``okf.unquote_scalar``'s ONE job (D1/(a)/(i)), and a second copy of that rule here would be
the drifting one. Re-measured across all four bases (29 500 field reads: ``type``, ``title``,
``req_number``, ``prosessnr`` on every concept file) the two now agree EXACTLY except for
quoted scalars (2 728 of 29 500 checks — every one a quote-stripping difference, none a value
difference), so this helper stays for that one reason, not for the nested-override bug P15
closed.
the drifting one. Measured across the corpora of the time (29 500 field reads: ``type``,
``title``, ``req_number``, ``prosessnr`` on every concept file) the two agreed EXACTLY except
for quoted scalars (2 728 of 29 500 checks — every one a quote-stripping difference, none a
value difference), so this helper stays for that one reason, not for the nested-override bug
P15 closed.
Uses ``okf._split_frontmatter`` deliberately: it is the module's ONE place ``---`` is compared
(B4), and a second delimiter rule here would be the copy that drifts.
@ -108,8 +108,8 @@ def own_frontmatter(path: Path) -> dict[str, str]:
def _bundle_dir(name: str) -> Path:
"""The FROZEN copy this repository pins, resolved at call time.
Absence SKIPS (MAJOR-3's ceiling: no corpus is mounted in the handover archive), drift is
allowed to propagate and FAIL — a measurement of the wrong corpus is not a missing one.
Absence SKIPS (a user's own store, named by ``PORTFOLIO_FROZEN_BUNDLES``, may not hold it),
drift is allowed to propagate and FAIL — a measurement of the wrong corpus is not a missing one.
"""
try:
return frozen_bundles.bundle_dir(name)
@ -120,7 +120,7 @@ def _bundle_dir(name: str) -> Path:
#: The ONE reader, imported from production rather than copied here (P17b). It used to be a
#: private copy in this file and a second, looser one inside ``stress.main`` — and the multi-base
#: form is exactly the change that would have let the two drift into different answers about one
#: set. A set declaring ONE base is one block, so the four pre-P17b files parse unchanged.
#: set. A set declaring ONE base is one block.
read_bundle_txt = read_bundle_declarations
@ -197,8 +197,8 @@ def _base_by_approach(set_dir: Path) -> dict[str, Path]:
# --------------------------------------------------------------------------------------------
def test_the_five_context_sets_are_present() -> None:
assert len(_SETS) == 5, f"expected five context sets under {_CONTEXT_ROOT}, found {_SET_IDS}"
def test_the_three_context_sets_are_present() -> None:
assert len(_SETS) == 3, f"expected three context sets under {_CONTEXT_ROOT}, found {_SET_IDS}"
# --------------------------------------------------------------------------------------------
@ -268,8 +268,8 @@ def test_b_every_fasit_concept_is_in_the_base_as_recorded(set_dir: Path) -> None
Resolving per approach rather than per set is the multi-base half: in a set spanning two
bases, checking every path against one of them would fail half the fasit while proving
nothing about the other, and checking against "either" would let a path meant for N200 be
satisfied by a coincidence in R761.
nothing about the other, and checking against "either" would let a path meant for the
requirements base be satisfied by a coincidence in the process catalogue.
"""
bases = _base_by_approach(set_dir)
fasit = json.loads((set_dir / "fasit.json").read_text(encoding="utf-8"))
@ -299,10 +299,11 @@ def test_c_rule_u_every_unanswerable_question_is_unanswerable(set_dir: Path) ->
"""Rule U over EVERY base the set declares, as ONE scan.
For a multi-base set "the base cannot answer this" becomes "NEITHER base can", and the union
is the honest reading: an anchor absent from N200 but present in R761 is a question the pass
as a whole CAN reach. MEASURED 15.09 and the reason this is not a formality — ``enhetspris``
is absent from n200-2024 and carried by 70 of r761-2025's 2 756 concepts, so an anchor set
admitted per base would have admitted a question the pass could ground.
is the honest reading: an anchor absent from the requirements base but present in the process
catalogue is a question the pass as a whole CAN reach. MEASURED on the example bases, and the
reason this is not a formality — ``enhetspris`` is absent from driftskrav-2027 and carried by 57
of prosesskatalog-2027's 301 concepts, so an anchor set admitted per base would have admitted a
question the pass could ground.
"""
declared = read_bundle_txt(set_dir / "bundle.txt")
concepts: list[tuple[str, dict[str, str], str]] = []
@ -359,18 +360,18 @@ _GOOD_MANDATE = {
"label": "One",
"affected_codes": ["X-1"],
"claimed_saving_nok": 1.0,
"bundle_id": "vegnormal-n500-2024",
"bundle_id": "eksempel-driftskrav-2027",
},
{
"id": "a2",
"label": "Two",
"affected_codes": ["X-2"],
"claimed_saving_nok": 2.0,
"bundle_id": "vegnormal-n500-2024",
"bundle_id": "eksempel-driftskrav-2027",
},
],
}
_GOOD_BUNDLE_TXT = "name: n500-2024\nbundle_id: vegnormal-n500-2024\n"
_GOOD_BUNDLE_TXT = "name: driftskrav-2027\nbundle_id: eksempel-driftskrav-2027\n"
def test_known_positive_a_a_malformed_mandate_is_refused(tmp_path: Path) -> None:
@ -386,7 +387,7 @@ def test_known_positive_a_a_malformed_mandate_is_refused(tmp_path: Path) -> None
def test_known_positive_d_a_mandate_routed_at_another_base_is_caught(tmp_path: Path) -> None:
broken = json.loads(json.dumps(_GOOD_MANDATE))
broken["approaches"][1]["bundle_id"] = "vegnormal-n100-2023"
broken["approaches"][1]["bundle_id"] = "eksempel-prosesskatalog-2027"
set_dir = _broken_set(tmp_path, mandate=broken, bundle=_GOOD_BUNDLE_TXT, fasit={})
declared = read_bundle_txt(set_dir / "bundle.txt")
ids = {block["bundle_id"] for block in declared}
@ -394,11 +395,11 @@ def test_known_positive_d_a_mandate_routed_at_another_base_is_caught(tmp_path: P
# The SAME two set relations arm (d) asserts, and the broken set must fail the first of them:
# an approach routed at a base the set does not declare.
assert not routed <= ids
assert sorted(routed - ids) == ["vegnormal-n100-2023"]
assert sorted(routed - ids) == ["eksempel-prosesskatalog-2027"]
def test_known_positive_c_an_anchor_the_base_carries_is_reported() -> None:
concepts = [("a.md", {"type": "Krav"}, "en tunnel med ventilasjon og belysning")]
concepts = [("a.md", {"type": "Krav"}, "et serverrom med ventilasjon og belysning")]
assert anchors_are_absent(["enhetspris"], concepts) == []
assert anchors_are_absent(["ventilasjon"], concepts) == ["ventilasjon"]
@ -411,7 +412,7 @@ def test_known_positive_c_an_empty_scan_is_refused_never_vacuously_absent() -> N
def test_known_positive_c_an_unusable_anchor_is_refused() -> None:
concepts = [("a.md", {"type": "Krav"}, "tekst")]
with pytest.raises(ValueError, match="at least"):
anchors_are_absent(["vei"], concepts)
anchors_are_absent(["rom"], concepts)
with pytest.raises(ValueError, match="lowercase"):
anchors_are_absent(["Enhetspris"], concepts)
with pytest.raises(ValueError, match="no anchors"):
@ -432,7 +433,7 @@ def test_known_positive_b_a_fasit_path_the_base_does_not_carry_is_caught(tmp_pat
def test_known_positive_bundle_txt_must_declare_both_keys(tmp_path: Path) -> None:
path = tmp_path / "bundle.txt"
path.write_text("name: n500-2024\n", encoding="utf-8")
path.write_text("name: driftskrav-2027\n", encoding="utf-8")
with pytest.raises(ValueError, match="bundle_id"):
read_bundle_txt(path)
@ -446,7 +447,8 @@ def test_known_positive_a_second_block_needs_its_own_bundle_id(tmp_path: Path) -
"""
path = tmp_path / "bundle.txt"
path.write_text(
"name: n200-2024\nbundle_id: vegnormal-n200-2024\nname: r761-2025\n", encoding="utf-8"
"name: driftskrav-2027\nbundle_id: eksempel-driftskrav-2027\nname: prosesskatalog-2027\n",
encoding="utf-8",
)
with pytest.raises(ValueError, match="bundle_id"):
read_bundle_txt(path)
@ -455,13 +457,13 @@ def test_known_positive_a_second_block_needs_its_own_bundle_id(tmp_path: Path) -
def test_a_multi_base_bundle_txt_parses_into_one_block_per_base(tmp_path: Path) -> None:
path = tmp_path / "bundle.txt"
path.write_text(
"name: n200-2024\nbundle_id: vegnormal-n200-2024\n"
"name: r761-2025\nbundle_id: vegnormal-r761-2025\n",
"name: driftskrav-2027\nbundle_id: eksempel-driftskrav-2027\n"
"name: prosesskatalog-2027\nbundle_id: eksempel-prosesskatalog-2027\n",
encoding="utf-8",
)
assert read_bundle_txt(path) == (
{"name": "n200-2024", "bundle_id": "vegnormal-n200-2024"},
{"name": "r761-2025", "bundle_id": "vegnormal-r761-2025"},
{"name": "driftskrav-2027", "bundle_id": "eksempel-driftskrav-2027"},
{"name": "prosesskatalog-2027", "bundle_id": "eksempel-prosesskatalog-2027"},
)
@ -499,12 +501,12 @@ def test_the_fasit_titles_are_distinct_not_the_collapsed_sources_title(set_dir:
# (f) + (g) + (h): the set is ANCHORED (P21 B2).
#
# The measured reason these exist. Four paid rounds ran entirely un-anchored, because the only
# file loader reads ``cost-baseline.json`` out of the BUNDLE and no road normal carries prices — a
# vegnormal is knowledge, the price belongs to the PROJECT. With ``--cost-baseline`` the project
# supplies its own schedule, so the validator's stage 0 judges again: (f) every answerable approach
# has a line to reconcile against, and (g) the falsification arm has NONE, so the code it proposes
# is refused as "not in the project's cost baseline" — by stage 0, the one stage that can tell an
# invented line from a real one, instead of by the weaker downstream gates.
# file loader reads ``cost-baseline.json`` out of the BUNDLE and no requirements base carries
# prices — a requirement is knowledge, the price belongs to the PROJECT. With ``--cost-baseline``
# the project supplies its own schedule, so the validator's stage 0 judges again: (f) every
# answerable approach has a line to reconcile against, and (g) the falsification arm has NONE, so
# the code it proposes is refused as "not in the project's cost baseline" — by stage 0, the one
# stage that can tell an invented line from a real one, instead of by the weaker downstream gates.
#
# (f) and (g) are SEPARATE arms rather than one loop over all approaches, because they are opposite
# claims about opposite rows: a single arm asserting "exactly the non-refuse codes are present"
@ -580,14 +582,14 @@ def test_h_no_cost_line_smuggles_in_an_uncommissioned_requirement_number(set_dir
"""No line of the schedule is a reference number the base declares AND nobody commissions.
**The order words this arm as "no baseline code is a requirement number the base declares", and
that rule was FELLED BY MEASUREMENT before anything was built on it.** Measured 15.09 against
``okf.declared_reference_numbers`` over the four mounted bases: the four project-coded sets
carry 0 such codes, and ``kontrakt-sorasen-2027`` carries FIVE of five — ``12.1``, ``12.12``,
``22.1``, ``52.11``, ``51.1`` are real R761 ``prosessnr``. That is not an accident in the set;
it is what R761 Prosesskoden IS. A Norwegian road contract's bill of quantities is priced BY
process code, so the project's schedule and the corpus's vocabulary share an identifier
namespace by design — and the order's rule would have forced a rewrite of the ONE set P20's
decision (e) was chosen to preserve.
that rule was FELLED BY MEASUREMENT before anything was built on it.** Measured against
``okf.declared_reference_numbers``: the project-coded sets carry 0 such codes, and the set
priced by process number (today ``driftsavtale-2027``) carries FIVE of five — ``12.1``,
``12.12``, ``22.1``, ``52.11``, ``51.1`` are declared ``prosessnr`` of its catalogue. That is
not an accident in the set; it is what a process catalogue IS. An agreement settled process by
process is priced BY process number, so the project's schedule and the corpus's vocabulary
share an identifier namespace by design — and the order's rule would have forced a rewrite of
the ONE kind of set P20's decision (e) was chosen to preserve.
The COMPLEMENT keeps both: a schedule may price what the commission names, and may not
INTRODUCE a corpus identifier as a cost line nobody ordered. The order's own mutation still
@ -622,28 +624,27 @@ def test_h_no_cost_line_smuggles_in_an_uncommissioned_requirement_number(set_dir
def test_known_positive_f_a_missing_cost_line_is_caught(tmp_path: Path) -> None:
baseline = _set_baseline(_CONTEXT_ROOT / "gate-nordvik-2027")
assert "GATE-KRYSS-01" in baseline.items
baseline = _set_baseline(_CONTEXT_ROOT / "serverrom-2027")
assert "SRV-KJOL-01" in baseline.items
stripped = CostBaseline(
project_id=baseline.project_id,
items={k: v for k, v in baseline.items.items() if k != "GATE-KRYSS-01"},
items={k: v for k, v in baseline.items.items() if k != "SRV-KJOL-01"},
)
assert "GATE-KRYSS-01" not in stripped.items
assert "SRV-KJOL-01" not in stripped.items
def test_known_positive_g_a_line_for_the_falsification_arm_is_caught() -> None:
"""The order's mutation (i): give a4 a line, and (g)'s assertion must fail on this set."""
baseline = _set_baseline(_CONTEXT_ROOT / "gate-nordvik-2027")
baseline = _set_baseline(_CONTEXT_ROOT / "serverrom-2027")
priced = dict(baseline.items)
priced["GATE-GANG-ENHET"] = CostBaselineLine(quantity=6, unit_cost=50_000.0)
fasit = json.loads((_CONTEXT_ROOT / "gate-nordvik-2027" / "fasit.json").read_text("utf-8"))
priced["SRV-RACK-ENHET"] = CostBaselineLine(quantity=6, unit_cost=50_000.0)
fasit = json.loads((_CONTEXT_ROOT / "serverrom-2027" / "fasit.json").read_text("utf-8"))
by_id = {
a.id: a
for a in load_mandate(_CONTEXT_ROOT / "gate-nordvik-2027" / "mandate.json").approaches
a.id: a for a in load_mandate(_CONTEXT_ROOT / "serverrom-2027" / "mandate.json").approaches
}
for row in fasit["must_refuse"]:
carried = [c for c in by_id[row["approach_id"]].affected_codes if c in priced]
assert carried == ["GATE-GANG-ENHET"]
assert carried == ["SRV-RACK-ENHET"]
def test_known_positive_h_an_uncommissioned_requirement_number_is_caught() -> None: