create-quiver 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +49 -17
- package/README_FOR_AI.md +31 -29
- package/ROADMAP.md +15 -3
- package/docs/AI_ONBOARDING_PROMPT.md.template +7 -1
- package/docs/COMMANDS.md.template +44 -18
- package/docs/STATUS.md.template +5 -1
- package/docs/WORKFLOW.md.template +13 -11
- package/package.json +9 -3
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EVIDENCE_REPORT.md +293 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EXECUTION_PLAN.md +58 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/SPEC.md +242 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/STATUS.md +35 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/pr.md +77 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/slice.json +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/CLOSURE_BRIEF.md +43 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/slice.json +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/slice.json +53 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/slice.json +55 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/CLOSURE_BRIEF.md +39 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/slice.json +53 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/CLOSURE_BRIEF.md +38 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/CLOSURE_BRIEF.md +39 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/slice.json +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/slice.json +53 -0
- package/specs/quiver-v26-0121-smoke-hardening/EVIDENCE_REPORT.md +208 -0
- package/specs/quiver-v26-0121-smoke-hardening/EXECUTION_PLAN.md +57 -0
- package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +137 -0
- package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +32 -0
- package/specs/quiver-v26-0121-smoke-hardening/pr.md +96 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/slice.json +73 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/CLOSURE_BRIEF.md +38 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/EXECUTION_BRIEF.md +51 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/slice.json +76 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/CLOSURE_BRIEF.md +37 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/slice.json +75 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/CLOSURE_BRIEF.md +37 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/slice.json +77 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/slice.json +77 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/slice.json +84 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/slice.json +82 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/slice.json +92 -0
- package/src/create-quiver/commands/ai.js +577 -27
- package/src/create-quiver/commands/flow.js +6 -5
- package/src/create-quiver/commands/graph.js +6 -4
- package/src/create-quiver/commands/plan.js +3 -3
- package/src/create-quiver/index.js +328 -12
- package/src/create-quiver/lib/actionable-error.js +27 -0
- package/src/create-quiver/lib/agent-profiles.js +1 -1
- package/src/create-quiver/lib/ai/context-packs.js +4 -0
- package/src/create-quiver/lib/ai/execution-plan.js +7 -1
- package/src/create-quiver/lib/ai/executor.js +270 -20
- package/src/create-quiver/lib/ai/export-state.js +534 -0
- package/src/create-quiver/lib/ai/github.js +83 -0
- package/src/create-quiver/lib/ai/onboarding-template.js +215 -2
- package/src/create-quiver/lib/ai/plan-review.js +5 -2
- package/src/create-quiver/lib/ai/providers.js +4 -3
- package/src/create-quiver/lib/ai/run-state.js +414 -0
- package/src/create-quiver/lib/ai/spec-generator.js +12 -0
- package/src/create-quiver/lib/ai/spec-templates.js +78 -9
- package/src/create-quiver/lib/approvals.js +22 -3
- package/src/create-quiver/lib/demo.js +189 -14
- package/src/create-quiver/lib/doctor.js +75 -0
- package/src/create-quiver/lib/handoff.js +81 -12
- package/src/create-quiver/lib/init-docs.js +24 -6
- package/src/create-quiver/lib/init-layout.js +8 -0
- package/src/create-quiver/lib/json.js +53 -3
- package/src/create-quiver/lib/readiness.js +18 -3
- package/src/create-quiver/lib/scope.js +50 -7
- package/src/create-quiver/lib/slice-graph.js +138 -38
- package/src/create-quiver/lib/slice.js +6 -1
- package/src/create-quiver/lib/spec-worktrees.js +16 -2
|
@@ -1,7 +1,57 @@
|
|
|
1
1
|
function stripJsonComments(text) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
const input = String(text || '');
|
|
3
|
+
let output = '';
|
|
4
|
+
let inString = false;
|
|
5
|
+
let escaped = false;
|
|
6
|
+
|
|
7
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
8
|
+
const char = input[index];
|
|
9
|
+
const next = input[index + 1];
|
|
10
|
+
|
|
11
|
+
if (inString) {
|
|
12
|
+
output += char;
|
|
13
|
+
if (escaped) {
|
|
14
|
+
escaped = false;
|
|
15
|
+
} else if (char === '\\') {
|
|
16
|
+
escaped = true;
|
|
17
|
+
} else if (char === '"') {
|
|
18
|
+
inString = false;
|
|
19
|
+
}
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (char === '"') {
|
|
24
|
+
inString = true;
|
|
25
|
+
output += char;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (char === '/' && next === '/') {
|
|
30
|
+
while (index < input.length && input[index] !== '\n') {
|
|
31
|
+
index += 1;
|
|
32
|
+
}
|
|
33
|
+
if (index < input.length) {
|
|
34
|
+
output += '\n';
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (char === '/' && next === '*') {
|
|
40
|
+
index += 2;
|
|
41
|
+
while (index < input.length && !(input[index] === '*' && input[index + 1] === '/')) {
|
|
42
|
+
if (input[index] === '\n') {
|
|
43
|
+
output += '\n';
|
|
44
|
+
}
|
|
45
|
+
index += 1;
|
|
46
|
+
}
|
|
47
|
+
index += 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
output += char;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return output;
|
|
5
55
|
}
|
|
6
56
|
|
|
7
57
|
function parseJsonWithComments(text) {
|
|
@@ -2,7 +2,7 @@ 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
4
|
const { parseJsonWithComments } = require('./json');
|
|
5
|
-
const { buildGraph, readAllSlices, SliceGraphError, topoSort } = require('./slice-graph');
|
|
5
|
+
const { buildGraph, normalizeDeclaredDependencies, readAllSlices, SliceGraphError, topoSort } = require('./slice-graph');
|
|
6
6
|
const { resolveSliceContext, toAlias } = require('./slice');
|
|
7
7
|
|
|
8
8
|
function ensureExists(filePath, message) {
|
|
@@ -117,6 +117,17 @@ function baseRecoveryMessage(remote, baseBranch) {
|
|
|
117
117
|
return `No se encontro la base '${baseBranch}' como rama local ni como '${remote}/${baseBranch}'. Para validacion estructural usa --local; para validacion contra otra base usa --base <branch>; o configura/fetchea el remoto '${remote}'.`;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
function resolveReadinessRoot(localMode) {
|
|
121
|
+
try {
|
|
122
|
+
return runGit(['rev-parse', '--show-toplevel'], process.cwd());
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (localMode) {
|
|
125
|
+
return process.cwd();
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
function validateSliceDocumentedOnBase(repoRoot, slice, options = {}) {
|
|
121
132
|
const gate = options.gate || 'execution';
|
|
122
133
|
const remote = options.remote || 'origin';
|
|
@@ -178,8 +189,12 @@ function validateDeclaredDependencyContract(repoRoot, slice) {
|
|
|
178
189
|
throw new Error(`create-quiver: depends_on contiene referencias duplicadas en ${currentRef}.`);
|
|
179
190
|
}
|
|
180
191
|
|
|
192
|
+
const normalizedDeclared = normalizeDeclaredDependencies(currentNode, declared);
|
|
193
|
+
if (normalizedDeclared.length !== new Set(normalizedDeclared).size) {
|
|
194
|
+
throw new Error(`create-quiver: depends_on contiene referencias duplicadas en ${currentRef}.`);
|
|
195
|
+
}
|
|
181
196
|
const currentSet = new Set(currentNode.depends_on || []);
|
|
182
|
-
for (const dep of
|
|
197
|
+
for (const dep of normalizedDeclared) {
|
|
183
198
|
if (!currentSet.has(dep)) {
|
|
184
199
|
throw new Error(`create-quiver: depends_on apunta a una referencia inexistente o invalida: ${dep}`);
|
|
185
200
|
}
|
|
@@ -209,7 +224,7 @@ function checkSliceReadiness(sliceInput, options = {}) {
|
|
|
209
224
|
const localMode = options.local === true;
|
|
210
225
|
const strictOverlap = options.strictOverlap === true;
|
|
211
226
|
const remote = options.remote || 'origin';
|
|
212
|
-
const repoRoot =
|
|
227
|
+
const repoRoot = resolveReadinessRoot(localMode);
|
|
213
228
|
const slice = resolveSliceContext(repoRoot, sliceInput);
|
|
214
229
|
const baseBranch = options.baseBranch || slice.baseBranch || 'develop';
|
|
215
230
|
|
|
@@ -19,6 +19,48 @@ function normalizeScopePath(filePath) {
|
|
|
19
19
|
return normalizeContextPath(filePath);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function globToRegExp(pattern) {
|
|
23
|
+
const source = String(pattern || '')
|
|
24
|
+
.split('')
|
|
25
|
+
.map((char, index, chars) => {
|
|
26
|
+
if (char === '*') {
|
|
27
|
+
return chars[index + 1] === '*' ? '\0' : '[^/]*';
|
|
28
|
+
}
|
|
29
|
+
if (char === '\0') {
|
|
30
|
+
return '.*';
|
|
31
|
+
}
|
|
32
|
+
return /[\\^$+?.()|[\]{}]/.test(char) ? `\\${char}` : char;
|
|
33
|
+
})
|
|
34
|
+
.join('')
|
|
35
|
+
.replace(/\0\[\^\/\]\*/g, '.*');
|
|
36
|
+
|
|
37
|
+
return new RegExp(`^${source}$`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function allowedPathMatches(filePath, allowedPath) {
|
|
41
|
+
const file = normalizeScopePath(filePath);
|
|
42
|
+
const allowed = normalizeScopePath(allowedPath);
|
|
43
|
+
|
|
44
|
+
if (!file || !allowed) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (file === allowed) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (allowed.endsWith('/**')) {
|
|
53
|
+
const prefix = allowed.slice(0, -3);
|
|
54
|
+
return file === prefix || file.startsWith(`${prefix}/`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (allowed.includes('*')) {
|
|
58
|
+
return globToRegExp(allowed).test(file);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
22
64
|
function parseStatusPorcelain(text) {
|
|
23
65
|
if (!text) {
|
|
24
66
|
return [];
|
|
@@ -74,30 +116,30 @@ function diffWorktreeSnapshots(beforeSnapshot, afterSnapshot) {
|
|
|
74
116
|
}
|
|
75
117
|
|
|
76
118
|
function validateScopeSnapshot({ allowedFiles = [], beforeSnapshot, afterSnapshot, strict = true } = {}) {
|
|
77
|
-
const normalizedAllowedFiles = new Set(
|
|
119
|
+
const normalizedAllowedFiles = Array.from(new Set(
|
|
78
120
|
Array.isArray(allowedFiles)
|
|
79
121
|
? allowedFiles.map(normalizeScopePath).filter(Boolean)
|
|
80
122
|
: [],
|
|
81
|
-
);
|
|
123
|
+
));
|
|
82
124
|
const changedFiles = diffWorktreeSnapshots(beforeSnapshot, afterSnapshot);
|
|
83
|
-
const outOfScopeFiles = changedFiles.filter((file) => !normalizedAllowedFiles.
|
|
125
|
+
const outOfScopeFiles = changedFiles.filter((file) => !normalizedAllowedFiles.some((allowedFile) => allowedPathMatches(file, allowedFile)));
|
|
84
126
|
|
|
85
127
|
if (outOfScopeFiles.length === 0) {
|
|
86
128
|
return {
|
|
87
129
|
ok: true,
|
|
88
130
|
changedFiles,
|
|
89
131
|
outOfScopeFiles,
|
|
90
|
-
allowedFiles:
|
|
132
|
+
allowedFiles: normalizedAllowedFiles,
|
|
91
133
|
beforeSnapshot,
|
|
92
134
|
afterSnapshot,
|
|
93
135
|
};
|
|
94
136
|
}
|
|
95
137
|
|
|
96
138
|
const message = formatError(
|
|
97
|
-
`scope violation detected: changed files outside slice
|
|
139
|
+
`scope violation detected: changed files outside declared slice scope: ${outOfScopeFiles.join(', ')}`,
|
|
98
140
|
);
|
|
99
141
|
const error = new ScopeValidationError('SCOPE_VIOLATION', message, {
|
|
100
|
-
allowedFiles:
|
|
142
|
+
allowedFiles: normalizedAllowedFiles,
|
|
101
143
|
beforeSnapshot,
|
|
102
144
|
afterSnapshot,
|
|
103
145
|
changedFiles,
|
|
@@ -112,7 +154,7 @@ function validateScopeSnapshot({ allowedFiles = [], beforeSnapshot, afterSnapsho
|
|
|
112
154
|
ok: false,
|
|
113
155
|
changedFiles,
|
|
114
156
|
outOfScopeFiles,
|
|
115
|
-
allowedFiles:
|
|
157
|
+
allowedFiles: normalizedAllowedFiles,
|
|
116
158
|
beforeSnapshot,
|
|
117
159
|
afterSnapshot,
|
|
118
160
|
error,
|
|
@@ -121,6 +163,7 @@ function validateScopeSnapshot({ allowedFiles = [], beforeSnapshot, afterSnapsho
|
|
|
121
163
|
|
|
122
164
|
module.exports = {
|
|
123
165
|
ScopeValidationError,
|
|
166
|
+
allowedPathMatches,
|
|
124
167
|
captureWorktreeSnapshot,
|
|
125
168
|
diffWorktreeSnapshots,
|
|
126
169
|
checkScope,
|
|
@@ -66,6 +66,15 @@ function sortFileList(files) {
|
|
|
66
66
|
return Array.from(new Set((Array.isArray(files) ? files : []).map((file) => String(file)).filter(Boolean))).sort((a, b) => a.localeCompare(b));
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
function resolveWriteScope(json) {
|
|
70
|
+
const allowedWritePaths = sortFileList(json?.allowed_write_paths);
|
|
71
|
+
if (allowedWritePaths.length > 0) {
|
|
72
|
+
return allowedWritePaths;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return sortFileList(json?.files);
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
function normalizeDependencyRef(slice, dependency) {
|
|
70
79
|
const dep = String(dependency || '').trim();
|
|
71
80
|
if (!dep) {
|
|
@@ -95,6 +104,61 @@ function normalizeDependencyRef(slice, dependency) {
|
|
|
95
104
|
return `${slice.specSlug}/${dep}`;
|
|
96
105
|
}
|
|
97
106
|
|
|
107
|
+
function readSliceFile(rootDir, rootName, specSlug, sliceDirName) {
|
|
108
|
+
const sliceDir = path.join(rootDir, rootName, specSlug, 'slices', sliceDirName);
|
|
109
|
+
const slicePath = path.join(sliceDir, 'slice.json');
|
|
110
|
+
if (!fs.existsSync(slicePath)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const json = parseJsonWithComments(fs.readFileSync(slicePath, 'utf8'));
|
|
115
|
+
const sliceId = String(json.slice_id || sliceDirName).trim();
|
|
116
|
+
const ref = `${specSlug}/${sliceId}`;
|
|
117
|
+
return {
|
|
118
|
+
ref,
|
|
119
|
+
specFamily: rootName,
|
|
120
|
+
specSlug,
|
|
121
|
+
sliceId,
|
|
122
|
+
slicePath,
|
|
123
|
+
sliceDir,
|
|
124
|
+
files: resolveWriteScope(json),
|
|
125
|
+
expected_read_paths: sortFileList(json.expected_read_paths),
|
|
126
|
+
allowed_write_paths: sortFileList(json.allowed_write_paths),
|
|
127
|
+
validation_hints: sortFileList(json.validation_hints),
|
|
128
|
+
dependencies: Array.isArray(json.dependencies) ? json.dependencies.map((item) => String(item).trim()).filter(Boolean) : [],
|
|
129
|
+
depends_on: Array.isArray(json.depends_on) ? json.depends_on.map((item) => String(item).trim()).filter(Boolean) : [],
|
|
130
|
+
parallel_safe: typeof json.parallel_safe === 'string' ? json.parallel_safe : null,
|
|
131
|
+
parallel_safe_reason: typeof json.parallel_safe_reason === 'string' ? json.parallel_safe_reason : null,
|
|
132
|
+
status: typeof json.status === 'string' ? json.status : 'draft',
|
|
133
|
+
ticket: typeof json.ticket === 'string' ? json.ticket : '',
|
|
134
|
+
title: typeof json.title === 'string' ? json.title : sliceId,
|
|
135
|
+
json,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readSpecSlices(rootDir, rootName, specSlug) {
|
|
140
|
+
const slicesDir = path.join(rootDir, rootName, specSlug, 'slices');
|
|
141
|
+
if (!fs.existsSync(slicesDir)) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const slices = [];
|
|
146
|
+
|
|
147
|
+
for (const sliceEntry of fs.readdirSync(slicesDir, { withFileTypes: true })) {
|
|
148
|
+
if (!sliceEntry.isDirectory() || isPlaceholderSliceDir(sliceEntry.name)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const slice = readSliceFile(rootDir, rootName, specSlug, sliceEntry.name);
|
|
153
|
+
if (slice) {
|
|
154
|
+
slices.push(slice);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
slices.sort((left, right) => compareSliceRefs(left.ref, right.ref));
|
|
159
|
+
return slices;
|
|
160
|
+
}
|
|
161
|
+
|
|
98
162
|
function readAllSlices(rootDir) {
|
|
99
163
|
const roots = ['specs', 'specs-fix'];
|
|
100
164
|
const slices = [];
|
|
@@ -110,44 +174,7 @@ function readAllSlices(rootDir) {
|
|
|
110
174
|
continue;
|
|
111
175
|
}
|
|
112
176
|
|
|
113
|
-
|
|
114
|
-
const slicesDir = path.join(rootPath, specSlug, 'slices');
|
|
115
|
-
if (!fs.existsSync(slicesDir)) {
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
for (const sliceEntry of fs.readdirSync(slicesDir, { withFileTypes: true })) {
|
|
120
|
-
if (!sliceEntry.isDirectory() || isPlaceholderSliceDir(sliceEntry.name)) {
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const sliceDir = path.join(slicesDir, sliceEntry.name);
|
|
125
|
-
const slicePath = path.join(sliceDir, 'slice.json');
|
|
126
|
-
if (!fs.existsSync(slicePath)) {
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const json = parseJsonWithComments(fs.readFileSync(slicePath, 'utf8'));
|
|
131
|
-
const sliceId = String(json.slice_id || sliceEntry.name).trim();
|
|
132
|
-
const ref = `${specSlug}/${sliceId}`;
|
|
133
|
-
slices.push({
|
|
134
|
-
ref,
|
|
135
|
-
specFamily: rootName,
|
|
136
|
-
specSlug,
|
|
137
|
-
sliceId,
|
|
138
|
-
slicePath,
|
|
139
|
-
sliceDir,
|
|
140
|
-
files: sortFileList(json.files),
|
|
141
|
-
dependencies: Array.isArray(json.dependencies) ? json.dependencies.map((item) => String(item).trim()).filter(Boolean) : [],
|
|
142
|
-
depends_on: Array.isArray(json.depends_on) ? json.depends_on.map((item) => String(item).trim()).filter(Boolean) : [],
|
|
143
|
-
parallel_safe: typeof json.parallel_safe === 'string' ? json.parallel_safe : null,
|
|
144
|
-
parallel_safe_reason: typeof json.parallel_safe_reason === 'string' ? json.parallel_safe_reason : null,
|
|
145
|
-
status: typeof json.status === 'string' ? json.status : 'draft',
|
|
146
|
-
ticket: typeof json.ticket === 'string' ? json.ticket : '',
|
|
147
|
-
title: typeof json.title === 'string' ? json.title : sliceId,
|
|
148
|
-
json,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
177
|
+
slices.push(...readSpecSlices(rootDir, rootName, specEntry.name));
|
|
151
178
|
}
|
|
152
179
|
}
|
|
153
180
|
|
|
@@ -171,6 +198,75 @@ function declaredDependenciesForSlice(slice) {
|
|
|
171
198
|
return null;
|
|
172
199
|
}
|
|
173
200
|
|
|
201
|
+
function readSliceByRef(rootDir, ref) {
|
|
202
|
+
const value = String(ref || '').trim();
|
|
203
|
+
const slashIndex = value.indexOf('/');
|
|
204
|
+
if (slashIndex === -1) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const specSlug = value.slice(0, slashIndex);
|
|
209
|
+
const sliceId = value.slice(slashIndex + 1);
|
|
210
|
+
if (!specSlug || !sliceId) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const rootName of ['specs', 'specs-fix']) {
|
|
215
|
+
const direct = readSliceFile(rootDir, rootName, specSlug, sliceId);
|
|
216
|
+
if (direct) {
|
|
217
|
+
return direct;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const slice of readSpecSlices(rootDir, rootName, specSlug)) {
|
|
221
|
+
if (slice.ref === value) {
|
|
222
|
+
return slice;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function readSlicesForSpec(rootDir, specSlug) {
|
|
231
|
+
const targetSpec = String(specSlug || '').trim();
|
|
232
|
+
if (!targetSpec) {
|
|
233
|
+
return readAllSlices(rootDir);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const byRef = new Map();
|
|
237
|
+
const queue = [];
|
|
238
|
+
|
|
239
|
+
function addSlice(slice) {
|
|
240
|
+
if (!slice || byRef.has(slice.ref)) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
byRef.set(slice.ref, slice);
|
|
244
|
+
queue.push(slice);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const rootName of ['specs', 'specs-fix']) {
|
|
248
|
+
for (const slice of readSpecSlices(rootDir, rootName, targetSpec)) {
|
|
249
|
+
addSlice(slice);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
while (queue.length > 0) {
|
|
254
|
+
const slice = queue.shift();
|
|
255
|
+
for (const dep of declaredDependenciesForSlice(slice) || []) {
|
|
256
|
+
if (byRef.has(dep)) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const dependencySlice = readSliceByRef(rootDir, dep);
|
|
261
|
+
if (dependencySlice) {
|
|
262
|
+
addSlice(dependencySlice);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return Array.from(byRef.values()).sort((left, right) => compareSliceRefs(left.ref, right.ref));
|
|
268
|
+
}
|
|
269
|
+
|
|
174
270
|
function inferDependencies(slices) {
|
|
175
271
|
const bySpec = new Map();
|
|
176
272
|
const normalized = slices.map((slice) => ({
|
|
@@ -454,7 +550,11 @@ module.exports = {
|
|
|
454
550
|
detectFileConflicts,
|
|
455
551
|
inferDependencies,
|
|
456
552
|
isFoundationSliceId,
|
|
553
|
+
normalizeDeclaredDependencies,
|
|
554
|
+
normalizeDependencyRef,
|
|
457
555
|
readAllSlices,
|
|
556
|
+
readSlicesForSpec,
|
|
458
557
|
naturalNumberFromSliceId,
|
|
558
|
+
resolveWriteScope,
|
|
459
559
|
topoSort,
|
|
460
560
|
};
|
|
@@ -42,14 +42,18 @@ function readSliceMeta(slicePath) {
|
|
|
42
42
|
const branchName = typeof git.branch_name === 'string' ? git.branch_name.trim() : '';
|
|
43
43
|
const sliceId = typeof json.slice_id === 'string' ? json.slice_id.trim() : '';
|
|
44
44
|
const status = String(json.status || 'draft').trim() || 'draft';
|
|
45
|
+
const allowedWritePaths = Array.isArray(json.allowed_write_paths) ? json.allowed_write_paths.map((item) => String(item)) : [];
|
|
46
|
+
const legacyFiles = Array.isArray(json.files) ? json.files.map((item) => String(item)) : [];
|
|
45
47
|
|
|
46
48
|
return {
|
|
47
49
|
acceptance: Array.isArray(json.acceptance) ? json.acceptance : [],
|
|
50
|
+
allowedWritePaths,
|
|
48
51
|
baseBranch,
|
|
49
52
|
branchName,
|
|
50
53
|
branchSlug,
|
|
51
54
|
branchType,
|
|
52
|
-
|
|
55
|
+
expectedReadPaths: Array.isArray(json.expected_read_paths) ? json.expected_read_paths.map((item) => String(item)) : [],
|
|
56
|
+
files: allowedWritePaths.length > 0 ? allowedWritePaths : legacyFiles,
|
|
53
57
|
git,
|
|
54
58
|
isBaseline: sliceId.startsWith('slice-00'),
|
|
55
59
|
json,
|
|
@@ -60,6 +64,7 @@ function readSliceMeta(slicePath) {
|
|
|
60
64
|
status,
|
|
61
65
|
tests: Array.isArray(json.tests) ? json.tests : [],
|
|
62
66
|
ticket,
|
|
67
|
+
validationHints: Array.isArray(json.validation_hints) ? json.validation_hints.map((item) => String(item)) : [],
|
|
63
68
|
};
|
|
64
69
|
}
|
|
65
70
|
|
|
@@ -185,6 +185,7 @@ function startSpecWorktree(repoRoot, specInput, options = {}) {
|
|
|
185
185
|
return {
|
|
186
186
|
...identity,
|
|
187
187
|
baseRef,
|
|
188
|
+
dryRun: options.dryRun === true,
|
|
188
189
|
reused: true,
|
|
189
190
|
slice00,
|
|
190
191
|
worktreePath: existingWorktree,
|
|
@@ -199,6 +200,17 @@ function startSpecWorktree(repoRoot, specInput, options = {}) {
|
|
|
199
200
|
throw new Error(formatError('current checkout is not clean. Commit or stash before starting a spec worktree.'));
|
|
200
201
|
}
|
|
201
202
|
|
|
203
|
+
if (options.dryRun === true) {
|
|
204
|
+
return {
|
|
205
|
+
...identity,
|
|
206
|
+
baseRef,
|
|
207
|
+
currentBranch: currentBranch(repoRoot),
|
|
208
|
+
dryRun: true,
|
|
209
|
+
reused: false,
|
|
210
|
+
slice00,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
202
214
|
worktreePrune(repoRoot);
|
|
203
215
|
fs.mkdirSync(path.dirname(identity.worktreePath), { recursive: true });
|
|
204
216
|
|
|
@@ -212,6 +224,7 @@ function startSpecWorktree(repoRoot, specInput, options = {}) {
|
|
|
212
224
|
...identity,
|
|
213
225
|
baseRef,
|
|
214
226
|
currentBranch: currentBranch(repoRoot),
|
|
227
|
+
dryRun: false,
|
|
215
228
|
reused: false,
|
|
216
229
|
slice00,
|
|
217
230
|
};
|
|
@@ -219,13 +232,14 @@ function startSpecWorktree(repoRoot, specInput, options = {}) {
|
|
|
219
232
|
|
|
220
233
|
function formatSpecStartResult(result) {
|
|
221
234
|
return `${[
|
|
222
|
-
'Spec worktree ready',
|
|
235
|
+
result.dryRun ? 'Spec worktree start dry-run' : 'Spec worktree ready',
|
|
223
236
|
`Branch: ${result.branchName}`,
|
|
224
237
|
`Base: ${result.baseRef}`,
|
|
225
238
|
`Worktree: ${result.worktreePath}`,
|
|
226
239
|
`Reused: ${result.reused ? 'yes' : 'no'}`,
|
|
227
240
|
`slice-00: ${result.slice00 ? result.slice00.status : 'missing'}`,
|
|
228
|
-
|
|
241
|
+
result.dryRun && !result.reused ? `Would create worktree: ${result.worktreePath}` : '',
|
|
242
|
+
].filter(Boolean).join('\n')}\n`;
|
|
229
243
|
}
|
|
230
244
|
|
|
231
245
|
function closeSpecWorktree(repoRoot, specInput, options = {}) {
|