@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.
Files changed (62) hide show
  1. package/package.json +2 -3
  2. package/src/deliverable-spec/index.ts +0 -19
  3. package/src/deliverable-spec/lib/schema.ts +0 -270
  4. package/src/deliverable-spec/lib/types.ts +0 -26
  5. package/src/payload-codec/index.ts +0 -44
  6. package/src/payload-codec/lib/blob-store.ts +0 -176
  7. package/src/payload-codec/lib/codec-context.ts +0 -38
  8. package/src/payload-codec/lib/codec.ts +0 -605
  9. package/src/payload-codec/lib/enums.ts +0 -58
  10. package/src/payload-codec/lib/errors.ts +0 -54
  11. package/src/payload-codec/lib/http-blob-store.ts +0 -267
  12. package/src/payload-codec/lib/lru-cache.ts +0 -81
  13. package/src/payload-codec/lib/payload.ts +0 -26
  14. package/src/payload-codec/temporal/index.ts +0 -36
  15. package/src/workflow/index.ts +0 -108
  16. package/src/workflow/lib/action-input-validator.ts +0 -160
  17. package/src/workflow/lib/compiler/action-shape.ts +0 -71
  18. package/src/workflow/lib/compiler/canonical-json.ts +0 -66
  19. package/src/workflow/lib/compiler/compile.ts +0 -1742
  20. package/src/workflow/lib/compiler/concurrency.ts +0 -223
  21. package/src/workflow/lib/compiler/dag.ts +0 -108
  22. package/src/workflow/lib/compiler/gate-defaults.ts +0 -153
  23. package/src/workflow/lib/compiler/index.ts +0 -11
  24. package/src/workflow/lib/compiler/inputs.ts +0 -254
  25. package/src/workflow/lib/compiler/installation-resource-validator.ts +0 -114
  26. package/src/workflow/lib/compiler/manifest-source.ts +0 -71
  27. package/src/workflow/lib/compiler/matrix.ts +0 -135
  28. package/src/workflow/lib/compiler/mount-plan.ts +0 -190
  29. package/src/workflow/lib/compiler/payload-reach-in.ts +0 -497
  30. package/src/workflow/lib/compiler/permissions.ts +0 -64
  31. package/src/workflow/lib/compiler/retry-timeout.ts +0 -105
  32. package/src/workflow/lib/compiler/review-step.ts +0 -548
  33. package/src/workflow/lib/compiler/types.ts +0 -172
  34. package/src/workflow/lib/compiler/variable-requirements.ts +0 -208
  35. package/src/workflow/lib/deliverable-spec-introspection-error.ts +0 -63
  36. package/src/workflow/lib/deliverable-spec-keys.ts +0 -147
  37. package/src/workflow/lib/deliverable-spec-source-scan.ts +0 -280
  38. package/src/workflow/lib/dispatch-inputs/index.ts +0 -160
  39. package/src/workflow/lib/dispatch-inputs/to-json-schema.ts +0 -60
  40. package/src/workflow/lib/duration.ts +0 -43
  41. package/src/workflow/lib/errors.ts +0 -37
  42. package/src/workflow/lib/expression/ast.ts +0 -108
  43. package/src/workflow/lib/expression/context.ts +0 -148
  44. package/src/workflow/lib/expression/evaluator.ts +0 -492
  45. package/src/workflow/lib/expression/index.ts +0 -28
  46. package/src/workflow/lib/expression/interpolation.ts +0 -84
  47. package/src/workflow/lib/expression/parser.ts +0 -264
  48. package/src/workflow/lib/expression/template.ts +0 -117
  49. package/src/workflow/lib/expression/tokenizer.ts +0 -200
  50. package/src/workflow/lib/expression/tokens.ts +0 -30
  51. package/src/workflow/lib/expression/walk-artifact-refs.ts +0 -232
  52. package/src/workflow/lib/installation-resource-kind.ts +0 -107
  53. package/src/workflow/lib/schemas-loader.ts +0 -64
  54. package/src/workflow/lib/serializer.ts +0 -30
  55. package/src/workflow/lib/types.ts +0 -417
  56. package/src/workflow/lib/validate.ts +0 -199
  57. package/src/workspace-manifest/index.ts +0 -27
  58. package/src/workspace-manifest/lib/compile.ts +0 -619
  59. package/src/workspace-manifest/lib/interpolate.ts +0 -166
  60. package/src/workspace-manifest/lib/resolve-extends.ts +0 -260
  61. package/src/workspace-manifest/lib/schema.ts +0 -692
  62. package/src/workspace-manifest/lib/types.ts +0 -446
