lattice-graph 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +391 -0
- package/package.json +56 -0
- package/src/commands/build.ts +208 -0
- package/src/commands/init.ts +111 -0
- package/src/commands/lint.ts +245 -0
- package/src/commands/populate.ts +224 -0
- package/src/commands/update.ts +175 -0
- package/src/config.ts +93 -0
- package/src/extract/extractor.ts +13 -0
- package/src/extract/parser.ts +117 -0
- package/src/extract/python/calls.ts +121 -0
- package/src/extract/python/extractor.ts +171 -0
- package/src/extract/python/frameworks.ts +142 -0
- package/src/extract/python/imports.ts +115 -0
- package/src/extract/python/symbols.ts +121 -0
- package/src/extract/tags.ts +77 -0
- package/src/extract/typescript/calls.ts +110 -0
- package/src/extract/typescript/extractor.ts +130 -0
- package/src/extract/typescript/imports.ts +71 -0
- package/src/extract/typescript/symbols.ts +252 -0
- package/src/graph/database.ts +95 -0
- package/src/graph/queries.ts +336 -0
- package/src/graph/writer.ts +147 -0
- package/src/main.ts +525 -0
- package/src/output/json.ts +79 -0
- package/src/output/text.ts +265 -0
- package/src/types/config.ts +32 -0
- package/src/types/graph.ts +87 -0
- package/src/types/lint.ts +21 -0
- package/src/types/result.ts +58 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { err, ok, type Result } from "../types/result.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Initializes a Lattice project by creating .lattice/ directory
|
|
7
|
+
* and a starter lattice.toml with detected languages.
|
|
8
|
+
*
|
|
9
|
+
* @param projectRoot - Path to the project root directory
|
|
10
|
+
* @returns Ok on success, Err with a message on failure
|
|
11
|
+
*/
|
|
12
|
+
// @lattice:flow init
|
|
13
|
+
function executeInit(projectRoot: string): Result<string, string> {
|
|
14
|
+
try {
|
|
15
|
+
// Create .lattice directory
|
|
16
|
+
const latticeDir = join(projectRoot, ".lattice");
|
|
17
|
+
mkdirSync(latticeDir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
// Generate lattice.toml if it doesn't exist
|
|
20
|
+
const tomlPath = join(projectRoot, "lattice.toml");
|
|
21
|
+
if (!existsSync(tomlPath)) {
|
|
22
|
+
const languages = detectLanguages(projectRoot);
|
|
23
|
+
const root = detectRoot(projectRoot);
|
|
24
|
+
const toml = generateToml(languages, root);
|
|
25
|
+
writeFileSync(tomlPath, toml);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return ok("Initialized Lattice project");
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return err(`Init failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Detects which languages are present in the project by scanning for file extensions. */
|
|
35
|
+
function detectLanguages(projectRoot: string): readonly string[] {
|
|
36
|
+
const languages: string[] = [];
|
|
37
|
+
const glob = new Bun.Glob("**/*.{py,ts,tsx,js,jsx}");
|
|
38
|
+
|
|
39
|
+
let hasPython = false;
|
|
40
|
+
let hasTypeScript = false;
|
|
41
|
+
|
|
42
|
+
for (const path of glob.scanSync({ cwd: projectRoot, dot: false })) {
|
|
43
|
+
if (
|
|
44
|
+
path.includes("node_modules") ||
|
|
45
|
+
path.includes(".git") ||
|
|
46
|
+
path.includes("test") ||
|
|
47
|
+
path.includes("fixture") ||
|
|
48
|
+
path.includes("vendor") ||
|
|
49
|
+
path.includes("dist")
|
|
50
|
+
)
|
|
51
|
+
continue;
|
|
52
|
+
if (path.endsWith(".py")) hasPython = true;
|
|
53
|
+
if (path.endsWith(".ts") || path.endsWith(".tsx")) hasTypeScript = true;
|
|
54
|
+
if (hasPython && hasTypeScript) break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (hasPython) languages.push("python");
|
|
58
|
+
if (hasTypeScript) languages.push("typescript");
|
|
59
|
+
|
|
60
|
+
return languages;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Detects the source root — uses "src" if it exists, otherwise ".". */
|
|
64
|
+
function detectRoot(projectRoot: string): string {
|
|
65
|
+
const srcPath = `${projectRoot}/src`;
|
|
66
|
+
try {
|
|
67
|
+
const { statSync } = require("node:fs");
|
|
68
|
+
if (statSync(srcPath).isDirectory()) return "src";
|
|
69
|
+
} catch {
|
|
70
|
+
// src/ doesn't exist
|
|
71
|
+
}
|
|
72
|
+
return ".";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Generates a starter lattice.toml with detected languages. */
|
|
76
|
+
function generateToml(languages: readonly string[], root: string): string {
|
|
77
|
+
const langArray = languages.map((l) => `"${l}"`).join(", ");
|
|
78
|
+
const lines: string[] = [
|
|
79
|
+
"[project]",
|
|
80
|
+
`languages = [${langArray}]`,
|
|
81
|
+
`root = "${root}"`,
|
|
82
|
+
'exclude = ["node_modules", "venv", ".git", "dist", "__pycache__", ".lattice"]',
|
|
83
|
+
"",
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
if (languages.includes("python")) {
|
|
87
|
+
lines.push(
|
|
88
|
+
"[python]",
|
|
89
|
+
`source_roots = ["${root}"]`,
|
|
90
|
+
'test_paths = ["tests"]',
|
|
91
|
+
"frameworks = []",
|
|
92
|
+
"",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (languages.includes("typescript")) {
|
|
97
|
+
lines.push(
|
|
98
|
+
"[typescript]",
|
|
99
|
+
`source_roots = ["${root}"]`,
|
|
100
|
+
'test_paths = ["tests"]',
|
|
101
|
+
"frameworks = []",
|
|
102
|
+
"",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
lines.push("[lint]", "strict = false", "ignore = []", "");
|
|
107
|
+
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export { executeInit };
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { LatticeConfig } from "../types/config.ts";
|
|
3
|
+
import type { LintIssue, LintResult } from "../types/lint.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runs all lint checks against the built knowledge graph.
|
|
7
|
+
* Does not modify the database — reports only.
|
|
8
|
+
*
|
|
9
|
+
* @param db - An open Database handle (readonly)
|
|
10
|
+
* @param config - Lattice configuration for boundary package detection
|
|
11
|
+
* @returns Lint result with issues, coverage, and unresolved count
|
|
12
|
+
*/
|
|
13
|
+
// @lattice:flow lint
|
|
14
|
+
function executeLint(db: Database, _config: LatticeConfig): LintResult {
|
|
15
|
+
const issues: LintIssue[] = [];
|
|
16
|
+
|
|
17
|
+
checkMissingFlowTags(db, issues);
|
|
18
|
+
checkInvalidTags(db, issues);
|
|
19
|
+
checkTypos(db, issues);
|
|
20
|
+
checkOrphanedEvents(db, issues);
|
|
21
|
+
checkStaleBoundaryTags(db, issues);
|
|
22
|
+
|
|
23
|
+
const coverage = computeCoverage(db);
|
|
24
|
+
const unresolvedCount = countUnresolved(db);
|
|
25
|
+
|
|
26
|
+
return { issues, coverage, unresolvedCount };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Checks for route handlers without @lattice:flow tags. */
|
|
30
|
+
function checkMissingFlowTags(db: Database, issues: LintIssue[]): void {
|
|
31
|
+
// Nodes with route metadata (detected by framework extractors) but no flow tag
|
|
32
|
+
const rows = db
|
|
33
|
+
.query(
|
|
34
|
+
`SELECT n.id, n.name, n.file, n.line_start FROM nodes n
|
|
35
|
+
WHERE n.metadata IS NOT NULL AND n.metadata LIKE '%"route"%'
|
|
36
|
+
AND NOT EXISTS (SELECT 1 FROM tags t WHERE t.node_id = n.id AND t.kind = 'flow')`,
|
|
37
|
+
)
|
|
38
|
+
.all() as { id: string; name: string; file: string; line_start: number }[];
|
|
39
|
+
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
issues.push({
|
|
42
|
+
severity: "error",
|
|
43
|
+
file: row.file,
|
|
44
|
+
line: row.line_start,
|
|
45
|
+
symbol: row.name,
|
|
46
|
+
message: `Route handler missing @lattice:flow tag`,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Checks for tags placed on invalid node kinds (e.g., flow tag on a class). */
|
|
52
|
+
function checkInvalidTags(db: Database, issues: LintIssue[]): void {
|
|
53
|
+
const rows = db
|
|
54
|
+
.query(
|
|
55
|
+
`SELECT t.kind AS tag_kind, t.value, n.id, n.name, n.kind AS node_kind, n.file, n.line_start
|
|
56
|
+
FROM tags t JOIN nodes n ON t.node_id = n.id
|
|
57
|
+
WHERE n.kind NOT IN ('function', 'method')`,
|
|
58
|
+
)
|
|
59
|
+
.all() as {
|
|
60
|
+
tag_kind: string;
|
|
61
|
+
value: string;
|
|
62
|
+
id: string;
|
|
63
|
+
name: string;
|
|
64
|
+
node_kind: string;
|
|
65
|
+
file: string;
|
|
66
|
+
line_start: number;
|
|
67
|
+
}[];
|
|
68
|
+
|
|
69
|
+
for (const row of rows) {
|
|
70
|
+
issues.push({
|
|
71
|
+
severity: "error",
|
|
72
|
+
file: row.file,
|
|
73
|
+
line: row.line_start,
|
|
74
|
+
symbol: row.name,
|
|
75
|
+
message: `@lattice:${row.tag_kind} tag on a ${row.node_kind} — tags should only be on functions or methods`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Checks for probable typos by finding tag values used only once when similar values exist. */
|
|
81
|
+
function checkTypos(db: Database, issues: LintIssue[]): void {
|
|
82
|
+
// Group tag values by kind, find singletons
|
|
83
|
+
const tagCounts = db
|
|
84
|
+
.query("SELECT kind, value, COUNT(*) as cnt FROM tags GROUP BY kind, value")
|
|
85
|
+
.all() as { kind: string; value: string; cnt: number }[];
|
|
86
|
+
|
|
87
|
+
const singletons = tagCounts.filter((t) => t.cnt === 1);
|
|
88
|
+
const commons = tagCounts.filter((t) => t.cnt > 1);
|
|
89
|
+
|
|
90
|
+
for (const single of singletons) {
|
|
91
|
+
const similar = commons.filter(
|
|
92
|
+
(c) => c.kind === single.kind && editDistance(single.value, c.value) <= 2,
|
|
93
|
+
);
|
|
94
|
+
if (similar.length > 0) {
|
|
95
|
+
const bestMatch = similar[0];
|
|
96
|
+
const tagNode = db
|
|
97
|
+
.query(
|
|
98
|
+
`SELECT n.name, n.file, n.line_start FROM tags t JOIN nodes n ON t.node_id = n.id
|
|
99
|
+
WHERE t.kind = ? AND t.value = ?`,
|
|
100
|
+
)
|
|
101
|
+
.get(single.kind, single.value) as {
|
|
102
|
+
name: string;
|
|
103
|
+
file: string;
|
|
104
|
+
line_start: number;
|
|
105
|
+
} | null;
|
|
106
|
+
|
|
107
|
+
if (tagNode && bestMatch) {
|
|
108
|
+
issues.push({
|
|
109
|
+
severity: "warning",
|
|
110
|
+
file: tagNode.file,
|
|
111
|
+
line: tagNode.line_start,
|
|
112
|
+
symbol: tagNode.name,
|
|
113
|
+
message: `@lattice:${single.kind} "${single.value}" — did you mean "${bestMatch.value}"? (used ${bestMatch.cnt} times elsewhere)`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Checks for events that are emitted but never handled, or handled but never emitted. */
|
|
121
|
+
function checkOrphanedEvents(db: Database, issues: LintIssue[]): void {
|
|
122
|
+
// Emits with no matching handles
|
|
123
|
+
const orphanedEmits = db
|
|
124
|
+
.query(
|
|
125
|
+
`SELECT t.value, n.name, n.file, n.line_start FROM tags t
|
|
126
|
+
JOIN nodes n ON t.node_id = n.id
|
|
127
|
+
WHERE t.kind = 'emits'
|
|
128
|
+
AND NOT EXISTS (SELECT 1 FROM tags h WHERE h.kind = 'handles' AND h.value = t.value)`,
|
|
129
|
+
)
|
|
130
|
+
.all() as { value: string; name: string; file: string; line_start: number }[];
|
|
131
|
+
|
|
132
|
+
for (const row of orphanedEmits) {
|
|
133
|
+
issues.push({
|
|
134
|
+
severity: "warning",
|
|
135
|
+
file: row.file,
|
|
136
|
+
line: row.line_start,
|
|
137
|
+
symbol: row.name,
|
|
138
|
+
message: `@lattice:emits "${row.value}" has no handler — no @lattice:handles "${row.value}" found`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Handles with no matching emits
|
|
143
|
+
const orphanedHandles = db
|
|
144
|
+
.query(
|
|
145
|
+
`SELECT t.value, n.name, n.file, n.line_start FROM tags t
|
|
146
|
+
JOIN nodes n ON t.node_id = n.id
|
|
147
|
+
WHERE t.kind = 'handles'
|
|
148
|
+
AND NOT EXISTS (SELECT 1 FROM tags e WHERE e.kind = 'emits' AND e.value = t.value)`,
|
|
149
|
+
)
|
|
150
|
+
.all() as { value: string; name: string; file: string; line_start: number }[];
|
|
151
|
+
|
|
152
|
+
for (const row of orphanedHandles) {
|
|
153
|
+
issues.push({
|
|
154
|
+
severity: "warning",
|
|
155
|
+
file: row.file,
|
|
156
|
+
line: row.line_start,
|
|
157
|
+
symbol: row.name,
|
|
158
|
+
message: `@lattice:handles "${row.value}" has no emitter — no @lattice:emits "${row.value}" found`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Checks for stale boundary tags.
|
|
165
|
+
* A boundary tag is stale if the tagged function has no uncertain call edges at all
|
|
166
|
+
* (meaning it doesn't call any external code that could be the boundary).
|
|
167
|
+
*/
|
|
168
|
+
function checkStaleBoundaryTags(db: Database, issues: LintIssue[]): void {
|
|
169
|
+
const boundaryTags = db
|
|
170
|
+
.query(
|
|
171
|
+
`SELECT t.node_id, t.value, n.name, n.file, n.line_start FROM tags t
|
|
172
|
+
JOIN nodes n ON t.node_id = n.id
|
|
173
|
+
WHERE t.kind = 'boundary'`,
|
|
174
|
+
)
|
|
175
|
+
.all() as { node_id: string; value: string; name: string; file: string; line_start: number }[];
|
|
176
|
+
|
|
177
|
+
for (const tag of boundaryTags) {
|
|
178
|
+
// A boundary function should have at least one uncertain call edge
|
|
179
|
+
// (calls to external packages are marked uncertain during extraction)
|
|
180
|
+
const hasExternalCall = db
|
|
181
|
+
.query("SELECT 1 FROM edges WHERE source_id = ? AND certainty = 'uncertain' LIMIT 1")
|
|
182
|
+
.get(tag.node_id);
|
|
183
|
+
|
|
184
|
+
if (!hasExternalCall) {
|
|
185
|
+
issues.push({
|
|
186
|
+
severity: "warning",
|
|
187
|
+
file: tag.file,
|
|
188
|
+
line: tag.line_start,
|
|
189
|
+
symbol: tag.name,
|
|
190
|
+
message: `@lattice:boundary "${tag.value}" may be stale — no external calls found in this function`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Computes tag coverage: how many detected entry points are tagged vs total. */
|
|
197
|
+
function computeCoverage(db: Database): { tagged: number; total: number } {
|
|
198
|
+
const total = db
|
|
199
|
+
.query(
|
|
200
|
+
"SELECT COUNT(*) as c FROM nodes WHERE metadata IS NOT NULL AND metadata LIKE '%\"route\"%'",
|
|
201
|
+
)
|
|
202
|
+
.get() as { c: number };
|
|
203
|
+
|
|
204
|
+
const tagged = db
|
|
205
|
+
.query(
|
|
206
|
+
`SELECT COUNT(*) as c FROM nodes n
|
|
207
|
+
WHERE n.metadata IS NOT NULL AND n.metadata LIKE '%"route"%'
|
|
208
|
+
AND EXISTS (SELECT 1 FROM tags t WHERE t.node_id = n.id AND t.kind = 'flow')`,
|
|
209
|
+
)
|
|
210
|
+
.get() as { c: number };
|
|
211
|
+
|
|
212
|
+
return { tagged: tagged.c, total: total.c };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Counts unresolved references in the database. */
|
|
216
|
+
function countUnresolved(db: Database): number {
|
|
217
|
+
const row = db.query("SELECT COUNT(*) as c FROM unresolved").get() as { c: number };
|
|
218
|
+
return row.c;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Computes the Levenshtein edit distance between two strings.
|
|
223
|
+
* Used for typo detection in tag values.
|
|
224
|
+
*/
|
|
225
|
+
function editDistance(a: string, b: string): number {
|
|
226
|
+
const m = a.length;
|
|
227
|
+
const n = b.length;
|
|
228
|
+
|
|
229
|
+
// Use two rows instead of a full matrix to avoid index safety issues
|
|
230
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
231
|
+
let curr = new Array<number>(n + 1).fill(0);
|
|
232
|
+
|
|
233
|
+
for (let i = 1; i <= m; i++) {
|
|
234
|
+
curr[0] = i;
|
|
235
|
+
for (let j = 1; j <= n; j++) {
|
|
236
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
237
|
+
curr[j] = Math.min((prev[j] ?? 0) + 1, (curr[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
238
|
+
}
|
|
239
|
+
[prev, curr] = [curr, prev];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return prev[n] ?? 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export { executeLint };
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { LatticeConfig } from "../types/config.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates a structured workflow that instructs a coding agent to tag the codebase.
|
|
6
|
+
* Includes tag spec, few-shot examples, project context, and a step-by-step process
|
|
7
|
+
* with explicit validation checkpoints.
|
|
8
|
+
*
|
|
9
|
+
* @param db - An open Database handle with a built graph
|
|
10
|
+
* @param _config - Lattice configuration (reserved for future use)
|
|
11
|
+
* @returns A complete instruction string for the coding agent
|
|
12
|
+
*/
|
|
13
|
+
// @lattice:flow populate
|
|
14
|
+
function executePopulate(db: Database, _config: LatticeConfig): string {
|
|
15
|
+
const sections: string[] = [];
|
|
16
|
+
|
|
17
|
+
sections.push(tagSpecSection());
|
|
18
|
+
sections.push(examplesSection());
|
|
19
|
+
sections.push(projectSummarySection(db));
|
|
20
|
+
sections.push(workflowSection());
|
|
21
|
+
|
|
22
|
+
return sections.join("\n\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Outputs the tag spec — what each tag means and the syntax rules. */
|
|
26
|
+
function tagSpecSection(): string {
|
|
27
|
+
return `## Lattice Tag Specification
|
|
28
|
+
|
|
29
|
+
Four tags, placed in comments directly above function definitions:
|
|
30
|
+
|
|
31
|
+
- \`@lattice:flow <name>\` — Flow entry point. Where execution begins for a business operation. Route handlers, CLI commands, cron jobs, queue consumers.
|
|
32
|
+
- \`@lattice:boundary <system>\` — External boundary. Where code leaves the codebase. API calls, database queries, cache operations, third-party SDKs.
|
|
33
|
+
- \`@lattice:emits <event>\` — Event emission. Publishes to a queue, event bus, or notification system.
|
|
34
|
+
- \`@lattice:handles <event>\` — Event consumption. Subscribes to or processes events. Must match a corresponding emits tag.
|
|
35
|
+
|
|
36
|
+
Rules:
|
|
37
|
+
- Place the tag comment directly above the function definition, no blank lines between
|
|
38
|
+
- Names are kebab-case: \`checkout\`, \`user-registration\`, \`order.created\`, \`aws-s3\`
|
|
39
|
+
- Multiple values: \`# @lattice:flow checkout, payment\`
|
|
40
|
+
- Do NOT tag intermediate functions — only entry points and boundaries. Everything in between is derived from the call graph automatically.`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Few-shot examples showing correct tagging across different scenarios. */
|
|
44
|
+
function examplesSection(): string {
|
|
45
|
+
return `## Examples
|
|
46
|
+
|
|
47
|
+
### Python — FastAPI route with boundary and events
|
|
48
|
+
|
|
49
|
+
\`\`\`python
|
|
50
|
+
# @lattice:flow checkout
|
|
51
|
+
@app.post("/api/checkout")
|
|
52
|
+
def handle_checkout(req):
|
|
53
|
+
order = create_order(req) # no tag — derived from call graph
|
|
54
|
+
return order
|
|
55
|
+
|
|
56
|
+
def create_order(req): # no tag — derived from call graph
|
|
57
|
+
charge(req.amount, req.token)
|
|
58
|
+
save_order(req)
|
|
59
|
+
emit_order_created(req.order_id)
|
|
60
|
+
|
|
61
|
+
# @lattice:boundary stripe
|
|
62
|
+
def charge(amount, token):
|
|
63
|
+
return stripe.charges.create(amount=amount, source=token)
|
|
64
|
+
|
|
65
|
+
# @lattice:boundary postgres
|
|
66
|
+
def save_order(req):
|
|
67
|
+
db.execute("INSERT INTO orders ...")
|
|
68
|
+
|
|
69
|
+
# @lattice:emits order.created
|
|
70
|
+
def emit_order_created(order_id):
|
|
71
|
+
queue.publish("order.created", {"order_id": order_id})
|
|
72
|
+
|
|
73
|
+
# @lattice:handles order.created
|
|
74
|
+
def send_confirmation(event):
|
|
75
|
+
sendgrid.send(event.order_id)
|
|
76
|
+
\`\`\`
|
|
77
|
+
|
|
78
|
+
### TypeScript — Express route with database boundary
|
|
79
|
+
|
|
80
|
+
\`\`\`typescript
|
|
81
|
+
// @lattice:flow user-registration
|
|
82
|
+
router.post("/api/users", async (req, res) => {
|
|
83
|
+
const user = await createUser(req.body);
|
|
84
|
+
res.json(user);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// @lattice:boundary postgres
|
|
88
|
+
async function createUser(data: CreateUserInput): Promise<User> {
|
|
89
|
+
return db.query("INSERT INTO users ...").run(data);
|
|
90
|
+
}
|
|
91
|
+
\`\`\`
|
|
92
|
+
|
|
93
|
+
### Python — Celery task as entry point
|
|
94
|
+
|
|
95
|
+
\`\`\`python
|
|
96
|
+
# @lattice:flow invoice-generation
|
|
97
|
+
@shared_task
|
|
98
|
+
def generate_invoice(order_id):
|
|
99
|
+
order = fetch_order(order_id)
|
|
100
|
+
pdf = render_invoice(order)
|
|
101
|
+
send_invoice_email(order, pdf)
|
|
102
|
+
|
|
103
|
+
# @lattice:boundary s3
|
|
104
|
+
def render_invoice(order):
|
|
105
|
+
pdf = create_pdf(order)
|
|
106
|
+
s3.upload(f"invoices/{order.id}.pdf", pdf)
|
|
107
|
+
return pdf
|
|
108
|
+
\`\`\``;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Brief project summary from the graph — just enough context, not a file listing. */
|
|
112
|
+
function projectSummarySection(db: Database): string {
|
|
113
|
+
const lines: string[] = ["## This Project", ""];
|
|
114
|
+
|
|
115
|
+
const fileCount = (db.query("SELECT COUNT(DISTINCT file) as c FROM nodes").get() as { c: number })
|
|
116
|
+
.c;
|
|
117
|
+
const nodeCount = (db.query("SELECT COUNT(*) as c FROM nodes").get() as { c: number }).c;
|
|
118
|
+
const edgeCount = (db.query("SELECT COUNT(*) as c FROM edges").get() as { c: number }).c;
|
|
119
|
+
|
|
120
|
+
lines.push(`${fileCount} files, ${nodeCount} symbols, ${edgeCount} call edges in the graph.`);
|
|
121
|
+
|
|
122
|
+
const existingTags = db
|
|
123
|
+
.query("SELECT kind, value, node_id FROM tags ORDER BY kind, value")
|
|
124
|
+
.all() as { kind: string; value: string; node_id: string }[];
|
|
125
|
+
|
|
126
|
+
if (existingTags.length > 0) {
|
|
127
|
+
lines.push("");
|
|
128
|
+
lines.push("### Already Tagged");
|
|
129
|
+
lines.push("");
|
|
130
|
+
for (const tag of existingTags) {
|
|
131
|
+
lines.push(`- \`@lattice:${tag.kind} ${tag.value}\` on \`${tag.node_id}\``);
|
|
132
|
+
}
|
|
133
|
+
lines.push("");
|
|
134
|
+
lines.push("Review these existing tags and add any that are missing.");
|
|
135
|
+
} else {
|
|
136
|
+
lines.push("");
|
|
137
|
+
lines.push("No tags exist yet. Read the source files and add tags where appropriate.");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return lines.join("\n");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The complete step-by-step workflow with validation checkpoints. */
|
|
144
|
+
function workflowSection(): string {
|
|
145
|
+
return `## Workflow
|
|
146
|
+
|
|
147
|
+
Follow these steps in order. Do not skip validation steps.
|
|
148
|
+
|
|
149
|
+
### Step 1: Tag entry points
|
|
150
|
+
|
|
151
|
+
Read the source files and identify all entry points — route handlers, CLI commands, cron jobs, queue consumers, event listeners. Add \`@lattice:flow <name>\` above each one.
|
|
152
|
+
|
|
153
|
+
Use domain names for flows: "checkout", "user-registration", "invoice-generation" — not function names.
|
|
154
|
+
|
|
155
|
+
### Step 2: Tag boundaries
|
|
156
|
+
|
|
157
|
+
Identify all functions that call external systems — APIs, databases, caches, file storage, third-party SDKs. Add \`@lattice:boundary <system>\` above each one.
|
|
158
|
+
|
|
159
|
+
Use the external system name: "stripe", "postgres", "redis", "s3" — not the function or library name.
|
|
160
|
+
|
|
161
|
+
### Step 3: Tag events
|
|
162
|
+
|
|
163
|
+
If the codebase uses event-driven patterns, identify publishers and consumers. Add \`@lattice:emits <event>\` and \`@lattice:handles <event>\` where applicable. Event names must match between emitters and handlers.
|
|
164
|
+
|
|
165
|
+
### Step 4: Rebuild and lint
|
|
166
|
+
|
|
167
|
+
\`\`\`bash
|
|
168
|
+
lattice build && lattice lint
|
|
169
|
+
\`\`\`
|
|
170
|
+
|
|
171
|
+
Fix any errors reported by lint:
|
|
172
|
+
- Missing tags on detected entry points or boundary calls
|
|
173
|
+
- Invalid tags (wrong placement, bad syntax)
|
|
174
|
+
- Typos in tag names
|
|
175
|
+
- Orphaned events (emits without handles, or vice versa)
|
|
176
|
+
- Stale tags (boundary tag on a function that no longer calls that system)
|
|
177
|
+
|
|
178
|
+
Repeat this step until lint reports zero errors.
|
|
179
|
+
|
|
180
|
+
### Step 5: Verify flows
|
|
181
|
+
|
|
182
|
+
Run \`lattice overview\` and check:
|
|
183
|
+
- Are all business flows listed?
|
|
184
|
+
- Are all external systems represented in boundaries?
|
|
185
|
+
- Are all event connections shown?
|
|
186
|
+
|
|
187
|
+
If anything is missing, go back to steps 1-3 and add the missing tags.
|
|
188
|
+
|
|
189
|
+
### Step 6: Verify call trees
|
|
190
|
+
|
|
191
|
+
For each flow listed in \`lattice overview\`, run:
|
|
192
|
+
|
|
193
|
+
\`\`\`bash
|
|
194
|
+
lattice flow <name>
|
|
195
|
+
\`\`\`
|
|
196
|
+
|
|
197
|
+
Check each call tree:
|
|
198
|
+
- Does it start at the correct entry point?
|
|
199
|
+
- Does it reach the expected boundaries?
|
|
200
|
+
- Are there functions in the tree that should be boundaries but aren't tagged?
|
|
201
|
+
- Does event propagation cross into the expected handlers?
|
|
202
|
+
|
|
203
|
+
If a call tree is missing expected functions, those functions may not be reachable from the entry point through the call graph. Check if there are missing call edges (dynamic dispatch, dependency injection) and consider whether additional tags are needed.
|
|
204
|
+
|
|
205
|
+
### Step 7: Verify impact
|
|
206
|
+
|
|
207
|
+
For each boundary, run:
|
|
208
|
+
|
|
209
|
+
\`\`\`bash
|
|
210
|
+
lattice impact <symbol>
|
|
211
|
+
\`\`\`
|
|
212
|
+
|
|
213
|
+
Check that the affected flows make sense. If a boundary is used by flows you didn't expect, investigate whether the flow tagging is correct.
|
|
214
|
+
|
|
215
|
+
### Done
|
|
216
|
+
|
|
217
|
+
The codebase is tagged when:
|
|
218
|
+
- \`lattice lint\` reports zero errors
|
|
219
|
+
- \`lattice overview\` shows all expected flows, boundaries, and events
|
|
220
|
+
- Each \`lattice flow <name>\` shows a complete, sensible call tree
|
|
221
|
+
- \`lattice impact\` on key functions shows the expected affected flows`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export { executePopulate };
|