From dd4b34b74b8c75e6eb51f79ce6be438258956ddb Mon Sep 17 00:00:00 2001 From: Julian Matschinske Date: Sun, 27 Sep 2026 23:20:28 +0200 Subject: [PATCH] ci: check project names against the org's naming rule Vendors Bitspark/.github's naming/naming.mjs and its repository snapshot unchanged into scripts/, runs it in CI, and names the rule in AGENTS.md. The prose already conforms; the check keeps it that way (bitruntime#18). Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 2 + AGENTS.md | 9 ++ scripts/bitspark-repositories.txt | 261 ++++++++++++++++++++++++++++++ scripts/naming.mjs | 138 ++++++++++++++++ 4 files changed, 410 insertions(+) create mode 100644 scripts/bitspark-repositories.txt create mode 100644 scripts/naming.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d71d045..412f662 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,8 @@ jobs: cache-dependency-path: package-lock.json - name: Go formatting run: test -z "$(gofmt -l $(git ls-files '*.go'))" + - name: Project names + run: node scripts/naming.mjs - run: go vet ./... - run: go test -race -count=1 ./... - run: npm ci --ignore-scripts diff --git a/AGENTS.md b/AGENTS.md index cf1f014..beba12f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,15 @@ before changing this tree. - Do not add private checkout dependencies, local orchestration state or credentials. +## Naming + +Write every Bitspark project exactly as its repository is named, in prose, +headings, comments and messages alike: `bitwire`, `nightseam`, `bitruntime`, +`bitsystem3` (the org's +[naming rule](https://github.com/Bitspark/.github/blob/master/NAMING.md)). CI +runs `node scripts/naming.mjs`; refresh its name list with +`node scripts/naming.mjs --update`. + ## Repository layout Use component-first source paths with two-letter language directories: diff --git a/scripts/bitspark-repositories.txt b/scripts/bitspark-repositories.txt new file mode 100644 index 0000000..4fa381d --- /dev/null +++ b/scripts/bitspark-repositories.txt @@ -0,0 +1,261 @@ +# Bitspark repository names, excluding forks. Refresh: node naming.mjs --update +.github +agent-hub +agent-program +agent-program-centralized-mvp-1 +agent-program-centralized-mvp-2 +agent-program-centralized-mvp-2-claude +agent-program-meta +agent-program-node-sandbox +agsim +aif-code +aiface +aifunc +aifunc-api +aifunc-mono +aifunc-msa +aifunc-web +aifuncref +aiscape +arche +arche-agents +arche-core +arche-runner +archon +archon-internal +archon-legacy +atlas +baci-canary +baci-staging-synthetic +bb +bb-agent +bb-agent-gate +bb-agent-harness +bb-agent-hooks +bb-agent-router +bb-anchor +bb-artifacts +bb-cluster +bb-console +bb-datahub +bb-go +bb-hub +bb-identity +bb-ledger +bb-machine +bb-provision +bb-trust +bb-vault +bb2 +biot +bit-services-contract +bitagent-accounts +bitagent-blackboard +bitagent-ci +bitagent-cli +bitagent-co +bitagent-commons +bitagent-core +bitagent-demo +bitagent-design +bitagent-fleet +bitagent-gateway +bitagent-hooks +bitagent-kernel +bitagent-lab +bitagent-lab-legacy +bitagent-os +bitagent-runtime +bitagent-runtime-legacy +bitagent-vault +bitagent-website +bitbooks +bitcube +bitlink +bitmachine +bitnode +bitnode-js +bitnode-old +bitruntime +bitschema +bitspark-design +bitspark.de +bitspark.dev +bitstore +bitstore-svc +bitsystem +bitsystem2 +bitsystem3 +bittheory +bittree +bittype +bitverse +bitwire +bitwire-svc +bm2 +bn-bytes +bn-cells +bn-trees +bn-values +bn-wires +bn2-system-meta +bn3-system +bne-chora +claude-lab +codeartist +codeless-client +codeless-server +computehub +constellation +corpus +crossproduct +cursus +decisionmaker +defs-anchor +defs-node +defs-os +defs-source +defs-space +defs-tisl +defs-tisl-tests +defs-transport +defs-tree +defs-types +dehaze-jjmed +dehaze-risk-app +deixis +deixis-svc +demo-repository +demo.login.bitspark.de +demonight-spring-2019 +design +devspaces +dm +embwd19 +example-iot-dht11 +example-iot-moisture +fieldtest +flabnet-federation +flabnet-node +foundry +frozen-thoughts +go-bitlink +go-bitnode-legacy +go-blank-sparkable +go-liquidscript +go-llm +go-tree +go-tree_old +go-vingi +go-vyze +golm +graph +harmos +harmos-lab +healthdati-design +healthdati.net +hgm-personio +hlte +horos +iot-dht +izinyoka +kosmos +kosmos-archived +kosmos-legacy +l111m +llllm +logos +logos-bench +logos-db +logos-hub +logos-kernel +mimos +models-bindings +models-gomod +ng-vyze +ng-vyze-legacy +nightfall +nightforge +nightglass +nighthall +nightjar +nightmill +nightseam +nightseam-cloud +nightseam-flows +nightshift +nightstudio +nightwork +ontos +pcb-agent +pharos +playground.tryslang.com +prism +python-vyze +quackstage +radial +refile +repo-meta +repo-tool +repo-tools +reveng +schema +semantic-history +shelm +shelm_archive +skopos +slang +slang-auth +slang-ci +slang-customer +slang-def +slang-deployment +slang-design +slang-ecosystem +slang-editor +slang-examples +slang-infra +slang-lib +slang-meta +slang-narrow +slang-repo +slang-runnable +slang-search +slang-showcases +slang-studio +slang-transit +slang-ui +slang-usage +slang-vision +slang-website +space-lab +stack +stele +tabula +taxis +telos +telos-legacy +telos-old +telstar +thesmos +tool-defs +tool-defs-ai +tool-defs-anchor +tool-defs-node +tool-defs-os +tool-defs-space +tool-defs-tisl +tool-defs-transport +tree-lab +tree-sitter-markdown +types-gomod +vylang-server +vyze +vyze-js +vyze-js-cli +vyze-modeler +vyze-view-angular +vyze-view-angular-old +vyze-view-vision +weft +xilinx-board diff --git a/scripts/naming.mjs b/scripts/naming.mjs new file mode 100644 index 0000000..cf0e597 --- /dev/null +++ b/scripts/naming.mjs @@ -0,0 +1,138 @@ +#!/usr/bin/env node +// Bitspark naming check. The rule: a project's name is its GitHub repository +// name, character for character (https://github.com/Bitspark/.github/blob/master/NAMING.md). +// +// node naming.mjs report prose that spells a repository name differently +// node naming.mjs --fix rewrite those spellings in place +// node naming.mjs --update refresh bitspark-repositories.txt from GitHub (needs gh) +// +// Copy this file and bitspark-repositories.txt into a repository unchanged. +// Paths listed in .namingignore at the repository root are skipped; each line +// is a path prefix or a glob with `*`, and `#` starts a comment. +import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs'; +import { dirname, join, relative, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = dirname(fileURLToPath(import.meta.url)); +const snapshot = join(here, 'bitspark-repositories.txt'); + +// Repository names that are also ordinary words or external products. The rule +// applies to them, but prose cannot tell "stack" the word from Stack the +// repository, so the check does not flag them. +const ordinaryWords = new Set([ + 'arche', 'atlas', 'constellation', 'corpus', 'design', 'dm', 'fieldtest', 'foundry', + 'graph', 'prism', 'radial', 'refile', 'schema', 'slang', 'stack', 'taxis', 'weft', +]); + +if (process.argv.includes('--update')) { + const listed = JSON.parse(execFileSync('gh', ['repo', 'list', 'Bitspark', '--limit', '1000', '--json', 'name,isFork'], { encoding: 'utf8', maxBuffer: 1 << 30 })); + // A fork carries its upstream project's name, which that project spells. + const names = listed.filter((r) => !r.isFork).map((r) => r.name).sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' })); + writeFileSync(snapshot, `# Bitspark repository names, excluding forks. Refresh: node naming.mjs --update\n${names.join('\n')}\n`); + console.log(`${names.length} names written to ${snapshot}`); + process.exit(0); +} + +const canonical = new Map(); +for (const line of readFileSync(snapshot, 'utf8').split('\n')) { + const name = line.trim(); + if (name && !name.startsWith('#') && !ordinaryWords.has(name.toLowerCase())) canonical.set(name.toLowerCase(), name); +} + +const root = execFileSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim(); +const ignore = existsSync(join(root, '.namingignore')) + ? readFileSync(join(root, '.namingignore'), 'utf8').split('\n').map((l) => l.replace(/#.*/, '').trim()).filter(Boolean) + : []; +const ignored = (path) => ignore.some((pattern) => pattern.includes('*') + ? new RegExp(`^${pattern.split('*').map((p) => p.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*')}$`).test(path) + : path === pattern || path.startsWith(pattern.endsWith('/') ? pattern : `${pattern}/`)); + +// The snapshot is the list of names, not prose about them. +const own = relative(root, snapshot).split(sep).join('/'); +const files = execFileSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf8', maxBuffer: 1 << 30 }).split('\0') + .filter((path) => /\.(md|mdx|markdown|txt|rst)$/i.test(path) && path !== own && !ignored(path)) + // Build and dependency files that end in .txt are code, not prose. + .filter((path) => !/(^|\/)(CMakeLists|requirements[^/]*|constraints)\.txt$/i.test(path)); + +// Blank out what is not prose: inline code, link targets, URLs and HTML tags. +// Identifiers and addresses follow their own rules; a path that names a +// repository already spells it as the repository does. +function prose(line) { + const blank = (m) => ' '.repeat(m.length); + return line + .replace(/(`+)[^`]*?\1/g, blank) + .replace(/\]\([^)]*\)/g, blank) + .replace(/<[^>]*>/g, blank) + .replace(/\b[a-z][a-z0-9+.-]*:\/\/\S+/gi, blank); +} + +// The longest hyphen-joined run of segments that is a repository name wins, so +// logos-db is one name while Bitwire-based still flags Bitwire. An underscore +// belongs to its word: shelm_archive is one word, not SHELM. +function findings(masked) { + const found = []; + for (const token of masked.matchAll(/[A-Za-z0-9_]+(?:-[A-Za-z0-9_]+)*/g)) { + const segments = token[0].split('-'); + let offset = token.index; + for (let i = 0; i < segments.length;) { + let matched = 0; + for (let j = segments.length; j > i; j--) { + const candidate = segments.slice(i, j).join('-'); + const name = canonical.get(candidate.toLowerCase()); + if (name) { + if (candidate !== name) found.push({ at: offset, text: candidate, name }); + matched = j - i; + break; + } + } + const step = Math.max(matched, 1); + offset += segments.slice(i, i + step).join('-').length + 1; + i += step; + } + } + return found; +} + +const fix = process.argv.includes('--fix'); +let count = 0; +for (const path of files) { + const full = join(root, path); + if (statSync(full).size > 2_000_000) continue; + const lines = readFileSync(full, 'utf8').split('\n'); + let fence = null; + let changed = false; + lines.forEach((line, index) => { + const opening = line.match(/^\s*(```+|~~~+)\s*([\w+-]*)/); + if (fence) { + if (opening && !opening[2] && opening[1][0] === fence.marker[0] && opening[1].length >= fence.marker.length) { + fence = null; + return; + } + // A ```text block is prose laid out by hand; any other block is code. + if (!fence.prose) return; + } else if (opening) { + fence = { marker: opening[1], prose: /^(text|txt)$/i.test(opening[2]) }; + return; + } + if (/^\s*>/.test(line)) return; // quotations stay verbatim + const found = findings(prose(line)); + for (const f of found) { + count++; + if (!fix) console.log(`${path}:${index + 1}:${f.at + 1}: ${f.text} → ${f.name}`); + } + if (fix && found.length) { + let out = line; + for (const f of [...found].reverse()) out = out.slice(0, f.at) + f.name + out.slice(f.at + f.text.length); + lines[index] = out; + changed = true; + } + }); + if (changed) writeFileSync(full, lines.join('\n')); +} +if (fix) { + console.log(`${count} spellings rewritten.`); +} else if (count) { + console.log(`\n${count} spellings differ from their repository names. Run node ${join(here, 'naming.mjs')} --fix, then review.`); + process.exit(1); +}