@dotdotgod/cli 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/package.json +1 -1
- package/src/cli/usage.mjs +5 -7
- package/src/commands/config.mjs +4 -0
- package/src/commands/load-snapshot.mjs +11 -5
- package/src/core.mjs +7 -7
- package/src/graph/extract.mjs +2 -2
- package/src/graph/store.mjs +1 -1
- package/src/impact/report.mjs +0 -4
- package/src/index/files.mjs +28 -16
- package/src/init.mjs +2 -10
- package/src/load-snapshot/summary.mjs +96 -2
- package/src/memory/config.mjs +91 -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
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# @dotdotgod/cli
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@dotdotgod/cli) [](https://github.com/dotdotgod/dotdotgod-kit/tree/main/packages/cli) [](
|
|
3
|
+
[](https://www.npmjs.com/package/@dotdotgod/cli) [](https://github.com/dotdotgod/dotdotgod-kit/tree/main/packages/cli) [](https://github.com/dotdotgod/dotdotgod-kit/blob/main/LICENSE)
|
|
4
4
|
|
|
5
5
|
Command-line tools for dotdotgod project memory.
|
|
6
6
|
|
|
7
7
|
Use this package when you want to initialize a docs-first project scaffold, validate project-memory docs, build a local graph/cache, load bounded context snapshots, expand project references, or ask what should be checked after a file changes.
|
|
8
8
|
|
|
9
|
+
These commands implement dotdotgod's project-memory loop — init, load, plan, impact — for shells, scripts, CI, and any agent without a dedicated adapter.
|
|
10
|
+
|
|
9
11
|
## Start Here
|
|
10
12
|
|
|
11
13
|
Run one command without installing globally:
|
|
@@ -34,7 +36,7 @@ dotdotgod graph impact . --changed <path> --compact
|
|
|
34
36
|
- `init` creates `AGENTS.md`, thin agent entrypoints, docs indexes, active-plan space, archive map, and local cache ignores.
|
|
35
37
|
- `validate` checks the dotdotgod docs/project-memory structure, local links, traceability blocks, config validity, and optional index freshness.
|
|
36
38
|
- `index` builds `.dotdotgod/manifest.json` and compact graph shards from maintained project files.
|
|
37
|
-
- `load-snapshot` returns a bounded first-pass project map for AI agents.
|
|
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.
|
|
38
40
|
- `resolve` and `expand` map explicit or high-signal prompt references to project files.
|
|
39
41
|
- `graph impact` ranks likely related specs, tests, docs, commands, and source files for a changed path.
|
|
40
42
|
- `traceability links` checks or repairs generated Markdown traceability-link sections.
|
|
@@ -139,4 +141,4 @@ Default exclusions include dependency, generated, cache, and secret-like paths s
|
|
|
139
141
|
|
|
140
142
|
## Learn More
|
|
141
143
|
|
|
142
|
-
See the [root README](
|
|
144
|
+
See the [root README](https://github.com/dotdotgod/dotdotgod-kit#readme), [Context curation](https://github.com/dotdotgod/dotdotgod-kit/blob/main/docs/concept/CONTEXT_CURATION.md), [Context mechanics](https://github.com/dotdotgod/dotdotgod-kit/blob/main/docs/concept/CONTEXT_MECHANICS.md), [Memory area config](https://github.com/dotdotgod/dotdotgod-kit/blob/main/docs/spec/MEMORY_AREA_CONFIG.md), and [Traceability config](https://github.com/dotdotgod/dotdotgod-kit/blob/main/docs/spec/TRACEABILITY_CONFIG.md).
|
package/package.json
CHANGED
package/src/cli/usage.mjs
CHANGED
|
@@ -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
|
@@ -44,6 +44,10 @@ function formatConfigOutput(payload) {
|
|
|
44
44
|
lines.push(`- validation markdown: maxLines=${payload.config.validation.markdown.maxLines}, maxChars=${payload.config.validation.markdown.maxChars}`);
|
|
45
45
|
lines.push(`- validation markdown exclude: ${(payload.config.validation.markdown.exclude ?? []).join(', ') || 'none'}`);
|
|
46
46
|
lines.push(`- impact ranking preset: ${payload.config.impactRanking.preset}`);
|
|
47
|
+
lines.push(`- load pinned paths: ${(payload.config.load?.pinnedPaths ?? []).join(', ') || 'none'}`);
|
|
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'}`);
|
|
47
51
|
lines.push(`- trello sync paths: ${(payload.config.integrations?.trello?.syncPaths ?? []).join(', ') || 'none'}`);
|
|
48
52
|
const lowSignal = payload.config.referenceExpansion?.fuzzy?.lowSignal ?? { terms: [], add: [], remove: [] };
|
|
49
53
|
lines.push(`- fuzzy low-signal terms: ${(lowSignal.terms ?? []).join(', ') || 'none'} (add: ${(lowSignal.add ?? []).join(', ') || 'none'}; remove: ${(lowSignal.remove ?? []).join(', ') || 'none'})`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseCommon } from '../cli/usage.mjs';
|
|
2
2
|
import { buildCommunities } from '../graph/communities.mjs';
|
|
3
3
|
import { memoryConfigSummary, readMemoryConfig } from '../memory/config.mjs';
|
|
4
|
-
import { buildMemoryAreas, detectCommandGuidance } from '../load-snapshot/summary.mjs';
|
|
4
|
+
import { buildMemoryAreas, buildPinnedFiles, detectCommandGuidance } from '../load-snapshot/summary.mjs';
|
|
5
5
|
import { graphSummary, readFreshIndex } from '../index/cache.mjs';
|
|
6
6
|
|
|
7
7
|
export function runLoadSnapshot(argv) {
|
|
@@ -10,7 +10,9 @@ export function runLoadSnapshot(argv) {
|
|
|
10
10
|
const summary = graphSummary(index);
|
|
11
11
|
const communities = buildCommunities(index, { communities: 5, items: 5 });
|
|
12
12
|
const memoryAreas = buildMemoryAreas(index, { items: 4 });
|
|
13
|
-
const
|
|
13
|
+
const resolvedConfig = readMemoryConfig(options.root);
|
|
14
|
+
const memoryConfig = index.memoryConfig ?? memoryConfigSummary(resolvedConfig);
|
|
15
|
+
const pinnedFiles = buildPinnedFiles(options.root, resolvedConfig);
|
|
14
16
|
const memoryPolicy = {
|
|
15
17
|
source: memoryConfig.source ?? 'default',
|
|
16
18
|
sharedAreas: (memoryConfig.areas ?? []).filter((area) => area.scope === 'shared').map((area) => area.id),
|
|
@@ -18,7 +20,7 @@ export function runLoadSnapshot(argv) {
|
|
|
18
20
|
freshAreas: (memoryConfig.areas ?? []).filter((area) => area.freshness === 'fresh').map((area) => area.id),
|
|
19
21
|
staleAreas: (memoryConfig.areas ?? []).filter((area) => area.freshness === 'stale').map((area) => area.id),
|
|
20
22
|
};
|
|
21
|
-
const bounds = { communities: 5, communityItems: 5, memoryAreaItems: 4, fullGraphIncluded: false, archiveBodiesIncluded: status.archiveBodiesIncluded, archiveMapIncluded: true };
|
|
23
|
+
const bounds = { communities: 5, communityItems: 5, memoryAreaItems: 4, pinnedPathItems: pinnedFiles.bounds.maxPaths, pinnedBodyItems: pinnedFiles.bounds.maxBodies, pinnedBodyChars: pinnedFiles.bounds.maxBodyChars, fullGraphIncluded: false, archiveBodiesIncluded: status.archiveBodiesIncluded, archiveMapIncluded: true };
|
|
22
24
|
const quality = {
|
|
23
25
|
indexedFiles: status.indexedFiles,
|
|
24
26
|
currentFiles: status.currentFiles,
|
|
@@ -28,13 +30,17 @@ export function runLoadSnapshot(argv) {
|
|
|
28
30
|
omittedCommunityItems: communities.communities.reduce((sum, community) => sum + (community.omitted ?? 0), 0),
|
|
29
31
|
memoryAreas: memoryAreas.total,
|
|
30
32
|
omittedMemoryAreaItems: memoryAreas.areas.reduce((sum, area) => sum + (area.omitted ?? 0), 0),
|
|
33
|
+
pinnedPaths: pinnedFiles.paths.length,
|
|
34
|
+
omittedPinnedPaths: pinnedFiles.omittedPaths,
|
|
35
|
+
pinnedBodies: pinnedFiles.bodies.length,
|
|
36
|
+
omittedPinnedBodies: pinnedFiles.omittedBodies,
|
|
31
37
|
graphNodes: summary.nodes,
|
|
32
38
|
graphEdges: summary.edges,
|
|
33
39
|
};
|
|
34
40
|
const commandGuidance = detectCommandGuidance(options.root);
|
|
35
|
-
let payload = { root: options.root, cache: status, metadata, graph: summary, memoryConfig, memoryPolicy, memoryAreas, communities, bounds, quality, commandGuidance };
|
|
41
|
+
let payload = { root: options.root, cache: status, metadata, graph: summary, memoryConfig, memoryPolicy, memoryAreas, pinnedFiles, communities, bounds, quality, commandGuidance };
|
|
36
42
|
const serialized = JSON.stringify(payload);
|
|
37
43
|
payload = { ...payload, quality: { ...quality, snapshotBytes: Buffer.byteLength(serialized), approxSnapshotTokens: Math.ceil(serialized.length / 4) } };
|
|
38
44
|
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
|
39
|
-
else console.log(`dotdotgod load snapshot\n- cache: ${status.status}${metadata.cacheRefreshed ? ' (refreshed)' : ''}\n- indexed files: ${status.indexedFiles}\n- current files: ${status.currentFiles}\n- archive bodies included: ${status.archiveBodiesIncluded ? 'yes' : 'no'}\n- graph: ${summary.nodes} nodes, ${summary.edges} edges\n- memory areas: ${memoryAreas.areas.length}/${memoryAreas.total} shown\n- communities: ${communities.communities.length}/${communities.total} shown, ${communities.omitted} omitted`);
|
|
45
|
+
else console.log(`dotdotgod load snapshot\n- cache: ${status.status}${metadata.cacheRefreshed ? ' (refreshed)' : ''}\n- indexed files: ${status.indexedFiles}\n- current files: ${status.currentFiles}\n- archive bodies included: ${status.archiveBodiesIncluded ? 'yes' : 'no'}\n- graph: ${summary.nodes} nodes, ${summary.edges} edges\n- memory areas: ${memoryAreas.areas.length}/${memoryAreas.total} shown\n- pinned files: ${pinnedFiles.paths.length} paths (${pinnedFiles.omittedPaths} omitted), ${pinnedFiles.bodies.length} bodies (${pinnedFiles.omittedBodies} omitted)\n- communities: ${communities.communities.length}/${communities.total} shown, ${communities.omitted} omitted`);
|
|
40
46
|
}
|
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_VALIDATION_POLICY, SEMANTIC_RELATIONS, cloneImpactRankingPolicy, cloneIntegrationsPolicy, cloneReferenceExpansionPolicy, cloneValidationPolicy, defaultDotdotgodConfigData, defaultDotdotgodConfigText, defaultMemoryConfig, isMarkdownSizeExcluded, 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
|
-
export { buildMemoryAreas, detectCommandGuidance, detectPackageManager } from './load-snapshot/summary.mjs';
|
|
23
|
-
export { isReadmeIndexPath, retrievalMetadataForPath
|
|
24
|
-
export {
|
|
22
|
+
export { buildMemoryAreas, buildPinnedFiles, detectCommandGuidance, detectPackageManager } from './load-snapshot/summary.mjs';
|
|
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;
|
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/report.mjs
CHANGED
|
@@ -121,7 +121,3 @@ export function buildCompactImpactReport(impact, limits = {}) {
|
|
|
121
121
|
const top10 = (impact.related ?? []).filter((item) => item.id !== `file:${impact.changed}`).slice(0, 10);
|
|
122
122
|
return { changed: impact.changed, 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 } };
|
|
123
123
|
}
|
|
124
|
-
|
|
125
|
-
export function neighborhood(index, changedPath) {
|
|
126
|
-
return buildImpactReport(index, changedPath).related;
|
|
127
|
-
}
|
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',
|
|
@@ -21,7 +20,7 @@ const INDEX_TEXT_FILENAMES = new Set([
|
|
|
21
20
|
]);
|
|
22
21
|
const INDEX_EXCLUDED_DIRS = new Set(['.git', CACHE_DIR, 'node_modules', 'vendor', '.venv', 'venv', 'target', 'dist', 'build', 'coverage', '.next', '.turbo', '.cache', '.pytest_cache', '__pycache__']);
|
|
23
22
|
|
|
24
|
-
function isExcludedIndexDir(path) {
|
|
23
|
+
export function isExcludedIndexDir(path) {
|
|
25
24
|
return path.split('/').some((part) => INDEX_EXCLUDED_DIRS.has(part));
|
|
26
25
|
}
|
|
27
26
|
|
|
@@ -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
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { rel } from '../common/paths.mjs';
|
|
4
|
+
import { isExcludedIndexDir } from '../index/files.mjs';
|
|
5
|
+
import { defaultMemoryConfig, isSecretLikePathPattern, matchPathPattern, memoryAreaForPath } from '../memory/config.mjs';
|
|
4
6
|
|
|
5
7
|
export function detectPackageManager(root) {
|
|
6
8
|
const packageFile = join(root, 'package.json');
|
|
@@ -48,6 +50,98 @@ export function detectCommandGuidance(root) {
|
|
|
48
50
|
};
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
const PINNED_WALK_CAP = 200;
|
|
54
|
+
|
|
55
|
+
function walkPinnedMatches(root, base, matches, cap) {
|
|
56
|
+
if (!existsSync(base)) return [];
|
|
57
|
+
const results = [];
|
|
58
|
+
const walk = (dir) => {
|
|
59
|
+
if (results.length >= cap) return;
|
|
60
|
+
let entries;
|
|
61
|
+
try {
|
|
62
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
63
|
+
} catch {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (results.length >= cap) return;
|
|
69
|
+
const absolute = join(dir, entry.name);
|
|
70
|
+
const path = rel(root, absolute);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
if (!isExcludedIndexDir(path)) walk(absolute);
|
|
73
|
+
} else if (entry.isFile() && matches(path)) results.push(path);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
walk(base);
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function expandPinnedPattern(root, pattern) {
|
|
81
|
+
if (!pattern.includes('*')) {
|
|
82
|
+
try {
|
|
83
|
+
return statSync(join(root, pattern)).isFile() ? [pattern] : [];
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const base = pattern.endsWith('/**') ? join(root, pattern.slice(0, -3)) : root;
|
|
89
|
+
return walkPinnedMatches(root, base, (path) => !isSecretLikePathPattern(path) && matchPathPattern(path, pattern), PINNED_WALK_CAP);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readPinnedBody(root, path, maxBodyChars) {
|
|
93
|
+
if (isSecretLikePathPattern(path)) return { path, status: 'skipped', reason: 'secret-like-path' };
|
|
94
|
+
let content;
|
|
95
|
+
try {
|
|
96
|
+
content = readFileSync(join(root, path));
|
|
97
|
+
} catch {
|
|
98
|
+
return { path, status: 'missing' };
|
|
99
|
+
}
|
|
100
|
+
if (content.includes(0)) return { path, status: 'skipped', reason: 'binary' };
|
|
101
|
+
const text = content.toString('utf8');
|
|
102
|
+
const truncated = text.length > maxBodyChars;
|
|
103
|
+
return { path, status: truncated ? 'truncated' : 'present', truncated, chars: text.length, content: truncated ? text.slice(0, maxBodyChars) : text };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function buildPinnedFiles(root, config = defaultMemoryConfig(), limits = {}) {
|
|
107
|
+
const policy = config.load ?? { pinnedPaths: [], pinnedBodies: [] };
|
|
108
|
+
const maxPaths = limits.paths ?? 20;
|
|
109
|
+
const maxBodies = limits.bodies ?? 5;
|
|
110
|
+
const maxBodyChars = limits.bodyChars ?? 10000;
|
|
111
|
+
const entries = new Map();
|
|
112
|
+
const addMatches = (pattern, kind) => {
|
|
113
|
+
const matches = expandPinnedPattern(root, pattern);
|
|
114
|
+
for (const path of matches.length > 0 ? matches : [pattern]) {
|
|
115
|
+
const entry = entries.get(path) ?? { path, status: matches.length > 0 ? 'present' : 'missing', pinnedBy: new Set() };
|
|
116
|
+
entry.pinnedBy.add(kind);
|
|
117
|
+
entries.set(path, entry);
|
|
118
|
+
}
|
|
119
|
+
return matches;
|
|
120
|
+
};
|
|
121
|
+
for (const pattern of policy.pinnedPaths ?? []) addMatches(pattern, 'pinnedPaths');
|
|
122
|
+
const bodyCandidates = new Set();
|
|
123
|
+
for (const pattern of policy.pinnedBodies ?? []) {
|
|
124
|
+
for (const path of addMatches(pattern, 'pinnedBodies')) bodyCandidates.add(path);
|
|
125
|
+
}
|
|
126
|
+
const allPaths = [...entries.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
127
|
+
const paths = allPaths.slice(0, maxPaths).map((entry) => ({ path: entry.path, status: entry.status, pinnedBy: [...entry.pinnedBy].sort() }));
|
|
128
|
+
const bodies = [];
|
|
129
|
+
let omittedBodies = 0;
|
|
130
|
+
for (const path of [...bodyCandidates].sort()) {
|
|
131
|
+
if (bodies.length >= maxBodies) omittedBodies += 1;
|
|
132
|
+
else bodies.push(readPinnedBody(root, path, maxBodyChars));
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
source: config.source ?? 'default',
|
|
136
|
+
configured: { pinnedPaths: [...(policy.pinnedPaths ?? [])], pinnedBodies: [...(policy.pinnedBodies ?? [])] },
|
|
137
|
+
paths,
|
|
138
|
+
omittedPaths: allPaths.length - paths.length,
|
|
139
|
+
bodies,
|
|
140
|
+
omittedBodies,
|
|
141
|
+
bounds: { maxPaths, maxBodies, maxBodyChars },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
51
145
|
export function buildMemoryAreas(index, limits = {}) {
|
|
52
146
|
const graph = index?.graph ?? { nodes: [], edges: [] };
|
|
53
147
|
const itemLimit = limits.items ?? 4;
|
package/src/memory/config.mjs
CHANGED
|
@@ -24,6 +24,16 @@ export const DEFAULT_VALIDATION_POLICY = {
|
|
|
24
24
|
export const DEFAULT_INTEGRATIONS_POLICY = {
|
|
25
25
|
trello: { syncPaths: [] },
|
|
26
26
|
};
|
|
27
|
+
export const DEFAULT_LOAD_POLICY = {
|
|
28
|
+
pinnedPaths: [],
|
|
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/**'],
|
|
36
|
+
};
|
|
27
37
|
export const DEFAULT_IMPACT_RANKING_POLICY = {
|
|
28
38
|
preset: 'balanced',
|
|
29
39
|
weights: { ppr: 40, traceability: 30, memoryPolicy: 10, verification: 15, proximity: 10, semantic: 10, freshness: 5, archivePenalty: -25 },
|
|
@@ -71,6 +81,7 @@ const DEFAULT_MEMORY_AREAS = [
|
|
|
71
81
|
{ id: 'active-plan', label: 'Active Plans', paths: ['docs/plan/**'], scope: 'local', freshness: 'fresh', role: 'active-task-intent', priority: 95, includeBodiesByDefault: true },
|
|
72
82
|
{ id: 'archive-map', label: 'Archive Map', paths: ['docs/archive/README.md'], scope: 'local', freshness: 'stale', role: 'historical-memory-map', priority: 65, includeBodiesByDefault: true },
|
|
73
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 },
|
|
74
85
|
];
|
|
75
86
|
|
|
76
87
|
function cloneClarifyGuidance(clarify) {
|
|
@@ -170,8 +181,22 @@ export function cloneIntegrationsPolicy(policy = DEFAULT_INTEGRATIONS_POLICY) {
|
|
|
170
181
|
return { trello: { syncPaths: [...(policy.trello?.syncPaths ?? [])] } };
|
|
171
182
|
}
|
|
172
183
|
|
|
184
|
+
export function cloneLoadPolicy(policy = DEFAULT_LOAD_POLICY) {
|
|
185
|
+
return {
|
|
186
|
+
pinnedPaths: [...(policy.pinnedPaths ?? [])],
|
|
187
|
+
pinnedBodies: [...(policy.pinnedBodies ?? [])],
|
|
188
|
+
documentationSummary: {
|
|
189
|
+
exclude: [...(policy.documentationSummary?.exclude ?? DEFAULT_LOAD_POLICY.documentationSummary.exclude)],
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function clonePlanModePolicy(policy = DEFAULT_PLAN_MODE_POLICY) {
|
|
195
|
+
return { writablePaths: [...(policy.writablePaths ?? DEFAULT_PLAN_MODE_POLICY.writablePaths)] };
|
|
196
|
+
}
|
|
197
|
+
|
|
173
198
|
export function defaultMemoryConfig() {
|
|
174
|
-
return { source: 'default', areas: DEFAULT_MEMORY_AREAS.map(cloneArea), traceability: cloneTraceabilityPolicy(), validation: cloneValidationPolicy(), impactRanking: cloneImpactRankingPolicy(), referenceExpansion: cloneReferenceExpansionPolicy(), integrations: cloneIntegrationsPolicy() };
|
|
199
|
+
return { source: 'default', areas: DEFAULT_MEMORY_AREAS.map(cloneArea), traceability: cloneTraceabilityPolicy(), validation: cloneValidationPolicy(), impactRanking: cloneImpactRankingPolicy(), referenceExpansion: cloneReferenceExpansionPolicy(), integrations: cloneIntegrationsPolicy(), load: cloneLoadPolicy(), planMode: clonePlanModePolicy() };
|
|
175
200
|
}
|
|
176
201
|
|
|
177
202
|
export function normalizePathPattern(value = '') {
|
|
@@ -214,14 +239,6 @@ export function memoryAreaForPath(path = '', config = defaultMemoryConfig()) {
|
|
|
214
239
|
return resolveMemoryArea(path, config)?.id;
|
|
215
240
|
}
|
|
216
241
|
|
|
217
|
-
export function memoryRoleForPath(path = '', config = defaultMemoryConfig()) {
|
|
218
|
-
return resolveMemoryArea(path, config)?.role;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function retrievalPriorityForPath(path = '', config = defaultMemoryConfig()) {
|
|
222
|
-
return resolveMemoryArea(path, config)?.priority ?? 30;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
242
|
function normalizeTraceabilityPolicy(raw) {
|
|
226
243
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return cloneTraceabilityPolicy();
|
|
227
244
|
return {
|
|
@@ -253,11 +270,33 @@ function normalizeIntegrationsPolicy(raw) {
|
|
|
253
270
|
return cloneIntegrationsPolicy({ trello: { syncPaths: [...new Set(syncPaths)] } });
|
|
254
271
|
}
|
|
255
272
|
|
|
256
|
-
function isSecretLikePathPattern(value = '') {
|
|
273
|
+
export function isSecretLikePathPattern(value = '') {
|
|
257
274
|
const normalized = normalizePathPattern(value);
|
|
258
275
|
return /(^|\/)(\.env|\.npmrc|\.pypirc|id_rsa|id_dsa|id_ed25519|credentials?|secrets?)(\.|\/|$)/i.test(normalized);
|
|
259
276
|
}
|
|
260
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
|
+
|
|
286
|
+
function normalizeLoadPolicy(raw) {
|
|
287
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return cloneLoadPolicy();
|
|
288
|
+
const normalize = (values) => (Array.isArray(values) ? [...new Set(values.map(normalizePathPattern))] : []);
|
|
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
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
261
300
|
export function trelloSyncPaths(config = defaultMemoryConfig()) {
|
|
262
301
|
return [...new Set(['docs/trello/**', ...((config.integrations ?? DEFAULT_INTEGRATIONS_POLICY).trello?.syncPaths ?? [])])];
|
|
263
302
|
}
|
|
@@ -361,6 +400,41 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
|
|
|
361
400
|
}
|
|
362
401
|
}
|
|
363
402
|
}
|
|
403
|
+
const load = data.load;
|
|
404
|
+
if (load !== undefined) {
|
|
405
|
+
if (!load || typeof load !== 'object' || Array.isArray(load)) {
|
|
406
|
+
add('LOAD_CONFIG_INVALID', 'load', 'Expected an object.');
|
|
407
|
+
} else {
|
|
408
|
+
for (const [key, code] of [['pinnedPaths', 'LOAD_CONFIG_INVALID_PINNED_PATHS'], ['pinnedBodies', 'LOAD_CONFIG_INVALID_PINNED_BODIES']]) {
|
|
409
|
+
const values = load[key];
|
|
410
|
+
if (values === undefined) continue;
|
|
411
|
+
if (!Array.isArray(values)) add(code, `load.${key}`, 'Expected an array of path strings.');
|
|
412
|
+
else {
|
|
413
|
+
if (values.some((value) => !isValidPathPattern(value) || (typeof value === 'string' && value.trim().startsWith('/')))) add(code, `load.${key}`, 'Expected repository-relative path strings using exact paths, /** subtree patterns, or **/suffix patterns.');
|
|
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.');
|
|
415
|
+
}
|
|
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.');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
364
438
|
const referenceExpansion = data.referenceExpansion;
|
|
365
439
|
if (referenceExpansion !== undefined) {
|
|
366
440
|
if (!referenceExpansion || typeof referenceExpansion !== 'object' || Array.isArray(referenceExpansion)) {
|
|
@@ -472,7 +546,9 @@ export function readMemoryConfig(root = '.') {
|
|
|
472
546
|
const impactRanking = normalizeImpactRankingPolicy(data.impactRanking);
|
|
473
547
|
const referenceExpansion = normalizeReferenceExpansionPolicy(data.referenceExpansion);
|
|
474
548
|
const integrations = normalizeIntegrationsPolicy(data.integrations);
|
|
475
|
-
|
|
549
|
+
const load = normalizeLoadPolicy(data.load);
|
|
550
|
+
const planMode = normalizePlanModePolicy(data.planMode);
|
|
551
|
+
return configuredAreas.length > 0 ? { source: name, areas: configuredAreas, traceability, validation, impactRanking, referenceExpansion, integrations, load, planMode, errors: [] } : { ...defaultMemoryConfig(), traceability, validation, impactRanking, referenceExpansion, integrations, load, planMode, source: name, errors: [] };
|
|
476
552
|
} catch (error) {
|
|
477
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.` }] };
|
|
478
554
|
}
|
|
@@ -505,6 +581,8 @@ export function defaultDotdotgodConfigData() {
|
|
|
505
581
|
impactRanking: cloneImpactRankingPolicy(config.impactRanking),
|
|
506
582
|
referenceExpansion: { fuzzy: { lowSignal: { add: [], remove: [] } } },
|
|
507
583
|
integrations: cloneIntegrationsPolicy(config.integrations),
|
|
584
|
+
load: cloneLoadPolicy(config.load),
|
|
585
|
+
planMode: clonePlanModePolicy(config.planMode),
|
|
508
586
|
};
|
|
509
587
|
}
|
|
510
588
|
|
|
@@ -531,6 +609,8 @@ export function memoryConfigSummary(config) {
|
|
|
531
609
|
impactRanking: cloneImpactRankingPolicy(config.impactRanking ?? DEFAULT_IMPACT_RANKING_POLICY),
|
|
532
610
|
referenceExpansion: cloneReferenceExpansionPolicy(config.referenceExpansion),
|
|
533
611
|
integrations: cloneIntegrationsPolicy(config.integrations),
|
|
612
|
+
load: cloneLoadPolicy(config.load),
|
|
613
|
+
planMode: clonePlanModePolicy(config.planMode),
|
|
534
614
|
};
|
|
535
615
|
}
|
|
536
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;
|