@magland/mochi 0.3.1

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 (116) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +108 -0
  3. package/dist/ansi.js +174 -0
  4. package/dist/api/admin.js +416 -0
  5. package/dist/api/auth.js +166 -0
  6. package/dist/api/backup.js +598 -0
  7. package/dist/api/ci.js +336 -0
  8. package/dist/api/contents.js +339 -0
  9. package/dist/api/issues.js +165 -0
  10. package/dist/api/pulls.js +244 -0
  11. package/dist/api/releases.js +83 -0
  12. package/dist/api/repos.js +156 -0
  13. package/dist/api/write.js +518 -0
  14. package/dist/api.js +326 -0
  15. package/dist/assets.js +29 -0
  16. package/dist/atom.js +32 -0
  17. package/dist/atomic.js +171 -0
  18. package/dist/avatar.js +81 -0
  19. package/dist/browse.js +630 -0
  20. package/dist/build-info.json +4 -0
  21. package/dist/ci/actionref.js +86 -0
  22. package/dist/ci/api.js +829 -0
  23. package/dist/ci/artifacts.js +201 -0
  24. package/dist/ci/dispatch.js +30 -0
  25. package/dist/ci/engine.js +1321 -0
  26. package/dist/ci/expr.js +526 -0
  27. package/dist/ci/manual.js +199 -0
  28. package/dist/ci/present.js +82 -0
  29. package/dist/ci/protocol.js +6 -0
  30. package/dist/ci/runners.js +256 -0
  31. package/dist/ci/runs.js +208 -0
  32. package/dist/ci/trigger.js +28 -0
  33. package/dist/ci/views.js +441 -0
  34. package/dist/ci/wake.js +194 -0
  35. package/dist/ci/web.js +617 -0
  36. package/dist/ci/workflow.js +436 -0
  37. package/dist/cli/admin-cmd.js +324 -0
  38. package/dist/cli/api-cmd.js +128 -0
  39. package/dist/cli/backup-cmd.js +1500 -0
  40. package/dist/cli/exit.js +69 -0
  41. package/dist/cli/input.js +64 -0
  42. package/dist/cli/issue-cmd.js +243 -0
  43. package/dist/cli/output.js +93 -0
  44. package/dist/cli/parse.js +317 -0
  45. package/dist/cli/pr-cmd.js +289 -0
  46. package/dist/cli/release-cmd.js +171 -0
  47. package/dist/cli/repo-cmd.js +763 -0
  48. package/dist/cli/repo.js +101 -0
  49. package/dist/cli/run-cmd.js +438 -0
  50. package/dist/cli/target.js +54 -0
  51. package/dist/cli-api.js +84 -0
  52. package/dist/compare.js +111 -0
  53. package/dist/config.js +212 -0
  54. package/dist/credentials.js +235 -0
  55. package/dist/deploy-cli.js +859 -0
  56. package/dist/deploy-runner-cli.js +592 -0
  57. package/dist/diff.js +171 -0
  58. package/dist/discussion.js +253 -0
  59. package/dist/egress.js +559 -0
  60. package/dist/filecache.js +68 -0
  61. package/dist/find.js +162 -0
  62. package/dist/forms.js +737 -0
  63. package/dist/git.js +547 -0
  64. package/dist/githttp.js +428 -0
  65. package/dist/html.js +87 -0
  66. package/dist/icons.js +101 -0
  67. package/dist/import-cli.js +316 -0
  68. package/dist/index.js +752 -0
  69. package/dist/issues.js +308 -0
  70. package/dist/issueweb.js +447 -0
  71. package/dist/job-cli.js +197 -0
  72. package/dist/jobtoken.js +96 -0
  73. package/dist/languages.js +383 -0
  74. package/dist/layout.js +100 -0
  75. package/dist/lfs.js +438 -0
  76. package/dist/lfsstore.js +425 -0
  77. package/dist/limit.js +259 -0
  78. package/dist/logo.js +61 -0
  79. package/dist/markdown.js +382 -0
  80. package/dist/migrate.js +334 -0
  81. package/dist/multipart.js +90 -0
  82. package/dist/ops.js +869 -0
  83. package/dist/pagescript.js +465 -0
  84. package/dist/perms.js +370 -0
  85. package/dist/pointer.js +55 -0
  86. package/dist/profile.js +106 -0
  87. package/dist/pulls.js +320 -0
  88. package/dist/pullweb.js +461 -0
  89. package/dist/redirects.js +455 -0
  90. package/dist/releases.js +435 -0
  91. package/dist/render.js +233 -0
  92. package/dist/runner/actions.js +448 -0
  93. package/dist/runner/client.js +428 -0
  94. package/dist/runner/context.js +247 -0
  95. package/dist/runner/docker.js +197 -0
  96. package/dist/runner/externals.js +175 -0
  97. package/dist/runner/job.js +290 -0
  98. package/dist/runner/manual-run.js +272 -0
  99. package/dist/runner/overrides.js +554 -0
  100. package/dist/runner/steps.js +571 -0
  101. package/dist/runner/wake.js +84 -0
  102. package/dist/runner-cli.js +405 -0
  103. package/dist/scan.js +231 -0
  104. package/dist/server.js +424 -0
  105. package/dist/session.js +267 -0
  106. package/dist/site.js +259 -0
  107. package/dist/siteshost.js +94 -0
  108. package/dist/source.js +90 -0
  109. package/dist/style.js +1295 -0
  110. package/dist/themes.js +369 -0
  111. package/dist/vault.js +442 -0
  112. package/dist/version.js +88 -0
  113. package/dist/views.js +1007 -0
  114. package/dist/web.js +182 -0
  115. package/dist/webops.js +1402 -0
  116. package/package.json +71 -0
