@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.
- package/README.md +33 -22
- package/bin/cairn.js +5 -0
- package/lib/init.js +58 -14
- package/lib/render/cli.js +106 -0
- package/lib/render/engine.js +148 -0
- package/lib/render/index.js +210 -0
- package/package.json +2 -1
- package/presets/EXTRACTION.md +210 -0
- package/presets/README.md +86 -0
- package/presets/angular/code-guidelines.md +197 -0
- package/presets/angular/slots/architect-discussion-topics.md +10 -0
- package/presets/angular/slots/architect-mandatory-docs.md +5 -0
- package/presets/angular/slots/architect-references.md +4 -0
- package/presets/angular/slots/implementation-reference.md +34 -0
- package/presets/angular/slots/key-patterns.md +11 -0
- package/presets/angular/slots/plan-step-ordering.md +11 -0
- package/presets/angular/slots/review-checklist.md +16 -0
- package/presets/angular/variants/i18n-external-service.md +11 -0
- package/presets/angular/variants/i18n-glossr.md +63 -0
- package/presets/core/AGENTS.md +49 -0
- package/presets/core/README.md +35 -0
- package/presets/core/WORKFLOW.md +56 -0
- package/presets/core/agents/architect.md +269 -0
- package/presets/core/agents/developer.md +145 -0
- package/presets/core/agents/reviewer.md +167 -0
- package/presets/core/commands/_stub.md +7 -0
- package/presets/core/workflow.json +45 -0
- package/presets/drizzle/code-guidelines.md +33 -0
- package/presets/drizzle/slots/architect-discussion-topics.md +4 -0
- package/presets/drizzle/slots/architect-mandatory-docs.md +4 -0
- package/presets/drizzle/slots/implementation-reference.md +17 -0
- package/presets/drizzle/slots/key-patterns.md +7 -0
- package/presets/drizzle/slots/review-checklist.md +10 -0
- package/presets/nestjs/code-guidelines.md +273 -0
- package/presets/nestjs/slots/architect-discussion-topics.md +4 -0
- package/presets/nestjs/slots/architect-mandatory-docs.md +5 -0
- package/presets/nestjs/slots/architect-references.md +5 -0
- package/presets/nestjs/slots/implementation-reference.md +45 -0
- package/presets/nestjs/slots/key-patterns.md +11 -0
- package/presets/nestjs/slots/plan-step-ordering.md +12 -0
- package/presets/nestjs/slots/review-checklist.md +12 -0
- package/presets/nestjs/variants/validation-class-validator.md +120 -0
- package/presets/nestjs/variants/validation-zod.md +194 -0
- package/presets/nextjs/code-guidelines.md +45 -0
- package/presets/nextjs/slots/architect-discussion-topics.md +5 -0
- package/presets/nextjs/slots/architect-mandatory-docs.md +3 -0
- package/presets/nextjs/slots/architect-references.md +6 -0
- package/presets/nextjs/slots/implementation-reference.md +24 -0
- package/presets/nextjs/slots/key-patterns.md +8 -0
- package/presets/nextjs/slots/plan-step-ordering.md +11 -0
- package/presets/nextjs/slots/review-checklist.md +11 -0
- package/presets/react/code-guidelines.md +46 -0
- package/presets/react/slots/architect-discussion-topics.md +5 -0
- package/presets/react/slots/architect-references.md +5 -0
- package/presets/react/slots/implementation-reference.md +26 -0
- package/presets/react/slots/key-patterns.md +8 -0
- package/presets/react/slots/plan-step-ordering.md +9 -0
- package/presets/react/slots/review-checklist.md +10 -0
- package/presets/tailwind/code-guidelines.md +28 -0
- package/presets/tailwind/slots/implementation-reference.md +8 -0
- package/presets/tailwind/slots/key-patterns.md +5 -0
- package/presets/tailwind/slots/review-checklist.md +8 -0
- package/presets/typeorm/code-guidelines.md +329 -0
- package/presets/typeorm/slots/architect-discussion-topics.md +4 -0
- package/presets/typeorm/slots/architect-mandatory-docs.md +3 -0
- package/presets/typeorm/slots/implementation-reference.md +19 -0
- package/presets/typeorm/slots/key-patterns.md +8 -0
- 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.
|
|
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.
|