@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.
Files changed (64) hide show
  1. package/dist/src/cli/check-coverage.d.ts +2 -0
  2. package/dist/src/cli/check-coverage.js +71 -0
  3. package/dist/src/cli/check-mode.d.ts +6 -0
  4. package/dist/src/cli/check-mode.js +44 -2
  5. package/dist/src/cli/check.js +5 -1
  6. package/dist/src/cli/fork-instructions.d.ts +19 -0
  7. package/dist/src/cli/fork-instructions.js +183 -0
  8. package/dist/src/cli/fork-scaffold.d.ts +16 -0
  9. package/dist/src/cli/fork-scaffold.js +129 -0
  10. package/dist/src/cli/index.js +29 -4
  11. package/dist/src/cli/init.js +4 -0
  12. package/dist/src/cli/search.js +6 -4
  13. package/dist/src/cli/ui-build.d.ts +4 -0
  14. package/dist/src/cli/ui-build.js +13 -0
  15. package/dist/src/cli/ui.js +18 -2
  16. package/dist/src/code-refs.js +25 -4
  17. package/dist/src/fork/upstream-drift-cli.d.ts +1 -0
  18. package/dist/src/fork/upstream-drift-cli.js +2 -0
  19. package/dist/src/fork/upstream-drift.d.ts +26 -0
  20. package/dist/src/fork/upstream-drift.js +185 -0
  21. package/dist/src/fork/upstream-guard-cli.d.ts +1 -0
  22. package/dist/src/fork/upstream-guard-cli.js +2 -0
  23. package/dist/src/fork/upstream-guard.d.ts +72 -0
  24. package/dist/src/fork/upstream-guard.js +445 -0
  25. package/dist/src/init-version.d.ts +1 -1
  26. package/dist/src/init-version.js +1 -1
  27. package/dist/src/lattice.d.ts +2 -1
  28. package/dist/src/lattice.js +1 -0
  29. package/dist/src/search/search.d.ts +1 -0
  30. package/dist/src/search/search.js +15 -9
  31. package/dist/src/view/client/assets/index-VZwPPiFE.js +334 -0
  32. package/dist/src/view/client/assets/index-W8ktvNj9.css +1 -0
  33. package/dist/src/view/client/assets/logo-CNF8zs47.svg +182 -0
  34. package/dist/src/view/client/index.html +2 -2
  35. package/dist/src/view/git-diff.js +17 -0
  36. package/dist/src/view/graph.d.ts +6 -0
  37. package/dist/src/view/graph.js +176 -0
  38. package/dist/src/view/markdown.d.ts +5 -1
  39. package/dist/src/view/markdown.js +112 -25
  40. package/dist/src/view/protocol.d.ts +43 -0
  41. package/dist/src/view/protocol.js +1 -1
  42. package/dist/src/view/references.d.ts +2 -0
  43. package/dist/src/view/references.js +30 -8
  44. package/dist/src/view/repository.d.ts +5 -2
  45. package/dist/src/view/repository.js +13 -12
  46. package/dist/src/view/search.js +1 -0
  47. package/dist/src/view/server.d.ts +3 -0
  48. package/dist/src/view/server.js +42 -9
  49. package/dist/src/view/source-target.d.ts +8 -0
  50. package/dist/src/view/source-target.js +22 -0
  51. package/dist/src/view/static-build.d.ts +16 -0
  52. package/dist/src/view/static-build.js +408 -0
  53. package/dist/src/view/static-protocol.d.ts +23 -0
  54. package/dist/src/view/static-protocol.js +10 -0
  55. package/dist/src/view/store.d.ts +9 -1
  56. package/dist/src/view/store.js +82 -15
  57. package/dist/src/view/table-of-contents.d.ts +9 -0
  58. package/dist/src/view/table-of-contents.js +103 -0
  59. package/package.json +3 -1
  60. package/templates/AGENTS.md +2 -13
  61. package/templates/fork/conventions.md +95 -0
  62. package/templates/skill/SKILL.md +3 -14
  63. package/dist/src/view/client/assets/index-B0n0u03y.css +0 -1
  64. package/dist/src/view/client/assets/index-xm2_px4q.js +0 -23
@@ -21,9 +21,25 @@ export function openBrowser(url) {
21
21
  }
22
22
  /** Start `lat ui`, report its URL, and launch the default browser. */
