@thelogicatelier/sylva 1.0.4 → 1.0.10

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 (39) hide show
  1. package/README.md +20 -0
  2. package/dist/awareness/braveSearch.d.ts +18 -0
  3. package/dist/awareness/braveSearch.js +134 -0
  4. package/dist/awareness/detector.d.ts +18 -0
  5. package/dist/awareness/detector.js +176 -0
  6. package/dist/awareness/index.d.ts +11 -0
  7. package/dist/awareness/index.js +259 -0
  8. package/dist/awareness/manifestParsers/dotnetParsers.d.ts +13 -0
  9. package/dist/awareness/manifestParsers/dotnetParsers.js +151 -0
  10. package/dist/awareness/manifestParsers/goParsers.d.ts +9 -0
  11. package/dist/awareness/manifestParsers/goParsers.js +137 -0
  12. package/dist/awareness/manifestParsers/index.d.ts +13 -0
  13. package/dist/awareness/manifestParsers/index.js +182 -0
  14. package/dist/awareness/manifestParsers/javaParsers.d.ts +13 -0
  15. package/dist/awareness/manifestParsers/javaParsers.js +243 -0
  16. package/dist/awareness/manifestParsers/openclawParser.d.ts +6 -0
  17. package/dist/awareness/manifestParsers/openclawParser.js +103 -0
  18. package/dist/awareness/manifestParsers/packageJsonParser.d.ts +7 -0
  19. package/dist/awareness/manifestParsers/packageJsonParser.js +209 -0
  20. package/dist/awareness/manifestParsers/pythonParsers.d.ts +21 -0
  21. package/dist/awareness/manifestParsers/pythonParsers.js +344 -0
  22. package/dist/awareness/manifestParsers/rustParsers.d.ts +9 -0
  23. package/dist/awareness/manifestParsers/rustParsers.js +153 -0
  24. package/dist/awareness/manifestScanner.d.ts +11 -0
  25. package/dist/awareness/manifestScanner.js +182 -0
  26. package/dist/awareness/types.d.ts +105 -0
  27. package/dist/awareness/types.js +6 -0
  28. package/dist/awareness/versionResolver.d.ts +17 -0
  29. package/dist/awareness/versionResolver.js +62 -0
  30. package/dist/awareness/webGrounding.d.ts +20 -0
  31. package/dist/awareness/webGrounding.js +102 -0
  32. package/dist/cli.js +19 -4
  33. package/dist/constants.js +11 -0
  34. package/dist/modules.d.ts +5 -2
  35. package/dist/modules.js +17 -4
  36. package/dist/prompts.d.ts +6 -0
  37. package/dist/prompts.js +5 -2
  38. package/dist/utils.js +12 -6
  39. package/package.json +2 -2
package/README.md CHANGED
@@ -80,9 +80,29 @@ npx @thelogicatelier/sylva --local-repository . -m openai/gpt-5.2 -i 25
80
80
 
