@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.
- package/dist/ai/analyse-runner.d.ts +16 -0
- package/dist/ai/analyse-runner.d.ts.map +1 -1
- package/dist/ai/analyse-runner.js +417 -53
- package/dist/ai/analyse-runner.js.map +1 -1
- package/dist/ai/microcall-orchestrator.d.ts +187 -0
- package/dist/ai/microcall-orchestrator.d.ts.map +1 -0
- package/dist/ai/microcall-orchestrator.js +673 -0
- package/dist/ai/microcall-orchestrator.js.map +1 -0
- package/dist/ai/skeleton-emitter.d.ts +94 -0
- package/dist/ai/skeleton-emitter.d.ts.map +1 -0
- package/dist/ai/skeleton-emitter.js +752 -0
- package/dist/ai/skeleton-emitter.js.map +1 -0
- package/dist/analyse-prepass/adapters/express-routes.d.ts +71 -0
- package/dist/analyse-prepass/adapters/express-routes.d.ts.map +1 -0
- package/dist/analyse-prepass/adapters/express-routes.js +329 -0
- package/dist/analyse-prepass/adapters/express-routes.js.map +1 -0
- package/dist/analyse-prepass/adapters/typescript-interfaces.d.ts +91 -0
- package/dist/analyse-prepass/adapters/typescript-interfaces.d.ts.map +1 -0
- package/dist/analyse-prepass/adapters/typescript-interfaces.js +411 -0
- package/dist/analyse-prepass/adapters/typescript-interfaces.js.map +1 -0
- package/dist/analyse-prepass/backends/gitnexus.d.ts.map +1 -1
- package/dist/analyse-prepass/backends/gitnexus.js +36 -8
- package/dist/analyse-prepass/backends/gitnexus.js.map +1 -1
- package/dist/analyse-prepass/backends/index.d.ts.map +1 -1
- package/dist/analyse-prepass/backends/index.js +3 -5
- package/dist/analyse-prepass/backends/index.js.map +1 -1
- package/dist/analyse-prepass/behavior-step-classifier.d.ts +3 -0
- package/dist/analyse-prepass/behavior-step-classifier.d.ts.map +1 -1
- package/dist/analyse-prepass/behavior-step-classifier.js +1 -0
- package/dist/analyse-prepass/behavior-step-classifier.js.map +1 -1
- package/dist/analyse-prepass/index.d.ts +69 -0
- package/dist/analyse-prepass/index.d.ts.map +1 -1
- package/dist/analyse-prepass/index.js +385 -17
- package/dist/analyse-prepass/index.js.map +1 -1
- package/dist/analyse-prepass/method-body-walker.d.ts +4 -0
- package/dist/analyse-prepass/method-body-walker.d.ts.map +1 -1
- package/dist/analyse-prepass/method-body-walker.js +14 -0
- package/dist/analyse-prepass/method-body-walker.js.map +1 -1
- package/dist/audit/realize-recorder.d.ts +164 -0
- package/dist/audit/realize-recorder.d.ts.map +1 -0
- package/dist/audit/realize-recorder.js +153 -0
- package/dist/audit/realize-recorder.js.map +1 -0
- package/dist/audit/verify-checks.d.ts +32 -0
- package/dist/audit/verify-checks.d.ts.map +1 -0
- package/dist/audit/verify-checks.js +202 -0
- package/dist/audit/verify-checks.js.map +1 -0
- package/dist/audit/verify-recorder.d.ts +84 -0
- package/dist/audit/verify-recorder.d.ts.map +1 -0
- package/dist/audit/verify-recorder.js +90 -0
- package/dist/audit/verify-recorder.js.map +1 -0
- package/dist/inference/core/specly-converter.d.ts.map +1 -1
- package/dist/inference/core/specly-converter.js +67 -36
- package/dist/inference/core/specly-converter.js.map +1 -1
- package/dist/libs/instance-factories/services/templates/_shared/step-matching.js +39 -15
- package/dist/realize/index.d.ts.map +1 -1
- package/dist/realize/index.js +63 -0
- package/dist/realize/index.js.map +1 -1
- package/libs/instance-factories/services/templates/_shared/step-matching.ts +61 -16
- 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":"
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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 =
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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 =
|
|
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
|