@wairon/cli 5.1.1-dev.4 → 5.1.1-dev.6

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/index.js CHANGED
@@ -1211,7 +1211,7 @@ var init_defaults = __esm({
1211
1211
  copilot: ".github/prompts",
1212
1212
  codex: ".codex/agents"
1213
1213
  };
1214
- WAIRON_VERSION = "5.1.1-dev.4";
1214
+ WAIRON_VERSION = "5.1.1-dev.6";
1215
1215
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
1216
1216
  ARCHITECT_AGENT_ID = "agent-architect";
1217
1217
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -15996,10 +15996,40 @@ var init_specs2 = __esm({
15996
15996
  return null;
15997
15997
  }
15998
15998
  }
15999
+ /**
16000
+ * Refuse a write that would land on a file already holding a DIFFERENT spec.
16001
+ *
16002
+ * In the nested layout a spec's path is derived from its parent — a component
16003
+ * for an interface, a contract for an implementation — and the spec's own id
16004
+ * is not part of it (see getInterfacePath / getImplementationPath, which take
16005
+ * the id but ignore it once the parent resolves). So a second id bound to the
16006
+ * same parent resolves to the SAME file.
16007
+ *
16008
+ * Left unguarded that is silent data loss, and doubly invisible: the caller's
16009
+ * `existing` lookup is by the NEW id, finds nothing, and every re-author
16010
+ * notice ("REMOVED …", the carry-forward seam) stays quiet. An agent renaming
16011
+ * an interface by defining a new one destroys the old contract, its
16012
+ * narratives and its lint.allows, and is told "Successfully defined".
16013
+ */
16014
+ assertPathHoldsNoOtherSpec(p, id, kind, parentLabel) {
16015
+ if (!pathExists(p)) return;
16016
+ let occupantId;
16017
+ try {
16018
+ occupantId = readYamlFile(p)?.id;
16019
+ } catch {
16020
+ return;
16021
+ }
16022
+ if (!occupantId) return;
16023
+ if (occupantId === id || splitNamespace(occupantId).localId === splitNamespace(id).localId) return;
16024
+ throw new Error(
16025
+ `Cannot write ${kind} "${id}": ${parentLabel} is already ${kind === "interface" ? "served by" : "implemented by"} "${occupantId}" at ${path13.relative(this.rootDir, p)}, and both ids resolve to that one file. Re-author "${occupantId}" instead, or delete it first if you meant to replace it.`
16026
+ );
16027
+ }
15999
16028
  /** Returns non-fatal placement notices (see saveTypeSpec) — empty when there is nothing to clarify. */
16000
16029
  saveInterfaceSpec(spec, opts) {
16001
16030
  const notices = [];
16002
16031
  const p = this.getInterfacePath(spec.id, spec.component);
16032
+ this.assertPathHoldsNoOtherSpec(p, spec.id, "interface", `component "${spec.component}"`);
16003
16033
  ensureDir(path13.dirname(p));
16004
16034
  const specToWrite = this.prepareInterfaceForWrite(spec);
16005
16035
  const existing = this.loadInterfaceSpec(spec.id);
@@ -16063,6 +16093,7 @@ var init_specs2 = __esm({
16063
16093
  saveImplementationSpec(spec, opts) {
16064
16094
  const notices = [];
16065
16095
  const p = this.getImplementationPath(spec.id, spec.contract);
16096
+ this.assertPathHoldsNoOtherSpec(p, spec.id, "implementation", `contract "${spec.contract}"`);
16066
16097
  ensureDir(path13.dirname(p));
16067
16098
  const specToWrite = this.prepareImplementationForWrite(spec);
16068
16099
  const existing = this.loadImplementationSpec(spec.id);
@@ -16357,6 +16388,35 @@ var init_specs2 = __esm({
16357
16388
  }
16358
16389
  return refs;
16359
16390
  };
16391
+ const normalizeIdentity = (k) => String(k ?? "").toLowerCase().replace(/[_\-\s]/g, "");
16392
+ const assertDeltaMarkers = (item, label) => {
16393
+ if (!item || typeof item !== "object") return;
16394
+ if ("remove" in item && typeof item.remove !== "boolean") {
16395
+ throw new Error(
16396
+ `Refusing to update ${label}: "remove" must be the boolean true, got ${JSON.stringify(item.remove)}. A non-boolean is ignored, which would leave the element in place while reporting success.`
16397
+ );
16398
+ }
16399
+ if ("action" in item && item.action !== "add" && item.action !== "delete") {
16400
+ throw new Error(
16401
+ `Refusing to update ${label}: unknown action ${JSON.stringify(item.action)} \u2014 expected "add" or "delete". An unknown verb is ignored, which would leave the element in place while reporting success.`
16402
+ );
16403
+ }
16404
+ };
16405
+ const assertUnmatchedIsAnAddition = (deltaItem, key, existingKeys, label) => {
16406
+ if (deltaItem?.remove === true || deltaItem?.action === "delete") {
16407
+ throw new Error(
16408
+ `Refusing to delete ${label} "${String(key)}": nothing with that identity exists. ` + (existingKeys.length ? `Present: ${existingKeys.map(String).join(", ")}.` : "The collection is empty.")
16409
+ );
16410
+ }
16411
+ const near = existingKeys.find(
16412
+ (k) => String(k) !== String(key) && normalizeIdentity(k) === normalizeIdentity(key)
16413
+ );
16414
+ if (near !== void 0) {
16415
+ throw new Error(
16416
+ `Refusing to add ${label} "${String(key)}": "${String(near)}" already exists and differs only in case or separators. Use the existing identity to edit it, or pick a name that is not a near-duplicate.`
16417
+ );
16418
+ }
16419
+ };
16360
16420
  const mergeNarrative = (existingSteps, deltaSteps) => {
16361
16421
  let steps = [...existingSteps];
16362
16422
  const sortedDeltas = [...deltaSteps].sort((a, b) => a.stepNumber - b.stepNumber);
@@ -16425,6 +16485,7 @@ var init_specs2 = __esm({
16425
16485
  const mergeMethods = (existingMethods, deltaMethods) => {
16426
16486
  const merged = [...existingMethods];
16427
16487
  for (const deltaMethod of deltaMethods) {
16488
+ assertDeltaMarkers(deltaMethod, `method "${deltaMethod?.name}"`);
16428
16489
  const idx = merged.findIndex((m) => m.name === deltaMethod.name);
16429
16490
  if (idx !== -1) {
16430
16491
  if (deltaMethod.remove === true || deltaMethod.action === "delete") {
@@ -16444,9 +16505,8 @@ var init_specs2 = __esm({
16444
16505
  };
16445
16506
  }
16446
16507
  } else {
16447
- if (deltaMethod.remove !== true && deltaMethod.action !== "delete") {
16448
- merged.push(deltaMethod);
16449
- }
16508
+ assertUnmatchedIsAnAddition(deltaMethod, deltaMethod.name, merged.map((m) => m.name), "method");
16509
+ merged.push(deltaMethod);
16450
16510
  }
16451
16511
  }
16452
16512
  return merged;
@@ -16454,6 +16514,7 @@ var init_specs2 = __esm({
16454
16514
  const mergeNamedArray = (existing, delta2) => {
16455
16515
  const merged = [...existing];
16456
16516
  for (const deltaItem of delta2) {
16517
+ assertDeltaMarkers(deltaItem, `entry "${deltaItem?.name}"`);
16457
16518
  const idx = merged.findIndex((item) => item.name === deltaItem.name);
16458
16519
  if (idx !== -1) {
16459
16520
  if (deltaItem.remove === true || deltaItem.action === "delete") {
@@ -16466,9 +16527,8 @@ var init_specs2 = __esm({
16466
16527
  };
16467
16528
  }
16468
16529
  } else {
16469
- if (deltaItem.remove !== true && deltaItem.action !== "delete") {
16470
- merged.push(deltaItem);
16471
- }
16530
+ assertUnmatchedIsAnAddition(deltaItem, deltaItem.name, merged.map((i) => i.name), "entry");
16531
+ merged.push(deltaItem);
16472
16532
  }
16473
16533
  }
16474
16534
  return merged;
@@ -16476,6 +16536,7 @@ var init_specs2 = __esm({
16476
16536
  const mergeKeyedArray = (existing, delta2, keyOf) => {
16477
16537
  const merged = [...existing];
16478
16538
  for (const deltaItem of delta2) {
16539
+ assertDeltaMarkers(deltaItem, `entry "${keyOf(deltaItem)}"`);
16479
16540
  const idx = merged.findIndex((item) => keyOf(item) === keyOf(deltaItem));
16480
16541
  if (idx !== -1) {
16481
16542
  if (deltaItem.remove === true || deltaItem.action === "delete") {
@@ -16483,7 +16544,8 @@ var init_specs2 = __esm({
16483
16544
  } else {
16484
16545
  merged[idx] = { ...merged[idx], ...deltaItem };
16485
16546
  }
16486
- } else if (deltaItem.remove !== true && deltaItem.action !== "delete") {
16547
+ } else {
16548
+ assertUnmatchedIsAnAddition(deltaItem, keyOf(deltaItem), merged.map(keyOf), "entry");
16487
16549
  merged.push(deltaItem);
16488
16550
  }
16489
16551
  }
@@ -16492,6 +16554,8 @@ var init_specs2 = __esm({
16492
16554
  const mergePublicInterfaces = (existing, delta2) => {
16493
16555
  const merged = [...existing];
16494
16556
  for (const deltaItem of delta2) {
16557
+ const piKey = (i) => `${i?.component}.${i?.interface}`;
16558
+ assertDeltaMarkers(deltaItem, `publicInterface "${piKey(deltaItem)}"`);
16495
16559
  const idx = merged.findIndex((item) => item.component === deltaItem.component && item.interface === deltaItem.interface);
16496
16560
  if (idx !== -1) {
16497
16561
  if (deltaItem.remove === true || deltaItem.action === "delete") {
@@ -16503,9 +16567,8 @@ var init_specs2 = __esm({
16503
16567
  };
16504
16568
  }
16505
16569
  } else {
16506
- if (deltaItem.remove !== true && deltaItem.action !== "delete") {
16507
- merged.push(deltaItem);
16508
- }
16570
+ assertUnmatchedIsAnAddition(deltaItem, piKey(deltaItem), merged.map(piKey), "publicInterface");
16571
+ merged.push(deltaItem);
16509
16572
  }
16510
16573
  }
16511
16574
  return merged;
@@ -16546,11 +16609,22 @@ var init_specs2 = __esm({
16546
16609
  for (const deltaItem of delta2) {
16547
16610
  const key = identityKeyOf(field, deltaItem);
16548
16611
  const idx = key === null ? -1 : merged.findIndex((item) => identityKeyOf(field, item) === key);
16612
+ assertDeltaMarkers(deltaItem, `${field} entry "${String(key)}"`);
16549
16613
  const isDelete = deltaItem?.remove === true || deltaItem?.action === "delete";
16550
16614
  if (idx !== -1) {
16551
16615
  if (isDelete) merged.splice(idx, 1);
16552
16616
  else merged[idx] = { ...merged[idx], ...deltaItem };
16553
- } else if (!isDelete) {
16617
+ } else {
16618
+ if (key !== null) {
16619
+ assertUnmatchedIsAnAddition(
16620
+ deltaItem,
16621
+ key,
16622
+ merged.map((i) => identityKeyOf(field, i)).filter((k) => k !== null),
16623
+ field
16624
+ );
16625
+ } else if (isDelete) {
16626
+ throw new Error(`Refusing to delete a ${field} entry with no resolvable identity.`);
16627
+ }
16554
16628
  merged.push(deltaItem);
16555
16629
  }
16556
16630
  }