@erclx/aitk 0.51.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/claude-design-extract/SKILL.md +2 -1
  3. package/claude/skills/claude-docs/SKILL.md +1 -1
  4. package/claude/skills/claude-memory-capture/SKILL.md +2 -1
  5. package/claude/skills/claude-standards-audit/SKILL.md +5 -3
  6. package/claude/skills/create-skill/SKILL.md +2 -1
  7. package/claude/skills/create-snippet/SKILL.md +3 -2
  8. package/claude/skills/create-snippet/references/snippets.md +2 -1
  9. package/claude/skills/create-standard/SKILL.md +2 -1
  10. package/claude/skills/docs-sync/SKILL.md +2 -1
  11. package/claude/skills/git-issue/SKILL.md +2 -1
  12. package/claude/skills/git-issue/references/issue.md +2 -1
  13. package/claude/skills/git-pr/SKILL.md +2 -1
  14. package/claude/skills/git-pr/references/pr.md +2 -1
  15. package/claude/skills/git-split/references/pr.md +2 -1
  16. package/claude/skills/git-stage/SKILL.md +2 -1
  17. package/docs/agents/context-audit-checks.md +3 -1
  18. package/docs/agents/context-audit.md +35 -2
  19. package/docs/agents/index.md +1 -1
  20. package/docs/agents/install-and-sync.md +6 -0
  21. package/docs/ai-workflow.md +1 -1
  22. package/docs/operating-model.md +4 -4
  23. package/docs/visual-design-workflow.md +16 -16
  24. package/docs/zshrc-aliases.md +1 -1
  25. package/governance/rules/claude/500-prose.md +4 -3
  26. package/governance/rules/claude/501-markdown.md +13 -0
  27. package/governance/stacks/base.toml +1 -1
  28. package/package.json +1 -1
  29. package/scripts/core/install-check.sh +1 -1
  30. package/scripts/core/verify.sh +83 -0
  31. package/src/claude/seeds.ts +17 -3
  32. package/src/commands/context.ts +34 -7
  33. package/src/comments/vocabulary.ts +1 -1
  34. package/src/context/audit.ts +14 -0
  35. package/src/context/gate.ts +43 -0
  36. package/src/seed-marker.ts +74 -0
  37. package/src/sync/seeds-report.ts +16 -1
  38. package/src/tooling/inject.ts +17 -1
  39. package/standards/bundled/issue.md +2 -1
  40. package/standards/bundled/pr.md +2 -1
  41. package/standards/bundled/snippets.md +2 -1
  42. package/standards/diagrams.md +4 -3
  43. package/standards/index.md +2 -1
  44. package/standards/markdown.md +72 -0
  45. package/standards/prose.md +6 -57
  46. package/standards/publish.md +3 -2
  47. package/standards/readme.md +3 -2
  48. package/standards/skill.md +2 -1
  49. package/standards/standard.md +2 -1
  50. package/standards/versioning.md +2 -1
  51. package/standards/wireframes.md +3 -2
  52. package/tooling/claude/seeds/.claude/hooks/standards-audit.sh +5 -1
@@ -1,7 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
- import { chmod } from 'node:fs/promises'
3
- import { join } from 'node:path'
2
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
3
+ import { dirname, join } from 'node:path'
4
4
  import { copyPreservingMode } from '@/copy'
5
+ import { rewritesOnInstall, stripSeedMarker } from '@/seed-marker'
5
6
 
6
7
  const SEEDS_DIR = join('tooling', 'claude', 'seeds')
7
8
  const CLAUDE_DIR = '.claude'
