devworkspace 0.2.1 → 0.2.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 +24 -0
- package/README.md +4 -3
- package/dist/agents/claude/content.js +3 -2
- package/dist/agents/claude/content.js.map +1 -1
- package/dist/cli/args.d.ts +16 -0
- package/dist/cli/args.js +5 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/help.js +30 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/render.d.ts +7 -1
- package/dist/cli/render.js +64 -11
- package/dist/cli/render.js.map +1 -1
- package/dist/cli/run.js +47 -8
- package/dist/cli/run.js.map +1 -1
- package/dist/config/schemas.d.ts +5 -0
- package/dist/config/schemas.js +2 -0
- package/dist/config/schemas.js.map +1 -1
- package/dist/discovery/commands.js +17 -3
- package/dist/discovery/commands.js.map +1 -1
- package/dist/discovery/detect.d.ts +6 -0
- package/dist/discovery/detect.js +12 -6
- package/dist/discovery/detect.js.map +1 -1
- package/dist/discovery/docs.d.ts +10 -0
- package/dist/discovery/docs.js +52 -0
- package/dist/discovery/docs.js.map +1 -0
- package/dist/discovery/entrypoints.d.ts +20 -0
- package/dist/discovery/entrypoints.js +67 -0
- package/dist/discovery/entrypoints.js.map +1 -0
- package/dist/knowledge/facts.d.ts +59 -2
- package/dist/knowledge/facts.js +115 -17
- package/dist/knowledge/facts.js.map +1 -1
- package/dist/prompts/onboarding.js +6 -3
- package/dist/prompts/onboarding.js.map +1 -1
- package/dist/services/analysis.js +6 -0
- package/dist/services/analysis.js.map +1 -1
- package/dist/services/doctor.js +14 -3
- package/dist/services/doctor.js.map +1 -1
- package/dist/services/knowledge.d.ts +17 -0
- package/dist/services/knowledge.js +28 -2
- package/dist/services/knowledge.js.map +1 -1
- package/dist/services/refresh.js +5 -5
- package/dist/services/refresh.js.map +1 -1
- package/dist/services/status.d.ts +11 -2
- package/dist/services/status.js +11 -9
- package/dist/services/status.js.map +1 -1
- package/dist/services/tasks.d.ts +2 -0
- package/dist/services/tasks.js +20 -12
- package/dist/services/tasks.js.map +1 -1
- package/dist/services/verify.d.ts +47 -0
- package/dist/services/verify.js +172 -0
- package/dist/services/verify.js.map +1 -0
- package/dist/tasks/routing.js +8 -3
- package/dist/tasks/routing.js.map +1 -1
- package/dist/tasks/task.d.ts +49 -5
- package/dist/tasks/task.js +68 -11
- package/dist/tasks/task.js.map +1 -1
- package/dist/verification/safety.d.ts +38 -0
- package/dist/verification/safety.js +120 -0
- package/dist/verification/safety.js.map +1 -0
- package/dist/verification/scope.d.ts +19 -0
- package/dist/verification/scope.js +119 -0
- package/dist/verification/scope.js.map +1 -0
- package/docs/knowledge.md +2 -0
- package/docs/testing.md +25 -2
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docs.js","sourceRoot":"","sources":["../../src/discovery/docs.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'node:path';\n\nconst DOC_FILE = /\\.(?:md|mdx|rst)$/i;\nconst DOC_DIR = /^(?:docs?|documentation|adrs?|architecture|decisions?)$/i;\nconst TOP_LEVEL = /^(?:readme(?:\\.[a-z]+)?|contributing\\.md|architecture\\.md|changelog\\.md)$/i;\nconst PACKAGE_LEVEL = /^(?:readme|architecture|contributing)\\.(?:md|mdx|rst)$/i;\n/** Directories whose Markdown is sample data, not the project's own documentation. */\nconst SAMPLE_DIRS = new Set(['fixtures', 'fixture', '__fixtures__', 'examples', 'example', 'samples', 'sample', 'testdata', 'test-data', 'demo', 'demos', 'third_party', 'thirdparty']);\n\nexport const MAX_DOCS = 40;\nconst MAX_DEPTH = 6;\nconst MAX_DOC_DIR_INDEX = 2; // docs/ may sit at most two directories below the root: backend/docs, apps/api/docs\n\n/** Files that matter most when the list has to be cut: indexes, architecture notes, ADRs and READMEs. */\nfunction priority(file: string): number {\n const name = path.posix.basename(file);\n if (/^readme/i.test(name)) return 0;\n if (/(?:^|[/_-])(?:index|architecture|overview|quick[_-]?start|adrs?[-_]?\\d*)(?=[/_.-]|$)/i.test(file)) return 1;\n return 2;\n}\n\n/**\n * Documentation worth pointing an agent at: top-level READMEs and notes, Markdown in docs/adr/architecture directories\n * (also inside packages such as backend/docs), and READMEs of packages. Pure function of the scanned file set, so it is\n * deterministic; generated and dependency directories were already excluded by the scan.\n */\nexport function detectDocs(files: Iterable<string>, packageDirs: string[]): { docs: string[]; omitted: number } {\n const pkgReadmes = new Set(packageDirs.filter((d) => d !== '').map((d) => d));\n const found = new Set<string>();\n for (const f of files) {\n if (!DOC_FILE.test(f)) continue;\n const segs = f.split('/');\n if (segs.length > MAX_DEPTH) continue;\n const dirs = segs.slice(0, -1);\n const name = segs[segs.length - 1]!;\n if (dirs.length === 0) {\n if (TOP_LEVEL.test(name)) found.add(f);\n continue;\n }\n const docIdx = dirs.findIndex((s) => DOC_DIR.test(s));\n if (docIdx !== -1 && docIdx <= MAX_DOC_DIR_INDEX && !dirs.slice(0, docIdx).some((s) => SAMPLE_DIRS.has(s.toLowerCase()))) {\n found.add(f);\n continue;\n }\n if (PACKAGE_LEVEL.test(name) && pkgReadmes.has(dirs.join('/')) && !dirs.some((s) => SAMPLE_DIRS.has(s.toLowerCase()))) found.add(f);\n }\n const all = [...found].sort((a, b) => priority(a) - priority(b) || a.localeCompare(b));\n return { docs: all.slice(0, MAX_DOCS).sort(), omitted: Math.max(0, all.length - MAX_DOCS) };\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PackageInfo } from './detect.js';
|
|
2
|
+
export type EntryKind = 'server' | 'worker' | 'main' | 'cli';
|
|
3
|
+
export interface EntryPoint {
|
|
4
|
+
/** Path relative to the package directory (POSIX). */
|
|
5
|
+
path: string;
|
|
6
|
+
/** The package directory relative to the project root; "." for the root package. */
|
|
7
|
+
cwd: string;
|
|
8
|
+
kind: EntryKind;
|
|
9
|
+
/** Where the evidence came from, e.g. `backend/package.json#scripts.dev`. */
|
|
10
|
+
evidence: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Entry points that a package itself declares: files named by its start-like scripts, its `main` and its `bin`.
|
|
14
|
+
* A file is never an entry point just because it is called server.ts, worker.ts or index.ts.
|
|
15
|
+
*/
|
|
16
|
+
export declare function discoverEntryPoints(packages: {
|
|
17
|
+
dir: string;
|
|
18
|
+
pkg: PackageInfo;
|
|
19
|
+
manifest: string;
|
|
20
|
+
}[], files: Set<string>): EntryPoint[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
/** Script names that start a long-running process. Everything else (build, test, db:migrate, ...) is not an entry point. */
|
|
3
|
+
const RUN_SCRIPT = /^(?:dev|start|serve|server|watch|worker|workers|queue|consumer)(?::.+)?$/;
|
|
4
|
+
const WORKER_HINT = /worker|queue|consumer/i;
|
|
5
|
+
const FILE_TOKEN = /(?:^|[\s"'=;&|])((?:\.{1,2}\/)?[\w@./-]+\.(?:[cm]?[jt]s|[jt]sx))(?=$|[\s"';&|])/g;
|
|
6
|
+
const NOT_AN_ENTRY = /\.(?:config|test|spec|d)\.[cm]?[jt]s$|(^|\/)(?:node_modules|dist|build)\//;
|
|
7
|
+
const OUTPUT_DIR = /^(?:\.\/)?(?:dist|build|out|lib)\//;
|
|
8
|
+
const SOURCE_EXT = ['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs', '.jsx'];
|
|
9
|
+
function resolveIn(files, dir, token) {
|
|
10
|
+
const rel = (p) => path.posix.normalize(dir ? `${dir}/${p}` : p);
|
|
11
|
+
const direct = rel(token.replace(/^\.\//, ''));
|
|
12
|
+
if (!OUTPUT_DIR.test(token) && files.has(direct))
|
|
13
|
+
return direct;
|
|
14
|
+
if (OUTPUT_DIR.test(token)) {
|
|
15
|
+
// `node dist/app/server.js` runs the compiled form of src/app/server.ts
|
|
16
|
+
const stem = token.replace(OUTPUT_DIR, '').replace(/\.[cm]?js$/, '');
|
|
17
|
+
for (const ext of SOURCE_EXT) {
|
|
18
|
+
const candidate = rel(`src/${stem}${ext}`);
|
|
19
|
+
if (files.has(candidate))
|
|
20
|
+
return candidate;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Entry points that a package itself declares: files named by its start-like scripts, its `main` and its `bin`.
|
|
27
|
+
* A file is never an entry point just because it is called server.ts, worker.ts or index.ts.
|
|
28
|
+
*/
|
|
29
|
+
export function discoverEntryPoints(packages, files) {
|
|
30
|
+
const found = new Map();
|
|
31
|
+
const add = (dir, projectRel, kind, evidence) => {
|
|
32
|
+
const rel = dir ? projectRel.slice(dir.length + 1) : projectRel;
|
|
33
|
+
const key = `${dir}\u0000${rel}`;
|
|
34
|
+
const prev = found.get(key);
|
|
35
|
+
// A worker script wins over a generic server label for the same file.
|
|
36
|
+
if (!prev || (kind === 'worker' && prev.kind !== 'worker'))
|
|
37
|
+
found.set(key, { path: rel, cwd: dir || '.', kind, evidence });
|
|
38
|
+
};
|
|
39
|
+
for (const { dir, pkg, manifest } of packages) {
|
|
40
|
+
for (const [name, body] of Object.entries(pkg.scripts)) {
|
|
41
|
+
if (!RUN_SCRIPT.test(name))
|
|
42
|
+
continue;
|
|
43
|
+
for (const m of body.matchAll(FILE_TOKEN)) {
|
|
44
|
+
const token = m[1];
|
|
45
|
+
if (token.startsWith('-') || NOT_AN_ENTRY.test(token.replace(OUTPUT_DIR, 'x/')))
|
|
46
|
+
continue;
|
|
47
|
+
const hit = resolveIn(files, dir, token);
|
|
48
|
+
if (!hit)
|
|
49
|
+
continue;
|
|
50
|
+
add(dir, hit, WORKER_HINT.test(name) || WORKER_HINT.test(path.posix.basename(hit)) ? 'worker' : 'server', `${manifest}#scripts.${name}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (pkg.main) {
|
|
54
|
+
const hit = resolveIn(files, dir, pkg.main);
|
|
55
|
+
if (hit && !NOT_AN_ENTRY.test(hit))
|
|
56
|
+
add(dir, hit, 'main', `${manifest}#main`);
|
|
57
|
+
}
|
|
58
|
+
for (const bin of pkg.bin ?? []) {
|
|
59
|
+
const hit = resolveIn(files, dir, bin);
|
|
60
|
+
if (hit && !NOT_AN_ENTRY.test(hit))
|
|
61
|
+
add(dir, hit, 'cli', `${manifest}#bin`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const order = { server: 0, worker: 1, main: 2, cli: 3 };
|
|
65
|
+
return [...found.values()].sort((a, b) => a.cwd.localeCompare(b.cwd) || order[a.kind] - order[b.kind] || a.path.localeCompare(b.path)).slice(0, 60);
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=entrypoints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entrypoints.js","sourceRoot":"","sources":["../../src/discovery/entrypoints.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAe7B,4HAA4H;AAC5H,MAAM,UAAU,GAAG,0EAA0E,CAAC;AAC9F,MAAM,WAAW,GAAG,wBAAwB,CAAC;AAC7C,MAAM,UAAU,GAAG,kFAAkF,CAAC;AACtG,MAAM,YAAY,GAAG,2EAA2E,CAAC;AACjG,MAAM,UAAU,GAAG,oCAAoC,CAAC;AACxD,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAElF,SAAS,SAAS,CAAC,KAAkB,EAAE,GAAW,EAAE,KAAa;IAC/D,MAAM,GAAG,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjF,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;IAC/C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IAChE,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3B,wEAAwE;QACxE,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;QACrE,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;YAC7B,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,IAAI,GAAG,GAAG,EAAE,CAAC,CAAC;YAC3C,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC;gBAAE,OAAO,SAAS,CAAC;QAC7C,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,QAA+D,EAAE,KAAkB;IACrH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC5C,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,UAAkB,EAAE,IAAe,EAAE,QAAgB,EAAQ,EAAE;QACvF,MAAM,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC;QAChE,MAAM,GAAG,GAAG,GAAG,GAAG,SAAS,GAAG,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,sEAAsE;QACtE,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC7H,CAAC,CAAC;IAEF,KAAK,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,QAAQ,EAAE,CAAC;QAC9C,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACvD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,SAAS;YACrC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC1C,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;gBACpB,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;oBAAE,SAAS;gBAC1F,MAAM,GAAG,GAAG,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;gBACzC,IAAI,CAAC,GAAG;oBAAE,SAAS;gBACnB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,GAAG,QAAQ,YAAY,IAAI,EAAE,CAAC,CAAC;YAC3I,CAAC;QACH,CAAC;QACD,IAAI,GAAG,CAAC,IAAI,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;YAC5C,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC;gBAAE,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,QAAQ,OAAO,CAAC,CAAC;QAChF,CAAC;QACD,KAAK,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,EAAE,EAAE,CAAC;YAChC,MAAM,GAAG,GAAG,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;YACvC,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC;gBAAE,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,QAAQ,MAAM,CAAC,CAAC;QAC9E,CAAC;IACH,CAAC;IACD,MAAM,KAAK,GAA8B,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;IACnF,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACtJ,CAAC","sourcesContent":["import path from 'node:path';\nimport type { PackageInfo } from './detect.js';\n\nexport type EntryKind = 'server' | 'worker' | 'main' | 'cli';\n\nexport interface EntryPoint {\n /** Path relative to the package directory (POSIX). */\n path: string;\n /** The package directory relative to the project root; \".\" for the root package. */\n cwd: string;\n kind: EntryKind;\n /** Where the evidence came from, e.g. `backend/package.json#scripts.dev`. */\n evidence: string;\n}\n\n/** Script names that start a long-running process. Everything else (build, test, db:migrate, ...) is not an entry point. */\nconst RUN_SCRIPT = /^(?:dev|start|serve|server|watch|worker|workers|queue|consumer)(?::.+)?$/;\nconst WORKER_HINT = /worker|queue|consumer/i;\nconst FILE_TOKEN = /(?:^|[\\s\"'=;&|])((?:\\.{1,2}\\/)?[\\w@./-]+\\.(?:[cm]?[jt]s|[jt]sx))(?=$|[\\s\"';&|])/g;\nconst NOT_AN_ENTRY = /\\.(?:config|test|spec|d)\\.[cm]?[jt]s$|(^|\\/)(?:node_modules|dist|build)\\//;\nconst OUTPUT_DIR = /^(?:\\.\\/)?(?:dist|build|out|lib)\\//;\nconst SOURCE_EXT = ['.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs', '.jsx'];\n\nfunction resolveIn(files: Set<string>, dir: string, token: string): string | null {\n const rel = (p: string): string => path.posix.normalize(dir ? `${dir}/${p}` : p);\n const direct = rel(token.replace(/^\\.\\//, ''));\n if (!OUTPUT_DIR.test(token) && files.has(direct)) return direct;\n if (OUTPUT_DIR.test(token)) {\n // `node dist/app/server.js` runs the compiled form of src/app/server.ts\n const stem = token.replace(OUTPUT_DIR, '').replace(/\\.[cm]?js$/, '');\n for (const ext of SOURCE_EXT) {\n const candidate = rel(`src/${stem}${ext}`);\n if (files.has(candidate)) return candidate;\n }\n }\n return null;\n}\n\n/**\n * Entry points that a package itself declares: files named by its start-like scripts, its `main` and its `bin`.\n * A file is never an entry point just because it is called server.ts, worker.ts or index.ts.\n */\nexport function discoverEntryPoints(packages: { dir: string; pkg: PackageInfo; manifest: string }[], files: Set<string>): EntryPoint[] {\n const found = new Map<string, EntryPoint>();\n const add = (dir: string, projectRel: string, kind: EntryKind, evidence: string): void => {\n const rel = dir ? projectRel.slice(dir.length + 1) : projectRel;\n const key = `${dir}\\u0000${rel}`;\n const prev = found.get(key);\n // A worker script wins over a generic server label for the same file.\n if (!prev || (kind === 'worker' && prev.kind !== 'worker')) found.set(key, { path: rel, cwd: dir || '.', kind, evidence });\n };\n\n for (const { dir, pkg, manifest } of packages) {\n for (const [name, body] of Object.entries(pkg.scripts)) {\n if (!RUN_SCRIPT.test(name)) continue;\n for (const m of body.matchAll(FILE_TOKEN)) {\n const token = m[1]!;\n if (token.startsWith('-') || NOT_AN_ENTRY.test(token.replace(OUTPUT_DIR, 'x/'))) continue;\n const hit = resolveIn(files, dir, token);\n if (!hit) continue;\n add(dir, hit, WORKER_HINT.test(name) || WORKER_HINT.test(path.posix.basename(hit)) ? 'worker' : 'server', `${manifest}#scripts.${name}`);\n }\n }\n if (pkg.main) {\n const hit = resolveIn(files, dir, pkg.main);\n if (hit && !NOT_AN_ENTRY.test(hit)) add(dir, hit, 'main', `${manifest}#main`);\n }\n for (const bin of pkg.bin ?? []) {\n const hit = resolveIn(files, dir, bin);\n if (hit && !NOT_AN_ENTRY.test(hit)) add(dir, hit, 'cli', `${manifest}#bin`);\n }\n }\n const order: Record<EntryKind, number> = { server: 0, worker: 1, main: 2, cli: 3 };\n return [...found.values()].sort((a, b) => a.cwd.localeCompare(b.cwd) || order[a.kind] - order[b.kind] || a.path.localeCompare(b.path)).slice(0, 60);\n}\n"]}
|
|
@@ -19,7 +19,29 @@ export declare function saveFacts(ws: Ws, file: FactsFile): Promise<void>;
|
|
|
19
19
|
*/
|
|
20
20
|
export declare function authorityRank(f: Pick<Fact, 'status' | 'sourceType'>): number;
|
|
21
21
|
export declare function factId(key: string): string;
|
|
22
|
-
export declare
|
|
22
|
+
export declare const DISCOVERY_FILE = ".devworkspace/analysis/discovery.json";
|
|
23
|
+
export declare const isGeneratedSource: (rel: string) => boolean;
|
|
24
|
+
export declare function sectionForKey(key: string): string | null;
|
|
25
|
+
/** One piece of evidence a fact depends on. `hashKey` is how its fingerprint is stored in `sourceHashes`. */
|
|
26
|
+
export interface Dependency {
|
|
27
|
+
hashKey: string;
|
|
28
|
+
file: string;
|
|
29
|
+
section?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Turn a fact's cited sources into the evidence it really depends on:
|
|
33
|
+
* - `file` is the file's content hash;
|
|
34
|
+
* - `discovery.json#section` is the fingerprint of that one section of the generated discovery report;
|
|
35
|
+
* - a bare generated file is not evidence by itself. discovery.json narrows to the section named by the fact's key
|
|
36
|
+
* when there is one; any other generated file is ignored (and listed in `ignored`) instead of turning every
|
|
37
|
+
* re-analysis into staleness.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveDependencies(factKey: string, sources: string[]): {
|
|
40
|
+
deps: Dependency[];
|
|
41
|
+
ignored: string[];
|
|
42
|
+
};
|
|
43
|
+
export declare function hashDependencies(root: string, deps: Dependency[], cache?: Map<string, string>): Promise<Record<string, string>>;
|
|
44
|
+
export declare function hashSources(root: string, sources: string[], cache?: Map<string, string>, factKey?: string): Promise<Record<string, string>>;
|
|
23
45
|
export declare function buildFact(root: string, input: FactInput, now: string, existing?: Fact): Promise<Fact>;
|
|
24
46
|
export type UpsertAction = 'created' | 'updated' | 'unchanged';
|
|
25
47
|
export declare function upsertFact(file: FactsFile, fact: Fact): UpsertAction;
|
|
@@ -35,9 +57,44 @@ export interface StalenessReport {
|
|
|
35
57
|
key: string;
|
|
36
58
|
reason: string;
|
|
37
59
|
}[];
|
|
60
|
+
/** Facts citing a generated file that cannot serve as evidence (no section applies): they are never marked stale by it. */
|
|
61
|
+
unscoped: {
|
|
62
|
+
key: string;
|
|
63
|
+
source: string;
|
|
64
|
+
}[];
|
|
38
65
|
}
|
|
39
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Compare stored evidence fingerprints with the repository now. Mutates facts that became stale.
|
|
68
|
+
* A dependency with no stored fingerprint (recorded before it was tracked) adopts the current one instead of going
|
|
69
|
+
* stale: "unknown" is not evidence of change.
|
|
70
|
+
*/
|
|
40
71
|
export declare function applyStaleness(root: string, file: FactsFile, now: string): Promise<StalenessReport>;
|
|
72
|
+
export interface StalenessView {
|
|
73
|
+
/** Already marked STALE in facts.json. */
|
|
74
|
+
recorded: {
|
|
75
|
+
key: string;
|
|
76
|
+
reason: string;
|
|
77
|
+
}[];
|
|
78
|
+
/** Not yet marked, but evidence changed since the fact was recorded (found by re-checking now). */
|
|
79
|
+
detected: {
|
|
80
|
+
key: string;
|
|
81
|
+
reason: string;
|
|
82
|
+
}[];
|
|
83
|
+
needsReview: {
|
|
84
|
+
key: string;
|
|
85
|
+
reason: string;
|
|
86
|
+
}[];
|
|
87
|
+
unscoped: {
|
|
88
|
+
key: string;
|
|
89
|
+
source: string;
|
|
90
|
+
}[];
|
|
91
|
+
total: number;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Read-only staleness: the single answer that `status`, `refresh --dry-run` and `doctor` all report. Detector facts
|
|
95
|
+
* are excluded because every analyze/refresh re-derives them. Nothing is written.
|
|
96
|
+
*/
|
|
97
|
+
export declare function evaluateStaleness(root: string, file: FactsFile, now: string): Promise<StalenessView>;
|
|
41
98
|
/** Re-hash a fact's sources and mark it verified (called after the agent re-checks it against the repository). */
|
|
42
99
|
export declare function verifyFact(root: string, file: FactsFile, key: string, now: string): Promise<Fact>;
|
|
43
100
|
export declare function removeFact(file: FactsFile, key: string): boolean;
|
package/dist/knowledge/facts.js
CHANGED
|
@@ -2,7 +2,7 @@ import { promises as fs } from 'node:fs';
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { DevWorkspaceError } from '../errors/index.js';
|
|
4
4
|
import { SCHEMA_VERSION, validateFacts, } from '../config/schemas.js';
|
|
5
|
-
import { pathExists, sha256File } from '../utils/fs.js';
|
|
5
|
+
import { pathExists, readJsonFile, sha256File, sha256Text } from '../utils/fs.js';
|
|
6
6
|
import { findAbsolutePaths, resolveInside, validateRelativePath } from '../utils/paths.js';
|
|
7
7
|
import { containsSecret } from '../security/redact.js';
|
|
8
8
|
import { isSensitiveFile } from '../security/files.js';
|
|
@@ -45,25 +45,90 @@ function defaultStatus(t) {
|
|
|
45
45
|
export function factId(key) {
|
|
46
46
|
return 'f-' + createHash('sha256').update(key).digest('hex').slice(0, 10);
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
/** Files DevWorkspace itself regenerates. Their bytes change on every analysis, so they are never evidence as a whole. */
|
|
49
|
+
const GENERATED_PREFIXES = ['.devworkspace/analysis/', '.devworkspace/state/', '.devworkspace/local/'];
|
|
50
|
+
export const DISCOVERY_FILE = '.devworkspace/analysis/discovery.json';
|
|
51
|
+
export const isGeneratedSource = (rel) => GENERATED_PREFIXES.some((p) => rel.startsWith(p));
|
|
52
|
+
/** Fact-key words that name the discovery.json section a fact is about (so "ci.none" depends on the ci section only). */
|
|
53
|
+
const SECTION_BY_WORD = {
|
|
54
|
+
ci: 'ci',
|
|
55
|
+
docker: 'docker',
|
|
56
|
+
deploy: 'deployment',
|
|
57
|
+
deployment: 'deployment',
|
|
58
|
+
docs: 'docs',
|
|
59
|
+
doc: 'docs',
|
|
60
|
+
readme: 'docs',
|
|
61
|
+
entry: 'entryPoints',
|
|
62
|
+
entrypoints: 'entryPoints',
|
|
63
|
+
monorepo: 'monorepo',
|
|
64
|
+
packages: 'monorepo',
|
|
65
|
+
frameworks: 'frameworks',
|
|
66
|
+
languages: 'languages',
|
|
67
|
+
commands: 'commands',
|
|
68
|
+
};
|
|
69
|
+
export function sectionForKey(key) {
|
|
70
|
+
for (const w of key.toLowerCase().split(/[._-]/))
|
|
71
|
+
if (SECTION_BY_WORD[w])
|
|
72
|
+
return SECTION_BY_WORD[w];
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Turn a fact's cited sources into the evidence it really depends on:
|
|
77
|
+
* - `file` is the file's content hash;
|
|
78
|
+
* - `discovery.json#section` is the fingerprint of that one section of the generated discovery report;
|
|
79
|
+
* - a bare generated file is not evidence by itself. discovery.json narrows to the section named by the fact's key
|
|
80
|
+
* when there is one; any other generated file is ignored (and listed in `ignored`) instead of turning every
|
|
81
|
+
* re-analysis into staleness.
|
|
82
|
+
*/
|
|
83
|
+
export function resolveDependencies(factKey, sources) {
|
|
84
|
+
const deps = [];
|
|
85
|
+
const ignored = [];
|
|
50
86
|
for (const s of sources) {
|
|
51
|
-
|
|
87
|
+
const hash = s.indexOf('#');
|
|
88
|
+
if (hash > 0) {
|
|
89
|
+
deps.push({ hashKey: s, file: s.slice(0, hash), section: s.slice(hash + 1) });
|
|
90
|
+
}
|
|
91
|
+
else if (isGeneratedSource(s)) {
|
|
92
|
+
const section = s === DISCOVERY_FILE ? sectionForKey(factKey) : null;
|
|
93
|
+
if (section)
|
|
94
|
+
deps.push({ hashKey: `${s}#${section}`, file: s, section });
|
|
95
|
+
else
|
|
96
|
+
ignored.push(s);
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
deps.push({ hashKey: s, file: s });
|
|
100
|
+
}
|
|
101
|
+
return { deps, ignored };
|
|
102
|
+
}
|
|
103
|
+
export async function hashDependencies(root, deps, cache = new Map()) {
|
|
104
|
+
const out = {};
|
|
105
|
+
for (const d of deps) {
|
|
106
|
+
let h = cache.get(d.hashKey);
|
|
52
107
|
if (h === undefined) {
|
|
53
|
-
const full = resolveInside(root,
|
|
108
|
+
const full = resolveInside(root, d.file);
|
|
54
109
|
try {
|
|
55
110
|
const st = await fs.lstat(full);
|
|
56
|
-
|
|
111
|
+
if (!st.isFile())
|
|
112
|
+
h = MISSING_HASH;
|
|
113
|
+
else if (d.section === undefined)
|
|
114
|
+
h = await sha256File(full);
|
|
115
|
+
else {
|
|
116
|
+
const json = (await readJsonFile(full));
|
|
117
|
+
h = d.section in json ? sha256Text(JSON.stringify(json[d.section])) : MISSING_HASH;
|
|
118
|
+
}
|
|
57
119
|
}
|
|
58
120
|
catch {
|
|
59
121
|
h = MISSING_HASH;
|
|
60
122
|
}
|
|
61
|
-
cache.set(
|
|
123
|
+
cache.set(d.hashKey, h);
|
|
62
124
|
}
|
|
63
|
-
out[
|
|
125
|
+
out[d.hashKey] = h;
|
|
64
126
|
}
|
|
65
127
|
return out;
|
|
66
128
|
}
|
|
129
|
+
export async function hashSources(root, sources, cache = new Map(), factKey = '') {
|
|
130
|
+
return hashDependencies(root, resolveDependencies(factKey, sources).deps, cache);
|
|
131
|
+
}
|
|
67
132
|
const KEY_RE = /^[a-z0-9][a-z0-9._-]{0,99}$/;
|
|
68
133
|
export async function buildFact(root, input, now, existing) {
|
|
69
134
|
if (!KEY_RE.test(input.key)) {
|
|
@@ -95,8 +160,8 @@ export async function buildFact(root, input, now, existing) {
|
|
|
95
160
|
throw new DevWorkspaceError('SECURITY', `Fact source "${s}" is a sensitive file.`);
|
|
96
161
|
}
|
|
97
162
|
const status = input.status ?? defaultStatus(input.sourceType);
|
|
98
|
-
const hashes = await hashSources(root, sources);
|
|
99
|
-
const missing =
|
|
163
|
+
const hashes = await hashSources(root, sources, new Map(), input.key);
|
|
164
|
+
const missing = Object.entries(hashes).filter(([, h]) => h === MISSING_HASH).map(([k]) => k);
|
|
100
165
|
if (missing.length > 0 && input.sourceType !== 'user') {
|
|
101
166
|
throw new DevWorkspaceError('NOT_FOUND', `Fact source file(s) not found: ${missing.join(', ')}`, {
|
|
102
167
|
hint: 'Cite files that exist in the repository.',
|
|
@@ -140,10 +205,19 @@ export function upsertFact(file, fact) {
|
|
|
140
205
|
file.facts[i] = fact;
|
|
141
206
|
return 'updated';
|
|
142
207
|
}
|
|
143
|
-
|
|
208
|
+
const describeChange = (hashKey, now) => {
|
|
209
|
+
const hash = hashKey.indexOf('#');
|
|
210
|
+
const where = hash > 0 ? `${hashKey.slice(0, hash)} (section "${hashKey.slice(hash + 1)}")` : hashKey;
|
|
211
|
+
return `${where} ${now === MISSING_HASH ? 'removed' : 'changed'}`;
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* Compare stored evidence fingerprints with the repository now. Mutates facts that became stale.
|
|
215
|
+
* A dependency with no stored fingerprint (recorded before it was tracked) adopts the current one instead of going
|
|
216
|
+
* stale: "unknown" is not evidence of change.
|
|
217
|
+
*/
|
|
144
218
|
export async function applyStaleness(root, file, now) {
|
|
145
219
|
const cache = new Map();
|
|
146
|
-
const report = { checked: 0, newlyStale: [], alreadyStale: [], needsReview: [] };
|
|
220
|
+
const report = { checked: 0, newlyStale: [], alreadyStale: [], needsReview: [], unscoped: [] };
|
|
147
221
|
for (const f of file.facts) {
|
|
148
222
|
if (f.status === 'STALE') {
|
|
149
223
|
report.alreadyStale.push(f.key);
|
|
@@ -151,12 +225,24 @@ export async function applyStaleness(root, file, now) {
|
|
|
151
225
|
}
|
|
152
226
|
if (f.sources.length === 0)
|
|
153
227
|
continue;
|
|
228
|
+
const { deps, ignored } = resolveDependencies(f.key, f.sources);
|
|
229
|
+
for (const source of ignored)
|
|
230
|
+
report.unscoped.push({ key: f.key, source });
|
|
231
|
+
if (deps.length === 0)
|
|
232
|
+
continue;
|
|
154
233
|
report.checked++;
|
|
155
|
-
const current = await
|
|
156
|
-
const changed =
|
|
234
|
+
const current = await hashDependencies(root, deps, cache);
|
|
235
|
+
const changed = [];
|
|
236
|
+
for (const d of deps) {
|
|
237
|
+
const stored = f.sourceHashes[d.hashKey];
|
|
238
|
+
if (stored === undefined)
|
|
239
|
+
f.sourceHashes[d.hashKey] = current[d.hashKey];
|
|
240
|
+
else if (stored !== current[d.hashKey])
|
|
241
|
+
changed.push(describeChange(d.hashKey, current[d.hashKey]));
|
|
242
|
+
}
|
|
157
243
|
if (changed.length === 0)
|
|
158
244
|
continue;
|
|
159
|
-
const reason = changed.
|
|
245
|
+
const reason = changed.join('; ');
|
|
160
246
|
if (f.status === 'USER_DECISION') {
|
|
161
247
|
report.needsReview.push({ key: f.key, reason });
|
|
162
248
|
continue;
|
|
@@ -168,13 +254,25 @@ export async function applyStaleness(root, file, now) {
|
|
|
168
254
|
}
|
|
169
255
|
return report;
|
|
170
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Read-only staleness: the single answer that `status`, `refresh --dry-run` and `doctor` all report. Detector facts
|
|
259
|
+
* are excluded because every analyze/refresh re-derives them. Nothing is written.
|
|
260
|
+
*/
|
|
261
|
+
export async function evaluateStaleness(root, file, now) {
|
|
262
|
+
const probe = structuredClone(file);
|
|
263
|
+
const rep = await applyStaleness(root, probe, now);
|
|
264
|
+
const sourceType = (key) => probe.facts.find((f) => f.key === key)?.sourceType;
|
|
265
|
+
const recorded = file.facts.filter((f) => f.status === 'STALE' && f.sourceType !== 'detector').map((f) => ({ key: f.key, reason: f.staleReason ?? 'evidence changed' }));
|
|
266
|
+
const detected = rep.newlyStale.filter((s) => sourceType(s.key) !== 'detector');
|
|
267
|
+
return { recorded, detected, needsReview: rep.needsReview, unscoped: rep.unscoped, total: recorded.length + detected.length };
|
|
268
|
+
}
|
|
171
269
|
/** Re-hash a fact's sources and mark it verified (called after the agent re-checks it against the repository). */
|
|
172
270
|
export async function verifyFact(root, file, key, now) {
|
|
173
271
|
const f = file.facts.find((x) => x.key === key);
|
|
174
272
|
if (!f)
|
|
175
273
|
throw new DevWorkspaceError('NOT_FOUND', `No fact with key "${key}".`);
|
|
176
|
-
f.sourceHashes = await hashSources(root, f.sources);
|
|
177
|
-
const gone = f.
|
|
274
|
+
f.sourceHashes = await hashSources(root, f.sources, new Map(), f.key);
|
|
275
|
+
const gone = Object.entries(f.sourceHashes).filter(([, h]) => h === MISSING_HASH).map(([k]) => k);
|
|
178
276
|
if (gone.length > 0) {
|
|
179
277
|
throw new DevWorkspaceError('NOT_FOUND', `Cannot verify "${key}": source(s) missing: ${gone.join(', ')}`, {
|
|
180
278
|
hint: 'Update the fact with new sources or remove it.',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"facts.js","sourceRoot":"","sources":["../../src/knowledge/facts.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport { createHash } from 'node:crypto';\nimport { DevWorkspaceError } from '../errors/index.js';\nimport {\n SCHEMA_VERSION,\n validateFacts,\n type Confidence,\n type Fact,\n type FactsFile,\n type FactStatus,\n type SourceType,\n} from '../config/schemas.js';\nimport { pathExists, sha256File } from '../utils/fs.js';\nimport { findAbsolutePaths, resolveInside, validateRelativePath } from '../utils/paths.js';\nimport { containsSecret } from '../security/redact.js';\nimport { isSensitiveFile } from '../security/files.js';\nimport { readVersioned, writeVersioned } from '../core/versioned.js';\nimport type { Ws } from '../core/workspace.js';\n\nexport interface FactInput {\n key: string;\n value: string;\n source: string;\n sourceType: SourceType;\n sources?: string[];\n confidence: Confidence;\n status?: FactStatus;\n}\n\nexport const MISSING_HASH = 'MISSING';\n\nexport function emptyFacts(): FactsFile {\n return { schemaVersion: SCHEMA_VERSION, facts: [] };\n}\n\nexport async function loadFacts(ws: Ws): Promise<FactsFile> {\n if (!(await pathExists(ws.facts))) return emptyFacts();\n return (await readVersioned(ws.facts, 'facts', SCHEMA_VERSION, validateFacts)).data;\n}\n\nexport async function saveFacts(ws: Ws, file: FactsFile): Promise<void> {\n validateFacts(file);\n await writeVersioned(ws, ws.facts, file);\n}\n\n/**\n * Authority rank. Mirrors the hierarchy: user decisions > current repository evidence >\n * verified facts > AI-derived findings > stale/unknown. Lower-ranked input never silently replaces a higher-ranked fact.\n */\nexport function authorityRank(f: Pick<Fact, 'status' | 'sourceType'>): number {\n if (f.status === 'USER_DECISION') return 5;\n if (f.status === 'DETECTED' && f.sourceType !== 'ai') return 4;\n if (f.status === 'VERIFIED') return f.sourceType === 'ai' ? 3 : 4;\n if (f.status === 'INFERRED') return 2;\n return 1;\n}\n\nfunction defaultStatus(t: SourceType): FactStatus {\n if (t === 'user') return 'USER_DECISION';\n if (t === 'ai') return 'INFERRED';\n return 'DETECTED';\n}\n\nexport function factId(key: string): string {\n return 'f-' + createHash('sha256').update(key).digest('hex').slice(0, 10);\n}\n\nexport async function hashSources(root: string, sources: string[], cache: Map<string, string> = new Map()): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const s of sources) {\n let h = cache.get(s);\n if (h === undefined) {\n const full = resolveInside(root, s);\n try {\n const st = await fs.lstat(full);\n h = st.isFile() ? await sha256File(full) : MISSING_HASH;\n } catch {\n h = MISSING_HASH;\n }\n cache.set(s, h);\n }\n out[s] = h;\n }\n return out;\n}\n\nconst KEY_RE = /^[a-z0-9][a-z0-9._-]{0,99}$/;\n\nexport async function buildFact(root: string, input: FactInput, now: string, existing?: Fact): Promise<Fact> {\n if (!KEY_RE.test(input.key)) {\n throw new DevWorkspaceError('USAGE', `Invalid fact key \"${input.key}\".`, {\n hint: 'Use lowercase letters, digits, dots, dashes (e.g. \"auth.session-store\").',\n });\n }\n if (input.value.trim() === '') throw new DevWorkspaceError('USAGE', 'Fact value must not be empty.');\n if (input.value.length > 4000) throw new DevWorkspaceError('USAGE', 'Fact value is too long (max 4000 characters).', { hint: 'Store a concise summary; link to the source file instead.' });\n if (containsSecret(input.value) || containsSecret(input.source)) {\n throw new DevWorkspaceError('SECURITY', 'Fact looks like it contains a secret; refusing to store it.', {\n hint: 'Describe where the secret is configured, never its value.',\n });\n }\n const abs = findAbsolutePaths(input.value);\n if (abs.length > 0) {\n throw new DevWorkspaceError('UNSAFE_PATH', `Fact value contains a machine-specific absolute path (${abs[0]}).`, {\n hint: 'Use project-relative paths so knowledge stays portable.',\n });\n }\n const sources = [...new Set(input.sources ?? [])];\n for (const s of sources) {\n const err = validateRelativePath(s);\n if (err) throw new DevWorkspaceError('UNSAFE_PATH', `Invalid fact source \"${s}\" (${err}).`);\n if (isSensitiveFile(s)) throw new DevWorkspaceError('SECURITY', `Fact source \"${s}\" is a sensitive file.`);\n }\n const status = input.status ?? defaultStatus(input.sourceType);\n const hashes = await hashSources(root, sources);\n const missing = sources.filter((s) => hashes[s] === MISSING_HASH);\n if (missing.length > 0 && input.sourceType !== 'user') {\n throw new DevWorkspaceError('NOT_FOUND', `Fact source file(s) not found: ${missing.join(', ')}`, {\n hint: 'Cite files that exist in the repository.',\n });\n }\n return {\n id: factId(input.key),\n key: input.key,\n value: input.value.trim(),\n source: input.source,\n sourceType: input.sourceType,\n sources,\n sourceHashes: hashes,\n confidence: input.confidence,\n status,\n createdAt: existing?.createdAt ?? now,\n updatedAt: now,\n lastVerifiedAt: status === 'VERIFIED' || status === 'DETECTED' || status === 'USER_DECISION' ? now : null,\n };\n}\n\nexport type UpsertAction = 'created' | 'updated' | 'unchanged';\n\nexport function upsertFact(file: FactsFile, fact: Fact): UpsertAction {\n const i = file.facts.findIndex((f) => f.key === fact.key);\n if (i === -1) {\n file.facts.push(fact);\n return 'created';\n }\n const existing = file.facts[i]!;\n if (existing.status !== 'STALE' && authorityRank(fact) < authorityRank(existing)) {\n throw new DevWorkspaceError('CONFLICT', `Fact \"${fact.key}\" is held at higher authority (${existing.status}); refusing to overwrite it.`, {\n hint: 'Ask the user to confirm the change, then record it as a user decision.',\n });\n }\n fact.createdAt = existing.createdAt;\n const same =\n existing.value === fact.value &&\n existing.status === fact.status &&\n existing.confidence === fact.confidence &&\n JSON.stringify(existing.sourceHashes) === JSON.stringify(fact.sourceHashes);\n // Leave an identical fact byte-for-byte alone so repeated analysis never churns version control.\n if (same) return 'unchanged';\n file.facts[i] = fact;\n return 'updated';\n}\n\nexport interface StalenessReport {\n checked: number;\n newlyStale: { key: string; reason: string }[];\n alreadyStale: string[];\n /** User decisions whose sources changed. They are never auto-staled; a human should review them. */\n needsReview: { key: string; reason: string }[];\n}\n\n/** Compare stored source hashes with the repository now. Mutates facts that became stale. */\nexport async function applyStaleness(root: string, file: FactsFile, now: string): Promise<StalenessReport> {\n const cache = new Map<string, string>();\n const report: StalenessReport = { checked: 0, newlyStale: [], alreadyStale: [], needsReview: [] };\n for (const f of file.facts) {\n if (f.status === 'STALE') {\n report.alreadyStale.push(f.key);\n continue;\n }\n if (f.sources.length === 0) continue;\n report.checked++;\n const current = await hashSources(root, f.sources, cache);\n const changed = f.sources.filter((s) => current[s] !== f.sourceHashes[s]);\n if (changed.length === 0) continue;\n const reason = changed.map((s) => `${s} ${current[s] === MISSING_HASH ? 'removed' : 'changed'}`).join('; ');\n if (f.status === 'USER_DECISION') {\n report.needsReview.push({ key: f.key, reason });\n continue;\n }\n f.status = 'STALE';\n f.staleReason = reason;\n f.updatedAt = now;\n report.newlyStale.push({ key: f.key, reason });\n }\n return report;\n}\n\n/** Re-hash a fact's sources and mark it verified (called after the agent re-checks it against the repository). */\nexport async function verifyFact(root: string, file: FactsFile, key: string, now: string): Promise<Fact> {\n const f = file.facts.find((x) => x.key === key);\n if (!f) throw new DevWorkspaceError('NOT_FOUND', `No fact with key \"${key}\".`);\n f.sourceHashes = await hashSources(root, f.sources);\n const gone = f.sources.filter((s) => f.sourceHashes[s] === MISSING_HASH);\n if (gone.length > 0) {\n throw new DevWorkspaceError('NOT_FOUND', `Cannot verify \"${key}\": source(s) missing: ${gone.join(', ')}`, {\n hint: 'Update the fact with new sources or remove it.',\n });\n }\n if (f.status !== 'USER_DECISION') f.status = f.sourceType === 'detector' ? 'DETECTED' : 'VERIFIED';\n delete f.staleReason;\n f.lastVerifiedAt = now;\n f.updatedAt = now;\n return f;\n}\n\nexport function removeFact(file: FactsFile, key: string): boolean {\n const n = file.facts.length;\n file.facts = file.facts.filter((f) => f.key !== key);\n return file.facts.length !== n;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"facts.js","sourceRoot":"","sources":["../../src/knowledge/facts.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport { createHash } from 'node:crypto';\nimport { DevWorkspaceError } from '../errors/index.js';\nimport {\n SCHEMA_VERSION,\n validateFacts,\n type Confidence,\n type Fact,\n type FactsFile,\n type FactStatus,\n type SourceType,\n} from '../config/schemas.js';\nimport { pathExists, readJsonFile, sha256File, sha256Text } from '../utils/fs.js';\nimport { findAbsolutePaths, resolveInside, validateRelativePath } from '../utils/paths.js';\nimport { containsSecret } from '../security/redact.js';\nimport { isSensitiveFile } from '../security/files.js';\nimport { readVersioned, writeVersioned } from '../core/versioned.js';\nimport type { Ws } from '../core/workspace.js';\n\nexport interface FactInput {\n key: string;\n value: string;\n source: string;\n sourceType: SourceType;\n sources?: string[];\n confidence: Confidence;\n status?: FactStatus;\n}\n\nexport const MISSING_HASH = 'MISSING';\n\nexport function emptyFacts(): FactsFile {\n return { schemaVersion: SCHEMA_VERSION, facts: [] };\n}\n\nexport async function loadFacts(ws: Ws): Promise<FactsFile> {\n if (!(await pathExists(ws.facts))) return emptyFacts();\n return (await readVersioned(ws.facts, 'facts', SCHEMA_VERSION, validateFacts)).data;\n}\n\nexport async function saveFacts(ws: Ws, file: FactsFile): Promise<void> {\n validateFacts(file);\n await writeVersioned(ws, ws.facts, file);\n}\n\n/**\n * Authority rank. Mirrors the hierarchy: user decisions > current repository evidence >\n * verified facts > AI-derived findings > stale/unknown. Lower-ranked input never silently replaces a higher-ranked fact.\n */\nexport function authorityRank(f: Pick<Fact, 'status' | 'sourceType'>): number {\n if (f.status === 'USER_DECISION') return 5;\n if (f.status === 'DETECTED' && f.sourceType !== 'ai') return 4;\n if (f.status === 'VERIFIED') return f.sourceType === 'ai' ? 3 : 4;\n if (f.status === 'INFERRED') return 2;\n return 1;\n}\n\nfunction defaultStatus(t: SourceType): FactStatus {\n if (t === 'user') return 'USER_DECISION';\n if (t === 'ai') return 'INFERRED';\n return 'DETECTED';\n}\n\nexport function factId(key: string): string {\n return 'f-' + createHash('sha256').update(key).digest('hex').slice(0, 10);\n}\n\n/** Files DevWorkspace itself regenerates. Their bytes change on every analysis, so they are never evidence as a whole. */\nconst GENERATED_PREFIXES = ['.devworkspace/analysis/', '.devworkspace/state/', '.devworkspace/local/'];\nexport const DISCOVERY_FILE = '.devworkspace/analysis/discovery.json';\n\nexport const isGeneratedSource = (rel: string): boolean => GENERATED_PREFIXES.some((p) => rel.startsWith(p));\n\n/** Fact-key words that name the discovery.json section a fact is about (so \"ci.none\" depends on the ci section only). */\nconst SECTION_BY_WORD: Record<string, string> = {\n ci: 'ci',\n docker: 'docker',\n deploy: 'deployment',\n deployment: 'deployment',\n docs: 'docs',\n doc: 'docs',\n readme: 'docs',\n entry: 'entryPoints',\n entrypoints: 'entryPoints',\n monorepo: 'monorepo',\n packages: 'monorepo',\n frameworks: 'frameworks',\n languages: 'languages',\n commands: 'commands',\n};\n\nexport function sectionForKey(key: string): string | null {\n for (const w of key.toLowerCase().split(/[._-]/)) if (SECTION_BY_WORD[w]) return SECTION_BY_WORD[w]!;\n return null;\n}\n\n/** One piece of evidence a fact depends on. `hashKey` is how its fingerprint is stored in `sourceHashes`. */\nexport interface Dependency {\n hashKey: string;\n file: string;\n section?: string;\n}\n\n/**\n * Turn a fact's cited sources into the evidence it really depends on:\n * - `file` is the file's content hash;\n * - `discovery.json#section` is the fingerprint of that one section of the generated discovery report;\n * - a bare generated file is not evidence by itself. discovery.json narrows to the section named by the fact's key\n * when there is one; any other generated file is ignored (and listed in `ignored`) instead of turning every\n * re-analysis into staleness.\n */\nexport function resolveDependencies(factKey: string, sources: string[]): { deps: Dependency[]; ignored: string[] } {\n const deps: Dependency[] = [];\n const ignored: string[] = [];\n for (const s of sources) {\n const hash = s.indexOf('#');\n if (hash > 0) {\n deps.push({ hashKey: s, file: s.slice(0, hash), section: s.slice(hash + 1) });\n } else if (isGeneratedSource(s)) {\n const section = s === DISCOVERY_FILE ? sectionForKey(factKey) : null;\n if (section) deps.push({ hashKey: `${s}#${section}`, file: s, section });\n else ignored.push(s);\n } else deps.push({ hashKey: s, file: s });\n }\n return { deps, ignored };\n}\n\nexport async function hashDependencies(root: string, deps: Dependency[], cache: Map<string, string> = new Map()): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const d of deps) {\n let h = cache.get(d.hashKey);\n if (h === undefined) {\n const full = resolveInside(root, d.file);\n try {\n const st = await fs.lstat(full);\n if (!st.isFile()) h = MISSING_HASH;\n else if (d.section === undefined) h = await sha256File(full);\n else {\n const json = (await readJsonFile(full)) as Record<string, unknown>;\n h = d.section in json ? sha256Text(JSON.stringify(json[d.section])) : MISSING_HASH;\n }\n } catch {\n h = MISSING_HASH;\n }\n cache.set(d.hashKey, h);\n }\n out[d.hashKey] = h;\n }\n return out;\n}\n\nexport async function hashSources(root: string, sources: string[], cache: Map<string, string> = new Map(), factKey = ''): Promise<Record<string, string>> {\n return hashDependencies(root, resolveDependencies(factKey, sources).deps, cache);\n}\n\nconst KEY_RE = /^[a-z0-9][a-z0-9._-]{0,99}$/;\n\nexport async function buildFact(root: string, input: FactInput, now: string, existing?: Fact): Promise<Fact> {\n if (!KEY_RE.test(input.key)) {\n throw new DevWorkspaceError('USAGE', `Invalid fact key \"${input.key}\".`, {\n hint: 'Use lowercase letters, digits, dots, dashes (e.g. \"auth.session-store\").',\n });\n }\n if (input.value.trim() === '') throw new DevWorkspaceError('USAGE', 'Fact value must not be empty.');\n if (input.value.length > 4000) throw new DevWorkspaceError('USAGE', 'Fact value is too long (max 4000 characters).', { hint: 'Store a concise summary; link to the source file instead.' });\n if (containsSecret(input.value) || containsSecret(input.source)) {\n throw new DevWorkspaceError('SECURITY', 'Fact looks like it contains a secret; refusing to store it.', {\n hint: 'Describe where the secret is configured, never its value.',\n });\n }\n const abs = findAbsolutePaths(input.value);\n if (abs.length > 0) {\n throw new DevWorkspaceError('UNSAFE_PATH', `Fact value contains a machine-specific absolute path (${abs[0]}).`, {\n hint: 'Use project-relative paths so knowledge stays portable.',\n });\n }\n const sources = [...new Set(input.sources ?? [])];\n for (const s of sources) {\n const err = validateRelativePath(s);\n if (err) throw new DevWorkspaceError('UNSAFE_PATH', `Invalid fact source \"${s}\" (${err}).`);\n if (isSensitiveFile(s)) throw new DevWorkspaceError('SECURITY', `Fact source \"${s}\" is a sensitive file.`);\n }\n const status = input.status ?? defaultStatus(input.sourceType);\n const hashes = await hashSources(root, sources, new Map(), input.key);\n const missing = Object.entries(hashes).filter(([, h]) => h === MISSING_HASH).map(([k]) => k);\n if (missing.length > 0 && input.sourceType !== 'user') {\n throw new DevWorkspaceError('NOT_FOUND', `Fact source file(s) not found: ${missing.join(', ')}`, {\n hint: 'Cite files that exist in the repository.',\n });\n }\n return {\n id: factId(input.key),\n key: input.key,\n value: input.value.trim(),\n source: input.source,\n sourceType: input.sourceType,\n sources,\n sourceHashes: hashes,\n confidence: input.confidence,\n status,\n createdAt: existing?.createdAt ?? now,\n updatedAt: now,\n lastVerifiedAt: status === 'VERIFIED' || status === 'DETECTED' || status === 'USER_DECISION' ? now : null,\n };\n}\n\nexport type UpsertAction = 'created' | 'updated' | 'unchanged';\n\nexport function upsertFact(file: FactsFile, fact: Fact): UpsertAction {\n const i = file.facts.findIndex((f) => f.key === fact.key);\n if (i === -1) {\n file.facts.push(fact);\n return 'created';\n }\n const existing = file.facts[i]!;\n if (existing.status !== 'STALE' && authorityRank(fact) < authorityRank(existing)) {\n throw new DevWorkspaceError('CONFLICT', `Fact \"${fact.key}\" is held at higher authority (${existing.status}); refusing to overwrite it.`, {\n hint: 'Ask the user to confirm the change, then record it as a user decision.',\n });\n }\n fact.createdAt = existing.createdAt;\n const same =\n existing.value === fact.value &&\n existing.status === fact.status &&\n existing.confidence === fact.confidence &&\n JSON.stringify(existing.sourceHashes) === JSON.stringify(fact.sourceHashes);\n // Leave an identical fact byte-for-byte alone so repeated analysis never churns version control.\n if (same) return 'unchanged';\n file.facts[i] = fact;\n return 'updated';\n}\n\nexport interface StalenessReport {\n checked: number;\n newlyStale: { key: string; reason: string }[];\n alreadyStale: string[];\n /** User decisions whose sources changed. They are never auto-staled; a human should review them. */\n needsReview: { key: string; reason: string }[];\n /** Facts citing a generated file that cannot serve as evidence (no section applies): they are never marked stale by it. */\n unscoped: { key: string; source: string }[];\n}\n\nconst describeChange = (hashKey: string, now: string): string => {\n const hash = hashKey.indexOf('#');\n const where = hash > 0 ? `${hashKey.slice(0, hash)} (section \"${hashKey.slice(hash + 1)}\")` : hashKey;\n return `${where} ${now === MISSING_HASH ? 'removed' : 'changed'}`;\n};\n\n/**\n * Compare stored evidence fingerprints with the repository now. Mutates facts that became stale.\n * A dependency with no stored fingerprint (recorded before it was tracked) adopts the current one instead of going\n * stale: \"unknown\" is not evidence of change.\n */\nexport async function applyStaleness(root: string, file: FactsFile, now: string): Promise<StalenessReport> {\n const cache = new Map<string, string>();\n const report: StalenessReport = { checked: 0, newlyStale: [], alreadyStale: [], needsReview: [], unscoped: [] };\n for (const f of file.facts) {\n if (f.status === 'STALE') {\n report.alreadyStale.push(f.key);\n continue;\n }\n if (f.sources.length === 0) continue;\n const { deps, ignored } = resolveDependencies(f.key, f.sources);\n for (const source of ignored) report.unscoped.push({ key: f.key, source });\n if (deps.length === 0) continue;\n report.checked++;\n const current = await hashDependencies(root, deps, cache);\n const changed: string[] = [];\n for (const d of deps) {\n const stored = f.sourceHashes[d.hashKey];\n if (stored === undefined) f.sourceHashes[d.hashKey] = current[d.hashKey]!;\n else if (stored !== current[d.hashKey]) changed.push(describeChange(d.hashKey, current[d.hashKey]!));\n }\n if (changed.length === 0) continue;\n const reason = changed.join('; ');\n if (f.status === 'USER_DECISION') {\n report.needsReview.push({ key: f.key, reason });\n continue;\n }\n f.status = 'STALE';\n f.staleReason = reason;\n f.updatedAt = now;\n report.newlyStale.push({ key: f.key, reason });\n }\n return report;\n}\n\nexport interface StalenessView {\n /** Already marked STALE in facts.json. */\n recorded: { key: string; reason: string }[];\n /** Not yet marked, but evidence changed since the fact was recorded (found by re-checking now). */\n detected: { key: string; reason: string }[];\n needsReview: { key: string; reason: string }[];\n unscoped: { key: string; source: string }[];\n total: number;\n}\n\n/**\n * Read-only staleness: the single answer that `status`, `refresh --dry-run` and `doctor` all report. Detector facts\n * are excluded because every analyze/refresh re-derives them. Nothing is written.\n */\nexport async function evaluateStaleness(root: string, file: FactsFile, now: string): Promise<StalenessView> {\n const probe = structuredClone(file);\n const rep = await applyStaleness(root, probe, now);\n const sourceType = (key: string): SourceType | undefined => probe.facts.find((f) => f.key === key)?.sourceType;\n const recorded = file.facts.filter((f) => f.status === 'STALE' && f.sourceType !== 'detector').map((f) => ({ key: f.key, reason: f.staleReason ?? 'evidence changed' }));\n const detected = rep.newlyStale.filter((s) => sourceType(s.key) !== 'detector');\n return { recorded, detected, needsReview: rep.needsReview, unscoped: rep.unscoped, total: recorded.length + detected.length };\n}\n\n/** Re-hash a fact's sources and mark it verified (called after the agent re-checks it against the repository). */\nexport async function verifyFact(root: string, file: FactsFile, key: string, now: string): Promise<Fact> {\n const f = file.facts.find((x) => x.key === key);\n if (!f) throw new DevWorkspaceError('NOT_FOUND', `No fact with key \"${key}\".`);\n f.sourceHashes = await hashSources(root, f.sources, new Map(), f.key);\n const gone = Object.entries(f.sourceHashes).filter(([, h]) => h === MISSING_HASH).map(([k]) => k);\n if (gone.length > 0) {\n throw new DevWorkspaceError('NOT_FOUND', `Cannot verify \"${key}\": source(s) missing: ${gone.join(', ')}`, {\n hint: 'Update the fact with new sources or remove it.',\n });\n }\n if (f.status !== 'USER_DECISION') f.status = f.sourceType === 'detector' ? 'DETECTED' : 'VERIFIED';\n delete f.staleReason;\n f.lastVerifiedAt = now;\n f.updatedAt = now;\n return f;\n}\n\nexport function removeFact(file: FactsFile, key: string): boolean {\n const n = file.facts.length;\n file.facts = file.facts.filter((f) => f.key !== key);\n return file.facts.length !== n;\n}\n"]}
|
|
@@ -10,12 +10,15 @@ export function discoverySummary(input) {
|
|
|
10
10
|
for (const p of d.monorepo.packages.slice(0, 25))
|
|
11
11
|
lines.push(` - ${p.path} (${p.name}${p.packageManager ? `, ${p.packageManager}` : ''})${p.frameworks.length ? ` - ${p.frameworks.join(', ')}` : ''}`);
|
|
12
12
|
lines.push(`- Top-level areas: ${list(d.topDirs.slice(0, 15).map((t) => `${t.name} (${t.files} files)`))}`);
|
|
13
|
-
|
|
13
|
+
const eps = d.entryPoints ?? [];
|
|
14
|
+
lines.push(`- Entry points (declared by package scripts, main or bin): ${eps.length ? eps.slice(0, 20).map((e) => `${e.cwd === '.' ? '' : e.cwd + '/'}${e.path} (${e.kind}) [${e.evidence}]`).join('; ') : 'none detected'}`);
|
|
15
|
+
if (d.entryHints.length > 0)
|
|
16
|
+
lines.push(`- Entry-point hints (unconfirmed names): ${list(d.entryHints)}`);
|
|
14
17
|
lines.push(`- Test configuration: ${list(d.testConfigs)}`);
|
|
15
18
|
lines.push(`- CI: ${list(d.ci.slice(0, 8))}`);
|
|
16
19
|
lines.push(`- Docker/containers: ${list(d.docker)}`);
|
|
17
20
|
lines.push(`- Deployment hints: ${list(d.deployment)}`);
|
|
18
|
-
lines.push(`- Docs: ${list(d.docs.slice(0,
|
|
21
|
+
lines.push(`- Docs: ${list(d.docs.slice(0, 20))}${d.docs.length > 20 ? ` (+${d.docs.length - 20} more in discovery.json)` : ''}`);
|
|
19
22
|
lines.push(`- Existing AI instruction files: ${list(d.aiInstructions)}`);
|
|
20
23
|
if (d.sensitiveFilesPresent.length > 0) {
|
|
21
24
|
lines.push(`- Credential-like files present (names only, contents NOT read and must not be read): ${d.sensitiveFilesPresent.join(', ')}`);
|
|
@@ -28,7 +31,7 @@ export function discoverySummary(input) {
|
|
|
28
31
|
return lines.join('\n');
|
|
29
32
|
}
|
|
30
33
|
export function contractSummary(d) {
|
|
31
|
-
const rows = d.commands.map((c) => `| ${c.id} | ${c.available ? 'Yes' : 'No'} | ${c.cwd ? '`' + c.cwd + '`' : '(root)'} | ${c.command ? '`' + c.command + '`' : '-'} | ${c.required} | ${c.origin} |`);
|
|
34
|
+
const rows = d.commands.map((c) => `| ${c.id} | ${c.available ? 'Yes' : 'No'} | ${c.cwd ? '`' + c.cwd + '`' : '(root)'} | ${c.command ? '`' + c.command + '`' : '-'}${c.mutates ? ' (modifies files; never run by DevWorkspace)' : ''} | ${c.required} | ${c.origin} |`);
|
|
32
35
|
return ['| Check | Available | Run in | Command | Required | Origin |', '| --- | --- | --- | --- | --- | --- |', ...rows].join('\n');
|
|
33
36
|
}
|
|
34
37
|
export function integrationSummary(items) {
|