From 803f11aafd04fcbe11922c972b9262a612737862 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 25 May 2026 08:22:48 +0000 Subject: [PATCH 01/11] CI: fail build when audit-harness citation markers leak into compiled output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a post-build scan step to `.github/workflows/build.yml` that greps `dist/`, `commonjs/`, and `es/` for two internal-only marker patterns: - `\[V[0-9]+\]` — audit-harness citation markers used in spec drafts - `§[[:space:]]*Sources` — section heading used in audit-harness footers Both are conventions from the audit-harness tooling and belong in internal docs/prompts only. If they ever appear in compiled JS it means a comment or string literal slipped through from a spec draft into shipped output — the scan fails the workflow with the offending file path and line number. The step runs after `npm run bundle-all` (which produces the three output directories) and skips gracefully if a directory is missing, so unrelated build failures aren't masked by this guardrail. Manual verification: - Synthesized `dist/foo.js` containing both markers — grep matched both lines and exited 1 with a clear message. - Repeated with clean JS — grep exited 0. - Repeated with no output dirs — step exited 0 (skip path). --- .github/workflows/build.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 13313f8a73..0c17ef8971 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,3 +37,30 @@ jobs: - name: Build run: npm run bundle-all + + - name: Verify no audit-harness markers leaked into build output + shell: bash + run: | + # Fails the build if internal audit-harness citation markers ([V]) + # or `§Sources` footers leak from spec drafts/comments into compiled output. + # Markers belong only in internal docs/prompts, never in shipped JS. + set -u + paths=() + for dir in dist commonjs es; do + if [ -d "$dir" ]; then + paths+=("$dir") + fi + done + if [ ${#paths[@]} -eq 0 ]; then + echo "No build output directories found (dist/, commonjs/, es/); skipping marker scan." + exit 0 + fi + echo "Scanning ${paths[*]} for audit-harness markers..." + if grep -rnE '\[V[0-9]+\]|§[[:space:]]*Sources' "${paths[@]}"; then + echo "" + echo "ERROR: audit-harness markers ([V] or §Sources) found in build output." + echo "These markers are an internal spec-drafting convention and must never" + echo "ship in compiled JS. Strip them from the source comments/strings above." + exit 1 + fi + echo "OK: no audit-harness markers found in build output." From 2a20b008ddd322537d1711f766140b87a2fbb569 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 25 May 2026 13:12:51 +0000 Subject: [PATCH 02/11] CI: branch grep on rc to surface scan errors instead of masking them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bugbot review #3296952334 flagged that the `if grep ...` form treats grep's exit code 2 (scan/IO error) identically to exit code 1 (no matches) — so a permission or read error on dist/, commonjs/, or es/ would silently green- light the step. Split the rc into 0/1/other and fail the step explicitly on any non-zero, non-1 result. --- .github/workflows/build.yml | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0c17ef8971..7287d1c411 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,11 +56,26 @@ jobs: exit 0 fi echo "Scanning ${paths[*]} for audit-harness markers..." - if grep -rnE '\[V[0-9]+\]|§[[:space:]]*Sources' "${paths[@]}"; then - echo "" - echo "ERROR: audit-harness markers ([V] or §Sources) found in build output." - echo "These markers are an internal spec-drafting convention and must never" - echo "ship in compiled JS. Strip them from the source comments/strings above." - exit 1 - fi - echo "OK: no audit-harness markers found in build output." + # grep exit codes: 0=match, 1=no-match, 2+=scan/IO error. A bare + # `if grep` collapses 1 and 2 into the same branch, silently green- + # lighting the step on a read error. Branch on rc explicitly. + set +e + grep -rnE '\[V[0-9]+\]|§[[:space:]]*Sources' "${paths[@]}" + rc=$? + set -e + case "$rc" in + 0) + echo "" + echo "ERROR: audit-harness markers ([V] or §Sources) found in build output." + echo "These markers are an internal spec-drafting convention and must never" + echo "ship in compiled JS. Strip them from the source comments/strings above." + exit 1 + ;; + 1) + echo "OK: no audit-harness markers found in build output." + ;; + *) + echo "ERROR: grep exited with rc=$rc while scanning ${paths[*]} — aborting CI step." >&2 + exit "$rc" + ;; + esac From 1083965744ea676597f849e564c99ad5c4831646 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 25 May 2026 15:16:24 +0000 Subject: [PATCH 03/11] CI: add integration test for audit-marker scan covering source-maps Validates the build.yml marker-scan step against synthetic fixtures: clean build, marker in dist/*.js, marker in dist/*.js.map (sourcesContent), marker in commonjs/*.js, marker in es/*.mjs. Wired as a single self-test step in build.yml that runs once per OS (node 22, ci install). Empirically confirmed (probed by planting a marker in src/index.ts and running `npm run bundle-all`) that source comments survive into: - commonjs/index.js and es/index.mjs (babel preserves comments) - dist/hyperformula{,.full}.js (webpack development build preserves comments) - dist/hyperformula.js.map (`sourcesContent` embeds full original source) All three surfaces are inside the existing `grep -rn dist commonjs es` scope, so the scan already covers source-maps. The new self-test pins this behavior so a future bundler/comment-stripping change cannot silently erode coverage. --- .github/workflows/build.yml | 17 ++++ scripts/test-marker-scan.sh | 154 ++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100755 scripts/test-marker-scan.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7287d1c411..2254a01240 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,6 +44,15 @@ jobs: # Fails the build if internal audit-harness citation markers ([V]) # or `§Sources` footers leak from spec drafts/comments into compiled output. # Markers belong only in internal docs/prompts, never in shipped JS. + # + # Empirical scope notes (probed 2026-05-25, see scripts/test-marker-scan.sh): + # Source comments leak through THREE surfaces and ALL are covered here: + # 1) commonjs/*.js and es/*.mjs (babel preserves comments) + # 2) dist/hyperformula.js and dist/hyperformula.full.js (webpack preserves + # comments in the development build) + # 3) dist/*.js.map (`sourcesContent` embeds full original source, so + # markers survive in source-maps even if stripped from .js) + # `grep -rn` over dist/ catches .map files because they are plain JSON. set -u paths=() for dir in dist commonjs es; do @@ -79,3 +88,11 @@ jobs: exit "$rc" ;; esac + + - name: Self-test marker-scan logic against synthetic fixtures + # Run once per OS (the script is platform-agnostic and only takes <1s). + # Validates that the inline scan above catches markers in dist/*.js, + # dist/*.js.map (sourcesContent), commonjs/*.js, and es/*.mjs surfaces. + if: matrix.node-version == 22 && matrix.install-command == 'ci' + shell: bash + run: bash scripts/test-marker-scan.sh diff --git a/scripts/test-marker-scan.sh b/scripts/test-marker-scan.sh new file mode 100755 index 0000000000..c0ea2d35fb --- /dev/null +++ b/scripts/test-marker-scan.sh @@ -0,0 +1,154 @@ +#!/usr/bin/env bash +# Self-test for the audit-harness marker scan used in .github/workflows/build.yml. +# +# Why this exists: +# The CI step in build.yml greps the build output (dist/, commonjs/, es/) for +# `[V]` and `§Sources` markers — internal spec-drafting tokens that must +# never ship in compiled JS. Empirically (probed 2026-05-25 by planting +# `// [V99] test marker` in src/index.ts and running `npm run bundle-all`), +# markers leak through THREE distinct surfaces: +# 1) babel-transpiled commonjs/*.js and es/*.mjs preserve source comments +# 2) webpack-bundled dist/hyperformula.js and dist/hyperformula.full.js +# preserve source comments (development build, no comment-stripping) +# 3) dist/*.js.map source-maps embed full original source in +# `sourcesContent`, so comments survive into the map even when stripped +# from the .js itself (not applicable here, but defends future configs) +# This script asserts the grep logic catches markers in all three surfaces. +# +# Exit code: 0 on all assertions pass, non-zero on any failure. + +set -uo pipefail + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly TMP_ROOT="$(mktemp -d -t hf-marker-scan-XXXXXX)" +trap 'rm -rf "$TMP_ROOT"' EXIT + +# ---- Mirror of the CI scan logic from .github/workflows/build.yml ---------- +# Keep this in sync with build.yml. The script-under-test must behave +# identically to the YAML inline script. +run_marker_scan() { + local root="$1" + local paths=() + local dir + for dir in dist commonjs es; do + if [ -d "$root/$dir" ]; then + paths+=("$root/$dir") + fi + done + if [ ${#paths[@]} -eq 0 ]; then + echo "No build output directories found; skipping marker scan." + return 0 + fi + local rc=0 + grep -rnE '\[V[0-9]+\]|§[[:space:]]*Sources' "${paths[@]}" || rc=$? + case "$rc" in + 0) return 1 ;; # markers found -> CI would fail + 1) return 0 ;; # no markers -> CI would pass + *) return "$rc" ;; + esac +} + +# ---- Fixture builders ------------------------------------------------------- +make_clean_fixture() { + local root="$1" + mkdir -p "$root/dist" "$root/commonjs" "$root/es" + cat >"$root/dist/hyperformula.js" <<'EOF' +// HyperFormula bundle (synthetic, clean) +const HyperFormula = function() { return 42; }; +module.exports = HyperFormula; +EOF + cat >"$root/dist/hyperformula.js.map" <<'EOF' +{"version":3,"sources":["webpack:///./src/index.ts"],"sourcesContent":["const HyperFormula = function() { return 42; };\nmodule.exports = HyperFormula;\n"],"mappings":"AAAA"} +EOF + cat >"$root/commonjs/index.js" <<'EOF' +"use strict"; +exports.foo = 1; +EOF + cat >"$root/es/index.mjs" <<'EOF' +export const foo = 1; +EOF +} + +# Variant: marker in dist .js file (e.g. preserved source comment). +make_marker_in_dist_js() { + local root="$1" + make_clean_fixture "$root" + cat >>"$root/dist/hyperformula.js" <<'EOF' +// [V12] internal citation marker — must not ship +EOF +} + +# Variant: marker in source-map sourcesContent only (stripped from .js). +make_marker_in_sourcemap() { + local root="$1" + make_clean_fixture "$root" + cat >"$root/dist/hyperformula.js.map" <<'EOF' +{"version":3,"sources":["webpack:///./src/index.ts"],"sourcesContent":["// [V7] citation that survived into sourcesContent\nconst HyperFormula = function() { return 42; };\n"],"mappings":"AAAA"} +EOF +} + +# Variant: §Sources footer leaked into commonjs output. +make_marker_in_commonjs() { + local root="$1" + make_clean_fixture "$root" + cat >>"$root/commonjs/index.js" <<'EOF' +// §Sources: internal/spec.md +EOF +} + +# Variant: marker in es/*.mjs. +make_marker_in_es() { + local root="$1" + make_clean_fixture "$root" + cat >>"$root/es/index.mjs" <<'EOF' +// [V42] another internal token +EOF +} + +# ---- Assertion harness ------------------------------------------------------ +PASS_COUNT=0 +FAIL_COUNT=0 + +assert_scan() { + local name="$1" + local expected="$2" # "clean" -> expect rc 0 ; "dirty" -> expect rc 1 + local root="$3" + + local rc=0 + run_marker_scan "$root" >/tmp/scan-out 2>&1 || rc=$? + + local got + case "$rc" in + 0) got="clean" ;; + 1) got="dirty" ;; + *) got="error($rc)" ;; + esac + + if [ "$got" = "$expected" ]; then + echo "PASS $name (expected=$expected, got=$got)" + PASS_COUNT=$((PASS_COUNT + 1)) + else + echo "FAIL $name (expected=$expected, got=$got)" + echo " scan output:" + sed 's/^/ /' /tmp/scan-out + FAIL_COUNT=$((FAIL_COUNT + 1)) + fi +} + +# ---- Test cases ------------------------------------------------------------- +echo "=== audit-marker scan self-test ===" +echo "Fixture root: $TMP_ROOT" +echo "" + +f="$TMP_ROOT/case-clean"; make_clean_fixture "$f"; assert_scan "clean build (no markers)" "clean" "$f" +f="$TMP_ROOT/case-dist-js"; make_marker_in_dist_js "$f"; assert_scan "marker in dist/*.js comment" "dirty" "$f" +f="$TMP_ROOT/case-dist-map"; make_marker_in_sourcemap "$f"; assert_scan "marker in dist/*.js.map (sourcesContent)" "dirty" "$f" +f="$TMP_ROOT/case-commonjs"; make_marker_in_commonjs "$f"; assert_scan "§Sources in commonjs/*.js" "dirty" "$f" +f="$TMP_ROOT/case-es"; make_marker_in_es "$f"; assert_scan "marker in es/*.mjs" "dirty" "$f" + +echo "" +echo "=== summary: $PASS_COUNT passed, $FAIL_COUNT failed ===" + +if [ "$FAIL_COUNT" -gt 0 ]; then + exit 1 +fi From b9a7ba2ea09418e5ec855c978600dc942e51e3cf Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Tue, 26 May 2026 02:54:20 +0000 Subject: [PATCH 04/11] CI: extract marker-scan into shared script so workflow step and self-test cannot drift The verify step in build.yml previously inlined the audit-marker grep logic while scripts/test-marker-scan.sh kept its own duplicate copy. A workflow-only edit could silently desynchronize the live scan from the self-test fixtures that are supposed to guard it. Move the scan into scripts/marker-scan.sh as a single parameterized entry point (accepts paths as $@, exit 0=clean, 1=dirty, 2+=error). The workflow step now invokes `bash scripts/marker-scan.sh dist commonjs es`, and the self-test drives the SAME script against synthetic fixture roots. --- .github/workflows/build.yml | 53 ++------------------------- scripts/marker-scan.sh | 73 +++++++++++++++++++++++++++++++++++++ scripts/test-marker-scan.sh | 34 ++++++++++------- 3 files changed, 97 insertions(+), 63 deletions(-) create mode 100755 scripts/marker-scan.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2254a01240..b1dcb4eadd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,59 +39,14 @@ jobs: run: npm run bundle-all - name: Verify no audit-harness markers leaked into build output + # Delegates to scripts/marker-scan.sh so the live CI scan and the + # self-test below exercise the same code path and cannot drift. shell: bash - run: | - # Fails the build if internal audit-harness citation markers ([V]) - # or `§Sources` footers leak from spec drafts/comments into compiled output. - # Markers belong only in internal docs/prompts, never in shipped JS. - # - # Empirical scope notes (probed 2026-05-25, see scripts/test-marker-scan.sh): - # Source comments leak through THREE surfaces and ALL are covered here: - # 1) commonjs/*.js and es/*.mjs (babel preserves comments) - # 2) dist/hyperformula.js and dist/hyperformula.full.js (webpack preserves - # comments in the development build) - # 3) dist/*.js.map (`sourcesContent` embeds full original source, so - # markers survive in source-maps even if stripped from .js) - # `grep -rn` over dist/ catches .map files because they are plain JSON. - set -u - paths=() - for dir in dist commonjs es; do - if [ -d "$dir" ]; then - paths+=("$dir") - fi - done - if [ ${#paths[@]} -eq 0 ]; then - echo "No build output directories found (dist/, commonjs/, es/); skipping marker scan." - exit 0 - fi - echo "Scanning ${paths[*]} for audit-harness markers..." - # grep exit codes: 0=match, 1=no-match, 2+=scan/IO error. A bare - # `if grep` collapses 1 and 2 into the same branch, silently green- - # lighting the step on a read error. Branch on rc explicitly. - set +e - grep -rnE '\[V[0-9]+\]|§[[:space:]]*Sources' "${paths[@]}" - rc=$? - set -e - case "$rc" in - 0) - echo "" - echo "ERROR: audit-harness markers ([V] or §Sources) found in build output." - echo "These markers are an internal spec-drafting convention and must never" - echo "ship in compiled JS. Strip them from the source comments/strings above." - exit 1 - ;; - 1) - echo "OK: no audit-harness markers found in build output." - ;; - *) - echo "ERROR: grep exited with rc=$rc while scanning ${paths[*]} — aborting CI step." >&2 - exit "$rc" - ;; - esac + run: bash scripts/marker-scan.sh dist commonjs es - name: Self-test marker-scan logic against synthetic fixtures # Run once per OS (the script is platform-agnostic and only takes <1s). - # Validates that the inline scan above catches markers in dist/*.js, + # Validates that scripts/marker-scan.sh catches markers in dist/*.js, # dist/*.js.map (sourcesContent), commonjs/*.js, and es/*.mjs surfaces. if: matrix.node-version == 22 && matrix.install-command == 'ci' shell: bash diff --git a/scripts/marker-scan.sh b/scripts/marker-scan.sh new file mode 100755 index 0000000000..e64b30ec4a --- /dev/null +++ b/scripts/marker-scan.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Shared audit-harness marker scan used by both the CI verify step in +# .github/workflows/build.yml and the self-test in scripts/test-marker-scan.sh. +# +# Centralizing the scan in one script keeps the live CI logic and its self-test +# from drifting independently — a workflow-only edit would otherwise miss the +# self-test fixture coverage. +# +# Why this scan exists: +# Greps build output for `[V]` and `§Sources` markers — internal +# spec-drafting tokens that must never ship in compiled JS. Source comments +# leak through THREE surfaces and ALL are covered here: +# 1) commonjs/*.js and es/*.mjs (babel preserves comments) +# 2) dist/hyperformula.js and dist/hyperformula.full.js (webpack preserves +# comments in the development build) +# 3) dist/*.js.map (`sourcesContent` embeds full original source, so markers +# survive in source-maps even if stripped from .js) +# `grep -rn` over dist/ catches .map files because they are plain JSON. +# +# Usage: +# bash scripts/marker-scan.sh [ ...] +# +# Exit codes: +# 0 — scan ran cleanly, no markers found (or no requested dirs exist) +# 1 — markers found in at least one scanned file +# 2+ — grep failed with an I/O / scan error; the caller must surface this + +set -u + +if [ "$#" -lt 1 ]; then + echo "usage: $0 [ ...]" >&2 + exit 2 +fi + +paths=() +for dir in "$@"; do + if [ -d "$dir" ]; then + paths+=("$dir") + fi +done + +if [ ${#paths[@]} -eq 0 ]; then + echo "No build output directories found ($*); skipping marker scan." + exit 0 +fi + +echo "Scanning ${paths[*]} for audit-harness markers..." + +# grep exit codes: 0=match, 1=no-match, 2+=scan/IO error. A bare `if grep` +# collapses 1 and 2 into the same branch, silently green-lighting on read +# errors. Branch on rc explicitly. +set +e +grep -rnE '\[V[0-9]+\]|§[[:space:]]*Sources' "${paths[@]}" +rc=$? +set -e + +case "$rc" in + 0) + echo "" + echo "ERROR: audit-harness markers ([V] or §Sources) found in build output." + echo "These markers are an internal spec-drafting convention and must never" + echo "ship in compiled JS. Strip them from the source comments/strings above." + exit 1 + ;; + 1) + echo "OK: no audit-harness markers found in build output." + exit 0 + ;; + *) + echo "ERROR: grep exited with rc=$rc while scanning ${paths[*]} — aborting scan." >&2 + exit "$rc" + ;; +esac diff --git a/scripts/test-marker-scan.sh b/scripts/test-marker-scan.sh index c0ea2d35fb..4b1e0f2ab5 100755 --- a/scripts/test-marker-scan.sh +++ b/scripts/test-marker-scan.sh @@ -2,30 +2,34 @@ # Self-test for the audit-harness marker scan used in .github/workflows/build.yml. # # Why this exists: -# The CI step in build.yml greps the build output (dist/, commonjs/, es/) for -# `[V]` and `§Sources` markers — internal spec-drafting tokens that must -# never ship in compiled JS. Empirically (probed 2026-05-25 by planting -# `// [V99] test marker` in src/index.ts and running `npm run bundle-all`), -# markers leak through THREE distinct surfaces: +# The CI step in build.yml invokes `scripts/marker-scan.sh dist commonjs es` +# to grep the build output for `[V]` and `§Sources` markers — internal +# spec-drafting tokens that must never ship in compiled JS. Empirically +# (probed 2026-05-25 by planting `// [V99] test marker` in src/index.ts and +# running `npm run bundle-all`), markers leak through THREE distinct surfaces: # 1) babel-transpiled commonjs/*.js and es/*.mjs preserve source comments # 2) webpack-bundled dist/hyperformula.js and dist/hyperformula.full.js # preserve source comments (development build, no comment-stripping) # 3) dist/*.js.map source-maps embed full original source in # `sourcesContent`, so comments survive into the map even when stripped # from the .js itself (not applicable here, but defends future configs) -# This script asserts the grep logic catches markers in all three surfaces. +# This script exercises the SAME scripts/marker-scan.sh that CI runs, against +# synthetic fixtures covering all three surfaces — so the workflow step and +# the self-test cannot drift independently. # # Exit code: 0 on all assertions pass, non-zero on any failure. set -uo pipefail readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly MARKER_SCAN="$SCRIPT_DIR/marker-scan.sh" readonly TMP_ROOT="$(mktemp -d -t hf-marker-scan-XXXXXX)" trap 'rm -rf "$TMP_ROOT"' EXIT -# ---- Mirror of the CI scan logic from .github/workflows/build.yml ---------- -# Keep this in sync with build.yml. The script-under-test must behave -# identically to the YAML inline script. +# ---- Adapter: drives the shared scan against a synthetic fixture root ------ +# Invokes scripts/marker-scan.sh — the same script CI runs — passing the +# fixture's dist/commonjs/es subdirectories. This is the only place where the +# self-test couples to the scan; the scan logic itself lives in marker-scan.sh. run_marker_scan() { local root="$1" local paths=() @@ -36,14 +40,16 @@ run_marker_scan() { fi done if [ ${#paths[@]} -eq 0 ]; then - echo "No build output directories found; skipping marker scan." - return 0 + # Mirror the script's "no dirs" branch so the assertion harness treats this + # as a clean (rc=0) outcome. + bash "$MARKER_SCAN" "$root/__missing__" + return $? fi local rc=0 - grep -rnE '\[V[0-9]+\]|§[[:space:]]*Sources' "${paths[@]}" || rc=$? + bash "$MARKER_SCAN" "${paths[@]}" || rc=$? case "$rc" in - 0) return 1 ;; # markers found -> CI would fail - 1) return 0 ;; # no markers -> CI would pass + 0) return 0 ;; # no markers -> CI would pass + 1) return 1 ;; # markers found -> CI would fail *) return "$rc" ;; esac } From e6a8c1994b7c657902348a04621480b891e8947d Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Sun, 31 May 2026 21:51:41 +0000 Subject: [PATCH 05/11] fix(ci): scan for current lowercase markers and cover typings/languages output marker-scan grepped only the legacy [V] form, so current markers ([vrf_1], [dec_3], ...) passed the gate; it also scanned only dist/commonjs/es, missing the typings/ and languages/ bundle outputs (both preserve source comments). Extend the grep to the lowercase prefix+_digits grammar, add typings+languages to the CI invocation and the self-test dir list, and add fixtures for both gaps. --- .github/workflows/build.yml | 2 +- scripts/marker-scan.sh | 11 +++++++---- scripts/test-marker-scan.sh | 30 +++++++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b1dcb4eadd..e785a9ed43 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,7 +42,7 @@ jobs: # Delegates to scripts/marker-scan.sh so the live CI scan and the # self-test below exercise the same code path and cannot drift. shell: bash - run: bash scripts/marker-scan.sh dist commonjs es + run: bash scripts/marker-scan.sh dist commonjs es typings languages - name: Self-test marker-scan logic against synthetic fixtures # Run once per OS (the script is platform-agnostic and only takes <1s). diff --git a/scripts/marker-scan.sh b/scripts/marker-scan.sh index e64b30ec4a..9fa7929623 100755 --- a/scripts/marker-scan.sh +++ b/scripts/marker-scan.sh @@ -7,9 +7,12 @@ # self-test fixture coverage. # # Why this scan exists: -# Greps build output for `[V]` and `§Sources` markers — internal -# spec-drafting tokens that must never ship in compiled JS. Source comments -# leak through THREE surfaces and ALL are covered here: +# Greps build output for audit-harness citation markers and the `§Sources` +# footer — internal spec-drafting tokens that must never ship in compiled +# output. Markers are the current lowercase prefixed form `[vrf_1]`/`[dec_3]`/ +# `[con_2]`/`[que_5]`/`[wrg_7]`/`[crf_4]` (parser `^\[[a-z][a-z0-9_]*\]$`) plus +# the legacy `[V]` form. Source comments leak through THREE surfaces and +# ALL are covered here: # 1) commonjs/*.js and es/*.mjs (babel preserves comments) # 2) dist/hyperformula.js and dist/hyperformula.full.js (webpack preserves # comments in the development build) @@ -50,7 +53,7 @@ echo "Scanning ${paths[*]} for audit-harness markers..." # collapses 1 and 2 into the same branch, silently green-lighting on read # errors. Branch on rc explicitly. set +e -grep -rnE '\[V[0-9]+\]|§[[:space:]]*Sources' "${paths[@]}" +grep -rnE '\[(V[0-9]+|(vrf|dec|con|que|wrg|crf)_[0-9]+)\]|§[[:space:]]*Sources' "${paths[@]}" rc=$? set -e diff --git a/scripts/test-marker-scan.sh b/scripts/test-marker-scan.sh index 4b1e0f2ab5..500771e8f6 100755 --- a/scripts/test-marker-scan.sh +++ b/scripts/test-marker-scan.sh @@ -34,7 +34,8 @@ run_marker_scan() { local root="$1" local paths=() local dir - for dir in dist commonjs es; do + # Mirror the build.yml invocation exactly so the self-test and CI cannot drift. + for dir in dist commonjs es typings languages; do if [ -d "$root/$dir" ]; then paths+=("$root/$dir") fi @@ -111,6 +112,31 @@ make_marker_in_es() { EOF } +# Variant: CURRENT lowercase prefixed marker (e.g. [vrf_3]) in commonjs output. +# The pre-2026-05-21 [V] form is legacy; real specs emit [vrf_/dec_/con_/ +# que_/wrg_/crf_]_. Isolates the scan-PATTERN coverage. +make_marker_in_commonjs_current() { + local root="$1" + make_clean_fixture "$root" + cat >>"$root/commonjs/index.js" <<'EOF' +// [vrf_3] current-grammar citation marker — must not ship +EOF +} + +# Variant: marker leaked into typings/*.d.ts. `bundle:typings` (tsc -d) and +# `bundle:languages` are build outputs that preserve source comments, so they +# are leak surfaces too. Uses a legacy [V] marker to isolate the DIRECTORY- +# coverage defect from the pattern defect. +make_marker_in_typings() { + local root="$1" + make_clean_fixture "$root" + mkdir -p "$root/typings" + cat >"$root/typings/index.d.ts" <<'EOF' +/** Public API surface. [V12] citation leaked into declarations. */ +export declare const foo: number; +EOF +} + # ---- Assertion harness ------------------------------------------------------ PASS_COUNT=0 FAIL_COUNT=0 @@ -151,6 +177,8 @@ f="$TMP_ROOT/case-dist-js"; make_marker_in_dist_js "$f"; assert_scan f="$TMP_ROOT/case-dist-map"; make_marker_in_sourcemap "$f"; assert_scan "marker in dist/*.js.map (sourcesContent)" "dirty" "$f" f="$TMP_ROOT/case-commonjs"; make_marker_in_commonjs "$f"; assert_scan "§Sources in commonjs/*.js" "dirty" "$f" f="$TMP_ROOT/case-es"; make_marker_in_es "$f"; assert_scan "marker in es/*.mjs" "dirty" "$f" +f="$TMP_ROOT/case-commonjs-current"; make_marker_in_commonjs_current "$f"; assert_scan "current [vrf_n] marker in commonjs/*.js" "dirty" "$f" +f="$TMP_ROOT/case-typings"; make_marker_in_typings "$f"; assert_scan "marker in typings/*.d.ts" "dirty" "$f" echo "" echo "=== summary: $PASS_COUNT passed, $FAIL_COUNT failed ===" From 25a566a6716f0d90fb6c628319bcccd69f8ea25d Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Tue, 2 Jun 2026 07:01:11 +0000 Subject: [PATCH 06/11] =?UTF-8?q?fix(ci):=20rename=20audit-harness=20foote?= =?UTF-8?q?r=20marker=20=C2=A7Sources=20to=20=C2=A7AuditSources=20in=20bui?= =?UTF-8?q?ld-output=20scan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/marker-scan.sh | 6 +++--- scripts/test-marker-scan.sh | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/marker-scan.sh b/scripts/marker-scan.sh index 9fa7929623..cd304ce6da 100755 --- a/scripts/marker-scan.sh +++ b/scripts/marker-scan.sh @@ -7,7 +7,7 @@ # self-test fixture coverage. # # Why this scan exists: -# Greps build output for audit-harness citation markers and the `§Sources` +# Greps build output for audit-harness citation markers and the `§AuditSources` # footer — internal spec-drafting tokens that must never ship in compiled # output. Markers are the current lowercase prefixed form `[vrf_1]`/`[dec_3]`/ # `[con_2]`/`[que_5]`/`[wrg_7]`/`[crf_4]` (parser `^\[[a-z][a-z0-9_]*\]$`) plus @@ -53,14 +53,14 @@ echo "Scanning ${paths[*]} for audit-harness markers..." # collapses 1 and 2 into the same branch, silently green-lighting on read # errors. Branch on rc explicitly. set +e -grep -rnE '\[(V[0-9]+|(vrf|dec|con|que|wrg|crf)_[0-9]+)\]|§[[:space:]]*Sources' "${paths[@]}" +grep -rnE '\[(V[0-9]+|(vrf|dec|con|que|wrg|crf)_[0-9]+)\]|§[[:space:]]*AuditSources' "${paths[@]}" rc=$? set -e case "$rc" in 0) echo "" - echo "ERROR: audit-harness markers ([V] or §Sources) found in build output." + echo "ERROR: audit-harness markers ([V]/[vrf_n] or §AuditSources) found in build output." echo "These markers are an internal spec-drafting convention and must never" echo "ship in compiled JS. Strip them from the source comments/strings above." exit 1 diff --git a/scripts/test-marker-scan.sh b/scripts/test-marker-scan.sh index 500771e8f6..1390e06e4a 100755 --- a/scripts/test-marker-scan.sh +++ b/scripts/test-marker-scan.sh @@ -94,12 +94,12 @@ make_marker_in_sourcemap() { EOF } -# Variant: §Sources footer leaked into commonjs output. +# Variant: §AuditSources footer leaked into commonjs output. make_marker_in_commonjs() { local root="$1" make_clean_fixture "$root" cat >>"$root/commonjs/index.js" <<'EOF' -// §Sources: internal/spec.md +// §AuditSources: internal/spec.md EOF } @@ -175,7 +175,7 @@ echo "" f="$TMP_ROOT/case-clean"; make_clean_fixture "$f"; assert_scan "clean build (no markers)" "clean" "$f" f="$TMP_ROOT/case-dist-js"; make_marker_in_dist_js "$f"; assert_scan "marker in dist/*.js comment" "dirty" "$f" f="$TMP_ROOT/case-dist-map"; make_marker_in_sourcemap "$f"; assert_scan "marker in dist/*.js.map (sourcesContent)" "dirty" "$f" -f="$TMP_ROOT/case-commonjs"; make_marker_in_commonjs "$f"; assert_scan "§Sources in commonjs/*.js" "dirty" "$f" +f="$TMP_ROOT/case-commonjs"; make_marker_in_commonjs "$f"; assert_scan "§AuditSources in commonjs/*.js" "dirty" "$f" f="$TMP_ROOT/case-es"; make_marker_in_es "$f"; assert_scan "marker in es/*.mjs" "dirty" "$f" f="$TMP_ROOT/case-commonjs-current"; make_marker_in_commonjs_current "$f"; assert_scan "current [vrf_n] marker in commonjs/*.js" "dirty" "$f" f="$TMP_ROOT/case-typings"; make_marker_in_typings "$f"; assert_scan "marker in typings/*.d.ts" "dirty" "$f" From 542062c1e9a03515c10531745eda6ea6d206b4fc Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Tue, 2 Jun 2026 11:25:37 +0000 Subject: [PATCH 07/11] docs(ci): fix stale marker references in marker-scan self-test header --- scripts/test-marker-scan.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/test-marker-scan.sh b/scripts/test-marker-scan.sh index 1390e06e4a..e330a0e73e 100755 --- a/scripts/test-marker-scan.sh +++ b/scripts/test-marker-scan.sh @@ -2,8 +2,9 @@ # Self-test for the audit-harness marker scan used in .github/workflows/build.yml. # # Why this exists: -# The CI step in build.yml invokes `scripts/marker-scan.sh dist commonjs es` -# to grep the build output for `[V]` and `§Sources` markers — internal +# The CI step in build.yml invokes +# `scripts/marker-scan.sh dist commonjs es typings languages` to grep the build +# output for `[V]`/`[vrf_n]` citation markers and the `§AuditSources` footer — internal # spec-drafting tokens that must never ship in compiled JS. Empirically # (probed 2026-05-25 by planting `// [V99] test marker` in src/index.ts and # running `npm run bundle-all`), markers leak through THREE distinct surfaces: From 11747dd91d77b78f90783a66bedfe308d6f86eeb Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Wed, 1 Jul 2026 11:30:11 +0000 Subject: [PATCH 08/11] ci: also scan docs/api and docs/guide for citation markers Per review: extend the marker scan to the documentation surfaces. docs/guide (hand-written markdown) was previously unscanned and is a real leak path; it is present in the checkout and now covered. docs/api is generated by a separate docs build, so marker-scan.sh scans it only when present (it skips any missing path). --- .github/workflows/build.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e785a9ed43..b60be0afd0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,11 +38,14 @@ jobs: - name: Build run: npm run bundle-all - - name: Verify no audit-harness markers leaked into build output + - name: Verify no audit-harness markers leaked into build output or docs # Delegates to scripts/marker-scan.sh so the live CI scan and the # self-test below exercise the same code path and cannot drift. + # docs/guide (hand-written) is scanned from the checkout; docs/api is + # generated by a separate docs build, so it is only scanned when present + # (marker-scan.sh skips any path that does not exist). shell: bash - run: bash scripts/marker-scan.sh dist commonjs es typings languages + run: bash scripts/marker-scan.sh dist commonjs es typings languages docs/api docs/guide - name: Self-test marker-scan logic against synthetic fixtures # Run once per OS (the script is platform-agnostic and only takes <1s). From 077dcc19ebc07e29cc2aacf44edfe3afae4b12de Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 16 Jul 2026 12:08:18 +0000 Subject: [PATCH 09/11] fix(ci): write self-test scan output under trapped TMP_ROOT, not /tmp assert_scan wrote to a hardcoded /tmp/scan-out, bypassing the `trap 'rm -rf "$TMP_ROOT"' EXIT` cleanup and racing on a shared path. Redirect to "$TMP_ROOT/scan-out" so it lives under the per-run mktemp dir and is cleaned up with the rest of the fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/test-marker-scan.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/test-marker-scan.sh b/scripts/test-marker-scan.sh index e330a0e73e..fc3dcb70ce 100755 --- a/scripts/test-marker-scan.sh +++ b/scripts/test-marker-scan.sh @@ -148,7 +148,7 @@ assert_scan() { local root="$3" local rc=0 - run_marker_scan "$root" >/tmp/scan-out 2>&1 || rc=$? + run_marker_scan "$root" >"$TMP_ROOT/scan-out" 2>&1 || rc=$? local got case "$rc" in @@ -163,7 +163,7 @@ assert_scan() { else echo "FAIL $name (expected=$expected, got=$got)" echo " scan output:" - sed 's/^/ /' /tmp/scan-out + sed 's/^/ /' "$TMP_ROOT/scan-out" FAIL_COUNT=$((FAIL_COUNT + 1)) fi } From 6739dc3817b0d1630e973d3ede1f49c07533048e Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 16 Jul 2026 19:47:37 +0000 Subject: [PATCH 10/11] fix(ci): sync marker-scan self-test with docs/api + docs/guide paths The live CI step (build.yml) scans docs/api and docs/guide, but the self-test's run_marker_scan() mirror loop still omitted them, so the anti-drift adapter had itself drifted and would not catch a docs-path regression. Add both paths to the loop and a docs/guide/*.md fixture case that exercises the surface. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/test-marker-scan.sh | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/test-marker-scan.sh b/scripts/test-marker-scan.sh index fc3dcb70ce..e497beed29 100755 --- a/scripts/test-marker-scan.sh +++ b/scripts/test-marker-scan.sh @@ -36,7 +36,7 @@ run_marker_scan() { local paths=() local dir # Mirror the build.yml invocation exactly so the self-test and CI cannot drift. - for dir in dist commonjs es typings languages; do + for dir in dist commonjs es typings languages docs/api docs/guide; do if [ -d "$root/$dir" ]; then paths+=("$root/$dir") fi @@ -138,6 +138,20 @@ export declare const foo: number; EOF } +# Variant: marker leaked into a published docs page. build.yml also scans +# docs/api (generated) and docs/guide (hand-written) so citation markers never +# reach the online docs; this fixture isolates that DIRECTORY-coverage surface. +make_marker_in_docs_guide() { + local root="$1" + make_clean_fixture "$root" + mkdir -p "$root/docs/guide" + cat >"$root/docs/guide/some-page.md" <<'EOF' +# Some guide page + +Content with an internal [vrf_3] citation that must never ship to the docs site. +EOF +} + # ---- Assertion harness ------------------------------------------------------ PASS_COUNT=0 FAIL_COUNT=0 @@ -180,6 +194,7 @@ f="$TMP_ROOT/case-commonjs"; make_marker_in_commonjs "$f"; assert_scan f="$TMP_ROOT/case-es"; make_marker_in_es "$f"; assert_scan "marker in es/*.mjs" "dirty" "$f" f="$TMP_ROOT/case-commonjs-current"; make_marker_in_commonjs_current "$f"; assert_scan "current [vrf_n] marker in commonjs/*.js" "dirty" "$f" f="$TMP_ROOT/case-typings"; make_marker_in_typings "$f"; assert_scan "marker in typings/*.d.ts" "dirty" "$f" +f="$TMP_ROOT/case-docs-guide"; make_marker_in_docs_guide "$f"; assert_scan "marker in docs/guide/*.md" "dirty" "$f" echo "" echo "=== summary: $PASS_COUNT passed, $FAIL_COUNT failed ===" From cd30d48af4b3877a92c3556b112eb121a37fb5fc Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Sun, 25 Oct 2026 01:55:55 +0000 Subject: [PATCH 11/11] ci: run the citation marker check once, after the docs build, and fail on missing paths The monorepo move put the engine's build output under hyperformula/, so the check in build.yml scanned paths that no longer exist and passed. docs/api is generated only by docs:build, so build.yml never scanned it. - script/check-citation-markers.mjs replaces scripts/marker-scan.sh and its self-test; `npm run check:citation-markers` is its only caller - one step in build-docs.yml after `npm run docs:build`, which builds both the engine and the docs, instead of a step in each of the 18 build jobs - a missing or empty path fails the check instead of being skipped - the pattern is checked against marker and non-marker samples before each scan - dev-docs/STRUCTURE.md, dev-docs/BUILD.md and script/AGENTS.md list the script Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build-docs.yml | 3 + .github/workflows/build.yml | 17 --- dev-docs/BUILD.md | 2 + dev-docs/STRUCTURE.md | 2 +- package.json | 1 + script/AGENTS.md | 3 +- script/check-citation-markers.mjs | 109 ++++++++++++++++ scripts/marker-scan.sh | 76 ----------- scripts/test-marker-scan.sh | 204 ------------------------------ 9 files changed, 118 insertions(+), 299 deletions(-) create mode 100644 script/check-citation-markers.mjs delete mode 100755 scripts/marker-scan.sh delete mode 100755 scripts/test-marker-scan.sh diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 6cf0ff8ccc..29c168ea8b 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -41,3 +41,6 @@ jobs: - name: Build docs run: npm run docs:build + + - name: Check the build output and the docs for citation markers + run: npm run check:citation-markers diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b60be0afd0..13313f8a73 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,20 +37,3 @@ jobs: - name: Build run: npm run bundle-all - - - name: Verify no audit-harness markers leaked into build output or docs - # Delegates to scripts/marker-scan.sh so the live CI scan and the - # self-test below exercise the same code path and cannot drift. - # docs/guide (hand-written) is scanned from the checkout; docs/api is - # generated by a separate docs build, so it is only scanned when present - # (marker-scan.sh skips any path that does not exist). - shell: bash - run: bash scripts/marker-scan.sh dist commonjs es typings languages docs/api docs/guide - - - name: Self-test marker-scan logic against synthetic fixtures - # Run once per OS (the script is platform-agnostic and only takes <1s). - # Validates that scripts/marker-scan.sh catches markers in dist/*.js, - # dist/*.js.map (sourcesContent), commonjs/*.js, and es/*.mjs surfaces. - if: matrix.node-version == 22 && matrix.install-command == 'ci' - shell: bash - run: bash scripts/test-marker-scan.sh diff --git a/dev-docs/BUILD.md b/dev-docs/BUILD.md index c1273b71af..130fce1441 100644 --- a/dev-docs/BUILD.md +++ b/dev-docs/BUILD.md @@ -26,6 +26,8 @@ The generator lives in `docs/script/` rather than beside the engine build script Both are git-ignored, and both are regenerated as the first step of `docs:dev` and `docs:build`. `docs:generate-function-docs` is a gate, not a formatter: it fails the build on a missing catalogue entry or a `'Custom'` category. See [`FUNCTION-CATALOGUE.md`](../hyperformula/dev-docs/FUNCTION-CATALOGUE.md). +After `docs:build`, the `build-docs` workflow runs `npm run check:citation-markers`. It fails when a citation marker from a draft document (the patterns are listed in the script) appears in the engine's build output or in `docs/api/` and `docs/guide/`, and when one of those directories is missing or empty. + ## Deploying the portal The portal is served by a Cloudflare Worker, and **deployment is driven by Workers Builds on push, not from a developer's machine**: `master` deploys to production, every other branch uploads a preview version. The commands below exist for debugging that pipeline. The triggers, the Worker name, and the dashboard build settings are in [`docs/README.md`](../docs/README.md#deployment). diff --git a/dev-docs/STRUCTURE.md b/dev-docs/STRUCTURE.md index 32e886004b..1393625c0e 100644 --- a/dev-docs/STRUCTURE.md +++ b/dev-docs/STRUCTURE.md @@ -114,7 +114,7 @@ Each script lives with whatever invokes it, and every one has exactly one caller | Directory | Holds | |---|---| -| `script/` | `release/` and `check-licenses.mjs` — both span the whole repository | +| `script/` | `release/`, `check-licenses.mjs` and `check-citation-markers.mjs` — all three span the whole repository | | `hyperformula/script/` | `check-file.js`, `check-publish-package.js`, `if-ne-env.js` — called by the engine's build; `check-license-key-parser-drift.js` — called by `vendored-parser.yml`; `postinstall-skills-message.js` — shipped in the package and run by npm when a user installs it, silent inside this repository | | `docs/script/` | the built-in-functions generator and `prepare-cf-assets.js` — called by the portal | | `hyperformula-ui/scripts/` | `generateCssText.mjs` and `generateApiDocs.mjs` — called by the UI core's `build:styles` and `docs` | diff --git a/package.json b/package.json index c9dca68250..ff3da5566d 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "docs:preview:cf": "npm --prefix docs run preview:cf --", "audit": "npm audit --omit=dev", "check:licenses": "node script/check-licenses.mjs", + "check:citation-markers": "node script/check-citation-markers.mjs", "release": "bash script/release/release.sh" }, "devDependencies": { diff --git a/script/AGENTS.md b/script/AGENTS.md index 82645d6634..1b048ee262 100644 --- a/script/AGENTS.md +++ b/script/AGENTS.md @@ -6,8 +6,9 @@ Repository-level scripts. Each package keeps its own in `hyperformula/script/` a |---|---|---| | `check-licenses.mjs` | `npm run check:licenses` | Asserts every production dependency is permissively licensed | | `release/` | `npm run release` | The release procedure, across the packages and the sibling repositories | +| `check-citation-markers.mjs` | `npm run check:citation-markers`, which `build-docs.yml` runs after `docs:build` | Fails when a citation marker from a draft document appears in the engine's build output or the docs | - What the build produces, and where the licence gate fits: [`dev-docs/BUILD.md`](../dev-docs/BUILD.md) - Everything else: [`dev-docs/README.md`](../dev-docs/README.md) -No workflow runs these; they are run by hand from the repository root. `release/release.sh` needs an operator at a terminal on purpose — after `--real-run`, its typed confirmation is the last guard before `npm publish` — so never make it run without one. Nothing here is covered by tests; a change is verified by running it. +Only `check-citation-markers.mjs` runs in CI; the others are run by hand from the repository root. `release/release.sh` needs an operator at a terminal on purpose — after `--real-run`, its typed confirmation is the last guard before `npm publish` — so never make it run without one. Nothing here is covered by tests; a change is verified by running it. diff --git a/script/check-citation-markers.mjs b/script/check-citation-markers.mjs new file mode 100644 index 0000000000..bbcf164620 --- /dev/null +++ b/script/check-citation-markers.mjs @@ -0,0 +1,109 @@ +#!/usr/bin/env node +/** + * Fails when a citation marker from a draft document appears in the engine's + * build output or in the documentation. + * + * The markers are `[V]`, `[_]` with the prefix `vrf`, `dec`, + * `con`, `que`, `wrg` or `crf`, and the footer heading `§AuditSources`. A source + * comment survives into `commonjs/`, `es/`, the development bundle in `dist/` + * and the `sourcesContent` of the source maps in `dist/`, so every file of the + * output is scanned, not only the minified bundles. + * + * Run it after `npm run docs:build`, which builds both the engine and the docs. + * A path that is missing or holds no files fails the check instead of being + * skipped, so a moved output directory cannot turn it into a scan of nothing. + */ +import { readdirSync, readFileSync, statSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') + +/** The engine's build output and the documentation, relative to the repository root. */ +const SCANNED = [ + 'hyperformula/commonjs', + 'hyperformula/dist', + 'hyperformula/es', + 'hyperformula/languages', + 'hyperformula/typings', + 'docs/api', + 'docs/guide', +] + +const MARKER = /\[(?:V\d+|(?:vrf|dec|con|que|wrg|crf)_\d+)\]|§\s*AuditSources/g + +/** Checked before every scan, so a broken pattern fails instead of finding nothing. */ +const MUST_MATCH = ['[V3]', '[vrf_1]', '[dec_2]', '[con_3]', '[que_4]', '[wrg_5]', '[crf_6]', '§AuditSources', '§ AuditSources'] +const MUST_NOT_MATCH = ['[V]', '[v1]', '[vrf_]', '[vrf_1a]', '[abc_1]', 'AuditSources', 'cells[V1'] + +/** + * Prints an error and exits with code 2, which means the scan could not run. + * + * @param {string} message What went wrong. + */ +function abort(message) { + console.error(`ERROR: ${message}`) + process.exit(2) +} + +/** + * Lists every file under a directory. + * + * @param {string} dir Absolute path of the directory. + * @returns {string[]} Absolute paths of the files. + */ +function listFiles(dir) { + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const full = path.join(dir, entry.name) + if (entry.isDirectory()) { + return listFiles(full) + } + return entry.isFile() ? [full] : [] + }) +} + +for (const sample of MUST_MATCH) { + if (!sample.match(MARKER)) { + abort(`the marker pattern does not match ${sample}`) + } +} +for (const sample of MUST_NOT_MATCH) { + if (sample.match(MARKER)) { + abort(`the marker pattern matches ${sample}, which is not a marker`) + } +} + +const hits = [] +let fileCount = 0 +for (const relative of SCANNED) { + const dir = path.join(REPO_ROOT, relative) + let isDirectory = false + try { + isDirectory = statSync(dir).isDirectory() + } catch { + abort(`${relative} does not exist. Build the engine and the docs first: npm run docs:build`) + } + if (!isDirectory) { + abort(`${relative} is not a directory`) + } + const files = listFiles(dir) + if (files.length === 0) { + abort(`${relative} contains no files`) + } + console.log(`${relative}: ${files.length} files`) + fileCount += files.length + for (const file of files) { + const text = readFileSync(file, 'utf8') + for (const match of text.matchAll(MARKER)) { + const line = text.slice(0, match.index).split('\n').length + hits.push(`${path.relative(REPO_ROOT, file)}:${line}: ${match[0]}`) + } + } +} + +if (hits.length > 0) { + console.error(hits.join('\n')) + console.error(`\nERROR: ${hits.length} citation markers found. Remove them from the source comments or strings they come from.`) + process.exit(1) +} +console.log(`No citation markers in ${fileCount} files.`) diff --git a/scripts/marker-scan.sh b/scripts/marker-scan.sh deleted file mode 100755 index cd304ce6da..0000000000 --- a/scripts/marker-scan.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env bash -# Shared audit-harness marker scan used by both the CI verify step in -# .github/workflows/build.yml and the self-test in scripts/test-marker-scan.sh. -# -# Centralizing the scan in one script keeps the live CI logic and its self-test -# from drifting independently — a workflow-only edit would otherwise miss the -# self-test fixture coverage. -# -# Why this scan exists: -# Greps build output for audit-harness citation markers and the `§AuditSources` -# footer — internal spec-drafting tokens that must never ship in compiled -# output. Markers are the current lowercase prefixed form `[vrf_1]`/`[dec_3]`/ -# `[con_2]`/`[que_5]`/`[wrg_7]`/`[crf_4]` (parser `^\[[a-z][a-z0-9_]*\]$`) plus -# the legacy `[V]` form. Source comments leak through THREE surfaces and -# ALL are covered here: -# 1) commonjs/*.js and es/*.mjs (babel preserves comments) -# 2) dist/hyperformula.js and dist/hyperformula.full.js (webpack preserves -# comments in the development build) -# 3) dist/*.js.map (`sourcesContent` embeds full original source, so markers -# survive in source-maps even if stripped from .js) -# `grep -rn` over dist/ catches .map files because they are plain JSON. -# -# Usage: -# bash scripts/marker-scan.sh [ ...] -# -# Exit codes: -# 0 — scan ran cleanly, no markers found (or no requested dirs exist) -# 1 — markers found in at least one scanned file -# 2+ — grep failed with an I/O / scan error; the caller must surface this - -set -u - -if [ "$#" -lt 1 ]; then - echo "usage: $0 [ ...]" >&2 - exit 2 -fi - -paths=() -for dir in "$@"; do - if [ -d "$dir" ]; then - paths+=("$dir") - fi -done - -if [ ${#paths[@]} -eq 0 ]; then - echo "No build output directories found ($*); skipping marker scan." - exit 0 -fi - -echo "Scanning ${paths[*]} for audit-harness markers..." - -# grep exit codes: 0=match, 1=no-match, 2+=scan/IO error. A bare `if grep` -# collapses 1 and 2 into the same branch, silently green-lighting on read -# errors. Branch on rc explicitly. -set +e -grep -rnE '\[(V[0-9]+|(vrf|dec|con|que|wrg|crf)_[0-9]+)\]|§[[:space:]]*AuditSources' "${paths[@]}" -rc=$? -set -e - -case "$rc" in - 0) - echo "" - echo "ERROR: audit-harness markers ([V]/[vrf_n] or §AuditSources) found in build output." - echo "These markers are an internal spec-drafting convention and must never" - echo "ship in compiled JS. Strip them from the source comments/strings above." - exit 1 - ;; - 1) - echo "OK: no audit-harness markers found in build output." - exit 0 - ;; - *) - echo "ERROR: grep exited with rc=$rc while scanning ${paths[*]} — aborting scan." >&2 - exit "$rc" - ;; -esac diff --git a/scripts/test-marker-scan.sh b/scripts/test-marker-scan.sh deleted file mode 100755 index e497beed29..0000000000 --- a/scripts/test-marker-scan.sh +++ /dev/null @@ -1,204 +0,0 @@ -#!/usr/bin/env bash -# Self-test for the audit-harness marker scan used in .github/workflows/build.yml. -# -# Why this exists: -# The CI step in build.yml invokes -# `scripts/marker-scan.sh dist commonjs es typings languages` to grep the build -# output for `[V]`/`[vrf_n]` citation markers and the `§AuditSources` footer — internal -# spec-drafting tokens that must never ship in compiled JS. Empirically -# (probed 2026-05-25 by planting `// [V99] test marker` in src/index.ts and -# running `npm run bundle-all`), markers leak through THREE distinct surfaces: -# 1) babel-transpiled commonjs/*.js and es/*.mjs preserve source comments -# 2) webpack-bundled dist/hyperformula.js and dist/hyperformula.full.js -# preserve source comments (development build, no comment-stripping) -# 3) dist/*.js.map source-maps embed full original source in -# `sourcesContent`, so comments survive into the map even when stripped -# from the .js itself (not applicable here, but defends future configs) -# This script exercises the SAME scripts/marker-scan.sh that CI runs, against -# synthetic fixtures covering all three surfaces — so the workflow step and -# the self-test cannot drift independently. -# -# Exit code: 0 on all assertions pass, non-zero on any failure. - -set -uo pipefail - -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -readonly MARKER_SCAN="$SCRIPT_DIR/marker-scan.sh" -readonly TMP_ROOT="$(mktemp -d -t hf-marker-scan-XXXXXX)" -trap 'rm -rf "$TMP_ROOT"' EXIT - -# ---- Adapter: drives the shared scan against a synthetic fixture root ------ -# Invokes scripts/marker-scan.sh — the same script CI runs — passing the -# fixture's dist/commonjs/es subdirectories. This is the only place where the -# self-test couples to the scan; the scan logic itself lives in marker-scan.sh. -run_marker_scan() { - local root="$1" - local paths=() - local dir - # Mirror the build.yml invocation exactly so the self-test and CI cannot drift. - for dir in dist commonjs es typings languages docs/api docs/guide; do - if [ -d "$root/$dir" ]; then - paths+=("$root/$dir") - fi - done - if [ ${#paths[@]} -eq 0 ]; then - # Mirror the script's "no dirs" branch so the assertion harness treats this - # as a clean (rc=0) outcome. - bash "$MARKER_SCAN" "$root/__missing__" - return $? - fi - local rc=0 - bash "$MARKER_SCAN" "${paths[@]}" || rc=$? - case "$rc" in - 0) return 0 ;; # no markers -> CI would pass - 1) return 1 ;; # markers found -> CI would fail - *) return "$rc" ;; - esac -} - -# ---- Fixture builders ------------------------------------------------------- -make_clean_fixture() { - local root="$1" - mkdir -p "$root/dist" "$root/commonjs" "$root/es" - cat >"$root/dist/hyperformula.js" <<'EOF' -// HyperFormula bundle (synthetic, clean) -const HyperFormula = function() { return 42; }; -module.exports = HyperFormula; -EOF - cat >"$root/dist/hyperformula.js.map" <<'EOF' -{"version":3,"sources":["webpack:///./src/index.ts"],"sourcesContent":["const HyperFormula = function() { return 42; };\nmodule.exports = HyperFormula;\n"],"mappings":"AAAA"} -EOF - cat >"$root/commonjs/index.js" <<'EOF' -"use strict"; -exports.foo = 1; -EOF - cat >"$root/es/index.mjs" <<'EOF' -export const foo = 1; -EOF -} - -# Variant: marker in dist .js file (e.g. preserved source comment). -make_marker_in_dist_js() { - local root="$1" - make_clean_fixture "$root" - cat >>"$root/dist/hyperformula.js" <<'EOF' -// [V12] internal citation marker — must not ship -EOF -} - -# Variant: marker in source-map sourcesContent only (stripped from .js). -make_marker_in_sourcemap() { - local root="$1" - make_clean_fixture "$root" - cat >"$root/dist/hyperformula.js.map" <<'EOF' -{"version":3,"sources":["webpack:///./src/index.ts"],"sourcesContent":["// [V7] citation that survived into sourcesContent\nconst HyperFormula = function() { return 42; };\n"],"mappings":"AAAA"} -EOF -} - -# Variant: §AuditSources footer leaked into commonjs output. -make_marker_in_commonjs() { - local root="$1" - make_clean_fixture "$root" - cat >>"$root/commonjs/index.js" <<'EOF' -// §AuditSources: internal/spec.md -EOF -} - -# Variant: marker in es/*.mjs. -make_marker_in_es() { - local root="$1" - make_clean_fixture "$root" - cat >>"$root/es/index.mjs" <<'EOF' -// [V42] another internal token -EOF -} - -# Variant: CURRENT lowercase prefixed marker (e.g. [vrf_3]) in commonjs output. -# The pre-2026-05-21 [V] form is legacy; real specs emit [vrf_/dec_/con_/ -# que_/wrg_/crf_]_. Isolates the scan-PATTERN coverage. -make_marker_in_commonjs_current() { - local root="$1" - make_clean_fixture "$root" - cat >>"$root/commonjs/index.js" <<'EOF' -// [vrf_3] current-grammar citation marker — must not ship -EOF -} - -# Variant: marker leaked into typings/*.d.ts. `bundle:typings` (tsc -d) and -# `bundle:languages` are build outputs that preserve source comments, so they -# are leak surfaces too. Uses a legacy [V] marker to isolate the DIRECTORY- -# coverage defect from the pattern defect. -make_marker_in_typings() { - local root="$1" - make_clean_fixture "$root" - mkdir -p "$root/typings" - cat >"$root/typings/index.d.ts" <<'EOF' -/** Public API surface. [V12] citation leaked into declarations. */ -export declare const foo: number; -EOF -} - -# Variant: marker leaked into a published docs page. build.yml also scans -# docs/api (generated) and docs/guide (hand-written) so citation markers never -# reach the online docs; this fixture isolates that DIRECTORY-coverage surface. -make_marker_in_docs_guide() { - local root="$1" - make_clean_fixture "$root" - mkdir -p "$root/docs/guide" - cat >"$root/docs/guide/some-page.md" <<'EOF' -# Some guide page - -Content with an internal [vrf_3] citation that must never ship to the docs site. -EOF -} - -# ---- Assertion harness ------------------------------------------------------ -PASS_COUNT=0 -FAIL_COUNT=0 - -assert_scan() { - local name="$1" - local expected="$2" # "clean" -> expect rc 0 ; "dirty" -> expect rc 1 - local root="$3" - - local rc=0 - run_marker_scan "$root" >"$TMP_ROOT/scan-out" 2>&1 || rc=$? - - local got - case "$rc" in - 0) got="clean" ;; - 1) got="dirty" ;; - *) got="error($rc)" ;; - esac - - if [ "$got" = "$expected" ]; then - echo "PASS $name (expected=$expected, got=$got)" - PASS_COUNT=$((PASS_COUNT + 1)) - else - echo "FAIL $name (expected=$expected, got=$got)" - echo " scan output:" - sed 's/^/ /' "$TMP_ROOT/scan-out" - FAIL_COUNT=$((FAIL_COUNT + 1)) - fi -} - -# ---- Test cases ------------------------------------------------------------- -echo "=== audit-marker scan self-test ===" -echo "Fixture root: $TMP_ROOT" -echo "" - -f="$TMP_ROOT/case-clean"; make_clean_fixture "$f"; assert_scan "clean build (no markers)" "clean" "$f" -f="$TMP_ROOT/case-dist-js"; make_marker_in_dist_js "$f"; assert_scan "marker in dist/*.js comment" "dirty" "$f" -f="$TMP_ROOT/case-dist-map"; make_marker_in_sourcemap "$f"; assert_scan "marker in dist/*.js.map (sourcesContent)" "dirty" "$f" -f="$TMP_ROOT/case-commonjs"; make_marker_in_commonjs "$f"; assert_scan "§AuditSources in commonjs/*.js" "dirty" "$f" -f="$TMP_ROOT/case-es"; make_marker_in_es "$f"; assert_scan "marker in es/*.mjs" "dirty" "$f" -f="$TMP_ROOT/case-commonjs-current"; make_marker_in_commonjs_current "$f"; assert_scan "current [vrf_n] marker in commonjs/*.js" "dirty" "$f" -f="$TMP_ROOT/case-typings"; make_marker_in_typings "$f"; assert_scan "marker in typings/*.d.ts" "dirty" "$f" -f="$TMP_ROOT/case-docs-guide"; make_marker_in_docs_guide "$f"; assert_scan "marker in docs/guide/*.md" "dirty" "$f" - -echo "" -echo "=== summary: $PASS_COUNT passed, $FAIL_COUNT failed ===" - -if [ "$FAIL_COUNT" -gt 0 ]; then - exit 1 -fi