Full name-preserving port of src/rune/release_notes.py to src/go/release-notes (GO verdict, #2479 dichotomy tranche — 16/16 fns, dichotomy audit now FULL): - ReleaseNote record: __init__ sort-never-dedup semantics, to_dict insertion-order, from_dict; unicode codepoint sorts byte-parity - render_markdown: short-SHA [:12] clamping (rune-safe pySlice), empty section omission, backfill/provenance footers, unicode rstrip - extract_evidence_facts: closed-contract references + correctly_predicted proven, maintenance resolved/open, TUI projection decisions, release_evidence final_deployed/deployed_identity variants, sorted-set dedup; nested _clean echo for the §24F unsupported-claim filter - build_note: extras merged per key through the SAME exclude filter - summary_from_body: Python splitlines() full separator set (\v \f \x1c-\x1e \x85 \u2028 \u2029), H1-skip, heading/italic/item breaks, blank-gap stop, em-dash fallback - sha_from_body: (?i) tag/deployed markup regexes - Gitea surface: _api (HTTPError-before-OSError order, JSON/"_raw"/"_error" degrades with [:200] truncation), fetch_release, publish_release (create/update/idempotent, ordered POST payload, failed-dict shape) — HTTP behind the injectable Transport seam with request capture - converged_deployed_sha + os_readlink behind the FileSystem seam (RUNE_DEPLOY_ROOT default preserved) - auto_initial_note / convergence_refresh / build_from_deployment: store-missing/corrupt degrades, provenance defaults, tag-SHA-never- changes convergence truth - truth.json: 78 frozen rows from the live oracle (scripts/ gen_release_notes_truth.py) covering EVERY public function and branch — incl. scripted-transport request capture (method/url/headers/body byte-parity), 200+ char truncation, non-ascii escapes; replay 78/78 exact-equal, live-regenerated and re-verified; go test green; gofmt+vet clean - CI: release-notes gate upgraded to the HARD FULL-port parity gate (replay + TestTruthParity) - release_notes.py untouched (Python oracle remains the parity authority) Contract: BUILDER-2026-10-11T15:00:35Z-e67406-factory-wave (draft; phase walk + closure with both SHAs in the Phase B commit). Dichotomy audit: release_notes 16/16 → FULL.
599 lines
25 KiB
Python
599 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
"""gen_release_notes_truth.py — freeze oracle truth for the FULL Go port of
|
|
src/rune/release_notes.py (dichotomy wave #2479).
|
|
|
|
Every public function, every branch, run against the LIVE Python oracle
|
|
(RUNE_*_CORE=python pinned per the poisoned-truth law). The Gitea HTTP
|
|
surface is exercised through a scripted fake urllib transport that records
|
|
(method, url, headers, body) for each call — the Go replay rebuilds the same
|
|
requests byte-for-byte and replays the same scripted responses.
|
|
|
|
Usage: uv run python scripts/gen_release_notes_truth.py [--out PATH]
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import io
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT / "src"))
|
|
os.environ.setdefault("RUNE_CAPABILITY_CORE", "python")
|
|
|
|
from rune.release_notes import ( # noqa: E402
|
|
ReleaseNote,
|
|
_api,
|
|
auto_initial_note,
|
|
build_from_deployment,
|
|
build_note,
|
|
convergence_refresh,
|
|
converged_deployed_sha,
|
|
extract_evidence_facts,
|
|
fetch_release,
|
|
publish_release,
|
|
render_markdown,
|
|
sha_from_body,
|
|
summary_from_body,
|
|
)
|
|
|
|
ROWS: list[dict] = []
|
|
|
|
|
|
def row(name: str, **kw) -> None:
|
|
kw["name"] = name
|
|
ROWS.append(kw)
|
|
|
|
|
|
# ── scripted fake urllib transport ───────────────────────────────────────────
|
|
|
|
|
|
class FakeResp(io.BytesIO):
|
|
status = 200
|
|
|
|
def __init__(self, status: int, body: bytes):
|
|
super().__init__(body)
|
|
self.status = status
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *a):
|
|
return False
|
|
|
|
|
|
class FakeHTTPError(urllib.error.HTTPError):
|
|
def __init__(self, code: int, body: bytes):
|
|
super().__init__("http://fake", code, "error", None, io.BytesIO(body))
|
|
|
|
|
|
def script_transport(calls: list, script: list[dict]):
|
|
"""Monkeypatch urllib.request.urlopen with a recorder + scripted replies."""
|
|
real = urllib.request.urlopen
|
|
|
|
def fake(req, timeout=None):
|
|
spec = script.pop(0)
|
|
calls.append(
|
|
{
|
|
"method": req.get_method(),
|
|
"url": req.full_url,
|
|
"headers": dict(req.headers),
|
|
"body": req.data.decode() if req.data is not None else None,
|
|
}
|
|
)
|
|
kind = spec["kind"]
|
|
if kind == "ok":
|
|
return FakeResp(spec["status"], spec["body"].encode())
|
|
if kind == "http":
|
|
raise FakeHTTPError(spec["status"], spec["body"].encode())
|
|
raise OSError(spec["os_error"])
|
|
|
|
urllib.request.urlopen = fake
|
|
return real
|
|
|
|
|
|
def run_api(script: list[dict], fn) -> dict:
|
|
calls: list = []
|
|
real = script_transport(calls, list(script))
|
|
try:
|
|
out = fn()
|
|
finally:
|
|
urllib.request.urlopen = real
|
|
return {"result": out, "calls": calls}
|
|
|
|
|
|
def want_note(n: ReleaseNote) -> str:
|
|
return json.dumps(n.to_dict())
|
|
|
|
|
|
def resp_ok(status: int, body) -> dict:
|
|
return {"kind": "ok", "status": status, "body": body if isinstance(body, str) else json.dumps(body)}
|
|
|
|
|
|
def resp_http(status: int, body) -> dict:
|
|
return {"kind": "http", "status": status, "body": body if isinstance(body, str) else json.dumps(body)}
|
|
|
|
|
|
def resp_os(msg: str) -> dict:
|
|
return {"kind": "os", "os_error": msg}
|
|
|
|
|
|
# ── fixture helpers ──────────────────────────────────────────────────────────
|
|
|
|
SHA_A = "a" * 40
|
|
SHA_B = "b" * 40
|
|
|
|
|
|
def entity(kind: str, ident: dict, status: str | None = None, ext: dict | None = None) -> dict:
|
|
e = {"id": f"{kind}-1", "kind": kind, "identities": ident}
|
|
if status is not None:
|
|
e["status"] = status
|
|
if ext is not None:
|
|
e["ext"] = ext
|
|
return e
|
|
|
|
|
|
def contract(need: str, quality: dict | None = None, rel_evidence=None, cid: str = "contract-1") -> dict:
|
|
ext: dict = {}
|
|
if quality is not None:
|
|
ext["contract_quality"] = quality
|
|
if rel_evidence is not None:
|
|
ext["release_evidence"] = rel_evidence
|
|
return {"id": cid, "kind": "construction_contract", "status": "closed", "identities": {"need": need}, "ext": ext}
|
|
|
|
|
|
def note_dict(**over) -> dict:
|
|
d = {
|
|
"version": "0.142.0",
|
|
"summary": "evidence-derived release",
|
|
"improved": ["faster recall"],
|
|
"added": ["new gate"],
|
|
"fixed": ["the drift bug"],
|
|
"proven": ["parity 25/25"],
|
|
"limitations": ["known: x"],
|
|
"references": ["issue #2"],
|
|
"release_sha": SHA_A,
|
|
"deployed_sha": SHA_A,
|
|
"provenance": ["contract B-1"],
|
|
"backfill": False,
|
|
}
|
|
d.update(over)
|
|
return d
|
|
|
|
|
|
# ── ReleaseNote record: __init__ sorting/defaults, to_dict, from_dict ────────
|
|
|
|
row(
|
|
"init-unsorted-dedup",
|
|
op="init",
|
|
args={"version": "1.2.3", "summary": "s", "improved": ["z", "a", "z", "m"], "added": None, "fixed": ["f"],
|
|
"proven": [], "limitations": ["l2", "l1"], "references": [], "release_sha": "abc", "deployed_sha": ""},
|
|
)
|
|
row("init-all-defaults", op="init", args={"version": "0.0.1", "summary": "minimal"})
|
|
row("init-unicode-sort", op="init", args={"version": "2.0.0", "summary": "s", "improved": ["émigré", "apple", "Zebra", "zebra", "Änderung"]})
|
|
row(
|
|
"roundtrip-full",
|
|
op="roundtrip",
|
|
args=note_dict(backfill=True, provenance=["p2", "p1"]),
|
|
)
|
|
row("roundtrip-empty", op="roundtrip", args=note_dict(version="9.9.9", summary="", improved=[], added=[], fixed=[], proven=[], limitations=[], references=[], release_sha="", deployed_sha="", provenance=[]))
|
|
|
|
|
|
# freeze init/roundtrip wants
|
|
for r in ROWS:
|
|
if r["op"] == "init":
|
|
kw = dict(r["args"])
|
|
r["want"] = want_note(ReleaseNote(**kw))
|
|
elif r["op"] == "roundtrip":
|
|
d = dict(r["args"])
|
|
n = ReleaseNote.from_dict(d)
|
|
r["want"] = json.dumps(n.to_dict())
|
|
|
|
# ── render_markdown ──────────────────────────────────────────────────────────
|
|
|
|
for name, nd in [
|
|
("render-both-same", note_dict()),
|
|
("render-both-diff", note_dict(release_sha=SHA_A, deployed_sha=SHA_B, backfill=True, provenance=["p1", "p2"])),
|
|
("render-deployed-only", note_dict(release_sha="", deployed_sha=SHA_B)),
|
|
("render-no-shas", note_dict(release_sha="", deployed_sha="")),
|
|
("render-empty-sections", note_dict(improved=[], added=[], fixed=[], proven=[], limitations=[], references=[], provenance=[])),
|
|
("render-short-shas", note_dict(release_sha="abc", deployed_sha="abd", improved=["x"], added=[], fixed=[], proven=[], limitations=[], references=[])),
|
|
("render-short-equal-shas", note_dict(release_sha="abc", deployed_sha="abc")),
|
|
("render-unicode", note_dict(summary="Über — concluído ✓", improved=["café ☕ — faster"], added=[], fixed=[], proven=[], limitations=[], references=[])),
|
|
]:
|
|
n = ReleaseNote(**dict(nd))
|
|
row(name, op="render", args=nd, want=render_markdown(n))
|
|
|
|
# ── extract_evidence_facts ───────────────────────────────────────────────────
|
|
|
|
doc1 = {
|
|
"entities": [
|
|
contract("close the loop on recall gating " + "detail " * 20, {"correctly_predicted": ["e1", "e2"]}),
|
|
entity("maintenance_item", {"status": "resolved", "issue": "drift on promote " + "more " * 20}),
|
|
entity("maintenance_item", {"status": "open", "issue": "known flake in CI"}),
|
|
entity("maintenance_item", {"status": "wontfix", "issue": "ignored"}),
|
|
entity("tui_projection_decision", {"feature": "build panel", "projection": "live gates"}),
|
|
contract("not closed yet", cid="contract-2"),
|
|
entity("mystery_kind", {"x": "y"}),
|
|
]
|
|
}
|
|
row("extract-mixed", op="extract", args={"doc": doc1})
|
|
|
|
doc2 = {
|
|
"entities": [
|
|
contract("dup need", {"correctly_predicted": []}, cid="c-a"),
|
|
contract("dup need", cid="c-b"),
|
|
{"id": "c-c", "kind": "construction_contract", "status": "closed", "identities": {"need": "dup need"},
|
|
"ext": {"release_evidence": {"final_deployed": SHA_A}}},
|
|
{"id": "c-d", "kind": "construction_contract", "status": "closed", "identities": {"need": "identity only"},
|
|
"ext": {"release_evidence": {"deployed_identity": "v9-build-77"}}},
|
|
{"id": "c-e", "kind": "construction_contract", "status": "closed", "identities": {"need": "both keys"},
|
|
"ext": {"release_evidence": {"final_deployed": SHA_B, "deployed_identity": "ignored-when-final-present"}}},
|
|
{"id": "c-f", "kind": "construction_contract", "status": "closed", "identities": {"need": "neither key"},
|
|
"ext": {"release_evidence": {"other": 1}}},
|
|
{"id": "c-g", "kind": "construction_contract", "status": "closed", "identities": {"need": "empty evidence dict"},
|
|
"ext": {"release_evidence": {}}},
|
|
]
|
|
}
|
|
row("extract-release-evidence-variants", op="extract", args={"doc": doc2})
|
|
row("extract-empty", op="extract", args={"doc": {"entities": []}})
|
|
row("extract-no-entities-key", op="extract", args={"doc": {}})
|
|
row(
|
|
"extract-unicode-truncation",
|
|
op="extract",
|
|
args={"doc": {"entities": [
|
|
contract("é" * 95),
|
|
entity("maintenance_item", {"status": "resolved", "issue": "ü" * 95}),
|
|
entity("tui_projection_decision", {"feature": "féature", "projection": "projęction — " + "z" * 200}),
|
|
]}},
|
|
)
|
|
|
|
for r in ROWS:
|
|
if r["op"] == "extract":
|
|
r["want"] = json.dumps(extract_evidence_facts(r["args"]["doc"]))
|
|
|
|
# ── build_note ───────────────────────────────────────────────────────────────
|
|
|
|
bn_doc = {
|
|
"entities": [
|
|
contract("recall gating closed"),
|
|
entity("maintenance_item", {"status": "resolved", "issue": "drift bug"}),
|
|
entity("maintenance_item", {"status": "open", "issue": "legacy parser"}),
|
|
]
|
|
}
|
|
row(
|
|
"build-note-merge",
|
|
op="build_note",
|
|
args={"version": "0.150.0", "summary": "converged release", "scaffold_doc": bn_doc,
|
|
"release_sha": SHA_A, "deployed_sha": SHA_B, "extra": {"improved": ["faster recall", "manual extra"], "fixed": ["manual fix"]},
|
|
"provenance": ["deploy workflow"], "backfill": False},
|
|
)
|
|
row(
|
|
"build-note-exclude",
|
|
op="build_note",
|
|
args={"version": "0.151.0", "summary": "filtered", "scaffold_doc": bn_doc,
|
|
"exclude_unsupported": ["RECALL", "manual"], "extra": {"improved": ["recall gating — RETRACTED", "safe item"]}},
|
|
)
|
|
row(
|
|
"build-note-no-extras",
|
|
op="build_note",
|
|
args={"version": "0.152.0", "summary": "plain", "scaffold_doc": {"entities": []}},
|
|
)
|
|
for r in ROWS:
|
|
if r["op"] == "build_note":
|
|
a = r["args"]
|
|
n = build_note(
|
|
version=a["version"], summary=a["summary"], scaffold_doc=a["scaffold_doc"],
|
|
release_sha=a.get("release_sha", ""), deployed_sha=a.get("deployed_sha", ""),
|
|
extra=a.get("extra"), provenance=a.get("provenance"), backfill=a.get("backfill", False),
|
|
exclude_unsupported=a.get("exclude_unsupported"),
|
|
)
|
|
r["want"] = want_note(n)
|
|
|
|
# ── summary_from_body ────────────────────────────────────────────────────────
|
|
|
|
for name, body in [
|
|
("summary-rendered", render_markdown(ReleaseNote(**note_dict()))),
|
|
("summary-then-heading", "# Rune v1\n\nThe good stuff.\n\n## Improved\n- x"),
|
|
("summary-break-on-item", "# Rune v1\n\nFirst line.\nSecond line.\n- item breaks"),
|
|
("summary-break-on-italic", "# Rune v1\n\nWords here.\n_italic breaks_"),
|
|
("summary-blank-gap-stops", "# Rune v1\n\nPara one.\n\n## Improved\n- x"),
|
|
("summary-leading-blanks", "# Rune v1\n\n\n\nafter blanks"),
|
|
("summary-title-only", "# Rune v1\n"),
|
|
("summary-empty-body", ""),
|
|
("summary-crlf", "# Rune v1\r\n\r\nCRLF body line.\r\n"),
|
|
("summary-ws-strip", "# Rune v1\n\n padded line \n"),
|
|
("summary-vertical-tab", "# Rune v1\n\ntitle\x0bvertical sep"),
|
|
("summary-hash-inside", "# Rune v1\n\nnot heading # inside"),
|
|
]:
|
|
row(name, op="summary", args={"body": body}, want=summary_from_body(body))
|
|
|
|
# ── sha_from_body ────────────────────────────────────────────────────────────
|
|
|
|
body_same = render_markdown(ReleaseNote(**note_dict()))
|
|
body_diff = render_markdown(ReleaseNote(**note_dict(release_sha=SHA_A, deployed_sha=SHA_B)))
|
|
for name, body, which in [
|
|
("sha-tag", body_same, "tag"),
|
|
("sha-deployed-same", body_same, "deployed"),
|
|
("sha-tag-diff", body_diff, "tag"),
|
|
("sha-deployed-diff", body_diff, "deployed"),
|
|
("sha-missing", "no shas here", "tag"),
|
|
("sha-missing-deployed", "no shas here", "deployed"),
|
|
("sha-uppercase-hex", "_Tag commit: `ABCDEF1234567890ABCDEF1234567890ABCDEF12`_", "tag"),
|
|
("sha-lowercase-keyword", "_tag COMMIT: `" + SHA_A[:12] + "` (deployed: `" + SHA_A[:12] + "`)_", "tag"),
|
|
("sha-only-deployed-markup", "**final deployed: `" + SHA_B[:12] + "`**", "deployed"),
|
|
("sha-both-present-first-wins", "_Tag commit: `aaaaaaaaaaaa` · **final deployed: `bbbbbbbbbbbb`**", "tag"),
|
|
]:
|
|
row(name, op="sha", args={"body": body, "which": which}, want=sha_from_body(body, which))
|
|
|
|
# ── _api / fetch_release / publish_release ───────────────────────────────────
|
|
|
|
server, repo, token = "https://gitea.example", "nick/rune", "tok-123"
|
|
|
|
api_cases = [
|
|
("api-get-json", {"url": f"{server}/api/v1/x", "token": token, "method": "GET", "payload": None},
|
|
[resp_ok(200, '{"id": 5, "html_url": "https://gitea.example/nick/rune/releases/tag/v1"}')]),
|
|
("api-get-empty-body", {"url": f"{server}/api/v1/x", "token": token, "method": "GET", "payload": None},
|
|
[resp_ok(204, "")]),
|
|
("api-post-payload", {"url": f"{server}/api/v1/x", "token": token, "method": "POST",
|
|
"payload": [["tag_name", "v0.1.0"], ["name", "Rune — ünïcode ✓"], ["draft", False]]},
|
|
[resp_ok(201, '{"id": 9}')]),
|
|
("api-http-json-error", {"url": f"{server}/api/v1/x", "token": token, "method": "GET", "payload": None},
|
|
[resp_http(404, '{"message": "release not found"}')]),
|
|
("api-http-plain-text", {"url": f"{server}/api/v1/x", "token": token, "method": "GET", "payload": None},
|
|
[resp_http(404, "page not found")]),
|
|
("api-http-plain-long", {"url": f"{server}/api/v1/x", "token": token, "method": "GET", "payload": None},
|
|
[resp_http(500, "E" * 500)]),
|
|
("api-http-empty-body", {"url": f"{server}/api/v1/x", "token": token, "method": "GET", "payload": None},
|
|
[resp_http(502, "")]),
|
|
("api-os-error", {"url": f"{server}/api/v1/x", "token": token, "method": "GET", "payload": None},
|
|
[resp_os("dial tcp: connection refused")]),
|
|
("api-os-error-long", {"url": f"{server}/api/v1/x", "token": token, "method": "GET", "payload": None},
|
|
[resp_os("O" * 300)]),
|
|
]
|
|
for name, args, script in api_cases:
|
|
out = run_api(script, lambda a=args: _api(a["url"], a["token"], a["method"], dict(a["payload"]) if a["payload"] else None))
|
|
row(name, op="api", args=args, script=script, result=json.dumps(out["result"]), calls=out["calls"])
|
|
|
|
fr_cases = [
|
|
("fetch-200", [resp_ok(200, '{"id": 5, "tag_name": "v0.142.0", "body": "stored"}')]),
|
|
("fetch-404-plain", [resp_http(404, "page not found")]),
|
|
("fetch-500", [resp_http(500, '{"message": "boom"}')]),
|
|
]
|
|
for name, script in fr_cases:
|
|
out = run_api(script, lambda: fetch_release(server, repo, token, "v0.142.0"))
|
|
row(name, op="fetch_release", args={"server": server, "repo": repo, "token": token, "tag": "v0.142.0"},
|
|
script=script, result=json.dumps(out["result"]), calls=out["calls"])
|
|
|
|
pub_note = ReleaseNote(**note_dict(version="0.160.0"))
|
|
pub_cases = [
|
|
("publish-created",
|
|
[resp_http(404, "page not found"), resp_ok(201, '{"id": 7, "html_url": "https://gitea.example/nick/rune/releases/tag/v0.160.0"}')],
|
|
{}),
|
|
("publish-created-failure-json",
|
|
[resp_http(404, "page not found"), resp_http(422, '{"message": "tag already exists"}')],
|
|
{}),
|
|
("publish-created-failure-plain",
|
|
[resp_http(404, "page not found"), resp_http(422, "validation failed, no json")],
|
|
{}),
|
|
("publish-updated",
|
|
[resp_ok(200, '{"id": 3}'), resp_ok(200, '{"id": 3, "html_url": "https://gitea.example/nick/rune/releases/tag/v0.160.0"}')],
|
|
{}),
|
|
("publish-update-failure-plain",
|
|
[resp_ok(200, '{"id": 3}'), resp_http(500, "internal server error — " + "x" * 250)],
|
|
{}),
|
|
("publish-target-commitish",
|
|
[resp_http(404, "page not found"), resp_ok(201, '{"id": 11}')],
|
|
{"target_commitish": "main"}),
|
|
("publish-update-no-html-url",
|
|
[resp_ok(200, '{"id": 4}'), resp_ok(200, '{"id": 4}')],
|
|
{}),
|
|
("publish-fetch-os-error-creates",
|
|
[resp_os("getaddrinfo failed"), resp_ok(201, '{"id": 12}')],
|
|
{}),
|
|
]
|
|
for name, script, kw in pub_cases:
|
|
out = run_api(script, lambda kw=kw: publish_release(server, repo, token, pub_note, **kw))
|
|
row(name, op="publish_release",
|
|
args={"server": server, "repo": repo, "token": token, "note": pub_note.to_dict(),
|
|
"target_commitish": kw.get("target_commitish", "master")},
|
|
script=script, result=json.dumps(out["result"]), calls=out["calls"])
|
|
|
|
# ── converged_deployed_sha (real filesystem via temp roots) ─────────────────
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
root = Path(td)
|
|
# case 1: healthy link
|
|
dep1 = root / "dep1"
|
|
tgt1 = root / "rel1"
|
|
(tgt1 / "src" / "rune").mkdir(parents=True)
|
|
(tgt1 / "src" / "rune" / "_build_identity.json").write_text(json.dumps({"commit": SHA_A, "version": "0.1.0"}))
|
|
dep1.mkdir()
|
|
(dep1 / "current").symlink_to(tgt1)
|
|
row("converged-ok", op="converged", deploy_root="dep1",
|
|
setup={"files": {"rel1/src/rune/_build_identity.json": json.dumps({"commit": SHA_A, "version": "0.1.0"})},
|
|
"links": {"dep1/current": "{ROOT}/rel1"}},
|
|
want=converged_deployed_sha(str(dep1)))
|
|
|
|
# case 2: missing current symlink
|
|
dep2 = root / "dep2"
|
|
dep2.mkdir()
|
|
row("converged-no-link", op="converged", deploy_root="dep2", setup={"files": {}, "links": {}}, want=converged_deployed_sha(str(dep2)))
|
|
|
|
# case 3: identity file missing
|
|
dep3 = root / "dep3"
|
|
tgt3 = root / "rel3"
|
|
(tgt3 / "src" / "rune").mkdir(parents=True)
|
|
dep3.mkdir()
|
|
(dep3 / "current").symlink_to(tgt3)
|
|
row("converged-no-identity", op="converged", deploy_root="dep3", setup={"files": {}, "links": {"dep3/current": "{ROOT}/rel3"}}, want=converged_deployed_sha(str(dep3)))
|
|
|
|
# case 4: bad JSON identity
|
|
dep4 = root / "dep4"
|
|
tgt4 = root / "rel4"
|
|
(tgt4 / "src" / "rune").mkdir(parents=True)
|
|
(tgt4 / "src" / "rune" / "_build_identity.json").write_text("{not json")
|
|
dep4.mkdir()
|
|
(dep4 / "current").symlink_to(tgt4)
|
|
row("converged-bad-json", op="converged", deploy_root="dep4", setup={"files": {"rel4/src/rune/_build_identity.json": "{not json"}, "links": {"dep4/current": "{ROOT}/rel4"}}, want=converged_deployed_sha(str(dep4)))
|
|
|
|
# case 5: commit key missing → ""
|
|
dep5 = root / "dep5"
|
|
tgt5 = root / "rel5"
|
|
(tgt5 / "src" / "rune").mkdir(parents=True)
|
|
(tgt5 / "src" / "rune" / "_build_identity.json").write_text(json.dumps({"version": "0.2.0"}))
|
|
dep5.mkdir()
|
|
(dep5 / "current").symlink_to(tgt5)
|
|
row("converged-no-commit-key", op="converged", deploy_root="dep5", setup={"files": {"rel5/src/rune/_build_identity.json": json.dumps({"version": "0.2.0"})}, "links": {"dep5/current": "{ROOT}/rel5"}}, want=converged_deployed_sha(str(dep5)))
|
|
|
|
# case 6: non-string commit → str()
|
|
dep6 = root / "dep6"
|
|
tgt6 = root / "rel6"
|
|
(tgt6 / "src" / "rune").mkdir(parents=True)
|
|
(tgt6 / "src" / "rune" / "_build_identity.json").write_text(json.dumps({"commit": 424242}))
|
|
dep6.mkdir()
|
|
(dep6 / "current").symlink_to(tgt6)
|
|
row("converged-int-commit", op="converged", deploy_root="dep6", setup={"files": {"rel6/src/rune/_build_identity.json": json.dumps({"commit": 424242})}, "links": {"dep6/current": "{ROOT}/rel6"}}, want=converged_deployed_sha(str(dep6)))
|
|
|
|
# ── auto_initial_note ────────────────────────────────────────────────────────
|
|
|
|
store_doc = json.dumps({"entities": [
|
|
contract("contracts closed at init", {"correctly_predicted": ["e1"]}, cid="c-init"),
|
|
entity("maintenance_item", {"status": "resolved", "issue": "init bug"}),
|
|
entity("maintenance_item", {"status": "open", "issue": "open item"}),
|
|
]})
|
|
row(
|
|
"auto-init-store",
|
|
op="auto_initial",
|
|
args={"version": "0.170.0", "summary": "init summary", "store": store_doc, "release_sha": SHA_A,
|
|
"deployed_sha": SHA_B, "extra": {"improved": ["init improvement"]}},
|
|
)
|
|
row(
|
|
"auto-init-store-missing",
|
|
op="auto_initial",
|
|
args={"version": "0.170.1", "summary": "no store", "store": None, "release_sha": SHA_A},
|
|
)
|
|
row(
|
|
"auto-init-store-bad-json",
|
|
op="auto_initial",
|
|
args={"version": "0.170.2", "summary": "bad store", "store": "{oops", "release_sha": SHA_A},
|
|
)
|
|
row(
|
|
"auto-init-custom-provenance",
|
|
op="auto_initial",
|
|
args={"version": "0.170.3", "summary": "prov", "store": store_doc, "release_sha": SHA_A,
|
|
"provenance": ["explicit provenance"], "extra": {"added": ["new thing"], "fixed": ["old thing"]}},
|
|
)
|
|
for r in ROWS:
|
|
if r["op"] == "auto_initial":
|
|
a = r["args"]
|
|
tmp = None
|
|
path = "missing-store.json"
|
|
if a["store"] is not None:
|
|
tmp = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False)
|
|
tmp.write(a["store"])
|
|
tmp.close()
|
|
path = tmp.name
|
|
try:
|
|
n = auto_initial_note(
|
|
version=a["version"], summary=a["summary"], scaffold_store=path,
|
|
release_sha=a["release_sha"], deployed_sha=a.get("deployed_sha", ""),
|
|
provenance=a.get("provenance"), extra=a.get("extra"),
|
|
)
|
|
finally:
|
|
if tmp is not None:
|
|
os.unlink(path)
|
|
r["want"] = want_note(n)
|
|
|
|
# ── convergence_refresh ──────────────────────────────────────────────────────
|
|
|
|
refresh_store = json.dumps({"entities": [
|
|
entity("tui_projection_decision", {"feature": "gates", "projection": "panel"}),
|
|
contract("newly closed after tag", cid="c-late"),
|
|
]})
|
|
base = ReleaseNote(**note_dict(version="0.180.0", release_sha=SHA_A, deployed_sha=SHA_A, provenance=["init prov"], backfill=True))
|
|
row(
|
|
"refresh-merge",
|
|
op="refresh",
|
|
args={"note": base.to_dict(), "deployed_sha": SHA_B, "store": refresh_store, "extra_provenance": "convergence pass"},
|
|
)
|
|
row(
|
|
"refresh-dup-provenance",
|
|
op="refresh",
|
|
args={"note": base.to_dict(), "deployed_sha": SHA_B, "store": None, "extra_provenance": "init prov"},
|
|
)
|
|
row(
|
|
"refresh-bad-store",
|
|
op="refresh",
|
|
args={"note": base.to_dict(), "deployed_sha": SHA_B, "store": "{{{", "extra_provenance": ""},
|
|
)
|
|
for r in ROWS:
|
|
if r["op"] == "refresh":
|
|
a = r["args"]
|
|
tmp = None
|
|
path = "missing-store.json"
|
|
if a["store"] is not None:
|
|
tmp = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False)
|
|
tmp.write(a["store"])
|
|
tmp.close()
|
|
path = tmp.name
|
|
try:
|
|
n = convergence_refresh(ReleaseNote.from_dict(a["note"]), deployed_sha=a["deployed_sha"],
|
|
scaffold_store=path, extra_provenance=a["extra_provenance"])
|
|
finally:
|
|
if tmp is not None:
|
|
os.unlink(path)
|
|
r["want"] = want_note(n)
|
|
|
|
# ── build_from_deployment ────────────────────────────────────────────────────
|
|
|
|
row(
|
|
"bfd-default-summary",
|
|
op="bfd",
|
|
args={"version": "0.190.0", "deployed_sha": SHA_B, "store": store_doc, "release_sha": SHA_A},
|
|
)
|
|
row(
|
|
"bfd-explicit-summary",
|
|
op="bfd",
|
|
args={"version": "0.190.1", "deployed_sha": SHA_B, "store": None, "release_sha": "", "summary": "custom summary"},
|
|
)
|
|
for r in ROWS:
|
|
if r["op"] == "bfd":
|
|
a = r["args"]
|
|
tmp = None
|
|
path = "missing-store.json"
|
|
if a["store"] is not None:
|
|
tmp = tempfile.NamedTemporaryFile("w", suffix=".json", delete=False)
|
|
tmp.write(a["store"])
|
|
tmp.close()
|
|
path = tmp.name
|
|
try:
|
|
n = build_from_deployment(version=a["version"], deployed_sha=a["deployed_sha"], scaffold_store=path,
|
|
release_sha=a.get("release_sha", ""), summary=a.get("summary", ""))
|
|
finally:
|
|
if tmp is not None:
|
|
os.unlink(path)
|
|
r["want"] = want_note(n)
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--out", default=str(ROOT / "src" / "go" / "release-notes" / "truth.json"))
|
|
a = ap.parse_args()
|
|
Path(a.out).write_text(json.dumps(ROWS, indent=1) + "\n", encoding="utf-8")
|
|
ops: dict[str, int] = {}
|
|
for r in ROWS:
|
|
ops[r["op"]] = ops.get(r["op"], 0) + 1
|
|
print(f"frozen {len(ROWS)} rows → {a.out}")
|
|
for k in sorted(ops):
|
|
print(f" {k}: {ops[k]}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|