willfire 0.1.12 → 0.1.13

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 (60) hide show
  1. package/dist/execute.d.ts +1 -1
  2. package/dist/execute.js +3 -1
  3. package/dist/expr/applyFunction.d.ts +11 -0
  4. package/dist/expr/applyFunction.js +41 -0
  5. package/dist/expr/asBool.d.ts +10 -0
  6. package/dist/expr/asBool.js +12 -0
  7. package/dist/expr/coalesceAnd.d.ts +7 -0
  8. package/dist/expr/coalesceAnd.js +17 -0
  9. package/dist/expr/coalesceOr.d.ts +6 -0
  10. package/dist/expr/coalesceOr.js +16 -0
  11. package/dist/expr/compare.d.ts +12 -0
  12. package/dist/expr/compare.js +54 -0
  13. package/dist/expr/cursor.d.ts +11 -0
  14. package/dist/expr/cursor.js +25 -0
  15. package/dist/expr/evaluate.d.ts +3 -0
  16. package/dist/expr/evaluate.js +6 -0
  17. package/dist/expr/evaluateValue.d.ts +14 -0
  18. package/dist/expr/evaluateValue.js +37 -0
  19. package/dist/expr/fromJson.d.ts +11 -0
  20. package/dist/expr/fromJson.js +29 -0
  21. package/dist/expr/index.d.ts +35 -0
  22. package/dist/expr/index.js +34 -0
  23. package/dist/expr/indexVal.d.ts +7 -0
  24. package/dist/expr/indexVal.js +34 -0
  25. package/dist/expr/lookup.d.ts +2 -0
  26. package/dist/expr/lookup.js +51 -0
  27. package/dist/expr/negate.d.ts +1 -0
  28. package/dist/expr/negate.js +3 -0
  29. package/dist/expr/parseAnd.d.ts +3 -0
  30. package/dist/expr/parseAnd.js +10 -0
  31. package/dist/expr/parseAtom.d.ts +3 -0
  32. package/dist/expr/parseAtom.js +43 -0
  33. package/dist/expr/parseCall.d.ts +9 -0
  34. package/dist/expr/parseCall.js +25 -0
  35. package/dist/expr/parseCmp.d.ts +3 -0
  36. package/dist/expr/parseCmp.js +12 -0
  37. package/dist/expr/parseOr.d.ts +11 -0
  38. package/dist/expr/parseOr.js +18 -0
  39. package/dist/expr/parsePrimary.d.ts +4 -0
  40. package/dist/expr/parsePrimary.js +16 -0
  41. package/dist/expr/parseUnary.d.ts +3 -0
  42. package/dist/expr/parseUnary.js +11 -0
  43. package/dist/expr/tokenize.d.ts +24 -0
  44. package/dist/expr/tokenize.js +74 -0
  45. package/dist/expr/truthy.d.ts +7 -0
  46. package/dist/expr/truthy.js +29 -0
  47. package/dist/expr/val.d.ts +72 -0
  48. package/dist/expr/val.js +1 -0
  49. package/dist/jobs/evalIf.d.ts +1 -1
  50. package/dist/jobs/evalIf.js +1 -1
  51. package/dist/jobs/expandJobs.d.ts +1 -1
  52. package/dist/jobs/expandJobs.js +1 -1
  53. package/dist/jobs/expandWorkflowJobs.d.ts +1 -1
  54. package/dist/jobs/prScope.d.ts +1 -1
  55. package/dist/matrix/expandMatrix.d.ts +1 -1
  56. package/dist/matrix/expandMatrixDetailed.d.ts +1 -1
  57. package/dist/matrix/expandMatrixDetailed.js +1 -1
  58. package/package.json +1 -1
  59. package/dist/expr.d.ts +0 -117
  60. package/dist/expr.js +0 -455
