@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
@@ -0,0 +1,190 @@
1
+ import type { BlockPackId } from "@milaboratories/pl-model-middle-layer";
2
+ import { AnyChannel, StableChannel } from "@milaboratories/pl-model-middle-layer";
3
+ import * as semver from "semver";
4
+ import type { KindOverview } from "./schema_kinds";
5
+
6
+ /**
7
+ * PURE, IO-free block-kind resolution core.
8
+ *
9
+ * Given a parsed {@link KindOverview} projection (fetched by the reader) and a
10
+ * version selector, this module answers "which concrete implementing block
11
+ * satisfies `{name}@{selector}`?" with a discriminated {@link KindResolution}.
12
+ *
13
+ * Everything here is fed literals in unit tests — no `FolderReader`, no cache,
14
+ * no `semver` use beyond range math. This is the net-new, correctness-critical
15
+ * seam (implementation-path §6): selector→range translation and channel
16
+ * selection each have edges worth isolating and testing.
17
+ *
18
+ * Naming: this module never parses the `{name}@{version}` *reference* string
19
+ * (that codec is `parseKindRef`, block_kind_ref.ts) nor the npm-name→path split
20
+ * (`npmNameToKindPath`, schema_kinds.ts). It consumes an already-split selector.
21
+ */
22
+
23
+ /**
24
+ * The three version-selector tiers of the redefined-semver scheme
25
+ * (01-kind-and-lifecycle.md:63):
26
+ * - `exact` — `@X.Y.Z`, pin to one version.
27
+ * - `patch` — `~X.Y.Z`, behavior frozen (patch floats).
28
+ * - `minor` — `^X.Y.Z`, behavior floats (minor floats).
29
+ */
30
+ export type SelectorOp = "exact" | "patch" | "minor";
31
+
32
+ export interface Selector {
33
+ op: SelectorOp;
34
+ version: string;
35
+ }
36
+
37
+ /**
38
+ * Split a raw selector string into its {@link Selector} operator + version.
39
+ *
40
+ * The leading character encodes the tier: `~`→patch, `^`→minor, `@`→exact.
41
+ * A bare version (no recognized leading operator) is treated as `exact`. Note
42
+ * that `@` as a *leading* char is redundant with the bare form and accepted
43
+ * only because the reference form is `{name}@{version}` — after `parseKindRef`
44
+ * splits on the final `@`, the version half is normally bare or `~`/`^`-prefixed.
45
+ */
46
+ export function parseSelector(raw: string): Selector {
47
+ const s = raw.trim();
48
+ switch (s[0]) {
49
+ case "~":
50
+ return { op: "patch", version: s.slice(1) };
51
+ case "^":
52
+ return { op: "minor", version: s.slice(1) };
53
+ case "@":
54
+ return { op: "exact", version: s.slice(1) };
55
+ default:
56
+ return { op: "exact", version: s };
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Map a {@link Selector} to a semver range string:
62
+ * - `exact` — `=X.Y.Z`
63
+ * - `patch` — `>=X.Y.Z <X.(Y+1).0-0`, behavior frozen, params float
64
+ * - `minor` — `>=X.Y.Z <(X+1).0.0-0`, behavior floats, never crosses a params-break
65
+ *
66
+ * The kind scheme redefines the *meaning* of major/minor/patch, not the range
67
+ * arithmetic — so the arithmetic is written out here rather than delegated to
68
+ * npm's `~`/`^` operators, which are not major-agnostic. Below 1.0.0 npm expands
69
+ * both `^0.2.3` and `~0.2.3` to `>=0.2.3 <0.3.0-0`, treating a `0.x` minor as the
70
+ * breaking boundary per ecosystem convention. Under the kind scheme a
71
+ * params-break is a MAJOR bump at every major including zero, and nothing
72
+ * guarantees a kind starts at `1.0.0`, so borrowing `^` would silently collapse
73
+ * `patch` and `minor` into one tier for any `0.x` kind — erasing the distinction
74
+ * the whole scheme rests on.
75
+ *
76
+ * For `X >= 1` these forms are equivalent to `~`/`^`, down to npm's `-0`
77
+ * upper-bound suffix that keeps prereleases of the boundary out of range.
78
+ *
79
+ * @throws if `sel.version` is not a valid semver version. {@link parseSelector}
80
+ * does not validate it; the model-side `parseKindSelector` does.
81
+ */
82
+ export function selectorToRange(sel: Selector): string {
83
+ if (sel.op === "exact") return `=${sel.version}`;
84
+ const major = semver.major(sel.version);
85
+ const minor = semver.minor(sel.version);
86
+ const upperBound = sel.op === "patch" ? `${major}.${minor + 1}.0` : `${major + 1}.0.0`;
87
+ return `>=${sel.version} <${upperBound}-0`;
88
+ }
89
+
90
+ /**
91
+ * Outcome of resolving a kind reference against its overview projection.
92
+ *
93
+ * The three failure reasons are distinct on purpose (the caller maps each to a
94
+ * different user-facing error):
95
+ * - `no-matching-kind-version` — the selector satisfies zero kind versions
96
+ * (`maxSatisfying` returned `null`).
97
+ * - `no-implementation` — a kind version matched, but it has no implementing
98
+ * block at all.
99
+ * - `no-stable-implementation` — implementing blocks exist for the matched
100
+ * kind version, but none is on the `stable` channel and `allowUnstable` is
101
+ * off.
102
+ */
103
+ export type KindResolution =
104
+ | { ok: true; blockId: BlockPackId; channel: string }
105
+ | { ok: false; reason: "no-matching-kind-version" }
106
+ | { ok: false; reason: "no-implementation" }
107
+ | { ok: false; reason: "no-stable-implementation" };
108
+
109
+ export type KindResolutionReason = Extract<KindResolution, { ok: false }>["reason"];
110
+
111
+ /**
112
+ * Typed error the reader/facade throw when {@link resolveKind} fails, carrying
113
+ * the discriminated {@link KindResolutionReason} and the offending reference so
114
+ * a caller can map it to a spec-level error without re-deriving the reason.
115
+ */
116
+ export class KindResolutionError extends Error {
117
+ constructor(
118
+ public readonly reason: KindResolutionReason,
119
+ public readonly ref: string,
120
+ ) {
121
+ super(`Cannot resolve block kind "${ref}": ${reason}`);
122
+ this.name = "KindResolutionError";
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Newest implementing block for `(kindVersion, channel)` from the flat
128
+ * `implementers` list — the RMW source of truth. Newest by *block* version via
129
+ * `semver.gt`. Under `allowUnstable` every implementer of the kind version is
130
+ * eligible regardless of channel membership (the `AnyChannel` view); otherwise
131
+ * only implementers published to `channel` count.
132
+ */
133
+ function pickNewestImplementer(
134
+ implementers: KindOverview["implementers"],
135
+ kindVersion: string,
136
+ channel: string,
137
+ allowUnstable: boolean,
138
+ ): BlockPackId | undefined {
139
+ const candidates = implementers.filter(
140
+ (i) => semver.eq(i.kindVersion, kindVersion) && (allowUnstable || i.channels.includes(channel)),
141
+ );
142
+ if (candidates.length === 0) return undefined;
143
+ return candidates.reduce((a, b) => (semver.gt(b.id.version, a.id.version) ? b : a)).id;
144
+ }
145
+
146
+ /**
147
+ * Resolve a kind reference to a concrete implementing block.
148
+ *
149
+ * 1. Pick the newest kind version satisfying the selector via
150
+ * `semver.maxSatisfying` over the overview's `kindVersions`.
151
+ * 2. Select the target channel: `stable` by default, `any` when `allowUnstable`
152
+ * is set (the apply-time flag).
153
+ * 3. Pick the newest implementing block in that `(version, channel)` via
154
+ * `semver.gt`, reading the flat `implementers` source of truth and falling
155
+ * back to the derived `latestByChannel` projection for older overviews.
156
+ *
157
+ * @param overview parsed & normalized kind projection (reader-supplied).
158
+ * @param selector either a raw selector string (`^1.2.3`, `~1.2.3`, `1.2.3`)
159
+ * or a pre-parsed {@link Selector}.
160
+ */
161
+ export function resolveKind(
162
+ overview: KindOverview,
163
+ selector: string | Selector,
164
+ opt: { allowUnstable: boolean },
165
+ ): KindResolution {
166
+ const sel = typeof selector === "string" ? parseSelector(selector) : selector;
167
+ const range = selectorToRange(sel);
168
+
169
+ const versions = overview.kindVersions.map((v) => v.kindVersion);
170
+ const picked = semver.maxSatisfying(versions, range);
171
+ if (picked === null) return { ok: false, reason: "no-matching-kind-version" };
172
+
173
+ const entry = overview.kindVersions.find((v) => semver.eq(v.kindVersion, picked));
174
+ const channel = opt.allowUnstable ? AnyChannel : StableChannel;
175
+
176
+ const blockId =
177
+ pickNewestImplementer(overview.implementers, picked, channel, opt.allowUnstable) ??
178
+ entry?.latestByChannel[channel];
179
+ if (blockId) return { ok: true, blockId, channel };
180
+
181
+ // Nothing in the target channel. Distinguish "no implementer at all" from
182
+ // "implementers exist but none stable". When `allowUnstable` is set the target
183
+ // channel is `any`, so an empty result here always means zero implementers.
184
+ const hasAnyImpl =
185
+ overview.implementers.some((i) => semver.eq(i.kindVersion, picked)) ||
186
+ (entry !== undefined && Object.keys(entry.latestByChannel).length > 0);
187
+
188
+ if (!hasAnyImpl) return { ok: false, reason: "no-implementation" };
189
+ return { ok: false, reason: "no-stable-implementation" };
190
+ }
@@ -4,9 +4,15 @@ import { randomUUID } from "crypto";
4
4
  import path from "path";
5
5
  import fsp from "fs/promises";
6
6
  import { BlockRegistryV2 } from "./registry";
7
- import { UpdateSuggestions, BlockPackManifest } from "@milaboratories/pl-model-middle-layer";
7
+ import {
8
+ UpdateSuggestions,
9
+ BlockPackManifest,
10
+ StableChannel,
11
+ } from "@milaboratories/pl-model-middle-layer";
8
12
  import { inferUpdateSuggestions } from "./registry_reader";
9
13
  import { OverviewSnapshotsPrefix } from "./schema_internal";
14
+ import { KindOverview, kindOverviewPath, npmNameToKindPath } from "./schema_kinds";
15
+ import { resolveKind } from "./kind_resolver";
10
16
 
11
17
  type TestStorageInstance = {
12
18
  storage: RegistryStorage;
@@ -260,6 +266,246 @@ test.each(testStorages)(
260
266
  },
261
267
  );
262
268
 
269
+ /**
270
+ * Fixture for the kind-projection test: a block version whose description
271
+ * carries a `{name}@{version}` kind reference. Only the id and that reference
272
+ * matter to the reconciler's kind pass.
273
+ */
274
+ const kindTestKindNpmName = "@platforma-open/kindorg.kindpkg.kind";
275
+ /** A second, unrelated kind — the migration target in the kind-change test. */
276
+ const kindTestOtherKindNpmName = "@platforma-open/kindorg.otherpkg.kind";
277
+
278
+ function kindImplementingManifest(
279
+ version: string,
280
+ kindVersion: string,
281
+ kindNpmName: string = kindTestKindNpmName,
282
+ ): BlockPackManifest {
283
+ return BlockPackManifest.parse({
284
+ schema: "v2",
285
+ description: {
286
+ id: { organization: "testorg", name: "kindblk", version },
287
+ title: "Kind implementing block",
288
+ summary: "Test package",
289
+ kind: `${kindNpmName}@${kindVersion}`,
290
+ components: {
291
+ workflow: { type: "workflow-v1", main: { type: "relative", path: "workflow.json" } },
292
+ model: { type: "relative", path: "model.json" },
293
+ ui: { type: "relative", path: "ui.json" },
294
+ },
295
+ meta: {
296
+ title: "Kind implementing block",
297
+ description: "Test package description",
298
+ organization: { name: "Test Organization", url: "https://test.com" },
299
+ tags: [],
300
+ },
301
+ },
302
+ files: [
303
+ {
304
+ name: "model.json",
305
+ size: 13,
306
+ sha256: "6FD977DB9B2AFE87A9CEEE48432881299A6AAF83D935FBBE83007660287F9C2E",
307
+ },
308
+ ],
309
+ });
310
+ }
311
+
312
+ const kindTestFileReader = async (fileName: string) => {
313
+ if (fileName === "model.json") return Buffer.from('{"test":true}');
314
+ throw new Error(`Unknown file: ${fileName}`);
315
+ };
316
+
317
+ /**
318
+ * Channel membership is per *block version* (`v2/{org}/{name}/{version}/channels/{channel}`),
319
+ * and every writer of a channel marker drops an update seed for that same
320
+ * version. So a `normal` (read-modify-write) reconcile pass always re-reads the
321
+ * versions whose membership changed, and the per-entry `channels` snapshot
322
+ * stored in `kinds/{org}/{name}/overview.json` cannot go stale behind it.
323
+ *
324
+ * This test pins that invariant on both sides of a stable-channel handover:
325
+ * adding `stable` to a newer version (both versions stable — marking is not a
326
+ * moving pointer, so the older one keeps its own membership) and then removing
327
+ * `stable` from the older one. Either step must leave every entry's snapshot
328
+ * equal to the markers actually present in storage, and stable resolution must
329
+ * follow. Regression guard: any future change that mutates a marker without
330
+ * seeding that exact version — or that turns `stable` into a single moving
331
+ * pointer — leaves the kind index asserting a version is stable when it is not.
332
+ */
333
+ test.each(testStorages)(
334
+ "channel handover keeps kind overview channel snapshots correct in normal mode with $name",
335
+ async ({ storageProvider }) => {
336
+ const { storage, teardown } = storageProvider();
337
+ const registry = new BlockRegistryV2(storage, undefined, { skipSnapshotCreation: true });
338
+
339
+ const ovPath = kindOverviewPath(npmNameToKindPath(kindTestKindNpmName));
340
+ const readKindOverview = async (): Promise<KindOverview> => {
341
+ const content = await storage.getFile(ovPath);
342
+ expect(content).toBeDefined();
343
+ return KindOverview.parse(JSON.parse(content!.toString()));
344
+ };
345
+ const channelsOf = (overview: KindOverview, version: string): string[] => {
346
+ const entry = overview.implementers.find((i) => i.id.version === version);
347
+ expect(entry, `no implementer entry for ${version}`).toBeDefined();
348
+ return [...entry!.channels].sort();
349
+ };
350
+ const stableResolution = (overview: KindOverview) =>
351
+ resolveKind(overview, "1.0.0", { allowUnstable: false });
352
+
353
+ try {
354
+ // Two versions of one block, both implementing kind 1.0.0.
355
+ const a = kindImplementingManifest("1.0.0", "1.0.0");
356
+ const b = kindImplementingManifest("2.0.0", "1.0.0");
357
+ await registry.publishPackage(a, kindTestFileReader);
358
+ await registry.publishPackage(b, kindTestFileReader);
359
+
360
+ // A stable, reconcile.
361
+ await registry.addPackageToChannel(a.description.id, StableChannel);
362
+ await registry.updateIfNeeded("normal");
363
+
364
+ let overview = await readKindOverview();
365
+ expect(channelsOf(overview, "1.0.0")).toEqual([StableChannel]);
366
+ expect(channelsOf(overview, "2.0.0")).toEqual([]);
367
+ expect(stableResolution(overview)).toEqual({
368
+ ok: true,
369
+ blockId: a.description.id,
370
+ channel: StableChannel,
371
+ });
372
+
373
+ // B stable, reconcile in normal mode. A keeps its own marker (marking is
374
+ // additive per version), so both entries must read stable and resolution
375
+ // must move to the newer version.
376
+ await registry.addPackageToChannel(b.description.id, StableChannel);
377
+ await registry.updateIfNeeded("normal");
378
+
379
+ overview = await readKindOverview();
380
+ expect(channelsOf(overview, "1.0.0")).toEqual([StableChannel]);
381
+ expect(channelsOf(overview, "2.0.0")).toEqual([StableChannel]);
382
+ expect(await storage.listFiles(`v2/testorg/kindblk/1.0.0/channels/`)).toEqual([
383
+ StableChannel,
384
+ ]);
385
+ expect(stableResolution(overview)).toEqual({
386
+ ok: true,
387
+ blockId: b.description.id,
388
+ channel: StableChannel,
389
+ });
390
+
391
+ // Now the losing side of a real handover: drop stable from A. The version
392
+ // losing membership is the one seeded, so a normal pass must clear its
393
+ // snapshot rather than carry the stale `["stable"]`.
394
+ await registry.removePackageFromChannel(a.description.id, StableChannel);
395
+ await registry.updateIfNeeded("normal");
396
+
397
+ overview = await readKindOverview();
398
+ expect(channelsOf(overview, "1.0.0")).toEqual([]);
399
+ expect(channelsOf(overview, "2.0.0")).toEqual([StableChannel]);
400
+ expect(await storage.listFiles(`v2/testorg/kindblk/1.0.0/channels/`)).toEqual([]);
401
+ expect(stableResolution(overview)).toEqual({
402
+ ok: true,
403
+ blockId: b.description.id,
404
+ channel: StableChannel,
405
+ });
406
+
407
+ // Dropping the last stable marker must make the kind unresolvable on the
408
+ // stable channel — not silently fall back to a no-longer-stable version.
409
+ await registry.removePackageFromChannel(b.description.id, StableChannel);
410
+ await registry.updateIfNeeded("normal");
411
+
412
+ overview = await readKindOverview();
413
+ expect(channelsOf(overview, "1.0.0")).toEqual([]);
414
+ expect(channelsOf(overview, "2.0.0")).toEqual([]);
415
+ expect(stableResolution(overview)).toEqual({
416
+ ok: false,
417
+ reason: "no-stable-implementation",
418
+ });
419
+ expect(resolveKind(overview, "1.0.0", { allowUnstable: true })).toEqual({
420
+ ok: true,
421
+ blockId: b.description.id,
422
+ channel: "any",
423
+ });
424
+ } finally {
425
+ await teardown();
426
+ }
427
+ },
428
+ );
429
+
430
+ /**
431
+ * A block version can change which kind it implements: `publishPackage` overwrites
432
+ * same-version content (unlike `publishKind`, which is source-hash guarded), so a
433
+ * re-publish can move `{org}:{name}:{version}` from kind A to kind B.
434
+ *
435
+ * A `normal` pass only ever reads the CURRENT manifest, which names B — so nothing
436
+ * in the fresh scan points at A. Without an explicit visit A keeps listing the block
437
+ * as its implementer, and `resolveKind(A)` answers with a block whose manifest
438
+ * implements B: a mismatch that surfaces only at apply time, as rejected params.
439
+ * The reconciler recovers the old ref from the package overview it already loaded.
440
+ *
441
+ * Both halves are pinned here: A survives, minus the migrated version, while it
442
+ * still has an implementer — and is deleted once the last one leaves.
443
+ */
444
+ test.each(testStorages)(
445
+ "changing a block's kind clears its old kind overview in normal mode with $name",
446
+ async ({ storageProvider }) => {
447
+ const { storage, teardown } = storageProvider();
448
+ const registry = new BlockRegistryV2(storage, undefined, { skipSnapshotCreation: true });
449
+
450
+ const ovPathA = kindOverviewPath(npmNameToKindPath(kindTestKindNpmName));
451
+ const ovPathB = kindOverviewPath(npmNameToKindPath(kindTestOtherKindNpmName));
452
+ const readOverview = async (ovPath: string): Promise<KindOverview | undefined> => {
453
+ const content = await storage.getFile(ovPath);
454
+ return content === undefined ? undefined : KindOverview.parse(JSON.parse(content.toString()));
455
+ };
456
+ const implementerVersions = (overview: KindOverview | undefined): string[] =>
457
+ (overview?.implementers ?? []).map((i) => i.id.version).sort();
458
+
459
+ try {
460
+ // Two versions of one block, both implementing kind A, both stable.
461
+ const a1 = kindImplementingManifest("1.0.0", "1.0.0");
462
+ const a2 = kindImplementingManifest("2.0.0", "1.0.0");
463
+ await registry.publishPackage(a1, kindTestFileReader);
464
+ await registry.publishPackage(a2, kindTestFileReader);
465
+ await registry.addPackageToChannel(a1.description.id, StableChannel);
466
+ await registry.addPackageToChannel(a2.description.id, StableChannel);
467
+ await registry.updateIfNeeded("normal");
468
+
469
+ expect(implementerVersions(await readOverview(ovPathA))).toEqual(["1.0.0", "2.0.0"]);
470
+ expect(await readOverview(ovPathB)).toBeUndefined();
471
+
472
+ // Re-publish 1.0.0 against kind B — same block id, different kind.
473
+ const b1 = kindImplementingManifest("1.0.0", "1.0.0", kindTestOtherKindNpmName);
474
+ await registry.publishPackage(b1, kindTestFileReader);
475
+ await registry.updateIfNeeded("normal");
476
+
477
+ const overviewA = await readOverview(ovPathA);
478
+ const overviewB = await readOverview(ovPathB);
479
+ expect(implementerVersions(overviewA)).toEqual(["2.0.0"]);
480
+ expect(implementerVersions(overviewB)).toEqual(["1.0.0"]);
481
+ // Each kind must resolve to the block that actually implements it now.
482
+ expect(resolveKind(overviewA!, "1.0.0", { allowUnstable: false })).toEqual({
483
+ ok: true,
484
+ blockId: a2.description.id,
485
+ channel: StableChannel,
486
+ });
487
+ expect(resolveKind(overviewB!, "1.0.0", { allowUnstable: false })).toEqual({
488
+ ok: true,
489
+ blockId: b1.description.id,
490
+ channel: StableChannel,
491
+ });
492
+
493
+ // Migrate the last implementer off A: its overview is orphaned and removed.
494
+ const b2 = kindImplementingManifest("2.0.0", "1.0.0", kindTestOtherKindNpmName);
495
+ await registry.publishPackage(b2, kindTestFileReader);
496
+ await registry.updateIfNeeded("normal");
497
+
498
+ expect(await readOverview(ovPathA)).toBeUndefined();
499
+ expect(implementerVersions(await readOverview(ovPathB))).toEqual(["1.0.0", "2.0.0"]);
500
+ expect(
501
+ resolveKind((await readOverview(ovPathB))!, "1.0.0", { allowUnstable: false }),
502
+ ).toEqual({ ok: true, blockId: b2.description.id, channel: StableChannel });
503
+ } finally {
504
+ await teardown();
505
+ }
506
+ },
507
+ );
508
+
263
509
  test.each([
264
510
  {
265
511
  name: "test1",