@specverse/engines 6.0.7 → 6.0.9

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 (47) hide show
  1. package/dist/analyse-prepass/adapters/index.d.ts +9 -0
  2. package/dist/analyse-prepass/adapters/index.d.ts.map +1 -0
  3. package/dist/analyse-prepass/adapters/index.js +9 -0
  4. package/dist/analyse-prepass/adapters/index.js.map +1 -0
  5. package/dist/analyse-prepass/adapters/typescript-prisma.d.ts +67 -0
  6. package/dist/analyse-prepass/adapters/typescript-prisma.d.ts.map +1 -0
  7. package/dist/analyse-prepass/adapters/typescript-prisma.js +167 -0
  8. package/dist/analyse-prepass/adapters/typescript-prisma.js.map +1 -0
  9. package/dist/analyse-prepass/backends/codegraph.d.ts +56 -0
  10. package/dist/analyse-prepass/backends/codegraph.d.ts.map +1 -0
  11. package/dist/analyse-prepass/backends/codegraph.js +303 -0
  12. package/dist/analyse-prepass/backends/codegraph.js.map +1 -0
  13. package/dist/analyse-prepass/backends/gitnexus.d.ts +71 -0
  14. package/dist/analyse-prepass/backends/gitnexus.d.ts.map +1 -0
  15. package/dist/analyse-prepass/backends/gitnexus.js +367 -0
  16. package/dist/analyse-prepass/backends/gitnexus.js.map +1 -0
  17. package/dist/analyse-prepass/backends/grep-only.d.ts +33 -0
  18. package/dist/analyse-prepass/backends/grep-only.d.ts.map +1 -0
  19. package/dist/analyse-prepass/backends/grep-only.js +377 -0
  20. package/dist/analyse-prepass/backends/grep-only.js.map +1 -0
  21. package/dist/analyse-prepass/backends/index.d.ts +28 -0
  22. package/dist/analyse-prepass/backends/index.d.ts.map +1 -0
  23. package/dist/analyse-prepass/backends/index.js +36 -0
  24. package/dist/analyse-prepass/backends/index.js.map +1 -0
  25. package/dist/analyse-prepass/backends/method-patterns.d.ts +27 -0
  26. package/dist/analyse-prepass/backends/method-patterns.d.ts.map +1 -0
  27. package/dist/analyse-prepass/backends/method-patterns.js +177 -0
  28. package/dist/analyse-prepass/backends/method-patterns.js.map +1 -0
  29. package/dist/analyse-prepass/backends/walk.d.ts +7 -0
  30. package/dist/analyse-prepass/backends/walk.d.ts.map +1 -0
  31. package/dist/analyse-prepass/backends/walk.js +105 -0
  32. package/dist/analyse-prepass/backends/walk.js.map +1 -0
  33. package/dist/analyse-prepass/index.d.ts +76 -0
  34. package/dist/analyse-prepass/index.d.ts.map +1 -0
  35. package/dist/analyse-prepass/index.js +114 -0
  36. package/dist/analyse-prepass/index.js.map +1 -0
  37. package/dist/analyse-prepass/interface.d.ts +222 -0
  38. package/dist/analyse-prepass/interface.d.ts.map +1 -0
  39. package/dist/analyse-prepass/interface.js +20 -0
  40. package/dist/analyse-prepass/interface.js.map +1 -0
  41. package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +104 -0
  42. package/dist/libs/instance-factories/orms/templates/prisma/schema-generator.js +25 -4
  43. package/libs/instance-factories/cli/templates/commander/command-generator.ts +104 -0
  44. package/libs/instance-factories/orms/templates/prisma/schema-generator.ts +43 -5
  45. package/package.json +5 -1
  46. package/libs/instance-factories/cli/templates/commander/command-generator.d.ts +0 -14
  47. package/libs/instance-factories/cli/templates/commander/command-generator.js +0 -182
