@milaboratories/pl-model-common 1.47.3 → 1.49.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 (242) hide show
  1. package/dist/author_marker.d.ts +1 -2
  2. package/dist/author_marker.d.ts.map +1 -1
  3. package/dist/base64.d.ts +4 -6
  4. package/dist/base64.d.ts.map +1 -1
  5. package/dist/block_state.d.ts +13 -9
  6. package/dist/block_state.d.ts.map +1 -1
  7. package/dist/bmodel/block_config.d.ts +51 -29
  8. package/dist/bmodel/block_config.d.ts.map +1 -1
  9. package/dist/bmodel/block_kind_ref.cjs +45 -0
  10. package/dist/bmodel/block_kind_ref.cjs.map +1 -0
  11. package/dist/bmodel/block_kind_ref.d.ts +55 -0
  12. package/dist/bmodel/block_kind_ref.d.ts.map +1 -0
  13. package/dist/bmodel/block_kind_ref.js +43 -0
  14. package/dist/bmodel/block_kind_ref.js.map +1 -0
  15. package/dist/bmodel/code.d.ts +4 -5
  16. package/dist/bmodel/code.d.ts.map +1 -1
  17. package/dist/bmodel/container.d.ts +13 -5
  18. package/dist/bmodel/container.d.ts.map +1 -1
  19. package/dist/bmodel/index.cjs +4 -0
  20. package/dist/bmodel/index.d.ts +2 -1
  21. package/dist/bmodel/index.js +2 -1
  22. package/dist/bmodel/normalization.d.ts +1 -3
  23. package/dist/bmodel/normalization.d.ts.map +1 -1
  24. package/dist/bmodel/types.d.ts +1 -2
  25. package/dist/bmodel/types.d.ts.map +1 -1
  26. package/dist/branding.d.ts +2 -3
  27. package/dist/branding.d.ts.map +1 -1
  28. package/dist/columns/accessor_traversal.d.ts +6 -8
  29. package/dist/columns/accessor_traversal.d.ts.map +1 -1
  30. package/dist/columns/column_registry.d.ts +2 -3
  31. package/dist/columns/column_registry.d.ts.map +1 -1
  32. package/dist/columns/column_selector.d.ts +9 -9
  33. package/dist/columns/column_selector.d.ts.map +1 -1
  34. package/dist/columns/dedup.cjs +1 -1
  35. package/dist/columns/dedup.cjs.map +1 -1
  36. package/dist/columns/dedup.d.ts +2 -4
  37. package/dist/columns/dedup.d.ts.map +1 -1
  38. package/dist/columns/dedup.js +1 -1
  39. package/dist/columns/dedup.js.map +1 -1
  40. package/dist/columns/providers.cjs +1 -1
  41. package/dist/columns/providers.cjs.map +1 -1
  42. package/dist/columns/providers.d.ts +4 -5
  43. package/dist/columns/providers.d.ts.map +1 -1
  44. package/dist/columns/providers.js +1 -1
  45. package/dist/columns/providers.js.map +1 -1
  46. package/dist/columns/types.d.ts +12 -9
  47. package/dist/columns/types.d.ts.map +1 -1
  48. package/dist/common_types.d.ts +4 -6
  49. package/dist/common_types.d.ts.map +1 -1
  50. package/dist/dialog/index.d.ts +4 -5
  51. package/dist/dialog/index.d.ts.map +1 -1
  52. package/dist/driver_kit.d.ts +2 -2
  53. package/dist/driver_kit.d.ts.map +1 -1
  54. package/dist/drivers/ChunkedStreamReader.d.ts +3 -5
  55. package/dist/drivers/ChunkedStreamReader.d.ts.map +1 -1
  56. package/dist/drivers/blob.d.ts +36 -25
  57. package/dist/drivers/blob.d.ts.map +1 -1
  58. package/dist/drivers/columns/columns_collection_driver.d.ts +6 -7
  59. package/dist/drivers/columns/columns_collection_driver.d.ts.map +1 -1
  60. package/dist/drivers/columns/discover_columns_options.d.ts +9 -8
  61. package/dist/drivers/columns/discover_columns_options.d.ts.map +1 -1
  62. package/dist/drivers/index.cjs +5 -0
  63. package/dist/drivers/index.d.ts +5 -3
  64. package/dist/drivers/index.js +3 -3
  65. package/dist/drivers/interfaces.d.ts +2 -3
  66. package/dist/drivers/interfaces.d.ts.map +1 -1
  67. package/dist/drivers/log.d.ts +26 -28
  68. package/dist/drivers/log.d.ts.map +1 -1
  69. package/dist/drivers/ls.d.ts +37 -27
  70. package/dist/drivers/ls.d.ts.map +1 -1
  71. package/dist/drivers/pframe/column_filter.d.ts +1 -3
  72. package/dist/drivers/pframe/column_filter.d.ts.map +1 -1
  73. package/dist/drivers/pframe/data_info.d.ts +77 -50
  74. package/dist/drivers/pframe/data_info.d.ts.map +1 -1
  75. package/dist/drivers/pframe/data_types.d.ts +50 -45
  76. package/dist/drivers/pframe/data_types.d.ts.map +1 -1
  77. package/dist/drivers/pframe/driver.d.ts +5 -5
  78. package/dist/drivers/pframe/driver.d.ts.map +1 -1
  79. package/dist/drivers/pframe/filter_spec.d.ts +10 -12
  80. package/dist/drivers/pframe/filter_spec.d.ts.map +1 -1
  81. package/dist/drivers/pframe/find_columns.d.ts +2 -4
  82. package/dist/drivers/pframe/find_columns.d.ts.map +1 -1
  83. package/dist/drivers/pframe/index.cjs +5 -0
  84. package/dist/drivers/pframe/index.d.ts +5 -3
  85. package/dist/drivers/pframe/index.js +3 -3
  86. package/dist/drivers/pframe/linker_columns.d.ts +4 -10
  87. package/dist/drivers/pframe/linker_columns.d.ts.map +1 -1
  88. package/dist/drivers/pframe/pframe.d.ts +3 -3
  89. package/dist/drivers/pframe/pframe.d.ts.map +1 -1
  90. package/dist/drivers/pframe/query/query_common.d.ts +52 -46
  91. package/dist/drivers/pframe/query/query_common.d.ts.map +1 -1
  92. package/dist/drivers/pframe/query/query_data.d.ts +26 -21
  93. package/dist/drivers/pframe/query/query_data.d.ts.map +1 -1
  94. package/dist/drivers/pframe/query/query_spec.d.ts +22 -19
  95. package/dist/drivers/pframe/query/query_spec.d.ts.map +1 -1
  96. package/dist/drivers/pframe/query/utils.d.ts +15 -11
  97. package/dist/drivers/pframe/query/utils.d.ts.map +1 -1
  98. package/dist/drivers/pframe/spec/anchored.d.ts +3 -5
  99. package/dist/drivers/pframe/spec/anchored.d.ts.map +1 -1
  100. package/dist/drivers/pframe/spec/discovered_column.d.ts +10 -11
  101. package/dist/drivers/pframe/spec/discovered_column.d.ts.map +1 -1
  102. package/dist/drivers/pframe/spec/filtered_column.d.ts +17 -18
  103. package/dist/drivers/pframe/spec/filtered_column.d.ts.map +1 -1
  104. package/dist/drivers/pframe/spec/ids.cjs +151 -0
  105. package/dist/drivers/pframe/spec/ids.cjs.map +1 -1
  106. package/dist/drivers/pframe/spec/ids.d.ts +63 -11
  107. package/dist/drivers/pframe/spec/ids.d.ts.map +1 -1
  108. package/dist/drivers/pframe/spec/ids.js +150 -3
  109. package/dist/drivers/pframe/spec/ids.js.map +1 -1
  110. package/dist/drivers/pframe/spec/index.cjs +5 -0
  111. package/dist/drivers/pframe/spec/index.d.ts +3 -3
  112. package/dist/drivers/pframe/spec/index.js +3 -3
  113. package/dist/drivers/pframe/spec/native_id.d.ts +3 -3
  114. package/dist/drivers/pframe/spec/native_id.d.ts.map +1 -1
  115. package/dist/drivers/pframe/spec/overridden.d.ts +14 -16
  116. package/dist/drivers/pframe/spec/overridden.d.ts.map +1 -1
  117. package/dist/drivers/pframe/spec/selectors.d.ts +22 -22
  118. package/dist/drivers/pframe/spec/selectors.d.ts.map +1 -1
  119. package/dist/drivers/pframe/spec/spec.cjs +31 -0
  120. package/dist/drivers/pframe/spec/spec.cjs.map +1 -1
  121. package/dist/drivers/pframe/spec/spec.d.ts +94 -70
  122. package/dist/drivers/pframe/spec/spec.d.ts.map +1 -1
  123. package/dist/drivers/pframe/spec/spec.js +31 -1
  124. package/dist/drivers/pframe/spec/spec.js.map +1 -1
  125. package/dist/drivers/pframe/spec_driver.d.ts +30 -25
  126. package/dist/drivers/pframe/spec_driver.d.ts.map +1 -1
  127. package/dist/drivers/pframe/table.d.ts +1 -3
  128. package/dist/drivers/pframe/table.d.ts.map +1 -1
  129. package/dist/drivers/pframe/table_calculate.d.ts +55 -46
  130. package/dist/drivers/pframe/table_calculate.d.ts.map +1 -1
  131. package/dist/drivers/pframe/table_common.d.ts +9 -10
  132. package/dist/drivers/pframe/table_common.d.ts.map +1 -1
  133. package/dist/drivers/pframe/unique_values.d.ts +3 -3
  134. package/dist/drivers/pframe/unique_values.d.ts.map +1 -1
  135. package/dist/drivers/upload.d.ts +2 -3
  136. package/dist/drivers/upload.d.ts.map +1 -1
  137. package/dist/drivers/urls.d.ts +7 -8
  138. package/dist/drivers/urls.d.ts.map +1 -1
  139. package/dist/errors.d.ts +35 -36
  140. package/dist/errors.d.ts.map +1 -1
  141. package/dist/flags/block_flags.cjs.map +1 -1
  142. package/dist/flags/block_flags.d.ts +7 -7
  143. package/dist/flags/block_flags.d.ts.map +1 -1
  144. package/dist/flags/block_flags.js.map +1 -1
  145. package/dist/flags/flag_utils.d.ts +6 -8
  146. package/dist/flags/flag_utils.d.ts.map +1 -1
  147. package/dist/flags/type_utils.d.ts +6 -7
  148. package/dist/flags/type_utils.d.ts.map +1 -1
  149. package/dist/httpAuth.d.ts +4 -5
  150. package/dist/httpAuth.d.ts.map +1 -1
  151. package/dist/index.cjs +30 -0
  152. package/dist/index.d.ts +15 -3
  153. package/dist/index.js +9 -3
  154. package/dist/json.d.ts +11 -12
  155. package/dist/json.d.ts.map +1 -1
  156. package/dist/navigation.d.ts +16 -11
  157. package/dist/navigation.d.ts.map +1 -1
  158. package/dist/plid.cjs +1 -1
  159. package/dist/plid.cjs.map +1 -1
  160. package/dist/plid.d.ts +8 -9
  161. package/dist/plid.d.ts.map +1 -1
  162. package/dist/plid.js +1 -1
  163. package/dist/plid.js.map +1 -1
  164. package/dist/pool/entry.d.ts +8 -6
  165. package/dist/pool/entry.d.ts.map +1 -1
  166. package/dist/pool/query.d.ts +3 -4
  167. package/dist/pool/query.d.ts.map +1 -1
  168. package/dist/pool/spec.d.ts +33 -30
  169. package/dist/pool/spec.d.ts.map +1 -1
  170. package/dist/pool_entry.d.ts +2 -3
  171. package/dist/pool_entry.d.ts.map +1 -1
  172. package/dist/project_id.d.ts +1 -3
  173. package/dist/project_id.d.ts.map +1 -1
  174. package/dist/ref.d.ts +22 -23
  175. package/dist/ref.d.ts.map +1 -1
  176. package/dist/resource_types.d.ts +2 -3
  177. package/dist/resource_types.d.ts.map +1 -1
  178. package/dist/services/node_service_handlers.d.ts +1 -3
  179. package/dist/services/node_service_handlers.d.ts.map +1 -1
  180. package/dist/services/service_capabilities.d.ts +7 -9
  181. package/dist/services/service_capabilities.d.ts.map +1 -1
  182. package/dist/services/service_declarations.d.ts +2 -2
  183. package/dist/services/service_declarations.d.ts.map +1 -1
  184. package/dist/services/service_injectors.d.ts +7 -8
  185. package/dist/services/service_injectors.d.ts.map +1 -1
  186. package/dist/services/service_registry.d.ts +2 -4
  187. package/dist/services/service_registry.d.ts.map +1 -1
  188. package/dist/services/service_types.d.ts +24 -26
  189. package/dist/services/service_types.d.ts.map +1 -1
  190. package/dist/template/index.cjs +20 -0
  191. package/dist/template/index.d.ts +5 -0
  192. package/dist/template/index.js +5 -0
  193. package/dist/template/kind_selector.cjs +92 -0
  194. package/dist/template/kind_selector.cjs.map +1 -0
  195. package/dist/template/kind_selector.d.ts +78 -0
  196. package/dist/template/kind_selector.d.ts.map +1 -0
  197. package/dist/template/kind_selector.js +87 -0
  198. package/dist/template/kind_selector.js.map +1 -0
  199. package/dist/template/project_template_v1.cjs +231 -0
  200. package/dist/template/project_template_v1.cjs.map +1 -0
  201. package/dist/template/project_template_v1.d.ts +217 -0
  202. package/dist/template/project_template_v1.d.ts.map +1 -0
  203. package/dist/template/project_template_v1.js +225 -0
  204. package/dist/template/project_template_v1.js.map +1 -0
  205. package/dist/template/template_ref_form.cjs +73 -0
  206. package/dist/template/template_ref_form.cjs.map +1 -0
  207. package/dist/template/template_ref_form.d.ts +75 -0
  208. package/dist/template/template_ref_form.d.ts.map +1 -0
  209. package/dist/template/template_ref_form.js +72 -0
  210. package/dist/template/template_ref_form.js.map +1 -0
  211. package/dist/template/template_relocate.cjs +67 -0
  212. package/dist/template/template_relocate.cjs.map +1 -0
  213. package/dist/template/template_relocate.d.ts +36 -0
  214. package/dist/template/template_relocate.d.ts.map +1 -0
  215. package/dist/template/template_relocate.js +67 -0
  216. package/dist/template/template_relocate.js.map +1 -0
  217. package/dist/utag.d.ts +2 -4
  218. package/dist/utag.d.ts.map +1 -1
  219. package/dist/util.d.ts +2 -3
  220. package/dist/util.d.ts.map +1 -1
  221. package/dist/value_or_error.d.ts +2 -3
  222. package/dist/value_or_error.d.ts.map +1 -1
  223. package/package.json +5 -5
  224. package/src/bmodel/block_kind_ref.ts +59 -0
  225. package/src/bmodel/container.ts +9 -0
  226. package/src/bmodel/index.ts +1 -0
  227. package/src/columns/dedup.ts +1 -1
  228. package/src/columns/providers.ts +1 -1
  229. package/src/drivers/pframe/spec/ids.test.ts +90 -0
  230. package/src/drivers/pframe/spec/ids.ts +191 -1
  231. package/src/drivers/pframe/spec/spec.ts +31 -0
  232. package/src/flags/block_flags.ts +1 -1
  233. package/src/index.ts +1 -0
  234. package/src/plid.ts +5 -5
  235. package/src/template/index.ts +4 -0
  236. package/src/template/kind_selector.ts +126 -0
  237. package/src/template/project_template_v1.test.ts +315 -0
  238. package/src/template/project_template_v1.ts +444 -0
  239. package/src/template/template_ref_form.test.ts +86 -0
  240. package/src/template/template_ref_form.ts +108 -0
  241. package/src/template/template_relocate.test.ts +239 -0
  242. package/src/template/template_relocate.ts +89 -0
