feat(fase6): gate-promote approved verdicts back into the OKF wiki (Steg 8)
Close the last agentic-loop seam (målbilde §3/§6/§7/§11 step 6): an APPROVED verdict is promoted from the raw output layer into the context layer (the OKF bundle) as a navigable `type: verdict` concept file, so human/persona-approved knowledge reaches the next run's hypothesis. - okf.py (pure stdlib, MAF-free): render_frontmatter / write_concept_file / link_in_index — the D7-portable OKF write counterpart of navigate. - verdicts.py: promote_verdict + PromotionRefused gate (fail-closed; only approved decisions enter the wiki, never raw agent output), provenance stamp (who/experiment/when; timestamp a required kwarg), neutral index label (signal reaches a prompt only via the gated ExpeL fold, never bundle_context), _safe_filename_token (id sanitised for path/link). - R4 = optional+gated: a public opt-in primitive, NOT wired into run_project (mirrors write_verdict — the system reads, the gate promotes). - Load-bearing trio (test_step8_promotion_loadbearing.py): gate refuses a non-approved verdict, approved verdict is navigable, promoted signal stays out of the read-context — all proven RED-on-detach. Suite 144->148. Design hardened by an adversarial plan-critic (12 findings; the BLOCKER — index-link leak into bundle_context via index_summary — closed by the neutral label + a no-leak test). Honesty limits documented: promoted file is minimal (signal as prose only), and the learning-key id means same-candidate approvals share a filename (last-write-wins). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
This commit is contained in:
parent
e2861cac0c
commit
6b645ad32a
6 changed files with 385 additions and 2 deletions
|
|
@ -97,6 +97,71 @@ def test_parse_frontmatter_reads_scalar_fields() -> None:
|
|||
assert fm["decision"] == "approved_with_adjustment"
|
||||
|
||||
|
||||
def test_render_frontmatter_roundtrips_consumed_fields(tmp_path) -> None:
|
||||
"""Step-8 writer: ``render_frontmatter`` + ``write_concept_file`` emit a block that
|
||||
``parse_frontmatter`` re-reads with the fields ``seed_store_from_bundle`` consumes
|
||||
(``type``, ``decision``, ``description``) intact. NOT a bijection — only these scalar fields
|
||||
are guaranteed to survive write -> read."""
|
||||
fm = {
|
||||
"type": "verdict",
|
||||
"decision": "approved",
|
||||
"description": "LED-retrofit godkjent (realiseringsgrad=0.57)",
|
||||
"verdict_id": "abc123",
|
||||
}
|
||||
okf.write_concept_file(str(tmp_path), "promoted-verdict-abc123.md", fm, "body prose\n")
|
||||
parsed = okf.parse_frontmatter(tmp_path / "promoted-verdict-abc123.md")
|
||||
assert parsed["type"] == "verdict"
|
||||
assert parsed["decision"] == "approved"
|
||||
assert parsed["description"] == "LED-retrofit godkjent (realiseringsgrad=0.57)"
|
||||
assert parsed["verdict_id"] == "abc123"
|
||||
|
||||
|
||||
def test_render_frontmatter_single_lines_scalars(tmp_path) -> None:
|
||||
"""A multi-line rationale must NOT corrupt the line-oriented frontmatter block (parse stops at
|
||||
``---``). Newlines in a scalar value are flattened to spaces, so every following key survives."""
|
||||
fm = {
|
||||
"type": "verdict",
|
||||
"description": "line one\nline two\n---\nnot a delimiter",
|
||||
"decision": "approved",
|
||||
}
|
||||
okf.write_concept_file(str(tmp_path), "f.md", fm, "body\n")
|
||||
parsed = okf.parse_frontmatter(tmp_path / "f.md")
|
||||
assert "\n" not in parsed["description"]
|
||||
assert parsed["decision"] == "approved" # the trailing key was NOT lost to a spurious ---
|
||||
|
||||
|
||||
def _minimal_bundle(tmp_path) -> str:
|
||||
(tmp_path / "index.md").write_text(
|
||||
"---\ntype: index\n---\n\n# Bundle\n\n- [proj](bygg.md)\n", encoding="utf-8"
|
||||
)
|
||||
(tmp_path / "bygg.md").write_text("---\ntype: project\n---\n\nbody\n", encoding="utf-8")
|
||||
return str(tmp_path)
|
||||
|
||||
|
||||
def test_link_in_index_makes_concept_file_navigable(tmp_path) -> None:
|
||||
"""Step-8 writer: a file written into a bundle is reachable by ``navigate_bundle`` only after
|
||||
``link_in_index`` adds an intra-bundle cross-link the navigator follows (``_LINK_RE``)."""
|
||||
bundle_dir = _minimal_bundle(tmp_path)
|
||||
fm = {"type": "verdict", "decision": "approved", "description": "d"}
|
||||
okf.write_concept_file(bundle_dir, "promoted-verdict-x.md", fm, "body\n")
|
||||
assert "promoted-verdict-x.md" not in {f.name for f in okf.navigate_bundle(bundle_dir).files}
|
||||
added = okf.link_in_index(bundle_dir, "promoted-verdict-x.md", "Promotert")
|
||||
assert added is True
|
||||
bundle = okf.navigate_bundle(bundle_dir)
|
||||
assert "promoted-verdict-x.md" in {f.name for f in bundle.files}
|
||||
assert [f.name for f in bundle.verdicts] == ["promoted-verdict-x.md"]
|
||||
|
||||
|
||||
def test_link_in_index_is_idempotent(tmp_path) -> None:
|
||||
"""Linking the same target twice adds exactly one bullet (returns ``False`` the second time) —
|
||||
so re-promoting an existing verdict does not double-link the index."""
|
||||
bundle_dir = _minimal_bundle(tmp_path)
|
||||
assert okf.link_in_index(bundle_dir, "promoted-verdict-x.md", "A") is True
|
||||
assert okf.link_in_index(bundle_dir, "promoted-verdict-x.md", "B") is False
|
||||
body = (tmp_path / "index.md").read_text(encoding="utf-8")
|
||||
assert body.count("(promoted-verdict-x.md)") == 1
|
||||
|
||||
|
||||
def test_okf_is_maf_free() -> None:
|
||||
"""D7 portability: ``okf.py`` IMPORTS no ``agent_framework`` / ``mcp`` (the docstring may name
|
||||
them to document the constraint, exactly as ``retrieval.py`` does) — checked via the AST, not a
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue