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
package/dist/execute.d.ts CHANGED
@@ -29,7 +29,7 @@
29
29
  * already satisfied by materializing the tree. A bare checkout is therefore
30
30
  * recorded as done; a checkout *with inputs* is not modelled and stops.
31
31
  */
32
- import { type Scope } from "./expr.js";
32
+ import { type Scope } from "./expr/val.js";
33
33
  import type { ResolveRef, WorkflowSource } from "./types.js";
34
34
  /**
35
35
  * Permission to execute named jobs from one repo's workflows.
package/dist/execute.js CHANGED
@@ -34,7 +34,9 @@ import { mkdir, mkdtemp, readdir, readFile, stat, writeFile } from "node:fs/prom
34
34
  import { tmpdir } from "node:os";
35
35
  import { join, resolve } from "node:path";
36
36
  import { parse as parseYaml } from "yaml";
37
- import { evaluate, evaluateValue, UNKNOWN } from "./expr.js";
37
+ import { evaluate } from "./expr/evaluate.js";
38
+ import { evaluateValue } from "./expr/evaluateValue.js";
39
+ import { UNKNOWN } from "./expr/val.js";
38
40
  /** `owner/repo:job1,job2` as the CLI spells a grant. */
39
41
  export function parseGrant(spec) {
40
42
  const colon = spec.indexOf(":");
@@ -0,0 +1,11 @@
1
+ import { type Val } from "./val.js";
2
+ /**
3
+ * The functions worth modelling.
4
+ *
5
+ * `always()` is true by definition. `contains` on two known strings is the one
6
+ * that unlocks the fleet's `gates` pattern. `fromJSON` is what a dynamic matrix
7
+ * axis is built out of. `success()`, `failure()` and `cancelled()` depend on
8
+ * jobs that have not run, and everything else is simply not modelled — all
9
+ * unknown.
10
+ */
11
+ export declare function applyFunction(name: string, args: Val[]): Val;
@@ -0,0 +1,41 @@
1
+ import { asBool } from "./asBool.js";
2
+ import { fromJson } from "./fromJson.js";
3
+ import { UNKNOWN } from "./val.js";
4
+ /**
5
+ * The functions worth modelling.
6
+ *
7
+ * `always()` is true by definition. `contains` on two known strings is the one
8
+ * that unlocks the fleet's `gates` pattern. `fromJSON` is what a dynamic matrix
9
+ * axis is built out of. `success()`, `failure()` and `cancelled()` depend on
10
+ * jobs that have not run, and everything else is simply not modelled — all
11
+ * unknown.
12
+ */
13
+ export function applyFunction(name, args) {
14
+ if (name === "always") {
15
+ return { kind: "value", v: true };
16
+ }
17
+ if (name === "fromjson" && args.length === 1) {
18
+ return fromJson(args[0]);
19
+ }
20
+ if (name === "contains" && args.length === 2) {
21
+ const [hay, needle] = args;
22
+ if (hay.kind !== "value" || needle.kind !== "value") {
23
+ return UNKNOWN;
24
+ }
25
+ if (typeof hay.v !== "string" || typeof needle.v !== "string") {
26
+ return UNKNOWN;
27
+ }
28
+ return asBool(hay.v.includes(needle.v));
29
+ }
30
+ if ((name === "startswith" || name === "endswith") && args.length === 2) {
31
+ const [s, part] = args;
32
+ if (s.kind !== "value" || part.kind !== "value") {
33
+ return UNKNOWN;
34
+ }
35
+ if (typeof s.v !== "string" || typeof part.v !== "string") {
36
+ return UNKNOWN;
37
+ }
38
+ return asBool(name === "startswith" ? s.v.startsWith(part.v) : s.v.endsWith(part.v));
39
+ }
40
+ return UNKNOWN;
41
+ }
@@ -0,0 +1,10 @@
1
+ import { type Val } from "./val.js";
2
+ /**
3
+ * Wrap a decided answer as a concrete boolean value.
4
+ *
5
+ * Comparison, negation and the string functions all yield a real boolean, so
6
+ * they produce a `value` — which keeps `!x == true` comparable. Only
7
+ * short-circuiting produces the bare `truthy`/`falsy` points, because that is
8
+ * the one case where truthiness is known and the value is not.
9
+ */
10
+ export declare function asBool(b: boolean | null): Val;
@@ -0,0 +1,12 @@
1
+ import { UNKNOWN } from "./val.js";
2
+ /**
3
+ * Wrap a decided answer as a concrete boolean value.
4
+ *
5
+ * Comparison, negation and the string functions all yield a real boolean, so
6
+ * they produce a `value` — which keeps `!x == true` comparable. Only
7
+ * short-circuiting produces the bare `truthy`/`falsy` points, because that is
8
+ * the one case where truthiness is known and the value is not.
9
+ */
10
+ export function asBool(b) {
11
+ return b === null ? UNKNOWN : { kind: "value", v: b };
12
+ }
@@ -0,0 +1,7 @@
1
+ import { type Val } from "./val.js";
2
+ /**
3
+ * `A && B`. Short-circuits from either side: a falsy left decides it, and so
4
+ * does a falsy right, because a truthy left would then yield the falsy right.
5
+ * Only "left unknown, right not falsy" is genuinely undecided.
6
+ */
7
+ export declare function coalesceAnd(left: Val, right: Val): Val;
@@ -0,0 +1,17 @@
1
+ import { truthy } from "./truthy.js";
2
+ import { UNKNOWN } from "./val.js";
3
+ /**
4
+ * `A && B`. Short-circuits from either side: a falsy left decides it, and so
5
+ * does a falsy right, because a truthy left would then yield the falsy right.
6
+ * Only "left unknown, right not falsy" is genuinely undecided.
7
+ */
8
+ export function coalesceAnd(left, right) {
9
+ const l = truthy(left);
10
+ if (l === false) {
11
+ return left;
12
+ }
13
+ if (l === true) {
14
+ return right;
15
+ }
16
+ return truthy(right) === false ? { kind: "falsy" } : UNKNOWN;
17
+ }
@@ -0,0 +1,6 @@
1
+ import { type Val } from "./val.js";
2
+ /**
3
+ * `A || B`. The mirror image: a truthy left decides it, and so does a truthy
4
+ * right, since a falsy left would then yield the truthy right.
5
+ */
6
+ export declare function coalesceOr(left: Val, right: Val): Val;
@@ -0,0 +1,16 @@
1
+ import { truthy } from "./truthy.js";
2
+ import { UNKNOWN } from "./val.js";
3
+ /**
4
+ * `A || B`. The mirror image: a truthy left decides it, and so does a truthy
5
+ * right, since a falsy left would then yield the truthy right.
6
+ */
7
+ export function coalesceOr(left, right) {
8
+ const l = truthy(left);
9
+ if (l === true) {
10
+ return left;
11
+ }
12
+ if (l === false) {
13
+ return right;
14
+ }
15
+ return truthy(right) === true ? { kind: "truthy" } : UNKNOWN;
16
+ }
@@ -0,0 +1,12 @@
1
+ import { type Val } from "./val.js";
2
+ /**
3
+ * Comparison, deliberately narrow: both sides must be concrete and of the same
4
+ * primitive type.
5
+ *
6
+ * GitHub coerces across types when it compares, and the corner cases are
7
+ * genuinely surprising (`'' == 0` is true). Every comparison that matters in
8
+ * practice is string-to-string — `inputs.gates == ''`,
9
+ * `github.event_name == 'pull_request'` — so modelling the coercion table
10
+ * would add risk without adding reach. Mixed types return unknown.
11
+ */
12
+ export declare function compare(op: string, left: Val, right: Val): Val;
@@ -0,0 +1,54 @@
1
+ import { asBool } from "./asBool.js";
2
+ import { UNKNOWN } from "./val.js";
3
+ /**
4
+ * Comparison, deliberately narrow: both sides must be concrete and of the same
5
+ * primitive type.
6
+ *
7
+ * GitHub coerces across types when it compares, and the corner cases are
8
+ * genuinely surprising (`'' == 0` is true). Every comparison that matters in
9
+ * practice is string-to-string — `inputs.gates == ''`,
10
+ * `github.event_name == 'pull_request'` — so modelling the coercion table
11
+ * would add risk without adding reach. Mixed types return unknown.
12
+ */
13
+ export function compare(op, left, right) {
14
+ // GitHub compares arrays and objects by instance, and two written sides are
15
+ // never the same instance: `==` is false, `!=` is true, ordering unknowable.
16
+ if (left.kind === "json" || right.kind === "json") {
17
+ if (op === "==") {
18
+ return asBool(false);
19
+ }
20
+ if (op === "!=") {
21
+ return asBool(true);
22
+ }
23
+ return UNKNOWN;
24
+ }
25
+ if (left.kind !== "value" || right.kind !== "value") {
26
+ return UNKNOWN;
27
+ }
28
+ const a = left.v;
29
+ const b = right.v;
30
+ if (typeof a !== typeof b) {
31
+ return UNKNOWN;
32
+ }
33
+ if (op === "==") {
34
+ return asBool(a === b);
35
+ }
36
+ if (op === "!=") {
37
+ return asBool(a !== b);
38
+ }
39
+ // Ordering on booleans is not modelled. GitHub coerces them to numbers, and
40
+ // the answer is never one a workflow author meant to ask for.
41
+ if (typeof a === "boolean" || typeof b === "boolean") {
42
+ return UNKNOWN;
43
+ }
44
+ if (op === "<") {
45
+ return asBool(a < b);
46
+ }
47
+ if (op === "<=") {
48
+ return asBool(a <= b);
49
+ }
50
+ if (op === ">") {
51
+ return asBool(a > b);
52
+ }
53
+ return asBool(a >= b);
54
+ }
@@ -0,0 +1,11 @@
1
+ import type { Tok } from "./tokenize.js";
2
+ /** A read position over a token stream, shared by the parse functions. */
3
+ export declare class Cursor {
4
+ private readonly toks;
5
+ private pos;
6
+ constructor(toks: Tok[]);
7
+ peek(): Tok | undefined;
8
+ advance(): void;
9
+ eatOp(v: string): boolean;
10
+ done(): boolean;
11
+ }
@@ -0,0 +1,25 @@
1
+ /** A read position over a token stream, shared by the parse functions. */
2
+ export class Cursor {
3
+ toks;
4
+ pos = 0;
5
+ constructor(toks) {
6
+ this.toks = toks;
7
+ }
8
+ peek() {
9
+ return this.toks[this.pos];
10
+ }
11
+ advance() {
12
+ this.pos++;
13
+ }
14
+ eatOp(v) {
15
+ const t = this.peek();
16
+ if (t != null && t.t === "op" && t.v === v) {
17
+ this.pos++;
18
+ return true;
19
+ }
20
+ return false;
21
+ }
22
+ done() {
23
+ return this.pos >= this.toks.length;
24
+ }
25
+ }
@@ -0,0 +1,3 @@
1
+ import type { Scope } from "./val.js";
2
+ /** Evaluate a condition to a truthiness, or null when it cannot be settled. */
3
+ export declare function evaluate(cond: string, scope?: Scope): boolean | null;
@@ -0,0 +1,6 @@
1
+ import { evaluateValue } from "./evaluateValue.js";
2
+ import { truthy } from "./truthy.js";
3
+ /** Evaluate a condition to a truthiness, or null when it cannot be settled. */
4
+ export function evaluate(cond, scope = {}) {
5
+ return truthy(evaluateValue(cond, scope));
6
+ }
@@ -0,0 +1,14 @@
1
+ import { type Scope, type Val } from "./val.js";
2
+ /**
3
+ * Evaluate an expression to a value, or UNKNOWN when it cannot be settled.
4
+ *
5
+ * The `${{ }}` wrapper is optional in `if:` and stripped when present. An
6
+ * expression that is only *partly* wrapped (`foo ${{ bar }} baz`) is a string
7
+ * interpolation rather than an expression, and is not modelled.
8
+ *
9
+ * A `if:` wants {@link evaluate}, which is this narrowed to truthiness. This
10
+ * one is for the places that need the value itself — a matrix axis written as
11
+ * `${{ fromJSON(...) }}` is an array, and its truthiness says nothing about
12
+ * how many jobs it schedules.
13
+ */
14
+ export declare function evaluateValue(expr: string, scope?: Scope): Val;
@@ -0,0 +1,37 @@
1
+ import { Cursor } from "./cursor.js";
2
+ import { parseOr } from "./parseOr.js";
3
+ import { tokenize } from "./tokenize.js";
4
+ import { UNKNOWN } from "./val.js";
5
+ /**
6
+ * Evaluate an expression to a value, or UNKNOWN when it cannot be settled.
7
+ *
8
+ * The `${{ }}` wrapper is optional in `if:` and stripped when present. An
9
+ * expression that is only *partly* wrapped (`foo ${{ bar }} baz`) is a string
10
+ * interpolation rather than an expression, and is not modelled.
11
+ *
12
+ * A `if:` wants {@link evaluate}, which is this narrowed to truthiness. This
13
+ * one is for the places that need the value itself — a matrix axis written as
14
+ * `${{ fromJSON(...) }}` is an array, and its truthiness says nothing about
15
+ * how many jobs it schedules.
16
+ */
17
+ export function evaluateValue(expr, scope = {}) {
18
+ const stripped = expr.trim().replace(/^\$\{\{(.*)\}\}$/s, "$1").trim();
19
+ if (stripped === "") {
20
+ return UNKNOWN;
21
+ }
22
+ if (stripped.includes("${{")) {
23
+ return UNKNOWN;
24
+ }
25
+ const toks = tokenize(stripped);
26
+ if (toks == null || toks.length === 0) {
27
+ return UNKNOWN;
28
+ }
29
+ const cur = new Cursor(toks);
30
+ const val = parseOr(cur, scope);
31
+ // Trailing tokens mean the grammar did not cover this expression; whatever
32
+ // was parsed describes only a prefix of it, so it decides nothing.
33
+ if (!cur.done()) {
34
+ return UNKNOWN;
35
+ }
36
+ return val;
37
+ }
@@ -0,0 +1,11 @@
1
+ import { type Val } from "./val.js";
2
+ /**
3
+ * `fromJSON(s)` on a known string.
4
+ *
5
+ * The result is sorted into the lattice rather than dropped in whole: a scalar
6
+ * is an ordinary value and stays comparable, `null` has a known truthiness and
7
+ * no useful value, and only an array or an object needs the `json` point.
8
+ * Anything unparseable is unknown — a workflow that reaches this at runtime
9
+ * fails, and predicting a failure is not this function's job.
10
+ */
11
+ export declare function fromJson(arg: Val): Val;
@@ -0,0 +1,29 @@
1
+ import { UNKNOWN } from "./val.js";
2
+ /**
3
+ * `fromJSON(s)` on a known string.
4
+ *
5
+ * The result is sorted into the lattice rather than dropped in whole: a scalar
6
+ * is an ordinary value and stays comparable, `null` has a known truthiness and
7
+ * no useful value, and only an array or an object needs the `json` point.
8
+ * Anything unparseable is unknown — a workflow that reaches this at runtime
9
+ * fails, and predicting a failure is not this function's job.
10
+ */
11
+ export function fromJson(arg) {
12
+ if (arg.kind !== "value" || typeof arg.v !== "string") {
13
+ return UNKNOWN;
14
+ }
15
+ let parsed;
16
+ try {
17
+ parsed = JSON.parse(arg.v);
18
+ }
19
+ catch {
20
+ return UNKNOWN;
21
+ }
22
+ if (parsed === null) {
23
+ return { kind: "falsy" };
24
+ }
25
+ if (typeof parsed === "object") {
26
+ return { kind: "json", v: parsed };
27
+ }
28
+ return { kind: "value", v: parsed };
29
+ }
@@ -0,0 +1,35 @@
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 module:
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
+ export { evaluate } from "./evaluate.js";
33
+ export { evaluateValue } from "./evaluateValue.js";
34
+ export { UNKNOWN } from "./val.js";
35
+ export type { Scope, Val } from "./val.js";
@@ -0,0 +1,34 @@
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 module:
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
+ export { evaluate } from "./evaluate.js";
33
+ export { evaluateValue } from "./evaluateValue.js";
34
+ export { UNKNOWN } from "./val.js";
@@ -0,0 +1,7 @@
1
+ import { type Val } from "./val.js";
2
+ /**
3
+ * `a[i]` on what `fromJSON` produced. GitHub yields `null` for a missing
4
+ * index, which models as the empty string — the coercion GitHub applies.
5
+ * Anything off that path is unknown, never a guess.
6
+ */
7
+ export declare function indexVal(base: Val, idx: Val): Val;
@@ -0,0 +1,34 @@
1
+ import { UNKNOWN } from "./val.js";
2
+ /**
3
+ * `a[i]` on what `fromJSON` produced. GitHub yields `null` for a missing
4
+ * index, which models as the empty string — the coercion GitHub applies.
5
+ * Anything off that path is unknown, never a guess.
6
+ */
7
+ export function indexVal(base, idx) {
8
+ if (base.kind !== "json") {
9
+ return UNKNOWN;
10
+ }
11
+ if (idx.kind !== "value") {
12
+ return UNKNOWN;
13
+ }
14
+ let el;
15
+ if (Array.isArray(base.v)) {
16
+ if (typeof idx.v !== "number") {
17
+ return UNKNOWN;
18
+ }
19
+ el = base.v[idx.v];
20
+ }
21
+ else {
22
+ if (typeof idx.v !== "string") {
23
+ return UNKNOWN;
24
+ }
25
+ el = base.v[idx.v];
26
+ }
27
+ if (el == null) {
28
+ return { kind: "value", v: "" };
29
+ }
30
+ if (typeof el === "object") {
31
+ return { kind: "json", v: el };
32
+ }
33
+ return { kind: "value", v: el };
34
+ }
@@ -0,0 +1,2 @@
1
+ import { type Scope, type Val } from "./val.js";
2
+ export declare function lookup(scope: Scope, path: string): Val;
@@ -0,0 +1,51 @@
1
+ import { UNKNOWN } from "./val.js";
2
+ export function lookup(scope, path) {
3
+ const dot = path.indexOf(".");
4
+ if (dot < 0) {
5
+ return UNKNOWN;
6
+ }
7
+ const head = path.slice(0, dot);
8
+ const rest = path.slice(dot + 1);
9
+ if (head === "inputs") {
10
+ return scope.inputs?.[rest] ?? UNKNOWN;
11
+ }
12
+ if (head === "github") {
13
+ const v = scope.github?.[rest];
14
+ return v === undefined ? UNKNOWN : { kind: "value", v };
15
+ }
16
+ if (head === "needs") {
17
+ // Only `needs.<job>.outputs.<name>` is modelled. `needs.<job>.result`
18
+ // is a verdict on a run that has not happened; anything else is not a
19
+ // shape the context has.
20
+ const parts = rest.split(".");
21
+ if (parts.length !== 3 || parts[1] !== "outputs") {
22
+ return UNKNOWN;
23
+ }
24
+ const job = scope.needs?.[parts[0]];
25
+ if (job == null) {
26
+ return UNKNOWN;
27
+ }
28
+ // A known job's missing output is the empty string, not a hole: the
29
+ // caller promised the set is complete, and that is what the runner
30
+ // substitutes for an output no step wrote.
31
+ return { kind: "value", v: job.outputs[parts[2]] ?? "" };
32
+ }
33
+ if (head === "steps") {
34
+ // The same shape as `needs`, for the same reason: only
35
+ // `steps.<id>.outputs.<name>` is modelled. `steps.<id>.outcome` and
36
+ // `.conclusion` are verdicts on how a step ran, which the executor does
37
+ // not track — a failed step fails the whole execution instead.
38
+ const parts = rest.split(".");
39
+ if (parts.length !== 3 || parts[1] !== "outputs") {
40
+ return UNKNOWN;
41
+ }
42
+ const step = scope.steps?.[parts[0]];
43
+ if (step == null) {
44
+ return UNKNOWN;
45
+ }
46
+ return { kind: "value", v: step.outputs[parts[2]] ?? "" };
47
+ }
48
+ // `matrix.*`, `env.*`, `vars.*`, `secrets.*`: all require something that
49
+ // has not happened yet at prediction time.
50
+ return UNKNOWN;
51
+ }
@@ -0,0 +1 @@
1
+ export declare function negate(b: boolean | null): boolean | null;
@@ -0,0 +1,3 @@
1
+ export function negate(b) {
2
+ return b === null ? null : !b;
3
+ }
@@ -0,0 +1,3 @@
1
+ import type { Cursor } from "./cursor.js";
2
+ import type { Scope, Val } from "./val.js";
3
+ export declare function parseAnd(cur: Cursor, scope: Scope): Val;
@@ -0,0 +1,10 @@
1
+ import { coalesceAnd } from "./coalesceAnd.js";
2
+ import { parseCmp } from "./parseCmp.js";
3
+ export function parseAnd(cur, scope) {
4
+ let left = parseCmp(cur, scope);
5
+ while (cur.eatOp("&&")) {
6
+ const right = parseCmp(cur, scope);
7
+ left = coalesceAnd(left, right);
8
+ }
9
+ return left;
10
+ }
@@ -0,0 +1,3 @@
1
+ import type { Cursor } from "./cursor.js";
2
+ import { type Scope, type Val } from "./val.js";
3
+ export declare function parseAtom(cur: Cursor, scope: Scope): Val;
@@ -0,0 +1,43 @@
1
+ import { lookup } from "./lookup.js";
2
+ import { parseCall } from "./parseCall.js";
3
+ import { parseOr } from "./parseOr.js";
4
+ import { UNKNOWN } from "./val.js";
5
+ export function parseAtom(cur, scope) {
6
+ const t = cur.peek();
7
+ if (t == null) {
8
+ return UNKNOWN;
9
+ }
10
+ if (t.t === "op" && t.v === "(") {
11
+ cur.advance();
12
+ const v = parseOr(cur, scope);
13
+ if (!cur.eatOp(")")) {
14
+ return UNKNOWN;
15
+ }
16
+ return v;
17
+ }
18
+ if (t.t === "str") {
19
+ cur.advance();
20
+ return { kind: "value", v: t.v };
21
+ }
22
+ if (t.t === "num") {
23
+ cur.advance();
24
+ return { kind: "value", v: t.v };
25
+ }
26
+ if (t.t === "bool") {
27
+ cur.advance();
28
+ return { kind: "value", v: t.v };
29
+ }
30
+ if (t.t === "null") {
31
+ cur.advance();
32
+ return { kind: "value", v: "" };
33
+ }
34
+ if (t.t === "path") {
35
+ cur.advance();
36
+ // A `(` right after a name makes it a call, not a path.
37
+ if (cur.eatOp("(")) {
38
+ return parseCall(cur, scope, t.v);
39
+ }
40
+ return lookup(scope, t.v);
41
+ }
42
+ return UNKNOWN;
43
+ }
@@ -0,0 +1,9 @@
1
+ import type { Cursor } from "./cursor.js";
2
+ import { type Scope, type Val } from "./val.js";
3
+ /**
4
+ * A function call, entered after its `(` was consumed. Arguments are always
5
+ * parsed, even for functions we cannot evaluate — the tokens have to be
6
+ * consumed either way or the rest of the expression parses against the wrong
7
+ * position.
8
+ */
9
+ export declare function parseCall(cur: Cursor, scope: Scope, name: string): Val;
@@ -0,0 +1,25 @@
1
+ import { applyFunction } from "./applyFunction.js";
2
+ import { parseOr } from "./parseOr.js";
3
+ import { UNKNOWN } from "./val.js";
4
+ /**
5
+ * A function call, entered after its `(` was consumed. Arguments are always
6
+ * parsed, even for functions we cannot evaluate — the tokens have to be
7
+ * consumed either way or the rest of the expression parses against the wrong
8
+ * position.
9
+ */
10
+ export function parseCall(cur, scope, name) {
11
+ const args = [];
12
+ if (!cur.eatOp(")")) {
13
+ for (;;) {
14
+ args.push(parseOr(cur, scope));
15
+ if (cur.eatOp(",")) {
16
+ continue;
17
+ }
18
+ if (cur.eatOp(")")) {
19
+ break;
20
+ }
21
+ return UNKNOWN; // malformed argument list
22
+ }
23
+ }
24
+ return applyFunction(name.toLowerCase(), args);
25
+ }
@@ -0,0 +1,3 @@
1
+ import type { Cursor } from "./cursor.js";
2
+ import type { Scope, Val } from "./val.js";
3
+ export declare function parseCmp(cur: Cursor, scope: Scope): Val;