From b70b5c7004b801ba82c579ed43fad3abadb233fd Mon Sep 17 00:00:00 2001 From: Mozi Date: Thu, 17 Sep 2026 11:43:24 -0400 Subject: [PATCH 1/3] feat: ask about contribution mode at install time, default off install.sh gains --contribution-mode off|on (default off) and, for on, a required --operator-ref. After a successful install the composer writes the contribution-state record at records/CONTRIB-STATE.md, rendered from the protean-ops ingredient's own installed template; it vendors no schema and never overwrites an existing record. Default off changes nothing else. on without --operator-ref exits 1, because a permission a flag alone can widen has no audit trail; on without the ops ingredient in the selection exits 5 rather than inventing the schema. The choice is a flag, not an environment variable, so it is recorded and reviewable. The pins in kit.lock.json are not re-pointed here: the matching ingredient changes travel on their own branches and the re-pin follows their release tags. --- .github/workflows/verify.yml | 3 + CHANGELOG.md | 29 ++++++ README.md | 45 ++++++++++ build/install.py | 121 ++++++++++++++++++++++++- install.sh | 4 + tests/test_contribution_mode.py | 153 ++++++++++++++++++++++++++++++++ tests/test_kit_lock.py | 7 +- 7 files changed, 360 insertions(+), 2 deletions(-) create mode 100644 tests/test_contribution_mode.py diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index dce3d96..1626389 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -41,3 +41,6 @@ jobs: - name: Run the composition lock tests run: python3 -m unittest tests.test_kit_lock + + - name: Run the contribution-mode tests + run: python3 -m unittest tests.test_contribution_mode diff --git a/CHANGELOG.md b/CHANGELOG.md index d214451..e2e7aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,35 @@ outside this repository. ## Unreleased +- **What:** add an install-time contribution-mode toggle to the composer. The installer takes + `--contribution-mode off|on` (default `off`) and, when switching on, a required + `--operator-ref `. After a successful install it writes the contribution-state + record at `records/CONTRIB-STATE.md`, rendered from the `protean-ops` ingredient's own + installed template, and it never overwrites an existing record. +- **Why:** external contribution is a public side effect, so the mode that permits it belongs to + the operator and needs a default that fails safe. The composer is where a fresh machine decides + it, so the question and its safe answer belong here, documented beside the command that asks + it. +- **Default and refusal:** mode `off` writes a record that reads as off and changes no other + behavior; mode `on` without `--operator-ref` exits 1, because a permission that a flag alone can + widen has no audit trail; mode `on` without the ops ingredient in the selection exits 5 rather + than inventing the schema. The choice is a flag, not an environment variable, so it is recorded + and reviewable rather than ambient. +- **Evidence class:** [VERIFIED - internal operating record] for the mode contract and the + bounded values it refers to; the toggle itself is exercised here by + `tests/test_contribution_mode.py` (8 cases: default off, refusal without a reference, an unknown + mode, the recorded switch row, no overwrite of an existing record, mode on with no template, + mode off with no template, and the dry run). +- **Verification:** `python3 -m unittest tests.test_contribution_mode` and + `python3 -m unittest tests.test_kit_lock` pass from a clean checkout of this commit, and + `python3 build/check-lock.py kit.lock.json` still passes. The workflow gains one step that runs + the new test file. +- **Open item:** the pins in `kit.lock.json` are not re-pointed by this change. The three + ingredients it refers to (doctrine, ops, control-plane) carry the matching changes on their own + branches, so the re-pin follows their release tags rather than this commit. Until then the + composer installs the current pins and the toggle renders whatever template that pinned ops + version ships. + - **What:** add the composition lock and the installer. `kit.lock.json` pins six standalone ingredients by annotated tag, peeled commit SHA, and tree hash (`protean-tree-v1`). `install.sh` (a thin entrypoint over `build/install.py`) diff --git a/README.md b/README.md index 3bf8926..40fb9a1 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,50 @@ planned writes, and it writes nothing. bash install.sh --all --target ./protean-installed --dry-run ``` +### Contribution mode at install time + +The installer asks one question about continual contribution mode and answers it +with a safe default. External contribution is **off** unless you ask for it: + +``` +bash install.sh --all --target ./protean-installed +bash install.sh --all --target ./protean-installed --contribution-mode on --operator-ref +``` + +- **Default, and the safe one.** Without the flag the mode is recorded as off. + Nothing else changes; the run behaves exactly as it does today. +- **Turning it on is a recorded decision.** `--contribution-mode on` is refused + without `--operator-ref `, because a permission that can be widened + by a flag alone has no audit trail. The reference is recorded in the state + record as a `switch` row. +- **No environment variable.** The choice is a command-line flag, not a + `HERMES_*` setting: it is a one-time install decision, and a variable that sits + in a shell profile is neither recorded nor reviewable. +- **The composer still vendors nothing.** The record is rendered from the + `protean-ops` ingredient's own template, installed beside it at + `records/CONTRIB-STATE.md`. When the ops ingredient is not in the selection, + mode off writes no record and reports it, and mode on fails closed (exit 5) + rather than inventing a schema. An existing record is never overwritten, so a + re-install cannot silently widen a permission. + +What the recorded mode means once the kit is running: + +| | internal mode | external mode | +|---|---|---| +| default | always on, no toggle | off | +| what it governs | work on a repository the operator administers | work whose target is not such a repository | +| while off | not affected | no remote write of any kind, including a push to a fork; a local branch, commit, test run, and rendered draft are allowed | +| enforcement | the contribution-state gate, asserted, never assumed | the same gate, fail closed on an absent or unreadable record | + +Four bounded values belong to the gate, not to this document, and the gate +recomputes all of them from timestamped rows: at most two live contribution lanes +of which one may be external; the per-window rate table for pull requests, issues, +comments, and reviews; the per-thread write limit and the per-repository burst +detector; and a write window of 05:00-21:00 in the operator's local zone, outside +which an external lane still runs to a ready draft but writes nothing remotely. +The kill switch is one field, `external_contrib`, in the state record, and a +missing record reads as off. + The composer holds the lock, the installer, the documentation, and the gates. It **contains no ingredient payload and no submodule**: composition is manifest-only. Each ingredient is fetched from its pinned tag, verified against the tag's peeled @@ -350,6 +394,7 @@ success zero. python3 build/check-lock.py kit.lock.json --verify python3 build/check-ingredient-contract.py --lock kit.lock.json --verify python3 -m unittest tests.test_kit_lock +python3 -m unittest tests.test_contribution_mode ``` `check-lock.py` validates the lock and, with `--verify`, recomputes every pin's diff --git a/build/install.py b/build/install.py index 2279437..7571175 100644 --- a/build/install.py +++ b/build/install.py @@ -6,6 +6,15 @@ tags and writes them into the target. It copies no ingredient payload into the composer and uses no git submodules. +Contribution mode is an install-time choice with a safe default: + + bash install.sh --all --target --contribution-mode off|on [--operator-ref ] + + off (the default) records the mode as off; on requires --operator-ref and is + refused without it. The composer vendors no schema: when the selection installs + the ops ingredient, the record is rendered from that ingredient's own template + and written beside it. An existing record is never overwritten. + bash install.sh --all --target bash install.sh --ingredient --target bash install.sh --ingredient --no-deps --target @@ -28,6 +37,7 @@ Stdlib only. No third-party import. """ +import datetime import hashlib import json import os @@ -44,6 +54,9 @@ ALGORITHM = "protean-tree-v1" CACHE_ENV = "PROTEAN_CACHE" STAGING = ".protean-staging" +STATE_REL = "records/CONTRIB-STATE.md" +STATE_TMPL_REL = "records/CONTRIB-STATE.md.tmpl" +CONTRIBUTION_MODES = ("off", "on") class Failure(Exception): @@ -78,7 +91,8 @@ def usage(): def parse_args(argv): opts = {"all": False, "ingredients": [], "no_deps": False, "offline": False, - "dry_run": False, "target": None, "lock": LOCK_FILE, "cache": None} + "dry_run": False, "target": None, "lock": LOCK_FILE, "cache": None, + "contribution_mode": "off", "operator_ref": None} i = 0 while i < len(argv): arg = argv[i] @@ -120,6 +134,22 @@ def parse_args(argv): raise Failure(1, "usage: --cache needs a directory") opts["cache"] = argv[i + 1] i += 2 + elif arg == "--contribution-mode": + if i + 1 >= len(argv): + raise Failure(1, "usage: --contribution-mode needs off or on") + opts["contribution_mode"] = argv[i + 1].strip().lower() + i += 2 + elif arg.startswith("--contribution-mode="): + opts["contribution_mode"] = arg.split("=", 1)[1].strip().lower() + i += 1 + elif arg == "--operator-ref": + if i + 1 >= len(argv): + raise Failure(1, "usage: --operator-ref needs a reference") + opts["operator_ref"] = argv[i + 1] + i += 2 + elif arg.startswith("--operator-ref="): + opts["operator_ref"] = arg.split("=", 1)[1] + i += 1 else: raise Failure(1, "usage: unknown flag: %s" % arg) if not opts["all"] and not opts["ingredients"]: @@ -130,6 +160,13 @@ def parse_args(argv): raise Failure(1, "usage: --no-deps applies to a single-ingredient selection") if not opts["dry_run"] and not opts["target"]: raise Failure(1, "usage: --target is required (or use --dry-run)") + if opts["contribution_mode"] not in CONTRIBUTION_MODES: + raise Failure(1, "usage: --contribution-mode must be off or on, got %r" + % opts["contribution_mode"]) + if opts["contribution_mode"] == "on" and not opts["operator_ref"]: + raise Failure(1, "--contribution-mode on requires --operator-ref : " + "turning external contribution on is a recorded operator decision, " + "and an unrecorded switch is refused") return opts @@ -538,6 +575,76 @@ def install_one(entry, cache, opts, plan, entry_dir): return written, gates +# --------------------------------------------------------------------------- +# Contribution mode (install-time toggle; the schema belongs to protean-ops) +# --------------------------------------------------------------------------- + +def utc_stamp(): + return datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def row_cells(line): + if not line.strip().startswith("|"): + return [] + return [c.strip() for c in line.strip().strip("|").split("|")] + + +def set_header_value(text, key, value): + out = [] + for line in text.splitlines(): + cells = row_cells(line) + if len(cells) == 2 and cells[0].lower() == key.lower(): + line = "| %s | %s |" % (cells[0], value) + out.append(line) + return "\n".join(out) + "\n" + + +def contribution_switch_row(now, mode, operator_ref): + stamp = now.replace("-", "").replace(":", "").replace("T", "-").replace("Z", "") + return ("| cc-%s | switch | - | - | - | external | approval %s | external_contrib=%s " + "operator reference %s | installer | %s | - |" + % (stamp, operator_ref, mode, operator_ref, now)) + + +def contribution_state(target, mode, operator_ref): + """Write the contribution-state record. Returns (relative path, note). + + The schema belongs to the ops ingredient, so this renders that ingredient's + own installed template instead of carrying a copy of it. An existing record + is left untouched: an install never silently widens a permission. + """ + dest = os.path.join(target, STATE_REL) + if os.path.exists(dest): + return None, ("%s already exists and was left unchanged; an install never overwrites " + "an existing contribution-state record" % STATE_REL) + template = os.path.join(target, STATE_TMPL_REL) + if not os.path.isfile(template): + if mode == "on": + raise Failure(5, "contribution mode 'on' was requested, but %s is not installed, " + "so the contribution-state record cannot be rendered from its " + "template" % STATE_TMPL_REL) + return None, ("%s not written: %s is not in this selection" + % (STATE_REL, STATE_TMPL_REL)) + with open(template, encoding="utf-8") as fh: + text = fh.read() + now = utc_stamp() + reason = "installer: contribution mode %s" % mode + if operator_ref: + reason += ", operator reference %s" % operator_ref + text = set_header_value(text, "internal_contrib", "on") + text = set_header_value(text, "external_contrib", mode) + text = set_header_value(text, "read_at", now) + text = set_header_value(text, "last_change", "%s %s" % (now, reason)) + if mode == "on": + row = contribution_switch_row(now, mode, operator_ref) + text = "\n".join(row if "empty state" in line.lower() else line + for line in text.splitlines()) + "\n" + os.makedirs(os.path.dirname(dest), exist_ok=True) + with open(dest, "w", encoding="utf-8") as fh: + fh.write(text) + return STATE_REL, None + + # --------------------------------------------------------------------------- # Driver # --------------------------------------------------------------------------- @@ -549,6 +656,9 @@ def dry_run(lock, order, plan, cache, offline): info("dependency mode: %s" % ("degraded (--no-deps)" if plan["no_deps"] else "full")) info("install order: %s" % ", ".join(order)) info("network mode: %s" % ("offline (cache only)" if offline else "online")) + info("contribution mode: %s%s" % (plan["contribution_mode"], + "" if plan["contribution_mode"] == "off" + else " (operator reference %s)" % plan["operator_ref"])) info("planned writes (target-relative), per ingredient:") for slug in order: entry = plan["entries"][slug] @@ -590,6 +700,8 @@ def main(argv): "no_deps": opts["no_deps"], "degraded": degraded, "entries": seen, + "contribution_mode": opts["contribution_mode"], + "operator_ref": opts["operator_ref"], } cache = cache_root(opts) if opts["dry_run"]: @@ -630,6 +742,13 @@ def main(argv): info(" installed: %s" % ", ".join(installed)) info(" selection: %s" % plan["selection_label"]) info(" read-back: every written file compared to its cached blob") + info(" contribution mode: %s" % opts["contribution_mode"]) + state_rel, state_note = contribution_state(plan["target"], opts["contribution_mode"], + opts["operator_ref"]) + if state_rel: + info(" contribution state: wrote %s" % state_rel) + elif state_note: + warnings.append(state_note) for message in warnings: info(" note: %s" % message) info(" next: read each installed ingredient's README.md under the target") diff --git a/install.sh b/install.sh index 6256d73..43bbb7b 100755 --- a/install.sh +++ b/install.sh @@ -11,6 +11,10 @@ # ingredient from its annotated tag, verifies the tag's peeled commit sha and the # ingredient tree hash, installs it, and reads every written file back. # +# Contribution mode is an install-time choice with a safe default: +# --contribution-mode off|on (default off; on requires --operator-ref) +# The record is rendered from the ops ingredient's own installed template. +# # Exit codes: 0 ok, 1 usage/input, 2 lock invalid, 3 dependency, 4 integrity, # 5 install, 6 post-install verification. A run that installs part of the # selection exits non-zero: there is no partial-success zero. diff --git a/tests/test_contribution_mode.py b/tests/test_contribution_mode.py new file mode 100644 index 0000000..26216f7 --- /dev/null +++ b/tests/test_contribution_mode.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Tests for the install-time contribution-mode toggle. + +Everything runs offline against fixture repositories built in a temporary +directory, reusing the fixtures from tests/test_kit_lock.py. No network access, +no third-party import. + +Run: python3 -m unittest tests.test_contribution_mode +""" +import os +import sys +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from test_kit_lock import (FixtureCase, INSTALL, build_ingredient, lock_entry, # noqa: E402 + run, warm_cache, write_lock) + +OPS_SLUG = "protean-ops-fixture" +STATE_TEMPLATE = """# CONTRIB-STATE - continual contribution mode (schema template) + +## Header (fixed keys) + +| key | value | +|---|---| +| schema_version | 1 | +| internal_contrib | on | +| external_contrib | off | +| read_at | - | +| mode_epoch | - | +| last_change | - | + +## Rows (append-only; fixed columns) + +| row id | kind | repo | action | thread | mode | authority | detail | lane / session | time | evidence | +|---|---|---|---|---|---|---|---|---|---|---| +| (empty state - no rows recorded) | - | - | - | - | - | - | - | - | - | - | +""" + + +def header_of(path): + header = {} + with open(path, encoding="utf-8") as fh: + for line in fh: + cells = [c.strip() for c in line.strip().strip("|").split("|")] + if len(cells) == 2 and line.strip().startswith("|"): + header[cells[0].lower()] = cells[1] + return header + + +class ContributionModeCase(FixtureCase): + def ops_fixture(self, slug=OPS_SLUG, with_template=True, **kwargs): + extra = {"records/CONTRIB-STATE.md.tmpl": STATE_TEMPLATE} if with_template else None + fixture = build_ingredient(self.repos, slug, + install_targets=["records", "gates/" + slug], + extra_files=extra, **kwargs) + warm_cache(self.cache, slug, fixture) + entry = lock_entry(slug, fixture) + return fixture, write_lock(os.path.join(self.base, "kit.lock.json"), [entry]) + + def install(self, target, *args, lock=None): + return run(INSTALL, "--all", "--target", target, "--offline", "--cache", self.cache, + "--lock", lock, *args) + + +class TestContributionMode(ContributionModeCase): + def test_the_default_is_off_and_the_record_is_written(self): + _fixture, lock = self.ops_fixture() + target = os.path.join(self.base, "installed") + result = self.install(target, lock=lock) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + record = os.path.join(target, "records", "CONTRIB-STATE.md") + self.assertTrue(os.path.isfile(record), result.stdout) + header = header_of(record) + self.assertEqual(header["external_contrib"], "off") + self.assertEqual(header["internal_contrib"], "on") + self.assertNotEqual(header["read_at"], "-") + self.assertIn("contribution mode: off", result.stdout) + self.assertIn("contribution state: wrote", result.stdout) + + def test_on_without_an_operator_reference_is_refused(self): + _fixture, lock = self.ops_fixture() + target = os.path.join(self.base, "installed") + result = self.install(target, "--contribution-mode", "on", lock=lock) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("operator-ref", result.stdout + result.stderr) + self.assertFalse(os.path.exists(os.path.join(target, "records", "CONTRIB-STATE.md"))) + + def test_an_unknown_mode_is_a_usage_error(self): + _fixture, lock = self.ops_fixture() + result = self.install(os.path.join(self.base, "installed"), + "--contribution-mode", "maybe", lock=lock) + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertIn("off or on", result.stdout + result.stderr) + + def test_on_with_an_operator_reference_records_the_switch_row(self): + _fixture, lock = self.ops_fixture() + target = os.path.join(self.base, "installed") + result = self.install(target, "--contribution-mode", "on", "--operator-ref", "OP-9", + lock=lock) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + record = os.path.join(target, "records", "CONTRIB-STATE.md") + header = header_of(record) + self.assertEqual(header["external_contrib"], "on") + with open(record, encoding="utf-8") as fh: + body = fh.read() + self.assertIn("| switch |", body) + self.assertIn("OP-9", body) + self.assertNotIn("empty state", body) + + def test_an_existing_record_is_never_overwritten(self): + _fixture, lock = self.ops_fixture() + target = os.path.join(self.base, "installed") + first = self.install(target, lock=lock) + self.assertEqual(first.returncode, 0, first.stdout + first.stderr) + record = os.path.join(target, "records", "CONTRIB-STATE.md") + with open(record, encoding="utf-8") as fh: + before = fh.read() + second = self.install(target, "--contribution-mode", "on", "--operator-ref", "OP-9", + lock=lock) + self.assertEqual(second.returncode, 0, second.stdout + second.stderr) + self.assertIn("left unchanged", second.stdout) + with open(record, encoding="utf-8") as fh: + self.assertEqual(before, fh.read()) + + def test_mode_on_without_the_ops_template_is_exit_5(self): + _fixture, lock = self.ops_fixture(with_template=False) + result = self.install(os.path.join(self.base, "installed"), + "--contribution-mode", "on", "--operator-ref", "OP-9", lock=lock) + self.assertEqual(result.returncode, 5, result.stdout + result.stderr) + self.assertIn("template", result.stdout + result.stderr) + + def test_mode_off_without_the_ops_template_is_a_reported_note(self): + _fixture, lock = self.ops_fixture(with_template=False) + target = os.path.join(self.base, "installed") + result = self.install(target, lock=lock) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("not in this selection", result.stdout) + self.assertFalse(os.path.exists(os.path.join(target, "records", "CONTRIB-STATE.md"))) + + def test_dry_run_reports_the_mode_and_writes_nothing(self): + _fixture, lock = self.ops_fixture() + target = os.path.join(self.base, "dry-target") + result = run(INSTALL, "--all", "--target", target, "--dry-run", "--cache", self.cache, + "--lock", lock, "--contribution-mode", "on", "--operator-ref", "OP-9") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("contribution mode: on", result.stdout) + self.assertIn("no files written", result.stdout) + self.assertFalse(os.path.exists(target)) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test_kit_lock.py b/tests/test_kit_lock.py index 6e74ad3..4f0df2b 100644 --- a/tests/test_kit_lock.py +++ b/tests/test_kit_lock.py @@ -46,7 +46,7 @@ def git(args, cwd): def build_ingredient(base, slug, version="1.0.0", gate_exit=0, requires=None, recommends=None, install_targets=None, contract=CONTRACT, - tree_tweak=None): + tree_tweak=None, extra_files=None): """A real fixture ingredient repository with an annotated tag.""" path = os.path.join(base, slug) os.makedirs(path, exist_ok=True) @@ -87,6 +87,11 @@ def build_ingredient(base, slug, version="1.0.0", gate_exit=0, requires=None, if tree_tweak: with open(os.path.join(path, tree_tweak), "w", encoding="utf-8") as fh: fh.write("extra\n") + for rel, content in (extra_files or {}).items(): + dest = os.path.join(path, rel) + os.makedirs(os.path.dirname(dest), exist_ok=True) + with open(dest, "w", encoding="utf-8") as fh: + fh.write(content) git(["add", "-A"], cwd=path) git(IDENTITY + ["commit", "--quiet", "-m", "%s %s" % (slug, version)], cwd=path) git(IDENTITY + ["tag", "-a", "v" + version, "-m", "%s %s" % (slug, version)], cwd=path) From 940e7944b0592101f768a64d4fdbea501447f391 Mon Sep 17 00:00:00 2001 From: Mozi Date: Thu, 17 Sep 2026 12:18:28 -0400 Subject: [PATCH 2/3] ci: migrate verify-all.py to the live gates after the kit partition The partition (b95cf39) deleted 7 gate scripts whose functions moved into the ingredient repos' own gates, but verify-all.py still invoked them, so the release gate runner failed 7/10 on main and on this PR alike. - verify-all.py: drop the dead gates (sweep-gate, review-gate, surface-scan, check-artifact-contract, preflight/check, report/check, generate.py --help); keep check-lock, check-ingredient-contract, test_kit_lock; add test_contribution_mode. Update the docstring gate list to the 4 live gates. - verify.yml: remove the dead 'Verify surface-scan passes' step. Zero behavior change outside CI: install.py, tests, toggle paths, lockfile, docs untouched. --- .github/workflows/verify.yml | 4 --- build/verify-all.py | 54 +++++++----------------------------- 2 files changed, 10 insertions(+), 48 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 1626389..d973953 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -29,10 +29,6 @@ jobs: chmod +x build/verify-all.py python3 build/verify-all.py - - name: Verify surface-scan passes - run: | - python3 build/surface-scan.py - - name: Verify the composition lock run: python3 build/check-lock.py kit.lock.json diff --git a/build/verify-all.py b/build/verify-all.py index aee1049..576908f 100644 --- a/build/verify-all.py +++ b/build/verify-all.py @@ -7,16 +7,10 @@ ORDER (this list is the gate list; the composer holds no separate release-gates document, and this docstring is authoritative): - 1. sweep-gate.py — extraction source verification - 2. review-gate.py — semantic sign-off enforcement - 3. surface-scan.py — multi-surface leak detector - 4. check-artifact-contract.py self-test - 5. preflight/check.py self-test - 6. report/check.py self-test - 7. composition lock validity (check-lock.py, static) - 8. ingredient contract (check-ingredient-contract.py, static) - 9. composition lock tests (tests/test_kit_lock.py) - 10. fresh-clone test (generate.py fork-dryrun check) + 1. composition lock validity (check-lock.py, static) + 2. ingredient contract (check-ingredient-contract.py, static) + 3. composition lock tests (tests/test_kit_lock.py) + 4. contribution mode tests (tests/test_contribution_mode.py) Exit: 0 = all gates pass; 1 = any gate failed. """ @@ -76,51 +70,23 @@ def main(): gates = [] - # 1. sweep-gate - gates.append(("sweep-gate.py", [sys.executable, os.path.join(HERE, "sweep-gate.py")])) - - # 2. review-gate - gates.append(("review-gate.py", [sys.executable, os.path.join(HERE, "review-gate.py")])) - - # 3. surface-scan - gates.append(("surface-scan.py", [sys.executable, os.path.join(HERE, "surface-scan.py")])) - - # 4. check-artifact-contract self-test - gates.append(("check-artifact-contract self-test", - [sys.executable, os.path.join(HERE, "check-artifact-contract.py"), "--self-test"])) - - # 5. preflight/check.py self-test - gates.append(("preflight/check self-test", - [sys.executable, os.path.join(HERE, "preflight", "check.py"), "--selftest"])) - - # 6. report/check.py self-test - gates.append(("report/check self-test", - [sys.executable, os.path.join(HERE, "report", "check.py"), "--selftest"])) - - # 7. composition lock validity + # 1. composition lock validity gates.append(("check-lock.py", [sys.executable, os.path.join(HERE, "check-lock.py"), os.path.join(ROOT, "kit.lock.json")])) - # 8. ingredient contract (reports pinned trees it cannot read, never invents one) + # 2. ingredient contract (reports pinned trees it cannot read, never invents one) gates.append(("check-ingredient-contract.py", [sys.executable, os.path.join(HERE, "check-ingredient-contract.py"), "--lock", os.path.join(ROOT, "kit.lock.json")])) - # 9. composition lock tests + # 3. composition lock tests gates.append(("composition lock tests", [sys.executable, "-m", "unittest", "tests.test_kit_lock"])) - # 10. fresh-clone test (fork-dryrun.sh) - fork_dryrun = os.path.join(HERE, "fork-dryrun.sh") - if os.path.isfile(fork_dryrun): - # Use current repo as source and 'main' as branch; no private paths. - gates.append(("fresh-clone fork-dryrun", - ["bash", fork_dryrun, ROOT, "main"])) - else: - # Alternative: verify generate.py works in fork mode - gates.append(("fresh-clone generate test", - [sys.executable, os.path.join(HERE, "generate.py"), "--help"])) + # 4. contribution mode tests + gates.append(("contribution mode tests", + [sys.executable, "-m", "unittest", "tests.test_contribution_mode"])) # Run all gates results = [] From bcfe50e2d0626c6c13c9e665a9091e75fce30fd7 Mon Sep 17 00:00:00 2001 From: Proteus Date: Thu, 17 Sep 2026 12:32:40 -0400 Subject: [PATCH 3/3] chore: re-pin continual contribution ingredients --- kit.lock.json | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/kit.lock.json b/kit.lock.json index d3d6246..15f228e 100644 --- a/kit.lock.json +++ b/kit.lock.json @@ -25,9 +25,9 @@ { "slug": "protean-doctrine", "repo": "https://github.com/aska-digital/protean-doctrine", - "ref": "refs/tags/v1.0.2", - "sha": "f5d6f795181522249b6e8d18d483f6f2c85058a9", - "version": "1.0.2", + "ref": "refs/tags/v1.1.0", + "sha": "f72d6da9be8bdf11dfa300ace7cbd47e2453fa79", + "version": "1.1.0", "contract": "The operating doctrine: the default pipeline, role delegation map, handoff protocol, QA gates, and external-writing discipline.", "kind": "skills", "installTargets": [ @@ -38,8 +38,8 @@ "requires": [], "recommends": [], "integrity": { - "fileCount": 18, - "treeSha256": "faeecb7ce747b2c7ad205d97f947e198e6240362908263fd90365a64f3463b27", + "fileCount": 20, + "treeSha256": "0d751b7b5227d8543e899de20ebd5e11a91b2a8854831df04b24a6ea28ab5a62", "algorithm": "protean-tree-v1" }, "gates": [ @@ -61,9 +61,9 @@ { "slug": "protean-ops", "repo": "https://github.com/aska-digital/protean-ops", - "ref": "refs/tags/v1.0.0", - "sha": "f2abc3bf6a7e0925d26e14606d0645a0ea765bad", - "version": "1.0.0", + "ref": "refs/tags/v1.1.0", + "sha": "0b78ff77a2ba32c1b0d1c6464a18ad36053ed303", + "version": "1.1.0", "contract": "The dispatch/rotation/inflight ops kit: append-only record schemas and the deterministic gates that check them.", "kind": "records", "installTargets": [ @@ -74,8 +74,8 @@ "requires": [], "recommends": [], "integrity": { - "fileCount": 48, - "treeSha256": "09170568f2b0b112745a9f4c9bd13f300d61f6c523fefae06a17aff662e8789c", + "fileCount": 61, + "treeSha256": "05daa2e9cf5bec2fca085f79a1026b38f3bae7f892666678847edb291b04fc6f", "algorithm": "protean-tree-v1" }, "gates": [ @@ -114,6 +114,12 @@ "cwd": ".", "postInstall": true, "cmd": "python3 scripts/protean-ops/check-decision-report.py records/examples/valid/decision-report.html --evidence records/examples/valid/decision-evidence.md" + }, + { + "name": "op-contrib-state", + "cwd": ".", + "postInstall": true, + "cmd": "python3 scripts/protean-ops/check-contrib-state.py records/examples/valid/CONTRIB-STATE.md" } ], "open": [] @@ -260,9 +266,9 @@ { "slug": "protean-control-plane", "repo": "https://github.com/aska-digital/protean-control-plane", - "ref": "refs/tags/v1.0.0", - "sha": "f593dd1f816e8b348e2a6f5b37b2da6392f9b47a", - "version": "1.0.0", + "ref": "refs/tags/v1.1.0", + "sha": "ca71a6686710e713a3dc8f2fad666b892c300d37", + "version": "1.1.0", "contract": "The control-plane skill pack: one canonical procedure home that routes a request to the minimum skill bundle, with the gate table and failure policy.", "kind": "skills", "installTargets": [ @@ -279,8 +285,8 @@ "protean-sym2p" ], "integrity": { - "fileCount": 17, - "treeSha256": "c879d69ad8283bc40879334dbae415b4bb022f6d4e6dcb8a7f40aa82282e3fbd", + "fileCount": 19, + "treeSha256": "5a17769608d90942b246aa5e28657708c914704f958bb09b625d1e2f8630d323", "algorithm": "protean-tree-v1" }, "gates": [