@dotdotgod/cli 0.2.28 → 0.3.0
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/package.json +1 -1
- package/src/cli/usage.mjs +1 -1
- package/src/commands/config.mjs +1 -0
- package/src/commands/query.mjs +1 -1
- package/src/config/templates.mjs +16 -5
- package/src/core.mjs +1 -1
- package/src/graph/communities.mjs +20 -21
- package/src/impact/report.mjs +2 -2
- package/src/impact/scoring.mjs +7 -11
- package/src/init.mjs +18 -9
- package/src/memory/config.mjs +36 -12
- package/src/query/chunks.mjs +4 -4
- package/src/reference/extract.mjs +7 -3
- package/src/reference/resolve.mjs +5 -4
- package/src/validate/run.mjs +9 -8
package/package.json
CHANGED
package/src/cli/usage.mjs
CHANGED
|
@@ -11,7 +11,7 @@ export function commandUsage(command = 'root') {
|
|
|
11
11
|
dotdotgod validate <root> [--include-local-memory] [--check-index] [--max-lines n] [--max-chars n] [--no-link-check] [--json]`;
|
|
12
12
|
case 'init':
|
|
13
13
|
return `Usage:
|
|
14
|
-
dotdotgod init <root> [--project-name NAME] [--template NAME] [--dotdot-setting] [--dry-run] [--json]
|
|
14
|
+
dotdotgod init <root> [--project-name NAME] [--template NAME] [--documentation-root PATH] [--dotdot-setting] [--dry-run] [--json]
|
|
15
15
|
|
|
16
16
|
Create AGENTS.md, agent entrypoints, docs indexes, a project config from an initialization template, and local memory gitignore entries.`;
|
|
17
17
|
case 'index':
|
package/src/commands/config.mjs
CHANGED
|
@@ -40,6 +40,7 @@ function formatConfigOutput(payload) {
|
|
|
40
40
|
const errors = payload.errors ?? [];
|
|
41
41
|
const lines = [`dotdotgod config: ${payload.source}${errors.length > 0 ? ' (invalid; using defaults)' : ''}`];
|
|
42
42
|
lines.push(`- path: ${payload.path ?? 'none'}`);
|
|
43
|
+
lines.push(`- documentation root: ${payload.config.documentation.root}`);
|
|
43
44
|
lines.push(`- memory areas: ${payload.config.areas.length}`);
|
|
44
45
|
lines.push(`- embedding: ${payload.embedding.profile.provider}/${payload.embedding.profile.model} (${payload.embedding.source})`);
|
|
45
46
|
lines.push(`- traceability required: ${(payload.config.traceability.required ?? []).join(', ') || 'none'}`);
|
package/src/commands/query.mjs
CHANGED
|
@@ -63,7 +63,7 @@ export async function buildVectorIndex(root, embedOrOptions = {}) {
|
|
|
63
63
|
const resolved = await resolveEmbedder(root, options);
|
|
64
64
|
const config = readMemoryConfig(root);
|
|
65
65
|
const exclude = config.load?.documentationSummary?.exclude ?? ['docs/plan', 'docs/archive'];
|
|
66
|
-
const chunks = collectDocumentationChunks(root, exclude);
|
|
66
|
+
const chunks = collectDocumentationChunks(root, exclude, config.documentation?.root ?? 'docs');
|
|
67
67
|
const cached = readVectorCache(root, resolved.identity);
|
|
68
68
|
const cachedOffsets = new Map((cached?.chunks ?? []).map((chunk, index) => [chunk.fingerprint, index]));
|
|
69
69
|
const rows = new Array(chunks.length);
|
package/src/config/templates.mjs
CHANGED
|
@@ -42,11 +42,11 @@ const BUILT_IN_TEMPLATE_SCAFFOLDS = {
|
|
|
42
42
|
],
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
export function builtInTemplateScaffold(name) {
|
|
45
|
+
export function builtInTemplateScaffold(name, { documentationRoot = 'docs' } = {}) {
|
|
46
46
|
const entries = BUILT_IN_TEMPLATE_SCAFFOLDS[name];
|
|
47
47
|
if (!entries) return null;
|
|
48
48
|
return entries.map(([path, heading, description]) => ({
|
|
49
|
-
path,
|
|
49
|
+
path: path.replace(/^docs(?=\/|$)/, documentationRoot),
|
|
50
50
|
type: heading ? 'file' : 'directory',
|
|
51
51
|
...(heading ? { content: `# ${heading}\n\n${description}` } : {}),
|
|
52
52
|
}));
|
|
@@ -122,7 +122,13 @@ function withPolicy(areas, required, definitionsList) {
|
|
|
122
122
|
return config;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
function rebaseBuiltIn(value, root) {
|
|
126
|
+
if (Array.isArray(value)) return value.map(item => rebaseBuiltIn(item, root));
|
|
127
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, rebaseBuiltIn(item, root)]));
|
|
128
|
+
return typeof value === 'string' ? value.replace(/^docs(?=\/|$)/, root) : value;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function rawBuiltInTemplateData(name) {
|
|
126
132
|
if (name === 'software') return defaultDotdotgodConfigData();
|
|
127
133
|
if (name === 'research') return withPolicy([
|
|
128
134
|
area('spec', 'Research Specifications', ['docs/spec/**'], 'research-contract', 85),
|
|
@@ -162,8 +168,13 @@ export function builtInTemplateData(name) {
|
|
|
162
168
|
return null;
|
|
163
169
|
}
|
|
164
170
|
|
|
165
|
-
export function
|
|
166
|
-
const data =
|
|
171
|
+
export function builtInTemplateData(name, { documentationRoot = 'docs' } = {}) {
|
|
172
|
+
const data = rawBuiltInTemplateData(name);
|
|
173
|
+
return data ? rebaseBuiltIn(data, documentationRoot) : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function builtInTemplateText(name, options) {
|
|
177
|
+
const data = builtInTemplateData(name, options);
|
|
167
178
|
return data ? `${JSON.stringify(data, null, 2)}\n` : null;
|
|
168
179
|
}
|
|
169
180
|
|
package/src/core.mjs
CHANGED
|
@@ -13,7 +13,7 @@ export { commandUsage, hasHelpToken, helpCommandFromArgs, isHelpToken, isVersion
|
|
|
13
13
|
export { rel } from './common/paths.mjs';
|
|
14
14
|
export { extractAnchors, extractFirstHeading, extractLinks, headingToAnchor, isKebabCase, isNumberedSeriesFilename, isUpperSnakeMarkdown, removeCodeBlocks, suggestFilenameFromHeading } from './docs/markdown.mjs';
|
|
15
15
|
export { extractDotdotgodTraceabilityBlocks, findTraceabilityLinksRegion, isLocalRelativeTraceabilityPath, renderCompactTraceabilityBlock, stripTraceabilityLinksRegion, syncTraceabilityLinksInContent, traceabilityExample, validateTraceabilityBlock, validateTraceabilityLinksRegion, validateTraceabilityPlacement } from './docs/traceability.mjs';
|
|
16
|
-
export { DEFAULT_IMPACT_RANKING_POLICY, DEFAULT_LOAD_POLICY, DEFAULT_VALIDATION_POLICY, SEMANTIC_RELATIONS, cloneImpactRankingPolicy, cloneLoadPolicy, cloneReferenceExpansionPolicy, cloneValidationPolicy, defaultDotdotgodConfigData, defaultDotdotgodConfigText, defaultMemoryConfig, isMarkdownSizeExcluded, isSecretLikePathPattern, isValidPathPattern, matchPathPattern, memoryAreaForPath, memoryConfigSummary, normalizeLowSignalTerm, readMemoryConfig, requiresTraceability, resolveMemoryArea, validateMemoryConfigData } from './memory/config.mjs';
|
|
16
|
+
export { DEFAULT_IMPACT_RANKING_POLICY, DEFAULT_LOAD_POLICY, DEFAULT_VALIDATION_POLICY, SEMANTIC_RELATIONS, cloneImpactRankingPolicy, cloneLoadPolicy, cloneReferenceExpansionPolicy, cloneValidationPolicy, defaultDotdotgodConfigData, defaultDotdotgodConfigText, defaultMemoryConfig, isValidDocumentationRoot, normalizeDocumentationRoot, isMarkdownSizeExcluded, isSecretLikePathPattern, isValidPathPattern, matchPathPattern, memoryAreaForPath, memoryConfigSummary, normalizeLowSignalTerm, readMemoryConfig, requiresTraceability, resolveMemoryArea, validateMemoryConfigData } from './memory/config.mjs';
|
|
17
17
|
export { BUILT_IN_TEMPLATE_NAMES, DEFAULT_TEMPLATE_NAME, builtInTemplateData, builtInTemplateScaffold, builtInTemplateText, dotdotgodHome, resolveInitializationTemplate } from './config/templates.mjs';
|
|
18
18
|
export { buildCommunities, relationWeight } from './graph/communities.mjs';
|
|
19
19
|
export { addEdge, addNode, compactGraph, expandGraph, graphStats, jsonSize, shardFile, writeJson } from './graph/store.mjs';
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { Graph, leiden } from 'leiden-ts';
|
|
2
|
+
import { resolveMemoryArea } from '../memory/config.mjs';
|
|
2
3
|
|
|
3
4
|
const DURABLE_COMMUNITY_NODE_TYPES = new Set(['file', 'memory_area', 'package_resource', 'package', 'script', 'binary']);
|
|
4
5
|
|
|
5
|
-
function docsArea(path = '') {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (path.startsWith('docs/plan/')) return 'active-plan';
|
|
10
|
-
if (path.startsWith('docs/archive/')) return 'archive';
|
|
11
|
-
return null;
|
|
6
|
+
function docsArea(path = '', config) {
|
|
7
|
+
const area = resolveMemoryArea(path, config);
|
|
8
|
+
const roles = { 'behavior-truth': 'spec', 'verification-knowledge': 'test', 'architecture-rationale': 'architecture', 'active-task-intent': 'active-plan', 'historical-memory-map': 'archive', 'historical-memory-body': 'archive', 'project-documentation': 'docs' };
|
|
9
|
+
return roles[area?.role] ?? null;
|
|
12
10
|
}
|
|
13
11
|
|
|
14
|
-
function communityKeyForNode(node) {
|
|
12
|
+
function communityKeyForNode(node, config) {
|
|
15
13
|
if (node.type === 'memory_area') return `memory-${node.area}`;
|
|
16
14
|
const path = node.path ?? node.id?.replace(/^file:/, '').replace(/^test:/, '') ?? '';
|
|
17
15
|
if (path.startsWith('packages/pi/extensions/plan-mode/')) return 'pi-plan-mode';
|
|
@@ -21,7 +19,7 @@ function communityKeyForNode(node) {
|
|
|
21
19
|
if (path.startsWith('packages/claude-code/')) return 'claude-code-adapter';
|
|
22
20
|
if (path.startsWith('packages/codex/')) return 'codex-adapter';
|
|
23
21
|
if (path.startsWith('packages/shared/')) return 'shared-adapter-resources';
|
|
24
|
-
const area = docsArea(path);
|
|
22
|
+
const area = docsArea(path, config);
|
|
25
23
|
if (area) return `docs-${area}`;
|
|
26
24
|
if (node.type === 'package' || node.type === 'script' || node.type === 'binary' || node.type === 'dependency' || node.type === 'package_resource') return 'package-metadata';
|
|
27
25
|
if (node.type === 'command') return `command-${node.name}`;
|
|
@@ -47,11 +45,11 @@ export function relationWeight(relation) {
|
|
|
47
45
|
return 1;
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
function addCommunityDetails(community, node, itemLimit) {
|
|
48
|
+
function addCommunityDetails(community, node, itemLimit, config) {
|
|
51
49
|
community.nodeCount += 1;
|
|
52
50
|
const path = node.path ?? node.id?.replace(/^file:/, '').replace(/^test:/, '');
|
|
53
51
|
if (node.type === 'file') {
|
|
54
|
-
const area = docsArea(path);
|
|
52
|
+
const area = docsArea(path, config);
|
|
55
53
|
community.omitted += addBounded(area ? community.docs : community.files, path, itemLimit);
|
|
56
54
|
} else if (node.type === 'heading' && node.path) community.omitted += addBounded(community.docs, node.path, itemLimit);
|
|
57
55
|
else if (node.type === 'memory_area') community.omitted += addBounded(community.docs, `memory_area:${node.area}`, itemLimit);
|
|
@@ -65,14 +63,14 @@ function makeCommunity(id, label = communityLabel(id)) {
|
|
|
65
63
|
return { id, label, files: [], docs: [], commands: [], events: [], tests: [], packageResources: [], nodeCount: 0, edgeCount: 0, omitted: 0 };
|
|
66
64
|
}
|
|
67
65
|
|
|
68
|
-
function deterministicCommunities(graph, maxCommunities, itemLimit, fallback = false) {
|
|
66
|
+
function deterministicCommunities(graph, maxCommunities, itemLimit, config, fallback = false) {
|
|
69
67
|
const map = new Map();
|
|
70
68
|
for (const node of graph.nodes) {
|
|
71
|
-
const id = communityKeyForNode(node);
|
|
69
|
+
const id = communityKeyForNode(node, config);
|
|
72
70
|
if (!map.has(id)) map.set(id, makeCommunity(id));
|
|
73
|
-
addCommunityDetails(map.get(id), node, itemLimit);
|
|
71
|
+
addCommunityDetails(map.get(id), node, itemLimit, config);
|
|
74
72
|
}
|
|
75
|
-
const nodeToCommunity = new Map(graph.nodes.map((node) => [node.id, communityKeyForNode(node)]));
|
|
73
|
+
const nodeToCommunity = new Map(graph.nodes.map((node) => [node.id, communityKeyForNode(node, config)]));
|
|
76
74
|
for (const edge of graph.edges) {
|
|
77
75
|
const source = nodeToCommunity.get(edge.source);
|
|
78
76
|
const target = nodeToCommunity.get(edge.target);
|
|
@@ -112,18 +110,19 @@ function buildLeidenProjection(graph) {
|
|
|
112
110
|
return { durable, edges: [...adjacency.entries()].map(([key, weight]) => [...key.split(':').map(Number), weight]) };
|
|
113
111
|
}
|
|
114
112
|
|
|
115
|
-
function labelForLeidenCommunity(nodes) {
|
|
113
|
+
function labelForLeidenCommunity(nodes, config) {
|
|
116
114
|
const counts = new Map();
|
|
117
|
-
for (const node of nodes) counts.set(communityKeyForNode(node), (counts.get(communityKeyForNode(node)) ?? 0) + 1);
|
|
115
|
+
for (const node of nodes) counts.set(communityKeyForNode(node, config), (counts.get(communityKeyForNode(node, config)) ?? 0) + 1);
|
|
118
116
|
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0]?.[0] ?? 'community';
|
|
119
117
|
}
|
|
120
118
|
|
|
121
119
|
export function buildCommunities(index, limits = {}) {
|
|
120
|
+
const config = index?.memoryConfig;
|
|
122
121
|
const graph = index?.graph ?? { nodes: [], edges: [] };
|
|
123
122
|
const maxCommunities = limits.communities ?? 8;
|
|
124
123
|
const itemLimit = limits.items ?? 8;
|
|
125
124
|
const projection = buildLeidenProjection(graph);
|
|
126
|
-
if (projection.durable.length < 3 || projection.edges.length === 0) return deterministicCommunities(graph, maxCommunities, itemLimit, true);
|
|
125
|
+
if (projection.durable.length < 3 || projection.edges.length === 0) return deterministicCommunities(graph, maxCommunities, itemLimit, config, true);
|
|
127
126
|
|
|
128
127
|
try {
|
|
129
128
|
const result = leiden(Graph.fromEdgeList(projection.durable.length, projection.edges), { seed: 42, resolution: limits.resolution ?? 1.0 });
|
|
@@ -133,9 +132,9 @@ export function buildCommunities(index, limits = {}) {
|
|
|
133
132
|
byCommunity.get(communityId).push(projection.durable[index]);
|
|
134
133
|
});
|
|
135
134
|
const communities = [...byCommunity.entries()].map(([communityId, nodes]) => {
|
|
136
|
-
const labelKey = labelForLeidenCommunity(nodes);
|
|
135
|
+
const labelKey = labelForLeidenCommunity(nodes, config);
|
|
137
136
|
const community = makeCommunity(`leiden-${communityId}`, communityLabel(labelKey));
|
|
138
|
-
for (const node of nodes.sort((a, b) => a.id.localeCompare(b.id))) addCommunityDetails(community, node, itemLimit);
|
|
137
|
+
for (const node of nodes.sort((a, b) => a.id.localeCompare(b.id))) addCommunityDetails(community, node, itemLimit, config);
|
|
139
138
|
return community;
|
|
140
139
|
});
|
|
141
140
|
const nodeToCommunity = new Map();
|
|
@@ -148,6 +147,6 @@ export function buildCommunities(index, limits = {}) {
|
|
|
148
147
|
const all = communities.sort((a, b) => (b.nodeCount + b.edgeCount) - (a.nodeCount + a.edgeCount) || a.id.localeCompare(b.id));
|
|
149
148
|
return { communities: all.slice(0, maxCommunities), omitted: Math.max(0, all.length - maxCommunities), total: all.length, method: 'leiden', fallback: false, modularity: result.modularity };
|
|
150
149
|
} catch {
|
|
151
|
-
return deterministicCommunities(graph, maxCommunities, itemLimit, true);
|
|
150
|
+
return deterministicCommunities(graph, maxCommunities, itemLimit, config, true);
|
|
152
151
|
}
|
|
153
152
|
}
|
package/src/impact/report.mjs
CHANGED
|
@@ -96,13 +96,13 @@ function buildCombinedImpactReport(index, changedPaths, limits = {}) {
|
|
|
96
96
|
const retrieval = node.retrieval ?? retrievalMetadataForPath(path);
|
|
97
97
|
const reasonSignals = reasonList.map((reason) => `reason:${reason}`);
|
|
98
98
|
const hasCuratedEvidence = reasonList.some((reason) => curatedRelations.has(reason.replace(/^incoming:/, '')));
|
|
99
|
-
const scored = scoreImpactItem({ ...node, reasons: reasonList, retrieval }, seedSet, changedPaths, policy, pprScores);
|
|
99
|
+
const scored = scoreImpactItem({ ...node, reasons: reasonList, retrieval }, seedSet, changedPaths, policy, pprScores, config);
|
|
100
100
|
return { ...node, reasons: reasonList, hasCuratedEvidence, ...(vectorEvidence.has(id) ? { vectorEvidence: vectorEvidence.get(id) } : {}), retrieval: { ...retrieval, signals: [...new Set([...(retrieval.signals ?? []), ...reasonSignals])] }, ...scored };
|
|
101
101
|
}).sort(compareImpactItems(seeds));
|
|
102
102
|
const related = selectImpactItems(relatedAll, maxRelated, seeds);
|
|
103
103
|
for (const item of related) {
|
|
104
104
|
if (item.type === 'file') {
|
|
105
|
-
const area = docsArea(item.path);
|
|
105
|
+
const area = docsArea(item.path, config);
|
|
106
106
|
if (area) addImpactItem(groups.docs, { ...item, area }, limits.docs ?? 10);
|
|
107
107
|
else if (isTestPath(item.path)) addImpactItem(groups.tests, item, limits.tests ?? 10);
|
|
108
108
|
else addImpactItem(groups.files, item, limits.files ?? 10);
|
package/src/impact/scoring.mjs
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
import { SEMANTIC_RELATIONS } from '../memory/config.mjs';
|
|
1
|
+
import { SEMANTIC_RELATIONS, resolveMemoryArea } from '../memory/config.mjs';
|
|
2
2
|
import { relationWeight } from '../graph/communities.mjs';
|
|
3
3
|
|
|
4
4
|
export function isTestPath(path = '') {
|
|
5
5
|
return /(^|\/)(test|tests)\//.test(path) || /\.(test|spec)\.(mjs|cjs|js|jsx|ts|tsx)$/.test(path);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function docsArea(path = '') {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if (path.startsWith('docs/plan/')) return 'plan';
|
|
13
|
-
if (path.startsWith('docs/archive/')) return 'archive-index';
|
|
14
|
-
if (path.startsWith('docs/')) return 'docs';
|
|
15
|
-
return undefined;
|
|
8
|
+
export function docsArea(path = '', config) {
|
|
9
|
+
const area = resolveMemoryArea(path, config);
|
|
10
|
+
const byRole = { 'behavior-truth': 'spec', 'architecture-rationale': 'arch', 'verification-knowledge': 'test-docs', 'active-task-intent': 'plan', 'historical-memory-map': 'archive-index', 'historical-memory-body': 'archive-index', 'project-documentation': 'docs', 'documentation-routing-map': 'docs' };
|
|
11
|
+
return byRole[area?.role] ?? (area?.id === 'spec' ? 'spec' : area?.id === 'architecture' ? 'arch' : area?.id === 'test' ? 'test-docs' : undefined);
|
|
16
12
|
}
|
|
17
13
|
|
|
18
14
|
function clamp(value, min, max) {
|
|
@@ -61,10 +57,10 @@ export function buildPersonalizedPageRank(graph, seeds, policy) {
|
|
|
61
57
|
return ranks;
|
|
62
58
|
}
|
|
63
59
|
|
|
64
|
-
export function scoreImpactItem(item, seeds, changedPaths, policy, pprScores) {
|
|
60
|
+
export function scoreImpactItem(item, seeds, changedPaths, policy, pprScores, config) {
|
|
65
61
|
const seedSet = seeds instanceof Set ? seeds : new Set(Array.isArray(seeds) ? seeds : [seeds]);
|
|
66
62
|
const paths = Array.isArray(changedPaths) ? changedPaths : [changedPaths];
|
|
67
|
-
const archiveSeeded = paths.some((path) => path
|
|
63
|
+
const archiveSeeded = paths.some((path) => resolveMemoryArea(path, config)?.role?.startsWith('historical-memory'));
|
|
68
64
|
if (seedSet.has(item.id)) return { impactScore: 100, scoreBreakdown: { seed: 100, connection: { ppr: 80, reference: policy.ppr.reference }, memory: { priority: 20, policyAdjustments: 0 } } };
|
|
69
65
|
const retrieval = item.retrieval ?? {};
|
|
70
66
|
const probability = pprScores.get(item.id) ?? 0;
|
package/src/init.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
-
import { builtInTemplateScaffold, resolveInitializationTemplate } from './config/templates.mjs';
|
|
3
|
+
import { builtInTemplateData, builtInTemplateScaffold, resolveInitializationTemplate } from './config/templates.mjs';
|
|
4
|
+
import { isValidDocumentationRoot, normalizeDocumentationRoot, readMemoryConfig } from './memory/config.mjs';
|
|
4
5
|
|
|
5
6
|
function action(status, path, extra = {}) {
|
|
6
7
|
return { status, path, ...extra };
|
|
@@ -12,17 +13,18 @@ function formatAction(item) {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
function parseInitOptions(argv) {
|
|
15
|
-
const options = { root: null, projectName: '', dotdotSetting: false, dryRun: false, json: false, template: null };
|
|
16
|
+
const options = { root: null, projectName: '', dotdotSetting: false, dryRun: false, json: false, template: null, documentationRoot: 'docs', documentationRootExplicit: false };
|
|
16
17
|
for (let i = 0; i < argv.length; i += 1) {
|
|
17
18
|
const arg = argv[i];
|
|
18
19
|
if (arg === '--json') options.json = true;
|
|
19
20
|
else if (arg === '--dotdot-setting') options.dotdotSetting = true;
|
|
20
21
|
else if (arg === '--dry-run') options.dryRun = true;
|
|
21
|
-
else if (arg === '--project-name' || arg === '--template') {
|
|
22
|
+
else if (arg === '--project-name' || arg === '--template' || arg === '--documentation-root') {
|
|
22
23
|
const value = argv[i + 1];
|
|
23
24
|
if (!value || value.startsWith('-')) throw new Error(`${arg} requires a value`);
|
|
24
25
|
if (arg === '--project-name') options.projectName = value;
|
|
25
|
-
else options.template = value;
|
|
26
|
+
else if (arg === '--template') options.template = value;
|
|
27
|
+
else { options.documentationRoot = normalizeDocumentationRoot(value); options.documentationRootExplicit = true; }
|
|
26
28
|
i += 1;
|
|
27
29
|
} else if (!arg.startsWith('-') && !options.root) {
|
|
28
30
|
options.root = arg;
|
|
@@ -33,6 +35,7 @@ function parseInitOptions(argv) {
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
if (!options.root) throw new Error('Missing required argument: <root>');
|
|
38
|
+
if (!isValidDocumentationRoot(options.documentationRoot)) throw new Error('Invalid --documentation-root: expected a safe repository-relative directory');
|
|
36
39
|
options.root = resolve(options.root);
|
|
37
40
|
if (!options.projectName) options.projectName = basename(options.root);
|
|
38
41
|
return options;
|
|
@@ -287,7 +290,7 @@ function initFiles(options) {
|
|
|
287
290
|
files.push(['docs/arch/DOCS_STRUCTURE.md', docsStructureContent()]);
|
|
288
291
|
files.push(['docs/arch/CODE_CONVENTIONS.md', codeConventionsContent()]);
|
|
289
292
|
}
|
|
290
|
-
return files;
|
|
293
|
+
return files.map(([path, content]) => [path.replace(/^docs(?=\/|$)/, options.documentationRoot), content.replaceAll('docs/', `${options.documentationRoot}/`).replaceAll('`docs`', `\`${options.documentationRoot}\``)]);
|
|
291
294
|
}
|
|
292
295
|
|
|
293
296
|
export function runInit(argv, usage) {
|
|
@@ -310,13 +313,19 @@ export function runInit(argv, usage) {
|
|
|
310
313
|
if (!options.dryRun) mkdirSync(options.root, { recursive: true });
|
|
311
314
|
|
|
312
315
|
const configExists = existsSync(join(options.root, 'dotdotgod.config.json'));
|
|
313
|
-
|
|
316
|
+
if (configExists && options.documentationRootExplicit) {
|
|
317
|
+
const existingRoot = readMemoryConfig(options.root).documentation.root;
|
|
318
|
+
if (existingRoot !== options.documentationRoot) initError(options, 'DOCUMENTATION_ROOT_CONFLICT', `Existing config uses documentation root ${existingRoot}; initialization does not migrate documentation.`);
|
|
319
|
+
}
|
|
320
|
+
let template = configExists ? null : resolveInitializationTemplate(options.template);
|
|
314
321
|
if (template && !template.ok) initError(options, template.error.code, template.error.message);
|
|
322
|
+
if (template?.source === 'custom' && normalizeDocumentationRoot(template.data.documentation?.root ?? 'docs') !== options.documentationRoot) initError(options, 'DOCUMENTATION_ROOT_CONFLICT', 'Custom template documentation.root conflicts with --documentation-root; custom policy paths are never rewritten.');
|
|
323
|
+
if (template?.source === 'bundled') { const data = builtInTemplateData(template.name, { documentationRoot: options.documentationRoot }); template = { ...template, data, text: `${JSON.stringify(data, null, 2)}\n` }; }
|
|
315
324
|
|
|
316
325
|
const actions = [];
|
|
317
326
|
for (const [relativePath, content] of initFiles(options)) writeInitFile(options, relativePath, content, actions);
|
|
318
327
|
if (template?.source === 'bundled') {
|
|
319
|
-
for (const item of builtInTemplateScaffold(template.name) ?? []) {
|
|
328
|
+
for (const item of builtInTemplateScaffold(template.name, { documentationRoot: options.documentationRoot }) ?? []) {
|
|
320
329
|
if (item.type === 'directory') ensureInitDirectory(options, item.path, actions);
|
|
321
330
|
else writeInitFile(options, item.path, item.content, actions);
|
|
322
331
|
}
|
|
@@ -324,9 +333,9 @@ export function runInit(argv, usage) {
|
|
|
324
333
|
|
|
325
334
|
writeInitFile(options, 'dotdotgod.config.json', template?.text ?? '', actions);
|
|
326
335
|
|
|
327
|
-
for (const entry of [
|
|
336
|
+
for (const entry of [`${options.documentationRoot}/plan`, `${options.documentationRoot}/archive`, '.dotdotgod']) ensureGitignoreEntry(options, entry, actions);
|
|
328
337
|
|
|
329
|
-
const payload = { ok: true, command: 'init', root: options.root, projectName: options.projectName, dryRun: options.dryRun, dotdotSetting: options.dotdotSetting, template: template ? { name: template.name, source: template.source, selectedBy: template.selectedBy } : null, actions };
|
|
338
|
+
const payload = { ok: true, command: 'init', root: options.root, projectName: options.projectName, documentationRoot: options.documentationRoot, dryRun: options.dryRun, dotdotSetting: options.dotdotSetting, template: template ? { name: template.name, source: template.source, selectedBy: template.selectedBy } : null, actions };
|
|
330
339
|
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
|
331
340
|
else {
|
|
332
341
|
if (template) console.log(`template ${template.source}:${template.name} (${template.selectedBy})`);
|
package/src/memory/config.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { isKebabCase } from '../docs/markdown.mjs';
|
|
|
5
5
|
import { validateEmbeddingProfile } from '../query/embedding-config.mjs';
|
|
6
6
|
|
|
7
7
|
const MEMORY_CONFIG_FILE = 'dotdotgod.config.json';
|
|
8
|
+
export const DEFAULT_DOCUMENTATION_ROOT = 'docs';
|
|
8
9
|
const MEMORY_SCOPES = new Set(['shared', 'local']);
|
|
9
10
|
const MEMORY_FRESHNESS = new Set(['fresh', 'stale']);
|
|
10
11
|
export const DEFAULT_TRACEABILITY_KEYS = [
|
|
@@ -65,7 +66,9 @@ const DEFAULT_FUZZY_LOW_SIGNAL_TERMS = [
|
|
|
65
66
|
'a', 'an', 'and', 'are', 'as', 'by', 'docs', 'document', 'for', 'from', 'it', 'of', 'on', 'plan', 'test', 'the', 'to', 'update', 'version', 'with',
|
|
66
67
|
'계획', '문서', '수정', '업데이트', '버전', '정보', '확인', '테스트',
|
|
67
68
|
];
|
|
68
|
-
|
|
69
|
+
function memoryAreasForRoot(documentationRoot = DEFAULT_DOCUMENTATION_ROOT) {
|
|
70
|
+
const d = (path = '') => path ? `${documentationRoot}/${path}` : documentationRoot;
|
|
71
|
+
return [
|
|
69
72
|
{ id: 'rules', label: 'Agent Rules', paths: ['AGENTS.md'], scope: 'shared', freshness: 'fresh', role: 'agent-working-rules', priority: 100, includeBodiesByDefault: true },
|
|
70
73
|
{ id: 'agent-entrypoint', label: 'Agent Entrypoints', paths: ['CLAUDE.md', 'CODEX.md'], scope: 'shared', freshness: 'fresh', role: 'agent-specific-entrypoint', priority: 85, includeBodiesByDefault: true },
|
|
71
74
|
{ id: 'project-overview', label: 'Project Overview', paths: ['README.md'], scope: 'shared', freshness: 'fresh', role: 'project-map', priority: 85, includeBodiesByDefault: true },
|
|
@@ -76,8 +79,10 @@ const DEFAULT_MEMORY_AREAS = [
|
|
|
76
79
|
{ id: 'active-plan', label: 'Active Plans', paths: ['docs/plan/**'], scope: 'local', freshness: 'fresh', role: 'active-task-intent', priority: 95, includeBodiesByDefault: true },
|
|
77
80
|
{ id: 'archive-map', label: 'Archive Map', paths: ['docs/archive/README.md'], scope: 'local', freshness: 'stale', role: 'historical-memory-map', priority: 65, includeBodiesByDefault: true },
|
|
78
81
|
{ 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 },
|
|
79
|
-
{ id: 'docs', label: 'Project Documentation', paths: ['
|
|
80
|
-
];
|
|
82
|
+
{ id: 'docs', label: 'Project Documentation', paths: [d('**')], scope: 'shared', freshness: 'fresh', role: 'project-documentation', priority: 60, includeBodiesByDefault: true },
|
|
83
|
+
].map(area => ({ ...area, paths: area.paths.map(path => path.replace(/^docs(?=\/|$)/, documentationRoot)), excludePaths: (area.excludePaths ?? []).map(path => path.replace(/^docs(?=\/|$)/, documentationRoot)) }));
|
|
84
|
+
}
|
|
85
|
+
const DEFAULT_MEMORY_AREAS = memoryAreasForRoot();
|
|
81
86
|
|
|
82
87
|
function cloneClarifyGuidance(clarify) {
|
|
83
88
|
if (!clarify || typeof clarify !== 'object' || Array.isArray(clarify)) return undefined;
|
|
@@ -198,8 +203,19 @@ export function clonePlanModePolicy(policy = DEFAULT_PLAN_MODE_POLICY) {
|
|
|
198
203
|
return { writablePaths: [...(policy.writablePaths ?? DEFAULT_PLAN_MODE_POLICY.writablePaths)] };
|
|
199
204
|
}
|
|
200
205
|
|
|
201
|
-
export function
|
|
202
|
-
return
|
|
206
|
+
export function normalizeDocumentationRoot(value = DEFAULT_DOCUMENTATION_ROOT) {
|
|
207
|
+
return String(value).trim().replaceAll('\\', '/').replace(/^\.\//, '').replace(/\/{2,}/g, '/').replace(/^\/+|\/+$/g, '');
|
|
208
|
+
}
|
|
209
|
+
export function isValidDocumentationRoot(value) {
|
|
210
|
+
if (typeof value !== 'string' || !value.trim() || /^[A-Za-z]:[\\/]/.test(value) || /^[/\\]{2}/.test(value) || value.trim().startsWith('/')) return false;
|
|
211
|
+
const root = normalizeDocumentationRoot(value);
|
|
212
|
+
if (!root || root === '.' || root.split('/').some(part => part === '..' || !part || /[*?\[\]{}]/.test(part)) || isSecretLikePathPattern(root)) return false;
|
|
213
|
+
const first = root.split('/')[0];
|
|
214
|
+
return first !== 'dotdotgod.config.json' && first !== '.dotdotgod';
|
|
215
|
+
}
|
|
216
|
+
export function defaultMemoryConfig({ documentationRoot = DEFAULT_DOCUMENTATION_ROOT } = {}) {
|
|
217
|
+
const root = normalizeDocumentationRoot(documentationRoot);
|
|
218
|
+
return { source: 'default', documentation: { root }, areas: memoryAreasForRoot(root).map(cloneArea), traceability: cloneTraceabilityPolicy({ ...DEFAULT_TRACEABILITY_POLICY, required: [`${root}/spec/**`] }), validation: cloneValidationPolicy(), impactRanking: cloneImpactRankingPolicy(), referenceExpansion: cloneReferenceExpansionPolicy(), load: cloneLoadPolicy({ ...DEFAULT_LOAD_POLICY, documentationSummary: { exclude: [`${root}/plan`, `${root}/archive`] } }), planMode: clonePlanModePolicy({ writablePaths: [`${root}/plan/**`, `${root}/archive/**`] }) };
|
|
203
219
|
}
|
|
204
220
|
|
|
205
221
|
export function normalizePathPattern(value = '') {
|
|
@@ -336,6 +352,10 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
|
|
|
336
352
|
add('MEMORY_CONFIG_INVALID', null, 'Config must be a JSON object.');
|
|
337
353
|
return errors;
|
|
338
354
|
}
|
|
355
|
+
if (data.documentation !== undefined) {
|
|
356
|
+
if (!data.documentation || typeof data.documentation !== 'object' || Array.isArray(data.documentation)) add('DOCUMENTATION_CONFIG_INVALID', 'documentation', 'Expected an object.');
|
|
357
|
+
else if (!isValidDocumentationRoot(data.documentation.root)) add('DOCUMENTATION_CONFIG_INVALID_ROOT', 'documentation.root', 'Expected a safe repository-relative directory without traversal, glob syntax, secrets, or reserved config/cache overlap.');
|
|
358
|
+
}
|
|
339
359
|
if (data.embedding !== undefined) {
|
|
340
360
|
try { validateEmbeddingProfile(data.embedding, 'embedding'); }
|
|
341
361
|
catch (error) { add('EMBEDDING_CONFIG_INVALID', 'embedding', error instanceof Error ? error.message : String(error)); }
|
|
@@ -420,7 +440,7 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
|
|
|
420
440
|
} else if (planMode.writablePaths !== undefined) {
|
|
421
441
|
const paths = planMode.writablePaths;
|
|
422
442
|
if (!Array.isArray(paths)) add('PLAN_MODE_CONFIG_INVALID_WRITABLE_PATHS', 'planMode.writablePaths', 'Expected an array of documentation path strings.');
|
|
423
|
-
else if (paths.some((value) => !isValidPathPattern(value) || typeof value !== 'string' || value.trim().startsWith('/') || (!normalizePathPattern(value).startsWith(
|
|
443
|
+
else if (paths.some((value) => !isValidPathPattern(value) || typeof value !== 'string' || value.trim().startsWith('/') || (!normalizePathPattern(value).startsWith(`${normalizeDocumentationRoot(data.documentation?.root ?? DEFAULT_DOCUMENTATION_ROOT)}/`)) || normalizePathPattern(value).startsWith(`${normalizeDocumentationRoot(data.documentation?.root ?? DEFAULT_DOCUMENTATION_ROOT)}/.`) || isSecretLikePathPattern(value))) add('PLAN_MODE_CONFIG_INVALID_WRITABLE_PATHS', 'planMode.writablePaths', 'Expected safe repository-relative paths under documentation.root using exact paths or /** subtree patterns.');
|
|
424
444
|
}
|
|
425
445
|
}
|
|
426
446
|
const referenceExpansion = data.referenceExpansion;
|
|
@@ -493,14 +513,16 @@ export function readMemoryConfig(root = '.') {
|
|
|
493
513
|
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
494
514
|
const errors = validateMemoryConfigData(data, root, MEMORY_CONFIG_FILE);
|
|
495
515
|
if (errors.length > 0) return { ...defaultMemoryConfig(), source: MEMORY_CONFIG_FILE, errors };
|
|
516
|
+
const documentationRoot = normalizeDocumentationRoot(data.documentation?.root ?? DEFAULT_DOCUMENTATION_ROOT);
|
|
517
|
+
const defaults = defaultMemoryConfig({ documentationRoot });
|
|
496
518
|
const configuredAreas = data.memory?.areas?.map(normalizeMemoryArea) ?? [];
|
|
497
|
-
const traceability = data.traceability === undefined ?
|
|
519
|
+
const traceability = data.traceability === undefined ? defaults.traceability : normalizeTraceabilityPolicy(data.traceability);
|
|
498
520
|
const validation = data.validation === undefined ? cloneValidationPolicy() : normalizeValidationPolicy(data.validation);
|
|
499
521
|
const impactRanking = normalizeImpactRankingPolicy(data.impactRanking);
|
|
500
522
|
const referenceExpansion = normalizeReferenceExpansionPolicy(data.referenceExpansion);
|
|
501
|
-
const load = normalizeLoadPolicy(data.load);
|
|
502
|
-
const planMode = normalizePlanModePolicy(data.planMode);
|
|
503
|
-
return configuredAreas.length > 0 ? { source: MEMORY_CONFIG_FILE, areas: configuredAreas, traceability, validation, impactRanking, referenceExpansion, load, planMode, errors: [] } : { ...
|
|
523
|
+
const load = data.load === undefined ? defaults.load : normalizeLoadPolicy(data.load);
|
|
524
|
+
const planMode = data.planMode === undefined ? defaults.planMode : normalizePlanModePolicy(data.planMode);
|
|
525
|
+
return configuredAreas.length > 0 ? { source: MEMORY_CONFIG_FILE, documentation: { root: documentationRoot }, areas: configuredAreas, traceability, validation, impactRanking, referenceExpansion, load, planMode, errors: [] } : { ...defaults, traceability, validation, impactRanking, referenceExpansion, load, planMode, source: MEMORY_CONFIG_FILE, errors: [] };
|
|
504
526
|
} catch (error) {
|
|
505
527
|
return { ...defaultMemoryConfig(), source: MEMORY_CONFIG_FILE, errors: [{ file: MEMORY_CONFIG_FILE, code: 'MEMORY_CONFIG_INVALID_JSON', message: `Invalid JSON: ${error instanceof Error ? error.message : String(error)}\nFix: repair ${MEMORY_CONFIG_FILE} so it is valid JSON, or remove it before regenerating the default config with \`dotdotgod config init <root>\`.` }] };
|
|
506
528
|
}
|
|
@@ -520,9 +542,10 @@ function serializableMemoryArea(area) {
|
|
|
520
542
|
}, area);
|
|
521
543
|
}
|
|
522
544
|
|
|
523
|
-
export function defaultDotdotgodConfigData() {
|
|
524
|
-
const config = defaultMemoryConfig();
|
|
545
|
+
export function defaultDotdotgodConfigData({ documentationRoot = DEFAULT_DOCUMENTATION_ROOT } = {}) {
|
|
546
|
+
const config = defaultMemoryConfig({ documentationRoot });
|
|
525
547
|
return {
|
|
548
|
+
documentation: { ...config.documentation },
|
|
526
549
|
memory: {
|
|
527
550
|
areas: (config.areas ?? []).map(serializableMemoryArea),
|
|
528
551
|
},
|
|
@@ -542,6 +565,7 @@ export function defaultDotdotgodConfigText() {
|
|
|
542
565
|
export function memoryConfigSummary(config) {
|
|
543
566
|
return {
|
|
544
567
|
source: config.source ?? 'default',
|
|
568
|
+
documentation: { root: config.documentation?.root ?? DEFAULT_DOCUMENTATION_ROOT },
|
|
545
569
|
areas: (config.areas ?? []).map((area) => withOptionalAreaMetadata({
|
|
546
570
|
id: area.id,
|
|
547
571
|
label: area.label,
|
package/src/query/chunks.mjs
CHANGED
|
@@ -10,8 +10,8 @@ export function textFingerprint(text) {
|
|
|
10
10
|
return createHash('sha256').update(text).digest('hex');
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function collectDocumentationMarkdown(root, exclude = ['docs/plan', 'docs/archive']) {
|
|
14
|
-
const docsRoot = join(root,
|
|
13
|
+
export function collectDocumentationMarkdown(root, exclude = ['docs/plan', 'docs/archive'], documentationRoot = 'docs') {
|
|
14
|
+
const docsRoot = join(root, documentationRoot);
|
|
15
15
|
const files = [];
|
|
16
16
|
const walk = (directory) => {
|
|
17
17
|
let entries;
|
|
@@ -84,8 +84,8 @@ export function chunkMarkdown(path, content, maxChars = MAX_CHUNK_CHARS) {
|
|
|
84
84
|
return sections;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
export function collectDocumentationChunks(root, exclude) {
|
|
88
|
-
return collectDocumentationMarkdown(root, exclude).flatMap((path) => {
|
|
87
|
+
export function collectDocumentationChunks(root, exclude, documentationRoot = 'docs') {
|
|
88
|
+
return collectDocumentationMarkdown(root, exclude, documentationRoot).flatMap((path) => {
|
|
89
89
|
const content = readFileSync(join(root, path), 'utf8');
|
|
90
90
|
return chunkMarkdown(path, content);
|
|
91
91
|
});
|
|
@@ -54,8 +54,12 @@ export function referencePathForNode(node) {
|
|
|
54
54
|
return '';
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export function isArchiveBodyPath(path = '') {
|
|
58
|
-
|
|
57
|
+
export function isArchiveBodyPath(path = '', memoryConfig) {
|
|
58
|
+
const area = memoryConfig?.areas?.find((candidate) => candidate.id === 'archive-body' || candidate.role === 'historical-memory-body');
|
|
59
|
+
if (!area) return path.startsWith('docs/archive/') && path !== 'docs/archive/README.md';
|
|
60
|
+
const normalize = (value) => value.replace(/\/\*\*$/, '');
|
|
61
|
+
return (area.paths ?? []).some((pattern) => path === normalize(pattern) || path.startsWith(`${normalize(pattern)}/`))
|
|
62
|
+
&& !(area.excludePaths ?? []).some((excluded) => path === excluded);
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
function aliasEntriesForPath(path = '') {
|
|
@@ -112,7 +116,7 @@ export function extractFuzzyReferences(prompt = '', index = null, options = {})
|
|
|
112
116
|
for (const node of graph.nodes ?? []) {
|
|
113
117
|
if (!['file', 'heading'].includes(node.type)) continue;
|
|
114
118
|
const path = referencePathForNode(node);
|
|
115
|
-
if (options.includeArchive !== true && isArchiveBodyPath(path)) continue;
|
|
119
|
+
if (options.includeArchive !== true && isArchiveBodyPath(path, options.memoryConfig ?? index?.memoryConfig)) continue;
|
|
116
120
|
for (const entry of referenceCandidateAliases(node)) {
|
|
117
121
|
const alias = String(entry.alias ?? '').replace(/\.md$/i, '').replace(/[_-]+/g, ' ').trim();
|
|
118
122
|
if (!fuzzyPhraseAllowed(alias, lowSignalTerms)) continue;
|
|
@@ -16,6 +16,7 @@ function exactReferenceScore(ref, alias, kind) {
|
|
|
16
16
|
export function resolveReferenceCandidates(index, ref, options = {}) {
|
|
17
17
|
const graph = index?.graph ?? { nodes: [], edges: [] };
|
|
18
18
|
const includeArchive = options.includeArchive === true;
|
|
19
|
+
const memoryConfig = options.memoryConfig ?? index?.memoryConfig;
|
|
19
20
|
const limit = Number.isFinite(options.maxResults) ? options.maxResults : DEFAULT_REFERENCE_LIMIT;
|
|
20
21
|
const query = String(ref ?? '').trim().replace(/^\[\[/, '').replace(/\]\]$/, '').split('|')[0].trim();
|
|
21
22
|
const normalized = normalizeReferenceAlias(query);
|
|
@@ -23,7 +24,7 @@ export function resolveReferenceCandidates(index, ref, options = {}) {
|
|
|
23
24
|
for (const node of graph.nodes ?? []) {
|
|
24
25
|
if (!['file', 'heading'].includes(node.type)) continue;
|
|
25
26
|
const path = referencePathForNode(node);
|
|
26
|
-
if (!includeArchive && isArchiveBodyPath(path)) continue;
|
|
27
|
+
if (!includeArchive && isArchiveBodyPath(path, memoryConfig)) continue;
|
|
27
28
|
let best = null;
|
|
28
29
|
for (const entry of referenceCandidateAliases(node)) {
|
|
29
30
|
const aliasKey = normalizeReferenceAlias(entry.alias);
|
|
@@ -95,7 +96,7 @@ export function runResolve(argv) {
|
|
|
95
96
|
const ref = options.rootArgv?.join(' ');
|
|
96
97
|
if (!ref) usage('Missing required argument: <ref>.', 'resolve');
|
|
97
98
|
const { status, index, metadata } = readFreshIndex(options.root);
|
|
98
|
-
const refs = [resolveReferenceCandidates(index, ref, options)];
|
|
99
|
+
const refs = [resolveReferenceCandidates(index, ref, { ...options, memoryConfig: readMemoryConfig(options.root) })];
|
|
99
100
|
const payload = { ok: status.ok, command: 'resolve', root: options.root, status, metadata, refs, omitted: refs.reduce((sum, item) => sum + item.omitted, 0) };
|
|
100
101
|
if (options.json) console.log(JSON.stringify(payload, null, 2));
|
|
101
102
|
else console.log(formatReferenceOutput(payload));
|
|
@@ -110,8 +111,8 @@ export function runExpand(argv) {
|
|
|
110
111
|
const { status, index, metadata } = readFreshIndex(options.root);
|
|
111
112
|
const fuzzyRefs = options.fuzzy ? extractFuzzyReferences(prompt, index, { ...options, memoryConfig: readMemoryConfig(options.root), existingTargets: refsInPrompt.map((item) => item.target) }) : [];
|
|
112
113
|
let refs = [
|
|
113
|
-
...refsInPrompt.map((item) => ({ ...resolveReferenceCandidates(index, item.target, options), source: 'explicit', raw: item.raw, label: item.label })),
|
|
114
|
-
...fuzzyRefs.map((item) => ({ ...resolveReferenceCandidates(index, item.target, options), source: 'fuzzy', raw: item.raw, confidence: item.confidence, reasons: item.reasons })),
|
|
114
|
+
...refsInPrompt.map((item) => ({ ...resolveReferenceCandidates(index, item.target, { ...options, memoryConfig: readMemoryConfig(options.root) }), source: 'explicit', raw: item.raw, label: item.label })),
|
|
115
|
+
...fuzzyRefs.map((item) => ({ ...resolveReferenceCandidates(index, item.target, { ...options, memoryConfig: readMemoryConfig(options.root) }), source: 'fuzzy', raw: item.raw, confidence: item.confidence, reasons: item.reasons })),
|
|
115
116
|
];
|
|
116
117
|
if (options.withImpact) refs = refs.map((item) => {
|
|
117
118
|
const topPath = item.top?.path;
|
package/src/validate/run.mjs
CHANGED
|
@@ -24,7 +24,9 @@ export function runValidate(argv) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const root = resolve(options.root);
|
|
27
|
-
const
|
|
27
|
+
const memoryConfig = readMemoryConfig(root);
|
|
28
|
+
const documentationRoot = memoryConfig.documentation?.root ?? 'docs';
|
|
29
|
+
const docs = join(root, documentationRoot);
|
|
28
30
|
const errors = [];
|
|
29
31
|
const warnings = [];
|
|
30
32
|
const markdownFiles = [];
|
|
@@ -37,7 +39,7 @@ export function runValidate(argv) {
|
|
|
37
39
|
if (path.includes('/node_modules') || path === 'node_modules') return true;
|
|
38
40
|
if (path.includes('/.git') || path === '.git') return true;
|
|
39
41
|
if (path === CACHE_DIR || path.startsWith(`${CACHE_DIR}/`)) return true;
|
|
40
|
-
if (!options.includeLocalMemory && (path
|
|
42
|
+
if (!options.includeLocalMemory && (resolveMemoryArea(path, memoryConfig)?.scope === 'local')) return true;
|
|
41
43
|
return false;
|
|
42
44
|
};
|
|
43
45
|
const walk = (dir) => {
|
|
@@ -55,7 +57,6 @@ export function runValidate(argv) {
|
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
if (!existsSync(docs)) usage(`docs directory not found: ${docs}`);
|
|
58
|
-
const memoryConfig = readMemoryConfig(root);
|
|
59
60
|
const validationPolicy = cloneValidationPolicy(memoryConfig.validation ?? DEFAULT_VALIDATION_POLICY);
|
|
60
61
|
const maxLines = options.maxLines ?? validationPolicy.markdown.maxLines;
|
|
61
62
|
const maxChars = options.maxChars ?? validationPolicy.markdown.maxChars;
|
|
@@ -78,10 +79,10 @@ export function runValidate(argv) {
|
|
|
78
79
|
(memoryConfig.areas ?? [])
|
|
79
80
|
.filter(area => area.scope === 'shared')
|
|
80
81
|
.flatMap(area => area.paths ?? [])
|
|
81
|
-
.map(p => { const
|
|
82
|
+
.map(p => { const prefix = `${documentationRoot}/`; return p.startsWith(prefix) && p.endsWith('/**') ? p.slice(prefix.length, -3) : null; })
|
|
82
83
|
.filter(Boolean)
|
|
83
84
|
);
|
|
84
|
-
const filenameQualityScope = [...FILENAME_QUALITY_AREAS].sort().map(area =>
|
|
85
|
+
const filenameQualityScope = [...FILENAME_QUALITY_AREAS].sort().map(area => `${documentationRoot}/${area}`).join(', ') || 'configured shared documentation areas';
|
|
85
86
|
const FILENAME_QUALITY_PROMPT =
|
|
86
87
|
'Filenames and paths are LLM/agent context signals used for impact ranking, search, and retrieval.\n' +
|
|
87
88
|
`Use meaning-based filenames in ${filenameQualityScope}. A filename should identify its primary endpoint, screen, policy, state, response type, or domain subject. ` +
|
|
@@ -191,15 +192,15 @@ export function runValidate(argv) {
|
|
|
191
192
|
if (!entry.isDirectory()) continue;
|
|
192
193
|
if (!isKebabCase(entry.name)) addError(join(areaRoot, entry.name), 'SLUG_NAMING', `Archive/plan/report slug must be kebab-case: ${entry.name}`, null, 'rename the task/report directory to kebab-case and update any README index links that reference it.');
|
|
193
194
|
const readme = join(areaRoot, entry.name, 'README.md');
|
|
194
|
-
if (!existsSync(readme)) addError(readme, 'MISSING_README', `Expected README.md in
|
|
195
|
+
if (!existsSync(readme)) addError(readme, 'MISSING_README', `Expected README.md in ${documentationRoot}/${area}/${entry.name}/`, null, 'add a README.md that summarizes the task/report and links any supporting files.');
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
const gitignore = join(root, '.gitignore');
|
|
199
|
-
if (!existsSync(gitignore)) addError(gitignore, 'MISSING_GITIGNORE', 'Expected .gitignore', null,
|
|
200
|
+
if (!existsSync(gitignore)) addError(gitignore, 'MISSING_GITIGNORE', 'Expected .gitignore', null, `create .gitignore and include ${documentationRoot}/plan, ${documentationRoot}/archive, and .dotdotgod entries.`);
|
|
200
201
|
else {
|
|
201
202
|
const content = readFileSync(gitignore, 'utf8').split('\n').map((line) => line.trim());
|
|
202
|
-
for (const required of [
|
|
203
|
+
for (const required of [`${documentationRoot}/plan`, `${documentationRoot}/archive`, CACHE_DIR]) if (!content.includes(required)) addError(gitignore, 'MISSING_GITIGNORE_ENTRY', `Expected .gitignore entry: ${required}`, null, `add ${required} to .gitignore so local plans, archives, and cache files stay untracked.`);
|
|
203
204
|
}
|
|
204
205
|
if (options.json) console.log(JSON.stringify({ ok: errors.length === 0, errors, warnings }, null, 2));
|
|
205
206
|
else {
|