From e32b214da64c47e4403047ec0807630b7eb373cc Mon Sep 17 00:00:00 2001 From: infotradescout Date: Thu, 17 Sep 2026 12:18:36 -0500 Subject: [PATCH 1/6] Add fail-closed product delivery evidence checks and bounded discovery admission --- .../scripts/delivery_acceptance.py | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 skills/selective-intelligence/scripts/delivery_acceptance.py diff --git a/skills/selective-intelligence/scripts/delivery_acceptance.py b/skills/selective-intelligence/scripts/delivery_acceptance.py new file mode 100644 index 0000000..80dba80 --- /dev/null +++ b/skills/selective-intelligence/scripts/delivery_acceptance.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""Fail-closed product delivery checks; not a substitute for observing the product. + +Inputs are adapter-produced evidence. Hashes bind reports to files and revisions; +they do not establish that the report or a subjective design judgment is true. +The strongest verdict is ready_for_review, never user_accepted. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import re +from pathlib import Path +from typing import Any + +SCHEMA = "si.product_delivery.v1" +SHA = re.compile(r"^[a-f0-9]{64}$") +MODES = {"automatic", "manual", "optional_manual", "interactive"} +SCOPES = {"synthetic", "native", "live"} +KINDS = {"workflow", "visual", "restart", "safety", "launch"} +MAX_JSON = 2 * 1024 * 1024 +MAX_ARTIFACT = 32 * 1024 * 1024 + + +def digest(value: Any) -> str: + return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":"), + allow_nan=False).encode()).hexdigest() + + +def read_json(path: Path) -> Any: + if not path.is_file() or path.is_symlink() or path.stat().st_size > MAX_JSON: + raise ValueError("Expected a bounded, regular JSON file") + return json.loads(path.read_text(encoding="utf-8"), + parse_constant=lambda value: (_ for _ in ()).throw(ValueError(value))) + + +def _text(value: Any) -> bool: + return isinstance(value, str) and bool(value.strip()) and len(value) <= 4096 + + +def contract_errors(contract: Any) -> list[str]: + errors: list[str] = [] + if not isinstance(contract, dict) or contract.get("schema") != SCHEMA: + return ["contract_schema_invalid"] + for name in ("project", "source_revision", "intent_source"): + if not _text(contract.get(name)): + errors.append("contract_" + name + "_missing") + owner = contract.get("canonical") + if not isinstance(owner, dict) or any(not _text(owner.get(k)) for k in + ("code_root", "entrypoint", "data_owner")): + errors.append("canonical_owner_missing") + requirements = contract.get("requirements") + if not isinstance(requirements, list) or not requirements or len(requirements) > 100: + return errors + ["requirements_missing_or_unbounded"] + seen: set[str] = set() + primary = 0 + for requirement in requirements: + if not isinstance(requirement, dict): + errors.append("requirement_invalid") + continue + rid = requirement.get("id") + if not _text(rid) or rid in seen: + errors.append("requirement_id_invalid_or_duplicate") + else: + seen.add(rid) + if not isinstance(requirement.get("mode"), str) or requirement["mode"] not in MODES or not _text(requirement.get("outcome")): + errors.append("requirement_semantics_missing") + if type(requirement.get("primary")) is not bool: + errors.append("requirement_primary_flag_missing") + primary += requirement.get("primary") is True + scopes = requirement.get("accepted_scopes") + if not isinstance(scopes, list) or not scopes or any(not isinstance(s, str) or s not in SCOPES for s in scopes): + errors.append("requirement_evidence_scope_missing") + kinds = requirement.get("required_evidence") + if not isinstance(kinds, list) or not kinds or any(not isinstance(k, str) or k not in KINDS for k in kinds): + errors.append("requirement_evidence_kind_missing") + if requirement.get("primary") and (not isinstance(kinds, list) or "workflow" not in kinds): + errors.append("primary_requires_workflow_evidence") + if not primary: + errors.append("primary_user_job_missing") + if type(contract.get("user_facing")) is not bool: + errors.append("surface_scope_missing") + if type(contract.get("requires_installation")) is not bool: + errors.append("installation_scope_missing") + return errors + + +def _artifact(reference: Any, root: Path) -> bool: + if not isinstance(reference, dict) or not _text(reference.get("path")): + return False + if not isinstance(reference.get("sha256"), str) or not SHA.fullmatch(reference["sha256"]): + return False + relative = Path(reference["path"]) + if relative.is_absolute() or ".." in relative.parts or "\\" in reference["path"]: + return False + target = root / relative + try: + if target.is_symlink() or not target.resolve().is_relative_to(root.resolve()): + return False + if not target.is_file() or not 0 < target.stat().st_size <= MAX_ARTIFACT: + return False + return hashlib.sha256(target.read_bytes()).hexdigest() == reference["sha256"] + except (OSError, RuntimeError): + return False + + +def evaluate(contract: Any, evidence: Any, artifact_root: Path) -> dict[str, Any]: + """Validate a delivery packet without reading or changing a product's save data.""" + blockers = contract_errors(contract) + result = {"schema": "si.product_delivery.verdict.v1", "passed": False, + "verdict": "needs_repair", "user_acceptance": "not_claimed", + "blockers": blockers, "evaluated_requirements": [], + "boundary": "Evidence integrity and coverage checks, not proof of subjective quality or client adoption."} + if blockers: + return result + result["contract_hash"] = digest(contract) + if not isinstance(evidence, dict) or evidence.get("schema") != "si.product_delivery.evidence.v1": + blockers.append("evidence_schema_invalid") + return result + if evidence.get("contract_hash") != result["contract_hash"]: + blockers.append("evidence_not_bound_to_current_intent") + if evidence.get("source_revision") != contract["source_revision"]: + blockers.append("evidence_not_bound_to_current_source") + if evidence.get("canonical") != contract["canonical"]: + blockers.append("evidence_wrong_product_owner") + rejections = evidence.get("unresolved_user_rejections") + if not isinstance(rejections, list) or rejections: + blockers.append("user_rejection_unresolved_or_unreported") + targets = evidence.get("active_targets") + if not isinstance(targets, list) or targets != [contract["canonical"]]: + blockers.append("competing_or_unverified_delivery_targets") + if contract["requires_installation"]: + install = evidence.get("installation") or {} + if (not isinstance(install, dict) or install.get("running") is not True + or install.get("source_revision") != contract["source_revision"] + or install.get("entrypoint") != contract["canonical"]["entrypoint"] + or not _artifact(install.get("artifact"), artifact_root)): + blockers.append("intended_installation_not_verified") + proofs = evidence.get("proofs") + if not isinstance(proofs, list) or len(proofs) > 500: + blockers.append("proofs_missing_or_unbounded") + return result + valid = [] + seen = set() + for proof in proofs: + if not isinstance(proof, dict): + blockers.append("invalid_proof") + continue + pid = proof.get("id") + if not _text(pid) or pid in seen: + blockers.append("proof_id_invalid_or_duplicate") + continue + seen.add(pid) + integrity = (proof.get("status") == "passed" and proof.get("executed") is True + and proof.get("source_revision") == contract["source_revision"] + and proof.get("contract_hash") == result["contract_hash"] + and isinstance(proof.get("scope"), str) and proof["scope"] in SCOPES + and isinstance(proof.get("kind"), str) and proof["kind"] in KINDS + and proof.get("entrypoint") == contract["canonical"]["entrypoint"] + and _text(proof.get("runner")) and _text(proof.get("target")) + and _artifact(proof.get("artifact"), artifact_root)) + if integrity: + valid.append(proof) + for requirement in contract["requirements"]: + rid = requirement["id"] + matching = [p for p in valid if p.get("requirement_id") == rid + and p.get("scope") in requirement["accepted_scopes"] + and p.get("mode") == requirement["mode"]] + kinds = {p["kind"] for p in matching} + missing = sorted(set(requirement["required_evidence"]) - kinds) + failures = [] + if missing: + failures.append("missing:" + ",".join(missing)) + if requirement["mode"] == "automatic": + workflows = [p for p in matching if p["kind"] == "workflow"] + if not workflows or not any(type(p.get("manual_entry_steps")) is int + and p["manual_entry_steps"] == 0 + and p.get("trigger_observed") is True + and p.get("result_observed") is True for p in workflows): + failures.append("automatic_workflow_not_proved_without_manual_entry") + if requirement["primary"] and contract["user_facing"]: + reviews = [p for p in matching if p["kind"] == "visual"] + if not reviews or not any(p.get("reviewed") is True + and p.get("blocking_findings") == [] + and _text(p.get("reviewer")) + and _artifact(p.get("screenshot"), artifact_root) for p in reviews): + failures.append("primary_experience_not_rendered_and_reviewed") + result["evaluated_requirements"].append({"id": rid, "passed": not failures, + "failures": failures}) + blockers.extend(rid + ":" + failure for failure in failures) + result["passed"] = not blockers + result["verdict"] = "ready_for_review" if result["passed"] else "needs_repair" + return result + + +def admit_discovery(ledger: dict[str, Any], *, question: str, target: str, + action: str, known_path: bool = False) -> dict[str, Any]: + """Durable callers must save returned ledger; calls outside SI are not intercepted. + + At most two equivalent searches per unresolved question and target. Rewording + the query or a fresh context cannot reset that count. A pending search is + continued with its existing handle, not another search. No safety denial is + made retryable by this helper. + """ + if not all(_text(v) for v in (question, target, action)): + raise ValueError("Discovery identity is required") + if action not in {"search", "read", "poll", "act"}: + raise ValueError("Unknown discovery action") + state = json.loads(json.dumps(ledger, allow_nan=False)) + entry = state.setdefault(digest([question, target]), {"searches": 0}) + if type(entry.get("searches")) is not int or entry["searches"] < 0: + raise ValueError("Corrupt discovery ledger") + if action == "search": + if known_path: + raise ValueError("Known target: read it instead of searching again") + if entry.get("pending"): + raise ValueError("Continue the existing search handle instead of launching another") + if entry["searches"] >= 2: + raise ValueError("No-progress search budget exhausted; act, narrow using evidence, or checkpoint") + entry["searches"] += 1 + return state + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--contract", required=True) + parser.add_argument("--evidence", required=True) + parser.add_argument("--artifact-root", required=True) + args = parser.parse_args() + try: + result = evaluate(read_json(Path(args.contract)), read_json(Path(args.evidence)), + Path(args.artifact_root)) + except (ValueError, OSError, TypeError) as exc: + print(json.dumps({"passed": False, "verdict": "needs_repair", "error": str(exc)})) + return 2 + print(json.dumps(result, indent=2)) + return 0 if result["passed"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) From db303a5c2eef8b6973b0d0db2c7a7924515e7ffe Mon Sep 17 00:00:00 2001 From: infotradescout Date: Thu, 17 Sep 2026 12:19:10 -0500 Subject: [PATCH 2/6] Wire explicit product acceptance into the quality CLI without weakening source checks --- .../scripts/quality_gate.py | 94 ++++++++----------- 1 file changed, 39 insertions(+), 55 deletions(-) diff --git a/skills/selective-intelligence/scripts/quality_gate.py b/skills/selective-intelligence/scripts/quality_gate.py index e139148..b2ddbcf 100644 --- a/skills/selective-intelligence/scripts/quality_gate.py +++ b/skills/selective-intelligence/scripts/quality_gate.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 -"""Run the complete local, no-paid SI change gate with one command.""" -from __future__ import annotations +"""Run SI source protection, or explicit product-delivery acceptance. +A passing source gate is not evidence that a user's product is ready. +""" +from __future__ import annotations import argparse import hashlib import json @@ -10,8 +12,6 @@ from datetime import UTC, datetime from pathlib import Path from typing import Any - - SKILL_ROOT = Path(__file__).resolve().parent.parent REPOSITORY_ROOT = SKILL_ROOT.parents[1] @@ -19,36 +19,17 @@ def _run(name: str, argv: list[str]) -> dict[str, Any]: proc = subprocess.run(argv, cwd=SKILL_ROOT, capture_output=True, text=True, check=False) output = (proc.stdout + proc.stderr).encode("utf-8", errors="replace") - return { - "name": name, - "passed": proc.returncode == 0, - "exitCode": proc.returncode, - "outputSha256": hashlib.sha256(output).hexdigest(), - "outputBytes": len(output), - } + return {"name": name, "passed": proc.returncode == 0, "exitCode": proc.returncode, + "outputSha256": hashlib.sha256(output).hexdigest(), "outputBytes": len(output)} def _source_identity() -> dict[str, Any]: - head = subprocess.run( - ["git", "rev-parse", "HEAD"], - cwd=REPOSITORY_ROOT, - capture_output=True, - text=True, - check=False, - ) - listed = subprocess.run( - ["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z"], - cwd=REPOSITORY_ROOT, - capture_output=True, - check=False, - ) - status = subprocess.run( - ["git", "status", "--porcelain", "--untracked-files=normal"], - cwd=REPOSITORY_ROOT, - capture_output=True, - text=True, - check=False, - ) + head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=REPOSITORY_ROOT, + capture_output=True, text=True, check=False) + listed = subprocess.run(["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z"], + cwd=REPOSITORY_ROOT, capture_output=True, check=False) + status = subprocess.run(["git", "status", "--porcelain", "--untracked-files=normal"], + cwd=REPOSITORY_ROOT, capture_output=True, text=True, check=False) if listed.returncode == 0: relative_paths = [Path(value.decode("utf-8")) for value in listed.stdout.split(b"\0") if value] root = REPOSITORY_ROOT @@ -67,20 +48,11 @@ def _source_identity() -> dict[str, Any]: payload = path.read_bytes() except OSError: continue - records.append( - { - "path": normalized, - "bytes": len(payload), - "sha256": hashlib.sha256(payload).hexdigest(), - } - ) + records.append({"path": normalized, "bytes": len(payload), "sha256": hashlib.sha256(payload).hexdigest()}) snapshot = json.dumps(records, sort_keys=True, separators=(",", ":")).encode("utf-8") - return { - "gitHead": head.stdout.strip() if head.returncode == 0 else None, - "workingTreeDirty": bool(status.stdout.strip()) if status.returncode == 0 else None, - "sourceSnapshotSha256": hashlib.sha256(snapshot).hexdigest(), - "sourceFileCount": len(records), - } + return {"gitHead": head.stdout.strip() if head.returncode == 0 else None, + "workingTreeDirty": bool(status.stdout.strip()) if status.returncode == 0 else None, + "sourceSnapshotSha256": hashlib.sha256(snapshot).hexdigest(), "sourceFileCount": len(records)} def run_gate() -> dict[str, Any]: @@ -94,22 +66,34 @@ def run_gate() -> dict[str, Any]: _run("unit_tests", [py, "-B", "-m", "unittest", "discover", "-s", str(tests), "-p", "test_*.py"]), _run("release_integrity", [py, "-B", str(scripts / "release.py"), "doctor", "--json"]), ] - return { - "schemaVersion": "si.quality_gate.v1", - "observedAt": datetime.now(UTC).isoformat(), - "sourceIdentity": _source_identity(), - "paidServiceRequired": False, - "passed": all(check["passed"] for check in checks), - "checks": checks, - "boundary": "local deterministic protection; hosted branch enforcement is separate external state", - } + return {"schemaVersion": "si.quality_gate.v1", "observedAt": datetime.now(UTC).isoformat(), + "sourceIdentity": _source_identity(), "paidServiceRequired": False, + "passed": all(check["passed"] for check in checks), "checks": checks, + "productDelivery": "not_evaluated", + "boundary": "local deterministic source protection; product acceptance and hosted enforcement are separate"} + + +def run_delivery(contract: Path, evidence: Path, artifact_root: Path) -> dict[str, Any]: + # Use one canonical evaluator. Do not duplicate a weaker check in an adapter. + from delivery_acceptance import evaluate, read_json + return evaluate(read_json(contract), read_json(evidence), artifact_root) def main() -> int: - parser = argparse.ArgumentParser(description="Run all portable SI change protections locally") + parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--output") + parser.add_argument("--delivery", action="store_true", help="Evaluate the product, not SI source tests") + parser.add_argument("--contract") + parser.add_argument("--evidence") + parser.add_argument("--artifact-root") args = parser.parse_args() - result = run_gate() + supplied = (args.contract, args.evidence, args.artifact_root) + if (args.delivery and not all(supplied)) or (not args.delivery and any(supplied)): + parser.error("Product acceptance requires --delivery, --contract, --evidence and --artifact-root together") + try: + result = run_delivery(*(Path(value) for value in supplied)) if args.delivery else run_gate() + except (ValueError, OSError, TypeError) as exc: + result = {"passed": False, "verdict": "needs_repair", "error": str(exc)} payload = json.dumps(result, indent=2) + "\n" if args.output: Path(args.output).write_text(payload, encoding="utf-8") From 53773ec147cfb706a44e9d0cade8287bd017d9ce Mon Sep 17 00:00:00 2001 From: infotradescout Date: Thu, 17 Sep 2026 12:23:17 -0500 Subject: [PATCH 3/6] Regress manual substitution, duplicate delivery targets, missing proof and search loops --- .../tests/test_delivery_acceptance.py | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 skills/selective-intelligence/tests/test_delivery_acceptance.py diff --git a/skills/selective-intelligence/tests/test_delivery_acceptance.py b/skills/selective-intelligence/tests/test_delivery_acceptance.py new file mode 100644 index 0000000..6db87c8 --- /dev/null +++ b/skills/selective-intelligence/tests/test_delivery_acceptance.py @@ -0,0 +1,252 @@ +"""Counterexamples from a failed automatic-companion delivery; synthetic data only.""" +import copy +import hashlib +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +SCRIPTS = Path(__file__).resolve().parents[1] / "scripts" +sys.path.insert(0, str(SCRIPTS)) +import delivery_acceptance as DA + + +class DeliveryAcceptanceTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.artifact = self.file("workflow.json", b'{"result":"fixture only"}') + self.image = self.file("view.png", b'fixture-image-for-integrity-only') + self.contract = { + "schema": DA.SCHEMA, "project": "Synthetic automatic companion", + "source_revision": "fixture-revision", "intent_source": "explicit-user-request", + "canonical": {"code_root": "app", "entrypoint": "loopback:1234", "data_owner": "journal"}, + "user_facing": True, "requires_installation": True, + "requirements": [{"id": "auto-harvest", "mode": "automatic", "primary": True, + "outcome": "Claim appears without entering a form", + "accepted_scopes": ["native"], + "required_evidence": ["workflow", "visual", "restart", "safety", "launch"]}], + } + self.packet = { + "schema": "si.product_delivery.evidence.v1", "contract_hash": DA.digest(self.contract), + "source_revision": self.contract["source_revision"], "canonical": self.contract["canonical"], + "unresolved_user_rejections": [], "active_targets": [self.contract["canonical"]], + "installation": {"running": True, "source_revision": self.contract["source_revision"], + "entrypoint": "loopback:1234", "artifact": self.artifact}, + "proofs": [self.proof(kind) for kind in ("workflow", "visual", "restart", "safety", "launch")], + } + + def file(self, name, data): + (self.root / name).write_bytes(data) + return {"path": name, "sha256": hashlib.sha256(data).hexdigest()} + + def proof(self, kind): + return {"id": kind, "requirement_id": "auto-harvest", "kind": kind, "mode": "automatic", + "status": "passed", "executed": True, "scope": "native", "runner": "synthetic-test-runner", + "target": "app/automatic-harvest", "entrypoint": "loopback:1234", + "source_revision": self.contract["source_revision"], "contract_hash": DA.digest(self.contract), + "artifact": self.artifact, "manual_entry_steps": 0, "trigger_observed": True, + "result_observed": True, "reviewed": True, "reviewer": "fixture-reviewer", + "blocking_findings": [], "screenshot": self.image} + + def result(self): + return DA.evaluate(self.contract, self.packet, self.root) + + def fails(self): + result = self.result() + self.assertFalse(result["passed"]) + self.assertTrue(result["blockers"]) + return result + + def test_complete_integrity_packet_only_reaches_review(self): + self.assertEqual(self.result()["verdict"], "ready_for_review") + self.assertEqual(self.result()["user_acceptance"], "not_claimed") + + def test_test_count_is_not_a_user_journey(self): + self.packet["proofs"] = [] + self.packet["passed_tests"] = 10000 + self.fails() + + def test_manual_substitute_fails_automatic_requirement(self): + for p in self.packet["proofs"]: p["mode"] = "manual" + self.fails() + + def test_manual_form_in_automatic_path_fails(self): + self.packet["proofs"][0]["manual_entry_steps"] = 1 + self.fails() + + def test_false_is_not_zero_manual_steps(self): + self.packet["proofs"][0]["manual_entry_steps"] = False + self.fails() + + def test_unobserved_game_event_fails(self): + self.packet["proofs"][0]["trigger_observed"] = False + self.fails() + + def test_unobserved_ui_result_fails(self): + self.packet["proofs"][0]["result_observed"] = False + self.fails() + + def test_synthetic_cannot_claim_native_acceptance(self): + for p in self.packet["proofs"]: p["scope"] = "synthetic" + self.fails() + + def test_wrong_revision_fails(self): + self.packet["source_revision"] = "old" + self.fails() + + def test_correction_invalidates_old_evidence(self): + self.contract["requirements"][0]["outcome"] = "Changed authoritative outcome" + self.fails() + + def test_second_app_blocks_delivery(self): + self.packet["active_targets"].append({"entrypoint": "loopback:9999"}) + self.fails() + + def test_no_running_target_blocks_delivery(self): + self.packet["active_targets"] = [] + self.fails() + + def test_working_url_is_not_installation_proof(self): + self.packet["installation"].pop("artifact") + self.fails() + + def test_stopped_installation_fails(self): + self.packet["installation"]["running"] = False + self.fails() + + def test_other_port_proof_is_rejected(self): + self.packet["proofs"][0]["entrypoint"] = "loopback:9999" + self.fails() + + def test_hash_mismatch_fails(self): + (self.root / "workflow.json").write_bytes(b'changed') + self.fails() + + def test_nonexistent_evidence_fails(self): + (self.root / "workflow.json").unlink() + self.fails() + + def test_path_traversal_fails(self): + self.packet["proofs"][0]["artifact"] = {"path": "../workflow.json", "sha256": "0" * 64} + self.fails() + + def test_symlink_escape_fails(self): + outside = Path(self.temp.name).parent / (self.root.name + "-outside") + outside.write_bytes(b'outside') + self.addCleanup(outside.unlink) + (self.root / "link").symlink_to(outside) + self.assertFalse(DA._artifact({"path": "link", "sha256": hashlib.sha256(b'outside').hexdigest()}, self.root)) + + def test_ui_review_blocker_fails(self): + self.packet["proofs"][1]["blocking_findings"] = ["manual-first navigation"] + self.fails() + + def test_screenshot_alone_is_not_visual_review(self): + self.packet["proofs"][1].pop("reviewer") + self.fails() + + def test_no_screenshot_fails(self): + self.packet["proofs"][1].pop("screenshot") + self.fails() + + def test_skip_is_not_pass(self): + self.packet["proofs"][0]["status"] = "skipped" + self.fails() + + def test_prepared_is_not_executed(self): + self.packet["proofs"][0]["executed"] = False + self.fails() + + def test_unresolved_user_rejection_blocks(self): + self.packet["unresolved_user_rejections"] = ["not acceptable as first delivery"] + self.fails() + + def test_missing_rejection_disposition_blocks(self): + del self.packet["unresolved_user_rejections"] + self.fails() + + def test_duplicate_proofs_block(self): + self.packet["proofs"].append(copy.deepcopy(self.packet["proofs"][0])) + self.fails() + + def test_missing_contract_fields_cannot_lower_bar(self): + del self.contract["requirements"] + self.fails() + + def test_invalid_enum_json_cannot_crash_gate(self): + for field, value in (("mode", []), ("accepted_scopes", [{}]), ("required_evidence", [{}])): + with self.subTest(field=field): + c = copy.deepcopy(self.contract) + c["requirements"][0][field] = value + self.assertFalse(DA.evaluate(c, self.packet, self.root)["passed"]) + + def test_cli_fails_closed_for_invalid_json(self): + (self.root / "invalid.json").write_text("{bad") + p = subprocess.run([sys.executable, str(SCRIPTS / "delivery_acceptance.py"), + "--contract", str(self.root / "invalid.json"), "--evidence", "missing", + "--artifact-root", str(self.root)], capture_output=True, text=True) + self.assertEqual(p.returncode, 2) + self.assertFalse(json.loads(p.stdout)["passed"]) + + def test_no_progress_searches_do_not_loop(self): + ledger = {} + for _ in range(2): + ledger = DA.admit_discovery(ledger, question="find owner", target="project", action="search") + reloaded = json.loads(json.dumps(ledger)) + with self.assertRaisesRegex(ValueError, "budget exhausted"): + DA.admit_discovery(reloaded, question="find owner", target="project", action="search") + + def test_known_path_is_read_not_searched(self): + with self.assertRaisesRegex(ValueError, "Known target"): + DA.admit_discovery({}, question="find owner", target="known.mjs", action="search", known_path=True) + + def test_pending_search_is_polled_not_restarted(self): + ledger = {DA.digest(["q", "target"]): {"searches": 1, "pending": "search-id"}} + with self.assertRaisesRegex(ValueError, "existing search handle"): + DA.admit_discovery(ledger, question="q", target="target", action="search") + self.assertEqual(DA.admit_discovery(ledger, question="q", target="target", action="poll"), ledger) + + def test_no_mutation_to_input_evidence(self): + before = copy.deepcopy(self.packet) + self.result() + self.assertEqual(self.packet, before) + + + +class QualityGateIntegrationTests(unittest.TestCase): + setUp = DeliveryAcceptanceTests.setUp + file = DeliveryAcceptanceTests.file + proof = DeliveryAcceptanceTests.proof + """Exercise the canonical CLI's explicit product mode with synthetic reports.""" + def cli(self, *extra): + c=self.root/'contract.json';e=self.root/'evidence.json' + c.write_text(json.dumps(self.contract));e.write_text(json.dumps(self.packet)) + return subprocess.run([sys.executable,str(SCRIPTS/'quality_gate.py'),*extra, + '--contract',str(c),'--evidence',str(e),'--artifact-root',str(self.root)],capture_output=True,text=True) + + def test_canonical_quality_cli_checks_delivery(self): + proc=self.cli('--delivery');self.assertEqual(proc.returncode,0,proc.stderr) + self.assertEqual(json.loads(proc.stdout)['verdict'],'ready_for_review') + + def test_canonical_quality_cli_blocks_manual_substitute(self): + self.packet['proofs'][0]['manual_entry_steps']=1 + proc=self.cli('--delivery');self.assertEqual(proc.returncode,1) + self.assertEqual(json.loads(proc.stdout)['verdict'],'needs_repair') + + def test_product_inputs_cannot_silently_run_source_gate(self): + proc=self.cli();self.assertEqual(proc.returncode,2) + self.assertIn('--delivery',proc.stderr) + + def test_delivery_flag_without_contract_cannot_pass(self): + proc=subprocess.run([sys.executable,str(SCRIPTS/'quality_gate.py'),'--delivery'],capture_output=True,text=True) + self.assertEqual(proc.returncode,2) + + def test_unexpected_requirement_kind_type_fails_closed(self): + self.contract['requirements'][0]['required_evidence']=5 + self.assertFalse(DA.evaluate(self.contract,self.packet,self.root)['passed']) + +if __name__ == "__main__": unittest.main() From 0a808d57a4b515c89504d9c0fb9ca08380838dba Mon Sep 17 00:00:00 2001 From: infotradescout Date: Thu, 17 Sep 2026 12:24:29 -0500 Subject: [PATCH 4/6] Define first-delivery acceptance separately from source correctness and installed-client adoption --- .../references/product-delivery-acceptance.md | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 skills/selective-intelligence/references/product-delivery-acceptance.md diff --git a/skills/selective-intelligence/references/product-delivery-acceptance.md b/skills/selective-intelligence/references/product-delivery-acceptance.md new file mode 100644 index 0000000..12fa795 --- /dev/null +++ b/skills/selective-intelligence/references/product-delivery-acceptance.md @@ -0,0 +1,27 @@ +# Product delivery acceptance + +A successful component test is not a successful product delivery. Preserve the user's primary job, interaction mode, desired finish, prohibitions and existing canonical owner before implementation. Do not redefine an automatic product as a manual notebook when telemetry work is unfinished. Manual tools can be optional without becoming the main workflow. A small first delivery must close a real user loop, not merely expose internal records. + +## Required acceptance packet + +Use the canonical `scripts/quality_gate.py --delivery --contract --evidence --artifact-root ` at a product delivery boundary. This is agent/adapter work, not an instruction for the user to run a command. Preserve the complete product scope while selecting a bounded delivery. Requirements must state `automatic`, `interactive`, `manual`, or `optional_manual`, identify primary outcomes, and state whether proof must be synthetic, native or live. Do not reduce native/live requirements to synthetic to obtain a pass. + +Bind evidence to the current contract hash, source revision, canonical code root, application entrypoint and data owner. Preserve unresolved user rejection as a blocker until its specific defects have been resolved and rechecked; an agent's confidence cannot substitute for the user's acceptance. Verify exactly one intended delivery target. Explicitly account for old installations and existing data before replacing or redirecting them. Never delete an old journal to make duplication disappear. + +An automatic workflow must show its triggering change and resulting user-visible state without a manual-entry step. Prove startup, update, persistence and recovery in the required environment. Retain the distinction between synthetic replay, native observation and a live external event. An initial historical import is not a newly observed event. A working URL alone is not installation proof. + +For primary user-facing work, require a rendered screen, actual interaction review and disposition of blocking usability findings. Review discoverability, data relevance, density, legibility, empty/error/loading/stale states and the amount of user effort. Test counts and attractive colors are not substitutes. Screenshots alone are insufficient. No numerical aesthetic score or model self-approval establishes acceptance. + +## Enforcement and honest boundaries + +`delivery_acceptance.py` validates the packet and referenced artifact bytes. The integrated quality CLI returns nonzero for missing, stale, wrong-mode or conflicting proof. Its strongest result is `ready_for_review`, never `user_accepted`. The original five-stage SI source gate remains separate and reports `productDelivery: not_evaluated`. + +These checks cannot authenticate fabricated reports, infer a correct product definition from an arbitrary string, intercept ungoverned tool calls, or force an external client to adopt them. An adapter must invoke the gate before promoting product-delivery state. Source publication is not runtime installation. Complete adapter/distribution regeneration, full repository validation and an installed-client forward test before claiming system-wide enforcement. Keep this work unmerged while those gates are outstanding. + +## Repeated discovery failures + +The `admit_discovery` helper caps equivalent searches for one unresolved question and target, rejects searching a known path, and requires polling an existing search handle. The caller must persist its returned ledger. Query rewording and context reset must retain the same question identity. It is not yet a universal interceptor of connector calls. A safety denial is never made retryable by this helper. + +## Regression fixture + +The test suite models a failed automatic companion: two competing app targets, manual input substituted for automatic capture, many passing component tests, a screenshot without interaction review, an unstarted collector, old evidence after a correction, and unresolved dissatisfaction. Every case must remain blocked. The fixtures are synthetic validator tests, not evidence that a real game or product passed acceptance. From c7f8959df5e3914dc70e8839e2295650d3783e01 Mon Sep 17 00:00:00 2001 From: infotradescout Date: Thu, 17 Sep 2026 12:25:15 -0500 Subject: [PATCH 5/6] Require user-job acceptance before product delivery claims --- .../references/evidence-and-completion.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skills/selective-intelligence/references/evidence-and-completion.md b/skills/selective-intelligence/references/evidence-and-completion.md index 3de1bbb..cd920a4 100644 --- a/skills/selective-intelligence/references/evidence-and-completion.md +++ b/skills/selective-intelligence/references/evidence-and-completion.md @@ -1,6 +1,6 @@ # Evidence and Completion Model -Use this reference when the output contains public factual claims, identity information, sensitive fields, or conflicting sources. +Use this reference when the output contains public factual claims, identity information, sensitive fields, conflicting sources, or a product-delivery claim. ## Governing authority order @@ -96,6 +96,8 @@ A result is complete when it performs its intended job without placeholders or i Closed builds do not imply a closed release. Release completion additionally requires every included requirement, critical journey, prohibition, necessary actor, operational gate, and invalidated prior proof to reconcile at the current baseline. +Before promoting a user-facing product to ready for review, apply [product-delivery-acceptance.md](product-delivery-acceptance.md) through the canonical quality CLI's `--delivery` mode. Preserve automatic versus optional-manual requirements and require one intended product owner, actual workflow evidence and rendered interaction review. Source tests alone remain source validation. A screenshot, imported history, working URL or unstarted observer cannot substitute for the primary workflow. Unresolved user rejection keeps delivery open. The gate never declares user acceptance or proves that all installed clients enforce it. + **Cross-runtime equivalence:** Given the same task, context, and evidence, completion means an equivalently correct user outcome across models and environments—not merely fluent prose from the current runtime. Different wording is fine; different intent, scope, product truth, workflow, quality threshold, or final outcome is not. A runtime that cannot meet the bar must be corrected, retried, or blocked before side effects land. See [model-neutral-execution.md](model-neutral-execution.md#governing-requirement-model-interchangeability). One strong benchmark demonstrates that the quality bar is achievable; repeating that bar without heavy user repair is what counts as reliability. ## Council provenance From 07d00955001b7fcba8403407091676e4b585fb0c Mon Sep 17 00:00:00 2001 From: infotradescout Date: Thu, 17 Sep 2026 14:17:02 -0500 Subject: [PATCH 6/6] Register and project product delivery acceptance in the supported runtimes --- .../chatgpt/metadata/chatgpt-adapter.json | 20 +- .../chatgpt/selective-intelligence/SKILL.md | 2 + .../references/evidence-and-completion.md | 4 +- .../references/product-delivery-acceptance.md | 27 ++ .../scripts/delivery_acceptance.py | 242 ++++++++++++++++++ skills/selective-intelligence/SKILL.md | 2 + .../metadata/distribution.json | 9 +- tools/test_chatgpt_adapter.py | 7 +- 8 files changed, 299 insertions(+), 14 deletions(-) create mode 100644 adapters/chatgpt/selective-intelligence/references/product-delivery-acceptance.md create mode 100644 adapters/chatgpt/selective-intelligence/scripts/delivery_acceptance.py diff --git a/adapters/chatgpt/metadata/chatgpt-adapter.json b/adapters/chatgpt/metadata/chatgpt-adapter.json index 95385fd..964d112 100644 --- a/adapters/chatgpt/metadata/chatgpt-adapter.json +++ b/adapters/chatgpt/metadata/chatgpt-adapter.json @@ -7,7 +7,7 @@ "adapter_path": "adapters/chatgpt/selective-intelligence", "transformation": "runtime_only_with_nested_role_entrypoints_as_role_references", "single_skill_entrypoint": "SKILL.md", - "runtime_file_count": 59, + "runtime_file_count": 61, "role_path_map": { "subskills/si-aligner/SKILL.md": "subskills/si-aligner/ROLE.md", "subskills/si-intake/SKILL.md": "subskills/si-intake/ROLE.md", @@ -18,9 +18,9 @@ "subskills/si-worker/SKILL.md": "subskills/si-worker/ROLE.md" }, "behavioral_contract": "source_preserved_not_installed_behavior_proof", - "source_manifest_sha256": "3eb375527a741dd34be9bd63d18c82edd182823ebc3fdd9d16228eaceec4ae7b", + "source_manifest_sha256": "0797e9d318206258af21b0152dcb61949c2478fab2837ecdb9bcc67e67f66c89", "source_file_sha256": { - "SKILL.md": "afb4c6e096aa83683105c45b5525ac34ee436654be4dacc9c2a44f01bf2033b1", + "SKILL.md": "87eb73b0792238f5e909b20bee3ddfc3c0740ee40471874fc76e877dd765d2e5", "VERSION": "326dd5d8641f399990de42def50ac29eb4c9222601c0a0a3fc3c947dd3bec65d", "agents/openai.yaml": "5090139b222271205759e9c1cd34fbe154a0aae91221cb9fea47e7ecaf29e56c", "assets/icon.svg": "bbd2fc7be4ae81f8ff16bd26e26eab6d8547ef3ba1d6d7e49ba970c9a28fc77f", @@ -33,7 +33,7 @@ "references/architecture-reuse.md": "5d1c4d09ca8e7407d19c7fc9d68587b2ea0b61be61e5c3448890af2c8766138d", "references/continuity-and-impact.md": "78f93ccaaed14e58544c5a0c78b6bf933922b4c576a597b13d63ac74d0de53e0", "references/durable-progress-and-recovery.md": "1457eed72572aa6e0f34ab6773b81735e3f1ee22e5427e44a238619b034a609d", - "references/evidence-and-completion.md": "f05424d0e1b8c68624aab05d3e03361e312a08a92bc3b3338b86e29a1971569f", + "references/evidence-and-completion.md": "a72baee12b008168f6dc38d734fb2e4e5ce0e4a5d05345a82282337a045ca7f7", "references/failure-patterns-and-gates.md": "90b4b2fc15069a33cdef3fc39d653b718f4ef4e27345c0db5dab3d7fca5408b5", "references/feedback-and-learning-loop.md": "5e25a722d5f28b4a91f5d29d1e623c8a3c182057ce671cdc3e94bb3fd3cf7db9", "references/first-checkpoint.md": "383ca9161be9cc93878354bbbb5a15ced98752a5d352f7d0dd6fa6243ea64724", @@ -78,10 +78,12 @@ "subskills/si-planner/SKILL.md": "5c36d354272e89e1c26dc15a236b4af183ca55d7494b2a8023259dcb08c6b8fa", "subskills/si-queue-manager/SKILL.md": "89881a69484d5bce571cd9ac886fc75c367ad1352aa121b02a6315d4b0a39930", "subskills/si-verifier/SKILL.md": "24188bbe576fdf18d05b4ac4cce4d4976ce95de41f3c446b0c81384c101b378d", - "subskills/si-worker/SKILL.md": "d53d69ba98c734aa6e035a119e42cbf3b353fd9652c7d40d8937412415a0c1bc" + "subskills/si-worker/SKILL.md": "d53d69ba98c734aa6e035a119e42cbf3b353fd9652c7d40d8937412415a0c1bc", + "scripts/delivery_acceptance.py": "e8749f4141f7397fc42458400e90b87256f429c4963832bbf920dbb8837d40f9", + "references/product-delivery-acceptance.md": "574643b517d7a39d5146142bec2bbbaa8a47f5869ad3a021f9fbd271b8fb47f2" }, "projected_file_sha256": { - "SKILL.md": "f034d6a4ff310d8bbb95a27a792f9b0b2e21acdab1db348f7859d8258fd32b86", + "SKILL.md": "69752eee3760c835ede48f3d49a9590a76cb63ade977ca647196f85aebc37c8c", "VERSION": "326dd5d8641f399990de42def50ac29eb4c9222601c0a0a3fc3c947dd3bec65d", "agents/openai.yaml": "5090139b222271205759e9c1cd34fbe154a0aae91221cb9fea47e7ecaf29e56c", "assets/icon.svg": "bbd2fc7be4ae81f8ff16bd26e26eab6d8547ef3ba1d6d7e49ba970c9a28fc77f", @@ -94,7 +96,7 @@ "references/architecture-reuse.md": "5d1c4d09ca8e7407d19c7fc9d68587b2ea0b61be61e5c3448890af2c8766138d", "references/continuity-and-impact.md": "78f93ccaaed14e58544c5a0c78b6bf933922b4c576a597b13d63ac74d0de53e0", "references/durable-progress-and-recovery.md": "1457eed72572aa6e0f34ab6773b81735e3f1ee22e5427e44a238619b034a609d", - "references/evidence-and-completion.md": "f05424d0e1b8c68624aab05d3e03361e312a08a92bc3b3338b86e29a1971569f", + "references/evidence-and-completion.md": "a72baee12b008168f6dc38d734fb2e4e5ce0e4a5d05345a82282337a045ca7f7", "references/failure-patterns-and-gates.md": "90b4b2fc15069a33cdef3fc39d653b718f4ef4e27345c0db5dab3d7fca5408b5", "references/feedback-and-learning-loop.md": "5e25a722d5f28b4a91f5d29d1e623c8a3c182057ce671cdc3e94bb3fd3cf7db9", "references/first-checkpoint.md": "383ca9161be9cc93878354bbbb5a15ced98752a5d352f7d0dd6fa6243ea64724", @@ -139,7 +141,9 @@ "subskills/si-planner/ROLE.md": "5c36d354272e89e1c26dc15a236b4af183ca55d7494b2a8023259dcb08c6b8fa", "subskills/si-queue-manager/ROLE.md": "89881a69484d5bce571cd9ac886fc75c367ad1352aa121b02a6315d4b0a39930", "subskills/si-verifier/ROLE.md": "24188bbe576fdf18d05b4ac4cce4d4976ce95de41f3c446b0c81384c101b378d", - "subskills/si-worker/ROLE.md": "d53d69ba98c734aa6e035a119e42cbf3b353fd9652c7d40d8937412415a0c1bc" + "subskills/si-worker/ROLE.md": "d53d69ba98c734aa6e035a119e42cbf3b353fd9652c7d40d8937412415a0c1bc", + "scripts/delivery_acceptance.py": "e8749f4141f7397fc42458400e90b87256f429c4963832bbf920dbb8837d40f9", + "references/product-delivery-acceptance.md": "574643b517d7a39d5146142bec2bbbaa8a47f5869ad3a021f9fbd271b8fb47f2" }, "execution_entrypoint": "scripts/build_engine.py", "execution_dependencies": [ diff --git a/adapters/chatgpt/selective-intelligence/SKILL.md b/adapters/chatgpt/selective-intelligence/SKILL.md index 3840ac7..59b7f70 100644 --- a/adapters/chatgpt/selective-intelligence/SKILL.md +++ b/adapters/chatgpt/selective-intelligence/SKILL.md @@ -59,6 +59,8 @@ Never collapse: Bind claims to revision/environment/scope/evidence. Production finishes at required live behavior unless the user specifies otherwise. Audits, research, diagnostics, and held work may stop earlier; state that boundary. See [evidence and completion](references/evidence-and-completion.md). +Product handoff requires [delivery acceptance](references/product-delivery-acceptance.md). + ## Product identity before templates Software does not imply SaaS; management authority cannot invent product behavior. Current governing intent defines identity; correct assumptions, plans, handoffs, and proof. Preserve approved language, design, responsibilities, memberships, fees, providers, and services. Invent rules only for actual gaps. diff --git a/adapters/chatgpt/selective-intelligence/references/evidence-and-completion.md b/adapters/chatgpt/selective-intelligence/references/evidence-and-completion.md index 3de1bbb..cd920a4 100644 --- a/adapters/chatgpt/selective-intelligence/references/evidence-and-completion.md +++ b/adapters/chatgpt/selective-intelligence/references/evidence-and-completion.md @@ -1,6 +1,6 @@ # Evidence and Completion Model -Use this reference when the output contains public factual claims, identity information, sensitive fields, or conflicting sources. +Use this reference when the output contains public factual claims, identity information, sensitive fields, conflicting sources, or a product-delivery claim. ## Governing authority order @@ -96,6 +96,8 @@ A result is complete when it performs its intended job without placeholders or i Closed builds do not imply a closed release. Release completion additionally requires every included requirement, critical journey, prohibition, necessary actor, operational gate, and invalidated prior proof to reconcile at the current baseline. +Before promoting a user-facing product to ready for review, apply [product-delivery-acceptance.md](product-delivery-acceptance.md) through the canonical quality CLI's `--delivery` mode. Preserve automatic versus optional-manual requirements and require one intended product owner, actual workflow evidence and rendered interaction review. Source tests alone remain source validation. A screenshot, imported history, working URL or unstarted observer cannot substitute for the primary workflow. Unresolved user rejection keeps delivery open. The gate never declares user acceptance or proves that all installed clients enforce it. + **Cross-runtime equivalence:** Given the same task, context, and evidence, completion means an equivalently correct user outcome across models and environments—not merely fluent prose from the current runtime. Different wording is fine; different intent, scope, product truth, workflow, quality threshold, or final outcome is not. A runtime that cannot meet the bar must be corrected, retried, or blocked before side effects land. See [model-neutral-execution.md](model-neutral-execution.md#governing-requirement-model-interchangeability). One strong benchmark demonstrates that the quality bar is achievable; repeating that bar without heavy user repair is what counts as reliability. ## Council provenance diff --git a/adapters/chatgpt/selective-intelligence/references/product-delivery-acceptance.md b/adapters/chatgpt/selective-intelligence/references/product-delivery-acceptance.md new file mode 100644 index 0000000..12fa795 --- /dev/null +++ b/adapters/chatgpt/selective-intelligence/references/product-delivery-acceptance.md @@ -0,0 +1,27 @@ +# Product delivery acceptance + +A successful component test is not a successful product delivery. Preserve the user's primary job, interaction mode, desired finish, prohibitions and existing canonical owner before implementation. Do not redefine an automatic product as a manual notebook when telemetry work is unfinished. Manual tools can be optional without becoming the main workflow. A small first delivery must close a real user loop, not merely expose internal records. + +## Required acceptance packet + +Use the canonical `scripts/quality_gate.py --delivery --contract --evidence --artifact-root ` at a product delivery boundary. This is agent/adapter work, not an instruction for the user to run a command. Preserve the complete product scope while selecting a bounded delivery. Requirements must state `automatic`, `interactive`, `manual`, or `optional_manual`, identify primary outcomes, and state whether proof must be synthetic, native or live. Do not reduce native/live requirements to synthetic to obtain a pass. + +Bind evidence to the current contract hash, source revision, canonical code root, application entrypoint and data owner. Preserve unresolved user rejection as a blocker until its specific defects have been resolved and rechecked; an agent's confidence cannot substitute for the user's acceptance. Verify exactly one intended delivery target. Explicitly account for old installations and existing data before replacing or redirecting them. Never delete an old journal to make duplication disappear. + +An automatic workflow must show its triggering change and resulting user-visible state without a manual-entry step. Prove startup, update, persistence and recovery in the required environment. Retain the distinction between synthetic replay, native observation and a live external event. An initial historical import is not a newly observed event. A working URL alone is not installation proof. + +For primary user-facing work, require a rendered screen, actual interaction review and disposition of blocking usability findings. Review discoverability, data relevance, density, legibility, empty/error/loading/stale states and the amount of user effort. Test counts and attractive colors are not substitutes. Screenshots alone are insufficient. No numerical aesthetic score or model self-approval establishes acceptance. + +## Enforcement and honest boundaries + +`delivery_acceptance.py` validates the packet and referenced artifact bytes. The integrated quality CLI returns nonzero for missing, stale, wrong-mode or conflicting proof. Its strongest result is `ready_for_review`, never `user_accepted`. The original five-stage SI source gate remains separate and reports `productDelivery: not_evaluated`. + +These checks cannot authenticate fabricated reports, infer a correct product definition from an arbitrary string, intercept ungoverned tool calls, or force an external client to adopt them. An adapter must invoke the gate before promoting product-delivery state. Source publication is not runtime installation. Complete adapter/distribution regeneration, full repository validation and an installed-client forward test before claiming system-wide enforcement. Keep this work unmerged while those gates are outstanding. + +## Repeated discovery failures + +The `admit_discovery` helper caps equivalent searches for one unresolved question and target, rejects searching a known path, and requires polling an existing search handle. The caller must persist its returned ledger. Query rewording and context reset must retain the same question identity. It is not yet a universal interceptor of connector calls. A safety denial is never made retryable by this helper. + +## Regression fixture + +The test suite models a failed automatic companion: two competing app targets, manual input substituted for automatic capture, many passing component tests, a screenshot without interaction review, an unstarted collector, old evidence after a correction, and unresolved dissatisfaction. Every case must remain blocked. The fixtures are synthetic validator tests, not evidence that a real game or product passed acceptance. diff --git a/adapters/chatgpt/selective-intelligence/scripts/delivery_acceptance.py b/adapters/chatgpt/selective-intelligence/scripts/delivery_acceptance.py new file mode 100644 index 0000000..80dba80 --- /dev/null +++ b/adapters/chatgpt/selective-intelligence/scripts/delivery_acceptance.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""Fail-closed product delivery checks; not a substitute for observing the product. + +Inputs are adapter-produced evidence. Hashes bind reports to files and revisions; +they do not establish that the report or a subjective design judgment is true. +The strongest verdict is ready_for_review, never user_accepted. +""" +from __future__ import annotations + +import argparse +import hashlib +import json +import re +from pathlib import Path +from typing import Any + +SCHEMA = "si.product_delivery.v1" +SHA = re.compile(r"^[a-f0-9]{64}$") +MODES = {"automatic", "manual", "optional_manual", "interactive"} +SCOPES = {"synthetic", "native", "live"} +KINDS = {"workflow", "visual", "restart", "safety", "launch"} +MAX_JSON = 2 * 1024 * 1024 +MAX_ARTIFACT = 32 * 1024 * 1024 + + +def digest(value: Any) -> str: + return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":"), + allow_nan=False).encode()).hexdigest() + + +def read_json(path: Path) -> Any: + if not path.is_file() or path.is_symlink() or path.stat().st_size > MAX_JSON: + raise ValueError("Expected a bounded, regular JSON file") + return json.loads(path.read_text(encoding="utf-8"), + parse_constant=lambda value: (_ for _ in ()).throw(ValueError(value))) + + +def _text(value: Any) -> bool: + return isinstance(value, str) and bool(value.strip()) and len(value) <= 4096 + + +def contract_errors(contract: Any) -> list[str]: + errors: list[str] = [] + if not isinstance(contract, dict) or contract.get("schema") != SCHEMA: + return ["contract_schema_invalid"] + for name in ("project", "source_revision", "intent_source"): + if not _text(contract.get(name)): + errors.append("contract_" + name + "_missing") + owner = contract.get("canonical") + if not isinstance(owner, dict) or any(not _text(owner.get(k)) for k in + ("code_root", "entrypoint", "data_owner")): + errors.append("canonical_owner_missing") + requirements = contract.get("requirements") + if not isinstance(requirements, list) or not requirements or len(requirements) > 100: + return errors + ["requirements_missing_or_unbounded"] + seen: set[str] = set() + primary = 0 + for requirement in requirements: + if not isinstance(requirement, dict): + errors.append("requirement_invalid") + continue + rid = requirement.get("id") + if not _text(rid) or rid in seen: + errors.append("requirement_id_invalid_or_duplicate") + else: + seen.add(rid) + if not isinstance(requirement.get("mode"), str) or requirement["mode"] not in MODES or not _text(requirement.get("outcome")): + errors.append("requirement_semantics_missing") + if type(requirement.get("primary")) is not bool: + errors.append("requirement_primary_flag_missing") + primary += requirement.get("primary") is True + scopes = requirement.get("accepted_scopes") + if not isinstance(scopes, list) or not scopes or any(not isinstance(s, str) or s not in SCOPES for s in scopes): + errors.append("requirement_evidence_scope_missing") + kinds = requirement.get("required_evidence") + if not isinstance(kinds, list) or not kinds or any(not isinstance(k, str) or k not in KINDS for k in kinds): + errors.append("requirement_evidence_kind_missing") + if requirement.get("primary") and (not isinstance(kinds, list) or "workflow" not in kinds): + errors.append("primary_requires_workflow_evidence") + if not primary: + errors.append("primary_user_job_missing") + if type(contract.get("user_facing")) is not bool: + errors.append("surface_scope_missing") + if type(contract.get("requires_installation")) is not bool: + errors.append("installation_scope_missing") + return errors + + +def _artifact(reference: Any, root: Path) -> bool: + if not isinstance(reference, dict) or not _text(reference.get("path")): + return False + if not isinstance(reference.get("sha256"), str) or not SHA.fullmatch(reference["sha256"]): + return False + relative = Path(reference["path"]) + if relative.is_absolute() or ".." in relative.parts or "\\" in reference["path"]: + return False + target = root / relative + try: + if target.is_symlink() or not target.resolve().is_relative_to(root.resolve()): + return False + if not target.is_file() or not 0 < target.stat().st_size <= MAX_ARTIFACT: + return False + return hashlib.sha256(target.read_bytes()).hexdigest() == reference["sha256"] + except (OSError, RuntimeError): + return False + + +def evaluate(contract: Any, evidence: Any, artifact_root: Path) -> dict[str, Any]: + """Validate a delivery packet without reading or changing a product's save data.""" + blockers = contract_errors(contract) + result = {"schema": "si.product_delivery.verdict.v1", "passed": False, + "verdict": "needs_repair", "user_acceptance": "not_claimed", + "blockers": blockers, "evaluated_requirements": [], + "boundary": "Evidence integrity and coverage checks, not proof of subjective quality or client adoption."} + if blockers: + return result + result["contract_hash"] = digest(contract) + if not isinstance(evidence, dict) or evidence.get("schema") != "si.product_delivery.evidence.v1": + blockers.append("evidence_schema_invalid") + return result + if evidence.get("contract_hash") != result["contract_hash"]: + blockers.append("evidence_not_bound_to_current_intent") + if evidence.get("source_revision") != contract["source_revision"]: + blockers.append("evidence_not_bound_to_current_source") + if evidence.get("canonical") != contract["canonical"]: + blockers.append("evidence_wrong_product_owner") + rejections = evidence.get("unresolved_user_rejections") + if not isinstance(rejections, list) or rejections: + blockers.append("user_rejection_unresolved_or_unreported") + targets = evidence.get("active_targets") + if not isinstance(targets, list) or targets != [contract["canonical"]]: + blockers.append("competing_or_unverified_delivery_targets") + if contract["requires_installation"]: + install = evidence.get("installation") or {} + if (not isinstance(install, dict) or install.get("running") is not True + or install.get("source_revision") != contract["source_revision"] + or install.get("entrypoint") != contract["canonical"]["entrypoint"] + or not _artifact(install.get("artifact"), artifact_root)): + blockers.append("intended_installation_not_verified") + proofs = evidence.get("proofs") + if not isinstance(proofs, list) or len(proofs) > 500: + blockers.append("proofs_missing_or_unbounded") + return result + valid = [] + seen = set() + for proof in proofs: + if not isinstance(proof, dict): + blockers.append("invalid_proof") + continue + pid = proof.get("id") + if not _text(pid) or pid in seen: + blockers.append("proof_id_invalid_or_duplicate") + continue + seen.add(pid) + integrity = (proof.get("status") == "passed" and proof.get("executed") is True + and proof.get("source_revision") == contract["source_revision"] + and proof.get("contract_hash") == result["contract_hash"] + and isinstance(proof.get("scope"), str) and proof["scope"] in SCOPES + and isinstance(proof.get("kind"), str) and proof["kind"] in KINDS + and proof.get("entrypoint") == contract["canonical"]["entrypoint"] + and _text(proof.get("runner")) and _text(proof.get("target")) + and _artifact(proof.get("artifact"), artifact_root)) + if integrity: + valid.append(proof) + for requirement in contract["requirements"]: + rid = requirement["id"] + matching = [p for p in valid if p.get("requirement_id") == rid + and p.get("scope") in requirement["accepted_scopes"] + and p.get("mode") == requirement["mode"]] + kinds = {p["kind"] for p in matching} + missing = sorted(set(requirement["required_evidence"]) - kinds) + failures = [] + if missing: + failures.append("missing:" + ",".join(missing)) + if requirement["mode"] == "automatic": + workflows = [p for p in matching if p["kind"] == "workflow"] + if not workflows or not any(type(p.get("manual_entry_steps")) is int + and p["manual_entry_steps"] == 0 + and p.get("trigger_observed") is True + and p.get("result_observed") is True for p in workflows): + failures.append("automatic_workflow_not_proved_without_manual_entry") + if requirement["primary"] and contract["user_facing"]: + reviews = [p for p in matching if p["kind"] == "visual"] + if not reviews or not any(p.get("reviewed") is True + and p.get("blocking_findings") == [] + and _text(p.get("reviewer")) + and _artifact(p.get("screenshot"), artifact_root) for p in reviews): + failures.append("primary_experience_not_rendered_and_reviewed") + result["evaluated_requirements"].append({"id": rid, "passed": not failures, + "failures": failures}) + blockers.extend(rid + ":" + failure for failure in failures) + result["passed"] = not blockers + result["verdict"] = "ready_for_review" if result["passed"] else "needs_repair" + return result + + +def admit_discovery(ledger: dict[str, Any], *, question: str, target: str, + action: str, known_path: bool = False) -> dict[str, Any]: + """Durable callers must save returned ledger; calls outside SI are not intercepted. + + At most two equivalent searches per unresolved question and target. Rewording + the query or a fresh context cannot reset that count. A pending search is + continued with its existing handle, not another search. No safety denial is + made retryable by this helper. + """ + if not all(_text(v) for v in (question, target, action)): + raise ValueError("Discovery identity is required") + if action not in {"search", "read", "poll", "act"}: + raise ValueError("Unknown discovery action") + state = json.loads(json.dumps(ledger, allow_nan=False)) + entry = state.setdefault(digest([question, target]), {"searches": 0}) + if type(entry.get("searches")) is not int or entry["searches"] < 0: + raise ValueError("Corrupt discovery ledger") + if action == "search": + if known_path: + raise ValueError("Known target: read it instead of searching again") + if entry.get("pending"): + raise ValueError("Continue the existing search handle instead of launching another") + if entry["searches"] >= 2: + raise ValueError("No-progress search budget exhausted; act, narrow using evidence, or checkpoint") + entry["searches"] += 1 + return state + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--contract", required=True) + parser.add_argument("--evidence", required=True) + parser.add_argument("--artifact-root", required=True) + args = parser.parse_args() + try: + result = evaluate(read_json(Path(args.contract)), read_json(Path(args.evidence)), + Path(args.artifact_root)) + except (ValueError, OSError, TypeError) as exc: + print(json.dumps({"passed": False, "verdict": "needs_repair", "error": str(exc)})) + return 2 + print(json.dumps(result, indent=2)) + return 0 if result["passed"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/selective-intelligence/SKILL.md b/skills/selective-intelligence/SKILL.md index b3a6039..9760b03 100644 --- a/skills/selective-intelligence/SKILL.md +++ b/skills/selective-intelligence/SKILL.md @@ -56,6 +56,8 @@ Never collapse: Bind claims to revision/environment/scope/evidence. Production finishes at required live behavior unless the user specifies otherwise. Audits, research, diagnostics, and held work may stop earlier; state that boundary. See [evidence and completion](references/evidence-and-completion.md). +Product handoff requires [delivery acceptance](references/product-delivery-acceptance.md). + ## Product identity before templates Software does not imply SaaS; management authority cannot invent product behavior. Current governing intent defines identity; correct assumptions, plans, handoffs, and proof. Preserve approved language, design, responsibilities, memberships, fees, providers, and services. Invent rules only for actual gaps. diff --git a/skills/selective-intelligence/metadata/distribution.json b/skills/selective-intelligence/metadata/distribution.json index 200b691..8f060b7 100644 --- a/skills/selective-intelligence/metadata/distribution.json +++ b/skills/selective-intelligence/metadata/distribution.json @@ -201,7 +201,9 @@ "subskills/si-planner/SKILL.md", "subskills/si-queue-manager/SKILL.md", "subskills/si-verifier/SKILL.md", - "subskills/si-worker/SKILL.md" + "subskills/si-worker/SKILL.md", + "scripts/delivery_acceptance.py", + "references/product-delivery-acceptance.md" ], "release_files": [ "AI-GUIDE.md", @@ -321,7 +323,10 @@ "tests/test_product_model_handoff_completeness.py", "tests/test_product_model_repair_handoff.py", "tests/test_prompt_queue_continuity.py", - "evals/results-1.0.8.json" + "evals/results-1.0.8.json", + "scripts/delivery_acceptance.py", + "references/product-delivery-acceptance.md", + "tests/test_delivery_acceptance.py" ], "whole_run_usage_governor": { "scope": "entire_run", diff --git a/tools/test_chatgpt_adapter.py b/tools/test_chatgpt_adapter.py index 9041a84..debbbc3 100644 --- a/tools/test_chatgpt_adapter.py +++ b/tools/test_chatgpt_adapter.py @@ -27,9 +27,10 @@ def main() -> int: files = sorted(path.relative_to(ADAPTER_ROOT).as_posix() for path in ADAPTER_ROOT.rglob("*") if path.is_file()) skill_entrypoints = [path for path in files if Path(path).name == "SKILL.md"] require(skill_entrypoints == ["SKILL.md"], f"expected one SKILL.md, found {skill_entrypoints}") - # Keep all 50 existing runtime files and add eight missing execution owners - # and dependencies. This is a repository budget, not platform acceptance. - require(len(files) <= 59, f"runtime adapter exceeds the explicit delivery budget: {len(files)} files") + # Preserve the previous 59-file budget; admit only the two reviewed delivery owners. + delivery_files = {"scripts/delivery_acceptance.py", "references/product-delivery-acceptance.md"} + require(delivery_files.issubset(files), "product delivery runtime owner is missing") + require(len(set(files) - delivery_files) <= 59, f"legacy runtime adapter exceeds its 59-file budget: {len(set(files) - delivery_files)}") require(BUILD.EXECUTION_RUNTIME_FILES.issubset(files), "runtime execution dependency is missing") # Re-derive the exact allowlist, bytes and source identities. File count or # version labels alone cannot establish that a repair reached this package.