@wairon/cli 5.1.1-dev.49 → 5.1.1-dev.50

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.49";
68
+ WAIRON_VERSION = "5.1.1-dev.50";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  SUPPORTED_ALIASES = ["wai"];
71
71
  SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
@@ -934,6 +934,17 @@ var init_project = __esm({
934
934
  * carried as debt.
935
935
  */
936
936
  exclude: import_zod4.z.array(import_zod4.z.string()).optional(),
937
+ /**
938
+ * Project-relative directories holding this project's TESTS. Declaring none
939
+ * means no test scan and no `testsToRevisit` on any write — opt-in on the
940
+ * same terms as `sourceRoots`, so a project that never asked for it never
941
+ * pays for it.
942
+ *
943
+ * Separate from `sourceRoots` deliberately: tests are not code the specs are
944
+ * expected to claim, and putting them there would make every test file an
945
+ * UNCLAIMED_SOURCE_FILE.
946
+ */
947
+ testRoots: import_zod4.z.array(import_zod4.z.string()).optional(),
937
948
  /**
938
949
  * The files under the source roots that no spec names yet, frozen. A listed
939
950
  * file is not reported; an unlisted one is UNCLAIMED_SOURCE_FILE; an entry
@@ -15821,6 +15832,17 @@ function stripComments(text2, patterns) {
15821
15832
  function escapeRe(s) {
15822
15833
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15823
15834
  }
15835
+ function boundNames(captured, localOnly) {
15836
+ const names = [];
15837
+ for (const piece of captured.split(",")) {
15838
+ const sides = piece.split(" as ");
15839
+ for (const side of localOnly ? sides.slice(-1) : sides) {
15840
+ const name = side.trim();
15841
+ if (/^[A-Za-z_$][\w$]*$/.test(name)) names.push(name);
15842
+ }
15843
+ }
15844
+ return names;
15845
+ }
15824
15846
  function collectStrings(text2) {
15825
15847
  const anchors = /* @__PURE__ */ new Set();
15826
15848
  for (const m of text2.matchAll(STRING_RE)) {
@@ -15838,10 +15860,7 @@ function analyzeWithPatterns(text2, patterns) {
15838
15860
  for (const m of stripped.matchAll(re)) {
15839
15861
  const captured = m[1];
15840
15862
  if (!captured) continue;
15841
- for (const piece of captured.split(",")) {
15842
- const name = (piece.includes(" as ") ? piece.split(" as ")[1] : piece).trim();
15843
- if (name && /^[A-Za-z_$][\w$]*$/.test(name)) declared.add(name);
15844
- }
15863
+ for (const name of boundNames(captured, true)) declared.add(name);
15845
15864
  }
15846
15865
  }
15847
15866
  for (const re of patterns.imports) {
@@ -15856,6 +15875,7 @@ function analyzeWithPatterns(text2, patterns) {
15856
15875
  for (const re of patterns.declarations) {
15857
15876
  re.lastIndex = 0;
15858
15877
  const m = re.exec(line2);
15878
+ re.lastIndex = 0;
15859
15879
  if (m?.[1]) exported.add(m[1]);
15860
15880
  }
15861
15881
  }
@@ -16173,7 +16193,7 @@ function looksBinary(buffer) {
16173
16193
  const probe2 = buffer.subarray(0, Math.min(buffer.length, 4096));
16174
16194
  return probe2.includes(0);
16175
16195
  }
16176
- function walkSourceRoots(sourceRoots, exclude, projectRoot2) {
16196
+ function walkDeclaredRoots(roots, exclude, projectRoot2) {
16177
16197
  const found = [];
16178
16198
  const seen = /* @__PURE__ */ new Set();
16179
16199
  const excluded = exclude.map(pathKey);
@@ -16203,7 +16223,7 @@ function walkSourceRoots(sourceRoots, exclude, projectRoot2) {
16203
16223
  }
16204
16224
  }
16205
16225
  };
16206
- for (const root of sourceRoots) {
16226
+ for (const root of roots) {
16207
16227
  const key = pathKey(root);
16208
16228
  if (path13.isAbsolute(key) || path13.normalize(key).split(path13.sep)[0] === "..") continue;
16209
16229
  const absolute = path13.resolve(projectRoot2, key);
@@ -16224,7 +16244,7 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
16224
16244
  const files = [];
16225
16245
  const seen = /* @__PURE__ */ new Set();
16226
16246
  const exactCache = /* @__PURE__ */ new Map();
16227
- const rootFiles = walkSourceRoots(sourceRoots, exclude, projectRoot2);
16247
+ const rootFiles = walkDeclaredRoots(sourceRoots, exclude, projectRoot2);
16228
16248
  const declaredPaths = [];
16229
16249
  for (const impl of implementations) {
16230
16250
  declaredPaths.push(...implementationSourceFiles(impl));
@@ -16302,7 +16322,49 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
16302
16322
  }
16303
16323
  return { files, projectRoot: projectRoot2, rootFiles };
16304
16324
  }
16305
- var fs10, path13, import_module2, EXTENSION_LANGUAGE, C_FAMILY_COMMENTS, JS_PATTERNS, LANGUAGE_PATTERNS, PATTERN_ANALYSIS_MAX_BYTES, IDENTIFIER_RE, STRING_RE, tsResolutionCache, TS_RESOLUTION_RETRY_MS;
16325
+ function readTestFiles(testRoots, projectRoot2) {
16326
+ const scanned = [];
16327
+ for (const key of walkDeclaredRoots(testRoots, [], projectRoot2)) {
16328
+ let text2;
16329
+ try {
16330
+ const buffer = fs10.readFileSync(path13.resolve(projectRoot2, key));
16331
+ if (looksBinary(buffer)) continue;
16332
+ text2 = buffer.toString("utf8");
16333
+ } catch {
16334
+ continue;
16335
+ }
16336
+ const imports = /* @__PURE__ */ new Set();
16337
+ for (const m of text2.matchAll(NAMED_IMPORT_BINDINGS_RE)) {
16338
+ for (const name of boundNames(m[1] ?? "", false)) imports.add(name);
16339
+ }
16340
+ const words2 = /* @__PURE__ */ new Set();
16341
+ for (const m of text2.matchAll(IDENTIFIER_RE)) words2.add(m[0]);
16342
+ scanned.push({ path: key, imports, words: words2 });
16343
+ }
16344
+ return scanned;
16345
+ }
16346
+ function findTestsReferencing(methods, projectRoot2, testRoots = []) {
16347
+ const scanned = readTestFiles(testRoots, projectRoot2);
16348
+ if (scanned.length === 0) return [];
16349
+ const indiscriminateAbove = scanned.length / 10;
16350
+ const found = [];
16351
+ for (const method2 of methods) {
16352
+ const symbol = method2.symbol ?? method2.name;
16353
+ const imported = scanned.filter((f) => f.imports.has(symbol)).map((f) => f.path);
16354
+ const mentioned = scanned.filter((f) => !f.imports.has(symbol) && f.words.has(symbol)).map((f) => f.path);
16355
+ const indiscriminate = mentioned.length > indiscriminateAbove;
16356
+ if (imported.length === 0 && mentioned.length === 0) continue;
16357
+ found.push({
16358
+ method: method2.name,
16359
+ symbol,
16360
+ imported,
16361
+ mentioned: indiscriminate ? [] : mentioned,
16362
+ indiscriminate
16363
+ });
16364
+ }
16365
+ return found;
16366
+ }
16367
+ var fs10, path13, import_module2, EXTENSION_LANGUAGE, C_FAMILY_COMMENTS, NAMED_IMPORT_BINDINGS_RE, JS_PATTERNS, LANGUAGE_PATTERNS, PATTERN_ANALYSIS_MAX_BYTES, IDENTIFIER_RE, STRING_RE, tsResolutionCache, TS_RESOLUTION_RETRY_MS;
16306
16368
  var init_source_analysis = __esm({
16307
16369
  "src/core/source-analysis.ts"() {
16308
16370
  "use strict";
@@ -16335,6 +16397,7 @@ var init_source_analysis = __esm({
16335
16397
  ".swift": "swift"
16336
16398
  };
16337
16399
  C_FAMILY_COMMENTS = { lineComments: ["//"], blockComments: [["/*", "*/"]] };
16400
+ NAMED_IMPORT_BINDINGS_RE = /\bimport\s*\{([^}]*)\}/g;
16338
16401
  JS_PATTERNS = {
16339
16402
  ...C_FAMILY_COMMENTS,
16340
16403
  declarations: [
@@ -16344,7 +16407,7 @@ var init_source_analysis = __esm({
16344
16407
  /([A-Za-z_$][\w$]*)\s*[:=]\s*(?:async\s+)?(?:function\b|\()/g,
16345
16408
  /(?:^|\s)(?:public|private|protected|static|async|get|set)\s+([A-Za-z_$][\w$]*)\s*\(/g,
16346
16409
  // named import bindings realize forwarding adapters
16347
- /\bimport\s*\{([^}]*)\}/g
16410
+ NAMED_IMPORT_BINDINGS_RE
16348
16411
  ],
16349
16412
  imports: [
16350
16413
  /\b(?:import|export)\b[^'"\n]*['"]([^'"]+)['"]/g,
@@ -23097,6 +23160,7 @@ var init_gate_identity = __esm({
23097
23160
  var validation_exports = {};
23098
23161
  __export(validation_exports, {
23099
23162
  computeGateStateId: () => computeGateStateId,
23163
+ findTestsReferencing: () => findTestsReferencing2,
23100
23164
  listRules: () => listRules2,
23101
23165
  loadProjectConfig: () => loadProjectConfig3,
23102
23166
  validateAsComplete: () => validateAsComplete,
@@ -23121,6 +23185,9 @@ function projectPackSelections() {
23121
23185
  return [];
23122
23186
  }
23123
23187
  }
23188
+ function findTestsReferencing2(methods, projectRoot2, testRoots = []) {
23189
+ return findTestsReferencing(methods, projectRoot2, testRoots);
23190
+ }
23124
23191
  function issueKey(issue2) {
23125
23192
  return `${issue2.code}|${issue2.specId ?? ""}`;
23126
23193
  }
@@ -24887,6 +24954,7 @@ function moveChangeReports(edits, dryRun) {
24887
24954
  changes,
24888
24955
  ineffective: [],
24889
24956
  notices: [],
24957
+ testsToRevisit: [],
24890
24958
  summary: `${dryRun ? "Would move" : "Moved"} into ${edit.kind} "${edit.id}": ${changes.length} change${changes.length === 1 ? "" : "s"}.`
24891
24959
  }));
24892
24960
  }
@@ -26951,6 +27019,7 @@ var init_specs2 = __esm({
26951
27019
  changes,
26952
27020
  ineffective,
26953
27021
  notices,
27022
+ testsToRevisit: [],
26954
27023
  summary: `No change to ${kind} "${id}" \u2014 the delta matches what is stored, so nothing ${dryRun ? "would be" : "was"} written.`
26955
27024
  };
26956
27025
  }
@@ -26965,6 +27034,7 @@ var init_specs2 = __esm({
26965
27034
  changes,
26966
27035
  ineffective,
26967
27036
  notices,
27037
+ testsToRevisit: [],
26968
27038
  summary: `Dry run on ${kind} "${id}": ${changes.length} change${changes.length === 1 ? "" : "s"} would be made. Nothing was written.`
26969
27039
  };
26970
27040
  }
@@ -27000,6 +27070,10 @@ var init_specs2 = __esm({
27000
27070
  changes,
27001
27071
  ineffective,
27002
27072
  notices,
27073
+ // The store never searches for tests: it holds the tree and knows
27074
+ // nothing about the rule engine that reads code. The GATED write above
27075
+ // it fills this in, which is also the only write path a human drives.
27076
+ testsToRevisit: [],
27003
27077
  summary: `Updated ${kind} "${id}": ${changes.length} change${changes.length === 1 ? "" : "s"}.`
27004
27078
  };
27005
27079
  }
@@ -28228,11 +28302,11 @@ function candidateOptions() {
28228
28302
  function noticesFrom(verdict) {
28229
28303
  return verdict.warnings.map((w) => `${w.code}: ${w.message}`);
28230
28304
  }
28231
- function componentCandidateGate() {
28305
+ function componentCandidateGate(options = candidateOptions()) {
28232
28306
  return {
28233
28307
  gate: (kind, merged) => {
28234
28308
  if (kind !== "component") return;
28235
- const verdict = validateComponentCandidate(merged, candidateOptions());
28309
+ const verdict = validateComponentCandidate(merged, options);
28236
28310
  if (verdict.errors.length) throw new Error(formatCandidateRefusal(verdict));
28237
28311
  return noticesFrom(verdict);
28238
28312
  }
@@ -28244,7 +28318,27 @@ function addComponent(candidate) {
28244
28318
  return [...noticesFrom(verdict), ...saveComponentSpec(candidate)];
28245
28319
  }
28246
28320
  function updateSpecGated(kind, id, delta, dryRun) {
28247
- return updateSpec(kind, id, delta, componentCandidateGate(), dryRun);
28321
+ const bound2 = candidateOptions();
28322
+ const gate = componentCandidateGate(bound2);
28323
+ const report2 = updateSpec(kind, id, delta, gate, dryRun);
28324
+ const testRoots = bound2.rules?.conformance?.testRoots ?? [];
28325
+ const changed = changedMethods(report2, delta);
28326
+ if (changed.length > 0 && testRoots.length > 0) {
28327
+ report2.testsToRevisit = findTestsReferencing2(changed, getProjectRoot(), testRoots);
28328
+ }
28329
+ return report2;
28330
+ }
28331
+ function changedMethods(report2, delta) {
28332
+ const named = /* @__PURE__ */ new Set();
28333
+ for (const change of report2.changes) {
28334
+ const [field, name] = change.path.split(".");
28335
+ if (field === "methods" && name) named.add(name);
28336
+ }
28337
+ const written = Array.isArray(delta?.methods) ? delta.methods : [];
28338
+ return [...named].map((name) => ({
28339
+ name,
28340
+ symbol: written.find((m) => m?.name === name)?.symbol
28341
+ }));
28248
28342
  }
28249
28343
  function methodMoveGate(source) {
28250
28344
  const bound2 = candidateOptions();
@@ -28279,6 +28373,7 @@ var init_authoring = __esm({
28279
28373
  init_validation();
28280
28374
  init_candidate();
28281
28375
  init_core();
28376
+ init_fs();
28282
28377
  }
28283
28378
  });
28284
28379
 
@@ -28386,6 +28481,17 @@ function renderChangeReport(report2) {
28386
28481
  lines.push("", "NO EFFECT:", ...report2.ineffective.map((i) => `- ${i}`));
28387
28482
  }
28388
28483
  if (report2.notices.length) lines.push("", "NOTICE:", ...report2.notices.map((n) => `- ${n}`));
28484
+ if (report2.testsToRevisit.length) {
28485
+ lines.push("", "TESTS TO REVISIT:");
28486
+ for (const entry of report2.testsToRevisit) {
28487
+ lines.push(`- ${entry.method} (searched as "${entry.symbol}")`);
28488
+ if (entry.imported.length) lines.push(` imports it: ${entry.imported.join(", ")}`);
28489
+ if (entry.mentioned.length) lines.push(` names it: ${entry.mentioned.join(", ")}`);
28490
+ if (entry.indiscriminate) {
28491
+ lines.push(" names it: withheld \u2014 the bare name matches more than a tenth of the suite, so the list carries no signal.");
28492
+ }
28493
+ }
28494
+ }
28389
28495
  return lines.join("\n");
28390
28496
  }
28391
28497
  function writeReceipt(sentence, receipt) {
@@ -29990,6 +30096,15 @@ var init_server = __esm({
29990
30096
  ),
29991
30097
  notices: import_zod11.z.array(import_zod11.z.string()).describe("Store placement notices, gate warnings and delta notices."),
29992
30098
  summary: import_zod11.z.string().describe("One line for people."),
30099
+ testsToRevisit: import_zod11.z.array(import_zod11.z.object({
30100
+ method: import_zod11.z.string().describe("The contract method this write changed or deleted."),
30101
+ symbol: import_zod11.z.string().describe("The code-level name searched for: the method's `symbol` when it declares one, else its name."),
30102
+ imported: import_zod11.z.array(import_zod11.z.string()).describe("Test files that IMPORT that symbol \u2014 the high-confidence list, because an import is a binding and not a coincidence."),
30103
+ mentioned: import_zod11.z.array(import_zod11.z.string()).describe("Test files that name the symbol without importing it \u2014 usually a test driving it through a portal. Empty when `indiscriminate` is true."),
30104
+ indiscriminate: import_zod11.z.boolean().describe("True when the bare name matched more than a tenth of the test suite, so the mention list was withheld as noise.")
30105
+ })).describe(
30106
+ "The tests that encode a method this write changed or deleted, one entry per such method. Empty when the write touched no method, when the project declares no `rules.conformance.testRoots`, or when nothing references them. Read it before promising that existing tests still pass."
30107
+ ),
29993
30108
  ...staleServerOutput
29994
30109
  };
29995
30110
  validationIssueOutput = {