@praneeth_54/agentdoctor 3.0.0 → 3.0.2
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 +52 -1
- package/README.md +10 -8
- package/dist/agent/chat/deterministic.js +22 -4
- package/dist/agent/context/retrieve.d.ts +2 -1
- package/dist/agent/context/retrieve.js +14 -1
- package/dist/agent/tools/execute.js +39 -43
- package/dist/agent/tools/write.d.ts +1 -1
- package/dist/agent/tools/write.js +18 -4
- package/dist/ai/providers/adversarial-local.d.ts +7 -0
- package/dist/ai/providers/adversarial-local.js +146 -0
- package/dist/cli/commands/agent.js +13 -3
- package/dist/cli/commands/architecture.js +7 -2
- package/dist/cli/commands/brain.js +7 -2
- package/dist/cli/commands/chat.js +13 -3
- 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.js +13 -3
- package/dist/cli/commands/scan.js +6 -0
- package/dist/cli/commands/start.js +22 -7
- package/dist/cli/commands/v2.js +61 -11
- package/dist/cli/commands/verify.js +6 -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 +20 -244
- 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/intelligence/graph/build.js +10 -30
- package/dist/intelligence/graph/incremental.d.ts +2 -0
- package/dist/intelligence/graph/incremental.js +9 -0
- package/dist/mcp/intelligence/path-safety.d.ts +6 -1
- package/dist/mcp/intelligence/path-safety.js +55 -2
- package/dist/platform/graph/build.js +9 -0
- package/dist/product/decisions/ledger.d.ts +13 -1
- package/dist/product/decisions/ledger.js +41 -10
- package/dist/product/discovery/roots.d.ts +10 -0
- package/dist/product/discovery/roots.js +137 -15
- package/dist/product/graph/enrich-languages.js +7 -32
- package/dist/product/index.d.ts +4 -1
- package/dist/product/index.js +2 -1
- package/dist/product/map/software-map.js +2 -1
- package/dist/product/memory/institutional.js +5 -0
- package/dist/product/search/software-search.js +5 -0
- package/dist/product/twin/store.js +9 -1
- package/dist/product/whatif/engine.js +22 -1
- package/dist/project/ownership.d.ts +65 -0
- package/dist/project/ownership.js +169 -0
- package/package.json +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** AgentDoctor dashboard design system CSS. */
|
|
2
|
+
export const DASHBOARD_CSS = `
|
|
3
|
+
:root{
|
|
4
|
+
--bg:#0b0f14;--bg-elev:#111820;--bg-soft:#151d27;--panel:#161e28;--border:#243040;
|
|
5
|
+
--fg:#e8eef5;--muted:#93a0b0;--faint:#6b7888;--accent:#4aa3ff;--accent-2:#2d7fd6;
|
|
6
|
+
--ok:#3ecf8e;--warn:#e6b84d;--crit:#e26060;--unk:#8b9aab;--focus:#7cc4ff;
|
|
7
|
+
--sidebar-w:248px;--sidebar-collapsed:64px;--topbar-h:52px;
|
|
8
|
+
--radius:10px;--font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
|
|
9
|
+
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
|
10
|
+
}
|
|
11
|
+
*{box-sizing:border-box}
|
|
12
|
+
html,body{height:100%;margin:0}
|
|
13
|
+
body{font-family:var(--font);background:var(--bg);color:var(--fg);line-height:1.45;overflow-x:hidden}
|
|
14
|
+
a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}
|
|
15
|
+
button,input,textarea,select{font:inherit}
|
|
16
|
+
button:focus-visible,a:focus-visible,input:focus-visible,textarea:focus-visible,[tabindex]:focus-visible{
|
|
17
|
+
outline:2px solid var(--focus);outline-offset:2px}
|
|
18
|
+
.app{display:grid;grid-template-columns:var(--sidebar-w) 1fr;grid-template-rows:var(--topbar-h) 1fr;height:100vh;min-height:100vh}
|
|
19
|
+
.app.collapsed{grid-template-columns:var(--sidebar-collapsed) 1fr}
|
|
20
|
+
.topbar{grid-column:1/-1;display:flex;align-items:center;gap:.75rem;padding:0 .9rem;border-bottom:1px solid var(--border);background:var(--bg-elev);min-width:0}
|
|
21
|
+
.brand{display:flex;align-items:baseline;gap:.45rem;flex:0 0 auto}
|
|
22
|
+
.brand-name{font-weight:700;letter-spacing:.02em;font-size:.98rem}
|
|
23
|
+
.brand-ver{color:var(--muted);font-size:.78rem}
|
|
24
|
+
.project-chip{display:flex;flex-direction:column;min-width:0;max-width:280px;padding:.25rem .55rem;border:1px solid var(--border);border-radius:8px;background:var(--bg-soft)}
|
|
25
|
+
.project-chip strong{font-size:.82rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
26
|
+
.project-chip span{font-size:.7rem;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
27
|
+
.topbar-spacer{flex:1}
|
|
28
|
+
.topbar-actions{display:flex;align-items:center;gap:.4rem;flex:0 0 auto}
|
|
29
|
+
.btn{appearance:none;border:1px solid var(--border);background:var(--bg-soft);color:var(--fg);border-radius:8px;padding:.35rem .65rem;font-size:.8rem;cursor:pointer}
|
|
30
|
+
.btn:hover{border-color:#35506a}
|
|
31
|
+
.btn-accent{background:var(--accent);border-color:var(--accent-2);color:#041018;font-weight:650}
|
|
32
|
+
.btn-ghost{background:transparent}
|
|
33
|
+
.kbd{font-family:var(--mono);font-size:.7rem;color:var(--muted);border:1px solid var(--border);border-radius:5px;padding:.05rem .3rem}
|
|
34
|
+
.sidebar{border-right:1px solid var(--border);background:var(--bg-elev);overflow:auto;padding:.6rem .45rem .9rem;display:flex;flex-direction:column;gap:.35rem}
|
|
35
|
+
.side-toggle{align-self:flex-end;margin:.15rem .2rem .4rem}
|
|
36
|
+
.nav-group{margin:.35rem 0 .15rem}
|
|
37
|
+
.nav-group-label{font-size:.65rem;letter-spacing:.08em;text-transform:uppercase;color:var(--faint);padding:.2rem .55rem;font-weight:650}
|
|
38
|
+
.nav-link{display:flex;align-items:center;gap:.55rem;padding:.42rem .55rem;border-radius:8px;color:var(--muted);font-size:.84rem;text-decoration:none;border:1px solid transparent}
|
|
39
|
+
.nav-link:hover{background:var(--bg-soft);color:var(--fg);text-decoration:none}
|
|
40
|
+
.nav-link.active{background:#1a2838;color:var(--fg);border-color:#2b425a}
|
|
41
|
+
.nav-ico{width:1.1rem;text-align:center;opacity:.85;flex:0 0 auto}
|
|
42
|
+
.app.collapsed .nav-group-label,.app.collapsed .nav-text,.app.collapsed .project-chip span,.app.collapsed .brand-ver{display:none}
|
|
43
|
+
.app.collapsed .nav-link{justify-content:center;padding:.5rem}
|
|
44
|
+
.app.collapsed .project-chip{max-width:100%;padding:.35rem}
|
|
45
|
+
.main{overflow:auto;min-width:0;background:var(--bg)}
|
|
46
|
+
.content{padding:1rem 1.15rem 2rem;max-width:1180px;margin:0 auto}
|
|
47
|
+
.page-header{margin:0 0 1rem}
|
|
48
|
+
.page-header h1{margin:0;font-size:1.35rem;font-weight:700}
|
|
49
|
+
.page-desc{margin:.35rem 0 0;color:var(--muted);font-size:.9rem;max-width:70ch}
|
|
50
|
+
.meta-row{display:flex;flex-wrap:wrap;gap:.35rem;margin-top:.55rem}
|
|
51
|
+
.badge{display:inline-flex;align-items:center;gap:.25rem;padding:.14rem .48rem;border-radius:999px;border:1px solid var(--border);font-size:.72rem;color:var(--muted);background:var(--bg-soft)}
|
|
52
|
+
.badge.ok{color:var(--ok);border-color:#2a5a44}.badge.attention{color:var(--warn);border-color:#5a4a22}
|
|
53
|
+
.badge.warning,.badge.critical{color:var(--crit);border-color:#6a3030}.badge.unknown{color:var(--unk)}
|
|
54
|
+
.badge.truth{text-transform:uppercase;letter-spacing:.04em;font-size:.68rem}
|
|
55
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:.7rem}
|
|
56
|
+
.card{background:var(--panel);border:1px solid var(--border);border-radius:var(--radius);padding:.85rem .9rem;min-width:0;display:flex;flex-direction:column;gap:.35rem}
|
|
57
|
+
.card h3{margin:0;font-size:.88rem}.card p{margin:0;font-size:.8rem;color:var(--muted)}
|
|
58
|
+
.card .actions{margin-top:auto;padding-top:.35rem}
|
|
59
|
+
.section{margin:1rem 0;background:var(--panel);border:1px solid var(--border);border-radius:var(--radius);padding:1rem}
|
|
60
|
+
.section h2{margin:0 0 .65rem;font-size:1rem}
|
|
61
|
+
.section h3{margin:.8rem 0 .4rem;font-size:.9rem}
|
|
62
|
+
.empty,.error,.loading{padding:1.05rem 1.1rem;border:1px dashed var(--border);border-radius:var(--radius);color:var(--muted);background:var(--bg-soft)}
|
|
63
|
+
.empty strong,.error strong,.loading strong{display:block;color:var(--fg);font-size:.92rem;margin-bottom:.35rem}
|
|
64
|
+
.empty p,.error p,.loading p{margin:.3rem 0 0;font-size:.84rem}
|
|
65
|
+
.error{border-style:solid;border-color:#6a3030;color:#f0b4b4;background:#1a1214}
|
|
66
|
+
.ask-banner{margin:0 0 .85rem;padding:.65rem .8rem;border:1px solid #2b425a;border-radius:var(--radius);background:#121a24;color:var(--muted);font-size:.82rem}
|
|
67
|
+
.ask-banner strong{color:var(--fg)}
|
|
68
|
+
.scope-note{margin:.5rem 0 0;font-size:.8rem;color:var(--faint)}
|
|
69
|
+
.filter-input{width:100%;text-align:left;margin-bottom:.5rem}
|
|
70
|
+
.section-plain{margin:1rem 0}
|
|
71
|
+
.section-plain h2{margin:0 0 .65rem;font-size:1rem}
|
|
72
|
+
.status-dot{width:.55rem;height:.55rem;border-radius:50%;background:var(--ok);display:inline-block;margin-right:.35rem;vertical-align:middle}
|
|
73
|
+
.status-dot.warn{background:var(--warn)}.status-dot.off{background:var(--unk)}
|
|
74
|
+
.topbar-status{font-size:.72rem;color:var(--muted);white-space:nowrap}
|
|
75
|
+
.uncertainty{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:.55rem;margin:.65rem 0}
|
|
76
|
+
.uncertainty .card h3{font-size:.78rem;text-transform:uppercase;letter-spacing:.04em;color:var(--faint)}
|
|
77
|
+
@media (max-width:1280px){
|
|
78
|
+
.content{max-width:100%}
|
|
79
|
+
}
|
|
80
|
+
@media (max-width:1024px){
|
|
81
|
+
.project-chip{max-width:200px}
|
|
82
|
+
}
|
|
83
|
+
@media (max-width:768px){
|
|
84
|
+
.topbar{gap:.45rem;padding:0 .55rem}
|
|
85
|
+
.topbar-status,.brand-ver{display:none}
|
|
86
|
+
.btn .kbd{display:none}
|
|
87
|
+
.content{padding:.75rem .7rem 1.5rem}
|
|
88
|
+
}
|
|
89
|
+
@media (max-width:390px){
|
|
90
|
+
.project-chip{max-width:36vw}
|
|
91
|
+
.brand-name{font-size:.9rem}
|
|
92
|
+
}
|
|
93
|
+
.skeleton{height:.85rem;border-radius:6px;background:linear-gradient(90deg,#15202b,#1c2835,#15202b);background-size:200% 100%;animation:sk 1.2s infinite}
|
|
94
|
+
@keyframes sk{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
|
95
|
+
.table-wrap{overflow:auto;border:1px solid var(--border);border-radius:8px}
|
|
96
|
+
table,.table-wrap table{width:100%;border-collapse:collapse;font-size:.8rem}
|
|
97
|
+
table.data{width:100%;border-collapse:collapse;font-size:.8rem}
|
|
98
|
+
table th,table td,table.data th,table.data td{padding:.45rem .55rem;border-bottom:1px solid var(--border);text-align:left;vertical-align:top}
|
|
99
|
+
table.data th{color:var(--muted);font-weight:600;background:var(--bg-soft);position:sticky;top:0}
|
|
100
|
+
table.data tr:hover td{background:#182230}
|
|
101
|
+
.mono,.code{font-family:var(--mono);font-size:.78rem}
|
|
102
|
+
.tech details{margin-top:.75rem}
|
|
103
|
+
.tech summary{cursor:pointer;color:var(--accent);font-size:.85rem}
|
|
104
|
+
.pre{white-space:pre-wrap;word-break:break-word;font-family:var(--mono);font-size:.75rem;color:#d5dde6;margin:0;max-height:420px;overflow:auto;background:var(--bg-soft);border:1px solid var(--border);border-radius:8px;padding:.65rem}
|
|
105
|
+
.chips{display:flex;flex-wrap:wrap;gap:.3rem}
|
|
106
|
+
.path-sec{font-size:.75rem;color:var(--faint);word-break:break-all;margin:.25rem 0 0}
|
|
107
|
+
.chat-layout{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(240px,1fr);gap:.85rem}
|
|
108
|
+
.chat-log{min-height:280px;max-height:52vh;overflow:auto;border:1px solid var(--border);border-radius:8px;padding:.75rem;background:var(--bg-soft)}
|
|
109
|
+
.chat-msg{margin:0 0 .75rem;padding:.55rem .65rem;border-radius:8px;border:1px solid var(--border);background:var(--panel)}
|
|
110
|
+
.chat-msg.user{border-color:#2b425a}
|
|
111
|
+
.composer{display:flex;flex-direction:column;gap:.45rem;margin-top:.55rem}
|
|
112
|
+
.composer textarea{width:100%;min-height:72px;resize:vertical;background:#0d1520;color:var(--fg);border:1px solid var(--border);border-radius:8px;padding:.55rem}
|
|
113
|
+
.graph-legend{display:flex;flex-wrap:wrap;gap:.4rem;margin:.5rem 0}
|
|
114
|
+
.graph-canvas{width:100%;height:320px;border:1px solid var(--border);border-radius:8px;background:#0d1520;overflow:hidden}
|
|
115
|
+
.palette{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:flex-start;justify-content:center;padding:12vh 1rem;z-index:50}
|
|
116
|
+
.palette.open{display:flex}
|
|
117
|
+
.palette-box{width:min(640px,100%);background:var(--bg-elev);border:1px solid var(--border);border-radius:12px;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.45)}
|
|
118
|
+
.palette-box input{width:100%;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--fg);padding:.85rem 1rem;font-size:1rem}
|
|
119
|
+
.palette-list{max-height:50vh;overflow:auto}
|
|
120
|
+
.palette-item{display:flex;justify-content:space-between;gap:1rem;padding:.65rem 1rem;cursor:pointer;border:0;background:transparent;color:var(--fg);width:100%;text-align:left}
|
|
121
|
+
.palette-item:hover,.palette-item.active{background:var(--bg-soft)}
|
|
122
|
+
.palette-item small{color:var(--muted)}
|
|
123
|
+
.drawer-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.45);display:none;z-index:40}
|
|
124
|
+
.drawer-backdrop.open{display:block}
|
|
125
|
+
.mobile-nav{display:none}
|
|
126
|
+
.safety-row{display:flex;flex-wrap:wrap;gap:.3rem;align-items:center}
|
|
127
|
+
@media (max-width:980px){
|
|
128
|
+
.app,.app.collapsed{grid-template-columns:1fr}
|
|
129
|
+
.sidebar{position:fixed;inset:var(--topbar-h) auto 0 0;width:min(280px,86vw);z-index:45;transform:translateX(-105%);transition:transform .18s ease;box-shadow:8px 0 30px rgba(0,0,0,.35)}
|
|
130
|
+
.sidebar.open{transform:translateX(0)}
|
|
131
|
+
.mobile-nav{display:inline-flex}
|
|
132
|
+
.chat-layout{grid-template-columns:1fr}
|
|
133
|
+
}
|
|
134
|
+
@media (max-width:640px){
|
|
135
|
+
.content{padding:.85rem}.page-header h1{font-size:1.15rem}.grid{grid-template-columns:1fr}
|
|
136
|
+
.project-chip{max-width:42vw}
|
|
137
|
+
}
|
|
138
|
+
`.trim();
|
package/dist/discovery/files.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { DEFAULT_IGNORE_DIRECTORIES, DEFAULT_MAX_FILE_SIZE_BYTES } from "../constants.js";
|
|
4
|
+
import { classifyRelativePathOwnership, decideDirectoryTraversal } from "../project/ownership.js";
|
|
4
5
|
import { isPathInsideRoot, toPosixRelative } from "../utils/path.js";
|
|
5
6
|
import { isLogLikePath } from "./log-like.js";
|
|
6
7
|
/**
|
|
@@ -63,6 +64,15 @@ export async function discoverFiles(options) {
|
|
|
63
64
|
directoriesSkipped.push(relativePath);
|
|
64
65
|
continue;
|
|
65
66
|
}
|
|
67
|
+
const traversal = await decideDirectoryTraversal({
|
|
68
|
+
projectRoot: root,
|
|
69
|
+
absoluteDir: absolutePath,
|
|
70
|
+
relativeDir: relativePath,
|
|
71
|
+
});
|
|
72
|
+
if (!traversal.traverse) {
|
|
73
|
+
directoriesSkipped.push(`${relativePath} (${traversal.ownership})`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
66
76
|
queue.push(absolutePath);
|
|
67
77
|
continue;
|
|
68
78
|
}
|
|
@@ -79,6 +89,9 @@ export async function discoverFiles(options) {
|
|
|
79
89
|
filesSkippedOversized += 1;
|
|
80
90
|
continue;
|
|
81
91
|
}
|
|
92
|
+
if (classifyRelativePathOwnership(relativePath) !== "project_owned") {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
82
95
|
files.push({
|
|
83
96
|
absolutePath,
|
|
84
97
|
relativePath,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import { createHash } from "node:crypto";
|
|
4
3
|
import ts from "typescript";
|
|
4
|
+
import { discoverFiles } from "../../discovery/files.js";
|
|
5
5
|
import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
|
|
6
6
|
import { buildRepositoryGraph } from "../../platform/graph/build.js";
|
|
7
7
|
import { CONTRACTS_VERSION } from "../../contracts/index.js";
|
|
@@ -45,35 +45,15 @@ async function applyLanguageEnrichment(graph, root) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
export async function listTsFiles(root, limit = 400) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
catch {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
for (const e of entries) {
|
|
61
|
-
if (skip.has(e.name))
|
|
62
|
-
continue;
|
|
63
|
-
const abs = path.join(dir, e.name);
|
|
64
|
-
if (e.isSymbolicLink())
|
|
65
|
-
continue;
|
|
66
|
-
if (e.isDirectory()) {
|
|
67
|
-
await walk(abs);
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
if (/\.(ts|tsx|mts|cts)$/i.test(e.name) && !e.name.endsWith(".d.ts")) {
|
|
71
|
-
out.push(abs);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
await walk(root);
|
|
76
|
-
return out.sort();
|
|
48
|
+
const discovered = await discoverFiles({ root });
|
|
49
|
+
return discovered.files
|
|
50
|
+
.filter((f) => {
|
|
51
|
+
const name = path.basename(f.relativePath);
|
|
52
|
+
return /\.(ts|tsx|mts|cts)$/i.test(name) && !name.endsWith(".d.ts");
|
|
53
|
+
})
|
|
54
|
+
.map((f) => f.absolutePath)
|
|
55
|
+
.sort()
|
|
56
|
+
.slice(0, limit);
|
|
77
57
|
}
|
|
78
58
|
/**
|
|
79
59
|
* TypeScript compiler API based graph enrichment.
|
|
@@ -12,6 +12,8 @@ export interface GraphSnapshot {
|
|
|
12
12
|
nodes: GraphNode[];
|
|
13
13
|
edges: GraphEdge[];
|
|
14
14
|
limitations: string[];
|
|
15
|
+
/** Reject cached graphs built under a prior ownership boundary. */
|
|
16
|
+
ownershipBoundaryVersion?: number;
|
|
15
17
|
}
|
|
16
18
|
export interface GraphStatus {
|
|
17
19
|
present: boolean;
|
|
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { PACKAGE_VERSION } from "../../constants.js";
|
|
6
|
+
import { OWNERSHIP_BOUNDARY_VERSION } from "../../project/ownership.js";
|
|
6
7
|
import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
|
|
7
8
|
import { atomicWriteTextFile } from "../../utils/fs.js";
|
|
8
9
|
import { buildIntelligenceGraph, listTsFiles } from "./build.js";
|
|
@@ -75,6 +76,13 @@ async function loadSnapshot(root) {
|
|
|
75
76
|
if (!isValidSnapshot(parsed)) {
|
|
76
77
|
return { ok: false, corrupt: true, error: "index.json failed schema checks" };
|
|
77
78
|
}
|
|
79
|
+
if (parsed.ownershipBoundaryVersion !== OWNERSHIP_BOUNDARY_VERSION) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
corrupt: false,
|
|
83
|
+
error: `graph index ownership boundary is stale (have ${String(parsed.ownershipBoundaryVersion ?? "missing")}, need ${OWNERSHIP_BOUNDARY_VERSION})`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
78
86
|
return { ok: true, snapshot: parsed };
|
|
79
87
|
}
|
|
80
88
|
catch (error) {
|
|
@@ -109,6 +117,7 @@ async function buildAndPersist(root, mode = "auto") {
|
|
|
109
117
|
...graph.limitations,
|
|
110
118
|
"Incremental index persists file hashes for change detection",
|
|
111
119
|
],
|
|
120
|
+
ownershipBoundaryVersion: OWNERSHIP_BOUNDARY_VERSION,
|
|
112
121
|
};
|
|
113
122
|
const saved = await persistSnapshot(root, snapshot);
|
|
114
123
|
return { snapshot, path: saved };
|
|
@@ -4,6 +4,11 @@ export { tryDecodeUriComponent };
|
|
|
4
4
|
* Validate a user-supplied repository-relative or in-repo target.
|
|
5
5
|
* Returns a normalized path relative to root (POSIX) when the target is path-like and safe,
|
|
6
6
|
* or null when the target is treated as a non-path identifier (graph id / label).
|
|
7
|
-
* Throws a safe Error (no outside path leakage) when a path-like target escapes the root
|
|
7
|
+
* Throws a safe Error (no outside path leakage) when a path-like target escapes the root
|
|
8
|
+
* or is outside project ownership (.private, AgentDoctorOS, fixtures, validation).
|
|
8
9
|
*/
|
|
9
10
|
export declare function assertSafeRepoTarget(root: string, rawTarget: string): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Async ownership gate including nested-repository ancestry (for MCP file-path tools).
|
|
13
|
+
*/
|
|
14
|
+
export declare function assertSafeOwnedRepoTarget(root: string, rawTarget: string): Promise<string | null>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { assertProjectOwnedRepoPath, classifyRelativePathOwnership, ProjectOwnershipError, } from "../../project/ownership.js";
|
|
2
4
|
import { PathEscapeError, rejectHostilePathInput, resolveSafeRepoPath, tryDecodeUriComponent, } from "../../security/paths.js";
|
|
3
5
|
import { sanitizeForOutput } from "../../utils/path.js";
|
|
4
6
|
export { tryDecodeUriComponent };
|
|
@@ -6,7 +8,8 @@ export { tryDecodeUriComponent };
|
|
|
6
8
|
* Validate a user-supplied repository-relative or in-repo target.
|
|
7
9
|
* Returns a normalized path relative to root (POSIX) when the target is path-like and safe,
|
|
8
10
|
* or null when the target is treated as a non-path identifier (graph id / label).
|
|
9
|
-
* Throws a safe Error (no outside path leakage) when a path-like target escapes the root
|
|
11
|
+
* Throws a safe Error (no outside path leakage) when a path-like target escapes the root
|
|
12
|
+
* or is outside project ownership (.private, AgentDoctorOS, fixtures, validation).
|
|
10
13
|
*/
|
|
11
14
|
export function assertSafeRepoTarget(root, rawTarget) {
|
|
12
15
|
if (typeof rawTarget !== "string" || rawTarget.includes("\0")) {
|
|
@@ -52,12 +55,62 @@ export function assertSafeRepoTarget(root, rawTarget) {
|
|
|
52
55
|
const primary = candidates[candidates.length - 1];
|
|
53
56
|
try {
|
|
54
57
|
const abs = resolveSafeRepoPath(root, primary);
|
|
55
|
-
|
|
58
|
+
const lexicalRel = path.isAbsolute(primary)
|
|
59
|
+
? path.relative(root, abs).split(path.sep).join("/")
|
|
60
|
+
: primary.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
61
|
+
// Lexical non-owned prefixes always deny.
|
|
62
|
+
if (classifyRelativePathOwnership(lexicalRel) !== "project_owned") {
|
|
63
|
+
throw new Error("path outside project ownership");
|
|
64
|
+
}
|
|
65
|
+
// Realpath-relative classification catches symlink aliases into non-owned trees.
|
|
66
|
+
let realRoot = root;
|
|
67
|
+
let realAbs = abs;
|
|
68
|
+
try {
|
|
69
|
+
realRoot = fs.realpathSync(root);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* keep */
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
realAbs = fs.realpathSync(abs);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* keep */
|
|
79
|
+
}
|
|
80
|
+
const realRel = path.relative(realRoot, realAbs).split(path.sep).join("/") || ".";
|
|
81
|
+
if (realRel.startsWith("..") ||
|
|
82
|
+
path.isAbsolute(realRel) ||
|
|
83
|
+
classifyRelativePathOwnership(realRel) !== "project_owned") {
|
|
84
|
+
throw new Error("path outside project ownership");
|
|
85
|
+
}
|
|
86
|
+
return realRel || lexicalRel;
|
|
56
87
|
}
|
|
57
88
|
catch (error) {
|
|
58
89
|
if (error instanceof PathEscapeError) {
|
|
59
90
|
throw new Error(error.message);
|
|
60
91
|
}
|
|
92
|
+
if (error instanceof Error && error.message === "path outside project ownership") {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
61
95
|
throw new Error("invalid target");
|
|
62
96
|
}
|
|
63
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Async ownership gate including nested-repository ancestry (for MCP file-path tools).
|
|
100
|
+
*/
|
|
101
|
+
export async function assertSafeOwnedRepoTarget(root, rawTarget) {
|
|
102
|
+
const rel = assertSafeRepoTarget(root, rawTarget);
|
|
103
|
+
if (rel === null)
|
|
104
|
+
return null;
|
|
105
|
+
try {
|
|
106
|
+
const abs = resolveSafeRepoPath(root, rel);
|
|
107
|
+
await assertProjectOwnedRepoPath(root, abs, rel);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof PathEscapeError || error instanceof ProjectOwnershipError) {
|
|
111
|
+
throw new Error(error.message);
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
return rel;
|
|
116
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import { decideDirectoryTraversal } from "../../project/ownership.js";
|
|
4
5
|
import { readTextFile } from "../../utils/fs.js";
|
|
5
6
|
import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
|
|
6
7
|
const SKIP = new Set([
|
|
@@ -48,6 +49,14 @@ async function walk(root, dir, files, dirs, limit) {
|
|
|
48
49
|
if (entry.isSymbolicLink())
|
|
49
50
|
continue;
|
|
50
51
|
if (entry.isDirectory()) {
|
|
52
|
+
const relativeDir = toPosixRelative(root, absolute);
|
|
53
|
+
const traversal = await decideDirectoryTraversal({
|
|
54
|
+
projectRoot: root,
|
|
55
|
+
absoluteDir: absolute,
|
|
56
|
+
relativeDir,
|
|
57
|
+
});
|
|
58
|
+
if (!traversal.traverse)
|
|
59
|
+
continue;
|
|
51
60
|
dirs.push(absolute);
|
|
52
61
|
await walk(root, absolute, files, dirs, limit);
|
|
53
62
|
continue;
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
+
import { type ProjectOwnershipClass } from "../../project/ownership.js";
|
|
1
2
|
import type { ProductEvidence, TruthLabel } from "../truth.js";
|
|
3
|
+
/** Why a decision document is attributed to the current project. */
|
|
4
|
+
export type DecisionSourceKind = "adr_file" | "decision_ledger" | "architecture_document" | "internal_note" | "unknown";
|
|
2
5
|
export interface DecisionRecord {
|
|
3
6
|
id: string;
|
|
4
7
|
title: string;
|
|
5
8
|
status?: string;
|
|
9
|
+
/** Wire-compat provenance channel. */
|
|
6
10
|
source: "adr-file" | "ledger";
|
|
11
|
+
sourceKind: DecisionSourceKind;
|
|
7
12
|
truth: TruthLabel;
|
|
13
|
+
/** Explicit meaning for UI — never “architecture proven correct”. */
|
|
14
|
+
truthMeaning: string;
|
|
15
|
+
projectRoot: string;
|
|
16
|
+
ownership: ProjectOwnershipClass;
|
|
8
17
|
evidence: ProductEvidence[];
|
|
9
18
|
recordedAt?: string;
|
|
10
19
|
bodyExcerpt?: string;
|
|
@@ -16,9 +25,12 @@ export interface DecisionLedgerResult {
|
|
|
16
25
|
limitations: string[];
|
|
17
26
|
}
|
|
18
27
|
export declare function parseAdrDecisions(rootInput: string, maxFileSizeBytes?: number): Promise<DecisionRecord[]>;
|
|
19
|
-
export declare function appendDecisionLedgerEntry(rootInput: string, record: Omit<DecisionRecord, "source" | "truth"> & {
|
|
28
|
+
export declare function appendDecisionLedgerEntry(rootInput: string, record: Omit<DecisionRecord, "source" | "truth" | "sourceKind" | "truthMeaning" | "projectRoot" | "ownership"> & {
|
|
20
29
|
source?: DecisionRecord["source"];
|
|
30
|
+
sourceKind?: DecisionSourceKind;
|
|
21
31
|
truth?: TruthLabel;
|
|
32
|
+
truthMeaning?: string;
|
|
33
|
+
ownership?: ProjectOwnershipClass;
|
|
22
34
|
}): Promise<DecisionRecord>;
|
|
23
35
|
export declare function loadDecisionLedger(rootInput: string): Promise<DecisionLedgerResult>;
|
|
24
36
|
export declare function resolveDecisionPath(rootInput: string, candidate: string): string;
|
|
@@ -2,21 +2,26 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../../constants.js";
|
|
4
4
|
import { detectProject } from "../../detectors/project.js";
|
|
5
|
-
import {
|
|
5
|
+
import { classifyRelativePathOwnership, isProjectOwnedRelativePath, verifiedDecisionTruthMeaning, } from "../../project/ownership.js";
|
|
6
6
|
import { resolveSafeRepoPath } from "../../security/paths.js";
|
|
7
|
+
import { pathExists, readTextFile } from "../../utils/fs.js";
|
|
7
8
|
import { resolveRepoRoot } from "../../utils/path.js";
|
|
8
|
-
import { pathExists } from "../../utils/fs.js";
|
|
9
9
|
function ledgerFile(root) {
|
|
10
10
|
return path.join(resolveRepoRoot(root), ".agentdoctor", "decisions", "entries.jsonl");
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* ADR-shaped paths under the current project-owned tree only.
|
|
14
|
+
* Nested foreign trees with an adr directory segment are excluded by ownership
|
|
15
|
+
* before this runs; this still refuses internal_docs / private paths as defense in depth.
|
|
16
|
+
*/
|
|
12
17
|
function isAdrPath(relativePath) {
|
|
18
|
+
if (!isProjectOwnedRelativePath(relativePath))
|
|
19
|
+
return false;
|
|
13
20
|
const norm = relativePath.replace(/\\/g, "/").toLowerCase();
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
norm.includes("/adr/") ||
|
|
17
|
-
/^adr-\d+/i.test(path.basename(relativePath)));
|
|
21
|
+
const base = path.basename(relativePath);
|
|
22
|
+
return norm.startsWith("docs/adr/") || norm.startsWith("adr/") || /^adr[-_]\d+/i.test(base);
|
|
18
23
|
}
|
|
19
|
-
function parseAdr(relativePath, content) {
|
|
24
|
+
function parseAdr(projectRoot, relativePath, content) {
|
|
20
25
|
const lines = content.split(/\r?\n/);
|
|
21
26
|
let title = "";
|
|
22
27
|
let status;
|
|
@@ -31,12 +36,17 @@ function parseAdr(relativePath, content) {
|
|
|
31
36
|
if (!title)
|
|
32
37
|
return null;
|
|
33
38
|
const id = relativePath.replace(/[^\w./-]/g, "_");
|
|
39
|
+
const ownership = classifyRelativePathOwnership(relativePath);
|
|
34
40
|
return {
|
|
35
41
|
id,
|
|
36
42
|
title,
|
|
37
43
|
...(status !== undefined ? { status } : {}),
|
|
38
44
|
source: "adr-file",
|
|
45
|
+
sourceKind: "adr_file",
|
|
39
46
|
truth: "VERIFIED",
|
|
47
|
+
truthMeaning: verifiedDecisionTruthMeaning(),
|
|
48
|
+
projectRoot,
|
|
49
|
+
ownership,
|
|
40
50
|
evidence: [{ path: relativePath, line: 1, excerpt: title.slice(0, 120) }],
|
|
41
51
|
bodyExcerpt: content.slice(0, 400),
|
|
42
52
|
};
|
|
@@ -52,7 +62,7 @@ export async function parseAdrDecisions(rootInput, maxFileSizeBytes = DEFAULT_MA
|
|
|
52
62
|
const text = await readTextFile(path.join(root, rel), maxFileSizeBytes);
|
|
53
63
|
if (!text)
|
|
54
64
|
continue;
|
|
55
|
-
const parsed = parseAdr(rel, text);
|
|
65
|
+
const parsed = parseAdr(root, rel, text);
|
|
56
66
|
if (parsed)
|
|
57
67
|
decisions.push(parsed);
|
|
58
68
|
}
|
|
@@ -66,7 +76,11 @@ export async function appendDecisionLedgerEntry(rootInput, record) {
|
|
|
66
76
|
const full = {
|
|
67
77
|
...record,
|
|
68
78
|
source: record.source ?? "ledger",
|
|
79
|
+
sourceKind: record.sourceKind ?? "decision_ledger",
|
|
69
80
|
truth: record.truth ?? "VERIFIED",
|
|
81
|
+
truthMeaning: record.truthMeaning ?? verifiedDecisionTruthMeaning(),
|
|
82
|
+
projectRoot: root,
|
|
83
|
+
ownership: record.ownership ?? "project_owned",
|
|
70
84
|
recordedAt: record.recordedAt ?? new Date().toISOString(),
|
|
71
85
|
};
|
|
72
86
|
await fs.appendFile(ledgerFile(root), `${JSON.stringify(full)}\n`, "utf8");
|
|
@@ -76,7 +90,9 @@ export async function loadDecisionLedger(rootInput) {
|
|
|
76
90
|
const root = resolveRepoRoot(rootInput);
|
|
77
91
|
const limitations = [
|
|
78
92
|
"ADR parsing reads markdown titles and Status lines only.",
|
|
79
|
-
"
|
|
93
|
+
"Only project-owned trees are scanned; nested repositories and private/validation checkouts are excluded.",
|
|
94
|
+
"VERIFIED means the ADR file was found and parsed — not that the architecture decision is independently proven correct.",
|
|
95
|
+
"Ledger JSONL entries are merged with discovered ADR files (deduped by id) when present and project-owned.",
|
|
80
96
|
];
|
|
81
97
|
const fromAdr = await parseAdrDecisions(root);
|
|
82
98
|
const fromLedger = [];
|
|
@@ -87,7 +103,22 @@ export async function loadDecisionLedger(rootInput) {
|
|
|
87
103
|
if (!line.trim())
|
|
88
104
|
continue;
|
|
89
105
|
try {
|
|
90
|
-
|
|
106
|
+
const raw = JSON.parse(line);
|
|
107
|
+
const evidencePath = raw.evidence?.[0]?.path ?? raw.id;
|
|
108
|
+
if (!isProjectOwnedRelativePath(String(evidencePath)) &&
|
|
109
|
+
!isProjectOwnedRelativePath(String(raw.id))) {
|
|
110
|
+
limitations.push(`Skipped non-owned ledger entry: ${raw.id}`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
fromLedger.push({
|
|
114
|
+
...raw,
|
|
115
|
+
source: raw.source ?? "ledger",
|
|
116
|
+
sourceKind: raw.sourceKind ?? "decision_ledger",
|
|
117
|
+
truth: raw.truth ?? "VERIFIED",
|
|
118
|
+
truthMeaning: raw.truthMeaning ?? verifiedDecisionTruthMeaning(),
|
|
119
|
+
projectRoot: raw.projectRoot ?? root,
|
|
120
|
+
ownership: raw.ownership ?? classifyRelativePathOwnership(String(evidencePath)),
|
|
121
|
+
});
|
|
91
122
|
}
|
|
92
123
|
catch {
|
|
93
124
|
limitations.push("Skipped malformed decision ledger line.");
|
|
@@ -25,6 +25,16 @@ export interface DiscoverProjectOptions {
|
|
|
25
25
|
/** Auto-select highest score when exactly one strong candidate. */
|
|
26
26
|
autoSelect?: boolean;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* True when `cwd` is the user home directory or a similarly unsafe broad folder
|
|
30
|
+
* (Desktop / Downloads / Documents). Used to refuse silent whole-home scans.
|
|
31
|
+
*/
|
|
32
|
+
export declare function classifyBroadUserScanRoot(cwdInput: string, homeInput?: string): {
|
|
33
|
+
blocked: true;
|
|
34
|
+
reason: string;
|
|
35
|
+
} | {
|
|
36
|
+
blocked: false;
|
|
37
|
+
};
|
|
28
38
|
/**
|
|
29
39
|
* Discover likely project roots from cwd without silently scanning home/Desktop/Downloads
|
|
30
40
|
* or unbounded parent trees.
|