← All Pull Requests

Contribution Assurance: verifiable evidence skill, revision ancestry and test gates #9

Closed opened by Josh Aberdeen

Linked full evidence

Closes/relates to Wiki Skill Pack issue #7, which includes the sanitized evidence ZIP and per-revision audit.

Addition

Adds an opt-in wiki-contribution-assurance sub-skill to the existing Wiki Skill Pack with:

  • read-only, non-publishing executable evidence verifier;
  • complete per-revision matrix requirement and explicit unavailable-history reporting;
  • positive/negative reproduction, root cause vs feature-request separation, cross-caller impacts and regression evidence;
  • privacy, source hash, current upstream HEAD, actual diff and maintainer handoff gates;
  • explicit distinction between candidate, merge-ready and verified deployed operation;
  • mandatory contributor permission before remote publishing.

Changed files

SKILL.md, README.md, package.json, page.json, install.sh, uninstall.sh, and four new files under skills/wiki-contribution-assurance/.

Evidence

  • Base SHA: 85ddda3e8b5102b11c04def834850196d350ab35 (v0.8.58).
  • 40/40 synthetic regression tests passed.
  • New skill sandbox install/uninstall passed.
  • ADOM package lint passed, 0 errors; three existing non-blocking warnings remain.
  • 174/174 accessible revisions accounted for with 173 immutable adjacent comparisons. The exact ledger, patch and test outputs are attached to Issue #7.
  • All file contents scanned for sensitive local path and credential indicators; no customer designs or tokens included.

Safeguards and maintainer checks

The verifier does not execute untrusted manifest commands. Structural integrity and source-history accounting do not substitute for independent test execution, human engineering review, native platform tests or a released-binary smoke check. Maintainers retain merge/publish/release control.

This PR does not publish the 0.8.59 package or modify the current default branch until merged; please verify the branch comparison against master.

Diff Skip to comments (1)

