fraim 2.0.290 → 2.0.292

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.
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /**
3
+ * Test Evidence Contract Validation (Issue #1419, Change 4)
4
+ *
5
+ * The #1419 spike found that a written "never report a result you have not
6
+ * read" instruction is not reliably obeyed even by an agent that has just
7
+ * stated, in its own text, that the work is outstanding
8
+ * (docs/evidence/1419-spike-findings.md, Round 2). This adds a mechanical
9
+ * backstop that does not depend on the model remembering the instruction: a
10
+ * phase whose job-frontmatter declares `requiresTestEvidence: true` must
11
+ * supply `findings.testEvidence` — the actual observed result (exit code,
12
+ * duration, and either a timestamp or the log path read) — before that phase
13
+ * can be marked `complete`.
14
+ *
15
+ * This is a schema-shape check only. It cannot verify the numbers are
16
+ * truthful, only that the agent committed to a concrete, falsifiable claim
17
+ * instead of a bare "passed" — the same "operationalize the existing
18
+ * Supervision Contract line as a resume prompt rather than leave it to the
19
+ * agent to remember" pattern the RFC uses for Change 3's continuation message.
20
+ *
21
+ * Lives in src/core so both the local MCP proxy and evals can import the same
22
+ * contract without cross-layer dependencies — same pattern as
23
+ * quality-evidence.ts and handoff-contracts.ts.
24
+ *
25
+ * Primary call site: src/local-mcp-server/stdio-server.ts seekMentoring
26
+ * handler, after the existing handoff-contract enforcement block.
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.isTestEvidencePhase = isTestEvidencePhase;
30
+ exports.validateTestEvidence = validateTestEvidence;
31
+ exports.validateTestEvidenceContract = validateTestEvidenceContract;
32
+ exports.buildTestEvidenceRejectionMessage = buildTestEvidenceRejectionMessage;
33
+ /**
34
+ * Returns true when `currentPhase`'s job-frontmatter declares
35
+ * `requiresTestEvidence: true`. No hardcoded phase-name list: any job/phase
36
+ * combination opts in by declaring the flag on its own `phases` entry.
37
+ */
38
+ function isTestEvidencePhase(currentPhase, phases) {
39
+ return phases?.[currentPhase]?.requiresTestEvidence === true;
40
+ }
41
+ /**
42
+ * Validates findings.testEvidence. Returns null if valid, or an array of
43
+ * human-readable error strings describing what is wrong.
44
+ *
45
+ * Required minimum shape:
46
+ * {
47
+ * exitCode: number,
48
+ * durationMs: number,
49
+ * timestamp: string, // either this...
50
+ * logPath: string, // ...or this
51
+ * }
52
+ */
53
+ function validateTestEvidence(value) {
54
+ if (value === undefined || value === null) {
55
+ return ['findings.testEvidence is missing'];
56
+ }
57
+ if (typeof value !== 'object' || Array.isArray(value)) {
58
+ return ['findings.testEvidence must be an object'];
59
+ }
60
+ const obj = value;
61
+ const errors = [];
62
+ if (typeof obj.exitCode !== 'number' || !Number.isFinite(obj.exitCode)) {
63
+ errors.push(`findings.testEvidence.exitCode must be a number (got ${obj.exitCode === undefined ? 'missing' : typeof obj.exitCode})`);
64
+ }
65
+ if (typeof obj.durationMs !== 'number' || !Number.isFinite(obj.durationMs) || obj.durationMs < 0) {
66
+ errors.push(`findings.testEvidence.durationMs must be a non-negative number (got ${obj.durationMs === undefined ? 'missing' : typeof obj.durationMs})`);
67
+ }
68
+ const hasTimestamp = typeof obj.timestamp === 'string' && obj.timestamp.trim().length > 0;
69
+ const hasLogPath = typeof obj.logPath === 'string' && obj.logPath.trim().length > 0;
70
+ if (!hasTimestamp && !hasLogPath) {
71
+ errors.push('findings.testEvidence must include a non-empty timestamp or logPath — the point of this evidence is proving the result was actually observed, not merely narrated');
72
+ }
73
+ return errors.length > 0 ? errors : null;
74
+ }
75
+ /**
76
+ * Orchestrator: called from the seekMentoring handler. Returns an empty
77
+ * array when no contract is violated for this call (phase doesn't require
78
+ * test evidence, or the call is not a completion).
79
+ */
80
+ function validateTestEvidenceContract(args) {
81
+ const phase = args.currentPhase ?? '';
82
+ if (args.status !== 'complete')
83
+ return [];
84
+ if (!isTestEvidencePhase(phase, args.phases))
85
+ return [];
86
+ return validateTestEvidence((args.findings ?? {}).testEvidence) ?? [];
87
+ }
88
+ const TEST_EVIDENCE_SCHEMA = `\`\`\`javascript
89
+ findings: {
90
+ testEvidence: {
91
+ exitCode: 0, // the real exit code you read, not assumed
92
+ durationMs: 12345, // how long the run actually took
93
+ timestamp: "2026-08-29T18:00:00Z", // when you observed it...
94
+ logPath: "path/to/run.log" // ...and/or where the log lives
95
+ }
96
+ }
97
+ \`\`\``;
98
+ /**
99
+ * Builds the rejection message returned to the agent when the test evidence
100
+ * contract is violated.
101
+ */
102
+ function buildTestEvidenceRejectionMessage(currentPhase, errors) {
103
+ const errorBullets = errors.map((e) => `- ${e}`).join('\n');
104
+ return [
105
+ `❌ **seekMentoring rejected** at phase \`${currentPhase}\`.`,
106
+ '',
107
+ `This phase's Outcome depends on a test/validation run. Report the actual observed result, ` +
108
+ `not a narrated success, before it can complete. The following problems were found:`,
109
+ '',
110
+ errorBullets,
111
+ '',
112
+ 'Required minimum schema:',
113
+ '',
114
+ TEST_EVIDENCE_SCHEMA,
115
+ '',
116
+ 'The job is **not** marked complete. Run the validation, read its real exit code and duration, ' +
117
+ 'and resubmit with `findings.testEvidence` populated.',
118
+ ].join('\n');
119
+ }
@@ -66,6 +66,7 @@ const local_registry_resolver_1 = require("../core/utils/local-registry-resolver
66
66
  const ai_mentor_1 = require("../core/ai-mentor");
67
67
  const quality_evidence_1 = require("../core/quality-evidence");
68
68
  const handoff_contracts_1 = require("../core/handoff-contracts");
69
+ const test_evidence_contract_1 = require("../core/test-evidence-contract");
69
70
  const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
70
71
  const usage_collector_js_1 = require("./usage-collector.js");
71
72
  const otlp_metrics_receiver_js_1 = require("./otlp-metrics-receiver.js");
@@ -2298,6 +2299,27 @@ class FraimLocalMCPServer {
2298
2299
  return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, rejection);
2299
2300
  }
