@specverse/engines 6.42.3 → 6.60.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.d.ts.map +1 -1
- package/dist/ai/analyse-runner.js +53 -1
- package/dist/ai/analyse-runner.js.map +1 -1
- package/dist/ai/prompt-runner.d.ts +39 -1
- package/dist/ai/prompt-runner.d.ts.map +1 -1
- package/dist/ai/prompt-runner.js +44 -3
- package/dist/ai/prompt-runner.js.map +1 -1
- package/dist/ai/providers/claude-cli.d.ts.map +1 -1
- package/dist/ai/providers/claude-cli.js +8 -1
- package/dist/ai/providers/claude-cli.js.map +1 -1
- package/dist/ai/skill-loader.d.ts +50 -0
- package/dist/ai/skill-loader.d.ts.map +1 -0
- package/dist/ai/skill-loader.js +96 -0
- package/dist/ai/skill-loader.js.map +1 -0
- package/dist/analyse-prepass/adapters/index.d.ts +2 -0
- package/dist/analyse-prepass/adapters/index.d.ts.map +1 -1
- package/dist/analyse-prepass/adapters/index.js +2 -0
- package/dist/analyse-prepass/adapters/index.js.map +1 -1
- package/dist/analyse-prepass/adapters/module-functions.d.ts +95 -0
- package/dist/analyse-prepass/adapters/module-functions.d.ts.map +1 -0
- package/dist/analyse-prepass/adapters/module-functions.js +358 -0
- package/dist/analyse-prepass/adapters/module-functions.js.map +1 -0
- package/dist/analyse-prepass/adapters/naming-convention-fks.d.ts +90 -0
- package/dist/analyse-prepass/adapters/naming-convention-fks.d.ts.map +1 -0
- package/dist/analyse-prepass/adapters/naming-convention-fks.js +181 -0
- package/dist/analyse-prepass/adapters/naming-convention-fks.js.map +1 -0
- package/dist/analyse-prepass/index.d.ts +8 -0
- package/dist/analyse-prepass/index.d.ts.map +1 -1
- package/dist/analyse-prepass/index.js +130 -0
- package/dist/analyse-prepass/index.js.map +1 -1
- package/dist/libs/instance-factories/cli/templates/commander/cli-entry-generator.js +11 -12
- package/dist/libs/instance-factories/cli/templates/commander/command-generator.js +2 -2
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +29 -10
- package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +10 -9
- package/dist/libs/instance-factories/services/templates/prisma/behavior-generator.js +24 -2
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +28 -20
- package/dist/normalise/index.d.ts +14 -0
- package/dist/normalise/index.d.ts.map +1 -0
- package/dist/normalise/index.js +14 -0
- package/dist/normalise/index.js.map +1 -0
- package/dist/normalise/load-overrides.d.ts +43 -0
- package/dist/normalise/load-overrides.d.ts.map +1 -0
- package/dist/normalise/load-overrides.js +121 -0
- package/dist/normalise/load-overrides.js.map +1 -0
- package/dist/normalise/normalise-rules.d.ts +181 -0
- package/dist/normalise/normalise-rules.d.ts.map +1 -0
- package/dist/normalise/normalise-rules.js +79 -0
- package/dist/normalise/normalise-rules.js.map +1 -0
- package/dist/normalise/rules/cluster-module-functions.d.ts +31 -0
- package/dist/normalise/rules/cluster-module-functions.d.ts.map +1 -0
- package/dist/normalise/rules/cluster-module-functions.js +238 -0
- package/dist/normalise/rules/cluster-module-functions.js.map +1 -0
- package/dist/normalise/rules/crud-into-curved.d.ts +117 -0
- package/dist/normalise/rules/crud-into-curved.d.ts.map +1 -0
- package/dist/normalise/rules/crud-into-curved.js +303 -0
- package/dist/normalise/rules/crud-into-curved.js.map +1 -0
- package/dist/normalise/rules/drop-trivial-passthrough.d.ts +92 -0
- package/dist/normalise/rules/drop-trivial-passthrough.d.ts.map +1 -0
- package/dist/normalise/rules/drop-trivial-passthrough.js +217 -0
- package/dist/normalise/rules/drop-trivial-passthrough.js.map +1 -0
- package/dist/normalise/runner.d.ts +58 -0
- package/dist/normalise/runner.d.ts.map +1 -0
- package/dist/normalise/runner.js +114 -0
- package/dist/normalise/runner.js.map +1 -0
- package/dist/parser/import-resolver/resolver.js +1 -1
- package/dist/parser/import-resolver/resolver.js.map +1 -1
- package/dist/realize/engines/typescript-engine.js +1 -1
- package/dist/realize/engines/typescript-engine.js.map +1 -1
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +221 -88
- package/dist/realize/index.js.map +1 -1
- package/dist/realize/library/library.js +1 -1
- package/dist/realize/library/library.js.map +1 -1
- package/dist/realize/library/resolver.d.ts.map +1 -1
- package/dist/realize/library/resolver.js +14 -1
- package/dist/realize/library/resolver.js.map +1 -1
- package/dist/realize/owner-emit-shared.d.ts +114 -0
- package/dist/realize/owner-emit-shared.d.ts.map +1 -0
- package/dist/realize/owner-emit-shared.js +227 -0
- package/dist/realize/owner-emit-shared.js.map +1 -0
- package/dist/realize/per-action-recovery.d.ts +74 -0
- package/dist/realize/per-action-recovery.d.ts.map +1 -0
- package/dist/realize/per-action-recovery.js +268 -0
- package/dist/realize/per-action-recovery.js.map +1 -0
- package/dist/realize/per-owner-emit.d.ts +7 -58
- package/dist/realize/per-owner-emit.d.ts.map +1 -1
- package/dist/realize/per-owner-emit.js +67 -215
- package/dist/realize/per-owner-emit.js.map +1 -1
- package/dist/realize/per-owner-runner.d.ts +24 -4
- package/dist/realize/per-owner-runner.d.ts.map +1 -1
- package/dist/realize/per-owner-runner.js +77 -19
- package/dist/realize/per-owner-runner.js.map +1 -1
- package/dist/realize/post-emit-verify/diagnostics.d.ts +107 -0
- package/dist/realize/post-emit-verify/diagnostics.d.ts.map +1 -0
- package/dist/realize/post-emit-verify/diagnostics.js +148 -0
- package/dist/realize/post-emit-verify/diagnostics.js.map +1 -0
- package/dist/realize/post-emit-verify/feedback-runner.d.ts +123 -0
- package/dist/realize/post-emit-verify/feedback-runner.d.ts.map +1 -0
- package/dist/realize/post-emit-verify/feedback-runner.js +232 -0
- package/dist/realize/post-emit-verify/feedback-runner.js.map +1 -0
- package/dist/realize/post-emit-verify/index.d.ts +19 -0
- package/dist/realize/post-emit-verify/index.d.ts.map +1 -0
- package/dist/realize/post-emit-verify/index.js +18 -0
- package/dist/realize/post-emit-verify/index.js.map +1 -0
- package/dist/realize/post-emit-verify/reemit.d.ts +82 -0
- package/dist/realize/post-emit-verify/reemit.d.ts.map +1 -0
- package/dist/realize/post-emit-verify/reemit.js +124 -0
- package/dist/realize/post-emit-verify/reemit.js.map +1 -0
- package/dist/realize/post-emit-verify/types.d.ts +187 -0
- package/dist/realize/post-emit-verify/types.d.ts.map +1 -0
- package/dist/realize/post-emit-verify/types.js +28 -0
- package/dist/realize/post-emit-verify/types.js.map +1 -0
- package/dist/realize/post-emit-verify/verifier-manifest.d.ts +29 -0
- package/dist/realize/post-emit-verify/verifier-manifest.d.ts.map +1 -0
- package/dist/realize/post-emit-verify/verifier-manifest.js +57 -0
- package/dist/realize/post-emit-verify/verifier-manifest.js.map +1 -0
- package/dist/realize/post-emit-verify/verifiers/stub-completeness.d.ts +85 -0
- package/dist/realize/post-emit-verify/verifiers/stub-completeness.d.ts.map +1 -0
- package/dist/realize/post-emit-verify/verifiers/stub-completeness.js +298 -0
- package/dist/realize/post-emit-verify/verifiers/stub-completeness.js.map +1 -0
- package/dist/realize/post-emit-verify/verifiers/tsc.d.ts +24 -0
- package/dist/realize/post-emit-verify/verifiers/tsc.d.ts.map +1 -0
- package/dist/realize/post-emit-verify/verifiers/tsc.js +148 -0
- package/dist/realize/post-emit-verify/verifiers/tsc.js.map +1 -0
- package/dist/realize/realize-context-snapshot.d.ts +70 -0
- package/dist/realize/realize-context-snapshot.d.ts.map +1 -0
- package/dist/realize/realize-context-snapshot.js +96 -0
- package/dist/realize/realize-context-snapshot.js.map +1 -0
- package/dist/realize/realize-rules.d.ts +113 -0
- package/dist/realize/realize-rules.d.ts.map +1 -0
- package/dist/realize/realize-rules.js +271 -0
- package/dist/realize/realize-rules.js.map +1 -0
- package/dist/realize/structural-validator.d.ts +36 -2
- package/dist/realize/structural-validator.d.ts.map +1 -1
- package/dist/realize/structural-validator.js +50 -7
- package/dist/realize/structural-validator.js.map +1 -1
- package/libs/instance-factories/cli/templates/commander/cli-entry-generator.ts +11 -12
- package/libs/instance-factories/cli/templates/commander/command-generator.ts +2 -2
- package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +49 -15
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +19 -3
- package/libs/instance-factories/services/templates/prisma/behavior-generator.ts +62 -2
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +47 -20
- package/package.json +9 -1
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalise rule: `crud-into-curved`.
|
|
3
|
+
*
|
|
4
|
+
* Phase 3 of `2026-05-13-NORMALISE-PHASE.md` §5 Rule 2. Lifts Service
|
|
5
|
+
* operations that match the canonical CRUD shape onto their target
|
|
6
|
+
* entity's Controller `cured:` block. This eliminates redundant service
|
|
7
|
+
* shells for plain CRUD wrappers — SpecVerse generates
|
|
8
|
+
* `cured.retrieve/create/update/delete` from a controller's cured
|
|
9
|
+
* declaration, so an explicit `Service.getClub` that just does
|
|
10
|
+
* "look up by id" is duplicating convention.
|
|
11
|
+
*
|
|
12
|
+
* Schema note: CURVED operations live on Controllers, NOT on Models.
|
|
13
|
+
* If the target entity has no Controller declared, the rule synthesises
|
|
14
|
+
* a `<Entity>Controller` (with `model: <Entity>`) in the same component
|
|
15
|
+
* as the Model. The cured op lands there.
|
|
16
|
+
*
|
|
17
|
+
* Before (post-Rule-1 / cluster-module-functions):
|
|
18
|
+
*
|
|
19
|
+
* components:
|
|
20
|
+
* App:
|
|
21
|
+
* models:
|
|
22
|
+
* Club:
|
|
23
|
+
* attributes: [...]
|
|
24
|
+
* services:
|
|
25
|
+
* ClubsService:
|
|
26
|
+
* operations:
|
|
27
|
+
* getClub:
|
|
28
|
+
* steps: [Look up Club by id, Return Club or null]
|
|
29
|
+
* requires: [id is provided]
|
|
30
|
+
* getClubPositions: # NOT lifted (filtered query)
|
|
31
|
+
* steps: [...]
|
|
32
|
+
*
|
|
33
|
+
* After:
|
|
34
|
+
*
|
|
35
|
+
* components:
|
|
36
|
+
* App:
|
|
37
|
+
* models:
|
|
38
|
+
* Club:
|
|
39
|
+
* attributes: [...]
|
|
40
|
+
* controllers:
|
|
41
|
+
* ClubController: # synthesised by this rule
|
|
42
|
+
* model: Club
|
|
43
|
+
* cured:
|
|
44
|
+
* retrieve:
|
|
45
|
+
* steps: [Look up Club by id, Return Club or null]
|
|
46
|
+
* requires: [id is provided]
|
|
47
|
+
* services:
|
|
48
|
+
* ClubsService:
|
|
49
|
+
* operations:
|
|
50
|
+
* getClubPositions: { ... } # remains
|
|
51
|
+
*
|
|
52
|
+
* If the service becomes empty after lifting, it is dropped entirely.
|
|
53
|
+
*
|
|
54
|
+
* Conservative matching:
|
|
55
|
+
* - Op name must be EXACTLY `<verb><EntityName>` — no trailing suffix
|
|
56
|
+
* (`getRound` ✓, `getRoundsForUser` ✗, `getRoundStatus` ✗)
|
|
57
|
+
* - Target Entity name must resolve UNIQUELY to ONE model across all
|
|
58
|
+
* components (the cured block goes to whichever component the model
|
|
59
|
+
* lives in — cross-component lifts are fine since the cured: lives
|
|
60
|
+
* ON the model, not at the service site)
|
|
61
|
+
* - Skip when the entity name is declared in multiple components (rare,
|
|
62
|
+
* but ambiguous — caller can rename the duplicate)
|
|
63
|
+
* - Target model's `cured.<curvedOp>` must NOT already be declared
|
|
64
|
+
* (don't overwrite spec-author intent)
|
|
65
|
+
* - Verb patterns recognised:
|
|
66
|
+
* get / retrieve / find → retrieve
|
|
67
|
+
* create / add → create (Phase 3: only `create` to stay safe)
|
|
68
|
+
* update → update
|
|
69
|
+
* delete / remove → delete (Phase 3: only `delete` to stay safe)
|
|
70
|
+
*
|
|
71
|
+
* NOT in Phase 3 scope:
|
|
72
|
+
* - retrieve_many (list / getAll / search — naming varies too widely)
|
|
73
|
+
* - Evolve transitions (would need lifecycle state-machine signal)
|
|
74
|
+
* - Cross-component lifts (target model in a different component)
|
|
75
|
+
* - Multi-arg create variants (createClubWithPositions)
|
|
76
|
+
*/
|
|
77
|
+
/** Verb → CURVED op mapping. Order matters for prefix matching (longer first). */
|
|
78
|
+
const VERB_MAP = [
|
|
79
|
+
{ verb: 'retrieve', curved: 'retrieve' },
|
|
80
|
+
{ verb: 'create', curved: 'create' },
|
|
81
|
+
{ verb: 'update', curved: 'update' },
|
|
82
|
+
{ verb: 'delete', curved: 'delete' },
|
|
83
|
+
{ verb: 'find', curved: 'retrieve' },
|
|
84
|
+
{ verb: 'get', curved: 'retrieve' },
|
|
85
|
+
];
|
|
86
|
+
/**
|
|
87
|
+
* Parse an operation name into (verb, entityName) if it matches the
|
|
88
|
+
* exact `<verb><PascalCaseEntity>` shape. Returns null otherwise.
|
|
89
|
+
*
|
|
90
|
+
* The entity portion must be PascalCase, contain no further uppercase
|
|
91
|
+
* boundaries after the leading letter (i.e. single PascalCase word),
|
|
92
|
+
* AND must end at the end of the string (no trailing qualifier like
|
|
93
|
+
* `getRoundStatus` or `getClubsForUser`).
|
|
94
|
+
*/
|
|
95
|
+
function parseCrudOpName(opName) {
|
|
96
|
+
for (const { verb, curved } of VERB_MAP) {
|
|
97
|
+
if (!opName.startsWith(verb))
|
|
98
|
+
continue;
|
|
99
|
+
const rest = opName.slice(verb.length);
|
|
100
|
+
if (rest.length === 0)
|
|
101
|
+
continue;
|
|
102
|
+
// First character must be uppercase (start of entity name).
|
|
103
|
+
if (!(rest[0] >= 'A' && rest[0] <= 'Z'))
|
|
104
|
+
continue;
|
|
105
|
+
// Rest must be a SINGLE PascalCase word — no further capital letters
|
|
106
|
+
// after the first character. This rules out compound names like
|
|
107
|
+
// `getClubPositions` (Positions is a 2nd PascalCase token →
|
|
108
|
+
// domain-filtered query, NOT plain retrieve).
|
|
109
|
+
let hasSecondCapital = false;
|
|
110
|
+
for (let i = 1; i < rest.length; i++) {
|
|
111
|
+
const c = rest[i];
|
|
112
|
+
if (c >= 'A' && c <= 'Z') {
|
|
113
|
+
hasSecondCapital = true;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (hasSecondCapital)
|
|
118
|
+
continue;
|
|
119
|
+
return { verb, curved, entity: rest };
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Build a model-name → component-name index across all components. Used
|
|
125
|
+
* by the cross-component target lookup. When an entity name is declared
|
|
126
|
+
* in multiple components, it's recorded as ambiguous (the rule then
|
|
127
|
+
* skips matches against that name).
|
|
128
|
+
*/
|
|
129
|
+
function buildModelIndex(spec) {
|
|
130
|
+
const out = new Map();
|
|
131
|
+
const components = spec.components;
|
|
132
|
+
if (!components)
|
|
133
|
+
return out;
|
|
134
|
+
for (const [componentName, componentBody] of Object.entries(components)) {
|
|
135
|
+
const models = componentBody?.models;
|
|
136
|
+
if (!models)
|
|
137
|
+
continue;
|
|
138
|
+
for (const modelName of Object.keys(models)) {
|
|
139
|
+
const existing = out.get(modelName);
|
|
140
|
+
if (existing === undefined)
|
|
141
|
+
out.set(modelName, componentName);
|
|
142
|
+
else if (existing !== componentName)
|
|
143
|
+
out.set(modelName, 'ambiguous');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function detectMatchesInComponent(spec, modelIndex, componentName, componentBody) {
|
|
149
|
+
const out = [];
|
|
150
|
+
const services = componentBody.services;
|
|
151
|
+
if (!services)
|
|
152
|
+
return out;
|
|
153
|
+
for (const [serviceName, serviceBody] of Object.entries(services)) {
|
|
154
|
+
if (!serviceBody || typeof serviceBody !== 'object')
|
|
155
|
+
continue;
|
|
156
|
+
const ops = serviceBody.operations;
|
|
157
|
+
if (!ops || typeof ops !== 'object')
|
|
158
|
+
continue;
|
|
159
|
+
for (const opName of Object.keys(ops)) {
|
|
160
|
+
const parsed = parseCrudOpName(opName);
|
|
161
|
+
if (!parsed)
|
|
162
|
+
continue;
|
|
163
|
+
const targetComponent = modelIndex.get(parsed.entity);
|
|
164
|
+
if (!targetComponent || targetComponent === 'ambiguous')
|
|
165
|
+
continue;
|
|
166
|
+
// Look up the target controller's cured block (if a controller
|
|
167
|
+
// already exists for this model). Skip if the op is already
|
|
168
|
+
// declared. Controllers may be missing — that's fine; the
|
|
169
|
+
// transform will synthesise one.
|
|
170
|
+
const components = spec.components;
|
|
171
|
+
const targetComponentBody = components[targetComponent];
|
|
172
|
+
const existingController = findControllerForModel(targetComponentBody, parsed.entity);
|
|
173
|
+
if (existingController) {
|
|
174
|
+
const existingCured = existingController.cured;
|
|
175
|
+
if (existingCured && parsed.curved in existingCured)
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
out.push({
|
|
179
|
+
sourceComponentName: componentName,
|
|
180
|
+
targetComponentName: targetComponent,
|
|
181
|
+
modelName: parsed.entity,
|
|
182
|
+
serviceName,
|
|
183
|
+
operationName: opName,
|
|
184
|
+
curvedOp: parsed.curved,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Find the controller in `componentBody` whose `model:` field references
|
|
192
|
+
* the given entity name. Returns the controller body, or null if none.
|
|
193
|
+
* The controller's KEY in the controllers object is the controller name
|
|
194
|
+
* (e.g. ClubController); the `model:` field declares which model it
|
|
195
|
+
* operates on.
|
|
196
|
+
*/
|
|
197
|
+
function findControllerForModel(componentBody, modelName) {
|
|
198
|
+
const controllers = componentBody.controllers;
|
|
199
|
+
if (!controllers || typeof controllers !== 'object')
|
|
200
|
+
return null;
|
|
201
|
+
for (const ctrlBody of Object.values(controllers)) {
|
|
202
|
+
if (!ctrlBody || typeof ctrlBody !== 'object')
|
|
203
|
+
continue;
|
|
204
|
+
if (ctrlBody.model === modelName) {
|
|
205
|
+
return ctrlBody;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
function applyCrudMatch(spec, data) {
|
|
211
|
+
const components = spec.components;
|
|
212
|
+
const sourceComponent = components[data.sourceComponentName];
|
|
213
|
+
const targetComponent = components[data.targetComponentName];
|
|
214
|
+
const services = sourceComponent.services;
|
|
215
|
+
const serviceBody = services[data.serviceName];
|
|
216
|
+
const ops = serviceBody.operations;
|
|
217
|
+
const opBody = ops[data.operationName];
|
|
218
|
+
// Locate the target controller (synthesise one if absent).
|
|
219
|
+
let targetController = findControllerForModel(targetComponent, data.modelName);
|
|
220
|
+
let targetControllerName = null;
|
|
221
|
+
if (!targetController) {
|
|
222
|
+
targetControllerName = `${data.modelName}Controller`;
|
|
223
|
+
targetController = { model: data.modelName };
|
|
224
|
+
// Insert into the component's controllers map. Create the map if
|
|
225
|
+
// it doesn't exist (preserving the standard position in the
|
|
226
|
+
// component — models then controllers then services).
|
|
227
|
+
const existingControllers = targetComponent.controllers ?? {};
|
|
228
|
+
existingControllers[targetControllerName] = targetController;
|
|
229
|
+
targetComponent.controllers = existingControllers;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
// Find the key of the existing controller (we have its body, need its name for description).
|
|
233
|
+
const allControllers = targetComponent.controllers;
|
|
234
|
+
for (const [k, v] of Object.entries(allControllers)) {
|
|
235
|
+
if (v === targetController) {
|
|
236
|
+
targetControllerName = k;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Insert under controller.cured.<curvedOp>. Preserve standard order.
|
|
242
|
+
const existingCured = targetController.cured ?? {};
|
|
243
|
+
const newCured = {};
|
|
244
|
+
const STANDARD_ORDER = ['create', 'update', 'retrieve', 'retrieve_many', 'validate', 'evolve', 'delete'];
|
|
245
|
+
const allKeys = new Set([...Object.keys(existingCured), data.curvedOp]);
|
|
246
|
+
for (const k of STANDARD_ORDER) {
|
|
247
|
+
if (allKeys.has(k)) {
|
|
248
|
+
newCured[k] = k === data.curvedOp ? opBody : existingCured[k];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
for (const [k, v] of Object.entries(existingCured)) {
|
|
252
|
+
if (!STANDARD_ORDER.includes(k))
|
|
253
|
+
newCured[k] = v;
|
|
254
|
+
}
|
|
255
|
+
targetController.cured = newCured;
|
|
256
|
+
// Remove operation from service.
|
|
257
|
+
delete ops[data.operationName];
|
|
258
|
+
// If the service has no remaining operations, drop the service. If
|
|
259
|
+
// doing so leaves an empty services map, drop that too.
|
|
260
|
+
if (Object.keys(ops).length === 0) {
|
|
261
|
+
delete services[data.serviceName];
|
|
262
|
+
if (Object.keys(services).length === 0) {
|
|
263
|
+
delete sourceComponent.services;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return spec;
|
|
267
|
+
}
|
|
268
|
+
export const CRUD_INTO_CURVED_RULE = {
|
|
269
|
+
id: 'crud-into-curved',
|
|
270
|
+
summary: 'Lift Service operations matching <verb><Entity> exactly (where Entity is a model in the same component) to the model\'s cured: block. Drop service if empty.',
|
|
271
|
+
ordering: 10,
|
|
272
|
+
enabledByDefault: true,
|
|
273
|
+
appliesWhen: (ctx) => ctx.source !== 'hand-authored',
|
|
274
|
+
detect: (spec) => {
|
|
275
|
+
const matches = [];
|
|
276
|
+
const components = spec.components;
|
|
277
|
+
if (!components || typeof components !== 'object')
|
|
278
|
+
return matches;
|
|
279
|
+
const modelIndex = buildModelIndex(spec);
|
|
280
|
+
for (const [componentName, componentBody] of Object.entries(components)) {
|
|
281
|
+
if (!componentBody || typeof componentBody !== 'object')
|
|
282
|
+
continue;
|
|
283
|
+
const found = detectMatchesInComponent(spec, modelIndex, componentName, componentBody);
|
|
284
|
+
for (const data of found) {
|
|
285
|
+
const crossComp = data.sourceComponentName !== data.targetComponentName;
|
|
286
|
+
matches.push({
|
|
287
|
+
description: `${data.sourceComponentName}.${data.serviceName}.${data.operationName} → ${data.targetComponentName}.${data.modelName}.cured.${data.curvedOp}${crossComp ? ' (cross-component)' : ''}`,
|
|
288
|
+
data,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return matches;
|
|
293
|
+
},
|
|
294
|
+
transform: (spec, match) => {
|
|
295
|
+
return applyCrudMatch(spec, match.data);
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
export const __internal = {
|
|
299
|
+
parseCrudOpName,
|
|
300
|
+
buildModelIndex,
|
|
301
|
+
detectMatchesInComponent,
|
|
302
|
+
};
|
|
303
|
+
//# sourceMappingURL=crud-into-curved.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crud-into-curved.js","sourceRoot":"","sources":["../../../src/normalise/rules/crud-into-curved.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalise rule: `drop-trivial-passthrough`.
|
|
3
|
+
*
|
|
4
|
+
* Phase 4 of `2026-05-13-NORMALISE-PHASE.md` §5 Rule 3. Removes
|
|
5
|
+
* Controller.cured operations whose declared body is just the framework
|
|
6
|
+
* default — a single look-up / insert / update / delete with no
|
|
7
|
+
* preconditions, no published events, and no branching.
|
|
8
|
+
*
|
|
9
|
+
* Rationale: SpecVerse's realize phase auto-emits standard CURVED
|
|
10
|
+
* implementations for every Model. When an explicit `cured.retrieve:`
|
|
11
|
+
* block exists in the spec, realize uses it INSTEAD of the framework
|
|
12
|
+
* default. If the explicit body is semantically identical to the
|
|
13
|
+
* default ("Look up X by id, return X or null"), declaring it is
|
|
14
|
+
* noise — drop it and let inference fill in the default at realize
|
|
15
|
+
* time.
|
|
16
|
+
*
|
|
17
|
+
* Before (post-Rule-2):
|
|
18
|
+
*
|
|
19
|
+
* ClubController:
|
|
20
|
+
* model: Club
|
|
21
|
+
* cured:
|
|
22
|
+
* retrieve:
|
|
23
|
+
* steps:
|
|
24
|
+
* - Look up Club by id
|
|
25
|
+
* - Return Club or null
|
|
26
|
+
* requires: [id is provided]
|
|
27
|
+
*
|
|
28
|
+
* After:
|
|
29
|
+
*
|
|
30
|
+
* (Whole ClubController removed — synthesised by Rule 2,
|
|
31
|
+
* body was trivial, no other content remains.)
|
|
32
|
+
*
|
|
33
|
+
* Triviality criteria (ALL must hold):
|
|
34
|
+
* - `steps:` has ≤ 2 entries
|
|
35
|
+
* - No `publishes:` field, or `publishes: []`
|
|
36
|
+
* - Step text matches the operation's verb pattern:
|
|
37
|
+
* retrieve → look up / find / fetch / retrieve / select
|
|
38
|
+
* create → create / insert / store / persist
|
|
39
|
+
* update → update / set / modify
|
|
40
|
+
* delete → delete / remove
|
|
41
|
+
*
|
|
42
|
+
* Cleanup after dropping ops:
|
|
43
|
+
* - If `cured` becomes empty, delete it
|
|
44
|
+
* - If the Controller is now empty except for `model:` (i.e. we
|
|
45
|
+
* synthesised it via Rule 2 and there's nothing else), drop the
|
|
46
|
+
* whole controller. Be conservative: keep controllers that have
|
|
47
|
+
* `actions:`, `subscriptions:`, `path:`, or other content the user
|
|
48
|
+
* might have added.
|
|
49
|
+
* - If the component's controllers map is empty, delete it.
|
|
50
|
+
*
|
|
51
|
+
* NOT in scope:
|
|
52
|
+
* - Service operations (would require knowing the inference default
|
|
53
|
+
* applies, which it doesn't for filtered/predicate queries)
|
|
54
|
+
* - retrieve_many / list operations (default inference may or may not
|
|
55
|
+
* match the explicit body)
|
|
56
|
+
* - Bodies with non-trivial preconditions/postconditions (those carry
|
|
57
|
+
* real spec intent worth preserving even when steps look default)
|
|
58
|
+
*/
|
|
59
|
+
import type { NormaliseRule, NormaliseSpec } from '../normalise-rules.js';
|
|
60
|
+
interface DropMatchData {
|
|
61
|
+
componentName: string;
|
|
62
|
+
controllerName: string;
|
|
63
|
+
curvedOp: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Is the body of a cured op trivial enough to drop? Examines steps +
|
|
67
|
+
* publishes + the keyword match for the curved op.
|
|
68
|
+
*
|
|
69
|
+
* Trivial rules:
|
|
70
|
+
* - No `publishes:` (event-driven side effect = non-trivial)
|
|
71
|
+
* - ≤2 steps
|
|
72
|
+
* - 0 steps → trivial (already minimal)
|
|
73
|
+
* - 1 step → trivial as long as it doesn't contain FORBIDDEN keywords
|
|
74
|
+
* (a single step IS the operation; verb-keyword check is meaningless
|
|
75
|
+
* because LLMs often collapse "look up X / return X" into one step
|
|
76
|
+
* like "Return Club by id")
|
|
77
|
+
* - 2 steps → require at least one step matches the verb keyword AND
|
|
78
|
+
* the other matches TRIVIAL_FOLLOWUP (e.g. "Return X or null"); no
|
|
79
|
+
* step may match FORBIDDEN
|
|
80
|
+
*/
|
|
81
|
+
declare function isTrivialBody(body: unknown, curvedOp: string): boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Detect drop candidates by walking spec.components[*].controllers[*].cured[*].
|
|
84
|
+
*/
|
|
85
|
+
declare function detectDropMatches(spec: NormaliseSpec): DropMatchData[];
|
|
86
|
+
export declare const DROP_TRIVIAL_PASSTHROUGH_RULE: NormaliseRule;
|
|
87
|
+
export declare const __internal: {
|
|
88
|
+
isTrivialBody: typeof isTrivialBody;
|
|
89
|
+
detectDropMatches: typeof detectDropMatches;
|
|
90
|
+
};
|
|
91
|
+
export {};
|
|
92
|
+
//# sourceMappingURL=drop-trivial-passthrough.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drop-trivial-passthrough.d.ts","sourceRoot":"","sources":["../../../src/normalise/rules/drop-trivial-passthrough.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAEH,OAAO,KAAK,EACV,aAAa,EAGb,aAAa,EACd,MAAM,uBAAuB,CAAC;AAE/B,UAAU,aAAa;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;CAClB;AA2BD;;;;;;;;;;;;;;;GAeG;AACH,iBAAS,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAqC/D;AAED;;GAEG;AACH,iBAAS,iBAAiB,CAAC,IAAI,EAAE,aAAa,GAAG,aAAa,EAAE,CAsB/D;AAkCD,eAAO,MAAM,6BAA6B,EAAE,aAsB3C,CAAC;AAEF,eAAO,MAAM,UAAU;;;CAGtB,CAAC"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalise rule: `drop-trivial-passthrough`.
|
|
3
|
+
*
|
|
4
|
+
* Phase 4 of `2026-05-13-NORMALISE-PHASE.md` §5 Rule 3. Removes
|
|
5
|
+
* Controller.cured operations whose declared body is just the framework
|
|
6
|
+
* default — a single look-up / insert / update / delete with no
|
|
7
|
+
* preconditions, no published events, and no branching.
|
|
8
|
+
*
|
|
9
|
+
* Rationale: SpecVerse's realize phase auto-emits standard CURVED
|
|
10
|
+
* implementations for every Model. When an explicit `cured.retrieve:`
|
|
11
|
+
* block exists in the spec, realize uses it INSTEAD of the framework
|
|
12
|
+
* default. If the explicit body is semantically identical to the
|
|
13
|
+
* default ("Look up X by id, return X or null"), declaring it is
|
|
14
|
+
* noise — drop it and let inference fill in the default at realize
|
|
15
|
+
* time.
|
|
16
|
+
*
|
|
17
|
+
* Before (post-Rule-2):
|
|
18
|
+
*
|
|
19
|
+
* ClubController:
|
|
20
|
+
* model: Club
|
|
21
|
+
* cured:
|
|
22
|
+
* retrieve:
|
|
23
|
+
* steps:
|
|
24
|
+
* - Look up Club by id
|
|
25
|
+
* - Return Club or null
|
|
26
|
+
* requires: [id is provided]
|
|
27
|
+
*
|
|
28
|
+
* After:
|
|
29
|
+
*
|
|
30
|
+
* (Whole ClubController removed — synthesised by Rule 2,
|
|
31
|
+
* body was trivial, no other content remains.)
|
|
32
|
+
*
|
|
33
|
+
* Triviality criteria (ALL must hold):
|
|
34
|
+
* - `steps:` has ≤ 2 entries
|
|
35
|
+
* - No `publishes:` field, or `publishes: []`
|
|
36
|
+
* - Step text matches the operation's verb pattern:
|
|
37
|
+
* retrieve → look up / find / fetch / retrieve / select
|
|
38
|
+
* create → create / insert / store / persist
|
|
39
|
+
* update → update / set / modify
|
|
40
|
+
* delete → delete / remove
|
|
41
|
+
*
|
|
42
|
+
* Cleanup after dropping ops:
|
|
43
|
+
* - If `cured` becomes empty, delete it
|
|
44
|
+
* - If the Controller is now empty except for `model:` (i.e. we
|
|
45
|
+
* synthesised it via Rule 2 and there's nothing else), drop the
|
|
46
|
+
* whole controller. Be conservative: keep controllers that have
|
|
47
|
+
* `actions:`, `subscriptions:`, `path:`, or other content the user
|
|
48
|
+
* might have added.
|
|
49
|
+
* - If the component's controllers map is empty, delete it.
|
|
50
|
+
*
|
|
51
|
+
* NOT in scope:
|
|
52
|
+
* - Service operations (would require knowing the inference default
|
|
53
|
+
* applies, which it doesn't for filtered/predicate queries)
|
|
54
|
+
* - retrieve_many / list operations (default inference may or may not
|
|
55
|
+
* match the explicit body)
|
|
56
|
+
* - Bodies with non-trivial preconditions/postconditions (those carry
|
|
57
|
+
* real spec intent worth preserving even when steps look default)
|
|
58
|
+
*/
|
|
59
|
+
const VERB_KEYWORDS = {
|
|
60
|
+
retrieve: /\b(look\s*up|find|fetch|retrieve|select|get|read|query|load|pull)\b/i,
|
|
61
|
+
create: /\b(create|insert|store|persist|add|save)\b/i,
|
|
62
|
+
update: /\b(update|set|modify|change|patch)\b/i,
|
|
63
|
+
delete: /\b(delete|remove|destroy|drop|erase)\b/i,
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Patterns indicating that a step is a pure description of default
|
|
67
|
+
* behavior — "Return X", "Throw NotFoundError", "Yield records". Steps
|
|
68
|
+
* matching this MAY appear alongside a verb-keyword step without
|
|
69
|
+
* disqualifying the body from being trivial. Used to allow common
|
|
70
|
+
* second-step language like "Return Club or null".
|
|
71
|
+
*/
|
|
72
|
+
const TRIVIAL_FOLLOWUP_RE = /^\s*(return|throw|yield|raise)\b/i;
|
|
73
|
+
/**
|
|
74
|
+
* Patterns indicating non-default behavior — side effects, validation
|
|
75
|
+
* logic, business operations. Any step matching one of these
|
|
76
|
+
* disqualifies the body. The intent is to catch real spec content
|
|
77
|
+
* (`Validate permissions`, `Notify subscribers`, `Audit access`) that
|
|
78
|
+
* a developer wouldn't want silently discarded.
|
|
79
|
+
*/
|
|
80
|
+
const FORBIDDEN_STEP_RE = /\b(validate|notify|publish|log|audit|check|verify|ensure|calculate|compute|enrich|transform|merge|join|filter|sort|paginate)\b/i;
|
|
81
|
+
/**
|
|
82
|
+
* Is the body of a cured op trivial enough to drop? Examines steps +
|
|
83
|
+
* publishes + the keyword match for the curved op.
|
|
84
|
+
*
|
|
85
|
+
* Trivial rules:
|
|
86
|
+
* - No `publishes:` (event-driven side effect = non-trivial)
|
|
87
|
+
* - ≤2 steps
|
|
88
|
+
* - 0 steps → trivial (already minimal)
|
|
89
|
+
* - 1 step → trivial as long as it doesn't contain FORBIDDEN keywords
|
|
90
|
+
* (a single step IS the operation; verb-keyword check is meaningless
|
|
91
|
+
* because LLMs often collapse "look up X / return X" into one step
|
|
92
|
+
* like "Return Club by id")
|
|
93
|
+
* - 2 steps → require at least one step matches the verb keyword AND
|
|
94
|
+
* the other matches TRIVIAL_FOLLOWUP (e.g. "Return X or null"); no
|
|
95
|
+
* step may match FORBIDDEN
|
|
96
|
+
*/
|
|
97
|
+
function isTrivialBody(body, curvedOp) {
|
|
98
|
+
if (!body || typeof body !== 'object') {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
const b = body;
|
|
102
|
+
const publishes = b.publishes;
|
|
103
|
+
if (Array.isArray(publishes) && publishes.length > 0)
|
|
104
|
+
return false;
|
|
105
|
+
const steps = b.steps;
|
|
106
|
+
if (!steps)
|
|
107
|
+
return true;
|
|
108
|
+
if (!Array.isArray(steps))
|
|
109
|
+
return false;
|
|
110
|
+
if (steps.length > 2)
|
|
111
|
+
return false;
|
|
112
|
+
if (steps.length === 0)
|
|
113
|
+
return true;
|
|
114
|
+
const verbRe = VERB_KEYWORDS[curvedOp];
|
|
115
|
+
if (!verbRe)
|
|
116
|
+
return false;
|
|
117
|
+
// Quick guard: any step containing forbidden business-logic keywords
|
|
118
|
+
// disqualifies the body regardless of step count.
|
|
119
|
+
for (const step of steps) {
|
|
120
|
+
if (typeof step !== 'string')
|
|
121
|
+
return false;
|
|
122
|
+
if (FORBIDDEN_STEP_RE.test(step))
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
// Single-step body — trivial as long as no forbidden keywords appear.
|
|
126
|
+
if (steps.length === 1)
|
|
127
|
+
return true;
|
|
128
|
+
// Two-step body — require ≥1 verb-keyword match, others must be
|
|
129
|
+
// trivial follow-ups (Return / Throw etc).
|
|
130
|
+
let sawVerb = false;
|
|
131
|
+
for (const step of steps) {
|
|
132
|
+
const s = step;
|
|
133
|
+
if (verbRe.test(s))
|
|
134
|
+
sawVerb = true;
|
|
135
|
+
else if (TRIVIAL_FOLLOWUP_RE.test(s))
|
|
136
|
+
continue;
|
|
137
|
+
else
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return sawVerb;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Detect drop candidates by walking spec.components[*].controllers[*].cured[*].
|
|
144
|
+
*/
|
|
145
|
+
function detectDropMatches(spec) {
|
|
146
|
+
const out = [];
|
|
147
|
+
const components = spec.components;
|
|
148
|
+
if (!components)
|
|
149
|
+
return out;
|
|
150
|
+
for (const [componentName, componentBody] of Object.entries(components)) {
|
|
151
|
+
const controllers = componentBody?.controllers;
|
|
152
|
+
if (!controllers)
|
|
153
|
+
continue;
|
|
154
|
+
for (const [controllerName, controllerBody] of Object.entries(controllers)) {
|
|
155
|
+
const cured = controllerBody?.cured;
|
|
156
|
+
if (!cured)
|
|
157
|
+
continue;
|
|
158
|
+
for (const [curvedOp, body] of Object.entries(cured)) {
|
|
159
|
+
if (isTrivialBody(body, curvedOp)) {
|
|
160
|
+
out.push({ componentName, controllerName, curvedOp });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
function applyDropMatch(spec, data) {
|
|
168
|
+
const components = spec.components;
|
|
169
|
+
const component = components[data.componentName];
|
|
170
|
+
const controllers = component.controllers;
|
|
171
|
+
const controller = controllers[data.controllerName];
|
|
172
|
+
const cured = controller.cured;
|
|
173
|
+
delete cured[data.curvedOp];
|
|
174
|
+
// Clean up empty cured.
|
|
175
|
+
if (Object.keys(cured).length === 0) {
|
|
176
|
+
delete controller.cured;
|
|
177
|
+
}
|
|
178
|
+
// Drop the controller only when its remaining content is just `model:`
|
|
179
|
+
// (a synthesised shell from Rule 2). Conservatively keep controllers
|
|
180
|
+
// that have any of: actions, subscriptions, path, description, or
|
|
181
|
+
// other fields the user might have authored.
|
|
182
|
+
const remainingKeys = Object.keys(controller);
|
|
183
|
+
const isJustModelShell = remainingKeys.length === 1 && remainingKeys[0] === 'model';
|
|
184
|
+
const isEmpty = remainingKeys.length === 0;
|
|
185
|
+
if (isJustModelShell || isEmpty) {
|
|
186
|
+
delete controllers[data.controllerName];
|
|
187
|
+
if (Object.keys(controllers).length === 0) {
|
|
188
|
+
delete component.controllers;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return spec;
|
|
192
|
+
}
|
|
193
|
+
export const DROP_TRIVIAL_PASSTHROUGH_RULE = {
|
|
194
|
+
id: 'drop-trivial-passthrough',
|
|
195
|
+
summary: 'Remove explicit cured: operations whose body is just the framework default (≤2 steps, verb-keyword match, no events). Inference re-emits the default at realize time.',
|
|
196
|
+
ordering: 20,
|
|
197
|
+
enabledByDefault: true,
|
|
198
|
+
appliesWhen: (ctx) => ctx.source !== 'hand-authored',
|
|
199
|
+
detect: (spec) => {
|
|
200
|
+
const matches = [];
|
|
201
|
+
for (const data of detectDropMatches(spec)) {
|
|
202
|
+
matches.push({
|
|
203
|
+
description: `${data.componentName}.${data.controllerName}.cured.${data.curvedOp} — trivial body, drop (inference will emit framework default)`,
|
|
204
|
+
data,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return matches;
|
|
208
|
+
},
|
|
209
|
+
transform: (spec, match) => {
|
|
210
|
+
return applyDropMatch(spec, match.data);
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
export const __internal = {
|
|
214
|
+
isTrivialBody,
|
|
215
|
+
detectDropMatches,
|
|
216
|
+
};
|
|
217
|
+
//# sourceMappingURL=drop-trivial-passthrough.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drop-trivial-passthrough.js","sourceRoot":"","sources":["../../../src/normalise/rules/drop-trivial-passthrough.ts"],"names":[],"mappings":"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"}
|