@dotdotgod/cli 0.2.4 → 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 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, load bounded context snapshots, expand project references, or ask what should be checked after a file changes.
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,16 +27,16 @@ Common first commands:
27
27
  ```bash
28
28
  dotdotgod init .
29
29
  dotdotgod validate .
30
- dotdotgod load-snapshot .
30
+ dotdotgod query . "project documentation"
31
31
  dotdotgod graph impact . --changed <path> --compact
32
32
  ```
33
33
 
34
34
  ## What It Does
35
35
 
36
- - `init` creates `AGENTS.md`, thin agent entrypoints, docs indexes, active-plan space, archive map, and local cache ignores.
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
- - `load-snapshot` returns a bounded first-pass project map for AI agents, including files pinned through the `load.pinnedPaths`/`load.pinnedBodies` config so context such as code conventions stays visible on every load.
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 load-snapshot .
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
- | Give an agent a bounded project map | `dotdotgod load-snapshot .` |
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",
4
- "description": "CLI for dotdotgod validation, project memory snapshots, and graph indexing.",
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] [--force] [--dry-run] [--json]
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> [--force] [--json]
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> [--force] [--json]
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 'load-snapshot':
34
+ case 'query':
35
35
  return `Usage:
36
- dotdotgod load-snapshot <root> [--json]`;
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] [--force] [--dry-run] [--json]
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> [--force] [--json]
105
+ dotdotgod config init <root> [--json]
104
106
  dotdotgod status <root> [--json]
105
- dotdotgod load-snapshot <root> [--json]
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]
@@ -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, allowForce = false, usageKey = 'config') {
7
- const options = { root: '.', json: false, force: 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, overwritten: false, error: { code, message } };
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, isInit ? 'config init' : 'config');
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
- const rcPath = join(options.root, '.dotdotgodrc.json');
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: !existed, overwritten: existed };
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: ${existed ? 'overwrote' : 'created'} ${target}`);
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: configSourcePath(options.root, config.source), config: memoryConfigSummary(config), errors };
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 { runLoadSnapshot } from './commands/load-snapshot.mjs';
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 './load-snapshot/summary.mjs';
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 { runLoadSnapshot } from './commands/load-snapshot.mjs';
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 === 'load-snapshot') runLoadSnapshot(args);
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);
@@ -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', '.dotdotgodrc.json', 'package.json', 'pnpm-workspace.yaml', 'tsconfig.json', 'jsconfig.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,24 +1,22 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { basename, dirname, join, resolve } from 'node:path';
3
+ import { defaultDotdotgodConfigText } from './memory/config.mjs';
3
4
 
4
5
  function action(status, path, extra = {}) {
5
6
  return { status, path, ...extra };
6
7
  }
7
8
 
8
9
  function formatAction(item) {
9
- const extras = [];
10
- if (item.backup) extras.push(`backup=${item.backup}`);
11
- if (item.add) extras.push(`add=${item.add}`);
12
- 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}`;
13
12
  }
14
13
 
