pi-gsd 2.0.1 → 2.0.2

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 (65) hide show
  1. package/dist/pi-gsd-hooks.js +1532 -0
  2. package/package.json +3 -5
  3. package/.gsd/extensions/pi-gsd-hooks.ts +0 -973
  4. package/src/cli.ts +0 -644
  5. package/src/commands/base.ts +0 -67
  6. package/src/commands/commit.ts +0 -22
  7. package/src/commands/config.ts +0 -71
  8. package/src/commands/frontmatter.ts +0 -51
  9. package/src/commands/index.ts +0 -76
  10. package/src/commands/init.ts +0 -43
  11. package/src/commands/milestone.ts +0 -37
  12. package/src/commands/phase.ts +0 -92
  13. package/src/commands/progress.ts +0 -71
  14. package/src/commands/roadmap.ts +0 -40
  15. package/src/commands/scaffold.ts +0 -19
  16. package/src/commands/state.ts +0 -102
  17. package/src/commands/template.ts +0 -52
  18. package/src/commands/verify.ts +0 -70
  19. package/src/commands/workstream.ts +0 -98
  20. package/src/commands/wxp.ts +0 -65
  21. package/src/lib/commands.ts +0 -1040
  22. package/src/lib/config.ts +0 -385
  23. package/src/lib/core.ts +0 -1167
  24. package/src/lib/frontmatter.ts +0 -462
  25. package/src/lib/init.ts +0 -517
  26. package/src/lib/milestone.ts +0 -290
  27. package/src/lib/model-profiles.ts +0 -272
  28. package/src/lib/phase.ts +0 -1012
  29. package/src/lib/profile-output.ts +0 -237
  30. package/src/lib/profile-pipeline.ts +0 -556
  31. package/src/lib/roadmap.ts +0 -378
  32. package/src/lib/schemas.ts +0 -290
  33. package/src/lib/security.ts +0 -176
  34. package/src/lib/state.ts +0 -1175
  35. package/src/lib/template.ts +0 -246
  36. package/src/lib/uat.ts +0 -289
  37. package/src/lib/verify.ts +0 -879
  38. package/src/lib/workstream.ts +0 -524
  39. package/src/output.ts +0 -45
  40. package/src/schemas/pi-gsd-settings.schema.json +0 -80
  41. package/src/schemas/wxp.xsd +0 -619
  42. package/src/schemas/wxp.zod.ts +0 -318
  43. package/src/wxp/__tests__/arguments.test.ts +0 -86
  44. package/src/wxp/__tests__/conditions.test.ts +0 -106
  45. package/src/wxp/__tests__/executor.test.ts +0 -95
  46. package/src/wxp/__tests__/helpers.ts +0 -26
  47. package/src/wxp/__tests__/integration.test.ts +0 -166
  48. package/src/wxp/__tests__/new-features.test.ts +0 -222
  49. package/src/wxp/__tests__/parser.test.ts +0 -159
  50. package/src/wxp/__tests__/paste.test.ts +0 -66
  51. package/src/wxp/__tests__/schema.test.ts +0 -120
  52. package/src/wxp/__tests__/security.test.ts +0 -87
  53. package/src/wxp/__tests__/shell.test.ts +0 -85
  54. package/src/wxp/__tests__/string-ops.test.ts +0 -25
  55. package/src/wxp/__tests__/variables.test.ts +0 -65
  56. package/src/wxp/arguments.ts +0 -89
  57. package/src/wxp/conditions.ts +0 -78
  58. package/src/wxp/executor.ts +0 -191
  59. package/src/wxp/index.ts +0 -191
  60. package/src/wxp/parser.ts +0 -198
  61. package/src/wxp/paste.ts +0 -51
  62. package/src/wxp/security.ts +0 -102
  63. package/src/wxp/shell.ts +0 -81
  64. package/src/wxp/string-ops.ts +0 -44
  65. package/src/wxp/variables.ts +0 -109
