reffy-cli 1.1.1 → 1.1.2
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/dist/plan-archive.js +90 -21
- package/package.json +1 -1
package/dist/plan-archive.js
CHANGED
|
@@ -33,20 +33,27 @@ async function listFilesRecursive(dir) {
|
|
|
33
33
|
}
|
|
34
34
|
return files.sort();
|
|
35
35
|
}
|
|
36
|
-
function
|
|
36
|
+
function parseArchiveableRequirements(content, relPath) {
|
|
37
37
|
const lines = content.split(/\r?\n/);
|
|
38
38
|
const sectionTypes = new Set();
|
|
39
|
-
const
|
|
40
|
-
|
|
39
|
+
const added = [];
|
|
40
|
+
const modified = [];
|
|
41
|
+
let currentSectionType = null;
|
|
41
42
|
let currentTitle = null;
|
|
42
43
|
let currentLines = [];
|
|
43
44
|
const flush = () => {
|
|
44
45
|
if (!currentTitle)
|
|
45
46
|
return;
|
|
46
|
-
|
|
47
|
+
const block = {
|
|
47
48
|
title: currentTitle,
|
|
48
49
|
content: currentLines.join("\n").trimEnd(),
|
|
49
|
-
}
|
|
50
|
+
};
|
|
51
|
+
if (currentSectionType === "ADDED") {
|
|
52
|
+
added.push(block);
|
|
53
|
+
}
|
|
54
|
+
else if (currentSectionType === "MODIFIED") {
|
|
55
|
+
modified.push(block);
|
|
56
|
+
}
|
|
50
57
|
currentTitle = null;
|
|
51
58
|
currentLines = [];
|
|
52
59
|
};
|
|
@@ -56,7 +63,7 @@ function parseAddedRequirements(content, relPath) {
|
|
|
56
63
|
flush();
|
|
57
64
|
const sectionType = sectionMatch[1] ?? "";
|
|
58
65
|
sectionTypes.add(sectionType);
|
|
59
|
-
|
|
66
|
+
currentSectionType = sectionType === "ADDED" || sectionType === "MODIFIED" ? sectionType : null;
|
|
60
67
|
continue;
|
|
61
68
|
}
|
|
62
69
|
const requirementMatch = line.match(REQUIREMENT_PATTERN);
|
|
@@ -71,17 +78,17 @@ function parseAddedRequirements(content, relPath) {
|
|
|
71
78
|
}
|
|
72
79
|
}
|
|
73
80
|
flush();
|
|
74
|
-
const unsupported = Array.from(sectionTypes).filter((sectionType) => sectionType !== "ADDED");
|
|
81
|
+
const unsupported = Array.from(sectionTypes).filter((sectionType) => sectionType !== "ADDED" && sectionType !== "MODIFIED");
|
|
75
82
|
if (unsupported.length > 0) {
|
|
76
83
|
throw new Error(`${relPath}: unsupported delta sections for archive: ${unsupported.join(", ")}`);
|
|
77
84
|
}
|
|
78
|
-
if (
|
|
85
|
+
if (sectionTypes.size === 0) {
|
|
79
86
|
throw new Error(`${relPath}: no supported archiveable requirements found`);
|
|
80
87
|
}
|
|
81
|
-
if (
|
|
82
|
-
throw new Error(`${relPath}: no
|
|
88
|
+
if (added.length === 0 && modified.length === 0) {
|
|
89
|
+
throw new Error(`${relPath}: no archiveable requirements found`);
|
|
83
90
|
}
|
|
84
|
-
return
|
|
91
|
+
return { added, modified };
|
|
85
92
|
}
|
|
86
93
|
function buildNewCurrentSpec(capability, changeId, blocks) {
|
|
87
94
|
return [
|
|
@@ -95,18 +102,74 @@ function buildNewCurrentSpec(capability, changeId, blocks) {
|
|
|
95
102
|
"",
|
|
96
103
|
].join("\n");
|
|
97
104
|
}
|
|
98
|
-
function
|
|
105
|
+
function parseCurrentSpecRequirementBlocks(existing, relPath) {
|
|
99
106
|
if (!existing.includes(REQUIREMENTS_HEADING)) {
|
|
100
107
|
throw new Error(`${relPath}: current spec is missing a "## Requirements" section`);
|
|
101
108
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
const lines = existing.split(/\r?\n/);
|
|
110
|
+
const blocks = [];
|
|
111
|
+
let inRequirements = false;
|
|
112
|
+
let currentTitle = null;
|
|
113
|
+
let currentLines = [];
|
|
114
|
+
const flush = () => {
|
|
115
|
+
if (!currentTitle)
|
|
116
|
+
return;
|
|
117
|
+
blocks.push({
|
|
118
|
+
title: currentTitle,
|
|
119
|
+
content: currentLines.join("\n").trimEnd(),
|
|
120
|
+
});
|
|
121
|
+
currentTitle = null;
|
|
122
|
+
currentLines = [];
|
|
123
|
+
};
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
if (line.trim() === REQUIREMENTS_HEADING) {
|
|
126
|
+
inRequirements = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!inRequirements) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (/^##\s+/.test(line) && line.trim() !== REQUIREMENTS_HEADING) {
|
|
133
|
+
flush();
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
const requirementMatch = line.match(REQUIREMENT_PATTERN);
|
|
137
|
+
if (requirementMatch) {
|
|
138
|
+
flush();
|
|
139
|
+
currentTitle = requirementMatch[1]?.trim() ?? "unknown";
|
|
140
|
+
currentLines = [line];
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (currentTitle) {
|
|
144
|
+
currentLines.push(line);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
flush();
|
|
148
|
+
return blocks;
|
|
149
|
+
}
|
|
150
|
+
function mergeRequirementsIntoCurrentSpec(existing, delta, relPath) {
|
|
151
|
+
const currentBlocks = parseCurrentSpecRequirementBlocks(existing, relPath);
|
|
152
|
+
const currentTitles = new Set(currentBlocks.map((block) => block.title));
|
|
153
|
+
for (const block of delta.added) {
|
|
154
|
+
if (currentTitles.has(block.title)) {
|
|
106
155
|
throw new Error(`${relPath}: current spec already contains requirement "${block.title}"`);
|
|
107
156
|
}
|
|
157
|
+
currentBlocks.push(block);
|
|
158
|
+
currentTitles.add(block.title);
|
|
159
|
+
}
|
|
160
|
+
for (const block of delta.modified) {
|
|
161
|
+
const index = currentBlocks.findIndex((currentBlock) => currentBlock.title === block.title);
|
|
162
|
+
if (index < 0) {
|
|
163
|
+
throw new Error(`${relPath}: current spec is missing requirement "${block.title}" required for MODIFIED archive`);
|
|
164
|
+
}
|
|
165
|
+
currentBlocks[index] = block;
|
|
166
|
+
}
|
|
167
|
+
const requirementsHeadingIndex = existing.indexOf(REQUIREMENTS_HEADING);
|
|
168
|
+
if (requirementsHeadingIndex < 0) {
|
|
169
|
+
throw new Error(`${relPath}: current spec is missing a "## Requirements" section`);
|
|
108
170
|
}
|
|
109
|
-
|
|
171
|
+
const prefix = existing.slice(0, requirementsHeadingIndex + REQUIREMENTS_HEADING.length);
|
|
172
|
+
return `${prefix}\n${currentBlocks.map((block) => `\n${block.content}`).join("")}\n`;
|
|
110
173
|
}
|
|
111
174
|
async function buildSpecUpdates(repoRoot, changeId, changeDir) {
|
|
112
175
|
const specsRoot = path.join(changeDir, "specs");
|
|
@@ -121,11 +184,17 @@ async function buildSpecUpdates(repoRoot, changeId, changeDir) {
|
|
|
121
184
|
continue;
|
|
122
185
|
const deltaContent = await fs.readFile(deltaPath, "utf8");
|
|
123
186
|
const relDeltaPath = path.relative(repoRoot, deltaPath).split(path.sep).join("/");
|
|
124
|
-
const
|
|
187
|
+
const delta = parseArchiveableRequirements(deltaContent, relDeltaPath);
|
|
125
188
|
const currentSpecPath = path.join(repoRoot, DEFAULT_PLANNING_DIRNAME, "specs", capability, "spec.md");
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
189
|
+
const currentSpecExists = await pathExists(currentSpecPath);
|
|
190
|
+
const nextContent = currentSpecExists
|
|
191
|
+
? mergeRequirementsIntoCurrentSpec(await fs.readFile(currentSpecPath, "utf8"), delta, path.relative(repoRoot, currentSpecPath).split(path.sep).join("/"))
|
|
192
|
+
: (() => {
|
|
193
|
+
if (delta.modified.length > 0) {
|
|
194
|
+
throw new Error(`${relDeltaPath}: cannot archive MODIFIED requirements without an existing current spec`);
|
|
195
|
+
}
|
|
196
|
+
return buildNewCurrentSpec(capability, changeId, delta.added);
|
|
197
|
+
})();
|
|
129
198
|
updates.set(currentSpecPath, nextContent);
|
|
130
199
|
}
|
|
131
200
|
return updates;
|