--- a/SKILL.md+++ b/SKILL.md@@ -1,150 +1,154 @@⋯ 114 unchanged lines ⋯ | **wiki-readme-slideshow** | A narrated, fullscreen slideshow inside a `readme.html` (per-slide TTS, arrow keys). | | **wiki-video-captions** | A narrated `::adom-video` in a README with WebVTT captions and chapters. | | **wiki-component-lod** | High/medium/low component GLB detail caches for heavy board scenes. |+| **wiki-contribution-assurance** | Evidence-backed source defect audits, revision ancestry, tests and PR qualification. | | **wiki-bloat** | THE rule against page sprawl: find where a thing already lives BEFORE creating anything; "add to the skillpack" means add a SKILL.md there, never mint a page. |  ## Talk to the AI⋯ 27 unchanged lines ⋯ For component LOD/LDO caches, zoom-driven detail, heavy board scenes or GPU/memory freezes, read `wiki-component-lod`. It covers both asset generation and runtime qualification.  For annotated wiki hero versus clean board LOD separation, footprint overlays leaking into high detail, or image-first LOD previews, read `wiki-component-lod` and `wiki-component`. All four board LODs exclude footprint, pad-label and silkscreen helpers; wiki heroes retain them.++- "Audit every revision and prove this software fix before contributing" -> wiki-contribution-assurance.+--- a/README.md+++ b/README.md@@ -1,220 +1,224 @@ # Adom Wiki Skill Pack -**One install, 23 skills that teach the AI to use the Adom Wiki without breaking+**One install, focused skills that teach the AI to use the Adom Wiki without breaking pages, plus a write-up of the wiki repo archetypes that have emerged from real use.** ⋯ 20 unchanged lines ⋯ adom-wiki pkg install adom-wiki-skillpack ``` -This drops the main `adom-wiki-skillpack` skill plus 22 sub-skills onto your+This drops the main `adom-wiki-skillpack` skill plus focused sub-skills onto your container, and pulls in **`adom-wiki-hero-image`** (a dependency) for hero generation. ⋯ 129 unchanged lines ⋯ | **wiki-workspaces** | Ship a Hydrogen bench with a project: `hydrogen/workspace.json`, export with adom-cli, push, fork or clone, apply in a container. | | **wiki-brand** | Whose brand a page wears: every owner's guide at `<owner>/brand`, the `adom-wiki brand` verbs, the BRAND_* hints, and the rule that Adom's look belongs only on adom pages. | | **wiki-hero-glb** | What the wiki's 3D viewer reads from a GLB: declared layers and the hidden-layer intro, tooltips, switchable groups and lights, animation knobs, extensions, materials, naming. |+| **wiki-contribution-assurance** | Trace genuine defects to source, check every recoverable revision and verify contribution evidence before seeking submit permission. | | **wiki-bloat** | The rule against page sprawl: find where a thing already lives before creating anything. |  ## Talk to the AI⋯ 49 unchanged lines ⋯ Read [wiki-video-captions](skills/wiki-video-captions/SKILL.md) before embedding a film in a README. It covers `::adom-video`, public/private manifests, speech caption generation, timing after edits, dead-air checks and live player proof.++- "Build a complete, tested upstream contribution" -> wiki-contribution-assurance (requires explicit approval before publication).+--- a/package.json+++ b/package.json@@ -1,165 +1,176 @@ {   "slug": "adom-wiki-skillpack",   "title": "Adom Wiki Skill Pack",-  "version": "0.8.58",-  "changelog": "0.8.55: wiki-publish-safely matches adom-wiki 1.8.2 (tested 2026-10-30, #5): publish writes both layers (commits the tarball files + a merged page.json to the repo), a first publish creates the page with no ghost row, and page.json is merged (manifest keys win, the hero and hand-added keys survive); the old rules are kept for older CLIs/servers. 0.8.54: publish checklist: page.json must carry the same discovery_triggers as package.json; a page.json push without them silently wipes the page's triggers (adom/wiki#240).",+  "version": "0.8.59",+  "changelog": "0.8.59 proposal: introduce wiki-contribution-assurance with read-only verifier, source ancestry matrix, privacy and test gates. Maintainer review and native acceptance pending. 0.8.55: wiki-publish-safely matches adom-wiki 1.8.2 (tested 2026-10-30, #5): publish writes both layers (commits the tarball files + a merged page.json to the repo), a first publish creates the page with no ghost row, and page.json is merged (manifest keys win, the hero and hand-added keys survive); the old rules are kept for older CLIs/servers. 0.8.54: publish checklist: page.json must carry the same discovery_triggers as package.json; a page.json push without them silently wipes the page's triggers (adom/wiki#240).",   "type": "skill",   "hero": {     "path": "screenshots/hero.png"⋯ 78 unchanged lines ⋯     "closed captions",     "VTT subtitles",     "no dead air",-    "voiceover gaps"+    "voiceover gaps",+    "root cause and revision matrix",+    "contribution assurance",+    "test a bug before submitting",+    "validate a pull request",+    "audit every source revision",+    "verify an upstream fix"   ],   "dependencies": {     "adom/adom-wiki-hero-image": "^0.2.2",⋯ 36 unchanged lines ⋯     "skills/wiki-workspaces/SKILL.md",     "skills/wiki-brand/SKILL.md",     "skills/wiki-hero-glb/SKILL.md",-    "skills/wiki-video-captions/SKILL.md"+    "skills/wiki-video-captions/SKILL.md",+    "skills/wiki-contribution-assurance/SKILL.md",+    "skills/wiki-contribution-assurance/tools/verify.py",+    "skills/wiki-contribution-assurance/tests/test_verify.py",+    "skills/wiki-contribution-assurance/references/evidence-contract.md"   ],   "scripts": {     "install": "./install.sh",⋯ 26 unchanged lines ⋯   ],   "discovery_pitch": "Anything touching wiki.adom.inc: publishing, page archetypes, readmes, heroes, discovery, and the wiki-bloat rule (find where a thing already lives before creating any page)." }+--- a/page.json+++ b/page.json@@ -1,270 +1,281 @@⋯ 183 unchanged lines ⋯     "closed captions",     "VTT subtitles",     "no dead air",-    "voiceover gaps"+    "voiceover gaps",+    "root cause and revision matrix",+    "contribution assurance",+    "test a bug before submitting",+    "validate a pull request",+    "audit every source revision",+    "verify an upstream fix"   ],   "discovery_pitch": "Anything touching wiki.adom.inc: publishing, page archetypes, readmes, heroes, discovery, and the wiki-bloat rule (find where a thing already lives before creating any page).",   "sample_prompts": [⋯ 10 unchanged lines ⋯       "prompt": "Before I create this wiki page, check whether the thing already has a home."     }   ],-  "version": "0.8.58",+  "version": "0.8.59",   "changelog": "0.8.55: wiki-publish-safely matches adom-wiki 1.8.2 (tested 2026-10-30, #5): publish writes both layers (commits the tarball files + a merged page.json to the repo), a first publish creates the page with no ghost row, and page.json is merged (manifest keys win, the hero and hand-added keys survive); the old rules are kept for older CLIs/servers. 0.8.54: publish checklist: page.json must carry the same discovery_triggers as package.json; a page.json push without them silently wipes the page's triggers (adom/wiki#240).",   "description": "The Adom Wiki Skill Pack : one main skill plus the focused sub-skills that teach the AI to publish to wiki.adom.inc without breaking pages, name the wiki repo archetypes (Page, Skillpack, Family), and publish rich component pages (chips, molecules, boards). The wiki is six tools in one: git, package manager, releases, discussions, pull requests, and discovery. One package manager install drops the whole set onto your container. Includes reviewed Z-up component artwork, surface-selected MPN marking, reusable thumbnail caches and component-page widgets. Includes canonical video widgets, synchronized captions and chapters, and voiceover pacing checks.",   "dependencies": {⋯ 37 unchanged lines ⋯     "skills/wiki-workspaces/SKILL.md",     "skills/wiki-brand/SKILL.md",     "skills/wiki-hero-glb/SKILL.md",-    "skills/wiki-video-captions/SKILL.md"+    "skills/wiki-video-captions/SKILL.md",+    "skills/wiki-contribution-assurance/SKILL.md",+    "skills/wiki-contribution-assurance/tools/verify.py",+    "skills/wiki-contribution-assurance/tests/test_verify.py",+    "skills/wiki-contribution-assurance/references/evidence-contract.md"   ],   "scripts": {     "install": "./install.sh",⋯ 16 unchanged lines ⋯   },   "org": "adom" }+--- a/install.sh+++ b/install.sh@@ -1,55 +1,57 @@⋯ 36 unchanged lines ⋯ install_skill wiki-skillpack      skills/wiki-skillpack/SKILL.md install_skill wiki-hero           skills/wiki-hero/SKILL.md install_skill wiki-breadcrumbs    skills/wiki-breadcrumbs/SKILL.md+install_skill wiki-contribution-assurance skills/wiki-contribution-assurance/SKILL.md install_skill wiki-forking        skills/wiki-forking/SKILL.md install_skill wiki-discovery      skills/wiki-discovery/SKILL.md install_skill wiki-scoring        skills/wiki-scoring/SKILL.md⋯ 10 unchanged lines ⋯ echo "OK: Installed Adom Wiki Skill Pack and all bundled skills to $DEST" done echo "Hint: Fresh AI sessions discover installed skills; existing conversations must explicitly read wiki-component/SKILL.md for updated rules."+--- a/uninstall.sh+++ b/uninstall.sh@@ -1,13 +1,14 @@⋯ 5 unchanged lines ⋯   DESTINATIONS=("$HOME/.claude/skills" "${CODEX_HOME:-$HOME/.codex}/skills") fi for DEST in "${DESTINATIONS[@]}"; do-  for name in adom-wiki-skillpack wiki-anatomy wiki-api wiki-publish-safely wiki-readme wiki-video-captions wiki-readme-html wiki-readme-slideshow wiki-visibility wiki-repo-styles wiki-component wiki-component-lod wiki-family wiki-skillpack wiki-hero wiki-breadcrumbs wiki-forking wiki-discovery wiki-scoring wiki-bloat wiki-sub-readme wiki-subreadme wiki-hero-studio wiki-workspaces wiki-brand wiki-hero-glb; do+  for name in adom-wiki-skillpack wiki-anatomy wiki-api wiki-publish-safely wiki-readme wiki-video-captions wiki-readme-html wiki-readme-slideshow wiki-visibility wiki-repo-styles wiki-component wiki-component-lod wiki-family wiki-skillpack wiki-hero wiki-breadcrumbs wiki-forking wiki-contribution-assurance wiki-discovery wiki-scoring wiki-bloat wiki-sub-readme wiki-subreadme wiki-hero-studio wiki-workspaces wiki-brand wiki-hero-glb; do     rm -rf -- "$DEST/$name"   done done echo "Removed this package's skill directories. Other skills are preserved."+--- a/skills/wiki-contribution-assurance/SKILL.md+++ b/skills/wiki-contribution-assurance/SKILL.md@@ -0,0 +1,193 @@+---+name: wiki-contribution-assurance+description: >-+  Audit real software defects and produce independently verifiable Adom Wiki+  contributions. Trigger: "audit a blocker", "trace each code revision",+  "prove the root cause", "make this fix generic", "test before submit",+  "build a complete PR", "validate evidence", "verify the released fix".+---++Parent skill: **adom-wiki-skillpack**++# Contribution Assurance++A generic engineering defect-to-release contract. Its companion tool is+`tools/verify.py`, a read-only Python standard-library verifier. The skill+is applicable to ANY repository and workflow: stage count and board identity+must not be hardcoded. This is not a replacement for repository-specific+`CONTRIBUTING.md`, maintainers, or their actual release acceptance.++## Non-negotiable publication control++**Never create a public issue, PR, Wiki page, fork, remote branch, push,+comment, upload or release without the user's explicit permission to submit+this particular package.** Permission to investigate, repair locally,+test or prepare is not permission to disclose. A previous blanket approval+is not enough after the packet changes. The tool `verify.py` never performs+any network writes, executes packet-supplied commands, or submits anything.++Before requesting submission approval, give the user the complete proposed+diff, sensitive-data check, full revision history, test results, gaps,+target repository, intended visibility and exact operations to be performed.+Only submit AFTER explicit approval. Maintainer merge and release are separately+controlled and never performed by the contribution skill.++## Lifecycle++| Stage | Required outcome | Evidence |+| --- | --- | --- |+| Observe | Actual vs documented expected behavior | Verb/contract, exact call/result, environment |+| Classify | Genuine defect, intended behavior, missing feature, dependency, or unknown | Negative control and alternatives |+| Reproduce | Small self-contained failing fixture | Real commands, exit statuses, logs, hashes |+| Trace | First *provable* relevant revision to current immutable head | Complete per-commit matrix + source comparisons |+| Prove | Causal change, not merely correlated release | Old failing/last good tests or stated limitations |+| Design | General, narrow, compatible remedy | Caller map, contract and alternatives |+| Implement | Isolated source change | Pinned base, patch, rollback, untouched production |+| Test | Reproduce fails before and passes after | Tests, production build and fault injection as applicable |+| Qualify | Evidence verifier and privacy review | Gate report, hashes, complete diff |+| Approve | User permission for this submission | Approval scope, visibility, public artifacts |+| Contribute | Branch PR from exact current head | Remote diff readback and independent review |+| Follow | Maintainer review, merged SHA, released package, executing binary | Native acceptance and original regression smoke |++No stage transitions should rely on a narrative like "I checked it".+Attach machine-readable results and independent observations.++## Defect triage before coding++1. Read the actual current API or behavioral contract.+2. Capture exact failing input, observed output and original code/runtime+   version. Save complete unredacted evidence **privately**.+3. Develop a synthetic fixture that exposes the failure without customer+   designs, credentials or proprietary source. Include a negative control.+4. Determine whether it is a genuine defect. An enhancement request,+   environmental outage or unsupported behavior is not a code defect.+   User-requested *new capabilities* use the separate `feature_request`+   classification, with sponsor, prior-art analysis, design rationale,+   acceptance criteria and backward compatibility instead of invented+   "first bad version" or defect causality.+5. Identify exact owning repository. Distinguish Bridge core, app bridge,+   AI Flow, Hydrogen orchestration, EDA engine, operating system and plugins.+6. Check existing issues/PRs to avoid duplication. Apply least-disruptive+   safe recovery independently of the upstream patch when urgent.++## Historical ancestry: EVERY revision, not selected examples++1. Pin immutable upstream HEAD, record the history retrieval command,+   repository, timestamp, refs and full returned commit list.+2. Follow source across renamed/moved files and symbols; include relevant+   callers, generated contracts and downstream dependencies.+3. Create `revision-matrix.json` with one entry per reachable revision in+   the captured history, in the same order. Use `tools/verify.py make-matrix`+   to generate a **fully unclassified** skeleton, never invented findings.+4. For each revision, compare relevant source state to adjacent ancestry,+   then classify it as `introduced`, `modified`, `preserved`, `reverted`,+   `resolved`, `unrelated`, `absent`, or `unknown`. Record evidence+   path/hash for every causal source-change classification. Record irrelevant+   revisions instead of silently dropping them.+5. Find the last demonstrated good and first demonstrated bad source states,+   test them against the same reproducible fixture where feasible. A+   first recoverable revision already containing a bug is NOT proof that+   that revision introduced the bug.+6. Explicitly disclose shallow history, squashed commits, missing parents,+   unavailable branches, incomplete file migrations, or sparse server logs.+   A matrix complete for the retrieved list alone does not prove that the+   remote repository exposed every ancestor. Cite independently verified+   lineage when making a complete-history claim.+7. If the branch head advances, rebase only intended hunks, rebuild the+   matrix or append missing revisions, rerun affected tests, review the+   actual submitted diff and record new hashes.++The verifier checks matrix-to-log one-to-one alignment and evidence fields;+it cannot independently establish historical causality or whether an upstream+API silently omitted earlier commits. That remains a substantive audit task.++## Generality and mutation safety++Test every relevant call surface and stage, including upstream/downstream+dependencies. Prefer shared contracts over one stage-specific conditional.+Preserve frozen engineering requirements. All mutation APIs must define+input validation, side effects, atomicity, revision fences, idempotent retries,+failure/recovery status and a verified result. Consider concurrency, dropped+replies, malformed input, restart, stale revision, partially applied changes+and dependent resources. Never silently bypass a failed electrical, safety+or fabrication gate.++## Evidence packet++The recommended packet lives outside the repository until permitted to+publish. Every case has:++- `packet.json` with case ID, classification, contract and target repo.+- `history.json` captured from an immutable, documented source.+- `revision-matrix.json` listing every retrieved revision.+- `repro/` safe synthetic fixture(s), before/after expected results.+- `patch.diff` with source hash, base SHA and rollback.+- `tests/` with *actual* exit codes, commands, logs and tool/platform details.+  New features use `absence_before` rather than a fabricated `repro_before`+  defect; substantive target behavior must still pass after the implementation.+- `review.md` with affected callers/stages/platforms, risks, unsupported+  environments, external tests and maintainer responsibilities.+- `reports/` containing `verify.py` output and provenance.++Use **`references/evidence-contract.md`** for the manifest contract and+complete gate semantics. The executable verifier does not execute tests:+tests must be run separately, and their logs/exit codes archived.++## Ten acceptance gates++| Gate | Requirement |+| --- | --- |+| G1 | Genuine defect classified against a supported contract |+| G2 | Reproduction plus negative control, synthetic fixture |+| G3 | Full captured revision history and per-revision matrix |+| G4 | Causal proof, first bad / last good or explicit unknown boundary |+| G5 | Genericity across affected callers/stages/platforms |+| G6 | Minimal hash-pinned diff, rollback and compatibility |+| G7 | Executed tests, negative/positive and production build |+| G8 | Correct owner/branch, privacy scan, current head and diff |+| G9 | Maintainer responsibilities and review state |+| G10 | Actual release, deployed-binary identity, original regression smoke |++Gate states are `pass`, `fail`, `blocked`, `unverified`, or+`not_applicable`, with evidence, even when a proposal is provisional.+The bundled verifier currently derives pass/fail only; other states must+be recorded in maintainer reporting and are never silently converted to pass.++The verifier has four modes:+- `proposal`: preliminary evidence; not submission-ready;+- `candidate_ready`: G1-G7 plus privacy and duplicate checks, before any remote push;+- `merge_ready`: G1-G9, including remote diff and maintainer handoff;+- `operational`: G1-G10, including verified installed runtime.++Do not treat a `candidate_ready` result as permission to submit. This is+the ideal state to present the user with the complete review package and+request explicit approval for exact public operations.++**A successful deterministic report is not permission to submit, nor a+substitute for source inspection or live native acceptance.**++## Companion commands (local, non-publishing)++```bash+python3 tools/verify.py make-matrix --log history.json --out revision-matrix.json+python3 tools/verify.py verify packet.json --phase merge_ready --out reports/check.json+python3 -m unittest discover -s tests -p 'test_*.py' -v+```++`make-matrix` deliberately starts every classification as `unknown`, so+the process cannot falsely pass a history gate by creating a skeleton.+`verify` returns nonzero on incomplete evidence.++## Reuse existing ADOM workflows++- `wiki-issue-loop` for issue resolution, linking and maintainer return.+- `wiki-forking` for branch PR mechanics and remote-diff review.+- `wiki-publish-safely` to protect the Wiki's storage layers.+- `wiki-skillpack` for package contents and install/uninstall mapping.+- Source-specific `CONTRIBUTING.md` for release, CI and native tooling.+- `adom-bridge-sdk-audit` and relevant EDA standard for Bridge changes.++**Do not introduce a mandatory multi-agent approval quorum.**+Use objective checks, bounded recovery and explicit remaining evidence.+One owner, one authoritative branch, one approved patch per fix.+--- a/skills/wiki-contribution-assurance/tools/verify.py+++ b/skills/wiki-contribution-assurance/tools/verify.py@@ -0,0 +1,386 @@+#!/usr/bin/env python3+"""Deterministic preflight for ADOM contribution evidence packets.++No network, git mutation, PR creation, or execution of untrusted packet commands.+Statuses are derived from packet content, not user/agent-approved flags.+"""+import argparse+import hashlib+import json+from pathlib import Path+import re+import sys++SHA = re.compile(r"^[0-9a-f]{40}$")+DIGEST = re.compile(r"^[0-9a-f]{64}$")+ALLOWED = {"introduced", "modified", "preserved", "reverted", "resolved",+           "unrelated", "absent", "unknown"}+PENDING = {"unknown"}+SECRETS = [+    re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"),+    re.compile(r"\bsk-proj-[A-Za-z0-9_-]{12,}"),+    re.compile(r"\bgh[opusr]_[A-Za-z0-9]{24,}"),+    re.compile(r"\bxox[baprs]-[A-Za-z0-9-]{20,}"),+    re.compile(r"(?i)\b(?:api_key|access_token|client_secret)\s*[:=]\s*['\"]?[A-Za-z0-9_-]{25,}"),+]+PHASES = ("proposal", "candidate_ready", "merge_ready", "operational")+++def read_json(path):+    with path.open("r", encoding="utf-8") as f:+        return json.load(f)+++def sha256(path):+    h = hashlib.sha256()+    with path.open("rb") as f:+        while chunk := f.read(1024 * 1024):+            h.update(chunk)+    return h.hexdigest()+++def scoped(root, relative):+    if not isinstance(relative, str) or not relative or Path(relative).is_absolute():+        raise ValueError("artifact path must be non-empty and relative")+    target = (root / relative).resolve()+    if not target.is_relative_to(root.resolve()) or not target.is_file():+        raise ValueError("artifact path leaves evidence root or is missing: " + relative)+    return target+++def check_artifact(root, item):+    if not isinstance(item, dict):+        return False, "artifact must be an object"+    try:+        p = scoped(root, item.get("path"))+    except (ValueError, TypeError) as e:+        return False, str(e)+    digest = item.get("sha256")+    if not isinstance(digest, str) or not DIGEST.fullmatch(digest):+        return False, f"missing or invalid SHA256 for {p.name}"+    if sha256(p) != digest:+        return False, f"SHA256 mismatch for {p.name}"+    return True, ""+++def log_records(value):+    if isinstance(value, list):+        return value+    if isinstance(value, dict):+        return value.get("data", {}).get("log", value.get("log", []))+    return []+++def gate(ok, notes, failures):+    return {"status": "pass" if ok else "fail", "evidence": notes, "failures": failures}+++def verify(packet_path, phase="merge_ready", current_head=None):+    if phase not in PHASES:+        raise ValueError("phase must be proposal, candidate_ready, merge_ready, or operational")+    root = packet_path.resolve().parent+    p = read_json(packet_path)+    if not isinstance(p, dict):+        raise ValueError("evidence packet must be a JSON object")+    for name in ("defect", "reproduction", "repository", "history", "causal",+                 "scope", "patch", "upstream", "privacy", "maintainers", "release"):+        if not isinstance(p.get(name), dict):+            p[name] = {}+    results = {}++    def emit(number, checks, evidence):+        failures = [msg for condition, msg in checks if not condition]+        results[f"G{number}"] = gate(not failures, evidence, failures)++    def goodsha(s):+        return isinstance(s, str) and bool(SHA.fullmatch(s))++    classification = p.get("classification")+    bug = p.get("defect", {})+    feat = p.get("feature") if isinstance(p.get("feature"), dict) else {}+    feature_mode = classification == "feature_request"+    emit(1, [+        (p.get("schemaVersion") == "1.0", "unsupported or missing schemaVersion"),+        (classification in ("genuine_defect", "feature_request", "not_a_defect",+                            "expected_behavior", "external_dependency"),+         "classification is missing or unknown"),+        (isinstance(bug.get("observed"), str) and len(bug["observed"]) >= 12,+         "observed behavior missing"),+        (isinstance(bug.get("expected"), str) and len(bug["expected"]) >= 12,+         "expected behavior missing"),+        (bool(bug.get("contract")), "reference to contract/requirement missing"),+        (classification in ("genuine_defect", "feature_request") or phase == "proposal",+         "merge-ready submissions require either a genuine defect or an explicitly requested new feature"),+        (not feature_mode or (bool(feat.get("requestedBy")) and+             bool(feat.get("acceptanceCriteria"))),+         "feature requests must identify their sponsor and acceptance criteria"),+    ], ["defect", "classification"])++    reproduction = p.get("reproduction", {})+    art = reproduction.get("fixture", {})+    ok_fixture, err_fixture = check_artifact(root, art)+    emit(2, [+        (ok_fixture, err_fixture or "fixture hash required"),+        (bool(reproduction.get("command")), "reproduction command missing"),+        (isinstance(reproduction.get("expectedBefore"), str) and bool(reproduction["expectedBefore"]),+         "original failure outcome missing"),+        (isinstance(reproduction.get("observedBefore"), str) and bool(reproduction["observedBefore"]),+         "observed failure missing"),+        (bool(reproduction.get("negativeControl")), "negative control missing"),+    ], ["reproduction"])++    history = p.get("history", {})+    repo = p.get("repository", {})+    log_file = history.get("log")+    matrix_file = history.get("matrix")+    matrix = []+    commits = []+    history_failures = []+    for name, rel in (("history log", log_file), ("revision matrix", matrix_file)):+        try:+            path = scoped(root, rel)+            if name == "history log":+                commits = log_records(read_json(path))+            else:+                matrix = read_json(path)+        except (TypeError, ValueError, json.JSONDecodeError) as e:+            history_failures.append(f"{name}: {e}")+    hashes = [x.get("hash") if isinstance(x, dict) else None for x in commits] if isinstance(commits, list) else []+    matrix_hashes = [x.get("hash") if isinstance(x, dict) else None for x in matrix] if isinstance(matrix, list) else []+    valid_history = bool(hashes) and all(goodsha(h) for h in hashes) and len(set(hashes)) == len(hashes)+    valid_matrix = isinstance(matrix, list) and hashes == matrix_hashes+    base = repo.get("baseSha")+    changed = []+    unknown = []+    source_evidence = []+    for item in matrix if isinstance(matrix, list) else []:+        if not isinstance(item, dict):+            unknown.append("non-object revision matrix entry")+            continue+        cls = item.get("classification")+        if cls not in ALLOWED:+            unknown.append(str(item.get("hash", "?")) + ": invalid classification")+        elif cls in PENDING:+            unknown.append(str(item.get("hash", "?")) + ": unclassified")+        if cls in ("introduced", "modified", "reverted", "resolved"):+            changed.append(item)+        proof = item.get("evidence", [])+        if not isinstance(proof, list) or not proof:+            source_evidence.append(str(item.get("hash", "?")) + ": revision lacks classification evidence")+    head_ok = goodsha(base) and bool(hashes) and hashes[0] == base+    emit(3, [+        (not history_failures, "; ".join(history_failures)),+        (valid_history, "history must contain unique 40-char commit hashes"),+        (valid_matrix, "matrix must have one entry per commit in the exact log order"),+        (head_ok, "base SHA must match first revision of captured history"),+        (history.get("complete") is True, "history completeness not verified"),+        (history.get("rootSha") == (hashes[-1] if hashes else None),+         "historical root SHA missing/mismatch"),+        (bool(history.get("retrievalCommand")), "source retrieval command missing"),+        (bool(history.get("exhaustionEvidence")), "missing provenance of exhaustive history"),+        (not unknown, "; ".join(unknown[:8])),+        (not source_evidence, "; ".join(source_evidence[:8])),+    ], ["history.log", "history.matrix", "repository.baseSha"])++    causal = p.get("causal", {})+    first_bad = causal.get("firstKnownBad")+    last_good = causal.get("lastKnownGood")+    observed_root = causal.get("rootCause")+    if feature_mode:+        emit(4, [+            (bool(feat.get("priorArtReviewed")), "existing alternatives/related skills not reviewed"),+            (bool(feat.get("designRationale")), "new-feature design rationale missing"),+            (isinstance(feat.get("acceptanceCriteria"), list) and len(feat["acceptanceCriteria"]) >= 1,+             "feature acceptance criteria missing"),+            (bool(feat.get("backwardCompatibility")), "feature backward compatibility missing"),+        ], ["feature"])+    else:+        emit(4, [+            (goodsha(first_bad) and first_bad in hashes, "firstKnownBad not in revision log"),+            ((last_good is None and bool(causal.get("unknownOlderBoundary"))) or+             (goodsha(last_good) and last_good in hashes),+             "lastKnownGood or explicit unknownOlderBoundary required"),+            (isinstance(observed_root, str) and len(observed_root) >= 25, "root cause statement missing"),+            (bool(causal.get("proof")), "causal proof reference missing"),+            (bool(changed), "no relevant source change classified"),+        ], ["causal"])++    scope = p.get("scope", {})+    emit(5, [+        (isinstance(scope.get("callers"), list) and len(scope["callers"]) >= 1,+         "affected callers not enumerated"),+        (isinstance(scope.get("platforms"), list) and len(scope["platforms"]) >= 1,+         "supported/untested platforms not enumerated"),+        (scope.get("stageIndependent") is True,+         "general workflow/stage-independent applicability not established"),+        (bool(scope.get("contractCompatibility")), "API compatibility assessment missing"),+    ], ["scope"])++    patch = p.get("patch", {})+    patch_ok, patch_error = check_artifact(root, patch)+    emit(6, [+        (patch_ok, patch_error or "source patch missing"),+        (patch.get("baseSha") == base, "patch base does not match audited upstream HEAD"),+        (bool(patch.get("diffReviewed")), "patch/diff not reviewed"),+        (bool(patch.get("rollback")), "rollback instructions missing"),+    ], ["patch"])++    raw_tests = p.get("tests", [])+    tests = raw_tests if isinstance(raw_tests, list) else []+    test_failures = []+    if not isinstance(raw_tests, list):+        test_failures.append("tests must be an array of executed test records")+    roles = set()+    if not isinstance(tests, list) or not tests:+        test_failures.append("no executed tests supplied")+    else:+        for t in tests:+            if not isinstance(t, dict):+                test_failures.append("invalid test record")+                continue+            role = t.get("role")+            if role:+                roles.add(role)+            log_ok, error = check_artifact(root, t.get("log", {}))+            if not log_ok:+                test_failures.append(error)+            if not t.get("command") or not t.get("environment"):+                test_failures.append(f"{role}: missing command or environment")+            actual = t.get("exitCode")+            expected = t.get("expectedExitCode")+            if type(actual) is not int or type(expected) is not int or actual != expected:+                test_failures.append(f"{role}: actual exit status does not match declared expectation")+    emit(7, [+        (not test_failures, "; ".join(test_failures[:10])),+        (("absence_before" if feature_mode else "repro_before") in roles,+         "missing baseline before-change test"),+        ("repro_after" in roles, "missing after-patch regression test"),+        ("production_build" in roles, "missing production-build check"),+    ], ["tests"])++    upstream = p.get("upstream", {})+    privacy = p.get("privacy", {})+    scan_fail = []+    paths_to_scan = [packet_path]+    for hist_path in (log_file, matrix_file):+        try:+            paths_to_scan.append(scoped(root, hist_path))+        except (TypeError, ValueError):+            continue+    for record in [art, patch] + [t.get("log", {}) for t in tests if isinstance(t, dict)]:+        try:+            paths_to_scan.append(scoped(root, record.get("path")))+        except (TypeError, ValueError):+            continue+    for f in paths_to_scan:+        if f.stat().st_size > 4 * 1024 * 1024:+            scan_fail.append(f.name + ": artifact too large for inline privacy scan")+            continue+        sample = f.read_bytes().decode("utf-8", "replace")+        for regex in SECRETS:+            if regex.search(sample):+                scan_fail.append(f.name + ": potential credential/private key")+                break+    if current_head is None:+        head_rechecked = upstream.get("recheckedHeadSha") == base+    else:+        head_rechecked = current_head == base+    emit(8, [+        (bool(upstream.get("owner")), "target owner missing"),+        (head_rechecked, "upstream HEAD changed or not rechecked"),+        (upstream.get("duplicateChecked") is True, "duplicate issue/PR lookup missing"),+        (upstream.get("submittedDiffReviewed") is True, "proposed remote diff not inspected"),+        (privacy.get("reviewed") is True and not scan_fail,+         "privacy review incomplete: " + "; ".join(scan_fail)),+        (bool(upstream.get("branch")), "branch lineage missing"),+    ], ["upstream", "privacy"])++    maintainers = p.get("maintainers", {})+    emit(9, [+        (isinstance(maintainers.get("tasks"), list) and bool(maintainers.get("tasks")),+         "maintainer test/review handoff missing"),+        (bool(maintainers.get("reviewStatus")), "maintainer review state missing"),+    ], ["maintainers"])++    release = p.get("release", {})+    emit(10, [+        (goodsha(release.get("mergedSha")), "merge commit missing"),+        (bool(release.get("version")), "release identity missing"),+        (bool(release.get("installedBinaryHash")) and bool(DIGEST.fullmatch(str(release.get("installedBinaryHash")))),+         "installed binary SHA256 missing"),+        (release.get("runtimeVerified") is True, "running binary was not verified"),+        (release.get("regressionSmokePass") is True, "original reproduction not verified on installed release"),+    ], ["release"])++    if phase == "candidate_ready":+        needed = range(1, 8)+    elif phase == "merge_ready":+        needed = range(1, 10)+    elif phase == "operational":+        needed = range(1, 11)+    else:+        needed = range(1, 2)+    missing = [f"G{g}" for g in needed if results[f"G{g}"]["status"] != "pass"]+    if phase == "candidate_ready" and (privacy.get("reviewed") is not True or scan_fail):+        missing.append("PRIVACY")+    if phase == "candidate_ready" and upstream.get("duplicateChecked") is not True:+        missing.append("DUPLICATES")+    result = {+        "schemaVersion": "1.0",+        "caseId": p.get("caseId"),+        "requestedPhase": phase,+        "qualified": not missing,+        "missingGates": missing,+        "gates": results,+        "summary": f"{sum(v['status']=='pass' for v in results.values())}/10 gates have evidence",+        "disclaimer": "Structural evidence verification only. Not external source attestation, permission to submit, proof of real test execution or native acceptance. Human and maintainer review required.",+    }+    return result+++def generate_matrix(log_path, output):+    records = log_records(read_json(log_path))+    if not isinstance(records, list) or not records or len(records) != len(set(x.get("hash") for x in records)):+        raise ValueError("log must be nonempty and have distinct commit hashes")+    rows = []+    for x in records:+        h = x.get("hash")+        if not isinstance(h, str) or not SHA.fullmatch(h):+            raise ValueError("bad commit hash in log")+        rows.append({"hash": h, "timestamp": x.get("timestamp"), "subject": x.get("subject", ""),+                     "classification": "unknown", "files": [], "evidence": [],+                     "sourceSha256": None})+    output.write_text(json.dumps(rows, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")+    return len(rows)+++def main(argv=None):+    cli = argparse.ArgumentParser(description="Validate an evidence packet. Does not submit, merge or publish.")+    sub = cli.add_subparsers(dest="action", required=True)+    check = sub.add_parser("verify")+    check.add_argument("packet", type=Path)+    check.add_argument("--phase", choices=PHASES, default="merge_ready")+    check.add_argument("--current-head", help="explicit remote HEAD SHA from a trusted lookup (required for pre-merge assurance)")+    check.add_argument("--out", type=Path, help="write JSON report")+    matrix = sub.add_parser("make-matrix")+    matrix.add_argument("--log", required=True, type=Path)+    matrix.add_argument("--out", required=True, type=Path)+    args = cli.parse_args(argv)+    try:+        if args.action == "make-matrix":+            print("Revision matrix created:", generate_matrix(args.log, args.out))+            return 0+        result = verify(args.packet, args.phase, args.current_head)+        content = json.dumps(result, indent=2, ensure_ascii=False) + "\n"+        if args.out:+            args.out.write_text(content, encoding="utf-8")+        print(content, end="")+        return 0 if result["qualified"] else 1+    except (OSError, ValueError, json.JSONDecodeError, TypeError) as e:+        print(f"VALIDATION_ERROR: {e}", file=sys.stderr)+        return 2+++if __name__ == "__main__":+    raise SystemExit(main())+--- a/skills/wiki-contribution-assurance/tests/test_verify.py+++ b/skills/wiki-contribution-assurance/tests/test_verify.py@@ -0,0 +1,340 @@+#!/usr/bin/env python3+"""Synthetic regression tests for Contribution Assurance's fail-closed verifier."""+import hashlib+import sys+import json+from pathlib import Path+import tempfile+import unittest++SCRIPT = Path(__file__).resolve().parents[1] / "tools" / "verify.py"+sys.path.insert(0,str(SCRIPT.parent))+mod = __import__("verify")++def h(letter):+    return letter * 40++def artifact(root, name, content):+    p = root / name+    p.parent.mkdir(parents=True, exist_ok=True)+    if isinstance(content, str):+        content = content.encode()+    p.write_bytes(content)+    return {"path": name, "sha256": hashlib.sha256(content).hexdigest()}+++def example(root):+    commits = [+      {"hash": h("c"), "subject": "current correction", "timestamp": 3},+      {"hash": h("b"), "subject": "introduce bad parsing", "timestamp": 2},+      {"hash": h("a"), "subject": "old known good", "timestamp": 1},+    ]+    (root/"history.json").write_text(json.dumps({"data":{"log":commits}}))+    mod.generate_matrix(root/"history.json", root/"matrix.json")+    rows = json.loads((root/"matrix.json").read_text())+    for row, classification in zip(rows, ["modified", "introduced", "absent"]):+        row["classification"] = classification+        row["evidence"] = ["synthetic source comparison for " + row["hash"][:8]]+    (root/"matrix.json").write_text(json.dumps(rows))+    fixture = artifact(root, "repro/fixture.txt", "synthetic fixture no private design")+    patch = artifact(root, "diffs/change.patch", "--- a/code\n+++ b/code\n+ reject unknown fields\n")+    tests = [+      {"role":"repro_before", "command":"run fixture against old version", "environment":"fixture-os",+       "exitCode":1, "expectedExitCode":1,+       "log":artifact(root,"logs/before.log","AssertionError: bad field was accepted")},+      {"role":"repro_after", "command":"run fixture against patch", "environment":"fixture-os",+       "exitCode":0, "expectedExitCode":0,+       "log":artifact(root,"logs/after.log","test result: ok; 1 passed; 0 failed")},+      {"role":"production_build", "command":"compile neutral targets", "environment":"fixture-os",+       "exitCode":0, "expectedExitCode":0,+       "log":artifact(root,"logs/build.log","Finished release build, exit 0")},+    ]+    p = {+      "schemaVersion":"1.0",+      "caseId":"synthetic-move-field",+      "classification":"genuine_defect",+      "defect":{"observed":"An unsupported field is silently ignored.",+                "expected":"Unknown fields must be rejected before commit.",+                "contract":"public verb specification, archived ref SHA"},+      "reproduction":{"fixture":fixture,"command":"run synthetic command",+                      "expectedBefore":"must report unknown argument",+                      "observedBefore":"succeeded and left angle unchanged",+                      "negativeControl":"canonical field applies rotation"},+      "repository":{"ref":"test/example", "baseSha":h("c")},+      "history":{"log":"history.json", "matrix":"matrix.json", "complete":True, "rootSha":h("a"), "retrievalCommand":"synthetic complete commit export",+                 "exhaustionEvidence":"synthetic repository exhausted at root"},+      "causal":{"firstKnownBad":h("b"),"lastKnownGood":h("a"),+                "rootCause":"Parser discards unfamiliar JSON keys without validation.",+                "proof":"synthetic before/after unit regression"},+      "scope":{"callers":["direct","batch"],"platforms":["linux neutral test","windows requires native"],+               "stageIndependent":True,"contractCompatibility":"valid request shapes preserved"},+      "patch":{**patch,"baseSha":h("c"),"diffReviewed":True,"rollback":"restore previous patch"},+      "tests":tests,+      "upstream":{"owner":"test/owner","recheckedHeadSha":h("c"),+                  "duplicateChecked":True,"submittedDiffReviewed":True,"branch":"fix/test"},+      "privacy":{"reviewed":True},+      "maintainers":{"tasks":["Native Windows test and merge review"],"reviewStatus":"draft"},+      "release":{},+    }+    target = root/"packet.json"+    target.write_text(json.dumps(p,indent=2))+    return target,p+++class AssuranceTests(unittest.TestCase):+    def setUp(self):+        self.t = tempfile.TemporaryDirectory()+        self.addCleanup(self.t.cleanup)+        self.root = Path(self.t.name)+        self.path, self.p = example(self.root)++    def update(self):+        self.path.write_text(json.dumps(self.p,indent=2))+        return self.path++    def verify(self, phase="merge_ready", current_head=None):+        return mod.verify(self.update(),phase,current_head)++    def test_valid_synthetic_full_evidence_passes_merge_ready(self):+        v=self.verify()+        self.assertTrue(v["qualified"],v["missingGates"])+        self.assertEqual(v["missingGates"],[])++    def test_proposal_allows_incomplete_but_never_claims_merge(self):+        self.p["tests"]=[]+        v=self.verify("proposal")+        self.assertTrue(v["qualified"])+        self.assertEqual(v["gates"]["G7"]["status"],"fail")+        self.assertEqual(v["summary"],"8/10 gates have evidence")++    def test_non_defect_rejected_at_merge_gate(self):+        self.p["classification"]="expected_behavior"+        v=self.verify()+        self.assertFalse(v["qualified"])+        self.assertIn("G1",v["missingGates"])++    def test_missing_original_failure_evidence_rejected(self):+        self.p["reproduction"]["observedBefore"]=""+        self.assertIn("G2",self.verify()["missingGates"])++    def test_duplicate_revision_rejected(self):+        rows=json.loads((self.root/"matrix.json").read_text())+        rows[2]["hash"]=rows[1]["hash"]+        (self.root/"matrix.json").write_text(json.dumps(rows))+        self.assertIn("G3",self.verify()["missingGates"])++    def test_missing_revision_rejected(self):+        rows=json.loads((self.root/"matrix.json").read_text())+        rows.pop()+        (self.root/"matrix.json").write_text(json.dumps(rows))+        self.assertIn("G3",self.verify()["missingGates"])++    def test_unclassified_revision_rejected(self):+        rows=json.loads((self.root/"matrix.json").read_text())+        rows[0]["classification"]="unknown"+        (self.root/"matrix.json").write_text(json.dumps(rows))+        self.assertIn("G3",self.verify()["missingGates"])++    def test_missing_source_proof_rejected(self):+        rows=json.loads((self.root/"matrix.json").read_text())+        rows[1]["evidence"]=[]+        (self.root/"matrix.json").write_text(json.dumps(rows))+        self.assertIn("G3",self.verify()["missingGates"])++    def test_not_complete_history_blocks_acceptance(self):+        self.p["history"]["complete"]=False+        self.assertIn("G3",self.verify()["missingGates"])++    def test_wrong_head_detected(self):+        self.p["repository"]["baseSha"]=h("d")+        v=self.verify()+        self.assertIn("G3",v["missingGates"])+        self.assertIn("G6",v["missingGates"])++    def test_changed_upstream_head_blocks(self):+        self.assertIn("G8",self.verify(current_head=h("d"))["missingGates"])++    def test_missing_causal_proof_blocks(self):+        self.p["causal"]["proof"]=""+        self.assertIn("G4",self.verify()["missingGates"])++    def test_genericity_is_required(self):+        self.p["scope"]["stageIndependent"]=False+        self.assertIn("G5",self.verify()["missingGates"])++    def test_tampered_patch_detected(self):+        (self.root/"diffs/change.patch").write_text("tampered")+        self.assertIn("G6",self.verify()["missingGates"])++    def test_tampered_test_log_detected(self):+        (self.root/"logs/after.log").write_text("fake pass")+        self.assertIn("G7",self.verify()["missingGates"])++    def test_missing_production_build_blocks(self):+        self.p["tests"]=self.p["tests"][:2]+        self.assertIn("G7",self.verify()["missingGates"])++    def test_test_exit_code_mismatch_blocks(self):+        self.p["tests"][1]["exitCode"]=3+        self.assertIn("G7",self.verify()["missingGates"])++    def test_cross_root_path_rejected(self):+        self.p["patch"]["path"]="../private.key"+        self.assertIn("G6",self.verify()["missingGates"])++    def test_absolute_path_rejected(self):+        self.p["reproduction"]["fixture"]["path"]="/etc/passwd"+        self.assertIn("G2",self.verify()["missingGates"])++    def test_symlink_escape_rejected(self):+        outside = Path(self.t.name).parent/"outside-assurance-test"+        try:+            outside.write_text("oops")+            (self.root/"repro/escape").symlink_to(outside)+            self.p["reproduction"]["fixture"]["path"]="repro/escape"+            self.assertIn("G2",self.verify()["missingGates"])+        finally:+            if outside.exists():outside.unlink()++    def test_secret_scanner_blocks(self):+        p=self.root/"diffs/change.patch"+        p.write_text("-----"+"BEGIN OPENSSH PRIVATE KEY-----\nspoof\n")+        self.p["patch"]["sha256"]=mod.sha256(p)+        self.assertIn("G8",self.verify()["missingGates"])++    def test_no_diffs_checked_blocks(self):+        self.p["upstream"]["submittedDiffReviewed"]=False+        self.assertIn("G8",self.verify()["missingGates"])++    def test_release_cannot_be_claimed_without_runtime(self):+        v=self.verify("operational")+        self.assertFalse(v["qualified"])+        self.assertIn("G10",v["missingGates"])++    def test_release_qualified_with_proof(self):+        self.p["release"]={"mergedSha":h("d"),"version":"2.1.1",+                           "installedBinaryHash":"a"*64,"runtimeVerified":True,+                           "regressionSmokePass":True}+        self.assertTrue(self.verify("operational")["qualified"])++    def test_make_matrix_does_not_invent_causality(self):+        rows=json.loads((self.root/"matrix.json").read_text())+        self.assertEqual([r["classification"] for r in rows],+                         ["modified","introduced","absent"])+        dest=self.root/"draft-matrix.json"+        self.assertEqual(mod.generate_matrix(self.root/"history.json",dest),3)+        self.assertTrue(all(r["classification"]=="unknown" for r in json.loads(dest.read_text())))++    def test_user_requested_feature_can_pass_with_prior_art(self):+        self.p["classification"]="feature_request"+        self.p["feature"]={+            "requestedBy":"explicit user request",+            "acceptanceCriteria":["isolated skill and verifier tests pass"],+            "priorArtReviewed":"reviewed upstream skill map",+            "designRationale":"new assurance contract is not currently covered",+            "backwardCompatibility":"new opt-in skill; old skills unaffected"+        }+        self.p["tests"][0]["role"]="absence_before"+        self.assertTrue(self.verify("candidate_ready")["qualified"])++    def test_feature_without_prior_art_is_blocked(self):+        self.p["classification"]="feature_request"+        self.p["feature"]={"requestedBy":"explicit user request",+            "acceptanceCriteria":["test works"],"priorArtReviewed":"",+            "designRationale":"new need","backwardCompatibility":"additive"}+        self.p["tests"][0]["role"]="absence_before"+        self.assertIn("G4",self.verify("candidate_ready")["missingGates"])++    def test_candidate_ready_before_external_submission(self):+        self.p["upstream"]["submittedDiffReviewed"]=False+        self.assertTrue(self.verify("candidate_ready")["qualified"])+        self.assertFalse(self.verify("merge_ready")["qualified"])++    def test_candidate_requires_privacy_and_duplicate_check(self):+        self.p["privacy"]["reviewed"]=False+        self.assertIn("PRIVACY",self.verify("candidate_ready")["missingGates"])+        self.p["privacy"]["reviewed"]=True+        self.p["upstream"]["duplicateChecked"]=False+        self.assertIn("DUPLICATES",self.verify("candidate_ready")["missingGates"])++    def test_malformed_tests_fail_without_exception(self):+        for value in (None, 3, True, "bogus", {"test": "fake"}):+            self.p["tests"] = value+            outcome = self.verify("candidate_ready")+            self.assertFalse(outcome["qualified"])+            self.assertIn("G7",outcome["missingGates"])++    def test_malformed_packet_shapes_fail_without_traceback(self):+        for mutation in [+            {"defect": []}, {"history": None}, {"patch": "bogus"},+            {"tests": "not-an-array"}, {"scope": 55},+            {"reproduction": 0}+        ]:+            before=dict(self.p)+            self.p.update(mutation)+            try:+                outcome=self.verify("candidate_ready")+                self.assertFalse(outcome["qualified"])+            finally:+                self.p=before++    def test_wrong_schema_version_fails(self):+        self.p["schemaVersion"]="9"+        self.assertIn("G1",self.verify()["missingGates"])++    def test_missing_history_retrieval_provenance_fails(self):+        self.p["history"]["retrievalCommand"]=""+        self.assertIn("G3",self.verify()["missingGates"])++    def test_root_boundary_mismatch_fails(self):+        self.p["history"]["rootSha"]=h("b")+        self.assertIn("G3",self.verify()["missingGates"])++    def test_unrelated_revision_without_evidence_fails(self):+        rows=json.loads((self.root/"matrix.json").read_text())+        rows[0]["classification"]="unrelated"+        rows[0]["evidence"]=[]+        (self.root/"matrix.json").write_text(json.dumps(rows))+        self.assertIn("G3",self.verify()["missingGates"])++    def test_secret_in_history_is_scanned(self):+        path=self.root/"history.json"+        j=json.loads(path.read_text())+        j["data"]["log"][0]["subject"]="-----"+"BEGIN OPENSSH PRIVATE KEY-----"+        path.write_text(json.dumps(j))+        self.assertIn("G8",self.verify()["missingGates"])++    def test_hundreds_of_stages_are_supported(self):+        self.p["scope"]["callers"]=["stage-"+str(x) for x in range(1,3001)]+        self.assertTrue(self.verify()["qualified"])++    def test_three_thousand_revisions_are_still_one_to_one(self):+        commits=[{"hash":f"{i:040x}","subject":"revision","timestamp":i} for i in range(3000,0,-1)]+        (self.root/"history.json").write_text(json.dumps({"data":{"log":commits}}))+        matrix=[{"hash":r["hash"],"classification":"preserved","evidence":["verified synthetic fixture"]} for r in commits]+        matrix[0]["classification"]="modified"+        (self.root/"matrix.json").write_text(json.dumps(matrix))+        self.p["repository"]["baseSha"]=commits[0]["hash"]+        self.p["patch"]["baseSha"]=commits[0]["hash"]+        self.p["upstream"]["recheckedHeadSha"]=commits[0]["hash"]+        self.p["causal"]["firstKnownBad"]=commits[0]["hash"]+        self.p["causal"]["lastKnownGood"]=commits[1]["hash"]+        self.p["history"]["rootSha"]=commits[-1]["hash"]+        self.assertTrue(self.verify()["qualified"],self.verify()["missingGates"])++    def test_bad_commit_hash_rejected_by_generator(self):+        j=json.loads((self.root/"history.json").read_text())+        j["data"]["log"][0]["hash"]="bad"+        (self.root/"history.json").write_text(json.dumps(j))+        with self.assertRaises(ValueError):mod.generate_matrix(self.root/"history.json",self.root/"new.json")++    def test_cannot_insert_second_same_revision(self):+        j=json.loads((self.root/"history.json").read_text())+        j["data"]["log"].append(j["data"]["log"][0])+        (self.root/"history.json").write_text(json.dumps(j))+        with self.assertRaises(ValueError):mod.generate_matrix(self.root/"history.json",self.root/"new.json")+++if __name__ == "__main__":+    unittest.main(verbosity=2)+--- a/skills/wiki-contribution-assurance/references/evidence-contract.md+++ b/skills/wiki-contribution-assurance/references/evidence-contract.md@@ -0,0 +1,224 @@+# Contribution Assurance evidence contract (v1.0)++The Skill Pack delegates executable local checks to `tools/verify.py`. This+spec is intended for *source maintainers and AI contributors*. It does not+authorize publication, supply credentials, or replace the target repository's+contributing rules.++## Completeness and confidence++Separate what the contributor observed from what is inferred:++- **Observed**: exact native tool response, saved file state, process exit code,+  source revision hash or reproducible test.+- **Derived**: conclusions computed deterministically from captured evidence.+- **Inferred**: plausible but not yet independently verified mechanism.+- **Unknown**: unavailable branch/history, tool output, platform, or cause.++Never call an inferred cause proven merely because a matching patch passes tests.+For a genuine fix, attempt an original-fails/revised-passes test using the+same synthetic fixture.++**Never turn absence of a result into a pass.** Terminal status after each+gate must contain its checks, evidence and exact remaining blockers.++## Evidence manifest (packet.json)++```json+{+  "schemaVersion": "1.0",+  "caseId": "synthetic-example",+  "classification": "genuine_defect",+  "defect": {+    "observed": "Observed failure with an exact command and output",+    "expected": "Expected behaviour per source contract",+    "contract": "reference to contract or independent requirement"+  },+  "reproduction": {+    "fixture": {"path": "repro/fixture.json", "sha256": "64 hex chars"},+    "command": "exact reproduction command (not executed by verifier)",+    "expectedBefore": "old result expected to reject",+    "observedBefore": "old implementation accepted",+    "negativeControl": "canonical operation works"+  },+  "repository": {"ref": "owner/repository", "baseSha": "40 hex chars"},+  "history": {+    "log": "history.json",+    "matrix": "revision-matrix.json",+    "complete": true,+    "exhaustionEvidence": "how completeness was independently established"+  },+  "causal": {+    "firstKnownBad": "40 hex chars in history",+    "lastKnownGood": "40 hex chars in history",+    "rootCause": "mechanism demonstrated by source and tests",+    "proof": "reference to before/after test and comparative patch"+  },+  "scope": {+    "callers": ["caller 1", "caller 2"],+    "platforms": ["Linux verified", "Windows native acceptance pending"],+    "stageIndependent": true,+    "contractCompatibility": "valid fields/callers preserved"+  },+  "patch": {+    "path": "patch.diff", "sha256": "64 hex chars",+    "baseSha": "40 hex chars", "diffReviewed": true,+    "rollback": "exact rollback procedure"+  },+  "tests": [+    {+      "role": "repro_before",+      "command": "test command",+      "environment": "OS, tool versions, target",+      "expectedExitCode": 1,+      "exitCode": 1,+      "log": {"path": "tests/before.log", "sha256": "64 hex chars"}+    },+    {+      "role": "repro_after",+      "command": "test command",+      "environment": "OS, tool versions, target",+      "expectedExitCode": 0, "exitCode": 0,+      "log": {"path": "tests/after.log", "sha256": "64 hex chars"}+    },+    {+      "role": "production_build",+      "command": "build command",+      "environment": "OS, tool versions, target",+      "expectedExitCode": 0, "exitCode": 0,+      "log": {"path": "tests/build.log", "sha256": "64 hex chars"}+    }+  ],+  "upstream": {+    "owner": "owner/repository",+    "recheckedHeadSha": "40 hex chars",+    "duplicateChecked": true,+    "submittedDiffReviewed": true,+    "branch": "fix/subject"+  },+  "privacy": {"reviewed": true},+  "maintainers": {+    "tasks": ["native acceptance", "release publication"],+    "reviewStatus": "draft"+  },+  "release": {+    "mergedSha": "40 hex chars",+    "version": "new release",+    "installedBinaryHash": "64 hex chars",+    "runtimeVerified": true,+    "regressionSmokePass": true+  }+}+```++Replace illustrative hashes with *real* SHA256 and commit IDs. The release+portion remains empty until independently verified.++## History matrix++`history.json` is the complete captured `adom-wiki repo log` response (or+an equivalent ordered array of revision records). The matrix must have exactly+one entry for each revision, the same hashes and order, with no duplicates:++```json+[+  {+    "hash": "40 hex chars", "timestamp": 1794006755,+    "subject": "original commit message",+    "classification": "introduced",+    "files": ["path/to/source.rs"],+    "sourceSha256": "64 hex chars",+    "evidence": ["source ref/diff/test log that verifies the classification"]+  }+]+```++`unrelated` and `preserved` should still document the relevant unchanged+source state, not merely carry a guess. `unknown` blocks merge-readiness.+`absent` documents pre-introduction revisions; distinguish file absence+from source relocated to another path.++`history.complete: true` is a **claim** made by the author: the verifier+cannot prove from a self-provided JSON array that a remote repository did+not omit an earlier commit. Retain the retrieval command, external+`repo log` tip/root proof, history range, any shallow/squash/rename limits,+and an independent reviewer attestation. Claim only `earliest recoverable+revision` if the boundary is uncertain.++## Test evidence and trust boundary++The verifier hashes supplied artifacts and checks structural consistency,+but it intentionally does NOT run user-supplied commands. The CI or+maintainer must independently execute them. Exit codes in a manifest are+claims until matched with trusted CI/process evidence. A fabricated+`exitCode: 0` does not become verified just because the JSON parses.++For mutations, include stale revisions, unknown arguments, concurrent+calls, interruption before/after commit, duplicate retries, idempotency,+revision identity, and post-commit readback as appropriate. For read-only+docs, apply proportionate tests and disclose why heavyweight tests do not+apply. It is better to document `not applicable` than invent compliance.++## Generic stage and component handling++Stage numbers and hardware names must never be embedded in core assurance+logic. Instead enumerate detected callers and dependency edges in the+case-specific `scope`, then verify the change for every affected path.+For a 25-stage PCB workflow, do not assume the first failing stage equals+the current UI stage. Preserve the frozen engineering and safety contracts.++## Maintainer responsibility++The contributor provides reproducible evidence, a minimal source patch,+tests, safe install/rollback plan and exact review questions.+The maintainer reviews and merges according to their own governance,+performs platform-native acceptance, publishes a new version and confirms+release metadata. Actual deployment requires a separate runtime check+that proves the running binary contains the repaired implementation.++A merged or published fix is NOT automatically installed, and an installed+package is NOT automatically what the running process is executing.++## Security and permission++- Keep private, full-fidelity incident records in the owner's controlled+  workspace. Use synthetic fixtures for public contributions.+- Use an explicit user review and authorization event BEFORE any external+  upload, issue, PR, branch push or comment.+- Keep source payload and test material sanitised; never include secrets,+  project CAD files, private IP addresses, unpublished circuitry or+  customer information.+- The basic secret detector is a safeguard, not a complete DLP scanner.+- Recheck the upstream base and submitted diff; never push whole-file+  replacements onto a changed head.++## Genuine defects versus new features++`classification:genuine_defect` requires first-known-bad/last-known-good+source analysis and a prepatch failure demonstrating a contract violation.++`classification:feature_request` is permitted only when a user or maintainer+explicitly requests a capability that did not previously exist. It adds:++```json+{+  "feature": {+    "requestedBy": "explicit user or maintainer request",+    "priorArtReviewed": "existing skills, projects and alternatives checked",+    "designRationale": "why a new capability is justified",+    "acceptanceCriteria": ["a separately testable required behavior"],+    "backwardCompatibility": "how existing consumers remain unaffected"+  }+}+```++For this class, G4 checks prior art, rationale, acceptance and compatibility.+G7 uses `absence_before` rather than `repro_before`. The entire available+revision list is still inventoried, but no invented "original defective+revision" is required for a new capability. A mere label change from+`expected_behavior` to `feature_request` without a real sponsor and+criteria must not make a bug-fix claim valid.++**Permission control is separate from these evidence gates.** A passing+`candidate_ready` report does not permit any issue, PR or branch push.+

Comments

Colby Knox ·

Thanks for the detailed proposal, and for running the tests.

Some of what it guards against is already built into the wiki itself, which is where we'd rather keep it:

  • A publish that contains a token or key is refused (SECRET_DETECTED).
  • Uploading a file to a public page warns that anyone can download it and that it goes out in notification emails (ATTACHMENT_PUBLIC).
  • Publishing public content that reads as internal gets a warning (PUBLIC_INTERNAL_CONTENT).
  • Issues are filed as a bug report or a feature request, and a PR links the issues it closes.

Your point about merged versus released is a good one, and we're going to add it to the wiki: an issue fixed by a merged PR will show that the fix isn't in a release yet, and which release it shipped in once it is.

We're keeping the skill pack to how the wiki works. This is a general contribution process for any repository, and inside the pack it would add steps to every PR an agent opens here. It fits better as its own package: you're welcome to publish it under your name and share the link. Closing this PR and #7.

Log in to comment.