stealthos-cli 0.1.0-alpha.3 → 0.1.0-alpha.4

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 (176) hide show
  1. package/ai/CONTRACT.md +110 -0
  2. package/ai/INDEX.md +203 -0
  3. package/ai/README.md +434 -0
  4. package/ai/ROUTER.md +288 -0
  5. package/ai/agents/README.md +103 -0
  6. package/ai/agents/architect.md +59 -0
  7. package/ai/agents/backend-engineer.md +62 -0
  8. package/ai/agents/founder.md +45 -0
  9. package/ai/agents/frontend-engineer.md +61 -0
  10. package/ai/agents/product-manager.md +56 -0
  11. package/ai/agents/qa-engineer.md +53 -0
  12. package/ai/agents/researcher.md +74 -0
  13. package/ai/agents/reviewer.md +73 -0
  14. package/ai/agents/security-engineer.md +59 -0
  15. package/ai/agents/sre-engineer.md +70 -0
  16. package/ai/agents/tech-lead.md +70 -0
  17. package/ai/architecture/README.md +35 -0
  18. package/ai/architecture/components.md +24 -0
  19. package/ai/architecture/containers.md +30 -0
  20. package/ai/architecture/event-flows.md +36 -0
  21. package/ai/architecture/sequence-diagrams.md +38 -0
  22. package/ai/architecture/system-context.md +46 -0
  23. package/ai/architecture/threat-modeling.md +40 -0
  24. package/ai/blueprints/README.md +67 -0
  25. package/ai/blueprints/_schema.json +40 -0
  26. package/ai/blueprints/ai-platform.json +28 -0
  27. package/ai/blueprints/crm.json +22 -0
  28. package/ai/blueprints/game.json +25 -0
  29. package/ai/blueprints/mobile.json +24 -0
  30. package/ai/blueprints/realtime.json +22 -0
  31. package/ai/blueprints/saas.json +25 -0
  32. package/ai/blueprints/telemetry.json +30 -0
  33. package/ai/blueprints/web.json +23 -0
  34. package/ai/bootstrap/discovery-questions.md +117 -0
  35. package/ai/bootstrap/dispatcher.md +85 -0
  36. package/ai/bootstrap/existing-project.md +191 -0
  37. package/ai/bootstrap/new-project.md +127 -0
  38. package/ai/bootstrap/tech-mapping.md +164 -0
  39. package/ai/clients/README.md +114 -0
  40. package/ai/clients/antigravity.md +125 -0
  41. package/ai/clients/claude-code.md +65 -0
  42. package/ai/clients/cline.md +69 -0
  43. package/ai/clients/codex-aider-cli.md +82 -0
  44. package/ai/clients/continue.md +67 -0
  45. package/ai/clients/copilot.md +49 -0
  46. package/ai/clients/cursor.md +81 -0
  47. package/ai/clients/snippets/mcp-absolute-paths.json +9 -0
  48. package/ai/clients/snippets/mcp-http.json +7 -0
  49. package/ai/clients/snippets/mcp-stdio.json +9 -0
  50. package/ai/clients/trae.md +69 -0
  51. package/ai/clients/windsurf.md +71 -0
  52. package/ai/core/pipeline/execution-engine.md +157 -0
  53. package/ai/engineering/README.md +32 -0
  54. package/ai/engineering/observability/incident-response.md +82 -0
  55. package/ai/evals/protocol-tests.md +150 -0
  56. package/ai/evolution/agent-evolution.md +161 -0
  57. package/ai/evolution/improvements.md +91 -0
  58. package/ai/evolution/learnings.md +49 -0
  59. package/ai/evolution/patterns-discovered.md +48 -0
  60. package/ai/execution/README.md +33 -0
  61. package/ai/execution/backlog.md +27 -0
  62. package/ai/execution/milestones.md +26 -0
  63. package/ai/execution/roadmap.md +30 -0
  64. package/ai/execution/sprint.md +42 -0
  65. package/ai/governance/README.md +34 -0
  66. package/ai/governance/architecture-principles.md +99 -0
  67. package/ai/governance/definition-of-done.md +88 -0
  68. package/ai/governance/definition-of-ready.md +69 -0
  69. package/ai/governance/engineering-principles.md +70 -0
  70. package/ai/governance/quality-gates.md +85 -0
  71. package/ai/governance/security-policies.md +84 -0
  72. package/ai/hooks/enforce-audit.ps1 +41 -0
  73. package/ai/hooks/enforce-audit.sh +39 -0
  74. package/ai/hooks/guard-edit.ps1 +182 -0
  75. package/ai/hooks/guard-edit.sh +161 -0
  76. package/ai/hooks/inject-os-reminder.ps1 +40 -0
  77. package/ai/hooks/inject-os-reminder.sh +16 -0
  78. package/ai/manifest.json +238 -0
  79. package/ai/memory/_detected-stack.json +33 -0
  80. package/ai/memory/_summary.md +49 -0
  81. package/ai/memory/archive/.gitkeep +3 -0
  82. package/ai/memory/completed-tasks.md +156 -0
  83. package/ai/memory/decisions.md +257 -0
  84. package/ai/memory/errors-and-solutions.md +41 -0
  85. package/ai/memory/known-issues.md +40 -0
  86. package/ai/memory/pending-tasks.md +37 -0
  87. package/ai/memory/project-context.md +67 -0
  88. package/ai/operating-system/architecture.md +54 -0
  89. package/ai/operating-system/coding-standards.md +84 -0
  90. package/ai/operating-system/folder-structure.md +126 -0
  91. package/ai/operating-system/performance-rules.md +86 -0
  92. package/ai/operating-system/quality-control.md +81 -0
  93. package/ai/operating-system/security-rules.md +91 -0
  94. package/ai/operating-system/workflow.md +86 -0
  95. package/ai/product/README.md +24 -0
  96. package/ai/product/business-rules.md +26 -0
  97. package/ai/product/personas.md +29 -0
  98. package/ai/product/user-journeys.md +30 -0
  99. package/ai/product/vision.md +35 -0
  100. package/ai/rules/behavior.md +45 -0
  101. package/ai/rules/do.md +47 -0
  102. package/ai/rules/dont.md +46 -0
  103. package/ai/rules/execution-flow.md +125 -0
  104. package/ai/rules/structural-constraints.md +59 -0
  105. package/ai/rules/structure-canon.md +116 -0
  106. package/ai/runtime.md +179 -0
  107. package/ai/scripts/detect-stack.ps1 +166 -0
  108. package/ai/scripts/detect-stack.sh +172 -0
  109. package/ai/scripts/init-ai-os.ps1 +170 -0
  110. package/ai/scripts/init-ai-os.sh +99 -0
  111. package/ai/scripts/lint-os.ps1 +99 -0
  112. package/ai/scripts/lint-os.sh +85 -0
  113. package/ai/scripts/start-os.ps1 +151 -0
  114. package/ai/scripts/start-os.sh +141 -0
  115. package/ai/server/README.md +105 -0
  116. package/ai/server/aios-server.mjs +2134 -0
  117. package/ai/server/package-lock.json +802 -0
  118. package/ai/server/package.json +31 -0
  119. package/ai/server/src/analyzer/graph-builder.ts +92 -0
  120. package/ai/server/src/analyzer/index.ts +191 -0
  121. package/ai/server/src/analyzer/module-mapper.ts +171 -0
  122. package/ai/server/src/analyzer/smell-detector.ts +54 -0
  123. package/ai/server/src/analyzer/stack-detector.ts +70 -0
  124. package/ai/server/src/index.ts +16 -0
  125. package/ai/server/src/packager/context-builder.ts +217 -0
  126. package/ai/server/src/packager/index.ts +3 -0
  127. package/ai/server/src/packager/memory-injector.ts +128 -0
  128. package/ai/server/src/packager/module-summarizer.ts +60 -0
  129. package/ai/server/src/packager/token-estimator.ts +26 -0
  130. package/ai/server/src/snapshot/index.ts +3 -0
  131. package/ai/server/src/snapshot/snapshot-creator.ts +206 -0
  132. package/ai/server/src/snapshot/snapshot-diff.ts +86 -0
  133. package/ai/server/src/snapshot/snapshot-restore.ts +14 -0
  134. package/ai/server/src/types.ts +94 -0
  135. package/ai/server/tsconfig.json +26 -0
  136. package/ai/skills/architecture-design.md +82 -0
  137. package/ai/skills/backend-engineering.md +57 -0
  138. package/ai/skills/database-design.md +76 -0
  139. package/ai/skills/frontend-engineering.md +63 -0
  140. package/ai/skills/performance.md +73 -0
  141. package/ai/skills/scalability.md +84 -0
  142. package/ai/skills/security.md +71 -0
  143. package/ai/skills/testing.md +77 -0
  144. package/ai/specs/ADR/ADR-0002-typescript-runtime.md +103 -0
  145. package/ai/specs/ADR/ADR-0004-runtime-orchestrator.md +94 -0
  146. package/ai/specs/ADR/ADR-0005-workflow-engine.md +105 -0
  147. package/ai/specs/ADR/ADR-0006-runtime-state.md +104 -0
  148. package/ai/specs/ADR/ADR-0007-state-compiler-drift-context-layers-artifact-index.md +82 -0
  149. package/ai/specs/ADR/ADR-0008-intent-runtime-discovery-branching.md +93 -0
  150. package/ai/specs/ADR/ADR-0009-confidence-system-maturity-tracking.md +113 -0
  151. package/ai/specs/ADR/ADR-0010-structural-architecture-standards.md +121 -0
  152. package/ai/specs/ADR/ADR-0011-mcp-prompts.md +86 -0
  153. package/ai/specs/ADR/ADR-0012-stealthos-hybrid-architecture.md +174 -0
  154. package/ai/specs/ADR/_TEMPLATE.md +60 -0
  155. package/ai/specs/BRD/_TEMPLATE.md +50 -0
  156. package/ai/specs/PRD/_TEMPLATE.md +72 -0
  157. package/ai/specs/README.md +43 -0
  158. package/ai/specs/RFC/RFC-0001-runtime-orchestrator.md +149 -0
  159. package/ai/specs/RFC/RFC-0002-runtime-orchestrator-extended.md +134 -0
  160. package/ai/specs/RFC/_TEMPLATE.md +61 -0
  161. package/ai/specs/RUNBOOKS/_TEMPLATE.md +68 -0
  162. package/ai/specs/SDD/_TEMPLATE.md +104 -0
  163. package/ai/specs/TASKS/_TEMPLATE.md +52 -0
  164. package/ai/tools/debugging.md +64 -0
  165. package/ai/tools/dependency-analysis.md +46 -0
  166. package/ai/tools/internet-research.md +42 -0
  167. package/ai/tools/mcp-discovery.md +44 -0
  168. package/ai/workflows/_schema.json +81 -0
  169. package/ai/workflows/init.json +148 -0
  170. package/ai/workflows/sync.json +71 -0
  171. package/ai/workflows/work.json +91 -0
  172. package/package.json +7 -1
  173. package/scripts/bundle-ai.mjs +58 -0
  174. package/src/cli.mjs +1 -1
  175. package/src/commands/install.mjs +35 -11
  176. package/src/lib/resolve-source.mjs +27 -10
