@stonepandastudio/cairn 0.4.1 → 0.5.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 (68) hide show
  1. package/README.md +33 -22
  2. package/bin/cairn.js +5 -0
  3. package/lib/init.js +58 -14
  4. package/lib/render/cli.js +106 -0
  5. package/lib/render/engine.js +148 -0
  6. package/lib/render/index.js +210 -0
  7. package/package.json +2 -1
  8. package/presets/EXTRACTION.md +210 -0
  9. package/presets/README.md +86 -0
  10. package/presets/angular/code-guidelines.md +197 -0
  11. package/presets/angular/slots/architect-discussion-topics.md +10 -0
  12. package/presets/angular/slots/architect-mandatory-docs.md +5 -0
  13. package/presets/angular/slots/architect-references.md +4 -0
  14. package/presets/angular/slots/implementation-reference.md +34 -0
  15. package/presets/angular/slots/key-patterns.md +11 -0
  16. package/presets/angular/slots/plan-step-ordering.md +11 -0
  17. package/presets/angular/slots/review-checklist.md +16 -0
  18. package/presets/angular/variants/i18n-external-service.md +11 -0
  19. package/presets/angular/variants/i18n-glossr.md +63 -0
  20. package/presets/core/AGENTS.md +49 -0
  21. package/presets/core/README.md +35 -0
  22. package/presets/core/WORKFLOW.md +56 -0
  23. package/presets/core/agents/architect.md +269 -0
  24. package/presets/core/agents/developer.md +145 -0
  25. package/presets/core/agents/reviewer.md +167 -0
  26. package/presets/core/commands/_stub.md +7 -0
  27. package/presets/core/workflow.json +45 -0
  28. package/presets/drizzle/code-guidelines.md +33 -0
  29. package/presets/drizzle/slots/architect-discussion-topics.md +4 -0
  30. package/presets/drizzle/slots/architect-mandatory-docs.md +4 -0
  31. package/presets/drizzle/slots/implementation-reference.md +17 -0
  32. package/presets/drizzle/slots/key-patterns.md +7 -0
  33. package/presets/drizzle/slots/review-checklist.md +10 -0
  34. package/presets/nestjs/code-guidelines.md +273 -0
  35. package/presets/nestjs/slots/architect-discussion-topics.md +4 -0
  36. package/presets/nestjs/slots/architect-mandatory-docs.md +5 -0
  37. package/presets/nestjs/slots/architect-references.md +5 -0
  38. package/presets/nestjs/slots/implementation-reference.md +45 -0
  39. package/presets/nestjs/slots/key-patterns.md +11 -0
  40. package/presets/nestjs/slots/plan-step-ordering.md +12 -0
  41. package/presets/nestjs/slots/review-checklist.md +12 -0
  42. package/presets/nestjs/variants/validation-class-validator.md +120 -0
  43. package/presets/nestjs/variants/validation-zod.md +194 -0
  44. package/presets/nextjs/code-guidelines.md +45 -0
  45. package/presets/nextjs/slots/architect-discussion-topics.md +5 -0
  46. package/presets/nextjs/slots/architect-mandatory-docs.md +3 -0
  47. package/presets/nextjs/slots/architect-references.md +6 -0
  48. package/presets/nextjs/slots/implementation-reference.md +24 -0
  49. package/presets/nextjs/slots/key-patterns.md +8 -0
  50. package/presets/nextjs/slots/plan-step-ordering.md +11 -0
  51. package/presets/nextjs/slots/review-checklist.md +11 -0
  52. package/presets/react/code-guidelines.md +46 -0
  53. package/presets/react/slots/architect-discussion-topics.md +5 -0
  54. package/presets/react/slots/architect-references.md +5 -0
  55. package/presets/react/slots/implementation-reference.md +26 -0
  56. package/presets/react/slots/key-patterns.md +8 -0
  57. package/presets/react/slots/plan-step-ordering.md +9 -0
  58. package/presets/react/slots/review-checklist.md +10 -0
  59. package/presets/tailwind/code-guidelines.md +28 -0
  60. package/presets/tailwind/slots/implementation-reference.md +8 -0
  61. package/presets/tailwind/slots/key-patterns.md +5 -0
  62. package/presets/tailwind/slots/review-checklist.md +8 -0
  63. package/presets/typeorm/code-guidelines.md +329 -0
  64. package/presets/typeorm/slots/architect-discussion-topics.md +4 -0
  65. package/presets/typeorm/slots/architect-mandatory-docs.md +3 -0
  66. package/presets/typeorm/slots/implementation-reference.md +19 -0
  67. package/presets/typeorm/slots/key-patterns.md +8 -0
  68. package/presets/typeorm/slots/review-checklist.md +8 -0