@@ -0,0 +1,12 @@
1
+ import { compare } from "./compare.js";
2
+ import { parseUnary } from "./parseUnary.js";
3
+ export function parseCmp(cur, scope) {
4
+ const left = parseUnary(cur, scope);
5
+ for (const op of ["==", "!=", "<=", ">=", "<", ">"]) {
6
+ if (cur.eatOp(op)) {
7
+ const right = parseUnary(cur, scope);
8
+ return compare(op, left, right);
9
+ }
10
+ }
11
+ return left;
12
+ }
@@ -0,0 +1,11 @@
1
+ import type { Cursor } from "./cursor.js";
2
+ import type { Scope, Val } from "./val.js";
3
+ /**
4
+ * Recursive descent over GitHub's precedence order, loosest first:
5
+ * `||`, then `&&`, then comparison, then `!`, then a primary.
6
+ *
7
+ * `&&` and `||` are value operators, not boolean ones — `a || b` yields the
8
+ * first truthy operand, which is why `(x || y) != '[]'` parses as a
9
+ * comparison against a coalesced value rather than a boolean.
10
+ */
11
+ export declare function parseOr(cur: Cursor, scope: Scope): Val;
@@ -0,0 +1,18 @@
1
+ import { coalesceOr } from "./coalesceOr.js";
2
+ import { parseAnd } from "./parseAnd.js";
3
+ /**
4
+ * Recursive descent over GitHub's precedence order, loosest first:
5
+ * `||`, then `&&`, then comparison, then `!`, then a primary.
6
+ *
7
+ * `&&` and `||` are value operators, not boolean ones — `a || b` yields the
8
+ * first truthy operand, which is why `(x || y) != '[]'` parses as a
9
+ * comparison against a coalesced value rather than a boolean.
10
+ */
11
+ export function parseOr(cur, scope) {
12
+ let left = parseAnd(cur, scope);
13
+ while (cur.eatOp("||")) {
14
+ const right = parseAnd(cur, scope);
15
+ left = coalesceOr(left, right);
16
+ }
17
+ return left;
18
+ }
@@ -0,0 +1,4 @@
1
+ import type { Cursor } from "./cursor.js";
2
+ import { type Scope, type Val } from "./val.js";
3
+ /** An atom plus any `[...]` accesses hanging off it, tightest-binding. */
4
+ export declare function parsePrimary(cur: Cursor, scope: Scope): Val;
@@ -0,0 +1,16 @@
1
+ import { indexVal } from "./indexVal.js";
2
+ import { parseAtom } from "./parseAtom.js";
3
+ import { parseOr } from "./parseOr.js";
4
+ import { UNKNOWN } from "./val.js";
5
+ /** An atom plus any `[...]` accesses hanging off it, tightest-binding. */
6
+ export function parsePrimary(cur, scope) {
7
+ let v = parseAtom(cur, scope);
8
+ while (cur.eatOp("[")) {
9
+ const idx = parseOr(cur, scope);
10
+ if (!cur.eatOp("]")) {
11
+ return UNKNOWN;
12
+ }
13
+ v = indexVal(v, idx);
14
+ }
15
+ return v;
16
+ }
@@ -0,0 +1,3 @@
1
+ import type { Cursor } from "./cursor.js";
2
+ import type { Scope, Val } from "./val.js";
3
+ export declare function parseUnary(cur: Cursor, scope: Scope): Val;
@@ -0,0 +1,11 @@
1
+ import { asBool } from "./asBool.js";
2
+ import { negate } from "./negate.js";
3
+ import { parsePrimary } from "./parsePrimary.js";
4
+ import { truthy } from "./truthy.js";
5
+ export function parseUnary(cur, scope) {
6
+ if (cur.eatOp("!")) {
7
+ const v = parseUnary(cur, scope);
8
+ return asBool(negate(truthy(v)));
9
+ }
10
+ return parsePrimary(cur, scope);
11
+ }
@@ -0,0 +1,24 @@
1
+ export type Tok = {
2
+ t: "str";
3
+ v: string;
4
+ } | {
5
+ t: "num";
6
+ v: number;
7
+ } | {
8
+ t: "bool";
9
+ v: boolean;
10
+ } | {
11
+ t: "null";
12
+ } | {
13
+ t: "path";
14
+ v: string;
15
+ } | {
16
+ t: "op";
17
+ v: string;
18
+ };
19
+ /**
20
+ * Split a condition into tokens, or return null if it contains something this
21
+ * evaluator has no token for. Returning null rather than throwing keeps the
22
+ * "unrecognized is unknown" rule in one place at the top of `evaluate`.
23
+ */
24
+ export declare function tokenize(src: string): Tok[] | null;
@@ -0,0 +1,74 @@
1
+ const OPS = ["&&", "||", "==", "!=", "<=", ">=", "!", "<", ">", "(", ")", "[", "]", ","];
2
+ /**
3
+ * Split a condition into tokens, or return null if it contains something this
4
+ * evaluator has no token for. Returning null rather than throwing keeps the
5
+ * "unrecognized is unknown" rule in one place at the top of `evaluate`.
6
+ */
7
+ export function tokenize(src) {
8
+ const out = [];
9
+ let i = 0;
10
+ while (i < src.length) {
11
+ const c = src[i];
12
+ if (c === " " || c === "\t" || c === "\n" || c === "\r") {
13
+ i++;
14
+ continue;
15
+ }
16
+ // Single-quoted string. GitHub escapes an inner quote by doubling it.
17
+ if (c === "'") {
18
+ let j = i + 1;
19
+ let s = "";
20
+ for (;;) {
21
+ if (j >= src.length) {
22
+ return null; // unterminated
23
+ }
24
+ if (src[j] === "'") {
25
+ if (src[j + 1] === "'") {
26
+ s += "'";
27
+ j += 2;
28
+ continue;
29
+ }
30
+ j++;
31
+ break;
32
+ }
33
+ s += src[j];
34
+ j++;
35
+ }
36
+ out.push({ t: "str", v: s });
37
+ i = j;
38
+ continue;
39
+ }
40
+ const op = OPS.find((o) => src.startsWith(o, i));
41
+ if (op != null) {
42
+ out.push({ t: "op", v: op });
43
+ i += op.length;
44
+ continue;
45
+ }
46
+ const word = /^[A-Za-z_][A-Za-z0-9_.\-]*/.exec(src.slice(i));
47
+ if (word != null) {
48
+ const w = word[0];
49
+ i += w.length;
50
+ const lower = w.toLowerCase();
51
+ if (lower === "true") {
52
+ out.push({ t: "bool", v: true });
53
+ }
54
+ else if (lower === "false") {
55
+ out.push({ t: "bool", v: false });
56
+ }
57
+ else if (lower === "null") {
58
+ out.push({ t: "null" });
59
+ }
60
+ else {
61
+ out.push({ t: "path", v: w });
62
+ }
63
+ continue;
64
+ }
65
+ const num = /^-?\d+(\.\d+)?/.exec(src.slice(i));
66
+ if (num != null) {
67
+ out.push({ t: "num", v: Number(num[0]) });
68
+ i += num[0].length;
69
+ continue;
70
+ }
71
+ return null; // a character we have no token for
72
+ }
73
+ return out;
74
+ }
@@ -0,0 +1,7 @@
1
+ import type { Val } from "./val.js";
2
+ /**
3
+ * GitHub's truthiness: empty string, zero, `false` and null are false, and
4
+ * every other value is true. `'0'` and `'false'` are non-empty strings, so
5
+ * both are true — the same trap as JavaScript, kept deliberately identical.
6
+ */
7
+ export declare function truthy(val: Val): boolean | null;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * GitHub's truthiness: empty string, zero, `false` and null are false, and
3
+ * every other value is true. `'0'` and `'false'` are non-empty strings, so
4
+ * both are true — the same trap as JavaScript, kept deliberately identical.
5
+ */
6
+ export function truthy(val) {
7
+ switch (val.kind) {
8
+ case "truthy":
9
+ return true;
10
+ case "falsy":
11
+ return false;
12
+ case "unknown":
13
+ return null;
14
+ // GitHub does cast an array or an object to a boolean, but no workflow
15
+ // asks it to, and the answer is not worth guessing at to find out.
16
+ case "json":
17
+ return null;
18
+ case "value": {
19
+ const v = val.v;
20
+ if (typeof v === "boolean") {
21
+ return v;
22
+ }
23
+ if (typeof v === "number") {
24
+ return v !== 0;
25
+ }
26
+ return v !== "";
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * A partially-known value.
3
+ *
4
+ * `truthy` and `falsy` exist because short-circuiting yields truthiness
5
+ * without a value: `unknown && false` is falsy, but there is no string to
6
+ * hand back for it. Collapsing those into `unknown` would throw away the
7
+ * only fact worth having.
8
+ */
9
+ export type Val = {
10
+ kind: "value";
11
+ v: string | number | boolean;
12
+ }
13
+ /**
14
+ * An array or an object, which only `fromJSON` produces. Kept apart from
15
+ * `value` because nothing else in the language accepts one: comparison
16
+ * refuses it, and its truthiness is not modelled. Matrix expansion is the
17
+ * one consumer, and it asks for the array directly.
18
+ */
19
+ | {
20
+ kind: "json";
21
+ v: unknown[] | Record<string, unknown>;
22
+ } | {
23
+ kind: "truthy";
24
+ } | {
25
+ kind: "falsy";
26
+ } | {
27
+ kind: "unknown";
28
+ };
29
+ export declare const UNKNOWN: Val;
30
+ /** What a bare path resolves against. Anything absent is unknown, not empty. */
31
+ export interface Scope {
32
+ /**
33
+ * The inputs the caller passed, plus declared defaults for the ones it did
34
+ * not. A key mapped to `unknown` is deliberate: the caller supplied it, but
35
+ * as a `${{ }}` template we cannot evaluate. That is different from absent.
36
+ */
37
+ inputs?: Record<string, Val>;
38
+ /** `github.*` values that are fixed for the run being predicted. */
39
+ github?: Record<string, string>;
40
+ /**
41
+ * Outputs of jobs this workflow's jobs `needs`, keyed by job id.
42
+ *
43
+ * `outputs` is the *complete* set for that job, which is what makes a key
44
+ * that is absent from it mean the empty string — the same answer the runner
45
+ * gives for an output no step wrote. Handing in a partial map is therefore a
46
+ * lie, not a shortcut: leave the job out entirely instead, and every lookup
47
+ * against it stays unknown.
48
+ *
49
+ * The values are raw strings, because that is what a step wrote to
50
+ * `$GITHUB_OUTPUT` and what the runner substitutes. Parsing eagerly would
51
+ * break the guards written against them — `!= '[]'` compares a string to a
52
+ * string, and an array on the left makes it unknown. `fromJSON` is the only
53
+ * thing that turns one into a structure, at the point the workflow asks.
54
+ */
55
+ needs?: Record<string, {
56
+ outputs: Record<string, string>;
57
+ }>;
58
+ /**
59
+ * Outputs of steps that already ran, keyed by step id. Only one caller can
60
+ * fill this honestly: the executor's step walk (see ../execute/runSteps.ts),
61
+ * which is the single place a step has actually run by the time an
62
+ * expression reads it. The contract is the same as `needs`: a step named
63
+ * here carries its *complete* output set, so an absent key is the empty
64
+ * string the runner substitutes, while a step this map does not name stays
65
+ * unknown. A skipped step is present with no outputs at all — which is
66
+ * exactly what lets `steps.a.outputs.x || steps.b.outputs.x` coalesce past
67
+ * it.
68
+ */
69
+ steps?: Record<string, {
70
+ outputs: Record<string, string>;
71
+ }>;
72
+ }
@@ -0,0 +1 @@
1
+ export const UNKNOWN = { kind: "unknown" };
@@ -1,4 +1,4 @@
1
- import { type Scope } from "../expr.js";
1
+ import type { Scope } from "../expr/val.js";
2
2
  /**
3
3
  * Return run|skipped|unknown for a job-level `if:`.
4
4
  *
@@ -1,4 +1,4 @@
1
- import { evaluate } from "../expr.js";
1
+ import { evaluate } from "../expr/evaluate.js";
2
2
  import { prScope } from "./prScope.js";
3
3
  /**
4
4
  * Return run|skipped|unknown for a job-level `if:`.
@@ -1,4 +1,4 @@
1
- import { type Scope } from "../expr.js";
1
+ import { type Scope } from "../expr/val.js";
2
2
  import type { JobExecutor } from "../execute.js";
3
3
  import type { Ctx, ExpandedJob, Workflow, WorkflowReader, WorkflowSource } from "../types.js";
4
4
  export declare function expandJobs(wf: Workflow, ctx: Ctx, reader: WorkflowReader, source: WorkflowSource, depth?: number, prefix?: string, prefixResolved?: boolean, scope?: Scope, executor?: JobExecutor): Promise<ExpandedJob[]>;
@@ -1,5 +1,5 @@
1
1
  import { parse as parseYaml } from "yaml";
2
- import { UNKNOWN } from "../expr.js";
2
+ import { UNKNOWN } from "../expr/val.js";
3
3
  import { expandMatrixDetailed } from "../matrix/expandMatrixDetailed.js";
4
4
  import { jobDisplayName } from "../names/jobDisplayName.js";
5
5
  import { skippedDisplayName } from "../names/skippedDisplayName.js";
@@ -1,4 +1,4 @@
1
- import type { Scope } from "../expr.js";
1
+ import type { Scope } from "../expr/val.js";
2
2
  import type { JobExecutor } from "../execute.js";
3
3
  import type { Ctx, ExpandedJob, Workflow, WorkflowReader, WorkflowSource } from "../types.js";
4
4
  /**
@@ -1,4 +1,4 @@
1
- import type { Scope } from "../expr.js";
1
+ import type { Scope } from "../expr/val.js";
2
2
  /**
3
3
  * A scope with the fixed pull-request facts filled in.
4
4
  *
@@ -1,4 +1,4 @@
1
- import type { Scope } from "../expr.js";
1
+ import type { Scope } from "../expr/val.js";
2
2
  import type { Combo } from "../types.js";
3
3
  /** Return list of matrix combination dicts, or null if dynamic. */
4
4
  export declare function expandMatrix(strategy: any, scope?: Scope): Combo[] | null;
@@ -1,3 +1,3 @@
1
- import { type Scope } from "../expr.js";
1
+ import type { Scope } from "../expr/val.js";
2
2
  import type { DetailedCombos } from "../types.js";
3
3
  export declare function expandMatrixDetailed(strategy: any, scope?: Scope): DetailedCombos;
@@ -1,4 +1,4 @@
1
- import { evaluateValue } from "../expr.js";
1
+ import { evaluateValue } from "../expr/evaluateValue.js";
2
2
  /**
3
3
  * The values of one matrix axis, or null when they cannot be known.
4
4
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "willfire",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Predict the set of CI check entries GitHub Actions will create for a pull request",
5
5
  "license": "MIT",
6
6
  "packageManager": "pnpm@10.33.0",
package/dist/expr.d.ts DELETED
@@ -1,117 +0,0 @@
1
- /**
2
- * A tri-state evaluator for the slice of GitHub expressions that job `if:`
3
- * conditions actually use.
4
- *
5
- * The point is not to reimplement the expression language. It is to stop
6
- * returning `unknown` for conditions that are already decided. A reusable
7
- * workflow's jobs commonly guard on the caller's own literals:
8
- *
9
- * if: ${{ github.event_name == 'pull_request'
10
- * && (inputs.gates == '' || contains(inputs.gates, '"mutation"'))
11
- * && (needs.detect.outputs.mutation_languages || ...) != '[]' }}
12
- *
13
- * The last clause needs a job that has not run yet. The middle one does not:
14
- * the caller passed `gates` as a literal string, and it does not contain
15
- * `"mutation"`, so the clause is false, so the whole `&&` is false whatever
16
- * `detect` reports. Deciding that is the difference between a predictable
17
- * check name and a hole in the prediction.
18
- *
19
- * Two ideas carry the whole file:
20
- *
21
- * 1. **Truthiness can be known when the value is not.** `A && B` with an
22
- * unknown `A` and a false `B` is false either way. So the lattice has four
23
- * points, not three: a concrete value, known-truthy, known-falsy, and
24
- * nothing at all.
25
- *
26
- * 2. **Unrecognized is unknown, never a guess.** An unparseable condition, an
27
- * unsupported function, a comparison between types we do not model — all
28
- * collapse to `unknown` and leave the caller exactly where it was. This
29
- * adds no tolerance and no third outcome; it converts conditions that are
30
- * already decidable into the answer they already have.
31
- */
32
- /**
33
- * A partially-known value.
34
- *
35
- * `truthy` and `falsy` exist because short-circuiting yields truthiness
36
- * without a value: `unknown && false` is falsy, but there is no string to
37
- * hand back for it. Collapsing those into `unknown` would throw away the
38
- * only fact worth having.
39
- */
40
- export type Val = {
41
- kind: "value";
42
- v: string | number | boolean;
43
- }
44
- /**
45
- * An array or an object, which only `fromJSON` produces. Kept apart from
46
- * `value` because nothing else in the language accepts one: comparison
47
- * refuses it, and its truthiness is not modelled. Matrix expansion is the
48
- * one consumer, and it asks for the array directly.
49
- */
50
- | {
51
- kind: "json";
52
- v: unknown[] | Record<string, unknown>;
53
- } | {
54
- kind: "truthy";
55
- } | {
56
- kind: "falsy";
57
- } | {
58
- kind: "unknown";
59
- };
60
- export declare const UNKNOWN: Val;
61
- /** What a bare path resolves against. Anything absent is unknown, not empty. */
62
- export interface Scope {
63
- /**
64
- * The inputs the caller passed, plus declared defaults for the ones it did
65
- * not. A key mapped to `unknown` is deliberate: the caller supplied it, but
66
- * as a `${{ }}` template we cannot evaluate. That is different from absent.
67
- */
68
- inputs?: Record<string, Val>;
69
- /** `github.*` values that are fixed for the run being predicted. */
70
- github?: Record<string, string>;
71
- /**
72
- * Outputs of jobs this workflow's jobs `needs`, keyed by job id.
73
- *
74
- * `outputs` is the *complete* set for that job, which is what makes a key
75
- * that is absent from it mean the empty string — the same answer the runner
76
- * gives for an output no step wrote. Handing in a partial map is therefore a
77
- * lie, not a shortcut: leave the job out entirely instead, and every lookup
78
- * against it stays unknown.
79
- *
80
- * The values are raw strings, because that is what a step wrote to
81
- * `$GITHUB_OUTPUT` and what the runner substitutes. Parsing eagerly would
82
- * break the guards written against them — `!= '[]'` compares a string to a
83
- * string, and an array on the left makes it unknown. `fromJSON` is the only
84
- * thing that turns one into a structure, at the point the workflow asks.
85
- */
86
- needs?: Record<string, {
87
- outputs: Record<string, string>;
88
- }>;
89
- /**
90
- * Outputs of steps that already ran, keyed by step id. Only one caller can
91
- * fill this honestly: the executor's step walk (see execute.ts), which is
92
- * the single place a step has actually run by the time an expression reads
93
- * it. The contract is the same as `needs`: a step named here carries its
94
- * *complete* output set, so an absent key is the empty string the runner
95
- * substitutes, while a step this map does not name stays unknown. A skipped
96
- * step is present with no outputs at all — which is exactly what lets
97
- * `steps.a.outputs.x || steps.b.outputs.x` coalesce past it.
98
- */
99
- steps?: Record<string, {
100
- outputs: Record<string, string>;
101
- }>;
102
- }
103
- /**
104
- * Evaluate an expression to a value, or UNKNOWN when it cannot be settled.
105
- *
106
- * The `${{ }}` wrapper is optional in `if:` and stripped when present. An
107
- * expression that is only *partly* wrapped (`foo ${{ bar }} baz`) is a string
108
- * interpolation rather than an expression, and is not modelled.
109
- *
110
- * A `if:` wants {@link evaluate}, which is this narrowed to truthiness. This
111
- * one is for the places that need the value itself — a matrix axis written as
112
- * `${{ fromJSON(...) }}` is an array, and its truthiness says nothing about
113
- * how many jobs it schedules.
114
- */
115
- export declare function evaluateValue(expr: string, scope?: Scope): Val;
116
- /** Evaluate a condition to a truthiness, or null when it cannot be settled. */
117
- export declare function evaluate(cond: string, scope?: Scope): boolean | null;