@dotdotgod/cli 0.1.50 → 0.2.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/core.mjs +1 -1
- package/src/docs/markdown.mjs +40 -0
- package/src/memory/config.mjs +28 -1
- package/src/validate/run.mjs +60 -6
package/package.json
CHANGED
package/src/core.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { runTrelloSync } from './trello/sync.mjs';
|
|
|
13
13
|
export { CACHE_VERSION } from './index/constants.mjs';
|
|
14
14
|
export { commandUsage, hasHelpToken, helpCommandFromArgs, isHelpToken, isVersionToken, parseCommon, printVersion, usage } from './cli/usage.mjs';
|
|
15
15
|
export { rel } from './common/paths.mjs';
|
|
16
|
-
export { extractAnchors, extractLinks, headingToAnchor, isKebabCase, isUpperSnakeMarkdown, removeCodeBlocks } from './docs/markdown.mjs';
|
|
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
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, memoryRoleForPath, normalizeLowSignalTerm, readMemoryConfig, requiresTraceability, resolveMemoryArea, retrievalPriorityForPath, trelloSyncPaths, validateMemoryConfigData } from './memory/config.mjs';
|
|
19
19
|
export { buildCommunities, relationWeight } from './graph/communities.mjs';
|
package/src/docs/markdown.mjs
CHANGED
|
@@ -34,6 +34,46 @@ export function headingToAnchor(text) {
|
|
|
34
34
|
.replace(/\s+/g, '-');
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export function isNumberedSeriesFilename(name, siblingNames) {
|
|
38
|
+
const stem = name.replace(/\.md$/, '');
|
|
39
|
+
const parts = stem.split('_');
|
|
40
|
+
if (parts.length < 2) return false;
|
|
41
|
+
const lastSeg = parts[parts.length - 1];
|
|
42
|
+
const firstSeg = parts[0];
|
|
43
|
+
if (/^\d+$/.test(lastSeg)) {
|
|
44
|
+
const prefix = parts.slice(0, -1).join('_');
|
|
45
|
+
if (siblingNames.some(sib => {
|
|
46
|
+
if (sib === name) return false;
|
|
47
|
+
const s = sib.replace(/\.md$/, '').split('_');
|
|
48
|
+
return s.length >= 2 && /^\d+$/.test(s[s.length - 1]) && s.slice(0, -1).join('_') === prefix;
|
|
49
|
+
})) return true;
|
|
50
|
+
}
|
|
51
|
+
if (/^\d+$/.test(firstSeg)) {
|
|
52
|
+
const suffix = parts.slice(1).join('_');
|
|
53
|
+
if (siblingNames.some(sib => {
|
|
54
|
+
if (sib === name) return false;
|
|
55
|
+
const s = sib.replace(/\.md$/, '').split('_');
|
|
56
|
+
return s.length >= 2 && /^\d+$/.test(s[0]) && s.slice(1).join('_') === suffix;
|
|
57
|
+
})) return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function extractFirstHeading(content) {
|
|
63
|
+
const match = content.match(/^#+\s+(.+)$/m);
|
|
64
|
+
return match ? match[1].replace(/[`*_]/g, '').trim() : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function suggestFilenameFromHeading(heading) {
|
|
68
|
+
if (!heading) return undefined;
|
|
69
|
+
const stem = heading
|
|
70
|
+
.replace(/[^A-Za-z0-9\s]/g, '')
|
|
71
|
+
.trim()
|
|
72
|
+
.replace(/\s+/g, '_')
|
|
73
|
+
.toUpperCase();
|
|
74
|
+
return stem.length >= 3 ? `${stem}.md` : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
37
77
|
export function extractAnchors(content) {
|
|
38
78
|
const anchors = new Set();
|
|
39
79
|
const seen = new Map();
|
package/src/memory/config.mjs
CHANGED
|
@@ -11,7 +11,15 @@ export const DEFAULT_TRACEABILITY_POLICY = {
|
|
|
11
11
|
exclude: ['**/README.md'],
|
|
12
12
|
};
|
|
13
13
|
export const DEFAULT_VALIDATION_POLICY = {
|
|
14
|
-
markdown: {
|
|
14
|
+
markdown: {
|
|
15
|
+
maxLines: 200,
|
|
16
|
+
maxChars: 10000,
|
|
17
|
+
exclude: [],
|
|
18
|
+
filename: {
|
|
19
|
+
warnNumberedSeries: true,
|
|
20
|
+
allow: [],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
15
23
|
};
|
|
16
24
|
export const DEFAULT_INTEGRATIONS_POLICY = {
|
|
17
25
|
trello: { syncPaths: [] },
|
|
@@ -101,11 +109,15 @@ export function cloneTraceabilityPolicy(policy = DEFAULT_TRACEABILITY_POLICY) {
|
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
export function cloneValidationPolicy(policy = DEFAULT_VALIDATION_POLICY) {
|
|
112
|
+
const filename = policy.markdown?.filename;
|
|
104
113
|
return {
|
|
105
114
|
markdown: {
|
|
106
115
|
maxLines: policy.markdown?.maxLines ?? DEFAULT_VALIDATION_POLICY.markdown.maxLines,
|
|
107
116
|
maxChars: policy.markdown?.maxChars ?? DEFAULT_VALIDATION_POLICY.markdown.maxChars,
|
|
108
117
|
exclude: [...(policy.markdown?.exclude ?? [])],
|
|
118
|
+
filename: filename
|
|
119
|
+
? { warnNumberedSeries: filename.warnNumberedSeries ?? true, allow: [...(filename.allow ?? [])] }
|
|
120
|
+
: { ...DEFAULT_VALIDATION_POLICY.markdown.filename, allow: [] },
|
|
109
121
|
},
|
|
110
122
|
};
|
|
111
123
|
}
|
|
@@ -221,11 +233,16 @@ function normalizeTraceabilityPolicy(raw) {
|
|
|
221
233
|
function normalizeValidationPolicy(raw) {
|
|
222
234
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return cloneValidationPolicy();
|
|
223
235
|
const markdown = raw.markdown && typeof raw.markdown === 'object' && !Array.isArray(raw.markdown) ? raw.markdown : {};
|
|
236
|
+
const filename = markdown.filename && typeof markdown.filename === 'object' && !Array.isArray(markdown.filename) ? markdown.filename : undefined;
|
|
224
237
|
return cloneValidationPolicy({
|
|
225
238
|
markdown: {
|
|
226
239
|
maxLines: Number.isInteger(markdown.maxLines) ? markdown.maxLines : DEFAULT_VALIDATION_POLICY.markdown.maxLines,
|
|
227
240
|
maxChars: Number.isInteger(markdown.maxChars) ? markdown.maxChars : DEFAULT_VALIDATION_POLICY.markdown.maxChars,
|
|
228
241
|
exclude: Array.isArray(markdown.exclude) ? markdown.exclude.map(normalizePathPattern) : [],
|
|
242
|
+
filename: filename ? {
|
|
243
|
+
warnNumberedSeries: typeof filename.warnNumberedSeries === 'boolean' ? filename.warnNumberedSeries : true,
|
|
244
|
+
allow: Array.isArray(filename.allow) ? filename.allow.map(normalizePathPattern) : [],
|
|
245
|
+
} : undefined,
|
|
229
246
|
},
|
|
230
247
|
});
|
|
231
248
|
}
|
|
@@ -314,6 +331,16 @@ export function validateMemoryConfigData(data, root = '.', file = 'dotdotgod.con
|
|
|
314
331
|
if (markdown.maxChars !== undefined && (!Number.isInteger(markdown.maxChars) || markdown.maxChars < 1)) add('VALIDATION_CONFIG_INVALID_MAX_CHARS', 'validation.markdown.maxChars', 'Expected a positive integer.');
|
|
315
332
|
if (markdown.exclude !== undefined && !Array.isArray(markdown.exclude)) add('VALIDATION_CONFIG_INVALID_EXCLUDE', 'validation.markdown.exclude', 'Expected an array of path strings.');
|
|
316
333
|
else if (Array.isArray(markdown.exclude) && markdown.exclude.some((value) => !isValidPathPattern(value))) add('VALIDATION_CONFIG_INVALID_EXCLUDE', 'validation.markdown.exclude', 'Expected path strings using exact paths, /** subtree patterns, or **/suffix patterns.');
|
|
334
|
+
if (markdown.filename !== undefined) {
|
|
335
|
+
const fn = markdown.filename;
|
|
336
|
+
if (!fn || typeof fn !== 'object' || Array.isArray(fn)) {
|
|
337
|
+
add('VALIDATION_CONFIG_INVALID_FILENAME', 'validation.markdown.filename', 'Expected an object.');
|
|
338
|
+
} else {
|
|
339
|
+
if (fn.warnNumberedSeries !== undefined && typeof fn.warnNumberedSeries !== 'boolean') add('VALIDATION_CONFIG_INVALID_FILENAME', 'validation.markdown.filename.warnNumberedSeries', 'Expected a boolean.');
|
|
340
|
+
if (fn.allow !== undefined && !Array.isArray(fn.allow)) add('VALIDATION_CONFIG_INVALID_FILENAME_ALLOW', 'validation.markdown.filename.allow', 'Expected an array of path strings.');
|
|
341
|
+
else if (Array.isArray(fn.allow) && fn.allow.some((value) => !isValidPathPattern(value))) add('VALIDATION_CONFIG_INVALID_FILENAME_ALLOW', 'validation.markdown.filename.allow', 'Expected path strings using exact paths, /** subtree patterns, or **/suffix patterns.');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
317
344
|
}
|
|
318
345
|
}
|
|
319
346
|
}
|
package/src/validate/run.mjs
CHANGED
|
@@ -2,9 +2,9 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
|
2
2
|
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
3
3
|
import { usage } from '../cli/usage.mjs';
|
|
4
4
|
import { rel } from '../common/paths.mjs';
|
|
5
|
-
import { extractAnchors, extractLinks, isKebabCase, isUpperSnakeMarkdown } from '../docs/markdown.mjs';
|
|
5
|
+
import { extractAnchors, extractFirstHeading, extractLinks, isKebabCase, isNumberedSeriesFilename, isUpperSnakeMarkdown, suggestFilenameFromHeading } from '../docs/markdown.mjs';
|
|
6
6
|
import { extractDotdotgodTraceabilityBlocks, stripTraceabilityLinksRegion, syncTraceabilityLinksInContent, traceabilityExample, validateTraceabilityBlock, validateTraceabilityLinksRegion, validateTraceabilityPlacement } from '../docs/traceability.mjs';
|
|
7
|
-
import { DEFAULT_VALIDATION_POLICY, cloneValidationPolicy, isMarkdownSizeExcluded, readMemoryConfig, requiresTraceability, resolveMemoryArea } from '../memory/config.mjs';
|
|
7
|
+
import { DEFAULT_VALIDATION_POLICY, cloneValidationPolicy, isMarkdownSizeExcluded, matchPathPattern, readMemoryConfig, requiresTraceability, resolveMemoryArea } from '../memory/config.mjs';
|
|
8
8
|
import { cacheFile, collectIndexFiles, fingerprint } from '../index/files.mjs';
|
|
9
9
|
import { CACHE_DIR, CACHE_VERSION } from '../index/constants.mjs';
|
|
10
10
|
import { readIndex } from '../index/cache.mjs';
|
|
@@ -26,9 +26,11 @@ export function runValidate(argv) {
|
|
|
26
26
|
const root = resolve(options.root);
|
|
27
27
|
const docs = join(root, 'docs');
|
|
28
28
|
const errors = [];
|
|
29
|
+
const warnings = [];
|
|
29
30
|
const markdownFiles = [];
|
|
30
31
|
const fileCache = new Map();
|
|
31
32
|
const addError = (file, code, message, line = null, fix = null, prompt = null) => errors.push({ file: rel(root, file), line, code, message: fix ? `${message}\nFix: ${fix}` : message, ...(prompt ? { prompt } : {}) });
|
|
33
|
+
const addWarning = (file, code, message, metadata = {}) => warnings.push({ file: rel(root, file), code, message, ...metadata });
|
|
32
34
|
const shouldSkipDir = (dir) => {
|
|
33
35
|
const path = rel(root, dir);
|
|
34
36
|
if (!path || path === '.') return false;
|
|
@@ -66,7 +68,32 @@ export function runValidate(argv) {
|
|
|
66
68
|
return `Split ${path} into focused documents by documentation area and role. Keep the current ${areaText} area semantics and ${roleText} role intact, move role-specific detail into smaller UPPER_SNAKE_CASE markdown files, and update the nearest README.md index with each new file's purpose.`;
|
|
67
69
|
};
|
|
68
70
|
for (const error of memoryConfig.errors ?? []) errors.push(error);
|
|
71
|
+
const FILENAME_QUALITY_AREAS = new Set(
|
|
72
|
+
(memoryConfig.areas ?? [])
|
|
73
|
+
.filter(area => area.scope === 'shared')
|
|
74
|
+
.flatMap(area => area.paths ?? [])
|
|
75
|
+
.map(p => { const m = p.match(/^docs\/([^/*]+)\/\*\*$/); return m ? m[1] : null; })
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
);
|
|
78
|
+
const FILENAME_QUALITY_PROMPT =
|
|
79
|
+
'Background: Filenames and paths are LLM/agent context signals used for impact ranking, search, and retrieval. ' +
|
|
80
|
+
'Abstract or sequentially numbered filenames carry low information signal.\n' +
|
|
81
|
+
'Naming principles:\n' +
|
|
82
|
+
'1. Filenames in docs/spec, docs/arch, docs/test should be meaning-based wherever possible.\n' +
|
|
83
|
+
'2. The filename alone should let you infer the primary subject: endpoint, screen, policy, state, or response type.\n' +
|
|
84
|
+
'3. When splitting documents, derive names from headings, API paths, or domain terms rather than sequence numbers or abstract words.\n' +
|
|
85
|
+
'Avoid - numbered splits: API_1.md, API_2.md, API_3_1_*.md, 01_*.md, *_01.md\n' +
|
|
86
|
+
'Avoid - overly abstract: COMMON.md, OVERVIEW.md, DETAILS.md, NOTES.md, MISC.md, PART_1.md, SECTION_1.md\n' +
|
|
87
|
+
'Acceptable exceptions: README.md (directory index), files with clear HTTP status code semantics (e.g. RESPONSE_200_OK.md), ' +
|
|
88
|
+
'files in the project validation.markdown.filename.allow allowlist.\n' +
|
|
89
|
+
'Good renames: API_1.md → BIZ_RESERVATIONS_SCHEDULE.md, COMMON.md → BIZ_AUTH_ERROR_CONVENTIONS.md, OVERVIEW.md → REGISTRATION_HANDOFF_SUMMARY.md';
|
|
69
90
|
walk(docs);
|
|
91
|
+
const dirSiblingMap = new Map();
|
|
92
|
+
for (const f of markdownFiles) {
|
|
93
|
+
const dir = dirname(f);
|
|
94
|
+
if (!dirSiblingMap.has(dir)) dirSiblingMap.set(dir, []);
|
|
95
|
+
dirSiblingMap.get(dir).push(basename(f));
|
|
96
|
+
}
|
|
70
97
|
for (const file of markdownFiles) {
|
|
71
98
|
const name = basename(file);
|
|
72
99
|
const docsRel = rel(docs, file);
|
|
@@ -75,6 +102,23 @@ export function runValidate(argv) {
|
|
|
75
102
|
if (docsRel && !docsRel.startsWith('..') && !isPlanInternalWorkspaceFile && !isUpperSnakeMarkdown(name)) addError(file, 'FILE_NAMING', `Markdown file must be UPPER_SNAKE_CASE.md or README.md: ${name}`, null, 'rename the markdown file to UPPER_SNAKE_CASE.md or README.md and update any links that reference it.');
|
|
76
103
|
const content = readFileSync(file, 'utf8');
|
|
77
104
|
fileCache.set(file, content);
|
|
105
|
+
const docArea = docsRel?.split('/')[0];
|
|
106
|
+
if (name !== 'README.md' && !isPlanInternalWorkspaceFile
|
|
107
|
+
&& isUpperSnakeMarkdown(name) && FILENAME_QUALITY_AREAS.has(docArea)) {
|
|
108
|
+
const fp = validationPolicy.markdown?.filename ?? {};
|
|
109
|
+
const allow = fp.allow ?? [];
|
|
110
|
+
if (!allow.some((p) => matchPathPattern(rootRel, p)) && (fp.warnNumberedSeries ?? true)) {
|
|
111
|
+
const siblings = dirSiblingMap.get(dirname(file)) ?? [];
|
|
112
|
+
if (isNumberedSeriesFilename(name, siblings)) {
|
|
113
|
+
const heading = extractFirstHeading(content);
|
|
114
|
+
const suggestion = suggestFilenameFromHeading(heading);
|
|
115
|
+
addWarning(file, 'FILE_NAMING_NUMBERED',
|
|
116
|
+
`Numbered series filename: ${name}. Filenames are LLM context signals; use API path, domain, or subject instead of sequence numbers.`,
|
|
117
|
+
{ pattern: 'numbered-series', ...(heading ? { heading } : {}), ...(suggestion ? { suggestion } : {}), prompt: FILENAME_QUALITY_PROMPT }
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
78
122
|
const budgetContent = stripTraceabilityLinksRegion(content);
|
|
79
123
|
const lines = budgetContent.split('\n').length;
|
|
80
124
|
const chars = budgetContent.length;
|
|
@@ -159,11 +203,21 @@ export function runValidate(argv) {
|
|
|
159
203
|
const content = readFileSync(gitignore, 'utf8').split('\n').map((line) => line.trim());
|
|
160
204
|
for (const required of ['docs/plan', 'docs/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.`);
|
|
161
205
|
}
|
|
162
|
-
if (options.json) console.log(JSON.stringify({ ok: errors.length === 0, errors }, null, 2));
|
|
163
|
-
else if (errors.length === 0) console.log(`✅ docs validation passed (${markdownFiles.length} markdown files)`);
|
|
206
|
+
if (options.json) console.log(JSON.stringify({ ok: errors.length === 0, errors, warnings }, null, 2));
|
|
164
207
|
else {
|
|
165
|
-
for (const
|
|
166
|
-
|
|
208
|
+
for (const w of warnings) {
|
|
209
|
+
console.log(`⚠ ${w.file} [${w.code}] ${w.message}`);
|
|
210
|
+
if (w.heading) console.log(` Heading: "${w.heading}"`);
|
|
211
|
+
if (w.suggestion) console.log(` Suggestion: rename to ${w.suggestion} and update links`);
|
|
212
|
+
if (w.prompt) console.log(` Prompt: ${w.prompt}`);
|
|
213
|
+
}
|
|
214
|
+
if (errors.length === 0) {
|
|
215
|
+
const warnNote = warnings.length > 0 ? `, ${warnings.length} warning(s)` : '';
|
|
216
|
+
console.log(`✅ docs validation passed (${markdownFiles.length} markdown files${warnNote})`);
|
|
217
|
+
} else {
|
|
218
|
+
for (const error of errors) console.log(`${error.line ? `${error.file}:${error.line}` : error.file} [${error.code}] ${error.message}${error.prompt ? `\nPrompt: ${error.prompt}` : ''}`);
|
|
219
|
+
console.log(`\n❌ ${errors.length} docs validation error(s)`);
|
|
220
|
+
}
|
|
167
221
|
}
|
|
168
222
|
process.exit(errors.length === 0 ? 0 : 1);
|
|
169
223
|
}
|