@@ -0,0 +1,217 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import type { ContextPackage, ProjectState } from "../types.js";
5
+ import { analyzeProject } from "../analyzer/index.js";
6
+ import { extractKeywords, injectMemory } from "./memory-injector.js";
7
+ import { renderModuleYaml, summarizeModule } from "./module-summarizer.js";
8
+ import { estimateTokens, checkHeadroom } from "./token-estimator.js";
9
+
10
+ export interface BuildOptions {
11
+ intent?: string;
12
+ maxModules?: number;
13
+ state?: ProjectState;
14
+ write?: boolean;
15
+ windowTokens?: number;
16
+ }
17
+
18
+ export async function buildContextPackage(
19
+ projectRoot: string,
20
+ options: BuildOptions = {},
21
+ ): Promise<ContextPackage> {
22
+ const intent = (options.intent ?? "").trim();
23
+ const state = options.state ?? (await analyzeProject(projectRoot, { write: false }));
24
+ const keywords = extractKeywords(intent);
25
+ const maxModules = options.maxModules ?? 12;
26
+
27
+ const rankedModules = state.modules
28
+ .map((m) => ({ m, s: scoreModule(m.id, m.exports, m.imports, keywords) }))
29
+ .sort((a, b) => b.s - a.s);
30
+
31
+ const chosen =
32
+ keywords.length === 0
33
+ ? topByInbound(state, maxModules)
34
+ : rankedModules
35
+ .filter((x) => x.s > 0)
36
+ .slice(0, maxModules)
37
+ .map((x) => x.m);
38
+
39
+ if (chosen.length === 0 && state.modules.length > 0) {
40
+ chosen.push(...topByInbound(state, Math.min(5, state.modules.length)));
41
+ }
42
+
43
+ const memory = await injectMemory(projectRoot, keywords, 6);
44
+
45
+ const sections: string[] = [];
46
+
47
+ sections.push(renderHeader(intent, state));
48
+ sections.push(renderStackBlock(state));
49
+
50
+ if (memory.rules_summary) {
51
+ sections.push("## Critical rules (from .ai/rules/dont.md)\n\n" + memory.rules_summary);
52
+ }
53
+
54
+ if (chosen.length) {
55
+ const moduleBlocks = chosen.map((m) => {
56
+ const sum = summarizeModule(m, state.graph, state.smells);
57
+ return "```yaml\n" + renderModuleYaml(sum) + "\n```";
58
+ });
59
+ sections.push("## Modules in scope (" + chosen.length + ")\n\n" + moduleBlocks.join("\n\n"));
60
+ } else if (state.modules.length === 0) {
61
+ sections.push(
62
+ "## Modules in scope\n\n_(no code modules detected yet — project is in `.ai/` setup phase)_",
63
+ );
64
+ }
65
+
66
+ if (memory.decisions.length) {
67
+ sections.push(
68
+ "## Relevant decisions (from memory)\n\n" +
69
+ memory.decisions.map((d) => `### ${d.title}\n\n${truncate(d.body, 600)}`).join("\n\n"),
70
+ );
71
+ }
72
+ if (memory.errors.length) {
73
+ sections.push(
74
+ "## Past errors & solutions\n\n" +
75
+ memory.errors.map((d) => `### ${d.title}\n\n${truncate(d.body, 500)}`).join("\n\n"),
76
+ );
77
+ }
78
+ if (memory.issues.length) {
79
+ sections.push(
80
+ "## Known issues\n\n" +
81
+ memory.issues.map((d) => `### ${d.title}\n\n${truncate(d.body, 400)}`).join("\n\n"),
82
+ );
83
+ }
84
+
85
+ if (state.smells.length) {
86
+ const top = state.smells.slice(0, 8);
87
+ sections.push(
88
+ "## Top smells\n\n" +
89
+ top
90
+ .map(
91
+ (s) =>
92
+ `- **${s.kind}** [${s.severity}] \`${s.location.file}${
93
+ s.location.line ? ":" + s.location.line : ""
94
+ }\` — ${s.message}`,
95
+ )
96
+ .join("\n"),
97
+ );
98
+ }
99
+
100
+ const markdown = sections.join("\n\n").trim() + "\n";
101
+ const tokens = estimateTokens(markdown);
102
+ const sourceHash = sha256OfStateRef(state);
103
+
104
+ const pkg: ContextPackage = {
105
+ intent: intent || "(no intent — general project overview)",
106
+ generated_at: new Date().toISOString(),
107
+ estimated_tokens: tokens,
108
+ source_state_hash: sourceHash,
109
+ modules_included: chosen.map((m) => m.id),
110
+ markdown,
111
+ };
112
+
113
+ if (options.write !== false) {
114
+ const ctxDir = join(projectRoot, ".ai", "context", "packages");
115
+ await mkdir(ctxDir, { recursive: true });
116
+ const slug = slugify(intent) || "overview";
117
+ const mdPath = join(ctxDir, `${slug}.md`);
118
+ const jsonPath = join(ctxDir, `${slug}.json`);
119
+ await writeFile(mdPath, markdown, "utf8");
120
+ await writeFile(
121
+ jsonPath,
122
+ JSON.stringify(
123
+ {
124
+ intent: pkg.intent,
125
+ generated_at: pkg.generated_at,
126
+ estimated_tokens: pkg.estimated_tokens,
127
+ source_state_hash: pkg.source_state_hash,
128
+ modules_included: pkg.modules_included,
129
+ headroom: checkHeadroom(markdown, options.windowTokens),
130
+ file: `${slug}.md`,
131
+ },
132
+ null,
133
+ 2,
134
+ ),
135
+ "utf8",
136
+ );
137
+ }
138
+
139
+ return pkg;
140
+ }
141
+
142
+ function scoreModule(
143
+ id: string,
144
+ exportsList: string[],
145
+ imports: string[],
146
+ keywords: string[],
147
+ ): number {
148
+ if (keywords.length === 0) return 0;
149
+ let s = 0;
150
+ const lowerId = id.toLowerCase();
151
+ for (const k of keywords) {
152
+ if (k.length < 3) continue;
153
+ if (lowerId.includes(k)) s += 5;
154
+ for (const e of exportsList) if (e.toLowerCase().includes(k)) s += 3;
155
+ for (const i of imports) if (i.toLowerCase().includes(k)) s += 1;
156
+ }
157
+ return s;
158
+ }
159
+
160
+ function topByInbound(state: ProjectState, limit: number) {
161
+ const inbound = new Map<string, number>();
162
+ for (const e of state.graph.edges) {
163
+ inbound.set(e.to, (inbound.get(e.to) ?? 0) + 1);
164
+ }
165
+ return [...state.modules]
166
+ .sort((a, b) => (inbound.get(b.id) ?? 0) - (inbound.get(a.id) ?? 0))
167
+ .slice(0, limit);
168
+ }
169
+
170
+ function renderHeader(intent: string, state: ProjectState): string {
171
+ return [
172
+ "# Context Package",
173
+ "",
174
+ `**Intent:** ${intent || "_(general project overview)_"}`,
175
+ `**Generated:** ${new Date().toISOString()}`,
176
+ `**Runtime:** v${state.version}`,
177
+ `**Source:** \`.ai/context/project-state.json\` (${state.modules.length} modules, ${state.smells.length} smells)`,
178
+ ].join("\n");
179
+ }
180
+
181
+ function renderStackBlock(state: ProjectState): string {
182
+ const s = state.stack;
183
+ const lines = [
184
+ "## Stack",
185
+ "",
186
+ `- Languages: ${s.languages.join(", ") || "(none detected)"}`,
187
+ `- Frameworks: ${s.frameworks.join(", ") || "(none)"}`,
188
+ ];
189
+ if (s.package_manager) lines.push(`- Package manager: ${s.package_manager}`);
190
+ if (s.databases?.length) lines.push(`- Databases: ${s.databases.join(", ")}`);
191
+ if (s.test_frameworks?.length) lines.push(`- Test frameworks: ${s.test_frameworks.join(", ")}`);
192
+ return lines.join("\n");
193
+ }
194
+
195
+ function truncate(text: string, n: number): string {
196
+ const t = text.trim();
197
+ return t.length <= n ? t : t.slice(0, n - 1) + "…";
198
+ }
199
+
200
+ function slugify(s: string): string {
201
+ return s
202
+ .toLowerCase()
203
+ .normalize("NFD")
204
+ .replace(/[̀-ͯ]/g, "")
205
+ .replace(/[^\w\s-]/g, "")
206
+ .trim()
207
+ .replace(/\s+/g, "-")
208
+ .slice(0, 50);
209
+ }
210
+
211
+ function sha256OfStateRef(state: ProjectState): string {
212
+ const h = createHash("sha256");
213
+ h.update(state.generated_at);
214
+ h.update(String(state.modules.length));
215
+ h.update(String(state.graph.edges.length));
216
+ return h.digest("hex").slice(0, 16);
217
+ }
@@ -0,0 +1,3 @@
1
+ export { buildContextPackage } from "./context-builder.js";
2
+ export { estimateTokens, checkHeadroom } from "./token-estimator.js";
3
+ export { extractKeywords } from "./memory-injector.js";
@@ -0,0 +1,128 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ export interface MemoryEntry {
6
+ source: string;
7
+ title: string;
8
+ body: string;
9
+ }
10
+
11
+ export interface InjectedMemory {
12
+ decisions: MemoryEntry[];
13
+ errors: MemoryEntry[];
14
+ issues: MemoryEntry[];
15
+ rules_summary: string;
16
+ }
17
+
18
+ export async function injectMemory(
19
+ projectRoot: string,
20
+ keywords: string[],
21
+ limit = 6,
22
+ ): Promise<InjectedMemory> {
23
+ const memDir = join(projectRoot, ".ai", "memory");
24
+ const rulesDir = join(projectRoot, ".ai", "rules");
25
+
26
+ const decisions = await loadEntries(join(memDir, "decisions.md"), "decisions.md");
27
+ const errors = await loadEntries(join(memDir, "errors-and-solutions.md"), "errors-and-solutions.md");
28
+ const issues = await loadEntries(join(memDir, "known-issues.md"), "known-issues.md");
29
+
30
+ const rank = (e: MemoryEntry) => score(e.title + " " + e.body, keywords);
31
+
32
+ return {
33
+ decisions: pickTop(decisions, rank, limit),
34
+ errors: pickTop(errors, rank, limit),
35
+ issues: pickTop(issues, rank, limit),
36
+ rules_summary: await summarizeRules(rulesDir),
37
+ };
38
+ }
39
+
40
+ async function loadEntries(filePath: string, source: string): Promise<MemoryEntry[]> {
41
+ if (!existsSync(filePath)) return [];
42
+ const txt = await readFile(filePath, "utf8").catch(() => "");
43
+ if (!txt) return [];
44
+ const sections = txt.split(/\n(?=##\s+)/);
45
+ const entries: MemoryEntry[] = [];
46
+ for (const sec of sections) {
47
+ const trimmed = sec.trim();
48
+ if (!trimmed.startsWith("##")) continue;
49
+ const firstLineEnd = trimmed.indexOf("\n");
50
+ const title = (firstLineEnd > 0 ? trimmed.slice(0, firstLineEnd) : trimmed)
51
+ .replace(/^#+\s*/, "")
52
+ .trim();
53
+ const body = firstLineEnd > 0 ? trimmed.slice(firstLineEnd + 1).trim() : "";
54
+ if (title.toLowerCase().includes("template") || title.toLowerCase().includes("exemplo")) continue;
55
+ entries.push({ source, title, body });
56
+ }
57
+ return entries;
58
+ }
59
+
60
+ function score(text: string, keywords: string[]): number {
61
+ if (keywords.length === 0) return 0;
62
+ const lower = text.toLowerCase();
63
+ let s = 0;
64
+ for (const k of keywords) {
65
+ if (k.length < 3) continue;
66
+ const occurrences = lower.split(k).length - 1;
67
+ s += occurrences;
68
+ }
69
+ return s;
70
+ }
71
+
72
+ function pickTop<T>(items: T[], rank: (item: T) => number, limit: number): T[] {
73
+ return [...items]
74
+ .map((item) => ({ item, s: rank(item) }))
75
+ .filter((x) => x.s > 0)
76
+ .sort((a, b) => b.s - a.s)
77
+ .slice(0, limit)
78
+ .map((x) => x.item);
79
+ }
80
+
81
+ async function summarizeRules(rulesDir: string): Promise<string> {
82
+ if (!existsSync(rulesDir)) return "";
83
+ const dont = await readFile(join(rulesDir, "dont.md"), "utf8").catch(() => "");
84
+ if (!dont) return "";
85
+ const lines = dont.split("\n").filter((l) => /^\s*-\s+/.test(l) || /^##\s+/.test(l));
86
+ return lines.slice(0, 25).join("\n");
87
+ }
88
+
89
+ export function extractKeywords(intent: string): string[] {
90
+ const stop = new Set([
91
+ "para",
92
+ "como",
93
+ "uma",
94
+ "que",
95
+ "the",
96
+ "and",
97
+ "for",
98
+ "with",
99
+ "from",
100
+ "this",
101
+ "that",
102
+ "into",
103
+ "your",
104
+ "vai",
105
+ "com",
106
+ "ser",
107
+ "dos",
108
+ "das",
109
+ "por",
110
+ "ele",
111
+ "ela",
112
+ "isto",
113
+ "isso",
114
+ "pelo",
115
+ "pela",
116
+ "sobre",
117
+ "novo",
118
+ "nova",
119
+ "novos",
120
+ "novas",
121
+ ]);
122
+ return intent
123
+ .toLowerCase()
124
+ .replace(/[^\w\sáéíóúâêîôûãõàèìòùç-]/g, " ")
125
+ .split(/\s+/)
126
+ .map((w) => w.trim())
127
+ .filter((w) => w.length >= 3 && !stop.has(w));
128
+ }
@@ -0,0 +1,60 @@
1
+ import type { DependencyGraph, ModuleInfo, Smell } from "../types.js";
2
+
3
+ export interface ModuleSummary {
4
+ module: string;
5
+ language: string;
6
+ size_lines: number;
7
+ exports: string[];
8
+ depends_on: string[];
9
+ inbound: string[];
10
+ smells: string[];
11
+ }
12
+
13
+ export function summarizeModule(
14
+ m: ModuleInfo,
15
+ graph: DependencyGraph,
16
+ smells: Smell[],
17
+ ): ModuleSummary {
18
+ const internalDeps = graph.edges
19
+ .filter((e) => e.from === m.id)
20
+ .map((e) => e.to);
21
+ const externalDeps = m.imports.filter((i) => !i.startsWith("."));
22
+ const inbound = graph.edges.filter((e) => e.to === m.id).map((e) => e.from);
23
+ const moduleSmells = smells
24
+ .filter((s) => s.location.file === m.path)
25
+ .map((s) => `${s.kind}${s.location.line ? `:L${s.location.line}` : ""} — ${s.message}`);
26
+
27
+ return {
28
+ module: m.id,
29
+ language: m.language,
30
+ size_lines: m.size_lines,
31
+ exports: m.exports.slice(0, 10),
32
+ depends_on: [...internalDeps, ...externalDeps].slice(0, 12),
33
+ inbound: inbound.slice(0, 10),
34
+ smells: moduleSmells.slice(0, 5),
35
+ };
36
+ }
37
+
38
+ export function renderModuleYaml(s: ModuleSummary): string {
39
+ const lines: string[] = [];
40
+ lines.push(`module: ${s.module}`);
41
+ lines.push(`language: ${s.language}`);
42
+ lines.push(`size_lines: ${s.size_lines}`);
43
+ if (s.exports.length) {
44
+ lines.push("exports:");
45
+ for (const e of s.exports) lines.push(` - ${e}`);
46
+ }
47
+ if (s.depends_on.length) {
48
+ lines.push("depends_on:");
49
+ for (const d of s.depends_on) lines.push(` - ${d}`);
50
+ }
51
+ if (s.inbound.length) {
52
+ lines.push("inbound:");
53
+ for (const i of s.inbound) lines.push(` - ${i}`);
54
+ }
55
+ if (s.smells.length) {
56
+ lines.push("smells:");
57
+ for (const sm of s.smells) lines.push(` - "${sm.replace(/"/g, '\\"')}"`);
58
+ }
59
+ return lines.join("\n");
60
+ }
@@ -0,0 +1,26 @@
1
+ const CHARS_PER_TOKEN = 3.8;
2
+
3
+ export function estimateTokens(text: string): number {
4
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
5
+ }
6
+
7
+ export interface HeadroomCheck {
8
+ estimated: number;
9
+ budget: number;
10
+ fits: boolean;
11
+ utilization_pct: number;
12
+ }
13
+
14
+ const DEFAULT_WINDOW = 200_000;
15
+ const HEADROOM_RATIO = 0.4;
16
+
17
+ export function checkHeadroom(text: string, windowTokens: number = DEFAULT_WINDOW): HeadroomCheck {
18
+ const estimated = estimateTokens(text);
19
+ const budget = Math.floor(windowTokens * HEADROOM_RATIO);
20
+ return {
21
+ estimated,
22
+ budget,
23
+ fits: estimated <= budget,
24
+ utilization_pct: Math.round((estimated / windowTokens) * 1000) / 10,
25
+ };
26
+ }
@@ -0,0 +1,3 @@
1
+ export { createSnapshot } from "./snapshot-creator.js";
2
+ export { listSnapshots, diffSnapshots } from "./snapshot-diff.js";
3
+ export { restoreSnapshot } from "./snapshot-restore.js";
@@ -0,0 +1,206 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import { join, relative } from "node:path";
5
+ import type { ProjectState, SnapshotManifest } from "../types.js";
6
+ import { analyzeProject } from "../analyzer/index.js";
7
+
8
+ const TRACKED_ROOTS = [".ai", ".claude", "CLAUDE.md", "GEMINI.md", "GPT.md"];
9
+ const HASH_IGNORED_DIRS = new Set([
10
+ "node_modules",
11
+ ".git",
12
+ "dist",
13
+ "build",
14
+ ".runtime",
15
+ "archive",
16
+ ".turbo",
17
+ ".vercel",
18
+ ".next",
19
+ ]);
20
+
21
+ export interface CreateOptions {
22
+ label: string;
23
+ state?: ProjectState;
24
+ }
25
+
26
+ export async function createSnapshot(
27
+ projectRoot: string,
28
+ options: CreateOptions,
29
+ ): Promise<SnapshotManifest> {
30
+ const label = sanitizeLabel(options.label);
31
+ const id = `${timestamp()}-${label}`;
32
+ const snapDir = join(projectRoot, ".ai", "snapshots", id);
33
+ await mkdir(snapDir, { recursive: true });
34
+
35
+ const state = options.state ?? (await analyzeProject(projectRoot, { write: false }));
36
+ const fileHashes = await hashTrackedFiles(projectRoot);
37
+
38
+ const stateFile = "state.json";
39
+ const summaryFile = "summary.md";
40
+
41
+ const manifest: SnapshotManifest = {
42
+ id,
43
+ label,
44
+ created_at: new Date().toISOString(),
45
+ project_root: projectRoot,
46
+ state_file: stateFile,
47
+ summary_file: summaryFile,
48
+ file_hashes: fileHashes,
49
+ stats: {
50
+ total_files_hashed: Object.keys(fileHashes).length,
51
+ modules: state.modules.length,
52
+ smells: state.smells.length,
53
+ },
54
+ };
55
+
56
+ await writeFile(join(snapDir, stateFile), JSON.stringify(state, null, 2), "utf8");
57
+ await writeFile(
58
+ join(snapDir, summaryFile),
59
+ renderSummary(manifest, state, await readMemorySnapshot(projectRoot)),
60
+ "utf8",
61
+ );
62
+ await writeFile(join(snapDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
63
+
64
+ return manifest;
65
+ }
66
+
67
+ function timestamp(): string {
68
+ const d = new Date();
69
+ const pad = (n: number) => String(n).padStart(2, "0");
70
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
71
+ }
72
+
73
+ function sanitizeLabel(label: string): string {
74
+ return label.replace(/[^\w.-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "snap";
75
+ }
76
+
77
+ async function hashTrackedFiles(projectRoot: string): Promise<Record<string, string>> {
78
+ const hashes: Record<string, string> = {};
79
+ for (const root of TRACKED_ROOTS) {
80
+ const abs = join(projectRoot, root);
81
+ if (!existsSync(abs)) continue;
82
+ await walkAndHash(abs, projectRoot, hashes);
83
+ }
84
+ return hashes;
85
+ }
86
+
87
+ async function walkAndHash(
88
+ dir: string,
89
+ projectRoot: string,
90
+ out: Record<string, string>,
91
+ ): Promise<void> {
92
+ let stats;
93
+ try {
94
+ stats = await readdir(dir, { withFileTypes: true });
95
+ } catch {
96
+ // single-file root (CLAUDE.md etc.)
97
+ try {
98
+ const content = await readFile(dir);
99
+ const rel = relative(projectRoot, dir).replace(/\\/g, "/");
100
+ out[rel] = sha256(content);
101
+ } catch {
102
+ // ignore
103
+ }
104
+ return;
105
+ }
106
+ for (const e of stats) {
107
+ if (HASH_IGNORED_DIRS.has(e.name)) continue;
108
+ const p = join(dir, e.name);
109
+ if (e.isDirectory()) {
110
+ await walkAndHash(p, projectRoot, out);
111
+ } else if (e.isFile()) {
112
+ try {
113
+ const content = await readFile(p);
114
+ const rel = relative(projectRoot, p).replace(/\\/g, "/");
115
+ out[rel] = sha256(content);
116
+ } catch {
117
+ // ignore unreadable
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ function sha256(buf: Buffer): string {
124
+ return createHash("sha256").update(buf).digest("hex");
125
+ }
126
+
127
+ interface MemorySnapshot {
128
+ decisions_count: number;
129
+ pending_tasks_count: number;
130
+ known_issues_count: number;
131
+ last_decision?: string;
132
+ }
133
+
134
+ async function readMemorySnapshot(projectRoot: string): Promise<MemorySnapshot> {
135
+ const memDir = join(projectRoot, ".ai", "memory");
136
+ const snap: MemorySnapshot = {
137
+ decisions_count: 0,
138
+ pending_tasks_count: 0,
139
+ known_issues_count: 0,
140
+ };
141
+ const read = async (name: string) =>
142
+ readFile(join(memDir, name), "utf8").catch(() => "");
143
+
144
+ const decisions = await read("decisions.md");
145
+ const pending = await read("pending-tasks.md");
146
+ const issues = await read("known-issues.md");
147
+
148
+ snap.decisions_count = countMatches(decisions, /^##?\s+ADR-/gm) || countMatches(decisions, /^##\s+/gm);
149
+ snap.pending_tasks_count = countMatches(pending, /^\s*-\s+\[\s?\]/gm);
150
+ snap.known_issues_count = countMatches(issues, /^##\s+/gm);
151
+
152
+ const lastDecision = decisions.split(/\n##\s+/).slice(-1)[0]?.split("\n")[0];
153
+ if (lastDecision && lastDecision.trim().length > 0 && !lastDecision.includes("#")) {
154
+ snap.last_decision = lastDecision.trim().slice(0, 120);
155
+ }
156
+ return snap;
157
+ }
158
+
159
+ function countMatches(text: string, re: RegExp): number {
160
+ const m = text.match(re);
161
+ return m ? m.length : 0;
162
+ }
163
+
164
+ function renderSummary(
165
+ manifest: SnapshotManifest,
166
+ state: ProjectState,
167
+ mem: MemorySnapshot,
168
+ ): string {
169
+ const lines: string[] = [];
170
+ lines.push(`# Snapshot ${manifest.id}`, "");
171
+ lines.push(`**Label:** ${manifest.label}`);
172
+ lines.push(`**Created:** ${manifest.created_at}`);
173
+ lines.push(`**Root:** \`${manifest.project_root}\``, "");
174
+
175
+ lines.push("## Project state at capture", "");
176
+ lines.push(`- Languages: ${state.stack.languages.join(", ") || "(none)"}`);
177
+ lines.push(`- Modules: ${state.modules.length}`);
178
+ lines.push(`- Smells: ${state.smells.length}`);
179
+ lines.push(`- Circular deps: ${state.graph.cycles.length}`);
180
+ lines.push(`- Total files (repo): ${state.stats.total_files}`);
181
+ lines.push("");
182
+
183
+ lines.push("## Files hashed", "");
184
+ lines.push(`- Count: ${manifest.stats.total_files_hashed}`);
185
+ lines.push("");
186
+
187
+ lines.push("## Memory at capture", "");
188
+ lines.push(`- Decisions logged: ${mem.decisions_count}`);
189
+ lines.push(`- Pending tasks: ${mem.pending_tasks_count}`);
190
+ lines.push(`- Known issues: ${mem.known_issues_count}`);
191
+ if (mem.last_decision) {
192
+ lines.push(`- Last decision: _${mem.last_decision}_`);
193
+ }
194
+ lines.push("");
195
+
196
+ if (state.smells.length) {
197
+ lines.push("## Top smells", "");
198
+ for (const s of state.smells.slice(0, 10)) {
199
+ const loc = `${s.location.file}${s.location.line ? ":" + s.location.line : ""}`;
200
+ lines.push(`- **${s.kind}** [${s.severity}] \`${loc}\` — ${s.message}`);
201
+ }
202
+ lines.push("");
203
+ }
204
+
205
+ return lines.join("\n");
206
+ }