soturail 0.7.0 → 0.8.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.
Files changed (48) hide show
  1. package/README.md +38 -3
  2. package/dist/cli.js +4 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/brain.d.ts +2 -0
  5. package/dist/commands/brain.js +32 -0
  6. package/dist/commands/brain.js.map +1 -0
  7. package/dist/commands/eval.js +9 -4
  8. package/dist/commands/eval.js.map +1 -1
  9. package/dist/commands/reverse.d.ts +2 -0
  10. package/dist/commands/reverse.js +20 -0
  11. package/dist/commands/reverse.js.map +1 -0
  12. package/dist/commands/rules.d.ts +1 -0
  13. package/dist/commands/rules.js +24 -0
  14. package/dist/commands/rules.js.map +1 -1
  15. package/dist/core/agent-exporter.js +1 -0
  16. package/dist/core/agent-exporter.js.map +1 -1
  17. package/dist/core/agent-runtime.js +5 -2
  18. package/dist/core/agent-runtime.js.map +1 -1
  19. package/dist/core/config.d.ts +16 -0
  20. package/dist/core/config.js +27 -2
  21. package/dist/core/config.js.map +1 -1
  22. package/dist/core/evaluation-suite.d.ts +7 -3
  23. package/dist/core/evaluation-suite.js +137 -13
  24. package/dist/core/evaluation-suite.js.map +1 -1
  25. package/dist/core/evidence-pack.js +9 -0
  26. package/dist/core/evidence-pack.js.map +1 -1
  27. package/dist/core/project-brain.d.ts +202 -0
  28. package/dist/core/project-brain.js +790 -0
  29. package/dist/core/project-brain.js.map +1 -0
  30. package/dist/core/reverse-specification.d.ts +34 -0
  31. package/dist/core/reverse-specification.js +361 -0
  32. package/dist/core/reverse-specification.js.map +1 -0
  33. package/dist/core/version.d.ts +1 -1
  34. package/dist/core/version.js +1 -1
  35. package/docs/agents.md +18 -0
  36. package/docs/diagram-rail.md +8 -0
  37. package/docs/evaluation-suite.md +20 -0
  38. package/docs/future-rails-index.md +5 -5
  39. package/docs/harness-rail.md +14 -0
  40. package/docs/knowledge-to-rules.md +37 -14
  41. package/docs/memory-rail.md +11 -0
  42. package/docs/project-brain.md +92 -0
  43. package/docs/releases/README.md +1 -0
  44. package/docs/releases/RELEASE_NOTES_v0.8.0.md +44 -0
  45. package/docs/reverse-specification-rail.md +73 -0
  46. package/docs/rules.md +18 -0
  47. package/docs/workflow-rail.md +15 -0
  48. package/package.json +1 -1
