@milaboratories/pl-middle-layer 1.74.18 → 1.75.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 (60) hide show
  1. package/dist/middle_layer/block.cjs.map +1 -1
  2. package/dist/middle_layer/block.js.map +1 -1
  3. package/dist/middle_layer/build_stamp.cjs +1 -1
  4. package/dist/middle_layer/build_stamp.js +1 -1
  5. package/dist/middle_layer/column_links.cjs +106 -0
  6. package/dist/middle_layer/column_links.cjs.map +1 -0
  7. package/dist/middle_layer/column_links.js +104 -0
  8. package/dist/middle_layer/column_links.js.map +1 -0
  9. package/dist/middle_layer/middle_layer.cjs +1 -0
  10. package/dist/middle_layer/middle_layer.cjs.map +1 -1
  11. package/dist/middle_layer/middle_layer.js +1 -0
  12. package/dist/middle_layer/middle_layer.js.map +1 -1
  13. package/dist/middle_layer/project.cjs +45 -19
  14. package/dist/middle_layer/project.cjs.map +1 -1
  15. package/dist/middle_layer/project.d.ts.map +1 -1
  16. package/dist/middle_layer/project.js +46 -20
  17. package/dist/middle_layer/project.js.map +1 -1
  18. package/dist/middle_layer/project_overview.cjs +56 -24
  19. package/dist/middle_layer/project_overview.cjs.map +1 -1
  20. package/dist/middle_layer/project_overview.js +55 -25
  21. package/dist/middle_layer/project_overview.js.map +1 -1
  22. package/dist/model/args_derivation.cjs +153 -0
  23. package/dist/model/args_derivation.cjs.map +1 -0
  24. package/dist/model/args_derivation.d.ts +21 -0
  25. package/dist/model/args_derivation.d.ts.map +1 -0
  26. package/dist/model/args_derivation.js +148 -0
  27. package/dist/model/args_derivation.js.map +1 -0
  28. package/dist/model/project_helper.cjs +34 -8
  29. package/dist/model/project_helper.cjs.map +1 -1
  30. package/dist/model/project_helper.d.ts +18 -5
  31. package/dist/model/project_helper.d.ts.map +1 -1
  32. package/dist/model/project_helper.js +34 -8
  33. package/dist/model/project_helper.js.map +1 -1
  34. package/dist/model/project_model.cjs +1 -0
  35. package/dist/model/project_model.cjs.map +1 -1
  36. package/dist/model/project_model.d.ts +1 -1
  37. package/dist/model/project_model.js +1 -0
  38. package/dist/model/project_model.js.map +1 -1
  39. package/dist/mutator/project.cjs +54 -8
  40. package/dist/mutator/project.cjs.map +1 -1
  41. package/dist/mutator/project.d.ts +1 -0
  42. package/dist/mutator/project.d.ts.map +1 -1
  43. package/dist/mutator/project.js +54 -8
  44. package/dist/mutator/project.js.map +1 -1
  45. package/package.json +16 -16
  46. package/src/middle_layer/block.ts +4 -1
  47. package/src/middle_layer/column_links.test.ts +350 -0
  48. package/src/middle_layer/column_links.ts +152 -0
  49. package/src/middle_layer/middle_layer.ts +2 -0
  50. package/src/middle_layer/project.ts +39 -5
  51. package/src/middle_layer/project_overview.test.ts +200 -0
  52. package/src/middle_layer/project_overview.ts +82 -28
  53. package/src/model/args.test.ts +39 -1
  54. package/src/model/args_derivation.test.ts +326 -0
  55. package/src/model/args_derivation.ts +194 -0
  56. package/src/model/project_helper.ts +55 -25
  57. package/src/model/project_model.ts +2 -0
  58. package/src/mutator/project-v3.test.ts +105 -1
  59. package/src/mutator/project.ts +81 -24
  60. package/src/test/fake_project.ts +222 -0
@@ -24,10 +24,15 @@ import { finalityStopRules } from "./finality_stop_rules";
24
24
  import type { ComputableStableDefined, ComputableValueOrErrors } from "@milaboratories/computable";
25
25
  import { Computable } from "@milaboratories/computable";
26
26
  import { projectOverview } from "./project_overview";
27
+ import { blockDependencies, blockDependents, labelBlockColumnLinks } from "./column_links";
27
28
  import type { BlockPackSpecAny } from "../model";