@@ -1,172 +0,0 @@
1
- import type {
2
- ActionExecutionKind,
3
- Briefcase,
4
- TaskQueueName,
5
- TriggerPayload,
6
- WalletContents,
7
- WorkflowRef,
8
- } from '@xemahq/kernel-contracts/workflow';
9
- import type { ActionManifest, WorkflowDocument } from '../types';
10
-
11
- /**
12
- * Everything the compiler needs beyond the workflow document itself. The
13
- * engine gathers these inputs before calling `compileWorkflow(...)`:
14
- *
15
- * - `workflow` — validated WorkflowDocument (pass through
16
- * `validateWorkflowDocument` first).
17
- * - `workflowRef` — slug/revision/content-contract identity, resolved from the DB.
18
- * - `trigger` — the payload that fired this run.
19
- * - `resolvedRefs` — map of `uses:` strings to resolved refs, populated
20
- * by looking up each job's `uses:` in the action-manifest registry
21
- * (for `xema/...`) or the reusable-workflow registry (for
22
- * `xema://workflow/...`).
23
- * - `workflowDefinitionContentHash` — content hash of the immutable source revision the
24
- * compiler is consuming. Emitted into CompiledRun for audit.
25
- */
26
- export interface CompileInput {
27
- readonly workflow: WorkflowDocument;
28
- readonly workflowRef: WorkflowRef;
29
- readonly trigger: TriggerPayload;
30
- readonly resolvedRefs: Readonly<Record<string, ResolvedRef>>;
31
- /**
32
- * Map of authored literal agent ref → immutable Agent revision metadata.
33
- * The engine resolves each exact ref through llm-registry-api before
34
- * compilation. Literal refs are therefore compile-time selectors only:
35
- * CompiledRun stores the immutable revision id + content hash.
36
- * Expression-shaped refs (e.g. `${{ inputs.agentSlug }}`) are skipped
37
- * — they resolve at dispatch and the activity's `AGENT_NOT_REGISTERED`
38
- * preflight catches them at runtime.
39
- *
40
- * Pass an empty map (or omit) to opt out of agent validation (e.g.
41
- * for the `preview-raw` endpoint when the engine cannot reach the
42
- * registry, or for tests that don't exercise the registry).
43
- */
44
- readonly resolvedAgents?: Readonly<Record<string, ResolvedAgentMeta>>;
45
- /**
46
- * Map of literal deliverable-spec refs (used in `with.deliverableSpecRef`
47
- * and the `produces:` array) to the spec metadata the compiler needs
48
- * for validation. The engine pre-fetches this from
49
- * deliverable-specs-api. Same literal-vs-expression rule as
50
- * `resolvedAgents`. Same opt-out semantics: empty / omitted = skip.
51
- */
52
- readonly resolvedDeliverableSpecs?: Readonly<
53
- Record<string, ResolvedDeliverableSpec>
54
- >;
55
- readonly workflowDefinitionContentHash: string;
56
- /**
57
- * Contents of every wallet referenced under `requires.wallets`, plus
58
- * any additional wallets a dispatch caller asked for. The compiler
59
- * unions the entries to derive `CompiledRun.requiredVariables` and to
60
- * validate `${{ vars.X }}` / `${{ secrets.X }}` references. When
61
- * omitted, wallet-derived validation is skipped — used by
62
- * `preview-raw` during authoring where the engine can't reach the
63
- * variable store.
64
- */
65
- readonly resolvedWallets?: Readonly<Record<string, WalletContents>>;
66
- /**
67
- * Installation scope of this dispatch — present when the workflow is
68
- * dispatched from a biome installation (webhook trigger gated on
69
- * the installation, manual dispatch from the installation's UX, a
70
- * scheduled trigger tied to the installation). Absent for system /
71
- * org-wide dispatches that don't belong to any installation.
72
- *
73
- * The compiler's installation-resource validator uses this scope to
74
- * resolve `x-installation-resource: { kind: ... }` references in
75
- * action input schemas against the installation's bound resources
76
- * (wallets / repos / channels / …). When `installationScope` is
77
- * undefined, the validator skips installation-bound fields — system
78
- * workflows can still reference wallet ids, but compile-time
79
- * binding-set validation only happens for biome-installed runs.
80
- */
81
- readonly installationScope?: InstallationCompileScope;
82
- /**
83
- * When true, the compiler substitutes type-appropriate sentinel values
84
- * for missing required inputs instead of throwing. Intended only for
85
- * the `preview-raw` endpoint (Monaco editor live feedback) where the
86
- * caller deliberately omits real input values during YAML authoring.
87
- * Never set this on real run compilation paths.
88
- */
89
- readonly previewMode?: boolean;
90
- /**
91
- * Run-scoped Briefcase the dispatch caller attached to this run.
92
- * Synthesized server-side from the StartRun request body (uploads /
93
- * references / vars / mcpTools) and threaded into the CompiledRun so
94
- * the run's snapshot captures the dispatch payload alongside its
95
- * inputs and trigger. Absent for runs without dispatch-attached
96
- * context (compiler treats absent and `emptyBriefcase()` identically
97
- * downstream).
98
- */
99
- readonly briefcase?: Briefcase;
100
- }
101
-
102
- /**
103
- * Installation context the engine pre-fetches at dispatch time. Each
104
- * `boundResources` entry lists the resource ids the installation is
105
- * bound to for a single `InstallationResourceKind` — the compiler
106
- * checks every literal value in a `x-installation-resource` field
107
- * against the matching kind's set and rejects unbound values.
108
- *
109
- * The engine populates this from biome-host-api right before calling
110
- * the compiler. Passing an empty array for a kind means "no resources
111
- * bound" — every literal value for that kind fails validation.
112
- */
113
- export interface InstallationCompileScope {
114
- readonly installationId: string;
115
- readonly orgId: string;
116
- readonly projectId: string;
117
- readonly biomeId: string;
118
- readonly boundResources: Readonly<Record<string, readonly string[]>>;
119
- }
120
-
121
- /**
122
- * Minimal agent metadata the compiler keeps for each resolved agent,
123
- * keyed by bare slug (the slug segment of a `with.agentRef`). The engine
124
- * fetches the full row from llm-registry-api but the compiler only needs
125
- * the existence proof and the snapshot hash (so a future `CompiledRun`
126
- * can carry it for cache-invalidation matching). Extend with more fields
127
- * only when the compiler actually consumes them.
128
- */
129
- export interface ResolvedAgentMeta {
130
- readonly slug: string;
131
- readonly agentRevisionId: string;
132
- readonly agentContentHash: string;
133
- }
134
-
135
- /**
136
- * Minimal deliverable-spec metadata the compiler keeps for each literal
137
- * `deliverableSpecRef`. Same minimality rule as `ResolvedAgentMeta`.
138
- *
139
- * Optional `topLevelKeys` enables compile-time field validation for
140
- * expressions of shape
141
- * `${{ job.outputs.deliverable.content.value.<field> }}` — when
142
- * populated, the DSL asserts `<field>` is one of these. The engine
143
- * extracts the keys from the spec's `zodSchemaSource` (Zod) or
144
- * `content`'s `properties` (JSON Schema) at registry-fetch time. Specs
145
- * whose kind has no known shape (e.g. CUSTOM, ENDPOINT_FETCH) leave it
146
- * undefined and the validator skips them.
147
- */
148
- export interface ResolvedDeliverableSpec {
149
- readonly ref: string;
150
- readonly slug: string;
151
- readonly snapshotHash: string;
152
- readonly topLevelKeys?: readonly string[];
153
- }
154
-
155
- /**
156
- * Resolved reference for a single `uses:` value. Emitted by the engine's
157
- * ref-resolution layer before compilation; the compiler trusts it blindly.
158
- *
159
- * For action refs, `actionManifest` is the full manifest (the compiler
160
- * needs retry/timeout/allowedMounts). For reusable-workflow refs,
161
- * `actionManifest` is null — reusable workflows have their own compilation
162
- * pass, so this outer compilation only records the pointer.
163
- */
164
- export interface ResolvedRef {
165
- readonly id: string;
166
- readonly version: string;
167
- readonly manifestSha256: string;
168
- readonly executionKind: ActionExecutionKind;
169
- readonly taskQueue: TaskQueueName;
170
- readonly isReusableWorkflow: boolean;
171
- readonly actionManifest: ActionManifest | null;
172
- }
@@ -1,208 +0,0 @@
1
- import {
2
- WorkflowErrorCode,
3
- type WalletContents,
4
- type WorkflowVariableRequirement,
5
- } from '@xemahq/kernel-contracts/workflow';
6
- import { WorkflowDslError } from '../errors';
7
- import {
8
- compileExpression,
9
- ExpressionNodeKind,
10
- extractInterpolations,
11
- type ExpressionNode,
12
- } from '../expression';
13
- import { ExpressionRoot } from '../expression/ast';
14
- import { stripInterpolation } from '../expression/interpolation';
15
- import type { WorkflowDocument } from '../types';
16
-
17
- /**
18
- * Compile-time handling of `requires.wallets` and the `vars.*` /
19
- * `secrets.*` expression namespaces.
20
- *
21
- * The compiler trusts the engine to pre-fetch the contents of every
22
- * wallet listed in `workflow.requires.wallets` (plus any additional
23
- * wallets the dispatch caller asked for) and pass them as the
24
- * `resolvedWallets` map. Two responsibilities here, both fail-fast:
25
- *
26
- * 1. `WORKFLOW_WALLET_NOT_FOUND` — every wallet name declared under
27
- * `requires.wallets` must have an entry in `resolvedWallets`.
28
- * Missing entry = the engine couldn't find the wallet for the
29
- * workflow's project/org scope.
30
- *
31
- * 2. `DSL_EXPRESSION_INVALID` — every `${{ vars.X }}` / `${{ secrets.X }}`
32
- * member access must resolve to a key in the union of the resolved
33
- * wallets' contents (modulo the YAML-static `vars:` block which can
34
- * also satisfy a `vars.X` lookup). Catches typos / stale references
35
- * at compile time instead of a runtime evaluator failure.
36
- *
37
- * The function returns the canonical flat list emitted into
38
- * `CompiledRun.requiredVariables` — the union of every wallet's
39
- * `(key, isSecret)` entries with duplicates collapsed (last writer wins
40
- * for `isSecret` if two wallets disagree; the engine surfaces the
41
- * conflict at fetch time with a different code).
42
- */
43
- export function resolveWalletRequirements(
44
- doc: WorkflowDocument,
45
- resolvedWallets: Readonly<Record<string, WalletContents>> | undefined,
46
- ): readonly WorkflowVariableRequirement[] {
47
- const declaredWallets = doc.requires?.wallets ?? [];
48
-
49
- if (resolvedWallets === undefined) {
50
- // Engine opted out (preview-raw / tests). Skip both checks and
51
- // emit an empty requirements list — `${{ vars.X }}` / `${{ secrets.X }}`
52
- // references that don't satisfy the YAML-static fallback will still
53
- // surface at runtime, which is the documented contract for opt-out.
54
- return Object.freeze([]);
55
- }
56
-
57
- const missingWallets: string[] = [];
58
- for (const name of declaredWallets) {
59
- if (!Object.hasOwn(resolvedWallets, name)) {
60
- missingWallets.push(name);
61
- }
62
- }
63
- if (missingWallets.length > 0) {
64
- throw new WorkflowDslError(
65
- WorkflowErrorCode.WORKFLOW_WALLET_NOT_FOUND,
66
- `Wallet(s) declared in requires.wallets are not available for this project/org scope: ${missingWallets.join(', ')}`,
67
- { missing: missingWallets },
68
- );
69
- }
70
-
71
- const merged = new Map<string, WorkflowVariableRequirement>();
72
- for (const name of declaredWallets) {
73
- const wallet = resolvedWallets[name];
74
- if (!wallet) {
75
- // `declaredWallets` is derived from the same `resolvedWallets`
76
- // map upstream, so a missing entry here would be an internal
77
- // invariant violation — surface it instead of silently producing
78
- // an empty merge.
79
- throw new Error(
80
- `Internal: declaredWallets references unresolved wallet '${name}'.`,
81
- );
82
- }
83
- for (const entry of wallet.entries) {
84
- merged.set(entry.key, {
85
- name: entry.key,
86
- secret: entry.isSecret,
87
- optional: false,
88
- });
89
- }
90
- }
91
- return Object.freeze([...merged.values()]);
92
- }
93
-
94
- /**
95
- * Walk every `${{ ... }}` expression body in the workflow and, for each
96
- * `vars.<NAME>` / `secrets.<NAME>` member access, verify the referenced
97
- * name is satisfiable from the resolved wallets (or, for `vars.*` only,
98
- * from the YAML-static `vars:` block).
99
- */
100
- export function validateVariableReferences(
101
- doc: WorkflowDocument,
102
- requiredVariables: readonly WorkflowVariableRequirement[],
103
- resolvedWallets: Readonly<Record<string, WalletContents>> | undefined,
104
- ): void {
105
- if (resolvedWallets === undefined) {
106
- return;
107
- }
108
- const staticVarNames = new Set(Object.keys(doc.vars ?? {}));
109
- const allowedVars = new Set<string>(staticVarNames);
110
- const allowedSecrets = new Set<string>();
111
- for (const r of requiredVariables) {
112
- if (r.secret) {
113
- allowedSecrets.add(r.name);
114
- } else {
115
- allowedVars.add(r.name);
116
- }
117
- }
118
-
119
- for (const { source, pathLabel } of collectAllExpressions(doc)) {
120
- const ast = compileExpression(source);
121
- walkVariableAccesses(ast, (root, name) => {
122
- if (root === ExpressionRoot.VARS && !allowedVars.has(name)) {
123
- throw new WorkflowDslError(
124
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
125
- `Expression at ${pathLabel} reads vars.${name}, but '${name}' is not provided by workflow.vars or any wallet in requires.wallets.`,
126
- { pathLabel, root, name, source },
127
- );
128
- }
129
- if (root === ExpressionRoot.SECRETS && !allowedSecrets.has(name)) {
130
- throw new WorkflowDslError(
131
- WorkflowErrorCode.DSL_EXPRESSION_INVALID,
132
- `Expression at ${pathLabel} reads secrets.${name}, but '${name}' is not provided by any wallet in requires.wallets (or the wallet entry is not marked secret).`,
133
- { pathLabel, root, name, source },
134
- );
135
- }
136
- });
137
- }
138
- }
139
-
140
- interface CollectedExpression {
141
- readonly source: string;
142
- readonly pathLabel: string;
143
- }
144
-
145
- function collectAllExpressions(doc: WorkflowDocument): CollectedExpression[] {
146
- const out: CollectedExpression[] = [];
147
- for (const [jobKey, job] of Object.entries(doc.jobs)) {
148
- for (const expr of extractInterpolations(job.with, ['jobs', jobKey, 'with'])) {
149
- out.push({ source: expr.source, pathLabel: expr.path.join('.') });
150
- }
151
- for (const expr of extractInterpolations(job.outputs, ['jobs', jobKey, 'outputs'])) {
152
- out.push({ source: expr.source, pathLabel: expr.path.join('.') });
153
- }
154
- if (typeof job.if === 'string' && job.if.length > 0) {
155
- out.push({ source: stripInterpolation(job.if), pathLabel: `jobs.${jobKey}.if` });
156
- }
157
- }
158
- return out;
159
- }
160
-
161
- /**
162
- * Visits every `<root>.<name>` member access in the AST and invokes
163
- * `onAccess(root, name)`. Other access shapes are ignored — the caller
164
- * validates only the first hop, which is sufficient because both
165
- * `vars.*` and `secrets.*` are string-keyed flat maps.
166
- */
167
- function walkVariableAccesses(
168
- node: ExpressionNode,
169
- onAccess: (root: ExpressionRoot, name: string) => void,
170
- ): void {
171
- switch (node.kind) {
172
- case ExpressionNodeKind.MEMBER: {
173
- const target = node.target;
174
- if (
175
- target.kind === ExpressionNodeKind.IDENTIFIER &&
176
- (target.name === ExpressionRoot.VARS ||
177
- target.name === ExpressionRoot.SECRETS)
178
- ) {
179
- onAccess(target.name as ExpressionRoot, node.property);
180
- return;
181
- }
182
- walkVariableAccesses(target, onAccess);
183
- return;
184
- }
185
- case ExpressionNodeKind.INDEX:
186
- walkVariableAccesses(node.target, onAccess);
187
- walkVariableAccesses(node.index, onAccess);
188
- return;
189
- case ExpressionNodeKind.CALL:
190
- for (const arg of node.args) {
191
- walkVariableAccesses(arg, onAccess);
192
- }
193
- return;
194
- case ExpressionNodeKind.UNARY_NOT:
195
- walkVariableAccesses(node.operand, onAccess);
196
- return;
197
- case ExpressionNodeKind.BINARY_EQ:
198
- case ExpressionNodeKind.BINARY_NEQ:
199
- case ExpressionNodeKind.BINARY_AND:
200
- case ExpressionNodeKind.BINARY_OR:
201
- walkVariableAccesses(node.left, onAccess);
202
- walkVariableAccesses(node.right, onAccess);
203
- return;
204
- case ExpressionNodeKind.IDENTIFIER:
205
- case ExpressionNodeKind.LITERAL:
206
- return;
207
- }
208
- }
@@ -1,63 +0,0 @@
1
- /**
2
- * Typed failure raised when a deliverable spec's Zod source cannot be
3
- * introspected UNAMBIGUOUSLY. Distinct from the `null` return of the
4
- * extractors, which means "this source has no introspectable shape at
5
- * all" — a spec kind or body we simply cannot read. This error means the
6
- * opposite: the source DOES declare a shape, but the spec has not said
7
- * WHICH one is the contract, so any answer would be a guess.
8
- *
9
- * Callers MUST branch on `.reason` — free-form message matching is
10
- * forbidden so handling stays exhaustive (same convention as
11
- * `WorkflowDslError.code`).
12
- */
13
-
14
- /** Closed set of introspection failure reasons. */
15
- export enum DeliverableSpecIntrospectionFailure {
16
- /**
17
- * The source declares two or more top-level `z.object({ … })` schemas
18
- * and the spec did not name which one is the contract. Resolving this
19
- * by position ("the first one") is guesswork: a schema hoisted above
20
- * the contract purely so the contract can reference it would silently
21
- * become the contract.
22
- */
23
- AmbiguousSchema = 'AMBIGUOUS_SCHEMA',
24
- /**
25
- * The spec named a contract export (`zodSchemaExport`) that the source
26
- * does not declare as a top-level `z.object({ … })` — a typo, a rename,
27
- * or an unbalanced literal the scanner could not close.
28
- */
29
- ExportNotFound = 'EXPORT_NOT_FOUND',
30
- }
31
-
32
- export class DeliverableSpecIntrospectionError extends Error {
33
- readonly reason: DeliverableSpecIntrospectionFailure;
34
- readonly details: Readonly<Record<string, unknown>>;
35
-
36
- constructor(
37
- reason: DeliverableSpecIntrospectionFailure,
38
- message: string,
39
- details: Readonly<Record<string, unknown>> = {},
40
- ) {
41
- super(message);
42
- this.name = 'DeliverableSpecIntrospectionError';
43
- this.reason = reason;
44
- this.details = details;
45
- Object.setPrototypeOf(this, new.target.prototype);
46
- }
47
-
48
- toJSON(): Readonly<Record<string, unknown>> {
49
- return {
50
- name: this.name,
51
- reason: this.reason,
52
- message: this.message,
53
- details: this.details,
54
- };
55
- }
56
- }
57
-
58
- /** Narrowing helper. */
59
- export function isDeliverableSpecIntrospectionError(
60
- err: unknown,
61
- ): err is DeliverableSpecIntrospectionError {
62
- return err instanceof DeliverableSpecIntrospectionError;
63
- }
@@ -1,147 +0,0 @@
1
- /**
2
- * Static introspection of deliverable-spec content to recover the
3
- * top-level keys an agent's output object MUST expose. The compiler uses
4
- * these keys to validate workflow expressions of shape
5
- * `${{ job.outputs.deliverable.content.value.<field> }}` against the
6
- * spec the producing job declared via `with.deliverableSpecRef` —
7
- * catching "the YAML reads `.changeUnits` but the spec has
8
- * `.handoffPackage`" at compile time, not at runtime when projection
9
- * blows up. The same keys are the ONLY content gate the harvester has
10
- * for a `ZOD_SCHEMA` spec (nothing anywhere evaluates the Zod schema),
11
- * so a wrong key set silently rejects correct agent output.
12
- *
13
- * Two extractors:
14
- * - `extractZodTopLevelObjectKeys`: reads the field names of the
15
- * `z.object({ … })` literal a spec designates as its contract, out of
16
- * TypeScript Zod source. Same scanner the deliverable-specs-api
17
- * ZodSchemaHandler uses for harvest-time validation; single source of
18
- * truth lives here so the DSL compiler and the harvester agree.
19
- * - `extractJsonSchemaTopLevelKeys`: pulls keys from the `properties`
20
- * object of a JSON Schema document.
21
- *
22
- * Both return `null` when a source has NO introspectable shape, so
23
- * callers can skip the field check (preview / specs without introspectable
24
- * shape). `null` never means "we guessed" — an unresolvable-but-present
25
- * shape raises `DeliverableSpecIntrospectionError` instead.
26
- */
27
-
28
- import {
29
- DeliverableSpecIntrospectionError,
30
- DeliverableSpecIntrospectionFailure,
31
- } from './deliverable-spec-introspection-error';
32
- import { scanTopLevelZodObjectDeclarations } from './deliverable-spec-source-scan';
33
-
34
- import type { TopLevelZodObjectDeclaration } from './deliverable-spec-source-scan';
35
-
36
- /**
37
- * Return the top-level field identifiers of the `z.object({ … })` literal
38
- * that IS this spec's contract.
39
- *
40
- * Which literal that is comes from the spec, not from source order:
41
- *
42
- * - `exportName` given → the literal bound to that name. Absent from
43
- * the source ⇒ `EXPORT_NOT_FOUND`, never a fallback to another
44
- * schema. A spec that names a contract it does not have is broken,
45
- * and guessing would reintroduce exactly the bug this parameter
46
- * exists to remove.
47
- * - `exportName` omitted, source declares exactly ONE top-level
48
- * literal → that one. There is nothing to choose between, so the
49
- * spec is not asked to restate it.
50
- * - `exportName` omitted, source declares TWO OR MORE →
51
- * `AMBIGUOUS_SCHEMA`. This is the case the predecessor resolved by
52
- * taking the first literal in the file, which silently picked an
53
- * element/helper schema hoisted above the contract that references
54
- * it. There is no correct positional answer; the spec must say.
55
- * - source declares NONE → `null` (no introspectable shape).
56
- *
57
- * Deliberately a scanner, not a Zod evaluation: this runs in the DSL
58
- * compiler, a CI gate, and a request-scoped API handler, none of which
59
- * may transpile or execute spec-authored TypeScript.
60
- *
61
- * @throws {DeliverableSpecIntrospectionError} on a named export that is
62
- * absent, or on an unnamed contract in a multi-schema source.
63
- */
64
- export function extractZodTopLevelObjectKeys(
65
- source: string,
66
- exportName?: string | null,
67
- ): readonly string[] | null {
68
- if (!source || source.trim().length === 0) return null;
69
- const declarations = scanTopLevelZodObjectDeclarations(source);
70
- const named = exportName?.trim() ?? '';
71
-
72
- if (named.length > 0) {
73
- const match = declarations.find((d) => d.exportName === named);
74
- if (!match) {
75
- throw new DeliverableSpecIntrospectionError(
76
- DeliverableSpecIntrospectionFailure.ExportNotFound,
77
- `deliverable spec names '${named}' as its Zod contract, but the source declares no top-level \`z.object({ … })\` bound to that name. ` +
78
- `${describeDeclarations(declarations)} Fix the spec's \`zodSchemaExport\`, or check the literal's braces are balanced.`,
79
- { exportName: named, available: availableNames(declarations) },
80
- );
81
- }
82
- return match.keys;
83
- }
84
-
85
- if (declarations.length === 0) return null;
86
- if (declarations.length === 1) return declarations[0]!.keys;
87
-
88
- throw new DeliverableSpecIntrospectionError(
89
- DeliverableSpecIntrospectionFailure.AmbiguousSchema,
90
- `deliverable spec source declares ${declarations.length} top-level \`z.object({ … })\` schemas and the spec does not say which one is the contract. ` +
91
- `${describeDeclarations(declarations)} Set \`zodSchemaExport\` on the spec to the contract's export name.`,
92
- { available: availableNames(declarations) },
93
- );
94
- }
95
-
96
- /**
97
- * Pull the top-level field names from a JSON Schema document. Accepts
98
- * either a parsed object or a raw JSON string. Honors `properties` —
99
- * the conventional shape — and ignores `patternProperties` /
100
- * `additionalProperties` / Draft 2020 `unevaluatedProperties` because
101
- * those don't enumerate concrete keys an author can reference by name.
102
- *
103
- * Returns `null` when the schema is unparseable or has no `properties`
104
- * map.
105
- */
106
- export function extractJsonSchemaTopLevelKeys(
107
- source: string | object,
108
- ): readonly string[] | null {
109
- let parsed: unknown;
110
- if (typeof source === 'string') {
111
- if (source.trim().length === 0) return null;
112
- try {
113
- parsed = JSON.parse(source);
114
- } catch {
115
- return null;
116
- }
117
- } else {
118
- parsed = source;
119
- }
120
- if (!isPlainObject(parsed)) return null;
121
- const properties = (parsed as Record<string, unknown>)['properties'];
122
- if (!isPlainObject(properties)) return null;
123
- const keys = Object.keys(properties);
124
- return Object.freeze(keys);
125
- }
126
-
127
- function isPlainObject(v: unknown): v is Record<string, unknown> {
128
- return v !== null && typeof v === 'object' && !Array.isArray(v);
129
- }
130
-
131
- function availableNames(
132
- declarations: readonly TopLevelZodObjectDeclaration[],
133
- ): readonly string[] {
134
- return declarations
135
- .map((d) => d.exportName)
136
- .filter((n): n is string => n !== null);
137
- }
138
-
139
- function describeDeclarations(
140
- declarations: readonly TopLevelZodObjectDeclaration[],
141
- ): string {
142
- const names = availableNames(declarations);
143
- if (names.length === 0) {
144
- return 'The source declares no NAMED top-level schema.';
145
- }
146
- return `Top-level schemas in this source: ${names.join(', ')}.`;
147
- }