@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,60 @@
1
+ // `.structure` metadata file — read/write + floor check.
2
+ //
3
+ // Single JSON file at the block root holding the layout-version
4
+ // integer and (in future) any other structurer metadata. Two
5
+ // constants govern lifecycle gates:
6
+ // - STRUCTURE_VERSION : latest known. Initial value 1.
7
+ // - STRUCTURE_MIN_SUPPORTED : floor. Initial value 0.
8
+ // Both are bumped by structurer releases; rule predicates may consult
9
+ // `ctx.version` for one-shot version-gated transitions.
10
+
11
+ import type { FileSystem } from "./fs/api";
12
+
13
+ export const STRUCTURE_VERSION = 1;
14
+ export const STRUCTURE_MIN_SUPPORTED = 0;
15
+ /** Block-relative path of the structurer metadata file. */
16
+ export const STRUCTURE_META_FILE = ".structure";
17
+
18
+ export type StructureMeta = { version: number };
19
+
20
+ /** Read `.structure`. Missing file or invalid payload → 0. */
21
+ export function readStructureVersion(fs: FileSystem): number {
22
+ if (!fs.exists(STRUCTURE_META_FILE)) return 0;
23
+ try {
24
+ const raw = fs.read(STRUCTURE_META_FILE);
25
+ const parsed = JSON.parse(raw) as Partial<StructureMeta>;
26
+ if (typeof parsed?.version === "number") return parsed.version;
27
+ return 0;
28
+ } catch {
29
+ return 0;
30
+ }
31
+ }
32
+
33
+ /** Throw if version is below floor. */
34
+ export function assertVersionAboveFloor(
35
+ version: number,
36
+ floor: number = STRUCTURE_MIN_SUPPORTED,
37
+ ): void {
38
+ if (version < floor) {
39
+ throw new StructureVersionFloorError(version, floor);
40
+ }
41
+ }
42
+
43
+ export class StructureVersionFloorError extends Error {
44
+ constructor(
45
+ public readonly version: number,
46
+ public readonly floor: number,
47
+ ) {
48
+ super(
49
+ `Block .structure version is ${version}, below floor ${floor}. ` +
50
+ `Bootstrap via the legacy-migration skill, then re-run.`,
51
+ );
52
+ this.name = "StructureVersionFloorError";
53
+ }
54
+ }
55
+
56
+ export function writeStructureVersion(fs: FileSystem, version: number = STRUCTURE_VERSION): void {
57
+ const payload: StructureMeta = { version };
58
+ // Compact, single-line — this is a tiny machine file, no pretty-print.
59
+ fs.write(STRUCTURE_META_FILE, JSON.stringify(payload));
60
+ }
@@ -0,0 +1,164 @@
1
+ // `block-tools structure init` constructor: collects BlockVars from CLI
2
+ // flags (prompting for any missing field unless --non-interactive),
3
+ // assembles `ctx.modules` via the shared `modulesForInit`, and runs the
4
+ // engine in init mode against a NodeFileSystem rooted at the target dir.
5
+ //
6
+ // Replaces the manual `git clone platforma-block-boilerplate` workflow
7
+ // and `pl-dev create-block`. `--platform` is single-valued: a
8
+ // freshly-init'd block has at most one software module.
9
+
10
+ import { confirm, input, select } from "@inquirer/prompts";
11
+ import { NodeFileSystem } from "./engine/fs/node";
12
+ import { NodeTemplateProvider } from "./engine/templates";
13
+ import { run as engineRun } from "./engine/runner";
14
+ import { createRunContext, modulesForInit } from "./engine/ctx";
15
+ import { STRUCTURE } from "./structure-definition";
16
+ import { STRUCTURE_VERSION } from "./engine/version";
17
+ import { matchesBumpPattern, buildRegistryLookupForNames } from "./engine/registry-client";
18
+ import { SDK_CATALOG_PACKAGES } from "./rules/root-pnpm-workspace";
19
+ import type { BlockVars } from "./engine/api";
20
+
21
+ /** Platform choices `init` can scaffold a software module for. Python is
22
+ * the only platform with a real, buildable scaffold at v1; any other
23
+ * value is rejected rather than scaffolded as a non-buildable placeholder,
24
+ * which would defeat the "init must produce a working block" intent.
25
+ * `softwarePlatform` stays in BlockVars for when another platform gets a
26
+ * real scaffold. (Note: Tengo is the workflow language, not a software
27
+ * platform.) */
28
+ export const SUPPORTED_PLATFORMS = ["python"] as const;
29
+
30
+ export type InitFlagValues = {
31
+ npmOrg?: string;
32
+ orgScope?: string;
33
+ shortName?: string;
34
+ /** Tri-state: true (--with-software), false (--no-software),
35
+ * undefined (unspecified → prompt or, non-interactive, default off). */
36
+ withSoftware?: boolean;
37
+ platform?: string;
38
+ nonInteractive: boolean;
39
+ };
40
+
41
+ export type InitInput = {
42
+ /** Resolved BlockVars (from `resolveBlockVars`). */
43
+ vars: BlockVars;
44
+ /** Resolved absolute target directory. */
45
+ blockPath: string;
46
+ /** `<block-tools>/src/structure/templates`. */
47
+ templatesRoot: string;
48
+ log: (msg: string) => void;
49
+ };
50
+
51
+ class InitFlagError extends Error {
52
+ constructor(flag: string) {
53
+ super(
54
+ `Missing required flag --${flag} in --non-interactive mode. ` +
55
+ `Supply every BlockVars flag, or drop --non-interactive to be prompted.`,
56
+ );
57
+ this.name = "InitFlagError";
58
+ }
59
+ }
60
+
61
+ async function need(
62
+ value: string | undefined,
63
+ flag: string,
64
+ nonInteractive: boolean,
65
+ prompt: () => Promise<string>,
66
+ ): Promise<string> {
67
+ if (value !== undefined && value !== "") return value;
68
+ if (nonInteractive) throw new InitFlagError(flag);
69
+ return prompt();
70
+ }
71
+
72
+ /** Resolve the full BlockVars (incl. softwarePlatform) from flags +
73
+ * prompts. */
74
+ export async function resolveBlockVars(flags: InitFlagValues): Promise<BlockVars> {
75
+ const npmOrg = await need(flags.npmOrg, "npm-org", flags.nonInteractive, () =>
76
+ input({ message: "npm org (e.g. @platforma-open):", default: "@platforma-open" }),
77
+ );
78
+ const orgScope = await need(flags.orgScope, "org-scope", flags.nonInteractive, () =>
79
+ input({ message: "org scope (e.g. my-org):" }),
80
+ );
81
+ const shortName = await need(flags.shortName, "short-name", flags.nonInteractive, () =>
82
+ input({ message: "block short name (e.g. mixcr-clonotyping):" }),
83
+ );
84
+
85
+ // Software: explicit flag wins; otherwise prompt (interactive) or
86
+ // default off (non-interactive).
87
+ let hasSoftware: boolean;
88
+ if (flags.withSoftware !== undefined) {
89
+ hasSoftware = flags.withSoftware;
90
+ } else if (flags.nonInteractive) {
91
+ hasSoftware = false;
92
+ } else {
93
+ hasSoftware = await confirm({ message: "Include a software module?", default: false });
94
+ }
95
+
96
+ let softwarePlatform: string | undefined;
97
+ if (hasSoftware) {
98
+ softwarePlatform = await need(flags.platform, "platform", flags.nonInteractive, () =>
99
+ select({
100
+ message: "Software platform:",
101
+ choices: SUPPORTED_PLATFORMS.map((p) => ({ name: p, value: p })),
102
+ }),
103
+ );
104
+ if (!(SUPPORTED_PLATFORMS as readonly string[]).includes(softwarePlatform)) {
105
+ throw new Error(
106
+ `Unsupported --platform '${softwarePlatform}'. ` +
107
+ `Only ${SUPPORTED_PLATFORMS.join(", ")} ${
108
+ SUPPORTED_PLATFORMS.length === 1 ? "is" : "are"
109
+ } supported at v1.`,
110
+ );
111
+ }
112
+ } else if (flags.platform) {
113
+ throw new Error("--platform was given but software is disabled (--no-software).");
114
+ }
115
+
116
+ const facadeName = `${npmOrg}/${orgScope}.${shortName}`;
117
+ return {
118
+ facadeName,
119
+ baseName: `${orgScope}.${shortName}`,
120
+ npmOrg,
121
+ orgScope,
122
+ shortName,
123
+ softwarePlatform,
124
+ };
125
+ }
126
+
127
+ export async function runInit(init: InitInput): Promise<BlockVars> {
128
+ const vars = init.vars;
129
+ const modules = modulesForInit(vars);
130
+
131
+ const fs = new NodeFileSystem(init.blockPath);
132
+ const templates = new NodeTemplateProvider(init.templatesRoot);
133
+ const ctx = createRunContext({
134
+ blockVars: vars,
135
+ modules,
136
+ isSdkInternal: false,
137
+ version: STRUCTURE_VERSION,
138
+ dryRun: false,
139
+ });
140
+
141
+ // Init REQUIRES network: resolve the SDK catalog families to npm latest
142
+ // before the run, so the generated block carries real current versions
143
+ // (no placeholders). The names come from the SDK catalog package list —
144
+ // there is no on-disk pnpm-workspace.yaml to read yet. A registry failure
145
+ // aborts init rather than silently shipping an unresolved placeholder.
146
+ const sdkNames = SDK_CATALOG_PACKAGES.filter(matchesBumpPattern);
147
+ let registryLookup: (name: string) => string | undefined;
148
+ try {
149
+ registryLookup = await buildRegistryLookupForNames(sdkNames);
150
+ } catch (err) {
151
+ throw new Error(
152
+ `init requires network access to resolve SDK catalog versions from npm, ` +
153
+ `but the registry lookup failed: ${err instanceof Error ? err.message : String(err)}`,
154
+ );
155
+ }
156
+
157
+ engineRun(STRUCTURE, fs, ctx, { templates, initMode: true, registryLookup });
158
+ init.log(
159
+ `Initialised block '${vars.facadeName}'${
160
+ vars.softwarePlatform ? ` (software: ${vars.softwarePlatform})` : " (no software)"
161
+ } at ${init.blockPath}`,
162
+ );
163
+ return vars;
164
+ }
@@ -0,0 +1,156 @@
1
+ // Block-level (orchestrator) `package.json`: the initial generator and the
2
+ // drift-correcting body rules, co-located. Workspace-scope deps reflect the
3
+ // discovered modules at run time; the generator pre-computes them and the
4
+ // body rule re-asserts them.
5
+
6
+ import {
7
+ ensureField,
8
+ ensureScript,
9
+ ensureDevDep,
10
+ ensureWorkspaceScopeDeps,
11
+ enforceAlphabeticalOrder,
12
+ enforceFieldOrder,
13
+ type RunContext,
14
+ } from "../engine/api";
15
+ import { getActiveRunContext } from "../engine/builders";
16
+ import { findModules, scopeDepMap } from "../engine/ctx";
17
+ import { canonicalPackageJsonOrder } from "./shared/key-order";
18
+
19
+ /** The facade's `block.components` map: the dist artifact each module build
20
+ * emits, keyed by role. Fully determined by the discovered modules — the
21
+ * body rule re-asserts this exact value, so it is engine-managed (NOT an
22
+ * author seed). Mirrors the artifact paths produced by each scope's build:
23
+ * workflow → pl-tengo, model → build-model, ui → ts-builder. */
24
+ export function blockComponents(ctx: RunContext): Record<string, string> {
25
+ const components: Record<string, string> = {};
26
+ for (const m of findModules(ctx, "workflow"))
27
+ components.workflow = `${m.name}/dist/tengo/tpl/main.plj.gz`;
28
+ for (const m of findModules(ctx, "model")) components.model = `${m.name}/dist/model.json`;
29
+ for (const m of findModules(ctx, "ui")) components.ui = `${m.name}/dist`;
30
+ return components;
31
+ }
32
+
33
+ /** Fail loudly if any required `block.*` field is empty — a missing
34
+ * title / description / logo / organization or empty components map would
35
+ * otherwise produce an init block that `block-tools pack` rejects only
36
+ * later. Asserted at generation time so the failure is immediate. */
37
+ function assertBlockFieldsFilled(block: {
38
+ components: Record<string, string>;
39
+ meta: Record<string, unknown>;
40
+ }): void {
41
+ if (Object.keys(block.components).length === 0) {
42
+ throw new Error("block.components is empty — no workflow/model/ui module was discovered");
43
+ }
44
+ const meta = block.meta as {
45
+ title?: unknown;
46
+ description?: unknown;
47
+ logo?: unknown;
48
+ organization?: { name?: unknown; url?: unknown; logo?: unknown };
49
+ };
50
+ const required: [string, unknown][] = [
51
+ ["block.meta.title", meta.title],
52
+ ["block.meta.description", meta.description],
53
+ ["block.meta.logo", meta.logo],
54
+ ["block.meta.organization.name", meta.organization?.name],
55
+ ["block.meta.organization.url", meta.organization?.url],
56
+ ["block.meta.organization.logo", meta.organization?.logo],
57
+ ];
58
+ for (const [path, value] of required) {
59
+ if (typeof value !== "string" || value === "") {
60
+ throw new Error(`required block field '${path}' is empty`);
61
+ }
62
+ }
63
+ }
64
+
65
+ export function blockPackageJsonInitial(ctx: RunContext): Record<string, unknown> {
66
+ const v = ctx.blockVars;
67
+ // The facade depends on its model / ui / workflow modules — not on its
68
+ // software module (the workflow owns that dep) and not on its test module
69
+ // (the test package depends on the facade, so a reciprocal dep would be a
70
+ // pnpm/turbo cycle).
71
+ const deps: Record<string, string> = {
72
+ ...scopeDepMap(ctx, "model"),
73
+ ...scopeDepMap(ctx, "ui"),
74
+ ...scopeDepMap(ctx, "workflow"),
75
+ };
76
+ // `block.components` is engine-managed (re-asserted by the body rule);
77
+ // `block.meta` is an author SEED — written once at init, never touched on
78
+ // refresh, so the author owns title / description / organization / logo.
79
+ // `block-tools pack` requires both. The author edits the placeholder meta
80
+ // before publishing.
81
+ const block = {
82
+ components: blockComponents(ctx),
83
+ meta: {
84
+ title: v.shortName,
85
+ description: `${v.shortName} block`,
86
+ logo: "file:logos/block-logo.png",
87
+ organization: {
88
+ name: v.orgScope,
89
+ url: "https://example.com",
90
+ logo: "file:logos/organization-logo.png",
91
+ },
92
+ },
93
+ };
94
+ assertBlockFieldsFilled(block);
95
+ return {
96
+ name: `${v.facadeName}.block`,
97
+ version: "1.0.0",
98
+ // type:"module" intentionally omitted.
99
+ files: ["index.d.ts", "index.js"],
100
+ scripts: {
101
+ build: "shx rm -rf ./block-pack && block-tools pack",
102
+ "mark-stable":
103
+ "block-tools mark-stable -r 's3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1'",
104
+ prepublishOnly:
105
+ "block-tools pack && block-tools publish -r 's3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1'",
106
+ "do-pack": "shx rm -f *.tgz && block-tools pack && pnpm pack && shx mv *.tgz package.tgz",
107
+ },
108
+ dependencies: deps,
109
+ devDependencies: { "@platforma-sdk/block-tools": "sdk:", shx: "catalog:" },
110
+ block,
111
+ };
112
+ }
113
+
114
+ export function blockPackageJsonRules(): void {
115
+ // type:"module" intentionally omitted: the facade's index.js is the
116
+ // CommonJS dev-block descriptor (module.exports / __dirname); ESM would
117
+ // break it. The facade has no TS sources to compile.
118
+ ensureField("files", ["index.d.ts", "index.js"]);
119
+
120
+ ensureScript("build", "shx rm -rf ./block-pack && block-tools pack");
121
+ ensureScript(
122
+ "mark-stable",
123
+ "block-tools mark-stable -r 's3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1'",
124
+ );
125
+ ensureScript(
126
+ "prepublishOnly",
127
+ "block-tools pack && block-tools publish -r 's3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1'",
128
+ );
129
+ ensureScript(
130
+ "do-pack",
131
+ "shx rm -f *.tgz && block-tools pack && pnpm pack && shx mv *.tgz package.tgz",
132
+ );
133
+
134
+ // One workspace dep per discovered module in the facade's scopes. The
135
+ // facade depends on model / ui / workflow — not software (the workflow
136
+ // owns it) and not test (a reciprocal dep would be a pnpm/turbo cycle).
137
+ ensureWorkspaceScopeDeps("model");
138
+ ensureWorkspaceScopeDeps("ui");
139
+ ensureWorkspaceScopeDeps("workflow");
140
+
141
+ ensureDevDep("@platforma-sdk/block-tools", "sdk:");
142
+ // shx powers the cross-platform build / do-pack scripts above.
143
+ ensureDevDep("shx", "catalog:");
144
+
145
+ // `block.components` is fully determined by the discovered modules — keep
146
+ // it in sync on refresh. `block.meta` is deliberately NOT touched here: it
147
+ // is an author-owned seed (set once in the init package.json), so refresh
148
+ // must never overwrite the author's title / description / logo.
149
+ ensureField("block.components", blockComponents(getActiveRunContext()));
150
+
151
+ enforceAlphabeticalOrder("dependencies");
152
+ enforceAlphabeticalOrder("devDependencies");
153
+ enforceAlphabeticalOrder("peerDependencies");
154
+ enforceAlphabeticalOrder("optionalDependencies");
155
+ enforceFieldOrder([...canonicalPackageJsonOrder]);
156
+ }
@@ -0,0 +1,29 @@
1
+ // Block-scope rules — the orchestrator package at `block/`.
2
+ // Workspace-scope deps in the body resolve from ctx.modules.
3
+
4
+ import { scope, fixed, managed, seed, file, binaryFile, generate } from "../engine/api";
5
+ import { getActiveRunContext } from "../engine/builders";
6
+ import { blockPackageJsonInitial, blockPackageJsonRules } from "./block-package-json";
7
+
8
+ export function blockRules(): void {
9
+ scope("block", () => {
10
+ fixed("index.js", file("block/index.js"));
11
+ fixed("index.d.ts", file("block/index.d.ts"));
12
+
13
+ // Placeholder logos referenced by `block.meta` (logo + organization.logo).
14
+ // Binary seeds: written once at init, author-owned thereafter — refresh
15
+ // never overwrites them, so a custom logo survives. `block-tools pack`
16
+ // embeds these into the block pack; without them pack would fail on the
17
+ // `file:logos/...` meta references.
18
+ seed("logos/block-logo.png", binaryFile("block/logos/block-logo.png"));
19
+ seed("logos/organization-logo.png", binaryFile("block/logos/organization-logo.png"));
20
+
21
+ managed(
22
+ "package.json",
23
+ generate(() => blockPackageJsonInitial(getActiveRunContext())),
24
+ () => {
25
+ blockPackageJsonRules();
26
+ },
27
+ );
28
+ });
29
+ }
@@ -0,0 +1,63 @@
1
+ // Migrations + legacy cleanup live here.
2
+ //
3
+ // Two patterns coexist:
4
+ //
5
+ // 1. One-shot migrations gated on filesystem state. Folder renames,
6
+ // cross-cutting one-time transforms. Use `when(pathExists/pathMissing)`
7
+ // so the migration fires once and is idempotent on subsequent runs.
8
+ //
9
+ // 2. Legacy cleanup primitives. Unconditional `removeScript`,
10
+ // `removeDep`, `remove(path)` calls naming artefacts the canonical
11
+ // layout never produces. Idempotent by definition (no-op on absent
12
+ // keys/paths). Each line is a one-way decision: every block converges.
13
+
14
+ import { scope, remove, managed, generate, removeScript, removeDep } from "../engine/api";
15
+ import { getActiveRunContext } from "../engine/builders";
16
+ import { rootPackageJsonInitial } from "./root-package-json";
17
+
18
+ export function testFrameworkMigration(): void {
19
+ // No active rename-bearing migration yet. Reserved for the test
20
+ // framework rollout.
21
+ }
22
+
23
+ // Unconditional legacy-cleanup rules. Each is a no-op on an
24
+ // already-canonical block (remove on an absent path, removeScript /
25
+ // removeDep on an absent key) and idempotent, so they sit at top level
26
+ // with no `when` gate. The trailing comment on each line names the real
27
+ // block(s) that carry the artefact being cleaned.
28
+ //
29
+ // Root-scope cleanup is skipped for `--sdk-internal` blocks (no root
30
+ // module in that mode), so it never touches in-monorepo blocks.
31
+ export function legacyCleanup(): void {
32
+ // eslint → oxlint: the canonical layout ships `.oxlintrc.json` /
33
+ // `.oxfmtrc.json`; flat eslint configs are retired.
34
+ scope("model", () => {
35
+ remove("eslint.config.mjs"); // samples-and-data, mixcr-clonotyping, clonotype-clustering, antibody-sequence-liabilities
36
+ remove("vite.config.mts"); // samples-and-data (vite build → ts-builder)
37
+ });
38
+ scope("ui", () => {
39
+ remove("eslint.config.mjs"); // samples-and-data, mixcr-clonotyping, clonotype-clustering, antibody-sequence-liabilities
40
+ remove("vite.config.ts"); // samples-and-data (vite build → ts-builder)
41
+ remove("tsconfig.app.json"); // samples-and-data (vite-era split tsconfig)
42
+ remove("tsconfig.node.json"); // samples-and-data (vite-era split tsconfig)
43
+ });
44
+ scope("test", () => {
45
+ remove("eslint.config.mjs"); // all 5 experiment blocks
46
+ });
47
+
48
+ // Root-scope cleanup. The second `managed("package.json")` body composes
49
+ // after rootRules.
50
+ scope("root", () => {
51
+ remove(".prettierrc"); // samples-and-data, sequence-properties (prettier → oxfmt)
52
+ managed(
53
+ "package.json",
54
+ generate(() => rootPackageJsonInitial(getActiveRunContext())),
55
+ () => {
56
+ // `pretty` (prettier) is superseded by the canonical `fmt` (oxfmt);
57
+ // the standalone deps-updater was merged into block-tools.
58
+ removeScript("pretty"); // samples-and-data
59
+ removeDep("@platforma-sdk/blocks-deps-updater"); // samples-and-data
60
+ },
61
+ );
62
+ });
63
+ }
@@ -0,0 +1,123 @@
1
+ // Model `package.json`: the initial generator and the drift-correcting body
2
+ // rules, co-located. Identity (name/version) stays untouched; the body
3
+ // enforces type/main/types/exports + canonical scripts + canonical dep sets,
4
+ // then projects canonical field / dependency order.
5
+ //
6
+ // Authored oxfmt-clean: the model is one of the two scopes `ts-builder
7
+ // check` runs oxfmt on, so neither half enforces field / dependency ordering
8
+ // beyond what oxfmt itself produces — the enforce* calls match oxfmt's order
9
+ // exactly, so refreshing a legacy block yields oxfmt-clean output and the
10
+ // build→check gate (oxfmt --check) passes without a prior `pnpm fmt`.
11
+
12
+ import {
13
+ ensureField,
14
+ ensureScript,
15
+ ensureDep,
16
+ ensureDevDep,
17
+ ensureDevDeps,
18
+ ensurePeerDeps,
19
+ enforceAlphabeticalOrder,
20
+ enforceFieldOrder,
21
+ when,
22
+ whenFilesExist,
23
+ type RunContext,
24
+ } from "../engine/api";
25
+ import { canonicalPackageJsonOrder } from "./shared/key-order";
26
+ import { COLOCATED_TEST_GLOB } from "./shared/colocated-tests";
27
+
28
+ export function modelPackageJsonInitial(ctx: RunContext): Record<string, unknown> {
29
+ const v = ctx.blockVars;
30
+ return {
31
+ name: `${v.facadeName}.model`,
32
+ version: "1.0.0",
33
+ type: "module",
34
+ // The block-model build emits both index.cjs and index.js. `main` is
35
+ // the CommonJS entry (require fallback); `module` the ESM entry.
36
+ main: "dist/index.cjs",
37
+ module: "dist/index.js",
38
+ types: "dist/index.d.ts",
39
+ exports: {
40
+ ".": {
41
+ types: "./dist/index.d.ts",
42
+ sources: "./src/index.ts",
43
+ import: "./dist/index.js",
44
+ },
45
+ "./dist/*": "./dist/*",
46
+ },
47
+ scripts: {
48
+ fmt: "ts-builder format",
49
+ watch: "ts-builder build --target block-model --watch",
50
+ build: "ts-builder build --target block-model && block-tools build-model",
51
+ check: "ts-builder check --target block-model",
52
+ // No `test`: the vitest `test` script is wired by the body rule ONLY
53
+ // when co-located test files exist (a freshly-init'd model has none).
54
+ },
55
+ dependencies: {
56
+ "@platforma-sdk/model": "sdk:",
57
+ },
58
+ devDependencies: {
59
+ "@milaboratories/ts-builder": "sdk:",
60
+ "@milaboratories/ts-configs": "sdk:",
61
+ "@platforma-sdk/block-tools": "sdk:",
62
+ vitest: "catalog:",
63
+ },
64
+ peerDependencies: {
65
+ "@types/node": "*",
66
+ typescript: "*",
67
+ },
68
+ };
69
+ }
70
+
71
+ export function modelPackageJsonRules(): void {
72
+ ensureField("type", "module");
73
+ // build emits both; main = CJS entry, module = ESM entry (prod convention).
74
+ ensureField("main", "dist/index.cjs");
75
+ ensureField("module", "dist/index.js");
76
+ ensureField("types", "dist/index.d.ts");
77
+ ensureField("exports", {
78
+ ".": {
79
+ types: "./dist/index.d.ts",
80
+ sources: "./src/index.ts",
81
+ import: "./dist/index.js",
82
+ },
83
+ "./dist/*": "./dist/*",
84
+ });
85
+
86
+ ensureScript("fmt", "ts-builder format");
87
+ ensureScript("watch", "ts-builder build --target block-model --watch");
88
+ ensureScript("build", "ts-builder build --target block-model && block-tools build-model");
89
+ ensureScript("check", "ts-builder check --target block-model");
90
+
91
+ ensureDep("@platforma-sdk/model", "sdk:");
92
+
93
+ ensureDevDeps({
94
+ "@milaboratories/ts-builder": "sdk:",
95
+ "@milaboratories/ts-configs": "sdk:",
96
+ "@platforma-sdk/block-tools": "sdk:",
97
+ vitest: "catalog:",
98
+ });
99
+
100
+ ensurePeerDeps({
101
+ "@types/node": "*",
102
+ typescript: "*",
103
+ });
104
+
105
+ // The vitest `test` script + node ambient types are wired ONLY when the
106
+ // model carries co-located unit tests (`src/**/*.test.ts`). A test-less
107
+ // model gets neither — turbo simply has no `test` task for it, and the
108
+ // scope stays a refresh fixpoint. When tests ARE present, `@types/node`
109
+ // is promoted from peer to a devDep (the single-section invariant drops
110
+ // the peer) so the tests' `node:*` imports resolve under the
111
+ // `types: ["node"]` that model/tsconfig adds under the same predicate.
112
+ // Runs AFTER ensurePeerDeps so the promotion is not reverted.
113
+ when(whenFilesExist(COLOCATED_TEST_GLOB), () => {
114
+ ensureScript("test", "vitest run --passWithNoTests");
115
+ ensureDevDep("@types/node", "*");
116
+ });
117
+
118
+ enforceAlphabeticalOrder("dependencies");
119
+ enforceAlphabeticalOrder("devDependencies");
120
+ enforceAlphabeticalOrder("peerDependencies");
121
+ enforceAlphabeticalOrder("optionalDependencies");
122
+ enforceFieldOrder([...canonicalPackageJsonOrder]);
123
+ }
@@ -0,0 +1,65 @@
1
+ // Model-scope rules. Static config files + managed package.json; the
2
+ // `src/index.ts` seed is dropped by `init` and never touched again
3
+ // (block author owns it).
4
+
5
+ import {
6
+ scope,
7
+ fixed,
8
+ managed,
9
+ seed,
10
+ file,
11
+ generate,
12
+ when,
13
+ whenFilesExist,
14
+ ensureField,
15
+ removeField,
16
+ pruneKeysMatching,
17
+ } from "../engine/api";
18
+ import { getActiveRunContext } from "../engine/builders";
19
+ import { modelPackageJsonInitial, modelPackageJsonRules } from "./model-package-json";
20
+ import { COLOCATED_TEST_GLOB } from "./shared/colocated-tests";
21
+
22
+ export function modelRules(): void {
23
+ scope("model", () => {
24
+ // tsconfig is managed (not fixed) so node ambient types can be provided
25
+ // CONDITIONALLY. The body still enforces the canonical shape —
26
+ // `{extends, include}` with NO stray keys — and, ONLY when the model
27
+ // carries co-located unit tests (`src/**/*.test.ts`), adds
28
+ // `compilerOptions.types: ["node"]` so those tests type-check: provide
29
+ // node types, do NOT exclude the tests. A test-less model is
30
+ // canonicalised to bare `{extends, include}`
31
+ // and stays a refresh fixpoint. The matching `@types/node` devDep is
32
+ // wired by the package.json rule under the same predicate.
33
+ managed("tsconfig.json", file("model/tsconfig.json"), () => {
34
+ ensureField("extends", "@milaboratories/ts-configs/block/model");
35
+ // Canonical default carries no compilerOptions; drop any legacy block's
36
+ // before (conditionally) re-adding only `types`.
37
+ removeField("compilerOptions");
38
+ when(whenFilesExist(COLOCATED_TEST_GLOB), () => {
39
+ ensureField("compilerOptions.types", ["node"]);
40
+ });
41
+ ensureField("include", ["src/**/*"]);
42
+ // Strip anything else a legacy tsconfig carried (files, references, a
43
+ // different extends layout) — keep only the canonical keys.
44
+ pruneKeysMatching((k) => k !== "extends" && k !== "include" && k !== "compilerOptions");
45
+ });
46
+ fixed(".oxlintrc.json", file("model/.oxlintrc.json"));
47
+ fixed(".oxfmtrc.json", file("model/.oxfmtrc.json"));
48
+
49
+ // A minimal-but-REAL BlockModelV3: `build-model` rejects a non-model
50
+ // export ("Malformed model object"), and a bare `.create(dataModel)`
51
+ // still errors ("Args rendering function not set"). This seed is the
52
+ // smallest chain that produces a valid `dist/model.json` — empty data
53
+ // model, identity args, a single "Main" section. The block author owns
54
+ // and extends it after init.
55
+ seed("src/index.ts", file("model/src/index.ts"));
56
+
57
+ managed(
58
+ "package.json",
59
+ generate(() => modelPackageJsonInitial(getActiveRunContext())),
60
+ () => {
61
+ modelPackageJsonRules();
62
+ },
63
+ );
64
+ });
65
+ }