@@ -0,0 +1,210 @@
1
+ 'use strict';
2
+
3
+ // cairn render — turn the bundled `presets/` into a repo's `ai/` scaffolding.
4
+ //
5
+ // Greenfield only: it writes files that do not exist, records them in the
6
+ // manifest, and leaves anything already on disk alone (reporting it). The
7
+ // three-way merge that would let it re-render over local edits is `cairn sync`,
8
+ // which is not built yet — for a fresh repo there is nothing to merge.
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ const { render, stripHeaderComment } = require('./engine');
14
+ const { hashContent } = require('../manifest');
15
+
16
+ const PRIMARY_ONLY_SLOTS = new Set(['plan-step-ordering', 'architect-references']);
17
+
18
+ const DEFAULT_WORKFLOW = {
19
+ steps: [
20
+ { id: 'brief', fn: 'create_brief', command: 'create-brief', role: 'architect', oneLine: 'creates the task brief scaffold' },
21
+ { id: 'describe', fn: 'enrich_description', command: 'enrich-description', role: 'architect', oneLine: 'iterative discussion, then writes the enriched description' },
22
+ { id: 'plan', fn: 'plan_task', command: 'plan-task', role: 'architect', oneLine: 'produces the implementation plan and context-doc updates' },
23
+ { id: 'execute', fn: 'execute_plan', command: 'execute-plan', role: 'developer', oneLine: 'implements the plan' },
24
+ { id: 'review', fn: 'code_review', command: 'code-review', role: 'reviewer', oneLine: 'checks the result against the plan and the guidelines' },
25
+ ],
26
+ };
27
+
28
+ const ROLE_MODEL = { architect: 'Sonnet', developer: 'Haiku', reviewer: 'Sonnet' };
29
+
30
+ function cap(s) {
31
+ return s ? s[0].toUpperCase() + s.slice(1) : s;
32
+ }
33
+
34
+ function trackerContext(t = {}) {
35
+ const provider = t.provider || 'none';
36
+ const remote = provider === 'jira-server' || provider === 'youtrack';
37
+ return {
38
+ provider,
39
+ remote,
40
+ providerLabel: { 'jira-server': 'Jira', youtrack: 'YouTrack', none: 'no tracker' }[provider] || provider,
41
+ exampleKey: t.projectKey || (remote ? 'PROJ' : 'TASK'),
42
+ slug: provider === 'jira-server' ? 'jira' : provider === 'youtrack' ? 'youtrack' : 'tracker',
43
+ subtaskMarker: provider === 'youtrack' ? 'youtrack-subtask' : 'jira-subtask',
44
+ reviewStage: (t.statuses && t.statuses.done) || 'Review',
45
+ parentTerm: remote ? 'parent issue' : 'task',
46
+ childTerm: remote ? 'child issue' : 'step',
47
+ };
48
+ }
49
+
50
+ function loadWorkflow(config, presetsDir) {
51
+ if (config.workflow && Array.isArray(config.workflow.steps)) return config.workflow;
52
+ const file = path.join(presetsDir, 'core', 'workflow.json');
53
+ if (fs.existsSync(file)) {
54
+ try {
55
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
56
+ } catch {
57
+ /* fall through to the built-in default */
58
+ }
59
+ }
60
+ return DEFAULT_WORKFLOW;
61
+ }
62
+
63
+ function buildContext(config, presetsDir) {
64
+ const stack = config.stack && config.stack.length ? config.stack : ['unknown'];
65
+ const tracker = trackerContext(config.tracker);
66
+ const workflow = loadWorkflow(config, presetsDir);
67
+
68
+ const agents = config.agents || {};
69
+ const roleName = (role) =>
70
+ (agents[role] && agents[role].name) || `${cap(stack[0])}${cap(role)}Agent`;
71
+ const roleDoc = (role) => (agents[role] && agents[role].doc) || `ai/agents/${role}.md`;
72
+
73
+ workflow.steps.forEach((s) => {
74
+ s.agentName = roleName(s.role);
75
+ s.agentDoc = roleDoc(s.role);
76
+ s.model = ROLE_MODEL[s.role] || null;
77
+ s.trackerAction = tracker.remote ? s.trackerAction || 'sync — see the rows below' : 'n/a';
78
+ s.tracker = tracker.remote && s.role === 'architect' ? true : null;
79
+ });
80
+
81
+ return {
82
+ stack,
83
+ tracker,
84
+ workflow,
85
+ model: config.model || 'claude-sonnet-5',
86
+ architectAgent: roleName('architect'),
87
+ developerAgent: roleName('developer'),
88
+ reviewerAgent: roleName('reviewer'),
89
+ project: {
90
+ assistantName:
91
+ (config.vars && config.vars.assistantName) || `${cap(stack[0])} Dev Assistant`,
92
+ },
93
+ agents: ['architect', 'developer', 'reviewer'].map((r) => ({ name: roleName(r), doc: roleDoc(r) })),
94
+ ...(config.vars || {}),
95
+ };
96
+ }
97
+
98
+ function makeResolvePartial(stack, presetsDir) {
99
+ return (name) => {
100
+ const m = name.match(/^stack\/(.+)$/);
101
+ if (!m) return null;
102
+ const slot = m[1];
103
+ const entries = PRIMARY_ONLY_SLOTS.has(slot) ? stack.slice(0, 1) : stack;
104
+ const parts = [];
105
+ for (const entry of entries) {
106
+ const file = path.join(presetsDir, entry, 'slots', `${slot}.md`);
107
+ if (fs.existsSync(file)) parts.push(stripHeaderComment(fs.readFileSync(file, 'utf8')).trim());
108
+ }
109
+ if (!parts.length) return null;
110
+ // Table-row slots (every part starts with `|`) concatenate line-to-line;
111
+ // prose slots need a blank line between them.
112
+ const sep = parts.every((p) => p.startsWith('|')) ? '\n' : '\n\n';
113
+ return parts.join(sep);
114
+ };
115
+ }
116
+
117
+ function readPreset(presetsDir, rel) {
118
+ const file = path.join(presetsDir, rel);
119
+ return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
120
+ }
121
+
122
+ // Concatenate `<entry>/code-guidelines.md` for every stack entry that ships one.
123
+ function codeGuidelines(stack, presetsDir, ctx, resolvePartial) {
124
+ const blocks = [];
125
+ for (const entry of stack) {
126
+ const raw = readPreset(presetsDir, `${entry}/code-guidelines.md`);
127
+ if (raw) blocks.push(render(raw, ctx, resolvePartial).trim());
128
+ }
129
+ return blocks.length ? blocks.join('\n\n---\n\n') + '\n' : null;
130
+ }
131
+
132
+ // Returns { planned: [[rel, content, source, note?]], kept: [{ rel, reason }] }.
133
+ // `manifest` + `repoPath` drive the greenfield guard: a file cairn has not
134
+ // written is never touched, and one it wrote but a human has since edited is
135
+ // kept and reported (the re-render that respects local edits is `cairn sync`).
136
+ function planRender({ config, presetsDir, manifest = { files: {} }, repoPath = null }) {
137
+ const ctx = buildContext(config, presetsDir);
138
+ const resolvePartial = makeResolvePartial(ctx.stack, presetsDir);
139
+
140
+ const targets = [];
141
+
142
+ const stub = readPreset(presetsDir, 'core/commands/_stub.md');
143
+ if (stub) {
144
+ for (const step of ctx.workflow.steps) {
145
+ targets.push([
146
+ `.claude/commands/${step.command}.md`,
147
+ render(stub, { ...ctx, step }, resolvePartial),
148
+ 'core/commands/_stub.md',
149
+ ]);
150
+ }
151
+ }
152
+
153
+ const stepFor = (role) => ctx.workflow.steps.find((s) => s.role === role) || {};
154
+ for (const role of ['architect', 'developer', 'reviewer']) {
155
+ const raw = readPreset(presetsDir, `core/agents/${role}.md`);
156
+ if (raw) {
157
+ targets.push([
158
+ `ai/agents/${role}.md`,
159
+ render(raw, { ...ctx, step: stepFor(role) }, resolvePartial),
160
+ `core/agents/${role}.md`,
161
+ ]);
162
+ }
163
+ }
164
+
165
+ for (const [rel, src] of [
166
+ ['ai/AGENTS.md', 'core/AGENTS.md'],
167
+ ['ai/WORKFLOW.md', 'core/WORKFLOW.md'],
168
+ ]) {
169
+ const raw = readPreset(presetsDir, src);
170
+ if (raw) targets.push([rel, render(raw, ctx, resolvePartial), src]);
171
+ }
172
+
173
+ const guidelines = codeGuidelines(ctx.stack, presetsDir, ctx, resolvePartial);
174
+ if (guidelines) {
175
+ targets.push([
176
+ 'ai/infrastructure/code-guidelines.md',
177
+ guidelines,
178
+ 'presets:<stack>/code-guidelines.md',
179
+ 'seed — fork it, cairn will not re-render over your edits',
180
+ ]);
181
+ }
182
+
183
+ const planned = [];
184
+ const kept = [];
185
+ for (const [rel, content, source, note] of targets) {
186
+ const entry = manifest.files[rel];
187
+ const abs = repoPath ? path.join(repoPath, rel) : null;
188
+ const onDisk = abs && fs.existsSync(abs) ? fs.readFileSync(abs, 'utf8') : null;
189
+
190
+ if (entry && onDisk !== null && hashContent(onDisk) !== entry.hash) {
191
+ kept.push({ rel, reason: 'hand-edited since cairn wrote it' });
192
+ continue;
193
+ }
194
+ if (entry && onDisk !== null && hashContent(content) === entry.hash) continue; // current, on disk
195
+ if (entry && onDisk !== null) {
196
+ kept.push({ rel, reason: 'the template changed — `cairn sync` will merge it' });
197
+ continue;
198
+ }
199
+ if (!entry && onDisk !== null) {
200
+ kept.push({ rel, reason: 'already on disk, not written by cairn' });
201
+ continue;
202
+ }
203
+ // Not on disk: brand new, or a manifested file that was deleted — (re)write it.
204
+ planned.push([rel, content, source, entry ? 'restoring' : note]);
205
+ }
206
+
207
+ return { planned, kept, context: ctx };
208
+ }
209
+
210
+ module.exports = { planRender, buildContext, trackerContext, DEFAULT_WORKFLOW };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonepandastudio/cairn",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Shared AI workflow scaffolding for Stone Panda repos — issue tracker client and drift doctor.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -24,6 +24,7 @@
24
24
  "bin/",
