@dotdotgod/cli 0.2.14 → 0.2.17

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
@@ -38,9 +38,8 @@ dotdotgod graph impact . --changed <path> --compact
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
- - `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.
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. Non-seed scores use fixed weighted PPR connection `80` plus memory policy `20`. A bounded request-local multilingual vector overlay participates in PPR when the query cache is available; vector preparation failures degrade to structural-only results.
42
42
  - `traceability links` checks or repairs generated Markdown traceability-link sections.
43
- - `trello sync` plans or writes Trello linked-doc metadata from configured docs paths.
44
43
 
45
44
  ## Commands
46
45
 
@@ -77,7 +76,6 @@ dotdotgod graph communities .
77
76
  | Resolve `[[...]]` references from a prompt | `dotdotgod expand . "Update [[PLAN_MODE]]"` |
78
77
  | Repair generated traceability-link sections | `dotdotgod traceability links . --write` |
79
78
  | See what else to inspect after a change | `dotdotgod graph impact . --changed <path> --compact` |
80
- | Preview Trello linked-doc updates | `dotdotgod trello sync . --dry-run` |
81
79
 
82
80
  ## Changed-File Impact
83
81
 
@@ -87,17 +85,19 @@ $ dotdotgod graph impact . --changed packages/cli/src/core.mjs --compact
87
85
 
88
86
  ```text
89
87
  docs:
90
- - docs/spec/REFERENCE_EXPANSION.md (91; incoming:implemented_by, semantic_similarity)
91
- - docs/test/REFERENCE_EXPANSION.md (65.3; verified_by, semantic_similarity)
88
+ - docs/spec/CONFIG_COMMAND.md (56.8; incoming:links_to, incoming:implemented_by)
89
+ - docs/spec/cli/TRACEABILITY_LINKS.md (26.3; vector_similarity)
90
+
91
+ tests:
92
+ - packages/cli/test/core.test.mjs (6.5; verified_by)
92
93
 
93
94
  files:
94
95
  - packages/cli/src/core.mjs (100; changed-file)
