Skip to content

chore: version packages #1079

chore: version packages

chore: version packages #1079

Workflow file for this run

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
# Moonshot standing evidence (bet B4.1).
#
# Every headline number in docs/vision/ came out of a script under
# scripts/moonshot/ or tools/world-gym/ that, until this workflow existed, ran
# exactly once, on one laptop, with nothing watching it. Thirteen other CI
# workflows and not one of them touched that tree: the only automated
# protection was @ifc-lite/provenance's own vitest suite. A kernel change
# could silently invalidate eight months of published claims and CI would stay
# green.
#
# This lane re-runs the exams and asserts them. Every step is named after the
# claim it defends, so a failure notification alone says which act broke
# without opening the log -- the same diagnostic style as
# .github/workflows/wide-arithmetic.yml.
#
# E1 provenance unit suite node-hash-v0 + certificates + merge battery
# E2 the four gate demos G0 / G1 / G2-footprint / G2-merge
# E3 B3.5 five-act demo asserted BYTE-FOR-BYTE against a seeded golden
# E3c the tripwire's own exam E3b must go RED on a seeded perturbation
# E4 certified optimization short run -> v1 + v2 certificate chains
# E5 chain verification --mode full AND --mode spot
# E6 tamper batteries v1 (5 cases) + v2 (18 cases)
# E7 gradient battery analytic-vs-finite-difference, 1000 points
# E8 world-gym determinism 20/20 seeds byte-identical
# E9 prose vs artifact every numeral in every REPORT/DESIGN and in
# docs/vision must be backed or marked
#
# E3 is the densest signal in the program. `scripts/moonshot/b35-demo/run.mjs`
# writes a report whose entire `deterministic` subtree is a pure function of
# master seed 20260724 (wall clocks and the timestamp live ONLY under
# `volatile`), so one number moving anywhere across world-gym,
# @ifc-lite/create, @ifc-lite/provenance, the benchmark scorer or the geometry
# kernel surfaces as a named path diff. See scripts/moonshot/ci/assert-b35-golden.mjs.
#
# COST. This is deliberately a cheap, non-blocking lane and MUST NOT become a
# required check:
# - weekly `schedule` + on-demand `workflow_dispatch`, same profile as
# determinism.yml / wide-arithmetic.yml;
# - `push`/`pull_request` ONLY on the paths that can actually invalidate the
# results (the geometry/processing kernel, the packages the demos import,
# the wasm bundle, world-gym, and the moonshot scripts themselves). A
# docs-only or viewer-only PR never sees this job at all.
# - free `ubuntu-latest` runners (public repo), never Depot.
# The measurable work is ~30 s; everything else is toolchain. Nearly all the
# wall clock is the wasm32 build, which the shared Swatinem cargo cache makes
# cheap once warm.
#
# FIXTURE GATE. G0's exam and G1's data-plane scale run both hard-code the
# 169 MB Holter Tower fixture (tests/models/ara3d/ISSUE_053_20181220Holter_Tower_10.ifc).
# Runtime is not the problem (G0 ~7 s, G1 ~6 s once the file is on disk) -- the
# 177 MB pull plus its entry in the repo's shared 10 GB Actions cache is. So
# the scheduled/dispatched runs fetch it and run both; `push`/`pull_request`
# runs fetch only duplex.ifc (2.4 MB) and SKIP those two steps with an
# explicit "skipped: fixture-gated" line in the log AND the step summary --
# never a silent pass. Kernel coverage on the push path is not lost: E2c, E3
# (act 5), E5 and E6 all re-measure through the wasm geometry kernel.
name: Moonshot standing evidence
on:
schedule:
# Weekly, Sunday 06:41 UTC. Deliberately a different day and minute from
# determinism.yml (Mon 03:17) and wide-arithmetic.yml (Wed 04:29) so the
# three weekly lanes never share a scheduler slot.
- cron: '41 6 * * 0'
workflow_dispatch:
inputs:
holter:
description: 'Fetch the 169 MB Holter fixture and run the G0 / G1 scale exams'
type: boolean
default: true
# NOTE: the two path lists below are duplicated verbatim. GitHub Actions does
# not support YAML anchors/aliases in workflow files, so they cannot be
# shared -- keep them in lockstep by hand.
push:
branches: [main]
paths:
# The exact-CSG kernel and the meshing pipeline: E2c, E3 act 5, E5 and
# E6 all re-measure real geometry through the wasm bundle, so a kernel
# change is exactly what must re-run these.
- 'rust/geometry/**'
- 'rust/processing/**'
# Authoring path used by the diff-spike's seeded IFC builds (raw-hash
# commitments in the endpoint certificates depend on it byte-for-byte).
- 'packages/create/**'
# node-hash-v0, certificates, the merge battery: E1 through E3 rest on it.
- 'packages/provenance/**'
# The wasm bundle every kernel re-measurement goes through.
- 'packages/wasm/**'
# Seeded world generation, labeling, reward channels and the benchmark
# scorer: E3 acts 1/3/4 and E8.
- 'tools/world-gym/**'
# The exams themselves.
- 'scripts/moonshot/**'
# The gate record. E9's numeral gate reads these, and a wrong figure has
# already reached this tree once through a docs-only commit.
- 'docs/vision/**'
- '.github/workflows/moonshot.yml'
pull_request:
branches: [main]
# Same list as `push` above. NOTE: path-filtered, non-blocking, and NOT to
# be added to branch protection -- this lane exists to be looked at, not to
# gate merges.
paths:
- 'rust/geometry/**'
- 'rust/processing/**'
- 'packages/create/**'
- 'packages/provenance/**'
- 'packages/wasm/**'
- 'tools/world-gym/**'
- 'scripts/moonshot/**'
- 'docs/vision/**'
- '.github/workflows/moonshot.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# Cancel superseded PR pushes; never cancel a scheduled evidence run.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
env:
# Same early opt-in as the other workflows: GitHub deprecated Node 20 for
# JavaScript actions, and some pinned actions here have no Node-24 release.
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
# Short certified-optimization budget shared by E4/E5/E6. `--segment 96`
# over the 480 recorded steps yields 5 segments, which is what keeps the v2
# tamper battery at its full 18 cases: with the default 256-step segments the
# same run collapses to 2 segments, where `v2-forge-endroot` becomes vacuous
# (the middle segment IS the last one, so its endRoot already equals the
# final state root -> reported SKIPPED) and `v2-spot-sampling-miss` cannot
# run at all (it needs segments > spotK = min(4, segments)). Shrinking the
# segment size rather than growing --rounds/--max-iter buys that coverage for
# free: the optimizer budget, and therefore the runtime, is unchanged.
CERT_ARGS: '--rounds 4 --max-iter 120 --segment 96 --emit-v1'
# Gradient battery point count. The published M3 gate protocol is 1000
# points and the whole battery costs ~1.2 s, so there is nothing to save by
# reducing it -- running the real protocol keeps this step's PASS/FAIL
# identical to the number quoted in docs/vision/.
BATTERY_POINTS: '1000'
jobs:
standing-evidence:
name: Moonshot exams (provenance, gates, B3.5 golden, certificates, determinism)
# Free GitHub-hosted runner -- this lane is weekly + non-blocking, so a cold
# cargo build is acceptable and costs nothing but wall clock.
runs-on: ubuntu-latest
# The exams themselves take ~30 s. The budget is the wasm32 build: ~3-5 min
# with a warm Swatinem cache, up to ~15 min cold. 30 gives cold builds room
# without letting a hang sit forever.
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Fixtures live in a GitHub Release (tests/models/manifest.json,
# AGENTS.md section 9); the repo does not use Git LFS.
lfs: false
persist-credentials: false
- name: Setup pnpm
# v6 reads the version from package.json's `packageManager` field;
# passing `with.version` here would error with ERR_PNPM_BAD_PM_VERSION.
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
# PINNED to an exact patch, not the 22.x line. The B3.5 golden is a
# byte-comparison of an optimizer's output: act 5 runs projected
# gradient descent, and while IEEE arithmetic and JSON.stringify are
# spec-fixed, V8's Math.exp/log/pow are NOT - a V8 update inside the
# same Node major could shift a last-ulp result and turn this lane red
# for a reason that is not a kernel regression. The golden was blessed
# on v22.14.0; re-blessing is a deliberate act (assert-b35-golden.mjs
# --update), so the runtime that produces it is pinned too.
node-version: 22.14.0
cache: pnpm
# Rust nightly (pinned by rust-toolchain.toml) + wasm-pack + cargo cache,
# single-sourced with test.yml / release.yml / sdk-canary.yml /
# benchmark.yml so the toolchain never drifts between lanes.
- name: Setup WASM build toolchain
uses: ./.github/actions/setup-wasm-build
with:
cache-prefix: ci-moonshot
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Built from source on purpose -- no prebuilt fast path here. A published
# bundle would defeat the entire lane: the point is to catch a kernel
# change that invalidates a published number, and that change is in the
# Rust source by definition.
- name: Build WASM bindings from source
run: bash scripts/build-wasm.sh
# The demos import sibling packages' built `dist/` by relative path
# (@ifc-lite/cli, clash, create, data, encoding, geometry, provenance,
# query and their transitive deps). Filtered rather than a bare
# `pnpm build` so the viewer/landing/embed bundles -- which nothing here
# imports -- are never built: 28 turbo tasks instead of 40.
- name: Build workspace packages the exams import
run: |
pnpm turbo build \
--filter=@ifc-lite/cli \
--filter=@ifc-lite/clash \
--filter=@ifc-lite/create \
--filter=@ifc-lite/query \
--filter=@ifc-lite/provenance
# --- Fixtures ------------------------------------------------------
- name: Decide fixture scope
id: fixtures
shell: bash
run: |
scope=minimal
case "${{ github.event_name }}" in
schedule) scope=full ;;
workflow_dispatch) [ "${{ inputs.holter }}" = "false" ] || scope=full ;;
esac
echo "scope=${scope}" >> "$GITHUB_OUTPUT"
if [ "$scope" = full ]; then
echo "fixture scope: FULL (duplex.ifc + the 169 MB Holter Tower) -- G0 and G1 will run."
else
echo "fixture scope: MINIMAL (duplex.ifc only) -- G0 and G1 are fixture-gated on this trigger."
fi
- name: Cache moonshot fixtures
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: tests/models
# Namespaced away from test.yml's `ci-fixtures-...` key, which holds
# the FULL ~994 MiB tree. This lane needs two files; restoring the
# whole tree would cost more than re-downloading them. The scope is
# part of the key so a minimal cache is never mistaken for a full one.
key: ci-moonshot-fixtures-${{ runner.os }}-${{ steps.fixtures.outputs.scope }}-${{ hashFiles('tests/models/manifest.json') }}
# Run unconditionally, cache hit or not: fetch-fixtures.mjs hashes what is
# already on disk and downloads only what is missing or wrong, so this is
# simultaneously the fetch and the integrity check, and a corrupt cache
# entry can never be used silently. FIXTURE_TIMEOUT_MS is raised from its
# 60 s default because that is a per-attempt budget covering the whole
# body: 177 MB on a slow link genuinely exceeds it and burns the retry
# budget on one stalled object (observed locally).
- name: Fetch fixtures (selective)
shell: bash
env:
FIXTURE_TIMEOUT_MS: '600000'
run: |
files=(ara3d/duplex.ifc)
if [ "${{ steps.fixtures.outputs.scope }}" = full ]; then
files+=(ara3d/ISSUE_053_20181220Holter_Tower_10.ifc)
fi
echo "fetching: ${files[*]}"
node scripts/fixtures/fetch-fixtures.mjs "${files[@]}"
# --- E1: the one thing that already had automated protection ---------
- name: E1 provenance unit suite (node-hash-v0, certificates, merge battery)
run: pnpm --filter @ifc-lite/provenance test
# --- E2: the four gate demos, each asserted on exit code -------------
# G0 and G1 are the Holter-gated pair; see the FIXTURE GATE note above.
- name: E2a gate G0 certificate demo (169 MB model, verify < 500 ms)
shell: bash
run: |
if [ "${{ steps.fixtures.outputs.scope }}" != full ]; then
echo "::notice title=E2a G0::skipped: fixture-gated (needs the 169 MB Holter Tower; fetched on schedule/workflow_dispatch only)"
echo '- **E2a gate G0** -- skipped: fixture-gated (Holter 169 MB)' >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
node scripts/moonshot/g0-certificate-demo.mjs
echo '- **E2a gate G0** -- PASS' >> "$GITHUB_STEP_SUMMARY"
- name: E2b gate G1 memoized recompute (mesh run + 100k-node scale run)
shell: bash
run: |
if [ "${{ steps.fixtures.outputs.scope }}" != full ]; then
echo "::notice title=E2b G1::skipped: fixture-gated (its data-plane scale run hard-codes the 169 MB Holter Tower; the mesh-bearing run alone cannot be selected)"
echo '- **E2b gate G1** -- skipped: fixture-gated (Holter 169 MB)' >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
node scripts/moonshot/g1-memoized-recompute.mjs
echo '- **E2b gate G1** -- PASS' >> "$GITHUB_STEP_SUMMARY"
# Meshes duplex.ifc through the wasm kernel, so this one runs on every
# trigger and is the push path's primary geometry tripwire alongside E3/E5.
- name: E2c gate G2 footprint tightness (kernel-meshed conflict predicate)
shell: bash
run: |
node scripts/moonshot/g2-footprint-tightness.mjs
echo '- **E2c gate G2 footprint tightness** -- PASS' >> "$GITHUB_STEP_SUMMARY"
- name: E2d gate G2 merge soundness (1000-schedule property battery)
shell: bash
run: |
node scripts/moonshot/g2-merge-soundness.mjs
echo '- **E2d gate G2 merge soundness** -- PASS' >> "$GITHUB_STEP_SUMMARY"
# --- E3: the five-act demo, byte-for-byte against its seeded golden ---
# Split in two on purpose: a red "run" step means an act threw; a red
# "golden" step means every act completed but a measured number moved.
# Those are different investigations and must not share a step name.
- name: E3a B3.5 five-act demo (run, master seed 20260724)
run: node scripts/moonshot/b35-demo/run.mjs
- name: E3b B3.5 deterministic subtree vs committed golden
shell: bash
run: |
node scripts/moonshot/ci/assert-b35-golden.mjs
echo '- **E3 B3.5 five-act demo** -- deterministic subtree matches golden' >> "$GITHUB_STEP_SUMMARY"
# E3c is the only step in this lane that tests the LANE rather than the
# program. Every other step asserts that something is still true; this one
# asserts that E3b -- the single genuine drift tripwire in the whole
# workflow, per the G4 review's audit of all 17 assertion steps -- would
# actually notice if it were not. It perturbs a carbon factor by 1e-6
# relative, re-runs the demo, requires E3b to go red naming act 5 and
# `kernelValidation/kernelCarbonKg`, then restores the tree and requires
# E3b to go green again. It exits NON-ZERO if the tripwire stays green,
# which is the failure the exam is really about: an assertion that cannot
# fail is not evidence.
#
# SCOPE, stated because the G4 re-review caught the earlier framing
# overclaiming: the perturbed constant is JAVASCRIPT. This step proves the
# golden is wired to act 5 and can fail; it does NOT prove a wasm kernel
# measurement still reaches it (disconnect the kernel and this still
# passes, because kernelCarbonKg is a product with the perturbed
# constant). The kernel half is evidenced out-of-lane by
# `assert-b35-golden.mjs --kernel`, which edits rust/geometry and rebuilds
# the bundle -- minutes and a rust toolchain, i.e. two more wasm builds on
# top of the one this job already pays for. Its committed output is
# scripts/moonshot/ci/kernel-perturbation-evidence.txt.
#
# Cost is ~12 s (two extra demo runs). It rewrites and restores three
# files under try/finally plus SIGINT/SIGTERM handlers and verifies with
# `git status` that it put them back, so a cancelled run cannot leave a
# perturbed checkout behind. It tolerates E3a's fresh `volatile`
# timestamps in demo-report.{json,md} -- what it requires is that the
# status is IDENTICAL afterwards, not that it was pristine before.
#
# The committed output of this step is scripts/moonshot/ci/self-test-evidence.txt.
- name: E3c B3.5 tripwire self-test (the golden must be able to fail)
shell: bash
run: |
node scripts/moonshot/ci/assert-b35-golden.mjs --self-test
echo '- **E3c B3.5 tripwire self-test** -- the golden goes red on a 1e-6 perturbation and green on restore' >> "$GITHUB_STEP_SUMMARY"
# --- E4/E5/E6: proof-carrying optimization ---------------------------
# Scope, honestly: E4 builds the chain and E5/E6 verify it IN THE SAME
# RUN, so the endpoint certificate binds a kernel measurement taken by
# the same binary that E5's re-measurement uses. That makes these steps a
# forgery test (can a tampered chain pass?), not a drift test (did the
# kernel change?) -- verified: a 1e-6 extrusion-depth perturbation in
# rust/geometry leaves all of E4/E5/E6 green because the chain and the
# verifier move together. The standing regression signal against kernel
# drift is E3b, whose golden is committed and therefore does NOT move.
- name: E4 certified optimization run (short budget, v1 + v2 chains)
shell: bash
run: |
# CERT_ARGS is a flag list, not one word -- split it into an array
# rather than relying on unquoted expansion.
read -r -a cert_args <<< "$CERT_ARGS"
node scripts/moonshot/diff-spike/optimize-certified.mjs \
"${cert_args[@]}" --out "$RUNNER_TEMP/certified"
- name: E5a chain verification --mode full (every segment replayed)
run: node scripts/moonshot/diff-spike/verify-trajectory.mjs "$RUNNER_TEMP/certified/trajectory-chain-v2.json" --mode full
- name: E5b chain verification --mode spot (sampled segments)
run: node scripts/moonshot/diff-spike/verify-trajectory.mjs "$RUNNER_TEMP/certified/trajectory-chain-v2.json" --mode spot
- name: E6a tamper battery v1 (per-step chain, 5 forgeries)
shell: bash
run: |
node scripts/moonshot/diff-spike/tamper-test.mjs "$RUNNER_TEMP/certified/trajectory-chain.json" | tee "$RUNNER_TEMP/tamper-v1.log"
grep -q 'tamper test PASS: control verified, 5 tampers detected' "$RUNNER_TEMP/tamper-v1.log" || {
echo "::error title=E6a::v1 battery exited 0 but did not report the expected 5 detected forgeries."
exit 1
}
# The count assertion is the point: a v2 case that silently turns vacuous
# still exits 0 (it reports SKIPPED, not FAIL), so a green exit code alone
# would quietly hide lost coverage. 18 = 12 forgeries + 4 spot-k guards +
# the sampling hit/miss pair, which is what `--segment 96` buys.
- name: E6b tamper battery v2 (checkpointed chain + sidecar, 18 cases)
shell: bash
run: |
node scripts/moonshot/diff-spike/tamper-test.mjs "$RUNNER_TEMP/certified/trajectory-chain-v2.json" | tee "$RUNNER_TEMP/tamper-v2.log"
grep -q 'tamper test PASS (v2): controls verified, 18 tamper cases behaved as specified' "$RUNNER_TEMP/tamper-v2.log" || {
echo "::error title=E6b::v2 battery did not report the full 18-case coverage."
echo "A case reported SKIPPED (vacuous on this chain) or the case count changed."
grep -n 'SKIPPED' "$RUNNER_TEMP/tamper-v2.log" || true
echo "Coverage depends on the chain having > 4 segments AND a middle segment that is not the"
echo "last one; CERT_ARGS in this workflow pins --segment 96 over 480 steps to guarantee both."
exit 1
}
if grep -q 'SKIPPED' "$RUNNER_TEMP/tamper-v2.log"; then
echo "::error title=E6b::a v2 tamper case reported SKIPPED -- coverage silently dropped."
grep -n 'SKIPPED' "$RUNNER_TEMP/tamper-v2.log"
exit 1
fi
echo '- **E4/E5/E6 proof-carrying optimization** -- chain verified (full + spot), 5 v1 + 18 v2 forgeries detected' >> "$GITHUB_STEP_SUMMARY"
# --- E7: the differentiability claim ---------------------------------
- name: E7 gradient battery (analytic vs central finite differences)
shell: bash
run: |
node scripts/moonshot/diff-spike/battery.mjs "$BATTERY_POINTS"
echo "- **E7 gradient battery** -- PASS at ${BATTERY_POINTS} points" >> "$GITHUB_STEP_SUMMARY"
# --- E8: the seeded-world determinism claim --------------------------
- name: E8 world-gym determinism (20/20 seeds byte-identical)
shell: bash
run: |
node tools/world-gym/determinism-check.mjs
echo '- **E8 world-gym determinism** -- 20/20 seeds byte-identical' >> "$GITHUB_STEP_SUMMARY"
# --- E8a: salt intake and containment --------------------------------
# E8 above runs the generator UNSALTED, so until this step existed NOTHING
# in CI executed the salt paths -- and every defect this module has had
# was a SILENT DROP: an argv shape that produced an unsalted run while the
# operator believed it was salted (`--salt-env=VAR` parsed as nothing, a
# trailing `--salt-env` resolving to '', the salt landing on an enumerable
# own property and serializing into artifacts). All three were caught by a
# reviewer, none by a check. A silent drop is invisible to any assertion
# that a run merely finished, so these tests assert WHY each refusal
# happens and that no refusal echoes the secret. The suite is
# mutation-checked: reintroducing any of the four turns it red.
- name: E8a salt intake and containment
shell: bash
run: |
node --test tools/world-gym/lib/salt.test.mjs
echo '- **E8a salt intake** -- 23/23, refusal REASONS asserted, no refusal echoes the salt' >> "$GITHUB_STEP_SUMMARY"
# --- E8b: the B5.5 scan-to-parametric pipeline ------------------------
# B5.5's headline comes off a 3.9 GB client scan that cannot be
# committed, fetched, or run here, so the usual shape of this lane -- a
# fixture-backed re-run asserted against a golden -- is not available for
# it. This step runs the REAL stage scripts (extract -> generate ->
# score) over a synthetic two-room cloud whose every quantity is known in
# closed form, and asserts the numbers: the fitted planes, the room
# areas and heights, the measured partition width, the emitted model's
# ABSOLUTE placement datum, and the scorer's deviation arithmetic against
# deliberate misses placed either side of the 5% bar.
#
# The placement case is why this exists. `addIfcWall`/`addIfcSlab` place
# relative to the storey while `addIfcSpace` places relative to the
# world, so handing the same Z to both puts the walls at 2x the storey
# elevation -- and the exam cannot see it, because every quantity it
# scores is an IfcSpace quantity and therefore invariant under a rigid Z
# shift of the walls. The suite carries a negative control that rebuilds
# the model the broken way and requires the check to reject it.
#
# ~0.5 s: the cloud is 0.4 M points and the models are a few solids.
- name: E8b B5.5 scan-to-parametric pipeline (synthetic, closed-form)
shell: bash
run: |
node scripts/moonshot/ci/b55-pipeline-regression.mjs
echo '- **E8b B5.5 pipeline** -- extraction, placement datum and scoring arithmetic asserted on a synthetic apartment' >> "$GITHUB_STEP_SUMMARY"
# --- E9: the claim NOBODY else in this lane checks --------------------
# Every other step re-runs code and asserts the result. This one reads the
# PROSE and asserts that each numeral in it either matches an artifact or
# carries an inline `<!-- numeral-ok: <token> :: <reason> -->` saying why it
# cannot. The G4 review's section 6 is the reason: five of the seven things
# its audit found un-catchable were the same thing -- "prose that no
# artifact backs", including all three of its hard catches. Re-running a
# battery cannot find a sentence that disagrees with the JSON it came from.
#
# Scope is deliberately wider than the bets: it covers `docs/vision/**` as
# well, because the earlier report-only version was rooted at
# scripts/moonshot and therefore could not see the plans and reviews at all
# -- which is how a wrong B4.5 figure reached the finishing plan's
# amendment list in the commit that was fixing wrong figures.
#
# A STALE marker (one whose numeral has since become backed, or has left
# the document) fails this step too. That is intentional: when another
# bet's branch merges and its figures become checkable, the gate says so
# rather than leaving a permanent hole. The fix is to delete two lines.
- name: E9 prose vs artifact numerals (bets + docs/vision, gated)
shell: bash
run: |
# The gate's own exam FIRST. E9 can only be trusted while its
# `:: none` advisory still fires, and an advisory that has regressed
# into silence is indistinguishable from a clean tree -- so a green
# E9 with a broken advisory is exactly the false pass this lane
# exists to prevent. Same shape as E3c guarding E3b.
node scripts/moonshot/ci/check-report-numerals.mjs --self-test
node scripts/moonshot/ci/check-report-numerals.mjs --gate
echo '- **E9 prose vs artifact** -- every numeral backed by an artifact or marked with a reason' >> "$GITHUB_STEP_SUMMARY"
# Runs even when something above failed, so the summary always records
# which trigger this was and whether the Holter-gated exams were part of
# the evidence. `if: always()` deliberately, but with no `continue-on-error`
# anywhere above -- any red step still fails the job.
- name: Evidence summary
if: always()
shell: bash
env:
SCOPE: ${{ steps.fixtures.outputs.scope }}
run: |
{
echo "### Moonshot standing evidence"
echo ""
echo "trigger: \`${{ github.event_name }}\` | fixture scope: \`${SCOPE:-not-reached}\` | commit: \`${{ github.sha }}\`"
echo ""
if [ -z "$SCOPE" ]; then
echo "> The job failed before the fixture scope was decided -- the failure is in setup"
echo "> (checkout / toolchain / wasm build / package build), not in an exam."
echo ""
elif [ "$SCOPE" != full ]; then
echo "> G0 and G1 were **fixture-gated** on this trigger (the 169 MB Holter Tower is fetched"
echo "> on \`schedule\` / \`workflow_dispatch\` only). Run this workflow manually to include them."
echo ""
fi
} >> "$GITHUB_STEP_SUMMARY"