create-quiver 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/.claude/settings.local.json +52 -0
  2. package/.github/workflows/ci.yml +2 -2
  3. package/BACKLOG.md +139 -0
  4. package/CHANGELOG.md +20 -1
  5. package/README.md +31 -1
  6. package/README_FOR_AI.md +25 -13
  7. package/ROADMAP.md +28 -6
  8. package/docs/AI_ONBOARDING_PROMPT.md.template +4 -0
  9. package/docs/COMMANDS.md.template +25 -0
  10. package/docs/INDEX.md.template +2 -0
  11. package/docs/SUPPORT_MATRIX.md.template +9 -0
  12. package/docs/WORKFLOW.md.template +4 -0
  13. package/docs/examples/graph.md.template +62 -0
  14. package/docs/examples/next.md.template +27 -0
  15. package/docs/examples/plan.md.template +28 -0
  16. package/package.json +5 -2
  17. package/package.template.json +8 -3
  18. package/scripts/check-slice-readiness.sh +6 -4
  19. package/scripts/init-docs.sh +57 -9
  20. package/specs/[project-name]/HANDOFF.md.template +37 -0
  21. package/specs/[project-name]/slices/slice-template/slice.json +5 -0
  22. package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-01-project-scan-command/slice.json +1 -1
  23. package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-02-ai-onboarding-prompt/slice.json +1 -1
  24. package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-03-doctor-readme-adoption-flow/slice.json +1 -1
  25. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-01-cross-platform-support-contract/slice.json +1 -1
  26. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-02-node-init-docs-runtime/slice.json +1 -1
  27. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-03-node-migrate-analyze-doctor-flow/slice.json +1 -1
  28. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-04-node-slice-lifecycle-commands/slice.json +1 -1
  29. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-05-generated-project-scripts-and-migration/slice.json +1 -1
  30. package/specs/quiver-v13-token-efficient-ai-context/EVIDENCE_REPORT.md +1 -1
  31. package/specs/quiver-v13-token-efficient-ai-context/SPEC.md +1 -1
  32. package/specs/quiver-v15-init-required-before-migrate/EVIDENCE_REPORT.md +26 -0
  33. package/specs/quiver-v15-init-required-before-migrate/SPEC.md +66 -0
  34. package/specs/quiver-v15-init-required-before-migrate/STATUS.md +26 -0
  35. package/specs/quiver-v15-init-required-before-migrate/slices/slice-01-migrate-initialization-precondition/slice.json +65 -0
  36. package/specs/quiver-v15-init-required-before-migrate/slices/slice-02-doctor-not-initialized-guidance/slice.json +61 -0
  37. package/specs/quiver-v15-init-required-before-migrate/slices/slice-03-docs-smokes-init-before-migrate/slice.json +64 -0
  38. package/specs/quiver-v16-handoff-contract/EVIDENCE_REPORT.md +26 -0
  39. package/specs/quiver-v16-handoff-contract/SPEC.md +68 -0
  40. package/specs/quiver-v16-handoff-contract/STATUS.md +26 -0
  41. package/specs/quiver-v16-handoff-contract/slices/slice-01-handoff-template-and-contract/slice.json +66 -0
  42. package/specs/quiver-v16-handoff-contract/slices/slice-02-check-handoff-command/slice.json +70 -0
  43. package/specs/quiver-v16-handoff-contract/slices/slice-03-handoff-scaffold-optional/slice.json +67 -0
  44. package/specs/quiver-v17-orchestration-foundation/EVIDENCE_REPORT.md +32 -0
  45. package/specs/quiver-v17-orchestration-foundation/SPEC.md +79 -0
  46. package/specs/quiver-v17-orchestration-foundation/STATUS.md +31 -0
  47. package/specs/quiver-v17-orchestration-foundation/slices/slice-01-ci-matrix-verified/slice.json +68 -0
  48. package/specs/quiver-v17-orchestration-foundation/slices/slice-02-slice-graph-library/slice.json +65 -0
  49. package/specs/quiver-v17-orchestration-foundation/slices/slice-03-depends-on-validation/slice.json +72 -0
  50. package/specs/quiver-v18-slice-orchestration/EVIDENCE_REPORT.md +38 -0
  51. package/specs/quiver-v18-slice-orchestration/SPEC.md +91 -0
  52. package/specs/quiver-v18-slice-orchestration/STATUS.md +33 -0
  53. package/specs/quiver-v18-slice-orchestration/slices/slice-01-plan-command/slice.json +79 -0
  54. package/specs/quiver-v18-slice-orchestration/slices/slice-02-graph-mvp-tree/slice.json +75 -0
  55. package/specs/quiver-v18-slice-orchestration/slices/slice-03-graph-extended-formats/slice.json +70 -0
  56. package/specs/quiver-v18-slice-orchestration/slices/slice-04-next-command/slice.json +73 -0
  57. package/specs/quiver-v18-stabilization/EVIDENCE_REPORT.md +26 -0
  58. package/specs/quiver-v18-stabilization/SPEC.md +62 -0
  59. package/specs/quiver-v18-stabilization/STATUS.md +30 -0
  60. package/specs/quiver-v18-stabilization/slices/slice-01-fix-legacy-dependency-resolution/CLOSURE_BRIEF.md +29 -0
  61. package/specs/quiver-v18-stabilization/slices/slice-01-fix-legacy-dependency-resolution/EXECUTION_BRIEF.md +134 -0
  62. package/specs/quiver-v18-stabilization/slices/slice-01-fix-legacy-dependency-resolution/slice.json +56 -0
  63. package/specs/quiver-v18-stabilization/slices/slice-02-roadmap-and-branch-cleanup/CLOSURE_BRIEF.md +29 -0
  64. package/specs/quiver-v18-stabilization/slices/slice-02-roadmap-and-branch-cleanup/EXECUTION_BRIEF.md +118 -0
  65. package/specs/quiver-v18-stabilization/slices/slice-02-roadmap-and-branch-cleanup/slice.json +57 -0
  66. package/specs/quiver-v18-stabilization/slices/slice-03-publish-drafts-branch/CLOSURE_BRIEF.md +23 -0
  67. package/specs/quiver-v18-stabilization/slices/slice-03-publish-drafts-branch/EXECUTION_BRIEF.md +73 -0
  68. package/specs/quiver-v18-stabilization/slices/slice-03-publish-drafts-branch/slice.json +49 -0
  69. package/specs/quiver-v19-self-install-dev-dep/EVIDENCE_REPORT.md +19 -0
  70. package/specs/quiver-v19-self-install-dev-dep/SPEC.md +51 -0
  71. package/specs/quiver-v19-self-install-dev-dep/STATUS.md +20 -0
  72. package/specs/quiver-v19-self-install-dev-dep/slices/slice-01-auto-install-dev-dep/CLOSURE_BRIEF.md +29 -0
  73. package/specs/quiver-v19-self-install-dev-dep/slices/slice-01-auto-install-dev-dep/EXECUTION_BRIEF.md +287 -0
  74. package/specs/quiver-v19-self-install-dev-dep/slices/slice-01-auto-install-dev-dep/slice.json +56 -0
  75. package/src/create-quiver/commands/graph.js +97 -0
  76. package/src/create-quiver/commands/next.js +134 -0
  77. package/src/create-quiver/commands/plan.js +205 -0
  78. package/src/create-quiver/index.js +203 -3
  79. package/src/create-quiver/lib/handoff.js +104 -0
  80. package/src/create-quiver/lib/init-docs.js +108 -13
  81. package/src/create-quiver/lib/json.js +14 -0
  82. package/src/create-quiver/lib/lifecycle.js +3 -2
  83. package/src/create-quiver/lib/readiness.js +55 -1
  84. package/src/create-quiver/lib/renderers/dot.js +129 -0
  85. package/src/create-quiver/lib/renderers/mermaid.js +119 -0
  86. package/src/create-quiver/lib/renderers/tree.js +116 -0
  87. package/src/create-quiver/lib/slice-graph.js +453 -0
  88. package/src/create-quiver/lib/slice.js +2 -1
  89. package/src/create-quiver/lib/state.js +50 -0
