@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/cli/index.js CHANGED
@@ -65,7 +65,7 @@ var init_defaults = __esm({
65
65
  copilot: ".github/prompts",
66
66
  codex: ".codex/agents"
67
67
  };
68
- WAIRON_VERSION = "5.1.1-dev.4";
68
+ WAIRON_VERSION = "5.1.1-dev.6";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  ARCHITECT_AGENT_ID = "agent-architect";
71
71
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -16585,10 +16585,40 @@ var init_specs2 = __esm({
16585
16585
  return null;
16586
16586
  }
16587
16587
  }
16588
+ /**
16589
+ * Refuse a write that would land on a file already holding a DIFFERENT spec.
16590
+ *
16591
+ * In the nested layout a spec's path is derived from its parent — a component
16592
+ * for an interface, a contract for an implementation — and the spec's own id
16593
+ * is not part of it (see getInterfacePath / getImplementationPath, which take
16594
+ * the id but ignore it once the parent resolves). So a second id bound to the
16595
+ * same parent resolves to the SAME file.
16596
+ *
16597
+ * Left unguarded that is silent data loss, and doubly invisible: the caller's
16598
+ * `existing` lookup is by the NEW id, finds nothing, and every re-author
16599
+ * notice ("REMOVED …", the carry-forward seam) stays quiet. An agent renaming
16600
+ * an interface by defining a new one destroys the old contract, its
16601
+ * narratives and its lint.allows, and is told "Successfully defined".
16602
+ */
16603
+ assertPathHoldsNoOtherSpec(p, id, kind, parentLabel) {
16604
+ if (!pathExists(p)) return;
16605
+ let occupantId;
16606
+ try {
16607
+ occupantId = readYamlFile(p)?.id;
16608
+ } catch {
16609
+ return;
16610
+ }
16611
+ if (!occupantId) return;
16612
+ if (occupantId === id || splitNamespace(occupantId).localId === splitNamespace(id).localId) return;
16613
+ throw new Error(
16614
+ `Cannot write ${kind} "${id}": ${parentLabel} is already ${kind === "interface" ? "served by" : "implemented by"} "${occupantId}" at ${path17.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.`
16615
+ );
16616
+ }
16588
16617
  /** Returns non-fatal placement notices (see saveTypeSpec) — empty when there is nothing to clarify. */
16589
16618
  saveInterfaceSpec(spec, opts) {
16590
16619
  const notices = [];
16591
16620
  const p = this.getInterfacePath(spec.id, spec.component);
16621
+ this.assertPathHoldsNoOtherSpec(p, spec.id, "interface", `component "${spec.component}"`);
16592
16622
  ensureDir(path17.dirname(p));
16593
16623
  const specToWrite = this.prepareInterfaceForWrite(spec);
16594
16624
  const existing = this.loadInterfaceSpec(spec.id);
@@ -16652,6 +16682,7 @@ var init_specs2 = __esm({
16652
16682
  saveImplementationSpec(spec, opts) {
16653
16683
  const notices = [];
16654
16684
  const p = this.getImplementationPath(spec.id, spec.contract);
16685
+ this.assertPathHoldsNoOtherSpec(p, spec.id, "implementation", `contract "${spec.contract}"`);
16655
16686
  ensureDir(path17.dirname(p));
16656
16687
  const specToWrite = this.prepareImplementationForWrite(spec);
16657
16688
  const existing = this.loadImplementationSpec(spec.id);
@@ -16946,6 +16977,35 @@ var init_specs2 = __esm({
16946
16977
  }
16947
16978
  return refs;
16948
16979
  };
16980
+ const normalizeIdentity = (k) => String(k ?? "").toLowerCase().replace(/[_\-\s]/g, "");
16981
+ const assertDeltaMarkers = (item, label) => {
16982
+ if (!item || typeof item !== "object") return;
16983
+ if ("remove" in item && typeof item.remove !== "boolean") {
16984
+ throw new Error(
16985
+ `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.`
16986
+ );
16987
+ }
16988
+ if ("action" in item && item.action !== "add" && item.action !== "delete") {
16989
+ throw new Error(
16990
+ `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.`
16991
+ );
16992
+ }
16993
+ };
16994
+ const assertUnmatchedIsAnAddition = (deltaItem, key, existingKeys, label) => {
16995
+ if (deltaItem?.remove === true || deltaItem?.action === "delete") {
16996
+ throw new Error(
16997
+ `Refusing to delete ${label} "${String(key)}": nothing with that identity exists. ` + (existingKeys.length ? `Present: ${existingKeys.map(String).join(", ")}.` : "The collection is empty.")
16998
+ );
16999
+ }
17000
+ const near = existingKeys.find(
17001
+ (k) => String(k) !== String(key) && normalizeIdentity(k) === normalizeIdentity(key)
17002
+ );
17003
+ if (near !== void 0) {
17004
+ throw new Error(
17005
+ `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.`
17006
+ );
17007
+ }
17008
+ };
16949
17009
  const mergeNarrative = (existingSteps, deltaSteps) => {
16950
17010
  let steps = [...existingSteps];
16951
17011
  const sortedDeltas = [...deltaSteps].sort((a, b) => a.stepNumber - b.stepNumber);
@@ -17014,6 +17074,7 @@ var init_specs2 = __esm({
17014
17074
  const mergeMethods = (existingMethods, deltaMethods) => {
17015
17075
  const merged = [...existingMethods];
17016
17076
  for (const deltaMethod of deltaMethods) {
17077
+ assertDeltaMarkers(deltaMethod, `method "${deltaMethod?.name}"`);
17017
17078
  const idx = merged.findIndex((m) => m.name === deltaMethod.name);
17018
17079
  if (idx !== -1) {
17019
17080
  if (deltaMethod.remove === true || deltaMethod.action === "delete") {
@@ -17033,9 +17094,8 @@ var init_specs2 = __esm({
17033
17094
  };
17034
17095
  }
17035
17096
  } else {
17036
- if (deltaMethod.remove !== true && deltaMethod.action !== "delete") {
17037
- merged.push(deltaMethod);
17038
- }
17097
+ assertUnmatchedIsAnAddition(deltaMethod, deltaMethod.name, merged.map((m) => m.name), "method");
17098
+ merged.push(deltaMethod);
17039
17099
  }
17040
17100
  }
17041
17101
  return merged;
@@ -17043,6 +17103,7 @@ var init_specs2 = __esm({
17043
17103
  const mergeNamedArray = (existing, delta2) => {
17044
17104
  const merged = [...existing];
17045
17105
  for (const deltaItem of delta2) {
17106
+ assertDeltaMarkers(deltaItem, `entry "${deltaItem?.name}"`);
17046
17107
  const idx = merged.findIndex((item) => item.name === deltaItem.name);
17047
17108
  if (idx !== -1) {
17048
17109
  if (deltaItem.remove === true || deltaItem.action === "delete") {
@@ -17055,9 +17116,8 @@ var init_specs2 = __esm({
17055
17116
  };
17056
17117
  }
17057
17118
  } else {
17058
- if (deltaItem.remove !== true && deltaItem.action !== "delete") {
17059
- merged.push(deltaItem);
17060
- }
17119
+ assertUnmatchedIsAnAddition(deltaItem, deltaItem.name, merged.map((i) => i.name), "entry");
17120
+ merged.push(deltaItem);
17061
17121
  }
17062
17122
  }
17063
17123
  return merged;
@@ -17065,6 +17125,7 @@ var init_specs2 = __esm({
17065
17125
  const mergeKeyedArray = (existing, delta2, keyOf) => {
17066
17126
  const merged = [...existing];
17067
17127
  for (const deltaItem of delta2) {
17128
+ assertDeltaMarkers(deltaItem, `entry "${keyOf(deltaItem)}"`);
17068
17129
  const idx = merged.findIndex((item) => keyOf(item) === keyOf(deltaItem));
17069
17130
  if (idx !== -1) {
17070
17131
  if (deltaItem.remove === true || deltaItem.action === "delete") {
@@ -17072,7 +17133,8 @@ var init_specs2 = __esm({
17072
17133
  } else {
17073
17134
  merged[idx] = { ...merged[idx], ...deltaItem };
17074
17135
  }
17075
- } else if (deltaItem.remove !== true && deltaItem.action !== "delete") {
17136
+ } else {
17137
+ assertUnmatchedIsAnAddition(deltaItem, keyOf(deltaItem), merged.map(keyOf), "entry");
17076
17138
  merged.push(deltaItem);
17077
17139
  }
17078
17140
  }
@@ -17081,6 +17143,8 @@ var init_specs2 = __esm({
17081
17143
  const mergePublicInterfaces = (existing, delta2) => {
17082
17144
  const merged = [...existing];
17083
17145
  for (const deltaItem of delta2) {
17146
+ const piKey = (i) => `${i?.component}.${i?.interface}`;
17147
+ assertDeltaMarkers(deltaItem, `publicInterface "${piKey(deltaItem)}"`);
17084
17148
  const idx = merged.findIndex((item) => item.component === deltaItem.component && item.interface === deltaItem.interface);
17085
17149
  if (idx !== -1) {
17086
17150
  if (deltaItem.remove === true || deltaItem.action === "delete") {
@@ -17092,9 +17156,8 @@ var init_specs2 = __esm({
17092
17156
  };
17093
17157
  }
17094
17158
  } else {
17095
- if (deltaItem.remove !== true && deltaItem.action !== "delete") {
17096
- merged.push(deltaItem);
17097
- }
17159
+ assertUnmatchedIsAnAddition(deltaItem, piKey(deltaItem), merged.map(piKey), "publicInterface");
17160
+ merged.push(deltaItem);
17098
17161
  }
17099
17162
  }
17100
17163
  return merged;
@@ -17135,11 +17198,22 @@ var init_specs2 = __esm({
17135
17198
  for (const deltaItem of delta2) {
17136
17199
  const key = identityKeyOf(field, deltaItem);
17137
17200
  const idx = key === null ? -1 : merged.findIndex((item) => identityKeyOf(field, item) === key);
17201
+ assertDeltaMarkers(deltaItem, `${field} entry "${String(key)}"`);
17138
17202
  const isDelete = deltaItem?.remove === true || deltaItem?.action === "delete";
17139
17203
  if (idx !== -1) {
17140
17204
  if (isDelete) merged.splice(idx, 1);
17141
17205
  else merged[idx] = { ...merged[idx], ...deltaItem };
17142
- } else if (!isDelete) {
17206
+ } else {
17207
+ if (key !== null) {
17208
+ assertUnmatchedIsAnAddition(
17209
+ deltaItem,
17210
+ key,
17211
+ merged.map((i) => identityKeyOf(field, i)).filter((k) => k !== null),
17212
+ field
17213
+ );
17214
+ } else if (isDelete) {
17215
+ throw new Error(`Refusing to delete a ${field} entry with no resolvable identity.`);
17216
+ }
17143
17217
  merged.push(deltaItem);
17144
17218
  }
17145
17219
  }