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

@ -0,0 +1,415 @@
"""Build the two fictional example knowledge bases from ``kilde.json`` and pin them.
Usage (from the repository root)::
uv run python examples/syntetiske-kunnskapsbaser/generate.py
Writes ``src/portfolio_optimiser/data/kunnskapsbaser/<name>-<sha12>/`` for each base, removes any
older ``<name>-*`` copy, and rewrites the ``bundles`` entries of
``src/portfolio_optimiser/frozen_bundles.json`` with the new digests. Renewing a base is therefore
ONE command, and the new copy and the new pin land in the same diff.
**Deterministic by construction**: no clock, no randomness, sorted output, and document ids are
``uuid5`` of a fixed namespace and the document's own number. Running it twice gives byte-identical
bases and the same pins — ``--check`` says whether the tracked copies are what the source builds.
**Why generated rather than built with ``okf build``**: the code consumes per-document identifier
frontmatter (``req_number``, ``prosessnr``, ``seksjon``), and ``okf build`` takes frontmatter only
as ONE ``--frontmatter KEY=VALUE`` applied to every concept. The OKF layout itself — a root
``index.md`` declaring ``okf_version`` and ``bundle_id``, a linked ``index.md`` per level, one
concept document per file with a block ``sources`` sequence — is written here directly.
Everything the bases say is invented: Eksempelvirksomheten, its requirements and its process
catalogue describe no real organisation, and no text is taken from a real standard.
"""
from __future__ import annotations
import argparse
import json
import shutil
import sys
import tempfile
import uuid
from pathlib import Path
from typing import Any
HERE = Path(__file__).resolve().parent
REPO = HERE.parents[1]
SOURCE = HERE / "kilde.json"
TARGET = REPO / "src" / "portfolio_optimiser" / "data" / "kunnskapsbaser"
PIN_FILE = REPO / "src" / "portfolio_optimiser" / "frozen_bundles.json"
_NAMESPACE = uuid.uuid5(uuid.NAMESPACE_URL, "https://example.invalid/eksempelvirksomheten")
_DESCRIPTION_MAX = 160
def _uid(*parts: str) -> str:
return str(uuid.uuid5(_NAMESPACE, "/".join(parts)))
def _quoted(value: str) -> str:
"""A number-shaped scalar is quoted, as a YAML dumper would, so it stays a string."""
return f"'{value}'"
def _float_like(value: str) -> bool:
head, dot, tail = value.partition(".")
return head.isdigit() and (not dot or (tail.isdigit() and "." not in tail))
def _capitalised(text: str) -> str:
return text[:1].upper() + text[1:]
def _short(text: str) -> str:
if len(text) <= _DESCRIPTION_MAX:
return text
return text[: _DESCRIPTION_MAX - 1].rstrip() + "…"
def _frontmatter(pairs: list[tuple[str, str]], source: tuple[str, str]) -> str:
lines = ["---", *(f"{key}: {value}" for key, value in pairs)]
lines += ["sources:", f" - resource: {source[0]}", f" title: {source[1]}", "---"]
return "\n".join(lines) + "\n"
def _write(root: Path, rel: str, text: str) -> None:
path = root / rel
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8", newline="\n")
def _sort_key(number: str) -> tuple[int, ...]:
"""Natural order of a section number such as ``4.2.5.1``."""
return tuple(int(part) for part in number.split("."))
# ------------------------------------------------------------------------------ the requirements
def _requirements(part: dict[str, Any], spec: dict[str, Any]) -> list[dict[str, str]]:
"""Every requirement of one part: explicit ones verbatim, the rest from the aspect templates."""
aspects = spec["aspects"]
params = spec["parameters"]
out: list[dict[str, str]] = []
for index, section in enumerate(part["sections"]):
if "krav" in section:
for suffix, text in section["krav"]:
out.append(
{
"section": section["nr"],
"section_title": section["title"],
"suffix": suffix,
"text": text,
"guidance": section["veiledning"],
}
)
continue
subject = section["subject"]
for n in range(1, int(section["count"]) + 1):
aspect = aspects[(index + n - 1) % len(aspects)]
values = {
"S": _capitalised(subject),
"s": subject,
**{
key: str(options[(index + n) % len(options)]) for key, options in params.items()
},
}
out.append(
{
"section": section["nr"],
"section_title": section["title"],
"suffix": str(n),
"text": aspect["krav"].format(**values),
"guidance": aspect["veiledning"].format(**values),
}
)
return out
def build_requirements(spec: dict[str, Any], stamp: str, honesty: str, root: Path) -> None:
source_root = spec["source_root"]
level_lines: list[str] = []
standard_lines: list[str] = []
total = 0
for part in spec["parts"]:
pid = part["id"]
edition = f"{pid}:2027"
source = (f"{source_root}/{pid.lower()}", edition)
entries: list[tuple[tuple[int, ...], str, str]] = []
for req in _requirements(part, spec):
number = f"Krav {req['section']}—{req['suffix']}"
title = f"{number} {req['section_title']}"
doc_id = f"id-{_uid('driftskrav', pid, number)}"
section = req["section"]
text = req["text"]
pairs = [
("type", "Krav"),
("title", title),
("description", _short(text)),
("kravtype", "bør" if " bør " in f" {text} " else "skal"),
("standard", pid),
("utgave", edition),
("req_number", number),
("status", "stable"),
("trust_tier", "unverified"),
("seksjon", _quoted(section) if _float_like(section) else section),
("seksjonstittel", req["section_title"]),
("ingested_at", stamp),
("source_element_id", doc_id),
]
body = f"\n## Krav\n\n{text}\n\n## Veiledning (ikke-normativ)\n\n{req['guidance']}\n"
_write(root, f"krav/{pid}/{doc_id}.md", _frontmatter(pairs, source) + body)
entries.append(
(_sort_key(section), req["suffix"], f"- [{title}]({doc_id}.md) — {_short(text)}")
)
entries.sort(key=lambda e: (e[0], [int(p) for p in e[1].split("_")]))
_write(root, f"krav/{pid}/index.md", "# Krav\n\n" + "\n".join(e[2] for e in entries) + "\n")
level_lines.append(
f"- [{pid}]({pid}/index.md) — {len(entries)} konsepter under krav/{pid}."
)
total += len(entries)
pairs = [
("type", "Standard"),
("title", edition),
("description", f"Driftskrav del {pid[1:]}: {part['title']}"),
("standard", pid),
("utgave", edition),
("status", "stable"),
("trust_tier", "unverified"),
("krav_i_bundlen", str(len(entries))),
("ingested_at", stamp),
]
body = (
f"\n## Om delen\n\n{part['scope']} Delen bærer {len(entries)} krav, hvert som eget "
f"konsept under `krav/{pid}/`.\n\n## Opphav\n\n{honesty}\n"
)
_write(root, f"standard/{pid}.md", _frontmatter(pairs, source) + body)
standard_lines.append(
f"- [{edition}]({pid}.md) — Driftskrav del {pid[1:]}: {part['title']}"
)
_write(root, "krav/index.md", "# Kataloger\n\n" + "\n".join(level_lines) + "\n")
_write(root, "standard/index.md", "# Standard\n\n" + "\n".join(standard_lines) + "\n")
_write(
root,
"index.md",
f"---\nokf_version: 0.2\nbundle_id: {spec['bundle_id']}\n---\n\n# Kataloger\n\n"
f"- [krav](krav/index.md) — {total} konsepter under krav.\n"
f"- [standard](standard/index.md) — {len(spec['parts'])} konsepter under standard.\n",
)
# ------------------------------------------------------------------------- the process catalogue
def _processes(spec: dict[str, Any]) -> list[dict[str, Any]]:
"""The catalogue as a flat list, parents before children, each with its number and relations."""
out: list[dict[str, Any]] = []
first = int(spec["default_subs_for_first"])
for main in spec["main"]:
main_node = {
"nr": main["nr"],
"title": f"Hovedprosess {main['nr']} {main['title']}",
"link": f"Hovedprosess {main['nr']} {main['title']}",
"level": 1,
"main": main["nr"],
"parents": [],
"omfang": None,
"seksjon": f"Hovedprosess {main['nr']}",
"seksjonstittel": main["title"],
}
out.append(main_node)
for group in main["groups"]:
group_node = {
"nr": group["nr"],
"title": group["title"],
"link": f"{group['nr']} {group['title']}",
"level": 2,
"main": main["nr"],
"parents": [main_node],
"omfang": None,
}
out.append(group_node)
for i, proc in enumerate(group["processes"], start=1):
obj = proc.get("obj", proc["title"][:1].lower() + proc["title"][1:])
nr = f"{group['nr']}.{i}"
node = {
"nr": nr,
"title": proc["title"],
"link": f"{nr} {proc['title']}",
"level": 3,
"main": main["nr"],
"parents": [group_node, main_node],
"omfang": proc.get("omfang", spec["default_omfang"].format(obj=obj)),
}
out.append(node)
subs = proc.get("subs")
if subs is None:
subs = spec["default_subs"] if i <= first else []
for j, sub in enumerate(subs, start=1):
sub_nr = f"{nr}{j}"
title = sub["title"].format(obj=obj)
out.append(
{
"nr": sub_nr,
"title": title,
"link": f"{sub_nr} {title}",
"level": 4,
"main": main["nr"],
"parents": [node, group_node, main_node],
"omfang": sub["omfang"].format(obj=obj),
}
)
return out
def build_catalogue(spec: dict[str, Any], stamp: str, honesty: str, root: Path) -> None:
cat = spec["catalogue"]
source = (f"{spec['source_root']}/{cat.lower()}", spec["edition"])
nodes = _processes(spec)
for node in nodes:
node["dir"] = node["nr"].replace(".", "-")
node["file"] = f"_{node['main']}_id-{_uid('prosesskatalog', node['nr'])}.md"
node["children"] = []
by_nr = {node["nr"]: node for node in nodes}
for node in nodes:
if node["parents"]:
by_nr[node["parents"][0]["nr"]]["children"].append(node)
def rel(node: dict[str, Any]) -> str:
return f"../{node['dir']}/{node['file']}"
for node in nodes:
description = node["omfang"].split(". ")[0].rstrip(".") + "." if node["omfang"] else ""
summary = _short(description or node["title"])
pairs = [
("type", "Prosess"),
("title", node["title"]),
("description", summary),
("standard", spec["catalogue_title"]),
("utgave", spec["edition"]),
("prosessnr", _quoted(node["nr"])),
("hovedprosess", _quoted(node["main"])),
]
if node["parents"]:
pairs.append(("forelder", _quoted(node["parents"][0]["nr"])))
pairs += [
("nivaa", str(node["level"])),
("status", "stable"),
("trust_tier", "unverified"),
("seksjon", node.get("seksjon", _quoted(node["nr"]))),
("seksjonstittel", node.get("seksjonstittel", node["title"])),
("ingested_at", stamp),
("source_element_id", node["file"][:-3]),
]
body = ""
if node["omfang"]:
body += f"\n## a) Omfang\n\n{node['omfang']}\n"
body += "\n## Relasjoner\n"
if node["parents"]:
body += "\n### Overordnede prosesser\n\n"
body += "\n".join(f"- [{p['link']}]({rel(p)})" for p in node["parents"]) + "\n"
if node["children"]:
body += "\n### Underprosesser\n\n"
body += "\n".join(f"- [{c['link']}]({rel(c)})" for c in node["children"]) + "\n"
_write(root, f"{cat}/{node['dir']}/{node['file']}", _frontmatter(pairs, source) + body)
_write(
root,
f"{cat}/{node['dir']}/index.md",
f"# Prosess\n\n- [{node['title']}]({node['file']}) — {summary}\n",
)
dirs = sorted(node["dir"] for node in nodes)
_write(
root,
f"{cat}/index.md",
"# Kataloger\n\n"
+ "\n".join(f"- [{d}]({d}/index.md) — 1 konsept under {cat}/{d}." for d in dirs)
+ "\n",
)
mains = [node for node in nodes if node["level"] == 1]
pairs = [
("type", "Katalog"),
("title", spec["edition"]),
("description", spec["edition"]),
("standard", spec["catalogue_title"]),
("utgave", spec["edition"]),
("status", "stable"),
("trust_tier", "unverified"),
("prosesser_i_bundlen", str(len(nodes))),
("ingested_at", stamp),
]
body = (
f"\n## Dekning\n\n{spec['scope']} Bundlen bærer {len(nodes)} prosesser, hver som eget "
f"konsept under `{cat}/<prosessnummer>/`.\n\n## Opphav\n\n{honesty}\n\n"
"## Hovedprosesser\n\n"
+ "\n".join(f"- [{m['title']}]({cat}/{m['dir']}/{m['file']})" for m in mains)
+ "\n"
)
_write(root, f"{cat}.md", _frontmatter(pairs, source) + body)
_write(
root,
"index.md",
f"---\nokf_version: 0.2\nbundle_id: {spec['bundle_id']}\n---\n\n# Kataloger\n\n"
f"- [{cat}]({cat}/index.md) — {len(nodes)} konsepter under {cat}.\n\n# Katalog\n\n"
f"- [{spec['edition']}]({cat}.md) — {spec['edition']}\n",
)
# ------------------------------------------------------------------------------------ the pins
def _digest(root: Path) -> tuple[str, int]:
sys.path.insert(0, str(REPO / "src"))
from portfolio_optimiser.frozen_bundles import digest_bundle
return digest_bundle(root)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
parser.add_argument(
"--check",
action="store_true",
help="build into a temporary directory and compare with the pins; write nothing",
)
args = parser.parse_args(argv)
source = json.loads(SOURCE.read_text(encoding="utf-8"))
stamp, honesty = source["ingested_at"], source["honesty"]
builders = (
(source["driftskrav"], build_requirements),
(source["prosesskatalog"], build_catalogue),
)
pins = json.loads(PIN_FILE.read_text(encoding="utf-8"))
built: dict[str, dict[str, Any]] = {}
drift = 0
with tempfile.TemporaryDirectory() as tmp:
for spec, build in builders:
staged = Path(tmp) / spec["name"]
build(spec, stamp, honesty, staged)
sha, files = _digest(staged)
directory = f"{spec['name']}-{sha[:12]}"
pinned = pins["bundles"].get(spec["name"], {})
same = pinned.get("sha256") == sha and (TARGET / directory).is_dir()
print(f"{spec['name']}: {directory} ({files} files){'' if same else ' CHANGED'}")
if args.check:
drift += 0 if same else 1
continue
for old in TARGET.glob(f"{spec['name']}-*"):
shutil.rmtree(old)
TARGET.mkdir(parents=True, exist_ok=True)
shutil.copytree(staged, TARGET / directory)
built[spec["name"]] = {"directory": directory, "sha256": sha, "files": files}
if args.check:
return 1 if drift else 0
pins["bundles"] = built
PIN_FILE.write_text(json.dumps(pins, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())