create-quiver 0.12.1 → 0.14.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/CHANGELOG.md +27 -0
- package/README.md +24 -9
- package/README_FOR_AI.md +15 -6
- package/ROADMAP.md +15 -2
- package/docs/COMMANDS.md.template +12 -3
- package/docs/TROUBLESHOOTING.md.template +29 -0
- package/docs/WORKFLOW.md.template +13 -12
- package/package.json +2 -1
- package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +2 -2
- package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +5 -5
- package/specs/quiver-v27-reliability-ai-workflow-hardening/AUDIT_V24_V25_V26.md +67 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/COMMAND_CONTRACTS.md +125 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/COVERAGE_MATRIX.md +74 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/EVIDENCE_REPORT.md +179 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/EXECUTION_PLAN.md +71 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/SPEC.md +176 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/STATUS.md +37 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/pr.md +132 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/slice.json +75 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/CLOSURE_BRIEF.md +37 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/slice.json +79 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/slice.json +75 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/slice.json +78 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/slice.json +77 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/slice.json +84 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/slice.json +99 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/slice.json +88 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/slice.json +85 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/slice.json +91 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/COVERAGE_MATRIX.md +117 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EVIDENCE_REPORT.md +200 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EXECUTION_PLAN.md +60 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/SPEC.md +132 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/STATUS.md +36 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/pr.md +128 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/CLOSURE_BRIEF.md +44 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/slice.json +71 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/CLOSURE_BRIEF.md +38 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/slice.json +83 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/slice.json +85 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/slice.json +82 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/slice.json +85 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/EXECUTION_BRIEF.md +59 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/slice.json +94 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/CLOSURE_BRIEF.md +40 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/slice.json +98 -0
- package/src/create-quiver/commands/ai.js +563 -21
- package/src/create-quiver/commands/flow.js +52 -4
- package/src/create-quiver/commands/graph.js +7 -7
- package/src/create-quiver/commands/plan.js +6 -15
- package/src/create-quiver/commands/spec.js +292 -0
- package/src/create-quiver/index.js +125 -25
- package/src/create-quiver/lib/agent-profiles.js +15 -3
- package/src/create-quiver/lib/ai/artifacts.js +318 -0
- package/src/create-quiver/lib/ai/context-packs.js +2 -2
- package/src/create-quiver/lib/ai/execution-plan.js +9 -0
- package/src/create-quiver/lib/ai/executor.js +3 -2
- package/src/create-quiver/lib/ai/export-state.js +287 -95
- package/src/create-quiver/lib/ai/github.js +93 -4
- package/src/create-quiver/lib/ai/plan-review.js +161 -0
- package/src/create-quiver/lib/ai/run-state.js +17 -2
- package/src/create-quiver/lib/ai/spec-generator.js +87 -13
- package/src/create-quiver/lib/ai/spec-templates.js +72 -12
- package/src/create-quiver/lib/analyze.js +2 -2
- package/src/create-quiver/lib/approvals.js +14 -2
- package/src/create-quiver/lib/doctor.js +79 -0
- package/src/create-quiver/lib/git.js +40 -1
- package/src/create-quiver/lib/handoff.js +43 -1
- package/src/create-quiver/lib/init-docs.js +11 -7
- package/src/create-quiver/lib/init-layout.js +1 -0
- package/src/create-quiver/lib/lifecycle.js +52 -3
- package/src/create-quiver/lib/locks.js +134 -0
- package/src/create-quiver/lib/package-safety.js +7 -0
- package/src/create-quiver/lib/paths.js +74 -0
- package/src/create-quiver/lib/project-scan.js +74 -0
- package/src/create-quiver/lib/project-state-resolver.js +430 -0
- package/src/create-quiver/lib/readiness.js +48 -7
- package/src/create-quiver/lib/scope.js +2 -1
- package/src/create-quiver/lib/slice.js +8 -4
- package/src/create-quiver/lib/spec-worktrees.js +169 -38
- package/src/create-quiver/lib/statuses.js +115 -0
|
@@ -4,6 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const { readPhaseApproval } = require('../lib/approvals');
|
|
5
5
|
const { readPlanReview } = require('../lib/ai/plan-review');
|
|
6
6
|
const { listAgentProfiles } = require('../lib/agent-profiles');
|
|
7
|
+
const { readProjectScanStatus } = require('../lib/project-scan');
|
|
7
8
|
const { buildGraph, naturalNumberFromSliceId, readAllSlices } = require('../lib/slice-graph');
|
|
8
9
|
const { hasQuiverInitializationEvidence, readState } = require('../lib/state');
|
|
9
10
|
|
|
@@ -11,6 +12,46 @@ function exists(projectRoot, relativePath) {
|
|
|
11
12
|
return fs.existsSync(path.join(projectRoot, relativePath));
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
function readJsonIfExists(filePath) {
|
|
16
|
+
if (!fs.existsSync(filePath)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function detectPackageManager(projectRoot) {
|
|
28
|
+
const packageManagerField = readJsonIfExists(path.join(projectRoot, 'package.json'))?.packageManager;
|
|
29
|
+
if (typeof packageManagerField === 'string' && packageManagerField.trim()) {
|
|
30
|
+
return packageManagerField.split('@')[0];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const signals = [
|
|
34
|
+
['bun', 'bun.lockb'],
|
|
35
|
+
['bun', 'bun.lock'],
|
|
36
|
+
['pnpm', 'pnpm-lock.yaml'],
|
|
37
|
+
['yarn', 'yarn.lock'],
|
|
38
|
+
['npm', 'package-lock.json'],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
for (const [manager, filename] of signals) {
|
|
42
|
+
if (exists(projectRoot, filename)) {
|
|
43
|
+
return manager;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return 'npm';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatRunScriptCommand(packageManager, scriptName) {
|
|
51
|
+
const manager = ['npm', 'pnpm', 'yarn', 'bun'].includes(packageManager) ? packageManager : 'npm';
|
|
52
|
+
return `${manager} run ${scriptName}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
14
55
|
function listSpecSlugs(projectRoot) {
|
|
15
56
|
const specsDir = path.join(projectRoot, 'specs');
|
|
16
57
|
if (!fs.existsSync(specsDir)) {
|
|
@@ -53,6 +94,7 @@ function summarizeDocs(projectRoot) {
|
|
|
53
94
|
hasProjectMap: exists(projectRoot, 'docs/PROJECT_MAP.md'),
|
|
54
95
|
hasAiContext: exists(projectRoot, 'docs/AI_CONTEXT.md'),
|
|
55
96
|
hasOnboardingPrompt: exists(projectRoot, 'docs/AI_ONBOARDING_PROMPT.md'),
|
|
97
|
+
scanStatus: readProjectScanStatus(projectRoot),
|
|
56
98
|
};
|
|
57
99
|
const missing = [
|
|
58
100
|
['docs/PROJECT_MAP.md', docs.hasProjectMap],
|
|
@@ -77,7 +119,7 @@ function summarizeAgentProfiles(projectRoot) {
|
|
|
77
119
|
};
|
|
78
120
|
}
|
|
79
121
|
|
|
80
|
-
function buildFacts({ initialized, docs, approvals, planReview, agents, specSlugs, state, slices = null }) {
|
|
122
|
+
function buildFacts({ initialized, docs, approvals, planReview, agents, packageManager, specSlugs, state, slices = null }) {
|
|
81
123
|
return {
|
|
82
124
|
initialized,
|
|
83
125
|
hasProjectMap: docs.hasProjectMap,
|
|
@@ -91,6 +133,9 @@ function buildFacts({ initialized, docs, approvals, planReview, agents, specSlug
|
|
|
91
133
|
agents,
|
|
92
134
|
specSlugs,
|
|
93
135
|
slices,
|
|
136
|
+
contextSource: docs.scanStatus,
|
|
137
|
+
packageManager,
|
|
138
|
+
flowScriptCommand: formatRunScriptCommand(packageManager, 'quiver:flow'),
|
|
94
139
|
quiverVersion: state?.quiver_version || null,
|
|
95
140
|
};
|
|
96
141
|
}
|
|
@@ -207,8 +252,9 @@ function detectFlowState(projectRoot) {
|
|
|
207
252
|
};
|
|
208
253
|
const planReview = safeReadPlanReview(projectRoot);
|
|
209
254
|
const agents = summarizeAgentProfiles(projectRoot);
|
|
255
|
+
const packageManager = detectPackageManager(projectRoot);
|
|
210
256
|
const specSlugs = listSpecSlugs(projectRoot);
|
|
211
|
-
const facts = buildFacts({ initialized, docs, approvals, planReview, agents, specSlugs, state });
|
|
257
|
+
const facts = buildFacts({ initialized, docs, approvals, planReview, agents, packageManager, specSlugs, state });
|
|
212
258
|
|
|
213
259
|
if (!initialized) {
|
|
214
260
|
return baseReport({
|
|
@@ -430,7 +476,7 @@ function detectFlowState(projectRoot) {
|
|
|
430
476
|
}
|
|
431
477
|
|
|
432
478
|
const slices = summarizeSlices(projectRoot, specSlugs);
|
|
433
|
-
const sliceFacts = buildFacts({ initialized, docs, approvals, planReview, agents, specSlugs, state, slices: {
|
|
479
|
+
const sliceFacts = buildFacts({ initialized, docs, approvals, planReview, agents, packageManager, specSlugs, state, slices: {
|
|
434
480
|
completed: slices.completedCount,
|
|
435
481
|
pending: slices.pendingCount,
|
|
436
482
|
ready: slices.ready.map((slice) => slice.ref),
|
|
@@ -516,10 +562,12 @@ function formatFlowReport(report) {
|
|
|
516
562
|
'Command path:',
|
|
517
563
|
'- Bootstrap and remote use: npx create-quiver <command>',
|
|
518
564
|
'- Short alias after local install: quiver <command>',
|
|
519
|
-
|
|
565
|
+
`- Generated project script: ${report.facts.flowScriptCommand}`,
|
|
520
566
|
'',
|
|
567
|
+
`Package manager: ${report.facts.packageManager}`,
|
|
521
568
|
`Stage: ${report.label}`,
|
|
522
569
|
`Next safe command: ${report.nextCommand}`,
|
|
570
|
+
`Context source: ${report.facts.contextSource.summary}`,
|
|
523
571
|
];
|
|
524
572
|
|
|
525
573
|
if (report.blockers.length > 0) {
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { filterSlicesForExecution, resolveProjectState } = require('../lib/project-state-resolver');
|
|
2
|
+
const { computeLevels, detectFileConflicts } = require('../lib/slice-graph');
|
|
2
3
|
const { renderDotGraph } = require('../lib/renderers/dot');
|
|
3
4
|
const { renderMermaidGraph } = require('../lib/renderers/mermaid');
|
|
4
5
|
const { renderTreeGraph, isUnicodeEnabled } = require('../lib/renderers/tree');
|
|
5
6
|
|
|
6
|
-
const EXCLUDED_STATUSES = new Set(['completed', 'skipped', 'cancelled']);
|
|
7
|
-
const HISTORY_EXCLUDED_STATUSES = new Set(['skipped', 'cancelled']);
|
|
8
|
-
|
|
9
7
|
function toGraphNode(node) {
|
|
10
8
|
return {
|
|
11
9
|
ref: node.ref,
|
|
@@ -14,6 +12,7 @@ function toGraphNode(node) {
|
|
|
14
12
|
title: node.title || node.sliceId,
|
|
15
13
|
hours: Number.isFinite(Number(node.json?.estimated_hours)) ? Number(node.json.estimated_hours) : 0,
|
|
16
14
|
status: node.status || 'draft',
|
|
15
|
+
canonical_status: node.canonical_status || 'planned',
|
|
17
16
|
files: Array.isArray(node.files) ? node.files : [],
|
|
18
17
|
depends_on: Array.isArray(node.depends_on) ? node.depends_on : [],
|
|
19
18
|
};
|
|
@@ -29,14 +28,15 @@ function buildConflictPayload(levelIndex, groups) {
|
|
|
29
28
|
|
|
30
29
|
function collectGraph(repoRoot, options = {}) {
|
|
31
30
|
const specSlug = options.specSlug ? String(options.specSlug).trim() : '';
|
|
32
|
-
const
|
|
31
|
+
const state = resolveProjectState(repoRoot, { specSlug });
|
|
32
|
+
const graph = state.graph;
|
|
33
33
|
if (!specSlug) {
|
|
34
34
|
computeLevels(graph);
|
|
35
35
|
}
|
|
36
36
|
const includeCompleted = options.includeCompleted === true;
|
|
37
|
-
const
|
|
37
|
+
const executionRefs = new Set(filterSlicesForExecution(graph.nodes, { includeCompleted }).map((node) => node.ref));
|
|
38
38
|
const pendingNodes = graph.nodes.filter((node) => {
|
|
39
|
-
if (
|
|
39
|
+
if (!executionRefs.has(node.ref)) {
|
|
40
40
|
return false;
|
|
41
41
|
}
|
|
42
42
|
if (specSlug && node.specSlug !== specSlug) {
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
1
|
const { relativePosixPath } = require('../lib/paths');
|
|
4
|
-
const {
|
|
5
|
-
|
|
6
|
-
const EXCLUDED_STATUSES = new Set(['completed', 'skipped', 'cancelled']);
|
|
7
|
-
const HISTORY_EXCLUDED_STATUSES = new Set(['skipped', 'cancelled']);
|
|
2
|
+
const { filterSlicesForExecution, resolveProjectState } = require('../lib/project-state-resolver');
|
|
3
|
+
const { topoSort } = require('../lib/slice-graph');
|
|
8
4
|
|
|
9
5
|
function toHourCount(value) {
|
|
10
6
|
const parsed = Number(value);
|
|
@@ -26,6 +22,7 @@ function sliceToPlanItem(repoRoot, slice, index, readySet) {
|
|
|
26
22
|
ticket: slice.ticket || '',
|
|
27
23
|
title: slice.title || slice.sliceId,
|
|
28
24
|
status: slice.status || 'draft',
|
|
25
|
+
canonical_status: slice.canonical_status || 'planned',
|
|
29
26
|
hours: toHourCount(slice.json?.estimated_hours),
|
|
30
27
|
files: Array.isArray(slice.files) ? slice.files : [],
|
|
31
28
|
depends_on: Array.isArray(slice.depends_on) ? slice.depends_on : [],
|
|
@@ -117,17 +114,11 @@ function buildCriticalPath(graph, refs) {
|
|
|
117
114
|
|
|
118
115
|
function collectPlan(repoRoot, options = {}) {
|
|
119
116
|
const specSlug = options.specSlug ? String(options.specSlug).trim() : '';
|
|
120
|
-
const
|
|
121
|
-
const graph =
|
|
117
|
+
const state = resolveProjectState(repoRoot, { specSlug });
|
|
118
|
+
const graph = state.graph;
|
|
122
119
|
const topo = topoSort(graph);
|
|
123
120
|
const includeCompleted = options.includeCompleted === true;
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
const pendingRefs = new Set(
|
|
127
|
-
graph.nodes
|
|
128
|
-
.filter((node) => !excluded.has(String(node.status || '').toLowerCase()))
|
|
129
|
-
.map((node) => node.ref),
|
|
130
|
-
);
|
|
121
|
+
const pendingRefs = new Set(filterSlicesForExecution(graph.nodes, { includeCompleted }).map((node) => node.ref));
|
|
131
122
|
|
|
132
123
|
const readyRefs = buildReadySet(graph, Array.from(pendingRefs));
|
|
133
124
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
|
|
4
|
+
const { checkHandoff } = require('../lib/handoff');
|
|
5
|
+
const { parseJsonWithComments } = require('../lib/json');
|
|
6
|
+
const { assertPathInsideRoot, validateProjectRelativePaths } = require('../lib/paths');
|
|
4
7
|
const { resolveReviewedTechnicalPlanInput } = require('../lib/ai/plan-review');
|
|
5
8
|
const {
|
|
6
9
|
buildSpecGenerationManifest,
|
|
@@ -24,6 +27,292 @@ function readInputText(repoRoot, inputPath) {
|
|
|
24
27
|
return fs.readFileSync(resolved, 'utf8');
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
function resolveSpecDir(repoRoot, specInput) {
|
|
31
|
+
const value = String(specInput || '').trim();
|
|
32
|
+
if (!value) {
|
|
33
|
+
throw new Error(formatError('missing spec directory. Use: npx create-quiver spec validate specs/<spec-slug>'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const candidates = [
|
|
37
|
+
path.resolve(repoRoot, value),
|
|
38
|
+
path.resolve(repoRoot, 'specs', value),
|
|
39
|
+
path.resolve(repoRoot, 'specs-fix', value),
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
for (const candidate of candidates) {
|
|
43
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
44
|
+
assertPathInsideRoot(repoRoot, candidate, 'spec path');
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw new Error(formatError(`spec directory not found: ${value}`));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function findSliceJsonFiles(specDir) {
|
|
53
|
+
const slicesRoot = path.join(specDir, 'slices');
|
|
54
|
+
const files = [];
|
|
55
|
+
if (!fs.existsSync(slicesRoot)) {
|
|
56
|
+
return files;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const stack = [slicesRoot];
|
|
60
|
+
while (stack.length > 0) {
|
|
61
|
+
const current = stack.pop();
|
|
62
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
63
|
+
const fullPath = path.join(current, entry.name);
|
|
64
|
+
if (entry.isDirectory()) {
|
|
65
|
+
stack.push(fullPath);
|
|
66
|
+
} else if (entry.isFile() && entry.name === 'slice.json') {
|
|
67
|
+
files.push(fullPath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return files.sort();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function shortSliceId(sliceId) {
|
|
76
|
+
const match = String(sliceId || '').match(/^(slice-\d+)/);
|
|
77
|
+
return match ? match[1] : String(sliceId || '');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeDependency(specSlug, sliceId, dep) {
|
|
81
|
+
const value = String(dep || '').trim();
|
|
82
|
+
if (!value) {
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
if (value.includes('/')) {
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
return `${specSlug}/${value}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function detectCycle(nodes) {
|
|
92
|
+
const visiting = new Set();
|
|
93
|
+
const visited = new Set();
|
|
94
|
+
const stack = [];
|
|
95
|
+
|
|
96
|
+
function visit(ref) {
|
|
97
|
+
if (visiting.has(ref)) {
|
|
98
|
+
const start = stack.indexOf(ref);
|
|
99
|
+
return [...stack.slice(start), ref];
|
|
100
|
+
}
|
|
101
|
+
if (visited.has(ref)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
visiting.add(ref);
|
|
106
|
+
stack.push(ref);
|
|
107
|
+
const node = nodes.get(ref);
|
|
108
|
+
for (const dep of node?.deps || []) {
|
|
109
|
+
if (!nodes.has(dep)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const cycle = visit(dep);
|
|
113
|
+
if (cycle) {
|
|
114
|
+
return cycle;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
stack.pop();
|
|
118
|
+
visiting.delete(ref);
|
|
119
|
+
visited.add(ref);
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const ref of nodes.keys()) {
|
|
124
|
+
const cycle = visit(ref);
|
|
125
|
+
if (cycle) {
|
|
126
|
+
return cycle;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function missingGitFields(git) {
|
|
133
|
+
const data = git && typeof git === 'object' ? git : {};
|
|
134
|
+
return ['branch_type', 'base_branch', 'branch_slug', 'branch_name']
|
|
135
|
+
.filter((field) => !String(data[field] || '').trim());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildSpecValidationReport(repoRoot, specInput, options = {}) {
|
|
139
|
+
const strict = options.strict === true;
|
|
140
|
+
const specDir = resolveSpecDir(repoRoot, specInput);
|
|
141
|
+
const relativeSpecDir = toRelativePosix(repoRoot, specDir);
|
|
142
|
+
const specSlug = path.basename(specDir);
|
|
143
|
+
const errors = [];
|
|
144
|
+
const warnings = [];
|
|
145
|
+
const checked = [];
|
|
146
|
+
const docs = ['SPEC.md', 'STATUS.md', 'EVIDENCE_REPORT.md'];
|
|
147
|
+
const docText = {};
|
|
148
|
+
|
|
149
|
+
for (const doc of docs) {
|
|
150
|
+
const filePath = path.join(specDir, doc);
|
|
151
|
+
if (!fs.existsSync(filePath)) {
|
|
152
|
+
errors.push(`missing required spec document: ${relativeSpecDir}/${doc}`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
docText[doc] = fs.readFileSync(filePath, 'utf8');
|
|
156
|
+
checked.push(`${relativeSpecDir}/${doc}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const sliceFiles = findSliceJsonFiles(specDir);
|
|
160
|
+
if (sliceFiles.length === 0) {
|
|
161
|
+
errors.push(`spec has no slices: ${relativeSpecDir}/slices`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const nodes = new Map();
|
|
165
|
+
const seenSliceIds = new Set();
|
|
166
|
+
|
|
167
|
+
for (const sliceFile of sliceFiles) {
|
|
168
|
+
const relativeSliceFile = toRelativePosix(repoRoot, sliceFile);
|
|
169
|
+
checked.push(relativeSliceFile);
|
|
170
|
+
|
|
171
|
+
let json;
|
|
172
|
+
try {
|
|
173
|
+
json = parseJsonWithComments(fs.readFileSync(sliceFile, 'utf8'));
|
|
174
|
+
} catch (error) {
|
|
175
|
+
errors.push(`invalid JSON in ${relativeSliceFile}: ${error.message}`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const sliceId = String(json.slice_id || '').trim();
|
|
180
|
+
const expectedSliceId = path.basename(path.dirname(sliceFile));
|
|
181
|
+
if (!sliceId) {
|
|
182
|
+
errors.push(`${relativeSliceFile} is missing slice_id`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (sliceId !== expectedSliceId) {
|
|
186
|
+
errors.push(`${relativeSliceFile} slice_id must match directory name (${expectedSliceId})`);
|
|
187
|
+
}
|
|
188
|
+
if (seenSliceIds.has(sliceId)) {
|
|
189
|
+
errors.push(`duplicate slice_id in spec: ${sliceId}`);
|
|
190
|
+
}
|
|
191
|
+
seenSliceIds.add(sliceId);
|
|
192
|
+
|
|
193
|
+
const writeScope = Array.isArray(json.allowed_write_paths) && json.allowed_write_paths.length > 0
|
|
194
|
+
? json.allowed_write_paths
|
|
195
|
+
: json.files;
|
|
196
|
+
if (!Array.isArray(writeScope) || writeScope.length === 0) {
|
|
197
|
+
errors.push(`${relativeSliceFile} must declare files or allowed_write_paths`);
|
|
198
|
+
}
|
|
199
|
+
const missingGit = missingGitFields(json.git);
|
|
200
|
+
if (missingGit.length > 0) {
|
|
201
|
+
errors.push(`${relativeSliceFile} must declare git.branch_type, git.base_branch, git.branch_slug, and git.branch_name for local execution (missing: ${missingGit.join(', ')}).`);
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
validateProjectRelativePaths(writeScope, `${relativeSliceFile} write scope`);
|
|
205
|
+
validateProjectRelativePaths(json.expected_read_paths, `${relativeSliceFile} expected_read_paths`);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
errors.push(error.message);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const briefName of ['EXECUTION_BRIEF.md', 'CLOSURE_BRIEF.md']) {
|
|
211
|
+
const briefRel = toRelativePosix(repoRoot, path.join(path.dirname(sliceFile), briefName));
|
|
212
|
+
try {
|
|
213
|
+
checkHandoff(briefRel, repoRoot);
|
|
214
|
+
checked.push(briefRel);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
errors.push(error.message);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const ref = `${specSlug}/${sliceId}`;
|
|
221
|
+
const deps = (Array.isArray(json.depends_on) ? json.depends_on : [])
|
|
222
|
+
.map((dep) => normalizeDependency(specSlug, sliceId, dep))
|
|
223
|
+
.filter(Boolean);
|
|
224
|
+
nodes.set(ref, { deps, json, ref, sliceId });
|
|
225
|
+
|
|
226
|
+
const statusNeedle = shortSliceId(sliceId);
|
|
227
|
+
if (docText['STATUS.md'] && !docText['STATUS.md'].includes(sliceId) && !docText['STATUS.md'].includes(statusNeedle)) {
|
|
228
|
+
warnings.push(`STATUS.md does not reference ${sliceId}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (String(json.status || '').trim() === 'completed') {
|
|
232
|
+
if (!json.completed_at) {
|
|
233
|
+
warnings.push(`${relativeSliceFile} is completed but missing completed_at`);
|
|
234
|
+
}
|
|
235
|
+
if (docText['EVIDENCE_REPORT.md'] && !docText['EVIDENCE_REPORT.md'].includes(sliceId) && !docText['EVIDENCE_REPORT.md'].includes(statusNeedle)) {
|
|
236
|
+
warnings.push(`EVIDENCE_REPORT.md does not reference completed slice ${sliceId}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const [ref, node] of nodes.entries()) {
|
|
242
|
+
for (const dep of node.deps) {
|
|
243
|
+
if (dep.startsWith(`${specSlug}/`) && !nodes.has(dep)) {
|
|
244
|
+
errors.push(`${ref} depends on missing slice ${dep}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const cycle = detectCycle(nodes);
|
|
250
|
+
if (cycle) {
|
|
251
|
+
errors.push(`dependency cycle detected: ${cycle.join(' -> ')}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (strict && warnings.length > 0) {
|
|
255
|
+
errors.push(...warnings.map((warning) => `strict warning: ${warning}`));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
ok: errors.length === 0,
|
|
260
|
+
specDir: relativeSpecDir,
|
|
261
|
+
checked,
|
|
262
|
+
errors,
|
|
263
|
+
warnings,
|
|
264
|
+
slices: sliceFiles.length,
|
|
265
|
+
strict,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function formatSpecValidationReport(report) {
|
|
270
|
+
const lines = [
|
|
271
|
+
'Quiver spec validation',
|
|
272
|
+
`Spec: ${report.specDir}`,
|
|
273
|
+
`Slices: ${report.slices}`,
|
|
274
|
+
`Strict: ${report.strict ? 'yes' : 'no'}`,
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
if (report.checked.length > 0) {
|
|
278
|
+
lines.push('Checked files:');
|
|
279
|
+
for (const file of report.checked) {
|
|
280
|
+
lines.push(`- ${file}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (report.warnings.length > 0) {
|
|
285
|
+
lines.push('Warnings:');
|
|
286
|
+
for (const warning of report.warnings) {
|
|
287
|
+
lines.push(`- ${warning}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (report.errors.length > 0) {
|
|
292
|
+
lines.push('Errors:');
|
|
293
|
+
for (const error of report.errors) {
|
|
294
|
+
lines.push(`- ${error}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
lines.push(report.ok ? 'PASS: spec validation passed.' : 'FAIL: spec validation failed.');
|
|
299
|
+
return `${lines.join('\n')}\n`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function runValidateSpec(repoRoot, specInput, options = {}) {
|
|
303
|
+
const report = buildSpecValidationReport(repoRoot, specInput, options);
|
|
304
|
+
process.stdout.write(formatSpecValidationReport(report));
|
|
305
|
+
|
|
306
|
+
if (!report.ok) {
|
|
307
|
+
const error = new Error(formatError(`spec validate failed for ${report.specDir}`));
|
|
308
|
+
error.code = 'SPEC_VALIDATE_FAILED';
|
|
309
|
+
error.details = report;
|
|
310
|
+
throw error;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return report;
|
|
314
|
+
}
|
|
315
|
+
|
|
27
316
|
function buildSpecCreatePreview(repoRoot, options = {}) {
|
|
28
317
|
const resolved = resolveReviewedTechnicalPlanInput(repoRoot, options.input || undefined);
|
|
29
318
|
const inputPath = resolved.inputPath;
|
|
@@ -126,8 +415,11 @@ function runCreateSpec(repoRoot, options = {}) {
|
|
|
126
415
|
}
|
|
127
416
|
|
|
128
417
|
module.exports = {
|
|
418
|
+
buildSpecValidationReport,
|
|
129
419
|
buildSpecCreatePreview,
|
|
130
420
|
formatSpecCreateDryRun,
|
|
131
421
|
formatSpecCreateResult,
|
|
422
|
+
formatSpecValidationReport,
|
|
132
423
|
runCreateSpec,
|
|
424
|
+
runValidateSpec,
|
|
133
425
|
};
|