diff --git a/.github/workflows/spec-refs.yml b/.github/workflows/spec-refs.yml new file mode 100644 index 000000000..4ac3cd8db --- /dev/null +++ b/.github/workflows/spec-refs.yml @@ -0,0 +1,45 @@ +name: spec citations resolve + +# Every `SPEC §N` a standard's code cites is a section that standard's SPEC.md +# has. See the header of scripts/check-spec-refs.py for why: a citation to a +# section that does not exist reads as authority it does not have, and the three +# instances found so far were each discovered by reading, long after they were +# written. +# +# Deliberately NO `paths:` filter, for two reasons. +# +# The check is one Python pass over the tree — about a second, no toolchain, no +# cargo — so running it on every change costs less than describing when to run +# it. And a correct filter here would have to name every standard's `**/*.rs` +# plus every `SPEC.md`; `standards-build.yml` records that `**`-prefixed globs +# do not match on this forge the way they do elsewhere, and that a gate which +# quietly fails to fire is worse than no gate, because it reports nothing and +# everyone reads that as green. An unfiltered cheap check cannot fail that way. +# +# The self-test runs first and on its own line. A check that cannot fail is +# decoration, and this one would be easy to break into permanent silence — a +# regex that stops matching finds nothing and exits 0. + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + pull_request: + push: + branches: + - main + +jobs: + spec-refs: + name: Code cites no section its SPEC lacks + runs-on: linux + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: The check can fail (self-test) + run: bash scripts/check-spec-refs-selftest.sh + + - name: Every SPEC citation resolves + run: python3 scripts/check-spec-refs.py diff --git a/aac/aac-rs/crates/aac-core/src/obf.rs b/aac/aac-rs/crates/aac-core/src/obf.rs index aafd7bbc5..78e03821e 100644 --- a/aac/aac-rs/crates/aac-core/src/obf.rs +++ b/aac/aac-rs/crates/aac-core/src/obf.rs @@ -33,7 +33,7 @@ //! button (message banking), travel in the file: the photo as the button's //! image, the recording as its `sound`, each as a `data:` URI when the //! export is given their bytes ([`ObfExportOptions::media`]). -//! - A word hidden in place (SPEC §3.3) is a button with `hidden: true`. +//! - A word hidden in place (SPEC §2.1, §3 item 3) is a button with `hidden: true`. //! - AuraSense extensions: `ext_aurasense_locked` (board and button), //! `ext_aurasense_word_class` (the button's part of speech, from the core //! vocabulary) and `ext_aurasense_provenance` (where each piece came from: diff --git a/aac/aac-rs/crates/aac-web/src/actions.rs b/aac/aac-rs/crates/aac-web/src/actions.rs index 781fb3abb..a2c99ea0e 100644 --- a/aac/aac-rs/crates/aac-web/src/actions.rs +++ b/aac/aac-rs/crates/aac-web/src/actions.rs @@ -93,7 +93,7 @@ pub fn handle_cell_action( }) }) }); - // A hidden word keeps its place but cannot be chosen (SPEC §3.3), unless + // A hidden word keeps its place but cannot be chosen (SPEC §3.1 item 3), unless // hidden words are shown for now: a press there does nothing at all (Help's // message stays, words being said go on). A word with no word in the // person's language yet is hidden in place too, shown or not. diff --git a/aac/aac-rs/crates/aac-web/src/components/grid_cell.rs b/aac/aac-rs/crates/aac-web/src/components/grid_cell.rs index ec905e31e..108ca4527 100644 --- a/aac/aac-rs/crates/aac-web/src/components/grid_cell.rs +++ b/aac/aac-rs/crates/aac-web/src/components/grid_cell.rs @@ -10,7 +10,7 @@ //! always its words. //! //! A photo the person added takes the symbol's place (media.rs). A hidden -//! word (SPEC §3.3) keeps its place but looks like an empty one and cannot +//! word (SPEC §3.1 item 3) keeps its place but looks like an empty one and cannot //! be chosen, unless hidden words are shown for now; in Edit mode it shows //! faded, and every cell, and every empty place, opens the cell editor //! (editing.rs). diff --git a/sandbox/sandbox-rs/crates/sandbox-build/src/lib.rs b/sandbox/sandbox-rs/crates/sandbox-build/src/lib.rs index d16adf8a9..248f8c2dd 100644 --- a/sandbox/sandbox-rs/crates/sandbox-build/src/lib.rs +++ b/sandbox/sandbox-rs/crates/sandbox-build/src/lib.rs @@ -225,7 +225,8 @@ pub struct BuildTask { pub steps: Vec, } -/// The cascade tier that resolved a step (deterministic-first, SPEC §12.1). +/// The cascade tier that resolved a step (deterministic-first, `spec/build.md`; +/// the receipt that records it is SPEC §12). /// A build never invokes a frontier model *inside* the sandbox — execution is /// the deterministic verification — so the only tiers seen here are the cheap /// ones: a content-addressed cache hit, or a fresh deterministic execution. diff --git a/sandbox/sandbox-rs/crates/sandbox-exec-wasm/src/lib.rs b/sandbox/sandbox-rs/crates/sandbox-exec-wasm/src/lib.rs index 437006f73..6aaae5984 100644 --- a/sandbox/sandbox-rs/crates/sandbox-exec-wasm/src/lib.rs +++ b/sandbox/sandbox-rs/crates/sandbox-exec-wasm/src/lib.rs @@ -515,7 +515,8 @@ mod tests { .iter() .any(|e| matches!(e, sandbox_attest::JournalEntry::HostCall { name, .. } if name == "storage_read"))); - // The brokered I/O is metered as a storage line (SPEC §6.5): one write + + // The brokered I/O is metered as a storage line (`spec/receipts.md`, + // storage activity — its own axis, outside §6.2's per-device sum): one write + // one read ⇒ 2 ops, 7 bytes read ("payload"), an honest estimate. let storage = report.storage.expect("storage activity was metered"); assert_eq!(storage.iops, 2); diff --git a/scripts/check-spec-refs-selftest.sh b/scripts/check-spec-refs-selftest.sh new file mode 100755 index 000000000..67e7de4aa --- /dev/null +++ b/scripts/check-spec-refs-selftest.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# +# check-spec-refs-selftest.sh — check-spec-refs.py can fail. +# +# A check that cannot fail is decoration. This builds scratch standards under a +# temporary root and asserts that the check +# 1. passes when every cited section is a heading; +# 2. fails when a cited section is not a heading (the case it exists for); +# 3. accepts both heading spellings, `## 4.` and `## §4`; +# 4. does NOT synthesise parents: a `## 4.10` heading alone must not satisfy a +# `SPEC §4` citation, which is how a standard citing a section it never +# wrote would otherwise pass; +# 5. resolves a citation against the SPEC.md of the standard the file sits in, +# so one standard's section cannot excuse another's citation; +# 6. ignores `*.rs` under a `target/` directory, which is build output. +# Nothing here reads the real tree, and nothing needs a network. +set -uo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +check=$here/check-spec-refs.py +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +fails=0 + +# mk +mk() { + mkdir -p "$1/$2/$(dirname "$4")" + printf '%s\n' "$3" > "$1/$2/SPEC.md" + printf '%s\n' "$5" > "$1/$2/$4" +} + +want() { # want