@@ -1,120 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import {
3
- ArgSchema,
4
- OutSchema,
5
- ShellNodeSchema,
6
- StringOpNodeSchema,
7
- ArgumentsNodeSchema,
8
- PasteNodeSchema,
9
- VersionTagSchema,
10
- IncludeNodeSchema,
11
- WxpVariableSchema,
12
- WxpSecurityConfigSchema,
13
- } from "../../schemas/wxp.zod.js";
14
-
15
- describe("ArgSchema", () => {
16
- it("parses a literal arg", () => {
17
- const arg = ArgSchema.parse({ string: "execute-phase" });
18
- expect(arg.string).toBe("execute-phase");
19
- });
20
-
21
- it("parses a variable-ref arg", () => {
22
- const arg = ArgSchema.parse({ name: "phase", wrap: '"' });
23
- expect(arg.name).toBe("phase");
24
- expect(arg.wrap).toBe('"');
25
- });
26
-
27
- it("parses a typed literal arg", () => {
28
- const arg = ArgSchema.parse({ type: "string", value: "@file:" });
29
- expect(arg.type).toBe("string");
30
- expect(arg.value).toBe("@file:");
31
- });
32
-
33
- it("parses a gsd-arguments flag arg", () => {
34
- const arg = ArgSchema.parse({ name: "auto", type: "flag", flag: "--auto", optional: true });
35
- expect(arg.type).toBe("flag");
36
- expect(arg.flag).toBe("--auto");
37
- expect(arg.optional).toBe(true);
38
- });
39
-
40
- it("rejects invalid type value", () => {
41
- expect(() => ArgSchema.parse({ name: "x", type: "invalid" })).toThrow();
42
- });
43
- });
44
-
45
- describe("OutSchema", () => {
46
- it("parses a valid out element", () => {
47
- const out = OutSchema.parse({ type: "string", name: "init" });
48
- expect(out.type).toBe("string");
49
- expect(out.name).toBe("init");
50
- });
51
-
52
- it("requires type and name", () => {
53
- expect(() => OutSchema.parse({ name: "x" })).toThrow();
54
- expect(() => OutSchema.parse({ type: "string" })).toThrow();
55
- });
56
- });
57
-
58
- describe("ShellNodeSchema", () => {
59
- it("parses a shell node with args and outs", () => {
60
- const node = ShellNodeSchema.parse({
61
- type: "shell",
62
- command: "pi-gsd-tools",
63
- args: [{ string: "init" }, { string: "execute-phase" }, { name: "phase", wrap: '"' }],
64
- outs: [{ type: "string", name: "init" }],
65
- suppressErrors: false,
66
- });
67
- expect(node.command).toBe("pi-gsd-tools");
68
- expect(node.args).toHaveLength(3);
69
- expect(node.outs).toHaveLength(1);
70
- });
71
-
72
- it("defaults args and outs to empty arrays", () => {
73
- const node = ShellNodeSchema.parse({ type: "shell", command: "git" });
74
- expect(node.args).toEqual([]);
75
- expect(node.outs).toEqual([]);
76
- expect(node.suppressErrors).toBe(false);
77
- });
78
- });
79
-
80
- describe("StringOpNodeSchema", () => {
81
- it("parses a split string-op", () => {
82
- const node = StringOpNodeSchema.parse({
83
- type: "string-op",
84
- op: "split",
85
- args: [{ name: "init" }, { type: "string", value: "@file:" }],
86
- outs: [{ type: "string", name: "init-file" }],
87
- });
88
- expect(node.op).toBe("split");
89
- });
90
- });
91
-
92
- describe("ArgumentsNodeSchema", () => {
93
- it("parses a full gsd-arguments block", () => {
94
- const node = ArgumentsNodeSchema.parse({
95
- type: "arguments",
96
- settings: { keepExtraArgs: false, strictArgs: false, delimiters: [] },
97
- args: [
98
- { name: "phase", type: "number" },
99
- { name: "auto", type: "flag", flag: "--auto", optional: true },
100
- { name: "user-text", type: "string", optional: true },
101
- ],
102
- });
103
- expect(node.args).toHaveLength(3);
104
- expect(node.args[1].type).toBe("flag");
105
- });
106
- });
107
-
108
- describe("WxpSecurityConfigSchema", () => {
109
- it("parses with structured trustedPaths", () => {
110
- const cfg = WxpSecurityConfigSchema.parse({
111
- trustedPaths: [
112
- { position: "pkg", path: ".gsd/harnesses/pi/get-shit-done" },
113
- { position: "project", path: ".pi/gsd" },
114
- ],
115
- shellAllowlist: ["pi-gsd-tools", "git"],
116
- });
117
- expect(cfg.trustedPaths).toHaveLength(2);
118
- expect(cfg.shellTimeoutMs).toBe(30_000);
119
- });
120
- });
@@ -1,87 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { checkTrustedPath, checkAllowlist, DEFAULT_SHELL_ALLOWLIST } from "../security.js";
3
- import type { WxpSecurityConfig } from "../../schemas/wxp.zod.js";
4
-
5
- const PROJECT_ROOT = "/project";
6
- const PKG_ROOT = "/pkg";
7
-
8
- const cfg: WxpSecurityConfig = {
9
- trustedPaths: [
10
- { position: "pkg", path: ".gsd/harnesses/pi/get-shit-done" },
11
- { position: "project", path: ".pi/gsd" },
12
- ],
13
- untrustedPaths: [],
14
- shellAllowlist: [...DEFAULT_SHELL_ALLOWLIST],
15
- shellBanlist: [],
16
- shellTimeoutMs: 30_000,
17
- };
18
-
19
- describe("checkTrustedPath", () => {
20
- it("returns ok=true for file inside pkg harness", () => {
21
- const result = checkTrustedPath(
22
- "/pkg/.gsd/harnesses/pi/get-shit-done/workflows/execute-phase.md",
23
- cfg, PROJECT_ROOT, PKG_ROOT,
24
- );
25
- expect(result.ok).toBe(true);
26
- });
27
-
28
- it("returns ok=true for file inside project harness", () => {
29
- const result = checkTrustedPath(
30
- "/project/.pi/gsd/workflows/plan-phase.md",
31
- cfg, PROJECT_ROOT, PKG_ROOT,
32
- );
33
- expect(result.ok).toBe(true);
34
- });
35
-
36
- it("returns ok=false for file outside trusted paths", () => {
37
- const result = checkTrustedPath("/untrusted/file.md", cfg, PROJECT_ROOT, PKG_ROOT);
38
- expect(result.ok).toBe(false);
39
- });
40
-
41
- it("hard blocks .planning/ regardless of trusted paths", () => {
42
- const cfgWithPlanning: WxpSecurityConfig = {
43
- ...cfg,
44
- trustedPaths: [{ position: "absolute", path: "/project" }],
45
- };
46
- const result = checkTrustedPath(
47
- "/project/.planning/STATE.md",
48
- cfgWithPlanning, PROJECT_ROOT, PKG_ROOT,
49
- );
50
- expect(result.ok).toBe(false);
51
- if (!result.ok) expect(result.reason).toContain(".planning/");
52
- });
53
-
54
- it("untrustedPaths override trustedPaths", () => {
55
- const cfgWithUntrusted: WxpSecurityConfig = {
56
- ...cfg,
57
- untrustedPaths: [{ position: "project", path: ".pi/gsd/dangerous" }],
58
- };
59
- const result = checkTrustedPath(
60
- "/project/.pi/gsd/dangerous/file.md",
61
- cfgWithUntrusted, PROJECT_ROOT, PKG_ROOT,
62
- );
63
- expect(result.ok).toBe(false);
64
- });
65
- });
66
-
67
- describe("checkAllowlist", () => {
68
- it("returns ok=true for all default allowlist entries", () => {
69
- for (const cmd of DEFAULT_SHELL_ALLOWLIST) {
70
- expect(checkAllowlist(cmd, cfg).ok).toBe(true);
71
- }
72
- });
73
-
74
- it("returns ok=false for bash", () => {
75
- expect(checkAllowlist("bash", cfg).ok).toBe(false);
76
- });
77
-
78
- it("strips path prefix before checking", () => {
79
- expect(checkAllowlist("/usr/bin/git", cfg).ok).toBe(true);
80
- expect(checkAllowlist("/usr/bin/bash", cfg).ok).toBe(false);
81
- });
82
-
83
- it("banlist overrides allowlist", () => {
84
- const cfgWithBan: WxpSecurityConfig = { ...cfg, shellBanlist: ["git"] };
85
- expect(checkAllowlist("git", cfgWithBan).ok).toBe(false);
86
- });
87
- });
@@ -1,85 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import type { Mock } from "vitest";
3
-
4
- vi.mock("node:child_process", () => ({
5
- execFileSync: vi.fn().mockReturnValue("mocked-output\n"),
6
- }));
7
-
8
- import { executeShell, WxpShellError } from "../shell.js";
9
- import { createVariableStore } from "../variables.js";
10
- import type { WxpSecurityConfig, XmlNode } from "../../schemas/wxp.zod.js";
11
- import { execFileSync } from "node:child_process";
12
- import { x } from "./helpers.js";
13
-
14
- const cfg: WxpSecurityConfig = {
15
- trustedPaths: [],
16
- untrustedPaths: [],
17
- shellAllowlist: ["pi-gsd-tools", "git", "node", "cat", "ls", "echo", "find"],
18
- shellBanlist: [],
19
- shellTimeoutMs: 30_000,
20
- };
21
-
22
- function shellNode(command: string, args: XmlNode[], outs: XmlNode[], suppress = false) {
23
- return x("shell", { command }, [
24
- x("args", {}, args),
25
- x("outs", {}, suppress ? [...outs, x("suppress-errors")] : outs),
26
- ]);
27
- }
28
-
29
- describe("executeShell", () => {
30
- beforeEach(() => {
31
- vi.clearAllMocks();
32
- (execFileSync as Mock).mockReturnValue("mocked-output\n");
33
- });
34
-
35
- it("executes allowlisted command and stores stdout in out variable", () => {
36
- const vars = createVariableStore();
37
- executeShell(
38
- shellNode("pi-gsd-tools",
39
- [x("arg", { string: "state" }), x("arg", { string: "json" })],
40
- [x("out", { type: "string", name: "state" })]),
41
- vars, cfg,
42
- );
43
- expect(vars.get("state")).toBe("mocked-output");
44
- });
45
-
46
- it("throws WxpShellError for non-allowlisted command (no process spawned)", () => {
47
- const vars = createVariableStore();
48
- expect(() => executeShell(
49
- shellNode("bash", [], []),
50
- vars, cfg,
51
- )).toThrow(WxpShellError);
52
- expect(execFileSync).not.toHaveBeenCalled();
53
- });
54
-
55
- it("resolves variable args with wrap", () => {
56
- const vars = createVariableStore();
57
- vars.set("phase", "16");
58
- executeShell(
59
- shellNode("pi-gsd-tools",
60
- [x("arg", { string: "init" }), x("arg", { string: "execute-phase" }), x("arg", { name: "phase", wrap: '"' })],
61
- [x("out", { type: "string", name: "init" })]),
62
- vars, cfg,
63
- );
64
- expect(execFileSync).toHaveBeenCalledWith(
65
- "pi-gsd-tools",
66
- ["init", "execute-phase", '"16"'],
67
- expect.objectContaining({ timeout: 30_000 }),
68
- );
69
- });
70
-
71
- it("suppress-errors: stores empty string on failure instead of throwing", () => {
72
- (execFileSync as Mock).mockImplementationOnce(() => {
73
- throw Object.assign(new Error("exit 1"), { stderr: "error" });
74
- });
75
- const vars = createVariableStore();
76
- expect(() => executeShell(
77
- shellNode("pi-gsd-tools",
78
- [x("arg", { string: "agent-skills" })],
79
- [x("out", { type: "string", name: "skills" })],
80
- true),
81
- vars, cfg,
82
- )).not.toThrow();
83
- expect(vars.get("skills")).toBe("");
84
- });
85
- });
@@ -1,25 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { executeStringOp, WxpStringOpError } from "../string-ops.js";
3
- import { createVariableStore } from "../variables.js";
4
- import { x } from "./helpers.js";
5
-
6
- function splitNode(srcName: string, delimiter: string, outName: string) {
7
- return x("string-op", { op: "split" }, [
8
- x("args", {}, [x("arg", { name: srcName }), x("arg", { type: "string", value: delimiter })]),
9
- x("outs", {}, [x("out", { type: "string", name: outName })]),
10
- ]);
11
- }
12
-
13
- describe("executeStringOp split", () => {
14
- it("splits @file: prefix and stores remainder", () => {
15
- const vars = createVariableStore();
16
- vars.set("init", "@file:/tmp/gsd-init.json");
17
- executeStringOp(splitNode("init", "@file:", "init-file"), vars);
18
- expect(vars.get("init-file")).toBe("/tmp/gsd-init.json");
19
- });
20
-
21
- it("throws WxpStringOpError when source variable is undefined", () => {
22
- const vars = createVariableStore();
23
- expect(() => executeStringOp(splitNode("missing", "@file:", "out"), vars)).toThrow(WxpStringOpError);
24
- });
25
- });
@@ -1,65 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { createVariableStore } from "../variables.js";
3
-
4
- describe("VariableStore", () => {
5
- it("sets and gets a variable", () => {
6
- const store = createVariableStore();
7
- store.set("x", "hello");
8
- expect(store.get("x")).toBe("hello");
9
- });
10
-
11
- it("returns undefined for missing variable", () => {
12
- const store = createVariableStore();
13
- expect(store.get("missing")).toBeUndefined();
14
- });
15
-
16
- it("has() returns true for existing variable", () => {
17
- const store = createVariableStore();
18
- store.set("a", "1");
19
- expect(store.has("a")).toBe(true);
20
- expect(store.has("b")).toBe(false);
21
- });
22
-
23
- it("snapshot() returns all key-value pairs", () => {
24
- const store = createVariableStore();
25
- store.set("a", "1", "file1");
26
- store.set("b", "2", "file2");
27
- const snap = store.snapshot();
28
- expect(snap["a"]).toBe("1");
29
- expect(snap["b"]).toBe("2");
30
- });
31
-
32
- it("collision detection: same name from different owners gets prefixed", () => {
33
- const store = createVariableStore();
34
- store.set("result", "first", "execute-phase");
35
- store.set("result", "second", "plan-phase");
36
- // Original "result" key should be gone; prefixed keys should exist
37
- expect(store.get("result")).toBeUndefined();
38
- expect(store.get("execute-phase:result")).toBe("first");
39
- expect(store.get("plan-phase:result")).toBe("second");
40
- });
41
-
42
- it("no collision when owner is the same", () => {
43
- const store = createVariableStore();
44
- store.set("x", "v1", "file");
45
- store.set("x", "v2", "file");
46
- // Same owner: overwrite, no prefix
47
- expect(store.get("x")).toBe("v2");
48
- });
49
-
50
- it("no collision when no owner provided", () => {
51
- const store = createVariableStore();
52
- store.set("x", "v1");
53
- store.set("x", "v2");
54
- expect(store.get("x")).toBe("v2");
55
- });
56
-
57
- it("entries() iterates over all stored variables", () => {
58
- const store = createVariableStore();
59
- store.set("a", "1");
60
- store.set("b", "2");
61
- const keys = [...store.entries()].map(([k]) => k);
62
- expect(keys).toContain("a");
63
- expect(keys).toContain("b");
64
- });
65
- });
@@ -1,89 +0,0 @@
1
- import type { XmlNode } from "../schemas/wxp.zod.js";
2
- import type { VariableStore } from "./variables.js";
3
-
4
- export class WxpArgumentsError extends Error {
5
- constructor(message: string) {
6
- super(message);
7
- this.name = "WxpArgumentsError";
8
- }
9
- }
10
-
11
- export function parseArguments(node: XmlNode, rawArguments: string, vars: VariableStore): void {
12
- const settingsNode = node.children.find((c) => c.tag === "settings");
13
- const keepExtraArgs = settingsNode?.children.some((c) => c.tag === "keep-extra-args") ?? false;
14
- const strictArgs = settingsNode?.children.some((c) => c.tag === "strict-args") ?? false;
15
-
16
- const delimContainer = settingsNode?.children.find((c) => c.tag === "delimiters");
17
- const firstDelim = delimContainer?.children.find((c) => c.tag === "delimiter");
18
-
19
- let tokens: string[];
20
- if (firstDelim) {
21
- const raw = firstDelim.attrs["value"] ?? "";
22
- const sep = raw === "\\n" ? "\n" : raw;
23
- tokens = rawArguments.split(sep).map((t) => t.trim()).filter(Boolean);
24
- } else {
25
- tokens = rawArguments.trim().split(/\s+/).filter(Boolean);
26
- }
27
-
28
- const argDefs = node.children.filter((c) => c.tag === "arg");
29
- const consumed = new Set<number>();
30
-
31
- // ── Pass 1: flags ──────────────────────────────────────────────────────────
32
- for (const def of argDefs.filter((a) => a.attrs["type"] === "flag")) {
33
- const flagToken = def.attrs["flag"] ?? `--${def.attrs["name"]}`;
34
- const idx = tokens.indexOf(flagToken);
35
- const name = def.attrs["name"];
36
- if (!name) continue;
37
- if (idx === -1) {
38
- vars.set(name, "false", undefined);
39
- } else {
40
- vars.set(name, "true", undefined);
41
- consumed.add(idx);
42
- }
43
- }
44
-
45
- // ── Pass 2: positionals ────────────────────────────────────────────────────
46
- const positionals = argDefs.filter((a) => a.attrs["type"] !== "flag");
47
- const remaining = tokens.filter((_, i) => !consumed.has(i));
48
- let tokenIdx = 0;
49
-
50
- for (let i = 0; i < positionals.length; i++) {
51
- const def = positionals[i];
52
- const name = def.attrs["name"];
53
- const type = def.attrs["type"] ?? "string";
54
- const isLast = i === positionals.length - 1;
55
- if (!name) continue;
56
-
57
- if (tokenIdx >= remaining.length) {
58
- if (!("optional" in def.attrs)) {
59
- throw new WxpArgumentsError(`Missing required argument '${name}' (type: ${type})`);
60
- }
61
- vars.set(name, "", undefined);
62
- continue;
63
- }
64
-
65
- if (type === "string" && isLast) {
66
- vars.set(name, remaining.slice(tokenIdx).join(" "), undefined);
67
- tokenIdx = remaining.length;
68
- } else if (type === "number") {
69
- const raw = remaining[tokenIdx++];
70
- const num = Number(raw);
71
- if (isNaN(num)) throw new WxpArgumentsError(`Argument '${name}' expected a number, got '${raw}'`);
72
- vars.set(name, String(num), undefined);
73
- } else if (type === "boolean") {
74
- const raw = remaining[tokenIdx++].toLowerCase();
75
- if (raw !== "true" && raw !== "false") {
76
- throw new WxpArgumentsError(`Argument '${name}' expected true/false, got '${raw}'`);
77
- }
78
- vars.set(name, raw, undefined);
79
- } else {
80
- vars.set(name, remaining[tokenIdx++] ?? "", undefined);
81
- }
82
- }
83
-
84
- const extra = remaining.slice(tokenIdx).join(" ");
85
- if (extra) {
86
- if (strictArgs) throw new WxpArgumentsError(`Unexpected extra arguments: '${extra}'`);
87
- if (keepExtraArgs) vars.set("_extra", extra, undefined);
88
- }
89
- }
@@ -1,78 +0,0 @@
1
- import type { XmlNode } from "../schemas/wxp.zod.js";
2
- import type { VariableStore } from "./variables.js";
3
-
4
- const BINARY_OPS = new Set([
5
- "equals", "not-equals", "starts-with", "contains",
6
- "less-than", "greater-than", "less-than-or-equal", "greater-than-or-equal",
7
- ]);
8
-
9
- export const CONDITION_OPS = new Set([...BINARY_OPS, "and", "or"]);
10
-
11
- function resolveOperand(node: XmlNode, vars: VariableStore): string {
12
- if (node.attrs["name"]) return vars.resolve(node.attrs["name"]) ?? "";
13
- if (node.attrs["value"] !== undefined) return node.attrs["value"];
14
- return "";
15
- }
16
-
17
- function isNumeric(node: XmlNode): boolean {
18
- return node.attrs["type"] === "number";
19
- }
20
-
21
- function evalBinary(node: XmlNode, vars: VariableStore): boolean {
22
- const leftNode = node.children.find((c) => c.tag === "left");
23
- const rightNode = node.children.find((c) => c.tag === "right");
24
- if (!leftNode || !rightNode) return false;
25
-
26
- const numeric = isNumeric(leftNode) || isNumeric(rightNode);
27
- if (numeric) {
28
- const l = Number(resolveOperand(leftNode, vars));
29
- const r = Number(resolveOperand(rightNode, vars));
30
- switch (node.tag) {
31
- case "equals": return l === r;
32
- case "not-equals": return l !== r;
33
- case "less-than": return l < r;
34
- case "greater-than": return l > r;
35
- case "less-than-or-equal": return l <= r;
36
- case "greater-than-or-equal": return l >= r;
37
- default: return false;
38
- }
39
- }
40
-
41
- const l = resolveOperand(leftNode, vars);
42
- const r = resolveOperand(rightNode, vars);
43
- switch (node.tag) {
44
- case "equals": return l === r;
45
- case "not-equals": return l !== r;
46
- case "starts-with": return l.startsWith(r);
47
- case "contains": return l.includes(r);
48
- case "less-than": return Number(l) < Number(r);
49
- case "greater-than": return Number(l) > Number(r);
50
- case "less-than-or-equal": return Number(l) <= Number(r);
51
- case "greater-than-or-equal": return Number(l) >= Number(r);
52
- default: return false;
53
- }
54
- }
55
-
56
- export function evaluateCondExprNode(node: XmlNode, vars: VariableStore): boolean {
57
- if (node.tag === "and") {
58
- return node.children.filter((c) => CONDITION_OPS.has(c.tag)).every((c) => evaluateCondExprNode(c, vars));
59
- }
60
- if (node.tag === "or") {
61
- return node.children.filter((c) => CONDITION_OPS.has(c.tag)).some((c) => evaluateCondExprNode(c, vars));
62
- }
63
- return evalBinary(node, vars);
64
- }
65
-
66
- /** Evaluate the condition of an <if> node. */
67
- export function evaluateCondition(ifNode: XmlNode, vars: VariableStore): boolean {
68
- const condContainer = ifNode.children.find((c) => c.tag === "condition");
69
- if (!condContainer) return false;
70
- const exprNode = condContainer.children.find((c) => CONDITION_OPS.has(c.tag));
71
- return exprNode ? evaluateCondExprNode(exprNode, vars) : false;
72
- }
73
-
74
- /** Evaluate a <where> node's condition. */
75
- export function evaluateWhere(whereNode: XmlNode, vars: VariableStore): boolean {
76
- const exprNode = whereNode.children.find((c) => CONDITION_OPS.has(c.tag));
77
- return exprNode ? evaluateCondExprNode(exprNode, vars) : true;
78
- }