create-quiver 0.6.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 +37 -13
- package/README_FOR_AI.md +27 -7
- package/ROADMAP.md +78 -0
- package/docs/AI_CONTEXT.md.template +19 -26
- 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 +17 -4
- package/package.json +2 -1
- package/package.template.json +11 -0
- package/scripts/cleanup-slice.sh +2 -172
- package/scripts/init-docs.sh +97 -24
- 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-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 +299 -123
- 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,300 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { catFileExists, currentBranch, hasLocalBranch, hasRemoteBranch, mergeBaseIsAncestor, revListCount, runGit, statusPorcelain, worktreeList } = require('./git');
|
|
4
|
+
const { resolveSliceContext, toAlias } = require('./slice');
|
|
5
|
+
|
|
6
|
+
function ensureExists(filePath, message) {
|
|
7
|
+
if (!fs.existsSync(filePath)) {
|
|
8
|
+
throw new Error(message);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function walkSlices(rootDir, acc, repoRoot) {
|
|
13
|
+
if (!fs.existsSync(rootDir)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
|
|
18
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
walkSlices(fullPath, acc, repoRoot);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (entry.isFile() && entry.name === 'slice.json' && fullPath.includes(`${path.sep}slices${path.sep}`)) {
|
|
25
|
+
const json = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
26
|
+
const branchName = json.git?.branch_name;
|
|
27
|
+
if (!branchName) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
acc.set(branchName, {
|
|
31
|
+
sliceId: json.slice_id || '',
|
|
32
|
+
files: Array.isArray(json.files) ? json.files : [],
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseWorktrees(text) {
|
|
39
|
+
const entries = [];
|
|
40
|
+
const chunks = text.trim().split('\n\n').filter(Boolean);
|
|
41
|
+
|
|
42
|
+
for (const chunk of chunks) {
|
|
43
|
+
const entry = {};
|
|
44
|
+
for (const line of chunk.split('\n')) {
|
|
45
|
+
const idx = line.indexOf(' ');
|
|
46
|
+
if (idx === -1) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
entry[line.slice(0, idx)] = line.slice(idx + 1);
|
|
50
|
+
}
|
|
51
|
+
entries.push(entry);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return entries;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function collectOverlapWarnings(repoRoot, currentBranchName, currentFiles) {
|
|
58
|
+
const sliceMap = new Map();
|
|
59
|
+
walkSlices(path.join(repoRoot, 'specs'), sliceMap, repoRoot);
|
|
60
|
+
walkSlices(path.join(repoRoot, 'specs-fix'), sliceMap, repoRoot);
|
|
61
|
+
|
|
62
|
+
const worktrees = parseWorktrees(runGit(['worktree', 'list', '--porcelain'], repoRoot));
|
|
63
|
+
const warnings = [];
|
|
64
|
+
|
|
65
|
+
for (const entry of worktrees) {
|
|
66
|
+
const worktreePath = entry.worktree;
|
|
67
|
+
const branchRef = entry.branch || '';
|
|
68
|
+
const branchName = branchRef.replace('refs/heads/', '');
|
|
69
|
+
|
|
70
|
+
if (!branchName || branchName === currentBranchName || worktreePath === repoRoot) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const meta = sliceMap.get(branchName);
|
|
75
|
+
if (!meta || meta.sliceId.startsWith('slice-00')) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const dirty = statusPorcelain(worktreePath) !== '';
|
|
80
|
+
const aheadCount = revListCount(worktreePath, 'origin/develop..HEAD');
|
|
81
|
+
const active = dirty || aheadCount > 0;
|
|
82
|
+
|
|
83
|
+
if (!active) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const overlap = currentFiles.filter((item) => meta.files.includes(item));
|
|
88
|
+
if (overlap.length > 0) {
|
|
89
|
+
warnings.push(`${branchName}|${overlap.join(', ')}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return warnings;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function checkSliceReadiness(sliceInput, options = {}) {
|
|
97
|
+
const gate = options.gate || 'execution';
|
|
98
|
+
const strictOverlap = options.strictOverlap === true;
|
|
99
|
+
const repoRoot = runGit(['rev-parse', '--show-toplevel'], process.cwd());
|
|
100
|
+
const slice = resolveSliceContext(repoRoot, sliceInput);
|
|
101
|
+
|
|
102
|
+
for (const specFile of ['SPEC.md', 'STATUS.md', 'EVIDENCE_REPORT.md']) {
|
|
103
|
+
ensureExists(path.join(repoRoot, slice.specDirRel, specFile), `create-quiver: falta '${slice.specDirRel}/${specFile}'.`);
|
|
104
|
+
}
|
|
105
|
+
console.log('PASS: El spec local tiene SPEC.md, STATUS.md y EVIDENCE_REPORT.md.');
|
|
106
|
+
|
|
107
|
+
if (catFileExists(repoRoot, `origin/develop:${slice.sliceRel}`)) {
|
|
108
|
+
console.log('PASS: El slice ya existe en origin/develop (PR base documental mergeado).');
|
|
109
|
+
} else if (gate === 'validation') {
|
|
110
|
+
console.log('WARN: El slice no existe todavia en origin/develop. El PR base documental sigue pendiente de merge. Podes abrir el PR del slice igual — el humano mergea en orden.');
|
|
111
|
+
} else {
|
|
112
|
+
throw new Error('create-quiver: el slice no existe en origin/develop. Mergea primero el PR base documental.');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const overlapWarnings = collectOverlapWarnings(repoRoot, currentBranch(repoRoot), slice.files);
|
|
116
|
+
if (overlapWarnings.length === 0) {
|
|
117
|
+
console.log('PASS: No se detecto overlap con worktrees activos.');
|
|
118
|
+
} else {
|
|
119
|
+
for (const warning of overlapWarnings) {
|
|
120
|
+
const [overlapBranch, overlapFiles] = warning.split('|');
|
|
121
|
+
if (strictOverlap) {
|
|
122
|
+
throw new Error(`create-quiver: Overlap con worktree activo '${overlapBranch}': ${overlapFiles}`);
|
|
123
|
+
}
|
|
124
|
+
console.log(`WARN: Overlap con worktree activo '${overlapBranch}': ${overlapFiles}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
switch (gate) {
|
|
129
|
+
case 'ready':
|
|
130
|
+
if (slice.status !== 'ready') {
|
|
131
|
+
throw new Error(`create-quiver: Gate ready: slice.json debe estar en status=ready. Estado actual: ${slice.status}. Completa la especificacion en el Track 1 antes de pasar a ejecucion.`);
|
|
132
|
+
}
|
|
133
|
+
console.log('PASS: Gate ready: el slice esta marcado como ready para ejecucion.');
|
|
134
|
+
break;
|
|
135
|
+
case 'execution':
|
|
136
|
+
if (slice.status === 'blocked') {
|
|
137
|
+
throw new Error('create-quiver: El slice esta bloqueado (status=blocked). Resolve el bloqueante antes de ejecutar.');
|
|
138
|
+
}
|
|
139
|
+
if (slice.status === 'cancelled') {
|
|
140
|
+
throw new Error('create-quiver: El slice esta cancelado (status=cancelled).');
|
|
141
|
+
}
|
|
142
|
+
if (slice.status === 'completed') {
|
|
143
|
+
console.log('WARN: El slice ya figura como completed. Revisa si realmente corresponde reejecutarlo.');
|
|
144
|
+
}
|
|
145
|
+
if (slice.status === 'draft') {
|
|
146
|
+
console.log("WARN: El slice esta en estado 'draft'. Considera marcarlo como 'ready' antes de ejecutar.");
|
|
147
|
+
}
|
|
148
|
+
console.log('PASS: Gate execution: metadata y precondiciones minimas OK.');
|
|
149
|
+
break;
|
|
150
|
+
case 'validation':
|
|
151
|
+
if (slice.status !== 'completed') {
|
|
152
|
+
throw new Error('create-quiver: Para gate validation, slice.json debe estar en status=completed.');
|
|
153
|
+
}
|
|
154
|
+
if (!slice.json.completed_at) {
|
|
155
|
+
throw new Error('create-quiver: Para gate validation, slice.json debe tener completed_at.');
|
|
156
|
+
}
|
|
157
|
+
if (!slice.json.started_at) {
|
|
158
|
+
throw new Error('create-quiver: Para gate validation, slice.json debe tener started_at.');
|
|
159
|
+
}
|
|
160
|
+
if (!slice.json.actual_hours || Number(slice.json.actual_hours) <= 0) {
|
|
161
|
+
throw new Error('create-quiver: Para gate validation, slice.json debe tener actual_hours > 0.');
|
|
162
|
+
}
|
|
163
|
+
console.log('PASS: Gate validation: slice marcado como completado y con trazabilidad minima.');
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function checkPrReadiness(sliceInput) {
|
|
169
|
+
const repoRoot = runGit(['rev-parse', '--show-toplevel'], process.cwd());
|
|
170
|
+
const scriptDir = path.dirname(__filename);
|
|
171
|
+
const slice = resolveSliceContext(repoRoot, sliceInput);
|
|
172
|
+
const current = currentBranch(repoRoot);
|
|
173
|
+
const prPath = path.join(path.dirname(slice.sliceAbs), 'pr.md');
|
|
174
|
+
|
|
175
|
+
checkSliceReadiness(slice.sliceAbs, { gate: 'validation' });
|
|
176
|
+
checkScope(slice.sliceAbs, { strict: true });
|
|
177
|
+
|
|
178
|
+
if (!slice.branchName) {
|
|
179
|
+
throw new Error('create-quiver: Falta git.branch_name en el slice.');
|
|
180
|
+
}
|
|
181
|
+
if (!fs.existsSync(prPath)) {
|
|
182
|
+
throw new Error('create-quiver: Falta pr.md junto al slice.');
|
|
183
|
+
}
|
|
184
|
+
if (current !== slice.branchName) {
|
|
185
|
+
throw new Error(`create-quiver: Debes ejecutar este check desde la rama del slice. Actual: ${current} Esperada: ${slice.branchName}`);
|
|
186
|
+
}
|
|
187
|
+
console.log('PASS: La rama actual coincide con la rama declarada por el slice.');
|
|
188
|
+
if (statusPorcelain(repoRoot) !== '') {
|
|
189
|
+
throw new Error('create-quiver: El worktree no esta limpio. Cerra la implementacion antes de abrir el PR.');
|
|
190
|
+
}
|
|
191
|
+
console.log('PASS: El worktree esta limpio.');
|
|
192
|
+
|
|
193
|
+
const aheadCount = revListCount(repoRoot, 'origin/develop..HEAD');
|
|
194
|
+
if (aheadCount <= 0) {
|
|
195
|
+
if (mergeBaseIsAncestor(repoRoot, 'HEAD', 'origin/develop')) {
|
|
196
|
+
throw new Error('create-quiver: La rama ya fue absorbida por origin/develop. Este gate aplica antes del merge.');
|
|
197
|
+
}
|
|
198
|
+
throw new Error('create-quiver: La rama no tiene commits propios respecto de origin/develop.');
|
|
199
|
+
}
|
|
200
|
+
console.log('PASS: La rama tiene commits propios contra origin/develop.');
|
|
201
|
+
|
|
202
|
+
const prText = fs.readFileSync(prPath, 'utf8');
|
|
203
|
+
for (const heading of ['## Title', '## Summary', '## Scope', '## Files', '## How to Test (DETAILED - REQUIRED)', '## Evidence', '## Rollback', '## Risks / Notes']) {
|
|
204
|
+
if (!prText.includes(heading)) {
|
|
205
|
+
throw new Error(`create-quiver: Falta la seccion obligatoria '${heading}' en pr.md.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
console.log('PASS: pr.md contiene las secciones obligatorias.');
|
|
209
|
+
|
|
210
|
+
for (const subheading of ['### Required Environment', '### Worktree Access', '### Run the Project', '### Use Cases', '### Technical Verification']) {
|
|
211
|
+
if (!prText.includes(subheading)) {
|
|
212
|
+
throw new Error(`create-quiver: Falta la subseccion '${subheading}' dentro de How to Test.`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
console.log('PASS: How to Test incluye entorno, acceso al worktree, arranque, casos de uso y verificación técnica.');
|
|
216
|
+
|
|
217
|
+
if (!/#### Case [0-9]+:/.test(prText)) {
|
|
218
|
+
throw new Error('create-quiver: How to Test debe tener al menos un caso de uso documentado (#### Case 1: ...).');
|
|
219
|
+
}
|
|
220
|
+
console.log('PASS: Al menos un caso de uso documentado.');
|
|
221
|
+
|
|
222
|
+
if (!/git revert /.test(prText)) {
|
|
223
|
+
throw new Error('create-quiver: Rollback debe incluir al menos un comando git revert.');
|
|
224
|
+
}
|
|
225
|
+
console.log('PASS: Rollback incluye comando git revert.');
|
|
226
|
+
|
|
227
|
+
if (/^\s*-\s*`manual review`$/mi.test(prText) || /^\s*-\s*`visual check`$/mi.test(prText) || /^\s*-\s*`screen test`$/mi.test(prText) || /^\s*-\s*`visual validation`$/mi.test(prText)) {
|
|
228
|
+
throw new Error('create-quiver: How to Test cannot rely only on generic phrases.');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.log(`PASS: Gate PR listo para '${slice.sliceId}'.`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function checkScope(sliceInput, options = {}) {
|
|
235
|
+
const strict = options.strict === true;
|
|
236
|
+
const repoRoot = runGit(['rev-parse', '--show-toplevel'], process.cwd());
|
|
237
|
+
const slice = resolveSliceContext(repoRoot, sliceInput);
|
|
238
|
+
const declared = slice.files;
|
|
239
|
+
|
|
240
|
+
let touchedRaw = '';
|
|
241
|
+
if (hasRemoteBranch(repoRoot, 'develop')) {
|
|
242
|
+
touchedRaw = runGit(['diff', '--name-only', 'origin/develop...HEAD'], repoRoot);
|
|
243
|
+
} else if (hasLocalBranch(repoRoot, 'develop')) {
|
|
244
|
+
touchedRaw = runGit(['diff', '--name-only', 'develop...HEAD'], repoRoot);
|
|
245
|
+
} else {
|
|
246
|
+
console.log('WARN: No se encontro rama origin/develop ni develop. Saltando check de scope.');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!touchedRaw) {
|
|
251
|
+
console.log('WARN: No se encontraron archivos modificados respecto de develop.');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const touched = touchedRaw.trim().split('\n').filter(Boolean);
|
|
256
|
+
const autoAllowed = [
|
|
257
|
+
/^specs\//,
|
|
258
|
+
/^docs\//,
|
|
259
|
+
/^\.worktrees\//,
|
|
260
|
+
/WORKTREE_CONTEXT\.md$/,
|
|
261
|
+
/EVIDENCE_REPORT\.md$/,
|
|
262
|
+
/STATUS\.md$/,
|
|
263
|
+
/SPEC\.md$/,
|
|
264
|
+
/\/pr\.md$/,
|
|
265
|
+
/\/slice\.json$/,
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
const outOfScope = touched.filter((file) => {
|
|
269
|
+
if (declared.includes(file)) return false;
|
|
270
|
+
if (autoAllowed.some((re) => re.test(file))) return false;
|
|
271
|
+
return true;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (outOfScope.length === 0) {
|
|
275
|
+
console.log('PASS: Todos los archivos tocados estan dentro del scope declarado en slice.json.');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let violationCount = 0;
|
|
280
|
+
for (const file of outOfScope) {
|
|
281
|
+
violationCount += 1;
|
|
282
|
+
if (strict) {
|
|
283
|
+
throw new Error(`create-quiver: Archivo fuera del scope: ${file}`);
|
|
284
|
+
}
|
|
285
|
+
console.log(`WARN: Archivo fuera del scope: ${file}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (violationCount > 0) {
|
|
289
|
+
if (strict) {
|
|
290
|
+
throw new Error(`${violationCount} archivo(s) fuera del scope declarado. Actualiza slice.json.files o revierte los cambios fuera de alcance.`);
|
|
291
|
+
}
|
|
292
|
+
console.log(`WARN: ${violationCount} archivo(s) fuera del scope declarado. Considera actualizar slice.json.files o revertir los cambios no previstos.`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = {
|
|
297
|
+
checkPrReadiness,
|
|
298
|
+
checkScope,
|
|
299
|
+
checkSliceReadiness,
|
|
300
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { resolveTargetRoot, toPosixPath } = require('./paths');
|
|
4
|
+
|
|
5
|
+
function readJson(filePath) {
|
|
6
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function canonicalizePath(dirPath) {
|
|
10
|
+
try {
|
|
11
|
+
return fs.realpathSync(dirPath);
|
|
12
|
+
} catch {
|
|
13
|
+
return path.resolve(dirPath);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toAlias(ticket) {
|
|
18
|
+
const parts = String(ticket || '').split('-').filter(Boolean);
|
|
19
|
+
const prefix = (parts[0] || 'GEN').replace(/[^A-Za-z0-9]/g, '').toUpperCase();
|
|
20
|
+
const suffix = (parts[parts.length - 1] || '00').replace(/[^A-Za-z0-9]/g, '').toUpperCase();
|
|
21
|
+
const short = prefix.length <= 3 ? prefix : prefix.slice(0, 3);
|
|
22
|
+
return `${short || 'GEN'}-${suffix || '00'}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveSlicePath(sliceInput) {
|
|
26
|
+
if (!fs.existsSync(sliceInput)) {
|
|
27
|
+
throw new Error(`create-quiver: no existe el slice '${sliceInput}'.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return canonicalizePath(sliceInput);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readSliceMeta(slicePath) {
|
|
34
|
+
const json = readJson(slicePath);
|
|
35
|
+
const ticket = typeof json.ticket === 'string' ? json.ticket.trim() : '';
|
|
36
|
+
const git = json.git ?? {};
|
|
37
|
+
const branchType = typeof git.branch_type === 'string' ? git.branch_type.trim() : '';
|
|
38
|
+
const baseBranch = typeof git.base_branch === 'string' ? git.base_branch.trim() : '';
|
|
39
|
+
const branchSlug = typeof git.branch_slug === 'string' ? git.branch_slug.trim() : '';
|
|
40
|
+
const branchName = typeof git.branch_name === 'string' ? git.branch_name.trim() : '';
|
|
41
|
+
const sliceId = typeof json.slice_id === 'string' ? json.slice_id.trim() : '';
|
|
42
|
+
const status = String(json.status || 'draft').trim() || 'draft';
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
acceptance: Array.isArray(json.acceptance) ? json.acceptance : [],
|
|
46
|
+
baseBranch,
|
|
47
|
+
branchName,
|
|
48
|
+
branchSlug,
|
|
49
|
+
branchType,
|
|
50
|
+
files: Array.isArray(json.files) ? json.files : [],
|
|
51
|
+
git,
|
|
52
|
+
isBaseline: sliceId.startsWith('slice-00'),
|
|
53
|
+
json,
|
|
54
|
+
sliceId,
|
|
55
|
+
slicePath,
|
|
56
|
+
specFamily: null,
|
|
57
|
+
specSlug: null,
|
|
58
|
+
status,
|
|
59
|
+
tests: Array.isArray(json.tests) ? json.tests : [],
|
|
60
|
+
ticket,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateSliceMetaForStart(slice) {
|
|
65
|
+
if (!slice.sliceId) {
|
|
66
|
+
throw new Error('create-quiver: falta "slice_id" en el slice.');
|
|
67
|
+
}
|
|
68
|
+
if (!slice.ticket) {
|
|
69
|
+
throw new Error('create-quiver: falta "ticket" en el slice.');
|
|
70
|
+
}
|
|
71
|
+
if (!slice.branchType || !slice.baseBranch || !slice.branchSlug || !slice.branchName) {
|
|
72
|
+
throw new Error('create-quiver: el bloque "git" debe incluir "branch_type", "base_branch", "branch_slug" y "branch_name".');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const expectedBaseByType = {
|
|
76
|
+
feature: 'develop',
|
|
77
|
+
bugfix: 'develop',
|
|
78
|
+
hotfix: 'main',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (!expectedBaseByType[slice.branchType]) {
|
|
82
|
+
throw new Error(`create-quiver: git.branch_type invalido: "${slice.branchType}". Usa "feature", "bugfix" o "hotfix".`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const expectedBaseBranch = expectedBaseByType[slice.branchType];
|
|
86
|
+
if (slice.baseBranch !== expectedBaseBranch) {
|
|
87
|
+
throw new Error(`create-quiver: git.base_branch invalido para ${slice.branchType}. Esperado: "${expectedBaseBranch}".`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const expectedBranchName = `${slice.branchType}/${slice.ticket}-${slice.branchSlug}`;
|
|
91
|
+
if (slice.branchName !== expectedBranchName) {
|
|
92
|
+
throw new Error(`create-quiver: git.branch_name invalido. Esperado: "${expectedBranchName}".`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveSliceContext(repoRoot, slicePath) {
|
|
97
|
+
const absSlicePath = resolveSlicePath(slicePath);
|
|
98
|
+
const relSlicePath = toPosixPath(path.relative(repoRoot, absSlicePath));
|
|
99
|
+
const parts = relSlicePath.split('/');
|
|
100
|
+
|
|
101
|
+
if (parts[0] !== 'specs' && parts[0] !== 'specs-fix') {
|
|
102
|
+
throw new Error('create-quiver: el slice debe vivir dentro de specs/ o specs-fix/.');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const specFamily = parts[0];
|
|
106
|
+
const specSlug = parts[1];
|
|
107
|
+
const specDirRel = `${specFamily}/${specSlug}`;
|
|
108
|
+
const specDirAbs = path.join(repoRoot, specDirRel);
|
|
109
|
+
const slice = readSliceMeta(absSlicePath);
|
|
110
|
+
slice.specFamily = specFamily;
|
|
111
|
+
slice.specSlug = specSlug;
|
|
112
|
+
slice.specDirRel = specDirRel;
|
|
113
|
+
slice.specDirAbs = specDirAbs;
|
|
114
|
+
slice.sliceRel = relSlicePath;
|
|
115
|
+
slice.sliceAbs = absSlicePath;
|
|
116
|
+
slice.prPath = path.join(path.dirname(absSlicePath), 'pr.md');
|
|
117
|
+
return slice;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function activeSlicePath(repoRoot) {
|
|
121
|
+
return path.join(repoRoot, 'docs', 'ai', 'ACTIVE_SLICE.md');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function renderActiveSlice(slice) {
|
|
125
|
+
const lines = [
|
|
126
|
+
'# Active Slice',
|
|
127
|
+
'',
|
|
128
|
+
'## Slice ID',
|
|
129
|
+
'',
|
|
130
|
+
slice.sliceId || 'slice-unknown',
|
|
131
|
+
'',
|
|
132
|
+
'## Title',
|
|
133
|
+
'',
|
|
134
|
+
slice.json.title || slice.sliceId || 'Untitled slice',
|
|
135
|
+
'',
|
|
136
|
+
'## Objective',
|
|
137
|
+
'',
|
|
138
|
+
slice.json.objective || 'Sin objetivo declarado.',
|
|
139
|
+
'',
|
|
140
|
+
'## allowed_files',
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
if (Array.isArray(slice.files) && slice.files.length > 0) {
|
|
144
|
+
for (const file of slice.files) {
|
|
145
|
+
lines.push(`- ${file}`);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
lines.push('- n/a');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
lines.push('', '## Validation Commands');
|
|
152
|
+
if (Array.isArray(slice.tests) && slice.tests.length > 0) {
|
|
153
|
+
for (const command of slice.tests) {
|
|
154
|
+
lines.push(`- ${command}`);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
lines.push('- n/a');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
lines.push('', '## Definition of Done');
|
|
161
|
+
if (Array.isArray(slice.acceptance) && slice.acceptance.length > 0) {
|
|
162
|
+
for (const item of slice.acceptance) {
|
|
163
|
+
lines.push(`- ${item}`);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
lines.push('- n/a');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
lines.push('', '## Prohibition', '', 'Do not edit any file outside allowed_files.', '');
|
|
170
|
+
|
|
171
|
+
return `${lines.join('\n')}\n`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function worktreesRootForRepo(repoRoot, branchName) {
|
|
175
|
+
const repoName = path.basename(repoRoot);
|
|
176
|
+
const repoParent = path.dirname(repoRoot);
|
|
177
|
+
return process.env.SLICE_WORKTREES_DIR || path.join(repoParent, '.worktrees', repoName);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function safeBranchName(branchName) {
|
|
181
|
+
return String(branchName || '').replace(/[^A-Za-z0-9._-]/g, '-');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
canonicalizePath,
|
|
186
|
+
readSliceMeta,
|
|
187
|
+
resolveSliceContext,
|
|
188
|
+
safeBranchName,
|
|
189
|
+
activeSlicePath,
|
|
190
|
+
renderActiveSlice,
|
|
191
|
+
toAlias,
|
|
192
|
+
validateSliceMetaForStart,
|
|
193
|
+
worktreesRootForRepo,
|
|
194
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function statePath(projectRoot) {
|
|
5
|
+
return path.join(projectRoot, '.quiver', 'state.json');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function ensureDir(dirPath) {
|
|
9
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readState(projectRoot) {
|
|
13
|
+
const filePath = statePath(projectRoot);
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(filePath)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeState(projectRoot, nextState) {
|
|
23
|
+
const stateDir = path.join(projectRoot, '.quiver');
|
|
24
|
+
ensureDir(stateDir);
|
|
25
|
+
fs.writeFileSync(statePath(projectRoot), `${JSON.stringify(nextState, null, 2)}\n`);
|
|
26
|
+
return statePath(projectRoot);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function updateStateForInit(projectRoot, projectName, cliVersion) {
|
|
30
|
+
const currentState = readState(projectRoot) || {};
|
|
31
|
+
const now = new Date().toISOString();
|
|
32
|
+
const nextState = {
|
|
33
|
+
...currentState,
|
|
34
|
+
quiver_version: cliVersion,
|
|
35
|
+
project_name: projectName || currentState.project_name || '',
|
|
36
|
+
initialized_version: currentState.initialized_version || cliVersion,
|
|
37
|
+
migrated_version: currentState.migrated_version ?? null,
|
|
38
|
+
last_initialized_at: currentState.last_initialized_at || now,
|
|
39
|
+
last_migration_at: currentState.last_migration_at ?? null,
|
|
40
|
+
last_analysis_at: currentState.last_analysis_at ?? null,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
writeState(projectRoot, nextState);
|
|
44
|
+
return nextState;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function updateStateForMigrate(projectRoot, projectName, cliVersion) {
|
|
48
|
+
const currentState = readState(projectRoot) || {};
|
|
49
|
+
const now = new Date().toISOString();
|
|
50
|
+
const nextState = {
|
|
51
|
+
...currentState,
|
|
52
|
+
quiver_version: cliVersion,
|
|
53
|
+
project_name: projectName || currentState.project_name || '',
|
|
54
|
+
initialized_version: currentState.initialized_version ?? null,
|
|
55
|
+
migrated_version: cliVersion,
|
|
56
|
+
last_initialized_at: currentState.last_initialized_at ?? null,
|
|
57
|
+
last_migration_at: now,
|
|
58
|
+
last_analysis_at: currentState.last_analysis_at ?? null,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
writeState(projectRoot, nextState);
|
|
62
|
+
return nextState;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function updateStateForAnalyze(projectRoot, cliVersion) {
|
|
66
|
+
const currentState = readState(projectRoot);
|
|
67
|
+
|
|
68
|
+
if (!currentState) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const nextState = {
|
|
73
|
+
...currentState,
|
|
74
|
+
quiver_version: cliVersion,
|
|
75
|
+
last_analysis_at: new Date().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
writeState(projectRoot, nextState);
|
|
79
|
+
return nextState;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
readState,
|
|
84
|
+
statePath,
|
|
85
|
+
updateStateForAnalyze,
|
|
86
|
+
updateStateForInit,
|
|
87
|
+
updateStateForMigrate,
|
|
88
|
+
writeState,
|
|
89
|
+
};
|