@@ -0,0 +1,790 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { appendJsonl, ensureWorkspace, getWorkspacePaths, readJsonl, relativeToRoot, writeJson } from "./config.js";
6
+ import { getAgentCapability, listAgentCapabilities } from "./agent-runtime.js";
7
+ import { keywordScore, makeRailId, sha256Text, summarizeText } from "./rail-utils.js";
8
+ import { SOTURAIL_VERSION } from "./version.js";
9
+ const execFileAsync = promisify(execFile);
10
+ export async function initBrain(root = process.cwd()) {
11
+ await ensureWorkspace(root);
12
+ const paths = getWorkspacePaths(root);
13
+ await Promise.all([
14
+ fs.mkdir(paths.brainDir, { recursive: true }),
15
+ fs.mkdir(paths.brainExportsDir, { recursive: true }),
16
+ fs.mkdir(paths.brainSpecsDir, { recursive: true })
17
+ ]);
18
+ await Promise.all([
19
+ writeFileIfMissing(paths.brainClaimsFile, ""),
20
+ writeFileIfMissing(paths.brainDecisionsFile, ""),
21
+ writeFileIfMissing(paths.brainBugsFile, ""),
22
+ writeFileIfMissing(paths.brainGapsFile, ""),
23
+ writeFileIfMissing(paths.brainRulesFile, ""),
24
+ writeFileIfMissing(paths.brainStaleEventsFile, "")
25
+ ]);
26
+ const now = new Date().toISOString();
27
+ const counts = await brainCounts(root);
28
+ await writeJsonIfMissing(paths.brainProjectProfileFile, defaultProfile(root, counts, now));
29
+ await writeJsonIfMissing(paths.brainArchitectureFile, defaultArchitecture(root, now));
30
+ await writeJsonIfMissing(paths.brainIndexFile, { schemaVersion: "soturail.brain.index.v1", updatedAt: now, claims: [], decisions: [], gaps: [], rules: [] });
31
+ await writeJsonIfMissing(paths.brainFreshnessFile, { schemaVersion: "soturail.brain.freshness.v1", updatedAt: now, checkedRecords: 0, suspect: 0, stale: 0, warnings: [], events: [] });
32
+ const createdPaths = [
33
+ paths.brainDir,
34
+ paths.brainExportsDir,
35
+ paths.brainClaimsFile,
36
+ paths.brainDecisionsFile,
37
+ paths.brainBugsFile,
38
+ paths.brainGapsFile,
39
+ paths.brainRulesFile,
40
+ paths.brainStaleEventsFile,
41
+ paths.brainProjectProfileFile,
42
+ paths.brainArchitectureFile,
43
+ paths.brainIndexFile,
44
+ paths.brainFreshnessFile
45
+ ].map((filePath) => relativeToRoot(root, filePath));
46
+ return {
47
+ paths: createdPaths,
48
+ output: [
49
+ "SotuRail brain init",
50
+ `brain_dir: ${relativeToRoot(root, paths.brainDir)}`,
51
+ `exports_dir: ${relativeToRoot(root, paths.brainExportsDir)}`,
52
+ `claims: ${relativeToRoot(root, paths.brainClaimsFile)}`,
53
+ `profile: ${relativeToRoot(root, paths.brainProjectProfileFile)}`,
54
+ "next_commands:",
55
+ "- soturail brain scan",
56
+ "- soturail brain profile",
57
+ "- soturail brain doctor",
58
+ "- soturail brain export --agent codex"
59
+ ].join("\n") + "\n"
60
+ };
61
+ }
62
+ export async function scanBrain(root = process.cwd()) {
63
+ await initBrain(root);
64
+ const now = new Date().toISOString();
65
+ const paths = getWorkspacePaths(root);
66
+ const packageJson = await readPackageJson(root);
67
+ const tests = await listRelativeFiles(path.join(root, "tests"), root, (file) => file.endsWith(".test.ts"));
68
+ const docsFolders = await listDocsFolders(root);
69
+ const commands = await detectCliCommands(root);
70
+ const rails = await detectRails(root);
71
+ const claims = await scanClaims(root, now);
72
+ const added = await appendRecordsIfNew(paths.brainClaimsFile, claims);
73
+ const failures = await readJsonl(paths.harnessFailuresFile);
74
+ await appendRecordsIfNew(paths.brainBugsFile, failures.map((failure) => ({
75
+ schemaVersion: "soturail.brain.bug.v1",
76
+ id: makeRailId("bug", `${failure.id}:${failure.whatFailed}`),
77
+ bug: failure.whatFailed,
78
+ status: "recurring",
79
+ sourcePath: ".soturail/harness/failures.jsonl",
80
+ tags: ["harness", failure.preventionCandidate.replace(/\s+/g, "-")],
81
+ createdAt: now
82
+ })));
83
+ const counts = await brainCounts(root);
84
+ const releaseNotesPath = packageJson.version ? path.join("docs", "releases", `RELEASE_NOTES_v${packageJson.version}.md`) : "unknown";
85
+ const profile = {
86
+ schemaVersion: "soturail.brain.project-profile.v1",
87
+ projectName: packageJson.name ?? path.basename(root),
88
+ packageVersion: packageJson.version ?? "unknown",
89
+ cliVersion: SOTURAIL_VERSION,
90
+ identity: "Local-first Context OS for AI coding agents.",
91
+ mainLanguageRuntime: ["TypeScript", "Node >=20", "optional Rust/native"],
92
+ docsFolders,
93
+ releaseNotesPath,
94
+ rails,
95
+ commands,
96
+ tests,
97
+ workflowHarnessDiagramEval: {
98
+ workflow: await exists(paths.workflowsDir) ? "present" : "missing",
99
+ harness: await exists(paths.harnessDir) ? "present" : "missing",
100
+ diagram: await exists(paths.diagramsDir) ? "present" : "missing",
101
+ eval: await exists(path.join(paths.workspace, "eval", "latest.json")) ? "latest report present" : "no latest report"
102
+ },
103
+ agentHosts: listAgentCapabilities().map((capability) => capability.id),
104
+ knownReleaseProcess: ["npm run typecheck", "npm run build", "npm test", "npm run release:check", "tag/release after checks", "npm publish only after release gates"],
105
+ counts,
106
+ updatedAt: now
107
+ };
108
+ await writeJson(paths.brainProjectProfileFile, profile);
109
+ await writeJson(paths.brainArchitectureFile, defaultArchitecture(root, now));
110
+ await writeBrainIndex(root);
111
+ return {
112
+ profile,
113
+ claimsAdded: added,
114
+ output: [
115
+ "SotuRail brain scan",
116
+ `project: ${profile.projectName}`,
117
+ `package_version: ${profile.packageVersion}`,
118
+ `cli_version: ${profile.cliVersion}`,
119
+ `claims_added: ${added}`,
120
+ `claims_total: ${counts.claims}`,
121
+ `release_notes_path: ${profile.releaseNotesPath}`,
122
+ `rails: ${rails.join(", ")}`,
123
+ `commands_detected: ${commands.length}`,
124
+ `tests_detected: ${tests.length}`,
125
+ "next_commands:",
126
+ "- soturail brain recall \"release notes\"",
127
+ "- soturail rules from-brain",
128
+ "- soturail reverse gaps"
129
+ ].join("\n") + "\n"
130
+ };
131
+ }
132
+ export async function brainProfile(root = process.cwd()) {
133
+ await ensureProfile(root);
134
+ const paths = getWorkspacePaths(root);
135
+ const profile = JSON.parse(await fs.readFile(paths.brainProjectProfileFile, "utf8"));
136
+ const counts = await brainCounts(root);
137
+ return [
138
+ "SotuRail brain profile",
139
+ `project: ${profile.projectName}`,
140
+ `package_version: ${profile.packageVersion}`,
141
+ `cli_version: ${profile.cliVersion}`,
142
+ `identity: ${profile.identity}`,
143
+ `main_runtime: ${profile.mainLanguageRuntime.join(", ")}`,
144
+ `release_notes_path: ${profile.releaseNotesPath}`,
145
+ `rails: ${profile.rails.join(", ")}`,
146
+ `workflow_support: ${profile.workflowHarnessDiagramEval.workflow}`,
147
+ `diagram_support: ${profile.workflowHarnessDiagramEval.diagram}`,
148
+ `eval_support: ${profile.workflowHarnessDiagramEval.eval}`,
149
+ `claims: ${counts.claims}`,
150
+ `gaps: ${counts.gaps}`,
151
+ `suspect_or_stale: ${counts.suspectOrStale}`,
152
+ "next_commands:",
153
+ "- soturail brain stale",
154
+ "- soturail brain export --agent generic",
155
+ "- soturail reverse specs ./src"
156
+ ].join("\n") + "\n";
157
+ }
158
+ export async function recallBrain(query, root = process.cwd(), limit = 8) {
159
+ await ensureProfile(root);
160
+ const records = await allRecallRecords(root);
161
+ const scored = records
162
+ .map((record) => {
163
+ const text = recallText(record);
164
+ const score = keywordScore(query, `${text} ${record.tags.join(" ")}`);
165
+ const exact = text.toLowerCase().includes(query.toLowerCase());
166
+ const statusBoost = statusScore(record.status);
167
+ const confidenceBoost = confidenceScore(record.confidence);
168
+ return {
169
+ ...record,
170
+ score: score.score + statusBoost + confidenceBoost + (exact ? 8 : 0),
171
+ reason: [exact ? "exact phrase" : score.reason, `status ${record.status}`, record.confidence ? `confidence ${record.confidence}` : ""].filter(Boolean).join(", ")
172
+ };
173
+ })
174
+ .filter((record) => record.score > 0)
175
+ .sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))
176
+ .slice(0, limit);
177
+ return [
178
+ "SotuRail brain recall",
179
+ `query: ${query}`,
180
+ `matches_count: ${scored.length}`,
181
+ "",
182
+ ...scored.flatMap((record) => [
183
+ `- ${record.id} [${record.type}] score ${record.score.toFixed(2)}`,
184
+ ` Status/confidence: ${record.status}${record.confidence ? ` / ${record.confidence}` : ""}`,
185
+ ` Source: ${record.sourcePath || "unknown"}`,
186
+ ` Reason: ${record.reason}`,
187
+ ` Text: ${summarizeText(recallText(record), 220)}`,
188
+ ""
189
+ ])
190
+ ].join("\n").trimEnd() + "\n";
191
+ }
192
+ export async function staleBrain(root = process.cwd()) {
193
+ await initBrain(root);
194
+ const paths = getWorkspacePaths(root);
195
+ const claims = await readJsonl(paths.brainClaimsFile);
196
+ const existingEvents = await readJsonl(paths.brainStaleEventsFile);
197
+ const warnings = [];
198
+ const events = [];
199
+ for (const claim of claims) {
200
+ const absolute = path.resolve(root, claim.sourcePath);
201
+ if (!(await exists(absolute))) {
202
+ events.push(staleEvent(claim.id, "source file missing", claim.rangeHash, "missing", "stale"));
203
+ continue;
204
+ }
205
+ const evidence = await sourceEvidence(root, claim.sourcePath, undefined, claim.sourceRange);
206
+ if (claim.rangeHash !== evidence.rangeHash) {
207
+ events.push(staleEvent(claim.id, "rangeHash changed", claim.rangeHash, evidence.rangeHash, "suspect"));
208
+ }
209
+ else if (claim.fileHash !== evidence.fileHash) {
210
+ warnings.push(`${claim.id}: fileHash changed but rangeHash is unchanged`);
211
+ }
212
+ for (const validationPath of claim.validatedBy) {
213
+ if (!(await exists(path.resolve(root, validationPath)))) {
214
+ events.push(staleEvent(claim.id, `validatedBy missing: ${validationPath}`, claim.rangeHash, "missing-validation", "suspect"));
215
+ }
216
+ }
217
+ }
218
+ const newEvents = events.filter((event) => !existingEvents.some((existing) => existing.id === event.id));
219
+ for (const event of newEvents)
220
+ await appendJsonl(paths.brainStaleEventsFile, event);
221
+ const freshness = {
222
+ schemaVersion: "soturail.brain.freshness.v1",
223
+ updatedAt: new Date().toISOString(),
224
+ checkedRecords: claims.length,
225
+ suspect: events.filter((event) => event.status === "suspect").length,
226
+ stale: events.filter((event) => event.status === "stale").length,
227
+ warnings,
228
+ events
229
+ };
230
+ await writeJson(paths.brainFreshnessFile, freshness);
231
+ return {
232
+ freshness,
233
+ output: [
234
+ "SotuRail brain stale",
235
+ `checked_records: ${freshness.checkedRecords}`,
236
+ `new_events: ${newEvents.length}`,
237
+ `suspect: ${freshness.suspect}`,
238
+ `stale: ${freshness.stale}`,
239
+ `warnings: ${freshness.warnings.length}`,
240
+ `freshness: ${relativeToRoot(root, paths.brainFreshnessFile)}`,
241
+ "",
242
+ ...(newEvents.length > 0 ? newEvents.map((event) => `- ${event.recordId}: ${event.reason} [${event.status}]`) : ["No stale evidence drift detected."])
243
+ ].join("\n") + "\n"
244
+ };
245
+ }
246
+ export async function brainDoctor(root = process.cwd()) {
247
+ await initBrain(root);
248
+ const paths = getWorkspacePaths(root);
249
+ const counts = await brainCounts(root);
250
+ const jsonlFiles = [
251
+ paths.brainClaimsFile,
252
+ paths.brainDecisionsFile,
253
+ paths.brainBugsFile,
254
+ paths.brainGapsFile,
255
+ paths.brainRulesFile,
256
+ paths.brainStaleEventsFile
257
+ ];
258
+ const files = [];
259
+ for (const filePath of jsonlFiles) {
260
+ files.push({ path: relativeToRoot(root, filePath), present: await exists(filePath), validJsonl: await validJsonl(filePath) });
261
+ }
262
+ for (const filePath of [paths.brainProjectProfileFile, paths.brainIndexFile, paths.brainFreshnessFile]) {
263
+ files.push({ path: relativeToRoot(root, filePath), present: await exists(filePath) });
264
+ }
265
+ const claims = await readJsonl(paths.brainClaimsFile);
266
+ const findings = [
267
+ ...(claims.some((claim) => !claim.sourcePath) ? ["Some claims are missing source paths."] : []),
268
+ ...(claims.some((claim) => claim.status === "verified" && claim.validatedBy.length === 0) ? ["Some verified claims have no validation references."] : []),
269
+ ...(counts.gaps > 0 ? [`Open gaps recorded: ${counts.gaps}`] : []),
270
+ ...(counts.suspectOrStale > 0 ? [`Suspect or stale records/events: ${counts.suspectOrStale}`] : []),
271
+ ...(await exists(path.join(paths.brainExportsDir, "agent-brief.md")) ? [] : ["Agent brief export is missing."])
272
+ ];
273
+ const report = {
274
+ schemaVersion: "soturail.brain.doctor.v1",
275
+ ok: files.every((file) => file.present && file.validJsonl !== false),
276
+ createdAt: new Date().toISOString(),
277
+ files,
278
+ counts,
279
+ findings,
280
+ nextCommands: ["soturail brain scan", "soturail brain stale", "soturail brain export --agent generic", "soturail rules from-brain"]
281
+ };
282
+ await writeJson(paths.brainDoctorFile, report);
283
+ return {
284
+ report,
285
+ output: [
286
+ "SotuRail brain doctor",
287
+ `ok: ${report.ok}`,
288
+ `claims: ${counts.claims}`,
289
+ `decisions: ${counts.decisions}`,
290
+ `bugs: ${counts.bugs}`,
291
+ `gaps: ${counts.gaps}`,
292
+ `rules: ${counts.rules}`,
293
+ `stale_events: ${counts.staleEvents}`,
294
+ `doctor_json: ${relativeToRoot(root, paths.brainDoctorFile)}`,
295
+ "",
296
+ "Files:",
297
+ ...files.map((file) => `- ${file.path}: ${file.present ? "present" : "missing"}${file.validJsonl === undefined ? "" : `, jsonl_valid=${file.validJsonl}`}`),
298
+ "",
299
+ "Findings:",
300
+ ...(findings.length > 0 ? findings.map((finding) => `- ${finding}`) : ["- none"]),
301
+ "",
302
+ "Next commands:",
303
+ ...report.nextCommands.map((command) => `- ${command}`)
304
+ ].join("\n") + "\n"
305
+ };
306
+ }
307
+ export async function exportBrain(agent, root = process.cwd()) {
308
+ await ensureProfile(root);
309
+ const paths = getWorkspacePaths(root);
310
+ const profile = JSON.parse(await fs.readFile(paths.brainProjectProfileFile, "utf8"));
311
+ const claims = await readJsonl(paths.brainClaimsFile);
312
+ const decisions = await readJsonl(paths.brainDecisionsFile);
313
+ const gaps = await readJsonl(paths.brainGapsFile);
314
+ const bugs = await readJsonl(paths.brainBugsFile);
315
+ const rules = await readJsonl(paths.brainRulesFile);
316
+ const freshness = await readJson(paths.brainFreshnessFile).catch(() => null);
317
+ const approvedMemory = await readJsonl(paths.memoryApprovedFile).catch(() => []);
318
+ const base = renderBrainBrief(agent, profile, claims, decisions, gaps, bugs, rules, freshness, approvedMemory);
319
+ const content = agent === "claude" ? `<project_brain>\n${base}\n</project_brain>\n` : base;
320
+ const outputPath = path.join(paths.brainExportsDir, `${agent}.md`);
321
+ await fs.writeFile(outputPath, content, "utf8");
322
+ if (agent === "generic")
323
+ await fs.writeFile(path.join(paths.brainExportsDir, "agent-brief.md"), content, "utf8");
324
+ return {
325
+ path: outputPath,
326
+ content,
327
+ output: [
328
+ "SotuRail brain export",
329
+ `agent: ${agent}`,
330
+ `path: ${relativeToRoot(root, outputPath)}`,
331
+ ...(agent === "generic" ? [`generic_alias: ${relativeToRoot(root, path.join(paths.brainExportsDir, "agent-brief.md"))}`] : []),
332
+ "review_required: true"
333
+ ].join("\n") + "\n"
334
+ };
335
+ }
336
+ export async function rulesFromBrain(root = process.cwd()) {
337
+ await ensureProfile(root);
338
+ const paths = getWorkspacePaths(root);
339
+ const claims = await readJsonl(paths.brainClaimsFile);
340
+ const decisions = await readJsonl(paths.brainDecisionsFile);
341
+ const now = new Date().toISOString();
342
+ const rules = [];
343
+ for (const claim of claims.filter((item) => item.status === "verified").slice(0, 25)) {
344
+ const ruleText = ruleFromClaim(claim.claim);
345
+ if (!ruleText)
346
+ continue;
347
+ rules.push({
348
+ schemaVersion: "soturail.brain.rule.v1",
349
+ id: makeRailId("rule", `${claim.id}:${ruleText}`),
350
+ rule: ruleText,
351
+ sourceClaimIds: [claim.id],
352
+ enforcement: claim.kind === "release" || claim.kind === "security" || claim.kind === "policy" ? "manual-review" : "advisory",
353
+ status: "active",
354
+ createdAt: now
355
+ });
356
+ }
357
+ for (const decision of decisions.filter((item) => item.status === "active").slice(0, 10)) {
358
+ rules.push({
359
+ schemaVersion: "soturail.brain.rule.v1",
360
+ id: makeRailId("rule", `${decision.id}:${decision.decision}`),
361
+ rule: ruleFromDecision(decision),
362
+ sourceClaimIds: [],
363
+ sourceDecisionIds: [decision.id],
364
+ enforcement: "advisory",
365
+ status: "active",
366
+ createdAt: now
367
+ });
368
+ }
369
+ await appendRecordsIfNew(paths.brainRulesFile, rules);
370
+ await fs.mkdir(paths.rulesDir, { recursive: true });
371
+ const markdownPath = path.join(paths.rulesDir, "from-brain.md");
372
+ await fs.writeFile(markdownPath, [
373
+ "# Brain-Derived Rules",
374
+ "",
375
+ "These rules are derived from verified Project Brain claims and active decisions. Review before using as policy gates.",
376
+ "",
377
+ ...rules.map((rule) => `- ${rule.rule} (sources: ${[...rule.sourceClaimIds, ...(rule.sourceDecisionIds ?? [])].join(", ") || "none"})`),
378
+ ""
379
+ ].join("\n"), "utf8");
380
+ return {
381
+ rules,
382
+ markdownPath,
383
+ output: [
384
+ "SotuRail rules from-brain",
385
+ `rules_written: ${rules.length}`,
386
+ `jsonl: ${relativeToRoot(root, paths.brainRulesFile)}`,
387
+ `markdown: ${relativeToRoot(root, markdownPath)}`,
388
+ "review_required: true"
389
+ ].join("\n") + "\n"
390
+ };
391
+ }
392
+ export async function readBrainCounts(root = process.cwd()) {
393
+ return brainCounts(root);
394
+ }
395
+ export async function brainExportExists(root = process.cwd()) {
396
+ return exists(path.join(getWorkspacePaths(root).brainExportsDir, "agent-brief.md"));
397
+ }
398
+ async function scanClaims(root, now) {
399
+ const commit = await currentCommit(root);
400
+ const packageJson = await readPackageJson(root);
401
+ const claims = [];
402
+ const add = async (claim, kind, sourcePath, tags, validatedBy = []) => {
403
+ const evidence = await sourceEvidence(root, sourcePath, claim.split(/\s+/).slice(0, 4).join(" "));
404
+ claims.push({
405
+ schemaVersion: "soturail.brain.claim.v1",
406
+ id: makeRailId("claim", `${kind}:${claim}:${sourcePath}`),
407
+ claim,
408
+ kind,
409
+ status: evidence.exists ? "verified" : "unverified",
410
+ confidence: evidence.exists ? "high" : "medium",
411
+ sourcePath,
412
+ sourceCommit: commit,
413
+ sourceRange: evidence.sourceRange,
414
+ fileHash: evidence.fileHash,
415
+ rangeHash: evidence.rangeHash,
416
+ validatedBy,
417
+ relatedWorkflowIds: [],
418
+ relatedEvidenceIds: [],
419
+ tags,
420
+ createdAt: now,
421
+ updatedAt: now
422
+ });
423
+ };
424
+ if (packageJson.name)
425
+ await add(`Package name is ${packageJson.name}.`, "architecture", "package.json", ["package", "identity"]);
426
+ if (packageJson.version)
427
+ await add(`Package version is ${packageJson.version}.`, "release", "package.json", ["package", "version"], ["tests/soturail.test.ts"]);
428
+ await add(`CLI version source is ${SOTURAIL_VERSION}.`, "release", "src/core/version.ts", ["cli", "version"], ["tests/soturail.test.ts"]);
429
+ await add("Release notes live under docs/releases/.", "release", "src/core/release-preflight.ts", ["release", "docs"], ["tests/v070.test.ts"]);
430
+ await add("SotuRail keeps native acceleration optional.", "architecture", "package.json", ["native", "optional"]);
431
+ await add("No arbitrary shell execution is exposed through SotuRail MCP by default.", "security", "src/core/agent-runtime.ts", ["mcp", "security"], ["tests/v060.test.ts"]);
432
+ await add("Workflow Rail stores local workflow evidence under .soturail/workflows/.", "workflow", "src/core/workflow-store.ts", ["workflow", "evidence"], ["tests/v070.test.ts"]);
433
+ await add("Diagram Rail uses lightweight Mermaid validation and is not a full Mermaid parser.", "docs", "src/core/diagram-validator.ts", ["diagram", "validation"], ["tests/v070.test.ts"]);
434
+ await add("Evaluation reports are written under .soturail/eval/.", "test", "src/core/evaluation-suite.ts", ["eval", "reports"], ["tests/v061.test.ts"]);
435
+ for (const command of await detectCliCommands(root)) {
436
+ await add(`CLI command '${command}' is registered.`, "command", "src/cli.ts", ["command", command]);
437
+ }
438
+ for (const capability of listAgentCapabilities()) {
439
+ await add(`Agent host '${capability.id}' supports ${capability.promptOnlyFallback} prompt-only fallback.`, "agent", "src/core/agent-runtime.ts", ["agent", capability.id]);
440
+ }
441
+ return claims;
442
+ }
443
+ async function writeBrainIndex(root) {
444
+ const paths = getWorkspacePaths(root);
445
+ const claims = await readJsonl(paths.brainClaimsFile);
446
+ const decisions = await readJsonl(paths.brainDecisionsFile);
447
+ const gaps = await readJsonl(paths.brainGapsFile);
448
+ const rules = await readJsonl(paths.brainRulesFile);
449
+ const index = {
450
+ schemaVersion: "soturail.brain.index.v1",
451
+ updatedAt: new Date().toISOString(),
452
+ claims: claims.map((claim) => ({ id: claim.id, kind: claim.kind, status: claim.status, sourcePath: claim.sourcePath, tags: claim.tags })),
453
+ decisions: decisions.map((decision) => ({ id: decision.id, status: decision.status, sourcePath: decision.sourcePath, tags: decision.tags })),
454
+ gaps: gaps.map((gap) => ({ id: gap.id, severity: gap.severity, status: gap.status, sourcePath: gap.sourcePath })),
455
+ rules: rules.map((rule) => ({ id: rule.id, status: rule.status, sourceClaimIds: rule.sourceClaimIds }))
456
+ };
457
+ await writeJson(paths.brainIndexFile, index);
458
+ }
459
+ async function brainCounts(root) {
460
+ const paths = getWorkspacePaths(root);
461
+ const claims = await readJsonl(paths.brainClaimsFile);
462
+ const gaps = await readJsonl(paths.brainGapsFile);
463
+ const staleEvents = await readJsonl(paths.brainStaleEventsFile);
464
+ return {
465
+ claims: claims.length,
466
+ decisions: (await readJsonl(paths.brainDecisionsFile)).length,
467
+ bugs: (await readJsonl(paths.brainBugsFile)).length,
468
+ gaps: gaps.filter((gap) => gap.status === "open").length,
469
+ rules: (await readJsonl(paths.brainRulesFile)).length,
470
+ staleEvents: staleEvents.length,
471
+ suspectOrStale: claims.filter((claim) => claim.status === "suspect" || claim.status === "stale").length + staleEvents.filter((event) => event.status !== "resolved").length
472
+ };
473
+ }
474
+ function defaultProfile(root, counts, now) {
475
+ return {
476
+ schemaVersion: "soturail.brain.project-profile.v1",
477
+ projectName: path.basename(root),
478
+ packageVersion: "unknown",
479
+ cliVersion: SOTURAIL_VERSION,
480
+ identity: "Local-first Context OS for AI coding agents.",
481
+ mainLanguageRuntime: ["TypeScript", "Node >=20"],
482
+ docsFolders: [],
483
+ releaseNotesPath: "unknown",
484
+ rails: [],
485
+ commands: [],
486
+ tests: [],
487
+ workflowHarnessDiagramEval: {},
488
+ agentHosts: [],
489
+ knownReleaseProcess: [],
490
+ counts,
491
+ updatedAt: now
492
+ };
493
+ }
494
+ function defaultArchitecture(root, now) {
495
+ return {
496
+ schemaVersion: "soturail.brain.architecture.v1",
497
+ storage: {
498
+ jsonl: [
499
+ ".soturail/brain/claims.jsonl",
500
+ ".soturail/brain/decisions.jsonl",
501
+ ".soturail/brain/bugs.jsonl",
502
+ ".soturail/brain/gaps.jsonl",
503
+ ".soturail/brain/rules.jsonl",
504
+ ".soturail/brain/stale-events.jsonl"
505
+ ],
506
+ jsonViews: [
507
+ ".soturail/brain/project-profile.json",
508
+ ".soturail/brain/architecture.json",
509
+ ".soturail/brain/brain-index.json",
510
+ ".soturail/brain/freshness.json",
511
+ ".soturail/brain/doctor.json"
512
+ ],
513
+ exports: [relativeToRoot(root, path.join(getWorkspacePaths(root).brainExportsDir, "agent-brief.md"))]
514
+ },
515
+ rails: [
516
+ { name: "Memory", evidence: ".soturail/memory/" },
517
+ { name: "Workflow", evidence: ".soturail/workflows/" },
518
+ { name: "Harness", evidence: ".soturail/harness/" },
519
+ { name: "Diagram", evidence: ".soturail/diagrams/ and docs/diagrams/" },
520
+ { name: "Evaluation", evidence: ".soturail/eval/" }
521
+ ],
522
+ boundaries: ["no external LLM calls", "no embeddings required", "no cloud service", "agent-safe exports are reviewed Markdown/tagged context"],
523
+ updatedAt: now
524
+ };
525
+ }
526
+ async function sourceEvidence(root, relativePath, needle, forcedRange) {
527
+ const absolute = path.resolve(root, relativePath);
528
+ const raw = await fs.readFile(absolute, "utf8").catch(() => "");
529
+ if (!raw)
530
+ return { exists: false, sourceRange: { startLine: 1, endLine: 1 }, fileHash: "sha256-missing", rangeHash: "sha256-missing" };
531
+ const lines = raw.split(/\r?\n/);
532
+ let startLine = forcedRange?.startLine ?? 1;
533
+ let endLine = forcedRange?.endLine ?? Math.min(lines.length, 20);
534
+ if (!forcedRange && needle) {
535
+ const index = lines.findIndex((line) => line.toLowerCase().includes(needle.toLowerCase().split(/\s+/)[0] ?? needle.toLowerCase()));
536
+ if (index >= 0) {
537
+ startLine = index + 1;
538
+ endLine = Math.min(lines.length, index + 3);
539
+ }
540
+ }
541
+ const rangeText = lines.slice(Math.max(0, startLine - 1), endLine).join("\n");
542
+ return {
543
+ exists: true,
544
+ sourceRange: { startLine, endLine },
545
+ fileHash: `sha256-${sha256Text(raw)}`,
546
+ rangeHash: `sha256-${sha256Text(rangeText)}`
547
+ };
548
+ }
549
+ function staleEvent(recordId, reason, previousHash, currentHash, status) {
550
+ return {
551
+ schemaVersion: "soturail.brain.stale-event.v1",
552
+ id: makeRailId("stale", `${recordId}:${reason}:${previousHash}:${currentHash}`),
553
+ recordId,
554
+ reason,
555
+ previousHash,
556
+ currentHash,
557
+ status,
558
+ createdAt: new Date().toISOString()
559
+ };
560
+ }
561
+ async function allRecallRecords(root) {
562
+ const paths = getWorkspacePaths(root);
563
+ return [
564
+ ...(await readJsonl(paths.brainClaimsFile)).map((record) => ({ ...record, type: "claim" })),
565
+ ...(await readJsonl(paths.brainDecisionsFile)).map((record) => ({ ...record, type: "decision", confidence: "medium" })),
566
+ ...(await readJsonl(paths.brainBugsFile)).map((record) => ({ ...record, type: "bug", confidence: "medium" })),
567
+ ...(await readJsonl(paths.brainGapsFile)).map((record) => ({ ...record, type: "gap", confidence: "low", tags: ["gap"], sourcePath: record.sourcePath })),
568
+ ...(await readJsonl(paths.brainRulesFile)).map((record) => ({ ...record, type: "rule", confidence: "medium", sourcePath: ".soturail/brain/rules.jsonl", tags: ["rule"] }))
569
+ ];
570
+ }
571
+ function recallText(record) {
572
+ return record.claim ?? record.decision ?? record.bug ?? record.gap ?? record.rule ?? record.title ?? record.id;
573
+ }
574
+ function statusScore(status) {
575
+ if (status === "verified" || status === "active" || status === "recurring")
576
+ return 4;
577
+ if (status === "unverified" || status === "open")
578
+ return 2;
579
+ if (status === "suspect")
580
+ return 0;
581
+ if (status === "stale")
582
+ return -3;
583
+ return 1;
584
+ }
585
+ function confidenceScore(confidence) {
586
+ if (confidence === "high")
587
+ return 3;
588
+ if (confidence === "medium")
589
+ return 1;
590
+ return 0;
591
+ }
592
+ function renderBrainBrief(agent, profile, claims, decisions, gaps, bugs, rules, freshness, approvedMemory) {
593
+ const capability = getAgentCapability(agent);
594
+ const verifiedClaims = claims.filter((claim) => claim.status === "verified").slice(0, 12);
595
+ return [
596
+ `# SotuRail Project Brain Brief (${agent})`,
597
+ "",
598
+ "This brief is evidence-backed local context. Do not overclaim beyond the sources listed here.",
599
+ "",
600
+ "## Project Identity",
601
+ "",
602
+ `- project: ${profile.projectName}`,
603
+ `- package_version: ${profile.packageVersion}`,
604
+ `- cli_version: ${profile.cliVersion}`,
605
+ `- identity: ${profile.identity}`,
606
+ `- recommended_payloads: ${capability.recommendedPayloads.join(" + ")}`,
607
+ "",
608
+ "## Verified Claims",
609
+ "",
610
+ ...(verifiedClaims.length > 0 ? verifiedClaims.map((claim) => `- ${claim.claim} (${claim.id}, source: ${claim.sourcePath}:${claim.sourceRange.startLine})`) : ["- none yet"]),
611
+ "",
612
+ "## Active Rules",
613
+ "",
614
+ ...nonEmpty(rules.filter((rule) => rule.status === "active").slice(0, 12).map((rule) => `- ${rule.rule} (sources: ${rule.sourceClaimIds.join(", ") || (rule.sourceDecisionIds ?? []).join(", ") || "none"})`), "- none yet"),
615
+ "",
616
+ "## Active Decisions",
617
+ "",
618
+ ...nonEmpty(decisions.filter((decision) => decision.status === "active").slice(0, 8).map((decision) => `- ${decision.title}: ${decision.decision} (source: ${decision.sourcePath})`), "- none yet"),
619
+ "",
620
+ "## Known Gaps",
621
+ "",
622
+ ...nonEmpty(gaps.filter((gap) => gap.status === "open").slice(0, 8).map((gap) => `- [${gap.severity}] ${gap.gap} (source: ${gap.sourcePath})`), "- none recorded"),
623
+ "",
624
+ "## Recurring Bugs And Harness Notes",
625
+ "",
626
+ ...nonEmpty(bugs.slice(0, 8).map((bug) => `- ${bug.bug} (source: ${bug.sourcePath})`), "- none recorded"),
627
+ "",
628
+ "## Safe Commands",
629
+ "",
630
+ "- soturail brain doctor",
631
+ "- soturail context budget --explain",
632
+ "- soturail workflow verify",
633
+ "- soturail eval run --suite brain",
634
+ "- npm run release:check",
635
+ "",
636
+ "## Release And Workflow Notes",
637
+ "",
638
+ `- release_notes_path: ${profile.releaseNotesPath}`,
639
+ `- workflow: ${profile.workflowHarnessDiagramEval.workflow ?? "unknown"}`,
640
+ `- harness: ${profile.workflowHarnessDiagramEval.harness ?? "unknown"}`,
641
+ `- diagram: ${profile.workflowHarnessDiagramEval.diagram ?? "unknown"}`,
642
+ `- eval: ${profile.workflowHarnessDiagramEval.eval ?? "unknown"}`,
643
+ "",
644
+ "## Approved Memory Summary",
645
+ "",
646
+ ...(approvedMemory.length > 0 ? approvedMemory.slice(0, 5).map((memory) => `- ${memory.text} (${memory.source})`) : ["- no approved memory exported"]),
647
+ "",
648
+ "## Stale Or Suspect Warnings",
649
+ "",
650
+ ...(freshness && (freshness.suspect > 0 || freshness.stale > 0) ? freshness.events.slice(0, 8).map((event) => `- ${event.recordId}: ${event.reason} [${event.status}]`) : ["- none from latest freshness check"]),
651
+ "",
652
+ "## Safety Notes",
653
+ "",
654
+ "- Keep secrets local and redacted.",
655
+ "- Raw logs need explicit recovery and review.",
656
+ "- Brain claims are only as fresh as their source hashes.",
657
+ "- Generated exports should be reviewed before agent handoff.",
658
+ ""
659
+ ].join("\n");
660
+ }
661
+ function ruleFromClaim(claim) {
662
+ if (/release notes live under docs\/releases/i.test(claim))
663
+ return "Release notes must live under docs/releases/.";
664
+ if (/release|publish|npm|version|tag/i.test(claim))
665
+ return `Verify release evidence for: ${claim.replace(/\.$/, "")}.`;
666
+ if (/arbitrary shell|mcp/i.test(claim))
667
+ return "Do not expose arbitrary shell execution through MCP.";
668
+ if (/native/i.test(claim))
669
+ return "Keep native acceleration optional.";
670
+ if (/diagram|Mermaid/i.test(claim))
671
+ return "Do not treat diagram validation as a full Mermaid parser.";
672
+ return `Preserve evidence for: ${claim.replace(/\.$/, "")}.`;
673
+ }
674
+ function nonEmpty(values, fallback) {
675
+ return values.length > 0 ? values : [fallback];
676
+ }
677
+ function ruleFromDecision(decision) {
678
+ return `Follow active decision: ${decision.title}.`;
679
+ }
680
+ async function readPackageJson(root) {
681
+ const raw = await fs.readFile(path.join(root, "package.json"), "utf8").catch(() => "{}");
682
+ try {
683
+ const parsed = JSON.parse(raw);
684
+ return parsed;
685
+ }
686
+ catch {
687
+ return {};
688
+ }
689
+ }
690
+ async function detectCliCommands(root) {
691
+ const raw = await fs.readFile(path.join(root, "src", "cli.ts"), "utf8").catch(() => "");
692
+ return [...raw.matchAll(/register([A-Z][A-Za-z]+)Command\(program\)/g)]
693
+ .map((match) => (match[1] ?? "").replace(/[A-Z]/g, (letter, index) => `${index === 0 ? "" : "-"}${letter.toLowerCase()}`))
694
+ .filter(Boolean)
695
+ .sort((a, b) => a.localeCompare(b));
696
+ }
697
+ async function detectRails(root) {
698
+ const candidates = [
699
+ ["memory", "src/commands/memory.ts"],
700
+ ["context", "src/commands/context.ts"],
701
+ ["workflow", "src/commands/workflow.ts"],
702
+ ["harness", "src/commands/harness.ts"],
703
+ ["diagram", "src/commands/diagram.ts"],
704
+ ["evaluation", "src/commands/eval.ts"],
705
+ ["agents", "src/commands/agents.ts"],
706
+ ["rules", "src/commands/rules.ts"],
707
+ ["brain", "src/commands/brain.ts"],
708
+ ["reverse", "src/commands/reverse.ts"]
709
+ ];
710
+ const present = [];
711
+ for (const [name, file] of candidates) {
712
+ if (await exists(path.join(root, file)))
713
+ present.push(name);
714
+ }
715
+ return present;
716
+ }
717
+ async function listDocsFolders(root) {
718
+ const docsRoot = path.join(root, "docs");
719
+ const entries = await fs.readdir(docsRoot, { withFileTypes: true }).catch(() => []);
720
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => path.join("docs", entry.name).replace(/\\/g, "/")).sort();
721
+ }
722
+ async function listRelativeFiles(dir, root, predicate) {
723
+ const output = [];
724
+ async function visit(current) {
725
+ const entries = await fs.readdir(current, { withFileTypes: true }).catch(() => []);
726
+ for (const entry of entries) {
727
+ const absolute = path.join(current, entry.name);
728
+ if (entry.isDirectory()) {
729
+ if (!["node_modules", ".git", "dist", ".soturail"].includes(entry.name))
730
+ await visit(absolute);
731
+ continue;
732
+ }
733
+ if (entry.isFile() && predicate(absolute))
734
+ output.push(relativeToRoot(root, absolute).replace(/\\/g, "/"));
735
+ }
736
+ }
737
+ await visit(dir);
738
+ return output.sort((a, b) => a.localeCompare(b));
739
+ }
740
+ async function currentCommit(root) {
741
+ try {
742
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--short", "HEAD"], { cwd: root, timeout: 3000, windowsHide: true });
743
+ return stdout.trim() || "unknown";
744
+ }
745
+ catch {
746
+ return "unknown";
747
+ }
748
+ }
749
+ async function appendRecordsIfNew(filePath, records) {
750
+ const existing = new Set((await readJsonl(filePath)).map((record) => record.id));
751
+ let added = 0;
752
+ for (const record of records) {
753
+ if (existing.has(record.id))
754
+ continue;
755
+ await appendJsonl(filePath, record);
756
+ existing.add(record.id);
757
+ added += 1;
758
+ }
759
+ return added;
760
+ }
761
+ async function ensureProfile(root) {
762
+ const paths = getWorkspacePaths(root);
763
+ if (!(await exists(paths.brainProjectProfileFile)))
764
+ await scanBrain(root);
765
+ }
766
+ async function readJson(filePath) {
767
+ return JSON.parse(await fs.readFile(filePath, "utf8"));
768
+ }
769
+ async function validJsonl(filePath) {
770
+ try {
771
+ await readJsonl(filePath);
772
+ return true;
773
+ }
774
+ catch {
775
+ return false;
776
+ }
777
+ }
778
+ async function writeFileIfMissing(filePath, content) {
779
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
780
+ if (!(await exists(filePath)))
781
+ await fs.writeFile(filePath, content, "utf8");
782
+ }
783
+ async function writeJsonIfMissing(filePath, value) {
784
+ if (!(await exists(filePath)))
785
+ await writeJson(filePath, value);
786
+ }
787
+ async function exists(filePath) {
788
+ return fs.access(filePath).then(() => true).catch(() => false);
789
+ }
790
+ //# sourceMappingURL=project-brain.js.map