ai-workflow-logic 0.1.9 → 0.2.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.
@@ -121,8 +121,8 @@ function reserveName(ai: AIWorkflowProfileDurableState, receipt: AIAgentInvocati
121
121
  }
122
122
 
123
123
  function resolveSelector(ai: AIWorkflowProfileDurableState, selector: AIAgentSelector, config: AIAgentEffectConfig): AIAgentInstanceReference {
124
- const instanceId = 'byInstanceId' in selector ? selector.byInstanceId : ai.instanceIdByName[selector.byInstanceName];
125
- if (instanceId === undefined) fail('AI_AGENT_INSTANCE_NAME_NOT_FOUND', 'selector.byInstanceName', 'Named Agent instance does not exist.');
124
+ const instanceId = 'byId' in selector ? selector.byId : ai.instanceIdByName[selector.byName];
125
+ if (instanceId === undefined) fail('AI_AGENT_INSTANCE_NAME_NOT_FOUND', 'selector.byName', 'Named Agent instance does not exist.');
126
126
  const instance = ai.instancesById[instanceId];
127
127
  if (!instance) fail('AI_AGENT_INSTANCE_ID_NOT_FOUND', 'selector', 'Agent instance does not exist.');
128
128
  if (instance.instanceName !== undefined && ai.instanceIdByName[instance.instanceName] !== instance.instanceId) fail('AI_AGENT_INSTANCE_INDEX_CONFLICT', 'checkpoint.profile.ai', 'Agent instance indexes are not reciprocal.');
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  AIAgentDefinitionProjection,
3
+ AIAgentMessagePrefixItemProjection,
3
4
  AIAgentMessageProjection,
4
5
  AIAgentMessageRole,
5
6
  AIWorkflowEffectPolicyProjection,
@@ -27,6 +28,8 @@ import { AIWorkflowResourceProjectionError } from './errors.js';
27
28
 
28
29
  const AGENT_SUBDOMAINS = Object.freeze([
29
30
  'Messages',
31
+ 'MessagePrefix',
32
+ 'ContextPipeline',
30
33
  'InputSchemaRef',
31
34
  'OutputSchemaRef',
32
35
  'ToolRefs',
@@ -144,12 +147,44 @@ function projectAgentDefinition(
144
147
  diagnostics: AIWorkflowResourceProjectionDiagnostic[],
145
148
  ): AIAgentDefinitionProjection | undefined {
146
149
  validateAllowedSubdomains(resource, AGENT_SUBDOMAINS, diagnostics);
147
- const messagesNode = requiredSubdomain(resource, 'Messages', diagnostics);
150
+ const messagesNode = exactOptionalSubdomain(resource, 'Messages', diagnostics);
151
+ const messagePrefixNode = exactOptionalSubdomain(resource, 'MessagePrefix', diagnostics);
152
+ if (messagesNode && messagePrefixNode) {
153
+ diagnostics.push(diagnostic(
154
+ 'AI_AGENT_MESSAGE_PREFIX_CONFLICT',
155
+ resource.documentUri,
156
+ 'AIAgentDefinition must declare exactly one of legacy Messages or MessagePrefix.',
157
+ ));
158
+ } else if (!messagesNode && !messagePrefixNode) {
159
+ diagnostics.push(diagnostic(
160
+ 'AI_AGENT_MESSAGE_PREFIX_MISSING',
161
+ resource.documentUri,
162
+ 'AIAgentDefinition must declare legacy Messages or MessagePrefix.',
163
+ ));
164
+ }
148
165
  const toolRefsNode = requiredSubdomain(resource, 'ToolRefs', diagnostics);
149
166
  const materialPortRefsNode = requiredSubdomain(resource, 'MaterialPortRefs', diagnostics);
167
+ const projectedMessagePrefix = messagePrefixNode
168
+ ? projectMessagePrefix(resource, messagePrefixNode, resources, edges, diagnostics)
169
+ : undefined;
150
170
  const messages = messagesNode
151
171
  ? projectMessages(resource, messagesNode, resources, edges, diagnostics)
152
- : [];
172
+ : projectedMessagePrefix
173
+ ? projectedMessagePrefix
174
+ .filter((item): item is Extract<AIAgentMessagePrefixItemProjection, { type: 'message' }> => item.type === 'message')
175
+ .map(({ type: _type, ...message }) => Object.freeze(message))
176
+ : [];
177
+ const messagePrefix = projectedMessagePrefix
178
+ ?? messages.map((message) => Object.freeze({ ...message, type: 'message' as const }));
179
+ const contextPipeline = projectOptionalTypedSubdomain(
180
+ resource,
181
+ 'ContextPipeline',
182
+ resources,
183
+ 'agent-context-pipeline',
184
+ edges,
185
+ diagnostics,
186
+ 'AgentContextPipeline',
187
+ );
153
188
  const inputSchema = projectOptionalSchemaSubdomain(resource, 'InputSchemaRef', resources, 'agent-input-schema', edges, diagnostics);
154
189
  const outputSchema = projectOptionalSchemaSubdomain(resource, 'OutputSchemaRef', resources, 'agent-output-schema', edges, diagnostics);
155
190
  const tools = toolRefsNode
@@ -160,12 +195,14 @@ function projectAgentDefinition(
160
195
  ? projectTypedRefList(resource, materialPortRefsNode, 'MaterialPortRef', resources, 'agent-material-port', edges, diagnostics, 'MaterialPort')
161
196
  : [];
162
197
 
163
- if (!messagesNode || !toolRefsNode || !materialPortRefsNode) return undefined;
198
+ if ((!messagesNode && !messagePrefixNode) || (messagesNode && messagePrefixNode) || !toolRefsNode || !materialPortRefsNode) return undefined;
164
199
  return Object.freeze({
165
200
  kind: 'AIAgentDefinition',
166
201
  fqn: resource.fqn ?? resource.resourceId,
167
202
  resource,
203
+ messagePrefix: Object.freeze(messagePrefix),
168
204
  messages: Object.freeze(messages),
205
+ ...(contextPipeline === undefined ? {} : { contextPipeline }),
169
206
  ...(inputSchema === undefined ? {} : { inputSchema }),
170
207
  ...(outputSchema === undefined ? {} : { outputSchema }),
171
208
  tools: Object.freeze(tools),
@@ -174,6 +211,67 @@ function projectAgentDefinition(
174
211
  });
175
212
  }
176
213
 
214
+ function exactOptionalSubdomain(
215
+ resource: ResourceRecord,
216
+ name: string,
217
+ diagnostics: AIWorkflowResourceProjectionDiagnostic[],
218
+ ): ResourceNode | undefined {
219
+ const value = resource.node.subdomains[name];
220
+ if (!value) return undefined;
221
+ if (value.tag !== name) {
222
+ diagnostics.push(diagnostic(
223
+ 'AI_RESOURCE_SUBDOMAIN_TAG_MISMATCH',
224
+ `${resource.documentUri}#${name}`,
225
+ `Subdomain ${name} must contain a ${name} node, got '${value.tag}'.`,
226
+ ));
227
+ return undefined;
228
+ }
229
+ return value;
230
+ }
231
+
232
+ function projectMessagePrefix(
233
+ resource: ResourceRecord,
234
+ prefixNode: ResourceNode,
235
+ resources: ReadonlyMap<string, ResourceRecord>,
236
+ edges: ResourceDependencyEdge[],
237
+ diagnostics: AIWorkflowResourceProjectionDiagnostic[],
238
+ ): AIAgentMessagePrefixItemProjection[] {
239
+ const output: AIAgentMessagePrefixItemProjection[] = [];
240
+ const ids = new Set<string>();
241
+ prefixNode.body.forEach((value, index) => {
242
+ const location = `${resource.documentUri}#MessagePrefix:${index}`;
243
+ if (!isResourceNode(value) || (value.tag !== 'Message' && value.tag !== 'MessageSource')) {
244
+ diagnostics.push(diagnostic(
245
+ 'AI_AGENT_MESSAGE_PREFIX_NODE_INVALID',
246
+ location,
247
+ 'MessagePrefix may contain only Message or MessageSource nodes.',
248
+ ));
249
+ return;
250
+ }
251
+ const id = canonicalString(value.resourceId);
252
+ if (!id) {
253
+ diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ID_MISSING', location, `${value.tag} must declare an explicit #id.`));
254
+ return;
255
+ }
256
+ if (ids.has(id)) {
257
+ diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ID_DUPLICATE', location, `MessagePrefix id '${id}' is declared more than once.`));
258
+ return;
259
+ }
260
+ ids.add(id);
261
+ if (value.tag === 'Message') {
262
+ const message = projectMessageNode(resource, value, resources, edges, diagnostics, location);
263
+ if (message) output.push(Object.freeze({ ...message, type: 'message' }));
264
+ return;
265
+ }
266
+ validateAllowedNodeSubdomains(value, [], location, diagnostics);
267
+ const source = projectTypedValues(value.properties.kind, value.properties.ref, resources, location, diagnostics, 'AgentMessageSource');
268
+ if (!source) return;
269
+ edges.push(edge(resource.resourceId, source.resource.resourceId, 'agent-message-source'));
270
+ output.push(Object.freeze({ type: 'message-source', id, source }));
271
+ });
272
+ return output;
273
+ }
274
+
177
275
  function projectMessages(
178
276
  resource: ResourceRecord,
179
277
  messagesNode: ResourceNode,
@@ -189,39 +287,45 @@ function projectMessages(
189
287
  diagnostics.push(diagnostic('AI_AGENT_MESSAGE_NODE_INVALID', location, 'Messages may contain only Message nodes.'));
190
288
  return;
191
289
  }
192
- validateAllowedNodeSubdomains(value, ['SchemaRef'], location, diagnostics);
193
290
  const id = canonicalString(value.resourceId);
194
- const role = value.properties.role;
195
- if (!id) {
196
- diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ID_MISSING', location, 'Message must declare an explicit #id.'));
197
- } else if (ids.has(id)) {
291
+ if (id && ids.has(id)) {
198
292
  diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ID_DUPLICATE', location, `Message id '${id}' is declared more than once.`));
199
- } else {
200
- ids.add(id);
293
+ return;
201
294
  }
202
- if (!isMessageRole(role)) {
203
- diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ROLE_INVALID', location, 'Message role must be system, developer, user, or assistant.'));
295
+ if (id) {
296
+ ids.add(id);
204
297
  }
205
- const prompt = projectTypedValues(
206
- value.properties.promptKind,
207
- value.properties.promptRef,
208
- resources,
209
- location,
210
- diagnostics,
211
- );
212
- if (prompt) edges.push(edge(resource.resourceId, prompt.resource.resourceId, 'agent-message-prompt'));
213
- const schemaNode = value.subdomains.SchemaRef;
214
- const schema = schemaNode?.tag === 'SchemaRef'
215
- ? projectMessageSchemaRefNode(schemaNode, resources, `${location}#SchemaRef`, diagnostics)
216
- : undefined;
217
- if (schemaNode && schemaNode.tag !== 'SchemaRef') diagnostics.push(diagnostic('AI_RESOURCE_SUBDOMAIN_TAG_MISMATCH', `${location}#SchemaRef`, `Subdomain SchemaRef must contain a SchemaRef node, got '${schemaNode.tag}'.`));
218
- if (schema) edges.push(edge(resource.resourceId, schema.resource.resourceId, 'agent-message-schema'));
219
- if (!id || !isMessageRole(role) || !prompt) return;
220
- output.push(Object.freeze({ id, role, prompt, ...(schema === undefined ? {} : { schema }) }));
298
+ const message = projectMessageNode(resource, value, resources, edges, diagnostics, location);
299
+ if (message) output.push(message);
221
300
  });
222
301
  return output;
223
302
  }
224
303
 
304
+ function projectMessageNode(
305
+ resource: ResourceRecord,
306
+ value: ResourceNode,
307
+ resources: ReadonlyMap<string, ResourceRecord>,
308
+ edges: ResourceDependencyEdge[],
309
+ diagnostics: AIWorkflowResourceProjectionDiagnostic[],
310
+ location: string,
311
+ ): AIAgentMessageProjection | undefined {
312
+ validateAllowedNodeSubdomains(value, ['SchemaRef'], location, diagnostics);
313
+ const id = canonicalString(value.resourceId);
314
+ const role = value.properties.role;
315
+ if (!id) diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ID_MISSING', location, 'Message must declare an explicit #id.'));
316
+ if (!isMessageRole(role)) diagnostics.push(diagnostic('AI_AGENT_MESSAGE_ROLE_INVALID', location, 'Message role must be system, developer, user, or assistant.'));
317
+ const prompt = projectTypedValues(value.properties.promptKind, value.properties.promptRef, resources, location, diagnostics);
318
+ if (prompt) edges.push(edge(resource.resourceId, prompt.resource.resourceId, 'agent-message-prompt'));
319
+ const schemaNode = value.subdomains.SchemaRef;
320
+ const schema = schemaNode?.tag === 'SchemaRef'
321
+ ? projectMessageSchemaRefNode(schemaNode, resources, `${location}#SchemaRef`, diagnostics)
322
+ : undefined;
323
+ if (schemaNode && schemaNode.tag !== 'SchemaRef') diagnostics.push(diagnostic('AI_RESOURCE_SUBDOMAIN_TAG_MISMATCH', `${location}#SchemaRef`, `Subdomain SchemaRef must contain a SchemaRef node, got '${schemaNode.tag}'.`));
324
+ if (schema) edges.push(edge(resource.resourceId, schema.resource.resourceId, 'agent-message-schema'));
325
+ if (!id || !isMessageRole(role) || !prompt) return undefined;
326
+ return Object.freeze({ id, role, prompt, ...(schema === undefined ? {} : { schema }) });
327
+ }
328
+
225
329
  function projectTypedRefList(
226
330
  owner: ResourceRecord,
227
331
  list: ResourceNode,
@@ -265,6 +369,7 @@ function projectOptionalTypedSubdomain(
265
369
  relation: AIWorkflowAgentResourceRelation,
266
370
  edges: ResourceDependencyEdge[],
267
371
  diagnostics: AIWorkflowResourceProjectionDiagnostic[],
372
+ expectedKind?: string,
268
373
  ): AIWorkflowTypedResourceRef | undefined {
269
374
  const node = owner.node.subdomains[name];
270
375
  if (!node) return undefined;
@@ -272,7 +377,7 @@ function projectOptionalTypedSubdomain(
272
377
  diagnostics.push(diagnostic('AI_RESOURCE_SUBDOMAIN_TAG_MISMATCH', `${owner.documentUri}#${name}`, `Subdomain ${name} must contain a ${name} node, got '${node.tag}'.`));
273
378
  return undefined;
274
379
  }
275
- const typedRef = projectTypedRefNode(node, resources, `${owner.documentUri}#${name}`, diagnostics);
380
+ const typedRef = projectTypedRefNode(node, resources, `${owner.documentUri}#${name}`, diagnostics, expectedKind);
276
381
  if (typedRef) edges.push(edge(owner.resourceId, typedRef.resource.resourceId, relation));
277
382
  return typedRef;
278
383
  }
@@ -115,13 +115,13 @@ function optionalStringArray(value: unknown, location: string): readonly string[
115
115
  }
116
116
 
117
117
  export function normalizeAIAgentSelector(value: unknown, location = 'selector'): AIAgentSelector {
118
- const input = record(value, [], ['byInstanceId', 'byInstanceName'], location);
119
- const hasId = Object.prototype.hasOwnProperty.call(input, 'byInstanceId');
120
- const hasName = Object.prototype.hasOwnProperty.call(input, 'byInstanceName');
118
+ const input = record(value, [], ['byId', 'byName'], location);
119
+ const hasId = Object.prototype.hasOwnProperty.call(input, 'byId');
120
+ const hasName = Object.prototype.hasOwnProperty.call(input, 'byName');
121
121
  if (hasId === hasName) fail('AI_AGENT_SELECTOR_EXACTLY_ONE', location, 'Selector must contain exactly one supported key.');
122
122
  return Object.freeze(hasId
123
- ? { byInstanceId: identity(input.byInstanceId, `${location}.byInstanceId`) }
124
- : { byInstanceName: identity(input.byInstanceName, `${location}.byInstanceName`) });
123
+ ? { byId: identity(input.byId, `${location}.byId`) }
124
+ : { byName: identity(input.byName, `${location}.byName`) });
125
125
  }
126
126
 
127
127
  export function normalizeAIAgentEffectConfig(value: unknown, location = 'config'): AIAgentEffectConfig {
@@ -399,7 +399,9 @@ function validateAIDataProfileTransition(
399
399
  throw new FlowRunCheckpointTransitionError(`AI Data patch operation ${operation.op} is unsupported.`);
400
400
  }
401
401
  for (const [field, expected] of Object.entries(operation.changes)) {
402
- if (!sameClosedValue(candidate[field as keyof typeof candidate], expected)) {
402
+ const actual = (candidate as Readonly<Record<string, unknown>>)[field];
403
+ if (field === 'subflow' && expected === null && actual === undefined) continue;
404
+ if (!sameClosedValue(actual, expected)) {
403
405
  throw new FlowRunCheckpointTransitionError(`AI Data update-node ${nodeId} does not project declared ${field}.`);
404
406
  }
405
407
  }
package/src/filesystem.ts CHANGED
@@ -4,12 +4,32 @@ import {
4
4
  } from 'halfcode-compiler.xnl/resource-core';
5
5
  import type { LoadedAIWorkflowResourcePackage } from 'ai-workflow-contract';
6
6
  import { projectAIWorkflowAppBundles } from './index.js';
7
+ import {
8
+ resolveAIWorkflowResourceTree,
9
+ type AIWorkflowResourceResolutionContext,
10
+ } from './resource-resolution.js';
11
+
12
+ export {
13
+ AI_WORKFLOW_RESOURCE_SCHEMA_VALIDATOR,
14
+ DEPA_AI_RESOURCE_READER_PROFILE_ID,
15
+ createAIWorkflowResourceResolutionContext,
16
+ createResolvedAIWorkflowContentIdentityIndex,
17
+ resolveAIWorkflowResourceTree,
18
+ } from './resource-resolution.js';
19
+ export type { AIWorkflowResourceResolutionContext } from './resource-resolution.js';
20
+
21
+ export interface LoadAIWorkflowResourcePackageOptions extends LoadResourceTreeOptions {
22
+ readonly resolutionContext?: AIWorkflowResourceResolutionContext;
23
+ }
7
24
 
8
25
  export async function loadAIWorkflowResourcePackage(
9
- options: LoadResourceTreeOptions,
26
+ options: LoadAIWorkflowResourcePackageOptions,
10
27
  ): Promise<LoadedAIWorkflowResourcePackage> {
11
- const tree = await loadResourceTree(options);
28
+ const { resolutionContext, ...loadOptions } = options;
29
+ const authoredTree = await loadResourceTree(loadOptions);
30
+ const tree = resolveAIWorkflowResourceTree(authoredTree, resolutionContext);
12
31
  return Object.freeze({
32
+ authoredTree,
13
33
  tree,
14
34
  appBundles: projectAIWorkflowAppBundles(tree.registry, {
15
35
  packageResourceId: tree.manifest.resourceId,
package/src/index.ts CHANGED
@@ -36,6 +36,21 @@ import { AIWorkflowFoundationError, AIWorkflowResourceProjectionError } from './
36
36
 
37
37
  export { AIWorkflowFoundationError, AIWorkflowResourceProjectionError } from './errors.js';
38
38
  export { projectAIWorkflowAgentResources } from './agent-resources.js';
39
+ export {
40
+ AIAgentDefinitionSelectionError,
41
+ admitAIAgentDefinitionSelection,
42
+ freezeSelectedAIAgentTaskBinding,
43
+ normalizeAIAgentTaskRequirement,
44
+ projectAIAgentDefinitionCandidates,
45
+ reconcileAuthoredAIAgentDefinition,
46
+ } from './agent-definition-selection.js';
47
+ export type {
48
+ AIAgentDefinitionSelectionErrorCode,
49
+ AdmitAIAgentDefinitionSelectionInput,
50
+ FreezeSelectedAIAgentTaskBindingInput,
51
+ ProjectAIAgentDefinitionCandidatesInput,
52
+ ReconcileAuthoredAIAgentDefinitionInput,
53
+ } from './agent-definition-selection.js';
39
54
  export {
40
55
  FlowRunContractValidationError,
41
56
  normalizeAIWorkflowProfileDurableState,
@@ -0,0 +1,121 @@
1
+ import Ajv, { type ValidateFunction } from 'ajv';
2
+ import {
3
+ DEPA_AI_RESOURCE_KIND_OWNERS,
4
+ DEPA_AI_RESOURCE_KIND_READER_REGISTRATIONS,
5
+ DEPA_AI_RESOURCE_KIND_SPEC_REVISIONS,
6
+ } from 'ai-workflow-contract';
7
+ import type {
8
+ AuthoredResourceTree,
9
+ JsonSchemaValidator,
10
+ KindSpecRevision,
11
+ PortableSpec,
12
+ ResolvedResourceTree,
13
+ ResourceContentIdentity,
14
+ } from 'halfcode-compiler.xnl/resource-core';
15
+ import { createResourceContentIdentity } from 'halfcode-compiler.xnl/resource-core';
16
+ import {
17
+ CORE_KIND_READER_REGISTRATIONS,
18
+ CORE_KIND_SPEC_REVISIONS,
19
+ CORE_KIND_SUBJECT_OWNERS,
20
+ KindContractRegistry,
21
+ KindReaderRegistry,
22
+ SpecResolutionRegistry,
23
+ createReaderProfile,
24
+ resolveResourceTree,
25
+ type ReaderProfile,
26
+ } from 'halfcode-compiler.xnl/kind-definition';
27
+
28
+ export const DEPA_AI_RESOURCE_READER_PROFILE_ID = 'depa.flows.ai-resources.v1' as const;
29
+
30
+ export interface AIWorkflowResourceResolutionContext {
31
+ readonly contracts: KindContractRegistry;
32
+ readonly readers: KindReaderRegistry;
33
+ readonly readerProfile: ReaderProfile;
34
+ readonly resolutions: SpecResolutionRegistry;
35
+ readonly validator: JsonSchemaValidator;
36
+ }
37
+
38
+ const ajv = new Ajv({ allErrors: true, strict: false, validateFormats: false });
39
+ const compiledSchemas = new Map<string, ValidateFunction>();
40
+
41
+ export const AI_WORKFLOW_RESOURCE_SCHEMA_VALIDATOR: JsonSchemaValidator = Object.freeze({
42
+ validate(revision: KindSpecRevision, value: PortableSpec): readonly string[] {
43
+ let validate = compiledSchemas.get(revision.contractFingerprint);
44
+ if (!validate) {
45
+ validate = ajv.compile(revision.specSchema);
46
+ compiledSchemas.set(revision.contractFingerprint, validate);
47
+ }
48
+ return validate(value)
49
+ ? Object.freeze([])
50
+ : Object.freeze((validate.errors ?? []).map((error) =>
51
+ `${error.instancePath || '/'} ${error.message ?? 'invalid'}`));
52
+ },
53
+ });
54
+
55
+ export function createAIWorkflowResourceResolutionContext(
56
+ validator: JsonSchemaValidator = AI_WORKFLOW_RESOURCE_SCHEMA_VALIDATOR,
57
+ ): AIWorkflowResourceResolutionContext {
58
+ const contracts = new KindContractRegistry();
59
+ for (const owner of [...CORE_KIND_SUBJECT_OWNERS, ...DEPA_AI_RESOURCE_KIND_OWNERS]) {
60
+ contracts.registerOwner(owner);
61
+ }
62
+ for (const revision of [...CORE_KIND_SPEC_REVISIONS, ...DEPA_AI_RESOURCE_KIND_SPEC_REVISIONS]) {
63
+ contracts.registerRevision(revision);
64
+ }
65
+
66
+ const readerRegistrations = [
67
+ ...CORE_KIND_READER_REGISTRATIONS,
68
+ ...DEPA_AI_RESOURCE_KIND_READER_REGISTRATIONS,
69
+ ];
70
+ const readers = new KindReaderRegistry();
71
+ for (const reader of readerRegistrations) readers.register(reader);
72
+ const readerProfile = createReaderProfile(
73
+ DEPA_AI_RESOURCE_READER_PROFILE_ID,
74
+ readerRegistrations.map((reader) => ({
75
+ readerId: reader.readerId,
76
+ subjectFqn: reader.subjectFqn,
77
+ readerSpecVersion: reader.readerSpecVersion,
78
+ contractFingerprint: reader.contractFingerprint,
79
+ readerImplementationFingerprint: reader.readerImplementationFingerprint,
80
+ })),
81
+ );
82
+ return Object.freeze({
83
+ contracts,
84
+ readers,
85
+ readerProfile,
86
+ resolutions: new SpecResolutionRegistry(contracts),
87
+ validator,
88
+ });
89
+ }
90
+
91
+ export function resolveAIWorkflowResourceTree(
92
+ tree: AuthoredResourceTree,
93
+ context: AIWorkflowResourceResolutionContext = createAIWorkflowResourceResolutionContext(),
94
+ ): ResolvedResourceTree {
95
+ return resolveResourceTree({ tree, ...context });
96
+ }
97
+
98
+ /**
99
+ * Projects Halfcode's exact resolved content identity into the public dependency
100
+ * snapshot shape. The digest is still constructed by Halfcode; depa-flows only
101
+ * chooses the resolved receipt identity as the authority input.
102
+ */
103
+ export function createResolvedAIWorkflowContentIdentityIndex(
104
+ tree: ResolvedResourceTree,
105
+ ): ReadonlyMap<string, ResourceContentIdentity> {
106
+ const entries: Array<readonly [string, ResourceContentIdentity]> = [];
107
+ for (const records of tree.registry.byKind.values()) {
108
+ for (const record of records) {
109
+ entries.push([record.resourceId, createResourceContentIdentity({
110
+ resourceId: record.resourceId,
111
+ authorityDigest: record.effectiveContentDigest,
112
+ })]);
113
+ }
114
+ }
115
+ entries.sort(([left], [right]) => compareCodeUnits(left, right));
116
+ return new Map(entries);
117
+ }
118
+
119
+ function compareCodeUnits(left: string, right: string): number {
120
+ return left < right ? -1 : left > right ? 1 : 0;
121
+ }