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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { catFileExists, currentBranch, hasLocalBranch, hasRemoteBranch, mergeBaseIsAncestor, revListCount, runGit, statusPorcelain, worktreeList } = require('./git');
|
|
4
|
+
const { parseJsonWithComments } = require('./json');
|
|
5
|
+
const { buildGraph, readAllSlices, SliceGraphError, topoSort } = require('./slice-graph');
|
|
4
6
|
const { resolveSliceContext, toAlias } = require('./slice');
|
|
5
7
|
|
|
6
8
|
function ensureExists(filePath, message) {
|
|
@@ -22,7 +24,7 @@ function walkSlices(rootDir, acc, repoRoot) {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
if (entry.isFile() && entry.name === 'slice.json' && fullPath.includes(`${path.sep}slices${path.sep}`)) {
|
|
25
|
-
const json =
|
|
27
|
+
const json = parseJsonWithComments(fs.readFileSync(fullPath, 'utf8'));
|
|
26
28
|
const branchName = json.git?.branch_name;
|
|
27
29
|
if (!branchName) {
|
|
28
30
|
continue;
|
|
@@ -93,6 +95,56 @@ function collectOverlapWarnings(repoRoot, currentBranchName, currentFiles) {
|
|
|
93
95
|
return warnings;
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
function validateDeclaredDependencyContract(repoRoot, slice) {
|
|
99
|
+
const declaredDependsOn = Array.isArray(slice.json.depends_on) ? slice.json.depends_on : null;
|
|
100
|
+
const declaredParallelSafe = typeof slice.json.parallel_safe === 'string' ? slice.json.parallel_safe.trim() : '';
|
|
101
|
+
const hasDependsOn = declaredDependsOn !== null;
|
|
102
|
+
const hasParallelSafe = declaredParallelSafe.length > 0;
|
|
103
|
+
|
|
104
|
+
if (!hasDependsOn && !hasParallelSafe) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const graph = buildGraph(readAllSlices(repoRoot));
|
|
109
|
+
const currentRef = `${slice.specSlug}/${slice.sliceId}`;
|
|
110
|
+
const currentNode = graph.nodes.find((node) => node.ref === currentRef);
|
|
111
|
+
|
|
112
|
+
if (!currentNode) {
|
|
113
|
+
throw new Error(`create-quiver: No se encontro el slice actual en el grafo: ${currentRef}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (hasDependsOn) {
|
|
117
|
+
const declared = declaredDependsOn.map((dep) => String(dep).trim()).filter(Boolean);
|
|
118
|
+
if (declared.length !== new Set(declared).size) {
|
|
119
|
+
throw new Error(`create-quiver: depends_on contiene referencias duplicadas en ${currentRef}.`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const currentSet = new Set(currentNode.depends_on || []);
|
|
123
|
+
for (const dep of declared) {
|
|
124
|
+
if (!currentSet.has(dep)) {
|
|
125
|
+
throw new Error(`create-quiver: depends_on apunta a una referencia inexistente o invalida: ${dep}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (declaredParallelSafe === 'never') {
|
|
131
|
+
const reason = typeof slice.json.parallel_safe_reason === 'string' ? slice.json.parallel_safe_reason.trim() : '';
|
|
132
|
+
if (!reason) {
|
|
133
|
+
throw new Error('create-quiver: parallel_safe="never" requiere parallel_safe_reason.');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
// If the graph contains a cycle, topoSort will surface the path.
|
|
139
|
+
topoSort(graph);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error instanceof SliceGraphError && error.code === 'CYCLE_DETECTED') {
|
|
142
|
+
throw new Error(`create-quiver: El slice declarado introduce un ciclo: ${error.message}`);
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
96
148
|
function checkSliceReadiness(sliceInput, options = {}) {
|
|
97
149
|
const gate = options.gate || 'execution';
|
|
98
150
|
const strictOverlap = options.strictOverlap === true;
|
|
@@ -125,6 +177,8 @@ function checkSliceReadiness(sliceInput, options = {}) {
|
|
|
125
177
|
}
|
|
126
178
|
}
|
|
127
179
|
|
|
180
|
+
validateDeclaredDependencyContract(repoRoot, slice);
|
|
181
|
+
|
|
128
182
|
switch (gate) {
|
|
129
183
|
case 'ready':
|
|
130
184
|
if (slice.status !== 'ready') {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
function escapeDotLabel(value) {
|
|
2
|
+
return String(value || '')
|
|
3
|
+
.replace(/\\/g, '\\\\')
|
|
4
|
+
.replace(/"/g, '\\"')
|
|
5
|
+
.replace(/\r?\n/g, '\\n');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function toConflictMap(conflicts) {
|
|
9
|
+
const map = new Map();
|
|
10
|
+
|
|
11
|
+
for (const conflict of Array.isArray(conflicts) ? conflicts : []) {
|
|
12
|
+
for (const ref of Array.isArray(conflict.slices) ? conflict.slices : []) {
|
|
13
|
+
map.set(ref, {
|
|
14
|
+
files: Array.isArray(conflict.files) ? conflict.files : [],
|
|
15
|
+
slices: Array.isArray(conflict.slices) ? conflict.slices : [],
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return map;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function flattenVisibleNodes(report) {
|
|
24
|
+
const nodes = [];
|
|
25
|
+
|
|
26
|
+
for (const level of Array.isArray(report.levels) ? report.levels : []) {
|
|
27
|
+
for (const node of Array.isArray(level) ? level : []) {
|
|
28
|
+
nodes.push(node);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
nodes.sort((left, right) => String(left.ref || '').localeCompare(String(right.ref || '')));
|
|
33
|
+
return nodes;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildNodeIds(nodes) {
|
|
37
|
+
const used = new Set();
|
|
38
|
+
const ids = new Map();
|
|
39
|
+
|
|
40
|
+
for (const node of nodes) {
|
|
41
|
+
const base = `n_${String(node.ref || '').replace(/[^a-zA-Z0-9_]/g, '_') || 'slice'}`;
|
|
42
|
+
let candidate = base;
|
|
43
|
+
let suffix = 2;
|
|
44
|
+
|
|
45
|
+
while (used.has(candidate)) {
|
|
46
|
+
candidate = `${base}_${suffix}`;
|
|
47
|
+
suffix += 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
used.add(candidate);
|
|
51
|
+
ids.set(node.ref, candidate);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return ids;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildLabel(node, conflict, options = {}) {
|
|
58
|
+
const parts = [];
|
|
59
|
+
const conflictPrefix = conflict ? (options.unicode ? '⚠ ' : '! ') : '';
|
|
60
|
+
parts.push(`${conflictPrefix}${node.ref}`);
|
|
61
|
+
|
|
62
|
+
if (node.title && node.title !== node.slice_id && node.title !== node.ref) {
|
|
63
|
+
parts.push(node.title);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof node.hours === 'number') {
|
|
67
|
+
parts.push(`${node.hours}h`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (node.status) {
|
|
71
|
+
parts.push(`[${node.status}]`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (options.showConflicts && conflict && Array.isArray(conflict.files) && conflict.files.length > 0) {
|
|
75
|
+
parts.push(`Files: ${conflict.files.join(', ')}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return escapeDotLabel(parts.join('\n'));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function renderDotGraph(report, options = {}) {
|
|
82
|
+
const nodes = flattenVisibleNodes(report);
|
|
83
|
+
if (nodes.length === 0) {
|
|
84
|
+
return 'digraph QuiverGraph {\n // No pending slices found.\n}\n';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const nodeIds = buildNodeIds(nodes);
|
|
88
|
+
const nodeRefs = new Set(nodes.map((node) => node.ref));
|
|
89
|
+
const conflictMap = toConflictMap(report.conflicts);
|
|
90
|
+
const lines = [
|
|
91
|
+
'digraph QuiverGraph {',
|
|
92
|
+
' rankdir=TB;',
|
|
93
|
+
' node [shape=box, style="rounded,filled", fillcolor="#f8f9fa", color="#495057", fontname="Helvetica"];',
|
|
94
|
+
' edge [color="#6c757d"];',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
for (const node of nodes) {
|
|
98
|
+
const conflict = conflictMap.get(node.ref);
|
|
99
|
+
const id = nodeIds.get(node.ref);
|
|
100
|
+
const label = buildLabel(node, conflict, options);
|
|
101
|
+
const attrs = [`label="${label}"`];
|
|
102
|
+
|
|
103
|
+
if (conflict) {
|
|
104
|
+
attrs.push(`fillcolor="${options.showConflicts ? '#fff3cd' : '#f8f9fa'}"`);
|
|
105
|
+
attrs.push(`color="${options.showConflicts ? '#d39e00' : '#495057'}"`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
lines.push(` ${id} [${attrs.join(', ')}];`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const node of nodes) {
|
|
112
|
+
const targetId = nodeIds.get(node.ref);
|
|
113
|
+
const deps = Array.isArray(node.depends_on) ? node.depends_on : [];
|
|
114
|
+
for (const dep of deps) {
|
|
115
|
+
if (!nodeRefs.has(dep)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const sourceId = nodeIds.get(dep);
|
|
119
|
+
lines.push(` ${sourceId} -> ${targetId};`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
lines.push('}');
|
|
124
|
+
return `${lines.join('\n')}\n`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
renderDotGraph,
|
|
129
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
function escapeMermaidLabel(value) {
|
|
2
|
+
return String(value || '')
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/"/g, '"')
|
|
7
|
+
.replace(/\r?\n/g, '<br/>');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function toConflictMap(conflicts) {
|
|
11
|
+
const map = new Map();
|
|
12
|
+
|
|
13
|
+
for (const conflict of Array.isArray(conflicts) ? conflicts : []) {
|
|
14
|
+
for (const ref of Array.isArray(conflict.slices) ? conflict.slices : []) {
|
|
15
|
+
map.set(ref, {
|
|
16
|
+
files: Array.isArray(conflict.files) ? conflict.files : [],
|
|
17
|
+
slices: Array.isArray(conflict.slices) ? conflict.slices : [],
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return map;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function flattenVisibleNodes(report) {
|
|
26
|
+
const nodes = [];
|
|
27
|
+
|
|
28
|
+
for (const level of Array.isArray(report.levels) ? report.levels : []) {
|
|
29
|
+
for (const node of Array.isArray(level) ? level : []) {
|
|
30
|
+
nodes.push(node);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
nodes.sort((left, right) => String(left.ref || '').localeCompare(String(right.ref || '')));
|
|
35
|
+
return nodes;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildNodeIds(nodes) {
|
|
39
|
+
const used = new Set();
|
|
40
|
+
const ids = new Map();
|
|
41
|
+
|
|
42
|
+
for (const node of nodes) {
|
|
43
|
+
const base = `n_${String(node.ref || '').replace(/[^a-zA-Z0-9_]/g, '_') || 'slice'}`;
|
|
44
|
+
let candidate = base;
|
|
45
|
+
let suffix = 2;
|
|
46
|
+
|
|
47
|
+
while (used.has(candidate)) {
|
|
48
|
+
candidate = `${base}_${suffix}`;
|
|
49
|
+
suffix += 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
used.add(candidate);
|
|
53
|
+
ids.set(node.ref, candidate);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return ids;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildLabel(node, conflict, options = {}) {
|
|
60
|
+
const parts = [];
|
|
61
|
+
const conflictPrefix = conflict ? (options.unicode ? '⚠ ' : '! ') : '';
|
|
62
|
+
parts.push(`${conflictPrefix}${node.ref}`);
|
|
63
|
+
|
|
64
|
+
if (node.title && node.title !== node.slice_id && node.title !== node.ref) {
|
|
65
|
+
parts.push(node.title);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof node.hours === 'number') {
|
|
69
|
+
parts.push(`${node.hours}h`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (node.status) {
|
|
73
|
+
parts.push(`[${node.status}]`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (options.showConflicts && conflict && Array.isArray(conflict.files) && conflict.files.length > 0) {
|
|
77
|
+
parts.push(`Files: ${conflict.files.join(', ')}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return escapeMermaidLabel(parts.join('\n'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function renderMermaidGraph(report, options = {}) {
|
|
84
|
+
const nodes = flattenVisibleNodes(report);
|
|
85
|
+
if (nodes.length === 0) {
|
|
86
|
+
return '```mermaid\nflowchart TD\n %% No pending slices found.\n```\n';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const nodeIds = buildNodeIds(nodes);
|
|
90
|
+
const nodeRefs = new Set(nodes.map((node) => node.ref));
|
|
91
|
+
const conflictMap = toConflictMap(report.conflicts);
|
|
92
|
+
const lines = ['```mermaid', 'flowchart TD'];
|
|
93
|
+
|
|
94
|
+
for (const node of nodes) {
|
|
95
|
+
const conflict = conflictMap.get(node.ref);
|
|
96
|
+
const id = nodeIds.get(node.ref);
|
|
97
|
+
const label = buildLabel(node, conflict, options);
|
|
98
|
+
lines.push(` ${id}["${label}"]`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const node of nodes) {
|
|
102
|
+
const targetId = nodeIds.get(node.ref);
|
|
103
|
+
const deps = Array.isArray(node.depends_on) ? node.depends_on : [];
|
|
104
|
+
for (const dep of deps) {
|
|
105
|
+
if (!nodeRefs.has(dep)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const sourceId = nodeIds.get(dep);
|
|
109
|
+
lines.push(` ${sourceId} --> ${targetId}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
lines.push('```');
|
|
114
|
+
return `${lines.join('\n')}\n`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
renderMermaidGraph,
|
|
119
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
function isUnicodeEnabled(options = {}) {
|
|
2
|
+
if (options.unicode === true) {
|
|
3
|
+
return true;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const locale = `${process.env.LANG || ''} ${process.env.LC_ALL || ''} ${process.env.LC_CTYPE || ''}`;
|
|
7
|
+
return /UTF-8/i.test(locale);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function toConflictMap(conflicts) {
|
|
11
|
+
const map = new Map();
|
|
12
|
+
|
|
13
|
+
for (const conflict of Array.isArray(conflicts) ? conflicts : []) {
|
|
14
|
+
for (const ref of Array.isArray(conflict.slices) ? conflict.slices : []) {
|
|
15
|
+
map.set(ref, {
|
|
16
|
+
files: Array.isArray(conflict.files) ? conflict.files : [],
|
|
17
|
+
slices: Array.isArray(conflict.slices) ? conflict.slices : [],
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return map;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function countParallelLots(level, conflicts) {
|
|
26
|
+
const conflictRefs = new Set((Array.isArray(conflicts) ? conflicts : []).flatMap((group) => group.slices || []));
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
|
|
29
|
+
let lots = Array.isArray(conflicts) ? conflicts.length : 0;
|
|
30
|
+
|
|
31
|
+
for (const slice of Array.isArray(level) ? level : []) {
|
|
32
|
+
if (seen.has(slice.ref)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!conflictRefs.has(slice.ref)) {
|
|
37
|
+
lots += 1;
|
|
38
|
+
seen.add(slice.ref);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const group = (Array.isArray(conflicts) ? conflicts : []).find((entry) => Array.isArray(entry.slices) && entry.slices.includes(slice.ref));
|
|
43
|
+
if (!group) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const ref of group.slices || []) {
|
|
48
|
+
seen.add(ref);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return Math.max(lots, 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatLevelHeader(levelIndex, level, conflicts, options = {}) {
|
|
56
|
+
const unicode = isUnicodeEnabled(options);
|
|
57
|
+
const label = unicode ? `Level ${levelIndex}` : `Level ${levelIndex}`;
|
|
58
|
+
const lotCount = countParallelLots(level, conflicts);
|
|
59
|
+
return `${label} (${level.length} slices, ${lotCount} lots)`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatConflictSuffix(conflict, options = {}) {
|
|
63
|
+
if (!conflict || !Array.isArray(conflict.files) || conflict.files.length === 0) {
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (options.showConflicts) {
|
|
68
|
+
return ` [${conflict.files.join(', ')}]`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderTreeGraph(report, options = {}) {
|
|
75
|
+
const unicode = isUnicodeEnabled(options);
|
|
76
|
+
const branch = unicode ? '├─' : '+--';
|
|
77
|
+
const lastBranch = unicode ? '└─' : '\\--';
|
|
78
|
+
const pipe = unicode ? '│ ' : '| ';
|
|
79
|
+
const conflictMap = toConflictMap(report.conflicts);
|
|
80
|
+
const lines = [];
|
|
81
|
+
|
|
82
|
+
if (!Array.isArray(report.levels) || report.levels.length === 0) {
|
|
83
|
+
return 'No pending slices found.\n';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
lines.push('Quiver graph');
|
|
87
|
+
|
|
88
|
+
report.levels.forEach((level, levelIndex) => {
|
|
89
|
+
const levelConflicts = Array.isArray(report.conflicts)
|
|
90
|
+
? report.conflicts.filter((conflict) => conflict.level === levelIndex)
|
|
91
|
+
: [];
|
|
92
|
+
lines.push(formatLevelHeader(levelIndex, level, levelConflicts, options));
|
|
93
|
+
|
|
94
|
+
level.forEach((slice, sliceIndex) => {
|
|
95
|
+
const connector = sliceIndex === level.length - 1 ? lastBranch : branch;
|
|
96
|
+
const conflict = conflictMap.get(slice.ref);
|
|
97
|
+
const marker = conflict ? (unicode ? '⚠' : '!') : ' ';
|
|
98
|
+
const sharedPaths = conflict && options.showConflicts ? ` ${formatConflictSuffix(conflict, options)}` : '';
|
|
99
|
+
const title = slice.title ? ` ${slice.title}` : '';
|
|
100
|
+
const hours = typeof slice.hours === 'number' ? ` (${slice.hours}h)` : '';
|
|
101
|
+
const status = slice.status ? ` [${slice.status}]` : '';
|
|
102
|
+
lines.push(`${connector} ${marker} ${slice.ref}${title}${hours}${status}${sharedPaths}`.replace(/\s+/g, ' ').trimEnd());
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (levelIndex < report.levels.length - 1) {
|
|
106
|
+
lines.push(unicode ? pipe.trimEnd() : '|');
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return `${lines.join('\n')}\n`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
renderTreeGraph,
|
|
115
|
+
isUnicodeEnabled,
|
|
116
|
+
};
|