@toolbaux/guardian 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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +366 -0
  3. package/dist/adapters/csharp-adapter.js +149 -0
  4. package/dist/adapters/go-adapter.js +96 -0
  5. package/dist/adapters/index.js +16 -0
  6. package/dist/adapters/java-adapter.js +122 -0
  7. package/dist/adapters/python-adapter.js +183 -0
  8. package/dist/adapters/runner.js +69 -0
  9. package/dist/adapters/types.js +1 -0
  10. package/dist/adapters/typescript-adapter.js +179 -0
  11. package/dist/benchmarking/framework.js +91 -0
  12. package/dist/cli.js +343 -0
  13. package/dist/commands/analyze-depth.js +43 -0
  14. package/dist/commands/api-spec-extractor.js +52 -0
  15. package/dist/commands/breaking-change-analyzer.js +334 -0
  16. package/dist/commands/config-compliance.js +219 -0
  17. package/dist/commands/constraints.js +221 -0
  18. package/dist/commands/context.js +101 -0
  19. package/dist/commands/data-flow-tracer.js +291 -0
  20. package/dist/commands/dependency-impact-analyzer.js +27 -0
  21. package/dist/commands/diff.js +146 -0
  22. package/dist/commands/discrepancy.js +71 -0
  23. package/dist/commands/doc-generate.js +163 -0
  24. package/dist/commands/doc-html.js +120 -0
  25. package/dist/commands/drift.js +88 -0
  26. package/dist/commands/extract.js +16 -0
  27. package/dist/commands/feature-context.js +116 -0
  28. package/dist/commands/generate.js +339 -0
  29. package/dist/commands/guard.js +182 -0
  30. package/dist/commands/init.js +209 -0
  31. package/dist/commands/intel.js +20 -0
  32. package/dist/commands/license-dependency-auditor.js +33 -0
  33. package/dist/commands/performance-hotspot-profiler.js +42 -0
  34. package/dist/commands/search.js +314 -0
  35. package/dist/commands/security-boundary-auditor.js +359 -0
  36. package/dist/commands/simulate.js +294 -0
  37. package/dist/commands/summary.js +27 -0
  38. package/dist/commands/test-coverage-mapper.js +264 -0
  39. package/dist/commands/verify-drift.js +62 -0
  40. package/dist/config.js +441 -0
  41. package/dist/extract/ai-context-hints.js +107 -0
  42. package/dist/extract/analyzers/backend.js +1704 -0
  43. package/dist/extract/analyzers/depth.js +264 -0
  44. package/dist/extract/analyzers/frontend.js +2221 -0
  45. package/dist/extract/api-usage-tracker.js +19 -0
  46. package/dist/extract/cache.js +53 -0
  47. package/dist/extract/codebase-intel.js +190 -0
  48. package/dist/extract/compress.js +452 -0
  49. package/dist/extract/context-block.js +356 -0
  50. package/dist/extract/contracts.js +183 -0
  51. package/dist/extract/discrepancies.js +233 -0
  52. package/dist/extract/docs-loader.js +110 -0
  53. package/dist/extract/docs.js +2379 -0
  54. package/dist/extract/drift.js +1578 -0
  55. package/dist/extract/duplicates.js +435 -0
  56. package/dist/extract/feature-arcs.js +138 -0
  57. package/dist/extract/graph.js +76 -0
  58. package/dist/extract/html-doc.js +1409 -0
  59. package/dist/extract/ignore.js +45 -0
  60. package/dist/extract/index.js +455 -0
  61. package/dist/extract/llm-client.js +159 -0
  62. package/dist/extract/pattern-registry.js +141 -0
  63. package/dist/extract/product-doc.js +497 -0
  64. package/dist/extract/python.js +1202 -0
  65. package/dist/extract/runtime.js +193 -0
  66. package/dist/extract/schema-evolution-validator.js +35 -0
  67. package/dist/extract/test-gap-analyzer.js +20 -0
  68. package/dist/extract/tests.js +74 -0
  69. package/dist/extract/types.js +1 -0
  70. package/dist/extract/validate-backend.js +30 -0
  71. package/dist/extract/writer.js +11 -0
  72. package/dist/output-layout.js +37 -0
  73. package/dist/project-discovery.js +309 -0
  74. package/dist/schema/architecture.js +350 -0
  75. package/dist/schema/feature-spec.js +89 -0
  76. package/dist/schema/index.js +8 -0
  77. package/dist/schema/ux.js +46 -0
  78. package/package.json +75 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * `guardian init` — initialize guardian for a project.
3
+ *
4
+ * Creates:
5
+ * 1. guardian.config.json (if missing)
6
+ * 2. .specs/ directory
7
+ * 3. Pre-commit hook that auto-runs extract + context injection
8
+ * 4. Injects guardian context block into CLAUDE.md
9
+ * 5. Adds .specs/ to .gitignore exclusion (tracked by default)
10
+ *
11
+ * Safe to run multiple times — only creates what's missing.
12
+ */
13
+ import fs from "node:fs/promises";
14
+ import path from "node:path";
15
+ const DEFAULT_CONFIG = {
16
+ project: {
17
+ backendRoot: "./backend",
18
+ frontendRoot: "./frontend",
19
+ },
20
+ docs: {
21
+ mode: "full",
22
+ },
23
+ };
24
+ const HOOK_SCRIPT = `#!/bin/sh
25
+ # guardian pre-commit hook — keeps architecture context fresh
26
+ # Installed by: guardian init
27
+
28
+ # Only run if guardian is available
29
+ if ! command -v guardian >/dev/null 2>&1; then
30
+ exit 0
31
+ fi
32
+
33
+ # Run extract + ai-context generation (fast, ~2-5s)
34
+ guardian extract --output .specs 2>/dev/null
35
+ guardian generate --ai-context --output .specs 2>/dev/null
36
+
37
+ # Inject context into CLAUDE.md if it exists
38
+ if [ -f CLAUDE.md ]; then
39
+ guardian context --input .specs --output CLAUDE.md 2>/dev/null
40
+ fi
41
+
42
+ # Auto-stage the updated files
43
+ git add .specs/machine/architecture-context.md 2>/dev/null
44
+ git add CLAUDE.md 2>/dev/null
45
+
46
+ exit 0
47
+ `;
48
+ export async function runInit(options) {
49
+ const root = path.resolve(options.projectRoot || process.cwd());
50
+ const specsDir = path.join(root, options.output || ".specs");
51
+ console.log(`Initializing Guardian in ${root}\n`);
52
+ // 1. Create .specs/ directory
53
+ await fs.mkdir(path.join(specsDir, "machine", "docs"), { recursive: true });
54
+ await fs.mkdir(path.join(specsDir, "human", "docs"), { recursive: true });
55
+ console.log(" ✓ Created .specs/ directory");
56
+ // 2. Create guardian.config.json if missing
57
+ const configPath = path.join(root, "guardian.config.json");
58
+ if (!(await fileExists(configPath))) {
59
+ const config = { ...DEFAULT_CONFIG };
60
+ // Auto-detect roots
61
+ if (options.backendRoot) {
62
+ config.project.backendRoot = options.backendRoot;
63
+ }
64
+ else {
65
+ for (const candidate of ["./backend", "./server", "./api", "./src"]) {
66
+ if (await dirExists(path.join(root, candidate))) {
67
+ config.project.backendRoot = candidate;
68
+ break;
69
+ }
70
+ }
71
+ }
72
+ if (options.frontendRoot) {
73
+ config.project.frontendRoot = options.frontendRoot;
74
+ }
75
+ else {
76
+ for (const candidate of ["./frontend", "./client", "./web", "./app"]) {
77
+ if (await dirExists(path.join(root, candidate))) {
78
+ config.project.frontendRoot = candidate;
79
+ break;
80
+ }
81
+ }
82
+ }
83
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
84
+ console.log(` ✓ Created guardian.config.json (backend: ${config.project.backendRoot}, frontend: ${config.project.frontendRoot})`);
85
+ }
86
+ else {
87
+ console.log(" · guardian.config.json already exists");
88
+ }
89
+ // 3. Install pre-commit hook
90
+ if (!options.skipHook) {
91
+ const gitDir = path.join(root, ".git");
92
+ if (await dirExists(gitDir)) {
93
+ const hooksDir = path.join(gitDir, "hooks");
94
+ await fs.mkdir(hooksDir, { recursive: true });
95
+ const hookPath = path.join(hooksDir, "pre-commit");
96
+ let shouldInstall = true;
97
+ if (await fileExists(hookPath)) {
98
+ const existing = await fs.readFile(hookPath, "utf8");
99
+ if (existing.includes("guardian")) {
100
+ console.log(" · Pre-commit hook already has guardian");
101
+ shouldInstall = false;
102
+ }
103
+ else {
104
+ // Append to existing hook
105
+ const appended = existing.trimEnd() + "\n\n" + HOOK_SCRIPT.split("\n").slice(1).join("\n");
106
+ await fs.writeFile(hookPath, appended, "utf8");
107
+ await fs.chmod(hookPath, 0o755);
108
+ console.log(" ✓ Appended guardian to existing pre-commit hook");
109
+ shouldInstall = false;
110
+ }
111
+ }
112
+ if (shouldInstall) {
113
+ await fs.writeFile(hookPath, HOOK_SCRIPT, "utf8");
114
+ await fs.chmod(hookPath, 0o755);
115
+ console.log(" ✓ Installed pre-commit hook (.git/hooks/pre-commit)");
116
+ }
117
+ }
118
+ else {
119
+ console.log(" · No .git directory found — skipping hook installation");
120
+ }
121
+ }
122
+ // 4. Inject context block into CLAUDE.md
123
+ const claudeMdPath = path.join(root, "CLAUDE.md");
124
+ if (await fileExists(claudeMdPath)) {
125
+ const existing = await fs.readFile(claudeMdPath, "utf8");
126
+ if (!existing.includes("guardian:auto-context")) {
127
+ const block = [
128
+ "",
129
+ "<!-- guardian:auto-context -->",
130
+ "<!-- This block is auto-updated by guardian. Do not edit manually. -->",
131
+ `<!-- Run: guardian extract --output .specs && guardian context --input .specs --output CLAUDE.md -->`,
132
+ "<!-- /guardian:auto-context -->",
133
+ "",
134
+ ].join("\n");
135
+ await fs.writeFile(claudeMdPath, existing.trimEnd() + "\n" + block, "utf8");
136
+ console.log(" ✓ Added guardian placeholder to CLAUDE.md");
137
+ }
138
+ else {
139
+ console.log(" · CLAUDE.md already has guardian context block");
140
+ }
141
+ }
142
+ else {
143
+ // Create minimal CLAUDE.md
144
+ const projectName = path.basename(root);
145
+ const content = [
146
+ `# ${projectName}`,
147
+ "",
148
+ "## SpecGuard Architecture Context",
149
+ "",
150
+ "<!-- guardian:auto-context -->",
151
+ "<!-- This block is auto-updated by guardian. Do not edit manually. -->",
152
+ `<!-- Run: guardian extract --output .specs && guardian context --input .specs --output CLAUDE.md -->`,
153
+ "<!-- /guardian:auto-context -->",
154
+ "",
155
+ ].join("\n");
156
+ await fs.writeFile(claudeMdPath, content, "utf8");
157
+ console.log(" ✓ Created CLAUDE.md with guardian context block");
158
+ }
159
+ // 5. Run initial extract + context injection
160
+ console.log("\n Running initial extraction...");
161
+ try {
162
+ const { runExtract } = await import("./extract.js");
163
+ await runExtract({
164
+ projectRoot: root,
165
+ backendRoot: options.backendRoot,
166
+ frontendRoot: options.frontendRoot,
167
+ output: specsDir,
168
+ includeFileGraph: true,
169
+ });
170
+ const { runGenerate } = await import("./generate.js");
171
+ await runGenerate({
172
+ projectRoot: root,
173
+ backendRoot: options.backendRoot,
174
+ frontendRoot: options.frontendRoot,
175
+ output: specsDir,
176
+ aiContext: true,
177
+ });
178
+ // Inject context into CLAUDE.md
179
+ const { runContext } = await import("./context.js");
180
+ await runContext({
181
+ input: specsDir,
182
+ output: claudeMdPath,
183
+ });
184
+ console.log("\n✓ SpecGuard initialized. Architecture context is in CLAUDE.md and .specs/");
185
+ console.log(" Pre-commit hook will keep it fresh on every commit.");
186
+ }
187
+ catch (err) {
188
+ console.error(`\n ⚠ Initial extraction failed: ${err.message}`);
189
+ console.log(" Run manually: guardian extract --output .specs");
190
+ }
191
+ }
192
+ async function fileExists(p) {
193
+ try {
194
+ const stat = await fs.stat(p);
195
+ return stat.isFile();
196
+ }
197
+ catch {
198
+ return false;
199
+ }
200
+ }
201
+ async function dirExists(p) {
202
+ try {
203
+ const stat = await fs.stat(p);
204
+ return stat.isDirectory();
205
+ }
206
+ catch {
207
+ return false;
208
+ }
209
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `specguard intel` — build codebase-intelligence.json from existing snapshots.
3
+ *
4
+ * Reads: specs-out/machine/architecture.snapshot.yaml + ux.snapshot.yaml
5
+ * Writes: specs-out/machine/codebase-intelligence.json
6
+ *
7
+ * Also auto-runs at the end of `specguard extract`.
8
+ */
9
+ import path from "node:path";
10
+ import { writeCodebaseIntelligence } from "../extract/codebase-intel.js";
11
+ import { getOutputLayout } from "../output-layout.js";
12
+ export async function runIntel(options) {
13
+ const specsDir = path.resolve(options.specs);
14
+ const layout = getOutputLayout(specsDir);
15
+ const outputPath = options.output
16
+ ? path.resolve(options.output)
17
+ : path.join(layout.machineDir, "codebase-intelligence.json");
18
+ await writeCodebaseIntelligence(specsDir, outputPath);
19
+ console.log(`Wrote ${outputPath}`);
20
+ }
@@ -0,0 +1,33 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ export async function run() {
4
+ const root = process.cwd();
5
+ const pkgPath = path.join(root, 'package.json');
6
+ let pkg = {};
7
+ try {
8
+ pkg = JSON.parse(await fs.promises.readFile(pkgPath, 'utf8'));
9
+ }
10
+ catch (_) { }
11
+ const deps = Object.keys(Object.assign({}, pkg.dependencies || {}, pkg.devDependencies || {}));
12
+ const results = [];
13
+ for (const d of deps) {
14
+ try {
15
+ const p = path.join(root, 'node_modules', d, 'package.json');
16
+ const text = await fs.promises.readFile(p, 'utf8');
17
+ const pj = JSON.parse(text);
18
+ results.push({ name: d, license: pj.license || pj.licenses && pj.licenses[0] && pj.licenses[0].type });
19
+ }
20
+ catch (_) {
21
+ results.push({ name: d, license: 'UNKNOWN' });
22
+ }
23
+ }
24
+ const out = path.join(root, 'out', 'license-audit.json');
25
+ await fs.promises.mkdir(path.dirname(out), { recursive: true });
26
+ await fs.promises.writeFile(out, JSON.stringify(results, null, 2), 'utf8');
27
+ console.log('Wrote license audit to', out);
28
+ return results;
29
+ }
30
+ const _isMain = (typeof require !== 'undefined' && (require.main === module)) || (process.argv[1] && process.argv[1].endsWith('license-dependency-auditor.ts'));
31
+ if (_isMain) {
32
+ run().catch(err => { console.error(err); process.exit(1); });
33
+ }
@@ -0,0 +1,42 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ async function readDirRecursive(dir) {
4
+ try {
5
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
6
+ const files = [];
7
+ for (const e of entries) {
8
+ const res = path.join(dir, e.name);
9
+ if (e.isDirectory())
10
+ files.push(...await readDirRecursive(res));
11
+ else
12
+ files.push(res);
13
+ }
14
+ return files;
15
+ }
16
+ catch (err) {
17
+ return [];
18
+ }
19
+ }
20
+ export async function run() {
21
+ const root = process.cwd();
22
+ const srcDir = path.join(root, 'src');
23
+ const files = await readDirRecursive(srcDir);
24
+ const candidates = [];
25
+ for (const f of files) {
26
+ if (!f.endsWith('.ts') && !f.endsWith('.js'))
27
+ continue;
28
+ const text = await fs.promises.readFile(f, 'utf8');
29
+ const ln = text.split('\n').length;
30
+ if (ln > 200)
31
+ candidates.push({ file: path.relative(root, f), lines: ln });
32
+ }
33
+ const out = path.join(root, 'out', 'perf-hotspots.json');
34
+ await fs.promises.mkdir(path.dirname(out), { recursive: true });
35
+ await fs.promises.writeFile(out, JSON.stringify(candidates, null, 2), 'utf8');
36
+ console.log('Wrote performance hotspots to', out);
37
+ return candidates;
38
+ }
39
+ const _isMain = (typeof require !== 'undefined' && (require.main === module)) || (process.argv[1] && process.argv[1].endsWith('performance-hotspot-profiler.ts'));
40
+ if (_isMain) {
41
+ run().catch(err => { console.error(err); process.exit(1); });
42
+ }
@@ -0,0 +1,314 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import yaml from "js-yaml";
4
+ import { loadHeatmap } from "../extract/compress.js";
5
+ import { resolveMachineInputDir } from "../output-layout.js";
6
+ export async function runSearch(options) {
7
+ const inputDir = await resolveMachineInputDir(options.input || "specs-out");
8
+ const { architecture, ux } = await loadSnapshots(inputDir);
9
+ const heatmap = await loadHeatmap(inputDir);
10
+ const types = normalizeTypes(options.types);
11
+ const matches = searchSnapshots({
12
+ architecture,
13
+ ux,
14
+ query: options.query,
15
+ types,
16
+ heatmap
17
+ });
18
+ const content = renderSearchMarkdown(options.query, matches);
19
+ if (options.output) {
20
+ const outputPath = path.resolve(options.output);
21
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
22
+ await fs.writeFile(outputPath, content, "utf8");
23
+ console.log(`Wrote ${outputPath}`);
24
+ return;
25
+ }
26
+ console.log(content);
27
+ }
28
+ async function loadSnapshots(inputDir) {
29
+ const architecturePath = path.join(inputDir, "architecture.snapshot.yaml");
30
+ const uxPath = path.join(inputDir, "ux.snapshot.yaml");
31
+ let architectureRaw;
32
+ let uxRaw;
33
+ try {
34
+ [architectureRaw, uxRaw] = await Promise.all([
35
+ fs.readFile(architecturePath, "utf8"),
36
+ fs.readFile(uxPath, "utf8")
37
+ ]);
38
+ }
39
+ catch (error) {
40
+ if (error.code === "ENOENT") {
41
+ throw new Error(`Could not find snapshots in ${inputDir}. Run \`specguard extract\` first.`);
42
+ }
43
+ throw error;
44
+ }
45
+ return {
46
+ architecture: yaml.load(architectureRaw),
47
+ ux: yaml.load(uxRaw)
48
+ };
49
+ }
50
+ function normalizeTypes(types) {
51
+ if (!types || types.length === 0) {
52
+ return new Set(["models", "endpoints", "components", "modules", "tasks"]);
53
+ }
54
+ const normalized = new Set();
55
+ for (const entry of types) {
56
+ for (const part of entry.split(",").map((value) => value.trim().toLowerCase())) {
57
+ if (part === "models" ||
58
+ part === "endpoints" ||
59
+ part === "components" ||
60
+ part === "modules" ||
61
+ part === "tasks") {
62
+ normalized.add(part);
63
+ }
64
+ }
65
+ }
66
+ return normalized.size > 0
67
+ ? normalized
68
+ : new Set(["models", "endpoints", "components", "modules", "tasks"]);
69
+ }
70
+ function tokenize(value) {
71
+ return value
72
+ .toLowerCase()
73
+ .split(/[^a-z0-9_/.{}-]+/)
74
+ .map((token) => token.trim())
75
+ .filter(Boolean);
76
+ }
77
+ function scoreItem(queryTokens, item) {
78
+ if (queryTokens.length === 0) {
79
+ return 0;
80
+ }
81
+ const normalizedName = item.name.toLowerCase();
82
+ const normalizedFile = (item.file ?? "").toLowerCase();
83
+ const normalizedText = item.text
84
+ .filter((entry) => typeof entry === "string")
85
+ .map((entry) => entry.toLowerCase());
86
+ let total = 0;
87
+ for (const token of queryTokens) {
88
+ if (normalizedName === token) {
89
+ total += 1;
90
+ continue;
91
+ }
92
+ if (normalizedName.includes(token)) {
93
+ total += 0.7;
94
+ continue;
95
+ }
96
+ if (normalizedText.some((entry) => entry.includes(token))) {
97
+ total += 0.4;
98
+ continue;
99
+ }
100
+ if (normalizedFile.includes(token)) {
101
+ total += 0.2;
102
+ continue;
103
+ }
104
+ }
105
+ const phrase = queryTokens.join(" ");
106
+ if (phrase && normalizedName.includes(phrase)) {
107
+ total += 0.15;
108
+ }
109
+ return Math.min(1, total / queryTokens.length);
110
+ }
111
+ function searchSnapshots(params) {
112
+ const { architecture, ux, query, types, heatmap } = params;
113
+ const queryTokens = tokenize(query);
114
+ const matches = [];
115
+ const pageUsage = buildComponentPageUsage(ux);
116
+ const moduleHeatmap = new Map((heatmap?.levels.find((level) => level.level === "module")?.entries ?? []).map((entry) => [
117
+ entry.id,
118
+ entry.score
119
+ ]));
120
+ if (types.has("models")) {
121
+ for (const model of architecture.data_models) {
122
+ const score = scoreItem(queryTokens, {
123
+ name: model.name,
124
+ file: model.file,
125
+ text: [...model.fields, ...model.relationships, model.framework]
126
+ });
127
+ if (score <= 0) {
128
+ continue;
129
+ }
130
+ matches.push({
131
+ type: "models",
132
+ name: model.name,
133
+ score,
134
+ markdown: [
135
+ `**${model.name}** · ${model.file}`,
136
+ `Fields: ${formatList(model.fields, 8)}`,
137
+ `Relations: ${formatList(model.relationships, 8)}`
138
+ ]
139
+ });
140
+ }
141
+ }
142
+ if (types.has("endpoints")) {
143
+ for (const endpoint of architecture.endpoints) {
144
+ const score = scoreItem(queryTokens, {
145
+ name: `${endpoint.method} ${endpoint.path}`,
146
+ file: endpoint.file,
147
+ text: [
148
+ endpoint.handler,
149
+ endpoint.module,
150
+ endpoint.request_schema ?? "",
151
+ endpoint.response_schema ?? "",
152
+ ...endpoint.service_calls
153
+ ]
154
+ });
155
+ if (score <= 0) {
156
+ continue;
157
+ }
158
+ matches.push({
159
+ type: "endpoints",
160
+ name: `${endpoint.method} ${endpoint.path}`,
161
+ score,
162
+ markdown: [
163
+ `${endpoint.method.padEnd(4, " ")} ${endpoint.path} → ${endpoint.handler} (${endpoint.file})`,
164
+ `Module: ${endpoint.module} · Request: ${endpoint.request_schema ?? "none"} · Response: ${endpoint.response_schema ?? "none"}`
165
+ ]
166
+ });
167
+ }
168
+ }
169
+ if (types.has("components")) {
170
+ for (const component of ux.components) {
171
+ const score = scoreItem(queryTokens, {
172
+ name: component.name,
173
+ file: component.file,
174
+ text: [
175
+ component.kind,
176
+ component.export_kind,
177
+ ...(component.props ?? []).map((prop) => `${prop.name}:${prop.type}`)
178
+ ]
179
+ });
180
+ if (score <= 0) {
181
+ continue;
182
+ }
183
+ matches.push({
184
+ type: "components",
185
+ name: component.name,
186
+ score,
187
+ markdown: [
188
+ `**${component.name}** · ${component.file} · import: ${component.export_kind ?? "unknown"}`,
189
+ `Props: ${formatProps(component.props)}`,
190
+ `Used by: ${formatList(pageUsage.get(component.id) ?? [], 4)}`
191
+ ]
192
+ });
193
+ }
194
+ }
195
+ if (types.has("modules")) {
196
+ for (const module of architecture.modules) {
197
+ const score = scoreItem(queryTokens, {
198
+ name: module.id,
199
+ file: module.path,
200
+ text: [...module.files, ...module.endpoints, ...module.imports]
201
+ });
202
+ if (score <= 0) {
203
+ continue;
204
+ }
205
+ const couplingScore = moduleHeatmap.get(module.id);
206
+ matches.push({
207
+ type: "modules",
208
+ name: module.id,
209
+ score,
210
+ markdown: [
211
+ `**${module.id}** · ${module.path} · ${module.files.length} files${typeof couplingScore === "number"
212
+ ? ` · coupling score: ${couplingScore.toFixed(2)}`
213
+ : ""}`,
214
+ `Contains: ${formatList(module.files, 4)}`
215
+ ]
216
+ });
217
+ }
218
+ }
219
+ if (types.has("tasks")) {
220
+ for (const task of architecture.tasks) {
221
+ const score = scoreItem(queryTokens, {
222
+ name: task.name,
223
+ file: task.file,
224
+ text: [task.kind, task.queue ?? "", task.schedule ?? ""]
225
+ });
226
+ if (score <= 0) {
227
+ continue;
228
+ }
229
+ matches.push({
230
+ type: "tasks",
231
+ name: task.name,
232
+ score,
233
+ markdown: [
234
+ `**${task.name}** · ${task.file}`,
235
+ `Kind: ${task.kind}${task.queue ? ` · Queue: ${task.queue}` : ""}${task.schedule ? ` · Schedule: ${task.schedule}` : ""}`
236
+ ]
237
+ });
238
+ }
239
+ }
240
+ return matches.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
241
+ }
242
+ function buildComponentPageUsage(ux) {
243
+ const usage = new Map();
244
+ for (const page of ux.pages) {
245
+ const pageLabel = `${page.component} (${page.path})`;
246
+ const ids = new Set([
247
+ page.component_id,
248
+ ...page.components_direct_ids,
249
+ ...page.components_descendants_ids
250
+ ]);
251
+ for (const id of ids) {
252
+ const entry = usage.get(id) ?? new Set();
253
+ entry.add(pageLabel);
254
+ usage.set(id, entry);
255
+ }
256
+ }
257
+ return new Map(Array.from(usage.entries()).map(([id, pages]) => [
258
+ id,
259
+ Array.from(pages).sort((a, b) => a.localeCompare(b))
260
+ ]));
261
+ }
262
+ function formatList(items, limit) {
263
+ if (!items || items.length === 0) {
264
+ return "none";
265
+ }
266
+ if (items.length <= limit) {
267
+ return items.join(", ");
268
+ }
269
+ return `${items.slice(0, limit).join(", ")} +${items.length - limit} more`;
270
+ }
271
+ function formatProps(props) {
272
+ if (!props || props.length === 0) {
273
+ return "none";
274
+ }
275
+ return props
276
+ .slice(0, 6)
277
+ .map((prop) => `${prop.name}${prop.optional ? "?" : ""}: ${prop.type}`)
278
+ .join(", ");
279
+ }
280
+ function renderSearchMarkdown(query, matches) {
281
+ const grouped = new Map();
282
+ for (const match of matches) {
283
+ const entry = grouped.get(match.type) ?? [];
284
+ entry.push(match);
285
+ grouped.set(match.type, entry);
286
+ }
287
+ const labels = [
288
+ ["models", "Data Models"],
289
+ ["endpoints", "Endpoints"],
290
+ ["components", "Components"],
291
+ ["modules", "Modules"],
292
+ ["tasks", "Tasks"]
293
+ ];
294
+ const lines = [];
295
+ lines.push(`# Search: "${query}" - ${matches.length} matches`);
296
+ lines.push("");
297
+ if (matches.length === 0) {
298
+ lines.push("*No matches found.*");
299
+ return lines.join("\n");
300
+ }
301
+ for (const [type, label] of labels) {
302
+ const entries = grouped.get(type) ?? [];
303
+ if (entries.length === 0) {
304
+ continue;
305
+ }
306
+ lines.push(`## ${label} (${entries.length})`);
307
+ lines.push("");
308
+ for (const entry of entries.slice(0, 8)) {
309
+ lines.push(...entry.markdown);
310
+ lines.push("");
311
+ }
312
+ }
313
+ return lines.join("\n").trimEnd();
314
+ }