dotmd-cli 0.71.4 → 0.72.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/bin/dotmd.mjs +19 -1
- package/dotmd.config.example.mjs +8 -0
- package/package.json +1 -1
- package/src/commands.mjs +1 -1
- package/src/config.mjs +15 -0
- package/src/query.mjs +22 -3
- package/src/render.mjs +2 -1
- package/src/validate.mjs +31 -0
package/bin/dotmd.mjs
CHANGED
|
@@ -526,6 +526,10 @@ Options:
|
|
|
526
526
|
--errors-only Show only errors, suppress warnings entirely
|
|
527
527
|
--fix Auto-fix broken refs, lint issues, and regenerate index
|
|
528
528
|
--json Output errors and warnings as JSON (always full detail)
|
|
529
|
+
--min-docs <n> Fail if fewer than <n> docs were scanned — a floor that
|
|
530
|
+
tells "nothing is wrong" apart from "nothing was looked
|
|
531
|
+
at". Overrides \`minDocs\` in config for this run;
|
|
532
|
+
skipped when checking specific paths.
|
|
529
533
|
--dry-run, -n Preview fixes without writing (with --fix)`,
|
|
530
534
|
|
|
531
535
|
archive: `dotmd archive <file-or-slug> — archive a document
|
|
@@ -1760,7 +1764,19 @@ async function main() {
|
|
|
1760
1764
|
const errorsOnly = args.includes('--errors-only');
|
|
1761
1765
|
const noCollapse = args.includes('--no-collapse');
|
|
1762
1766
|
const verbose = args.includes('--verbose');
|
|
1763
|
-
|
|
1767
|
+
// `--min-docs N` overrides the configured floor for this run (CI passes it
|
|
1768
|
+
// without editing config). Its value is not a path target.
|
|
1769
|
+
const minDocsFlagIdx = restArgs.indexOf('--min-docs');
|
|
1770
|
+
const minDocsRaw = minDocsFlagIdx === -1 ? null : restArgs[minDocsFlagIdx + 1];
|
|
1771
|
+
if (minDocsFlagIdx !== -1 && !/^\d+$/.test(minDocsRaw ?? '')) {
|
|
1772
|
+
die('`--min-docs` needs a positive integer, e.g. `dotmd check --min-docs 500`.');
|
|
1773
|
+
}
|
|
1774
|
+
const minDocsOverride = minDocsRaw == null ? null : Number(minDocsRaw);
|
|
1775
|
+
const minDocsValueIdx = minDocsFlagIdx === -1 ? -1 : minDocsFlagIdx + 1;
|
|
1776
|
+
const checkTargets = restArgs.filter((arg, i) => !arg.startsWith('-') && i !== minDocsValueIdx);
|
|
1777
|
+
const { applyScanFloor } = await import('../src/validate.mjs');
|
|
1778
|
+
const scanFloorConfig = minDocsOverride == null ? config : { ...config, minDocs: minDocsOverride };
|
|
1779
|
+
const applyFloor = (idx) => applyScanFloor(idx, scanFloorConfig, { scoped: checkTargets.length > 0 });
|
|
1764
1780
|
const skippedCheckHooks = config._execution?.suppressSideEffects
|
|
1765
1781
|
? ['validate', 'transformDoc', 'formatSnapshot', 'renderCheck']
|
|
1766
1782
|
.filter(name => typeof config.hooks?.[name] === 'function')
|
|
@@ -1810,6 +1826,7 @@ async function main() {
|
|
|
1810
1826
|
const freshIndex = buildIndex(config);
|
|
1811
1827
|
applyIndexFilters(freshIndex);
|
|
1812
1828
|
applyPathScopeToIndex(freshIndex, config, checkTargets);
|
|
1829
|
+
applyFloor(freshIndex);
|
|
1813
1830
|
if (args.includes('--json')) {
|
|
1814
1831
|
process.stdout.write(JSON.stringify(checkJson(freshIndex), null, 2) + '\n');
|
|
1815
1832
|
} else {
|
|
@@ -1821,6 +1838,7 @@ async function main() {
|
|
|
1821
1838
|
}
|
|
1822
1839
|
|
|
1823
1840
|
applyPathScopeToIndex(index, config, checkTargets);
|
|
1841
|
+
applyFloor(index);
|
|
1824
1842
|
|
|
1825
1843
|
if (args.includes('--json')) {
|
|
1826
1844
|
process.stdout.write(JSON.stringify(checkJson(index), null, 2) + '\n');
|
package/dotmd.config.example.mjs
CHANGED
|
@@ -13,6 +13,14 @@ export const archiveDir = 'archived';
|
|
|
13
13
|
// Directories to skip when scanning
|
|
14
14
|
export const excludeDirs = ['evidence'];
|
|
15
15
|
|
|
16
|
+
// Floor under the scan surface. `dotmd check` fails when it scans fewer docs than
|
|
17
|
+
// this, so a broken root or an over-eager exclude can't read as a clean estate —
|
|
18
|
+
// zero errors and zero docs look identical otherwise. Off when unset. Set it well
|
|
19
|
+
// below your real count (round down hard); raise it as the corpus grows.
|
|
20
|
+
// Override for one run with `dotmd check --min-docs <n>`; skipped for path-scoped
|
|
21
|
+
// checks, which are deliberate subsets.
|
|
22
|
+
// export const minDocs = 500;
|
|
23
|
+
|
|
16
24
|
// Document types — each type has its own status vocabulary and context layout.
|
|
17
25
|
// Defaults: plan, doc, prompt. Override to customize statuses per type, or add new types.
|
|
18
26
|
//
|
package/package.json
CHANGED
package/src/commands.mjs
CHANGED
|
@@ -142,7 +142,7 @@ const definitions = [
|
|
|
142
142
|
form('migrate <type>', { subcommands: ['migrate'], args: positionals(1, 1), options: [flag('--yes', '-y'), flag('--json'), flag('--ignore-lifecycle-override')] }),
|
|
143
143
|
form('', { options: [value('--type'), flag('--json')] }),
|
|
144
144
|
]),
|
|
145
|
-
command('check', mutates('managed fix sweeps and repo-generated index; otherwise validation'), 'mutate', [form('[paths...]', { args: positionals(0, Infinity), options: [flag('--fix'), flag('--errors-only'), flag('--no-collapse'), flag('--json'), flag('--verbose')] })]),
|
|
145
|
+
command('check', mutates('managed fix sweeps and repo-generated index; otherwise validation'), 'mutate', [form('[paths...]', { args: positionals(0, Infinity), options: [flag('--fix'), flag('--errors-only'), flag('--no-collapse'), flag('--json'), flag('--verbose'), value('--min-docs')] })]),
|
|
146
146
|
command('index', mutates('repo-generated index destination; --print is read-only'), 'mutate', [form('', { options: [flag('--print')] })]),
|
|
147
147
|
|
|
148
148
|
command('self-check', none, 'internal', [form('', { options: [flag('--json')] })], { visibility: 'internal' }),
|
package/src/config.mjs
CHANGED
|
@@ -20,6 +20,8 @@ const DEFAULTS = {
|
|
|
20
20
|
root: '.',
|
|
21
21
|
archiveDir: 'archived',
|
|
22
22
|
excludeDirs: [],
|
|
23
|
+
// Floor under the scan surface; null = off. See `applyScanFloor` in validate.mjs.
|
|
24
|
+
minDocs: null,
|
|
23
25
|
|
|
24
26
|
types: {
|
|
25
27
|
plan: {
|
|
@@ -517,6 +519,18 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
517
519
|
}
|
|
518
520
|
}
|
|
519
521
|
|
|
522
|
+
// A floor under the scan surface. Unset (the default) is off, so no existing
|
|
523
|
+
// repo changes behavior. See `applyScanFloor` in src/validate.mjs for why a
|
|
524
|
+
// silent zero-doc pass is the failure mode worth spending a config key on.
|
|
525
|
+
let minDocs = null;
|
|
526
|
+
if (config.minDocs !== undefined && config.minDocs !== null) {
|
|
527
|
+
if (!Number.isInteger(config.minDocs) || config.minDocs < 1) {
|
|
528
|
+
earlyWarnings.push(`Config: minDocs must be a positive integer; ignoring \`${config.minDocs}\`.`);
|
|
529
|
+
} else {
|
|
530
|
+
minDocs = config.minDocs;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
520
534
|
const configWarnings = [...earlyWarnings, ...validateConfig(userConfig, config, validStatuses, indexPath)];
|
|
521
535
|
|
|
522
536
|
return {
|
|
@@ -531,6 +545,7 @@ export async function resolveConfig(cwd, explicitConfigPath) {
|
|
|
531
545
|
archiveDir: config.archiveDir,
|
|
532
546
|
excludeDirs: new Set(config.excludeDirs),
|
|
533
547
|
docsRootPrefix,
|
|
548
|
+
minDocs,
|
|
534
549
|
|
|
535
550
|
statusOrder,
|
|
536
551
|
validStatuses,
|
package/src/query.mjs
CHANGED
|
@@ -396,14 +396,33 @@ function scanBodyForKeyword(doc, needle, config) {
|
|
|
396
396
|
const lines = body.split('\n');
|
|
397
397
|
const matches = [];
|
|
398
398
|
for (let i = 0; i < lines.length && matches.length < MAX_BODY_MATCHES; i++) {
|
|
399
|
-
const
|
|
400
|
-
const at =
|
|
399
|
+
const raw = lines[i].trim();
|
|
400
|
+
const at = raw.toLowerCase().indexOf(needle);
|
|
401
401
|
if (at === -1) continue;
|
|
402
|
-
|
|
402
|
+
const { text, at: shown } = displayableExcerptLine(raw, at, needle);
|
|
403
|
+
matches.push({ line: bodyLineOffset + i + 1, text: excerptAround(text, shown, needle.length) });
|
|
403
404
|
}
|
|
404
405
|
return matches;
|
|
405
406
|
}
|
|
406
407
|
|
|
408
|
+
// HTML comments are invisible when the markdown renders, so they are noise in an
|
|
409
|
+
// excerpt — and dotmd's own conventions put them inline (a managed status token in
|
|
410
|
+
// a hub table row reads as `| [x](x.md) | <!--s-->active<!--/s--> — next |`), which
|
|
411
|
+
// is paid on every agent read of every search result.
|
|
412
|
+
//
|
|
413
|
+
// Matching still happens against the raw line, so a needle that only occurs INSIDE
|
|
414
|
+
// a comment is never lost: if stripping would hide the match, the raw line is shown
|
|
415
|
+
// instead. Whitespace is squeezed only on lines a strip actually touched.
|
|
416
|
+
const HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
417
|
+
|
|
418
|
+
export function displayableExcerptLine(raw, at, needle) {
|
|
419
|
+
if (!raw.includes('<!--')) return { text: raw, at };
|
|
420
|
+
const stripped = raw.replace(HTML_COMMENT_RE, '').replace(/[ \t]{2,}/g, ' ').trim();
|
|
421
|
+
const shown = stripped.toLowerCase().indexOf(needle);
|
|
422
|
+
if (shown === -1) return { text: raw, at };
|
|
423
|
+
return { text: stripped, at: shown };
|
|
424
|
+
}
|
|
425
|
+
|
|
407
426
|
// Window a long line around the match so the needle is always visible.
|
|
408
427
|
function excerptAround(text, at, needleLen) {
|
|
409
428
|
if (text.length <= EXCERPT_WIDTH) return text;
|
package/src/render.mjs
CHANGED
|
@@ -501,7 +501,8 @@ function _renderCheck(index, opts = {}) {
|
|
|
501
501
|
if (index.errors.length > 0) {
|
|
502
502
|
lines.push(red('Errors'));
|
|
503
503
|
for (const issue of index.errors) {
|
|
504
|
-
|
|
504
|
+
// Repo-level findings (e.g. the scan floor) carry no path.
|
|
505
|
+
lines.push(issue.path ? `- ${issue.path}: ${issue.message}` : `- ${issue.message}`);
|
|
505
506
|
}
|
|
506
507
|
lines.push('');
|
|
507
508
|
const actions = renderManualFixes({ errors: index.errors, warnings: [] }).trimEnd();
|
package/src/validate.mjs
CHANGED
|
@@ -703,3 +703,34 @@ export function computeChecklistCompletionRate(checklist) {
|
|
|
703
703
|
if (!checklist.total) return null;
|
|
704
704
|
return Number((checklist.completed / checklist.total).toFixed(4));
|
|
705
705
|
}
|
|
706
|
+
|
|
707
|
+
// A floor under the scan surface.
|
|
708
|
+
//
|
|
709
|
+
// Every other check in this file asks "is this doc wrong?" — none of them can ask
|
|
710
|
+
// "did we look at anything?". If the scan surface breaks (a root that stopped
|
|
711
|
+
// resolving, a config edit that narrowed the tree, a rename that moved docs/ out
|
|
712
|
+
// from under us), `dotmd check` reports zero errors, and that output is
|
|
713
|
+
// byte-identical to a clean estate. A guard's whole failure mode is going quiet,
|
|
714
|
+
// and this is the one that would go quiet silently.
|
|
715
|
+
//
|
|
716
|
+
// Off unless `minDocs` is configured — a floor is a claim about YOUR corpus size
|
|
717
|
+
// and dotmd cannot guess it. Deliberately an error, not a warning: a warning exits
|
|
718
|
+
// 0, which is the exact outcome this exists to prevent.
|
|
719
|
+
export function applyScanFloor(index, config, { scoped = false } = {}) {
|
|
720
|
+
const floor = config?.minDocs;
|
|
721
|
+
if (!floor) return index;
|
|
722
|
+
// A path-scoped check (`dotmd check docs/plans/x.md`) is a deliberate subset, so
|
|
723
|
+
// the floor would fire on every single-file check. Not a corpus claim at all.
|
|
724
|
+
if (scoped) return index;
|
|
725
|
+
if (index.docs.length >= floor) return index;
|
|
726
|
+
|
|
727
|
+
index.errors.push({
|
|
728
|
+
path: null,
|
|
729
|
+
level: 'error',
|
|
730
|
+
message: `only ${index.docs.length} docs in the scan surface (expected at least ${floor}) — `
|
|
731
|
+
+ 'the file list broke, so a pass here would mean nothing. Check `root`/`excludeDirs` in '
|
|
732
|
+
+ 'your config, or lower `minDocs` if the corpus genuinely shrank.',
|
|
733
|
+
meta: { kind: 'scan-floor', found: index.docs.length, floor },
|
|
734
|
+
});
|
|
735
|
+
return index;
|
|
736
|
+
}
|