imprint-mcp 0.6.0 → 0.6.2
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/README.md +2 -0
- package/package.json +4 -2
- package/prompts/audit-agent.md +4 -2
- package/prompts/auth-compile-agent.md +149 -158
- package/prompts/build-planning.md +14 -9
- package/prompts/compile-agent.md +46 -23
- package/prompts/live-verifier-agent.md +101 -0
- package/prompts/playbook-compilation.md +8 -1
- package/prompts/tool-candidate-detection.md +9 -29
- package/src/cli.ts +83 -1
- package/src/imprint/audit.ts +219 -20
- package/src/imprint/auth-compile-agent.ts +215 -155
- package/src/imprint/auth-compile-tools.ts +300 -118
- package/src/imprint/auth-verifier.ts +167 -180
- package/src/imprint/backend-cache.ts +18 -11
- package/src/imprint/backend-ladder.ts +225 -177
- package/src/imprint/build-plan.ts +3 -8
- package/src/imprint/cdp-browser-fetch.ts +930 -83
- package/src/imprint/claude-cli-compile.ts +46 -20
- package/src/imprint/codex-cli-compile.ts +436 -139
- package/src/imprint/compile-agent-types.ts +15 -10
- package/src/imprint/compile-agent.ts +88 -28
- package/src/imprint/compile-tools.ts +228 -39
- package/src/imprint/compile-verification.ts +187 -0
- package/src/imprint/compile.ts +71 -2
- package/src/imprint/credential-store.ts +57 -44
- package/src/imprint/emit.ts +3 -1
- package/src/imprint/freeform-redact.ts +27 -6
- package/src/imprint/live-verifier.ts +1665 -0
- package/src/imprint/llm.ts +22 -3
- package/src/imprint/login.ts +107 -25
- package/src/imprint/mcp-compile-server.ts +447 -114
- package/src/imprint/mcp-live-verifier-server.ts +359 -0
- package/src/imprint/mcp-server.ts +116 -35
- package/src/imprint/probe-backends.ts +88 -7
- package/src/imprint/recording-request.ts +36 -0
- package/src/imprint/request-capture.ts +40 -17
- package/src/imprint/runtime.ts +490 -337
- package/src/imprint/teach-state.ts +36 -2
- package/src/imprint/teach.ts +302 -146
- package/src/imprint/tool-candidates.ts +8 -10
- package/src/imprint/tool-loader.ts +12 -4
- package/src/imprint/types.ts +137 -68
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
existsSync,
|
|
12
12
|
mkdirSync,
|
|
13
13
|
readFileSync,
|
|
14
|
+
readdirSync,
|
|
14
15
|
realpathSync,
|
|
16
|
+
statSync,
|
|
15
17
|
unlinkSync,
|
|
16
18
|
writeFileSync,
|
|
17
19
|
} from 'node:fs';
|
|
@@ -27,6 +29,11 @@ import {
|
|
|
27
29
|
readBuildPlanFile,
|
|
28
30
|
resolveAssignedModules,
|
|
29
31
|
} from './build-plan.ts';
|
|
32
|
+
import {
|
|
33
|
+
LIVE_EVIDENCE_PATH_ENV,
|
|
34
|
+
type LiveIntegrationEvidence,
|
|
35
|
+
readLiveIntegrationEvidence,
|
|
36
|
+
} from './compile-verification.ts';
|
|
30
37
|
import { splitSetCookieHeader } from './cookie-jar.ts';
|
|
31
38
|
import { isSameRegistrableDomain, registrableDomain } from './etld.ts';
|
|
32
39
|
import {
|
|
@@ -80,8 +87,9 @@ export function buildCompileTools(
|
|
|
80
87
|
? { IMPRINT_TEACH_CREDENTIALS: JSON.stringify(context.teachCredentials) }
|
|
81
88
|
: undefined;
|
|
82
89
|
const tools = [
|
|
83
|
-
buildReadSessionSummaryTool(session, context),
|
|
90
|
+
buildReadSessionSummaryTool(session, context, toolDir),
|
|
84
91
|
buildReadRequestTool(session),
|
|
92
|
+
buildSearchRequestsTool(session),
|
|
85
93
|
buildRevealRequestTool(sessionPath),
|
|
86
94
|
buildDiffRequestForEventTool(session, context),
|
|
87
95
|
buildReadResponseBodyTool(session),
|
|
@@ -114,6 +122,10 @@ export interface CompileToolContext {
|
|
|
114
122
|
buildPlanPath?: string;
|
|
115
123
|
/** Shared-module build manifest (verified flags) for this site. */
|
|
116
124
|
sharedModules?: SharedModuleManifestEntry[];
|
|
125
|
+
/** A bounded teach resume is revising an already-generated tool. Surface the
|
|
126
|
+
* existing artifact and durable verifier/audit feedback to the agent so it
|
|
127
|
+
* preserves proven behavior instead of re-deriving the tool from raw capture. */
|
|
128
|
+
revisionMode?: boolean;
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
// ─── Tool: read_build_plan ───────────────────────────────────────────────────
|
|
@@ -163,7 +175,7 @@ function buildReadBuildPlanTool(
|
|
|
163
175
|
? ` 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.`
|
|
164
176
|
: '';
|
|
165
177
|
tokenNotes.push(
|
|
166
|
-
`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 \`
|
|
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; if the producer ran but the field is absent, fix the producer parser/output contract instead of waiving. Do not fabricate a fallback value or mutate producer.result.data to make the test pass.`,
|
|
167
179
|
);
|
|
168
180
|
}
|
|
169
181
|
// Per-source wiring guidance for the general dependency contract. These are
|
|
@@ -231,6 +243,7 @@ function buildReadBuildPlanTool(
|
|
|
231
243
|
export function buildReadSessionSummaryTool(
|
|
232
244
|
session: Session,
|
|
233
245
|
context: CompileToolContext,
|
|
246
|
+
toolDir?: string,
|
|
234
247
|
): AgentTool {
|
|
235
248
|
return {
|
|
236
249
|
name: 'read_session_summary',
|
|
@@ -342,6 +355,10 @@ export function buildReadSessionSummaryTool(
|
|
|
342
355
|
captureHints: captureHints.length > 0 ? captureHints : undefined,
|
|
343
356
|
paramGroundingHints: paramGroundingHints.length > 0 ? paramGroundingHints : undefined,
|
|
344
357
|
inputProvenanceHints: inputProvenanceHints.length > 0 ? inputProvenanceHints : undefined,
|
|
358
|
+
revisionContext:
|
|
359
|
+
context.revisionMode && toolDir
|
|
360
|
+
? buildExistingArtifactRevisionContext(toolDir)
|
|
361
|
+
: undefined,
|
|
345
362
|
loadBearingRequests,
|
|
346
363
|
};
|
|
347
364
|
|
|
@@ -360,6 +377,65 @@ export function buildReadSessionSummaryTool(
|
|
|
360
377
|
};
|
|
361
378
|
}
|
|
362
379
|
|
|
380
|
+
const REVISION_ARTIFACT_NAMES = [
|
|
381
|
+
'workflow.json',
|
|
382
|
+
'parser.ts',
|
|
383
|
+
'request-transform.ts',
|
|
384
|
+
'integration.test.ts',
|
|
385
|
+
'playbook.yaml',
|
|
386
|
+
] as const;
|
|
387
|
+
|
|
388
|
+
const REVISION_DIAGNOSTIC_NAMES = [
|
|
389
|
+
'.live-verification.json',
|
|
390
|
+
'.live-verification-evidence.json',
|
|
391
|
+
'.live-verifier-log.jsonl',
|
|
392
|
+
] as const;
|
|
393
|
+
|
|
394
|
+
/** Describe, but do not interpret or gate on, prior compile evidence. The
|
|
395
|
+
* compile agent owns the judgment: it can read any listed file through the
|
|
396
|
+
* existing read_file tool and decide what to preserve, repair, narrow, or
|
|
397
|
+
* document. This is intentionally agent context rather than another runtime
|
|
398
|
+
* validator. */
|
|
399
|
+
function buildExistingArtifactRevisionContext(toolDir: string): Record<string, unknown> {
|
|
400
|
+
const describe = (name: string): { path: string; bytes: number } | null => {
|
|
401
|
+
const absolute = pathJoin(toolDir, name);
|
|
402
|
+
if (!existsSync(absolute)) return null;
|
|
403
|
+
try {
|
|
404
|
+
return { path: name, bytes: statSync(absolute).size };
|
|
405
|
+
} catch {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const artifacts = REVISION_ARTIFACT_NAMES.map(describe).filter(
|
|
411
|
+
(entry): entry is { path: string; bytes: number } => entry !== null,
|
|
412
|
+
);
|
|
413
|
+
const diagnostics = REVISION_DIAGNOSTIC_NAMES.map(describe).filter(
|
|
414
|
+
(entry): entry is { path: string; bytes: number } => entry !== null,
|
|
415
|
+
);
|
|
416
|
+
const notesDir = pathJoin(toolDir, 'notes');
|
|
417
|
+
const feedbackNotes = existsSync(notesDir)
|
|
418
|
+
? readdirSync(notesDir)
|
|
419
|
+
.filter((name) => name.endsWith('.md'))
|
|
420
|
+
.sort()
|
|
421
|
+
.map((name) => describe(pathJoin('notes', name)))
|
|
422
|
+
.filter((entry): entry is { path: string; bytes: number } => entry !== null)
|
|
423
|
+
: [];
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
mode: 'revise_existing_artifact',
|
|
427
|
+
instruction: [
|
|
428
|
+
'This bounded resume is a revision, not a from-scratch compile.',
|
|
429
|
+
'Read the existing workflow, parser, request transform, integration test, durable live-verification diagnostics, and feedback notes before re-reading raw recording bodies.',
|
|
430
|
+
'Preserve behavior that live evidence proves works. Repair or narrow only the behavior contradicted by evidence.',
|
|
431
|
+
'If a secondary branch cannot be grounded, remove its dependent parameters and claims together and record the reasoning in workflow limitations; do not guess.',
|
|
432
|
+
].join(' '),
|
|
433
|
+
existingArtifacts: artifacts,
|
|
434
|
+
durableDiagnostics: diagnostics,
|
|
435
|
+
feedbackNotes,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
363
439
|
// ─── Inline request/response data for candidate-scoped requests ─────────────
|
|
364
440
|
|
|
365
441
|
// claude-cli truncates tool results > ~40K chars. Keep the total summary
|
|
@@ -891,6 +967,63 @@ export function buildReadRequestTool(session: Session): AgentTool {
|
|
|
891
967
|
};
|
|
892
968
|
}
|
|
893
969
|
|
|
970
|
+
export function buildSearchRequestsTool(session: Session): AgentTool {
|
|
971
|
+
return {
|
|
972
|
+
name: 'search_requests',
|
|
973
|
+
description:
|
|
974
|
+
'Search recorded requests by method, resource type, URL fragment, response status, and sequence range. Returns exact seq IDs for read_request/read_response_body.',
|
|
975
|
+
input_schema: {
|
|
976
|
+
type: 'object',
|
|
977
|
+
properties: {
|
|
978
|
+
method: { type: 'string' },
|
|
979
|
+
resourceType: { type: 'string' },
|
|
980
|
+
urlContains: { type: 'string' },
|
|
981
|
+
status: { type: 'number' },
|
|
982
|
+
afterSeq: { type: 'number' },
|
|
983
|
+
beforeSeq: { type: 'number' },
|
|
984
|
+
limit: { type: 'number', description: 'Default 50, maximum 200' },
|
|
985
|
+
},
|
|
986
|
+
required: [],
|
|
987
|
+
},
|
|
988
|
+
handler: async (input: unknown) => {
|
|
989
|
+
const filters = input as {
|
|
990
|
+
method?: string;
|
|
991
|
+
resourceType?: string;
|
|
992
|
+
urlContains?: string;
|
|
993
|
+
status?: number;
|
|
994
|
+
afterSeq?: number;
|
|
995
|
+
beforeSeq?: number;
|
|
996
|
+
limit?: number;
|
|
997
|
+
};
|
|
998
|
+
const method = filters.method?.toUpperCase();
|
|
999
|
+
const resourceType = filters.resourceType?.toLowerCase();
|
|
1000
|
+
const url = filters.urlContains?.toLowerCase();
|
|
1001
|
+
const requests = session.requests
|
|
1002
|
+
.filter((request) => {
|
|
1003
|
+
if (method && request.method.toUpperCase() !== method) return false;
|
|
1004
|
+
if (resourceType && request.resourceType?.toLowerCase() !== resourceType) return false;
|
|
1005
|
+
if (url && !request.url.toLowerCase().includes(url)) return false;
|
|
1006
|
+
if (filters.status !== undefined && request.response?.status !== filters.status)
|
|
1007
|
+
return false;
|
|
1008
|
+
if (filters.afterSeq !== undefined && request.seq <= filters.afterSeq) return false;
|
|
1009
|
+
if (filters.beforeSeq !== undefined && request.seq >= filters.beforeSeq) return false;
|
|
1010
|
+
return true;
|
|
1011
|
+
})
|
|
1012
|
+
.sort((a, b) => a.seq - b.seq)
|
|
1013
|
+
.slice(0, Math.max(1, Math.min(200, Math.trunc(filters.limit ?? 50))))
|
|
1014
|
+
.map((request) => ({
|
|
1015
|
+
seq: request.seq,
|
|
1016
|
+
resourceType: request.resourceType,
|
|
1017
|
+
method: request.method,
|
|
1018
|
+
url: request.url,
|
|
1019
|
+
status: request.response?.status,
|
|
1020
|
+
mimeType: request.response?.mimeType,
|
|
1021
|
+
}));
|
|
1022
|
+
return { result: JSON.stringify({ count: requests.length, requests }, null, 2) };
|
|
1023
|
+
},
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
|
|
894
1027
|
// ─── Tool: reveal_request ─────────────────────────────────────────────────────
|
|
895
1028
|
|
|
896
1029
|
/** Read the raw recording from disk, bypassing the in-memory (possibly-redacted)
|
|
@@ -1287,6 +1420,7 @@ export async function runCommand(
|
|
|
1287
1420
|
cwd: string,
|
|
1288
1421
|
timeoutMs: number,
|
|
1289
1422
|
extraEnv?: Record<string, string>,
|
|
1423
|
+
onOutput?: (stream: 'stdout' | 'stderr', chunk: string) => void,
|
|
1290
1424
|
): Promise<{ result: string; isError?: boolean }> {
|
|
1291
1425
|
return new Promise((resolve) => {
|
|
1292
1426
|
// `detached: true` makes the child its own process-group leader so a timeout
|
|
@@ -1304,11 +1438,15 @@ export async function runCommand(
|
|
|
1304
1438
|
const TRUNCATE_LIMIT = 16 * 1024; // 16KB
|
|
1305
1439
|
|
|
1306
1440
|
proc.stdout.on('data', (chunk) => {
|
|
1307
|
-
|
|
1441
|
+
const text = chunk.toString();
|
|
1442
|
+
stdout += text;
|
|
1443
|
+
onOutput?.('stdout', text);
|
|
1308
1444
|
});
|
|
1309
1445
|
|
|
1310
1446
|
proc.stderr.on('data', (chunk) => {
|
|
1311
|
-
|
|
1447
|
+
const text = chunk.toString();
|
|
1448
|
+
stderr += text;
|
|
1449
|
+
onOutput?.('stderr', text);
|
|
1312
1450
|
});
|
|
1313
1451
|
|
|
1314
1452
|
const timeout = setTimeout(() => {
|
|
@@ -1658,7 +1796,7 @@ interface BunTestRun {
|
|
|
1658
1796
|
|
|
1659
1797
|
/** Per-exposed-parameter verification outcome. `verified` is true only when a
|
|
1660
1798
|
* `param:<name>` integration test actually ran green against live data. */
|
|
1661
|
-
interface ParamVerification {
|
|
1799
|
+
export interface ParamVerification {
|
|
1662
1800
|
name: string;
|
|
1663
1801
|
verified: boolean;
|
|
1664
1802
|
/** Why an exposed param is unverified. Undefined when `verified` is true.
|
|
@@ -1669,7 +1807,7 @@ interface ParamVerification {
|
|
|
1669
1807
|
* - `waived-chain`: the param is a producer-sourced token but the producer
|
|
1670
1808
|
* tool could not be run at compile time (anti-bot / not compiled), so the
|
|
1671
1809
|
* chain could not be verified. */
|
|
1672
|
-
reason?: 'waived-bot' | 'waived-infra' | 'annotated' | 'waived-chain';
|
|
1810
|
+
reason?: 'waived-bot' | 'waived-infra' | 'annotated' | 'waived-chain' | 'semantic-gap';
|
|
1673
1811
|
/** For a producer-sourced token param, the sibling tool + output field its
|
|
1674
1812
|
* value comes from. Stamped into workflow.json (`param.sourcedFrom`) so the
|
|
1675
1813
|
* MCP description tells the orchestrating LLM where to mint it and the audit
|
|
@@ -1693,11 +1831,15 @@ interface TokenSource {
|
|
|
1693
1831
|
* bot-defense / infra detection and error surfacing) and the per-test pass/fail
|
|
1694
1832
|
* names via a JUnit report written to a transient file in the tool dir.
|
|
1695
1833
|
*/
|
|
1696
|
-
async function runBunTestWithResults(
|
|
1834
|
+
export async function runBunTestWithResults(
|
|
1697
1835
|
testPath: string,
|
|
1698
1836
|
toolDir: string,
|
|
1699
1837
|
timeoutMs: number,
|
|
1700
1838
|
env: Record<string, string> = {},
|
|
1839
|
+
opts: {
|
|
1840
|
+
bail?: boolean;
|
|
1841
|
+
onOutput?: (stream: 'stdout' | 'stderr', chunk: string) => void;
|
|
1842
|
+
} = {},
|
|
1701
1843
|
): Promise<BunTestRun> {
|
|
1702
1844
|
const junitPath = pathJoin(toolDir, `.imprint-junit-${basename(testPath)}.xml`);
|
|
1703
1845
|
try {
|
|
@@ -1706,10 +1848,11 @@ async function runBunTestWithResults(
|
|
|
1706
1848
|
// best-effort
|
|
1707
1849
|
}
|
|
1708
1850
|
const result = await runCommand(
|
|
1709
|
-
`bun test ${testPath} --reporter=junit --reporter-outfile=${junitPath}`,
|
|
1851
|
+
`bun test ${testPath} --reporter=junit --reporter-outfile=${junitPath}${opts.bail ? ' --bail=1' : ''}`,
|
|
1710
1852
|
toolDir,
|
|
1711
1853
|
timeoutMs,
|
|
1712
1854
|
env,
|
|
1855
|
+
opts.onOutput,
|
|
1713
1856
|
);
|
|
1714
1857
|
const output = JSON.parse(result.result) as {
|
|
1715
1858
|
stdout: string;
|
|
@@ -1744,6 +1887,10 @@ interface TestBlock {
|
|
|
1744
1887
|
body: string;
|
|
1745
1888
|
}
|
|
1746
1889
|
|
|
1890
|
+
function hasCapturedLiveWorkflowCall(source: string): boolean {
|
|
1891
|
+
return /\b(?:runWorkflowWithLadder|runCapturedIntegrationCase)\s*\(/.test(source);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1747
1894
|
/** Split a test file into `test(...)` / `it(...)` blocks (title + source from
|
|
1748
1895
|
* that test's start to the next test's start). Good enough to check whether a
|
|
1749
1896
|
* named per-parameter test's body actually calls the workflow. */
|
|
@@ -2183,7 +2330,7 @@ export function classifyParamCoverage(opts: {
|
|
|
2183
2330
|
if (passedLive) {
|
|
2184
2331
|
const chained =
|
|
2185
2332
|
!!block &&
|
|
2186
|
-
|
|
2333
|
+
hasCapturedLiveWorkflowCall(block.body) &&
|
|
2187
2334
|
SIBLING_WORKFLOW_RE.test(block.body);
|
|
2188
2335
|
if (chained) {
|
|
2189
2336
|
paramVerification.push({ name: lp.name, verified: true, sourcedFrom });
|
|
@@ -2211,7 +2358,7 @@ export function classifyParamCoverage(opts: {
|
|
|
2211
2358
|
if (passedLive) {
|
|
2212
2359
|
// Anti-tautology: a passing per-param test must actually exercise the live
|
|
2213
2360
|
// workflow, not assert a constant.
|
|
2214
|
-
if (block &&
|
|
2361
|
+
if (block && !hasCapturedLiveWorkflowCall(block.body)) {
|
|
2215
2362
|
tautological.push(lp.name);
|
|
2216
2363
|
} else {
|
|
2217
2364
|
paramVerification.push({ name: lp.name, verified: true });
|
|
@@ -2244,7 +2391,7 @@ export function hasLiveBaselineWorkflowTest(integrationSrc: string): boolean {
|
|
|
2244
2391
|
return extractTestBlocks(integrationSrc).some(
|
|
2245
2392
|
(block) =>
|
|
2246
2393
|
!block.title.includes('param:') &&
|
|
2247
|
-
|
|
2394
|
+
hasCapturedLiveWorkflowCall(block.body) &&
|
|
2248
2395
|
!returnsOnFailedResult.test(block.body) &&
|
|
2249
2396
|
(requiresOk.test(block.body) || throwsOnFailure.test(block.body)),
|
|
2250
2397
|
);
|
|
@@ -3307,7 +3454,7 @@ export function contractedInputGate(
|
|
|
3307
3454
|
unresolved++;
|
|
3308
3455
|
const how =
|
|
3309
3456
|
ri.source === 'auth'
|
|
3310
|
-
? `wire it as \`${token}\` (the authenticate tool
|
|
3457
|
+
? `wire it as \`${token}\` (the authenticate tool names it in authConfig.persist)`
|
|
3311
3458
|
: ri.source === 'browser_state'
|
|
3312
3459
|
? `wire it as \`${token}\` and add the capture/bootstrap that produces it`
|
|
3313
3460
|
: ri.source === 'generated'
|
|
@@ -3369,6 +3516,10 @@ export async function externalVerification(
|
|
|
3369
3516
|
* verifier blocks invented ${credential.X} placeholders that are not in this
|
|
3370
3517
|
* set or in requiredInputs. */
|
|
3371
3518
|
credentialNames?: string[];
|
|
3519
|
+
/** Agentic compile path only: validate the integration suite's source here,
|
|
3520
|
+
* but let the independent semantic verifier invoke the final live suite as
|
|
3521
|
+
* a tool. Parser/type/schema checks remain deterministic. */
|
|
3522
|
+
deferLiveIntegrationToSemanticAgent?: boolean;
|
|
3372
3523
|
} = {},
|
|
3373
3524
|
): Promise<{
|
|
3374
3525
|
failures: string[];
|
|
@@ -3382,9 +3533,13 @@ export async function externalVerification(
|
|
|
3382
3533
|
firstError: string;
|
|
3383
3534
|
exhaustedBackends: string[];
|
|
3384
3535
|
};
|
|
3536
|
+
/** Actual tool-level inputs and parsed results captured by the single live
|
|
3537
|
+
* integration run. The caller hands these to the independent semantic agent. */
|
|
3538
|
+
integrationEvidence: LiveIntegrationEvidence[];
|
|
3385
3539
|
}> {
|
|
3386
3540
|
const failures: string[] = [];
|
|
3387
3541
|
const warnings: string[] = [];
|
|
3542
|
+
let integrationEvidence: LiveIntegrationEvidence[] = [];
|
|
3388
3543
|
const paramVerification: ParamVerification[] = [];
|
|
3389
3544
|
let liveVerification:
|
|
3390
3545
|
| { kind: 'waived-bot' | 'waived-infra'; firstError: string; exhaustedBackends: string[] }
|
|
@@ -3589,6 +3744,7 @@ export async function externalVerification(
|
|
|
3589
3744
|
|
|
3590
3745
|
const notTemplated: string[] = [];
|
|
3591
3746
|
const inventedOnly: string[] = [];
|
|
3747
|
+
const intentionallyOmitted: Array<{ name: string; feature: string; reason: string }> = [];
|
|
3592
3748
|
|
|
3593
3749
|
for (const lp of opts.likelyParams) {
|
|
3594
3750
|
const placeholder = `\${param.${lp.name}}`;
|
|
@@ -3626,7 +3782,19 @@ export async function externalVerification(
|
|
|
3626
3782
|
}
|
|
3627
3783
|
|
|
3628
3784
|
if (!inBody && !inHeader && !inOriginalQuery && !inInventedQuery) {
|
|
3629
|
-
|
|
3785
|
+
const limitation = workflow.limitations?.find((entry) =>
|
|
3786
|
+
entry.omittedParameters?.includes(lp.name),
|
|
3787
|
+
);
|
|
3788
|
+
const stillDeclared = workflow.parameters.some((param) => param.name === lp.name);
|
|
3789
|
+
if (limitation && !stillDeclared) {
|
|
3790
|
+
intentionallyOmitted.push({
|
|
3791
|
+
name: lp.name,
|
|
3792
|
+
feature: limitation.feature,
|
|
3793
|
+
reason: limitation.reason,
|
|
3794
|
+
});
|
|
3795
|
+
} else {
|
|
3796
|
+
notTemplated.push(lp.name);
|
|
3797
|
+
}
|
|
3630
3798
|
} else if (!inBody && !inHeader && !inOriginalQuery && inInventedQuery) {
|
|
3631
3799
|
inventedOnly.push(lp.name);
|
|
3632
3800
|
}
|
|
@@ -3634,7 +3802,7 @@ export async function externalVerification(
|
|
|
3634
3802
|
|
|
3635
3803
|
if (notTemplated.length > 0) {
|
|
3636
3804
|
failures.push(
|
|
3637
|
-
`${notTemplated.length} likelyParam(s)
|
|
3805
|
+
`${notTemplated.length} likelyParam(s) have no valid disposition: ${notTemplated.join(', ')}. Each must either appear as \${param.NAME} in a request URL, body, or header, or be removed from the public parameters list and named in workflow.limitations[].omittedParameters with an evidence-based reason. For parameters recorded as null or [] (filters the user toggled but didn\'t apply), find the correct position in the request body and replace the placeholder value with \${param.NAME}.`,
|
|
3638
3806
|
);
|
|
3639
3807
|
}
|
|
3640
3808
|
if (inventedOnly.length > 0) {
|
|
@@ -3642,6 +3810,13 @@ export async function externalVerification(
|
|
|
3642
3810
|
`${inventedOnly.length} likelyParam(s) are templated only in URL query params that do not exist in any recorded request URL: ${inventedOnly.join(', ')}. The API server likely ignores these invented params — wire them into the request body or an existing query param instead. For complex body formats, use a requestTransformModule to construct the body programmatically.`,
|
|
3643
3811
|
);
|
|
3644
3812
|
}
|
|
3813
|
+
if (intentionallyOmitted.length > 0) {
|
|
3814
|
+
warnings.push(
|
|
3815
|
+
`${intentionallyOmitted.length} detector-suggested parameter(s) were intentionally omitted by the compile agent: ${intentionallyOmitted
|
|
3816
|
+
.map(({ name, feature, reason }) => `${name} (${feature}: ${reason})`)
|
|
3817
|
+
.join('; ')}. The limitations are retained in workflow.json and surfaced to callers.`,
|
|
3818
|
+
);
|
|
3819
|
+
}
|
|
3645
3820
|
}
|
|
3646
3821
|
} catch (err) {
|
|
3647
3822
|
failures.push(
|
|
@@ -3758,8 +3933,13 @@ export async function externalVerification(
|
|
|
3758
3933
|
if (!hasLiveBaselineWorkflowTest(integrationSrc)) {
|
|
3759
3934
|
integrationOutcome = 'failed';
|
|
3760
3935
|
failures.push(
|
|
3761
|
-
'integration.test.ts has no strict baseline live workflow test: add a non-`param:` test that calls `
|
|
3936
|
+
'integration.test.ts has no strict baseline live workflow test: add a non-`param:` test that calls `runCapturedIntegrationCase(...)` on this tool workflow, fails when `result.ok` is false, and asserts the successful response contains real data. Static workflow-shape assertions or green returns on bot/infra errors are not enough to verify a generated tool.',
|
|
3762
3937
|
);
|
|
3938
|
+
} else if (opts.deferLiveIntegrationToSemanticAgent) {
|
|
3939
|
+
// The independent verifier owns the final live execution. Keeping the
|
|
3940
|
+
// suite out of this deterministic gate avoids two disconnected verdicts
|
|
3941
|
+
// and lets the reviewer inspect the exact outputs it requested.
|
|
3942
|
+
integrationOutcome = 'absent';
|
|
3763
3943
|
} else {
|
|
3764
3944
|
// Scale the verifier's live-test timeout to the suite size: the baseline plus
|
|
3765
3945
|
// one live `runWorkflowWithLadder` per param, each gated by the ~25s compile
|
|
@@ -3769,26 +3949,30 @@ export async function externalVerification(
|
|
|
3769
3949
|
const paramCount = opts.likelyParams?.length ?? 0;
|
|
3770
3950
|
const pacingMs = Number(process.env.IMPRINT_COMPILE_ACT_SPACING_MS ?? 25_000) || 0;
|
|
3771
3951
|
const verifierTimeoutMs = Math.min(120_000 + paramCount * (pacingMs + 20_000), 10 * 60_000);
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3952
|
+
const evidencePath = pathJoin(
|
|
3953
|
+
toolDir,
|
|
3954
|
+
`.imprint-live-evidence-${process.pid}-${Date.now()}.jsonl`,
|
|
3955
|
+
);
|
|
3956
|
+
try {
|
|
3957
|
+
if (existsSync(evidencePath)) unlinkSync(evidencePath);
|
|
3958
|
+
} catch {
|
|
3959
|
+
// best-effort cleanup before the run
|
|
3960
|
+
}
|
|
3961
|
+
const run = await runBunTestWithResults(integrationTestPath, toolDir, verifierTimeoutMs, {
|
|
3962
|
+
[LIVE_EVIDENCE_PATH_ENV]: evidencePath,
|
|
3963
|
+
});
|
|
3964
|
+
try {
|
|
3965
|
+
integrationEvidence = readLiveIntegrationEvidence(evidencePath);
|
|
3966
|
+
} catch (error) {
|
|
3967
|
+
failures.push(
|
|
3968
|
+
`could not read captured live integration evidence: ${error instanceof Error ? error.message : String(error)}`,
|
|
3969
|
+
);
|
|
3970
|
+
} finally {
|
|
3971
|
+
try {
|
|
3972
|
+
if (existsSync(evidencePath)) unlinkSync(evidencePath);
|
|
3973
|
+
} catch {
|
|
3974
|
+
// Evidence is ephemeral and intentionally never kept with artifacts.
|
|
3975
|
+
}
|
|
3792
3976
|
}
|
|
3793
3977
|
integrationPassedTests = run.passed;
|
|
3794
3978
|
|
|
@@ -3804,7 +3988,11 @@ export async function externalVerification(
|
|
|
3804
3988
|
integrationOutcome = verdict.outcome;
|
|
3805
3989
|
|
|
3806
3990
|
if (verdict.outcome === 'passed') {
|
|
3807
|
-
|
|
3991
|
+
if (integrationEvidence.length === 0) {
|
|
3992
|
+
failures.push(
|
|
3993
|
+
'integration.test.ts passed but captured no live evidence. Import `runCapturedIntegrationCase` from `imprint/compile-verification` and use it for every live workflow call so the independent verifier can inspect the actual tool inputs and parsed outputs.',
|
|
3994
|
+
);
|
|
3995
|
+
}
|
|
3808
3996
|
} else if (verdict.captureFailName !== null) {
|
|
3809
3997
|
const capName = verdict.captureFailName;
|
|
3810
3998
|
// If the failing capture is a `response_header` on a REPLAYED workflow
|
|
@@ -3855,7 +4043,7 @@ export async function externalVerification(
|
|
|
3855
4043
|
);
|
|
3856
4044
|
} else {
|
|
3857
4045
|
failures.push(
|
|
3858
|
-
`bun test integration.test.ts exited ${run.exitCode} — the workflow failed to produce live data
|
|
4046
|
+
`bun test integration.test.ts exited ${run.exitCode} — the workflow failed to produce live data. The baseline suite runs exactly once per verification cycle.\nstdout:\n${run.stdout}\nstderr:\n${run.stderr}`,
|
|
3859
4047
|
);
|
|
3860
4048
|
}
|
|
3861
4049
|
}
|
|
@@ -3872,7 +4060,8 @@ export async function externalVerification(
|
|
|
3872
4060
|
!referencedStateBroken &&
|
|
3873
4061
|
existsSync(integrationTestPath) &&
|
|
3874
4062
|
opts.likelyParams &&
|
|
3875
|
-
opts.likelyParams.length > 0
|
|
4063
|
+
opts.likelyParams.length > 0 &&
|
|
4064
|
+
!opts.deferLiveIntegrationToSemanticAgent
|
|
3876
4065
|
) {
|
|
3877
4066
|
const integrationSrc = readFileSync(integrationTestPath, 'utf8');
|
|
3878
4067
|
|
|
@@ -4088,5 +4277,5 @@ export async function externalVerification(
|
|
|
4088
4277
|
}
|
|
4089
4278
|
}
|
|
4090
4279
|
|
|
4091
|
-
return { failures, warnings, paramVerification, liveVerification };
|
|
4280
|
+
return { failures, warnings, paramVerification, liveVerification, integrationEvidence };
|
|
4092
4281
|
}
|