ai-workflow-logic 0.1.11 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-workflow-logic",
3
- "version": "0.1.11",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "description": "AI workflow domain projections and pure runtime helpers with an explicit Halfcode filesystem adapter",
6
6
  "type": "module",
@@ -13,15 +13,16 @@
13
13
  "./run-freeze": "./src/run-freeze.ts"
14
14
  },
15
15
  "dependencies": {
16
- "ai-workflow-contract": "0.1.9",
16
+ "ai-workflow-contract": "0.2.0",
17
+ "ajv": "8.20.0",
17
18
  "flow-step-space-contract": "0.1.1",
18
19
  "flow-step-space-logic": "0.1.1",
19
- "halfcode-compiler.xnl": "0.2.9",
20
- "holarchy-core-contract": "0.1.1",
20
+ "halfcode-compiler.xnl": "0.3.0",
21
+ "holarchy-core-contract": "0.2.0",
21
22
  "task-manager-contract": "0.1.3",
22
23
  "task-manager-logic": "0.1.6",
23
24
  "work-ctrl-flow-contract": "0.1.3",
24
- "work-ctrl-flow-logic": "0.1.4"
25
+ "work-ctrl-flow-logic": "0.1.5"
25
26
  },
26
27
  "files": [
27
28
  "src"
@@ -22,6 +22,7 @@ import {
22
22
  type ResourceAuthoringTransactionPort,
23
23
  } from 'halfcode-compiler.xnl/authoring-runtime';
24
24
  import {
25
+ RESOURCE_ENVELOPE_VERSION,
25
26
  resolveEffectiveResourceContentIdentities,
26
27
  sha256Digest,
27
28
  type EffectiveResourceRegistry,
@@ -281,14 +282,16 @@ export async function reconcileAuthoredAIAgentDefinition(
281
282
  || input.receipt.catalogId !== proposal.catalogId
282
283
  || input.receipt.resourceId !== proposal.resourceId
283
284
  || input.receipt.kind !== proposal.kind
284
- || input.receipt.apiVersion !== proposal.apiVersion
285
+ || input.receipt.envelopeVersion !== proposal.envelopeVersion
286
+ || input.receipt.writerSpecVersion !== proposal.writerSpecVersion
285
287
  || input.receipt.sourceShape !== proposal.sourceShape
286
288
  || input.receipt.documentUri !== proposal.documentUri
287
289
  || input.receipt.authorityDigestBefore !== null
288
290
  || input.receipt.authorityDigestAfter !== sha256Digest(proposal.authorityText)
289
291
  || input.receipt.registryRevisionBefore !== proposal.expected.registryRevision
290
292
  || input.receipt.kind !== 'AIAgentDefinition'
291
- || resource.metadata.apiVersion !== input.receipt.apiVersion
293
+ || resource.metadata.envelopeVersion !== input.receipt.envelopeVersion
294
+ || resource.metadata.specVersion !== input.receipt.writerSpecVersion
292
295
  || resource.sourceShape !== input.receipt.sourceShape
293
296
  || resource.documentUri !== input.receipt.documentUri
294
297
  || !identity
@@ -504,7 +507,8 @@ function isValidAuthoringProposal(
504
507
  'catalogId',
505
508
  'resourceId',
506
509
  'kind',
507
- 'apiVersion',
510
+ 'envelopeVersion',
511
+ 'writerSpecVersion',
508
512
  'sourceShape',
509
513
  'documentUri',
510
514
  'authorityText',
@@ -521,7 +525,9 @@ function isValidAuthoringProposal(
521
525
  && definitionRef === resourceRef(nonEmptyString(proposal.resourceId, 'proposal.resourceId'))
522
526
  && !candidateSet.candidates.some(({ agentDefinitionRef }) => agentDefinitionRef === definitionRef)
523
527
  && nonEmptyString(proposal.catalogId, 'proposal.catalogId').length > 0
524
- && nonEmptyString(proposal.apiVersion, 'proposal.apiVersion').length > 0
528
+ && proposal.envelopeVersion === RESOURCE_ENVELOPE_VERSION
529
+ && Number.isSafeInteger(proposal.writerSpecVersion)
530
+ && Number(proposal.writerSpecVersion) > 0
525
531
  && /^vfs:\/\/@\/.+\.xnl$/u.test(nonEmptyString(proposal.documentUri, 'proposal.documentUri'))
526
532
  && stringValue(proposal.authorityText, 'proposal.authorityText').length > 0;
527
533
  } catch {
@@ -536,7 +542,8 @@ function cloneAuthoringProposal(value: ResourceAuthoringProposal): ResourceAutho
536
542
  catalogId: value.catalogId,
537
543
  resourceId: value.resourceId,
538
544
  kind: value.kind,
539
- apiVersion: value.apiVersion,
545
+ envelopeVersion: value.envelopeVersion,
546
+ writerSpecVersion: value.writerSpecVersion,
540
547
  sourceShape: value.sourceShape,
541
548
  documentUri: value.documentUri,
542
549
  authorityText: value.authorityText,
@@ -555,7 +562,8 @@ function validateReceiptShapeAndDigest(receiptValue: ResourceAuthoringReceipt):
555
562
  'catalogId',
556
563
  'resourceId',
557
564
  'kind',
558
- 'apiVersion',
565
+ 'envelopeVersion',
566
+ 'writerSpecVersion',
559
567
  'sourceShape',
560
568
  'documentUri',
561
569
  'authorityDigestBefore',
@@ -572,6 +580,9 @@ function validateReceiptShapeAndDigest(receiptValue: ResourceAuthoringReceipt):
572
580
  if (receipt.schemaVersion !== RESOURCE_AUTHORING_SCHEMA_VERSION
573
581
  || (receipt.operation !== 'create' && receipt.operation !== 'update')
574
582
  || receipt.sourceShape !== 'single-file'
583
+ || receipt.envelopeVersion !== RESOURCE_ENVELOPE_VERSION
584
+ || !Number.isSafeInteger(receipt.writerSpecVersion)
585
+ || Number(receipt.writerSpecVersion) <= 0
575
586
  || (receipt.authorityDigestBefore !== null && !isDigest(receipt.authorityDigestBefore))
576
587
  || !isDigest(receipt.planDigest)
577
588
  || !isDigest(receipt.authorityDigestAfter)
@@ -587,7 +598,7 @@ function validateReceiptShapeAndDigest(receiptValue: ResourceAuthoringReceipt):
587
598
  || (origin.logicalPath !== undefined && typeof origin.logicalPath !== 'string')) {
588
599
  throw new Error('invalid receipt fields');
589
600
  }
590
- for (const field of ['transactionId', 'catalogId', 'resourceId', 'kind', 'apiVersion', 'documentUri'] as const) {
601
+ for (const field of ['transactionId', 'catalogId', 'resourceId', 'kind', 'envelopeVersion', 'documentUri'] as const) {
591
602
  nonEmptyString(receipt[field], `receipt.${field}`);
592
603
  }
593
604
  for (const field of ['layerId', 'packageId', 'documentUri'] as const) {
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,
@@ -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
+ }
package/src/run-freeze.ts CHANGED
@@ -18,7 +18,6 @@ import {
18
18
  buildResourceDependencySnapshot,
19
19
  composeLayeredResourceRegistry,
20
20
  loadResourceTree,
21
- resolveEffectiveResourceContentIdentities,
22
21
  sha256Digest,
23
22
  type EffectiveResourceRegistry,
24
23
  type ResourceContentIdentity,
@@ -26,6 +25,10 @@ import {
26
25
  } from 'halfcode-compiler.xnl/resource-core';
27
26
  import { projectAIWorkflowAgentResources } from './agent-resources.js';
28
27
  import { sourceRegistryForAIWorkflowAgentResourceProjection } from './agent-resource-brand.js';
28
+ import {
29
+ createResolvedAIWorkflowContentIdentityIndex,
30
+ resolveAIWorkflowResourceTree,
31
+ } from './resource-resolution.js';
29
32
 
30
33
  export interface FreezeAIWorkflowRunResourcesInput {
31
34
  readonly registry: EffectiveResourceRegistry;
@@ -39,14 +42,15 @@ export async function freezeAIWorkflowRunResourcesFromDirectory(
39
42
  rootDir: string,
40
43
  task: AIWorkflowAgentTaskRef,
41
44
  ): Promise<AIWorkflowRunResourceFreezeReceipt> {
42
- const tree = await loadResourceTree({ rootDir });
45
+ const authoredTree = await loadResourceTree({ rootDir });
46
+ const tree = resolveAIWorkflowResourceTree(authoredTree);
43
47
  const layers = [{ id: 'workspace', tree }] as const;
44
48
  const registry = composeLayeredResourceRegistry({ layers });
45
49
  return freezeAIWorkflowRunResources({
46
50
  registry,
47
51
  projection: projectAIWorkflowAgentResources(registry),
48
52
  task,
49
- contentIdentities: resolveEffectiveResourceContentIdentities({ registry, layers }),
53
+ contentIdentities: createResolvedAIWorkflowContentIdentityIndex(tree),
50
54
  });
51
55
  }
52
56
 
@@ -72,13 +76,14 @@ export async function freezeAIWorkflowHolonTaskTargetFromDirectory(
72
76
  rootDir: string,
73
77
  target: HolonTaskTarget,
74
78
  ): Promise<AIWorkflowHolonTaskTargetFreezeReceipt> {
75
- const tree = await loadResourceTree({ rootDir });
79
+ const authoredTree = await loadResourceTree({ rootDir });
80
+ const tree = resolveAIWorkflowResourceTree(authoredTree);
76
81
  const layers = [{ id: 'workspace', tree }] as const;
77
82
  const registry = composeLayeredResourceRegistry({ layers });
78
83
  return freezeAIWorkflowHolonTaskTarget({
79
84
  registry,
80
85
  target,
81
- contentIdentities: resolveEffectiveResourceContentIdentities({ registry, layers }),
86
+ contentIdentities: createResolvedAIWorkflowContentIdentityIndex(tree),
82
87
  });
83
88
  }
84
89