@specverse/engines 6.21.2 → 6.27.10

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 (59) hide show
  1. package/dist/ai/analyse-runner.d.ts +16 -0
  2. package/dist/ai/analyse-runner.d.ts.map +1 -1
  3. package/dist/ai/analyse-runner.js +417 -53
  4. package/dist/ai/analyse-runner.js.map +1 -1
  5. package/dist/ai/microcall-orchestrator.d.ts +187 -0
  6. package/dist/ai/microcall-orchestrator.d.ts.map +1 -0
  7. package/dist/ai/microcall-orchestrator.js +673 -0
  8. package/dist/ai/microcall-orchestrator.js.map +1 -0
  9. package/dist/ai/skeleton-emitter.d.ts +94 -0
  10. package/dist/ai/skeleton-emitter.d.ts.map +1 -0
  11. package/dist/ai/skeleton-emitter.js +752 -0
  12. package/dist/ai/skeleton-emitter.js.map +1 -0
  13. package/dist/analyse-prepass/adapters/express-routes.d.ts +71 -0
  14. package/dist/analyse-prepass/adapters/express-routes.d.ts.map +1 -0
  15. package/dist/analyse-prepass/adapters/express-routes.js +329 -0
  16. package/dist/analyse-prepass/adapters/express-routes.js.map +1 -0
  17. package/dist/analyse-prepass/adapters/typescript-interfaces.d.ts +91 -0
  18. package/dist/analyse-prepass/adapters/typescript-interfaces.d.ts.map +1 -0
  19. package/dist/analyse-prepass/adapters/typescript-interfaces.js +411 -0
  20. package/dist/analyse-prepass/adapters/typescript-interfaces.js.map +1 -0
  21. package/dist/analyse-prepass/backends/gitnexus.d.ts.map +1 -1
  22. package/dist/analyse-prepass/backends/gitnexus.js +36 -8
  23. package/dist/analyse-prepass/backends/gitnexus.js.map +1 -1
  24. package/dist/analyse-prepass/backends/index.d.ts.map +1 -1
  25. package/dist/analyse-prepass/backends/index.js +3 -5
  26. package/dist/analyse-prepass/backends/index.js.map +1 -1
  27. package/dist/analyse-prepass/behavior-step-classifier.d.ts +3 -0
  28. package/dist/analyse-prepass/behavior-step-classifier.d.ts.map +1 -1
  29. package/dist/analyse-prepass/behavior-step-classifier.js +1 -0
  30. package/dist/analyse-prepass/behavior-step-classifier.js.map +1 -1
  31. package/dist/analyse-prepass/index.d.ts +69 -0
  32. package/dist/analyse-prepass/index.d.ts.map +1 -1
  33. package/dist/analyse-prepass/index.js +385 -17
  34. package/dist/analyse-prepass/index.js.map +1 -1
  35. package/dist/analyse-prepass/method-body-walker.d.ts +4 -0
  36. package/dist/analyse-prepass/method-body-walker.d.ts.map +1 -1
  37. package/dist/analyse-prepass/method-body-walker.js +14 -0
  38. package/dist/analyse-prepass/method-body-walker.js.map +1 -1
  39. package/dist/audit/realize-recorder.d.ts +164 -0
  40. package/dist/audit/realize-recorder.d.ts.map +1 -0
  41. package/dist/audit/realize-recorder.js +153 -0
  42. package/dist/audit/realize-recorder.js.map +1 -0
  43. package/dist/audit/verify-checks.d.ts +32 -0
  44. package/dist/audit/verify-checks.d.ts.map +1 -0
  45. package/dist/audit/verify-checks.js +202 -0
  46. package/dist/audit/verify-checks.js.map +1 -0
  47. package/dist/audit/verify-recorder.d.ts +84 -0
  48. package/dist/audit/verify-recorder.d.ts.map +1 -0
  49. package/dist/audit/verify-recorder.js +90 -0
  50. package/dist/audit/verify-recorder.js.map +1 -0
  51. package/dist/inference/core/specly-converter.d.ts.map +1 -1
  52. package/dist/inference/core/specly-converter.js +67 -36
  53. package/dist/inference/core/specly-converter.js.map +1 -1
  54. package/dist/libs/instance-factories/services/templates/_shared/step-matching.js +39 -15
  55. package/dist/realize/index.d.ts.map +1 -1
  56. package/dist/realize/index.js +63 -0
  57. package/dist/realize/index.js.map +1 -1
  58. package/libs/instance-factories/services/templates/_shared/step-matching.ts +61 -16
  59. package/package.json +1 -1
@@ -52,6 +52,22 @@ export interface RunAnalyseOptions {
52
52
  caseName?: string;
53
53
  /** Loose case meta for the manifest (kind: 'analyse' is fixed). */
54
54
  caseMeta?: RunCaseEntry;
55
+ /**
56
+ * Phase B opt-in: use the skeleton-emitter + per-action micro-calls
57
+ * pipeline instead of the giant single analyse call. See plan:
58
+ * specverse-self/docs/plans/2026-05-04-ANALYSE-VIA-FAITHFUL-SKELETON-AND-MICROCALLS.md
59
+ *
60
+ * Default false (existing behaviour). When true:
61
+ * - runPrepass produces facts (unchanged)
62
+ * - emitFaithfulSkeleton turns facts into a faithful skeleton
63
+ * - runActionMicrocalls fires parallel LLM calls per action stub
64
+ * - assembleFinalSpec merges skeleton + filled bodies into main.specly
65
+ * - skeleton.specly + skeleton-provenance.json + microcall-decisions.json
66
+ * sidecars written alongside existing artifacts
67
+ */
68
+ useMicrocalls?: boolean;
69
+ /** Concurrency cap for per-action micro-calls (default 10). */
70
+ microcallConcurrency?: number;
55
71
  }
56
72
  export interface RunAnalyseResult {
57
73
  outputDir: string;
@@ -1 +1 @@
1
- {"version":3,"file":"analyse-runner.d.ts","sourceRoot":"","sources":["../../src/ai/analyse-runner.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AACxC,OAAO,EAGL,KAAK,WAAW,EAAE,KAAK,YAAY,EACpC,MAAM,gBAAgB,CAAC;AAiBxB,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+CAA+C;IAC/C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mEAAmE;IACnE,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,YAAY,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,UAAU,EAAE,OAAO,GAAG,UAAU,CAAC;IACjC,aAAa,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,OAAO,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,oBAAoB,EAAE,OAAO,CAAC;CAC/B;AAqCD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA6TnF"}
1
+ {"version":3,"file":"analyse-runner.d.ts","sourceRoot":"","sources":["../../src/ai/analyse-runner.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AACxC,OAAO,EAGL,KAAK,WAAW,EAAE,KAAK,YAAY,EACpC,MAAM,gBAAgB,CAAC;AAiBxB,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,+CAA+C;IAC/C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mEAAmE;IACnE,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB;;;;;;;;;;;;OAYG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,+DAA+D;IAC/D,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,UAAU,EAAE,OAAO,GAAG,UAAU,CAAC;IACjC,aAAa,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,OAAO,EAAE;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,WAAW,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,oBAAoB,EAAE,OAAO,CAAC;CAC/B;AAqCD;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAwnBnF"}
@@ -22,9 +22,10 @@
22
22
  * artifacts. It writes manifest.json itself with what it knows; callers
23
23
  * can re-write it with additional fields after scoring.
24
24
  */
25
- import { writeFileSync, readFileSync, existsSync, rmSync, cpSync } from 'fs';
26
- import { join } from 'path';
25
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync, cpSync, statSync, readdirSync } from 'fs';
26
+ import { join, relative } from 'path';
27
27
  import { execSync, spawnSync } from 'child_process';
28
+ import { createHash } from 'crypto';
28
29
  import { promptsDir, llmOutputDir, factsDir, specsDir, ensureSubdirs, writeStageSpec, writeProjectReadme, writeManifest, writeReadme, } from './run-paths.js';
29
30
  import { runPrompt, extractSpecAndManifest } from './prompt-runner.js';
30
31
  import { resolveModel, resolveProviderId } from './model-resolver.js';
@@ -87,6 +88,48 @@ export async function runAnalyse(opts) {
87
88
  const startedAt = new Date().toISOString();
88
89
  const t0 = Date.now();
89
90
  ensureSubdirs(opts.outputDir);
91
+ // Verify-stage recorder (#audit-trail-2026-05-03 plan, engines 6.22.x+).
92
+ // Captures every schema-validation pass + retry event + structural-check
93
+ // result at the moment they happen during the analyse pipeline.
94
+ const { InMemoryVerifyRecorder, categoriseValidationError } = await import('../audit/verify-recorder.js');
95
+ const { runAllChecks } = await import('../audit/verify-checks.js');
96
+ const verifyRecorder = new InMemoryVerifyRecorder();
97
+ // Wrap runValidate so every call records into the verify recorder. Also
98
+ // runs structural checks after a successful schema-validation pass.
99
+ // The js-yaml import is hoisted out of the function so it loads once
100
+ // (ESM project — `require()` is not available without createRequire).
101
+ const yamlModule = await import('js-yaml');
102
+ const recordedRunValidate = (specPath, kind = 'schema-validation') => {
103
+ const t = Date.now();
104
+ const r = runValidate(specPath);
105
+ const errorList = r.ok ? [] : r.errors.split('\n').filter((l) => l.trim());
106
+ verifyRecorder.recordPass({
107
+ kind,
108
+ ok: r.ok,
109
+ errorCount: errorList.length,
110
+ errors: errorList.slice(0, 50), // cap to keep sidecar bounded
111
+ durationMs: Date.now() - t,
112
+ });
113
+ // Structural checks run after a clean schema-validation pass. They
114
+ // surface SpecVerse-semantic invariant violations as human-review
115
+ // items without failing the validation gate.
116
+ if (r.ok && existsSync(specPath)) {
117
+ try {
118
+ const parsed = yamlModule.load(readFileSync(specPath, 'utf8'));
119
+ const checkResults = runAllChecks(parsed);
120
+ for (const cr of checkResults)
121
+ verifyRecorder.recordStructuralCheck(cr);
122
+ }
123
+ catch (e) {
124
+ verifyRecorder.recordHumanReviewItem({
125
+ severity: 'warning',
126
+ kind: 'structural-checks-skipped',
127
+ message: `Could not parse spec for structural checks: ${e?.message ?? e}`,
128
+ });
129
+ }
130
+ }
131
+ return r;
132
+ };
90
133
  // 1. Resolve facts content (if any). Two sources: --facts <path> (manual)
91
134
  // or runPrepass on sourceDir (--auto-facts). Manual wins if both passed.
92
135
  let factsContent = '';
@@ -97,9 +140,12 @@ export async function runAnalyse(opts) {
97
140
  // #27-C: pre-classified candidate-step timelines are surfaced
98
141
  // alongside method facts as a third structured-input stream.
99
142
  let candidateStepsContent = '';
143
+ // Hoisted so the microcall branch (Phase B) can build the skeleton.
144
+ let prepassFacts;
100
145
  if (opts.autoFacts && !opts.factsPath) {
101
146
  const { runPrepass, formatFactsAsMarkdown, formatMethodFactsAsMarkdown, formatCandidateMethodsAsMarkdown } = await import('./../analyse-prepass/index.js');
102
147
  const facts = await runPrepass(opts.sourceDir);
148
+ prepassFacts = facts;
103
149
  factsContent = formatFactsAsMarkdown(facts);
104
150
  methodFactsContent = formatMethodFactsAsMarkdown(facts.methodFactSheets);
105
151
  candidateStepsContent = formatCandidateMethodsAsMarkdown(facts.candidateMethods);
@@ -116,6 +162,33 @@ export async function runAnalyse(opts) {
116
162
  writeFileSync(join(factsDir(opts.outputDir), 'facts.json'), JSON.stringify(facts, null, 2));
117
163
  }
118
164
  catch { /* non-serializable; skip */ }
165
+ // Audit-trail sidecars (#audit-trail-2026-05-03 plan, engines 6.22.0+).
166
+ // These let `spv report run` regenerate the markdown report without
167
+ // heuristic re-parsing of facts.md / candidate-steps.md.
168
+ if (facts.auditDecisions) {
169
+ try {
170
+ const t1 = Date.now();
171
+ const stats = {
172
+ schemaVersion: '1.0',
173
+ extractedAt: new Date().toISOString(),
174
+ sourceDir: opts.sourceDir,
175
+ backend: facts.meta.backend,
176
+ adaptersRun: facts.meta.adaptersRun,
177
+ durationMs: facts.meta.durationMs,
178
+ totalDurationMs: t1 - t0,
179
+ ...facts.auditDecisions.stats,
180
+ };
181
+ writeFileSync(join(factsDir(opts.outputDir), 'extract-decisions.json'), JSON.stringify(facts.auditDecisions, null, 2));
182
+ writeFileSync(join(factsDir(opts.outputDir), 'extract-stats.json'), JSON.stringify(stats, null, 2));
183
+ // Source fingerprint — SHA256 of the input tree for reproducibility.
184
+ const fingerprint = await computeSourceFingerprint(opts.sourceDir);
185
+ writeFileSync(join(factsDir(opts.outputDir), 'source-fingerprint.json'), JSON.stringify(fingerprint, null, 2));
186
+ }
187
+ catch (e) {
188
+ // Sidecar writes are advisory — don't fail the run on serialization issues.
189
+ console.error(`[audit] sidecar emit failed: ${e?.message ?? e}`);
190
+ }
191
+ }
119
192
  }
120
193
  else if (opts.factsPath) {
121
194
  factsContent = readFileSync(opts.factsPath, 'utf8');
@@ -145,83 +218,233 @@ export async function runAnalyse(opts) {
145
218
  timeout: opts.timeoutMs ?? 15 * 60_000,
146
219
  sessionId: opts.sessionId,
147
220
  });
148
- // 3. Analyse pass.
149
- const draftResult = await runPrompt({
150
- operation: 'analyse',
151
- values: {
152
- implementationPath: opts.sourceDir,
153
- spec: '',
154
- environment: 'development',
155
- },
156
- model,
157
- timeoutMs: opts.timeoutMs ?? 15 * 60_000,
158
- userPrefix,
159
- onAssembled: ({ system, user }) => {
160
- writeFileSync(join(promptsDir(opts.outputDir), 'analyse-system.txt'), system);
161
- writeFileSync(join(promptsDir(opts.outputDir), 'analyse-user.txt'), user);
162
- },
163
- });
164
- // analyse@9.6+ emits TWO code blocks: spec (components+deployments) +
165
- // canonical capability-mapping manifest. extractSpecAndManifest pulls
166
- // both; older single-block output still works (manifest is null then).
167
- let { spec: draftSpec, manifest: draftManifest } = extractSpecAndManifest(draftResult.text);
221
+ // 3. Analyse pass — two paths:
222
+ // (a) useMicrocalls=true (Phase B): skeleton + per-action micro-calls + manifest call
223
+ // (b) default: one giant runPrompt call producing spec + manifest in one shot
224
+ let draftResult;
225
+ let draftSpec;
226
+ let draftManifest;
227
+ // Microcall state hoisted out of the if-block so the retry path
228
+ // (TODO #152 surgical retry) can re-fire failed microcalls instead
229
+ // of calling the giant analyse prompt.
230
+ let microcallState = null;
231
+ if (opts.useMicrocalls) {
232
+ if (!prepassFacts) {
233
+ throw new Error('useMicrocalls requires autoFacts (skeleton emitter consumes prepass facts)');
234
+ }
235
+ // Skeleton emitter (Phase A) — deterministic facts → faithful skeleton.
236
+ const { emitFaithfulSkeleton } = await import('./skeleton-emitter.js');
237
+ const { skeleton, provenance: skeletonProvenance } = emitFaithfulSkeleton(prepassFacts);
238
+ writeFileSync(join(specsDir(opts.outputDir), 'skeleton.specly'), skeleton);
239
+ writeFileSync(join(factsDir(opts.outputDir), 'skeleton-provenance.json'), JSON.stringify(skeletonProvenance, null, 2));
240
+ // Build candidateStepsByMethodKey from facts. Each method's timeline as
241
+ // markdown text the per-action prompt can consume directly.
242
+ const candidateStepsByMethodKey = new Map();
243
+ for (const cls of prepassFacts.candidateMethods ?? []) {
244
+ for (let mi = 0; mi < cls.methods.length; mi++) {
245
+ const m = cls.methods[mi];
246
+ const lines = m.candidates.map((c, i) => `${i + 1}. [${c.kind}] ${c.summary}`);
247
+ candidateStepsByMethodKey.set(`${cls.entityName}::${mi}`, lines.join('\n'));
248
+ }
249
+ }
250
+ // Build a manifest overview from the skeleton (top-level component
251
+ // names + a summary of detected entities).
252
+ const skeletonOverview = skeleton.split('\n').slice(0, 80).join('\n');
253
+ // Run the orchestrator.
254
+ const { runActionMicrocalls, assembleFinalSpec } = await import('./microcall-orchestrator.js');
255
+ const microcallResult = await runActionMicrocalls({
256
+ skeleton,
257
+ skeletonProvenance,
258
+ candidateStepsByMethodKey,
259
+ model,
260
+ // Default 5 (was 10) — empirically claude-cli rate-limits at >5 in
261
+ // parallel for sustained calls. Override via --microcall-concurrency
262
+ // when running against a higher-tier API key.
263
+ concurrencyCap: opts.microcallConcurrency ?? 5,
264
+ timeoutMsPerCall: 5 * 60_000,
265
+ manifestInputs: {
266
+ skeletonOverview,
267
+ importsList: '(imports list extraction TBD — stub)',
268
+ language: 'typescript',
269
+ frameworks: prepassFacts.meta?.adaptersRun?.join(', ') || 'none-detected',
270
+ },
271
+ });
272
+ // Forward-log: write microcall provenance sidecar.
273
+ writeFileSync(join(llmOutputDir(opts.outputDir), 'microcall-decisions.json'), JSON.stringify(microcallResult.provenance, null, 2));
274
+ // Save state for surgical retry.
275
+ microcallState = {
276
+ skeleton,
277
+ skeletonProvenance,
278
+ candidateStepsByMethodKey,
279
+ actionBodies: microcallResult.actionBodies,
280
+ };
281
+ // Assemble final spec + manifest.
282
+ draftSpec = assembleFinalSpec(skeleton, skeletonProvenance, microcallResult.actionBodies);
283
+ draftManifest = microcallResult.manifestYaml;
284
+ // Synthesize a draftResult equivalent for downstream code paths.
285
+ draftResult = {
286
+ text: draftSpec,
287
+ tokens: { input: null, output: null },
288
+ duration_ms: microcallResult.provenance.wallTimeMs,
289
+ system: '(microcall path — multiple system prompts)',
290
+ user: '(microcall path — multiple user prompts)',
291
+ providerId: opts.providerId ?? resolveProviderId(),
292
+ modelId: opts.modelId ?? 'default',
293
+ };
294
+ }
295
+ else {
296
+ // Existing one-shot path.
297
+ draftResult = await runPrompt({
298
+ operation: 'analyse',
299
+ values: {
300
+ implementationPath: opts.sourceDir,
301
+ spec: '',
302
+ environment: 'development',
303
+ },
304
+ model,
305
+ timeoutMs: opts.timeoutMs ?? 15 * 60_000,
306
+ userPrefix,
307
+ onAssembled: ({ system, user }) => {
308
+ writeFileSync(join(promptsDir(opts.outputDir), 'analyse-system.txt'), system);
309
+ writeFileSync(join(promptsDir(opts.outputDir), 'analyse-user.txt'), user);
310
+ },
311
+ });
312
+ const extracted = extractSpecAndManifest(draftResult.text);
313
+ draftSpec = extracted.spec;
314
+ draftManifest = extracted.manifest;
315
+ }
168
316
  const draftSpecPath = writeStageSpec(opts.outputDir, 'draft', draftSpec);
169
317
  if (draftManifest)
170
318
  writeManifestArtifact(opts.outputDir, draftManifest);
171
319
  writeFileSync(join(llmOutputDir(opts.outputDir), 'analyse.txt'), draftResult.text);
320
+ // Audit-trail attribution moved to a separate focused LLM call after
321
+ // final-validate (engines 6.22.4+, assets 1.13.1+). See plan:
322
+ // specverse-self/docs/plans/2026-05-03-PIPELINE-AUDIT-TRAIL-INSTRUMENTATION.md
323
+ // and TODO #47 for spec-call decomposition follow-up work.
172
324
  // 4. Validate draft; retry up to MAX_RETRIES times if invalid. Each retry
173
325
  // feeds the most recent validation error + the prior draft back to the LLM
174
326
  // so it can fix one defect at a time. Larger specs surface multiple subtle
175
327
  // YAML defects in succession (cal-com case, 2026-04-28); a single retry is
176
328
  // not enough.
177
329
  const MAX_RETRIES = 3;
178
- let validateAfterDraft = runValidate(draftSpecPath);
330
+ let validateAfterDraft = recordedRunValidate(draftSpecPath);
179
331
  let draftRetried = false;
180
332
  if (!validateAfterDraft.ok) {
181
333
  writeFileSync(join(opts.outputDir, 'validate-error-1.txt'), validateAfterDraft.errors);
182
- let priorDraft = draftSpec;
183
- let priorError = validateAfterDraft.errors;
184
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
185
- try {
186
- const retryResult = await runPrompt({
187
- operation: 'analyse',
188
- values: {
189
- implementationPath: opts.sourceDir,
190
- spec: '',
191
- environment: 'development',
192
- },
334
+ // Microcalls path → surgical retry: parse error paths, re-fire only
335
+ // affected microcalls. Skeleton-structural errors are recorded as
336
+ // humanReviewItems and NOT retried (those are code bugs, not LLM-fixable).
337
+ if (opts.useMicrocalls && microcallState) {
338
+ const { classifyValidationErrors, runSurgicalRetry, assembleFinalSpec } = await import('./microcall-orchestrator.js');
339
+ let priorError = validateAfterDraft.errors;
340
+ let currentBodies = microcallState.actionBodies;
341
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
342
+ const classification = classifyValidationErrors(priorError, microcallState.skeletonProvenance);
343
+ for (const e of classification.structuralErrors) {
344
+ verifyRecorder.recordHumanReviewItem({
345
+ severity: 'error',
346
+ kind: 'skeleton-structural-error',
347
+ message: e,
348
+ });
349
+ }
350
+ if (classification.actionPathErrors.size === 0) {
351
+ // No action-stub errors — only structural. LLM retry can't fix
352
+ // those; fail validation cleanly.
353
+ break;
354
+ }
355
+ const retryStart = Date.now();
356
+ const surgical = await runSurgicalRetry({
357
+ skeleton: microcallState.skeleton,
358
+ skeletonProvenance: microcallState.skeletonProvenance,
359
+ candidateStepsByMethodKey: microcallState.candidateStepsByMethodKey,
360
+ prior: { actionBodies: currentBodies, classification },
193
361
  model,
194
- timeoutMs: opts.timeoutMs ?? 15 * 60_000,
195
- userPrefix: `Your previous draft (attempt ${attempt}) failed \`spv validate\` with the following error:\n\n` +
196
- '```\n' + priorError + '\n```\n\n' +
197
- 'Your previous output:\n```specly\n' + priorDraft + '\n```\n\n' +
198
- 'Fix the error and emit a corrected spec. Keep everything else identical.\n\n',
199
- onAssembled: ({ system, user }) => {
200
- writeFileSync(join(promptsDir(opts.outputDir), `analyse-retry-${attempt}-system.txt`), system);
201
- writeFileSync(join(promptsDir(opts.outputDir), `analyse-retry-${attempt}-user.txt`), user);
202
- },
362
+ concurrencyCap: opts.microcallConcurrency ?? 5,
363
+ timeoutMsPerCall: 5 * 60_000,
203
364
  });
204
- const { spec: retrySpec, manifest: retryManifest } = extractSpecAndManifest(retryResult.text);
365
+ currentBodies = surgical.actionBodies;
366
+ const retrySpec = assembleFinalSpec(microcallState.skeleton, microcallState.skeletonProvenance, currentBodies);
205
367
  const retrySpecPath = join(specsDir(opts.outputDir), `draft-retry-${attempt}.specly`);
206
368
  writeFileSync(retrySpecPath, retrySpec);
207
- writeFileSync(join(llmOutputDir(opts.outputDir), `analyse-retry-${attempt}.txt`), retryResult.text);
208
- const validateAfterRetry = runValidate(retrySpecPath);
369
+ const validateAfterRetry = recordedRunValidate(retrySpecPath);
209
370
  writeFileSync(join(opts.outputDir, `validate-after-retry-${attempt}.txt`), validateAfterRetry.ok ? 'OK' : validateAfterRetry.errors);
371
+ verifyRecorder.recordRetry({
372
+ trigger: attempt === 1 ? 'first-validate-failed' : `retry-${attempt - 1}-failed`,
373
+ errorCount: priorError.split('\n').filter((l) => l.trim()).length,
374
+ errorCategories: categoriseValidationError(priorError),
375
+ tokenCountIn: undefined,
376
+ tokenCountOut: undefined,
377
+ latencyMs: Date.now() - retryStart,
378
+ itemsChangedSummary: `surgical retry: ${surgical.retriedStubs} stub(s) re-fired (${classification.structuralErrors.length} structural error(s) skipped)` +
379
+ (validateAfterRetry.ok
380
+ ? `, resolved ${priorError.split('\n').length} error line(s)`
381
+ : `, ${validateAfterRetry.errors.split('\n').length} error line(s) remain`),
382
+ ok: validateAfterRetry.ok,
383
+ });
210
384
  if (validateAfterRetry.ok) {
211
385
  draftSpec = retrySpec;
212
386
  writeStageSpec(opts.outputDir, 'draft', retrySpec);
213
- if (retryManifest)
214
- writeManifestArtifact(opts.outputDir, retryManifest);
215
387
  validateAfterDraft = validateAfterRetry;
216
388
  draftRetried = true;
217
389
  break;
218
390
  }
219
- // Not OK — set up for next retry attempt with the new error + new draft.
220
- priorDraft = retrySpec;
221
391
  priorError = validateAfterRetry.errors;
222
392
  }
223
- catch { /* retry failed; continue with original draft */
224
- break;
393
+ }
394
+ else {
395
+ // Legacy giant-call path retry — full spec re-emission via analyse prompt.
396
+ let priorDraft = draftSpec;
397
+ let priorError = validateAfterDraft.errors;
398
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
399
+ try {
400
+ const retryResult = await runPrompt({
401
+ operation: 'analyse',
402
+ values: {
403
+ implementationPath: opts.sourceDir,
404
+ spec: '',
405
+ environment: 'development',
406
+ },
407
+ model,
408
+ timeoutMs: opts.timeoutMs ?? 15 * 60_000,
409
+ userPrefix: `Your previous draft (attempt ${attempt}) failed \`spv validate\` with the following error:\n\n` +
410
+ '```\n' + priorError + '\n```\n\n' +
411
+ 'Your previous output:\n```specly\n' + priorDraft + '\n```\n\n' +
412
+ 'Fix the error and emit a corrected spec. Keep everything else identical.\n\n',
413
+ onAssembled: ({ system, user }) => {
414
+ writeFileSync(join(promptsDir(opts.outputDir), `analyse-retry-${attempt}-system.txt`), system);
415
+ writeFileSync(join(promptsDir(opts.outputDir), `analyse-retry-${attempt}-user.txt`), user);
416
+ },
417
+ });
418
+ const { spec: retrySpec, manifest: retryManifest } = extractSpecAndManifest(retryResult.text);
419
+ const retrySpecPath = join(specsDir(opts.outputDir), `draft-retry-${attempt}.specly`);
420
+ writeFileSync(retrySpecPath, retrySpec);
421
+ writeFileSync(join(llmOutputDir(opts.outputDir), `analyse-retry-${attempt}.txt`), retryResult.text);
422
+ const validateAfterRetry = recordedRunValidate(retrySpecPath);
423
+ writeFileSync(join(opts.outputDir, `validate-after-retry-${attempt}.txt`), validateAfterRetry.ok ? 'OK' : validateAfterRetry.errors);
424
+ verifyRecorder.recordRetry({
425
+ trigger: attempt === 1 ? 'first-validate-failed' : `retry-${attempt - 1}-failed`,
426
+ errorCount: priorError.split('\n').filter((l) => l.trim()).length,
427
+ errorCategories: categoriseValidationError(priorError),
428
+ itemsChangedSummary: validateAfterRetry.ok
429
+ ? `retry-${attempt} resolved ${priorError.split('\n').length} error line(s)`
430
+ : `retry-${attempt} still has ${validateAfterRetry.errors.split('\n').length} error line(s)`,
431
+ ok: validateAfterRetry.ok,
432
+ });
433
+ if (validateAfterRetry.ok) {
434
+ draftSpec = retrySpec;
435
+ writeStageSpec(opts.outputDir, 'draft', retrySpec);
436
+ if (retryManifest)
437
+ writeManifestArtifact(opts.outputDir, retryManifest);
438
+ validateAfterDraft = validateAfterRetry;
439
+ draftRetried = true;
440
+ break;
441
+ }
442
+ priorDraft = retrySpec;
443
+ priorError = validateAfterRetry.errors;
444
+ }
445
+ catch {
446
+ break;
447
+ }
225
448
  }
226
449
  }
227
450
  }
@@ -338,8 +561,63 @@ export async function runAnalyse(opts) {
338
561
  }
339
562
  // 6. Final validation.
340
563
  const finalSpecPath = join(specsDir(opts.outputDir), 'main.specly');
341
- const validateFinal = runValidate(finalSpecPath);
564
+ const validateFinal = recordedRunValidate(finalSpecPath);
342
565
  writeFileSync(join(opts.outputDir, 'validate.txt'), validateFinal.ok ? 'OK' : validateFinal.errors);
566
+ // 6.5. Audit-trail attribution call (focused, small, post-validate).
567
+ // Runs only when:
568
+ // (a) candidate-step timelines were generated (we're auto-facts mode), AND
569
+ // (b) final spec validated cleanly (no point attributing an invalid spec)
570
+ // Failure is non-fatal — falls back to no-attribution and logs a human
571
+ // review item.
572
+ if (validateFinal.ok && candidateStepsContent) {
573
+ try {
574
+ const finalSpec = readFileSync(finalSpecPath, 'utf8');
575
+ const attributeResult = await runPrompt({
576
+ operation: 'attribute',
577
+ values: {
578
+ spec: finalSpec,
579
+ candidateSteps: candidateStepsContent,
580
+ },
581
+ model,
582
+ timeoutMs: 5 * 60_000, // 5 minutes — attribution is a small focused call
583
+ onAssembled: ({ system, user }) => {
584
+ writeFileSync(join(promptsDir(opts.outputDir), 'attribute-system.txt'), system);
585
+ writeFileSync(join(promptsDir(opts.outputDir), 'attribute-user.txt'), user);
586
+ },
587
+ });
588
+ writeFileSync(join(llmOutputDir(opts.outputDir), 'attribute.txt'), attributeResult.text);
589
+ const attribution = await extractAttributionBlock(attributeResult.text);
590
+ if (attribution) {
591
+ writeFileSync(join(llmOutputDir(opts.outputDir), 'analyse-attribution.json'), JSON.stringify({ schemaVersion: '1.0', attribution: attribution.attribution ?? [] }, null, 2));
592
+ writeFileSync(join(llmOutputDir(opts.outputDir), 'analyse-decisions.json'), JSON.stringify({ schemaVersion: '1.0', elidedWalkerMethods: attribution.elidedWalkerMethods ?? [] }, null, 2));
593
+ }
594
+ else {
595
+ verifyRecorder.recordHumanReviewItem({
596
+ severity: 'warning',
597
+ kind: 'attribute-block-missing',
598
+ message: 'Attribute LLM call returned response without AUDIT-ATTRIBUTION block; report assembler falls back to name-similarity heuristic.',
599
+ });
600
+ }
601
+ }
602
+ catch (e) {
603
+ verifyRecorder.recordHumanReviewItem({
604
+ severity: 'warning',
605
+ kind: 'attribute-call-failed',
606
+ message: `Attribute LLM call failed: ${e?.message ?? e}. Report assembler falls back to heuristic.`,
607
+ });
608
+ }
609
+ }
610
+ // Drain verify recorder into sidecars.
611
+ try {
612
+ const verifyDir = join(opts.outputDir, 'verify');
613
+ if (!existsSync(verifyDir))
614
+ mkdirSync(verifyDir, { recursive: true });
615
+ writeFileSync(join(verifyDir, 'invariants.json'), JSON.stringify(verifyRecorder.buildInvariants(), null, 2));
616
+ writeFileSync(join(verifyDir, 'retries.json'), JSON.stringify(verifyRecorder.buildRetries(), null, 2));
617
+ }
618
+ catch (e) {
619
+ console.error(`[audit] verify sidecar emit failed: ${e?.message ?? e}`);
620
+ }
343
621
  // 7. Optional realize.
344
622
  let realize = null;
345
623
  if (opts.realize && validateFinal.ok) {
@@ -396,4 +674,90 @@ export async function runAnalyse(opts) {
396
674
  validateAfterDraftOk: validateAfterDraft.ok,
397
675
  };
398
676
  }
677
+ /**
678
+ * Compute SHA256 fingerprint of a source tree for reproducibility records.
679
+ * Skips node_modules, dist, .git, and any path containing those segments.
680
+ */
681
+ async function computeSourceFingerprint(rootPath) {
682
+ const skipDirs = new Set(['node_modules', 'dist', '.git', '.next', '.turbo', '.cache', '.specverse']);
683
+ const files = [];
684
+ let totalBytes = 0;
685
+ const walk = (dir) => {
686
+ let entries;
687
+ try {
688
+ entries = readdirSync(dir, { withFileTypes: true }).map((e) => ({
689
+ name: e.name,
690
+ isDir: e.isDirectory(),
691
+ }));
692
+ }
693
+ catch {
694
+ return;
695
+ }
696
+ entries.sort((a, b) => a.name.localeCompare(b.name));
697
+ for (const e of entries) {
698
+ if (skipDirs.has(e.name))
699
+ continue;
700
+ const full = join(dir, e.name);
701
+ if (e.isDir) {
702
+ walk(full);
703
+ }
704
+ else {
705
+ try {
706
+ const st = statSync(full);
707
+ if (!st.isFile())
708
+ continue;
709
+ const data = readFileSync(full);
710
+ totalBytes += data.length;
711
+ files.push({
712
+ rel: relative(rootPath, full),
713
+ bytes: data.length,
714
+ hash: createHash('sha256').update(data).digest('hex'),
715
+ });
716
+ }
717
+ catch { /* unreadable file — skip */ }
718
+ }
719
+ }
720
+ };
721
+ walk(rootPath);
722
+ // Combine per-file hashes into a single tree hash (order-stable via sort).
723
+ files.sort((a, b) => a.rel.localeCompare(b.rel));
724
+ const treeHash = createHash('sha256');
725
+ for (const f of files) {
726
+ treeHash.update(f.rel).update('\0').update(f.hash).update('\0');
727
+ }
728
+ return {
729
+ schemaVersion: '1.0',
730
+ rootPath,
731
+ sha256: treeHash.digest('hex'),
732
+ fileCount: files.length,
733
+ byteCount: totalBytes,
734
+ scannedAt: new Date().toISOString(),
735
+ };
736
+ }
737
+ /**
738
+ * Extract the AUDIT-ATTRIBUTION YAML block from a v9.17+ analyse LLM
739
+ * response. The block follows the spec/manifest blocks and is keyed by
740
+ * a sentinel comment on its first line. Returns null when not present
741
+ * (older prompts, or LLM omitted the block).
742
+ */
743
+ async function extractAttributionBlock(text) {
744
+ // Match a fenced yaml block that begins with the AUDIT-ATTRIBUTION sentinel comment.
745
+ const re = /```ya?ml\s*\n#\s*AUDIT-ATTRIBUTION\s*\n([\s\S]*?)```/i;
746
+ const m = text.match(re);
747
+ if (!m)
748
+ return null;
749
+ const body = m[1];
750
+ if (!body)
751
+ return null;
752
+ try {
753
+ const yaml = await import('js-yaml');
754
+ const parsed = yaml.load(body);
755
+ if (parsed && typeof parsed === 'object')
756
+ return parsed;
757
+ return null;
758
+ }
759
+ catch {
760
+ return null;
761
+ }
762
+ }
399
763
  //# sourceMappingURL=analyse-runner.js.map