2300
2301
  }
2302
+ // Test evidence contract enforcement (Issue #1419, Change 4).
2303
+ //
2304
+ // A phase whose job-frontmatter declares requiresTestEvidence: true
2305
+ // (e.g. implement-validate, implement-regression) must supply the
2306
+ // actual observed test/validation result — not a narrated success —
2307
+ // before it can be marked complete. This is the mechanical backstop
2308
+ // the #1419 spike found necessary: a written policy note alone was
2309
+ // not obeyed even by an agent that had just stated the work was
2310
+ // outstanding. Reuses handoffPhaseMap (same mentor.getJobPhaseMap
2311
+ // call already made above) rather than re-fetching the job's phases.
2312
+ const testEvidenceErrors = (0, test_evidence_contract_1.validateTestEvidenceContract)({
2313
+ currentPhase: args.currentPhase,
2314
+ status: args.status,
2315
+ findings: args.findings,
2316
+ phases: handoffPhaseMap,
2317
+ });
2318
+ if (testEvidenceErrors.length > 0) {
2319
+ this.log(`⚠️ Test evidence contract rejected seekMentoring for ${args.jobName}:${args.currentPhase}: ${testEvidenceErrors.join('; ')}`);
2320
+ const rejection = (0, test_evidence_contract_1.buildTestEvidenceRejectionMessage)(args.currentPhase, testEvidenceErrors);
2321
+ return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, rejection);
2322
+ }
2301
2323
  return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, tutoringResponse.message);
2302
2324
  }
2303
2325
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.290",
3
+ "version": "2.0.292",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {