thumbgate 1.27.18 → 1.27.20
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/.claude/commands/dashboard.md +15 -0
- package/.claude/commands/thumbgate-blocked.md +27 -0
- package/.claude/commands/thumbgate-dashboard.md +15 -0
- package/.claude/commands/thumbgate-doctor.md +30 -0
- package/.claude/commands/thumbgate-guard.md +36 -0
- package/.claude/commands/thumbgate-protect.md +30 -0
- package/.claude/commands/thumbgate-rules.md +30 -0
- package/.claude-plugin/plugin.json +4 -3
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +33 -12
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +246 -30
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/gemini/function-declarations.json +1 -0
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +263 -11
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bench/observability-eval-suite.json +26 -0
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +1456 -122
- package/bin/dashboard-cli.js +7 -0
- package/bin/postinstall.js +11 -27
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +216 -0
- package/config/gates/claim-verification.json +12 -0
- package/config/gates/default.json +31 -2
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +23 -13
- package/config/merge-quality-checks.json +0 -1
- package/config/model-candidates.json +121 -6
- package/config/post-deploy-marketing-pages.json +80 -0
- package/config/tessl-tiles.json +1 -3
- package/openapi/openapi.yaml +12 -0
- package/package.json +225 -100
- package/public/about.html +162 -0
- package/public/agent-manager.html +179 -0
- package/public/agents-cost-savings.html +153 -0
- package/public/ai-malpractice-prevention.html +818 -0
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
- package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
- package/public/assets/brand/thumbgate-mark-team.svg +26 -0
- package/public/assets/brand/thumbgate-mark.svg +21 -0
- package/public/assets/brand/thumbgate-wordmark.svg +20 -0
- package/public/assets/claude-thumbgate-statusbar.svg +8 -0
- package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
- package/public/assets/legal-intake-control-flow.svg +66 -0
- package/public/blog.html +4 -4
- package/public/brand/thumbgate-mark.svg +19 -0
- package/public/brand/thumbgate-og.svg +16 -0
- package/public/chatgpt-app.html +330 -0
- package/public/codex-enterprise.html +123 -0
- package/public/codex-plugin.html +72 -20
- package/public/compare.html +31 -8
- package/public/dashboard.html +930 -166
- package/public/diagnostic.html +345 -0
- package/public/federal.html +2 -2
- package/public/guide.html +33 -13
- package/public/index.html +469 -111
- package/public/install.html +193 -0
- package/public/js/buyer-intent.js +672 -0
- package/public/learn.html +183 -18
- package/public/lessons.html +168 -10
- package/public/numbers.html +7 -7
- package/public/pricing.html +399 -0
- package/public/pro.html +34 -11
- package/scripts/action-receipts.js +324 -0
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/agent-readiness.js +20 -3
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/audit.js +65 -0
- package/scripts/auto-promote-gates.js +82 -10
- package/scripts/auto-wire-hooks.js +14 -0
- package/scripts/aws-blocks-guardrails.js +272 -0
- package/scripts/billing.js +93 -1
- package/scripts/bot-detection.js +61 -3
- package/scripts/build-metadata.js +50 -10
- package/scripts/classifier-routing.js +130 -0
- package/scripts/cli-feedback.js +4 -2
- package/scripts/cli-schema.js +97 -0
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/commercial-offer.js +82 -2
- package/scripts/context-manager.js +74 -6
- package/scripts/dashboard-chat.js +332 -0
- package/scripts/dashboard.js +68 -2
- package/scripts/export-databricks-bundle.js +5 -1
- package/scripts/export-dpo-pairs.js +7 -2
- package/scripts/feedback-aggregate.js +281 -0
- package/scripts/feedback-loop.js +123 -1
- package/scripts/feedback-quality.js +87 -0
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gate-stats.js +89 -0
- package/scripts/gates-engine.js +1176 -85
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-runtime.js +20 -14
- package/scripts/hook-stop-anti-claim.js +301 -0
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/hybrid-feedback-context.js +142 -7
- package/scripts/install-shim.js +87 -0
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/license.js +10 -10
- package/scripts/llm-client.js +169 -4
- package/scripts/local-model-profile.js +15 -8
- package/scripts/mcp-config.js +7 -1
- package/scripts/mcp-oauth.js +293 -0
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/meta-agent-loop.js +36 -0
- package/scripts/noop-detect.js +285 -0
- package/scripts/operational-integrity.js +39 -5
- package/scripts/oss-pr-opportunity-scout.js +35 -5
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plan-gate.js +243 -0
- package/scripts/plausible-domain-config.js +99 -0
- package/scripts/plausible-server-events.js +9 -6
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/rate-limiter.js +64 -13
- package/scripts/repeat-metric.js +137 -0
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-redaction.js +166 -0
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +260 -10
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/self-harness-optimizer.js +141 -0
- package/scripts/self-healing-check.js +193 -0
- package/scripts/self-protection.js +90 -0
- package/scripts/seo-gsd.js +916 -7
- package/scripts/silent-failure-cluster.js +531 -0
- package/scripts/statusline-cache-path.js +17 -2
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/statusline-local-stats.js +9 -1
- package/scripts/statusline-meta.js +28 -2
- package/scripts/statusline.sh +20 -4
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/telemetry-analytics.js +357 -0
- package/scripts/thompson-sampling.js +31 -10
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/tool-registry.js +169 -1
- package/scripts/trajectory-scorer.js +63 -0
- package/scripts/vector-store.js +45 -0
- package/scripts/verify-marketing-pages-deployed.js +212 -0
- package/scripts/visitor-journey.js +172 -0
- package/scripts/workflow-sentinel.js +286 -53
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +2683 -319
- package/.claude-plugin/marketplace.json +0 -85
- package/adapters/chatgpt/openapi.yaml +0 -1695
- package/scripts/bot-detector.js +0 -50
|
@@ -53,10 +53,31 @@ function listJsonFiles(dirPath) {
|
|
|
53
53
|
return results;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
const STOPWORDS = new Set([
|
|
57
|
+
'a', 'about', 'above', 'after', 'again', 'against', 'all', 'am', 'an', 'and', 'any', 'are', 'arent', 'as', 'at',
|
|
58
|
+
'be', 'because', 'been', 'before', 'being', 'below', 'between', 'both', 'but', 'by',
|
|
59
|
+
'cant', 'cannot', 'could', 'couldnt', 'did', 'didnt', 'do', 'does', 'doesnt', 'doing', 'dont', 'down', 'during',
|
|
60
|
+
'each', 'few', 'for', 'from', 'further', 'had', 'hadnt', 'has', 'hasnt', 'have', 'havent', 'having',
|
|
61
|
+
'he', 'hed', 'hell', 'hes', 'her', 'here', 'heres', 'hers', 'herself', 'him', 'himself', 'his',
|
|
62
|
+
'how', 'hows', 'i', 'id', 'ill', 'im', 'ive', 'if', 'in', 'into', 'is', 'isnt', 'it', 'its', 'itself',
|
|
63
|
+
'lets', 'me', 'more', 'most', 'mustnt', 'my', 'myself',
|
|
64
|
+
'no', 'nor', 'not', 'of', 'off', 'on', 'once', 'only', 'or', 'other', 'ought', 'our', 'ours', 'ourselves', 'out', 'over', 'own',
|
|
65
|
+
'same', 'shant', 'she', 'shed', 'shell', 'shes', 'should', 'shouldnt', 'so', 'some', 'such',
|
|
66
|
+
'than', 'that', 'thats', 'the', 'their', 'theirs', 'them', 'themselves', 'then', 'there', 'theres', 'these', 'they', 'theyd', 'theyll', 'theyre', 'theyve', 'this', 'those', 'through', 'to', 'too', 'under', 'until', 'up', 'very',
|
|
67
|
+
'was', 'wasnt', 'we', 'wed', 'well', 'were', 'weve', 'werent', 'what', 'whats', 'when', 'whens', 'where', 'wheres', 'which', 'while', 'who', 'whos', 'whom', 'why', 'whys',
|
|
68
|
+
'with', 'wont', 'would', 'wouldnt', 'you', 'youd', 'youll', 'youre', 'youve', 'your', 'yours', 'yourself', 'yourselves'
|
|
69
|
+
]);
|
|
70
|
+
|
|
56
71
|
function tokenize(text) {
|
|
57
72
|
return String(text || '').toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
58
73
|
}
|
|
59
74
|
|
|
75
|
+
function getSearchTokens(queryText) {
|
|
76
|
+
const allTokens = tokenize(queryText);
|
|
77
|
+
const contentTokens = allTokens.filter(t => !STOPWORDS.has(t));
|
|
78
|
+
return contentTokens.length > 0 ? contentTokens : allTokens;
|
|
79
|
+
}
|
|
80
|
+
|
|
60
81
|
function unique(arr) {
|
|
61
82
|
return [...new Set(arr)];
|
|
62
83
|
}
|
|
@@ -82,7 +103,7 @@ function substringBoost(query, text) {
|
|
|
82
103
|
const q = query.toLowerCase();
|
|
83
104
|
const t = text.toLowerCase();
|
|
84
105
|
if (t.includes(q)) return 0.3;
|
|
85
|
-
const words = q.split(/\s+/).filter((w) => w.length > 2);
|
|
106
|
+
const words = q.split(/\s+/).filter((w) => w.length > 2 && !STOPWORDS.has(w));
|
|
86
107
|
const matched = words.filter((w) => t.includes(w)).length;
|
|
87
108
|
return words.length > 0 ? (matched / words.length) * 0.2 : 0;
|
|
88
109
|
}
|
|
@@ -117,8 +138,12 @@ function scoreRecord(queryTokens, queryText, record) {
|
|
|
117
138
|
const recency = recencyScore(record.timestamp);
|
|
118
139
|
const signalBoost = record.signal === 'down' ? 0.05 : 0;
|
|
119
140
|
|
|
141
|
+
const matchScore = jaccard + substr;
|
|
142
|
+
const isWildcard = !queryText || queryText === '*';
|
|
143
|
+
const score = isWildcard ? (recency + signalBoost || 0.01) : (matchScore > 0 ? matchScore + recency + signalBoost : 0);
|
|
144
|
+
|
|
120
145
|
return {
|
|
121
|
-
score:
|
|
146
|
+
score: Number(score.toFixed(4)),
|
|
122
147
|
record,
|
|
123
148
|
matchedTokens: unique(queryTokens).filter((t) => new Set(recordTokens).has(t)),
|
|
124
149
|
};
|
|
@@ -129,7 +154,7 @@ function scoreRecord(queryTokens, queryText, record) {
|
|
|
129
154
|
// ---------------------------------------------------------------------------
|
|
130
155
|
|
|
131
156
|
function searchFeedbackLog(queryText, limit = 5, options = {}) {
|
|
132
|
-
const logPath = path.join(getFeedbackDir(), 'feedback-log.jsonl');
|
|
157
|
+
const logPath = path.join(options.feedbackDir || getFeedbackDir(), 'feedback-log.jsonl');
|
|
133
158
|
let records = readJsonl(logPath);
|
|
134
159
|
|
|
135
160
|
// SQLite fallback: if JSONL is empty/tiny, pull records from the lesson DB
|
|
@@ -156,7 +181,7 @@ function searchFeedbackLog(queryText, limit = 5, options = {}) {
|
|
|
156
181
|
|
|
157
182
|
// Wildcard query: return all records sorted by recency
|
|
158
183
|
const isWildcard = queryText === '*' || queryText === '';
|
|
159
|
-
const queryTokens = isWildcard ? [] :
|
|
184
|
+
const queryTokens = isWildcard ? [] : getSearchTokens(queryText);
|
|
160
185
|
|
|
161
186
|
let scored = isWildcard
|
|
162
187
|
? records.map((r) => ({ score: recencyScore(r.timestamp) || 0.01, record: r, matchedTokens: [] }))
|
|
@@ -186,9 +211,9 @@ function searchFeedbackLog(queryText, limit = 5, options = {}) {
|
|
|
186
211
|
}
|
|
187
212
|
|
|
188
213
|
function searchContextFs(queryText, limit = 5, options = {}) {
|
|
189
|
-
const contextDir = getContextFsDir();
|
|
214
|
+
const contextDir = options.contextDir || (options.feedbackDir ? path.join(options.feedbackDir, 'contextfs') : getContextFsDir());
|
|
190
215
|
const namespaces = options.namespaces || ['memory/error', 'memory/learning', 'rules', 'raw_history'];
|
|
191
|
-
const queryTokens =
|
|
216
|
+
const queryTokens = getSearchTokens(queryText);
|
|
192
217
|
const scored = [];
|
|
193
218
|
|
|
194
219
|
for (const ns of namespaces) {
|
|
@@ -221,12 +246,12 @@ function searchContextFs(queryText, limit = 5, options = {}) {
|
|
|
221
246
|
}));
|
|
222
247
|
}
|
|
223
248
|
|
|
224
|
-
function searchPreventionRules(queryText, limit = 5) {
|
|
225
|
-
const rulesPath = path.join(getFeedbackDir(), 'prevention-rules.md');
|
|
249
|
+
function searchPreventionRules(queryText, limit = 5, options = {}) {
|
|
250
|
+
const rulesPath = path.join(options.feedbackDir || getFeedbackDir(), 'prevention-rules.md');
|
|
226
251
|
if (!fs.existsSync(rulesPath)) return [];
|
|
227
252
|
|
|
228
253
|
const content = fs.readFileSync(rulesPath, 'utf-8');
|
|
229
|
-
const queryTokens =
|
|
254
|
+
const queryTokens = getSearchTokens(queryText);
|
|
230
255
|
const blocks = content.split(/^#{1,3}\s+/m).filter(Boolean);
|
|
231
256
|
|
|
232
257
|
return blocks
|
|
@@ -252,7 +277,7 @@ function searchPreventionRules(queryText, limit = 5) {
|
|
|
252
277
|
function searchAll(queryText, limit = 10, options = {}) {
|
|
253
278
|
const feedbackResults = searchFeedbackLog(queryText, limit, options);
|
|
254
279
|
const contextResults = searchContextFs(queryText, limit, options);
|
|
255
|
-
const ruleResults = searchPreventionRules(queryText, limit);
|
|
280
|
+
const ruleResults = searchPreventionRules(queryText, limit, options);
|
|
256
281
|
|
|
257
282
|
const merged = [
|
|
258
283
|
...feedbackResults.map((r) => ({ ...r, _source_type: 'feedback' })),
|
package/scripts/gate-stats.js
CHANGED
|
@@ -9,6 +9,7 @@ const { sequencePathFor } = require('./risk-scorer');
|
|
|
9
9
|
|
|
10
10
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
11
11
|
const MANUAL_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
|
|
12
|
+
const STATS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-stats.json');
|
|
12
13
|
|
|
13
14
|
function loadGatesFile(filePath) {
|
|
14
15
|
if (!fs.existsSync(filePath)) return [];
|
|
@@ -65,6 +66,15 @@ function calculateStats() {
|
|
|
65
66
|
// sample can produce a misleading 0.0% floor.
|
|
66
67
|
const bayesErrorRate = tryComputeBayesErrorRate();
|
|
67
68
|
|
|
69
|
+
// Calibration: per-gate assessment of whether block actions are well-supported
|
|
70
|
+
// by negative feedback, or potentially over/under-blocking without confirmation.
|
|
71
|
+
const calibration = computeCalibration(allGates);
|
|
72
|
+
|
|
73
|
+
// First-time fix rate: 1 - (recurringBlocks / totalBlocksAndWarns)
|
|
74
|
+
// Measures how often a single gate fire resolves the issue vs the agent retrying.
|
|
75
|
+
// Returns null when there is no recorded block/warn data yet.
|
|
76
|
+
const firstTimeFixRate = computeFirstTimeFixRate();
|
|
77
|
+
|
|
68
78
|
return {
|
|
69
79
|
totalGates: allGates.length,
|
|
70
80
|
manualGates: manualGates.length,
|
|
@@ -77,10 +87,70 @@ function calculateStats() {
|
|
|
77
87
|
lastPromotion,
|
|
78
88
|
estimatedHoursSaved,
|
|
79
89
|
bayesErrorRate,
|
|
90
|
+
calibration,
|
|
91
|
+
firstTimeFixRate,
|
|
80
92
|
gates: allGates,
|
|
81
93
|
};
|
|
82
94
|
}
|
|
83
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Assess each gate's calibration by comparing block occurrences to confirmed
|
|
98
|
+
* negative feedback counts. A gate with many blocks but no confirming negative
|
|
99
|
+
* feedback may be over-blocking; one with matching feedback is well-calibrated.
|
|
100
|
+
*
|
|
101
|
+
* @param {Array} gates - Combined array of manual + auto-promoted gate objects
|
|
102
|
+
* @returns {Array<{gateId: string, occurrences: number, action: string, calibrationNote: string}>}
|
|
103
|
+
*/
|
|
104
|
+
function computeCalibration(gates) {
|
|
105
|
+
const calibration = [];
|
|
106
|
+
for (const gate of gates || []) {
|
|
107
|
+
if (!gate || !gate.id) continue;
|
|
108
|
+
const occurrences = Number(gate.occurrences || 0);
|
|
109
|
+
const action = gate.action || 'unknown';
|
|
110
|
+
// Only annotate gates with recorded occurrence data
|
|
111
|
+
if (occurrences === 0) continue;
|
|
112
|
+
|
|
113
|
+
if (action === 'block') {
|
|
114
|
+
const confirmedNegative = Number(gate.confirmedNegative || gate.negativeCount || 0);
|
|
115
|
+
let calibrationNote;
|
|
116
|
+
if (occurrences > 10 && confirmedNegative === 0) {
|
|
117
|
+
calibrationNote = `over-blocking (${occurrences} blocks, 0 confirmed)`;
|
|
118
|
+
} else if (confirmedNegative > 0) {
|
|
119
|
+
calibrationNote = `well-calibrated (${occurrences} blocks, ${confirmedNegative} confirmed)`;
|
|
120
|
+
} else {
|
|
121
|
+
// Low occurrence count with no feedback — not enough data yet
|
|
122
|
+
calibrationNote = `insufficient data (${occurrences} blocks, 0 confirmed)`;
|
|
123
|
+
}
|
|
124
|
+
calibration.push({ gateId: gate.id, occurrences, action, calibrationNote });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return calibration;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Compute the first-time fix rate from the persisted gate-stats.json file.
|
|
132
|
+
*
|
|
133
|
+
* firstTimeFixRate = 1 - (recurringBlocks / totalBlocksAndWarns)
|
|
134
|
+
*
|
|
135
|
+
* Returns null when there are no recorded block/warn events yet.
|
|
136
|
+
* Returns a number in [0, 1] otherwise, where 1.0 means every gate fire
|
|
137
|
+
* was a first-time occurrence and 0.0 means every gate fired at least twice.
|
|
138
|
+
*/
|
|
139
|
+
function computeFirstTimeFixRate() {
|
|
140
|
+
try {
|
|
141
|
+
if (!fs.existsSync(STATS_PATH)) return null;
|
|
142
|
+
const raw = fs.readFileSync(STATS_PATH, 'utf8');
|
|
143
|
+
const data = JSON.parse(raw);
|
|
144
|
+
const totalBlocksAndWarns = (data.blocked || 0) + (data.warned || 0);
|
|
145
|
+
if (totalBlocksAndWarns === 0) return null;
|
|
146
|
+
const recurring = data.recurringBlocks || 0;
|
|
147
|
+
const rate = 1 - (recurring / totalBlocksAndWarns);
|
|
148
|
+
return Math.max(0, Math.min(1, rate));
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
84
154
|
function tryComputeBayesErrorRate() {
|
|
85
155
|
try {
|
|
86
156
|
const seqPath = sequencePathFor();
|
|
@@ -142,6 +212,13 @@ function formatStats(stats) {
|
|
|
142
212
|
lines.push(` Last promotion: ${formatLastPromotion(stats.lastPromotion)}`);
|
|
143
213
|
lines.push(` Estimated time saved: ~${stats.estimatedHoursSaved} hours`);
|
|
144
214
|
lines.push(` Bayes error rate: ${formatBayesErrorRate(stats.bayesErrorRate)}`);
|
|
215
|
+
lines.push(` First-time fix rate: ${formatFirstTimeFixRate(stats.firstTimeFixRate)}`);
|
|
216
|
+
if (Array.isArray(stats.calibration) && stats.calibration.length > 0) {
|
|
217
|
+
lines.push('Calibration:');
|
|
218
|
+
for (const entry of stats.calibration) {
|
|
219
|
+
lines.push(` - ${entry.gateId}: ${entry.calibrationNote}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
145
222
|
return lines.join('\n');
|
|
146
223
|
}
|
|
147
224
|
|
|
@@ -153,6 +230,14 @@ function formatBayesErrorRate(rate) {
|
|
|
153
230
|
return `${pct}% — high irreducible error; the feature set can't discriminate`;
|
|
154
231
|
}
|
|
155
232
|
|
|
233
|
+
function formatFirstTimeFixRate(rate) {
|
|
234
|
+
if (rate === null || rate === undefined) return 'n/a (no blocks or warns recorded yet)';
|
|
235
|
+
const pct = (rate * 100).toFixed(1);
|
|
236
|
+
if (rate >= 0.95) return `${pct}% — agents fix issues on first block (excellent)`;
|
|
237
|
+
if (rate >= 0.80) return `${pct}% — most blocks resolved first time (good)`;
|
|
238
|
+
return `${pct}% — recurring blocks detected; agents may be ignoring gate feedback`;
|
|
239
|
+
}
|
|
240
|
+
|
|
156
241
|
if (require.main === module) {
|
|
157
242
|
try {
|
|
158
243
|
const stats = calculateStats();
|
|
@@ -168,7 +253,11 @@ module.exports = {
|
|
|
168
253
|
formatStats,
|
|
169
254
|
formatLastPromotion,
|
|
170
255
|
formatBayesErrorRate,
|
|
256
|
+
formatFirstTimeFixRate,
|
|
257
|
+
computeFirstTimeFixRate,
|
|
171
258
|
loadGatesFile,
|
|
172
259
|
tryComputeBayesErrorRate,
|
|
260
|
+
computeCalibration,
|
|
173
261
|
MANUAL_GATES_PATH,
|
|
262
|
+
STATS_PATH,
|
|
174
263
|
};
|