@@ -0,0 +1,222 @@
1
+ /**
2
+ * StructuralPrepass — pluggable backend interface for analyse pre-pass.
3
+ *
4
+ * The pre-pass extracts deterministic structural facts from a source codebase
5
+ * (entities / relationships / lifecycle state literals / etc.) BEFORE the LLM
6
+ * sees it, so the LLM can focus on semantic interpretation rather than
7
+ * structural discovery.
8
+ *
9
+ * Backends: grep-only (zero deps, fallback for tests/CI), codegraph
10
+ * (recommended default), gitnexus (alternative for multi-component cases
11
+ * where Leiden community detection adds value).
12
+ *
13
+ * Adapters: per-framework translators (typescript-prisma first, then
14
+ * typescript-typeorm / python-sqlalchemy / go-gorm / etc.) that consume
15
+ * this interface and produce SpecVerse-vocabulary facts.
16
+ *
17
+ * Design rationale: see specverse-self/docs/proposals/STRUCTURAL-PREPASS-FOR-ANALYSE.md
18
+ */
19
+ /** A symbol detected by tree-sitter (or grep heuristics) — class / function / method / etc. */
20
+ export interface Symbol {
21
+ kind: 'class' | 'function' | 'method' | 'interface' | 'type_alias' | 'constant' | 'enum';
22
+ name: string;
23
+ /** Fully-qualified name where applicable (e.g. "PollController.evolve"). */
24
+ qualifiedName: string;
25
+ filePath: string;
26
+ startLine: number;
27
+ endLine: number;
28
+ /** Function/method signature, e.g. "(id: string, data: any): Promise<any>" */
29
+ signature?: string;
30
+ /** Leading docstring or JSDoc, if present */
31
+ docstring?: string;
32
+ /** Whether the symbol is exported from its module */
33
+ isExported?: boolean;
34
+ /** Decorators on the symbol (TypeScript / Python / Java) */
35
+ decorators?: string[];
36
+ /** Source-level language (typescript, javascript, python, go, ...) */
37
+ language: string;
38
+ }
39
+ /** An import edge — file imports symbols from target module. */
40
+ export interface Import {
41
+ fromFile: string;
42
+ toModule: string;
43
+ symbols: string[];
44
+ }
45
+ /** Capability flags — adapters check before using optional methods. */
46
+ export interface Capabilities {
47
+ /** Backend can resolve callers/callees of a symbol via call graph. */
48
+ callGraph: boolean;
49
+ /** Backend resolves cross-file references (vs single-file regex). */
50
+ crossFileResolution: boolean;
51
+ /** Backend has community-detection clustering (e.g. Leiden in GitNexus). */
52
+ componentClustering: boolean;
53
+ /** Backend supports full-text search across source. */
54
+ fullTextSearch: boolean;
55
+ /** Backend automatically syncs as files change (e.g. CodeGraph file watchers). */
56
+ fileWatching: boolean;
57
+ }
58
+ export interface FileFilter {
59
+ dir?: string;
60
+ lang?: string;
61
+ glob?: string;
62
+ }
63
+ export interface ClassFilter {
64
+ dir?: string;
65
+ namePattern?: RegExp;
66
+ }
67
+ export interface MethodFilter {
68
+ /** Class qualified-name (e.g. "OrderProcessorController") */
69
+ class?: string;
70
+ /** Names to filter to — e.g. ['evolve', 'create', 'validate'] */
71
+ nameIn?: string[];
72
+ }
73
+ export interface InterfaceFilter {
74
+ dir?: string;
75
+ namePattern?: RegExp;
76
+ }
77
+ export interface IndexResult {
78
+ files: number;
79
+ symbols: number;
80
+ durationMs: number;
81
+ }
82
+ /**
83
+ * Per-method fact sheet for behavior extraction.
84
+ *
85
+ * The structural pre-pass surfaces these facts from a method's body via
86
+ * tree-sitter call graph (where available) + regex heuristics for common
87
+ * patterns (event emission, DB writes, throws, async boundaries). The
88
+ * SpecVerse adapter layer (and downstream behavior-extraction prompt)
89
+ * consumes these to translate procedural code into spec-vocabulary
90
+ * `behaviors:` blocks with `steps:` decomposition.
91
+ *
92
+ * Design rationale: see specverse-self/docs/proposals/BEHAVIOR-EXTRACTION-FOR-ANALYSE.md
93
+ * — per-method fact sheets shrink the LLM's job from "summarize 200 lines
94
+ * of code" to "given this structured fact sheet, write a SpecVerse behavior
95
+ * block". Smaller cognitive load → smaller variance surface.
96
+ */
97
+ export interface MethodFactSheet {
98
+ qualifiedName: string;
99
+ signature: string;
100
+ /** Raw source body (the function body — between { and }). */
101
+ body: string;
102
+ filePath: string;
103
+ startLine: number;
104
+ endLine: number;
105
+ language: string;
106
+ /**
107
+ * Methods/functions this one invokes (call graph).
108
+ * From CodeGraph: SQL on edges table (kind='call').
109
+ * From grep-only: regex for `(\w+)\(` calls within the body.
110
+ */
111
+ calls: Symbol[];
112
+ /**
113
+ * Event names emitted from this method's body.
114
+ * Patterns matched: emit() / dispatchEvent() / .next() (RxJS) /
115
+ * eventBus.publish() / pubsub.publish() / send() with first-arg literal.
116
+ */
117
+ emits: string[];
118
+ /**
119
+ * Persisted-state writes detected in the body (per ORM heuristic).
120
+ * Prisma: prisma.X.create() / .update() / .delete() / .upsert().
121
+ * TypeORM: repository.save() / .update() / .remove().
122
+ * SQLAlchemy: session.add() / .delete() / commit-tracked mutations.
123
+ * For arbitrary code, populated only when the adapter knows the ORM.
124
+ */
125
+ dbWrites: Array<{
126
+ entity: string;
127
+ field?: string;
128
+ op: 'create' | 'update' | 'delete' | 'upsert' | 'unknown';
129
+ }>;
130
+ /**
131
+ * External system calls detected in the body.
132
+ * Patterns: fetch() / axios.X() / http.request() (HTTP),
133
+ * kafka.send() / rabbit.publish() (queue), redis.set() / cache.set() (cache).
134
+ */
135
+ externalCalls: Array<{
136
+ kind: 'http' | 'queue' | 'cache' | 'unknown';
137
+ target?: string;
138
+ }>;
139
+ /**
140
+ * Explicit throw statements with their error type. `throw new BadRequestError(...)`
141
+ * → `BadRequestError`. Useful for inferring preconditions / error states.
142
+ */
143
+ throws: string[];
144
+ /**
145
+ * Async boundaries — locations where the method awaits, runs in parallel,
146
+ * or races. Useful for distinguishing sequential from parallel steps.
147
+ */
148
+ asyncBoundaries: Array<{
149
+ type: 'await' | 'promise.all' | 'promise.race' | 'promise.allSettled';
150
+ line: number;
151
+ }>;
152
+ /**
153
+ * Cyclomatic complexity proxy: count of branch points (if/else/switch/catch/ternary).
154
+ * High values suggest the LLM should look for branching steps in spec.
155
+ */
156
+ branchPoints: number;
157
+ /**
158
+ * For SpecVerse-realized backends: the body has comment-delineated regions
159
+ * (PRECONDITIONS / EXECUTE / POSTCONDITIONS / EVENTS). When detected, the
160
+ * regions are sliced out for the verify-behavior pass to cross-check spec
161
+ * preconditions/postconditions/events against source-side markers.
162
+ *
163
+ * For arbitrary backends, all fields here are undefined.
164
+ */
165
+ bodyTextSliced?: {
166
+ preconditions?: string;
167
+ execute?: string;
168
+ postconditions?: string;
169
+ events?: string;
170
+ };
171
+ }
172
+ /**
173
+ * Pluggable backend for structural extraction. Implementations:
174
+ * - backends/grep-only.ts — no external tools, regex per language
175
+ * - backends/codegraph.ts — tree-sitter via CodeGraph CLI + SQLite
176
+ * - backends/gitnexus.ts — tree-sitter via GitNexus MCP + LadybugDB
177
+ */
178
+ export interface StructuralPrepass {
179
+ /** Initialize the backend on a source directory. */
180
+ init(sourceDir: string): Promise<void>;
181
+ /** Build / refresh the index. Returns stats. May be a no-op on grep-only. */
182
+ index(): Promise<IndexResult>;
183
+ /** List files in scope, optionally filtered. */
184
+ listFiles(filter?: FileFilter): Promise<string[]>;
185
+ /** List classes (or class-like declarations across languages). */
186
+ listClasses(filter?: ClassFilter): Promise<Symbol[]>;
187
+ /** List methods on classes. Class membership encoded in qualifiedName. */
188
+ listMethods(filter?: MethodFilter): Promise<Symbol[]>;
189
+ /** List interfaces / type-defs. */
190
+ listInterfaces(filter?: InterfaceFilter): Promise<Symbol[]>;
191
+ /** List imports for a single file. */
192
+ listImports(file: string): Promise<Import[]>;
193
+ /** Read raw source text. Adapters use this for regex extraction of literals. */
194
+ fileSourceText(file: string): Promise<string>;
195
+ /**
196
+ * Optional: callers/callees graph traversal.
197
+ * Throws if `capabilities.callGraph === false`.
198
+ */
199
+ callers?(symbol: string): Promise<Symbol[]>;
200
+ callees?(symbol: string): Promise<Symbol[]>;
201
+ /**
202
+ * Get the per-method fact sheet for a single method (by qualified name,
203
+ * e.g. "OrderProcessorController.processOrder"). Returns null if the
204
+ * method isn't found or isn't analyseable.
205
+ *
206
+ * All backends implement this — grep-only via regex over the method body,
207
+ * CodeGraph via SQL on the edges table for call data + regex for everything
208
+ * else (events, dbWrites, etc. aren't tree-sitter symbols, they're patterns).
209
+ */
210
+ getMethodDetails(qualifiedName: string): Promise<MethodFactSheet | null>;
211
+ /**
212
+ * Optional: cluster files into communities (Leiden / similar).
213
+ * Throws if `capabilities.componentClustering === false`.
214
+ */
215
+ clusterFiles?(): Promise<Array<{
216
+ id: string;
217
+ files: string[];
218
+ }>>;
219
+ /** Capability flags — adapters check before calling optional methods. */
220
+ capabilities: Capabilities;
221
+ }
222
+ //# sourceMappingURL=interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../src/analyse-prepass/interface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,+FAA+F;AAC/F,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,WAAW,GAAG,YAAY,GAAG,UAAU,GAAG,MAAM,CAAC;IACzF,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4DAA4D;IAC5D,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,gEAAgE;AAChE,MAAM,WAAW,MAAM;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,SAAS,EAAE,OAAO,CAAC;IACnB,qEAAqE;IACrE,mBAAmB,EAAE,OAAO,CAAC;IAC7B,4EAA4E;IAC5E,mBAAmB,EAAE,OAAO,CAAC;IAC7B,uDAAuD;IACvD,cAAc,EAAE,OAAO,CAAC;IACxB,kFAAkF;IAClF,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;;OAIG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;IAEhB;;;;OAIG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;IAEhB;;;;;;OAMG;IACH,QAAQ,EAAE,KAAK,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,EAAE,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;KAC3D,CAAC,CAAC;IAEH;;;;OAIG;IACH,aAAa,EAAE,KAAK,CAAC;QACnB,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;QAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC,CAAC;IAEH;;;OAGG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;IAEjB;;;OAGG;IACH,eAAe,EAAE,KAAK,CAAC;QACrB,IAAI,EAAE,OAAO,GAAG,aAAa,GAAG,cAAc,GAAG,oBAAoB,CAAC;QACtE,IAAI,EAAE,MAAM,CAAC;KACd,CAAC,CAAC;IAEH;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE;QACf,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,oDAAoD;IACpD,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvC,6EAA6E;IAC7E,KAAK,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;IAE9B,gDAAgD;IAChD,SAAS,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAElD,kEAAkE;IAClE,WAAW,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAErD,0EAA0E;IAC1E,WAAW,CAAC,MAAM,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEtD,mCAAmC;IACnC,cAAc,CAAC,MAAM,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAE5D,sCAAsC;IACtC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAE7C,gFAAgF;IAChF,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE9C;;;OAGG;IACH,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5C,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAE5C;;;;;;;;OAQG;IACH,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;IAEzE;;;OAGG;IACH,YAAY,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC,CAAC;IAEjE,yEAAyE;IACzE,YAAY,EAAE,YAAY,CAAC;CAC5B"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * StructuralPrepass — pluggable backend interface for analyse pre-pass.
3
+ *
4
+ * The pre-pass extracts deterministic structural facts from a source codebase
5
+ * (entities / relationships / lifecycle state literals / etc.) BEFORE the LLM
6
+ * sees it, so the LLM can focus on semantic interpretation rather than
7
+ * structural discovery.
8
+ *
9
+ * Backends: grep-only (zero deps, fallback for tests/CI), codegraph
10
+ * (recommended default), gitnexus (alternative for multi-component cases
11
+ * where Leiden community detection adds value).
12
+ *
13
+ * Adapters: per-framework translators (typescript-prisma first, then
14
+ * typescript-typeorm / python-sqlalchemy / go-gorm / etc.) that consume
15
+ * this interface and produce SpecVerse-vocabulary facts.
16
+ *
17
+ * Design rationale: see specverse-self/docs/proposals/STRUCTURAL-PREPASS-FOR-ANALYSE.md
18
+ */
19
+ export {};
20
+ //# sourceMappingURL=interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interface.js","sourceRoot":"","sources":["../../src/analyse-prepass/interface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG"}
@@ -374,6 +374,110 @@ import type { ParserEngine, InferenceEngine, RealizeEngine } from '@specverse/ty
374
374
  }
