airgen-cli 0.23.0 → 0.23.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/dist/commands/baselines.js +73 -8
- package/package.json +1 -1
|
@@ -117,14 +117,16 @@ export function registerBaselineCommands(program, client) {
|
|
|
117
117
|
});
|
|
118
118
|
cmd
|
|
119
119
|
.command("restore")
|
|
120
|
-
.description("Restore requirements from a baseline (PATCHes live requirements with snapshot values)")
|
|
120
|
+
.description("Restore requirements from a baseline (PATCHes live requirements with snapshot values). Joins on immutable internal id, so survives reqs reassign.")
|
|
121
121
|
.argument("<tenant>", "Tenant slug")
|
|
122
122
|
.argument("<project>", "Project slug")
|
|
123
123
|
.argument("<baseline-ref>", "Baseline ref")
|
|
124
|
-
.option("--ref <pattern>", "Restore only requirements whose ref includes this substring")
|
|
124
|
+
.option("--ref <pattern>", "Restore only requirements whose original ref includes this substring")
|
|
125
125
|
.option("--fields <list>", "Comma-separated fields to restore (default: rationale,text,pattern,verification,complianceStatus,complianceRationale,tags)", "rationale,text,pattern,verification,complianceStatus,complianceRationale,tags")
|
|
126
126
|
.option("--dry-run", "Print what would be restored without making changes")
|
|
127
127
|
.option("--yes", "Skip confirmation prompt")
|
|
128
|
+
.option("--match-by-text", "Fall back to text-content matching for requirements not found by id (e.g. recreated entities)")
|
|
129
|
+
.option("--strict", "Exit non-zero if any requirement fails to restore")
|
|
128
130
|
.action(async (tenant, project, baselineRef, opts) => {
|
|
129
131
|
const snap = await client.get(`/baselines/${tenant}/${project}/${encodeURIComponent(baselineRef)}`);
|
|
130
132
|
let reqs = snap.requirementVersions ?? [];
|
|
@@ -145,10 +147,34 @@ export function registerBaselineCommands(program, client) {
|
|
|
145
147
|
console.error(`Created: ${snap.baseline.createdAt ?? ""}`);
|
|
146
148
|
console.error(`Requirements to restore: ${reqs.length}`);
|
|
147
149
|
console.error(`Fields: ${fields.join(", ")}`);
|
|
150
|
+
console.error(`Join key: immutable id (survives reqs reassign)${opts.matchByText ? " + text-match fallback" : ""}`);
|
|
148
151
|
console.error("");
|
|
152
|
+
// Optionally fetch current requirements for text-match fallback
|
|
153
|
+
let textIndex = null;
|
|
154
|
+
if (opts.matchByText) {
|
|
155
|
+
console.error("Fetching current project requirements for text-match fallback...");
|
|
156
|
+
const allCurrent = [];
|
|
157
|
+
let page = 1;
|
|
158
|
+
const limit = 200;
|
|
159
|
+
while (true) {
|
|
160
|
+
const pageData = await client.get(`/requirements/${tenant}/${project}`, { page: String(page), limit: String(limit) });
|
|
161
|
+
const items = pageData.data ?? [];
|
|
162
|
+
allCurrent.push(...items);
|
|
163
|
+
if (page >= (pageData.meta?.totalPages ?? 1))
|
|
164
|
+
break;
|
|
165
|
+
page++;
|
|
166
|
+
}
|
|
167
|
+
textIndex = new Map();
|
|
168
|
+
for (const c of allCurrent) {
|
|
169
|
+
if (c.text && c.ref) {
|
|
170
|
+
textIndex.set(c.text.trim(), c.ref);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
console.error(`Indexed ${textIndex.size} current requirements by text.\n`);
|
|
174
|
+
}
|
|
149
175
|
if (opts.dryRun) {
|
|
150
176
|
for (const r of reqs) {
|
|
151
|
-
const
|
|
177
|
+
const origRef = refFromRequirementId(r.requirementId);
|
|
152
178
|
const summary = fields.map(f => {
|
|
153
179
|
const v = r[f];
|
|
154
180
|
if (v == null)
|
|
@@ -156,7 +182,7 @@ export function registerBaselineCommands(program, client) {
|
|
|
156
182
|
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
157
183
|
return `${f}=${s.length > 40 ? s.slice(0, 40) + "…" : s}`;
|
|
158
184
|
}).join(" | ");
|
|
159
|
-
console.log(`[dry-run] ${
|
|
185
|
+
console.log(`[dry-run] ${origRef}: ${summary}`);
|
|
160
186
|
}
|
|
161
187
|
console.error(`\n[dry-run] Would restore ${reqs.length} requirements. Re-run without --dry-run to apply.`);
|
|
162
188
|
return;
|
|
@@ -168,25 +194,64 @@ export function registerBaselineCommands(program, client) {
|
|
|
168
194
|
}
|
|
169
195
|
let ok = 0;
|
|
170
196
|
let failed = 0;
|
|
197
|
+
let skipped = 0;
|
|
198
|
+
let textMatched = 0;
|
|
199
|
+
const failures = [];
|
|
171
200
|
for (const r of reqs) {
|
|
172
|
-
const
|
|
201
|
+
const origRef = refFromRequirementId(r.requirementId);
|
|
173
202
|
const body = {};
|
|
174
203
|
for (const f of fields) {
|
|
175
204
|
const v = r[f];
|
|
176
205
|
if (v !== undefined)
|
|
177
206
|
body[f] = v;
|
|
178
207
|
}
|
|
208
|
+
// Skip requirements where the baseline has no values for any of the requested fields.
|
|
209
|
+
// (e.g. restoring --fields tags on a requirement that had no tags at baseline time)
|
|
210
|
+
if (Object.keys(body).length === 0) {
|
|
211
|
+
skipped++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
// Primary: PATCH by immutable composite id (the baseline's requirementId).
|
|
215
|
+
// Backend resolver matches r.id, which is set on creation and never mutates.
|
|
179
216
|
try {
|
|
180
|
-
await client.patch(`/requirements/${tenant}/${project}/${encodeURIComponent(
|
|
217
|
+
await client.patch(`/requirements/${tenant}/${project}/${encodeURIComponent(r.requirementId)}`, body);
|
|
181
218
|
ok++;
|
|
182
219
|
if (ok % 25 === 0)
|
|
183
220
|
console.error(` restored ${ok}/${reqs.length}…`);
|
|
221
|
+
continue;
|
|
184
222
|
}
|
|
185
223
|
catch (err) {
|
|
224
|
+
// Fall through to text-match fallback if enabled
|
|
225
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
226
|
+
if (textIndex && r.text) {
|
|
227
|
+
const liveRef = textIndex.get(r.text.trim());
|
|
228
|
+
if (liveRef) {
|
|
229
|
+
try {
|
|
230
|
+
await client.patch(`/requirements/${tenant}/${project}/${encodeURIComponent(liveRef)}`, body);
|
|
231
|
+
ok++;
|
|
232
|
+
textMatched++;
|
|
233
|
+
console.error(` text-matched ${origRef} -> ${liveRef}`);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
catch (err2) {
|
|
237
|
+
failed++;
|
|
238
|
+
failures.push({ ref: origRef, reason: `text-match to ${liveRef}: ${err2 instanceof Error ? err2.message : String(err2)}` });
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
186
243
|
failed++;
|
|
187
|
-
|
|
244
|
+
failures.push({ ref: origRef, reason: errMsg });
|
|
245
|
+
console.error(` FAILED ${origRef}: ${errMsg}`);
|
|
188
246
|
}
|
|
189
247
|
}
|
|
190
|
-
console.error(`\nDone. Restored ${ok}, failed ${failed}.`);
|
|
248
|
+
console.error(`\nDone. Restored ${ok}${textMatched > 0 ? ` (${textMatched} via text-match)` : ""}, skipped ${skipped} (no baseline values for requested fields), failed ${failed}.`);
|
|
249
|
+
if (failed > 0 && !opts.matchByText) {
|
|
250
|
+
console.error(`\nHint: re-run with --match-by-text to attempt content-based recovery for the ${failed} failures.`);
|
|
251
|
+
}
|
|
252
|
+
if (failed > 0 && opts.strict) {
|
|
253
|
+
console.error("\n--strict: exiting non-zero due to failures.");
|
|
254
|
+
process.exit(2);
|
|
255
|
+
}
|
|
191
256
|
});
|
|
192
257
|
}
|