@xemahq/dsl 0.8.1 → 0.8.2
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 -3
- package/src/deliverable-spec/index.ts +0 -19
- package/src/deliverable-spec/lib/schema.ts +0 -270
- package/src/deliverable-spec/lib/types.ts +0 -26
- package/src/payload-codec/index.ts +0 -44
- package/src/payload-codec/lib/blob-store.ts +0 -176
- package/src/payload-codec/lib/codec-context.ts +0 -38
- package/src/payload-codec/lib/codec.ts +0 -605
- package/src/payload-codec/lib/enums.ts +0 -58
- package/src/payload-codec/lib/errors.ts +0 -54
- package/src/payload-codec/lib/http-blob-store.ts +0 -267
- package/src/payload-codec/lib/lru-cache.ts +0 -81
- package/src/payload-codec/lib/payload.ts +0 -26
- package/src/payload-codec/temporal/index.ts +0 -36
- package/src/workflow/index.ts +0 -108
- package/src/workflow/lib/action-input-validator.ts +0 -160
- package/src/workflow/lib/compiler/action-shape.ts +0 -71
- package/src/workflow/lib/compiler/canonical-json.ts +0 -66
- package/src/workflow/lib/compiler/compile.ts +0 -1742
- package/src/workflow/lib/compiler/concurrency.ts +0 -223
- package/src/workflow/lib/compiler/dag.ts +0 -108
- package/src/workflow/lib/compiler/gate-defaults.ts +0 -153
- package/src/workflow/lib/compiler/index.ts +0 -11
- package/src/workflow/lib/compiler/inputs.ts +0 -254
- package/src/workflow/lib/compiler/installation-resource-validator.ts +0 -114
- package/src/workflow/lib/compiler/manifest-source.ts +0 -71
- package/src/workflow/lib/compiler/matrix.ts +0 -135
- package/src/workflow/lib/compiler/mount-plan.ts +0 -190
- package/src/workflow/lib/compiler/payload-reach-in.ts +0 -497
- package/src/workflow/lib/compiler/permissions.ts +0 -64
- package/src/workflow/lib/compiler/retry-timeout.ts +0 -105
- package/src/workflow/lib/compiler/review-step.ts +0 -548
- package/src/workflow/lib/compiler/types.ts +0 -172
- package/src/workflow/lib/compiler/variable-requirements.ts +0 -208
- package/src/workflow/lib/deliverable-spec-introspection-error.ts +0 -63
- package/src/workflow/lib/deliverable-spec-keys.ts +0 -147
- package/src/workflow/lib/deliverable-spec-source-scan.ts +0 -280
- package/src/workflow/lib/dispatch-inputs/index.ts +0 -160
- package/src/workflow/lib/dispatch-inputs/to-json-schema.ts +0 -60
- package/src/workflow/lib/duration.ts +0 -43
- package/src/workflow/lib/errors.ts +0 -37
- package/src/workflow/lib/expression/ast.ts +0 -108
- package/src/workflow/lib/expression/context.ts +0 -148
- package/src/workflow/lib/expression/evaluator.ts +0 -492
- package/src/workflow/lib/expression/index.ts +0 -28
- package/src/workflow/lib/expression/interpolation.ts +0 -84
- package/src/workflow/lib/expression/parser.ts +0 -264
- package/src/workflow/lib/expression/template.ts +0 -117
- package/src/workflow/lib/expression/tokenizer.ts +0 -200
- package/src/workflow/lib/expression/tokens.ts +0 -30
- package/src/workflow/lib/expression/walk-artifact-refs.ts +0 -232
- package/src/workflow/lib/installation-resource-kind.ts +0 -107
- package/src/workflow/lib/schemas-loader.ts +0 -64
- package/src/workflow/lib/serializer.ts +0 -30
- package/src/workflow/lib/types.ts +0 -417
- package/src/workflow/lib/validate.ts +0 -199
- package/src/workspace-manifest/index.ts +0 -27
- package/src/workspace-manifest/lib/compile.ts +0 -619
- package/src/workspace-manifest/lib/interpolate.ts +0 -166
- package/src/workspace-manifest/lib/resolve-extends.ts +0 -260
- package/src/workspace-manifest/lib/schema.ts +0 -692
- package/src/workspace-manifest/lib/types.ts +0 -446
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/dsl",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Consolidated Xema OS DSL bundle — pure compilers/codecs for the workflow DSL, workspace-manifest DSL, deliverable-spec DSL, and the workflow payload codec. One package, per-surface subpath exports. No framework/runtime deps.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Neuralchowder Inc. <developer@xema.dev> (https://xema.dev)",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"types": "dist/workflow/index.d.ts",
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
|
-
"src",
|
|
23
22
|
"schema"
|
|
24
23
|
],
|
|
25
24
|
"devDependencies": {
|
|
@@ -32,7 +31,7 @@
|
|
|
32
31
|
"ts-jest": "^29.4.9",
|
|
33
32
|
"typescript": "5.9.3",
|
|
34
33
|
"undici-types": "~7.16.0",
|
|
35
|
-
"@xemahq/kernel-contracts": "^8.0.
|
|
34
|
+
"@xemahq/kernel-contracts": "^8.0.1"
|
|
36
35
|
},
|
|
37
36
|
"dependencies": {
|
|
38
37
|
"ajv": "^8.20.0",
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// ── @xemahq/deliverable-spec-dsl ──
|
|
3
|
-
//
|
|
4
|
-
// Zod schema for the `manifest.yaml` files that ship inside biome
|
|
5
|
-
// `deliverable-specs/` content directories. A deliverable-spec manifest
|
|
6
|
-
// declares the SHAPE of a single catalog row: kind, content body, page
|
|
7
|
-
// layout, validator parameters.
|
|
8
|
-
//
|
|
9
|
-
// Consumers:
|
|
10
|
-
// - The deliverable-specs service: the seeder validates each manifest at
|
|
11
|
-
// boot using this schema; the schemas controller serves the JSON
|
|
12
|
-
// Schema rendition at `/schemas/deliverable-spec/v1.json` so authors
|
|
13
|
-
// can wire their YAML editor via `# yaml-language-server: $schema=…`.
|
|
14
|
-
//
|
|
15
|
-
// Sibling to `@xemahq/workflow-dsl` and `@xemahq/workspace-manifest-dsl`.
|
|
16
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
-
|
|
18
|
-
export * from './lib/schema';
|
|
19
|
-
export * from './lib/types';
|
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
-
// ── Zod schemas for `manifest.yaml` (deliverable-spec) ──
|
|
5
|
-
//
|
|
6
|
-
// Single source of truth: the deliverable-specs-api seeder validates each
|
|
7
|
-
// biome-shipped manifest with `DeliverableSpecManifestSchema.parse(…)`,
|
|
8
|
-
// and the schemas controller serves `z.toJSONSchema(…)` at
|
|
9
|
-
// `https://xema.dev/schemas/deliverable/v1/DeliverableSpec.json`. Authors
|
|
10
|
-
// point Monaco / IDE YAML extensions at the served URL via
|
|
11
|
-
// `# yaml-language-server: $schema=…`.
|
|
12
|
-
//
|
|
13
|
-
// Envelope shape mirrors workflow.yaml + workspace-manifest.yaml:
|
|
14
|
-
// apiVersion: xema.dev/deliverable/v1
|
|
15
|
-
// kind: DeliverableSpec | DeliverableSpecOverlay
|
|
16
|
-
// metadata: { slug, version, title, description? } (overlay: slug, title)
|
|
17
|
-
// spec: { kind, category, …, content?, pages?, … }
|
|
18
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
-
|
|
20
|
-
// Kebab-case slug, optionally namespaced (`foo-bar` or `family/leaf`).
|
|
21
|
-
const SlugRegex = /^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)?$/;
|
|
22
|
-
const VersionRegex = /^\d+\.\d+\.\d+$/;
|
|
23
|
-
const KebabRegex = /^[a-z][a-z0-9-]*$/;
|
|
24
|
-
// Overlay slugs are composed keys joining a stack/concern token to one
|
|
25
|
-
// or more spec categories with `+` (e.g.
|
|
26
|
-
// `react-architecture+api-design`). Lowercase alphanumerics, `-`, `+`.
|
|
27
|
-
const OverlaySlugRegex = /^[a-z][a-z0-9-]*(?:\+[a-z][a-z0-9-]*)*$/;
|
|
28
|
-
// Tag values are lowercase alphanumerics with `-`, `_`, `.` allowed
|
|
29
|
-
// (e.g. `tensorflow`, `python-3.12`).
|
|
30
|
-
const TagSlugRegex = /^[a-z0-9][a-z0-9-_.]*$/;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Closed set of deliverable kinds (the inner kind enum on `spec.kind` —
|
|
34
|
-
* NOT to be confused with the envelope `kind: DeliverableSpec`). Mirrors
|
|
35
|
-
* the `DeliverableSpecKind` enum used by the deliverable-specs service
|
|
36
|
-
* — the wire values must stay byte-identical because they're persisted
|
|
37
|
-
* verbatim to `DeliverableSpec.kind`.
|
|
38
|
-
*/
|
|
39
|
-
export const DELIVERABLE_SPEC_KINDS = [
|
|
40
|
-
'DOCUMENT_TEMPLATE',
|
|
41
|
-
'ZOD_SCHEMA',
|
|
42
|
-
'JSON_SCHEMA',
|
|
43
|
-
'ENDPOINT_FETCH',
|
|
44
|
-
'STRUCTURED_JSON',
|
|
45
|
-
'CUSTOM',
|
|
46
|
-
'RESPONSE_ONLY',
|
|
47
|
-
] as const;
|
|
48
|
-
|
|
49
|
-
export type DeliverableSpecKindLiteral = (typeof DELIVERABLE_SPEC_KINDS)[number];
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Closed set of artifact-versioning modes accepted by the harvester.
|
|
53
|
-
* Mirrors `ArtifactVersioningMode` in `@xemahq/platform-common` — kept as
|
|
54
|
-
* a string-literal trio here to keep this package free of platform deps.
|
|
55
|
-
*/
|
|
56
|
-
export const VERSIONING_MODES = ['append', 'new', 'replace'] as const;
|
|
57
|
-
|
|
58
|
-
export type VersioningModeLiteral = (typeof VERSIONING_MODES)[number];
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Inline page entry — body is generated by the agent at run time from
|
|
62
|
-
* `slug`/`title` alone. Stored as `{slug, title, parentSlug, content: null}`
|
|
63
|
-
* in `DeliverableSpec.pages`.
|
|
64
|
-
*/
|
|
65
|
-
export const InlinePageSourceSchema = z.object({
|
|
66
|
-
slug: z.string().regex(KebabRegex, 'page slug must be kebab-case'),
|
|
67
|
-
title: z.string().min(1).max(200),
|
|
68
|
-
parentSlug: z
|
|
69
|
-
.string()
|
|
70
|
-
.regex(KebabRegex)
|
|
71
|
-
.nullable()
|
|
72
|
-
.optional(),
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* File-backed page entry — the seeder reads `<file>` relative to the
|
|
77
|
-
* manifest, parses YAML frontmatter for `slug`/`title`/`parentSlug`, and
|
|
78
|
-
* stores the remaining markdown body as `content`.
|
|
79
|
-
*/
|
|
80
|
-
export const FilePageSourceSchema = z.object({
|
|
81
|
-
file: z.string().min(1),
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
export const PageSourceSchema = z.union([
|
|
85
|
-
InlinePageSourceSchema,
|
|
86
|
-
FilePageSourceSchema,
|
|
87
|
-
]);
|
|
88
|
-
|
|
89
|
-
const TargetSlotRegex = /^(?:deliverables\/|repos\/[a-z0-9][a-z0-9-_]*\/)$/;
|
|
90
|
-
|
|
91
|
-
// ─── Envelope: DeliverableSpec ──────────────────────────────────────────
|
|
92
|
-
|
|
93
|
-
export const DeliverableSpecMetadataSchema = z.object({
|
|
94
|
-
slug: z
|
|
95
|
-
.string()
|
|
96
|
-
.min(1)
|
|
97
|
-
.max(150)
|
|
98
|
-
.regex(
|
|
99
|
-
SlugRegex,
|
|
100
|
-
'slug must be kebab-case (`foo-bar`) or kebab/kebab (`family/leaf`)',
|
|
101
|
-
),
|
|
102
|
-
version: z
|
|
103
|
-
.string()
|
|
104
|
-
.regex(VersionRegex, 'version must be strict semver (MAJOR.MINOR.PATCH)'),
|
|
105
|
-
title: z.string().min(1).max(200),
|
|
106
|
-
description: z.string().max(4000).optional(),
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
export const DeliverableSpecSpecSchema = z
|
|
110
|
-
.object({
|
|
111
|
-
kind: z.enum(DELIVERABLE_SPEC_KINDS),
|
|
112
|
-
category: z.string().regex(KebabRegex, 'category must be kebab-case').max(80),
|
|
113
|
-
complexity: z
|
|
114
|
-
.string()
|
|
115
|
-
.regex(KebabRegex, 'complexity must be kebab-case')
|
|
116
|
-
.optional(),
|
|
117
|
-
tags: z.array(z.string()).max(20).optional(),
|
|
118
|
-
phases: z.array(z.string()).max(20).optional(),
|
|
119
|
-
workTypes: z.array(z.string()).max(20).optional(),
|
|
120
|
-
|
|
121
|
-
// Authoritative body for DOCUMENT_TEMPLATE / JSON_SCHEMA /
|
|
122
|
-
// STRUCTURED_JSON / CUSTOM / RESPONSE_ONLY. Mutually exclusive with
|
|
123
|
-
// `contentFile` (the seeder reads the file relative to manifest.yaml
|
|
124
|
-
// when `contentFile` is set).
|
|
125
|
-
content: z.string().max(1_000_000).optional(),
|
|
126
|
-
contentFile: z.string().min(1).optional(),
|
|
127
|
-
|
|
128
|
-
multiPage: z.boolean().optional(),
|
|
129
|
-
pages: z.array(PageSourceSchema).max(64).optional(),
|
|
130
|
-
|
|
131
|
-
pageAllocationHints: z.record(z.string(), z.unknown()).optional(),
|
|
132
|
-
topologySchema: z.record(z.string(), z.unknown()).optional(),
|
|
133
|
-
sectionGuidance: z.record(z.string(), z.unknown()).optional(),
|
|
134
|
-
scope: z.record(z.string(), z.unknown()).optional(),
|
|
135
|
-
reviewDimensions: z.array(z.unknown()).optional(),
|
|
136
|
-
standards: z.array(z.unknown()).optional(),
|
|
137
|
-
|
|
138
|
-
// ZOD_SCHEMA-specific. Mutually exclusive with `zodSchemaSourceFile`.
|
|
139
|
-
zodSchemaSource: z.string().max(200_000).optional(),
|
|
140
|
-
zodSchemaSourceFile: z.string().min(1).optional(),
|
|
141
|
-
// Names WHICH exported `z.object({ … })` in the source is the
|
|
142
|
-
// deliverable contract. Required only when the source declares more
|
|
143
|
-
// than one top-level schema — with a single schema there is nothing
|
|
144
|
-
// to disambiguate. A multi-schema source without this fails fast at
|
|
145
|
-
// introspection rather than silently taking the first literal (which
|
|
146
|
-
// is an element/helper schema whenever the contract references one).
|
|
147
|
-
zodSchemaExport: z
|
|
148
|
-
.string()
|
|
149
|
-
.regex(
|
|
150
|
-
/^[A-Za-z_$][A-Za-z0-9_$]*$/,
|
|
151
|
-
'zodSchemaExport must be a JavaScript identifier (the exported const name)',
|
|
152
|
-
)
|
|
153
|
-
.optional(),
|
|
154
|
-
|
|
155
|
-
// ENDPOINT_FETCH-specific. Shape matches `EndpointFetchSpec` in
|
|
156
|
-
// `@xemahq/kernel-contracts/agent-workspace` — kept as an open record here
|
|
157
|
-
// to avoid a cross-package dependency; the activity validates the
|
|
158
|
-
// detailed shape at run time.
|
|
159
|
-
fetchSpec: z.record(z.string(), z.unknown()).optional(),
|
|
160
|
-
|
|
161
|
-
assets: z.array(z.unknown()).optional(),
|
|
162
|
-
rules: z.record(z.string(), z.unknown()).optional(),
|
|
163
|
-
|
|
164
|
-
targetSlot: z
|
|
165
|
-
.string()
|
|
166
|
-
.regex(
|
|
167
|
-
TargetSlotRegex,
|
|
168
|
-
"targetSlot must be 'deliverables/' or 'repos/<slug>/'",
|
|
169
|
-
)
|
|
170
|
-
.optional(),
|
|
171
|
-
|
|
172
|
-
versioningMode: z.enum(VERSIONING_MODES).optional(),
|
|
173
|
-
})
|
|
174
|
-
.refine(
|
|
175
|
-
(s) => !(s.content !== undefined && s.contentFile !== undefined),
|
|
176
|
-
{ message: 'spec cannot declare both `content` and `contentFile`' },
|
|
177
|
-
)
|
|
178
|
-
.refine(
|
|
179
|
-
(s) =>
|
|
180
|
-
!(s.zodSchemaSource !== undefined && s.zodSchemaSourceFile !== undefined),
|
|
181
|
-
{
|
|
182
|
-
message:
|
|
183
|
-
'spec cannot declare both `zodSchemaSource` and `zodSchemaSourceFile`',
|
|
184
|
-
},
|
|
185
|
-
)
|
|
186
|
-
.refine(
|
|
187
|
-
(s) =>
|
|
188
|
-
!(s.multiPage === true && (s.pages === undefined || s.pages.length === 0)),
|
|
189
|
-
{ message: 'multiPage=true requires non-empty `pages[]`' },
|
|
190
|
-
)
|
|
191
|
-
.refine(
|
|
192
|
-
(s) =>
|
|
193
|
-
s.zodSchemaExport === undefined ||
|
|
194
|
-
s.zodSchemaSource !== undefined ||
|
|
195
|
-
s.zodSchemaSourceFile !== undefined,
|
|
196
|
-
{
|
|
197
|
-
message:
|
|
198
|
-
'`zodSchemaExport` names an export in the Zod source, so it requires `zodSchemaSource` or `zodSchemaSourceFile`',
|
|
199
|
-
},
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
export const DeliverableSpecManifestSchema = z
|
|
203
|
-
.object({
|
|
204
|
-
apiVersion: z.literal('xema.dev/deliverable/v1'),
|
|
205
|
-
kind: z.literal('DeliverableSpec'),
|
|
206
|
-
metadata: DeliverableSpecMetadataSchema,
|
|
207
|
-
spec: DeliverableSpecSpecSchema,
|
|
208
|
-
})
|
|
209
|
-
.meta({
|
|
210
|
-
id: 'https://xema.dev/schemas/deliverable/v1/DeliverableSpec.json',
|
|
211
|
-
title: 'Xema DeliverableSpec',
|
|
212
|
-
description:
|
|
213
|
-
'Biome-shipped catalog row defining a deliverable kind, content body, page layout, and validator parameters.',
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// ─── Envelope: DeliverableSpecOverlay ───────────────────────────────────
|
|
217
|
-
|
|
218
|
-
export const DeliverableSpecOverlayMetadataSchema = z.object({
|
|
219
|
-
slug: z
|
|
220
|
-
.string()
|
|
221
|
-
.min(1)
|
|
222
|
-
.max(150)
|
|
223
|
-
.regex(
|
|
224
|
-
OverlaySlugRegex,
|
|
225
|
-
'overlay slug must be kebab tokens joined with `+` (e.g. `react-architecture+api-design`)',
|
|
226
|
-
),
|
|
227
|
-
title: z.string().min(1).max(200),
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
export const DeliverableSpecOverlaySpecSchema = z
|
|
231
|
-
.object({
|
|
232
|
-
tagCategory: z
|
|
233
|
-
.string()
|
|
234
|
-
.min(1)
|
|
235
|
-
.max(80)
|
|
236
|
-
.regex(KebabRegex, 'tagCategory must be kebab-case'),
|
|
237
|
-
tagValue: z
|
|
238
|
-
.string()
|
|
239
|
-
.min(1)
|
|
240
|
-
.max(200)
|
|
241
|
-
.regex(
|
|
242
|
-
TagSlugRegex,
|
|
243
|
-
'tagValue must be a slug (lowercase alphanumerics, `+ . - _` allowed)',
|
|
244
|
-
),
|
|
245
|
-
specCategories: z.array(z.string()).max(40).optional(),
|
|
246
|
-
content: z.string().max(1_000_000).optional(),
|
|
247
|
-
contentFile: z.string().min(1).optional(),
|
|
248
|
-
priority: z.number().int().min(0).max(1000).optional(),
|
|
249
|
-
})
|
|
250
|
-
.refine((s) => s.content !== undefined || s.contentFile !== undefined, {
|
|
251
|
-
message: 'spec must declare either `content` or `contentFile`',
|
|
252
|
-
})
|
|
253
|
-
.refine(
|
|
254
|
-
(s) => !(s.content !== undefined && s.contentFile !== undefined),
|
|
255
|
-
{ message: 'spec cannot declare both `content` and `contentFile`' },
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
export const DeliverableSpecOverlayManifestSchema = z
|
|
259
|
-
.object({
|
|
260
|
-
apiVersion: z.literal('xema.dev/deliverable/v1'),
|
|
261
|
-
kind: z.literal('DeliverableSpecOverlay'),
|
|
262
|
-
metadata: DeliverableSpecOverlayMetadataSchema,
|
|
263
|
-
spec: DeliverableSpecOverlaySpecSchema,
|
|
264
|
-
})
|
|
265
|
-
.meta({
|
|
266
|
-
id: 'https://xema.dev/schemas/deliverable/v1/DeliverableSpecOverlay.json',
|
|
267
|
-
title: 'Xema DeliverableSpecOverlay',
|
|
268
|
-
description:
|
|
269
|
-
'Tag-driven overlay that appends to a base spec\'s content when the render context\'s tags[] includes tagValue.',
|
|
270
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
DeliverableSpecManifestSchema,
|
|
5
|
-
DeliverableSpecOverlayManifestSchema,
|
|
6
|
-
FilePageSourceSchema,
|
|
7
|
-
InlinePageSourceSchema,
|
|
8
|
-
PageSourceSchema,
|
|
9
|
-
} from './schema';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* TypeScript types inferred from the Zod schemas. Consumers prefer these
|
|
13
|
-
* over the schemas themselves at compile time so refactors of the shape
|
|
14
|
-
* surface as type errors at every call site.
|
|
15
|
-
*/
|
|
16
|
-
export type DeliverableSpecManifest = z.infer<
|
|
17
|
-
typeof DeliverableSpecManifestSchema
|
|
18
|
-
>;
|
|
19
|
-
|
|
20
|
-
export type DeliverableSpecOverlayManifest = z.infer<
|
|
21
|
-
typeof DeliverableSpecOverlayManifestSchema
|
|
22
|
-
>;
|
|
23
|
-
|
|
24
|
-
export type InlinePageSource = z.infer<typeof InlinePageSourceSchema>;
|
|
25
|
-
export type FilePageSource = z.infer<typeof FilePageSourceSchema>;
|
|
26
|
-
export type PageSource = z.infer<typeof PageSourceSchema>;
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
// workflow-payload-codec — an engine-agnostic PayloadCodec that transparently
|
|
3
|
-
// spills large payloads to a content-addressed BlobStore (an HTTP blob store
|
|
4
|
-
// in prod; in-memory in tests). Register this codec on the Client AND on the
|
|
5
|
-
// Worker — both sides must decode the envelope symmetrically. The core has no
|
|
6
|
-
// workflow-engine dependency; the Temporal binding lives in the optional
|
|
7
|
-
// `@xemahq/dsl/payload-codec/temporal` adapter.
|
|
8
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
9
|
-
|
|
10
|
-
export type { Payload, PayloadCodec } from './lib/payload';
|
|
11
|
-
|
|
12
|
-
export {
|
|
13
|
-
BlobStoreKind,
|
|
14
|
-
DEFAULT_CACHE_CAPACITY_BYTES,
|
|
15
|
-
DEFAULT_SPILL_THRESHOLD_BYTES,
|
|
16
|
-
SPILL_ENCODING_V1,
|
|
17
|
-
SPILL_ENVELOPE_VERSION,
|
|
18
|
-
SpillMetadataKey,
|
|
19
|
-
} from './lib/enums';
|
|
20
|
-
export {
|
|
21
|
-
PayloadCodecError,
|
|
22
|
-
PayloadCodecErrorCode,
|
|
23
|
-
isPayloadCodecError,
|
|
24
|
-
} from './lib/errors';
|
|
25
|
-
export {
|
|
26
|
-
InMemoryBlobStore,
|
|
27
|
-
sha256Hex,
|
|
28
|
-
type BlobRef,
|
|
29
|
-
type BlobStore,
|
|
30
|
-
type StoredBlob,
|
|
31
|
-
} from './lib/blob-store';
|
|
32
|
-
export { HttpBlobStore, type HttpBlobStoreOptions } from './lib/http-blob-store';
|
|
33
|
-
export { BytesLruCache } from './lib/lru-cache';
|
|
34
|
-
export {
|
|
35
|
-
SpillPayloadCodec,
|
|
36
|
-
type SpillPayloadCodecOptions,
|
|
37
|
-
type CodecLifecycleEvent,
|
|
38
|
-
type CodecObserver,
|
|
39
|
-
} from './lib/codec';
|
|
40
|
-
export {
|
|
41
|
-
getCodecOrgId,
|
|
42
|
-
runWithCodecOrgId,
|
|
43
|
-
type CodecContext,
|
|
44
|
-
} from './lib/codec-context';
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
import { BlobStoreKind } from './enums';
|
|
4
|
-
import { PayloadCodecError, PayloadCodecErrorCode } from './errors';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Content-addressed blob stored by a BlobStore. The (orgId, sha256) pair
|
|
8
|
-
* uniquely identifies a blob in a multi-tenant store; `uri` is the store's
|
|
9
|
-
* opaque retrieval handle. The `orgId` MUST round-trip through the codec
|
|
10
|
-
* envelope so a decoder can fetch the bytes back without ambient context.
|
|
11
|
-
*/
|
|
12
|
-
export interface StoredBlob {
|
|
13
|
-
readonly store: BlobStoreKind;
|
|
14
|
-
readonly uri: string;
|
|
15
|
-
readonly sha256: string;
|
|
16
|
-
readonly orgId: string;
|
|
17
|
-
readonly sizeBytes: number;
|
|
18
|
-
readonly contentType: string;
|
|
19
|
-
readonly createdAt: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Reference to a stored blob — what the codec writes into the envelope. */
|
|
23
|
-
export interface BlobRef {
|
|
24
|
-
readonly store: BlobStoreKind;
|
|
25
|
-
readonly uri: string;
|
|
26
|
-
readonly sha256: string;
|
|
27
|
-
readonly orgId: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Generic blob store contract. Implementations MUST:
|
|
32
|
-
* - Content-address by (orgId, sha256); same bytes for the same org ⇒
|
|
33
|
-
* same uri.
|
|
34
|
-
* - Be idempotent: `put` of already-present (orgId, bytes) returns the
|
|
35
|
-
* existing row without throwing; `delete` of an absent (orgId, sha256)
|
|
36
|
-
* returns `{ deleted: false }` without throwing.
|
|
37
|
-
* - Verify sha256 on `get` and throw on mismatch.
|
|
38
|
-
*
|
|
39
|
-
* All implementations MUST NOT mutate their inputs. The codec reuses the
|
|
40
|
-
* same Uint8Array across multiple payloads when batching.
|
|
41
|
-
*/
|
|
42
|
-
export interface BlobStore {
|
|
43
|
-
readonly kind: BlobStoreKind;
|
|
44
|
-
put(bytes: Uint8Array, contentType: string, orgId: string): Promise<StoredBlob>;
|
|
45
|
-
get(ref: BlobRef): Promise<Uint8Array>;
|
|
46
|
-
/**
|
|
47
|
-
* Idempotent removal. Used by the workflow-engine retention sweeper
|
|
48
|
-
* after a `WorkflowRun` row is archived. Returns `{ deleted: true }`
|
|
49
|
-
* if a blob was actually removed, `{ deleted: false }` if no row was
|
|
50
|
-
* present (already-cleaned, retry-safe).
|
|
51
|
-
*/
|
|
52
|
-
delete(ref: Pick<BlobRef, 'sha256' | 'orgId'>): Promise<{ deleted: boolean }>;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* In-memory BlobStore for tests + dev loopback. NOT horizontally scalable
|
|
57
|
-
* — each process has its own map, so engine + worker don't share state.
|
|
58
|
-
* Production must use HttpBlobStore against artifact-store-api.
|
|
59
|
-
*
|
|
60
|
-
* Tenant scoping mirrors the production store: rows are keyed by
|
|
61
|
-
* `(orgId, sha256)` so a `get` with the wrong orgId returns BLOB_GET_FAILED
|
|
62
|
-
* (matches the artifact-store-api behavior of returning 404 cross-org).
|
|
63
|
-
*/
|
|
64
|
-
export class InMemoryBlobStore implements BlobStore {
|
|
65
|
-
readonly kind = BlobStoreKind.IN_MEMORY;
|
|
66
|
-
private readonly blobs = new Map<
|
|
67
|
-
string,
|
|
68
|
-
{ bytes: Uint8Array; contentType: string; createdAt: string }
|
|
69
|
-
>();
|
|
70
|
-
|
|
71
|
-
async put(
|
|
72
|
-
bytes: Uint8Array,
|
|
73
|
-
contentType: string,
|
|
74
|
-
orgId: string,
|
|
75
|
-
): Promise<StoredBlob> {
|
|
76
|
-
assertNonEmptyOrgId(orgId);
|
|
77
|
-
const sha256 = sha256Hex(bytes);
|
|
78
|
-
const key = makeKey(orgId, sha256);
|
|
79
|
-
const existing = this.blobs.get(key);
|
|
80
|
-
if (existing) {
|
|
81
|
-
return {
|
|
82
|
-
store: this.kind,
|
|
83
|
-
uri: `memory://${orgId}/${sha256}`,
|
|
84
|
-
sha256,
|
|
85
|
-
orgId,
|
|
86
|
-
sizeBytes: existing.bytes.byteLength,
|
|
87
|
-
contentType: existing.contentType,
|
|
88
|
-
createdAt: existing.createdAt,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
const createdAt = new Date().toISOString();
|
|
92
|
-
this.blobs.set(key, { bytes, contentType, createdAt });
|
|
93
|
-
return {
|
|
94
|
-
store: this.kind,
|
|
95
|
-
uri: `memory://${orgId}/${sha256}`,
|
|
96
|
-
sha256,
|
|
97
|
-
orgId,
|
|
98
|
-
sizeBytes: bytes.byteLength,
|
|
99
|
-
contentType,
|
|
100
|
-
createdAt,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async get(ref: BlobRef): Promise<Uint8Array> {
|
|
105
|
-
if (ref.store !== this.kind) {
|
|
106
|
-
throw new PayloadCodecError(
|
|
107
|
-
PayloadCodecErrorCode.UNKNOWN_STORE_KIND,
|
|
108
|
-
`InMemoryBlobStore cannot read blobs from store '${ref.store}'.`,
|
|
109
|
-
{ expected: this.kind, received: ref.store },
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
assertNonEmptyOrgId(ref.orgId);
|
|
113
|
-
const found = this.blobs.get(makeKey(ref.orgId, ref.sha256));
|
|
114
|
-
if (!found) {
|
|
115
|
-
throw new PayloadCodecError(
|
|
116
|
-
PayloadCodecErrorCode.BLOB_GET_FAILED,
|
|
117
|
-
`InMemoryBlobStore: no blob with sha256=${ref.sha256} for orgId=${ref.orgId}`,
|
|
118
|
-
{ sha256: ref.sha256, orgId: ref.orgId },
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
const actualSha = sha256Hex(found.bytes);
|
|
122
|
-
if (actualSha !== ref.sha256) {
|
|
123
|
-
// Should be impossible — we keyed by sha256. Guard anyway because
|
|
124
|
-
// bytes-mutation bugs in callers would corrupt the Map.
|
|
125
|
-
throw new PayloadCodecError(
|
|
126
|
-
PayloadCodecErrorCode.SHA256_MISMATCH,
|
|
127
|
-
'InMemoryBlobStore: stored bytes no longer match the sha256 they were keyed by.',
|
|
128
|
-
{ expected: ref.sha256, actual: actualSha },
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
return found.bytes;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async delete(
|
|
135
|
-
ref: Pick<BlobRef, 'sha256' | 'orgId'>,
|
|
136
|
-
): Promise<{ deleted: boolean }> {
|
|
137
|
-
assertNonEmptyOrgId(ref.orgId);
|
|
138
|
-
const deleted = this.blobs.delete(makeKey(ref.orgId, ref.sha256));
|
|
139
|
-
return { deleted };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Test helper — number of stored blobs. */
|
|
143
|
-
size(): number {
|
|
144
|
-
return this.blobs.size;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Test helper — wipe the store. */
|
|
148
|
-
clear(): void {
|
|
149
|
-
this.blobs.clear();
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Compute lowercase-hex sha256 of a byte buffer. */
|
|
154
|
-
export function sha256Hex(bytes: Uint8Array): string {
|
|
155
|
-
return createHash('sha256').update(bytes).digest('hex');
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// NUL is the separator because it cannot occur in an org id or a hex digest, so
|
|
159
|
-
// no two distinct blobs can collide on a concatenation. It MUST stay spelled as
|
|
160
|
-
// the unicode ESCAPE, never as a raw NUL byte in the source: one raw byte makes
|
|
161
|
-
// every binary-skipping search tool (ugrep, ripgrep, `grep -I`) classify the
|
|
162
|
-
// WHOLE file as binary and skip it silently, and a file no search can read is a
|
|
163
|
-
// file no audit, no review and no boundary check covers. Enforced by the
|
|
164
|
-
// aggregator boundary gate `grep-readable-sources`.
|
|
165
|
-
function makeKey(orgId: string, sha256: string): string {
|
|
166
|
-
return `${orgId}\u0000${sha256}`;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function assertNonEmptyOrgId(orgId: string): void {
|
|
170
|
-
if (typeof orgId !== 'string' || orgId.length === 0) {
|
|
171
|
-
throw new PayloadCodecError(
|
|
172
|
-
PayloadCodecErrorCode.ORG_ID_REQUIRED,
|
|
173
|
-
'BlobStore operations require a non-empty orgId.',
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Per-call codec context. Wrapping a Temporal client invocation in
|
|
5
|
-
* {@link runWithCodecOrgId} lets the {@link SpillPayloadCodec}'s
|
|
6
|
-
* `orgIdProvider` resolve the right tenant for blob writes:
|
|
7
|
-
*
|
|
8
|
-
* await runWithCodecOrgId(run.orgId, () =>
|
|
9
|
-
* client.workflow.start('rootRunWorkflow', { args, ... }),
|
|
10
|
-
* );
|
|
11
|
-
*
|
|
12
|
-
* The engine establishes this around every `client.workflow.*` call so the
|
|
13
|
-
* codec can stamp `X-Xema-Org-Id` on its blob-store traffic. The decode side
|
|
14
|
-
* does NOT depend on this context — orgId rides inside the spill envelope.
|
|
15
|
-
*
|
|
16
|
-
* Workers don't typically use this ALS for codec calls (the worker reads
|
|
17
|
-
* orgId from the active activity-auth-context or a fallback env var) but
|
|
18
|
-
* the helpers are exported for symmetry.
|
|
19
|
-
*/
|
|
20
|
-
export interface CodecContext {
|
|
21
|
-
readonly orgId: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const storage = new AsyncLocalStorage<CodecContext>();
|
|
25
|
-
|
|
26
|
-
export function getCodecOrgId(): string | undefined {
|
|
27
|
-
return storage.getStore()?.orgId;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function runWithCodecOrgId<T>(
|
|
31
|
-
orgId: string,
|
|
32
|
-
fn: () => Promise<T>,
|
|
33
|
-
): Promise<T> {
|
|
34
|
-
if (typeof orgId !== 'string' || orgId.length === 0) {
|
|
35
|
-
throw new Error('runWithCodecOrgId: orgId must be a non-empty string.');
|
|
36
|
-
}
|
|
37
|
-
return storage.run({ orgId }, fn);
|
|
38
|
-
}
|