375
375
  const inferredSpec = { ...componentData, componentName, components: inferredYaml?.components || {} };
376
376
 
377
+ // --estimate: report what realize WOULD do, broken down by layer.
378
+ // Skip the actual realizeAll call (no manifest needed, no LLM cost).
379
+ // The three layers:
380
+ // L1 \u2014 Instance factory (templates, no LLM)
381
+ // L2 \u2014 Convention pattern matching (CURVED ops + default events, no LLM)
382
+ // L3 \u2014 AI from steps (one LLM call per declared step)
383
+ if (options.estimate) {
384
+ const components = inferredSpec.components || {};
385
+ const compNames = Object.keys(components);
386
+ let entityCount = 0, controllerCount = 0, serviceCount = 0, eventCount = 0, viewCount = 0;
387
+ let modelBehaviorSteps = 0, controllerActionSteps = 0, serviceOpSteps = 0;
388
+ let modelBehaviorsWithSteps = 0, controllerActionsWithSteps = 0, serviceOpsWithSteps = 0;
389
+ let curvedOpCount = 0;
390
+ const sumSteps = (block: any) => {
391
+ if (!block || typeof block !== 'object') return { ops: 0, opsWithSteps: 0, totalSteps: 0 };
392
+ let ops = 0, opsWithSteps = 0, totalSteps = 0;
393
+ for (const def of Object.values(block)) {
394
+ ops++;
395
+ const steps = (def as any)?.steps;
396
+ if (Array.isArray(steps) && steps.length > 0) {
397
+ opsWithSteps++;
398
+ totalSteps += steps.length;
399
+ }
400
+ }
401
+ return { ops, opsWithSteps, totalSteps };
402
+ };
403
+ for (const compName of compNames) {
404
+ const comp = components[compName] || {};
405
+ const models = comp.models || {};
406
+ const controllers = comp.controllers || {};
407
+ const services = comp.services || {};
408
+ const events = comp.events || {};
409
+ const views = comp.views || {};
410
+ entityCount += Object.keys(models).length;
411
+ controllerCount += Object.keys(controllers).length;
412
+ serviceCount += Object.keys(services).length;
413
+ eventCount += Object.keys(events).length;
414
+ viewCount += Object.keys(views).length;
415
+ for (const m of Object.values(models)) {
416
+ const r = sumSteps((m as any)?.behaviors);
417
+ modelBehaviorsWithSteps += r.opsWithSteps;
418
+ modelBehaviorSteps += r.totalSteps;
419
+ }
420
+ for (const c of Object.values(controllers)) {
421
+ const r = sumSteps((c as any)?.actions);
422
+ controllerActionsWithSteps += r.opsWithSteps;
423
+ controllerActionSteps += r.totalSteps;
424
+ const cured = (c as any)?.cured || (c as any)?.curved;
425
+ if (cured && typeof cured === 'object') curvedOpCount += Object.keys(cured).length;
426
+ }
427
+ for (const s of Object.values(services)) {
428
+ const r = sumSteps((s as any)?.operations);
429
+ serviceOpsWithSteps += r.opsWithSteps;
430
+ serviceOpSteps += r.totalSteps;
431
+ }
432
+ }
433
+ const totalLlmCalls = modelBehaviorSteps + controllerActionSteps + serviceOpSteps;
434
+ const fileEstimate = entityCount * 7 + controllerCount + serviceCount * 2 + viewCount + 15;
435
+ console.log('');
436
+ console.log('\u2554\u2550\u2550 spv realize --estimate \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550');
437
+ console.log('\u2551');
438
+ console.log('\u2551 Type: ' + type);
439
+ console.log('\u2551 Components: ' + compNames.length + (compNames.length > 0 ? ' (' + compNames.join(', ') + ')' : ''));
440
+ console.log('\u2551');
441
+ console.log('\u2551 \u2500\u2500 Spec inventory (post-inference) \u2500\u2500');
442
+ console.log('\u2551 Entities (models): ' + entityCount);
443
+ console.log('\u2551 Controllers: ' + controllerCount);
444
+ console.log('\u2551 Services: ' + serviceCount);
445
+ console.log('\u2551 Events: ' + eventCount);
446
+ console.log('\u2551 Views: ' + viewCount);
447
+ console.log('\u2551');
448
+ console.log('\u2551 \u2500\u2500 Output by realize layer \u2500\u2500');
449
+ console.log('\u2551');
450
+ console.log('\u2551 L1 \u2014 Instance factory (file scaffolding, no LLM):');
451
+ console.log('\u2551 File scaffolding for each entity / controller / service / view');
452
+ console.log('\u2551 + framework boilerplate (Fastify routes, Prisma schema, React shell)');
453
+ console.log('\u2551 Estimate: ~' + fileEstimate + ' files');
454
+ console.log('\u2551');
455
+ console.log('\u2551 L2 \u2014 Convention pattern matching (bodies, no LLM):');
456
+ console.log('\u2551 CURVED ops auto-implemented: ' + curvedOpCount);
457
+ console.log('\u2551 Default events on lifecycles: ' + eventCount + ' (declared) + auto-derived');
458
+ console.log('\u2551 Default validation patterns: ~' + (entityCount * 2));
459
+ console.log('\u2551');
460
+ console.log('\u2551 L3 \u2014 AI from steps (LLM, 1 call per step):');
461
+ console.log('\u2551 Model behaviors with steps: ' + modelBehaviorsWithSteps + ' behaviors \u2192 ' + modelBehaviorSteps + ' steps');
462
+ console.log('\u2551 Controller actions with steps: ' + controllerActionsWithSteps + ' actions \u2192 ' + controllerActionSteps + ' steps');
463
+ console.log('\u2551 Service ops with steps: ' + serviceOpsWithSteps + ' operations \u2192 ' + serviceOpSteps + ' steps');
464
+ console.log('\u2551 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500');
465
+ console.log('\u2551 Total LLM calls: ' + totalLlmCalls);
466
+ console.log('\u2551');
467
+ if (totalLlmCalls > 0) {
468
+ console.log('\u2551 \u2500\u2500 Estimated wall time + cost (L3 only) \u2500\u2500');
469
+ console.log('\u2551 On Opus 4.7 / Max: ~' + Math.ceil(totalLlmCalls * 0.2) + ' min wall (free, claude-cli session-cached)');
470
+ console.log('\u2551 On Sonnet 4.6: ~' + Math.ceil(totalLlmCalls * 0.1) + ' min wall (free)');
471
+ console.log('\u2551 On Anthropic API: ~' + Math.ceil(totalLlmCalls * 0.1) + ' min wall, ~$' + (totalLlmCalls * 0.015).toFixed(2) + ' (Sonnet rates)');
472
+ console.log('\u2551 On DeepSeek/Together: ~' + Math.ceil(totalLlmCalls * 0.08) + ' min wall, ~$' + (totalLlmCalls * 0.0008).toFixed(3) + ' (10-30\xD7 cheaper)');
473
+ } else {
474
+ console.log('\u2551 No LLM cost \u2014 all output is L1 + L2 (templates + conventions only).');
475
+ }
476
+ console.log('\u2551');
477
+ console.log('\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550');
478
+ return;
479
+ }
480
+
377
481
  // Realize \u2014 let the realize engine handle its own library.