15
14
  function parseInitOptions(argv) {
16
- const options = { root: null, projectName: '', dotdotSetting: false, force: false, dryRun: false, json: false };
15
+ const options = { root: null, projectName: '', dotdotSetting: false, dryRun: false, json: false };
17
16
  for (let i = 0; i < argv.length; i += 1) {
18
17
  const arg = argv[i];
19
18
  if (arg === '--json') options.json = true;
20
19
  else if (arg === '--dotdot-setting') options.dotdotSetting = true;
21
- else if (arg === '--force') options.force = true;
22
20
  else if (arg === '--dry-run') options.dryRun = true;
23
21
  else if (arg === '--project-name') {
24
22
  const value = argv[i + 1];
@@ -50,25 +48,19 @@ function initError(options, code, message) {
50
48
 
51
49
  function writeInitFile(options, relativePath, content, actions) {
52
50
  const target = join(options.root, relativePath);
53
- if (existsSync(target) && !options.force) {
51
+ if (existsSync(target)) {
54
52
  actions.push(action('skipped', target));
55
53
  return;
56
54
  }
57
55
 
58
- let backup = '';
59
- if (existsSync(target) && options.force) {
60
- backup = `${target}.bak.${new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z')}`;
61
- if (!options.dryRun) renameSync(target, backup);
62
- }
63
-
64
56
  if (options.dryRun) {
65
- actions.push(action(backup ? 'would_replace' : 'would_create', target, backup ? { backup } : {}));
57
+ actions.push(action('would_create', target));
66
58
  return;
67
59
  }
68
60
 
69
61
  mkdirSync(dirname(target), { recursive: true });
70
- writeFileSync(target, `${content}\n`);
71
- actions.push(action(backup ? 'replaced' : 'created', target, backup ? { backup } : {}));
62
+ writeFileSync(target, content.endsWith('\n') ? content : `${content}\n`);
63
+ actions.push(action('created', target));
72
64
  }
73
65
 
74
66
  function ensureGitignoreEntry(options, entry, actions) {
@@ -298,9 +290,12 @@ export function runInit(argv, usage) {
298
290
 
299
291
  const actions = [];
300
292
  for (const [relativePath, content] of initFiles(options)) writeInitFile(options, relativePath, content, actions);
293
+
294
+ writeInitFile(options, 'dotdotgod.config.json', defaultDotdotgodConfigText(), actions);
295
+
301
296
  for (const entry of ['docs/plan', 'docs/archive', '.dotdotgod']) ensureGitignoreEntry(options, entry, actions);
302
297
 
303
- const payload = { ok: true, command: 'init', root: options.root, projectName: options.projectName, dryRun: options.dryRun, force: options.force, dotdotSetting: options.dotdotSetting, actions };
298
+ const payload = { ok: true, command: 'init', root: options.root, projectName: options.projectName, dryRun: options.dryRun, dotdotSetting: options.dotdotSetting, actions };
304
299
  if (options.json) console.log(JSON.stringify(payload, null, 2));
305
300
  else for (const item of actions) console.log(formatAction(item));
306
301
  }
@@ -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 MEMORY_CONFIG_FILES = ['dotdotgod.config.json', '.dotdotgodrc.json'];
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
- for (const name of MEMORY_CONFIG_FILES) {
537
- const path = join(root, name);
538
- if (!existsSync(path)) continue;
539
- try {
540
- const data = JSON.parse(readFileSync(path, 'utf8'));
541
- const errors = validateMemoryConfigData(data, root, name);
542
- if (errors.length > 0) return { ...defaultMemoryConfig(), source: name, errors };
543
- const configuredAreas = data.memory?.areas?.map(normalizeMemoryArea) ?? [];
544
- const traceability = data.traceability === undefined ? cloneTraceabilityPolicy() : normalizeTraceabilityPolicy(data.traceability);
545
- const validation = data.validation === undefined ? cloneValidationPolicy() : normalizeValidationPolicy(data.validation);
546
- const impactRanking = normalizeImpactRankingPolicy(data.impactRanking);
547
- const referenceExpansion = normalizeReferenceExpansionPolicy(data.referenceExpansion);
548
- const integrations = normalizeIntegrationsPolicy(data.integrations);
549
- const load = normalizeLoadPolicy(data.load);
550
- const planMode = normalizePlanModePolicy(data.planMode);
551
- return configuredAreas.length > 0 ? { source: name, areas: configuredAreas, traceability, validation, impactRanking, referenceExpansion, integrations, load, planMode, errors: [] } : { ...defaultMemoryConfig(), traceability, validation, impactRanking, referenceExpansion, integrations, load, planMode, source: name, errors: [] };
552
- } catch (error) {
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 '../memory/config.mjs';
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
- loadSnapshot: `${prefix} load-snapshot . --json`,
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
+ }
@@ -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>` or a lazy-refreshing command such as `dotdotgod load-snapshot <root> --json`.');
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>` or a lazy-refreshing command such as `dotdotgod load-snapshot <root> --json`.');
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
- }