@abloh/repository 1.0.1 → 1.0.2-staging.34831899691

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +365 -155
  2. package/dist/index.js +1963 -1721
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1,8 +1,12 @@
1
+ // src/aggregator-chain.ts
2
+ import { join as join9 } from "path";
3
+
1
4
  // src/repo-access.ts
2
5
  import { realpathSync as realpathSync2 } from "fs";
3
6
  import { relative, resolve as resolve2 } from "path";
4
7
  import {
5
8
  normalizeRepoPath as normalizeRepoPath2,
9
+ overlayRepoReader,
6
10
  readPackageManifest
7
11
  } from "@abloh/core";
8
12
 
@@ -116,6 +120,10 @@ function diskRepoAccess(repoDir) {
116
120
  }
117
121
  };
118
122
  }
123
+ function overlayRepoAccess(base, files) {
124
+ const reader = overlayRepoReader(base.reader, files);
125
+ return reader === base.reader ? base : { ...base, reader };
126
+ }
119
127
  function repoPath(ctx, absolute) {
120
128
  const target = resolve2(absolute);
121
129
  for (const base of ctx.roots) {
@@ -147,6 +155,50 @@ function accessSubdirectories(ctx, absolute) {
147
155
  return ctx.reader.list(path).filter((name) => ctx.reader.entryKind(path === "" ? name : `${path}/${name}`) === "directory");
148
156
  }
149
157
 
158
+ // src/detect.ts
159
+ import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
160
+ import { dirname as dirname4, isAbsolute as isAbsolute4, join as join8, relative as relative5, resolve as resolve6, sep as sep4 } from "path";
161
+ import { SCRIPT_NAMED_RUNNERS as SCRIPT_NAMED_RUNNERS3, angularRunnerName, commandTokens, compiledOutputEvidence, debugPreparationOverflowStack, readsCompiledOutput, detectModuleLayout, directRunnerCommand, localBinaryPrefix, manifestScripts as manifestScripts3, parseLiteralArguments, parsePreparedTestCommand, planCommand as planCommand3, nextActionSentence, readPackageManagerDeclaration, readPackageManagerPin, resolveCommandPlan, runnerNamesInScript, scriptRuns as scriptRuns2, selectAngularTestTarget, nameDrivenRunners, summaryThenAction, watchModeRefusal, withWatchModeDisabled, withoutWatchDisableFlags } from "@abloh/core";
162
+
163
+ // src/ci-recipe.ts
164
+ import { basename, isAbsolute, relative as relative3, resolve as resolve3 } from "path";
165
+ import { parse } from "yaml";
166
+ import {
167
+ declaredDependencies as declaredDependencies2,
168
+ manifestScripts,
169
+ normalizeRepoPath as normalizeRepoPath3,
170
+ readPackageManifest as readPackageManifest2,
171
+ runIsDroppable,
172
+ scriptRuns
173
+ } from "@abloh/core";
174
+ import {
175
+ admitsMainlineBranch,
176
+ assignServicePorts,
177
+ environmentValueProblem,
178
+ isEnvironmentName,
179
+ isReservedCustomerVariable,
180
+ MAX_ENVIRONMENT_SERVICES,
181
+ serviceNameProblem,
182
+ SCRIPT_NAMED_RUNNERS,
183
+ UNDRIVABLE_RUNNERS,
184
+ undrivableRunner,
185
+ matchesCredentialShape,
186
+ matrixValueText as matrixValueText2,
187
+ nothingToMeasurePathFilter,
188
+ planCommand,
189
+ preparedCommandProblem,
190
+ readFailFastDeclaration,
191
+ refusalLines,
192
+ messageFromFacts,
193
+ renderRefusal,
194
+ reservedComposeProblem,
195
+ reservedComposition,
196
+ MAX_SYSTEM_PACKAGES,
197
+ runnerImageVocabulary,
198
+ sameMatrixValue,
199
+ systemPackageProblem
200
+ } from "@abloh/core";
201
+
150
202
  // src/browser-lane.ts
151
203
  import { dirname as dirname2, join as join2, relative as relative2, sep as sep2 } from "path";
152
204
  function repoPathOrEmpty(ctx, absolute) {
@@ -312,44 +364,6 @@ function readBrowserLane(repoDir, ctx = diskRepoAccess(repoDir)) {
312
364
  return null;
313
365
  }
314
366
 
315
- // src/ci-recipe.ts
316
- import { basename, isAbsolute, relative as relative3, resolve as resolve3 } from "path";
317
- import { parse } from "yaml";
318
- import {
319
- declaredDependencies as declaredDependencies2,
320
- manifestScripts,
321
- normalizeRepoPath as normalizeRepoPath3,
322
- readPackageManifest as readPackageManifest2,
323
- scriptRuns
324
- } from "@abloh/core";
325
- import {
326
- admitsMainlineBranch,
327
- assignServicePorts,
328
- environmentValueProblem,
329
- isEnvironmentName,
330
- isReservedCustomerVariable,
331
- MAX_ENVIRONMENT_SERVICES,
332
- serviceNameProblem,
333
- SCRIPT_NAMED_RUNNERS,
334
- UNDRIVABLE_RUNNERS,
335
- undrivableRunner,
336
- matchesCredentialShape,
337
- matrixValueText as matrixValueText2,
338
- nothingToMeasurePathFilter,
339
- planCommand,
340
- preparedCommandProblem,
341
- readFailFastDeclaration,
342
- refusalLines,
343
- messageFromFacts,
344
- renderRefusal,
345
- reservedComposeProblem,
346
- reservedComposition,
347
- MAX_SYSTEM_PACKAGES,
348
- runnerImageVocabulary,
349
- sameMatrixValue,
350
- systemPackageProblem
351
- } from "@abloh/core";
352
-
353
367
  // src/workflow-environment.ts
354
368
  import { matrixValueText } from "@abloh/core";
355
369
  function layerEnvironment(scopes) {
@@ -1037,7 +1051,7 @@ function readStep(step, manifest, reader) {
1037
1051
  const script = workspaceFirst[2];
1038
1052
  if (TEST_NAMES.test(script)) return { kind: "test", command };
1039
1053
  if (BUILD_NAMES.test(script)) return { kind: "build", command, env };
1040
- return { kind: "other" };
1054
+ return { kind: "other", command, why: "abloh could not read what it does" };
1041
1055
  }
1042
1056
  const scoped = /--workspace[= ]([^\s]+)|--filter[= ]([^\s]+)/u.test(tail);
1043
1057
  if (manifest.scripts[name] !== void 0 || scoped) {
@@ -1050,7 +1064,7 @@ function readStep(step, manifest, reader) {
1050
1064
  if (manifest.scripts[name] !== void 0 && scriptRunsBuild(manifest, name)) {
1051
1065
  return { kind: "build", command, env };
1052
1066
  }
1053
- return { kind: "other" };
1067
+ return { kind: "other", command, why: scriptNotABuildBecause(manifest, name) };
1054
1068
  }
1055
1069
  if (TEST_BINARIES.has(name)) return { kind: "test", command };
1056
1070
  if (BUILD_BINARIES.has(name)) {
@@ -1170,7 +1184,20 @@ function scriptRunsBuild(manifest, name) {
1170
1184
  lifecycle: false
1171
1185
  });
1172
1186
  if (runs.length === 0) return false;
1173
- return runs.every((run) => run.kind === "preparation" && (run.role === "build" || run.role === "tool"));
1187
+ return runs.every((run) => run.kind === "preparation" && run.role === "build");
1188
+ }
1189
+ function scriptNotABuildBecause(manifest, name) {
1190
+ const runs = scriptRuns({
1191
+ scripts: manifest.scripts,
1192
+ dependencies: manifest.dependencies,
1193
+ script: name,
1194
+ lifecycle: false
1195
+ });
1196
+ if (runs.length > 0 && runs.every((run) => runIsDroppable(run) && run.kind !== "test")) {
1197
+ return "it checks the tree rather than writing into it";
1198
+ }
1199
+ const unknown = runs.find((run) => run.kind === "preparation" && run.role === "tool");
1200
+ return unknown === void 0 ? "abloh could not read what it does" : `abloh has no reading of ${unknown.name}, so it cannot say the suite needs it`;
1174
1201
  }
1175
1202
  function clip(command) {
1176
1203
  return command.length > 70 ? `${command.slice(0, 70)}\u2026` : command;
@@ -1334,6 +1361,7 @@ function emptyCiRecipe(refusal) {
1334
1361
  browserLane: null,
1335
1362
  unreadableJobCalls: [],
1336
1363
  refusal,
1364
+ stepsNotCopied: [],
1337
1365
  notices: []
1338
1366
  };
1339
1367
  }
@@ -1359,6 +1387,7 @@ function deriveCiRecipe(input) {
1359
1387
  browserLane: null,
1360
1388
  unreadableJobCalls: [],
1361
1389
  refusal: `your CI configuration could not be read (${error instanceof Error ? error.message : "unknown error"})`,
1390
+ stepsNotCopied: [],
1362
1391
  notices: []
1363
1392
  };
1364
1393
  }
@@ -1385,6 +1414,7 @@ function derive(input) {
1385
1414
  runnerImage: null,
1386
1415
  browserLane: null,
1387
1416
  unreadableJobCalls: calls,
1417
+ stepsNotCopied: [],
1388
1418
  refusal,
1389
1419
  notices: [...bounds, ...notices]
1390
1420
  });
@@ -1598,20 +1628,21 @@ function assemble(input) {
1598
1628
  for (const refused of chosen.refusals) {
1599
1629
  if (!unread.some((seen2) => seen2.step === refused.step && seen2.why === refused.why)) unread.push(refused);
1600
1630
  }
1601
- const unreadMessage = messageFromFacts({
1602
- kind: "setup-ci-steps",
1603
- unread,
1631
+ const stepsRead = {
1604
1632
  workflow: chosen.workflowPath,
1605
1633
  job: chosen.jobId,
1606
1634
  /* THE MAINTAINER'S OWN FILE SAID IT, and `at` is the receipt spelling every other notice in
1607
1635
  this function uses, so a reader meets one name for the job on every line. */
1608
1636
  at: `${chosen.file}::${chosen.jobId}`
1609
- });
1610
- if (unreadMessage.kind === "message") {
1611
- const refusal = unreadMessage.refusal;
1612
- const rendered = renderRefusal(refusal, "terminal");
1637
+ };
1638
+ const sayStepsRead = (facts2) => {
1639
+ const outcome = messageFromFacts({ kind: "setup-ci-steps", ...stepsRead, ...facts2 });
1640
+ if (outcome.kind !== "message") return;
1641
+ const rendered = renderRefusal(outcome.refusal, "terminal");
1613
1642
  if (rendered !== null) notices.push(refusalLines(rendered).join("\n"));
1614
- }
1643
+ };
1644
+ sayStepsRead({ unread, notCopied: [] });
1645
+ sayStepsRead({ unread: [], notCopied: [...chosen.notCopied] });
1615
1646
  const drafted = [];
1616
1647
  const seen = /* @__PURE__ */ new Set();
1617
1648
  const push = (step, source) => {
@@ -1838,6 +1869,11 @@ function assemble(input) {
1838
1869
  runnerImage,
1839
1870
  browserLane,
1840
1871
  unreadableJobCalls: input.unreadableJobCalls,
1872
+ /* THE STEPS THE CHOSEN JOB RUNS THAT THE RECIPE LEFT OUT. See {@link CiRecipe.stepsNotCopied}:
1873
+ this is the chosen candidate's own list and never a pooling of every job's, because the
1874
+ recipe is derived from one job and a sentence about another job's steps would name edits to a
1875
+ file abloh is not reading. */
1876
+ stepsNotCopied: chosen.notCopied,
1841
1877
  refusal: null,
1842
1878
  notices
1843
1879
  };
@@ -2052,6 +2088,7 @@ function blankCandidate(file, jobId, label) {
2052
2088
  testSteps: [],
2053
2089
  installs: false,
2054
2090
  refusals: [],
2091
+ notCopied: [],
2055
2092
  refusedTestSteps: [],
2056
2093
  /* A `.travis.yml` or a CircleCI job declares no `env:` block and no `services:` this module has
2057
2094
  a preset for, so the three keys those produce are empty here by construction. */
@@ -2489,6 +2526,7 @@ function readJob(reader, manifest, file, jobId, job, workflowEnv, bounds = []) {
2489
2526
  testSteps: [],
2490
2527
  installs: false,
2491
2528
  refusals: [],
2529
+ notCopied: [],
2492
2530
  refusedTestSteps: [],
2493
2531
  node: null,
2494
2532
  nodeEnv: NO_NODE_ENVIRONMENT,
@@ -2595,6 +2633,15 @@ function readJob(reader, manifest, file, jobId, job, workflowEnv, bounds = []) {
2595
2633
  candidate.refusedTestSteps.push(classified.why);
2596
2634
  }
2597
2635
  break;
2636
+ case "other":
2637
+ if (!candidate.notCopied.some((seen) => seen.command === classified.command)) {
2638
+ candidate.notCopied.push({
2639
+ step: stepNumber(step),
2640
+ command: classified.command,
2641
+ why: classified.why
2642
+ });
2643
+ }
2644
+ break;
2598
2645
  default:
2599
2646
  break;
2600
2647
  }
@@ -2626,1108 +2673,277 @@ function ciRefusalIsActionable(refusal) {
2626
2673
  }
2627
2674
  var __testing = { classifyRepoDefinedBuild, readAptStep, readCiNodeDeclaration };
2628
2675
 
2629
- // src/config-draft.ts
2630
- import { parse as parse2 } from "yaml";
2631
- import {
2632
- CUSTOMER_TYPED,
2633
- DeclaredRefusalError,
2634
- refusalFromFacts,
2635
- WORKFLOW_FILE_PATH,
2636
- WORKFLOW_JOB_ID,
2637
- SEALED_BUN_VERSION,
2638
- SEALED_DENO_VERSION,
2639
- SETUP_SCRIPT_PATH,
2640
- configFlakyLines,
2641
- installCommandNeedsCorepackEnable,
2642
- parseLiteralArguments,
2643
- systemPackageInstallCommand,
2644
- configHeaderLines,
2645
- configSettingLines,
2646
- configSetupJobLines,
2647
- credentialShapeRefusal,
2648
- matchesCredentialShape as matchesCredentialShape2,
2649
- workflowYaml,
2650
- yamlScalar
2651
- } from "@abloh/core";
2652
-
2653
- // src/init-refusal.ts
2654
- var InitRefusal = class extends Error {
2655
- failureClass;
2656
- /**
2657
- * THE WALK KEY THIS REFUSAL IS ABOUT, where the throw site knows one the class cannot.
2658
- *
2659
- * Null on almost every throw, and that is right: the class already implies the key for a refusal
2660
- * that can only ever be about one - `no-reviewed-image` is `runtimeImage` wherever it is raised,
2661
- * and `init-refusal-shape.ts` holds that mapping in one table rather than repeating it at forty
2662
- * throw sites. This field is for the refusals where the class covers SEVERAL keys, which today is
2663
- * the answers document: `invalid-answers-file` is about whichever key the document got wrong, and
2664
- * a fix depends on knowing which.
2665
- */
2666
- questionKey;
2667
- /**
2668
- * THIS REFUSAL'S OWN REMEDY, where the class table's is not true of this instance.
2669
- *
2670
- * Null on almost every throw, and that is still right: a per-class remedy is what stops forty
2671
- * throw sites carrying forty drifting copies of the same sentence. But a CLASS is not always one
2672
- * situation, and `repository-too-large` is where that broke (external refusal review, rank 15).
2673
- * Its four throw sites all tell the human to pass `--environment-image`, because the scan they
2674
- * abandon is the Dockerfile scan and an image is what makes it unnecessary. The class table told
2675
- * the MACHINE to pass `--subdir`, which narrows nothing: the scan walks the repository root
2676
- * whatever `--subdir` says. A human and an agent reading the same refusal were sent to different
2677
- * options, and only one of them could work.
2678
- *
2679
- * So a throw site that knows better than its class says so here, and `init-refusal-shape.ts`
2680
- * prefers it. One refusal instance, one remedy, whoever is reading.
2681
- */
2682
- remedy;
2683
- constructor(failureClass, message, questionKey = null, remedy = null) {
2684
- super(message);
2685
- this.name = "InitRefusal";
2686
- this.failureClass = failureClass;
2687
- this.questionKey = questionKey;
2688
- this.remedy = remedy;
2676
+ // src/deno-project.ts
2677
+ import { lstatSync as lstatSync2, readFileSync as readFileSync2 } from "fs";
2678
+ import { join as join4 } from "path";
2679
+ import { parseJsonc } from "@abloh/core";
2680
+ var DENO_CONFIG_FILES = ["deno.json", "deno.jsonc"];
2681
+ var DENO_LOCKFILE = "deno.lock";
2682
+ function stageInvokesDenoTest(stage) {
2683
+ const tokens = stage.trim().split(/\s+/u).filter((token) => token !== "");
2684
+ const first = (tokens[0] ?? "").replace(/^.*\//u, "");
2685
+ return first === "deno" && /^deno\s+test(?:\s|$)/u.test(stage.trim());
2686
+ }
2687
+ function denoTestInvocation(taskBody) {
2688
+ if (taskBody === null) return null;
2689
+ const script = taskBody.trim();
2690
+ if (script === "") return null;
2691
+ if (/[;|<>`$()\n]/u.test(script)) return null;
2692
+ if (/(^|[^&])&([^&]|$)/u.test(script)) return null;
2693
+ const stages = script.split("&&").map((stage) => stage.trim());
2694
+ if (stages.some((stage) => stage === "")) return null;
2695
+ const matches = stages.filter(stageInvokesDenoTest);
2696
+ return matches.length === 1 ? matches[0] : null;
2697
+ }
2698
+ function readDenoProject(dir, readText = diskReadText) {
2699
+ for (const name of DENO_CONFIG_FILES) {
2700
+ const path = join4(dir, name);
2701
+ const text = readText(path);
2702
+ if (text === null) continue;
2703
+ const parsed = parseJsonc(text);
2704
+ const tasks = parsed?.tasks;
2705
+ const declared = tasks !== null && typeof tasks === "object" ? tasks["test"] : void 0;
2706
+ const body = typeof declared === "string" ? declared : declared !== null && typeof declared === "object" && typeof declared.command === "string" ? declared.command : null;
2707
+ if (body === null || body.trim() === "") return null;
2708
+ return { configFile: name, testTask: body, testInvocation: denoTestInvocation(body) };
2689
2709
  }
2690
- };
2691
- function initRefusal(failureClass, message, questionKey = null, remedy = null) {
2692
- return new InitRefusal(failureClass, message, questionKey, remedy);
2710
+ return null;
2693
2711
  }
2694
- function refusalRemedyOf(error) {
2695
- return error instanceof InitRefusal ? error.remedy : null;
2712
+ function denoLockfilePath(dir, readText = diskReadText) {
2713
+ return readText(join4(dir, DENO_LOCKFILE)) === null ? null : DENO_LOCKFILE;
2696
2714
  }
2697
- function initFailureClassOf(error) {
2698
- return error instanceof InitRefusal ? error.failureClass : "unclassified";
2715
+ function scriptRunsDenoTest(testScript) {
2716
+ if (testScript === null) return false;
2717
+ return testScript.split(/&&|\|\||[;&|]/u).map((stage) => stage.trim()).some(stageInvokesDenoTest);
2718
+ }
2719
+ function diskReadText(absolutePath) {
2720
+ try {
2721
+ const info = lstatSync2(absolutePath);
2722
+ if (!info.isFile() || info.isSymbolicLink()) return null;
2723
+ return readFileSync2(absolutePath, "utf8");
2724
+ } catch {
2725
+ return null;
2726
+ }
2699
2727
  }
2700
2728
 
2701
- // src/config-draft.ts
2702
- function commentLines(text, indent, width = 108) {
2703
- const lines = [];
2704
- let current = "";
2705
- for (const word of text.split(/\s+/u).filter((value) => value !== "")) {
2706
- const candidate = current === "" ? word : `${current} ${word}`;
2707
- if (`${indent}# ${candidate}`.length > width && current !== "") {
2708
- lines.push(`${indent}# ${current}`);
2709
- current = word;
2710
- continue;
2729
+ // src/rush-workspace.ts
2730
+ import { parseJsonc as parseJsonc2 } from "@abloh/core";
2731
+ var RUSH_CONFIG_FILE = "rush.json";
2732
+ var RUSH_TEST_SCRIPTS = ["test", "_phase:test"];
2733
+ var RUSH_CONFIG_DIRECTORY = "common/config/rush";
2734
+ var RUSH_SUBSPACE_DIRECTORY = "common/config/subspaces";
2735
+ var RUSH_LOCKFILE_NAME = "pnpm-lock.yaml";
2736
+ var RUSH_REPO_STATE_NAME = "repo-state.json";
2737
+ var RUSH_INSTALL_COMMAND = "node common/scripts/install-run-rush.js install";
2738
+ var RUSH_INSTALL_SCRIPT = "common/scripts/install-run-rush.js";
2739
+ function classifyRushTestScript(manifest, directory) {
2740
+ const table = manifestScripts2(manifest);
2741
+ for (const script of RUSH_TEST_SCRIPTS) {
2742
+ const body = table[script];
2743
+ if (typeof body !== "string" || body.trim() === "") continue;
2744
+ if (/[;&|<>`\n]|\$\(/u.test(body)) {
2745
+ return {
2746
+ state: "unmeasurable",
2747
+ reason: `the Rush project at ${directory} composes shell commands in its ${script} script rather than running one suite`,
2748
+ remedy: `reduce ${directory}'s ${script} script to one command, or declare a plain "test" script that runs one suite`
2749
+ };
2711
2750
  }
2712
- current = candidate;
2751
+ return { state: "command", script, command: body.trim() };
2713
2752
  }
2714
- if (current !== "") lines.push(`${indent}# ${current}`);
2715
- return lines;
2753
+ return {
2754
+ state: "unmeasurable",
2755
+ reason: `the Rush project at ${directory} declares neither a "test" nor a "_phase:test" script`,
2756
+ remedy: `give ${directory} a "test" or "_phase:test" script in its package.json that runs its tests`
2757
+ };
2716
2758
  }
2717
- function renderEnvironmentBlock(input) {
2718
- const quote = input.yamlQuote ?? '"';
2719
- const receiptLinesQ = (entries, indent) => receiptLines(entries, indent, quote);
2720
- const lines = [];
2721
- if (input.coverageNote != null) lines.push(...commentLines(input.coverageNote, "", 80), "");
2722
- if (input.subdir !== null) {
2723
- lines.push(
2724
- "# The package measured.",
2725
- "target:",
2726
- ` directory: ${yamlScalar(input.subdir, quote)}`,
2727
- ""
2728
- );
2729
- }
2730
- lines.push(
2731
- ENVIRONMENT_BLOCK_HEADER,
2732
- "environment:",
2733
- ...receiptLinesQ([
2734
- { key: "runtimeImage", value: input.runtimeImage, receipt: input.evidence.runtimeImage }
2735
- ], " "),
2736
- ...input.runtimes.length === 0 ? [] : [
2737
- " runtimes: # pinned runtimes added to the proof image, installed at build time",
2738
- ...input.runtimes.flatMap((need) => [
2739
- ...commentLines(need.evidence, " "),
2740
- ` - ${yamlScalar(need.runtime, quote)}`
2741
- ])
2742
- ],
2743
- ...input.browser === null ? [] : [
2744
- ...commentLines(
2745
- "your suite drives a real browser, so the proof image carries one and the run uses the browser seal - still --network none, reaching 127.0.0.1 and nothing else. Read from: " + input.browser.evidence.join("; "),
2746
- " "
2747
- ),
2748
- ` browser: ${yamlScalar(input.browser.value, quote)}`
2749
- ],
2750
- ` installDirectory: ${yamlScalar(input.installDirectory, quote)}`,
2751
- ...receiptLinesQ([
2752
- /* THE POINTER, WHERE FOUR DERIVED KEYS USED TO BE (Kenneth, 2026-08-26). `systemPackages`,
2753
- `installCommand` and `setupCommands` are the commands that turn a checkout into a tree the
2754
- suite can run in, and they are now steps in a file the customer owns. What is left in this
2755
- block is everything that is NOT a command: the image, the directory, the test commands, the
2756
- services, the variables, the files. The receipt names the script rather than a source file
2757
- because the script carries a receipt of its own on every step. */
2758
- {
2759
- key: "setup",
2760
- value: input.setupPath ?? SETUP_SCRIPT_PATH,
2761
- receipt: "written by init and yours to edit. Every cold run executes it step by step"
2762
- },
2763
- /* THE ONE THING THE SCRIPT CANNOT SAY, so the file still does: WHY there is no install step
2764
- in it. A repository with no frozen recipe gets a script with nothing to install, and a
2765
- reader who only ever opens `abloh.yml` would otherwise see a pointer at a file that quietly
2766
- does less than they think. The remedy is the same sentence the console prints. */
2767
- ...input.installCommand === null ? [{ key: "installCommand", value: null, receipt: input.evidence.installCommand }] : [],
2768
- /* THE ONE RECEIPT THAT GAINS A CLAUSE IN THE FILE TOO, and only on the repositories it is
2769
- true of. `genericAdapterCost` carries what changed and why the prompt alone was not it. */
2770
- {
2771
- key: "testCommand",
2772
- value: input.testCommand,
2773
- receipt: testCommandReceipt({
2774
- evidence: input.evidence.testCommand,
2775
- cost: input.testCommandCostNote
2776
- })
2777
- },
2778
- /* WRITTEN ONLY WHERE IT SAYS SOMETHING, and its receipt is the whole reason it says anything.
2779
- See `ConfigInput.runner`: on an uncontested repository the key would restate what
2780
- `bindPreparedTestCommand` reads off the command one line above, so there is no line and the
2781
- PROMPT is where the detected runner is shown. Where a candidate was discarded, the line
2782
- records the decision AND names what was skipped, in the same words the prompt used - which
2783
- is what makes an unattended init as informed as an interactive one. */
2784
- ...input.runner === null ? [] : [{ key: "runner", value: input.runner, receipt: input.evidence.runner }],
2785
- /* THE ANSWER TO THE ONE QUESTION A VITEST REPOSITORY WITH NO COVERAGE PROVIDER IS ASKED.
2786
- WRITTEN ONLY WHERE SOMEBODY ANSWERED IT, on the same restraint every derived key here
2787
- follows: absent already means `install`, so a line saying `install` on a repository nobody
2788
- asked would be a claim about a conversation that never happened. Both answers are written
2789
- where there WAS one, because the file is where the run reads it. */
2790
- ...input.coverageProvider === void 0 ? [] : [{
2791
- key: "coverageProvider",
2792
- value: input.coverageProvider,
2793
- receipt: input.coverageProvider === "install" ? "you agreed abloh may stage @vitest/coverage-v8 in the job that measures, into its own cache outside this repository" : "you asked abloh not to install @vitest/coverage-v8, so the changed-line check reports itself as declined"
2794
- }],
2795
- ...input.sealedTestCommand !== null || input.sealedNote !== null ? [{
2796
- key: "sealedTestCommand",
2797
- value: input.sealedTestCommand,
2798
- receipt: input.sealedTestCommand === null ? input.sealedNote ?? "" : input.evidence.sealedTestCommand
2799
- }] : [],
2800
- /* THE ONE KEY WHOSE ABSENCE PRODUCES A WRONG NUMBER RATHER THAN A REFUSAL, so the file states
2801
- the finding even when it cannot state a value: a reviewer reading a commented-out
2802
- `rebuildCommand` beside "these two files say your tests read your build output" has the
2803
- whole question in front of them, and a file that stayed silent would leave a run reporting
2804
- every mutant as a survivor with nothing anywhere to explain it. */
2805
- ...input.rebuild === null ? [] : [{
2806
- key: "rebuildCommand",
2807
- value: input.rebuild.value,
2808
- /* THE KEY IS NAMED IN THE SENTENCE, because a null value renders as a bare comment with
2809
- no key beside it - and a reader who agrees with the finding then has nothing to type.
2810
- It is the same rule `installRemedy` follows one key up. */
2811
- receipt: input.rebuild.value !== null ? input.rebuild.evidence : `${input.rebuild.evidence}. Set rebuildCommand to the build that writes it - this package declares ${input.rebuild.candidates.join(" and ")}. Without one, every planted bug is reported as missed`
2812
- }]
2813
- ], " ")
2814
- );
2815
- if (input.services.length > 0) {
2816
- lines.push(
2817
- " services: # fresh instances per proof phase, reachable on localhost exactly as in your CI"
2818
- );
2819
- for (const service of input.services) {
2820
- lines.push(
2821
- ...commentLines(service.evidence, " "),
2822
- ` - name: ${yamlScalar(service.name, quote)}`,
2823
- ` image: ${yamlScalar(service.image, quote)}`
2824
- );
2825
- if (service.env.length > 0) {
2826
- lines.push(" env: # your workflow's own values; the suite connects with these");
2827
- for (const entry of service.env) lines.push(` ${entry.name}: ${yamlScalar(entry.value, quote)}`);
2828
- }
2829
- if (service.healthCommand !== null) {
2830
- lines.push(` healthCommand: ${yamlScalar(service.healthCommand, quote)}`);
2831
- }
2832
- if (service.healthPort !== null) lines.push(` healthPort: ${service.healthPort}`);
2833
- if (service.localhostPort != null && service.localhostPort !== service.healthPort) {
2834
- lines.push(
2835
- ` localhostPort: ${service.localhostPort} # another declared service listens on this one's port, so a run reaches it here`
2836
- );
2837
- }
2759
+ function manifestScripts2(manifest) {
2760
+ const scripts = manifest?.scripts;
2761
+ return scripts !== null && typeof scripts === "object" ? scripts : {};
2762
+ }
2763
+ function enumerateRushProjects(readFile, limit) {
2764
+ const parsed = parseJsonc2(readFile(RUSH_CONFIG_FILE));
2765
+ const declared = parsed?.projects;
2766
+ if (!Array.isArray(declared)) return [];
2767
+ const projects = [];
2768
+ for (const entry of declared) {
2769
+ if (entry === null || typeof entry !== "object") continue;
2770
+ const name = entry.packageName;
2771
+ const folder = entry.projectFolder;
2772
+ if (typeof name !== "string" || typeof folder !== "string") continue;
2773
+ const directory = normalizeProjectFolder(folder);
2774
+ if (directory === null) continue;
2775
+ const manifestPath = `${directory}/package.json`;
2776
+ const manifestText = readFile(manifestPath);
2777
+ if (manifestText === null) {
2778
+ projects.push({
2779
+ directory,
2780
+ name,
2781
+ test: {
2782
+ state: "unmeasurable",
2783
+ // Enumerated even so: the directory must not be silently absorbed into a parent's file
2784
+ // list, which would attribute this project's changed files to another project.
2785
+ reason: `the Rush project ${name} declares the folder ${directory}, which has no readable package.json`,
2786
+ remedy: `add ${manifestPath}, or remove ${name} from rush.json's projects`
2787
+ }
2788
+ });
2789
+ continue;
2838
2790
  }
2791
+ const manifest = parseJsonc2(manifestText);
2792
+ projects.push({
2793
+ directory,
2794
+ name,
2795
+ test: manifest === null ? {
2796
+ state: "unmeasurable",
2797
+ reason: `the Rush project ${name}'s manifest at ${manifestPath} is not readable JSON`,
2798
+ remedy: `fix ${manifestPath} so its manifest parses`
2799
+ } : classifyRushTestScript(manifest, directory)
2800
+ });
2801
+ if (projects.length >= limit) break;
2839
2802
  }
2840
- if (input.identityFiles.length > 0) {
2841
- lines.push(" identityFiles: # a change here triggers reinstall instead of container reuse");
2842
- for (const path of input.identityFiles) lines.push(` - ${yamlScalar(path, quote)}`);
2843
- }
2844
- if (input.requiredVariables.length > 0) {
2845
- lines.push(" requiredVariables: # names only; values come from your environment and are masked in every log");
2846
- for (const variable of input.requiredVariables) {
2847
- lines.push(...commentLines(variable.evidence, " "), ` - ${yamlScalar(variable.name, quote)}`);
2803
+ return projects.sort((a, b) => a.directory < b.directory ? -1 : a.directory > b.directory ? 1 : 0);
2804
+ }
2805
+ function normalizeProjectFolder(raw) {
2806
+ const path = raw.trim().replace(/\\/gu, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
2807
+ if (path === "" || path.startsWith("/") || /^[A-Za-z]:/u.test(path)) return null;
2808
+ if (/[;&|<>`\s'"$]|\$\(/u.test(path)) return null;
2809
+ if (path.split("/").some((segment) => segment === "..")) return null;
2810
+ return path;
2811
+ }
2812
+ function rushIdentityFiles(input) {
2813
+ const files = [RUSH_CONFIG_FILE];
2814
+ const collect = (directory) => {
2815
+ for (const name of [RUSH_LOCKFILE_NAME, RUSH_REPO_STATE_NAME]) {
2816
+ const path = `${directory}/${name}`;
2817
+ if (input.exists(path)) files.push(path);
2848
2818
  }
2819
+ };
2820
+ collect(RUSH_CONFIG_DIRECTORY);
2821
+ for (const subspace of [...input.subdirectories(RUSH_SUBSPACE_DIRECTORY)].sort()) {
2822
+ collect(`${RUSH_SUBSPACE_DIRECTORY}/${subspace}`);
2849
2823
  }
2850
- if (input.environmentValues.length > 0) {
2851
- for (const variable of input.environmentValues) {
2852
- const shape = matchesCredentialShape2(variable.value);
2853
- if (shape !== null) throw initRefusal("credential-in-environment-value", credentialShapeRefusal(variable.name, shape));
2854
- }
2855
- lines.push(" environmentValues: # literal values your CI sets; a secret goes in requiredVariables, by name");
2856
- for (const variable of input.environmentValues) {
2857
- lines.push(
2858
- ...commentLines(variable.evidence, " "),
2859
- ` ${variable.name}: ${yamlScalar(variable.value, quote)}`
2860
- );
2861
- }
2824
+ return files;
2825
+ }
2826
+ function rushLockfiles(input) {
2827
+ return rushIdentityFiles(input).filter((path) => path.endsWith(`/${RUSH_LOCKFILE_NAME}`));
2828
+ }
2829
+ function rushNodeVersionRange(rushJsonText) {
2830
+ const parsed = parseJsonc2(rushJsonText);
2831
+ const range = parsed?.nodeSupportedVersionRange;
2832
+ return typeof range === "string" && range.trim() !== "" ? range.trim() : null;
2833
+ }
2834
+
2835
+ // src/workspace-aggregator.ts
2836
+ import { SCRIPT_NAMED_RUNNERS as SCRIPT_NAMED_RUNNERS2, binaryName, planCommand as planCommand2 } from "@abloh/core";
2837
+ function invokedProgram(command) {
2838
+ const plan = planCommand2(command);
2839
+ if (plan.kind === "program") return { program: plan.program, args: plan.args };
2840
+ if (plan.kind === "manager-passthrough" && plan.token !== null) {
2841
+ return { program: binaryName(plan.token), args: plan.args };
2862
2842
  }
2863
- if (input.generatedFiles.length > 0) {
2864
- lines.push(" generatedFiles: # written by your tooling during a run; an undeclared change aborts it");
2865
- for (const generated of input.generatedFiles) {
2866
- lines.push(...commentLines(generated.evidence, " "), ` - ${yamlScalar(generated.path, quote)}`);
2843
+ return null;
2844
+ }
2845
+ var TOOLS = ["turbo", "lage", "nx", "lerna"];
2846
+ function nxTargets(args) {
2847
+ const targets = [];
2848
+ for (let at = 0; at < args.length; at += 1) {
2849
+ const arg = args[at];
2850
+ const inline = /^(?:-t|--targets?)=(.+)$/u.exec(arg);
2851
+ if (inline !== null) {
2852
+ targets.push(...inline[1].split(","));
2853
+ continue;
2867
2854
  }
2868
- }
2869
- if (input.rewrittenFiles.length > 0) {
2870
- lines.push(" rewrittenFiles: # committed files your own install rewrites; measured as your install left them");
2871
- for (const rewritten of input.rewrittenFiles) {
2872
- lines.push(...commentLines(rewritten.evidence, " "), ` - ${yamlScalar(rewritten.path, quote)}`);
2855
+ if (/^(?:-t|--targets?)$/u.test(arg)) {
2856
+ const value = args[at + 1];
2857
+ if (value !== void 0 && !value.startsWith("-")) {
2858
+ targets.push(...value.split(","));
2859
+ at += 1;
2860
+ }
2873
2861
  }
2874
2862
  }
2875
- return lines;
2863
+ return targets.filter((task) => task !== "");
2876
2864
  }
2877
- function renderConfig(input) {
2878
- const coverage = input.coverageNote == null ? [] : [...commentLines(input.coverageNote, "", 80), ""];
2879
- const lines = [
2880
- ...coverage,
2881
- ...configHeaderLines("init"),
2882
- ...configSettingLines(),
2883
- /* AFTER THE TWO SETTINGS EVERY FILE CARRIES AND BEFORE THE JOB, because it is a setting of the
2884
- same kind - a rule about the whole run rather than a fact about this repository's CI. */
2885
- ...configFlakyLines(input.flaky),
2886
- /* BEFORE THE ENVIRONMENT BLOCK, because it is the shortest answer to the first question a
2887
- reviewer of this pull request asks - "which of our jobs is this running in" - and the
2888
- environment block below is the long answer to a different one. */
2889
- ...configSetupJobLines(
2890
- input.setupJob,
2891
- input.setupMatrixLeg,
2892
- input.setupFailFast,
2893
- input.setupStepUnreadableConditions,
2894
- input.yamlQuote ?? '"'
2895
- ),
2896
- ...renderEnvironmentBlock({ ...input, coverageNote: null })
2897
- ];
2898
- return `${lines.join("\n")}
2899
- `;
2900
- }
2901
- function renderWorkflow(input) {
2902
- return readableWorkflow(workflowYaml({
2903
- node: input.workflowNode,
2904
- /* THE ONE RECIPE, NAMED. `environment.setup` when the repository declares its own path, so a
2905
- re-run of `init` over a customer's chosen path writes a workflow that calls that path. */
2906
- setupScriptPath: input.setupPath ?? SETUP_SCRIPT_PATH,
2907
- /* The same directory the sealed image runs those steps in - `installWorkdir` in the runner -
2908
- because a recipe that installs in one directory on the runner and another in the proof image
2909
- is two recipes again. */
2910
- setupWorkingDirectory: input.installDirectory,
2911
- runtimes: input.runtimes.map((need) => ({
2912
- name: need.runtime,
2913
- version: need.runtime === "bun" ? SEALED_BUN_VERSION : SEALED_DENO_VERSION,
2914
- evidence: need.evidence
2915
- }))
2916
- }));
2917
- }
2918
- function readableWorkflow(text) {
2919
- try {
2920
- parse2(text);
2921
- } catch (error) {
2922
- throw new DeclaredRefusalError(refusalFromFacts({
2923
- kind: "setup-step-written",
2924
- workflow: WORKFLOW_FILE_PATH,
2925
- job: WORKFLOW_JOB_ID,
2926
- why: `it does not parse as YAML: ${error instanceof Error ? error.message : "unknown error"}`
2927
- }));
2928
- }
2929
- return text;
2930
- }
2931
- function testCommandReceipt(input) {
2932
- return input.cost == null ? input.evidence : `${input.evidence}. ${input.cost}`;
2933
- }
2934
- function receiptLines(entries, indent, quote) {
2935
- const bodyOf = (entry) => entry.value === null ? null : `${indent}${entry.key}: ${yamlScalar(entry.value, quote)}`;
2936
- const itemBody = (item) => `${indent} - ${yamlScalar(item.value, quote)}`;
2937
- const withReceipt = (body, receipt) => `${body} # ${receipt}`;
2938
- const lines = [];
2939
- for (const entry of entries) {
2940
- if (entry.items !== void 0) {
2941
- if (entry.items.length === 0) continue;
2942
- lines.push(`${indent}${entry.key}:${entry.receipt === "" ? "" : ` # ${entry.receipt}`}`);
2943
- for (const item of entry.items) lines.push(withReceipt(itemBody(item), item.receipt));
2944
- continue;
2945
- }
2946
- const body = bodyOf(entry);
2947
- if (body === null) {
2948
- if (entry.receipt !== "") lines.push(`${indent}# ${entry.key}: ${entry.receipt}`);
2949
- continue;
2865
+ function aggregatorInvocation(testScript) {
2866
+ if (testScript === null) return null;
2867
+ const script = testScript.trim();
2868
+ if (script === "" || /[;&|<>`\n]|\$\(/u.test(script)) return null;
2869
+ const invoked = invokedProgram(script);
2870
+ if (invoked === null) return null;
2871
+ const tool = TOOLS.find((candidate) => candidate === invoked.program);
2872
+ if (tool === void 0) return null;
2873
+ const args = invoked.args;
2874
+ if (tool === "nx") {
2875
+ const explicit = nxTargets(args);
2876
+ if (explicit.length > 0) return { tool, tasks: explicit };
2877
+ const positional = args.filter((arg) => !arg.startsWith("-"));
2878
+ const head = positional[0];
2879
+ if (head === void 0) return { tool, tasks: [] };
2880
+ if (head === "run-many" || head === "affected") return { tool, tasks: [] };
2881
+ if (head === "run") {
2882
+ const target = positional[1]?.split(":")[1];
2883
+ return { tool, tasks: target === void 0 ? [] : [target] };
2950
2884
  }
2951
- lines.push(withReceipt(body, entry.receipt));
2885
+ return { tool, tasks: [head] };
2952
2886
  }
2953
- return lines;
2954
- }
2955
- var ENVIRONMENT_BLOCK_HEADER = "# Rebuild recipe for the proof container. Comments name the source file.";
2956
- var STEP_WHAT = {
2957
- systemPackages: "OS packages your suite needs, pinned",
2958
- corepack: "put the package manager on PATH, for your own lifecycle scripts",
2959
- install: "dependencies, from your lockfile",
2960
- /**
2961
- * THE SAME STEP ON A REPOSITORY THAT CARRIES NO LOCKFILE (census run 7 F3, 2026-09-06).
2962
- *
2963
- * A SECOND SPELLING RATHER THAN A REWORD, on the rule above: `install`'s exact text is what a
2964
- * re-run recognizes as its own step, so changing it would orphan every install step already
2965
- * committed. Both are install steps to {@link initStepRole} and to {@link isInstallStep}, which is
2966
- * what every reader asks instead of comparing against one of them.
2967
- *
2968
- * IT EXISTS BECAUSE THE OTHER ONE WOULD BE FALSE. `fastify/fastify` and `pinojs/pino` both
2969
- * `.gitignore` their lockfile, so the drafted step resolves rather than installing frozen, and a
2970
- * comment saying "from your lockfile" above a command that has none is a claim the file cannot
2971
- * back. The `source` beside it names the ignore rule.
2972
- */
2973
- installUnlocked: "dependencies, resolved - your repository commits no lockfile",
2974
- build: "your build"
2975
- };
2976
- function isInstallStep(what) {
2977
- return what === STEP_WHAT.install || what === STEP_WHAT.installUnlocked;
2978
- }
2979
- function setupStepOrigin(step) {
2980
- return initStepRole(step.what) === null ? CUSTOMER_TYPED : "abloh-derived";
2887
+ if (tool === "lerna") {
2888
+ const positional = args.filter((arg) => !arg.startsWith("-"));
2889
+ if (positional[0] !== "run") return null;
2890
+ const leading2 = positional.slice(1, 2);
2891
+ return { tool, tasks: leading2 };
2892
+ }
2893
+ const leading = args.slice(0, args.findIndex((arg) => arg.startsWith("-")) === -1 ? args.length : args.findIndex((arg) => arg.startsWith("-")));
2894
+ const tasks = leading[0] === "run" ? leading.slice(1) : leading;
2895
+ return { tool, tasks };
2981
2896
  }
2982
- function initStepRole(what) {
2983
- if (what === STEP_WHAT.systemPackages) return "systemPackages";
2984
- if (what === STEP_WHAT.corepack || isInstallStep(what)) return "installCommand";
2985
- if (what === STEP_WHAT.build) return "setupCommands";
2986
- return null;
2897
+ function aggregatorTaskPhrase(invocation) {
2898
+ if (invocation.tasks.length === 0) return "the tasks its own configuration selects";
2899
+ const named = invocation.tasks.length < 3 ? invocation.tasks.join(" and ") : `${invocation.tasks.slice(0, -1).join(", ")} and ${invocation.tasks.at(-1)}`;
2900
+ return `each package's own ${named} script${invocation.tasks.length === 1 ? "" : "s"}`;
2987
2901
  }
2988
- function systemPackagesStep(pinned) {
2989
- if (pinned.length === 0) return null;
2990
- return {
2991
- what: STEP_WHAT.systemPackages,
2992
- source: [...new Set(pinned.map((entry) => entry.source))].join(", "),
2993
- /* THE SAME COMMAND THE IMAGE ALREADY RAN, one line per operation rather than one semicolon
2994
- chain, because this file is read by a person now. `set -e` is in the header, so the chain's
2995
- own guard is redundant here and its semicolons are noise. */
2996
- command: systemPackageInstallCommand(pinned, { pin: "optional" }).replace(/^set -e;\s*/u, "").split(";").map((part) => part.trim()).filter((part) => part !== "").join("\n")
2997
- };
2902
+ var ROOT_CONFIG_EXECUTORS = /* @__PURE__ */ new Map([
2903
+ // Every spelling read out of a real repository on 2026-08-16: `@nx/jest:jest` is what ngxs/store
2904
+ // and nrwl/nx-labs declare, `@nx/vitest:test` is what analogjs/analog declares, and the `@nrwl/*`
2905
+ // names are the pre-16 package scope the same executors shipped under.
2906
+ ["@nx/jest:jest", { runner: "jest", configOption: "jestConfig" }],
2907
+ ["@nrwl/jest:jest", { runner: "jest", configOption: "jestConfig" }],
2908
+ ["@nx/vite:test", { runner: "vitest", configOption: "configFile" }],
2909
+ ["@nx/vitest:test", { runner: "vitest", configOption: "configFile" }],
2910
+ ["@nrwl/vite:test", { runner: "vitest", configOption: "configFile" }]
2911
+ ]);
2912
+ function workspaceRootCommand(runner, configFile, directory) {
2913
+ return runner === "jest" ? `npx --no-install jest --config ${configFile}` : `npx --no-install vitest run --root ${directory} --config ${configFile}`;
2998
2914
  }
2999
- function stepBody(env, command) {
3000
- if (env.length === 0) return command;
3001
- return [
3002
- ...[...env].sort((left, right) => left.name.localeCompare(right.name)).map((value) => `export ${value.name}=${shellSingleQuoted(value.value)}`),
3003
- command
3004
- ].join("\n");
2915
+ var RUN_COMMANDS_EXECUTORS = /* @__PURE__ */ new Set(["nx:run-commands", "@nrwl/workspace:run-commands"]);
2916
+ function commandRunner(command) {
2917
+ const plan = planCommand2(command);
2918
+ if (plan.kind === "node-test") return "node-test";
2919
+ if (plan.kind === "runner") return plan.runner;
2920
+ const invoked = invokedProgram(command);
2921
+ if (invoked === null) return null;
2922
+ return SCRIPT_NAMED_RUNNERS2.includes(invoked.program) ? invoked.program : null;
3005
2923
  }
3006
- function shellSingleQuoted(value) {
3007
- return `'${value.replaceAll("'", `'\\''`)}'`;
2924
+ function normalizeWorkspacePath(raw) {
2925
+ const path = raw.trim().replace(/\\/gu, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
2926
+ if (path === "" || path.startsWith("/") || /^[A-Za-z]:/u.test(path)) return null;
2927
+ if (/[;&|<>`\s'"$]|\$\(/u.test(path)) return null;
2928
+ if (path.split("/").some((segment) => segment === "..")) return null;
2929
+ return path;
3008
2930
  }
3009
- function setupScriptSteps(input) {
3010
- const steps = [];
3011
- const pinned = systemPackagesStep(input.systemPackages);
3012
- if (pinned !== null) steps.push(pinned);
3013
- if (input.installCommand !== null) {
3014
- if (installCommandNeedsCorepackEnable(parseLiteralArguments(input.installCommand))) {
3015
- steps.push({
3016
- what: STEP_WHAT.corepack,
3017
- source: input.evidence.installCommand,
3018
- command: "corepack enable"
3019
- });
3020
- }
3021
- steps.push({
3022
- /* The step and its comment are one statement: a resolved install may not be introduced as
3023
- coming from a lockfile the commit does not hold. See {@link ConfigInput.installUnlocked}. */
3024
- what: input.installUnlocked === true ? STEP_WHAT.installUnlocked : STEP_WHAT.install,
3025
- source: input.evidence.installCommand,
3026
- /* THE COMMAND IS ABLOH'S AND THE ENVIRONMENT IS THE CUSTOMER'S. See
3027
- {@link ConfigInput.installEnvironment}: the command comes from the lockfile and the values
3028
- come from the step their own CI installs with. */
3029
- command: stepBody(input.installEnvironment, input.installCommand)
3030
- });
3031
- }
3032
- for (const entry of input.setupCommands) {
3033
- steps.push({ what: STEP_WHAT.build, source: entry.source, command: stepBody(entry.env, entry.command) });
2931
+ function classifyNxTestTarget(projectJson, directory) {
2932
+ const targets = projectJson?.targets ?? projectJson?.architect;
2933
+ const target = targets?.["test"];
2934
+ if (target === void 0) {
2935
+ return {
2936
+ state: "unmeasurable",
2937
+ reason: `the Nx project at ${directory} declares no test target`,
2938
+ remedy: `give ${directory} a test target in its project.json, or a "test" script in its package.json`
2939
+ };
3034
2940
  }
3035
- return steps;
3036
- }
3037
-
3038
- // src/setup-question-descriptor.ts
3039
- import {
3040
- judgeConfigKeyValue,
3041
- SETUP_FAIL_FAST_ANSWERS,
3042
- SETUP_FAIL_FAST_CONSEQUENCES
3043
- } from "@abloh/core";
3044
- var ATOM = /\s*,\s*/u;
3045
- var COMMANDS = /\s*&&\s*/u;
3046
- function splitAnswerList(shape, raw) {
3047
- const trimmed = raw.trim();
3048
- if (trimmed === "") return [];
3049
- return trimmed.split(shape.kind === "commands" ? COMMANDS : ATOM).map((entry) => entry.trim()).filter((entry) => entry !== "");
3050
- }
3051
- function splitAtFirstEquals(entry) {
3052
- const at = entry.indexOf("=");
3053
- return at === -1 ? [entry, null] : [entry.slice(0, at), entry.slice(at + 1)];
3054
- }
3055
- var SERVICE_ENTRY = /^(?<image>.*@sha256:[a-f0-9]{64})(?::(?<port>\d{1,5}))?$/u;
3056
- function readServiceEntries(raw) {
3057
- return splitAnswerList({ kind: "list" }, raw).map((entry) => {
3058
- const [name, rest] = splitAtFirstEquals(entry);
3059
- const gate = rest === null ? null : SERVICE_ENTRY.exec(rest);
3060
- if (gate === null) return { name, image: rest ?? "" };
3061
- const port = gate.groups?.["port"];
2941
+ const executor = typeof target.executor === "string" ? target.executor : typeof target.builder === "string" ? target.builder : null;
2942
+ if (executor === null) {
3062
2943
  return {
3063
- name,
3064
- image: gate.groups?.["image"] ?? "",
3065
- ...port === void 0 ? {} : { healthPort: Number(port) }
3066
- };
3067
- });
3068
- }
3069
- function readAnswerValue(descriptor, raw) {
3070
- switch (descriptor.shape.kind) {
3071
- case "scalar":
3072
- return raw.trim();
3073
- case "list":
3074
- return splitAnswerList(descriptor.shape, raw);
3075
- case "commands":
3076
- return splitAnswerList(descriptor.shape, raw);
3077
- case "names":
3078
- return splitAnswerList(descriptor.shape, raw);
3079
- case "packages":
3080
- return splitAnswerList(descriptor.shape, raw).map((entry) => {
3081
- const [name, version] = splitAtFirstEquals(entry);
3082
- return version === null ? { name } : { name, version };
3083
- });
3084
- case "services":
3085
- return readServiceEntries(raw);
3086
- case "pairs":
3087
- return Object.fromEntries(
3088
- splitAnswerList(descriptor.shape, raw).map((entry) => {
3089
- const [name, value] = splitAtFirstEquals(entry);
3090
- return [name, value ?? ""];
3091
- })
3092
- );
3093
- }
3094
- }
3095
- function setupAnswerProblem(key, raw) {
3096
- const descriptor = SETUP_QUESTION_DESCRIPTORS[key];
3097
- if (descriptor === void 0) return `abloh asks no question about ${key}`;
3098
- const value = raw.trim();
3099
- if (value === "") return descriptor.required ? "this question needs an answer" : null;
3100
- if (descriptor.candidates.length > 0 && !descriptor.candidates.includes(value)) {
3101
- return `${value} is not one of ${descriptor.candidates.join(", ")}`;
3102
- }
3103
- if (descriptor.key !== "target.directory" && descriptor.channel === "discovery") return null;
3104
- const verdict = judgeConfigKeyValue(key, readAnswerValue(descriptor, raw));
3105
- if (verdict.ok) return null;
3106
- return verdict.at === null || verdict.at === void 0 ? verdict.reason : `${verdict.at} ${verdict.reason}`;
3107
- }
3108
- function applyAnswersToContract(draft, answers) {
3109
- let out = draft;
3110
- for (const descriptor of Object.values(SETUP_QUESTION_DESCRIPTORS)) {
3111
- if (descriptor.apply === void 0) continue;
3112
- const raw = answers[descriptor.key];
3113
- if (raw === void 0 || raw.trim() === "") continue;
3114
- out = descriptor.apply(out, raw);
3115
- }
3116
- return out;
3117
- }
3118
- var ANSWERED_EVIDENCE = "you answered this when you set abloh up";
3119
- function evidenceFor(entries) {
3120
- return entries.map((path) => ({ path, evidence: ANSWERED_EVIDENCE }));
3121
- }
3122
- var SETUP_QUESTION_DESCRIPTORS = {
3123
- "environment.installCommand": {
3124
- key: "environment.installCommand",
3125
- ask: "How does this repository install its dependencies?",
3126
- why: "Abloh read your CI without running it, so it saw the command your workflow declares and not what your install actually writes. On the borrow road your own job installs and Abloh measures inside it, so this is only needed when Abloh builds for itself.",
3127
- candidates: [],
3128
- required: false,
3129
- unreadable: true,
3130
- shape: { kind: "scalar" },
3131
- channel: "contract",
3132
- apply: (draft, answer) => ({
3133
- ...draft,
3134
- installCommand: answer.trim(),
3135
- evidence: { ...draft.evidence, installCommand: ANSWERED_EVIDENCE }
3136
- })
3137
- },
3138
- "environment.setupCommands": {
3139
- key: "environment.setupCommands",
3140
- ask: "What does this repository build between installing and testing?",
3141
- why: "Abloh read the build steps out of the job it rides. A step it could not read is one the proof container never runs, and a suite that needs it then fails inside abloh rather than in your CI.",
3142
- candidates: [],
3143
- required: true,
3144
- /* ONE STEP PER ` && `, WHICH IS THE ONE KEY THAT IS NOT COMMA-SEPARATED and the CLI's prompt says
3145
- so for the same reason: a comma is an ordinary character inside a command, and a build is read
3146
- top to bottom. `splitAnswerList` is the one grammar both roads ask. */
3147
- shape: { kind: "commands" },
3148
- channel: "contract",
3149
- apply: (draft, answer) => ({
3150
- ...draft,
3151
- setupCommands: splitAnswerList({ kind: "commands" }, answer).map((command) => ({
3152
- command,
3153
- source: ANSWERED_EVIDENCE,
3154
- /* A COMMAND SOMEBODY TYPED CARRIES THE ENVIRONMENT THEY TYPED INTO IT. `CiSetupCommand.env`
3155
- is the environment abloh READ off a workflow step, so an answered command has none: what
3156
- the answer says is the whole of what this command runs. */
3157
- env: []
3158
- }))
3159
- })
3160
- },
3161
- "environment.runtimeImage": {
3162
- key: "environment.runtimeImage",
3163
- ask: "Which image should Abloh build its proof container from?",
3164
- why: "Abloh pins one by pulling it and reading its digest, which needs a machine. Left unanswered, a run uses the reviewed default for the Node your repository declares.",
3165
- candidates: [],
3166
- required: false,
3167
- unreadable: true,
3168
- shape: { kind: "scalar" },
3169
- channel: "contract",
3170
- apply: (draft, answer) => ({
3171
- ...draft,
3172
- runtimeImage: answer.trim(),
3173
- evidence: { ...draft.evidence, runtimeImage: ANSWERED_EVIDENCE }
3174
- })
3175
- },
3176
- "setup.job": {
3177
- key: "setup.job",
3178
- ask: "Which job should abloh measure in?",
3179
- why: "Everything else in the contract is read out of that one job.",
3180
- candidates: [],
3181
- required: true,
3182
- shape: { kind: "scalar" },
3183
- channel: "discovery"
3184
- },
3185
- "setup.matrixLeg": {
3186
- key: "setup.matrixLeg",
3187
- ask: "Which leg of that job's matrix?",
3188
- why: "The job abloh rides fans out, and a leg names one value per dimension of it.",
3189
- candidates: [],
3190
- required: true,
3191
- shape: { kind: "scalar" },
3192
- channel: "discovery"
3193
- },
3194
- "setup.failFast": {
3195
- key: "setup.failFast",
3196
- ask: "That job cancels every matrix leg when one fails. May abloh turn that off for it?",
3197
- /* THE VALUES THE CONTRACT ACCEPTS, on `flaky`'s own rule: the card writes the chosen candidate
3198
- into `abloh.yml`, so a candidate that read well and parsed badly would be a form that writes an
3199
- invalid file. What each one MEANS is the ask above it. */
3200
- why: SETUP_FAIL_FAST_ANSWERS.map((answer) => `${answer} ${SETUP_FAIL_FAST_CONSEQUENCES[answer]}.`).join(" "),
3201
- candidates: SETUP_FAIL_FAST_ANSWERS,
3202
- required: true,
3203
- shape: { kind: "scalar" },
3204
- /* A LINE IN THE FILE AND NOT A RE-DERIVATION. It does not move which job the recipe is read out
3205
- of - it is the maintainer's standing answer about the job already chosen - and
3206
- `configSetupJobLines` writes it on every road, with or without a job, because it is the one
3207
- statement in that block that was never abloh's to discard. */
3208
- channel: "contract",
3209
- apply: (draft, answer) => ({ ...draft, setupFailFast: answer.trim() })
3210
- },
3211
- flaky: {
3212
- key: "flaky",
3213
- ask: "Your suite was red on untouched code. What should abloh do about those tests?",
3214
- /* THE VALUES THE CONTRACT ACCEPTS, not a prose label: the card writes the chosen candidate into
3215
- `abloh.yml`, so a candidate that read well and parsed badly would be a form that writes an
3216
- invalid file. What each one MEANS is the ask above it. */
3217
- why: "quarantine sets them aside and measures the rest. strict measures nothing until they pass.",
3218
- candidates: ["quarantine", "strict"],
3219
- required: true,
3220
- shape: { kind: "scalar" },
3221
- channel: "contract",
3222
- /* `quarantine` WRITES NO LINE and that is not the answer being dropped: it is what a file with
3223
- no `flaky:` key already means, which `configFlakyLines` states at the renderer. */
3224
- apply: (draft, answer) => ({ ...draft, flaky: answer.trim() })
3225
- },
3226
- "environment.testCommand": {
3227
- key: "environment.testCommand",
3228
- ask: "Which command runs your tests?",
3229
- why: "Abloh read your CI without running it, and the command it found did not run a suite it could observe.",
3230
- candidates: [],
3231
- required: true,
3232
- shape: { kind: "scalar" },
3233
- channel: "contract",
3234
- apply: (draft, answer) => ({ ...draft, testCommand: answer.trim() })
3235
- },
3236
- "environment.sealedTestCommand": {
3237
- key: "environment.sealedTestCommand",
3238
- ask: "Which command runs your tests inside abloh's proof container?",
3239
- why: "Your own test script did something abloh's container could not, so it needs the runner on its own.",
3240
- candidates: [],
3241
- required: true,
3242
- shape: { kind: "scalar" },
3243
- channel: "contract",
3244
- apply: (draft, answer) => ({ ...draft, sealedTestCommand: answer.trim() })
3245
- },
3246
- "environment.rebuildCommand": {
3247
- key: "environment.rebuildCommand",
3248
- ask: "Which command builds the output your tests import?",
3249
- why: "Your tests read compiled output rather than the source abloh plants in, so without the build every planted bug is reported as missed. Abloh cannot resolve which of your build scripts writes it from your files alone.",
3250
- candidates: [],
3251
- required: true,
3252
- shape: { kind: "scalar" },
3253
- channel: "contract",
3254
- /* THE CANDIDATES AND THE MANAGER SURVIVE THE ANSWER, because they are the drafting's reading of
3255
- this repository and the answer is a value: a run that later clears the value would otherwise
3256
- lose the finding that made the key worth writing. Absent, they are the empty pair, which is
3257
- what a draft that found no build script already carries. */
3258
- apply: (draft, answer) => ({
3259
- ...draft,
3260
- rebuild: {
3261
- ...draft.rebuild ?? { candidates: [], manager: "" },
3262
- value: answer.trim(),
3263
- evidence: ANSWERED_EVIDENCE
3264
- }
3265
- })
3266
- },
3267
- "target.directory": {
3268
- key: "target.directory",
3269
- ask: "Which package should abloh measure?",
3270
- why: "This repository holds several, and a run measures one.",
3271
- candidates: [],
3272
- required: true,
3273
- shape: { kind: "scalar" },
3274
- /* A DISCOVERY ANSWER AND NOT A LINE. The runner, the test command and the whole measurement plan
3275
- are read out of the package being measured, so writing this key over a draft derived from the
3276
- root would leave every sibling value describing a different package. It goes back through
3277
- `resolveMeasurementPlan` as its `subdir`, exactly as the job pin goes back through the job. */
3278
- channel: "discovery"
3279
- },
3280
- "environment.coverageProvider": {
3281
- key: "environment.coverageProvider",
3282
- ask: "May abloh install the coverage provider your runner needs?",
3283
- why: "Your tree does not carry one, and without it abloh cannot say which of your changed lines your tests execute.",
3284
- candidates: ["install", "skip"],
3285
- required: true,
3286
- shape: { kind: "scalar" },
3287
- channel: "contract",
3288
- /* WRITTEN FOR BOTH ANSWERS, because a recorded answer is the difference between a maintainer
3289
- who chose the default and one who was never asked - see `ConfigInput.coverageProvider`. */
3290
- apply: (draft, answer) => ({ ...draft, coverageProvider: answer.trim() })
3291
- },
3292
- "environment.requiredVariables": {
3293
- key: "environment.requiredVariables",
3294
- ask: "Which environment variables does your suite need?",
3295
- why: "Names only. The values come from your own environment and are masked in every log.",
3296
- candidates: [],
3297
- required: true,
3298
- shape: { kind: "names" },
3299
- channel: "contract",
3300
- apply: (draft, answer) => ({
3301
- ...draft,
3302
- requiredVariables: splitAnswerList({ kind: "names" }, answer).map((name) => ({
3303
- name,
3304
- evidence: ANSWERED_EVIDENCE
3305
- }))
3306
- })
3307
- },
3308
- "environment.environmentValues": {
3309
- key: "environment.environmentValues",
3310
- ask: "Which literal values does your suite need in its environment?",
3311
- why: "A secret goes in requiredVariables by name instead, and never here.",
3312
- candidates: [],
3313
- required: true,
3314
- shape: { kind: "pairs" },
3315
- channel: "contract",
3316
- apply: (draft, answer) => ({
3317
- ...draft,
3318
- environmentValues: splitAnswerList({ kind: "pairs" }, answer).map((entry) => {
3319
- const [name, value] = splitAtFirstEquals(entry);
3320
- return { name, value: value ?? "", evidence: ANSWERED_EVIDENCE };
3321
- })
3322
- })
3323
- },
3324
- "environment.generatedFiles": {
3325
- key: "environment.generatedFiles",
3326
- ask: "Which directories does your own tooling write during a run?",
3327
- why: "An undeclared change to the checkout aborts a run, and abloh cannot tell yours from a stray one.",
3328
- candidates: [],
3329
- required: true,
3330
- shape: { kind: "list" },
3331
- channel: "contract",
3332
- apply: (draft, answer) => ({
3333
- ...draft,
3334
- generatedFiles: evidenceFor(splitAnswerList({ kind: "list" }, answer))
3335
- })
3336
- },
3337
- "environment.rewrittenFiles": {
3338
- key: "environment.rewrittenFiles",
3339
- ask: "Which committed files does your own install rewrite?",
3340
- why: "A rate measured over bytes your install changed is a rate about something else.",
3341
- candidates: [],
3342
- required: true,
3343
- shape: { kind: "list" },
3344
- channel: "contract",
3345
- apply: (draft, answer) => ({
3346
- ...draft,
3347
- rewrittenFiles: evidenceFor(splitAnswerList({ kind: "list" }, answer))
3348
- })
3349
- },
3350
- "environment.identityFiles": {
3351
- key: "environment.identityFiles",
3352
- ask: "Which files should a change to reinstall rather than reuse the container?",
3353
- why: "Abloh reuses a prepared container between runs unless one of these moved.",
3354
- candidates: [],
3355
- required: true,
3356
- shape: { kind: "list" },
3357
- channel: "contract",
3358
- apply: (draft, answer) => ({
3359
- ...draft,
3360
- identityFiles: splitAnswerList({ kind: "list" }, answer)
3361
- })
3362
- },
3363
- "environment.systemPackages": {
3364
- key: "environment.systemPackages",
3365
- ask: "Which OS packages does your build need?",
3366
- why: "The reviewed proof image is slim, so a native dependency that compiles on install needs its toolchain named.",
3367
- candidates: [],
3368
- required: true,
3369
- shape: { kind: "packages" },
3370
- channel: "contract",
3371
- apply: (draft, answer) => ({
3372
- ...draft,
3373
- systemPackages: splitAnswerList({ kind: "packages" }, answer).map((entry) => {
3374
- const [name, version] = splitAtFirstEquals(entry);
3375
- return { name, version, source: ANSWERED_EVIDENCE };
3376
- })
3377
- })
3378
- },
3379
- "environment.runtimes": {
3380
- key: "environment.runtimes",
3381
- ask: "Which runtimes beyond Node does your suite need?",
3382
- /* NO CLOSED CANDIDATE LIST, because this key takes SEVERAL: `setupAnswerProblem` compares a
3383
- closed list against the WHOLE answer, so `bun, deno` would be refused by a two-word list that
3384
- accepts each of them alone. `judgeConfigKeyValue` names the two the contract holds, entry by
3385
- entry, which is the reading that fits a list. */
3386
- why: "The proof image carries Node. A runtime your tests reach for and the image does not carry fails inside abloh rather than in your CI.",
3387
- candidates: [],
3388
- required: true,
3389
- shape: { kind: "names" },
3390
- channel: "contract",
3391
- apply: (draft, answer) => ({
3392
- ...draft,
3393
- runtimes: splitAnswerList({ kind: "names" }, answer).map((runtime) => ({
3394
- runtime,
3395
- evidence: ANSWERED_EVIDENCE
3396
- }))
3397
- })
3398
- },
3399
- "environment.services": {
3400
- key: "environment.services",
3401
- ask: "Which backing services does your suite need, and where is each one ready?",
3402
- why: "A proof runs in the same network namespace as its services, so your suite reaches them on localhost exactly as in your CI. Abloh pins the image by digest because a mutable tag cannot be part of what a run binds, and each service needs something to wait on before the suite starts.",
3403
- candidates: [],
3404
- required: true,
3405
- shape: { kind: "services" },
3406
- channel: "contract",
3407
- /* THE ENVIRONMENT IS NOT TYPED HERE and the entry says so at {@link SERVICE_ENTRY}: an image that
3408
- needs credentials fails its readiness gate and reports that, which is the same narrow door the
3409
- CLI's own typed entry goes through. */
3410
- apply: (draft, answer) => ({
3411
- ...draft,
3412
- services: readServiceEntries(answer).map((service) => ({
3413
- name: service.name,
3414
- spelling: `${service.name}=${service.image}`,
3415
- image: service.image,
3416
- env: [],
3417
- healthCommand: null,
3418
- healthPort: service.healthPort ?? null,
3419
- source: ANSWERED_EVIDENCE,
3420
- evidence: ANSWERED_EVIDENCE
3421
- }))
3422
- })
3423
- },
3424
- "environment.installDirectory": {
3425
- key: "environment.installDirectory",
3426
- ask: "Which directory owns the lockfile your install reads?",
3427
- why: "A workspace package installs from the root that holds its lockfile, not from its own folder.",
3428
- candidates: [],
3429
- required: false,
3430
- shape: { kind: "scalar" },
3431
- channel: "contract",
3432
- apply: (draft, answer) => ({ ...draft, installDirectory: answer.trim() })
3433
- },
3434
- "environment.runner": {
3435
- key: "environment.runner",
3436
- ask: "Which test runner does this repository use?",
3437
- why: "Abloh reads it from your own command, and a declaration outranks that reading.",
3438
- candidates: [],
3439
- required: false,
3440
- shape: { kind: "scalar" },
3441
- channel: "contract",
3442
- apply: (draft, answer) => ({
3443
- ...draft,
3444
- runner: answer.trim(),
3445
- boundRunner: answer.trim(),
3446
- evidence: { ...draft.evidence, runner: "declared in abloh.yml" }
3447
- })
3448
- }
3449
- };
3450
- var ANSWERABLE_SETUP_KEYS = Object.keys(SETUP_QUESTION_DESCRIPTORS);
3451
- function contractDescriptors() {
3452
- return Object.values(SETUP_QUESTION_DESCRIPTORS).filter(
3453
- (descriptor) => descriptor.channel === "contract"
3454
- );
3455
- }
3456
- function answeredTargetDirectory(answers) {
3457
- const value = answers["target.directory"];
3458
- return value === void 0 || value.trim() === "" ? void 0 : value.trim();
3459
- }
3460
-
3461
- // src/deno-project.ts
3462
- import { lstatSync as lstatSync2, readFileSync as readFileSync2 } from "fs";
3463
- import { join as join4 } from "path";
3464
- import { parseJsonc } from "@abloh/core";
3465
- var DENO_CONFIG_FILES = ["deno.json", "deno.jsonc"];
3466
- var DENO_LOCKFILE = "deno.lock";
3467
- function stageInvokesDenoTest(stage) {
3468
- const tokens = stage.trim().split(/\s+/u).filter((token) => token !== "");
3469
- const first = (tokens[0] ?? "").replace(/^.*\//u, "");
3470
- return first === "deno" && /^deno\s+test(?:\s|$)/u.test(stage.trim());
3471
- }
3472
- function denoTestInvocation(taskBody) {
3473
- if (taskBody === null) return null;
3474
- const script = taskBody.trim();
3475
- if (script === "") return null;
3476
- if (/[;|<>`$()\n]/u.test(script)) return null;
3477
- if (/(^|[^&])&([^&]|$)/u.test(script)) return null;
3478
- const stages = script.split("&&").map((stage) => stage.trim());
3479
- if (stages.some((stage) => stage === "")) return null;
3480
- const matches = stages.filter(stageInvokesDenoTest);
3481
- return matches.length === 1 ? matches[0] : null;
3482
- }
3483
- function readDenoProject(dir, readText = diskReadText) {
3484
- for (const name of DENO_CONFIG_FILES) {
3485
- const path = join4(dir, name);
3486
- const text = readText(path);
3487
- if (text === null) continue;
3488
- const parsed = parseJsonc(text);
3489
- const tasks = parsed?.tasks;
3490
- const declared = tasks !== null && typeof tasks === "object" ? tasks["test"] : void 0;
3491
- const body = typeof declared === "string" ? declared : declared !== null && typeof declared === "object" && typeof declared.command === "string" ? declared.command : null;
3492
- if (body === null || body.trim() === "") return null;
3493
- return { configFile: name, testTask: body, testInvocation: denoTestInvocation(body) };
3494
- }
3495
- return null;
3496
- }
3497
- function denoLockfilePath(dir, readText = diskReadText) {
3498
- return readText(join4(dir, DENO_LOCKFILE)) === null ? null : DENO_LOCKFILE;
3499
- }
3500
- function scriptRunsDenoTest(testScript) {
3501
- if (testScript === null) return false;
3502
- return testScript.split(/&&|\|\||[;&|]/u).map((stage) => stage.trim()).some(stageInvokesDenoTest);
3503
- }
3504
- function diskReadText(absolutePath) {
3505
- try {
3506
- const info = lstatSync2(absolutePath);
3507
- if (!info.isFile() || info.isSymbolicLink()) return null;
3508
- return readFileSync2(absolutePath, "utf8");
3509
- } catch {
3510
- return null;
3511
- }
3512
- }
3513
-
3514
- // src/detect.ts
3515
- import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
3516
- import { dirname as dirname4, isAbsolute as isAbsolute4, join as join8, relative as relative5, resolve as resolve6, sep as sep4 } from "path";
3517
- import { SCRIPT_NAMED_RUNNERS as SCRIPT_NAMED_RUNNERS3, angularRunnerName, commandTokens, compiledOutputEvidence, debugPreparationOverflowStack, readsCompiledOutput, detectModuleLayout, directRunnerCommand, localBinaryPrefix, manifestScripts as manifestScripts3, parseLiteralArguments as parseLiteralArguments2, parsePreparedTestCommand, planCommand as planCommand3, nextActionSentence, readPackageManagerDeclaration, readPackageManagerPin, resolveCommandPlan, runnerNamesInScript, scriptRuns as scriptRuns2, selectAngularTestTarget, nameDrivenRunners, summaryThenAction, watchModeRefusal, withWatchModeDisabled, withoutWatchDisableFlags } from "@abloh/core";
3518
-
3519
- // src/rush-workspace.ts
3520
- import { parseJsonc as parseJsonc2 } from "@abloh/core";
3521
- var RUSH_CONFIG_FILE = "rush.json";
3522
- var RUSH_TEST_SCRIPTS = ["test", "_phase:test"];
3523
- var RUSH_CONFIG_DIRECTORY = "common/config/rush";
3524
- var RUSH_SUBSPACE_DIRECTORY = "common/config/subspaces";
3525
- var RUSH_LOCKFILE_NAME = "pnpm-lock.yaml";
3526
- var RUSH_REPO_STATE_NAME = "repo-state.json";
3527
- var RUSH_INSTALL_COMMAND = "node common/scripts/install-run-rush.js install";
3528
- var RUSH_INSTALL_SCRIPT = "common/scripts/install-run-rush.js";
3529
- function classifyRushTestScript(manifest, directory) {
3530
- const table = manifestScripts2(manifest);
3531
- for (const script of RUSH_TEST_SCRIPTS) {
3532
- const body = table[script];
3533
- if (typeof body !== "string" || body.trim() === "") continue;
3534
- if (/[;&|<>`\n]|\$\(/u.test(body)) {
3535
- return {
3536
- state: "unmeasurable",
3537
- reason: `the Rush project at ${directory} composes shell commands in its ${script} script rather than running one suite`,
3538
- remedy: `reduce ${directory}'s ${script} script to one command, or declare a plain "test" script that runs one suite`
3539
- };
3540
- }
3541
- return { state: "command", script, command: body.trim() };
3542
- }
3543
- return {
3544
- state: "unmeasurable",
3545
- reason: `the Rush project at ${directory} declares neither a "test" nor a "_phase:test" script`,
3546
- remedy: `give ${directory} a "test" or "_phase:test" script in its package.json that runs its tests`
3547
- };
3548
- }
3549
- function manifestScripts2(manifest) {
3550
- const scripts = manifest?.scripts;
3551
- return scripts !== null && typeof scripts === "object" ? scripts : {};
3552
- }
3553
- function enumerateRushProjects(readFile, limit) {
3554
- const parsed = parseJsonc2(readFile(RUSH_CONFIG_FILE));
3555
- const declared = parsed?.projects;
3556
- if (!Array.isArray(declared)) return [];
3557
- const projects = [];
3558
- for (const entry of declared) {
3559
- if (entry === null || typeof entry !== "object") continue;
3560
- const name = entry.packageName;
3561
- const folder = entry.projectFolder;
3562
- if (typeof name !== "string" || typeof folder !== "string") continue;
3563
- const directory = normalizeProjectFolder(folder);
3564
- if (directory === null) continue;
3565
- const manifestPath = `${directory}/package.json`;
3566
- const manifestText = readFile(manifestPath);
3567
- if (manifestText === null) {
3568
- projects.push({
3569
- directory,
3570
- name,
3571
- test: {
3572
- state: "unmeasurable",
3573
- // Enumerated even so: the directory must not be silently absorbed into a parent's file
3574
- // list, which would attribute this project's changed files to another project.
3575
- reason: `the Rush project ${name} declares the folder ${directory}, which has no readable package.json`,
3576
- remedy: `add ${manifestPath}, or remove ${name} from rush.json's projects`
3577
- }
3578
- });
3579
- continue;
3580
- }
3581
- const manifest = parseJsonc2(manifestText);
3582
- projects.push({
3583
- directory,
3584
- name,
3585
- test: manifest === null ? {
3586
- state: "unmeasurable",
3587
- reason: `the Rush project ${name}'s manifest at ${manifestPath} is not readable JSON`,
3588
- remedy: `fix ${manifestPath} so its manifest parses`
3589
- } : classifyRushTestScript(manifest, directory)
3590
- });
3591
- if (projects.length >= limit) break;
3592
- }
3593
- return projects.sort((a, b) => a.directory < b.directory ? -1 : a.directory > b.directory ? 1 : 0);
3594
- }
3595
- function normalizeProjectFolder(raw) {
3596
- const path = raw.trim().replace(/\\/gu, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
3597
- if (path === "" || path.startsWith("/") || /^[A-Za-z]:/u.test(path)) return null;
3598
- if (/[;&|<>`\s'"$]|\$\(/u.test(path)) return null;
3599
- if (path.split("/").some((segment) => segment === "..")) return null;
3600
- return path;
3601
- }
3602
- function rushIdentityFiles(input) {
3603
- const files = [RUSH_CONFIG_FILE];
3604
- const collect = (directory) => {
3605
- for (const name of [RUSH_LOCKFILE_NAME, RUSH_REPO_STATE_NAME]) {
3606
- const path = `${directory}/${name}`;
3607
- if (input.exists(path)) files.push(path);
3608
- }
3609
- };
3610
- collect(RUSH_CONFIG_DIRECTORY);
3611
- for (const subspace of [...input.subdirectories(RUSH_SUBSPACE_DIRECTORY)].sort()) {
3612
- collect(`${RUSH_SUBSPACE_DIRECTORY}/${subspace}`);
3613
- }
3614
- return files;
3615
- }
3616
- function rushLockfiles(input) {
3617
- return rushIdentityFiles(input).filter((path) => path.endsWith(`/${RUSH_LOCKFILE_NAME}`));
3618
- }
3619
- function rushNodeVersionRange(rushJsonText) {
3620
- const parsed = parseJsonc2(rushJsonText);
3621
- const range = parsed?.nodeSupportedVersionRange;
3622
- return typeof range === "string" && range.trim() !== "" ? range.trim() : null;
3623
- }
3624
-
3625
- // src/workspace-aggregator.ts
3626
- import { SCRIPT_NAMED_RUNNERS as SCRIPT_NAMED_RUNNERS2, binaryName, planCommand as planCommand2 } from "@abloh/core";
3627
- function invokedProgram(command) {
3628
- const plan = planCommand2(command);
3629
- if (plan.kind === "program") return { program: plan.program, args: plan.args };
3630
- if (plan.kind === "manager-passthrough" && plan.token !== null) {
3631
- return { program: binaryName(plan.token), args: plan.args };
3632
- }
3633
- return null;
3634
- }
3635
- var TOOLS = ["turbo", "lage", "nx"];
3636
- function nxTargets(args) {
3637
- const targets = [];
3638
- for (let at = 0; at < args.length; at += 1) {
3639
- const arg = args[at];
3640
- const inline = /^(?:-t|--targets?)=(.+)$/u.exec(arg);
3641
- if (inline !== null) {
3642
- targets.push(...inline[1].split(","));
3643
- continue;
3644
- }
3645
- if (/^(?:-t|--targets?)$/u.test(arg)) {
3646
- const value = args[at + 1];
3647
- if (value !== void 0 && !value.startsWith("-")) {
3648
- targets.push(...value.split(","));
3649
- at += 1;
3650
- }
3651
- }
3652
- }
3653
- return targets.filter((task) => task !== "");
3654
- }
3655
- function aggregatorInvocation(testScript) {
3656
- if (testScript === null) return null;
3657
- const script = testScript.trim();
3658
- if (script === "" || /[;&|<>`\n]|\$\(/u.test(script)) return null;
3659
- const invoked = invokedProgram(script);
3660
- if (invoked === null) return null;
3661
- const tool = TOOLS.find((candidate) => candidate === invoked.program);
3662
- if (tool === void 0) return null;
3663
- const args = invoked.args;
3664
- if (tool === "nx") {
3665
- const explicit = nxTargets(args);
3666
- if (explicit.length > 0) return { tool, tasks: explicit };
3667
- const positional = args.filter((arg) => !arg.startsWith("-"));
3668
- const head = positional[0];
3669
- if (head === void 0) return { tool, tasks: [] };
3670
- if (head === "run-many" || head === "affected") return { tool, tasks: [] };
3671
- if (head === "run") {
3672
- const target = positional[1]?.split(":")[1];
3673
- return { tool, tasks: target === void 0 ? [] : [target] };
3674
- }
3675
- return { tool, tasks: [head] };
3676
- }
3677
- const leading = args.slice(0, args.findIndex((arg) => arg.startsWith("-")) === -1 ? args.length : args.findIndex((arg) => arg.startsWith("-")));
3678
- const tasks = leading[0] === "run" ? leading.slice(1) : leading;
3679
- return { tool, tasks };
3680
- }
3681
- function aggregatorTaskPhrase(invocation) {
3682
- if (invocation.tasks.length === 0) return "the tasks its own configuration selects";
3683
- const named = invocation.tasks.length < 3 ? invocation.tasks.join(" and ") : `${invocation.tasks.slice(0, -1).join(", ")} and ${invocation.tasks.at(-1)}`;
3684
- return `each package's own ${named} script${invocation.tasks.length === 1 ? "" : "s"}`;
3685
- }
3686
- var ROOT_CONFIG_EXECUTORS = /* @__PURE__ */ new Map([
3687
- // Every spelling read out of a real repository on 2026-08-16: `@nx/jest:jest` is what ngxs/store
3688
- // and nrwl/nx-labs declare, `@nx/vitest:test` is what analogjs/analog declares, and the `@nrwl/*`
3689
- // names are the pre-16 package scope the same executors shipped under.
3690
- ["@nx/jest:jest", { runner: "jest", configOption: "jestConfig" }],
3691
- ["@nrwl/jest:jest", { runner: "jest", configOption: "jestConfig" }],
3692
- ["@nx/vite:test", { runner: "vitest", configOption: "configFile" }],
3693
- ["@nx/vitest:test", { runner: "vitest", configOption: "configFile" }],
3694
- ["@nrwl/vite:test", { runner: "vitest", configOption: "configFile" }]
3695
- ]);
3696
- function workspaceRootCommand(runner, configFile, directory) {
3697
- return runner === "jest" ? `npx --no-install jest --config ${configFile}` : `npx --no-install vitest run --root ${directory} --config ${configFile}`;
3698
- }
3699
- var RUN_COMMANDS_EXECUTORS = /* @__PURE__ */ new Set(["nx:run-commands", "@nrwl/workspace:run-commands"]);
3700
- function commandRunner(command) {
3701
- const plan = planCommand2(command);
3702
- if (plan.kind === "node-test") return "node-test";
3703
- if (plan.kind === "runner") return plan.runner;
3704
- const invoked = invokedProgram(command);
3705
- if (invoked === null) return null;
3706
- return SCRIPT_NAMED_RUNNERS2.includes(invoked.program) ? invoked.program : null;
3707
- }
3708
- function normalizeWorkspacePath(raw) {
3709
- const path = raw.trim().replace(/\\/gu, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
3710
- if (path === "" || path.startsWith("/") || /^[A-Za-z]:/u.test(path)) return null;
3711
- if (/[;&|<>`\s'"$]|\$\(/u.test(path)) return null;
3712
- if (path.split("/").some((segment) => segment === "..")) return null;
3713
- return path;
3714
- }
3715
- function classifyNxTestTarget(projectJson, directory) {
3716
- const targets = projectJson?.targets ?? projectJson?.architect;
3717
- const target = targets?.["test"];
3718
- if (target === void 0) {
3719
- return {
3720
- state: "unmeasurable",
3721
- reason: `the Nx project at ${directory} declares no test target`,
3722
- remedy: `give ${directory} a test target in its project.json, or a "test" script in its package.json`
3723
- };
3724
- }
3725
- const executor = typeof target.executor === "string" ? target.executor : typeof target.builder === "string" ? target.builder : null;
3726
- if (executor === null) {
3727
- return {
3728
- state: "unmeasurable",
3729
- reason: `the Nx project at ${directory} declares a test target with no executor`,
3730
- remedy: `name an executor on ${directory}'s test target, or give the package a "test" script`
2944
+ state: "unmeasurable",
2945
+ reason: `the Nx project at ${directory} declares a test target with no executor`,
2946
+ remedy: `name an executor on ${directory}'s test target, or give the package a "test" script`
3731
2947
  };
3732
2948
  }
3733
2949
  const rootConfig = ROOT_CONFIG_EXECUTORS.get(executor);
@@ -3893,9 +3109,9 @@ import { InvariantError } from "@abloh/core";
3893
3109
  import { execFileSync } from "child_process";
3894
3110
  import { lstatSync as lstatSync3, mkdirSync, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync3, rmSync } from "fs";
3895
3111
  import { dirname as dirname3, isAbsolute as isAbsolute3, join as join6, normalize as normalize2, resolve as resolve5, sep as sep3 } from "path";
3896
- import { DeclaredRefusalError as DeclaredRefusalError2, mergeBaseNotEstablished, refusalFromFacts as refusalFromFacts2 } from "@abloh/core";
3112
+ import { DeclaredRefusalError, mergeBaseNotEstablished, refusalFromFacts } from "@abloh/core";
3897
3113
  var refuseSubmodule = (path, observed) => {
3898
- throw new DeclaredRefusalError2(refusalFromFacts2({ kind: "submodule-admission", path, observed }));
3114
+ throw new DeclaredRefusalError(refusalFromFacts({ kind: "submodule-admission", path, observed }));
3899
3115
  };
3900
3116
  var GITLINK_MODE = "160000";
3901
3117
  function git(repoDir, args) {
@@ -3934,8 +3150,8 @@ function assertNoGitlinkChanges(repoDir, base, head, uncommitted) {
3934
3150
  const newMode = fields[1];
3935
3151
  if (oldMode === GITLINK_MODE || newMode === GITLINK_MODE) offenders.push(path);
3936
3152
  }
3937
- const refusal = refusalFromFacts2({ kind: "submodule-change", offenders });
3938
- if (refusal !== null) throw new DeclaredRefusalError2(refusal);
3153
+ const refusal = refusalFromFacts({ kind: "submodule-change", offenders });
3154
+ if (refusal !== null) throw new DeclaredRefusalError(refusal);
3939
3155
  }
3940
3156
  function gitAllowFail(repoDir, args) {
3941
3157
  try {
@@ -5177,7 +4393,7 @@ function splitAtRunnerStageInWrapper(testScript, runner, scripts) {
5177
4393
  if (/[;&|<>`]|\$\(/u.test(script)) return null;
5178
4394
  let argv;
5179
4395
  try {
5180
- argv = parseLiteralArguments2(script, "npm-run-all test script");
4396
+ argv = parseLiteralArguments(script, "npm-run-all test script");
5181
4397
  } catch {
5182
4398
  return null;
5183
4399
  }
@@ -5208,7 +4424,7 @@ function splitAtRunnerStageInWrapper(testScript, runner, scripts) {
5208
4424
  if (leaf === void 0 || leaf.trim() === "") return null;
5209
4425
  let leafArgv;
5210
4426
  try {
5211
- leafArgv = parseLiteralArguments2(leaf, `wrapper child ${name}`);
4427
+ leafArgv = parseLiteralArguments(leaf, `wrapper child ${name}`);
5212
4428
  } catch {
5213
4429
  leafArgv = [];
5214
4430
  }
@@ -5301,7 +4517,7 @@ function jasmineSuiteFromScript(testScript) {
5301
4517
  for (const stage of stages) {
5302
4518
  let tokens;
5303
4519
  try {
5304
- tokens = parseLiteralArguments2(stage, "test script");
4520
+ tokens = parseLiteralArguments(stage, "test script");
5305
4521
  } catch {
5306
4522
  return void 0;
5307
4523
  }
@@ -5406,6 +4622,7 @@ function classifyPackageManifest(pkg, aggregatorTasks = []) {
5406
4622
  ...adopted,
5407
4623
  scripts,
5408
4624
  aggregatorTasks: [],
4625
+ aggregatorInvocationScript: null,
5409
4626
  aggregatorTask: taskSuite.task,
5410
4627
  aggregatorTaskRefusal: null
5411
4628
  };
@@ -5424,6 +4641,7 @@ function classifyPackageManifest(pkg, aggregatorTasks = []) {
5424
4641
  scripts,
5425
4642
  aggregator: aggregated?.tool ?? null,
5426
4643
  aggregatorTasks: aggregated?.tasks ?? [],
4644
+ aggregatorInvocationScript: aggregated === null ? null : testScript,
5427
4645
  aggregatorTask: null,
5428
4646
  aggregatorTaskRefusal: taskSuite === null ? null : taskSuite.refusal
5429
4647
  };
@@ -5446,7 +4664,12 @@ function classifyWithAliasHop(manifest, family, aggregatorTasks = []) {
5446
4664
  if (typeof inner !== "string") return base;
5447
4665
  const hopped = classifyPackageManifest({ ...manifest, scripts: { ...base.scripts, test: inner } });
5448
4666
  if (hopped.runner !== null) return hopped;
5449
- return hopped.aggregator === null ? base : { ...base, aggregator: hopped.aggregator, aggregatorTasks: hopped.aggregatorTasks };
4667
+ return hopped.aggregator === null ? base : {
4668
+ ...base,
4669
+ aggregator: hopped.aggregator,
4670
+ aggregatorTasks: hopped.aggregatorTasks,
4671
+ aggregatorInvocationScript: hopped.aggregatorInvocationScript
4672
+ };
5450
4673
  }
5451
4674
  function probeNxProject(ctx, repoDir, dir) {
5452
4675
  const text = accessText(ctx, join8(dir, "project.json"));
@@ -5542,658 +4765,1658 @@ function probePkg(ctx, dir, aliasFamily = null, repoDir = dir, aggregatorTasks =
5542
4765
  refusal: null
5543
4766
  };
5544
4767
  }
5545
- function probeManifest(ctx, dir, manifest, aliasFamily, repoDir, aggregatorTasks) {
5546
- if (manifest === null) {
5547
- const nx = probeNxProject(ctx, repoDir, dir);
5548
- if (nx !== null) {
5549
- return {
5550
- dir,
5551
- runner: nx.runner,
5552
- testScript: null,
5553
- note: null,
5554
- scripts: {},
5555
- aggregator: null,
5556
- aggregatorTasks: [],
5557
- aggregatorTask: null,
5558
- angular: null,
5559
- declaredCommand: nx.command,
5560
- workspaceRootSuite: nx.workspaceRootSuite
5561
- };
4768
+ function probeManifest(ctx, dir, manifest, aliasFamily, repoDir, aggregatorTasks) {
4769
+ if (manifest === null) {
4770
+ const nx = probeNxProject(ctx, repoDir, dir);
4771
+ if (nx !== null) {
4772
+ return {
4773
+ dir,
4774
+ runner: nx.runner,
4775
+ testScript: null,
4776
+ note: null,
4777
+ scripts: {},
4778
+ aggregator: null,
4779
+ aggregatorTasks: [],
4780
+ aggregatorTask: null,
4781
+ angular: null,
4782
+ declaredCommand: nx.command,
4783
+ workspaceRootSuite: nx.workspaceRootSuite
4784
+ };
4785
+ }
4786
+ return probeDenoProject(ctx, dir);
4787
+ }
4788
+ const pkg = manifest;
4789
+ const classified = classifyWithAliasHop(pkg, aliasFamily, aggregatorTasks);
4790
+ if (classified.aggregatorTask !== null && classified.testScript !== null) {
4791
+ return {
4792
+ dir,
4793
+ ...classified,
4794
+ angular: null,
4795
+ declaredCommand: aggregatorTaskCommand(
4796
+ classified.testScript,
4797
+ classified.runner,
4798
+ detectModuleLayout(repoDir, dir)
4799
+ ),
4800
+ workspaceRootSuite: null
4801
+ };
4802
+ }
4803
+ if (classified.runner === null && scriptRunsDenoTest(classified.testScript)) {
4804
+ return {
4805
+ dir,
4806
+ ...classified,
4807
+ runner: "deno",
4808
+ note: "deno",
4809
+ angular: null,
4810
+ declaredCommand: runnerInvocation(classified.testScript, "deno"),
4811
+ workspaceRootSuite: null
4812
+ };
4813
+ }
4814
+ if (classified.runner === null && classified.testScript === null) {
4815
+ const nx = probeNxProject(ctx, repoDir, dir);
4816
+ if (nx !== null) {
4817
+ return {
4818
+ dir,
4819
+ ...classified,
4820
+ runner: nx.runner,
4821
+ angular: null,
4822
+ declaredCommand: nx.command,
4823
+ workspaceRootSuite: nx.workspaceRootSuite
4824
+ };
4825
+ }
4826
+ const rush = probeRushProject(ctx, repoDir, dir);
4827
+ if (rush !== null) return rush;
4828
+ const deno = probeDenoProject(ctx, dir);
4829
+ if (deno !== null) return deno;
4830
+ }
4831
+ const angular = probeAngular(ctx, dir, classified);
4832
+ if (angular !== null) {
4833
+ return {
4834
+ dir,
4835
+ ...classified,
4836
+ runner: angularRunnerName(angular.runner),
4837
+ angular,
4838
+ note: `angular:${angular.builder}`,
4839
+ declaredCommand: null,
4840
+ workspaceRootSuite: null
4841
+ };
4842
+ }
4843
+ return {
4844
+ dir,
4845
+ ...classified,
4846
+ angular: null,
4847
+ declaredCommand: null,
4848
+ workspaceRootSuite: null
4849
+ };
4850
+ }
4851
+ function classifyPackageDirectory(repoDir, dir, ctx = diskRepoAccess(repoDir)) {
4852
+ const reading = accessManifest(ctx, join8(repoDir, dir, "package.json"));
4853
+ if (reading.standing !== "read") return null;
4854
+ try {
4855
+ return classifyWithAliasHop(
4856
+ reading.manifest,
4857
+ rootAliasFamily(ctx, repoDir),
4858
+ rootAggregatorTasksInRepository(ctx, repoDir)
4859
+ );
4860
+ } catch {
4861
+ return null;
4862
+ }
4863
+ }
4864
+ function declaredCommandContext(repoDir, subdir, ctx = diskRepoAccess(repoDir)) {
4865
+ const packageReading = accessManifest(ctx, join8(repoDir, subdir ?? ".", "package.json"));
4866
+ const scripts = packageReading.standing === "read" ? manifestScripts3(packageReading.manifest) : {};
4867
+ const rootReading = accessManifest(ctx, join8(repoDir, "package.json"));
4868
+ const manifest = rootReading.standing === "read" ? rootReading.manifest : void 0;
4869
+ return { scripts, ...manifest === void 0 ? {} : { manifest } };
4870
+ }
4871
+ function rootAliasFamily(ctx, repoDir) {
4872
+ return withRootManifest(ctx, repoDir, aliasHopFamily, null);
4873
+ }
4874
+ function rootAggregatorTasksInRepository(ctx, repoDir) {
4875
+ return withRootManifest(ctx, repoDir, rootAggregatorTasks, []);
4876
+ }
4877
+ function withRootManifest(ctx, repoDir, read, absent) {
4878
+ const reading = accessManifest(ctx, join8(repoDir, "package.json"));
4879
+ if (reading.standing !== "read") return absent;
4880
+ try {
4881
+ return read(reading.manifest);
4882
+ } catch {
4883
+ return absent;
4884
+ }
4885
+ }
4886
+ var MAX_FANOUT_CHILDREN = 16;
4887
+ var NATIVE_COVERAGE_RUNNERS = /* @__PURE__ */ new Set([
4888
+ "jest",
4889
+ "vitest",
4890
+ "mocha",
4891
+ "node-test",
4892
+ "ava",
4893
+ "tap",
4894
+ "jasmine"
4895
+ ]);
4896
+ function corepackManaged(manager) {
4897
+ return manager.version !== null && (manager.pm === "npm" || manager.pm === "pnpm" || manager.pm === "yarn");
4898
+ }
4899
+ function packageScriptCommand(manager, script) {
4900
+ return manager.pm === "bun" ? `bun run ${script}` : manager.pm === "deno" ? `deno task ${script}` : corepackManaged(manager) ? `corepack ${manager.pm} run ${script}` : `${manager.pm} run ${script}`;
4901
+ }
4902
+ function packageTestCommand(manager) {
4903
+ return manager.pm === "bun" ? "bun run test" : manager.pm === "deno" ? "deno task test" : corepackManaged(manager) ? `corepack ${manager.pm} test` : manager.pm === "npm" ? "npm test" : `${manager.pm} test`;
4904
+ }
4905
+ function scriptPattern(pattern) {
4906
+ const escaped = pattern.replace(/[\\^$+.[\]{}()|]/gu, "\\$&");
4907
+ return new RegExp(`^${escaped.replace(/\*/gu, "[^:]*").replace(/\?/gu, "[^:]")}$`, "u");
4908
+ }
4909
+ var WRAPPERS = ["npm-run-all", "run-p", "run-s"];
4910
+ var WRAPPER_SCHEDULING_OPTIONS = /^(?:-[aclps]+|--parallel|--sequential|--continue-on-error|--silent|--aggregate-output|--print-label)$/u;
4911
+ function leafRunner(leaf) {
4912
+ const named = runnerNamesInScript(leaf);
4913
+ const runner = named.length === 1 ? named[0] : invokesNodeTest(leaf) ? "node-test" : null;
4914
+ if (runner === null || !NATIVE_COVERAGE_RUNNERS.has(runner)) return null;
4915
+ return runnerSuiteInvocations(leaf, runner) > 1 ? null : runner;
4916
+ }
4917
+ function wrapperFanout(probe, manager) {
4918
+ const script = probe.testScript;
4919
+ if (script === null) return void 0;
4920
+ const executable = script.trim().split(/\s+/u)[0]?.replace(/^.*\//u, "");
4921
+ if (!WRAPPERS.includes(executable)) return void 0;
4922
+ if (/[;&|<>`]|\$\(/u.test(script)) return { refusal: "it composes shell commands around the wrapper" };
4923
+ let argv;
4924
+ try {
4925
+ argv = parseLiteralArguments(script, "npm-run-all test script");
4926
+ } catch {
4927
+ return { refusal: "its wrapper invocation could not be parsed as one literal command" };
4928
+ }
4929
+ const patterns = [];
4930
+ let parallel = executable === "run-p";
4931
+ let sequential = executable === "run-s";
4932
+ for (const arg of argv.slice(1)) {
4933
+ if (arg.startsWith("-")) {
4934
+ if (!WRAPPER_SCHEDULING_OPTIONS.test(arg)) {
4935
+ return { refusal: `the wrapper option ${arg} can change which suites run to completion` };
4936
+ }
4937
+ if (arg === "--parallel" || /^-[aclps]+$/u.test(arg) && arg.includes("p")) parallel = true;
4938
+ if (arg === "--sequential" || /^-[aclps]+$/u.test(arg) && arg.includes("s")) sequential = true;
4939
+ continue;
4940
+ }
4941
+ patterns.push(arg);
4942
+ }
4943
+ if (patterns.length === 0) return { refusal: "its wrapper selects no scripts" };
4944
+ if (parallel && sequential) return { refusal: "its wrapper asks for parallel and sequential at once" };
4945
+ const selected = /* @__PURE__ */ new Set();
4946
+ const names = Object.keys(probe.scripts).sort();
4947
+ for (const pattern of patterns) {
4948
+ const matches = names.filter((name) => name !== "test" && scriptPattern(pattern).test(name));
4949
+ if (matches.length === 0) return { refusal: `no script matches the pattern ${pattern}` };
4950
+ for (const match of matches) selected.add(match);
4951
+ }
4952
+ const children = scriptChildren([...selected].sort(), probe, manager);
4953
+ return "refusal" in children ? children : { children: children.children, execution: parallel ? "parallel" : "sequential" };
4954
+ }
4955
+ function concurrentlyFanout(probe, manager) {
4956
+ const script = probe.testScript;
4957
+ if (script === null) return void 0;
4958
+ if (script.trim().split(/\s+/u)[0]?.replace(/^.*\//u, "") !== "concurrently") return void 0;
4959
+ if (/[;&|<>`]|\$\(/u.test(script)) return { refusal: "it composes shell commands around concurrently" };
4960
+ let argv;
4961
+ try {
4962
+ argv = parseLiteralArguments(script, "concurrently test script");
4963
+ } catch {
4964
+ return { refusal: "its concurrently invocation could not be parsed as one literal command" };
4965
+ }
4966
+ const patterns = [];
4967
+ for (const arg of argv.slice(1)) {
4968
+ if (arg.startsWith("-")) {
4969
+ if (!/^(?:--(?:names|prefix|prefix-colors|group|raw)|-[np]|-c)$/u.test(arg)) {
4970
+ return { refusal: `the concurrently option ${arg} can change which suites run to completion` };
4971
+ }
4972
+ continue;
4973
+ }
4974
+ if (!arg.startsWith("npm:")) {
4975
+ return { refusal: "concurrently is given a raw command line rather than an npm: script reference" };
4976
+ }
4977
+ patterns.push(arg.slice("npm:".length));
4978
+ }
4979
+ if (patterns.length === 0) return { refusal: "its concurrently invocation selects no scripts" };
4980
+ const selected = /* @__PURE__ */ new Set();
4981
+ const names = Object.keys(probe.scripts).sort();
4982
+ for (const pattern of patterns) {
4983
+ const matches = names.filter((name) => name !== "test" && scriptPattern(pattern).test(name));
4984
+ if (matches.length === 0) return { refusal: `no script matches the pattern ${pattern}` };
4985
+ for (const match of matches) selected.add(match);
4986
+ }
4987
+ const children = scriptChildren([...selected].sort(), probe, manager);
4988
+ return "refusal" in children ? children : { children: children.children, execution: "parallel" };
4989
+ }
4990
+ function scriptChildren(scripts, probe, manager) {
4991
+ if (scripts.length === 0) return { refusal: "it selects no scripts" };
4992
+ if (scripts.length > MAX_FANOUT_CHILDREN) {
4993
+ return { refusal: `it launches ${scripts.length} suites, more than the ${MAX_FANOUT_CHILDREN} this run will measure` };
4994
+ }
4995
+ const children = [];
4996
+ for (const script of scripts) {
4997
+ if (!/^[A-Za-z0-9:_-]{1,128}$/u.test(script)) return { refusal: `the script name ${script} is not a plain name` };
4998
+ const leaf = probe.scripts[script];
4999
+ if (leaf === void 0 || /[;&|<>`]|\$\(/u.test(leaf)) {
5000
+ return { refusal: `the script ${script} composes shell commands rather than running one suite` };
5001
+ }
5002
+ let leafArgv;
5003
+ try {
5004
+ leafArgv = parseLiteralArguments(leaf, `fanout child ${script}`);
5005
+ } catch {
5006
+ return { refusal: `the script ${script} could not be parsed as one literal command` };
5007
+ }
5008
+ if (leafArgv.some((arg) => WRAPPERS.includes(arg))) {
5009
+ return { refusal: `the script ${script} is itself a wrapper, and this expands one level only` };
5010
+ }
5011
+ const runner = leafRunner(leaf);
5012
+ if (runner === null) return { refusal: `the script ${script} names no single runner with native coverage` };
5013
+ children.push({ script, runner, testCommand: packageScriptCommand(manager, script) });
5014
+ }
5015
+ return { children };
5016
+ }
5017
+ function chainFanout(probe, manager) {
5018
+ const script = probe.testScript;
5019
+ if (script === null || script.trim() === "") return void 0;
5020
+ if (suiteStages(script, probe.scripts) < 2) return void 0;
5021
+ if (/[|<>`]|\$\(/u.test(script) || /(^|[^&])&([^&]|$)/u.test(script)) {
5022
+ return { refusal: "its suites are composed with something other than `&&`, so which of them ran is not decidable" };
5023
+ }
5024
+ if (script.includes(";")) {
5025
+ return { refusal: "its suites are separated by `;`, which runs the next one whether or not the last passed" };
5026
+ }
5027
+ const chainStages = script.split("&&").map((stage) => stage.trim());
5028
+ if (chainStages.some((stage) => stage === "")) return { refusal: "its `&&` chain has an empty stage" };
5029
+ if (chainStages.length > MAX_FANOUT_CHILDREN) {
5030
+ return { refusal: `it chains ${chainStages.length} stages, more than the ${MAX_FANOUT_CHILDREN} this run will measure` };
5031
+ }
5032
+ const children = [];
5033
+ for (const stage of chainStages) {
5034
+ if (suiteStages(stage, probe.scripts) === 0) continue;
5035
+ const alias = /^(?:corepack\s+)?(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?([A-Za-z0-9:._-]+)$/u.exec(stage);
5036
+ if (alias !== null) {
5037
+ const family = stage.trim().replace(/^corepack\s+/u, "").split(/\s+/u)[0];
5038
+ if (family !== manager.pm) {
5039
+ return { refusal: `the stage \`${stage}\` runs under ${family} while this package resolves to ${manager.pm}` };
5040
+ }
5041
+ const named = scriptChildren([alias[1]], probe, manager);
5042
+ if ("refusal" in named) return named;
5043
+ children.push(...named.children);
5044
+ continue;
5562
5045
  }
5563
- return probeDenoProject(ctx, dir);
5046
+ const runner = [...NATIVE_COVERAGE_RUNNERS].find((candidate) => stageInvokes(stage, candidate));
5047
+ if (runner === void 0) {
5048
+ return { refusal: `the stage \`${stage}\` is neither a suite nor a script this run can name` };
5049
+ }
5050
+ const testCommand = runner === "node-test" ? stage : `npx --no-install ${stage}`;
5051
+ try {
5052
+ parsePreparedTestCommand(testCommand, "fanout stage");
5053
+ } catch {
5054
+ return { refusal: `the stage \`${stage}\` is not a literal command this run can execute on its own` };
5055
+ }
5056
+ children.push({ runner, testCommand });
5564
5057
  }
5565
- const pkg = manifest;
5566
- const classified = classifyWithAliasHop(pkg, aliasFamily, aggregatorTasks);
5567
- if (classified.aggregatorTask !== null && classified.testScript !== null) {
5058
+ if (children.length < 2) return void 0;
5059
+ return { children, execution: "sequential" };
5060
+ }
5061
+ function suiteFanoutPlan(probe, manager) {
5062
+ const aggregated = probe.aggregator === null ? null : { tool: probe.aggregator, tasks: [...probe.aggregatorTasks] };
5063
+ if (aggregated !== null) {
5568
5064
  return {
5569
- dir,
5570
- ...classified,
5571
- angular: null,
5572
- declaredCommand: aggregatorTaskCommand(
5573
- classified.testScript,
5574
- classified.runner,
5575
- detectModuleLayout(repoDir, dir)
5576
- ),
5577
- workspaceRootSuite: null
5065
+ coverageExclusion: `this package's test script delegates to ${aggregated.tool}, which runs ${aggregatorTaskPhrase(aggregated)} rather than one suite, so no single coverage report describes it; abloh measures such a repository per package instead`
5578
5066
  };
5579
5067
  }
5580
- if (classified.runner === null && scriptRunsDenoTest(classified.testScript)) {
5068
+ const resolved = wrapperFanout(probe, manager) ?? concurrentlyFanout(probe, manager) ?? chainFanout(probe, manager);
5069
+ if (resolved === void 0) return {};
5070
+ if ("refusal" in resolved) {
5071
+ return { coverageExclusion: `this package's test script launches more than one suite, but ${resolved.refusal}` };
5072
+ }
5073
+ const runners = [...new Set(resolved.children.map((child) => child.runner))].sort();
5074
+ if (runners.length > 1) {
5581
5075
  return {
5582
- dir,
5583
- ...classified,
5584
- runner: "deno",
5585
- note: "deno",
5586
- angular: null,
5587
- declaredCommand: runnerInvocation(classified.testScript, "deno"),
5588
- workspaceRootSuite: null
5076
+ coverageExclusion: `this package's test script launches ${resolved.children.length} suites across ${runners.join(" and ")}, and one coverage report cannot carry two providers, so none of them is measured rather than some`
5589
5077
  };
5590
5078
  }
5591
- if (classified.runner === null && classified.testScript === null) {
5592
- const nx = probeNxProject(ctx, repoDir, dir);
5593
- if (nx !== null) {
5594
- return {
5595
- dir,
5596
- ...classified,
5597
- runner: nx.runner,
5598
- angular: null,
5599
- declaredCommand: nx.command,
5600
- workspaceRootSuite: nx.workspaceRootSuite
5601
- };
5079
+ return { coverageChildren: resolved.children, coverageExecution: resolved.execution };
5080
+ }
5081
+ function compiledOutputFields(input) {
5082
+ if (input.angular || input.runner === "deno") return {};
5083
+ const reading = readsCompiledOutput({
5084
+ /*
5085
+ * THE ONE READING ON THIS PATH THAT DOES NOT GO THROUGH THE REPOSITORY READER, and it is a
5086
+ * decision rather than an oversight.
5087
+ *
5088
+ * `readsCompiledOutput` FOLLOWS `extends` INTO `node_modules` by design - rushstack's projects
5089
+ * extend `@rushstack/heft-node-rig/profiles/default/tsconfig-base.json`, whose `outDir` is five
5090
+ * hops out of the store and back into the project's own `lib`, and refusing to follow it means
5091
+ * this reading never sees the repository the compile-then-test ruling was measured on. An
5092
+ * installed store is exactly where symlinks live and exactly where "inside the checkout" is a
5093
+ * different question, so the reader's two refusals - a symlinked leaf, and a parent that
5094
+ * resolves outside the root - would silently narrow it. What that costs is not a refusal: a
5095
+ * config this reading cannot see produces a different `rebuildCommand`, and a wrong rebuild is
5096
+ * the one failure in this product that ends in a confident WRONG NUMBER.
5097
+ *
5098
+ * IT IS SAFE HERE FOR THE REASON THE READER EXISTS ELSEWHERE. The paths are composed by
5099
+ * `compiled-output.ts` from a config's own `extends` and from `node_modules` walked upwards,
5100
+ * never from a repository-author-controlled string this module passes through; and the only
5101
+ * road that reaches it is one that HAS a checkout, since a service reading over the contents
5102
+ * API has no installed store to follow into anyway.
5103
+ */
5104
+ readFile: (path) => {
5105
+ const absolute = join8(input.repoDir, ...path.split("/"));
5106
+ if (!existsSync2(absolute)) return null;
5107
+ try {
5108
+ return readFileSync5(absolute, "utf8");
5109
+ } catch {
5110
+ return null;
5111
+ }
5112
+ },
5113
+ packageDir: input.packageDir ?? "",
5114
+ runner: input.runner,
5115
+ invocation: input.invocation,
5116
+ /* THE COMMAND THIS RUN EXECUTES, beside the invocation and never instead of it. `runnerInvocation`
5117
+ answers a narrower question - whether a stage can be RE-EMITTED - so it declines a stage with
5118
+ anything in front of the runner, and `devcontainers/cli`'s whole suite is
5119
+ `env TS_NODE_PROJECT=... mocha ... src/test/*.test.ts`. The reading in `@abloh/core` resolves the
5120
+ runner's stage out of this where no invocation arrived; its own header states both halves. */
5121
+ testCommand: input.testCommand,
5122
+ scripts: input.scripts,
5123
+ manager: input.manager,
5124
+ /*
5125
+ * THE SECOND READING'S SEAM IS THE REPOSITORY READER and never the raw filesystem one above.
5126
+ *
5127
+ * That one exists to follow a `tsconfig` `extends` into an installed store, which is the one
5128
+ * place this detection legitimately leaves the tree. This reading opens the SUITE'S OWN TEST
5129
+ * FILES, which are the repository's, so it takes the reader whose refusals are about staying
5130
+ * inside it - a symlinked leaf and a parent resolving out of the root.
5131
+ */
5132
+ reader: input.ctx.reader
5133
+ });
5134
+ const found = reading.state === "reads-compiled-output" ? reading : null;
5135
+ return {
5136
+ rebuildCommand: found?.rebuildCommand ?? void 0,
5137
+ rebuildEvidence: found === null ? void 0 : compiledOutputEvidence(found),
5138
+ rebuildCandidates: found === null || found.candidates.length === 0 ? void 0 : found.candidates,
5139
+ rebuildOutputDirectory: found?.outputDirectory ?? void 0
5140
+ };
5141
+ }
5142
+ function repositoryRootOf(detected) {
5143
+ if (detected.subdir === null || detected.workspaceRootSuite !== void 0) return detected.workDir;
5144
+ return resolve6(detected.workDir, ...detected.subdir.split("/").map(() => ".."));
5145
+ }
5146
+ var VITEST_CONFIG_FILES = [
5147
+ "vitest.config.ts",
5148
+ "vitest.config.mts",
5149
+ "vitest.config.cts",
5150
+ "vitest.config.js",
5151
+ "vitest.config.mjs",
5152
+ "vitest.config.cjs",
5153
+ "vite.config.ts",
5154
+ "vite.config.mts",
5155
+ "vite.config.cts",
5156
+ "vite.config.js",
5157
+ "vite.config.mjs",
5158
+ "vite.config.cjs"
5159
+ ];
5160
+ function detectVitestDir(dir, ctx = diskRepoAccess(dir)) {
5161
+ for (const name of VITEST_CONFIG_FILES) {
5162
+ const p = join8(dir, name);
5163
+ let src;
5164
+ try {
5165
+ src = accessText(ctx, p);
5166
+ } catch (e) {
5167
+ debugPreparationOverflowStack(e);
5168
+ throw new TargetDetectionError(
5169
+ `${name} at ${dir} could not be read: ${e instanceof Error ? e.message : "unreadable"}`
5170
+ );
5602
5171
  }
5603
- const rush = probeRushProject(ctx, repoDir, dir);
5604
- if (rush !== null) return rush;
5605
- const deno = probeDenoProject(ctx, dir);
5606
- if (deno !== null) return deno;
5172
+ if (src === null) continue;
5173
+ return vitestDirIn(src);
5607
5174
  }
5608
- const angular = probeAngular(ctx, dir, classified);
5609
- if (angular !== null) {
5610
- return {
5611
- dir,
5612
- ...classified,
5613
- runner: angularRunnerName(angular.runner),
5614
- angular,
5615
- note: `angular:${angular.builder}`,
5616
- declaredCommand: null,
5617
- workspaceRootSuite: null
5618
- };
5175
+ return void 0;
5176
+ }
5177
+ function vitestDirIn(source) {
5178
+ const masked = maskLiterals(source);
5179
+ const open = firstUnmasked(source, masked, /(?:^|[\s,;{])["']?test["']?\s*:\s*\{/gu);
5180
+ if (open === -1) return void 0;
5181
+ let depth = 0;
5182
+ let close = -1;
5183
+ for (let at = open; at < masked.length; at++) {
5184
+ if (masked[at] === "{") depth += 1;
5185
+ else if (masked[at] === "}") {
5186
+ depth -= 1;
5187
+ if (depth === 0) {
5188
+ close = at;
5189
+ break;
5190
+ }
5191
+ }
5192
+ }
5193
+ if (close === -1) return void 0;
5194
+ const quote = firstUnmasked(source.slice(0, close), masked.slice(0, close), /\bdir["']?\s*:\s*['"`]/gu, open);
5195
+ if (quote === -1) return void 0;
5196
+ const end = masked.indexOf(source[quote], quote + 1);
5197
+ if (end === -1 || end >= close) return void 0;
5198
+ const value = source.slice(quote + 1, end);
5199
+ return value === "" ? void 0 : value;
5200
+ }
5201
+ function firstUnmasked(source, masked, pattern, from = 0) {
5202
+ pattern.lastIndex = from;
5203
+ for (let match = pattern.exec(source); match !== null; match = pattern.exec(source)) {
5204
+ const at = match.index + match[0].length - 1;
5205
+ if (masked[at] === source[at]) return at;
5619
5206
  }
5620
- return {
5621
- dir,
5622
- ...classified,
5623
- angular: null,
5624
- declaredCommand: null,
5625
- workspaceRootSuite: null
5626
- };
5207
+ return -1;
5627
5208
  }
5628
- function classifyPackageDirectory(repoDir, dir, ctx = diskRepoAccess(repoDir)) {
5629
- const reading = accessManifest(ctx, join8(repoDir, dir, "package.json"));
5630
- if (reading.standing !== "read") return null;
5631
- try {
5632
- return classifyWithAliasHop(
5633
- reading.manifest,
5634
- rootAliasFamily(ctx, repoDir),
5635
- rootAggregatorTasksInRepository(ctx, repoDir)
5636
- );
5637
- } catch {
5638
- return null;
5209
+ function maskLiterals(source) {
5210
+ const out = source.split("");
5211
+ const blank = (from, to) => {
5212
+ for (let at2 = from; at2 < to && at2 < out.length; at2++) if (out[at2] !== "\n") out[at2] = " ";
5213
+ };
5214
+ let at = 0;
5215
+ let previous = "";
5216
+ while (at < source.length) {
5217
+ const char = source[at];
5218
+ if (char === '"' || char === "'" || char === "`") {
5219
+ let end = at + 1;
5220
+ while (end < source.length && source[end] !== char) end += source[end] === "\\" ? 2 : 1;
5221
+ blank(at + 1, end);
5222
+ at = Math.min(end + 1, source.length);
5223
+ previous = char;
5224
+ continue;
5225
+ }
5226
+ if (char === "/" && source[at + 1] === "/") {
5227
+ const end = source.indexOf("\n", at);
5228
+ blank(at, end === -1 ? source.length : end);
5229
+ at = end === -1 ? source.length : end;
5230
+ continue;
5231
+ }
5232
+ if (char === "/" && source[at + 1] === "*") {
5233
+ const end = source.indexOf("*/", at + 2);
5234
+ blank(at, end === -1 ? source.length : end + 2);
5235
+ at = end === -1 ? source.length : end + 2;
5236
+ continue;
5237
+ }
5238
+ if (char === "/" && previous !== "" && /[(,=:[!&|?{};+\-*]/u.test(previous)) {
5239
+ let end = at + 1;
5240
+ while (end < source.length && source[end] !== "/" && source[end] !== "\n") end += source[end] === "\\" ? 2 : 1;
5241
+ if (source[end] === "/") {
5242
+ blank(at, end + 1);
5243
+ at = end + 1;
5244
+ continue;
5245
+ }
5246
+ }
5247
+ if (!/\s/u.test(char)) previous = char;
5248
+ at += 1;
5639
5249
  }
5250
+ return out.join("");
5640
5251
  }
5641
- function declaredCommandContext(repoDir, subdir, ctx = diskRepoAccess(repoDir)) {
5642
- const packageReading = accessManifest(ctx, join8(repoDir, subdir ?? ".", "package.json"));
5643
- const scripts = packageReading.standing === "read" ? manifestScripts3(packageReading.manifest) : {};
5644
- const rootReading = accessManifest(ctx, join8(repoDir, "package.json"));
5645
- const manifest = rootReading.standing === "read" ? rootReading.manifest : void 0;
5646
- return { scripts, ...manifest === void 0 ? {} : { manifest } };
5252
+ function workDirPrefix(detection) {
5253
+ return detection.workspaceRootSuite === void 0 ? detection.subdir : null;
5647
5254
  }
5648
- function rootAliasFamily(ctx, repoDir) {
5649
- return withRootManifest(ctx, repoDir, aliasHopFamily, null);
5255
+ function classicPerTestRunner(runner) {
5256
+ return runner !== "command" && classicPerTestRefusal(runner) === null;
5650
5257
  }
5651
- function rootAggregatorTasksInRepository(ctx, repoDir) {
5652
- return withRootManifest(ctx, repoDir, rootAggregatorTasks, []);
5258
+ function classicPerTestRefusal(runner) {
5259
+ return runner === "deno" ? "deno's mutation runs the whole suite per mutant (Stryker ships no deno runner), so the classic per-test layer has no per-test report to read; the sealed v2 engine drives deno one test at a time and is where a proposed test for this repository comes from" : null;
5653
5260
  }
5654
- function withRootManifest(ctx, repoDir, read, absent) {
5655
- const reading = accessManifest(ctx, join8(repoDir, "package.json"));
5656
- if (reading.standing !== "read") return absent;
5261
+ function declaresWorkspace(ctx, repoDir) {
5262
+ for (const name of ["pnpm-workspace.yaml", "pnpm-workspace.yml", "nx.json", "lerna.json"]) {
5263
+ if (accessExists(ctx, join8(repoDir, name))) return true;
5264
+ }
5657
5265
  try {
5658
- return read(reading.manifest);
5266
+ const reading = accessManifest(ctx, join8(repoDir, "package.json"));
5267
+ if (reading.standing !== "read") return false;
5268
+ const workspaces = reading.manifest.workspaces;
5269
+ if (Array.isArray(workspaces)) return workspaces.length > 0;
5270
+ if (workspaces !== null && typeof workspaces === "object") {
5271
+ return Array.isArray(workspaces.packages);
5272
+ }
5659
5273
  } catch {
5660
- return absent;
5274
+ return false;
5661
5275
  }
5276
+ return false;
5662
5277
  }
5663
- var MAX_FANOUT_CHILDREN = 16;
5664
- var NATIVE_COVERAGE_RUNNERS = /* @__PURE__ */ new Set([
5665
- "jest",
5666
- "vitest",
5667
- "mocha",
5668
- "node-test",
5669
- "ava",
5670
- "tap",
5671
- "jasmine"
5672
- ]);
5673
- function corepackManaged(manager) {
5674
- return manager.version !== null && (manager.pm === "npm" || manager.pm === "pnpm" || manager.pm === "yarn");
5278
+ var NAMED_CANDIDATES = 8;
5279
+ function testableWorkspacePackages(repoDir, refusals, ctx = diskRepoAccess(repoDir)) {
5280
+ const aliasFamily = rootAliasFamily(ctx, repoDir);
5281
+ const aggregatorTasks = rootAggregatorTasksInRepository(ctx, repoDir);
5282
+ const found = [];
5283
+ const enumeration = enumerateWorkspacePackages(repoDir);
5284
+ if (refusals !== void 0) refusals.push(...enumeration.refusals);
5285
+ for (const dir of enumeration.packages) {
5286
+ const { probe, refusal } = probePkg(ctx, join8(repoDir, dir), aliasFamily, repoDir, aggregatorTasks);
5287
+ if (refusal !== null) {
5288
+ if (refusals !== void 0) refusals.push(refusal.summary);
5289
+ continue;
5290
+ }
5291
+ if (probe === null) continue;
5292
+ if (probe.runner !== null || declaresASuite(probe)) found.push(dir);
5293
+ }
5294
+ return found;
5675
5295
  }
5676
- function packageScriptCommand(manager, script) {
5677
- return manager.pm === "bun" ? `bun run ${script}` : manager.pm === "deno" ? `deno task ${script}` : corepackManaged(manager) ? `corepack ${manager.pm} run ${script}` : `${manager.pm} run ${script}`;
5296
+ function declaresASuite(probe) {
5297
+ if (probe.testScript === null) return false;
5298
+ const runs = scriptRuns2({ scripts: probe.scripts, text: probe.testScript });
5299
+ if (runs.length === 0) return true;
5300
+ return runs.some((run) => run.kind === "test" || run.kind === "unknown");
5678
5301
  }
5679
- function packageTestCommand(manager) {
5680
- return manager.pm === "bun" ? "bun run test" : manager.pm === "deno" ? "deno task test" : corepackManaged(manager) ? `corepack ${manager.pm} test` : manager.pm === "npm" ? "npm test" : `${manager.pm} test`;
5302
+ function candidateRemedy(repoDir) {
5303
+ const refusals = [];
5304
+ const candidates = testableWorkspacePackages(repoDir, refusals);
5305
+ if (candidates.length === 0) {
5306
+ return refusals.length > 0 ? `${refusals[0]}. Declare environment.testCommand in abloh.yml with the command that runs your suite, or run abloh init from the package that holds it` : "declare environment.testCommand in abloh.yml with the command that runs your suite, or run abloh init from the package that holds it";
5307
+ }
5308
+ return `point --subdir (or abloh.yml target.directory) at one of the ${candidates.length} package(s) here that declare a suite, such as ${candidates[0]}`;
5681
5309
  }
5682
- function scriptPattern(pattern) {
5683
- const escaped = pattern.replace(/[\\^$+.[\]{}()|]/gu, "\\$&");
5684
- return new RegExp(`^${escaped.replace(/\*/gu, "[^:]*").replace(/\?/gu, "[^:]")}$`, "u");
5310
+ function candidateSentence(repoDir) {
5311
+ const candidates = testableWorkspacePackages(repoDir);
5312
+ if (candidates.length === 0) return "";
5313
+ const named = candidates.slice(0, NAMED_CANDIDATES);
5314
+ const more = candidates.length - named.length;
5315
+ return `. The packages here that declare a suite are ${named.join(", ")}` + (more > 0 ? ` and ${more} more` : "");
5316
+ }
5317
+ function detectTarget(repoDir, subdir, resolution = {}, ctx = diskRepoAccess(repoDir)) {
5318
+ const aliasFamily = rootAliasFamily(ctx, repoDir);
5319
+ const aggregatorTasks = rootAggregatorTasksInRepository(ctx, repoDir);
5320
+ const explicitResult = subdir ? probePkg(ctx, join8(repoDir, subdir), aliasFamily, repoDir, aggregatorTasks) : null;
5321
+ if (explicitResult?.refusal != null) throw manifestRefusal(explicitResult.refusal);
5322
+ const explicit = explicitResult?.probe ?? null;
5323
+ if (subdir && explicit === null) {
5324
+ throw new TargetNotFoundError(
5325
+ "configured-directory-has-no-package",
5326
+ `the configured target directory ${JSON.stringify(subdir)} has no package.json, so there is no package there to measure; point --subdir (or abloh.yml target.directory) at the directory holding the package's package.json, or remove it to measure the repository root`,
5327
+ "point --subdir (or abloh.yml target.directory) at the directory holding the package's package.json, or remove it to measure the repository root"
5328
+ );
5329
+ }
5330
+ const rootResult = probePkg(ctx, repoDir, aliasFamily, repoDir, aggregatorTasks);
5331
+ if (rootResult.refusal !== null) throw manifestRefusal(rootResult.refusal);
5332
+ const root = rootResult.probe;
5333
+ const probe = explicit !== null && (explicit.runner !== null || explicit.testScript !== null) ? explicit : root !== null && (root.runner !== null || root.testScript !== null) ? root : null;
5334
+ if (!probe) {
5335
+ throw new TargetNotFoundError(
5336
+ "no-testable-package",
5337
+ `no package.json with a test runner or test script found under ${repoDir}` + (subdir ? ` or in the configured target directory ${JSON.stringify(subdir)}` : "") + candidateSentence(repoDir),
5338
+ candidateRemedy(repoDir)
5339
+ );
5340
+ }
5341
+ const workspaceRootSuite = probe.workspaceRootSuite;
5342
+ const commandDir = workspaceRootSuite === null ? probe.dir : repoDir;
5343
+ const manager = detectPackageManagerContext(repoDir, commandDir, resolution, ctx);
5344
+ const pm = manager.pm;
5345
+ const runner = probe.runner ?? "command";
5346
+ const projectTestCommand = packageTestCommand(manager);
5347
+ const direct = directRunnerCommand(runner, detectModuleLayout(repoDir, probe.dir));
5348
+ if (probe.declaredCommand === null && probe.testScript === null && direct !== null && probe.dir === repoDir && declaresWorkspace(ctx, repoDir)) {
5349
+ throw new TargetNotFoundError(
5350
+ "workspace-root-declares-no-test-script",
5351
+ `${repoDir} is a workspace root that declares no test script, so there is no suite here to measure and Abloh will not guess one from an installed runner: point --subdir (or abloh.yml target.directory) at the package you want measured, or declare environment.testCommand if one command runs the suite from the root` + candidateSentence(repoDir),
5352
+ "point --subdir (or abloh.yml target.directory) at the package you want measured, or declare environment.testCommand if one command runs the suite from the root" + candidateSentence(repoDir)
5353
+ );
5354
+ }
5355
+ const testCommand = probe.declaredCommand ?? (probe.testScript !== null || direct === null ? projectTestCommand : direct);
5356
+ const rel = relative5(repoDir, probe.dir);
5357
+ const resolvedSubdir = rel === "" ? null : rel;
5358
+ const lifecycleHooks = [probe.scripts.pretest, probe.scripts.posttest].filter(
5359
+ (hook) => typeof hook === "string"
5360
+ );
5361
+ const runnerStage = probe.testScript !== null && testCommand === projectTestCommand ? runnerStageCommand(probe.testScript, runner, lifecycleHooks, probe.scripts) : null;
5362
+ const preRunnerStages = probe.testScript !== null && testCommand === projectTestCommand ? preRunnerStageCommands(probe.testScript, runner, probe.scripts) : [];
5363
+ const watchDeclaration = testCommand === projectTestCommand ? probe.testScript : testCommand;
5364
+ if (watchDeclaration !== null) {
5365
+ const refusal = watchModeRefusal(
5366
+ runner,
5367
+ watchDeclaration,
5368
+ testCommand === projectTestCommand ? `scripts.test in ${join8(relative5(repoDir, probe.dir), "package.json")}` : probe.declaredCommand === testCommand ? "your declared environment.testCommand" : "your test command"
5369
+ );
5370
+ if (refusal !== null) throw new TargetDetectionError(refusal);
5371
+ }
5372
+ const watchNormalized = watchDeclaration === null ? null : withWatchModeDisabled({
5373
+ runner,
5374
+ command: testCommand,
5375
+ watchDeclaration,
5376
+ packageManagerMajor: manager.version === null ? null : Number.parseInt(manager.version, 10)
5377
+ });
5378
+ const effectiveTestCommand = watchNormalized ?? testCommand;
5379
+ const watchNormalizedStage = runnerStage === null ? null : withWatchModeDisabled({ runner, command: runnerStage, watchDeclaration: runnerStage });
5380
+ const invocation = RUNNER_INVOCATION_CARRYING_RUNNERS.has(runner) ? runnerInvocation(probe.testScript, runner, probe.scripts) ?? probe.declaredCommand ?? (testCommand === direct ? direct : null) : null;
5381
+ return {
5382
+ pm,
5383
+ runner,
5384
+ testCommand: effectiveTestCommand,
5385
+ ...watchNormalized === null ? {} : { watchModeNormalized: true },
5386
+ ...probe.aggregator === null ? {} : { workspaceAggregator: probe.aggregator },
5387
+ ...probe.aggregatorTask == null ? {} : { aggregatorTask: probe.aggregatorTask },
5388
+ ...runnerStage === null ? {} : { runnerStageCommand: watchNormalizedStage ?? runnerStage },
5389
+ /* Only alongside a runner stage, because these are exactly what running that stage alone would
5390
+ otherwise skip. A one-stage script gated by a lifecycle hook has a runner stage and no
5391
+ earlier stage of its own, and gets an empty list rather than a key. */
5392
+ ...runnerStage === null || preRunnerStages.length === 0 ? {} : { preRunnerStageCommands: preRunnerStages },
5393
+ ...invocation === null ? {} : { runnerInvocation: invocation },
5394
+ vitestDir: runner === "vitest" ? detectVitestDir(probe.dir, ctx) : void 0,
5395
+ jasmineSuite: runner === "jasmine" ? detectJasmineSuite(probe.dir, probe.testScript, ctx) : void 0,
5396
+ ...probe.angular === null ? {} : { angularTarget: probe.angular },
5397
+ subdir: resolvedSubdir,
5398
+ workDir: commandDir,
5399
+ ...workspaceRootSuite === null ? {} : { workspaceRootSuite },
5400
+ ...probe.note ? { identifiedRunner: probe.note } : {},
5401
+ ...compiledOutputFields({
5402
+ ctx,
5403
+ repoDir,
5404
+ packageDir: resolvedSubdir,
5405
+ runner,
5406
+ invocation,
5407
+ testCommand: effectiveTestCommand,
5408
+ scripts: probe.scripts,
5409
+ manager: pm,
5410
+ angular: probe.angular !== null
5411
+ }),
5412
+ ...runner === "command" ? suiteFanoutPlan(probe, manager) : {}
5413
+ };
5414
+ }
5415
+
5416
+ // src/aggregator-chain.ts
5417
+ import { runnerConfigFromInvocation } from "@abloh/core";
5418
+ var EXCLUDE_FLAGS = /* @__PURE__ */ new Set(["--exclude"]);
5419
+ var SELECTION_FLAGS = /* @__PURE__ */ new Set([
5420
+ "--filter",
5421
+ "-F",
5422
+ // turbo
5423
+ "--projects",
5424
+ "-p",
5425
+ // nx
5426
+ "--scope",
5427
+ "--ignore",
5428
+ "--since",
5429
+ // lerna
5430
+ "--to"
5431
+ // lage
5432
+ ]);
5433
+ function flagValues(args, names) {
5434
+ const found = [];
5435
+ for (let at = 0; at < args.length; at += 1) {
5436
+ const arg = args[at];
5437
+ const eq = arg.indexOf("=");
5438
+ const name = eq === -1 ? arg : arg.slice(0, eq);
5439
+ if (!names.has(name)) continue;
5440
+ if (eq !== -1) {
5441
+ found.push(arg.slice(eq + 1));
5442
+ continue;
5443
+ }
5444
+ const next = args[at + 1];
5445
+ if (next !== void 0 && !next.startsWith("-")) {
5446
+ found.push(next);
5447
+ at += 1;
5448
+ } else {
5449
+ found.push("");
5450
+ }
5451
+ }
5452
+ return found;
5685
5453
  }
5686
- var WRAPPERS = ["npm-run-all", "run-p", "run-s"];
5687
- var WRAPPER_SCHEDULING_OPTIONS = /^(?:-[aclps]+|--parallel|--sequential|--continue-on-error|--silent|--aggregate-output|--print-label)$/u;
5688
- function leafRunner(leaf) {
5689
- const named = runnerNamesInScript(leaf);
5690
- const runner = named.length === 1 ? named[0] : invokesNodeTest(leaf) ? "node-test" : null;
5691
- if (runner === null || !NATIVE_COVERAGE_RUNNERS.has(runner)) return null;
5692
- return runnerSuiteInvocations(leaf, runner) > 1 ? null : runner;
5454
+ function selectionFlagsPresent(args) {
5455
+ const present = [];
5456
+ for (const arg of args) {
5457
+ const eq = arg.indexOf("=");
5458
+ const name = eq === -1 ? arg : arg.slice(0, eq);
5459
+ if (SELECTION_FLAGS.has(name)) present.push(arg);
5460
+ }
5461
+ return present;
5693
5462
  }
5694
- function wrapperFanout(probe, manager) {
5695
- const script = probe.testScript;
5696
- if (script === null) return void 0;
5697
- const executable = script.trim().split(/\s+/u)[0]?.replace(/^.*\//u, "");
5698
- if (!WRAPPERS.includes(executable)) return void 0;
5699
- if (/[;&|<>`]|\$\(/u.test(script)) return { refusal: "it composes shell commands around the wrapper" };
5700
- let argv;
5463
+ function plainGlob(pattern) {
5464
+ const path = pattern.trim().replace(/\\/gu, "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
5465
+ if (path === "" || path.startsWith("/") || /[{}()[\]!?+@|^$]/u.test(path)) return null;
5466
+ if (path.split("/").some((segment) => segment === "..")) return null;
5467
+ const source = path.split("/").map((segment) => segment === "**" ? "[^/]+(?:/[^/]+)*" : segment === "*" ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join("/");
5701
5468
  try {
5702
- argv = parseLiteralArguments2(script, "npm-run-all test script");
5469
+ return new RegExp(`^${source}$`, "u");
5703
5470
  } catch {
5704
- return { refusal: "its wrapper invocation could not be parsed as one literal command" };
5471
+ return null;
5705
5472
  }
5706
- const patterns = [];
5707
- let parallel = executable === "run-p";
5708
- let sequential = executable === "run-s";
5709
- for (const arg of argv.slice(1)) {
5710
- if (arg.startsWith("-")) {
5711
- if (!WRAPPER_SCHEDULING_OPTIONS.test(arg)) {
5712
- return { refusal: `the wrapper option ${arg} can change which suites run to completion` };
5713
- }
5714
- if (arg === "--parallel" || /^-[aclps]+$/u.test(arg) && arg.includes("p")) parallel = true;
5715
- if (arg === "--sequential" || /^-[aclps]+$/u.test(arg) && arg.includes("s")) sequential = true;
5473
+ }
5474
+ var CONFIG_STEMS = {
5475
+ vitest: ["vitest.config", "vite.config"],
5476
+ jest: ["jest.config"],
5477
+ mocha: [".mocharc"],
5478
+ ava: ["ava.config"],
5479
+ jasmine: ["jasmine.json"],
5480
+ tap: ["tap.yaml"]
5481
+ };
5482
+ var CONFIG_EXTENSIONS = ["", ".ts", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json", ".yaml", ".yml"];
5483
+ function configFileFor(repoDir, ctx, directory, runner, script) {
5484
+ const named = runnerConfigFromInvocation({ runner, testCommand: script });
5485
+ if (named !== null) return joinInsideRepository(directory, named);
5486
+ for (const stem of CONFIG_STEMS[runner] ?? []) {
5487
+ for (const extension of CONFIG_EXTENSIONS) {
5488
+ const relative7 = directory === "" ? `${stem}${extension}` : `${directory}/${stem}${extension}`;
5489
+ if (accessKind(ctx, join9(repoDir, relative7)) === "file") return relative7;
5490
+ }
5491
+ }
5492
+ return null;
5493
+ }
5494
+ function joinInsideRepository(directory, declared) {
5495
+ const raw = declared.replace(/^["']|["']$/gu, "").replace(/\\/gu, "/");
5496
+ if (raw === "" || raw.startsWith("/") || /^[A-Za-z]:/u.test(raw) || /[;&|<>`$]/u.test(raw)) return null;
5497
+ const resolved = [];
5498
+ for (const segment of [...directory.split("/"), ...raw.split("/")]) {
5499
+ if (segment === "" || segment === ".") continue;
5500
+ if (segment === "..") {
5501
+ if (resolved.length === 0) return null;
5502
+ resolved.pop();
5716
5503
  continue;
5717
5504
  }
5718
- patterns.push(arg);
5505
+ resolved.push(segment);
5719
5506
  }
5720
- if (patterns.length === 0) return { refusal: "its wrapper selects no scripts" };
5721
- if (parallel && sequential) return { refusal: "its wrapper asks for parallel and sequential at once" };
5722
- const selected = /* @__PURE__ */ new Set();
5723
- const names = Object.keys(probe.scripts).sort();
5724
- for (const pattern of patterns) {
5725
- const matches = names.filter((name) => name !== "test" && scriptPattern(pattern).test(name));
5726
- if (matches.length === 0) return { refusal: `no script matches the pattern ${pattern}` };
5727
- for (const match of matches) selected.add(match);
5507
+ return resolved.length === 0 ? null : resolved.join("/");
5508
+ }
5509
+ function manifestAt(ctx, repoDir, directory) {
5510
+ const reading = accessManifest(ctx, join9(repoDir, directory, "package.json"));
5511
+ return reading.standing === "read" ? reading.manifest : null;
5512
+ }
5513
+ function aggregatorChain(repoDir, ctx = diskRepoAccess(repoDir)) {
5514
+ const rootManifest = manifestAt(ctx, repoDir, "");
5515
+ if (rootManifest === null) return null;
5516
+ const family = aliasHopFamily(rootManifest);
5517
+ const classified = classifyWithAliasHop(rootManifest, family);
5518
+ if (classified.aggregator === null) return null;
5519
+ const invocationScript = classified.aggregatorInvocationScript;
5520
+ const invocation = invocationScript === null ? null : aggregatorInvocation(invocationScript);
5521
+ const args = invocationScript === null ? [] : invocationScript.trim().split(/\s+/u).slice(1);
5522
+ const unreadSelection = selectionFlagsPresent(args);
5523
+ const excludeGlobs = [];
5524
+ for (const value of flagValues(args, EXCLUDE_FLAGS)) {
5525
+ const glob = plainGlob(value);
5526
+ if (glob === null) unreadSelection.push(`--exclude=${value}`);
5527
+ else excludeGlobs.push(glob);
5528
+ }
5529
+ const enumeration = enumerateWorkspacePackages(repoDir, ctx);
5530
+ const packages = [];
5531
+ const gaps = [];
5532
+ const excluded = [];
5533
+ for (const directory of enumeration.packages) {
5534
+ if (excludeGlobs.some((glob) => glob.test(directory))) {
5535
+ excluded.push(directory);
5536
+ continue;
5537
+ }
5538
+ const manifest = manifestAt(ctx, repoDir, directory);
5539
+ if (manifest === null) {
5540
+ gaps.push({ directory, reason: "abloh could not read its package.json" });
5541
+ continue;
5542
+ }
5543
+ const packageFamily = aliasHopFamily(manifest) ?? family;
5544
+ const suite = classifyWithAliasHop(manifest, packageFamily, classified.aggregatorTasks);
5545
+ if (suite.runner === null || suite.testScript === null) {
5546
+ gaps.push({
5547
+ directory,
5548
+ reason: suite.aggregatorTaskRefusal ?? `nothing it declares under ${describeTasks(classified.aggregatorTasks)} runs a test runner abloh recognizes`
5549
+ });
5550
+ continue;
5551
+ }
5552
+ packages.push({
5553
+ directory,
5554
+ task: suite.aggregatorTask ?? "test",
5555
+ script: suite.testScript,
5556
+ runner: suite.runner,
5557
+ configFile: configFileFor(repoDir, ctx, directory, suite.runner, suite.testScript)
5558
+ });
5728
5559
  }
5729
- const children = scriptChildren([...selected].sort(), probe, manager);
5730
- return "refusal" in children ? children : { children: children.children, execution: parallel ? "parallel" : "sequential" };
5560
+ return {
5561
+ tool: classified.aggregator,
5562
+ tasks: invocation?.tasks ?? classified.aggregatorTasks,
5563
+ packages,
5564
+ gaps,
5565
+ unreadSelection,
5566
+ excluded,
5567
+ refusals: enumeration.refusals
5568
+ };
5731
5569
  }
5732
- function concurrentlyFanout(probe, manager) {
5733
- const script = probe.testScript;
5734
- if (script === null) return void 0;
5735
- if (script.trim().split(/\s+/u)[0]?.replace(/^.*\//u, "") !== "concurrently") return void 0;
5736
- if (/[;&|<>`]|\$\(/u.test(script)) return { refusal: "it composes shell commands around concurrently" };
5737
- let argv;
5738
- try {
5739
- argv = parseLiteralArguments2(script, "concurrently test script");
5740
- } catch {
5741
- return { refusal: "its concurrently invocation could not be parsed as one literal command" };
5570
+ function describeTasks(tasks) {
5571
+ const named = tasks.length === 0 ? ["test"] : [...tasks];
5572
+ if (named.length === 1) return named[0];
5573
+ return `${named.slice(0, -1).join(", ")} or ${named.at(-1)}`;
5574
+ }
5575
+
5576
+ // src/config-draft.ts
5577
+ import { parse as parse2 } from "yaml";
5578
+ import {
5579
+ CUSTOMER_TYPED,
5580
+ DeclaredRefusalError as DeclaredRefusalError2,
5581
+ refusalFromFacts as refusalFromFacts2,
5582
+ WORKFLOW_FILE_PATH,
5583
+ WORKFLOW_JOB_ID,
5584
+ SEALED_BUN_VERSION,
5585
+ SEALED_DENO_VERSION,
5586
+ SETUP_SCRIPT_PATH,
5587
+ configFlakyLines,
5588
+ installCommandNeedsCorepackEnable,
5589
+ parseLiteralArguments as parseLiteralArguments2,
5590
+ systemPackageInstallCommand,
5591
+ configHeaderLines,
5592
+ configSettingLines,
5593
+ configSetupJobLines,
5594
+ credentialShapeRefusal,
5595
+ matchesCredentialShape as matchesCredentialShape2,
5596
+ workflowYaml,
5597
+ yamlScalar
5598
+ } from "@abloh/core";
5599
+
5600
+ // src/init-refusal.ts
5601
+ var InitRefusal = class extends Error {
5602
+ failureClass;
5603
+ /**
5604
+ * THE WALK KEY THIS REFUSAL IS ABOUT, where the throw site knows one the class cannot.
5605
+ *
5606
+ * Null on almost every throw, and that is right: the class already implies the key for a refusal
5607
+ * that can only ever be about one - `no-reviewed-image` is `runtimeImage` wherever it is raised,
5608
+ * and `init-refusal-shape.ts` holds that mapping in one table rather than repeating it at forty
5609
+ * throw sites. This field is for the refusals where the class covers SEVERAL keys, which today is
5610
+ * the answers document: `invalid-answers-file` is about whichever key the document got wrong, and
5611
+ * a fix depends on knowing which.
5612
+ */
5613
+ questionKey;
5614
+ /**
5615
+ * THIS REFUSAL'S OWN REMEDY, where the class table's is not true of this instance.
5616
+ *
5617
+ * Null on almost every throw, and that is still right: a per-class remedy is what stops forty
5618
+ * throw sites carrying forty drifting copies of the same sentence. But a CLASS is not always one
5619
+ * situation, and `repository-too-large` is where that broke (external refusal review, rank 15).
5620
+ * Its four throw sites all tell the human to pass `--environment-image`, because the scan they
5621
+ * abandon is the Dockerfile scan and an image is what makes it unnecessary. The class table told
5622
+ * the MACHINE to pass `--subdir`, which narrows nothing: the scan walks the repository root
5623
+ * whatever `--subdir` says. A human and an agent reading the same refusal were sent to different
5624
+ * options, and only one of them could work.
5625
+ *
5626
+ * So a throw site that knows better than its class says so here, and `init-refusal-shape.ts`
5627
+ * prefers it. One refusal instance, one remedy, whoever is reading.
5628
+ */
5629
+ remedy;
5630
+ constructor(failureClass, message, questionKey = null, remedy = null) {
5631
+ super(message);
5632
+ this.name = "InitRefusal";
5633
+ this.failureClass = failureClass;
5634
+ this.questionKey = questionKey;
5635
+ this.remedy = remedy;
5742
5636
  }
5743
- const patterns = [];
5744
- for (const arg of argv.slice(1)) {
5745
- if (arg.startsWith("-")) {
5746
- if (!/^(?:--(?:names|prefix|prefix-colors|group|raw)|-[np]|-c)$/u.test(arg)) {
5747
- return { refusal: `the concurrently option ${arg} can change which suites run to completion` };
5637
+ };
5638
+ function initRefusal(failureClass, message, questionKey = null, remedy = null) {
5639
+ return new InitRefusal(failureClass, message, questionKey, remedy);
5640
+ }
5641
+ function refusalRemedyOf(error) {
5642
+ return error instanceof InitRefusal ? error.remedy : null;
5643
+ }
5644
+ function initFailureClassOf(error) {
5645
+ return error instanceof InitRefusal ? error.failureClass : "unclassified";
5646
+ }
5647
+
5648
+ // src/config-draft.ts
5649
+ function commentLines(text, indent, width = 108) {
5650
+ const lines = [];
5651
+ let current = "";
5652
+ for (const word of text.split(/\s+/u).filter((value) => value !== "")) {
5653
+ const candidate = current === "" ? word : `${current} ${word}`;
5654
+ if (`${indent}# ${candidate}`.length > width && current !== "") {
5655
+ lines.push(`${indent}# ${current}`);
5656
+ current = word;
5657
+ continue;
5658
+ }
5659
+ current = candidate;
5660
+ }
5661
+ if (current !== "") lines.push(`${indent}# ${current}`);
5662
+ return lines;
5663
+ }
5664
+ function renderEnvironmentBlock(input) {
5665
+ const quote = input.yamlQuote ?? '"';
5666
+ const receiptLinesQ = (entries, indent) => receiptLines(entries, indent, quote);
5667
+ const lines = [];
5668
+ if (input.coverageNote != null) lines.push(...commentLines(input.coverageNote, "", 80), "");
5669
+ if (input.subdir !== null) {
5670
+ lines.push(
5671
+ "# The package measured.",
5672
+ "target:",
5673
+ ` directory: ${yamlScalar(input.subdir, quote)}`,
5674
+ ""
5675
+ );
5676
+ }
5677
+ lines.push(
5678
+ ENVIRONMENT_BLOCK_HEADER,
5679
+ "environment:",
5680
+ ...receiptLinesQ([
5681
+ { key: "runtimeImage", value: input.runtimeImage, receipt: input.evidence.runtimeImage }
5682
+ ], " "),
5683
+ ...input.runtimes.length === 0 ? [] : [
5684
+ " runtimes: # pinned runtimes added to the proof image, installed at build time",
5685
+ ...input.runtimes.flatMap((need) => [
5686
+ ...commentLines(need.evidence, " "),
5687
+ ` - ${yamlScalar(need.runtime, quote)}`
5688
+ ])
5689
+ ],
5690
+ ...input.browser === null ? [] : [
5691
+ ...commentLines(
5692
+ "your suite drives a real browser, so the proof image carries one and the run uses the browser seal - still --network none, reaching 127.0.0.1 and nothing else. Read from: " + input.browser.evidence.join("; "),
5693
+ " "
5694
+ ),
5695
+ ` browser: ${yamlScalar(input.browser.value, quote)}`
5696
+ ],
5697
+ ` installDirectory: ${yamlScalar(input.installDirectory, quote)}`,
5698
+ ...receiptLinesQ([
5699
+ /* THE POINTER, WHERE FOUR DERIVED KEYS USED TO BE (Kenneth, 2026-08-26). `systemPackages`,
5700
+ `installCommand` and `setupCommands` are the commands that turn a checkout into a tree the
5701
+ suite can run in, and they are now steps in a file the customer owns. What is left in this
5702
+ block is everything that is NOT a command: the image, the directory, the test commands, the
5703
+ services, the variables, the files. The receipt names the script rather than a source file
5704
+ because the script carries a receipt of its own on every step. */
5705
+ {
5706
+ key: "setup",
5707
+ value: input.setupPath ?? SETUP_SCRIPT_PATH,
5708
+ receipt: "written by init and yours to edit. Every cold run executes it step by step"
5709
+ },
5710
+ /* THE ONE THING THE SCRIPT CANNOT SAY, so the file still does: WHY there is no install step
5711
+ in it. A repository with no frozen recipe gets a script with nothing to install, and a
5712
+ reader who only ever opens `abloh.yml` would otherwise see a pointer at a file that quietly
5713
+ does less than they think. The remedy is the same sentence the console prints. */
5714
+ ...input.installCommand === null ? [{ key: "installCommand", value: null, receipt: input.evidence.installCommand }] : [],
5715
+ /* THE ONE RECEIPT THAT GAINS A CLAUSE IN THE FILE TOO, and only on the repositories it is
5716
+ true of. `genericAdapterCost` carries what changed and why the prompt alone was not it. */
5717
+ {
5718
+ key: "testCommand",
5719
+ value: input.testCommand,
5720
+ receipt: testCommandReceipt({
5721
+ evidence: input.evidence.testCommand,
5722
+ cost: input.testCommandCostNote
5723
+ })
5724
+ },
5725
+ /* WRITTEN ONLY WHERE IT SAYS SOMETHING, and its receipt is the whole reason it says anything.
5726
+ See `ConfigInput.runner`: on an uncontested repository the key would restate what
5727
+ `bindPreparedTestCommand` reads off the command one line above, so there is no line and the
5728
+ PROMPT is where the detected runner is shown. Where a candidate was discarded, the line
5729
+ records the decision AND names what was skipped, in the same words the prompt used - which
5730
+ is what makes an unattended init as informed as an interactive one. */
5731
+ ...input.runner === null ? [] : [{ key: "runner", value: input.runner, receipt: input.evidence.runner }],
5732
+ /* THE ANSWER TO THE ONE QUESTION A VITEST REPOSITORY WITH NO COVERAGE PROVIDER IS ASKED.
5733
+ WRITTEN ONLY WHERE SOMEBODY ANSWERED IT, on the same restraint every derived key here
5734
+ follows: absent already means `install`, so a line saying `install` on a repository nobody
5735
+ asked would be a claim about a conversation that never happened. Both answers are written
5736
+ where there WAS one, because the file is where the run reads it. */
5737
+ ...input.coverageProvider === void 0 ? [] : [{
5738
+ key: "coverageProvider",
5739
+ value: input.coverageProvider,
5740
+ receipt: input.coverageProvider === "install" ? "you agreed abloh may stage @vitest/coverage-v8 where a run measures, into its own cache outside this repository" : "you asked abloh not to install @vitest/coverage-v8, so the changed-line check reports itself as declined"
5741
+ }],
5742
+ ...input.sealedTestCommand !== null || input.sealedNote !== null ? [{
5743
+ key: "sealedTestCommand",
5744
+ value: input.sealedTestCommand,
5745
+ receipt: input.sealedTestCommand === null ? input.sealedNote ?? "" : input.evidence.sealedTestCommand
5746
+ }] : [],
5747
+ /* THE ONE KEY WHOSE ABSENCE PRODUCES A WRONG NUMBER RATHER THAN A REFUSAL, so the file states
5748
+ the finding even when it cannot state a value: a reviewer reading a commented-out
5749
+ `rebuildCommand` beside "these two files say your tests read your build output" has the
5750
+ whole question in front of them, and a file that stayed silent would leave a run reporting
5751
+ every mutant as a survivor with nothing anywhere to explain it. */
5752
+ ...input.rebuild === null ? [] : [{
5753
+ key: "rebuildCommand",
5754
+ value: input.rebuild.value,
5755
+ /* THE KEY IS NAMED IN THE SENTENCE, because a null value renders as a bare comment with
5756
+ no key beside it - and a reader who agrees with the finding then has nothing to type.
5757
+ It is the same rule `installRemedy` follows one key up. */
5758
+ receipt: input.rebuild.value !== null ? input.rebuild.evidence : `${input.rebuild.evidence}. Set rebuildCommand to the build that writes it - this package declares ${input.rebuild.candidates.join(" and ")}. Without one, every planted bug is reported as missed`
5759
+ }]
5760
+ ], " ")
5761
+ );
5762
+ if (input.services.length > 0) {
5763
+ lines.push(
5764
+ " services: # fresh instances per proof phase, reachable on localhost exactly as in your CI"
5765
+ );
5766
+ for (const service of input.services) {
5767
+ lines.push(
5768
+ ...commentLines(service.evidence, " "),
5769
+ ` - name: ${yamlScalar(service.name, quote)}`,
5770
+ ` image: ${yamlScalar(service.image, quote)}`
5771
+ );
5772
+ if (service.env.length > 0) {
5773
+ lines.push(" env: # your workflow's own values; the suite connects with these");
5774
+ for (const entry of service.env) lines.push(` ${entry.name}: ${yamlScalar(entry.value, quote)}`);
5775
+ }
5776
+ if (service.healthCommand !== null) {
5777
+ lines.push(` healthCommand: ${yamlScalar(service.healthCommand, quote)}`);
5778
+ }
5779
+ if (service.healthPort !== null) lines.push(` healthPort: ${service.healthPort}`);
5780
+ if (service.localhostPort != null && service.localhostPort !== service.healthPort) {
5781
+ lines.push(
5782
+ ` localhostPort: ${service.localhostPort} # another declared service listens on this one's port, so a run reaches it here`
5783
+ );
5748
5784
  }
5749
- continue;
5750
- }
5751
- if (!arg.startsWith("npm:")) {
5752
- return { refusal: "concurrently is given a raw command line rather than an npm: script reference" };
5753
5785
  }
5754
- patterns.push(arg.slice("npm:".length));
5755
- }
5756
- if (patterns.length === 0) return { refusal: "its concurrently invocation selects no scripts" };
5757
- const selected = /* @__PURE__ */ new Set();
5758
- const names = Object.keys(probe.scripts).sort();
5759
- for (const pattern of patterns) {
5760
- const matches = names.filter((name) => name !== "test" && scriptPattern(pattern).test(name));
5761
- if (matches.length === 0) return { refusal: `no script matches the pattern ${pattern}` };
5762
- for (const match of matches) selected.add(match);
5763
5786
  }
5764
- const children = scriptChildren([...selected].sort(), probe, manager);
5765
- return "refusal" in children ? children : { children: children.children, execution: "parallel" };
5766
- }
5767
- function scriptChildren(scripts, probe, manager) {
5768
- if (scripts.length === 0) return { refusal: "it selects no scripts" };
5769
- if (scripts.length > MAX_FANOUT_CHILDREN) {
5770
- return { refusal: `it launches ${scripts.length} suites, more than the ${MAX_FANOUT_CHILDREN} this run will measure` };
5787
+ if (input.identityFiles.length > 0) {
5788
+ lines.push(" identityFiles: # a change here makes CI reinstall instead of reusing its image");
5789
+ for (const path of input.identityFiles) lines.push(` - ${yamlScalar(path, quote)}`);
5771
5790
  }
5772
- const children = [];
5773
- for (const script of scripts) {
5774
- if (!/^[A-Za-z0-9:_-]{1,128}$/u.test(script)) return { refusal: `the script name ${script} is not a plain name` };
5775
- const leaf = probe.scripts[script];
5776
- if (leaf === void 0 || /[;&|<>`]|\$\(/u.test(leaf)) {
5777
- return { refusal: `the script ${script} composes shell commands rather than running one suite` };
5778
- }
5779
- let leafArgv;
5780
- try {
5781
- leafArgv = parseLiteralArguments2(leaf, `fanout child ${script}`);
5782
- } catch {
5783
- return { refusal: `the script ${script} could not be parsed as one literal command` };
5784
- }
5785
- if (leafArgv.some((arg) => WRAPPERS.includes(arg))) {
5786
- return { refusal: `the script ${script} is itself a wrapper, and this expands one level only` };
5791
+ if (input.requiredVariables.length > 0) {
5792
+ lines.push(" requiredVariables: # names only; values come from your environment and are masked in every log");
5793
+ for (const variable of input.requiredVariables) {
5794
+ lines.push(...commentLines(variable.evidence, " "), ` - ${yamlScalar(variable.name, quote)}`);
5787
5795
  }
5788
- const runner = leafRunner(leaf);
5789
- if (runner === null) return { refusal: `the script ${script} names no single runner with native coverage` };
5790
- children.push({ script, runner, testCommand: packageScriptCommand(manager, script) });
5791
- }
5792
- return { children };
5793
- }
5794
- function chainFanout(probe, manager) {
5795
- const script = probe.testScript;
5796
- if (script === null || script.trim() === "") return void 0;
5797
- if (suiteStages(script, probe.scripts) < 2) return void 0;
5798
- if (/[|<>`]|\$\(/u.test(script) || /(^|[^&])&([^&]|$)/u.test(script)) {
5799
- return { refusal: "its suites are composed with something other than `&&`, so which of them ran is not decidable" };
5800
- }
5801
- if (script.includes(";")) {
5802
- return { refusal: "its suites are separated by `;`, which runs the next one whether or not the last passed" };
5803
- }
5804
- const chainStages = script.split("&&").map((stage) => stage.trim());
5805
- if (chainStages.some((stage) => stage === "")) return { refusal: "its `&&` chain has an empty stage" };
5806
- if (chainStages.length > MAX_FANOUT_CHILDREN) {
5807
- return { refusal: `it chains ${chainStages.length} stages, more than the ${MAX_FANOUT_CHILDREN} this run will measure` };
5808
5796
  }
5809
- const children = [];
5810
- for (const stage of chainStages) {
5811
- if (suiteStages(stage, probe.scripts) === 0) continue;
5812
- const alias = /^(?:corepack\s+)?(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?([A-Za-z0-9:._-]+)$/u.exec(stage);
5813
- if (alias !== null) {
5814
- const family = stage.trim().replace(/^corepack\s+/u, "").split(/\s+/u)[0];
5815
- if (family !== manager.pm) {
5816
- return { refusal: `the stage \`${stage}\` runs under ${family} while this package resolves to ${manager.pm}` };
5817
- }
5818
- const named = scriptChildren([alias[1]], probe, manager);
5819
- if ("refusal" in named) return named;
5820
- children.push(...named.children);
5821
- continue;
5822
- }
5823
- const runner = [...NATIVE_COVERAGE_RUNNERS].find((candidate) => stageInvokes(stage, candidate));
5824
- if (runner === void 0) {
5825
- return { refusal: `the stage \`${stage}\` is neither a suite nor a script this run can name` };
5797
+ if (input.environmentValues.length > 0) {
5798
+ for (const variable of input.environmentValues) {
5799
+ const shape = matchesCredentialShape2(variable.value);
5800
+ if (shape !== null) throw initRefusal("credential-in-environment-value", credentialShapeRefusal(variable.name, shape));
5826
5801
  }
5827
- const testCommand = runner === "node-test" ? stage : `npx --no-install ${stage}`;
5828
- try {
5829
- parsePreparedTestCommand(testCommand, "fanout stage");
5830
- } catch {
5831
- return { refusal: `the stage \`${stage}\` is not a literal command this run can execute on its own` };
5802
+ lines.push(" environmentValues: # literal values your CI sets; a secret goes in requiredVariables, by name");
5803
+ for (const variable of input.environmentValues) {
5804
+ lines.push(
5805
+ ...commentLines(variable.evidence, " "),
5806
+ ` ${variable.name}: ${yamlScalar(variable.value, quote)}`
5807
+ );
5832
5808
  }
5833
- children.push({ runner, testCommand });
5834
- }
5835
- if (children.length < 2) return void 0;
5836
- return { children, execution: "sequential" };
5837
- }
5838
- function suiteFanoutPlan(probe, manager) {
5839
- const aggregated = probe.aggregator === null ? null : { tool: probe.aggregator, tasks: [...probe.aggregatorTasks] };
5840
- if (aggregated !== null) {
5841
- return {
5842
- coverageExclusion: `this package's test script delegates to ${aggregated.tool}, which runs ${aggregatorTaskPhrase(aggregated)} rather than one suite, so no single coverage report describes it; abloh measures such a repository per package instead`
5843
- };
5844
- }
5845
- const resolved = wrapperFanout(probe, manager) ?? concurrentlyFanout(probe, manager) ?? chainFanout(probe, manager);
5846
- if (resolved === void 0) return {};
5847
- if ("refusal" in resolved) {
5848
- return { coverageExclusion: `this package's test script launches more than one suite, but ${resolved.refusal}` };
5849
- }
5850
- const runners = [...new Set(resolved.children.map((child) => child.runner))].sort();
5851
- if (runners.length > 1) {
5852
- return {
5853
- coverageExclusion: `this package's test script launches ${resolved.children.length} suites across ${runners.join(" and ")}, and one coverage report cannot carry two providers, so none of them is measured rather than some`
5854
- };
5855
5809
  }
5856
- return { coverageChildren: resolved.children, coverageExecution: resolved.execution };
5857
- }
5858
- function compiledOutputFields(input) {
5859
- if (input.angular || input.runner === "deno") return {};
5860
- const reading = readsCompiledOutput({
5861
- /*
5862
- * THE ONE READING ON THIS PATH THAT DOES NOT GO THROUGH THE REPOSITORY READER, and it is a
5863
- * decision rather than an oversight.
5864
- *
5865
- * `readsCompiledOutput` FOLLOWS `extends` INTO `node_modules` by design - rushstack's projects
5866
- * extend `@rushstack/heft-node-rig/profiles/default/tsconfig-base.json`, whose `outDir` is five
5867
- * hops out of the store and back into the project's own `lib`, and refusing to follow it means
5868
- * this reading never sees the repository the compile-then-test ruling was measured on. An
5869
- * installed store is exactly where symlinks live and exactly where "inside the checkout" is a
5870
- * different question, so the reader's two refusals - a symlinked leaf, and a parent that
5871
- * resolves outside the root - would silently narrow it. What that costs is not a refusal: a
5872
- * config this reading cannot see produces a different `rebuildCommand`, and a wrong rebuild is
5873
- * the one failure in this product that ends in a confident WRONG NUMBER.
5874
- *
5875
- * IT IS SAFE HERE FOR THE REASON THE READER EXISTS ELSEWHERE. The paths are composed by
5876
- * `compiled-output.ts` from a config's own `extends` and from `node_modules` walked upwards,
5877
- * never from a repository-author-controlled string this module passes through; and the only
5878
- * road that reaches it is one that HAS a checkout, since a service reading over the contents
5879
- * API has no installed store to follow into anyway.
5880
- */
5881
- readFile: (path) => {
5882
- const absolute = join8(input.repoDir, ...path.split("/"));
5883
- if (!existsSync2(absolute)) return null;
5884
- try {
5885
- return readFileSync5(absolute, "utf8");
5886
- } catch {
5887
- return null;
5888
- }
5889
- },
5890
- packageDir: input.packageDir ?? "",
5891
- runner: input.runner,
5892
- invocation: input.invocation,
5893
- /* THE COMMAND THIS RUN EXECUTES, beside the invocation and never instead of it. `runnerInvocation`
5894
- answers a narrower question - whether a stage can be RE-EMITTED - so it declines a stage with
5895
- anything in front of the runner, and `devcontainers/cli`'s whole suite is
5896
- `env TS_NODE_PROJECT=... mocha ... src/test/*.test.ts`. The reading in `@abloh/core` resolves the
5897
- runner's stage out of this where no invocation arrived; its own header states both halves. */
5898
- testCommand: input.testCommand,
5899
- scripts: input.scripts,
5900
- manager: input.manager,
5901
- /*
5902
- * THE SECOND READING'S SEAM IS THE REPOSITORY READER and never the raw filesystem one above.
5903
- *
5904
- * That one exists to follow a `tsconfig` `extends` into an installed store, which is the one
5905
- * place this detection legitimately leaves the tree. This reading opens the SUITE'S OWN TEST
5906
- * FILES, which are the repository's, so it takes the reader whose refusals are about staying
5907
- * inside it - a symlinked leaf and a parent resolving out of the root.
5908
- */
5909
- reader: input.ctx.reader
5910
- });
5911
- const found = reading.state === "reads-compiled-output" ? reading : null;
5912
- return {
5913
- rebuildCommand: found?.rebuildCommand ?? void 0,
5914
- rebuildEvidence: found === null ? void 0 : compiledOutputEvidence(found),
5915
- rebuildCandidates: found === null || found.candidates.length === 0 ? void 0 : found.candidates,
5916
- rebuildOutputDirectory: found?.outputDirectory ?? void 0
5917
- };
5918
- }
5919
- function repositoryRootOf(detected) {
5920
- if (detected.subdir === null || detected.workspaceRootSuite !== void 0) return detected.workDir;
5921
- return resolve6(detected.workDir, ...detected.subdir.split("/").map(() => ".."));
5922
- }
5923
- var VITEST_CONFIG_FILES = [
5924
- "vitest.config.ts",
5925
- "vitest.config.mts",
5926
- "vitest.config.cts",
5927
- "vitest.config.js",
5928
- "vitest.config.mjs",
5929
- "vitest.config.cjs",
5930
- "vite.config.ts",
5931
- "vite.config.mts",
5932
- "vite.config.cts",
5933
- "vite.config.js",
5934
- "vite.config.mjs",
5935
- "vite.config.cjs"
5936
- ];
5937
- function detectVitestDir(dir, ctx = diskRepoAccess(dir)) {
5938
- for (const name of VITEST_CONFIG_FILES) {
5939
- const p = join8(dir, name);
5940
- let src;
5941
- try {
5942
- src = accessText(ctx, p);
5943
- } catch (e) {
5944
- debugPreparationOverflowStack(e);
5945
- throw new TargetDetectionError(
5946
- `${name} at ${dir} could not be read: ${e instanceof Error ? e.message : "unreadable"}`
5947
- );
5810
+ if (input.generatedFiles.length > 0) {
5811
+ lines.push(" generatedFiles: # written by your tooling during a run; an undeclared change aborts it");
5812
+ for (const generated of input.generatedFiles) {
5813
+ lines.push(...commentLines(generated.evidence, " "), ` - ${yamlScalar(generated.path, quote)}`);
5948
5814
  }
5949
- if (src === null) continue;
5950
- return vitestDirIn(src);
5951
5815
  }
5952
- return void 0;
5953
- }
5954
- function vitestDirIn(source) {
5955
- const masked = maskLiterals(source);
5956
- const open = firstUnmasked(source, masked, /(?:^|[\s,;{])["']?test["']?\s*:\s*\{/gu);
5957
- if (open === -1) return void 0;
5958
- let depth = 0;
5959
- let close = -1;
5960
- for (let at = open; at < masked.length; at++) {
5961
- if (masked[at] === "{") depth += 1;
5962
- else if (masked[at] === "}") {
5963
- depth -= 1;
5964
- if (depth === 0) {
5965
- close = at;
5966
- break;
5967
- }
5816
+ if (input.rewrittenFiles.length > 0) {
5817
+ lines.push(" rewrittenFiles: # committed files your own install rewrites; measured as your install left them");
5818
+ for (const rewritten of input.rewrittenFiles) {
5819
+ lines.push(...commentLines(rewritten.evidence, " "), ` - ${yamlScalar(rewritten.path, quote)}`);
5968
5820
  }
5969
5821
  }
5970
- if (close === -1) return void 0;
5971
- const quote = firstUnmasked(source.slice(0, close), masked.slice(0, close), /\bdir["']?\s*:\s*['"`]/gu, open);
5972
- if (quote === -1) return void 0;
5973
- const end = masked.indexOf(source[quote], quote + 1);
5974
- if (end === -1 || end >= close) return void 0;
5975
- const value = source.slice(quote + 1, end);
5976
- return value === "" ? void 0 : value;
5822
+ return lines;
5977
5823
  }
5978
- function firstUnmasked(source, masked, pattern, from = 0) {
5979
- pattern.lastIndex = from;
5980
- for (let match = pattern.exec(source); match !== null; match = pattern.exec(source)) {
5981
- const at = match.index + match[0].length - 1;
5982
- if (masked[at] === source[at]) return at;
5824
+ function renderConfig(input) {
5825
+ const coverage = input.coverageNote == null ? [] : [...commentLines(input.coverageNote, "", 80), ""];
5826
+ const lines = [
5827
+ ...coverage,
5828
+ ...configHeaderLines("init"),
5829
+ ...configSettingLines(),
5830
+ /* AFTER THE TWO SETTINGS EVERY FILE CARRIES AND BEFORE THE JOB, because it is a setting of the
5831
+ same kind - a rule about the whole run rather than a fact about this repository's CI. */
5832
+ ...configFlakyLines(input.flaky),
5833
+ /* BEFORE THE ENVIRONMENT BLOCK, because it is the shortest answer to the first question a
5834
+ reviewer of this pull request asks - "which of our jobs is this running in" - and the
5835
+ environment block below is the long answer to a different one. */
5836
+ ...configSetupJobLines(
5837
+ input.setupJob,
5838
+ input.setupMatrixLeg,
5839
+ input.setupFailFast,
5840
+ input.setupStepUnreadableConditions,
5841
+ input.yamlQuote ?? '"'
5842
+ ),
5843
+ ...renderEnvironmentBlock({ ...input, coverageNote: null })
5844
+ ];
5845
+ return `${lines.join("\n")}
5846
+ `;
5847
+ }
5848
+ function renderWorkflow(input) {
5849
+ return readableWorkflow(workflowYaml({
5850
+ node: input.workflowNode,
5851
+ /* THE ONE RECIPE, NAMED. `environment.setup` when the repository declares its own path, so a
5852
+ re-run of `init` over a customer's chosen path writes a workflow that calls that path. */
5853
+ setupScriptPath: input.setupPath ?? SETUP_SCRIPT_PATH,
5854
+ /* The same directory the sealed image runs those steps in - `installWorkdir` in the runner -
5855
+ because a recipe that installs in one directory on the runner and another in the proof image
5856
+ is two recipes again. */
5857
+ setupWorkingDirectory: input.installDirectory,
5858
+ runtimes: input.runtimes.map((need) => ({
5859
+ name: need.runtime,
5860
+ version: need.runtime === "bun" ? SEALED_BUN_VERSION : SEALED_DENO_VERSION,
5861
+ evidence: need.evidence
5862
+ }))
5863
+ }));
5864
+ }
5865
+ function readableWorkflow(text) {
5866
+ try {
5867
+ parse2(text);
5868
+ } catch (error) {
5869
+ throw new DeclaredRefusalError2(refusalFromFacts2({
5870
+ kind: "setup-step-written",
5871
+ workflow: WORKFLOW_FILE_PATH,
5872
+ job: WORKFLOW_JOB_ID,
5873
+ why: `it does not parse as YAML: ${error instanceof Error ? error.message : "unknown error"}`
5874
+ }));
5983
5875
  }
5984
- return -1;
5876
+ return text;
5985
5877
  }
5986
- function maskLiterals(source) {
5987
- const out = source.split("");
5988
- const blank = (from, to) => {
5989
- for (let at2 = from; at2 < to && at2 < out.length; at2++) if (out[at2] !== "\n") out[at2] = " ";
5990
- };
5991
- let at = 0;
5992
- let previous = "";
5993
- while (at < source.length) {
5994
- const char = source[at];
5995
- if (char === '"' || char === "'" || char === "`") {
5996
- let end = at + 1;
5997
- while (end < source.length && source[end] !== char) end += source[end] === "\\" ? 2 : 1;
5998
- blank(at + 1, end);
5999
- at = Math.min(end + 1, source.length);
6000
- previous = char;
6001
- continue;
6002
- }
6003
- if (char === "/" && source[at + 1] === "/") {
6004
- const end = source.indexOf("\n", at);
6005
- blank(at, end === -1 ? source.length : end);
6006
- at = end === -1 ? source.length : end;
5878
+ function testCommandReceipt(input) {
5879
+ return input.cost == null ? input.evidence : `${input.evidence}. ${input.cost}`;
5880
+ }
5881
+ function receiptLines(entries, indent, quote) {
5882
+ const bodyOf = (entry) => entry.value === null ? null : `${indent}${entry.key}: ${yamlScalar(entry.value, quote)}`;
5883
+ const itemBody = (item) => `${indent} - ${yamlScalar(item.value, quote)}`;
5884
+ const withReceipt = (body, receipt) => `${body} # ${receipt}`;
5885
+ const lines = [];
5886
+ for (const entry of entries) {
5887
+ if (entry.items !== void 0) {
5888
+ if (entry.items.length === 0) continue;
5889
+ lines.push(`${indent}${entry.key}:${entry.receipt === "" ? "" : ` # ${entry.receipt}`}`);
5890
+ for (const item of entry.items) lines.push(withReceipt(itemBody(item), item.receipt));
6007
5891
  continue;
6008
5892
  }
6009
- if (char === "/" && source[at + 1] === "*") {
6010
- const end = source.indexOf("*/", at + 2);
6011
- blank(at, end === -1 ? source.length : end + 2);
6012
- at = end === -1 ? source.length : end + 2;
5893
+ const body = bodyOf(entry);
5894
+ if (body === null) {
5895
+ if (entry.receipt !== "") lines.push(`${indent}# ${entry.key}: ${entry.receipt}`);
6013
5896
  continue;
6014
5897
  }
6015
- if (char === "/" && previous !== "" && /[(,=:[!&|?{};+\-*]/u.test(previous)) {
6016
- let end = at + 1;
6017
- while (end < source.length && source[end] !== "/" && source[end] !== "\n") end += source[end] === "\\" ? 2 : 1;
6018
- if (source[end] === "/") {
6019
- blank(at, end + 1);
6020
- at = end + 1;
6021
- continue;
6022
- }
6023
- }
6024
- if (!/\s/u.test(char)) previous = char;
6025
- at += 1;
5898
+ lines.push(withReceipt(body, entry.receipt));
6026
5899
  }
6027
- return out.join("");
5900
+ return lines;
6028
5901
  }
6029
- function workDirPrefix(detection) {
6030
- return detection.workspaceRootSuite === void 0 ? detection.subdir : null;
5902
+ var ENVIRONMENT_BLOCK_HEADER = "# How CI rebuilds this project for abloh's check. Comments name the source file.";
5903
+ var RETIRED_ENVIRONMENT_BLOCK_HEADERS = [
5904
+ "# Rebuild recipe for the proof container. Comments name the source file."
5905
+ ];
5906
+ function isEnvironmentBlockHeader(line) {
5907
+ return line === ENVIRONMENT_BLOCK_HEADER || RETIRED_ENVIRONMENT_BLOCK_HEADERS.includes(line);
6031
5908
  }
6032
- function classicPerTestRunner(runner) {
6033
- return runner !== "command" && classicPerTestRefusal(runner) === null;
5909
+ var STEP_WHAT = {
5910
+ systemPackages: "OS packages your suite needs, pinned",
5911
+ corepack: "put the package manager on PATH, for your own lifecycle scripts",
5912
+ install: "dependencies, from your lockfile",
5913
+ /**
5914
+ * THE SAME STEP ON A REPOSITORY THAT CARRIES NO LOCKFILE (census run 7 F3, 2026-09-06).
5915
+ *
5916
+ * A SECOND SPELLING RATHER THAN A REWORD, on the rule above: `install`'s exact text is what a
5917
+ * re-run recognizes as its own step, so changing it would orphan every install step already
5918
+ * committed. Both are install steps to {@link initStepRole} and to {@link isInstallStep}, which is
5919
+ * what every reader asks instead of comparing against one of them.
5920
+ *
5921
+ * IT EXISTS BECAUSE THE OTHER ONE WOULD BE FALSE. `fastify/fastify` and `pinojs/pino` both
5922
+ * `.gitignore` their lockfile, so the drafted step resolves rather than installing frozen, and a
5923
+ * comment saying "from your lockfile" above a command that has none is a claim the file cannot
5924
+ * back. The `source` beside it names the ignore rule.
5925
+ */
5926
+ installUnlocked: "dependencies, resolved - your repository commits no lockfile",
5927
+ build: "your build"
5928
+ };
5929
+ function isInstallStep(what) {
5930
+ return what === STEP_WHAT.install || what === STEP_WHAT.installUnlocked;
6034
5931
  }
6035
- function classicPerTestRefusal(runner) {
6036
- return runner === "deno" ? "deno's mutation runs the whole suite per mutant (Stryker ships no deno runner), so the classic per-test layer has no per-test report to read; the sealed v2 engine drives deno one test at a time and is where a proposed test for this repository comes from" : null;
5932
+ function setupStepOrigin(step) {
5933
+ const role = initStepRole(step.what);
5934
+ if (role === null) return CUSTOMER_TYPED;
5935
+ return role === "setupCommands" ? "abloh-composed" : "abloh-derived";
6037
5936
  }
6038
- function declaresWorkspace(ctx, repoDir) {
6039
- for (const name of ["pnpm-workspace.yaml", "pnpm-workspace.yml", "nx.json", "lerna.json"]) {
6040
- if (accessExists(ctx, join8(repoDir, name))) return true;
6041
- }
6042
- try {
6043
- const reading = accessManifest(ctx, join8(repoDir, "package.json"));
6044
- if (reading.standing !== "read") return false;
6045
- const workspaces = reading.manifest.workspaces;
6046
- if (Array.isArray(workspaces)) return workspaces.length > 0;
6047
- if (workspaces !== null && typeof workspaces === "object") {
6048
- return Array.isArray(workspaces.packages);
5937
+ function initStepRole(what) {
5938
+ if (what === STEP_WHAT.systemPackages) return "systemPackages";
5939
+ if (what === STEP_WHAT.corepack || isInstallStep(what)) return "installCommand";
5940
+ if (what === STEP_WHAT.build) return "setupCommands";
5941
+ return null;
5942
+ }
5943
+ function systemPackagesStep(pinned) {
5944
+ if (pinned.length === 0) return null;
5945
+ return {
5946
+ what: STEP_WHAT.systemPackages,
5947
+ source: [...new Set(pinned.map((entry) => entry.source))].join(", "),
5948
+ /* THE SAME COMMAND THE IMAGE ALREADY RAN, one line per operation rather than one semicolon
5949
+ chain, because this file is read by a person now. `set -e` is in the header, so the chain's
5950
+ own guard is redundant here and its semicolons are noise. */
5951
+ command: systemPackageInstallCommand(pinned, { pin: "optional" }).replace(/^set -e;\s*/u, "").split(";").map((part) => part.trim()).filter((part) => part !== "").join("\n")
5952
+ };
5953
+ }
5954
+ function stepBody(env, command) {
5955
+ if (env.length === 0) return command;
5956
+ return [
5957
+ ...[...env].sort((left, right) => left.name.localeCompare(right.name)).map((value) => `export ${value.name}=${shellSingleQuoted(value.value)}`),
5958
+ command
5959
+ ].join("\n");
5960
+ }
5961
+ function shellSingleQuoted(value) {
5962
+ return `'${value.replaceAll("'", `'\\''`)}'`;
5963
+ }
5964
+ function setupScriptSteps(input) {
5965
+ const steps = [];
5966
+ const pinned = systemPackagesStep(input.systemPackages);
5967
+ if (pinned !== null) steps.push(pinned);
5968
+ if (input.installCommand !== null) {
5969
+ if (installCommandNeedsCorepackEnable(parseLiteralArguments2(input.installCommand))) {
5970
+ steps.push({
5971
+ what: STEP_WHAT.corepack,
5972
+ source: input.evidence.installCommand,
5973
+ command: "corepack enable"
5974
+ });
6049
5975
  }
6050
- } catch {
6051
- return false;
5976
+ steps.push({
5977
+ /* The step and its comment are one statement: a resolved install may not be introduced as
5978
+ coming from a lockfile the commit does not hold. See {@link ConfigInput.installUnlocked}. */
5979
+ what: input.installUnlocked === true ? STEP_WHAT.installUnlocked : STEP_WHAT.install,
5980
+ source: input.evidence.installCommand,
5981
+ /* THE COMMAND IS ABLOH'S AND THE ENVIRONMENT IS THE CUSTOMER'S. See
5982
+ {@link ConfigInput.installEnvironment}: the command comes from the lockfile and the values
5983
+ come from the step their own CI installs with. */
5984
+ command: stepBody(input.installEnvironment, input.installCommand)
5985
+ });
5986
+ }
5987
+ for (const entry of input.setupCommands) {
5988
+ steps.push({ what: STEP_WHAT.build, source: entry.source, command: stepBody(entry.env, entry.command) });
5989
+ }
5990
+ return steps;
5991
+ }
5992
+
5993
+ // src/setup-question-descriptor.ts
5994
+ import {
5995
+ judgeConfigKeyValue,
5996
+ SETUP_FAIL_FAST_ANSWERS,
5997
+ SETUP_FAIL_FAST_CONSEQUENCES
5998
+ } from "@abloh/core";
5999
+ var ATOM = /\s*,\s*/u;
6000
+ var COMMANDS = /\s*&&\s*/u;
6001
+ function splitAnswerList(shape, raw) {
6002
+ const trimmed = raw.trim();
6003
+ if (trimmed === "") return [];
6004
+ return trimmed.split(shape.kind === "commands" ? COMMANDS : ATOM).map((entry) => entry.trim()).filter((entry) => entry !== "");
6005
+ }
6006
+ function splitAtFirstEquals(entry) {
6007
+ const at = entry.indexOf("=");
6008
+ return at === -1 ? [entry, null] : [entry.slice(0, at), entry.slice(at + 1)];
6009
+ }
6010
+ var SERVICE_ENTRY = /^(?<image>.*@sha256:[a-f0-9]{64})(?::(?<port>\d{1,5}))?$/u;
6011
+ function readServiceEntries(raw) {
6012
+ return splitAnswerList({ kind: "list" }, raw).map((entry) => {
6013
+ const [name, rest] = splitAtFirstEquals(entry);
6014
+ const gate = rest === null ? null : SERVICE_ENTRY.exec(rest);
6015
+ if (gate === null) return { name, image: rest ?? "" };
6016
+ const port = gate.groups?.["port"];
6017
+ return {
6018
+ name,
6019
+ image: gate.groups?.["image"] ?? "",
6020
+ ...port === void 0 ? {} : { healthPort: Number(port) }
6021
+ };
6022
+ });
6023
+ }
6024
+ function readAnswerValue(descriptor, raw) {
6025
+ switch (descriptor.shape.kind) {
6026
+ case "scalar":
6027
+ return raw.trim();
6028
+ case "list":
6029
+ return splitAnswerList(descriptor.shape, raw);
6030
+ case "commands":
6031
+ return splitAnswerList(descriptor.shape, raw);
6032
+ case "names":
6033
+ return splitAnswerList(descriptor.shape, raw);
6034
+ case "packages":
6035
+ return splitAnswerList(descriptor.shape, raw).map((entry) => {
6036
+ const [name, version] = splitAtFirstEquals(entry);
6037
+ return version === null ? { name } : { name, version };
6038
+ });
6039
+ case "services":
6040
+ return readServiceEntries(raw);
6041
+ case "pairs":
6042
+ return Object.fromEntries(
6043
+ splitAnswerList(descriptor.shape, raw).map((entry) => {
6044
+ const [name, value] = splitAtFirstEquals(entry);
6045
+ return [name, value ?? ""];
6046
+ })
6047
+ );
6052
6048
  }
6053
- return false;
6054
6049
  }
6055
- var NAMED_CANDIDATES = 8;
6056
- function testableWorkspacePackages(repoDir, refusals, ctx = diskRepoAccess(repoDir)) {
6057
- const aliasFamily = rootAliasFamily(ctx, repoDir);
6058
- const aggregatorTasks = rootAggregatorTasksInRepository(ctx, repoDir);
6059
- const found = [];
6060
- const enumeration = enumerateWorkspacePackages(repoDir);
6061
- if (refusals !== void 0) refusals.push(...enumeration.refusals);
6062
- for (const dir of enumeration.packages) {
6063
- const { probe, refusal } = probePkg(ctx, join8(repoDir, dir), aliasFamily, repoDir, aggregatorTasks);
6064
- if (refusal !== null) {
6065
- if (refusals !== void 0) refusals.push(refusal.summary);
6066
- continue;
6067
- }
6068
- if (probe === null) continue;
6069
- if (probe.runner !== null || declaresASuite(probe)) found.push(dir);
6050
+ function setupAnswerProblem(key, raw) {
6051
+ const descriptor = SETUP_QUESTION_DESCRIPTORS[key];
6052
+ if (descriptor === void 0) return `abloh asks no question about ${key}`;
6053
+ const value = raw.trim();
6054
+ if (value === "") return descriptor.required ? "this question needs an answer" : null;
6055
+ if (descriptor.candidates.length > 0 && !descriptor.candidates.includes(value)) {
6056
+ return `${value} is not one of ${descriptor.candidates.join(", ")}`;
6070
6057
  }
6071
- return found;
6072
- }
6073
- function declaresASuite(probe) {
6074
- if (probe.testScript === null) return false;
6075
- const runs = scriptRuns2({ scripts: probe.scripts, text: probe.testScript });
6076
- if (runs.length === 0) return true;
6077
- return runs.some((run) => run.kind === "test" || run.kind === "unknown");
6058
+ if (descriptor.key !== "target.directory" && descriptor.channel === "discovery") return null;
6059
+ const verdict = judgeConfigKeyValue(key, readAnswerValue(descriptor, raw));
6060
+ if (verdict.ok) return null;
6061
+ return verdict.at === null || verdict.at === void 0 ? verdict.reason : `${verdict.at} ${verdict.reason}`;
6078
6062
  }
6079
- function candidateRemedy(repoDir) {
6080
- const refusals = [];
6081
- const candidates = testableWorkspacePackages(repoDir, refusals);
6082
- if (candidates.length === 0) {
6083
- return refusals.length > 0 ? `${refusals[0]}. Declare environment.testCommand in abloh.yml with the command that runs your suite, or run abloh init from the package that holds it` : "declare environment.testCommand in abloh.yml with the command that runs your suite, or run abloh init from the package that holds it";
6063
+ function applyAnswersToContract(draft, answers) {
6064
+ let out = draft;
6065
+ for (const descriptor of Object.values(SETUP_QUESTION_DESCRIPTORS)) {
6066
+ if (descriptor.apply === void 0) continue;
6067
+ const raw = answers[descriptor.key];
6068
+ if (raw === void 0 || raw.trim() === "") continue;
6069
+ out = descriptor.apply(out, raw);
6084
6070
  }
6085
- return `point --subdir (or abloh.yml target.directory) at one of the ${candidates.length} package(s) here that declare a suite, such as ${candidates[0]}`;
6071
+ return out;
6086
6072
  }
6087
- function candidateSentence(repoDir) {
6088
- const candidates = testableWorkspacePackages(repoDir);
6089
- if (candidates.length === 0) return "";
6090
- const named = candidates.slice(0, NAMED_CANDIDATES);
6091
- const more = candidates.length - named.length;
6092
- return `. The packages here that declare a suite are ${named.join(", ")}` + (more > 0 ? ` and ${more} more` : "");
6073
+ var ANSWERED_EVIDENCE = "you answered this when you set abloh up";
6074
+ function evidenceFor(entries) {
6075
+ return entries.map((path) => ({ path, evidence: ANSWERED_EVIDENCE }));
6093
6076
  }
6094
- function detectTarget(repoDir, subdir, resolution = {}, ctx = diskRepoAccess(repoDir)) {
6095
- const aliasFamily = rootAliasFamily(ctx, repoDir);
6096
- const aggregatorTasks = rootAggregatorTasksInRepository(ctx, repoDir);
6097
- const explicitResult = subdir ? probePkg(ctx, join8(repoDir, subdir), aliasFamily, repoDir, aggregatorTasks) : null;
6098
- if (explicitResult?.refusal != null) throw manifestRefusal(explicitResult.refusal);
6099
- const explicit = explicitResult?.probe ?? null;
6100
- if (subdir && explicit === null) {
6101
- throw new TargetNotFoundError(
6102
- "configured-directory-has-no-package",
6103
- `the configured target directory ${JSON.stringify(subdir)} has no package.json, so there is no package there to measure; point --subdir (or abloh.yml target.directory) at the directory holding the package's package.json, or remove it to measure the repository root`,
6104
- "point --subdir (or abloh.yml target.directory) at the directory holding the package's package.json, or remove it to measure the repository root"
6105
- );
6106
- }
6107
- const rootResult = probePkg(ctx, repoDir, aliasFamily, repoDir, aggregatorTasks);
6108
- if (rootResult.refusal !== null) throw manifestRefusal(rootResult.refusal);
6109
- const root = rootResult.probe;
6110
- const probe = explicit !== null && (explicit.runner !== null || explicit.testScript !== null) ? explicit : root !== null && (root.runner !== null || root.testScript !== null) ? root : null;
6111
- if (!probe) {
6112
- throw new TargetNotFoundError(
6113
- "no-testable-package",
6114
- `no package.json with a test runner or test script found under ${repoDir}` + (subdir ? ` or in the configured target directory ${JSON.stringify(subdir)}` : "") + candidateSentence(repoDir),
6115
- candidateRemedy(repoDir)
6116
- );
6117
- }
6118
- const workspaceRootSuite = probe.workspaceRootSuite;
6119
- const commandDir = workspaceRootSuite === null ? probe.dir : repoDir;
6120
- const manager = detectPackageManagerContext(repoDir, commandDir, resolution, ctx);
6121
- const pm = manager.pm;
6122
- const runner = probe.runner ?? "command";
6123
- const projectTestCommand = packageTestCommand(manager);
6124
- const direct = directRunnerCommand(runner, detectModuleLayout(repoDir, probe.dir));
6125
- if (probe.declaredCommand === null && probe.testScript === null && direct !== null && probe.dir === repoDir && declaresWorkspace(ctx, repoDir)) {
6126
- throw new TargetNotFoundError(
6127
- "workspace-root-declares-no-test-script",
6128
- `${repoDir} is a workspace root that declares no test script, so there is no suite here to measure and Abloh will not guess one from an installed runner: point --subdir (or abloh.yml target.directory) at the package you want measured, or declare environment.testCommand if one command runs the suite from the root` + candidateSentence(repoDir),
6129
- "point --subdir (or abloh.yml target.directory) at the package you want measured, or declare environment.testCommand if one command runs the suite from the root" + candidateSentence(repoDir)
6130
- );
6077
+ var SETUP_QUESTION_DESCRIPTORS = {
6078
+ "environment.installCommand": {
6079
+ key: "environment.installCommand",
6080
+ ask: "How does this repository install its dependencies?",
6081
+ why: "Abloh read your CI without running it, so it saw the command your workflow declares and not what your install actually writes. On the borrow road your own job installs and Abloh measures inside it, so this is only needed when Abloh builds for itself.",
6082
+ candidates: [],
6083
+ required: false,
6084
+ unreadable: true,
6085
+ shape: { kind: "scalar" },
6086
+ channel: "contract",
6087
+ apply: (draft, answer) => ({
6088
+ ...draft,
6089
+ installCommand: answer.trim(),
6090
+ evidence: { ...draft.evidence, installCommand: ANSWERED_EVIDENCE }
6091
+ })
6092
+ },
6093
+ "environment.setupCommands": {
6094
+ key: "environment.setupCommands",
6095
+ ask: "What does this repository build between installing and testing?",
6096
+ why: "Abloh read the build steps out of the job it rides. A step it could not read is one the proof container never runs, and a suite that needs it then fails inside abloh rather than in your CI.",
6097
+ candidates: [],
6098
+ required: true,
6099
+ /* ONE STEP PER ` && `, WHICH IS THE ONE KEY THAT IS NOT COMMA-SEPARATED and the CLI's prompt says
6100
+ so for the same reason: a comma is an ordinary character inside a command, and a build is read
6101
+ top to bottom. `splitAnswerList` is the one grammar both roads ask. */
6102
+ shape: { kind: "commands" },
6103
+ channel: "contract",
6104
+ apply: (draft, answer) => ({
6105
+ ...draft,
6106
+ setupCommands: splitAnswerList({ kind: "commands" }, answer).map((command) => ({
6107
+ command,
6108
+ source: ANSWERED_EVIDENCE,
6109
+ /* A COMMAND SOMEBODY TYPED CARRIES THE ENVIRONMENT THEY TYPED INTO IT. `CiSetupCommand.env`
6110
+ is the environment abloh READ off a workflow step, so an answered command has none: what
6111
+ the answer says is the whole of what this command runs. */
6112
+ env: []
6113
+ }))
6114
+ })
6115
+ },
6116
+ "environment.runtimeImage": {
6117
+ key: "environment.runtimeImage",
6118
+ ask: "Which image should Abloh build its proof container from?",
6119
+ why: "Abloh pins one by pulling it and reading its digest, which needs a machine. Left unanswered, a run uses the reviewed default for the Node your repository declares.",
6120
+ candidates: [],
6121
+ required: false,
6122
+ unreadable: true,
6123
+ shape: { kind: "scalar" },
6124
+ channel: "contract",
6125
+ apply: (draft, answer) => ({
6126
+ ...draft,
6127
+ runtimeImage: answer.trim(),
6128
+ evidence: { ...draft.evidence, runtimeImage: ANSWERED_EVIDENCE }
6129
+ })
6130
+ },
6131
+ "setup.job": {
6132
+ key: "setup.job",
6133
+ ask: "Which job should abloh measure in?",
6134
+ why: "Everything else in the contract is read out of that one job.",
6135
+ candidates: [],
6136
+ required: true,
6137
+ shape: { kind: "scalar" },
6138
+ channel: "discovery"
6139
+ },
6140
+ "setup.matrixLeg": {
6141
+ key: "setup.matrixLeg",
6142
+ ask: "Which leg of that job's matrix?",
6143
+ why: "The job abloh rides fans out, and a leg names one value per dimension of it.",
6144
+ candidates: [],
6145
+ required: true,
6146
+ shape: { kind: "scalar" },
6147
+ channel: "discovery"
6148
+ },
6149
+ "setup.failFast": {
6150
+ key: "setup.failFast",
6151
+ ask: "That job cancels every matrix leg when one fails. May abloh turn that off for it?",
6152
+ /* THE VALUES THE CONTRACT ACCEPTS, on `flaky`'s own rule: the card writes the chosen candidate
6153
+ into `abloh.yml`, so a candidate that read well and parsed badly would be a form that writes an
6154
+ invalid file. What each one MEANS is the ask above it. */
6155
+ why: SETUP_FAIL_FAST_ANSWERS.map((answer) => `${answer} ${SETUP_FAIL_FAST_CONSEQUENCES[answer]}.`).join(" "),
6156
+ candidates: SETUP_FAIL_FAST_ANSWERS,
6157
+ required: true,
6158
+ shape: { kind: "scalar" },
6159
+ /* A LINE IN THE FILE AND NOT A RE-DERIVATION. It does not move which job the recipe is read out
6160
+ of - it is the maintainer's standing answer about the job already chosen - and
6161
+ `configSetupJobLines` writes it on every road, with or without a job, because it is the one
6162
+ statement in that block that was never abloh's to discard. */
6163
+ channel: "contract",
6164
+ apply: (draft, answer) => ({ ...draft, setupFailFast: answer.trim() })
6165
+ },
6166
+ flaky: {
6167
+ key: "flaky",
6168
+ ask: "Your suite was red on untouched code. What should abloh do about those tests?",
6169
+ /* THE VALUES THE CONTRACT ACCEPTS, not a prose label: the card writes the chosen candidate into
6170
+ `abloh.yml`, so a candidate that read well and parsed badly would be a form that writes an
6171
+ invalid file. What each one MEANS is the ask above it. */
6172
+ why: "quarantine sets them aside and measures the rest. strict measures nothing until they pass.",
6173
+ candidates: ["quarantine", "strict"],
6174
+ required: true,
6175
+ shape: { kind: "scalar" },
6176
+ channel: "contract",
6177
+ /* `quarantine` WRITES NO LINE and that is not the answer being dropped: it is what a file with
6178
+ no `flaky:` key already means, which `configFlakyLines` states at the renderer. */
6179
+ apply: (draft, answer) => ({ ...draft, flaky: answer.trim() })
6180
+ },
6181
+ "environment.testCommand": {
6182
+ key: "environment.testCommand",
6183
+ ask: "Which command runs your tests?",
6184
+ why: "Abloh read your CI without running it, and the command it found did not run a suite it could observe.",
6185
+ candidates: [],
6186
+ required: true,
6187
+ shape: { kind: "scalar" },
6188
+ channel: "contract",
6189
+ apply: (draft, answer) => ({ ...draft, testCommand: answer.trim() })
6190
+ },
6191
+ "environment.sealedTestCommand": {
6192
+ key: "environment.sealedTestCommand",
6193
+ ask: "Which command runs your tests inside abloh's proof container?",
6194
+ why: "Your own test script did something abloh's container could not, so it needs the runner on its own.",
6195
+ candidates: [],
6196
+ required: true,
6197
+ shape: { kind: "scalar" },
6198
+ channel: "contract",
6199
+ apply: (draft, answer) => ({ ...draft, sealedTestCommand: answer.trim() })
6200
+ },
6201
+ "environment.rebuildCommand": {
6202
+ key: "environment.rebuildCommand",
6203
+ ask: "Which command builds the output your tests import?",
6204
+ why: "Your tests read compiled output rather than the source abloh plants in, so without the build every planted bug is reported as missed. Abloh cannot resolve which of your build scripts writes it from your files alone.",
6205
+ candidates: [],
6206
+ required: true,
6207
+ shape: { kind: "scalar" },
6208
+ channel: "contract",
6209
+ /* THE CANDIDATES AND THE MANAGER SURVIVE THE ANSWER, because they are the drafting's reading of
6210
+ this repository and the answer is a value: a run that later clears the value would otherwise
6211
+ lose the finding that made the key worth writing. Absent, they are the empty pair, which is
6212
+ what a draft that found no build script already carries. */
6213
+ apply: (draft, answer) => ({
6214
+ ...draft,
6215
+ rebuild: {
6216
+ ...draft.rebuild ?? { candidates: [], manager: "" },
6217
+ value: answer.trim(),
6218
+ evidence: ANSWERED_EVIDENCE
6219
+ }
6220
+ })
6221
+ },
6222
+ "target.directory": {
6223
+ key: "target.directory",
6224
+ ask: "Which package should abloh measure?",
6225
+ why: "This repository holds several, and a run measures one.",
6226
+ candidates: [],
6227
+ required: true,
6228
+ shape: { kind: "scalar" },
6229
+ /* A DISCOVERY ANSWER AND NOT A LINE. The runner, the test command and the whole measurement plan
6230
+ are read out of the package being measured, so writing this key over a draft derived from the
6231
+ root would leave every sibling value describing a different package. It goes back through
6232
+ `resolveMeasurementPlan` as its `subdir`, exactly as the job pin goes back through the job. */
6233
+ channel: "discovery"
6234
+ },
6235
+ "environment.coverageProvider": {
6236
+ key: "environment.coverageProvider",
6237
+ ask: "May abloh install the coverage provider your runner needs?",
6238
+ why: "Your tree does not carry one, and without it abloh cannot say which of your changed lines your tests execute.",
6239
+ candidates: ["install", "skip"],
6240
+ required: true,
6241
+ shape: { kind: "scalar" },
6242
+ channel: "contract",
6243
+ /* WRITTEN FOR BOTH ANSWERS, because a recorded answer is the difference between a maintainer
6244
+ who chose the default and one who was never asked - see `ConfigInput.coverageProvider`. */
6245
+ apply: (draft, answer) => ({ ...draft, coverageProvider: answer.trim() })
6246
+ },
6247
+ "environment.requiredVariables": {
6248
+ key: "environment.requiredVariables",
6249
+ ask: "Which environment variables does your suite need?",
6250
+ why: "Names only. The values come from your own environment and are masked in every log.",
6251
+ candidates: [],
6252
+ required: true,
6253
+ shape: { kind: "names" },
6254
+ channel: "contract",
6255
+ apply: (draft, answer) => ({
6256
+ ...draft,
6257
+ requiredVariables: splitAnswerList({ kind: "names" }, answer).map((name) => ({
6258
+ name,
6259
+ evidence: ANSWERED_EVIDENCE
6260
+ }))
6261
+ })
6262
+ },
6263
+ "environment.environmentValues": {
6264
+ key: "environment.environmentValues",
6265
+ ask: "Which literal values does your suite need in its environment?",
6266
+ why: "A secret goes in requiredVariables by name instead, and never here.",
6267
+ candidates: [],
6268
+ required: true,
6269
+ shape: { kind: "pairs" },
6270
+ channel: "contract",
6271
+ apply: (draft, answer) => ({
6272
+ ...draft,
6273
+ environmentValues: splitAnswerList({ kind: "pairs" }, answer).map((entry) => {
6274
+ const [name, value] = splitAtFirstEquals(entry);
6275
+ return { name, value: value ?? "", evidence: ANSWERED_EVIDENCE };
6276
+ })
6277
+ })
6278
+ },
6279
+ "environment.generatedFiles": {
6280
+ key: "environment.generatedFiles",
6281
+ ask: "Which directories does your own tooling write during a run?",
6282
+ why: "An undeclared change to the checkout aborts a run, and abloh cannot tell yours from a stray one.",
6283
+ candidates: [],
6284
+ required: true,
6285
+ shape: { kind: "list" },
6286
+ channel: "contract",
6287
+ apply: (draft, answer) => ({
6288
+ ...draft,
6289
+ generatedFiles: evidenceFor(splitAnswerList({ kind: "list" }, answer))
6290
+ })
6291
+ },
6292
+ "environment.rewrittenFiles": {
6293
+ key: "environment.rewrittenFiles",
6294
+ ask: "Which committed files does your own install rewrite?",
6295
+ why: "A rate measured over bytes your install changed is a rate about something else.",
6296
+ candidates: [],
6297
+ required: true,
6298
+ shape: { kind: "list" },
6299
+ channel: "contract",
6300
+ apply: (draft, answer) => ({
6301
+ ...draft,
6302
+ rewrittenFiles: evidenceFor(splitAnswerList({ kind: "list" }, answer))
6303
+ })
6304
+ },
6305
+ "environment.identityFiles": {
6306
+ key: "environment.identityFiles",
6307
+ ask: "Which files should a change to reinstall rather than reuse the container?",
6308
+ why: "Abloh reuses a prepared container between runs unless one of these moved.",
6309
+ candidates: [],
6310
+ required: true,
6311
+ shape: { kind: "list" },
6312
+ channel: "contract",
6313
+ apply: (draft, answer) => ({
6314
+ ...draft,
6315
+ identityFiles: splitAnswerList({ kind: "list" }, answer)
6316
+ })
6317
+ },
6318
+ "environment.systemPackages": {
6319
+ key: "environment.systemPackages",
6320
+ ask: "Which OS packages does your build need?",
6321
+ why: "The reviewed proof image is slim, so a native dependency that compiles on install needs its toolchain named.",
6322
+ candidates: [],
6323
+ required: true,
6324
+ shape: { kind: "packages" },
6325
+ channel: "contract",
6326
+ apply: (draft, answer) => ({
6327
+ ...draft,
6328
+ systemPackages: splitAnswerList({ kind: "packages" }, answer).map((entry) => {
6329
+ const [name, version] = splitAtFirstEquals(entry);
6330
+ return { name, version, source: ANSWERED_EVIDENCE };
6331
+ })
6332
+ })
6333
+ },
6334
+ "environment.runtimes": {
6335
+ key: "environment.runtimes",
6336
+ ask: "Which runtimes beyond Node does your suite need?",
6337
+ /* NO CLOSED CANDIDATE LIST, because this key takes SEVERAL: `setupAnswerProblem` compares a
6338
+ closed list against the WHOLE answer, so `bun, deno` would be refused by a two-word list that
6339
+ accepts each of them alone. `judgeConfigKeyValue` names the two the contract holds, entry by
6340
+ entry, which is the reading that fits a list. */
6341
+ why: "The proof image carries Node. A runtime your tests reach for and the image does not carry fails inside abloh rather than in your CI.",
6342
+ candidates: [],
6343
+ required: true,
6344
+ shape: { kind: "names" },
6345
+ channel: "contract",
6346
+ apply: (draft, answer) => ({
6347
+ ...draft,
6348
+ runtimes: splitAnswerList({ kind: "names" }, answer).map((runtime) => ({
6349
+ runtime,
6350
+ evidence: ANSWERED_EVIDENCE
6351
+ }))
6352
+ })
6353
+ },
6354
+ "environment.services": {
6355
+ key: "environment.services",
6356
+ ask: "Which backing services does your suite need, and where is each one ready?",
6357
+ why: "A proof runs in the same network namespace as its services, so your suite reaches them on localhost exactly as in your CI. Abloh pins the image by digest because a mutable tag cannot be part of what a run binds, and each service needs something to wait on before the suite starts.",
6358
+ candidates: [],
6359
+ required: true,
6360
+ shape: { kind: "services" },
6361
+ channel: "contract",
6362
+ /* THE ENVIRONMENT IS NOT TYPED HERE and the entry says so at {@link SERVICE_ENTRY}: an image that
6363
+ needs credentials fails its readiness gate and reports that, which is the same narrow door the
6364
+ CLI's own typed entry goes through. */
6365
+ apply: (draft, answer) => ({
6366
+ ...draft,
6367
+ services: readServiceEntries(answer).map((service) => ({
6368
+ name: service.name,
6369
+ spelling: `${service.name}=${service.image}`,
6370
+ image: service.image,
6371
+ env: [],
6372
+ healthCommand: null,
6373
+ healthPort: service.healthPort ?? null,
6374
+ source: ANSWERED_EVIDENCE,
6375
+ evidence: ANSWERED_EVIDENCE
6376
+ }))
6377
+ })
6378
+ },
6379
+ "environment.installDirectory": {
6380
+ key: "environment.installDirectory",
6381
+ ask: "Which directory owns the lockfile your install reads?",
6382
+ why: "A workspace package installs from the root that holds its lockfile, not from its own folder.",
6383
+ candidates: [],
6384
+ required: false,
6385
+ shape: { kind: "scalar" },
6386
+ channel: "contract",
6387
+ apply: (draft, answer) => ({ ...draft, installDirectory: answer.trim() })
6388
+ },
6389
+ "environment.runner": {
6390
+ key: "environment.runner",
6391
+ ask: "Which test runner does this repository use?",
6392
+ why: "Abloh reads it from your own command, and a declaration outranks that reading.",
6393
+ candidates: [],
6394
+ required: false,
6395
+ shape: { kind: "scalar" },
6396
+ channel: "contract",
6397
+ apply: (draft, answer) => ({
6398
+ ...draft,
6399
+ runner: answer.trim(),
6400
+ boundRunner: answer.trim(),
6401
+ evidence: { ...draft.evidence, runner: "declared in abloh.yml" }
6402
+ })
6131
6403
  }
6132
- const testCommand = probe.declaredCommand ?? (probe.testScript !== null || direct === null ? projectTestCommand : direct);
6133
- const rel = relative5(repoDir, probe.dir);
6134
- const resolvedSubdir = rel === "" ? null : rel;
6135
- const lifecycleHooks = [probe.scripts.pretest, probe.scripts.posttest].filter(
6136
- (hook) => typeof hook === "string"
6404
+ };
6405
+ var ANSWERABLE_SETUP_KEYS = Object.keys(SETUP_QUESTION_DESCRIPTORS);
6406
+ function contractDescriptors() {
6407
+ return Object.values(SETUP_QUESTION_DESCRIPTORS).filter(
6408
+ (descriptor) => descriptor.channel === "contract"
6137
6409
  );
6138
- const runnerStage = probe.testScript !== null && testCommand === projectTestCommand ? runnerStageCommand(probe.testScript, runner, lifecycleHooks, probe.scripts) : null;
6139
- const preRunnerStages = probe.testScript !== null && testCommand === projectTestCommand ? preRunnerStageCommands(probe.testScript, runner, probe.scripts) : [];
6140
- const watchDeclaration = testCommand === projectTestCommand ? probe.testScript : testCommand;
6141
- if (watchDeclaration !== null) {
6142
- const refusal = watchModeRefusal(
6143
- runner,
6144
- watchDeclaration,
6145
- testCommand === projectTestCommand ? `scripts.test in ${join8(relative5(repoDir, probe.dir), "package.json")}` : probe.declaredCommand === testCommand ? "your declared environment.testCommand" : "your test command"
6146
- );
6147
- if (refusal !== null) throw new TargetDetectionError(refusal);
6148
- }
6149
- const watchNormalized = watchDeclaration === null ? null : withWatchModeDisabled({
6150
- runner,
6151
- command: testCommand,
6152
- watchDeclaration,
6153
- packageManagerMajor: manager.version === null ? null : Number.parseInt(manager.version, 10)
6154
- });
6155
- const effectiveTestCommand = watchNormalized ?? testCommand;
6156
- const watchNormalizedStage = runnerStage === null ? null : withWatchModeDisabled({ runner, command: runnerStage, watchDeclaration: runnerStage });
6157
- const invocation = RUNNER_INVOCATION_CARRYING_RUNNERS.has(runner) ? runnerInvocation(probe.testScript, runner, probe.scripts) ?? probe.declaredCommand ?? (testCommand === direct ? direct : null) : null;
6158
- return {
6159
- pm,
6160
- runner,
6161
- testCommand: effectiveTestCommand,
6162
- ...watchNormalized === null ? {} : { watchModeNormalized: true },
6163
- ...probe.aggregator === null ? {} : { workspaceAggregator: probe.aggregator },
6164
- ...probe.aggregatorTask == null ? {} : { aggregatorTask: probe.aggregatorTask },
6165
- ...runnerStage === null ? {} : { runnerStageCommand: watchNormalizedStage ?? runnerStage },
6166
- /* Only alongside a runner stage, because these are exactly what running that stage alone would
6167
- otherwise skip. A one-stage script gated by a lifecycle hook has a runner stage and no
6168
- earlier stage of its own, and gets an empty list rather than a key. */
6169
- ...runnerStage === null || preRunnerStages.length === 0 ? {} : { preRunnerStageCommands: preRunnerStages },
6170
- ...invocation === null ? {} : { runnerInvocation: invocation },
6171
- vitestDir: runner === "vitest" ? detectVitestDir(probe.dir, ctx) : void 0,
6172
- jasmineSuite: runner === "jasmine" ? detectJasmineSuite(probe.dir, probe.testScript, ctx) : void 0,
6173
- ...probe.angular === null ? {} : { angularTarget: probe.angular },
6174
- subdir: resolvedSubdir,
6175
- workDir: commandDir,
6176
- ...workspaceRootSuite === null ? {} : { workspaceRootSuite },
6177
- ...probe.note ? { identifiedRunner: probe.note } : {},
6178
- ...compiledOutputFields({
6179
- ctx,
6180
- repoDir,
6181
- packageDir: resolvedSubdir,
6182
- runner,
6183
- invocation,
6184
- testCommand: effectiveTestCommand,
6185
- scripts: probe.scripts,
6186
- manager: pm,
6187
- angular: probe.angular !== null
6188
- }),
6189
- ...runner === "command" ? suiteFanoutPlan(probe, manager) : {}
6190
- };
6410
+ }
6411
+ function answeredTargetDirectory(answers) {
6412
+ const value = answers["target.directory"];
6413
+ return value === void 0 || value.trim() === "" ? void 0 : value.trim();
6191
6414
  }
6192
6415
 
6193
6416
  // src/measurement-plan-resolver.ts
6194
6417
  import { readFileSync as readFileSync6 } from "fs";
6195
6418
  import { createHash } from "crypto";
6196
- import { join as join9, relative as relative6 } from "path";
6419
+ import { join as join10, relative as relative6 } from "path";
6197
6420
  import {
6198
6421
  MEASUREMENT_PLAN_SCHEMA,
6199
6422
  EVIDENCE_OUTPUTS,
@@ -6203,7 +6426,7 @@ import {
6203
6426
  resolvePreparedTestCommand
6204
6427
  } from "@abloh/core";
6205
6428
  function policyDeclaration(repoDir, ctx = diskRepoAccess(repoDir)) {
6206
- const policyPath = join9(repoDir, "abloh.yml");
6429
+ const policyPath = join10(repoDir, "abloh.yml");
6207
6430
  if (!repoFileExists(ctx, policyPath)) {
6208
6431
  return { directory: null, testCommand: void 0, runner: void 0 };
6209
6432
  }
@@ -6296,7 +6519,7 @@ function resolveMeasurementPlan(input) {
6296
6519
  });
6297
6520
  const declaration = declarationOf(repoDir, input.declaration, ctx);
6298
6521
  const workDir = relative6(repoDir, detection.workDir) || ".";
6299
- const policyPath = join9(repoDir, "abloh.yml");
6522
+ const policyPath = join10(repoDir, "abloh.yml");
6300
6523
  const environmentPolicy = input.declaration !== void 0 && input.declaration.source === "supplied" ? input.declaration.environment ?? null : repoFileExists(ctx, policyPath) ? tryLoadPolicy(policyPath)?.environment ?? null : null;
6301
6524
  const command = resolvePreparedTestCommand({
6302
6525
  ...input.cliTestCommand === void 0 ? {} : { cli: input.cliTestCommand },
@@ -6309,27 +6532,27 @@ function resolveMeasurementPlan(input) {
6309
6532
  bindings.push({ path: "abloh.yml", digest: digestOf(ctx, policyPath), kind: "policy" });
6310
6533
  }
6311
6534
  for (const name of PLAN_MANIFEST_FILES) {
6312
- const path = join9(detection.workDir, name);
6535
+ const path = join10(detection.workDir, name);
6313
6536
  if (repoFileExists(ctx, path)) {
6314
6537
  bindings.push({
6315
- path: join9(workDir === "." ? "" : workDir, name),
6538
+ path: join10(workDir === "." ? "" : workDir, name),
6316
6539
  digest: digestOf(ctx, path),
6317
6540
  kind: "manifest"
6318
6541
  });
6319
6542
  }
6320
6543
  }
6321
6544
  for (const name of PLAN_LOCKFILES) {
6322
- const path = join9(repoDir, name);
6545
+ const path = join10(repoDir, name);
6323
6546
  if (repoFileExists(ctx, path)) {
6324
6547
  bindings.push({ path: name, digest: digestOf(ctx, path), kind: "lockfile" });
6325
6548
  }
6326
6549
  }
6327
6550
  const setupPath = environmentPolicy?.setup ?? null;
6328
6551
  if (setupPath !== null) {
6329
- bindings.push({ path: setupPath, digest: digestOf(ctx, join9(repoDir, setupPath)), kind: "setup" });
6552
+ bindings.push({ path: setupPath, digest: digestOf(ctx, join10(repoDir, setupPath)), kind: "setup" });
6330
6553
  }
6331
6554
  for (const name of environmentPolicy?.identityFiles ?? []) {
6332
- bindings.push({ path: name, digest: digestOf(ctx, join9(repoDir, name)), kind: "identity" });
6555
+ bindings.push({ path: name, digest: digestOf(ctx, join10(repoDir, name)), kind: "identity" });
6333
6556
  }
6334
6557
  bindings.sort(
6335
6558
  (left, right) => left.path === right.path ? left.kind.localeCompare(right.kind) : left.path.localeCompare(right.path)
@@ -6368,7 +6591,7 @@ function resolveMeasurementPlan(input) {
6368
6591
  systemPackages: environmentPolicy?.systemPackages ?? []
6369
6592
  },
6370
6593
  setup: {
6371
- script: setupPath === null ? null : { path: setupPath, digest: digestOf(ctx, join9(repoDir, setupPath)) },
6594
+ script: setupPath === null ? null : { path: setupPath, digest: digestOf(ctx, join10(repoDir, setupPath)) },
6372
6595
  installCommand: environmentPolicy?.installCommand ?? null,
6373
6596
  installDirectory: environmentPolicy?.installDirectory ?? ".",
6374
6597
  setupCommands: environmentPolicy?.setupCommands ?? [],
@@ -6506,7 +6729,7 @@ function nodeDeclarationProblemReason(rejection) {
6506
6729
  }
6507
6730
 
6508
6731
  // src/setup-job.ts
6509
- import { join as join10 } from "path";
6732
+ import { join as join11 } from "path";
6510
6733
  import {
6511
6734
  filterSetupCandidates,
6512
6735
  jobOnMatrixLeg,
@@ -6562,7 +6785,7 @@ function declaredSetupJob(repoDir) {
6562
6785
  return declaredSetupPin(repoDir).job;
6563
6786
  }
6564
6787
  function declaredSetupPin(repoDir, ctx = diskRepoAccess(repoDir)) {
6565
- const text = accessText(ctx, join10(repoDir, "abloh.yml"));
6788
+ const text = accessText(ctx, join11(repoDir, "abloh.yml"));
6566
6789
  if (text === null) return {};
6567
6790
  const reading = readSetupPinFromDocument(text);
6568
6791
  if (reading.kind === "absent") return {};
@@ -7278,6 +7501,20 @@ function failFastInsertion(lines, job) {
7278
7501
  }
7279
7502
  return null;
7280
7503
  }
7504
+ function workflowRunsAbloh(text) {
7505
+ let document;
7506
+ try {
7507
+ document = parse3(text);
7508
+ } catch {
7509
+ return false;
7510
+ }
7511
+ const jobs = document?.jobs;
7512
+ if (typeof jobs !== "object" || jobs === null) return false;
7513
+ return Object.values(jobs).some((job) => {
7514
+ const steps = job?.steps;
7515
+ return Array.isArray(steps) && alreadyEndsInAblohStep(steps);
7516
+ });
7517
+ }
7281
7518
  function alreadyEndsInAblohStep(steps) {
7282
7519
  const repository = SETUP_STEP_ACTION.split("@")[0] ?? SETUP_STEP_ACTION;
7283
7520
  return steps.some(
@@ -7593,6 +7830,7 @@ export {
7593
7830
  DENO_LOCKFILE,
7594
7831
  ENVIRONMENT_BLOCK_HEADER,
7595
7832
  InitRefusal,
7833
+ RETIRED_ENVIRONMENT_BLOCK_HEADERS,
7596
7834
  RUSH_CONFIG_DIRECTORY,
7597
7835
  RUSH_CONFIG_FILE,
7598
7836
  RUSH_INSTALL_COMMAND,
@@ -7616,6 +7854,7 @@ export {
7616
7854
  accessSubdirectories,
7617
7855
  accessText,
7618
7856
  admitSubmodules,
7857
+ aggregatorChain,
7619
7858
  aggregatorInvocation,
7620
7859
  aggregatorTaskPhrase,
7621
7860
  aliasHopFamily,
@@ -7670,12 +7909,14 @@ export {
7670
7909
  initStepRole,
7671
7910
  invokesNodeTest,
7672
7911
  isBrowserInstallCommand,
7912
+ isEnvironmentBlockHeader,
7673
7913
  isInstallStep,
7674
7914
  jasmineSuiteFromScript,
7675
7915
  layerEnvironment,
7676
7916
  materializeSubmodule,
7677
7917
  nodeDeclarationProblemReason,
7678
7918
  nodeSpecVersions,
7919
+ overlayRepoAccess,
7679
7920
  placeSetupStep,
7680
7921
  policyDeclaration,
7681
7922
  preRunnerStageCommands,
@@ -7721,5 +7962,6 @@ export {
7721
7962
  unreadableConditionLine,
7722
7963
  vitestDirIn,
7723
7964
  withoutPackageRunnerPrefix,
7724
- workDirPrefix
7965
+ workDirPrefix,
7966
+ workflowRunsAbloh
7725
7967
  };