378
482
  // Locate the nearest implementation manifest by walking up from the
379
483
  // spec file, then falling back to the user's cwd. This lets the same
@@ -17,12 +17,24 @@ function generatePrismaSchema(context) {
17
17
  }
18
18
  }
19
19
  const header = generateHeader(implType);
20
+ const enumCollector = /* @__PURE__ */ new Map();
20
21
  const modelSchemas = allModels.map(
21
- (model) => generateModelSchema(model, relationMap, backRefs, hasOneTargets, allModels)
22
+ (model) => generateModelSchema(model, relationMap, backRefs, hasOneTargets, allModels, enumCollector)
22
23
  ).join("\n\n");
24
+ let enumBlock = "";
25
+ if (enumCollector.size > 0) {
26
+ const enumDecls = [];
27
+ for (const [enumName, values] of enumCollector) {
28
+ const body = values.map((v) => ` ${v}`).join("\n");
29
+ enumDecls.push(`enum ${enumName} {
30
+ ${body}
31
+ }`);
32
+ }
33
+ enumBlock = enumDecls.join("\n\n") + "\n\n";
34
+ }
23
35
  return `${header}
24
36
 
25
- ${modelSchemas}`;
37
+ ${enumBlock}${modelSchemas}`;
26
38
  }
27
39
  function buildRelationMap(allModels) {
28
40
  const targetRefs = /* @__PURE__ */ new Map();
@@ -148,7 +160,7 @@ datasource db {
148
160
  url = ${provider === "sqlite" ? '"file:./dev.db"' : 'env("DATABASE_URL")'}
149
161
  }`;
