@specverse/engines 6.53.1 → 6.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai/analyse-runner.d.ts.map +1 -1
- package/dist/ai/analyse-runner.js +22 -1
- package/dist/ai/analyse-runner.js.map +1 -1
- package/dist/analyse-prepass/adapters/module-functions.d.ts +25 -0
- package/dist/analyse-prepass/adapters/module-functions.d.ts.map +1 -1
- package/dist/analyse-prepass/adapters/module-functions.js +54 -0
- package/dist/analyse-prepass/adapters/module-functions.js.map +1 -1
- package/dist/analyse-prepass/backends/gitnexus.d.ts +28 -0
- package/dist/analyse-prepass/backends/gitnexus.d.ts.map +1 -1
- package/dist/analyse-prepass/backends/gitnexus.js +36 -2
- package/dist/analyse-prepass/backends/gitnexus.js.map +1 -1
- package/dist/analyse-prepass/index.d.ts.map +1 -1
- package/dist/analyse-prepass/index.js +17 -1
- package/dist/analyse-prepass/index.js.map +1 -1
- package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +29 -10
- package/dist/libs/instance-factories/services/templates/_shared/step-matching.js +11 -0
- package/dist/libs/instance-factories/services/templates/mongodb-native/step-conventions.js +39 -19
- package/dist/libs/instance-factories/services/templates/postgres-native/step-conventions.js +35 -18
- package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +16 -11
- package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +1 -1
- package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +34 -12
- package/dist/libs/instance-factories/services/templates/shared-patterns.js +5 -5
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +91 -10
- package/dist/realize/index.js.map +1 -1
- 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 +255 -0
- package/dist/realize/per-action-recovery.js.map +1 -0
- package/dist/realize/per-owner-emit.d.ts +6 -0
- package/dist/realize/per-owner-emit.d.ts.map +1 -1
- package/dist/realize/per-owner-emit.js +22 -6
- package/dist/realize/per-owner-emit.js.map +1 -1
- package/dist/realize/per-owner-runner.d.ts +23 -2
- package/dist/realize/per-owner-runner.d.ts.map +1 -1
- package/dist/realize/per-owner-runner.js +91 -46
- 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 +41 -1
- package/dist/realize/post-emit-verify/feedback-runner.d.ts.map +1 -1
- package/dist/realize/post-emit-verify/feedback-runner.js +62 -6
- package/dist/realize/post-emit-verify/feedback-runner.js.map +1 -1
- package/dist/realize/post-emit-verify/index.d.ts +4 -2
- package/dist/realize/post-emit-verify/index.d.ts.map +1 -1
- package/dist/realize/post-emit-verify/index.js +3 -1
- package/dist/realize/post-emit-verify/index.js.map +1 -1
- package/dist/realize/post-emit-verify/reemit.d.ts +22 -1
- package/dist/realize/post-emit-verify/reemit.d.ts.map +1 -1
- package/dist/realize/post-emit-verify/reemit.js +20 -18
- package/dist/realize/post-emit-verify/reemit.js.map +1 -1
- package/dist/realize/post-emit-verify/types.d.ts +49 -0
- package/dist/realize/post-emit-verify/types.d.ts.map +1 -1
- package/dist/realize/post-emit-verify/verifier-manifest.d.ts.map +1 -1
- package/dist/realize/post-emit-verify/verifier-manifest.js +2 -0
- package/dist/realize/post-emit-verify/verifier-manifest.js.map +1 -1
- package/dist/realize/post-emit-verify/verifiers/stub-completeness.d.ts +127 -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 +423 -0
- package/dist/realize/post-emit-verify/verifiers/stub-completeness.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/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/controllers/templates/fastify/routes-generator.ts +49 -15
- package/libs/instance-factories/services/templates/_shared/step-matching.ts +43 -0
- package/libs/instance-factories/services/templates/mongodb-native/step-conventions.ts +39 -19
- package/libs/instance-factories/services/templates/postgres-native/step-conventions.ts +35 -18
- package/libs/instance-factories/services/templates/prisma/__tests__/step-conventions-create.test.ts +184 -0
- package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +34 -5
- package/libs/instance-factories/services/templates/prisma/controller-generator.ts +6 -1
- package/libs/instance-factories/services/templates/prisma/step-conventions.ts +34 -12
- package/libs/instance-factories/services/templates/shared-patterns.ts +20 -10
- package/package.json +1 -1
- package/libs/instance-factories/services/templates/_shared/step-matching.d.ts +0 -39
- package/libs/instance-factories/services/templates/_shared/step-matching.d.ts.map +0 -1
- package/libs/instance-factories/services/templates/_shared/step-matching.js +0 -90
- package/libs/instance-factories/services/templates/_shared/step-matching.js.map +0 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-owner realize-context snapshot.
|
|
3
|
+
*
|
|
4
|
+
* Phase 3 of 2026-05-13-VERIFIER-DIAGNOSTIC-TREATMENT-SPLIT.
|
|
5
|
+
*
|
|
6
|
+
* The per-owner emit prompt has TARGET RUNTIME, [PRE-BAKED] step bullets,
|
|
7
|
+
* cross-service operation signatures, available capabilities, library
|
|
8
|
+
* whitelist — everything the LLM needs to emit a real action body. The
|
|
9
|
+
* post-emit feedback runner's `buildLlmReemit` historically had NONE of
|
|
10
|
+
* this; the reemit prompt provided only the previous bad output + the
|
|
11
|
+
* verifier errors.
|
|
12
|
+
*
|
|
13
|
+
* Empirical consequence: sonnet still bridged the gap via inference;
|
|
14
|
+
* smaller models (Ollama qwen3-coder:30b, MarrBox) drifted further from
|
|
15
|
+
* the realize-emit skill's contract, introducing cross-file tsc
|
|
16
|
+
* regressions when asked to fix STUB002 stubs (measured 2026-05-13 at
|
|
17
|
+
* engines 6.54.0 on idle-meta).
|
|
18
|
+
*
|
|
19
|
+
* Phase 3 closes the gap by SNAPSHOTTING the per-owner context to disk
|
|
20
|
+
* at emit time and READING it at reemit time. The snapshot lives at
|
|
21
|
+
* `<outputDir>/.realize-context/<OwnerName>.md` and contains a compact
|
|
22
|
+
* markdown blob — directly embeddable in the reemit user prompt.
|
|
23
|
+
*
|
|
24
|
+
* Decoupling: the snapshot writer (called from per-owner-runner)
|
|
25
|
+
* doesn't know about reemit; the snapshot loader (called from reemit
|
|
26
|
+
* factory) doesn't know about per-owner-emit. They share only the
|
|
27
|
+
* file layout convention.
|
|
28
|
+
*/
|
|
29
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
30
|
+
import { dirname, join } from 'path';
|
|
31
|
+
const CONTEXT_DIR_NAME = '.realize-context';
|
|
32
|
+
/**
|
|
33
|
+
* Render the context as a compact markdown blob suitable for direct
|
|
34
|
+
* embedding in the reemit prompt body. Mirrors the section headings
|
|
35
|
+
* used in the per-owner-emit prompt template so the LLM sees the same
|
|
36
|
+
* structural cues.
|
|
37
|
+
*/
|
|
38
|
+
export function formatContextSnapshot(ctx) {
|
|
39
|
+
const lines = [];
|
|
40
|
+
lines.push(`# Realize context for owner: ${ctx.ownerName}`);
|
|
41
|
+
lines.push('');
|
|
42
|
+
lines.push(`## TARGET RUNTIME`);
|
|
43
|
+
lines.push(ctx.targetRuntime);
|
|
44
|
+
lines.push('');
|
|
45
|
+
lines.push(`## AVAILABLE SPEC SURFACE`);
|
|
46
|
+
lines.push(ctx.availableSpecSurface);
|
|
47
|
+
lines.push('');
|
|
48
|
+
lines.push(`## AVAILABLE CAPABILITIES`);
|
|
49
|
+
lines.push(ctx.availableCapabilities);
|
|
50
|
+
lines.push('');
|
|
51
|
+
lines.push(`## AVAILABLE CLIENTS`);
|
|
52
|
+
lines.push(ctx.availableClients);
|
|
53
|
+
lines.push('');
|
|
54
|
+
lines.push(`## ACTIONS`);
|
|
55
|
+
lines.push(ctx.perActionScaffolding);
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
58
|
+
/** Resolve the per-owner snapshot path. Owner names are
|
|
59
|
+
* always TypeScript identifiers, so no path-sanitization concerns. */
|
|
60
|
+
function snapshotPath(outputDir, ownerName) {
|
|
61
|
+
return join(outputDir, CONTEXT_DIR_NAME, `${ownerName}.md`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Persist a context snapshot to disk. Best-effort — caller should
|
|
65
|
+
* never crash on snapshot failure (the reemit prompt degrades to the
|
|
66
|
+
* pre-Phase-3 behavior cleanly).
|
|
67
|
+
*/
|
|
68
|
+
export function writeContextSnapshot(outputDir, ctx) {
|
|
69
|
+
try {
|
|
70
|
+
const path = snapshotPath(outputDir, ctx.ownerName);
|
|
71
|
+
const dir = dirname(path);
|
|
72
|
+
if (!existsSync(dir))
|
|
73
|
+
mkdirSync(dir, { recursive: true });
|
|
74
|
+
writeFileSync(path, formatContextSnapshot(ctx));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// best-effort; reemit falls back to the Phase 1/2 prompt shape
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Load a previously-written snapshot. Returns the raw markdown blob,
|
|
82
|
+
* or `null` if no snapshot exists for the owner. Callers (the reemit
|
|
83
|
+
* factory) include the blob in the prompt body when present.
|
|
84
|
+
*/
|
|
85
|
+
export function loadContextSnapshot(outputDir, ownerName) {
|
|
86
|
+
try {
|
|
87
|
+
const path = snapshotPath(outputDir, ownerName);
|
|
88
|
+
if (!existsSync(path))
|
|
89
|
+
return null;
|
|
90
|
+
return readFileSync(path, 'utf8');
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=realize-context-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realize-context-snapshot.js","sourceRoot":"","sources":["../../src/realize/realize-context-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAErC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAyB5C;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAwB;IAC5D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,gCAAgC,GAAG,CAAC,SAAS,EAAE,CAAC,CAAC;IAC5D,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;IAChC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAC9B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;IACxC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IACrC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;IACxC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IACtC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;IACnC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACjC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACzB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IACrC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED;uEACuE;AACvE,SAAS,YAAY,CAAC,SAAiB,EAAE,SAAiB;IACxD,OAAO,IAAI,CAAC,SAAS,EAAE,gBAAgB,EAAE,GAAG,SAAS,KAAK,CAAC,CAAC;AAC9D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,SAAiB,EACjB,GAAwB;IAExB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,YAAY,CAAC,SAAS,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;QACpD,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1D,aAAa,CAAC,IAAI,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAC,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,+DAA+D;IACjE,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,SAAiB,EACjB,SAAiB;IAEjB,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,YAAY,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QAChD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QACnC,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -39,9 +39,17 @@ export interface PrebakedSnippet {
|
|
|
39
39
|
/** The pre-baked TypeScript snippet. The matcher's `.call` string. */
|
|
40
40
|
snippet: string;
|
|
41
41
|
}
|
|
42
|
-
/** Outcome of the structural-preservation check.
|
|
42
|
+
/** Outcome of the structural-preservation check.
|
|
43
|
+
*
|
|
44
|
+
* `ok: true` with a non-empty `warnings` array means the LLM dropped
|
|
45
|
+
* SOME pre-baked snippets but stayed at or under the drop-fraction
|
|
46
|
+
* threshold — the output is accepted (no γ-fallback) but the dropped
|
|
47
|
+
* snippets are surfaced so the caller can log them. `ok: false` means
|
|
48
|
+
* the drop fraction exceeded the threshold: the LLM substantially
|
|
49
|
+
* ignored the scaffolding and the owner should γ-fall-back. */
|
|
43
50
|
export type StructuralValidationResult = {
|
|
44
51
|
ok: true;
|
|
52
|
+
warnings?: MissingSnippet[];
|
|
45
53
|
} | {
|
|
46
54
|
ok: false;
|
|
47
55
|
missing: MissingSnippet[];
|
|
@@ -56,6 +64,27 @@ export interface MissingSnippet {
|
|
|
56
64
|
/** The closest match found in the LLM body, or null when no near-match. */
|
|
57
65
|
closestMatch: string | null;
|
|
58
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Default drop-fraction threshold: an owner may drop up to (and
|
|
69
|
+
* including) HALF of its pre-baked snippets and still be accepted
|
|
70
|
+
* (with the drops surfaced as warnings). Above half → γ-fallback.
|
|
71
|
+
*
|
|
72
|
+
* Rationale (2026-05-14, Phase 5 first target of
|
|
73
|
+
* `2026-05-13-VERIFIER-DIAGNOSTIC-TREATMENT-SPLIT.md`): pre-fix the
|
|
74
|
+
* validator rejected an ENTIRE owner if it dropped a SINGLE snippet.
|
|
75
|
+
* For sonnet this rarely fires; for openai-compatible models (Ollama
|
|
76
|
+
* qwen3-coder:30b, MarrBox) it fired on ~13/36 owners in the idle-meta
|
|
77
|
+
* trial — every paraphrased snippet nuked the whole owner to γ-stub.
|
|
78
|
+
* The post-emit feedback loop then recovered those owners anyway via
|
|
79
|
+
* context-complete reemit (which does NOT run this validator), so the
|
|
80
|
+
* strictness bought an extra LLM round-trip and STILL lost determinism.
|
|
81
|
+
* A threshold keeps the gross-failure guard ("the LLM basically
|
|
82
|
+
* ignored the scaffolding") while letting a minority of paraphrases
|
|
83
|
+
* through — they ride the normal tsc + stub-completeness gates instead.
|
|
84
|
+
*
|
|
85
|
+
* Overridable via `SPECVERSE_REALIZE_STRUCTURAL_DROP_THRESHOLD`.
|
|
86
|
+
*/
|
|
87
|
+
export declare const DEFAULT_STRUCTURAL_DROP_THRESHOLD = 0.5;
|
|
59
88
|
/**
|
|
60
89
|
* Validate that every pre-baked snippet appears in the LLM-emitted body.
|
|
61
90
|
*
|
|
@@ -64,8 +93,13 @@ export interface MissingSnippet {
|
|
|
64
93
|
* step number, step text, and the closest near-match found in the body
|
|
65
94
|
* (or `null` if there's no token-overlap candidate).
|
|
66
95
|
*
|
|
96
|
+
* `dropThreshold` is the fraction of countable snippets that may be
|
|
97
|
+
* dropped before the result flips to `ok: false`. At or below the
|
|
98
|
+
* threshold, the result is `ok: true` with the dropped snippets in
|
|
99
|
+
* `warnings`. Above it, `ok: false` with the full `missing` list.
|
|
100
|
+
*
|
|
67
101
|
* Empty `prebakedSnippets` is a vacuous pass — when no convention
|
|
68
102
|
* matched, there's nothing to preserve.
|
|
69
103
|
*/
|
|
70
|
-
export declare function validateLlmOutputPreservesConventions(llmBody: string, prebakedSnippets: PrebakedSnippet[]): StructuralValidationResult;
|
|
104
|
+
export declare function validateLlmOutputPreservesConventions(llmBody: string, prebakedSnippets: PrebakedSnippet[], dropThreshold?: number): StructuralValidationResult;
|
|
71
105
|
//# sourceMappingURL=structural-validator.d.ts.map
|
|
@@ -1 +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
|
|
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;;;;;;;gEAOgE;AAChE,MAAM,MAAM,0BAA0B,GAClC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAA;CAAE,GACzC;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;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,iCAAiC,MAAM,CAAC;AAErD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qCAAqC,CACnD,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,eAAe,EAAE,EACnC,aAAa,GAAE,MAA0C,GACxD,0BAA0B,CA8D5B"}
|
|
@@ -50,6 +50,27 @@ function canonicalise(code) {
|
|
|
50
50
|
.replace(/\s+/g, ' ')
|
|
51
51
|
.trim();
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Default drop-fraction threshold: an owner may drop up to (and
|
|
55
|
+
* including) HALF of its pre-baked snippets and still be accepted
|
|
56
|
+
* (with the drops surfaced as warnings). Above half → γ-fallback.
|
|
57
|
+
*
|
|
58
|
+
* Rationale (2026-05-14, Phase 5 first target of
|
|
59
|
+
* `2026-05-13-VERIFIER-DIAGNOSTIC-TREATMENT-SPLIT.md`): pre-fix the
|
|
60
|
+
* validator rejected an ENTIRE owner if it dropped a SINGLE snippet.
|
|
61
|
+
* For sonnet this rarely fires; for openai-compatible models (Ollama
|
|
62
|
+
* qwen3-coder:30b, MarrBox) it fired on ~13/36 owners in the idle-meta
|
|
63
|
+
* trial — every paraphrased snippet nuked the whole owner to γ-stub.
|
|
64
|
+
* The post-emit feedback loop then recovered those owners anyway via
|
|
65
|
+
* context-complete reemit (which does NOT run this validator), so the
|
|
66
|
+
* strictness bought an extra LLM round-trip and STILL lost determinism.
|
|
67
|
+
* A threshold keeps the gross-failure guard ("the LLM basically
|
|
68
|
+
* ignored the scaffolding") while letting a minority of paraphrases
|
|
69
|
+
* through — they ride the normal tsc + stub-completeness gates instead.
|
|
70
|
+
*
|
|
71
|
+
* Overridable via `SPECVERSE_REALIZE_STRUCTURAL_DROP_THRESHOLD`.
|
|
72
|
+
*/
|
|
73
|
+
export const DEFAULT_STRUCTURAL_DROP_THRESHOLD = 0.5;
|
|
53
74
|
/**
|
|
54
75
|
* Validate that every pre-baked snippet appears in the LLM-emitted body.
|
|
55
76
|
*
|
|
@@ -58,19 +79,29 @@ function canonicalise(code) {
|
|
|
58
79
|
* step number, step text, and the closest near-match found in the body
|
|
59
80
|
* (or `null` if there's no token-overlap candidate).
|
|
60
81
|
*
|
|
82
|
+
* `dropThreshold` is the fraction of countable snippets that may be
|
|
83
|
+
* dropped before the result flips to `ok: false`. At or below the
|
|
84
|
+
* threshold, the result is `ok: true` with the dropped snippets in
|
|
85
|
+
* `warnings`. Above it, `ok: false` with the full `missing` list.
|
|
86
|
+
*
|
|
61
87
|
* Empty `prebakedSnippets` is a vacuous pass — when no convention
|
|
62
88
|
* matched, there's nothing to preserve.
|
|
63
89
|
*/
|
|
64
|
-
export function validateLlmOutputPreservesConventions(llmBody, prebakedSnippets) {
|
|
90
|
+
export function validateLlmOutputPreservesConventions(llmBody, prebakedSnippets, dropThreshold = DEFAULT_STRUCTURAL_DROP_THRESHOLD) {
|
|
65
91
|
if (prebakedSnippets.length === 0) {
|
|
66
92
|
return { ok: true };
|
|
67
93
|
}
|
|
68
94
|
const llmCanonical = canonicalise(llmBody);
|
|
69
95
|
const missing = [];
|
|
96
|
+
// Countable = snippets with a non-empty canonical form. Comment-only
|
|
97
|
+
// snippets are skipped (they're vacuously "preserved") and must NOT
|
|
98
|
+
// inflate the denominator, or the drop fraction would understate.
|
|
99
|
+
let countable = 0;
|
|
70
100
|
for (const snippet of prebakedSnippets) {
|
|
71
101
|
const expected = canonicalise(snippet.snippet);
|
|
72
102
|
if (expected.length === 0)
|
|
73
103
|
continue; // skip comment-only snippets
|
|
104
|
+
countable++;
|
|
74
105
|
if (llmCanonical.includes(expected)) {
|
|
75
106
|
continue;
|
|
76
107
|
}
|
|
@@ -103,7 +134,14 @@ export function validateLlmOutputPreservesConventions(llmBody, prebakedSnippets)
|
|
|
103
134
|
if (missing.length === 0) {
|
|
104
135
|
return { ok: true };
|
|
105
136
|
}
|
|
106
|
-
|
|
137
|
+
// Below/at threshold → accept with warnings; above → γ-fallback.
|
|
138
|
+
// countable is guaranteed > 0 here (missing is non-empty, and every
|
|
139
|
+
// entry in missing incremented countable).
|
|
140
|
+
const dropFraction = missing.length / countable;
|
|
141
|
+
if (dropFraction <= dropThreshold) {
|
|
142
|
+
return { ok: true, warnings: missing };
|
|
143
|
+
}
|
|
144
|
+
const reason = formatMissingReport(missing, countable, dropThreshold);
|
|
107
145
|
return { ok: false, missing, reason };
|
|
108
146
|
}
|
|
109
147
|
/**
|
|
@@ -143,11 +181,14 @@ function extractDistinctiveTokens(snippet) {
|
|
|
143
181
|
}
|
|
144
182
|
/**
|
|
145
183
|
* Format a human-readable diff report for the realize pipeline to print
|
|
146
|
-
* when the structural validator rejects an LLM output
|
|
184
|
+
* when the structural validator rejects an LLM output (drop fraction
|
|
185
|
+
* exceeded the threshold).
|
|
147
186
|
*/
|
|
148
|
-
function formatMissingReport(missing) {
|
|
187
|
+
function formatMissingReport(missing, countable, dropThreshold) {
|
|
149
188
|
const lines = [];
|
|
150
|
-
|
|
189
|
+
const pct = Math.round((missing.length / countable) * 100);
|
|
190
|
+
lines.push(`LLM output dropped or rewrote ${missing.length}/${countable} pre-baked convention snippet${countable === 1 ? '' : 's'} ` +
|
|
191
|
+
`(${pct}%, over the ${Math.round(dropThreshold * 100)}% threshold):`);
|
|
151
192
|
for (const m of missing) {
|
|
152
193
|
lines.push('');
|
|
153
194
|
lines.push(` Step ${m.stepNum}: ${m.stepText}`);
|
|
@@ -160,8 +201,10 @@ function formatMissingReport(missing) {
|
|
|
160
201
|
}
|
|
161
202
|
}
|
|
162
203
|
lines.push('');
|
|
163
|
-
lines.push('The convention matcher is deterministic; pre-baked snippets
|
|
164
|
-
'
|
|
204
|
+
lines.push('The convention matcher is deterministic; pre-baked snippets should be preserved verbatim. ' +
|
|
205
|
+
'A minority of drops is tolerated (see SPECVERSE_REALIZE_STRUCTURAL_DROP_THRESHOLD); this owner ' +
|
|
206
|
+
'exceeded that. If you believe the matcher is wrong for this case, fix the convention pattern ' +
|
|
207
|
+
'in step-conventions.ts.');
|
|
165
208
|
return lines.join('\n');
|
|
166
209
|
}
|
|
167
210
|
//# sourceMappingURL=structural-validator.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"structural-validator.js","sourceRoot":"","sources":["../../src/realize/structural-validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;
|
|
1
|
+
{"version":3,"file":"structural-validator.js","sourceRoot":"","sources":["../../src/realize/structural-validator.ts"],"names":[],"mappings":"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"}
|
|
@@ -25,8 +25,10 @@ export default function generateFastifyRoutes(context: TemplateContext): string
|
|
|
25
25
|
const isModelController = !!modelName;
|
|
26
26
|
const handlerName = isModelController ? `${modelName}Controller` : controllerName;
|
|
27
27
|
|
|
28
|
-
// Generate imports
|
|
29
|
-
|
|
28
|
+
// Generate imports — defer until after handlers are built so we
|
|
29
|
+
// know whether FastifyRequest/FastifyReply are referenced.
|
|
30
|
+
// (Variable populated below.)
|
|
31
|
+
let imports: string;
|
|
30
32
|
|
|
31
33
|
// Convert CURED operations to endpoints if endpoints don't exist
|
|
32
34
|
let endpoints = controller.endpoints;
|
|
@@ -85,13 +87,30 @@ export default function generateFastifyRoutes(context: TemplateContext): string
|
|
|
85
87
|
.map((endpoint: any) => generateRouteHandler(endpoint, modelName, handlerName, isModelController, implType, controllerName))
|
|
86
88
|
.join('\n\n');
|
|
87
89
|
|
|
88
|
-
|
|
90
|
+
// When there are no internal handlers, suppress unused-param TS6133 by
|
|
91
|
+
// prefixing `fastify`/`options` with `_` and skip the `handler` decl
|
|
92
|
+
// entirely. Same for the external-routes export.
|
|
93
|
+
const hasInternalHandlers = internalEndpoints.length > 0;
|
|
94
|
+
const hasExternalHandlers = externalEndpoints.length > 0;
|
|
95
|
+
|
|
96
|
+
// Determine which fastify types the emitted body actually references.
|
|
97
|
+
// FastifyInstance is always used (function signatures). FastifyRequest +
|
|
98
|
+
// FastifyReply only appear inside individual route handlers.
|
|
99
|
+
const handlersText = internalHandlers + '\n' + externalHandlers;
|
|
100
|
+
const usesFastifyRequest = /\bFastifyRequest\b/.test(handlersText);
|
|
101
|
+
const usesFastifyReply = /\bFastifyReply\b/.test(handlersText);
|
|
102
|
+
imports = generateImports(
|
|
103
|
+
controller, modelName, handlerName, isModelController, implType,
|
|
104
|
+
{ usesFastifyRequest, usesFastifyReply },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const externalRoutesExport = hasExternalHandlers ? `
|
|
89
108
|
/**
|
|
90
109
|
* Mount the external (root-prefix) routes for this controller.
|
|
91
110
|
*
|
|
92
111
|
* Called by the generated main.ts at root scope (no prefix) so action.path
|
|
93
112
|
* declarations like '/api/v2/auth/register' land at exactly that URL,
|
|
94
|
-
* bypassing the controller's '/api/<
|
|
113
|
+
* bypassing the controller's '/api/<prefix>' prefix.
|
|
95
114
|
*/
|
|
96
115
|
export async function registerExternalRoutes(
|
|
97
116
|
fastify: FastifyInstance,
|
|
@@ -103,6 +122,15 @@ ${externalHandlers.split('\n').map(line => ' ' + line).join('\n')}
|
|
|
103
122
|
}
|
|
104
123
|
` : '';
|
|
105
124
|
|
|
125
|
+
// Body fragments — only declare `handler` when there are handlers to
|
|
126
|
+
// reference it; suppress unused-param TS6133 by underscore-prefixing
|
|
127
|
+
// when the function body is empty.
|
|
128
|
+
const mainBody = hasInternalHandlers
|
|
129
|
+
? ` const handler = ${isModelController ? 'options.controllers' : 'options.services'}.${handlerName};\n\n${internalHandlers.split('\n').map(line => ' ' + line).join('\n')}`
|
|
130
|
+
: ' // No endpoints declared on this controller — empty routes plugin.';
|
|
131
|
+
const mainFastifyParam = hasInternalHandlers ? 'fastify' : '_fastify';
|
|
132
|
+
const mainOptionsParam = hasInternalHandlers ? 'options' : '_options';
|
|
133
|
+
|
|
106
134
|
// Generate the complete route file
|
|
107
135
|
return `${imports}
|
|
108
136
|
|
|
@@ -114,28 +142,34 @@ ${externalHandlers.split('\n').map(line => ' ' + line).join('\n')}
|
|
|
114
142
|
* Operations: ${controller.endpoints?.map((e: any) => e.operation).join(', ') || 'CURED'}
|
|
115
143
|
*/
|
|
116
144
|
export default async function ${routeName.replace('Controller', '')}Routes(
|
|
117
|
-
|
|
118
|
-
|
|
145
|
+
${mainFastifyParam}: FastifyInstance,
|
|
146
|
+
${mainOptionsParam}: any
|
|
119
147
|
) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
${internalHandlers.split('\n').map(line => ' ' + line).join('\n')}
|
|
148
|
+
${mainBody}
|
|
123
149
|
}
|
|
124
150
|
${externalRoutesExport}`;
|
|
125
151
|
}
|
|
126
152
|
|
|
127
153
|
/**
|
|
128
|
-
* Generate imports for the route file
|
|
154
|
+
* Generate imports for the route file. The fastify type imports are
|
|
155
|
+
* gated on actual handler usage to avoid unused-import TS6133 on
|
|
156
|
+
* empty-routes plugins (e.g. backend-only ProductController without
|
|
157
|
+
* any CURVED ops or custom actions).
|
|
129
158
|
*/
|
|
130
159
|
function generateImports(
|
|
131
|
-
|
|
160
|
+
_controller: any,
|
|
132
161
|
modelName: string,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
implType: any
|
|
162
|
+
_handlerName: string,
|
|
163
|
+
_isModelController: boolean,
|
|
164
|
+
implType: any,
|
|
165
|
+
usage: { usesFastifyRequest: boolean; usesFastifyReply: boolean } =
|
|
166
|
+
{ usesFastifyRequest: true, usesFastifyReply: true },
|
|
136
167
|
): string {
|
|
168
|
+
const fastifyTypes = ['FastifyInstance'];
|
|
169
|
+
if (usage.usesFastifyRequest) fastifyTypes.push('FastifyRequest');
|
|
170
|
+
if (usage.usesFastifyReply) fastifyTypes.push('FastifyReply');
|
|
137
171
|
const imports = [
|
|
138
|
-
`import {
|
|
172
|
+
`import { ${fastifyTypes.join(', ')} } from 'fastify';`,
|
|
139
173
|
];
|
|
140
174
|
|
|
141
175
|
// Don't import controller/service - they're passed via options
|
|
@@ -30,6 +30,49 @@ export function toVar(name: string): string {
|
|
|
30
30
|
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Common English adjectives/articles that can leak into entity-noun
|
|
35
|
+
* extraction from NL spec steps. The matcher's pattern fragments use
|
|
36
|
+
* this to skip a leading adjective when capturing the entity noun, so
|
|
37
|
+
* "Create new Player with…" extracts `Player`, not `new`.
|
|
38
|
+
*
|
|
39
|
+
* Concern-separation note: this is NL-input-side. The TS-output-side
|
|
40
|
+
* counterpart is `TS_RESERVED_WORDS` from `@specverse/types`. Don't
|
|
41
|
+
* merge the two lists — they have distinct consumers and tuning
|
|
42
|
+
* lifecycles. NL adjectives are open-ended (extend as we see leakage);
|
|
43
|
+
* reserved identifiers are fixed by the target language spec.
|
|
44
|
+
*/
|
|
45
|
+
export const NL_LEADING_ADJECTIVES: readonly string[] = [
|
|
46
|
+
'new', 'existing', 'current', 'the', 'a', 'an',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Regex fragment for the optional leading-adjective skip, e.g.
|
|
51
|
+
* pattern: new RegExp(`^create\\s+${NL_ADJECTIVE_PREFIX}(\\w+)…`, 'i')
|
|
52
|
+
*
|
|
53
|
+
* Patterns that prefer regex literals can inline the equivalent group:
|
|
54
|
+
* /^create\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)…/i
|
|
55
|
+
* — keep this in sync if you do.
|
|
56
|
+
*/
|
|
57
|
+
export const NL_ADJECTIVE_PREFIX = `(?:(?:${NL_LEADING_ADJECTIVES.join('|')})\\s+)?`;
|
|
58
|
+
|
|
59
|
+
// Safe-identifier primitives live in `@specverse/types`:
|
|
60
|
+
// - `safeEntityName(name)` → returns the lowerCamel identifier, or
|
|
61
|
+
// null when the name would emit a TS
|
|
62
|
+
// reserved word as an entity reference
|
|
63
|
+
// (`prisma.<X>.create`). Use this in
|
|
64
|
+
// step-conventions' `generateCall` to
|
|
65
|
+
// replace the toVar+predicate pair.
|
|
66
|
+
// - `safeFunctionName(name)` → returns `{ name, reserved }`; on
|
|
67
|
+
// reserved input, name is suffix-renamed
|
|
68
|
+
// (`delete` → `delete_`) and the caller
|
|
69
|
+
// emits a re-export so consumers
|
|
70
|
+
// importing the original name still
|
|
71
|
+
// resolve. Used by the γ-stub renderers.
|
|
72
|
+
// Import directly from `@specverse/types`; this file no longer
|
|
73
|
+
// re-exports the predicate (was `isUnsafeEntityName`, retired in favour
|
|
74
|
+
// of the more expressive primitives).
|
|
75
|
+
|
|
33
76
|
// #54 (2026-05-10): SharedStepContext, SharedConvention, MatchResult
|
|
34
77
|
// and MatcherFn are now canonical in @specverse/types. Re-exported here
|
|
35
78
|
// so existing dynamic-import call sites that grab types from this path
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type SharedConvention,
|
|
22
22
|
type SharedStepContext,
|
|
23
23
|
} from '../_shared/step-matching.js';
|
|
24
|
+
import { safeEntityName } from '@specverse/types';
|
|
24
25
|
|
|
25
26
|
export type MongoStepConvention = SharedConvention<MongoStepContext>;
|
|
26
27
|
|
|
@@ -178,7 +179,8 @@ export const MONGO_STEP_CONVENTIONS: MongoStepConvention[] = [
|
|
|
178
179
|
generateCall: (m, ctx) => {
|
|
179
180
|
const model = m[1];
|
|
180
181
|
const field = m[2];
|
|
181
|
-
const modelVar =
|
|
182
|
+
const modelVar = safeEntityName(model);
|
|
183
|
+
if (!modelVar) return '';
|
|
182
184
|
const collection = toCollection(model);
|
|
183
185
|
const params = ctx.parameterNames || [];
|
|
184
186
|
const declared = ctx.declaredVars || new Set();
|
|
@@ -212,7 +214,8 @@ export const MONGO_STEP_CONVENTIONS: MongoStepConvention[] = [
|
|
|
212
214
|
const model = m[1];
|
|
213
215
|
const f1 = m[2];
|
|
214
216
|
const f2 = m[3];
|
|
215
|
-
const modelVar =
|
|
217
|
+
const modelVar = safeEntityName(model);
|
|
218
|
+
if (!modelVar) return '';
|
|
216
219
|
const collection = toCollection(model);
|
|
217
220
|
const declared = ctx.declaredVars || new Set();
|
|
218
221
|
if (declared.has(modelVar)) {
|
|
@@ -246,13 +249,19 @@ export const MONGO_STEP_CONVENTIONS: MongoStepConvention[] = [
|
|
|
246
249
|
},
|
|
247
250
|
|
|
248
251
|
// --- Create model record ---
|
|
249
|
-
// Matches: "Create X", "Create new X with ..."
|
|
252
|
+
// Matches: "Create X", "Create new X with ...", "Create existing X with ...".
|
|
253
|
+
// Leading-adjective skip kept in sync with NL_LEADING_ADJECTIVES in
|
|
254
|
+
// _shared/step-matching.ts. Reserved-word safety net via
|
|
255
|
+
// safeEntityName from @specverse/types — returns null when the extracted noun would emit a TS reserved word
|
|
256
|
+
// (e.g. mis-parsed adjectives) by returning '' so the matcher falls
|
|
257
|
+
// through to the AI [WRITE] fallback.
|
|
250
258
|
{
|
|
251
259
|
name: 'create',
|
|
252
|
-
pattern: /^create\s+(?:new\s+)?(\w+)(?:\s+(?:with\s+)?(.+))?/i,
|
|
260
|
+
pattern: /^create\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)(?:\s+(?:with\s+)?(.+))?/i,
|
|
253
261
|
generateCall: (m, ctx) => {
|
|
254
262
|
const model = m[1];
|
|
255
|
-
const modelVar =
|
|
263
|
+
const modelVar = safeEntityName(model);
|
|
264
|
+
if (!modelVar) return '';
|
|
256
265
|
const collection = toCollection(model);
|
|
257
266
|
const params = ctx.parameterNames || [];
|
|
258
267
|
const declared = ctx.declaredVars || new Set();
|
|
@@ -272,12 +281,13 @@ export const MONGO_STEP_CONVENTIONS: MongoStepConvention[] = [
|
|
|
272
281
|
// lets the AI layer handle it.
|
|
273
282
|
{
|
|
274
283
|
name: 'update-field',
|
|
275
|
-
pattern: /^update\s+(\w+)\s+(\w+)\s+to\s+(.+)/i,
|
|
284
|
+
pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+(\w+)\s+to\s+(.+)/i,
|
|
276
285
|
generateCall: (m, ctx) => {
|
|
277
286
|
const model = m[1];
|
|
287
|
+
const modelVar = safeEntityName(model);
|
|
288
|
+
if (!modelVar) return '';
|
|
278
289
|
const field = m[2];
|
|
279
290
|
const rawValue = m[3];
|
|
280
|
-
const modelVar = toVar(model);
|
|
281
291
|
if (!ctx.declaredVars?.has(modelVar)) return ''; // signal "not really matched"
|
|
282
292
|
const collection = toCollection(model);
|
|
283
293
|
const val = resolveValue(rawValue, ctx);
|
|
@@ -292,11 +302,12 @@ export const MONGO_STEP_CONVENTIONS: MongoStepConvention[] = [
|
|
|
292
302
|
// --- Update field timestamp (e.g. "Update device lastLoginAt timestamp") ---
|
|
293
303
|
{
|
|
294
304
|
name: 'update-field-timestamp',
|
|
295
|
-
pattern: /^update\s+(\w+)\s+(\w+)\s+timestamp$/i,
|
|
305
|
+
pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)\s+(\w+)\s+timestamp$/i,
|
|
296
306
|
generateCall: (m, ctx) => {
|
|
297
307
|
const model = m[1];
|
|
308
|
+
const modelVar = safeEntityName(model);
|
|
309
|
+
if (!modelVar) return '';
|
|
298
310
|
const field = m[2];
|
|
299
|
-
const modelVar = toVar(model);
|
|
300
311
|
if (!ctx.declaredVars?.has(modelVar)) return '';
|
|
301
312
|
const collection = toCollection(model);
|
|
302
313
|
return ` // Step ${ctx.stepNum}: Update ${model}.${field} timestamp
|
|
@@ -310,10 +321,11 @@ export const MONGO_STEP_CONVENTIONS: MongoStepConvention[] = [
|
|
|
310
321
|
// --- Generic "Update X" (writes args back to record) ---
|
|
311
322
|
{
|
|
312
323
|
name: 'update',
|
|
313
|
-
pattern: /^update\s+(\w+)(?:\s+(.+))?$/i,
|
|
324
|
+
pattern: /^update\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)(?:\s+(.+))?$/i,
|
|
314
325
|
generateCall: (m, ctx) => {
|
|
315
326
|
const model = m[1];
|
|
316
|
-
const modelVar =
|
|
327
|
+
const modelVar = safeEntityName(model);
|
|
328
|
+
if (!modelVar) return '';
|
|
317
329
|
if (!ctx.declaredVars?.has(modelVar)) return '';
|
|
318
330
|
const collection = toCollection(model);
|
|
319
331
|
return ` // Step ${ctx.stepNum}: Update ${model}
|
|
@@ -327,10 +339,11 @@ export const MONGO_STEP_CONVENTIONS: MongoStepConvention[] = [
|
|
|
327
339
|
// --- Delete model record ---
|
|
328
340
|
{
|
|
329
341
|
name: 'delete',
|
|
330
|
-
pattern: /^delete\s+(\w+)/i,
|
|
342
|
+
pattern: /^delete\s+(?:(?:new|existing|current|the|a|an)\s+)?(\w+)/i,
|
|
331
343
|
generateCall: (m, ctx) => {
|
|
332
344
|
const model = m[1];
|
|
333
|
-
const modelVar =
|
|
345
|
+
const modelVar = safeEntityName(model);
|
|
346
|
+
if (!modelVar) return '';
|
|
334
347
|
if (!ctx.declaredVars?.has(modelVar)) return '';
|
|
335
348
|
const collection = toCollection(model);
|
|
336
349
|
return ` // Step ${ctx.stepNum}: Delete ${model}
|
|
@@ -349,7 +362,8 @@ export const MONGO_STEP_CONVENTIONS: MongoStepConvention[] = [
|
|
|
349
362
|
generateCall: (m, ctx) => {
|
|
350
363
|
const model = m[1];
|
|
351
364
|
const state = m[2];
|
|
352
|
-
const modelVar =
|
|
365
|
+
const modelVar = safeEntityName(model);
|
|
366
|
+
if (!modelVar) return '';
|
|
353
367
|
if (!ctx.declaredVars?.has(modelVar)) return '';
|
|
354
368
|
const collection = toCollection(model);
|
|
355
369
|
return ` // Step ${ctx.stepNum}: Transition ${model} to ${state}
|
|
@@ -424,7 +438,8 @@ export const MONGO_STEP_CONVENTIONS: MongoStepConvention[] = [
|
|
|
424
438
|
pattern: /^(?:persist|save|store)\s+(\w+(?:\s+\w+)?)(?:\s+(?:for|to|record).*)?$/i,
|
|
425
439
|
generateCall: (m, ctx) => {
|
|
426
440
|
// m[1] may be "refresh token" — collapse to camelCase + collection
|
|
427
|
-
const target =
|
|
441
|
+
const target = safeEntityName(m[1].replace(/\s+(.)/g, (_, c) => c.toUpperCase()));
|
|
442
|
+
if (!target) return '';
|
|
428
443
|
const collection = toCollection(target);
|
|
429
444
|
const recordSrc = mostRecentStepResult(ctx) ?? 'args';
|
|
430
445
|
return ` // Step ${ctx.stepNum}: Persist ${m[1]}
|
|
@@ -442,7 +457,8 @@ export const MONGO_STEP_CONVENTIONS: MongoStepConvention[] = [
|
|
|
442
457
|
name: 'conditional-create',
|
|
443
458
|
pattern: /^if\s+(\w+)\s+does\s+not\s+exist,?\s+create\s+new\s+(\w+)(?:\s+with\s+.+)?$/i,
|
|
444
459
|
generateCall: (m, ctx) => {
|
|
445
|
-
const modelVar =
|
|
460
|
+
const modelVar = safeEntityName(m[1]);
|
|
461
|
+
if (!modelVar) return '';
|
|
446
462
|
const Model = pascal(m[2]);
|
|
447
463
|
const collection = toCollection(Model);
|
|
448
464
|
if (!ctx.declaredVars?.has(modelVar)) return ''; // need prior find
|
|
@@ -472,7 +488,8 @@ export const MONGO_STEP_CONVENTIONS: MongoStepConvention[] = [
|
|
|
472
488
|
name: 'conditional-update',
|
|
473
489
|
pattern: /^if\s+(\w+)\s+exists,?\s+update\s+(\w+)(?:\s+(.+))?$/i,
|
|
474
490
|
generateCall: (m, ctx) => {
|
|
475
|
-
const modelVar =
|
|
491
|
+
const modelVar = safeEntityName(m[1]);
|
|
492
|
+
if (!modelVar) return '';
|
|
476
493
|
const field = m[2];
|
|
477
494
|
const collection = toCollection(m[1]);
|
|
478
495
|
if (!ctx.declaredVars?.has(modelVar)) return '';
|
|
@@ -547,7 +564,8 @@ export const MONGO_STEP_CONVENTIONS: MongoStepConvention[] = [
|
|
|
547
564
|
pattern: /^otherwise\s+create\s+(?:new\s+)?(\w+)\s+record$/i,
|
|
548
565
|
generateCall: (m, ctx) => {
|
|
549
566
|
const Model = pascal(m[1]);
|
|
550
|
-
const modelVar =
|
|
567
|
+
const modelVar = safeEntityName(Model);
|
|
568
|
+
if (!modelVar) return '';
|
|
551
569
|
const collection = toCollection(Model);
|
|
552
570
|
// The model var was declared (as `let`) by the find that paired
|
|
553
571
|
// with the prior conditional-update; reassign it here so downstream
|
|
@@ -612,9 +630,11 @@ export const MONGO_STEP_CONVENTIONS: MongoStepConvention[] = [
|
|
|
612
630
|
generateCall: (m, ctx) => {
|
|
613
631
|
const service = m[1];
|
|
614
632
|
const method = m[2];
|
|
633
|
+
const serviceVar = safeEntityName(service);
|
|
634
|
+
if (!serviceVar) return '';
|
|
615
635
|
const args = (ctx.parameterNames || []).join(', ');
|
|
616
636
|
return ` // Step ${ctx.stepNum}: Call ${service}.${method}
|
|
617
|
-
await (${
|
|
637
|
+
await (${serviceVar} as any).${method}({ ${args} });`;
|
|
618
638
|
},
|
|
619
639
|
},
|
|
620
640
|
|