@toolproof-core/lib 1.0.3 → 1.0.5

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 (85) hide show
  1. package/dist/artifacts/artifacts.d.ts +4 -3
  2. package/dist/artifacts/artifacts.d.ts.map +1 -1
  3. package/dist/firebase/firebaseAdminHelpers.d.ts +2 -2
  4. package/dist/firebase/firebaseAdminHelpers.d.ts.map +1 -1
  5. package/dist/firebase/firebaseAdminHelpers.js.map +1 -1
  6. package/dist/shared.d.ts +33 -0
  7. package/dist/shared.d.ts.map +1 -0
  8. package/dist/shared.js +67 -0
  9. package/dist/shared.js.map +1 -0
  10. package/dist/types/types.d.ts +2 -5
  11. package/dist/types/types.d.ts.map +1 -1
  12. package/dist/types/types.js.map +1 -1
  13. package/dist/utils/extractFrom.d.ts +5 -0
  14. package/dist/utils/extractFrom.d.ts.map +1 -0
  15. package/dist/utils/extractFrom.js +72 -0
  16. package/dist/utils/extractFrom.js.map +1 -0
  17. package/dist/utils/extractFromCosmosData.d.ts +9 -0
  18. package/dist/utils/extractFromCosmosData.d.ts.map +1 -0
  19. package/dist/utils/extractFromCosmosData.js +20 -0
  20. package/dist/utils/extractFromCosmosData.js.map +1 -0
  21. package/dist/utils/extractFromResourceDict.d.ts +12 -0
  22. package/dist/utils/extractFromResourceDict.d.ts.map +1 -0
  23. package/dist/utils/extractFromResourceDict.js +23 -0
  24. package/dist/utils/extractFromResourceDict.js.map +1 -0
  25. package/dist/utils/extractFromResourceMap.d.ts +12 -0
  26. package/dist/utils/extractFromResourceMap.d.ts.map +1 -0
  27. package/dist/utils/extractFromResourceMap.js +23 -0
  28. package/dist/utils/extractFromResourceMap.js.map +1 -0
  29. package/dist/utils/extractFromResourcesByResourceTypeHandle.d.ts +8 -0
  30. package/dist/utils/extractFromResourcesByResourceTypeHandle.d.ts.map +1 -0
  31. package/dist/utils/extractFromResourcesByResourceTypeHandle.js +31 -0
  32. package/dist/utils/extractFromResourcesByResourceTypeHandle.js.map +1 -0
  33. package/dist/utils/extractFromStrategy.d.ts +5 -0
  34. package/dist/utils/extractFromStrategy.d.ts.map +1 -0
  35. package/dist/utils/extractFromStrategy.js +72 -0
  36. package/dist/utils/extractFromStrategy.js.map +1 -0
  37. package/dist/utils/extraction.d.ts +12 -0
  38. package/dist/utils/extraction.d.ts.map +1 -0
  39. package/dist/utils/extraction.js +27 -0
  40. package/dist/utils/extraction.js.map +1 -0
  41. package/dist/utils/generateFrom.d.ts +4 -0
  42. package/dist/utils/generateFrom.d.ts.map +1 -0
  43. package/dist/utils/generateFrom.js +18 -0
  44. package/dist/utils/generateFrom.js.map +1 -0
  45. package/dist/utils/generateFromTo.d.ts +4 -0
  46. package/dist/utils/generateFromTo.d.ts.map +1 -0
  47. package/dist/utils/generateFromTo.js +18 -0
  48. package/dist/utils/generateFromTo.js.map +1 -0
  49. package/dist/utils/parallelization.d.ts +3 -0
  50. package/dist/utils/parallelization.d.ts.map +1 -0
  51. package/dist/utils/parallelization.js +136 -0
  52. package/dist/utils/parallelization.js.map +1 -0
  53. package/dist/utils/resourceChain.d.ts +22 -0
  54. package/dist/utils/resourceChain.d.ts.map +1 -0
  55. package/dist/utils/resourceChain.js +38 -0
  56. package/dist/utils/resourceChain.js.map +1 -0
  57. package/dist/utils/roleResourceBinding.d.ts +5 -0
  58. package/dist/utils/roleResourceBinding.d.ts.map +1 -0
  59. package/dist/utils/roleResourceBinding.js +74 -0
  60. package/dist/utils/roleResourceBinding.js.map +1 -0
  61. package/dist/utils/runnableStrategyCreation.d.ts +8 -0
  62. package/dist/utils/runnableStrategyCreation.d.ts.map +1 -0
  63. package/dist/utils/runnableStrategyCreation.js +24 -0
  64. package/dist/utils/runnableStrategyCreation.js.map +1 -0
  65. package/dist/utils/stepCreation.d.ts +10 -0
  66. package/dist/utils/stepCreation.d.ts.map +1 -0
  67. package/dist/utils/stepCreation.js +80 -0
  68. package/dist/utils/stepCreation.js.map +1 -0
  69. package/package.json +48 -41
  70. package/src/_lib/setup/firebaseAdminInit.ts +38 -38
  71. package/src/artifacts/artifacts.ts +40 -40
  72. package/src/firebase/firebaseAdminHelpers.ts +157 -157
  73. package/src/shared.ts +126 -0
  74. package/src/types/types.ts +20 -24
  75. package/src/utils/extractFrom.ts +93 -0
  76. package/src/utils/extractFromResourcesByResourceTypeHandle.ts +55 -0
  77. package/src/utils/{creation.ts → generateFrom.ts} +26 -26
  78. package/src/utils/parallelization.ts +177 -0
  79. package/src/utils/resourceChain.ts +66 -0
  80. package/src/utils/roleResourceBinding.ts +109 -0
  81. package/src/utils/runnableStrategyCreation.ts +47 -0
  82. package/src/utils/stepCreation.ts +112 -0
  83. package/tsconfig.json +14 -14
  84. package/tsconfig.tsbuildinfo +1 -1
  85. package/src/utils/resourceMapExtraction.ts +0 -47
