From 15a1111a0d06fe71bb6ef91eb2b9b6581913a5ab Mon Sep 17 00:00:00 2001 From: 4444J99 <4444J99@users.noreply.github.com> Date: Fri, 3 Jul 2026 18:52:34 -0400 Subject: [PATCH] Wire the governance organ to advance autonomously limen task ORG-governance-organ-selffeed-0703 --- cli/tests/test_publication_policy.py | 69 ++++++ institutio/governance/parameters.yaml | 4 +- organ-ladder.json | 26 +- organs/governance/PUBLICATION-POLICY.md | 16 +- scripts/heartbeat-loop.sh | 7 +- scripts/organ-health.py | 2 +- scripts/publication-policy.py | 300 +++++++++++++++++++++++- 7 files changed, 398 insertions(+), 26 deletions(-) diff --git a/cli/tests/test_publication_policy.py b/cli/tests/test_publication_policy.py index 5260d16e..0ed26edb 100644 --- a/cli/tests/test_publication_policy.py +++ b/cli/tests/test_publication_policy.py @@ -7,6 +7,7 @@ """ import importlib.util +import json from pathlib import Path import pytest @@ -17,6 +18,18 @@ spec.loader.exec_module(pp) +def load_publication_policy(tmp_path: Path, monkeypatch): + monkeypatch.setenv("LIMEN_ROOT", str(tmp_path)) + spec = importlib.util.spec_from_file_location( + "publication_policy_under_test", + ROOT / "scripts" / "publication-policy.py", + ) + module = importlib.util.module_from_spec(spec) + assert spec and spec.loader + spec.loader.exec_module(module) + return module + + # --- the redactor scrubs OWNER identifiers ----------------------------------- @pytest.mark.parametrize( "src,want", @@ -123,3 +136,59 @@ def test_self_test_passes(): def test_residual_pii_detects_and_clears(): assert pp._residual_pii("Anthony Padavano here") is not None assert pp._residual_pii(pp.redact("Anthony Padavano here")) is None + + +def test_maturity_assessment_includes_converged_gate_refs(): + assessment = pp._assess_maturity([]) + assert "convergence_authority_referenced" in assessment["passed"] + assert assessment["maturity_pct"] >= 80 + + +def test_publication_policy_maturity_advance_selects_rank10_entry(tmp_path: Path, monkeypatch): + module = load_publication_policy(tmp_path, monkeypatch) + ladder = tmp_path / "organ-ladder.json" + ladder.write_text( + json.dumps( + { + "organs": [ + { + "rank": 5, + "pillar": "governance", + "organ": "Aerarium / Cvrsvs Honorvm", + "maturity": 75, + "stage": "maturing", + "note": "cursus honorum — already wired", + }, + { + "rank": 10, + "pillar": "governance", + "organ": "Publication Policy (the disclosure court)", + "maturity": 60, + "stage": "building", + "note": "converges gates; beat C_PUBPOLICY, rung PUBPOLICY", + }, + ] + }, + ensure_ascii=False, + ) + ) + + result = module._advance_maturity({"maturity_pct": 80}) + + assert result["bumped"] is True + text = ladder.read_text() + assert "cursus honorum — already wired" in text + assert "\\u2014" not in text + data = json.loads(text) + aerarium, pubpolicy = data["organs"] + assert aerarium["maturity"] == 75 + assert aerarium["stage"] == "maturing" + assert "publication-policy.py" not in aerarium["note"] + assert pubpolicy["maturity"] == 80 + assert pubpolicy["stage"] == "maturing" + assert "Maturity 60%->80% auto-advanced by publication-policy.py beat" in pubpolicy["note"] + + second = module._advance_maturity({"maturity_pct": 80}) + + assert second["bumped"] is False + assert ladder.read_text().count("Maturity 60%->80% auto-advanced by publication-policy.py beat") == 1 diff --git a/institutio/governance/parameters.yaml b/institutio/governance/parameters.yaml index 8e81eb1d..42adc519 100644 --- a/institutio/governance/parameters.yaml +++ b/institutio/governance/parameters.yaml @@ -1152,13 +1152,13 @@ parameters: env: LIMEN_BEAT_PUBPOLICY self_update: none owner: pubpolicy - note: "Beat cadence for the publication-policy organ (scripts/publication-policy.py --verify): re-prove the content-disposition engine stays sound (redactor owner-scoped, matrix + classifier intact) every N beats. Read-only self-test, stamps logs/publication-policy-state.json." + note: "Beat cadence for the publication-policy organ (scripts/publication-policy.py --verify): re-prove the content-disposition engine stays sound (redactor owner-scoped, matrix + classifier intact) every N beats, stamp logs/publication-policy-state.json, and advance the rank-10 Publication Policy maturity in organ-ladder.json when objective criteria grow." LIMEN_PUBPOLICY: default: "1" # ON — verify the disposition engine each due beat env: LIMEN_PUBPOLICY self_update: none owner: pubpolicy - note: "Beat gate for the publication-policy organ (the ONE content-disposition decision: repo visibility x content class -> disposition). 1=self-verify each due beat; 0=off. Fail-open, never gates the beat. See organs/governance/PUBLICATION-POLICY.md." + note: "Beat gate for the publication-policy organ (the ONE content-disposition decision: repo visibility x content class -> disposition). 1=self-verify and self-feed each due beat; 0=off. Fail-open, never gates the beat. See organs/governance/PUBLICATION-POLICY.md." LIMEN_OWNER_IDENTITY: default: "" # unset — the redactor uses its built-in owner instance env: LIMEN_OWNER_IDENTITY diff --git a/organ-ladder.json b/organ-ladder.json index ae957147..a18103f2 100644 --- a/organ-ladder.json +++ b/organ-ladder.json @@ -1,11 +1,11 @@ { "_comment": "VLTIMA MATERIA institutional census, made executable. The rich win because civilization's pillars (a legal org, a family office, a PR machine) stand behind them; this ranks those same pillars rebuilt as AI-run ORGANS so the idle fleet can build them continuously. Mirror of revenue-ladder.json: generate-organ-backlog.py reads this and emits bounded, lockless organ-build tasks toward each organ's next maturity band. Identity/levers are DERIVED here, never pinned in code. Each organ is a FRACTAL: macro (a platform anyone can hold) + micro (Anthony's own instance). Build stages: scaffold(<30%) -> building(30-60%) -> maturing(60-90%) -> mature(>=90, no build tasks). Organ artifacts are authored into the limen repo under organs// (same proven home as the Studium) so dispatch never hits a dead clone; repo-specific product code is handled by the revenue/coverage generators.", - "spine": "Institutional weight for one person, then anyone \u2014 the prosthesis for human weakness. Not tips; institutions.", + "spine": "Institutional weight for one person, then anyone — the prosthesis for human weakness. Not tips; institutions.", "maturity_stages": { - "scaffold": "0-29% \u2014 kernel + charter + first vertical slice", - "building": "30-59% \u2014 close the gap to a usable institution; wire it self-feeding", - "maturing": "60-89% \u2014 excellent macro+micro face; operationalize governance", - "mature": ">=90% \u2014 self-running; no generated build work" + "scaffold": "0-29% — kernel + charter + first vertical slice", + "building": "30-59% — close the gap to a usable institution; wire it self-feeding", + "maturing": "60-89% — excellent macro+micro face; operationalize governance", + "mature": ">=90% — self-running; no generated build work" }, "organs": [ { @@ -94,15 +94,15 @@ "organ": "Publication Policy (the disclosure court)", "repo": "organvm/limen", "home": "organs/governance/", - "maturity": 60, - "stage": "building", + "maturity": 80, + "stage": "maturing", "whose_hand": "mine (the publish / flip-visibility click is his)", "rival": "a newsroom standards desk + a data-protection office fused into one rule table", "domain_map": "(repo visibility x content class) -> one disposition; owner-scoped redaction as case law", "macro": "a portable content-disposition standard any repo/estate can defer to instead of judging per-file", "micro": "ORGANVM's own PII/strategy sweep: the ONE answer for 'does this belong on this repo'", "first_artifact": "scripts/publication-policy.py --verify green; matrix + owner-scoped redactor shipped (2026-07-02)", - "note": "converges the scattered gates (creds _SECRET_RX, scan-legacy SENSITIVE_PATTERNS, persona contracts, positioning awaiting_publish) behind one referenced rule table; beat C_PUBPOLICY, rung PUBPOLICY" + "note": "converges the scattered gates (creds _SECRET_RX, scan-legacy SENSITIVE_PATTERNS, persona contracts, positioning awaiting_publish) behind one referenced rule table; beat C_PUBPOLICY, rung PUBPOLICY. Maturity 60%->80% auto-advanced by publication-policy.py beat (2026-07-03)." }, { "rank": 6, @@ -178,13 +178,13 @@ "codex (87% efficiency, the best earner)", "jules (94% idle)" ], - "binding_constraint": "SUPPLY of high-value work, not capacity \u2014 these organs ARE the supply", + "binding_constraint": "SUPPLY of high-value work, not capacity — these organs ARE the supply", "source": "logs/usage-limits.json, logs/usage.json, logs/ledger.json (assessed 2026-06-23)" }, "your_levers": [ - "card-0186 Santander fraud hold \u2014 keystone: the whole stack's billing rides on it (one call clears it; also frees Anthropic/GitHub autopay)", - "financial identity \u2014 receiving real money (bank/KYC for a payout rail or a signed deal) is genuinely yours", - "the send / the signature \u2014 outbound emails and any contract: I draft, you send/sign (reversible-only invariant)", - "institutional logins \u2014 D2L (education), and privileged/filing acts with Micah (legal)" + "card-0186 Santander fraud hold — keystone: the whole stack's billing rides on it (one call clears it; also frees Anthropic/GitHub autopay)", + "financial identity — receiving real money (bank/KYC for a payout rail or a signed deal) is genuinely yours", + "the send / the signature — outbound emails and any contract: I draft, you send/sign (reversible-only invariant)", + "institutional logins — D2L (education), and privileged/filing acts with Micah (legal)" ] } diff --git a/organs/governance/PUBLICATION-POLICY.md b/organs/governance/PUBLICATION-POLICY.md index 15b12413..3d92e314 100644 --- a/organs/governance/PUBLICATION-POLICY.md +++ b/organs/governance/PUBLICATION-POLICY.md @@ -7,7 +7,7 @@ clear and never re-litigated. - **Engine:** [`scripts/publication-policy.py`](../../scripts/publication-policy.py) - **Tests / predicate:** [`cli/tests/test_publication_policy.py`](../../cli/tests/test_publication_policy.py) + `publication-policy.py --verify` -- **Beat:** `C_PUBPOLICY` (heartbeat) → `--verify` each cycle → stamps `logs/publication-policy-state.json` → proprioception rung `PUBPOLICY`. +- **Beat:** `C_PUBPOLICY` (heartbeat) → `--verify` each cycle → stamps `logs/publication-policy-state.json` → proprioception rung `PUBPOLICY` → advances this organ's `organ-ladder.json` maturity when objective checks pass. ## The decision @@ -85,3 +85,17 @@ python3 scripts/publication-policy.py --verify # is the engin The 2026-07-02 PII-sweep re-audit was resolved *by this engine* — see [`DISCLOSURE-AUDIT.md`](DISCLOSURE-AUDIT.md) for the per-repo disposition ledger it produced. + +## Self-feed + +The publication-policy beat is not just a smoke test. `scripts/publication-policy.py --verify` +assesses the rank-10 `Publication Policy` organ from repo-local criteria: engine soundness, +test coverage, heartbeat wiring, proprioception wiring, parameter registration, and whether the +legacy gates above all point back to this rule table. When the assessed maturity exceeds the +stored maturity, it updates only the `Publication Policy` entry in `organ-ladder.json` through a +mkdir lock plus atomic replace. It never touches `tasks.yaml`, never dispatches work, and never +lowers maturity. + +The remaining 20% is intentionally held behind the estate cleanup in +[`DISCLOSURE-AUDIT.md`](DISCLOSURE-AUDIT.md): verified/stripped public-HEAD residuals, resolved +inferred cases, and external/his-hand residues. diff --git a/scripts/heartbeat-loop.sh b/scripts/heartbeat-loop.sh index e067ebd3..54b7cf71 100755 --- a/scripts/heartbeat-loop.sh +++ b/scripts/heartbeat-loop.sh @@ -156,7 +156,7 @@ C_HEALTH="${LIMEN_BEAT_HEALTH:-6}" # CARE (refresh the personal health off C_LIFE="${LIMEN_BEAT_LIFE:-6}" # STEWARD (refresh the digital-life office: accounts/assets/subscription purge clock; PII off-repo) C_GOVERNANCE="${LIMEN_BEAT_GOVERNANCE:-8}" # GOVERN (run the cursus honorum seed validator + governance standing report) C_FINANCIAL="${LIMEN_BEAT_FINANCIAL:-8}" # FINANCE (run the financial-office consolidator + advance maturity) -C_PUBPOLICY="${LIMEN_BEAT_PUBPOLICY:-8}" # DISCLOSE (verify the content-disposition engine: redactor owner-scoped, matrix + classifier intact) +C_PUBPOLICY="${LIMEN_BEAT_PUBPOLICY:-8}" # DISCLOSE (verify the content-disposition engine + advance Publication Policy maturity) C_WALLS="${LIMEN_BEAT_WALLS:-12}" # WALLS (regenerate the credential Wall #320 + his-hand Wall #330 so they never drift) C_CVSTOS="${LIMEN_BEAT_CVSTOS:-24}" # KEEP (CVSTOS — host stays factory: chat-app/local debt census + factory-invariant + reaper proprioception; filesystem walk ⇒ rare) C_VVLTVS="${LIMEN_BEAT_VVLTVS:-24}" # FACE (VVLTVS — verify the public face reflects the live SSOT: profile/portfolio drift + contribution-mix radar; offline read ⇒ cheap) @@ -501,8 +501,9 @@ while true; do { python3 "$LIMEN_ROOT/scripts/financial-organ.py" 2>&1 | tail -1 || true; stamp financial; } # DISCLOSE — verify the publication-policy engine (the ONE content-disposition decision) stays sound # every C_PUBPOLICY beats: redactor owner-scoped (never eats product emails / placeholders / 555 - # fixtures), disposition matrix + classifier intact. Read-only self-test, stamps the pubpolicy voice. - # Idempotent, fail-open — never gates the beat. Gate off with LIMEN_PUBPOLICY=0. + # fixtures), disposition matrix + classifier intact. Also assesses and advances the rank-10 + # Publication Policy maturity in organ-ladder.json when objective checks grow. Idempotent, + # fail-open — never gates the beat. Gate off with LIMEN_PUBPOLICY=0. due_voice pubpolicy "$C_PUBPOLICY" && [ "${LIMEN_PUBPOLICY:-1}" = "1" ] && \ { python3 "$LIMEN_ROOT/scripts/publication-policy.py" --verify 2>&1 | tail -1 || true; stamp pubpolicy; } # CVSTOS — the keeper of the host. Every C_CVSTOS beats: census the chat-app/local debt (all diff --git a/scripts/organ-health.py b/scripts/organ-health.py index b00dfcd2..47c52e13 100644 --- a/scripts/organ-health.py +++ b/scripts/organ-health.py @@ -287,7 +287,7 @@ def _registry(): probe=lambda: _mtime(LOGS / "governance-organ-state.json")), dict(key="pubpolicy", rung="PUBPOLICY", voice="pubpolicy", cadence_key="PUBPOLICY", gate="LIMEN_PUBPOLICY", gate_default="1", - what="content-disposition engine: (repo visibility x content class) -> one disposition; owner-scoped redactor", + what="content-disposition engine: (repo visibility x content class) -> one disposition; owner-scoped redactor; rank-10 maturity self-feed", probe=lambda: _mtime(LOGS / "publication-policy-state.json")), dict(key="cvstos", rung="CVSTOS", voice="cvstos", cadence_key="CVSTOS", gate="LIMEN_CVSTOS", gate_default="1", diff --git a/scripts/publication-policy.py b/scripts/publication-policy.py index c2fb417d..c97f9a62 100644 --- a/scripts/publication-policy.py +++ b/scripts/publication-policy.py @@ -29,7 +29,7 @@ disposition --visibility V --class C -> the one disposition + autonomy redact [--apply] -> owner-identifier-only redaction (dry by default) audit [--json] -> run the matrix over a ledger of items - --verify -> self-test predicate (exit 0 <=> engine sound) + --verify -> self-test + maturity self-feed (exit 0 <=> engine sound) --check -> cheap stamp/presence check Fractal: OWNER identity is a NAMED config (macro form works for anyone); the default @@ -43,12 +43,16 @@ import os import re import sys -from datetime import datetime +import tempfile +import time +from datetime import datetime, timezone from pathlib import Path ROOT = Path(os.environ.get("LIMEN_ROOT", Path(__file__).resolve().parents[1])) LOGS = ROOT / "logs" STAMP = LOGS / "publication-policy-state.json" +LADDER = ROOT / "organ-ladder.json" +PUBPOLICY_ORGAN_PREFIX = "Publication Policy" # --------------------------------------------------------------------------- # OWNER identity (the only thing the redactor is scoped to). Generic underneath, @@ -183,6 +187,277 @@ def classify(path: str, text: str | None = None, owner: dict | None = None) -> t } +# --------------------------------------------------------------------------- +# Maturity assessment + ladder advancement. +# +# The rank-10 Publication Policy organ shares pillar="governance" with the +# rank-5 Aerarium organ, so selection is by organ identity, not pillar. +# --------------------------------------------------------------------------- +_MATURITY_CRITERIA: list[tuple[str, str, int]] = [ + # Foundation + first artifact: current self-feed slice can earn 80%. + ("engine_exists", "scripts/publication-policy.py exists", 8), + ("self_test_passes", "publication-policy.py --verify predicate is sound", 12), + ("rule_doc_exists", "PUBLICATION-POLICY.md rule table exists", 8), + ("cli_tests_exist", "publication policy test suite exists", 6), + ("beat_wired", "C_PUBPOLICY heartbeat beat runs the verifier", 10), + ("health_rung_wired", "PUBPOLICY proprioception rung is registered", 8), + ("parameters_registered", "PUBPOLICY cadence/gate parameters are registered", 6), + ("convergence_authority_referenced", "scattered gates reference the rule table", 12), + ("ladder_entry_registered", "Publication Policy organ is registered in organ-ladder.json", 4), + ("auto_advance_wired", "publication-policy.py advances its own ladder maturity", 6), + # Remaining estate cleanup: future slices can close the last 20%. + ("auto_remediation_backlog_closed", "confirmed public HEAD remediation backlog is closed", 10), + ("inferred_cases_verified", "all inferred disclosure-audit cases are verified", 5), + ("external_his_levers_resolved", "third-party/external publication-policy levers are resolved", 5), +] + + +def _exists(rel: str) -> bool: + return (ROOT / rel).exists() + + +def _contains(rel: str, *needles: str) -> bool: + try: + text = (ROOT / rel).read_text(encoding="utf-8", errors="replace") + except OSError: + return False + return all(n in text for n in needles) + + +def _publication_policy_entry(data: dict) -> dict | None: + organs = data.get("organs") or [] + for organ in organs: + if str(organ.get("organ", "")).startswith(PUBPOLICY_ORGAN_PREFIX): + return organ + for organ in organs: + if ( + organ.get("pillar") == "governance" + and organ.get("rank") == 10 + and "C_PUBPOLICY" in str(organ.get("note", "")) + ): + return organ + return None + + +def _ladder_has_pubpolicy() -> bool: + try: + data = json.loads(LADDER.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return False + return _publication_policy_entry(data) is not None + + +def _convergence_refs_present() -> bool: + refs = { + "scripts/creds-hydrate.py": "PUBLICATION-POLICY.md", + "scripts/scan-legacy-session-batch.py": "PUBLICATION-POLICY.md", + "scripts/generate-positioning.py": "PUBLICATION-POLICY.md", + "spec/contracts/surface-manifest.schema.json": "PUBLICATION-POLICY.md", + } + return all(_contains(path, needle) for path, needle in refs.items()) + + +def _audit_text() -> str: + try: + return (ROOT / "organs" / "governance" / "DISCLOSURE-AUDIT.md").read_text( + encoding="utf-8", + errors="replace", + ) + except OSError: + return "" + + +def _assess_maturity(self_test_fails: list[str] | None = None) -> dict: + """Objective publication-policy maturity. Future cleanup work can only bump up.""" + passed = [] + failed = [] + total_weight = sum(w for _, _, w in _MATURITY_CRITERIA) + audit = _audit_text() + self_test_fails = _self_test() if self_test_fails is None else self_test_fails + + for key, _label, _weight in _MATURITY_CRITERIA: + ok = False + if key == "engine_exists": + ok = _exists("scripts/publication-policy.py") + elif key == "self_test_passes": + ok = not self_test_fails + elif key == "rule_doc_exists": + ok = _contains( + "organs/governance/PUBLICATION-POLICY.md", + "The disposition matrix", + "Convergence table", + ) + elif key == "cli_tests_exist": + ok = _exists("cli/tests/test_publication_policy.py") + elif key == "beat_wired": + ok = _contains( + "scripts/heartbeat-loop.sh", + "C_PUBPOLICY", + "scripts/publication-policy.py", + "--verify", + "LIMEN_PUBPOLICY", + ) + elif key == "health_rung_wired": + ok = _contains( + "scripts/organ-health.py", + 'rung="PUBPOLICY"', + "publication-policy-state.json", + ) + elif key == "parameters_registered": + ok = _contains( + "institutio/governance/parameters.yaml", + "LIMEN_BEAT_PUBPOLICY", + "LIMEN_PUBPOLICY", + "owner: pubpolicy", + ) + elif key == "convergence_authority_referenced": + ok = _convergence_refs_present() + elif key == "ladder_entry_registered": + ok = _ladder_has_pubpolicy() + elif key == "auto_advance_wired": + ok = "_advance_maturity" in Path(__file__).read_text(encoding="utf-8", errors="replace") + elif key == "auto_remediation_backlog_closed": + ok = bool(audit) and "Backlog (auto, reversible)" not in audit and "verify-then-strip" not in audit + elif key == "inferred_cases_verified": + ok = bool(audit) and "class inferred" not in audit and "verify per-PR" not in audit + elif key == "external_his_levers_resolved": + ok = bool(audit) and "His lever:" not in audit and "third-party-data review" not in audit + + (passed if ok else failed).append(key) + + earned = sum(w for k, _, w in _MATURITY_CRITERIA if k in passed) + maturity = round(earned / total_weight * 100) if total_weight else 0 + return { + "maturity_pct": maturity, + "earned_weight": earned, + "total_weight": total_weight, + "passed": passed, + "failed": failed, + } + + +def _queue_lock(path: Path, timeout: float = 5.0) -> bool: + lock_dir = path.with_name(path.name + ".lock") + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + try: + lock_dir.mkdir() + return True + except FileExistsError: + time.sleep(0.1) + return False + + +def _queue_unlock(path: Path) -> None: + lock_dir = path.with_name(path.name + ".lock") + try: + lock_dir.rmdir() + except OSError: + pass + + +def _derive_stage(maturity: int | float) -> str: + if maturity < 30: + return "scaffold" + if maturity < 60: + return "building" + if maturity < 90: + return "maturing" + return "mature" + + +def _append_advancement_note(existing: str, old: int | float, new: int | float) -> str: + stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d") + receipt = f"Maturity {old}%->{new}% auto-advanced by publication-policy.py beat ({stamp})." + existing = (existing or "").strip() + if receipt in existing: + return existing + return f"{existing} {receipt}".strip() + + +def _advance_maturity(assessment: dict) -> dict: + assessed = assessment["maturity_pct"] + try: + data = json.loads(LADDER.read_text(encoding="utf-8")) if LADDER.exists() else {} + except (json.JSONDecodeError, OSError) as exc: + return {"bumped": False, "old": None, "new": assessed, "reason": f"cannot read ladder: {exc}"} + + entry = _publication_policy_entry(data) + if entry is None: + return {"bumped": False, "old": None, "new": assessed, "reason": "Publication Policy entry not found"} + + stored = entry.get("maturity", 0) + if not isinstance(stored, (int, float)): + stored = 0 + correct_stage = _derive_stage(stored) + stage_mismatch = entry.get("stage") != correct_stage + + if assessed <= stored and not stage_mismatch: + return { + "bumped": False, + "old": stored, + "new": assessed, + "reason": f"assessed ({assessed}%) <= stored ({stored}%)", + } + + if not _queue_lock(LADDER, timeout=5.0): + return {"bumped": False, "old": stored, "new": assessed, "reason": "queue lock busy — try next beat"} + + try: + data = json.loads(LADDER.read_text(encoding="utf-8")) + entry = _publication_policy_entry(data) + if entry is None: + return {"bumped": False, "old": stored, "new": assessed, "reason": "Publication Policy entry disappeared"} + + stored = entry.get("maturity", stored) + if not isinstance(stored, (int, float)): + stored = 0 + should_write = False + if assessed > stored: + entry["maturity"] = assessed + entry["note"] = _append_advancement_note(str(entry.get("note", "")), stored, assessed) + should_write = True + derived_stage = _derive_stage(entry.get("maturity", stored)) + if entry.get("stage") != derived_stage: + entry["stage"] = derived_stage + should_write = True + if not should_write: + return {"bumped": False, "old": stored, "new": assessed, "reason": "no change needed"} + + new_value = entry.get("maturity", stored) + fd, tmp = tempfile.mkstemp(dir=LADDER.parent, suffix=".tmp") + try: + with os.fdopen(fd, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + f.write("\n") + os.replace(tmp, LADDER) + except BaseException: + try: + os.unlink(tmp) + except OSError: + pass + raise + + if assessed > stored: + return { + "bumped": True, + "old": stored, + "new": new_value, + "reason": f"advanced from {stored}% to {new_value}%", + } + return { + "bumped": False, + "changed": True, + "old": stored, + "new": new_value, + "reason": f"corrected stage to {_derive_stage(new_value)}", + } + except (json.JSONDecodeError, OSError) as exc: + return {"bumped": False, "old": stored, "new": assessed, "reason": f"write error: {exc}"} + finally: + _queue_unlock(LADDER) + + def disposition(visibility: str, cls: str) -> tuple[str, str]: v = "public" if str(visibility).lower().startswith("pub") else "private" if cls not in DISPOSITIONS: @@ -409,7 +684,7 @@ def check(cond, msg): return fails -def _stamp(ok: bool, note: str = "") -> None: +def _stamp(ok: bool, note: str = "", assessment: dict | None = None, advancement: dict | None = None) -> None: try: LOGS.mkdir(exist_ok=True) STAMP.write_text( @@ -419,6 +694,8 @@ def _stamp(ok: bool, note: str = "") -> None: "sound": ok, "classes": list(CLASSES), "note": note, + "assessment": assessment or {}, + "advancement": advancement or {}, }, indent=2, ) @@ -432,13 +709,24 @@ def _stamp(ok: bool, note: str = "") -> None: def cmd_verify(_args) -> int: fails = _self_test() - _stamp(not fails, "verify") + assessment = _assess_maturity(fails) + if fails: + advancement = {"bumped": False, "old": None, "new": assessment["maturity_pct"], "reason": "self-test failed"} + else: + advancement = _advance_maturity(assessment) + _stamp(not fails, "verify", assessment, advancement) if fails: print("publication-policy UNSOUND:") for f in fails: print(f" - {f}") return 1 - print("publication-policy sound: redactor owner-scoped, matrix + classifier verified") + parts = [ + "publication-policy sound: redactor owner-scoped, matrix + classifier verified", + f"maturity={assessment['maturity_pct']}%", + ] + if advancement.get("bumped"): + parts.append(f"ADVANCED:{advancement['old']}->{advancement['new']}%") + print(" | ".join(parts)) return 0 @@ -450,7 +738,7 @@ def cmd_check(_args) -> int: def main(argv=None) -> int: ap = argparse.ArgumentParser(description="publication-policy — the disposition engine") - ap.add_argument("--verify", action="store_true", help="self-test predicate (exit 0 <=> sound)") + ap.add_argument("--verify", action="store_true", help="self-test predicate + maturity self-feed") ap.add_argument("--check", action="store_true", help="cheap stamp presence check") sub = ap.add_subparsers(dest="cmd")