@@ -0,0 +1,14 @@
1
+ function stripJsonComments(text) {
2
+ return String(text || '')
3
+ .replace(/^\s*\/\/.*$/gm, '')
4
+ .replace(/\/\*[\s\S]*?\*\//g, '');
5
+ }
6
+
7
+ function parseJsonWithComments(text) {
8
+ return JSON.parse(stripJsonComments(text));
9
+ }
10
+
11
+ module.exports = {
12
+ parseJsonWithComments,
13
+ stripJsonComments,
14
+ };
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { branchDelete, catFileExists, currentBranch, fetchBranch, fetchRemote, hasLocalBranch, hasRemoteBranch, lsRemoteHeads, mergeBaseIsAncestor, revListCount, runGit, statusPorcelain, worktreeAdd, worktreeList, worktreePrune, worktreeRemove } = require('./git');
4
+ const { parseJsonWithComments } = require('./json');
4
5
  const { writeFrontMatter } = require('./init-docs');
5
6
  const { resolveTargetRoot } = require('./paths');
6
7
  const { activeSlicePath, renderActiveSlice, resolveSliceContext, safeBranchName, toAlias, validateSliceMetaForStart, worktreesRootForRepo } = require('./slice');
@@ -131,7 +132,7 @@ function refreshActiveSlicesBoard(repoRoot) {
131
132
  continue;
132
133
  }
133
134
  if (entry.isFile() && entry.name === 'slice.json' && fullPath.includes(`${path.sep}slices${path.sep}`)) {
134
- const json = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
135
+ const json = parseJsonWithComments(fs.readFileSync(fullPath, 'utf8'));
135
136
  const relPath = path.relative(repoRoot, fullPath);
136
137
  const parts = relPath.split(path.sep);
137
138
  const specFamily = parts[0];
@@ -181,7 +182,7 @@ function refreshActiveSlicesBoard(repoRoot) {
181
182
  const liveSlicePath = path.join(worktreePath, slice.relPath);
182
183
  if (fs.existsSync(liveSlicePath)) {
183
184
  try {
184
- const liveJson = JSON.parse(fs.readFileSync(liveSlicePath, 'utf8'));
185
+ const liveJson = parseJsonWithComments(fs.readFileSync(liveSlicePath, 'utf8'));
185
186
  liveStatus = liveJson.status || liveStatus;
186
187
  } catch {
187
188
  // ignore
@@ -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 = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
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, '&lt;')
5
+ .replace(/>/g, '&gt;')
6
+ .replace(/"/g, '&quot;')
7
+ .replace(/\r?\n/g, '<br/>');
8
+ }
9
+
10
+ function toConflictMap(conflicts) {
11
+ const map = new Map();
12
+
13
+ for (const conflict of Array.isArray(conflicts) ? conflicts : []) {
14
+ for (const ref of Array.isArray(conflict.slices) ? conflict.slices : []) {
15
+ map.set(ref, {
16
+ files: Array.isArray(conflict.files) ? conflict.files : [],
17
+ slices: Array.isArray(conflict.slices) ? conflict.slices : [],
18
+ });
19
+ }
20
+ }
21
+
22
+ return map;
23
+ }
24
+
25
+ function flattenVisibleNodes(report) {
26
+ const nodes = [];
27
+
28
+ for (const level of Array.isArray(report.levels) ? report.levels : []) {
29
+ for (const node of Array.isArray(level) ? level : []) {
30
+ nodes.push(node);
31
+ }
32
+ }
33
+
34
+ nodes.sort((left, right) => String(left.ref || '').localeCompare(String(right.ref || '')));
35
+ return nodes;
36
+ }
37
+
38
+ function buildNodeIds(nodes) {
39
+ const used = new Set();
40
+ const ids = new Map();
41
+
42
+ for (const node of nodes) {
43
+ const base = `n_${String(node.ref || '').replace(/[^a-zA-Z0-9_]/g, '_') || 'slice'}`;
44
+ let candidate = base;
45
+ let suffix = 2;
46
+
47
+ while (used.has(candidate)) {
48
+ candidate = `${base}_${suffix}`;
49
+ suffix += 1;
50
+ }
51
+
52
+ used.add(candidate);
53
+ ids.set(node.ref, candidate);
54
+ }
55
+
56
+ return ids;
57
+ }
58
+
59
+ function buildLabel(node, conflict, options = {}) {
60
+ const parts = [];
61
+ const conflictPrefix = conflict ? (options.unicode ? '⚠ ' : '! ') : '';
62
+ parts.push(`${conflictPrefix}${node.ref}`);
63
+
64
+ if (node.title && node.title !== node.slice_id && node.title !== node.ref) {
65
+ parts.push(node.title);
66
+ }
67
+
68
+ if (typeof node.hours === 'number') {
69
+ parts.push(`${node.hours}h`);
70
+ }
71
+
72
+ if (node.status) {
73
+ parts.push(`[${node.status}]`);
74
+ }
75
+
76
+ if (options.showConflicts && conflict && Array.isArray(conflict.files) && conflict.files.length > 0) {
77
+ parts.push(`Files: ${conflict.files.join(', ')}`);
78
+ }
79
+
80
+ return escapeMermaidLabel(parts.join('\n'));
81
+ }
82
+
83
+ function renderMermaidGraph(report, options = {}) {
84
+ const nodes = flattenVisibleNodes(report);
85
+ if (nodes.length === 0) {
86
+ return '```mermaid\nflowchart TD\n %% No pending slices found.\n```\n';
87
+ }
88
+
89
+ const nodeIds = buildNodeIds(nodes);
90
+ const nodeRefs = new Set(nodes.map((node) => node.ref));
91
+ const conflictMap = toConflictMap(report.conflicts);
92
+ const lines = ['```mermaid', 'flowchart TD'];
93
+
94
+ for (const node of nodes) {
95
+ const conflict = conflictMap.get(node.ref);
96
+ const id = nodeIds.get(node.ref);
97
+ const label = buildLabel(node, conflict, options);
98
+ lines.push(` ${id}["${label}"]`);
99
+ }
100
+
101
+ for (const node of nodes) {
102
+ const targetId = nodeIds.get(node.ref);
103
+ const deps = Array.isArray(node.depends_on) ? node.depends_on : [];
104
+ for (const dep of deps) {
105
+ if (!nodeRefs.has(dep)) {
106
+ continue;
107
+ }
108
+ const sourceId = nodeIds.get(dep);
109
+ lines.push(` ${sourceId} --> ${targetId}`);
110
+ }
111
+ }
112
+
113
+ lines.push('```');
114
+ return `${lines.join('\n')}\n`;
115
+ }
116
+
117
+ module.exports = {
118
+ renderMermaidGraph,
119
+ };
@@ -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
+ };