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.
@@ -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 ref = refFromRequirementId(r.requirementId);
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] ${ref}: ${summary}`);
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 ref = refFromRequirementId(r.requirementId);
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(ref)}`, body);
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
- console.error(` FAILED ${ref}: ${err instanceof Error ? err.message : String(err)}`);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airgen-cli",
3
- "version": "0.23.0",
3
+ "version": "0.23.1",
4
4
  "description": "AIRGen CLI — requirements engineering from the command line",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",