@platforma-sdk/block-tools 2.10.13 → 2.10.15

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 (169) hide show
  1. package/dist/cli.js +23 -2
  2. package/dist/cli.js.map +1 -1
  3. package/dist/cli.mjs +1944 -100
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/cmd/index.d.ts +10 -4
  6. package/dist/cmd/structure/check.d.ts +10 -0
  7. package/dist/cmd/structure/init.d.ts +18 -0
  8. package/dist/cmd/structure/refresh.d.ts +11 -0
  9. package/dist/cmd/update-deps.d.ts +7 -0
  10. package/dist/structure/cli/run-structure.d.ts +19 -0
  11. package/dist/structure/engine/api.d.ts +55 -0
  12. package/dist/structure/engine/builders.d.ts +95 -0
  13. package/dist/structure/engine/content-rules.d.ts +148 -0
  14. package/dist/structure/engine/ctx.d.ts +34 -0
  15. package/dist/structure/engine/discovery-fs.d.ts +22 -0
  16. package/dist/structure/engine/discovery.d.ts +39 -0
  17. package/dist/structure/engine/flatten.d.ts +3 -0
  18. package/dist/structure/engine/fs/api.d.ts +27 -0
  19. package/dist/structure/engine/fs/memory.d.ts +16 -0
  20. package/dist/structure/engine/fs/node.d.ts +14 -0
  21. package/dist/structure/engine/glob.d.ts +4 -0
  22. package/dist/structure/engine/ir.d.ts +124 -0
  23. package/dist/structure/engine/parsers/json.d.ts +37 -0
  24. package/dist/structure/engine/parsers/lines.d.ts +9 -0
  25. package/dist/structure/engine/parsers/yaml.d.ts +18 -0
  26. package/dist/structure/engine/registry-client.d.ts +34 -0
  27. package/dist/structure/engine/runner.d.ts +45 -0
  28. package/dist/structure/engine/templates.d.ts +30 -0
  29. package/dist/structure/engine/testing.d.ts +34 -0
  30. package/dist/structure/engine/version.d.ts +18 -0
  31. package/dist/structure/init-block-constructor.d.ts +32 -0
  32. package/dist/structure/rules/block-package-json.d.ts +9 -0
  33. package/dist/structure/rules/block.d.ts +1 -0
  34. package/dist/structure/rules/migrations.d.ts +2 -0
  35. package/dist/structure/rules/model-package-json.d.ts +3 -0
  36. package/dist/structure/rules/model.d.ts +1 -0
  37. package/dist/structure/rules/root-catalog-bump.d.ts +1 -0
  38. package/dist/structure/rules/root-gitignore.d.ts +2 -0
  39. package/dist/structure/rules/root-package-json.d.ts +3 -0
  40. package/dist/structure/rules/root-pnpm-workspace.d.ts +15 -0
  41. package/dist/structure/rules/root.d.ts +1 -0
  42. package/dist/structure/rules/shared/colocated-tests.d.ts +2 -0
  43. package/dist/structure/rules/shared/key-order.d.ts +1 -0
  44. package/dist/structure/rules/software-package-json.d.ts +3 -0
  45. package/dist/structure/rules/software.d.ts +1 -0
  46. package/dist/structure/rules/test-package-json.d.ts +3 -0
  47. package/dist/structure/rules/test.d.ts +1 -0
  48. package/dist/structure/rules/ui-package-json.d.ts +3 -0
  49. package/dist/structure/rules/ui.d.ts +1 -0
  50. package/dist/structure/rules/workflow-package-json.d.ts +3 -0
  51. package/dist/structure/rules/workflow.d.ts +1 -0
  52. package/dist/structure/structure-definition.d.ts +1 -0
  53. package/package.json +8 -6
  54. package/src/cmd/index.ts +10 -4
  55. package/src/cmd/structure/check.ts +51 -0
  56. package/src/cmd/structure/init.ts +65 -0
  57. package/src/cmd/structure/refresh.ts +44 -0
  58. package/src/cmd/update-deps.ts +15 -3
  59. package/src/structure/__tests__/colocated-test-wiring.test.ts +107 -0
  60. package/src/structure/__tests__/floor-version.test.ts +52 -0
  61. package/src/structure/__tests__/init-catalog-resolution.test.ts +82 -0
  62. package/src/structure/__tests__/init-check-parity.test.ts +59 -0
  63. package/src/structure/__tests__/model-package-json.snapshot.test.ts +60 -0
  64. package/src/structure/__tests__/oxfmt-clean-init.test.ts +64 -0
  65. package/src/structure/__tests__/root-catalog-bump.test.ts +110 -0
  66. package/src/structure/__tests__/seed-disjointness.test.ts +79 -0
  67. package/src/structure/cli/run-structure.ts +89 -0
  68. package/src/structure/engine/__tests__/content-rules/active-state-guards.test.ts +61 -0
  69. package/src/structure/engine/__tests__/content-rules/catalog-pin.test.ts +46 -0
  70. package/src/structure/engine/__tests__/content-rules/enforce-alphabetical-order.test.ts +55 -0
  71. package/src/structure/engine/__tests__/content-rules/enforce-field-order.test.ts +73 -0
  72. package/src/structure/engine/__tests__/content-rules/ensure-catalog-absent.test.ts +29 -0
  73. package/src/structure/engine/__tests__/content-rules/ensure-catalog-latest.test.ts +70 -0
  74. package/src/structure/engine/__tests__/content-rules/ensure-dep.test.ts +63 -0
  75. package/src/structure/engine/__tests__/content-rules/ensure-deps-bulk.test.ts +46 -0
  76. package/src/structure/engine/__tests__/content-rules/ensure-field-entries.test.ts +35 -0
  77. package/src/structure/engine/__tests__/content-rules/ensure-field.test.ts +56 -0
  78. package/src/structure/engine/__tests__/content-rules/ensure-script.test.ts +39 -0
  79. package/src/structure/engine/__tests__/content-rules/ensure-workspace-scope-deps.test.ts +92 -0
  80. package/src/structure/engine/__tests__/content-rules/gitignore.test.ts +50 -0
  81. package/src/structure/engine/__tests__/content-rules/pin-catalog-to.test.ts +64 -0
  82. package/src/structure/engine/__tests__/content-rules/prune-keys.test.ts +59 -0
  83. package/src/structure/engine/__tests__/content-rules/registry-client.test.ts +116 -0
  84. package/src/structure/engine/__tests__/content-rules/remove-field.test.ts +49 -0
  85. package/src/structure/engine/__tests__/content-rules/require-field.test.ts +40 -0
  86. package/src/structure/engine/__tests__/content-rules/transform-at.test.ts +33 -0
  87. package/src/structure/engine/__tests__/content-rules/when-inner.test.ts +244 -0
  88. package/src/structure/engine/__tests__/content-rules/workspace-module-paths.test.ts +53 -0
  89. package/src/structure/engine/__tests__/discovery.test.ts +160 -0
  90. package/src/structure/engine/__tests__/fs-memory.test.ts +59 -0
  91. package/src/structure/engine/__tests__/glob.test.ts +43 -0
  92. package/src/structure/engine/__tests__/parsers-json.test.ts +189 -0
  93. package/src/structure/engine/__tests__/parsers-lines.test.ts +44 -0
  94. package/src/structure/engine/__tests__/parsers-yaml.test.ts +90 -0
  95. package/src/structure/engine/__tests__/recheck.test.ts +99 -0
  96. package/src/structure/engine/__tests__/registry-lookup.test.ts +49 -0
  97. package/src/structure/engine/__tests__/runner-when-inside-managed.test.ts +136 -0
  98. package/src/structure/engine/__tests__/runner.test.ts +136 -0
  99. package/src/structure/engine/__tests__/synthetic.test.ts +153 -0
  100. package/src/structure/engine/__tests__/version.test.ts +44 -0
  101. package/src/structure/engine/__tests__/when-files-exist.test.ts +89 -0
  102. package/src/structure/engine/api.ts +131 -0
  103. package/src/structure/engine/builders.ts +341 -0
  104. package/src/structure/engine/content-rules.ts +616 -0
  105. package/src/structure/engine/ctx.ts +94 -0
  106. package/src/structure/engine/discovery-fs.ts +209 -0
  107. package/src/structure/engine/discovery.ts +168 -0
  108. package/src/structure/engine/flatten.ts +117 -0
  109. package/src/structure/engine/fs/api.ts +40 -0
  110. package/src/structure/engine/fs/memory.ts +132 -0
  111. package/src/structure/engine/fs/node.ts +89 -0
  112. package/src/structure/engine/glob.ts +42 -0
  113. package/src/structure/engine/ir.ts +149 -0
  114. package/src/structure/engine/parsers/json.ts +212 -0
  115. package/src/structure/engine/parsers/lines.ts +42 -0
  116. package/src/structure/engine/parsers/yaml.ts +72 -0
  117. package/src/structure/engine/registry-client.ts +140 -0
  118. package/src/structure/engine/runner.ts +519 -0
  119. package/src/structure/engine/templates.ts +74 -0
  120. package/src/structure/engine/testing.ts +93 -0
  121. package/src/structure/engine/version.ts +60 -0
  122. package/src/structure/init-block-constructor.ts +164 -0
  123. package/src/structure/rules/block-package-json.ts +156 -0
  124. package/src/structure/rules/block.ts +29 -0
  125. package/src/structure/rules/migrations.ts +63 -0
  126. package/src/structure/rules/model-package-json.ts +123 -0
  127. package/src/structure/rules/model.ts +65 -0
  128. package/src/structure/rules/root-catalog-bump.ts +77 -0
  129. package/src/structure/rules/root-gitignore.ts +27 -0
  130. package/src/structure/rules/root-package-json.ts +119 -0
  131. package/src/structure/rules/root-pnpm-workspace.ts +103 -0
  132. package/src/structure/rules/root.ts +43 -0
  133. package/src/structure/rules/shared/colocated-tests.ts +11 -0
  134. package/src/structure/rules/shared/key-order.ts +34 -0
  135. package/src/structure/rules/software-package-json.ts +86 -0
  136. package/src/structure/rules/software.ts +33 -0
  137. package/src/structure/rules/test-package-json.ts +70 -0
  138. package/src/structure/rules/test.ts +40 -0
  139. package/src/structure/rules/ui-package-json.ts +108 -0
  140. package/src/structure/rules/ui.ts +75 -0
  141. package/src/structure/rules/workflow-package-json.ts +92 -0
  142. package/src/structure/rules/workflow.ts +51 -0
  143. package/src/structure/structure-definition.ts +28 -0
  144. package/src/structure/templates/static/block/index.d.ts +6 -0
  145. package/src/structure/templates/static/block/index.js +8 -0
  146. package/src/structure/templates/static/block/logos/block-logo.png +0 -0
  147. package/src/structure/templates/static/block/logos/organization-logo.png +0 -0
  148. package/src/structure/templates/static/model/.oxfmtrc.json +4 -0
  149. package/src/structure/templates/static/model/.oxlintrc.json +3 -0
  150. package/src/structure/templates/static/model/src/index.ts +13 -0
  151. package/src/structure/templates/static/model/tsconfig.json +4 -0
  152. package/src/structure/templates/static/root/.gitignore +13 -0
  153. package/src/structure/templates/static/root/.vscode/settings.json +3 -0
  154. package/src/structure/templates/static/root/turbo.json +35 -0
  155. package/src/structure/templates/static/test/tsconfig.json +5 -0
  156. package/src/structure/templates/static/test/vitest.config.mts +8 -0
  157. package/src/structure/templates/static/ui/.oxfmtrc.json +3 -0
  158. package/src/structure/templates/static/ui/.oxlintrc.json +3 -0
  159. package/src/structure/templates/static/ui/index.html +12 -0
  160. package/src/structure/templates/static/ui/src/main.ts +5 -0
  161. package/src/structure/templates/static/ui/tsconfig.json +4 -0
  162. package/src/structure/templates/static/workflow/format.el +42 -0
  163. package/src/structure/templates/static/workflow/tsconfig.json +16 -0
  164. package/src/structure/templates/static/workflow/vitest.config.mts +9 -0
  165. package/src/structure/templates/text/README.md +5 -0
  166. package/src/structure/templates/text/docs/description.md +3 -0
  167. package/src/structure/templates/text/ui/src/MainPage.tpl.vue +9 -0
  168. package/src/structure/templates/text/ui/src/app.tpl.ts +11 -0
  169. package/src/structure/templates/text/workflow/src/main.tpl.tengo +10 -0