@@ -0,0 +1,526 @@
1
+ "use strict";
2
+ // The GitHub Actions expression language: the `${{ ... }}` fragments in
3
+ // workflow files. One evaluator serves both sides of the CI system: the
4
+ // server evaluates job-level conditions, concurrency groups, and matrix
5
+ // substitutions, and the runner evaluates step-level conditions, env maps,
6
+ // and run bodies. Both are this module, so behavior cannot drift.
7
+ //
8
+ // The language is small: literals, `!`, comparisons, `&&`/`||`, property and
9
+ // index dereference on contexts, and a fixed set of functions. `&&` and `||`
10
+ // return operand values as in JavaScript. Comparisons follow GitHub's rules:
11
+ // equal-typed strings compare case-insensitively, mixed types are cast to
12
+ // numbers. The object-filter syntax (`*` in a dereference chain) is not
13
+ // implemented; nothing in scope uses it and it fails with a clear message.
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ExprError = void 0;
16
+ exports.truthy = truthy;
17
+ exports.stringify = stringify;
18
+ exports.evaluate = evaluate;
19
+ exports.render = render;
20
+ exports.renderDeep = renderDeep;
21
+ exports.evalCondition = evalCondition;
22
+ exports.mentionsStatusOverride = mentionsStatusOverride;
23
+ class ExprError extends Error {
24
+ }
25
+ exports.ExprError = ExprError;
26
+ function tokenize(src) {
27
+ const tokens = [];
28
+ let i = 0;
29
+ while (i < src.length) {
30
+ const c = src[i];
31
+ if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
32
+ i++;
33
+ continue;
34
+ }
35
+ if (c === "'") {
36
+ // Single-quoted string; '' is an escaped quote.
37
+ let s = '';
38
+ i++;
39
+ for (;;) {
40
+ if (i >= src.length)
41
+ throw new ExprError('unterminated string literal');
42
+ if (src[i] === "'") {
43
+ if (src[i + 1] === "'") {
44
+ s += "'";
45
+ i += 2;
46
+ }
47
+ else {
48
+ i++;
49
+ break;
50
+ }
51
+ }
52
+ else {
53
+ s += src[i++];
54
+ }
55
+ }
56
+ tokens.push({ kind: 'str', value: s });
57
+ continue;
58
+ }
59
+ const two = src.slice(i, i + 2);
60
+ if (two === '==' || two === '!=' || two === '<=' || two === '>=' || two === '&&' || two === '||') {
61
+ tokens.push({ kind: 'op', value: two });
62
+ i += 2;
63
+ continue;
64
+ }
65
+ if ('!<>()[].,'.includes(c)) {
66
+ tokens.push({ kind: 'op', value: c });
67
+ i++;
68
+ continue;
69
+ }
70
+ if (/[0-9]/.test(c) || (c === '-' && /[0-9.]/.test(src[i + 1] ?? ''))) {
71
+ const m = src.slice(i).match(/^-?(0x[0-9a-fA-F]+|\d+(\.\d+)?([eE][+-]?\d+)?|\.\d+)/);
72
+ if (!m)
73
+ throw new ExprError(`invalid number at: ${src.slice(i, i + 12)}`);
74
+ tokens.push({ kind: 'num', value: Number(m[0]) });
75
+ i += m[0].length;
76
+ continue;
77
+ }
78
+ if (/[A-Za-z_]/.test(c)) {
79
+ const m = src.slice(i).match(/^[A-Za-z_][A-Za-z0-9_-]*/);
80
+ tokens.push({ kind: 'ident', value: m[0] });
81
+ i += m[0].length;
82
+ continue;
83
+ }
84
+ if (c === '*') {
85
+ throw new ExprError('the object-filter syntax (*) is not supported');
86
+ }
87
+ throw new ExprError(`unexpected character '${c}' in expression`);
88
+ }
89
+ return tokens;
90
+ }
91
+ class Parser {
92
+ tokens;
93
+ pos = 0;
94
+ constructor(tokens) {
95
+ this.tokens = tokens;
96
+ }
97
+ parse() {
98
+ const n = this.or();
99
+ if (this.pos < this.tokens.length) {
100
+ throw new ExprError(`unexpected token after expression: ${JSON.stringify(this.tokens[this.pos])}`);
101
+ }
102
+ return n;
103
+ }
104
+ peekOp(v) {
105
+ const t = this.tokens[this.pos];
106
+ return t !== undefined && t.kind === 'op' && t.value === v;
107
+ }
108
+ eatOp(v) {
109
+ if (this.peekOp(v)) {
110
+ this.pos++;
111
+ return true;
112
+ }
113
+ return false;
114
+ }
115
+ expectOp(v) {
116
+ if (!this.eatOp(v))
117
+ throw new ExprError(`expected '${v}' in expression`);
118
+ }
119
+ or() {
120
+ let l = this.and();
121
+ while (this.eatOp('||'))
122
+ l = { t: 'bin', op: '||', l, r: this.and() };
123
+ return l;
124
+ }
125
+ and() {
126
+ let l = this.equality();
127
+ while (this.eatOp('&&'))
128
+ l = { t: 'bin', op: '&&', l, r: this.equality() };
129
+ return l;
130
+ }
131
+ equality() {
132
+ let l = this.comparison();
133
+ for (;;) {
134
+ if (this.eatOp('=='))
135
+ l = { t: 'bin', op: '==', l, r: this.comparison() };
136
+ else if (this.eatOp('!='))
137
+ l = { t: 'bin', op: '!=', l, r: this.comparison() };
138
+ else
139
+ return l;
140
+ }
141
+ }
142
+ comparison() {
143
+ let l = this.unary();
144
+ for (;;) {
145
+ if (this.eatOp('<='))
146
+ l = { t: 'bin', op: '<=', l, r: this.unary() };
147
+ else if (this.eatOp('>='))
148
+ l = { t: 'bin', op: '>=', l, r: this.unary() };
149
+ else if (this.eatOp('<'))
150
+ l = { t: 'bin', op: '<', l, r: this.unary() };
151
+ else if (this.eatOp('>'))
152
+ l = { t: 'bin', op: '>', l, r: this.unary() };
153
+ else
154
+ return l;
155
+ }
156
+ }
157
+ unary() {
158
+ if (this.eatOp('!'))
159
+ return { t: 'not', e: this.unary() };
160
+ return this.postfix();
161
+ }
162
+ postfix() {
163
+ let n = this.primary();
164
+ for (;;) {
165
+ if (this.eatOp('.')) {
166
+ const t = this.tokens[this.pos];
167
+ if (!t || t.kind !== 'ident')
168
+ throw new ExprError("expected a property name after '.'");
169
+ this.pos++;
170
+ n = { t: 'deref', e: n, prop: t.value };
171
+ }
172
+ else if (this.eatOp('[')) {
173
+ const i = this.or();
174
+ this.expectOp(']');
175
+ n = { t: 'index', e: n, i };
176
+ }
177
+ else {
178
+ return n;
179
+ }
180
+ }
181
+ }
182
+ primary() {
183
+ const t = this.tokens[this.pos];
184
+ if (!t)
185
+ throw new ExprError('unexpected end of expression');
186
+ if (t.kind === 'num') {
187
+ this.pos++;
188
+ return { t: 'lit', v: t.value };
189
+ }
190
+ if (t.kind === 'str') {
191
+ this.pos++;
192
+ return { t: 'lit', v: t.value };
193
+ }
194
+ if (t.kind === 'op' && t.value === '(') {
195
+ this.pos++;
196
+ const n = this.or();
197
+ this.expectOp(')');
198
+ return n;
199
+ }
200
+ if (t.kind === 'ident') {
201
+ this.pos++;
202
+ const lower = t.value.toLowerCase();
203
+ if (lower === 'null')
204
+ return { t: 'lit', v: null };
205
+ if (lower === 'true')
206
+ return { t: 'lit', v: true };
207
+ if (lower === 'false')
208
+ return { t: 'lit', v: false };
209
+ if (this.eatOp('(')) {
210
+ const args = [];
211
+ if (!this.peekOp(')')) {
212
+ do {
213
+ args.push(this.or());
214
+ } while (this.eatOp(','));
215
+ }
216
+ this.expectOp(')');
217
+ return { t: 'call', name: lower, args };
218
+ }
219
+ return { t: 'ctx', name: t.value };
220
+ }
221
+ throw new ExprError(`unexpected token in expression: ${JSON.stringify(t)}`);
222
+ }
223
+ }
224
+ // ---- evaluation ----
225
+ function truthy(v) {
226
+ if (v === null || v === false)
227
+ return false;
228
+ if (typeof v === 'number')
229
+ return v !== 0 && !Number.isNaN(v);
230
+ if (typeof v === 'string')
231
+ return v !== '';
232
+ return true; // arrays and objects
233
+ }
234
+ function toNumber(v) {
235
+ if (v === null)
236
+ return 0;
237
+ if (typeof v === 'boolean')
238
+ return v ? 1 : 0;
239
+ if (typeof v === 'number')
240
+ return v;
241
+ if (typeof v === 'string') {
242
+ const s = v.trim();
243
+ if (s === '')
244
+ return 0;
245
+ return Number(s);
246
+ }
247
+ return NaN;
248
+ }
249
+ function compareEqual(l, r) {
250
+ if (typeof l === 'string' && typeof r === 'string')
251
+ return l.toLowerCase() === r.toLowerCase();
252
+ if (typeof l === typeof r && (l === null) === (r === null)) {
253
+ if (typeof l === 'number') {
254
+ return !Number.isNaN(l) && !Number.isNaN(r) && l === r;
255
+ }
256
+ if (typeof l === 'boolean' || l === null)
257
+ return l === r;
258
+ // Arrays and objects: equal only when the same instance, per GitHub.
259
+ if (typeof l === 'object')
260
+ return l === r;
261
+ }
262
+ const ln = toNumber(l);
263
+ const rn = toNumber(r);
264
+ return !Number.isNaN(ln) && !Number.isNaN(rn) && ln === rn;
265
+ }
266
+ function compareOrder(op, l, r) {
267
+ if (typeof l === 'string' && typeof r === 'string') {
268
+ const a = l.toLowerCase();
269
+ const b = r.toLowerCase();
270
+ if (op === '<')
271
+ return a < b;
272
+ if (op === '<=')
273
+ return a <= b;
274
+ if (op === '>')
275
+ return a > b;
276
+ return a >= b;
277
+ }
278
+ const a = toNumber(l);
279
+ const b = toNumber(r);
280
+ if (Number.isNaN(a) || Number.isNaN(b))
281
+ return false;
282
+ if (op === '<')
283
+ return a < b;
284
+ if (op === '<=')
285
+ return a <= b;
286
+ if (op === '>')
287
+ return a > b;
288
+ return a >= b;
289
+ }
290
+ // Property access is case-insensitive, preferring an exact match.
291
+ function getProp(obj, prop) {
292
+ if (obj === null || typeof obj !== 'object')
293
+ return null;
294
+ if (Array.isArray(obj))
295
+ return null;
296
+ if (Object.prototype.hasOwnProperty.call(obj, prop))
297
+ return obj[prop];
298
+ const lower = prop.toLowerCase();
299
+ for (const k of Object.keys(obj)) {
300
+ if (k.toLowerCase() === lower)
301
+ return obj[k];
302
+ }
303
+ return null;
304
+ }
305
+ function stringify(v) {
306
+ if (v === null)
307
+ return '';
308
+ if (typeof v === 'boolean')
309
+ return v ? 'true' : 'false';
310
+ if (typeof v === 'number')
311
+ return String(v);
312
+ if (typeof v === 'string')
313
+ return v;
314
+ return JSON.stringify(v);
315
+ }
316
+ function formatFn(args) {
317
+ if (args.length === 0 || typeof args[0] !== 'string')
318
+ throw new ExprError('format() needs a format string');
319
+ const fmt = args[0];
320
+ let out = '';
321
+ let i = 0;
322
+ while (i < fmt.length) {
323
+ const c = fmt[i];
324
+ if (c === '{') {
325
+ if (fmt[i + 1] === '{') {
326
+ out += '{';
327
+ i += 2;
328
+ continue;
329
+ }
330
+ const m = fmt.slice(i).match(/^\{(\d+)\}/);
331
+ if (!m)
332
+ throw new ExprError('malformed placeholder in format()');
333
+ const idx = parseInt(m[1], 10) + 1;
334
+ if (idx >= args.length)
335
+ throw new ExprError(`format() placeholder {${m[1]}} has no argument`);
336
+ out += stringify(args[idx]);
337
+ i += m[0].length;
338
+ }
339
+ else if (c === '}') {
340
+ if (fmt[i + 1] === '}') {
341
+ out += '}';
342
+ i += 2;
343
+ }
344
+ else {
345
+ throw new ExprError("unescaped '}' in format()");
346
+ }
347
+ }
348
+ else {
349
+ out += c;
350
+ i++;
351
+ }
352
+ }
353
+ return out;
354
+ }
355
+ function containsFn(hay, needle) {
356
+ if (Array.isArray(hay))
357
+ return hay.some((x) => compareEqual(x, needle));
358
+ return stringify(hay).toLowerCase().includes(stringify(needle).toLowerCase());
359
+ }
360
+ const BUILTINS = {
361
+ contains: (a, b) => containsFn(a, b),
362
+ startswith: (a, b) => stringify(a).toLowerCase().startsWith(stringify(b).toLowerCase()),
363
+ endswith: (a, b) => stringify(a).toLowerCase().endsWith(stringify(b).toLowerCase()),
364
+ format: (...args) => formatFn(args),
365
+ join: (a, sep) => {
366
+ const s = sep === undefined ? ',' : stringify(sep);
367
+ if (Array.isArray(a))
368
+ return a.map(stringify).join(s);
369
+ return stringify(a);
370
+ },
371
+ tojson: (a) => JSON.stringify(a === undefined ? null : a, null, 2),
372
+ fromjson: (a) => {
373
+ try {
374
+ return JSON.parse(stringify(a));
375
+ }
376
+ catch {
377
+ throw new ExprError(`fromJSON(): invalid JSON: ${stringify(a).slice(0, 80)}`);
378
+ }
379
+ },
380
+ hashfiles: () => {
381
+ throw new ExprError('hashFiles() is not supported yet');
382
+ },
383
+ };
384
+ function evalNode(n, env) {
385
+ switch (n.t) {
386
+ case 'lit':
387
+ return n.v;
388
+ case 'ctx': {
389
+ const v = getProp(env.contexts, n.name);
390
+ if (v === null && !Object.keys(env.contexts).some((k) => k.toLowerCase() === n.name.toLowerCase())) {
391
+ throw new ExprError(`unknown context or value: ${n.name}`);
392
+ }
393
+ return v;
394
+ }
395
+ case 'not':
396
+ return !truthy(evalNode(n.e, env));
397
+ case 'bin': {
398
+ if (n.op === '&&') {
399
+ const l = evalNode(n.l, env);
400
+ return truthy(l) ? evalNode(n.r, env) : l;
401
+ }
402
+ if (n.op === '||') {
403
+ const l = evalNode(n.l, env);
404
+ return truthy(l) ? l : evalNode(n.r, env);
405
+ }
406
+ const l = evalNode(n.l, env);
407
+ const r = evalNode(n.r, env);
408
+ if (n.op === '==')
409
+ return compareEqual(l, r);
410
+ if (n.op === '!=')
411
+ return !compareEqual(l, r);
412
+ return compareOrder(n.op, l, r);
413
+ }
414
+ case 'deref':
415
+ return getProp(evalNode(n.e, env), n.prop);
416
+ case 'index': {
417
+ const obj = evalNode(n.e, env);
418
+ const idx = evalNode(n.i, env);
419
+ if (Array.isArray(obj)) {
420
+ const i = toNumber(idx);
421
+ if (!Number.isInteger(i) || i < 0 || i >= obj.length)
422
+ return null;
423
+ return obj[i];
424
+ }
425
+ return getProp(obj, stringify(idx));
426
+ }
427
+ case 'call': {
428
+ const fn = env.functions?.[n.name] ?? BUILTINS[n.name];
429
+ if (!fn)
430
+ throw new ExprError(`unknown function: ${n.name}`);
431
+ return fn(...n.args.map((a) => evalNode(a, env)));
432
+ }
433
+ }
434
+ }
435
+ function evaluate(expression, env) {
436
+ return evalNode(new Parser(tokenize(expression)).parse(), env);
437
+ }
438
+ // ---- ${{ }} interpolation over strings ----
439
+ // Find the ${{ ... }} spans in a string, respecting single-quoted string
440
+ // literals inside the expression (which may contain '}}').
441
+ function findSpans(s) {
442
+ const spans = [];
443
+ let i = 0;
444
+ while (i < s.length) {
445
+ const start = s.indexOf('${{', i);
446
+ if (start === -1)
447
+ break;
448
+ let j = start + 3;
449
+ let inStr = false;
450
+ let end = -1;
451
+ while (j < s.length) {
452
+ const c = s[j];
453
+ if (inStr) {
454
+ if (c === "'") {
455
+ if (s[j + 1] === "'")
456
+ j++;
457
+ else
458
+ inStr = false;
459
+ }
460
+ }
461
+ else if (c === "'") {
462
+ inStr = true;
463
+ }
464
+ else if (c === '}' && s[j + 1] === '}') {
465
+ end = j;
466
+ break;
467
+ }
468
+ j++;
469
+ }
470
+ if (end === -1)
471
+ throw new ExprError('unterminated ${{ in string');
472
+ spans.push({ start, end: end + 2, expr: s.slice(start + 3, end).trim() });
473
+ i = end + 2;
474
+ }
475
+ return spans;
476
+ }
477
+ // Replace every ${{ expr }} in a string with the stringified result.
478
+ function render(s, env) {
479
+ const spans = findSpans(s);
480
+ if (spans.length === 0)
481
+ return s;
482
+ let out = '';
483
+ let pos = 0;
484
+ for (const sp of spans) {
485
+ out += s.slice(pos, sp.start);
486
+ out += stringify(evaluate(sp.expr, env));
487
+ pos = sp.end;
488
+ }
489
+ return out + s.slice(pos);
490
+ }
491
+ // Render every string leaf of a template value (e.g. an env map).
492
+ function renderDeep(v, env) {
493
+ if (typeof v === 'string')
494
+ return render(v, env);
495
+ if (Array.isArray(v))
496
+ return v.map((x) => renderDeep(x, env));
497
+ if (typeof v === 'object' && v !== null) {
498
+ const out = {};
499
+ for (const [k, val] of Object.entries(v))
500
+ out[k] = renderDeep(val, env);
501
+ return out;
502
+ }
503
+ return v === undefined ? null : v;
504
+ }
505
+ // An `if:` condition: bare expressions are wrapped implicitly; a condition
506
+ // that is exactly one ${{ }} evaluates to its value; a mixed string renders
507
+ // first and the resulting text is judged (GitHub's much-lamented behavior,
508
+ // reproduced here so workflows behave identically).
509
+ function evalCondition(cond, env) {
510
+ const trimmed = cond.trim();
511
+ if (trimmed === '')
512
+ return true;
513
+ if (!trimmed.includes('${{'))
514
+ return truthy(evaluate(trimmed, env));
515
+ const spans = findSpans(trimmed);
516
+ if (spans.length === 1 && spans[0].start === 0 && spans[0].end === trimmed.length) {
517
+ return truthy(evaluate(spans[0].expr, env));
518
+ }
519
+ const rendered = render(trimmed, env).trim().toLowerCase();
520
+ return rendered !== '' && rendered !== 'false' && rendered !== '0' && rendered !== 'null';
521
+ }
522
+ // Whether a condition references a status function that overrides the
523
+ // default success() gate (always(), failure(), cancelled()).
524
+ function mentionsStatusOverride(cond) {
525
+ return /\b(always|failure|cancelled)\s*\(/.test(cond);
526
+ }