28
29
  import { randomUUID } from "node:crypto";
29
30
  import { withProject, withProjectAuthored } from "../mutator/project";
30
- import type { ExtendedResourceData, PruningFunction } from "@milaboratories/pl-tree";
31
+ import type {
32
+ ExtendedResourceData,
33
+ PlTreeNodeAccessor,
34
+ PruningFunction,
35
+ } from "@milaboratories/pl-tree";
31
36
  import {
32
37
  SynchronizedTreeState,
33
38
  treeDumpStats,
@@ -38,6 +43,7 @@ import { setTimeout } from "node:timers/promises";
38
43
  import { frontendData } from "./frontend_path";
39
44
  import type { NavigationState } from "@milaboratories/pl-model-common";
40
45
  import { getBlockParameters, blockOutputs } from "./block";
46
+ import { getBlockPackInfo } from "./util";
41
47
  import type { FrontendData } from "../model/frontend";
42
48
  import type { ProjectId, ProjectStructure } from "../model/project_model";
43
49
  import { projectFieldName } from "../model/project_model";
@@ -56,9 +62,10 @@ import type {
56
62
  BlockSettings,
57
63
  BlockStateInternalV3,
58
64
  } from "@milaboratories/pl-model-middle-layer";
65
+ import type { ColumnLink } from "@milaboratories/pl-model-common";
59
66
  import { activeConfigs } from "./active_cfg";
60
67
  import { NavigationStates } from "./navigation_states";
61
- import { extractConfig, BLOCK_STORAGE_FACADE_VERSION } from "@platforma-sdk/model";
68
+ import { checkBlockFlag, extractConfig, BLOCK_STORAGE_FACADE_VERSION } from "@platforma-sdk/model";
62
69
  import fs from "node:fs/promises";
63
70
  import canonicalize from "canonicalize";
64
71
  import type { ProjectOverviewLight } from "./project_overview_light";
@@ -68,6 +75,9 @@ import { cacheBlockPackTemplate } from "../mutator/template/template_cache";
68
75
 
69
76
  type BlockStateComputables = {
70
77
  readonly fullState: Computable<BlockStateInternalV3>;
78
+ /** Column links of the block, nested into its full state. */
79
+ readonly dependencies: Computable<ColumnLink[] | undefined>;
80
+ readonly dependents: Computable<ColumnLink[] | undefined>;
71
81
  };
72
82
 
73
83
  function stringifyForDump(object: unknown): string {
@@ -94,6 +104,13 @@ function stringifyForDump(object: unknown): string {
94
104
  });
95
105
  }
96
106
 
107
+ /** Releases what the computables of a block hold. */
108
+ function resetBlockState(computables: BlockStateComputables): void {
109
+ computables.fullState.resetState();
110
+ computables.dependencies.resetState();
111
+ computables.dependents.resetState();
112
+ }
113
+
97
114
  /** Data access object, to manipulate and read single opened (!) project data. */
