soturail 0.8.0 → 0.9.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/README.md +57 -16
- package/dist/commands/bench.d.ts +1 -0
- package/dist/commands/bench.js +18 -6
- package/dist/commands/bench.js.map +1 -1
- package/dist/commands/brain.js +14 -7
- package/dist/commands/brain.js.map +1 -1
- package/dist/commands/native.js +12 -0
- package/dist/commands/native.js.map +1 -1
- package/dist/commands/rules.js +20 -1
- package/dist/commands/rules.js.map +1 -1
- package/dist/commands/self.js +19 -0
- package/dist/commands/self.js.map +1 -1
- package/dist/core/baseline-snapshot.d.ts +27 -0
- package/dist/core/baseline-snapshot.js +225 -0
- package/dist/core/baseline-snapshot.js.map +1 -0
- package/dist/core/benchmark-rail.d.ts +44 -0
- package/dist/core/benchmark-rail.js +322 -0
- package/dist/core/benchmark-rail.js.map +1 -0
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.js +6 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/evaluation-suite.js +117 -2
- package/dist/core/evaluation-suite.js.map +1 -1
- package/dist/core/evidence-pack.js +17 -0
- package/dist/core/evidence-pack.js.map +1 -1
- package/dist/core/harness-rail.js +2 -0
- package/dist/core/harness-rail.js.map +1 -1
- package/dist/core/native-candidates.d.ts +44 -0
- package/dist/core/native-candidates.js +163 -0
- package/dist/core/native-candidates.js.map +1 -0
- package/dist/core/project-brain.d.ts +70 -4
- package/dist/core/project-brain.js +475 -30
- package/dist/core/project-brain.js.map +1 -1
- package/dist/core/release-preflight.js +8 -0
- package/dist/core/release-preflight.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/docs/agents.md +9 -1
- package/docs/baseline-snapshots.md +54 -0
- package/docs/benchmarking.md +69 -0
- package/docs/code-graph.md +33 -0
- package/docs/diagram-rail.md +2 -0
- package/docs/evaluation-suite.md +23 -0
- package/docs/harness-rail.md +10 -0
- package/docs/knowledge-to-rules.md +23 -0
- package/docs/memory-rail.md +2 -0
- package/docs/native-performance-policy.md +62 -0
- package/docs/native-runner.md +24 -0
- package/docs/project-brain.md +101 -4
- package/docs/release-workflow.md +13 -0
- package/docs/releases/README.md +2 -0
- package/docs/releases/RELEASE_NOTES_v0.8.1.md +42 -0
- package/docs/releases/RELEASE_NOTES_v0.9.0.md +35 -0
- package/docs/reverse-specification-rail.md +19 -0
- package/docs/rules.md +3 -0
- package/docs/workflow-rail.md +23 -0
- package/package.json +1 -1
|
@@ -41,7 +41,12 @@ export async function initBrain(root = process.cwd()) {
|
|
|
41
41
|
paths.brainProjectProfileFile,
|
|
42
42
|
paths.brainArchitectureFile,
|
|
43
43
|
paths.brainIndexFile,
|
|
44
|
-
paths.brainFreshnessFile
|
|
44
|
+
paths.brainFreshnessFile,
|
|
45
|
+
paths.brainConsolidatedClaimsFile,
|
|
46
|
+
paths.brainConsolidationReportJson,
|
|
47
|
+
paths.brainConsolidationReportMd,
|
|
48
|
+
paths.brainRepairPlanJson,
|
|
49
|
+
paths.brainRepairPlanMd
|
|
45
50
|
].map((filePath) => relativeToRoot(root, filePath));
|
|
46
51
|
return {
|
|
47
52
|
paths: createdPaths,
|
|
@@ -124,6 +129,8 @@ export async function scanBrain(root = process.cwd()) {
|
|
|
124
129
|
`tests_detected: ${tests.length}`,
|
|
125
130
|
"next_commands:",
|
|
126
131
|
"- soturail brain recall \"release notes\"",
|
|
132
|
+
"- soturail brain consolidate --dry-run",
|
|
133
|
+
"- soturail brain stale --repair-plan",
|
|
127
134
|
"- soturail rules from-brain",
|
|
128
135
|
"- soturail reverse gaps"
|
|
129
136
|
].join("\n") + "\n"
|
|
@@ -150,7 +157,8 @@ export async function brainProfile(root = process.cwd()) {
|
|
|
150
157
|
`gaps: ${counts.gaps}`,
|
|
151
158
|
`suspect_or_stale: ${counts.suspectOrStale}`,
|
|
152
159
|
"next_commands:",
|
|
153
|
-
"- soturail brain
|
|
160
|
+
"- soturail brain consolidate --dry-run",
|
|
161
|
+
"- soturail brain stale --repair-plan",
|
|
154
162
|
"- soturail brain export --agent generic",
|
|
155
163
|
"- soturail reverse specs ./src"
|
|
156
164
|
].join("\n") + "\n";
|
|
@@ -189,7 +197,7 @@ export async function recallBrain(query, root = process.cwd(), limit = 8) {
|
|
|
189
197
|
])
|
|
190
198
|
].join("\n").trimEnd() + "\n";
|
|
191
199
|
}
|
|
192
|
-
export async function staleBrain(root = process.cwd()) {
|
|
200
|
+
export async function staleBrain(root = process.cwd(), options = {}) {
|
|
193
201
|
await initBrain(root);
|
|
194
202
|
const paths = getWorkspacePaths(root);
|
|
195
203
|
const claims = await readJsonl(paths.brainClaimsFile);
|
|
@@ -199,19 +207,38 @@ export async function staleBrain(root = process.cwd()) {
|
|
|
199
207
|
for (const claim of claims) {
|
|
200
208
|
const absolute = path.resolve(root, claim.sourcePath);
|
|
201
209
|
if (!(await exists(absolute))) {
|
|
202
|
-
events.push(staleEvent(claim.id, "source file missing", claim.rangeHash, "missing", "stale"));
|
|
210
|
+
events.push(staleEvent(claim.id, "source file missing", claim.rangeHash, "missing", "stale", { previousRange: claim.sourceRange }));
|
|
203
211
|
continue;
|
|
204
212
|
}
|
|
205
213
|
const evidence = await sourceEvidence(root, claim.sourcePath, undefined, claim.sourceRange);
|
|
206
214
|
if (claim.rangeHash !== evidence.rangeHash) {
|
|
207
|
-
|
|
215
|
+
const relocation = await relocateSourceRange(root, claim);
|
|
216
|
+
if (relocation.status === "relocated") {
|
|
217
|
+
events.push(staleEvent(claim.id, "source range relocated", claim.rangeHash, relocation.rangeHash, "relocated", {
|
|
218
|
+
previousRange: claim.sourceRange,
|
|
219
|
+
...(relocation.range ? { candidateRange: relocation.range } : {}),
|
|
220
|
+
similarity: relocation.similarity
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
else if (relocation.status === "candidate") {
|
|
224
|
+
events.push(staleEvent(claim.id, "rangeHash changed; relocation candidate", claim.rangeHash, evidence.rangeHash, "suspect", {
|
|
225
|
+
previousRange: claim.sourceRange,
|
|
226
|
+
...(relocation.range ? { candidateRange: relocation.range } : {}),
|
|
227
|
+
similarity: relocation.similarity
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
events.push(staleEvent(claim.id, "rangeHash changed", claim.rangeHash, evidence.rangeHash, "suspect", {
|
|
232
|
+
previousRange: claim.sourceRange
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
208
235
|
}
|
|
209
236
|
else if (claim.fileHash !== evidence.fileHash) {
|
|
210
237
|
warnings.push(`${claim.id}: fileHash changed but rangeHash is unchanged`);
|
|
211
238
|
}
|
|
212
239
|
for (const validationPath of claim.validatedBy) {
|
|
213
240
|
if (!(await exists(path.resolve(root, validationPath)))) {
|
|
214
|
-
events.push(staleEvent(claim.id, `validatedBy missing: ${validationPath}`, claim.rangeHash, "missing-validation", "suspect"));
|
|
241
|
+
events.push(staleEvent(claim.id, `validatedBy missing: ${validationPath}`, claim.rangeHash, "missing-validation", "suspect", { previousRange: claim.sourceRange }));
|
|
215
242
|
}
|
|
216
243
|
}
|
|
217
244
|
}
|
|
@@ -228,22 +255,71 @@ export async function staleBrain(root = process.cwd()) {
|
|
|
228
255
|
events
|
|
229
256
|
};
|
|
230
257
|
await writeJson(paths.brainFreshnessFile, freshness);
|
|
258
|
+
const repairPlan = options.repairPlan ? await writeRepairPlan(root, claims, events) : undefined;
|
|
231
259
|
return {
|
|
232
260
|
freshness,
|
|
261
|
+
...(repairPlan ? { repairPlan } : {}),
|
|
233
262
|
output: [
|
|
234
263
|
"SotuRail brain stale",
|
|
235
264
|
`checked_records: ${freshness.checkedRecords}`,
|
|
236
265
|
`new_events: ${newEvents.length}`,
|
|
237
266
|
`suspect: ${freshness.suspect}`,
|
|
238
267
|
`stale: ${freshness.stale}`,
|
|
268
|
+
`relocated: ${freshness.events.filter((event) => event.status === "relocated").length}`,
|
|
239
269
|
`warnings: ${freshness.warnings.length}`,
|
|
240
270
|
`freshness: ${relativeToRoot(root, paths.brainFreshnessFile)}`,
|
|
271
|
+
...(repairPlan ? [
|
|
272
|
+
`repair_plan_json: ${relativeToRoot(root, paths.brainRepairPlanJson)}`,
|
|
273
|
+
`repair_plan_md: ${relativeToRoot(root, paths.brainRepairPlanMd)}`
|
|
274
|
+
] : []),
|
|
241
275
|
"",
|
|
242
276
|
...(newEvents.length > 0 ? newEvents.map((event) => `- ${event.recordId}: ${event.reason} [${event.status}]`) : ["No stale evidence drift detected."])
|
|
243
277
|
].join("\n") + "\n"
|
|
244
278
|
};
|
|
245
279
|
}
|
|
246
|
-
export async function
|
|
280
|
+
export async function consolidateBrain(root = process.cwd(), options = {}) {
|
|
281
|
+
await initBrain(root);
|
|
282
|
+
const paths = getWorkspacePaths(root);
|
|
283
|
+
const claims = await readJsonl(paths.brainClaimsFile);
|
|
284
|
+
const dryRun = options.dryRun !== false;
|
|
285
|
+
const groups = findDuplicateClaimGroups(claims);
|
|
286
|
+
const duplicateIds = new Set(groups.flatMap((group) => group.duplicateIds));
|
|
287
|
+
const canonicalIds = new Set(groups.map((group) => group.canonicalId));
|
|
288
|
+
const canonicalClaims = claims.filter((claim) => !duplicateIds.has(claim.id));
|
|
289
|
+
const consolidated = canonicalClaims.map((claim) => ({
|
|
290
|
+
...claim,
|
|
291
|
+
mergedClaimIds: groups.find((group) => group.canonicalId === claim.id)?.duplicateIds ?? []
|
|
292
|
+
}));
|
|
293
|
+
const report = {
|
|
294
|
+
schemaVersion: "soturail.brain.consolidation.v1",
|
|
295
|
+
createdAt: new Date().toISOString(),
|
|
296
|
+
dryRun,
|
|
297
|
+
claimsRead: claims.length,
|
|
298
|
+
duplicateGroups: groups.length,
|
|
299
|
+
mergedClaims: duplicateIds.size,
|
|
300
|
+
canonicalClaims: canonicalIds.size + claims.filter((claim) => !duplicateIds.has(claim.id) && !canonicalIds.has(claim.id)).length,
|
|
301
|
+
groups
|
|
302
|
+
};
|
|
303
|
+
await writeJson(paths.brainConsolidationReportJson, report);
|
|
304
|
+
await fs.writeFile(paths.brainConsolidationReportMd, renderConsolidationReport(root, report), "utf8");
|
|
305
|
+
await fs.writeFile(paths.brainConsolidatedClaimsFile, consolidated.map((claim) => JSON.stringify(claim)).join("\n") + (consolidated.length > 0 ? "\n" : ""), "utf8");
|
|
306
|
+
return {
|
|
307
|
+
report,
|
|
308
|
+
output: [
|
|
309
|
+
"SotuRail brain consolidate",
|
|
310
|
+
`dry_run: ${dryRun}`,
|
|
311
|
+
`claims_read: ${report.claimsRead}`,
|
|
312
|
+
`duplicate_groups: ${report.duplicateGroups}`,
|
|
313
|
+
`merged_claims: ${report.mergedClaims}`,
|
|
314
|
+
`canonical_claims: ${report.canonicalClaims}`,
|
|
315
|
+
`consolidated: ${relativeToRoot(root, paths.brainConsolidatedClaimsFile)}`,
|
|
316
|
+
`report_json: ${relativeToRoot(root, paths.brainConsolidationReportJson)}`,
|
|
317
|
+
`report: ${relativeToRoot(root, paths.brainConsolidationReportMd)}`,
|
|
318
|
+
"history_preserved: true"
|
|
319
|
+
].join("\n") + "\n"
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
export async function brainDoctor(root = process.cwd(), options = {}) {
|
|
247
323
|
await initBrain(root);
|
|
248
324
|
const paths = getWorkspacePaths(root);
|
|
249
325
|
const counts = await brainCounts(root);
|
|
@@ -263,21 +339,47 @@ export async function brainDoctor(root = process.cwd()) {
|
|
|
263
339
|
files.push({ path: relativeToRoot(root, filePath), present: await exists(filePath) });
|
|
264
340
|
}
|
|
265
341
|
const claims = await readJsonl(paths.brainClaimsFile);
|
|
342
|
+
const rules = await readJsonl(paths.brainRulesFile);
|
|
343
|
+
const duplicateGroups = findDuplicateClaimGroups(claims).length;
|
|
344
|
+
const activeSourceIds = new Set([...claims.map((claim) => claim.id), ...(await readJsonl(paths.brainDecisionsFile)).map((decision) => decision.id)]);
|
|
345
|
+
const rulesWithoutSources = rules.filter((rule) => [...rule.sourceClaimIds, ...(rule.sourceDecisionIds ?? [])].every((id) => !activeSourceIds.has(id))).length;
|
|
346
|
+
const integrationStatus = {
|
|
347
|
+
projectProfile: await exists(paths.brainProjectProfileFile) ? "present" : "missing",
|
|
348
|
+
workflow: await exists(paths.workflowsDir) ? "present" : "missing",
|
|
349
|
+
harness: await exists(paths.harnessDir) ? "present" : "missing",
|
|
350
|
+
diagram: await exists(paths.diagramsDir) ? "present" : "missing",
|
|
351
|
+
eval: await exists(path.join(paths.workspace, "eval", "latest.json")) ? "latest report present" : "no latest report",
|
|
352
|
+
agentExports: await exists(path.join(paths.brainExportsDir, "agent-brief.md")) ? "present" : "missing",
|
|
353
|
+
releaseProcess: await exists(path.join(root, "docs", "releases")) ? "docs/releases present" : "unknown"
|
|
354
|
+
};
|
|
266
355
|
const findings = [
|
|
267
356
|
...(claims.some((claim) => !claim.sourcePath) ? ["Some claims are missing source paths."] : []),
|
|
268
357
|
...(claims.some((claim) => claim.status === "verified" && claim.validatedBy.length === 0) ? ["Some verified claims have no validation references."] : []),
|
|
358
|
+
...(duplicateGroups > 0 ? [`Duplicate claim groups found: ${duplicateGroups}`] : []),
|
|
269
359
|
...(counts.gaps > 0 ? [`Open gaps recorded: ${counts.gaps}`] : []),
|
|
270
360
|
...(counts.suspectOrStale > 0 ? [`Suspect or stale records/events: ${counts.suspectOrStale}`] : []),
|
|
361
|
+
...(rulesWithoutSources > 0 ? [`Rules without live brain sources: ${rulesWithoutSources}`] : []),
|
|
271
362
|
...(await exists(path.join(paths.brainExportsDir, "agent-brief.md")) ? [] : ["Agent brief export is missing."])
|
|
272
363
|
];
|
|
364
|
+
const repairPlan = options.repairPlan ? (await staleBrain(root, { repairPlan: true })).repairPlan : undefined;
|
|
273
365
|
const report = {
|
|
274
366
|
schemaVersion: "soturail.brain.doctor.v1",
|
|
275
367
|
ok: files.every((file) => file.present && file.validJsonl !== false),
|
|
276
368
|
createdAt: new Date().toISOString(),
|
|
277
369
|
files,
|
|
278
370
|
counts,
|
|
371
|
+
duplicateClaimGroups: duplicateGroups,
|
|
372
|
+
integrationStatus,
|
|
373
|
+
...(repairPlan ? { repairPlanPath: relativeToRoot(root, paths.brainRepairPlanMd) } : {}),
|
|
279
374
|
findings,
|
|
280
|
-
nextCommands: [
|
|
375
|
+
nextCommands: [
|
|
376
|
+
"soturail brain scan",
|
|
377
|
+
"soturail brain consolidate --dry-run",
|
|
378
|
+
"soturail brain stale --repair-plan",
|
|
379
|
+
"soturail brain export --agent generic",
|
|
380
|
+
"soturail rules from-brain",
|
|
381
|
+
"soturail eval run --suite brain"
|
|
382
|
+
]
|
|
281
383
|
};
|
|
282
384
|
await writeJson(paths.brainDoctorFile, report);
|
|
283
385
|
return {
|
|
@@ -291,11 +393,17 @@ export async function brainDoctor(root = process.cwd()) {
|
|
|
291
393
|
`gaps: ${counts.gaps}`,
|
|
292
394
|
`rules: ${counts.rules}`,
|
|
293
395
|
`stale_events: ${counts.staleEvents}`,
|
|
396
|
+
`duplicate_claim_groups: ${duplicateGroups}`,
|
|
397
|
+
`rules_without_sources: ${rulesWithoutSources}`,
|
|
294
398
|
`doctor_json: ${relativeToRoot(root, paths.brainDoctorFile)}`,
|
|
399
|
+
...(repairPlan ? [`repair_plan: ${relativeToRoot(root, paths.brainRepairPlanMd)}`] : []),
|
|
295
400
|
"",
|
|
296
401
|
"Files:",
|
|
297
402
|
...files.map((file) => `- ${file.path}: ${file.present ? "present" : "missing"}${file.validJsonl === undefined ? "" : `, jsonl_valid=${file.validJsonl}`}`),
|
|
298
403
|
"",
|
|
404
|
+
"Integration status:",
|
|
405
|
+
...Object.entries(integrationStatus).map(([key, value]) => `- ${key}: ${value}`),
|
|
406
|
+
"",
|
|
299
407
|
"Findings:",
|
|
300
408
|
...(findings.length > 0 ? findings.map((finding) => `- ${finding}`) : ["- none"]),
|
|
301
409
|
"",
|
|
@@ -304,7 +412,7 @@ export async function brainDoctor(root = process.cwd()) {
|
|
|
304
412
|
].join("\n") + "\n"
|
|
305
413
|
};
|
|
306
414
|
}
|
|
307
|
-
export async function exportBrain(agent, root = process.cwd()) {
|
|
415
|
+
export async function exportBrain(agent, root = process.cwd(), options = {}) {
|
|
308
416
|
await ensureProfile(root);
|
|
309
417
|
const paths = getWorkspacePaths(root);
|
|
310
418
|
const profile = JSON.parse(await fs.readFile(paths.brainProjectProfileFile, "utf8"));
|
|
@@ -315,7 +423,7 @@ export async function exportBrain(agent, root = process.cwd()) {
|
|
|
315
423
|
const rules = await readJsonl(paths.brainRulesFile);
|
|
316
424
|
const freshness = await readJson(paths.brainFreshnessFile).catch(() => null);
|
|
317
425
|
const approvedMemory = await readJsonl(paths.memoryApprovedFile).catch(() => []);
|
|
318
|
-
const base = renderBrainBrief(agent, profile, claims, decisions, gaps, bugs, rules, freshness, approvedMemory);
|
|
426
|
+
const base = renderBrainBrief(agent, profile, claims, decisions, gaps, bugs, rules, freshness, approvedMemory, options);
|
|
319
427
|
const content = agent === "claude" ? `<project_brain>\n${base}\n</project_brain>\n` : base;
|
|
320
428
|
const outputPath = path.join(paths.brainExportsDir, `${agent}.md`);
|
|
321
429
|
await fs.writeFile(outputPath, content, "utf8");
|
|
@@ -328,6 +436,8 @@ export async function exportBrain(agent, root = process.cwd()) {
|
|
|
328
436
|
"SotuRail brain export",
|
|
329
437
|
`agent: ${agent}`,
|
|
330
438
|
`path: ${relativeToRoot(root, outputPath)}`,
|
|
439
|
+
`limit: ${normalizeLimit(options.limit)}`,
|
|
440
|
+
`include_suspect: ${options.includeSuspect === true}`,
|
|
331
441
|
...(agent === "generic" ? [`generic_alias: ${relativeToRoot(root, path.join(paths.brainExportsDir, "agent-brief.md"))}`] : []),
|
|
332
442
|
"review_required: true"
|
|
333
443
|
].join("\n") + "\n"
|
|
@@ -338,19 +448,24 @@ export async function rulesFromBrain(root = process.cwd()) {
|
|
|
338
448
|
const paths = getWorkspacePaths(root);
|
|
339
449
|
const claims = await readJsonl(paths.brainClaimsFile);
|
|
340
450
|
const decisions = await readJsonl(paths.brainDecisionsFile);
|
|
451
|
+
const staleEvents = await readJsonl(paths.brainStaleEventsFile);
|
|
452
|
+
const unsafeClaimIds = new Set(staleEvents.filter((event) => event.status === "suspect" || event.status === "stale").map((event) => event.recordId));
|
|
341
453
|
const now = new Date().toISOString();
|
|
342
454
|
const rules = [];
|
|
343
|
-
for (const claim of claims.filter((item) => item.status
|
|
455
|
+
for (const claim of claims.filter((item) => item.status !== "rejected" && item.status !== "stale" && item.status !== "suspect").slice(0, 25)) {
|
|
456
|
+
if (unsafeClaimIds.has(claim.id))
|
|
457
|
+
continue;
|
|
344
458
|
const ruleText = ruleFromClaim(claim.claim);
|
|
345
459
|
if (!ruleText)
|
|
346
460
|
continue;
|
|
461
|
+
const verified = claim.status === "verified";
|
|
347
462
|
rules.push({
|
|
348
463
|
schemaVersion: "soturail.brain.rule.v1",
|
|
349
464
|
id: makeRailId("rule", `${claim.id}:${ruleText}`),
|
|
350
465
|
rule: ruleText,
|
|
351
466
|
sourceClaimIds: [claim.id],
|
|
352
|
-
enforcement: claim
|
|
353
|
-
status: "active",
|
|
467
|
+
enforcement: ruleEnforcementForClaim(claim),
|
|
468
|
+
status: verified ? "active" : "draft",
|
|
354
469
|
createdAt: now
|
|
355
470
|
});
|
|
356
471
|
}
|
|
@@ -369,22 +484,26 @@ export async function rulesFromBrain(root = process.cwd()) {
|
|
|
369
484
|
await appendRecordsIfNew(paths.brainRulesFile, rules);
|
|
370
485
|
await fs.mkdir(paths.rulesDir, { recursive: true });
|
|
371
486
|
const markdownPath = path.join(paths.rulesDir, "from-brain.md");
|
|
487
|
+
const jsonPath = path.join(paths.rulesDir, "from-brain.json");
|
|
372
488
|
await fs.writeFile(markdownPath, [
|
|
373
489
|
"# Brain-Derived Rules",
|
|
374
490
|
"",
|
|
375
|
-
"These rules are derived from verified Project Brain claims and active decisions.
|
|
491
|
+
"These rules are derived from verified Project Brain claims and active decisions. Suspect or stale claims are excluded from active rules by default.",
|
|
376
492
|
"",
|
|
377
|
-
...rules.map((rule) => `- ${rule.rule} (sources: ${[...rule.sourceClaimIds, ...(rule.sourceDecisionIds ?? [])].join(", ") || "none"})`),
|
|
493
|
+
...rules.map((rule) => `- [${rule.status}/${rule.enforcement}] ${rule.rule} (sources: ${[...rule.sourceClaimIds, ...(rule.sourceDecisionIds ?? [])].join(", ") || "none"})`),
|
|
378
494
|
""
|
|
379
495
|
].join("\n"), "utf8");
|
|
496
|
+
await writeJson(jsonPath, { schemaVersion: "soturail.rules.from-brain.v1", createdAt: now, rules });
|
|
380
497
|
return {
|
|
381
498
|
rules,
|
|
382
499
|
markdownPath,
|
|
500
|
+
jsonPath,
|
|
383
501
|
output: [
|
|
384
502
|
"SotuRail rules from-brain",
|
|
385
503
|
`rules_written: ${rules.length}`,
|
|
386
504
|
`jsonl: ${relativeToRoot(root, paths.brainRulesFile)}`,
|
|
387
505
|
`markdown: ${relativeToRoot(root, markdownPath)}`,
|
|
506
|
+
`json: ${relativeToRoot(root, jsonPath)}`,
|
|
388
507
|
"review_required: true"
|
|
389
508
|
].join("\n") + "\n"
|
|
390
509
|
};
|
|
@@ -468,7 +587,7 @@ async function brainCounts(root) {
|
|
|
468
587
|
gaps: gaps.filter((gap) => gap.status === "open").length,
|
|
469
588
|
rules: (await readJsonl(paths.brainRulesFile)).length,
|
|
470
589
|
staleEvents: staleEvents.length,
|
|
471
|
-
suspectOrStale: claims.filter((claim) => claim.status === "suspect" || claim.status === "stale").length + staleEvents.filter((event) => event.status
|
|
590
|
+
suspectOrStale: claims.filter((claim) => claim.status === "suspect" || claim.status === "stale").length + staleEvents.filter((event) => event.status === "suspect" || event.status === "stale").length
|
|
472
591
|
};
|
|
473
592
|
}
|
|
474
593
|
function defaultProfile(root, counts, now) {
|
|
@@ -508,7 +627,9 @@ function defaultArchitecture(root, now) {
|
|
|
508
627
|
".soturail/brain/architecture.json",
|
|
509
628
|
".soturail/brain/brain-index.json",
|
|
510
629
|
".soturail/brain/freshness.json",
|
|
511
|
-
".soturail/brain/doctor.json"
|
|
630
|
+
".soturail/brain/doctor.json",
|
|
631
|
+
".soturail/brain/consolidation-report.json",
|
|
632
|
+
".soturail/brain/stale-repair-plan.json"
|
|
512
633
|
],
|
|
513
634
|
exports: [relativeToRoot(root, path.join(getWorkspacePaths(root).brainExportsDir, "agent-brief.md"))]
|
|
514
635
|
},
|
|
@@ -546,18 +667,252 @@ async function sourceEvidence(root, relativePath, needle, forcedRange) {
|
|
|
546
667
|
rangeHash: `sha256-${sha256Text(rangeText)}`
|
|
547
668
|
};
|
|
548
669
|
}
|
|
549
|
-
function staleEvent(recordId, reason, previousHash, currentHash, status) {
|
|
670
|
+
function staleEvent(recordId, reason, previousHash, currentHash, status, metadata = {}) {
|
|
550
671
|
return {
|
|
551
672
|
schemaVersion: "soturail.brain.stale-event.v1",
|
|
552
|
-
id: makeRailId("stale", `${recordId}:${reason}:${previousHash}:${currentHash}`),
|
|
673
|
+
id: makeRailId("stale", `${recordId}:${reason}:${previousHash}:${currentHash}:${metadata.candidateRange?.startLine ?? ""}`),
|
|
553
674
|
recordId,
|
|
554
675
|
reason,
|
|
555
676
|
previousHash,
|
|
556
677
|
currentHash,
|
|
557
678
|
status,
|
|
679
|
+
...metadata,
|
|
558
680
|
createdAt: new Date().toISOString()
|
|
559
681
|
};
|
|
560
682
|
}
|
|
683
|
+
async function relocateSourceRange(root, claim) {
|
|
684
|
+
const absolute = path.resolve(root, claim.sourcePath);
|
|
685
|
+
const raw = await fs.readFile(absolute, "utf8").catch(() => "");
|
|
686
|
+
if (!raw)
|
|
687
|
+
return { status: "missing", similarity: 0, rangeHash: "sha256-missing" };
|
|
688
|
+
const lines = raw.split(/\r?\n/);
|
|
689
|
+
const windowSize = Math.max(1, Math.min(12, claim.sourceRange.endLine - claim.sourceRange.startLine + 1));
|
|
690
|
+
const queryTokens = tokenSet(`${claim.claim} ${claim.kind} ${claim.tags.join(" ")}`);
|
|
691
|
+
const preferredStart = Math.max(0, claim.sourceRange.startLine - 1);
|
|
692
|
+
let best = null;
|
|
693
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
694
|
+
const window = lines.slice(index, Math.min(lines.length, index + windowSize)).join("\n");
|
|
695
|
+
const similarity = sourceSimilarity(queryTokens, window, claim.claim, index, preferredStart);
|
|
696
|
+
if (!best || similarity > best.similarity) {
|
|
697
|
+
best = { range: { startLine: index + 1, endLine: Math.min(lines.length, index + windowSize) }, similarity, text: window };
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (!best)
|
|
701
|
+
return { status: "missing", similarity: 0, rangeHash: "sha256-missing" };
|
|
702
|
+
const rounded = Number(best.similarity.toFixed(3));
|
|
703
|
+
if (rounded >= 0.62) {
|
|
704
|
+
return { status: "relocated", range: best.range, similarity: rounded, rangeHash: `sha256-${sha256Text(best.text)}` };
|
|
705
|
+
}
|
|
706
|
+
if (rounded >= 0.35) {
|
|
707
|
+
return { status: "candidate", range: best.range, similarity: rounded, rangeHash: `sha256-${sha256Text(best.text)}` };
|
|
708
|
+
}
|
|
709
|
+
return { status: "missing", similarity: rounded, rangeHash: `sha256-${sha256Text(best.text)}` };
|
|
710
|
+
}
|
|
711
|
+
function sourceSimilarity(queryTokens, window, claim, index, preferredStart) {
|
|
712
|
+
const windowTokens = tokenSet(window);
|
|
713
|
+
if (windowTokens.size === 0)
|
|
714
|
+
return 0;
|
|
715
|
+
const overlap = [...queryTokens].filter((token) => windowTokens.has(token)).length;
|
|
716
|
+
const union = new Set([...queryTokens, ...windowTokens]).size || 1;
|
|
717
|
+
const jaccard = overlap / union;
|
|
718
|
+
const claimWords = [...tokenSet(claim)].slice(0, 8);
|
|
719
|
+
const phraseBoost = claimWords.length > 0 && claimWords.every((word) => windowTokens.has(word)) ? 0.28 : 0;
|
|
720
|
+
const nearbyBoost = Math.abs(index - preferredStart) <= 8 ? 0.08 : 0;
|
|
721
|
+
return Math.min(1, jaccard + phraseBoost + nearbyBoost);
|
|
722
|
+
}
|
|
723
|
+
function tokenSet(value) {
|
|
724
|
+
return new Set(value.toLowerCase().replace(/[^a-z0-9_./-]+/g, " ").split(/\s+/).filter((token) => token.length > 2));
|
|
725
|
+
}
|
|
726
|
+
async function writeRepairPlan(root, claims, events) {
|
|
727
|
+
const paths = getWorkspacePaths(root);
|
|
728
|
+
const claimById = new Map(claims.map((claim) => [claim.id, claim]));
|
|
729
|
+
const suggestions = events
|
|
730
|
+
.filter((event) => event.status === "suspect" || event.status === "stale" || event.status === "relocated")
|
|
731
|
+
.map((event) => {
|
|
732
|
+
const claim = claimById.get(event.recordId);
|
|
733
|
+
const sourcePath = claim?.sourcePath ?? "unknown";
|
|
734
|
+
const text = claim?.claim ?? event.recordId;
|
|
735
|
+
const relatedValidation = claim?.validatedBy ?? [];
|
|
736
|
+
const affectedRange = event.previousRange ?? claim?.sourceRange;
|
|
737
|
+
const action = event.status === "relocated"
|
|
738
|
+
? "Confirm the relocated range still proves the claim, then rescan/reverse-claim the source if the new range should become canonical."
|
|
739
|
+
: event.status === "stale"
|
|
740
|
+
? "Restore or replace the missing source evidence, then rerun brain scan and stale checks."
|
|
741
|
+
: "Inspect the source range and validation references before trusting or exporting this claim.";
|
|
742
|
+
const suggestion = {
|
|
743
|
+
recordId: event.recordId,
|
|
744
|
+
type: "claim",
|
|
745
|
+
text,
|
|
746
|
+
reason: event.reason,
|
|
747
|
+
sourcePath,
|
|
748
|
+
recommendedCommand: sourcePath === "unknown" ? "soturail brain scan" : `soturail reverse claims ${sourcePath}`,
|
|
749
|
+
recommendedHumanAction: action,
|
|
750
|
+
relatedValidation
|
|
751
|
+
};
|
|
752
|
+
if (affectedRange)
|
|
753
|
+
suggestion.affectedRange = affectedRange;
|
|
754
|
+
if (event.candidateRange)
|
|
755
|
+
suggestion.candidateRange = event.candidateRange;
|
|
756
|
+
if (event.similarity !== undefined)
|
|
757
|
+
suggestion.similarity = event.similarity;
|
|
758
|
+
return suggestion;
|
|
759
|
+
});
|
|
760
|
+
const plan = {
|
|
761
|
+
schemaVersion: "soturail.brain.repair-plan.v1",
|
|
762
|
+
createdAt: new Date().toISOString(),
|
|
763
|
+
suggestions
|
|
764
|
+
};
|
|
765
|
+
await writeJson(paths.brainRepairPlanJson, plan);
|
|
766
|
+
await fs.writeFile(paths.brainRepairPlanMd, renderRepairPlan(root, plan), "utf8");
|
|
767
|
+
return plan;
|
|
768
|
+
}
|
|
769
|
+
function renderRepairPlan(root, plan) {
|
|
770
|
+
return [
|
|
771
|
+
"# SotuRail Brain Stale Repair Plan",
|
|
772
|
+
"",
|
|
773
|
+
"This plan is guidance only. SotuRail does not auto-edit code, docs or claims.",
|
|
774
|
+
"",
|
|
775
|
+
`createdAt: ${plan.createdAt}`,
|
|
776
|
+
`suggestions: ${plan.suggestions.length}`,
|
|
777
|
+
"",
|
|
778
|
+
...(plan.suggestions.length > 0 ? plan.suggestions.flatMap((item) => [
|
|
779
|
+
`## ${item.recordId}`,
|
|
780
|
+
"",
|
|
781
|
+
`- type: ${item.type}`,
|
|
782
|
+
`- reason: ${item.reason}`,
|
|
783
|
+
`- source: ${item.sourcePath}`,
|
|
784
|
+
`- affected_range: ${formatRange(item.affectedRange)}`,
|
|
785
|
+
`- candidate_range: ${formatRange(item.candidateRange)}`,
|
|
786
|
+
`- similarity: ${item.similarity ?? "n/a"}`,
|
|
787
|
+
`- recommended_command: \`${item.recommendedCommand}\``,
|
|
788
|
+
`- human_action: ${item.recommendedHumanAction}`,
|
|
789
|
+
`- related_validation: ${item.relatedValidation.length > 0 ? item.relatedValidation.join(", ") : "none"}`,
|
|
790
|
+
"",
|
|
791
|
+
summarizeText(item.text, 240),
|
|
792
|
+
""
|
|
793
|
+
]) : [
|
|
794
|
+
"No stale, suspect or relocated records need repair guidance.",
|
|
795
|
+
""
|
|
796
|
+
]),
|
|
797
|
+
"## Safe Follow-up",
|
|
798
|
+
"",
|
|
799
|
+
"- Run `soturail brain stale --repair-plan` after source changes.",
|
|
800
|
+
"- Run `soturail reverse claims ./src` after confirming source changes.",
|
|
801
|
+
"- Run `soturail brain consolidate --dry-run` before deriving new rules.",
|
|
802
|
+
"- Run `soturail eval run --suite brain` before release.",
|
|
803
|
+
"",
|
|
804
|
+
`json: ${relativeToRoot(root, getWorkspacePaths(root).brainRepairPlanJson)}`,
|
|
805
|
+
""
|
|
806
|
+
].join("\n");
|
|
807
|
+
}
|
|
808
|
+
function formatRange(range) {
|
|
809
|
+
return range ? `${range.startLine}-${range.endLine}` : "n/a";
|
|
810
|
+
}
|
|
811
|
+
function findDuplicateClaimGroups(claims) {
|
|
812
|
+
const groups = [];
|
|
813
|
+
const consumed = new Set();
|
|
814
|
+
const candidates = claims.filter((claim) => claim.status !== "rejected");
|
|
815
|
+
for (const claim of candidates) {
|
|
816
|
+
if (consumed.has(claim.id))
|
|
817
|
+
continue;
|
|
818
|
+
const matches = candidates.filter((candidate) => candidate.id !== claim.id && !consumed.has(candidate.id) && compatibleClaims(claim, candidate));
|
|
819
|
+
if (matches.length === 0)
|
|
820
|
+
continue;
|
|
821
|
+
const members = [claim, ...matches].sort(compareCanonicalClaim);
|
|
822
|
+
const canonical = members[0] ?? claim;
|
|
823
|
+
for (const member of members)
|
|
824
|
+
consumed.add(member.id);
|
|
825
|
+
const duplicateMembers = members.filter((member) => member.id !== canonical.id);
|
|
826
|
+
groups.push({
|
|
827
|
+
canonicalId: canonical.id,
|
|
828
|
+
duplicateIds: duplicateMembers.map((member) => member.id),
|
|
829
|
+
reason: duplicateMembers.every((member) => normalizeClaimText(member.claim) === normalizeClaimText(canonical.claim))
|
|
830
|
+
? "normalized claim text match"
|
|
831
|
+
: "near-duplicate claim text with compatible kind/source",
|
|
832
|
+
score: Math.max(...duplicateMembers.map((member) => claimSimilarity(canonical, member))),
|
|
833
|
+
claims: members.map((member) => ({
|
|
834
|
+
id: member.id,
|
|
835
|
+
status: member.status,
|
|
836
|
+
confidence: member.confidence,
|
|
837
|
+
sourcePath: member.sourcePath,
|
|
838
|
+
claim: member.claim
|
|
839
|
+
}))
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
return groups.sort((left, right) => left.canonicalId.localeCompare(right.canonicalId));
|
|
843
|
+
}
|
|
844
|
+
function compatibleClaims(left, right) {
|
|
845
|
+
if (left.kind !== right.kind)
|
|
846
|
+
return false;
|
|
847
|
+
if (left.status === "rejected" || right.status === "rejected")
|
|
848
|
+
return false;
|
|
849
|
+
if (left.status === "stale" && right.status === "verified")
|
|
850
|
+
return false;
|
|
851
|
+
if (right.status === "stale" && left.status === "verified")
|
|
852
|
+
return false;
|
|
853
|
+
const normalizedLeft = normalizeClaimText(left.claim);
|
|
854
|
+
const normalizedRight = normalizeClaimText(right.claim);
|
|
855
|
+
if (normalizedLeft === normalizedRight)
|
|
856
|
+
return true;
|
|
857
|
+
if (left.sourcePath !== right.sourcePath && tagOverlap(left.tags, right.tags) === 0)
|
|
858
|
+
return false;
|
|
859
|
+
return claimSimilarity(left, right) >= 0.82;
|
|
860
|
+
}
|
|
861
|
+
function normalizeClaimText(value) {
|
|
862
|
+
return value.toLowerCase().replace(/[`"'.,:;!?()[\]{}]/g, " ").replace(/\s+/g, " ").trim();
|
|
863
|
+
}
|
|
864
|
+
function claimSimilarity(left, right) {
|
|
865
|
+
const leftTokens = tokenSet(left.claim);
|
|
866
|
+
const rightTokens = tokenSet(right.claim);
|
|
867
|
+
const overlap = [...leftTokens].filter((token) => rightTokens.has(token)).length;
|
|
868
|
+
const union = new Set([...leftTokens, ...rightTokens]).size || 1;
|
|
869
|
+
const tagBonus = tagOverlap(left.tags, right.tags) > 0 ? 0.08 : 0;
|
|
870
|
+
const sourceBonus = left.sourcePath === right.sourcePath ? 0.08 : 0;
|
|
871
|
+
return Math.min(1, overlap / union + tagBonus + sourceBonus);
|
|
872
|
+
}
|
|
873
|
+
function tagOverlap(left, right) {
|
|
874
|
+
const rightSet = new Set(right);
|
|
875
|
+
return left.filter((tag) => rightSet.has(tag)).length;
|
|
876
|
+
}
|
|
877
|
+
function compareCanonicalClaim(left, right) {
|
|
878
|
+
return canonicalClaimScore(right) - canonicalClaimScore(left) || left.id.localeCompare(right.id);
|
|
879
|
+
}
|
|
880
|
+
function canonicalClaimScore(claim) {
|
|
881
|
+
const status = claim.status === "verified" ? 8 : claim.status === "unverified" ? 4 : claim.status === "suspect" ? 2 : 0;
|
|
882
|
+
const confidence = claim.confidence === "high" ? 3 : claim.confidence === "medium" ? 1 : 0;
|
|
883
|
+
const validation = claim.validatedBy.length > 0 ? 3 : 0;
|
|
884
|
+
return status + confidence + validation;
|
|
885
|
+
}
|
|
886
|
+
function renderConsolidationReport(root, report) {
|
|
887
|
+
return [
|
|
888
|
+
"# SotuRail Brain Claim Consolidation Report",
|
|
889
|
+
"",
|
|
890
|
+
"This report preserves append-only history. It writes a consolidated view; it does not delete original claim records.",
|
|
891
|
+
"",
|
|
892
|
+
`createdAt: ${report.createdAt}`,
|
|
893
|
+
`dry_run: ${report.dryRun}`,
|
|
894
|
+
`claims_read: ${report.claimsRead}`,
|
|
895
|
+
`duplicate_groups: ${report.duplicateGroups}`,
|
|
896
|
+
`merged_claims: ${report.mergedClaims}`,
|
|
897
|
+
`canonical_claims: ${report.canonicalClaims}`,
|
|
898
|
+
"",
|
|
899
|
+
...(report.groups.length > 0 ? report.groups.flatMap((group) => [
|
|
900
|
+
`## ${group.canonicalId}`,
|
|
901
|
+
"",
|
|
902
|
+
`- reason: ${group.reason}`,
|
|
903
|
+
`- score: ${group.score.toFixed(3)}`,
|
|
904
|
+
`- duplicates: ${group.duplicateIds.join(", ")}`,
|
|
905
|
+
"",
|
|
906
|
+
...group.claims.map((claim) => `- ${claim.id} [${claim.status}/${claim.confidence}] ${claim.claim} (${claim.sourcePath})`),
|
|
907
|
+
""
|
|
908
|
+
]) : [
|
|
909
|
+
"No duplicate claim groups found.",
|
|
910
|
+
""
|
|
911
|
+
]),
|
|
912
|
+
`json: ${relativeToRoot(root, getWorkspacePaths(root).brainConsolidationReportJson)}`,
|
|
913
|
+
""
|
|
914
|
+
].join("\n");
|
|
915
|
+
}
|
|
561
916
|
async function allRecallRecords(root) {
|
|
562
917
|
const paths = getWorkspacePaths(root);
|
|
563
918
|
return [
|
|
@@ -589,13 +944,29 @@ function confidenceScore(confidence) {
|
|
|
589
944
|
return 1;
|
|
590
945
|
return 0;
|
|
591
946
|
}
|
|
592
|
-
function renderBrainBrief(agent, profile, claims, decisions, gaps, bugs, rules, freshness, approvedMemory) {
|
|
947
|
+
function renderBrainBrief(agent, profile, claims, decisions, gaps, bugs, rules, freshness, approvedMemory, options = {}) {
|
|
593
948
|
const capability = getAgentCapability(agent);
|
|
594
|
-
const
|
|
595
|
-
|
|
949
|
+
const limit = normalizeLimit(options.limit);
|
|
950
|
+
const eventByRecord = new Map((freshness?.events ?? []).map((event) => [event.recordId, event]));
|
|
951
|
+
const verifiedClaims = claims.filter((claim) => claim.status === "verified" && !hasUnsafeEvent(eventByRecord.get(claim.id))).slice(0, limit);
|
|
952
|
+
const suspectClaims = claims.filter((claim) => claim.status === "suspect" || eventByRecord.get(claim.id)?.status === "suspect").slice(0, limit);
|
|
953
|
+
const staleClaims = claims.filter((claim) => claim.status === "stale" || eventByRecord.get(claim.id)?.status === "stale").slice(0, limit);
|
|
954
|
+
const verifiedRules = rules.filter((rule) => rule.status === "active" && rule.sourceClaimIds.every((id) => !hasUnsafeEvent(eventByRecord.get(id)))).slice(0, limit);
|
|
955
|
+
const activeDecisions = decisions.filter((decision) => decision.status === "active").slice(0, limit);
|
|
956
|
+
const openGaps = gaps.filter((gap) => gap.status === "open").slice(0, limit);
|
|
957
|
+
const recurringBugs = bugs.slice(0, limit);
|
|
958
|
+
const warnings = [
|
|
959
|
+
...(freshness?.events ?? []).filter((event) => event.status === "suspect" || event.status === "stale").slice(0, limit).map((event) => `- ${event.recordId}: ${event.reason} [${event.status}]`),
|
|
960
|
+
...(options.includeSuspect === true ? suspectClaims.map((claim) => `- ${claim.id}: ${claim.claim} [${claim.status}]`) : [])
|
|
961
|
+
];
|
|
962
|
+
const incomplete = claims.length === 0 || profile.releaseNotesPath === "unknown";
|
|
963
|
+
const hostNote = hostBrainBriefNote(agent);
|
|
964
|
+
const lines = [
|
|
596
965
|
`# SotuRail Project Brain Brief (${agent})`,
|
|
597
966
|
"",
|
|
598
967
|
"This brief is evidence-backed local context. Do not overclaim beyond the sources listed here.",
|
|
968
|
+
hostNote,
|
|
969
|
+
...(incomplete ? ["", "Data completeness: incomplete brain data. Run `soturail brain scan` and `soturail brain stale --repair-plan`."] : []),
|
|
599
970
|
"",
|
|
600
971
|
"## Project Identity",
|
|
601
972
|
"",
|
|
@@ -605,34 +976,60 @@ function renderBrainBrief(agent, profile, claims, decisions, gaps, bugs, rules,
|
|
|
605
976
|
`- identity: ${profile.identity}`,
|
|
606
977
|
`- recommended_payloads: ${capability.recommendedPayloads.join(" + ")}`,
|
|
607
978
|
"",
|
|
979
|
+
"## Verified Rules",
|
|
980
|
+
"",
|
|
981
|
+
...nonEmpty(verifiedRules.map((rule) => `- ${rule.rule} (${rule.id}, enforcement: ${rule.enforcement}, sources: ${[...rule.sourceClaimIds, ...(rule.sourceDecisionIds ?? [])].join(", ") || "none"})`), "- none yet"),
|
|
982
|
+
"",
|
|
608
983
|
"## Verified Claims",
|
|
609
984
|
"",
|
|
610
985
|
...(verifiedClaims.length > 0 ? verifiedClaims.map((claim) => `- ${claim.claim} (${claim.id}, source: ${claim.sourcePath}:${claim.sourceRange.startLine})`) : ["- none yet"]),
|
|
611
986
|
"",
|
|
612
987
|
"## Active Rules",
|
|
613
988
|
"",
|
|
614
|
-
...nonEmpty(
|
|
989
|
+
...nonEmpty(verifiedRules.map((rule) => `- ${rule.rule} (sources: ${rule.sourceClaimIds.join(", ") || (rule.sourceDecisionIds ?? []).join(", ") || "none"})`), "- none yet"),
|
|
990
|
+
"",
|
|
991
|
+
"## Current Release Process",
|
|
992
|
+
"",
|
|
993
|
+
...nonEmpty(profile.knownReleaseProcess.slice(0, limit).map((step) => `- ${step}`), "- no release process recorded yet"),
|
|
615
994
|
"",
|
|
616
995
|
"## Active Decisions",
|
|
617
996
|
"",
|
|
618
|
-
...nonEmpty(
|
|
997
|
+
...nonEmpty(activeDecisions.map((decision) => `- ${decision.title}: ${decision.decision} (source: ${decision.sourcePath})`), "- none yet"),
|
|
619
998
|
"",
|
|
620
999
|
"## Known Gaps",
|
|
621
1000
|
"",
|
|
622
|
-
...nonEmpty(
|
|
1001
|
+
...nonEmpty(openGaps.map((gap) => `- [${gap.severity}] ${gap.gap} (source: ${gap.sourcePath})`), "- none recorded"),
|
|
623
1002
|
"",
|
|
624
|
-
"## Recurring Bugs
|
|
1003
|
+
"## Recurring Bugs / Harness Patterns",
|
|
625
1004
|
"",
|
|
626
|
-
...nonEmpty(
|
|
1005
|
+
...nonEmpty(recurringBugs.map((bug) => `- ${bug.bug} (source: ${bug.sourcePath})`), "- none recorded"),
|
|
1006
|
+
"",
|
|
1007
|
+
"## Suspect Claims",
|
|
1008
|
+
"",
|
|
1009
|
+
...nonEmpty(suspectClaims.map((claim) => `- ${claim.claim} (${claim.id}, source: ${claim.sourcePath}:${claim.sourceRange.startLine})`), "- none from latest data"),
|
|
1010
|
+
"",
|
|
1011
|
+
"## Stale Claims",
|
|
1012
|
+
"",
|
|
1013
|
+
...nonEmpty(staleClaims.map((claim) => `- ${claim.claim} (${claim.id}, source: ${claim.sourcePath}:${claim.sourceRange.startLine})`), "- none from latest data"),
|
|
627
1014
|
"",
|
|
628
1015
|
"## Safe Commands",
|
|
629
1016
|
"",
|
|
1017
|
+
"- soturail brain scan",
|
|
1018
|
+
"- soturail brain consolidate --dry-run",
|
|
1019
|
+
"- soturail brain stale --repair-plan",
|
|
630
1020
|
"- soturail brain doctor",
|
|
631
1021
|
"- soturail context budget --explain",
|
|
632
1022
|
"- soturail workflow verify",
|
|
633
1023
|
"- soturail eval run --suite brain",
|
|
634
1024
|
"- npm run release:check",
|
|
635
1025
|
"",
|
|
1026
|
+
"## Critical Commands",
|
|
1027
|
+
"",
|
|
1028
|
+
"- npm run typecheck",
|
|
1029
|
+
"- npm run build",
|
|
1030
|
+
"- npm test",
|
|
1031
|
+
"- npm run release:check",
|
|
1032
|
+
"",
|
|
636
1033
|
"## Release And Workflow Notes",
|
|
637
1034
|
"",
|
|
638
1035
|
`- release_notes_path: ${profile.releaseNotesPath}`,
|
|
@@ -641,13 +1038,32 @@ function renderBrainBrief(agent, profile, claims, decisions, gaps, bugs, rules,
|
|
|
641
1038
|
`- diagram: ${profile.workflowHarnessDiagramEval.diagram ?? "unknown"}`,
|
|
642
1039
|
`- eval: ${profile.workflowHarnessDiagramEval.eval ?? "unknown"}`,
|
|
643
1040
|
"",
|
|
1041
|
+
"## Recovery Pointers",
|
|
1042
|
+
"",
|
|
1043
|
+
"- Brain profile: `.soturail/brain/project-profile.json`",
|
|
1044
|
+
"- Brain freshness: `.soturail/brain/freshness.json`",
|
|
1045
|
+
"- Brain repair plan: `.soturail/brain/stale-repair-plan.md` when generated",
|
|
1046
|
+
"- Workflow evidence: `soturail workflow evidence <id>`",
|
|
1047
|
+
"- Raw logs: `soturail expand <raw_id>` with redaction by default",
|
|
1048
|
+
"",
|
|
644
1049
|
"## Approved Memory Summary",
|
|
645
1050
|
"",
|
|
646
|
-
...(approvedMemory.length > 0 ? approvedMemory.slice(0, 5).map((memory) => `- ${memory.text} (${memory.source})`) : ["- no approved memory exported"]),
|
|
1051
|
+
...(approvedMemory.length > 0 ? approvedMemory.slice(0, Math.min(limit, 5)).map((memory) => `- ${memory.text} (${memory.source})`) : ["- no approved memory exported"]),
|
|
647
1052
|
"",
|
|
648
1053
|
"## Stale Or Suspect Warnings",
|
|
649
1054
|
"",
|
|
650
|
-
...(
|
|
1055
|
+
...(warnings.length > 0 ? warnings : ["- none from latest freshness check"]),
|
|
1056
|
+
"",
|
|
1057
|
+
"## Do Not Do",
|
|
1058
|
+
"",
|
|
1059
|
+
"- Do not treat stale or suspect claims as verified.",
|
|
1060
|
+
"- Do not expose private memory, secrets or raw logs in agent prompts.",
|
|
1061
|
+
"- Do not publish or create releases without release gates.",
|
|
1062
|
+
"- Do not assume Project Brain is a full static analyzer or LLM reviewer.",
|
|
1063
|
+
"",
|
|
1064
|
+
"## Source References",
|
|
1065
|
+
"",
|
|
1066
|
+
...nonEmpty(verifiedClaims.map((claim) => `- ${claim.id}: ${claim.sourcePath}:${claim.sourceRange.startLine}`), "- none yet"),
|
|
651
1067
|
"",
|
|
652
1068
|
"## Safety Notes",
|
|
653
1069
|
"",
|
|
@@ -656,7 +1072,27 @@ function renderBrainBrief(agent, profile, claims, decisions, gaps, bugs, rules,
|
|
|
656
1072
|
"- Brain claims are only as fresh as their source hashes.",
|
|
657
1073
|
"- Generated exports should be reviewed before agent handoff.",
|
|
658
1074
|
""
|
|
659
|
-
]
|
|
1075
|
+
];
|
|
1076
|
+
return lines.join("\n");
|
|
1077
|
+
}
|
|
1078
|
+
function normalizeLimit(limit) {
|
|
1079
|
+
if (typeof limit !== "number" || !Number.isFinite(limit))
|
|
1080
|
+
return 10;
|
|
1081
|
+
return Math.max(1, Math.min(50, Math.trunc(limit ?? 10)));
|
|
1082
|
+
}
|
|
1083
|
+
function hasUnsafeEvent(event) {
|
|
1084
|
+
return event?.status === "suspect" || event?.status === "stale";
|
|
1085
|
+
}
|
|
1086
|
+
function hostBrainBriefNote(agent) {
|
|
1087
|
+
if (agent === "claude")
|
|
1088
|
+
return "Host formatting: Markdown wrapped in XML-like tags for Claude Code prompt boundaries.";
|
|
1089
|
+
if (agent === "codex")
|
|
1090
|
+
return "Host formatting: AGENTS.md-friendly Markdown with source references and safe commands.";
|
|
1091
|
+
if (agent === "gemini")
|
|
1092
|
+
return "Host formatting: Markdown sections suitable for larger-context review.";
|
|
1093
|
+
if (agent === "cursor")
|
|
1094
|
+
return "Host formatting: short rules-friendly sections for project rules/context handoff.";
|
|
1095
|
+
return "Host formatting: clean Markdown for generic agents.";
|
|
660
1096
|
}
|
|
661
1097
|
function ruleFromClaim(claim) {
|
|
662
1098
|
if (/release notes live under docs\/releases/i.test(claim))
|
|
@@ -671,6 +1107,15 @@ function ruleFromClaim(claim) {
|
|
|
671
1107
|
return "Do not treat diagram validation as a full Mermaid parser.";
|
|
672
1108
|
return `Preserve evidence for: ${claim.replace(/\.$/, "")}.`;
|
|
673
1109
|
}
|
|
1110
|
+
function ruleEnforcementForClaim(claim) {
|
|
1111
|
+
if (claim.status !== "verified")
|
|
1112
|
+
return "advisory";
|
|
1113
|
+
if (claim.validatedBy.length > 0)
|
|
1114
|
+
return "test-backed";
|
|
1115
|
+
if (claim.kind === "release" || claim.kind === "security" || claim.kind === "policy")
|
|
1116
|
+
return "policy-gate";
|
|
1117
|
+
return "manual-review";
|
|
1118
|
+
}
|
|
674
1119
|
function nonEmpty(values, fallback) {
|
|
675
1120
|
return values.length > 0 ? values : [fallback];
|
|
676
1121
|
}
|