150
162
  }
151
- function generateModelSchema(model, relationMap, backRefs, hasOneTargets, allModels) {
163
+ function generateModelSchema(model, relationMap, backRefs, hasOneTargets, allModels, enumCollector) {
152
164
  const modelName = model.name;
153
165
  let schema = `model ${modelName} {
154
166
  `;
@@ -158,6 +170,7 @@ function generateModelSchema(model, relationMap, backRefs, hasOneTargets, allMod
158
170
  schema += ` ${generateField(attr, model)}
159
171
  `;
160
172
  });
173
+ const ENUM_VALUE_REGEX = /^[a-zA-Z][a-zA-Z0-9_]*$/;
161
174
  const rawLifecycles = model.lifecycles || [];
162
175
  const lifecycleList = Array.isArray(rawLifecycles) ? rawLifecycles : Object.entries(rawLifecycles).map(([name, lc]) => ({ name, ...typeof lc === "object" ? lc : {} }));
163
176
  for (const lifecycle of lifecycleList) {
@@ -166,8 +179,16 @@ function generateModelSchema(model, relationMap, backRefs, hasOneTargets, allMod
166
179
  if (!exists && lifecycle.states?.length > 0) {
167
180
  const defaultState = lifecycle.states[0];
168
181
  const padding = " ".repeat(Math.max(1, 15 - fieldName.length));
169
- schema += ` ${fieldName}${padding}String @default("${defaultState}")
182
+ const allValid = lifecycle.states.every((s) => ENUM_VALUE_REGEX.test(s));
183
+ if (allValid && enumCollector) {
184
+ const enumName = `${modelName}${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;
185
+ enumCollector.set(enumName, lifecycle.states);
186
+ schema += ` ${fieldName}${padding}${enumName} @default(${defaultState})
170
187
  `;
188
+ } else {
189
+ schema += ` ${fieldName}${padding}String @default("${defaultState}")
190
+ `;
191
+ }
171
192
  }
172
193
  }
173
194
  relationships.forEach((rel) => {
@@ -429,6 +429,110 @@ import type { ParserEngine, InferenceEngine, RealizeEngine } from '@specverse/ty
429
429
  }
430
430
  const inferredSpec = { ...componentData, componentName, components: inferredYaml?.components || {} };
431
431
 
432
+ // --estimate: report what realize WOULD do, broken down by layer.
433
+ // Skip the actual realizeAll call (no manifest needed, no LLM cost).
434
+ // The three layers:
435
+ // L1 — Instance factory (templates, no LLM)
436
+ // L2 — Convention pattern matching (CURVED ops + default events, no LLM)
437
+ // L3 — AI from steps (one LLM call per declared step)
438
+ if (options.estimate) {
439
+ const components = inferredSpec.components || {};
440
+ const compNames = Object.keys(components);
441
+ let entityCount = 0, controllerCount = 0, serviceCount = 0, eventCount = 0, viewCount = 0;
442
+ let modelBehaviorSteps = 0, controllerActionSteps = 0, serviceOpSteps = 0;
443
+ let modelBehaviorsWithSteps = 0, controllerActionsWithSteps = 0, serviceOpsWithSteps = 0;
444
+ let curvedOpCount = 0;
445
+ const sumSteps = (block: any) => {
446
+ if (!block || typeof block !== 'object') return { ops: 0, opsWithSteps: 0, totalSteps: 0 };
447
+ let ops = 0, opsWithSteps = 0, totalSteps = 0;
448
+ for (const def of Object.values(block)) {
449
+ ops++;
450
+ const steps = (def as any)?.steps;
451
+ if (Array.isArray(steps) && steps.length > 0) {
452
+ opsWithSteps++;
453
+ totalSteps += steps.length;
454
+ }
455
+ }
456
+ return { ops, opsWithSteps, totalSteps };
457
+ };
458
+ for (const compName of compNames) {
459
+ const comp = components[compName] || {};
460
+ const models = comp.models || {};
461
+ const controllers = comp.controllers || {};
462
+ const services = comp.services || {};
463
+ const events = comp.events || {};
464
+ const views = comp.views || {};
465
+ entityCount += Object.keys(models).length;
466
+ controllerCount += Object.keys(controllers).length;
467
+ serviceCount += Object.keys(services).length;
468
+ eventCount += Object.keys(events).length;
469
+ viewCount += Object.keys(views).length;
470
+ for (const m of Object.values(models)) {
471
+ const r = sumSteps((m as any)?.behaviors);
472
+ modelBehaviorsWithSteps += r.opsWithSteps;
473
+ modelBehaviorSteps += r.totalSteps;
474
+ }
475
+ for (const c of Object.values(controllers)) {
476
+ const r = sumSteps((c as any)?.actions);
477
+ controllerActionsWithSteps += r.opsWithSteps;
478
+ controllerActionSteps += r.totalSteps;
479
+ const cured = (c as any)?.cured || (c as any)?.curved;
480
+ if (cured && typeof cured === 'object') curvedOpCount += Object.keys(cured).length;
481
+ }
482
+ for (const s of Object.values(services)) {
483
+ const r = sumSteps((s as any)?.operations);
484
+ serviceOpsWithSteps += r.opsWithSteps;
485
+ serviceOpSteps += r.totalSteps;
486
+ }
487
+ }
488
+ const totalLlmCalls = modelBehaviorSteps + controllerActionSteps + serviceOpSteps;
489
+ const fileEstimate = entityCount * 7 + controllerCount + serviceCount * 2 + viewCount + 15;
490
+ console.log('');
491
+ console.log('╔══ spv realize --estimate ══════════════════════════════════════════');
492
+ console.log('║');
493
+ console.log('║ Type: ' + type);
494
+ console.log('║ Components: ' + compNames.length + (compNames.length > 0 ? ' (' + compNames.join(', ') + ')' : ''));
495
+ console.log('║');
496
+ console.log('║ ── Spec inventory (post-inference) ──');
497
+ console.log('║ Entities (models): ' + entityCount);
498
+ console.log('║ Controllers: ' + controllerCount);
499
+ console.log('║ Services: ' + serviceCount);
500
+ console.log('║ Events: ' + eventCount);
501
+ console.log('║ Views: ' + viewCount);
502
+ console.log('║');
503
+ console.log('║ ── Output by realize layer ──');
504
+ console.log('║');
505
+ console.log('║ L1 — Instance factory (file scaffolding, no LLM):');
506
+ console.log('║ File scaffolding for each entity / controller / service / view');
507
+ console.log('║ + framework boilerplate (Fastify routes, Prisma schema, React shell)');
508
+ console.log('║ Estimate: ~' + fileEstimate + ' files');
509
+ console.log('║');
510
+ console.log('║ L2 — Convention pattern matching (bodies, no LLM):');
511
+ console.log('║ CURVED ops auto-implemented: ' + curvedOpCount);
512
+ console.log('║ Default events on lifecycles: ' + eventCount + ' (declared) + auto-derived');
513
+ console.log('║ Default validation patterns: ~' + (entityCount * 2));
514
+ console.log('║');
515
+ console.log('║ L3 — AI from steps (LLM, 1 call per step):');
516
+ console.log('║ Model behaviors with steps: ' + modelBehaviorsWithSteps + ' behaviors → ' + modelBehaviorSteps + ' steps');
517
+ console.log('║ Controller actions with steps: ' + controllerActionsWithSteps + ' actions → ' + controllerActionSteps + ' steps');
518
+ console.log('║ Service ops with steps: ' + serviceOpsWithSteps + ' operations → ' + serviceOpSteps + ' steps');
519
+ console.log('║ ────────────────────────────────────────────────────────');
520
+ console.log('║ Total LLM calls: ' + totalLlmCalls);
521
+ console.log('║');
522
+ if (totalLlmCalls > 0) {
523
+ console.log('║ ── Estimated wall time + cost (L3 only) ──');
524
+ console.log('║ On Opus 4.7 / Max: ~' + Math.ceil(totalLlmCalls * 0.2) + ' min wall (free, claude-cli session-cached)');
525
+ console.log('║ On Sonnet 4.6: ~' + Math.ceil(totalLlmCalls * 0.1) + ' min wall (free)');
526
+ console.log('║ On Anthropic API: ~' + Math.ceil(totalLlmCalls * 0.1) + ' min wall, ~$' + (totalLlmCalls * 0.015).toFixed(2) + ' (Sonnet rates)');
527
+ console.log('║ On DeepSeek/Together: ~' + Math.ceil(totalLlmCalls * 0.08) + ' min wall, ~$' + (totalLlmCalls * 0.0008).toFixed(3) + ' (10-30× cheaper)');
528
+ } else {
529
+ console.log('║ No LLM cost — all output is L1 + L2 (templates + conventions only).');
530
+ }
531
+ console.log('║');
532
+ console.log('╚════════════════════════════════════════════════════════════════════');
533
+ return;
534
+ }
535
+
432
536
  // Realize — let the realize engine handle its own library.
433
537
  // Locate the nearest implementation manifest by walking up from the
434
538
  // spec file, then falling back to the user's cwd. This lets the same
@@ -42,12 +42,28 @@ export default function generatePrismaSchema(context: TemplateContext): string {
42
42
  // Generate header
43
43
  const header = generateHeader(implType);
44
44
 
45
+ // Collector for Prisma enum declarations derived from model lifecycles.
46
+ // Generated as a side-effect of generateModelSchema and emitted at the
47
+ // top level (Prisma requires enums declared OUTSIDE model blocks).
48
+ const enumCollector = new Map<string, string[]>();
49
+
45
50
  // Generate models
46
51
  const modelSchemas = allModels.map((model: any) =>
47
- generateModelSchema(model, relationMap, backRefs, hasOneTargets, allModels)
52
+ generateModelSchema(model, relationMap, backRefs, hasOneTargets, allModels, enumCollector)
48
53
  ).join('\n\n');
49
54
 
50
- return `${header}\n\n${modelSchemas}`;
55
+ // Emit collected enums before models, so models can reference them.
56
+ let enumBlock = '';
57
+ if (enumCollector.size > 0) {
58
+ const enumDecls: string[] = [];
59
+ for (const [enumName, values] of enumCollector) {
60
+ const body = values.map((v) => ` ${v}`).join('\n');
61
+ enumDecls.push(`enum ${enumName} {\n${body}\n}`);
62
+ }
63
+ enumBlock = enumDecls.join('\n\n') + '\n\n';
64
+ }
65
+
66
+ return `${header}\n\n${enumBlock}${modelSchemas}`;
51
67
  }
52
68
 
53
69
  /**
@@ -285,7 +301,8 @@ function generateModelSchema(
285
301
  relationMap: Map<string, string | null>,
286
302
  backRefs: Map<string, string[]>,
287
303
  hasOneTargets: Set<string>,
288
- allModels?: any[]
304
+ allModels?: any[],
305
+ enumCollector?: Map<string, string[]>,
289
306
  ): string {
290
307
  const modelName = model.name;
291
308
  let schema = `model ${modelName} {\n`;
@@ -305,7 +322,19 @@ function generateModelSchema(
305
322
  schema += ` ${generateField(attr, model)}\n`;
306
323
  });
307
324
 
308
- // Add lifecycle status fields (if model has lifecycles)
325
+ // Add lifecycle status fields (if model has lifecycles).
326
+ //
327
+ // Each lifecycle's state literals become a Prisma `enum` declared at the
328
+ // schema's top level. The field's type is the enum, not a plain `String`.
329
+ // This preserves the state vocabulary in source so the realize→analyse
330
+ // round-trip oracle can recover lifecycle states (previously, only the
331
+ // default state survived — the others were silently dropped).
332
+ //
333
+ // Prisma enum value identifier rules: `^[a-zA-Z][a-zA-Z0-9_]*$`. SpecVerse
334
+ // states are already snake_case (enforced by the convention prompt) so they
335
+ // satisfy this without transformation. If a state somehow contains an
336
+ // illegal char, fall back to plain String so we don't emit an invalid schema.
337
+ const ENUM_VALUE_REGEX = /^[a-zA-Z][a-zA-Z0-9_]*$/;
309
338
  const rawLifecycles = model.lifecycles || [];
310
339
  const lifecycleList = Array.isArray(rawLifecycles)
311
340
  ? rawLifecycles
@@ -316,7 +345,16 @@ function generateModelSchema(
316
345
  if (!exists && lifecycle.states?.length > 0) {
317
346
  const defaultState = lifecycle.states[0];
318
347
  const padding = ' '.repeat(Math.max(1, 15 - fieldName.length));
319
- schema += ` ${fieldName}${padding}String @default("${defaultState}")\n`;
348
+ const allValid = lifecycle.states.every((s: string) => ENUM_VALUE_REGEX.test(s));
349
+ if (allValid && enumCollector) {
350
+ const enumName = `${modelName}${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`;
351
+ enumCollector.set(enumName, lifecycle.states);
352
+ schema += ` ${fieldName}${padding}${enumName} @default(${defaultState})\n`;
353
+ } else {
354
+ // Fallback for invalid identifiers or when no collector — preserves the
355
+ // pre-fix behavior so downstream code never sees an undefined enum ref.
356
+ schema += ` ${fieldName}${padding}String @default("${defaultState}")\n`;
357
+ }
320
358
  }
321
359
  }
322
360
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specverse/engines",
3
- "version": "6.0.7",
3
+ "version": "6.0.9",
4
4
  "description": "SpecVerse toolchain — parser, inference, realize, generators, AI, registry, bundles",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,6 +19,10 @@
19
19
  "types": "./dist/inference/index.d.ts",
20
20
  "import": "./dist/inference/index.js"
21
21
  },
22
+ "./analyse-prepass": {
23
+ "types": "./dist/analyse-prepass/index.d.ts",
24
+ "import": "./dist/analyse-prepass/index.js"
25
+ },
22
26
  "./realize": {
23
27
  "types": "./dist/realize/index.d.ts",
24
28
  "import": "./dist/realize/index.js"
@@ -1,14 +0,0 @@
1
- /**
2
- * Command Generator
3
- *
4
- * Generates individual command files from command specifications.
5
- * Each command registers itself on a Commander program and wires
6
- * to its corresponding service for business logic delegation.
7
- */
8
- import type { TemplateContext } from '@specverse/types';
9
- /**
10
- * Generate a single command file.
11
- * Called once per command in the spec.
12
- */
13
- export default function generateCommand(context: TemplateContext): string;
14
- //# sourceMappingURL=command-generator.d.ts.map