@@ -0,0 +1,239 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import type { ColumnUniversalId } from "../drivers";
3
+ import {
4
+ createColumnDiscoveredId,
5
+ createColumnFilteredId,
6
+ createColumnOverriddenId,
7
+ } from "../drivers";
8
+ import { canonicalizeJson } from "../json";
9
+ import { createGlobalPObjectId, createLocalPObjectId } from "../pool";
10
+ import { createPlRef } from "../ref";
11
+ import { relocateBlockIds } from "./template_relocate";
12
+
13
+ /**
14
+ * Pointing a block's params at the project being built.
15
+ *
16
+ * This is the whole of what a template does about references, so what these tests pin is not
17
+ * "the walk visits everything" but the two properties a textual rewrite could not have: a
18
+ * value that merely looks like an id is left alone, and an identifier comes back canonical.
19
+ */
20
+
21
+ const leafId = (blockId: string, name: string) =>
22
+ createGlobalPObjectId(blockId, name) as ColumnUniversalId;
23
+
24
+ const to = (from: string, into: string) => new Map([[from, into]]);
25
+
26
+ describe("relocateBlockIds", () => {
27
+ test("a PlRef is repointed where it sits", () => {
28
+ const params = { input: createPlRef("old", "reads") };
29
+
30
+ expect(relocateBlockIds(params, to("old", "new"))).toEqual({
31
+ input: createPlRef("new", "reads"),
32
+ });
33
+ });
34
+
35
+ test("an identifier held as a canonical string is repointed too", () => {
36
+ const params = { anchor: leafId("old", "reads") };
37
+
38
+ expect(relocateBlockIds(params, to("old", "new"))).toEqual({ anchor: leafId("new", "reads") });
39
+ });
40
+
41
+ test("an id the map does not mention is left alone — that is the ordering rule", () => {
42
+ // A reference to an entry further down the file: the caller's map holds only entries
43
+ // already created, so this keeps its template-local id and the applied block reports
44
+ // itself as missing references instead of pointing at a block below it.
45
+ const params = { input: createPlRef("later", "reads") };
46
+
47
+ expect(relocateBlockIds(params, to("earlier", "new"))).toEqual(params);
48
+ });
49
+
50
+ test("a local leaf has no block id, and comes back the same string", () => {
51
+ const local = createLocalPObjectId("some/path") as ColumnUniversalId;
52
+
53
+ expect(relocateBlockIds({ column: local }, to("old", "new"))).toEqual({ column: local });
54
+ });
55
+
56
+ test("nothing to relocate means the very same params object", () => {
57
+ const params = { species: "hsa", numbers: [1, 2, 3], note: `see {"not":"an id"}` };
58
+
59
+ expect(relocateBlockIds(params, to("old", "new"))).toEqual(params);
60
+ });
61
+
62
+ test("an empty map hands the params back untouched", () => {
63
+ const params = { input: createPlRef("old", "reads") };
64
+
65
+ expect(relocateBlockIds(params, new Map())).toBe(params);
66
+ });
67
+
68
+ describe("what a textual rewrite got wrong", () => {
69
+ test("a value that merely equals a block id is NOT rewritten", () => {
70
+ // The whole reason this is structural. `producedBy` is spec data that happens to carry
71
+ // the same string; a textual pass over the payload rewrote it along with the reference.
72
+ const id = createColumnOverriddenId({
73
+ source: leafId("old", "clonotypes"),
74
+ specOverrides: { domain: { producedBy: "old", chain: "old" } },
75
+ });
76
+
77
+ const moved = relocateBlockIds({ a: id }, to("old", "new")) as { a: string };
78
+ const key = JSON.parse(moved.a) as { source: string; specOverrides: { domain: unknown } };
79
+
80
+ expect(key.source).toContain('"blockId":"new"');
81
+ expect(key.specOverrides.domain).toEqual({ producedBy: "old", chain: "old" });
82
+ });
83
+
84
+ test("an identifier in a map KEY is repointed, and the result is canonical again", () => {
85
+ // Rebuilding is what restores canonical order: the keys sort by their new values, so
86
+ // the result equals the id a fresh project would build for the same column.
87
+ const qual = [{ axis: { name: "sampleId" }, contextDomain: {} }];
88
+ const before = createColumnDiscoveredId({
89
+ column: leafId("src", "clonotypes"),
90
+ queriesQualifications: { [leafId("aaa", "x")]: qual, [leafId("zzz", "y")]: qual },
91
+ });
92
+
93
+ const moved = relocateBlockIds(
94
+ { a: before },
95
+ new Map([
96
+ ["src", "src2"],
97
+ ["aaa", "zz9"],
98
+ ["zzz", "aa1"],
99
+ ]),
100
+ ) as { a: string };
101
+
102
+ expect(moved.a).toBe(
103
+ createColumnDiscoveredId({
104
+ column: leafId("src2", "clonotypes"),
105
+ queriesQualifications: { [leafId("zz9", "x")]: qual, [leafId("aa1", "y")]: qual },
106
+ }),
107
+ );
108
+ expect(moved.a).toBe(canonicalizeJson(JSON.parse(moved.a)));
109
+ });
110
+ });
111
+
112
+ describe("a domain that qualifies an axis", () => {
113
+ // An axis a block produced names that block in its domain — `pl7.app/sampleId` is
114
+ // qualified by the block that defined the samples, a clonotype key by the run that called
115
+ // the clonotypes. A chart bound to such an axis is bound to a reference.
116
+ const axis = (runId: string) => ({
117
+ kind: "axis",
118
+ name: "pl7.app/vdj/clonotypeKey",
119
+ type: "String",
120
+ domain: { "pl7.app/vdj/chain": "IGHeavy", "pl7.app/vdj/clonotypingRunId": runId },
121
+ });
122
+
123
+ test("an entry naming one of this template's entries is repointed", () => {
124
+ expect(relocateBlockIds({ x: axis("old") }, to("old", "new"))).toEqual({ x: axis("new") });
125
+ });
126
+
127
+ test("an entry that is not an entry id is left alone", () => {
128
+ // The map is the whole test of what is a reference: nothing reads a value's shape, so an
129
+ // ordinary qualifier cannot be caught by it.
130
+ const moved = relocateBlockIds({ x: axis("old") }, to("old", "new")) as {
131
+ x: { domain: Record<string, string> };
132
+ };
133
+
134
+ expect(moved.x.domain["pl7.app/vdj/chain"]).toBe("IGHeavy");
135
+ });
136
+
137
+ test("a domain naming a block this template does not carry keeps it", () => {
138
+ // The ordering rule again: an entry created later is not in the map yet, and the applied
139
+ // block reports itself as missing references rather than pointing at the wrong block.
140
+ expect(relocateBlockIds({ x: axis("later") }, to("earlier", "new"))).toEqual({
141
+ x: axis("later"),
142
+ });
143
+ });
144
+
145
+ test("a qualifier that merely collides with an entry id is NOT a reference", () => {
146
+ // Entry ids are validated only as non-empty strings, so a hand-written template names its
147
+ // entries readably. `algo` is a qualifier of the data, and an entry called `closest` does
148
+ // not make it one: matching the map is not evidence, the key is.
149
+ const params = { method: { domain: { algo: "closest", chain: "IGHeavy" } } };
150
+
151
+ expect(relocateBlockIds(params, to("closest", "new"))).toEqual(params);
152
+ });
153
+
154
+ test("a domain INSIDE a recognized identifier is still spec data, and stays", () => {
155
+ // The boundary of the rule above. This id is taken apart by the structural remapper,
156
+ // which never descends to the generic object case, so its overrides are untouched.
157
+ const id = createColumnOverriddenId({
158
+ source: leafId("old", "clonotypes"),
159
+ specOverrides: { domain: { producedBy: "old" } },
160
+ });
161
+
162
+ const moved = relocateBlockIds({ a: id }, to("old", "new")) as { a: string };
163
+ const key = JSON.parse(moved.a) as { specOverrides: { domain: unknown } };
164
+
165
+ expect(key.specOverrides.domain).toEqual({ producedBy: "old" });
166
+ });
167
+ });
168
+
169
+ describe("depth", () => {
170
+ test("three nested forms are repointed at the bottom and rebuilt on the way up", () => {
171
+ const deep = (block: string) =>
172
+ createColumnFilteredId({
173
+ source: createColumnOverriddenId({
174
+ source: leafId(block, "clonotypes"),
175
+ specOverrides: { domain: { species: "hsa" } },
176
+ }),
177
+ axisFilters: [[0, "IGH"]],
178
+ });
179
+
180
+ expect(relocateBlockIds({ anchor: deep("old") }, to("old", "new"))).toEqual({
181
+ anchor: deep("new"),
182
+ });
183
+ });
184
+
185
+ test("a discovered column's linker path names other blocks, and each is repointed", () => {
186
+ // The case one wrapper around the whole identifier had to cover: several ids, from
187
+ // several blocks, at several depths.
188
+ const disc = (a: string, b: string, c: string) =>
189
+ createColumnDiscoveredId({
190
+ column: leafId(a, "clonotypes"),
191
+ path: [
192
+ { type: "linker", column: leafId(b, "cell-to-clone") },
193
+ { type: "linker", column: leafId(c, "clone-to-gene") },
194
+ ],
195
+ });
196
+
197
+ const moved = relocateBlockIds(
198
+ { anchor: disc("A", "B", "C") },
199
+ new Map([
200
+ ["A", "A2"],
201
+ ["B", "B2"],
202
+ ["C", "C2"],
203
+ ]),
204
+ );
205
+
206
+ expect(moved).toEqual({ anchor: disc("A2", "B2", "C2") });
207
+ });
208
+
209
+ test("escape padding is peeled and put back as it was found", () => {
210
+ const padded = (block: string) => JSON.stringify(leafId(block, "reads"));
211
+
212
+ expect(relocateBlockIds({ hit: padded("old") }, to("old", "new"))).toEqual({
213
+ hit: padded("new"),
214
+ });
215
+ });
216
+
217
+ test("identifiers are reached in arrays and nested objects alike", () => {
218
+ const params = {
219
+ inputs: [createPlRef("old", "a"), { nested: leafId("old", "b") }],
220
+ deeper: { list: [[createPlRef("old", "c")]] },
221
+ };
222
+
223
+ expect(relocateBlockIds(params, to("old", "new"))).toEqual({
224
+ inputs: [createPlRef("new", "a"), { nested: leafId("new", "b") }],
225
+ deeper: { list: [[createPlRef("new", "c")]] },
226
+ });
227
+ });
228
+
229
+ test("params keyed BY a column id have the key repointed", () => {
230
+ // A block may key its params by column — per-column settings. A key carries a
231
+ // reference exactly as much as a value does.
232
+ const params = { [leafId("old", "reads")]: { visible: true } };
233
+
234
+ expect(relocateBlockIds(params, to("old", "new"))).toEqual({
235
+ [leafId("new", "reads")]: { visible: true },
236
+ });
237
+ });
238
+ });
239
+ });
@@ -0,0 +1,89 @@
1
+ import { isPlainObject } from "es-toolkit";
2
+
3
+ import { BlockScopedDomain, isColumnUniversalKey, remapColumnIdBlockIds } from "../drivers";
4
+
5
+ /**
6
+ * Point every column identifier in a block's params at the blocks of the project being built.
7
+ *
8
+ * The whole of what a template does about references, and it lives here — in the package the
9
+ * block's own bundle imports — because knowing which values carry block ids is knowing the
10
+ * reference system. The engine carrying the params neither marks them, reads them, nor
11
+ * rewrites them: it hands the block its params and this map, and takes back what comes out.
12
+ *
13
+ * Params travel verbatim precisely so that this is possible. A file holds a `PlRef` as the
14
+ * object the block stored and a column id as the canonical string the block stored, with no
15
+ * marker of any kind, and the identifiers are found here by recognizing them — the same way
16
+ * the project's own dependency detector finds them in live args.
17
+ *
18
+ * Rewriting is structural, never textual: an identifier is taken apart, its `blockId` fields
19
+ * are replaced, and it is rebuilt canonically. That is what keeps a value that merely *looks*
20
+ * like an id — an axis filter, a domain this package can see belongs to an identifier it
21
+ * knows — from being rewritten along with it, and what re-sorts a qualifications map whose
22
+ * keys are identifiers.
23
+ *
24
+ * A domain reached on its own is the exception, and `relocateDomain` below says why: an axis is
25
+ * qualified by the block that produced it. Only the domain keys known to name a block are
26
+ * repointed — matching an entry id is not on its own evidence of a reference.
27
+ *
28
+ * An id the map does not mention is left as it is. That is the ordering rule doing its work:
29
+ * a caller building the map as it creates blocks passes only the entries already created, so
30
+ * a reference to an entry further down the file stays pointing at a block that does not
31
+ * exist, and the applied block reports itself as missing references rather than being wired
32
+ * to something below it.
33
+ *
34
+ * @param params Whatever the block projected, as the document stored it
35
+ * @param blockIds template-local entry id → the block id that entry was given
36
+ */
37
+ export function relocateBlockIds<T>(params: T, blockIds: ReadonlyMap<string, string>): T {
38
+ if (blockIds.size === 0) return params;
39
+ const remapBlockId = (blockId: string) => blockIds.get(blockId) ?? blockId;
40
+
41
+ const walk = (node: unknown): unknown => {
42
+ // Any string may be an identifier under any amount of escaping; one that is not comes
43
+ // back as the very same string, so this needs no test of its own here.
44
+ if (typeof node === "string") return remapColumnIdBlockIds(node, remapBlockId);
45
+
46
+ // Before the generic object case: an identifier IS an object, and descending into one
47
+ // would rewrite the strings nested in it piecemeal instead of rebuilding the whole id —
48
+ // losing the bottom-up canonicalization that keeps the result a valid identifier.
49
+ if (isColumnUniversalKey(node)) return remapColumnIdBlockIds(node, remapBlockId);
50
+
51
+ if (Array.isArray(node)) return node.map(walk);
52
+
53
+ if (typeof node === "object" && node !== null) {
54
+ // Keys as well as values: params may be keyed by column id — per-column settings, say
55
+ // — and a key is exactly as much of a reference as a value is.
56
+ return Object.fromEntries(
57
+ Object.entries(node).map(([key, value]) => [
58
+ remapColumnIdBlockIds(key, remapBlockId),
59
+ key === "domain" && isPlainObject(value) ? relocateDomain(value) : walk(value),
60
+ ]),
61
+ );
62
+ }
63
+
64
+ return node;
65
+ };
66
+
67
+ /**
68
+ * The block-naming entries of a domain, repointed at the project being built: an axis a block
69
+ * produced names that block in its domain, so those are references like any other.
70
+ *
71
+ * Only the keys {@link BlockScopedDomain} lists, because a template's entry ids are arbitrary
72
+ * non-empty strings — a hand-written template may name an entry `closest`, and a qualifier
73
+ * reading `closest` is not a reference to it. Matching the map is not on its own enough to
74
+ * tell one from the other; the key is.
75
+ *
76
+ * Reached only from the generic object case — inside an identifier this package recognizes a
77
+ * domain is spec data and stays as it is, which is what keeps an overridden column's
78
+ * `specOverrides.domain` untouched.
79
+ */
80
+ const relocateDomain = (domain: Record<string, unknown>): Record<string, unknown> =>
81
+ Object.fromEntries(
82
+ Object.entries(domain).map(([key, value]) => [
83
+ key,
84
+ typeof value === "string" && BlockScopedDomain.has(key) ? remapBlockId(value) : walk(value),
85
+ ]),
86
+ );
87
+
88
+ return walk(params) as T;
89
+ }