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
415
examples/syntetiske-kunnskapsbaser/generate.py
Normal file
415
examples/syntetiske-kunnskapsbaser/generate.py
Normal 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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue