Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions .github/workflows/governance-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,37 @@ jobs:
enforce "Python files" "only allowed for SaltStack" "$PY_FILES"

- name: Check for npm/bun artifacts
# standards#67 — npm-avoidant: package-lock.json must never be tracked
# estate-wide. Check recursively (not just root) to catch monorepo
# sub-packages. See docs/JS-RUNTIME-POLICY.adoc.
run: |
if [ -f "package-lock.json" ] || [ -f "bun.lockb" ] || [ -f ".npmrc" ] || [ -f "yarn.lock" ]; then
echo "❌ npm/bun/yarn artifacts detected. Use Deno instead."
LOCK_FILES=$(git ls-files 'package-lock.json' '**/package-lock.json' 2>/dev/null || true)
BUN_FILES=$(find . -name "bun.lockb" -not -path "./.git/*" 2>/dev/null || true)
YARN_FILES=$(find . -name "yarn.lock" -not -path "./.git/*" 2>/dev/null || true)
NPMRC_FILES=$(find . -name ".npmrc" -not -path "./.git/*" 2>/dev/null || true)
FAILED=""
if [ -n "$LOCK_FILES" ]; then
echo "❌ Tracked package-lock.json detected (standards#67 — npm-avoidant):"
printf '%s\n' "$LOCK_FILES"
FAILED=1
fi
if [ -n "$BUN_FILES" ]; then
echo "❌ bun.lockb detected. Use Deno instead."
printf '%s\n' "$BUN_FILES"
FAILED=1
fi
if [ -n "$YARN_FILES" ]; then
echo "❌ yarn.lock detected. Use Deno instead."
printf '%s\n' "$YARN_FILES"
FAILED=1
fi
if [ -n "$NPMRC_FILES" ]; then
echo "❌ .npmrc detected. Use Deno deno.json for JS config."
printf '%s\n' "$NPMRC_FILES"
FAILED=1
fi
if [ -n "$FAILED" ]; then
echo "See hyperpolymath/standards docs/JS-RUNTIME-POLICY.adoc for remediation."
exit 1
fi
echo "✅ No npm/bun violations"
Expand Down
131 changes: 131 additions & 0 deletions docs/JS-RUNTIME-POLICY.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
= JS Runtime & Package Management Policy
:revdate: 2026-05-19
:status: ACTIVE
:issue-67: hyperpolymath/standards#67
:issue-68: hyperpolymath/standards#68

This document is the canonical estate-wide policy for JavaScript/TypeScript
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).

== Runtime Hierarchy

Tool selection MUST follow this hierarchy:

[cols="1,2,3", options="header"]
|===
| Priority | Tool | Rationale

| 1
| *Deno*
| Architectural standard. Secure-by-default, no `node_modules` pollution,
built-in TypeScript support. All new JS/TS work starts here.

| 2
| *Bun*
| Practical performance fallback. Drop-in Node compatibility. Use where
specific npm-ecosystem libraries are required and Deno cannot reach.

| 3
| *pnpm*
| Efficiency fallback. Prefer over npm when Bun is incompatible.

| 4
| *npm*
| Absolute last resort. Only permitted for platform-specific requirements
(e.g. VSCode Extension publishing) or legacy audits. Every npm use must
carry a `# hypatia:ignore` inline exemption comment.
|===

== Hard Rules (enforced by governance-reusable.yml)

1. `package-lock.json` MUST NOT be tracked in any repo. ({issue-67})
- Add to `.gitignore` via canonical template propagation.
- If already tracked: `git rm --cached package-lock.json` and add to
`.gitignore`. Use `purge-node-modules.sh --force` to clean local
copies.
2. `bun.lockb`, `yarn.lock`, `.npmrc` MUST NOT be tracked.
3. `node_modules/` MUST NOT be tracked (already in canonical `.gitignore`).
4. `.editorconfig` and `.claude/` MUST NOT be tracked. ({issue-68})
- These are local-only (agent scaffolding + editor config).
- Add to `.gitignore` via canonical template propagation.

== Canonical .gitignore Entries (template source of truth)

The following entries MUST be present in every repo's `.gitignore` (carried
from rsr-template-repo and v3-templater via template propagation):

----
# npm-avoidant (standards#67): estate JS-runtime policy is Deno>Bun>pnpm>npm.
# npm lockfiles must never be committed estate-wide.
package-lock.json
**/package-lock.json

# Agent & editor scaffolding (standards#68) — local-only, never committed
# estate-wide. Owner decision 2026-05-16: gitignore both rather than commit
# per-repo agent/editor config.
.editorconfig
.claude/
----

== Remediation

=== Remove a tracked package-lock.json

[source,bash]
----
git rm --cached package-lock.json
echo 'package-lock.json' >> .gitignore
echo '**/package-lock.json' >> .gitignore
git commit -m "chore(gitignore): remove tracked package-lock.json (standards#67)"
----

=== Bulk remediation (estate-wide audit)

Use the propagation script at `scripts/propagate-gitignore-67-68.sh` in this
repo to identify and prepare per-repo branches. The script is READ-ONLY by
default (dry-run); pass `--fix` to stage changes for review.

See also: `npm-avoidant/scripts/purge-node-modules.sh` for removing local
`node_modules/` and lockfile debris.

== Consumer-repo Audit (2026-05-19 snapshot)

Obtained via `git ls-files` sweep across `/home/hyperpolymath/dev/repos/`:

[cols="2,1", options="header"]
|===
| Metric | Count

| Repos with tracked `package-lock.json`
| 10

| Repos with tracked `.editorconfig`
| 118

| Repos with tracked `.claude/` files
| 56
|===

Detailed list of repos with tracked `package-lock.json` (primary blocker for
standards#67):

* `ci` (nested: `firefox-lsp/vscode-extension/package-lock.json`)
* `claude-integrations` (nested: `firefox-lsp/vscode-extension/package-lock.json`)
* `developer-ecosystem` (nested: `rescript-ecosystem/rescript-tea/package-lock.json`)
* `git-scripts` (nested: `ui/package-lock.json`)
* `hyperpolymath-archive` (nested: `flatracoon-netstack/interface/package-lock.json`)
* `panll` (root)
* `repos-monorepo` (nested: multiple `boj-cartridges/` sub-packages)
* `rescript-tea` (root)
* `typed-wasm` (root)
* `v3-templater` (root — RESOLVED by PR #68/69, now in `.gitignore`)

NOTE: `.editorconfig` (118 repos) and `.claude/` (56 repos) are already-committed
historical files. The gitignore fix prevents new commits; removing existing
tracked copies is a separate per-repo chore, owner-gated. See
`scripts/propagate-gitignore-67-68.sh` for the propagation recipe.
180 changes: 180 additions & 0 deletions scripts/propagate-gitignore-67-68.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: PMPL-1.0-or-later
# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
#
# propagate-gitignore-67-68.sh — Propagate canonical .gitignore additions
# for standards#67 (package-lock.json) and standards#68 (.editorconfig /
# .claude/) to consumer repos.
#
# Mode: READ-ONLY (audit) by default.
# Pass --fix to stage changes on a branch in each affected repo.
# The script NEVER commits or pushes — that is the human's job.
#
# Principles (do not violate):
# * Non-destructive: only modifies .gitignore (never removes tracked files).
# * Idempotent: entries already present are not duplicated.
# * No auto-commit / auto-push (estate guardrail: no unattended mutations).
# * Shell-only: no Python, no SaltStack, no Ruby.
#
# Usage:
# bash standards/scripts/propagate-gitignore-67-68.sh [--fix] [REPO_DIR]
#
# REPO_DIR defaults to ~/dev/repos.
#
# With --fix, for each affected repo this script:
# 1. Creates branch chore/gitignore-67-68 off default branch (if not exists).
# 2. Appends missing entries to .gitignore.
# 3. git rm --cached any tracked package-lock.json (staged, not committed).
# Leaves commits + push to the human operator.
#
# Output: tab-separated audit lines suitable for review.

set -euo pipefail

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

ISSUES_REF="Refs hyperpolymath/standards#67\nRefs hyperpolymath/standards#68"
BRANCH="chore/gitignore-67-68"
FIX_MODE=false
REPO_ROOT="${HOME}/dev/repos"

while [[ $# -gt 0 ]]; do
case "$1" in
--fix) FIX_MODE=true; shift ;;
*) REPO_ROOT="$1"; shift ;;
esac
done

# Canonical .gitignore block to inject (only entries not already present)
BLOCK_67='# npm-avoidant (standards#67): estate JS-runtime policy is Deno>Bun>pnpm>npm.
# npm lockfiles must never be committed estate-wide.
package-lock.json
**/package-lock.json'

BLOCK_68='# Agent & editor scaffolding (standards#68) — local-only, never committed
# estate-wide. Owner decision 2026-05-16: gitignore both rather than commit
# per-repo agent/editor config.
.editorconfig
.claude/'

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

log() { printf '%s\n' "$*" >&2; }
sep() { log "---"; }

# Returns 0 if pattern is already in .gitignore (or if no .gitignore present)
already_ignored() {
local gi="$1" pattern="$2"
[ -f "$gi" ] && grep -qxF "$pattern" "$gi"
}

append_if_missing() {
local gi="$1"; shift
local header="$1"; shift
local -a entries=("$@")
local needs_header=false
for entry in "${entries[@]}"; do
already_ignored "$gi" "$entry" || needs_header=true
done
if $needs_header; then
printf '\n%s\n' "$header" >> "$gi"
for entry in "${entries[@]}"; do
if ! already_ignored "$gi" "$entry"; then
printf '%s\n' "$entry" >> "$gi"
fi
done
fi
}

# ---------------------------------------------------------------------------
# Main audit loop
# ---------------------------------------------------------------------------

echo "# propagate-gitignore-67-68.sh audit — $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "# mode: $([ "$FIX_MODE" = true ] && echo FIX || echo DRY-RUN)"
echo "# repo_root: $REPO_ROOT"
echo "#"
printf '%-50s\t%s\t%s\t%s\t%s\n' "REPO" "PKG_LOCK_TRACKED" "EDITORCONFIG_TRACKED" "CLAUDE_TRACKED" "GITIGNORE_HAS_67_68"

for repo in "$REPO_ROOT"/*/; do
[ -d "$repo/.git" ] || continue
name="${repo%/}"
name="${name##*/}"

pkg_lock_tracked=$(git -C "$repo" ls-files 'package-lock.json' '**/package-lock.json' 2>/dev/null | wc -l | tr -d ' ')
editorconfig_tracked=$(git -C "$repo" ls-files '.editorconfig' 2>/dev/null | wc -l | tr -d ' ')
claude_tracked=$(git -C "$repo" ls-files '.claude/' 2>/dev/null | wc -l | tr -d ' ')

gi="$repo/.gitignore"
gi_has_67=true
gi_has_68=true
{ already_ignored "$gi" "package-lock.json" && already_ignored "$gi" "**/package-lock.json"; } || gi_has_67=false
{ already_ignored "$gi" ".editorconfig" && already_ignored "$gi" ".claude/"; } || gi_has_68=false
gi_status="$([ "$gi_has_67" = true ] && echo y || echo N)/$([ "$gi_has_68" = true ] && echo y || echo N)"

printf '%-50s\t%s\t%s\t%s\t%s\n' \
"$name" "$pkg_lock_tracked" "$editorconfig_tracked" "$claude_tracked" "$gi_status"

if $FIX_MODE; then
needs_work=false
$gi_has_67 || needs_work=true
$gi_has_68 || needs_work=true
[ "$pkg_lock_tracked" -gt 0 ] && needs_work=true

if $needs_work; then
default_branch=$(git -C "$repo" remote show origin 2>/dev/null \
| grep 'HEAD branch' | cut -d' ' -f5 || echo main)
current_branch=$(git -C "$repo" branch --show-current 2>/dev/null || echo "")

# Create fix branch if not already on it
if [ "$current_branch" != "$BRANCH" ]; then
if git -C "$repo" rev-parse --verify "refs/heads/$BRANCH" >/dev/null 2>&1; then
git -C "$repo" checkout "$BRANCH" 2>/dev/null
else
git -C "$repo" fetch origin "$default_branch" 2>/dev/null || true
git -C "$repo" checkout -b "$BRANCH" "origin/$default_branch" 2>/dev/null \
|| git -C "$repo" checkout "$BRANCH" 2>/dev/null
fi
fi

# Ensure .gitignore exists
[ -f "$gi" ] || touch "$gi"

# Append missing blocks
if ! $gi_has_67; then
append_if_missing "$gi" \
"# npm-avoidant (standards#67): estate JS-runtime policy is Deno>Bun>pnpm>npm." \
"# npm lockfiles must never be committed estate-wide." \
"package-lock.json" \
"**/package-lock.json"
fi
if ! $gi_has_68; then
append_if_missing "$gi" \
"# Agent & editor scaffolding (standards#68) — local-only, never committed" \
"# estate-wide. Owner decision 2026-05-16: gitignore both rather than commit" \
"# per-repo agent/editor config." \
".editorconfig" \
".claude/"
fi

# Un-track package-lock.json if tracked
if [ "$pkg_lock_tracked" -gt 0 ]; then
git -C "$repo" ls-files 'package-lock.json' '**/package-lock.json' 2>/dev/null \
| while IFS= read -r f; do
git -C "$repo" rm --cached "$f" 2>/dev/null && log " [rm-cached] $name: $f"
done
fi

log " [fix applied] $name — branch: $BRANCH"
fi
fi
done

echo "#"
echo "# Audit complete."
echo "# --fix mode: review per-repo branches and open draft PRs manually."
echo "# References: $ISSUES_REF"
Loading