imprint-mcp 0.6.2 → 0.6.3
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/package.json +1 -1
- package/prompts/audit-agent.md +6 -5
- package/prompts/build-planning.md +2 -2
- package/prompts/compile-agent.md +6 -4
- package/prompts/live-verifier-agent.md +24 -11
- package/src/cli.ts +38 -3
- package/src/imprint/agent.ts +7 -2
- package/src/imprint/audit.ts +212 -20
- package/src/imprint/auth-compile-tools.ts +64 -13
- package/src/imprint/compile-agent-types.ts +25 -2
- package/src/imprint/compile-agent.ts +197 -40
- package/src/imprint/compile-tools.ts +164 -22
- package/src/imprint/live-verifier.ts +167 -6
- package/src/imprint/llm.ts +4 -0
- package/src/imprint/mcp-compile-server.ts +188 -83
- package/src/imprint/mcp-server.ts +16 -1
- package/src/imprint/probe-backends.ts +60 -25
- package/src/imprint/teach-state.ts +52 -2
- package/src/imprint/teach.ts +94 -69
|
@@ -11,6 +11,7 @@ import { join as pathJoin } from 'node:path';
|
|
|
11
11
|
import {
|
|
12
12
|
type AgentProgress,
|
|
13
13
|
type AgentResult,
|
|
14
|
+
type AgentTool,
|
|
14
15
|
type OnDeadlineReached,
|
|
15
16
|
doneTool,
|
|
16
17
|
giveUpTool,
|
|
@@ -19,7 +20,12 @@ import {
|
|
|
19
20
|
import { type SharedModuleManifestEntry, resolvePlanSliceFromFile } from './build-plan.ts';
|
|
20
21
|
import { compileViaClaudeCli } from './claude-cli-compile.ts';
|
|
21
22
|
import { compileViaCodexCli } from './codex-cli-compile.ts';
|
|
22
|
-
import
|
|
23
|
+
import {
|
|
24
|
+
type CompileAgentProgress,
|
|
25
|
+
type CompileAgentResult,
|
|
26
|
+
advanceIncompleteSemanticVerificationRuns,
|
|
27
|
+
advanceSemanticVerificationCycle,
|
|
28
|
+
} from './compile-agent-types.ts';
|
|
23
29
|
import { formatCandidateContext, formatToolPlan } from './compile-agent-types.ts';
|
|
24
30
|
import {
|
|
25
31
|
applyLiveVerification,
|
|
@@ -29,11 +35,13 @@ import {
|
|
|
29
35
|
} from './compile-tools.ts';
|
|
30
36
|
import { type Replacement, extractCredentials } from './credential-extract.ts';
|
|
31
37
|
import {
|
|
38
|
+
isInfrastructureOnlyInconclusiveReport,
|
|
32
39
|
mergeSemanticParamVerification,
|
|
33
40
|
runLiveSemanticVerification,
|
|
34
41
|
semanticVerificationFailures,
|
|
35
42
|
} from './live-verifier.ts';
|
|
36
43
|
import {
|
|
44
|
+
DEFAULT_VERIFICATION_PROVIDER,
|
|
37
45
|
type LLMOptions,
|
|
38
46
|
type ProviderName,
|
|
39
47
|
type ToolUseProvider,
|
|
@@ -43,6 +51,11 @@ import {
|
|
|
43
51
|
} from './llm.ts';
|
|
44
52
|
import { loadJsonFile } from './load-json.ts';
|
|
45
53
|
import { createLog } from './log.ts';
|
|
54
|
+
import {
|
|
55
|
+
canAcceptInconclusiveDecision,
|
|
56
|
+
compileArtifactFingerprint,
|
|
57
|
+
inconclusiveDecisionAtSemanticCap,
|
|
58
|
+
} from './mcp-compile-server.ts';
|
|
46
59
|
import { localSiteDir } from './paths.ts';
|
|
47
60
|
import { rebindExistingBackendsCacheToWorkflow } from './probe-backends.ts';
|
|
48
61
|
import { detectPageMintedHeaders, redactSession } from './redact.ts';
|
|
@@ -57,6 +70,31 @@ const log = createLog('compile-agent');
|
|
|
57
70
|
const REPO_ROOT = pathJoin(import.meta.dir, '..', '..');
|
|
58
71
|
const PROMPTS_DIR = pathJoin(REPO_ROOT, 'prompts');
|
|
59
72
|
|
|
73
|
+
function compileDoneTool(): AgentTool {
|
|
74
|
+
const tool = doneTool();
|
|
75
|
+
return {
|
|
76
|
+
...tool,
|
|
77
|
+
description:
|
|
78
|
+
'Call this when compilation is complete. After an infrastructure-only inconclusive live review, call it again on unchanged artifacts with an explicit acceptance decision and reasoning.',
|
|
79
|
+
input_schema: {
|
|
80
|
+
...tool.input_schema,
|
|
81
|
+
properties: {
|
|
82
|
+
...tool.input_schema.properties,
|
|
83
|
+
accept_inconclusive: {
|
|
84
|
+
type: 'boolean',
|
|
85
|
+
description:
|
|
86
|
+
'Explicitly ship an unchanged deterministic artifact after an infrastructure-only inconclusive live review.',
|
|
87
|
+
},
|
|
88
|
+
inconclusive_reason: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description:
|
|
91
|
+
'Concrete compiler reasoning for accepting the unchanged artifact without a liveVerified stamp.',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
60
98
|
let claudeCliCompiler = compileViaClaudeCli;
|
|
61
99
|
let codexCliCompiler = compileViaCodexCli;
|
|
62
100
|
|
|
@@ -237,7 +275,7 @@ export async function compileAgent(opts: CompileAgentOptions): Promise<CompileAg
|
|
|
237
275
|
sharedModules: opts.sharedModules,
|
|
238
276
|
revisionMode: opts.revisionMode,
|
|
239
277
|
}),
|
|
240
|
-
|
|
278
|
+
compileDoneTool(),
|
|
241
279
|
giveUpTool(),
|
|
242
280
|
];
|
|
243
281
|
|
|
@@ -327,12 +365,20 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
|
|
|
327
365
|
let conversationLog: AgentResult['conversationLog'] = [];
|
|
328
366
|
|
|
329
367
|
const MAX_VERIFICATION_CYCLES = 5;
|
|
330
|
-
|
|
368
|
+
const MAX_INCOMPLETE_VERIFIER_RUNS = 2;
|
|
369
|
+
let semanticVerificationCycles = 0;
|
|
370
|
+
let incompleteVerifierRuns = 0;
|
|
371
|
+
let pendingInconclusive:
|
|
372
|
+
| {
|
|
373
|
+
artifactFingerprint: string;
|
|
374
|
+
report: Awaited<ReturnType<typeof runLiveSemanticVerification>>;
|
|
375
|
+
}
|
|
376
|
+
| undefined;
|
|
331
377
|
let result: AgentResult | null = null;
|
|
332
378
|
let currentInitialMessage = initialUserMessage;
|
|
333
379
|
|
|
334
|
-
while (
|
|
335
|
-
|
|
380
|
+
while (true) {
|
|
381
|
+
const currentSemanticCycle = Math.min(semanticVerificationCycles + 1, MAX_VERIFICATION_CYCLES);
|
|
336
382
|
|
|
337
383
|
// Wrap the user's onProgress callback to inject verification cycle info
|
|
338
384
|
const userOnProgress = opts.onProgress;
|
|
@@ -340,7 +386,7 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
|
|
|
340
386
|
? (p: AgentProgress) =>
|
|
341
387
|
userOnProgress({
|
|
342
388
|
...p,
|
|
343
|
-
verificationCycle,
|
|
389
|
+
verificationCycle: currentSemanticCycle,
|
|
344
390
|
maxVerificationCycles: MAX_VERIFICATION_CYCLES,
|
|
345
391
|
})
|
|
346
392
|
: undefined;
|
|
@@ -399,42 +445,48 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
|
|
|
399
445
|
log(`verification warnings (non-blocking):\n${warnings.join('\n')}`);
|
|
400
446
|
}
|
|
401
447
|
|
|
448
|
+
let semanticReviewCompleted = false;
|
|
449
|
+
let semanticReviewAttempted = false;
|
|
402
450
|
if (failures.length === 0) {
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
451
|
+
const doneInput = result.doneInput ?? {};
|
|
452
|
+
const currentFingerprint = compileArtifactFingerprint(absoluteToolDir);
|
|
453
|
+
if (
|
|
454
|
+
pendingInconclusive &&
|
|
455
|
+
canAcceptInconclusiveDecision({
|
|
456
|
+
semanticVerificationCycles,
|
|
457
|
+
maxVerificationCycles: MAX_VERIFICATION_CYCLES,
|
|
458
|
+
completedReview: pendingInconclusive.report.completedReview,
|
|
459
|
+
infrastructureOnly: isInfrastructureOnlyInconclusiveReport(
|
|
460
|
+
pendingInconclusive.report.report,
|
|
461
|
+
),
|
|
462
|
+
pendingFingerprint: pendingInconclusive.artifactFingerprint,
|
|
463
|
+
currentFingerprint,
|
|
464
|
+
acceptInconclusive: doneInput.accept_inconclusive === true,
|
|
465
|
+
inconclusiveReason:
|
|
466
|
+
typeof doneInput.inconclusive_reason === 'string'
|
|
467
|
+
? doneInput.inconclusive_reason
|
|
468
|
+
: undefined,
|
|
469
|
+
})
|
|
470
|
+
) {
|
|
471
|
+
const semantic = pendingInconclusive.report;
|
|
472
|
+
const inconclusiveReason = String(doneInput.inconclusive_reason).trim();
|
|
473
|
+
applyLiveVerification(absoluteToolDir, {
|
|
474
|
+
kind: 'waived-infra',
|
|
475
|
+
firstError: inconclusiveReason,
|
|
476
|
+
exhaustedBackends: [],
|
|
477
|
+
});
|
|
422
478
|
const paramWarnings = applyParamVerification(
|
|
423
479
|
absoluteToolDir,
|
|
424
480
|
mergeSemanticParamVerification(paramVerification, semantic.report),
|
|
425
481
|
);
|
|
426
|
-
// liveVerified/parameter annotations change the hash-strict cache key,
|
|
427
|
-
// but not transport capabilities. Rebind the backend already exercised
|
|
428
|
-
// by the verifier instead of triggering another probe.
|
|
429
482
|
rebindExistingBackendsCacheToWorkflow(absoluteToolDir);
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
483
|
+
warnings.push(
|
|
484
|
+
`independent semantic verification was inconclusive: ${semantic.report.summary}`,
|
|
485
|
+
`compiler accepted the unchanged artifact without live verification: ${inconclusiveReason}`,
|
|
486
|
+
...paramWarnings,
|
|
487
|
+
);
|
|
434
488
|
message = result.doneSummary ?? 'Task completed';
|
|
435
|
-
|
|
436
|
-
message += `\n\nWarnings:\n${allWarnings.join('\n')}`;
|
|
437
|
-
}
|
|
489
|
+
message += `\n\nWarnings:\n${warnings.join('\n')}`;
|
|
438
490
|
if (!opts.keepTest) {
|
|
439
491
|
for (const f of ['parser.test.ts', 'integration.test.ts']) {
|
|
440
492
|
const testPath = pathJoin(absoluteToolDir, f);
|
|
@@ -443,22 +495,127 @@ Begin by calling read_session_summary to orient yourself, then proceed per the s
|
|
|
443
495
|
}
|
|
444
496
|
break;
|
|
445
497
|
}
|
|
498
|
+
|
|
499
|
+
const capDecision = inconclusiveDecisionAtSemanticCap({
|
|
500
|
+
semanticVerificationCycles,
|
|
501
|
+
maxVerificationCycles: MAX_VERIFICATION_CYCLES,
|
|
502
|
+
pendingFingerprint: pendingInconclusive?.artifactFingerprint,
|
|
503
|
+
currentFingerprint,
|
|
504
|
+
});
|
|
505
|
+
if (capDecision === 'fail-artifact-changed') {
|
|
506
|
+
pendingInconclusive = undefined;
|
|
507
|
+
failures.push(
|
|
508
|
+
'compile artifacts changed after the final allowed inconclusive semantic review; refusing to run an additional review beyond the semantic cycle cap',
|
|
509
|
+
);
|
|
510
|
+
} else if (capDecision === 'await-compiler-decision') {
|
|
511
|
+
failures.push(
|
|
512
|
+
'semantic review limit reached with an unchanged infrastructure-only inconclusive result; explicitly accept the unverified artifact with reasoning or revise it',
|
|
513
|
+
);
|
|
514
|
+
} else {
|
|
515
|
+
if (pendingInconclusive && pendingInconclusive.artifactFingerprint !== currentFingerprint) {
|
|
516
|
+
pendingInconclusive = undefined;
|
|
517
|
+
}
|
|
518
|
+
const semanticVerificationStartedAt = Date.now();
|
|
519
|
+
semanticReviewAttempted = true;
|
|
520
|
+
const semantic = await runLiveSemanticVerification({
|
|
521
|
+
provider: DEFAULT_VERIFICATION_PROVIDER,
|
|
522
|
+
toolDir: absoluteToolDir,
|
|
523
|
+
evidence: integrationEvidence,
|
|
524
|
+
// Live semantic verification is a distinct phase. Do not inherit a
|
|
525
|
+
// nearly exhausted compile-agent deadline.
|
|
526
|
+
});
|
|
527
|
+
deadlineMs += Date.now() - semanticVerificationStartedAt;
|
|
528
|
+
semanticReviewCompleted = semantic.completedReview;
|
|
529
|
+
if (semantic.report.status === 'inconclusive') {
|
|
530
|
+
pendingInconclusive = {
|
|
531
|
+
artifactFingerprint: compileArtifactFingerprint(absoluteToolDir),
|
|
532
|
+
report: semantic,
|
|
533
|
+
};
|
|
534
|
+
} else {
|
|
535
|
+
pendingInconclusive = undefined;
|
|
536
|
+
}
|
|
537
|
+
failures.push(...semanticVerificationFailures(semantic.report));
|
|
538
|
+
if (semantic.report.status === 'approved_with_gaps') {
|
|
539
|
+
warnings.push(
|
|
540
|
+
`independent semantic verification approved with gaps: ${semantic.report.gaps.join('; ')}`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
if (failures.length === 0) {
|
|
544
|
+
// Success (possibly with explicit semantic gaps). Persist per-parameter
|
|
545
|
+
// flags and mint liveVerified=true only after independent approval.
|
|
546
|
+
applyLiveVerification(absoluteToolDir, undefined);
|
|
547
|
+
const paramWarnings = applyParamVerification(
|
|
548
|
+
absoluteToolDir,
|
|
549
|
+
mergeSemanticParamVerification(paramVerification, semantic.report),
|
|
550
|
+
);
|
|
551
|
+
// liveVerified/parameter annotations change the hash-strict cache key,
|
|
552
|
+
// but not transport capabilities. Rebind the backend already exercised
|
|
553
|
+
// by the verifier instead of triggering another probe.
|
|
554
|
+
rebindExistingBackendsCacheToWorkflow(absoluteToolDir);
|
|
555
|
+
const allWarnings = [...warnings, ...paramWarnings];
|
|
556
|
+
if (paramWarnings.length > 0) {
|
|
557
|
+
log(`parameter verification:\n${paramWarnings.join('\n')}`);
|
|
558
|
+
}
|
|
559
|
+
message = result.doneSummary ?? 'Task completed';
|
|
560
|
+
if (allWarnings.length > 0) {
|
|
561
|
+
message += `\n\nWarnings:\n${allWarnings.join('\n')}`;
|
|
562
|
+
}
|
|
563
|
+
if (!opts.keepTest) {
|
|
564
|
+
for (const f of ['parser.test.ts', 'integration.test.ts']) {
|
|
565
|
+
const testPath = pathJoin(absoluteToolDir, f);
|
|
566
|
+
if (existsSync(testPath)) unlinkSync(testPath);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
446
572
|
}
|
|
447
573
|
|
|
448
574
|
// Verification failed — re-enter the loop with a continuation message
|
|
449
|
-
|
|
575
|
+
semanticVerificationCycles = advanceSemanticVerificationCycle(
|
|
576
|
+
semanticVerificationCycles,
|
|
577
|
+
semanticReviewCompleted,
|
|
578
|
+
);
|
|
579
|
+
incompleteVerifierRuns = advanceIncompleteSemanticVerificationRuns(
|
|
580
|
+
incompleteVerifierRuns,
|
|
581
|
+
semanticReviewAttempted,
|
|
582
|
+
semanticReviewCompleted,
|
|
583
|
+
);
|
|
584
|
+
if (incompleteVerifierRuns >= MAX_INCOMPLETE_VERIFIER_RUNS) {
|
|
450
585
|
outcome = 'error';
|
|
451
|
-
message = `
|
|
586
|
+
message = `Independent semantic verifier failed to complete after ${MAX_INCOMPLETE_VERIFIER_RUNS} bounded runs. Final failures:\n${failures.join('\n')}`;
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
const awaitingFinalInconclusiveDecision =
|
|
590
|
+
semanticVerificationCycles >= MAX_VERIFICATION_CYCLES && pendingInconclusive !== undefined;
|
|
591
|
+
if (
|
|
592
|
+
semanticVerificationCycles >= MAX_VERIFICATION_CYCLES &&
|
|
593
|
+
!awaitingFinalInconclusiveDecision
|
|
594
|
+
) {
|
|
595
|
+
outcome = 'error';
|
|
596
|
+
message = `Semantic verification failed after ${MAX_VERIFICATION_CYCLES} cycles. Final failures:\n${failures.join('\n')}`;
|
|
452
597
|
break;
|
|
453
598
|
}
|
|
454
599
|
|
|
455
|
-
|
|
456
|
-
|
|
600
|
+
const failurePhase = semanticReviewCompleted
|
|
601
|
+
? `semantic verification failed (cycle ${semanticVerificationCycles}/${MAX_VERIFICATION_CYCLES})`
|
|
602
|
+
: semanticReviewAttempted
|
|
603
|
+
? 'semantic verifier did not complete a review (semantic cycle limit unchanged)'
|
|
604
|
+
: 'deterministic verification failed (semantic cycle limit unchanged)';
|
|
605
|
+
log(`${failurePhase}, resuming agent loop...`);
|
|
606
|
+
currentInitialMessage = `You called done but ${failurePhase}:
|
|
457
607
|
|
|
458
608
|
${failures.map((f) => `- ${f}`).join('\n')}
|
|
459
609
|
|
|
460
610
|
Resume your work. Read the files you wrote (workflow.json, parser.ts, parser.test.ts), fix the issues, re-run tests, and call done again when fixed.`;
|
|
461
|
-
currentInitialMessage += `\n\nUse the verifier's semantic evidence, not a requirement for 100% of the original candidate inputs. If the core intent works but a secondary parameter cannot be supported from the recording and live evidence without guessing, remove that parameter and add workflow.limitations with omittedParameters and a specific reason. Do not use a limitation to hide a broken core tool, and do not keep a known broken or ignored public input.`;
|
|
611
|
+
currentInitialMessage += `\n\nUse the verifier's semantic evidence, not a requirement for 100% of the original candidate inputs. If the core intent works but a secondary parameter cannot be supported from the recording and live evidence without guessing, remove that parameter and add workflow.limitations with omittedParameters and a specific reason. Treat emitsTokens as downstream compatibility targets, not automatic definitions of producer success: when useful core records lack an optional consumer token, preserve the records and supported input classes, document the missing output and affected consumers, and leave those consumers limited/unverified. Narrow an input class only when its core results are unusable or misleading. Do not use a limitation to hide a broken core tool, and do not keep a known broken or ignored public input.`;
|
|
612
|
+
if (pendingInconclusive) {
|
|
613
|
+
currentInitialMessage +=
|
|
614
|
+
semanticVerificationCycles >= MAX_VERIFICATION_CYCLES &&
|
|
615
|
+
pendingInconclusive.report.completedReview
|
|
616
|
+
? '\n\nThe final allowed live semantic review was infrastructure-only inconclusive. Review its evidence and log. If no tool defect is exposed and the unchanged deterministic artifact should ship explicitly unverified, call done again with accept_inconclusive=true and a concrete inconclusive_reason. This does not mint liveVerified.'
|
|
617
|
+
: '\n\nThe live verifier was infrastructure-only inconclusive. Review its evidence and log. Fix any exposed tool defect, or call done normally to request another independent review. Explicit unverified acceptance is available only after the semantic review limit is reached.';
|
|
618
|
+
}
|
|
462
619
|
}
|
|
463
620
|
|
|
464
621
|
// 10. Final flush of the complete conversation log
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
} from './types.ts';
|
|
58
58
|
|
|
59
59
|
const REPO_ROOT = pathJoin(import.meta.dir, '..', '..');
|
|
60
|
+
const MAX_SHARED_MODULE_SOURCE_CHARS = 50_000;
|
|
60
61
|
|
|
61
62
|
// Env var read by the agent-written parser.test.ts to locate the redacted
|
|
62
63
|
// session. The test loads it, finds the load-bearing request seq, and feeds
|
|
@@ -160,11 +161,11 @@ function buildReadBuildPlanTool(
|
|
|
160
161
|
];
|
|
161
162
|
if (emitsTokens.length > 0) {
|
|
162
163
|
tokenNotes.push(
|
|
163
|
-
`PRODUCER
|
|
164
|
+
`PRODUCER COMPATIBILITY TARGET: emit ${emitsTokens
|
|
164
165
|
.map((e) => `\`${e.field}\``)
|
|
165
166
|
.join(
|
|
166
167
|
', ',
|
|
167
|
-
)} in
|
|
168
|
+
)} in the exact shape described whenever a live result supplies or proves the full value a sibling consumer needs. Never fabricate a missing token. Useful core records may retain a null/absent token when workflow.limitations names the missing output and affected consumers; preserve their supported input classes. A missing downstream token alone is not a reason to discard valid producer records.`,
|
|
168
169
|
);
|
|
169
170
|
}
|
|
170
171
|
for (const tp of tokenParams) {
|
|
@@ -175,7 +176,7 @@ function buildReadBuildPlanTool(
|
|
|
175
176
|
? ` The producer declares shape: ${producerShape}. If that shape contains requiredKeys=[...], request-transform.ts must decode/unpack the fresh value and reference each required key in the outgoing request.`
|
|
176
177
|
: '';
|
|
177
178
|
tokenNotes.push(
|
|
178
|
-
`CONSUMER CONTRACT: param \`${tp.param}\` is an opaque token minted by the \`${tp.sourceTool}\` tool's \`${tp.sourceField}\` output.${shapeNote} Write a CHAINED \`param:${tp.param}\` integration test that calls \`runCapturedIntegrationCase\` on \`../${tp.sourceTool}/workflow.json\` directly inside the \`param:${tp.param}\` test block, reads \`${tp.sourceField}\` from the producer's ACTUAL output shape (top-level field, or an item in any returned collection such as flights/items/results), and passes THAT fresh value (not the recorded constant) into this tool — then asserts the response is non-empty. When several params come from the same producer, mint once and choose one producer item containing all sibling fields so the values stay from the same result. On producer bot/infra error, rethrow so the suite waives
|
|
179
|
+
`CONSUMER CONTRACT: param \`${tp.param}\` is an opaque token minted by the \`${tp.sourceTool}\` tool's \`${tp.sourceField}\` output.${shapeNote} Write a CHAINED \`param:${tp.param}\` integration test that calls \`runCapturedIntegrationCase\` on \`../${tp.sourceTool}/workflow.json\` directly inside the \`param:${tp.param}\` test block, reads \`${tp.sourceField}\` from the producer's ACTUAL output shape (top-level field, or an item in any returned collection such as flights/items/results), and passes THAT fresh value (not the recorded constant) into this tool — then asserts the response is non-empty. When several params come from the same producer, mint once and choose one producer item containing all sibling fields so the values stay from the same result. On producer bot/infra error, rethrow so the suite waives. If useful producer records legitimately lack the field, do not fabricate or mutate output: keep this consumer verified:false, waived-chain, or inconclusive with diagnostics, and require an honest producer limitation.`,
|
|
179
180
|
);
|
|
180
181
|
}
|
|
181
182
|
// Per-source wiring guidance for the general dependency contract. These are
|
|
@@ -212,6 +213,7 @@ function buildReadBuildPlanTool(
|
|
|
212
213
|
kind: m.kind,
|
|
213
214
|
purpose: m.purpose,
|
|
214
215
|
exportSignatures: m.exportSignatures,
|
|
216
|
+
...readAssignedSharedModuleSource(buildPlanPath, m.path),
|
|
215
217
|
})),
|
|
216
218
|
parserGuidance: slice.tool.parserGuidance,
|
|
217
219
|
paramChecklist: slice.tool.paramChecklist,
|
|
@@ -238,6 +240,30 @@ function buildReadBuildPlanTool(
|
|
|
238
240
|
};
|
|
239
241
|
}
|
|
240
242
|
|
|
243
|
+
function readAssignedSharedModuleSource(
|
|
244
|
+
buildPlanPath: string,
|
|
245
|
+
modulePath: string,
|
|
246
|
+
): { source?: string; sourceTruncated?: boolean; sourceUnavailable?: string } {
|
|
247
|
+
const siteDir = realpathSync(dirname(buildPlanPath));
|
|
248
|
+
const candidate = pathJoin(siteDir, modulePath);
|
|
249
|
+
if (!existsSync(candidate)) {
|
|
250
|
+
return { sourceUnavailable: 'The verified shared module file is missing.' };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const resolved = realpathSync(candidate);
|
|
254
|
+
const relative = pathRelative(siteDir, resolved);
|
|
255
|
+
if (relative.startsWith('..') || relative === '') {
|
|
256
|
+
return { sourceUnavailable: 'The shared module resolved outside the site directory.' };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const source = readFileSync(resolved, 'utf8');
|
|
260
|
+
if (source.length <= MAX_SHARED_MODULE_SOURCE_CHARS) return { source };
|
|
261
|
+
return {
|
|
262
|
+
source: source.slice(0, MAX_SHARED_MODULE_SOURCE_CHARS),
|
|
263
|
+
sourceTruncated: true,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
241
267
|
// ─── Tool: read_session_summary ──────────────────────────────────────────────
|
|
242
268
|
|
|
243
269
|
export function buildReadSessionSummaryTool(
|
|
@@ -1887,24 +1913,121 @@ interface TestBlock {
|
|
|
1887
1913
|
body: string;
|
|
1888
1914
|
}
|
|
1889
1915
|
|
|
1916
|
+
function matchingClosingBrace(source: string, openIndex: number): number | null {
|
|
1917
|
+
let depth = 0;
|
|
1918
|
+
let quote: "'" | '"' | '`' | null = null;
|
|
1919
|
+
let escaped = false;
|
|
1920
|
+
let lineComment = false;
|
|
1921
|
+
let blockComment = false;
|
|
1922
|
+
for (let index = openIndex; index < source.length; index++) {
|
|
1923
|
+
const char = source[index] ?? '';
|
|
1924
|
+
const next = source[index + 1] ?? '';
|
|
1925
|
+
if (lineComment) {
|
|
1926
|
+
if (char === '\n') lineComment = false;
|
|
1927
|
+
continue;
|
|
1928
|
+
}
|
|
1929
|
+
if (blockComment) {
|
|
1930
|
+
if (char === '*' && next === '/') {
|
|
1931
|
+
blockComment = false;
|
|
1932
|
+
index++;
|
|
1933
|
+
}
|
|
1934
|
+
continue;
|
|
1935
|
+
}
|
|
1936
|
+
if (quote) {
|
|
1937
|
+
if (escaped) {
|
|
1938
|
+
escaped = false;
|
|
1939
|
+
} else if (char === '\\') {
|
|
1940
|
+
escaped = true;
|
|
1941
|
+
} else if (char === quote) {
|
|
1942
|
+
quote = null;
|
|
1943
|
+
}
|
|
1944
|
+
continue;
|
|
1945
|
+
}
|
|
1946
|
+
if (char === '/' && next === '/') {
|
|
1947
|
+
lineComment = true;
|
|
1948
|
+
index++;
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1951
|
+
if (char === '/' && next === '*') {
|
|
1952
|
+
blockComment = true;
|
|
1953
|
+
index++;
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1956
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
1957
|
+
quote = char;
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
if (char === '{') depth++;
|
|
1961
|
+
if (char === '}' && --depth === 0) return index;
|
|
1962
|
+
}
|
|
1963
|
+
return null;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1890
1966
|
function hasCapturedLiveWorkflowCall(source: string): boolean {
|
|
1891
1967
|
return /\b(?:runWorkflowWithLadder|runCapturedIntegrationCase)\s*\(/.test(source);
|
|
1892
1968
|
}
|
|
1893
1969
|
|
|
1894
|
-
/**
|
|
1895
|
-
*
|
|
1896
|
-
*
|
|
1970
|
+
/** Resolve small local function wrappers around a captured workflow call. The
|
|
1971
|
+
* generated suites commonly factor one invocation helper above their tests;
|
|
1972
|
+
* keeping the resolution local prevents a call in an unrelated param test from
|
|
1973
|
+
* laundering a synthetic baseline elsewhere in the file. */
|
|
1974
|
+
function capturedWorkflowHelperNames(source: string): Set<string> {
|
|
1975
|
+
const functionRe = /\b(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{/g;
|
|
1976
|
+
const functions = [...source.matchAll(functionRe)].map((match) => ({
|
|
1977
|
+
name: match[1] ?? '',
|
|
1978
|
+
index: match.index ?? 0,
|
|
1979
|
+
}));
|
|
1980
|
+
const testStarts = [...source.matchAll(/\b(?:test|it)\s*\(/g)].map((match) => match.index ?? 0);
|
|
1981
|
+
const boundaries = [...functions.map((entry) => entry.index), ...testStarts, source.length].sort(
|
|
1982
|
+
(a, b) => a - b,
|
|
1983
|
+
);
|
|
1984
|
+
const bodies = new Map<string, string>();
|
|
1985
|
+
for (const entry of functions) {
|
|
1986
|
+
const end = boundaries.find((boundary) => boundary > entry.index) ?? source.length;
|
|
1987
|
+
bodies.set(entry.name, source.slice(entry.index, end));
|
|
1988
|
+
}
|
|
1989
|
+
const captured = new Set<string>();
|
|
1990
|
+
let changed = true;
|
|
1991
|
+
while (changed) {
|
|
1992
|
+
changed = false;
|
|
1993
|
+
for (const [name, body] of bodies) {
|
|
1994
|
+
if (captured.has(name)) continue;
|
|
1995
|
+
const callsCapturedHelper = [...captured].some((helper) =>
|
|
1996
|
+
new RegExp(`\\b${helper.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\(`).test(body),
|
|
1997
|
+
);
|
|
1998
|
+
if (hasCapturedLiveWorkflowCall(body) || callsCapturedHelper) {
|
|
1999
|
+
captured.add(name);
|
|
2000
|
+
changed = true;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
return captured;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
/** Split a test file into `test(...)` / `it(...)` blocks (title + callback
|
|
2008
|
+
* source). Good enough to check whether a named per-parameter test's body
|
|
2009
|
+
* actually calls the workflow. */
|
|
1897
2010
|
export function extractTestBlocks(src: string): TestBlock[] {
|
|
1898
2011
|
const re = /\b(?:test|it)\s*\(\s*(['"`])((?:\\.|(?!\1).)*)\1/g;
|
|
1899
|
-
const starts: Array<{ index: number; title: string }> = [];
|
|
2012
|
+
const starts: Array<{ index: number; title: string; afterTitle: number }> = [];
|
|
1900
2013
|
for (const m of src.matchAll(re)) {
|
|
1901
|
-
starts.push({
|
|
2014
|
+
starts.push({
|
|
2015
|
+
index: m.index ?? 0,
|
|
2016
|
+
title: m[2] ?? '',
|
|
2017
|
+
afterTitle: (m.index ?? 0) + m[0].length,
|
|
2018
|
+
});
|
|
1902
2019
|
}
|
|
1903
2020
|
const blocks: TestBlock[] = [];
|
|
1904
2021
|
for (let i = 0; i < starts.length; i++) {
|
|
1905
2022
|
const start = starts[i];
|
|
1906
2023
|
if (!start) continue;
|
|
1907
|
-
const
|
|
2024
|
+
const fallbackEnd = i + 1 < starts.length ? (starts[i + 1]?.index ?? src.length) : src.length;
|
|
2025
|
+
const callbackOpen = src.indexOf('{', start.afterTitle);
|
|
2026
|
+
const callbackClose =
|
|
2027
|
+
callbackOpen >= 0 && callbackOpen < fallbackEnd
|
|
2028
|
+
? matchingClosingBrace(src, callbackOpen)
|
|
2029
|
+
: null;
|
|
2030
|
+
const end = callbackClose === null ? fallbackEnd : callbackClose + 1;
|
|
1908
2031
|
blocks.push({ title: start.title, body: src.slice(start.index, end) });
|
|
1909
2032
|
}
|
|
1910
2033
|
return blocks;
|
|
@@ -2287,6 +2410,7 @@ export function classifyIntegrationOutcome(input: {
|
|
|
2287
2410
|
* - chained pass → `{ verified: true, sourcedFrom }`
|
|
2288
2411
|
* - passed but not chained (the recorded-value tautology) → `unchained` (blocking)
|
|
2289
2412
|
* - suite waived (producer anti-bot) → `{ verified: false, reason: 'waived-chain' }`
|
|
2413
|
+
* - explicitly annotated unavailable producer output → `{ verified: false, reason: 'waived-chain' }`
|
|
2290
2414
|
* - else → `unchained` (blocking)
|
|
2291
2415
|
*/
|
|
2292
2416
|
export function classifyParamCoverage(opts: {
|
|
@@ -2321,6 +2445,10 @@ export function classifyParamCoverage(opts: {
|
|
|
2321
2445
|
const token = `param:${lp.name}`;
|
|
2322
2446
|
const passedLive = [...opts.passedTests].some((n) => n.includes(token));
|
|
2323
2447
|
const block = blocks.find((b) => b.title.includes(token));
|
|
2448
|
+
const annotationRe = new RegExp(
|
|
2449
|
+
`//\\s*exposed-but-not-verified[^\\n]*\\b${lp.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`,
|
|
2450
|
+
);
|
|
2451
|
+
const isAnnotated = annotationRe.test(opts.integrationSrc);
|
|
2324
2452
|
|
|
2325
2453
|
// Producer-sourced token param: requires a chained test that mints a fresh
|
|
2326
2454
|
// value from the producer's sibling workflow.
|
|
@@ -2344,17 +2472,24 @@ export function classifyParamCoverage(opts: {
|
|
|
2344
2472
|
reason: 'waived-chain',
|
|
2345
2473
|
sourcedFrom,
|
|
2346
2474
|
});
|
|
2475
|
+
} else if (isAnnotated) {
|
|
2476
|
+
// The compile agent may explicitly preserve a limited consumer when
|
|
2477
|
+
// its producer returns useful core records but cannot mint this token.
|
|
2478
|
+
// The durable verified:false+sourcedFrom stamp keeps orchestration
|
|
2479
|
+
// honest; opaque defaults and passing recorded-token tests remain
|
|
2480
|
+
// blocked by the independent guards above and below.
|
|
2481
|
+
paramVerification.push({
|
|
2482
|
+
name: lp.name,
|
|
2483
|
+
verified: false,
|
|
2484
|
+
reason: 'waived-chain',
|
|
2485
|
+
sourcedFrom,
|
|
2486
|
+
});
|
|
2347
2487
|
} else {
|
|
2348
2488
|
unchained.push(lp.name);
|
|
2349
2489
|
}
|
|
2350
2490
|
continue;
|
|
2351
2491
|
}
|
|
2352
2492
|
|
|
2353
|
-
const annotationRe = new RegExp(
|
|
2354
|
-
`//\\s*exposed-but-not-verified[^\\n]*\\b${lp.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`,
|
|
2355
|
-
);
|
|
2356
|
-
const isAnnotated = annotationRe.test(opts.integrationSrc);
|
|
2357
|
-
|
|
2358
2493
|
if (passedLive) {
|
|
2359
2494
|
// Anti-tautology: a passing per-param test must actually exercise the live
|
|
2360
2495
|
// workflow, not assert a constant.
|
|
@@ -2388,13 +2523,20 @@ export function hasLiveBaselineWorkflowTest(integrationSrc: string): boolean {
|
|
|
2388
2523
|
const throwsOnFailure = /if\s*\(\s*!result\.ok\s*\)\s*\{[\s\S]{0,500}\bthrow\b/;
|
|
2389
2524
|
const returnsOnFailedResult =
|
|
2390
2525
|
/if\s*\([^)]*![^)]*\.result\.ok[\s\S]{0,200}\)\s*(?:\{[\s\S]{0,300}\breturn\b|return\b)/;
|
|
2391
|
-
|
|
2392
|
-
|
|
2526
|
+
const capturedHelpers = capturedWorkflowHelperNames(integrationSrc);
|
|
2527
|
+
return extractTestBlocks(integrationSrc).some((block) => {
|
|
2528
|
+
const invokesCapturedWorkflow =
|
|
2529
|
+
hasCapturedLiveWorkflowCall(block.body) ||
|
|
2530
|
+
[...capturedHelpers].some((helper) =>
|
|
2531
|
+
new RegExp(`\\b${helper.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\(`).test(block.body),
|
|
2532
|
+
);
|
|
2533
|
+
return (
|
|
2393
2534
|
!block.title.includes('param:') &&
|
|
2394
|
-
|
|
2535
|
+
invokesCapturedWorkflow &&
|
|
2395
2536
|
!returnsOnFailedResult.test(block.body) &&
|
|
2396
|
-
(requiresOk.test(block.body) || throwsOnFailure.test(block.body))
|
|
2397
|
-
|
|
2537
|
+
(requiresOk.test(block.body) || throwsOnFailure.test(block.body))
|
|
2538
|
+
);
|
|
2539
|
+
});
|
|
2398
2540
|
}
|
|
2399
2541
|
|
|
2400
2542
|
/**
|
|
@@ -4149,7 +4291,7 @@ export async function externalVerification(
|
|
|
4149
4291
|
})
|
|
4150
4292
|
.join(', ');
|
|
4151
4293
|
failures.push(
|
|
4152
|
-
`${coverage.unchained.length} producer-sourced token param(s) lack a CHAINED \`param:<name>\` test that mints a FRESH value from the producer tool: ${details}. Each test must call runWorkflowWithLadder on the named producer's \`workflow.json\`, read the named field from its result, and pass THAT value (not the recorded constant) into this tool — then assert the response is non-empty. If
|
|
4294
|
+
`${coverage.unchained.length} producer-sourced token param(s) lack a CHAINED \`param:<name>\` test that mints a FRESH value from the producer tool: ${details}. Each test must call runWorkflowWithLadder on the named producer's \`workflow.json\`, read the named field from its result, and pass THAT value (not the recorded constant) into this tool — then assert the response is non-empty. If live producer results legitimately cannot supply the token, keep the consumer explicitly unverified with an \`// exposed-but-not-verified: <param> ...\` annotation and document the producer/consumer limitation instead of fabricating it. See prompts/compile-agent.md "Producer-sourced token parameters".`,
|
|
4153
4295
|
);
|
|
4154
4296
|
}
|
|
4155
4297
|
}
|
|
@@ -4175,9 +4317,9 @@ export async function externalVerification(
|
|
|
4175
4317
|
missing.length === 1 ? 'it' : 'them'
|
|
4176
4318
|
} as an input token, but parser.ts does not emit ${
|
|
4177
4319
|
missing.length === 1 ? 'that field' : 'those fields'
|
|
4178
|
-
}.
|
|
4320
|
+
}. Reference ${
|
|
4179
4321
|
missing.length === 1 ? 'it' : 'each'
|
|
4180
|
-
}
|
|
4322
|
+
} under the EXACT field name and emit the full value whenever live evidence supplies it; null/absent values are allowed only for useful records whose missing output and affected consumers are documented in workflow.limitations — see read_build_plan "emitsTokens".`,
|
|
4181
4323
|
);
|
|
4182
4324
|
}
|
|
4183
4325
|
|