@@ -0,0 +1,136 @@
1
+ // Engine runner — structural pass, managed pass, mode semantics.
2
+ //
3
+ // Covers:
4
+ // - init (fresh FS): structural + managed primitives all fire.
5
+ // - refresh (post-init same FS): idempotent — second run is no-op.
6
+ // - check (dryRun): no writes; same changes reported as refresh would.
7
+ // - .structure is written only in refresh default mode (not check,
8
+ // not --update-deps-only).
9
+
10
+ import { describe, test, expect } from "vitest";
11
+ import { defineStructure, scope, managed, fixed, remove, text, generate } from "../api";
12
+ import type { Structure } from "../ir";
13
+ import type { RunContext } from "../api";
14
+ import { createRunContext } from "../ctx";
15
+ import { MemoryFileSystem } from "../fs/memory";
16
+ import { run } from "../runner";
17
+ import { ensureField } from "../content-rules";
18
+ import { STRUCTURE_META_FILE, STRUCTURE_VERSION } from "../version";
19
+
20
+ function syntheticStructure(): Structure {
21
+ return defineStructure(() => {
22
+ scope("root", () => {
23
+ fixed("turbo.json", text('{ "$schema": "https://turbo.build/schema.json" }\n'));
24
+ remove("legacy.txt");
25
+ managed(
26
+ "package.json",
27
+ generate(() => ({ name: "demo", version: "1.0.0" })),
28
+ () => {
29
+ ensureField("type", "module");
30
+ ensureField("scripts.build", "echo build");
31
+ },
32
+ );
33
+ });
34
+ scope("model", () => {
35
+ fixed("tsconfig.json", text('{ "extends": "../tsconfig.base.json" }\n'));
36
+ });
37
+ });
38
+ }
39
+
40
+ function ctxFor(opts: Partial<RunContext> = {}): RunContext {
41
+ return createRunContext({
42
+ blockVars: {
43
+ facadeName: "@platforma-open/demo",
44
+ baseName: "demo",
45
+ npmOrg: "@platforma-open",
46
+ orgScope: "demo",
47
+ shortName: "demo",
48
+ },
49
+ modules: [
50
+ { scope: "root", name: "@platforma-open/demo", path: "" },
51
+ { scope: "model", name: "@platforma-open/demo.model", path: "model" },
52
+ ],
53
+ ...opts,
54
+ });
55
+ }
56
+
57
+ describe("runner — init + refresh + check", () => {
58
+ test("init (fresh FS): structural + managed primitives all fire", async () => {
59
+ const fs = new MemoryFileSystem();
60
+ const ctx = ctxFor();
61
+ const result = run(syntheticStructure(), fs, ctx);
62
+ // Expected changes: fixed root/turbo.json + managed root/package.json
63
+ // + fixed model/tsconfig.json. remove("legacy.txt") is no-op (file
64
+ // never existed). Plus the .structure-version write at end (not in
65
+ // change list).
66
+ const paths = result.changes.map((c) => c.path).sort();
67
+ expect(paths).toContain("turbo.json");
68
+ expect(paths).toContain("package.json");
69
+ expect(paths).toContain("model/tsconfig.json");
70
+ expect(paths).not.toContain("legacy.txt");
71
+
72
+ // Files actually written.
73
+ expect(fs.exists("turbo.json")).toBe(true);
74
+ expect(fs.exists("model/tsconfig.json")).toBe(true);
75
+ const pkg = JSON.parse(fs.read("package.json"));
76
+ expect(pkg.type).toBe("module");
77
+ expect(pkg.scripts.build).toBe("echo build");
78
+ // .structure-version written.
79
+ expect(fs.exists(STRUCTURE_META_FILE)).toBe(true);
80
+ expect(JSON.parse(fs.read(STRUCTURE_META_FILE))).toEqual({
81
+ version: STRUCTURE_VERSION,
82
+ });
83
+ });
84
+
85
+ test("refresh (second run on same FS): idempotent — zero changes", async () => {
86
+ const fs = new MemoryFileSystem();
87
+ const ctx = ctxFor();
88
+ run(syntheticStructure(), fs, ctx);
89
+ const second = run(syntheticStructure(), fs, ctx);
90
+ expect(second.changes).toEqual([]);
91
+ });
92
+
93
+ test("check (dryRun): no writes; reports what refresh would do", async () => {
94
+ const fs = new MemoryFileSystem();
95
+ const ctx = ctxFor({ dryRun: true });
96
+ const result = run(syntheticStructure(), fs, ctx);
97
+ // Reports the same primitives that refresh would apply.
98
+ const paths = result.changes.map((c) => c.path).sort();
99
+ expect(paths).toContain("turbo.json");
100
+ expect(paths).toContain("package.json");
101
+ expect(paths).toContain("model/tsconfig.json");
102
+ // No actual writes.
103
+ expect(fs.exists("turbo.json")).toBe(false);
104
+ expect(fs.exists(STRUCTURE_META_FILE)).toBe(false);
105
+ });
106
+
107
+ test("refresh removes a file that does exist", async () => {
108
+ const fs = new MemoryFileSystem({ "legacy.txt": "x" });
109
+ const ctx = ctxFor();
110
+ const result = run(syntheticStructure(), fs, ctx);
111
+ const removed = result.changes.find((c) => c.primitive === "remove");
112
+ expect(removed).toBeDefined();
113
+ expect(removed!.path).toBe("legacy.txt");
114
+ expect(fs.exists("legacy.txt")).toBe(false);
115
+ });
116
+ });
117
+
118
+ describe("runner — .structure write gated by mode", () => {
119
+ test("refresh default mode writes .structure", async () => {
120
+ const fs = new MemoryFileSystem();
121
+ run(syntheticStructure(), fs, ctxFor());
122
+ expect(fs.exists(STRUCTURE_META_FILE)).toBe(true);
123
+ });
124
+
125
+ test("check (dryRun) does NOT write .structure", async () => {
126
+ const fs = new MemoryFileSystem();
127
+ run(syntheticStructure(), fs, ctxFor({ dryRun: true }));
128
+ expect(fs.exists(STRUCTURE_META_FILE)).toBe(false);
129
+ });
130
+
131
+ test("refresh --update-deps-only does NOT write .structure", async () => {
132
+ const fs = new MemoryFileSystem();
133
+ run(syntheticStructure(), fs, ctxFor({ updateDepsOnly: true }));
134
+ expect(fs.exists(STRUCTURE_META_FILE)).toBe(false);
135
+ });
136
+ });
@@ -0,0 +1,153 @@
1
+ // Synthetic-fixture round trip through the IR.
2
+ //
3
+ // Exercises module-global builders: `defineStructure(() => { ... })`
4
+ // with builders imported by name. Covers a scope frame, a when frame, both
5
+ // mode frames (`onUpdateDeps` and `onInitOrUpdate`), a rename. Asserts IR
6
+ // shape, the post-flatten ordered list (including the per-leaf `modes`
7
+ // set), and that two independent build invocations produce
8
+ // structurally-equal trees (the active-context machinery resets cleanly
9
+ // between runs).
10
+
11
+ import { describe, test, expect } from "vitest";
12
+ import {
13
+ defineStructure,
14
+ scope,
15
+ when,
16
+ onUpdateDeps,
17
+ onInitOrUpdate,
18
+ fixed,
19
+ managed,
20
+ remove,
21
+ rename,
22
+ file,
23
+ } from "../api";
24
+ import type { Structure } from "../ir";
25
+ import { flatten } from "../flatten";
26
+ import { createRunContext } from "../ctx";
27
+
28
+ function buildStructure(): Structure {
29
+ return defineStructure(() => {
30
+ scope("root", () => {
31
+ fixed("turbo.json", file("root/turbo.json"));
32
+ remove(".prettierrc");
33
+ managed("package.json", file("root/package.json"), () => {
34
+ /* body — stub */
35
+ });
36
+ when(
37
+ ({ ctx }) => !ctx.isSdkInternal,
38
+ () => {
39
+ rename("test/", "test-legacy/");
40
+ },
41
+ );
42
+ });
43
+ scope("model", () => {
44
+ fixed("tsconfig.json", file("model/tsconfig.json"));
45
+ });
46
+ onUpdateDeps(() => {
47
+ scope("root", () => {
48
+ managed("pnpm-workspace.yaml", file("root/pnpm-workspace.yaml"), () => {
49
+ /* update-deps-only catalog bumps live here */
50
+ });
51
+ });
52
+ });
53
+ onInitOrUpdate(() => {
54
+ scope("root", () => {
55
+ managed("catalog.yaml", file("root/catalog.yaml"), () => {
56
+ /* init + update-deps version resolution lives here */
57
+ });
58
+ });
59
+ });
60
+ });
61
+ }
62
+
63
+ describe("synthetic fixture", () => {
64
+ test("IR shape matches expectation", () => {
65
+ const s = buildStructure();
66
+ expect(s.children.length).toBe(4);
67
+ const rootFrame = s.children[0]!;
68
+ expect(rootFrame.kind).toBe("scope");
69
+ if (rootFrame.kind !== "scope") throw new Error("unreachable");
70
+ expect(rootFrame.scope).toBe("root");
71
+ expect(rootFrame.children.length).toBe(4);
72
+ expect(rootFrame.children[0]!.kind).toBe("fixed");
73
+ expect(rootFrame.children[1]!.kind).toBe("remove");
74
+ expect(rootFrame.children[2]!.kind).toBe("managed");
75
+ expect(rootFrame.children[3]!.kind).toBe("when");
76
+ // Both mode frames flatten to `kind: "mode"`, distinguished by `modes`.
77
+ const updFrame = s.children[2]!;
78
+ const initUpdFrame = s.children[3]!;
79
+ expect(updFrame.kind).toBe("mode");
80
+ expect(initUpdFrame.kind).toBe("mode");
81
+ if (updFrame.kind !== "mode" || initUpdFrame.kind !== "mode") throw new Error("unreachable");
82
+ expect(updFrame.modes).toEqual(["updateDeps"]);
83
+ expect(initUpdFrame.modes).toEqual(["init", "updateDeps"]);
84
+ });
85
+
86
+ test("flatten produces expected flat-item list (per-leaf modes tagged)", () => {
87
+ const s = buildStructure();
88
+ const ctx = createRunContext({
89
+ blockVars: {
90
+ facadeName: "@platforma-open/x.demo",
91
+ baseName: "x.demo",
92
+ npmOrg: "@platforma-open",
93
+ orgScope: "x",
94
+ shortName: "demo",
95
+ },
96
+ modules: [
97
+ { scope: "root", name: "@platforma-open/x.demo", path: "" },
98
+ { scope: "model", name: "@platforma-open/x.demo.model", path: "model" },
99
+ ],
100
+ });
101
+ const flat = flatten(s, ctx);
102
+ // 4 leaves in root scope + 1 in model + 1 in onUpdateDeps→root + 1 in
103
+ // onInitOrUpdate→root.
104
+ expect(flat.length).toBe(7);
105
+ expect(flat[0]!.resolvedPath).toBe("turbo.json");
106
+ // A normal leaf fires on default refresh/check + init, never update-deps.
107
+ expect(flat[0]!.modes).toEqual(["default", "init"]);
108
+
109
+ const renameItem = flat.find((f) => f.leaf.kind === "rename");
110
+ expect(renameItem).toBeDefined();
111
+ expect(renameItem!.resolvedPath).toBe("test/");
112
+ expect(renameItem!.resolvedTo).toBe("test-legacy/");
113
+ expect(renameItem!.triggers.length).toBe(1);
114
+
115
+ const updOnly = flat.find((f) => f.resolvedPath === "pnpm-workspace.yaml");
116
+ expect(updOnly!.modes).toEqual(["updateDeps"]);
117
+
118
+ const initOrUpd = flat.find((f) => f.resolvedPath === "catalog.yaml");
119
+ expect(initOrUpd!.modes).toEqual(["init", "updateDeps"]);
120
+
121
+ // Leaves that fire in update-deps mode = both mode-framed leaves.
122
+ const updItems = flat.filter((f) => f.modes.includes("updateDeps"));
123
+ expect(updItems.map((f) => f.resolvedPath).sort()).toEqual([
124
+ "catalog.yaml",
125
+ "pnpm-workspace.yaml",
126
+ ]);
127
+ });
128
+
129
+ test("two builds produce structurally-equal trees", () => {
130
+ const a = buildStructure();
131
+ const b = buildStructure();
132
+ // Functions inside the tree (trigger, body, generate) won't
133
+ // compare deep-equal across builds — strip them for the shape
134
+ // check.
135
+ const strip = (val: unknown): unknown => {
136
+ if (typeof val === "function") return "[fn]";
137
+ if (Array.isArray(val)) return val.map(strip);
138
+ if (val && typeof val === "object") {
139
+ const out: Record<string, unknown> = {};
140
+ for (const [k, v] of Object.entries(val as object)) {
141
+ out[k] = strip(v);
142
+ }
143
+ return out;
144
+ }
145
+ return val;
146
+ };
147
+ expect(strip(a)).toEqual(strip(b));
148
+ });
149
+
150
+ test("builder called outside defineStructure throws", () => {
151
+ expect(() => fixed("oops.json", file("nope"))).toThrow(/defineStructure/);
152
+ });
153
+ });
@@ -0,0 +1,44 @@
1
+ // `.structure` metadata file handling: read default, read parsed
2
+ // value, write, floor enforcement.
3
+
4
+ import { describe, test, expect } from "vitest";
5
+ import { MemoryFileSystem } from "../fs/memory";
6
+ import {
7
+ readStructureVersion,
8
+ writeStructureVersion,
9
+ assertVersionAboveFloor,
10
+ STRUCTURE_VERSION,
11
+ StructureVersionFloorError,
12
+ } from "../version";
13
+
14
+ describe(".structure metadata file", () => {
15
+ test("missing file reads as 0", async () => {
16
+ const fs = new MemoryFileSystem();
17
+ expect(await readStructureVersion(fs)).toBe(0);
18
+ });
19
+
20
+ test("write then read round-trips", async () => {
21
+ const fs = new MemoryFileSystem();
22
+ await writeStructureVersion(fs, 3);
23
+ expect(await readStructureVersion(fs)).toBe(3);
24
+ const written = fs.read(".structure");
25
+ expect(JSON.parse(written)).toEqual({ version: 3 });
26
+ });
27
+
28
+ test("default write uses STRUCTURE_VERSION", async () => {
29
+ const fs = new MemoryFileSystem();
30
+ await writeStructureVersion(fs);
31
+ expect(await readStructureVersion(fs)).toBe(STRUCTURE_VERSION);
32
+ });
33
+
34
+ test("floor check throws below floor", () => {
35
+ expect(() => assertVersionAboveFloor(2, 3)).toThrow(StructureVersionFloorError);
36
+ expect(() => assertVersionAboveFloor(3, 3)).not.toThrow();
37
+ expect(() => assertVersionAboveFloor(0, 0)).not.toThrow();
38
+ });
39
+
40
+ test("invalid JSON reads as 0", async () => {
41
+ const fs = new MemoryFileSystem({ ".structure": "not-json" });
42
+ expect(await readStructureVersion(fs)).toBe(0);
43
+ });
44
+ });
@@ -0,0 +1,89 @@
1
+ // `whenFilesExist` resolves its glob MODULE-RELATIVE to the leaf's bound
2
+ // module, so the same predicate gates differently per scope when a scope
3
+ // frame fans out. Drives the real runner so the per-item TriggerContext
4
+ // threading (modulePath -> filesMatch) is exercised end-to-end.
5
+
6
+ import { describe, test, expect } from "vitest";
7
+ import { defineStructure, managed, scope, generate, when, whenFilesExist } from "../api";
8
+ import type { Structure } from "../ir";
9
+ import { createRunContext } from "../ctx";
10
+ import { MemoryFileSystem } from "../fs/memory";
11
+ import { run } from "../runner";
12
+ import { ensureField } from "../content-rules";
13
+
14
+ function structure(): Structure {
15
+ return defineStructure(() => {
16
+ for (const s of ["model", "ui"] as const) {
17
+ scope(s, () => {
18
+ managed(
19
+ "tsconfig.json",
20
+ generate(() => ({ extends: "base" })),
21
+ () => {
22
+ when(whenFilesExist("src/**/*.test.ts"), () => {
23
+ ensureField("compilerOptions.types", ["node"]);
24
+ });
25
+ },
26
+ );
27
+ });
28
+ }
29
+ });
30
+ }
31
+
32
+ function ctx() {
33
+ return createRunContext({
34
+ blockVars: {
35
+ facadeName: "@platforma-open/demo",
36
+ baseName: "demo",
37
+ npmOrg: "@platforma-open",
38
+ orgScope: "demo",
39
+ shortName: "demo",
40
+ },
41
+ modules: [
42
+ { scope: "model", name: "@platforma-open/demo.model", path: "model" },
43
+ { scope: "ui", name: "@platforma-open/demo.ui", path: "ui" },
44
+ ],
45
+ });
46
+ }
47
+
48
+ async function typesOf(fs: MemoryFileSystem, scopePath: string): Promise<unknown> {
49
+ const json = JSON.parse(fs.read(`${scopePath}/tsconfig.json`)) as {
50
+ compilerOptions?: { types?: unknown };
51
+ };
52
+ return json.compilerOptions?.types;
53
+ }
54
+
55
+ describe("whenFilesExist — module-relative gating via the runner", () => {
56
+ test("gates per module: only the scope whose own src has a test file fires", async () => {
57
+ const fs = new MemoryFileSystem();
58
+ // A co-located test under model/, none under ui/.
59
+ fs.write("model/src/label.test.ts", "// test");
60
+ fs.write("ui/src/main.ts", "// not a test");
61
+
62
+ run(structure(), fs, ctx());
63
+
64
+ expect(await typesOf(fs, "model")).toEqual(["node"]);
65
+ // ui has no test file -> predicate false -> no node types.
66
+ expect(await typesOf(fs, "ui")).toBeUndefined();
67
+ });
68
+
69
+ test("nested test path (src/test/*.test.ts) still matches via **", async () => {
70
+ const fs = new MemoryFileSystem();
71
+ fs.write("ui/src/test/import.test.ts", "// nested test");
72
+
73
+ run(structure(), fs, ctx());
74
+
75
+ expect(await typesOf(fs, "ui")).toEqual(["node"]);
76
+ expect(await typesOf(fs, "model")).toBeUndefined();
77
+ });
78
+
79
+ test("no test files anywhere -> neither scope gets node types", async () => {
80
+ const fs = new MemoryFileSystem();
81
+ fs.write("model/src/index.ts", "// source");
82
+ fs.write("ui/src/main.ts", "// source");
83
+
84
+ run(structure(), fs, ctx());
85
+
86
+ expect(await typesOf(fs, "model")).toBeUndefined();
87
+ expect(await typesOf(fs, "ui")).toBeUndefined();
88
+ });
89
+ });
@@ -0,0 +1,131 @@
1
+ // Public API of the structurer engine.
2
+ //
3
+ // `defineStructure(() => { ... })` is the single entry point. Inside
4
+ // the lambda, rule authors call builders imported by name from this
5
+ // module — `scope`, `when`, `fixed`, `managed`, `scaffold`, `seed`,
6
+ // `remove`, `rename`, `onUpdateDeps`, plus the content-form helpers
7
+ // `file` / `text` / `tpl` / `generate`. Builders read a module-global
8
+ // active context (set up by `defineStructure`) and append to its tree.
9
+ // Calling a builder outside `defineStructure` throws at runtime.
10
+
11
+ import type { ContentForm, ManagedBody, RunMode, Structure, TriggerFn } from "./ir";
12
+
13
+ export type Scope = "root" | "block" | "model" | "ui" | "workflow" | "test" | "software";
14
+
15
+ /** Variables describing a block — populated by `init` or parsed by
16
+ * `check`/`refresh` from the block's package.json `name`. */
17
+ export type BlockVars = {
18
+ /** e.g. `@platforma-open/my-org.mixcr-clonotyping` */
19
+ facadeName: string;
20
+ /** e.g. `my-org.mixcr-clonotyping` */
21
+ baseName: string;
22
+ /** e.g. `@platforma-open` */
23
+ npmOrg: string;
24
+ /** e.g. `my-org` */
25
+ orgScope: string;
26
+ /** e.g. `mixcr-clonotyping` */
27
+ shortName: string;
28
+ /** Platform picked at init for the (single) software sub-package.
29
+ * Only populated by `init`; absent on `check` / `refresh`. Blocks
30
+ * that legitimately have multiple software modules (encountered via
31
+ * DISCOVERY on refresh) are not affected — this field only governs
32
+ * what `init` scaffolds. */
33
+ softwarePlatform?: string;
34
+ };
35
+
36
+ /** Discovered workspace module. */
37
+ export type Module = {
38
+ scope: Scope;
39
+ /** Package name from the module's package.json. */
40
+ name: string;
41
+ /** Block-relative path of the module ("" for workspace root). */
42
+ path: string;
43
+ };
44
+
45
+ /** Run context — passed into engine.run and surfaced inside trigger
46
+ * predicates as the `ctx` field. */
47
+ export type RunContext = {
48
+ /** `--sdk-internal` flag. */
49
+ isSdkInternal: boolean;
50
+ /** `--update-deps-only` flag. When true, the run is in mode
51
+ * `"updateDeps"`: only `onUpdateDeps` / `onInitOrUpdate` leaves fire;
52
+ * all default-mode leaves are skipped. The caller is then expected to
53
+ * run `pnpm install` and re-invoke `refresh` without the flag for the
54
+ * default-mode rules to apply against the updated dep set. */
55
+ updateDepsOnly: boolean;
56
+ /** Discovered modules (every scope). */
57
+ modules: Module[];
58
+ /** Block vars; on `init` populated from flags/prompts, on
59
+ * `check`/`refresh` parsed from block/package.json. */
60
+ blockVars: BlockVars;
61
+ /** Layout version read from `.structure`. */
62
+ version: number;
63
+ /** True for `check` mode and the post-run recheck. */
64
+ dryRun: boolean;
65
+ };
66
+
67
+ export type { ContentForm, ManagedBody, RunMode, Structure, TriggerFn };
68
+
69
+ export {
70
+ defineStructure,
71
+ scope,
72
+ when,
73
+ whenFilesExist,
74
+ onUpdateDeps,
75
+ onInitOrUpdate,
76
+ fixed,
77
+ managed,
78
+ scaffold,
79
+ seed,
80
+ remove,
81
+ rename,
82
+ file,
83
+ binaryFile,
84
+ text,
85
+ tpl,
86
+ generate,
87
+ blockVars,
88
+ } from "./builders";
89
+
90
+ export {
91
+ // JSON managed builders
92
+ ensureField,
93
+ removeField,
94
+ requireField,
95
+ ensureFieldEntries,
96
+ ensureScript,
97
+ removeScript,
98
+ ensureDep,
99
+ ensureDevDep,
100
+ ensurePeerDep,
101
+ ensureOptionalDep,
102
+ removeDep,
103
+ ensureDeps,
104
+ ensureDevDeps,
105
+ ensurePeerDeps,
106
+ ensureOptionalDeps,
107
+ ensureWorkspaceScopeDeps,
108
+ ensureWorkspaceScopeDevDeps,
109
+ ensureWorkspaceScopePeerDeps,
110
+ pruneKeysMatching,
111
+ pruneKeysMatchingAt,
112
+ enforceFieldOrder,
113
+ enforceFieldOrderAt,
114
+ enforceAlphabeticalOrder,
115
+ transformAt,
116
+ // YAML managed builders
117
+ ensureWorkspaceModulePaths,
118
+ ensureCatalogVersion,
119
+ pinCatalogTo,
120
+ ensureCatalogLatest,
121
+ ensureCatalogAbsent,
122
+ // Lines managed builders
123
+ ensureGitignoreEntries,
124
+ removeGitignoreEntries,
125
+ // Active-state wrappers (engine-internal but exported for tests).
126
+ withManagedBody,
127
+ withManagedYaml,
128
+ withManagedLines,
129
+ } from "./content-rules";
130
+
131
+ export type { DepVersion } from "./content-rules";