@plebeiantech/lat.md 0.12.3-fork.4 → 0.12.3-fork.6
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/dist/src/cli/check-coverage.d.ts +2 -0
- package/dist/src/cli/check-coverage.js +71 -0
- package/dist/src/cli/check-mode.d.ts +6 -0
- package/dist/src/cli/check-mode.js +44 -2
- package/dist/src/cli/check.js +5 -1
- package/dist/src/cli/fork-instructions.d.ts +19 -0
- package/dist/src/cli/fork-instructions.js +183 -0
- package/dist/src/cli/fork-scaffold.d.ts +16 -0
- package/dist/src/cli/fork-scaffold.js +129 -0
- package/dist/src/cli/index.js +29 -4
- package/dist/src/cli/init.js +4 -0
- package/dist/src/cli/search.js +6 -4
- package/dist/src/cli/ui-build.d.ts +4 -0
- package/dist/src/cli/ui-build.js +13 -0
- package/dist/src/cli/ui.js +18 -2
- package/dist/src/code-refs.js +25 -4
- package/dist/src/fork/upstream-drift-cli.d.ts +1 -0
- package/dist/src/fork/upstream-drift-cli.js +2 -0
- package/dist/src/fork/upstream-drift.d.ts +26 -0
- package/dist/src/fork/upstream-drift.js +185 -0
- package/dist/src/fork/upstream-guard-cli.d.ts +1 -0
- package/dist/src/fork/upstream-guard-cli.js +2 -0
- package/dist/src/fork/upstream-guard.d.ts +72 -0
- package/dist/src/fork/upstream-guard.js +445 -0
- package/dist/src/init-version.d.ts +1 -1
- package/dist/src/init-version.js +1 -1
- package/dist/src/lattice.d.ts +2 -1
- package/dist/src/lattice.js +1 -0
- package/dist/src/search/search.d.ts +1 -0
- package/dist/src/search/search.js +15 -9
- package/dist/src/view/client/assets/index-VZwPPiFE.js +334 -0
- package/dist/src/view/client/assets/index-W8ktvNj9.css +1 -0
- package/dist/src/view/client/assets/logo-CNF8zs47.svg +182 -0
- package/dist/src/view/client/index.html +2 -2
- package/dist/src/view/git-diff.js +17 -0
- package/dist/src/view/graph.d.ts +6 -0
- package/dist/src/view/graph.js +176 -0
- package/dist/src/view/markdown.d.ts +5 -1
- package/dist/src/view/markdown.js +112 -25
- package/dist/src/view/protocol.d.ts +43 -0
- package/dist/src/view/protocol.js +1 -1
- package/dist/src/view/references.d.ts +2 -0
- package/dist/src/view/references.js +30 -8
- package/dist/src/view/repository.d.ts +5 -2
- package/dist/src/view/repository.js +13 -12
- package/dist/src/view/search.js +1 -0
- package/dist/src/view/server.d.ts +3 -0
- package/dist/src/view/server.js +42 -9
- package/dist/src/view/source-target.d.ts +8 -0
- package/dist/src/view/source-target.js +22 -0
- package/dist/src/view/static-build.d.ts +16 -0
- package/dist/src/view/static-build.js +408 -0
- package/dist/src/view/static-protocol.d.ts +23 -0
- package/dist/src/view/static-protocol.js +10 -0
- package/dist/src/view/store.d.ts +9 -1
- package/dist/src/view/store.js +82 -15
- package/dist/src/view/table-of-contents.d.ts +9 -0
- package/dist/src/view/table-of-contents.js +103 -0
- package/package.json +3 -1
- package/templates/AGENTS.md +2 -13
- package/templates/fork/conventions.md +95 -0
- package/templates/skill/SKILL.md +3 -14
- package/dist/src/view/client/assets/index-B0n0u03y.css +0 -1
- package/dist/src/view/client/assets/index-xm2_px4q.js +0 -23
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { relative } from 'node:path';
|
|
3
|
+
import { scanCodeRefs } from '../code-refs.js';
|
|
4
|
+
import { listLatticeFiles } from '../lattice.js';
|
|
5
|
+
import { indexNameFor } from './check-mode.js';
|
|
6
|
+
import { toPosix } from '../walk.js';
|
|
7
|
+
import { basename, dirname } from 'node:path';
|
|
8
|
+
/**
|
|
9
|
+
* Fail a `lat.md/` tree that has documents and no `@lat:` ref pointing into it
|
|
10
|
+
* from anywhere in the codebase.
|
|
11
|
+
*
|
|
12
|
+
* `require-code-mention` was the only thing asking for refs, and it is opt-in
|
|
13
|
+
* frontmatter: an agent setting up a tree can simply not write it, and every
|
|
14
|
+
* check still passes. One did. Its reasons were both ordinary — it read a list
|
|
15
|
+
* of example comment markers as an exclusive language list and concluded its
|
|
16
|
+
* language was unsupported, and it was optimising for a green check rather
|
|
17
|
+
* than a correct one, so the option that could not fail was the one it took.
|
|
18
|
+
*
|
|
19
|
+
* A graph nothing points at cannot be reached from the code, which is the only
|
|
20
|
+
* reason it exists. So the floor is one ref, and the message says where to put
|
|
21
|
+
* the first one.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately a floor and not a coverage ratio. Half this repository's own
|
|
24
|
+
* documents have no incoming ref and should not have one — a per-document rule
|
|
25
|
+
* would have to be either wrong here or watered down everywhere.
|
|
26
|
+
*/
|
|
27
|
+
/** Nothing to anchor: a tree of pure navigation, or a project with no code. */
|
|
28
|
+
function isIndex(latticeDir, file) {
|
|
29
|
+
const holder = basename(dirname(file));
|
|
30
|
+
return (basename(file) ===
|
|
31
|
+
indexNameFor(holder === '' ? basename(latticeDir) : holder));
|
|
32
|
+
}
|
|
33
|
+
export async function checkCoverage(latticeDir, projectRoot = dirname(latticeDir)) {
|
|
34
|
+
const files = await listLatticeFiles(latticeDir);
|
|
35
|
+
const documents = files.filter((f) => !isIndex(latticeDir, f));
|
|
36
|
+
if (documents.length === 0)
|
|
37
|
+
return [];
|
|
38
|
+
const scan = await scanCodeRefs(projectRoot);
|
|
39
|
+
if (scan.files.length === 0)
|
|
40
|
+
return [];
|
|
41
|
+
if (scan.refs.length > 0)
|
|
42
|
+
return [];
|
|
43
|
+
const rootIndexPath = `${latticeDir}/${indexNameFor(basename(latticeDir))}`;
|
|
44
|
+
const rootIndexRel = toPosix(relative(process.cwd(), rootIndexPath));
|
|
45
|
+
// Quote the ref the reader should actually paste. A generic placeholder gets
|
|
46
|
+
// pasted verbatim and then fails `lat check code-refs`, which turns one
|
|
47
|
+
// error into two.
|
|
48
|
+
const stem = basename(latticeDir).replace(/\.md$/, '');
|
|
49
|
+
let anchor = `${stem}#Your Project`;
|
|
50
|
+
try {
|
|
51
|
+
const heading = (await readFile(rootIndexPath, 'utf-8')).match(/^#\s+(.+)$/m);
|
|
52
|
+
if (heading)
|
|
53
|
+
anchor = `${stem}#${heading[1].trim()}`;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// No root index, or unreadable — the placeholder still names the shape.
|
|
57
|
+
}
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
file: rootIndexRel,
|
|
61
|
+
line: 1,
|
|
62
|
+
target: toPosix(relative(projectRoot, latticeDir)),
|
|
63
|
+
message: `${documents.length} document(s) here and no \`@lat:\` code ref anywhere in the project — nothing in the codebase can reach this graph.\n` +
|
|
64
|
+
' Start at the application entrypoint (config/application.rb, src/index.ts, main.go, manage.py) and point it at the root index:\n' +
|
|
65
|
+
` # @lat: [[${anchor}]]\n` +
|
|
66
|
+
' Then add one ref per section that source code implements, at that code.\n' +
|
|
67
|
+
' The marker is a comment syntax, not a language allowlist: `//` for C-family, `#` for Ruby, Python, shell, Elixir and the rest. The scan is textual and reads every non-markdown file.\n' +
|
|
68
|
+
' An `@lat:` line is a machine directive, like a magic comment or a linter pragma. A project convention that minimises comments does not reach it.',
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
}
|
|
@@ -3,4 +3,10 @@ export declare const DIATAXIS_MODES: readonly ["tutorial", "how-to", "reference"
|
|
|
3
3
|
export type DiataxisMode = (typeof DIATAXIS_MODES)[number];
|
|
4
4
|
/** Directory below lat.md/ that holds each mode. */
|
|
5
5
|
export declare const MODE_DIRS: Record<DiataxisMode, string>;
|
|
6
|
+
/**
|
|
7
|
+
* The index file a directory is required to carry, e.g. `lat.md` for the root
|
|
8
|
+
* and `reference.md` for `reference/`. An index is navigation rather than
|
|
9
|
+
* content, so the `require-mode` gate below never asks one to declare a mode.
|
|
10
|
+
*/
|
|
11
|
+
export declare function indexNameFor(dirName: string): string;
|
|
6
12
|
export declare function checkMode(latticeDir: string, projectRoot?: string): Promise<CheckError[]>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { dirname, relative } from 'node:path';
|
|
2
|
+
import { basename, dirname, join, relative } from 'node:path';
|
|
3
3
|
import { listLatticeFiles, parseFrontmatter } from '../lattice.js';
|
|
4
4
|
import { parse } from '../parser.js';
|
|
5
5
|
import { quoteUntrusted } from '../untrusted.js';
|
|
@@ -181,9 +181,38 @@ function findImperativeHits(tree, content) {
|
|
|
181
181
|
}
|
|
182
182
|
return hits;
|
|
183
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* The index file a directory is required to carry, e.g. `lat.md` for the root
|
|
186
|
+
* and `reference.md` for `reference/`. An index is navigation rather than
|
|
187
|
+
* content, so the `require-mode` gate below never asks one to declare a mode.
|
|
188
|
+
*/
|
|
189
|
+
export function indexNameFor(dirName) {
|
|
190
|
+
return dirName.endsWith('.md') ? dirName : dirName + '.md';
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Whether the root index turns the mode requirement on.
|
|
194
|
+
*
|
|
195
|
+
* Opt-in, and deliberately read from the tree rather than from a flag or an
|
|
196
|
+
* environment variable: the rule belongs to a documentation set, not to a
|
|
197
|
+
* machine or a CI job, and a tree that predates the rule has to keep passing.
|
|
198
|
+
* `lat init` stamps the flag into the root index it scaffolds, so a project
|
|
199
|
+
* set up after this exists is gated from its first commit while an older one
|
|
200
|
+
* is untouched until someone adds the line.
|
|
201
|
+
*/
|
|
202
|
+
async function requireModeEnabled(latticeDir) {
|
|
203
|
+
const indexPath = join(latticeDir, indexNameFor(basename(latticeDir)));
|
|
204
|
+
try {
|
|
205
|
+
const content = await readFile(indexPath, 'utf-8');
|
|
206
|
+
return parseFrontmatter(content).raw['require-mode'] === true;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
184
212
|
export async function checkMode(latticeDir, projectRoot = dirname(latticeDir)) {
|
|
185
213
|
const files = await listLatticeFiles(latticeDir);
|
|
186
214
|
const errors = [];
|
|
215
|
+
const requireMode = await requireModeEnabled(latticeDir);
|
|
187
216
|
for (const file of files) {
|
|
188
217
|
const content = await readFile(file, 'utf-8');
|
|
189
218
|
const relPath = relative(process.cwd(), file);
|
|
@@ -238,8 +267,21 @@ export async function checkMode(latticeDir, projectRoot = dirname(latticeDir)) {
|
|
|
238
267
|
continue;
|
|
239
268
|
}
|
|
240
269
|
const mode = (declared ?? dirMode);
|
|
241
|
-
if (mode === undefined)
|
|
270
|
+
if (mode === undefined) {
|
|
271
|
+
const segments = relFromLattice.split('/');
|
|
272
|
+
const fileName = segments.pop();
|
|
273
|
+
const holder = segments.length === 0 ? basename(latticeDir) : segments.at(-1);
|
|
274
|
+
const isIndex = fileName === indexNameFor(holder);
|
|
275
|
+
if (requireMode && !isIndex) {
|
|
276
|
+
errors.push({
|
|
277
|
+
file: relPath,
|
|
278
|
+
line: 1,
|
|
279
|
+
target,
|
|
280
|
+
message: 'document declares no Diátaxis mode and is not in a mode directory — move it under lat.md/tutorials/, lat.md/how-to/, lat.md/reference/ or lat.md/explanation/, or declare one under its `lat:` frontmatter.\n A document that would fail its mode is usually two documents: the lookup half belongs in reference/, the reasoning half in explanation/.\n (The root index sets require-mode: true. Remove that line to turn this off.)',
|
|
281
|
+
});
|
|
282
|
+
}
|
|
242
283
|
continue;
|
|
284
|
+
}
|
|
243
285
|
const tree = parse(content);
|
|
244
286
|
if (mode === 'how-to') {
|
|
245
287
|
if (!orderedListPresent(tree)) {
|
package/dist/src/cli/check.js
CHANGED
|
@@ -6,6 +6,7 @@ import { scanCodeRefs } from '../code-refs.js';
|
|
|
6
6
|
import { checkMode } from './check-mode.js';
|
|
7
7
|
import { checkStatus } from './check-status.js';
|
|
8
8
|
import { checkFrontmatter } from './check-frontmatter.js';
|
|
9
|
+
import { checkCoverage } from './check-coverage.js';
|
|
9
10
|
import { indexEntryNameFromDest } from './link-scheme.js';
|
|
10
11
|
import { fencedLineMask } from './gen-index.js';
|
|
11
12
|
import { SOURCE_EXTENSIONS, clearSymbolCache } from '../source-parser.js';
|
|
@@ -621,6 +622,7 @@ export async function checkAllCommand(ctx, opts = {}) {
|
|
|
621
622
|
const modeErrors = await checkMode(ctx.latDir, ctx.projectRoot);
|
|
622
623
|
const statusErrors = await checkStatus(ctx.latDir, ctx.projectRoot);
|
|
623
624
|
const fmErrors = await checkFrontmatter(ctx.latDir, ctx.projectRoot);
|
|
625
|
+
const coverageErrors = await checkCoverage(ctx.latDir, ctx.projectRoot);
|
|
624
626
|
const elapsed = Date.now() - startTime;
|
|
625
627
|
const allErrors = [...md.errors, ...linkErrors, ...code.errors];
|
|
626
628
|
const allFiles = { ...md.files };
|
|
@@ -658,12 +660,14 @@ export async function checkAllCommand(ctx, opts = {}) {
|
|
|
658
660
|
lines.push(...formatCheckErrors(modeErrors, s));
|
|
659
661
|
lines.push(...formatCheckErrors(statusErrors, s));
|
|
660
662
|
lines.push(...formatCheckErrors(fmErrors, s));
|
|
663
|
+
lines.push(...formatCheckErrors(coverageErrors, s));
|
|
661
664
|
const totalErrors = allErrors.length +
|
|
662
665
|
indexErrors.length +
|
|
663
666
|
sectionErrors.length +
|
|
664
667
|
modeErrors.length +
|
|
665
668
|
statusErrors.length +
|
|
666
|
-
fmErrors.length
|
|
669
|
+
fmErrors.length +
|
|
670
|
+
coverageErrors.length;
|
|
667
671
|
if (totalErrors > 0) {
|
|
668
672
|
lines.push(formatErrorCount(totalErrors, s));
|
|
669
673
|
return { output: lines.join('\n'), isError: true };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare function readForkConventions(): string;
|
|
2
|
+
/**
|
|
3
|
+
* Replace the fork block in `content`, or append one when absent.
|
|
4
|
+
*
|
|
5
|
+
* Returns null when the block is already byte-identical, so the caller can
|
|
6
|
+
* stay quiet rather than announce a write that did not happen.
|
|
7
|
+
*/
|
|
8
|
+
export declare function spliceForkBlock(content: string, body: string): string | null;
|
|
9
|
+
/** Read the body currently fenced by the fork markers, or null if absent. */
|
|
10
|
+
export declare function extractForkBlock(content: string): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Write the fork's conventions into every instruction file and skill directory
|
|
13
|
+
* the selected agents produced.
|
|
14
|
+
*
|
|
15
|
+
* `fileHashes` is upstream's record, threaded through so `writeInitMeta`
|
|
16
|
+
* persists these entries alongside its own; a stored hash is what lets a later
|
|
17
|
+
* run tell "the user edited our block" from "the template moved on".
|
|
18
|
+
*/
|
|
19
|
+
export declare function writeForkInstructions(root: string, latDir: string, fileHashes: Record<string, string>, ask: (message: string) => Promise<boolean>): Promise<void>;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { styleText } from 'node:util';
|
|
4
|
+
import { contentHash, readFileHash } from '../init-version.js';
|
|
5
|
+
import { findTemplatesDir } from './templates.js';
|
|
6
|
+
/**
|
|
7
|
+
* The fork's own instruction channel into a consumer project.
|
|
8
|
+
*
|
|
9
|
+
* Upstream teaches a consumer's agent through `templates/AGENTS.md` and
|
|
10
|
+
* `templates/skill/SKILL.md`. Both are upstream files, and both are the ones
|
|
11
|
+
* upstream is most likely to rewrite, so every sentence this fork adds to them
|
|
12
|
+
* is a merge conflict bought on credit. The fork's Diátaxis block cost 13
|
|
13
|
+
* lines in one and 14 in the other before this module existed.
|
|
14
|
+
*
|
|
15
|
+
* Everything here writes into fork-owned paths instead. The entire upstream
|
|
16
|
+
* footprint is one import and one call in `initCmd`.
|
|
17
|
+
*
|
|
18
|
+
* Two shapes, because the destinations behave differently:
|
|
19
|
+
*
|
|
20
|
+
* - Always-loaded instruction files (`CLAUDE.md`, `AGENTS.md`,
|
|
21
|
+
* `.github/copilot-instructions.md`) already carry upstream's
|
|
22
|
+
* `%% lat:begin %%` block, so this appends a *second* block under its own
|
|
23
|
+
* `%% lat-fork:begin %%` markers. Upstream's `appendTemplateSection` slices
|
|
24
|
+
* between its own markers and leaves anything after them intact, so the two
|
|
25
|
+
* blocks do not fight on re-run.
|
|
26
|
+
* - Skill directories get a separate `lat-md-conventions/SKILL.md`. Upstream
|
|
27
|
+
* writes its `lat-md` skill with a whole-file hash comparison, so appending
|
|
28
|
+
* to that file would make every later `lat init` report it as
|
|
29
|
+
* user-modified and prompt.
|
|
30
|
+
*/
|
|
31
|
+
const FORK_BEGIN = '%% lat-fork:begin %%';
|
|
32
|
+
const FORK_END = '%% lat-fork:end %%';
|
|
33
|
+
/**
|
|
34
|
+
* Hash-map key for a fork block inside a file upstream also owns. Suffixed so
|
|
35
|
+
* it can never collide with upstream's entry for the same path — both live in
|
|
36
|
+
* one flat `file_hashes` record.
|
|
37
|
+
*/
|
|
38
|
+
function forkHashKey(relPath) {
|
|
39
|
+
return `${relPath}#lat-fork`;
|
|
40
|
+
}
|
|
41
|
+
/** Instruction files that use upstream's marker-append shape. */
|
|
42
|
+
const BLOCK_TARGETS = [
|
|
43
|
+
'CLAUDE.md',
|
|
44
|
+
'AGENTS.md',
|
|
45
|
+
'.github/copilot-instructions.md',
|
|
46
|
+
];
|
|
47
|
+
/**
|
|
48
|
+
* Skill roots every agent integration installs into. Written to only when the
|
|
49
|
+
* directory already exists, which is what keeps this list from creating skill
|
|
50
|
+
* directories for agents the user did not select.
|
|
51
|
+
*/
|
|
52
|
+
const SKILL_ROOTS = [
|
|
53
|
+
'.claude/skills',
|
|
54
|
+
'.pi/skills',
|
|
55
|
+
'.agents/skills',
|
|
56
|
+
'.codex/skills',
|
|
57
|
+
];
|
|
58
|
+
const SKILL_NAME = 'lat-md-conventions';
|
|
59
|
+
const SKILL_FRONTMATTER = `---
|
|
60
|
+
name: ${SKILL_NAME}
|
|
61
|
+
description: >-
|
|
62
|
+
Rules lat check enforces in this project: Diátaxis mode placement for every
|
|
63
|
+
lat.md/ document, mandatory @lat: code refs starting at the application
|
|
64
|
+
entrypoint, and test specs wired to their tests. Load alongside lat-md
|
|
65
|
+
whenever creating or editing files under lat.md/.
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
`;
|
|
69
|
+
export function readForkConventions() {
|
|
70
|
+
return readFileSync(join(findTemplatesDir(), 'fork', 'conventions.md'), 'utf-8');
|
|
71
|
+
}
|
|
72
|
+
function wrapBlock(body) {
|
|
73
|
+
return `${FORK_BEGIN}\n${body}${body.endsWith('\n') ? '' : '\n'}${FORK_END}\n`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Replace the fork block in `content`, or append one when absent.
|
|
77
|
+
*
|
|
78
|
+
* Returns null when the block is already byte-identical, so the caller can
|
|
79
|
+
* stay quiet rather than announce a write that did not happen.
|
|
80
|
+
*/
|
|
81
|
+
export function spliceForkBlock(content, body) {
|
|
82
|
+
const wrapped = wrapBlock(body);
|
|
83
|
+
const begin = content.indexOf(FORK_BEGIN);
|
|
84
|
+
const end = content.indexOf(FORK_END);
|
|
85
|
+
if (begin === -1 || end === -1 || end <= begin) {
|
|
86
|
+
let base = content;
|
|
87
|
+
if (base.length > 0 && !base.endsWith('\n'))
|
|
88
|
+
base += '\n';
|
|
89
|
+
return base + (base.length > 0 ? '\n' : '') + wrapped;
|
|
90
|
+
}
|
|
91
|
+
const endWithMarker = end + FORK_END.length;
|
|
92
|
+
const endWithNl = content[endWithMarker] === '\n' ? endWithMarker + 1 : endWithMarker;
|
|
93
|
+
const existing = content.slice(begin, endWithNl);
|
|
94
|
+
if (existing === wrapped)
|
|
95
|
+
return null;
|
|
96
|
+
return content.slice(0, begin) + wrapped + content.slice(endWithNl);
|
|
97
|
+
}
|
|
98
|
+
/** Read the body currently fenced by the fork markers, or null if absent. */
|
|
99
|
+
export function extractForkBlock(content) {
|
|
100
|
+
const begin = content.indexOf(FORK_BEGIN);
|
|
101
|
+
const end = content.indexOf(FORK_END);
|
|
102
|
+
if (begin === -1 || end === -1 || end <= begin)
|
|
103
|
+
return null;
|
|
104
|
+
return content.slice(begin + FORK_BEGIN.length + 1, end);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Write the fork's conventions into every instruction file and skill directory
|
|
108
|
+
* the selected agents produced.
|
|
109
|
+
*
|
|
110
|
+
* `fileHashes` is upstream's record, threaded through so `writeInitMeta`
|
|
111
|
+
* persists these entries alongside its own; a stored hash is what lets a later
|
|
112
|
+
* run tell "the user edited our block" from "the template moved on".
|
|
113
|
+
*/
|
|
114
|
+
export async function writeForkInstructions(root, latDir, fileHashes, ask) {
|
|
115
|
+
const body = readForkConventions();
|
|
116
|
+
const bodyHash = contentHash(body);
|
|
117
|
+
const written = [];
|
|
118
|
+
for (const relPath of BLOCK_TARGETS) {
|
|
119
|
+
const absPath = join(root, relPath);
|
|
120
|
+
if (!existsSync(absPath))
|
|
121
|
+
continue;
|
|
122
|
+
const current = readFileSync(absPath, 'utf-8');
|
|
123
|
+
const existingBlock = extractForkBlock(current);
|
|
124
|
+
if (existingBlock !== null) {
|
|
125
|
+
const existingHash = contentHash(existingBlock);
|
|
126
|
+
if (existingHash === bodyHash) {
|
|
127
|
+
fileHashes[forkHashKey(relPath)] = bodyHash;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const storedHash = readFileHash(latDir, forkHashKey(relPath));
|
|
131
|
+
if (storedHash && existingHash !== storedHash) {
|
|
132
|
+
console.log(styleText('yellow', ` ${relPath}`) +
|
|
133
|
+
' lat-fork section has been modified.');
|
|
134
|
+
if (!(await ask(' Replace lat-fork section with latest template?'))) {
|
|
135
|
+
console.log(styleText('dim', ' Kept existing section.'));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const updated = spliceForkBlock(current, body);
|
|
141
|
+
if (updated === null) {
|
|
142
|
+
fileHashes[forkHashKey(relPath)] = bodyHash;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
writeFileSync(absPath, updated);
|
|
146
|
+
fileHashes[forkHashKey(relPath)] = bodyHash;
|
|
147
|
+
written.push(relPath);
|
|
148
|
+
}
|
|
149
|
+
const skillBody = SKILL_FRONTMATTER + body;
|
|
150
|
+
const skillHash = contentHash(skillBody);
|
|
151
|
+
for (const skillRoot of SKILL_ROOTS) {
|
|
152
|
+
const rootAbs = join(root, skillRoot);
|
|
153
|
+
if (!existsSync(rootAbs))
|
|
154
|
+
continue;
|
|
155
|
+
const relPath = `${skillRoot}/${SKILL_NAME}/SKILL.md`;
|
|
156
|
+
const absPath = join(root, relPath);
|
|
157
|
+
if (existsSync(absPath)) {
|
|
158
|
+
const current = readFileSync(absPath, 'utf-8');
|
|
159
|
+
if (contentHash(current) === skillHash) {
|
|
160
|
+
fileHashes[relPath] = skillHash;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const storedHash = readFileHash(latDir, relPath);
|
|
164
|
+
if (storedHash && contentHash(current) !== storedHash) {
|
|
165
|
+
console.log(styleText('yellow', ` ${relPath}`) + ' has been modified.');
|
|
166
|
+
if (!(await ask(' Overwrite with latest lat template?'))) {
|
|
167
|
+
console.log(styleText('dim', ' Kept existing file.'));
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
mkdirSync(join(absPath, '..'), { recursive: true });
|
|
173
|
+
writeFileSync(absPath, skillBody);
|
|
174
|
+
fileHashes[relPath] = skillHash;
|
|
175
|
+
written.push(relPath);
|
|
176
|
+
}
|
|
177
|
+
if (written.length > 0) {
|
|
178
|
+
console.log('');
|
|
179
|
+
console.log(styleText('green', ' lat.md conventions') +
|
|
180
|
+
' written to ' +
|
|
181
|
+
written.join(', '));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Prepend the `require-mode` flag to a root index that has no frontmatter. */
|
|
2
|
+
export declare function stampRequireMode(content: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Append the four mode directories to the root index.
|
|
5
|
+
*
|
|
6
|
+
* Without this a fresh `lat init` leaves `lat check index` reporting four
|
|
7
|
+
* missing entries, which is a poor first impression of a tool whose whole
|
|
8
|
+
* pitch is that the check passes.
|
|
9
|
+
*/
|
|
10
|
+
export declare function listModeDirs(content: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Scaffold the four mode directories and turn the gate on. Called only on the
|
|
13
|
+
* run that creates `lat.md/`, so an existing tree is never restructured
|
|
14
|
+
* underneath its owner.
|
|
15
|
+
*/
|
|
16
|
+
export declare function writeForkScaffold(latDir: string): void;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { styleText } from 'node:util';
|
|
4
|
+
import { DIATAXIS_MODES, MODE_DIRS, indexNameFor } from './check-mode.js';
|
|
5
|
+
/**
|
|
6
|
+
* Give a freshly created `lat.md/` the Diátaxis shape, and turn the gate on.
|
|
7
|
+
*
|
|
8
|
+
* Upstream's `templates/init/` holds two files and is copied wholesale, so a
|
|
9
|
+
* new tree is one flat root index. An agent asked to document a codebase then
|
|
10
|
+
* writes flat files beside it — which is not merely untidy: `checkMode` binds
|
|
11
|
+
* only inside the four mode directories, so flat placement puts every document
|
|
12
|
+
* out of reach of every shape rule. A bella-derms session did exactly that and
|
|
13
|
+
* confirmed afterwards that its largest document produced 64 errors the moment
|
|
14
|
+
* it was moved into `reference/`.
|
|
15
|
+
*
|
|
16
|
+
* Structure is the cheaper half of the fix. Four directories that already
|
|
17
|
+
* exist, each with a passing index, make placement the path of least
|
|
18
|
+
* resistance; `require-mode: true` in the root index makes flat placement an
|
|
19
|
+
* error rather than an option.
|
|
20
|
+
*
|
|
21
|
+
* Written from here rather than added to `templates/init/` because the flag
|
|
22
|
+
* has to land inside `lat.md/lat.md`, which upstream's template owns.
|
|
23
|
+
*/
|
|
24
|
+
/** One-line summary and body for each mode's index, keyed by directory. */
|
|
25
|
+
const MODE_INDEX = {
|
|
26
|
+
tutorials: {
|
|
27
|
+
title: 'Tutorials',
|
|
28
|
+
lead: 'Guided first passes through this system, each ending somewhere the reader can see.',
|
|
29
|
+
},
|
|
30
|
+
'how-to': {
|
|
31
|
+
title: 'How-to',
|
|
32
|
+
lead: 'Recipes for tasks that recur, written for someone who already knows what they want.',
|
|
33
|
+
},
|
|
34
|
+
reference: {
|
|
35
|
+
title: 'Reference',
|
|
36
|
+
lead: 'Lookup facts about this system: fields, states, limits, names and defaults.',
|
|
37
|
+
},
|
|
38
|
+
explanation: {
|
|
39
|
+
title: 'Explanation',
|
|
40
|
+
lead: 'Why this system is shaped the way it is, and what the alternatives cost.',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* A mode index has to satisfy its own mode. A tutorial index needs ordered
|
|
45
|
+
* steps and a stated outcome, a how-to index needs ordered steps, and a
|
|
46
|
+
* reference index must not carry a second paragraph — so each stub is written
|
|
47
|
+
* to the rule it will be checked against rather than to one house style.
|
|
48
|
+
*/
|
|
49
|
+
function indexBody(dir) {
|
|
50
|
+
const { title, lead } = MODE_INDEX[dir];
|
|
51
|
+
const header = `# ${title}\n\n${lead}\n`;
|
|
52
|
+
if (dir === 'tutorials') {
|
|
53
|
+
return (header +
|
|
54
|
+
'\nBy the end of any document here the reader will have run something and seen it work.\n' +
|
|
55
|
+
'\n1. Pick the tutorial closest to what you are trying to learn.\n' +
|
|
56
|
+
'2. Follow it top to bottom without skipping.\n' +
|
|
57
|
+
'3. Come back to `reference/` for the details it left out.\n');
|
|
58
|
+
}
|
|
59
|
+
if (dir === 'how-to') {
|
|
60
|
+
return (header +
|
|
61
|
+
'\n1. Find the recipe for the task in front of you.\n' +
|
|
62
|
+
'2. Follow its steps.\n' +
|
|
63
|
+
'3. If no recipe fits, write one here once the task is done.\n');
|
|
64
|
+
}
|
|
65
|
+
if (dir === 'reference') {
|
|
66
|
+
return header;
|
|
67
|
+
}
|
|
68
|
+
return (header +
|
|
69
|
+
'\nEach document here answers a "why", not a "how". The steps that follow ' +
|
|
70
|
+
'from a decision belong in `how-to/`, and the facts it fixes belong in ' +
|
|
71
|
+
'`reference/`.\n');
|
|
72
|
+
}
|
|
73
|
+
/** Prepend the `require-mode` flag to a root index that has no frontmatter. */
|
|
74
|
+
export function stampRequireMode(content) {
|
|
75
|
+
if (/^---\n/.test(content))
|
|
76
|
+
return content;
|
|
77
|
+
return `---\nlat:\n require-mode: true\n---\n\n${content.replace(/^\n+/, '')}`;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Append the four mode directories to the root index.
|
|
81
|
+
*
|
|
82
|
+
* Without this a fresh `lat init` leaves `lat check index` reporting four
|
|
83
|
+
* missing entries, which is a poor first impression of a tool whose whole
|
|
84
|
+
* pitch is that the check passes.
|
|
85
|
+
*/
|
|
86
|
+
export function listModeDirs(content) {
|
|
87
|
+
const entries = DIATAXIS_MODES.map((mode) => {
|
|
88
|
+
const dir = MODE_DIRS[mode];
|
|
89
|
+
const { title, lead } = MODE_INDEX[dir];
|
|
90
|
+
return `- [${title}](${dir}/${dir}.md) — ${lead}`;
|
|
91
|
+
}).join('\n');
|
|
92
|
+
if (content.includes(`(${MODE_DIRS.reference}/`))
|
|
93
|
+
return content;
|
|
94
|
+
let base = content;
|
|
95
|
+
if (!base.endsWith('\n'))
|
|
96
|
+
base += '\n';
|
|
97
|
+
return `${base}\nEvery document belongs in exactly one of these.\n\n${entries}\n`;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Scaffold the four mode directories and turn the gate on. Called only on the
|
|
101
|
+
* run that creates `lat.md/`, so an existing tree is never restructured
|
|
102
|
+
* underneath its owner.
|
|
103
|
+
*/
|
|
104
|
+
export function writeForkScaffold(latDir) {
|
|
105
|
+
const created = [];
|
|
106
|
+
for (const mode of DIATAXIS_MODES) {
|
|
107
|
+
const dir = MODE_DIRS[mode];
|
|
108
|
+
const dirPath = join(latDir, dir);
|
|
109
|
+
const indexPath = join(dirPath, `${dir}.md`);
|
|
110
|
+
if (existsSync(indexPath))
|
|
111
|
+
continue;
|
|
112
|
+
mkdirSync(dirPath, { recursive: true });
|
|
113
|
+
writeFileSync(indexPath, indexBody(dir));
|
|
114
|
+
created.push(`${dir}/`);
|
|
115
|
+
}
|
|
116
|
+
const rootIndex = join(latDir, indexNameFor(basename(latDir)));
|
|
117
|
+
if (existsSync(rootIndex)) {
|
|
118
|
+
const current = readFileSync(rootIndex, 'utf-8');
|
|
119
|
+
const stamped = listModeDirs(stampRequireMode(current));
|
|
120
|
+
if (stamped !== current)
|
|
121
|
+
writeFileSync(rootIndex, stamped);
|
|
122
|
+
}
|
|
123
|
+
if (created.length > 0) {
|
|
124
|
+
console.log(styleText('green', 'Scaffolded Diátaxis modes') +
|
|
125
|
+
' — ' +
|
|
126
|
+
created.join(' ') +
|
|
127
|
+
styleText('dim', ' (every document belongs in one of these)'));
|
|
128
|
+
}
|
|
129
|
+
}
|
package/dist/src/cli/index.js
CHANGED
|
@@ -6,8 +6,18 @@ if (!process.argv.includes('--verbose')) {
|
|
|
6
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
7
|
import { dirname, join } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { Command } from 'commander';
|
|
9
|
+
import { Command, InvalidArgumentError } from 'commander';
|
|
10
10
|
import { resolveCheckContext, resolveContext } from './context.js';
|
|
11
|
+
function parsePort(value) {
|
|
12
|
+
if (!/^\d+$/.test(value)) {
|
|
13
|
+
throw new InvalidArgumentError('port must be an integer from 1 to 65535');
|
|
14
|
+
}
|
|
15
|
+
const port = Number(value);
|
|
16
|
+
if (port < 1 || port > 65_535) {
|
|
17
|
+
throw new InvalidArgumentError('port must be an integer from 1 to 65535');
|
|
18
|
+
}
|
|
19
|
+
return port;
|
|
20
|
+
}
|
|
11
21
|
/** Reserve `-- <directory>` for an explicit check target. */
|
|
12
22
|
function splitCheckTarget(args) {
|
|
13
23
|
let commandIndex = -1;
|
|
@@ -90,13 +100,28 @@ program
|
|
|
90
100
|
const { sectionCommand } = await import('./section.js');
|
|
91
101
|
handleResult(await sectionCommand(ctx, query));
|
|
92
102
|
});
|
|
93
|
-
program
|
|
103
|
+
const ui = program
|
|
94
104
|
.command('ui')
|
|
95
105
|
.description('Open lat.md in a local browser')
|
|
96
|
-
.
|
|
106
|
+
.option('--logo-text <text>', 'top-left logo text')
|
|
107
|
+
.option('--port <number>', 'server port (default: 4242; explicit ports are strict)', parsePort)
|
|
108
|
+
.action(async (opts) => {
|
|
97
109
|
const ctx = resolveContext(program.opts());
|
|
98
110
|
const { uiCommand } = await import('./ui.js');
|
|
99
|
-
handleResult(await uiCommand(ctx));
|
|
111
|
+
handleResult(await uiCommand(ctx, { logoText: opts.logoText, port: opts.port }));
|
|
112
|
+
});
|
|
113
|
+
ui.command('build')
|
|
114
|
+
.description('Export lat.md as a static website')
|
|
115
|
+
.argument('[output]', 'output directory relative to the project', 'lat-ui')
|
|
116
|
+
.option('--base <path>', 'deployment base path', '/')
|
|
117
|
+
.option('--logo-text <text>', 'top-left logo text')
|
|
118
|
+
.action(async (output, opts) => {
|
|
119
|
+
const ctx = resolveContext(program.opts());
|
|
120
|
+
const { uiBuildCommand } = await import('./ui-build.js');
|
|
121
|
+
handleResult(await uiBuildCommand(ctx, output, {
|
|
122
|
+
basePath: opts.base,
|
|
123
|
+
logoText: opts.logoText ?? ui.opts().logoText,
|
|
124
|
+
}));
|
|
100
125
|
});
|
|
101
126
|
program
|
|
102
127
|
.command('refs')
|
package/dist/src/cli/init.js
CHANGED
|
@@ -15,6 +15,8 @@ import { INIT_VERSION, writeInitMeta, readInitVersion, readFileHash, contentHash
|
|
|
15
15
|
import { getLocalVersion, fetchLatestVersion } from '../version.js';
|
|
16
16
|
import { selectMenu } from './select-menu.js';
|
|
17
17
|
import { checklistMenu } from './checklist-menu.js';
|
|
18
|
+
import { writeForkInstructions } from './fork-instructions.js';
|
|
19
|
+
import { writeForkScaffold } from './fork-scaffold.js';
|
|
18
20
|
async function confirm(rl, message) {
|
|
19
21
|
while (true) {
|
|
20
22
|
let answer;
|
|
@@ -900,6 +902,7 @@ export async function initCmd(targetDir) {
|
|
|
900
902
|
mkdirSync(latDir, { recursive: true });
|
|
901
903
|
cpSync(templateDir, latDir, { recursive: true });
|
|
902
904
|
console.log(styleText('green', 'Created lat.md/'));
|
|
905
|
+
writeForkScaffold(latDir);
|
|
903
906
|
}
|
|
904
907
|
// Step 2: Configure fresh/outdated setups, ask interactive users about an
|
|
905
908
|
// available key, and offer to rebuild an index whose backend differs. This
|
|
@@ -1004,6 +1007,7 @@ export async function initCmd(targetDir) {
|
|
|
1004
1007
|
console.log(styleText('bold', 'Setting up Codex...'));
|
|
1005
1008
|
await setupCodex(root, latDir, fileHashes, ask, commandStyle);
|
|
1006
1009
|
}
|
|
1010
|
+
await writeForkInstructions(root, latDir, fileHashes, ask);
|
|
1007
1011
|
// Record init version and file hashes so `lat check` can detect stale setups
|
|
1008
1012
|
writeInitMeta(latDir, fileHashes);
|
|
1009
1013
|
console.log('');
|
package/dist/src/cli/search.js
CHANGED
|
@@ -73,10 +73,12 @@ async function resolveMatches(latDir, results, preloadedSections) {
|
|
|
73
73
|
const allSections = preloadedSections ?? (await loadAllSections(latDir));
|
|
74
74
|
const flat = flattenSections(allSections);
|
|
75
75
|
const byId = new Map(flat.map((s) => [s.id, s]));
|
|
76
|
-
return results
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
return results.flatMap((result) => {
|
|
77
|
+
const section = byId.get(result.id);
|
|
78
|
+
return section
|
|
79
|
+
? [{ section, reason: 'semantic match', score: result.score }]
|
|
80
|
+
: [];
|
|
81
|
+
});
|
|
80
82
|
}
|
|
81
83
|
/**
|
|
82
84
|
* Run a semantic search across lat.md sections.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CmdContext, CmdResult } from '../context.js';
|
|
2
|
+
import { type StaticViewBuildOptions } from '../view/static-build.js';
|
|
3
|
+
/** Export the current vault as a serverless, read-only Lat UI. */
|
|
4
|
+
export declare function uiBuildCommand(ctx: CmdContext, output?: string, options?: StaticViewBuildOptions): Promise<CmdResult>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { buildStaticView, } from '../view/static-build.js';
|
|
2
|
+
/** Export the current vault as a serverless, read-only Lat UI. */
|
|
3
|
+
export async function uiBuildCommand(ctx, output = 'lat-ui', options = {}) {
|
|
4
|
+
try {
|
|
5
|
+
const result = await buildStaticView(ctx, output, options);
|
|
6
|
+
return {
|
|
7
|
+
output: `Built ${result.documents} documents and ${result.sources} source views at ${result.outputDir}`,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
return { output: error.message, isError: true };
|
|
12
|
+
}
|
|
13
|
+
}
|