@platforma-sdk/block-tools 2.13.0 → 2.14.0

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 (68) hide show
  1. package/dist/cli.js +12 -12
  2. package/dist/cli.js.map +1 -1
  3. package/dist/cli.mjs +727 -648
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/cmd/build-kind-manifest.d.ts +7 -0
  6. package/dist/config-CtXMWNgm.js +3 -0
  7. package/dist/config-CtXMWNgm.js.map +1 -0
  8. package/dist/{config-CiC347sB.mjs → config-Dx45aFJ_.mjs} +792 -456
  9. package/dist/config-Dx45aFJ_.mjs.map +1 -0
  10. package/dist/index.js +1 -1
  11. package/dist/index.mjs +15 -15
  12. package/dist/structure/engine/api.d.ts +1 -1
  13. package/dist/structure/rules/kind-package-json.d.ts +3 -0
  14. package/dist/structure/rules/kind.d.ts +1 -0
  15. package/dist/v2/build_kind_dist.d.ts +25 -0
  16. package/dist/v2/index.d.ts +4 -0
  17. package/dist/v2/kind/resolve-refs.d.ts +63 -0
  18. package/dist/v2/kind/version-match.d.ts +23 -0
  19. package/dist/v2/model/block_description.d.ts +8 -1
  20. package/dist/v2/publish-block.d.ts +23 -0
  21. package/dist/v2/registry/index.d.ts +2 -0
  22. package/dist/v2/registry/kind_resolver.d.ts +121 -0
  23. package/dist/v2/registry/registry.d.ts +23 -0
  24. package/dist/v2/registry/registry_reader.d.ts +34 -1
  25. package/dist/v2/registry/schema_kinds.d.ts +632 -0
  26. package/dist/v2/registry/schema_public.d.ts +72 -0
  27. package/package.json +10 -10
  28. package/src/cli.test.ts +2 -1
  29. package/src/cli.ts +2 -0
  30. package/src/cmd/build-kind-manifest.ts +41 -0
  31. package/src/cmd/publish.ts +10 -1
  32. package/src/structure/__tests__/model-package-json.snapshot.test.ts +1 -0
  33. package/src/structure/engine/api.ts +1 -1
  34. package/src/structure/engine/ctx.ts +1 -0
  35. package/src/structure/engine/discovery.ts +16 -2
  36. package/src/structure/rules/block-package-json.ts +18 -0
  37. package/src/structure/rules/kind-package-json.ts +117 -0
  38. package/src/structure/rules/kind.ts +29 -0
  39. package/src/structure/rules/model-package-json.ts +8 -0
  40. package/src/structure/rules/model.ts +15 -13
  41. package/src/structure/rules/shared/pascal-case.ts +1 -1
  42. package/src/structure/structure-definition.ts +2 -0
  43. package/src/structure/templates/static/kind/.oxfmtrc.json +4 -0
  44. package/src/structure/templates/static/kind/.oxlintrc.json +3 -0
  45. package/src/structure/templates/static/kind/src/index.ts +49 -0
  46. package/src/structure/templates/static/kind/tsconfig.json +10 -0
  47. package/src/structure/templates/text/model/src/index.tpl.ts +18 -0
  48. package/src/v2/build_dist.ts +35 -0
  49. package/src/v2/build_kind_dist.ts +104 -0
  50. package/src/v2/from_pack_v2.test.ts +2 -2
  51. package/src/v2/index.ts +4 -0
  52. package/src/v2/kind/resolve-refs.ts +150 -0
  53. package/src/v2/kind/version-match.test.ts +60 -0
  54. package/src/v2/kind/version-match.ts +55 -0
  55. package/src/v2/model/block_description.ts +15 -4
  56. package/src/v2/publish-block.ts +60 -0
  57. package/src/v2/registry/index.ts +2 -0
  58. package/src/v2/registry/kind_resolver.test.ts +123 -0
  59. package/src/v2/registry/kind_resolver.ts +190 -0
  60. package/src/v2/registry/registry.test.ts +247 -1
  61. package/src/v2/registry/registry.ts +227 -4
  62. package/src/v2/registry/registry_reader.ts +68 -0
  63. package/src/v2/registry/schema_kinds.test.ts +65 -0
  64. package/src/v2/registry/schema_kinds.ts +169 -0
  65. package/dist/config-BmJWc8sR.js +0 -3
  66. package/dist/config-BmJWc8sR.js.map +0 -1
  67. package/dist/config-CiC347sB.mjs.map +0 -1
  68. package/src/structure/templates/static/model/src/index.ts +0 -13
