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.
Files changed (65) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +4 -3
  3. package/dist/agents/claude/content.js +3 -2
  4. package/dist/agents/claude/content.js.map +1 -1
  5. package/dist/cli/args.d.ts +16 -0
  6. package/dist/cli/args.js +5 -0
  7. package/dist/cli/args.js.map +1 -1
  8. package/dist/cli/help.js +30 -7
  9. package/dist/cli/help.js.map +1 -1
  10. package/dist/cli/render.d.ts +7 -1
  11. package/dist/cli/render.js +64 -11
  12. package/dist/cli/render.js.map +1 -1
  13. package/dist/cli/run.js +47 -8
  14. package/dist/cli/run.js.map +1 -1
  15. package/dist/config/schemas.d.ts +5 -0
  16. package/dist/config/schemas.js +2 -0
  17. package/dist/config/schemas.js.map +1 -1
  18. package/dist/discovery/commands.js +17 -3
  19. package/dist/discovery/commands.js.map +1 -1
  20. package/dist/discovery/detect.d.ts +6 -0
  21. package/dist/discovery/detect.js +12 -6
  22. package/dist/discovery/detect.js.map +1 -1
  23. package/dist/discovery/docs.d.ts +10 -0
  24. package/dist/discovery/docs.js +52 -0
  25. package/dist/discovery/docs.js.map +1 -0
  26. package/dist/discovery/entrypoints.d.ts +20 -0
  27. package/dist/discovery/entrypoints.js +67 -0
  28. package/dist/discovery/entrypoints.js.map +1 -0
  29. package/dist/knowledge/facts.d.ts +59 -2
  30. package/dist/knowledge/facts.js +115 -17
  31. package/dist/knowledge/facts.js.map +1 -1
  32. package/dist/prompts/onboarding.js +6 -3
  33. package/dist/prompts/onboarding.js.map +1 -1
  34. package/dist/services/analysis.js +6 -0
  35. package/dist/services/analysis.js.map +1 -1
  36. package/dist/services/doctor.js +14 -3
  37. package/dist/services/doctor.js.map +1 -1
  38. package/dist/services/knowledge.d.ts +17 -0
  39. package/dist/services/knowledge.js +28 -2
  40. package/dist/services/knowledge.js.map +1 -1
  41. package/dist/services/refresh.js +5 -5
  42. package/dist/services/refresh.js.map +1 -1
  43. package/dist/services/status.d.ts +11 -2
  44. package/dist/services/status.js +11 -9
  45. package/dist/services/status.js.map +1 -1
  46. package/dist/services/tasks.d.ts +2 -0
  47. package/dist/services/tasks.js +20 -12
  48. package/dist/services/tasks.js.map +1 -1
  49. package/dist/services/verify.d.ts +47 -0
  50. package/dist/services/verify.js +172 -0
  51. package/dist/services/verify.js.map +1 -0
  52. package/dist/tasks/routing.js +8 -3
  53. package/dist/tasks/routing.js.map +1 -1
  54. package/dist/tasks/task.d.ts +49 -5
  55. package/dist/tasks/task.js +68 -11
  56. package/dist/tasks/task.js.map +1 -1
  57. package/dist/verification/safety.d.ts +38 -0
  58. package/dist/verification/safety.js +120 -0
  59. package/dist/verification/safety.js.map +1 -0
  60. package/dist/verification/scope.d.ts +19 -0
  61. package/dist/verification/scope.js +119 -0
  62. package/dist/verification/scope.js.map +1 -0
  63. package/docs/knowledge.md +2 -0
  64. package/docs/testing.md +25 -2
  65. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docs.js","sourceRoot":"","sources":["../../src/discovery/docs.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,QAAQ,GAAG,oBAAoB,CAAC;AACtC,MAAM,OAAO,GAAG,0DAA0D,CAAC;AAC3E,MAAM,SAAS,GAAG,4EAA4E,CAAC;AAC/F,MAAM,aAAa,GAAG,yDAAyD,CAAC;AAChF,sFAAsF;AACtF,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,CAAC,CAAC,CAAC;AAExL,MAAM,CAAC,MAAM,QAAQ,GAAG,EAAE,CAAC;AAC3B,MAAM,SAAS,GAAG,CAAC,CAAC;AACpB,MAAM,iBAAiB,GAAG,CAAC,CAAC,CAAC,oFAAoF;AAEjH,yGAAyG;AACzG,SAAS,QAAQ,CAAC,IAAY;IAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACvC,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACpC,IAAI,uFAAuF,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACjH,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,KAAuB,EAAE,WAAqB;IACvE,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9E,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,SAAS;QAChC,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC1B,IAAI,IAAI,CAAC,MAAM,GAAG,SAAS;YAAE,SAAS;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;QACpC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACvC,SAAS;QACX,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACtD,IAAI,MAAM,KAAK,CAAC,CAAC,IAAI,MAAM,IAAI,iBAAiB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC;YACzH,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YACb,SAAS;QACX,CAAC;QACD,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACtI,CAAC;IACD,MAAM,GAAG,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IACvF,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,MAAM,GAAG,QAAQ,CAAC,EAAE,CAAC;AAC9F,CAAC","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":"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","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 function hashSources(root: string, sources: string[], cache?: Map<string, string>): Promise<Record<string, string>>;
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
- /** Compare stored source hashes with the repository now. Mutates facts that became stale. */
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;
@@ -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
- export async function hashSources(root, sources, cache = new Map()) {
49
- const out = {};
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
- let h = cache.get(s);
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, s);
108
+ const full = resolveInside(root, d.file);
54
109
  try {
55
110
  const st = await fs.lstat(full);
56
- h = st.isFile() ? await sha256File(full) : MISSING_HASH;
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(s, h);
123
+ cache.set(d.hashKey, h);
62
124
  }
63
- out[s] = h;
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 = sources.filter((s) => hashes[s] === MISSING_HASH);
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
- /** Compare stored source hashes with the repository now. Mutates facts that became stale. */
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 hashSources(root, f.sources, cache);
156
- const changed = f.sources.filter((s) => current[s] !== f.sourceHashes[s]);
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.map((s) => `${s} ${current[s] === MISSING_HASH ? 'removed' : 'changed'}`).join('; ');
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.sources.filter((s) => f.sourceHashes[s] === MISSING_HASH);
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
- lines.push(`- Entry-point candidates: ${list(d.entryHints)}`);
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, 12))}`);
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) {