25
25
  "lib/",
26
26
  "templates/",
27
+ "presets/",
27
28
  "schema.json",
28
29
  "README.md"
29
30
  ],
@@ -0,0 +1,210 @@
1
+ # Extraction record
2
+
3
+ What was pulled into `presets/`, from where, and — more importantly — what was
4
+ deliberately left in the repos. Measured 2026-08-01 by splitting each
5
+ `code-guidelines.md` on `## ` headings and running cairn's own line differ over
6
+ matching sections.
7
+
8
+ The file-level numbers in `DESIGN.md` (backend 43% same, frontend 76%) turned out
9
+ to be the wrong resolution. Per section the picture is much sharper.
10
+
11
+ ## Backend — glossr-api (828 ln) vs snap-backend (404 ln)
12
+
13
+ | Section | Same | Disposition |
14
+ |---|---|---|
15
+ | General TypeScript Guidelines | 100% | → `nestjs` |
16
+ | NestJS Module Structure | 100% | → `nestjs` |
17
+ | Services | 100% | → `nestjs` |
18
+ | Error Handling | 100% | → `nestjs` |
19
+ | Security | 100% | → `nestjs` |
20
+ | Constants | 100% | → `nestjs` |
21
+ | Enums | 100% | → `nestjs` |
22
+ | Code Quality | 100% | → `nestjs` |
23
+ | File Organization | 93% | → `nestjs` (union; glossr's `interfaces/` kept) |
24
+ | File Naming Conventions | 80% | → `nestjs`, DTO lines to `dto-naming` slot |
25
+ | TypeORM Entities | 100% | → `typeorm` |
26
+ | Database Query Decomposition Pattern | 100% | → `typeorm` |
27
+ | Mappers | 100% | → `typeorm` — **but see finding 1** |
28
+ | Database Operations | 85% | → `typeorm` (glossr's stronger wording) |
29
+ | Query Configuration | 75% | → `typeorm` (glossr is a strict superset) |
30
+ | Validation | 43% | spine → `nestjs`, rest → `validation-stack` slot |
31
+ | DTOs | 28% | spine → `nestjs`, rest → `dto-definition` slot |
32
+ | Controllers | 16% | spine → `nestjs`, rest → `controller-response` slot |
33
+ | Response Transformation | disjoint | entirely `response-transformation` slot |
34
+ | **Testing** | **7%** | **stays project-owned** |
35
+ | Interfaces, Error Codes, No Magic Strings, Pagination | glossr only | → `nestjs` (promoted, generic) |
36
+ | QueryBuilder Field Selection, Migrations | glossr only | → `typeorm` (promoted, generic) |
37
+ | Redis Key Naming | glossr only | **stays project-owned** — names a specific app prefix |
38
+
39
+ ### The backend divergence is one axis, not entropy
40
+
41
+ Controllers 16%, DTOs 28%, Validation 43% and the two mutually exclusive Response
42
+ Transformation sections are all the same disagreement:
43
+
44
+ ```
45
+ glossr-api Zod nestjs-zod, createZodDto, @ZodResponse
46
+ snap-backend class-* class-validator, class-transformer, @ResponseMapper
47
+ ```
48
+
49
+ That is a **choice with two known values**, not accumulated rot — so it is a slot
50
+ with two variants, not a reason to fork the file. The file-level 43% reading
51
+ suggested "leave it alone"; the section-level reading says most of the document is
52
+ shareable and one decision is not.
53
+
54
+ ## Frontend — glossr-frontend (231 ln) vs snap-frontend (191 ln)
55
+
56
+ | Section | Same | Disposition |
57
+ |---|---|---|
58
+ | Preamble (Angular rules) | 100% | → `angular` |
59
+ | Theming and Colors (+ 4 subsections, 94 ln) | 100% | → `angular` |
60
+ | Component Selectors | 100% | → `angular` |
61
+ | Constants | 100% | → `angular` |
62
+ | Enums | 100% | → `angular` |
63
+ | Translation Key Naming / Examples | 100% | → `angular` |
64
+ | Translations — sourcing | 48% | → `translation-source` slot |
65
+ | Change detection, Comments | snap only | → `angular` (promoted, generic) |
66
+
67
+ Cleaner than the backend: after the sourcing slot is carved out, essentially the
68
+ whole document is shared.
69
+
70
+ ## CLI — glossr-cli (203 ln)
71
+
72
+ Not extracted. It shares no section headings with either pair, and per
73
+ `DESIGN.md` it is a stack of one, so nothing can be measured against it. A
74
+ `presets/cli/` should wait for a second CLI repo or a deliberate hand-authored
75
+ decision — not be reverse-engineered from a sample of one.
76
+
77
+ ## Deliberately not extracted
78
+
79
+ - **Testing (7% same).** Two genuinely different regimes: glossr keeps all tests
80
+ under `test/` with integration helpers and factories; snap colocates `.spec.ts`
81
+ and targets >80% coverage. Extracting this would force one project onto the
82
+ other's layout.
83
+ - **Redis Key Naming.** Correct guidance, but it hardcodes `REDIS_APP_PREFIX =
84
+ 'glossr'`. Generalising it would leave a rule with no concrete value in it.
85
+ - **`code-guidelines.md` as a whole remains `fork` mode** per `DESIGN.md`. These
86
+ presets are seeds. A repo that takes one owns its copy; cairn does not plan to
87
+ rewrite these files on upgrade.
88
+
89
+ ## Findings
90
+
91
+ Two things surfaced only because the content was read section by section.
92
+
93
+ ### 1. glossr-api's Mappers section documents a decorator it does not have
94
+
95
+ The section is byte-identical to snap-backend's and says:
96
+
97
+ > Basic entity-to-DTO conversion (use `@ResponseMapper` decorator)
98
+
99
+ `grep -rl ResponseMapper glossr-api/src` returns **nothing**. glossr moved to Zod;
100
+ the Mappers section was copied and never updated. It has been instructing agents to
101
+ reach for a decorator that does not exist in that codebase.
102
+
103
+ **A 100%-identical section is not automatically a correct one.** Identical means
104
+ "nobody has diverged", which includes "nobody has noticed". The preset version now
105
+ points at the response-transformation slot instead of naming a decorator.
106
+
107
+ ### 2. Stale line-number cross-references
108
+
109
+ `glossr-api` contains *"See **Query Configuration** section below (lines 232-325)"*
110
+ — the section actually starts at line 323. Both repos contain *"Create Maps for O(1)
111
+ lookup (per guideline line 122)"*. Line numbers in a document that gets edited are
112
+ wrong the moment it is edited. Replaced with section-name references.
113
+
114
+ Also fixed in passing, present identically in both frontend repos: the typos
115
+ `cosntants` and `inrastructure`. Identical typos across two repos are the clearest
116
+ possible proof of copy-paste, and are a decent cheap signal for finding more.
117
+
118
+ ## Agents, workflow, commands — 2026-09-09
119
+
120
+ Second pass, `snap-proof/{backend,frontend}` again. The 2026-07-31 survey scored the
121
+ reviewer contract at 85–92% and treated that as a ceiling. Reading them in full, the
122
+ **structure** is closer to identical — the divergence is confined to stack-specific
123
+ rule tables and reference lists, which are exactly slot-shaped.
124
+
125
+ | artefact | disposition | measurement |
126
+ |---|---|---|
127
+ | `.claude/commands/*.md` | → `core/commands/_stub.md` | every stub byte-identical modulo `{{ step.agentName }}` + `{{ step.agentDoc }}` (frontend copies carry a stray BOM) |
128
+ | `ai/agents/{node,angular}-reviewer.md` | contract → `core/agents/reviewer.md`; Dimension-2 table → `{nestjs,angular}/slots/review-checklist.md` | `diff` shows ~15 changed lines out of 160, all in the rule table, the model-check string, and agent names |
129
+ | `ai/agents/{node,angular}-developer.md` | spine → `core/agents/developer.md`; "Implementation reference" → `{nestjs,angular}/slots/implementation-reference.md` | E.1–E.5 identical; frontend carried 3 safety notes the backend lacked (folded into core) |
130
+ | `ai/agents/{node-architect,architect-lead}.md` | Mode A/B + file-roles + Plan format → `core/agents/architect.md`; the rest → 5 small `{nestjs,angular}/slots/architect-*.md` | whole scaffold shared; slots are the mandatory-doc list, discussion topics, plan-step ordering, A.1 references |
131
+ | `ai/WORKFLOW.md` | → `core/WORKFLOW.md`, rendered from `workflow.steps[].tracker` + `tracker.*` | same document both sides; only the provider block, stage names, and sync-point actions differ — all config |
132
+ | `ai/AGENTS.md` | → `core/AGENTS.md` skeleton | same structure; rendering it removes live drift (frontend's still says "Jira" while its `.env` and commands are on YouTrack) |
133
+
134
+ ### `react` + `drizzle` seeded from charityiq-app — 2026-09-09
135
+
136
+ `charityiq-app` is a separate product (single founder, UK charity SaaS), Next.js 16
137
+ App Router + Drizzle + Supabase + Inngest, at Sprint 0 — a scaffold with documented
138
+ intent (`CLAUDE.md` hard rules, `docs/adr/0001-stack.md`) and almost no code
139
+ (everything under `lib/` is a `TODO(Sprint N)` stub).
140
+
141
+ So the seed is **conventions and structure, not code patterns** — and there is no
142
+ second repo to diff against, so what is "React-generic" vs "charityiq-specific" is
143
+ hand-judged, not measured. Same limit as `preset-cli`.
144
+
145
+ - `presets/react/` — components (folder-by-role: `ui`/`feature`/`brand`), no
146
+ `localStorage`, Tailwind v4 `@theme` tokens (no `tailwind.config.ts`), Vitest +
147
+ Testing Library + MSW + Playwright, Biome (tabs, double quotes). **Not** in it:
148
+ RSC, Server Actions, route handlers, the App Router — those are Next.js-specific
149
+ and belong in a future `presets/nextjs` that composes on top.
150
+ - `presets/drizzle/` — one schema module (`lib/db/schema.ts`), `drizzle-kit
151
+ generate` for mechanical migrations, hand-SQL only in `supabase/migrations/`, RLS
152
+ policies in the migration not the schema, server client never imported from a
153
+ `"use client"` module. Parallel to `presets/typeorm`.
154
+ - `presets/nextjs/` — the App-Router framework layer, added on top of `react`
155
+ (`--stack nextjs,react,…`, nextjs first so its fullstack plan ordering wins). Owns
156
+ the server/client boundary (`"use client"` discipline, no server-only imports
157
+ across it), Server Actions (`'use server'`, validate args, revalidate after a
158
+ write), Route Handlers (webhooks, verify caller), the App-Router file conventions,
159
+ `next/font` / `next/image` / `Metadata`, and a "the framework moves fast, read the
160
+ installed docs" note. **Not** in it: `requireTier()` + `audit.log()` on every
161
+ mutation — that is charityiq's business rule, tier-3 fork, not Next.js-generic.
162
+ - `presets/tailwind/` — styling is a third composable axis, orthogonal to the
163
+ framework (proof: Angular repos use SCSS `theme.$variable`, charityiq uses
164
+ Tailwind `@theme`, either could use either). The generic rules: tokens in a
165
+ `@theme` block not a `tailwind.config.ts`, no raw hex in markup, dark mode by
166
+ re-mapping semantic tokens. The React preset was made styling-agnostic to match.
167
+ Angular's SCSS theming stays inline in `presets/angular` for now — one system
168
+ across its two repos, not worth its own preset until a second SCSS-theming repo
169
+ appears (then: `presets/scss-theme`).
170
+
171
+ Stays charityiq-owned (tier 3, `fork`): the hard business rules — `requireTier()` +
172
+ `audit.log()` on every mutation, RAG-grounded AI through `lib/ai/client.ts`, the
173
+ audit row in the same transaction as the state change — plus `docs/adr/`, the build
174
+ guide, and the versioned-prompt registry (`docs/prompts/<name>/<version>.md`). Those
175
+ are the project's constitution, not shareable scaffolding.
176
+
177
+ ### The nestjs slots were split nestjs / typeorm — 2026-09-09
178
+
179
+ The first cut of `presets/nestjs/slots/*` (review-checklist, implementation-reference,
180
+ key-patterns, architect-mandatory-docs, architect-discussion-topics) mixed framework
181
+ rules with TypeORM rules. Split: the framework half stays in `nestjs/slots/`, the ORM
182
+ half moved to `presets/typeorm/slots/` under the same filenames. The renderer appends
183
+ one from each `stack[]` entry, so `--stack nestjs,typeorm` reconstitutes the original
184
+ and `--stack nestjs,drizzle` would pair the framework rules with a Drizzle slot set
185
+ instead. `plan-step-ordering` and `architect-references` are primary-stack-only (not
186
+ concatenated) — noted in their headers.
187
+
188
+ ### QA agents — preset-level, not extracted
189
+
190
+ Backend runs three (`qa-architect` / `qa-developer` / `qa-orchestrator`, ~630 ln
191
+ of Jest + factories + `src/feature/` layout + `yarn test:db` scripts); frontend runs
192
+ one (`angular-qa`, Playwright + `data-testid` + page objects). 33% similar at best,
193
+ confirming the 2026-07-31 read. The bodies are one project's test setup, not
194
+ shareable content — vendoring them as a "preset" would over-claim. They belong under
195
+ `{nestjs,angular}/agents/` once a second repo on the same stack shows what is
196
+ actually shared, the same "no peer" limit `preset-cli` has.
197
+
198
+ ### Rot the renderer removes
199
+
200
+ - backend `node-reviewer.md` still requires `claude-sonnet-4-6` — frontend already says `claude-sonnet-5`
201
+ - frontend `AGENTS.md` + `WORKFLOW.md` are entirely Jira; the repo is on YouTrack (`.env`, `.claude/commands/youtrack-*`)
202
+ - same review step is `code_review` in backend, `review_plan` in frontend — one verb, rendered from `workflow.steps[].command`
203
+ - backend reviewer/developer hard-code an `ai/tasks/PROOF-[task]/` path — breaks on `LOCAL-` folders; the core versions use `ai/tasks/[task]/`
204
+
205
+ ## Not wired up yet
206
+
207
+ There is no renderer. Nothing consumes `{{> slot: … }}` or `{{> stack/… }}`, and no
208
+ repo reads these files. They are the input for the agent-preset roadmap step
209
+ (`DESIGN.md`, "### Sequencing" step 4), and are already useful as the canonical
210
+ version of scaffolding that currently exists as two drifting copies.
@@ -0,0 +1,86 @@
1
+ # Presets
2
+
3
+ Extracted guideline content shared across the repos. Internal only — an in-repo
4
+ folder, deliberately package-shaped so promotion to `@stonepandastudio/cairn-preset-*` is
5
+ adding a `package.json` rather than a refactor.
6
+
7
+ Nothing here is wired into a renderer yet. These are the *inputs* the renderer will
8
+ consume once the projects layer and extension mechanisms land (see `DESIGN.md`,
9
+ "Multi-repo projects"). Until then they are seeds: readable, diffable, and already
10
+ useful as the canonical version of guidance that currently exists in two drifting
11
+ copies.
12
+
13
+ ## Layout
14
+
15
+ ```
16
+ presets/
17
+ core/ stack-agnostic — agent role contracts, command stub, WORKFLOW/AGENTS skeletons
18
+ agents/ architect.md, developer.md, reviewer.md (85–92% identical across stacks)
19
+ commands/ _stub.md (renders one file per workflow step)
20
+ nestjs/ NestJS + TypeScript framework spine, validation-stack + ORM agnostic
21
+ variants/ validation-zod.md, validation-class-validator.md
22
+ slots/ the framework side of each {{> stack/… }} point
23
+ typeorm/ ORM add-on — entities, queries, migrations, decomposition
24
+ slots/ the ORM side of the same {{> stack/… }} points (appended to nestjs)
25
+ drizzle/ ORM add-on — Drizzle schema, drizzle-kit migrations, RLS in migrations
26
+ slots/ the ORM side, for a Postgres/Supabase stack
27
+ angular/ Angular spine + SCSS theming + i18n (styling still inline here — one system, two repos)
28
+ variants/ i18n-glossr.md, i18n-external-service.md
29
+ slots/ the Angular side of the same {{> stack/… }} points
30
+ react/ React frontend spine — components, hooks, state, testing (styling-agnostic)
31
+ slots/ the React side of the same {{> stack/… }} points
32
+ nextjs/ App-Router layer over `react` — server/client boundary, actions, route handlers
33
+ slots/ the Next.js side of the same {{> stack/… }} points
34
+ tailwind/ styling add-on — @theme tokens, no tailwind.config.ts, dark mode via token re-map
35
+ slots/ the styling side of the same {{> stack/… }} points
36
+ EXTRACTION.md what was extracted from where, and what deliberately was not
37
+ ```
38
+
39
+ Compose the slots a repo needs. Rough axes — **framework** (`nestjs`, `react`,
40
+ `nextjs` on top of `react`), **ORM** (`typeorm` / `drizzle`), **styling** (`tailwind`):
41
+
42
+ ```
43
+ --stack nestjs,typeorm snap-proof / glossr backend
44
+ --stack angular snap-proof / glossr frontend (SCSS theming inline for now)
45
+ --stack nextjs,react,tailwind,drizzle charityiq — nextjs primary (owns plan ordering + fullstack refs)
46
+ --stack react,tailwind a Vite SPA against a separate API
47
+ ```
48
+
49
+ `typeorm` and `angular` are seeded from two repos; `drizzle`, `react`, `nextjs`,
50
+ `tailwind` from one (charityiq). A single-consumer preset is hand-judged, not
51
+ measured, and gets refined when a second repo on that axis appears — the
52
+ `preset-cli` "no peer" limit.
53
+
54
+ `nestjs` and `typeorm` are separate on purpose. Every backend repo composes both
55
+ today, but the split is what proves the composition model works — a future backend
56
+ on Prisma or Drizzle takes `nestjs` without `typeorm`, and a repo declares the
57
+ composition as `--stack nestjs,typeorm`.
58
+
59
+ ## Slots
60
+
61
+ A slot marks a point where a shared template knows a decision exists but not which
62
+ way a repo (or a stack) went. Two flavours:
63
+
64
+ - **`{{> slot: validation-stack }}`** in a `code-guidelines.md` — the repo picks a
65
+ file from `variants/` by name. Where a repo needs something no variant covers, it
66
+ supplies its own partial — the signal that a third variant may be warranted.
67
+ - **`{{> stack/review-checklist }}`** in a `core/` agent contract — filled from each
68
+ stack entry's `slots/` folder. Two fill modes:
69
+ - **concatenated** — the renderer appends the slot from every entry in `stack[]`,
70
+ in order. `--stack nestjs,typeorm` → `review-checklist` is the nestjs framework
71
+ rows followed by the typeorm ORM rows. Used for checklists, doc lists, reference
72
+ lists.
73
+ - **primary-only** — the slot is taken from `stack[0]` alone, because it is one
74
+ coherent thing that cannot be concatenated. `plan-step-ordering` (a numbered
75
+ sequence) and `architect-references` are primary-only; their file header says so.
76
+
77
+ Slots exist to keep the merge cheap. A template update that does not touch a slot
78
+ never conflicts with a repo's choice, which is the whole reason the extension
79
+ mechanisms are three rather than one.
80
+
81
+ ## What is deliberately absent
82
+
83
+ Sections that are genuinely per-project stay per-project — they are listed in
84
+ `EXTRACTION.md` with the measurement that justified leaving them out. Extracting
85
+ content that *should* differ is the failure mode this whole exercise is designed to
86
+ avoid: it would report every legitimate project difference as drift, forever.