@specverse/engines 6.32.11 → 6.33.1
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/dist/ai/analyse-runner.js +1 -1
- package/dist/ai/analyse-runner.js.map +1 -1
- package/dist/ai/behaviours-runner.js +1 -1
- package/dist/ai/behaviours-runner.js.map +1 -1
- package/dist/ai/deployment-emitter.d.ts +3 -0
- package/dist/ai/deployment-emitter.d.ts.map +1 -1
- package/dist/ai/deployment-emitter.js +145 -0
- package/dist/ai/deployment-emitter.js.map +1 -1
- package/dist/ai/skeleton-emitter.d.ts +1 -1
- package/dist/ai/skeleton-emitter.d.ts.map +1 -1
- package/dist/ai/skeleton-emitter.js +73 -26
- package/dist/ai/skeleton-emitter.js.map +1 -1
- package/dist/analyse-prepass/imports-graph.d.ts +274 -0
- package/dist/analyse-prepass/imports-graph.d.ts.map +1 -1
- package/dist/analyse-prepass/imports-graph.js +770 -0
- package/dist/analyse-prepass/imports-graph.js.map +1 -1
- package/dist/analyse-prepass/index.d.ts +20 -0
- package/dist/analyse-prepass/index.d.ts.map +1 -1
- package/dist/analyse-prepass/index.js +17 -0
- package/dist/analyse-prepass/index.js.map +1 -1
- package/dist/libs/instance-factories/orms/templates/prisma/schema-generator.js +66 -4
- package/dist/parser/unified-parser.d.ts.map +1 -1
- package/dist/parser/unified-parser.js +103 -0
- package/dist/parser/unified-parser.js.map +1 -1
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +84 -147
- package/dist/realize/index.js.map +1 -1
- package/dist/realize/per-action-emitter.d.ts +235 -0
- package/dist/realize/per-action-emitter.d.ts.map +1 -0
- package/dist/realize/per-action-emitter.js +229 -0
- package/dist/realize/per-action-emitter.js.map +1 -0
- package/dist/realize/per-action-llm-emit.d.ts +87 -0
- package/dist/realize/per-action-llm-emit.d.ts.map +1 -0
- package/dist/realize/per-action-llm-emit.js +427 -0
- package/dist/realize/per-action-llm-emit.js.map +1 -0
- package/dist/realize/per-action-runner.d.ts +127 -0
- package/dist/realize/per-action-runner.d.ts.map +1 -0
- package/dist/realize/per-action-runner.js +269 -0
- package/dist/realize/per-action-runner.js.map +1 -0
- package/dist/realize/structural-validator.d.ts +71 -0
- package/dist/realize/structural-validator.d.ts.map +1 -0
- package/dist/realize/structural-validator.js +167 -0
- package/dist/realize/structural-validator.js.map +1 -0
- package/libs/instance-factories/orms/templates/prisma/__tests__/schema-generator.test.ts +416 -0
- package/libs/instance-factories/orms/templates/prisma/schema-generator.ts +182 -5
- package/package.json +3 -3
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-action realize runner — Phase 3 of the L3 redesign.
|
|
3
|
+
*
|
|
4
|
+
* The orchestration glue between the spec walker (controllers + services
|
|
5
|
+
* + their actions/operations) and the per-action emitter. This is the
|
|
6
|
+
* call site that REPLACES the legacy `generateAiBehaviors` step in
|
|
7
|
+
* `realize/index.ts` when the per-action path is enabled.
|
|
8
|
+
*
|
|
9
|
+
* Responsibilities:
|
|
10
|
+
* 1. Walk the spec's controllers + services and collect every custom
|
|
11
|
+
* action (controller actions + service operations) along with the
|
|
12
|
+
* owning entity name + step list + spec metadata.
|
|
13
|
+
* 2. For each action, build an ActionSpec + RealizeContext and call
|
|
14
|
+
* emitActionBody. The matcher (prisma / mongo-native / pg-native)
|
|
15
|
+
* and conventions array are passed through as injected
|
|
16
|
+
* dependencies — same shape the legacy ai-behaviors path uses.
|
|
17
|
+
* 3. Group results by owner and emit one `<Owner>.ai.ts` file per
|
|
18
|
+
* owner, containing one exported async function per action with
|
|
19
|
+
* the LLM-emitted (or stub-γ-emitted) body inline.
|
|
20
|
+
*
|
|
21
|
+
* Concurrency: the LLM call site (when a real LLM is configured) is
|
|
22
|
+
* sequential per owner — keeps surgical-retry's parse-error feedback
|
|
23
|
+
* clean and bounds the API spend in the worst case. A future Phase
|
|
24
|
+
* could parallelise across owners; not the default in V1.
|
|
25
|
+
*
|
|
26
|
+
* The legacy `<Owner>.ai.ts` emitted by `ai-behaviors-generator.ts`
|
|
27
|
+
* contains per-STEP helpers; the new file contains per-ACTION pure
|
|
28
|
+
* functions. DR2 says keep the filename, repurpose the content shape.
|
|
29
|
+
* Both shapes co-exist when this runner runs alongside the legacy
|
|
30
|
+
* generator (gated by env flag); the new file overwrites the legacy
|
|
31
|
+
* output when both are run for the same owner. The integration test
|
|
32
|
+
* pins the new shape; the env flag governs which runs in realize.
|
|
33
|
+
*/
|
|
34
|
+
import { type ActionSpec, type LlmEmitFn, type SharedConvention, type SharedStepContext, type MatcherFn } from './per-action-emitter.js';
|
|
35
|
+
/**
|
|
36
|
+
* One action collected from the spec walk. Tracks both the owning
|
|
37
|
+
* entity (for grouping into a single `.ai.ts` file) and the spec
|
|
38
|
+
* metadata the per-action emitter needs.
|
|
39
|
+
*/
|
|
40
|
+
export interface CollectedAction {
|
|
41
|
+
ownerKind: 'controller' | 'service' | 'model';
|
|
42
|
+
/** Controller / service name (the file owner). */
|
|
43
|
+
ownerName: string;
|
|
44
|
+
/** The controller's `model:` reference (or service's "self" name). */
|
|
45
|
+
modelName: string;
|
|
46
|
+
/** Action / operation name as it appears in the spec. */
|
|
47
|
+
actionName: string;
|
|
48
|
+
/** Original ActionSpec — passed straight to emitActionBody. */
|
|
49
|
+
spec: ActionSpec;
|
|
50
|
+
}
|
|
51
|
+
/** Per-owner emission result — drives the `.ai.ts` file write. */
|
|
52
|
+
export interface OwnerEmissionResult {
|
|
53
|
+
ownerName: string;
|
|
54
|
+
/** Map<actionName, body> — the body strings the emitter produced. */
|
|
55
|
+
bodies: Map<string, string>;
|
|
56
|
+
/** Per-action ActionSpec lookup, for shaping the `.ai.ts` signatures. */
|
|
57
|
+
specs: Map<string, ActionSpec>;
|
|
58
|
+
}
|
|
59
|
+
/** Options for {@link runPerActionEmission}. */
|
|
60
|
+
export interface RunPerActionEmissionOptions {
|
|
61
|
+
/** The full spec (already parsed / inferred). */
|
|
62
|
+
spec: any;
|
|
63
|
+
/** Output directory — `.ai.ts` files go under `${outputDir}/backend/src/behaviors/`. */
|
|
64
|
+
outputDir: string;
|
|
65
|
+
/** Convention matcher (matchAgainstConventions, or a test stub). */
|
|
66
|
+
matcher?: MatcherFn;
|
|
67
|
+
/** ORM-specific convention list — prisma / mongo-native / pg-native. */
|
|
68
|
+
conventions?: ReadonlyArray<SharedConvention<SharedStepContext>>;
|
|
69
|
+
/** AI-args expression for the matcher's fallback shape. */
|
|
70
|
+
aiArgsExpr?: (inputs: string[], paramNames: string[]) => string;
|
|
71
|
+
/** LLM emit hook — typically createRealizeActionLlmEmit(). When omitted,
|
|
72
|
+
* every action falls through to γ-stub mode. */
|
|
73
|
+
llmEmit?: LlmEmitFn;
|
|
74
|
+
/** LLM availability predicate. Default mirrors the per-action emitter. */
|
|
75
|
+
canCallLLM?: () => boolean;
|
|
76
|
+
/** When true, skip writing to disk and return the in-memory result.
|
|
77
|
+
* Used by tests to assert the produced bodies without touching fs. */
|
|
78
|
+
dryRun?: boolean;
|
|
79
|
+
/** Quiet mode for tests — suppress console.log progress lines. */
|
|
80
|
+
silent?: boolean;
|
|
81
|
+
}
|
|
82
|
+
/** Aggregate result from {@link runPerActionEmission}. */
|
|
83
|
+
export interface RunPerActionEmissionResult {
|
|
84
|
+
/** One entry per owning entity (controller or service). */
|
|
85
|
+
owners: OwnerEmissionResult[];
|
|
86
|
+
/** Files actually written to disk (empty in dryRun mode). */
|
|
87
|
+
filesWritten: string[];
|
|
88
|
+
/** Aggregate counters for logging / report assembly. */
|
|
89
|
+
totals: {
|
|
90
|
+
actionCount: number;
|
|
91
|
+
ownerCount: number;
|
|
92
|
+
stubCount: number;
|
|
93
|
+
llmCount: number;
|
|
94
|
+
fullyMatchedCount: number;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Walk the spec, dispatch each custom action through the per-action
|
|
99
|
+
* emitter, and write per-owner `.ai.ts` files.
|
|
100
|
+
*
|
|
101
|
+
* Collection rules:
|
|
102
|
+
* - Controllers: every entry under `controller.actions` becomes a
|
|
103
|
+
* CollectedAction. CURVED ops (`controller.cured.{create,...}`)
|
|
104
|
+
* are skipped — the L1 instance-factory templates handle those
|
|
105
|
+
* deterministically.
|
|
106
|
+
* - Services: every entry under `service.operations` becomes a
|
|
107
|
+
* CollectedAction. Service operations don't have CURVED kinship.
|
|
108
|
+
* - Model behaviours (`model.behaviors.<name>`) are NOT collected by
|
|
109
|
+
* this runner — the controller / service is the realized surface;
|
|
110
|
+
* model behaviours flow through controllers via the inference
|
|
111
|
+
* pipeline before realize sees them. A future phase could collect
|
|
112
|
+
* them directly if needed.
|
|
113
|
+
*/
|
|
114
|
+
export declare function runPerActionEmission(opts: RunPerActionEmissionOptions): Promise<RunPerActionEmissionResult>;
|
|
115
|
+
/**
|
|
116
|
+
* Walk the spec and collect every custom controller action + service
|
|
117
|
+
* operation that has a non-empty `steps:` block. Without steps, the
|
|
118
|
+
* per-action emitter has no work to do — those actions stay as
|
|
119
|
+
* placeholder methods on the controller / service.
|
|
120
|
+
*/
|
|
121
|
+
export declare function collectActions(spec: any): CollectedAction[];
|
|
122
|
+
/**
|
|
123
|
+
* Render the `.ai.ts` content for one owner. Exported so tests can
|
|
124
|
+
* assert on the produced shape without going to disk.
|
|
125
|
+
*/
|
|
126
|
+
export declare function renderOwnerAiFile(owner: OwnerEmissionResult): string;
|
|
127
|
+
//# sourceMappingURL=per-action-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"per-action-runner.d.ts","sourceRoot":"","sources":["../../src/realize/per-action-runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAIH,OAAO,EAEL,KAAK,UAAU,EAEf,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACf,MAAM,yBAAyB,CAAC;AAEjC;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,YAAY,GAAG,SAAS,GAAG,OAAO,CAAC;IAC9C,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,kEAAkE;AAClE,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,yEAAyE;IACzE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAChC;AAED,gDAAgD;AAChD,MAAM,WAAW,2BAA2B;IAC1C,iDAAiD;IACjD,IAAI,EAAE,GAAG,CAAC;IACV,wFAAwF;IACxF,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,wEAAwE;IACxE,WAAW,CAAC,EAAE,aAAa,CAAC,gBAAgB,CAAC,iBAAiB,CAAC,CAAC,CAAC;IACjE,2DAA2D;IAC3D,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,MAAM,CAAC;IAChE;qDACiD;IACjD,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;IAC3B;2EACuE;IACvE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,kEAAkE;IAClE,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,0DAA0D;AAC1D,MAAM,WAAW,0BAA0B;IACzC,2DAA2D;IAC3D,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAC9B,6DAA6D;IAC7D,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,wDAAwD;IACxD,MAAM,EAAE;QACN,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,iBAAiB,EAAE,MAAM,CAAC;KAC3B,CAAC;CACH;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,2BAA2B,GAChC,OAAO,CAAC,0BAA0B,CAAC,CA0DrC;AAID;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,GAAG,GAAG,eAAe,EAAE,CAqD3D;AAuDD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,mBAAmB,GAAG,MAAM,CAgCpE"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-action realize runner — Phase 3 of the L3 redesign.
|
|
3
|
+
*
|
|
4
|
+
* The orchestration glue between the spec walker (controllers + services
|
|
5
|
+
* + their actions/operations) and the per-action emitter. This is the
|
|
6
|
+
* call site that REPLACES the legacy `generateAiBehaviors` step in
|
|
7
|
+
* `realize/index.ts` when the per-action path is enabled.
|
|
8
|
+
*
|
|
9
|
+
* Responsibilities:
|
|
10
|
+
* 1. Walk the spec's controllers + services and collect every custom
|
|
11
|
+
* action (controller actions + service operations) along with the
|
|
12
|
+
* owning entity name + step list + spec metadata.
|
|
13
|
+
* 2. For each action, build an ActionSpec + RealizeContext and call
|
|
14
|
+
* emitActionBody. The matcher (prisma / mongo-native / pg-native)
|
|
15
|
+
* and conventions array are passed through as injected
|
|
16
|
+
* dependencies — same shape the legacy ai-behaviors path uses.
|
|
17
|
+
* 3. Group results by owner and emit one `<Owner>.ai.ts` file per
|
|
18
|
+
* owner, containing one exported async function per action with
|
|
19
|
+
* the LLM-emitted (or stub-γ-emitted) body inline.
|
|
20
|
+
*
|
|
21
|
+
* Concurrency: the LLM call site (when a real LLM is configured) is
|
|
22
|
+
* sequential per owner — keeps surgical-retry's parse-error feedback
|
|
23
|
+
* clean and bounds the API spend in the worst case. A future Phase
|
|
24
|
+
* could parallelise across owners; not the default in V1.
|
|
25
|
+
*
|
|
26
|
+
* The legacy `<Owner>.ai.ts` emitted by `ai-behaviors-generator.ts`
|
|
27
|
+
* contains per-STEP helpers; the new file contains per-ACTION pure
|
|
28
|
+
* functions. DR2 says keep the filename, repurpose the content shape.
|
|
29
|
+
* Both shapes co-exist when this runner runs alongside the legacy
|
|
30
|
+
* generator (gated by env flag); the new file overwrites the legacy
|
|
31
|
+
* output when both are run for the same owner. The integration test
|
|
32
|
+
* pins the new shape; the env flag governs which runs in realize.
|
|
33
|
+
*/
|
|
34
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
35
|
+
import { dirname, join } from 'path';
|
|
36
|
+
import { emitActionBody, } from './per-action-emitter.js';
|
|
37
|
+
/**
|
|
38
|
+
* Walk the spec, dispatch each custom action through the per-action
|
|
39
|
+
* emitter, and write per-owner `.ai.ts` files.
|
|
40
|
+
*
|
|
41
|
+
* Collection rules:
|
|
42
|
+
* - Controllers: every entry under `controller.actions` becomes a
|
|
43
|
+
* CollectedAction. CURVED ops (`controller.cured.{create,...}`)
|
|
44
|
+
* are skipped — the L1 instance-factory templates handle those
|
|
45
|
+
* deterministically.
|
|
46
|
+
* - Services: every entry under `service.operations` becomes a
|
|
47
|
+
* CollectedAction. Service operations don't have CURVED kinship.
|
|
48
|
+
* - Model behaviours (`model.behaviors.<name>`) are NOT collected by
|
|
49
|
+
* this runner — the controller / service is the realized surface;
|
|
50
|
+
* model behaviours flow through controllers via the inference
|
|
51
|
+
* pipeline before realize sees them. A future phase could collect
|
|
52
|
+
* them directly if needed.
|
|
53
|
+
*/
|
|
54
|
+
export async function runPerActionEmission(opts) {
|
|
55
|
+
const collected = collectActions(opts.spec);
|
|
56
|
+
const ownerGroups = groupByOwner(collected);
|
|
57
|
+
const owners = [];
|
|
58
|
+
const filesWritten = [];
|
|
59
|
+
const totals = {
|
|
60
|
+
actionCount: 0,
|
|
61
|
+
ownerCount: 0,
|
|
62
|
+
stubCount: 0,
|
|
63
|
+
llmCount: 0,
|
|
64
|
+
fullyMatchedCount: 0,
|
|
65
|
+
};
|
|
66
|
+
for (const [ownerName, actions] of ownerGroups) {
|
|
67
|
+
const bodies = new Map();
|
|
68
|
+
const specs = new Map();
|
|
69
|
+
for (const collectedAction of actions) {
|
|
70
|
+
const realizeCtx = {
|
|
71
|
+
componentName: ownerName,
|
|
72
|
+
modelName: collectedAction.modelName,
|
|
73
|
+
matcher: opts.matcher,
|
|
74
|
+
conventions: opts.conventions,
|
|
75
|
+
aiArgsExpr: opts.aiArgsExpr,
|
|
76
|
+
llmEmit: opts.llmEmit,
|
|
77
|
+
canCallLLM: opts.canCallLLM,
|
|
78
|
+
};
|
|
79
|
+
const body = await emitActionBody(collectedAction.spec, realizeCtx);
|
|
80
|
+
bodies.set(collectedAction.actionName, body);
|
|
81
|
+
specs.set(collectedAction.actionName, collectedAction.spec);
|
|
82
|
+
totals.actionCount++;
|
|
83
|
+
// Heuristic counters — body shape tells us which branch fired.
|
|
84
|
+
if (body.includes('throw new Error(') && body.includes(': not implemented")')) {
|
|
85
|
+
totals.stubCount++;
|
|
86
|
+
}
|
|
87
|
+
else if (collectedAction.spec.steps.length > 0) {
|
|
88
|
+
// No way to distinguish LLM vs full-convention from here, but
|
|
89
|
+
// we can spot the "stub" branch above; everything else is a
|
|
90
|
+
// composed body.
|
|
91
|
+
totals.fullyMatchedCount++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const result = { ownerName, bodies, specs };
|
|
95
|
+
owners.push(result);
|
|
96
|
+
totals.ownerCount++;
|
|
97
|
+
if (!opts.dryRun) {
|
|
98
|
+
const filePath = writeOwnerAiFile(opts.outputDir, result);
|
|
99
|
+
filesWritten.push(filePath);
|
|
100
|
+
if (!opts.silent) {
|
|
101
|
+
console.log(` ✅ Per-action AI behaviors: ${ownerName}.ai.ts (${actions.length} action${actions.length === 1 ? '' : 's'})`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { owners, filesWritten, totals };
|
|
106
|
+
}
|
|
107
|
+
// ─── Spec walking ────────────────────────────────────────────────────
|
|
108
|
+
/**
|
|
109
|
+
* Walk the spec and collect every custom controller action + service
|
|
110
|
+
* operation that has a non-empty `steps:` block. Without steps, the
|
|
111
|
+
* per-action emitter has no work to do — those actions stay as
|
|
112
|
+
* placeholder methods on the controller / service.
|
|
113
|
+
*/
|
|
114
|
+
export function collectActions(spec) {
|
|
115
|
+
const out = [];
|
|
116
|
+
// Controllers — accept either {Name: {...}} object or [{name, ...}] array.
|
|
117
|
+
// When keyed by name (the canonical spec shape), the entries don't
|
|
118
|
+
// necessarily carry a `name` field; we fall back to the map key.
|
|
119
|
+
const controllerEntries = Array.isArray(spec.controllers)
|
|
120
|
+
? spec.controllers.map((c) => [c?.name ?? '', c])
|
|
121
|
+
: Object.entries(spec.controllers || {});
|
|
122
|
+
for (const [keyName, controller] of controllerEntries) {
|
|
123
|
+
if (!controller?.actions)
|
|
124
|
+
continue;
|
|
125
|
+
const ownerName = controller.name || keyName;
|
|
126
|
+
if (!ownerName)
|
|
127
|
+
continue;
|
|
128
|
+
const modelName = controller.model || controller.modelReference || ownerName.replace(/Controller$/, '');
|
|
129
|
+
for (const [actionName, actionRaw] of Object.entries(controller.actions)) {
|
|
130
|
+
const action = normalizeActionSpec(actionRaw, actionName);
|
|
131
|
+
if (action.steps.length === 0)
|
|
132
|
+
continue;
|
|
133
|
+
out.push({
|
|
134
|
+
ownerKind: 'controller',
|
|
135
|
+
ownerName,
|
|
136
|
+
modelName,
|
|
137
|
+
actionName,
|
|
138
|
+
spec: { ...action, ownerName, ownerKind: 'controller' },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Services — accept either {Name: {...}} object or [{name, ...}] array.
|
|
143
|
+
const serviceEntries = Array.isArray(spec.services)
|
|
144
|
+
? spec.services.map((s) => [s?.name ?? '', s])
|
|
145
|
+
: Object.entries(spec.services || {});
|
|
146
|
+
for (const [keyName, service] of serviceEntries) {
|
|
147
|
+
if (!service?.operations)
|
|
148
|
+
continue;
|
|
149
|
+
const ownerName = service.name || keyName;
|
|
150
|
+
if (!ownerName)
|
|
151
|
+
continue;
|
|
152
|
+
const modelName = ownerName.replace(/Service$/, '');
|
|
153
|
+
const operationEntries = Array.isArray(service.operations)
|
|
154
|
+
? service.operations.map((op) => [op.name, op])
|
|
155
|
+
: Object.entries(service.operations);
|
|
156
|
+
for (const [actionName, opRaw] of operationEntries) {
|
|
157
|
+
const action = normalizeActionSpec(opRaw, actionName);
|
|
158
|
+
if (action.steps.length === 0)
|
|
159
|
+
continue;
|
|
160
|
+
out.push({
|
|
161
|
+
ownerKind: 'service',
|
|
162
|
+
ownerName,
|
|
163
|
+
modelName,
|
|
164
|
+
actionName,
|
|
165
|
+
spec: { ...action, ownerName, ownerKind: 'service' },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
/** Coerce a raw action / operation spec object into the ActionSpec shape. */
|
|
172
|
+
function normalizeActionSpec(raw, actionName) {
|
|
173
|
+
const stepsRaw = raw?.steps ?? raw?.implementation?.steps ?? [];
|
|
174
|
+
const steps = Array.isArray(stepsRaw)
|
|
175
|
+
? stepsRaw.map((s) => (typeof s === 'string' ? s : s?.step)).filter((s) => typeof s === 'string')
|
|
176
|
+
: [];
|
|
177
|
+
return {
|
|
178
|
+
name: actionName,
|
|
179
|
+
ownerName: '', // filled in by caller
|
|
180
|
+
ownerKind: 'controller', // overwritten by caller
|
|
181
|
+
steps,
|
|
182
|
+
description: raw?.description,
|
|
183
|
+
parameters: raw?.parameters,
|
|
184
|
+
returns: typeof raw?.returns === 'string' ? raw.returns : raw?.returns ? JSON.stringify(raw.returns) : undefined,
|
|
185
|
+
requires: raw?.requires ?? raw?.preconditions,
|
|
186
|
+
ensures: raw?.ensures ?? raw?.postconditions,
|
|
187
|
+
publishes: raw?.publishes ?? raw?.events,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/** Group collected actions by owner so we emit one .ai.ts file per owner. */
|
|
191
|
+
function groupByOwner(actions) {
|
|
192
|
+
const groups = new Map();
|
|
193
|
+
for (const a of actions) {
|
|
194
|
+
const list = groups.get(a.ownerName) ?? [];
|
|
195
|
+
list.push(a);
|
|
196
|
+
groups.set(a.ownerName, list);
|
|
197
|
+
}
|
|
198
|
+
return groups;
|
|
199
|
+
}
|
|
200
|
+
// ─── File emission ───────────────────────────────────────────────────
|
|
201
|
+
/**
|
|
202
|
+
* Write a single owner's `.ai.ts` file to disk. Returns the file path.
|
|
203
|
+
*
|
|
204
|
+
* The file is structured as:
|
|
205
|
+
* - Header docstring (DR2 framing — repurposed from per-step to per-action).
|
|
206
|
+
* - One `export async function <actionName>(input): Promise<...>` per
|
|
207
|
+
* action, with the body produced by emitActionBody.
|
|
208
|
+
*
|
|
209
|
+
* Signatures are intentionally loose (`input: any`) for V1; Phase 4 /
|
|
210
|
+
* 5 will tighten them using the spec's `parameters:` types. The per-step
|
|
211
|
+
* `.ai.ts` files used the same loose signature, so we don't regress.
|
|
212
|
+
*/
|
|
213
|
+
function writeOwnerAiFile(outputDir, owner) {
|
|
214
|
+
const filePath = join(outputDir, 'backend', 'src', 'behaviors', `${owner.ownerName}.ai.ts`);
|
|
215
|
+
const dir = dirname(filePath);
|
|
216
|
+
if (!existsSync(dir))
|
|
217
|
+
mkdirSync(dir, { recursive: true });
|
|
218
|
+
writeFileSync(filePath, renderOwnerAiFile(owner), 'utf-8');
|
|
219
|
+
return filePath;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Render the `.ai.ts` content for one owner. Exported so tests can
|
|
223
|
+
* assert on the produced shape without going to disk.
|
|
224
|
+
*/
|
|
225
|
+
export function renderOwnerAiFile(owner) {
|
|
226
|
+
const blocks = [];
|
|
227
|
+
blocks.push(`/**
|
|
228
|
+
* ${owner.ownerName} — Per-action AI behaviors (Realize L3 per-action emission).
|
|
229
|
+
*
|
|
230
|
+
* One exported async function per spec action / operation. Each body
|
|
231
|
+
* was emitted by the per-action realize pipeline:
|
|
232
|
+
*
|
|
233
|
+
* - L2 convention matcher pre-baked deterministic snippets for known
|
|
234
|
+
* step patterns. Those snippets are present verbatim in the body.
|
|
235
|
+
* - The LLM (when available) wove the [WRITE]-step code around the
|
|
236
|
+
* pre-baked snippets to produce one coherent method body per action.
|
|
237
|
+
* - When neither convention nor LLM produced a coherent body, the
|
|
238
|
+
* function throws a "not implemented" stub at runtime and preserves
|
|
239
|
+
* the convention-matched scaffolding as a code-block comment for
|
|
240
|
+
* reviewer context (DR4-γ shape).
|
|
241
|
+
*
|
|
242
|
+
* REVIEW SURFACE — anything in this file that ISN'T deterministic
|
|
243
|
+
* convention output came from an LLM. Inspect carefully before
|
|
244
|
+
* deploying production logic.
|
|
245
|
+
*
|
|
246
|
+
* Generated: ${new Date().toISOString().split('T')[0]}
|
|
247
|
+
*/
|
|
248
|
+
`);
|
|
249
|
+
for (const [actionName, spec] of owner.specs) {
|
|
250
|
+
const body = owner.bodies.get(actionName) ?? ` throw new Error('${actionName}: missing body');`;
|
|
251
|
+
blocks.push(renderActionFunction(actionName, spec, body));
|
|
252
|
+
}
|
|
253
|
+
return blocks.join('\n');
|
|
254
|
+
}
|
|
255
|
+
/** Render a single per-action `export async function ...` block. */
|
|
256
|
+
function renderActionFunction(actionName, spec, body) {
|
|
257
|
+
const description = spec.description ? ` * ${spec.description}\n *` : '';
|
|
258
|
+
const stepsDoc = spec.steps.length > 0
|
|
259
|
+
? ` *\n * Spec steps:\n${spec.steps.map((s) => ` * - ${s}`).join('\n')}\n`
|
|
260
|
+
: '';
|
|
261
|
+
return `/**
|
|
262
|
+
* ${actionName}
|
|
263
|
+
${description}${stepsDoc} */
|
|
264
|
+
export async function ${actionName}(input: any): Promise<any> {
|
|
265
|
+
${body}
|
|
266
|
+
}
|
|
267
|
+
`;
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=per-action-runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"per-action-runner.js","sourceRoot":"","sources":["../../src/realize/per-action-runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural validator for per-action LLM output (DR3 of the L3 proposal).
|
|
3
|
+
*
|
|
4
|
+
* The L2 convention matcher produces deterministic TypeScript snippets
|
|
5
|
+
* for spec steps it can match. Those pre-baked snippets are passed to the
|
|
6
|
+
* LLM as scaffolding (DR3-A). The LLM emits a coherent method body that
|
|
7
|
+
* weaves the pre-baked snippets together with [WRITE]-step code it
|
|
8
|
+
* authors itself.
|
|
9
|
+
*
|
|
10
|
+
* The structural validator is the trust-but-verify counterpart to that
|
|
11
|
+
* pattern: after the LLM emits, we diff the output against the pre-baked
|
|
12
|
+
* snippets. Every pre-baked snippet must appear in the LLM's body
|
|
13
|
+
* verbatim (modulo whitespace + trailing punctuation). If a snippet is
|
|
14
|
+
* missing — i.e. the LLM rewrote a deterministic piece — the realize
|
|
15
|
+
* pipeline fails loudly with a diff showing what was preserved + what
|
|
16
|
+
* was dropped.
|
|
17
|
+
*
|
|
18
|
+
* Why "verbatim modulo whitespace": pre-baked snippets are already
|
|
19
|
+
* idiomatic TS. Letting the LLM reformat them silently undermines
|
|
20
|
+
* "deterministic" — we'd lose reproducibility across runs. But forcing
|
|
21
|
+
* exact byte-for-byte match would fail on innocuous LLM behaviours
|
|
22
|
+
* (re-indenting a single line of pre-baked code to match the surrounding
|
|
23
|
+
* indentation it adopted). We compare on the canonical form: the
|
|
24
|
+
* snippet's "code essence" stripped of leading whitespace and trailing
|
|
25
|
+
* comment-only lines.
|
|
26
|
+
*
|
|
27
|
+
* This validator is orthogonal to the parse-check (esbuild) and the
|
|
28
|
+
* tsc-check (per-action-llm-emit's surgical retry loop). All three
|
|
29
|
+
* gates compose: parse-fail → surgical retry; structural-fail → loud
|
|
30
|
+
* fail (LLM rewrote a deterministic piece); tsc-fail → fall through to
|
|
31
|
+
* γ stub.
|
|
32
|
+
*/
|
|
33
|
+
/** A single pre-baked snippet to verify is preserved in the LLM output. */
|
|
34
|
+
export interface PrebakedSnippet {
|
|
35
|
+
/** Step number (1-indexed) the snippet came from — for error messages. */
|
|
36
|
+
stepNum: number;
|
|
37
|
+
/** Human-readable step text — for error messages. */
|
|
38
|
+
stepText: string;
|
|
39
|
+
/** The pre-baked TypeScript snippet. The matcher's `.call` string. */
|
|
40
|
+
snippet: string;
|
|
41
|
+
}
|
|
42
|
+
/** Outcome of the structural-preservation check. */
|
|
43
|
+
export type StructuralValidationResult = {
|
|
44
|
+
ok: true;
|
|
45
|
+
} | {
|
|
46
|
+
ok: false;
|
|
47
|
+
missing: MissingSnippet[];
|
|
48
|
+
reason: string;
|
|
49
|
+
};
|
|
50
|
+
/** Per-snippet failure detail. */
|
|
51
|
+
export interface MissingSnippet {
|
|
52
|
+
stepNum: number;
|
|
53
|
+
stepText: string;
|
|
54
|
+
/** The original pre-baked snippet (canonicalised). */
|
|
55
|
+
expected: string;
|
|
56
|
+
/** The closest match found in the LLM body, or null when no near-match. */
|
|
57
|
+
closestMatch: string | null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Validate that every pre-baked snippet appears in the LLM-emitted body.
|
|
61
|
+
*
|
|
62
|
+
* The check is substring containment on the canonical form (whitespace +
|
|
63
|
+
* leading-comment normalised). For each missing snippet we surface the
|
|
64
|
+
* step number, step text, and the closest near-match found in the body
|
|
65
|
+
* (or `null` if there's no token-overlap candidate).
|
|
66
|
+
*
|
|
67
|
+
* Empty `prebakedSnippets` is a vacuous pass — when no convention
|
|
68
|
+
* matched, there's nothing to preserve.
|
|
69
|
+
*/
|
|
70
|
+
export declare function validateLlmOutputPreservesConventions(llmBody: string, prebakedSnippets: PrebakedSnippet[]): StructuralValidationResult;
|
|
71
|
+
//# sourceMappingURL=structural-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structural-validator.d.ts","sourceRoot":"","sources":["../../src/realize/structural-validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,2EAA2E;AAC3E,MAAM,WAAW,eAAe;IAC9B,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,oDAAoD;AACpD,MAAM,MAAM,0BAA0B,GAClC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D,kCAAkC;AAClC,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAuBD;;;;;;;;;;GAUG;AACH,wBAAgB,qCAAqC,CACnD,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,eAAe,EAAE,GAClC,0BAA0B,CAiD5B"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural validator for per-action LLM output (DR3 of the L3 proposal).
|
|
3
|
+
*
|
|
4
|
+
* The L2 convention matcher produces deterministic TypeScript snippets
|
|
5
|
+
* for spec steps it can match. Those pre-baked snippets are passed to the
|
|
6
|
+
* LLM as scaffolding (DR3-A). The LLM emits a coherent method body that
|
|
7
|
+
* weaves the pre-baked snippets together with [WRITE]-step code it
|
|
8
|
+
* authors itself.
|
|
9
|
+
*
|
|
10
|
+
* The structural validator is the trust-but-verify counterpart to that
|
|
11
|
+
* pattern: after the LLM emits, we diff the output against the pre-baked
|
|
12
|
+
* snippets. Every pre-baked snippet must appear in the LLM's body
|
|
13
|
+
* verbatim (modulo whitespace + trailing punctuation). If a snippet is
|
|
14
|
+
* missing — i.e. the LLM rewrote a deterministic piece — the realize
|
|
15
|
+
* pipeline fails loudly with a diff showing what was preserved + what
|
|
16
|
+
* was dropped.
|
|
17
|
+
*
|
|
18
|
+
* Why "verbatim modulo whitespace": pre-baked snippets are already
|
|
19
|
+
* idiomatic TS. Letting the LLM reformat them silently undermines
|
|
20
|
+
* "deterministic" — we'd lose reproducibility across runs. But forcing
|
|
21
|
+
* exact byte-for-byte match would fail on innocuous LLM behaviours
|
|
22
|
+
* (re-indenting a single line of pre-baked code to match the surrounding
|
|
23
|
+
* indentation it adopted). We compare on the canonical form: the
|
|
24
|
+
* snippet's "code essence" stripped of leading whitespace and trailing
|
|
25
|
+
* comment-only lines.
|
|
26
|
+
*
|
|
27
|
+
* This validator is orthogonal to the parse-check (esbuild) and the
|
|
28
|
+
* tsc-check (per-action-llm-emit's surgical retry loop). All three
|
|
29
|
+
* gates compose: parse-fail → surgical retry; structural-fail → loud
|
|
30
|
+
* fail (LLM rewrote a deterministic piece); tsc-fail → fall through to
|
|
31
|
+
* γ stub.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* Canonicalise a code chunk for comparison: strip leading whitespace per
|
|
35
|
+
* line, drop pure-comment lines (the matcher prepends `// Step N: ...`
|
|
36
|
+
* markers that the LLM is allowed to reword or absorb into prose), and
|
|
37
|
+
* collapse internal whitespace runs to a single space.
|
|
38
|
+
*
|
|
39
|
+
* The result is a fingerprint of the snippet's executable shape — enough
|
|
40
|
+
* to spot rewrites (different identifier, different control flow) but
|
|
41
|
+
* loose enough to tolerate harmless reformatting (alignment, spacing
|
|
42
|
+
* around operators).
|
|
43
|
+
*/
|
|
44
|
+
function canonicalise(code) {
|
|
45
|
+
return code
|
|
46
|
+
.split('\n')
|
|
47
|
+
.map((line) => line.trim())
|
|
48
|
+
.filter((line) => line.length > 0 && !line.startsWith('//'))
|
|
49
|
+
.join(' ')
|
|
50
|
+
.replace(/\s+/g, ' ')
|
|
51
|
+
.trim();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Validate that every pre-baked snippet appears in the LLM-emitted body.
|
|
55
|
+
*
|
|
56
|
+
* The check is substring containment on the canonical form (whitespace +
|
|
57
|
+
* leading-comment normalised). For each missing snippet we surface the
|
|
58
|
+
* step number, step text, and the closest near-match found in the body
|
|
59
|
+
* (or `null` if there's no token-overlap candidate).
|
|
60
|
+
*
|
|
61
|
+
* Empty `prebakedSnippets` is a vacuous pass — when no convention
|
|
62
|
+
* matched, there's nothing to preserve.
|
|
63
|
+
*/
|
|
64
|
+
export function validateLlmOutputPreservesConventions(llmBody, prebakedSnippets) {
|
|
65
|
+
if (prebakedSnippets.length === 0) {
|
|
66
|
+
return { ok: true };
|
|
67
|
+
}
|
|
68
|
+
const llmCanonical = canonicalise(llmBody);
|
|
69
|
+
const missing = [];
|
|
70
|
+
for (const snippet of prebakedSnippets) {
|
|
71
|
+
const expected = canonicalise(snippet.snippet);
|
|
72
|
+
if (expected.length === 0)
|
|
73
|
+
continue; // skip comment-only snippets
|
|
74
|
+
if (llmCanonical.includes(expected)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// Near-match heuristic: extract a list of progressively-shorter
|
|
78
|
+
// dotted tokens from the snippet (e.g. `prisma.customer.findUnique`,
|
|
79
|
+
// `prisma.customer`, `prisma`) and search the LLM body for any of
|
|
80
|
+
// them. Surfaces the closest line to the reviewer — so when the LLM
|
|
81
|
+
// rewrote `findUnique` to `findFirst` (kept the `prisma.customer`
|
|
82
|
+
// prefix), we find the rewritten line rather than reporting "no
|
|
83
|
+
// near-match found".
|
|
84
|
+
const distinctiveTokens = extractDistinctiveTokens(snippet.snippet);
|
|
85
|
+
let closestMatch = null;
|
|
86
|
+
for (const tok of distinctiveTokens) {
|
|
87
|
+
for (const line of llmBody.split('\n')) {
|
|
88
|
+
if (line.includes(tok)) {
|
|
89
|
+
closestMatch = line.trim();
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (closestMatch)
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
missing.push({
|
|
97
|
+
stepNum: snippet.stepNum,
|
|
98
|
+
stepText: snippet.stepText,
|
|
99
|
+
expected: snippet.snippet.trim(),
|
|
100
|
+
closestMatch,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (missing.length === 0) {
|
|
104
|
+
return { ok: true };
|
|
105
|
+
}
|
|
106
|
+
const reason = formatMissingReport(missing);
|
|
107
|
+
return { ok: false, missing, reason };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Extract a list of progressively-shorter distinctive identifier
|
|
111
|
+
* tokens from a snippet, ordered most-specific first. Used by the
|
|
112
|
+
* closest-match heuristic to surface a rewritten line even when the
|
|
113
|
+
* LLM changed the trailing call shape (e.g. `findUnique → findFirst`)
|
|
114
|
+
* but kept the prefix.
|
|
115
|
+
*
|
|
116
|
+
* Returns the empty array when the snippet has no dotted identifiers.
|
|
117
|
+
*/
|
|
118
|
+
function extractDistinctiveTokens(snippet) {
|
|
119
|
+
// Collect every dotted identifier (foo.bar, prisma.order.create, etc.)
|
|
120
|
+
const rawMatches = snippet.match(/[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+/g);
|
|
121
|
+
const matches = rawMatches ? Array.from(rawMatches) : [];
|
|
122
|
+
if (matches.length === 0)
|
|
123
|
+
return [];
|
|
124
|
+
const out = [];
|
|
125
|
+
// Start with the longest dotted identifier, then walk back along
|
|
126
|
+
// its prefix segments (one fewer dot each step) so we have a series
|
|
127
|
+
// of fall-back queries to match against the LLM body.
|
|
128
|
+
matches.sort((a, b) => b.length - a.length);
|
|
129
|
+
const longest = matches[0];
|
|
130
|
+
out.push(longest);
|
|
131
|
+
const segments = longest.split('.');
|
|
132
|
+
for (let i = segments.length - 1; i >= 2; i--) {
|
|
133
|
+
out.push(segments.slice(0, i).join('.'));
|
|
134
|
+
}
|
|
135
|
+
// Add any other dotted identifiers from the snippet, in case the
|
|
136
|
+
// longest one was a transient (e.g. variable assignment) and the
|
|
137
|
+
// distinguishing call sits later.
|
|
138
|
+
for (const m of matches.slice(1)) {
|
|
139
|
+
if (!out.includes(m))
|
|
140
|
+
out.push(m);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Format a human-readable diff report for the realize pipeline to print
|
|
146
|
+
* when the structural validator rejects an LLM output.
|
|
147
|
+
*/
|
|
148
|
+
function formatMissingReport(missing) {
|
|
149
|
+
const lines = [];
|
|
150
|
+
lines.push(`LLM output dropped or rewrote ${missing.length} pre-baked convention snippet${missing.length === 1 ? '' : 's'}:`);
|
|
151
|
+
for (const m of missing) {
|
|
152
|
+
lines.push('');
|
|
153
|
+
lines.push(` Step ${m.stepNum}: ${m.stepText}`);
|
|
154
|
+
lines.push(` Expected (verbatim): ${m.expected.replace(/\n/g, '\n ')}`);
|
|
155
|
+
if (m.closestMatch) {
|
|
156
|
+
lines.push(` Closest in LLM body: ${m.closestMatch}`);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
lines.push(` Closest in LLM body: (no near-match found)`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
lines.push('');
|
|
163
|
+
lines.push('The convention matcher is deterministic; pre-baked snippets must be preserved verbatim. ' +
|
|
164
|
+
'If you believe the matcher is wrong for this case, fix the convention pattern in step-conventions.ts.');
|
|
165
|
+
return lines.join('\n');
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=structural-validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structural-validator.js","sourceRoot":"","sources":["../../src/realize/structural-validator.ts"],"names":[],"mappings":"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"}
|