create-quiver 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.claude/settings.local.json +45 -0
  2. package/.github/workflows/ci.yml +9 -32
  3. package/AGENTS.md.template +41 -0
  4. package/BACKLOG.md +139 -0
  5. package/CHANGELOG.md +17 -0
  6. package/README.md +68 -14
  7. package/README_FOR_AI.md +48 -16
  8. package/ROADMAP.md +100 -0
  9. package/docs/AI_CONTEXT.md.template +19 -26
  10. package/docs/AI_ONBOARDING_PROMPT.md.template +16 -0
  11. package/docs/COMMANDS.md.template +25 -0
  12. package/docs/CONTEXTO.md.template +4 -17
  13. package/docs/DECISIONS.md.template +18 -0
  14. package/docs/DEEP.md.template +34 -0
  15. package/docs/DOCUMENTATION_GUIDE.md.template +9 -7
  16. package/docs/GITFLOW_PR_GUIDE.md.template +7 -0
  17. package/docs/INDEX.md.template +11 -0
  18. package/docs/QUICK.md.template +27 -0
  19. package/docs/STANDARD.md.template +49 -0
  20. package/docs/STATUS.md.template +2 -2
  21. package/docs/SUPPORT_MATRIX.md.template +16 -4
  22. package/docs/TESTING_GUIDE_FOR_AI.md.template +4 -3
  23. package/docs/TROUBLESHOOTING.md.template +14 -0
  24. package/docs/WORKFLOW.md.template +21 -4
  25. package/docs/examples/graph.md.template +62 -0
  26. package/docs/examples/next.md.template +27 -0
  27. package/docs/examples/plan.md.template +28 -0
  28. package/package.json +6 -2
  29. package/package.template.json +16 -0
  30. package/scripts/check-slice-readiness.sh +6 -4
  31. package/scripts/cleanup-slice.sh +2 -172
  32. package/scripts/init-docs.sh +147 -26
  33. package/scripts/package-quiver.sh +5 -0
  34. package/scripts/start-slice.sh +3 -425
  35. package/specs/[project-name]/EVIDENCE_REPORT.md.template +3 -1
  36. package/specs/[project-name]/HANDOFF.md.template +37 -0
  37. package/specs/[project-name]/slices/slice-template/slice.json +7 -2
  38. package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-01-project-scan-command/slice.json +1 -1
  39. package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-02-ai-onboarding-prompt/slice.json +1 -1
  40. package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-03-doctor-readme-adoption-flow/slice.json +1 -1
  41. package/specs/quiver-v12-cross-platform-native-runtime/EVIDENCE_REPORT.md +30 -0
  42. package/specs/quiver-v12-cross-platform-native-runtime/SPEC.md +86 -0
  43. package/specs/quiver-v12-cross-platform-native-runtime/STATUS.md +29 -0
  44. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-01-cross-platform-support-contract/slice.json +69 -0
  45. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-02-node-init-docs-runtime/slice.json +76 -0
  46. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-03-node-migrate-analyze-doctor-flow/slice.json +74 -0
  47. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-04-node-slice-lifecycle-commands/slice.json +81 -0
  48. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-05-generated-project-scripts-and-migration/slice.json +78 -0
  49. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-06-cross-platform-ci-release-readiness/slice.json +74 -0
  50. package/specs/quiver-v13-token-efficient-ai-context/EVIDENCE_REPORT.md +28 -0
  51. package/specs/quiver-v13-token-efficient-ai-context/SPEC.md +68 -0
  52. package/specs/quiver-v13-token-efficient-ai-context/STATUS.md +26 -0
  53. package/specs/quiver-v13-token-efficient-ai-context/slices/slice-01-token-efficient-ai-modes-guidance/slice.json +65 -0
  54. package/specs/quiver-v13-token-efficient-ai-context/slices/slice-02-decision-log-context-checkpoint/slice.json +64 -0
  55. package/specs/quiver-v13-token-efficient-ai-context/slices/slice-03-project-map-reading-order/slice.json +66 -0
  56. package/specs/quiver-v14-tiered-context-pack/EVIDENCE_REPORT.md +42 -0
  57. package/specs/quiver-v14-tiered-context-pack/SPEC.md +116 -0
  58. package/specs/quiver-v14-tiered-context-pack/STATUS.md +35 -0
  59. package/specs/quiver-v14-tiered-context-pack/slices/slice-01-tiered-context-pack/slice.json +77 -0
  60. package/specs/quiver-v14-tiered-context-pack/slices/slice-02-agents-md-router/slice.json +74 -0
  61. package/specs/quiver-v14-tiered-context-pack/slices/slice-03-active-slice-lifecycle/slice.json +74 -0
  62. package/specs/quiver-v14-tiered-context-pack/slices/slice-04-dedup-frontmatter/slice.json +83 -0
  63. package/specs/quiver-v14-tiered-context-pack/slices/slice-05-doctor-smokes-tiered-pack/slice.json +84 -0
  64. package/specs/quiver-v15-init-required-before-migrate/EVIDENCE_REPORT.md +26 -0
  65. package/specs/quiver-v15-init-required-before-migrate/SPEC.md +66 -0
  66. package/specs/quiver-v15-init-required-before-migrate/STATUS.md +26 -0
  67. package/specs/quiver-v15-init-required-before-migrate/slices/slice-01-migrate-initialization-precondition/slice.json +65 -0
  68. package/specs/quiver-v15-init-required-before-migrate/slices/slice-02-doctor-not-initialized-guidance/slice.json +61 -0
  69. package/specs/quiver-v15-init-required-before-migrate/slices/slice-03-docs-smokes-init-before-migrate/slice.json +64 -0
  70. package/specs/quiver-v16-handoff-contract/EVIDENCE_REPORT.md +26 -0
  71. package/specs/quiver-v16-handoff-contract/SPEC.md +68 -0
  72. package/specs/quiver-v16-handoff-contract/STATUS.md +26 -0
  73. package/specs/quiver-v16-handoff-contract/slices/slice-01-handoff-template-and-contract/slice.json +66 -0
  74. package/specs/quiver-v16-handoff-contract/slices/slice-02-check-handoff-command/slice.json +70 -0
  75. package/specs/quiver-v16-handoff-contract/slices/slice-03-handoff-scaffold-optional/slice.json +67 -0
  76. package/specs/quiver-v17-orchestration-foundation/EVIDENCE_REPORT.md +32 -0
  77. package/specs/quiver-v17-orchestration-foundation/SPEC.md +79 -0
  78. package/specs/quiver-v17-orchestration-foundation/STATUS.md +31 -0
  79. package/specs/quiver-v17-orchestration-foundation/slices/slice-01-ci-matrix-verified/slice.json +68 -0
  80. package/specs/quiver-v17-orchestration-foundation/slices/slice-02-slice-graph-library/slice.json +65 -0
  81. package/specs/quiver-v17-orchestration-foundation/slices/slice-03-depends-on-validation/slice.json +72 -0
  82. package/specs/quiver-v18-slice-orchestration/EVIDENCE_REPORT.md +38 -0
  83. package/specs/quiver-v18-slice-orchestration/SPEC.md +91 -0
  84. package/specs/quiver-v18-slice-orchestration/STATUS.md +33 -0
  85. package/specs/quiver-v18-slice-orchestration/slices/slice-01-plan-command/slice.json +79 -0
  86. package/specs/quiver-v18-slice-orchestration/slices/slice-02-graph-mvp-tree/slice.json +75 -0
  87. package/specs/quiver-v18-slice-orchestration/slices/slice-03-graph-extended-formats/slice.json +70 -0
  88. package/specs/quiver-v18-slice-orchestration/slices/slice-04-next-command/slice.json +73 -0
  89. package/specs/quiver-v18-stabilization/EVIDENCE_REPORT.md +26 -0
  90. package/specs/quiver-v18-stabilization/SPEC.md +62 -0
  91. package/specs/quiver-v18-stabilization/STATUS.md +30 -0
  92. package/specs/quiver-v18-stabilization/slices/slice-01-fix-legacy-dependency-resolution/CLOSURE_BRIEF.md +29 -0
  93. package/specs/quiver-v18-stabilization/slices/slice-01-fix-legacy-dependency-resolution/EXECUTION_BRIEF.md +134 -0
  94. package/specs/quiver-v18-stabilization/slices/slice-01-fix-legacy-dependency-resolution/slice.json +56 -0
  95. package/specs/quiver-v18-stabilization/slices/slice-02-roadmap-and-branch-cleanup/CLOSURE_BRIEF.md +29 -0
  96. package/specs/quiver-v18-stabilization/slices/slice-02-roadmap-and-branch-cleanup/EXECUTION_BRIEF.md +118 -0
  97. package/specs/quiver-v18-stabilization/slices/slice-02-roadmap-and-branch-cleanup/slice.json +57 -0
  98. package/specs/quiver-v18-stabilization/slices/slice-03-publish-drafts-branch/CLOSURE_BRIEF.md +23 -0
  99. package/specs/quiver-v18-stabilization/slices/slice-03-publish-drafts-branch/EXECUTION_BRIEF.md +73 -0
  100. package/specs/quiver-v18-stabilization/slices/slice-03-publish-drafts-branch/slice.json +49 -0
  101. package/src/create-quiver/commands/graph.js +97 -0
  102. package/src/create-quiver/commands/next.js +134 -0
  103. package/src/create-quiver/commands/plan.js +205 -0
  104. package/src/create-quiver/index.js +476 -123
  105. package/src/create-quiver/lib/analyze.js +9 -0
  106. package/src/create-quiver/lib/doctor.js +212 -0
  107. package/src/create-quiver/lib/git.js +154 -0
  108. package/src/create-quiver/lib/handoff.js +104 -0
  109. package/src/create-quiver/lib/init-docs.js +674 -0
  110. package/src/create-quiver/lib/json.js +14 -0
  111. package/src/create-quiver/lib/lifecycle.js +479 -0
  112. package/src/create-quiver/lib/paths.js +19 -0
  113. package/src/create-quiver/lib/readiness.js +354 -0
  114. package/src/create-quiver/lib/renderers/dot.js +129 -0
  115. package/src/create-quiver/lib/renderers/mermaid.js +119 -0
  116. package/src/create-quiver/lib/renderers/tree.js +116 -0
  117. package/src/create-quiver/lib/scope.js +5 -0
  118. package/src/create-quiver/lib/slice-graph.js +453 -0
  119. package/src/create-quiver/lib/slice.js +195 -0
  120. package/src/create-quiver/lib/state.js +139 -0
