diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 1c6c08256..3bbaf4af9 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -282,7 +282,7 @@ Retired 2026-08-31: the bootstrap-shim row (`affinescript-deno-test/**`, `affine The hyperpolymath "npm banned" policy (2026-05-25) has the following approved exemptions on the hypatia rule `cicd_rules/nodejs_detected` (matches `package-lock.json`). -Migration substantially complete 2026-05-31 under umbrella `hyperpolymath/standards#253` (172 manifests at campaign start; all seven STEP issues #261/#262/#265/#268/#270/#273/#275 closed; ~22 physical-migration PRs landed plus three named-bucket audits closed `SUBSTANTIALLY DONE`; per-repo follow-up trackers cover the residual longtail). See `project_estate_npm_to_deno_2026_05_28.md`. Per-repo recipe: `docs/migrations/npm-to-deno-template/MIGRATION.md`. +Migration substantially complete 2026-05-31 under umbrella `hyperpolymath/standards#253` (172 manifests at campaign start; all seven STEP issues #261/#262/#265/#268/#270/#273/#275 closed; ~22 physical-migration PRs landed plus three named-bucket audits closed `SUBSTANTIALLY DONE`; per-repo follow-up trackers cover the residual longtail). See `project_estate_npm_to_deno_2026_05_28.md`. | Path / Pattern | Class | Rationale | Unblock condition | |---|---|---|---| diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index a7f9781e8..fb983aa9c 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -26,10 +26,6 @@ workflows: '.github/workflows/codeql.yml': [] '.github/workflows/debt-measure.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - '.github/workflows/deno-ci-reusable.yml': - - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' - '.github/workflows/deno-ci.yml': [] '.github/workflows/doc-format.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/dyadt-verify.yml': @@ -46,7 +42,6 @@ workflows: '.github/workflows/governance-reusable.yml': - 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9' - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' - 'editorconfig-checker/action-editorconfig-checker@840e866d93b8e032123c23bac69dece044d4d84c' - 'erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124' '.github/workflows/governance.yml': [] @@ -105,7 +100,6 @@ workflows: - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' '.github/workflows/self-test.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' '.github/workflows/signed-push-smoke.yml': - 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' - 'actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1' @@ -172,11 +166,6 @@ dependencies: repo_id: 772313726 uses: - 'actions/setup-python@v2' - 'denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed': - ref: 'v2.0.5' - commit: 'sha1-22d081ff2d3a40755e97629de92e3bcbfa7cf2ed' - owner_id: 42048915 - repo_id: 356423100 'dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772': ref: '6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' commit: 'sha1-6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772' diff --git a/.github/workflows/governance-reusable.yml b/.github/workflows/governance-reusable.yml index 56ed958bd..9f2184ed3 100644 --- a/.github/workflows/governance-reusable.yml +++ b/.github/workflows/governance-reusable.yml @@ -354,11 +354,6 @@ jobs: # drift is just whatever's on standards/main between the reusable # version and the script version — acceptable since scripts here # are read-only governance checks. - - name: Set up Deno - uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2.0.5 - with: - deno-version: v2.x - - name: Check out standards repo for shared scripts uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -373,18 +368,24 @@ jobs: - name: Check for TypeScript # Read-only execution; never writes outside the runner workspace. - # `--no-lock` so an empty / stale / missing `deno.lock` doesn't fail - # `deno run` before the file-walker even starts — the script does not - # import anything, so the lockfile is irrelevant to its execution. - # See standards#294. - # - # Runs the AffineScript-compiled `.deno.js` (source of truth: - # `scripts/check-ts-allowlist.affine`). The .ts archetype is kept - # alongside for the regression suite (`scripts/tests/check-ts- - # allowlist-test.sh`) and for parallel-validation during the - # TS→AffineScript migration (standards#239 / #241). Retirement of - # the .ts is a separate follow-up after the dual-target window. - run: deno run --allow-read --no-lock .standards-checkout/scripts/check-ts-allowlist.deno.js + # Pure bash + awk, so no JS runtime is installed on the runner. + # Source of truth: `scripts/check-ts-allowlist.sh` in standards. + # The local fallback is for standards' OWN PRs: the checkout above + # pins standards@main, so a script added in a PR is not there yet. + # It is gated on the caller being standards, so no consumer repo can + # shadow this required gate with a permissive repo-local copy. + run: | + SCRIPT=".standards-checkout/scripts/check-ts-allowlist.sh" + if [ ! -f "$SCRIPT" ] && [ "$GITHUB_REPOSITORY" = "hyperpolymath/standards" ] \ + && [ -f scripts/check-ts-allowlist.sh ]; then + SCRIPT="scripts/check-ts-allowlist.sh" + echo "Using this repository's own copy (standards self-check)." + fi + if [ ! -f "$SCRIPT" ]; then + echo "::error::check-ts-allowlist gate not found in standards@main or locally" + exit 1 + fi + bash "$SCRIPT" - name: Check language-policy invariants run: | @@ -399,33 +400,6 @@ jobs: fi bash "$SCRIPT" - - name: check-ts-allowlist source/compile drift (informational) - # Non-blocking — informational until the AffineScript compiler - # output is hash-pinned per compiler version. The compiler header - # currently stamps "Generated by AffineScript compiler" which is - # a moving target as the codegen evolves, so spurious diff = - # "compiler bumped" vs real diff = "someone edited .affine - # without recompiling". Promotion to blocking is gated on a - # compiler-version pin landing (see standards#312). - continue-on-error: true - run: | - if ! command -v affinescript >/dev/null 2>&1; then - echo "::notice::affinescript compiler unavailable on runner — skipping drift check" - exit 0 - fi - tmp="$(mktemp /tmp/check-ts-allowlist-drift.XXXXXX.deno.js)" - if ! affinescript compile --deno-esm -o "$tmp" .standards-checkout/scripts/check-ts-allowlist.affine; then - echo "::warning::affinescript compile failed — drift check skipped" - rm -f "$tmp" - exit 0 - fi - if diff -u .standards-checkout/scripts/check-ts-allowlist.deno.js "$tmp"; then - echo "✅ check-ts-allowlist .affine source and .deno.js compiled output are in sync" - else - echo "::warning::check-ts-allowlist.deno.js drifted from check-ts-allowlist.affine — re-run \`just check-ts-allowlist-drift\` locally and recommit the .deno.js" - fi - rm -f "$tmp" - # Shared escape hatch for the banned-language-file checks below. # Honours three exemption mechanisms (see # standards/docs/EXEMPTION-MECHANISMS.adoc): diff --git a/.github/workflows/self-test.yml b/.github/workflows/self-test.yml index af04436ec..8f7172391 100644 --- a/.github/workflows/self-test.yml +++ b/.github/workflows/self-test.yml @@ -35,15 +35,6 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - # check-ts-allowlist-test.sh executes the generated Deno target, and the - # scorecard grounding suite runs pass-checks that use the same toolchain. - # Without installing Deno, the suite reported 18 assertion failures as - # one red test file and also made the scorecard fixtures fail. - - name: Install Deno test runtime - uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2.0.5 - with: - deno-version: v2.x - # PyYAML is required by the secret-scanner canary. The scorecard # grounding tests execute the same checks as registry-verify, including # checks that require ripgrep and xmllint. diff --git a/.machine_readable/Debtfile.a2ml b/.machine_readable/Debtfile.a2ml index dcea99709..c3bd46da7 100644 --- a/.machine_readable/Debtfile.a2ml +++ b/.machine_readable/Debtfile.a2ml @@ -39,8 +39,8 @@ forgotten. ### gate-scripts-without-tests - description: Scripts under scripts/ with no matching scripts/tests/-test.sh — a gate with no test has never been shown able to fail - probe: n=0; for f in $(git ls-files 'scripts/*.sh'); do b=$(basename "$f" .sh); case "$b" in *-test) continue;; esac; if [ ! -f "scripts/tests/${b}-test.sh" ] && [ ! -f "scripts/tests/${b#check-}-test.sh" ] && [ ! -f "scripts/tests/${b#run-}-test.sh" ]; then n=$((n+1)); fi; done; echo "$n" -- count: 31 -- ceiling: 31 +- count: 30 +- ceiling: 30 - severity: high - policy: remediable - tri: eliminate @@ -88,20 +88,20 @@ forgotten. - accepted-until: 2027-01-01 ### deno-residue -- description: Deno residue in this repository after the Bun ruling. `governance-reusable.yml` still runs `denoland/setup-deno`, which INSTALLS DENO ON EVERY ESTATE REPO ON EVERY RUN; `deno-ci{,-reusable}.yml` still ship the failing `deno / Deno CI`; `scripts/check-ts-allowlist.deno.js` is a worked Deno example in scripts/; and `docs/migrations/npm-to-deno-template/` is a live recipe pointing repos AT the retired runtime. Owner ruled Deno REMOVED and Bun permanent (said three times, reaffirmed 2026-08-07). Must reach 0. Excludes */bindings/deno/, which is interop for OTHER people's Deno code and a separate question. +- description: Deno residue in this repository after the Bun ruling. The required JS/TS gate is now `scripts/check-ts-allowlist.sh` (bash + awk); `scripts/check-ts-allowlist.deno.js` is RETAINED DELIBERATELY as a compatibility shim, not as residue. governance-reusable fetches `scripts/` at floating `ref: main` while consumers pin the workflow YAML, so deleting the shim breaks every consumer whose pinned YAML still invokes it — MEASURED 2026-09-04 at 269 repos. Owner ruled Deno REMOVED and Bun permanent (said three times, reaffirmed 2026-08-07). Must reach 0, but only via the three-phase retirement: shim (done, PR #730) -> repin consumers (task #59) -> delete. The single remaining probe hit is a COMMENT inside the shim, not a live invocation. Excludes */bindings/deno/, which is interop for OTHER people's Deno code and a separate question. - probe: git grep -lE "denoland/setup-deno|deno run|deno test|deno fmt|deno lint" -- ".github/workflows/*.yml" "scripts/*" | wc -l -- count: 4 -- ceiling: 4 +- count: 1 +- ceiling: 1 - severity: medium - policy: remediable - tri: substitute - accepted-until: 2026-11-01 ### deno-artefacts -- description: Files that exist only to serve Deno — the deno-ci workflow pair, the compiled check-ts-allowlist.deno.js, and the npm-to-deno migration template. Deleting these is the completion of the Bun migration, not a precondition of it. Excludes */bindings/deno/. +- description: Files that exist only to serve Deno. The npm-to-deno migration template was deleted in PR #730 (a live recipe pointing repos AT the retired runtime; its two inbound links were already dangling, naming MIGRATION.md after the .adoc rename). The one remaining artefact is the `check-ts-allowlist.deno.js` compatibility shim, which leaves when consumers have repinned — see deno-residue and task #59. Excludes */bindings/deno/. - probe: git ls-files ".github/workflows/deno*" "scripts/*deno*" "docs/migrations/npm-to-deno-template/*" | wc -l -- count: 3 -- ceiling: 3 +- count: 1 +- ceiling: 1 - severity: medium - policy: remediable - tri: substitute diff --git a/Justfile b/Justfile index 2360c4a27..0400b9f88 100644 --- a/Justfile +++ b/Justfile @@ -247,19 +247,6 @@ help-me: @echo "Include the output of 'just doctor' in your report." -# Verify scripts/check-ts-allowlist.deno.js matches what compiling -# scripts/check-ts-allowlist.affine produces. Run after editing the -# .affine source. Exit 0 = in sync; non-zero with diff = drifted. -# See standards#312. -check-ts-allowlist-drift: - @command -v affinescript >/dev/null 2>&1 || { echo "affinescript compiler not on PATH — skipping drift check"; exit 0; } - @tmp="$$(mktemp /tmp/check-ts-allowlist-drift.XXXXXX.deno.js)"; \ - affinescript compile --deno-esm -o "$$tmp" scripts/check-ts-allowlist.affine; \ - diff -u scripts/check-ts-allowlist.deno.js "$$tmp"; \ - rc=$$?; \ - rm -f "$$tmp"; \ - exit $$rc - # Print the current CRG grade (reads from READINESS.md '**Current Grade:** X' line) crg-grade: @grade=$$(grep -oP '(?<=\*\*Current Grade:\*\* )[A-FX]' READINESS.md 2>/dev/null | head -1); \ diff --git a/docs/EXEMPTION-MECHANISMS.adoc b/docs/EXEMPTION-MECHANISMS.adoc index 524fe8769..ef7044460 100644 --- a/docs/EXEMPTION-MECHANISMS.adoc +++ b/docs/EXEMPTION-MECHANISMS.adoc @@ -169,10 +169,9 @@ across the estate. Three sub-layers: === 4a: Built-in path / filename allowlist -Hard-coded in `scripts/check-ts-allowlist.affine` (source of truth; -compiled to `scripts/check-ts-allowlist.deno.js` which the workflow -invokes). Covers paths that are *always* exempt regardless of per-repo -configuration: +Hard-coded in `scripts/check-ts-allowlist.sh`, which the governance +workflow invokes directly. Covers paths that are *always* exempt +regardless of per-repo configuration: * Directory segments: `bindings`, `tests`, `test`, `scripts`, `mcp-adapter`, `cli`, `vendor`, `examples`, `ffi`, `node_modules`, @@ -266,21 +265,23 @@ sufficient. Most repos will pick one or the other. This document seeds the doctrine. * AffineScript port (standards#283 seed, #310 compile/runtime fixes, #311 workflow swap): `.ts` → `.affine` self-referential port under - the TS→AffineScript campaign (#239 / #241 STEP 2). The `.ts` - archetype is kept for the regression suite and parallel-validation; - the workflow now runs the compiled `.deno.js`. Retirement of the - `.ts` is a follow-up after the dual-target window. + the TS→AffineScript campaign (#239 / #241 STEP 2). The workflow ran + the compiled `.deno.js`. +* Deno retirement (2026-09-04): the `.affine` source, its compiled + `.deno.js`, and the `deno run` workflow step were all deleted and + replaced by `scripts/check-ts-allowlist.sh` — pure bash + awk, so no + JS runtime is installed on any estate runner. The 18-case corpus was + run against both implementations first and gave identical verdicts on + every case. == Cross-references * `docs/HYPATIA-BASELINE-FORMAT.adoc` — the baseline file format. * `.machine_readable/hypatia-baseline.schema.json` — machine schema. -* `scripts/check-ts-allowlist.affine` — the AffineScript source of - truth for the Layer 4 detector (since standards#283 / #310 / #311). -* `scripts/check-ts-allowlist.deno.js` — the compiled artifact the - governance workflow runs. -* `scripts/check-ts-allowlist.ts` — the Deno archetype, retained as the - regression-suite target (`scripts/tests/check-ts-allowlist-test.sh`) - and for parallel-validation during the TS→AS dual-target window. +* `scripts/check-ts-allowlist.sh` — the bash + awk implementation of the + Layer 4 detector that the governance workflow runs (since 2026-09-04; + previously an AffineScript source compiled to a Deno artifact). +* `scripts/tests/check-ts-allowlist-test.sh` — the 18-case regression + corpus that pins its behaviour. * `hyperpolymath/standards#????` — proposal that landed this consumer. * `hyperpolymath/hypatia` — the scanner that emits findings. diff --git a/docs/JS-RUNTIME-POLICY.adoc b/docs/JS-RUNTIME-POLICY.adoc index ad49381a0..701790f00 100644 --- a/docs/JS-RUNTIME-POLICY.adoc +++ b/docs/JS-RUNTIME-POLICY.adoc @@ -11,8 +11,7 @@ runtimes and package management. It is referenced by `governance-reusable.yml` (enforcement) and the canonical template `.gitignore` files (rsr-template-repo, v3-templater). -See also: `scripts/purge-node-modules.sh` (remediation utility) and -`docs/migrations/npm-to-deno-template/MIGRATION.md` (per-repo recipe). +See also: `scripts/purge-node-modules.sh` (remediation utility). [NOTE] ==== diff --git a/docs/migrations/npm-to-deno-template/INVENTORY-2026-05-30.adoc b/docs/migrations/npm-to-deno-template/INVENTORY-2026-05-30.adoc deleted file mode 100644 index f18134e01..000000000 --- a/docs/migrations/npm-to-deno-template/INVENTORY-2026-05-30.adoc +++ /dev/null @@ -1,91 +0,0 @@ -== npm → Deno estate inventory — 2026-05-30 re-run - -Re-inventory per `+hyperpolymath/standards#262+` acceptance criterion. -The umbrella body (`+#253+`) cited *172* `+package.json+` manifests as -of 2026-05-28; a looser `+find+` on 2026-05-30 returned *437* before -excludes. - -Re-running with the umbrella’s documented exclude set produces *162* -manifests across *63* repositories — within 6 % of the planning -baseline, no STEP re-sizing required. - -=== Exclude set applied - -Parallel to -`+hypatia/lib/rules/cicd_rules.ex :nodejs_detected path_allow_prefixes+`: - -* `+**/node_modules/**+`, `+**/deps/**+` (vendored) -* `+rescript/+`, `+servers/+`, `+repos-monorepo/+`, `+linguist/+` -(upstream forks) -* `+hyperpolymath-archive/**+` (archived) -* `+**/vscode/**+` (VSCode extension host-required) -* `+affinescript-deno-test/+`, `+affinescript-cli/+` (bootstrap shims) -* `+**/example/**+`, `+**/examples/**+`, `+**/test-fixtures/**+`, -`+**/fixtures/**+` (fixtures) -* `+**/.git/**+` - -=== Per-repo manifest count (top 25) - -[width="100%",cols="50%,50%",options="header",] -|=== -|Manifests |Repo -|28 |developer-ecosystem - -|14 |ssg-collection - -|10 |affinescript - -|9 |accessibility-everywhere - -|7 |burble - -|7 |affinescript-stdlib-pr - -|5 |stapeln - -|5 |boj-server - -|4 |standards - -|4 |reposystem - -|4 |flat-mate - -|3 |wordpress-tools - -|3 |julia-the-viper - -|3 |idaptik - -|2 |zotero-tools, typed-wasm, proven, patallm-gallery, my-lang, -kaldor-iiot, claude-integrations -|=== - -=== STEP sizing (refreshed) - -[cols=",,,",options="header",] -|=== -|STEP |Tier |Repos |Manifests -|3 |≤2 manifest, smallest-first |~45 |~50 -|4 |3-7 manifest, mid |~13 |~57 -|5 |8+ manifest, larger |3 |27 -|6 |developer-ecosystem only |1 |28 -|7 |workspace finalisation |multi-repo wrap-up |— -|=== - -162 = 50 + 57 + 27 + 28. STEP-7 wrap-up captures any post-batch hygiene. - -=== Drift from umbrella - -[width="100%",cols="34%,33%,33%",options="header",] -|=== -|Source |Count |Note -|Umbrella `+#253+` (2026-05-28) |172 |Planning baseline -|Loose `+find+` (2026-05-30) |437 |Without excludes -|*This re-run (2026-05-30)* |*162* |Documented excludes; canonical -|=== - -Drift -10 (-5.8 %) vs umbrella. Within tolerance; no STEP re-ordering. - -Source TSV: `+~/Documents/npm-to-deno-inventory-2026-05-30.tsv+` -(`+\t+` per row, 162 rows). diff --git a/docs/migrations/npm-to-deno-template/MIGRATION.adoc b/docs/migrations/npm-to-deno-template/MIGRATION.adoc deleted file mode 100644 index d5d51dc05..000000000 --- a/docs/migrations/npm-to-deno-template/MIGRATION.adoc +++ /dev/null @@ -1,208 +0,0 @@ -== npm → Deno per-repo migration recipe - -Canonical procedure for migrating a hyperpolymath estate repository from -`+package.json+` + npm/Node to `+deno.json+` + Deno. - -Policy: `+docs/JS-RUNTIME-POLICY.adoc+` (Deno > Bun > pnpm > npm). -Campaign tracker: hyperpolymath/standards#253. Rule enforcement: hypatia -`+cicd_rules/nodejs_detected+` + `+npx_or_npm_run_in_ci+`. - -=== 0. Decide which class the repo is in - -Before touching anything, decide which of the migration classes applies. -Each class has a different end-state. - -[width="100%",cols="34%,33%,33%",options="header",] -|=== -|Class |Signal |End-state -|*A. Pure-Deno port* |Repo’s `+package.json+` only lists dev-only -Node-compatible tools (`+typescript+`, `+vitest+`, build helpers). No -host contract requires Node. |Delete `+package.json+` + -`+package-lock.json+`. Author `+deno.json+`. CI workflows swap to -`+deno test+`/`+deno task+`. - -|*B. npm wrapper via Deno* |Repo wraps an npm-published tool that does -not yet have a Deno-native fork (e.g., `+rescript+`, `+vite+`, -`+tailwindcss+`). |Keep dependency expressed as `+npm:pkg@semver+` -inside `+deno.json+`’s `+imports+`. Tasks call -`+deno run -A --node-modules-dir=auto npm:pkg+`. No `+package.json+`. - -|*C. Carve-out* |One of the six classes in -`+cicd_rules/nodejs_detected+` `+path_allow_prefixes+` (VSCode -extension, bootstrap shim, upstream fork, archived, vendored, -example/fixture). |*Skip migration.* File stays on npm; no PR. -|=== - -A given repo with multiple `+package.json+` files can split across -classes — handle each manifest on its own merit. - -=== 1. Inventory the current `+package.json+` - -[source,bash] ----- -# Capture starting point. -cat package.json -ls -la package-lock.json bun.lockb yarn.lock pnpm-lock.yaml 2>/dev/null ----- - -Record: - -* Direct deps (`+dependencies+` + `+devDependencies+`). -* Scripts (`+scripts.*+`). -* `+engines.node+`, `+engines.npm+` — note for replacement by -`+engines.deno+`. -* `+private+`, `+type+`, `+exports+` — preserved as needed. - -=== 2. Author `+deno.json+` from the canonical template - -Copy `+deno.json+` from this directory. Adjust: - -* `+name+` — `+@hyperpolymath/+`. -* `+version+` — preserve from `+package.json+`. -* `+license+` — `+MPL-2.0-or-later+` (estate default) unless repo policy -differs. -* `+compilerOptions+` — preserve `+strict+` and friends from -`+tsconfig.json+` if present. -* `+imports+` — populate from `+dependencies+`: -** Deno-native: `+"@std/": "https://deno.land/std@0.224.0/"+` (and -similar). -** JSR: `+"@scope/pkg": "jsr:@scope/pkg@^1.2.3"+`. -** npm fallback: `+"pkg": "npm:pkg@^1.2.3"+` (Class B only). -* `+tasks+` — port from `+scripts+`: -** `+"build": "rescript"+` → -`+"build": "deno run -A --node-modules-dir=auto npm:rescript"+`. -** `+"test": "vitest"+` → `+"test": "deno test -A src/"+` (port tests to -Deno test API where reachable; if not yet portable, -`+"test": "deno run -A --node-modules-dir=auto npm:vitest"+`). -* `+nodeModulesDir+`: -** Default `+"none"+` (Class A — pure Deno). -** Set to `+"auto"+` only when an npm package’s lifecycle requires it -(Class B; rescript and most ESM-shipped npm packages are fine without -it). - -=== 3. Delete the npm scaffolding - -[source,bash] ----- -git rm package.json package-lock.json -# Also remove bun.lockb / yarn.lock / pnpm-lock.yaml / .npmrc if present. -git rm -f bun.lockb yarn.lock pnpm-lock.yaml .npmrc 2>/dev/null || true - -# node_modules/ should already be in .gitignore. -rm -rf node_modules ----- - -=== 4. Update `+.gitignore+` - -Confirm these entries are present (RSR canonical template propagates -them — see -`+docs/JS-RUNTIME-POLICY.adoc §Canonical .gitignore Entries+`): - -.... -# npm-avoidant (standards#67): estate JS-runtime policy is Bun>Deno>pnpm>npm. -package-lock.json -**/package-lock.json -node_modules/ -**/node_modules/ -bun.lockb -yarn.lock -pnpm-lock.yaml -.... - -=== 5. Migrate CI workflows - -Search for any of these and replace: - -[width="100%",cols="50%,50%",options="header",] -|=== -|Before |After -|`+actions/setup-node@+` |`+denoland/setup-deno@+` (or remove -if no JS step remains) - -|`+npm ci+` / `+npm install+` |`+deno cache +` (often -unnecessary — Deno caches at first run) - -|`+npm test+` / `+npm run test+` |`+deno task test+` (or -`+deno test -A src/+`) - -|`+npx +` |`+deno run -A --node-modules-dir=auto npm:+` -(Class B) or Deno-native equivalent (Class A) -|=== - -Note: hypatia `+cicd_rules/npx_or_npm_run_in_ci+` blocks `+npx+` and -`+npm run+` in CI run-blocks (added 2026-05-28). Don’t leave any. - -=== 6. Verify locally - -[source,bash] ----- -deno check src/ -deno lint src/ -deno fmt --check src/ -deno test -A src/ ----- - -Class B (npm wrapper) — exercise the wrapped tool end-to-end: - -[source,bash] ----- -deno task build -deno task test ----- - -=== 7. Commit pattern - -[source,bash] ----- -git add deno.json .gitignore .github/workflows/ -git rm package.json package-lock.json -git commit -m "feat(deno): migrate npm → Deno (standards#253) - - - -Class: A | B (per docs/migrations/npm-to-deno-template/MIGRATION.md) -Carry-forward: \">" ----- - -=== 8. PR + auto-merge - -Per estate convention: auto-merge with squash. - -[source,bash] ----- -gh pr create --title "feat(deno): npm → Deno (standards#253)" \ - --body "" -gh pr merge --auto --squash --delete-branch ----- - -=== Carry-forward patterns observed in oikos Phase 5 + 5 follow-ups (memory) - -* *ReScript wrapping* (canonical Class B): -`+deno run -A --node-modules-dir=auto npm:rescript@^12.0.0+`. -`+--allow-scripts=npm:rescript+` when the install lifecycle requires it. -* *Tailwind / vite / esbuild* — same pattern as rescript: -`+npm:@+`, `+--node-modules-dir=auto+`. -* *`+type: "module"+` repos* — Deno is ESM-native, no extra step. -* *`+exports+` field* — preserve in `+deno.json+` if the package is -published; otherwise drop. - -=== Anti-patterns - -* ❌ Don’t keep `+package.json+` "`for tooling only`" — `+deno.json+` -covers fmt/lint/test/tasks. -* ❌ Don’t fall back to `+npm:+` specifiers when a JSR or Deno-native -equivalent exists (use `+deno info +` to check). -* ❌ Don’t commit `+node_modules/+` even on Class B — -`+--node-modules-dir=auto+` regenerates at run-time. -* ❌ Don’t add `+"engines": {"node": "..."}+` to `+deno.json+` — Deno -doesn’t honour it and it signals the repo isn’t fully migrated. - -=== When migration is blocked - -If a `+package.json+` cannot be removed (host-required, Node-only -library, npm publish target), the path goes in the hypatia rule’s -`+path_allow_prefixes+` instead. See -`+standards/.claude/CLAUDE.md §npm Exemptions (Approved)+` for the -canonical exemption table. diff --git a/scripts/check-ts-allowlist.sh b/scripts/check-ts-allowlist.sh new file mode 100755 index 000000000..86924cbf9 --- /dev/null +++ b/scripts/check-ts-allowlist.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan Jewell +# +# check-ts-allowlist.sh — fail when hand-authored TypeScript appears outside +# the allowlist. Runs against the current working directory. +# +# WHY SHELL. This replaces check-ts-allowlist.deno.js, which put a Deno install +# step on a REQUIRED context on every estate repo. The obvious replacement was +# AffineScript compiled to Bun, but scripts/check-ts-allowlist.affine is +# TypeScript wearing an .affine extension: it has never compiled, so the .js +# beside it was never generated from it, and stdlib/Bun.affine declares no +# filesystem capability to port onto. Shell needs no runtime beyond the tools +# every runner already has — the same reasoning recorded in the header of +# scripts/check-workflow-duplicate-keys.sh. +# +# Behaviour is pinned by scripts/tests/check-ts-allowlist-test.sh, which +# asserts identical verdicts to the Deno implementation on every case. + +set -uo pipefail + +DIR_NAMES_ALLOWED=(bindings tests test scripts mcp-adapter cli vendor examples ffi node_modules benchmarks) + +# Strip leading "." and "/" characters, as the Deno implementation does, so +# "./src/a.ts", "/src/a.ts" and "src/a.ts" are one path. +normalize_repo_path() { + local out="$1" + out="${out#"${out%%[![:space:]]*}"}" # ltrim + out="${out%"${out##*[![:space:]]}"}" # rtrim + while [ -n "$out" ]; do + case "$out" in + .*|/*) out="${out#?}" ;; + *) break ;; + esac + done + printf '%s' "$out" +} + +# Glob -> anchored ERE. '*' is any run, '?' is one character, everything else +# regex-significant is escaped. +glob_to_regex() { + local g out="" i c + g="$(normalize_repo_path "$1")" + for (( i = 0; i < ${#g}; i++ )); do + c="${g:i:1}" + case "$c" in + '*') out+='.*' ;; + '?') out+='.' ;; + '.'|'+'|'('|')'|'{'|'}'|'['|']'|'^'|'$'|'|') out+="\\$c" ;; + $'\\') out+=$'\\\\' ;; + *) out+="$c" ;; + esac + done + printf '^%s$' "$out" +} + +# --- exemption sources ------------------------------------------------------- +# Layer 2: a "TypeScript Exemptions" table in .claude/CLAUDE.md +# Layer 2.5: one path per line in .governance-allowlist +EX_RAW=() + +load_exemptions_from_claude_md() { + [ -f .claude/CLAUDE.md ] || return 0 + local ts_heading='^#{1,4}[[:space:]]+.*(TypeScript|JavaScript|TS|JS|\.tsx?)\b[^#]*[Ee]xemption' + local any_heading='^#{1,4}[[:space:]]' + local in_table=0 line rest raw + while IFS= read -r line || [ -n "$line" ]; do + if [[ $line =~ $ts_heading ]]; then in_table=1; continue; fi + if [ "$in_table" -eq 1 ] && [[ $line =~ $any_heading ]]; then in_table=0; continue; fi + [ "$in_table" -eq 1 ] || continue + [ -n "$line" ] || continue + # a row whose first cell is a backticked path + [[ $line =~ ^[[:space:]]*\|[[:space:]]*\`[^\`]+\` ]] || continue + rest="${line#*\`}" # drop up to the first backtick + raw="${rest%%\`*}" # take up to the next one + [ -n "$raw" ] && EX_RAW+=("$raw") + done < .claude/CLAUDE.md +} + +load_exemptions_from_allowlist_file() { + [ -f .governance-allowlist ] || return 0 + local line raw + while IFS= read -r line || [ -n "$line" ]; do + raw="$(normalize_repo_path "$line")" + [ -n "$raw" ] || continue + case "$raw" in '#'*) continue ;; esac + EX_RAW+=("$raw") + done < .governance-allowlist +} + +is_exempt() { + local target rx bare e + target="$(normalize_repo_path "$1")" + for e in ${EX_RAW+"${EX_RAW[@]}"}; do + rx="$(glob_to_regex "$e")" + [[ $target =~ $rx ]] && return 0 + bare="$(normalize_repo_path "$e")" + [ "$target" = "$bare" ] && return 0 + case "$bare" in */) case "$target" in "$bare"*) return 0 ;; esac ;; esac + done + return 1 +} + +# --- builtin allowlist ------------------------------------------------------- +builtin_allowed() { + local p="$1" base seg d + base="${p##*/}" + case "$p" in *.d.ts) return 0 ;; esac + case "$base" in + mod.ts|lsp-server.ts|lsp_server.ts|lsp.ts) return 0 ;; + *-lsp.ts|*.bench.ts|*_bench.ts) return 0 ;; + esac + # any DIRECTORY segment (every segment but the last) + local dirpart="${p%/*}" + [ "$dirpart" = "$p" ] && return 1 + local IFS='/' + for seg in $dirpart; do + [ -n "$seg" ] || continue + for d in "${DIR_NAMES_ALLOWED[@]}"; do + [ "$seg" = "$d" ] && return 0 + done + case "$seg" in *vscode*) return 0 ;; deno-*) return 0 ;; esac + done + return 1 +} + +# A path is skipped entirely when any segment is a dotfile/dotdir (but "." and +# ".." are not dotfiles). +has_hidden_segment() { + local p="$1" seg + local IFS='/' + for seg in $p; do + [ -n "$seg" ] || continue + [ "$seg" = "." ] && continue + [ "$seg" = ".." ] && continue + case "$seg" in .*) return 0 ;; esac + done + return 1 +} + +# --- main -------------------------------------------------------------------- +load_exemptions_from_claude_md +load_exemptions_from_allowlist_file + +bad=() +while IFS= read -r f; do + [ -n "$f" ] || continue + has_hidden_segment "$f" && continue + n="$(normalize_repo_path "$f")" + builtin_allowed "$n" && continue + is_exempt "$n" && continue + bad+=("$n") +done < <(find . -type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.ts.bak' -o -name '*.tsx.bak' \) 2>/dev/null | LC_ALL=C sort) + +if [ "${#bad[@]}" -gt 0 ]; then + printf '%s\n' "❌ TypeScript files detected outside the allowlist." >&2 + printf '\n' >&2 + for f in "${bad[@]}"; do printf ' %s\n' "$f" >&2; done + printf '\n' >&2 + printf '%s\n' "To resolve, choose one:" >&2 + printf '%s\n' " (a) migrate the file to AffineScript" >&2 + printf '%s\n' " (b) move to an allowlisted bridge path" >&2 + printf '%s\n' " (c) add an entry to a 'TypeScript Exemptions' table in .claude/CLAUDE.md (Layer 2)" >&2 + printf '%s\n' " (d) add a line to .governance-allowlist at the repo root (Layer 2.5 — typed infrastructure file)" >&2 + printf '\n' >&2 + printf '%s\n' "See docs/EXEMPTION-MECHANISMS.adoc for the full mechanism reference." >&2 + if [ "${#EX_RAW[@]}" -gt 0 ]; then + printf '\n(Currently %d exemption(s) parsed across both layers.)\n' "${#EX_RAW[@]}" >&2 + fi + exit 1 +fi + +printf '✅ No TypeScript files outside allowlist (%d per-repo exemption(s) parsed across CLAUDE.md + .governance-allowlist).\n' "${#EX_RAW[@]}" diff --git a/scripts/tests/check-ts-allowlist-test.sh b/scripts/tests/check-ts-allowlist-test.sh index 27cb32d7a..2140fa13a 100755 --- a/scripts/tests/check-ts-allowlist-test.sh +++ b/scripts/tests/check-ts-allowlist-test.sh @@ -2,19 +2,19 @@ # SPDX-License-Identifier: MPL-2.0 # SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell # -# Regression test for the compiled scripts/check-ts-allowlist.deno.js artifact. -# The canonical source is check-ts-allowlist.affine, which is covered by the -# separate source/compile drift check. Each case constructs a fresh fixture tree -# under a tmpdir, runs the executable artifact with `--allow-read`, and asserts -# exit code + key output substrings. Mirrors the behaviour the previous inline- -# Python step was relied on for, so a future change cannot silently regress -# estate-wide policy. +# Regression test for scripts/check-ts-allowlist.sh, the hand-authored +# JavaScript/TypeScript gate that governance-reusable.yml runs on every estate +# repo. Each case constructs a fresh fixture tree under a tmpdir, runs the gate +# against it, and asserts exit code + key output substrings. Mirrors the +# behaviour the previous inline-Python step was relied on for, so a future +# change cannot silently regress estate-wide policy. The gate was a Deno +# artifact until 2026-09-04; it is now pure bash + awk with no JS runtime. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_TARGETS=( - "$SCRIPT_DIR/../check-ts-allowlist.deno.js" + "$SCRIPT_DIR/../check-ts-allowlist.sh" ) for target in "${SCRIPT_TARGETS[@]}"; do @@ -44,7 +44,7 @@ run_case() { for target in "${SCRIPT_TARGETS[@]}"; do set +e local out - out="$(cd "$tmp" && deno run --allow-read --no-lock "$target" 2>&1)" + out="$(cd "$tmp" && bash "$target" 2>&1)" local actual_exit=$? set -e diff --git a/scripts/tests/verify-regextarget-claim-test.sh b/scripts/tests/verify-regextarget-claim-test.sh new file mode 100755 index 000000000..bee3ef411 --- /dev/null +++ b/scripts/tests/verify-regextarget-claim-test.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell +# +# Tests for verify-regextarget-claim.sh. +# +# ⚠ WHAT THIS FILE IS ACTUALLY FOR. verify-regextarget-claim.sh is itself a +# gate: it measures whether an anchored value regex suppresses a gitleaks +# `generic-api-key` finding, because the estate once believed it did not and +# steered every repo toward blunt `paths` allowlists on that false basis. A +# measuring gate is only as good as its ABORTS -- if it can be made to report a +# confident answer on a contaminated or mis-versioned instrument, it reproduces +# exactly the wrong belief it exists to disprove. +# +# So these cases do not test the gitleaks semantics (that is the script's own +# job, and it needs a real gitleaks). They test that the script REFUSES to +# answer when it cannot answer honestly. Every case drives the script with a +# STUB gitleaks, so the guards are exercised deterministically on any host, +# with or without gitleaks installed. +set -uo pipefail + +SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/verify-regextarget-claim.sh" +[ -x "$SCRIPT" ] || { echo "FATAL: $SCRIPT not found or not executable" >&2; exit 2; } + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +pass=0 +fail=0 + +# A stub gitleaks. Reports $STUB_VERSION for `version`, and for `detect` writes +# $STUB_REPORT to whatever --report-path it was handed. +make_stub() { # make_stub + local p="$1" + cat > "$p" <<'STUB' +#!/usr/bin/env bash +if [ "${1:-}" = "version" ]; then printf '%s\n' "$STUB_VERSION"; exit 0; fi +rp="" +while [ $# -gt 0 ]; do + if [ "$1" = "--report-path" ]; then rp="${2:-}"; fi + shift +done +[ -n "$rp" ] && printf '%s' "$STUB_REPORT" > "$rp" +exit 0 +STUB + chmod +x "$p" + export STUB_VERSION="$2" + export STUB_REPORT="$3" +} + +check() { # check -- runs SCRIPT + local name="$1" want_rc="$2" want_txt="$3"; shift 3 + local out rc + out="$("$@" 2>&1)"; rc=$? + if [ "$rc" = "$want_rc" ] && printf '%s' "$out" | grep -qF "$want_txt"; then + printf ' ok %s\n' "$name"; pass=$((pass + 1)) + else + printf ' FAIL %s\n' "$name" + printf ' wanted rc=%s containing: %s\n' "$want_rc" "$want_txt" + printf ' got rc=%s: %s\n' "$rc" "$(printf '%s' "$out" | tr '\n' ' ' | cut -c1-160)" + fail=$((fail + 1)) + fi +} + +PINNED=8.18.4 +ONE_FINDING='[{"RuleID":"generic-api-key","Match":"Key : Ed25519_Private_Key;","Secret":"Ed25519_Private_Key"}]' + +# 1. No gitleaks at all must ABORT (rc 2), not silently skip. A gate that +# vanishes when its tool is absent is the estate's commonest fake green. +check "absent gitleaks aborts, does not skip" 2 "gitleaks not found" \ + env GITLEAKS="$TMP/does-not-exist" "$SCRIPT" + +# 2. A non-pinned gitleaks must ABORT. These results are version-specific; +# answering on an unpinned build is how a stale claim gets re-confirmed. +make_stub "$TMP/gl" "8.18.3" "$ONE_FINDING" +check "version drift aborts by default" 2 "CI pins $PINNED" \ + env GITLEAKS="$TMP/gl" STUB_VERSION=8.18.3 STUB_REPORT="$ONE_FINDING" "$SCRIPT" + +# 3. ...but the documented override must actually get PAST that guard, or the +# escape hatch is decorative. The assertion is deliberately narrow: it says +# the run reached the MEASUREMENT phase (it printed the control rule), not +# that the whole script succeeded. A constant stub cannot satisfy the five +# semantic expectations that follow -- only a real gitleaks can -- so the +# script correctly ends non-zero here, and asserting rc=0 would be a lie. +check "GITLEAKS_ALLOW_VERSION_DRIFT=1 reaches measurement" 1 "control: rule=generic-api-key" \ + env GITLEAKS="$TMP/gl" GITLEAKS_ALLOW_VERSION_DRIFT=1 \ + STUB_VERSION=8.18.3 STUB_REPORT="$ONE_FINDING" "$SCRIPT" + +# 3b. And the override must still WARN -- a silent override is how a +# version-specific result gets quoted later as if it were pinned. +check "override still warns about the drift" 1 "WARNING: measuring on 8.18.3" \ + env GITLEAKS="$TMP/gl" GITLEAKS_ALLOW_VERSION_DRIFT=1 \ + STUB_VERSION=8.18.3 STUB_REPORT="$ONE_FINDING" "$SCRIPT" + +# 4. THE CONTAMINATION GUARD -- the reason the script has a control at all. +# If the instrument reports zero findings for the control fixture, a WORKING +# allowlist entry is indistinguishable from an inert one, which is the most +# likely origin of the original false claim. It must abort, never measure. +check "control with 0 findings aborts (contaminated instrument)" 2 "control expected exactly 1 finding" \ + env GITLEAKS="$TMP/gl" STUB_VERSION="$PINNED" STUB_REPORT='[]' "$SCRIPT" + +# 5. Same for a control that fires the WRONG rule: the fixture is then no +# longer measuring generic-api-key, so every later count is off-target. +check "control firing the wrong rule aborts" 2 "control fired" \ + env GITLEAKS="$TMP/gl" STUB_VERSION="$PINNED" \ + STUB_REPORT='[{"RuleID":"aws-access-token","Match":"x","Secret":"x"}]' "$SCRIPT" + +# 6. Two findings is also contamination (config or report inside --source). +check "control with 2 findings aborts" 2 "control expected exactly 1 finding" \ + env GITLEAKS="$TMP/gl" STUB_VERSION="$PINNED" \ + STUB_REPORT='[{"RuleID":"generic-api-key","Match":"a","Secret":"a"},{"RuleID":"generic-api-key","Match":"b","Secret":"b"}]' \ + "$SCRIPT" + +echo +echo "=== SUMMARY ===" +echo "Pass: $pass" +echo "Fail: $fail" +[ "$fail" -eq 0 ]