95
- - packages/pi/extensions/plan-mode/index.ts (45; implemented_by, semantic_similarity)
96
96
  ```
97
97
 
98
98
  `graph impact` needs at least one `--changed <path>` and accepts repeated options. Multi-file output preserves input order, deduplicates repeated paths, returns a bounded combined ranking, and includes the top five non-seed results for each changed file. Use `--compact` for short text, `--yml` or `--yaml` for compact structured agent-facing output, and `--json` for machine-readable detail.
99
99
 
100
- The graph is built from maintained project files: Markdown links, README routes, headings, traceability blocks, package metadata, memory areas, imports, exports, commands, tests, and deterministic routing hints.
100
+ The indexed graph is built from maintained project files: Markdown links, README routes, headings, configured traceability relations, package metadata, scripts and packaged resources, dependencies, and memory-area membership. Impact analysis can add request-local vector edges for candidate discovery and PPR without mutating that indexed graph.
101
101
 
102
102
  ## Validation and Verification Boundaries
103
103
 
@@ -121,7 +121,7 @@ The default scaffold gives docs explicit roles:
121
121
  - `docs/archive/README.md`: local archive map included by default.
122
122
  - `docs/archive/**`: local archive bodies excluded by default unless targeted.
123
123
 
124
- Use `memory.areas` to customize memory classification and retrieval priority. Use `traceability.required` and `traceability.exclude` to customize which Markdown paths need `json dotdotgod` traceability blocks. Use `validation.markdown`, `impactRanking`, and `referenceExpansion` to tune size budgets, impact scoring, and fuzzy prompt matching.
124
+ Use `memory.areas` to customize memory classification and retrieval priority. Use `traceability.required` and `traceability.exclude` to select which Markdown paths need `json dotdotgod` blocks, and use ordered `traceability.keys` definitions to configure string-array labels, path or command targets, graph relations, and PPR weights. Use `validation.markdown` for size budgets and `referenceExpansion` for fuzzy prompt matching. The complete `impactRanking` namespace is non-blocking compatibility input: valid semantic candidate controls apply, malformed values fall back to defaults, and retired or unknown fields are ignored without changing fixed scoring. Legacy `load.pinnedPaths` and `load.pinnedBodies` values are also ignored.
125
125
 
126
126
  ## Indexing Scope
127
127
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdotgod/cli",
3
- "version": "0.2.14",
3
+ "version": "0.2.17",
4
4
  "description": "CLI for dotdotgod validation, local documentation search, and graph indexing.",
5
5
  "type": "module",
6
6
  "license": "Elastic-2.0",
package/src/cli/usage.mjs CHANGED
@@ -42,20 +42,6 @@ Search shared project documentation with the local multilingual E5 vector index.
42
42
  case 'expand':
43
43
  return `Usage:
44
44
  dotdotgod expand <root> <prompt> [--max-results n] [--include-archive] [--with-impact] [--fuzzy] [--json]`;
45
- case 'trello':
46
- return `Usage:
47
- dotdotgod trello sync <root> [--dry-run]`;
48
- case 'trello sync':
49
- return `Usage:
50
- dotdotgod trello sync <root> [--dry-run]
51
-
52
- Options:
53
- --dry-run Preview planned Trello updates without calling Trello APIs.
54
-
55
- Write mode:
56
- Omit --dry-run only in the trusted GitHub Actions default-branch push workflow.
57
- Requires TRELLO_API_KEY and TRELLO_TOKEN in the trusted workflow environment.
58
- Local/manual and pull request usage should run --dry-run.`;
59
45
  case 'traceability':
60
46
  return `Usage:
61
47
  dotdotgod traceability links <root> [--check|--write] [--json]`;
@@ -95,7 +81,6 @@ Commands:
95
81
  traceability links Check or write generated traceability links.
96
82
  graph impact Find files related to changed files.
97
83
  graph communities Find groups of related project-memory nodes.
98
- trello sync Synchronize Trello cards and documentation.
99
84
 
100
85
  Options:
101
86
  -h, --help Show help.
@@ -144,7 +129,6 @@ export function printVersion() {
144
129
  export function helpCommandFromArgs(args) {
145
130
  const nonHelp = args.filter((arg) => !isHelpToken(arg));
146
131
  if (nonHelp[0] === 'graph' && nonHelp[1]) return `graph ${nonHelp[1]}`;
147
- if (nonHelp[0] === 'trello' && nonHelp[1]) return `trello ${nonHelp[1]}`;
148
132
  if (nonHelp[0] === 'traceability' && nonHelp[1]) return `traceability ${nonHelp[1]}`;
149
133
  if (nonHelp[0] === 'config' && nonHelp[1] === 'init') return 'config init';
150
134
  return nonHelp[0] ?? 'root';
@@ -36,14 +36,16 @@ function formatConfigOutput(payload) {
36
36
  lines.push(`- memory areas: ${payload.config.areas.length}`);
37
37
  lines.push(`- traceability required: ${(payload.config.traceability.required ?? []).join(', ') || 'none'}`);
38
38
  lines.push(`- traceability exclude: ${(payload.config.traceability.exclude ?? []).join(', ') || 'none'}`);
39
+ lines.push('- traceability keys:');
40
+ for (const definition of payload.config.traceability.keys ?? []) lines.push(` - ${definition.key}: ${definition.target} -> ${definition.relation} (weight=${definition.weight}; label=${definition.label})`);
39
41
  lines.push(`- validation markdown: maxLines=${payload.config.validation.markdown.maxLines}, maxChars=${payload.config.validation.markdown.maxChars}`);
40
42
  lines.push(`- validation markdown exclude: ${(payload.config.validation.markdown.exclude ?? []).join(', ') || 'none'}`);
41
- lines.push(`- impact ranking preset: ${payload.config.impactRanking.preset}`);
43
+ const fixedImpact = payload.config.impactRanking.fixed;
44
+ lines.push(`- impact ranking: fixed PPR=${fixedImpact.connectionCap}, memory=${fixedImpact.memoryCap}, reference=${fixedImpact.ppr.reference}`);
42
45
  lines.push(`- load pinned paths: ${(payload.config.load?.pinnedPaths ?? []).join(', ') || 'none'}`);
43
46
  lines.push(`- load pinned bodies: ${(payload.config.load?.pinnedBodies ?? []).join(', ') || 'none'}`);
44
47
  lines.push(`- load documentation summary exclude: ${(payload.config.load?.documentationSummary?.exclude ?? []).join(', ') || 'none'}`);
45
48
  lines.push(`- plan mode writable paths: ${(payload.config.planMode?.writablePaths ?? []).join(', ') || 'none'}`);
46
- lines.push(`- trello sync paths: ${(payload.config.integrations?.trello?.syncPaths ?? []).join(', ') || 'none'}`);
47
49
  const lowSignal = payload.config.referenceExpansion?.fuzzy?.lowSignal ?? { terms: [], add: [], remove: [] };
48
50
  lines.push(`- fuzzy low-signal terms: ${(lowSignal.terms ?? []).join(', ') || 'none'} (add: ${(lowSignal.add ?? []).join(', ') || 'none'}; remove: ${(lowSignal.remove ?? []).join(', ') || 'none'})`);
49
51
  if (errors.length > 0) {
@@ -2,6 +2,8 @@ import { commandUsage, parseCommon, usage } from '../cli/usage.mjs';
2
2
  import { buildCommunities } from '../graph/communities.mjs';
3
3
  import { graphSummary, readFreshIndex } from '../index/cache.mjs';
4
4
  import { buildCompactImpactReport, buildImpactReport } from '../impact/report.mjs';
5
+ import { buildVectorImpactOverlay } from '../impact/vector-overlay.mjs';
6
+ import { canonicalizeChangedPath } from '../impact/vector-profile.mjs';
5
7
  import { formatCompactImpactOutput, formatYmlGraphImpactError, formatYmlImpactOutput } from '../impact/format.mjs';
6
8
 
7
9
  const MAX_CHANGED_FILES = 20;
@@ -65,8 +67,10 @@ export async function runGraph(argv) {
65
67
  }
66
68
  usage(message, 'graph impact');
67
69
  }
70
+ if (isImpact) options.changed = [...new Set(options.changed.map((path) => canonicalizeChangedPath(options.root, path)).filter(Boolean))];
68
71
  const { status, index, metadata } = readFreshIndex(options.root);
69
- const rawImpact = isImpact ? buildImpactReport(index, options.changed) : undefined;
72
+ const overlay = isImpact ? await buildVectorImpactOverlay(options.root, index, options.changed) : undefined;
73
+ const rawImpact = isImpact ? buildImpactReport(index, options.changed, { overlay, verboseSemantic: options.json }) : undefined;
70
74
  const impact = isImpact && (options.compact || options.yml) ? buildCompactImpactReport(rawImpact) : rawImpact;
71
75
  const payload = isImpact
72
76
  ? { ok: status.ok, command: 'graph impact', compact: options.compact || undefined, root: options.root, status, metadata, changed: options.changed[0], changedFiles: options.changed, related: impact.related, impact }
@@ -3,7 +3,7 @@ import { usage } from '../cli/usage.mjs';
3
3
  import { readMemoryConfig } from '../memory/config.mjs';
4
4
  import { collectDocumentationChunks } from '../query/chunks.mjs';
5
5
  import { embedTexts } from '../query/embedder.mjs';
6
- import { readVectorCache, VECTOR_DIMENSIONS, VECTOR_MODEL, writeVectorCache } from '../query/store.mjs';
6
+ import { isValidNormalizedVectorValues, readVectorCache, VECTOR_DIMENSIONS, VECTOR_MODEL, writeVectorCache } from '../query/store.mjs';
7
7
 
8
8
  const DEFAULT_LIMIT = 30;
9
9
  const MAX_LIMIT = 100;
@@ -31,10 +31,33 @@ export function parseQueryOptions(argv) {
31
31
  return options;
32
32
  }
33
33
 
34
- function cosine(left, vectors, offset) {
34
+ function assertNormalizedVector(vector, label) {
35
+ if (!vector || vector.length !== VECTOR_DIMENSIONS || !isValidNormalizedVectorValues(vector)) throw new Error(`${label} must be a finite normalized ${VECTOR_DIMENSIONS}-dimensional vector.`);
36
+ }
37
+
38
+ export function cosineScore(left, vectors, offset = 0) {
39
+ assertNormalizedVector(left, 'Query vector');
40
+ const right = vectors?.subarray ? vectors.subarray(offset, offset + VECTOR_DIMENSIONS) : vectors?.slice(offset, offset + VECTOR_DIMENSIONS);
41
+ assertNormalizedVector(right, 'Passage vector');
35
42
  let score = 0;
36
- for (let index = 0; index < VECTOR_DIMENSIONS; index += 1) score += left[index] * vectors[offset + index];
37
- return score;
43
+ for (let index = 0; index < VECTOR_DIMENSIONS; index += 1) score += left[index] * right[index];
44
+ if (!Number.isFinite(score) || score < -1.000001 || score > 1.000001) throw new Error('Cosine similarity was outside the finite normalized range.');
45
+ return Math.max(-1, Math.min(1, score));
46
+ }
47
+
48
+ export function rankVectorFiles(queryVector, index) {
49
+ assertNormalizedVector(queryVector, 'Query vector');
50
+ if (!index || !Array.isArray(index.chunks) || index.vectors?.length !== index.chunks.length * VECTOR_DIMENSIONS || !isValidNormalizedVectorValues(index.vectors)) throw new Error('Vector index contains invalid passage vectors.');
51
+ const ranked = index.chunks.map((chunk, position) => ({ ...chunk, vectorScore: cosineScore(queryVector, index.vectors, position * VECTOR_DIMENSIONS) }))
52
+ .sort((left, right) => right.vectorScore - left.vectorScore || left.path.localeCompare(right.path) || left.id.localeCompare(right.id));
53
+ const results = [];
54
+ const seen = new Set();
55
+ for (const chunk of ranked) {
56
+ if (seen.has(chunk.path)) continue;
57
+ seen.add(chunk.path);
58
+ results.push(chunk);
59
+ }
60
+ return results;
38
61
  }
39
62
 
40
63
  function lexicalBoost(query, chunk) {
@@ -66,7 +89,11 @@ export async function buildVectorIndex(root, embed = embedTexts) {
66
89
  for (let offset = 0; offset < missing.length; offset += EMBED_BATCH_SIZE) {
67
90
  const batch = missing.slice(offset, offset + EMBED_BATCH_SIZE);
68
91
  const embedded = await embed(batch.map(({ chunk }) => `passage: ${chunk.passage}`));
69
- embedded.forEach((vector, index) => vectors.set(vector, batch[index].index * VECTOR_DIMENSIONS));
92
+ if (!Array.isArray(embedded) || embedded.length !== batch.length) throw new Error('Embedder returned an unexpected passage-vector count.');
93
+ embedded.forEach((vector, index) => {
94
+ assertNormalizedVector(vector, 'Passage vector');
95
+ vectors.set(vector, batch[index].index * VECTOR_DIMENSIONS);
96
+ });
70
97
  }
71
98
  const storedChunks = chunks.map(({ passage, ...chunk }) => chunk);
72
99
  const manifest = writeVectorCache(root, storedChunks, vectors, {
@@ -81,9 +108,12 @@ export async function queryDocumentation(root, query, options = {}) {
81
108
  const embed = options.embed ?? embedTexts;
82
109
  const limit = options.limit ?? DEFAULT_LIMIT;
83
110
  const index = await buildVectorIndex(root, embed);
84
- const [queryVector] = await embed([`query: ${query}`]);
111
+ const embeddedQuery = await embed([`query: ${query}`]);
112
+ if (!Array.isArray(embeddedQuery) || embeddedQuery.length !== 1) throw new Error('Embedder returned an unexpected query-vector count.');
113
+ const [queryVector] = embeddedQuery;
114
+ assertNormalizedVector(queryVector, 'Query vector');
85
115
  const rankedChunks = index.chunks.map((chunk, position) => {
86
- const vectorScore = cosine(queryVector, index.vectors, position * VECTOR_DIMENSIONS);
116
+ const vectorScore = cosineScore(queryVector, index.vectors, position * VECTOR_DIMENSIONS);
87
117
  return { ...chunk, score: Number((vectorScore + lexicalBoost(query, chunk)).toFixed(6)), vectorScore: Number(vectorScore.toFixed(6)) };
88
118
  }).sort((left, right) => right.score - left.score || left.path.localeCompare(right.path));
89
119
  const results = [];
@@ -3,6 +3,7 @@ import { join, resolve } from 'node:path';
3
3
  import { usage } from '../cli/usage.mjs';
4
4
  import { rel } from '../common/paths.mjs';
5
5
  import { extractDotdotgodTraceabilityBlocks, syncTraceabilityLinksInContent, validateTraceabilityLinksRegion } from '../docs/traceability.mjs';
6
+ import { readMemoryConfig } from '../memory/config.mjs';
6
7
 
7
8
  function collectDocsMarkdownFiles(root) {
8
9
  const docs = join(root, 'docs');
@@ -43,15 +44,17 @@ export function parseTraceabilityOptions(argv) {
43
44
 
44
45
  export function runTraceability(argv) {
45
46
  const options = parseTraceabilityOptions(argv);
47
+ const config = readMemoryConfig(options.root);
46
48
  const files = collectDocsMarkdownFiles(options.root);
47
49
  const results = [];
48
- const errors = [];
50
+ const errors = [...(config.errors ?? [])];
49
51
  for (const file of files) {
52
+ if (options.write && errors.length > 0) break;
50
53
  const content = readFileSync(file, 'utf8');
51
54
  for (const error of validateTraceabilityLinksRegion(content, options.root, file)) errors.push(error);
52
55
  const blocks = extractDotdotgodTraceabilityBlocks(content).filter((block) => !block.error);
53
56
  if (blocks.length === 0) continue;
54
- const result = syncTraceabilityLinksInContent(content, blocks.at(-1).data, options.root, file);
57
+ const result = syncTraceabilityLinksInContent(content, blocks.at(-1).data, options.root, file, config);
55
58
  if (!result.ok) {
56
59
  for (const error of result.errors ?? []) errors.push(error);
57
60
  continue;
package/src/core.mjs CHANGED
@@ -7,40 +7,34 @@ 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';
10
- import { runTrelloSync } from './trello/sync.mjs';
11
10
 
12
11
  export { CACHE_VERSION } from './index/constants.mjs';
13
12
  export { commandUsage, hasHelpToken, helpCommandFromArgs, isHelpToken, isVersionToken, parseCommon, printVersion, usage } from './cli/usage.mjs';
14
13
  export { rel } from './common/paths.mjs';
15
14
  export { extractAnchors, extractFirstHeading, extractLinks, headingToAnchor, isKebabCase, isNumberedSeriesFilename, isUpperSnakeMarkdown, removeCodeBlocks, suggestFilenameFromHeading } from './docs/markdown.mjs';
16
15
  export { extractDotdotgodTraceabilityBlocks, findTraceabilityLinksRegion, isLocalRelativeTraceabilityPath, renderCompactTraceabilityBlock, stripTraceabilityLinksRegion, syncTraceabilityLinksInContent, traceabilityExample, validateTraceabilityBlock, validateTraceabilityLinksRegion, validateTraceabilityPlacement } from './docs/traceability.mjs';
17
- export { DEFAULT_IMPACT_RANKING_POLICY, DEFAULT_INTEGRATIONS_POLICY, DEFAULT_LOAD_POLICY, DEFAULT_VALIDATION_POLICY, SEMANTIC_RELATIONS, cloneImpactRankingPolicy, cloneIntegrationsPolicy, cloneLoadPolicy, cloneReferenceExpansionPolicy, cloneValidationPolicy, defaultDotdotgodConfigData, defaultDotdotgodConfigText, defaultMemoryConfig, isMarkdownSizeExcluded, isSecretLikePathPattern, isValidPathPattern, matchPathPattern, memoryAreaForPath, memoryConfigSummary, normalizeLowSignalTerm, readMemoryConfig, requiresTraceability, resolveMemoryArea, trelloSyncPaths, validateMemoryConfigData } from './memory/config.mjs';
16
+ export { DEFAULT_IMPACT_RANKING_POLICY, DEFAULT_LOAD_POLICY, DEFAULT_VALIDATION_POLICY, SEMANTIC_RELATIONS, cloneImpactRankingPolicy, cloneLoadPolicy, cloneReferenceExpansionPolicy, cloneValidationPolicy, defaultDotdotgodConfigData, defaultDotdotgodConfigText, defaultMemoryConfig, isMarkdownSizeExcluded, isSecretLikePathPattern, isValidPathPattern, matchPathPattern, memoryAreaForPath, memoryConfigSummary, normalizeLowSignalTerm, readMemoryConfig, requiresTraceability, resolveMemoryArea, validateMemoryConfigData } from './memory/config.mjs';
18
17
  export { buildCommunities, relationWeight } from './graph/communities.mjs';
19
18
  export { addEdge, addNode, compactGraph, expandGraph, graphStats, jsonSize, shardFile, writeJson } from './graph/store.mjs';
20
19
  export { cacheFile, collectIndexFiles, fingerprint, shouldIndexPath } from './index/files.mjs';
21
- export { buildMemoryAreas, buildPinnedFiles, detectCommandGuidance, detectPackageManager } from './memory/summary.mjs';
20
+ export { buildMemoryAreas, detectCommandGuidance, detectPackageManager } from './memory/summary.mjs';
22
21
  export { isReadmeIndexPath, retrievalMetadataForPath } from './graph/metadata.mjs';
23
22
  export { buildGraph } from './graph/extract.mjs';
24
23
  export { addDeterministicSemanticEdges } from './graph/semantic.mjs';
25
24
  export { buildIndex, writeIndex, readIndex, getStatus, runIndex, runStatus, readFreshIndex, graphSummary } from './index/cache.mjs';
26
25
  export { runValidate } from './validate/run.mjs';
27
26
  export { buildImpactReport, buildCompactImpactReport } from './impact/report.mjs';
27
+ export { buildChangedFileProfile, MAX_VECTOR_PROFILE_CHARS, MAX_VECTOR_PROFILE_FILE_BYTES, MAX_VECTOR_PROFILE_METADATA_ITEMS } from './impact/vector-profile.mjs';
28
+ export { buildVectorImpactOverlay, VECTOR_RELATION_WEIGHT } from './impact/vector-overlay.mjs';
28
29
  export { formatCompactImpactOutput, formatYmlGraphImpactError, formatYmlImpactOutput } from './impact/format.mjs';
29
30
  export { runConfig } from './commands/config.mjs';
30
- export { buildVectorIndex, parseQueryOptions, queryDocumentation, runQuery } from './commands/query.mjs';
31
+ export { buildVectorIndex, cosineScore, parseQueryOptions, queryDocumentation, rankVectorFiles, runQuery } from './commands/query.mjs';
31
32
  export { chunkMarkdown, collectDocumentationChunks, collectDocumentationMarkdown, textFingerprint } from './query/chunks.mjs';
32
33
  export { readVectorCache, VECTOR_DIMENSIONS, VECTOR_MODEL, VECTOR_SCHEMA_VERSION, vectorCachePaths, writeVectorCache } from './query/store.mjs';
33
34
  export { extractBracketReferences, extractFuzzyReferences, normalizeReferenceAlias } from './reference/extract.mjs';
34
35
  export { resolveReferenceCandidates, runResolve, runExpand } from './reference/resolve.mjs';
35
36
  export { parseTraceabilityOptions, runTraceability } from './commands/traceability.mjs';
36
37
  export { parseGraphOptions, runGraph } from './commands/graph.mjs';
37
- export { planTrelloDryRun, runTrelloSync } from './trello/sync.mjs';
38
- export { parseTrelloMetadata, extractTrelloShortLink } from './trello/metadata.mjs';
39
- export { normalizeGitHubRemote, resolveGitHubFileUrl, resolveGitHubRepositoryIdentity } from './trello/github-url.mjs';
40
- export { buildLinkedDocsData, summarizeTraceability } from './trello/summary.mjs';
41
- export { formatTrelloDryRunReport, formatTrelloWriteReport } from './trello/report.mjs';
42
- export { createTrelloClient, resolveTrelloCredentials } from './trello/client.mjs';
43
- export { customFieldTextValue, DEFAULT_DOTDOTGOD_CUSTOM_FIELD_NAME, dotdotgodCustomFieldName, mergeLinkedDocsCustomFieldValue, parseLinkedDocsCustomFieldPayload, serializeLinkedDocsCustomFieldPayload } from './trello/custom-fields.mjs';
44
38
 
45
39
  export async function runCli(argv = process.argv.slice(2)) {
46
40
  const [command = 'help', ...args] = argv;
@@ -58,13 +52,5 @@ export async function runCli(argv = process.argv.slice(2)) {
58
52
  else if (command === 'expand') runExpand(args);
59
53
  else if (command === 'traceability') runTraceability(args);
60
54
  else if (command === 'graph') await runGraph(args);
61
- else if (command === 'trello') {
62
- const result = await Promise.resolve(runTrelloSync(args));
63
- if (result.stdout) process.stdout.write(result.stdout);
64
- if (result.stderr) {
65
- console.error(result.stderr);
66
- console.error(commandUsage('trello'));
67
- }
68
- process.exit(result.exitCode);
69
- } else usage(`Unknown command: ${command}`);
55
+ else usage(`Unknown command: ${command}`);
70
56
  }
@@ -1,20 +1,26 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { basename, dirname, extname, relative, resolve } from 'node:path';
3
3
  import { rel } from '../common/paths.mjs';
4
- import { defaultMemoryConfig, resolveMemoryArea } from '../memory/config.mjs';
4
+ import { DEFAULT_TRACEABILITY_KEYS, defaultMemoryConfig, resolveMemoryArea } from '../memory/config.mjs';
5
5
 
6
6
  function isSecretIndexPath(path) {
7
7
  return /(^|\/)(\.env|\.npmrc|\.pypirc|id_rsa|id_dsa|id_ed25519|credentials|secrets?)(\.|\/|$)/i.test(path);
8
8
  }
9
9
 
10
- const TRACEABILITY_PATH_FIELDS = ['implementedBy', 'verifiedBy', 'relatedDocs'];
11
- const TRACEABILITY_COMMAND_FIELDS = ['verificationCommands'];
12
- const CONTRACT_ALLOWED_FIELDS = new Set(['id', 'title', 'sections', ...TRACEABILITY_PATH_FIELDS, ...TRACEABILITY_COMMAND_FIELDS]);
10
+ function traceabilityKeys(config = defaultMemoryConfig()) {
11
+ return config.traceability?.keys ?? DEFAULT_TRACEABILITY_KEYS;
12
+ }
13
+
14
+ function traceabilityDefinitionMap(config = defaultMemoryConfig()) {
15
+ return new Map(traceabilityKeys(config).map((definition) => [definition.key, definition]));
16
+ }
13
17
  export const TRACEABILITY_LINKS_START = '<!-- dotdotgod:traceability-links:start version=1 source=json-dotdotgod -->';
14
18
  export const TRACEABILITY_LINKS_END = '<!-- dotdotgod:traceability-links:end -->';
15
19
 
16
- export function traceabilityExample() {
17
- return 'Expected dotdotgod traceability block:\n\n```json dotdotgod\n{\n "kind": "spec",\n "implementedBy": ["packages/..."],\n "verifiedBy": ["packages/..."],\n "relatedDocs": ["docs/..."],\n "verificationCommands": ["pnpm ..."],\n "contracts": [{\n "id": "FEATURE-BEHAVIOR-001",\n "title": "Focused behavior contract",\n "sections": ["Focused Behavior"],\n "implementedBy": ["packages/..."],\n "verifiedBy": ["packages/..."],\n "relatedDocs": ["docs/..."],\n "verificationCommands": ["pnpm ..."]\n }]\n}\n```\n\nProperty guidance:\n- kind: use "spec" for behavior specs.\n- implementedBy: source/config/script files that implement this spec\'s behavior.\n- verifiedBy: test files or verification docs that check this behavior.\n- relatedDocs: docs with relevant architecture, test strategy, or product context.\n- verificationCommands: commands an agent can run to verify this behavior.\n- contracts: optional focused behavior contracts; each contract requires non-empty id and title, may include sections as navigation hints, and may include the same path and command fields. Unknown contract fields are validation errors.';
20
+ export function traceabilityExample(config = defaultMemoryConfig()) {
21
+ const fields = traceabilityKeys(config).map((definition) => ` "${definition.key}": [${definition.target === 'command' ? '"pnpm ..."' : '"packages/..."'}]`).join(',\n');
22
+ const guidance = traceabilityKeys(config).map((definition) => `- ${definition.key}: ${definition.description}`).join('\n');
23
+ return `Expected dotdotgod traceability block:\n\n\`\`\`json dotdotgod\n{\n "kind": "spec"${fields ? `,\n${fields}` : ''}\n}\n\`\`\`\n\nProperty guidance:\n- kind: use "spec" for behavior specs.\n${guidance}${guidance ? '\n' : ''}- contracts: optional focused behavior contracts; each contract requires non-empty id and title, may include sections, and may include configured traceability fields. Unknown contract fields are validation errors.`;
18
24
  }
19
25
 
20
26
  function lineForOffset(content, offset) {
@@ -46,8 +52,8 @@ export function isLocalRelativeTraceabilityPath(value) {
46
52
  return !isSecretIndexPath(value);
47
53
  }
48
54
 
49
- function traceabilityFieldError(file, code, field, message, line = null) {
50
- return { file, line, code, message: `${field ? `Field "${field}": ` : ''}${message}\nFix: update the traceability block so it matches the expected schema and points to existing project files or commands.\n\n${traceabilityExample()}` };
55
+ function traceabilityFieldError(file, code, field, message, line = null, config = defaultMemoryConfig()) {
56
+ return { file, line, code, message: `${field ? `Field "${field}": ` : ''}${message}\nFix: update the traceability block so it matches the expected schema and points to existing project files or commands.\n\n${traceabilityExample(config)}` };
51
57
  }
52
58
 
53
59
  export function validateTraceabilityPlacement(content, root, file) {
@@ -124,12 +130,13 @@ function renderPathList(title, paths, root, file) {
124
130
  return lines;
125
131
  }
126
132
 
127
- function renderCommandList(commands) {
128
- if (!Array.isArray(commands) || commands.length === 0) return [];
129
- return ['- Verification commands:', ...commands.map((command) => ` - \`${escapeCodeSpan(command)}\``)];
133
+ function renderDefinitionList(definition, values, root, file) {
134
+ if (!Array.isArray(values) || values.length === 0) return [];
135
+ if (definition.target === 'command') return [`- ${definition.label}:`, ...values.map((command) => ` - \`${escapeCodeSpan(command)}\``)];
136
+ return renderPathList(definition.label, values, root, file);
130
137
  }
131
138
 
132
- function renderContractList(contracts) {
139
+ function renderContractList(contracts, definitions) {
133
140
  if (!Array.isArray(contracts) || contracts.length === 0) return [];
134
141
  const lines = ['- Contracts:'];
135
142
  for (const contract of contracts) {
@@ -138,23 +145,19 @@ function renderContractList(contracts) {
138
145
  const title = typeof contract.title === 'string' ? contract.title : '<missing-title>';
139
146
  const details = [];
140
147
  if (Array.isArray(contract.sections) && contract.sections.length > 0) details.push(`sections: ${contract.sections.length}`);
141
- for (const [field, label] of [['implementedBy', 'impl'], ['verifiedBy', 'verify'], ['relatedDocs', 'docs'], ['verificationCommands', 'commands']]) {
142
- if (Array.isArray(contract[field]) && contract[field].length > 0) details.push(`${label}: ${contract[field].length}`);
143
- }
148
+ for (const definition of definitions) if (Array.isArray(contract[definition.key]) && contract[definition.key].length > 0) details.push(`${definition.key}: ${contract[definition.key].length}`);
144
149
  lines.push(` - \`${escapeCodeSpan(id)}\` — ${escapeMarkdownText(title)}${details.length > 0 ? ` (${details.join(', ')})` : ''}`);
145
150
  }
146
151
  return lines;
147
152
  }
148
153
 
149
- export function renderTraceabilityLinks(data, root, file) {
154
+ export function renderTraceabilityLinks(data, root, file, config = defaultMemoryConfig()) {
155
+ const definitions = traceabilityKeys(config);
150
156
  const body = [
151
157
  '### Traceability Links',
152
158
  '',
153
- ...renderPathList('Implemented by', data?.implementedBy, root, file),
154
- ...renderPathList('Verified by', data?.verifiedBy, root, file),
155
- ...renderPathList('Related docs', data?.relatedDocs, root, file),
156
- ...renderCommandList(data?.verificationCommands),
157
- ...renderContractList(data?.contracts),
159
+ ...definitions.flatMap((definition) => renderDefinitionList(definition, data?.[definition.key], root, file)),
160
+ ...renderContractList(data?.contracts, definitions),
158
161
  ];
159
162
  return `${TRACEABILITY_LINKS_START}\n<!-- generated: do not edit manually -->\n\n${body.join('\n')}\n\n${TRACEABILITY_LINKS_END}`;
160
163
  }
@@ -163,10 +166,10 @@ export function renderCompactTraceabilityBlock(data) {
163
166
  return `\`\`\`json dotdotgod\n${JSON.stringify(data)}\n\`\`\``;
164
167
  }
165
168
 
166
- export function syncTraceabilityLinksInContent(content, data, root, file) {
169
+ export function syncTraceabilityLinksInContent(content, data, root, file, config = defaultMemoryConfig()) {
167
170
  const region = findTraceabilityLinksRegion(content);
168
171
  if (region.status === 'invalid') return { ok: false, changed: false, errors: validateTraceabilityLinksRegion(content, root, file) };
169
- const generated = renderTraceabilityLinks(data, root, file);
172
+ const generated = renderTraceabilityLinks(data, root, file, config);
170
173
  const blocks = extractDotdotgodTraceabilityBlocks(content).filter((block) => !block.error);
171
174
  if (blocks.length === 0) return { ok: false, changed: false, errors: [traceabilityFieldError(rel(root, file), 'TRACEABILITY_LINKS_MISSING_BLOCK', null, 'Cannot generate traceability links without a valid `json dotdotgod` block.')] };
172
175
  const block = blocks.at(-1);
@@ -216,12 +219,19 @@ function validateTraceabilityCommandArray(data, field, add, { required = true, d
216
219
  for (const value of data[field]) if (typeof value !== 'string' || value.trim().length === 0) add('TRACEABILITY_INVALID_COMMAND', displayField, `invalid command: ${JSON.stringify(value)}.`);
217
220
  }
218
221
 
222
+ function validateConfiguredArray(data, definition, add, root, config, options) {
223
+ if (definition.target === 'command') validateTraceabilityCommandArray(data, definition.key, add, options);
224
+ else validateTraceabilityPathArray(data, definition.key, add, root, config, options);
225
+ }
226
+
219
227
  function validateTraceabilityContracts(data, root, add, config) {
220
228
  if (data.contracts === undefined) return;
221
229
  if (!Array.isArray(data.contracts)) {
222
230
  add('TRACEABILITY_INVALID_FIELD', 'contracts', 'must be an array of focused contract objects.');
223
231
  return;
224
232
  }
233
+ const definitions = traceabilityKeys(config);
234
+ const allowedFields = new Set(['id', 'title', 'sections', ...definitions.map((definition) => definition.key)]);
225
235
  const ids = new Set();
226
236
  for (const [index, contract] of data.contracts.entries()) {
227
237
  const base = `contracts[${index}]`;
@@ -230,7 +240,7 @@ function validateTraceabilityContracts(data, root, add, config) {
230
240
  continue;
231
241
  }
232
242
  for (const field of Object.keys(contract)) {
233
- if (!CONTRACT_ALLOWED_FIELDS.has(field)) add('TRACEABILITY_INVALID_FIELD', `${base}.${field}`, 'is not a supported contract field.');
243
+ if (!allowedFields.has(field)) add('TRACEABILITY_INVALID_FIELD', `${base}.${field}`, 'is not a supported contract field.');
234
244
  }
235
245
  if (typeof contract.id !== 'string' || contract.id.trim().length === 0) {
236
246
  add('TRACEABILITY_INVALID_FIELD', `${base}.id`, 'must be a non-empty stable contract ID string.');
@@ -246,21 +256,22 @@ function validateTraceabilityContracts(data, root, add, config) {
246
256
  if (typeof section !== 'string' || section.trim().length === 0) add('TRACEABILITY_INVALID_FIELD', `${base}.sections[${sectionIndex}]`, 'must be a non-empty heading title string.');
247
257
  }
248
258
  }
249
- for (const field of TRACEABILITY_PATH_FIELDS) validateTraceabilityPathArray(contract, field, add, root, config, { required: false, displayField: `${base}.${field}` });
250
- for (const field of TRACEABILITY_COMMAND_FIELDS) validateTraceabilityCommandArray(contract, field, add, { required: false, displayField: `${base}.${field}` });
259
+ for (const definition of definitions) validateConfiguredArray(contract, definition, add, root, config, { required: false, displayField: `${base}.${definition.key}` });
251
260
  }
252
261
  }
253
262
 
254
263
  export function validateTraceabilityBlock(data, root, file, line = null, config = defaultMemoryConfig()) {
255
264
  const errors = [];
256
- const add = (code, field, message) => errors.push(traceabilityFieldError(rel(root, file), code, field, message, line));
265
+ const add = (code, field, message) => errors.push(traceabilityFieldError(rel(root, file), code, field, message, line, config));
257
266
  if (!data || typeof data !== 'object' || Array.isArray(data)) {
258
267
  add('TRACEABILITY_INVALID_JSON', null, 'Traceability block must be a JSON object.');
259
268
  return errors;
260
269
  }
261
270
  if (data.kind !== 'spec') add('TRACEABILITY_INVALID_KIND', 'kind', 'must be "spec" for behavior specs.');
262
- for (const field of TRACEABILITY_PATH_FIELDS) validateTraceabilityPathArray(data, field, add, root, config);
263
- for (const field of TRACEABILITY_COMMAND_FIELDS) validateTraceabilityCommandArray(data, field, add);
271
+ const definitions = traceabilityKeys(config);
272
+ const allowedFields = new Set(['kind', 'contracts', ...definitions.map((definition) => definition.key)]);
273
+ for (const field of Object.keys(data)) if (!allowedFields.has(field)) add('TRACEABILITY_INVALID_FIELD', field, 'is not configured as a traceability field.');
274
+ for (const definition of definitions) validateConfiguredArray(data, definition, add, root, config, { required: true, displayField: definition.key });
264
275
  validateTraceabilityContracts(data, root, add, config);
265
276
  return errors;
266
277
  }
@@ -43,8 +43,7 @@ function addBounded(list, value, limit) {
43
43
  export function relationWeight(relation) {
44
44
  if (relation === 'implemented_by' || relation === 'verified_by') return 4;
45
45
  if (relation === 'includes_resource' || relation === 'routes_to' || relation === 'related_doc' || relation === 'verification_command') return 3;
46
- if (relation === 'links_to' || relation === 'belongs_to_area' || relation === 'declares_package' || relation === 'declares_bin' || relation === 'semantic_similarity') return 2;
47
- if (relation === 'mentions_package') return 1;
46
+ if (relation === 'links_to' || relation === 'belongs_to_area' || relation === 'declares_package' || relation === 'declares_bin' || relation === 'vector_similarity') return 2;
48
47
  return 1;
49
48
  }
50
49
 
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { existsSync } from 'node:fs';
2
3
  import { basename, extname, resolve } from 'node:path';
3
4
  import { rel } from '../common/paths.mjs';
@@ -72,12 +73,21 @@ function contractNodeId(filePath, contractId) {
72
73
  return `contract:${filePath}#${contractId}`;
73
74
  }
74
75
 
75
- function addTraceabilityCommands(graph, sourceId, filePath, commands, idPrefix) {
76
- for (const [index, command] of (Array.isArray(commands) ? commands : []).entries()) {
77
- if (typeof command !== 'string' || command.trim().length === 0) continue;
78
- const id = `verification_command:${idPrefix}#${index}`;
79
- addNode(graph, id, 'verification_command', { command, path: filePath });
80
- addEdge(graph, sourceId, id, 'verification_command', { confidence: 'CURATED_TRACEABILITY' });
76
+ function traceabilityCommandId(sourceId, key, command) {
77
+ const digest = createHash('sha256').update(`${sourceId}\0${key}\0${command}`).digest('hex').slice(0, 16);
78
+ return `command:${key}:${digest}`;
79
+ }
80
+
81
+ function addTraceabilityValues(graph, sourceId, root, filePath, definition, values, config) {
82
+ for (const value of Array.isArray(values) ? values : []) {
83
+ if (definition.target === 'path') {
84
+ addTraceabilityTarget(graph, sourceId, root, definition.relation, value, { traceabilityKey: definition.key, relationWeight: definition.weight }, config);
85
+ continue;
86
+ }
87
+ if (typeof value !== 'string' || value.trim().length === 0) continue;
88
+ const id = traceabilityCommandId(sourceId, definition.key, value);
89
+ addNode(graph, id, 'command', { command: value, path: filePath, traceabilityKey: definition.key, relation: definition.relation });
90
+ addEdge(graph, sourceId, id, definition.relation, { confidence: 'CURATED_TRACEABILITY', traceabilityKey: definition.key, relationWeight: definition.weight });
81
91
  }
82
92
  }
83
93
 
@@ -91,10 +101,7 @@ function addTraceabilityContracts(graph, root, fileId, filePath, contracts, conf
91
101
  const sections = Array.isArray(contract.sections) ? contract.sections.filter((section) => typeof section === 'string' && section.trim().length > 0) : undefined;
92
102
  addNode(graph, id, 'contract', { path: filePath, contractId: contract.id, title: contract.title, sections });
93
103
  addEdge(graph, fileId, id, 'defines_contract', { confidence: 'CURATED_TRACEABILITY' });
94
- for (const target of Array.isArray(contract.implementedBy) ? contract.implementedBy : []) addTraceabilityTarget(graph, id, root, 'implemented_by', target, {}, config);
95
- for (const target of Array.isArray(contract.verifiedBy) ? contract.verifiedBy : []) addTraceabilityTarget(graph, id, root, 'verified_by', target, {}, config);
96
- for (const target of Array.isArray(contract.relatedDocs) ? contract.relatedDocs : []) addTraceabilityTarget(graph, id, root, 'related_doc', target, {}, config);
97
- addTraceabilityCommands(graph, id, filePath, Array.isArray(contract.verificationCommands) ? contract.verificationCommands : [], id);
104
+ for (const definition of config.traceability?.keys ?? []) addTraceabilityValues(graph, id, root, filePath, definition, contract[definition.key], config);
98
105
  }
99
106
  }
100
107
 
@@ -102,10 +109,7 @@ export function addTraceabilityGraph(root, fileId, content, graph, config = defa
102
109
  const filePath = fileId.replace(/^file:/, '');
103
110
  for (const block of extractDotdotgodTraceabilityBlocks(content)) {
104
111
  if (block.error || !block.data || block.data.kind !== 'spec') continue;
105
- for (const target of block.data.implementedBy ?? []) addTraceabilityTarget(graph, fileId, root, 'implemented_by', target, {}, config);
106
- for (const target of block.data.verifiedBy ?? []) addTraceabilityTarget(graph, fileId, root, 'verified_by', target, {}, config);
107
- for (const target of block.data.relatedDocs ?? []) addTraceabilityTarget(graph, fileId, root, 'related_doc', target, {}, config);
108
- addTraceabilityCommands(graph, fileId, filePath, block.data.verificationCommands, fileId);
112
+ for (const definition of config.traceability?.keys ?? []) addTraceabilityValues(graph, fileId, root, filePath, definition, block.data[definition.key], config);
109
113
  addTraceabilityContracts(graph, root, fileId, filePath, block.data.contracts, config);
110
114
  }
111
115
  }
@@ -68,7 +68,7 @@ export function addDeterministicSemanticEdges(graph, config = defaultMemoryConfi
68
68
  const threshold = policy.semantic.threshold ?? 0.5;
69
69
  const topK = policy.semantic.topKPerFile ?? 5;
70
70
  const includeArchiveBodies = policy.semantic.includeArchiveBodies === true;
71
- const enabledSignals = new Set(policy.semantic.signals ?? DEFAULT_IMPACT_RANKING_POLICY.semantic.signals);
71
+ const enabledSignals = new Set(policy.semantic.signals ?? ['path', 'filename', 'heading', 'package']);
72
72
  const baseGraph = { nodes: graph.nodes.map((node) => ({ ...node })), edges: graph.edges.filter((edge) => !SEMANTIC_RELATIONS.has(edge.relation)).map((edge) => ({ ...edge })) };
73
73
  const files = baseGraph.nodes.filter((node) => node.type === 'file' && node.path);
74
74
  const profiles = files.map((node) => semanticProfileForFile(node, baseGraph));
@@ -69,8 +69,10 @@ export function formatYmlImpactOutput(payload, impact) {
69
69
  ` omitted_related: ${impact.omittedRelated ?? 0}`,
70
70
  ' ranking:',
71
71
  ` method: ${ymlScalar(impact.ranking?.method)}`,
72
- ` preset: ${ymlScalar(impact.ranking?.preset)}`,
73
72
  ` config_source: ${ymlScalar(impact.ranking?.configSource)}`,
73
+ ` connection_cap: ${impact.ranking?.connectionCap ?? 80}`,
74
+ ` memory_cap: ${impact.ranking?.memoryCap ?? 20}`,
75
+ ` ppr_reference: ${impact.ranking?.pprReference ?? 0.4}`,
74
76
  ' per_seed:',
75
77
  ];
76
78
  for (const entry of impact.perSeed ?? []) {