@platforma-sdk/block-tools 2.11.2 → 2.11.4

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 (89) hide show
  1. package/bin/run.js +9 -5
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.js +13 -13
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cli.mjs +845 -981
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/cmd/build-meta.d.ts +2 -9
  8. package/dist/cmd/build-model.d.ts +2 -10
  9. package/dist/cmd/list-overview-snapshots.d.ts +2 -9
  10. package/dist/cmd/mark-stable.d.ts +2 -13
  11. package/dist/cmd/pack.d.ts +2 -9
  12. package/dist/cmd/publish.d.ts +2 -12
  13. package/dist/cmd/refresh-registry.d.ts +2 -9
  14. package/dist/cmd/restore-overview-from-snapshot.d.ts +2 -10
  15. package/dist/cmd/structure/check.d.ts +2 -10
  16. package/dist/cmd/structure/init.d.ts +2 -18
  17. package/dist/cmd/structure/refresh.d.ts +10 -11
  18. package/dist/cmd/update-deps.d.ts +2 -6
  19. package/dist/cmd/upload-package-v1.d.ts +2 -14
  20. package/dist/config-BmJWc8sR.js +3 -0
  21. package/dist/config-BmJWc8sR.js.map +1 -0
  22. package/dist/{config-DpfTzcgG.mjs → config-CiC347sB.mjs} +453 -398
  23. package/dist/config-CiC347sB.mjs.map +1 -0
  24. package/dist/index.js +1 -1
  25. package/dist/index.mjs +15 -15
  26. package/dist/registry_v1/config_schema.d.ts +7 -7
  27. package/dist/registry_v1/flags.d.ts +2 -4
  28. package/dist/structure/cli/run-structure.d.ts +1 -1
  29. package/dist/structure/engine/discovery.d.ts +2 -3
  30. package/dist/structure/rules/block-package-json.d.ts +3 -3
  31. package/dist/structure/rules/shared/pascal-case.d.ts +1 -0
  32. package/dist/v2/index.d.ts +1 -0
  33. package/dist/v2/model/block_components.d.ts +11 -0
  34. package/dist/v2/model/block_meta.d.ts +7 -0
  35. package/dist/v2/resolve_to_registry.d.ts +37 -0
  36. package/dist/v2/source_package.d.ts +12 -0
  37. package/package.json +9 -24
  38. package/src/cli.test.ts +56 -0
  39. package/src/cli.ts +50 -0
  40. package/src/cmd/build-meta.ts +11 -22
  41. package/src/cmd/build-model.ts +16 -33
  42. package/src/cmd/list-overview-snapshots.ts +24 -28
  43. package/src/cmd/mark-stable.ts +35 -55
  44. package/src/cmd/pack.ts +11 -22
  45. package/src/cmd/publish.test.ts +35 -0
  46. package/src/cmd/publish.ts +55 -49
  47. package/src/cmd/refresh-registry.ts +22 -26
  48. package/src/cmd/restore-overview-from-snapshot.ts +24 -34
  49. package/src/cmd/structure/check.ts +19 -28
  50. package/src/cmd/structure/init.ts +40 -47
  51. package/src/cmd/structure/refresh.ts +45 -36
  52. package/src/cmd/update-deps.ts +22 -12
  53. package/src/cmd/upload-package-v1.ts +42 -60
  54. package/src/registry_v1/flags.ts +7 -7
  55. package/src/structure/__tests__/discovery-shortname.test.ts +3 -0
  56. package/src/structure/__tests__/facade-end-state.test.ts +195 -0
  57. package/src/structure/__tests__/model-package-json.snapshot.test.ts +1 -1
  58. package/src/structure/cli/run-structure.ts +2 -2
  59. package/src/structure/engine/__tests__/discovery.test.ts +22 -26
  60. package/src/structure/engine/discovery.ts +12 -30
  61. package/src/structure/rules/block-package-json.ts +146 -64
  62. package/src/structure/rules/block.ts +40 -5
  63. package/src/structure/rules/model-package-json.ts +6 -1
  64. package/src/structure/rules/shared/pascal-case.ts +16 -0
  65. package/src/structure/rules/software-package-json.ts +6 -1
  66. package/src/structure/rules/test-package-json.ts +5 -1
  67. package/src/structure/rules/ui-package-json.ts +5 -1
  68. package/src/structure/rules/workflow-package-json.ts +5 -1
  69. package/src/structure/templates/static/block/src/AGENTS.ts +5 -0
  70. package/src/structure/templates/static/block/src/agents-extra.ts +8 -0
  71. package/src/structure/templates/static/block/src/block-extra.ts +9 -0
  72. package/src/structure/templates/static/block/tsconfig.json +4 -0
  73. package/src/structure/templates/text/block/src/index.tpl.ts +55 -0
  74. package/src/v2/from_pack_v2.test.ts +142 -0
  75. package/src/v2/index.ts +1 -0
  76. package/src/v2/model/block_components.ts +33 -0
  77. package/src/v2/model/block_meta.ts +18 -0
  78. package/src/v2/model/content_conversion.ts +0 -1
  79. package/src/v2/resolve_to_registry.ts +84 -0
  80. package/src/v2/source_package.ts +34 -1
  81. package/bin/dev.cmd +0 -3
  82. package/bin/dev.js +0 -6
  83. package/dist/cmd/index.d.ts +0 -28
  84. package/dist/config-DpfTzcgG.mjs.map +0 -1
  85. package/dist/config-xxVy3IQR.js +0 -3
  86. package/dist/config-xxVy3IQR.js.map +0 -1
  87. package/src/cmd/index.ts +0 -31
  88. package/src/structure/templates/static/block/index.d.ts +0 -6
  89. package/src/structure/templates/static/block/index.js +0 -8
@@ -1,5 +1,5 @@
1
- // Workspace-package classification — covers all 7 detection rules
2
- // plus the block-scope override and the unclassified-error path.
1
+ // Workspace-package classification — covers the detection rules plus the
2
+ // block-scope override and the unclassified-error path.
3
3
 
4
4
  import { describe, test, expect } from "vitest";
5
5
  import { classifyOne, discoverModules, DiscoveryError, type WorkspacePackage } from "../discovery";
@@ -14,7 +14,7 @@ describe("discovery — single-package classification", () => {
14
14
  name: "x.software",
15
15
  "block-software": {},
16
16
  });
17
- expect(classifyOne(wp, new Set())).toBe("software");
17
+ expect(classifyOne(wp)).toBe("software");
18
18
  });
19
19
 
20
20
  test("rule 1 alt: software via package-builder + runenv dep", () => {
@@ -25,67 +25,62 @@ describe("discovery — single-package classification", () => {
25
25
  "@platforma-open/runenv-python-3": "catalog:",
26
26
  },
27
27
  });
28
- expect(classifyOne(wp, new Set())).toBe("software");
28
+ expect(classifyOne(wp)).toBe("software");
29
29
  });
30
30
 
31
- test("rule 2: workflow via workflow-tengo dep", () => {
31
+ test("workflow via workflow-tengo dep", () => {
32
32
  const wp = pkg("workflow", {
33
33
  name: "x.workflow",
34
34
  dependencies: { "@platforma-sdk/workflow-tengo": "catalog:" },
35
35
  });
36
- expect(classifyOne(wp, new Set())).toBe("workflow");
36
+ expect(classifyOne(wp)).toBe("workflow");
37
37
  });
38
38
 
39
- test("rule 3: ui via ui-vue dep and no main", () => {
39
+ test("ui via ui-vue dep and no main", () => {
40
40
  const wp = pkg("ui", {
41
41
  name: "x.ui",
42
42
  dependencies: { "@platforma-sdk/ui-vue": "catalog:" },
43
43
  });
44
- expect(classifyOne(wp, new Set())).toBe("ui");
44
+ expect(classifyOne(wp)).toBe("ui");
45
45
  });
46
46
 
47
- test("rule 4: block via block-tools + sibling model/ui/workflow", () => {
48
- const siblings = new Set(["x.model", "x.ui", "x.workflow"]);
47
+ test("block via the top-level `block` field", () => {
49
48
  const wp = pkg("block", {
50
49
  name: "x.block",
51
- dependencies: {
52
- "@platforma-sdk/block-tools": "catalog:",
53
- "x.model": "workspace:*",
54
- "x.ui": "workspace:*",
55
- },
50
+ block: { components: {}, meta: {} },
56
51
  });
57
- expect(classifyOne(wp, siblings)).toBe("block");
52
+ expect(classifyOne(wp)).toBe("block");
58
53
  });
59
54
 
60
- test("rule 4 fallback: files=[index.d.ts,index.js] + block-tools", () => {
55
+ test("a block-tools-ish package WITHOUT a `block` field is not classified `block`", () => {
61
56
  const wp = pkg("block", {
62
57
  name: "x.block",
63
58
  files: ["index.d.ts", "index.js"],
64
59
  dependencies: { "@platforma-sdk/block-tools": "catalog:" },
65
60
  });
66
- expect(classifyOne(wp, new Set())).toBe("block");
61
+ expect(classifyOne(wp)).toBeUndefined();
67
62
  });
68
63
 
69
- test("rule 5: model via model dep + main", () => {
64
+ test("model via model dep + main", () => {
70
65
  const wp = pkg("model", {
71
66
  name: "x.model",
72
67
  main: "dist/index.js",
73
68
  dependencies: { "@platforma-sdk/model": "catalog:" },
74
69
  });
75
- expect(classifyOne(wp, new Set())).toBe("model");
70
+ expect(classifyOne(wp)).toBe("model");
76
71
  });
77
72
 
78
- test("rule 6: test via @platforma-sdk/test dep", () => {
73
+ test("test via @platforma-sdk/test dep", () => {
79
74
  const wp = pkg("test", {
80
75
  name: "x.test",
81
76
  devDependencies: { "@platforma-sdk/test": "catalog:" },
82
77
  });
83
- expect(classifyOne(wp, new Set())).toBe("test");
78
+ expect(classifyOne(wp)).toBe("test");
84
79
  });
85
80
 
86
- test("rule 7: root as catch-all", () => {
81
+ test("root as catch-all", () => {
87
82
  const wp = pkg("", { name: "x" });
88
- expect(classifyOne(wp, new Set())).toBe("root");
83
+ expect(classifyOne(wp)).toBe("root");
89
84
  });
90
85
 
91
86
  test("override: block-scope wins over detection", () => {
@@ -94,7 +89,7 @@ describe("discovery — single-package classification", () => {
94
89
  "block-scope": "software",
95
90
  dependencies: { "@platforma-sdk/workflow-tengo": "catalog:" },
96
91
  });
97
- expect(classifyOne(wp, new Set())).toBe("software");
92
+ expect(classifyOne(wp)).toBe("software");
98
93
  });
99
94
 
100
95
  test("override: bad value throws DiscoveryError", () => {
@@ -102,7 +97,7 @@ describe("discovery — single-package classification", () => {
102
97
  name: "x",
103
98
  "block-scope": "nope",
104
99
  });
105
- expect(() => classifyOne(wp, new Set())).toThrow(DiscoveryError);
100
+ expect(() => classifyOne(wp)).toThrow(DiscoveryError);
106
101
  });
107
102
 
108
103
  test("unclassified non-root throws DiscoveryError", () => {
@@ -134,6 +129,7 @@ describe("discovery — full workspace", () => {
134
129
  }),
135
130
  pkg("block", {
136
131
  name: "x.block",
132
+ block: { components: {}, meta: {} },
137
133
  dependencies: {
138
134
  "@platforma-sdk/block-tools": "catalog:",
139
135
  "x.model": "workspace:*",
@@ -25,6 +25,8 @@ export type PackageJsonLike = {
25
25
  devDependencies?: Record<string, string>;
26
26
  peerDependencies?: Record<string, string>;
27
27
  optionalDependencies?: Record<string, string>;
28
+ // Top-level `block` field — the unambiguous facade marker.
29
+ block?: unknown;
28
30
  // Override field — value MUST be one of the seven scopes.
29
31
  "block-scope"?: string;
30
32
  "block-software"?: unknown;
@@ -61,11 +63,9 @@ function isRoot(p: string): boolean {
61
63
  * Classify one package.
62
64
  *
63
65
  * @param wp The workspace package.
64
- * @param siblingNames Set of all workspace package `name`s (used by
65
- * rule 4 "block": depends on ≥2 sibling .model/.ui/.workflow).
66
66
  * @returns Scope or `undefined` if unclassified.
67
67
  */
68
- export function classifyOne(wp: WorkspacePackage, siblingNames: Set<string>): Scope | undefined {
68
+ export function classifyOne(wp: WorkspacePackage): Scope | undefined {
69
69
  const override = wp.pkg["block-scope"];
70
70
  if (typeof override === "string") {
71
71
  if (!(ALL_SCOPES as string[]).includes(override)) {
@@ -88,6 +88,12 @@ export function classifyOne(wp: WorkspacePackage, siblingNames: Set<string>): Sc
88
88
  // sub-package dirs), so this only ever fires in standalone mode.
89
89
  if (isRoot(wp.path)) return "root";
90
90
 
91
+ // block (facade) — a top-level `block` field is the unambiguous facade
92
+ // marker; no non-facade workspace package carries one. MUST precede the model
93
+ // rule: a migrated facade carries `@platforma-sdk/model` as a devDep plus
94
+ // `main`, which would otherwise match model.
95
+ if (wp.pkg.block !== undefined) return "block";
96
+
91
97
  const deps = depKeys(wp.pkg);
92
98
 
93
99
  // 1. software — top-level `block-software` field OR
@@ -107,30 +113,10 @@ export function classifyOne(wp: WorkspacePackage, siblingNames: Set<string>): Sc
107
113
  return "ui";
108
114
  }
109
115
 
110
- // 4. block — block-tools in deps AND ≥2 sibling .model/.ui/.workflow.
111
- if (deps.has("@platforma-sdk/block-tools")) {
112
- const matches = [...deps].filter(
113
- (d) =>
114
- siblingNames.has(d) &&
115
- (d.endsWith(".model") || d.endsWith(".ui") || d.endsWith(".workflow")),
116
- );
117
- if (matches.length >= 2) return "block";
118
- // Fallback: `files == ["index.d.ts", "index.js"]` + block-tools.
119
- const files = wp.pkg.files;
120
- if (
121
- Array.isArray(files) &&
122
- files.length === 2 &&
123
- files.includes("index.d.ts") &&
124
- files.includes("index.js")
125
- ) {
126
- return "block";
127
- }
128
- }
129
-
130
- // 5. model — @platforma-sdk/model in deps AND main is set.
116
+ // 4. model — @platforma-sdk/model in deps AND main is set.
131
117
  if (deps.has("@platforma-sdk/model") && wp.pkg.main) return "model";
132
118
 
133
- // 6. test — @platforma-sdk/test in deps.
119
+ // 5. test — @platforma-sdk/test in deps.
134
120
  if (deps.has("@platforma-sdk/test")) return "test";
135
121
 
136
122
  // (root — the workspace root, path "", is classified above, before the
@@ -144,13 +130,9 @@ export function classifyOne(wp: WorkspacePackage, siblingNames: Set<string>): Sc
144
130
  * package.json.
145
131
  */
146
132
  export function discoverModules(packages: WorkspacePackage[]): Module[] {
147
- const siblingNames = new Set<string>();
148
- for (const p of packages) {
149
- if (typeof p.pkg.name === "string") siblingNames.add(p.pkg.name);
150
- }
151
133
  const out: Module[] = [];
152
134
  for (const p of packages) {
153
- const scope = classifyOne(p, siblingNames);
135
+ const scope = classifyOne(p);
154
136
  if (!scope) {
155
137
  throw new DiscoveryError(
156
138
  `Unclassified workspace package at '${p.path}'. ` +
@@ -1,24 +1,67 @@
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.
1
+ // Facade `package.json`: the initial generator and the drift-correcting body
2
+ // rules, co-located. The facade is the only sibling of a block that publishes,
3
+ // and it ships SLIM: `dependencies` is empty (the consumer's install pulls zero
4
+ // `@platforma-sdk/*` transitive runtime deps), siblings are build-time
5
+ // `workspace:*` devDeps, and the built `dist/` + `block-pack/` carry everything
6
+ // the loader needs.
5
7
 
6
8
  import {
7
9
  ensureField,
8
10
  ensureScript,
9
11
  ensureDevDep,
10
- ensureWorkspaceScopeDeps,
12
+ ensureWorkspaceScopeDevDeps,
13
+ requireField,
14
+ removeScript,
11
15
  enforceAlphabeticalOrder,
12
16
  enforceFieldOrder,
13
17
  type RunContext,
14
18
  } from "../engine/api";
15
- import { findModules, scopeDepMap } from "../engine/ctx";
19
+ import { findModules, scopeDepMaps } from "../engine/ctx";
16
20
  import { canonicalPackageJsonOrder } from "./shared/key-order";
17
21
 
22
+ /** Per-org publish coordinates: the S3 upload bucket and the public registry
23
+ * serve URL the facade's `prepublishOnly` script passes to `block-tools
24
+ * publish`. One block-registry bucket serves both orgs; only the public read
25
+ * hostname differs by org. Safe for the never-published `etc/blocks` test
26
+ * blocks — the script never runs for them. Extend the map per new org. */
27
+ const ORG_PUBLISH_TARGETS: Record<string, { s3: string; serveUrl: string }> = {
28
+ "@platforma-open": {
29
+ s3: "s3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1",
30
+ serveUrl: "https://blocks.pl-open.science",
31
+ },
32
+ "@milaboratories": {
33
+ s3: "s3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1",
34
+ serveUrl: "https://block.registry.platforma.bio/releases",
35
+ },
36
+ };
37
+
38
+ /** Fallback for an org not in the table — keeps the generated script valid
39
+ * (and inert until published) rather than throwing during a refresh. */
40
+ const FALLBACK_PUBLISH_TARGET = {
41
+ s3: "s3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1",
42
+ serveUrl: "https://blocks.pl-open.science",
43
+ };
44
+
45
+ /** Where the in-SDK blocks (`etc/blocks/*`) publish: the aux/dev registry, not
46
+ * production `pub/releases/`. These are the monorepo's test/example blocks, so
47
+ * one target regardless of org — their `@milaboratories` scope must NOT route
48
+ * them to the real catalog alongside standalone blocks. */
49
+ const SDK_INTERNAL_PUBLISH_TARGET = {
50
+ s3: "s3://milab-euce1-prod-pkgs-s3-block-registry/aux/dev/?region=eu-central-1",
51
+ serveUrl: "https://aux-blocks.pl-open.science",
52
+ };
53
+
54
+ function prepublishScript(npmOrg: string, isSdkInternal: boolean): string {
55
+ const t = isSdkInternal
56
+ ? SDK_INTERNAL_PUBLISH_TARGET
57
+ : (ORG_PUBLISH_TARGETS[npmOrg] ?? FALLBACK_PUBLISH_TARGET);
58
+ return `block-tools publish -r ${t.s3} --registry-serve-url ${t.serveUrl}`;
59
+ }
60
+
18
61
  /** The facade's `block.components` map: the dist artifact each module build
19
- * emits, keyed by role. Fully determined by the discovered modules — the
20
- * body rule re-asserts this exact value, so it is engine-managed (NOT an
21
- * author seed). Mirrors the artifact paths produced by each scope's build:
62
+ * emits, keyed by role. Fully determined by the discovered modules — the body
63
+ * rule re-asserts this exact value, so it is engine-managed (NOT an author
64
+ * seed). Mirrors the artifact paths produced by each scope's build:
22
65
  * workflow → pl-tengo, model → build-model, ui → ts-builder. */
23
66
  export function blockComponents(ctx: RunContext): Record<string, string> {
24
67
  const components: Record<string, string> = {};
@@ -29,6 +72,44 @@ export function blockComponents(ctx: RunContext): Record<string, string> {
29
72
  return components;
30
73
  }
31
74
 
75
+ /** The slim facade's canonical scripts (build / check / prepublishOnly /
76
+ * do-pack). Shared by the initial generator and the body rule so they cannot
77
+ * drift apart. `prepublishOnly` routes by block kind: in-SDK blocks publish to
78
+ * the aux/dev registry, standalone blocks to their per-org production target. */
79
+ function blockScripts(npmOrg: string, isSdkInternal: boolean): Record<string, string> {
80
+ return {
81
+ build: "ts-builder build --target block-facade && block-tools pack",
82
+ check: "ts-builder type-check --target block-facade",
83
+ prepublishOnly: prepublishScript(npmOrg, isSdkInternal),
84
+ "do-pack": "pnpm pack && shx mv *.tgz package.tgz",
85
+ };
86
+ }
87
+
88
+ /** The facade's build/publish devDependencies (the workspace `model`/`ui`/
89
+ * `workflow` siblings are added separately, from discovery). `sdk:` resolves
90
+ * to `workspace:*` for in-monorepo blocks and `catalog:` for standalone — the
91
+ * body rule re-asserts the same set. */
92
+ const FACADE_DEV_DEPS = {
93
+ "@milaboratories/ts-builder": "sdk:",
94
+ "@milaboratories/ts-configs": "sdk:",
95
+ "@platforma-sdk/block-tools": "sdk:",
96
+ "@platforma-sdk/model": "sdk:",
97
+ shx: "catalog:",
98
+ typescript: "catalog:",
99
+ } as const;
100
+
101
+ /** The facade's `exports` map: a single `.` entry. `sources` keeps monorepo IDE
102
+ * go-to-definition pointing at `src/` (external consumers don't enable it);
103
+ * `types`/`default` carry the bundled `.d.ts` / `.js` from `dist/`. No
104
+ * `./AGENTS` subpath — that surface ships as a registry-side component. */
105
+ const FACADE_EXPORTS = {
106
+ ".": {
107
+ sources: "./src/index.ts",
108
+ types: "./dist/index.d.ts",
109
+ default: "./dist/index.js",
110
+ },
111
+ };
112
+
32
113
  /** Fail loudly if any required `block.*` field is empty — a missing
33
114
  * title / description / logo / organization or empty components map would
34
115
  * otherwise produce an init block that `block-tools pack` rejects only
@@ -63,20 +144,18 @@ function assertBlockFieldsFilled(block: {
63
144
 
64
145
  export function blockPackageJsonInitial(ctx: RunContext): Record<string, unknown> {
65
146
  const v = ctx.blockVars;
66
- // The facade depends on its model / ui / workflow modules — not on its
67
- // software module (the workflow owns that dep) and not on its test module
68
- // (the test package depends on the facade, so a reciprocal dep would be a
69
- // pnpm/turbo cycle).
70
- const deps: Record<string, string> = {
71
- ...scopeDepMap(ctx, "model"),
72
- ...scopeDepMap(ctx, "ui"),
73
- ...scopeDepMap(ctx, "workflow"),
147
+ // The facade's siblings are build-time devDeps, never runtime deps — the
148
+ // facade ships its content bundled inside dist/ + block-pack/, so the
149
+ // consumer needs no runtime resolution into siblings. Software/test are
150
+ // excluded (the workflow owns software; a test→facade dep would be a cycle).
151
+ const devDeps: Record<string, string> = {
152
+ ...FACADE_DEV_DEPS,
153
+ ...scopeDepMaps(ctx, "model"),
154
+ ...scopeDepMaps(ctx, "ui"),
155
+ ...scopeDepMaps(ctx, "workflow"),
74
156
  };
75
- // `block.components` is engine-managed (re-asserted by the body rule);
76
- // `block.meta` is an author SEED — written once at init, never touched on
77
- // refresh, so the author owns title / description / organization / logo.
78
- // `block-tools pack` requires both. The author edits the placeholder meta
79
- // before publishing.
157
+ // `block-tools pack` requires both `block.components` and `block.meta`. The
158
+ // author edits the placeholder meta before publishing.
80
159
  const block = {
81
160
  components: blockComponents(ctx),
82
161
  meta: {
@@ -94,59 +173,62 @@ export function blockPackageJsonInitial(ctx: RunContext): Record<string, unknown
94
173
  return {
95
174
  name: `${v.facadeName}.block`,
96
175
  version: "1.0.0",
97
- // type:"module" intentionally omitted.
98
- files: ["index.d.ts", "index.js"],
99
- scripts: {
100
- build: "shx rm -rf ./block-pack && block-tools pack",
101
- "mark-stable":
102
- "block-tools mark-stable -r 's3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1'",
103
- prepublishOnly:
104
- "block-tools pack && block-tools publish -r 's3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1'",
105
- "do-pack": "shx rm -f *.tgz && block-tools pack && pnpm pack && shx mv *.tgz package.tgz",
106
- },
107
- dependencies: deps,
108
- devDependencies: { "@platforma-sdk/block-tools": "sdk:", shx: "catalog:" },
176
+ // Facade is ESM: rolldown-plugin-dts requires it, and the BlockPointer's
177
+ // `import.meta.url` path math needs it.
178
+ type: "module",
179
+ // Drives the npm tarball: only the built outputs ship; sources, tsconfig,
180
+ // and node_modules stay out.
181
+ files: ["dist", "block-pack"],
182
+ main: "./dist/index.js",
183
+ module: "./dist/index.js",
184
+ types: "./dist/index.d.ts",
185
+ exports: FACADE_EXPORTS,
186
+ scripts: blockScripts(v.npmOrg, ctx.isSdkInternal),
187
+ // Slim invariant: zero runtime deps reach the consumer.
188
+ dependencies: {},
189
+ devDependencies: devDeps,
109
190
  block,
110
191
  };
111
192
  }
112
193
 
113
194
  export function blockPackageJsonRules(ctx: RunContext): void {
114
- // type:"module" intentionally omitted: the facade's index.js is the
115
- // CommonJS dev-block descriptor (module.exports / __dirname); ESM would
116
- // break it. The facade has no TS sources to compile.
117
- ensureField("files", ["index.d.ts", "index.js"]);
118
-
119
- ensureScript("build", "shx rm -rf ./block-pack && block-tools pack");
120
- ensureScript(
121
- "mark-stable",
122
- "block-tools mark-stable -r 's3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1'",
123
- );
124
- ensureScript(
125
- "prepublishOnly",
126
- "block-tools pack && block-tools publish -r 's3://milab-euce1-prod-pkgs-s3-block-registry/pub/releases/?region=eu-central-1'",
127
- );
128
- ensureScript(
129
- "do-pack",
130
- "shx rm -f *.tgz && block-tools pack && pnpm pack && shx mv *.tgz package.tgz",
131
- );
132
-
133
- // One workspace dep per discovered module in the facade's scopes. The
134
- // facade depends on model / ui / workflow — not software (the workflow
135
- // owns it) and not test (a reciprocal dep would be a pnpm/turbo cycle).
136
- ensureWorkspaceScopeDeps("model");
137
- ensureWorkspaceScopeDeps("ui");
138
- ensureWorkspaceScopeDeps("workflow");
195
+ const v = ctx.blockVars;
196
+
197
+ ensureField("type", "module");
198
+ ensureField("files", ["dist", "block-pack"]);
199
+ ensureField("main", "./dist/index.js");
200
+ ensureField("module", "./dist/index.js");
201
+ ensureField("types", "./dist/index.d.ts");
202
+ ensureField("exports", FACADE_EXPORTS);
139
203
 
204
+ const scripts = blockScripts(v.npmOrg, ctx.isSdkInternal);
205
+ for (const [name, command] of Object.entries(scripts)) ensureScript(name, command);
206
+ removeScript("mark-stable");
207
+
208
+ // Slim invariant: the facade carries ZERO runtime dependencies. The siblings
209
+ // and the SDK build/publish deps are all devDeps, so `dependencies` ends
210
+ // empty. Software/test are excluded (the workflow owns software; a
211
+ // test→facade dep would be a cycle).
212
+ ensureWorkspaceScopeDevDeps("model");
213
+ ensureWorkspaceScopeDevDeps("ui");
214
+ ensureWorkspaceScopeDevDeps("workflow");
215
+ ensureDevDep("@milaboratories/ts-builder", "sdk:");
216
+ ensureDevDep("@milaboratories/ts-configs", "sdk:");
140
217
  ensureDevDep("@platforma-sdk/block-tools", "sdk:");
141
- // shx powers the cross-platform build / do-pack scripts above.
218
+ ensureDevDep("@platforma-sdk/model", "sdk:");
142
219
  ensureDevDep("shx", "catalog:");
220
+ ensureDevDep("typescript", "catalog:");
143
221
 
144
- // `block.components` is fully determined by the discovered modules — keep
145
- // it in sync on refresh. `block.meta` is deliberately NOT touched here: it
146
- // is an author-owned seed (set once in the init package.json), so refresh
147
- // must never overwrite the author's title / description / logo.
222
+ // `block.components` is fully determined by the discovered modules — keep it
223
+ // in sync on refresh. `block.meta` is deliberately NOT touched: it is an
224
+ // author-owned seed (set once in the init package.json), so refresh must
225
+ // never overwrite the author's title / description / logo.
148
226
  ensureField("block.components", blockComponents(ctx));
149
227
 
228
+ // The author owns `name` and `version`; `version` MUST be present (the
229
+ // facade publishes).
230
+ requireField("version", "facade package.json must carry a 'version' (the facade publishes)");
231
+
150
232
  enforceAlphabeticalOrder("dependencies");
151
233
  enforceAlphabeticalOrder("devDependencies");
152
234
  enforceAlphabeticalOrder("peerDependencies");
@@ -1,13 +1,48 @@
1
- // Block-scope rules — the orchestrator package at `block/`.
2
- // Workspace-scope deps in the body resolve from ctx.modules.
1
+ // Block-scope rules — the facade package at `block/`. The facade is the
2
+ // only sibling of a block that publishes: a slim 4-file `src/` surface
3
+ // (engine-owned `index.ts` + `AGENTS.ts`, author-owned `block-extra.ts` +
4
+ // `agents-extra.ts`), a facade `tsconfig.json`, and a slim publishable
5
+ // `package.json`. Workspace-scope deps in the body resolve from ctx.modules.
3
6
 
4
- import { scope, fixed, managed, seed, file, binaryFile, generate } from "../engine/api";
7
+ import {
8
+ scope,
9
+ fixed,
10
+ scaffold,
11
+ managed,
12
+ seed,
13
+ remove,
14
+ file,
15
+ tpl,
16
+ binaryFile,
17
+ generate,
18
+ } from "../engine/api";
5
19
  import { blockPackageJsonInitial, blockPackageJsonRules } from "./block-package-json";
20
+ import { pascalCase } from "./shared/pascal-case";
6
21
 
7
22
  export function blockRules(): void {
8
23
  scope("block", () => {
9
- fixed("index.js", file("block/index.js"));
10
- fixed("index.d.ts", file("block/index.d.ts"));
24
+ // `remove` is idempotent (absent file → no-op), so this is safe on a block
25
+ // that has no such entry files.
26
+ remove("index.js");
27
+ remove("index.d.ts");
28
+
29
+ // Four-file source surface. `index.ts` is engine-owned and per-block
30
+ // (model import path + `<PascalName>` aliases) → text-templated. `AGENTS.ts`
31
+ // is engine-owned and identical for every block → static. The two `*-extra`
32
+ // files are author scaffolds (`scaffold`: create-if-missing on every refresh,
33
+ // never overwritten).
34
+ fixed(
35
+ "src/index.ts",
36
+ tpl("block/src/index.tpl.ts", (ctx) => ({
37
+ facadeName: ctx.blockVars.facadeName,
38
+ pascalName: pascalCase(ctx.blockVars.shortName),
39
+ })),
40
+ );
41
+ fixed("src/AGENTS.ts", file("block/src/AGENTS.ts"));
42
+ scaffold("src/block-extra.ts", file("block/src/block-extra.ts"));
43
+ scaffold("src/agents-extra.ts", file("block/src/agents-extra.ts"));
44
+
45
+ fixed("tsconfig.json", file("block/tsconfig.json"));
11
46
 
12
47
  // Placeholder logos referenced by `block.meta` (logo + organization.logo).
13
48
  // Binary seeds: written once at init, author-owned thereafter — refresh
@@ -33,7 +33,7 @@ export function modelPackageJsonInitial(ctx: RunContext): Record<string, unknown
33
33
  const v = ctx.blockVars;
34
34
  return {
35
35
  name: `${v.facadeName}.model`,
36
- version: "1.0.0",
36
+ private: true,
37
37
  type: "module",
38
38
  // The block-model build emits both index.cjs and index.js. `main` is
39
39
  // the CommonJS entry (require fallback); `module` the ESM entry.
@@ -70,6 +70,11 @@ export function modelPackageJsonInitial(ctx: RunContext): Record<string, unknown
70
70
  }
71
71
 
72
72
  export function modelPackageJsonRules(): void {
73
+ // Controlled sibling — workspace-only, never published. `private: true` makes
74
+ // npm refuse to publish it; the `version` is kept (changesets-owned) so the
75
+ // packed template's lib carries a version.
76
+ ensureField("private", true);
77
+
73
78
  ensureField("type", "module");
74
79
  // build emits both; main = CJS entry, module = ESM entry (prod convention).
75
80
  ensureField("main", "dist/index.cjs");
@@ -0,0 +1,16 @@
1
+ // `pascalCase(shortName)` → the block-named alias prefix the facade entry
2
+ // template emits (`<PascalName>BlockContract`, …). Splits on any run of
3
+ // non-alphanumeric characters and capitalises each segment's first char:
4
+ // test-sum-numbers-v3 → TestSumNumbersV3
5
+ // mixcr-clonotyping-2 → MixcrClonotyping2
6
+ // samples-and-data → SamplesAndData
7
+ export function pascalCase(s: string): string {
8
+ const joined = s
9
+ .split(/[^A-Za-z0-9]+/)
10
+ .filter((seg) => seg.length > 0)
11
+ .map((seg) => seg.charAt(0).toUpperCase() + seg.slice(1))
12
+ .join("");
13
+ // A leading digit (e.g. `3d-viewer` → `3dViewer`) is not a valid identifier
14
+ // start — the prefix becomes `const 3dViewerBlockPointer = …`. Prepend `_`.
15
+ return /^[0-9]/.test(joined) ? `_${joined}` : joined;
16
+ }
@@ -46,7 +46,7 @@ export function softwarePackageJsonInitial(ctx: RunContext): Record<string, unkn
46
46
  const v = ctx.blockVars;
47
47
  return {
48
48
  name: `${v.facadeName}.software`,
49
- version: "1.0.0",
49
+ private: true,
50
50
  type: "module",
51
51
  description: "Block Software",
52
52
  files: ["./dist/**/*"],
@@ -65,6 +65,11 @@ export function softwarePackageJsonInitial(ctx: RunContext): Record<string, unkn
65
65
  }
66
66
 
67
67
  export function softwarePackageJsonRules(): void {
68
+ // Controlled sibling — workspace-only, never npm-published. (Software still
69
+ // publishes runenv artifacts via `pl-pkg`, which is unaffected by `private`.)
70
+ // The `version` is kept (changesets-owned) so the packed template carries it.
71
+ ensureField("private", true);
72
+
68
73
  ensureField("type", "module");
69
74
  ensureField("files", ["./dist/**/*"]);
70
75
 
@@ -19,7 +19,6 @@ export function testPackageJsonInitial(ctx: RunContext): Record<string, unknown>
19
19
  const v = ctx.blockVars;
20
20
  return {
21
21
  name: `${v.facadeName}.test`,
22
- version: "1.0.0",
23
22
  private: true,
24
23
  type: "module",
25
24
  scripts: {
@@ -41,6 +40,11 @@ export function testPackageJsonInitial(ctx: RunContext): Record<string, unknown>
41
40
  }
42
41
 
43
42
  export function testPackageJsonRules(): void {
43
+ // Controlled sibling — workspace-only, never published. (The initial already
44
+ // seeds `private: true`; the body re-asserts it. The `version` is kept —
45
+ // changesets-owned — so the packed template's lib carries a version.)
46
+ ensureField("private", true);
47
+
44
48
  ensureField("type", "module");
45
49
 
46
50
  ensureScript("fmt", "ts-builder format");
@@ -32,7 +32,7 @@ export function uiPackageJsonInitial(ctx: RunContext): Record<string, unknown> {
32
32
  const v = ctx.blockVars;
33
33
  return {
34
34
  name: `${v.facadeName}.ui`,
35
- version: "1.0.0",
35
+ private: true,
36
36
  type: "module",
37
37
  scripts: {
38
38
  fmt: "ts-builder format",
@@ -60,6 +60,10 @@ export function uiPackageJsonInitial(ctx: RunContext): Record<string, unknown> {
60
60
  }
61
61
 
62
62
  export function uiPackageJsonRules(): void {
63
+ // Controlled sibling — workspace-only, never published, but kept versioned
64
+ // (changesets-owned) so the packed template's lib carries a version.
65
+ ensureField("private", true);
66
+
63
67
  ensureField("type", "module");
64
68
 
65
69
  ensureScript("fmt", "ts-builder format");
@@ -25,7 +25,7 @@ export function workflowPackageJsonInitial(ctx: RunContext): Record<string, unkn
25
25
  const v = ctx.blockVars;
26
26
  return {
27
27
  name: `${v.facadeName}.workflow`,
28
- version: "1.0.0",
28
+ private: true,
29
29
  type: "module",
30
30
  scripts: {
31
31
  // No `fmt`: the workflow is Tengo, not TS — nothing for oxlint/oxfmt
@@ -54,6 +54,10 @@ export function workflowPackageJsonInitial(ctx: RunContext): Record<string, unkn
54
54
  }
55
55
 
56
56
  export function workflowPackageJsonRules(): void {
57
+ // Controlled sibling — workspace-only, never published, but kept versioned
58
+ // (changesets-owned) so the packed template's lib carries a version.
59
+ ensureField("private", true);
60
+
57
61
  ensureField("type", "module");
58
62
 
59
63
  // No `fmt` script: the workflow is Tengo, not TS — build + check run via
@@ -0,0 +1,5 @@
1
+ // This file is managed by `block-tools structure`. Do not edit by hand.
2
+ // Narrow MCP / AI surface.
3
+
4
+ export type { BlockContract, BlockOutputs, BlockData } from "./index";
5
+ export * from "./agents-extra";