@jaimevalasek/aioson 1.16.0 → 1.17.3
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/CHANGELOG.md +49 -0
- package/package.json +4 -2
- package/src/cli.js +5 -0
- package/src/commands/chain-audit.js +156 -0
- package/src/commands/runtime.js +27 -0
- package/src/commands/store-system.js +100 -12
- package/src/i18n/messages/en.js +9 -0
- package/src/i18n/messages/es.js +9 -0
- package/src/i18n/messages/fr.js +9 -0
- package/src/i18n/messages/pt-BR.js +9 -0
- package/src/neural-chain-agent-ingest.js +400 -0
- package/src/neural-chain-config.js +95 -0
- package/src/neural-chain-git-ingest.js +280 -0
- package/src/neural-chain-migration.js +61 -0
- package/src/neural-chain-noise-file.js +332 -0
- package/src/neural-chain-sanitize.js +0 -0
- package/src/neural-chain-telemetry.js +90 -0
- package/src/runtime-store.js +2 -0
- package/template/.aioson/agents/analyst.md +1 -1
- package/template/.aioson/agents/briefing.md +3 -1
- package/template/.aioson/agents/copywriter.md +1 -1
- package/template/.aioson/agents/dev.md +2 -2
- package/template/.aioson/agents/deyvin.md +12 -12
- package/template/.aioson/agents/neo.md +94 -72
- package/template/.aioson/agents/product.md +1 -1
- package/template/.aioson/agents/qa.md +3 -3
- package/template/.aioson/agents/sheldon.md +3 -3
- package/template/.aioson/agents/tester.md +1 -1
- package/template/.aioson/docs/briefing/briefing-craft.md +16 -0
- package/template/.aioson/docs/deyvin/runtime-handoffs.md +1 -1
- package/template/.aioson/docs/handoff-persistence.md +7 -7
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Neural Chain — agent_event ingest + per-session audit hook helper.
|
|
5
|
+
*
|
|
6
|
+
* Slice 3 closes the second edge type ('agent_event'). Co-edit pairs come
|
|
7
|
+
* from the `artifacts` list passed to `agent:done` (typically the files
|
|
8
|
+
* the agent created or modified during its session). Each session adds
|
|
9
|
+
* +1 to hit_count for every directional pair (A→B, B→A) in the artifacts.
|
|
10
|
+
*
|
|
11
|
+
* Confidence per BR-NC-01: `min(1.0, hit_count / CONFIDENCE_SATURATION=5)`.
|
|
12
|
+
* V1 simplification: running `hit_count` is treated as the agent_event
|
|
13
|
+
* frequency signal. BR-NC-01 specifies `count_last_30d`; M2 graph
|
|
14
|
+
* maintenance will introduce aging. Saturation at 5 hits bounds the
|
|
15
|
+
* approximation regardless.
|
|
16
|
+
*
|
|
17
|
+
* Hard cap per BR-NC-08: HARD_CAP_PER_NODE active edges per source_path,
|
|
18
|
+
* oldest archived by `last_seen_at` before exceeding the cap.
|
|
19
|
+
*
|
|
20
|
+
* EC-NC-05: empty / single-file artifact lists yield zero pairs →
|
|
21
|
+
* `runChainHookOnAgentDone` still emits exactly one `chain_audit` event
|
|
22
|
+
* with `impacts_found=0` so the guardrail metric series stays unbroken.
|
|
23
|
+
*
|
|
24
|
+
* Best-effort by design (BR-NC-11): every write is wrapped so a failure
|
|
25
|
+
* never blocks the caller in `runAgentDone`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
const { writeNoiseFile } = require('./neural-chain-noise-file');
|
|
30
|
+
const {
|
|
31
|
+
readChainConfig,
|
|
32
|
+
DEFAULT_AUTONOMY_MODE,
|
|
33
|
+
DEFAULT_CHAIN_AUTO_THRESHOLD
|
|
34
|
+
} = require('./neural-chain-config');
|
|
35
|
+
const { emitChainAuditEvent } = require('./neural-chain-telemetry');
|
|
36
|
+
const { isUnsafePath } = require('./neural-chain-sanitize');
|
|
37
|
+
|
|
38
|
+
const CONFIDENCE_SATURATION = 5;
|
|
39
|
+
const HARD_CAP_PER_NODE = 10000;
|
|
40
|
+
|
|
41
|
+
// BR-NC-02 rule (a) — common test naming patterns across languages.
|
|
42
|
+
// {stem} below = source module basename minus its extension.
|
|
43
|
+
// {stem}.test.{ext} • {stem}.spec.{ext}
|
|
44
|
+
// test_{stem}.{ext} • {stem}_test.{ext} • {stem}-test.{ext}
|
|
45
|
+
const SOURCE_EXT_RE = /\.[a-zA-Z0-9]+$/;
|
|
46
|
+
|
|
47
|
+
function escapeRegex(s) {
|
|
48
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isTestFileFor(targetPath, sourcePath) {
|
|
52
|
+
if (!targetPath || !sourcePath) return false;
|
|
53
|
+
const sourceBase = path.basename(String(sourcePath));
|
|
54
|
+
const targetBase = path.basename(String(targetPath));
|
|
55
|
+
const stem = sourceBase.replace(SOURCE_EXT_RE, '');
|
|
56
|
+
if (!stem || stem === sourceBase) return false; // source has no extension
|
|
57
|
+
const s = escapeRegex(stem);
|
|
58
|
+
const re = new RegExp(
|
|
59
|
+
`^(${s}\\.(test|spec)\\.[a-zA-Z0-9]+|test_${s}\\.[a-zA-Z0-9]+|${s}_test\\.[a-zA-Z0-9]+|${s}-test\\.[a-zA-Z0-9]+)$`,
|
|
60
|
+
'i'
|
|
61
|
+
);
|
|
62
|
+
return re.test(targetBase);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* BR-NC-02/03 classifier. Returns the marker to embed in the noise item:
|
|
67
|
+
* - 'AUTO-FIXABLE' — matches BR-NC-02 (a) or (c) in standard/autonomous
|
|
68
|
+
* - 'AUTO-FIXABLE-BEST-EFFORT' — non-match in autonomous mode
|
|
69
|
+
* - null — non-match in standard mode, or guarded mode
|
|
70
|
+
*
|
|
71
|
+
* BR-NC-02 (b) (literal identifier match via diff) is deferred — requires
|
|
72
|
+
* git diff parsing the prior session's edits, heavy for V1; planned as a
|
|
73
|
+
* follow-up. Documented in spec § "Decisões arquiteturais desta slice".
|
|
74
|
+
*/
|
|
75
|
+
function classifyImpact({ impact, sourceFile, autonomyMode, threshold }) {
|
|
76
|
+
if (autonomyMode === 'guarded') {
|
|
77
|
+
return { marker: null, classification: 'noise' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const isTestPair = isTestFileFor(impact && impact.target_path, sourceFile);
|
|
81
|
+
const isThresholdMatch =
|
|
82
|
+
impact &&
|
|
83
|
+
typeof impact.confidence === 'number' &&
|
|
84
|
+
impact.confidence > threshold &&
|
|
85
|
+
impact.edge_type === 'agent_event' &&
|
|
86
|
+
typeof impact.hit_count === 'number' &&
|
|
87
|
+
impact.hit_count > 5;
|
|
88
|
+
|
|
89
|
+
if (isTestPair || isThresholdMatch) {
|
|
90
|
+
return { marker: 'AUTO-FIXABLE', classification: 'auto_fixable' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (autonomyMode === 'autonomous') {
|
|
94
|
+
return { marker: 'AUTO-FIXABLE-BEST-EFFORT', classification: 'auto_fixable_best_effort' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { marker: null, classification: 'noise' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function deriveSessionPairs(artifacts) {
|
|
101
|
+
if (!Array.isArray(artifacts)) return [];
|
|
102
|
+
|
|
103
|
+
// SF-NC-01/02 Layer B — reject unsafe paths at ingest boundary BEFORE
|
|
104
|
+
// they reach chain_edges. Centralizes the newline/control-char check that
|
|
105
|
+
// shuts the BR-NC-03 guarded-mode bypass. Schema-level CHECK constraint
|
|
106
|
+
// deferred to M2 (SQLite ALTER TABLE doesn't support adding CHECK).
|
|
107
|
+
const files = artifacts.filter(
|
|
108
|
+
(f) =>
|
|
109
|
+
f &&
|
|
110
|
+
typeof f === 'string' &&
|
|
111
|
+
!f.startsWith('.aioson/') &&
|
|
112
|
+
!f.startsWith('.aioson\\') &&
|
|
113
|
+
!f.startsWith('.git/') &&
|
|
114
|
+
!f.startsWith('.git\\') &&
|
|
115
|
+
!isUnsafePath(f)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (files.length < 2) return [];
|
|
119
|
+
|
|
120
|
+
const pairs = [];
|
|
121
|
+
for (let i = 0; i < files.length; i++) {
|
|
122
|
+
for (let j = 0; j < files.length; j++) {
|
|
123
|
+
if (i === j) continue;
|
|
124
|
+
pairs.push({ source: files[i], target: files[j] });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return pairs;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function ingestAgentEventEdges({ db, artifacts, now = new Date() }) {
|
|
131
|
+
if (!db || typeof db.prepare !== 'function') {
|
|
132
|
+
throw new Error('ingestAgentEventEdges requires an open better-sqlite3 db handle');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const pairs = deriveSessionPairs(artifacts);
|
|
136
|
+
if (pairs.length === 0) {
|
|
137
|
+
return { skipped: true, reason: 'no_pairs', upserted: 0, archived: 0, capped_inserts: 0 };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const nowIso = now.toISOString();
|
|
141
|
+
const stats = { skipped: false, upserted: 0, archived: 0, capped_inserts: 0 };
|
|
142
|
+
const initialConfidence = Math.min(1.0, 1 / CONFIDENCE_SATURATION);
|
|
143
|
+
|
|
144
|
+
// UPSERT: on conflict, increment hit_count + recompute confidence.
|
|
145
|
+
// SQLite ON CONFLICT semantics — `hit_count` (no qualifier) = current row's value;
|
|
146
|
+
// `excluded.<col>` = the value being inserted.
|
|
147
|
+
const upsertStmt = db.prepare(`
|
|
148
|
+
INSERT INTO chain_edges (source_path, target_path, edge_type, confidence, start_at, last_seen_at, hit_count)
|
|
149
|
+
VALUES (?, ?, 'agent_event', ?, ?, ?, 1)
|
|
150
|
+
ON CONFLICT(source_path, target_path, edge_type) WHERE end_at IS NULL
|
|
151
|
+
DO UPDATE SET
|
|
152
|
+
hit_count = hit_count + 1,
|
|
153
|
+
confidence = MIN(1.0, (hit_count + 1.0) / ${CONFIDENCE_SATURATION}.0),
|
|
154
|
+
last_seen_at = excluded.last_seen_at
|
|
155
|
+
`);
|
|
156
|
+
|
|
157
|
+
const countActiveStmt = db.prepare(`
|
|
158
|
+
SELECT count(*) AS c FROM chain_edges
|
|
159
|
+
WHERE source_path = ? AND end_at IS NULL
|
|
160
|
+
`);
|
|
161
|
+
|
|
162
|
+
const findExistingStmt = db.prepare(`
|
|
163
|
+
SELECT id FROM chain_edges
|
|
164
|
+
WHERE source_path = ? AND target_path = ? AND edge_type = 'agent_event' AND end_at IS NULL
|
|
165
|
+
`);
|
|
166
|
+
|
|
167
|
+
const findOldestStmt = db.prepare(`
|
|
168
|
+
SELECT id FROM chain_edges
|
|
169
|
+
WHERE source_path = ? AND end_at IS NULL
|
|
170
|
+
ORDER BY last_seen_at ASC LIMIT 1
|
|
171
|
+
`);
|
|
172
|
+
|
|
173
|
+
const archiveStmt = db.prepare('UPDATE chain_edges SET end_at = ? WHERE id = ?');
|
|
174
|
+
|
|
175
|
+
const tx = db.transaction((items) => {
|
|
176
|
+
for (const item of items) {
|
|
177
|
+
// BR-NC-08 hard cap — enforce only on new edges; existing-edge updates
|
|
178
|
+
// don't grow the active set.
|
|
179
|
+
const existingRow = findExistingStmt.get(item.source, item.target);
|
|
180
|
+
if (!existingRow) {
|
|
181
|
+
const { c: activeCount } = countActiveStmt.get(item.source);
|
|
182
|
+
if (activeCount >= HARD_CAP_PER_NODE) {
|
|
183
|
+
const oldest = findOldestStmt.get(item.source);
|
|
184
|
+
if (oldest) {
|
|
185
|
+
archiveStmt.run(nowIso, oldest.id);
|
|
186
|
+
stats.archived += 1;
|
|
187
|
+
stats.capped_inserts += 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
upsertStmt.run(item.source, item.target, initialConfidence, nowIso, nowIso);
|
|
193
|
+
stats.upserted += 1;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
tx(pairs);
|
|
198
|
+
return stats;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function queryImpacts(db, sourcePath, limit = 20) {
|
|
202
|
+
try {
|
|
203
|
+
// BR-NC-01 — when both `git_co_edit` and `agent_event` edges exist for
|
|
204
|
+
// the same (source, target), report max(c_git, c_event) — NOT both rows
|
|
205
|
+
// (which would double-count and duplicate items in the noise file).
|
|
206
|
+
// Window function dedupes per target_path, keeping the row with the
|
|
207
|
+
// highest confidence (tiebreaker by hit_count then last_seen_at).
|
|
208
|
+
// Hotfix v1.17.1 — bug-found-002 from @qa Gate D residual M-02.
|
|
209
|
+
return db.prepare(`
|
|
210
|
+
SELECT target_path, edge_type, confidence, hit_count, last_seen_at
|
|
211
|
+
FROM (
|
|
212
|
+
SELECT target_path, edge_type, confidence, hit_count, last_seen_at,
|
|
213
|
+
ROW_NUMBER() OVER (
|
|
214
|
+
PARTITION BY target_path
|
|
215
|
+
ORDER BY confidence DESC, hit_count DESC, last_seen_at DESC
|
|
216
|
+
) AS rn
|
|
217
|
+
FROM chain_edges
|
|
218
|
+
WHERE source_path = ? AND end_at IS NULL
|
|
219
|
+
)
|
|
220
|
+
WHERE rn = 1
|
|
221
|
+
ORDER BY confidence DESC, hit_count DESC, last_seen_at DESC
|
|
222
|
+
LIMIT ?
|
|
223
|
+
`).all(sourcePath, limit);
|
|
224
|
+
} catch (_) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Per-session hook called from `runAgentDone` after standard telemetry is
|
|
231
|
+
* written. Performs ingest + per-file audit telemetry + EC-NC-05 no-op
|
|
232
|
+
* fallback. Never throws; the caller wraps the invocation in try/catch
|
|
233
|
+
* defensively, but this function additionally guards each SQL boundary
|
|
234
|
+
* via try/catch and emitChainAuditEvent so a partial failure never
|
|
235
|
+
* propagates.
|
|
236
|
+
*/
|
|
237
|
+
function runChainHookOnAgentDone({
|
|
238
|
+
db,
|
|
239
|
+
artifacts,
|
|
240
|
+
agentName = null,
|
|
241
|
+
featureSlug = null,
|
|
242
|
+
targetDir = null,
|
|
243
|
+
autonomyMode = null,
|
|
244
|
+
chainAutoThreshold = null,
|
|
245
|
+
now = new Date()
|
|
246
|
+
} = {}) {
|
|
247
|
+
if (!db || typeof db.prepare !== 'function') {
|
|
248
|
+
return { ok: false, reason: 'missing_db' };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Slice 6: resolve autonomy mode + threshold from `.aioson/config.md`
|
|
252
|
+
// frontmatter when not explicitly provided. EC-NC-07: missing file /
|
|
253
|
+
// missing keys / invalid values → runtime defaults (guarded, 0.8).
|
|
254
|
+
let resolvedMode = autonomyMode;
|
|
255
|
+
let resolvedThreshold = chainAutoThreshold;
|
|
256
|
+
if ((resolvedMode === null || resolvedThreshold === null) && targetDir) {
|
|
257
|
+
try {
|
|
258
|
+
const cfg = readChainConfig({ targetDir });
|
|
259
|
+
if (resolvedMode === null) resolvedMode = cfg.autonomyMode;
|
|
260
|
+
if (resolvedThreshold === null) resolvedThreshold = cfg.chainAutoThreshold;
|
|
261
|
+
} catch (_) {
|
|
262
|
+
// best-effort
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (resolvedMode === null) resolvedMode = DEFAULT_AUTONOMY_MODE;
|
|
266
|
+
if (resolvedThreshold === null) resolvedThreshold = DEFAULT_CHAIN_AUTO_THRESHOLD;
|
|
267
|
+
|
|
268
|
+
const safeArtifacts = Array.isArray(artifacts) ? artifacts.slice() : [];
|
|
269
|
+
|
|
270
|
+
let ingest;
|
|
271
|
+
try {
|
|
272
|
+
ingest = ingestAgentEventEdges({ db, artifacts: safeArtifacts, now });
|
|
273
|
+
} catch (err) {
|
|
274
|
+
ingest = { skipped: true, reason: 'ingest_failed', error: err && err.message ? err.message : String(err) };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const audits = [];
|
|
278
|
+
|
|
279
|
+
if (safeArtifacts.length === 0) {
|
|
280
|
+
// EC-NC-05: zero edits → emit one no-op audit event so the guardrail
|
|
281
|
+
// metric series stays continuous.
|
|
282
|
+
emitChainAuditEvent(db, {
|
|
283
|
+
agent: agentName,
|
|
284
|
+
message: 'chain:audit (no-op: no artifacts in session)',
|
|
285
|
+
feature_slug: featureSlug,
|
|
286
|
+
source_files: [],
|
|
287
|
+
impacts_found: 0,
|
|
288
|
+
auto_fixable_count: 0,
|
|
289
|
+
noise_file: null,
|
|
290
|
+
tokens_used: 0,
|
|
291
|
+
duration_ms: 0,
|
|
292
|
+
error: null,
|
|
293
|
+
// Extra context fields
|
|
294
|
+
skipped_reason: 'no_artifacts',
|
|
295
|
+
autonomy_mode: resolvedMode,
|
|
296
|
+
chain_auto_threshold: resolvedThreshold,
|
|
297
|
+
ingest_stats: ingest,
|
|
298
|
+
// Legacy singular alias preserved for backward-compat with any dashboard
|
|
299
|
+
// queries written against the v1.17.0 schema (will be removed v2).
|
|
300
|
+
source_file: null
|
|
301
|
+
});
|
|
302
|
+
return { ok: true, ingest, audits, ec_nc_05: true, noise_file: null, autonomy_mode: resolvedMode };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Pass 1 — collect impacts per source file AND classify each via
|
|
306
|
+
// BR-NC-02/03. Marker is attached to the impact in place so writeNoiseFile
|
|
307
|
+
// can render the prefix verbatim.
|
|
308
|
+
let autoFixableCount = 0;
|
|
309
|
+
for (const file of safeArtifacts) {
|
|
310
|
+
const startedAt = Date.now();
|
|
311
|
+
const rawImpacts = queryImpacts(db, file);
|
|
312
|
+
const durationMs = Date.now() - startedAt;
|
|
313
|
+
const classified = Array.isArray(rawImpacts)
|
|
314
|
+
? rawImpacts.map((impact) => {
|
|
315
|
+
const { marker, classification } = classifyImpact({
|
|
316
|
+
impact,
|
|
317
|
+
sourceFile: file,
|
|
318
|
+
autonomyMode: resolvedMode,
|
|
319
|
+
threshold: resolvedThreshold
|
|
320
|
+
});
|
|
321
|
+
if (classification === 'auto_fixable') autoFixableCount += 1;
|
|
322
|
+
return { ...impact, marker, classification };
|
|
323
|
+
})
|
|
324
|
+
: [];
|
|
325
|
+
audits.push({
|
|
326
|
+
source_file: file,
|
|
327
|
+
impacts: classified,
|
|
328
|
+
impacts_found: rawImpacts === null ? 0 : classified.length,
|
|
329
|
+
duration_ms: durationMs,
|
|
330
|
+
error: rawImpacts === null ? 'query_failed' : null
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// BR-NC-06/03: noise file is written in `standard` and `autonomous` modes
|
|
335
|
+
// too (Slice 6) — items carry the `[AUTO-FIXABLE]` / `[AUTO-FIXABLE-BEST-EFFORT]`
|
|
336
|
+
// prefix when applicable. `guarded` mode still writes one noise file with
|
|
337
|
+
// unprefixed items (same as Slice 4). Skip the write only when there are
|
|
338
|
+
// zero impacts across all artifacts.
|
|
339
|
+
let noiseFile = null;
|
|
340
|
+
const hasAnyImpacts = audits.some((a) => a.impacts_found > 0);
|
|
341
|
+
if (targetDir && hasAnyImpacts) {
|
|
342
|
+
try {
|
|
343
|
+
const result = writeNoiseFile({
|
|
344
|
+
targetDir,
|
|
345
|
+
featureSlug,
|
|
346
|
+
audits,
|
|
347
|
+
autonomyMode: resolvedMode,
|
|
348
|
+
now
|
|
349
|
+
});
|
|
350
|
+
noiseFile = result.path;
|
|
351
|
+
} catch (_) {
|
|
352
|
+
// BR-NC-11 best-effort: noise write must not block agent_done.
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Pass 2 — per-artifact telemetry events. noise_file is the same on every
|
|
357
|
+
// event (session-scoped) so dashboards can attribute the file regardless of
|
|
358
|
+
// which event row is sampled.
|
|
359
|
+
for (const audit of audits) {
|
|
360
|
+
emitChainAuditEvent(db, {
|
|
361
|
+
agent: agentName,
|
|
362
|
+
message: `chain:audit ${audit.source_file} → ${audit.error ? 'error' : `${audit.impacts_found} impacts`} (session hook)`,
|
|
363
|
+
feature_slug: featureSlug,
|
|
364
|
+
source_files: safeArtifacts,
|
|
365
|
+
impacts_found: audit.error ? null : audit.impacts_found,
|
|
366
|
+
auto_fixable_count: audit.error ? 0 : audit.impacts.filter((i) => i.classification === 'auto_fixable').length,
|
|
367
|
+
noise_file: noiseFile,
|
|
368
|
+
tokens_used: 0,
|
|
369
|
+
duration_ms: audit.duration_ms,
|
|
370
|
+
error: audit.error,
|
|
371
|
+
// Extra context fields
|
|
372
|
+
autonomy_mode: resolvedMode,
|
|
373
|
+
chain_auto_threshold: resolvedThreshold,
|
|
374
|
+
ingest_stats: ingest,
|
|
375
|
+
// Legacy singular alias preserved for backward-compat (removed v2)
|
|
376
|
+
source_file: audit.source_file
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
ok: true,
|
|
382
|
+
ingest,
|
|
383
|
+
audits,
|
|
384
|
+
noise_file: noiseFile,
|
|
385
|
+
autonomy_mode: resolvedMode,
|
|
386
|
+
chain_auto_threshold: resolvedThreshold,
|
|
387
|
+
auto_fixable_count: autoFixableCount
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
module.exports = {
|
|
392
|
+
deriveSessionPairs,
|
|
393
|
+
ingestAgentEventEdges,
|
|
394
|
+
runChainHookOnAgentDone,
|
|
395
|
+
queryImpacts,
|
|
396
|
+
classifyImpact,
|
|
397
|
+
isTestFileFor,
|
|
398
|
+
CONFIDENCE_SATURATION,
|
|
399
|
+
HARD_CAP_PER_NODE
|
|
400
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Neural Chain — config reader for autonomy mode + auto-fix threshold.
|
|
5
|
+
*
|
|
6
|
+
* Reads `.aioson/config.md` YAML frontmatter (if present) for two keys:
|
|
7
|
+
* - `autonomy_mode` → 'guarded' | 'standard' | 'autonomous'
|
|
8
|
+
* - `chain_auto_threshold` → REAL in [0.0, 1.0]
|
|
9
|
+
*
|
|
10
|
+
* EC-NC-07: missing file / missing frontmatter / missing key / invalid value
|
|
11
|
+
* → runtime defaults (`guarded`, 0.8). Never force-edits the config file.
|
|
12
|
+
*
|
|
13
|
+
* The current canonical `.aioson/config.md` is a pure documentation Markdown
|
|
14
|
+
* file with no frontmatter; users opt in to Neural Chain knobs by adding a
|
|
15
|
+
* `---` frontmatter block at the top of that file. See spec § BR-NC-03 +
|
|
16
|
+
* `requirements-neural-chain.md` § chain_auto_threshold for the contract.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
const { parseYamlFrontmatter } = require('./context');
|
|
22
|
+
|
|
23
|
+
const VALID_AUTONOMY_MODES = Object.freeze(['guarded', 'standard', 'autonomous']);
|
|
24
|
+
const DEFAULT_AUTONOMY_MODE = 'guarded';
|
|
25
|
+
const DEFAULT_CHAIN_AUTO_THRESHOLD = 0.8;
|
|
26
|
+
|
|
27
|
+
function normalizeAutonomyMode(value) {
|
|
28
|
+
if (typeof value !== 'string') return null;
|
|
29
|
+
const v = value.trim().toLowerCase();
|
|
30
|
+
return VALID_AUTONOMY_MODES.includes(v) ? v : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeThreshold(value) {
|
|
34
|
+
let n;
|
|
35
|
+
if (typeof value === 'number') {
|
|
36
|
+
n = value;
|
|
37
|
+
} else if (typeof value === 'string') {
|
|
38
|
+
n = parseFloat(value.trim());
|
|
39
|
+
} else {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) return null;
|
|
43
|
+
// SF-NC-03 — reject negative zero. JS quirk: -0 < 0 is false, so the range
|
|
44
|
+
// check above passes, but using -0 as a threshold is operationally
|
|
45
|
+
// equivalent to setting threshold=0 (everything above 0 confidence auto-
|
|
46
|
+
// fixes in standard/autonomous mode) and is a smell that the source value
|
|
47
|
+
// was crafted to dodge validation. Normalize positive zero only.
|
|
48
|
+
if (Object.is(n, -0)) return null;
|
|
49
|
+
return n;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readChainConfig({ targetDir } = {}) {
|
|
53
|
+
const defaults = {
|
|
54
|
+
autonomyMode: DEFAULT_AUTONOMY_MODE,
|
|
55
|
+
chainAutoThreshold: DEFAULT_CHAIN_AUTO_THRESHOLD,
|
|
56
|
+
source: 'defaults'
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (!targetDir || typeof targetDir !== 'string') return defaults;
|
|
60
|
+
|
|
61
|
+
const filePath = path.join(targetDir, '.aioson', 'config.md');
|
|
62
|
+
let text;
|
|
63
|
+
try {
|
|
64
|
+
text = fs.readFileSync(filePath, 'utf8');
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (err && err.code === 'ENOENT') return defaults;
|
|
67
|
+
// Any other I/O error → defaults (best-effort, never blocks the hook).
|
|
68
|
+
return { ...defaults, source: 'read_error', error: err.message };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const parsed = parseYamlFrontmatter(text);
|
|
72
|
+
if (!parsed.ok) {
|
|
73
|
+
// No frontmatter / malformed → defaults. EC-NC-07 forbids force-editing.
|
|
74
|
+
return { ...defaults, source: 'no_frontmatter', reason: parsed.reason };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const autonomyMode = normalizeAutonomyMode(parsed.data.autonomy_mode) || DEFAULT_AUTONOMY_MODE;
|
|
78
|
+
const threshold = normalizeThreshold(parsed.data.chain_auto_threshold);
|
|
79
|
+
const chainAutoThreshold = threshold === null ? DEFAULT_CHAIN_AUTO_THRESHOLD : threshold;
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
autonomyMode,
|
|
83
|
+
chainAutoThreshold,
|
|
84
|
+
source: 'config_md'
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
readChainConfig,
|
|
90
|
+
normalizeAutonomyMode,
|
|
91
|
+
normalizeThreshold,
|
|
92
|
+
VALID_AUTONOMY_MODES,
|
|
93
|
+
DEFAULT_AUTONOMY_MODE,
|
|
94
|
+
DEFAULT_CHAIN_AUTO_THRESHOLD
|
|
95
|
+
};
|