@milaboratories/pl-middle-layer 1.69.2 → 1.70.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.
@@ -1,7 +1,7 @@
1
1
  //#region src/middle_layer/build_stamp.ts
2
2
  function injectedStamp() {
3
3
  try {
4
- return "2f8b0ce483fa-dirty-1788868576066";
4
+ return "7f35d01a1aab-dirty-1788877949708";
5
5
  } catch {
6
6
  return;
7
7
  }
@@ -1,7 +1,7 @@
1
1
  //#region src/middle_layer/build_stamp.ts
2
2
  function injectedStamp() {
3
3
  try {
4
- return "2f8b0ce483fa-dirty-1788868576042";
4
+ return "7f35d01a1aab-dirty-1788877949682";
5
5
  } catch {
6
6
  return;
7
7
  }
@@ -36,11 +36,12 @@ const require_project_model_util = require("./project_model_util.cjs");
36
36
  function walkProjectForTemplateExport(structure, paramsProvider) {
37
37
  const entries = [];
38
38
  const problems = [];
39
- for (const { id } of require_project_model_util.allBlocks(structure)) {
39
+ for (const { id, label } of require_project_model_util.allBlocks(structure)) {
40
40
  const derived = paramsProvider(id);
41
41
  if (derived === void 0) {
42
42
  problems.push({
43
43
  blockId: id,
44
+ blockLabel: label,
44
45
  error: "Block state is unavailable, so its template params could not be derived"
45
46
  });
46
47
  continue;
@@ -48,6 +49,7 @@ function walkProjectForTemplateExport(structure, paramsProvider) {
48
49
  if (derived.error !== void 0) {
49
50
  problems.push({
50
51
  blockId: id,
52
+ blockLabel: label,
51
53
  error: derived.error
52
54
  });
53
55
  continue;
@@ -56,12 +58,14 @@ function walkProjectForTemplateExport(structure, paramsProvider) {
56
58
  if (typeof params !== "object" || params === null || Array.isArray(params)) {
57
59
  problems.push({
58
60
  blockId: id,
61
+ blockLabel: label,
59
62
  error: `templateParams() must return an object, got ${typeName(params)}`
60
63
  });
61
64
  continue;
62
65
  }
63
66
  entries.push({
64
67
  blockId: id,
68
+ blockLabel: label,
65
69
  params
66
70
  });
67
71
  }
@@ -1 +1 @@
1
- {"version":3,"file":"template_export.cjs","names":["allBlocks"],"sources":["../../src/model/template_export.ts"],"sourcesContent":["import type { ProjectStructure } from \"./project_model\";\nimport { allBlocks } from \"./project_model_util\";\n\n/**\n * One block's template-descriptor output as the walk receives it.\n *\n * Deliberately the same shape the `__pl_initializationParams_derive` facade callback\n * returns, so a provider can hand the VM's result straight through without\n * reshaping it.\n */\nexport type TemplateParamsResult =\n | { readonly error: string }\n | { readonly error?: undefined; readonly value: unknown };\n\n/** One block's contribution to the template being exported. */\nexport type TemplateExportEntry = {\n /**\n * The block's project-local id, which is also its template-local id: a template\n * has no id namespace of its own, so the id is reused verbatim and references\n * already stored in params need no translation.\n */\n readonly blockId: string;\n /**\n * The block's params exactly as it projected them.\n *\n * Always a mapping: a block that declared no `templateParams`, or whose lambda returned\n * something else, is reported as a problem rather than carried here — see the object check\n * in the walk. So nothing downstream has an absent case to decide.\n */\n readonly params: Record<string, unknown>;\n};\n\n/** Why one block could not be exported. */\nexport type TemplateExportProblem = {\n readonly blockId: string;\n readonly error: string;\n};\n\n/**\n * Outcome of the walk: the blocks that can be written, and the ones that cannot.\n *\n * Both lists are returned rather than throwing on the first failure, so the\n * caller can report every offending block at once instead of making the user fix\n * them one export at a time. Whether a non-empty `problems` aborts the export is\n * the caller's policy, not the walk's — but note that emitting `entries` while\n * ignoring `problems` can produce a file whose surviving entries reference a\n * dropped block, which is an unusable template.\n */\nexport type TemplateExportWalk = {\n readonly entries: readonly TemplateExportEntry[];\n readonly problems: readonly TemplateExportProblem[];\n};\n\n/**\n * Walk a project's blocks in dependency order, collecting each one's\n * template-descriptor output.\n *\n * **No topological sort is performed, because none is needed.** The project\n * structure is already stored in topological order, and that is enforced rather\n * than assumed: `productionGraph` traverses `allBlocks(structure)` and passes the\n * set of blocks seen *so far* as the allowed set to `inferAllReferencedBlocks`, so\n * a reference to a block that is not already above is recorded as a missing\n * reference instead of an upstream. A block can therefore only legally reference\n * blocks earlier in this sequence — which is exactly what a template file needs,\n * since its block order is the instantiation order and the engine creates blocks\n * upstream-first. Emitting entries in structure order satisfies that for free.\n *\n * Groups are flattened in order, so cross-group ordering is the structure's too.\n *\n * A structure that violates the ordering rule is reported as-is, not repaired:\n * reordering would change which references are legal in the first place.\n *\n * Params are written exactly as the block projected them. The walk parses nothing, rewrites\n * nothing and inspects nothing inside them, and neither does anything else between here and the\n * file — which values carry block ids is knowledge of the reference system, and a template\n * engine holds none of it. The block that receives these params on the way back in is what\n * recognizes them; a block that projects the wrong fields produces a template that does not\n * work, the same way one whose `templateParams` returns the wrong shape does.\n *\n * @param structure The project structure — the source of both membership and order\n * @param paramsProvider Yields a block's derived template params. Return\n * `undefined` for a block whose state cannot be read at all; such a block is\n * recorded as a problem rather than skipped, because a template that quietly\n * omits a block does not describe the project it was exported from, and the\n * surviving entries may still reference the omitted one.\n */\nexport function walkProjectForTemplateExport(\n structure: ProjectStructure,\n paramsProvider: (blockId: string) => TemplateParamsResult | undefined,\n): TemplateExportWalk {\n const entries: TemplateExportEntry[] = [];\n const problems: TemplateExportProblem[] = [];\n\n for (const { id } of allBlocks(structure)) {\n const derived = paramsProvider(id);\n\n if (derived === undefined) {\n problems.push({\n blockId: id,\n error: \"Block state is unavailable, so its template params could not be derived\",\n });\n continue;\n }\n\n if (derived.error !== undefined) {\n problems.push({ blockId: id, error: derived.error });\n continue;\n }\n\n const params = derived.value;\n\n // An entry's `params` must be a mapping. The lambda's declared return type is the\n // block kind's params type, and the kind's parser checks values coming IN, but\n // nothing checks what the lambda hands back on the way out — so a block whose\n // params type is a primitive or a tuple compiles fine and would produce an\n // unwritable entry. This is the only place that can catch it.\n if (typeof params !== \"object\" || params === null || Array.isArray(params)) {\n problems.push({\n blockId: id,\n error: `templateParams() must return an object, got ${typeName(params)}`,\n });\n continue;\n }\n\n entries.push({ blockId: id, params: params as Record<string, unknown> });\n }\n\n return { entries, problems };\n}\n\n/** Name the offending value's type for an error message, without printing the value. */\nfunction typeName(value: unknown): string {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"an array\";\n return `a ${typeof value}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsFA,SAAgB,6BACd,WACA,gBACoB;CACpB,MAAM,UAAiC,CAAC;CACxC,MAAM,WAAoC,CAAC;CAE3C,KAAK,MAAM,EAAE,QAAQA,2BAAAA,UAAU,SAAS,GAAG;EACzC,MAAM,UAAU,eAAe,EAAE;EAEjC,IAAI,YAAY,KAAA,GAAW;GACzB,SAAS,KAAK;IACZ,SAAS;IACT,OAAO;GACT,CAAC;GACD;EACF;EAEA,IAAI,QAAQ,UAAU,KAAA,GAAW;GAC/B,SAAS,KAAK;IAAE,SAAS;IAAI,OAAO,QAAQ;GAAM,CAAC;GACnD;EACF;EAEA,MAAM,SAAS,QAAQ;EAOvB,IAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,MAAM,GAAG;GAC1E,SAAS,KAAK;IACZ,SAAS;IACT,OAAO,+CAA+C,SAAS,MAAM;GACvE,CAAC;GACD;EACF;EAEA,QAAQ,KAAK;GAAE,SAAS;GAAY;EAAkC,CAAC;CACzE;CAEA,OAAO;EAAE;EAAS;CAAS;AAC7B;;AAGA,SAAS,SAAS,OAAwB;CACxC,IAAI,UAAU,MAAM,OAAO;CAC3B,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO;CACjC,OAAO,KAAK,OAAO;AACrB"}
1
+ {"version":3,"file":"template_export.cjs","names":["allBlocks"],"sources":["../../src/model/template_export.ts"],"sourcesContent":["import type { ProjectStructure } from \"./project_model\";\nimport { allBlocks } from \"./project_model_util\";\n\n/**\n * One block's template-descriptor output as the walk receives it.\n *\n * Deliberately the same shape the `__pl_initializationParams_derive` facade callback\n * returns, so a provider can hand the VM's result straight through without\n * reshaping it.\n */\nexport type TemplateParamsResult =\n | { readonly error: string }\n | { readonly error?: undefined; readonly value: unknown };\n\n/** One block's contribution to the template being exported. */\nexport type TemplateExportEntry = {\n /**\n * The block's project-local id, which is also its template-local id: a template\n * has no id namespace of its own, so the id is reused verbatim and references\n * already stored in params need no translation.\n */\n readonly blockId: string;\n /** The block's label from the project structure, carried so a problem found downstream can\n * name the block to a person. Not written to the template. */\n readonly blockLabel: string;\n /**\n * The block's params exactly as it projected them.\n *\n * Always a mapping: a block that declared no `templateParams`, or whose lambda returned\n * something else, is reported as a problem rather than carried here — see the object check\n * in the walk. So nothing downstream has an absent case to decide.\n */\n readonly params: Record<string, unknown>;\n};\n\n/** Why one block could not be exported. */\nexport type TemplateExportProblem = {\n readonly blockId: string;\n /** The block's label as the project structure holds it. An id means nothing to the person who\n * pressed Export; this is what a UI shows. */\n readonly blockLabel: string;\n readonly error: string;\n};\n\n/**\n * Outcome of the walk: the blocks that can be written, and the ones that cannot.\n *\n * Both lists are returned rather than throwing on the first failure, so the\n * caller can report every offending block at once instead of making the user fix\n * them one export at a time. Whether a non-empty `problems` aborts the export is\n * the caller's policy, not the walk's — but note that emitting `entries` while\n * ignoring `problems` can produce a file whose surviving entries reference a\n * dropped block, which is an unusable template.\n */\nexport type TemplateExportWalk = {\n readonly entries: readonly TemplateExportEntry[];\n readonly problems: readonly TemplateExportProblem[];\n};\n\n/**\n * Walk a project's blocks in dependency order, collecting each one's\n * template-descriptor output.\n *\n * **No topological sort is performed, because none is needed.** The project\n * structure is already stored in topological order, and that is enforced rather\n * than assumed: `productionGraph` traverses `allBlocks(structure)` and passes the\n * set of blocks seen *so far* as the allowed set to `inferAllReferencedBlocks`, so\n * a reference to a block that is not already above is recorded as a missing\n * reference instead of an upstream. A block can therefore only legally reference\n * blocks earlier in this sequence — which is exactly what a template file needs,\n * since its block order is the instantiation order and the engine creates blocks\n * upstream-first. Emitting entries in structure order satisfies that for free.\n *\n * Groups are flattened in order, so cross-group ordering is the structure's too.\n *\n * A structure that violates the ordering rule is reported as-is, not repaired:\n * reordering would change which references are legal in the first place.\n *\n * Params are written exactly as the block projected them. The walk parses nothing, rewrites\n * nothing and inspects nothing inside them, and neither does anything else between here and the\n * file — which values carry block ids is knowledge of the reference system, and a template\n * engine holds none of it. The block that receives these params on the way back in is what\n * recognizes them; a block that projects the wrong fields produces a template that does not\n * work, the same way one whose `templateParams` returns the wrong shape does.\n *\n * @param structure The project structure — the source of both membership and order\n * @param paramsProvider Yields a block's derived template params. Return\n * `undefined` for a block whose state cannot be read at all; such a block is\n * recorded as a problem rather than skipped, because a template that quietly\n * omits a block does not describe the project it was exported from, and the\n * surviving entries may still reference the omitted one.\n */\nexport function walkProjectForTemplateExport(\n structure: ProjectStructure,\n paramsProvider: (blockId: string) => TemplateParamsResult | undefined,\n): TemplateExportWalk {\n const entries: TemplateExportEntry[] = [];\n const problems: TemplateExportProblem[] = [];\n\n for (const { id, label } of allBlocks(structure)) {\n const derived = paramsProvider(id);\n\n if (derived === undefined) {\n problems.push({\n blockId: id,\n blockLabel: label,\n error: \"Block state is unavailable, so its template params could not be derived\",\n });\n continue;\n }\n\n if (derived.error !== undefined) {\n problems.push({ blockId: id, blockLabel: label, error: derived.error });\n continue;\n }\n\n const params = derived.value;\n\n // An entry's `params` must be a mapping. The lambda's declared return type is the\n // block kind's params type, and the kind's parser checks values coming IN, but\n // nothing checks what the lambda hands back on the way out — so a block whose\n // params type is a primitive or a tuple compiles fine and would produce an\n // unwritable entry. This is the only place that can catch it.\n if (typeof params !== \"object\" || params === null || Array.isArray(params)) {\n problems.push({\n blockId: id,\n blockLabel: label,\n error: `templateParams() must return an object, got ${typeName(params)}`,\n });\n continue;\n }\n\n entries.push({ blockId: id, blockLabel: label, params: params as Record<string, unknown> });\n }\n\n return { entries, problems };\n}\n\n/** Name the offending value's type for an error message, without printing the value. */\nfunction typeName(value: unknown): string {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"an array\";\n return `a ${typeof value}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4FA,SAAgB,6BACd,WACA,gBACoB;CACpB,MAAM,UAAiC,CAAC;CACxC,MAAM,WAAoC,CAAC;CAE3C,KAAK,MAAM,EAAE,IAAI,WAAWA,2BAAAA,UAAU,SAAS,GAAG;EAChD,MAAM,UAAU,eAAe,EAAE;EAEjC,IAAI,YAAY,KAAA,GAAW;GACzB,SAAS,KAAK;IACZ,SAAS;IACT,YAAY;IACZ,OAAO;GACT,CAAC;GACD;EACF;EAEA,IAAI,QAAQ,UAAU,KAAA,GAAW;GAC/B,SAAS,KAAK;IAAE,SAAS;IAAI,YAAY;IAAO,OAAO,QAAQ;GAAM,CAAC;GACtE;EACF;EAEA,MAAM,SAAS,QAAQ;EAOvB,IAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,MAAM,GAAG;GAC1E,SAAS,KAAK;IACZ,SAAS;IACT,YAAY;IACZ,OAAO,+CAA+C,SAAS,MAAM;GACvE,CAAC;GACD;EACF;EAEA,QAAQ,KAAK;GAAE,SAAS;GAAI,YAAY;GAAe;EAAkC,CAAC;CAC5F;CAEA,OAAO;EAAE;EAAS;CAAS;AAC7B;;AAGA,SAAS,SAAS,OAAwB;CACxC,IAAI,UAAU,MAAM,OAAO;CAC3B,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO;CACjC,OAAO,KAAK,OAAO;AACrB"}
@@ -3,6 +3,9 @@ import "./project_model.js";
3
3
  /** Why one block could not be exported. */
4
4
  export type TemplateExportProblem = {
5
5
  readonly blockId: string;
6
+ /** The block's label as the project structure holds it. An id means nothing to the person who
7
+ * pressed Export; this is what a UI shows. */
8
+ readonly blockLabel: string;
6
9
  readonly error: string;
7
10
  };
8
11
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"template_export.d.ts","names":[],"sources":["../../src/model/template_export.ts"],"mappings":";;;YAiCY;WACD;WACA"}
1
+ {"version":3,"file":"template_export.d.ts","names":[],"sources":["../../src/model/template_export.ts"],"mappings":";;;YAoCY;WACD;;;WAGA;WACA"}
@@ -36,11 +36,12 @@ import { allBlocks } from "./project_model_util.js";
36
36
  function walkProjectForTemplateExport(structure, paramsProvider) {
37
37
  const entries = [];
38
38
  const problems = [];
39
- for (const { id } of allBlocks(structure)) {
39
+ for (const { id, label } of allBlocks(structure)) {
40
40
  const derived = paramsProvider(id);
41
41
  if (derived === void 0) {
42
42
  problems.push({
43
43
  blockId: id,
44
+ blockLabel: label,
44
45
  error: "Block state is unavailable, so its template params could not be derived"
45
46
  });
46
47
  continue;
@@ -48,6 +49,7 @@ function walkProjectForTemplateExport(structure, paramsProvider) {
48
49
  if (derived.error !== void 0) {
49
50
  problems.push({
50
51
  blockId: id,
52
+ blockLabel: label,
51
53
  error: derived.error
52
54
  });
53
55
  continue;
@@ -56,12 +58,14 @@ function walkProjectForTemplateExport(structure, paramsProvider) {
56
58
  if (typeof params !== "object" || params === null || Array.isArray(params)) {
57
59
  problems.push({
58
60
  blockId: id,
61
+ blockLabel: label,
59
62
  error: `templateParams() must return an object, got ${typeName(params)}`
60
63
  });
61
64
  continue;
62
65
  }
63
66
  entries.push({
64
67
  blockId: id,
68
+ blockLabel: label,
65
69
  params
66
70
  });
67
71
  }
@@ -1 +1 @@
1
- {"version":3,"file":"template_export.js","names":[],"sources":["../../src/model/template_export.ts"],"sourcesContent":["import type { ProjectStructure } from \"./project_model\";\nimport { allBlocks } from \"./project_model_util\";\n\n/**\n * One block's template-descriptor output as the walk receives it.\n *\n * Deliberately the same shape the `__pl_initializationParams_derive` facade callback\n * returns, so a provider can hand the VM's result straight through without\n * reshaping it.\n */\nexport type TemplateParamsResult =\n | { readonly error: string }\n | { readonly error?: undefined; readonly value: unknown };\n\n/** One block's contribution to the template being exported. */\nexport type TemplateExportEntry = {\n /**\n * The block's project-local id, which is also its template-local id: a template\n * has no id namespace of its own, so the id is reused verbatim and references\n * already stored in params need no translation.\n */\n readonly blockId: string;\n /**\n * The block's params exactly as it projected them.\n *\n * Always a mapping: a block that declared no `templateParams`, or whose lambda returned\n * something else, is reported as a problem rather than carried here — see the object check\n * in the walk. So nothing downstream has an absent case to decide.\n */\n readonly params: Record<string, unknown>;\n};\n\n/** Why one block could not be exported. */\nexport type TemplateExportProblem = {\n readonly blockId: string;\n readonly error: string;\n};\n\n/**\n * Outcome of the walk: the blocks that can be written, and the ones that cannot.\n *\n * Both lists are returned rather than throwing on the first failure, so the\n * caller can report every offending block at once instead of making the user fix\n * them one export at a time. Whether a non-empty `problems` aborts the export is\n * the caller's policy, not the walk's — but note that emitting `entries` while\n * ignoring `problems` can produce a file whose surviving entries reference a\n * dropped block, which is an unusable template.\n */\nexport type TemplateExportWalk = {\n readonly entries: readonly TemplateExportEntry[];\n readonly problems: readonly TemplateExportProblem[];\n};\n\n/**\n * Walk a project's blocks in dependency order, collecting each one's\n * template-descriptor output.\n *\n * **No topological sort is performed, because none is needed.** The project\n * structure is already stored in topological order, and that is enforced rather\n * than assumed: `productionGraph` traverses `allBlocks(structure)` and passes the\n * set of blocks seen *so far* as the allowed set to `inferAllReferencedBlocks`, so\n * a reference to a block that is not already above is recorded as a missing\n * reference instead of an upstream. A block can therefore only legally reference\n * blocks earlier in this sequence — which is exactly what a template file needs,\n * since its block order is the instantiation order and the engine creates blocks\n * upstream-first. Emitting entries in structure order satisfies that for free.\n *\n * Groups are flattened in order, so cross-group ordering is the structure's too.\n *\n * A structure that violates the ordering rule is reported as-is, not repaired:\n * reordering would change which references are legal in the first place.\n *\n * Params are written exactly as the block projected them. The walk parses nothing, rewrites\n * nothing and inspects nothing inside them, and neither does anything else between here and the\n * file — which values carry block ids is knowledge of the reference system, and a template\n * engine holds none of it. The block that receives these params on the way back in is what\n * recognizes them; a block that projects the wrong fields produces a template that does not\n * work, the same way one whose `templateParams` returns the wrong shape does.\n *\n * @param structure The project structure — the source of both membership and order\n * @param paramsProvider Yields a block's derived template params. Return\n * `undefined` for a block whose state cannot be read at all; such a block is\n * recorded as a problem rather than skipped, because a template that quietly\n * omits a block does not describe the project it was exported from, and the\n * surviving entries may still reference the omitted one.\n */\nexport function walkProjectForTemplateExport(\n structure: ProjectStructure,\n paramsProvider: (blockId: string) => TemplateParamsResult | undefined,\n): TemplateExportWalk {\n const entries: TemplateExportEntry[] = [];\n const problems: TemplateExportProblem[] = [];\n\n for (const { id } of allBlocks(structure)) {\n const derived = paramsProvider(id);\n\n if (derived === undefined) {\n problems.push({\n blockId: id,\n error: \"Block state is unavailable, so its template params could not be derived\",\n });\n continue;\n }\n\n if (derived.error !== undefined) {\n problems.push({ blockId: id, error: derived.error });\n continue;\n }\n\n const params = derived.value;\n\n // An entry's `params` must be a mapping. The lambda's declared return type is the\n // block kind's params type, and the kind's parser checks values coming IN, but\n // nothing checks what the lambda hands back on the way out — so a block whose\n // params type is a primitive or a tuple compiles fine and would produce an\n // unwritable entry. This is the only place that can catch it.\n if (typeof params !== \"object\" || params === null || Array.isArray(params)) {\n problems.push({\n blockId: id,\n error: `templateParams() must return an object, got ${typeName(params)}`,\n });\n continue;\n }\n\n entries.push({ blockId: id, params: params as Record<string, unknown> });\n }\n\n return { entries, problems };\n}\n\n/** Name the offending value's type for an error message, without printing the value. */\nfunction typeName(value: unknown): string {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"an array\";\n return `a ${typeof value}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsFA,SAAgB,6BACd,WACA,gBACoB;CACpB,MAAM,UAAiC,CAAC;CACxC,MAAM,WAAoC,CAAC;CAE3C,KAAK,MAAM,EAAE,QAAQ,UAAU,SAAS,GAAG;EACzC,MAAM,UAAU,eAAe,EAAE;EAEjC,IAAI,YAAY,KAAA,GAAW;GACzB,SAAS,KAAK;IACZ,SAAS;IACT,OAAO;GACT,CAAC;GACD;EACF;EAEA,IAAI,QAAQ,UAAU,KAAA,GAAW;GAC/B,SAAS,KAAK;IAAE,SAAS;IAAI,OAAO,QAAQ;GAAM,CAAC;GACnD;EACF;EAEA,MAAM,SAAS,QAAQ;EAOvB,IAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,MAAM,GAAG;GAC1E,SAAS,KAAK;IACZ,SAAS;IACT,OAAO,+CAA+C,SAAS,MAAM;GACvE,CAAC;GACD;EACF;EAEA,QAAQ,KAAK;GAAE,SAAS;GAAY;EAAkC,CAAC;CACzE;CAEA,OAAO;EAAE;EAAS;CAAS;AAC7B;;AAGA,SAAS,SAAS,OAAwB;CACxC,IAAI,UAAU,MAAM,OAAO;CAC3B,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO;CACjC,OAAO,KAAK,OAAO;AACrB"}
1
+ {"version":3,"file":"template_export.js","names":[],"sources":["../../src/model/template_export.ts"],"sourcesContent":["import type { ProjectStructure } from \"./project_model\";\nimport { allBlocks } from \"./project_model_util\";\n\n/**\n * One block's template-descriptor output as the walk receives it.\n *\n * Deliberately the same shape the `__pl_initializationParams_derive` facade callback\n * returns, so a provider can hand the VM's result straight through without\n * reshaping it.\n */\nexport type TemplateParamsResult =\n | { readonly error: string }\n | { readonly error?: undefined; readonly value: unknown };\n\n/** One block's contribution to the template being exported. */\nexport type TemplateExportEntry = {\n /**\n * The block's project-local id, which is also its template-local id: a template\n * has no id namespace of its own, so the id is reused verbatim and references\n * already stored in params need no translation.\n */\n readonly blockId: string;\n /** The block's label from the project structure, carried so a problem found downstream can\n * name the block to a person. Not written to the template. */\n readonly blockLabel: string;\n /**\n * The block's params exactly as it projected them.\n *\n * Always a mapping: a block that declared no `templateParams`, or whose lambda returned\n * something else, is reported as a problem rather than carried here — see the object check\n * in the walk. So nothing downstream has an absent case to decide.\n */\n readonly params: Record<string, unknown>;\n};\n\n/** Why one block could not be exported. */\nexport type TemplateExportProblem = {\n readonly blockId: string;\n /** The block's label as the project structure holds it. An id means nothing to the person who\n * pressed Export; this is what a UI shows. */\n readonly blockLabel: string;\n readonly error: string;\n};\n\n/**\n * Outcome of the walk: the blocks that can be written, and the ones that cannot.\n *\n * Both lists are returned rather than throwing on the first failure, so the\n * caller can report every offending block at once instead of making the user fix\n * them one export at a time. Whether a non-empty `problems` aborts the export is\n * the caller's policy, not the walk's — but note that emitting `entries` while\n * ignoring `problems` can produce a file whose surviving entries reference a\n * dropped block, which is an unusable template.\n */\nexport type TemplateExportWalk = {\n readonly entries: readonly TemplateExportEntry[];\n readonly problems: readonly TemplateExportProblem[];\n};\n\n/**\n * Walk a project's blocks in dependency order, collecting each one's\n * template-descriptor output.\n *\n * **No topological sort is performed, because none is needed.** The project\n * structure is already stored in topological order, and that is enforced rather\n * than assumed: `productionGraph` traverses `allBlocks(structure)` and passes the\n * set of blocks seen *so far* as the allowed set to `inferAllReferencedBlocks`, so\n * a reference to a block that is not already above is recorded as a missing\n * reference instead of an upstream. A block can therefore only legally reference\n * blocks earlier in this sequence — which is exactly what a template file needs,\n * since its block order is the instantiation order and the engine creates blocks\n * upstream-first. Emitting entries in structure order satisfies that for free.\n *\n * Groups are flattened in order, so cross-group ordering is the structure's too.\n *\n * A structure that violates the ordering rule is reported as-is, not repaired:\n * reordering would change which references are legal in the first place.\n *\n * Params are written exactly as the block projected them. The walk parses nothing, rewrites\n * nothing and inspects nothing inside them, and neither does anything else between here and the\n * file — which values carry block ids is knowledge of the reference system, and a template\n * engine holds none of it. The block that receives these params on the way back in is what\n * recognizes them; a block that projects the wrong fields produces a template that does not\n * work, the same way one whose `templateParams` returns the wrong shape does.\n *\n * @param structure The project structure — the source of both membership and order\n * @param paramsProvider Yields a block's derived template params. Return\n * `undefined` for a block whose state cannot be read at all; such a block is\n * recorded as a problem rather than skipped, because a template that quietly\n * omits a block does not describe the project it was exported from, and the\n * surviving entries may still reference the omitted one.\n */\nexport function walkProjectForTemplateExport(\n structure: ProjectStructure,\n paramsProvider: (blockId: string) => TemplateParamsResult | undefined,\n): TemplateExportWalk {\n const entries: TemplateExportEntry[] = [];\n const problems: TemplateExportProblem[] = [];\n\n for (const { id, label } of allBlocks(structure)) {\n const derived = paramsProvider(id);\n\n if (derived === undefined) {\n problems.push({\n blockId: id,\n blockLabel: label,\n error: \"Block state is unavailable, so its template params could not be derived\",\n });\n continue;\n }\n\n if (derived.error !== undefined) {\n problems.push({ blockId: id, blockLabel: label, error: derived.error });\n continue;\n }\n\n const params = derived.value;\n\n // An entry's `params` must be a mapping. The lambda's declared return type is the\n // block kind's params type, and the kind's parser checks values coming IN, but\n // nothing checks what the lambda hands back on the way out — so a block whose\n // params type is a primitive or a tuple compiles fine and would produce an\n // unwritable entry. This is the only place that can catch it.\n if (typeof params !== \"object\" || params === null || Array.isArray(params)) {\n problems.push({\n blockId: id,\n blockLabel: label,\n error: `templateParams() must return an object, got ${typeName(params)}`,\n });\n continue;\n }\n\n entries.push({ blockId: id, blockLabel: label, params: params as Record<string, unknown> });\n }\n\n return { entries, problems };\n}\n\n/** Name the offending value's type for an error message, without printing the value. */\nfunction typeName(value: unknown): string {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"an array\";\n return `a ${typeof value}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4FA,SAAgB,6BACd,WACA,gBACoB;CACpB,MAAM,UAAiC,CAAC;CACxC,MAAM,WAAoC,CAAC;CAE3C,KAAK,MAAM,EAAE,IAAI,WAAW,UAAU,SAAS,GAAG;EAChD,MAAM,UAAU,eAAe,EAAE;EAEjC,IAAI,YAAY,KAAA,GAAW;GACzB,SAAS,KAAK;IACZ,SAAS;IACT,YAAY;IACZ,OAAO;GACT,CAAC;GACD;EACF;EAEA,IAAI,QAAQ,UAAU,KAAA,GAAW;GAC/B,SAAS,KAAK;IAAE,SAAS;IAAI,YAAY;IAAO,OAAO,QAAQ;GAAM,CAAC;GACtE;EACF;EAEA,MAAM,SAAS,QAAQ;EAOvB,IAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,MAAM,GAAG;GAC1E,SAAS,KAAK;IACZ,SAAS;IACT,YAAY;IACZ,OAAO,+CAA+C,SAAS,MAAM;GACvE,CAAC;GACD;EACF;EAEA,QAAQ,KAAK;GAAE,SAAS;GAAI,YAAY;GAAe;EAAkC,CAAC;CAC5F;CAEA,OAAO;EAAE;EAAS;CAAS;AAC7B;;AAGA,SAAS,SAAS,OAAwB;CACxC,IAAI,UAAU,MAAM,OAAO;CAC3B,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO;CACjC,OAAO,KAAK,OAAO;AACrB"}
@@ -71,6 +71,7 @@ function assembleProjectTemplateV1(walk, kindProvider, specProvider) {
71
71
  if (kind === void 0) {
72
72
  problems.push({
73
73
  blockId: entry.blockId,
74
+ blockLabel: entry.blockLabel,
74
75
  error: "Block declares no kind, so it cannot be written to a template: an entry's kind carries the params contract the entry is typed against"
75
76
  });
76
77
  continue;
@@ -81,6 +82,7 @@ function assembleProjectTemplateV1(walk, kindProvider, specProvider) {
81
82
  } catch (e) {
82
83
  problems.push({
83
84
  blockId: entry.blockId,
85
+ blockLabel: entry.blockLabel,
84
86
  error: `Block's stored kind reference is malformed: ${e instanceof Error ? e.message : String(e)}`
85
87
  });
86
88
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"template_serializer.cjs","names":["PROJECT_TEMPLATE_SCHEMA_V1","YAML","walkProjectForTemplateExport"],"sources":["../../src/model/template_serializer.ts"],"sourcesContent":["import YAML from \"yaml\";\nimport { pathToFileURL } from \"node:url\";\nimport type {\n BlockKindReference,\n BlockKindSelectorReference,\n BlockPackLocationReference,\n ProjectTemplateV1,\n ProjectTemplateV1Entry,\n} from \"@milaboratories/pl-model-common\";\nimport {\n PROJECT_TEMPLATE_SCHEMA_V1,\n kindReferenceToSelectorReference,\n parseProjectTemplateV1,\n} from \"@milaboratories/pl-model-common\";\nimport type { BlockPackSpec } from \"@milaboratories/pl-model-middle-layer\";\nimport type { ProjectStructure } from \"./project_model\";\nimport type {\n TemplateExportProblem,\n TemplateExportWalk,\n TemplateParamsResult,\n} from \"./template_export\";\nimport { walkProjectForTemplateExport } from \"./template_export\";\n\n/** A block's exact kind reference, or `undefined` for a block that declares no kind. */\nexport type BlockKindProvider = (blockId: string) => BlockKindReference | undefined;\n\n/**\n * A block's origin spec — where the installed block came from — or `undefined` when\n * it is not known for that block.\n *\n * The project stores this next to the kind reference, so both are read from the same\n * place and neither costs an extra round-trip.\n */\nexport type BlockPackSpecProvider = (blockId: string) => BlockPackSpec | undefined;\n\n/** What the caller gets back for a whole project. */\nexport type ProjectTemplateExportOutcome =\n | {\n readonly ok: true;\n readonly yaml: string;\n /** The document the YAML was rendered from, already validated. */\n readonly document: ProjectTemplateV1;\n }\n | {\n readonly ok: false;\n /** Every block that stands in the way, not just the first. */\n readonly problems: readonly TemplateExportProblem[];\n };\n\n/**\n * The `location` to write for a block installed from the filesystem, or `undefined`\n * for one that came from a registry and therefore needs no locator.\n *\n * Both filesystem spec shapes are emitted, and they anchor at different directories\n * — a dev block at its facade package, an npm-consumed one at its block-pack folder.\n * The document does not distinguish them: one URI is written either way, and telling\n * the two layouts apart is done by looking at what is actually there, by the side\n * that has the filesystem anyway. Encoding the layout in the file instead would\n * freeze today's two shapes into the format.\n *\n * A dev spec carries an OS path and is converted here, which also percent-encodes a\n * path containing spaces. An npm-consumed spec already carries a `file:` URL and is\n * passed through: it is the locator the block itself emitted, and reconstructing one\n * from it could only lose information.\n */\nexport function locationOf(spec: BlockPackSpec): BlockPackLocationReference | undefined {\n switch (spec.type) {\n case \"dev-v2\":\n return pathToFileURL(spec.folder).href as BlockPackLocationReference;\n case \"from-pack-v2\":\n return spec.packUrl as BlockPackLocationReference;\n // A registry block is found by name, which is what makes the entry portable —\n // writing where this machine happened to cache it would take that away. `dev-v1`\n // predates kinds entirely, so such a block has no kind and never reaches here.\n case \"dev-v1\":\n case \"from-registry-v1\":\n case \"from-registry-v2\":\n return undefined;\n }\n}\n\n/**\n * Turn a project into a template document.\n *\n * Assembly is deliberately dull — the entry is the block's id, its widened kind\n * reference, and the params the walk already collected. The interesting decisions\n * were made upstream; what is left here is the two things only this layer can\n * check, both of which produce problems rather than a broken file:\n *\n * - **A block with no kind cannot be written.** An entry's `kind` is required — it\n * is the params contract the entry is typed against — while a block's kind is\n * optional, so a block that predates kinds, or that uses the deprecated\n * kind-less model overload, has no legal entry. Reported per block. This is not\n * an edge case today: it is what most existing projects will hit until their\n * blocks are republished.\n * - **References must point at an entry declared earlier.** Verbatim id reuse\n * means a reference to a deleted block survives into the file naming nothing:\n * deleting a block only removes it from the structure and does not rewrite\n * downstream args, so a live project holds such references routinely.\n *\n * `block` is never emitted. That override exists to pin an implementation against\n * a kind's version range, and export always writes the exact version the block\n * implements, so there is nothing left for it to pin.\n *\n * `location` IS emitted, for every block that was installed from the filesystem. Such\n * a block is not in any registry, so the kind reference alone names nothing the\n * importer could find, and a file that omitted the one usable answer would describe a\n * project that cannot be recreated. It costs portability, and nothing says so: such a\n * file is the debugging path, read by the developer who wrote it on the machine that\n * wrote it.\n *\n * Problems from `walk` are carried through, so a caller can hand a walk straight\n * in and get one combined list.\n */\nexport function assembleProjectTemplateV1(\n walk: TemplateExportWalk,\n kindProvider: BlockKindProvider,\n specProvider: BlockPackSpecProvider,\n): { document: ProjectTemplateV1; problems: readonly TemplateExportProblem[] } {\n const problems: TemplateExportProblem[] = [...walk.problems];\n const blocks: ProjectTemplateV1Entry[] = [];\n\n for (const entry of walk.entries) {\n const kind = kindProvider(entry.blockId);\n\n if (kind === undefined) {\n problems.push({\n blockId: entry.blockId,\n error:\n \"Block declares no kind, so it cannot be written to a template: an entry's kind \" +\n \"carries the params contract the entry is typed against\",\n });\n continue;\n }\n\n let selector: BlockKindSelectorReference;\n try {\n // Widening validates, and therefore throws — which is why it happens here\n // and not where the reference is read: every read site sits inside a\n // recomputed project overview, where one malformed stored reference must not\n // be able to break unrelated blocks.\n selector = kindReferenceToSelectorReference(kind);\n } catch (e) {\n problems.push({\n blockId: entry.blockId,\n error: `Block's stored kind reference is malformed: ${e instanceof Error ? e.message : String(e)}`,\n });\n continue;\n }\n\n const spec = specProvider(entry.blockId);\n const location = spec === undefined ? undefined : locationOf(spec);\n\n blocks.push({\n id: entry.blockId,\n kind: selector,\n params: entry.params,\n ...(location !== undefined ? { location } : {}),\n });\n }\n\n // References are not examined. A project's structure is topological by construction, so an\n // entry cannot legally reference one below it — and checking would mean reading the params,\n // which only the block that wrote them can do.\n const document: ProjectTemplateV1 = { schema: PROJECT_TEMPLATE_SCHEMA_V1, blocks };\n\n return { document, problems };\n}\n\n/**\n * Render a template document to YAML text.\n *\n * Two non-default emitter settings, both about the file being read by someone\n * else's code:\n *\n * - **No line folding.** A wrapped scalar still parses, but it makes a diff between\n * two exported templates unreadable, which is most of the reason to prefer YAML\n * over JSON here.\n * - **Quote as if the reader were YAML 1.1**, while still parsing as 1.2. YAML 1.2\n * dropped `yes`/`no`/`on`/`off`/`y`/`n` as booleans and dropped sexagesimal\n * integers, so a 1.2 emitter leaves a params value of `\"yes\"` or `\"1:30\"` bare —\n * which a 1.1 reader (PyYAML's default, and Go's yaml.v2) turns into `true` and\n * `90`. A template is a contract for a second implementation, so the safe\n * combination is to quote against the stricter ruleset and read with the looser\n * one: a quoted scalar means the same thing under both. This adds no `%YAML`\n * directive — it only changes which scalars get quotes.\n */\nexport function stringifyProjectTemplateV1(document: ProjectTemplateV1): string {\n return YAML.stringify(document, { lineWidth: 0, version: \"1.1\" });\n}\n\n/**\n * Export a project as `template-v1` YAML, or report every reason it cannot be.\n *\n * All-or-nothing on purpose. A partial template silently drops blocks and the\n * surviving entries may reference the dropped ones, so what looks like a\n * successful export would produce a project missing pieces the user never chose\n * to leave out. Reporting everything at once instead of failing on the first\n * problem is the other half of that: fixing an export should take one pass.\n *\n * @param structure The project structure, which supplies both membership and order\n * @param paramsProvider A block's derived template params, in live form\n * @param kindProvider A block's exact kind reference, read from its stored config\n * @param specProvider A block's origin spec, read from the same stored container\n */\nexport function exportProjectAsTemplateV1(\n structure: ProjectStructure,\n paramsProvider: (blockId: string) => TemplateParamsResult | undefined,\n kindProvider: BlockKindProvider,\n specProvider: BlockPackSpecProvider,\n): ProjectTemplateExportOutcome {\n const walk = walkProjectForTemplateExport(structure, paramsProvider);\n const { document, problems } = assembleProjectTemplateV1(walk, kindProvider, specProvider);\n\n if (problems.length > 0) return { ok: false, problems };\n\n // Export must emit exactly what import parses, so that is asserted on every\n // export rather than only in tests — running the import-side parser over the\n // document we are about to write is the cheapest possible proof of it. Nothing\n // user-facing is expected to fail here: the kind grammar was checked by the\n // widening above, params were checked to be a mapping by the walk, and the\n // reference rules by the assembler. A throw means a bug in the assembler, with\n // one known exception: a project structure holding two blocks with the same id,\n // which is reachable through the mutator and produces duplicate entry ids.\n parseProjectTemplateV1(document);\n\n return {\n ok: true,\n yaml: stringifyProjectTemplateV1(document),\n document,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAiEA,SAAgB,WAAW,MAA6D;CACtF,QAAQ,KAAK,MAAb;EACE,KAAK,UACH,QAAA,GAAA,SAAA,cAAA,CAAqB,KAAK,MAAM,CAAC,CAAC;EACpC,KAAK,gBACH,OAAO,KAAK;EAId,KAAK;EACL,KAAK;EACL,KAAK,oBACH;CACJ;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,0BACd,MACA,cACA,cAC6E;CAC7E,MAAM,WAAoC,CAAC,GAAG,KAAK,QAAQ;CAC3D,MAAM,SAAmC,CAAC;CAE1C,KAAK,MAAM,SAAS,KAAK,SAAS;EAChC,MAAM,OAAO,aAAa,MAAM,OAAO;EAEvC,IAAI,SAAS,KAAA,GAAW;GACtB,SAAS,KAAK;IACZ,SAAS,MAAM;IACf,OACE;GAEJ,CAAC;GACD;EACF;EAEA,IAAI;EACJ,IAAI;GAKF,YAAA,GAAA,gCAAA,iCAAA,CAA4C,IAAI;EAClD,SAAS,GAAG;GACV,SAAS,KAAK;IACZ,SAAS,MAAM;IACf,OAAO,+CAA+C,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;GACjG,CAAC;GACD;EACF;EAEA,MAAM,OAAO,aAAa,MAAM,OAAO;EACvC,MAAM,WAAW,SAAS,KAAA,IAAY,KAAA,IAAY,WAAW,IAAI;EAEjE,OAAO,KAAK;GACV,IAAI,MAAM;GACV,MAAM;GACN,QAAQ,MAAM;GACd,GAAI,aAAa,KAAA,IAAY,EAAE,SAAS,IAAI,CAAC;EAC/C,CAAC;CACH;CAOA,OAAO;EAAE,UAAA;GAF6B,QAAQA,gCAAAA;GAA4B;EAE1D;EAAG;CAAS;AAC9B;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,2BAA2B,UAAqC;CAC9E,OAAOC,KAAAA,QAAK,UAAU,UAAU;EAAE,WAAW;EAAG,SAAS;CAAM,CAAC;AAClE;;;;;;;;;;;;;;;AAgBA,SAAgB,0BACd,WACA,gBACA,cACA,cAC8B;CAE9B,MAAM,EAAE,UAAU,aAAa,0BADlBC,wBAAAA,6BAA6B,WAAW,cACO,GAAG,cAAc,YAAY;CAEzF,IAAI,SAAS,SAAS,GAAG,OAAO;EAAE,IAAI;EAAO;CAAS;CAUtD,CAAA,GAAA,gCAAA,uBAAA,CAAuB,QAAQ;CAE/B,OAAO;EACL,IAAI;EACJ,MAAM,2BAA2B,QAAQ;EACzC;CACF;AACF"}
1
+ {"version":3,"file":"template_serializer.cjs","names":["PROJECT_TEMPLATE_SCHEMA_V1","YAML","walkProjectForTemplateExport"],"sources":["../../src/model/template_serializer.ts"],"sourcesContent":["import YAML from \"yaml\";\nimport { pathToFileURL } from \"node:url\";\nimport type {\n BlockKindReference,\n BlockKindSelectorReference,\n BlockPackLocationReference,\n ProjectTemplateV1,\n ProjectTemplateV1Entry,\n} from \"@milaboratories/pl-model-common\";\nimport {\n PROJECT_TEMPLATE_SCHEMA_V1,\n kindReferenceToSelectorReference,\n parseProjectTemplateV1,\n} from \"@milaboratories/pl-model-common\";\nimport type { BlockPackSpec } from \"@milaboratories/pl-model-middle-layer\";\nimport type { ProjectStructure } from \"./project_model\";\nimport type {\n TemplateExportProblem,\n TemplateExportWalk,\n TemplateParamsResult,\n} from \"./template_export\";\nimport { walkProjectForTemplateExport } from \"./template_export\";\n\n/** A block's exact kind reference, or `undefined` for a block that declares no kind. */\nexport type BlockKindProvider = (blockId: string) => BlockKindReference | undefined;\n\n/**\n * A block's origin spec — where the installed block came from — or `undefined` when\n * it is not known for that block.\n *\n * The project stores this next to the kind reference, so both are read from the same\n * place and neither costs an extra round-trip.\n */\nexport type BlockPackSpecProvider = (blockId: string) => BlockPackSpec | undefined;\n\n/** What the caller gets back for a whole project. */\nexport type ProjectTemplateExportOutcome =\n | {\n readonly ok: true;\n readonly yaml: string;\n /** The document the YAML was rendered from, already validated. */\n readonly document: ProjectTemplateV1;\n }\n | {\n readonly ok: false;\n /** Every block that stands in the way, not just the first. */\n readonly problems: readonly TemplateExportProblem[];\n };\n\n/**\n * The `location` to write for a block installed from the filesystem, or `undefined`\n * for one that came from a registry and therefore needs no locator.\n *\n * Both filesystem spec shapes are emitted, and they anchor at different directories\n * — a dev block at its facade package, an npm-consumed one at its block-pack folder.\n * The document does not distinguish them: one URI is written either way, and telling\n * the two layouts apart is done by looking at what is actually there, by the side\n * that has the filesystem anyway. Encoding the layout in the file instead would\n * freeze today's two shapes into the format.\n *\n * A dev spec carries an OS path and is converted here, which also percent-encodes a\n * path containing spaces. An npm-consumed spec already carries a `file:` URL and is\n * passed through: it is the locator the block itself emitted, and reconstructing one\n * from it could only lose information.\n */\nexport function locationOf(spec: BlockPackSpec): BlockPackLocationReference | undefined {\n switch (spec.type) {\n case \"dev-v2\":\n return pathToFileURL(spec.folder).href as BlockPackLocationReference;\n case \"from-pack-v2\":\n return spec.packUrl as BlockPackLocationReference;\n // A registry block is found by name, which is what makes the entry portable —\n // writing where this machine happened to cache it would take that away. `dev-v1`\n // predates kinds entirely, so such a block has no kind and never reaches here.\n case \"dev-v1\":\n case \"from-registry-v1\":\n case \"from-registry-v2\":\n return undefined;\n }\n}\n\n/**\n * Turn a project into a template document.\n *\n * Assembly is deliberately dull — the entry is the block's id, its widened kind\n * reference, and the params the walk already collected. The interesting decisions\n * were made upstream; what is left here is the two things only this layer can\n * check, both of which produce problems rather than a broken file:\n *\n * - **A block with no kind cannot be written.** An entry's `kind` is required — it\n * is the params contract the entry is typed against — while a block's kind is\n * optional, so a block that predates kinds, or that uses the deprecated\n * kind-less model overload, has no legal entry. Reported per block. This is not\n * an edge case today: it is what most existing projects will hit until their\n * blocks are republished.\n * - **References must point at an entry declared earlier.** Verbatim id reuse\n * means a reference to a deleted block survives into the file naming nothing:\n * deleting a block only removes it from the structure and does not rewrite\n * downstream args, so a live project holds such references routinely.\n *\n * `block` is never emitted. That override exists to pin an implementation against\n * a kind's version range, and export always writes the exact version the block\n * implements, so there is nothing left for it to pin.\n *\n * `location` IS emitted, for every block that was installed from the filesystem. Such\n * a block is not in any registry, so the kind reference alone names nothing the\n * importer could find, and a file that omitted the one usable answer would describe a\n * project that cannot be recreated. It costs portability, and nothing says so: such a\n * file is the debugging path, read by the developer who wrote it on the machine that\n * wrote it.\n *\n * Problems from `walk` are carried through, so a caller can hand a walk straight\n * in and get one combined list.\n */\nexport function assembleProjectTemplateV1(\n walk: TemplateExportWalk,\n kindProvider: BlockKindProvider,\n specProvider: BlockPackSpecProvider,\n): { document: ProjectTemplateV1; problems: readonly TemplateExportProblem[] } {\n const problems: TemplateExportProblem[] = [...walk.problems];\n const blocks: ProjectTemplateV1Entry[] = [];\n\n for (const entry of walk.entries) {\n const kind = kindProvider(entry.blockId);\n\n if (kind === undefined) {\n problems.push({\n blockId: entry.blockId,\n blockLabel: entry.blockLabel,\n error:\n \"Block declares no kind, so it cannot be written to a template: an entry's kind \" +\n \"carries the params contract the entry is typed against\",\n });\n continue;\n }\n\n let selector: BlockKindSelectorReference;\n try {\n // Widening validates, and therefore throws — which is why it happens here\n // and not where the reference is read: every read site sits inside a\n // recomputed project overview, where one malformed stored reference must not\n // be able to break unrelated blocks.\n selector = kindReferenceToSelectorReference(kind);\n } catch (e) {\n problems.push({\n blockId: entry.blockId,\n blockLabel: entry.blockLabel,\n error: `Block's stored kind reference is malformed: ${e instanceof Error ? e.message : String(e)}`,\n });\n continue;\n }\n\n const spec = specProvider(entry.blockId);\n const location = spec === undefined ? undefined : locationOf(spec);\n\n blocks.push({\n id: entry.blockId,\n kind: selector,\n params: entry.params,\n ...(location !== undefined ? { location } : {}),\n });\n }\n\n // References are not examined. A project's structure is topological by construction, so an\n // entry cannot legally reference one below it — and checking would mean reading the params,\n // which only the block that wrote them can do.\n const document: ProjectTemplateV1 = { schema: PROJECT_TEMPLATE_SCHEMA_V1, blocks };\n\n return { document, problems };\n}\n\n/**\n * Render a template document to YAML text.\n *\n * Two non-default emitter settings, both about the file being read by someone\n * else's code:\n *\n * - **No line folding.** A wrapped scalar still parses, but it makes a diff between\n * two exported templates unreadable, which is most of the reason to prefer YAML\n * over JSON here.\n * - **Quote as if the reader were YAML 1.1**, while still parsing as 1.2. YAML 1.2\n * dropped `yes`/`no`/`on`/`off`/`y`/`n` as booleans and dropped sexagesimal\n * integers, so a 1.2 emitter leaves a params value of `\"yes\"` or `\"1:30\"` bare —\n * which a 1.1 reader (PyYAML's default, and Go's yaml.v2) turns into `true` and\n * `90`. A template is a contract for a second implementation, so the safe\n * combination is to quote against the stricter ruleset and read with the looser\n * one: a quoted scalar means the same thing under both. This adds no `%YAML`\n * directive — it only changes which scalars get quotes.\n */\nexport function stringifyProjectTemplateV1(document: ProjectTemplateV1): string {\n return YAML.stringify(document, { lineWidth: 0, version: \"1.1\" });\n}\n\n/**\n * Export a project as `template-v1` YAML, or report every reason it cannot be.\n *\n * All-or-nothing on purpose. A partial template silently drops blocks and the\n * surviving entries may reference the dropped ones, so what looks like a\n * successful export would produce a project missing pieces the user never chose\n * to leave out. Reporting everything at once instead of failing on the first\n * problem is the other half of that: fixing an export should take one pass.\n *\n * @param structure The project structure, which supplies both membership and order\n * @param paramsProvider A block's derived template params, in live form\n * @param kindProvider A block's exact kind reference, read from its stored config\n * @param specProvider A block's origin spec, read from the same stored container\n */\nexport function exportProjectAsTemplateV1(\n structure: ProjectStructure,\n paramsProvider: (blockId: string) => TemplateParamsResult | undefined,\n kindProvider: BlockKindProvider,\n specProvider: BlockPackSpecProvider,\n): ProjectTemplateExportOutcome {\n const walk = walkProjectForTemplateExport(structure, paramsProvider);\n const { document, problems } = assembleProjectTemplateV1(walk, kindProvider, specProvider);\n\n if (problems.length > 0) return { ok: false, problems };\n\n // Export must emit exactly what import parses, so that is asserted on every\n // export rather than only in tests — running the import-side parser over the\n // document we are about to write is the cheapest possible proof of it. Nothing\n // user-facing is expected to fail here: the kind grammar was checked by the\n // widening above, params were checked to be a mapping by the walk, and the\n // reference rules by the assembler. A throw means a bug in the assembler, with\n // one known exception: a project structure holding two blocks with the same id,\n // which is reachable through the mutator and produces duplicate entry ids.\n parseProjectTemplateV1(document);\n\n return {\n ok: true,\n yaml: stringifyProjectTemplateV1(document),\n document,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAiEA,SAAgB,WAAW,MAA6D;CACtF,QAAQ,KAAK,MAAb;EACE,KAAK,UACH,QAAA,GAAA,SAAA,cAAA,CAAqB,KAAK,MAAM,CAAC,CAAC;EACpC,KAAK,gBACH,OAAO,KAAK;EAId,KAAK;EACL,KAAK;EACL,KAAK,oBACH;CACJ;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,0BACd,MACA,cACA,cAC6E;CAC7E,MAAM,WAAoC,CAAC,GAAG,KAAK,QAAQ;CAC3D,MAAM,SAAmC,CAAC;CAE1C,KAAK,MAAM,SAAS,KAAK,SAAS;EAChC,MAAM,OAAO,aAAa,MAAM,OAAO;EAEvC,IAAI,SAAS,KAAA,GAAW;GACtB,SAAS,KAAK;IACZ,SAAS,MAAM;IACf,YAAY,MAAM;IAClB,OACE;GAEJ,CAAC;GACD;EACF;EAEA,IAAI;EACJ,IAAI;GAKF,YAAA,GAAA,gCAAA,iCAAA,CAA4C,IAAI;EAClD,SAAS,GAAG;GACV,SAAS,KAAK;IACZ,SAAS,MAAM;IACf,YAAY,MAAM;IAClB,OAAO,+CAA+C,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;GACjG,CAAC;GACD;EACF;EAEA,MAAM,OAAO,aAAa,MAAM,OAAO;EACvC,MAAM,WAAW,SAAS,KAAA,IAAY,KAAA,IAAY,WAAW,IAAI;EAEjE,OAAO,KAAK;GACV,IAAI,MAAM;GACV,MAAM;GACN,QAAQ,MAAM;GACd,GAAI,aAAa,KAAA,IAAY,EAAE,SAAS,IAAI,CAAC;EAC/C,CAAC;CACH;CAOA,OAAO;EAAE,UAAA;GAF6B,QAAQA,gCAAAA;GAA4B;EAE1D;EAAG;CAAS;AAC9B;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,2BAA2B,UAAqC;CAC9E,OAAOC,KAAAA,QAAK,UAAU,UAAU;EAAE,WAAW;EAAG,SAAS;CAAM,CAAC;AAClE;;;;;;;;;;;;;;;AAgBA,SAAgB,0BACd,WACA,gBACA,cACA,cAC8B;CAE9B,MAAM,EAAE,UAAU,aAAa,0BADlBC,wBAAAA,6BAA6B,WAAW,cACO,GAAG,cAAc,YAAY;CAEzF,IAAI,SAAS,SAAS,GAAG,OAAO;EAAE,IAAI;EAAO;CAAS;CAUtD,CAAA,GAAA,gCAAA,uBAAA,CAAuB,QAAQ;CAE/B,OAAO;EACL,IAAI;EACJ,MAAM,2BAA2B,QAAQ;EACzC;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"template_serializer.d.ts","names":[],"sources":["../../src/model/template_serializer.ts"],"mappings":";;;;;;YAoCY;WAEG;WACA;;WAEA,UAAU;;WAGV;;WAEA,mBAAmB;;;;;;;;;;;;;;;;;;wBAmBlB,WAAW,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;wBA0HjC,2BAA2B,UAAU"}
1
+ {"version":3,"file":"template_serializer.d.ts","names":[],"sources":["../../src/model/template_serializer.ts"],"mappings":";;;;;;YAoCY;WAEG;WACA;;WAEA,UAAU;;WAGV;;WAEA,mBAAmB;;;;;;;;;;;;;;;;;;wBAmBlB,WAAW,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;wBA4HjC,2BAA2B,UAAU"}
@@ -69,6 +69,7 @@ function assembleProjectTemplateV1(walk, kindProvider, specProvider) {
69
69
  if (kind === void 0) {
70
70
  problems.push({
71
71
  blockId: entry.blockId,
72
+ blockLabel: entry.blockLabel,
72
73
  error: "Block declares no kind, so it cannot be written to a template: an entry's kind carries the params contract the entry is typed against"
73
74
  });
74
75
  continue;
@@ -79,6 +80,7 @@ function assembleProjectTemplateV1(walk, kindProvider, specProvider) {
79
80
  } catch (e) {
80
81
  problems.push({
81
82
  blockId: entry.blockId,
83
+ blockLabel: entry.blockLabel,
82
84
  error: `Block's stored kind reference is malformed: ${e instanceof Error ? e.message : String(e)}`
83
85
  });
84
86
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"template_serializer.js","names":[],"sources":["../../src/model/template_serializer.ts"],"sourcesContent":["import YAML from \"yaml\";\nimport { pathToFileURL } from \"node:url\";\nimport type {\n BlockKindReference,\n BlockKindSelectorReference,\n BlockPackLocationReference,\n ProjectTemplateV1,\n ProjectTemplateV1Entry,\n} from \"@milaboratories/pl-model-common\";\nimport {\n PROJECT_TEMPLATE_SCHEMA_V1,\n kindReferenceToSelectorReference,\n parseProjectTemplateV1,\n} from \"@milaboratories/pl-model-common\";\nimport type { BlockPackSpec } from \"@milaboratories/pl-model-middle-layer\";\nimport type { ProjectStructure } from \"./project_model\";\nimport type {\n TemplateExportProblem,\n TemplateExportWalk,\n TemplateParamsResult,\n} from \"./template_export\";\nimport { walkProjectForTemplateExport } from \"./template_export\";\n\n/** A block's exact kind reference, or `undefined` for a block that declares no kind. */\nexport type BlockKindProvider = (blockId: string) => BlockKindReference | undefined;\n\n/**\n * A block's origin spec — where the installed block came from — or `undefined` when\n * it is not known for that block.\n *\n * The project stores this next to the kind reference, so both are read from the same\n * place and neither costs an extra round-trip.\n */\nexport type BlockPackSpecProvider = (blockId: string) => BlockPackSpec | undefined;\n\n/** What the caller gets back for a whole project. */\nexport type ProjectTemplateExportOutcome =\n | {\n readonly ok: true;\n readonly yaml: string;\n /** The document the YAML was rendered from, already validated. */\n readonly document: ProjectTemplateV1;\n }\n | {\n readonly ok: false;\n /** Every block that stands in the way, not just the first. */\n readonly problems: readonly TemplateExportProblem[];\n };\n\n/**\n * The `location` to write for a block installed from the filesystem, or `undefined`\n * for one that came from a registry and therefore needs no locator.\n *\n * Both filesystem spec shapes are emitted, and they anchor at different directories\n * — a dev block at its facade package, an npm-consumed one at its block-pack folder.\n * The document does not distinguish them: one URI is written either way, and telling\n * the two layouts apart is done by looking at what is actually there, by the side\n * that has the filesystem anyway. Encoding the layout in the file instead would\n * freeze today's two shapes into the format.\n *\n * A dev spec carries an OS path and is converted here, which also percent-encodes a\n * path containing spaces. An npm-consumed spec already carries a `file:` URL and is\n * passed through: it is the locator the block itself emitted, and reconstructing one\n * from it could only lose information.\n */\nexport function locationOf(spec: BlockPackSpec): BlockPackLocationReference | undefined {\n switch (spec.type) {\n case \"dev-v2\":\n return pathToFileURL(spec.folder).href as BlockPackLocationReference;\n case \"from-pack-v2\":\n return spec.packUrl as BlockPackLocationReference;\n // A registry block is found by name, which is what makes the entry portable —\n // writing where this machine happened to cache it would take that away. `dev-v1`\n // predates kinds entirely, so such a block has no kind and never reaches here.\n case \"dev-v1\":\n case \"from-registry-v1\":\n case \"from-registry-v2\":\n return undefined;\n }\n}\n\n/**\n * Turn a project into a template document.\n *\n * Assembly is deliberately dull — the entry is the block's id, its widened kind\n * reference, and the params the walk already collected. The interesting decisions\n * were made upstream; what is left here is the two things only this layer can\n * check, both of which produce problems rather than a broken file:\n *\n * - **A block with no kind cannot be written.** An entry's `kind` is required — it\n * is the params contract the entry is typed against — while a block's kind is\n * optional, so a block that predates kinds, or that uses the deprecated\n * kind-less model overload, has no legal entry. Reported per block. This is not\n * an edge case today: it is what most existing projects will hit until their\n * blocks are republished.\n * - **References must point at an entry declared earlier.** Verbatim id reuse\n * means a reference to a deleted block survives into the file naming nothing:\n * deleting a block only removes it from the structure and does not rewrite\n * downstream args, so a live project holds such references routinely.\n *\n * `block` is never emitted. That override exists to pin an implementation against\n * a kind's version range, and export always writes the exact version the block\n * implements, so there is nothing left for it to pin.\n *\n * `location` IS emitted, for every block that was installed from the filesystem. Such\n * a block is not in any registry, so the kind reference alone names nothing the\n * importer could find, and a file that omitted the one usable answer would describe a\n * project that cannot be recreated. It costs portability, and nothing says so: such a\n * file is the debugging path, read by the developer who wrote it on the machine that\n * wrote it.\n *\n * Problems from `walk` are carried through, so a caller can hand a walk straight\n * in and get one combined list.\n */\nexport function assembleProjectTemplateV1(\n walk: TemplateExportWalk,\n kindProvider: BlockKindProvider,\n specProvider: BlockPackSpecProvider,\n): { document: ProjectTemplateV1; problems: readonly TemplateExportProblem[] } {\n const problems: TemplateExportProblem[] = [...walk.problems];\n const blocks: ProjectTemplateV1Entry[] = [];\n\n for (const entry of walk.entries) {\n const kind = kindProvider(entry.blockId);\n\n if (kind === undefined) {\n problems.push({\n blockId: entry.blockId,\n error:\n \"Block declares no kind, so it cannot be written to a template: an entry's kind \" +\n \"carries the params contract the entry is typed against\",\n });\n continue;\n }\n\n let selector: BlockKindSelectorReference;\n try {\n // Widening validates, and therefore throws — which is why it happens here\n // and not where the reference is read: every read site sits inside a\n // recomputed project overview, where one malformed stored reference must not\n // be able to break unrelated blocks.\n selector = kindReferenceToSelectorReference(kind);\n } catch (e) {\n problems.push({\n blockId: entry.blockId,\n error: `Block's stored kind reference is malformed: ${e instanceof Error ? e.message : String(e)}`,\n });\n continue;\n }\n\n const spec = specProvider(entry.blockId);\n const location = spec === undefined ? undefined : locationOf(spec);\n\n blocks.push({\n id: entry.blockId,\n kind: selector,\n params: entry.params,\n ...(location !== undefined ? { location } : {}),\n });\n }\n\n // References are not examined. A project's structure is topological by construction, so an\n // entry cannot legally reference one below it — and checking would mean reading the params,\n // which only the block that wrote them can do.\n const document: ProjectTemplateV1 = { schema: PROJECT_TEMPLATE_SCHEMA_V1, blocks };\n\n return { document, problems };\n}\n\n/**\n * Render a template document to YAML text.\n *\n * Two non-default emitter settings, both about the file being read by someone\n * else's code:\n *\n * - **No line folding.** A wrapped scalar still parses, but it makes a diff between\n * two exported templates unreadable, which is most of the reason to prefer YAML\n * over JSON here.\n * - **Quote as if the reader were YAML 1.1**, while still parsing as 1.2. YAML 1.2\n * dropped `yes`/`no`/`on`/`off`/`y`/`n` as booleans and dropped sexagesimal\n * integers, so a 1.2 emitter leaves a params value of `\"yes\"` or `\"1:30\"` bare —\n * which a 1.1 reader (PyYAML's default, and Go's yaml.v2) turns into `true` and\n * `90`. A template is a contract for a second implementation, so the safe\n * combination is to quote against the stricter ruleset and read with the looser\n * one: a quoted scalar means the same thing under both. This adds no `%YAML`\n * directive — it only changes which scalars get quotes.\n */\nexport function stringifyProjectTemplateV1(document: ProjectTemplateV1): string {\n return YAML.stringify(document, { lineWidth: 0, version: \"1.1\" });\n}\n\n/**\n * Export a project as `template-v1` YAML, or report every reason it cannot be.\n *\n * All-or-nothing on purpose. A partial template silently drops blocks and the\n * surviving entries may reference the dropped ones, so what looks like a\n * successful export would produce a project missing pieces the user never chose\n * to leave out. Reporting everything at once instead of failing on the first\n * problem is the other half of that: fixing an export should take one pass.\n *\n * @param structure The project structure, which supplies both membership and order\n * @param paramsProvider A block's derived template params, in live form\n * @param kindProvider A block's exact kind reference, read from its stored config\n * @param specProvider A block's origin spec, read from the same stored container\n */\nexport function exportProjectAsTemplateV1(\n structure: ProjectStructure,\n paramsProvider: (blockId: string) => TemplateParamsResult | undefined,\n kindProvider: BlockKindProvider,\n specProvider: BlockPackSpecProvider,\n): ProjectTemplateExportOutcome {\n const walk = walkProjectForTemplateExport(structure, paramsProvider);\n const { document, problems } = assembleProjectTemplateV1(walk, kindProvider, specProvider);\n\n if (problems.length > 0) return { ok: false, problems };\n\n // Export must emit exactly what import parses, so that is asserted on every\n // export rather than only in tests — running the import-side parser over the\n // document we are about to write is the cheapest possible proof of it. Nothing\n // user-facing is expected to fail here: the kind grammar was checked by the\n // widening above, params were checked to be a mapping by the walk, and the\n // reference rules by the assembler. A throw means a bug in the assembler, with\n // one known exception: a project structure holding two blocks with the same id,\n // which is reachable through the mutator and produces duplicate entry ids.\n parseProjectTemplateV1(document);\n\n return {\n ok: true,\n yaml: stringifyProjectTemplateV1(document),\n document,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAiEA,SAAgB,WAAW,MAA6D;CACtF,QAAQ,KAAK,MAAb;EACE,KAAK,UACH,OAAO,cAAc,KAAK,MAAM,CAAC,CAAC;EACpC,KAAK,gBACH,OAAO,KAAK;EAId,KAAK;EACL,KAAK;EACL,KAAK,oBACH;CACJ;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,0BACd,MACA,cACA,cAC6E;CAC7E,MAAM,WAAoC,CAAC,GAAG,KAAK,QAAQ;CAC3D,MAAM,SAAmC,CAAC;CAE1C,KAAK,MAAM,SAAS,KAAK,SAAS;EAChC,MAAM,OAAO,aAAa,MAAM,OAAO;EAEvC,IAAI,SAAS,KAAA,GAAW;GACtB,SAAS,KAAK;IACZ,SAAS,MAAM;IACf,OACE;GAEJ,CAAC;GACD;EACF;EAEA,IAAI;EACJ,IAAI;GAKF,WAAW,iCAAiC,IAAI;EAClD,SAAS,GAAG;GACV,SAAS,KAAK;IACZ,SAAS,MAAM;IACf,OAAO,+CAA+C,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;GACjG,CAAC;GACD;EACF;EAEA,MAAM,OAAO,aAAa,MAAM,OAAO;EACvC,MAAM,WAAW,SAAS,KAAA,IAAY,KAAA,IAAY,WAAW,IAAI;EAEjE,OAAO,KAAK;GACV,IAAI,MAAM;GACV,MAAM;GACN,QAAQ,MAAM;GACd,GAAI,aAAa,KAAA,IAAY,EAAE,SAAS,IAAI,CAAC;EAC/C,CAAC;CACH;CAOA,OAAO;EAAE,UAAA;GAF6B,QAAQ;GAA4B;EAE1D;EAAG;CAAS;AAC9B;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,2BAA2B,UAAqC;CAC9E,OAAO,KAAK,UAAU,UAAU;EAAE,WAAW;EAAG,SAAS;CAAM,CAAC;AAClE;;;;;;;;;;;;;;;AAgBA,SAAgB,0BACd,WACA,gBACA,cACA,cAC8B;CAE9B,MAAM,EAAE,UAAU,aAAa,0BADlB,6BAA6B,WAAW,cACO,GAAG,cAAc,YAAY;CAEzF,IAAI,SAAS,SAAS,GAAG,OAAO;EAAE,IAAI;EAAO;CAAS;CAUtD,uBAAuB,QAAQ;CAE/B,OAAO;EACL,IAAI;EACJ,MAAM,2BAA2B,QAAQ;EACzC;CACF;AACF"}
1
+ {"version":3,"file":"template_serializer.js","names":[],"sources":["../../src/model/template_serializer.ts"],"sourcesContent":["import YAML from \"yaml\";\nimport { pathToFileURL } from \"node:url\";\nimport type {\n BlockKindReference,\n BlockKindSelectorReference,\n BlockPackLocationReference,\n ProjectTemplateV1,\n ProjectTemplateV1Entry,\n} from \"@milaboratories/pl-model-common\";\nimport {\n PROJECT_TEMPLATE_SCHEMA_V1,\n kindReferenceToSelectorReference,\n parseProjectTemplateV1,\n} from \"@milaboratories/pl-model-common\";\nimport type { BlockPackSpec } from \"@milaboratories/pl-model-middle-layer\";\nimport type { ProjectStructure } from \"./project_model\";\nimport type {\n TemplateExportProblem,\n TemplateExportWalk,\n TemplateParamsResult,\n} from \"./template_export\";\nimport { walkProjectForTemplateExport } from \"./template_export\";\n\n/** A block's exact kind reference, or `undefined` for a block that declares no kind. */\nexport type BlockKindProvider = (blockId: string) => BlockKindReference | undefined;\n\n/**\n * A block's origin spec — where the installed block came from — or `undefined` when\n * it is not known for that block.\n *\n * The project stores this next to the kind reference, so both are read from the same\n * place and neither costs an extra round-trip.\n */\nexport type BlockPackSpecProvider = (blockId: string) => BlockPackSpec | undefined;\n\n/** What the caller gets back for a whole project. */\nexport type ProjectTemplateExportOutcome =\n | {\n readonly ok: true;\n readonly yaml: string;\n /** The document the YAML was rendered from, already validated. */\n readonly document: ProjectTemplateV1;\n }\n | {\n readonly ok: false;\n /** Every block that stands in the way, not just the first. */\n readonly problems: readonly TemplateExportProblem[];\n };\n\n/**\n * The `location` to write for a block installed from the filesystem, or `undefined`\n * for one that came from a registry and therefore needs no locator.\n *\n * Both filesystem spec shapes are emitted, and they anchor at different directories\n * — a dev block at its facade package, an npm-consumed one at its block-pack folder.\n * The document does not distinguish them: one URI is written either way, and telling\n * the two layouts apart is done by looking at what is actually there, by the side\n * that has the filesystem anyway. Encoding the layout in the file instead would\n * freeze today's two shapes into the format.\n *\n * A dev spec carries an OS path and is converted here, which also percent-encodes a\n * path containing spaces. An npm-consumed spec already carries a `file:` URL and is\n * passed through: it is the locator the block itself emitted, and reconstructing one\n * from it could only lose information.\n */\nexport function locationOf(spec: BlockPackSpec): BlockPackLocationReference | undefined {\n switch (spec.type) {\n case \"dev-v2\":\n return pathToFileURL(spec.folder).href as BlockPackLocationReference;\n case \"from-pack-v2\":\n return spec.packUrl as BlockPackLocationReference;\n // A registry block is found by name, which is what makes the entry portable —\n // writing where this machine happened to cache it would take that away. `dev-v1`\n // predates kinds entirely, so such a block has no kind and never reaches here.\n case \"dev-v1\":\n case \"from-registry-v1\":\n case \"from-registry-v2\":\n return undefined;\n }\n}\n\n/**\n * Turn a project into a template document.\n *\n * Assembly is deliberately dull — the entry is the block's id, its widened kind\n * reference, and the params the walk already collected. The interesting decisions\n * were made upstream; what is left here is the two things only this layer can\n * check, both of which produce problems rather than a broken file:\n *\n * - **A block with no kind cannot be written.** An entry's `kind` is required — it\n * is the params contract the entry is typed against — while a block's kind is\n * optional, so a block that predates kinds, or that uses the deprecated\n * kind-less model overload, has no legal entry. Reported per block. This is not\n * an edge case today: it is what most existing projects will hit until their\n * blocks are republished.\n * - **References must point at an entry declared earlier.** Verbatim id reuse\n * means a reference to a deleted block survives into the file naming nothing:\n * deleting a block only removes it from the structure and does not rewrite\n * downstream args, so a live project holds such references routinely.\n *\n * `block` is never emitted. That override exists to pin an implementation against\n * a kind's version range, and export always writes the exact version the block\n * implements, so there is nothing left for it to pin.\n *\n * `location` IS emitted, for every block that was installed from the filesystem. Such\n * a block is not in any registry, so the kind reference alone names nothing the\n * importer could find, and a file that omitted the one usable answer would describe a\n * project that cannot be recreated. It costs portability, and nothing says so: such a\n * file is the debugging path, read by the developer who wrote it on the machine that\n * wrote it.\n *\n * Problems from `walk` are carried through, so a caller can hand a walk straight\n * in and get one combined list.\n */\nexport function assembleProjectTemplateV1(\n walk: TemplateExportWalk,\n kindProvider: BlockKindProvider,\n specProvider: BlockPackSpecProvider,\n): { document: ProjectTemplateV1; problems: readonly TemplateExportProblem[] } {\n const problems: TemplateExportProblem[] = [...walk.problems];\n const blocks: ProjectTemplateV1Entry[] = [];\n\n for (const entry of walk.entries) {\n const kind = kindProvider(entry.blockId);\n\n if (kind === undefined) {\n problems.push({\n blockId: entry.blockId,\n blockLabel: entry.blockLabel,\n error:\n \"Block declares no kind, so it cannot be written to a template: an entry's kind \" +\n \"carries the params contract the entry is typed against\",\n });\n continue;\n }\n\n let selector: BlockKindSelectorReference;\n try {\n // Widening validates, and therefore throws — which is why it happens here\n // and not where the reference is read: every read site sits inside a\n // recomputed project overview, where one malformed stored reference must not\n // be able to break unrelated blocks.\n selector = kindReferenceToSelectorReference(kind);\n } catch (e) {\n problems.push({\n blockId: entry.blockId,\n blockLabel: entry.blockLabel,\n error: `Block's stored kind reference is malformed: ${e instanceof Error ? e.message : String(e)}`,\n });\n continue;\n }\n\n const spec = specProvider(entry.blockId);\n const location = spec === undefined ? undefined : locationOf(spec);\n\n blocks.push({\n id: entry.blockId,\n kind: selector,\n params: entry.params,\n ...(location !== undefined ? { location } : {}),\n });\n }\n\n // References are not examined. A project's structure is topological by construction, so an\n // entry cannot legally reference one below it — and checking would mean reading the params,\n // which only the block that wrote them can do.\n const document: ProjectTemplateV1 = { schema: PROJECT_TEMPLATE_SCHEMA_V1, blocks };\n\n return { document, problems };\n}\n\n/**\n * Render a template document to YAML text.\n *\n * Two non-default emitter settings, both about the file being read by someone\n * else's code:\n *\n * - **No line folding.** A wrapped scalar still parses, but it makes a diff between\n * two exported templates unreadable, which is most of the reason to prefer YAML\n * over JSON here.\n * - **Quote as if the reader were YAML 1.1**, while still parsing as 1.2. YAML 1.2\n * dropped `yes`/`no`/`on`/`off`/`y`/`n` as booleans and dropped sexagesimal\n * integers, so a 1.2 emitter leaves a params value of `\"yes\"` or `\"1:30\"` bare —\n * which a 1.1 reader (PyYAML's default, and Go's yaml.v2) turns into `true` and\n * `90`. A template is a contract for a second implementation, so the safe\n * combination is to quote against the stricter ruleset and read with the looser\n * one: a quoted scalar means the same thing under both. This adds no `%YAML`\n * directive — it only changes which scalars get quotes.\n */\nexport function stringifyProjectTemplateV1(document: ProjectTemplateV1): string {\n return YAML.stringify(document, { lineWidth: 0, version: \"1.1\" });\n}\n\n/**\n * Export a project as `template-v1` YAML, or report every reason it cannot be.\n *\n * All-or-nothing on purpose. A partial template silently drops blocks and the\n * surviving entries may reference the dropped ones, so what looks like a\n * successful export would produce a project missing pieces the user never chose\n * to leave out. Reporting everything at once instead of failing on the first\n * problem is the other half of that: fixing an export should take one pass.\n *\n * @param structure The project structure, which supplies both membership and order\n * @param paramsProvider A block's derived template params, in live form\n * @param kindProvider A block's exact kind reference, read from its stored config\n * @param specProvider A block's origin spec, read from the same stored container\n */\nexport function exportProjectAsTemplateV1(\n structure: ProjectStructure,\n paramsProvider: (blockId: string) => TemplateParamsResult | undefined,\n kindProvider: BlockKindProvider,\n specProvider: BlockPackSpecProvider,\n): ProjectTemplateExportOutcome {\n const walk = walkProjectForTemplateExport(structure, paramsProvider);\n const { document, problems } = assembleProjectTemplateV1(walk, kindProvider, specProvider);\n\n if (problems.length > 0) return { ok: false, problems };\n\n // Export must emit exactly what import parses, so that is asserted on every\n // export rather than only in tests — running the import-side parser over the\n // document we are about to write is the cheapest possible proof of it. Nothing\n // user-facing is expected to fail here: the kind grammar was checked by the\n // widening above, params were checked to be a mapping by the walk, and the\n // reference rules by the assembler. A throw means a bug in the assembler, with\n // one known exception: a project structure holding two blocks with the same id,\n // which is reachable through the mutator and produces duplicate entry ids.\n parseProjectTemplateV1(document);\n\n return {\n ok: true,\n yaml: stringifyProjectTemplateV1(document),\n document,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAiEA,SAAgB,WAAW,MAA6D;CACtF,QAAQ,KAAK,MAAb;EACE,KAAK,UACH,OAAO,cAAc,KAAK,MAAM,CAAC,CAAC;EACpC,KAAK,gBACH,OAAO,KAAK;EAId,KAAK;EACL,KAAK;EACL,KAAK,oBACH;CACJ;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,0BACd,MACA,cACA,cAC6E;CAC7E,MAAM,WAAoC,CAAC,GAAG,KAAK,QAAQ;CAC3D,MAAM,SAAmC,CAAC;CAE1C,KAAK,MAAM,SAAS,KAAK,SAAS;EAChC,MAAM,OAAO,aAAa,MAAM,OAAO;EAEvC,IAAI,SAAS,KAAA,GAAW;GACtB,SAAS,KAAK;IACZ,SAAS,MAAM;IACf,YAAY,MAAM;IAClB,OACE;GAEJ,CAAC;GACD;EACF;EAEA,IAAI;EACJ,IAAI;GAKF,WAAW,iCAAiC,IAAI;EAClD,SAAS,GAAG;GACV,SAAS,KAAK;IACZ,SAAS,MAAM;IACf,YAAY,MAAM;IAClB,OAAO,+CAA+C,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;GACjG,CAAC;GACD;EACF;EAEA,MAAM,OAAO,aAAa,MAAM,OAAO;EACvC,MAAM,WAAW,SAAS,KAAA,IAAY,KAAA,IAAY,WAAW,IAAI;EAEjE,OAAO,KAAK;GACV,IAAI,MAAM;GACV,MAAM;GACN,QAAQ,MAAM;GACd,GAAI,aAAa,KAAA,IAAY,EAAE,SAAS,IAAI,CAAC;EAC/C,CAAC;CACH;CAOA,OAAO;EAAE,UAAA;GAF6B,QAAQ;GAA4B;EAE1D;EAAG;CAAS;AAC9B;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,2BAA2B,UAAqC;CAC9E,OAAO,KAAK,UAAU,UAAU;EAAE,WAAW;EAAG,SAAS;CAAM,CAAC;AAClE;;;;;;;;;;;;;;;AAgBA,SAAgB,0BACd,WACA,gBACA,cACA,cAC8B;CAE9B,MAAM,EAAE,UAAU,aAAa,0BADlB,6BAA6B,WAAW,cACO,GAAG,cAAc,YAAY;CAEzF,IAAI,SAAS,SAAS,GAAG,OAAO;EAAE,IAAI;EAAO;CAAS;CAUtD,uBAAuB,QAAQ;CAE/B,OAAO;EACL,IAAI;EACJ,MAAM,2BAA2B,QAAQ;EACzC;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-middle-layer",
3
- "version": "1.69.2",
3
+ "version": "1.70.0",
4
4
  "description": "Pl Middle Layer",
5
5
  "keywords": [],
6
6
  "license": "UNLICENSED",
@@ -30,24 +30,24 @@
30
30
  "utility-types": "^3.11.0",
31
31
  "yaml": "^2.8.0",
32
32
  "zod": "~3.25.76",
33
- "@milaboratories/computable": "2.9.8",
34
- "@milaboratories/pf-driver": "1.9.1",
35
33
  "@milaboratories/helpers": "1.14.5",
34
+ "@milaboratories/pf-spec-driver": "1.5.1",
36
35
  "@milaboratories/columns-collection-driver": "0.2.4",
36
+ "@milaboratories/computable": "2.9.8",
37
+ "@milaboratories/pf-driver": "1.9.1",
38
+ "@milaboratories/pl-deployments": "3.0.16",
37
39
  "@milaboratories/pl-client": "3.16.0",
38
40
  "@milaboratories/pl-drivers": "1.16.19",
39
- "@milaboratories/pf-spec-driver": "1.5.1",
40
- "@milaboratories/pl-errors": "1.4.38",
41
- "@milaboratories/pl-deployments": "3.0.16",
42
41
  "@milaboratories/pl-http": "1.2.4",
43
- "@milaboratories/pl-model-common": "1.48.0",
44
42
  "@milaboratories/pl-model-backend": "1.4.23",
45
43
  "@milaboratories/pl-model-middle-layer": "1.32.0",
44
+ "@milaboratories/pl-tree": "1.14.2",
45
+ "@milaboratories/pl-model-common": "1.48.0",
46
46
  "@milaboratories/resolve-helper": "1.1.3",
47
47
  "@milaboratories/ts-helpers": "1.8.6",
48
- "@milaboratories/pl-tree": "1.14.2",
49
- "@platforma-sdk/block-tools": "2.14.6",
48
+ "@milaboratories/pl-errors": "1.4.38",
50
49
  "@platforma-sdk/workflow-tengo": "6.9.0",
50
+ "@platforma-sdk/block-tools": "2.14.6",
51
51
  "@platforma-sdk/model": "1.83.9"
52
52
  },
53
53
  "devDependencies": {
@@ -57,8 +57,8 @@
57
57
  "typescript": "7.0.2",
58
58
  "vitest": "^4.1.3",
59
59
  "@milaboratories/build-configs": "2.0.1",
60
- "@milaboratories/ts-builder": "1.7.2",
61
- "@milaboratories/ts-configs": "1.4.0"
60
+ "@milaboratories/ts-configs": "1.4.0",
61
+ "@milaboratories/ts-builder": "1.7.2"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">=22.19.0"
@@ -24,6 +24,35 @@ function providerFrom(params: Record<string, TemplateParamsResult>) {
24
24
 
25
25
  const ok = (value: unknown): TemplateParamsResult => ({ value });
26
26
 
27
+ describe("labels", () => {
28
+ test("an entry and a problem both carry the block's label, not its id", () => {
29
+ // `simpleStructure` labels every block after its id, which cannot tell a label read from
30
+ // the structure apart from an id copied into the field. Distinct labels here can.
31
+ const structure: ProjectStructure = {
32
+ groups: [
33
+ {
34
+ id: "g1",
35
+ label: "G1",
36
+ blocks: [
37
+ { id: "b1", label: "MiXCR Clonotyping", renderingMode: "Heavy" },
38
+ { id: "b2", label: "Clonotype Browser", renderingMode: "Heavy" },
39
+ ],
40
+ },
41
+ ],
42
+ };
43
+ const walk = walkProjectForTemplateExport(structure, providerFrom({ b1: ok({}) }));
44
+
45
+ expect(walk.entries).toEqual([{ blockId: "b1", blockLabel: "MiXCR Clonotyping", params: {} }]);
46
+ expect(walk.problems).toEqual([
47
+ {
48
+ blockId: "b2",
49
+ blockLabel: "Clonotype Browser",
50
+ error: "Block state is unavailable, so its template params could not be derived",
51
+ },
52
+ ]);
53
+ });
54
+ });
55
+
27
56
  describe("order", () => {
28
57
  test("entries come out in structure order — no sort, none needed", () => {
29
58
  // The structure IS the topological order: a block can only legally reference
@@ -101,7 +130,7 @@ describe("collecting each block's descriptor output", () => {
101
130
  providerFrom({ mixcr: ok(params) }),
102
131
  );
103
132
 
104
- expect(walk.entries).toEqual([{ blockId: "mixcr", params }]);
133
+ expect(walk.entries).toEqual([{ blockId: "mixcr", blockLabel: "mixcr", params }]);
105
134
  });
106
135
 
107
136
  test("a block with nothing to project yields empty params", () => {
@@ -112,7 +141,9 @@ describe("collecting each block's descriptor output", () => {
112
141
  providerFrom({ "pool-explorer": ok({}) }),
113
142
  );
114
143
 
115
- expect(walk.entries).toEqual([{ blockId: "pool-explorer", params: {} }]);
144
+ expect(walk.entries).toEqual([
145
+ { blockId: "pool-explorer", blockLabel: "pool-explorer", params: {} },
146
+ ]);
116
147
  expect(walk.problems).toEqual([]);
117
148
  });
118
149
  });
@@ -145,7 +176,7 @@ describe("what the walk does with params", () => {
145
176
  providerFrom({ block1: ok({}) }),
146
177
  );
147
178
 
148
- expect(walk.entries).toEqual([{ blockId: "block1", params: {} }]);
179
+ expect(walk.entries).toEqual([{ blockId: "block1", blockLabel: "block1", params: {} }]);
149
180
  });
150
181
 
151
182
  test("a wrapper's contents are never inspected, whatever they are", () => {
@@ -210,7 +241,7 @@ describe("problems", () => {
210
241
  // Every offending block is reported at once rather than aborting on the
211
242
  // first, so the user fixes them in one pass.
212
243
  expect(walk.problems).toEqual([
213
- { blockId: "b", error: "templateParams() threw: not exportable yet" },
244
+ { blockId: "b", blockLabel: "b", error: "templateParams() threw: not exportable yet" },
214
245
  ]);
215
246
  expect(walk.entries.map((e) => e.blockId)).toEqual(["a", "c"]);
216
247
  });
@@ -228,6 +259,7 @@ describe("problems", () => {
228
259
  expect(walk.problems).toEqual([
229
260
  {
230
261
  blockId: "ghost",
262
+ blockLabel: "ghost",
231
263
  error: "Block state is unavailable, so its template params could not be derived",
232
264
  },
233
265
  ]);
@@ -249,7 +281,11 @@ describe("problems", () => {
249
281
 
250
282
  expect(walk.entries).toEqual([]);
251
283
  expect(walk.problems).toEqual([
252
- { blockId: "odd", error: `templateParams() must return an object, got ${expected}` },
284
+ {
285
+ blockId: "odd",
286
+ blockLabel: "odd",
287
+ error: `templateParams() must return an object, got ${expected}`,
288
+ },
253
289
  ]);
254
290
  });
255
291
  });
@@ -20,6 +20,9 @@ export type TemplateExportEntry = {
20
20
  * already stored in params need no translation.
21
21
  */
22
22
  readonly blockId: string;
23
+ /** The block's label from the project structure, carried so a problem found downstream can
24
+ * name the block to a person. Not written to the template. */
25
+ readonly blockLabel: string;
23
26
  /**
24
27
  * The block's params exactly as it projected them.
25
28
  *
@@ -33,6 +36,9 @@ export type TemplateExportEntry = {
33
36
  /** Why one block could not be exported. */
34
37
  export type TemplateExportProblem = {
35
38
  readonly blockId: string;
39
+ /** The block's label as the project structure holds it. An id means nothing to the person who
40
+ * pressed Export; this is what a UI shows. */
41
+ readonly blockLabel: string;
36
42
  readonly error: string;
37
43
  };
38
44
 
@@ -91,19 +97,20 @@ export function walkProjectForTemplateExport(
91
97
  const entries: TemplateExportEntry[] = [];
92
98
  const problems: TemplateExportProblem[] = [];
93
99
 
94
- for (const { id } of allBlocks(structure)) {
100
+ for (const { id, label } of allBlocks(structure)) {
95
101
  const derived = paramsProvider(id);
96
102
 
97
103
  if (derived === undefined) {
98
104
  problems.push({
99
105
  blockId: id,
106
+ blockLabel: label,
100
107
  error: "Block state is unavailable, so its template params could not be derived",
101
108
  });
102
109
  continue;
103
110
  }
104
111
 
105
112
  if (derived.error !== undefined) {
106
- problems.push({ blockId: id, error: derived.error });
113
+ problems.push({ blockId: id, blockLabel: label, error: derived.error });
107
114
  continue;
108
115
  }
109
116
 
@@ -117,12 +124,13 @@ export function walkProjectForTemplateExport(
117
124
  if (typeof params !== "object" || params === null || Array.isArray(params)) {
118
125
  problems.push({
119
126
  blockId: id,
127
+ blockLabel: label,
120
128
  error: `templateParams() must return an object, got ${typeName(params)}`,
121
129
  });
122
130
  continue;
123
131
  }
124
132
 
125
- entries.push({ blockId: id, params: params as Record<string, unknown> });
133
+ entries.push({ blockId: id, blockLabel: label, params: params as Record<string, unknown> });
126
134
  }
127
135
 
128
136
  return { entries, problems };
@@ -198,6 +198,27 @@ describe("problems", () => {
198
198
  expect(result.problems[0].error).toContain("declares no kind");
199
199
  });
200
200
 
201
+ test("a problem raised here carries the block's label from the walk", () => {
202
+ // Labels distinct from ids, or the assertion could not tell a label carried through from
203
+ // an id copied into the field.
204
+ const structure: ProjectStructure = {
205
+ groups: [
206
+ {
207
+ id: "g1",
208
+ label: "G1",
209
+ blocks: [{ id: "legacy", label: "Old Aligner", renderingMode: "Heavy" }],
210
+ },
211
+ ],
212
+ };
213
+ const result = exportOf(structure, { legacy: ok({}) }, () => undefined);
214
+
215
+ expect(result.ok).toBe(false);
216
+ if (result.ok) return;
217
+ expect(result.problems).toEqual([
218
+ { blockId: "legacy", blockLabel: "Old Aligner", error: expect.stringContaining("no kind") },
219
+ ]);
220
+ });
221
+
201
222
  test("a malformed stored kind reference is a problem, not a throw", () => {
202
223
  const result = exportOf(
203
224
  simpleStructure("a"),
@@ -385,14 +406,16 @@ describe("assembleProjectTemplateV1", () => {
385
406
  test("carries the walk's problems through unchanged", () => {
386
407
  const { document, problems } = assembleProjectTemplateV1(
387
408
  {
388
- entries: [{ blockId: "a", params: {} }],
389
- problems: [{ blockId: "ghost", error: "state unavailable" }],
409
+ entries: [{ blockId: "a", blockLabel: "a", params: {} }],
410
+ problems: [{ blockId: "ghost", blockLabel: "ghost", error: "state unavailable" }],
390
411
  },
391
412
  kindPerBlock,
392
413
  () => registrySpec,
393
414
  );
394
415
 
395
- expect(problems).toEqual([{ blockId: "ghost", error: "state unavailable" }]);
416
+ expect(problems).toEqual([
417
+ { blockId: "ghost", blockLabel: "ghost", error: "state unavailable" },
418
+ ]);
396
419
  expect(document.blocks.map((b) => b.id)).toEqual(["a"]);
397
420
  });
398
421
  });
@@ -126,6 +126,7 @@ export function assembleProjectTemplateV1(
126
126
  if (kind === undefined) {
127
127
  problems.push({
128
128
  blockId: entry.blockId,
129
+ blockLabel: entry.blockLabel,
129
130
  error:
130
131
  "Block declares no kind, so it cannot be written to a template: an entry's kind " +
131
132
  "carries the params contract the entry is typed against",
@@ -143,6 +144,7 @@ export function assembleProjectTemplateV1(
143
144
  } catch (e) {
144
145
  problems.push({
145
146
  blockId: entry.blockId,
147
+ blockLabel: entry.blockLabel,
146
148
  error: `Block's stored kind reference is malformed: ${e instanceof Error ? e.message : String(e)}`,
147
149
  });
148
150
  continue;