package/src/cli.test.ts CHANGED
@@ -15,8 +15,9 @@ function longNames(cmd: Command): string[] {
15
15
  describe("block-tools CLI structure", () => {
16
16
  const program = buildProgram();
17
17
 
18
- it("exposes the full command surface (11 commands + structure topic)", () => {
18
+ it("exposes the full command surface (12 commands + structure topic)", () => {
19
19
  expect(program.commands.map((c) => c.name()).sort()).toEqual([
20
+ "build-kind-manifest",
20
21
  "build-meta",
21
22
  "build-model",
22
23
  "list-overview-snapshots",
package/src/cli.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Command } from "commander";
2
2
  import { buildMetaCommand } from "./cmd/build-meta";
3
3
  import { buildModelCommand } from "./cmd/build-model";
4
+ import { buildKindManifestCommand } from "./cmd/build-kind-manifest";
4
5
  import { packCommand } from "./cmd/pack";
5
6
  import { publishCommand } from "./cmd/publish";
6
7
  import { refreshRegistryCommand } from "./cmd/refresh-registry";
@@ -25,6 +26,7 @@ export function buildProgram(packageRoot: string = process.cwd()): Command {
25
26
 
26
27
  program.addCommand(buildMetaCommand());
27
28
  program.addCommand(buildModelCommand());
29
+ program.addCommand(buildKindManifestCommand());
28
30
  program.addCommand(packCommand());
29
31
  program.addCommand(publishCommand());
30
32
  program.addCommand(refreshRegistryCommand());
@@ -0,0 +1,41 @@
1
+ import { Command } from "commander";
2
+ import { buildKindDist } from "../v2/build_kind_dist";
3
+
4
+ // @todo: disscuss why we need manifest for kind
5
+
6
+ /**
7
+ * Thin commander wrapper (build-model.ts shape) delegating to the commander-free
8
+ * `buildKindDist` core. Bundles the compiled kind's identity + src source-hash
9
+ * into `dist/manifest.json`.
10
+ */
11
+ export function buildKindManifestCommand(): Command {
12
+ const cmd = new Command("build-kind-manifest").description(
13
+ "Writes the block-kind dist manifest (identity + src source-hash) from a pre-built kind bundle",
14
+ );
15
+
16
+ cmd.option(
17
+ "-i, --modulePath <path>",
18
+ "kind package dir (contains package.json, src/, dist/)",
19
+ ".",
20
+ );
21
+ cmd.option(
22
+ "-s, --srcDir <path>",
23
+ "source dir hashed into sourceHash, relative to modulePath",
24
+ "src",
25
+ );
26
+ cmd.option(
27
+ "-o, --dst <path>",
28
+ "output dir with the compiled bundle, relative to modulePath",
29
+ "dist",
30
+ );
31
+
32
+ cmd.action(async (flags) => {
33
+ await buildKindDist({
34
+ modulePath: flags.modulePath,
35
+ srcDir: flags.srcDir,
36
+ dst: flags.dst,
37
+ });
38
+ });
39
+
40
+ return cmd;
41
+ }
@@ -9,6 +9,7 @@ import {
9
9
  } from "@milaboratories/pl-model-middle-layer";
10
10
  import { storageByUrl } from "../io/storage";
11
11
  import { BlockRegistryV2 } from "../v2/registry/registry";
12
+ import { publishBlock } from "../v2/publish-block";
12
13
  import { buildPublishedCoords, writePublishedCoords } from "../v2/resolve_to_registry";
13
14
  import path from "node:path";
14
15
 
@@ -82,15 +83,23 @@ export function publishCommand(): Command {
82
83
  manifest as BlockPackManifest & Record<string, unknown>,
83
84
  );
84
85
  const manifestRoot = path.dirname(manifestPath);
86
+ // The facade package root holds `package.json` (with the direct kind
87
+ // dependency the kind-first publish reads); the manifest sits one level down
88
+ // in `block-pack/`. `readFacadeKindDependency`/`resolveFacadeKind` need the
89
+ // package root, not the block-pack dir.
90
+ const facadeDir = path.dirname(manifestRoot);
85
91
 
86
92
  logger.info(`Manifest root = ${manifestRoot}`);
93
+ logger.info(`Facade root = ${facadeDir}`);
87
94
 
88
95
  if (flags.versionOverride) manifest = overrideManifestVersion(manifest, flags.versionOverride);
89
96
 
90
97
  const storage = storageByUrl(flags.registry);
91
98
  const registry = new BlockRegistryV2(storage, logger);
92
99
 
93
- await registry.publishPackage(manifest, async (file) =>
100
+ // Kind-first publish: resolve refs -> version-match gate (before any S3
101
+ // write) -> publishKind (if the block declares a kind) -> publishPackage.
102
+ await publishBlock(registry, manifest, facadeDir, async (file) =>
94
103
  Buffer.from(await fs.promises.readFile(path.resolve(manifestRoot, file))),
95
104
  );
96
105
 
@@ -29,6 +29,7 @@ const EXPECTED_MODEL_PACKAGE_JSON = `{
29
29
  "check": "ts-builder check --target block-model"
30
30
  },
31
31
  "dependencies": {
32
+ "@platforma-open/test-org.demo.kind": "workspace:*",
32
33
  "@platforma-sdk/model": "catalog:"
33
34
  },
34
35
  "devDependencies": {
@@ -10,7 +10,7 @@
10
10
 
11
11
  import type { ContentForm, ManagedBody, RunMode, Structure, TriggerFn } from "./ir";
12
12
 
13
- export type Scope = "root" | "block" | "model" | "ui" | "workflow" | "test" | "software";
13
+ export type Scope = "root" | "block" | "model" | "ui" | "workflow" | "test" | "software" | "kind";
14
14
 
15
15
  /** Variables describing a block — populated by `init` or parsed by
16
16
  * `check`/`refresh` from the block's package.json `name`. */
@@ -55,6 +55,7 @@ export function modulesForInit(vars: BlockVars): Module[] {
55
55
  { scope: "root", name: facade, path: "" },
56
56
  { scope: "block", name: `${facade}.block`, path: "block" },
57
57
  { scope: "model", name: `${facade}.model`, path: "model" },
58
+ { scope: "kind", name: `${facade}.kind`, path: "kind" },
58
59
  { scope: "ui", name: `${facade}.ui`, path: "ui" },
59
60
  { scope: "workflow", name: `${facade}.workflow`, path: "workflow" },
60
61
  { scope: "test", name: `${facade}.test`, path: "test" },
@@ -1,4 +1,4 @@
1
- // Workspace-package classification — the seven scope detection rules.
1
+ // Workspace-package classification — the eight scope detection rules.
2
2
  //
3
3
  // Input: the parsed package.json of every workspace member, plus its
4
4
  // block-relative path. Output: `Module[]` (scope + name + path).
@@ -7,7 +7,16 @@
7
7
 
8
8
  import type { Module, Scope } from "./api";
9
9
 
10
- export const ALL_SCOPES: Scope[] = ["root", "block", "model", "ui", "workflow", "test", "software"];
10
+ export const ALL_SCOPES: Scope[] = [
11
+ "root",
12
+ "block",
13
+ "model",
14
+ "ui",
15
+ "workflow",
16
+ "test",
17
+ "software",
18
+ "kind",
19
+ ];
11
20
 
12
21
  export type WorkspacePackage = {
13
22
  /** Block-relative path of the package — "" or "." means the
@@ -96,6 +105,11 @@ export function classifyOne(wp: WorkspacePackage): Scope | undefined {
96
105
 
97
106
  const deps = depKeys(wp.pkg);
98
107
 
108
+ // kind — @platforma-sdk/block-kind is unique to kind packages (no other scope
109
+ // depends on it). MUST precede the model rule: a kind sets `main`, and were it
110
+ // to carry any model-ish signal it would otherwise misclassify.
111
+ if (deps.has("@platforma-sdk/block-kind")) return "kind";
112
+
99
113
  // 1. software — top-level `block-software` field OR
100
114
  // (package-builder AND any @platforma-open/*runenv* dep).
101
115
  if (wp.pkg["block-software"] !== undefined) return "software";
@@ -153,6 +153,11 @@ export function blockPackageJsonInitial(ctx: RunContext): Record<string, unknown
153
153
  ...scopeDepMaps(ctx, "model"),
154
154
  ...scopeDepMaps(ctx, "ui"),
155
155
  ...scopeDepMaps(ctx, "workflow"),
156
+ // The kind is a fourth block component the facade depends on directly (the
157
+ // tetrad); like its siblings it is a build-time workspace devDep, never a
158
+ // runtime dependency, and it is NOT a bundled `block.component` (its content
159
+ // publishes to the `kinds/` registry tree, not into block-pack).
160
+ ...scopeDepMaps(ctx, "kind"),
156
161
  };
157
162
  // `block-tools pack` requires both `block.components` and `block.meta`. The
158
163
  // author edits the placeholder meta before publishing.
@@ -194,6 +199,18 @@ export function blockPackageJsonInitial(ctx: RunContext): Record<string, unknown
194
199
  export function blockPackageJsonRules(ctx: RunContext): void {
195
200
  const v = ctx.blockVars;
196
201
 
202
+ // The kind is a MANDATORY block component: every block must declare exactly
203
+ // one sibling `kind/` package. Fail loudly otherwise — a kind-less block is
204
+ // invalid, and this is what surfaces the not-yet-migrated blocks.
205
+ const kindModules = findModules(ctx, "kind");
206
+ if (kindModules.length !== 1) {
207
+ throw new Error(
208
+ kindModules.length === 0
209
+ ? `block '${v.facadeName}' declares no kind — every block must have a sibling kind/ package`
210
+ : `block '${v.facadeName}' declares ${kindModules.length} kinds; exactly one is required`,
211
+ );
212
+ }
213
+
197
214
  ensureField("type", "module");
198
215
  ensureField("files", ["dist", "block-pack"]);
199
216
  ensureField("main", "./dist/index.js");
@@ -212,6 +229,7 @@ export function blockPackageJsonRules(ctx: RunContext): void {
212
229
  ensureWorkspaceScopeDevDeps("model");
213
230
  ensureWorkspaceScopeDevDeps("ui");
214
231
  ensureWorkspaceScopeDevDeps("workflow");
232
+ ensureWorkspaceScopeDevDeps("kind");
215
233
  ensureDevDep("@milaboratories/ts-builder", "sdk:");
216
234
  ensureDevDep("@milaboratories/ts-configs", "sdk:");
217
235
  ensureDevDep("@platforma-sdk/block-tools", "sdk:");
@@ -0,0 +1,117 @@
1
+ // Kind `package.json`: the initial generator and the drift-correcting body,
2
+ // co-located. A kind is a private workspace sibling (like the model): its
3
+ // content ships to the `kinds/` registry tree via `build-kind-manifest`, not to
4
+ // npm. Identity (name/version) stays untouched; the body enforces
5
+ // type/main/exports + canonical scripts + canonical dep sets, then projects
6
+ // canonical field / dependency order (oxfmt-clean, so build->check passes with
7
+ // no prior `pnpm fmt`).
8
+ //
9
+ // A kind builds TWICE, like the model: `dist/index.{js,cjs}` with dependencies
10
+ // external — what a block imports — and the self-contained `dist/kind.js` that
11
+ // the registry publishes and `build-kind-manifest` hashes. The `exports` map
12
+ // points at the externalized pair only, so a kind and the block implementing it
13
+ // share one copy of any dependency they both use; the self-contained bundle is
14
+ // read off disk by the manifest builder and needs no export.
15
+ //
16
+ // All three of `import` / `require` / `default` are spelled out: `block-tools
17
+ // build-model` loads the model via `require()`, and the model `require`s its
18
+ // kind, so the CJS arm has to resolve to real CJS.
19
+
20
+ import {
21
+ ensureField,
22
+ ensureScript,
23
+ ensureDep,
24
+ ensureDevDeps,
25
+ ensurePeerDeps,
26
+ removeScript,
27
+ enforceAlphabeticalOrder,
28
+ enforceFieldOrder,
29
+ type RunContext,
30
+ } from "../engine/api";
31
+ import { canonicalPackageJsonOrder } from "./shared/key-order";
32
+
33
+ const KIND_EXPORTS = {
34
+ ".": {
35
+ types: "./dist/index.d.ts",
36
+ sources: "./src/index.ts",
37
+ import: "./dist/index.js",
38
+ require: "./dist/index.cjs",
39
+ default: "./dist/index.js",
40
+ },
41
+ };
42
+
43
+ export function kindPackageJsonInitial(ctx: RunContext): Record<string, unknown> {
44
+ const v = ctx.blockVars;
45
+ return {
46
+ name: `${v.facadeName}.kind`,
47
+ private: true,
48
+ type: "module",
49
+ // `main` is the CommonJS entry (require fallback), `module` the ESM one —
50
+ // the model's convention. `dist/kind.js`, the self-contained bundle, is
51
+ // deliberately not an entry: it exists for the registry, not for importers.
52
+ main: "./dist/index.cjs",
53
+ module: "./dist/index.js",
54
+ types: "./dist/index.d.ts",
55
+ exports: KIND_EXPORTS,
56
+ scripts: {
57
+ fmt: "ts-builder format",
58
+ watch: "ts-builder build --target block-kind --watch",
59
+ build: "ts-builder build --target block-kind && block-tools build-kind-manifest",
60
+ check: "ts-builder check --target block-kind",
61
+ },
62
+ // A kind ships a runtime params check, so it is not a types-only package — but the
63
+ // check is plain TypeScript, held to the contract by its return type, so nothing but
64
+ // the SDK is needed to write one. An author whose params shape earns a schema library
65
+ // adds it here themselves; seeding one would make every kind carry it to be deleted.
66
+ dependencies: {
67
+ "@platforma-sdk/block-kind": "sdk:",
68
+ },
69
+ devDependencies: {
70
+ "@milaboratories/ts-builder": "sdk:",
71
+ "@milaboratories/ts-configs": "sdk:",
72
+ "@platforma-sdk/block-tools": "sdk:",
73
+ },
74
+ peerDependencies: {
75
+ "@types/node": "*",
76
+ typescript: "*",
77
+ },
78
+ };
79
+ }
80
+
81
+ export function kindPackageJsonRules(): void {
82
+ // Controlled sibling — workspace-only, never published to npm. `private: true`
83
+ // makes npm refuse to publish it; the `version` is kept (changesets-owned) and
84
+ // is what the kind's on-wire `{name}@{version}` reference is baked from.
85
+ ensureField("private", true);
86
+
87
+ ensureField("type", "module");
88
+ ensureField("main", "./dist/index.cjs");
89
+ ensureField("module", "./dist/index.js");
90
+ ensureField("types", "./dist/index.d.ts");
91
+ ensureField("exports", KIND_EXPORTS);
92
+
93
+ ensureScript("fmt", "ts-builder format");
94
+ ensureScript("watch", "ts-builder build --target block-kind --watch");
95
+ ensureScript("build", "ts-builder build --target block-kind && block-tools build-kind-manifest");
96
+ ensureScript("check", "ts-builder check --target block-kind");
97
+ removeScript("lint");
98
+
99
+ ensureDep("@platforma-sdk/block-kind", "sdk:");
100
+
101
+ ensureDevDeps({
102
+ "@milaboratories/ts-builder": "sdk:",
103
+ "@milaboratories/ts-configs": "sdk:",
104
+ "@platforma-sdk/block-tools": "sdk:",
105
+ });
106
+
107
+ ensurePeerDeps({
108
+ "@types/node": "*",
109
+ typescript: "*",
110
+ });
111
+
112
+ enforceAlphabeticalOrder("dependencies");
113
+ enforceAlphabeticalOrder("devDependencies");
114
+ enforceAlphabeticalOrder("peerDependencies");
115
+ enforceAlphabeticalOrder("optionalDependencies");
116
+ enforceFieldOrder([...canonicalPackageJsonOrder]);
117
+ }
@@ -0,0 +1,29 @@
1
+ // Kind-scope rules. Static config files + managed package.json; the
2
+ // `src/index.ts` seed is dropped by `init` and never touched again (the block
3
+ // author owns the BlockParams contract). The seed is a deliberate non-building
4
+ // sentinel — its BlockParams is undefined, so a scaffolded-but-unmigrated block
5
+ // fails to typecheck until the params contract is chosen on purpose.
6
+
7
+ import { scope, fixed, managed, seed, file, generate } from "../engine/api";
8
+ import { kindPackageJsonInitial, kindPackageJsonRules } from "./kind-package-json";
9
+
10
+ export function kindRules(): void {
11
+ scope("kind", () => {
12
+ fixed("tsconfig.json", file("kind/tsconfig.json"));
13
+ fixed(".oxlintrc.json", file("kind/.oxlintrc.json"));
14
+ fixed(".oxfmtrc.json", file("kind/.oxfmtrc.json"));
15
+
16
+ // Minimal-but-real kind: identity imported from the package's own
17
+ // package.json, plus a non-building BlockParams sentinel the block author
18
+ // must resolve (via the migration recipe or by hand) before the block builds.
19
+ seed("src/index.ts", file("kind/src/index.ts"));
20
+
21
+ managed(
22
+ "package.json",
23
+ generate((ctx) => kindPackageJsonInitial(ctx)),
24
+ () => {
25
+ kindPackageJsonRules();
26
+ },
27
+ );
28
+ });
29
+ }
@@ -15,6 +15,7 @@ import {
15
15
  ensureDep,
16
16
  ensureDevDep,
17
17
  ensureDevDeps,
18
+ ensureWorkspaceScopeDeps,
18
19
  removeDep,
19
20
  removeScript,
20
21
  removeField,
@@ -25,6 +26,7 @@ import {
25
26
  whenFilesExist,
26
27
  type RunContext,
27
28
  } from "../engine/api";
29
+ import { scopeDepMaps } from "../engine/ctx";
28
30
  import { canonicalPackageJsonOrder } from "./shared/key-order";
29
31
  import { COLOCATED_TEST_GLOB } from "./shared/colocated-tests";
30
32
  import { removeRetiredToolchainDeps } from "./shared/retired-deps";
@@ -56,6 +58,9 @@ export function modelPackageJsonInitial(ctx: RunContext): Record<string, unknown
56
58
  },
57
59
  dependencies: {
58
60
  "@platforma-sdk/model": "sdk:",
61
+ // The model imports its block's kind (to bake the kind reference into
62
+ // model.json), so it depends on the sibling kind package directly.
63
+ ...scopeDepMaps(ctx, "kind"),
59
64
  },
60
65
  devDependencies: {
61
66
  "@milaboratories/ts-builder": "sdk:",
@@ -95,6 +100,9 @@ export function modelPackageJsonRules(): void {
95
100
  ensureScript("check", "ts-builder check --target block-model");
96
101
 
97
102
  ensureDep("@platforma-sdk/model", "sdk:");
103
+ // The model imports its block's kind to bake the kind reference — direct
104
+ // dependency on the sibling kind package.
105
+ ensureWorkspaceScopeDeps("kind");
98
106
 
99
107
  ensureDevDeps({
100
108
  "@milaboratories/ts-builder": "sdk:",
@@ -2,17 +2,22 @@
2
2
  // `src/index.ts` seed is dropped by `init` and never touched again
3
3
  // (block author owns it).
4
4
 
5
- import { scope, fixed, managed, seed, file, generate, when, whenFilesExist } from "../engine/api";
5
+ import {
6
+ scope,
7
+ fixed,
8
+ managed,
9
+ seed,
10
+ file,
11
+ tpl,
12
+ generate,
13
+ when,
14
+ whenFilesExist,
15
+ } from "../engine/api";
6
16
  import { modelPackageJsonInitial, modelPackageJsonRules } from "./model-package-json";
7
17
  import { COLOCATED_TEST_GLOB } from "./shared/colocated-tests";
8
18
 
9
19
  export function modelRules(): void {
10
20
  scope("model", () => {
11
- // Two static end states by co-located-test presence: a test-bearing model
12
- // gets node ambient types (`.node.json`) so the tests type-check —
13
- // `@types/node` is wired alongside by the package.json rule; a test-less
14
- // model stays bare. `fixed` (engine-owned, whole-file overwrite) not
15
- // `managed`, so refresh is idempotent by construction — no key-order drift.
16
21
  when(
17
22
  whenFilesExist(COLOCATED_TEST_GLOB),
18
23
  () => fixed("tsconfig.json", file("model/tsconfig.node.json")),
@@ -21,13 +26,10 @@ export function modelRules(): void {
21
26
  fixed(".oxlintrc.json", file("model/.oxlintrc.json"));
22
27
  fixed(".oxfmtrc.json", file("model/.oxfmtrc.json"));
23
28
 
24
- // A minimal-but-REAL BlockModelV3: `build-model` rejects a non-model
25
- // export ("Malformed model object"), and a bare `.create(dataModel)`
26
- // still errors ("Args rendering function not set"). This seed is the
27
- // smallest chain that produces a valid `dist/model.json` — empty data
28
- // model, identity args, a single "Main" section. The block author owns
29
- // and extends it after init.
30
- seed("src/index.ts", file("model/src/index.ts"));
29
+ seed(
30
+ "src/index.ts",
31
+ tpl("model/src/index.tpl.ts", (ctx) => ({ kindPkg: `${ctx.blockVars.facadeName}.kind` })),
32
+ );
31
33
 
32
34
  managed(
33
35
  "package.json",
@@ -1,7 +1,7 @@
1
1
  // `pascalCase(shortName)` → the block-named alias prefix the facade entry
2
2
  // template emits (`<PascalName>BlockContract`, …). Splits on any run of
3
3
  // non-alphanumeric characters and capitalises each segment's first char:
4
- // test-sum-numbers-v3 → TestSumNumbersV3
4
+ // test-sum-numbers → TestSumNumbersV3
5
5
  // mixcr-clonotyping-2 → MixcrClonotyping2
6
6
  // samples-and-data → SamplesAndData
7
7
  export function pascalCase(s: string): string {
@@ -8,6 +8,7 @@ import { rootRules } from "./rules/root";
8
8
  import { rootCiRules } from "./rules/root-ci";
9
9
  import { blockRules } from "./rules/block";
10
10
  import { modelRules } from "./rules/model";
11
+ import { kindRules } from "./rules/kind";
11
12
  import { uiRules } from "./rules/ui";
12
13
  import { workflowRules } from "./rules/workflow";
13
14
  import { testRules } from "./rules/test";
@@ -20,6 +21,7 @@ export const STRUCTURE = defineStructure(() => {
20
21
  rootCiRules();
21
22
  blockRules();
22
23
  modelRules();
24
+ kindRules();
23
25
  uiRules();
24
26
  workflowRules();
25
27
  testRules();
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": ["node_modules/@milaboratories/ts-builder/configs/oxfmt.json"],
3
+ "ignorePatterns": ["dist", "coverage", "CHANGELOG.md"]
4
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": ["node_modules/@milaboratories/ts-builder/dist/configs/oxlint-node.json"]
3
+ }
@@ -0,0 +1,49 @@
1
+ import { assertParamsObject, defineBlockKind } from "@platforma-sdk/block-kind";
2
+ import { name, version } from "../package.json" with { type: "json" };
3
+
4
+ /**
5
+ * This block's init-params contract — the shape a block of this kind receives
6
+ * at creation, and exactly what a project template serializes for it.
7
+ *
8
+ * TODO(block-kind): replace `NEEDS_BLOCK_PARAMS` with the real params shape, then
9
+ * wire the model's `init(({ params }) => …)` to consume them. If this block takes
10
+ * no author-supplied params, set it to `Record<string, never>` deliberately.
11
+ *
12
+ * This is an intentional sentinel: `NEEDS_BLOCK_PARAMS` is an undefined type, so
13
+ * the block fails to typecheck (TS2304) until the contract is chosen on purpose.
14
+ * A scaffolded-but-unmigrated block must never compile with an empty contract by
15
+ * default — see the block-kind migration recipe in the `block-dev` skill.
16
+ */
17
+ export type BlockParams = NEEDS_BLOCK_PARAMS;
18
+
19
+ /**
20
+ * The same contract at runtime, for params that arrive from a template file rather than
21
+ * from typed code — the only point that can catch a hand-written entry being wrong.
22
+ *
23
+ * TODO(block-kind): read each key `BlockParams` declares and say what it must be, then
24
+ * return them. Plain TypeScript is the default here: a kind owes no schema library, and a
25
+ * check written by hand is held to the contract by the return type. Reach for a validation
26
+ * library only where the shape earns it, and add it to this package's dependencies yourself.
27
+ *
28
+ * Check the fields the contract requires, and stop there. A key the contract does not name
29
+ * needs no rejection: it is dropped by not being read.
30
+ *
31
+ * This is a second intentional sentinel. The function has to return `BlockParams`, so
32
+ * `return {}` stops compiling the moment the contract declares a required field — the check
33
+ * cannot drift from the contract by being left behind. Never satisfy it with a cast: `value
34
+ * as BlockParams` compiles today and checks nothing forever.
35
+ */
36
+ function parseInitializationParams(value: unknown): BlockParams {
37
+ assertParamsObject(value);
38
+
39
+ return {};
40
+ }
41
+
42
+ // Identity (`name`/`version`) comes from this package's own `package.json`, so
43
+ // the on-wire `{name}@{version}` reference can never drift from what npm
44
+ // publishes; the bundler inlines the JSON import.
45
+ export const kind = defineBlockKind<BlockParams>({
46
+ name,
47
+ version,
48
+ parseInitializationParams,
49
+ });
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "@milaboratories/ts-configs/block/facade",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": ".",
6
+ "resolveJsonModule": true
7
+ },
8
+ "include": ["src/**/*", "package.json"],
9
+ "exclude": ["dist", "node_modules"]
10
+ }
@@ -0,0 +1,18 @@
1
+ import type { InferOutputsType } from "@platforma-sdk/model";
2
+ import { BlockModelV3, DataModelBuilder } from "@platforma-sdk/model";
3
+ import type { BlockParams } from "${kindPkg}";
4
+ import { kind } from "${kindPkg}";
5
+
6
+ export type BlockData = BlockParams;
7
+
8
+ const dataModel = new DataModelBuilder({ kind })
9
+ .from<BlockData>("v1")
10
+ .init(({ params }) => params ?? {});
11
+
12
+ export const platforma = BlockModelV3.create({ dataModel, kind })
13
+ .args(() => ({}))
14
+ .templateParams((data) => ({}))
15
+ .sections(() => [{ type: "link", href: "/", label: "Main" }])
16
+ .done();
17
+
18
+ export type BlockOutputs = InferOutputsType<typeof platforma>;
@@ -3,6 +3,7 @@ import type {
3
3
  ManifestFileInfo,
4
4
  } from "@milaboratories/pl-model-middle-layer";
5
5
  import { BlockPackManifest, BlockPackManifestFile } from "@milaboratories/pl-model-middle-layer";
6
+ import type { BlockKindReference } from "@milaboratories/pl-model-common";
6
7
  import type { CompiledTemplateV3 } from "@milaboratories/pl-model-backend";
7
8
  import type { BlockPackDescriptionAbsolute } from "./model";
8
9
  import { consolidateBlockPackDescription } from "./model";
@@ -41,6 +42,33 @@ async function workflowRequiredCapabilities(
41
42
  return pack.template.requiredCapabilities;
42
43
  }
43
44
 
45
+ /**
46
+ * Returns the block-kind reference the model config declares (baked at the
47
+ * container level by `BlockModelV3.done()`), or `undefined` for kind-less
48
+ * blocks or malformed model files — fail-safe, mirroring
49
+ * {@link workflowRequiredCapabilities}.
50
+ *
51
+ * Note: `build-model` writes `JSON.stringify(config)` where `config` is the
52
+ * `BlockConfigContainer` itself, so `kind` sits at the top level of
53
+ * `model.json` (not nested under a `config` key).
54
+ */
55
+ async function modelKindReference(
56
+ descriptionRelative: BlockPackDescriptionManifest,
57
+ dst: string,
58
+ ): Promise<BlockKindReference | undefined> {
59
+ // After consolidateBlockPackDescription runs, components.model is always a
60
+ // `{type: "relative", path: ...}` reference into `dst`.
61
+ const model = descriptionRelative.components.model;
62
+ try {
63
+ const cfg = JSON.parse(await fsp.readFile(path.resolve(dst, model.path), "utf-8")) as {
64
+ kind?: BlockKindReference;
65
+ };
66
+ return cfg.kind ?? undefined;
67
+ } catch {
68
+ return undefined;
69
+ }
70
+ }
71
+
44
72
  export async function buildBlockPackDist(
45
73
  description: BlockPackDescriptionAbsolute,
46
74
  dst: string,
@@ -71,6 +99,13 @@ export async function buildBlockPackDist(
71
99
  };
72
100
  }
73
101
 
102
+ // Lift the model's container-level block-kind reference onto the manifest
103
+ // description (top-level, like featureFlags), so the published manifest
104
+ // advertises which kind the block implements. Fail-safe undefined leaves
105
+ // kind-less blocks unprojected.
106
+ const kindRef = await modelKindReference(descriptionRelative, dst);
107
+ if (kindRef) descriptionRelative.kind = kindRef;
108
+
74
109
  const filesForManifest = await Promise.all(
75
110
  files.map(async (f): Promise<ManifestFileInfo> => {
76
111
  const bytes = await fsp.readFile(path.resolve(dst, f));