@dotdotgod/cli 0.2.2 → 0.2.4
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 +4 -4
- package/package.json +1 -1
- package/src/cli/usage.mjs +9 -11
- package/src/commands/config.mjs +2 -0
- package/src/commands/graph.mjs +20 -6
- package/src/core.mjs +7 -7
- package/src/graph/extract.mjs +2 -2
- package/src/graph/store.mjs +1 -1
- package/src/impact/format.mjs +12 -2
- package/src/impact/report.mjs +44 -26
- package/src/impact/scoring.mjs +22 -10
- package/src/index/files.mjs +27 -15
- package/src/init.mjs +2 -10
- package/src/memory/config.mjs +55 -11
- package/src/reference/resolve.mjs +7 -13
- package/src/trello/custom-fields.mjs +4 -8
- package/src/trello/github-url.mjs +1 -1
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ dotdotgod graph impact . --changed <path> --compact
|
|
|
38
38
|
- `index` builds `.dotdotgod/manifest.json` and compact graph shards from maintained project files.
|
|
39
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.
|
|
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
|
|
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.
|
|
43
43
|
- `plan validate` validates active plan artifacts and simplified Plan Generator checkpoints before execution.
|
|
44
44
|
- `plan stage create` creates an internal `.dotdotgod-plan/NN_STAGE_NAME.md` checkpoint for a simplified Plan Generator stage.
|
|
@@ -66,8 +66,8 @@ dotdotgod plan validate docs/plan/<task-slug>/README.md
|
|
|
66
66
|
dotdotgod plan validate docs/plan/<task-slug>/README.md --stage 04
|
|
67
67
|
dotdotgod plan stage create 02 docs/plan/<task-slug>/README.md
|
|
68
68
|
dotdotgod graph impact . --changed <path>
|
|
69
|
-
dotdotgod graph impact . --changed <path> --compact
|
|
70
|
-
dotdotgod graph impact . --changed <path> --yml
|
|
69
|
+
dotdotgod graph impact . --changed <path> --changed <another-path> --compact
|
|
70
|
+
dotdotgod graph impact . --changed <path> --changed <another-path> --yml
|
|
71
71
|
dotdotgod graph communities .
|
|
72
72
|
```
|
|
73
73
|
|
|
@@ -104,7 +104,7 @@ files:
|
|
|
104
104
|
- packages/pi/extensions/plan-mode/index.ts (45; implemented_by, semantic_similarity)
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
`graph impact` needs `--changed <path>`
|
|
107
|
+
`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.
|
|
108
108
|
|
|
109
109
|
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.
|
|
110
110
|
|
package/package.json
CHANGED
package/src/cli/usage.mjs
CHANGED
|
@@ -81,13 +81,13 @@ Write mode:
|
|
|
81
81
|
Checks or writes generated Markdown traceability link sections from canonical fenced json dotdotgod blocks.`;
|
|
82
82
|
case 'graph':
|
|
83
83
|
return `Usage:
|
|
84
|
-
dotdotgod graph impact <root> --changed <path> [--compact|--json|--yml|--yaml]
|
|
84
|
+
dotdotgod graph impact <root> --changed <path> [--changed <path> ...] [--compact|--json|--yml|--yaml]
|
|
85
85
|
dotdotgod graph communities <root> [--json]`;
|
|
86
86
|
case 'graph impact':
|
|
87
87
|
return `Usage:
|
|
88
|
-
dotdotgod graph impact <root> --changed <path> [--compact|--json|--yml|--yaml]
|
|
88
|
+
dotdotgod graph impact <root> --changed <path> [--changed <path> ...] [--compact|--json|--yml|--yaml]
|
|
89
89
|
|
|
90
|
-
Ranks nodes related to
|
|
90
|
+
Ranks nodes related to one or more changed files. <root> is the project root; repeat --changed with project-relative file paths. Results include a combined ranking and the top five related nodes for each changed file. Use --compact for a short text summary or --yml/--yaml for structured agent-facing output.`;
|
|
91
91
|
case 'graph communities':
|
|
92
92
|
return `Usage:
|
|
93
93
|
dotdotgod graph communities <root> [--json]`;
|
|
@@ -109,7 +109,7 @@ Ranks nodes related to a changed file. <root> is the project root; --changed is
|
|
|
109
109
|
dotdotgod plan validate docs/plan/<task-slug>/README.md [--stage stage] [--json]
|
|
110
110
|
dotdotgod plan stage create <stage> [docs/plan/<task-slug>/README.md] [--json]
|
|
111
111
|
dotdotgod trello sync <root> [--dry-run]
|
|
112
|
-
dotdotgod graph impact <root> --changed <path> [--compact|--json|--yml|--yaml]
|
|
112
|
+
dotdotgod graph impact <root> --changed <path> [--changed <path> ...] [--compact|--json|--yml|--yaml]
|
|
113
113
|
dotdotgod graph communities <root> [--json]`;
|
|
114
114
|
}
|
|
115
115
|
}
|
|
@@ -137,17 +137,15 @@ export function isVersionToken(value) {
|
|
|
137
137
|
return VERSION_TOKENS.has(value);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
export function
|
|
140
|
+
export function printVersion() {
|
|
141
|
+
let version = 'unknown';
|
|
141
142
|
try {
|
|
142
143
|
const data = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
143
|
-
|
|
144
|
+
if (typeof data.version === 'string') version = data.version;
|
|
144
145
|
} catch {
|
|
145
|
-
|
|
146
|
+
// keep 'unknown'
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
export function printVersion() {
|
|
150
|
-
console.log(readCliVersion());
|
|
148
|
+
console.log(version);
|
|
151
149
|
process.exit(0);
|
|
152
150
|
}
|
|
153
151
|
|
package/src/commands/config.mjs
CHANGED
|
@@ -46,6 +46,8 @@ function formatConfigOutput(payload) {
|
|
|
46
46
|
lines.push(`- impact ranking preset: ${payload.config.impactRanking.preset}`);
|
|
47
47
|
lines.push(`- load pinned paths: ${(payload.config.load?.pinnedPaths ?? []).join(', ') || 'none'}`);
|
|
48
48
|
lines.push(`- load pinned bodies: ${(payload.config.load?.pinnedBodies ?? []).join(', ') || 'none'}`);
|
|
49
|
+
lines.push(`- load documentation summary exclude: ${(payload.config.load?.documentationSummary?.exclude ?? []).join(', ') || 'none'}`);
|
|
50
|
+
lines.push(`- plan mode writable paths: ${(payload.config.planMode?.writablePaths ?? []).join(', ') || 'none'}`);
|
|
49
51
|
lines.push(`- trello sync paths: ${(payload.config.integrations?.trello?.syncPaths ?? []).join(', ') || 'none'}`);
|
|
50
52
|
const lowSignal = payload.config.referenceExpansion?.fuzzy?.lowSignal ?? { terms: [], add: [], remove: [] };
|
|
51
53
|
lines.push(`- fuzzy low-signal terms: ${(lowSignal.terms ?? []).join(', ') || 'none'} (add: ${(lowSignal.add ?? []).join(', ') || 'none'}; remove: ${(lowSignal.remove ?? []).join(', ') || 'none'})`);
|
package/src/commands/graph.mjs
CHANGED
|
@@ -4,16 +4,18 @@ import { graphSummary, readFreshIndex } from '../index/cache.mjs';
|
|
|
4
4
|
import { buildCompactImpactReport, buildImpactReport } from '../impact/report.mjs';
|
|
5
5
|
import { formatCompactImpactOutput, formatYmlGraphImpactError, formatYmlImpactOutput } from '../impact/format.mjs';
|
|
6
6
|
|
|
7
|
+
const MAX_CHANGED_FILES = 20;
|
|
8
|
+
|
|
7
9
|
export function parseGraphOptions(argv) {
|
|
8
10
|
const filtered = [];
|
|
9
|
-
|
|
11
|
+
const changed = [];
|
|
10
12
|
let compact = false;
|
|
11
13
|
let yml = false;
|
|
12
14
|
for (let i = 0; i < argv.length; i += 1) {
|
|
13
15
|
if (argv[i] === '--changed') {
|
|
14
16
|
const next = argv[i + 1];
|
|
15
17
|
if (next && !next.startsWith('-')) {
|
|
16
|
-
changed
|
|
18
|
+
changed.push(next);
|
|
17
19
|
i += 1;
|
|
18
20
|
}
|
|
19
21
|
} else if (argv[i] === '--compact') compact = true;
|
|
@@ -21,13 +23,13 @@ export function parseGraphOptions(argv) {
|
|
|
21
23
|
else filtered.push(argv[i]);
|
|
22
24
|
}
|
|
23
25
|
const options = parseCommon(filtered);
|
|
24
|
-
options.changed = changed;
|
|
26
|
+
options.changed = [...new Set(changed)];
|
|
25
27
|
options.compact = compact;
|
|
26
28
|
options.yml = yml;
|
|
27
29
|
return options;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
export function runGraph(argv) {
|
|
32
|
+
export async function runGraph(argv) {
|
|
31
33
|
const sub = argv[0];
|
|
32
34
|
const isImpact = sub === 'impact';
|
|
33
35
|
if (!['impact', 'communities'].includes(sub)) usage(sub ? `Unknown graph command: ${sub}` : 'Missing graph command.', 'graph');
|
|
@@ -39,7 +41,7 @@ export function runGraph(argv) {
|
|
|
39
41
|
else usage(message, 'graph impact');
|
|
40
42
|
process.exit(2);
|
|
41
43
|
}
|
|
42
|
-
if (isImpact &&
|
|
44
|
+
if (isImpact && options.changed.length === 0) {
|
|
43
45
|
const message = 'Missing required option: --changed <path>. Run `dotdotgod graph impact <root> --changed <path>`.';
|
|
44
46
|
if (options.json) {
|
|
45
47
|
console.log(JSON.stringify({ ok: false, command: 'graph impact', compact: options.compact || undefined, root: options.root, error: { code: 'MISSING_CHANGED', message }, usage: commandUsage('graph impact') }, null, 2));
|
|
@@ -51,11 +53,23 @@ export function runGraph(argv) {
|
|
|
51
53
|
}
|
|
52
54
|
usage(message, 'graph impact');
|
|
53
55
|
}
|
|
56
|
+
if (isImpact && options.changed.length > MAX_CHANGED_FILES) {
|
|
57
|
+
const message = `Too many changed files: received ${options.changed.length}; maximum is ${MAX_CHANGED_FILES}.`;
|
|
58
|
+
if (options.json) {
|
|
59
|
+
console.log(JSON.stringify({ ok: false, command: 'graph impact', root: options.root, error: { code: 'TOO_MANY_CHANGED', message }, usage: commandUsage('graph impact') }, null, 2));
|
|
60
|
+
process.exit(2);
|
|
61
|
+
}
|
|
62
|
+
if (options.yml) {
|
|
63
|
+
console.log(formatYmlGraphImpactError(options, message, 'TOO_MANY_CHANGED'));
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
66
|
+
usage(message, 'graph impact');
|
|
67
|
+
}
|
|
54
68
|
const { status, index, metadata } = readFreshIndex(options.root);
|
|
55
69
|
const rawImpact = isImpact ? buildImpactReport(index, options.changed) : undefined;
|
|
56
70
|
const impact = isImpact && (options.compact || options.yml) ? buildCompactImpactReport(rawImpact) : rawImpact;
|
|
57
71
|
const payload = isImpact
|
|
58
|
-
? { ok: status.ok, command: 'graph impact', compact: options.compact || undefined, root: options.root, status, metadata, changed: options.changed, related: impact.related, impact }
|
|
72
|
+
? { 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 }
|
|
59
73
|
: { ok: status.ok, command: 'graph communities', root: options.root, status, metadata, graph: graphSummary(index), communities: buildCommunities(index) };
|
|
60
74
|
const refreshNote = metadata.cacheRefreshed ? ', refreshed' : '';
|
|
61
75
|
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
package/src/core.mjs
CHANGED
|
@@ -15,17 +15,17 @@ export { commandUsage, hasHelpToken, helpCommandFromArgs, isHelpToken, isVersion
|
|
|
15
15
|
export { rel } from './common/paths.mjs';
|
|
16
16
|
export { extractAnchors, extractFirstHeading, extractLinks, headingToAnchor, isKebabCase, isNumberedSeriesFilename, isUpperSnakeMarkdown, removeCodeBlocks, suggestFilenameFromHeading } from './docs/markdown.mjs';
|
|
17
17
|
export { extractDotdotgodTraceabilityBlocks, findTraceabilityLinksRegion, isLocalRelativeTraceabilityPath, renderCompactTraceabilityBlock, stripTraceabilityLinksRegion, syncTraceabilityLinksInContent, traceabilityExample, validateTraceabilityBlock, validateTraceabilityLinksRegion, validateTraceabilityPlacement } from './docs/traceability.mjs';
|
|
18
|
-
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,
|
|
18
|
+
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';
|
|
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
22
|
export { buildMemoryAreas, buildPinnedFiles, detectCommandGuidance, detectPackageManager } from './load-snapshot/summary.mjs';
|
|
23
|
-
export { isReadmeIndexPath, retrievalMetadataForPath
|
|
24
|
-
export {
|
|
23
|
+
export { isReadmeIndexPath, retrievalMetadataForPath } from './graph/metadata.mjs';
|
|
24
|
+
export { buildGraph } from './graph/extract.mjs';
|
|
25
25
|
export { addDeterministicSemanticEdges } from './graph/semantic.mjs';
|
|
26
26
|
export { buildIndex, writeIndex, readIndex, getStatus, runIndex, runStatus, readFreshIndex, graphSummary } from './index/cache.mjs';
|
|
27
27
|
export { runValidate } from './validate/run.mjs';
|
|
28
|
-
export { buildImpactReport, buildCompactImpactReport
|
|
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
31
|
export { runLoadSnapshot } from './commands/load-snapshot.mjs';
|
|
@@ -36,11 +36,11 @@ export { parseGraphOptions, runGraph } from './commands/graph.mjs';
|
|
|
36
36
|
export { buildPlanValidationRepairPrompt, createPlanStageCheckpoint, formatPlanValidationText, PLAN_REQUIRED_HEADERS, PLAN_STAGE_DIRECTORIES, resolvePlanValidationStage, runPlan, validatePlanArtifact } from './commands/plan.mjs';
|
|
37
37
|
export { planTrelloDryRun, runTrelloSync } from './trello/sync.mjs';
|
|
38
38
|
export { parseTrelloMetadata, extractTrelloShortLink } from './trello/metadata.mjs';
|
|
39
|
-
export { normalizeGitHubRemote,
|
|
39
|
+
export { normalizeGitHubRemote, resolveGitHubFileUrl, resolveGitHubRepositoryIdentity } from './trello/github-url.mjs';
|
|
40
40
|
export { buildLinkedDocsData, summarizeTraceability } from './trello/summary.mjs';
|
|
41
41
|
export { formatTrelloDryRunReport, formatTrelloWriteReport } from './trello/report.mjs';
|
|
42
42
|
export { createTrelloClient, resolveTrelloCredentials } from './trello/client.mjs';
|
|
43
|
-
export {
|
|
43
|
+
export { customFieldTextValue, DEFAULT_DOTDOTGOD_CUSTOM_FIELD_NAME, dotdotgodCustomFieldName, mergeLinkedDocsCustomFieldValue, parseLinkedDocsCustomFieldPayload, serializeLinkedDocsCustomFieldPayload } from './trello/custom-fields.mjs';
|
|
44
44
|
|
|
45
45
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
46
46
|
const [command = 'help', ...args] = argv;
|
|
@@ -57,7 +57,7 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
57
57
|
else if (command === 'resolve') runResolve(args);
|
|
58
58
|
else if (command === 'expand') runExpand(args);
|
|
59
59
|
else if (command === 'traceability') runTraceability(args);
|
|
60
|
-
else if (command === 'graph') runGraph(args);
|
|
60
|
+
else if (command === 'graph') await runGraph(args);
|
|
61
61
|
else if (command === 'plan') runPlan(args);
|
|
62
62
|
else if (command === 'trello') {
|
|
63
63
|
const result = await Promise.resolve(runTrelloSync(args));
|
package/src/graph/extract.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { defaultMemoryConfig } from '../memory/config.mjs';
|
|
|
6
6
|
import { addEdge, addNode } from './store.mjs';
|
|
7
7
|
import { addMemoryAreaMembership, addPackageResource, addTraceabilityGraph, fileNodeMetadata, headingId, isReadmeIndexPath } from './metadata.mjs';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
function extractMarkdownGraph(root, file, graph, config = defaultMemoryConfig()) {
|
|
10
10
|
const path = rel(root, file);
|
|
11
11
|
const content = readFileSync(file, 'utf8');
|
|
12
12
|
const fileId = `file:${path}`;
|
|
@@ -30,7 +30,7 @@ export function extractMarkdownGraph(root, file, graph, config = defaultMemoryCo
|
|
|
30
30
|
addTraceabilityGraph(root, fileId, content, graph, config);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
function extractPackageGraph(root, file, graph) {
|
|
34
34
|
const path = rel(root, file);
|
|
35
35
|
const fileId = `file:${path}`;
|
|
36
36
|
let pkg;
|
package/src/graph/store.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { SEMANTIC_RELATIONS } from '../memory/config.mjs';
|
|
4
|
+
import { CACHE_DIR } from '../index/constants.mjs';
|
|
4
5
|
|
|
5
|
-
const CACHE_DIR = '.dotdotgod';
|
|
6
6
|
const GRAPH_NODE_SHARDS = ['docs', 'packages', 'source'];
|
|
7
7
|
const GRAPH_EDGE_SHARDS = ['docs-links', 'packages', 'other'];
|
|
8
8
|
|
package/src/impact/format.mjs
CHANGED
|
@@ -15,7 +15,8 @@ function formatCompactImpactGroup(name, group) {
|
|
|
15
15
|
|
|
16
16
|
export function formatCompactImpactOutput(payload, impact) {
|
|
17
17
|
const refreshNote = payload.metadata.cacheRefreshed ? ', refreshed' : '';
|
|
18
|
-
const lines = [`graph impact compact: ${impact.related.length} related node(s), ${impact.omittedRelated ?? 0} omitted (${payload.status.status}${refreshNote} index)`];
|
|
18
|
+
const lines = [`graph impact compact: ${impact.related.length} related node(s), ${impact.omittedRelated ?? 0} omitted (${payload.status.status}${refreshNote} index)`, `changed files: ${(impact.changedFiles ?? [impact.changed]).join(', ')}`];
|
|
19
|
+
for (const entry of impact.perSeed ?? []) lines.push(...formatCompactImpactGroup(`top for ${entry.changed}`, { items: entry.related }));
|
|
19
20
|
for (const name of ['docs', 'contracts', 'tests', 'files', 'commands', 'events', 'packageResources', 'symbols']) lines.push(...formatCompactImpactGroup(name, impact.groups[name]));
|
|
20
21
|
return lines.join('\n');
|
|
21
22
|
}
|
|
@@ -58,6 +59,7 @@ export function formatYmlImpactOutput(payload, impact) {
|
|
|
58
59
|
'impact:',
|
|
59
60
|
` ok: ${payload.ok ? 'true' : 'false'}`,
|
|
60
61
|
` changed: ${ymlScalar(payload.changed)}`,
|
|
62
|
+
` changed_files: ${ymlList(payload.changedFiles ?? [payload.changed])}`,
|
|
61
63
|
` root: ${ymlScalar(payload.root)}`,
|
|
62
64
|
' output: "yml"',
|
|
63
65
|
' status:',
|
|
@@ -69,8 +71,16 @@ export function formatYmlImpactOutput(payload, impact) {
|
|
|
69
71
|
` method: ${ymlScalar(impact.ranking?.method)}`,
|
|
70
72
|
` preset: ${ymlScalar(impact.ranking?.preset)}`,
|
|
71
73
|
` config_source: ${ymlScalar(impact.ranking?.configSource)}`,
|
|
72
|
-
'
|
|
74
|
+
' per_seed:',
|
|
73
75
|
];
|
|
76
|
+
for (const entry of impact.perSeed ?? []) {
|
|
77
|
+
lines.push(` - changed: ${ymlScalar(entry.changed)}`);
|
|
78
|
+
lines.push(` omitted_related: ${entry.omittedRelated ?? 0}`);
|
|
79
|
+
lines.push(' related:');
|
|
80
|
+
if ((entry.related ?? []).length === 0) lines.push(' []');
|
|
81
|
+
else for (const item of entry.related) lines.push(...formatYmlImpactItem(item));
|
|
82
|
+
}
|
|
83
|
+
lines.push(' groups:');
|
|
74
84
|
for (const name of ['docs', 'contracts', 'tests', 'files', 'commands', 'events', 'packageResources', 'symbols']) lines.push(...formatYmlImpactGroup(name, impact.groups[name]));
|
|
75
85
|
lines.push(' recommended_actions:');
|
|
76
86
|
lines.push(' - "review_related_docs"');
|
package/src/impact/report.mjs
CHANGED
|
@@ -11,7 +11,13 @@ function addImpactItem(group, item, limit = 10) {
|
|
|
11
11
|
group.items.push(item);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function
|
|
14
|
+
function normalizeChangedPaths(changedPaths) {
|
|
15
|
+
const values = Array.isArray(changedPaths) ? changedPaths : [changedPaths];
|
|
16
|
+
return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function selectImpactItems(sortedItems, maxRelated, seeds) {
|
|
20
|
+
const seedSet = new Set(seeds);
|
|
15
21
|
const selected = [];
|
|
16
22
|
const selectedIds = new Set();
|
|
17
23
|
const deferred = [];
|
|
@@ -22,35 +28,36 @@ function selectImpactItems(sortedItems, maxRelated, seed) {
|
|
|
22
28
|
const add = (item, force = false) => {
|
|
23
29
|
if (selected.length >= maxRelated || selectedIds.has(item.id)) return;
|
|
24
30
|
const pathKey = item.path ?? item.id;
|
|
25
|
-
if (!force && item.id
|
|
31
|
+
if (!force && !seedSet.has(item.id) && selected.length < firstPageLimit) {
|
|
26
32
|
if (pathKey && (firstPagePathCounts.get(pathKey) ?? 0) >= 2) return deferred.push(item);
|
|
27
33
|
if (isLowActionabilityImpactItem(item) && lowActionabilityInFirstPage >= 2) return deferred.push(item);
|
|
28
34
|
if (isSemanticOnlyImpactItem(item) && semanticOnlyInFirstPage >= 3) return deferred.push(item);
|
|
29
35
|
}
|
|
30
36
|
selected.push(item);
|
|
31
37
|
selectedIds.add(item.id);
|
|
32
|
-
if (selected.length <= firstPageLimit && item.id
|
|
38
|
+
if (selected.length <= firstPageLimit && !seedSet.has(item.id)) {
|
|
33
39
|
if (pathKey) firstPagePathCounts.set(pathKey, (firstPagePathCounts.get(pathKey) ?? 0) + 1);
|
|
34
40
|
if (isLowActionabilityImpactItem(item)) lowActionabilityInFirstPage += 1;
|
|
35
41
|
if (isSemanticOnlyImpactItem(item)) semanticOnlyInFirstPage += 1;
|
|
36
42
|
}
|
|
37
43
|
};
|
|
38
|
-
for (const item of sortedItems) if (item.id
|
|
39
|
-
for (const item of sortedItems) if (item.id
|
|
44
|
+
for (const item of sortedItems) if (seedSet.has(item.id)) add(item, true);
|
|
45
|
+
for (const item of sortedItems) if (!seedSet.has(item.id)) add(item);
|
|
40
46
|
for (const item of deferred) add(item, true);
|
|
41
47
|
return selected;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
|
|
50
|
+
function buildCombinedImpactReport(index, changedPaths, limits = {}) {
|
|
45
51
|
const graph = index?.graph ?? { nodes: [], edges: [] };
|
|
46
52
|
const config = index?.memoryConfig ? { ...defaultMemoryConfig(), ...index.memoryConfig } : defaultMemoryConfig();
|
|
47
53
|
const policy = cloneImpactRankingPolicy(config.impactRanking ?? DEFAULT_IMPACT_RANKING_POLICY);
|
|
48
54
|
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
49
|
-
const
|
|
50
|
-
const
|
|
55
|
+
const seeds = changedPaths.map((path) => `file:${path}`);
|
|
56
|
+
const seedSet = new Set(seeds);
|
|
57
|
+
const maxRelated = Math.max(limits.related ?? 25, seeds.length);
|
|
51
58
|
const groups = { files: { items: [], omitted: 0 }, docs: { items: [], omitted: 0 }, contracts: { items: [], omitted: 0 }, tests: { items: [], omitted: 0 }, commands: { items: [], omitted: 0 }, events: { items: [], omitted: 0 }, packageResources: { items: [], omitted: 0 }, symbols: { items: [], omitted: 0 } };
|
|
52
|
-
const relatedIds = new Set(
|
|
53
|
-
const reasons = new Map([
|
|
59
|
+
const relatedIds = new Set(seeds);
|
|
60
|
+
const reasons = new Map(seeds.map((seed) => [seed, new Set(['changed-file'])]));
|
|
54
61
|
const addReason = (id, reason) => {
|
|
55
62
|
relatedIds.add(id);
|
|
56
63
|
if (!reasons.has(id)) reasons.set(id, new Set());
|
|
@@ -58,27 +65,27 @@ export function buildImpactReport(index, changedPath, limits = {}) {
|
|
|
58
65
|
};
|
|
59
66
|
|
|
60
67
|
for (const edge of graph.edges) {
|
|
61
|
-
if (edge.source
|
|
62
|
-
if (edge.target
|
|
68
|
+
if (seedSet.has(edge.source)) addReason(edge.target, edge.relation);
|
|
69
|
+
if (seedSet.has(edge.target)) addReason(edge.source, `incoming:${edge.relation}`);
|
|
63
70
|
}
|
|
64
71
|
const expansionRelations = new Set(['implemented_by', 'verified_by', 'related_doc', 'verification_command', ...SEMANTIC_RELATIONS]);
|
|
65
72
|
for (const edge of graph.edges) {
|
|
66
73
|
if (!expansionRelations.has(edge.relation)) continue;
|
|
67
|
-
if (relatedIds.has(edge.source) && edge.target
|
|
74
|
+
if (relatedIds.has(edge.source) && !seedSet.has(edge.target)) addReason(edge.target, edge.relation);
|
|
68
75
|
}
|
|
69
76
|
|
|
70
|
-
const pprScores = buildPersonalizedPageRank(graph,
|
|
71
|
-
const candidatePprMax = Math.max(0, ...[...relatedIds].filter((id) => id
|
|
77
|
+
const pprScores = buildPersonalizedPageRank(graph, seeds, policy);
|
|
78
|
+
const candidatePprMax = Math.max(0, ...[...relatedIds].filter((id) => !seedSet.has(id)).map((id) => pprScores.get(id) ?? 0));
|
|
72
79
|
const relatedAll = [...relatedIds].map((id) => {
|
|
73
80
|
const node = nodeById.get(id) ?? { id };
|
|
74
81
|
const path = node.path ?? id.replace(/^file:/, '').replace(/^test:/, '');
|
|
75
82
|
const reasonList = [...(reasons.get(id) ?? [])];
|
|
76
83
|
const retrieval = node.retrieval ?? retrievalMetadataForPath(path);
|
|
77
84
|
const reasonSignals = reasonList.map((reason) => `reason:${reason}`);
|
|
78
|
-
const scored = scoreImpactItem({ ...node, reasons: reasonList, retrieval },
|
|
85
|
+
const scored = scoreImpactItem({ ...node, reasons: reasonList, retrieval }, seedSet, changedPaths, policy, pprScores, candidatePprMax);
|
|
79
86
|
return { ...node, reasons: reasonList, retrieval: { ...retrieval, signals: [...new Set([...(retrieval.signals ?? []), ...reasonSignals])] }, ...scored };
|
|
80
|
-
}).sort(compareImpactItems(
|
|
81
|
-
const related = selectImpactItems(relatedAll, maxRelated,
|
|
87
|
+
}).sort(compareImpactItems(seeds));
|
|
88
|
+
const related = selectImpactItems(relatedAll, maxRelated, seeds);
|
|
82
89
|
for (const item of related) {
|
|
83
90
|
if (item.type === 'file') {
|
|
84
91
|
const area = docsArea(item.path);
|
|
@@ -91,7 +98,19 @@ export function buildImpactReport(index, changedPath, limits = {}) {
|
|
|
91
98
|
else if (item.type === 'event') addImpactItem(groups.events, item, limits.events ?? 10);
|
|
92
99
|
else if (item.type === 'package_resource') addImpactItem(groups.packageResources, item, limits.packageResources ?? 10);
|
|
93
100
|
}
|
|
94
|
-
return { changed:
|
|
101
|
+
return { changed: changedPaths[0], changedFiles: changedPaths, ranking: { method: policy.ppr.enabled === false ? 'policy-score' : 'personalized-pagerank+policy', preset: policy.preset, configSource: index?.memoryConfig?.source ?? 'default', weights: policy.weights, ppr: policy.ppr }, related, groups, omittedRelated: Math.max(0, relatedAll.length - related.length) };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function buildImpactReport(index, changedPaths, limits = {}) {
|
|
105
|
+
const normalized = normalizeChangedPaths(changedPaths);
|
|
106
|
+
const aggregate = buildCombinedImpactReport(index, normalized, limits);
|
|
107
|
+
const perSeedLimit = limits.perSeed ?? 5;
|
|
108
|
+
aggregate.perSeed = normalized.map((changed) => {
|
|
109
|
+
const report = buildCombinedImpactReport(index, [changed], { ...limits, related: Math.max(limits.related ?? 25, perSeedLimit + 1) });
|
|
110
|
+
const related = report.related.filter((item) => item.id !== `file:${changed}`).slice(0, perSeedLimit);
|
|
111
|
+
return { changed, related, omittedRelated: Math.max(0, report.related.length - 1 - related.length) + report.omittedRelated };
|
|
112
|
+
});
|
|
113
|
+
return aggregate;
|
|
95
114
|
}
|
|
96
115
|
|
|
97
116
|
function compactScoreBreakdown(scoreBreakdown = {}) {
|
|
@@ -113,15 +132,14 @@ function compactImpactGroup(group = { items: [], omitted: 0 }, limit = 5) {
|
|
|
113
132
|
}
|
|
114
133
|
|
|
115
134
|
export function buildCompactImpactReport(impact, limits = {}) {
|
|
116
|
-
const relatedLimit = limits.related ?? 10;
|
|
135
|
+
const relatedLimit = Math.max(limits.related ?? 10, impact.changedFiles?.length ?? 1);
|
|
117
136
|
const groupLimit = limits.groupItems ?? 5;
|
|
137
|
+
const changedFiles = impact.changedFiles ?? [impact.changed];
|
|
138
|
+
const seedIds = new Set(changedFiles.map((path) => `file:${path}`));
|
|
118
139
|
const related = (impact.related ?? []).slice(0, relatedLimit).map(compactImpactItem);
|
|
119
140
|
const groupNames = ['files', 'docs', 'contracts', 'tests', 'commands', 'events', 'packageResources', 'symbols'];
|
|
120
141
|
const groups = Object.fromEntries(groupNames.map((name) => [name, compactImpactGroup(impact.groups?.[name], groupLimit)]));
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function neighborhood(index, changedPath) {
|
|
126
|
-
return buildImpactReport(index, changedPath).related;
|
|
142
|
+
const perSeed = (impact.perSeed ?? []).map((entry) => ({ changed: entry.changed, related: (entry.related ?? []).slice(0, limits.perSeed ?? 5).map(compactImpactItem), omittedRelated: entry.omittedRelated ?? 0 }));
|
|
143
|
+
const top10 = (impact.related ?? []).filter((item) => !seedIds.has(item.id)).slice(0, 10);
|
|
144
|
+
return { changed: impact.changed, changedFiles, perSeed, compact: true, ranking: { method: impact.ranking?.method, preset: impact.ranking?.preset, configSource: impact.ranking?.configSource }, related, groups, omittedRelated: (impact.omittedRelated ?? 0) + Math.max(0, (impact.related?.length ?? 0) - related.length), quality: { rawRelated: impact.related?.length ?? 0, compactRelated: related.length, semanticOnlyTop10: top10.filter((item) => isSemanticOnlyImpactItem(item)).length, curatedTop10: top10.filter((item) => hasCuratedImpactReason(item)).length, lowActionabilityTop10: top10.filter((item) => isLowActionabilityImpactItem(item)).length } };
|
|
127
145
|
}
|
package/src/impact/scoring.mjs
CHANGED
|
@@ -33,13 +33,16 @@ function cappedTraceabilityScore(reasons, boosts, cap) {
|
|
|
33
33
|
return clamp(Math.max(...values) + Math.min(5, Math.max(0, values.length - 1) * 2), 0, cap);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function buildPersonalizedPageRank(graph,
|
|
36
|
+
export function buildPersonalizedPageRank(graph, seeds, policy) {
|
|
37
37
|
if (policy.ppr.enabled === false) return new Map();
|
|
38
|
+
const seedList = Array.isArray(seeds) ? [...new Set(seeds)] : [seeds];
|
|
39
|
+
const seedSet = new Set(seedList);
|
|
40
|
+
const restart = seedList.length > 0 ? 1 / seedList.length : 0;
|
|
38
41
|
const damping = policy.ppr.damping ?? 0.85;
|
|
39
42
|
const iterations = policy.ppr.iterations ?? 20;
|
|
40
43
|
const tolerance = policy.ppr.tolerance ?? 0.000001;
|
|
41
44
|
const ids = new Set(graph.nodes.map((node) => node.id));
|
|
42
|
-
ids.add(seed);
|
|
45
|
+
for (const seed of seedList) ids.add(seed);
|
|
43
46
|
const adjacency = new Map([...ids].map((id) => [id, []]));
|
|
44
47
|
for (const edge of graph.edges) {
|
|
45
48
|
const weight = policy.relationWeights[edge.relation] ?? relationWeight(edge.relation);
|
|
@@ -49,9 +52,9 @@ export function buildPersonalizedPageRank(graph, seed, policy) {
|
|
|
49
52
|
adjacency.get(edge.source).push([edge.target, weight]);
|
|
50
53
|
adjacency.get(edge.target).push([edge.source, weight]);
|
|
51
54
|
}
|
|
52
|
-
let ranks = new Map([...adjacency.keys()].map((id) => [id, id
|
|
55
|
+
let ranks = new Map([...adjacency.keys()].map((id) => [id, seedSet.has(id) ? restart : 0]));
|
|
53
56
|
for (let i = 0; i < iterations; i += 1) {
|
|
54
|
-
const next = new Map([...adjacency.keys()].map((id) => [id, id
|
|
57
|
+
const next = new Map([...adjacency.keys()].map((id) => [id, seedSet.has(id) ? (1 - damping) * restart : 0]));
|
|
55
58
|
for (const [id, edges] of adjacency.entries()) {
|
|
56
59
|
const rank = ranks.get(id) ?? 0;
|
|
57
60
|
const total = edges.reduce((sum, [, weight]) => sum + weight, 0);
|
|
@@ -65,8 +68,10 @@ export function buildPersonalizedPageRank(graph, seed, policy) {
|
|
|
65
68
|
return ranks;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
export function scoreImpactItem(item,
|
|
69
|
-
|
|
71
|
+
export function scoreImpactItem(item, seeds, changedPaths, policy, pprScores, maxPpr) {
|
|
72
|
+
const seedSet = seeds instanceof Set ? seeds : new Set(Array.isArray(seeds) ? seeds : [seeds]);
|
|
73
|
+
const paths = Array.isArray(changedPaths) ? changedPaths : [changedPaths];
|
|
74
|
+
if (seedSet.has(item.id)) return { impactScore: 100, scoreBreakdown: { seed: 100, ppr: 40, traceability: 0, memoryPolicy: 0, verification: 0, proximity: 0, semantic: 0, freshness: 0, archivePenalty: 0 } };
|
|
70
75
|
const reasons = item.reasons ?? [];
|
|
71
76
|
const retrieval = item.retrieval ?? {};
|
|
72
77
|
const pprNormalized = maxPpr > 0 ? (pprScores.get(item.id) ?? 0) / maxPpr : 0;
|
|
@@ -78,7 +83,7 @@ export function scoreImpactItem(item, seed, changedPath, policy, pprScores, maxP
|
|
|
78
83
|
const semantic = clamp(sumReasonBoosts(reasons, policy.semanticBoosts), 0, Math.abs(policy.weights.semantic ?? 10));
|
|
79
84
|
const freshness = retrieval.freshness === 'fresh' ? Math.abs(policy.weights.freshness ?? 5) : retrieval.freshness === 'stale' ? -Math.abs(policy.weights.freshness ?? 5) : 0;
|
|
80
85
|
let archivePenalty = 0;
|
|
81
|
-
if (!
|
|
86
|
+
if (!paths.some((path) => path.startsWith('docs/archive/'))) {
|
|
82
87
|
if (retrieval.area === 'archive-body') archivePenalty -= Math.abs(policy.weights.archivePenalty ?? -25);
|
|
83
88
|
if (retrieval.includeBodiesByDefault === false) archivePenalty -= 10;
|
|
84
89
|
if (retrieval.freshness === 'stale' && retrieval.area !== 'archive-map') archivePenalty -= 5;
|
|
@@ -125,10 +130,17 @@ function impactSelectionScore(item) {
|
|
|
125
130
|
return score;
|
|
126
131
|
}
|
|
127
132
|
|
|
128
|
-
export function compareImpactItems(
|
|
133
|
+
export function compareImpactItems(seeds) {
|
|
134
|
+
const seedList = Array.isArray(seeds) ? seeds : [seeds];
|
|
135
|
+
const seedOrder = new Map(seedList.map((seed, index) => [seed, index]));
|
|
129
136
|
return (a, b) => {
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
const aSeed = seedOrder.get(a.id);
|
|
138
|
+
const bSeed = seedOrder.get(b.id);
|
|
139
|
+
if (aSeed !== undefined || bSeed !== undefined) {
|
|
140
|
+
if (aSeed === undefined) return 1;
|
|
141
|
+
if (bSeed === undefined) return -1;
|
|
142
|
+
return aSeed - bSeed;
|
|
143
|
+
}
|
|
132
144
|
return impactSelectionScore(b) - impactSelectionScore(a) || (b.impactScore ?? 0) - (a.impactScore ?? 0) || a.id.localeCompare(b.id);
|
|
133
145
|
};
|
|
134
146
|
}
|
package/src/index/files.mjs
CHANGED
|
@@ -4,9 +4,8 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { basename, extname, join } from 'node:path';
|
|
5
5
|
import { rel } from '../common/paths.mjs';
|
|
6
6
|
import { defaultMemoryConfig, readMemoryConfig, resolveMemoryArea } from '../memory/config.mjs';
|
|
7
|
+
import { CACHE_DIR, MANIFEST_FILE } from './constants.mjs';
|
|
7
8
|
|
|
8
|
-
const CACHE_DIR = '.dotdotgod';
|
|
9
|
-
const MANIFEST_FILE = 'manifest.json';
|
|
10
9
|
const INDEX_TEXT_EXTENSIONS = new Set([
|
|
11
10
|
'.md', '.mdx', '.markdown', '.txt', '.rst', '.adoc', '.org',
|
|
12
11
|
'.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.py', '.pyw', '.go', '.rs', '.java', '.kt', '.kts', '.swift', '.rb', '.php', '.cs', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.m', '.mm', '.scala', '.clj', '.cljs', '.ex', '.exs', '.erl', '.hrl', '.lua', '.pl', '.pm', '.r', '.R', '.sql',
|
|
@@ -71,25 +70,38 @@ function walkIndexCandidates(root, config = readMemoryConfig(root)) {
|
|
|
71
70
|
return files;
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
73
|
+
function addConfiguredLocalMemoryCandidates(root, candidates, config, limit = 5000) {
|
|
74
|
+
let added = 0;
|
|
75
|
+
const addPath = (path) => {
|
|
76
|
+
if (added >= limit || !existsSync(path)) return;
|
|
77
|
+
let stat;
|
|
78
|
+
try { stat = statSync(path); } catch { return; }
|
|
79
|
+
const pathRel = rel(root, path);
|
|
80
|
+
if (stat.isFile()) {
|
|
81
|
+
candidates.add(pathRel);
|
|
82
|
+
added += 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!stat.isDirectory() || isExcludedIndexDir(pathRel)) return;
|
|
86
|
+
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
|
87
|
+
if (added >= limit) return;
|
|
88
|
+
addPath(join(path, entry.name));
|
|
85
89
|
}
|
|
86
90
|
};
|
|
87
|
-
|
|
91
|
+
|
|
92
|
+
for (const area of config.areas ?? []) {
|
|
93
|
+
if (area.scope !== 'local' || area.includeBodiesByDefault === false) continue;
|
|
94
|
+
for (const pattern of area.paths ?? []) {
|
|
95
|
+
if (pattern.startsWith('**/')) continue;
|
|
96
|
+
const base = pattern.endsWith('/**') ? pattern.slice(0, -3) : pattern;
|
|
97
|
+
addPath(join(root, base));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
88
100
|
}
|
|
89
101
|
|
|
90
102
|
export function collectIndexFiles(root, config = readMemoryConfig(root)) {
|
|
91
103
|
const candidates = new Set(gitIndexCandidates(root) ?? walkIndexCandidates(root, config));
|
|
92
|
-
|
|
104
|
+
addConfiguredLocalMemoryCandidates(root, candidates, config);
|
|
93
105
|
return [...candidates].filter((path) => shouldIndexPath(path, config)).map((path) => join(root, path)).filter(existsSync).sort();
|
|
94
106
|
}
|
|
95
107
|
|
package/src/init.mjs
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
3
|
|
|
4
|
-
function utcTimestamp() {
|
|
5
|
-
return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
|
|
6
|
-
}
|
|
7
|
-
|
|
8
4
|
function action(status, path, extra = {}) {
|
|
9
5
|
return { status, path, ...extra };
|
|
10
6
|
}
|
|
@@ -16,10 +12,6 @@ function formatAction(item) {
|
|
|
16
12
|
return `${item.status.padEnd(13)} ${item.path}${extras.length > 0 ? ` ${extras.join(' ')}` : ''}`;
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
function normalizeRoot(root) {
|
|
20
|
-
return resolve(root);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
15
|
function parseInitOptions(argv) {
|
|
24
16
|
const options = { root: null, projectName: '', dotdotSetting: false, force: false, dryRun: false, json: false };
|
|
25
17
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -42,7 +34,7 @@ function parseInitOptions(argv) {
|
|
|
42
34
|
}
|
|
43
35
|
}
|
|
44
36
|
if (!options.root) throw new Error('Missing required argument: <root>');
|
|
45
|
-
options.root =
|
|
37
|
+
options.root = resolve(options.root);
|
|
46
38
|
if (!options.projectName) options.projectName = basename(options.root);
|
|
47
39
|
return options;
|
|
48
40
|
}
|
|
@@ -65,7 +57,7 @@ function writeInitFile(options, relativePath, content, actions) {
|
|
|
65
57
|
|
|
66
58
|
let backup = '';
|
|
67
59
|
if (existsSync(target) && options.force) {
|
|
68
|
-
backup = `${target}.bak.${
|
|
60
|
+
backup = `${target}.bak.${new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z')}`;
|
|
69
61
|
if (!options.dryRun) renameSync(target, backup);
|
|
70
62
|
}
|
|
71
63
|
|
package/src/memory/config.mjs
CHANGED
|
@@ -27,6 +27,12 @@ export const DEFAULT_INTEGRATIONS_POLICY = {
|
|
|
27
27
|
export const DEFAULT_LOAD_POLICY = {
|
|
28
28
|
pinnedPaths: [],
|
|
29
29
|
pinnedBodies: [],
|
|
30
|
+
documentationSummary: {
|
|
31
|
+
exclude: ['docs/plan', 'docs/archive'],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
export const DEFAULT_PLAN_MODE_POLICY = {
|
|
35
|
+
writablePaths: ['docs/plan/**', 'docs/archive/**'],
|
|
30
36
|
};
|
|
31
37
|
export const DEFAULT_IMPACT_RANKING_POLICY = {
|
|
32
38
|
preset: 'balanced',
|
|
@@ -75,6 +81,7 @@ const DEFAULT_MEMORY_AREAS = [
|
|
|
75
81
|
{ id: 'active-plan', label: 'Active Plans', paths: ['docs/plan/**'], scope: 'local', freshness: 'fresh', role: 'active-task-intent', priority: 95, includeBodiesByDefault: true },
|
|
76
82
|
{ id: 'archive-map', label: 'Archive Map', paths: ['docs/archive/README.md'], scope: 'local', freshness: 'stale', role: 'historical-memory-map', priority: 65, includeBodiesByDefault: true },
|
|
77
83
|
{ id: 'archive-body', label: 'Archive Body', paths: ['docs/archive/**'], excludePaths: ['docs/archive/README.md'], scope: 'local', freshness: 'stale', role: 'historical-memory-body', priority: 20, includeBodiesByDefault: false },
|
|
84
|
+
{ id: 'docs', label: 'Project Documentation', paths: ['docs/**'], scope: 'shared', freshness: 'fresh', role: 'project-documentation', priority: 60, includeBodiesByDefault: true },
|
|
78
85
|
];
|
|
79
86
|
|
|
80
87
|
function cloneClarifyGuidance(clarify) {
|
|
@@ -178,11 +185,18 @@ export function cloneLoadPolicy(policy = DEFAULT_LOAD_POLICY) {
|
|
|
178
185
|
return {
|
|
179
186
|
pinnedPaths: [...(policy.pinnedPaths ?? [])],
|
|
180
187
|
pinnedBodies: [...(policy.pinnedBodies ?? [])],
|
|
188
|
+
documentationSummary: {
|
|
189
|
+
exclude: [...(policy.documentationSummary?.exclude ?? DEFAULT_LOAD_POLICY.documentationSummary.exclude)],
|
|
190
|
+
},
|
|
181
191
|
};
|
|
182
192
|
}
|
|
183
193
|
|
|
194
|
+
export function clonePlanModePolicy(policy = DEFAULT_PLAN_MODE_POLICY) {
|
|
195
|
+
return { writablePaths: [...(policy.writablePaths ?? DEFAULT_PLAN_MODE_POLICY.writablePaths)] };
|
|
196
|
+
}
|
|
197
|
+
|
|
184
198
|
export function defaultMemoryConfig() {
|
|
185
|
-
return { source: 'default', areas: DEFAULT_MEMORY_AREAS.map(cloneArea), traceability: cloneTraceabilityPolicy(), validation: cloneValidationPolicy(), impactRanking: cloneImpactRankingPolicy(), referenceExpansion: cloneReferenceExpansionPolicy(), integrations: cloneIntegrationsPolicy(), load: cloneLoadPolicy() };
|
|
199
|
+
return { source: 'default', areas: DEFAULT_MEMORY_AREAS.map(cloneArea), traceability: cloneTraceabilityPolicy(), validation: cloneValidationPolicy(), impactRanking: cloneImpactRankingPolicy(), referenceExpansion: cloneReferenceExpansionPolicy(), integrations: cloneIntegrationsPolicy(), load: cloneLoadPolicy(), planMode: clonePlanModePolicy() };
|
|
186
200
|
}
|
|
187
201
|
|
|
188
202
|
export function normalizePathPattern(value = '') {
|
|
@@ -225,14 +239,6 @@ export function memoryAreaForPath(path = '', config = defaultMemoryConfig()) {
|
|
|
225
239
|
return resolveMemoryArea(path, config)?.id;
|
|
226
240
|
}
|
|
227
241
|
|
|
228
|
-
export function memoryRoleForPath(path = '', config = defaultMemoryConfig()) {
|
|
229
|
-
return resolveMemoryArea(path, config)?.role;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export function retrievalPriorityForPath(path = '', config = defaultMemoryConfig()) {
|
|
233
|
-
return resolveMemoryArea(path, config)?.priority ?? 30;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
242
|
function normalizeTraceabilityPolicy(raw) {
|
|
237
243
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return cloneTraceabilityPolicy();
|
|
238
244
|
return {
|
|
@@ -269,10 +275,26 @@ export function isSecretLikePathPattern(value = '') {
|
|
|
269
275
|
return /(^|\/)(\.env|\.npmrc|\.pypirc|id_rsa|id_dsa|id_ed25519|credentials?|secrets?)(\.|\/|$)/i.test(normalized);
|
|
270
276
|
}
|
|
271
277
|
|
|
278
|
+
function normalizePlanModePolicy(raw) {
|
|
279
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return clonePlanModePolicy();
|
|
280
|
+
const writablePaths = raw.writablePaths === undefined
|
|
281
|
+
? DEFAULT_PLAN_MODE_POLICY.writablePaths
|
|
282
|
+
: [...new Set((Array.isArray(raw.writablePaths) ? raw.writablePaths : []).map(normalizePathPattern))];
|
|
283
|
+
return clonePlanModePolicy({ writablePaths });
|
|
284
|
+
}
|
|
285
|
+
|
|
272
286
|
function normalizeLoadPolicy(raw) {
|
|
273
287
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return cloneLoadPolicy();
|
|
274
288
|
const normalize = (values) => (Array.isArray(values) ? [...new Set(values.map(normalizePathPattern))] : []);
|
|
275
|
-
return cloneLoadPolicy({
|
|
289
|
+
return cloneLoadPolicy({
|
|
290
|
+
pinnedPaths: normalize(raw.pinnedPaths),
|
|
291
|
+
pinnedBodies: normalize(raw.pinnedBodies),
|
|
292
|
+
documentationSummary: {
|
|
293
|
+
exclude: raw.documentationSummary?.exclude === undefined
|
|
294
|
+
? DEFAULT_LOAD_POLICY.documentationSummary.exclude
|
|
295
|
+
: normalize(raw.documentationSummary.exclude),
|
|
296
|
+
},
|
|
297
|
+
});
|
|
276
298
|
}
|
|
277
299
|
|
|
278
300
|
export function trelloSyncPaths(config = defaultMemoryConfig()) {
|
|
@@ -392,6 +414,25 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
|
|
|
392
414
|
if (values.some((value) => typeof value === 'string' && isSecretLikePathPattern(value))) add('LOAD_CONFIG_SECRET_PINNED_PATH', `load.${key}`, 'Pinned load paths must not target secrets, credentials, environment files, or private keys.');
|
|
393
415
|
}
|
|
394
416
|
}
|
|
417
|
+
if (load.documentationSummary !== undefined) {
|
|
418
|
+
const summary = load.documentationSummary;
|
|
419
|
+
if (!summary || typeof summary !== 'object' || Array.isArray(summary)) {
|
|
420
|
+
add('LOAD_CONFIG_INVALID_DOCUMENTATION_SUMMARY', 'load.documentationSummary', 'Expected an object.');
|
|
421
|
+
} else if (summary.exclude !== undefined) {
|
|
422
|
+
if (!Array.isArray(summary.exclude)) add('LOAD_CONFIG_INVALID_DOCUMENTATION_SUMMARY_EXCLUDE', 'load.documentationSummary.exclude', 'Expected an array of path strings.');
|
|
423
|
+
else if (summary.exclude.some((value) => !isValidPathPattern(value) || (typeof value === 'string' && value.trim().startsWith('/')))) add('LOAD_CONFIG_INVALID_DOCUMENTATION_SUMMARY_EXCLUDE', 'load.documentationSummary.exclude', 'Expected repository-relative path strings using exact paths, /** subtree patterns, or **/suffix patterns.');
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const planMode = data.planMode;
|
|
429
|
+
if (planMode !== undefined) {
|
|
430
|
+
if (!planMode || typeof planMode !== 'object' || Array.isArray(planMode)) {
|
|
431
|
+
add('PLAN_MODE_CONFIG_INVALID', 'planMode', 'Expected an object.');
|
|
432
|
+
} else if (planMode.writablePaths !== undefined) {
|
|
433
|
+
const paths = planMode.writablePaths;
|
|
434
|
+
if (!Array.isArray(paths)) add('PLAN_MODE_CONFIG_INVALID_WRITABLE_PATHS', 'planMode.writablePaths', 'Expected an array of documentation path strings.');
|
|
435
|
+
else if (paths.some((value) => !isValidPathPattern(value) || typeof value !== 'string' || value.trim().startsWith('/') || (!normalizePathPattern(value).startsWith('docs/') && normalizePathPattern(value) !== 'docs') || normalizePathPattern(value).startsWith('docs/.') || isSecretLikePathPattern(value))) add('PLAN_MODE_CONFIG_INVALID_WRITABLE_PATHS', 'planMode.writablePaths', 'Expected safe repository-relative paths under docs/ using exact paths or /** subtree patterns.');
|
|
395
436
|
}
|
|
396
437
|
}
|
|
397
438
|
const referenceExpansion = data.referenceExpansion;
|
|
@@ -506,7 +547,8 @@ export function readMemoryConfig(root = '.') {
|
|
|
506
547
|
const referenceExpansion = normalizeReferenceExpansionPolicy(data.referenceExpansion);
|
|
507
548
|
const integrations = normalizeIntegrationsPolicy(data.integrations);
|
|
508
549
|
const load = normalizeLoadPolicy(data.load);
|
|
509
|
-
|
|
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: [] };
|
|
510
552
|
} catch (error) {
|
|
511
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.` }] };
|
|
512
554
|
}
|
|
@@ -540,6 +582,7 @@ export function defaultDotdotgodConfigData() {
|
|
|
540
582
|
referenceExpansion: { fuzzy: { lowSignal: { add: [], remove: [] } } },
|
|
541
583
|
integrations: cloneIntegrationsPolicy(config.integrations),
|
|
542
584
|
load: cloneLoadPolicy(config.load),
|
|
585
|
+
planMode: clonePlanModePolicy(config.planMode),
|
|
543
586
|
};
|
|
544
587
|
}
|
|
545
588
|
|
|
@@ -567,6 +610,7 @@ export function memoryConfigSummary(config) {
|
|
|
567
610
|
referenceExpansion: cloneReferenceExpansionPolicy(config.referenceExpansion),
|
|
568
611
|
integrations: cloneIntegrationsPolicy(config.integrations),
|
|
569
612
|
load: cloneLoadPolicy(config.load),
|
|
613
|
+
planMode: clonePlanModePolicy(config.planMode),
|
|
570
614
|
};
|
|
571
615
|
}
|
|
572
616
|
|
|
@@ -5,10 +5,6 @@ import { readFreshIndex } from '../index/cache.mjs';
|
|
|
5
5
|
import { buildCompactImpactReport, buildImpactReport } from '../impact/report.mjs';
|
|
6
6
|
import { DEFAULT_REFERENCE_LIMIT, extractBracketReferences, extractFuzzyReferences, isArchiveBodyPath, normalizeReferenceAlias, referenceCandidateAliases, referencePathForNode } from './extract.mjs';
|
|
7
7
|
|
|
8
|
-
function incomingRouteBonus(graph, nodeId) {
|
|
9
|
-
return graph.edges.some((edge) => edge.target === nodeId && edge.relation === 'routes_to') ? 6 : 0;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
8
|
function exactReferenceScore(ref, alias, kind) {
|
|
13
9
|
const rawRef = String(ref).trim().replace(/^\[\[/, '').replace(/\]\]$/, '').split('|')[0].trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
14
10
|
const rawAlias = String(alias).trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
@@ -40,7 +36,7 @@ export function resolveReferenceCandidates(index, ref, options = {}) {
|
|
|
40
36
|
}
|
|
41
37
|
if (!best) continue;
|
|
42
38
|
const retrievalPriority = Number(node.retrievalPriority ?? node.retrieval?.priority ?? 30);
|
|
43
|
-
const routeBonus =
|
|
39
|
+
const routeBonus = graph.edges.some((edge) => edge.target === node.id && edge.relation === 'routes_to') ? 6 : 0;
|
|
44
40
|
const memoryBonus = Math.min(12, Math.max(0, retrievalPriority / 10));
|
|
45
41
|
const headingBonus = node.type === 'heading' ? 2 : 0;
|
|
46
42
|
const score = Number((best.score + routeBonus + memoryBonus + headingBonus).toFixed(1));
|
|
@@ -80,13 +76,6 @@ function parseReferenceOptions(argv, command) {
|
|
|
80
76
|
return { ...options, root: resolve(operands[0] ?? '.'), json: filtered.includes('--json'), rootArgv: operands.slice(1) };
|
|
81
77
|
}
|
|
82
78
|
|
|
83
|
-
function attachImpactToRef(index, refResult) {
|
|
84
|
-
const topPath = refResult.top?.path;
|
|
85
|
-
if (!topPath) return refResult;
|
|
86
|
-
const impact = buildCompactImpactReport(buildImpactReport(index, topPath));
|
|
87
|
-
return { ...refResult, impact: { changed: topPath, related: impact.related, groups: impact.groups, omittedRelated: impact.omittedRelated, quality: impact.quality } };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
79
|
function formatReferenceOutput(payload) {
|
|
91
80
|
const refreshNote = payload.metadata.cacheRefreshed ? ', refreshed' : '';
|
|
92
81
|
const lines = [`${payload.command}: ${payload.refs.length} reference(s) (${payload.status.status}${refreshNote} index)`];
|
|
@@ -124,7 +113,12 @@ export function runExpand(argv) {
|
|
|
124
113
|
...refsInPrompt.map((item) => ({ ...resolveReferenceCandidates(index, item.target, options), source: 'explicit', raw: item.raw, label: item.label })),
|
|
125
114
|
...fuzzyRefs.map((item) => ({ ...resolveReferenceCandidates(index, item.target, options), source: 'fuzzy', raw: item.raw, confidence: item.confidence, reasons: item.reasons })),
|
|
126
115
|
];
|
|
127
|
-
if (options.withImpact) refs = refs.map((item) =>
|
|
116
|
+
if (options.withImpact) refs = refs.map((item) => {
|
|
117
|
+
const topPath = item.top?.path;
|
|
118
|
+
if (!topPath) return item;
|
|
119
|
+
const impact = buildCompactImpactReport(buildImpactReport(index, topPath));
|
|
120
|
+
return { ...item, impact: { changed: topPath, related: impact.related, groups: impact.groups, omittedRelated: impact.omittedRelated, quality: impact.quality } };
|
|
121
|
+
});
|
|
128
122
|
const payload = { ok: status.ok, command: 'expand', root: options.root, prompt, status, metadata, refs, omitted: refs.reduce((sum, item) => sum + item.omitted, 0) };
|
|
129
123
|
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
|
130
124
|
else console.log(formatReferenceOutput(payload));
|
|
@@ -8,7 +8,7 @@ export function dotdotgodCustomFieldName({ env = process.env, name = '' } = {})
|
|
|
8
8
|
return cleanString(name) || cleanString(env.DOTDOTGOD_TRELLO_CUSTOM_FIELD_NAME) || DEFAULT_DOTDOTGOD_CUSTOM_FIELD_NAME;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
function buildLinkedDocsCustomFieldData({ docsPath, githubUrl, trelloUrl, repositoryKey, repositoryLabel, title, summary, traceabilityState, traceabilityDetails = [] } = {}) {
|
|
12
12
|
return {
|
|
13
13
|
repositoryKey: cleanString(repositoryKey),
|
|
14
14
|
repositoryLabel: cleanString(repositoryLabel) || cleanString(repositoryKey).split('/').filter(Boolean).at(-1) || '',
|
|
@@ -22,12 +22,8 @@ export function buildLinkedDocsCustomFieldData({ docsPath, githubUrl, trelloUrl,
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function normalizeEntry(entry = {}) {
|
|
26
|
-
return buildLinkedDocsCustomFieldData(entry);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
25
|
export function serializeLinkedDocsCustomFieldPayload(entries = []) {
|
|
30
|
-
return JSON.stringify({ version: 1, entries: entries.map(
|
|
26
|
+
return JSON.stringify({ version: 1, entries: entries.map((entry) => buildLinkedDocsCustomFieldData(entry)) });
|
|
31
27
|
}
|
|
32
28
|
|
|
33
29
|
export function parseLinkedDocsCustomFieldPayload(text = '') {
|
|
@@ -54,7 +50,7 @@ export function parseLinkedDocsCustomFieldPayload(text = '') {
|
|
|
54
50
|
},
|
|
55
51
|
};
|
|
56
52
|
}
|
|
57
|
-
const entries = parsed.entries.map(
|
|
53
|
+
const entries = parsed.entries.map((entry) => buildLinkedDocsCustomFieldData(entry));
|
|
58
54
|
const keys = entries.map((entry) => entry.repositoryKey).filter(Boolean);
|
|
59
55
|
if (keys.length !== new Set(keys).size) {
|
|
60
56
|
return {
|
|
@@ -69,7 +65,7 @@ export function parseLinkedDocsCustomFieldPayload(text = '') {
|
|
|
69
65
|
}
|
|
70
66
|
|
|
71
67
|
export function mergeLinkedDocsCustomFieldValue(existingText = '', entry = {}) {
|
|
72
|
-
const current =
|
|
68
|
+
const current = buildLinkedDocsCustomFieldData(entry);
|
|
73
69
|
if (!current.repositoryKey) {
|
|
74
70
|
return {
|
|
75
71
|
ok: false,
|
|
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
function normalizeGitHubRepositoryKey(value = '') {
|
|
6
6
|
const text = String(value).trim();
|
|
7
7
|
const match = text.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/u);
|
|
8
8
|
if (!match) return null;
|