style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)

Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-04 15:58:06 +02:00
committed by GitHub
co-authored by Claude Sonnet 5
parent a5154ee884
commit ec27228a8e
1588 changed files with 28388 additions and 10890 deletions
+16 -16
View File
@@ -8,11 +8,11 @@
* Keeping discovery in one place means the two paths can never drift on which
* skills count as atoms, how titles/tokens are derived, or how frontmatter is read.
*
* Tiers discovered (the curated set — swarm-* and other Claude-Code-only skills
* Tiers discovered (the curated set: swarm-* and other Claude-Code-only skills
* are intentionally NOT matched here, so they never become atoms):
* horizontal — `.claude/skills/swedish-*\/SKILL.md` (regulatory)
* vertical — `.claude/skills/industry/<slug>\/SKILL.md` (industry)
* modifier — `.claude/skills/modifier/<slug>\/SKILL.md` (cross-cutting)
* horizontal: `.claude/skills/swedish-*\/SKILL.md` (regulatory)
* vertical : `.claude/skills/industry/<slug>\/SKILL.md` (industry)
* modifier : `.claude/skills/modifier/<slug>\/SKILL.md` (cross-cutting)
*/
import { readdir, readFile, stat } from 'node:fs/promises'
@@ -30,13 +30,13 @@ export interface DiscoveredAtom {
sni_prefixes: string[]
trigger_signals: Record<string, unknown>
/**
* Token estimate over the SKILL.md content ONLY — the unit actually loaded
* Token estimate over the SKILL.md content ONLY: the unit actually loaded
* into the system prompt / returned by gnubok_load_skill. (We deliberately do
* NOT count references/*.md, which are not read at runtime.)
*/
estimated_tokens: number
/**
* Repo-relative path to the body source — SKILL.md for top-level skills,
* Repo-relative path to the body source: SKILL.md for top-level skills,
* the references/*.md file for reference children (provenance + dev-fallback).
*/
body_path: string
@@ -50,7 +50,7 @@ export interface DiscoveredAtom {
* NULL for a top-level skill; the parent skill's id for a reference child.
* Reference rows are hidden from every catalog (the metadata index, the MCP
* skill list, the composer atom index, the settings panel) by a
* `parent_atom_id IS NULL` filter — they reach the model only via an explicit
* `parent_atom_id IS NULL` filter: they reach the model only via an explicit
* gnubok_load_skill(<child id>) call after the parent SKILL.md is loaded.
*/
parent_atom_id: string | null
@@ -69,7 +69,7 @@ function normalizeLineEndings(text: string): string {
// ── Frontmatter parsing ────────────────────────────────────────────────
// SKILL.md files use YAML frontmatter with `name`, `description`, and optionally
// `tier`, `sni_prefixes`, `trigger_signals`, `estimated_tokens`, `version`. We
// parse only the keys we care about — js-yaml is not in deps.
// parse only the keys we care about: js-yaml is not in deps.
interface Frontmatter {
raw: string
@@ -161,7 +161,7 @@ function unquote(s: string): string {
// ── Token estimation ──────────────────────────────────────────────────
// Chars/4 baseline (Anthropic guidance for English). Swedish text inflates on
// Opus 4.7's tokenizer — re-measure post-POC.
// Opus 4.7's tokenizer: re-measure post-POC.
export function estimateTokens(text: string): number {
return Math.ceil(text.length / 4)
}
@@ -237,11 +237,11 @@ async function readAtom(
const content = normalizeLineEndings(await readFile(skillPath, 'utf8'))
const fm = extractFrontmatter(content)
if (!fm) {
console.warn(` skipped ${relative(rootDir, skillPath)} — no frontmatter`)
console.warn(` skipped ${relative(rootDir, skillPath)}: no frontmatter`)
return []
}
if (!fm.description) {
console.warn(` skipped ${relative(rootDir, skillPath)} — missing description`)
console.warn(` skipped ${relative(rootDir, skillPath)}: missing description`)
return []
}
@@ -264,7 +264,7 @@ async function readAtom(
sni_prefixes: fm.sniPrefixes ?? [],
trigger_signals: fm.triggerSignals ?? {},
// Estimate over the loaded unit (SKILL.md + footer), not the whole
// directory — references are loaded separately and budgeted on their own row.
// directory: references are loaded separately and budgeted on their own row.
estimated_tokens: fm.estimatedTokens ?? estimateTokens(body),
body_path: relative(rootDir, skillPath),
body,
@@ -279,7 +279,7 @@ async function readAtom(
slug: `${slug}/${r.slug}`,
title: r.descriptor || deriveTitle(r.slug),
description: r.descriptor
? `${r.descriptor} — reference for ${parent.title}`
? `${r.descriptor}: reference for ${parent.title}`
: `Reference for ${parent.title}`,
sni_prefixes: [],
trigger_signals: {},
@@ -306,7 +306,7 @@ interface ReferenceFile {
relPath: string
/** Child-id suffix derived from the path, e.g. "bfl-bfnar". */
slug: string
/** Raw file content — the child's DB-inlined body. */
/** Raw file content: the child's DB-inlined body. */
body: string
/** First ATX heading (or first line), used as a human-readable label. */
descriptor: string
@@ -367,11 +367,11 @@ function buildReferencesFooter(parentId: string, refs: ReferenceFile[]): string
'',
'## Loadable references',
'',
'The reference files named above are NOT included in this body. When a question genuinely needs that depth, load the specific one on demand with `gnubok_load_skill` — and only then:',
'The reference files named above are NOT included in this body. When a question genuinely needs that depth, load the specific one on demand with `gnubok_load_skill`, and only then:',
'',
]
for (const r of refs) {
const label = r.descriptor ? ` — ${r.descriptor}` : ''
const label = r.descriptor ? `: ${r.descriptor}` : ''
lines.push(`- \`${r.relPath}\` → \`gnubok_load_skill("${parentId}/${r.slug}")\`${label}`)
}
return '\n' + lines.join('\n') + '\n'