@platforma-sdk/block-tools 2.10.14 → 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 +9 -7
  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,341 @@
1
+ // `defineStructure` + the module-global builder set.
2
+ //
3
+ // Active context is a module-level mutable: `defineStructure` pushes a
4
+ // fresh tree state, invokes the zero-arg lambda, pops, returns. Each
5
+ // builder reads the active state and appends to the current children
6
+ // list. Calling a builder outside `defineStructure` throws with a
7
+ // clear message. Re-entering `defineStructure` from inside another
8
+ // `defineStructure` lambda is rejected.
9
+
10
+ import type { Scope, RunContext, BlockVars, ContentForm } from "./api";
11
+ import type {
12
+ ManagedBody,
13
+ Structure,
14
+ TriggerFn,
15
+ TreeNode,
16
+ ScopeFrame,
17
+ WhenFrame,
18
+ ModeFrame,
19
+ RunMode,
20
+ } from "./ir";
21
+
22
+ type TreeState = {
23
+ root: TreeNode[];
24
+ /** Stack of children-lists; top is where new nodes append. */
25
+ stack: TreeNode[][];
26
+ inScope: boolean;
27
+ };
28
+
29
+ let activeTree: TreeState | undefined;
30
+ let activeRunContext: RunContext | undefined;
31
+
32
+ function requireActiveTree(builder: string): TreeState {
33
+ if (!activeTree) {
34
+ throw new Error(
35
+ `${builder}() called outside defineStructure(). ` +
36
+ `Builders must run inside the lambda passed to defineStructure(...).`,
37
+ );
38
+ }
39
+ return activeTree;
40
+ }
41
+
42
+ function appendNode(builder: string, node: TreeNode): void {
43
+ const t = requireActiveTree(builder);
44
+ t.stack[t.stack.length - 1]!.push(node);
45
+ }
46
+
47
+ /** Engine-internal: scope `blockVars()` lookups during a run. The body is
48
+ * synchronous, so the module-global `activeRunContext` cannot be observed
49
+ * by an interleaved run — a sync call stack runs to completion before any
50
+ * other `run()` can start. The try/finally restores the previous context
51
+ * (supporting the nested-run case the post-run recheck used to imply). */
52
+ export function withRunContext<T>(ctx: RunContext, fn: () => T): T {
53
+ const prev = activeRunContext;
54
+ activeRunContext = ctx;
55
+ try {
56
+ return fn();
57
+ } finally {
58
+ activeRunContext = prev;
59
+ }
60
+ }
61
+
62
+ /** Engine-internal: content-rule builders that need `ctx.modules` look
63
+ * up the active context through this accessor. Returns `undefined`
64
+ * outside a run. */
65
+ export function tryGetActiveRunContext(): RunContext | undefined {
66
+ return activeRunContext;
67
+ }
68
+
69
+ /** Get-or-die counterpart of `tryGetActiveRunContext`. Generators run
70
+ * only inside `engine.run()`; an absent context is framework misuse, not
71
+ * a valid state, so throw rather than emit degenerate (empty) output.
72
+ * Mirrors `blockVars()`. */
73
+ export function getActiveRunContext(): RunContext {
74
+ if (!activeRunContext) {
75
+ throw new Error(
76
+ "getActiveRunContext() called outside engine.run(). " +
77
+ "Generators only see the run context during a run.",
78
+ );
79
+ }
80
+ return activeRunContext;
81
+ }
82
+
83
+ /** The single entry point: build a {@link Structure} from `fn`, which calls
84
+ * the scaffold-DSL builders (`scope`, `fixed`, `managed`, `seed`, …). Throws
85
+ * if called nested, or if `fn` leaves an unclosed group frame. */
86
+ export function defineStructure(fn: () => void): Structure {
87
+ if (activeTree) {
88
+ throw new Error("defineStructure() cannot be called from inside another defineStructure().");
89
+ }
90
+ const root: TreeNode[] = [];
91
+ const state: TreeState = { root, stack: [root], inScope: false };
92
+ activeTree = state;
93
+ try {
94
+ fn();
95
+ if (state.stack.length !== 1) {
96
+ throw new Error(
97
+ `defineStructure: builder body left ${state.stack.length - 1} unclosed group frame(s)`,
98
+ );
99
+ }
100
+ const structure: Structure = { children: root };
101
+ // Validation: a path may not appear as both `seed` and any
102
+ // engine-managed primitive (`fixed` / `managed` / `scaffold`) within
103
+ // the same scope. The two contracts (write-once-and-ignore vs
104
+ // rewrite-on-every-refresh) are mutually exclusive. Detect at
105
+ // tree-build time.
106
+ validateSeedDisjointness(structure);
107
+ return structure;
108
+ } finally {
109
+ activeTree = undefined;
110
+ }
111
+ }
112
+
113
+ function validateSeedDisjointness(structure: Structure): void {
114
+ // (scope) → (path) → set of leaf kinds present at that path.
115
+ const perScope = new Map<string, Map<string, Set<string>>>();
116
+
117
+ function visit(node: TreeNode, scopeName: string): void {
118
+ if (node.kind === "scope") {
119
+ for (const c of node.children) visit(c, node.scope);
120
+ return;
121
+ }
122
+ if (node.kind === "when" || node.kind === "mode") {
123
+ for (const c of node.children) visit(c, scopeName);
124
+ return;
125
+ }
126
+ if (node.kind === "rename") return; // rename works on paths; not a content leaf
127
+ let scopeMap = perScope.get(scopeName);
128
+ if (!scopeMap) {
129
+ scopeMap = new Map();
130
+ perScope.set(scopeName, scopeMap);
131
+ }
132
+ let kinds = scopeMap.get(node.path);
133
+ if (!kinds) {
134
+ kinds = new Set();
135
+ scopeMap.set(node.path, kinds);
136
+ }
137
+ kinds.add(node.kind);
138
+ }
139
+
140
+ for (const n of structure.children) visit(n, "(top-level)");
141
+
142
+ for (const [scopeName, paths] of perScope) {
143
+ for (const [path, kinds] of paths) {
144
+ if (
145
+ kinds.has("seed") &&
146
+ (kinds.has("managed") || kinds.has("fixed") || kinds.has("scaffold"))
147
+ ) {
148
+ const others = [...kinds]
149
+ .filter((k) => k !== "seed")
150
+ .sort()
151
+ .join(", ");
152
+ throw new Error(
153
+ `defineStructure validation: path '${path}' under scope '${scopeName}' ` +
154
+ `is declared as both seed and ${others}. Seeds are block-author territory ` +
155
+ `(written once at init, untouched thereafter); engine-managed primitives ` +
156
+ `(fixed/managed/scaffold) rewrite on every refresh. The two contracts cannot ` +
157
+ `coexist on the same path.`,
158
+ );
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ /** Open a scope (`root` / `block` / `model` / `ui` / `workflow` / `test` /
165
+ * `software`); builders called inside `body` attach to it. Cannot nest. */
166
+ export function scope(name: Scope, body: () => void): void {
167
+ const t = requireActiveTree("scope");
168
+ if (t.inScope) {
169
+ throw new Error("scope() cannot nest. Already inside a scope.");
170
+ }
171
+ const frame: ScopeFrame = { kind: "scope", scope: name, children: [] };
172
+ t.stack[t.stack.length - 1]!.push(frame);
173
+ t.stack.push(frame.children);
174
+ t.inScope = true;
175
+ try {
176
+ body();
177
+ } finally {
178
+ t.stack.pop();
179
+ t.inScope = false;
180
+ }
181
+ }
182
+
183
+ /** Inner-mode `when` handler. Registered by `content-rules.ts` at
184
+ * load time. Returns true if the call was dispatched (an inner active
185
+ * state was present); false otherwise. Avoids a circular import. */
186
+ type InnerWhenHandler = (trigger: TriggerFn, body: () => void) => boolean;
187
+ let innerWhenHandler: InnerWhenHandler | undefined;
188
+
189
+ /** Engine-internal: register the inner-mode `when` handler (set by
190
+ * `content-rules.ts` at load time to avoid a circular import). */
191
+ export function registerInnerWhenHandler(h: InnerWhenHandler): void {
192
+ innerWhenHandler = h;
193
+ }
194
+
195
+ /**
196
+ * Conditionally run `body`. Two dispatch modes by active state:
197
+ * - inside `defineStructure(...)` → push a `WhenFrame` into the tree;
198
+ * trigger fires later at runner time per `FlatItem`.
199
+ * - inside a `managed(...)` body → evaluate `trigger` immediately
200
+ * against the runner-supplied `TriggerContext`; run/skip `body`
201
+ * synchronously.
202
+ *
203
+ * One `when` symbol for both layers — same user contract, dispatch
204
+ * picks the matching execution timing.
205
+ */
206
+ export function when(trigger: TriggerFn, body: () => void): void {
207
+ if (activeTree) {
208
+ const t = activeTree;
209
+ const frame: WhenFrame = { kind: "when", trigger, children: [] };
210
+ t.stack[t.stack.length - 1]!.push(frame);
211
+ t.stack.push(frame.children);
212
+ try {
213
+ body();
214
+ } finally {
215
+ t.stack.pop();
216
+ }
217
+ return;
218
+ }
219
+ if (innerWhenHandler && innerWhenHandler(trigger, body)) return;
220
+ throw new Error(
221
+ "when() called outside defineStructure() and outside any managed(...) body. " +
222
+ "Place this call inside one of the two.",
223
+ );
224
+ }
225
+
226
+ /** Trigger factory: true when at least one file in the leaf's module
227
+ * matches `glob` (module-relative — see `TriggerContext.filesMatch`). Use
228
+ * to gate a rule on the presence of co-located files, e.g. add a vitest
229
+ * `test` script only when `src/**\/*.test.ts` exists. Pairs with `when`:
230
+ * `when(whenFilesExist("src/**\/*.test.ts"), () => { ... })`. */
231
+ export function whenFilesExist(glob: string): TriggerFn {
232
+ return (tctx) => tctx.filesMatch(glob);
233
+ }
234
+
235
+ function pushModeFrame(builder: string, modes: RunMode[], body: () => void): void {
236
+ const t = requireActiveTree(builder);
237
+ const frame: ModeFrame = { kind: "mode", modes, children: [] };
238
+ t.stack[t.stack.length - 1]!.push(frame);
239
+ t.stack.push(frame.children);
240
+ try {
241
+ body();
242
+ } finally {
243
+ t.stack.pop();
244
+ }
245
+ }
246
+
247
+ /** Leaves inside fire ONLY under `--update-deps-only` (mode
248
+ * `"updateDeps"`); they are skipped on default refresh/check and init. */
249
+ export function onUpdateDeps(body: () => void): void {
250
+ pushModeFrame("onUpdateDeps", ["updateDeps"], body);
251
+ }
252
+
253
+ /** Leaves inside fire on `init` AND `--update-deps-only`, but NOT on
254
+ * default refresh/check. The frame for rules that resolve/write current
255
+ * versions: both init and update-deps mean "fetch and write latest",
256
+ * while a plain refresh leaves them as the author/lockfile has them. */
257
+ export function onInitOrUpdate(body: () => void): void {
258
+ pushModeFrame("onInitOrUpdate", ["init", "updateDeps"], body);
259
+ }
260
+
261
+ /** Engine-OWNED file: (re)written verbatim from `content` on every refresh —
262
+ * author edits are overwritten. */
263
+ export function fixed(path: string, content: ContentForm): void {
264
+ appendNode("fixed", { kind: "fixed", path, content });
265
+ }
266
+
267
+ /** Engine-RECONCILED file: parsed from disk (or `initial` when absent),
268
+ * mutated by `body` (the JSON/YAML/lines builders), then re-serialised.
269
+ * Content the body does not touch is preserved. */
270
+ export function managed(path: string, initial: ContentForm, body: ManagedBody): void {
271
+ appendNode("managed", { kind: "managed", path, initial, body });
272
+ }
273
+
274
+ /** Write-once file: created from `initial` if absent (on init AND refresh),
275
+ * never overwritten once present. For files the author subsequently owns. */
276
+ export function scaffold(path: string, initial: ContentForm): void {
277
+ appendNode("scaffold", { kind: "scaffold", path, initial });
278
+ }
279
+
280
+ /** Author-owned starter file: written from `initial` only on `init`, and only
281
+ * if absent; `refresh` / `check` never touch it. */
282
+ export function seed(path: string, initial: ContentForm): void {
283
+ appendNode("seed", { kind: "seed", path, initial });
284
+ }
285
+
286
+ /** Delete `path` if present, on every refresh. */
287
+ export function remove(path: string): void {
288
+ appendNode("remove", { kind: "remove", path });
289
+ }
290
+
291
+ /** Rename file `from` → `to` on refresh (migrating a renamed scaffold file). */
292
+ export function rename(from: string, to: string): void {
293
+ appendNode("rename", { kind: "rename", from, to });
294
+ }
295
+
296
+ /** Content form: a UTF-8 static template read from the structurer's static
297
+ * templates. Use inside `fixed` / `managed` / `scaffold` / `seed`. */
298
+ export function file(path: string): ContentForm {
299
+ return { kind: "file", path };
300
+ }
301
+
302
+ /** A binary static asset (e.g. a logo) read verbatim from `<root>/static/`.
303
+ * Unlike `file()`, the bytes are never decoded to a string — use for
304
+ * images / archives. Only valid inside `seed()` / `scaffold()`. */
305
+ export function binaryFile(path: string): ContentForm {
306
+ return { kind: "binary", path };
307
+ }
308
+
309
+ /** Content form: an inline literal string. */
310
+ export function text(value: string): ContentForm {
311
+ return { kind: "text", value };
312
+ }
313
+
314
+ /** Content form: a text template (the structurer's `text/` templates) with
315
+ * `${var}` placeholders substituted from `vars`. */
316
+ export function tpl(
317
+ path: string,
318
+ vars: Record<string, string> | (() => Record<string, string>),
319
+ ): ContentForm {
320
+ return { kind: "tpl", path, vars };
321
+ }
322
+
323
+ /** Content form: content computed at run time. The return value is serialised
324
+ * as JSON / YAML by the target file's extension, or used verbatim if it is
325
+ * already a string. */
326
+ export function generate(fn: () => unknown): ContentForm {
327
+ return { kind: "generate", fn };
328
+ }
329
+
330
+ /** The active run's {@link BlockVars} (block name parts + software platform).
331
+ * Valid only during a run (inside a generator or managed body); throws
332
+ * otherwise. */
333
+ export function blockVars(): BlockVars {
334
+ if (!activeRunContext) {
335
+ throw new Error(
336
+ "blockVars() called outside engine.run(). " +
337
+ "Generators and managed bodies only see BlockVars during a run.",
338
+ );
339
+ }
340
+ return activeRunContext.blockVars;
341
+ }