simulci 0.1.0 → 0.2.0

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 (2) hide show
  1. package/dist/index.js +436 -250
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
- import { basename, dirname, isAbsolute, join, posix, relative, resolve } from "node:path";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
5
5
  import { parseArgs } from "node:util";
6
6
  //#region \0rolldown/runtime.js
@@ -1122,7 +1122,7 @@ var Parser = class {
1122
1122
  }
1123
1123
  };
1124
1124
  //#endregion
1125
- //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+852776fb87daeb7010eafbce3cda467d415ec70f/node_modules/@ciatlas/core/src/expressions.ts
1125
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/expressions.ts
1126
1126
  /** Every named context the GitHub Actions runtime knows about. */
1127
1127
  const KNOWN_CONTEXTS = [
1128
1128
  "github",
@@ -1667,7 +1667,7 @@ function evaluateTemplate(text, resolve, options = {}) {
1667
1667
  return known$2(out, mergeTaint(...parts));
1668
1668
  }
1669
1669
  //#endregion
1670
- //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+852776fb87daeb7010eafbce3cda467d415ec70f/node_modules/@ciatlas/core/src/ir.ts
1670
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/ir.ts
1671
1671
  /** Case-insensitive lookup of an own key, matching how GitHub resolves context property names. */
1672
1672
  function lookup(rec, key) {
1673
1673
  if (Object.hasOwn(rec, key)) return rec[key];
@@ -1896,7 +1896,7 @@ function matrixResolver(combo) {
1896
1896
  };
1897
1897
  }
1898
1898
  //#endregion
1899
- //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+852776fb87daeb7010eafbce3cda467d415ec70f/node_modules/@ciatlas/core/src/checks.ts
1899
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/checks.ts
1900
1900
  /**
1901
1901
  * Check-run names of jobs, as GitHub reports them (and as branch protection requires them).
1902
1902
  *
@@ -2029,7 +2029,7 @@ function skippedCheckName(job) {
2029
2029
  return kind.kind === "static" ? kind.text || job.id : kind.template.trim();
2030
2030
  }
2031
2031
  //#endregion
2032
- //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+852776fb87daeb7010eafbce3cda467d415ec70f/node_modules/@ciatlas/core/src/text.ts
2032
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/text.ts
2033
2033
  /**
2034
2034
  * `s` without runs of `ch` at its end (and at its start with `start: true`). A loop rather than `/x+$/`, which
2035
2035
  * backtracks quadratically on long runs that are not at the end (CodeQL js/polynomial-redos).
@@ -2066,7 +2066,7 @@ function didYouMean(name, candidates) {
2066
2066
  return best.d <= Math.max(2, Math.floor(name.length / 3)) ? best.c : void 0;
2067
2067
  }
2068
2068
  //#endregion
2069
- //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+852776fb87daeb7010eafbce3cda467d415ec70f/node_modules/@ciatlas/core/src/glob.ts
2069
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/glob.ts
2070
2070
  /** Minimal glob: `*` matches within a segment, `**` across segments; plain strings match as path prefixes. */
2071
2071
  function matchesPattern(path, pattern) {
2072
2072
  if (!pattern.includes("*")) return path === pattern || path.startsWith(pattern.endsWith("/") ? pattern : `${pattern}/`);
@@ -2074,7 +2074,7 @@ function matchesPattern(path, pattern) {
2074
2074
  }
2075
2075
  const escapeRe = (s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
2076
2076
  //#endregion
2077
- //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+852776fb87daeb7010eafbce3cda467d415ec70f/node_modules/@ciatlas/core/src/queries.ts
2077
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/queries.ts
2078
2078
  /** Every reference in a unit's expressions, with the site it is in. */
2079
2079
  function* refsOf(unit) {
2080
2080
  for (const site of unit.sites) for (const seg of site.segments) for (const ref of seg.refs) yield {
@@ -2085,7 +2085,7 @@ function* refsOf(unit) {
2085
2085
  /** Secrets every workflow can read without declaring them (lowercase): the automatic `GITHUB_TOKEN`. */
2086
2086
  const BUILTIN_SECRETS = /* @__PURE__ */ new Set(["github_token"]);
2087
2087
  //#endregion
2088
- //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+852776fb87daeb7010eafbce3cda467d415ec70f/node_modules/@ciatlas/core/src/graph.ts
2088
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/graph.ts
2089
2089
  /** A path segment that names no key: the end of the path, a `*` filter or a `?` computed index. */
2090
2090
  const computed = (s) => s === void 0 || s === "*" || s === "?";
2091
2091
  /** Whether a path segment names the property `key` (lowercase): GitHub looks properties up ignoring case. */
@@ -9491,7 +9491,7 @@ var require_public_api = /* @__PURE__ */ __commonJSMin(((exports) => {
9491
9491
  exports.stringify = stringify;
9492
9492
  }));
9493
9493
  //#endregion
9494
- //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+852776fb87daeb7010eafbce3cda467d415ec70f/node_modules/@ciatlas/core/src/source.ts
9494
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/source.ts
9495
9495
  var import_dist = (/* @__PURE__ */ __commonJSMin(((exports) => {
9496
9496
  var composer = require_composer();
9497
9497
  var Document = require_Document();
@@ -9566,7 +9566,7 @@ function formatLoc(loc) {
9566
9566
  return `${loc.file}:${loc.line}:${loc.column}`;
9567
9567
  }
9568
9568
  //#endregion
9569
- //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+852776fb87daeb7010eafbce3cda467d415ec70f/node_modules/@ciatlas/core/src/logger.ts
9569
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/logger.ts
9570
9570
  const ORDER = {
9571
9571
  silent: 0,
9572
9572
  error: 1,
@@ -9631,7 +9631,45 @@ function createLogger(options = {}) {
9631
9631
  }
9632
9632
  const silentLogger = createLogger({ level: "silent" });
9633
9633
  //#endregion
9634
- //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+852776fb87daeb7010eafbce3cda467d415ec70f/node_modules/@ciatlas/core/src/parse.ts
9634
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/posix.ts
9635
+ /**
9636
+ * POSIX path arithmetic for repository-relative paths, with Node's `path.posix` semantics for what the parser and the
9637
+ * loader need, so the browser entry carries no `node:path`.
9638
+ */
9639
+ const posixIsAbsolute = (path) => path.startsWith("/");
9640
+ /** `a/./b/../c` → `a/c`, `` → `.`, `/..` → `/`; a trailing slash is kept, as `path.posix.normalize` keeps it. */
9641
+ function posixNormalize(path) {
9642
+ if (path === "") return ".";
9643
+ const absolute = path.startsWith("/");
9644
+ const trailing = path.endsWith("/");
9645
+ const out = [];
9646
+ for (const segment of path.split("/")) step(out, segment, absolute);
9647
+ if (out.length === 0) return absolute ? "/" : trailing ? "./" : ".";
9648
+ return `${absolute ? "/" : ""}${out.join("/")}${trailing ? "/" : ""}`;
9649
+ }
9650
+ /** One segment onto the normalized path: `.` and empty ones vanish, `..` pops (or stays, when relative and nothing is above). */
9651
+ function step(out, segment, absolute) {
9652
+ if (segment === "" || segment === ".") return;
9653
+ if (segment !== "..") {
9654
+ out.push(segment);
9655
+ return;
9656
+ }
9657
+ if (out.length && out.at(-1) !== "..") out.pop();
9658
+ else if (!absolute) out.push("..");
9659
+ }
9660
+ /** The non-empty parts joined and normalized; `.` when there is nothing. */
9661
+ function posixJoin(...parts) {
9662
+ const joined = parts.filter((p) => p !== "").join("/");
9663
+ return joined === "" ? "." : posixNormalize(joined);
9664
+ }
9665
+ /** The last segment, trailing slashes ignored; `` for `/` and ``. */
9666
+ function posixBasename(path) {
9667
+ const trimmed = path.replace(/\/+$/, "");
9668
+ const i = trimmed.lastIndexOf("/");
9669
+ return i === -1 ? trimmed : trimmed.slice(i + 1);
9670
+ }
9671
+ //#endregion
9672
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/parse.ts
9635
9673
  function createParseContext(repository) {
9636
9674
  let id = 0;
9637
9675
  return {
@@ -10249,7 +10287,7 @@ function outputDecls(unit, node, base, valueKey) {
10249
10287
  const WORKFLOW_FILE = /^\.github\/workflows\/[^/]+\.ya?ml$/;
10250
10288
  /** `a/./b/` → `a/b`, `` → `.`. Anything escaping the root (`../x`) keeps its `..` and is never read. */
10251
10289
  function normalizeRelative(path) {
10252
- return trimChar(posix.normalize(trimChar(path, "/") || "."), "/") || ".";
10290
+ return trimChar(posixNormalize(trimChar(path, "/") || "."), "/") || ".";
10253
10291
  }
10254
10292
  function classifyUses(raw, at, loc, ctx) {
10255
10293
  const value = raw.trim();
@@ -15117,7 +15155,9 @@ function parseWorkflow(entryFile, contextOrTrace) {
15117
15155
  value: readTemplate(context, WORKFLOW_ROOT, reader, fileId)
15118
15156
  };
15119
15157
  }
15120
- (/* @__PURE__ */ __commonJSMin(((exports, module) => {
15158
+ //#endregion
15159
+ //#region ../../node_modules/.pnpm/cronstrue@2.59.0/node_modules/cronstrue/dist/cronstrue.js
15160
+ var require_cronstrue = /* @__PURE__ */ __commonJSMin(((exports, module) => {
15121
15161
  (function webpackUniversalModuleDefinition(root, factory) {
15122
15162
  if (typeof exports === "object" && typeof module === "object") module.exports = factory();
15123
15163
  else if (typeof define === "function" && define.amd) define("cronstrue", [], factory);
@@ -15977,7 +16017,9 @@ function parseWorkflow(entryFile, contextOrTrace) {
15977
16017
  return __webpack_exports__;
15978
16018
  })();
15979
16019
  });
15980
- })))();
16020
+ }));
16021
+ //#endregion
16022
+ //#region ../../node_modules/.pnpm/@actions+workflow-parser@0.3.61/node_modules/@actions/workflow-parser/dist/model/converter/cron-constants.js
15981
16023
  const MONTHS = {
15982
16024
  jan: 1,
15983
16025
  feb: 2,
@@ -16023,8 +16065,7 @@ const DOW_RANGE = {
16023
16065
  max: 6,
16024
16066
  names: DAYS
16025
16067
  };
16026
- //#endregion
16027
- //#region ../../node_modules/.pnpm/@actions+workflow-parser@0.3.61/node_modules/@actions/workflow-parser/dist/model/converter/cron.js
16068
+ require_cronstrue();
16028
16069
  function isValidCron(cron) {
16029
16070
  const parts = cron.split(/ +/);
16030
16071
  if (parts.length != 5) return false;
@@ -17181,7 +17222,7 @@ function parseAction(entryFile, contextOrTrace) {
17181
17222
  };
17182
17223
  }
17183
17224
  //#endregion
17184
- //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+852776fb87daeb7010eafbce3cda467d415ec70f/node_modules/@ciatlas/core/src/schema.ts
17225
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/schema.ts
17185
17226
  /** Root definitions @actions/workflow-parser validates against (the strict ones its editor tooling uses). */
17186
17227
  const SCHEMA_ROOT = {
17187
17228
  workflow: "workflow-root-strict",
@@ -17277,7 +17318,7 @@ function mappingsAt(schema, root, path) {
17277
17318
  return [...out];
17278
17319
  }
17279
17320
  //#endregion
17280
- //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+852776fb87daeb7010eafbce3cda467d415ec70f/node_modules/@ciatlas/core/src/validate.ts
17321
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/validate.ts
17281
17322
  /**
17282
17323
  * The parser's errors for an invalid `${{ }}`: those of the expression lexer and parser (@actions/expressions, which
17283
17324
  * the sites' own expression parse uses too), and the template reader's for a `${{` without `}}`.
@@ -17673,7 +17714,7 @@ function mergeFix(key, where, whole) {
17673
17714
  return whole && single ? `${repeat}, or alias ${whole} instead.` : `${repeat}.`;
17674
17715
  }
17675
17716
  //#endregion
17676
- //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+852776fb87daeb7010eafbce3cda467d415ec70f/node_modules/@ciatlas/core/src/script-writes.ts
17717
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/script-writes.ts
17677
17718
  const escapes$1 = (path) => path === ".." || path.startsWith("../");
17678
17719
  /** `cmd` or `sudo -E cmd`: words that run the next word as the command. */
17679
17720
  const PREFIXES = /* @__PURE__ */ new Set([
@@ -17841,10 +17882,10 @@ function resolveWord(word, cwd) {
17841
17882
  }
17842
17883
  if (base === void 0 || w === "" || /[$`*?[\]{}~]/.test(w) || w.startsWith("/")) return void 0;
17843
17884
  if (/^[A-Za-z][\w+.-]*:/.test(w)) return void 0;
17844
- const path = normalizeRelative(posix.join(base, w));
17885
+ const path = normalizeRelative(posixJoin(base, w));
17845
17886
  return escapes$1(path) ? void 0 : path;
17846
17887
  }
17847
- const basename$1 = (word) => posix.basename(word.replaceAll("\\", "/").replace(/\/+$/, ""));
17888
+ const basename$1 = (word) => posixBasename(word.replaceAll("\\", "/").replace(/\/+$/, ""));
17848
17889
  /**
17849
17890
  * Paths an inline script writes to: destinations of copies, moves, links and `mkdir`, clones and worktrees,
17850
17891
  * extractions (`tar -C`, `unzip -d`), downloads (`-o`, `--output`) and redirections. Variables other than
@@ -17971,7 +18012,7 @@ function writesTo(write, path, isRepoDir) {
17971
18012
  return write.path !== "." && path.startsWith(`${write.path}/`) && !isRepoDir(write.path);
17972
18013
  }
17973
18014
  //#endregion
17974
- //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+852776fb87daeb7010eafbce3cda467d415ec70f/node_modules/@ciatlas/core/src/workspace.ts
18015
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/workspace.ts
17975
18016
  /**
17976
18017
  * Before any checkout, assume this repository at the workspace root: most jobs check it out there, possibly through
17977
18018
  * a step flowpact cannot see into, and a `./` action is unusable otherwise.
@@ -18346,106 +18387,10 @@ function workspaceResolver(opts) {
18346
18387
  };
18347
18388
  }
18348
18389
  //#endregion
18349
- //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+852776fb87daeb7010eafbce3cda467d415ec70f/node_modules/@ciatlas/core/src/project.ts
18390
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/project.ts
18350
18391
  const WORKFLOWS_DIR = ".github/workflows";
18351
- /** How many links one path may go through (the OS's ELOOP limit), and how many linked directories one walk expands. */
18352
- const MAX_LINKS = 40;
18353
- const MAX_LINKED_DIRS = 1e3;
18354
18392
  const ACTIONS_DIR = ".github/actions";
18355
18393
  const toPosix = (p) => p.split("\\").join("/");
18356
- /**
18357
- * Whether `abs` (after following symlinks) is inside the repository at `root` and outside its `.git` directory. A
18358
- * checkout can contain symlinks; this keeps a caller from reading e.g. /etc or the persisted git credentials, and from
18359
- * writing through a link. A path that does not exist yet is judged by its nearest existing parent.
18360
- */
18361
- function insideRepository(root, abs) {
18362
- let realRoot;
18363
- try {
18364
- realRoot = realpathSync(root);
18365
- } catch {
18366
- realRoot = resolve(root);
18367
- }
18368
- let target = resolve(abs);
18369
- let rest = "";
18370
- for (;;) try {
18371
- target = join(realpathSync(target), rest);
18372
- break;
18373
- } catch {
18374
- const parent = dirname(target);
18375
- if (parent === target) return false;
18376
- rest = join(basename(target), rest);
18377
- target = parent;
18378
- }
18379
- const rel = toPosix(relative(realRoot, target));
18380
- if (rel === "") return true;
18381
- if (escapesRoot(rel)) return false;
18382
- return rel !== ".git" && !rel.startsWith(".git/");
18383
- }
18384
- function nodeFileSystem(root) {
18385
- const abs = (p) => join(root, p);
18386
- const listings = /* @__PURE__ */ new Map();
18387
- /** On case-insensitive file systems, `.GitHub/x.YML` opens `.github/x.yml`; GitHub (Linux) would not find it. */
18388
- const exactCase = (p) => {
18389
- let dir = root;
18390
- for (const part of toPosix(p).split("/").filter((x) => x && x !== ".")) {
18391
- if (part === "..") return false;
18392
- let names = listings.get(dir);
18393
- if (!names) {
18394
- try {
18395
- names = new Set(readdirSync(dir));
18396
- } catch {
18397
- return false;
18398
- }
18399
- listings.set(dir, names);
18400
- }
18401
- if (!names.has(part)) return false;
18402
- dir = join(dir, part);
18403
- }
18404
- return true;
18405
- };
18406
- /** Follows symlinks and accepts only existing targets inside the repository (see insideRepository). */
18407
- const inside = (p) => existsSync(abs(p)) && exactCase(p) && insideRepository(root, abs(p));
18408
- const isFile = (p) => inside(p) && statSync(abs(p)).isFile();
18409
- return {
18410
- read: (p) => isFile(p) ? readFileSync(abs(p), "utf8") : void 0,
18411
- list: (dir) => {
18412
- if (!inside(dir) || !statSync(abs(dir)).isDirectory()) return [];
18413
- return readdirSync(abs(dir)).map((name) => toPosix(join(dir, name))).filter((f) => isFile(f));
18414
- },
18415
- walk: (dir) => {
18416
- if (!inside(dir) || !statSync(abs(dir)).isDirectory()) return [];
18417
- const out = [];
18418
- let linked = 0;
18419
- const queue = [{
18420
- dir: toPosix(dir),
18421
- above: /* @__PURE__ */ new Set(),
18422
- links: 0
18423
- }];
18424
- for (let i = 0; i < queue.length; i++) {
18425
- const next = queue[i];
18426
- const real = realpathSync(abs(next.dir));
18427
- if (next.above.has(real)) continue;
18428
- const above = new Set(next.above).add(real);
18429
- for (const name of readdirSync(abs(next.dir))) {
18430
- if (name === "node_modules" || name === ".git") continue;
18431
- const p = toPosix(join(next.dir, name));
18432
- if (isFile(p)) out.push(p);
18433
- else if (inside(p) && statSync(abs(p)).isDirectory()) {
18434
- const link = lstatSync(abs(p)).isSymbolicLink();
18435
- if (link && (next.links >= MAX_LINKS || linked++ >= MAX_LINKED_DIRS)) continue;
18436
- queue.push({
18437
- dir: p,
18438
- above,
18439
- links: next.links + (link ? 1 : 0)
18440
- });
18441
- }
18442
- }
18443
- }
18444
- return out;
18445
- },
18446
- isDir: (p) => inside(p) && statSync(abs(p)).isDirectory()
18447
- };
18448
- }
18449
18394
  /** The prefix of the paths below `dir`: `a/` for `a`, `a/`, `./a` or `./a/`, and `` for the root (`.`, `./`, ``). */
18450
18395
  const dirPrefix = (dir) => {
18451
18396
  const d = toPosix(dir).replace(/^\.(\/|$)/, "").replace(/\/$/, "");
@@ -18471,10 +18416,12 @@ const isWorkflowFile = (p) => /^\.github\/workflows\/[^/]+\.ya?ml$/i.test(p);
18471
18416
  /** GitHub matches the directory of a called workflow exactly, whatever the file system's case sensitivity. */
18472
18417
  const isCallableWorkflowPath = (p) => /^\.github\/workflows\/[^/]+\.ya?ml$/.test(p.replace(/\.ya?ml$/i, (e) => e.toLowerCase()));
18473
18418
  /** Local `uses:` targets must stay inside the repository. */
18474
- const escapesRoot = (p) => p === ".." || p.startsWith("../") || isAbsolute(p);
18475
- function loadProject(opts) {
18419
+ const escapesRoot = (p) => p === ".." || p.startsWith("../") || posixIsAbsolute(p);
18420
+ /** Loads a project through a host (`loadProject` of the Node and browser entries wire one in). */
18421
+ function loadProjectWith(opts, host) {
18476
18422
  const logger = (opts.logger ?? silentLogger).child("load");
18477
- const fs = opts.fs ?? nodeFileSystem(opts.root);
18423
+ const fs = opts.fs ?? host.defaultFs?.(opts.root);
18424
+ if (!fs) throw new Error("loadProject needs a file system here: pass fs (memoryFileSystem(files))");
18478
18425
  const ctx = createParseContext(opts.repository);
18479
18426
  const workflows = /* @__PURE__ */ new Map();
18480
18427
  const actions = /* @__PURE__ */ new Map();
@@ -18540,7 +18487,7 @@ function loadProject(opts) {
18540
18487
  let wholeRepository = false;
18541
18488
  const ignoredTargets = [];
18542
18489
  for (const p of opts.paths ?? []) {
18543
- const rel = toPosix(relative(opts.root, resolve(opts.root, p))) || ".";
18490
+ const rel = host.relativeTo(opts.root, p) || ".";
18544
18491
  if (rel === "." || rel === "") {
18545
18492
  wholeRepository = true;
18546
18493
  continue;
@@ -18649,6 +18596,119 @@ function externalUnits(remotes = [], ctx) {
18649
18596
  }
18650
18597
  return { external: out };
18651
18598
  }
18599
+ //#endregion
18600
+ //#region ../../node_modules/.pnpm/@ciatlas+core@https+++codeload.github.com+ciatlas+core+tar.gz+cf55cd8373233ba8a821439697c996db4188ecc3/node_modules/@ciatlas/core/src/node-fs.ts
18601
+ /** The project loader on a Node.js host: the working tree on disk, and `owner/repo` from the environment or git. */
18602
+ /** How many links one path may go through (the OS's ELOOP limit), and how many linked directories one walk expands. */
18603
+ const MAX_LINKS = 40;
18604
+ const MAX_LINKED_DIRS = 1e3;
18605
+ /**
18606
+ * Whether `abs` (after following symlinks) is inside the repository at `root` and outside its `.git` directory. A
18607
+ * checkout can contain symlinks; this keeps a caller from reading e.g. /etc or the persisted git credentials, and from
18608
+ * writing through a link. A path that does not exist yet is judged by its nearest existing parent.
18609
+ */
18610
+ function insideRepository(root, abs) {
18611
+ let realRoot;
18612
+ try {
18613
+ realRoot = realpathSync(root);
18614
+ } catch {
18615
+ realRoot = resolve(root);
18616
+ }
18617
+ let target = resolve(abs);
18618
+ let rest = "";
18619
+ for (;;) try {
18620
+ target = join(realpathSync(target), rest);
18621
+ break;
18622
+ } catch {
18623
+ const parent = dirname(target);
18624
+ if (parent === target) return false;
18625
+ rest = join(basename(target), rest);
18626
+ target = parent;
18627
+ }
18628
+ const rel = toPosix(relative(realRoot, target));
18629
+ if (rel === "") return true;
18630
+ if (rel === ".." || rel.startsWith("../")) return false;
18631
+ return rel !== ".git" && !rel.startsWith(".git/");
18632
+ }
18633
+ function nodeFileSystem(root) {
18634
+ const abs = (p) => join(root, p);
18635
+ const listings = /* @__PURE__ */ new Map();
18636
+ /** On case-insensitive file systems, `.GitHub/x.YML` opens `.github/x.yml`; GitHub (Linux) would not find it. */
18637
+ const exactCase = (p) => {
18638
+ let dir = root;
18639
+ for (const part of toPosix(p).split("/").filter((x) => x && x !== ".")) {
18640
+ if (part === "..") return false;
18641
+ let names = listings.get(dir);
18642
+ if (!names) {
18643
+ try {
18644
+ names = new Set(readdirSync(dir));
18645
+ } catch {
18646
+ return false;
18647
+ }
18648
+ listings.set(dir, names);
18649
+ }
18650
+ if (!names.has(part)) return false;
18651
+ dir = join(dir, part);
18652
+ }
18653
+ return true;
18654
+ };
18655
+ /** Follows symlinks and accepts only existing targets inside the repository (see insideRepository). */
18656
+ const inside = (p) => existsSync(abs(p)) && exactCase(p) && insideRepository(root, abs(p));
18657
+ const isFile = (p) => inside(p) && statSync(abs(p)).isFile();
18658
+ /** One directory of the walk: its files to `out`, its subdirectories to `queue`. */
18659
+ const expand = (next, out, queue, budget) => {
18660
+ const real = realpathSync(abs(next.dir));
18661
+ if (next.above.has(real)) return;
18662
+ const above = new Set(next.above).add(real);
18663
+ for (const name of readdirSync(abs(next.dir))) {
18664
+ if (name === "node_modules" || name === ".git") continue;
18665
+ const p = toPosix(join(next.dir, name));
18666
+ if (isFile(p)) out.push(p);
18667
+ else if (inside(p) && statSync(abs(p)).isDirectory()) enqueue(p, next, above, queue, budget);
18668
+ }
18669
+ };
18670
+ /** A subdirectory onto the queue, unless it is a link past the depth or count the walk allows. */
18671
+ const enqueue = (p, next, above, queue, budget) => {
18672
+ const link = lstatSync(abs(p)).isSymbolicLink();
18673
+ if (link && (next.links >= MAX_LINKS || budget.linked++ >= MAX_LINKED_DIRS)) return;
18674
+ queue.push({
18675
+ dir: p,
18676
+ above,
18677
+ links: next.links + (link ? 1 : 0)
18678
+ });
18679
+ };
18680
+ return {
18681
+ read: (p) => isFile(p) ? readFileSync(abs(p), "utf8") : void 0,
18682
+ list: (dir) => {
18683
+ if (!inside(dir) || !statSync(abs(dir)).isDirectory()) return [];
18684
+ return readdirSync(abs(dir)).map((name) => toPosix(join(dir, name))).filter((f) => isFile(f));
18685
+ },
18686
+ walk: (dir) => {
18687
+ if (!inside(dir) || !statSync(abs(dir)).isDirectory()) return [];
18688
+ const out = [];
18689
+ const queue = [{
18690
+ dir: toPosix(dir),
18691
+ above: /* @__PURE__ */ new Set(),
18692
+ links: 0
18693
+ }];
18694
+ const budget = { linked: 0 };
18695
+ for (let i = 0; i < queue.length; i++) expand(queue[i], out, queue, budget);
18696
+ return out;
18697
+ },
18698
+ isDir: (p) => inside(p) && statSync(abs(p)).isDirectory()
18699
+ };
18700
+ }
18701
+ const nodeHost = {
18702
+ defaultFs: (root) => nodeFileSystem(root),
18703
+ relativeTo: (root, p) => {
18704
+ const rel = relative(root, resolve(root, p));
18705
+ return isAbsolute(rel) ? ".." : toPosix(rel);
18706
+ }
18707
+ };
18708
+ /** Loads the project at `root`: from disk unless `fs` says otherwise. */
18709
+ function loadProject(opts) {
18710
+ return loadProjectWith(opts, nodeHost);
18711
+ }
18652
18712
  new Set(CONTEXT_FUNCTIONS.map((f) => f.name.toLowerCase()));
18653
18713
  //#endregion
18654
18714
  //#region ../engine/src/plan/values.ts
@@ -18701,6 +18761,12 @@ function* product(lists) {
18701
18761
  const [first, ...rest] = lists;
18702
18762
  for (const value of first) for (const tail of product(rest)) yield [value, ...tail];
18703
18763
  }
18764
+ /** A value in GitHub's string form: `null` is empty, objects are JSON, the rest `String()`. */
18765
+ function stringOf(value) {
18766
+ if (value === null) return "";
18767
+ if (typeof value === "object") return JSON.stringify(value);
18768
+ return String(value);
18769
+ }
18704
18770
  //#endregion
18705
18771
  //#region ../engine/src/contexts.ts
18706
18772
  /** The value of a context reference, with the unknown that explains a missing value. */
@@ -19045,28 +19111,24 @@ function literalValue(text) {
19045
19111
  function evaluateCondition(text, state, status) {
19046
19112
  const parsed = parseExpression(text);
19047
19113
  const reads = parsed.refs.map((ref) => ({
19048
- ref: `${ref.context}${ref.path.length ? `.${ref.path.join(".")}` : ""}`,
19114
+ ref: refName(ref),
19049
19115
  value: ref.dynamic ? unknown$1([`${ref.context}.${ref.path.join(".")}`]) : lookupContext(state, ref)
19050
19116
  }));
19051
- if (!parsed.ast) return {
19052
- value: unknown$1([`expression:${text}`], [true, false]),
19117
+ return {
19118
+ value: conditionValue(parsed, contextResolver(state), status, reads),
19053
19119
  reads
19054
19120
  };
19121
+ }
19122
+ /** A reference as a read names it: `needs.build.result`, `github`. */
19123
+ const refName = (ref) => `${ref.context}${ref.path.length ? `.${ref.path.join(".")}` : ""}`;
19124
+ /** Known true, known false, or unknown because of the reads and status answers it could not decide over. */
19125
+ function conditionValue(parsed, resolve, status, reads) {
19126
+ if (!parsed.ast) return unknown$1([`expression:${parsed.source}`], [true, false]);
19055
19127
  const options = { runtime: (name) => statusAnswer(status, name) };
19056
- const resolve = contextResolver(state);
19057
- if (definitelyTruthy(parsed.ast, resolve, options)) return {
19058
- value: known$1(true),
19059
- reads
19060
- };
19061
- if (definitelyFalsy(parsed.ast, resolve, options)) return {
19062
- value: known$1(false),
19063
- reads
19064
- };
19128
+ if (definitelyTruthy(parsed.ast, resolve, options)) return known$1(true);
19129
+ if (definitelyFalsy(parsed.ast, resolve, options)) return known$1(false);
19065
19130
  const unknowns = [...reads.flatMap((r) => r.value.known ? [] : r.value.unknowns), ...Object.values(status).flatMap((s) => s.known ? [] : s.unknowns)];
19066
- return {
19067
- value: unknown$1(unknowns.length ? unknowns : [`expression:${text}`], [true, false]),
19068
- reads
19069
- };
19131
+ return unknown$1(unknowns.length ? unknowns : [`expression:${parsed.source}`], [true, false]);
19070
19132
  }
19071
19133
  /** The status functions as core's evaluator asks for them; an unknown answer leaves the call unknown. */
19072
19134
  function statusAnswer(status, name) {
@@ -19095,11 +19157,6 @@ function evaluateValue(text, state, status) {
19095
19157
  return unknown$1(unknowns.length ? unknowns : [`expression:${text}`]);
19096
19158
  }
19097
19159
  /** A value as GitHub writes it into a string: `null` is empty, objects are JSON. */
19098
- function stringOf(value) {
19099
- if (value === null) return "";
19100
- if (typeof value === "object") return JSON.stringify(value);
19101
- return String(value);
19102
- }
19103
19160
  /**
19104
19161
  * A script with its `${{ }}` segments replaced by their values, for folding what it writes; a segment the plan cannot
19105
19162
  * evaluate becomes `UNKNOWN_MARKER`, which taints any write that includes it.
@@ -19218,7 +19275,7 @@ function pathsMatch(changed, filter) {
19218
19275
  }
19219
19276
  //#endregion
19220
19277
  //#region ../engine/src/paths.ts
19221
- const ID = /^[A-Za-z_][A-Za-z0-9_-]*$/;
19278
+ const ID$1 = /^[A-Za-z_][A-Za-z0-9_-]*$/;
19222
19279
  var UnitPathError = class extends Error {
19223
19280
  name = "UnitPathError";
19224
19281
  };
@@ -19242,7 +19299,7 @@ function parseJobSegment(part, text) {
19242
19299
  const m = /^([^[.]+)(?:\[([^\]]*)\])?(?:\.(.+))?$/.exec(part);
19243
19300
  if (!m) throw new UnitPathError(`cannot read '${part}' in '${text}'`);
19244
19301
  const [, job, leg, step] = m;
19245
- if (!ID.test(job)) throw new UnitPathError(`'${job}' is not a job id in '${text}'`);
19302
+ if (!ID$1.test(job)) throw new UnitPathError(`'${job}' is not a job id in '${text}'`);
19246
19303
  const segment = { job };
19247
19304
  if (leg !== void 0) segment.leg = parseLeg(leg, text);
19248
19305
  if (step !== void 0) segment.step = parseStep(step, text);
@@ -19263,9 +19320,18 @@ function parseStep(step, text) {
19263
19320
  if (!Number.isInteger(n) || n < 1) throw new UnitPathError(`'${step}' is not #<position> in '${text}'`);
19264
19321
  return { index: n };
19265
19322
  }
19266
- if (!ID.test(step)) throw new UnitPathError(`'${step}' is not a step id in '${text}'`);
19323
+ if (!ID$1.test(step)) throw new UnitPathError(`'${step}' is not a step id in '${text}'`);
19267
19324
  return { id: step };
19268
19325
  }
19326
+ function formatUnitPath(path) {
19327
+ const body = path.segments.map((s) => {
19328
+ const leg = s.leg ? `[${Object.entries(s.leg).map(([k, v]) => `${k}=${v}`).join(",")}]` : "";
19329
+ const step = s.step ? s.step.id !== void 0 ? s.step.id : `#${s.step.index}` : "";
19330
+ if (s.job === void 0) return step;
19331
+ return `${s.job}${leg}${step ? `.${step}` : ""}`;
19332
+ }).join("/");
19333
+ return path.workflow === void 0 ? body : `${path.workflow}:${body}`;
19334
+ }
19269
19335
  /** Whether a path names the location: same depth, same jobs and steps, leg values a subset of the location's. */
19270
19336
  function pathMatches(path, location) {
19271
19337
  if (path.workflow !== void 0 && !workflowMatches(path.workflow, location.workflow)) return false;
@@ -27637,6 +27703,107 @@ function unitPathOfId(id, paths) {
27637
27703
  return best === void 0 ? id : `${paths.get(best)}${id.slice(best.length)}`;
27638
27704
  }
27639
27705
  //#endregion
27706
+ //#region ../engine/src/plan/pins.ts
27707
+ const RACE = "race:fail-fast:";
27708
+ const ID = /^[A-Za-z_][A-Za-z0-9_-]*$/;
27709
+ /** Sets `producer` and an assume `pin` on every unknown about a planned unit's field. */
27710
+ function assumePins(plan, units) {
27711
+ for (const u of plan.unknowns) {
27712
+ const ref = unitRef(u.id, units);
27713
+ if (!ref) continue;
27714
+ u.producer = ref.node;
27715
+ const pin = pinOf(plan, ref.node, units.get(ref.node), ref.about);
27716
+ if (pin) u.pin = pin;
27717
+ }
27718
+ }
27719
+ /** The unit an unknown is about and what of it: the longest unit id followed by `.outputs.`, `.env.` or `.outcome`. */
27720
+ function unitRef(id, units) {
27721
+ if (id.startsWith(RACE)) {
27722
+ const node = id.slice(15);
27723
+ return units.get(node)?.kind === "job" ? {
27724
+ node,
27725
+ about: { field: "cancelled" }
27726
+ } : void 0;
27727
+ }
27728
+ if (id.startsWith("race:") || id.startsWith("conflict:")) return void 0;
27729
+ for (let dot = id.lastIndexOf("."); dot > 0; dot = id.lastIndexOf(".", dot - 1)) {
27730
+ const node = id.slice(0, dot);
27731
+ if (!units.has(node)) continue;
27732
+ const about = aboutOf(id.slice(dot + 1));
27733
+ return about ? {
27734
+ node,
27735
+ about
27736
+ } : void 0;
27737
+ }
27738
+ }
27739
+ function aboutOf(rest) {
27740
+ if (rest === "outcome") return { field: "outcome" };
27741
+ for (const field of ["outputs", "env"]) if (rest.startsWith(`${field}.`) && rest.length > field.length + 1) return {
27742
+ field,
27743
+ name: rest.slice(field.length + 1)
27744
+ };
27745
+ }
27746
+ function pinOf(plan, node, site, about) {
27747
+ if (about.field === "env" && site.kind === "job") return void 0;
27748
+ const named = namedBy(site.location);
27749
+ if (!named) return void 0;
27750
+ const entry = {};
27751
+ if (sharesJobId(plan, node, site.location)) entry.workflow = site.location.workflow;
27752
+ entry[site.kind] = named.path;
27753
+ if (named.matrix) entry.matrix = named.matrix;
27754
+ if (about.field === "cancelled" || about.field === "outcome") entry.outcome = about.field === "cancelled" ? "cancelled" : "failure";
27755
+ else entry[about.field] = { [about.name]: "" };
27756
+ return {
27757
+ layer: "assume",
27758
+ fragment: entry,
27759
+ at: ["assume"]
27760
+ };
27761
+ }
27762
+ /**
27763
+ * The unit path of a location, with the leg of its last segment taken out as `matrix:` values; undefined when a leg
27764
+ * further up cannot be written in the path, or an id is not one a path holds.
27765
+ */
27766
+ function namedBy(location) {
27767
+ const segments = segmentsOf(location);
27768
+ if (!segments) return void 0;
27769
+ const last = segments.at(-1);
27770
+ const own = last.job === void 0 ? void 0 : last.leg;
27771
+ if (own) delete last.leg;
27772
+ if (!segments.every((s) => s.leg === void 0 || legInPath(s.leg))) return void 0;
27773
+ const path = formatUnitPath({ segments });
27774
+ return own && Object.keys(own).length ? {
27775
+ path,
27776
+ matrix: own
27777
+ } : { path };
27778
+ }
27779
+ /** The chain as path segments: a job opens one, a step after a job's step opens one inside its composite action. */
27780
+ function segmentsOf(location) {
27781
+ const segments = [];
27782
+ for (const link of location.chain) if (link.kind === "step") addStep(segments, link);
27783
+ else if (ID.test(link.id)) segments.push(link.leg ? {
27784
+ job: link.id,
27785
+ leg: { ...link.leg }
27786
+ } : { job: link.id });
27787
+ else return void 0;
27788
+ return segments.length ? segments : void 0;
27789
+ }
27790
+ /** A step joins its job's segment, or opens one inside the composite action of the step before it. */
27791
+ function addStep(segments, link) {
27792
+ const step = link.id !== void 0 && ID.test(link.id) ? { id: link.id } : { index: link.index };
27793
+ const last = segments.at(-1);
27794
+ if (last?.job !== void 0 && last.step === void 0) last.step = step;
27795
+ else segments.push({ step });
27796
+ }
27797
+ /** Whether `[k=v,…]` reads back as these values: no `/`, `,` or `]`, no surrounding space, no `=` in a key. */
27798
+ const legInPath = (leg) => Object.entries(leg).every(([k, v]) => plainCell(k) && !k.includes("=") && plainCell(v));
27799
+ const plainCell = (s) => s.trim() === s && !/[/,\]]/.test(s);
27800
+ /** Whether another workflow of the plan has a job of the same id as the unit's first job. */
27801
+ function sharesJobId(plan, node, location) {
27802
+ const first = location.chain[0];
27803
+ if (first?.kind !== "job") return false;
27804
+ return plan.workflows.some((w) => w.depth === 0 && !node.startsWith(`${w.id}#`) && w.jobs.some((j) => j.jobId === first.id));
27805
+ }
27806
+ //#endregion
27640
27807
  //#region ../engine/src/scenario.ts
27641
27808
  /**
27642
27809
  * A scenario: the facts about one run that the engine simulates. Authors write the input shape (in a test's `when:`,
@@ -28630,7 +28797,7 @@ function verdictOf(gates) {
28630
28797
  //#endregion
28631
28798
  //#region ../engine/src/version.ts
28632
28799
  /** The simulci version. release-please updates it with every release. */
28633
- const VERSION = "0.1.0";
28800
+ const VERSION = "0.2.0";
28634
28801
  /** The schema version of every JSON document the engine emits or reads. A breaking change bumps it. */
28635
28802
  const SCHEMA_VERSIONS = {
28636
28803
  scenario: 1,
@@ -28685,6 +28852,8 @@ var Planner = class {
28685
28852
  diagnostics = [];
28686
28853
  applied = [];
28687
28854
  models = /* @__PURE__ */ new Map();
28855
+ /** Every planned step and leg by id, with the location an assume entry is matched against. */
28856
+ units = /* @__PURE__ */ new Map();
28688
28857
  index;
28689
28858
  constructor(loaded, scenario, detail, maxDepth, mocks, shipped) {
28690
28859
  this.loaded = loaded;
@@ -28700,7 +28869,7 @@ var Planner = class {
28700
28869
  depth: 0,
28701
28870
  checkPrefix: ""
28702
28871
  }));
28703
- return {
28872
+ const plan = {
28704
28873
  $schema: schemaUrl("runPlan"),
28705
28874
  schemaVersion: 1,
28706
28875
  engine: {
@@ -28718,6 +28887,8 @@ var Planner = class {
28718
28887
  })).sort((a, b) => a.id.localeCompare(b.id)),
28719
28888
  diagnostics: this.finishDiagnostics()
28720
28889
  };
28890
+ assumePins(plan, this.units);
28891
+ return plan;
28721
28892
  }
28722
28893
  /** The mocks' own diagnostics, plus the entries that named nothing in this plan (SC102). */
28723
28894
  finishDiagnostics() {
@@ -28865,74 +29036,65 @@ var Planner = class {
28865
29036
  total: exp.combos.length || 1
28866
29037
  };
28867
29038
  plan.legs = this.legsOf(job, exp, segments, legCtx);
28868
- this.failFast(plan, job);
29039
+ for (const leg of plan.legs) {
29040
+ const location = {
29041
+ workflow: ctx.wf.path,
29042
+ chain: [...ctx.chain, legLinkOf(job, leg)]
29043
+ };
29044
+ this.units.set(leg.id, {
29045
+ location,
29046
+ kind: "job"
29047
+ });
29048
+ this.applyLegFacts(job, leg, jobNode, location);
29049
+ }
29050
+ this.failFast(plan);
28869
29051
  plan.result = this.jobResultOf(runs, cond, continueOnError, plan.legs);
28870
- plan.outputs = this.jobOutputs(job, plan, jobNode);
28871
- this.applyJobFacts(job, plan, continueOnError, {
28872
- workflow: ctx.wf.path,
28873
- chain: [...ctx.chain, {
28874
- kind: "job",
28875
- id: job.id
28876
- }]
28877
- });
29052
+ plan.outputs = this.jobOutputs(plan, jobNode);
28878
29053
  return plan;
28879
29054
  }
28880
- /** A mock on the job: its outcome applies at the job boundary (the steps stay planned), its outputs overlay. */
28881
- applyJobFacts(job, plan, continueOnError, location) {
28882
- const facts = this.mocks.factsFor(location, plan.id);
29055
+ /**
29056
+ * A mock on the job or one of its legs (`test[node=22]`, or `test` with `matrix:`): its outcome is the leg's result
29057
+ * (the steps stay planned), its outputs overlay the leg's. The job's result and outputs are then read from its legs.
29058
+ */
29059
+ applyLegFacts(job, leg, jobNode, location) {
29060
+ const facts = this.mocks.factsFor(location, leg.id);
28883
29061
  if (facts.outcome) {
28884
- const result = jobResult({
28885
- skipped: false,
28886
- continueOnError,
28887
- legResults: [facts.outcome.value]
28888
- });
28889
- plan.result = plan.runs.verdict === "maybe" ? unknown$1(plan.result.known ? [] : plan.result.unknowns, ["skipped", ...candidates(result) ?? []]) : result;
28890
- this.record(facts.outcome.by, plan.id, "outcome");
28891
- if (job.steps.length) this.diagnostics.push({
29062
+ leg.result = facts.outcome.value;
29063
+ this.record(facts.outcome.by, leg.id, "outcome");
29064
+ if (job.steps.length) this.diagnoseOnce({
28892
29065
  severity: "info",
28893
29066
  code: "SC108",
28894
29067
  message: `${facts.outcome.by} sets the outcome of job ${job.id}; its steps are planned as written`,
28895
- node: plan.id,
29068
+ node: jobNode,
28896
29069
  mock: facts.outcome.by
28897
29070
  });
28898
29071
  }
28899
- const declared = job.uses ? Object.keys(plan.outputs) : Object.keys(job.outputs);
28900
- this.overlayOutputs(plan.outputs, facts, plan.id, {
29072
+ const declared = job.uses ? Object.keys(leg.outputs) : Object.keys(job.outputs);
29073
+ this.overlayOutputs(leg.outputs, facts, leg.id, {
28901
29074
  closed: true,
28902
29075
  names: declared,
28903
29076
  what: `job ${job.id}`
28904
29077
  });
28905
29078
  }
29079
+ /** A diagnostic said once: every leg of a matrix that meets the same mock says the same thing. */
29080
+ diagnoseOnce(d) {
29081
+ if (!this.diagnostics.some((x) => x.code === d.code && x.mock === d.mock && x.message === d.message)) this.diagnostics.push(d);
29082
+ }
28906
29083
  /** Fail-fast: once a leg fails, the other legs are cancelled or finish first; `needs` reads the job as failed either way. */
28907
- failFast(plan, job) {
29084
+ failFast(plan) {
28908
29085
  if (!plan.matrix?.failFast.known || !plan.matrix.failFast.value) return;
28909
29086
  if (!plan.legs.some((l) => l.result.known && l.result.value === "failure")) return;
28910
29087
  for (const leg of plan.legs) {
28911
29088
  if (!leg.result.known || leg.result.value !== "success") continue;
28912
29089
  const id = `race:fail-fast:${leg.id}`;
28913
- const matrix = Object.fromEntries(Object.entries(leg.values).map(([k, v]) => [k, v.known ? stringOf(v.value) : "?"]));
28914
- this.register({
28915
- id,
28916
- kind: "race",
28917
- reads: [],
28918
- affects: [],
28919
- pin: {
28920
- layer: "assume",
28921
- fragment: {
28922
- job: job.id,
28923
- matrix,
28924
- outcome: "cancelled"
28925
- },
28926
- at: ["assume"]
28927
- }
28928
- }, void 0);
29090
+ this.note([id]);
28929
29091
  leg.result = unknown$1([id], ["success", "cancelled"]);
28930
29092
  }
28931
29093
  }
28932
29094
  /** Mocked outputs over the unit's own; a name a closed interface does not declare is an error (SC101). */
28933
29095
  overlayOutputs(outputs, facts, node, iface) {
28934
29096
  for (const [name, fact] of Object.entries(facts.outputs)) {
28935
- if (iface.closed && !iface.names.includes(name)) this.diagnostics.push({
29097
+ if (iface.closed && !iface.names.includes(name)) this.diagnoseOnce({
28936
29098
  severity: "error",
28937
29099
  code: "SC101",
28938
29100
  message: `${fact.by} sets output '${name}', which ${iface.what} does not declare${iface.names.length ? ` (it declares ${iface.names.join(", ")})` : ""}`,
@@ -28970,13 +29132,25 @@ var Planner = class {
28970
29132
  node: ctx.wfNode,
28971
29133
  verdict: ctx.triggered.verdict
28972
29134
  };
28973
- const runs = decide(ctx.triggered, cond.value, [parent, ifReason]);
29135
+ const runs = this.nestingLimit(job, ctx, decide(ctx.triggered, cond.value, [parent, ifReason]));
28974
29136
  if (runs.verdict === "maybe") this.noteValue(cond.value, jobNode);
28975
29137
  return {
28976
29138
  runs,
28977
29139
  cond
28978
29140
  };
28979
29141
  }
29142
+ /** A call past the nesting limit does not run, whatever its `if:` says: the job stops, not only its legs. */
29143
+ nestingLimit(job, ctx, runs) {
29144
+ if (runs.verdict === "no" || !job.uses || ctx.depth + 1 < this.maxDepth) return runs;
29145
+ if (!this.index.calleeOf(job)) return runs;
29146
+ return {
29147
+ verdict: "no",
29148
+ chain: [...runs.chain, {
29149
+ kind: "limit",
29150
+ limit: "nesting"
29151
+ }]
29152
+ };
29153
+ }
28980
29154
  matrixPlan(job, exp, ctx, jobNode) {
28981
29155
  const matrix = {
28982
29156
  keys: exp.keys,
@@ -29025,25 +29199,29 @@ var Planner = class {
29025
29199
  };
29026
29200
  return [leg];
29027
29201
  }
29202
+ /**
29203
+ * The result `needs` reads. A reader runs only where the workflow does, so a job `maybe` only because its workflow
29204
+ * is reads as its legs did; an `if:` the plan cannot decide adds `skipped`.
29205
+ */
29028
29206
  jobResultOf(runs, cond, continueOnError, legs) {
29029
29207
  const result = jobResult({
29030
29208
  skipped: false,
29031
29209
  continueOnError,
29032
29210
  legResults: legs.map((l) => l.result)
29033
29211
  });
29034
- if (runs.verdict !== "maybe") return result;
29035
- return unknown$1(cond.value.known ? [] : cond.value.unknowns, ["skipped", ...candidates(result) ?? []]);
29212
+ return runs.verdict === "maybe" ? orSkipped(result, cond.value, "first") : result;
29036
29213
  }
29037
29214
  /** A job's outputs as readers see them: the legs' values, where an empty value never wins and the last to finish does (lab 8). */
29038
- jobOutputs(job, plan, jobNode) {
29215
+ jobOutputs(plan, jobNode) {
29039
29216
  const out = {};
29040
- const names = job.uses ? [...new Set(plan.legs.flatMap((l) => Object.keys(l.outputs)))] : Object.keys(job.outputs);
29217
+ const names = [...new Set(plan.legs.flatMap((l) => Object.keys(l.outputs)))];
29041
29218
  for (const name of names) {
29042
29219
  const values = plan.legs.map((l) => l.outputs[name] ?? known$1(""));
29043
29220
  const ids = values.flatMap((v) => v.known ? [] : v.unknowns);
29044
29221
  const nonEmpty = [...new Set(values.flatMap((v) => v.known && v.value !== "" ? [v.value] : []))];
29222
+ const chosen = nonEmpty[0] ?? "";
29045
29223
  if (ids.length) out[name] = unknown$1(ids);
29046
- else if (nonEmpty.length <= 1) out[name] = known$1(nonEmpty[0] ?? "");
29224
+ else if (nonEmpty.length <= 1) out[name] = values.find((v) => v.known && v.value === chosen);
29047
29225
  else {
29048
29226
  const race = `race:matrix-outputs:${jobNode}.${name}`;
29049
29227
  this.note([race]);
@@ -29077,23 +29255,18 @@ var Planner = class {
29077
29255
  if (combo) leg.origin = combo.origin;
29078
29256
  if (job.runsOn) leg.runsOn = this.runsOn(job, ctx.wf, state, legId);
29079
29257
  if (job.environment) leg.environment = this.environment(job, ctx.wf, state, legId);
29258
+ const location = {
29259
+ workflow: ctx.wf.path,
29260
+ chain: [...ctx.chain, legLinkOf(job, leg)]
29261
+ };
29080
29262
  if (job.uses) {
29081
- this.planCall(leg, state, seg, ctx);
29263
+ this.planCall(leg, state, seg, ctx, location);
29082
29264
  return leg;
29083
29265
  }
29084
29266
  leg.check = {
29085
29267
  name: `${ctx.checkPrefix}${seg.name}`,
29086
29268
  certain: seg.certain
29087
29269
  };
29088
- const legLink = {
29089
- kind: "job",
29090
- id: job.id
29091
- };
29092
- if (combo) legLink.leg = Object.fromEntries(Object.entries(leg.values).map(([k, v]) => [k, v.known ? stringOf(v.value) : "?"]));
29093
- const location = {
29094
- workflow: ctx.wf.path,
29095
- chain: [...ctx.chain, legLink]
29096
- };
29097
29270
  const steps = this.planSteps(job.steps, ctx.settings.steps, legId, runs, state, void 0, location);
29098
29271
  if (this.detail === "steps") leg.steps = steps.plans;
29099
29272
  leg.result = legResult(steps.conclusions);
@@ -29148,34 +29321,18 @@ var Planner = class {
29148
29321
  return v;
29149
29322
  }
29150
29323
  /** A reusable workflow called by the leg: nested with the caller's inputs and secrets, its result from its jobs. */
29151
- planCall(leg, state, seg, ctx) {
29324
+ planCall(leg, state, seg, ctx, location) {
29152
29325
  const { job } = ctx;
29153
29326
  const callee = this.index.calleeOf(job);
29154
29327
  if (!callee) {
29155
29328
  this.unsupportedCall(leg, job);
29156
29329
  return;
29157
29330
  }
29158
- if (ctx.depth + 1 >= this.maxDepth) {
29159
- leg.runs = {
29160
- verdict: "no",
29161
- chain: [...leg.runs.chain, {
29162
- kind: "limit",
29163
- limit: "nesting"
29164
- }]
29165
- };
29166
- leg.result = known$1("skipped");
29167
- return;
29168
- }
29169
29331
  const inputs = callInputs(job, callee, state);
29170
29332
  for (const v of Object.values(inputs)) this.noteValue(v);
29171
- const callLink = {
29172
- kind: "job",
29173
- id: job.id
29174
- };
29175
- if (Object.keys(leg.values).length) callLink.leg = Object.fromEntries(Object.entries(leg.values).map(([k, v]) => [k, v.known ? stringOf(v.value) : "?"]));
29176
29333
  leg.call = this.planWorkflow(callee, {
29177
29334
  depth: ctx.depth + 1,
29178
- chain: [...ctx.chain, callLink],
29335
+ chain: location.chain,
29179
29336
  caller: leg.id,
29180
29337
  triggered: {
29181
29338
  verdict: leg.runs.verdict,
@@ -29190,12 +29347,12 @@ var Planner = class {
29190
29347
  checkPrefix: `${ctx.checkPrefix}${seg.name} / `
29191
29348
  });
29192
29349
  const legResults = leg.call.jobs.map((j) => j.result);
29193
- leg.result = leg.runs.verdict === "no" ? known$1("skipped") : jobResult({
29350
+ leg.result = jobResult({
29194
29351
  skipped: false,
29195
29352
  continueOnError: known$1(false),
29196
29353
  legResults
29197
29354
  });
29198
- leg.outputs = leg.call.outputs;
29355
+ leg.outputs = { ...leg.call.outputs };
29199
29356
  }
29200
29357
  unsupportedCall(leg, job) {
29201
29358
  const why = job.uses?.kind === "remote-workflow" ? "a remote reusable workflow is not loaded" : "the called workflow was not found or cannot be called";
@@ -29261,6 +29418,10 @@ var Planner = class {
29261
29418
  return out;
29262
29419
  }
29263
29420
  planStep(step, st, sid, parentId, parentRuns, state, status, location) {
29421
+ this.units.set(sid, {
29422
+ location,
29423
+ kind: "step"
29424
+ });
29264
29425
  const evaluated = st?.if ?? impliedIf(step);
29265
29426
  const cond = evaluateCondition(evaluated, state, stepStatusFunctions(status));
29266
29427
  const parent = {
@@ -29314,10 +29475,7 @@ var Planner = class {
29314
29475
  envWrites: {}
29315
29476
  };
29316
29477
  const facts = this.mocks.factsFor(location, sid, step.uses?.raw, target.model);
29317
- const envWrites = {
29318
- ...this.scriptEnv(step, sid, state),
29319
- ...this.envWritesOf(facts, sid)
29320
- };
29478
+ const envWrites = this.envWrites(step, sid, state, facts);
29321
29479
  const { produced, base } = this.producedOutcome(facts, runs, cond, sid);
29322
29480
  if (target.action?.using === "composite") return {
29323
29481
  ...this.compositeProduct(target.action, step, sid, state, runs, location, facts, produced, base),
@@ -29336,13 +29494,26 @@ var Planner = class {
29336
29494
  envWrites
29337
29495
  };
29338
29496
  }
29339
- /** The outputs in interface order, then the names only a mock added; a name nothing valued stays unknown. */
29497
+ /**
29498
+ * The outputs in interface order, then the names only a mock added; a name nothing valued stays unknown. Only what
29499
+ * is unknown after the mocks is an unknown of the plan: a pinned output is not.
29500
+ */
29340
29501
  finishOutputs(opaque, sid) {
29341
29502
  const out = {};
29342
- for (const name of opaque.names) out[name] = opaque.outputs[name] ?? this.unknownOutput(sid, name);
29503
+ for (const name of opaque.names) out[name] = opaque.outputs[name] ?? outputUnknown(sid, name);
29343
29504
  for (const [name, v] of Object.entries(opaque.outputs)) out[name] ??= v;
29505
+ for (const v of Object.values(out)) this.noteValue(v);
29344
29506
  return out;
29345
29507
  }
29508
+ /** The `$GITHUB_ENV` writes the steps after this one read: the script's, under the mocked ones. */
29509
+ envWrites(step, sid, state, facts) {
29510
+ const writes = {
29511
+ ...this.scriptEnv(step, sid, state),
29512
+ ...this.envWritesOf(facts, sid)
29513
+ };
29514
+ for (const v of Object.values(writes)) this.noteValue(v);
29515
+ return writes;
29516
+ }
29346
29517
  envWritesOf(facts, sid) {
29347
29518
  const envWrites = {};
29348
29519
  for (const [name, fact] of Object.entries(facts.env)) {
@@ -29351,13 +29522,13 @@ var Planner = class {
29351
29522
  }
29352
29523
  return envWrites;
29353
29524
  }
29354
- /** The outcome as mocked, else success; a `maybe` step may also have been skipped. */
29525
+ /** The outcome as mocked, else success; a step whose own `if:` is unknown may also have been skipped. */
29355
29526
  producedOutcome(facts, runs, cond, sid) {
29356
29527
  const produced = facts.outcome ? facts.outcome.value : known$1("success");
29357
29528
  if (facts.outcome) this.record(facts.outcome.by, sid, "outcome");
29358
29529
  return {
29359
29530
  produced,
29360
- base: runs.verdict === "yes" ? produced : unknown$1(cond.value.known ? [] : cond.value.unknowns, [...candidates(produced) ?? [], "skipped"])
29531
+ base: runs.verdict === "yes" ? produced : orSkipped(produced, cond.value, "last")
29361
29532
  };
29362
29533
  }
29363
29534
  /** A composite action under its step: a mocked outcome makes it opaque, otherwise its inner steps decide. */
@@ -29407,11 +29578,7 @@ var Planner = class {
29407
29578
  scriptOutputs(step, sid, state) {
29408
29579
  const outputs = {};
29409
29580
  const folded = foldWrites(substituteTemplates(step.run ?? "", state), { unknownId: (n) => `${sid}.outputs.${n}` });
29410
- for (const name of /* @__PURE__ */ new Set([...step.writesOutputs.names, ...Object.keys(folded)])) {
29411
- const v = folded[name] ?? this.unknownOutput(sid, name);
29412
- this.noteValue(v);
29413
- outputs[name] = v;
29414
- }
29581
+ for (const name of /* @__PURE__ */ new Set([...step.writesOutputs.names, ...Object.keys(folded)])) outputs[name] = folded[name] ?? outputUnknown(sid, name);
29415
29582
  const open = !(step.writesOutputs.mentions && !step.writesOutputs.dynamic);
29416
29583
  return {
29417
29584
  outputs,
@@ -29423,17 +29590,10 @@ var Planner = class {
29423
29590
  /** The `$GITHUB_ENV` writes of a script, as the steps after it read them. */
29424
29591
  scriptEnv(step, sid, state) {
29425
29592
  if (step.run === void 0) return {};
29426
- const folded = foldWrites(substituteTemplates(step.run, state), {
29593
+ return foldWrites(substituteTemplates(step.run, state), {
29427
29594
  file: "GITHUB_ENV",
29428
29595
  unknownId: (n) => `${sid}.env.${n}`
29429
29596
  });
29430
- for (const v of Object.values(folded)) this.noteValue(v);
29431
- return folded;
29432
- }
29433
- unknownOutput(sid, name) {
29434
- const id = `${sid}.outputs.${name}`;
29435
- this.note([id]);
29436
- return unknown$1([id]);
29437
29597
  }
29438
29598
  planComposite(action, step, stepNode, state, runs, location) {
29439
29599
  const id = compositeNodeId(stepNode, action.path);
@@ -29626,6 +29786,27 @@ function legValues(combo, legId) {
29626
29786
  if (!combo) return {};
29627
29787
  return Object.fromEntries(Object.entries(combo.values).map(([k, c]) => [k, c.known ? known$1(c.value) : unknown$1([`${legId}.matrix.${k}`])]));
29628
29788
  }
29789
+ /**
29790
+ * A result when the unit's own `if:` may leave it skipped: `skipped` joins its candidates (a job has listed it first, a
29791
+ * step last). A parent's `maybe` adds nothing: the unit's readers (`needs`, `steps`) run only where its parent does.
29792
+ */
29793
+ function orSkipped(result, cond, at) {
29794
+ if (cond.known) return result;
29795
+ const ids = [...cond.unknowns, ...result.known ? [] : result.unknowns];
29796
+ const options = candidates(result);
29797
+ if (!options) return unknown$1(ids);
29798
+ return unknown$1(ids, at === "first" ? ["skipped", ...options] : [...options, "skipped"]);
29799
+ }
29800
+ const outputUnknown = (sid, name) => unknown$1([`${sid}.outputs.${name}`]);
29801
+ /** The link of a leg in a location: its job, with the leg's values (`?` for an unknown one) when it has a matrix. */
29802
+ function legLinkOf(job, leg) {
29803
+ const link = {
29804
+ kind: "job",
29805
+ id: job.id
29806
+ };
29807
+ if (Object.keys(leg.values).length) link.leg = Object.fromEntries(Object.entries(leg.values).map(([k, v]) => [k, v.known ? stringOf(v.value) : "?"]));
29808
+ return link;
29809
+ }
29629
29810
  /** The verdict under a parent and an `if:` value: the first `no` wins, an unknown makes it `maybe`. */
29630
29811
  function decide(parent, value, chain) {
29631
29812
  if (parent.verdict === "no") return {
@@ -29746,6 +29927,7 @@ function topoOrder(wf) {
29746
29927
  return order;
29747
29928
  }
29748
29929
  const KINDS = [
29930
+ [/^conflict:/, "conflict"],
29749
29931
  [/^vars\./, "var"],
29750
29932
  [/^inputs\./, "input"],
29751
29933
  [/^secrets\./, "secret"],
@@ -29755,6 +29937,7 @@ const KINDS = [
29755
29937
  [/^race:/, "race"],
29756
29938
  [/\.outputs\./, "step-output"],
29757
29939
  [/\.outcome$/, "outcome"],
29940
+ [/\.env\./, "env"],
29758
29941
  [/matrix/, "matrix"],
29759
29942
  [/hashfiles/i, "hashfiles"]
29760
29943
  ];
@@ -29768,7 +29951,6 @@ function kindOf(id) {
29768
29951
  return KINDS.find(([re]) => re.test(id))?.[1] ?? "unsupported";
29769
29952
  }
29770
29953
  const SCENARIO_PINS = [
29771
- [/^vars\.(.+)$/, (name) => ({ vars: { [name]: "" } })],
29772
29954
  [/^inputs\.((?!.*\.default$).+)$/, (name) => ({ inputs: { [name]: "" } })],
29773
29955
  [/^github\.(sha)$/, () => ({ sha: "" })],
29774
29956
  [/^github\.(actor|triggering_actor)$/, () => ({ actor: { login: "" } })],
@@ -29776,7 +29958,10 @@ const SCENARIO_PINS = [
29776
29958
  [/^github\.(ref|ref_name|ref_type)$/, () => ({ tag: "" })],
29777
29959
  [/^scenario\.changed$/, () => ({ changed: ["path/to/file"] })]
29778
29960
  ];
29779
- /** The scenario field that would pin an unknown read, when there is one. */
29961
+ /**
29962
+ * The scenario field that would pin an unknown read, when there is one. A step's or job's output, outcome or env
29963
+ * write is pinned by an assume entry, set over the finished plan (`assumePins`).
29964
+ */
29780
29965
  function pinFor(id) {
29781
29966
  for (const [re, fragment] of SCENARIO_PINS) {
29782
29967
  const m = re.exec(id);
@@ -31138,7 +31323,8 @@ const REF_GROUP = [
31138
31323
  const MERGED = /* @__PURE__ */ new Set([
31139
31324
  "inputs",
31140
31325
  "vars",
31141
- "actor"
31326
+ "actor",
31327
+ "workflow_run"
31142
31328
  ]);
31143
31329
  /** The case's scenario over the file's: records merge key by key, the ref group is replaced whole, the rest replaced. */
31144
31330
  function mergeScenario(given, when) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simulci",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Static simulator and test framework for GitHub Actions: which jobs and steps run for a scenario, and why.",
5
5
  "keywords": [
6
6
  "ci",