ai-workflow-contract 0.1.7 → 0.1.8
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 +2 -1
- package/src/holon-task.ts +320 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-workflow-contract",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared AI workflow contracts over WorkCtrlFlow and EagerDataFlow substrates",
|
|
6
6
|
"type": "module",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"eager-data-flow-contract": "0.1.3",
|
|
13
13
|
"halfcode-compiler.xnl": "0.2.2",
|
|
14
|
+
"task-manager-contract": "0.1.3",
|
|
14
15
|
"work-ctrl-flow-contract": "0.1.3"
|
|
15
16
|
},
|
|
16
17
|
"files": [
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import type { ImmutableJsonValue, TaskProfileEnvelope } from 'task-manager-contract';
|
|
2
|
+
import { canonicalOwnDataClone } from 'task-manager-contract';
|
|
3
|
+
|
|
4
|
+
export const HOLON_TASK_TARGET_SCHEMA_VERSION = 'ai-workflow.holon-task-target/v1' as const;
|
|
5
|
+
export const HOLON_TASK_SNAPSHOT_RECEIPT_SCHEMA_VERSION = 'ai-workflow.holon-task-snapshot-receipt/v1' as const;
|
|
6
|
+
export const AI_ORGANIZATION_TASK_PROFILE_KIND = 'depa.ai.organization-task' as const;
|
|
7
|
+
|
|
8
|
+
export type HolonTaskResourceRef = `resource://${string}`;
|
|
9
|
+
export type HolonTaskDigest = `sha256:${string}`;
|
|
10
|
+
type ClosedRecord<Shape> = Readonly<Record<string, ImmutableJsonValue>> & Readonly<Shape>;
|
|
11
|
+
|
|
12
|
+
export type HolonTaskTarget = ClosedRecord<{
|
|
13
|
+
readonly kind: 'holon-task-target';
|
|
14
|
+
readonly schemaVersion: typeof HOLON_TASK_TARGET_SCHEMA_VERSION;
|
|
15
|
+
readonly invocation: ClosedRecord<{
|
|
16
|
+
readonly workflowKind: 'AICtrlWorkflow' | 'AIDataWorkflow';
|
|
17
|
+
readonly workflowRef: HolonTaskResourceRef;
|
|
18
|
+
readonly nodeId: string;
|
|
19
|
+
readonly invocationId: string;
|
|
20
|
+
}>;
|
|
21
|
+
readonly holon: ClosedRecord<{
|
|
22
|
+
readonly rootHolonRef: string;
|
|
23
|
+
readonly effectiveAt: string;
|
|
24
|
+
}>;
|
|
25
|
+
readonly executionBinding: ClosedRecord<{
|
|
26
|
+
readonly ref: HolonTaskResourceRef;
|
|
27
|
+
readonly digest: HolonTaskDigest;
|
|
28
|
+
}>;
|
|
29
|
+
readonly taskSpace: ClosedRecord<{
|
|
30
|
+
readonly profileRef: HolonTaskResourceRef;
|
|
31
|
+
readonly policyRef: HolonTaskResourceRef;
|
|
32
|
+
readonly requiredRoleRefs: readonly string[];
|
|
33
|
+
readonly requiredCapabilityRefs: readonly HolonTaskResourceRef[];
|
|
34
|
+
}>;
|
|
35
|
+
readonly output: ClosedRecord<{
|
|
36
|
+
readonly schemaRef: HolonTaskResourceRef;
|
|
37
|
+
readonly materialPortRefs: readonly HolonTaskResourceRef[];
|
|
38
|
+
}>;
|
|
39
|
+
}>;
|
|
40
|
+
|
|
41
|
+
export type HolonTaskSnapshotReceipt = ClosedRecord<{
|
|
42
|
+
readonly kind: 'holon-task-snapshot-receipt';
|
|
43
|
+
readonly schemaVersion: typeof HOLON_TASK_SNAPSHOT_RECEIPT_SCHEMA_VERSION;
|
|
44
|
+
readonly taskSpaceId: string;
|
|
45
|
+
readonly holonRef: string;
|
|
46
|
+
readonly effectiveAt: string;
|
|
47
|
+
readonly holonSnapshotRef: string;
|
|
48
|
+
readonly holonSnapshotDigest: HolonTaskDigest;
|
|
49
|
+
readonly snapshotArtifactDigest: HolonTaskDigest;
|
|
50
|
+
readonly issuerReceiptId: HolonTaskDigest;
|
|
51
|
+
readonly issuerReceiptArtifactDigest: HolonTaskDigest;
|
|
52
|
+
readonly executionBindingRef: HolonTaskResourceRef;
|
|
53
|
+
readonly executionBindingDigest: HolonTaskDigest;
|
|
54
|
+
readonly eligibleMemberRefs: readonly string[];
|
|
55
|
+
readonly eligibleRoleRefs: readonly string[];
|
|
56
|
+
}>;
|
|
57
|
+
|
|
58
|
+
export type HolonTaskSnapshotAdoptionReceipt = ClosedRecord<{
|
|
59
|
+
readonly kind: 'holon-task-snapshot-adoption-receipt';
|
|
60
|
+
readonly schemaVersion: 'ai-workflow.holon-task-snapshot-adoption-receipt/v1';
|
|
61
|
+
readonly taskSpaceId: string;
|
|
62
|
+
readonly previousTaskId: string;
|
|
63
|
+
readonly successorTaskId: string;
|
|
64
|
+
readonly previousIssuerReceiptId: HolonTaskDigest;
|
|
65
|
+
readonly successorIssuerReceiptId: HolonTaskDigest;
|
|
66
|
+
readonly executionBindingDigest: HolonTaskDigest;
|
|
67
|
+
readonly adoptedAt: string;
|
|
68
|
+
}>;
|
|
69
|
+
|
|
70
|
+
export type AIOrganizationTaskProfileFacts = ClosedRecord<{
|
|
71
|
+
target: HolonTaskTarget;
|
|
72
|
+
snapshotReceipt: HolonTaskSnapshotReceipt;
|
|
73
|
+
assignmentReceipt: null;
|
|
74
|
+
memberRuntimeRef: null;
|
|
75
|
+
settlementReceipt: null;
|
|
76
|
+
snapshotAdoptions: readonly HolonTaskSnapshotAdoptionReceipt[];
|
|
77
|
+
}>;
|
|
78
|
+
|
|
79
|
+
export type AIOrganizationTaskProfile = TaskProfileEnvelope<
|
|
80
|
+
typeof AI_ORGANIZATION_TASK_PROFILE_KIND,
|
|
81
|
+
AIOrganizationTaskProfileFacts
|
|
82
|
+
>;
|
|
83
|
+
|
|
84
|
+
export interface AIWorkflowHolonTaskTargetFreezeReceipt {
|
|
85
|
+
readonly schemaVersion: 'ai-workflow.holon-task-target-freeze/v1';
|
|
86
|
+
readonly target: HolonTaskTarget;
|
|
87
|
+
readonly dependencySnapshot: Readonly<{
|
|
88
|
+
readonly registryRevision: string;
|
|
89
|
+
readonly snapshotRevision: HolonTaskDigest;
|
|
90
|
+
readonly closureResourceIds: readonly string[];
|
|
91
|
+
}>;
|
|
92
|
+
readonly semanticFingerprint: HolonTaskDigest;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** In-memory capability projected only from an authentic target freeze receipt. */
|
|
96
|
+
export interface FrozenHolonTaskTarget {
|
|
97
|
+
readonly schemaVersion: 'ai-workflow.frozen-holon-task-target/v1';
|
|
98
|
+
readonly target: HolonTaskTarget;
|
|
99
|
+
readonly semanticFingerprint: HolonTaskDigest;
|
|
100
|
+
readonly snapshotRevision: HolonTaskDigest;
|
|
101
|
+
readonly closureResourceIds: readonly string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function holonTaskTargetFromNodeConfig(
|
|
105
|
+
configInput: unknown,
|
|
106
|
+
expected: Readonly<{
|
|
107
|
+
readonly workflowKind: 'AICtrlWorkflow' | 'AIDataWorkflow';
|
|
108
|
+
readonly workflowRef: HolonTaskResourceRef;
|
|
109
|
+
readonly nodeId: string;
|
|
110
|
+
}>,
|
|
111
|
+
): HolonTaskTarget | undefined {
|
|
112
|
+
const config = record(canonicalOwnDataClone(configInput), '$config');
|
|
113
|
+
if (!Object.prototype.hasOwnProperty.call(config, 'holonTaskTarget')) return undefined;
|
|
114
|
+
const target = normalizeHolonTaskTarget(config.holonTaskTarget);
|
|
115
|
+
if (target.invocation.workflowKind !== expected.workflowKind
|
|
116
|
+
|| target.invocation.workflowRef !== expected.workflowRef
|
|
117
|
+
|| target.invocation.nodeId !== expected.nodeId) {
|
|
118
|
+
invalid('$config.holonTaskTarget.invocation', 'Target invocation identity does not match the authored node');
|
|
119
|
+
}
|
|
120
|
+
return target;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class HolonTaskContractError extends Error {
|
|
124
|
+
constructor(readonly path: string, message: string) {
|
|
125
|
+
super(`${path}: ${message}`);
|
|
126
|
+
this.name = 'HolonTaskContractError';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function normalizeHolonTaskTarget(value: unknown): HolonTaskTarget {
|
|
131
|
+
const root = record(canonicalOwnDataClone(value), '$target');
|
|
132
|
+
exactKeys(root, ['kind', 'schemaVersion', 'invocation', 'holon', 'executionBinding', 'taskSpace', 'output'], '$target');
|
|
133
|
+
exactLiteral(root.kind, 'holon-task-target', '$target.kind');
|
|
134
|
+
exactLiteral(root.schemaVersion, HOLON_TASK_TARGET_SCHEMA_VERSION, '$target.schemaVersion');
|
|
135
|
+
|
|
136
|
+
const invocation = record(root.invocation, '$target.invocation');
|
|
137
|
+
exactKeys(invocation, ['workflowKind', 'workflowRef', 'nodeId', 'invocationId'], '$target.invocation');
|
|
138
|
+
const workflowKind = oneOf(invocation.workflowKind, ['AICtrlWorkflow', 'AIDataWorkflow'] as const, '$target.invocation.workflowKind');
|
|
139
|
+
const holon = record(root.holon, '$target.holon');
|
|
140
|
+
exactKeys(holon, ['rootHolonRef', 'effectiveAt'], '$target.holon');
|
|
141
|
+
const executionBinding = record(root.executionBinding, '$target.executionBinding');
|
|
142
|
+
exactKeys(executionBinding, ['ref', 'digest'], '$target.executionBinding');
|
|
143
|
+
const taskSpace = record(root.taskSpace, '$target.taskSpace');
|
|
144
|
+
exactKeys(taskSpace, ['profileRef', 'policyRef', 'requiredRoleRefs', 'requiredCapabilityRefs'], '$target.taskSpace');
|
|
145
|
+
const output = record(root.output, '$target.output');
|
|
146
|
+
exactKeys(output, ['schemaRef', 'materialPortRefs'], '$target.output');
|
|
147
|
+
|
|
148
|
+
return Object.freeze({
|
|
149
|
+
kind: 'holon-task-target',
|
|
150
|
+
schemaVersion: HOLON_TASK_TARGET_SCHEMA_VERSION,
|
|
151
|
+
invocation: Object.freeze({
|
|
152
|
+
workflowKind,
|
|
153
|
+
workflowRef: resourceRef(invocation.workflowRef, '$target.invocation.workflowRef'),
|
|
154
|
+
nodeId: exactString(invocation.nodeId, '$target.invocation.nodeId'),
|
|
155
|
+
invocationId: exactString(invocation.invocationId, '$target.invocation.invocationId'),
|
|
156
|
+
}),
|
|
157
|
+
holon: Object.freeze({
|
|
158
|
+
rootHolonRef: exactString(holon.rootHolonRef, '$target.holon.rootHolonRef'),
|
|
159
|
+
effectiveAt: utcInstant(holon.effectiveAt, '$target.holon.effectiveAt'),
|
|
160
|
+
}),
|
|
161
|
+
executionBinding: Object.freeze({
|
|
162
|
+
ref: resourceRef(executionBinding.ref, '$target.executionBinding.ref'),
|
|
163
|
+
digest: digest(executionBinding.digest, '$target.executionBinding.digest'),
|
|
164
|
+
}),
|
|
165
|
+
taskSpace: Object.freeze({
|
|
166
|
+
profileRef: resourceRef(taskSpace.profileRef, '$target.taskSpace.profileRef'),
|
|
167
|
+
policyRef: resourceRef(taskSpace.policyRef, '$target.taskSpace.policyRef'),
|
|
168
|
+
requiredRoleRefs: stringArray(taskSpace.requiredRoleRefs, '$target.taskSpace.requiredRoleRefs'),
|
|
169
|
+
requiredCapabilityRefs: resourceRefArray(taskSpace.requiredCapabilityRefs, '$target.taskSpace.requiredCapabilityRefs'),
|
|
170
|
+
}),
|
|
171
|
+
output: Object.freeze({
|
|
172
|
+
schemaRef: resourceRef(output.schemaRef, '$target.output.schemaRef'),
|
|
173
|
+
materialPortRefs: resourceRefArray(output.materialPortRefs, '$target.output.materialPortRefs'),
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function normalizeHolonTaskSnapshotReceipt(value: unknown): HolonTaskSnapshotReceipt {
|
|
179
|
+
const root = record(canonicalOwnDataClone(value), '$snapshotReceipt');
|
|
180
|
+
exactKeys(root, [
|
|
181
|
+
'kind', 'schemaVersion', 'taskSpaceId', 'holonRef', 'effectiveAt', 'holonSnapshotRef',
|
|
182
|
+
'holonSnapshotDigest', 'snapshotArtifactDigest', 'issuerReceiptId',
|
|
183
|
+
'issuerReceiptArtifactDigest', 'executionBindingRef', 'executionBindingDigest',
|
|
184
|
+
'eligibleMemberRefs', 'eligibleRoleRefs',
|
|
185
|
+
], '$snapshotReceipt');
|
|
186
|
+
exactLiteral(root.kind, 'holon-task-snapshot-receipt', '$snapshotReceipt.kind');
|
|
187
|
+
exactLiteral(root.schemaVersion, HOLON_TASK_SNAPSHOT_RECEIPT_SCHEMA_VERSION, '$snapshotReceipt.schemaVersion');
|
|
188
|
+
return Object.freeze({
|
|
189
|
+
kind: 'holon-task-snapshot-receipt',
|
|
190
|
+
schemaVersion: HOLON_TASK_SNAPSHOT_RECEIPT_SCHEMA_VERSION,
|
|
191
|
+
taskSpaceId: exactString(root.taskSpaceId, '$snapshotReceipt.taskSpaceId'),
|
|
192
|
+
holonRef: exactString(root.holonRef, '$snapshotReceipt.holonRef'),
|
|
193
|
+
effectiveAt: utcInstant(root.effectiveAt, '$snapshotReceipt.effectiveAt'),
|
|
194
|
+
holonSnapshotRef: exactString(root.holonSnapshotRef, '$snapshotReceipt.holonSnapshotRef'),
|
|
195
|
+
holonSnapshotDigest: digest(root.holonSnapshotDigest, '$snapshotReceipt.holonSnapshotDigest'),
|
|
196
|
+
snapshotArtifactDigest: digest(root.snapshotArtifactDigest, '$snapshotReceipt.snapshotArtifactDigest'),
|
|
197
|
+
issuerReceiptId: digest(root.issuerReceiptId, '$snapshotReceipt.issuerReceiptId'),
|
|
198
|
+
issuerReceiptArtifactDigest: digest(root.issuerReceiptArtifactDigest, '$snapshotReceipt.issuerReceiptArtifactDigest'),
|
|
199
|
+
executionBindingRef: resourceRef(root.executionBindingRef, '$snapshotReceipt.executionBindingRef'),
|
|
200
|
+
executionBindingDigest: digest(root.executionBindingDigest, '$snapshotReceipt.executionBindingDigest'),
|
|
201
|
+
eligibleMemberRefs: stringArray(root.eligibleMemberRefs, '$snapshotReceipt.eligibleMemberRefs'),
|
|
202
|
+
eligibleRoleRefs: stringArray(root.eligibleRoleRefs, '$snapshotReceipt.eligibleRoleRefs'),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function createAIOrganizationTaskProfile(
|
|
207
|
+
targetInput: unknown,
|
|
208
|
+
receiptInput: unknown,
|
|
209
|
+
adoptionInputs: readonly unknown[] = [],
|
|
210
|
+
): AIOrganizationTaskProfile {
|
|
211
|
+
const target = normalizeHolonTaskTarget(targetInput);
|
|
212
|
+
const snapshotReceipt = normalizeHolonTaskSnapshotReceipt(receiptInput);
|
|
213
|
+
if (snapshotReceipt.taskSpaceId.length === 0
|
|
214
|
+
|| snapshotReceipt.holonRef !== target.holon.rootHolonRef
|
|
215
|
+
|| snapshotReceipt.effectiveAt !== target.holon.effectiveAt
|
|
216
|
+
|| snapshotReceipt.executionBindingRef !== target.executionBinding.ref
|
|
217
|
+
|| snapshotReceipt.executionBindingDigest !== target.executionBinding.digest) {
|
|
218
|
+
invalid('$profile', 'Snapshot receipt does not bind the exact target facts');
|
|
219
|
+
}
|
|
220
|
+
return Object.freeze({
|
|
221
|
+
kind: 'task-profile',
|
|
222
|
+
profileKind: AI_ORGANIZATION_TASK_PROFILE_KIND,
|
|
223
|
+
schemaVersion: 1,
|
|
224
|
+
facts: Object.freeze({
|
|
225
|
+
target,
|
|
226
|
+
snapshotReceipt,
|
|
227
|
+
assignmentReceipt: null,
|
|
228
|
+
memberRuntimeRef: null,
|
|
229
|
+
settlementReceipt: null,
|
|
230
|
+
snapshotAdoptions: Object.freeze(adoptionInputs.map(normalizeHolonTaskSnapshotAdoptionReceipt)),
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function normalizeHolonTaskSnapshotAdoptionReceipt(value: unknown): HolonTaskSnapshotAdoptionReceipt {
|
|
236
|
+
const root = record(canonicalOwnDataClone(value), '$snapshotAdoptionReceipt');
|
|
237
|
+
exactKeys(root, [
|
|
238
|
+
'kind', 'schemaVersion', 'taskSpaceId', 'previousTaskId', 'successorTaskId',
|
|
239
|
+
'previousIssuerReceiptId', 'successorIssuerReceiptId', 'executionBindingDigest', 'adoptedAt',
|
|
240
|
+
], '$snapshotAdoptionReceipt');
|
|
241
|
+
exactLiteral(root.kind, 'holon-task-snapshot-adoption-receipt', '$snapshotAdoptionReceipt.kind');
|
|
242
|
+
exactLiteral(root.schemaVersion, 'ai-workflow.holon-task-snapshot-adoption-receipt/v1', '$snapshotAdoptionReceipt.schemaVersion');
|
|
243
|
+
return Object.freeze({
|
|
244
|
+
kind: 'holon-task-snapshot-adoption-receipt',
|
|
245
|
+
schemaVersion: 'ai-workflow.holon-task-snapshot-adoption-receipt/v1',
|
|
246
|
+
taskSpaceId: exactString(root.taskSpaceId, '$snapshotAdoptionReceipt.taskSpaceId'),
|
|
247
|
+
previousTaskId: exactString(root.previousTaskId, '$snapshotAdoptionReceipt.previousTaskId'),
|
|
248
|
+
successorTaskId: exactString(root.successorTaskId, '$snapshotAdoptionReceipt.successorTaskId'),
|
|
249
|
+
previousIssuerReceiptId: digest(root.previousIssuerReceiptId, '$snapshotAdoptionReceipt.previousIssuerReceiptId'),
|
|
250
|
+
successorIssuerReceiptId: digest(root.successorIssuerReceiptId, '$snapshotAdoptionReceipt.successorIssuerReceiptId'),
|
|
251
|
+
executionBindingDigest: digest(root.executionBindingDigest, '$snapshotAdoptionReceipt.executionBindingDigest'),
|
|
252
|
+
adoptedAt: utcInstant(root.adoptedAt, '$snapshotAdoptionReceipt.adoptedAt'),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function record(value: unknown, path: string): Readonly<Record<string, unknown>> {
|
|
257
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) invalid(path, 'Expected an object');
|
|
258
|
+
return value as Readonly<Record<string, unknown>>;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function exactKeys(value: Readonly<Record<string, unknown>>, expected: readonly string[], path: string): void {
|
|
262
|
+
const actual = Object.keys(value);
|
|
263
|
+
const expectedSet = new Set(expected);
|
|
264
|
+
const extra = actual.find((key) => !expectedSet.has(key));
|
|
265
|
+
if (extra !== undefined) invalid(`${path}.${extra}`, 'Unsupported field');
|
|
266
|
+
const missing = expected.find((key) => !Object.prototype.hasOwnProperty.call(value, key));
|
|
267
|
+
if (missing !== undefined) invalid(`${path}.${missing}`, 'Required field is missing');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function exactString(value: unknown, path: string): string {
|
|
271
|
+
if (typeof value !== 'string' || value.length === 0 || value !== value.trim()) invalid(path, 'Expected an exact non-empty string');
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function exactLiteral<const Value extends string>(value: unknown, expected: Value, path: string): Value {
|
|
276
|
+
if (value !== expected) invalid(path, `Expected '${expected}'`);
|
|
277
|
+
return expected;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function oneOf<const Values extends readonly string[]>(value: unknown, expected: Values, path: string): Values[number] {
|
|
281
|
+
if (typeof value !== 'string' || !expected.includes(value)) invalid(path, `Expected one of ${expected.join('|')}`);
|
|
282
|
+
return value as Values[number];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function resourceRef(value: unknown, path: string): HolonTaskResourceRef {
|
|
286
|
+
const ref = exactString(value, path);
|
|
287
|
+
if (!/^resource:\/\/[^/\s][^\s]*$/.test(ref)) invalid(path, 'Expected a canonical resource:// reference');
|
|
288
|
+
return ref as HolonTaskResourceRef;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function digest(value: unknown, path: string): HolonTaskDigest {
|
|
292
|
+
if (typeof value !== 'string' || !/^sha256:[0-9a-f]{64}$/.test(value)) invalid(path, 'Expected a canonical SHA-256 digest');
|
|
293
|
+
return value as HolonTaskDigest;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function utcInstant(value: unknown, path: string): string {
|
|
297
|
+
const instant = exactString(value, path);
|
|
298
|
+
const parsed = Date.parse(instant);
|
|
299
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(instant)
|
|
300
|
+
|| Number.isNaN(parsed) || new Date(parsed).toISOString() !== instant) invalid(path, 'Expected a canonical UTC instant');
|
|
301
|
+
return instant;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function stringArray(value: unknown, path: string): readonly string[] {
|
|
305
|
+
if (!Array.isArray(value)) invalid(path, 'Expected an array');
|
|
306
|
+
const result = Object.freeze(value.map((item, index) => exactString(item, `${path}[${index}]`)));
|
|
307
|
+
if (new Set(result).size !== result.length) invalid(path, 'Duplicate values are not allowed');
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function resourceRefArray(value: unknown, path: string): readonly HolonTaskResourceRef[] {
|
|
312
|
+
if (!Array.isArray(value)) invalid(path, 'Expected an array');
|
|
313
|
+
const result = Object.freeze(value.map((item, index) => resourceRef(item, `${path}[${index}]`)));
|
|
314
|
+
if (new Set(result).size !== result.length) invalid(path, 'Duplicate values are not allowed');
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function invalid(path: string, message: string): never {
|
|
319
|
+
throw new HolonTaskContractError(path, message);
|
|
320
|
+
}
|