@praneeth_54/agentdoctor 2.1.0 → 3.0.1
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 +86 -3
- package/README.md +490 -290
- package/dist/agent/chat/deterministic.d.ts +6 -0
- package/dist/agent/chat/deterministic.js +180 -0
- package/dist/agent/chat/service.js +8 -14
- package/dist/agent/context/retrieve.d.ts +2 -1
- package/dist/agent/context/retrieve.js +14 -1
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.js +1 -0
- package/dist/agent/loop.d.ts +2 -0
- package/dist/agent/loop.js +31 -0
- package/dist/agent/roles.d.ts +12 -0
- package/dist/agent/roles.js +138 -0
- package/dist/agent/runtime.d.ts +21 -5
- package/dist/agent/runtime.js +141 -28
- package/dist/agent/tools/execute.d.ts +2 -0
- package/dist/agent/tools/execute.js +49 -43
- package/dist/agent/tools/write.d.ts +1 -1
- package/dist/agent/tools/write.js +18 -4
- package/dist/ai/config.js +1 -0
- package/dist/ai/providers/adversarial-local.d.ts +7 -0
- package/dist/ai/providers/adversarial-local.js +146 -0
- package/dist/ai/redact.d.ts +1 -0
- package/dist/ai/redact.js +47 -7
- package/dist/ai/types.d.ts +1 -1
- package/dist/cli/commands/agent.d.ts +2 -0
- package/dist/cli/commands/agent.js +32 -11
- package/dist/cli/commands/architecture.js +7 -2
- package/dist/cli/commands/brain.js +7 -2
- package/dist/cli/commands/chat.js +15 -7
- package/dist/cli/commands/complete.js +13 -3
- package/dist/cli/commands/fix.js +6 -0
- package/dist/cli/commands/learn.js +7 -2
- package/dist/cli/commands/mcp.js +7 -3
- package/dist/cli/commands/platform.js +7 -2
- package/dist/cli/commands/policy-graph-run.js +7 -0
- package/dist/cli/commands/product.d.ts +19 -0
- package/dist/cli/commands/product.js +157 -0
- package/dist/cli/commands/scan.js +6 -0
- package/dist/cli/commands/start.d.ts +15 -0
- package/dist/cli/commands/start.js +86 -0
- package/dist/cli/commands/v2.js +61 -11
- package/dist/cli/commands/verify.js +6 -0
- package/dist/cli/program.js +141 -0
- package/dist/cli/safe-root.d.ts +17 -0
- package/dist/cli/safe-root.js +32 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/brain-cli/service.js +5 -2
- package/dist/core/monorepo/detect.js +10 -0
- package/dist/core/secrets/scan.js +9 -0
- package/dist/core/understanding/brain/storage/store.d.ts +4 -0
- package/dist/core/understanding/brain/storage/store.js +22 -2
- package/dist/dashboard/page.d.ts +2 -0
- package/dist/dashboard/page.js +53 -0
- package/dist/dashboard/server.js +117 -140
- package/dist/dashboard/ui/client.d.ts +2 -0
- package/dist/dashboard/ui/client.js +2 -0
- package/dist/dashboard/ui/styles.d.ts +2 -0
- package/dist/dashboard/ui/styles.js +138 -0
- package/dist/discovery/files.js +13 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/intelligence/graph/build.js +46 -38
- package/dist/intelligence/graph/incremental.d.ts +2 -0
- package/dist/intelligence/graph/incremental.js +9 -0
- package/dist/intelligence/resolve/imports.js +1 -1
- package/dist/languages/dart.d.ts +10 -0
- package/dist/languages/dart.js +99 -0
- package/dist/languages/go.d.ts +13 -7
- package/dist/languages/go.js +84 -24
- package/dist/languages/index.d.ts +5 -1
- package/dist/languages/index.js +13 -36
- package/dist/languages/java.d.ts +10 -0
- package/dist/languages/java.js +80 -0
- package/dist/languages/kotlin.d.ts +10 -0
- package/dist/languages/kotlin.js +85 -0
- package/dist/languages/rust.d.ts +10 -0
- package/dist/languages/rust.js +93 -0
- package/dist/languages/types.d.ts +4 -3
- package/dist/mcp/agent/registry.d.ts +1 -1
- package/dist/mcp/agent/registry.js +109 -23
- package/dist/mcp/intelligence/handlers.d.ts +3 -0
- package/dist/mcp/intelligence/handlers.js +28 -0
- package/dist/mcp/intelligence/path-safety.d.ts +6 -1
- package/dist/mcp/intelligence/path-safety.js +55 -2
- package/dist/mcp/intelligence/registry.d.ts +1 -1
- package/dist/mcp/intelligence/registry.js +30 -1
- package/dist/platform/graph/build.js +9 -0
- package/dist/product/api/doctor.d.ts +14 -0
- package/dist/product/api/doctor.js +185 -0
- package/dist/product/api/openapi.d.ts +7 -0
- package/dist/product/api/openapi.js +122 -0
- package/dist/product/approval/model.d.ts +30 -0
- package/dist/product/approval/model.js +64 -0
- package/dist/product/approval/session.d.ts +49 -0
- package/dist/product/approval/session.js +134 -0
- package/dist/product/database/doctor.d.ts +30 -0
- package/dist/product/database/doctor.js +184 -0
- package/dist/product/decisions/ledger.d.ts +36 -0
- package/dist/product/decisions/ledger.js +141 -0
- package/dist/product/deps/analyze.d.ts +46 -0
- package/dist/product/deps/analyze.js +137 -0
- package/dist/product/deps/lockfiles.d.ts +25 -0
- package/dist/product/deps/lockfiles.js +200 -0
- package/dist/product/discovery/roots.d.ts +52 -0
- package/dist/product/discovery/roots.js +231 -0
- package/dist/product/dna/build.d.ts +50 -0
- package/dist/product/dna/build.js +255 -0
- package/dist/product/eval/lab.d.ts +17 -0
- package/dist/product/eval/lab.js +218 -0
- package/dist/product/events/doctor.d.ts +21 -0
- package/dist/product/events/doctor.js +147 -0
- package/dist/product/evidence-scan.d.ts +12 -0
- package/dist/product/evidence-scan.js +46 -0
- package/dist/product/evolution/timeline.d.ts +29 -0
- package/dist/product/evolution/timeline.js +123 -0
- package/dist/product/features/intelligence.d.ts +23 -0
- package/dist/product/features/intelligence.js +158 -0
- package/dist/product/forensic/mode.d.ts +25 -0
- package/dist/product/forensic/mode.js +70 -0
- package/dist/product/graph/enrich-languages.d.ts +20 -0
- package/dist/product/graph/enrich-languages.js +168 -0
- package/dist/product/health/code-health.d.ts +20 -0
- package/dist/product/health/code-health.js +149 -0
- package/dist/product/index.d.ts +72 -0
- package/dist/product/index.js +36 -0
- package/dist/product/ledger/change-ledger.d.ts +23 -0
- package/dist/product/ledger/change-ledger.js +64 -0
- package/dist/product/map/software-map.d.ts +17 -0
- package/dist/product/map/software-map.js +96 -0
- package/dist/product/memory/institutional.d.ts +16 -0
- package/dist/product/memory/institutional.js +92 -0
- package/dist/product/ops/incident.d.ts +20 -0
- package/dist/product/ops/incident.js +61 -0
- package/dist/product/ops/infra.d.ts +15 -0
- package/dist/product/ops/infra.js +112 -0
- package/dist/product/org/model.d.ts +34 -0
- package/dist/product/org/model.js +195 -0
- package/dist/product/privacy/doctor.d.ts +13 -0
- package/dist/product/privacy/doctor.js +90 -0
- package/dist/product/requirements/trace.d.ts +21 -0
- package/dist/product/requirements/trace.js +166 -0
- package/dist/product/search/index.d.ts +29 -0
- package/dist/product/search/index.js +116 -0
- package/dist/product/search/software-search.d.ts +19 -0
- package/dist/product/search/software-search.js +105 -0
- package/dist/product/security/doctor.d.ts +23 -0
- package/dist/product/security/doctor.js +124 -0
- package/dist/product/self/diagnose.d.ts +15 -0
- package/dist/product/self/diagnose.js +82 -0
- package/dist/product/techdebt/roadmap.d.ts +20 -0
- package/dist/product/techdebt/roadmap.js +118 -0
- package/dist/product/testbrain/analyze.d.ts +29 -0
- package/dist/product/testbrain/analyze.js +129 -0
- package/dist/product/truth.d.ts +12 -0
- package/dist/product/truth.js +30 -0
- package/dist/product/twin/digital-twin.d.ts +23 -0
- package/dist/product/twin/digital-twin.js +52 -0
- package/dist/product/twin/store.d.ts +21 -0
- package/dist/product/twin/store.js +78 -0
- package/dist/product/whatif/engine.d.ts +28 -0
- package/dist/product/whatif/engine.js +91 -0
- package/dist/project/ownership.d.ts +65 -0
- package/dist/project/ownership.js +169 -0
- package/dist/utils/fs.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { EXIT_CODES } from "../../types/index.js";
|
|
2
|
+
import { initBrainStore, rebuildBrain, getBrainStatus } from "../../core/brain-cli/service.js";
|
|
3
|
+
import { discoverProjectRoots } from "../../product/discovery/roots.js";
|
|
4
|
+
import { buildProjectDna, persistProjectDna } from "../../product/dna/build.js";
|
|
5
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
6
|
+
import { resolveCliProjectRoot } from "../safe-root.js";
|
|
7
|
+
function printJson(value) {
|
|
8
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
9
|
+
}
|
|
10
|
+
export async function runStartCommand(options) {
|
|
11
|
+
const prefer = options.select ?? options.root;
|
|
12
|
+
const discovery = await discoverProjectRoots({
|
|
13
|
+
cwd: prefer ? resolveRepoRoot(prefer) : process.cwd(),
|
|
14
|
+
...(prefer ? { prefer: resolveRepoRoot(prefer) } : {}),
|
|
15
|
+
...(options.maxEntries !== undefined ? { maxEntries: options.maxEntries } : {}),
|
|
16
|
+
autoSelect: !options.listOnly,
|
|
17
|
+
});
|
|
18
|
+
if (options.listOnly || discovery.blocked || !discovery.selected) {
|
|
19
|
+
if (options.json) {
|
|
20
|
+
printJson({ discovery, dna: null, brain: null });
|
|
21
|
+
return discovery.blocked ? EXIT_CODES.USAGE_ERROR : EXIT_CODES.SUCCESS;
|
|
22
|
+
}
|
|
23
|
+
if (discovery.blocked) {
|
|
24
|
+
process.stderr.write(`Blocked: ${discovery.blockReason}\n`);
|
|
25
|
+
return EXIT_CODES.USAGE_ERROR;
|
|
26
|
+
}
|
|
27
|
+
process.stdout.write("Project candidates:\n");
|
|
28
|
+
if (discovery.candidates.length === 0) {
|
|
29
|
+
process.stdout.write(" (none found — navigate into a project or pass a path)\n");
|
|
30
|
+
return EXIT_CODES.USAGE_ERROR;
|
|
31
|
+
}
|
|
32
|
+
for (const [i, c] of discovery.candidates.entries()) {
|
|
33
|
+
process.stdout.write(` [${i + 1}] ${c.root}\n score=${c.score} markers=${c.markers.join(", ")} (${c.reason})\n`);
|
|
34
|
+
}
|
|
35
|
+
process.stdout.write("\nRe-run with --select <path> to initialize that project.\n");
|
|
36
|
+
return EXIT_CODES.SUCCESS;
|
|
37
|
+
}
|
|
38
|
+
const root = discovery.selected.root;
|
|
39
|
+
const dna = await persistProjectDna(root);
|
|
40
|
+
let brainStatus = null;
|
|
41
|
+
if (options.initBrain !== false) {
|
|
42
|
+
brainStatus = await initBrainStore(root);
|
|
43
|
+
if (options.rebuildBrain) {
|
|
44
|
+
await rebuildBrain(root);
|
|
45
|
+
brainStatus = await getBrainStatus(root);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const payload = { discovery, dna, brain: brainStatus };
|
|
49
|
+
if (options.json) {
|
|
50
|
+
printJson(payload);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
process.stdout.write(`AgentDoctor start\n`);
|
|
54
|
+
process.stdout.write(` root: ${root}\n`);
|
|
55
|
+
process.stdout.write(` fingerprint: ${dna.fingerprint}\n`);
|
|
56
|
+
process.stdout.write(` type: ${dna.projectType}\n`);
|
|
57
|
+
process.stdout.write(` languages: ${dna.languages.join(", ") || "(none)"}\n`);
|
|
58
|
+
process.stdout.write(` frameworks: ${dna.frameworks.join(", ") || "(none)"}\n`);
|
|
59
|
+
process.stdout.write(` files: ${dna.counts.files} (source ${dna.counts.sourceFiles}, tests ${dna.counts.testFiles})\n`);
|
|
60
|
+
if (brainStatus) {
|
|
61
|
+
process.stdout.write(` brain: ${brainStatus.hasSnapshot ? `snapshot ${brainStatus.latestSnapshotId}` : "initialized (no snapshot yet — run with --rebuild-brain or brain rebuild)"}\n`);
|
|
62
|
+
}
|
|
63
|
+
process.stdout.write(` DNA written to .agentdoctor/dna/project-dna.json\n`);
|
|
64
|
+
}
|
|
65
|
+
return EXIT_CODES.SUCCESS;
|
|
66
|
+
}
|
|
67
|
+
export async function runDnaCommand(options) {
|
|
68
|
+
const gated = await resolveCliProjectRoot(options.root);
|
|
69
|
+
if (!gated.ok) {
|
|
70
|
+
console.error(`Error: ${gated.message}`);
|
|
71
|
+
return gated.code;
|
|
72
|
+
}
|
|
73
|
+
const root = gated.root;
|
|
74
|
+
const dna = options.persist ? await persistProjectDna(root) : await buildProjectDna(root);
|
|
75
|
+
if (options.json) {
|
|
76
|
+
printJson(dna);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
process.stdout.write(`Project DNA: ${dna.name} (${dna.fingerprint})\n`);
|
|
80
|
+
process.stdout.write(` type: ${dna.projectType}\n`);
|
|
81
|
+
process.stdout.write(` languages: ${dna.languages.join(", ") || "(none)"}\n`);
|
|
82
|
+
process.stdout.write(` frameworks: ${dna.frameworks.join(", ") || "(none)"}\n`);
|
|
83
|
+
process.stdout.write(` monorepo: ${dna.monorepo.isMonorepo ? dna.monorepo.tool : "no"}\n`);
|
|
84
|
+
}
|
|
85
|
+
return EXIT_CODES.SUCCESS;
|
|
86
|
+
}
|
package/dist/cli/commands/v2.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EXIT_CODES } from "../../types/index.js";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveCliProjectRoot } from "../safe-root.js";
|
|
3
3
|
import { analyzeChanges } from "../../core/changes/analyze.js";
|
|
4
4
|
import { analyzeContextHealth } from "../../core/context-health/analyze.js";
|
|
5
5
|
import { scanSecrets } from "../../core/secrets/scan.js";
|
|
@@ -16,7 +16,12 @@ function printJson(value) {
|
|
|
16
16
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
17
17
|
}
|
|
18
18
|
export async function runChangesCommand(options) {
|
|
19
|
-
const
|
|
19
|
+
const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
|
|
20
|
+
if (!gated.ok) {
|
|
21
|
+
console.error(`Error: ${gated.message}`);
|
|
22
|
+
return gated.code;
|
|
23
|
+
}
|
|
24
|
+
const root = gated.root;
|
|
20
25
|
const report = await analyzeChanges({
|
|
21
26
|
root,
|
|
22
27
|
...(options.since ? { since: options.since } : {}),
|
|
@@ -38,7 +43,12 @@ export async function runChangesCommand(options) {
|
|
|
38
43
|
return EXIT_CODES.SUCCESS;
|
|
39
44
|
}
|
|
40
45
|
export async function runContextHealthCommand(options) {
|
|
41
|
-
const
|
|
46
|
+
const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
|
|
47
|
+
if (!gated.ok) {
|
|
48
|
+
console.error(`Error: ${gated.message}`);
|
|
49
|
+
return gated.code;
|
|
50
|
+
}
|
|
51
|
+
const root = gated.root;
|
|
42
52
|
const report = await analyzeContextHealth(root);
|
|
43
53
|
if (options.json)
|
|
44
54
|
printJson(report);
|
|
@@ -51,7 +61,12 @@ export async function runContextHealthCommand(options) {
|
|
|
51
61
|
return EXIT_CODES.SUCCESS;
|
|
52
62
|
}
|
|
53
63
|
export async function runSecretsCommand(options) {
|
|
54
|
-
const
|
|
64
|
+
const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
|
|
65
|
+
if (!gated.ok) {
|
|
66
|
+
console.error(`Error: ${gated.message}`);
|
|
67
|
+
return gated.code;
|
|
68
|
+
}
|
|
69
|
+
const root = gated.root;
|
|
55
70
|
const report = await scanSecrets({ root, enabled: true });
|
|
56
71
|
if (options.json)
|
|
57
72
|
printJson(report);
|
|
@@ -67,8 +82,13 @@ export async function runSecretsCommand(options) {
|
|
|
67
82
|
}
|
|
68
83
|
export async function runFixUndoCommand(options) {
|
|
69
84
|
try {
|
|
85
|
+
const gated = await resolveCliProjectRoot(options.root);
|
|
86
|
+
if (!gated.ok) {
|
|
87
|
+
console.error(`Error: ${gated.message}`);
|
|
88
|
+
return gated.code;
|
|
89
|
+
}
|
|
70
90
|
const record = await undoFix({
|
|
71
|
-
root:
|
|
91
|
+
root: gated.root,
|
|
72
92
|
auditId: options.auditId,
|
|
73
93
|
});
|
|
74
94
|
process.stdout.write(`Undid Safe Fix audit ${options.auditId} → undo audit ${record.id}\n`);
|
|
@@ -80,7 +100,12 @@ export async function runFixUndoCommand(options) {
|
|
|
80
100
|
}
|
|
81
101
|
}
|
|
82
102
|
export async function runFixHistoryCommand(options) {
|
|
83
|
-
const
|
|
103
|
+
const gated = await resolveCliProjectRoot(options.root);
|
|
104
|
+
if (!gated.ok) {
|
|
105
|
+
console.error(`Error: ${gated.message}`);
|
|
106
|
+
return gated.code;
|
|
107
|
+
}
|
|
108
|
+
const audits = await listFixAudits(gated.root);
|
|
84
109
|
if (options.json)
|
|
85
110
|
printJson({ audits });
|
|
86
111
|
else if (audits.length === 0)
|
|
@@ -93,7 +118,12 @@ export async function runFixHistoryCommand(options) {
|
|
|
93
118
|
return EXIT_CODES.SUCCESS;
|
|
94
119
|
}
|
|
95
120
|
export async function runBaselineCommand(options) {
|
|
96
|
-
const
|
|
121
|
+
const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
|
|
122
|
+
if (!gated.ok) {
|
|
123
|
+
console.error(`Error: ${gated.message}`);
|
|
124
|
+
return gated.code;
|
|
125
|
+
}
|
|
126
|
+
const root = gated.root;
|
|
97
127
|
try {
|
|
98
128
|
if (options.action === "list") {
|
|
99
129
|
const names = await listNamedBaselines(root);
|
|
@@ -167,7 +197,12 @@ export async function runBaselineCommand(options) {
|
|
|
167
197
|
}
|
|
168
198
|
}
|
|
169
199
|
export async function runPackagesCommand(options) {
|
|
170
|
-
const
|
|
200
|
+
const gated = await resolveCliProjectRoot(options.root ?? process.cwd());
|
|
201
|
+
if (!gated.ok) {
|
|
202
|
+
console.error(`Error: ${gated.message}`);
|
|
203
|
+
return gated.code;
|
|
204
|
+
}
|
|
205
|
+
const root = gated.root;
|
|
171
206
|
const mono = await detectMonorepo(root);
|
|
172
207
|
if (!options.scan) {
|
|
173
208
|
if (options.json)
|
|
@@ -200,8 +235,13 @@ export async function runPackagesCommand(options) {
|
|
|
200
235
|
return EXIT_CODES.SUCCESS;
|
|
201
236
|
}
|
|
202
237
|
export async function runPrReviewCommand(options) {
|
|
238
|
+
const gated = await resolveCliProjectRoot(options.root);
|
|
239
|
+
if (!gated.ok) {
|
|
240
|
+
console.error(`Error: ${gated.message}`);
|
|
241
|
+
return gated.code;
|
|
242
|
+
}
|
|
203
243
|
const report = await analyzePullRequest({
|
|
204
|
-
root:
|
|
244
|
+
root: gated.root,
|
|
205
245
|
dryRun: true,
|
|
206
246
|
...(options.base ? { baseRef: options.base } : {}),
|
|
207
247
|
});
|
|
@@ -215,8 +255,13 @@ export async function runPrReviewCommand(options) {
|
|
|
215
255
|
}
|
|
216
256
|
export async function runDashboardCommand(options) {
|
|
217
257
|
try {
|
|
258
|
+
const gated = await resolveCliProjectRoot(options.root);
|
|
259
|
+
if (!gated.ok) {
|
|
260
|
+
console.error(`Error: ${gated.message}`);
|
|
261
|
+
return gated.code;
|
|
262
|
+
}
|
|
218
263
|
const server = await startDashboardServer({
|
|
219
|
-
root:
|
|
264
|
+
root: gated.root,
|
|
220
265
|
host: options.host ?? "127.0.0.1",
|
|
221
266
|
port: options.port ?? 8787,
|
|
222
267
|
readOnly: true,
|
|
@@ -237,7 +282,12 @@ export async function runDashboardCommand(options) {
|
|
|
237
282
|
}
|
|
238
283
|
}
|
|
239
284
|
export async function runPluginsCommand(options) {
|
|
240
|
-
const
|
|
285
|
+
const gated = await resolveCliProjectRoot(options.root);
|
|
286
|
+
if (!gated.ok) {
|
|
287
|
+
console.error(`Error: ${gated.message}`);
|
|
288
|
+
return gated.code;
|
|
289
|
+
}
|
|
290
|
+
const root = gated.root;
|
|
241
291
|
const plugins = await discoverPlugins(root);
|
|
242
292
|
const analyzerResults = options.run ? await runPluginAnalyzers(root) : undefined;
|
|
243
293
|
if (options.json) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { evaluateVerifyPolicy } from "../../core/policy/evaluate.js";
|
|
2
2
|
import { verify } from "../../core/verify/verify.js";
|
|
3
|
+
import { classifyBroadUserScanRoot } from "../../product/discovery/roots.js";
|
|
3
4
|
import { emitGithubReports } from "../../reporters/github/emit.js";
|
|
4
5
|
import { renderVerifyJsonReport } from "../../reporters/verify/json.js";
|
|
5
6
|
import { renderVerifyTerminalReport } from "../../reporters/verify/terminal.js";
|
|
@@ -15,6 +16,11 @@ export async function runVerifyCommand(options) {
|
|
|
15
16
|
console.error(`Error: not a directory: ${target}`);
|
|
16
17
|
return EXIT_CODES.USAGE_ERROR;
|
|
17
18
|
}
|
|
19
|
+
const broad = classifyBroadUserScanRoot(target);
|
|
20
|
+
if (broad.blocked) {
|
|
21
|
+
console.error(`Error: ${broad.reason}`);
|
|
22
|
+
return EXIT_CODES.USAGE_ERROR;
|
|
23
|
+
}
|
|
18
24
|
try {
|
|
19
25
|
const result = await verify({
|
|
20
26
|
cwd: target,
|
package/dist/cli/program.js
CHANGED
|
@@ -20,6 +20,8 @@ import { runWorkspaceCommand } from "./commands/workspace.js";
|
|
|
20
20
|
import { runAskCommand, runChatCommand } from "./commands/chat.js";
|
|
21
21
|
import { runAgentCommand, runPlanCommand } from "./commands/agent.js";
|
|
22
22
|
import { runLearnCommand } from "./commands/learn.js";
|
|
23
|
+
import { runProductCommand } from "./commands/product.js";
|
|
24
|
+
import { runStartCommand, runDnaCommand } from "./commands/start.js";
|
|
23
25
|
import { collectOpsHealth } from "../ops/health.js";
|
|
24
26
|
import { listSessions, loadSession } from "../platform/sessions/store.js";
|
|
25
27
|
import { exportReports } from "../platform/reports/export.js";
|
|
@@ -1282,6 +1284,7 @@ export function createProgram() {
|
|
|
1282
1284
|
.option("--list-tools", "List available agent tools", false)
|
|
1283
1285
|
.option("--tool <name>", "Run a single tool by name")
|
|
1284
1286
|
.option("--goal <text>", "Build a change plan for this goal")
|
|
1287
|
+
.option("--role <role>", "Specialized role for plan/apply (planner, coder, tester, reviewer, security, …)")
|
|
1285
1288
|
.option("--approve", "Record explicit human approval", false)
|
|
1286
1289
|
.option("--apply", "Apply approved changes (requires --approve)", false)
|
|
1287
1290
|
.option("--apply-ops <json>", "JSON array of {name, arguments} tool ops for --apply")
|
|
@@ -1294,6 +1297,7 @@ export function createProgram() {
|
|
|
1294
1297
|
listTools: Boolean(options.listTools),
|
|
1295
1298
|
...(options.tool !== undefined ? { tool: options.tool } : {}),
|
|
1296
1299
|
...(options.goal !== undefined ? { goal: options.goal } : {}),
|
|
1300
|
+
...(options.role !== undefined ? { role: options.role } : {}),
|
|
1297
1301
|
approve: Boolean(options.approve),
|
|
1298
1302
|
apply: Boolean(options.apply),
|
|
1299
1303
|
...(options.applyOps !== undefined ? { applyOpsJson: options.applyOps } : {}),
|
|
@@ -1543,6 +1547,143 @@ export function createProgram() {
|
|
|
1543
1547
|
const code = await runExplainCommand(rule);
|
|
1544
1548
|
process.exitCode = code;
|
|
1545
1549
|
});
|
|
1550
|
+
function bindProductCommand(name, description, action) {
|
|
1551
|
+
program
|
|
1552
|
+
.command(name)
|
|
1553
|
+
.description(description)
|
|
1554
|
+
.argument("[path]", "Repository path")
|
|
1555
|
+
.option("--json", "Emit JSON report", false)
|
|
1556
|
+
.action(async (pathArg, _options, command) => {
|
|
1557
|
+
const options = command.optsWithGlobals();
|
|
1558
|
+
process.exitCode = await runProductCommand({
|
|
1559
|
+
action,
|
|
1560
|
+
root: resolveTargetArgument(pathArg),
|
|
1561
|
+
json: Boolean(options.json),
|
|
1562
|
+
});
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
program
|
|
1566
|
+
.command("start")
|
|
1567
|
+
.description("Safe project discovery: detect project root, build Project DNA, initialize Project Brain")
|
|
1568
|
+
.argument("[path]", "Optional project path (default: cwd)")
|
|
1569
|
+
.option("--list", "List candidates without initializing", false)
|
|
1570
|
+
.option("--select <path>", "Explicitly select a project root")
|
|
1571
|
+
.option("--max-entries <n>", "Refuse scans larger than this entry budget", "25000")
|
|
1572
|
+
.option("--no-init-brain", "Skip Project Brain store initialization")
|
|
1573
|
+
.option("--rebuild-brain", "Compile Project Brain after init", false)
|
|
1574
|
+
.option("--json", "Emit JSON", false)
|
|
1575
|
+
.action(async (pathArg, options) => {
|
|
1576
|
+
process.exitCode = await runStartCommand({
|
|
1577
|
+
...(pathArg !== undefined ? { root: resolveTargetArgument(pathArg) } : {}),
|
|
1578
|
+
listOnly: Boolean(options.list),
|
|
1579
|
+
...(options.select !== undefined ? { select: options.select } : {}),
|
|
1580
|
+
maxEntries: Number(options.maxEntries ?? 25000),
|
|
1581
|
+
initBrain: options.initBrain !== false,
|
|
1582
|
+
rebuildBrain: Boolean(options.rebuildBrain),
|
|
1583
|
+
json: Boolean(options.json),
|
|
1584
|
+
});
|
|
1585
|
+
});
|
|
1586
|
+
program
|
|
1587
|
+
.command("dna")
|
|
1588
|
+
.description("Build deterministic Project DNA fingerprint")
|
|
1589
|
+
.argument("[path]", "Repository path")
|
|
1590
|
+
.option("--persist", "Write .agentdoctor/dna/project-dna.json", false)
|
|
1591
|
+
.option("--json", "Emit JSON", false)
|
|
1592
|
+
.action(async (pathArg, options) => {
|
|
1593
|
+
process.exitCode = await runDnaCommand({
|
|
1594
|
+
root: resolveTargetArgument(pathArg),
|
|
1595
|
+
persist: Boolean(options.persist),
|
|
1596
|
+
json: Boolean(options.json),
|
|
1597
|
+
});
|
|
1598
|
+
});
|
|
1599
|
+
bindProductCommand("requirements", "Trace requirements from markdown docs (evidence-backed)", "requirements");
|
|
1600
|
+
bindProductCommand("api", "Discover HTTP route patterns (regex evidence)", "api");
|
|
1601
|
+
bindProductCommand("database", "Discover schema objects from migrations/SQL/Prisma", "database");
|
|
1602
|
+
bindProductCommand("events", "Detect queues, workers, cron, and event markers", "events");
|
|
1603
|
+
bindProductCommand("dependency", "Analyze direct dependencies and workspace duplicates", "dependency");
|
|
1604
|
+
bindProductCommand("deps", "Alias for dependency", "dependency");
|
|
1605
|
+
bindProductCommand("health-code", "Aggregate code health indicators with evidence", "health-code");
|
|
1606
|
+
bindProductCommand("code-health", "Alias for health-code", "health-code");
|
|
1607
|
+
bindProductCommand("map", "Build navigable software map from project layout", "map");
|
|
1608
|
+
bindProductCommand("decisions", "Load ADR files and decision ledger", "decisions");
|
|
1609
|
+
bindProductCommand("forensic", "Read-only forensic analysis (git + ledgers + brain)", "forensic");
|
|
1610
|
+
bindProductCommand("twin", "Software digital twin snapshot", "twin");
|
|
1611
|
+
bindProductCommand("eval-lab", "Run evaluation lab fixture checks", "eval-lab");
|
|
1612
|
+
bindProductCommand("self-check", "Self-diagnosis of AgentDoctor installation", "self-check");
|
|
1613
|
+
bindProductCommand("infra", "Detect Docker/K8s/Terraform/CI artifacts", "infra");
|
|
1614
|
+
bindProductCommand("incident", "Build incident timeline hypotheses", "incident");
|
|
1615
|
+
bindProductCommand("security-doctor", "Security heuristics: secrets, auth markers, dangerous patterns", "security-doctor");
|
|
1616
|
+
bindProductCommand("test-brain", "Test Brain: test-impact plus graph file↔test mapping", "test-brain");
|
|
1617
|
+
bindProductCommand("privacy-doctor", "PII-ish pattern scan (not legal compliance)", "privacy-doctor");
|
|
1618
|
+
bindProductCommand("tech-debt", "Technical debt roadmap (TODO/FIXME, architecture, tests)", "tech-debt");
|
|
1619
|
+
bindProductCommand("features", "Feature intelligence (flows + API links)", "features");
|
|
1620
|
+
bindProductCommand("evolution", "Software evolution timeline from git + ledger", "evolution");
|
|
1621
|
+
bindProductCommand("org", "Load organization model (.agentdoctor/org)", "org");
|
|
1622
|
+
program
|
|
1623
|
+
.command("memory")
|
|
1624
|
+
.description("Query institutional memory (brain + ledgers)")
|
|
1625
|
+
.argument("<query>", "Memory query")
|
|
1626
|
+
.argument("[path]", "Repository path")
|
|
1627
|
+
.option("--json", "Emit JSON report", false)
|
|
1628
|
+
.action(async (query, pathArg, _options, command) => {
|
|
1629
|
+
const options = command.optsWithGlobals();
|
|
1630
|
+
process.exitCode = await runProductCommand({
|
|
1631
|
+
action: "memory",
|
|
1632
|
+
query,
|
|
1633
|
+
root: resolveTargetArgument(pathArg),
|
|
1634
|
+
json: Boolean(options.json),
|
|
1635
|
+
});
|
|
1636
|
+
});
|
|
1637
|
+
program
|
|
1638
|
+
.command("search")
|
|
1639
|
+
.description("Search symbols and concepts (brain + bounded file scan)")
|
|
1640
|
+
.argument("<query>", "Search query")
|
|
1641
|
+
.argument("[path]", "Repository path")
|
|
1642
|
+
.option("--json", "Emit JSON report", false)
|
|
1643
|
+
.action(async (query, pathArg, _options, command) => {
|
|
1644
|
+
const options = command.optsWithGlobals();
|
|
1645
|
+
process.exitCode = await runProductCommand({
|
|
1646
|
+
action: "search",
|
|
1647
|
+
query,
|
|
1648
|
+
root: resolveTargetArgument(pathArg),
|
|
1649
|
+
json: Boolean(options.json),
|
|
1650
|
+
});
|
|
1651
|
+
});
|
|
1652
|
+
program
|
|
1653
|
+
.command("role-agent")
|
|
1654
|
+
.description("Run the coding loop with a specialized agent role")
|
|
1655
|
+
.argument("[path]", "Repository path")
|
|
1656
|
+
.requiredOption("--role <role>", "Agent role (planner, coder, tester, reviewer, security, …)")
|
|
1657
|
+
.requiredOption("--goal <text>", "Goal for the role agent")
|
|
1658
|
+
.option("--approve", "Record human approval for writes", false)
|
|
1659
|
+
.option("--apply", "Apply changes (requires --approve)", false)
|
|
1660
|
+
.option("--json", "Emit JSON", false)
|
|
1661
|
+
.action(async (pathArg, options) => {
|
|
1662
|
+
process.exitCode = await runProductCommand({
|
|
1663
|
+
action: "role-agent",
|
|
1664
|
+
root: resolveTargetArgument(pathArg),
|
|
1665
|
+
role: options.role,
|
|
1666
|
+
goal: options.goal,
|
|
1667
|
+
approve: Boolean(options.approve),
|
|
1668
|
+
apply: Boolean(options.apply),
|
|
1669
|
+
json: Boolean(options.json),
|
|
1670
|
+
});
|
|
1671
|
+
});
|
|
1672
|
+
program
|
|
1673
|
+
.command("what-if")
|
|
1674
|
+
.description("Graph-backed change impact for a file or symbol")
|
|
1675
|
+
.argument("<target>", "File path or symbol")
|
|
1676
|
+
.argument("[path]", "Repository path")
|
|
1677
|
+
.option("--json", "Emit JSON report", false)
|
|
1678
|
+
.action(async (target, pathArg, _options, command) => {
|
|
1679
|
+
const options = command.optsWithGlobals();
|
|
1680
|
+
process.exitCode = await runProductCommand({
|
|
1681
|
+
action: "what-if",
|
|
1682
|
+
root: resolveTargetArgument(pathArg),
|
|
1683
|
+
target,
|
|
1684
|
+
json: Boolean(options.json),
|
|
1685
|
+
});
|
|
1686
|
+
});
|
|
1546
1687
|
program
|
|
1547
1688
|
.command("doctor")
|
|
1548
1689
|
.description("Check AgentDoctor installation health")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ExitCode } from "../types/index.js";
|
|
2
|
+
export type CliRootOk = {
|
|
3
|
+
ok: true;
|
|
4
|
+
root: string;
|
|
5
|
+
};
|
|
6
|
+
export type CliRootErr = {
|
|
7
|
+
ok: false;
|
|
8
|
+
message: string;
|
|
9
|
+
code: ExitCode;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a CLI project root and refuse home / Desktop / Downloads / Documents
|
|
13
|
+
* (and similarly broad user folders) before any recursive intelligence work.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveCliProjectRoot(pathArg?: string): Promise<CliRootOk | CliRootErr>;
|
|
16
|
+
/** Sync variant when the path is already known to exist (tests / internal). */
|
|
17
|
+
export declare function assertCliProjectRootSync(pathArg?: string): CliRootOk | CliRootErr;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { classifyBroadUserScanRoot } from "../product/discovery/roots.js";
|
|
2
|
+
import { EXIT_CODES } from "../types/index.js";
|
|
3
|
+
import { isDirectory } from "../utils/fs.js";
|
|
4
|
+
import { resolveRepoRoot } from "../utils/path.js";
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a CLI project root and refuse home / Desktop / Downloads / Documents
|
|
7
|
+
* (and similarly broad user folders) before any recursive intelligence work.
|
|
8
|
+
*/
|
|
9
|
+
export async function resolveCliProjectRoot(pathArg) {
|
|
10
|
+
const root = resolveRepoRoot(pathArg ?? process.cwd());
|
|
11
|
+
if (!(await isDirectory(root))) {
|
|
12
|
+
return {
|
|
13
|
+
ok: false,
|
|
14
|
+
message: `not a directory: ${root}`,
|
|
15
|
+
code: EXIT_CODES.USAGE_ERROR,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const broad = classifyBroadUserScanRoot(root);
|
|
19
|
+
if (broad.blocked) {
|
|
20
|
+
return { ok: false, message: broad.reason, code: EXIT_CODES.USAGE_ERROR };
|
|
21
|
+
}
|
|
22
|
+
return { ok: true, root };
|
|
23
|
+
}
|
|
24
|
+
/** Sync variant when the path is already known to exist (tests / internal). */
|
|
25
|
+
export function assertCliProjectRootSync(pathArg) {
|
|
26
|
+
const root = resolveRepoRoot(pathArg ?? process.cwd());
|
|
27
|
+
const broad = classifyBroadUserScanRoot(root);
|
|
28
|
+
if (broad.blocked) {
|
|
29
|
+
return { ok: false, message: broad.reason, code: EXIT_CODES.USAGE_ERROR };
|
|
30
|
+
}
|
|
31
|
+
return { ok: true, root };
|
|
32
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentId } from "./types/index.js";
|
|
2
|
-
export declare const PACKAGE_VERSION = "
|
|
2
|
+
export declare const PACKAGE_VERSION = "3.0.1";
|
|
3
3
|
export declare const DEFAULT_MAX_FILE_SIZE_BYTES: number;
|
|
4
4
|
/** Directories skipped during normal discovery (unless a rule needs them later). */
|
|
5
5
|
export declare const DEFAULT_IGNORE_DIRECTORIES: Set<string>;
|
package/dist/constants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = "
|
|
1
|
+
export const PACKAGE_VERSION = "3.0.1";
|
|
2
2
|
export const DEFAULT_MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024; // 2 MiB
|
|
3
3
|
/** Directories skipped during normal discovery (unless a rule needs them later). */
|
|
4
4
|
export const DEFAULT_IGNORE_DIRECTORIES = new Set([
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { LocalBrainStore, createBrainQueryEngine, redactBrainForStorage, } from "../understanding/brain/index.js";
|
|
4
4
|
import { compileProjectBrain } from "../../mcp/brain/compile.js";
|
|
5
|
+
import { OWNERSHIP_BOUNDARY_VERSION } from "../../project/ownership.js";
|
|
5
6
|
import { resolveRepoRoot } from "../../utils/path.js";
|
|
6
7
|
import { isDirectory } from "../../utils/fs.js";
|
|
7
8
|
function storeFor(root) {
|
|
@@ -16,11 +17,13 @@ export async function getBrainStatus(root) {
|
|
|
16
17
|
const storeRoot = path.join(resolved, ".agentdoctor", "project-brain");
|
|
17
18
|
try {
|
|
18
19
|
const meta = await store.readMeta();
|
|
20
|
+
const boundaryOk = meta.ownershipBoundaryVersion === OWNERSHIP_BOUNDARY_VERSION;
|
|
21
|
+
const hasUsableLatest = boundaryOk && meta.latestSnapshotId !== null;
|
|
19
22
|
return {
|
|
20
23
|
root: resolved,
|
|
21
24
|
storeRoot,
|
|
22
|
-
hasSnapshot:
|
|
23
|
-
latestSnapshotId: meta.latestSnapshotId,
|
|
25
|
+
hasSnapshot: hasUsableLatest,
|
|
26
|
+
latestSnapshotId: hasUsableLatest ? meta.latestSnapshotId : null,
|
|
24
27
|
snapshotCount: meta.snapshots.length,
|
|
25
28
|
projectName: meta.projectName || null,
|
|
26
29
|
schemaVersion: meta.schemaVersion || null,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { classifyRelativePathOwnership, isUnderNestedRepository } from "../../project/ownership.js";
|
|
3
4
|
import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
|
|
4
5
|
async function readJson(file) {
|
|
5
6
|
try {
|
|
@@ -99,6 +100,15 @@ export async function detectMonorepo(rootInput) {
|
|
|
99
100
|
continue;
|
|
100
101
|
if (seen.has(pkg.absolutePath))
|
|
101
102
|
continue;
|
|
103
|
+
const ownership = classifyRelativePathOwnership(pkg.relativePath);
|
|
104
|
+
if (ownership !== "project_owned") {
|
|
105
|
+
limitations.push(`Skipped non-owned workspace package: ${pkg.relativePath} (${ownership})`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (await isUnderNestedRepository(root, pkg.absolutePath)) {
|
|
109
|
+
limitations.push(`Skipped nested-repository workspace package: ${pkg.relativePath}`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
102
112
|
seen.add(pkg.absolutePath);
|
|
103
113
|
packages.push(pkg);
|
|
104
114
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { decideDirectoryTraversal } from "../../project/ownership.js";
|
|
3
4
|
import { readTextFile } from "../../utils/fs.js";
|
|
4
5
|
import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
|
|
5
6
|
const SECRET_PATTERNS = [
|
|
@@ -94,6 +95,14 @@ async function walkFiles(root, dir, out, limit) {
|
|
|
94
95
|
if (entry.isSymbolicLink())
|
|
95
96
|
continue;
|
|
96
97
|
if (entry.isDirectory()) {
|
|
98
|
+
const relativeDir = toPosixRelative(root, absolute);
|
|
99
|
+
const traversal = await decideDirectoryTraversal({
|
|
100
|
+
projectRoot: root,
|
|
101
|
+
absoluteDir: absolute,
|
|
102
|
+
relativeDir,
|
|
103
|
+
});
|
|
104
|
+
if (!traversal.traverse)
|
|
105
|
+
continue;
|
|
97
106
|
await walkFiles(root, absolute, out, limit);
|
|
98
107
|
continue;
|
|
99
108
|
}
|
|
@@ -9,6 +9,8 @@ export interface SnapshotMeta {
|
|
|
9
9
|
projectName: string;
|
|
10
10
|
brainId: string;
|
|
11
11
|
checksum: string;
|
|
12
|
+
/** Snapshots without this stamp (or with a mismatch) must not be served as current truth. */
|
|
13
|
+
ownershipBoundaryVersion?: number;
|
|
12
14
|
}
|
|
13
15
|
export interface BrainStoreMeta {
|
|
14
16
|
storageFormatVersion: BrainStorageFormatVersion;
|
|
@@ -16,6 +18,8 @@ export interface BrainStoreMeta {
|
|
|
16
18
|
projectName: string;
|
|
17
19
|
latestSnapshotId: string | null;
|
|
18
20
|
snapshots: SnapshotMeta[];
|
|
21
|
+
/** When missing or mismatched, latest brain is treated as stale vs current ownership rules. */
|
|
22
|
+
ownershipBoundaryVersion?: number;
|
|
19
23
|
}
|
|
20
24
|
export interface SnapshotComparison {
|
|
21
25
|
leftId: string;
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { atomicWriteTextFile } from "../../../../utils/fs.js";
|
|
5
|
+
import { OWNERSHIP_BOUNDARY_VERSION } from "../../../../project/ownership.js";
|
|
5
6
|
import { parseBrainDelta, serializeBrainDelta } from "../delta.js";
|
|
6
7
|
import { checkBrainCompatibility, migrateBrain } from "../migrate.js";
|
|
7
8
|
import { assertBrainContract } from "../contract.js";
|
|
@@ -198,6 +199,7 @@ export class LocalBrainStore {
|
|
|
198
199
|
projectName: brain.metadata.projectName,
|
|
199
200
|
brainId: brain.metadata.brainId,
|
|
200
201
|
checksum: nextChecksum,
|
|
202
|
+
ownershipBoundaryVersion: OWNERSHIP_BOUNDARY_VERSION,
|
|
201
203
|
};
|
|
202
204
|
const meta = await this.readMeta();
|
|
203
205
|
const snapshots = meta.snapshots.filter((s) => s.id !== snapshotId);
|
|
@@ -209,10 +211,15 @@ export class LocalBrainStore {
|
|
|
209
211
|
projectName: brain.metadata.projectName,
|
|
210
212
|
latestSnapshotId: snapshotId,
|
|
211
213
|
snapshots,
|
|
214
|
+
ownershipBoundaryVersion: OWNERSHIP_BOUNDARY_VERSION,
|
|
212
215
|
});
|
|
213
216
|
return snapMeta;
|
|
214
217
|
}
|
|
215
218
|
async loadSnapshot(snapshotId) {
|
|
219
|
+
const meta = await this.readMeta();
|
|
220
|
+
if (meta.ownershipBoundaryVersion !== OWNERSHIP_BOUNDARY_VERSION) {
|
|
221
|
+
throw new BrainStorageError(`brain store ownership boundary is stale (have ${String(meta.ownershipBoundaryVersion ?? "missing")}, need ${OWNERSHIP_BOUNDARY_VERSION}); rebuild required`);
|
|
222
|
+
}
|
|
216
223
|
const target = await assertInsideRootResolved(this.root, this.brainPath(snapshotId));
|
|
217
224
|
let raw;
|
|
218
225
|
try {
|
|
@@ -224,9 +231,17 @@ export class LocalBrainStore {
|
|
|
224
231
|
}
|
|
225
232
|
throw error;
|
|
226
233
|
}
|
|
227
|
-
const meta = await this.readMeta();
|
|
228
234
|
const snap = meta.snapshots.find((s) => s.id === snapshotId);
|
|
229
235
|
if (snap) {
|
|
236
|
+
if (snap.ownershipBoundaryVersion !== undefined &&
|
|
237
|
+
snap.ownershipBoundaryVersion !== OWNERSHIP_BOUNDARY_VERSION) {
|
|
238
|
+
throw new BrainStorageError(`snapshot ${snapshotId} ownership boundary is stale (have ${snap.ownershipBoundaryVersion}, need ${OWNERSHIP_BOUNDARY_VERSION}); rebuild required`);
|
|
239
|
+
}
|
|
240
|
+
// Pre-stamp snapshots created before ownershipBoundaryVersion was recorded are
|
|
241
|
+
// unsafe to serve once the store claims a current boundary — require rebuild.
|
|
242
|
+
if (snap.ownershipBoundaryVersion === undefined) {
|
|
243
|
+
throw new BrainStorageError(`snapshot ${snapshotId} missing ownershipBoundaryVersion; rebuild required`);
|
|
244
|
+
}
|
|
230
245
|
const actual = checksumPayload(raw);
|
|
231
246
|
if (actual !== snap.checksum) {
|
|
232
247
|
throw new BrainStorageError(`checksum mismatch for snapshot ${snapshotId}`);
|
|
@@ -261,7 +276,12 @@ export class LocalBrainStore {
|
|
|
261
276
|
return meta.latestSnapshotId;
|
|
262
277
|
}
|
|
263
278
|
async loadLatest() {
|
|
264
|
-
const
|
|
279
|
+
const meta = await this.readMeta();
|
|
280
|
+
if (meta.ownershipBoundaryVersion !== OWNERSHIP_BOUNDARY_VERSION) {
|
|
281
|
+
// Pre-boundary or outdated snapshots must not be served as current project truth.
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
const id = meta.latestSnapshotId;
|
|
265
285
|
if (!id) {
|
|
266
286
|
return null;
|
|
267
287
|
}
|