Skip to content

0.194.1: the two new gates resolve like the old ones (runnable binary… #587

0.194.1: the two new gates resolve like the old ones (runnable binary…

0.194.1: the two new gates resolve like the old ones (runnable binary… #587

Workflow file for this run

name: Validate Template Integrity
on:
push:
branches: [main]
paths:
- 'plugins/mycelium/**'
- '.claude-plugin/**'
- '.claude/**'
- 'tests/**'
- 'docs/**'
- '*.md'
- 'requirements-ci.txt'
- '.github/workflows/validate.yml'
pull_request:
branches: [main]
paths:
- 'plugins/mycelium/**'
- '.claude-plugin/**'
- '.claude/**'
- 'tests/**'
- 'docs/**'
- '*.md'
- 'requirements-ci.txt'
- '.github/workflows/validate.yml'
jobs:
validate:
runs-on: ubuntu-latest
permissions:
contents: read # security review DL-1262: the default token scope was unspecified
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.x'
cache: 'pip'
cache-dependency-path: requirements-ci.txt
- name: Install CI dependencies
run: pip install -r requirements-ci.txt
# Supply-chain gates (security review DL-1262, finding 4). Both are pinned in
# requirements-ci.txt so the auditors are themselves audited.
- name: Audit CI dependencies for known vulnerabilities (pip-audit)
run: pip-audit -r requirements-ci.txt --strict
- name: Lint the workflows themselves (zizmor)
run: zizmor --no-progress --min-severity medium .github/workflows
# Check 33 (plugin tree must not contain unconsented personal identifiers)
# fails open here by design — MYCELIUM_ATTRIBUTION_REGISTRY is intentionally
# unset in CI. The registry lives in a private companion repo to avoid
# publishing the very names whose private-channel attribution it tracks;
# CI on this public repo has no credentials to fetch it. The check's real
# enforcement point is the maintainer's pre-push validation; CI fail-open
# is the accepted residual. If a backstop becomes necessary (e.g., a real
# incident where a leak slipped past pre-push), revisit by piping a
# names-only subset via a GitHub Actions secret.
- name: Run structural integrity validation (includes Check 17 — ruff/shellcheck/pytest with baseline-aware logic)
# Output is teed so the warning-ingestion step below can classify it.
# pipefail is required: without it the step would report tee's exit code
# and a validator failure would pass silently.
run: |
set -o pipefail
bash tests/validate-template.sh 2>&1 | tee validator-output.txt
- name: Ingest CI warnings into the learning loop (classify against warning-handbook)
# Closes the loop this script was shipped for in v0.16.0 and never wired
# into: it was documented as "auto-updated from CI signals" while being
# invoked by nothing for ~2.5 months. Dry-run because a CI runner's
# filesystem is ephemeral — accumulation into .claude/memory/warnings-log.md
# happens on the maintainer's local/pre-push run, where the file persists.
# What CI adds is the live wire: every run exercises the classifier against
# real validator output, so a matcher that stops matching (the v0.60.1
# verify_citations failure mode) surfaces here instead of reading green.
# Non-blocking on findings, blocking on a crash — an ingestor that cannot
# run is a broken wire, which is the whole point of this step.
run: python3 plugins/mycelium/scripts/ingest_warnings.py --dry-run -f validator-output.txt
- name: Run wiring guard (no orphan scripts; plugin-root refs resolve; one path per state file; automation claims kept)
run: python3 plugins/mycelium/scripts/check_wiring.py --root .
- name: Run negative-control guard (every guard's test proves the guard BITES)
# Check 37 asks "does every check have a test?" — never whether that test
# would FAIL if the check broke. verify_citations.py shipped 14 green tests
# while matching 0% of real citations for ~2.5 months. This gates the
# difference between "we tested the sad path" and "we only tested green".
run: python3 plugins/mycelium/scripts/check_negative_control.py --root .
- name: Run test-authenticity guard (every test reaches real production code)
# The third question in the series. check_wiring asks "does this mechanism
# have a caller?"; check_negative_control asks "can this guard fail?";
# this asks "when the test runs, does production code run?" A test can
# import nothing, assert a tautology, or mock away the thing it names and
# still be green AND covered — coverage measures which lines ran, never
# whether the assertion after them meant anything.
run: python3 plugins/mycelium/scripts/check_test_authenticity.py --root .
- name: Run wiring-contract guard (project's own integration conventions hold)
# check_wiring.py enforces Mycelium's OWN hard-coded wiring rules. This
# enforces the contract DERIVED from a repo's majority convention, so it
# works in a consumer project whose joins the framework cannot know.
# Ungoverned files are reported, never silently excluded.
run: python3 plugins/mycelium/scripts/check_wiring_contract.py --root .
- name: Run canvas schema + trace validation
run: python3 plugins/mycelium/scripts/validate_canvas.py
- name: No shipped check reports success over empty input
run: python3 plugins/mycelium/scripts/check_empty_input_honesty.py --root .
- name: Run dead-reference doc check (markdown links resolve in either tree)
run: python3 plugins/mycelium/scripts/check_doc_references.py --root .
- name: Run legacy-path doc check (no stale .claude/{engine,orchestration,schemas}/ refs)
run: python3 plugins/mycelium/scripts/check_legacy_paths.py --root .
- name: Evidence landed — was the confidence it bears on ever looked at again?
# Never demands that a number MOVE. Considered-and-unchanged is a pass; unexamined is not.
# A check that pushed confidence upward would be an inflation engine pointed at the one
# value a project must not inflate.
run: python3 plugins/mycelium/scripts/check_confidence_managed.py --root .
- name: Run purpose-stance check (no sub-element silently contradicts the why/how/what)
# Advisory by default: a project without `purpose_properties` never opted in and
# must not break on a field it does not have. --strict belongs at a diamond
# transition, not here.
run: python3 plugins/mycelium/scripts/check_purpose_stance.py
- name: Run theory-fidelity structural guard (theories.md mechanism refs resolve; gates grounded; no name-only theory)
run: python3 plugins/mycelium/scripts/check_theory_fidelity.py --root .
- name: Promise-registry sweep freshness (did anyone look?)
# ADVISORY here on purpose. The registry gained no row for ten weeks while the
# failure class kept occurring, because its sweep is prose in a skill. This makes
# "nobody looked" visible without breaking a build over a date.
run: python3 plugins/mycelium/scripts/check_promise_registry_swept.py --root .
- name: Enforce CI/local gate parity (no gate may run only in CI)
# A gate that runs only in CI can go red only AFTER a push. Measured
# 2026-08-09 the workflow ran 11 gates and the shipped pre-push hook ran
# 4 — the 2026-06-18 "local validation != CI gates" correction recurring,
# because the fix was a second hand-maintained list rather than one list.
# `local-gate-set.txt` is now the single source and this asserts CI never
# outruns it. Add a step here without adding it there and this fails.
run: python3 plugins/mycelium/scripts/check_gate_parity.py --root .
- name: Fail-open scan (anti-pattern #9 signature) - REPORT ONLY
# Flags silent-default read chains: an `except` whose handler returns a
# benign value and says nothing, so the caller cannot tell "checked, fine"
# from "could not look". Reports rather than fails, because this tree has
# 42 such sites (31 silent) and NO baseline was seeded — a tolerated-debt
# baseline is exactly what ruff.toml forbids. Each needs a written verdict
# in harness/fail-open-reviewed.yml; add --strict once they exist.
# --strict as of 2026-08-26, when every one of the 43 sites got a written
# verdict. It fails ONLY on an UNREVIEWED handler, so the cost of a new
# default-on-exception site is one judgement in fail-open-reviewed.yml —
# which is the point. No baseline was ever seeded.
run: python3 plugins/mycelium/scripts/check_fail_open.py --strict
- name: Field-wiring scan (does a promise-shaped field have a consumer?)
# A field can be blessed by a schema, filled by a skill, and read by NOTHING —
# populated to a human, inert to the machine. Measured 2026-08-31: of 38
# promise-shaped fields, SIX had no consumer, and one of them was added HOURS
# EARLIER in the release that fixed this very class. Care did not survive an
# afternoon, so it is a gate.
# The rule is the founder's and is about PURPOSE, not mechanism: if any writer
# intends the field to be read as part of the pipeline — INCLUDING being rendered
# into a humane form such as a mermaid chart — it is wired. A renderer is a
# consumer. `human` is a legal answer once DECLARED. Only the undeclared case fails.
# --strict from the start: the five existing unwired fields are baselined in
# harness/field-consumers.yml, so this fails only on a NEW one.
run: python3 plugins/mycelium/scripts/check_field_wiring.py --strict
- name: Target progress (is anything comparing an actual against the target?)
# `target_value` sat beside `current_value` and NOTHING read either — the two halves
# of a measurement, adjacent in the same object, never compared. Founder-ruled
# 2026-08-31 to wire it. REPORT-ONLY (no --strict): the canvas legitimately holds
# aspirational targets, and failing a build over an unmeasured one teaches people to
# delete the target rather than measure it, which is the inversion the field exists
# to prevent.
run: python3 plugins/mycelium/scripts/check_target_progress.py --canvas-dir .claude/canvas
- name: touch_log entries are in ascending date order
# Every reader treats touch_log[-1] as "what happened last". An out-of-order
# log looks wrong to nobody and parses cleanly; it just returns the wrong
# answer to the question the file exists to answer. Surfaced in dogfood
# 2026-08-18, where it reported two already-sent replies as unsent.
# Zero files scanned exits 1 — an empty scan is UNKNOWN, never a pass.
run: python3 plugins/mycelium/scripts/check_touch_log_order.py --root .
- name: Run surface-registry guard (every declared reader actually reads its surface)
# right-content-wrong-surface, criterion met 2026-07-26. An artifact written
# to a surface nothing reads is invisible by construction. This verifies the
# WIRING CLAIM -- that each declared reader genuinely references its declared
# surface -- not that any given artifact landed. Open rows (a class with no
# authoritative surface yet) are reported, not failed: they are a recorded gap
# with a decision attached, not a regression.
run: python3 plugins/mycelium/scripts/check_surface_registry.py
- name: Run source-class fidelity guard (the label must match the source beside it)
# Dogfood 2026-08-08: five founder-sourced canvas entries were classed
# `external_human` -- the field meaning a human OUTSIDE the project said it --
# and in three of them the record's own prose already said otherwise. It is
# machine-consumed: check_source_independence.py counts distinct source_class
# values as method diversity, so a mislabel manufactures corroboration that
# does not exist. Every mislabel found pointed the same way: toward more
# external evidence than exists. Also fails on misaligned
# evidence_sources/source_classes arrays, since unequal lengths make every
# index pairing meaningless.
run: python3 plugins/mycelium/scripts/check_source_class_fidelity.py --root .
- name: Run instrument-contract guard (the header is an agent interface, not paperwork)
# THE HEADER IS READ BY MACHINES, WHICH IS WHY THIS IS A GATE AND NOT A REPORT.
# An instrument's contract block is how a later agent FINDS a frozen prediction,
# decides whether it is still live, and knows whether the block it is reading
# was written before the data. A human skims past a malformed header. An agent
# greps for `status: live`, gets nothing, and concludes there are no live
# instruments -- which is the false-absence failure this project logs most.
# Same reasoning as the source-class fidelity guard above: machine-consumed
# fields get gated, because the cost of a wrong one is silent.
run: python3 plugins/mycelium/scripts/check_instrument_contract.py --root .
- name: Run Python unit tests with coverage (verbose output for CI logs)
run: |
pytest tests/python/ -v \
--cov=plugins/mycelium/scripts \
--cov=plugins/mycelium/integrations \
--cov-report=term \
--cov-report=xml \
--cov-report=json \
--cov-fail-under=85
- name: Enforce per-file coverage floor (every shipped script must be tested)
# --cov-fail-under gates the TOTAL; this gates EACH file so a new untested
# script can't hide under the average (the v0.49.6 check_legacy_paths gap).
run: python3 plugins/mycelium/scripts/check_coverage_floor.py --root . --floor 70
# Note: standalone ruff and shellcheck checks are not run here — Check 17
# in validate-template.sh runs both with the correct baseline-aware
# thresholds (pre-existing warnings tolerated; regression above baseline
# fails). Single source of truth.
#
# Path layout (post-2026-05-09 legacy cleanup):
# tests/validate-template.sh — validator (was .claude/tests/)
# tests/python/ — Python unit tests
# plugins/mycelium/scripts/ — framework scripts (validate_canvas.py,
# parse_manifest.py, ingest_warnings.py,
# framework_guard.py, scope_check.py,
# _manifest_lib.py, upgrade.sh)
# .claude/ — project state only (canvas, diamonds,
# memory, evals, harness/decision-log.md,
# jit-tooling/active-metrics.yml,
# manifest.yml, settings.json)