@xemahq/dsl 0.6.1 → 0.8.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.
Files changed (58) hide show
  1. package/dist/deliverable-spec/lib/schema.d.ts +2 -0
  2. package/dist/deliverable-spec/lib/schema.d.ts.map +1 -1
  3. package/dist/deliverable-spec/lib/schema.js +10 -1
  4. package/dist/deliverable-spec/lib/schema.js.map +1 -1
  5. package/dist/schema/reusable-workflow.schema.json +9 -1
  6. package/dist/schema/workflow.schema.json +39 -13
  7. package/dist/workflow/index.d.ts +3 -0
  8. package/dist/workflow/index.d.ts.map +1 -1
  9. package/dist/workflow/index.js +7 -1
  10. package/dist/workflow/index.js.map +1 -1
  11. package/dist/workflow/lib/compiler/compile.d.ts.map +1 -1
  12. package/dist/workflow/lib/compiler/compile.js +105 -20
  13. package/dist/workflow/lib/compiler/compile.js.map +1 -1
  14. package/dist/workflow/lib/compiler/concurrency.js +1 -1
  15. package/dist/workflow/lib/compiler/concurrency.js.map +1 -1
  16. package/dist/workflow/lib/compiler/manifest-source.d.ts +2 -1
  17. package/dist/workflow/lib/compiler/manifest-source.d.ts.map +1 -1
  18. package/dist/workflow/lib/compiler/manifest-source.js +13 -4
  19. package/dist/workflow/lib/compiler/manifest-source.js.map +1 -1
  20. package/dist/workflow/lib/compiler/review-step.d.ts +2 -1
  21. package/dist/workflow/lib/compiler/review-step.d.ts.map +1 -1
  22. package/dist/workflow/lib/compiler/review-step.js +13 -1
  23. package/dist/workflow/lib/compiler/review-step.js.map +1 -1
  24. package/dist/workflow/lib/compiler/types.d.ts +3 -2
  25. package/dist/workflow/lib/compiler/types.d.ts.map +1 -1
  26. package/dist/workflow/lib/deliverable-spec-introspection-error.d.ts +12 -0
  27. package/dist/workflow/lib/deliverable-spec-introspection-error.d.ts.map +1 -0
  28. package/dist/workflow/lib/deliverable-spec-introspection-error.js +33 -0
  29. package/dist/workflow/lib/deliverable-spec-introspection-error.js.map +1 -0
  30. package/dist/workflow/lib/deliverable-spec-keys.d.ts +1 -1
  31. package/dist/workflow/lib/deliverable-spec-keys.d.ts.map +1 -1
  32. package/dist/workflow/lib/deliverable-spec-keys.js +27 -52
  33. package/dist/workflow/lib/deliverable-spec-keys.js.map +1 -1
  34. package/dist/workflow/lib/deliverable-spec-source-scan.d.ts +6 -0
  35. package/dist/workflow/lib/deliverable-spec-source-scan.d.ts.map +1 -0
  36. package/dist/workflow/lib/deliverable-spec-source-scan.js +189 -0
  37. package/dist/workflow/lib/deliverable-spec-source-scan.js.map +1 -0
  38. package/dist/workflow/lib/expression/context.d.ts +1 -1
  39. package/dist/workflow/lib/expression/context.d.ts.map +1 -1
  40. package/dist/workflow/lib/expression/context.js +1 -1
  41. package/dist/workflow/lib/expression/context.js.map +1 -1
  42. package/dist/workflow/lib/types.d.ts +1 -1
  43. package/dist/workflow/lib/types.d.ts.map +1 -1
  44. package/package.json +2 -2
  45. package/schema/reusable-workflow.schema.json +9 -1
  46. package/schema/workflow.schema.json +39 -13
  47. package/src/deliverable-spec/lib/schema.ts +23 -0
  48. package/src/workflow/index.ts +7 -0
  49. package/src/workflow/lib/compiler/compile.ts +302 -149
  50. package/src/workflow/lib/compiler/concurrency.ts +1 -1
  51. package/src/workflow/lib/compiler/manifest-source.ts +23 -8
  52. package/src/workflow/lib/compiler/review-step.ts +33 -2
  53. package/src/workflow/lib/compiler/types.ts +9 -9
  54. package/src/workflow/lib/deliverable-spec-introspection-error.ts +63 -0
  55. package/src/workflow/lib/deliverable-spec-keys.ts +88 -50
  56. package/src/workflow/lib/deliverable-spec-source-scan.ts +280 -0
  57. package/src/workflow/lib/expression/context.ts +2 -2
  58. package/src/workflow/lib/types.ts +38 -8
