create-quiver 0.5.0 → 0.7.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/.github/workflows/ci.yml +7 -30
- package/AGENTS.md.template +41 -0
- package/CHANGELOG.md +5 -0
- package/README.md +53 -9
- package/README_FOR_AI.md +36 -14
- package/ROADMAP.md +78 -0
- package/docs/AI_CONTEXT.md.template +19 -25
- package/docs/AI_ONBOARDING_PROMPT.md.template +12 -0
- package/docs/CONTEXTO.md.template +4 -17
- package/docs/DECISIONS.md.template +18 -0
- package/docs/DEEP.md.template +34 -0
- package/docs/DOCUMENTATION_GUIDE.md.template +9 -7
- package/docs/GITFLOW_PR_GUIDE.md.template +7 -0
- package/docs/INDEX.md.template +9 -0
- package/docs/QUICK.md.template +27 -0
- package/docs/STANDARD.md.template +49 -0
- package/docs/STATUS.md.template +2 -2
- package/docs/SUPPORT_MATRIX.md.template +7 -4
- package/docs/TESTING_GUIDE_FOR_AI.md.template +4 -3
- package/docs/TROUBLESHOOTING.md.template +14 -0
- package/docs/WORKFLOW.md.template +19 -5
- package/package.json +3 -1
- package/package.template.json +13 -1
- package/scripts/cleanup-slice.sh +2 -172
- package/scripts/init-docs.sh +246 -44
- package/scripts/package-quiver.sh +5 -0
- package/scripts/start-slice.sh +3 -425
- package/specs/[project-name]/EVIDENCE_REPORT.md.template +3 -1
- package/specs/[project-name]/slices/slice-template/slice.json +2 -2
- package/specs/quiver-v11-existing-project-migration/EVIDENCE_REPORT.md +38 -0
- package/specs/quiver-v11-existing-project-migration/SPEC.md +59 -0
- package/specs/quiver-v11-existing-project-migration/STATUS.md +26 -0
- package/specs/quiver-v11-existing-project-migration/slices/slice-01-non-destructive-migrate-command/slice.json +73 -0
- package/specs/quiver-v11-existing-project-migration/slices/slice-02-version-metadata-doctor-upgrade-checks/slice.json +71 -0
- package/specs/quiver-v11-existing-project-migration/slices/slice-03-upgrade-docs-legacy-project-smokes/slice.json +78 -0
- package/specs/quiver-v12-cross-platform-native-runtime/EVIDENCE_REPORT.md +30 -0
- package/specs/quiver-v12-cross-platform-native-runtime/SPEC.md +86 -0
- package/specs/quiver-v12-cross-platform-native-runtime/STATUS.md +29 -0
- package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-01-cross-platform-support-contract/slice.json +69 -0
- package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-02-node-init-docs-runtime/slice.json +76 -0
- package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-03-node-migrate-analyze-doctor-flow/slice.json +74 -0
- package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-04-node-slice-lifecycle-commands/slice.json +81 -0
- package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-05-generated-project-scripts-and-migration/slice.json +78 -0
- package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-06-cross-platform-ci-release-readiness/slice.json +74 -0
- package/specs/quiver-v13-token-efficient-ai-context/EVIDENCE_REPORT.md +28 -0
- package/specs/quiver-v13-token-efficient-ai-context/SPEC.md +68 -0
- package/specs/quiver-v13-token-efficient-ai-context/STATUS.md +26 -0
- package/specs/quiver-v13-token-efficient-ai-context/slices/slice-01-token-efficient-ai-modes-guidance/slice.json +65 -0
- package/specs/quiver-v13-token-efficient-ai-context/slices/slice-02-decision-log-context-checkpoint/slice.json +64 -0
- package/specs/quiver-v13-token-efficient-ai-context/slices/slice-03-project-map-reading-order/slice.json +66 -0
- package/specs/quiver-v14-tiered-context-pack/EVIDENCE_REPORT.md +42 -0
- package/specs/quiver-v14-tiered-context-pack/SPEC.md +116 -0
- package/specs/quiver-v14-tiered-context-pack/STATUS.md +35 -0
- package/specs/quiver-v14-tiered-context-pack/slices/slice-01-tiered-context-pack/slice.json +77 -0
- package/specs/quiver-v14-tiered-context-pack/slices/slice-02-agents-md-router/slice.json +74 -0
- package/specs/quiver-v14-tiered-context-pack/slices/slice-03-active-slice-lifecycle/slice.json +74 -0
- package/specs/quiver-v14-tiered-context-pack/slices/slice-04-dedup-frontmatter/slice.json +83 -0
- package/specs/quiver-v14-tiered-context-pack/slices/slice-05-doctor-smokes-tiered-pack/slice.json +84 -0
- package/src/create-quiver/index.js +360 -40
- package/src/create-quiver/lib/analyze.js +9 -0
- package/src/create-quiver/lib/doctor.js +212 -0
- package/src/create-quiver/lib/git.js +154 -0
- package/src/create-quiver/lib/init-docs.js +616 -0
- package/src/create-quiver/lib/lifecycle.js +478 -0
- package/src/create-quiver/lib/paths.js +19 -0
- package/src/create-quiver/lib/readiness.js +300 -0
- package/src/create-quiver/lib/scope.js +5 -0
- package/src/create-quiver/lib/slice.js +194 -0
- package/src/create-quiver/lib/state.js +89 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { writeState } = require('./state');
|
|
4
|
+
|
|
5
|
+
function ensureDir(dirPath) {
|
|
6
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function toProjectSlug(projectName) {
|
|
10
|
+
return projectName
|
|
11
|
+
.normalize('NFKD')
|
|
12
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
15
|
+
.replace(/^-+|-+$/g, '') || 'quiver-project';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function detectPackageManager(projectRoot) {
|
|
19
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
20
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
21
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
22
|
+
if (typeof packageJson.packageManager === 'string' && packageJson.packageManager.length > 0) {
|
|
23
|
+
return packageJson.packageManager.split('@')[0];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const candidates = [
|
|
28
|
+
['pnpm', 'pnpm-lock.yaml'],
|
|
29
|
+
['yarn', 'yarn.lock'],
|
|
30
|
+
['bun', 'bun.lockb'],
|
|
31
|
+
['bun', 'bun.lock'],
|
|
32
|
+
['npm', 'package-lock.json'],
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
for (const [manager, lockFile] of candidates) {
|
|
36
|
+
if (fs.existsSync(path.join(projectRoot, lockFile))) {
|
|
37
|
+
return manager;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return 'npm';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderTemplate(text, replacements) {
|
|
45
|
+
return text
|
|
46
|
+
.replace(/{{PROJECT_NAME}}/g, replacements.projectName)
|
|
47
|
+
.replace(/{{PROJECT_SLUG}}/g, replacements.projectSlug)
|
|
48
|
+
.replace(/\[project\]/g, replacements.projectSlug)
|
|
49
|
+
.replace(/\[project-name\]/g, replacements.projectSlug)
|
|
50
|
+
.replace(/\[project-slug\]/g, replacements.projectSlug)
|
|
51
|
+
.replace(/{{FECHA}}/g, replacements.currentDate)
|
|
52
|
+
.replace(/{{FECHA_PROXIMA}}/g, replacements.datePlus7)
|
|
53
|
+
.replace(/{{FECHA_PROXIMA_MES}}/g, replacements.datePlus30)
|
|
54
|
+
.replace(/{{FECHA_LAUNCH}}/g, replacements.datePlus35)
|
|
55
|
+
.replace(/{{ESTADO}}/g, 'En planificación')
|
|
56
|
+
.replace(/{{FASE}}/g, 'Fase 1')
|
|
57
|
+
.replace(/{{X}}%/g, '0%')
|
|
58
|
+
.replace(/{{PACKAGE_MANAGER}}/g, replacements.packageManager || 'npm')
|
|
59
|
+
.replace(/{{STACK_SUMMARY}}/g, replacements.stackSummary || 'unknown until analyze')
|
|
60
|
+
.replace(/{{PRIMARY_INSTALL}}/g, replacements.primaryInstall || 'npm install')
|
|
61
|
+
.replace(/{{PRIMARY_DEV}}/g, replacements.primaryDev || 'not defined')
|
|
62
|
+
.replace(/{{PRIMARY_TEST}}/g, replacements.primaryTest || 'not defined')
|
|
63
|
+
.replace(/{{ANALYZE_COMMAND}}/g, replacements.analyzeCommand || 'npx create-quiver analyze')
|
|
64
|
+
.replace(/{{DOCTOR_COMMAND}}/g, replacements.doctorCommand || 'npx create-quiver doctor')
|
|
65
|
+
.replace(/{{START_SLICE_COMMAND}}/g, replacements.startSliceCommand || 'npx create-quiver start-slice <slice.json>')
|
|
66
|
+
.replace(/{{CHECK_SLICE_COMMAND}}/g, replacements.checkSliceCommand || 'npx create-quiver check-slice <slice.json>')
|
|
67
|
+
.replace(/{{CHECK_PR_COMMAND}}/g, replacements.checkPrCommand || 'npx create-quiver check-pr <slice.json>')
|
|
68
|
+
.replace(/{{CLEANUP_SLICE_COMMAND}}/g, replacements.cleanupSliceCommand || 'npx create-quiver cleanup-slice <slice.json>')
|
|
69
|
+
.replace(/{{CHECK_SCOPE_COMMAND}}/g, replacements.checkScopeCommand || 'npx create-quiver check-scope <slice.json>')
|
|
70
|
+
.replace(/{{REFRESH_ACTIVE_SLICES_COMMAND}}/g, replacements.refreshActiveSlicesCommand || 'npx create-quiver refresh-active-slices');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function copyRenderedFile(sourcePath, destinationPath, replacements, skipIfExists, frontMatterFactory) {
|
|
74
|
+
const sourceText = fs.readFileSync(sourcePath, 'utf8');
|
|
75
|
+
const renderedText = renderTemplate(sourceText, replacements);
|
|
76
|
+
|
|
77
|
+
if (skipIfExists && fs.existsSync(destinationPath)) {
|
|
78
|
+
if (typeof frontMatterFactory === 'function') {
|
|
79
|
+
const existingText = fs.readFileSync(destinationPath, 'utf8');
|
|
80
|
+
const body = stripFrontMatter(existingText);
|
|
81
|
+
writeFrontMatter(destinationPath, frontMatterFactory({
|
|
82
|
+
body,
|
|
83
|
+
existingText,
|
|
84
|
+
renderedText,
|
|
85
|
+
replacements,
|
|
86
|
+
destinationPath,
|
|
87
|
+
}));
|
|
88
|
+
return 'frontmatter-updated';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return 'skipped';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ensureDir(path.dirname(destinationPath));
|
|
95
|
+
fs.writeFileSync(destinationPath, renderedText);
|
|
96
|
+
|
|
97
|
+
if (typeof frontMatterFactory === 'function') {
|
|
98
|
+
writeFrontMatter(destinationPath, frontMatterFactory({
|
|
99
|
+
body: renderedText,
|
|
100
|
+
existingText: null,
|
|
101
|
+
renderedText,
|
|
102
|
+
replacements,
|
|
103
|
+
destinationPath,
|
|
104
|
+
}));
|
|
105
|
+
return 'created-with-frontmatter';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return 'created';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function copyBinaryFile(sourcePath, destinationPath, skipIfExists) {
|
|
112
|
+
if (skipIfExists && fs.existsSync(destinationPath)) {
|
|
113
|
+
return 'skipped';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
ensureDir(path.dirname(destinationPath));
|
|
117
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const mode = fs.statSync(sourcePath).mode & 0o777;
|
|
121
|
+
fs.chmodSync(destinationPath, mode);
|
|
122
|
+
} catch {
|
|
123
|
+
// Best effort. Windows and some filesystems may not honor POSIX modes.
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return 'created';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function copyIfSourceExists(sourcePath, destinationPath, skipIfExists) {
|
|
130
|
+
if (!fs.existsSync(sourcePath)) {
|
|
131
|
+
return 'missing';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return copyBinaryFile(sourcePath, destinationPath, skipIfExists);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function stripFrontMatter(text) {
|
|
138
|
+
if (!text.startsWith('---\n')) {
|
|
139
|
+
return text;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const closing = text.indexOf('\n---\n', 4);
|
|
143
|
+
if (closing === -1) {
|
|
144
|
+
return text;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return text.slice(closing + 5).replace(/^\n+/, '');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function estimateTokenCost(text) {
|
|
151
|
+
return Math.max(1, Math.ceil(Buffer.byteLength(text, 'utf8') / 4));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function serializeFrontMatterValue(value) {
|
|
155
|
+
if (value === null) {
|
|
156
|
+
return 'null';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (typeof value === 'number') {
|
|
160
|
+
return String(value);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return JSON.stringify(String(value));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function serializeFrontMatter(fields) {
|
|
167
|
+
return [
|
|
168
|
+
'---',
|
|
169
|
+
`purpose: ${serializeFrontMatterValue(fields.purpose)}`,
|
|
170
|
+
`applies_when: ${serializeFrontMatterValue(fields.applies_when)}`,
|
|
171
|
+
`token_cost: ${serializeFrontMatterValue(fields.token_cost)}`,
|
|
172
|
+
`last_updated: ${serializeFrontMatterValue(fields.last_updated)}`,
|
|
173
|
+
`supersedes: ${serializeFrontMatterValue(fields.supersedes)}`,
|
|
174
|
+
'---',
|
|
175
|
+
].join('\n');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function buildFrontMatterFields({ purpose, appliesWhen, body, currentDate, supersedes = null }) {
|
|
179
|
+
return {
|
|
180
|
+
purpose,
|
|
181
|
+
applies_when: appliesWhen,
|
|
182
|
+
token_cost: estimateTokenCost(body),
|
|
183
|
+
last_updated: currentDate,
|
|
184
|
+
supersedes,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function writeFrontMatter(filePath, fields) {
|
|
189
|
+
const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
190
|
+
const body = stripFrontMatter(existing);
|
|
191
|
+
const frontMatter = serializeFrontMatter(fields);
|
|
192
|
+
const nextContent = body.length > 0 ? `${frontMatter}\n\n${body.replace(/\s+$/, '')}\n` : `${frontMatter}\n`;
|
|
193
|
+
ensureDir(path.dirname(filePath));
|
|
194
|
+
fs.writeFileSync(filePath, nextContent);
|
|
195
|
+
return nextContent;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function mergePackageJson(projectRoot, templateRoot, skipIfExists) {
|
|
199
|
+
const packageTemplate = path.join(templateRoot, 'package.template.json');
|
|
200
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
201
|
+
|
|
202
|
+
if (!fs.existsSync(packageTemplate)) {
|
|
203
|
+
return 'missing';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
207
|
+
fs.copyFileSync(packageTemplate, packageJsonPath);
|
|
208
|
+
return 'created';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const existing = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
212
|
+
const template = JSON.parse(fs.readFileSync(packageTemplate, 'utf8'));
|
|
213
|
+
|
|
214
|
+
existing.scripts = {
|
|
215
|
+
...(existing.scripts || {}),
|
|
216
|
+
...(template.scripts || {}),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
fs.writeFileSync(packageJsonPath, `${JSON.stringify(existing, null, 2)}\n`);
|
|
220
|
+
return skipIfExists ? 'merged' : 'updated';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function buildReadme(projectName, projectSlug) {
|
|
224
|
+
return `# ${projectName}
|
|
225
|
+
|
|
226
|
+
[Descripción breve del proyecto]
|
|
227
|
+
|
|
228
|
+
## Quick Start
|
|
229
|
+
|
|
230
|
+
Run Quiver from this project root. Do not install it globally.
|
|
231
|
+
|
|
232
|
+
\`\`\`bash
|
|
233
|
+
npm install
|
|
234
|
+
npx create-quiver analyze
|
|
235
|
+
npx create-quiver doctor
|
|
236
|
+
\`\`\`
|
|
237
|
+
|
|
238
|
+
If this project needs a pinned Quiver version, install it as a devDependency:
|
|
239
|
+
|
|
240
|
+
\`\`\`bash
|
|
241
|
+
npm install --save-dev create-quiver
|
|
242
|
+
\`\`\`
|
|
243
|
+
|
|
244
|
+
If you need to target another directory from outside the project, pass \`--dir\` explicitly. Quote paths that contain spaces.
|
|
245
|
+
|
|
246
|
+
After you run \`analyze\`, open \`docs/PROJECT_MAP.md\` for the detected stack, package manager, and command surface.
|
|
247
|
+
|
|
248
|
+
## Project NPM Scripts
|
|
249
|
+
|
|
250
|
+
The generated project includes \`quiver:*\` npm scripts that call the Node CLI and are the preferred repeatable workflow:
|
|
251
|
+
|
|
252
|
+
\`\`\`bash
|
|
253
|
+
npm run quiver:analyze
|
|
254
|
+
npm run quiver:doctor
|
|
255
|
+
npm run quiver:migrate
|
|
256
|
+
npm run quiver:start-slice -- specs/${projectSlug}/slices/slice-01/slice.json
|
|
257
|
+
npm run quiver:check-slice -- specs/${projectSlug}/slices/slice-01/slice.json
|
|
258
|
+
npm run quiver:check-pr -- specs/${projectSlug}/slices/slice-01/slice.json
|
|
259
|
+
npm run quiver:cleanup-slice -- specs/${projectSlug}/slices/slice-01/slice.json
|
|
260
|
+
npm run quiver:check-scope -- specs/${projectSlug}/slices/slice-01/slice.json
|
|
261
|
+
npm run quiver:refresh-active-slices
|
|
262
|
+
\`\`\`
|
|
263
|
+
|
|
264
|
+
The legacy Bash wrappers remain in \`tools/scripts/\` for compatibility, but new project-level automation should prefer the \`quiver:*\` scripts and the direct \`npx create-quiver ...\` commands below.
|
|
265
|
+
|
|
266
|
+
## Cross-Platform Support
|
|
267
|
+
|
|
268
|
+
Quiver is targeting native support on macOS, Linux, and Windows PowerShell/CMD. Bash is a legacy compatibility path until the runtime slices land, so the generated workflow should be read as a native Node-first contract rather than a Bash-first one. Windows support is only considered verified once the CI matrix is green.
|
|
269
|
+
|
|
270
|
+
## Upgrading Existing Projects
|
|
271
|
+
|
|
272
|
+
If the project already existed before this Quiver version, upgrade it from the project root:
|
|
273
|
+
|
|
274
|
+
\`\`\`bash
|
|
275
|
+
cd /path/to/your-project
|
|
276
|
+
npx create-quiver migrate
|
|
277
|
+
npx create-quiver analyze
|
|
278
|
+
npx create-quiver doctor
|
|
279
|
+
\`\`\`
|
|
280
|
+
|
|
281
|
+
If your team prefers a pinned local dependency, update the package first and then run the same flow:
|
|
282
|
+
|
|
283
|
+
\`\`\`bash
|
|
284
|
+
npm install --save-dev create-quiver@latest
|
|
285
|
+
npx create-quiver migrate
|
|
286
|
+
npx create-quiver analyze
|
|
287
|
+
npx create-quiver doctor
|
|
288
|
+
\`\`\`
|
|
289
|
+
|
|
290
|
+
## AI Context Onboarding
|
|
291
|
+
|
|
292
|
+
Read \`AGENTS.md\` first, then open \`docs/AI_ONBOARDING_PROMPT.md\` after analysis.
|
|
293
|
+
|
|
294
|
+
After analysis and doctor validation, open your AI agent in this project and run:
|
|
295
|
+
|
|
296
|
+
\`\`\`text
|
|
297
|
+
Read docs/AI_ONBOARDING_PROMPT.md and execute it.
|
|
298
|
+
Do not modify product code unless I explicitly authorize it.
|
|
299
|
+
Prepare the project context docs and report assumptions, risks, and files changed.
|
|
300
|
+
\`\`\`
|
|
301
|
+
|
|
302
|
+
Review the AI changes to docs/AI_CONTEXT.md, docs/CONTEXTO.md, docs/STATUS.md, and specs/${projectSlug}/SPEC.md before starting implementation work. Use \`docs/PROJECT_MAP.md\` for stack and command details.
|
|
303
|
+
|
|
304
|
+
## Decision Log
|
|
305
|
+
|
|
306
|
+
Record durable decisions in \`docs/DECISIONS.md\` so future AI agents do not re-litigate the same choices.
|
|
307
|
+
|
|
308
|
+
## First Slice Workflow
|
|
309
|
+
|
|
310
|
+
1. Review or refine specs/${projectSlug}/SPEC.md.
|
|
311
|
+
2. Create the first slice from specs/${projectSlug}/slices/slice-template/slice.json.
|
|
312
|
+
3. Start work with \`npx create-quiver start-slice <slice.json>\` or \`npm run quiver:start-slice -- <slice.json>\`.
|
|
313
|
+
4. Make one commit per slice.
|
|
314
|
+
5. Open one PR per spec.
|
|
315
|
+
|
|
316
|
+
## Verification Checklist
|
|
317
|
+
|
|
318
|
+
- [ ] npm install completes
|
|
319
|
+
- [ ] npx create-quiver analyze completes
|
|
320
|
+
- [ ] npx create-quiver doctor completes
|
|
321
|
+
- [ ] AI agent executed docs/AI_ONBOARDING_PROMPT.md
|
|
322
|
+
- [ ] Context docs were reviewed before the first slice
|
|
323
|
+
|
|
324
|
+
## Documentation
|
|
325
|
+
|
|
326
|
+
- [AI Context](./docs/AI_CONTEXT.md) - Contexto resumido para IA
|
|
327
|
+
- [Decision Log](./docs/DECISIONS.md) - Decisiones durables del proyecto
|
|
328
|
+
- [AI Onboarding Prompt](./docs/AI_ONBOARDING_PROMPT.md) - Handoff exacto para agentes después del análisis
|
|
329
|
+
- [Contexto](./docs/CONTEXTO.md) - Qué es ${projectName}
|
|
330
|
+
- [Workflow](./docs/WORKFLOW.md) - Cómo implementar
|
|
331
|
+
- [Support Matrix](./docs/SUPPORT_MATRIX.md) - Qué entornos están soportados
|
|
332
|
+
- [Troubleshooting](./docs/TROUBLESHOOTING.md) - Cómo recuperarse de fallos comunes
|
|
333
|
+
- [Status](./docs/STATUS.md) - Estado del proyecto
|
|
334
|
+
- [API Docs](./docs/api/) - Endpoint documentation (si aplica)
|
|
335
|
+
`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function initializeProjectDocs(options) {
|
|
339
|
+
const {
|
|
340
|
+
projectRoot,
|
|
341
|
+
projectName,
|
|
342
|
+
cliVersion,
|
|
343
|
+
migrateMode = false,
|
|
344
|
+
} = options;
|
|
345
|
+
|
|
346
|
+
const templateRoot = path.join(projectRoot, 'docs-template');
|
|
347
|
+
const replacements = {
|
|
348
|
+
projectName,
|
|
349
|
+
projectSlug: toProjectSlug(projectName),
|
|
350
|
+
currentDate: new Date().toISOString().slice(0, 10),
|
|
351
|
+
datePlus7: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10),
|
|
352
|
+
datePlus30: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10),
|
|
353
|
+
datePlus35: new Date(Date.now() + 35 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10),
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const dirs = [
|
|
357
|
+
'docs',
|
|
358
|
+
'docs/ai',
|
|
359
|
+
'docs/tools',
|
|
360
|
+
'docs/archive',
|
|
361
|
+
`specs/${replacements.projectSlug}/slices/slice-template`,
|
|
362
|
+
'tools/scripts',
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
for (const dir of dirs) {
|
|
366
|
+
ensureDir(path.join(projectRoot, dir));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const operations = [];
|
|
370
|
+
const agentsSourcePath = path.join(templateRoot, 'AGENTS.md.template');
|
|
371
|
+
if (fs.existsSync(agentsSourcePath)) {
|
|
372
|
+
const agentsDestinationPath = path.join(projectRoot, 'AGENTS.md');
|
|
373
|
+
const result = copyRenderedFile(agentsSourcePath, agentsDestinationPath, replacements, true);
|
|
374
|
+
operations.push({ source: 'AGENTS.md.template', destination: 'AGENTS.md', result });
|
|
375
|
+
}
|
|
376
|
+
const frontMatterFor = (purpose, appliesWhen, supersedes = null) => ({ body }) => buildFrontMatterFields({
|
|
377
|
+
purpose,
|
|
378
|
+
appliesWhen,
|
|
379
|
+
body,
|
|
380
|
+
currentDate: replacements.currentDate,
|
|
381
|
+
supersedes,
|
|
382
|
+
});
|
|
383
|
+
const templateCopies = [
|
|
384
|
+
['docs/INDEX.md.template', 'docs/INDEX.md'],
|
|
385
|
+
['docs/DECISIONS.md.template', 'docs/DECISIONS.md'],
|
|
386
|
+
['docs/AI_CONTEXT.md.template', 'docs/AI_CONTEXT.md', frontMatterFor('Agent-facing project context pack', 'onboarding, implementation, review')],
|
|
387
|
+
['docs/AI_ONBOARDING_PROMPT.md.template', 'docs/AI_ONBOARDING_PROMPT.md', frontMatterFor('AI onboarding handoff prompt', 'onboarding after analysis')],
|
|
388
|
+
['docs/CONTEXTO.md.template', 'docs/CONTEXTO.md', frontMatterFor('Human-readable project overview', 'onboarding, review')],
|
|
389
|
+
['docs/STATUS.md.template', 'docs/STATUS.md', frontMatterFor('Project status snapshot', 'progress review, planning')],
|
|
390
|
+
['docs/WORKFLOW.md.template', 'docs/WORKFLOW.md', frontMatterFor('Execution workflow contract', 'planning, implementation')],
|
|
391
|
+
['docs/SUPPORT_MATRIX.md.template', 'docs/SUPPORT_MATRIX.md'],
|
|
392
|
+
['docs/TROUBLESHOOTING.md.template', 'docs/TROUBLESHOOTING.md'],
|
|
393
|
+
['docs/MULTI_AGENT_WORKFLOW.md.template', 'docs/MULTI_AGENT_WORKFLOW.md'],
|
|
394
|
+
['docs/MOCK_DATA_GUIDE.md.template', 'docs/MOCK_DATA_GUIDE.md'],
|
|
395
|
+
['docs/UI_STANDARDS.md.template', 'docs/UI_STANDARDS.md'],
|
|
396
|
+
['docs/GITFLOW_PR_GUIDE.md.template', 'docs/GITFLOW_PR_GUIDE.md'],
|
|
397
|
+
['docs/DOCUMENTATION_GUIDE.md.template', 'docs/DOCUMENTATION_GUIDE.md'],
|
|
398
|
+
['docs/TESTING_GUIDE_FOR_AI.md.template', 'docs/TESTING_GUIDE_FOR_AI.md'],
|
|
399
|
+
['docs/ai/LESSONS.md.template', 'docs/ai/LESSONS.md', frontMatterFor('Slice learnings log', 'after slice completion')],
|
|
400
|
+
['specs/[project-name]/SPEC.md.template', `specs/${replacements.projectSlug}/SPEC.md`],
|
|
401
|
+
['specs/[project-name]/STATUS.md.template', `specs/${replacements.projectSlug}/STATUS.md`],
|
|
402
|
+
['specs/[project-name]/EVIDENCE_REPORT.md.template', `specs/${replacements.projectSlug}/EVIDENCE_REPORT.md`],
|
|
403
|
+
['specs/[project-name]/slices/slice-template/slice.json', `specs/${replacements.projectSlug}/slices/slice-template/slice.json`],
|
|
404
|
+
['specs/[project-name]/slices/pr.md.template', `specs/${replacements.projectSlug}/slices/slice-template/pr.md.template`],
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
for (const [source, destination, frontMatterFactory] of templateCopies) {
|
|
408
|
+
const sourcePath = path.join(templateRoot, source);
|
|
409
|
+
if (!fs.existsSync(sourcePath)) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const destinationPath = path.join(projectRoot, destination);
|
|
414
|
+
const result = copyRenderedFile(sourcePath, destinationPath, replacements, migrateMode, frontMatterFactory);
|
|
415
|
+
operations.push({ source, destination, result });
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const binaryCopies = [
|
|
419
|
+
['docs/UI_STANDARDS.md', 'docs/UI_STANDARDS.md'],
|
|
420
|
+
['docs/MOCK_DATA_GUIDE.md', 'docs/MOCK_DATA_GUIDE.md'],
|
|
421
|
+
['docs/ai/RULES.yaml', 'docs/ai/RULES.yaml'],
|
|
422
|
+
['LICENSE', 'LICENSE'],
|
|
423
|
+
['CONTRIBUTING.md', 'CONTRIBUTING.md'],
|
|
424
|
+
['CODE_OF_CONDUCT.md', 'CODE_OF_CONDUCT.md'],
|
|
425
|
+
['SECURITY.md', 'SECURITY.md'],
|
|
426
|
+
['CHANGELOG.md', 'CHANGELOG.md'],
|
|
427
|
+
['ROADMAP.md', 'ROADMAP.md'],
|
|
428
|
+
['.github/pull_request_template.md', '.github/pull_request_template.md'],
|
|
429
|
+
['.github/ISSUE_TEMPLATE/bug_report.md', '.github/ISSUE_TEMPLATE/bug_report.md'],
|
|
430
|
+
['.github/ISSUE_TEMPLATE/feature_request.md', '.github/ISSUE_TEMPLATE/feature_request.md'],
|
|
431
|
+
['.github/workflows/ci.yml', '.github/workflows/ci.yml'],
|
|
432
|
+
['scripts/start-slice.sh', 'tools/scripts/start-slice.sh'],
|
|
433
|
+
['scripts/refresh-active-slices.sh', 'tools/scripts/refresh-active-slices.sh'],
|
|
434
|
+
['scripts/check-slice-readiness.sh', 'tools/scripts/check-slice-readiness.sh'],
|
|
435
|
+
['scripts/check-pr-readiness.sh', 'tools/scripts/check-pr-readiness.sh'],
|
|
436
|
+
['scripts/cleanup-slice.sh', 'tools/scripts/cleanup-slice.sh'],
|
|
437
|
+
['scripts/check-scope.sh', 'tools/scripts/check-scope.sh'],
|
|
438
|
+
['scripts/migrate-project.sh', 'tools/scripts/migrate-project.sh'],
|
|
439
|
+
];
|
|
440
|
+
|
|
441
|
+
for (const [source, destination] of binaryCopies) {
|
|
442
|
+
const sourcePath = path.join(templateRoot, source);
|
|
443
|
+
const destinationPath = path.join(projectRoot, destination);
|
|
444
|
+
if (!fs.existsSync(sourcePath)) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const result = copyIfSourceExists(sourcePath, destinationPath, migrateMode);
|
|
449
|
+
operations.push({ source, destination, result });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const aiPrinciplesSource = path.join(templateRoot, 'docs/ai/PRINCIPLES.md');
|
|
453
|
+
if (fs.existsSync(aiPrinciplesSource)) {
|
|
454
|
+
const aiPrinciplesDestination = path.join(projectRoot, 'docs/ai/PRINCIPLES.md');
|
|
455
|
+
const result = copyRenderedFile(
|
|
456
|
+
aiPrinciplesSource,
|
|
457
|
+
aiPrinciplesDestination,
|
|
458
|
+
replacements,
|
|
459
|
+
migrateMode,
|
|
460
|
+
frontMatterFor('AI operating principles', 'all AI work'),
|
|
461
|
+
);
|
|
462
|
+
operations.push({ source: 'docs/ai/PRINCIPLES.md', destination: 'docs/ai/PRINCIPLES.md', result });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const packageResult = mergePackageJson(projectRoot, templateRoot, migrateMode);
|
|
466
|
+
operations.push({ source: 'package.template.json', destination: 'package.json', result: packageResult });
|
|
467
|
+
|
|
468
|
+
const mergedPackageJsonPath = path.join(projectRoot, 'package.json');
|
|
469
|
+
const mergedPackageJson = fs.existsSync(mergedPackageJsonPath)
|
|
470
|
+
? JSON.parse(fs.readFileSync(mergedPackageJsonPath, 'utf8'))
|
|
471
|
+
: {};
|
|
472
|
+
const packageScripts = mergedPackageJson.scripts || {};
|
|
473
|
+
|
|
474
|
+
const tierReplacements = {
|
|
475
|
+
...replacements,
|
|
476
|
+
packageManager: detectPackageManager(projectRoot),
|
|
477
|
+
stackSummary: 'unknown until analyze',
|
|
478
|
+
primaryInstall: 'npm install',
|
|
479
|
+
primaryDev: packageScripts.dev || packageScripts.start || 'not defined',
|
|
480
|
+
primaryTest: packageScripts.test || 'not defined',
|
|
481
|
+
analyzeCommand: 'npx create-quiver analyze',
|
|
482
|
+
doctorCommand: 'npx create-quiver doctor',
|
|
483
|
+
startSliceCommand: 'npx create-quiver start-slice <slice.json>',
|
|
484
|
+
checkSliceCommand: 'npx create-quiver check-slice <slice.json>',
|
|
485
|
+
checkPrCommand: 'npx create-quiver check-pr <slice.json>',
|
|
486
|
+
cleanupSliceCommand: 'npx create-quiver cleanup-slice <slice.json>',
|
|
487
|
+
checkScopeCommand: 'npx create-quiver check-scope <slice.json>',
|
|
488
|
+
refreshActiveSlicesCommand: 'npx create-quiver refresh-active-slices',
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const tierCopies = [
|
|
492
|
+
['docs/QUICK.md.template', 'docs/ai/QUICK.md'],
|
|
493
|
+
['docs/STANDARD.md.template', 'docs/ai/STANDARD.md'],
|
|
494
|
+
['docs/DEEP.md.template', 'docs/ai/DEEP.md'],
|
|
495
|
+
];
|
|
496
|
+
|
|
497
|
+
for (const [source, destination] of tierCopies) {
|
|
498
|
+
const sourcePath = path.join(templateRoot, source);
|
|
499
|
+
if (!fs.existsSync(sourcePath)) {
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const destinationPath = path.join(projectRoot, destination);
|
|
504
|
+
const result = copyRenderedFile(sourcePath, destinationPath, tierReplacements, migrateMode, ({
|
|
505
|
+
body,
|
|
506
|
+
}) => buildFrontMatterFields({
|
|
507
|
+
purpose: destination.endsWith('QUICK.md')
|
|
508
|
+
? 'Minimum execution briefing'
|
|
509
|
+
: destination.endsWith('STANDARD.md')
|
|
510
|
+
? 'Default context pack'
|
|
511
|
+
: 'Deep project context',
|
|
512
|
+
appliesWhen: destination.endsWith('QUICK.md')
|
|
513
|
+
? 'execution'
|
|
514
|
+
: destination.endsWith('STANDARD.md')
|
|
515
|
+
? 'planning, implementation'
|
|
516
|
+
: 'planning, escalation',
|
|
517
|
+
body,
|
|
518
|
+
currentDate: replacements.currentDate,
|
|
519
|
+
supersedes: null,
|
|
520
|
+
}));
|
|
521
|
+
operations.push({ source, destination, result });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const currentState = fs.existsSync(path.join(projectRoot, '.quiver', 'state.json'))
|
|
525
|
+
? JSON.parse(fs.readFileSync(path.join(projectRoot, '.quiver', 'state.json'), 'utf8'))
|
|
526
|
+
: null;
|
|
527
|
+
const nextState = migrateMode
|
|
528
|
+
? {
|
|
529
|
+
...(currentState || {}),
|
|
530
|
+
quiver_version: cliVersion,
|
|
531
|
+
project_name: projectName || currentState?.project_name || '',
|
|
532
|
+
initialized_version: currentState?.initialized_version ?? null,
|
|
533
|
+
migrated_version: cliVersion,
|
|
534
|
+
last_initialized_at: currentState?.last_initialized_at ?? null,
|
|
535
|
+
last_migration_at: new Date().toISOString(),
|
|
536
|
+
last_analysis_at: currentState?.last_analysis_at ?? null,
|
|
537
|
+
}
|
|
538
|
+
: {
|
|
539
|
+
...(currentState || {}),
|
|
540
|
+
quiver_version: cliVersion,
|
|
541
|
+
project_name: projectName || currentState?.project_name || '',
|
|
542
|
+
initialized_version: currentState?.initialized_version || cliVersion,
|
|
543
|
+
migrated_version: currentState?.migrated_version ?? null,
|
|
544
|
+
last_initialized_at: currentState?.last_initialized_at || new Date().toISOString(),
|
|
545
|
+
last_migration_at: currentState?.last_migration_at ?? null,
|
|
546
|
+
last_analysis_at: currentState?.last_analysis_at ?? null,
|
|
547
|
+
};
|
|
548
|
+
writeState(projectRoot, nextState);
|
|
549
|
+
|
|
550
|
+
const searchPath = path.join(projectRoot, 'docs', 'SEARCH.md');
|
|
551
|
+
if (!(migrateMode && fs.existsSync(searchPath))) {
|
|
552
|
+
const searchContent = `# Búsqueda por Tema
|
|
553
|
+
|
|
554
|
+
**Última actualización:** ${replacements.currentDate}
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## AI Context
|
|
559
|
+
|
|
560
|
+
- **Agent context pack:** \`docs/AI_CONTEXT.md\`
|
|
561
|
+
- **Project overview:** \`docs/CONTEXTO.md\`
|
|
562
|
+
- **Workflow:** \`docs/WORKFLOW.md\`
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## Autenticación
|
|
567
|
+
|
|
568
|
+
- **Spec:** \`../specs/${replacements.projectSlug}/slices/slice-01/slice.json\`
|
|
569
|
+
- **PR del slice:** \`../specs/${replacements.projectSlug}/slices/slice-01/pr.md\`
|
|
570
|
+
- **Bootstrap del slice:** \`npx create-quiver start-slice ../specs/${replacements.projectSlug}/slices/slice-01/slice.json\`
|
|
571
|
+
- **Hook:** \`hooks/useAuth.ts\`
|
|
572
|
+
- **API:** \`docs/api/auth/README.md\`
|
|
573
|
+
- **Componentes:** \`app/(auth)/\`
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
## IA Configuración
|
|
578
|
+
|
|
579
|
+
- **Principios:** \`docs/ai/PRINCIPLES.md\`
|
|
580
|
+
- **Reglas:** \`docs/ai/RULES.yaml\`
|
|
581
|
+
- **Lessons:** \`docs/ai/LESSONS.md\`
|
|
582
|
+
|
|
583
|
+
## Soporte
|
|
584
|
+
|
|
585
|
+
- **Support Matrix:** \`docs/SUPPORT_MATRIX.md\`
|
|
586
|
+
- **Troubleshooting:** \`docs/TROUBLESHOOTING.md\`
|
|
587
|
+
|
|
588
|
+
---
|
|
589
|
+
|
|
590
|
+
**Fin de la búsqueda**
|
|
591
|
+
`;
|
|
592
|
+
fs.writeFileSync(searchPath, `${searchContent}\n`);
|
|
593
|
+
operations.push({ source: 'docs/SEARCH.md', destination: 'docs/SEARCH.md', result: 'created' });
|
|
594
|
+
} else {
|
|
595
|
+
operations.push({ source: 'docs/SEARCH.md', destination: 'docs/SEARCH.md', result: 'skipped' });
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const readmePath = path.join(projectRoot, 'README.md');
|
|
599
|
+
if (!fs.existsSync(readmePath)) {
|
|
600
|
+
fs.writeFileSync(readmePath, `${buildReadme(projectName, replacements.projectSlug)}\n`);
|
|
601
|
+
operations.push({ source: 'README.md template', destination: 'README.md', result: 'created' });
|
|
602
|
+
} else {
|
|
603
|
+
operations.push({ source: 'README.md template', destination: 'README.md', result: 'skipped' });
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
projectSlug: replacements.projectSlug,
|
|
608
|
+
operations,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
module.exports = {
|
|
613
|
+
initializeProjectDocs,
|
|
614
|
+
writeFrontMatter,
|
|
615
|
+
toProjectSlug,
|
|
616
|
+
};
|