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:
parent
058dd25570
commit
37547fe292
1147 changed files with 24138 additions and 9503 deletions
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue