@milaboratories/pl-middle-layer 1.66.19 → 1.67.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 (130) hide show
  1. package/dist/block_registry/index.cjs +3 -0
  2. package/dist/block_registry/index.d.ts +2 -1
  3. package/dist/block_registry/index.js +2 -1
  4. package/dist/block_registry/location_provider.cjs +117 -0
  5. package/dist/block_registry/location_provider.cjs.map +1 -0
  6. package/dist/block_registry/location_provider.js +114 -0
  7. package/dist/block_registry/location_provider.js.map +1 -0
  8. package/dist/block_registry/registry.cjs +13 -0
  9. package/dist/block_registry/registry.cjs.map +1 -1
  10. package/dist/block_registry/registry.d.ts +14 -1
  11. package/dist/block_registry/registry.d.ts.map +1 -1
  12. package/dist/block_registry/registry.js +13 -0
  13. package/dist/block_registry/registry.js.map +1 -1
  14. package/dist/block_registry/template_provider.cjs +104 -0
  15. package/dist/block_registry/template_provider.cjs.map +1 -0
  16. package/dist/block_registry/template_provider.d.ts +62 -0
  17. package/dist/block_registry/template_provider.d.ts.map +1 -0
  18. package/dist/block_registry/template_provider.js +103 -0
  19. package/dist/block_registry/template_provider.js.map +1 -0
  20. package/dist/block_registry/watcher.cjs +3 -3
  21. package/dist/block_registry/watcher.js +2 -2
  22. package/dist/dev_env/util.js +2 -2
  23. package/dist/dev_env/util.js.map +1 -1
  24. package/dist/index.cjs +10 -0
  25. package/dist/index.d.ts +5 -1
  26. package/dist/index.js +5 -1
  27. package/dist/js_render/computable_context.cjs +1 -1
  28. package/dist/js_render/computable_context.js +1 -1
  29. package/dist/middle_layer/driver_kit.d.ts +1 -1
  30. package/dist/middle_layer/middle_layer.cjs +144 -2
  31. package/dist/middle_layer/middle_layer.cjs.map +1 -1
  32. package/dist/middle_layer/middle_layer.d.ts +67 -1
  33. package/dist/middle_layer/middle_layer.d.ts.map +1 -1
  34. package/dist/middle_layer/middle_layer.js +146 -4
  35. package/dist/middle_layer/middle_layer.js.map +1 -1
  36. package/dist/middle_layer/project.cjs +1 -0
  37. package/dist/middle_layer/project.cjs.map +1 -1
  38. package/dist/middle_layer/project.d.ts +2 -2
  39. package/dist/middle_layer/project.js +4 -4
  40. package/dist/middle_layer/project.js.map +1 -1
  41. package/dist/middle_layer/sharing_list.d.ts +1 -1
  42. package/dist/model/args.cjs +9 -28
  43. package/dist/model/args.cjs.map +1 -1
  44. package/dist/model/args.js +9 -28
  45. package/dist/model/args.js.map +1 -1
  46. package/dist/model/index.cjs +7 -0
  47. package/dist/model/index.d.ts +4 -1
  48. package/dist/model/index.js +4 -1
  49. package/dist/model/project_helper.cjs +111 -0
  50. package/dist/model/project_helper.cjs.map +1 -1
  51. package/dist/model/project_helper.d.ts +76 -1
  52. package/dist/model/project_helper.d.ts.map +1 -1
  53. package/dist/model/project_helper.js +111 -0
  54. package/dist/model/project_helper.js.map +1 -1
  55. package/dist/model/project_model.d.ts +1 -1
  56. package/dist/model/sharing_model.d.ts +1 -1
  57. package/dist/model/template_apply.cjs +67 -0
  58. package/dist/model/template_apply.cjs.map +1 -0
  59. package/dist/model/template_apply.d.ts +50 -0
  60. package/dist/model/template_apply.d.ts.map +1 -0
  61. package/dist/model/template_apply.js +64 -0
  62. package/dist/model/template_apply.js.map +1 -0
  63. package/dist/model/template_export.cjs +82 -0
  64. package/dist/model/template_export.cjs.map +1 -0
  65. package/dist/model/template_export.d.ts +9 -0
  66. package/dist/model/template_export.d.ts.map +1 -0
  67. package/dist/model/template_export.js +82 -0
  68. package/dist/model/template_export.js.map +1 -0
  69. package/dist/model/template_parser.cjs +74 -0
  70. package/dist/model/template_parser.cjs.map +1 -0
  71. package/dist/model/template_parser.d.ts +43 -0
  72. package/dist/model/template_parser.d.ts.map +1 -0
  73. package/dist/model/template_parser.js +72 -0
  74. package/dist/model/template_parser.js.map +1 -0
  75. package/dist/model/template_resolve.cjs +134 -0
  76. package/dist/model/template_resolve.cjs.map +1 -0
  77. package/dist/model/template_resolve.d.ts +193 -0
  78. package/dist/model/template_resolve.d.ts.map +1 -0
  79. package/dist/model/template_resolve.js +133 -0
  80. package/dist/model/template_resolve.js.map +1 -0
  81. package/dist/model/template_serializer.cjs +162 -0
  82. package/dist/model/template_serializer.cjs.map +1 -0
  83. package/dist/model/template_serializer.d.ts +17 -0
  84. package/dist/model/template_serializer.d.ts.map +1 -0
  85. package/dist/model/template_serializer.js +157 -0
  86. package/dist/model/template_serializer.js.map +1 -0
  87. package/dist/mutator/block-pack/block_pack.cjs +1 -1
  88. package/dist/mutator/block-pack/block_pack.js +1 -1
  89. package/dist/mutator/migration.cjs +1 -1
  90. package/dist/mutator/migration.js +1 -1
  91. package/dist/mutator/project.cjs +35 -5
  92. package/dist/mutator/project.cjs.map +1 -1
  93. package/dist/mutator/project.d.ts.map +1 -1
  94. package/dist/mutator/project.js +35 -5
  95. package/dist/mutator/project.js.map +1 -1
  96. package/dist/mutator/template_construct.cjs +77 -0
  97. package/dist/mutator/template_construct.cjs.map +1 -0
  98. package/dist/mutator/template_construct.js +77 -0
  99. package/dist/mutator/template_construct.js.map +1 -0
  100. package/dist/network_check/template.js +3 -3
  101. package/dist/network_check/template.js.map +1 -1
  102. package/package.json +18 -18
  103. package/src/block_registry/index.ts +1 -0
  104. package/src/block_registry/location_provider.test.ts +236 -0
  105. package/src/block_registry/location_provider.ts +124 -0
  106. package/src/block_registry/registry.ts +22 -0
  107. package/src/block_registry/template_provider.test.ts +275 -0
  108. package/src/block_registry/template_provider.ts +168 -0
  109. package/src/middle_layer/middle_layer.ts +192 -1
  110. package/src/middle_layer/project.ts +1 -1
  111. package/src/model/args.test.ts +26 -1
  112. package/src/model/args.ts +16 -31
  113. package/src/model/index.ts +21 -0
  114. package/src/model/project_helper.ts +238 -0
  115. package/src/model/project_helper_params_init.test.ts +249 -0
  116. package/src/model/template_apply.test.ts +65 -0
  117. package/src/model/template_apply.ts +128 -0
  118. package/src/model/template_export.test.ts +255 -0
  119. package/src/model/template_export.ts +136 -0
  120. package/src/model/template_parser.test.ts +264 -0
  121. package/src/model/template_parser.ts +108 -0
  122. package/src/model/template_resolve.test.ts +397 -0
  123. package/src/model/template_resolve.ts +346 -0
  124. package/src/model/template_serializer.test.ts +398 -0
  125. package/src/model/template_serializer.ts +232 -0
  126. package/src/model/template_serializer_fixtures.test.ts +274 -0
  127. package/src/mutator/project-v3.test.ts +11 -11
  128. package/src/mutator/project.ts +92 -5
  129. package/src/mutator/template_construct.test.ts +456 -0
  130. package/src/mutator/template_construct.ts +150 -0
@@ -0,0 +1,249 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import { getQuickJS, type QuickJSWASMModule } from "quickjs-emscripten";
3
+ import type { BlockConfig, ResultOrError } from "@platforma-sdk/model";
4
+ import { BlockStorageFacadeCallbacks } from "@platforma-sdk/model";
5
+ import { ProjectHelper } from "./project_helper";
6
+
7
+ /**
8
+ * `ProjectHelper.getInitialStorageFromParamsInVM` — the middle layer's side of the
9
+ * params-to-storage callback.
10
+ *
11
+ * The block model is hand-written here rather than loaded from a built block: the
12
+ * method's whole job is the boundary (does the callback exist, what crosses it as
13
+ * text, how each failure is reported), and a hand-written model is the only way to
14
+ * drive every failure branch on purpose. What the SDK puts behind the callback is
15
+ * covered by `sdk/model/src/template_init.test.ts`.
16
+ */
17
+
18
+ const HANDLE = BlockStorageFacadeCallbacks.StorageInitialFromParams;
19
+
20
+ /**
21
+ * No entries created yet, which is what these tests want: relocation is the SDK's half of this
22
+ * callback and is covered where it lives (`pl-model-common`'s `relocateBlockIds`). What is
23
+ * being driven here is the boundary — does the callback exist, what crosses it as text, how
24
+ * each failure is reported — and an empty map keeps the params arriving as written.
25
+ */
26
+ const NO_IDS: ReadonlyMap<string, string> = new Map();
27
+
28
+ /**
29
+ * A block model whose params callback body is `body`.
30
+ *
31
+ * Registration mirrors what the SDK's `tryRegisterCallback` does inside a real
32
+ * bundle: write the function into the render context's callback registry, which is
33
+ * where the invoker looks it up by handle.
34
+ */
35
+ function modelCode(body: string): string {
36
+ return `globalThis.cfgRenderCtx.callbackRegistry[${JSON.stringify(HANDLE)}] = ${body};`;
37
+ }
38
+
39
+ /**
40
+ * A config carrying `code` and declaring the callback.
41
+ *
42
+ * Only three fields are read on this path — `modelAPIVersion`, the callback handle,
43
+ * and `code` — so the rest of `BlockConfig` (outputs, sections, inputsValid) is left
44
+ * out and the cast stands in for it.
45
+ */
46
+ function configWith(
47
+ code: string,
48
+ options: { declareCallback?: boolean; modelAPIVersion?: number } = {},
49
+ ): BlockConfig {
50
+ const { declareCallback = true, modelAPIVersion = 2 } = options;
51
+ return {
52
+ modelAPIVersion,
53
+ sdkVersion: "1.0.0",
54
+ code: { type: "plain", content: code },
55
+ blockLifecycleCallbacks: declareCallback ? { [HANDLE]: { handle: HANDLE } } : {},
56
+ } as unknown as BlockConfig;
57
+ }
58
+
59
+ /** The storage the callback produced, parsed. Fails the test if it errored instead. */
60
+ function storageFrom(result: ResultOrError<string>): unknown {
61
+ if (result.error !== undefined) throw new Error(`expected storage, got: ${result.error.message}`);
62
+ return JSON.parse(result.value);
63
+ }
64
+
65
+ let helper: ProjectHelper;
66
+ let quickJs: QuickJSWASMModule;
67
+
68
+ beforeAll(async () => {
69
+ quickJs = await getQuickJS();
70
+ helper = new ProjectHelper(quickJs);
71
+ });
72
+
73
+ describe("getInitialStorageFromParamsInVM", () => {
74
+ test("params reach the block's callback and its storage comes back", () => {
75
+ // The model echoes what it was given, so this asserts both directions of the
76
+ // boundary at once: the params arrived as text, the storage came back as text.
77
+ const result = helper.getInitialStorageFromParamsInVM(
78
+ configWith(modelCode("(paramsJson) => ({ storageJson: paramsJson })")),
79
+ { numbers: [3, 1, 2], label: "run 1" },
80
+ NO_IDS,
81
+ );
82
+
83
+ expect(storageFrom(result)).toEqual({ numbers: [3, 1, 2], label: "run 1" });
84
+ });
85
+
86
+ test("a reference in params survives the crossing unchanged", () => {
87
+ // With no entries in the map there is nothing to repoint, so this asserts the crossing
88
+ // alone: a `__isRef` object must arrive byte-identical, since a mangled one would produce
89
+ // a block wired to nothing.
90
+ const ref = { __isRef: true, blockId: "11111111-1111-4111-8111-111111111111", name: "reads" };
91
+
92
+ const result = helper.getInitialStorageFromParamsInVM(
93
+ configWith(modelCode("(paramsJson) => ({ storageJson: paramsJson })")),
94
+ { source: ref },
95
+ NO_IDS,
96
+ );
97
+
98
+ expect(storageFrom(result)).toEqual({ source: ref });
99
+ });
100
+
101
+ test("absent params arrive as an empty object, never as the literal undefined", () => {
102
+ // `JSON.stringify(undefined)` is not a string at all, so the callback would be
103
+ // handed nothing and fail to parse. An entry with no params is supposed to go
104
+ // through the params-less initializer instead; this is the safety net.
105
+ const result = helper.getInitialStorageFromParamsInVM(
106
+ configWith(modelCode("(paramsJson) => ({ storageJson: paramsJson })")),
107
+ undefined,
108
+ NO_IDS,
109
+ );
110
+
111
+ expect(storageFrom(result)).toEqual({});
112
+ });
113
+
114
+ test("a block that does not support params is reported, with the way out", () => {
115
+ // The load-bearing case: the old block CAN be added to a project, so silently
116
+ // falling back to default initialization would produce a block that looks
117
+ // applied but ignores everything the template said about it.
118
+ //
119
+ // The wording is asserted because it is the whole value of this branch. Whoever
120
+ // applied the file did not build the block and cannot rebuild it, so a message
121
+ // about SDKs or callbacks would leave them with nothing to do.
122
+ const result = helper.getInitialStorageFromParamsInVM(
123
+ configWith(modelCode("(paramsJson) => ({ storageJson: paramsJson })"), {
124
+ declareCallback: false,
125
+ }),
126
+ { numbers: [1] },
127
+ NO_IDS,
128
+ );
129
+
130
+ expect(result.error?.message).toBe(
131
+ "This version of the block cannot be created from a template. Use a newer version " +
132
+ "of the block, or remove the pinned block version from the template entry so a " +
133
+ "supported one is chosen automatically.",
134
+ );
135
+ });
136
+
137
+ test("the callback's own error is passed through verbatim", () => {
138
+ // Params the block rejects — the expected failure for a hand-written template.
139
+ // The message is the block's, so it must not be wrapped or reworded.
140
+ const result = helper.getInitialStorageFromParamsInVM(
141
+ configWith(modelCode('() => ({ error: "numbers must not be empty" })')),
142
+ { numbers: [] },
143
+ NO_IDS,
144
+ );
145
+
146
+ expect(result.error?.message).toBe("numbers must not be empty");
147
+ });
148
+
149
+ test("a throwing model is reported with its reason in the message", () => {
150
+ // Not the same as the case above: this is the model failing rather than
151
+ // declining. The reason has to reach `message`, since that is all the layers
152
+ // between here and the user carry.
153
+ const result = helper.getInitialStorageFromParamsInVM(
154
+ configWith(modelCode('() => { throw new Error("boom"); }')),
155
+ { numbers: [1] },
156
+ NO_IDS,
157
+ );
158
+
159
+ expect(result.error?.message).toContain("Initial storage creation from params failed");
160
+ expect(result.error?.message).toContain("boom");
161
+ });
162
+
163
+ test("a v1 block is rejected outright", () => {
164
+ const result = helper.getInitialStorageFromParamsInVM(
165
+ configWith(modelCode("(paramsJson) => ({ storageJson: paramsJson })"), {
166
+ modelAPIVersion: 1,
167
+ }),
168
+ { numbers: [1] },
169
+ NO_IDS,
170
+ );
171
+
172
+ expect(result.error?.message).toMatch(/only supported for model API version 2/);
173
+ });
174
+ });
175
+
176
+ describe("validateTemplateParamsInVM", () => {
177
+ const VALIDATE = BlockStorageFacadeCallbacks.InitializationParamsValidate;
178
+
179
+ /** A model registering only the validate callback, with `body` as its implementation. */
180
+ const validatorConfig = (body: string, options: { declareCallback?: boolean } = {}) => {
181
+ const { declareCallback = true } = options;
182
+ return {
183
+ modelAPIVersion: 2,
184
+ sdkVersion: "1.0.0",
185
+ code: {
186
+ type: "plain",
187
+ content: `globalThis.cfgRenderCtx.callbackRegistry[${JSON.stringify(VALIDATE)}] = ${body};`,
188
+ },
189
+ blockLifecycleCallbacks: declareCallback ? { [VALIDATE]: { handle: VALIDATE } } : {},
190
+ } as unknown as BlockConfig;
191
+ };
192
+
193
+ test("params the kind accepts come back with no error and nothing else", () => {
194
+ // Every kind declares a parser, so a pass needs no flag saying whether anything ran:
195
+ // reaching here without an error means the params were held to the contract.
196
+ const result = helper.validateTemplateParamsInVM(validatorConfig("() => ({})"), {
197
+ numbers: [1, 2],
198
+ });
199
+
200
+ expect(result.error).toBeUndefined();
201
+ });
202
+
203
+ test("params the kind rejects come back with the kind's reason", () => {
204
+ // The whole point of the pre-flight: this is reported against the entry that
205
+ // carries the params, with no project created yet.
206
+ const result = helper.validateTemplateParamsInVM(
207
+ validatorConfig('() => ({ error: "numbers: Expected array, received string" })'),
208
+ { numbers: "1,2,3" },
209
+ );
210
+
211
+ expect(result.error?.message).toBe("numbers: Expected array, received string");
212
+ });
213
+
214
+ test("a block whose model predates the callback passes unchecked", () => {
215
+ // Unlike initialization, this creates nothing, so there is nothing to get wrong by
216
+ // proceeding — and such a block is refused outright at the point it is applied.
217
+ const result = helper.validateTemplateParamsInVM(
218
+ validatorConfig("() => ({})", { declareCallback: false }),
219
+ { numbers: [1] },
220
+ );
221
+
222
+ expect(result.error).toBeUndefined();
223
+ });
224
+
225
+ test("params reach the check as text, references included", () => {
226
+ // A reference id here may be a placeholder — the check runs before blocks exist —
227
+ // so what crosses must be the shape, unaltered.
228
+ const result = helper.validateTemplateParamsInVM(
229
+ validatorConfig("(paramsJson) => ({ error: paramsJson })"),
230
+ { source: { __isRef: true, blockId: "placeholder", name: "reads" } },
231
+ );
232
+
233
+ expect(JSON.parse(result.error!.message)).toEqual({
234
+ source: { __isRef: true, blockId: "placeholder", name: "reads" },
235
+ });
236
+ });
237
+
238
+ test("a check that throws is reported as failing to run", () => {
239
+ // Distinct from a rejection: the kind did not decline the params, its own code
240
+ // broke.
241
+ const result = helper.validateTemplateParamsInVM(
242
+ validatorConfig('() => { throw new Error("schema is broken"); }'),
243
+ { numbers: [1] },
244
+ );
245
+
246
+ expect(result.error?.message).toContain("Params check failed to run");
247
+ expect(result.error?.message).toContain("schema is broken");
248
+ });
249
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import type {
3
+ BlockKindReference,
4
+ BlockKindSelectorReference,
5
+ } from "@milaboratories/pl-model-common";
6
+ import { kindMismatch } from "./template_apply";
7
+
8
+ /**
9
+ * Whether the block prepared for an entry is the block that entry meant.
10
+ *
11
+ * Tested directly rather than through `applyTemplateToProject`, which needs a backend to
12
+ * prepare a block pack at all. It is a pure function for exactly that reason: the question
13
+ * does not need a project, and keeping it answerable without one is what makes it testable.
14
+ */
15
+
16
+ const ASKED = "@platforma-open/milaboratories.demo.kind@^1.0.0" as BlockKindSelectorReference;
17
+ const declares = (ref: string) => ref as BlockKindReference;
18
+
19
+ describe("kindMismatch", () => {
20
+ test("a version inside the asked range serves the entry", () => {
21
+ const declared = declares("@platforma-open/milaboratories.demo.kind@1.4.0");
22
+
23
+ expect(kindMismatch(ASKED, declared)).toBeUndefined();
24
+ });
25
+
26
+ test("a version outside it does not, and the message names both sides", () => {
27
+ const message = kindMismatch(ASKED, declares("@platforma-open/milaboratories.demo.kind@2.0.0"));
28
+
29
+ expect(message).toContain("@platforma-open/milaboratories.demo.kind@^1.0.0");
30
+ expect(message).toContain("version 2.0.0");
31
+ });
32
+
33
+ test("a different kind entirely is refused by name", () => {
34
+ // What a location makes possible: the path still resolves, but what is there now is a
35
+ // different block than the one the params were written for.
36
+ const message = kindMismatch(
37
+ ASKED,
38
+ declares("@platforma-open/milaboratories.other.kind@1.0.0"),
39
+ );
40
+
41
+ expect(message).toContain("@platforma-open/milaboratories.demo.kind");
42
+ expect(message).toContain("@platforma-open/milaboratories.other.kind");
43
+ });
44
+
45
+ test("a block declaring no kind cannot serve an entry that names one", () => {
46
+ // Reachable through a pinned version, which names a package and says nothing about
47
+ // kinds — so the block it names may well be one built before kinds existed.
48
+ expect(kindMismatch(ASKED, undefined)).toMatch(/declares no kind/);
49
+ });
50
+
51
+ test("an unreadable declaration is refused rather than thrown", () => {
52
+ // The entry's own selector was checked when the document was parsed, so a failure here
53
+ // is the block's stored reference. One bad block must not break the whole apply.
54
+ expect(kindMismatch(ASKED, declares("no-version-here"))).toMatch(/unreadable kind/);
55
+ });
56
+
57
+ test("the message names no route, so the caller can name the one the entry took", () => {
58
+ // The check is asked once, of the prepared block, whichever of the three routes found
59
+ // it. Only the caller knows whether there is a locator to point the reader at.
60
+ const message = kindMismatch(ASKED, undefined)!;
61
+
62
+ expect(message).not.toContain("file:");
63
+ expect(message).not.toContain("location");
64
+ });
65
+ });
@@ -0,0 +1,128 @@
1
+ import type {
2
+ BlockKindReference,
3
+ BlockKindSelectorReference,
4
+ } from "@milaboratories/pl-model-common";
5
+ import { parseKindRef, parseKindSelectorReference } from "@milaboratories/pl-model-common";
6
+ import { selectorToRange } from "@platforma-sdk/block-tools";
7
+ import { ensureError } from "@platforma-sdk/model";
8
+ import * as semver from "semver";
9
+
10
+ /**
11
+ * One entry the file describes in a way this project cannot honour.
12
+ *
13
+ * Thrown, not returned, and that is the whole failure policy: an apply either creates every
14
+ * entry or creates none. It travels out through the open transaction, which is therefore
15
+ * never committed, so a rejected file leaves the project exactly as it was. The caller turns
16
+ * it into a {@link TemplateApplyProblem}; anything else reaching that caller is an outage
17
+ * rather than a statement about the file, and keeps propagating.
18
+ */
19
+ export class TemplateEntryRejected extends Error {
20
+ constructor(
21
+ readonly entryId: string,
22
+ message: string,
23
+ ) {
24
+ super(message);
25
+ this.name = "TemplateEntryRejected";
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Something that stopped one entry from being applied.
31
+ *
32
+ * Every stage of an apply reports in this shape — resolution, validation,
33
+ * construction — so a caller assembles one list and the reader sees which entry in
34
+ * their file each problem belongs to. `error` is a finished sentence for the person
35
+ * who applied the file, not a code.
36
+ */
37
+ export type TemplateApplyProblem = {
38
+ /** The template-local id of the entry the problem belongs to. */
39
+ readonly entryId: string;
40
+ readonly error: string;
41
+ };
42
+
43
+ /** One entry that made it into the project. */
44
+ export type AppliedEntry = {
45
+ /** The entry's id in the file. */
46
+ readonly templateLocalId: string;
47
+ /** The project-local id the block was given. */
48
+ readonly blockId: string;
49
+ };
50
+
51
+ /**
52
+ * What a whole apply — reading, checking, resolving, placing — left behind.
53
+ *
54
+ * One shape for every stage's findings, because the reader does not care which stage
55
+ * objected. `problems` empty means the project holds exactly what the document described;
56
+ * otherwise `added` is empty, because no stage creates anything until every entry has passed.
57
+ *
58
+ * A stage that checks reports every problem it found, so a file with three mistakes takes one
59
+ * pass to fix. Placement stops at the first one — hence at most one problem from there.
60
+ */
61
+ export type TemplateApplyReport = {
62
+ readonly added: readonly AppliedEntry[];
63
+ readonly problems: readonly TemplateApplyProblem[];
64
+ };
65
+
66
+ /**
67
+ * Why the block prepared for an entry cannot serve it, or `undefined` when it can.
68
+ *
69
+ * An entry's `kind` is the contract its params are written against, so the block that ends up
70
+ * installed has to implement it. Only one of the three routes to a block gives that for free:
71
+ * kind resolution, where the kind's own projection picks the block. The two overrides do not —
72
+ * a location's folder can change without the file changing, and a pinned version names a
73
+ * package with no reference to a kind at all.
74
+ *
75
+ * So this belongs here, with the apply's other per-entry checks, and not in resolution: it is
76
+ * asked once, of the block that was actually prepared, whichever route found it. Resolution
77
+ * could answer it for a location — `byLocation` reads the block's config anyway — and doing so
78
+ * there would have been a second place stating the same invariant, with the pinned-version
79
+ * route still uncovered. One caller is the point.
80
+ *
81
+ * It is also the check that makes the params check meaningful, so it is asked first: params are
82
+ * checked by the INSTALLED block's kind parser, which against a block of the wrong kind would
83
+ * hold params written for one contract against another.
84
+ *
85
+ * Version comparison goes through the same selector-to-range translation the registry resolver
86
+ * uses, so "this resolves locally" and "this would resolve once published" cannot disagree
87
+ * about the version math.
88
+ *
89
+ * Messages name no route, so a caller adds whichever locator the entry carried.
90
+ */
91
+ export function kindMismatch(
92
+ asked: BlockKindSelectorReference,
93
+ declared: BlockKindReference | undefined,
94
+ ): string | undefined {
95
+ if (declared === undefined) {
96
+ return (
97
+ "The block resolved for this entry declares no kind, so it cannot be the " +
98
+ "implementation it asks for"
99
+ );
100
+ }
101
+
102
+ let wanted: { name: string; selector: { op: "exact" | "patch" | "minor"; version: string } };
103
+ let has: { name: string; version: string };
104
+ try {
105
+ wanted = parseKindSelectorReference(asked);
106
+ has = parseKindRef(declared);
107
+ } catch (e) {
108
+ // The entry's own selector was checked when the document was parsed, so this is the
109
+ // block's stored reference being unreadable.
110
+ return `The block resolved for this entry declares an unreadable kind: ${ensureError(e).message}`;
111
+ }
112
+
113
+ if (wanted.name !== has.name) {
114
+ return (
115
+ `This entry asks for kind '${wanted.name}', but the block resolved for it ` +
116
+ `implements '${has.name}'`
117
+ );
118
+ }
119
+
120
+ if (!semver.satisfies(has.version, selectorToRange(wanted.selector))) {
121
+ return (
122
+ `This entry asks for '${asked}', but the block resolved for it implements ` +
123
+ `version ${has.version} of that kind`
124
+ );
125
+ }
126
+
127
+ return undefined;
128
+ }