81
81
  For detailed guidance, see the [Choosing the Right Model](https://achatt89.github.io/sylva/models/choosing.html) and [Iteration Depth Guide](https://achatt89.github.io/sylva/models/iterations.html) docs.
82
82
 
83
+ ### Framework Awareness (NEW)
84
+
85
+ Sylva now includes **deterministic framework detection** that scans the entire repository (including nested subprojects and monorepos) for manifest files before invoking the LLM. This prevents framework hallucination and produces more accurate `AGENTS.md` output.
86
+
87
+ **What it detects:**
88
+ - **OpenClaw** (`openclaw.json`) — treated as the primary orchestrator
89
+ - **Node.js/JS/TS** — React, Angular, Vue, Next.js, Express, NestJS, etc. from `package.json`
90
+ - **Python** — Django, Flask, FastAPI from `requirements.txt`, `pyproject.toml`, `Pipfile`
91
+ - **Java/JVM** — Spring Boot, Quarkus from `pom.xml`, `build.gradle`
92
+ - **.NET** — ASP.NET Core from `*.csproj`, `global.json`
93
+ - **Go** — Gin, Echo, Fiber from `go.mod`
94
+ - **Rust** — Actix, Axum, Tokio from `Cargo.toml`
95
+
96
+ **Version certainty:** Versions are only reported when explicitly found in manifest/lockfiles. Never assumed.
97
+
98
+ **Web grounding:** When `BRAVE_API_KEY` is set, Sylva fetches official docs for detected frameworks (version-specific when exact versions are known, latest fallback otherwise).
99
+
100
+ **Debug output:** `awareness.json` is saved alongside `AGENTS.md` for full transparency.
101
+
83
102
  ### Environment Overrides
84
103
  - `AUTOSKILL_MODEL`: Set this to `gemini` or `anthropic` or `openai` to change the default execution provider globally without providing `-m` on every execution.
85
104
  - `GITHUB_REPO_URL`: Hardcode a repository for the CLI parser to utilize if no explicit configuration flags are provided.
105
+ - `BRAVE_API_KEY`: Set this to enable web-grounded framework documentation retrieval via Brave Search. If not set, Sylva will still detect frameworks deterministically but will skip web reference gathering.
86
106
 
87
107
  ### Test Structure
88
108
  Unit tests are located at `tests/` which mirror the `/src` folder structure separating logical test groupings.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Brave Search API client with caching.
3
+ * Uses BRAVE_API_KEY env var. Gracefully degrades if missing.
4
+ */
5
+ import { WebSearchResult } from "./types";
6
+ interface BraveSearchOptions {
7
+ cacheDir?: string;
8
+ }
9
+ /**
10
+ * Search Brave API for a query. Returns parsed results.
11
+ * Caches results to disk if cacheDir is provided.
12
+ */
13
+ export declare function braveSearch(query: string, options?: BraveSearchOptions): Promise<WebSearchResult[]>;
14
+ /**
15
+ * Check if Brave Search is available (API key set).
16
+ */
17
+ export declare function isBraveSearchAvailable(): boolean;
18
+ export {};
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ /**
3
+ * Brave Search API client with caching.
4
+ * Uses BRAVE_API_KEY env var. Gracefully degrades if missing.
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.braveSearch = braveSearch;
41
+ exports.isBraveSearchAvailable = isBraveSearchAvailable;
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const crypto = __importStar(require("crypto"));
45
+ const BRAVE_SEARCH_URL = "https://api.search.brave.com/res/v1/web/search";
46
+ /**
47
+ * Generate a cache key from query parameters.
48
+ */
49
+ function cacheKey(query) {
50
+ return crypto.createHash("md5").update(query).digest("hex");
51
+ }
52
+ /**
53
+ * Search Brave API for a query. Returns parsed results.
54
+ * Caches results to disk if cacheDir is provided.
55
+ */
56
+ async function braveSearch(query, options) {
57
+ const apiKey = process.env.BRAVE_API_KEY;
58
+ if (!apiKey) {
59
+ // Graceful degradation — detection still works, only web references are skipped
60
+ return [];
61
+ }
62
+ // Check cache first
63
+ if (options?.cacheDir) {
64
+ const cachedFile = path.join(options.cacheDir, `${cacheKey(query)}.json`);
65
+ if (fs.existsSync(cachedFile)) {
66
+ try {
67
+ const cached = JSON.parse(fs.readFileSync(cachedFile, "utf-8"));
68
+ return cached;
69
+ }
70
+ catch {
71
+ // Invalid cache, continue with search
72
+ }
73
+ }
74
+ }
75
+ try {
76
+ const url = `${BRAVE_SEARCH_URL}?q=${encodeURIComponent(query)}&count=5`;
77
+ const response = await fetch(url, {
78
+ headers: {
79
+ Accept: "application/json",
80
+ "Accept-Encoding": "gzip",
81
+ "X-Subscription-Token": apiKey,
82
+ },
83
+ });
84
+ if (!response.ok) {
85
+ const statusText = response.statusText || "Unknown error";
86
+ if (response.status === 401 || response.status === 403) {
87
+ console.warn(`❌ Brave Search API authentication failed (HTTP ${response.status}: ${statusText}).\n` +
88
+ ` Your BRAVE_API_KEY may be invalid or expired.\n` +
89
+ ` Get a valid key at: https://brave.com/search/api/\n` +
90
+ ` Framework detection still works — only web doc references are affected.`);
91
+ }
92
+ else if (response.status === 429) {
93
+ console.warn(`⚠️ Brave Search API rate limit exceeded (HTTP 429).\n` +
94
+ ` Query: "${query}"\n` +
95
+ ` Wait a moment and retry, or check your Brave API plan limits.`);
96
+ }
97
+ else {
98
+ console.warn(`⚠️ Brave Search API returned HTTP ${response.status} (${statusText}) for query: "${query}"`);
99
+ }
100
+ return [];
101
+ }
102
+ const data = (await response.json());
103
+ const webResults = data.web?.results || [];
104
+ const results = webResults.slice(0, 5).map((r) => ({
105
+ title: r.title || "",
106
+ url: r.url || "",
107
+ snippet: r.description || "",
108
+ }));
109
+ // Write to cache
110
+ if (options?.cacheDir) {
111
+ fs.mkdirSync(options.cacheDir, { recursive: true });
112
+ const cachedFile = path.join(options.cacheDir, `${cacheKey(query)}.json`);
113
+ fs.writeFileSync(cachedFile, JSON.stringify(results, null, 2), "utf-8");
114
+ }
115
+ return results;
116
+ }
117
+ catch (error) {
118
+ const errMsg = error.message;
119
+ if (errMsg.includes("fetch") || errMsg.includes("ENOTFOUND") || errMsg.includes("network")) {
120
+ console.warn(`⚠️ Brave Search network error for query "${query}": ${errMsg}\n` +
121
+ ` Check your internet connection. Framework detection is unaffected.`);
122
+ }
123
+ else {
124
+ console.warn(`⚠️ Brave Search failed for query "${query}": ${errMsg}`);
125
+ }
126
+ return [];
127
+ }
128
+ }
129
+ /**
130
+ * Check if Brave Search is available (API key set).
131
+ */
132
+ function isBraveSearchAvailable() {
133
+ return !!process.env.BRAVE_API_KEY;
134
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Stack detector and architecture model builder.
3
+ * Deterministic — no LLM involvement.
4
+ */
5
+ import { Signal, StackInfo, ArchitectureModel, VersionInfo } from "./types";
6
+ /**
7
+ * Detect stacks from signals.
8
+ * Groups signals by frameworkId and scores confidence.
9
+ */
10
+ export declare function detectStacks(signals: Signal[]): StackInfo[];
11
+ /**
12
+ * Detect architecture model from stacks and signals.
13
+ */
14
+ export declare function detectArchitecture(stacks: StackInfo[], signals: Signal[]): ArchitectureModel;
15
+ /**
16
+ * Format a VersionInfo for display in AGENTS.md
17
+ */
18
+ export declare function formatVersionForDisplay(frameworkName: string, version: VersionInfo): string;
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ /**
3
+ * Stack detector and architecture model builder.
4
+ * Deterministic — no LLM involvement.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.detectStacks = detectStacks;
8
+ exports.detectArchitecture = detectArchitecture;
9
+ exports.formatVersionForDisplay = formatVersionForDisplay;
10
+ const versionResolver_1 = require("./versionResolver");
11
+ /** Confidence scoring rules */
12
+ const CONFIDENCE_SCORES = {
13
+ orchestrator: 95,
14
+ angular: 85,
15
+ react: 80,
16
+ nextjs: 85,
17
+ vue: 80,
18
+ nuxt: 85,
19
+ svelte: 80,
20
+ sveltekit: 85,
21
+ express: 75,
22
+ nestjs: 85,
23
+ fastify: 75,
24
+ django: 85,
25
+ flask: 75,
26
+ fastapi: 80,
27
+ "spring-boot": 90,
28
+ spring: 80,
29
+ "java-maven": 70,
30
+ "java-gradle": 70,
31
+ dotnet: 80,
32
+ "aspnet-core": 85,
33
+ go: 80,
34
+ rust: 80,
35
+ "actix-web": 85,
36
+ axum: 85,
37
+ nodejs: 60,
38
+ python: 60,
39
+ typescript: 65,
40
+ docker: 50,
41
+ };
42
+ /**
43
+ * Detect stacks from signals.
44
+ * Groups signals by frameworkId and scores confidence.
45
+ */
46
+ function detectStacks(signals) {
47
+ const grouped = new Map();
48
+ for (const signal of signals) {
49
+ // Skip tooling/entrypoint signals for stack detection
50
+ if (signal.kind === "tooling" || signal.kind === "entrypoint")
51
+ continue;
52
+ if (!grouped.has(signal.frameworkId)) {
53
+ grouped.set(signal.frameworkId, []);
54
+ }
55
+ grouped.get(signal.frameworkId).push(signal);
56
+ }
57
+ const stacks = [];
58
+ for (const [frameworkId, frameworkSignals] of grouped) {
59
+ const frameworkName = frameworkSignals[0].frameworkName;
60
+ const confidence = CONFIDENCE_SCORES[frameworkId] || 50;
61
+ const version = (0, versionResolver_1.resolveVersion)(frameworkSignals);
62
+ // Determine rootPath — use the shallowest signal's scope
63
+ const rootPaths = frameworkSignals.map((s) => s.scope.pathRoot).filter(Boolean);
64
+ const rootPath = rootPaths.length > 0 ? rootPaths.sort((a, b) => a.length - b.length)[0] : undefined;
65
+ stacks.push({
66
+ frameworkId,
67
+ frameworkName,
68
+ confidence,
69
+ versions: version.certainty !== "unknown" || version.value ? [version] : [],
70
+ evidence: frameworkSignals,
71
+ rootPath,
72
+ });
73
+ }
74
+ // Sort by confidence descending
75
+ stacks.sort((a, b) => b.confidence - a.confidence);
76
+ return stacks;
77
+ }
78
+ /**
79
+ * Detect architecture model from stacks and signals.
80
+ */
81
+ function detectArchitecture(stacks, signals) {
82
+ // Find orchestrator
83
+ const orchestratorSignals = signals.filter((s) => s.kind === "orchestrator");
84
+ let primaryOrchestrator;
85
+ if (orchestratorSignals.length > 0) {
86
+ const orcSignal = orchestratorSignals[0];
87
+ primaryOrchestrator = {
88
+ id: orcSignal.frameworkId,
89
+ configPath: orcSignal.evidence.file,
90
+ details: {
91
+ excerpt: orcSignal.evidence.excerpt,
92
+ },
93
+ };
94
+ }
95
+ // Build workloads — group stacks by rootPath
96
+ const rootPathGroups = new Map();
97
+ for (const stack of stacks) {
98
+ const root = stack.rootPath || ".";
99
+ if (!rootPathGroups.has(root)) {
100
+ rootPathGroups.set(root, []);
101
+ }
102
+ rootPathGroups.get(root).push(stack);
103
+ }
104
+ const workloads = [];
105
+ for (const [rootPath, groupStacks] of rootPathGroups) {
106
+ // Collect entrypoints from signals
107
+ const entrypoints = signals
108
+ .filter((s) => s.kind === "entrypoint" &&
109
+ (s.scope.pathRoot === rootPath || (!s.scope.pathRoot && rootPath === ".")))
110
+ .map((s) => s.evidence.excerpt || s.evidence.reason);
111
+ // Collect build tools
112
+ const buildTools = signals
113
+ .filter((s) => s.kind === "tooling" &&
114
+ (s.scope.pathRoot === rootPath || (!s.scope.pathRoot && rootPath === ".")))
115
+ .map((s) => s.frameworkName);
116
+ workloads.push({
117
+ id: rootPath.replace(/[^a-zA-Z0-9]/g, "-"),
118
+ name: rootPath === "." ? "Root Project" : rootPath,
119
+ rootPath,
120
+ frameworks: groupStacks.map((s) => s.frameworkName),
121
+ entrypoints,
122
+ buildTools,
123
+ evidence: groupStacks.flatMap((s) => s.evidence.map((e) => e.evidence)),
124
+ });
125
+ }
126
+ // Determine repo type
127
+ const uniqueRoots = [...rootPathGroups.keys()].filter((r) => r !== ".");
128
+ let repoType;
129
+ if (uniqueRoots.length >= 2) {
130
+ repoType = "monorepo";
131
+ }
132
+ else if (uniqueRoots.length === 1 && rootPathGroups.has(".")) {
133
+ repoType = "monorepo";
134
+ }
135
+ else if (uniqueRoots.length === 0) {
136
+ repoType = "single";
137
+ }
138
+ else {
139
+ repoType = "single";
140
+ }
141
+ // Build navigation guidance
142
+ const whatToReadFirst = [];
143
+ const whereThingsLive = [];
144
+ if (primaryOrchestrator) {
145
+ whatToReadFirst.push(`OpenClaw config: ${primaryOrchestrator.configPath}`);
146
+ }
147
+ for (const workload of workloads) {
148
+ if (workload.frameworks.length > 0) {
149
+ whereThingsLive.push(`${workload.name}: ${workload.frameworks.join(", ")} (at ${workload.rootPath})`);
150
+ }
151
+ if (workload.entrypoints.length > 0) {
152
+ whatToReadFirst.push(`${workload.name} entrypoints: ${workload.entrypoints.slice(0, 3).join("; ")}`);
153
+ }
154
+ }
155
+ return {
156
+ primaryOrchestrator,
157
+ workloads,
158
+ repoType,
159
+ navigation: {
160
+ whatToReadFirst,
161
+ whereThingsLive,
162
+ },
163
+ };
164
+ }
165
+ /**
166
+ * Format a VersionInfo for display in AGENTS.md
167
+ */
168
+ function formatVersionForDisplay(frameworkName, version) {
169
+ if (version.certainty === "exact" && version.value) {
170
+ return `${frameworkName}: ${version.value} (exact; ${version.sourceFile || "manifest"})`;
171
+ }
172
+ if (version.certainty === "ambiguous" && version.value) {
173
+ return `${frameworkName}: ~${version.value} (ambiguous; ${version.notes || "range without lockfile resolution"})`;
174
+ }
175
+ return `${frameworkName}: unknown (no explicit version found)`;
176
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Awareness orchestrator.
3
+ * Top-level function that runs the entire awareness pipeline:
4
+ * scan → parse → resolve versions → detect stacks → web ground → build constraints.
5
+ */
6
+ import { AwarenessResult } from "./types";
7
+ /**
8
+ * Run the full awareness pipeline.
9
+ * This is the main entry point called from the CLI.
10
+ */
11
+ export declare function runAwareness(repoPath: string, repoName: string): Promise<AwarenessResult>;
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ /**
3
+ * Awareness orchestrator.
4
+ * Top-level function that runs the entire awareness pipeline:
5
+ * scan → parse → resolve versions → detect stacks → web ground → build constraints.
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.runAwareness = runAwareness;
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
44
+ const manifestScanner_1 = require("./manifestScanner");
45
+ const manifestParsers_1 = require("./manifestParsers");
46
+ const versionResolver_1 = require("./versionResolver");
47
+ const detector_1 = require("./detector");
48
+ const webGrounding_1 = require("./webGrounding");
49
+ /**
50
+ * The ARCHITECTURE CONSTRAINTS block injected into LLM context.
51
+ * This is authoritative and must not be overridden by the model.
52
+ */
53
+ function buildConstraintsBlock(stacks, resolvedVersions, hasOrchestrator) {
54
+ const lines = [
55
+ "=== ARCHITECTURE CONSTRAINTS (AUTHORITATIVE) ===",
56
+ "1) The detected frameworks/stacks listed below are authoritative because they were derived from repository manifest/config files.",
57
+ "2) You MUST NOT invent additional frameworks, build systems, languages, entrypoints, or tooling not present in the evidence.",
58
+ '3) If evidence is missing or ambiguous, say "unknown" or "ambiguous" and provide safe navigation guidance rather than guessing.',
59
+ "4) Versions:",
60
+ " - Only state a version if it is explicitly detected from manifests/locks.",
61
+ " - If version is unknown/ambiguous, state that clearly.",
62
+ " - If you cite docs, choose versioned docs when exact version is known; otherwise use latest docs and label as fallback.",
63
+ ];
64
+ if (hasOrchestrator) {
65
+ lines.push("5) OpenClaw:", " - If openclaw.json exists anywhere in the repo, treat OpenClaw as the orchestrator and structure AGENTS.md accordingly (OpenClaw Runtime + workloads).");
66
+ }
67
+ lines.push("", "DETECTED STACKS:");
68
+ for (const stack of stacks) {
69
+ const version = resolvedVersions.get(stack.frameworkId);
70
+ if (version) {
71
+ lines.push(` - ${(0, detector_1.formatVersionForDisplay)(stack.frameworkName, version)}`);
72
+ }
73
+ else {
74
+ lines.push(` - ${stack.frameworkName}: detected (confidence: ${stack.confidence}%)`);
75
+ }
76
+ // Add evidence summary
77
+ for (const ev of stack.evidence.slice(0, 2)) {
78
+ lines.push(` Evidence: ${ev.evidence.reason} [${ev.evidence.file}]`);
79
+ }
80
+ }
81
+ lines.push("=== END ARCHITECTURE CONSTRAINTS ===");
82
+ return lines.join("\n");
83
+ }
84
+ /**
85
+ * Build the full awareness context string for LLM prompts.
86
+ */
87
+ function buildAwarenessContext(result) {
88
+ const parts = [];
89
+ // Constraints block
90
+ parts.push(result.constraintsBlock);
91
+ parts.push("");
92
+ // Architecture summary
93
+ const arch = result.architecture;
94
+ parts.push("ARCHITECTURE SUMMARY:");
95
+ parts.push(` Repo type: ${arch.repoType}`);
96
+ if (arch.primaryOrchestrator) {
97
+ parts.push(` Primary orchestrator: ${arch.primaryOrchestrator.id} (config: ${arch.primaryOrchestrator.configPath})`);
98
+ }
99
+ if (arch.workloads.length > 0) {
100
+ parts.push(" Workloads:");
101
+ for (const w of arch.workloads) {
102
+ parts.push(` - ${w.name} (${w.rootPath}): ${w.frameworks.join(", ")}`);
103
+ }
104
+ }
105
+ if (arch.navigation.whatToReadFirst.length > 0) {
106
+ parts.push(" What to read first:");
107
+ for (const item of arch.navigation.whatToReadFirst) {
108
+ parts.push(` - ${item}`);
109
+ }
110
+ }
111
+ // Web references summary
112
+ if (result.webReferences.length > 0) {
113
+ parts.push("");
114
+ parts.push("FRAMEWORK REFERENCES:");
115
+ for (const ref of result.webReferences) {
116
+ const mode = ref.referenceMode === "versioned"
117
+ ? `Docs selected for version ${ref.versionInfo.value} (exact)`
118
+ : "Version unknown/ambiguous; using latest docs fallback";
119
+ parts.push(` ${ref.frameworkName}: ${mode}`);
120
+ for (const r of ref.results.slice(0, 3)) {
121
+ parts.push(` - ${r.title}: ${r.url}`);
122
+ }
123
+ }
124
+ }
125
+ else if (result.errors.length > 0) {
126
+ parts.push("");
127
+ parts.push("FRAMEWORK REFERENCES: Unavailable");
128
+ for (const err of result.errors) {
129
+ parts.push(` - ${err}`);
130
+ }
131
+ }
132
+ return parts.join("\n");
133
+ }
134
+ /**
135
+ * Save awareness.json for debugging/transparency.
136
+ */
137
+ function saveAwarenessJson(repoName, result, baseDir = "projects") {
138
+ const folderName = repoName.toLowerCase().replace(/\s+/g, "-");
139
+ const targetDir = path.join(baseDir, folderName);
140
+ fs.mkdirSync(targetDir, { recursive: true });
141
+ const filePath = path.join(targetDir, "awareness.json");
142
+ try {
143
+ // Serialize without circular references — remove evidence signals' circular refs
144
+ const serializable = {
145
+ manifests: result.manifests,
146
+ stacks: result.stacks.map((s) => ({
147
+ frameworkId: s.frameworkId,
148
+ frameworkName: s.frameworkName,
149
+ confidence: s.confidence,
150
+ versions: s.versions,
151
+ rootPath: s.rootPath,
152
+ evidenceCount: s.evidence.length,
153
+ evidenceSummary: s.evidence.slice(0, 3).map((e) => ({
154
+ kind: e.kind,
155
+ file: e.evidence.file,
156
+ reason: e.evidence.reason,
157
+ })),
158
+ })),
159
+ architecture: result.architecture,
160
+ webReferences: result.webReferences,
161
+ errors: result.errors,
162
+ constraintsBlock: result.constraintsBlock,
163
+ };
164
+ fs.writeFileSync(filePath, JSON.stringify(serializable, null, 2), "utf-8");
165
+ console.log(`✅ Saved awareness.json to: ${filePath}`);
166
+ }
167
+ catch (error) {
168
+ console.warn(`⚠️ Could not save awareness.json to ${filePath}: ${error.message}\n` +
169
+ ` This is a debug file and does not affect AGENTS.md generation.\n` +
170
+ ` Check disk space and directory write permissions if you need this output.`);
171
+ }
172
+ }
173
+ /**
174
+ * Run the full awareness pipeline.
175
+ * This is the main entry point called from the CLI.
176
+ */
177
+ async function runAwareness(repoPath, repoName) {
178
+ console.log("\n🔍 Running Framework Awareness scan...");
179
+ // Step 1: Scan for manifests (full-repo)
180
+ console.log(" → Scanning repository for manifest files...");
181
+ const manifests = (0, manifestScanner_1.scanManifests)(repoPath);
182
+ console.log(` → Found ${manifests.length} manifest file(s)`);
183
+ if (manifests.length > 0) {
184
+ for (const m of manifests.slice(0, 10)) {
185
+ console.log(` 📄 ${m.relativePath} (depth: ${m.depth})`);
186
+ }
187
+ if (manifests.length > 10) {
188
+ console.log(` ... and ${manifests.length - 10} more`);
189
+ }
190
+ }
191
+ // Step 2: Parse all manifests into signals
192
+ console.log(" → Parsing manifests...");
193
+ const signals = (0, manifestParsers_1.parseAllManifests)(manifests);
194
+ console.log(` → Generated ${signals.length} signal(s)`);
195
+ // Step 3: Resolve versions
196
+ console.log(" → Resolving versions...");
197
+ const resolvedVersions = (0, versionResolver_1.resolveAllVersions)(signals);
198
+ // Step 4: Detect stacks and architecture
199
+ console.log(" → Detecting stacks and architecture...");
200
+ const stacks = (0, detector_1.detectStacks)(signals);
201
+ const architecture = (0, detector_1.detectArchitecture)(stacks, signals);
202
+ const hasOrchestrator = signals.some((s) => s.kind === "orchestrator");
203
+ console.log(` → Detected ${stacks.length} stack(s), repo type: ${architecture.repoType}`);
204
+ for (const stack of stacks.slice(0, 8)) {
205
+ const version = resolvedVersions.get(stack.frameworkId);
206
+ const versionStr = version?.value
207
+ ? ` v${version.value} (${version.certainty})`
208
+ : ` (version ${version?.certainty || "unknown"})`;
209
+ console.log(` 🏗️ ${stack.frameworkName}${versionStr} [confidence: ${stack.confidence}%]`);
210
+ }
211
+ if (hasOrchestrator) {
212
+ console.log(" 🎯 OpenClaw orchestrator detected");
213
+ }
214
+ // Step 5: Build constraints block
215
+ const constraintsBlock = buildConstraintsBlock(stacks, resolvedVersions, hasOrchestrator);
216
+ // Step 6: Web grounding
217
+ console.log(" → Gathering web references...");
218
+ const cacheDir = path.join("projects", repoName.toLowerCase().replace(/\s+/g, "-"), "cache", "brave");
219
+ const { references: webReferences, errors } = await (0, webGrounding_1.gatherReferences)(stacks, 3, cacheDir);
220
+ if (webReferences.length > 0) {
221
+ console.log(` → Retrieved references for ${webReferences.length} framework(s)`);
222
+ for (const ref of webReferences) {
223
+ const mode = ref.referenceMode === "versioned"
224
+ ? `v${ref.versionInfo.value} docs`
225
+ : "latest docs (fallback)";
226
+ console.log(` 📚 ${ref.frameworkName}: ${ref.results.length} reference(s) [${mode}]`);
227
+ }
228
+ }
229
+ if (errors.length > 0) {
230
+ for (const err of errors) {
231
+ // Multi-line errors — indent subsequent lines
232
+ const lines = err.split("\n");
233
+ console.log(` ⚠️ ${lines[0]}`);
234
+ for (const line of lines.slice(1)) {
235
+ console.log(` ${line}`);
236
+ }
237
+ }
238
+ }
239
+ // Build partial result (without awarenessContext yet)
240
+ const partialResult = {
241
+ manifests,
242
+ signals,
243
+ stacks,
244
+ architecture,
245
+ webReferences,
246
+ constraintsBlock,
247
+ errors,
248
+ };
249
+ // Step 7: Build awareness context
250
+ const awarenessContext = buildAwarenessContext(partialResult);
251
+ const result = {
252
+ ...partialResult,
253
+ awarenessContext,
254
+ };
255
+ // Step 8: Save awareness.json
256
+ saveAwarenessJson(repoName, result);
257
+ console.log("✅ Framework Awareness scan complete\n");
258
+ return result;
259
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * .NET manifest parsers.
3
+ * Handles *.csproj files and global.json.
4
+ */
5
+ import { Signal, ManifestFile } from "../types";
6
+ /**
7
+ * Parse a .csproj file (MSBuild XML format)
8
+ */
9
+ export declare function parseCsproj(manifest: ManifestFile): Signal[];
10
+ /**
11
+ * Parse global.json (.NET SDK version pinning)
12
+ */
13
+ export declare function parseGlobalJson(manifest: ManifestFile): Signal[];