fraim 2.0.108 → 2.0.109

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,121 @@
1
+ "use strict";
2
+ /**
3
+ * Quality Evidence Validation (Issue #251)
4
+ *
5
+ * Pure helpers for validating the `evidence.quality` object that
6
+ * quality-producing FRAIM jobs must emit on their final seekMentoring
7
+ * completion. Lives in src/core so both the local MCP proxy and the
8
+ * remote server can import the same contract without cross-layer
9
+ * dependencies.
10
+ *
11
+ * Two call sites currently:
12
+ * 1. src/local-mcp-server/stdio-server.ts — enforces on local
13
+ * seekMentoring completion and POSTs valid scores to the remote
14
+ * via /api/analytics/quality-score.
15
+ * 2. src/routes/analytics.ts — defense-in-depth validation on the
16
+ * /api/analytics/quality-score endpoint.
17
+ *
18
+ * A third (rare) call site lives in src/services/mcp-service.ts for
19
+ * the case where the local proxy falls through to the remote's own
20
+ * seekMentoring handler (e.g., when the local AIMentor errors).
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.REQUIRED_QUALITY_FIELDS = exports.QUALITY_PRODUCING_JOBS = void 0;
24
+ exports.validateQualityEvidence = validateQualityEvidence;
25
+ exports.buildQualityRejectionMessage = buildQualityRejectionMessage;
26
+ /**
27
+ * Jobs that produce a quality assessment and MUST emit a quality score
28
+ * via `evidence.quality` on their final seekMentoring completion.
29
+ */
30
+ exports.QUALITY_PRODUCING_JOBS = [
31
+ 'process-interview-notes',
32
+ 'triage-customer-needs'
33
+ ];
34
+ /**
35
+ * Required numeric fields inside `evidence.quality` for QUALITY_PRODUCING_JOBS.
36
+ * The schema is intentionally lenient beyond these — extra fields are
37
+ * allowed so per-job assessment can evolve — but these five gate completion.
38
+ */
39
+ exports.REQUIRED_QUALITY_FIELDS = [
40
+ { path: 'composite', type: 'number' },
41
+ { path: 'participant.fit', type: 'number' },
42
+ { path: 'participant.urgency', type: 'number' },
43
+ { path: 'participant.authority', type: 'number' },
44
+ { path: 'evidence.quoteSpecificityAvg', type: 'number' }
45
+ ];
46
+ /**
47
+ * Validate an `evidence.quality` object against REQUIRED_QUALITY_FIELDS.
48
+ * Returns null if valid, or an array of human-readable error strings.
49
+ *
50
+ * Design note: when `quality` is entirely missing we still walk every
51
+ * required field and emit a per-field error, so the agent sees the
52
+ * complete schema in one round rather than being told only that
53
+ * `quality` itself is missing.
54
+ */
55
+ function validateQualityEvidence(quality) {
56
+ const errors = [];
57
+ const isMissing = quality === undefined || quality === null;
58
+ if (isMissing) {
59
+ errors.push('evidence.quality is missing');
60
+ }
61
+ else if (typeof quality !== 'object' || Array.isArray(quality)) {
62
+ errors.push('evidence.quality must be an object');
63
+ }
64
+ const effective = isMissing || typeof quality !== 'object' || Array.isArray(quality)
65
+ ? undefined
66
+ : quality;
67
+ for (const { path, type } of exports.REQUIRED_QUALITY_FIELDS) {
68
+ const parts = path.split('.');
69
+ let cursor = effective;
70
+ for (const part of parts) {
71
+ if (cursor && typeof cursor === 'object' && part in cursor) {
72
+ cursor = cursor[part];
73
+ }
74
+ else {
75
+ cursor = undefined;
76
+ break;
77
+ }
78
+ }
79
+ if (typeof cursor !== type || (type === 'number' && Number.isNaN(cursor))) {
80
+ const got = cursor === undefined ? 'missing' : (Number.isNaN(cursor) ? 'NaN' : typeof cursor);
81
+ errors.push(`evidence.quality.${path} must be a ${type} (got ${got})`);
82
+ }
83
+ }
84
+ return errors.length > 0 ? errors : null;
85
+ }
86
+ /**
87
+ * Build the rejection message sent back to the agent when a quality-producing
88
+ * job tries to complete without a valid `evidence.quality` object. The message
89
+ * lists the specific errors and the required minimum schema so the agent can
90
+ * fix everything in one round.
91
+ */
92
+ function buildQualityRejectionMessage(jobName, currentPhase, errors) {
93
+ const errorBullets = errors.map(e => `- ${e}`).join('\n');
94
+ return [
95
+ `❌ **Job completion rejected** for \`${jobName}\`.`,
96
+ '',
97
+ `This job is required to emit a quality score on its final \`seekMentoring\` call so the result is captured in \`fraim_quality_scores\` for the manager dashboard. The following problems were found in \`evidence.quality\`:`,
98
+ '',
99
+ errorBullets,
100
+ '',
101
+ 'Required minimum schema:',
102
+ '',
103
+ '```javascript',
104
+ 'evidence: {',
105
+ ' quality: {',
106
+ ' composite: <number 0-10>,',
107
+ ' participant: { fit: <number 1-10>, urgency: <number 1-10>, authority: <number 1-10> },',
108
+ ' evidence: {',
109
+ ' quoteSpecificityAvg: <number 1-10>',
110
+ ' // other fields are allowed and encouraged but not required',
111
+ ' },',
112
+ ' interviewee: "<name>",',
113
+ ' company: "<company>",',
114
+ ' artifactPath: "docs/customer-development/<slug>-interview.md"',
115
+ ' }',
116
+ '}',
117
+ '```',
118
+ '',
119
+ `You are still in phase \`${currentPhase}\`. The job is **not** marked complete. Build the \`evidence.quality\` object from your Phase 4 (\`conversation-quality-assessment\`) work and resubmit this final phase.`
120
+ ].join('\n');
121
+ }
@@ -29,6 +29,7 @@ const provider_utils_1 = require("../core/utils/provider-utils");
29
29
  const object_utils_1 = require("../core/utils/object-utils");