@@ -40,7 +40,10 @@
40
40
  * worker's runtime contract — they intentionally diverge.
41
41
  */
42
42
 
43
- import { WorkflowErrorCode } from '@xemahq/kernel-contracts/workflow';
43
+ import {
44
+ WorkflowErrorCode,
45
+ type CompiledManifestSource,
46
+ } from '@xemahq/kernel-contracts/workflow';
44
47
 
45
48
  import { WorkflowDslError } from '../errors';
46
49
  import {
@@ -379,6 +382,7 @@ export function rewriteReviewStepWith(
379
382
  jobKey: string,
380
383
  payload: WorkflowJobDeclaration,
381
384
  workflow: WorkflowDocument,
385
+ manifestSourcesByJob: Readonly<Record<string, CompiledManifestSource | null>>,
382
386
  ): Readonly<Record<string, unknown>> {
383
387
  if (!isReviewActionRef(payload.uses)) {
384
388
  return payload.with ?? {};
@@ -423,6 +427,11 @@ export function rewriteReviewStepWith(
423
427
  step: shape.redraft.step,
424
428
  uses: producer.uses,
425
429
  with: producer.with ?? {},
430
+ manifestSource: requireRedraftManifestSource(
431
+ jobKey,
432
+ shape.redraft.step,
433
+ manifestSourcesByJob,
434
+ ),
426
435
  };
427
436
  const strategy = compileRedraftStrategyIR(producer.strategy);
428
437
  if (strategy !== null) {
@@ -437,6 +446,26 @@ export function rewriteReviewStepWith(
437
446
  return out;
438
447
  }
439
448
 
449
+ function requireRedraftManifestSource(
450
+ reviewJobKey: string,
451
+ producerJobKey: string,
452
+ manifestSourcesByJob: Readonly<Record<string, CompiledManifestSource | null>>,
453
+ ): CompiledManifestSource {
454
+ const source = manifestSourcesByJob[producerJobKey];
455
+ if (source === undefined || source === null) {
456
+ throw new WorkflowDslError(
457
+ WorkflowErrorCode.DSL_SEMANTIC_INVALID,
458
+ `Job '${reviewJobKey}' redraft producer '${producerJobKey}' has no compiled Agent revision source.`,
459
+ {
460
+ jobKey: reviewJobKey,
461
+ fieldPath: 'with.redraft.step',
462
+ referencedJob: producerJobKey,
463
+ },
464
+ );
465
+ }
466
+ return source;
467
+ }
468
+
440
469
  /**
441
470
  * Pre-resolve a producer step's authored matrix strategy into a
442
471
  * JSON-serializable IR descriptor the worker carries through the
@@ -513,5 +542,7 @@ function expandStaticMatrixAxes(
513
542
  }
514
543
  entries = next;
515
544
  }
516
- return entries.map(Object.freeze) as readonly Readonly<Record<string, unknown>>[];
545
+ return entries.map(Object.freeze) as readonly Readonly<
546
+ Record<string, unknown>
547
+ >[];
517
548
  }
@@ -14,13 +14,13 @@ import type { ActionManifest, WorkflowDocument } from '../types';
14
14
  *
15
15
  * - `workflow` — validated WorkflowDocument (pass through
16
16
  * `validateWorkflowDocument` first).
17
- * - `workflowRef` — slug/version/isSystem, resolved from the DB.
17
+ * - `workflowRef` — slug/revision/content-contract identity, resolved from the DB.
18
18
  * - `trigger` — the payload that fired this run.
19
19
  * - `resolvedRefs` — map of `uses:` strings to resolved refs, populated
20
20
  * by looking up each job's `uses:` in the action-manifest registry
21
21
  * (for `xema/...`) or the reusable-workflow registry (for
22
22
  * `xema://workflow/...`).
23
- * - `workflowDefinitionVersionSha256` — sha256 of the source YAML the
23
+ * - `workflowDefinitionContentHash` — content hash of the immutable source revision the
24
24
  * compiler is consuming. Emitted into CompiledRun for audit.
25
25
  */
26
26
  export interface CompileInput {
@@ -29,11 +29,10 @@ export interface CompileInput {
29
29
  readonly trigger: TriggerPayload;
30
30
  readonly resolvedRefs: Readonly<Record<string, ResolvedRef>>;
31
31
  /**
32
- * Map of agent slug → agent metadata the compiler needs for
33
- * validation, keyed by bare slug. The engine pre-fetches this from
34
- * llm-registry-api. The compiler validates that every literal
35
- * `with.agentRef` / `with.reviewers[].agentRef` value resolves —
36
- * parsing the bare slug off the `<slug>@<version>` ref before lookup.
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.
37
36
  * Expression-shaped refs (e.g. `${{ inputs.agentSlug }}`) are skipped
38
37
  * — they resolve at dispatch and the activity's `AGENT_NOT_REGISTERED`
39
38
  * preflight catches them at runtime.
@@ -53,7 +52,7 @@ export interface CompileInput {
53
52
  readonly resolvedDeliverableSpecs?: Readonly<
54
53
  Record<string, ResolvedDeliverableSpec>
55
54
  >;
56
- readonly workflowDefinitionVersionSha256: string;
55
+ readonly workflowDefinitionContentHash: string;
57
56
  /**
58
57
  * Contents of every wallet referenced under `requires.wallets`, plus
59
58
  * any additional wallets a dispatch caller asked for. The compiler
@@ -129,7 +128,8 @@ export interface InstallationCompileScope {
129
128
  */
130
129
  export interface ResolvedAgentMeta {
131
130
  readonly slug: string;
132
- readonly snapshotHash: string;
131
+ readonly agentRevisionId: string;
132
+ readonly agentContentHash: string;
133
133
  }
134
134
 
135
135
  /**
@@ -0,0 +1,63 @@
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
+ }
@@ -6,34 +6,91 @@
6
6
  * spec the producing job declared via `with.deliverableSpecRef` —
7
7
  * catching "the YAML reads `.changeUnits` but the spec has
8
8
  * `.handoffPackage`" at compile time, not at runtime when projection
9
- * blows up.
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.
10
12
  *
11
13
  * Two extractors:
12
- * - `extractZodTopLevelObjectKeys`: parses a `z.object({ ... })`
13
- * literal out of TypeScript Zod source. Same regex-based brace walker
14
- * the deliverable-specs-api ZodSchemaHandler uses for harvest-time
15
- * validation; single source of truth lives here so the DSL compiler
16
- * and the harvester agree.
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.
17
19
  * - `extractJsonSchemaTopLevelKeys`: pulls keys from the `properties`
18
20
  * object of a JSON Schema document.
19
21
  *
20
- * Both return `null` when introspection fails so callers can skip the
21
- * field check (preview / specs without introspectable shape).
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.
22
26
  */
23
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
+
24
36
  /**
25
- * Parse the FIRST `z.object({ ... })` literal in the source and return
26
- * the top-level field identifiers. Returns `null` when the literal is
27
- * absent or its braces are unbalanced — the spec layer fails fast on
28
- * such specs at content-validation time, but here in the compiler we
29
- * gracefully degrade so a malformed spec doesn't block unrelated
30
- * expression checks elsewhere.
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.
31
63
  */
32
- export function extractZodTopLevelObjectKeys(source: string): readonly string[] | null {
64
+ export function extractZodTopLevelObjectKeys(
65
+ source: string,
66
+ exportName?: string | null,
67
+ ): readonly string[] | null {
33
68
  if (!source || source.trim().length === 0) return null;
34
- const body = sliceTopLevelObjectBody(source);
35
- if (body === null) return null;
36
- return Object.freeze(parseObjectKeysAtTopLevel(body));
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
+ );
37
94
  }
38
95
 
39
96
  /**
@@ -71,39 +128,20 @@ function isPlainObject(v: unknown): v is Record<string, unknown> {
71
128
  return v !== null && typeof v === 'object' && !Array.isArray(v);
72
129
  }
73
130
 
74
- /** Substring between matching braces of the first `z.object({ ... })`, or null. */
75
- function sliceTopLevelObjectBody(source: string): string | null {
76
- const match = /z\.object\s*\(\s*\{/.exec(source);
77
- if (!match) return null;
78
- const start = match.index + match[0].length;
79
- let depth = 1;
80
- for (let i = start; i < source.length; i += 1) {
81
- const ch = source[i];
82
- if (ch === '{') depth += 1;
83
- else if (ch === '}' && --depth === 0) return source.slice(start, i);
84
- }
85
- return null;
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);
86
137
  }
87
138
 
88
- /** Walk the body once, recording identifiers at brace+paren depth 0. */
89
- function parseObjectKeysAtTopLevel(body: string): string[] {
90
- const keys: string[] = [];
91
- const depth = { brace: 0, paren: 0 };
92
- for (let i = 0; i < body.length; i += 1) {
93
- if (updateDepth(depth, body[i])) continue;
94
- if (depth.brace !== 0 || depth.paren !== 0) continue;
95
- if (i !== 0 && body[i] !== '\n' && body[i] !== ',') continue;
96
- const rest = body.slice(i === 0 ? 0 : i + 1);
97
- const keyMatch = /^\s*([A-Za-z_$][A-Za-z0-9_$]*)\s*:/.exec(rest);
98
- if (keyMatch?.[1]) keys.push(keyMatch[1]);
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.';
99
145
  }
100
- return keys;
101
- }
102
-
103
- function updateDepth(depth: { brace: number; paren: number }, ch: string | undefined): boolean {
104
- if (ch === '{') { depth.brace += 1; return true; }
105
- if (ch === '}') { depth.brace = Math.max(0, depth.brace - 1); return true; }
106
- if (ch === '(') { depth.paren += 1; return true; }
107
- if (ch === ')') { depth.paren = Math.max(0, depth.paren - 1); return true; }
108
- return false;
146
+ return `Top-level schemas in this source: ${names.join(', ')}.`;
109
147
  }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Character-level scanner over TypeScript Zod source, used to locate the
3
+ * `z.object({ … })` literals a deliverable spec declares at the TOP level
4
+ * and to read their field names.
5
+ *
6
+ * Deliberately NOT a TypeScript parse or a Zod evaluation: introspection
7
+ * runs inside the DSL compiler, the CI gate, and a request-scoped API
8
+ * handler, none of which may transpile or execute spec-authored code.
9
+ * A scanner is enough because the only question asked is "which
10
+ * identifiers sit at depth 0 of this object literal".
11
+ *
12
+ * What it tracks, and why the naive regex it replaced could not:
13
+ * - comments, strings, template literals and regex literals, so a
14
+ * `z.object(`, a brace, or a comma appearing INSIDE one of those is
15
+ * never read as code;
16
+ * - brace + paren depth, so a NESTED `z.object(` (a field's own shape)
17
+ * is never confused with a top-level schema declaration;
18
+ * - the `const <Name> =` binding preceding each top-level literal, so a
19
+ * spec can name its contract instead of relying on source order.
20
+ *
21
+ * All three walkers below share ONE trivia primitive (`skipTriviaAt`) so
22
+ * they cannot drift into disagreeing about what counts as code.
23
+ */
24
+
25
+ /** A `z.object({ … })` literal declared at the top level of the source. */
26
+ export interface TopLevelZodObjectDeclaration {
27
+ /** Binding name (`export const Foo = z.object({…})` → `Foo`), or null when anonymous. */
28
+ readonly exportName: string | null;
29
+ /** Field identifiers at depth 0 of the literal, in source order, de-duplicated. */
30
+ readonly keys: readonly string[];
31
+ }
32
+
33
+ /**
34
+ * If a comment / string / template / regex literal STARTS at `index`,
35
+ * return the index just past its end; otherwise return `index` unchanged.
36
+ * An unterminated token consumes the rest of the source, which makes the
37
+ * callers treat it as "no readable shape from here" rather than
38
+ * re-entering code state mid-literal.
39
+ *
40
+ * Regex detection is bounded on purpose: a `/` is treated as a regex only
41
+ * when the previous non-whitespace code character is `(` or `,` — the only
42
+ * positions a regex occupies in a schema-declaration module
43
+ * (`.regex(/…/)`, `.refine(x, /…/)`). Division never appears in that
44
+ * position, so there is no ambiguous case to guess at.
45
+ */
46
+ function skipTriviaAt(source: string, index: number, prevCode: string): number {
47
+ const ch = source[index];
48
+ const next = source[index + 1];
49
+
50
+ if (ch === '/' && next === '/') return endOfLineComment(source, index + 2);
51
+ if (ch === '/' && next === '*') return endOfBlockComment(source, index + 2);
52
+ if (ch === "'" || ch === '"' || ch === '`') {
53
+ return endOfQuoted(source, index + 1, ch);
54
+ }
55
+ if (ch === '/' && (prevCode === '(' || prevCode === ',')) {
56
+ return endOfQuoted(source, index + 1, '/');
57
+ }
58
+ return index;
59
+ }
60
+
61
+ function endOfLineComment(source: string, from: number): number {
62
+ const nl = source.indexOf('\n', from);
63
+ return nl === -1 ? source.length : nl + 1;
64
+ }
65
+
66
+ function endOfBlockComment(source: string, from: number): number {
67
+ const end = source.indexOf('*/', from);
68
+ return end === -1 ? source.length : end + 2;
69
+ }
70
+
71
+ /** End of a `'…'` / `"…"` / `` `…` `` / `/…/` token, honoring backslash escapes. */
72
+ function endOfQuoted(source: string, from: number, terminator: string): number {
73
+ for (let i = from; i < source.length; i += 1) {
74
+ const ch = source[i];
75
+ if (ch === '\\') {
76
+ i += 1;
77
+ continue;
78
+ }
79
+ if (ch === terminator) return i + 1;
80
+ }
81
+ return source.length;
82
+ }
83
+
84
+ /** Nesting counters shared by the depth-tracking walkers. */
85
+ interface Depths {
86
+ brace: number;
87
+ paren: number;
88
+ bracket: number;
89
+ }
90
+
91
+ function newDepths(): Depths {
92
+ return { brace: 0, paren: 0, bracket: 0 };
93
+ }
94
+
95
+ function applyDepth(depths: Depths, ch: string | undefined): void {
96
+ if (ch === '{') depths.brace += 1;
97
+ else if (ch === '}') depths.brace = Math.max(0, depths.brace - 1);
98
+ else if (ch === '(') depths.paren += 1;
99
+ else if (ch === ')') depths.paren = Math.max(0, depths.paren - 1);
100
+ else if (ch === '[') depths.bracket += 1;
101
+ else if (ch === ']') depths.bracket = Math.max(0, depths.bracket - 1);
102
+ }
103
+
104
+ function isAtTopLevel(depths: Depths): boolean {
105
+ return (
106
+ depths.brace === 0 && depths.paren === 0 && depths.bracket === 0
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Find every `z.object({ … })` literal that sits at depth 0 — i.e. every
112
+ * schema the module declares in its own right, as opposed to a shape
113
+ * nested inside another schema.
114
+ *
115
+ * Literals whose braces never close are skipped: an unbalanced source has
116
+ * no readable shape, and reporting a truncated key list would be worse
117
+ * than reporting none.
118
+ */
119
+ export function scanTopLevelZodObjectDeclarations(
120
+ source: string,
121
+ ): readonly TopLevelZodObjectDeclaration[] {
122
+ const out: TopLevelZodObjectDeclaration[] = [];
123
+ const depths = newDepths();
124
+ let prevCode = '';
125
+ let i = 0;
126
+
127
+ while (i < source.length) {
128
+ const afterTrivia = skipTriviaAt(source, i, prevCode);
129
+ if (afterTrivia !== i) {
130
+ i = afterTrivia;
131
+ continue;
132
+ }
133
+
134
+ if (isAtTopLevel(depths) && matchesZodObjectAt(source, i)) {
135
+ const bodyStart = openingBraceIndexAfterZodObject(source, i);
136
+ const bodyEnd =
137
+ bodyStart === null ? null : matchingBraceIndex(source, bodyStart);
138
+ if (bodyStart !== null && bodyEnd !== null) {
139
+ out.push({
140
+ exportName: bindingNameBefore(source, i),
141
+ keys: parseObjectKeysAtTopLevel(source.slice(bodyStart, bodyEnd)),
142
+ });
143
+ // Resume AFTER the literal: its interior is already accounted
144
+ // for, and re-walking it could only produce nested matches we
145
+ // deliberately exclude.
146
+ i = bodyEnd + 1;
147
+ prevCode = '}';
148
+ continue;
149
+ }
150
+ }
151
+
152
+ const ch = source[i];
153
+ applyDepth(depths, ch);
154
+ if (ch !== undefined && !/\s/.test(ch)) prevCode = ch;
155
+ i += 1;
156
+ }
157
+
158
+ return out;
159
+ }
160
+
161
+ /** True when `z.object` starts exactly at `index` and is not part of a longer identifier. */
162
+ function matchesZodObjectAt(source: string, index: number): boolean {
163
+ if (!source.startsWith('z.object', index)) return false;
164
+ const prev = index > 0 ? (source[index - 1] ?? '') : '';
165
+ return !/[A-Za-z0-9_$.]/.test(prev);
166
+ }
167
+
168
+ /** Index just past the `{` of `z.object ( {`, or null when the call is not an object literal. */
169
+ function openingBraceIndexAfterZodObject(
170
+ source: string,
171
+ index: number,
172
+ ): number | null {
173
+ let i = skipWhitespace(source, index + 'z.object'.length);
174
+ if (source[i] !== '(') return null;
175
+ i = skipWhitespace(source, i + 1);
176
+ if (source[i] !== '{') return null;
177
+ return i + 1;
178
+ }
179
+
180
+ /**
181
+ * Index of the `}` closing the literal that starts at `bodyStart`, or null
182
+ * when it never closes.
183
+ */
184
+ function matchingBraceIndex(source: string, bodyStart: number): number | null {
185
+ let depth = 1;
186
+ let prevCode = '{';
187
+ let i = bodyStart;
188
+
189
+ while (i < source.length) {
190
+ const afterTrivia = skipTriviaAt(source, i, prevCode);
191
+ if (afterTrivia !== i) {
192
+ i = afterTrivia;
193
+ continue;
194
+ }
195
+ const ch = source[i];
196
+ if (ch === '{') depth += 1;
197
+ else if (ch === '}') {
198
+ depth -= 1;
199
+ if (depth === 0) return i;
200
+ }
201
+ if (ch !== undefined && !/\s/.test(ch)) prevCode = ch;
202
+ i += 1;
203
+ }
204
+ return null;
205
+ }
206
+
207
+ const BINDING_LOOKBEHIND = 400;
208
+ const BINDING_TAIL =
209
+ /(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?::[^=]*)?=\s*$/;
210
+
211
+ /** Name bound to the literal starting at `index` (`export const Foo = z.object(`), or null. */
212
+ function bindingNameBefore(source: string, index: number): string | null {
213
+ const from = Math.max(0, index - BINDING_LOOKBEHIND);
214
+ return BINDING_TAIL.exec(source.slice(from, index))?.[1] ?? null;
215
+ }
216
+
217
+ function skipWhitespace(source: string, index: number): number {
218
+ let i = index;
219
+ while (i < source.length && /\s/.test(source[i]!)) i += 1;
220
+ return i;
221
+ }
222
+
223
+ /**
224
+ * Field identifiers at depth 0 of an object-literal body.
225
+ *
226
+ * Splits on depth-0 commas and reads the leading key of each segment, so
227
+ * a key is recorded exactly once. (The predecessor scanned for a key after
228
+ * BOTH the comma and the newline that followed it, emitting every
229
+ * multi-line key twice — harmless for a set-membership test, but the
230
+ * duplicates leaked into compiler error messages and the "known fields"
231
+ * preview shown to authors.)
232
+ */
233
+ function parseObjectKeysAtTopLevel(body: string): readonly string[] {
234
+ const keys: string[] = [];
235
+ const seen = new Set<string>();
236
+ for (const segment of splitTopLevelSegments(body)) {
237
+ const key = leadingKeyOf(segment);
238
+ if (key !== null && !seen.has(key)) {
239
+ seen.add(key);
240
+ keys.push(key);
241
+ }
242
+ }
243
+ return Object.freeze(keys);
244
+ }
245
+
246
+ function splitTopLevelSegments(body: string): readonly string[] {
247
+ const segments: string[] = [];
248
+ const depths = newDepths();
249
+ let prevCode = '';
250
+ let start = 0;
251
+ let i = 0;
252
+
253
+ while (i < body.length) {
254
+ const afterTrivia = skipTriviaAt(body, i, prevCode);
255
+ if (afterTrivia !== i) {
256
+ i = afterTrivia;
257
+ continue;
258
+ }
259
+ const ch = body[i];
260
+ if (ch === ',' && isAtTopLevel(depths)) {
261
+ segments.push(body.slice(start, i));
262
+ start = i + 1;
263
+ } else {
264
+ applyDepth(depths, ch);
265
+ }
266
+ if (ch !== undefined && !/\s/.test(ch)) prevCode = ch;
267
+ i += 1;
268
+ }
269
+ segments.push(body.slice(start));
270
+ return segments;
271
+ }
272
+
273
+ const LEADING_TRIVIA = /^(?:\s|\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)+/;
274
+ const LEADING_KEY = /^(?:([A-Za-z_$][A-Za-z0-9_$]*)|'([^']+)'|"([^"]+)")\s*:/;
275
+
276
+ function leadingKeyOf(segment: string): string | null {
277
+ const match = LEADING_KEY.exec(segment.replace(LEADING_TRIVIA, ''));
278
+ if (!match) return null;
279
+ return match[1] ?? match[2] ?? match[3] ?? null;
280
+ }
@@ -85,7 +85,7 @@ export interface XemaContext {
85
85
  };
86
86
  readonly workflow: {
87
87
  readonly key: string;
88
- readonly version: string;
88
+ readonly revisionNumber: number;
89
89
  };
90
90
  readonly job: {
91
91
  readonly key: string;
@@ -142,7 +142,7 @@ export const EMPTY_CONTEXT_SEEDS = {
142
142
  correlationId: '',
143
143
  actor: Object.freeze({ subject: null, kind: XemaActorKind.System }),
144
144
  }),
145
- workflow: Object.freeze({ key: '', version: '' }),
145
+ workflow: Object.freeze({ key: '', revisionNumber: 0 }),
146
146
  job: Object.freeze({ key: '', attempt: 0 }),
147
147
  }) as XemaContext,
148
148
  } as const;