@plebeiantech/lat.md 0.12.3-fork.17 → 0.12.3-fork.20
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 +1 -1
- package/dist/src/cli/agent-invocation.d.ts +6 -0
- package/dist/src/cli/agent-invocation.js +23 -0
- package/dist/src/cli/fork-instructions.js +1 -1
- package/dist/src/cli/fork-scaffold.js +2 -2
- package/dist/src/cli/graph.d.ts +1 -1
- package/dist/src/cli/hook.js +11 -11
- package/dist/src/cli/index.js +41 -59
- package/dist/src/cli/init-preferences.js +5 -4
- package/dist/src/cli/init.d.ts +1 -1
- package/dist/src/cli/init.js +54 -45
- package/dist/src/cli/reindex.d.ts +1 -1
- package/dist/src/cli/reindex.js +2 -2
- package/dist/src/cli/search.d.ts +4 -4
- package/dist/src/cli/search.js +41 -28
- package/dist/src/cli/ui-build-server.d.ts +1 -1
- package/dist/src/cli/ui-build.d.ts +1 -1
- package/dist/src/cli/ui.d.ts +1 -1
- package/dist/src/fork/antigravity-hook.js +6 -6
- package/dist/src/fork/antigravity-init.d.ts +2 -2
- package/dist/src/fork/antigravity-init.js +2 -2
- package/dist/src/graph-export.js +7 -7
- package/dist/src/knowledge/index.js +2 -2
- package/dist/src/mcp/server.js +8 -8
- package/dist/src/search/cache.d.ts +1 -1
- package/dist/src/search/cache.js +40 -7
- package/dist/src/search/chunks.d.ts +2 -2
- package/dist/src/search/db.d.ts +5 -4
- package/dist/src/search/db.js +36 -40
- package/dist/src/search/embedder.js +1 -1
- package/dist/src/search/index.d.ts +1 -1
- package/dist/src/search/index.js +2 -2
- package/dist/src/search/lock.d.ts +4 -1
- package/dist/src/search/lock.js +30 -8
- package/dist/src/search/query.d.ts +5 -2
- package/dist/src/search/query.js +55 -38
- package/dist/src/search/search.d.ts +3 -1
- package/dist/src/search/search.js +21 -12
- package/dist/src/search/types.d.ts +2 -18
- package/dist/src/view/client/assets/{GraphView-BWlRdQM4.js → GraphView-Dij8mAA_.js} +1 -1
- package/dist/src/view/client/assets/{MarkdownEditor-MW-Ck1O0.js → MarkdownEditor-88Wbt0M1.js} +1 -1
- package/dist/src/view/client/assets/{abnfDiagram-VCTEODGH-Bn3RshMV.js → abnfDiagram-VCTEODGH-y0DlYa8E.js} +1 -1
- package/dist/src/view/client/assets/{arc-En9tdFQF.js → arc-FlMu8dnO.js} +1 -1
- package/dist/src/view/client/assets/{architectureDiagram-5GKGNRK7-B-9J1VAM.js → architectureDiagram-5GKGNRK7-DA_uWZW1.js} +1 -1
- package/dist/src/view/client/assets/{blockDiagram-I7D4REHJ-QlznwalP.js → blockDiagram-I7D4REHJ-BHQ5LU85.js} +1 -1
- package/dist/src/view/client/assets/{c4Diagram-7LVT6UL2-Bp2y8B4_.js → c4Diagram-7LVT6UL2-BRucKCt2.js} +1 -1
- package/dist/src/view/client/assets/channel-BgDNA_Ay.js +1 -0
- package/dist/src/view/client/assets/{chunk-2Q5K7J3B-CnBKL-lm.js → chunk-2Q5K7J3B-BJPcFivt.js} +1 -1
- package/dist/src/view/client/assets/{chunk-5VM5RSS4-LLYMi_n6.js → chunk-5VM5RSS4-v2q99bGz.js} +1 -1
- package/dist/src/view/client/assets/{chunk-F27PBJKO-VIeO7VF0.js → chunk-F27PBJKO-CcHOuJCs.js} +1 -1
- package/dist/src/view/client/assets/{chunk-IMKFNOWR-D5i67DVD.js → chunk-IMKFNOWR-DNxYuJNq.js} +1 -1
- package/dist/src/view/client/assets/{chunk-JWPE2WC7-D4qAtoL3.js → chunk-JWPE2WC7-C0eHKvLW.js} +1 -1
- package/dist/src/view/client/assets/{chunk-POPQ4Y6H-BMzOyiW_.js → chunk-POPQ4Y6H-57CWp2Ve.js} +1 -1
- package/dist/src/view/client/assets/{chunk-SVP7TREG-DWy5nbfp.js → chunk-SVP7TREG-BqcoxIJw.js} +1 -1
- package/dist/src/view/client/assets/{chunk-TICWLB2K-CVqCANFB.js → chunk-TICWLB2K-DRUJR5zH.js} +1 -1
- package/dist/src/view/client/assets/{chunk-XXDRQBXY-Bp23h6Tf.js → chunk-XXDRQBXY-Dez0oRoM.js} +1 -1
- package/dist/src/view/client/assets/classDiagram-ZZMXUADV-CFZ_Sgh5.js +1 -0
- package/dist/src/view/client/assets/classDiagram-v2-VYDZK3BY-CFZ_Sgh5.js +1 -0
- package/dist/src/view/client/assets/{cose-bilkent-JH36ORCC-BCw7fCC2.js → cose-bilkent-JH36ORCC-BHtud6Hg.js} +1 -1
- package/dist/src/view/client/assets/{cynefin-OW5HDTMX--lYN4N9m.js → cynefin-OW5HDTMX-BjBW8VGk.js} +1 -1
- package/dist/src/view/client/assets/{cynefinDiagram-5FMLGOSQ-9sK5EV7v.js → cynefinDiagram-5FMLGOSQ-DwGuD7oF.js} +1 -1
- package/dist/src/view/client/assets/{dagre-GXQ25YYZ-CoMdEFCq.js → dagre-GXQ25YYZ-C5zSlGK9.js} +1 -1
- package/dist/src/view/client/assets/{diagram-S7CK7UJ4-D13dyWdd.js → diagram-S7CK7UJ4-C_NhA3bj.js} +1 -1
- package/dist/src/view/client/assets/{diagram-UQ7AKVKN-PdwLPPPA.js → diagram-UQ7AKVKN-az7QPYNb.js} +1 -1
- package/dist/src/view/client/assets/{diagram-VSXAHHWV-CtWWa_A7.js → diagram-VSXAHHWV-BRvDuM8_.js} +1 -1
- package/dist/src/view/client/assets/{diagram-VX7I27RA-BeCYcSU-.js → diagram-VX7I27RA-BWLgF6S-.js} +1 -1
- package/dist/src/view/client/assets/{diagram-Z3DM3KII-DiWabOBM.js → diagram-Z3DM3KII-DV3d6mL5.js} +1 -1
- package/dist/src/view/client/assets/{ebnfDiagram-PWID7BFC-CZQ3MD_K.js → ebnfDiagram-PWID7BFC-yUXrEAvK.js} +1 -1
- package/dist/src/view/client/assets/{erDiagram-RLTQ6QDP-B1AhXIrP.js → erDiagram-RLTQ6QDP-D35FN1Pt.js} +1 -1
- package/dist/src/view/client/assets/{flowDiagram-HODETNUW-Bhr0JM6P.js → flowDiagram-HODETNUW-BciZqsjT.js} +1 -1
- package/dist/src/view/client/assets/{ganttDiagram-EL5Y4UJY-CbP8iAkg.js → ganttDiagram-EL5Y4UJY-BysPGgXS.js} +1 -1
- package/dist/src/view/client/assets/{gitGraphDiagram-WWUBYQGX-B9uuGdLF.js → gitGraphDiagram-WWUBYQGX-C3DdJ5xG.js} +1 -1
- package/dist/src/view/client/assets/index-CJokoRSp.js +13 -0
- package/dist/src/view/client/assets/{infoDiagram-27XIBGKW-BrfwQ_Cq.js → infoDiagram-27XIBGKW-D3HZrDRb.js} +1 -1
- package/dist/src/view/client/assets/{ishikawaDiagram-5VMMS53U-CHtJ36re.js → ishikawaDiagram-5VMMS53U-BXyVIXTU.js} +1 -1
- package/dist/src/view/client/assets/{journeyDiagram-3NMN7TZE-BChcgX1s.js → journeyDiagram-3NMN7TZE-D4-1NxVU.js} +1 -1
- package/dist/src/view/client/assets/{kanban-definition-UXKFOSKX-BnwFsKiB.js → kanban-definition-UXKFOSKX-4Gu_5yBA.js} +1 -1
- package/dist/src/view/client/assets/{layout-Dnd0pLdb.js → layout-Jf1K3TMM.js} +1 -1
- package/dist/src/view/client/assets/{linear-CFhW2ozn.js → linear-DWfwzUT_.js} +1 -1
- package/dist/src/view/client/assets/{mermaid.core--OP6lXtW.js → mermaid.core-DMx_DAl8.js} +5 -5
- package/dist/src/view/client/assets/{mindmap-definition-YA3MSWOX-D51y2zbm.js → mindmap-definition-YA3MSWOX-8pxY_RWi.js} +1 -1
- package/dist/src/view/client/assets/{pegDiagram-XKGWAZYB--zyQJDvI.js → pegDiagram-XKGWAZYB-DNAw_O5t.js} +1 -1
- package/dist/src/view/client/assets/{pieDiagram-E7YTZNPT-Cj8mvpJy.js → pieDiagram-E7YTZNPT-uuvRCDkC.js} +1 -1
- package/dist/src/view/client/assets/{quadrantDiagram-AXDQQJYC-qvhJrnBf.js → quadrantDiagram-AXDQQJYC-BER2O4Aq.js} +1 -1
- package/dist/src/view/client/assets/{railroadDiagram-O6MQD6OU-CtWvxpSo.js → railroadDiagram-O6MQD6OU-CxEsJ_0n.js} +1 -1
- package/dist/src/view/client/assets/{requirementDiagram-BXWQKSXE-CBxen_KG.js → requirementDiagram-BXWQKSXE-yS9rspca.js} +1 -1
- package/dist/src/view/client/assets/{sankeyDiagram-P5KCCOFB-B_fUkmfc.js → sankeyDiagram-P5KCCOFB-Bqt70a2j.js} +1 -1
- package/dist/src/view/client/assets/{sequenceDiagram-WJ2MYXX4-Dclv4p68.js → sequenceDiagram-WJ2MYXX4-BvOOEJNr.js} +1 -1
- package/dist/src/view/client/assets/{sizeCapture-INFHLROL-GfG0sMTZ.js → sizeCapture-INFHLROL-CvO_Pjjo.js} +1 -1
- package/dist/src/view/client/assets/{stateDiagram-D77RDMKH-YQWPnzxU.js → stateDiagram-D77RDMKH-KLmVOZkq.js} +1 -1
- package/dist/src/view/client/assets/stateDiagram-v2-MP3YSRHH-KCS8Mdzu.js +1 -0
- package/dist/src/view/client/assets/{swimlanes-42K2YHIH-5yiCveVS.js → swimlanes-42K2YHIH-DC1r4_sq.js} +1 -1
- package/dist/src/view/client/assets/swimlanesDiagram-VR7AAH4N-B2TcAwis.js +8 -0
- package/dist/src/view/client/assets/{timeline-definition-24CTP7MA-BvAbB5Zs.js → timeline-definition-24CTP7MA-ChXLOuBL.js} +1 -1
- package/dist/src/view/client/assets/{vennDiagram-4TSXK5OY-guC-r60n.js → vennDiagram-4TSXK5OY-D9_h25-Y.js} +1 -1
- package/dist/src/view/client/assets/{wardleyDiagram-VM6X3IG4-C0yXcU2x.js → wardleyDiagram-VM6X3IG4-CsQJGGW0.js} +1 -1
- package/dist/src/view/client/assets/{xychartDiagram-S5SC5T6Z-RPmkPNfK.js → xychartDiagram-S5SC5T6Z-DGMuPMlm.js} +1 -1
- package/dist/src/view/client/index.html +1 -1
- package/dist/src/view/diagnostics.d.ts +2 -2
- package/dist/src/view/diagnostics.js +5 -5
- package/dist/src/view/external-document-tree.js +1 -1
- package/dist/src/view/git-diff.js +1 -1
- package/dist/src/view/git.js +1 -1
- package/dist/src/view/graph.d.ts +2 -2
- package/dist/src/view/graph.js +2 -2
- package/dist/src/view/markdown.js +1 -1
- package/dist/src/view/preindexed-search.d.ts +1 -1
- package/dist/src/view/protocol.d.ts +2 -18
- package/dist/src/view/publication.d.ts +2 -0
- package/dist/src/view/publication.js +78 -0
- package/dist/src/view/references.d.ts +3 -3
- package/dist/src/view/references.js +3 -3
- package/dist/src/view/repository.d.ts +6 -6
- package/dist/src/view/repository.js +28 -14
- package/dist/src/view/request-security.d.ts +5 -0
- package/dist/src/view/request-security.js +46 -0
- package/dist/src/view/search-result.d.ts +1 -1
- package/dist/src/view/search-result.js +1 -1
- package/dist/src/view/search.d.ts +1 -1
- package/dist/src/view/server-build.d.ts +2 -2
- package/dist/src/view/server-build.js +2 -2
- package/dist/src/view/server-deployment.d.ts +1 -2
- package/dist/src/view/server-deployment.js +6 -46
- package/dist/src/view/server-index-worker.d.ts +1 -1
- package/dist/src/view/server.d.ts +1 -1
- package/dist/src/view/server.js +13 -3
- package/dist/src/view/source-target.js +2 -2
- package/dist/src/view/static-build.d.ts +1 -1
- package/dist/src/view/static-build.js +49 -17
- package/dist/src/view/store.d.ts +4 -2
- package/dist/src/view/store.js +31 -19
- package/dist/src/view/table-of-contents.d.ts +1 -1
- package/dist/src/view/table-of-contents.js +1 -1
- package/dist/src/view/vercel-server-build.d.ts +1 -1
- package/package.json +19 -29
- package/templates/AGENTS.md +2 -2
- package/templates/cursor-rules.md +2 -2
- package/templates/opencode-plugin.ts +9 -9
- package/templates/pi-extension.ts +11 -11
- package/templates/skill/SKILL.md +2 -1
- package/dist/src/cli/check-context.d.ts +0 -56
- package/dist/src/cli/check-context.js +0 -215
- package/dist/src/cli/check-coverage.d.ts +0 -2
- package/dist/src/cli/check-coverage.js +0 -75
- package/dist/src/cli/check-frontmatter.d.ts +0 -12
- package/dist/src/cli/check-frontmatter.js +0 -44
- package/dist/src/cli/check-mode.d.ts +0 -21
- package/dist/src/cli/check-mode.js +0 -394
- package/dist/src/cli/check-status.d.ts +0 -74
- package/dist/src/cli/check-status.js +0 -217
- package/dist/src/cli/check.d.ts +0 -52
- package/dist/src/cli/check.js +0 -834
- package/dist/src/cli/context.d.ts +0 -11
- package/dist/src/cli/context.js +0 -58
- package/dist/src/cli/expand.d.ts +0 -7
- package/dist/src/cli/expand.js +0 -141
- package/dist/src/cli/external.d.ts +0 -11
- package/dist/src/cli/external.js +0 -185
- package/dist/src/cli/gen-index.d.ts +0 -45
- package/dist/src/cli/gen-index.js +0 -235
- package/dist/src/cli/link-scheme.d.ts +0 -54
- package/dist/src/cli/link-scheme.js +0 -101
- package/dist/src/cli/locate.d.ts +0 -2
- package/dist/src/cli/locate.js +0 -18
- package/dist/src/cli/refs.d.ts +0 -23
- package/dist/src/cli/refs.js +0 -274
- package/dist/src/cli/section.d.ts +0 -45
- package/dist/src/cli/section.js +0 -328
- package/dist/src/cli/select-menu.d.ts +0 -13
- package/dist/src/cli/select-menu.js +0 -100
- package/dist/src/code-refs.d.ts +0 -29
- package/dist/src/code-refs.js +0 -479
- package/dist/src/config.d.ts +0 -29
- package/dist/src/config.js +0 -83
- package/dist/src/context.d.ts +0 -26
- package/dist/src/context.js +0 -11
- package/dist/src/document-formats.d.ts +0 -7
- package/dist/src/document-formats.js +0 -39
- package/dist/src/extensions/alert-marker.d.ts +0 -25
- package/dist/src/extensions/alert-marker.js +0 -55
- package/dist/src/extensions/wiki-link/from-markdown.d.ts +0 -5
- package/dist/src/extensions/wiki-link/from-markdown.js +0 -34
- package/dist/src/extensions/wiki-link/index.d.ts +0 -4
- package/dist/src/extensions/wiki-link/index.js +0 -3
- package/dist/src/extensions/wiki-link/syntax.d.ts +0 -14
- package/dist/src/extensions/wiki-link/syntax.js +0 -121
- package/dist/src/extensions/wiki-link/to-markdown.d.ts +0 -5
- package/dist/src/extensions/wiki-link/to-markdown.js +0 -28
- package/dist/src/extensions/wiki-link/types.d.ts +0 -34
- package/dist/src/extensions/wiki-link/types.js +0 -1
- package/dist/src/external-documents.d.ts +0 -56
- package/dist/src/external-documents.js +0 -404
- package/dist/src/external-sources.d.ts +0 -136
- package/dist/src/external-sources.js +0 -1098
- package/dist/src/fork/frontmatter-fields.d.ts +0 -36
- package/dist/src/fork/frontmatter-fields.js +0 -23
- package/dist/src/format.d.ts +0 -16
- package/dist/src/format.js +0 -76
- package/dist/src/init-version.d.ts +0 -10
- package/dist/src/init-version.js +0 -49
- package/dist/src/lattice-model.d.ts +0 -84
- package/dist/src/lattice-model.js +0 -467
- package/dist/src/lattice.d.ts +0 -14
- package/dist/src/lattice.js +0 -353
- package/dist/src/markdown-analysis-cache.d.ts +0 -20
- package/dist/src/markdown-analysis-cache.js +0 -139
- package/dist/src/markdown-analysis-worker.d.ts +0 -16
- package/dist/src/markdown-analysis-worker.js +0 -33
- package/dist/src/markdown-analysis.d.ts +0 -61
- package/dist/src/markdown-analysis.js +0 -169
- package/dist/src/markdown-analyzer-loader.d.ts +0 -4
- package/dist/src/markdown-analyzer-loader.js +0 -13
- package/dist/src/markdown-validation.d.ts +0 -25
- package/dist/src/markdown-validation.js +0 -104
- package/dist/src/parser-cache.d.ts +0 -17
- package/dist/src/parser-cache.js +0 -65
- package/dist/src/parser-import.d.ts +0 -8
- package/dist/src/parser-import.js +0 -1
- package/dist/src/parser.d.ts +0 -3
- package/dist/src/parser.js +0 -93
- package/dist/src/path.d.ts +0 -2
- package/dist/src/path.js +0 -4
- package/dist/src/profiler.d.ts +0 -16
- package/dist/src/profiler.js +0 -81
- package/dist/src/project-analysis.d.ts +0 -59
- package/dist/src/project-analysis.js +0 -233
- package/dist/src/project-discovery.d.ts +0 -6
- package/dist/src/project-discovery.js +0 -30
- package/dist/src/repository-path.d.ts +0 -11
- package/dist/src/repository-path.js +0 -52
- package/dist/src/source-formats.d.ts +0 -7
- package/dist/src/source-formats.js +0 -30
- package/dist/src/source-parser.d.ts +0 -67
- package/dist/src/source-parser.js +0 -1749
- package/dist/src/untrusted.d.ts +0 -18
- package/dist/src/untrusted.js +0 -93
- package/dist/src/view/client/assets/channel-uy0fFvcW.js +0 -1
- package/dist/src/view/client/assets/classDiagram-ZZMXUADV-DbSEqjA5.js +0 -1
- package/dist/src/view/client/assets/classDiagram-v2-VYDZK3BY-DbSEqjA5.js +0 -1
- package/dist/src/view/client/assets/index-nBmbYfun.js +0 -13
- package/dist/src/view/client/assets/stateDiagram-v2-MP3YSRHH-gUj6Hzgl.js +0 -1
- package/dist/src/view/client/assets/swimlanesDiagram-VR7AAH4N-DR7JhrxL.js +0 -8
- package/dist/src/walk.d.ts +0 -20
- package/dist/src/walk.js +0 -121
package/dist/src/cli/check.js
DELETED
|
@@ -1,834 +0,0 @@
|
|
|
1
|
-
import { lstat, rename, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
3
|
-
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
4
|
-
import { performance } from 'node:perf_hooks';
|
|
5
|
-
import { flattenSections, resolveRef } from '../lattice-model.js';
|
|
6
|
-
import { isSourceFileExtension, SOURCE_FILE_EXTENSIONS, } from '../source-formats.js';
|
|
7
|
-
import { inspectRepositoryPath, normalizeRepositoryPath, } from '../repository-path.js';
|
|
8
|
-
import { toPosix } from '../path.js';
|
|
9
|
-
import { TimingProfiler } from '../profiler.js';
|
|
10
|
-
import { checkMode } from './check-mode.js';
|
|
11
|
-
import { checkStatus } from './check-status.js';
|
|
12
|
-
import { checkFrontmatter } from './check-frontmatter.js';
|
|
13
|
-
import { checkCoverage } from './check-coverage.js';
|
|
14
|
-
import { indexEntryNameFromDest } from './link-scheme.js';
|
|
15
|
-
import { fencedLineMask } from './gen-index.js';
|
|
16
|
-
import { INIT_VERSION, readInitVersion } from '../init-version.js';
|
|
17
|
-
import { CheckRunContext } from './check-context.js';
|
|
18
|
-
import { parseLocalMarkdownTarget } from '../markdown-validation.js';
|
|
19
|
-
function filePart(id) {
|
|
20
|
-
const h = id.indexOf('#');
|
|
21
|
-
return h === -1 ? id : id.slice(0, h);
|
|
22
|
-
}
|
|
23
|
-
/** Format an ambiguous-ref error as structured markdown-like text. */
|
|
24
|
-
export function ambiguousRefMessage(target, candidates, suggested) {
|
|
25
|
-
const shortName = filePart(target);
|
|
26
|
-
const fileList = candidates.map((c) => ` - "${filePart(c)}.md"`).join('\n');
|
|
27
|
-
const lines = [];
|
|
28
|
-
if (suggested) {
|
|
29
|
-
lines.push(`ambiguous link '[[${target}]]' — did you mean '[[${suggested}]]'?`);
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
const options = candidates.map((a) => `'[[${a}]]'`).join(', ');
|
|
33
|
-
lines.push(`ambiguous link '[[${target}]]' — multiple paths match, use either of: ${options}`);
|
|
34
|
-
}
|
|
35
|
-
lines.push(` The short path "${shortName}" is ambiguous — ${candidates.length} files match:`, fileList, ` Please fix the link to use a fully qualified path.`);
|
|
36
|
-
return lines.join('\n');
|
|
37
|
-
}
|
|
38
|
-
async function profileTime(profile, label, work, detail) {
|
|
39
|
-
return profile ? profile.time(label, work, detail) : work();
|
|
40
|
-
}
|
|
41
|
-
function profileTimeSync(profile, label, work, detail) {
|
|
42
|
-
return profile ? profile.timeSync(label, work, detail) : work();
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Validate an unresolved wiki link as a repository path or source symbol.
|
|
46
|
-
* Returns null when valid, otherwise a user-facing error message.
|
|
47
|
-
*/
|
|
48
|
-
export async function repositoryRefError(target, projectRoot, sourceOptions = {}) {
|
|
49
|
-
const hashIdx = target.indexOf('#');
|
|
50
|
-
const authoredPath = hashIdx === -1 ? target : target.slice(0, hashIdx);
|
|
51
|
-
if (!authoredPath) {
|
|
52
|
-
return `broken link [[${target}]] — no matching section found`;
|
|
53
|
-
}
|
|
54
|
-
const filePart = normalizeRepositoryPath(authoredPath);
|
|
55
|
-
if (!filePart) {
|
|
56
|
-
return `broken link [[${target}]] — repository path "${authoredPath}" must stay within the project root`;
|
|
57
|
-
}
|
|
58
|
-
const symbolPart = hashIdx === -1 ? '' : target.slice(hashIdx + 1);
|
|
59
|
-
const ext = extname(filePart);
|
|
60
|
-
const sourcePath = isSourceFileExtension(ext);
|
|
61
|
-
const inspected = await inspectRepositoryPath(projectRoot, filePart);
|
|
62
|
-
if (inspected.kind === 'outside') {
|
|
63
|
-
return `broken link [[${target}]] — repository path "${filePart}" resolves outside the project root`;
|
|
64
|
-
}
|
|
65
|
-
if (hashIdx === -1) {
|
|
66
|
-
if (inspected.kind === 'missing') {
|
|
67
|
-
return `broken link [[${target}]] — repository file or directory "${filePart}" not found`;
|
|
68
|
-
}
|
|
69
|
-
if (inspected.kind === 'other') {
|
|
70
|
-
return `broken link [[${target}]] — repository path "${filePart}" is not a regular file or directory`;
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
if (inspected.kind === 'directory') {
|
|
75
|
-
return `broken link [[${target}]] — directory "${filePart}" cannot have a fragment`;
|
|
76
|
-
}
|
|
77
|
-
if (inspected.kind === 'other') {
|
|
78
|
-
return `broken link [[${target}]] — repository path "${filePart}" is not a regular file or directory`;
|
|
79
|
-
}
|
|
80
|
-
if (!sourcePath) {
|
|
81
|
-
if (ext || inspected.kind === 'file') {
|
|
82
|
-
const shownExtension = ext || '(none)';
|
|
83
|
-
const supported = SOURCE_FILE_EXTENSIONS.join(', ');
|
|
84
|
-
return `broken link [[${target}]] — unsupported file extension "${shownExtension}" for fragment reference. Supported: ${supported}`;
|
|
85
|
-
}
|
|
86
|
-
return `broken link [[${target}]] — no matching section found`;
|
|
87
|
-
}
|
|
88
|
-
if (inspected.kind === 'missing') {
|
|
89
|
-
return `broken link [[${target}]] — file "${filePart}" not found`;
|
|
90
|
-
}
|
|
91
|
-
if (!symbolPart) {
|
|
92
|
-
// Preserve the existing file-level behavior for an empty fragment.
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
try {
|
|
96
|
-
const { resolveSourceSymbol } = await import('../source-parser.js');
|
|
97
|
-
const { found, error } = await resolveSourceSymbol(filePart, symbolPart, projectRoot, sourceOptions);
|
|
98
|
-
if (error) {
|
|
99
|
-
return `broken link [[${target}]] — ${error}`;
|
|
100
|
-
}
|
|
101
|
-
if (!found) {
|
|
102
|
-
return `broken link [[${target}]] — symbol "${symbolPart}" not found in "${filePart}"`;
|
|
103
|
-
}
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
catch (err) {
|
|
107
|
-
return `broken link [[${target}]] — failed to parse "${filePart}": ${err instanceof Error ? err.message : String(err)}`;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
export async function checkMd(latticeDir, projectRoot = dirname(latticeDir), context) {
|
|
111
|
-
const run = context ?? new CheckRunContext(latticeDir, projectRoot);
|
|
112
|
-
run.clearSourceSymbolCache();
|
|
113
|
-
const files = await run.markdownFiles();
|
|
114
|
-
const { sectionIds, fileIndex, slugIndex } = await run.sectionIndex();
|
|
115
|
-
const errors = [];
|
|
116
|
-
const external = await run.externalResolver();
|
|
117
|
-
for (const error of external.snapshot.errors) {
|
|
118
|
-
errors.push({
|
|
119
|
-
file: relative(process.cwd(), error.file),
|
|
120
|
-
line: 1,
|
|
121
|
-
target: '',
|
|
122
|
-
message: error.message,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
for (const file of files) {
|
|
126
|
-
const refs = await run.refs(file);
|
|
127
|
-
const relPath = relative(process.cwd(), file);
|
|
128
|
-
for (const ref of refs) {
|
|
129
|
-
try {
|
|
130
|
-
if (external.parse(ref.target)) {
|
|
131
|
-
await run.resolveExternal(ref.target);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
errors.push({
|
|
137
|
-
file: relPath,
|
|
138
|
-
line: ref.line,
|
|
139
|
-
target: ref.target,
|
|
140
|
-
message: `broken external link [[${ref.target}]] — ${error.message}`,
|
|
141
|
-
});
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
const unknownExternal = external.unknownTargetMessage(ref.target);
|
|
145
|
-
if (unknownExternal) {
|
|
146
|
-
errors.push({
|
|
147
|
-
file: relPath,
|
|
148
|
-
line: ref.line,
|
|
149
|
-
target: ref.target,
|
|
150
|
-
message: unknownExternal,
|
|
151
|
-
});
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
const { resolved, ambiguous, suggested } = resolveRef(ref.target, sectionIds, fileIndex, slugIndex);
|
|
155
|
-
if (ambiguous) {
|
|
156
|
-
errors.push({
|
|
157
|
-
file: relPath,
|
|
158
|
-
line: ref.line,
|
|
159
|
-
target: ref.target,
|
|
160
|
-
message: ambiguousRefMessage(ref.target, ambiguous, suggested),
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
else if (!sectionIds.has(resolved.toLowerCase())) {
|
|
164
|
-
// Try resolving as a repository path or source symbol.
|
|
165
|
-
const sourceErr = await run.resolveRepositoryLink(ref.target, () => repositoryRefError(ref.target, projectRoot, run.sourceSymbolOptions()));
|
|
166
|
-
if (sourceErr !== null) {
|
|
167
|
-
errors.push({
|
|
168
|
-
file: relPath,
|
|
169
|
-
line: ref.line,
|
|
170
|
-
target: ref.target,
|
|
171
|
-
message: sourceErr,
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return { errors };
|
|
178
|
-
}
|
|
179
|
-
// --- Relative link validation ---
|
|
180
|
-
export async function checkLinks(latticeDir, context) {
|
|
181
|
-
const run = context ?? new CheckRunContext(latticeDir, dirname(latticeDir));
|
|
182
|
-
const files = await run.markdownFiles();
|
|
183
|
-
const errors = [];
|
|
184
|
-
for (const file of files) {
|
|
185
|
-
const links = await run.links(file);
|
|
186
|
-
const relPath = toPosix(relative(process.cwd(), file));
|
|
187
|
-
for (const diagnostic of await run.diagnostics(file)) {
|
|
188
|
-
if (diagnostic.rule !== 'markdown-reference-definition' &&
|
|
189
|
-
diagnostic.rule !== 'markdown-path-separator') {
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
errors.push({
|
|
193
|
-
file: relPath,
|
|
194
|
-
line: diagnostic.line,
|
|
195
|
-
target: diagnostic.target,
|
|
196
|
-
message: diagnostic.message,
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
for (const link of links) {
|
|
200
|
-
if ('identifier' in link)
|
|
201
|
-
continue;
|
|
202
|
-
const target = parseLocalMarkdownTarget(link.url);
|
|
203
|
-
if (target === null)
|
|
204
|
-
continue;
|
|
205
|
-
if (target.kind === 'invalid-backslash')
|
|
206
|
-
continue;
|
|
207
|
-
const abs = target.path ? resolve(dirname(file), target.path) : file;
|
|
208
|
-
if (!existsSync(abs)) {
|
|
209
|
-
const kind = link.kind === 'image' ? 'image' : 'link';
|
|
210
|
-
const shown = toPosix(relative(process.cwd(), abs));
|
|
211
|
-
errors.push({
|
|
212
|
-
file: relPath,
|
|
213
|
-
line: link.line,
|
|
214
|
-
target: link.url,
|
|
215
|
-
message: `broken ${kind} (${link.url}) — file "${shown}" not found`,
|
|
216
|
-
});
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
if (target.fragment &&
|
|
220
|
-
extname(abs).toLowerCase() === '.md' &&
|
|
221
|
-
link.kind !== 'image') {
|
|
222
|
-
const headings = await run.headings(abs);
|
|
223
|
-
if (!headings.has(target.fragment)) {
|
|
224
|
-
const shown = toPosix(relative(process.cwd(), abs));
|
|
225
|
-
errors.push({
|
|
226
|
-
file: relPath,
|
|
227
|
-
line: link.line,
|
|
228
|
-
target: link.url,
|
|
229
|
-
message: `broken link (${link.url}) — heading "#${target.fragment}" not found in "${shown}"`,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return errors;
|
|
236
|
-
}
|
|
237
|
-
export async function checkCodeRefs(latticeDir, projectRoot = dirname(latticeDir), context) {
|
|
238
|
-
const run = context ?? new CheckRunContext(latticeDir, projectRoot);
|
|
239
|
-
const [{ sectionIds, fileIndex, slugIndex }, scan, external] = await Promise.all([
|
|
240
|
-
run.sectionIndex(),
|
|
241
|
-
run.codeRefs(),
|
|
242
|
-
run.externalResolver(),
|
|
243
|
-
]);
|
|
244
|
-
const errors = [];
|
|
245
|
-
for (const error of external.snapshot.errors) {
|
|
246
|
-
errors.push({
|
|
247
|
-
file: relative(process.cwd(), error.file),
|
|
248
|
-
line: 1,
|
|
249
|
-
target: '',
|
|
250
|
-
message: error.message,
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
const mentionedSections = new Set();
|
|
254
|
-
for (const ref of scan.refs) {
|
|
255
|
-
try {
|
|
256
|
-
const externalTarget = profileTimeSync(run.profile, 'classify code-reference target', () => external.parse(ref.target), ref.target);
|
|
257
|
-
if (externalTarget) {
|
|
258
|
-
await run.resolveExternal(ref.target);
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
catch (error) {
|
|
263
|
-
errors.push({
|
|
264
|
-
file: relative(process.cwd(), join(projectRoot, ref.file)),
|
|
265
|
-
line: ref.line,
|
|
266
|
-
target: ref.target,
|
|
267
|
-
message: `@lat: [[${ref.target}]] — ${error.message}`,
|
|
268
|
-
});
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
const unknownExternal = profileTimeSync(run.profile, 'check unknown external handle', () => external.unknownTargetMessage(ref.target), ref.target);
|
|
272
|
-
if (unknownExternal) {
|
|
273
|
-
errors.push({
|
|
274
|
-
file: relative(process.cwd(), join(projectRoot, ref.file)),
|
|
275
|
-
line: ref.line,
|
|
276
|
-
target: ref.target,
|
|
277
|
-
message: `@lat: [[${ref.target}]] — ${unknownExternal}`,
|
|
278
|
-
});
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
const { resolved, ambiguous, suggested } = profileTimeSync(run.profile, 'resolve internal code reference', () => resolveRef(ref.target, sectionIds, fileIndex, slugIndex), ref.target);
|
|
282
|
-
mentionedSections.add(resolved.toLowerCase());
|
|
283
|
-
const displayPath = relative(process.cwd(), join(projectRoot, ref.file));
|
|
284
|
-
if (ambiguous) {
|
|
285
|
-
errors.push({
|
|
286
|
-
file: displayPath,
|
|
287
|
-
line: ref.line,
|
|
288
|
-
target: ref.target,
|
|
289
|
-
message: ambiguousRefMessage(ref.target, ambiguous, suggested),
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
else if (!sectionIds.has(resolved.toLowerCase())) {
|
|
293
|
-
errors.push({
|
|
294
|
-
file: displayPath,
|
|
295
|
-
line: ref.line,
|
|
296
|
-
target: ref.target,
|
|
297
|
-
message: `@lat: [[${ref.target}]] — no matching section found`,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
const files = await run.markdownFiles();
|
|
302
|
-
for (const file of files) {
|
|
303
|
-
const fm = await run.frontmatter(file);
|
|
304
|
-
if (!fm.requireCodeMention)
|
|
305
|
-
continue;
|
|
306
|
-
const fileSections = flattenSections(await run.sections(file));
|
|
307
|
-
const leafSections = fileSections.filter((section) => section.children.length === 0);
|
|
308
|
-
const relPath = relative(process.cwd(), file);
|
|
309
|
-
for (const leaf of leafSections) {
|
|
310
|
-
if (!mentionedSections.has(leaf.id.toLowerCase())) {
|
|
311
|
-
errors.push({
|
|
312
|
-
file: relPath,
|
|
313
|
-
line: leaf.startLine,
|
|
314
|
-
target: leaf.id,
|
|
315
|
-
message: `section "${leaf.id}" requires a code mention but none found`,
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
return { errors };
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Extract the immediate (first-level) entries from walkEntries results.
|
|
324
|
-
* Returns unique file and directory names visible in a given directory.
|
|
325
|
-
*/
|
|
326
|
-
function immediateEntries(walkedPaths) {
|
|
327
|
-
const entries = new Set();
|
|
328
|
-
for (const p of walkedPaths) {
|
|
329
|
-
const slash = p.indexOf('/');
|
|
330
|
-
entries.add(slash === -1 ? p : p.slice(0, slash));
|
|
331
|
-
}
|
|
332
|
-
return [...entries].sort();
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Parse bullet items from an index file. Recognizes both the legacy
|
|
336
|
-
* hand-written form (`- [[name]] — description`) and the generated form
|
|
337
|
-
* written by `lat check --fix` / `lat check index --fix`
|
|
338
|
-
* (`- [Title](name) — summary`) — the two do not collide because the second
|
|
339
|
-
* only matches when a `(` immediately follows the closing `]`, which never
|
|
340
|
-
* happens in a `[[...]]` wiki link.
|
|
341
|
-
*/
|
|
342
|
-
function parseIndexEntries(content) {
|
|
343
|
-
const names = new Set();
|
|
344
|
-
const lines = content.split('\n');
|
|
345
|
-
const fenced = fencedLineMask(lines);
|
|
346
|
-
const unfencedContent = lines.map((l, i) => (fenced[i] ? '' : l)).join('\n');
|
|
347
|
-
const wikiRe = /^- \[\[([^\]]+?)(?:\|[^\]]+)?\]\]/gm;
|
|
348
|
-
let match;
|
|
349
|
-
while ((match = wikiRe.exec(unfencedContent)) !== null) {
|
|
350
|
-
names.add(match[1]);
|
|
351
|
-
}
|
|
352
|
-
// Label allows escaped brackets (`\]`, `\[`) without ending the match early.
|
|
353
|
-
const mdRe = /^- \[((?:\\.|[^\]])*)\]\(([^)]*)\)/gm;
|
|
354
|
-
while ((match = mdRe.exec(unfencedContent)) !== null) {
|
|
355
|
-
const name = indexEntryNameFromDest(match[2]);
|
|
356
|
-
if (name !== null)
|
|
357
|
-
names.add(entryToStem(name));
|
|
358
|
-
}
|
|
359
|
-
return names;
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Convert a filesystem entry name to its wiki link stem.
|
|
363
|
-
* Strips `.md` extension from files; directories stay as-is.
|
|
364
|
-
*/
|
|
365
|
-
function entryToStem(name) {
|
|
366
|
-
return name.endsWith('.md') ? name.slice(0, -3) : name;
|
|
367
|
-
}
|
|
368
|
-
/** Generate a bullet-list snippet for the given entry names. */
|
|
369
|
-
function indexSnippet(entries) {
|
|
370
|
-
return entries.map((e) => `- [[${entryToStem(e)}]] — <describe>`).join('\n');
|
|
371
|
-
}
|
|
372
|
-
export async function checkIndex(latticeDir, context, opts = {}) {
|
|
373
|
-
const run = context ?? new CheckRunContext(latticeDir, dirname(latticeDir));
|
|
374
|
-
const errors = [];
|
|
375
|
-
const allPaths = await run.entries();
|
|
376
|
-
const referencedResources = new Set();
|
|
377
|
-
for (const file of await run.markdownFiles()) {
|
|
378
|
-
for (const link of await run.links(file)) {
|
|
379
|
-
if ('identifier' in link)
|
|
380
|
-
continue;
|
|
381
|
-
const target = parseLocalMarkdownTarget(link.url);
|
|
382
|
-
if (!target || target.kind === 'invalid-backslash' || !target.path) {
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
const absolutePath = resolve(dirname(file), target.path);
|
|
386
|
-
const resourcePath = toPosix(relative(latticeDir, absolutePath));
|
|
387
|
-
if (resourcePath &&
|
|
388
|
-
resourcePath !== '..' &&
|
|
389
|
-
!resourcePath.startsWith('../') &&
|
|
390
|
-
!resourcePath.toLowerCase().endsWith('.md')) {
|
|
391
|
-
referencedResources.add(resourcePath);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
// Flag non-.md files unless Markdown reaches them as local resources. The
|
|
396
|
-
// machine-local external override is the other intentional non-Markdown
|
|
397
|
-
// configuration file in the vault.
|
|
398
|
-
for (const p of allPaths) {
|
|
399
|
-
const name = p.includes('/') ? p.slice(p.lastIndexOf('/') + 1) : p;
|
|
400
|
-
if (!name.endsWith('.md') &&
|
|
401
|
-
p !== 'config.local.yaml' &&
|
|
402
|
-
!referencedResources.has(p)) {
|
|
403
|
-
const relDir = basename(latticeDir) + '/';
|
|
404
|
-
errors.push({
|
|
405
|
-
dir: relDir,
|
|
406
|
-
message: `"${p}" is not a .md file or a referenced local resource — remove it or link to it from Markdown`,
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
// Only .md files participate in index validation
|
|
411
|
-
const mdPaths = allPaths.filter((p) => p.endsWith('.md'));
|
|
412
|
-
// Collect all directories to check (including root, represented as '')
|
|
413
|
-
const dirs = new Set(['']);
|
|
414
|
-
for (const p of mdPaths) {
|
|
415
|
-
const parts = p.split('/');
|
|
416
|
-
// Add every directory prefix
|
|
417
|
-
for (let i = 1; i < parts.length; i++) {
|
|
418
|
-
dirs.add(parts.slice(0, i).join('/'));
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
// Deepest directories first: a subdirectory's index must already be
|
|
422
|
-
// regenerated before its parent index reads a title/summary from it.
|
|
423
|
-
const orderedDirs = [...dirs].sort((a, b) => b.split('/').length - a.split('/').length);
|
|
424
|
-
for (const dir of orderedDirs) {
|
|
425
|
-
// Determine the index file name and its expected path.
|
|
426
|
-
// The index file shares the directory's name — for `lat.md/` it's `lat.md`,
|
|
427
|
-
// for a subdir `api/` it's `api.md`.
|
|
428
|
-
const dirName = dir === '' ? basename(latticeDir) : dir.split('/').pop();
|
|
429
|
-
const indexFileName = dirName.endsWith('.md') ? dirName : dirName + '.md';
|
|
430
|
-
const indexRelPath = dir === '' ? indexFileName : dir + '/' + indexFileName;
|
|
431
|
-
// Get the immediate children of this directory
|
|
432
|
-
const prefix = dir === '' ? '' : dir + '/';
|
|
433
|
-
const childPaths = mdPaths
|
|
434
|
-
.filter((p) => p.startsWith(prefix) && p !== indexRelPath)
|
|
435
|
-
.map((p) => p.slice(prefix.length));
|
|
436
|
-
const children = immediateEntries(childPaths);
|
|
437
|
-
if (children.length === 0)
|
|
438
|
-
continue;
|
|
439
|
-
// Check if the index file exists
|
|
440
|
-
const indexFullPath = join(latticeDir, indexRelPath);
|
|
441
|
-
let content;
|
|
442
|
-
try {
|
|
443
|
-
content = await run.content(indexFullPath);
|
|
444
|
-
}
|
|
445
|
-
catch {
|
|
446
|
-
content = null;
|
|
447
|
-
}
|
|
448
|
-
if (content === null) {
|
|
449
|
-
if (opts.fix) {
|
|
450
|
-
const refusal = await writeGeneratedIndex(latticeDir, dir, indexFullPath, dirName, null, children);
|
|
451
|
-
if (refusal)
|
|
452
|
-
errors.push(refusal);
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
455
|
-
const relDir = dir === '' ? basename(latticeDir) + '/' : dir + '/';
|
|
456
|
-
errors.push({
|
|
457
|
-
dir: relDir,
|
|
458
|
-
message: `missing index file "${indexRelPath}" — create it with a directory listing:\n\n${indexSnippet(children)}`,
|
|
459
|
-
snippet: indexSnippet(children),
|
|
460
|
-
});
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
// Parse existing entries and validate.
|
|
464
|
-
// Listed entries are wiki link stems (no .md extension).
|
|
465
|
-
// Children are filesystem names (with .md for files, bare for dirs).
|
|
466
|
-
const listed = parseIndexEntries(content);
|
|
467
|
-
const childStems = new Set(children.map(entryToStem));
|
|
468
|
-
const relDir = dir === '' ? basename(latticeDir) + '/' : dir + '/';
|
|
469
|
-
const missing = [];
|
|
470
|
-
for (const child of children) {
|
|
471
|
-
if (!listed.has(entryToStem(child))) {
|
|
472
|
-
missing.push(child);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
const indexStem = entryToStem(indexFileName);
|
|
476
|
-
const stale = [];
|
|
477
|
-
for (const name of listed) {
|
|
478
|
-
if (!childStems.has(name) && name !== indexStem) {
|
|
479
|
-
stale.push(name);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
if (missing.length === 0 && stale.length === 0)
|
|
483
|
-
continue;
|
|
484
|
-
if (opts.fix) {
|
|
485
|
-
const refusal = await writeGeneratedIndex(latticeDir, dir, indexFullPath, dirName, content, children);
|
|
486
|
-
if (refusal)
|
|
487
|
-
errors.push(refusal);
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
if (missing.length > 0) {
|
|
491
|
-
errors.push({
|
|
492
|
-
dir: relDir,
|
|
493
|
-
message: `"${indexRelPath}" is missing entries — add:\n\n${indexSnippet(missing)}`,
|
|
494
|
-
snippet: indexSnippet(missing),
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
for (const name of stale) {
|
|
498
|
-
errors.push({
|
|
499
|
-
dir: relDir,
|
|
500
|
-
message: `"${indexRelPath}" lists "[[${name}]]" but it does not exist`,
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
return errors;
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Build entry sources for a directory's children and write the generated
|
|
508
|
-
* index. Returns an IndexError describing a refusal if the index path (or
|
|
509
|
-
* the temporary path used to write it) is a symlink; returns null on
|
|
510
|
-
* success.
|
|
511
|
-
*
|
|
512
|
-
* The write never follows a symlink: it writes to a temp file in the same
|
|
513
|
-
* directory (created with O_EXCL, so a pre-planted symlink there is refused
|
|
514
|
-
* rather than written through) and renames it over the target, since rename
|
|
515
|
-
* replaces a symlink rather than following it.
|
|
516
|
-
*/
|
|
517
|
-
async function writeGeneratedIndex(latticeDir, dir, indexFullPath, dirName, existingContent, children) {
|
|
518
|
-
const relIndexPath = relative(latticeDir, indexFullPath);
|
|
519
|
-
const relDir = dir === '' ? basename(latticeDir) + '/' : dir + '/';
|
|
520
|
-
try {
|
|
521
|
-
const stat = await lstat(indexFullPath);
|
|
522
|
-
if (stat.isSymbolicLink()) {
|
|
523
|
-
return {
|
|
524
|
-
dir: relDir,
|
|
525
|
-
message: `refusing to write generated index "${relIndexPath}" — it is a symlink`,
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
catch {
|
|
530
|
-
// Does not exist yet — fine, we're creating it.
|
|
531
|
-
}
|
|
532
|
-
const { renderIndexEntries, spliceIndexContent } = await import('./gen-index.js');
|
|
533
|
-
const entries = children.map((child) => {
|
|
534
|
-
const isDir = !child.endsWith('.md');
|
|
535
|
-
const dest = isDir ? `${child}/${child}.md` : child;
|
|
536
|
-
const readFrom = isDir
|
|
537
|
-
? join(latticeDir, dir, child, `${child}.md`)
|
|
538
|
-
: join(latticeDir, dir, child);
|
|
539
|
-
return { name: child, dest, readFrom };
|
|
540
|
-
});
|
|
541
|
-
const rendered = await renderIndexEntries(entries);
|
|
542
|
-
const dirLabel = dirName.endsWith('.md') ? dirName.slice(0, -3) : dirName;
|
|
543
|
-
const spliced = spliceIndexContent(existingContent, dirLabel, rendered);
|
|
544
|
-
if (!spliced.ok) {
|
|
545
|
-
return {
|
|
546
|
-
dir: relDir,
|
|
547
|
-
message: `refusing to write generated index "${relIndexPath}" — ${spliced.message}`,
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
const newContent = spliced.content;
|
|
551
|
-
const tempPath = `${indexFullPath}.tmp`;
|
|
552
|
-
try {
|
|
553
|
-
const tempStat = await lstat(tempPath);
|
|
554
|
-
if (tempStat.isSymbolicLink() || tempStat.isFile()) {
|
|
555
|
-
// Something is already at the temp path (possibly a pre-planted
|
|
556
|
-
// symlink). Refuse rather than write through or over it.
|
|
557
|
-
//
|
|
558
|
-
// This also catches a leftover temp file from a run killed between the
|
|
559
|
-
// write and the rename, which would otherwise block --fix for this
|
|
560
|
-
// directory forever. That case is harmless to clear, but the reader
|
|
561
|
-
// cannot tell the two apart from here, so say what to do rather than
|
|
562
|
-
// deleting a file this command was never asked to remove.
|
|
563
|
-
return {
|
|
564
|
-
dir: relDir,
|
|
565
|
-
message: `refusing to write generated index "${relIndexPath}" — its temporary write path "${relative(latticeDir, tempPath)}" already exists. Inspect it, then delete it to allow --fix to proceed.`,
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
catch {
|
|
570
|
-
// Nothing at the temp path — fine.
|
|
571
|
-
}
|
|
572
|
-
try {
|
|
573
|
-
// 'wx' is the load-bearing flag, not a detail: it fails rather than
|
|
574
|
-
// follows if anything appeared at the temp path since the lstat above,
|
|
575
|
-
// which closes the window between the two checks.
|
|
576
|
-
await writeFile(tempPath, newContent, { encoding: 'utf-8', flag: 'wx' });
|
|
577
|
-
}
|
|
578
|
-
catch (err) {
|
|
579
|
-
return {
|
|
580
|
-
dir: relDir,
|
|
581
|
-
message: `refusing to write generated index "${relIndexPath}" — could not create temporary write path "${relative(latticeDir, tempPath)}" (${err.message})`,
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
try {
|
|
585
|
-
await rename(tempPath, indexFullPath);
|
|
586
|
-
}
|
|
587
|
-
catch (err) {
|
|
588
|
-
await unlink(tempPath).catch(() => { });
|
|
589
|
-
throw err;
|
|
590
|
-
}
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
// --- Section structure validation ---
|
|
594
|
-
export async function checkSections(latticeDir, projectRoot = dirname(latticeDir), context) {
|
|
595
|
-
const run = context ?? new CheckRunContext(latticeDir, projectRoot);
|
|
596
|
-
const files = await run.markdownFiles();
|
|
597
|
-
const errors = [];
|
|
598
|
-
for (const file of files) {
|
|
599
|
-
const relPath = relative(process.cwd(), file);
|
|
600
|
-
for (const diagnostic of await run.diagnostics(file)) {
|
|
601
|
-
if (diagnostic.rule !== 'section-leading-paragraph')
|
|
602
|
-
continue;
|
|
603
|
-
errors.push({
|
|
604
|
-
file: relPath,
|
|
605
|
-
line: diagnostic.line,
|
|
606
|
-
target: diagnostic.target,
|
|
607
|
-
message: diagnostic.message,
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
return errors;
|
|
612
|
-
}
|
|
613
|
-
// --- Formatting helpers (shared by all check commands) ---
|
|
614
|
-
function formatCheckErrors(errors, s) {
|
|
615
|
-
const lines = [];
|
|
616
|
-
for (const err of errors) {
|
|
617
|
-
lines.push('');
|
|
618
|
-
const loc = s.cyan(err.file + ':' + err.line);
|
|
619
|
-
const [first, ...rest] = err.message.split('\n');
|
|
620
|
-
lines.push(`- ${loc}: ${s.red(first)}`);
|
|
621
|
-
for (const line of rest) {
|
|
622
|
-
lines.push(` ${s.red(line)}`);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
return lines;
|
|
626
|
-
}
|
|
627
|
-
function formatCheckIndexErrors(errors, s) {
|
|
628
|
-
const lines = [];
|
|
629
|
-
for (const err of errors) {
|
|
630
|
-
lines.push('');
|
|
631
|
-
const loc = s.cyan(err.dir);
|
|
632
|
-
const [first, ...rest] = err.message.split('\n');
|
|
633
|
-
lines.push(`- ${loc}: ${s.red(first)}`);
|
|
634
|
-
for (const line of rest) {
|
|
635
|
-
lines.push(` ${s.red(line)}`);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
return lines;
|
|
639
|
-
}
|
|
640
|
-
function formatErrorCount(count, s) {
|
|
641
|
-
return s.red(`\n${count} error${count === 1 ? '' : 's'} found`);
|
|
642
|
-
}
|
|
643
|
-
export async function checkAllCommand(ctx, options = {}) {
|
|
644
|
-
const startTime = performance.now();
|
|
645
|
-
const profile = options.profile ? new TimingProfiler() : undefined;
|
|
646
|
-
// `--fix` is index-only: an index is derived from frontmatter, so it can be
|
|
647
|
-
// regenerated. Nothing else `lat check` reports has a mechanical fix.
|
|
648
|
-
//
|
|
649
|
-
// A fixing run also has to finish before any other check starts. The checks
|
|
650
|
-
// below run concurrently over one shared parse cache, and --fix rewrites the
|
|
651
|
-
// very files they read; interleaving that write with those reads would make
|
|
652
|
-
// the run's verdict depend on scheduling. So it gets its own context, and
|
|
653
|
-
// the readers get a fresh one built after the writing is done.
|
|
654
|
-
const fixIndexErrors = options.fix
|
|
655
|
-
? await profileTime(profile, 'generate directory indexes', () => checkIndex(ctx.latDir, new CheckRunContext(ctx.latDir, ctx.projectRoot, profile), { fix: true }))
|
|
656
|
-
: null;
|
|
657
|
-
const run = new CheckRunContext(ctx.latDir, ctx.projectRoot, profile);
|
|
658
|
-
const [md, linkErrors, code, checkedIndexErrors, sectionErrors, modeErrors, statusErrors, fmErrors, coverageErrors,] = await Promise.all([
|
|
659
|
-
profileTime(profile, 'check Markdown wiki links', () => checkMd(ctx.latDir, ctx.projectRoot, run)),
|
|
660
|
-
profileTime(profile, 'check relative Markdown links', () => checkLinks(ctx.latDir, run)),
|
|
661
|
-
profileTime(profile, 'check @lat code references', () => checkCodeRefs(ctx.latDir, ctx.projectRoot, run)),
|
|
662
|
-
// Already generated above when --fix is set; re-reporting it would only
|
|
663
|
-
// describe the state the fix just left behind.
|
|
664
|
-
fixIndexErrors
|
|
665
|
-
? Promise.resolve([])
|
|
666
|
-
: profileTime(profile, 'check directory indexes', () => checkIndex(ctx.latDir, run)),
|
|
667
|
-
profileTime(profile, 'check section structure', () => checkSections(ctx.latDir, ctx.projectRoot, run)),
|
|
668
|
-
profileTime(profile, 'check Diátaxis modes', () => checkMode(ctx.latDir, ctx.projectRoot)),
|
|
669
|
-
profileTime(profile, 'check review status', () => checkStatus(ctx.latDir, ctx.projectRoot)),
|
|
670
|
-
profileTime(profile, 'check frontmatter placement', () => checkFrontmatter(ctx.latDir, ctx.projectRoot)),
|
|
671
|
-
profileTime(profile, 'check documentation coverage', () => checkCoverage(ctx.latDir, ctx.projectRoot)),
|
|
672
|
-
]);
|
|
673
|
-
const indexErrors = fixIndexErrors ?? checkedIndexErrors;
|
|
674
|
-
const elapsed = performance.now() - startTime;
|
|
675
|
-
const allErrors = [
|
|
676
|
-
...new Map([...md.errors, ...linkErrors, ...code.errors].map((error) => [
|
|
677
|
-
`${error.file}\0${error.line}\0${error.target}\0${error.message}`,
|
|
678
|
-
error,
|
|
679
|
-
])).values(),
|
|
680
|
-
];
|
|
681
|
-
const s = ctx.styler;
|
|
682
|
-
const elapsedStr = elapsed < 1000
|
|
683
|
-
? `${Math.round(elapsed)}ms`
|
|
684
|
-
: `${(elapsed / 1000).toFixed(1)}s`;
|
|
685
|
-
const lines = profile ? profile.format(elapsed) : [];
|
|
686
|
-
// Init version warning first — user should fix setup before addressing errors
|
|
687
|
-
if (!ctx.headless) {
|
|
688
|
-
const storedVersion = readInitVersion(ctx.latDir);
|
|
689
|
-
if (storedVersion === null) {
|
|
690
|
-
if (lines.length > 0)
|
|
691
|
-
lines.push('');
|
|
692
|
-
lines.push(s.yellow('Warning:') +
|
|
693
|
-
' No init version recorded — run ' +
|
|
694
|
-
s.cyan('lat init') +
|
|
695
|
-
' to set up agent hooks and configuration.');
|
|
696
|
-
}
|
|
697
|
-
else if (storedVersion < INIT_VERSION) {
|
|
698
|
-
if (lines.length > 0)
|
|
699
|
-
lines.push('');
|
|
700
|
-
lines.push(s.yellow('Warning:') +
|
|
701
|
-
' Your setup is outdated (v' +
|
|
702
|
-
storedVersion +
|
|
703
|
-
' → v' +
|
|
704
|
-
INIT_VERSION +
|
|
705
|
-
'). Re-run ' +
|
|
706
|
-
s.cyan('lat init') +
|
|
707
|
-
' to update agent hooks and configuration.');
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
lines.push(...formatCheckErrors(allErrors, s));
|
|
711
|
-
lines.push(...formatCheckIndexErrors(indexErrors, s));
|
|
712
|
-
lines.push(...formatCheckErrors(sectionErrors, s));
|
|
713
|
-
lines.push(...formatCheckErrors(modeErrors, s));
|
|
714
|
-
lines.push(...formatCheckErrors(statusErrors, s));
|
|
715
|
-
lines.push(...formatCheckErrors(fmErrors, s));
|
|
716
|
-
lines.push(...formatCheckErrors(coverageErrors, s));
|
|
717
|
-
const totalErrors = allErrors.length +
|
|
718
|
-
indexErrors.length +
|
|
719
|
-
sectionErrors.length +
|
|
720
|
-
modeErrors.length +
|
|
721
|
-
statusErrors.length +
|
|
722
|
-
fmErrors.length +
|
|
723
|
-
coverageErrors.length;
|
|
724
|
-
if (totalErrors > 0) {
|
|
725
|
-
lines.push(formatErrorCount(totalErrors, s));
|
|
726
|
-
return { output: lines.join('\n'), isError: true };
|
|
727
|
-
}
|
|
728
|
-
lines.push(s.green(`All checks passed in ${elapsedStr}`));
|
|
729
|
-
// Suggest ripgrep if check was slow (>1s) and rg is not available
|
|
730
|
-
if (elapsed > 1000) {
|
|
731
|
-
const { hasRipgrep } = await import('../code-refs.js');
|
|
732
|
-
if (!(await hasRipgrep())) {
|
|
733
|
-
lines.push(s.yellow('Tip:') +
|
|
734
|
-
' Install ' +
|
|
735
|
-
s.cyan('ripgrep') +
|
|
736
|
-
' (rg) for faster code scanning.' +
|
|
737
|
-
' See https://github.com/BurntSushi/ripgrep#installation');
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
return { output: lines.join('\n') };
|
|
741
|
-
}
|
|
742
|
-
export async function checkMdCommand(ctx) {
|
|
743
|
-
const { errors } = await checkMd(ctx.latDir, ctx.projectRoot);
|
|
744
|
-
const s = ctx.styler;
|
|
745
|
-
const lines = [];
|
|
746
|
-
lines.push(...formatCheckErrors(errors, s));
|
|
747
|
-
if (errors.length > 0) {
|
|
748
|
-
lines.push(formatErrorCount(errors.length, s));
|
|
749
|
-
return { output: lines.join('\n'), isError: true };
|
|
750
|
-
}
|
|
751
|
-
lines.push(s.green('md: All links OK'));
|
|
752
|
-
return { output: lines.join('\n') };
|
|
753
|
-
}
|
|
754
|
-
export async function checkLinksCommand(ctx) {
|
|
755
|
-
const errors = await checkLinks(ctx.latDir);
|
|
756
|
-
const s = ctx.styler;
|
|
757
|
-
const lines = [];
|
|
758
|
-
lines.push(...formatCheckErrors(errors, s));
|
|
759
|
-
if (errors.length > 0) {
|
|
760
|
-
lines.push(formatErrorCount(errors.length, s));
|
|
761
|
-
return { output: lines.join('\n'), isError: true };
|
|
762
|
-
}
|
|
763
|
-
lines.push(s.green('links: All relative links resolve'));
|
|
764
|
-
return { output: lines.join('\n') };
|
|
765
|
-
}
|
|
766
|
-
export async function checkCodeRefsCommand(ctx) {
|
|
767
|
-
const { errors } = await checkCodeRefs(ctx.latDir, ctx.projectRoot);
|
|
768
|
-
const s = ctx.styler;
|
|
769
|
-
const lines = [];
|
|
770
|
-
lines.push(...formatCheckErrors(errors, s));
|
|
771
|
-
if (errors.length > 0) {
|
|
772
|
-
lines.push(formatErrorCount(errors.length, s));
|
|
773
|
-
return { output: lines.join('\n'), isError: true };
|
|
774
|
-
}
|
|
775
|
-
lines.push(s.green('code-refs: All references OK'));
|
|
776
|
-
return { output: lines.join('\n') };
|
|
777
|
-
}
|
|
778
|
-
export async function checkIndexCommand(ctx, opts = {}) {
|
|
779
|
-
const errors = await checkIndex(ctx.latDir, undefined, opts);
|
|
780
|
-
const s = ctx.styler;
|
|
781
|
-
const lines = [];
|
|
782
|
-
if (opts.fix) {
|
|
783
|
-
lines.push(s.green('index: directory index files regenerated'));
|
|
784
|
-
if (errors.length > 0) {
|
|
785
|
-
lines.push(...formatCheckIndexErrors(errors, s));
|
|
786
|
-
lines.push(formatErrorCount(errors.length, s));
|
|
787
|
-
return { output: lines.join('\n'), isError: true };
|
|
788
|
-
}
|
|
789
|
-
return { output: lines.join('\n') };
|
|
790
|
-
}
|
|
791
|
-
lines.push(...formatCheckIndexErrors(errors, s));
|
|
792
|
-
if (errors.length > 0) {
|
|
793
|
-
lines.push(formatErrorCount(errors.length, s));
|
|
794
|
-
return { output: lines.join('\n'), isError: true };
|
|
795
|
-
}
|
|
796
|
-
lines.push(s.green('index: All directory index files OK'));
|
|
797
|
-
return { output: lines.join('\n') };
|
|
798
|
-
}
|
|
799
|
-
export async function checkSectionsCommand(ctx) {
|
|
800
|
-
const errors = await checkSections(ctx.latDir, ctx.projectRoot);
|
|
801
|
-
const s = ctx.styler;
|
|
802
|
-
const lines = [];
|
|
803
|
-
lines.push(...formatCheckErrors(errors, s));
|
|
804
|
-
if (errors.length > 0) {
|
|
805
|
-
lines.push(formatErrorCount(errors.length, s));
|
|
806
|
-
return { output: lines.join('\n'), isError: true };
|
|
807
|
-
}
|
|
808
|
-
lines.push(s.green('sections: All sections have valid leading paragraphs'));
|
|
809
|
-
return { output: lines.join('\n') };
|
|
810
|
-
}
|
|
811
|
-
export async function checkModeCommand(ctx) {
|
|
812
|
-
const errors = await checkMode(ctx.latDir, ctx.projectRoot);
|
|
813
|
-
const s = ctx.styler;
|
|
814
|
-
const lines = [];
|
|
815
|
-
lines.push(...formatCheckErrors(errors, s));
|
|
816
|
-
if (errors.length > 0) {
|
|
817
|
-
lines.push(formatErrorCount(errors.length, s));
|
|
818
|
-
return { output: lines.join('\n'), isError: true };
|
|
819
|
-
}
|
|
820
|
-
lines.push(s.green('mode: All documents match their Diátaxis mode'));
|
|
821
|
-
return { output: lines.join('\n') };
|
|
822
|
-
}
|
|
823
|
-
export async function checkStatusCommand(ctx) {
|
|
824
|
-
const errors = await checkStatus(ctx.latDir, ctx.projectRoot);
|
|
825
|
-
const s = ctx.styler;
|
|
826
|
-
const lines = [];
|
|
827
|
-
lines.push(...formatCheckErrors(errors, s));
|
|
828
|
-
if (errors.length > 0) {
|
|
829
|
-
lines.push(formatErrorCount(errors.length, s));
|
|
830
|
-
return { output: lines.join('\n'), isError: true };
|
|
831
|
-
}
|
|
832
|
-
lines.push(s.green('status: Every recorded review matches its document'));
|
|
833
|
-
return { output: lines.join('\n') };
|
|
834
|
-
}
|