@dotdotgod/cli 0.2.5 → 0.2.6
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/README.md +5 -5
- package/package.json +3 -2
- package/src/cli/usage.mjs +10 -8
- package/src/commands/config.mjs +8 -16
- package/src/commands/query.mjs +122 -0
- package/src/core.mjs +6 -4
- package/src/index/files.mjs +1 -1
- package/src/init.mjs +9 -30
- package/src/memory/config.mjs +18 -21
- package/src/{load-snapshot → memory}/summary.mjs +2 -2
- package/src/query/chunks.mjs +92 -0
- package/src/query/embedder.mjs +24 -0
- package/src/query/store.mjs +60 -0
- package/src/validate/run.mjs +2 -2
- package/src/commands/load-snapshot.mjs +0 -46
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Command-line tools for dotdotgod project memory.
|
|
6
6
|
|
|
7
|
-
Use this package when you want to initialize a docs-first project scaffold, validate project-memory docs, build a local graph/cache,
|
|
7
|
+
Use this package when you want to initialize a docs-first project scaffold, validate project-memory docs, build a local graph/cache, search shared documentation locally, expand project references, or ask what should be checked after a file changes.
|
|
8
8
|
|
|
9
9
|
These commands implement dotdotgod's project-memory loop — init, load, plan, impact — for shells, scripts, CI, and any agent without a dedicated adapter.
|
|
10
10
|
|
|
@@ -27,7 +27,7 @@ Common first commands:
|
|
|
27
27
|
```bash
|
|
28
28
|
dotdotgod init .
|
|
29
29
|
dotdotgod validate .
|
|
30
|
-
dotdotgod
|
|
30
|
+
dotdotgod query . "project documentation"
|
|
31
31
|
dotdotgod graph impact . --changed <path> --compact
|
|
32
32
|
```
|
|
33
33
|
|
|
@@ -36,7 +36,7 @@ dotdotgod graph impact . --changed <path> --compact
|
|
|
36
36
|
- `init` creates `AGENTS.md`, thin agent entrypoints, docs indexes, active-plan space, archive map, local cache ignores, and the complete editable default config.
|
|
37
37
|
- `validate` checks the dotdotgod docs/project-memory structure, local links, traceability blocks, config validity, and optional index freshness.
|
|
38
38
|
- `index` builds `.dotdotgod/manifest.json` and compact graph shards from maintained project files.
|
|
39
|
-
- `
|
|
39
|
+
- `query` locally embeds shared Markdown with `Xenova/multilingual-e5-small`, incrementally stores vectors under `.dotdotgod/vectors/`, and returns the best-ranked chunk from each relevant Markdown file.
|
|
40
40
|
- `resolve` and `expand` map explicit or high-signal prompt references to project files.
|
|
41
41
|
- `graph impact` ranks likely related specs, tests, docs, commands, and source files for one or more changed paths, with a combined ranking and per-file top five.
|
|
42
42
|
- `traceability links` checks or repairs generated Markdown traceability-link sections.
|
|
@@ -56,7 +56,7 @@ dotdotgod config .
|
|
|
56
56
|
dotdotgod config init .
|
|
57
57
|
dotdotgod status .
|
|
58
58
|
dotdotgod index .
|
|
59
|
-
dotdotgod
|
|
59
|
+
dotdotgod query . "plan mode tools"
|
|
60
60
|
dotdotgod resolve . PLAN_MODE
|
|
61
61
|
dotdotgod expand . "Update [[PLAN_MODE]] and [[HOOKS]]"
|
|
62
62
|
dotdotgod expand . "PLAN_MODE 수정하자" --fuzzy
|
|
@@ -80,7 +80,7 @@ dotdotgod graph communities .
|
|
|
80
80
|
| Add the dotdotgod scaffold to a repository | `dotdotgod init .` |
|
|
81
81
|
| Check docs, links, config, traceability, and optional index freshness | `dotdotgod validate . --check-index` |
|
|
82
82
|
| Refresh the local graph/cache | `dotdotgod index .` |
|
|
83
|
-
|
|
|
83
|
+
| Search shared project documentation locally | `dotdotgod query . "project documentation"` |
|
|
84
84
|
| Resolve `[[...]]` references from a prompt | `dotdotgod expand . "Update [[PLAN_MODE]]"` |
|
|
85
85
|
| Repair generated traceability-link sections | `dotdotgod traceability links . --write` |
|
|
86
86
|
| Validate an active plan before execution | `dotdotgod plan validate docs/plan/<task>/README.md` |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotdotgod/cli",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "CLI for dotdotgod validation,
|
|
3
|
+
"version": "0.2.6",
|
|
4
|
+
"description": "CLI for dotdotgod validation, local documentation search, and graph indexing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Elastic-2.0",
|
|
7
7
|
"publishConfig": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"LICENSE"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@huggingface/transformers": "^4.2.0",
|
|
32
33
|
"leiden-ts": "^0.1.0"
|
|
33
34
|
},
|
|
34
35
|
"homepage": "https://github.com/dotdotgod/dotdotgod-kit/tree/main/packages/cli#readme",
|
package/src/cli/usage.mjs
CHANGED
|
@@ -11,7 +11,7 @@ export function commandUsage(command = 'root') {
|
|
|
11
11
|
dotdotgod validate <root> [--include-local-memory] [--check-index] [--max-lines n] [--max-chars n] [--no-link-check] [--json]`;
|
|
12
12
|
case 'init':
|
|
13
13
|
return `Usage:
|
|
14
|
-
dotdotgod init <root> [--project-name NAME] [--dotdot-setting] [--
|
|
14
|
+
dotdotgod init <root> [--project-name NAME] [--dotdot-setting] [--dry-run] [--json]
|
|
15
15
|
|
|
16
16
|
Create AGENTS.md, agent entrypoints, docs indexes, and local memory gitignore entries.`;
|
|
17
17
|
case 'index':
|
|
@@ -20,20 +20,22 @@ Create AGENTS.md, agent entrypoints, docs indexes, and local memory gitignore en
|
|
|
20
20
|
case 'config':
|
|
21
21
|
return `Usage:
|
|
22
22
|
dotdotgod config <root> [--json]
|
|
23
|
-
dotdotgod config init <root> [--
|
|
23
|
+
dotdotgod config init <root> [--json]
|
|
24
24
|
|
|
25
25
|
Inspect or initialize the project-level dotdotgod config file.`;
|
|
26
26
|
case 'config init':
|
|
27
27
|
return `Usage:
|
|
28
|
-
dotdotgod config init <root> [--
|
|
28
|
+
dotdotgod config init <root> [--json]
|
|
29
29
|
|
|
30
30
|
Create dotdotgod.config.json with the built-in default memory, traceability, validation, and impact ranking policy.`;
|
|
31
31
|
case 'status':
|
|
32
32
|
return `Usage:
|
|
33
33
|
dotdotgod status <root> [--json]`;
|
|
34
|
-
case '
|
|
34
|
+
case 'query':
|
|
35
35
|
return `Usage:
|
|
36
|
-
dotdotgod
|
|
36
|
+
dotdotgod query <root> <query> [--limit n] [--json]
|
|
37
|
+
|
|
38
|
+
Search shared project documentation with the local multilingual E5 vector index.`;
|
|
37
39
|
case 'plan':
|
|
38
40
|
return `Usage:
|
|
39
41
|
dotdotgod plan validate docs/plan/<task-slug>/README.md [--stage stage] [--json]
|
|
@@ -97,12 +99,12 @@ Ranks nodes related to one or more changed files. <root> is the project root; re
|
|
|
97
99
|
dotdotgod [--version|-v]
|
|
98
100
|
dotdotgod help [command]
|
|
99
101
|
dotdotgod validate <root> [--include-local-memory] [--check-index] [--max-lines n] [--max-chars n] [--no-link-check] [--json]
|
|
100
|
-
dotdotgod init <root> [--project-name NAME] [--dotdot-setting] [--
|
|
102
|
+
dotdotgod init <root> [--project-name NAME] [--dotdot-setting] [--dry-run] [--json]
|
|
101
103
|
dotdotgod index <root> [--json]
|
|
102
104
|
dotdotgod config <root> [--json]
|
|
103
|
-
dotdotgod config init <root> [--
|
|
105
|
+
dotdotgod config init <root> [--json]
|
|
104
106
|
dotdotgod status <root> [--json]
|
|
105
|
-
dotdotgod
|
|
107
|
+
dotdotgod query <root> <query> [--limit n] [--json]
|
|
106
108
|
dotdotgod resolve <root> <ref> [--max-results n] [--include-archive] [--json]
|
|
107
109
|
dotdotgod expand <root> <prompt> [--max-results n] [--include-archive] [--with-impact] [--fuzzy] [--json]
|
|
108
110
|
dotdotgod traceability links <root> [--check|--write] [--json]
|
package/src/commands/config.mjs
CHANGED
|
@@ -3,13 +3,12 @@ import { join, resolve } from 'node:path';
|
|
|
3
3
|
import { usage } from '../cli/usage.mjs';
|
|
4
4
|
import { defaultDotdotgodConfigText, memoryConfigSummary, readMemoryConfig } from '../memory/config.mjs';
|
|
5
5
|
|
|
6
|
-
function parseConfigOptions(argv,
|
|
7
|
-
const options = { root: '.', json: false
|
|
6
|
+
function parseConfigOptions(argv, usageKey = 'config') {
|
|
7
|
+
const options = { root: '.', json: false };
|
|
8
8
|
let rootSet = false;
|
|
9
9
|
for (let i = 0; i < argv.length; i += 1) {
|
|
10
10
|
const arg = argv[i];
|
|
11
11
|
if (arg === '--json') options.json = true;
|
|
12
|
-
else if (allowForce && arg === '--force') options.force = true;
|
|
13
12
|
else if (!arg.startsWith('-') && !rootSet) {
|
|
14
13
|
options.root = arg;
|
|
15
14
|
rootSet = true;
|
|
@@ -20,12 +19,8 @@ function parseConfigOptions(argv, allowForce = false, usageKey = 'config') {
|
|
|
20
19
|
return options;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
function configSourcePath(root, source) {
|
|
24
|
-
return source && source !== 'default' ? join(root, source) : null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
22
|
function configInitError(options, code, message, path = null) {
|
|
28
|
-
const payload = { ok: false, command: 'config init', root: options.root, path, created: false,
|
|
23
|
+
const payload = { ok: false, command: 'config init', root: options.root, path, created: false, error: { code, message } };
|
|
29
24
|
if (options.json) {
|
|
30
25
|
console.log(JSON.stringify(payload, null, 2));
|
|
31
26
|
process.exit(2);
|
|
@@ -60,7 +55,7 @@ function formatConfigOutput(payload) {
|
|
|
60
55
|
|
|
61
56
|
export function runConfig(argv) {
|
|
62
57
|
const isInit = argv[0] === 'init';
|
|
63
|
-
const options = parseConfigOptions(isInit ? argv.slice(1) : argv, isInit
|
|
58
|
+
const options = parseConfigOptions(isInit ? argv.slice(1) : argv, isInit ? 'config init' : 'config');
|
|
64
59
|
if (isInit) {
|
|
65
60
|
if (!existsSync(options.root)) configInitError(options, 'ROOT_NOT_FOUND', `Project root not found: ${options.root}`);
|
|
66
61
|
try {
|
|
@@ -69,20 +64,17 @@ export function runConfig(argv) {
|
|
|
69
64
|
configInitError(options, 'ROOT_NOT_FOUND', `Project root not found: ${options.root}`);
|
|
70
65
|
}
|
|
71
66
|
const target = join(options.root, 'dotdotgod.config.json');
|
|
72
|
-
|
|
73
|
-
if (existsSync(rcPath)) configInitError(options, 'CONFIG_RC_EXISTS', `.dotdotgodrc.json already exists; remove or migrate it before initializing dotdotgod.config.json.`, rcPath);
|
|
74
|
-
const existed = existsSync(target);
|
|
75
|
-
if (existed && !options.force) configInitError(options, 'CONFIG_EXISTS', `dotdotgod.config.json already exists. Re-run with --force to overwrite it.`, target);
|
|
67
|
+
if (existsSync(target)) configInitError(options, 'CONFIG_EXISTS', 'dotdotgod.config.json already exists.', target);
|
|
76
68
|
writeFileSync(target, defaultDotdotgodConfigText());
|
|
77
|
-
const payload = { ok: true, command: 'config init', root: options.root, path: target, created:
|
|
69
|
+
const payload = { ok: true, command: 'config init', root: options.root, path: target, created: true };
|
|
78
70
|
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
|
79
|
-
else console.log(`dotdotgod config init:
|
|
71
|
+
else console.log(`dotdotgod config init: created ${target}`);
|
|
80
72
|
return;
|
|
81
73
|
}
|
|
82
74
|
|
|
83
75
|
const config = readMemoryConfig(options.root);
|
|
84
76
|
const errors = config.errors ?? [];
|
|
85
|
-
const payload = { ok: errors.length === 0, command: 'config', root: options.root, source: config.source ?? 'default', path:
|
|
77
|
+
const payload = { ok: errors.length === 0, command: 'config', root: options.root, source: config.source ?? 'default', path: config.source && config.source !== 'default' ? join(options.root, config.source) : null, config: memoryConfigSummary(config), errors };
|
|
86
78
|
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
|
87
79
|
else console.log(formatConfigOutput(payload));
|
|
88
80
|
if (errors.length > 0) process.exit(1);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { usage } from '../cli/usage.mjs';
|
|
3
|
+
import { readMemoryConfig } from '../memory/config.mjs';
|
|
4
|
+
import { collectDocumentationChunks } from '../query/chunks.mjs';
|
|
5
|
+
import { embedTexts } from '../query/embedder.mjs';
|
|
6
|
+
import { readVectorCache, VECTOR_DIMENSIONS, VECTOR_MODEL, writeVectorCache } from '../query/store.mjs';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_LIMIT = 30;
|
|
9
|
+
const MAX_LIMIT = 100;
|
|
10
|
+
const EMBED_BATCH_SIZE = 16;
|
|
11
|
+
|
|
12
|
+
export function parseQueryOptions(argv) {
|
|
13
|
+
const options = { root: '.', query: '', limit: DEFAULT_LIMIT, json: false };
|
|
14
|
+
let rootSet = false;
|
|
15
|
+
const queryParts = [];
|
|
16
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
17
|
+
const arg = argv[i];
|
|
18
|
+
if (arg === '--json') options.json = true;
|
|
19
|
+
else if (arg === '--limit') {
|
|
20
|
+
const value = Number(argv[++i]);
|
|
21
|
+
if (!Number.isInteger(value) || value < 1 || value > MAX_LIMIT) usage(`--limit must be an integer from 1 to ${MAX_LIMIT}.`, 'query');
|
|
22
|
+
options.limit = value;
|
|
23
|
+
} else if (arg.startsWith('-')) usage(`Unknown option: ${arg}`, 'query');
|
|
24
|
+
else if (!rootSet) {
|
|
25
|
+
options.root = resolve(arg);
|
|
26
|
+
rootSet = true;
|
|
27
|
+
} else queryParts.push(arg);
|
|
28
|
+
}
|
|
29
|
+
options.query = queryParts.join(' ').trim();
|
|
30
|
+
if (!options.query) usage('query requires search text.', 'query');
|
|
31
|
+
return options;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function cosine(left, vectors, offset) {
|
|
35
|
+
let score = 0;
|
|
36
|
+
for (let index = 0; index < VECTOR_DIMENSIONS; index += 1) score += left[index] * vectors[offset + index];
|
|
37
|
+
return score;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function lexicalBoost(query, chunk) {
|
|
41
|
+
const queryTerms = new Set(query.toLowerCase().split(/[^\p{L}\p{N}_.:/-]+/u).filter((term) => term.length > 1));
|
|
42
|
+
if (queryTerms.size === 0) return 0;
|
|
43
|
+
const haystack = `${chunk.path} ${chunk.heading}`.toLowerCase();
|
|
44
|
+
let matches = 0;
|
|
45
|
+
for (const term of queryTerms) if (haystack.includes(term)) matches += 1;
|
|
46
|
+
return Math.min(0.08, (matches / queryTerms.size) * 0.08);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function buildVectorIndex(root, embed = embedTexts) {
|
|
50
|
+
const config = readMemoryConfig(root);
|
|
51
|
+
const exclude = config.load?.documentationSummary?.exclude ?? ['docs/plan', 'docs/archive'];
|
|
52
|
+
const chunks = collectDocumentationChunks(root, exclude);
|
|
53
|
+
const cached = readVectorCache(root);
|
|
54
|
+
const cachedOffsets = new Map((cached?.chunks ?? []).map((chunk, index) => [chunk.fingerprint, index * VECTOR_DIMENSIONS]));
|
|
55
|
+
const vectors = new Float32Array(chunks.length * VECTOR_DIMENSIONS);
|
|
56
|
+
const missing = [];
|
|
57
|
+
let reused = 0;
|
|
58
|
+
chunks.forEach((chunk, index) => {
|
|
59
|
+
const oldOffset = cachedOffsets.get(chunk.fingerprint);
|
|
60
|
+
if (oldOffset === undefined || !cached) missing.push({ chunk, index });
|
|
61
|
+
else {
|
|
62
|
+
vectors.set(cached.vectors.subarray(oldOffset, oldOffset + VECTOR_DIMENSIONS), index * VECTOR_DIMENSIONS);
|
|
63
|
+
reused += 1;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
for (let offset = 0; offset < missing.length; offset += EMBED_BATCH_SIZE) {
|
|
67
|
+
const batch = missing.slice(offset, offset + EMBED_BATCH_SIZE);
|
|
68
|
+
const embedded = await embed(batch.map(({ chunk }) => `passage: ${chunk.passage}`));
|
|
69
|
+
embedded.forEach((vector, index) => vectors.set(vector, batch[index].index * VECTOR_DIMENSIONS));
|
|
70
|
+
}
|
|
71
|
+
const storedChunks = chunks.map(({ passage, ...chunk }) => chunk);
|
|
72
|
+
const manifest = writeVectorCache(root, storedChunks, vectors, {
|
|
73
|
+
excluded: exclude,
|
|
74
|
+
embedded: missing.length,
|
|
75
|
+
reused,
|
|
76
|
+
});
|
|
77
|
+
return { chunks: storedChunks, vectors, manifest };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function queryDocumentation(root, query, options = {}) {
|
|
81
|
+
const embed = options.embed ?? embedTexts;
|
|
82
|
+
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
83
|
+
const index = await buildVectorIndex(root, embed);
|
|
84
|
+
const [queryVector] = await embed([`query: ${query}`]);
|
|
85
|
+
const rankedChunks = index.chunks.map((chunk, position) => {
|
|
86
|
+
const vectorScore = cosine(queryVector, index.vectors, position * VECTOR_DIMENSIONS);
|
|
87
|
+
return { ...chunk, score: Number((vectorScore + lexicalBoost(query, chunk)).toFixed(6)), vectorScore: Number(vectorScore.toFixed(6)) };
|
|
88
|
+
}).sort((left, right) => right.score - left.score || left.path.localeCompare(right.path));
|
|
89
|
+
const results = [];
|
|
90
|
+
const seenPaths = new Set();
|
|
91
|
+
for (const chunk of rankedChunks) {
|
|
92
|
+
if (seenPaths.has(chunk.path)) continue;
|
|
93
|
+
seenPaths.add(chunk.path);
|
|
94
|
+
results.push(chunk);
|
|
95
|
+
if (results.length === limit) break;
|
|
96
|
+
}
|
|
97
|
+
return { ok: true, command: 'query', root, query, model: VECTOR_MODEL, dimensions: VECTOR_DIMENSIONS, limit, index: index.manifest, results };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function formatQueryResult(payload) {
|
|
101
|
+
const lines = [`dotdotgod query: ${payload.query}`, `- model: ${payload.model}`, `- results: ${payload.results.length}`];
|
|
102
|
+
payload.results.forEach((result, index) => {
|
|
103
|
+
lines.push(`${index + 1}. ${result.path}${result.heading ? ` — ${result.heading}` : ''} (${result.score.toFixed(3)})`);
|
|
104
|
+
const excerpt = result.text.replace(/\s+/g, ' ').trim().slice(0, 240);
|
|
105
|
+
if (excerpt) lines.push(` ${excerpt}${result.text.length > 240 ? '…' : ''}`);
|
|
106
|
+
});
|
|
107
|
+
return lines.join('\n');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function runQuery(argv) {
|
|
111
|
+
const options = parseQueryOptions(argv);
|
|
112
|
+
try {
|
|
113
|
+
const payload = await queryDocumentation(options.root, options.query, options);
|
|
114
|
+
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
|
115
|
+
else console.log(formatQueryResult(payload));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
const message = `dotdotgod query failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
118
|
+
if (options.json) console.log(JSON.stringify({ ok: false, command: 'query', root: options.root, query: options.query, error: message }, null, 2));
|
|
119
|
+
else console.error(message);
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/core.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { commandUsage, hasHelpToken, helpCommandFromArgs, isHelpToken, isVersion
|
|
|
3
3
|
import { runValidate } from './validate/run.mjs';
|
|
4
4
|
import { runIndex, runStatus } from './index/cache.mjs';
|
|
5
5
|
import { runConfig } from './commands/config.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import { runQuery } from './commands/query.mjs';
|
|
7
7
|
import { runResolve, runExpand } from './reference/resolve.mjs';
|
|
8
8
|
import { runTraceability } from './commands/traceability.mjs';
|
|
9
9
|
import { runGraph } from './commands/graph.mjs';
|
|
@@ -19,7 +19,7 @@ export { DEFAULT_IMPACT_RANKING_POLICY, DEFAULT_INTEGRATIONS_POLICY, DEFAULT_LOA
|
|
|
19
19
|
export { buildCommunities, relationWeight } from './graph/communities.mjs';
|
|
20
20
|
export { addEdge, addNode, compactGraph, expandGraph, graphStats, jsonSize, shardFile, writeJson } from './graph/store.mjs';
|
|
21
21
|
export { cacheFile, collectIndexFiles, fingerprint, shouldIndexPath } from './index/files.mjs';
|
|
22
|
-
export { buildMemoryAreas, buildPinnedFiles, detectCommandGuidance, detectPackageManager } from './
|
|
22
|
+
export { buildMemoryAreas, buildPinnedFiles, detectCommandGuidance, detectPackageManager } from './memory/summary.mjs';
|
|
23
23
|
export { isReadmeIndexPath, retrievalMetadataForPath } from './graph/metadata.mjs';
|
|
24
24
|
export { buildGraph } from './graph/extract.mjs';
|
|
25
25
|
export { addDeterministicSemanticEdges } from './graph/semantic.mjs';
|
|
@@ -28,7 +28,9 @@ export { runValidate } from './validate/run.mjs';
|
|
|
28
28
|
export { buildImpactReport, buildCompactImpactReport } from './impact/report.mjs';
|
|
29
29
|
export { formatCompactImpactOutput, formatYmlGraphImpactError, formatYmlImpactOutput } from './impact/format.mjs';
|
|
30
30
|
export { runConfig } from './commands/config.mjs';
|
|
31
|
-
export {
|
|
31
|
+
export { buildVectorIndex, parseQueryOptions, queryDocumentation, runQuery } from './commands/query.mjs';
|
|
32
|
+
export { chunkMarkdown, collectDocumentationChunks, collectDocumentationMarkdown, textFingerprint } from './query/chunks.mjs';
|
|
33
|
+
export { readVectorCache, VECTOR_DIMENSIONS, VECTOR_MODEL, VECTOR_SCHEMA_VERSION, vectorCachePaths, writeVectorCache } from './query/store.mjs';
|
|
32
34
|
export { extractBracketReferences, extractFuzzyReferences, normalizeReferenceAlias } from './reference/extract.mjs';
|
|
33
35
|
export { resolveReferenceCandidates, runResolve, runExpand } from './reference/resolve.mjs';
|
|
34
36
|
export { parseTraceabilityOptions, runTraceability } from './commands/traceability.mjs';
|
|
@@ -53,7 +55,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
53
55
|
else if (command === 'index') runIndex(args);
|
|
54
56
|
else if (command === 'config') runConfig(args);
|
|
55
57
|
else if (command === 'status') runStatus(args);
|
|
56
|
-
else if (command === '
|
|
58
|
+
else if (command === 'query') await runQuery(args);
|
|
57
59
|
else if (command === 'resolve') runResolve(args);
|
|
58
60
|
else if (command === 'expand') runExpand(args);
|
|
59
61
|
else if (command === 'traceability') runTraceability(args);
|
package/src/index/files.mjs
CHANGED
|
@@ -14,7 +14,7 @@ const INDEX_TEXT_EXTENSIONS = new Set([
|
|
|
14
14
|
]);
|
|
15
15
|
const INDEX_TEXT_FILENAMES = new Set([
|
|
16
16
|
'AGENTS.md', 'CLAUDE.md', 'CODEX.md', 'README', 'README.md', 'LICENSE', 'NOTICE', 'CHANGELOG', 'CHANGELOG.md', 'CONTRIBUTING.md', 'SECURITY.md', 'AUTHORS', 'CODEOWNERS', '.gitignore', '.editorconfig',
|
|
17
|
-
'dotdotgod.config.json', '
|
|
17
|
+
'dotdotgod.config.json', 'package.json', 'pnpm-workspace.yaml', 'tsconfig.json', 'jsconfig.json',
|
|
18
18
|
'Dockerfile', 'Containerfile', 'Makefile', 'Justfile', 'Procfile', 'Rakefile', 'Gemfile', 'go.mod', 'go.sum', 'Cargo.toml', 'Cargo.lock', 'pyproject.toml', 'requirements.txt', 'Pipfile', 'Pipfile.lock', 'poetry.lock', 'deno.json', 'deno.jsonc', 'bunfig.toml',
|
|
19
19
|
'.env.example', '.env.sample', '.env.template',
|
|
20
20
|
]);
|
package/src/init.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { defaultDotdotgodConfigText } from './memory/config.mjs';
|
|
4
4
|
|
|
@@ -7,20 +7,16 @@ function action(status, path, extra = {}) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
function formatAction(item) {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
if (item.add) extras.push(`add=${item.add}`);
|
|
13
|
-
if (item.existing) extras.push(`existing=${item.existing}`);
|
|
14
|
-
return `${item.status.padEnd(13)} ${item.path}${extras.length > 0 ? ` ${extras.join(' ')}` : ''}`;
|
|
10
|
+
const extra = item.add ? ` add=${item.add}` : '';
|
|
11
|
+
return `${item.status.padEnd(13)} ${item.path}${extra}`;
|
|
15
12
|
}
|
|
16
13
|
|
|
17
14
|
function parseInitOptions(argv) {
|
|
18
|
-
const options = { root: null, projectName: '', dotdotSetting: false,
|
|
15
|
+
const options = { root: null, projectName: '', dotdotSetting: false, dryRun: false, json: false };
|
|
19
16
|
for (let i = 0; i < argv.length; i += 1) {
|
|
20
17
|
const arg = argv[i];
|
|
21
18
|
if (arg === '--json') options.json = true;
|
|
22
19
|
else if (arg === '--dotdot-setting') options.dotdotSetting = true;
|
|
23
|
-
else if (arg === '--force') options.force = true;
|
|
24
20
|
else if (arg === '--dry-run') options.dryRun = true;
|
|
25
21
|
else if (arg === '--project-name') {
|
|
26
22
|
const value = argv[i + 1];
|
|
@@ -50,35 +46,21 @@ function initError(options, code, message) {
|
|
|
50
46
|
process.exit(2);
|
|
51
47
|
}
|
|
52
48
|
|
|
53
|
-
function backupPathFor(target) {
|
|
54
|
-
const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
|
55
|
-
const base = `${target}.bak.${timestamp}`;
|
|
56
|
-
let candidate = base;
|
|
57
|
-
for (let suffix = 1; existsSync(candidate); suffix += 1) candidate = `${base}.${suffix}`;
|
|
58
|
-
return candidate;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
49
|
function writeInitFile(options, relativePath, content, actions) {
|
|
62
50
|
const target = join(options.root, relativePath);
|
|
63
|
-
if (existsSync(target)
|
|
51
|
+
if (existsSync(target)) {
|
|
64
52
|
actions.push(action('skipped', target));
|
|
65
53
|
return;
|
|
66
54
|
}
|
|
67
55
|
|
|
68
|
-
let backup = '';
|
|
69
|
-
if (existsSync(target) && options.force) {
|
|
70
|
-
backup = backupPathFor(target);
|
|
71
|
-
if (!options.dryRun) renameSync(target, backup);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
56
|
if (options.dryRun) {
|
|
75
|
-
actions.push(action(
|
|
57
|
+
actions.push(action('would_create', target));
|
|
76
58
|
return;
|
|
77
59
|
}
|
|
78
60
|
|
|
79
61
|
mkdirSync(dirname(target), { recursive: true });
|
|
80
62
|
writeFileSync(target, content.endsWith('\n') ? content : `${content}\n`);
|
|
81
|
-
actions.push(action(
|
|
63
|
+
actions.push(action('created', target));
|
|
82
64
|
}
|
|
83
65
|
|
|
84
66
|
function ensureGitignoreEntry(options, entry, actions) {
|
|
@@ -309,14 +291,11 @@ export function runInit(argv, usage) {
|
|
|
309
291
|
const actions = [];
|
|
310
292
|
for (const [relativePath, content] of initFiles(options)) writeInitFile(options, relativePath, content, actions);
|
|
311
293
|
|
|
312
|
-
|
|
313
|
-
const rcPath = join(options.root, '.dotdotgodrc.json');
|
|
314
|
-
if (existsSync(rcPath)) actions.push(action('conflict', configPath, { existing: rcPath }));
|
|
315
|
-
else writeInitFile(options, 'dotdotgod.config.json', defaultDotdotgodConfigText(), actions);
|
|
294
|
+
writeInitFile(options, 'dotdotgod.config.json', defaultDotdotgodConfigText(), actions);
|
|
316
295
|
|
|
317
296
|
for (const entry of ['docs/plan', 'docs/archive', '.dotdotgod']) ensureGitignoreEntry(options, entry, actions);
|
|
318
297
|
|
|
319
|
-
const payload = { ok: true, command: 'init', root: options.root, projectName: options.projectName, dryRun: options.dryRun,
|
|
298
|
+
const payload = { ok: true, command: 'init', root: options.root, projectName: options.projectName, dryRun: options.dryRun, dotdotSetting: options.dotdotSetting, actions };
|
|
320
299
|
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
|
321
300
|
else for (const item of actions) console.log(formatAction(item));
|
|
322
301
|
}
|
package/src/memory/config.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { join, resolve } from 'node:path';
|
|
|
3
3
|
import { rel } from '../common/paths.mjs';
|
|
4
4
|
import { isKebabCase } from '../docs/markdown.mjs';
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const MEMORY_CONFIG_FILE = 'dotdotgod.config.json';
|
|
7
7
|
const MEMORY_SCOPES = new Set(['shared', 'local']);
|
|
8
8
|
const MEMORY_FRESHNESS = new Set(['fresh', 'stale']);
|
|
9
9
|
export const DEFAULT_TRACEABILITY_POLICY = {
|
|
@@ -533,27 +533,24 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
|
|
|
533
533
|
}
|
|
534
534
|
|
|
535
535
|
export function readMemoryConfig(root = '.') {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
return { ...defaultMemoryConfig(), source: name, errors: [{ file: name, code: 'MEMORY_CONFIG_INVALID_JSON', message: `Invalid JSON: ${error instanceof Error ? error.message : String(error)}\nFix: repair ${name} so it is valid JSON, or regenerate the default config with \`dotdotgod config init <root> --force\` if you want to reset it.` }] };
|
|
554
|
-
}
|
|
536
|
+
const path = join(root, MEMORY_CONFIG_FILE);
|
|
537
|
+
if (!existsSync(path)) return defaultMemoryConfig();
|
|
538
|
+
try {
|
|
539
|
+
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
540
|
+
const errors = validateMemoryConfigData(data, root, MEMORY_CONFIG_FILE);
|
|
541
|
+
if (errors.length > 0) return { ...defaultMemoryConfig(), source: MEMORY_CONFIG_FILE, errors };
|
|
542
|
+
const configuredAreas = data.memory?.areas?.map(normalizeMemoryArea) ?? [];
|
|
543
|
+
const traceability = data.traceability === undefined ? cloneTraceabilityPolicy() : normalizeTraceabilityPolicy(data.traceability);
|
|
544
|
+
const validation = data.validation === undefined ? cloneValidationPolicy() : normalizeValidationPolicy(data.validation);
|
|
545
|
+
const impactRanking = normalizeImpactRankingPolicy(data.impactRanking);
|
|
546
|
+
const referenceExpansion = normalizeReferenceExpansionPolicy(data.referenceExpansion);
|
|
547
|
+
const integrations = normalizeIntegrationsPolicy(data.integrations);
|
|
548
|
+
const load = normalizeLoadPolicy(data.load);
|
|
549
|
+
const planMode = normalizePlanModePolicy(data.planMode);
|
|
550
|
+
return configuredAreas.length > 0 ? { source: MEMORY_CONFIG_FILE, areas: configuredAreas, traceability, validation, impactRanking, referenceExpansion, integrations, load, planMode, errors: [] } : { ...defaultMemoryConfig(), traceability, validation, impactRanking, referenceExpansion, integrations, load, planMode, source: MEMORY_CONFIG_FILE, errors: [] };
|
|
551
|
+
} catch (error) {
|
|
552
|
+
return { ...defaultMemoryConfig(), source: MEMORY_CONFIG_FILE, errors: [{ file: MEMORY_CONFIG_FILE, code: 'MEMORY_CONFIG_INVALID_JSON', message: `Invalid JSON: ${error instanceof Error ? error.message : String(error)}\nFix: repair ${MEMORY_CONFIG_FILE} so it is valid JSON, or remove it before regenerating the default config with \`dotdotgod config init <root>\`.` }] };
|
|
555
553
|
}
|
|
556
|
-
return defaultMemoryConfig();
|
|
557
554
|
}
|
|
558
555
|
|
|
559
556
|
function serializableMemoryArea(area) {
|
|
@@ -2,7 +2,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { rel } from '../common/paths.mjs';
|
|
4
4
|
import { isExcludedIndexDir } from '../index/files.mjs';
|
|
5
|
-
import { defaultMemoryConfig, isSecretLikePathPattern, matchPathPattern, memoryAreaForPath } from '
|
|
5
|
+
import { defaultMemoryConfig, isSecretLikePathPattern, matchPathPattern, memoryAreaForPath } from './config.mjs';
|
|
6
6
|
|
|
7
7
|
export function detectPackageManager(root) {
|
|
8
8
|
const packageFile = join(root, 'package.json');
|
|
@@ -43,7 +43,7 @@ export function detectCommandGuidance(root) {
|
|
|
43
43
|
packageManager,
|
|
44
44
|
install: source === 'missing-install' ? 'npm install -D @dotdotgod/cli' : null,
|
|
45
45
|
validate: source === 'local-source' ? `${prefix} validate . --include-local-memory` : `${prefix} validate .`,
|
|
46
|
-
|
|
46
|
+
query: `${prefix} query . \"<focus>\" --json`,
|
|
47
47
|
index: `${prefix} index . --json`,
|
|
48
48
|
status: `${prefix} status . --json`,
|
|
49
49
|
verify: packageJson?.scripts?.verify ? `${packageManager} run verify` : null,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { isSecretLikePathPattern, matchPathPattern } from '../memory/config.mjs';
|
|
5
|
+
|
|
6
|
+
const MAX_CHUNK_CHARS = 1600;
|
|
7
|
+
const SKIPPED_DIRECTORIES = new Set(['node_modules', 'dist', 'build', 'coverage', '.git', '.dotdotgod']);
|
|
8
|
+
|
|
9
|
+
export function textFingerprint(text) {
|
|
10
|
+
return createHash('sha256').update(text).digest('hex');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function collectDocumentationMarkdown(root, exclude = ['docs/plan', 'docs/archive']) {
|
|
14
|
+
const docsRoot = join(root, 'docs');
|
|
15
|
+
const files = [];
|
|
16
|
+
const walk = (directory) => {
|
|
17
|
+
let entries;
|
|
18
|
+
try {
|
|
19
|
+
entries = readdirSync(directory, { withFileTypes: true, encoding: 'utf8' });
|
|
20
|
+
} catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (entry.name.startsWith('.')) continue;
|
|
26
|
+
const absolute = join(directory, entry.name);
|
|
27
|
+
const path = relative(root, absolute).replaceAll('\\', '/');
|
|
28
|
+
if (isSecretLikePathPattern(path) || exclude.some((pattern) => matchPathPattern(path, pattern) || path === pattern || path.startsWith(`${pattern}/`))) continue;
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
if (!SKIPPED_DIRECTORIES.has(entry.name)) walk(absolute);
|
|
31
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) files.push(path);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
walk(docsRoot);
|
|
35
|
+
return files.sort();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function splitSection(text, maxChars = MAX_CHUNK_CHARS) {
|
|
39
|
+
if (text.length <= maxChars) return [text];
|
|
40
|
+
const paragraphs = text.split(/\n\s*\n/);
|
|
41
|
+
const chunks = [];
|
|
42
|
+
let current = '';
|
|
43
|
+
for (const paragraph of paragraphs) {
|
|
44
|
+
if (current && current.length + paragraph.length + 2 > maxChars) {
|
|
45
|
+
chunks.push(current.trim());
|
|
46
|
+
current = '';
|
|
47
|
+
}
|
|
48
|
+
if (paragraph.length > maxChars) {
|
|
49
|
+
for (let offset = 0; offset < paragraph.length; offset += maxChars) chunks.push(paragraph.slice(offset, offset + maxChars).trim());
|
|
50
|
+
} else current += `${current ? '\n\n' : ''}${paragraph}`;
|
|
51
|
+
}
|
|
52
|
+
if (current.trim()) chunks.push(current.trim());
|
|
53
|
+
return chunks.filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function chunkMarkdown(path, content, maxChars = MAX_CHUNK_CHARS) {
|
|
57
|
+
const lines = content.split('\n');
|
|
58
|
+
const sections = [];
|
|
59
|
+
let headings = [];
|
|
60
|
+
let body = [];
|
|
61
|
+
const flush = () => {
|
|
62
|
+
const text = body.join('\n').trim();
|
|
63
|
+
if (!text && headings.length === 0) return;
|
|
64
|
+
const heading = headings.length > 0 ? headings.join(' > ') : path;
|
|
65
|
+
const pieces = splitSection(text || heading, maxChars);
|
|
66
|
+
pieces.forEach((piece, index) => {
|
|
67
|
+
const passage = `Path: ${path}\nSection: ${heading}\n\n${piece}`;
|
|
68
|
+
const fingerprint = textFingerprint(passage);
|
|
69
|
+
sections.push({ id: `${path}#${fingerprint.slice(0, 16)}`, path, heading, part: index + 1, text: piece, passage, fingerprint });
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
74
|
+
if (!match) {
|
|
75
|
+
body.push(line);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
flush();
|
|
79
|
+
body = [];
|
|
80
|
+
const level = match[1].length;
|
|
81
|
+
headings = [...headings.slice(0, level - 1), match[2]];
|
|
82
|
+
}
|
|
83
|
+
flush();
|
|
84
|
+
return sections;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function collectDocumentationChunks(root, exclude) {
|
|
88
|
+
return collectDocumentationMarkdown(root, exclude).flatMap((path) => {
|
|
89
|
+
const content = readFileSync(join(root, path), 'utf8');
|
|
90
|
+
return chunkMarkdown(path, content);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { VECTOR_DIMENSIONS, VECTOR_MODEL } from './store.mjs';
|
|
2
|
+
|
|
3
|
+
let extractorPromise;
|
|
4
|
+
|
|
5
|
+
async function extractor() {
|
|
6
|
+
if (!extractorPromise) {
|
|
7
|
+
extractorPromise = import('@huggingface/transformers').then(async ({ env, pipeline }) => {
|
|
8
|
+
env.allowLocalModels = true;
|
|
9
|
+
return pipeline('feature-extraction', VECTOR_MODEL);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
return extractorPromise;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function embedTexts(texts) {
|
|
16
|
+
if (texts.length === 0) return [];
|
|
17
|
+
const model = await extractor();
|
|
18
|
+
const output = await model(texts, { pooling: 'mean', normalize: true });
|
|
19
|
+
const rows = output.tolist();
|
|
20
|
+
if (!Array.isArray(rows) || rows.some((row) => !Array.isArray(row) || row.length !== VECTOR_DIMENSIONS)) {
|
|
21
|
+
throw new Error(`Unexpected ${VECTOR_MODEL} embedding shape.`);
|
|
22
|
+
}
|
|
23
|
+
return rows;
|
|
24
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const VECTOR_SCHEMA_VERSION = 1;
|
|
5
|
+
export const VECTOR_MODEL = 'Xenova/multilingual-e5-small';
|
|
6
|
+
export const VECTOR_DIMENSIONS = 384;
|
|
7
|
+
|
|
8
|
+
export function vectorCachePaths(root) {
|
|
9
|
+
const directory = join(root, '.dotdotgod', 'vectors');
|
|
10
|
+
return {
|
|
11
|
+
directory,
|
|
12
|
+
manifest: join(directory, 'manifest.json'),
|
|
13
|
+
chunks: join(directory, 'chunks.jsonl'),
|
|
14
|
+
embeddings: join(directory, 'embeddings.f32'),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function readVectorCache(root) {
|
|
19
|
+
const paths = vectorCachePaths(root);
|
|
20
|
+
if (!existsSync(paths.manifest) || !existsSync(paths.chunks) || !existsSync(paths.embeddings)) return null;
|
|
21
|
+
try {
|
|
22
|
+
const manifest = JSON.parse(readFileSync(paths.manifest, 'utf8'));
|
|
23
|
+
if (manifest.schemaVersion !== VECTOR_SCHEMA_VERSION || manifest.model !== VECTOR_MODEL || manifest.dimensions !== VECTOR_DIMENSIONS) return null;
|
|
24
|
+
const chunks = readFileSync(paths.chunks, 'utf8').split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
25
|
+
const buffer = readFileSync(paths.embeddings);
|
|
26
|
+
if (buffer.byteLength !== chunks.length * VECTOR_DIMENSIONS * 4) return null;
|
|
27
|
+
const vectors = new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4);
|
|
28
|
+
return { manifest, chunks, vectors: new Float32Array(vectors) };
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function atomicWrite(path, data) {
|
|
35
|
+
const temporary = `${path}.tmp-${process.pid}`;
|
|
36
|
+
writeFileSync(temporary, data);
|
|
37
|
+
renameSync(temporary, path);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function writeVectorCache(root, chunks, vectors, metadata = {}) {
|
|
41
|
+
const paths = vectorCachePaths(root);
|
|
42
|
+
mkdirSync(paths.directory, { recursive: true });
|
|
43
|
+
if (vectors.length !== chunks.length * VECTOR_DIMENSIONS) throw new Error('Vector count does not match chunk metadata.');
|
|
44
|
+
const manifest = {
|
|
45
|
+
schemaVersion: VECTOR_SCHEMA_VERSION,
|
|
46
|
+
model: VECTOR_MODEL,
|
|
47
|
+
dimensions: VECTOR_DIMENSIONS,
|
|
48
|
+
chunks: chunks.length,
|
|
49
|
+
generatedAt: new Date().toISOString(),
|
|
50
|
+
...metadata,
|
|
51
|
+
};
|
|
52
|
+
atomicWrite(paths.chunks, `${chunks.map((chunk) => JSON.stringify(chunk)).join('\n')}\n`);
|
|
53
|
+
atomicWrite(paths.embeddings, Buffer.from(vectors.buffer, vectors.byteOffset, vectors.byteLength));
|
|
54
|
+
atomicWrite(paths.manifest, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
55
|
+
return manifest;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function removeVectorCache(root) {
|
|
59
|
+
rmSync(vectorCachePaths(root).directory, { recursive: true, force: true });
|
|
60
|
+
}
|
package/src/validate/run.mjs
CHANGED
|
@@ -169,7 +169,7 @@ export function runValidate(argv) {
|
|
|
169
169
|
}
|
|
170
170
|
if (options.checkIndex) {
|
|
171
171
|
const index = readIndex(root);
|
|
172
|
-
if (!index) addError(cacheFile(root), 'INDEX_MISSING', 'Expected .dotdotgod index cache.', null, 'run `dotdotgod index <root>`
|
|
172
|
+
if (!index) addError(cacheFile(root), 'INDEX_MISSING', 'Expected .dotdotgod index cache.', null, 'run `dotdotgod index <root>` to build the graph index.');
|
|
173
173
|
else {
|
|
174
174
|
const schemaVersion = index.schemaVersion ?? index.version ?? null;
|
|
175
175
|
if (schemaVersion !== CACHE_VERSION) addError(cacheFile(root), 'INDEX_SCHEMA_MISMATCH', `Index schema is ${String(schemaVersion)}; expected ${CACHE_VERSION}.`, null, 'run `dotdotgod index <root>` to rebuild the cache with the current schema.');
|
|
@@ -181,7 +181,7 @@ export function runValidate(argv) {
|
|
|
181
181
|
const indexedHash = indexed.get(path);
|
|
182
182
|
const currentHash = fingerprint(file);
|
|
183
183
|
if (!indexedHash) addError(file, 'INDEX_MISSING_FILE', 'Markdown file is not present in the current graph index.', null, 'run `dotdotgod index <root>` to refresh the graph index.');
|
|
184
|
-
else if (indexedHash !== currentHash) addError(file, 'INDEX_STALE', 'Markdown fingerprint differs from the current graph index.', null, 'run `dotdotgod index <root>`
|
|
184
|
+
else if (indexedHash !== currentHash) addError(file, 'INDEX_STALE', 'Markdown fingerprint differs from the current graph index.', null, 'run `dotdotgod index <root>` to refresh the graph index.');
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { parseCommon } from '../cli/usage.mjs';
|
|
2
|
-
import { buildCommunities } from '../graph/communities.mjs';
|
|
3
|
-
import { memoryConfigSummary, readMemoryConfig } from '../memory/config.mjs';
|
|
4
|
-
import { buildMemoryAreas, buildPinnedFiles, detectCommandGuidance } from '../load-snapshot/summary.mjs';
|
|
5
|
-
import { graphSummary, readFreshIndex } from '../index/cache.mjs';
|
|
6
|
-
|
|
7
|
-
export function runLoadSnapshot(argv) {
|
|
8
|
-
const options = parseCommon(argv);
|
|
9
|
-
const { status, index, metadata } = readFreshIndex(options.root);
|
|
10
|
-
const summary = graphSummary(index);
|
|
11
|
-
const communities = buildCommunities(index, { communities: 5, items: 5 });
|
|
12
|
-
const memoryAreas = buildMemoryAreas(index, { items: 4 });
|
|
13
|
-
const resolvedConfig = readMemoryConfig(options.root);
|
|
14
|
-
const memoryConfig = index.memoryConfig ?? memoryConfigSummary(resolvedConfig);
|
|
15
|
-
const pinnedFiles = buildPinnedFiles(options.root, resolvedConfig);
|
|
16
|
-
const memoryPolicy = {
|
|
17
|
-
source: memoryConfig.source ?? 'default',
|
|
18
|
-
sharedAreas: (memoryConfig.areas ?? []).filter((area) => area.scope === 'shared').map((area) => area.id),
|
|
19
|
-
localAreas: (memoryConfig.areas ?? []).filter((area) => area.scope === 'local').map((area) => area.id),
|
|
20
|
-
freshAreas: (memoryConfig.areas ?? []).filter((area) => area.freshness === 'fresh').map((area) => area.id),
|
|
21
|
-
staleAreas: (memoryConfig.areas ?? []).filter((area) => area.freshness === 'stale').map((area) => area.id),
|
|
22
|
-
};
|
|
23
|
-
const bounds = { communities: 5, communityItems: 5, memoryAreaItems: 4, pinnedPathItems: pinnedFiles.bounds.maxPaths, pinnedBodyItems: pinnedFiles.bounds.maxBodies, pinnedBodyChars: pinnedFiles.bounds.maxBodyChars, fullGraphIncluded: false, archiveBodiesIncluded: status.archiveBodiesIncluded, archiveMapIncluded: true };
|
|
24
|
-
const quality = {
|
|
25
|
-
indexedFiles: status.indexedFiles,
|
|
26
|
-
currentFiles: status.currentFiles,
|
|
27
|
-
shownCommunities: communities.communities.length,
|
|
28
|
-
totalCommunities: communities.total,
|
|
29
|
-
omittedCommunities: communities.omitted,
|
|
30
|
-
omittedCommunityItems: communities.communities.reduce((sum, community) => sum + (community.omitted ?? 0), 0),
|
|
31
|
-
memoryAreas: memoryAreas.total,
|
|
32
|
-
omittedMemoryAreaItems: memoryAreas.areas.reduce((sum, area) => sum + (area.omitted ?? 0), 0),
|
|
33
|
-
pinnedPaths: pinnedFiles.paths.length,
|
|
34
|
-
omittedPinnedPaths: pinnedFiles.omittedPaths,
|
|
35
|
-
pinnedBodies: pinnedFiles.bodies.length,
|
|
36
|
-
omittedPinnedBodies: pinnedFiles.omittedBodies,
|
|
37
|
-
graphNodes: summary.nodes,
|
|
38
|
-
graphEdges: summary.edges,
|
|
39
|
-
};
|
|
40
|
-
const commandGuidance = detectCommandGuidance(options.root);
|
|
41
|
-
let payload = { root: options.root, cache: status, metadata, graph: summary, memoryConfig, memoryPolicy, memoryAreas, pinnedFiles, communities, bounds, quality, commandGuidance };
|
|
42
|
-
const serialized = JSON.stringify(payload);
|
|
43
|
-
payload = { ...payload, quality: { ...quality, snapshotBytes: Buffer.byteLength(serialized), approxSnapshotTokens: Math.ceil(serialized.length / 4) } };
|
|
44
|
-
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
|
45
|
-
else console.log(`dotdotgod load snapshot\n- cache: ${status.status}${metadata.cacheRefreshed ? ' (refreshed)' : ''}\n- indexed files: ${status.indexedFiles}\n- current files: ${status.currentFiles}\n- archive bodies included: ${status.archiveBodiesIncluded ? 'yes' : 'no'}\n- graph: ${summary.nodes} nodes, ${summary.edges} edges\n- memory areas: ${memoryAreas.areas.length}/${memoryAreas.total} shown\n- pinned files: ${pinnedFiles.paths.length} paths (${pinnedFiles.omittedPaths} omitted), ${pinnedFiles.bodies.length} bodies (${pinnedFiles.omittedBodies} omitted)\n- communities: ${communities.communities.length}/${communities.total} shown, ${communities.omitted} omitted`);
|
|
46
|
-
}
|