@10kdevs/matha 0.1.0
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/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/analysis/contract-matcher.js +223 -0
- package/dist/analysis/git-analyser.js +261 -0
- package/dist/analysis/stability-classifier.js +122 -0
- package/dist/brain/cortex.js +258 -0
- package/dist/brain/dopamine.js +184 -0
- package/dist/brain/frontal-lobe.js +219 -0
- package/dist/brain/hippocampus.js +134 -0
- package/dist/commands/after.js +334 -0
- package/dist/commands/before.js +328 -0
- package/dist/commands/init.js +266 -0
- package/dist/commands/migrate.js +16 -0
- package/dist/index.js +114 -0
- package/dist/mcp/server.js +305 -0
- package/dist/mcp/tools.js +379 -0
- package/dist/storage/reader.js +29 -0
- package/dist/storage/writer.js +111 -0
- package/dist/utils/markdown-parser.js +173 -0
- package/dist/utils/schema-version.js +91 -0
- package/package.json +62 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { readJsonOrNull } from '../storage/reader.js';
|
|
3
|
+
import { writeAtomic } from '../storage/writer.js';
|
|
4
|
+
const DEFAULT_TIERS = {
|
|
5
|
+
'rename/crud': { tier: 'lightweight', budget: 2000 },
|
|
6
|
+
'business_logic': { tier: 'capable', budget: 8000 },
|
|
7
|
+
'architecture': { tier: 'capable', budget: 16000 },
|
|
8
|
+
'frozen_component': { tier: 'capable', budget: 16000 },
|
|
9
|
+
'unknown': { tier: 'mid', budget: 4000 }
|
|
10
|
+
};
|
|
11
|
+
export async function analyseDeltas(mathaDir) {
|
|
12
|
+
const emptyAnalysis = {
|
|
13
|
+
analysedAt: new Date().toISOString(),
|
|
14
|
+
sessionCount: 0,
|
|
15
|
+
routingRules: [],
|
|
16
|
+
componentConfidence: [],
|
|
17
|
+
globalAvgTokenDelta: 0,
|
|
18
|
+
overBudgetRate: 0
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
const deltasPath = path.join(mathaDir, 'dopamine/deltas.json');
|
|
22
|
+
const deltas = await readJsonOrNull(deltasPath);
|
|
23
|
+
if (!deltas || !Array.isArray(deltas) || deltas.length === 0) {
|
|
24
|
+
return emptyAnalysis;
|
|
25
|
+
}
|
|
26
|
+
const validDeltas = deltas.filter(d => d.token_delta !== null);
|
|
27
|
+
let globalAvgTokenDelta = 0;
|
|
28
|
+
let overBudgetRate = 0;
|
|
29
|
+
if (validDeltas.length > 0) {
|
|
30
|
+
const sum = validDeltas.reduce((acc, d) => acc + d.token_delta, 0);
|
|
31
|
+
globalAvgTokenDelta = sum / validDeltas.length;
|
|
32
|
+
const overBudgetCount = validDeltas.filter(d => d.token_delta > 0).length;
|
|
33
|
+
overBudgetRate = overBudgetCount / validDeltas.length;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
analysedAt: new Date().toISOString(),
|
|
37
|
+
sessionCount: deltas.length,
|
|
38
|
+
routingRules: buildRoutingRules(deltas),
|
|
39
|
+
componentConfidence: buildConfidence(deltas),
|
|
40
|
+
globalAvgTokenDelta,
|
|
41
|
+
overBudgetRate
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return emptyAnalysis;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function buildRoutingRules(deltas) {
|
|
49
|
+
const groups = {};
|
|
50
|
+
for (const d of deltas) {
|
|
51
|
+
if (!groups[d.operation_type]) {
|
|
52
|
+
groups[d.operation_type] = [];
|
|
53
|
+
}
|
|
54
|
+
groups[d.operation_type].push(d);
|
|
55
|
+
}
|
|
56
|
+
const rules = [];
|
|
57
|
+
for (const [opType, records] of Object.entries(groups)) {
|
|
58
|
+
if (records.length < 3)
|
|
59
|
+
continue;
|
|
60
|
+
const validRecords = records.filter(r => r.token_delta !== null);
|
|
61
|
+
if (validRecords.length === 0)
|
|
62
|
+
continue;
|
|
63
|
+
const sum = validRecords.reduce((acc, r) => acc + r.token_delta, 0);
|
|
64
|
+
const avgTokenDelta = sum / validRecords.length;
|
|
65
|
+
const defaultDef = DEFAULT_TIERS[opType] || DEFAULT_TIERS['unknown'];
|
|
66
|
+
let recommendedTier = defaultDef.tier;
|
|
67
|
+
if (avgTokenDelta > 5000) {
|
|
68
|
+
if (recommendedTier === 'lightweight')
|
|
69
|
+
recommendedTier = 'mid';
|
|
70
|
+
else if (recommendedTier === 'mid')
|
|
71
|
+
recommendedTier = 'capable';
|
|
72
|
+
}
|
|
73
|
+
else if (avgTokenDelta < -2000) {
|
|
74
|
+
if (recommendedTier === 'capable')
|
|
75
|
+
recommendedTier = 'mid';
|
|
76
|
+
else if (recommendedTier === 'mid')
|
|
77
|
+
recommendedTier = 'lightweight';
|
|
78
|
+
}
|
|
79
|
+
let recommendedBudget = defaultDef.budget + avgTokenDelta;
|
|
80
|
+
if (recommendedBudget < 500) {
|
|
81
|
+
recommendedBudget = 500;
|
|
82
|
+
}
|
|
83
|
+
let confidence = 'low';
|
|
84
|
+
if (records.length >= 10)
|
|
85
|
+
confidence = 'high';
|
|
86
|
+
else if (records.length >= 5)
|
|
87
|
+
confidence = 'medium';
|
|
88
|
+
rules.push({
|
|
89
|
+
operation_type: opType,
|
|
90
|
+
component_pattern: '', // global rule
|
|
91
|
+
recommended_tier: recommendedTier,
|
|
92
|
+
recommended_budget: recommendedBudget,
|
|
93
|
+
confidence,
|
|
94
|
+
sample_size: records.length,
|
|
95
|
+
avg_token_delta: avgTokenDelta,
|
|
96
|
+
last_updated: new Date().toISOString()
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return rules;
|
|
100
|
+
}
|
|
101
|
+
export function buildConfidence(deltas) {
|
|
102
|
+
const groups = {};
|
|
103
|
+
for (const d of deltas) {
|
|
104
|
+
if (!groups[d.model_tier_used]) {
|
|
105
|
+
groups[d.model_tier_used] = [];
|
|
106
|
+
}
|
|
107
|
+
groups[d.model_tier_used].push(d);
|
|
108
|
+
}
|
|
109
|
+
const confidences = [];
|
|
110
|
+
for (const [tier, records] of Object.entries(groups)) {
|
|
111
|
+
const defaultVal = 0.0;
|
|
112
|
+
let adjustment = 0.0;
|
|
113
|
+
let violationCount = 0;
|
|
114
|
+
const validTokens = records.filter(r => r.token_delta !== null);
|
|
115
|
+
let avgTokenDelta = 0;
|
|
116
|
+
if (validTokens.length > 0) {
|
|
117
|
+
avgTokenDelta = validTokens.reduce((s, r) => s + r.token_delta, 0) / validTokens.length;
|
|
118
|
+
}
|
|
119
|
+
for (const r of records) {
|
|
120
|
+
if (r.contract_result === 'violated' || r.contract_result === 'partial') {
|
|
121
|
+
violationCount++;
|
|
122
|
+
}
|
|
123
|
+
if (r.contract_result === 'passed')
|
|
124
|
+
adjustment += 0.1;
|
|
125
|
+
else if (r.contract_result === 'violated')
|
|
126
|
+
adjustment -= 0.2;
|
|
127
|
+
else if (r.contract_result === 'partial')
|
|
128
|
+
adjustment -= 0.1;
|
|
129
|
+
if (r.token_delta !== null) {
|
|
130
|
+
if (r.token_delta > 5000)
|
|
131
|
+
adjustment -= 0.1;
|
|
132
|
+
else if (r.token_delta < 0)
|
|
133
|
+
adjustment += 0.05;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
adjustment = Math.max(-1.0, Math.min(1.0, adjustment));
|
|
137
|
+
confidences.push({
|
|
138
|
+
component: tier,
|
|
139
|
+
confidence_adjustment: adjustment,
|
|
140
|
+
violation_rate: violationCount / records.length,
|
|
141
|
+
avg_token_delta: avgTokenDelta,
|
|
142
|
+
session_count: records.length,
|
|
143
|
+
last_updated: new Date().toISOString()
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return confidences;
|
|
147
|
+
}
|
|
148
|
+
export async function getRecommendation(mathaDir, operationType) {
|
|
149
|
+
try {
|
|
150
|
+
const rulesPath = path.join(mathaDir, 'dopamine/routing-rules.json');
|
|
151
|
+
const analysis = await readJsonOrNull(rulesPath);
|
|
152
|
+
if (analysis && Array.isArray(analysis.routingRules)) {
|
|
153
|
+
const rule = analysis.routingRules.find(r => r.operation_type === operationType);
|
|
154
|
+
if (rule) {
|
|
155
|
+
return {
|
|
156
|
+
tier: rule.recommended_tier,
|
|
157
|
+
budget: rule.recommended_budget,
|
|
158
|
+
source: 'learned',
|
|
159
|
+
confidence: rule.confidence,
|
|
160
|
+
sample_size: rule.sample_size
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// silently fallback
|
|
167
|
+
}
|
|
168
|
+
const def = DEFAULT_TIERS[operationType] || DEFAULT_TIERS['unknown'];
|
|
169
|
+
return {
|
|
170
|
+
tier: def.tier,
|
|
171
|
+
budget: def.budget,
|
|
172
|
+
source: 'default',
|
|
173
|
+
confidence: null
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export async function persistAnalysis(mathaDir, analysis) {
|
|
177
|
+
try {
|
|
178
|
+
const rulesPath = path.join(mathaDir, 'dopamine/routing-rules.json');
|
|
179
|
+
await writeAtomic(rulesPath, analysis, { overwrite: true });
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// silently fail
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs an individual frontal-lobe gate.
|
|
3
|
+
* Never throws on malformed input; returns completed: false instead.
|
|
4
|
+
*/
|
|
5
|
+
export function runGate(gateId, context, input) {
|
|
6
|
+
try {
|
|
7
|
+
switch (gateId) {
|
|
8
|
+
case 1:
|
|
9
|
+
return {
|
|
10
|
+
gateId,
|
|
11
|
+
completed: isNonEmptyString(input),
|
|
12
|
+
output: input,
|
|
13
|
+
};
|
|
14
|
+
case 2:
|
|
15
|
+
return {
|
|
16
|
+
gateId,
|
|
17
|
+
completed: isNonEmptyStringArray(input),
|
|
18
|
+
output: input,
|
|
19
|
+
};
|
|
20
|
+
case 3: {
|
|
21
|
+
const orient = isNonEmptyObject(input) || isNonEmptyObject(context.stabilityData);
|
|
22
|
+
return {
|
|
23
|
+
gateId,
|
|
24
|
+
completed: orient,
|
|
25
|
+
output: isNonEmptyObject(input) ? input : context.stabilityData ?? {},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
case 4:
|
|
29
|
+
return {
|
|
30
|
+
gateId,
|
|
31
|
+
completed: isPresent(input),
|
|
32
|
+
output: input,
|
|
33
|
+
};
|
|
34
|
+
case 5:
|
|
35
|
+
return {
|
|
36
|
+
gateId,
|
|
37
|
+
completed: isNonEmptyStringArray(input),
|
|
38
|
+
output: input,
|
|
39
|
+
};
|
|
40
|
+
case 6:
|
|
41
|
+
return {
|
|
42
|
+
gateId,
|
|
43
|
+
completed: isPresent(input),
|
|
44
|
+
output: input,
|
|
45
|
+
};
|
|
46
|
+
case 7: {
|
|
47
|
+
const states = Array.isArray(input) ? input : [];
|
|
48
|
+
const missing = findMissingRequiredBuildGates(states);
|
|
49
|
+
const readyToBuild = missing.length === 0;
|
|
50
|
+
return {
|
|
51
|
+
gateId,
|
|
52
|
+
completed: readyToBuild,
|
|
53
|
+
output: { readyToBuild, missing },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
case 8:
|
|
57
|
+
return {
|
|
58
|
+
gateId,
|
|
59
|
+
completed: isPresent(input),
|
|
60
|
+
output: input,
|
|
61
|
+
};
|
|
62
|
+
default:
|
|
63
|
+
return {
|
|
64
|
+
gateId,
|
|
65
|
+
completed: false,
|
|
66
|
+
output: input,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return {
|
|
72
|
+
gateId,
|
|
73
|
+
completed: false,
|
|
74
|
+
output: input,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Validates that gates 01-06 are completed.
|
|
80
|
+
*/
|
|
81
|
+
export function validateSequence(states) {
|
|
82
|
+
const missing = [];
|
|
83
|
+
for (let gateId = 1; gateId <= 6; gateId++) {
|
|
84
|
+
const state = states.find((s) => s.gateId === gateId);
|
|
85
|
+
if (!state || !state.completed) {
|
|
86
|
+
missing.push(gateId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
valid: missing.length === 0,
|
|
91
|
+
missing,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Builds the session brief from gate outputs and hippocampus danger zone lookup.
|
|
96
|
+
*/
|
|
97
|
+
export async function generateBrief(context, states, hippocampus) {
|
|
98
|
+
const stateMap = new Map();
|
|
99
|
+
for (const s of states)
|
|
100
|
+
stateMap.set(s.gateId, s);
|
|
101
|
+
const why = asString(stateMap.get(1)?.output);
|
|
102
|
+
const bounds = asStringArray(stateMap.get(2)?.output);
|
|
103
|
+
const contract = asStringArray(stateMap.get(5)?.output);
|
|
104
|
+
const dangerZones = await hippocampus.getDangerZones(context.scope);
|
|
105
|
+
const routing = routeOperation(context.operationType);
|
|
106
|
+
const gate7 = stateMap.get(7);
|
|
107
|
+
const readyFromGate7 = typeof gate7?.output === 'object' &&
|
|
108
|
+
gate7?.output !== null &&
|
|
109
|
+
'readyToBuild' in gate7.output
|
|
110
|
+
? Boolean(gate7.output.readyToBuild)
|
|
111
|
+
: undefined;
|
|
112
|
+
const readyToBuild = readyFromGate7 ?? findMissingRequiredBuildGates(states).length === 0;
|
|
113
|
+
return {
|
|
114
|
+
sessionId: context.sessionId,
|
|
115
|
+
scope: context.scope,
|
|
116
|
+
operationType: context.operationType,
|
|
117
|
+
why,
|
|
118
|
+
bounds,
|
|
119
|
+
dangerZones,
|
|
120
|
+
contract,
|
|
121
|
+
modelTier: routing.modelTier,
|
|
122
|
+
tokenBudget: routing.tokenBudget,
|
|
123
|
+
gatesCompleted: states.filter((s) => s.completed).map((s) => s.gateId),
|
|
124
|
+
readyToBuild,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Gate 08 write-back.
|
|
129
|
+
* Writes discovery to hippocampus if correction and/or danger pattern are present.
|
|
130
|
+
*/
|
|
131
|
+
export async function runWriteBack(context, discovery, hippocampus) {
|
|
132
|
+
const correction = discovery.correction?.trim() ?? '';
|
|
133
|
+
const dangerPattern = discovery.dangerPattern?.trim() ?? '';
|
|
134
|
+
if (!correction && !dangerPattern)
|
|
135
|
+
return;
|
|
136
|
+
if (correction) {
|
|
137
|
+
const timestamp = new Date().toISOString();
|
|
138
|
+
const decision = {
|
|
139
|
+
id: `${context.sessionId}-${Date.now()}-decision`,
|
|
140
|
+
timestamp,
|
|
141
|
+
component: context.scope,
|
|
142
|
+
previous_assumption: discovery.previousAssumption?.trim() || 'No previous assumption recorded',
|
|
143
|
+
correction,
|
|
144
|
+
trigger: discovery.trigger?.trim() || 'Session write-back',
|
|
145
|
+
confidence: discovery.confidence ?? 'probable',
|
|
146
|
+
status: 'active',
|
|
147
|
+
supersedes: null,
|
|
148
|
+
session_id: context.sessionId,
|
|
149
|
+
};
|
|
150
|
+
await hippocampus.recordDecision(hippocampus.mathaDir, decision);
|
|
151
|
+
}
|
|
152
|
+
if (dangerPattern) {
|
|
153
|
+
const zone = {
|
|
154
|
+
id: `${context.sessionId}-${Date.now()}-danger`,
|
|
155
|
+
component: context.scope,
|
|
156
|
+
pattern: dangerPattern,
|
|
157
|
+
description: discovery.dangerDescription?.trim() ||
|
|
158
|
+
'Discovered during session write-back',
|
|
159
|
+
};
|
|
160
|
+
await hippocampus.recordDangerZone(hippocampus.mathaDir, zone);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function findMissingRequiredBuildGates(states) {
|
|
164
|
+
const required = [1, 2, 3, 4, 5];
|
|
165
|
+
const missing = [];
|
|
166
|
+
for (const gateId of required) {
|
|
167
|
+
const state = states.find((s) => s.gateId === gateId);
|
|
168
|
+
if (!state || !state.completed)
|
|
169
|
+
missing.push(gateId);
|
|
170
|
+
}
|
|
171
|
+
return missing;
|
|
172
|
+
}
|
|
173
|
+
function routeOperation(operationType) {
|
|
174
|
+
switch (operationType) {
|
|
175
|
+
case 'rename':
|
|
176
|
+
case 'crud':
|
|
177
|
+
return { modelTier: 'lightweight', tokenBudget: 2000 };
|
|
178
|
+
case 'business_logic':
|
|
179
|
+
return { modelTier: 'capable', tokenBudget: 8000 };
|
|
180
|
+
case 'architecture':
|
|
181
|
+
case 'frozen_component':
|
|
182
|
+
return { modelTier: 'capable', tokenBudget: 16000 };
|
|
183
|
+
default:
|
|
184
|
+
return { modelTier: 'mid', tokenBudget: 4000 };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function isPresent(value) {
|
|
188
|
+
if (value === null || value === undefined)
|
|
189
|
+
return false;
|
|
190
|
+
if (typeof value === 'string')
|
|
191
|
+
return value.trim().length > 0;
|
|
192
|
+
if (Array.isArray(value))
|
|
193
|
+
return value.length > 0;
|
|
194
|
+
if (typeof value === 'object')
|
|
195
|
+
return Object.keys(value).length > 0;
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
function isNonEmptyString(value) {
|
|
199
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
200
|
+
}
|
|
201
|
+
function isNonEmptyStringArray(value) {
|
|
202
|
+
return (Array.isArray(value) &&
|
|
203
|
+
value.length > 0 &&
|
|
204
|
+
value.every((item) => typeof item === 'string' && item.trim().length > 0));
|
|
205
|
+
}
|
|
206
|
+
function isNonEmptyObject(value) {
|
|
207
|
+
return (typeof value === 'object' &&
|
|
208
|
+
value !== null &&
|
|
209
|
+
!Array.isArray(value) &&
|
|
210
|
+
Object.keys(value).length > 0);
|
|
211
|
+
}
|
|
212
|
+
function asString(value) {
|
|
213
|
+
return typeof value === 'string' ? value : '';
|
|
214
|
+
}
|
|
215
|
+
function asStringArray(value) {
|
|
216
|
+
return Array.isArray(value)
|
|
217
|
+
? value.filter((v) => typeof v === 'string')
|
|
218
|
+
: [];
|
|
219
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { readJsonOrNull } from '../storage/reader.js';
|
|
3
|
+
import { writeAtomic } from '../storage/writer.js';
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
// ── INTENT ───────────────────────────────────────────────────────────
|
|
6
|
+
/**
|
|
7
|
+
* Returns the project intent, or null if not yet defined.
|
|
8
|
+
*/
|
|
9
|
+
export async function getIntent(mathaDir) {
|
|
10
|
+
const intentPath = path.join(mathaDir, 'hippocampus', 'intent.json');
|
|
11
|
+
return await readJsonOrNull(intentPath);
|
|
12
|
+
}
|
|
13
|
+
// ── RULES ────────────────────────────────────────────────────────────
|
|
14
|
+
/**
|
|
15
|
+
* Returns all non-negotiable business rules, or an empty array if none exist.
|
|
16
|
+
*/
|
|
17
|
+
export async function getRules(mathaDir) {
|
|
18
|
+
const rulesPath = path.join(mathaDir, 'hippocampus', 'rules.json');
|
|
19
|
+
const data = await readJsonOrNull(rulesPath);
|
|
20
|
+
return data?.rules ?? [];
|
|
21
|
+
}
|
|
22
|
+
// ── DECISIONS ────────────────────────────────────────────────────────
|
|
23
|
+
/**
|
|
24
|
+
* Records a decision entry to the decision log.
|
|
25
|
+
* Decision log is append-only: never modifies existing entries.
|
|
26
|
+
*
|
|
27
|
+
* @throws if a decision with the same id already exists
|
|
28
|
+
*/
|
|
29
|
+
export async function recordDecision(mathaDir, entry) {
|
|
30
|
+
const decisionsDir = path.join(mathaDir, 'hippocampus', 'decisions');
|
|
31
|
+
const decisionPath = path.join(decisionsDir, `${entry.id}.json`);
|
|
32
|
+
// Guard: reject if decision with this id already exists
|
|
33
|
+
try {
|
|
34
|
+
await fs.access(decisionPath);
|
|
35
|
+
throw new Error(`Decision with id '${entry.id}' already exists`);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (err.code !== 'ENOENT')
|
|
39
|
+
throw err;
|
|
40
|
+
// File does not exist — proceed
|
|
41
|
+
}
|
|
42
|
+
await writeAtomic(decisionPath, entry);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns all decision entries, optionally filtered by component.
|
|
46
|
+
* Results are sorted by timestamp descending (most recent first).
|
|
47
|
+
*
|
|
48
|
+
* @param component - Optional filter by component name
|
|
49
|
+
* @param limit - Optional limit on number of results
|
|
50
|
+
*/
|
|
51
|
+
export async function getDecisions(mathaDir, component, limit) {
|
|
52
|
+
const decisionsDir = path.join(mathaDir, 'hippocampus', 'decisions');
|
|
53
|
+
let files;
|
|
54
|
+
try {
|
|
55
|
+
files = await fs.readdir(decisionsDir);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (err.code === 'ENOENT')
|
|
59
|
+
return [];
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
const jsonFiles = files.filter((f) => f.endsWith('.json'));
|
|
63
|
+
const entries = [];
|
|
64
|
+
for (const file of jsonFiles) {
|
|
65
|
+
const filePath = path.join(decisionsDir, file);
|
|
66
|
+
try {
|
|
67
|
+
const entry = await readJsonOrNull(filePath);
|
|
68
|
+
if (entry)
|
|
69
|
+
entries.push(entry);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Skip malformed files
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Filter by component if provided
|
|
76
|
+
let filtered = component
|
|
77
|
+
? entries.filter((e) => e.component === component)
|
|
78
|
+
: entries;
|
|
79
|
+
// Sort by timestamp descending (most recent first)
|
|
80
|
+
filtered.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
81
|
+
// Apply limit if provided
|
|
82
|
+
if (limit !== undefined && limit > 0) {
|
|
83
|
+
filtered = filtered.slice(0, limit);
|
|
84
|
+
}
|
|
85
|
+
return filtered;
|
|
86
|
+
}
|
|
87
|
+
// ── DANGER ZONES ─────────────────────────────────────────────────────
|
|
88
|
+
/**
|
|
89
|
+
* Returns all danger zones, optionally filtered by context string.
|
|
90
|
+
*
|
|
91
|
+
* Context matching checks both component and description fields
|
|
92
|
+
* (case-insensitive).
|
|
93
|
+
*
|
|
94
|
+
* @param context - Optional context string to filter by
|
|
95
|
+
*/
|
|
96
|
+
export async function getDangerZones(mathaDir, context) {
|
|
97
|
+
const dangerZonesPath = path.join(mathaDir, 'hippocampus', 'danger-zones.json');
|
|
98
|
+
const data = await readJsonOrNull(dangerZonesPath);
|
|
99
|
+
const zones = data?.zones ?? [];
|
|
100
|
+
if (!context)
|
|
101
|
+
return zones;
|
|
102
|
+
const contextLower = context.toLowerCase();
|
|
103
|
+
return zones.filter((zone) => zone.component.toLowerCase().includes(contextLower) ||
|
|
104
|
+
zone.description.toLowerCase().includes(contextLower));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Records a new danger zone.
|
|
108
|
+
*/
|
|
109
|
+
export async function recordDangerZone(mathaDir, zone) {
|
|
110
|
+
const dangerZonesPath = path.join(mathaDir, 'hippocampus', 'danger-zones.json');
|
|
111
|
+
const existing = await readJsonOrNull(dangerZonesPath);
|
|
112
|
+
const zones = existing?.zones ?? [];
|
|
113
|
+
zones.push(zone);
|
|
114
|
+
await writeAtomic(dangerZonesPath, { zones }, { overwrite: true });
|
|
115
|
+
}
|
|
116
|
+
// ── OPEN QUESTIONS ───────────────────────────────────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* Returns all open questions, or an empty array if none exist.
|
|
119
|
+
*/
|
|
120
|
+
export async function getOpenQuestions(mathaDir) {
|
|
121
|
+
const questionsPath = path.join(mathaDir, 'hippocampus', 'open-questions.json');
|
|
122
|
+
const data = await readJsonOrNull(questionsPath);
|
|
123
|
+
return data?.questions ?? [];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Records a new open question.
|
|
127
|
+
*/
|
|
128
|
+
export async function recordOpenQuestion(mathaDir, question) {
|
|
129
|
+
const questionsPath = path.join(mathaDir, 'hippocampus', 'open-questions.json');
|
|
130
|
+
const existing = await readJsonOrNull(questionsPath);
|
|
131
|
+
const questions = existing?.questions ?? [];
|
|
132
|
+
questions.push(question);
|
|
133
|
+
await writeAtomic(questionsPath, { questions }, { overwrite: true });
|
|
134
|
+
}
|