fraim 2.0.114 → 2.0.116
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.
|
@@ -20,46 +20,43 @@
|
|
|
20
20
|
* seekMentoring handler (e.g., when the local AIMentor errors).
|
|
21
21
|
*/
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
-
exports.REQUIRED_QUALITY_FIELDS = exports.ALL_STAGE_CATEGORIES = exports.STAGE_DISPLAY_NAMES = exports.STAGE_CATEGORY_MAP = exports.QUALITY_PRODUCING_JOBS = void 0;
|
|
23
|
+
exports.REQUIRED_QUALITY_FIELDS = exports.ALL_STAGE_CATEGORIES = exports.STAGE_DISPLAY_NAMES = exports.STAGE_CATEGORY_MAP = exports.QUALITY_PRODUCING_JOBS = exports.QUALITY_REGISTRY = void 0;
|
|
24
24
|
exports.getRequiredFieldsForJob = getRequiredFieldsForJob;
|
|
25
25
|
exports.validateQualityEvidence = validateQualityEvidence;
|
|
26
26
|
exports.buildQualityRejectionMessage = buildQualityRejectionMessage;
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* Central registry of FRAIM jobs that interact with the quality telemetry system.
|
|
29
|
+
* This is the SINGLE SOURCE OF TRUTH for:
|
|
30
|
+
* 1. Stage mapping (where it appears on the dashboard).
|
|
31
|
+
* 2. Enforcement (whether it MUST emit a quality score to complete).
|
|
30
32
|
*/
|
|
31
|
-
exports.
|
|
33
|
+
exports.QUALITY_REGISTRY = {
|
|
32
34
|
// Customer Development
|
|
33
|
-
'process-interview-notes',
|
|
34
|
-
'triage-customer-needs',
|
|
35
|
+
'process-interview-notes': { stage: 'customer-development', enforced: true },
|
|
36
|
+
'triage-customer-needs': { stage: 'customer-development', enforced: true },
|
|
37
|
+
'interview-preparation': { stage: 'customer-discovery', enforced: false },
|
|
35
38
|
// Business Strategy
|
|
36
|
-
'business-
|
|
37
|
-
'
|
|
38
|
-
'founder-market-fit-analysis',
|
|
39
|
-
'review-business-strategy',
|
|
39
|
+
'review-business-strategy': { stage: 'business-strategy', enforced: true },
|
|
40
|
+
'business-plan-creation': { stage: 'business-strategy', enforced: false },
|
|
40
41
|
// Product Quality
|
|
41
|
-
'code-quality-assessment',
|
|
42
|
+
'code-quality-assessment': { stage: 'product-quality', enforced: true },
|
|
42
43
|
// Test Quality
|
|
43
|
-
'test-
|
|
44
|
-
|
|
45
|
-
'
|
|
46
|
-
|
|
44
|
+
'test-quality-assessment': { stage: 'test-quality', enforced: true },
|
|
45
|
+
// Fundraising
|
|
46
|
+
'investor-pitch-preparation': { stage: 'fundraising', enforced: false },
|
|
47
|
+
// Go-to-Market
|
|
48
|
+
'marketing-strategy-definition': { stage: 'go-to-market', enforced: false },
|
|
49
|
+
};
|
|
47
50
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
51
|
+
* @deprecated Use QUALITY_REGISTRY for new code.
|
|
52
|
+
* Derived enforcement list for backward compatibility.
|
|
50
53
|
*/
|
|
51
|
-
exports.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'review-business-strategy': 'business-strategy',
|
|
58
|
-
'code-quality-assessment': 'product-quality',
|
|
59
|
-
'test-standards-evaluation': 'test-quality',
|
|
60
|
-
'test-coverage-evaluation': 'test-quality',
|
|
61
|
-
'review-test-quality': 'test-quality',
|
|
62
|
-
};
|
|
54
|
+
exports.QUALITY_PRODUCING_JOBS = Object.keys(exports.QUALITY_REGISTRY).filter((job) => exports.QUALITY_REGISTRY[job].enforced);
|
|
55
|
+
/**
|
|
56
|
+
* @deprecated Use QUALITY_REGISTRY for new code.
|
|
57
|
+
* Derived stage mappings for backward compatibility.
|
|
58
|
+
*/
|
|
59
|
+
exports.STAGE_CATEGORY_MAP = Object.keys(exports.QUALITY_REGISTRY).reduce((acc, job) => ({ ...acc, [job]: exports.QUALITY_REGISTRY[job].stage }), {});
|
|
63
60
|
/**
|
|
64
61
|
* Human-readable stage names for dashboard display.
|
|
65
62
|
*/
|
|
@@ -107,8 +104,8 @@ const UNIVERSAL_REQUIRED_FIELDS = [
|
|
|
107
104
|
* All other jobs require only a composite score (principle-based, not prescriptive).
|
|
108
105
|
*/
|
|
109
106
|
function getRequiredFieldsForJob(jobName) {
|
|
110
|
-
const
|
|
111
|
-
if (stage === 'customer-development') {
|
|
107
|
+
const entry = exports.QUALITY_REGISTRY[jobName];
|
|
108
|
+
if (entry?.stage === 'customer-development') {
|
|
112
109
|
return CUSTOMER_DEV_REQUIRED_FIELDS;
|
|
113
110
|
}
|
|
114
111
|
return UNIVERSAL_REQUIRED_FIELDS;
|
|
@@ -155,6 +152,10 @@ function validateQualityEvidence(quality, jobName) {
|
|
|
155
152
|
errors.push(`evidence.quality.${path} must be a ${type} (got ${got})`);
|
|
156
153
|
}
|
|
157
154
|
}
|
|
155
|
+
// Strict schema enforcement: No nested wrappers (Issue 255)
|
|
156
|
+
if (effective && effective.dimensions) {
|
|
157
|
+
errors.push('evidence.quality.dimensions is forbidden. Use a flat schema where all scores are direct properties of the quality object.');
|
|
158
|
+
}
|
|
158
159
|
return errors.length > 0 ? errors : null;
|
|
159
160
|
}
|
|
160
161
|
/**
|
|
@@ -165,8 +166,8 @@ function validateQualityEvidence(quality, jobName) {
|
|
|
165
166
|
*/
|
|
166
167
|
function buildQualityRejectionMessage(jobName, currentPhase, errors) {
|
|
167
168
|
const errorBullets = errors.map(e => `- ${e}`).join('\n');
|
|
168
|
-
const
|
|
169
|
-
const isCustomerDev = stage === 'customer-development';
|
|
169
|
+
const entry = exports.QUALITY_REGISTRY[jobName];
|
|
170
|
+
const isCustomerDev = entry?.stage === 'customer-development';
|
|
170
171
|
const schemaExample = isCustomerDev
|
|
171
172
|
? [
|
|
172
173
|
'```javascript',
|
|
@@ -191,20 +192,27 @@ function buildQualityRejectionMessage(jobName, currentPhase, errors) {
|
|
|
191
192
|
' quality: {',
|
|
192
193
|
' composite: <number 0-10>,',
|
|
193
194
|
' coaching: "<actionable recommendation>",',
|
|
194
|
-
'
|
|
195
|
-
'
|
|
195
|
+
' marketEvidence: { score: <number>, rationale: "<string>" }, // flat, NO "dimensions" wrapper',
|
|
196
|
+
' unitEconomics: { score: <number>, rationale: "<string>" },',
|
|
197
|
+
' // add other sub-scores as direct properties',
|
|
196
198
|
' }',
|
|
197
199
|
'}',
|
|
198
200
|
'```'
|
|
199
201
|
];
|
|
202
|
+
const importantNote = [
|
|
203
|
+
'',
|
|
204
|
+
'> [!IMPORTANT]',
|
|
205
|
+
'> **Enforcement Notice**: Quality scores MUST follow a flat schema. Do NOT nest sub-dimension scores under a `dimensions` property. All sub-dimension objects must be direct properties of the `quality` object.',
|
|
206
|
+
''
|
|
207
|
+
];
|
|
200
208
|
return [
|
|
201
209
|
`❌ **Job completion rejected** for \`${jobName}\`.`,
|
|
202
210
|
'',
|
|
203
211
|
`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 quality dashboard. The following problems were found in \`evidence.quality\`:`,
|
|
204
212
|
'',
|
|
205
213
|
errorBullets,
|
|
206
|
-
|
|
207
|
-
`Required minimum schema for \`${stage ?? 'unknown'}\` stage:`,
|
|
214
|
+
...importantNote,
|
|
215
|
+
`Required minimum schema for \`${entry?.stage ?? 'unknown'}\` stage:`,
|
|
208
216
|
'',
|
|
209
217
|
...schemaExample,
|
|
210
218
|
'',
|
|
@@ -5,6 +5,7 @@ exports.determineDatabaseName = determineDatabaseName;
|
|
|
5
5
|
exports.getCurrentGitBranch = getCurrentGitBranch;
|
|
6
6
|
exports.determineSchema = determineSchema;
|
|
7
7
|
exports.getDefaultBranch = getDefaultBranch;
|
|
8
|
+
exports.sanitizeRepoIdentifier = sanitizeRepoIdentifier;
|
|
8
9
|
const child_process_1 = require("child_process");
|
|
9
10
|
/**
|
|
10
11
|
* Gets a unique port based on the current git branch name (if it's an issue branch)
|
|
@@ -93,3 +94,55 @@ function getDefaultBranch() {
|
|
|
93
94
|
// Default fallback
|
|
94
95
|
return 'main';
|
|
95
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Sanitizes a repository identifier by stripping local paths and normalizing to standard HTTPS URLs.
|
|
99
|
+
* Mandatory per Compliance requirement (C1) to ensure data privacy.
|
|
100
|
+
*
|
|
101
|
+
* @param repoUrl The raw repository URL or path to sanitize
|
|
102
|
+
* @returns Normalized HTTPS URL string, or undefined if the input is a local path or invalid
|
|
103
|
+
*/
|
|
104
|
+
function sanitizeRepoIdentifier(repoUrl) {
|
|
105
|
+
if (!repoUrl || typeof repoUrl !== 'string') {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const trimmed = repoUrl.trim();
|
|
109
|
+
// Reject local paths (Windows and POSIX)
|
|
110
|
+
if (trimmed.startsWith('/') ||
|
|
111
|
+
trimmed.startsWith('\\') ||
|
|
112
|
+
/^[a-zA-Z]:\\/.test(trimmed) ||
|
|
113
|
+
trimmed.startsWith('./') ||
|
|
114
|
+
trimmed.startsWith('../')) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
// Normalize GitHub/GitLab SSH and HTTPS URLs
|
|
118
|
+
// Patterns:
|
|
119
|
+
// git@github.com:owner/repo.git
|
|
120
|
+
// https://github.com/owner/repo.git
|
|
121
|
+
// git@gitlab.com:owner/repo.git
|
|
122
|
+
// https://gitlab.com/owner/repo.git
|
|
123
|
+
const gitPattern = /^(?:git@|https:\/\/)(github\.com|gitlab\.com)[:/]([^/]+\/[^/.]+?)(?:\.git)?$/i;
|
|
124
|
+
const match = trimmed.match(gitPattern);
|
|
125
|
+
if (match) {
|
|
126
|
+
const [, domain, path] = match;
|
|
127
|
+
return `https://${domain.toLowerCase()}/${path}`;
|
|
128
|
+
}
|
|
129
|
+
// If it looks like a generic URL but not from approved providers, reject for privacy
|
|
130
|
+
try {
|
|
131
|
+
const url = new URL(trimmed);
|
|
132
|
+
if (url.hostname.includes('github.com') || url.hostname.includes('gitlab.com')) {
|
|
133
|
+
let path = url.pathname;
|
|
134
|
+
if (path.startsWith('/'))
|
|
135
|
+
path = path.substring(1);
|
|
136
|
+
if (path.endsWith('.git'))
|
|
137
|
+
path = path.substring(0, path.length - 4);
|
|
138
|
+
// Only return if it matches owner/repo structure
|
|
139
|
+
if (path.split('/').length >= 2) {
|
|
140
|
+
return `https://${url.hostname.toLowerCase()}/${path}`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
// Not a valid URL
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
@@ -683,6 +683,7 @@ class FraimLocalMCPServer {
|
|
|
683
683
|
repoInfo.owner = owner;
|
|
684
684
|
}
|
|
685
685
|
this.repoInfo = repoInfo;
|
|
686
|
+
this.usageCollector.setRepoIdentifier(repoInfo.url || null);
|
|
686
687
|
const repoLabel = this.repoInfo.owner
|
|
687
688
|
? `${this.repoInfo.owner}/${this.repoInfo.name}`
|
|
688
689
|
: this.repoInfo.projectPath || this.repoInfo.name;
|
|
@@ -1968,6 +1969,9 @@ class FraimLocalMCPServer {
|
|
|
1968
1969
|
if (toolName && requestSessionId) {
|
|
1969
1970
|
const success = !response.error;
|
|
1970
1971
|
this.log(`📊 Collecting usage: ${toolName} (session: ${requestSessionId}, success: ${success})`);
|
|
1972
|
+
if (!this.repoInfo) {
|
|
1973
|
+
this.detectRepoInfo();
|
|
1974
|
+
}
|
|
1971
1975
|
// Capture the current queue size before collection
|
|
1972
1976
|
const beforeCount = this.usageCollector.getEventCount();
|
|
1973
1977
|
try {
|
|
@@ -13,6 +13,7 @@ class UsageCollector {
|
|
|
13
13
|
constructor() {
|
|
14
14
|
this.events = [];
|
|
15
15
|
this.userId = null;
|
|
16
|
+
this.repoIdentifier = null;
|
|
16
17
|
}
|
|
17
18
|
static resolveMentoringJobName(args) {
|
|
18
19
|
if (!args || typeof args !== 'object') {
|
|
@@ -36,6 +37,9 @@ class UsageCollector {
|
|
|
36
37
|
setUserId(userId) {
|
|
37
38
|
this.userId = userId;
|
|
38
39
|
}
|
|
40
|
+
setRepoIdentifier(repoIdentifier) {
|
|
41
|
+
this.repoIdentifier = repoIdentifier;
|
|
42
|
+
}
|
|
39
43
|
/**
|
|
40
44
|
* Collect MCP tool call event
|
|
41
45
|
*/
|
|
@@ -62,6 +66,7 @@ class UsageCollector {
|
|
|
62
66
|
sessionId,
|
|
63
67
|
success,
|
|
64
68
|
category: parsed.category,
|
|
69
|
+
repoIdentifier: this.repoIdentifier || undefined,
|
|
65
70
|
args: Object.keys(analyticsArgs).length > 0 ? analyticsArgs : undefined
|
|
66
71
|
};
|
|
67
72
|
this.events.push(event);
|
|
@@ -81,6 +86,7 @@ class UsageCollector {
|
|
|
81
86
|
userId: this.userId || 'unknown',
|
|
82
87
|
sessionId,
|
|
83
88
|
success,
|
|
89
|
+
repoIdentifier: this.repoIdentifier || undefined,
|
|
84
90
|
args
|
|
85
91
|
};
|
|
86
92
|
this.events.push(event);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.116",
|
|
4
4
|
"description": "FRAIM CLI - Framework for Rigor-based AI Management (alias for fraim-framework)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"test": "node scripts/test-with-server.js",
|
|
18
18
|
"test:isolated": "npx tsx --test --test-reporter=spec tests/isolated/test-*.ts",
|
|
19
19
|
"test:smoke": "node scripts/test-with-server.js --tags=smoke",
|
|
20
|
+
"test:coverage": "node scripts/test-with-server.js --tags=smoke --coverage",
|
|
20
21
|
"test:stripe": "node scripts/test-with-server.js tests/test-stripe-payment-complete.ts",
|
|
21
22
|
"test:stripe:ui": "playwright test tests/ui/test-payment-ui.spec.ts",
|
|
22
23
|
"test:perf": "node scripts/test-with-server.js tests/performance/analytics-perf.ts",
|