@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.
Files changed (84) hide show
  1. package/dist/ai/analyse-runner.d.ts.map +1 -1
  2. package/dist/ai/analyse-runner.js +22 -1
  3. package/dist/ai/analyse-runner.js.map +1 -1
  4. package/dist/analyse-prepass/adapters/module-functions.d.ts +25 -0
  5. package/dist/analyse-prepass/adapters/module-functions.d.ts.map +1 -1
  6. package/dist/analyse-prepass/adapters/module-functions.js +54 -0
  7. package/dist/analyse-prepass/adapters/module-functions.js.map +1 -1
  8. package/dist/analyse-prepass/backends/gitnexus.d.ts +28 -0
  9. package/dist/analyse-prepass/backends/gitnexus.d.ts.map +1 -1
  10. package/dist/analyse-prepass/backends/gitnexus.js +36 -2
  11. package/dist/analyse-prepass/backends/gitnexus.js.map +1 -1
  12. package/dist/analyse-prepass/index.d.ts.map +1 -1
  13. package/dist/analyse-prepass/index.js +17 -1
  14. package/dist/analyse-prepass/index.js.map +1 -1
  15. package/dist/libs/instance-factories/controllers/templates/fastify/routes-generator.js +29 -10
  16. package/dist/libs/instance-factories/services/templates/_shared/step-matching.js +11 -0
  17. package/dist/libs/instance-factories/services/templates/mongodb-native/step-conventions.js +39 -19
  18. package/dist/libs/instance-factories/services/templates/postgres-native/step-conventions.js +35 -18
  19. package/dist/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.js +16 -11
  20. package/dist/libs/instance-factories/services/templates/prisma/controller-generator.js +1 -1
  21. package/dist/libs/instance-factories/services/templates/prisma/step-conventions.js +34 -12
  22. package/dist/libs/instance-factories/services/templates/shared-patterns.js +5 -5
  23. package/dist/realize/index.d.ts.map +1 -1
  24. package/dist/realize/index.js +91 -10
  25. package/dist/realize/index.js.map +1 -1
  26. package/dist/realize/per-action-recovery.d.ts +74 -0
  27. package/dist/realize/per-action-recovery.d.ts.map +1 -0
  28. package/dist/realize/per-action-recovery.js +255 -0
  29. package/dist/realize/per-action-recovery.js.map +1 -0
  30. package/dist/realize/per-owner-emit.d.ts +6 -0
  31. package/dist/realize/per-owner-emit.d.ts.map +1 -1
  32. package/dist/realize/per-owner-emit.js +22 -6
  33. package/dist/realize/per-owner-emit.js.map +1 -1
  34. package/dist/realize/per-owner-runner.d.ts +23 -2
  35. package/dist/realize/per-owner-runner.d.ts.map +1 -1
  36. package/dist/realize/per-owner-runner.js +91 -46
  37. package/dist/realize/per-owner-runner.js.map +1 -1
  38. package/dist/realize/post-emit-verify/diagnostics.d.ts +107 -0
  39. package/dist/realize/post-emit-verify/diagnostics.d.ts.map +1 -0
  40. package/dist/realize/post-emit-verify/diagnostics.js +148 -0
  41. package/dist/realize/post-emit-verify/diagnostics.js.map +1 -0
  42. package/dist/realize/post-emit-verify/feedback-runner.d.ts +41 -1
  43. package/dist/realize/post-emit-verify/feedback-runner.d.ts.map +1 -1
  44. package/dist/realize/post-emit-verify/feedback-runner.js +62 -6
  45. package/dist/realize/post-emit-verify/feedback-runner.js.map +1 -1
  46. package/dist/realize/post-emit-verify/index.d.ts +4 -2
  47. package/dist/realize/post-emit-verify/index.d.ts.map +1 -1
  48. package/dist/realize/post-emit-verify/index.js +3 -1
  49. package/dist/realize/post-emit-verify/index.js.map +1 -1
  50. package/dist/realize/post-emit-verify/reemit.d.ts +22 -1
  51. package/dist/realize/post-emit-verify/reemit.d.ts.map +1 -1
  52. package/dist/realize/post-emit-verify/reemit.js +20 -18
  53. package/dist/realize/post-emit-verify/reemit.js.map +1 -1
  54. package/dist/realize/post-emit-verify/types.d.ts +49 -0
  55. package/dist/realize/post-emit-verify/types.d.ts.map +1 -1
  56. package/dist/realize/post-emit-verify/verifier-manifest.d.ts.map +1 -1
  57. package/dist/realize/post-emit-verify/verifier-manifest.js +2 -0
  58. package/dist/realize/post-emit-verify/verifier-manifest.js.map +1 -1
  59. package/dist/realize/post-emit-verify/verifiers/stub-completeness.d.ts +127 -0
  60. package/dist/realize/post-emit-verify/verifiers/stub-completeness.d.ts.map +1 -0
  61. package/dist/realize/post-emit-verify/verifiers/stub-completeness.js +423 -0
  62. package/dist/realize/post-emit-verify/verifiers/stub-completeness.js.map +1 -0
  63. package/dist/realize/realize-context-snapshot.d.ts +70 -0
  64. package/dist/realize/realize-context-snapshot.d.ts.map +1 -0
  65. package/dist/realize/realize-context-snapshot.js +96 -0
  66. package/dist/realize/realize-context-snapshot.js.map +1 -0
  67. package/dist/realize/structural-validator.d.ts +36 -2
  68. package/dist/realize/structural-validator.d.ts.map +1 -1
  69. package/dist/realize/structural-validator.js +50 -7
  70. package/dist/realize/structural-validator.js.map +1 -1
  71. package/libs/instance-factories/controllers/templates/fastify/routes-generator.ts +49 -15
  72. package/libs/instance-factories/services/templates/_shared/step-matching.ts +43 -0
  73. package/libs/instance-factories/services/templates/mongodb-native/step-conventions.ts +39 -19
  74. package/libs/instance-factories/services/templates/postgres-native/step-conventions.ts +35 -18
  75. package/libs/instance-factories/services/templates/prisma/__tests__/step-conventions-create.test.ts +184 -0
  76. package/libs/instance-factories/services/templates/prisma/ai-behaviors-generator.ts +34 -5
  77. package/libs/instance-factories/services/templates/prisma/controller-generator.ts +6 -1
  78. package/libs/instance-factories/services/templates/prisma/step-conventions.ts +34 -12
  79. package/libs/instance-factories/services/templates/shared-patterns.ts +20 -10
  80. package/package.json +1 -1
  81. package/libs/instance-factories/services/templates/_shared/step-matching.d.ts +0 -39
  82. package/libs/instance-factories/services/templates/_shared/step-matching.d.ts.map +0 -1
  83. package/libs/instance-factories/services/templates/_shared/step-matching.js +0 -90
  84. 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,oDAAoD;AACpD,MAAM,MAAM,0BAA0B,GAClC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GACZ;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D,kCAAkC;AAClC,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAuBD;;;;;;;;;;GAUG;AACH,wBAAgB,qCAAqC,CACnD,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,eAAe,EAAE,GAClC,0BAA0B,CAiD5B"}
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
- const reason = formatMissingReport(missing);
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
- lines.push(`LLM output dropped or rewrote ${missing.length} pre-baked convention snippet${missing.length === 1 ? '' : 's'}:`);
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 must be preserved verbatim. ' +
164
- 'If you believe the matcher is wrong for this case, fix the convention pattern in step-conventions.ts.');
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":"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"}
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
- const imports = generateImports(controller, modelName, handlerName, isModelController, implType);
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
- const externalRoutesExport = externalEndpoints.length > 0 ? `
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/<plural>' prefix.
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
- fastify: FastifyInstance,
118
- options: any
145
+ ${mainFastifyParam}: FastifyInstance,
146
+ ${mainOptionsParam}: any
119
147
  ) {
120
- const handler = ${isModelController ? 'options.controllers' : 'options.services'}.${handlerName};
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
- controller: any,
160
+ _controller: any,
132
161
  modelName: string,
133
- handlerName: string,
134
- isModelController: boolean,
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 { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';`,
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 = toVar(model);
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 = toVar(model);
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 = toVar(model);
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 = toVar(model);
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 = toVar(model);
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 = toVar(model);
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 = toVar(m[1].replace(/\s+(.)/g, (_, c) => c.toUpperCase()));
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 = toVar(m[1]);
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 = toVar(m[1]);
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 = toVar(Model);
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 (${toVar(service)} as any).${method}({ ${args} });`;
637
+ await (${serviceVar} as any).${method}({ ${args} });`;
618
638
  },
619
639
  },
620
640