30
30
  const local_registry_resolver_1 = require("../core/utils/local-registry-resolver");
31
31
  const ai_mentor_1 = require("../core/ai-mentor");
32
+ const quality_evidence_1 = require("../core/quality-evidence");
32
33
  const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
33
34
  const usage_collector_js_1 = require("./usage-collector.js");
34
35
  const otlp_metrics_receiver_js_1 = require("./otlp-metrics-receiver.js");
@@ -1664,6 +1665,40 @@ class FraimLocalMCPServer {
1664
1665
  const mentor = this.getMentor(requestSessionId);
1665
1666
  const tutoringResponse = await mentor.handleMentoringRequest(args);
1666
1667
  this.log(`✅ Local seekMentoring succeeded for ${args.jobName}:${args.currentPhase}`);
1668
+ // Quality enforcement (Issue #251).
1669
+ //
1670
+ // The local proxy owns seekMentoring for personalized-job support.
1671
+ // That means the server-side enforcement in mcp-service.handleSeekMentoring
1672
+ // rarely fires in practice, so this is the primary enforcement point.
1673
+ //
1674
+ // When a QUALITY_PRODUCING_JOBS job reaches its final phase
1675
+ // (nextPhase === null) with status === 'complete', the agent MUST
1676
+ // include a valid `evidence.quality` object. If invalid, swap the
1677
+ // accomplishment message for a rejection and DO NOT emit. If valid,
1678
+ // fire-and-forget POST to /api/analytics/quality-score so the row
1679
+ // lands in fraim_quality_scores.
1680
+ const isQualityJob = quality_evidence_1.QUALITY_PRODUCING_JOBS.includes(args.jobName);
1681
+ const isFinalCompletion = args.status === 'complete' &&
1682
+ (tutoringResponse.nextPhase === null || tutoringResponse.nextPhase === undefined);
1683
+ if (isQualityJob && isFinalCompletion) {
1684
+ const qualityErrors = (0, quality_evidence_1.validateQualityEvidence)(args.evidence?.quality);
1685
+ if (qualityErrors) {
1686
+ this.log(`❌ Quality enforcement rejected ${args.jobName} completion: ${qualityErrors.join('; ')}`);
1687
+ const rejection = (0, quality_evidence_1.buildQualityRejectionMessage)(args.jobName, args.currentPhase, qualityErrors);
1688
+ return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, rejection);
1689
+ }
1690
+ // Valid payload. Emit to the remote asynchronously.
1691
+ this.emitQualityScoreToRemote({
1692
+ jobName: args.jobName,
1693
+ jobId: args.jobId,
1694
+ sessionId: requestSessionId || args.sessionId || 'unknown',
1695
+ quality: args.evidence.quality,
1696
+ artifactPath: args.evidence.quality.artifactPath
1697
+ }).catch((err) => {
1698
+ // Best-effort: log but never fail the user-facing response.
1699
+ this.log(`⚠️ Quality score emission failed: ${err?.message || err}`);
1700
+ });
1701
+ }
1667
1702
  return await this.finalizeLocalToolTextResponse(request, requestSessionId, requestId, tutoringResponse.message);
1668
1703
  }
1669
1704
  catch (error) {
@@ -1971,6 +2006,33 @@ class FraimLocalMCPServer {
1971
2006
  this.log(`⚠️ Skipping usage collection: toolName=${toolName}, sessionId=${requestSessionId}`);
1972
2007
  }
1973
2008
  }
2009
+ /**
2010
+ * Emit a quality score to the remote FRAIM server (Issue #251).
2011
+ *
2012
+ * Called from the seekMentoring short-circuit when a QUALITY_PRODUCING_JOBS
2013
+ * job reaches its final phase with a validated `evidence.quality` payload.
2014
+ * Fire-and-forget from the caller's perspective: a failure here is logged
2015
+ * but does not block the user-facing response (analytics capture is best
2016
+ * effort, not blocking).
2017
+ *
2018
+ * On success the remote writes a row to the `fraim_quality_scores` MongoDB
2019
+ * collection which powers the manager dashboard trajectory chart.
2020
+ */
2021
+ async emitQualityScoreToRemote(payload) {
2022
+ const url = `${this.remoteUrl}/api/analytics/quality-score`;
2023
+ this.log(`📊 Emitting quality score → ${url} (job=${payload.jobName}, jobId=${payload.jobId})`);
2024
+ const response = await axios_1.default.post(url, payload, {
2025
+ headers: {
2026
+ 'Content-Type': 'application/json',
2027
+ 'x-api-key': this.apiKey
2028
+ },
2029
+ timeout: 10000
2030
+ });
2031
+ if (response.status < 200 || response.status >= 300) {
2032
+ throw new Error(`quality-score endpoint returned ${response.status}: ${JSON.stringify(response.data)}`);
2033
+ }
2034
+ this.log(`📊 ✅ Quality score accepted by remote (composite=${payload.quality.composite})`);
2035
+ }
1974
2036
  /**
1975
2037
  * Flush collected usage data to the remote server
1976
2038
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.108",
3
+ "version": "2.0.109",
4
4
  "description": "FRAIM CLI - Framework for Rigor-based AI Management (alias for fraim-framework)",
5
5
  "main": "index.js",
6
6
  "bin": {