package/src/shared.ts ADDED
@@ -0,0 +1,126 @@
1
+ import type {
2
+ CreationContextJson,
3
+ JsonDataJson,
4
+ ResourceIdentityJson,
5
+ ResourceInputPotentialJson,
6
+ ResourceJson,
7
+ ResourceMissingJson,
8
+ ResourceOutputPotentialJson,
9
+ ResourceTypeIdentityJson,
10
+ } from '@toolproof-core/schema';
11
+
12
+ import { resolveResourceChain } from './utils/resourceChain.js';
13
+ import {
14
+ bindInputRefInStrategyState,
15
+ bindInputResInStrategyState,
16
+ clearInputBindingInStrategyState,
17
+ } from './utils/roleResourceBinding.js';
18
+ import { createRunnableStrategy } from './utils/runnableStrategyCreation.js';
19
+ import {
20
+ cloneForStep,
21
+ cloneWhileStep,
22
+ createBranchStepFromJobPairs,
23
+ createJobStepFromJob,
24
+ createLoopStepFromJobs,
25
+ } from './utils/stepCreation.js';
26
+
27
+
28
+ export const RESOURCE_CHAIN = {
29
+ resolveResourceChain,
30
+ } as const;
31
+
32
+
33
+ export const ROLE_RESOURCE_BINDINGS = {
34
+ bindInputResInStrategyState,
35
+ bindInputRefInStrategyState,
36
+ clearInputBindingInStrategyState,
37
+ } as const;
38
+
39
+
40
+ export const RUNNABLE_STRATEGY_CREATION = {
41
+ createRunnableStrategy,
42
+ } as const;
43
+
44
+
45
+ export const STEP_CREATION = {
46
+ createJobStepFromJob,
47
+ createLoopStepFromJobs,
48
+ createBranchStepFromJobPairs,
49
+ cloneForStep,
50
+ cloneWhileStep,
51
+ } as const;
52
+
53
+
54
+ function asMaterializedResource(
55
+ base: {
56
+ identity: ResourceIdentityJson;
57
+ resourceTypeHandle: ResourceTypeIdentityJson;
58
+ creationContext: CreationContextJson;
59
+ },
60
+ nucleus: JsonDataJson,
61
+ opts?: { path?: string; timestamp?: string }
62
+ ): ResourceJson {
63
+ return {
64
+ identity: base.identity,
65
+ resourceTypeHandle: base.resourceTypeHandle,
66
+ creationContext: base.creationContext,
67
+ resourceShellKind: 'materialized',
68
+ version: 1,
69
+ timestamp: opts?.timestamp ?? new Date().toISOString(),
70
+ path: opts?.path ?? '',
71
+ nucleus,
72
+ } as unknown as ResourceJson;
73
+ }
74
+
75
+
76
+ export const RESOURCE_CREATION = {
77
+ createMissingResource(identity: ResourceIdentityJson, resourceTypeHandle: ResourceTypeIdentityJson): ResourceMissingJson {
78
+ return {
79
+ identity,
80
+ resourceTypeHandle,
81
+ resourceShellKind: 'missing',
82
+ };
83
+ },
84
+
85
+ createOutputPotentialResource(
86
+ identity: ResourceIdentityJson,
87
+ resourceTypeHandle: ResourceTypeIdentityJson,
88
+ creationContext: CreationContextJson
89
+ ): ResourceOutputPotentialJson {
90
+ return {
91
+ identity,
92
+ resourceTypeHandle,
93
+ creationContext,
94
+ resourceShellKind: 'outputPotential',
95
+ };
96
+ },
97
+
98
+ createInputPotentialResource(
99
+ identity: ResourceIdentityJson,
100
+ resourceTypeHandle: ResourceTypeIdentityJson,
101
+ creationContext: CreationContextJson
102
+ ): ResourceInputPotentialJson {
103
+ return {
104
+ identity,
105
+ resourceTypeHandle,
106
+ creationContext,
107
+ resourceShellKind: 'inputPotential',
108
+ };
109
+ },
110
+
111
+ createMaterializedResourceFromPotential(
112
+ potential: ResourceOutputPotentialJson | ResourceInputPotentialJson,
113
+ nucleus: JsonDataJson,
114
+ opts?: { path?: string; timestamp?: string }
115
+ ): ResourceJson {
116
+ return asMaterializedResource(
117
+ {
118
+ identity: potential.identity,
119
+ resourceTypeHandle: potential.resourceTypeHandle,
120
+ creationContext: potential.creationContext,
121
+ },
122
+ nucleus,
123
+ opts
124
+ );
125
+ },
126
+ } as const;
@@ -1,25 +1,21 @@
1
- import type {
2
- StepKindJson,
3
- ResourceJson,
4
- ResourceRoleIdentityJson,
5
- ResourceRoleValueJson,
6
- ResourceTypeIdentityJson,
7
- } from '@toolproof-core/schema';
8
- import { CONSTANTS } from '../artifacts/artifacts.js';
9
- import { MAPPINGS } from '../artifacts/artifacts.js';
10
-
11
- export type Bucket = typeof CONSTANTS.Persistence.Buckets.tp_resources;
12
-
13
- export type Collection = keyof typeof CONSTANTS.Persistence.Collections;
14
-
15
- export type SchemaLike = Record<string, unknown>;
16
-
17
- export type Role = { identity: ResourceRoleIdentityJson } & ResourceRoleValueJson;
18
-
19
- export type ResourceMap = Record<ResourceTypeIdentityJson, ResourceJson[]>;
20
-
21
- export type IdentityName = keyof typeof MAPPINGS.IdentityNameToIdentityPrefix;
22
-
23
- export type IdentityStringByIdentityName<K extends IdentityName> = `${(typeof MAPPINGS.IdentityNameToIdentityPrefix)[K]}${string}`;
24
-
1
+ import type {
2
+ StepKindJson,
3
+ ResourceJson,
4
+ ResourceTypeIdentityJson,
5
+ } from '@toolproof-core/schema';
6
+ import { CONSTANTS } from '../artifacts/artifacts.js';
7
+ import { MAPPINGS } from '../artifacts/artifacts.js';
8
+
9
+ export type Bucket = typeof CONSTANTS.Persistence.Buckets.tp_resources;
10
+
11
+ export type Collection = keyof typeof CONSTANTS.Persistence.Collections;
12
+
13
+ export type SchemaLike = Record<string, unknown>;
14
+
15
+ export type ResourcesByResourceTypeHandle = Record<ResourceTypeIdentityJson, ResourceJson[]>;
16
+
17
+ export type IdentityName = keyof typeof MAPPINGS.IdentityNameToIdentityPrefix;
18
+
19
+ export type IdentityStringByIdentityName<K extends IdentityName> = `${(typeof MAPPINGS.IdentityNameToIdentityPrefix)[K]}${string}`;
20
+
25
21
  export type StepIdentityStringByStepKind<K extends StepKindJson> = `${(typeof MAPPINGS.StepKindToStepIdentityPrefix)[K]}${string}`;
@@ -0,0 +1,93 @@
1
+ import type {
2
+ RawStrategyJson,
3
+ JobIdentityJson,
4
+ JobJson,
5
+ JobStepIdentityJson,
6
+ JobStepJson,
7
+ StepJson,
8
+ ResourceRoleIdentityJson,
9
+ ResourceRoleJson,
10
+ ErrorJson
11
+ } from '@toolproof-core/schema';
12
+ import { CONSTANTS } from '../artifacts/artifacts.js';
13
+
14
+
15
+ export function extractJobHandlesFromRawStrategy(rawStrategy: RawStrategyJson): JobIdentityJson[] {
16
+ const jobSteps = extractJobStepsFromRawStrategy(rawStrategy);
17
+ const ids = new Set<JobIdentityJson>();
18
+ for (const jStep of jobSteps) {
19
+ ids.add(jStep.jobHandle);
20
+ }
21
+ return Array.from(ids);
22
+ }
23
+
24
+ export function extractJobStepsFromRawStrategy(rawStrategy: RawStrategyJson): JobStepJson[] {
25
+ const jobSteps: JobStepJson[] = [];
26
+ const seen = new Set<JobStepIdentityJson>();
27
+
28
+ const addJobStep = (jobStep: JobStepJson) => {
29
+ if (seen.has(jobStep.identity)) {
30
+ throw new Error(`Duplicate job step identity encountered: ${jobStep.identity}`);
31
+ }
32
+ seen.add(jobStep.identity);
33
+ jobSteps.push(jobStep);
34
+ };
35
+
36
+ const visitStep = (step: StepJson) => {
37
+ switch (step.stepKind) {
38
+ case CONSTANTS.Enums.StepKind.job: {
39
+ addJobStep(step);
40
+ return;
41
+ }
42
+ case CONSTANTS.Enums.StepKind.branch: {
43
+ for (const conditional of step.cases) {
44
+ addJobStep(conditional.when);
45
+ addJobStep(conditional.what);
46
+ }
47
+ return;
48
+ }
49
+ case CONSTANTS.Enums.StepKind.while:
50
+ case CONSTANTS.Enums.StepKind.for: {
51
+ addJobStep(step.case.when);
52
+ addJobStep(step.case.what);
53
+ return;
54
+ }
55
+ default: {
56
+ const _exhaustive: never = step;
57
+ return _exhaustive;
58
+ }
59
+ }
60
+ };
61
+
62
+ for (const step of rawStrategy.steps) {
63
+ visitStep(step);
64
+ }
65
+
66
+ return jobSteps;
67
+ }
68
+
69
+
70
+ export function extractResourceRolesFromRawStrategy(rawStrategy: RawStrategyJson, jobMap: Map<JobIdentityJson, JobJson>): ResourceRoleJson[] {
71
+ const map = new Map<ResourceRoleIdentityJson, ResourceRoleJson>();
72
+ const jobHandles = extractJobHandlesFromRawStrategy(rawStrategy);
73
+
74
+ for (const jobHandle of jobHandles) {
75
+ const job = jobMap.get(jobHandle);
76
+ if (!job) continue;
77
+
78
+ for (const [rid, role] of Object.entries(job.roles.inputDict)) {
79
+ map.set(rid as ResourceRoleIdentityJson, {
80
+ identity: rid as ResourceRoleIdentityJson,
81
+ ...role,
82
+ });
83
+ }
84
+ for (const [rid, role] of Object.entries(job.roles.outputDict)) {
85
+ map.set(rid as ResourceRoleIdentityJson, {
86
+ identity: rid as ResourceRoleIdentityJson,
87
+ ...role,
88
+ });
89
+ }
90
+ }
91
+
92
+ return Array.from(map.values());
93
+ }
@@ -0,0 +1,55 @@
1
+ import type {
2
+ ResourceIdentityJson,
3
+ ResourceTypeIdentityJson,
4
+ ResourceJson,
5
+ } from '@toolproof-core/schema';
6
+
7
+
8
+ export function extractResourceMapForResourceType<TResource extends ResourceJson = ResourceJson>(
9
+ resourcesByResourceTypeHandle: Partial<Record<ResourceTypeIdentityJson, TResource[]>>,
10
+ resourceTypeHandle: ResourceTypeIdentityJson
11
+ ): Map<ResourceIdentityJson, TResource> {
12
+ const resources = resourcesByResourceTypeHandle[resourceTypeHandle] ?? [];
13
+ const map = new Map<ResourceIdentityJson, TResource>();
14
+
15
+ for (const resource of resources) {
16
+ map.set(resource.identity, resource);
17
+ }
18
+
19
+ return map;
20
+ }
21
+
22
+ // ATTENTION: should only need standalone type as type parameter, then we could extract the nuclueus type and identity type
23
+ export function extractNucleusMapForResourceType<
24
+ TKey extends string = string,
25
+ TNucleus extends { identity: string | number | boolean } = { identity: string | number | boolean },
26
+ TResource extends ResourceJson = ResourceJson
27
+ >(
28
+ resourcesByResourceTypeHandle: Partial<Record<ResourceTypeIdentityJson, TResource[]>>,
29
+ resourceTypeHandle: ResourceTypeIdentityJson,
30
+ ): Map<TKey, TNucleus> {
31
+ const resourceMap = extractResourceMapForResourceType<TResource>(resourcesByResourceTypeHandle, resourceTypeHandle);
32
+ const out = new Map<TKey, TNucleus>();
33
+
34
+ for (const resource of resourceMap.values()) {
35
+ const data = (resource as any).nucleus as unknown;
36
+ if (data === null || data === undefined) {
37
+ continue;
38
+ }
39
+ if (typeof data !== 'object') {
40
+ continue;
41
+ }
42
+
43
+ const maybeIdentity = (data as any).identity as unknown;
44
+ if (maybeIdentity === null || maybeIdentity === undefined) {
45
+ continue;
46
+ }
47
+
48
+ const value = data as TNucleus;
49
+ const key = String(maybeIdentity) as TKey;
50
+
51
+ out.set(key, value);
52
+ }
53
+
54
+ return out;
55
+ }
@@ -1,27 +1,27 @@
1
- import type { ResourceIdentityJson, ResourceTypeIdentityJson, ResourceJson, ResourceOutputPotentialJson } from '@toolproof-core/schema';
2
-
3
-
4
- export function generatePath(resourceTypeHandle: ResourceTypeIdentityJson, identity: ResourceIdentityJson): string {
5
- return `${resourceTypeHandle}/${identity}.json`;
6
- }
7
-
8
- export function fromOutputPotentialToMaterialized(
9
- outputPotential: ResourceOutputPotentialJson,
10
- nucleus: unknown,
11
- timestamp?: string
12
- ): ResourceJson {
13
- const { identity, resourceTypeHandle, creationContext } = outputPotential;
14
-
15
- const path = generatePath(resourceTypeHandle, identity);
16
-
17
- return {
18
- identity,
19
- resourceTypeHandle,
20
- creationContext,
21
- resourceShellKind: 'materialized',
22
- version: 1,
23
- timestamp: timestamp ?? new Date().toISOString(), // Preserve original timestamp when copying
24
- path,
25
- nucleus,
26
- };
1
+ import type { ResourceIdentityJson, ResourceTypeIdentityJson, ResourceJson, ResourceOutputPotentialJson } from '@toolproof-core/schema';
2
+
3
+
4
+ export function generatePath(resourceTypeHandle: ResourceTypeIdentityJson, identity: ResourceIdentityJson): string {
5
+ return `${resourceTypeHandle}/${identity}.json`;
6
+ }
7
+
8
+ export function generateMaterializedFromOutputPotential(
9
+ outputPotential: ResourceOutputPotentialJson,
10
+ nucleus: unknown,
11
+ timestamp?: string
12
+ ): ResourceJson {
13
+ const { identity, resourceTypeHandle, creationContext } = outputPotential;
14
+
15
+ const path = generatePath(resourceTypeHandle, identity);
16
+
17
+ return {
18
+ identity,
19
+ resourceTypeHandle,
20
+ creationContext,
21
+ resourceShellKind: 'materialized',
22
+ version: 1,
23
+ timestamp: timestamp ?? new Date().toISOString(), // Preserve original timestamp when copying
24
+ path,
25
+ nucleus,
26
+ };
27
27
  }
@@ -0,0 +1,177 @@
1
+ import type {
2
+ BranchStepJson,
3
+ ForStepJson,
4
+ JobStepIdentityJson,
5
+ JobStepJson,
6
+ ResourceRoleIdentityJson,
7
+ StepJson,
8
+ StrategyStateJson,
9
+ WhileStepJson,
10
+ } from '@toolproof-core/schema';
11
+
12
+
13
+ export function getIndependentThreads(steps: StepJson[], strategyState: StrategyStateJson): StepJson[][] {
14
+ type OwnerIndex = number;
15
+
16
+ const getOwnerLabel = (ownerIndex: OwnerIndex) => {
17
+ const step = steps[ownerIndex];
18
+ return `steps[${ownerIndex}] stepKind=${(step as any)?.stepKind ?? 'unknown'}`;
19
+ };
20
+
21
+ // Map each jobStep.identity (including macro-nested job steps) to owning top-level step index.
22
+ const jobStepIdToOwner = new Map<JobStepIdentityJson, OwnerIndex>();
23
+ const ownerToJobStepIds = new Map<OwnerIndex, JobStepIdentityJson[]>();
24
+ const jobStepById = new Map<JobStepIdentityJson, JobStepJson>();
25
+
26
+ const addJobStep = (jobStep: JobStepJson | undefined, ownerIndex: OwnerIndex) => {
27
+ if (!jobStep?.identity) return;
28
+
29
+ const existingOwner = jobStepIdToOwner.get(jobStep.identity);
30
+ if (existingOwner !== undefined) {
31
+ throw new Error(
32
+ `Duplicate jobStep.identity '${jobStep.identity}' found in ${getOwnerLabel(ownerIndex)} and ${getOwnerLabel(existingOwner)}`
33
+ );
34
+ }
35
+
36
+ jobStepIdToOwner.set(jobStep.identity, ownerIndex);
37
+ jobStepById.set(jobStep.identity, jobStep);
38
+ const bucket = ownerToJobStepIds.get(ownerIndex) ?? [];
39
+ bucket.push(jobStep.identity);
40
+ ownerToJobStepIds.set(ownerIndex, bucket);
41
+ };
42
+
43
+ steps.forEach((step, ownerIndex) => {
44
+ if (step.stepKind === 'job') {
45
+ addJobStep(step as JobStepJson, ownerIndex);
46
+ return;
47
+ }
48
+
49
+ if (step.stepKind === 'for') {
50
+ const loop = step as ForStepJson;
51
+ addJobStep(loop.case?.what, ownerIndex);
52
+ addJobStep(loop.case?.when, ownerIndex);
53
+ return;
54
+ }
55
+
56
+ if (step.stepKind === 'while') {
57
+ const loop = step as WhileStepJson;
58
+ addJobStep(loop.case?.what, ownerIndex);
59
+ addJobStep(loop.case?.when, ownerIndex);
60
+ return;
61
+ }
62
+
63
+ if (step.stepKind === 'branch') {
64
+ const branch = step as BranchStepJson;
65
+ for (const caseItem of branch.cases ?? []) {
66
+ addJobStep(caseItem?.what, ownerIndex);
67
+ addJobStep(caseItem?.when, ownerIndex);
68
+ }
69
+ return;
70
+ }
71
+ });
72
+
73
+ // Undirected adjacency used for connected components (thread groups).
74
+ const ownerAdj = new Map<OwnerIndex, Set<OwnerIndex>>();
75
+ // Directed dependencies used for strict ordering validation: owner -> producers it depends on.
76
+ const ownerDeps = new Map<OwnerIndex, Set<OwnerIndex>>();
77
+
78
+ const ensureOwner = (ownerIndex: OwnerIndex) => {
79
+ if (!ownerAdj.has(ownerIndex)) ownerAdj.set(ownerIndex, new Set());
80
+ if (!ownerDeps.has(ownerIndex)) ownerDeps.set(ownerIndex, new Set());
81
+ };
82
+
83
+ for (let i = 0; i < steps.length; i++) {
84
+ ensureOwner(i);
85
+ }
86
+
87
+ // Discover dependencies from strategyState.
88
+ for (const [jobStepId, ownerIndex] of jobStepIdToOwner) {
89
+ ensureOwner(ownerIndex);
90
+
91
+ const jobStep = jobStepById.get(jobStepId);
92
+ const inputBindings = jobStep?.roleBindings?.inputBindings ?? [];
93
+ const bucket = strategyState?.[jobStepId] ?? ({} as any);
94
+
95
+ for (const inputRoleHandle of inputBindings) {
96
+ const entry = bucket?.[inputRoleHandle as ResourceRoleIdentityJson] as any;
97
+ if (!entry || entry.resourceShellKind !== 'inputPotential') continue;
98
+
99
+ const creatorJobStepHandle = entry?.creationContext?.jobStepHandle as JobStepIdentityJson | undefined;
100
+ if (!creatorJobStepHandle) {
101
+ throw new Error(
102
+ `Unresolvable inputPotential in jobStep '${jobStepId}' (${getOwnerLabel(ownerIndex)}): missing creationContext.jobStepHandle for role '${inputRoleHandle}'`
103
+ );
104
+ }
105
+
106
+ const producerOwner = jobStepIdToOwner.get(creatorJobStepHandle);
107
+ if (producerOwner === undefined) {
108
+ throw new Error(
109
+ `Unresolvable inputPotential in jobStep '${jobStepId}' (${getOwnerLabel(ownerIndex)}): creator jobStepHandle '${creatorJobStepHandle}' not found in strategy steps`
110
+ );
111
+ }
112
+
113
+ ensureOwner(producerOwner);
114
+ ownerAdj.get(ownerIndex)!.add(producerOwner);
115
+ ownerAdj.get(producerOwner)!.add(ownerIndex);
116
+ ownerDeps.get(ownerIndex)!.add(producerOwner);
117
+ }
118
+ }
119
+
120
+ // Connected components over owners.
121
+ const visited = new Set<OwnerIndex>();
122
+ const components: OwnerIndex[][] = [];
123
+
124
+ for (let ownerIndex = 0; ownerIndex < steps.length; ownerIndex++) {
125
+ if (visited.has(ownerIndex)) continue;
126
+ const component: OwnerIndex[] = [];
127
+ const queue: OwnerIndex[] = [ownerIndex];
128
+ visited.add(ownerIndex);
129
+ while (queue.length > 0) {
130
+ const node = queue.shift()!;
131
+ component.push(node);
132
+ for (const neighbor of ownerAdj.get(node) ?? []) {
133
+ if (!visited.has(neighbor)) {
134
+ visited.add(neighbor);
135
+ queue.push(neighbor);
136
+ }
137
+ }
138
+ }
139
+ components.push(component);
140
+ }
141
+
142
+ // Strict validation: within a thread, the linear order must not contain forward refs.
143
+ for (const comp of components) {
144
+ const compSet = new Set(comp);
145
+ const ownersInOrder = steps.map((_, i) => i).filter((i) => compSet.has(i));
146
+
147
+ const position = new Map<OwnerIndex, number>();
148
+ ownersInOrder.forEach((owner, idx) => position.set(owner, idx));
149
+
150
+ for (const consumerOwner of ownersInOrder) {
151
+ const deps = ownerDeps.get(consumerOwner);
152
+ if (!deps || deps.size === 0) continue;
153
+
154
+ const consumerPos = position.get(consumerOwner)!;
155
+ for (const producerOwner of deps) {
156
+ const producerPos = position.get(producerOwner);
157
+ if (producerPos === undefined) {
158
+ throw new Error(
159
+ `Internal error: dependency producer ${getOwnerLabel(producerOwner)} missing from computed thread for ${getOwnerLabel(consumerOwner)}`
160
+ );
161
+ }
162
+ if (producerPos >= consumerPos) {
163
+ throw new Error(
164
+ `Invalid step order in thread: ${getOwnerLabel(consumerOwner)} depends on ${getOwnerLabel(producerOwner)}, but producer is not scheduled before consumer`
165
+ );
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ return components
172
+ .map((compOwners) => {
173
+ const ownerSet = new Set(compOwners);
174
+ return steps.filter((_, i) => ownerSet.has(i));
175
+ })
176
+ .filter((group) => group.length > 0);
177
+ }
@@ -0,0 +1,66 @@
1
+ import type {
2
+ CreationContextJson,
3
+ ResourceJson,
4
+ ResourceMissingJson,
5
+ ResourceInputPotentialJson,
6
+ ResourceOutputPotentialJson,
7
+ StrategyStateJson,
8
+ } from '@toolproof-core/schema';
9
+
10
+ export type ResolveResult =
11
+ | { status: 'materialized'; entry: ResourceJson; path: CreationContextJson[] }
12
+ | { status: 'missing'; entry: ResourceMissingJson; path: CreationContextJson[] }
13
+ | { status: 'outputPotential'; entry: ResourceOutputPotentialJson; path: CreationContextJson[] }
14
+ | { status: 'unresolved'; reason: 'not-found' | 'cycle' | 'depth-exceeded'; path: CreationContextJson[] };
15
+
16
+ export function resolveResourceChain(
17
+ strategyState: StrategyStateJson,
18
+ start: CreationContextJson,
19
+ opts?: { maxDepth?: number }
20
+ ): ResolveResult {
21
+ const maxDepth = opts?.maxDepth ?? 50;
22
+ const visited = new Set<string>();
23
+ const path: CreationContextJson[] = [];
24
+ let current: CreationContextJson = start;
25
+
26
+ for (let depth = 0; depth <= maxDepth; depth++) {
27
+ path.push(current);
28
+ const visitKey = `${current.jobStepHandle}::${current.resourceRoleHandle}`;
29
+ if (visited.has(visitKey)) {
30
+ return { status: 'unresolved', reason: 'cycle', path };
31
+ }
32
+ visited.add(visitKey);
33
+
34
+ const bucket = strategyState[current.jobStepHandle];
35
+ if (!bucket) return { status: 'unresolved', reason: 'not-found', path };
36
+ const entry = bucket[current.resourceRoleHandle] as (
37
+ | ResourceJson
38
+ | ResourceMissingJson
39
+ | ResourceInputPotentialJson
40
+ | ResourceOutputPotentialJson
41
+ | undefined
42
+ );
43
+ if (!entry) return { status: 'unresolved', reason: 'not-found', path };
44
+
45
+ if (entry.resourceShellKind === 'materialized') {
46
+ return { status: 'materialized', entry: entry as ResourceJson, path };
47
+ }
48
+ if (entry.resourceShellKind === 'missing') {
49
+ return { status: 'missing', entry: entry as ResourceMissingJson, path };
50
+ }
51
+ if (entry.resourceShellKind === 'outputPotential') {
52
+ return { status: 'outputPotential', entry: entry as ResourceOutputPotentialJson, path };
53
+ }
54
+
55
+ // inputPotential: follow ref backwards
56
+ if (entry.resourceShellKind === 'inputPotential') {
57
+ current = (entry as ResourceInputPotentialJson).creationContext;
58
+ continue;
59
+ }
60
+
61
+ // Unknown case
62
+ return { status: 'unresolved', reason: 'not-found', path };
63
+ }
64
+
65
+ return { status: 'unresolved', reason: 'depth-exceeded', path };
66
+ }