98
115
  export class Project {
99
116
  /** Data for the left panel, contain basic information about block status. */
@@ -292,8 +309,8 @@ export class Project {
292
309
  // Doing cleanup for deleted blocks
293
310
  for (const blockId of this.blockComputables.keys()) {
294
311
  if (!existingBlocks.has(blockId)) {
295
- const computable = this.blockComputables.get(blockId);
296
- if (computable !== undefined && computable !== null) computable.fullState.resetState();
312
+ const computables = this.blockComputables.get(blockId);
313
+ if (computables !== undefined && computables !== null) resetBlockState(computables);
297
314
  this.blockComputables.set(blockId, null);
298
315
  }
299
316
  }
@@ -749,13 +766,19 @@ export class Project {
749
766
  if (cached === undefined) {
750
767
  // state consists of inputs (args + ui state) and outputs
751
768
  const outputs = blockOutputs(this.projectTree.entry(), blockId, this.env);
769
+ const dependencies = blockDependencies(this.projectTree.entry(), blockId, this.env);
770
+ const dependents = blockDependents(this.projectTree.entry(), blockId, this.env);
752
771
  const fullState = Computable.make(
753
772
  (ctx) => {
773
+ // Links are computed only for a block whose UI reads them.
774
+ const linked = readsColumnLinks(ctx.accessor(this.projectTree.entry()).node(), blockId);
754
775
  return {
755
776
  parameters: getBlockParameters(this.projectTree.entry(), blockId, ctx),
756
777
  outputs,
757
778
  navigationState: this.navigationStates.getState(blockId),
758
779
  overview: this.overview,
780
+ dependencies: linked ? dependencies : undefined,
781
+ dependents: linked ? dependents : undefined,
759
782
  };
760
783
  },
761
784
  {
@@ -770,6 +793,9 @@ export class Project {
770
793
  ...v.parameters,
771
794
  outputs: newOutputs,
772
795
  navigationState: v.navigationState,
796
+ // The block state already nests the overview, so labelling by its titles adds
797
+ // nothing to watch.
798
+ columnLinks: labelBlockColumnLinks({ ...v, blockId }, v.overview?.blocks ?? []),
773
799
  } as BlockStateInternalV3;
774
800
  },
775
801
  },
@@ -777,6 +803,8 @@ export class Project {
777
803
 
778
804
  const computables: BlockStateComputables = {
779
805
  fullState: fullState.withPreCalculatedValueTree(),
806
+ dependencies,
807
+ dependents,
780
808
  };
781
809
 
782
810
  this.blockComputables.set(blockId, computables);
@@ -839,7 +867,7 @@ export class Project {
839
867
  this.overview.resetState();
840
868
  this.blockFrontends.forEach((c) => c.resetState());
841
869
  this.blockComputables.forEach((c) => {
842
- if (c !== null) c.fullState.resetState();
870
+ if (c !== null) resetBlockState(c);
843
871
  });
844
872
  this.activeConfigs.resetState();
845
873
  }
@@ -1022,6 +1050,12 @@ export function projectTreeTraverseStopRules(): Filter {
1022
1050
  return finalityStopRules(TreeFinality);
1023
1051
  }
1024
1052
 
1053
+ /** Whether the block's model declares that its UI reads the column links of the block state. */
1054
+ function readsColumnLinks(prj: PlTreeNodeAccessor, blockId: string): boolean {
1055
+ const info = getBlockPackInfo(prj, blockId);
1056
+ return info !== undefined && checkBlockFlag(info.cfg.featureFlags, "supportsColumnLinks");
1057
+ }
1058
+
1025
1059
  /** Returns true if sdk version of the block is old and we need to convert
1026
1060
  * ErrorLike errors to strings like it was.
1027
1061
  * We need it for keeping old blocks and new UI compatibility. */
@@ -0,0 +1,200 @@
1
+ import { Computable } from "@milaboratories/computable";
2
+ import type { ArgsIssue } from "@milaboratories/pl-model-common";
3
+ import type { PlTreeState } from "@milaboratories/pl-tree";
4
+ import { describe, expect, test } from "vitest";
5
+ import { projectFieldName } from "../model/project_model";
6
+ import { FakeProject } from "../test/fake_project";
7
+ import { argsAllowRun, type RunStateSource, withRunState } from "./project_overview";
8
+
9
+ const setThreshold: ArgsIssue = { severity: "error", message: "Set a threshold" };
10
+
11
+ describe("argsAllowRun", () => {
12
+ test("a block whose argsIssues found no errors in its current args may run", async () => {
13
+ const prj = new FakeProject();
14
+ prj.block("x", { currentArgs: prj.args({ n: 1 }), inputsValid: prj.argsErrors([], { n: 1 }) });
15
+
16
+ expect(await allowRun(prj.tree(), "x")).toBe(true);
17
+ });
18
+
19
+ test("a block with current args and no args errors record may run, as before the record", async () => {
20
+ // What a project saved by an older middle layer holds: current args and no `inputsValid`.
21
+ const prj = new FakeProject();
22
+ prj.block("x", { currentArgs: prj.args({ n: 1 }) });
23
+
24
+ expect(await allowRun(prj.tree(), "x")).toBe(true);
25
+ });
26
+
27
+ test("a block without current args may not run", async () => {
28
+ const prj = new FakeProject();
29
+ prj.block("x", { prodArgs: prj.args({ n: 1 }) });
30
+
31
+ expect(await allowRun(prj.tree(), "x")).toBe(false);
32
+ });
33
+
34
+ test("a block whose argsIssues found errors in its current args may not run", async () => {
35
+ const prj = new FakeProject();
36
+ prj.block("x", {
37
+ currentArgs: prj.args({ n: 1 }),
38
+ inputsValid: prj.argsErrors([setThreshold], { n: 1 }),
39
+ });
40
+
41
+ expect(await allowRun(prj.tree(), "x")).toBe(false);
42
+ });
43
+
44
+ test("errors found in the same args, whatever their key order, still apply", async () => {
45
+ const prj = new FakeProject();
46
+ prj.block("x", {
47
+ currentArgs: prj.args({ a: 1, b: { c: 2, d: 3 } }),
48
+ inputsValid: prj.argsErrors([setThreshold], { b: { d: 3, c: 2 }, a: 1 }),
49
+ });
50
+
51
+ expect(await allowRun(prj.tree(), "x")).toBe(false);
52
+ });
53
+
54
+ test("errors recorded for other args do not keep the block from running", async () => {
55
+ // What an older middle layer leaves behind: it writes new current args and never touches
56
+ // the `inputsValid` field.
57
+ const prj = new FakeProject();
58
+ prj.block("x", {
59
+ currentArgs: prj.args({ n: 2 }),
60
+ inputsValid: prj.argsErrors([setThreshold], { n: 1 }),
61
+ });
62
+
63
+ expect(await allowRun(prj.tree(), "x")).toBe(true);
64
+ });
65
+
66
+ test("a field that cannot be read does not keep the block from running", async () => {
67
+ const prj = new FakeProject();
68
+ prj.block("x", {
69
+ currentArgs: prj.args({ n: 1 }),
70
+ inputsValid: prj.value("json/object", [setThreshold]),
71
+ });
72
+
73
+ expect(await allowRun(prj.tree(), "x")).toBe(true);
74
+ });
75
+
76
+ test("may run again once the args and their record have no errors", async () => {
77
+ const prj = new FakeProject();
78
+ prj.block("x", {
79
+ currentArgs: prj.args({ n: 1 }),
80
+ inputsValid: prj.argsErrors([setThreshold], { n: 1 }),
81
+ });
82
+ const tree = prj.tree();
83
+ const computable = allowRunComputable(tree, "x");
84
+ expect(await computable.getValue()).toBe(false);
85
+
86
+ prj.block("x", { currentArgs: prj.args({ n: 2 }), inputsValid: prj.argsErrors([], { n: 2 }) });
87
+ tree.updateFromResourceData(prj.changes());
88
+
89
+ expect(await computable.getValue()).toBe(true);
90
+ });
91
+
92
+ test("current args changed without their record leave the old errors behind", async () => {
93
+ const prj = new FakeProject();
94
+ const argsErrors = prj.argsErrors([setThreshold], { n: 1 });
95
+ prj.block("x", { currentArgs: prj.args({ n: 1 }), inputsValid: argsErrors });
96
+ const tree = prj.tree();
97
+ const computable = allowRunComputable(tree, "x");
98
+ expect(await computable.getValue()).toBe(false);
99
+
100
+ prj.block("x", { currentArgs: prj.args({ n: 2 }), inputsValid: argsErrors });
101
+ tree.updateFromResourceData(prj.changes());
102
+
103
+ expect(await computable.getValue()).toBe(true);
104
+ });
105
+ });
106
+
107
+ describe("withRunState", () => {
108
+ test("a block whose results are up to date lets a changed dependent run despite its own args errors", () => {
109
+ const [upstream, dependent] = withRunState([
110
+ block({ id: "a", inputsValid: false }),
111
+ block({ id: "b", stale: true, upstreams: ["a"] }),
112
+ ]);
113
+ expect(upstream.canRun).toBe(false);
114
+ expect(dependent.canRun).toBe(true);
115
+ });
116
+
117
+ test("a changed block with args errors keeps its dependents from running", () => {
118
+ const [upstream, dependent, further] = withRunState([
119
+ block({ id: "a", stale: true, inputsValid: false }),
120
+ block({ id: "b", stale: true, upstreams: ["a"] }),
121
+ block({ id: "c", upstreams: ["a", "b"] }),
122
+ ]);
123
+ expect(upstream.canRun).toBe(false);
124
+ expect(dependent.canRun).toBe(false);
125
+ expect(further).toMatchObject({ stale: true, canRun: false });
126
+ });
127
+
128
+ test("a block with failed outputs and args errors keeps its dependents from running", () => {
129
+ const [, dependent] = withRunState([
130
+ block({ id: "a", outputErrors: true, inputsValid: false }),
131
+ block({ id: "b", stale: true, upstreams: ["a"] }),
132
+ ]);
133
+ expect(dependent.canRun).toBe(false);
134
+ });
135
+
136
+ test("a block becomes stale and runnable when an upstream block is stale", () => {
137
+ const [upstream, dependent] = withRunState([
138
+ block({ id: "a", stale: true }),
139
+ block({ id: "b", upstreams: ["a"] }),
140
+ ]);
141
+ expect(upstream).toMatchObject({ stale: true, canRun: true });
142
+ expect(dependent).toMatchObject({ stale: true, canRun: true });
143
+ });
144
+
145
+ test("a block with up-to-date results has nothing to run", () => {
146
+ const [b] = withRunState([block({ id: "a" })]);
147
+ expect(b).toMatchObject({ stale: false, canRun: false });
148
+ });
149
+
150
+ test("an up-to-date block whose validity is not about its args keeps its dependents from running", () => {
151
+ // A block whose code cannot run here, or whose model predates args issues and checks more
152
+ // than its args, as before args issues.
153
+ const [upstream, dependent] = withRunState([
154
+ block({ id: "a", inputsValid: undefined, inputsValidFromArgs: false }),
155
+ block({ id: "b", stale: true, upstreams: ["a"] }),
156
+ ]);
157
+ expect(upstream.canRun).toBe(false);
158
+ expect(dependent.canRun).toBe(false);
159
+ });
160
+
161
+ test("a changed block that references a missing block cannot run", () => {
162
+ const [b] = withRunState([block({ id: "a", stale: true, missingReference: true })]);
163
+ expect(b.canRun).toBe(false);
164
+ });
165
+ });
166
+
167
+ // Internals
168
+
169
+ /** Whether the block may run, read the way the project overview reads it. */
170
+ function allowRunComputable(tree: PlTreeState, blockId: string): Computable<boolean> {
171
+ const entry = tree.entry();
172
+ return Computable.make((ctx) => {
173
+ const prj = ctx.accessor(entry).node();
174
+ const currentArgs = prj
175
+ .traverse({
176
+ field: projectFieldName(blockId, "currentArgs"),
177
+ assertFieldType: "Dynamic",
178
+ stableIfNotFound: true,
179
+ })
180
+ ?.getDataAsJson();
181
+ return argsAllowRun(prj, blockId, currentArgs);
182
+ });
183
+ }
184
+
185
+ function allowRun(tree: PlTreeState, blockId: string): Promise<boolean> {
186
+ return allowRunComputable(tree, blockId).getValue();
187
+ }
188
+
189
+ /** A block with model API version 2 that ran with its current args, without errors, with no upstream blocks. */
190
+ function block(overrides: Partial<RunStateSource> & Pick<RunStateSource, "id">): RunStateSource {
191
+ return {
192
+ stale: false,
193
+ outputErrors: false,
194
+ inputsValid: true,
195
+ inputsValidFromArgs: true,
196
+ missingReference: false,
197
+ upstreams: [],
198
+ ...overrides,
199
+ };
200
+ }
@@ -1,4 +1,4 @@
1
- import type { PlTreeEntry } from "@milaboratories/pl-tree";
1
+ import type { PlTreeEntry, PlTreeNodeAccessor } from "@milaboratories/pl-tree";
2
2
  import type { ComputableStableDefined } from "@milaboratories/computable";
3
3
  import { Computable } from "@milaboratories/computable";
4
4
  import type { ProjectRenderingState, ProjectStructure } from "../model/project_model";
@@ -31,6 +31,7 @@ import { getBlockPackInfo } from "./util";
31
31
  import { resourceIdToString, type SignedResourceId } from "@milaboratories/pl-client";
32
32
  import { omitBy, isEqual } from "es-toolkit";
33
33
  import { getDebugFlags } from "../debug";
34
+ import { readArgsErrorsField } from "../model/args_derivation";
34
35
 
35
36
  type BlockInfo = {
36
37
  argsRid?: SignedResourceId;
@@ -168,6 +169,7 @@ export function projectOverview(
168
169
  subtitle,
169
170
  tags,
170
171
  inputsValid,
172
+ inputsValidFromArgs,
171
173
  sdkVersion,
172
174
  featureFlags,
173
175
  isIncompatibleWithRuntime,
@@ -260,10 +262,11 @@ export function projectOverview(
260
262
  }) as ComputableStableDefined<string[]>,
261
263
  ),
262
264
  // inputsValid: for modelAPIVersion 2, it's true if currentArgs exists (args derivation succeeded)
265
+ // and the block's argsIssues() is known to have found no errors in them.
263
266
  // For older blocks, use the inputsValid callback from config
264
267
  inputsValid:
265
268
  cfg.modelAPIVersion === BLOCK_STORAGE_FACADE_VERSION
266
- ? info.currentArguments !== undefined
269
+ ? argsAllowRun(prj, id, info.currentArguments)
267
270
  : cfg.inputsValid
268
271
  ? (computableFromCfgOrRF(
269
272
  env,
@@ -280,6 +283,7 @@ export function projectOverview(
280
283
  },
281
284
  }) as ComputableStableDefined<boolean>)
282
285
  : undefined,
286
+ inputsValidFromArgs: cfg.modelAPIVersion === BLOCK_STORAGE_FACADE_VERSION,
283
287
  sdkVersion: codeWithInfo?.sdkVersion,
284
288
  featureFlags: codeWithInfo?.featureFlags ?? {},
285
289
  isIncompatibleWithRuntime: false,
@@ -317,6 +321,7 @@ export function projectOverview(
317
321
  settings,
318
322
  sections,
319
323
  inputsValid,
324
+ inputsValidFromArgs: inputsValidFromArgs === true,
320
325
  updateInfo: {},
321
326
  currentBlockPack: bp?.info?.source,
322
327
  updates,
@@ -336,32 +341,14 @@ export function projectOverview(
336
341
  };
337
342
  },
338
343
  {
339
- postprocessValue: (value) => {
340
- const cantRun = new Set<string>();
341
- const staleBlocks = new Set<string>();
342
- return {
343
- ...value,
344
- blocks: value.blocks.map((b) => {
345
- if (!b.inputsValid) cantRun.add(b.id);
346
- if (b.stale) staleBlocks.add(b.id);
347
- const stale = b.stale || b.upstreams.findIndex((u) => staleBlocks.has(u)) !== -1;
348
- const canRun =
349
- (stale || b.outputErrors) &&
350
- Boolean(b.inputsValid) &&
351
- !b.missingReference &&
352
- b.upstreams.findIndex((u) => cantRun.has(u)) === -1;
353
- const bb = {
354
- ...b,
355
- canRun,
356
- stale,
357
- updateSuggestions: b.updates?.suggestions ?? [],
358
- updatedBlockPack: b.updates?.mainSuggestion,
359
- };
360
- delete bb["updates"];
361
- return bb;
362
- }),
363
- };
364
- },
344
+ postprocessValue: (value) => ({
345
+ ...value,
346
+ blocks: withRunState(value.blocks).map(({ updates, inputsValidFromArgs: _, ...b }) => ({
347
+ ...b,
348
+ updateSuggestions: updates?.suggestions ?? [],
349
+ updatedBlockPack: updates?.mainSuggestion,
350
+ })),
351
+ }),
365
352
  onRecalculation: getDebugFlags().logProjectOverviewStat
366
353
  ? (stats) => {
367
354
  console.log(`[projectOverview] ${JSON.stringify(stats)}`);
@@ -370,3 +357,70 @@ export function projectOverview(
370
357
  },
371
358
  ).withStableType();
372
359
  }
360
+
361
+ /**
362
+ * Whether the args of a block with model API version 2 let it run: the block has current args,
363
+ * and its `inputsValid` field records no errors its `argsIssues()` found in them (see
364
+ * `readArgsErrorsField`).
365
+ *
366
+ * @param currentArgs - The block's current args as decoded from its `currentArgs` field;
367
+ * `undefined` when it has none
368
+ */
369
+ export function argsAllowRun(
370
+ prj: PlTreeNodeAccessor,
371
+ blockId: string,
372
+ currentArgs: unknown,
373
+ ): boolean {
374
+ if (currentArgs === undefined) return false;
375
+ const argsErrors = prj.getField({
376
+ field: projectFieldName(blockId, "inputsValid"),
377
+ assertFieldType: "Dynamic",
378
+ stableIfNotFound: true,
379
+ });
380
+ return readArgsErrorsField(argsErrors?.value?.getData(), currentArgs).length === 0;
381
+ }
382
+
383
+ /** What the run state of a block is derived from. */
384
+ export type RunStateSource = {
385
+ id: string;
386
+ /** Whether the block's own args differ from those its results were rendered with. */
387
+ stale: boolean;
388
+ outputErrors: boolean;
389
+ inputsValid: boolean | undefined;
390
+ /**
391
+ * Whether `inputsValid` tells only whether the block's current args may run: a block with model
392
+ * API version 2 this runtime is compatible with. Otherwise it also stands for a block whose code
393
+ * cannot run here, or for checks of a model that predates args issues.
394
+ */
395
+ inputsValidFromArgs: boolean;
396
+ missingReference: boolean;
397
+ /** Every block upstream of this one, direct or not. */
398
+ upstreams: string[];
399
+ };
400
+
401
+ /**
402
+ * Adds the run state to each block of a project, the blocks listed upstream first. A block is
403
+ * stale when its own args changed or an upstream block is stale. It can run when it needs to run
404
+ * (it is stale or its outputs failed), its args allow it, it references no missing block, and no
405
+ * upstream block that needs to run is kept from running. An upstream block whose `inputsValid`
406
+ * comes from its args and whose results are up to date feeds its dependents even when its args
407
+ * could not run again; any other upstream block that is not valid keeps them from running.
408
+ */
409
+ export function withRunState<B extends RunStateSource>(
410
+ blocks: B[],
411
+ ): (B & { stale: boolean; canRun: boolean })[] {
412
+ const cantRun = new Set<string>();
413
+ const staleBlocks = new Set<string>();
414
+ return blocks.map((b) => {
415
+ if (b.stale) staleBlocks.add(b.id);
416
+ const stale = b.stale || b.upstreams.some((u) => staleBlocks.has(u));
417
+ const needsRun = stale || b.outputErrors;
418
+ if (!b.inputsValid && (needsRun || !b.inputsValidFromArgs)) cantRun.add(b.id);
419
+ const canRun =
420
+ needsRun &&
421
+ Boolean(b.inputsValid) &&
422
+ !b.missingReference &&
423
+ !b.upstreams.some((u) => cantRun.has(u));
424
+ return { ...b, stale, canRun };
425
+ });
426
+ }
@@ -1,7 +1,12 @@
1
1
  import { describe, expect, it, test } from "vitest";
2
2
  import canonicalize from "canonicalize";
3
3
  import type { ColumnUniversalId } from "@milaboratories/pl-model-common";
4
- import { canonicalizeJson, createColumnDiscoveredId } from "@milaboratories/pl-model-common";
4
+ import {
5
+ canonicalizeJson,
6
+ createColumnDiscoveredId,
7
+ createColumnValueId,
8
+ createGlobalPObjectId,
9
+ } from "@milaboratories/pl-model-common";
5
10
  import { inferAllReferencedBlocks, outputRef } from "./args";
6
11
 
7
12
  describe("inferAllReferencedBlocks", () => {
@@ -158,3 +163,36 @@ test("a block id in a map KEY is an upstream too", () => {
158
163
 
159
164
  expect([...result.upstreams].sort()).toEqual(["A", "B", "D"]);
160
165
  });
166
+
167
+ describe("a column value id names its producer as an upstream", () => {
168
+ const value = createColumnValueId({
169
+ column: createGlobalPObjectId("samples", "metadata.condition"),
170
+ value: "treated",
171
+ });
172
+
173
+ test("as a value", () => {
174
+ const result = inferAllReferencedBlocks({ target: value });
175
+
176
+ expect(result.upstreams).toEqual(new Set(["samples"]));
177
+ expect(result.missingReferences).toBe(false);
178
+ });
179
+
180
+ test("as a map key", () => {
181
+ const result = inferAllReferencedBlocks({ ranks: { [value]: 1 } });
182
+
183
+ expect(result.upstreams).toEqual(new Set(["samples"]));
184
+ });
185
+
186
+ test("under one extra stringify pass", () => {
187
+ const result = inferAllReferencedBlocks({ blob: JSON.stringify(value) });
188
+
189
+ expect(result.upstreams).toEqual(new Set(["samples"]));
190
+ });
191
+
192
+ test("outside the allowed set it is a missing reference", () => {
193
+ const result = inferAllReferencedBlocks({ target: value }, new Set(["other"]));
194
+
195
+ expect(result.upstreams.size).toBe(0);
196
+ expect(result.missingReferences).toBe(true);
197
+ });
198
+ });