create-quiver 0.15.2 → 0.15.3
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/README.md +1 -0
- package/README_FOR_AI.md +6 -4
- package/docs/CLI_UX_GUIDE.md +1 -1
- package/docs/COMMANDS.md.template +14 -3
- package/docs/INDEX.md +87 -0
- package/docs/WORKFLOW.md.template +2 -2
- package/docs/reference/commands.md +13 -1
- package/package.json +3 -1
- package/specs/quiver-v34-cli-dashboard-status/EVIDENCE_REPORT.md +119 -0
- package/specs/quiver-v34-cli-dashboard-status/EXECUTION_PLAN.md +77 -0
- package/specs/quiver-v34-cli-dashboard-status/SPEC.md +152 -0
- package/specs/quiver-v34-cli-dashboard-status/STATUS.md +29 -0
- package/specs/quiver-v34-cli-dashboard-status/pr.md +99 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-00-dashboard-foundation/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-00-dashboard-foundation/EXECUTION_BRIEF.md +49 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-00-dashboard-foundation/slice.json +68 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-01-dashboard-report-contract/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-01-dashboard-report-contract/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-01-dashboard-report-contract/slice.json +78 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-02-dashboard-command-rendering/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-02-dashboard-command-rendering/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-02-dashboard-command-rendering/slice.json +82 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-03-dashboard-edge-cases-and-guardrails/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-03-dashboard-edge-cases-and-guardrails/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-03-dashboard-edge-cases-and-guardrails/slice.json +77 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-04-docs-templates-and-scripts/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-04-docs-templates-and-scripts/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-04-docs-templates-and-scripts/slice.json +88 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-05-tests-smokes-release-readiness/CLOSURE_BRIEF.md +26 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-05-tests-smokes-release-readiness/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v34-cli-dashboard-status/slices/slice-05-tests-smokes-release-readiness/slice.json +85 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/EVIDENCE_REPORT.md +43 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/EXECUTION_PLAN.md +85 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/SPEC.md +147 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/STATUS.md +33 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/pr.md +106 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-00-foundation-and-doc-router/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-00-foundation-and-doc-router/EXECUTION_BRIEF.md +38 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-00-foundation-and-doc-router/slice.json +64 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-01-dashboard-cli-contract/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-01-dashboard-cli-contract/EXECUTION_BRIEF.md +49 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-01-dashboard-cli-contract/slice.json +80 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-02-dashboard-compact-renderer/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-02-dashboard-compact-renderer/EXECUTION_BRIEF.md +48 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-02-dashboard-compact-renderer/slice.json +73 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-03-dashboard-details-sections/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-03-dashboard-details-sections/EXECUTION_BRIEF.md +48 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-03-dashboard-details-sections/slice.json +72 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-04-version-command/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-04-version-command/EXECUTION_BRIEF.md +49 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-04-version-command/slice.json +82 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-05-docs-help-generated-guidance/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-05-docs-help-generated-guidance/EXECUTION_BRIEF.md +50 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-05-docs-help-generated-guidance/slice.json +90 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-06-tests-smokes-release-readiness/CLOSURE_BRIEF.md +21 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-06-tests-smokes-release-readiness/EXECUTION_BRIEF.md +51 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-06-tests-smokes-release-readiness/slice.json +85 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-07-package-and-cross-platform-smoke/CLOSURE_BRIEF.md +20 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-07-package-and-cross-platform-smoke/EXECUTION_BRIEF.md +49 -0
- package/specs/quiver-v35-compact-dashboard-version-ux/slices/slice-07-package-and-cross-platform-smoke/slice.json +79 -0
- package/specs/quiver-v36-ai-run-watch-portable/EVIDENCE_REPORT.md +33 -0
- package/specs/quiver-v36-ai-run-watch-portable/EXECUTION_PLAN.md +39 -0
- package/specs/quiver-v36-ai-run-watch-portable/SPEC.md +166 -0
- package/specs/quiver-v36-ai-run-watch-portable/STATUS.md +31 -0
- package/specs/quiver-v36-ai-run-watch-portable/pr.md +78 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-00-foundation-and-handoffs/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-00-foundation-and-handoffs/EXECUTION_BRIEF.md +43 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-00-foundation-and-handoffs/slice.json +61 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-01-run-schema-path-safety/CLOSURE_BRIEF.md +18 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-01-run-schema-path-safety/EXECUTION_BRIEF.md +43 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-01-run-schema-path-safety/slice.json +65 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-02-event-writer-redacted-logs/CLOSURE_BRIEF.md +18 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-02-event-writer-redacted-logs/EXECUTION_BRIEF.md +43 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-02-event-writer-redacted-logs/slice.json +66 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-03-provider-streaming-integration/CLOSURE_BRIEF.md +18 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-03-provider-streaming-integration/EXECUTION_BRIEF.md +41 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-03-provider-streaming-integration/slice.json +71 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-04-ai-run-watch-command/CLOSURE_BRIEF.md +18 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-04-ai-run-watch-command/EXECUTION_BRIEF.md +44 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-04-ai-run-watch-command/slice.json +73 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-05-main-progress-user-guidance/CLOSURE_BRIEF.md +18 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-05-main-progress-user-guidance/EXECUTION_BRIEF.md +44 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-05-main-progress-user-guidance/slice.json +71 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-06-docs-generated-guidance/CLOSURE_BRIEF.md +19 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-06-docs-generated-guidance/EXECUTION_BRIEF.md +45 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-06-docs-generated-guidance/slice.json +77 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-07-tests-cross-platform-readiness/CLOSURE_BRIEF.md +22 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-07-tests-cross-platform-readiness/EXECUTION_BRIEF.md +46 -0
- package/specs/quiver-v36-ai-run-watch-portable/slices/slice-07-tests-cross-platform-readiness/slice.json +87 -0
- package/src/create-quiver/commands/dashboard.js +31 -0
- package/src/create-quiver/index.js +97 -2
- package/src/create-quiver/lib/cli/ux-flags.js +6 -0
- package/src/create-quiver/lib/dashboard.js +896 -0
- package/src/create-quiver/lib/init-docs.js +6 -0
- package/src/create-quiver/lib/init-layout.js +2 -0
- package/src/create-quiver/lib/project-state-resolver.js +22 -2
- package/src/create-quiver/lib/version.js +157 -0
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const { collectLifecycleExport } = require('./ai/export-state');
|
|
5
|
+
const {
|
|
6
|
+
collectActiveSliceState,
|
|
7
|
+
filterSlicesForExecution,
|
|
8
|
+
groupSlicesBySpec,
|
|
9
|
+
isBlockedStatus,
|
|
10
|
+
isCompletedStatus,
|
|
11
|
+
normalizeStatus,
|
|
12
|
+
progressForSlice,
|
|
13
|
+
resolveProjectState,
|
|
14
|
+
summarizeGraph,
|
|
15
|
+
} = require('./project-state-resolver');
|
|
16
|
+
const { relativePosixPath } = require('./paths');
|
|
17
|
+
|
|
18
|
+
const DASHBOARD_SCHEMA_VERSION = 1;
|
|
19
|
+
const SPEC_FAMILIES = Object.freeze(['specs', 'specs-fix']);
|
|
20
|
+
const DEFAULT_DASHBOARD_LIMIT = 2;
|
|
21
|
+
const MAX_DASHBOARD_LIMIT = 100;
|
|
22
|
+
const SUPPORTED_DASHBOARD_SECTIONS = Object.freeze([
|
|
23
|
+
'overview',
|
|
24
|
+
'specs',
|
|
25
|
+
'slices',
|
|
26
|
+
'blockers',
|
|
27
|
+
'warnings',
|
|
28
|
+
'agents',
|
|
29
|
+
'approvals',
|
|
30
|
+
'runs',
|
|
31
|
+
'active-slice',
|
|
32
|
+
'next-steps',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
class DashboardError extends Error {
|
|
36
|
+
constructor(code, message, nextCommand = 'npx create-quiver dashboard') {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = 'DashboardError';
|
|
39
|
+
this.code = code;
|
|
40
|
+
this.nextCommand = nextCommand;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function compareRefs(left, right) {
|
|
45
|
+
return String(left || '').localeCompare(String(right || ''));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toPosix(filePath) {
|
|
49
|
+
return String(filePath || '').split(path.sep).join('/');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isPlaceholderSpecDir(name) {
|
|
53
|
+
return String(name || '').startsWith('[') && String(name || '').endsWith(']');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function listSpecDirectories(projectRoot) {
|
|
57
|
+
const specs = [];
|
|
58
|
+
|
|
59
|
+
for (const family of SPEC_FAMILIES) {
|
|
60
|
+
const familyPath = path.join(projectRoot, family);
|
|
61
|
+
if (!fs.existsSync(familyPath)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const entry of fs.readdirSync(familyPath, { withFileTypes: true })) {
|
|
66
|
+
if (!entry.isDirectory() || isPlaceholderSpecDir(entry.name)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const specRoot = path.join(familyPath, entry.name);
|
|
70
|
+
const hasSpecSignals = fs.existsSync(path.join(specRoot, 'SPEC.md'))
|
|
71
|
+
|| fs.existsSync(path.join(specRoot, 'STATUS.md'))
|
|
72
|
+
|| fs.existsSync(path.join(specRoot, 'slices'));
|
|
73
|
+
if (!hasSpecSignals) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
specs.push({
|
|
77
|
+
family,
|
|
78
|
+
path: toPosix(path.join(family, entry.name)),
|
|
79
|
+
slug: entry.name,
|
|
80
|
+
spec_path: fs.existsSync(path.join(specRoot, 'SPEC.md')) ? toPosix(path.join(family, entry.name, 'SPEC.md')) : null,
|
|
81
|
+
status_path: fs.existsSync(path.join(specRoot, 'STATUS.md')) ? toPosix(path.join(family, entry.name, 'STATUS.md')) : null,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return specs.sort((left, right) => left.slug.localeCompare(right.slug) || left.family.localeCompare(right.family));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sliceStatus(slice) {
|
|
90
|
+
return normalizeStatus('slice', slice?.canonical_status || slice?.status, 'planned');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isClosedSlice(slice) {
|
|
94
|
+
const status = sliceStatus(slice);
|
|
95
|
+
return status === 'completed' || status === 'skipped';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function summarizeProgress(slices) {
|
|
99
|
+
const items = Array.isArray(slices) ? slices : [];
|
|
100
|
+
const total = items.length;
|
|
101
|
+
const completed = items.filter((slice) => isCompletedStatus('slice', sliceStatus(slice))).length;
|
|
102
|
+
const skipped = items.filter((slice) => sliceStatus(slice) === 'skipped').length;
|
|
103
|
+
const blocked = items.filter((slice) => isBlockedStatus('slice', sliceStatus(slice), slice)).length;
|
|
104
|
+
const open = Math.max(0, total - completed - skipped);
|
|
105
|
+
const percent = total === 0 ? 0 : Math.round((completed / total) * 100);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
total,
|
|
109
|
+
completed,
|
|
110
|
+
skipped,
|
|
111
|
+
open,
|
|
112
|
+
blocked,
|
|
113
|
+
percent,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeSlice(projectRoot, slice) {
|
|
118
|
+
const sliceJsonPath = relativePosixPath(projectRoot, slice.slicePath);
|
|
119
|
+
const status = sliceStatus(slice);
|
|
120
|
+
return {
|
|
121
|
+
ref: slice.ref,
|
|
122
|
+
id: slice.sliceId,
|
|
123
|
+
title: slice.title || slice.sliceId,
|
|
124
|
+
status: slice.status || status,
|
|
125
|
+
canonical_status: status,
|
|
126
|
+
progress: progressForSlice(slice),
|
|
127
|
+
spec_slug: slice.specSlug,
|
|
128
|
+
spec_family: slice.specFamily,
|
|
129
|
+
path: relativePosixPath(projectRoot, slice.sliceDir),
|
|
130
|
+
slice_json: sliceJsonPath,
|
|
131
|
+
depends_on: Array.isArray(slice.depends_on) ? slice.depends_on : [],
|
|
132
|
+
blocked_reason: slice.json?.blocked_reason || null,
|
|
133
|
+
files_count: Array.isArray(slice.files) ? slice.files.length : 0,
|
|
134
|
+
start_slice_command: status === 'blocked' || isClosedSlice(slice) ? null : `npx create-quiver start-slice "${sliceJsonPath}"`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeSpec(spec, projectRoot) {
|
|
139
|
+
const progress = summarizeProgress(spec.slices);
|
|
140
|
+
const specPath = toPosix(path.join(spec.specFamily, spec.specSlug));
|
|
141
|
+
return {
|
|
142
|
+
slug: spec.specSlug,
|
|
143
|
+
family: spec.specFamily,
|
|
144
|
+
path: specPath,
|
|
145
|
+
spec_path: fs.existsSync(path.join(projectRoot, specPath, 'SPEC.md')) ? toPosix(path.join(specPath, 'SPEC.md')) : null,
|
|
146
|
+
status_path: fs.existsSync(path.join(projectRoot, specPath, 'STATUS.md')) ? toPosix(path.join(specPath, 'STATUS.md')) : null,
|
|
147
|
+
status: spec.status,
|
|
148
|
+
canonical_status: normalizeStatus('spec', spec.canonical_status || spec.status, 'planned'),
|
|
149
|
+
progress,
|
|
150
|
+
slices: spec.slices.map((slice) => slice.ref).sort(compareRefs),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function mergeSpecDirectories(specs, directories) {
|
|
155
|
+
const byKey = new Map(specs.map((spec) => [`${spec.family}/${spec.slug}`, spec]));
|
|
156
|
+
|
|
157
|
+
for (const directory of directories) {
|
|
158
|
+
const key = `${directory.family}/${directory.slug}`;
|
|
159
|
+
if (byKey.has(key)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
byKey.set(key, {
|
|
163
|
+
slug: directory.slug,
|
|
164
|
+
family: directory.family,
|
|
165
|
+
path: directory.path,
|
|
166
|
+
spec_path: directory.spec_path,
|
|
167
|
+
status_path: directory.status_path,
|
|
168
|
+
status: 'draft',
|
|
169
|
+
canonical_status: 'draft',
|
|
170
|
+
progress: summarizeProgress([]),
|
|
171
|
+
slices: [],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return Array.from(byKey.values()).sort((left, right) => left.slug.localeCompare(right.slug) || left.family.localeCompare(right.family));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function compactAgents(agents) {
|
|
179
|
+
return (Array.isArray(agents) ? agents : []).map((agent) => ({
|
|
180
|
+
role: agent.role,
|
|
181
|
+
configured: Boolean(agent.configured),
|
|
182
|
+
provider: agent.provider || null,
|
|
183
|
+
model: agent.model || null,
|
|
184
|
+
status: agent.status || 'idle',
|
|
185
|
+
canonical_status: agent.canonical_status || 'idle',
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function compactApprovals(approvals) {
|
|
190
|
+
return (Array.isArray(approvals) ? approvals : []).map((approval) => ({
|
|
191
|
+
phase: approval.phase,
|
|
192
|
+
status: approval.status,
|
|
193
|
+
canonical_status: approval.canonical_status,
|
|
194
|
+
latest_draft_version: approval.latest_draft_version || null,
|
|
195
|
+
approved_version: approval.approved_version || null,
|
|
196
|
+
error: approval.error || null,
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function compactRuns(runs) {
|
|
201
|
+
return (Array.isArray(runs) ? runs : []).map((run) => ({
|
|
202
|
+
run_id: run.run_id,
|
|
203
|
+
phase: run.phase,
|
|
204
|
+
status: run.status,
|
|
205
|
+
canonical_status: run.canonical_status,
|
|
206
|
+
spec_slug: run.spec_slug || null,
|
|
207
|
+
next_command: run.next_command || null,
|
|
208
|
+
updated_at: run.updated_at || null,
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function collectEvidenceSummary(slices) {
|
|
213
|
+
const refs = [];
|
|
214
|
+
let count = 0;
|
|
215
|
+
|
|
216
|
+
for (const slice of Array.isArray(slices) ? slices : []) {
|
|
217
|
+
const evidence = Array.isArray(slice.json?.evidence) ? slice.json.evidence : [];
|
|
218
|
+
if (evidence.length === 0) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
count += evidence.length;
|
|
222
|
+
refs.push(slice.ref);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
count,
|
|
227
|
+
slice_refs: Array.from(new Set(refs)).sort(compareRefs),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function filterGraphForRefs(graph, selectedRefs) {
|
|
232
|
+
if (!graph?.ok) {
|
|
233
|
+
return graph;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const refs = selectedRefs instanceof Set ? selectedRefs : new Set(selectedRefs);
|
|
237
|
+
return {
|
|
238
|
+
...graph,
|
|
239
|
+
edges: graph.edges.filter((edge) => refs.has(edge.from) && refs.has(edge.to)),
|
|
240
|
+
levels: graph.levels
|
|
241
|
+
.map((level) => ({
|
|
242
|
+
level: level.level,
|
|
243
|
+
slices: level.slices.filter((ref) => refs.has(ref)),
|
|
244
|
+
}))
|
|
245
|
+
.filter((level) => level.slices.length > 0)
|
|
246
|
+
.map((level, index) => ({
|
|
247
|
+
...level,
|
|
248
|
+
level: index,
|
|
249
|
+
})),
|
|
250
|
+
conflicts: graph.conflicts
|
|
251
|
+
.map((conflict) => ({
|
|
252
|
+
files: conflict.files,
|
|
253
|
+
slices: conflict.slices.filter((ref) => refs.has(ref)),
|
|
254
|
+
}))
|
|
255
|
+
.filter((conflict) => conflict.slices.length > 1),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function buildReadyRefs(graph, pendingSlices) {
|
|
260
|
+
if (!graph?.ok) {
|
|
261
|
+
return new Set();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const pendingRefs = new Set((Array.isArray(pendingSlices) ? pendingSlices : []).map((slice) => slice.ref));
|
|
265
|
+
const incomingCounts = new Map(Array.from(pendingRefs).map((ref) => [ref, 0]));
|
|
266
|
+
|
|
267
|
+
for (const edge of graph.edges || []) {
|
|
268
|
+
if (!pendingRefs.has(edge.from) || !pendingRefs.has(edge.to)) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
incomingCounts.set(edge.to, (incomingCounts.get(edge.to) || 0) + 1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return new Set(Array.from(incomingCounts.entries())
|
|
275
|
+
.filter(([, count]) => count === 0)
|
|
276
|
+
.map(([ref]) => ref));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function selectNextReady({ graph, pendingSlices, visibleSlices, projectRoot }) {
|
|
280
|
+
const readyRefs = buildReadyRefs(graph, pendingSlices);
|
|
281
|
+
const next = (Array.isArray(visibleSlices) ? visibleSlices : [])
|
|
282
|
+
.filter((slice) => readyRefs.has(slice.ref))
|
|
283
|
+
.filter((slice) => !isBlockedStatus('slice', sliceStatus(slice), slice))
|
|
284
|
+
.filter((slice) => !isClosedSlice(slice))
|
|
285
|
+
.sort((left, right) => compareRefs(left.ref, right.ref))[0] || null;
|
|
286
|
+
|
|
287
|
+
return next ? normalizeSlice(projectRoot, next) : null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function normalizeWarning(code, message) {
|
|
291
|
+
return { code, message };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function collectWarnings({ graphSummary, layout, specDirs, specSlug, visibleSlices }) {
|
|
295
|
+
const warnings = [];
|
|
296
|
+
|
|
297
|
+
if (!graphSummary.ok && graphSummary.error?.message) {
|
|
298
|
+
warnings.push(normalizeWarning(graphSummary.error.code || 'GRAPH_ERROR', graphSummary.error.message));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (layout.layout === 'legacy' || layout.layout === 'hybrid' || layout.layout === 'incomplete') {
|
|
302
|
+
warnings.push(normalizeWarning('LAYOUT_REQUIRES_ATTENTION', layout.recommendations.join(' ')));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (specDirs.length === 0) {
|
|
306
|
+
warnings.push(normalizeWarning('NO_SPECS_FOUND', 'No specs were found in specs/ or specs-fix/.'));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (visibleSlices.length === 0) {
|
|
310
|
+
warnings.push(normalizeWarning(
|
|
311
|
+
'NO_VISIBLE_SLICES',
|
|
312
|
+
specSlug
|
|
313
|
+
? `No visible slices were found for spec '${specSlug}' with the current filters.`
|
|
314
|
+
: 'No visible slices were found with the current filters.',
|
|
315
|
+
));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return warnings;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function collectBlockers({ graphSummary, layout, activeSlice, visibleSlices }) {
|
|
322
|
+
const blockers = [];
|
|
323
|
+
|
|
324
|
+
for (const slice of visibleSlices) {
|
|
325
|
+
if (isBlockedStatus('slice', sliceStatus(slice), slice)) {
|
|
326
|
+
blockers.push({
|
|
327
|
+
ref: slice.ref,
|
|
328
|
+
reason: slice.json?.blocked_reason || 'blocked',
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!graphSummary.ok && graphSummary.error?.message) {
|
|
334
|
+
blockers.push({ ref: 'slice-graph', reason: graphSummary.error.message });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (layout.layout === 'legacy' || layout.layout === 'hybrid' || layout.layout === 'incomplete') {
|
|
338
|
+
blockers.push({ ref: 'migration', reason: layout.recommendations.join(' ') });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (activeSlice?.reconciliation?.decision === 'blocked') {
|
|
342
|
+
blockers.push({ ref: 'active-slice', reason: activeSlice.reconciliation.reason });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return blockers;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function collectNextSteps({ graphSummary, nextReady, selectedSpec, specSlug, visibleSlices }) {
|
|
349
|
+
const steps = [];
|
|
350
|
+
|
|
351
|
+
if (!graphSummary.ok) {
|
|
352
|
+
steps.push({
|
|
353
|
+
id: 'inspect-graph',
|
|
354
|
+
command: specSlug ? `npx create-quiver graph --spec ${specSlug}` : 'npx create-quiver graph',
|
|
355
|
+
reason: 'Inspect slice dependency issues before executing slices.',
|
|
356
|
+
});
|
|
357
|
+
return steps;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (nextReady) {
|
|
361
|
+
steps.push({
|
|
362
|
+
id: 'start-next-ready-slice',
|
|
363
|
+
command: nextReady.start_slice_command,
|
|
364
|
+
reason: `Start ${nextReady.ref}.`,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (selectedSpec?.path) {
|
|
369
|
+
steps.push({
|
|
370
|
+
id: 'validate-spec',
|
|
371
|
+
command: `npx create-quiver spec validate ${selectedSpec.path}`,
|
|
372
|
+
reason: 'Validate the spec package before execution or PR work.',
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (!nextReady && visibleSlices.length === 0 && specSlug) {
|
|
377
|
+
steps.push({
|
|
378
|
+
id: 'include-completed-history',
|
|
379
|
+
command: `npx create-quiver dashboard --spec ${specSlug} --include-completed`,
|
|
380
|
+
reason: 'Show completed history for this spec.',
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!nextReady && !selectedSpec) {
|
|
385
|
+
steps.push({
|
|
386
|
+
id: 'draft-acceptance',
|
|
387
|
+
command: 'npx create-quiver ai plan --phase acceptance --input <requirements.md> --dry-run',
|
|
388
|
+
reason: 'Start the WDD + SDD planning flow before creating specs.',
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return steps;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function buildErrorPayload(error) {
|
|
396
|
+
const isDashboardError = error instanceof DashboardError;
|
|
397
|
+
return {
|
|
398
|
+
dashboard_schema_version: DASHBOARD_SCHEMA_VERSION,
|
|
399
|
+
error: {
|
|
400
|
+
code: isDashboardError ? error.code : 'DASHBOARD_ERROR',
|
|
401
|
+
message: error.message,
|
|
402
|
+
next_command: isDashboardError ? error.nextCommand : 'npx create-quiver dashboard',
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function collectDashboardReport(projectRoot, options = {}) {
|
|
408
|
+
const specSlug = options.specSlug ? String(options.specSlug).trim() : '';
|
|
409
|
+
const includeCompleted = options.includeCompleted === true;
|
|
410
|
+
const specDirs = listSpecDirectories(projectRoot);
|
|
411
|
+
const selectedSpecDir = specSlug ? specDirs.find((spec) => spec.slug === specSlug) : null;
|
|
412
|
+
|
|
413
|
+
if (specSlug && !selectedSpecDir) {
|
|
414
|
+
throw new DashboardError(
|
|
415
|
+
'SPEC_NOT_FOUND',
|
|
416
|
+
`Spec '${specSlug}' was not found in specs/ or specs-fix/.`,
|
|
417
|
+
'npx create-quiver ai specs list',
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const exportData = collectLifecycleExport(projectRoot, { includeCompleted: true });
|
|
422
|
+
const globalState = resolveProjectState(projectRoot, { allowGraphErrors: true });
|
|
423
|
+
const visibleState = resolveProjectState(projectRoot, {
|
|
424
|
+
allowGraphErrors: true,
|
|
425
|
+
specSlug,
|
|
426
|
+
});
|
|
427
|
+
const globalSlices = globalState.graph.nodes;
|
|
428
|
+
const baseVisibleSlices = specSlug
|
|
429
|
+
? visibleState.graph.nodes.filter((slice) => slice.specSlug === specSlug)
|
|
430
|
+
: visibleState.graph.nodes;
|
|
431
|
+
const visibleSlices = filterSlicesForExecution(baseVisibleSlices, { includeCompleted });
|
|
432
|
+
const pendingGraphSlices = filterSlicesForExecution(visibleState.graph.nodes, { includeCompleted: false });
|
|
433
|
+
const graphSummary = summarizeGraph(visibleState.graph);
|
|
434
|
+
const visibleRefs = new Set(visibleSlices.map((slice) => slice.ref));
|
|
435
|
+
const selectedGraph = filterGraphForRefs(graphSummary, visibleRefs);
|
|
436
|
+
const globalSpecs = mergeSpecDirectories(
|
|
437
|
+
groupSlicesBySpec(globalSlices).map((spec) => normalizeSpec(spec, projectRoot)),
|
|
438
|
+
specDirs,
|
|
439
|
+
);
|
|
440
|
+
const selectedSpec = specSlug
|
|
441
|
+
? globalSpecs.find((spec) => spec.slug === specSlug) || {
|
|
442
|
+
...selectedSpecDir,
|
|
443
|
+
status: 'draft',
|
|
444
|
+
canonical_status: 'draft',
|
|
445
|
+
progress: summarizeProgress([]),
|
|
446
|
+
slices: [],
|
|
447
|
+
}
|
|
448
|
+
: null;
|
|
449
|
+
const visibleSpecs = specSlug
|
|
450
|
+
? (selectedSpec ? [selectedSpec] : [])
|
|
451
|
+
: globalSpecs;
|
|
452
|
+
const activeSlice = collectActiveSliceState(projectRoot, { slices: globalSlices });
|
|
453
|
+
const nextReady = selectNextReady({
|
|
454
|
+
graph: visibleState.graph,
|
|
455
|
+
pendingSlices: pendingGraphSlices,
|
|
456
|
+
projectRoot,
|
|
457
|
+
visibleSlices,
|
|
458
|
+
});
|
|
459
|
+
const warnings = collectWarnings({
|
|
460
|
+
graphSummary,
|
|
461
|
+
layout: exportData.migration,
|
|
462
|
+
specDirs,
|
|
463
|
+
specSlug,
|
|
464
|
+
visibleSlices,
|
|
465
|
+
});
|
|
466
|
+
const blockers = collectBlockers({
|
|
467
|
+
activeSlice,
|
|
468
|
+
graphSummary,
|
|
469
|
+
layout: exportData.migration,
|
|
470
|
+
visibleSlices,
|
|
471
|
+
});
|
|
472
|
+
const nextSteps = collectNextSteps({
|
|
473
|
+
graphSummary,
|
|
474
|
+
nextReady,
|
|
475
|
+
selectedSpec,
|
|
476
|
+
specSlug,
|
|
477
|
+
visibleSlices,
|
|
478
|
+
});
|
|
479
|
+
const agents = compactAgents(exportData.agents);
|
|
480
|
+
const approvals = compactApprovals(exportData.approvals);
|
|
481
|
+
const runs = compactRuns(exportData.runs);
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
dashboard_schema_version: DASHBOARD_SCHEMA_VERSION,
|
|
485
|
+
generated_at: new Date().toISOString(),
|
|
486
|
+
source_metadata: {
|
|
487
|
+
generator: 'create-quiver',
|
|
488
|
+
command: 'dashboard',
|
|
489
|
+
resolver: 'project-state-resolver',
|
|
490
|
+
include_completed: includeCompleted,
|
|
491
|
+
spec_filter: specSlug || null,
|
|
492
|
+
},
|
|
493
|
+
project: exportData.project,
|
|
494
|
+
summary: {
|
|
495
|
+
specs: globalSpecs.length,
|
|
496
|
+
visible_specs: visibleSpecs.length,
|
|
497
|
+
slices: globalSlices.length,
|
|
498
|
+
visible_slices: visibleSlices.length,
|
|
499
|
+
runs: runs.length,
|
|
500
|
+
configured_agents: agents.filter((agent) => agent.configured).length,
|
|
501
|
+
agents: agents.length,
|
|
502
|
+
warnings: warnings.length,
|
|
503
|
+
blockers: blockers.length,
|
|
504
|
+
layout: exportData.migration.layout,
|
|
505
|
+
},
|
|
506
|
+
global_progress: summarizeProgress(globalSlices),
|
|
507
|
+
visible_progress: summarizeProgress(visibleSlices),
|
|
508
|
+
specs: visibleSpecs,
|
|
509
|
+
slices: visibleSlices.map((slice) => normalizeSlice(projectRoot, slice)),
|
|
510
|
+
next_ready: nextReady,
|
|
511
|
+
blockers,
|
|
512
|
+
warnings,
|
|
513
|
+
graph: {
|
|
514
|
+
ok: selectedGraph.ok,
|
|
515
|
+
edges: selectedGraph.ok ? selectedGraph.edges : [],
|
|
516
|
+
levels: selectedGraph.ok ? selectedGraph.levels : [],
|
|
517
|
+
conflicts: selectedGraph.ok ? selectedGraph.conflicts : [],
|
|
518
|
+
error: selectedGraph.ok ? null : selectedGraph.error,
|
|
519
|
+
},
|
|
520
|
+
agents,
|
|
521
|
+
approvals,
|
|
522
|
+
runs,
|
|
523
|
+
active_slice: {
|
|
524
|
+
supported_sources: activeSlice.supported_sources,
|
|
525
|
+
sources_count: activeSlice.sources.length,
|
|
526
|
+
reconciliation: activeSlice.reconciliation,
|
|
527
|
+
},
|
|
528
|
+
evidence: collectEvidenceSummary(visibleSlices),
|
|
529
|
+
next_steps: nextSteps,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function formatProgress(progress) {
|
|
534
|
+
return `${progress.completed}/${progress.total} completed, ${progress.open} open, ${progress.blocked} blocked, ${progress.percent}%`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function dashboardOptionError(message, nextCommand = 'npx create-quiver dashboard --help') {
|
|
538
|
+
return new DashboardError('INVALID_DASHBOARD_OPTIONS', message, nextCommand);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function parseDashboardLimit(rawLimit, errors) {
|
|
542
|
+
if (rawLimit === null || typeof rawLimit === 'undefined' || rawLimit === '') {
|
|
543
|
+
return DEFAULT_DASHBOARD_LIMIT;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const value = String(rawLimit).trim();
|
|
547
|
+
if (!/^[1-9]\d*$/.test(value)) {
|
|
548
|
+
errors.push('invalid value for --limit: expected an integer from 1 to 100');
|
|
549
|
+
return DEFAULT_DASHBOARD_LIMIT;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const parsed = Number.parseInt(value, 10);
|
|
553
|
+
if (parsed > MAX_DASHBOARD_LIMIT) {
|
|
554
|
+
errors.push('invalid value for --limit: expected an integer from 1 to 100');
|
|
555
|
+
return DEFAULT_DASHBOARD_LIMIT;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return parsed;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function normalizeDashboardOptions(options = {}) {
|
|
562
|
+
const errors = Array.isArray(options.optionErrors) ? [...options.optionErrors] : [];
|
|
563
|
+
const details = options.details === true;
|
|
564
|
+
const section = String(options.section || '').trim();
|
|
565
|
+
const hasLimit = options.limit !== null && typeof options.limit !== 'undefined';
|
|
566
|
+
const limit = parseDashboardLimit(options.limit, errors);
|
|
567
|
+
|
|
568
|
+
if (section && !SUPPORTED_DASHBOARD_SECTIONS.includes(section)) {
|
|
569
|
+
errors.push(`unsupported dashboard section: ${section}. Supported sections: ${SUPPORTED_DASHBOARD_SECTIONS.join(', ')}`);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (details && section) {
|
|
573
|
+
errors.push('dashboard --details cannot be combined with --section <name>');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (options.json && (details || section || hasLimit || errors.length > 0)) {
|
|
577
|
+
errors.push('dashboard --json cannot be combined with human-only flags: --details, --section, --limit');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (errors.length > 0) {
|
|
581
|
+
throw dashboardOptionError(errors.join(' '));
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
details,
|
|
586
|
+
limit,
|
|
587
|
+
section,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function truncateText(value, maxLength = 96) {
|
|
592
|
+
const text = String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
593
|
+
if (text.length <= maxLength) {
|
|
594
|
+
return text;
|
|
595
|
+
}
|
|
596
|
+
return `${text.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function dashboardCommand(report, suffix = '') {
|
|
600
|
+
const parts = ['npx create-quiver dashboard'];
|
|
601
|
+
if (report.source_metadata.spec_filter) {
|
|
602
|
+
parts.push('--spec', report.source_metadata.spec_filter);
|
|
603
|
+
}
|
|
604
|
+
if (report.source_metadata.include_completed) {
|
|
605
|
+
parts.push('--include-completed');
|
|
606
|
+
}
|
|
607
|
+
if (suffix) {
|
|
608
|
+
parts.push(suffix);
|
|
609
|
+
}
|
|
610
|
+
return parts.join(' ');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function truncationLine(report, section, hidden) {
|
|
614
|
+
return `- + ${hidden} more. Run: ${dashboardCommand(report, `--section ${section}`)}`;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function pushLimitedList(lines, report, section, items, limit, formatter) {
|
|
618
|
+
if (!items.length) {
|
|
619
|
+
lines.push('- none');
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
for (const item of items.slice(0, limit)) {
|
|
624
|
+
lines.push(`- ${formatter(item)}`);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const hidden = items.length - limit;
|
|
628
|
+
if (hidden > 0) {
|
|
629
|
+
lines.push(truncationLine(report, section, hidden));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function formatSpecLine(spec) {
|
|
634
|
+
return `${spec.slug}: ${spec.status}, ${spec.progress.percent}% (${spec.progress.completed}/${spec.progress.total})`;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function formatSliceLine(slice) {
|
|
638
|
+
const blocked = slice.blocked_reason ? ` blocked=${truncateText(slice.blocked_reason, 48)}` : '';
|
|
639
|
+
return `${slice.ref}: ${slice.status}, ${slice.progress}%${blocked}`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function formatAgentLine(agent) {
|
|
643
|
+
return `${agent.role}: ${agent.configured ? agent.provider || 'configured' : 'missing'}`;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function formatApprovalLine(approval) {
|
|
647
|
+
const version = approval.approved_version ? ` approved=v${approval.approved_version}` : '';
|
|
648
|
+
return `${approval.phase}: ${approval.status}${version}`;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function formatRunLine(run) {
|
|
652
|
+
return `${run.run_id}: ${run.phase} (${run.status})`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function pushSignalSection(lines, report, title, section, items, limit, formatter) {
|
|
656
|
+
lines.push(`${title}: ${items.length === 0 ? 'none' : ''}`.trimEnd());
|
|
657
|
+
if (items.length > 0) {
|
|
658
|
+
pushLimitedList(lines, report, section, items, limit, formatter);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function formatCompactDashboard(report, options) {
|
|
663
|
+
const nextCommand = report.next_steps[0]?.command || 'none';
|
|
664
|
+
const state = report.blockers.length > 0
|
|
665
|
+
? 'blocked'
|
|
666
|
+
: report.warnings.length > 0
|
|
667
|
+
? 'warning'
|
|
668
|
+
: report.next_ready
|
|
669
|
+
? 'ready'
|
|
670
|
+
: 'idle';
|
|
671
|
+
const lines = [
|
|
672
|
+
'Quiver Dashboard',
|
|
673
|
+
`Project: ${truncateText(report.project.name, 42)} | Layout: ${report.summary.layout} | Filter: ${report.source_metadata.spec_filter || 'all specs'}`,
|
|
674
|
+
`State: ${state}`,
|
|
675
|
+
`Next safe command: ${nextCommand}`,
|
|
676
|
+
`Progress: Global: ${formatProgress(report.global_progress)} | Visible: ${formatProgress(report.visible_progress)}`,
|
|
677
|
+
`Counts: specs ${report.summary.specs} (${report.summary.visible_specs} visible), slices ${report.summary.slices} (${report.summary.visible_slices} visible), agents ${report.summary.configured_agents}/${report.summary.agents}, runs ${report.summary.runs}`,
|
|
678
|
+
`Next ready slice: ${report.next_ready ? `${report.next_ready.ref} - ${truncateText(report.next_ready.title, 48)}` : 'none'}`,
|
|
679
|
+
];
|
|
680
|
+
|
|
681
|
+
pushSignalSection(lines, report, 'Blockers', 'blockers', report.blockers, options.limit, (blocker) => `${blocker.ref}: ${truncateText(blocker.reason, 70)}`);
|
|
682
|
+
pushSignalSection(lines, report, 'Warnings', 'warnings', report.warnings, options.limit, (warning) => `${warning.code}: ${truncateText(warning.message, 70)}`);
|
|
683
|
+
|
|
684
|
+
lines.push(`Active slice: ${report.active_slice.reconciliation.decision} (${truncateText(report.active_slice.reconciliation.reason, 60)})`);
|
|
685
|
+
lines.push(`Inspect: ${dashboardCommand(report, '--details')}`);
|
|
686
|
+
lines.push('');
|
|
687
|
+
return `${lines.join('\n')}\n`;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function formatDashboardOverview(report) {
|
|
691
|
+
const lines = [
|
|
692
|
+
'Dashboard Overview',
|
|
693
|
+
`Project: ${report.project.name}`,
|
|
694
|
+
`Layout: ${report.summary.layout}`,
|
|
695
|
+
`Filter: ${report.source_metadata.spec_filter || 'all specs'}`,
|
|
696
|
+
`Completed hidden: ${report.source_metadata.include_completed ? 'no' : 'yes'}`,
|
|
697
|
+
`Global progress: ${formatProgress(report.global_progress)}`,
|
|
698
|
+
`Visible progress: ${formatProgress(report.visible_progress)}`,
|
|
699
|
+
`Specs: ${report.summary.specs} (${report.summary.visible_specs} visible)`,
|
|
700
|
+
`Slices: ${report.summary.slices} (${report.summary.visible_slices} visible)`,
|
|
701
|
+
`Blockers: ${report.summary.blockers}`,
|
|
702
|
+
`Warnings: ${report.summary.warnings}`,
|
|
703
|
+
`Runs: ${report.summary.runs}`,
|
|
704
|
+
'',
|
|
705
|
+
];
|
|
706
|
+
|
|
707
|
+
return `${lines.join('\n')}\n`;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function formatDashboardSection(report, options) {
|
|
711
|
+
const lines = [];
|
|
712
|
+
const limit = options.limit;
|
|
713
|
+
|
|
714
|
+
if (options.section === 'overview') {
|
|
715
|
+
return formatDashboardOverview(report);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (options.section === 'specs') {
|
|
719
|
+
lines.push('Specs');
|
|
720
|
+
pushLimitedList(lines, report, 'specs', report.specs, limit, formatSpecLine);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (options.section === 'slices') {
|
|
724
|
+
lines.push('Slices');
|
|
725
|
+
pushLimitedList(lines, report, 'slices', report.slices, limit, formatSliceLine);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (options.section === 'blockers') {
|
|
729
|
+
lines.push('Blockers');
|
|
730
|
+
pushLimitedList(lines, report, 'blockers', report.blockers, limit, (blocker) => `${blocker.ref}: ${blocker.reason}`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (options.section === 'warnings') {
|
|
734
|
+
lines.push('Warnings');
|
|
735
|
+
pushLimitedList(lines, report, 'warnings', report.warnings, limit, (warning) => `${warning.code}: ${warning.message}`);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (options.section === 'agents') {
|
|
739
|
+
lines.push('Agents');
|
|
740
|
+
pushLimitedList(lines, report, 'agents', report.agents, limit, formatAgentLine);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (options.section === 'approvals') {
|
|
744
|
+
lines.push('Approvals');
|
|
745
|
+
pushLimitedList(lines, report, 'approvals', report.approvals, limit, formatApprovalLine);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (options.section === 'runs') {
|
|
749
|
+
lines.push('Runs');
|
|
750
|
+
pushLimitedList(lines, report, 'runs', report.runs, limit, formatRunLine);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (options.section === 'active-slice') {
|
|
754
|
+
lines.push(
|
|
755
|
+
'Active slice',
|
|
756
|
+
`- Sources: ${report.active_slice.sources_count}`,
|
|
757
|
+
`- Reconciliation: ${report.active_slice.reconciliation.decision} (${report.active_slice.reconciliation.reason})`,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (options.section === 'next-steps') {
|
|
762
|
+
lines.push('Next safe commands');
|
|
763
|
+
pushLimitedList(lines, report, 'next-steps', report.next_steps, limit, (step) => step.command);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
lines.push('');
|
|
767
|
+
return `${lines.join('\n')}\n`;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function formatDashboardDetails(report) {
|
|
771
|
+
const lines = [
|
|
772
|
+
'Quiver Dashboard',
|
|
773
|
+
`Project: ${report.project.name}`,
|
|
774
|
+
`Layout: ${report.summary.layout}`,
|
|
775
|
+
`Filter: ${report.source_metadata.spec_filter || 'all specs'}`,
|
|
776
|
+
`Completed hidden: ${report.source_metadata.include_completed ? 'no' : 'yes'}`,
|
|
777
|
+
'',
|
|
778
|
+
'Progress',
|
|
779
|
+
`- Global: ${formatProgress(report.global_progress)}`,
|
|
780
|
+
`- Visible: ${formatProgress(report.visible_progress)}`,
|
|
781
|
+
'',
|
|
782
|
+
'Next ready slice',
|
|
783
|
+
];
|
|
784
|
+
|
|
785
|
+
if (report.next_ready) {
|
|
786
|
+
lines.push(`- ${report.next_ready.ref}: ${report.next_ready.title}`);
|
|
787
|
+
lines.push(`- Start: ${report.next_ready.start_slice_command}`);
|
|
788
|
+
} else {
|
|
789
|
+
lines.push('- none');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
lines.push('', 'Specs');
|
|
793
|
+
if (report.specs.length === 0) {
|
|
794
|
+
lines.push('- none');
|
|
795
|
+
} else {
|
|
796
|
+
for (const spec of report.specs) {
|
|
797
|
+
lines.push(`- ${formatSpecLine(spec)}`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
lines.push('', 'Slices');
|
|
802
|
+
if (report.slices.length === 0) {
|
|
803
|
+
lines.push('- none');
|
|
804
|
+
} else {
|
|
805
|
+
for (const slice of report.slices) {
|
|
806
|
+
lines.push(`- ${formatSliceLine(slice)}`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
lines.push('', 'Blockers');
|
|
811
|
+
if (report.blockers.length === 0) {
|
|
812
|
+
lines.push('- none');
|
|
813
|
+
} else {
|
|
814
|
+
for (const blocker of report.blockers) {
|
|
815
|
+
lines.push(`- ${blocker.ref}: ${blocker.reason}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
lines.push('', 'Warnings');
|
|
820
|
+
if (report.warnings.length === 0) {
|
|
821
|
+
lines.push('- none');
|
|
822
|
+
} else {
|
|
823
|
+
for (const warning of report.warnings) {
|
|
824
|
+
lines.push(`- ${warning.code}: ${warning.message}`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
lines.push('', 'Agents');
|
|
829
|
+
if (report.agents.length === 0) {
|
|
830
|
+
lines.push('- none');
|
|
831
|
+
} else {
|
|
832
|
+
for (const agent of report.agents) {
|
|
833
|
+
lines.push(`- ${formatAgentLine(agent)}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
lines.push('', 'Approvals');
|
|
838
|
+
if (report.approvals.length === 0) {
|
|
839
|
+
lines.push('- none');
|
|
840
|
+
} else {
|
|
841
|
+
for (const approval of report.approvals) {
|
|
842
|
+
lines.push(`- ${formatApprovalLine(approval)}`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
lines.push('', 'Runs');
|
|
847
|
+
if (report.runs.length === 0) {
|
|
848
|
+
lines.push('- none');
|
|
849
|
+
} else {
|
|
850
|
+
for (const run of report.runs) {
|
|
851
|
+
lines.push(`- ${formatRunLine(run)}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
lines.push(
|
|
856
|
+
'',
|
|
857
|
+
'Active slice',
|
|
858
|
+
`- Sources: ${report.active_slice.sources_count}`,
|
|
859
|
+
`- Reconciliation: ${report.active_slice.reconciliation.decision} (${report.active_slice.reconciliation.reason})`,
|
|
860
|
+
'',
|
|
861
|
+
'Next safe commands',
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
if (report.next_steps.length === 0) {
|
|
865
|
+
lines.push('- none');
|
|
866
|
+
} else {
|
|
867
|
+
for (const step of report.next_steps) {
|
|
868
|
+
lines.push(`- ${step.command}`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
lines.push('');
|
|
873
|
+
return `${lines.join('\n')}\n`;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function formatHumanDashboard(report, options = {}) {
|
|
877
|
+
const normalized = normalizeDashboardOptions({ ...options, json: false });
|
|
878
|
+
if (normalized.details) {
|
|
879
|
+
return formatDashboardDetails(report);
|
|
880
|
+
}
|
|
881
|
+
if (normalized.section) {
|
|
882
|
+
return formatDashboardSection(report, normalized);
|
|
883
|
+
}
|
|
884
|
+
return formatCompactDashboard(report, normalized);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
module.exports = {
|
|
888
|
+
DASHBOARD_SCHEMA_VERSION,
|
|
889
|
+
DEFAULT_DASHBOARD_LIMIT,
|
|
890
|
+
DashboardError,
|
|
891
|
+
SUPPORTED_DASHBOARD_SECTIONS,
|
|
892
|
+
buildErrorPayload,
|
|
893
|
+
collectDashboardReport,
|
|
894
|
+
formatHumanDashboard,
|
|
895
|
+
normalizeDashboardOptions,
|
|
896
|
+
};
|