@@ -122,12 +123,25 @@ export function countByScope(seeds: readonly Seed[]): SeedCounts {
122
123
  /**
123
124
  * Copies each pending seed. Hooks get the executable bit the way `chmod +x`
124
125
  * granted it, added on top of whatever mode the destination already carried.
126
+ *
127
+ * A markdown seed is rewritten rather than copied, so the stub marker the seed
128
+ * gate reads does not reach the target. Every other seed copies byte for byte,
129
+ * which is what keeps the hook scripts and `settings.json` untouched.
125
130
  */
126
131
  export async function applySeeds(seeds: readonly Seed[]): Promise<string[]> {
127
132
  const applied: string[] = []
128
133
 
129
134
  for (const seed of seeds) {
130
- await copyPreservingMode(seed.src, seed.dest)
135
+ if (rewritesOnInstall(seed.src)) {
136
+ await mkdir(dirname(seed.dest), { recursive: true })
137
+ await writeFile(
138
+ seed.dest,
139
+ stripSeedMarker(await readFile(seed.src, 'utf8')),
140
+ )
141
+ } else {
142
+ await copyPreservingMode(seed.src, seed.dest)
143
+ }
144
+
131
145
  if (seed.executable) await chmod(seed.dest, 0o755)
132
146
  applied.push(seed.applyLabel)
133
147
  }
@@ -21,6 +21,7 @@ import {
21
21
  presentNames,
22
22
  resolveFolders,
23
23
  } from '@/context/folders'
24
+ import { isGating } from '@/context/gate'
24
25
  import { auditIndexes, type FolderDrift } from '@/context/index-drift'
25
26
  import {
26
27
  frameError,
@@ -34,8 +35,8 @@ import {
34
35
  plural,
35
36
  } from '@/ui'
36
37
 
37
- /** Returned when an unresolved citation is found, which is the gating check. */
38
- const EXIT_UNRESOLVED = 2
38
+ /** Returned when a gating finding is present. */
39
+ const EXIT_GATE = 2
39
40
 
40
41
  /** A name of dots alone is `.` or `..`, both of which escape the audit root. */
41
42
  const FOLDER_NAME = /^(?!\.+$)[A-Za-z0-9._-]+$/
@@ -44,6 +45,7 @@ interface AuditCommandOptions {
44
45
  readonly json?: boolean
45
46
  readonly folder?: string
46
47
  readonly citationsOnly?: boolean
48
+ readonly gate?: boolean
47
49
  }
48
50
 
49
51
  export function register(program: Command): void {
@@ -65,17 +67,23 @@ export function register(program: Command): void {
65
67
  'Comma-separated folder names, resolved under .claude/ then the project root',
66
68
  )
67
69
  .option('--citations-only', 'Run the gating citation check alone')
70
+ .option(
71
+ '--gate',
72
+ 'Also fail on a missing required section or index drift, the findings that are facts',
73
+ )
68
74
  .addHelpText(
69
75
  'after',
70
76
  [
71
77
  '',
72
78
  'Exit codes:',
73
- ' 0 the audit completed with every cited path resolving',
79
+ ' 0 the audit completed with no gating finding',
74
80
  ' 1 refused, with the reason on stderr',
75
- ' 2 a cited path did not resolve',
81
+ ' 2 a gating finding is present',
76
82
  '',
77
- 'Only unresolved citations set a failing exit code. Section, length,',
78
- 'depth, bullet, table, provenance, and index findings are advisory.',
83
+ 'An unresolved citation always gates. --gate widens the gate to the',
84
+ 'other two findings that are facts rather than judgments: a missing',
85
+ 'required section and index drift. Length, depth, bullet, table, and',
86
+ 'provenance findings are thresholds and stay advisory under both.',
79
87
  '',
80
88
  'Examples:',
81
89
  ' aitk context audit',
@@ -83,6 +91,7 @@ export function register(program: Command): void {
83
91
  ' aitk context audit --citations-only',
84
92
  ' aitk context audit --folder context,diagrams',
85
93
  ' aitk context audit --folder docs',
94
+ ' aitk context audit tooling/base/seeds --gate',
86
95
  '',
87
96
  ].join('\n'),
88
97
  )
@@ -118,6 +127,17 @@ async function runAudit(
118
127
  const root = resolve(path ?? process.cwd())
119
128
  const names = parseFolders(opts.folder)
120
129
  const gateOnly = opts.citationsOnly ?? false
130
+ const widened = opts.gate ?? false
131
+
132
+ // `--citations-only` runs the citation check alone, so the two findings
133
+ // `--gate` adds are never measured. Honouring both would exit 0 on a seed
134
+ // short a required section, which is the pass a gate exists to prevent.
135
+ if (gateOnly && widened) {
136
+ return refuse(
137
+ '--citations-only runs the citation check alone, so --gate would widen the gate to findings the run never measures. Pass one.',
138
+ gateOnly,
139
+ )
140
+ }
121
141
 
122
142
  if (typeof names === 'string') return refuse(names, gateOnly)
123
143
 
@@ -213,7 +233,14 @@ async function runAudit(
213
233
  )
214
234
  }
215
235
 
216
- return citations.unresolved.length > 0 ? EXIT_UNRESOLVED : 0
236
+ const gating = isGating({
237
+ unresolvedCitations: citations.unresolved.length,
238
+ sections,
239
+ drift,
240
+ widened,
241
+ })
242
+
243
+ return gating ? EXIT_GATE : 0
217
244
  }
218
245
 
219
246
  function refuse(message: string, gateOnly: boolean): number {
@@ -57,7 +57,7 @@ export function parseVocabulary(markdown: string): string[] | undefined {
57
57
  *
58
58
  * Reading the list out of the rule rather than hardcoding it is what keeps one
59
59
  * definition when the rule installs into a target, the same way
60
- * `.claude/hooks/standards-audit.sh` reads its bans out of `prose.md`.
60
+ * `.claude/hooks/standards-audit.sh` reads its word bans out of `prose.md`.
61
61
  */
62
62
  export async function loadVocabulary(root: string): Promise<Vocabulary> {
63
63
  for (const ruleRoot of RULE_ROOTS) {
@@ -1,6 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises'
2
2
  import { relative } from 'node:path'
3
3
  import type { AuditedFolder } from '@/context/folders'
4
+ import { isStubSeed } from '@/seed-marker'
4
5
 
5
6
  /** Checkpoints quoted from `standards/context.md`. Neither is a cap. */
6
7
  export const LENGTH_CHECKPOINT = 150
@@ -166,6 +167,12 @@ export interface EntryReport {
166
167
  * is `missingSections`, since one entry answers for its siblings.
167
168
  */
168
169
  readonly sections: readonly string[]
170
+ /**
171
+ * Whether the file declares itself a skeleton, which excludes it from the
172
+ * section check alone. Every other measure still reads it, since a stub is
173
+ * exempt from owing sections rather than from being well formed.
174
+ */
175
+ readonly stub: boolean
169
176
  }
170
177
 
171
178
  export interface SectionFinding {
@@ -544,6 +551,7 @@ export function measureEntry(
544
551
  provenance: governsContent ? provenance(lines) : [],
545
552
  heavyBullets: governsContent ? heavyBullets(lines) : [],
546
553
  sections: governsContent ? declaredSections(lines) : [],
554
+ stub: isStubSeed(source),
547
555
  }
548
556
  }
549
557
 
@@ -615,9 +623,15 @@ export function missingSections(
615
623
  for (const folder of folders) {
616
624
  if (!governsContent(folder) || folder.entries.length === 0) continue
617
625
 
626
+ // A stub owes no sections, so it is dropped before either branch rather
627
+ // than inside them. Leaving one in the split-folder aggregate would let a
628
+ // skeleton answer for the siblings that do owe the sections.
618
629
  const reports = folder.entries
619
630
  .map((path) => byRel.get(relative(root, path)))
620
631
  .filter((entry) => entry !== undefined)
632
+ .filter((entry) => !entry.stub)
633
+
634
+ if (reports.length === 0) continue
621
635
 
622
636
  if (folder.nested) {
623
637
  const missing = shortOf(reports.flatMap((entry) => entry.sections))
@@ -0,0 +1,43 @@
1
+ import type { SectionFinding } from '@/context/audit'
2
+ import type { FolderDrift } from '@/context/index-drift'
3
+
4
+ export interface GateInput {
5
+ /** Cited paths that resolved to nothing, which gate under either mode. */
6
+ readonly unresolvedCitations: number
7
+ readonly sections: readonly SectionFinding[]
8
+ readonly drift: readonly FolderDrift[]
9
+ /**
10
+ * Whether the caller asked for the widened gate. False leaves a missing
11
+ * section and a drifted index advisory, which is what the project-root stage
12
+ * runs so a judgment threshold never fails a push.
13
+ */
14
+ readonly widened: boolean
15
+ }
16
+
17
+ /** Whether any folder disagrees with its own index. */
18
+ export function hasDrift(drift: readonly FolderDrift[]): boolean {
19
+ return drift.some(
20
+ (folder) => folder.unlisted.length > 0 || folder.missing.length > 0,
21
+ )
22
+ }
23
+
24
+ /**
25
+ * Whether the audit found something that should fail the caller.
26
+ *
27
+ * An unresolved citation is a broken pointer and gates unconditionally. The two
28
+ * findings `--gate` adds are the ones answerable from the file itself: a
29
+ * required section it does not declare, and an index disagreeing with its
30
+ * folder. Length, depth, bullet, table, and provenance findings are thresholds
31
+ * a reader weighs, so they stay out under both modes.
32
+ */
33
+ export function isGating({
34
+ unresolvedCitations,
35
+ sections,
36
+ drift,
37
+ widened,
38
+ }: GateInput): boolean {
39
+ if (unresolvedCitations > 0) return true
40
+ if (!widened) return false
41
+
42
+ return sections.length > 0 || hasDrift(drift)
43
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The frontmatter field exempting a seed from the section check the seed gate
3
+ * runs.
4
+ *
5
+ * The check has a false-positive class its own comment records: a standard may
6
+ * sanction omitting a section, and no measure separates that from a file that
7
+ * forgot it. Reporting is the right response in a live project, where the
8
+ * finding is advisory. The seed gate promotes the same finding to a failing
9
+ * exit code, so the seed tree needs a way to say the omission is deliberate.
10
+ */
11
+ export const SEED_STUB_FIELD = 'stub'
12
+
13
+ /**
14
+ * Whether an install rewrites this seed rather than copying it byte for byte.
15
+ *
16
+ * This answers what the caller branches on and not whether the file holds a
17
+ * marker, which is `isStubSeed` reading content. Only markdown carries
18
+ * frontmatter, so only markdown can hold one, and the extension decides the
19
+ * copy path without opening the file. Every install path and the drift report
20
+ * ask this one question, or the report compares a marked source against a
21
+ * stripped target and reads a seed nobody touched as drifted.
22
+ */
23
+ export function rewritesOnInstall(src: string): boolean {
24
+ return src.endsWith('.md')
25
+ }
26
+
27
+ const FRONTMATTER = /^---\n([\s\S]*?)\n---(\n|$)/
28
+ const STUB_LINE = new RegExp(`^${SEED_STUB_FIELD}:[ \\t]*true[ \\t]*$`, 'm')
29
+
30
+ /**
31
+ * Whether the source declares itself a skeleton for the target to fill.
32
+ *
33
+ * Only `true` counts. A field set to anything else reads as a seed that meant
34
+ * to turn the exemption off, and treating an unparsed value as exempt would
35
+ * make a typo silence the gate.
36
+ */
37
+ export function isStubSeed(source: string): boolean {
38
+ const block = source.match(FRONTMATTER)
39
+ if (!block) return false
40
+
41
+ return STUB_LINE.test(block[1])
42
+ }
43
+
44
+ /**
45
+ * Removes the marker so it reaches no target.
46
+ *
47
+ * The field is toolkit bookkeeping about the seed tree, and a project that
48
+ * received it would carry a field its own tooling never reads. A file whose
49
+ * frontmatter holds nothing else loses the block entirely rather than keeping
50
+ * an empty one.
51
+ */
52
+ export function stripSeedMarker(source: string): string {
53
+ const block = source.match(FRONTMATTER)
54
+ if (!block) return source
55
+
56
+ const kept = block[1]
57
+ .split('\n')
58
+ .filter((line) => !STUB_LINE.test(line))
59
+ .join('\n')
60
+
61
+ // A function replacement, because a string one reads `$1` and `$&` in the
62
+ // kept fields as references to this match. A `description` naming a dollar
63
+ // amount would otherwise substitute the whole frontmatter into itself and
64
+ // carry the marker along with it.
65
+ if (kept.trim() !== '') {
66
+ const replacement = `---\n${kept}\n---${block[2] ?? ''}`
67
+ return source.replace(FRONTMATTER, () => replacement)
68
+ }
69
+
70
+ // Dropping the block takes the blank line that separated it from the body
71
+ // with it. Leaving that behind opens the installed file on whitespace, which
72
+ // is a diff every target would carry against its own formatter.
73
+ return source.slice(block[0].length).replace(/^\n+/, '')
74
+ }
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from 'node:fs'
2
2
  import { relative } from 'node:path'
3
3
  import { planSeeds, type Seed } from '@/claude/seeds'
4
+ import { rewritesOnInstall, stripSeedMarker } from '@/seed-marker'
4
5
  import { findInstalledOrigin, readHistoryIndex } from '@/sync/history'
5
6
 
6
7
  /**
@@ -106,6 +107,20 @@ function attribute(
106
107
  return false
107
108
  }
108
109
 
110
+ /**
111
+ * Compares the target against what an install would write, not against the seed
112
+ * source. The two differ for a markdown seed carrying the stub marker, which the
113
+ * install strips, so comparing sources would report a file the target never
114
+ * touched as drifted for as long as the marker stays set.
115
+ */
109
116
  function sameContent(source: string, dest: string): boolean {
110
- return readFileSync(source).equals(readFileSync(dest))
117
+ const installed = readFileSync(dest)
118
+
119
+ if (!rewritesOnInstall(source)) {
120
+ return readFileSync(source).equals(installed)
121
+ }
122
+
123
+ return (
124
+ stripSeedMarker(readFileSync(source, 'utf8')) === installed.toString('utf8')
125
+ )
111
126
  }
@@ -10,6 +10,7 @@ import {
10
10
  import { dirname, join } from 'node:path'
11
11
  import { $ } from 'bun'
12
12
  import { copyPreservingMode } from '@/copy'
13
+ import { rewritesOnInstall, stripSeedMarker } from '@/seed-marker'
13
14
  import { mergeSections, pruneSections } from '@/tooling/gitignore'
14
15
  import { ancestorsFirst, listFiles, type Manifest } from '@/tooling/manifest'
15
16
  import {
@@ -44,6 +45,21 @@ export async function injectConfigs(
44
45
  return applied
45
46
  }
46
47
 
48
+ /**
49
+ * Writes a seed to a target that does not have it, dropping the stub marker on
50
+ * the way. The marker is toolkit bookkeeping read by the seed gate, so a target
51
+ * receiving it would carry a field its own tooling never reads. Only markdown
52
+ * carries frontmatter, and every other seed copies byte for byte.
53
+ */
54
+ async function writeSeed(src: string, dest: string): Promise<void> {
55
+ if (!rewritesOnInstall(src)) {
56
+ await copyFile(src, dest)
57
+ return
58
+ }
59
+
60
+ await writeFile(dest, stripSeedMarker(await readFile(src, 'utf8')))
61
+ }
62
+
47
63
  /**
48
64
  * Copies a seed when the target lacks it. When the target already has one and
49
65
  * the seed is a `.txt` word list, missing lines are appended and the file is
@@ -53,7 +69,7 @@ async function mergeSeedFile(src: string, dest: string): Promise<void> {
53
69
  await mkdir(dirname(dest), { recursive: true })
54
70
 
55
71
  if (!existsSync(dest)) {
56
- await copyFile(src, dest)
72
+ await writeSeed(src, dest)
57
73
  return
58
74
  }
59
75
 
@@ -14,7 +14,8 @@ Does not govern:
14
14
 
15
15
  - Pull request title and body: `pr.md`
16
16
  - Whether a phase label may appear in issue text: `versioning.md`
17
- - Voice, punctuation, and banned words in issue prose: `prose.md`
17
+ - Voice and banned words in issue prose: `prose.md`
18
+ - Punctuation and formatting in issue prose: `markdown.md`
18
19
 
19
20
  ## Title
20
21
 
@@ -15,7 +15,8 @@ Does not govern:
15
15
  - Commit subject format, which shares the title form: `commit.md`
16
16
  - Branch naming: `branch.md`
17
17
  - Whether a phase label or a semver tag may appear in a title or body: `versioning.md`
18
- - Voice, punctuation, and banned words in pull request prose: `prose.md`
18
+ - Voice and banned words in pull request prose: `prose.md`
19
+ - Punctuation and formatting in pull request prose: `markdown.md`
19
20
 
20
21
  ## Title
21
22
 
@@ -13,7 +13,8 @@ Governs a snippet file: what one is for, whether a prompt qualifies as one, how
13
13
  Does not govern:
14
14
 
15
15
  - Skill folders, which carry frontmatter, references, and scripts a snippet has none of: `skill.md`
16
- - Voice, punctuation, and formatting in snippet prose: `prose.md`
16
+ - Voice and word choice in snippet prose: `prose.md`
17
+ - Punctuation and formatting in snippet prose: `markdown.md`
17
18
 
18
19
  ## What a snippet is
19
20
 
@@ -15,7 +15,8 @@ Governs per-kind diagram entries under `.claude/diagrams/`: which question each
15
15
 
16
16
  Does not govern:
17
17
 
18
- - Punctuation, formatting, and language in explanation prose and node labels: `prose.md`, whose bans the yield does not lift
18
+ - Language and word choice in explanation prose and node labels: `prose.md`, whose bans the yield does not lift
19
+ - Punctuation and formatting in explanation prose: `markdown.md`, which the yield does not reach
19
20
  - The mechanism behind any component a diagram draws: `context.md`
20
21
  - UI layout, on-screen copy, and interaction intent: `wireframes.md`
21
22
  - The decision record a components diagram is drawn from: `architecture.md`
@@ -93,7 +94,7 @@ A second entry for one kind takes a suffixed name (`request-flow-admin.md`) and
93
94
  - Do not duplicate prose across entries. An entry that restates its neighbor has taken the neighbor's job.
94
95
  - The audience is mixed, so vocabulary runs as a gradient across the set. `System context` assumes no knowledge of the repository. `Deployment` may assume the reader has read the others.
95
96
 
96
- This section states the voice for the surface, which is what claims the yield `prose.md` grants to a surface whose own standard sets it. Explanation prose is pedagogical here and the default developer-facing voice does not apply. The yield covers voice alone. Punctuation, formatting, and language bans stay in force.
97
+ This section states the voice for the surface, which is what claims the yield `prose.md` grants to a surface whose own standard sets it. Explanation prose is pedagogical here and the default developer-facing voice does not apply. The yield covers voice alone. The language bans in `prose.md` stay in force, as do the punctuation and formatting rules in `markdown.md`, which grants no yield at all.
97
98
 
98
99
  ## Verification
99
100
 
@@ -120,7 +121,7 @@ Reference the context entry by path when a reader needs the mechanism. The diagr
120
121
  - `System context` has no named source signal beyond `.claude/REQUIREMENTS.md`, so nothing tells a session it went stale. Re-read it when the boundary or the set of external dependencies moves.
121
122
  - The `claude-docs` sweep watches two things and writes frontmatter only. It appends `stale` when a path an entry cites leaves the tree, and it stubs a kind when a diff adds the source signal that kind is drawn from. Diagram bodies and explanation paragraphs are off limits to it, because a change that removes a module does not carry the new correct shape of the picture.
122
123
  - That watch samples thinly. It sees the one or two paths an entry happened to cite and nothing else, so a change elsewhere leaves the entry looking current. `verified` is what covers the gap, and an entry whose date sits far behind the branch is due a read whether or not anything flagged it.
123
- - The explanation paragraphs around a Mermaid block are prose and follow `prose.md`. The fenced block itself is not, which is why a check scoped to prose is the wrong thing to rely on for what sits inside it.
124
+ - The explanation paragraphs around a Mermaid block are prose and follow `prose.md` and `markdown.md`. The fenced block itself is not, which is why a check scoped to prose is the wrong thing to rely on for what sits inside it.
124
125
  - The punctuation bans still apply to node and subgraph labels, and nothing checks them there. An em dash in a label passes every gate the repository has, so read the labels before shipping the entry.
125
126
 
126
127
  ## Template
@@ -11,7 +11,8 @@ Reference docs for consistent authoring across the toolkit and target projects.
11
11
  - [Context entry reference](context.md): Shape and content rules for .claude/context/<domain>.md entries
12
12
  - [Design reference](design.md): Shape and content rules for .claude/DESIGN.md
13
13
  - [Diagram reference](diagrams.md): Shape and content rules for .claude/diagrams/<kind>.md files
14
- - [Prose reference](prose.md): Voice, structure, formatting, and language rules for reference markdown
14
+ - [Markdown reference](markdown.md): Headings, paragraph and list structure, code spans, punctuation, emphasis, and file references
15
+ - [Prose reference](prose.md): Voice, language, and frontmatter wording for reference markdown
15
16
  - [Publish reference](publish.md): Scan run against finished text leaving through a channel no automated check covers
16
17
  - [Readme reference](readme.md): Readme voice, structure, and content conventions
17
18
  - [Requirements reference](requirements.md): Shape and content rules for .claude/REQUIREMENTS.md
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: Markdown reference
3
+ description: Headings, paragraph and list structure, code spans, punctuation, emphasis, and file references
4
+ ---
5
+
6
+ # Markdown reference
7
+
8
+ Applies to markdown reference docs, READMEs, and inline documentation in repos. These are mechanics rather than voice, so no surface yields them. A surface stating its own voice claims that yield from `prose.md` and formats by this file regardless.
9
+
10
+ ## Scope
11
+
12
+ Governs the markdown mechanics of every markdown file: headings, paragraph and list structure, code spans and fences, punctuation, emphasis, and file references. It is an attribute standard rather than a document-type one, so it applies over documents whose shape another standard sets, and it carries no template because mechanics are written across every document and have none of their own to shape.
13
+
14
+ Does not govern:
15
+
16
+ - Voice, word choice, and the wording of a `title` or `description`: `prose.md`
17
+ - What sections a document has, or what belongs in each: the standard for that document type
18
+ - The text inside a fenced block, which follows the conventions of its own language rather than these
19
+ - The scan that applies the punctuation bans to finished text on its way out: `publish.md`
20
+
21
+ ## Headings
22
+
23
+ - H1 for document title, H2 for main sections, H3 for subsections
24
+ - Use sentence case for all headings (H1, H2, H3)
25
+ - Proper nouns and product names retain their casing in headings
26
+
27
+ ## Paragraphs and lists
28
+
29
+ - Use prose by default. Reserve bullets for discrete, unrelated items.
30
+ - Keep paragraphs to four sentences or fewer. Split longer blocks at the next logical boundary.
31
+ - Keep bullets tight. If a bullet needs more than a couple of sentences, it belongs in prose.
32
+ - Use dashes (`-`) not asterisks (`*`) for bulleted lists
33
+ - Do not end single-sentence or fragment bullets with a period. Use periods when a bullet has two or more sentences.
34
+ - For key path lists, use colon format: `- \`src/\`: description`. Never use an em dash.
35
+ - Do not introduce a list with a "Here are the X:" or "The following X:" lead-in
36
+
37
+ ## Code and identifiers
38
+
39
+ - Wrap commands, API names, file paths, and code identifiers in backticks
40
+ - Use a language identifier on all fenced code blocks (`markdown`, `typescript`, `plaintext`). Never use a bare ` ``` `
41
+ - In ASCII tree diagrams, use `←` for inline annotations. Never use `#`.
42
+
43
+ ## Punctuation
44
+
45
+ - Do not use em dashes (`—`) or semicolons (`;`). Rewrite or restructure the sentence to avoid them.
46
+ - Do not use parenthetical asides in prose (`the config (which is optional) controls...`). Split into its own sentence or drop it. Parentheses in rule definitions for grouping examples are fine.
47
+
48
+ The closed-set word bans sit in `prose.md` under `## Language` rather than here, because a banned word is a word-choice rule and these are character rules. A surface applying both reads both files.
49
+
50
+ ## Emphasis and dividers
51
+
52
+ - Do not over-format with excessive bold, italic, or header usage
53
+ - Do not use horizontal rules or dividers (`---`) in body content. The `---` delimiters of a YAML frontmatter block at the top of the file are allowed.
54
+
55
+ ## Links and file references
56
+
57
+ - Use descriptive anchor text for links. Avoid `click here` or `read more`.
58
+ - Wrap file references in backticks by default. Use a labeled markdown link (`[label](path)`) only on rendered-for-human surfaces (`README.md`, `docs/`) and in an index file, whose rows exist to be followed. Never repeat the path verbatim as the label.
59
+
60
+ ## Examples
61
+
62
+ Each pair shows a banned pattern and its fix.
63
+
64
+ ```markdown
65
+ Bad: See [.claude/context/retrieval.md](.claude/context/retrieval.md) for the retrieval flow.
66
+ Good: See `.claude/context/retrieval.md` for the retrieval flow.
67
+ ```
68
+
69
+ ```markdown
70
+ Bad: Read [docs/development.md](docs/development.md) before contributing.
71
+ Good: Read the [development guide](docs/development.md) before contributing.
72
+ ```
@@ -1,18 +1,19 @@
1
1
  ---
2
2
  title: Prose reference
3
- description: Voice, structure, formatting, and language rules for reference markdown
3
+ description: Voice, language, and frontmatter wording for reference markdown
4
4
  ---
5
5
 
6
6
  # Prose reference
7
7
 
8
- Applies to markdown reference docs, READMEs, and inline documentation in repos. It is the default voice for `.md` files and yields to any surface with its own voice, such as blogs, emails, changelogs, or commit messages. It also yields wherever another standard states the voice for the surface it governs, which is how a surface claims the exemption without this file having to name it. The yield covers voice alone. Punctuation, formatting, and language rules below stay in force on every surface, including the surfaces no automated check reaches.
8
+ Applies to markdown reference docs, READMEs, and inline documentation in repos. It is the default voice for `.md` files and yields to any surface with its own voice, such as blogs, emails, changelogs, or commit messages. It also yields wherever another standard states the voice for the surface it governs, which is how a surface claims the exemption without this file having to name it. The yield covers voice alone. The language rules below stay in force on every surface, including the surfaces no automated check reaches, as do the mechanics in `markdown.md`.
9
9
 
10
10
  ## Scope
11
11
 
12
- Governs voice, punctuation, formatting, and word choice wherever prose is written. It is an attribute standard rather than a document-type one, so it applies over documents whose shape another standard sets, and yields on voice alone where that standard states one.
12
+ Governs voice, word choice, and frontmatter wording wherever prose is written. It is an attribute standard rather than a document-type one, so it applies over documents whose shape another standard sets, yields on voice alone where that standard states one, and carries no template because voice is written across every document and has none of its own to shape.
13
13
 
14
14
  Does not govern:
15
15
 
16
+ - Headings, list and paragraph structure, code spans, punctuation, emphasis, and file references: `markdown.md`
16
17
  - What sections a document has, or what belongs in each: the standard for that document type
17
18
  - Which frontmatter fields a document carries, which is that standard's own subject. This file governs the wording of a `title` and a `description` and nothing else about them.
18
19
  - Phase-label and semver discipline: `versioning.md`
@@ -28,52 +29,8 @@ Does not govern:
28
29
  - Use substantive connectives where flow matters, but never add words solely for rhythm. Terse reference prose needs no padding.
29
30
  - Be direct on established facts. Hedge on genuinely uncertain claims.
30
31
  - Assume developer-level technical knowledge. Skip hand-holding explanations.
31
- - Keep paragraphs to four sentences or fewer. Split longer blocks at the next logical boundary.
32
-
33
- ## Structure
34
-
35
- ### Headings
36
-
37
- - H1 for document title, H2 for main sections, H3 for subsections
38
- - Use sentence case for all headings (H1, H2, H3)
39
- - Proper nouns and product names retain their casing in headings
40
-
41
- ### Paragraphs and lists
42
-
43
32
  - Front-load key information in each paragraph. Keep paragraphs concise and scannable.
44
33
  - Every sentence must provide new information. Cut redundant context.
45
- - Use prose by default. Reserve bullets for discrete, unrelated items.
46
- - Keep bullets tight. If a bullet needs more than a couple of sentences, it belongs in prose.
47
-
48
- ## Formatting
49
-
50
- ### Lists
51
-
52
- - Use dashes (`-`) not asterisks (`*`) for bulleted lists
53
- - Do not end single-sentence or fragment bullets with a period. Use periods when a bullet has two or more sentences.
54
- - For key path lists, use colon format: `- \`src/\`: description`. Never use an em dash.
55
- - Do not introduce a list with a "Here are the X:" or "The following X:" lead-in
56
-
57
- ### Code and identifiers
58
-
59
- - Wrap commands, API names, file paths, and code identifiers in backticks
60
- - Use a language identifier on all fenced code blocks (`markdown`, `typescript`, `plaintext`). Never use a bare ` ``` `
61
- - In ASCII tree diagrams, use `←` for inline annotations. Never use `#`.
62
-
63
- ### Punctuation
64
-
65
- - Do not use em dashes (`—`) or semicolons (`;`). Rewrite or restructure the sentence to avoid them.
66
- - Do not use parenthetical asides in prose (`the config (which is optional) controls...`). Split into its own sentence or drop it. Parentheses in rule definitions for grouping examples are fine.
67
-
68
- ### Emphasis and dividers
69
-
70
- - Do not over-format with excessive bold, italic, or header usage
71
- - Do not use horizontal rules or dividers (`---`) in body content. The `---` delimiters of a YAML frontmatter block at the top of the file are allowed.
72
-
73
- ### Links and file references
74
-
75
- - Use descriptive anchor text for links. Avoid `click here` or `read more`.
76
- - Wrap file references in backticks by default. Use a labeled markdown link (`[label](path)`) only on rendered-for-human surfaces (`README.md`, `docs/`) for cross-folder navigation. Never repeat the path verbatim as the label.
77
34
 
78
35
  ## Language
79
36
 
@@ -86,6 +43,8 @@ Does not govern:
86
43
  - Do not address the reader as a participant (`Let's`, `Here's`, `Here are`). State the content directly.
87
44
  - Commit to a position. Do not hedge in clusters (`It might be worth considering`) or use false balance (`While X is true, Y is also important`). Recommend, or state the tradeoff.
88
45
 
46
+ The character bans sit in `markdown.md` under `## Punctuation` rather than here, because an em dash and a semicolon are typography and these are word choice. A surface applying both reads both files.
47
+
89
48
  ## Frontmatter descriptions
90
49
 
91
50
  When frontmatter carries a short `title` or `description` used for catalog display:
@@ -122,13 +81,3 @@ Good: Use the `retry` option for failed webhooks. Set `maxRetries` to 3.
122
81
  Bad: It might be worth considering whether to enable caching.
123
82
  Good: Enable caching for read-heavy endpoints. Skip it for writes.
124
83
  ```
125
-
126
- ```markdown
127
- Bad: See [.claude/context/retrieval.md](.claude/context/retrieval.md) for the retrieval flow.
128
- Good: See `.claude/context/retrieval.md` for the retrieval flow.
129
- ```
130
-
131
- ```markdown
132
- Bad: Read [docs/development.md](docs/development.md) before contributing.
133
- Good: Read the [development guide](docs/development.md) before contributing.
134
- ```
@@ -11,7 +11,8 @@ Governs the scan an author runs against finished text on its way out, and the re
11
11
 
12
12
  Does not govern:
13
13
 
14
- - Which characters are banned, and the voice and formatting the text is written in: `prose.md`
14
+ - Which characters are banned, and the formatting the text carries: `markdown.md`
15
+ - The voice and word choice the text is written in: `prose.md`
15
16
  - The phase-label rule and the table of surfaces each namespace may appear on: `versioning.md`
16
17
  - Which gap a given surface has, and what it publishes through, which that surface names for itself
17
18
 
@@ -23,7 +24,7 @@ Run the scan as an explicit step against the finished text. Having read the unde
23
24
 
24
25
  ## Banned characters
25
26
 
26
- `prose.md` holds the character bans. Read it at scan time rather than working them from memory, then scan the drafted text and rewrite each occurrence.
27
+ `markdown.md` holds the character bans and `prose.md` holds the banned words. Read both at scan time rather than working them from memory, then scan the drafted text and rewrite each occurrence.
27
28
 
28
29
  Restructure the sentence rather than substituting the character. A semicolon swapped for a period leaves both clauses in the order the semicolon chose, which is the shape the ban exists to remove.
29
30