@plebeiantech/lat.md 0.12.3-fork.1

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.
Files changed (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/dist/src/cli/check-frontmatter.d.ts +12 -0
  4. package/dist/src/cli/check-frontmatter.js +44 -0
  5. package/dist/src/cli/check-mode.d.ts +6 -0
  6. package/dist/src/cli/check-mode.js +303 -0
  7. package/dist/src/cli/check-status.d.ts +74 -0
  8. package/dist/src/cli/check-status.js +217 -0
  9. package/dist/src/cli/check.d.ts +45 -0
  10. package/dist/src/cli/check.js +777 -0
  11. package/dist/src/cli/checklist-menu.d.ts +12 -0
  12. package/dist/src/cli/checklist-menu.js +111 -0
  13. package/dist/src/cli/comment-guard.d.ts +16 -0
  14. package/dist/src/cli/comment-guard.js +111 -0
  15. package/dist/src/cli/comment-reminder.d.ts +62 -0
  16. package/dist/src/cli/comment-reminder.js +210 -0
  17. package/dist/src/cli/context.d.ts +11 -0
  18. package/dist/src/cli/context.js +58 -0
  19. package/dist/src/cli/expand.d.ts +8 -0
  20. package/dist/src/cli/expand.js +94 -0
  21. package/dist/src/cli/gen-index.d.ts +45 -0
  22. package/dist/src/cli/gen-index.js +235 -0
  23. package/dist/src/cli/gen.d.ts +6 -0
  24. package/dist/src/cli/gen.js +42 -0
  25. package/dist/src/cli/graph.d.ts +7 -0
  26. package/dist/src/cli/graph.js +47 -0
  27. package/dist/src/cli/hook.d.ts +1 -0
  28. package/dist/src/cli/hook.js +432 -0
  29. package/dist/src/cli/index.d.ts +2 -0
  30. package/dist/src/cli/index.js +307 -0
  31. package/dist/src/cli/init.d.ts +9 -0
  32. package/dist/src/cli/init.js +1031 -0
  33. package/dist/src/cli/link-scheme.d.ts +54 -0
  34. package/dist/src/cli/link-scheme.js +101 -0
  35. package/dist/src/cli/locate.d.ts +2 -0
  36. package/dist/src/cli/locate.js +17 -0
  37. package/dist/src/cli/refs.d.ts +22 -0
  38. package/dist/src/cli/refs.js +229 -0
  39. package/dist/src/cli/reindex.d.ts +13 -0
  40. package/dist/src/cli/reindex.js +136 -0
  41. package/dist/src/cli/search.d.ts +36 -0
  42. package/dist/src/cli/search.js +217 -0
  43. package/dist/src/cli/section.d.ts +43 -0
  44. package/dist/src/cli/section.js +252 -0
  45. package/dist/src/cli/select-menu.d.ts +13 -0
  46. package/dist/src/cli/select-menu.js +100 -0
  47. package/dist/src/cli/templates.d.ts +1 -0
  48. package/dist/src/cli/templates.js +15 -0
  49. package/dist/src/cli/ui.d.ts +11 -0
  50. package/dist/src/cli/ui.js +34 -0
  51. package/dist/src/code-refs.d.ts +17 -0
  52. package/dist/src/code-refs.js +277 -0
  53. package/dist/src/config.d.ts +29 -0
  54. package/dist/src/config.js +83 -0
  55. package/dist/src/context.d.ts +23 -0
  56. package/dist/src/context.js +11 -0
  57. package/dist/src/extensions/wiki-link/from-markdown.d.ts +5 -0
  58. package/dist/src/extensions/wiki-link/from-markdown.js +34 -0
  59. package/dist/src/extensions/wiki-link/index.d.ts +4 -0
  60. package/dist/src/extensions/wiki-link/index.js +3 -0
  61. package/dist/src/extensions/wiki-link/syntax.d.ts +14 -0
  62. package/dist/src/extensions/wiki-link/syntax.js +121 -0
  63. package/dist/src/extensions/wiki-link/to-markdown.d.ts +5 -0
  64. package/dist/src/extensions/wiki-link/to-markdown.js +28 -0
  65. package/dist/src/extensions/wiki-link/types.d.ts +34 -0
  66. package/dist/src/extensions/wiki-link/types.js +1 -0
  67. package/dist/src/format.d.ts +8 -0
  68. package/dist/src/format.js +43 -0
  69. package/dist/src/graph-export.d.ts +60 -0
  70. package/dist/src/graph-export.js +309 -0
  71. package/dist/src/init-version.d.ts +10 -0
  72. package/dist/src/init-version.js +49 -0
  73. package/dist/src/knowledge/bd.d.ts +2 -0
  74. package/dist/src/knowledge/bd.js +107 -0
  75. package/dist/src/knowledge/claude-memory.d.ts +13 -0
  76. package/dist/src/knowledge/claude-memory.js +204 -0
  77. package/dist/src/knowledge/cq.d.ts +2 -0
  78. package/dist/src/knowledge/cq.js +74 -0
  79. package/dist/src/knowledge/index.d.ts +49 -0
  80. package/dist/src/knowledge/index.js +271 -0
  81. package/dist/src/knowledge/ranking.d.ts +17 -0
  82. package/dist/src/knowledge/ranking.js +62 -0
  83. package/dist/src/knowledge/session.d.ts +46 -0
  84. package/dist/src/knowledge/session.js +203 -0
  85. package/dist/src/knowledge/types.d.ts +47 -0
  86. package/dist/src/knowledge/types.js +10 -0
  87. package/dist/src/lattice.d.ts +108 -0
  88. package/dist/src/lattice.js +816 -0
  89. package/dist/src/mcp/server.d.ts +1 -0
  90. package/dist/src/mcp/server.js +58 -0
  91. package/dist/src/parser.d.ts +3 -0
  92. package/dist/src/parser.js +18 -0
  93. package/dist/src/search/db.d.ts +16 -0
  94. package/dist/src/search/db.js +54 -0
  95. package/dist/src/search/embedder.d.ts +25 -0
  96. package/dist/src/search/embedder.js +58 -0
  97. package/dist/src/search/index.d.ts +9 -0
  98. package/dist/src/search/index.js +66 -0
  99. package/dist/src/search/search.d.ts +9 -0
  100. package/dist/src/search/search.js +16 -0
  101. package/dist/src/source-parser.d.ts +27 -0
  102. package/dist/src/source-parser.js +916 -0
  103. package/dist/src/untrusted.d.ts +24 -0
  104. package/dist/src/untrusted.js +97 -0
  105. package/dist/src/version.d.ts +7 -0
  106. package/dist/src/version.js +39 -0
  107. package/dist/src/view/client/assets/index-B0n0u03y.css +1 -0
  108. package/dist/src/view/client/assets/index-xm2_px4q.js +23 -0
  109. package/dist/src/view/client/index.html +14 -0
  110. package/dist/src/view/diagnostics.d.ts +5 -0
  111. package/dist/src/view/diagnostics.js +171 -0
  112. package/dist/src/view/git-diff.d.ts +3 -0
  113. package/dist/src/view/git-diff.js +255 -0
  114. package/dist/src/view/git.d.ts +22 -0
  115. package/dist/src/view/git.js +193 -0
  116. package/dist/src/view/highlight.d.ts +2 -0
  117. package/dist/src/view/highlight.js +3767 -0
  118. package/dist/src/view/markdown.d.ts +22 -0
  119. package/dist/src/view/markdown.js +322 -0
  120. package/dist/src/view/protocol.d.ts +89 -0
  121. package/dist/src/view/protocol.js +1 -0
  122. package/dist/src/view/references.d.ts +62 -0
  123. package/dist/src/view/references.js +345 -0
  124. package/dist/src/view/repository.d.ts +12 -0
  125. package/dist/src/view/repository.js +167 -0
  126. package/dist/src/view/search.d.ts +14 -0
  127. package/dist/src/view/search.js +64 -0
  128. package/dist/src/view/server.d.ts +20 -0
  129. package/dist/src/view/server.js +233 -0
  130. package/dist/src/view/store.d.ts +57 -0
  131. package/dist/src/view/store.js +404 -0
  132. package/dist/src/walk.d.ts +18 -0
  133. package/dist/src/walk.js +27 -0
  134. package/package.json +85 -0
  135. package/templates/AGENTS.md +118 -0
  136. package/templates/README +1 -0
  137. package/templates/cursor-rules.md +77 -0
  138. package/templates/init/lat.md +1 -0
  139. package/templates/logo-dark.svg +182 -0
  140. package/templates/logo-light.svg +182 -0
  141. package/templates/logo.txt +7 -0
  142. package/templates/opencode-plugin.ts +180 -0
  143. package/templates/pi-extension.ts +364 -0
  144. package/templates/skill/SKILL.md +189 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yury Selivanov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ <p align="center">
2
+ <img src="templates/logo-dark.svg" alt="lat.md" width="500">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://github.com/1st1/lat.md/actions/workflows/ci.yml"><img src="https://github.com/1st1/lat.md/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
7
+ <a href="https://www.npmjs.com/package/lat.md"><img src="https://img.shields.io/npm/v/lat.md" alt="npm"></a>
8
+ </p>
9
+
10
+ <p align="center">A knowledge graph for your codebase, written in markdown.</p>
11
+
12
+ ## The problem
13
+
14
+ `AGENTS.md` doesn't scale. A single flat file can describe a small project, but as a codebase grows, maintaining one monolithic document becomes impractical. Key design decisions get buried, business logic goes undocumented, and agents hallucinate context they should be able to look up.
15
+
16
+ ## The idea
17
+
18
+ Compress the knowledge about your program domain into a **graph** — a set of interconnected markdown files that live in a `lat.md/` directory at the root of your project. Sections link to each other with `[[wiki links]]`, markdown files link into the codebase (`[[src/auth.ts#validateToken]]`), source files link back with `// @lat: [[section-id]]` comments, and `lat check` ensures nothing drifts out of sync.
19
+
20
+ - **Faster coding for agents** — instead of grepping through your codebase, agents search the knowledge graph to discover key design decisions, constraints, and domain context fast and consistently.
21
+
22
+ - **Faster workflow for humans** — your agents maintain lat files for you. When you review a diff, start with the semantic changes in `lat.md/` to understand *what* changed and *why*. Reviewing code becomes the secondary task.
23
+
24
+ - **Knowledge retention** — the context and reasoning behind your prompts is usually lost after a session ends. With lat, agents capture that knowledge into the graph as they work, so future sessions start with full context instead of rediscovering it from scratch.
25
+
26
+ - **Test specs with enforcement** — test cases can be described as sections in `lat.md/` and marked with `require-code-mention: true`. Each spec then must be referenced by a `// @lat:` comment in test code. `lat check` flags any spec without a backlink, so you can review and maintain test coverage from the knowledge graph.
27
+
28
+ The `lat` CLI gives agents and humans a system to navigate and maintain the graph:
29
+
30
+ - **`lat init`** — sets up popular coding agents with hooks and instructions to keep lat updated and correct
31
+ - **`lat check`** — enforces referential consistency; agents call it automatically before finishing work
32
+ - **`lat search`** and **`lat section`** — agents use these to understand your prompts and navigate the graph instead of endless `grep` calls
33
+
34
+ `lat` is a workflow that comes with tools — build pre-commit hooks and GitHub bots, run CI tasks that improve the knowledge graph in the background.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ npm install -g lat.md
40
+ ```
41
+
42
+ Then run `lat init` in the repo you want to use lat in.
43
+
44
+ ## How it works
45
+
46
+ Run `lat init` to scaffold a `lat.md/` directory, then write markdown files describing your architecture, business logic, test specs — whatever matters. Link between sections using `[[file#Section#Subsection]]` syntax. Link to source code symbols with `[[src/auth.ts#validateToken]]`. Annotate source code with `// @lat: [[section-id]]` (or `# @lat: [[section-id]]` in Python) comments to tie implementation back to concepts.
47
+
48
+ ```
49
+ my-project/
50
+ ├── lat.md/
51
+ │ ├── architecture.md # system design, key decisions
52
+ │ ├── auth.md # authentication & authorization logic
53
+ │ └── tests.md # test specs (require-code-mention: true)
54
+ ├── src/
55
+ │ ├── auth.ts # // @lat: [[auth#OAuth Flow]]
56
+ │ └── server.ts # // @lat: [[architecture#Request Pipeline]]
57
+ └── ...
58
+ ```
59
+
60
+ ## CLI
61
+
62
+ ```bash
63
+ lat init # scaffold a lat.md/ directory
64
+ lat check # run full graph and documentation validation
65
+ lat locate "OAuth Flow" # find sections by name (exact, fuzzy)
66
+ lat section "auth#OAuth Flow" # show a section with its links and refs
67
+ lat refs "auth#OAuth Flow" # find what references a section
68
+ lat search "how do we auth?" # semantic search via embeddings
69
+ lat expand "fix [[OAuth Flow]]" # expand [[refs]] in a prompt for agents
70
+ lat mcp # start MCP server for editor integration
71
+ ```
72
+
73
+ ## Configuration
74
+
75
+ Semantic search (`lat search`) works **offline by default** — no API key required. It uses a bundled local embedding model (all-MiniLM-L6-v2, compiled to WebAssembly; no native binaries, no network).
76
+
77
+ To use higher-quality hosted embeddings instead, provide an OpenAI (`sk-...`) or Vercel AI Gateway (`vck_...`) API key, resolved in order:
78
+
79
+ 1. `LAT_LLM_KEY` env var — direct value
80
+ 2. `LAT_LLM_KEY_FILE` env var — path to a file containing the key
81
+ 3. `LAT_LLM_KEY_HELPER` env var — shell command that prints the key (10s timeout)
82
+ 4. Config file — power users can set `llm_key` manually. Run `lat config` to print its location.
83
+
84
+ Switch backends any time with `lat reindex` (`--local` to force the offline model, `--remote` to use your key).
85
+
86
+ ## Development
87
+
88
+ Development requires Node.js 22, pnpm, and Rust installed through rustup. The
89
+ pnpm setup installs the matching WASM target and build tools project-locally.
90
+ See the [development process](lat.md/dev-process.md) for complete setup and
91
+ contribution guidance.
92
+
93
+ ```bash
94
+ pnpm install
95
+ pnpm buildall
96
+ pnpm test
97
+ ```
@@ -0,0 +1,12 @@
1
+ import type { CheckError } from './check.js';
2
+ /**
3
+ * Report frontmatter that parses to nothing useful, or that puts a lat field
4
+ * somewhere the parser will not look.
5
+ *
6
+ * Both failures are quiet by nature, and both fail OPEN. A misplaced or
7
+ * unparseable `require-code-mention` turns a validation off, and `lat check`
8
+ * then reports success on a file whose leaf sections are no longer required to
9
+ * carry `@lat:` coverage. Nothing else in the output says the rule stopped
10
+ * applying, which is precisely why it has to be said here.
11
+ */
12
+ export declare function checkFrontmatter(latticeDir: string, projectRoot?: string): Promise<CheckError[]>;
@@ -0,0 +1,44 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { dirname, relative } from 'node:path';
3
+ import { listLatticeFiles, parseFrontmatter } from '../lattice.js';
4
+ import { toPosix } from '../walk.js';
5
+ /**
6
+ * Report frontmatter that parses to nothing useful, or that puts a lat field
7
+ * somewhere the parser will not look.
8
+ *
9
+ * Both failures are quiet by nature, and both fail OPEN. A misplaced or
10
+ * unparseable `require-code-mention` turns a validation off, and `lat check`
11
+ * then reports success on a file whose leaf sections are no longer required to
12
+ * carry `@lat:` coverage. Nothing else in the output says the rule stopped
13
+ * applying, which is precisely why it has to be said here.
14
+ */
15
+ export async function checkFrontmatter(latticeDir, projectRoot = dirname(latticeDir)) {
16
+ const files = await listLatticeFiles(latticeDir);
17
+ const errors = [];
18
+ for (const file of files) {
19
+ const content = await readFile(file, 'utf-8');
20
+ const problems = parseFrontmatter(content).problems;
21
+ if (!problems)
22
+ continue;
23
+ const relPath = relative(process.cwd(), file);
24
+ const target = toPosix(relative(projectRoot, file)).replace(/\.md$/, '');
25
+ for (const problem of problems) {
26
+ if (problem.kind === 'root-level-field') {
27
+ errors.push({
28
+ file: relPath,
29
+ line: 1,
30
+ target,
31
+ message: `frontmatter sets "${problem.field}" at the document root, where it is ignored — nest it under "lat:" instead:\n ---\n lat:\n ${problem.field}: ...\n ---`,
32
+ });
33
+ continue;
34
+ }
35
+ errors.push({
36
+ file: relPath,
37
+ line: 1,
38
+ target,
39
+ message: `frontmatter is not valid YAML (${problem.message}) — every lat field in this document is being ignored, including any that turn a check on`,
40
+ });
41
+ }
42
+ }
43
+ return errors;
44
+ }
@@ -0,0 +1,6 @@
1
+ import type { CheckError } from './check.js';
2
+ export declare const DIATAXIS_MODES: readonly ["tutorial", "how-to", "reference", "explanation"];
3
+ export type DiataxisMode = (typeof DIATAXIS_MODES)[number];
4
+ /** Directory below lat.md/ that holds each mode. */
5
+ export declare const MODE_DIRS: Record<DiataxisMode, string>;
6
+ export declare function checkMode(latticeDir: string, projectRoot?: string): Promise<CheckError[]>;
@@ -0,0 +1,303 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { dirname, relative } from 'node:path';
3
+ import { listLatticeFiles, parseFrontmatter } from '../lattice.js';
4
+ import { parse } from '../parser.js';
5
+ import { quoteUntrusted } from '../untrusted.js';
6
+ import { toPosix } from '../walk.js';
7
+ export const DIATAXIS_MODES = [
8
+ 'tutorial',
9
+ 'how-to',
10
+ 'reference',
11
+ 'explanation',
12
+ ];
13
+ /** Directory below lat.md/ that holds each mode. */
14
+ export const MODE_DIRS = {
15
+ tutorial: 'tutorials',
16
+ 'how-to': 'how-to',
17
+ reference: 'reference',
18
+ explanation: 'explanation',
19
+ };
20
+ /** Reverse lookup: first path segment -> mode. Built once, not per file. */
21
+ const DIR_TO_MODE = Object.fromEntries(DIATAXIS_MODES.map((mode) => [MODE_DIRS[mode], mode]));
22
+ const OUTCOME_RE = /\b(outcome|by the end|you will (build|learn|have|create)|what you (will )?(build|learn|have|create))\b/i;
23
+ /**
24
+ * Imperative verbs that flag a command inside an explanation document.
25
+ * Deliberately excludes ambiguous verbs (Use, Go, Make, Type, Enter, Call,
26
+ * Import, Export) — each reads naturally as a noun or as descriptive prose
27
+ * at the start of a sentence ("Use cases include...", "Type inference..."),
28
+ * and including them produced false positives in testing.
29
+ */
30
+ const IMPERATIVE_VERBS = [
31
+ 'Run',
32
+ 'Install',
33
+ 'Add',
34
+ 'Set',
35
+ 'Open',
36
+ 'Create',
37
+ 'Delete',
38
+ 'Edit',
39
+ 'Copy',
40
+ 'Move',
41
+ 'Click',
42
+ 'Press',
43
+ 'Configure',
44
+ 'Update',
45
+ 'Remove',
46
+ 'Execute',
47
+ 'Choose',
48
+ 'Select',
49
+ 'Replace',
50
+ 'Rename',
51
+ 'Restart',
52
+ 'Deploy',
53
+ ];
54
+ const MAX_IMPERATIVE_ERRORS = 10;
55
+ function orderedListPresent(tree) {
56
+ let found = false;
57
+ const walk = (nodes) => {
58
+ for (const node of nodes) {
59
+ if (node.type === 'list' && node.ordered === true) {
60
+ found = true;
61
+ return;
62
+ }
63
+ if ('children' in node) {
64
+ walk(node.children);
65
+ }
66
+ if (found)
67
+ return;
68
+ }
69
+ };
70
+ walk(tree.children);
71
+ return found;
72
+ }
73
+ function outcomeStated(tree) {
74
+ for (const node of tree.children) {
75
+ if (node.type === 'heading' || node.type === 'paragraph') {
76
+ const text = inlineTextOf(node);
77
+ if (OUTCOME_RE.test(text))
78
+ return true;
79
+ }
80
+ }
81
+ return false;
82
+ }
83
+ function inlineTextOf(node) {
84
+ return node.children
85
+ .map((c) => {
86
+ if ('value' in c && typeof c.value === 'string')
87
+ return c.value;
88
+ if ('children' in c)
89
+ return inlineTextOf(c);
90
+ return '';
91
+ })
92
+ .join('');
93
+ }
94
+ /**
95
+ * Extra top-level paragraphs found under headings, beyond the one
96
+ * `checkSections` already permits as the leading summary. A paragraph
97
+ * before the first heading is the document lead, not narrative prose.
98
+ */
99
+ function extraParagraphs(tree) {
100
+ const offenders = [];
101
+ let currentHeading = null;
102
+ let seenParagraphUnderHeading = false;
103
+ for (const node of tree.children) {
104
+ if (node.type === 'heading') {
105
+ currentHeading = inlineTextOf(node);
106
+ seenParagraphUnderHeading = false;
107
+ continue;
108
+ }
109
+ if (node.type === 'paragraph') {
110
+ if (currentHeading === null) {
111
+ // Document lead paragraph before any heading — allowed.
112
+ continue;
113
+ }
114
+ if (!seenParagraphUnderHeading) {
115
+ seenParagraphUnderHeading = true;
116
+ }
117
+ else {
118
+ offenders.push({
119
+ line: node.position.start.line,
120
+ heading: currentHeading,
121
+ });
122
+ }
123
+ }
124
+ }
125
+ return offenders;
126
+ }
127
+ /**
128
+ * Line numbers (1-indexed, inclusive of both ends) that belong to a `code`
129
+ * or `yaml` (frontmatter) node anywhere in the tree. Walking the parsed tree
130
+ * — rather than re-scanning raw text with a fence regex — means a fence of
131
+ * any marker length or an indented code block is recognized correctly, and
132
+ * frontmatter needs no special case: it simply isn't prose in the tree.
133
+ */
134
+ function nonProseLines(tree) {
135
+ const lines = new Set();
136
+ const markRange = (node) => {
137
+ if (!node.position)
138
+ return;
139
+ for (let line = node.position.start.line; line <= node.position.end.line; line++) {
140
+ lines.add(line);
141
+ }
142
+ };
143
+ const walk = (nodes) => {
144
+ for (const node of nodes) {
145
+ if (node.type === 'code' || node.type === 'yaml') {
146
+ markRange(node);
147
+ continue;
148
+ }
149
+ if ('children' in node) {
150
+ walk(node.children);
151
+ }
152
+ }
153
+ };
154
+ walk(tree.children);
155
+ return lines;
156
+ }
157
+ function findImperativeHits(tree, content) {
158
+ const hits = [];
159
+ const lines = content.split('\n');
160
+ const skipLines = nonProseLines(tree);
161
+ for (let i = 0; i < lines.length; i++) {
162
+ const lineNumber = i + 1;
163
+ if (skipLines.has(lineNumber))
164
+ continue;
165
+ const trimmedStart = lines[i].trimStart();
166
+ if (trimmedStart.startsWith('#'))
167
+ continue;
168
+ let text = trimmedStart;
169
+ text = text.replace(/^(?:[-*+]\s+|\d+[.)]\s+)/, '');
170
+ text = text.replace(/^\*\*/, '');
171
+ for (const verb of IMPERATIVE_VERBS) {
172
+ if (text.startsWith(verb + ' ')) {
173
+ hits.push({ line: lineNumber, verb });
174
+ break;
175
+ }
176
+ }
177
+ }
178
+ return hits;
179
+ }
180
+ export async function checkMode(latticeDir, projectRoot = dirname(latticeDir)) {
181
+ const files = await listLatticeFiles(latticeDir);
182
+ const errors = [];
183
+ for (const file of files) {
184
+ const content = await readFile(file, 'utf-8');
185
+ const relPath = relative(process.cwd(), file);
186
+ const target = toPosix(relative(projectRoot, file)).replace(/\.md$/, '');
187
+ const fm = parseFrontmatter(content);
188
+ const declaredRaw = fm.raw['mode'];
189
+ // null (bare `mode:` key) is treated as absent; any other non-string is
190
+ // reported below via declaredNonString.
191
+ const declared = declaredRaw === null || declaredRaw === undefined
192
+ ? undefined
193
+ : typeof declaredRaw === 'string'
194
+ ? declaredRaw
195
+ : undefined;
196
+ const declaredNonString = declaredRaw !== null &&
197
+ declaredRaw !== undefined &&
198
+ typeof declaredRaw !== 'string'
199
+ ? declaredRaw
200
+ : undefined;
201
+ const relFromLattice = toPosix(relative(latticeDir, file));
202
+ const firstSegment = relFromLattice.split('/')[0];
203
+ const dirMode = firstSegment && firstSegment !== relFromLattice
204
+ ? DIR_TO_MODE[firstSegment]
205
+ : undefined;
206
+ if (declaredNonString !== undefined) {
207
+ errors.push({
208
+ file: relPath,
209
+ line: 1,
210
+ target,
211
+ message: `document declares unknown mode ${quoteUntrusted(JSON.stringify(declaredNonString), 40)} — use one of: tutorial, how-to, reference, explanation`,
212
+ });
213
+ continue;
214
+ }
215
+ if (declared !== undefined &&
216
+ !DIATAXIS_MODES.includes(declared)) {
217
+ errors.push({
218
+ file: relPath,
219
+ line: 1,
220
+ target,
221
+ message: `document declares unknown mode ${quoteUntrusted(declared, 40)} — use one of: tutorial, how-to, reference, explanation`,
222
+ });
223
+ continue;
224
+ }
225
+ if (declared !== undefined &&
226
+ dirMode !== undefined &&
227
+ declared !== dirMode) {
228
+ errors.push({
229
+ file: relPath,
230
+ line: 1,
231
+ target,
232
+ message: `document is in lat.md/${firstSegment}/ but declares mode: ${declared} — the mode must match the directory that holds it`,
233
+ });
234
+ continue;
235
+ }
236
+ const mode = (declared ?? dirMode);
237
+ if (mode === undefined)
238
+ continue;
239
+ const tree = parse(content);
240
+ if (mode === 'how-to') {
241
+ if (!orderedListPresent(tree)) {
242
+ errors.push({
243
+ file: relPath,
244
+ line: 1,
245
+ target,
246
+ message: `a how-to must give ordered steps — add a numbered list`,
247
+ });
248
+ }
249
+ continue;
250
+ }
251
+ if (mode === 'tutorial') {
252
+ if (!orderedListPresent(tree)) {
253
+ errors.push({
254
+ file: relPath,
255
+ line: 1,
256
+ target,
257
+ message: `a tutorial must give ordered steps — add a numbered list`,
258
+ });
259
+ }
260
+ if (!outcomeStated(tree)) {
261
+ errors.push({
262
+ file: relPath,
263
+ line: 1,
264
+ target,
265
+ message: `a tutorial must state its outcome — add a heading or sentence saying what the reader will have at the end`,
266
+ });
267
+ }
268
+ continue;
269
+ }
270
+ if (mode === 'reference') {
271
+ for (const offender of extraParagraphs(tree)) {
272
+ errors.push({
273
+ file: relPath,
274
+ line: offender.line,
275
+ target,
276
+ message: `a reference must not contain narrative prose — this is a second paragraph under "${offender.heading}"; use a list, a table, or a code block instead`,
277
+ });
278
+ }
279
+ continue;
280
+ }
281
+ if (mode === 'explanation') {
282
+ const hits = findImperativeHits(tree, content);
283
+ const capped = hits.slice(0, MAX_IMPERATIVE_ERRORS);
284
+ for (const hit of capped) {
285
+ errors.push({
286
+ file: relPath,
287
+ line: hit.line,
288
+ target,
289
+ message: `an explanation must not give commands — line starts with the imperative "${hit.verb}"; describe what happens instead, or move this to a how-to`,
290
+ });
291
+ }
292
+ if (hits.length > MAX_IMPERATIVE_ERRORS) {
293
+ errors.push({
294
+ file: relPath,
295
+ line: 1,
296
+ target,
297
+ message: `capped at ${MAX_IMPERATIVE_ERRORS} imperative-command errors for this document — fix these first, then re-run`,
298
+ });
299
+ }
300
+ }
301
+ }
302
+ return errors;
303
+ }
@@ -0,0 +1,74 @@
1
+ import type { Styler } from '../context.js';
2
+ import type { CheckError } from './check.js';
3
+ /**
4
+ * Who wrote a document, and whether a person has checked it.
5
+ *
6
+ * lat.md states that agents write the graph. Without this field an unmarked
7
+ * claim written by an agent reaches the model carrying exactly the authority
8
+ * of one a person read and approved.
9
+ */
10
+ export declare const DOC_STATUSES: readonly ["human-reviewed", "agent-extracted"];
11
+ export type DocStatus = (typeof DOC_STATUSES)[number];
12
+ export type DocProvenance = {
13
+ /** Raw `status` value, which may not be one this build understands. */
14
+ status?: string;
15
+ /** Raw `reviewed-hash` value, which may not be hex. */
16
+ reviewedHash?: string;
17
+ };
18
+ /**
19
+ * Read the provenance fields out of a document's frontmatter.
20
+ *
21
+ * Fields live under the top-level `lat:` mapping, so they are read off
22
+ * `LatFrontmatter.raw` rather than being promoted to named fields. Adding a
23
+ * named field would mean editing `parseFrontmatter`, and keeping that function
24
+ * closed to per-feature edits is the whole reason `raw` exists.
25
+ */
26
+ export declare function readProvenance(content: string): DocProvenance;
27
+ /**
28
+ * Hash the prose a review actually covered.
29
+ *
30
+ * A plain `status` field becomes a lie the moment an agent edits the document:
31
+ * the field still says a person checked the text, but the text is no longer
32
+ * the text that person read. Capturing the content at review time turns that
33
+ * silent failure into a check.
34
+ *
35
+ * Frontmatter is excluded deliberately: adding an unrelated frontmatter key
36
+ * does not change a single claim in the prose, and invalidating a review for
37
+ * it would train authors to re-stamp the hash without re-reading — which
38
+ * costs the field its meaning.
39
+ *
40
+ * Heading TEXT is excluded for the same reason — re-titling a section changes
41
+ * no claim in the prose. But a heading LINE is not: adding or removing a
42
+ * heading changes the document's structure (a new section exists that no one
43
+ * has reviewed), and that must flip the hash even though no non-heading line
44
+ * changed. So each heading line is reduced to its `#` marker (which carries
45
+ * its level and position) rather than dropped outright — retitling a section
46
+ * leaves the marker alone, but appending or removing a heading changes the
47
+ * sequence of markers and therefore the hash. (Swapping two headings of the
48
+ * same level produces the same marker sequence and is not detected — a
49
+ * narrower version of the blind spot this fix closes.)
50
+ */
51
+ export declare function hashReviewedBody(content: string): string;
52
+ export type ProvenanceNote = {
53
+ kind: 'unreviewed' | 'stale' | 'reviewed';
54
+ text: string;
55
+ };
56
+ /**
57
+ * The one-line provenance annotation for `lat section` and `lat search`, or
58
+ * null when the document says nothing about its provenance.
59
+ *
60
+ * A document with no `status` produces nothing at all. Most documents in most
61
+ * repositories have no status, and a line on every one of them would be noise
62
+ * that readers learn to skip — which would cost the warning its force on the
63
+ * documents that do carry one.
64
+ */
65
+ export declare function provenanceNote(content: string): ProvenanceNote | null;
66
+ /** Colourize a note for terminal output. */
67
+ export declare function formatProvenanceNote(note: ProvenanceNote, s: Styler): string;
68
+ /**
69
+ * Report documents whose recorded review no longer describes their text.
70
+ *
71
+ * This is a static validation. It needs no history and no database, and it
72
+ * catches most of what a temporal graph would answer.
73
+ */
74
+ export declare function checkStatus(latticeDir: string, projectRoot?: string): Promise<CheckError[]>;