create-quiver 0.7.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.
- package/.claude/settings.local.json +45 -0
- package/.github/workflows/ci.yml +2 -2
- package/BACKLOG.md +139 -0
- package/CHANGELOG.md +13 -1
- package/README.md +31 -1
- package/README_FOR_AI.md +25 -13
- package/ROADMAP.md +28 -6
- package/docs/AI_ONBOARDING_PROMPT.md.template +4 -0
- package/docs/COMMANDS.md.template +25 -0
- package/docs/INDEX.md.template +2 -0
- package/docs/SUPPORT_MATRIX.md.template +9 -0
- package/docs/WORKFLOW.md.template +4 -0
- package/docs/examples/graph.md.template +62 -0
- package/docs/examples/next.md.template +27 -0
- package/docs/examples/plan.md.template +28 -0
- package/package.json +5 -2
- package/package.template.json +8 -3
- package/scripts/check-slice-readiness.sh +6 -4
- package/scripts/init-docs.sh +57 -9
- package/specs/[project-name]/HANDOFF.md.template +37 -0
- package/specs/[project-name]/slices/slice-template/slice.json +5 -0
- package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-01-project-scan-command/slice.json +1 -1
- package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-02-ai-onboarding-prompt/slice.json +1 -1
- package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-03-doctor-readme-adoption-flow/slice.json +1 -1
- package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-01-cross-platform-support-contract/slice.json +1 -1
- package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-02-node-init-docs-runtime/slice.json +1 -1
- package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-03-node-migrate-analyze-doctor-flow/slice.json +1 -1
- package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-04-node-slice-lifecycle-commands/slice.json +1 -1
- package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-05-generated-project-scripts-and-migration/slice.json +1 -1
- package/specs/quiver-v13-token-efficient-ai-context/EVIDENCE_REPORT.md +1 -1
- package/specs/quiver-v13-token-efficient-ai-context/SPEC.md +1 -1
- package/specs/quiver-v15-init-required-before-migrate/EVIDENCE_REPORT.md +26 -0
- package/specs/quiver-v15-init-required-before-migrate/SPEC.md +66 -0
- package/specs/quiver-v15-init-required-before-migrate/STATUS.md +26 -0
- package/specs/quiver-v15-init-required-before-migrate/slices/slice-01-migrate-initialization-precondition/slice.json +65 -0
- package/specs/quiver-v15-init-required-before-migrate/slices/slice-02-doctor-not-initialized-guidance/slice.json +61 -0
- package/specs/quiver-v15-init-required-before-migrate/slices/slice-03-docs-smokes-init-before-migrate/slice.json +64 -0
- package/specs/quiver-v16-handoff-contract/EVIDENCE_REPORT.md +26 -0
- package/specs/quiver-v16-handoff-contract/SPEC.md +68 -0
- package/specs/quiver-v16-handoff-contract/STATUS.md +26 -0
- package/specs/quiver-v16-handoff-contract/slices/slice-01-handoff-template-and-contract/slice.json +66 -0
- package/specs/quiver-v16-handoff-contract/slices/slice-02-check-handoff-command/slice.json +70 -0
- package/specs/quiver-v16-handoff-contract/slices/slice-03-handoff-scaffold-optional/slice.json +67 -0
- package/specs/quiver-v17-orchestration-foundation/EVIDENCE_REPORT.md +32 -0
- package/specs/quiver-v17-orchestration-foundation/SPEC.md +79 -0
- package/specs/quiver-v17-orchestration-foundation/STATUS.md +31 -0
- package/specs/quiver-v17-orchestration-foundation/slices/slice-01-ci-matrix-verified/slice.json +68 -0
- package/specs/quiver-v17-orchestration-foundation/slices/slice-02-slice-graph-library/slice.json +65 -0
- package/specs/quiver-v17-orchestration-foundation/slices/slice-03-depends-on-validation/slice.json +72 -0
- package/specs/quiver-v18-slice-orchestration/EVIDENCE_REPORT.md +38 -0
- package/specs/quiver-v18-slice-orchestration/SPEC.md +91 -0
- package/specs/quiver-v18-slice-orchestration/STATUS.md +33 -0
- package/specs/quiver-v18-slice-orchestration/slices/slice-01-plan-command/slice.json +79 -0
- package/specs/quiver-v18-slice-orchestration/slices/slice-02-graph-mvp-tree/slice.json +75 -0
- package/specs/quiver-v18-slice-orchestration/slices/slice-03-graph-extended-formats/slice.json +70 -0
- package/specs/quiver-v18-slice-orchestration/slices/slice-04-next-command/slice.json +73 -0
- package/specs/quiver-v18-stabilization/EVIDENCE_REPORT.md +26 -0
- package/specs/quiver-v18-stabilization/SPEC.md +62 -0
- package/specs/quiver-v18-stabilization/STATUS.md +30 -0
- package/specs/quiver-v18-stabilization/slices/slice-01-fix-legacy-dependency-resolution/CLOSURE_BRIEF.md +29 -0
- package/specs/quiver-v18-stabilization/slices/slice-01-fix-legacy-dependency-resolution/EXECUTION_BRIEF.md +134 -0
- package/specs/quiver-v18-stabilization/slices/slice-01-fix-legacy-dependency-resolution/slice.json +56 -0
- package/specs/quiver-v18-stabilization/slices/slice-02-roadmap-and-branch-cleanup/CLOSURE_BRIEF.md +29 -0
- package/specs/quiver-v18-stabilization/slices/slice-02-roadmap-and-branch-cleanup/EXECUTION_BRIEF.md +118 -0
- package/specs/quiver-v18-stabilization/slices/slice-02-roadmap-and-branch-cleanup/slice.json +57 -0
- package/specs/quiver-v18-stabilization/slices/slice-03-publish-drafts-branch/CLOSURE_BRIEF.md +23 -0
- package/specs/quiver-v18-stabilization/slices/slice-03-publish-drafts-branch/EXECUTION_BRIEF.md +73 -0
- package/specs/quiver-v18-stabilization/slices/slice-03-publish-drafts-branch/slice.json +49 -0
- package/src/create-quiver/commands/graph.js +97 -0
- package/src/create-quiver/commands/next.js +134 -0
- package/src/create-quiver/commands/plan.js +205 -0
- package/src/create-quiver/index.js +179 -2
- package/src/create-quiver/lib/handoff.js +104 -0
- package/src/create-quiver/lib/init-docs.js +71 -13
- package/src/create-quiver/lib/json.js +14 -0
- package/src/create-quiver/lib/lifecycle.js +3 -2
- package/src/create-quiver/lib/readiness.js +55 -1
- package/src/create-quiver/lib/renderers/dot.js +129 -0
- package/src/create-quiver/lib/renderers/mermaid.js +119 -0
- package/src/create-quiver/lib/renderers/tree.js +116 -0
- package/src/create-quiver/lib/slice-graph.js +453 -0
- package/src/create-quiver/lib/slice.js +2 -1
- package/src/create-quiver/lib/state.js +50 -0
package/specs/quiver-v18-stabilization/slices/slice-03-publish-drafts-branch/EXECUTION_BRIEF.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# EXECUTION BRIEF — slice-03: Publicar rama de drafts
|
|
2
|
+
|
|
3
|
+
**Spec:** quiver-v18-stabilization
|
|
4
|
+
**Slice:** slice-03-publish-drafts-branch
|
|
5
|
+
**Estimated time:** 5 min
|
|
6
|
+
**Sin PR** — es un `git push` directo de una rama de referencia.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## ⛔ Gate obligatoria — leer antes de ejecutar cualquier cosa
|
|
11
|
+
|
|
12
|
+
Este slice está **bloqueado** hasta que el mantenedor confirme explícitamente:
|
|
13
|
+
|
|
14
|
+
1. Usé `quiver:plan`, `quiver:graph`, y `quiver:next` en al menos un ciclo real de trabajo.
|
|
15
|
+
2. Registré una observación en `specs/quiver-v18-slice-orchestration/EVIDENCE_REPORT.md`.
|
|
16
|
+
3. Confirmo que el checkpoint de v18 pasó.
|
|
17
|
+
|
|
18
|
+
**Si esa confirmación no está en el prompt que te delega este slice, abortá y pedila.**
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Contexto
|
|
23
|
+
|
|
24
|
+
La rama `drafts/v19-v22-orchestration-followups` existe solo localmente. Contiene 23 archivos (4 specs: v19–v22) en un único commit. ROADMAP.md y BACKLOG.md ya hacen referencia a esta rama por nombre. Publicarla en origin completa la cadena de trazabilidad y protege el trabajo ante pérdida local.
|
|
25
|
+
|
|
26
|
+
No es una rama candidata a merge. Es una rama de referencia que vive indefinidamente hasta que alguna de las specs se promueva formalmente.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Verificación previa
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Confirmar que la rama existe y tiene el commit correcto
|
|
34
|
+
git log drafts/v19-v22-orchestration-followups --oneline -1
|
|
35
|
+
# Esperás: 13eab96 docs(drafts): park v19-v22 spec drafts pending v18 checkpoint
|
|
36
|
+
|
|
37
|
+
# 2. Confirmar que NO existe ya en origin
|
|
38
|
+
git ls-remote origin drafts/v19-v22-orchestration-followups
|
|
39
|
+
# Esperás: salida vacía (si retorna algo, la rama ya fue publicada — slice completado)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Ejecución
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
GIT_SSH_COMMAND="ssh -i ~/ssh/github-personal" \
|
|
48
|
+
git push origin drafts/v19-v22-orchestration-followups
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Verificación post-push
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Confirmar que el SHA remoto coincide con el local
|
|
57
|
+
LOCAL_SHA=$(git rev-parse drafts/v19-v22-orchestration-followups)
|
|
58
|
+
REMOTE_SHA=$(git ls-remote origin drafts/v19-v22-orchestration-followups | awk '{print $1}')
|
|
59
|
+
|
|
60
|
+
echo "Local: $LOCAL_SHA"
|
|
61
|
+
echo "Remote: $REMOTE_SHA"
|
|
62
|
+
|
|
63
|
+
[ "$LOCAL_SHA" = "$REMOTE_SHA" ] && echo "OK" || echo "MISMATCH — investigar"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Restricciones
|
|
69
|
+
|
|
70
|
+
- **No** abras un PR para esta rama.
|
|
71
|
+
- **No** hagas `--force` en ninguna circunstancia.
|
|
72
|
+
- **No** edites ningún archivo en la rama antes de publicarla.
|
|
73
|
+
- **No** ejecutes este slice si el gate no fue confirmado explícitamente.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"slice_id": "slice-03-publish-drafts-branch",
|
|
3
|
+
"ticket": "QUIVER-03",
|
|
4
|
+
"type": "chore",
|
|
5
|
+
"title": "Publish `drafts/v19-v22-orchestration-followups` to origin",
|
|
6
|
+
"objective": "Push the locally-parked v19–v22 draft specs to origin so they are preserved in the remote in case of local machine loss and are accessible for future promotion.",
|
|
7
|
+
"description": "The branch `drafts/v19-v22-orchestration-followups` exists only locally. It contains 23 files across 4 spec directories (v19 Project Visibility, v20 Context Diagnostics, v21 Slice Archaeology, v22 Deferred Tooling) committed in a single commit. ROADMAP.md and BACKLOG.md already reference this branch by name. Pushing it to origin completes the traceability chain. This slice has no PR — it is a single `git push` operation. It is gated by the human checkpoint for v18: the maintainer must confirm that `plan`, `graph`, and `next` were used in real work before this slice is executed.",
|
|
8
|
+
"git": {
|
|
9
|
+
"branch_type": "chore",
|
|
10
|
+
"base_branch": null,
|
|
11
|
+
"branch_slug": "publish-drafts-v19-v22",
|
|
12
|
+
"branch_name": "drafts/v19-v22-orchestration-followups"
|
|
13
|
+
},
|
|
14
|
+
"must": [
|
|
15
|
+
"Confirm the human checkpoint for v18 was explicitly declared by the maintainer before executing",
|
|
16
|
+
"Run `git log drafts/v19-v22-orchestration-followups --oneline -1` and confirm the expected SHA is present",
|
|
17
|
+
"Push with: `GIT_SSH_COMMAND=\"ssh -i ~/ssh/github-personal\" git push origin drafts/v19-v22-orchestration-followups`",
|
|
18
|
+
"Verify with: `git ls-remote origin drafts/v19-v22-orchestration-followups` and confirm the SHA matches local"
|
|
19
|
+
],
|
|
20
|
+
"not_included": [
|
|
21
|
+
"Opening a PR for the drafts branch — it is a reference branch, not a merge candidate",
|
|
22
|
+
"Promoting any of v19–v22 to active spec — that is a separate decision after the checkpoint",
|
|
23
|
+
"Modifying any file in the drafts branch",
|
|
24
|
+
"Any other git push"
|
|
25
|
+
],
|
|
26
|
+
"acceptance": [
|
|
27
|
+
"Maintainer has explicitly confirmed the v18 human checkpoint",
|
|
28
|
+
"`git ls-remote origin drafts/v19-v22-orchestration-followups` returns a non-empty SHA",
|
|
29
|
+
"The returned SHA matches `git rev-parse drafts/v19-v22-orchestration-followups`"
|
|
30
|
+
],
|
|
31
|
+
"files": [],
|
|
32
|
+
"tests": [
|
|
33
|
+
"git ls-remote origin drafts/v19-v22-orchestration-followups",
|
|
34
|
+
"git rev-parse drafts/v19-v22-orchestration-followups"
|
|
35
|
+
],
|
|
36
|
+
"documentation": [],
|
|
37
|
+
"depends_on": [],
|
|
38
|
+
"assumptions": [
|
|
39
|
+
"The SSH key at `~/ssh/github-personal` has push access to `FabriJuncal/quiver`",
|
|
40
|
+
"The local branch `drafts/v19-v22-orchestration-followups` has not been rebased or force-reset since creation"
|
|
41
|
+
],
|
|
42
|
+
"estimated_hours": 0.1,
|
|
43
|
+
"actual_hours": 0.1,
|
|
44
|
+
"status": "completed",
|
|
45
|
+
"blocked_reason": null,
|
|
46
|
+
"ready_at": "2026-05-12T00:00:00Z",
|
|
47
|
+
"started_at": "2026-05-12T00:00:00Z",
|
|
48
|
+
"completed_at": "2026-05-12T00:00:00Z"
|
|
49
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const { buildGraph, computeLevels, detectFileConflicts, readAllSlices } = require('../lib/slice-graph');
|
|
2
|
+
const { renderDotGraph } = require('../lib/renderers/dot');
|
|
3
|
+
const { renderMermaidGraph } = require('../lib/renderers/mermaid');
|
|
4
|
+
const { renderTreeGraph, isUnicodeEnabled } = require('../lib/renderers/tree');
|
|
5
|
+
|
|
6
|
+
const EXCLUDED_STATUSES = new Set(['completed', 'skipped', 'cancelled']);
|
|
7
|
+
|
|
8
|
+
function toGraphNode(node) {
|
|
9
|
+
return {
|
|
10
|
+
ref: node.ref,
|
|
11
|
+
spec_slug: node.specSlug,
|
|
12
|
+
slice_id: node.sliceId,
|
|
13
|
+
title: node.title || node.sliceId,
|
|
14
|
+
hours: Number.isFinite(Number(node.json?.estimated_hours)) ? Number(node.json.estimated_hours) : 0,
|
|
15
|
+
status: node.status || 'draft',
|
|
16
|
+
files: Array.isArray(node.files) ? node.files : [],
|
|
17
|
+
depends_on: Array.isArray(node.depends_on) ? node.depends_on : [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildConflictPayload(levelIndex, groups) {
|
|
22
|
+
return groups.map((group) => ({
|
|
23
|
+
level: levelIndex,
|
|
24
|
+
files: group.files,
|
|
25
|
+
slices: group.slices,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function collectGraph(repoRoot, options = {}) {
|
|
30
|
+
const graph = buildGraph(readAllSlices(repoRoot));
|
|
31
|
+
computeLevels(graph);
|
|
32
|
+
const pendingNodes = graph.nodes.filter((node) => !EXCLUDED_STATUSES.has(String(node.status || '').toLowerCase()));
|
|
33
|
+
const pendingNodeRefs = new Set(pendingNodes.map((node) => node.ref));
|
|
34
|
+
const pendingEdges = graph.edges.filter((edge) => pendingNodeRefs.has(edge.from) && pendingNodeRefs.has(edge.to));
|
|
35
|
+
const levels = pendingNodes.length > 0 ? computeLevels({ nodes: pendingNodes, edges: pendingEdges, cycles: [] }) : [];
|
|
36
|
+
const levelEntries = levels.map((level, levelIndex) => {
|
|
37
|
+
const conflictGroups = detectFileConflicts(level);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
index: levelIndex,
|
|
41
|
+
slices: level.map(toGraphNode),
|
|
42
|
+
conflicts: buildConflictPayload(levelIndex, conflictGroups),
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const filteredLevels = typeof options.level === 'number'
|
|
47
|
+
? levelEntries.filter((entry) => entry.index === options.level)
|
|
48
|
+
: levelEntries;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
levels: filteredLevels.map((entry) => entry.slices),
|
|
52
|
+
conflicts: filteredLevels.flatMap((entry) => entry.conflicts),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatHumanGraph(report, options = {}) {
|
|
57
|
+
const format = options.format || 'tree';
|
|
58
|
+
const sharedOptions = {
|
|
59
|
+
showConflicts: options.showConflicts === true,
|
|
60
|
+
unicode: options.unicode === true || isUnicodeEnabled(options),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (format === 'tree') {
|
|
64
|
+
return renderTreeGraph(report, sharedOptions);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (format === 'mermaid') {
|
|
68
|
+
return renderMermaidGraph(report, sharedOptions);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (format === 'dot') {
|
|
72
|
+
return renderDotGraph(report, sharedOptions);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw new Error(`create-quiver: unsupported graph format: ${format}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function runGraph(repoRoot, options = {}) {
|
|
79
|
+
if (options.format && !['tree', 'mermaid', 'dot'].includes(options.format)) {
|
|
80
|
+
throw new Error(`create-quiver: unsupported graph format: ${options.format}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const report = collectGraph(repoRoot, options);
|
|
84
|
+
if (options.json) {
|
|
85
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
86
|
+
return report;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
process.stdout.write(formatHumanGraph(report, options));
|
|
90
|
+
return report;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
collectGraph,
|
|
95
|
+
formatHumanGraph,
|
|
96
|
+
runGraph,
|
|
97
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const readline = require('readline');
|
|
3
|
+
const { collectPlan } = require('./plan');
|
|
4
|
+
const { startSlice } = require('../lib/lifecycle');
|
|
5
|
+
|
|
6
|
+
function toStartSliceCommand(slicePath) {
|
|
7
|
+
return `npx create-quiver start-slice "${slicePath}"`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function collectNext(repoRoot, options = {}) {
|
|
11
|
+
const report = collectPlan(repoRoot, {
|
|
12
|
+
onlyReady: true,
|
|
13
|
+
specSlug: options.specSlug,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const allReady = report.plan.filter((item) => String(item.status || '').toLowerCase() !== 'blocked');
|
|
17
|
+
const next = allReady.length > 0 ? allReady[0] : null;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
all_ready: allReady,
|
|
21
|
+
next,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatHumanNext(report, options = {}) {
|
|
26
|
+
const lines = [options.allReady ? 'Ready slices' : 'Next ready slice'];
|
|
27
|
+
|
|
28
|
+
if (!report.next) {
|
|
29
|
+
lines.push('No ready slices found.');
|
|
30
|
+
return `${lines.join('\n')}\n`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const items = options.allReady ? report.all_ready : [report.next];
|
|
34
|
+
|
|
35
|
+
for (const [index, item] of items.entries()) {
|
|
36
|
+
const command = toStartSliceCommand(item.slice_path);
|
|
37
|
+
if (options.allReady) {
|
|
38
|
+
lines.push('');
|
|
39
|
+
lines.push(`[${index + 1}] ${item.ref}`);
|
|
40
|
+
lines.push(`Title: ${item.title}`);
|
|
41
|
+
lines.push(`Status: ${item.status}`);
|
|
42
|
+
lines.push(`Start: ${command}`);
|
|
43
|
+
} else {
|
|
44
|
+
lines.push(`Slice: ${item.ref}`);
|
|
45
|
+
lines.push(`Title: ${item.title}`);
|
|
46
|
+
lines.push(`Status: ${item.status}`);
|
|
47
|
+
lines.push(`Start: ${command}`);
|
|
48
|
+
if (report.all_ready.length > 1) {
|
|
49
|
+
lines.push(`Also ready: ${report.all_ready.slice(1).map((candidate) => candidate.ref).join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return `${lines.join('\n')}\n`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function promptConfirm(message, io = {}) {
|
|
58
|
+
const input = io.input || process.stdin;
|
|
59
|
+
const output = io.output || process.stdout;
|
|
60
|
+
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const rl = readline.createInterface({
|
|
63
|
+
input,
|
|
64
|
+
output,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
68
|
+
rl.close();
|
|
69
|
+
resolve(/^y(es)?$/i.test(String(answer || '').trim()));
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
rl.on('SIGINT', () => {
|
|
73
|
+
rl.close();
|
|
74
|
+
reject(new Error('create-quiver: confirmation aborted.'));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function runNext(repoRoot, options = {}) {
|
|
80
|
+
const report = collectNext(repoRoot, options);
|
|
81
|
+
const isTTY = options.isTTY || {
|
|
82
|
+
stdin: Boolean(process.stdin.isTTY),
|
|
83
|
+
stdout: Boolean(process.stdout.isTTY),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (options.autoStart) {
|
|
87
|
+
if (!isTTY.stdin || !isTTY.stdout) {
|
|
88
|
+
throw new Error('create-quiver: --auto-start requires an interactive TTY.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!report.next) {
|
|
92
|
+
process.stdout.write('No ready slices found.\n');
|
|
93
|
+
return report;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const confirmed = await (options.promptConfirm || promptConfirm)(`Start slice ${report.next.ref}?`);
|
|
97
|
+
if (confirmed) {
|
|
98
|
+
const slicePath = path.resolve(repoRoot, report.next.slice_path);
|
|
99
|
+
await Promise.resolve((options.startSliceFn || startSlice)(slicePath, { allowDraft: true }));
|
|
100
|
+
process.stdout.write(`Started: ${report.next.ref}\n`);
|
|
101
|
+
} else {
|
|
102
|
+
process.stdout.write('Aborted.\n');
|
|
103
|
+
}
|
|
104
|
+
return report;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (options.json) {
|
|
108
|
+
const payload = {
|
|
109
|
+
all_ready: report.all_ready.map((item) => ({
|
|
110
|
+
...item,
|
|
111
|
+
start_slice_command: toStartSliceCommand(item.slice_path),
|
|
112
|
+
})),
|
|
113
|
+
next: report.next
|
|
114
|
+
? {
|
|
115
|
+
...report.next,
|
|
116
|
+
start_slice_command: toStartSliceCommand(report.next.slice_path),
|
|
117
|
+
}
|
|
118
|
+
: null,
|
|
119
|
+
};
|
|
120
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
121
|
+
return payload;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
process.stdout.write(formatHumanNext(report, options));
|
|
125
|
+
return report;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = {
|
|
129
|
+
collectNext,
|
|
130
|
+
formatHumanNext,
|
|
131
|
+
promptConfirm,
|
|
132
|
+
runNext,
|
|
133
|
+
toStartSliceCommand,
|
|
134
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { relativePosixPath } = require('../lib/paths');
|
|
4
|
+
const { buildGraph, readAllSlices, topoSort } = require('../lib/slice-graph');
|
|
5
|
+
|
|
6
|
+
const EXCLUDED_STATUSES = new Set(['completed', 'skipped', 'cancelled']);
|
|
7
|
+
|
|
8
|
+
function toHourCount(value) {
|
|
9
|
+
const parsed = Number(value);
|
|
10
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function compareRefs(left, right) {
|
|
14
|
+
return String(left || '').localeCompare(String(right || ''));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sliceToPlanItem(repoRoot, slice, index, readySet) {
|
|
18
|
+
return {
|
|
19
|
+
index,
|
|
20
|
+
ref: slice.ref,
|
|
21
|
+
spec_family: slice.specFamily || '',
|
|
22
|
+
spec_slug: slice.specSlug,
|
|
23
|
+
slice_id: slice.sliceId,
|
|
24
|
+
slice_path: relativePosixPath(repoRoot, slice.slicePath),
|
|
25
|
+
ticket: slice.ticket || '',
|
|
26
|
+
title: slice.title || slice.sliceId,
|
|
27
|
+
status: slice.status || 'draft',
|
|
28
|
+
hours: toHourCount(slice.json?.estimated_hours),
|
|
29
|
+
files: Array.isArray(slice.files) ? slice.files : [],
|
|
30
|
+
depends_on: Array.isArray(slice.depends_on) ? slice.depends_on : [],
|
|
31
|
+
ready: readySet.has(slice.ref),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildSubgraph(graph, refs) {
|
|
36
|
+
const refSet = new Set(refs);
|
|
37
|
+
return {
|
|
38
|
+
nodes: graph.nodes.filter((node) => refSet.has(node.ref)),
|
|
39
|
+
edges: graph.edges.filter((edge) => refSet.has(edge.from) && refSet.has(edge.to)),
|
|
40
|
+
cycles: graph.cycles.filter((cycle) => cycle.every((ref) => refSet.has(ref))),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildReadySet(graph, pendingRefs) {
|
|
45
|
+
const pendingSet = new Set(pendingRefs);
|
|
46
|
+
const incomingCounts = new Map(pendingRefs.map((ref) => [ref, 0]));
|
|
47
|
+
|
|
48
|
+
for (const edge of graph.edges) {
|
|
49
|
+
if (!pendingSet.has(edge.from) || !pendingSet.has(edge.to)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
incomingCounts.set(edge.to, (incomingCounts.get(edge.to) || 0) + 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return new Set(Array.from(incomingCounts.entries()).filter(([, count]) => count === 0).map(([ref]) => ref));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildCriticalPath(graph, refs) {
|
|
59
|
+
if (refs.length === 0) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const subgraph = buildSubgraph(graph, refs);
|
|
64
|
+
const ordered = topoSort(subgraph);
|
|
65
|
+
const incoming = new Map(ordered.map((node) => [node.ref, []]));
|
|
66
|
+
|
|
67
|
+
for (const edge of subgraph.edges) {
|
|
68
|
+
if (!incoming.has(edge.to)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
incoming.get(edge.to).push(edge.from);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const best = new Map();
|
|
75
|
+
|
|
76
|
+
for (const node of ordered) {
|
|
77
|
+
const hours = toHourCount(node.json?.estimated_hours);
|
|
78
|
+
const predecessors = incoming.get(node.ref) || [];
|
|
79
|
+
let candidate = {
|
|
80
|
+
hours,
|
|
81
|
+
path: [node.ref],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (const pred of predecessors) {
|
|
85
|
+
const prev = best.get(pred);
|
|
86
|
+
if (!prev) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const nextCandidate = {
|
|
90
|
+
hours: prev.hours + hours,
|
|
91
|
+
path: [...prev.path, node.ref],
|
|
92
|
+
};
|
|
93
|
+
const pathLabel = nextCandidate.path.join('>');
|
|
94
|
+
const bestLabel = candidate.path.join('>');
|
|
95
|
+
if (nextCandidate.hours > candidate.hours || (nextCandidate.hours === candidate.hours && compareRefs(pathLabel, bestLabel) < 0)) {
|
|
96
|
+
candidate = nextCandidate;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
best.set(node.ref, candidate);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let winner = null;
|
|
104
|
+
for (const node of ordered) {
|
|
105
|
+
const candidate = best.get(node.ref);
|
|
106
|
+
if (!candidate) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (!winner || candidate.hours > winner.hours || (candidate.hours === winner.hours && compareRefs(candidate.path.join('>'), winner.path.join('>')) < 0)) {
|
|
110
|
+
winner = candidate;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return winner ? winner.path : [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function collectPlan(repoRoot, options = {}) {
|
|
118
|
+
const allSlices = readAllSlices(repoRoot);
|
|
119
|
+
const graph = buildGraph(allSlices);
|
|
120
|
+
const topo = topoSort(graph);
|
|
121
|
+
const excluded = EXCLUDED_STATUSES;
|
|
122
|
+
const specSlug = options.specSlug ? String(options.specSlug).trim() : '';
|
|
123
|
+
|
|
124
|
+
const pendingRefs = new Set(
|
|
125
|
+
graph.nodes
|
|
126
|
+
.filter((node) => !excluded.has(String(node.status || '').toLowerCase()))
|
|
127
|
+
.map((node) => node.ref),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const readyRefs = buildReadySet(graph, Array.from(pendingRefs));
|
|
131
|
+
|
|
132
|
+
const selectedNodes = topo.filter((node) => {
|
|
133
|
+
if (!pendingRefs.has(node.ref)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (specSlug && node.specSlug !== specSlug) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const readyNodes = selectedNodes.filter((node) => readyRefs.has(node.ref));
|
|
143
|
+
const targetNodes = options.onlyReady ? readyNodes : selectedNodes;
|
|
144
|
+
const criticalPath = buildCriticalPath(graph, targetNodes.map((node) => node.ref));
|
|
145
|
+
const totalHours = targetNodes.reduce((sum, node) => sum + toHourCount(node.json?.estimated_hours), 0);
|
|
146
|
+
const plan = targetNodes.map((node, index) => sliceToPlanItem(repoRoot, node, index + 1, readyRefs));
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
critical_path: criticalPath,
|
|
150
|
+
plan,
|
|
151
|
+
total_hours: totalHours,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formatHumanPlan(report, options = {}) {
|
|
156
|
+
const unicode = Boolean(options.unicode) || /UTF-8/i.test(process.env.LANG || '');
|
|
157
|
+
const title = options.onlyReady ? 'Ready slices' : 'Quiver plan';
|
|
158
|
+
const pathSeparator = unicode ? ' → ' : ' -> ';
|
|
159
|
+
const lines = [title, `Total hours: ${report.total_hours}`, `Critical path: ${report.critical_path.length > 0 ? report.critical_path.join(pathSeparator) : '-'}`, ''];
|
|
160
|
+
|
|
161
|
+
if (report.plan.length === 0) {
|
|
162
|
+
lines.push('No pending slices found.');
|
|
163
|
+
return `${lines.join('\n')}\n`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const rows = report.plan.map((item) => [
|
|
167
|
+
`[${item.index}]`,
|
|
168
|
+
item.ticket || '-',
|
|
169
|
+
item.ref,
|
|
170
|
+
item.title,
|
|
171
|
+
`${item.hours}`,
|
|
172
|
+
item.status,
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
const widths = rows[0].map((_, colIndex) => Math.max(
|
|
176
|
+
rows.reduce((max, row) => Math.max(max, row[colIndex].length), 0),
|
|
177
|
+
['[N]', 'TICKET', 'SPEC/SLICE', 'TITLE', 'HOURS', 'STATUS'][colIndex].length,
|
|
178
|
+
));
|
|
179
|
+
|
|
180
|
+
lines.push(['[N]', 'TICKET', 'SPEC/SLICE', 'TITLE', 'HOURS', 'STATUS'].map((label, index) => label.padEnd(widths[index])).join(' '));
|
|
181
|
+
lines.push(widths.map((width) => '-'.repeat(width)).join(' '));
|
|
182
|
+
|
|
183
|
+
for (const row of rows) {
|
|
184
|
+
lines.push(row.map((value, index) => value.padEnd(widths[index])).join(' '));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return `${lines.join('\n')}\n`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function runPlan(repoRoot, options = {}) {
|
|
191
|
+
const report = collectPlan(repoRoot, options);
|
|
192
|
+
if (options.json) {
|
|
193
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
194
|
+
return report;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
process.stdout.write(formatHumanPlan(report, options));
|
|
198
|
+
return report;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
collectPlan,
|
|
203
|
+
formatHumanPlan,
|
|
204
|
+
runPlan,
|
|
205
|
+
};
|