23
23
  export async function uiCommand(ctx, options = {}) {
24
- const server = await startViewServer(ctx, options);
24
+ let server;
25
+ try {
26
+ server = await startViewServer(ctx, options);
27
+ }
28
+ catch (error) {
29
+ if (options.port !== undefined &&
30
+ error.code === 'EADDRINUSE') {
31
+ return {
32
+ isError: true,
33
+ output: `Port ${options.port} is already in use. Choose another with --port <number>.`,
34
+ };
35
+ }
36
+ throw error;
37
+ }
25
38
  options.onStarted?.(server);
26
- const lines = [`Viewing lat.md at ${server.url}`];
39
+ const lines = [
40
+ `Viewing lat.md at ${server.url}`,
41
+ 'Note: you can use `lat ui build` to build a static version of the UI',
42
+ ];
27
43
  try {
28
44
  await (options.openBrowser ?? openBrowser)(server.url);
29
45
  }
@@ -1,6 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { execFile } from 'node:child_process';
3
- import { join, relative } from 'node:path';
3
+ import { join, posix, relative } from 'node:path';
4
4
  import { toPosix, walkEntries } from './walk.js';
5
5
  /** Glob patterns used to exclude directories/files from code-ref scanning.
6
6
  * Shared between rg args and the TS fallback's walkFiles filter. */
@@ -9,7 +9,10 @@ const EXCLUDE_GLOBS = ['*.md'];
9
9
  /** Walk project files for code-ref scanning. Uses walkEntries for .gitignore
10
10
  * support, then additionally skips .md files, lat.md/, .claude/, and sub-projects. */
11
11
  export async function walkFiles(dir) {
12
- const entries = await walkEntries(dir);
12
+ const entries = (await walkEntries(dir)).map(toPosix);
13
+ const generatedOutputs = new Set(entries
14
+ .filter((entry) => entry.endsWith('/.lat-ui-build'))
15
+ .map((entry) => `${posix.dirname(entry)}/`));
13
16
  // Collect directories that contain their own lat.md/ (sub-projects)
14
17
  const subProjects = new Set();
15
18
  for (const e of entries) {
@@ -21,6 +24,7 @@ export async function walkFiles(dir) {
21
24
  .filter((e) => !e.endsWith('.md') &&
22
25
  !e.startsWith('lat.md/') &&
23
26
  !e.startsWith('.claude/') &&
27
+ ![...generatedOutputs].some((prefix) => e.startsWith(prefix)) &&
24
28
  ![...subProjects].some((prefix) => e.startsWith(prefix)))
25
29
  .map((e) => join(dir, e));
26
30
  }
@@ -148,8 +152,22 @@ async function findSubProjects(projectRoot) {
148
152
  }
149
153
  return [...subProjects];
150
154
  }
155
+ /** Find static UI exports so generated JSON and bundles never become code refs. */
156
+ async function findGeneratedOutputs(projectRoot) {
157
+ const out = await tryExec('rg', ['--files', '--hidden', '--glob', '**/.lat-ui-build', '.'], projectRoot);
158
+ if (!out)
159
+ return [];
160
+ return [
161
+ ...new Set(out
162
+ .split('\n')
163
+ .filter(Boolean)
164
+ .map((line) => toPosix(line).replace(/^\.\//, ''))
165
+ .filter((line) => line.endsWith('/.lat-ui-build'))
166
+ .map((line) => posix.dirname(line))),
167
+ ];
168
+ }
151
169
  /** Build rg glob exclusion args. */
152
- function rgExcludeArgs(subProjects) {
170
+ function rgExcludeArgs(subProjects, generatedOutputs) {
153
171
  const args = [];
154
172
  for (const dir of EXCLUDE_DIRS)
155
173
  args.push('--glob', `!${dir}/`);
@@ -157,6 +175,8 @@ function rgExcludeArgs(subProjects) {
157
175
  args.push('--glob', `!${glob}`);
158
176
  for (const sp of subProjects)
159
177
  args.push('--glob', `!${sp}/`);
178
+ for (const output of generatedOutputs)
179
+ args.push('--glob', `!${output}/`);
160
180
  return args;
161
181
  }
162
182
  /**
@@ -167,7 +187,8 @@ function rgExcludeArgs(subProjects) {
167
187
  async function tryRipgrep(projectRoot) {
168
188
  // Detect sub-projects first so we can exclude them from all rg calls
169
189
  const subProjects = await findSubProjects(projectRoot);
170
- const excludes = rgExcludeArgs(subProjects);
190
+ const generatedOutputs = await findGeneratedOutputs(projectRoot);
191
+ const excludes = rgExcludeArgs(subProjects, generatedOutputs);
171
192
  // Search for @lat refs
172
193
  const searchArgs = [
173
194
  '--no-heading',
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { main } from './upstream-drift.js';
2
+ process.exit(main(process.argv.slice(2)));
@@ -0,0 +1,26 @@
1
+ export declare const UPSTREAM_URL = "https://github.com/vercel-labs/lat.md.git";
2
+ export declare const UPSTREAM_REF = "refs/remotes/upstream/main";
3
+ export type Drift = {
4
+ upstream: string;
5
+ ahead: number;
6
+ behind: number;
7
+ upstreamFiles: string[];
8
+ forkFiles: string[];
9
+ overlap: string[];
10
+ conflicts: string[];
11
+ mergesCleanly: boolean;
12
+ };
13
+ /**
14
+ * Conflicting paths from `git merge-tree --write-tree --name-only`, whose
15
+ * output is the written tree id, then one path per line, then a blank line
16
+ * and the human-readable log. Everything after that blank line is prose.
17
+ */
18
+ export declare function parseMergeTree(stdout: string): string[];
19
+ export declare function fetchUpstream(repo: string, url: string, ref: string): void;
20
+ export declare function analyzeDrift(opts: {
21
+ repo: string;
22
+ syncPoint: string;
23
+ upstreamRef: string;
24
+ }): Drift;
25
+ export declare function formatDrift(drift: Drift): string;
26
+ export declare function main(argv: string[]): number;
@@ -0,0 +1,185 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { parseNameStatus, parseSyncPoint, repoRoot, SYNC_POINT_FILE, } from './upstream-guard.js';
5
+ export const UPSTREAM_URL = 'https://github.com/vercel-labs/lat.md.git';
6
+ export const UPSTREAM_REF = 'refs/remotes/upstream/main';
7
+ function git(repo, args) {
8
+ const result = spawnSync('git', ['-C', repo, ...args], {
9
+ encoding: 'utf-8',
10
+ maxBuffer: 64 * 1024 * 1024,
11
+ });
12
+ if (result.error)
13
+ throw result.error;
14
+ return result;
15
+ }
16
+ /**
17
+ * Conflicting paths from `git merge-tree --write-tree --name-only`, whose
18
+ * output is the written tree id, then one path per line, then a blank line
19
+ * and the human-readable log. Everything after that blank line is prose.
20
+ */
21
+ export function parseMergeTree(stdout) {
22
+ const lines = stdout.split('\n');
23
+ const paths = [];
24
+ for (const line of lines.slice(1)) {
25
+ if (line.trim() === '')
26
+ break;
27
+ paths.push(line);
28
+ }
29
+ return paths;
30
+ }
31
+ function names(diff) {
32
+ return [...new Set(diff.map((d) => d.path))].sort();
33
+ }
34
+ function diffNames(repo, from, to) {
35
+ const result = git(repo, [
36
+ 'diff',
37
+ '--no-renames',
38
+ '--name-status',
39
+ '-z',
40
+ from,
41
+ to,
42
+ ]);
43
+ if (result.status !== 0) {
44
+ throw new Error(`git diff ${from}..${to} failed: ${result.stderr.trim()}`);
45
+ }
46
+ return names(parseNameStatus(result.stdout));
47
+ }
48
+ function count(repo, range) {
49
+ const result = git(repo, ['rev-list', '--count', range]);
50
+ if (result.status !== 0) {
51
+ throw new Error(`git rev-list ${range} failed: ${result.stderr.trim()}`);
52
+ }
53
+ return Number(result.stdout.trim());
54
+ }
55
+ export function fetchUpstream(repo, url, ref) {
56
+ const result = git(repo, [
57
+ 'fetch',
58
+ '--no-tags',
59
+ url,
60
+ `+refs/heads/main:${ref}`,
61
+ ]);
62
+ if (result.status !== 0) {
63
+ throw new Error(`fetching ${url} failed: ${result.stderr.trim()}`);
64
+ }
65
+ }
66
+ // @lat: [[fork#Watching the drift]]
67
+ export function analyzeDrift(opts) {
68
+ const root = repoRoot(opts.repo);
69
+ const head = git(root, ['rev-parse', 'HEAD']).stdout.trim();
70
+ const upstream = git(root, ['rev-parse', opts.upstreamRef]).stdout.trim();
71
+ const upstreamFiles = diffNames(root, opts.syncPoint, upstream);
72
+ const forkFiles = diffNames(root, opts.syncPoint, head);
73
+ const forkSet = new Set(forkFiles);
74
+ const merge = git(root, [
75
+ 'merge-tree',
76
+ '--write-tree',
77
+ '--name-only',
78
+ head,
79
+ upstream,
80
+ ]);
81
+ return {
82
+ upstream,
83
+ ahead: count(root, `${opts.syncPoint}..${upstream}`),
84
+ behind: count(root, `${upstream}..${head}`),
85
+ upstreamFiles,
86
+ forkFiles,
87
+ overlap: upstreamFiles.filter((f) => forkSet.has(f)),
88
+ conflicts: merge.status === 0 ? [] : parseMergeTree(merge.stdout),
89
+ mergesCleanly: merge.status === 0,
90
+ };
91
+ }
92
+ function list(paths) {
93
+ return paths.map((p) => `- \`${p}\``);
94
+ }
95
+ export function formatDrift(drift) {
96
+ const lines = [`## Upstream drift`, ''];
97
+ if (drift.ahead === 0) {
98
+ lines.push('Upstream has nothing new since the recorded sync point.', '');
99
+ return lines.join('\n');
100
+ }
101
+ lines.push(`Upstream is **${drift.ahead} commit(s)** ahead of the sync point, ` +
102
+ `touching **${drift.upstreamFiles.length} file(s)**. This fork has ` +
103
+ `${drift.behind} commit(s) of its own.`, '', `Upstream tip: \`${drift.upstream.slice(0, 12)}\``, '');
104
+ if (drift.mergesCleanly) {
105
+ lines.push('Merges without a textual conflict.', '');
106
+ }
107
+ else {
108
+ lines.push(`### ${drift.conflicts.length} conflicting file(s)`, '', ...list(drift.conflicts), '');
109
+ }
110
+ // A file both sides changed is where the next conflict comes from, even
111
+ // when today's merge is clean.
112
+ lines.push(`### ${drift.overlap.length} file(s) changed on both sides`, '', ...(drift.overlap.length === 0 ? ['None.'] : list(drift.overlap)), '', 'A clean merge is not a working tree. Two additions can collide in the', 'knowledge graph without conflicting in git, and an upstream type can', 'widen underneath a fork-owned test. The job checks both after merging.', '');
113
+ return lines.join('\n');
114
+ }
115
+ const USAGE = `Usage: node dist/src/fork/upstream-drift-cli.js [options]
116
+
117
+ --repo <dir> default the current working directory
118
+ --url <git-url> default ${UPSTREAM_URL}
119
+ --ref <ref> default ${UPSTREAM_REF}
120
+ --sync-point <rev> default the recorded sync point
121
+ --no-fetch use the ref already in the repository
122
+ --fail-on-conflict exit 1 when the merge would conflict
123
+ -h, --help this text
124
+ `;
125
+ export function main(argv) {
126
+ const opts = {
127
+ repo: process.cwd(),
128
+ url: UPSTREAM_URL,
129
+ ref: UPSTREAM_REF,
130
+ syncPoint: '',
131
+ fetch: true,
132
+ failOnConflict: false,
133
+ };
134
+ for (let i = 0; i < argv.length; i += 1) {
135
+ const arg = argv[i];
136
+ if (arg === '-h' || arg === '--help') {
137
+ process.stdout.write(USAGE);
138
+ return 0;
139
+ }
140
+ else if (arg === '--no-fetch')
141
+ opts.fetch = false;
142
+ else if (arg === '--fail-on-conflict')
143
+ opts.failOnConflict = true;
144
+ else if (arg === '--repo')
145
+ opts.repo = argv[++i] ?? '';
146
+ else if (arg === '--url')
147
+ opts.url = argv[++i] ?? '';
148
+ else if (arg === '--ref')
149
+ opts.ref = argv[++i] ?? '';
150
+ else if (arg === '--sync-point')
151
+ opts.syncPoint = argv[++i] ?? '';
152
+ else {
153
+ process.stderr.write(`unknown argument: ${arg}\n\n${USAGE}`);
154
+ return 2;
155
+ }
156
+ }
157
+ try {
158
+ const root = repoRoot(opts.repo);
159
+ if (opts.syncPoint === '') {
160
+ const file = resolve(root, SYNC_POINT_FILE);
161
+ if (!existsSync(file)) {
162
+ process.stderr.write(`upstream-drift: no sync point at ${file}\n`);
163
+ return 2;
164
+ }
165
+ opts.syncPoint = parseSyncPoint(readFileSync(file, 'utf-8')) ?? '';
166
+ if (opts.syncPoint === '') {
167
+ process.stderr.write(`upstream-drift: ${file} holds no revision\n`);
168
+ return 2;
169
+ }
170
+ }
171
+ if (opts.fetch)
172
+ fetchUpstream(root, opts.url, opts.ref);
173
+ const drift = analyzeDrift({
174
+ repo: root,
175
+ syncPoint: opts.syncPoint,
176
+ upstreamRef: opts.ref,
177
+ });
178
+ process.stdout.write(formatDrift(drift));
179
+ return opts.failOnConflict && !drift.mergesCleanly ? 1 : 0;
180
+ }
181
+ catch (error) {
182
+ process.stderr.write(`upstream-drift: ${error.message}\n`);
183
+ return 2;
184
+ }
185
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { main } from './upstream-guard.js';
2
+ process.exit(main(process.argv.slice(2)));
@@ -0,0 +1,72 @@
1
+ export declare const SYNC_POINT_FILE = "fork-upstream-sync-point";
2
+ /**
3
+ * The commit this fork was originally cut from. It is not the guard's
4
+ * baseline — see [[fork#The upstream guard#The sync point]] for why a frozen
5
+ * one cannot tell the fork's changes from upstream's.
6
+ */
7
+ export declare const ORIGINAL_FORK_POINT = "65d2f5b220853b2b10dca3de18d511f575bc12c7";
8
+ export declare const ALLOWLIST_FILE = "fork-upstream-allowlist.tsv";
9
+ export declare const REASON_PLACEHOLDER = "TODO";
10
+ export type AllowEntry = {
11
+ path: string;
12
+ reason: string;
13
+ line: number;
14
+ };
15
+ export type ParsedAllowlist = {
16
+ entries: AllowEntry[];
17
+ errors: string[];
18
+ };
19
+ export declare function parseAllowlist(text: string): ParsedAllowlist;
20
+ export declare function renderAllowlist(entries: AllowEntry[]): string;
21
+ export declare function needsReason(reason: string): boolean;
22
+ export type DiffEntry = {
23
+ status: string;
24
+ path: string;
25
+ };
26
+ export declare function parseNameStatus(raw: string): DiffEntry[];
27
+ export type Finding = {
28
+ kind: 'deletion' | 'unallowlisted' | 'missing-reason' | 'allowlist-error';
29
+ path: string;
30
+ message: string;
31
+ };
32
+ export type ClassifyInput = {
33
+ diff: DiffEntry[];
34
+ upstream: ReadonlySet<string>;
35
+ allowlist: ReadonlyMap<string, string>;
36
+ };
37
+ export type ClassifyResult = {
38
+ findings: Finding[];
39
+ warnings: string[];
40
+ touched: string[];
41
+ };
42
+ export declare function classify(input: ClassifyInput): ClassifyResult;
43
+ export declare function repoRoot(from: string): string;
44
+ export declare function hasCommit(repo: string, rev: string): boolean;
45
+ export declare function diffAgainst(repo: string, rev: string): DiffEntry[];
46
+ export declare function upstreamPaths(repo: string, rev: string, paths: string[]): Set<string>;
47
+ export declare function parseSyncPoint(text: string): string | undefined;
48
+ export declare function renderSyncPoint(previous: string, rev: string): string;
49
+ export declare function isAncestorOfHead(repo: string, rev: string): boolean;
50
+ export declare function isShallow(repo: string): boolean;
51
+ export type GuardOptions = {
52
+ repo: string;
53
+ /** Overrides the recorded sync point. Omitted, the file decides. */
54
+ syncPoint?: string;
55
+ allowlistFile: string;
56
+ syncPointFile?: string;
57
+ regenerate: boolean;
58
+ fetch: boolean;
59
+ setSyncPoint?: string;
60
+ };
61
+ export declare function fetchSyncPoint(repo: string, rev: string): boolean;
62
+ export type GuardResult = {
63
+ ok: boolean;
64
+ report: string;
65
+ findings: Finding[];
66
+ warnings: string[];
67
+ };
68
+ export declare function runGuard(options: GuardOptions): GuardResult;
69
+ export declare function parseArgs(argv: string[]): GuardOptions | {
70
+ help: true;
71
+ };
72
+ export declare function main(argv: string[]): number;