@@ -0,0 +1,354 @@
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 { parseJsonWithComments } = require('./json');
5
+ const { buildGraph, readAllSlices, SliceGraphError, topoSort } = require('./slice-graph');
6
+ const { resolveSliceContext, toAlias } = require('./slice');
7
+
8
+ function ensureExists(filePath, message) {
9
+ if (!fs.existsSync(filePath)) {
10
+ throw new Error(message);
11
+ }
12
+ }
13
+
14
+ function walkSlices(rootDir, acc, repoRoot) {
15
+ if (!fs.existsSync(rootDir)) {
16
+ return;
17
+ }
18
+
19
+ for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
20
+ const fullPath = path.join(rootDir, entry.name);
21
+ if (entry.isDirectory()) {
22
+ walkSlices(fullPath, acc, repoRoot);
23
+ continue;
24
+ }
25
+
26
+ if (entry.isFile() && entry.name === 'slice.json' && fullPath.includes(`${path.sep}slices${path.sep}`)) {
27
+ const json = parseJsonWithComments(fs.readFileSync(fullPath, 'utf8'));
28
+ const branchName = json.git?.branch_name;
29
+ if (!branchName) {
30
+ continue;
31
+ }
32
+ acc.set(branchName, {
33
+ sliceId: json.slice_id || '',
34
+ files: Array.isArray(json.files) ? json.files : [],
35
+ });
36
+ }
37
+ }
38
+ }
39
+
40
+ function parseWorktrees(text) {
41
+ const entries = [];
42
+ const chunks = text.trim().split('\n\n').filter(Boolean);
43
+
44
+ for (const chunk of chunks) {
45
+ const entry = {};
46
+ for (const line of chunk.split('\n')) {
47
+ const idx = line.indexOf(' ');
48
+ if (idx === -1) {
49
+ continue;
50
+ }
51
+ entry[line.slice(0, idx)] = line.slice(idx + 1);
52
+ }
53
+ entries.push(entry);
54
+ }
55
+
56
+ return entries;
57
+ }
58
+
59
+ function collectOverlapWarnings(repoRoot, currentBranchName, currentFiles) {
60
+ const sliceMap = new Map();
61
+ walkSlices(path.join(repoRoot, 'specs'), sliceMap, repoRoot);
62
+ walkSlices(path.join(repoRoot, 'specs-fix'), sliceMap, repoRoot);
63
+
64
+ const worktrees = parseWorktrees(runGit(['worktree', 'list', '--porcelain'], repoRoot));
65
+ const warnings = [];
66
+
67
+ for (const entry of worktrees) {
68
+ const worktreePath = entry.worktree;
69
+ const branchRef = entry.branch || '';
70
+ const branchName = branchRef.replace('refs/heads/', '');
71
+
72
+ if (!branchName || branchName === currentBranchName || worktreePath === repoRoot) {
73
+ continue;
74
+ }
75
+
76
+ const meta = sliceMap.get(branchName);
77
+ if (!meta || meta.sliceId.startsWith('slice-00')) {
78
+ continue;
79
+ }
80
+
81
+ const dirty = statusPorcelain(worktreePath) !== '';
82
+ const aheadCount = revListCount(worktreePath, 'origin/develop..HEAD');
83
+ const active = dirty || aheadCount > 0;
84
+
85
+ if (!active) {
86
+ continue;
87
+ }
88
+
89
+ const overlap = currentFiles.filter((item) => meta.files.includes(item));
90
+ if (overlap.length > 0) {
91
+ warnings.push(`${branchName}|${overlap.join(', ')}`);
92
+ }
93
+ }
94
+
95
+ return warnings;
96
+ }
97
+
98
+ function validateDeclaredDependencyContract(repoRoot, slice) {
99
+ const declaredDependsOn = Array.isArray(slice.json.depends_on) ? slice.json.depends_on : null;
100
+ const declaredParallelSafe = typeof slice.json.parallel_safe === 'string' ? slice.json.parallel_safe.trim() : '';
101
+ const hasDependsOn = declaredDependsOn !== null;
102
+ const hasParallelSafe = declaredParallelSafe.length > 0;
103
+
104
+ if (!hasDependsOn && !hasParallelSafe) {
105
+ return;
106
+ }
107
+
108
+ const graph = buildGraph(readAllSlices(repoRoot));
109
+ const currentRef = `${slice.specSlug}/${slice.sliceId}`;
110
+ const currentNode = graph.nodes.find((node) => node.ref === currentRef);
111
+
112
+ if (!currentNode) {
113
+ throw new Error(`create-quiver: No se encontro el slice actual en el grafo: ${currentRef}`);
114
+ }
115
+
116
+ if (hasDependsOn) {
117
+ const declared = declaredDependsOn.map((dep) => String(dep).trim()).filter(Boolean);
118
+ if (declared.length !== new Set(declared).size) {
119
+ throw new Error(`create-quiver: depends_on contiene referencias duplicadas en ${currentRef}.`);
120
+ }
121
+
122
+ const currentSet = new Set(currentNode.depends_on || []);
123
+ for (const dep of declared) {
124
+ if (!currentSet.has(dep)) {
125
+ throw new Error(`create-quiver: depends_on apunta a una referencia inexistente o invalida: ${dep}`);
126
+ }
127
+ }
128
+ }
129
+
130
+ if (declaredParallelSafe === 'never') {
131
+ const reason = typeof slice.json.parallel_safe_reason === 'string' ? slice.json.parallel_safe_reason.trim() : '';
132
+ if (!reason) {
133
+ throw new Error('create-quiver: parallel_safe="never" requiere parallel_safe_reason.');
134
+ }
135
+ }
136
+
137
+ try {
138
+ // If the graph contains a cycle, topoSort will surface the path.
139
+ topoSort(graph);
140
+ } catch (error) {
141
+ if (error instanceof SliceGraphError && error.code === 'CYCLE_DETECTED') {
142
+ throw new Error(`create-quiver: El slice declarado introduce un ciclo: ${error.message}`);
143
+ }
144
+ throw error;
145
+ }
146
+ }
147
+
148
+ function checkSliceReadiness(sliceInput, options = {}) {
149
+ const gate = options.gate || 'execution';
150
+ const strictOverlap = options.strictOverlap === true;
151
+ const repoRoot = runGit(['rev-parse', '--show-toplevel'], process.cwd());
152
+ const slice = resolveSliceContext(repoRoot, sliceInput);
153
+
154
+ for (const specFile of ['SPEC.md', 'STATUS.md', 'EVIDENCE_REPORT.md']) {
155
+ ensureExists(path.join(repoRoot, slice.specDirRel, specFile), `create-quiver: falta '${slice.specDirRel}/${specFile}'.`);
156
+ }
157
+ console.log('PASS: El spec local tiene SPEC.md, STATUS.md y EVIDENCE_REPORT.md.');
158
+
159
+ if (catFileExists(repoRoot, `origin/develop:${slice.sliceRel}`)) {
160
+ console.log('PASS: El slice ya existe en origin/develop (PR base documental mergeado).');
161
+ } else if (gate === 'validation') {
162
+ 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.');
163
+ } else {
164
+ throw new Error('create-quiver: el slice no existe en origin/develop. Mergea primero el PR base documental.');
165
+ }
166
+
167
+ const overlapWarnings = collectOverlapWarnings(repoRoot, currentBranch(repoRoot), slice.files);
168
+ if (overlapWarnings.length === 0) {
169
+ console.log('PASS: No se detecto overlap con worktrees activos.');
170
+ } else {
171
+ for (const warning of overlapWarnings) {
172
+ const [overlapBranch, overlapFiles] = warning.split('|');
173
+ if (strictOverlap) {
174
+ throw new Error(`create-quiver: Overlap con worktree activo '${overlapBranch}': ${overlapFiles}`);
175
+ }
176
+ console.log(`WARN: Overlap con worktree activo '${overlapBranch}': ${overlapFiles}`);
177
+ }
178
+ }
179
+
180
+ validateDeclaredDependencyContract(repoRoot, slice);
181
+
182
+ switch (gate) {
183
+ case 'ready':
184
+ if (slice.status !== 'ready') {
185
+ 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.`);
186
+ }
187
+ console.log('PASS: Gate ready: el slice esta marcado como ready para ejecucion.');
188
+ break;
189
+ case 'execution':
190
+ if (slice.status === 'blocked') {
191
+ throw new Error('create-quiver: El slice esta bloqueado (status=blocked). Resolve el bloqueante antes de ejecutar.');
192
+ }
193
+ if (slice.status === 'cancelled') {
194
+ throw new Error('create-quiver: El slice esta cancelado (status=cancelled).');
195
+ }
196
+ if (slice.status === 'completed') {
197
+ console.log('WARN: El slice ya figura como completed. Revisa si realmente corresponde reejecutarlo.');
198
+ }
199
+ if (slice.status === 'draft') {
200
+ console.log("WARN: El slice esta en estado 'draft'. Considera marcarlo como 'ready' antes de ejecutar.");
201
+ }
202
+ console.log('PASS: Gate execution: metadata y precondiciones minimas OK.');
203
+ break;
204
+ case 'validation':
205
+ if (slice.status !== 'completed') {
206
+ throw new Error('create-quiver: Para gate validation, slice.json debe estar en status=completed.');
207
+ }
208
+ if (!slice.json.completed_at) {
209
+ throw new Error('create-quiver: Para gate validation, slice.json debe tener completed_at.');
210
+ }
211
+ if (!slice.json.started_at) {
212
+ throw new Error('create-quiver: Para gate validation, slice.json debe tener started_at.');
213
+ }
214
+ if (!slice.json.actual_hours || Number(slice.json.actual_hours) <= 0) {
215
+ throw new Error('create-quiver: Para gate validation, slice.json debe tener actual_hours > 0.');
216
+ }
217
+ console.log('PASS: Gate validation: slice marcado como completado y con trazabilidad minima.');
218
+ break;
219
+ }
220
+ }
221
+
222
+ function checkPrReadiness(sliceInput) {
223
+ const repoRoot = runGit(['rev-parse', '--show-toplevel'], process.cwd());
224
+ const scriptDir = path.dirname(__filename);
225
+ const slice = resolveSliceContext(repoRoot, sliceInput);
226
+ const current = currentBranch(repoRoot);
227
+ const prPath = path.join(path.dirname(slice.sliceAbs), 'pr.md');
228
+
229
+ checkSliceReadiness(slice.sliceAbs, { gate: 'validation' });
230
+ checkScope(slice.sliceAbs, { strict: true });
231
+
232
+ if (!slice.branchName) {
233
+ throw new Error('create-quiver: Falta git.branch_name en el slice.');
234
+ }
235
+ if (!fs.existsSync(prPath)) {
236
+ throw new Error('create-quiver: Falta pr.md junto al slice.');
237
+ }
238
+ if (current !== slice.branchName) {
239
+ throw new Error(`create-quiver: Debes ejecutar este check desde la rama del slice. Actual: ${current} Esperada: ${slice.branchName}`);
240
+ }
241
+ console.log('PASS: La rama actual coincide con la rama declarada por el slice.');
242
+ if (statusPorcelain(repoRoot) !== '') {
243
+ throw new Error('create-quiver: El worktree no esta limpio. Cerra la implementacion antes de abrir el PR.');
244
+ }
245
+ console.log('PASS: El worktree esta limpio.');
246
+
247
+ const aheadCount = revListCount(repoRoot, 'origin/develop..HEAD');
248
+ if (aheadCount <= 0) {
249
+ if (mergeBaseIsAncestor(repoRoot, 'HEAD', 'origin/develop')) {
250
+ throw new Error('create-quiver: La rama ya fue absorbida por origin/develop. Este gate aplica antes del merge.');
251
+ }
252
+ throw new Error('create-quiver: La rama no tiene commits propios respecto de origin/develop.');
253
+ }
254
+ console.log('PASS: La rama tiene commits propios contra origin/develop.');
255
+
256
+ const prText = fs.readFileSync(prPath, 'utf8');
257
+ for (const heading of ['## Title', '## Summary', '## Scope', '## Files', '## How to Test (DETAILED - REQUIRED)', '## Evidence', '## Rollback', '## Risks / Notes']) {
258
+ if (!prText.includes(heading)) {
259
+ throw new Error(`create-quiver: Falta la seccion obligatoria '${heading}' en pr.md.`);
260
+ }
261
+ }
262
+ console.log('PASS: pr.md contiene las secciones obligatorias.');
263
+
264
+ for (const subheading of ['### Required Environment', '### Worktree Access', '### Run the Project', '### Use Cases', '### Technical Verification']) {
265
+ if (!prText.includes(subheading)) {
266
+ throw new Error(`create-quiver: Falta la subseccion '${subheading}' dentro de How to Test.`);
267
+ }
268
+ }
269
+ console.log('PASS: How to Test incluye entorno, acceso al worktree, arranque, casos de uso y verificación técnica.');
270
+
271
+ if (!/#### Case [0-9]+:/.test(prText)) {
272
+ throw new Error('create-quiver: How to Test debe tener al menos un caso de uso documentado (#### Case 1: ...).');
273
+ }
274
+ console.log('PASS: Al menos un caso de uso documentado.');
275
+
276
+ if (!/git revert /.test(prText)) {
277
+ throw new Error('create-quiver: Rollback debe incluir al menos un comando git revert.');
278
+ }
279
+ console.log('PASS: Rollback incluye comando git revert.');
280
+
281
+ 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)) {
282
+ throw new Error('create-quiver: How to Test cannot rely only on generic phrases.');
283
+ }
284
+
285
+ console.log(`PASS: Gate PR listo para '${slice.sliceId}'.`);
286
+ }
287
+
288
+ function checkScope(sliceInput, options = {}) {
289
+ const strict = options.strict === true;
290
+ const repoRoot = runGit(['rev-parse', '--show-toplevel'], process.cwd());
291
+ const slice = resolveSliceContext(repoRoot, sliceInput);
292
+ const declared = slice.files;
293
+
294
+ let touchedRaw = '';
295
+ if (hasRemoteBranch(repoRoot, 'develop')) {
296
+ touchedRaw = runGit(['diff', '--name-only', 'origin/develop...HEAD'], repoRoot);
297
+ } else if (hasLocalBranch(repoRoot, 'develop')) {
298
+ touchedRaw = runGit(['diff', '--name-only', 'develop...HEAD'], repoRoot);
299
+ } else {
300
+ console.log('WARN: No se encontro rama origin/develop ni develop. Saltando check de scope.');
301
+ return;
302
+ }
303
+
304
+ if (!touchedRaw) {
305
+ console.log('WARN: No se encontraron archivos modificados respecto de develop.');
306
+ return;
307
+ }
308
+
309
+ const touched = touchedRaw.trim().split('\n').filter(Boolean);
310
+ const autoAllowed = [
311
+ /^specs\//,
312
+ /^docs\//,
313
+ /^\.worktrees\//,
314
+ /WORKTREE_CONTEXT\.md$/,
315
+ /EVIDENCE_REPORT\.md$/,
316
+ /STATUS\.md$/,
317
+ /SPEC\.md$/,
318
+ /\/pr\.md$/,
319
+ /\/slice\.json$/,
320
+ ];
321
+
322
+ const outOfScope = touched.filter((file) => {
323
+ if (declared.includes(file)) return false;
324
+ if (autoAllowed.some((re) => re.test(file))) return false;
325
+ return true;
326
+ });
327
+
328
+ if (outOfScope.length === 0) {
329
+ console.log('PASS: Todos los archivos tocados estan dentro del scope declarado en slice.json.');
330
+ return;
331
+ }
332
+
333
+ let violationCount = 0;
334
+ for (const file of outOfScope) {
335
+ violationCount += 1;
336
+ if (strict) {
337
+ throw new Error(`create-quiver: Archivo fuera del scope: ${file}`);
338
+ }
339
+ console.log(`WARN: Archivo fuera del scope: ${file}`);
340
+ }
341
+
342
+ if (violationCount > 0) {
343
+ if (strict) {
344
+ throw new Error(`${violationCount} archivo(s) fuera del scope declarado. Actualiza slice.json.files o revierte los cambios fuera de alcance.`);
345
+ }
346
+ console.log(`WARN: ${violationCount} archivo(s) fuera del scope declarado. Considera actualizar slice.json.files o revertir los cambios no previstos.`);
347
+ }
348
+ }
349
+
350
+ module.exports = {
351
+ checkPrReadiness,
352
+ checkScope,
353
+ checkSliceReadiness,
354
+ };
@@ -0,0 +1,129 @@
1
+ function escapeDotLabel(value) {
2
+ return String(value || '')
3
+ .replace(/\\/g, '\\\\')
4
+ .replace(/"/g, '\\"')
5
+ .replace(/\r?\n/g, '\\n');
6
+ }
7
+
8
+ function toConflictMap(conflicts) {
9
+ const map = new Map();
10
+
11
+ for (const conflict of Array.isArray(conflicts) ? conflicts : []) {
12
+ for (const ref of Array.isArray(conflict.slices) ? conflict.slices : []) {
13
+ map.set(ref, {
14
+ files: Array.isArray(conflict.files) ? conflict.files : [],
15
+ slices: Array.isArray(conflict.slices) ? conflict.slices : [],
16
+ });
17
+ }
18
+ }
19
+
20
+ return map;
21
+ }
22
+
23
+ function flattenVisibleNodes(report) {
24
+ const nodes = [];
25
+
26
+ for (const level of Array.isArray(report.levels) ? report.levels : []) {
27
+ for (const node of Array.isArray(level) ? level : []) {
28
+ nodes.push(node);
29
+ }
30
+ }
31
+
32
+ nodes.sort((left, right) => String(left.ref || '').localeCompare(String(right.ref || '')));
33
+ return nodes;
34
+ }
35
+
36
+ function buildNodeIds(nodes) {
37
+ const used = new Set();
38
+ const ids = new Map();
39
+
40
+ for (const node of nodes) {
41
+ const base = `n_${String(node.ref || '').replace(/[^a-zA-Z0-9_]/g, '_') || 'slice'}`;
42
+ let candidate = base;
43
+ let suffix = 2;
44
+
45
+ while (used.has(candidate)) {
46
+ candidate = `${base}_${suffix}`;
47
+ suffix += 1;
48
+ }
49
+
50
+ used.add(candidate);
51
+ ids.set(node.ref, candidate);
52
+ }
53
+
54
+ return ids;
55
+ }
56
+
57
+ function buildLabel(node, conflict, options = {}) {
58
+ const parts = [];
59
+ const conflictPrefix = conflict ? (options.unicode ? '⚠ ' : '! ') : '';
60
+ parts.push(`${conflictPrefix}${node.ref}`);
61
+
62
+ if (node.title && node.title !== node.slice_id && node.title !== node.ref) {
63
+ parts.push(node.title);
64
+ }
65
+
66
+ if (typeof node.hours === 'number') {
67
+ parts.push(`${node.hours}h`);
68
+ }
69
+
70
+ if (node.status) {
71
+ parts.push(`[${node.status}]`);
72
+ }
73
+
74
+ if (options.showConflicts && conflict && Array.isArray(conflict.files) && conflict.files.length > 0) {
75
+ parts.push(`Files: ${conflict.files.join(', ')}`);
76
+ }
77
+
78
+ return escapeDotLabel(parts.join('\n'));
79
+ }
80
+
81
+ function renderDotGraph(report, options = {}) {
82
+ const nodes = flattenVisibleNodes(report);
83
+ if (nodes.length === 0) {
84
+ return 'digraph QuiverGraph {\n // No pending slices found.\n}\n';
85
+ }
86
+
87
+ const nodeIds = buildNodeIds(nodes);
88
+ const nodeRefs = new Set(nodes.map((node) => node.ref));
89
+ const conflictMap = toConflictMap(report.conflicts);
90
+ const lines = [
91
+ 'digraph QuiverGraph {',
92
+ ' rankdir=TB;',
93
+ ' node [shape=box, style="rounded,filled", fillcolor="#f8f9fa", color="#495057", fontname="Helvetica"];',
94
+ ' edge [color="#6c757d"];',
95
+ ];
96
+
97
+ for (const node of nodes) {
98
+ const conflict = conflictMap.get(node.ref);
99
+ const id = nodeIds.get(node.ref);
100
+ const label = buildLabel(node, conflict, options);
101
+ const attrs = [`label="${label}"`];
102
+
103
+ if (conflict) {
104
+ attrs.push(`fillcolor="${options.showConflicts ? '#fff3cd' : '#f8f9fa'}"`);
105
+ attrs.push(`color="${options.showConflicts ? '#d39e00' : '#495057'}"`);
106
+ }
107
+
108
+ lines.push(` ${id} [${attrs.join(', ')}];`);
109
+ }
110
+
111
+ for (const node of nodes) {
112
+ const targetId = nodeIds.get(node.ref);
113
+ const deps = Array.isArray(node.depends_on) ? node.depends_on : [];
114
+ for (const dep of deps) {
115
+ if (!nodeRefs.has(dep)) {
116
+ continue;
117
+ }
118
+ const sourceId = nodeIds.get(dep);
119
+ lines.push(` ${sourceId} -> ${targetId};`);
120
+ }
121
+ }
122
+
123
+ lines.push('}');
124
+ return `${lines.join('\n')}\n`;
125
+ }
126
+
127
+ module.exports = {
128
+ renderDotGraph,
129
+ };
@@ -0,0 +1,119 @@
1
+ function escapeMermaidLabel(value) {
2
+ return String(value || '')
3
+ .replace(/&/g, '&amp;')
4
+ .replace(/</g, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/"/g, '&quot;')
7
+ .replace(/\r?\n/g, '<br/>');
8
+ }
9
+
10
+ function toConflictMap(conflicts) {
11
+ const map = new Map();
12
+
13
+ for (const conflict of Array.isArray(conflicts) ? conflicts : []) {
14
+ for (const ref of Array.isArray(conflict.slices) ? conflict.slices : []) {
15
+ map.set(ref, {
16
+ files: Array.isArray(conflict.files) ? conflict.files : [],
17
+ slices: Array.isArray(conflict.slices) ? conflict.slices : [],
18
+ });
19
+ }
20
+ }
21
+
22
+ return map;
23
+ }
24
+
25
+ function flattenVisibleNodes(report) {
26
+ const nodes = [];
27
+
28
+ for (const level of Array.isArray(report.levels) ? report.levels : []) {
29
+ for (const node of Array.isArray(level) ? level : []) {
30
+ nodes.push(node);
31
+ }
32
+ }
33
+
34
+ nodes.sort((left, right) => String(left.ref || '').localeCompare(String(right.ref || '')));
35
+ return nodes;
36
+ }
37
+
38
+ function buildNodeIds(nodes) {
39
+ const used = new Set();
40
+ const ids = new Map();
41
+
42
+ for (const node of nodes) {
43
+ const base = `n_${String(node.ref || '').replace(/[^a-zA-Z0-9_]/g, '_') || 'slice'}`;
44
+ let candidate = base;
45
+ let suffix = 2;
46
+
47
+ while (used.has(candidate)) {
48
+ candidate = `${base}_${suffix}`;
49
+ suffix += 1;
50
+ }
51
+
52
+ used.add(candidate);
53
+ ids.set(node.ref, candidate);
54
+ }
55
+
56
+ return ids;
57
+ }
58
+
59
+ function buildLabel(node, conflict, options = {}) {
60
+ const parts = [];
61
+ const conflictPrefix = conflict ? (options.unicode ? '⚠ ' : '! ') : '';
62
+ parts.push(`${conflictPrefix}${node.ref}`);
63
+
64
+ if (node.title && node.title !== node.slice_id && node.title !== node.ref) {
65
+ parts.push(node.title);
66
+ }
67
+
68
+ if (typeof node.hours === 'number') {
69
+ parts.push(`${node.hours}h`);
70
+ }
71
+
72
+ if (node.status) {
73
+ parts.push(`[${node.status}]`);
74
+ }
75
+
76
+ if (options.showConflicts && conflict && Array.isArray(conflict.files) && conflict.files.length > 0) {
77
+ parts.push(`Files: ${conflict.files.join(', ')}`);
78
+ }
79
+
80
+ return escapeMermaidLabel(parts.join('\n'));
81
+ }
82
+
83
+ function renderMermaidGraph(report, options = {}) {
84
+ const nodes = flattenVisibleNodes(report);
85
+ if (nodes.length === 0) {
86
+ return '```mermaid\nflowchart TD\n %% No pending slices found.\n```\n';
87
+ }
88
+
89
+ const nodeIds = buildNodeIds(nodes);
90
+ const nodeRefs = new Set(nodes.map((node) => node.ref));
91
+ const conflictMap = toConflictMap(report.conflicts);
92
+ const lines = ['```mermaid', 'flowchart TD'];
93
+
94
+ for (const node of nodes) {
95
+ const conflict = conflictMap.get(node.ref);
96
+ const id = nodeIds.get(node.ref);
97
+ const label = buildLabel(node, conflict, options);
98
+ lines.push(` ${id}["${label}"]`);
99
+ }
100
+
101
+ for (const node of nodes) {
102
+ const targetId = nodeIds.get(node.ref);
103
+ const deps = Array.isArray(node.depends_on) ? node.depends_on : [];
104
+ for (const dep of deps) {
105
+ if (!nodeRefs.has(dep)) {
106
+ continue;
107
+ }
108
+ const sourceId = nodeIds.get(dep);
109
+ lines.push(` ${sourceId} --> ${targetId}`);
110
+ }
111
+ }
112
+
113
+ lines.push('```');
114
+ return `${lines.join('\n')}\n`;
115
+ }
116
+
117
+ module.exports = {
118
+ renderMermaidGraph,
119
+ };