fraim-hub 2.0.204
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 +467 -0
- package/bin/fraim-hub.js +17 -0
- package/dist/src/ai-hub/brand-store.js +17 -0
- package/dist/src/ai-hub/catalog.js +412 -0
- package/dist/src/ai-hub/cert-store.js +70 -0
- package/dist/src/ai-hub/cli.js +212 -0
- package/dist/src/ai-hub/configured-agents.js +283 -0
- package/dist/src/ai-hub/conversation-store.js +428 -0
- package/dist/src/ai-hub/desktop-main.js +449 -0
- package/dist/src/ai-hub/hosts.js +1669 -0
- package/dist/src/ai-hub/hub-latest-version.js +52 -0
- package/dist/src/ai-hub/hub-launch-decision.js +57 -0
- package/dist/src/ai-hub/hub-runtime-file.js +43 -0
- package/dist/src/ai-hub/managed-browser.js +269 -0
- package/dist/src/ai-hub/manager-turns.js +64 -0
- package/dist/src/ai-hub/office-sideload.js +156 -0
- package/dist/src/ai-hub/openclaw-bridge.js +250 -0
- package/dist/src/ai-hub/preferences.js +201 -0
- package/dist/src/ai-hub/remote-hub-gateway.js +88 -0
- package/dist/src/ai-hub/server.js +4013 -0
- package/dist/src/ai-hub/types.js +2 -0
- package/dist/src/ai-hub/url-safety.js +19 -0
- package/dist/src/api/admin/payments.js +33 -0
- package/dist/src/api/admin/sales-leads.js +21 -0
- package/dist/src/api/payment/create-session.js +338 -0
- package/dist/src/api/payment/dashboard-link.js +149 -0
- package/dist/src/api/payment/session-details.js +31 -0
- package/dist/src/api/payment/webhook.js +587 -0
- package/dist/src/api/sales/contact.js +44 -0
- package/dist/src/cli/api/get-provider-client.js +41 -0
- package/dist/src/cli/api/provider-client.js +107 -0
- package/dist/src/cli/commands/add-ide.js +462 -0
- package/dist/src/cli/fraim-hub.js +50 -0
- package/dist/src/cli/internal/device-flow-service.js +83 -0
- package/dist/src/cli/mcp/command-resolution.js +81 -0
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +136 -0
- package/dist/src/cli/mcp/ide-formats.js +317 -0
- package/dist/src/cli/mcp/mcp-server-builder.js +48 -0
- package/dist/src/cli/mcp/mcp-server-registry.js +173 -0
- package/dist/src/cli/providers/local-provider-registry.js +165 -0
- package/dist/src/cli/providers/provider-registry.js +230 -0
- package/dist/src/cli/setup/claude-code-telemetry.js +59 -0
- package/dist/src/cli/setup/codex-local-config.js +37 -0
- package/dist/src/cli/setup/ide-detector.js +383 -0
- package/dist/src/cli/setup/ide-global-integration.js +100 -0
- package/dist/src/cli/setup/ide-invocation-surfaces.js +160 -0
- package/dist/src/cli/setup/mcp-config-generator.js +225 -0
- package/dist/src/cli/setup/provider-prompts.js +339 -0
- package/dist/src/cli/utils/managed-agent-paths.js +118 -0
- package/dist/src/cli/utils/script-sync-utils.js +221 -0
- package/dist/src/cli/utils/user-config.js +100 -0
- package/dist/src/cli/utils/version-utils.js +35 -0
- package/dist/src/config/ai-manager-hiring.js +121 -0
- package/dist/src/config/feature-flags.js +25 -0
- package/dist/src/config/persona-capability-bundles.js +277 -0
- package/dist/src/config/persona-hiring.js +270 -0
- package/dist/src/config/portfolio-slug-overrides.js +17 -0
- package/dist/src/config/pricing.js +37 -0
- package/dist/src/config/stripe.js +43 -0
- package/dist/src/core/brand-store.js +232 -0
- package/dist/src/core/quality-evidence.js +331 -0
- package/dist/src/core/utils/git-utils.js +194 -0
- package/dist/src/core/utils/ports.js +32 -0
- package/dist/src/core/utils/project-fraim-paths.js +220 -0
- package/dist/src/db/payment-repository.js +61 -0
- package/dist/src/first-run/types.js +93 -0
- package/dist/src/fraim/db-service.js +2409 -0
- package/dist/src/local-mcp-server/agent-token-prices.js +167 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +975 -0
- package/dist/src/middleware/rate-limit.js +110 -0
- package/dist/src/models/payment.js +2 -0
- package/dist/src/routes/payment-routes.js +186 -0
- package/dist/src/routes/persona-catalog-routes.js +84 -0
- package/dist/src/services/dashboard-access.js +27 -0
- package/dist/src/services/email-service.js +951 -0
- package/dist/src/services/persona-entitlement-service.js +352 -0
- package/dist/src/services/workspace-identity.js +21 -0
- package/dist/src/types/analytics.js +2 -0
- package/dist/src/utils/payment-calculator.js +52 -0
- package/extensions/office-word/favicon.ico +0 -0
- package/extensions/office-word/icon-64.png +0 -0
- package/extensions/office-word/manifest.xml +33 -0
- package/extensions/office-word/taskpane.html +242 -0
- package/index.js +85 -0
- package/package.json +106 -0
- package/public/ai-hub/index.html +910 -0
- package/public/ai-hub/powerpoint-taskpane/icon-64.png +0 -0
- package/public/ai-hub/powerpoint-taskpane/index.html +236 -0
- package/public/ai-hub/powerpoint-taskpane/manifest.xml +30 -0
- package/public/ai-hub/review.css +444 -0
- package/public/ai-hub/script.js +11737 -0
- package/public/ai-hub/styles.css +5102 -0
- package/public/first-run/error-frame.js +100 -0
- package/public/first-run/index.html +35 -0
- package/public/first-run/script.js +739 -0
- package/public/first-run/styles.css +929 -0
- package/public/portfolio/ashley.html +523 -0
- package/public/portfolio/auditya.html +83 -0
- package/public/portfolio/banke.html +83 -0
- package/public/portfolio/beza.html +659 -0
- package/public/portfolio/careena.html +632 -0
- package/public/portfolio/casey.html +568 -0
- package/public/portfolio/celia.html +490 -0
- package/public/portfolio/deidre.html +642 -0
- package/public/portfolio/gautam.html +597 -0
- package/public/portfolio/hari.html +469 -0
- package/public/portfolio/huxley.html +1354 -0
- package/public/portfolio/index.html +741 -0
- package/public/portfolio/maestro.html +518 -0
- package/public/portfolio/mandy.html +590 -0
- package/public/portfolio/mona.html +597 -0
- package/public/portfolio/pam.html +887 -0
- package/public/portfolio/procella.html +107 -0
- package/public/portfolio/qasm.html +569 -0
- package/public/portfolio/ricardo.html +489 -0
- package/public/portfolio/sade.html +560 -0
- package/public/portfolio/sam.html +654 -0
- package/public/portfolio/sechar.html +580 -0
- package/public/portfolio/sreya.html +599 -0
- package/public/portfolio/swen.html +601 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MAX_LOGO_BYTES = exports.ORG_BRAND_FILENAME = void 0;
|
|
7
|
+
exports.resolveBrandColor = resolveBrandColor;
|
|
8
|
+
exports.relativeLuminance = relativeLuminance;
|
|
9
|
+
exports.contrastRatio = contrastRatio;
|
|
10
|
+
exports.hasSufficientContrast = hasSufficientContrast;
|
|
11
|
+
exports.sanitizeSvg = sanitizeSvg;
|
|
12
|
+
exports.sanitizeLogo = sanitizeLogo;
|
|
13
|
+
exports.normalizeBrand = normalizeBrand;
|
|
14
|
+
exports.serializeBrand = serializeBrand;
|
|
15
|
+
exports.parseBrand = parseBrand;
|
|
16
|
+
exports.readBrandFromDir = readBrandFromDir;
|
|
17
|
+
exports.writeBrandToDir = writeBrandToDir;
|
|
18
|
+
/**
|
|
19
|
+
* Org brand store — Issue #744 (Hub cobranding).
|
|
20
|
+
*
|
|
21
|
+
* Deterministic, dependency-free, AI-free logic (architecture-standards §1/§3):
|
|
22
|
+
* the org brand descriptor (company name, color, logo) is stored ALONGSIDE
|
|
23
|
+
* company info, in the same org context storage as `org_context.md`
|
|
24
|
+
* (manager directive, spec R7.4), as a single `org_brand.json` file.
|
|
25
|
+
*
|
|
26
|
+
* Security: a manager-supplied logo is a stored-XSS vector. `sanitizeSvg` uses a
|
|
27
|
+
* strict element/attribute allowlist (not a denylist) and drops scripts, event
|
|
28
|
+
* handlers, external references, styles, and any non-graphical element, so the
|
|
29
|
+
* stored logo is safe to inline in the Hub (spec Compliance: SOC2/ISO27001).
|
|
30
|
+
*/
|
|
31
|
+
const fs_1 = __importDefault(require("fs"));
|
|
32
|
+
const path_1 = __importDefault(require("path"));
|
|
33
|
+
/** File name of the brand descriptor, stored next to org_context.md. */
|
|
34
|
+
exports.ORG_BRAND_FILENAME = 'org_brand.json';
|
|
35
|
+
/** Max stored logo size (sanitized SVG markup or data: URI), in bytes. */
|
|
36
|
+
exports.MAX_LOGO_BYTES = 512 * 1024;
|
|
37
|
+
// ── Color ───────────────────────────────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* Normalize a hex color to lowercase `#rrggbb`, expanding `#rgb`. Returns null
|
|
40
|
+
* for anything that is not a valid 3- or 6-digit hex (named colors, functions,
|
|
41
|
+
* and injection attempts are rejected).
|
|
42
|
+
*/
|
|
43
|
+
function resolveBrandColor(input) {
|
|
44
|
+
if (typeof input !== 'string')
|
|
45
|
+
return null;
|
|
46
|
+
const raw = input.trim().replace(/^#/, '').toLowerCase();
|
|
47
|
+
if (/^[0-9a-f]{3}$/.test(raw)) {
|
|
48
|
+
return '#' + raw.split('').map((c) => c + c).join('');
|
|
49
|
+
}
|
|
50
|
+
if (/^[0-9a-f]{6}$/.test(raw)) {
|
|
51
|
+
return '#' + raw;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function hexToRgb(hex) {
|
|
56
|
+
const norm = resolveBrandColor(hex);
|
|
57
|
+
if (!norm)
|
|
58
|
+
return null;
|
|
59
|
+
const n = norm.slice(1);
|
|
60
|
+
return [parseInt(n.slice(0, 2), 16), parseInt(n.slice(2, 4), 16), parseInt(n.slice(4, 6), 16)];
|
|
61
|
+
}
|
|
62
|
+
/** WCAG relative luminance of an sRGB color. */
|
|
63
|
+
function relativeLuminance(hex) {
|
|
64
|
+
const rgb = hexToRgb(hex);
|
|
65
|
+
if (!rgb)
|
|
66
|
+
return 0;
|
|
67
|
+
const [r, g, b] = rgb.map((v) => {
|
|
68
|
+
const s = v / 255;
|
|
69
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
70
|
+
});
|
|
71
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
72
|
+
}
|
|
73
|
+
/** WCAG contrast ratio between two colors (1..21). */
|
|
74
|
+
function contrastRatio(a, b) {
|
|
75
|
+
const la = relativeLuminance(a);
|
|
76
|
+
const lb = relativeLuminance(b);
|
|
77
|
+
const lighter = Math.max(la, lb);
|
|
78
|
+
const darker = Math.min(la, lb);
|
|
79
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* True when `color` is readable as a UI accent against `background`. Uses the
|
|
83
|
+
* WCAG 3:1 threshold for graphical/large UI objects (the accent is used on
|
|
84
|
+
* filled buttons and tab underlines, not as body text). Guards R2.3 so an
|
|
85
|
+
* unreadable brand color never ships; callers fall back to the FRAIM accent.
|
|
86
|
+
*/
|
|
87
|
+
function hasSufficientContrast(color, background, min = 3.0) {
|
|
88
|
+
if (!resolveBrandColor(color) || !resolveBrandColor(background))
|
|
89
|
+
return false;
|
|
90
|
+
return contrastRatio(color, background) >= min;
|
|
91
|
+
}
|
|
92
|
+
// ── SVG sanitization (allowlist) ──────────────────────────────────────────────
|
|
93
|
+
/** Only these elements survive sanitization. */
|
|
94
|
+
const ALLOWED_SVG_TAGS = new Set([
|
|
95
|
+
'svg', 'g', 'path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon',
|
|
96
|
+
'text', 'tspan', 'defs', 'lineargradient', 'radialgradient', 'stop', 'title', 'desc',
|
|
97
|
+
]);
|
|
98
|
+
/** Only these attributes survive; everything else (incl. on*, href, style) is dropped. */
|
|
99
|
+
const ALLOWED_SVG_ATTRS = new Set([
|
|
100
|
+
'viewbox', 'xmlns', 'width', 'height', 'fill', 'fill-rule', 'fill-opacity', 'stroke',
|
|
101
|
+
'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray', 'stroke-opacity',
|
|
102
|
+
'opacity', 'd', 'points', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry',
|
|
103
|
+
'transform', 'offset', 'stop-color', 'stop-opacity', 'gradientunits', 'gradienttransform',
|
|
104
|
+
'font-family', 'font-size', 'font-weight', 'text-anchor', 'letter-spacing',
|
|
105
|
+
]);
|
|
106
|
+
/**
|
|
107
|
+
* Sanitize an SVG string via a strict allowlist. Returns safe SVG markup, or ''
|
|
108
|
+
* if the input is not an <svg> document. Removes: <script>, <foreignObject>,
|
|
109
|
+
* <image>, <use>, <a>, comments/PIs/DOCTYPE, all event handlers, all href /
|
|
110
|
+
* xlink:href, inline styles, and any element or attribute not on the allowlist.
|
|
111
|
+
*/
|
|
112
|
+
function sanitizeSvg(input) {
|
|
113
|
+
if (typeof input !== 'string')
|
|
114
|
+
return '';
|
|
115
|
+
// Bound the work up front so a pathological logo can't drive the tag-walk
|
|
116
|
+
// regex into catastrophic backtracking (DoS guard).
|
|
117
|
+
if (input.length > exports.MAX_LOGO_BYTES)
|
|
118
|
+
return '';
|
|
119
|
+
let s = input.trim();
|
|
120
|
+
if (!/^<svg[\s>]/i.test(s))
|
|
121
|
+
return '';
|
|
122
|
+
// Drop comments, CDATA, processing instructions, and DOCTYPE outright.
|
|
123
|
+
s = s.replace(/<!--[\s\S]*?-->/g, '');
|
|
124
|
+
s = s.replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, '');
|
|
125
|
+
s = s.replace(/<\?[\s\S]*?\?>/g, '');
|
|
126
|
+
s = s.replace(/<!DOCTYPE[\s\S]*?>/gi, '');
|
|
127
|
+
// Drop dangerous elements and their entire content.
|
|
128
|
+
s = s.replace(/<(script|style|foreignObject|iframe|use|image|a)\b[\s\S]*?<\/\1\s*>/gi, '');
|
|
129
|
+
// Drop self-closing / unclosed variants of the same dangerous elements.
|
|
130
|
+
s = s.replace(/<(script|style|foreignObject|iframe|use|image|a)\b[^>]*\/?>/gi, '');
|
|
131
|
+
// Walk every remaining tag; drop non-allowlisted elements and attributes.
|
|
132
|
+
s = s.replace(/<\/?([a-zA-Z][\w:-]*)((?:[^>"']|"[^"]*"|'[^']*')*)>/g, (full, rawName, rawAttrs) => {
|
|
133
|
+
const name = rawName.toLowerCase().replace(/^svg:/, '');
|
|
134
|
+
if (!ALLOWED_SVG_TAGS.has(name))
|
|
135
|
+
return '';
|
|
136
|
+
const isClose = /^<\//.test(full);
|
|
137
|
+
if (isClose)
|
|
138
|
+
return `</${name}>`;
|
|
139
|
+
const selfClose = /\/>$/.test(full);
|
|
140
|
+
const attrs = [];
|
|
141
|
+
const attrRe = /([a-zA-Z_:][\w:.-]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
142
|
+
let m;
|
|
143
|
+
while ((m = attrRe.exec(rawAttrs)) !== null) {
|
|
144
|
+
const attr = m[1].toLowerCase().replace(/^xml:/, '');
|
|
145
|
+
const value = m[3] !== undefined ? m[3] : m[4] || '';
|
|
146
|
+
if (!ALLOWED_SVG_ATTRS.has(attr))
|
|
147
|
+
continue;
|
|
148
|
+
// Defense in depth: never allow url()/javascript:/data: sneaking through a value.
|
|
149
|
+
if (/javascript:|url\s*\(|data:|expression\s*\(|<|>/i.test(value))
|
|
150
|
+
continue;
|
|
151
|
+
attrs.push(`${attr}="${value.replace(/"/g, '"')}"`);
|
|
152
|
+
}
|
|
153
|
+
const attrStr = attrs.length ? ' ' + attrs.join(' ') : '';
|
|
154
|
+
return `<${name}${attrStr}${selfClose ? '/' : ''}>`;
|
|
155
|
+
});
|
|
156
|
+
// If sanitization emptied the root, treat as invalid.
|
|
157
|
+
if (!/^<svg[\s>]/i.test(s.trim()))
|
|
158
|
+
return '';
|
|
159
|
+
return s.trim();
|
|
160
|
+
}
|
|
161
|
+
/** Accept only png/jpeg data URIs (sanitized SVG is handled separately). */
|
|
162
|
+
function isSafeRasterDataUri(s) {
|
|
163
|
+
return /^data:image\/(png|jpe?g);base64,[a-z0-9+/=\s]+$/i.test(s);
|
|
164
|
+
}
|
|
165
|
+
/** Sanitize a logo value: SVG markup -> sanitized SVG; png/jpeg data URI -> kept; else ''. */
|
|
166
|
+
function sanitizeLogo(input) {
|
|
167
|
+
if (typeof input !== 'string' || !input.trim())
|
|
168
|
+
return '';
|
|
169
|
+
const s = input.trim();
|
|
170
|
+
if (/^<svg[\s>]/i.test(s)) {
|
|
171
|
+
const clean = sanitizeSvg(s);
|
|
172
|
+
return clean.length <= exports.MAX_LOGO_BYTES ? clean : '';
|
|
173
|
+
}
|
|
174
|
+
if (isSafeRasterDataUri(s)) {
|
|
175
|
+
return s.length <= exports.MAX_LOGO_BYTES ? s : '';
|
|
176
|
+
}
|
|
177
|
+
return '';
|
|
178
|
+
}
|
|
179
|
+
// ── Serialize / parse ─────────────────────────────────────────────────────────
|
|
180
|
+
/** Normalize + sanitize a brand into its safe stored form. */
|
|
181
|
+
function normalizeBrand(brand) {
|
|
182
|
+
const out = {};
|
|
183
|
+
if (typeof brand.name === 'string' && brand.name.trim()) {
|
|
184
|
+
out.name = brand.name.trim().slice(0, 120);
|
|
185
|
+
}
|
|
186
|
+
const color = resolveBrandColor(brand.color);
|
|
187
|
+
if (color)
|
|
188
|
+
out.color = color;
|
|
189
|
+
const logo = sanitizeLogo(brand.logo);
|
|
190
|
+
if (logo)
|
|
191
|
+
out.logo = logo;
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
function serializeBrand(brand) {
|
|
195
|
+
return JSON.stringify(normalizeBrand(brand), null, 2);
|
|
196
|
+
}
|
|
197
|
+
/** Parse a brand JSON string into a normalized OrgBrand, or null on any error. */
|
|
198
|
+
function parseBrand(json) {
|
|
199
|
+
if (typeof json !== 'string' || !json.trim())
|
|
200
|
+
return null;
|
|
201
|
+
let obj;
|
|
202
|
+
try {
|
|
203
|
+
obj = JSON.parse(json);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
if (!obj || typeof obj !== 'object')
|
|
209
|
+
return null;
|
|
210
|
+
const norm = normalizeBrand(obj);
|
|
211
|
+
return Object.keys(norm).length ? norm : null;
|
|
212
|
+
}
|
|
213
|
+
// ── Directory read / write ─────────────────────────────────────────────────────
|
|
214
|
+
/** Read + normalize the brand from `<dir>/org_brand.json`, or null when absent/invalid. */
|
|
215
|
+
function readBrandFromDir(dir) {
|
|
216
|
+
try {
|
|
217
|
+
const file = path_1.default.join(dir, exports.ORG_BRAND_FILENAME);
|
|
218
|
+
if (!fs_1.default.existsSync(file))
|
|
219
|
+
return null;
|
|
220
|
+
return parseBrand(fs_1.default.readFileSync(file, 'utf8'));
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** Sanitize + normalize a brand and write it to `<dir>/org_brand.json`. Returns the stored brand. */
|
|
227
|
+
function writeBrandToDir(dir, brand) {
|
|
228
|
+
const normalized = normalizeBrand(brand);
|
|
229
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
230
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, exports.ORG_BRAND_FILENAME), JSON.stringify(normalized, null, 2), 'utf8');
|
|
231
|
+
return normalized;
|
|
232
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
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.ALL_STAGE_CATEGORIES = exports.STAGE_DISPLAY_NAMES = exports.STAGE_CATEGORY_MAP = exports.QUALITY_GATE_JOBS = exports.QUALITY_SCORE_JOBS = exports.QUALITY_PRODUCING_JOBS = exports.QUALITY_REGISTRY = void 0;
|
|
24
|
+
exports.getRequiredFieldsForJob = getRequiredFieldsForJob;
|
|
25
|
+
exports.validateQualityEvidence = validateQualityEvidence;
|
|
26
|
+
exports.buildQualityRejectionMessage = buildQualityRejectionMessage;
|
|
27
|
+
exports.QUALITY_REGISTRY = {
|
|
28
|
+
// Customer Development
|
|
29
|
+
// review-customer-development is the sole quality emitter for this stage.
|
|
30
|
+
// process-interview-notes is retained here for stage mapping only; it does
|
|
31
|
+
// not emit (enforced: false).
|
|
32
|
+
'process-interview-notes': { stage: 'customer-development', enforced: false },
|
|
33
|
+
'review-customer-development': { stage: 'customer-development', enforced: true, telemetryKind: 'score' },
|
|
34
|
+
'triage-customer-needs': { stage: 'customer-development', enforced: true, telemetryKind: 'gate' },
|
|
35
|
+
'interview-preparation': { stage: 'customer-discovery', enforced: false },
|
|
36
|
+
// Business Strategy
|
|
37
|
+
'review-business-strategy': { stage: 'business-strategy', enforced: true, telemetryKind: 'score' },
|
|
38
|
+
'business-plan-creation': { stage: 'business-strategy', enforced: false },
|
|
39
|
+
'branding-quality-audit': { stage: 'branding', enforced: true, telemetryKind: 'score' },
|
|
40
|
+
// Product Quality
|
|
41
|
+
'code-quality-assessment': { stage: 'product-quality', enforced: true, telemetryKind: 'score' },
|
|
42
|
+
// Test Quality
|
|
43
|
+
'test-quality-assessment': { stage: 'test-quality', enforced: true, telemetryKind: 'score' },
|
|
44
|
+
// Security
|
|
45
|
+
'security-review': { stage: 'security', enforced: true, telemetryKind: 'score' },
|
|
46
|
+
'production-readiness-review': { stage: 'production-readiness', enforced: true, telemetryKind: 'score' },
|
|
47
|
+
// Fundraising
|
|
48
|
+
'investor-pitch-preparation': { stage: 'fundraising', enforced: false },
|
|
49
|
+
// Go-to-Market
|
|
50
|
+
'marketing-strategy-definition': { stage: 'go-to-market', enforced: false },
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* @deprecated Use QUALITY_REGISTRY for new code.
|
|
54
|
+
* Derived enforcement list for backward compatibility.
|
|
55
|
+
*/
|
|
56
|
+
exports.QUALITY_PRODUCING_JOBS = Object.keys(exports.QUALITY_REGISTRY).filter((job) => exports.QUALITY_REGISTRY[job].enforced);
|
|
57
|
+
/**
|
|
58
|
+
* Jobs that write scored review rows used by the Quality tab scorecards.
|
|
59
|
+
* Gate-only telemetry jobs (for example `triage-customer-needs`) are excluded.
|
|
60
|
+
*/
|
|
61
|
+
exports.QUALITY_SCORE_JOBS = Object.keys(exports.QUALITY_REGISTRY).filter((job) => exports.QUALITY_REGISTRY[job].enforced && exports.QUALITY_REGISTRY[job].telemetryKind !== 'gate');
|
|
62
|
+
/**
|
|
63
|
+
* Jobs that emit gate-style telemetry without a composite score.
|
|
64
|
+
*/
|
|
65
|
+
exports.QUALITY_GATE_JOBS = Object.keys(exports.QUALITY_REGISTRY).filter((job) => exports.QUALITY_REGISTRY[job].enforced && exports.QUALITY_REGISTRY[job].telemetryKind === 'gate');
|
|
66
|
+
/**
|
|
67
|
+
* @deprecated Use QUALITY_REGISTRY for new code.
|
|
68
|
+
* Derived stage mappings for backward compatibility.
|
|
69
|
+
*/
|
|
70
|
+
exports.STAGE_CATEGORY_MAP = Object.keys(exports.QUALITY_REGISTRY).reduce((acc, job) => ({ ...acc, [job]: exports.QUALITY_REGISTRY[job].stage }), {});
|
|
71
|
+
/**
|
|
72
|
+
* Human-readable stage names for dashboard display.
|
|
73
|
+
*/
|
|
74
|
+
exports.STAGE_DISPLAY_NAMES = {
|
|
75
|
+
'customer-development': 'Customer Development',
|
|
76
|
+
'business-strategy': 'Business Strategy',
|
|
77
|
+
'branding': 'Branding',
|
|
78
|
+
'product-quality': 'Product Quality',
|
|
79
|
+
'test-quality': 'Test Quality',
|
|
80
|
+
'security': 'Security',
|
|
81
|
+
'production-readiness': 'Production Readiness',
|
|
82
|
+
'fundraising': 'Fundraising',
|
|
83
|
+
'go-to-market': 'Go-to-Market',
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* All known stage categories, in display order for the tile grid.
|
|
87
|
+
*/
|
|
88
|
+
exports.ALL_STAGE_CATEGORIES = [
|
|
89
|
+
'customer-development',
|
|
90
|
+
'business-strategy',
|
|
91
|
+
'branding',
|
|
92
|
+
'product-quality',
|
|
93
|
+
'test-quality',
|
|
94
|
+
'security',
|
|
95
|
+
'production-readiness',
|
|
96
|
+
'fundraising',
|
|
97
|
+
'go-to-market',
|
|
98
|
+
];
|
|
99
|
+
/**
|
|
100
|
+
* Required fields for customer-development jobs (legacy V1 schema).
|
|
101
|
+
* These jobs require participant and evidence sub-dimensions.
|
|
102
|
+
*/
|
|
103
|
+
const CUSTOMER_DEV_REQUIRED_FIELDS = [
|
|
104
|
+
{ path: 'composite', type: 'number' },
|
|
105
|
+
{ path: 'participant.fit', type: 'number' },
|
|
106
|
+
{ path: 'participant.urgency', type: 'number' },
|
|
107
|
+
{ path: 'participant.authority', type: 'number' },
|
|
108
|
+
{ path: 'evidence.quoteSpecificityAvg', type: 'number' }
|
|
109
|
+
];
|
|
110
|
+
/**
|
|
111
|
+
* Gate-only customer development jobs emit a decision without a composite score.
|
|
112
|
+
*/
|
|
113
|
+
const GATE_REQUIRED_FIELDS = [
|
|
114
|
+
{ path: 'gateDecision', type: 'string' }
|
|
115
|
+
];
|
|
116
|
+
/**
|
|
117
|
+
* Required fields for all other quality-producing jobs.
|
|
118
|
+
* Only composite and coaching are universally required — sub-dimensions
|
|
119
|
+
* vary by stage and are validated holistically, not mechanically.
|
|
120
|
+
*/
|
|
121
|
+
const UNIVERSAL_REQUIRED_FIELDS = [
|
|
122
|
+
{ path: 'composite', type: 'number' },
|
|
123
|
+
];
|
|
124
|
+
const SCORE_DIMENSION_REQUIRED_FIELDS = [
|
|
125
|
+
{ suffix: 'score', type: 'number' },
|
|
126
|
+
{ suffix: 'rationale', type: 'string' }
|
|
127
|
+
];
|
|
128
|
+
const QUALITY_SCORE_DIMENSIONS = {
|
|
129
|
+
'review-customer-development': [
|
|
130
|
+
'icpCoherence',
|
|
131
|
+
'interviewCoverage',
|
|
132
|
+
'evidenceQuality',
|
|
133
|
+
'patternSaturation',
|
|
134
|
+
'signalToProduct'
|
|
135
|
+
],
|
|
136
|
+
'review-business-strategy': [
|
|
137
|
+
'marketEvidence',
|
|
138
|
+
'competitiveRigor',
|
|
139
|
+
'unitEconomics',
|
|
140
|
+
'strategicCoherence'
|
|
141
|
+
],
|
|
142
|
+
'branding-quality-audit': [
|
|
143
|
+
'clarity',
|
|
144
|
+
'differentiation',
|
|
145
|
+
'coherence',
|
|
146
|
+
'proof',
|
|
147
|
+
'identityExpressiveness',
|
|
148
|
+
'governanceReadiness'
|
|
149
|
+
],
|
|
150
|
+
'code-quality-assessment': [
|
|
151
|
+
'typeSafety',
|
|
152
|
+
'errorHandling',
|
|
153
|
+
'architecture',
|
|
154
|
+
'maintainability'
|
|
155
|
+
],
|
|
156
|
+
'test-quality-assessment': [
|
|
157
|
+
'coverage',
|
|
158
|
+
'testIntegrity',
|
|
159
|
+
'testDesign',
|
|
160
|
+
'reliability'
|
|
161
|
+
],
|
|
162
|
+
'security-review': [
|
|
163
|
+
'findingSeverity',
|
|
164
|
+
'remediationReadiness',
|
|
165
|
+
'coverageCompleteness'
|
|
166
|
+
],
|
|
167
|
+
'production-readiness-review': [
|
|
168
|
+
'securityPosture',
|
|
169
|
+
'availabilityResilience',
|
|
170
|
+
'backupRestore',
|
|
171
|
+
'observabilityOps',
|
|
172
|
+
'releaseSafety',
|
|
173
|
+
'governanceRunbooks'
|
|
174
|
+
]
|
|
175
|
+
};
|
|
176
|
+
function requiredDimensionFields(jobName) {
|
|
177
|
+
const dimensions = QUALITY_SCORE_DIMENSIONS[jobName] ?? [];
|
|
178
|
+
return dimensions.flatMap((dimension) => SCORE_DIMENSION_REQUIRED_FIELDS.map(({ suffix, type }) => ({
|
|
179
|
+
path: `${dimension}.${suffix}`,
|
|
180
|
+
type
|
|
181
|
+
})));
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Returns the required quality fields for a given job.
|
|
185
|
+
* Gate-only jobs use the gate schema. The strict V1 interview schema is
|
|
186
|
+
* pinned to process-interview-notes for defense-in-depth on the
|
|
187
|
+
* /api/analytics/quality-score endpoint. Scored quality review jobs require
|
|
188
|
+
* their stage-specific rubric dimensions as direct `{ score, rationale }`
|
|
189
|
+
* properties so the Quality view can render those dimensions dynamically.
|
|
190
|
+
*/
|
|
191
|
+
function getRequiredFieldsForJob(jobName) {
|
|
192
|
+
const entry = exports.QUALITY_REGISTRY[jobName];
|
|
193
|
+
if (entry?.telemetryKind === 'gate') {
|
|
194
|
+
return GATE_REQUIRED_FIELDS;
|
|
195
|
+
}
|
|
196
|
+
if (jobName === 'process-interview-notes') {
|
|
197
|
+
return CUSTOMER_DEV_REQUIRED_FIELDS;
|
|
198
|
+
}
|
|
199
|
+
return [
|
|
200
|
+
...UNIVERSAL_REQUIRED_FIELDS,
|
|
201
|
+
...requiredDimensionFields(jobName)
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* @deprecated Use getRequiredFieldsForJob() for stage-aware validation.
|
|
206
|
+
* Kept for backward compatibility with existing imports.
|
|
207
|
+
*/
|
|
208
|
+
exports.REQUIRED_QUALITY_FIELDS = CUSTOMER_DEV_REQUIRED_FIELDS;
|
|
209
|
+
/**
|
|
210
|
+
* Validate an `evidence.quality` object against the required fields for
|
|
211
|
+
* a given job. If no jobName is provided, falls back to the universal
|
|
212
|
+
* schema (composite only).
|
|
213
|
+
*
|
|
214
|
+
* Returns null if valid, or an array of human-readable error strings.
|
|
215
|
+
*/
|
|
216
|
+
function validateQualityEvidence(quality, jobName) {
|
|
217
|
+
const errors = [];
|
|
218
|
+
const isMissing = quality === undefined || quality === null;
|
|
219
|
+
if (isMissing) {
|
|
220
|
+
errors.push('evidence.quality is missing');
|
|
221
|
+
}
|
|
222
|
+
else if (typeof quality !== 'object' || Array.isArray(quality)) {
|
|
223
|
+
errors.push('evidence.quality must be an object');
|
|
224
|
+
}
|
|
225
|
+
const effective = isMissing || typeof quality !== 'object' || Array.isArray(quality)
|
|
226
|
+
? undefined
|
|
227
|
+
: quality;
|
|
228
|
+
const requiredFields = jobName ? getRequiredFieldsForJob(jobName) : UNIVERSAL_REQUIRED_FIELDS;
|
|
229
|
+
for (const { path, type } of requiredFields) {
|
|
230
|
+
const parts = path.split('.');
|
|
231
|
+
let cursor = effective;
|
|
232
|
+
for (const part of parts) {
|
|
233
|
+
if (cursor && typeof cursor === 'object' && part in cursor) {
|
|
234
|
+
cursor = cursor[part];
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
cursor = undefined;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (typeof cursor !== type || (type === 'number' && Number.isNaN(cursor))) {
|
|
242
|
+
const got = cursor === undefined ? 'missing' : (Number.isNaN(cursor) ? 'NaN' : typeof cursor);
|
|
243
|
+
errors.push(`evidence.quality.${path} must be a ${type} (got ${got})`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Strict schema enforcement: No nested wrappers (Issue 255)
|
|
247
|
+
if (effective && effective.dimensions) {
|
|
248
|
+
errors.push('evidence.quality.dimensions is forbidden. Use a flat schema where all scores are direct properties of the quality object.');
|
|
249
|
+
}
|
|
250
|
+
return errors.length > 0 ? errors : null;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Build the rejection message sent back to the agent when a quality-producing
|
|
254
|
+
* job tries to complete without a valid `evidence.quality` object. The message
|
|
255
|
+
* lists the specific errors and the required minimum schema so the agent can
|
|
256
|
+
* fix everything in one round.
|
|
257
|
+
*/
|
|
258
|
+
function buildQualityRejectionMessage(jobName, currentPhase, errors) {
|
|
259
|
+
const errorBullets = errors.map(e => `- ${e}`).join('\n');
|
|
260
|
+
const entry = exports.QUALITY_REGISTRY[jobName];
|
|
261
|
+
const isGateOnly = entry?.telemetryKind === 'gate';
|
|
262
|
+
const isCustomerDev = entry?.stage === 'customer-development';
|
|
263
|
+
const dimensionExamples = (QUALITY_SCORE_DIMENSIONS[jobName] ?? ['marketEvidence', 'unitEconomics'])
|
|
264
|
+
.map((dimension) => ` ${dimension}: { score: <number>, rationale: "<string>" },`)
|
|
265
|
+
.join('\n');
|
|
266
|
+
const schemaExample = isGateOnly
|
|
267
|
+
? [
|
|
268
|
+
'```javascript',
|
|
269
|
+
'evidence: {',
|
|
270
|
+
' artifactPath: "<project-relative quality artifact path>",',
|
|
271
|
+
' quality: {',
|
|
272
|
+
' gateDecision: "<pass|flag|fail>",',
|
|
273
|
+
' interviewsAnalyzed: <number>,',
|
|
274
|
+
' distinctPainPatterns: <number>,',
|
|
275
|
+
' gaps: ["<gap summary>"],',
|
|
276
|
+
' coaching: "<actionable recommendation>"',
|
|
277
|
+
' }',
|
|
278
|
+
'}',
|
|
279
|
+
'```'
|
|
280
|
+
]
|
|
281
|
+
: isCustomerDev
|
|
282
|
+
? [
|
|
283
|
+
'```javascript',
|
|
284
|
+
'evidence: {',
|
|
285
|
+
' artifactPath: "<project-relative quality artifact path>",',
|
|
286
|
+
' quality: {',
|
|
287
|
+
' composite: <number 0-10>,',
|
|
288
|
+
' participant: { fit: <number 1-10>, urgency: <number 1-10>, authority: <number 1-10> },',
|
|
289
|
+
' evidence: {',
|
|
290
|
+
' quoteSpecificityAvg: <number 1-10>',
|
|
291
|
+
' // other fields are allowed and encouraged but not required',
|
|
292
|
+
' },',
|
|
293
|
+
' coaching: "<actionable recommendation>",',
|
|
294
|
+
' interviewee: "<name>",',
|
|
295
|
+
' company: "<company>"',
|
|
296
|
+
' }',
|
|
297
|
+
'}',
|
|
298
|
+
'```'
|
|
299
|
+
]
|
|
300
|
+
: [
|
|
301
|
+
'```javascript',
|
|
302
|
+
'evidence: {',
|
|
303
|
+
' artifactPath: "<project-relative quality artifact path>",',
|
|
304
|
+
' quality: {',
|
|
305
|
+
' composite: <number 0-10>,',
|
|
306
|
+
' coaching: "<actionable recommendation>",',
|
|
307
|
+
dimensionExamples,
|
|
308
|
+
' }',
|
|
309
|
+
'}',
|
|
310
|
+
'```'
|
|
311
|
+
];
|
|
312
|
+
const importantNote = [
|
|
313
|
+
'',
|
|
314
|
+
'> [!IMPORTANT]',
|
|
315
|
+
'> **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.',
|
|
316
|
+
''
|
|
317
|
+
];
|
|
318
|
+
return [
|
|
319
|
+
`❌ **Job completion rejected** for \`${jobName}\`.`,
|
|
320
|
+
'',
|
|
321
|
+
`This job is required to emit quality evidence on its final \`seekMentoring\` call so the result is captured in \`fraim_quality_scores\` for the quality dashboard. The following problems were found:`,
|
|
322
|
+
'',
|
|
323
|
+
errorBullets,
|
|
324
|
+
...importantNote,
|
|
325
|
+
`Required minimum schema for \`${entry?.stage ?? 'unknown'}\` stage:`,
|
|
326
|
+
'',
|
|
327
|
+
...schemaExample,
|
|
328
|
+
'',
|
|
329
|
+
`You are still in phase \`${currentPhase}\`. The job is **not** marked complete. Build the \`evidence.quality\` object from your quality assessment work and resubmit this final phase.`
|
|
330
|
+
].join('\n');
|
|
331
|
+
}
|