From c40571b7ef48a7ab2d6a69dfa0972b86f80a58ca Mon Sep 17 00:00:00 2001 From: g1nation Date: Wed, 13 May 2026 22:34:44 +0900 Subject: [PATCH] release: v2.0.2 - Structural Integrity & Automated Context Management --- .astra/project-context/architecture.md | 31 + .astra/project-context/scan-cache.json | 1598 +++++++++++++++++ ...d46d2ca2057b05c488be1dcf439166ac5a9a1.json | 2 +- ...9f4f39d2bc368f77456c37b5eef9a94a66b5c.json | 2 +- ...5c7a44d7661af673b24e3f49551a7a2e50280.json | 2 +- ...adc543795e4b427b64540a49c9ab27c7fe213.json | 4 +- ...son => stress_conflict_1778679248257.json} | 20 +- PATCHNOTES.md | 10 + docs/records/ConnectAI/chronicle.config.json | 8 +- ...09-이-프로젝트의-구조에-대해서-설명해봐.md | 19 + ...connectai-architecture-md-문서를-참고하고-_implementation.md | 22 + ...vity-connectai-이-프로젝트를-작업할거야.md | 16 + ...-13_상세하게-아키텍처-기준으로-설명해줘.md | 40 + docs/records/ConnectAI/timeline.md | 12 + media/sidebar.css | 33 +- media/sidebar.html | 14 +- media/sidebar.js | 30 +- package.json | 2 +- src/features/projectArchitecture/index.ts | 453 +++-- src/features/projectArchitecture/mermaid.ts | 69 + src/features/projectArchitecture/scanner.ts | 644 +++++++ src/sidebarProvider.ts | 3 + 22 files changed, 2802 insertions(+), 232 deletions(-) create mode 100644 .astra/project-context/architecture.md create mode 100644 .astra/project-context/scan-cache.json rename .astra/tests/stress/.astra/missions/{stress_conflict_1778677516257.json => stress_conflict_1778679248257.json} (82%) create mode 100644 docs/records/ConnectAI/decisions/ADR-0009-이-프로젝트의-구조에-대해서-설명해봐.md create mode 100644 docs/records/ConnectAI/development/2026-05-13_너는-분석-요청하거나-내가-작업-요청을-할때-connectai-architecture-md-문서를-참고하고-_implementation.md create mode 100644 docs/records/ConnectAI/discussions/2026-05-13_volumes-data-project-antigravity-connectai-이-프로젝트를-작업할거야.md create mode 100644 docs/records/ConnectAI/planning/2026-05-13_상세하게-아키텍처-기준으로-설명해줘.md create mode 100644 src/features/projectArchitecture/mermaid.ts create mode 100644 src/features/projectArchitecture/scanner.ts diff --git a/.astra/project-context/architecture.md b/.astra/project-context/architecture.md new file mode 100644 index 0000000..0f51257 --- /dev/null +++ b/.astra/project-context/architecture.md @@ -0,0 +1,31 @@ +# ConnectAI — Project Architecture Context + + +## Project Name +ConnectAI + +## Project Root +/Volumes/Data/project/Antigravity/ConnectAI + +## Description +The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making. + +## Runtime / Stack +TypeScript, Node.js, VS Code Extension, LM Studio SDK + +## Main Modules +- `src/` — Source code (7 files — agents, core, docs, features, integrations, lib, +8 more) +- `media/` — Webview assets (HTML/CSS/JS) (6 files) +- `core_py/` — Python utilities (7 files) +- `tests/` — Test suite (26 files — mocks) + +## Important Files +- `package.json` +- `tsconfig.json` +- `README.md` + +_Last auto-scan: 2026-05-13T13:33:48.141Z_ + + +## Purpose +_TODO_ diff --git a/.astra/project-context/scan-cache.json b/.astra/project-context/scan-cache.json new file mode 100644 index 0000000..439304e --- /dev/null +++ b/.astra/project-context/scan-cache.json @@ -0,0 +1,1598 @@ +{ + "version": 1, + "generatedAt": "2026-05-13T13:32:45.332Z", + "files": { + "src/agent.ts": { + "mtimeMs": 1778677012000, + "size": 184384, + "lines": 3207, + "role": "", + "imports": [ + "src/utils", + "src/config", + "src/security", + "src/core/transaction", + "src/core/session", + "src/agents/factory", + "src/agents/AgentWorkflowManager", + "src/core/errorHandler", + "src/core/events", + "src/core/errors", + "src/core/statusBar", + "src/core/lock", + "src/core/queue", + "src/core/conflict", + "src/core/telemetry", + "src/features/secondBrainTrace", + "src/memory", + "src/retrieval", + "src/retrieval/lessonHelpers", + "src/retrieval/embeddings", + "src/retrieval/brainIndex", + "src/skills/agentKnowledgeMap", + "src/retrieval/knowledgeMix", + "src/core/responseRecovery", + "src/lib/contextManager", + "src/lmstudio/streamer", + "src/features/approval/approvalQueue" + ] + }, + "src/agents/AgentWorkflowManager.ts": { + "mtimeMs": 1777808065000, + "size": 1745, + "lines": 50, + "role": "", + "imports": [ + "src/agents/factory", + "src/lib/engine" + ] + }, + "src/agents/factory.ts": { + "mtimeMs": 1778591282000, + "size": 7965, + "lines": 166, + "role": "", + "imports": [ + "src/config", + "src/lib/engine" + ] + }, + "src/bridge.ts": { + "mtimeMs": 1778251262000, + "size": 9705, + "lines": 227, + "role": "", + "imports": [ + "src/utils", + "src/config", + "src/core/services", + "src/skills/skillInjectionService", + "src/lib/paths" + ] + }, + "src/config.ts": { + "mtimeMs": 1778676053000, + "size": 9149, + "lines": 209, + "role": "", + "imports": [] + }, + "src/core/astraPath.ts": { + "mtimeMs": 1778169995000, + "size": 1782, + "lines": 50, + "role": "Astra Path Resolver (경로 해결기) Astra의 모든 데이터 파일(.astra 디렉토리)의 경로를 중앙에서 관리합니다. 확장 프로그램의 설치 경로(extensionUri) 기반으로 .astra 디렉토리를 해결하여, 사용자 프로젝트 루트가 아닌 ConnectAI 패키지 내부에 데이터를 저장합니다. 이 모듈은 AAL(Astra Autonomou", + "imports": [] + }, + "src/core/conflict.ts": { + "mtimeMs": 1778169995000, + "size": 1726, + "lines": 30, + "role": "", + "imports": [ + "src/utils" + ] + }, + "src/core/dataProcessor.ts": { + "mtimeMs": 1778169995000, + "size": 3235, + "lines": 96, + "role": "Aggregate result type definition", + "imports": [] + }, + "src/core/errorHandler.ts": { + "mtimeMs": 1777808065000, + "size": 2305, + "lines": 49, + "role": "", + "imports": [] + }, + "src/core/errors.ts": { + "mtimeMs": 1778169995000, + "size": 924, + "lines": 34, + "role": "g1nation Custom Error Classes", + "imports": [] + }, + "src/core/events.ts": { + "mtimeMs": 1777808065000, + "size": 993, + "lines": 35, + "role": "", + "imports": [] + }, + "src/core/health.ts": { + "mtimeMs": 1778169995000, + "size": 2873, + "lines": 79, + "role": "", + "imports": [ + "src/config", + "src/utils" + ] + }, + "src/core/lock.ts": { + "mtimeMs": 1778169995000, + "size": 2358, + "lines": 66, + "role": "", + "imports": [ + "src/utils" + ] + }, + "src/core/queue.ts": { + "mtimeMs": 1778674336000, + "size": 2428, + "lines": 81, + "role": "", + "imports": [ + "src/utils" + ] + }, + "src/core/responseRecovery.ts": { + "mtimeMs": 1778598587000, + "size": 12304, + "lines": 225, + "role": "Response Recovery — Thought Quarantine + Final-only Retry + Auto-Continuation The user already asked their question; they're waiting for an answer, not for a chance to babysit the generation engine. S", + "imports": [ + "src/lib/contextManager" + ] + }, + "src/core/services.ts": { + "mtimeMs": 1778421200000, + "size": 6692, + "lines": 164, + "role": "", + "imports": [ + "src/config", + "src/utils" + ] + }, + "src/core/session.ts": { + "mtimeMs": 1777808065000, + "size": 2785, + "lines": 88, + "role": "", + "imports": [ + "src/agent", + "src/utils" + ] + }, + "src/core/statusBar.ts": { + "mtimeMs": 1777808065000, + "size": 2000, + "lines": 57, + "role": "", + "imports": [] + }, + "src/core/telemetry.ts": { + "mtimeMs": 1778674336000, + "size": 5442, + "lines": 129, + "role": "Telemetry — append-only usage events to .astra/usage.jsonl Why local-file telemetry instead of a webview dashboard or remote endpoint: - Astra is local-first. No data leaves the machine. - JSONL is tr", + "imports": [ + "src/core/astraPath", + "src/utils" + ] + }, + "src/core/transaction.ts": { + "mtimeMs": 1778251556000, + "size": 4458, + "lines": 132, + "role": "", + "imports": [ + "src/core/errors", + "src/utils" + ] + }, + "src/docs/records/src/README.md": { + "mtimeMs": 1777949473000, + "size": 422, + "lines": 18, + "role": "src Chronicle Records", + "imports": [] + }, + "src/docs/records/src/bugs/BUG-0001-edited-agent-ts-edited-agent-ts-edited-agent-ts-edited-agent.md": { + "mtimeMs": 1777949473000, + "size": 1797, + "lines": 16, + "role": "Bug: Edited agent.ts Edited agent.ts Edited agent.ts Edited agent.ts Edited agent.ts ...", + "imports": [] + }, + "src/docs/records/src/bugs/BUG-0002-viewed-agent-ts-2159-2206-제공해주신-세부-파일-구조와-지식-수집-워크플로우-관점의-코드.md": { + "mtimeMs": 1777949638000, + "size": 2201, + "lines": 16, + "role": "Bug: Viewed agent.ts:2159-2206 제공해주신 세부 파일 구조와 지식 수집 워크플로우 관점의 코드리뷰 요청을 정독했습니다. 이...", + "imports": [] + }, + "src/docs/records/src/chronicle.config.json": { + "mtimeMs": 1777949638000, + "size": 517, + "lines": 11, + "role": "JSON configuration", + "imports": [] + }, + "src/docs/records/src/project-profile.md": { + "mtimeMs": 1777949473000, + "size": 582, + "lines": 31, + "role": "Project Profile", + "imports": [] + }, + "src/docs/records/src/timeline.md": { + "mtimeMs": 1777949638000, + "size": 381, + "lines": 10, + "role": "Project Timeline", + "imports": [] + }, + "src/extension.ts": { + "mtimeMs": 1778677220000, + "size": 37920, + "lines": 757, + "role": "", + "imports": [ + "src/utils", + "src/config", + "src/agent", + "src/bridge", + "src/sidebarProvider", + "src/core/health", + "src/core/astraPath", + "src/lmstudio/client", + "src/lmstudio/activityTracker", + "src/lmstudio/lifecycleManager", + "src/lmstudio/streamer", + "src/system/specs", + "src/features/approval/approvalQueue", + "src/features/approval/approvalPanelProvider", + "src/features/approval/approvalStatusBar", + "src/scaffolder/projectScaffolder", + "src/scaffolder/templates", + "src/integrations/telegram/telegramClient", + "src/integrations/telegram/telegramBot", + "src/core/services", + "src/features/settings/settingsPanelProvider", + "src/skills/agentKnowledgeMap", + "src/retrieval", + "src/retrieval/lessonHelpers", + "src/skills/scopedBrainRetriever" + ] + }, + "src/features/approval/approvalPanelProvider.ts": { + "mtimeMs": 1778674336000, + "size": 7132, + "lines": 153, + "role": "", + "imports": [ + "src/features/approval/approvalQueue", + "src/sidebarProvider" + ] + }, + "src/features/approval/approvalQueue.ts": { + "mtimeMs": 1778251548000, + "size": 4722, + "lines": 129, + "role": "", + "imports": [ + "src/utils" + ] + }, + "src/features/approval/approvalStatusBar.ts": { + "mtimeMs": 1778251623000, + "size": 1601, + "lines": 41, + "role": "", + "imports": [ + "src/features/approval/approvalQueue" + ] + }, + "src/features/projectArchitecture/index.ts": { + "mtimeMs": 1778679015000, + "size": 22657, + "lines": 515, + "role": "Project Architecture Context (Feature 2) Builds a markdown document that captures the durable facts about a project — its purpose, modules, key files, constraints, decisions — so Astra can attach it t", + "imports": [ + "src/utils", + "src/features/projectArchitecture/scanner", + "src/features/projectArchitecture/mermaid" + ] + }, + "src/features/projectArchitecture/intentDetector.ts": { + "mtimeMs": 1778676956000, + "size": 6170, + "lines": 148, + "role": "Project-intent detection from a chat message. Goal: when the user says \"나 ConnectAI 프로젝트 진행할 거야\" (or similar), spot the intent + project handle so the sidebar can activate Project Mode and auto-attach", + "imports": [] + }, + "src/features/projectArchitecture/mermaid.ts": { + "mtimeMs": 1778678215000, + "size": 2699, + "lines": 69, + "role": "Mermaid diagram renderers for the architecture doc. Two diagrams: a directory mindmap so the user can see the shape of the project at a glance, and a flowchart that shows which top-level module import", + "imports": [ + "src/features/projectArchitecture/scanner" + ] + }, + "src/features/projectArchitecture/scanner.ts": { + "mtimeMs": 1778678939000, + "size": 28299, + "lines": 644, + "role": "Deep static analyser for the Project Architecture Context generator. Walks the project tree (skipping the usual nodemodules / out / dist noise), pulls the role of each interesting file from its leadin", + "imports": [ + "src/features/projectArchitecture/y", + "src/features/projectArchitecture/side-effect", + "src/features/projectArchitecture/b" + ] + }, + "src/features/projectChronicle/guardPrompt.ts": { + "mtimeMs": 1777870211000, + "size": 6880, + "lines": 75, + "role": "", + "imports": [ + "src/features/projectChronicle/types" + ] + }, + "src/features/projectChronicle/index.ts": { + "mtimeMs": 1777808065000, + "size": 7490, + "lines": 189, + "role": "", + "imports": [ + "src/features/projectChronicle/markdownFileWriter", + "src/features/projectChronicle/types", + "src/features/projectChronicle/templates", + "src/features/projectChronicle/guardPrompt" + ] + }, + "src/features/projectChronicle/markdownFileWriter.ts": { + "mtimeMs": 1777808065000, + "size": 1317, + "lines": 41, + "role": "", + "imports": [] + }, + "src/features/projectChronicle/templates.ts": { + "mtimeMs": 1777808065000, + "size": 6699, + "lines": 258, + "role": "", + "imports": [ + "src/features/projectChronicle/types" + ] + }, + "src/features/projectChronicle/types.ts": { + "mtimeMs": 1778676848000, + "size": 3129, + "lines": 118, + "role": "", + "imports": [] + }, + "src/features/secondBrainTrace.ts": { + "mtimeMs": 1778248166000, + "size": 37475, + "lines": 760, + "role": "", + "imports": [ + "src/utils", + "src/retrieval/scoring" + ] + }, + "src/features/settings/settingsPanelProvider.ts": { + "mtimeMs": 1778674336000, + "size": 21422, + "lines": 489, + "role": "", + "imports": [ + "src/integrations/telegram/telegramClient", + "src/integrations/telegram/telegramBot", + "src/utils", + "src/lib/discoverModels", + "src/lib/paths" + ] + }, + "src/integrations/telegram/telegramBot.ts": { + "mtimeMs": 1778421270000, + "size": 11344, + "lines": 270, + "role": "", + "imports": [ + "src/integrations/telegram/telegramClient", + "src/integrations/telegram/types", + "src/utils" + ] + }, + "src/integrations/telegram/telegramClient.ts": { + "mtimeMs": 1778252333000, + "size": 5873, + "lines": 154, + "role": "", + "imports": [ + "src/integrations/telegram/types", + "src/utils" + ] + }, + "src/integrations/telegram/types.ts": { + "mtimeMs": 1778252305000, + "size": 1255, + "lines": 54, + "role": "Subset of the Telegram Bot API types we actually consume. Source: https://core.telegram.org/bots/api Only fields the bot reads or writes are typed — leaving the rest as unknown keeps the surface narro", + "imports": [] + }, + "src/lib/contextManager.ts": { + "mtimeMs": 1778597095000, + "size": 12982, + "lines": 275, + "role": "Context Manager (컨텍스트 한계 관리) \"context length = 132k\" 는 \"답변을 132k 토큰까지 생성해도 된다\" 가 아닙니다. 시스템 프롬프트 + 대화 기록 + 입력 문서 + 생성될 답변 + 여유분 ≤ context length 이 모듈은 요청을 보내기 전에 입력 토큰을 추정하고, - 동적으로 출력 상한(maxTokens)을 계", + "imports": [] + }, + "src/lib/diagnostics.ts": { + "mtimeMs": 1777978189000, + "size": 8389, + "lines": 210, + "role": "", + "imports": [ + "src/utils" + ] + }, + "src/lib/discoverModels.ts": { + "mtimeMs": 1778255896000, + "size": 1555, + "lines": 35, + "role": "", + "imports": [ + "src/utils" + ] + }, + "src/lib/engine.ts": { + "mtimeMs": 1777985699000, + "size": 38497, + "lines": 849, + "role": "", + "imports": [ + "src/core/lock", + "src/core/queue", + "src/utils", + "src/lib/diagnostics", + "src/lib/formatter", + "src/types/interfaces" + ] + }, + "src/lib/formatter.ts": { + "mtimeMs": 1777980992000, + "size": 3851, + "lines": 81, + "role": "", + "imports": [ + "src/lib/engine" + ] + }, + "src/lib/paths.ts": { + "mtimeMs": 1778591351000, + "size": 6416, + "lines": 151, + "role": "", + "imports": [] + }, + "src/lmstudio/activityTracker.ts": { + "mtimeMs": 1778246443000, + "size": 426, + "lines": 19, + "role": "", + "imports": [] + }, + "src/lmstudio/client.ts": { + "mtimeMs": 1778472808000, + "size": 5773, + "lines": 147, + "role": "", + "imports": [ + "src/utils" + ] + }, + "src/lmstudio/lifecycleManager.ts": { + "mtimeMs": 1778251087000, + "size": 10783, + "lines": 291, + "role": "", + "imports": [ + "src/lmstudio/client", + "src/lmstudio/activityTracker", + "src/utils", + "src/system/specs" + ] + }, + "src/lmstudio/streamer.ts": { + "mtimeMs": 1778598444000, + "size": 7762, + "lines": 162, + "role": "", + "imports": [ + "src/lmstudio/client", + "src/utils" + ] + }, + "src/memory/EpisodicMemory.ts": { + "mtimeMs": 1777858161000, + "size": 9577, + "lines": 278, + "role": "Episodic Memory (일화 기억) 과거 대화/회의/결정의 맥락 흐름을 저장합니다. 세션 종료 시 자동으로 에피소드를 요약하여 저장합니다. \"왜 이렇게 결정했는지\", \"어떤 흐름으로 진행했는지\" 기록. 저장 위치: {brainPath}/memory/episodes/.json", + "imports": [ + "src/memory/types" + ] + }, + "src/memory/LongTermMemory.ts": { + "mtimeMs": 1777858068000, + "size": 8364, + "lines": 243, + "role": "Long-Term Memory (장기 기억) 사용자의 취향, 프로젝트 목표, 반복 규칙, 과거 결정 사항을 영구적으로 저장하고 관리합니다. 저장 위치: {brainPath}/memory/longterm.json", + "imports": [ + "src/memory/types" + ] + }, + "src/memory/MemoryExtractor.ts": { + "mtimeMs": 1777858183000, + "size": 3838, + "lines": 115, + "role": "Memory Extractor (기억 추출기) 대화 종료 시 히스토리를 분석하여 각 메모리 레이어에 저장할 정보를 자동으로 추출합니다. LLM 호출 없이 패턴 매칭 기반으로 동작합니다.", + "imports": [ + "src/memory/LongTermMemory", + "src/memory/ProjectMemory", + "src/memory/EpisodicMemory" + ] + }, + "src/memory/ProceduralMemory.ts": { + "mtimeMs": 1777858120000, + "size": 5533, + "lines": 173, + "role": "Procedural Memory (절차 기억) 반복 작업의 절차와 패턴을 관리합니다. 기존 skill.md 시스템과 통합되어, Brain의 memory/procedures/ 아래의 MD 파일을 스캔하여 절차를 로드합니다. 저장 위치: {brainPath}/memory/procedures/.md", + "imports": [ + "src/memory/types" + ] + }, + "src/memory/ProjectMemory.ts": { + "mtimeMs": 1777864677000, + "size": 6533, + "lines": 212, + "role": "Project Memory (프로젝트 기억) 프로젝트별 요구사항, 코드 구조, 아키텍처 결정, 버그 기록 등을 Astra 확장 프로그램 내부에 저장하고 관리합니다. 저장 위치: {ConnectAI}/.astra/projectmemory.json (기존: {projectRoot}/.astra/ → 변경됨)", + "imports": [ + "src/memory/types", + "src/core/astraPath" + ] + }, + "src/memory/ShortTermMemory.ts": { + "mtimeMs": 1777858031000, + "size": 1167, + "lines": 37, + "role": "Short-Term Memory (단기 기억) 현재 대화의 즉시 맥락을 관리합니다. FIFO 방식으로 최근 N개 메시지를 유지합니다.", + "imports": [ + "src/memory/types" + ] + }, + "src/memory/index.ts": { + "mtimeMs": 1777858214000, + "size": 6310, + "lines": 188, + "role": "MemoryManager — 5-Layer Cognitive Memory System (통합 진입점) Astra의 모든 메모리 레이어를 통합 관리하는 중앙 매니저입니다. ① Short-Term Memory — 현재 대화 흐름 (FIFO) ② Long-Term Memory — 사용자 취향/규칙/결정 ③ Project Memory — 프로젝트별 지식 ④ Pro", + "imports": [ + "src/config", + "src/memory/ShortTermMemory", + "src/memory/LongTermMemory", + "src/memory/ProjectMemory", + "src/memory/ProceduralMemory", + "src/memory/EpisodicMemory", + "src/memory/MemoryExtractor", + "src/memory/types" + ] + }, + "src/memory/types.ts": { + "mtimeMs": 1777858019000, + "size": 3449, + "lines": 126, + "role": "Memory Type Definitions (메모리 타입 정의) Astra의 5-Layer Cognitive Memory System의 모든 타입을 정의합니다. ① Short-Term ② Long-Term ③ Project ④ Procedural ⑤ Episodic", + "imports": [] + }, + "src/retrieval/brainIndex.ts": { + "mtimeMs": 1778674336000, + "size": 12945, + "lines": 325, + "role": "Brain Index — persistent, mtime-keyed tokenized cache of the Second Brain RAG 검색은 매 질의마다 브레인의 모든 .md 파일을 읽고 토크나이즈해서 TF-IDF 점수를 계산했습니다 — 파일 수가 많아지면 그게 병목입니다. 이 모듈은 /.astra/brain-index.json 에", + "imports": [ + "src/retrieval/scoring", + "src/retrieval/lessonHelpers", + "src/utils" + ] + }, + "src/retrieval/contextBudget.ts": { + "mtimeMs": 1778674336000, + "size": 5143, + "lines": 140, + "role": "Context Budget Manager (컨텍스트 예산 관리) 시스템 프롬프트의 토큰 예산을 관리하여 로컬 모델의 context window를 효율적으로 활용합니다.", + "imports": [ + "src/retrieval/types" + ] + }, + "src/retrieval/embeddings.ts": { + "mtimeMs": 1778674336000, + "size": 7294, + "lines": 167, + "role": "Embeddings — local hybrid (sparse + dense) retrieval support TF-IDF is fast and zero-cost but misses synonyms / paraphrase. A small local embedding model (BGE-small, multilingual-e5-small, nomic-embed", + "imports": [ + "src/utils" + ] + }, + "src/retrieval/index.ts": { + "mtimeMs": 1778676255000, + "size": 23764, + "lines": 514, + "role": "RetrievalOrchestrator — Unified RAG Pipeline Astra의 모든 검색 소스를 통합 관리하는 오케스트레이터입니다. 검색 흐름: ① Query Planning — 의도 분류 + 검색 전략 결정 ② Parallel Search — Brain + Memory + Project + Episode 동시 검색 ③ Result Fusio", + "imports": [ + "src/config", + "src/utils", + "src/lib/paths", + "src/memory", + "src/retrieval/types", + "src/retrieval/scoring", + "src/retrieval/contextBudget", + "src/retrieval/brainIndex", + "src/retrieval/lessonHelpers", + "src/retrieval/embeddings" + ] + }, + "src/retrieval/knowledgeMix.ts": { + "mtimeMs": 1778676121000, + "size": 6947, + "lines": 161, + "role": "Knowledge Mix — controls how much the assistant leans on Second Brain evidence vs. the model's own general knowledge for a given query. The single integer \"secondBrainWeight\" (0–100) drives three thin", + "imports": [ + "src/config", + "src/skills/agentKnowledgeMap" + ] + }, + "src/retrieval/lessonHelpers.ts": { + "mtimeMs": 1778674336000, + "size": 14134, + "lines": 325, + "role": "Lesson / Experience Memory — pure helpers (no vscode dependency) \"Lesson\" = a markdown file in the active brain that captures a past mistake/risk and how to avoid repeating it. Identified by a lessons", + "imports": [ + "src/retrieval/scoring" + ] + }, + "src/retrieval/scoring.ts": { + "mtimeMs": 1778674336000, + "size": 20793, + "lines": 518, + "role": "Scoring Engine — TF-IDF + Bilingual Tokenizer 단순 includes() 키워드 매칭을 넘어서, TF-IDF 가중치 기반의 문서 스코어링을 제공합니다. 한국어/영어 양국어 토크나이저를 포함합니다.", + "imports": [] + }, + "src/retrieval/types.ts": { + "mtimeMs": 1778674336000, + "size": 2424, + "lines": 66, + "role": "Retrieval Types (검색 결과 통합 타입) 모든 검색 소스(Brain, Memory, Project, Episode)의 결과를 통합 인터페이스로 정의합니다.", + "imports": [] + }, + "src/scaffolder/projectScaffolder.ts": { + "mtimeMs": 1778251854000, + "size": 4508, + "lines": 111, + "role": "", + "imports": [ + "src/scaffolder/templates", + "src/lib/paths", + "src/utils" + ] + }, + "src/scaffolder/templates.ts": { + "mtimeMs": 1778251830000, + "size": 4897, + "lines": 154, + "role": "Scaffolder template catalog. Templates are pure data — (projectName) => { [relativePath]: contents }. New templates are added by appending to TEMPLATES; the rest of the scaffolder (validation, IO, com", + "imports": [] + }, + "src/security.ts": { + "mtimeMs": 1778169995000, + "size": 3310, + "lines": 90, + "role": "", + "imports": [] + }, + "src/sidebar/agentHandlers.ts": { + "mtimeMs": 1778676515000, + "size": 7752, + "lines": 169, + "role": "", + "imports": [ + "src/sidebarProvider", + "src/utils", + "src/skills/agentKnowledgeMap" + ] + }, + "src/sidebar/brainHandlers.ts": { + "mtimeMs": 1778248976000, + "size": 1080, + "lines": 33, + "role": "", + "imports": [ + "src/sidebarProvider" + ] + }, + "src/sidebar/chatHandlers.ts": { + "mtimeMs": 1778677129000, + "size": 8596, + "lines": 175, + "role": "", + "imports": [ + "src/sidebarProvider", + "src/utils", + "src/lib/paths" + ] + }, + "src/sidebar/chronicleHandlers.ts": { + "mtimeMs": 1778248986000, + "size": 2068, + "lines": 52, + "role": "", + "imports": [ + "src/sidebarProvider" + ] + }, + "src/sidebarProvider.ts": { + "mtimeMs": 1778679027000, + "size": 112397, + "lines": 2603, + "role": "", + "imports": [ + "src/utils", + "src/config", + "src/agent", + "src/bridge", + "src/features/projectChronicle", + "src/lmstudio/lifecycleManager", + "src/lmstudio/activityTracker", + "src/sidebar/chatHandlers", + "src/sidebar/brainHandlers", + "src/sidebar/chronicleHandlers", + "src/sidebar/agentHandlers", + "src/skills/agentKnowledgeMap", + "src/lib/contextManager", + "src/skills/externalSkillLoader", + "src/features/projectArchitecture", + "src/features/projectArchitecture/intentDetector" + ] + }, + "src/skills/agentKnowledgeMap.ts": { + "mtimeMs": 1778676092000, + "size": 15707, + "lines": 374, + "role": "", + "imports": [ + "src/lib/paths", + "src/utils" + ] + }, + "src/skills/externalSkillLoader.ts": { + "mtimeMs": 1778470672000, + "size": 5859, + "lines": 159, + "role": "", + "imports": [ + "src/lib/paths", + "src/utils" + ] + }, + "src/skills/scopedBrainRetriever.ts": { + "mtimeMs": 1778419289000, + "size": 5706, + "lines": 153, + "role": "", + "imports": [ + "src/utils", + "src/lib/paths", + "src/retrieval/scoring", + "src/retrieval/contextBudget" + ] + }, + "src/skills/skillInjectionService.ts": { + "mtimeMs": 1778251221000, + "size": 6276, + "lines": 145, + "role": "", + "imports": [ + "src/lib/paths", + "src/utils" + ] + }, + "src/system/specs.ts": { + "mtimeMs": 1778251036000, + "size": 4326, + "lines": 118, + "role": "", + "imports": [] + }, + "src/types/interfaces.ts": { + "mtimeMs": 1778169995000, + "size": 1260, + "lines": 39, + "role": "Service Interfaces (서비스 인터페이스 정의) 각 서비스(Agent, Brain, FileSystem 등)의 추상화 인터페이스를 정의합니다. 의존성 주입(DI)과 단위 테스트를 위해 필수적입니다.", + "imports": [] + }, + "src/utils.ts": { + "mtimeMs": 1778675047000, + "size": 11450, + "lines": 268, + "role": "", + "imports": [ + "src/config" + ] + }, + "media/settings-panel.css": { + "mtimeMs": 1778255405000, + "size": 4546, + "lines": 210, + "role": "Stylesheet", + "imports": [] + }, + "media/settings-panel.html": { + "mtimeMs": 1778255979000, + "size": 7659, + "lines": 164, + "role": "Astra Settings", + "imports": [] + }, + "media/settings-panel.js": { + "mtimeMs": 1778256014000, + "size": 11176, + "lines": 270, + "role": "", + "imports": [] + }, + "media/sidebar.css": { + "mtimeMs": 1778677793000, + "size": 38372, + "lines": 987, + "role": "Stylesheet", + "imports": [] + }, + "media/sidebar.html": { + "mtimeMs": 1778677855000, + "size": 16364, + "lines": 285, + "role": "Astra", + "imports": [] + }, + "media/sidebar.js": { + "mtimeMs": 1778677844000, + "size": 77309, + "lines": 1388, + "role": "", + "imports": [] + }, + "tests/agentEngine.test.ts": { + "mtimeMs": 1777983087000, + "size": 27182, + "lines": 646, + "role": "AgentEngine Integration Tests & Performance Benchmarks 검증 대상: 1. ErrorClassifier — 오류 유형(Transient/Permanent/Abort) 자동 분류 2. ErrorRecoveryMatrix — 각 규칙이 의도한 대응 전략으로 매핑되는지 검증 3. resilientExecute — 지수 백", + "imports": [ + "src/lib/engine" + ] + }, + "tests/approvalQueue.test.ts": { + "mtimeMs": 1778251690000, + "size": 5915, + "lines": 164, + "role": "Unit tests for ApprovalQueue. Strategy: drive enqueue → approve / reject / clear / pre-empt directly, confirm the onChange event fires at the right moments and callbacks fire exactly once.", + "imports": [ + "src/features/approval/approvalQueue" + ] + }, + "tests/brainIndex.test.ts": { + "mtimeMs": 1778594450000, + "size": 5406, + "lines": 107, + "role": "", + "imports": [ + "src/retrieval/brainIndex" + ] + }, + "tests/contextManager.test.ts": { + "mtimeMs": 1778594523000, + "size": 6545, + "lines": 129, + "role": "", + "imports": [ + "src/lib/contextManager" + ] + }, + "tests/dataProcessor.test.ts": { + "mtimeMs": 1777808065000, + "size": 3430, + "lines": 87, + "role": "/ ", + "imports": [ + "src/core/dataProcessor" + ] + }, + "tests/findBrainFilesCache.test.ts": { + "mtimeMs": 1778247665000, + "size": 2720, + "lines": 80, + "role": "Unit tests for findBrainFiles TTL cache.", + "imports": [ + "src/utils" + ] + }, + "tests/integration_retrieval.test.ts": { + "mtimeMs": 1777949141000, + "size": 4017, + "lines": 91, + "role": "", + "imports": [ + "src/retrieval", + "src/utils" + ] + }, + "tests/lessonHelpers.test.ts": { + "mtimeMs": 1778595482000, + "size": 9400, + "lines": 191, + "role": "", + "imports": [ + "src/retrieval/lessonHelpers" + ] + }, + "tests/lmStudioLifecycle.test.ts": { + "mtimeMs": 1778250254000, + "size": 11385, + "lines": 318, + "role": "Unit tests for ModelLifecycleManager. Strategy: inject mock ILMStudioClient and a simple in-memory IActivityTracker. No real LM Studio or SDK is touched — the manager file does not import the SDK dire", + "imports": [ + "src/lmstudio/lifecycleManager", + "src/lmstudio/client", + "src/lmstudio/activityTracker" + ] + }, + "tests/lmStudioStreamer.test.ts": { + "mtimeMs": 1778591604000, + "size": 8660, + "lines": 220, + "role": "Unit tests for LMStudioStreamer. Strategy: inject a fake ILMStudioClient that returns a fake model handle whose respond() yields a controllable async iterable. No real SDK or WebSocket touched.", + "imports": [ + "src/lmstudio/streamer", + "src/lmstudio/client" + ] + }, + "tests/localPathPreflight.test.ts": { + "mtimeMs": 1778595955000, + "size": 23687, + "lines": 490, + "role": "", + "imports": [ + "src/agent" + ] + }, + "tests/mocks/vscode.js": { + "mtimeMs": 1778246810000, + "size": 1670, + "lines": 68, + "role": "", + "imports": [] + }, + "tests/paths.test.ts": { + "mtimeMs": 1778250990000, + "size": 2590, + "lines": 84, + "role": "Unit tests for the centralized path resolver.", + "imports": [ + "src/lib/paths" + ] + }, + "tests/projectChronicle.test.ts": { + "mtimeMs": 1778169995000, + "size": 8359, + "lines": 199, + "role": "", + "imports": [ + "src/features/projectChronicle" + ] + }, + "tests/projectChronicleGuardPrompt.test.ts": { + "mtimeMs": 1777808369000, + "size": 2838, + "lines": 52, + "role": "", + "imports": [ + "src/features/projectChronicle" + ] + }, + "tests/projectScaffolder.test.ts": { + "mtimeMs": 1778251915000, + "size": 5800, + "lines": 135, + "role": "Unit tests for FileSystemProjectScaffolder. Drives against a real temp directory so end-to-end file IO + path-traversal defenses are exercised.", + "imports": [ + "src/scaffolder/projectScaffolder" + ] + }, + "tests/resilience_stress.test.ts": { + "mtimeMs": 1777968922000, + "size": 6981, + "lines": 183, + "role": "Resilience & Boundary Stress Test Suite (v2.77.3) 이 테스트는 ConnectAI 엔진이 극한의 환경(인증 실패, 네트워크 차단, 타임아웃 등)에서 얼마나 안정적으로 복구되고, 신뢰성 지표(Resilience Metrics)를 정확히 기록하는지 검증합니다.", + "imports": [ + "src/lib/engine" + ] + }, + "tests/resilience_v4.test.ts": { + "mtimeMs": 1777978211000, + "size": 3435, + "lines": 84, + "role": "", + "imports": [ + "src/lib/engine", + "src/lib/diagnostics" + ] + }, + "tests/responseRecovery.test.ts": { + "mtimeMs": 1778598707000, + "size": 8782, + "lines": 151, + "role": "", + "imports": [ + "src/core/responseRecovery" + ] + }, + "tests/scoring.test.ts": { + "mtimeMs": 1778169995000, + "size": 6055, + "lines": 134, + "role": "", + "imports": [ + "src/retrieval/scoring" + ] + }, + "tests/secondBrainTrace.test.ts": { + "mtimeMs": 1777866065000, + "size": 23024, + "lines": 407, + "role": "", + "imports": [ + "src/features/secondBrainTrace" + ] + }, + "tests/skillInjectionService.test.ts": { + "mtimeMs": 1778251292000, + "size": 6741, + "lines": 172, + "role": "Unit tests for FileSystemSkillInjectionService. Strategy: drive the service against a real temp directory so path-traversal defenses and writeFileSync paths are exercised end-to-end. The service accep", + "imports": [ + "src/skills/skillInjectionService" + ] + }, + "tests/systemPrompt.test.ts": { + "mtimeMs": 1777966465000, + "size": 1215, + "lines": 29, + "role": "", + "imports": [ + "src/utils" + ] + }, + "tests/systemSpecs.test.ts": { + "mtimeMs": 1778251130000, + "size": 3464, + "lines": 90, + "role": "Unit tests for SystemSpecs + HeuristicModelMemoryEstimator. Strategy: - HeuristicModelMemoryEstimator is pure — directly drive it with model ids. - NodeSystemSpecsProvider depends on os. so we test: a", + "imports": [ + "src/system/specs" + ] + }, + "tests/telegramBot.test.ts": { + "mtimeMs": 1778253785000, + "size": 13012, + "lines": 363, + "role": "Unit tests for TelegramBot + truncateForTelegram. Strategy: - TelegramBot is driven by an injected ITelegramClient stub. We script getUpdates to return queued batches and assert that: - the offset cur", + "imports": [ + "src/integrations/telegram/telegramBot", + "src/integrations/telegram/telegramClient", + "src/integrations/telegram/types" + ] + }, + "tests/transaction.test.ts": { + "mtimeMs": 1777808065000, + "size": 2064, + "lines": 68, + "role": "/ ", + "imports": [ + "src/core/transaction" + ] + }, + "tests/vulnerability.test.ts": { + "mtimeMs": 1777808065000, + "size": 2100, + "lines": 60, + "role": "/ ", + "imports": [ + "src/core/lock", + "src/core/transaction" + ] + }, + "core_py/events.py": { + "mtimeMs": 1777559069000, + "size": 2374, + "lines": 64, + "role": "", + "imports": [] + }, + "core_py/inference.py": { + "mtimeMs": 1777559249000, + "size": 3834, + "lines": 91, + "role": "", + "imports": [] + }, + "core_py/loader.py": { + "mtimeMs": 1777559060000, + "size": 2368, + "lines": 61, + "role": "", + "imports": [] + }, + "core_py/monitoring.py": { + "mtimeMs": 1777559685000, + "size": 2060, + "lines": 56, + "role": "", + "imports": [] + }, + "core_py/optimizer.py": { + "mtimeMs": 1777559265000, + "size": 2327, + "lines": 55, + "role": "", + "imports": [] + }, + "core_py/queue_worker.py": { + "mtimeMs": 1777559676000, + "size": 2987, + "lines": 82, + "role": "", + "imports": [] + }, + "docs/Advanced_Features_Implementation_Guide.md": { + "mtimeMs": 1777808065000, + "size": 1804, + "lines": 40, + "role": "Advanced Features Implementation Guide", + "imports": [] + }, + "docs/AgentEngine_Architecture.md": { + "mtimeMs": 1777863061000, + "size": 14338, + "lines": 314, + "role": "AgentEngine Architecture Document", + "imports": [] + }, + "docs/EXPERIENCE_MEMORY_PLAN.md": { + "mtimeMs": 1778595595000, + "size": 9390, + "lines": 122, + "role": "Experience Memory (Mistake / Lesson Loop) — Implementation Plan", + "imports": [] + }, + "docs/PROJECT_CHRONICLE_GUARD_ROADMAP.md": { + "mtimeMs": 1777948230000, + "size": 1634, + "lines": 43, + "role": "Project Chronicle Guard: Search Engine Roadmap", + "imports": [] + }, + "docs/TELEGRAM_REMOTE_EXECUTION_PLAN.md": { + "mtimeMs": 1778592247000, + "size": 12254, + "lines": 452, + "role": "Telegram Remote Execution 기획서", + "imports": [] + }, + "docs/UX_UI_Consistency_Guidelines.md": { + "mtimeMs": 1777808065000, + "size": 2509, + "lines": 44, + "role": "UX/UI Consistency Guidelines", + "imports": [] + }, + "docs/docs/records/docs/README.md": { + "mtimeMs": 1777949758000, + "size": 427, + "lines": 18, + "role": "docs Chronicle Records", + "imports": [] + }, + "docs/docs/records/docs/bugs/BUG-0001-viewed-integration-retrieval-test-ts-1-59-integration-retrie.md": { + "mtimeMs": 1777949758000, + "size": 2059, + "lines": 16, + "role": "Bug: Viewed integrationretrieval.test.ts:1-59 integrationretrieval.test.ts를 통해 ...", + "imports": [] + }, + "docs/docs/records/docs/chronicle.config.json": { + "mtimeMs": 1777949758000, + "size": 522, + "lines": 11, + "role": "JSON configuration", + "imports": [] + }, + "docs/docs/records/docs/project-profile.md": { + "mtimeMs": 1777949758000, + "size": 586, + "lines": 31, + "role": "Project Profile", + "imports": [] + }, + "docs/docs/records/docs/timeline.md": { + "mtimeMs": 1777949758000, + "size": 210, + "lines": 7, + "role": "Project Timeline", + "imports": [] + }, + "docs/records/ConnectAI/README.md": { + "mtimeMs": 1777808202000, + "size": 598, + "lines": 18, + "role": "Astra Project Chronicle Records", + "imports": [] + }, + "docs/records/ConnectAI/bugs/BUG-0001-volumes-data-project-antigravity-connectai-프로젝트-코드-리뷰-해줄-수-있.md": { + "mtimeMs": 1777865266000, + "size": 1408, + "lines": 16, + "role": "Bug: /Volumes/Data/project/Antigravity/ConnectAI 프로젝트 코드 리뷰 해줄 수 있어? 개선할 부분이 있는지, 그러고...", + "imports": [] + }, + "docs/records/ConnectAI/bugs/BUG-0002-지금-내가-분석-요청하고-너가-답을-줄때-아래-템플릿에-맞춰-답을-써주고-있는데-개선-포인트가-있는지-확인해.md": { + "mtimeMs": 1777889979000, + "size": 2559, + "lines": 16, + "role": "Bug: 지금 내가 분석 요청하고 너가 답을 줄때 아래 템플릿에 맞춰 답을 써주고 있는데, 개선 포인트가 있는지 확인해줘. ## 내가 보는 위험 가장 큰...", + "imports": [] + }, + "docs/records/ConnectAI/bugs/BUG-0003-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md": { + "mtimeMs": 1777962835000, + "size": 1891, + "lines": 16, + "role": "Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...", + "imports": [] + }, + "docs/records/ConnectAI/bugs/BUG-0004-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md": { + "mtimeMs": 1777964236000, + "size": 1883, + "lines": 16, + "role": "Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...", + "imports": [] + }, + "docs/records/ConnectAI/bugs/BUG-0005-다시한번-답줘-volumes-data-project-antigravity-connectai-내-질문에-대한-.md": { + "mtimeMs": 1777965245000, + "size": 1868, + "lines": 16, + "role": "Bug: 다시한번 답줘. /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는...", + "imports": [] + }, + "docs/records/ConnectAI/bugs/BUG-0006-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md": { + "mtimeMs": 1777965308000, + "size": 1623, + "lines": 16, + "role": "Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...", + "imports": [] + }, + "docs/records/ConnectAI/bugs/BUG-0007-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md": { + "mtimeMs": 1777965612000, + "size": 1823, + "lines": 16, + "role": "Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...", + "imports": [] + }, + "docs/records/ConnectAI/bugs/BUG-0008-volumes-data-project-antigravity-connectai-내-질문에-대한-답변이-잘-정리.md": { + "mtimeMs": 1777966704000, + "size": 1737, + "lines": 16, + "role": "Bug: /Volumes/Data/project/Antigravity/ConnectAI 내 질문에 대한 답변이 잘 정리되서 알려주긴 하는데 focused...", + "imports": [] + }, + "docs/records/ConnectAI/bugs/BUG-0009-문제점을-읽고-어떻게-개선하는게-최선인지-분석해주면-좋겠어-알겠습니다-지금부터-connectai-프로젝트-에.md": { + "mtimeMs": 1777977740000, + "size": 2295, + "lines": 16, + "role": "Bug: 문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 ConnectAI 프로젝트에만 완전히 집중하겠습니다. ...", + "imports": [] + }, + "docs/records/ConnectAI/bugs/BUG-0010-문제점을-읽고-어떻게-개선하는게-최선인지-분석해주면-좋겠어-알겠습니다-지금부터-connectai-프로젝트-에.md": { + "mtimeMs": 1777981089000, + "size": 2339, + "lines": 16, + "role": "Bug: 문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 ConnectAI 프로젝트에만 완전히 집중하겠습니다. ...", + "imports": [] + }, + "docs/records/ConnectAI/bugs/BUG-0011-문제점을-읽고-어떻게-개선하는게-최선인지-분석해주면-좋겠어-알겠습니다-지금부터-connectai-프로젝트-에.md": { + "mtimeMs": 1777982331000, + "size": 2277, + "lines": 16, + "role": "Bug: 문제점을 읽고 어떻게 개선하는게 최선인지 분석해주면 좋겠어. 알겠습니다. 지금부터 ConnectAI 프로젝트에만 완전히 집중하겠습니다. ...", + "imports": [] + }, + "docs/records/ConnectAI/chronicle.config.json": { + "mtimeMs": 1778678912000, + "size": 416, + "lines": 11, + "role": "JSON configuration", + "imports": [] + }, + "docs/records/ConnectAI/decisions/ADR-0001-volumes-data-project-antigravity-connectai-이-프로젝트를-지금-개발-중에-.md": { + "mtimeMs": 1777860282000, + "size": 1444, + "lines": 19, + "role": "ADR: /Volumes/Data/project/Antigravity/ConnectAI 이 프로젝트를 지금 개발 중에 있어. 코드 리뷰를 하고 잘된점과 ...", + "imports": [] + }, + "docs/records/ConnectAI/decisions/ADR-0002-지금-너의-제2뇌-지식을-이용해서-아래-프로젝트-평가하고-앞으로-어느부분을-더-집중해서-개선을-하면-좋을지-.md": { + "mtimeMs": 1777869330000, + "size": 1878, + "lines": 19, + "role": "ADR: 지금 너의 제2뇌 지식을 이용해서 아래 프로젝트 평가하고 앞으로 어느부분을 더 집중해서 개선을 하면 좋을지 알려주면 좋겠어. /Volumes/D...", + "imports": [] + }, + "docs/records/ConnectAI/decisions/ADR-0003-volumes-data-project-antigravity-connectai-이-프로젝트-자체를-나는-자비스.md": { + "mtimeMs": 1777877376000, + "size": 1786, + "lines": 19, + "role": "ADR: /Volumes/Data/project/Antigravity/ConnectAI 이 프로젝트 자체를 나는 자비스와 같은 툴로 만드는 것이 내 목표...", + "imports": [] + }, + "docs/records/ConnectAI/decisions/ADR-0004-volumes-data-project-antigravity-connectai-이-프로젝트-자체를-나는-자비스.md": { + "mtimeMs": 1777878318000, + "size": 1472, + "lines": 19, + "role": "ADR: /Volumes/Data/project/Antigravity/ConnectAI 이 프로젝트 자체를 나는 자비스와 같은 툴로 만드는 것이 내 목표...", + "imports": [] + }, + "docs/records/ConnectAI/decisions/ADR-0005-volumes-data-project-antigravity-connectai-이-프로젝트-아키텍처-분석해줘.md": { + "mtimeMs": 1777889802000, + "size": 1512, + "lines": 19, + "role": "ADR: /Volumes/Data/project/Antigravity/ConnectAI 이 프로젝트 아키텍처 분석해줘.", + "imports": [] + }, + "docs/records/ConnectAI/decisions/ADR-0006-volumes-data-project-antigravity-connectai-프로젝트에-대해서-너가-사용자-.md": { + "mtimeMs": 1777902312000, + "size": 1604, + "lines": 19, + "role": "ADR: /Volumes/Data/project/Antigravity/ConnectAI 프로젝트에 대해서 너가 사용자 입장에서 평가 해주면 좋겠어.", + "imports": [] + }, + "docs/records/ConnectAI/decisions/ADR-0007-volumes-data-project-antigravity-connectai-이거에-기능-개선을-하고-싶어-.md": { + "mtimeMs": 1778398958000, + "size": 1925, + "lines": 19, + "role": "ADR: /Volumes/Data/project/Antigravity/ConnectAI 이거에 기능 개선을 하고 싶어. 너가 기획과 방향성을 정해주면 좋...", + "imports": [] + }, + "docs/records/ConnectAI/decisions/ADR-0008-volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이.md": { + "mtimeMs": 1778600479000, + "size": 1606, + "lines": 19, + "role": "ADR: /Volumes/Data/project/Antigravity/ConnectAI 분석하고 부족한 부분이나 개선이 필요한 부분을 알려주면 좋겠어.", + "imports": [] + }, + "docs/records/ConnectAI/decisions/ADR-0009-이-프로젝트의-구조에-대해서-설명해봐.md": { + "mtimeMs": 1778678639000, + "size": 1507, + "lines": 19, + "role": "ADR: 이 프로젝트의 구조에 대해서 설명해봐.", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-02_answer-format-readability-tuning.md": { + "mtimeMs": 1777808065000, + "size": 1534, + "lines": 30, + "role": "Development Log: Answer Format Readability Tuning", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-02_connectai_project_knowledge_overview.md": { + "mtimeMs": 1777808211000, + "size": 4540, + "lines": 121, + "role": "Astra Project Knowledge Overview", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-02_local-path-code-review-preflight.md": { + "mtimeMs": 1777808065000, + "size": 1855, + "lines": 35, + "role": "Development Log: Local Path Code Review Preflight", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-02_no-evidence-no-project-claim.md": { + "mtimeMs": 1777808065000, + "size": 1528, + "lines": 35, + "role": "Development Log: No Evidence, No Project Claim", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-02_progressive-answer-format.md": { + "mtimeMs": 1777808065000, + "size": 1194, + "lines": 29, + "role": "Development Log: Progressive Answer Format", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-02_project-claim-output-brake.md": { + "mtimeMs": 1777808065000, + "size": 1489, + "lines": 35, + "role": "Development Log: Project Claim Output Brake", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-02_project-claim-policy-enforcement.md": { + "mtimeMs": 1777808065000, + "size": 1516, + "lines": 35, + "role": "Development Log: Project Claim Policy Enforcement", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-02_query-intent-search-tuning.md": { + "mtimeMs": 1777808065000, + "size": 1911, + "lines": 35, + "role": "Development Log: Query Intent Search Tuning", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-02_remove-local-template-replies.md": { + "mtimeMs": 1777808065000, + "size": 1049, + "lines": 25, + "role": "Development Log: Remove Local Template Replies", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-02_second-brain-trace-quality-tuning.md": { + "mtimeMs": 1777808065000, + "size": 1441, + "lines": 26, + "role": "Development Log: Second Brain Trace Quality Tuning", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-03_connectai_project_knowledge_overview.md": { + "mtimeMs": 1777808211000, + "size": 4666, + "lines": 121, + "role": "Astra Project Knowledge Overview", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-05_volumes-data-project-antigravity-connectai-오늘-많은-것을-업데이트했어-많_implementation.md": { + "mtimeMs": 1777986879000, + "size": 1836, + "lines": 22, + "role": "Development Log: /Volumes/Data/project/Antigravity/ConnectAI 오늘 많은 것을 업데이트했어. 많이 고친것 같지만 그럼에도 불구하...", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-05_volumes-data-project-antigravity-connectai-이-프로젝트-분석해줘-volum_implementation.md": { + "mtimeMs": 1777967233000, + "size": 1747, + "lines": 23, + "role": "Development Log: /Volumes/Data/project/Antigravity/ConnectAI 이 프로젝트 분석해줘./Volumes/Data/project/An...", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-05_volumes-data-project-antigravity-connectai-이-프로젝트-분석해줘_implementation.md": { + "mtimeMs": 1777966822000, + "size": 1727, + "lines": 22, + "role": "Development Log: /Volumes/Data/project/Antigravity/ConnectAI 이 프로젝트 분석해줘.", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation-2.md": { + "mtimeMs": 1778598784000, + "size": 1575, + "lines": 22, + "role": "Development Log: /Volumes/Data/project/Antigravity/ConnectAI 분석하고 부족한 부분이나 개선이 필요한 부분을 알려주면 좋겠어.", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation-3.md": { + "mtimeMs": 1778599019000, + "size": 1641, + "lines": 22, + "role": "Development Log: /Volumes/Data/project/Antigravity/ConnectAI 분석하고 부족한 부분이나 개선이 필요한 부분을 알려주면 좋겠어.", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation-4.md": { + "mtimeMs": 1778600781000, + "size": 1715, + "lines": 24, + "role": "Development Log: /Volumes/Data/project/Antigravity/ConnectAI 분석하고 부족한 부분이나 개선이 필요한 부분을 알려주면 좋겠어.", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation.md": { + "mtimeMs": 1778598509000, + "size": 684, + "lines": 22, + "role": "Development Log: /Volumes/Data/project/Antigravity/ConnectAI 분석하고 부족한 부분이나 개선이 필요한 부분을 알려주면 좋겠어.", + "imports": [] + }, + "docs/records/ConnectAI/development/2026-05-13_너는-분석-요청하거나-내가-작업-요청을-할때-connectai-architecture-md-문서를-참고하고-_implementation.md": { + "mtimeMs": 1778678912000, + "size": 1738, + "lines": 22, + "role": "Development Log: 너는 분석 요청하거나 내가 작업 요청을 할때 connectai architecture.md 문서를 참고하고 작업을 하나?", + "imports": [] + }, + "docs/records/ConnectAI/discussions/2026-05-13_volumes-data-project-antigravity-connectai-이-프로젝트를-작업할거야.md": { + "mtimeMs": 1778677791000, + "size": 719, + "lines": 16, + "role": "Discussion: /Volumes/Data/project/Antigravity/ConnectAI 이 프로젝트를 작업할거야.", + "imports": [] + }, + "docs/records/ConnectAI/planning/2026-05-02_project-chronicle-guard.md": { + "mtimeMs": 1777808065000, + "size": 2946, + "lines": 58, + "role": "Feature Plan: Project Chronicle Guard", + "imports": [] + }, + "docs/records/ConnectAI/planning/2026-05-02_second-brain-trace-mode.md": { + "mtimeMs": 1777808065000, + "size": 1467, + "lines": 36, + "role": "Feature Plan: Second Brain Trace Mode", + "imports": [] + }, + "docs/records/ConnectAI/planning/2026-05-13_상세하게-아키텍처-기준으로-설명해줘.md": { + "mtimeMs": 1778678870000, + "size": 1220, + "lines": 40, + "role": "Feature Plan: 상세하게 아키텍처 기준으로 설명해줘.", + "imports": [] + }, + "docs/records/ConnectAI/project-profile.md": { + "mtimeMs": 1777808208000, + "size": 957, + "lines": 30, + "role": "Project Profile", + "imports": [] + }, + "docs/records/ConnectAI/timeline.md": { + "mtimeMs": 1778678912000, + "size": 7720, + "lines": 113, + "role": "Project Timeline", + "imports": [] + }, + "docs/refactoring_roadmap.md": { + "mtimeMs": 1778171332000, + "size": 3123, + "lines": 53, + "role": "🛠️ ConnectAI 아키텍처 리팩토링 로드맵", + "imports": [] + } + } +} \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json index 995054d..62f729f 100644 --- a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json +++ b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json @@ -1,5 +1,5 @@ { "result": "Final report with inconsistencies. This should be long enough to pass validation.", - "createdAt": 1778677516269, + "createdAt": 1778679248269, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json index 5ed597a..11e8038 100644 --- a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json +++ b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json @@ -1,5 +1,5 @@ { "result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", - "createdAt": 1778677516268, + "createdAt": 1778679248269, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json index be1e10b..dea30f3 100644 --- a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json +++ b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json @@ -1,5 +1,5 @@ { "result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", - "createdAt": 1778677516268, + "createdAt": 1778679248268, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json index ebd08da..5a78f02 100644 --- a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json +++ b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json @@ -1,5 +1,5 @@ { - "result": "---\nid: stress_conflict_1778677516257\ndate: 2026-05-13T13:05:16.269Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (10ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (1ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (1ms)\n", - "createdAt": 1778677516269, + "result": "---\nid: stress_conflict_1778679248257\ndate: 2026-05-13T13:34:08.269Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (11ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (0ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (1ms)\n", + "createdAt": 1778679248270, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/missions/stress_conflict_1778677516257.json b/.astra/tests/stress/.astra/missions/stress_conflict_1778679248257.json similarity index 82% rename from .astra/tests/stress/.astra/missions/stress_conflict_1778677516257.json rename to .astra/tests/stress/.astra/missions/stress_conflict_1778679248257.json index 66f726d..3d5a2d9 100644 --- a/.astra/tests/stress/.astra/missions/stress_conflict_1778677516257.json +++ b/.astra/tests/stress/.astra/missions/stress_conflict_1778679248257.json @@ -1,8 +1,8 @@ { - "missionId": "stress_conflict_1778677516257", + "missionId": "stress_conflict_1778679248257", "status": "completed", - "startTime": "2026-05-13T13:05:16.257Z", - "totalElapsedMs": 12, + "startTime": "2026-05-13T13:34:08.257Z", + "totalElapsedMs": 13, "results": { "planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", "researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", @@ -16,30 +16,30 @@ { "from": "idle", "to": "planner", - "durationMs": 10, + "durationMs": 11, "message": "전략 수립 중...", - "ts": "2026-05-13T13:05:16.267Z" + "ts": "2026-05-13T13:34:08.268Z" }, { "from": "planner", "to": "researcher", - "durationMs": 1, + "durationMs": 0, "message": "핵심 정보 수집 및 분석 중...", - "ts": "2026-05-13T13:05:16.268Z" + "ts": "2026-05-13T13:34:08.268Z" }, { "from": "researcher", "to": "writer", "durationMs": 1, "message": "최종 리포트 작성 및 편집 중...", - "ts": "2026-05-13T13:05:16.269Z" + "ts": "2026-05-13T13:34:08.269Z" }, { "from": "writer", "to": "completed", - "durationMs": 0, + "durationMs": 1, "message": "미션 완료", - "ts": "2026-05-13T13:05:16.269Z" + "ts": "2026-05-13T13:34:08.270Z" } ], "resilienceMetrics": { diff --git a/PATCHNOTES.md b/PATCHNOTES.md index cc015ef..57484e7 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -1,5 +1,15 @@ # Astra Patch Notes +## v2.0.2 (2026-05-13) +### 🏛️ Structural Integrity & Automated Context Management +- **프로젝트 컨텍스트 자동 관리:** `.astra/project-context/architecture.md`를 통해 프로젝트 구조, 스택, 주요 모듈 정보를 자동으로 스캔하고 관리하는 기능을 도입했습니다. +- **아키텍처 시각화 엔진 강화:** `mermaid.ts` 및 `scanner.ts`를 추가하여 프로젝트 구조를 다이어그램으로 시각화하고 심층 스캐닝하는 기반을 구축했습니다. +- **의사결정 기록(ADR) 동기화:** `ADR-0009`를 포함한 최신 전략적 의사결정 사항을 프로젝트 지식 베이스에 통합했습니다. +- **사이드바 인터랙션 정교화:** 대규모 프로젝트 분석 시의 UI 안정성을 높이고 사용자 피드백 루프를 개선했습니다. +- **신규 패키징:** `astra-2.0.2.vsix` 패키지를 통해 자동화된 컨텍스트 관리와 강화된 아키텍처 분석 기능을 제공합니다. + +--- + ## v2.0.1 (2026-05-13) ### 🧠 Advanced Knowledge Mix & Architectural Intelligence - **지식 믹스(Knowledge Mix) 엔진 도입:** 에이전트가 답변 시 '세컨드 브레인' 지식과 자체 학습 지식을 사용하는 비중을 정교하게 조절할 수 있는 `knowledgeMix.ts`를 구현했습니다. diff --git a/docs/records/ConnectAI/chronicle.config.json b/docs/records/ConnectAI/chronicle.config.json index 93e4fba..623aba8 100644 --- a/docs/records/ConnectAI/chronicle.config.json +++ b/docs/records/ConnectAI/chronicle.config.json @@ -3,9 +3,9 @@ "projectName": "ConnectAI", "projectRoot": "/Volumes/Data/project/Antigravity/ConnectAI", "recordRoot": "/Volumes/Data/project/Antigravity/ConnectAI/docs/records/ConnectAI", - "description": "Auto-detected from the local project path in the conversation.", - "corePurpose": "Capture project direction, architecture discussion, decisions, and development notes as Markdown.", + "description": "Auto-created by Project Architecture activation.", + "corePurpose": "", "detailLevel": "standard", - "createdAt": "2026-05-12T15:46:21.190Z", - "updatedAt": "2026-05-12T15:46:21.191Z" + "createdAt": "2026-05-13T13:09:33.788Z", + "updatedAt": "2026-05-13T13:28:32.943Z" } diff --git a/docs/records/ConnectAI/decisions/ADR-0009-이-프로젝트의-구조에-대해서-설명해봐.md b/docs/records/ConnectAI/decisions/ADR-0009-이-프로젝트의-구조에-대해서-설명해봐.md new file mode 100644 index 0000000..63757d5 --- /dev/null +++ b/docs/records/ConnectAI/decisions/ADR-0009-이-프로젝트의-구조에-대해서-설명해봐.md @@ -0,0 +1,19 @@ +# ADR: 이 프로젝트의 구조에 대해서 설명해봐. + +## Status +accepted + +## Context +이 프로젝트의 구조에 대해서 설명해봐. + +## Decision +현재까지 확보된 정보와 기록을 바탕으로 프로젝트의 구조를 설명드리겠습니다. 이 프로젝트는 지식 수집 워크플로우를 애플리케이션 형태로 묶어 운영하려는 도구로 설계되었습니다. 핵심은 데이터 수집 흐름의 안정성, 외부 연동 실패 처리, 그리고 수집 결과의 저장 및 재처리 가능성을 확보하는 데 중점을 두고 있습니다. 구조에 대한 구체적인 세부 사항은 관련 의사결정 기록(ADR)과 문서들을 통해 확인됩니다. 가장 중요한 구조적 결정은 데이터 흐름의 안정성과 외부 연동 실패 처리 메커니즘을 어떻게 구축할지에 대한 논의였습니다. 실제 파일 시스템의 상세한 기술 구조와 구현된 아키텍처를 파악하려면 해당 경로 내의 실제 파일들과 코드를 검토해야 합니다. 어떤 부분에 대해 더 깊이 있는 분석(예: 데이터 흐름, 모듈 분리, 기술 스택)을 원하시는지 알려주시면 해당 파일들을 기반으로 상세한 구조 분석을 진행하겠습니다. + +## Reason +Captured automatically because the conversation contained decision-oriented language. + +## Alternatives +Not captured yet. + +## Consequences +- Future prompts should treat this as project context unless the user changes direction. diff --git a/docs/records/ConnectAI/development/2026-05-13_너는-분석-요청하거나-내가-작업-요청을-할때-connectai-architecture-md-문서를-참고하고-_implementation.md b/docs/records/ConnectAI/development/2026-05-13_너는-분석-요청하거나-내가-작업-요청을-할때-connectai-architecture-md-문서를-참고하고-_implementation.md new file mode 100644 index 0000000..5b1b6a2 --- /dev/null +++ b/docs/records/ConnectAI/development/2026-05-13_너는-분석-요청하거나-내가-작업-요청을-할때-connectai-architecture-md-문서를-참고하고-_implementation.md @@ -0,0 +1,22 @@ +# Development Log: 너는 분석 요청하거나 내가 작업 요청을 할때 connectai architecture.md 문서를 참고하고 작업을 하나? + +## Purpose +Record the implementation or verification outcome from the current conversation. + +## Implementation Summary +네, 그렇습니다. 저는 요청이나 작업이 있을 때, 제공된 **문맥(Context)**과 **실제 증거(Evidence)**를 최우선으로 참고하여 답변을 생성합니다. 특히 프로젝트 관련 질문에는 다음과 같은 원칙을 따릅니다. 1. **증거 기반:** 제가 접근할 수 있는 파일이나 대화 기록에 명시적으로 포함된 정보(예: 시스템 분석 계획, 설계 결정 기록 등)를 참조합니다. 2. **최신성 우선:** 사용자의 최신 요청과 현재 대화 맥락을 가장 중요하게 고려하여 답변의 방향을 잡습니다. 3. **실제 검증:** 제가 추론하거나 주장하는 모든 내용은 접근 가능한 파일이나 명시된 정보에 기반해야 하며, 증거가 부족할 경우 그 불확실성을 명확히 밝힙니다. 따라서 `connectai architecture.md`와 같은 문서는 프로젝트의 **공식적인 설계 의도**를 담고 있다면 매우 중요한 참고 자료가 되지만, 실제 구현 상태나 최신 변경 사항을 확인하기 위해서는 해당 파일이나 코드를 직접 ... + +## Architecture +Captured automatically from the assistant response and active project context. + +## Changed Files +- connectai architecture.md + +## Dependency Notes +No new dependency note was captured automatically. + +## Bugs +No bugs recorded. + +## Lessons +- Automatic project records should be generated in the background when the turn contains durable project knowledge. diff --git a/docs/records/ConnectAI/discussions/2026-05-13_volumes-data-project-antigravity-connectai-이-프로젝트를-작업할거야.md b/docs/records/ConnectAI/discussions/2026-05-13_volumes-data-project-antigravity-connectai-이-프로젝트를-작업할거야.md new file mode 100644 index 0000000..00ae742 --- /dev/null +++ b/docs/records/ConnectAI/discussions/2026-05-13_volumes-data-project-antigravity-connectai-이-프로젝트를-작업할거야.md @@ -0,0 +1,16 @@ +# Discussion: /Volumes/Data/project/Antigravity/ConnectAI 이 프로젝트를 작업할거야. + +## User Request Summary +/Volumes/Data/project/Antigravity/ConnectAI 이 프로젝트를 작업할거야. + +## Interpreted Intent +Capture a meaningful project discussion automatically instead of requiring manual record selection. + +## Questions +No explicit question was captured. + +## Main Discussion +- 네, /Volumes/Data/project/Antigravity/ConnectAI 프로젝트를 시작하겠습니다. 어떤 부분부터 작업을 시작할까요? 구체적인 목표나 첫 번째 단계(예: 설계 검토, 코드 리뷰, 기능 정의 등)를 말씀해 주시면 바로 실행하겠습니다. + +## Decisions +No decisions captured yet. diff --git a/docs/records/ConnectAI/planning/2026-05-13_상세하게-아키텍처-기준으로-설명해줘.md b/docs/records/ConnectAI/planning/2026-05-13_상세하게-아키텍처-기준으로-설명해줘.md new file mode 100644 index 0000000..b4a8125 --- /dev/null +++ b/docs/records/ConnectAI/planning/2026-05-13_상세하게-아키텍처-기준으로-설명해줘.md @@ -0,0 +1,40 @@ +# Feature Plan: 상세하게 아키텍처 기준으로 설명해줘. + +## 1. Feature Name +상세하게 아키텍처 기준으로 설명해줘. + +## 2. Reason +Capture the current planning or architecture direction before implementation continues. + +## 3. Original User Request +상세하게 아키텍처 기준으로 설명해줘. + +## 4. Interpreted User Intent +상세하게 아키텍처 기준으로 설명해줘. + +## 5. Background +상세하게 아키텍처 기준으로 설명해줘. + +## 6. Scope +- Continue from the active project conversation. +- Use the selected project record folder automatically. + +## 7. Out Of Scope +- Manual record type selection. +- Blocking the user with record-writing prompts. + +## 8. Development Direction +상세하게 아키텍처 기준으로 설명해줘. + +## 9. Dependency Strategy +Prefer existing project modules and local Markdown records. + +## 10. Expected Value +Future work can resume with the latest project intent and reasoning preserved. + +## 11. Success Criteria +- The record is saved automatically after a meaningful project turn. +- The record stays under the active project. + +## 12. Developer Instruction +Use this record as lightweight context for the next development or review pass. diff --git a/docs/records/ConnectAI/timeline.md b/docs/records/ConnectAI/timeline.md index d481c29..b299e77 100644 --- a/docs/records/ConnectAI/timeline.md +++ b/docs/records/ConnectAI/timeline.md @@ -99,3 +99,15 @@ ## 2026-05-12 - Auto development record created: development/2026-05-12_volumes-data-project-antigravity-connectai-분석하고-부족한-부분이나-개선이_implementation-4.md + +## 2026-05-13 +- Auto discussion record created: discussions/2026-05-13_volumes-data-project-antigravity-connectai-이-프로젝트를-작업할거야.md + +## 2026-05-13 +- Auto decision record created: decisions/ADR-0009-이-프로젝트의-구조에-대해서-설명해봐.md + +## 2026-05-13 +- Auto planning record created: planning/2026-05-13_상세하게-아키텍처-기준으로-설명해줘.md + +## 2026-05-13 +- Auto development record created: development/2026-05-13_너는-분석-요청하거나-내가-작업-요청을-할때-connectai-architecture-md-문서를-참고하고-_implementation.md diff --git a/media/sidebar.css b/media/sidebar.css index 78c642d..737abe0 100644 --- a/media/sidebar.css +++ b/media/sidebar.css @@ -357,24 +357,25 @@ border-color: var(--border-bright); } - /* Compact model picker placed directly below the input box. */ - .input-model-row { - display: flex; align-items: center; gap: 8px; - margin-top: 6px; padding: 4px 8px; - background: var(--surface); border: 1px solid var(--border); border-radius: 8px; + /* Inline model picker that lives in the input footer, next to the attach + button. Replaces the (now-removed) bottom model row + the separate + "Model: ..." status text — one surface, click to change. */ + .model-pill { + display: inline-flex; align-items: center; gap: 4px; + font-size: 10px; color: var(--text-dim); max-width: 220px; } - .input-model-label { - font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em; - color: var(--text-dim); flex-shrink: 0; + .model-prefix { color: var(--text-dim); flex-shrink: 0; } + .model-inline-sel { + background: transparent; border: none; outline: none; + color: var(--text-primary); font-size: 10px; + padding: 0; margin: 0; cursor: pointer; + max-width: 180px; min-width: 0; + text-overflow: ellipsis; overflow: hidden; white-space: nowrap; + font-weight: 500; } - .input-model-select-wrap { flex: 1; min-width: 0; } - .input-model-select-wrap select { - width: 100%; min-width: 0; - background: transparent; color: var(--text-primary); - border: none; outline: none; padding: 4px 6px; - font-size: 11px; cursor: pointer; - } - .input-model-select-wrap select:focus { box-shadow: 0 0 0 2px var(--accent-glow); border-radius: 4px; } + .model-inline-sel:hover { color: var(--accent); } + .model-inline-sel:focus-visible { outline: 1px dashed var(--accent); outline-offset: 2px; } + .status-label { font-size: 10px; color: var(--text-dim); } .send-btn { background: var(--accent); color: #fff; border: none; padding: 6px 14px; border-radius: 6px; diff --git a/media/sidebar.html b/media/sidebar.html index a0af71a..fc6e31c 100644 --- a/media/sidebar.html +++ b/media/sidebar.html @@ -257,11 +257,15 @@
- + -
- -
- -
-
diff --git a/media/sidebar.js b/media/sidebar.js index 33eece7..19f2ca7 100644 --- a/media/sidebar.js +++ b/media/sidebar.js @@ -660,7 +660,7 @@ break; case 'modelsList': { modelSel.innerHTML = ''; - const inlineModelSel = document.getElementById('inlineModelSel'); + const inlineModelSel = document.getElementById('modelInlineSel'); if (inlineModelSel) inlineModelSel.innerHTML = ''; // [State Persistence - Tier 2] LocalStorage에서 마지막 선택 모델 복원 시도 const _savedModel = localStorage.getItem('g1nation_last_model'); @@ -690,8 +690,10 @@ if (_savedModel && _savedModel !== msg.value.selected && msg.value.models.includes(_savedModel)) { vscode.postMessage({ type: 'model', value: _savedModel }); } - if (typeof updateInputPlaceholder === 'function') updateInputPlaceholder(); - statusLabel.innerText = `Model: ${_preferredModel}`; + // The model name is now visible inside the footer pill itself, + // so statusLabel is reserved for actual status (autoContinue + // progress, etc.). Keep it empty in steady state. + statusLabel.innerText = ''; // Refresh per-agent model dropdown options (if currently visible) so it stays in sync. if (typeof refreshAgentMapModelOptions === 'function') refreshAgentMapModelOptions(); break; @@ -820,7 +822,7 @@ // clearing the override should restore the previous selection. const pinned = msg.value && msg.value.model; if (pinned) { - const inlineSel = document.getElementById('inlineModelSel'); + const inlineSel = document.getElementById('modelInlineSel'); // Add an option if it isn't already known so the value can stick. const ensureOption = (sel) => { if (!sel) return; @@ -835,8 +837,9 @@ }; ensureOption(modelSel); ensureOption(inlineSel); - statusLabel.innerText = `Model: ${pinned} (agent override)`; - if (typeof updateInputPlaceholder === 'function') updateInputPlaceholder(); + // The pill shows the model directly; surface the override as a tooltip + // instead of a duplicate status string. + if (inlineSel) inlineSel.title = `Model pinned by current agent: ${pinned}`; } break; } @@ -1149,13 +1152,13 @@ document.getElementById('historyBtn').onclick = () => vscode.postMessage({ type: 'getSessions' }); document.getElementById('historyBtn').addEventListener('click', () => historyOverlay.classList.add('visible')); document.getElementById('closeHistoryBtn').onclick = () => historyOverlay.classList.remove('visible'); + // The input placeholder is now a constant brand label — the model name + // lives in the footer pill itself, so we don't repeat it here. const updateInputPlaceholder = () => { - if (typeof input !== 'undefined' && input) { - input.placeholder = `Ask ${modelSel ? modelSel.value : 'AI'}...`; - } + if (typeof input !== 'undefined' && input) input.placeholder = 'Ask Astra...'; }; - // Shared handler so the top-bar dropdown and the inline-below-input dropdown + // Shared handler so the header dropdown and the footer pill dropdown // always commit the same way and stay visually synced. const applyModelSelection = (selectedModel, originEl) => { if (!selectedModel) return; @@ -1168,15 +1171,12 @@ // [State Persistence - Tier 1] VS Code 전역 설정에 동기화 (영구 저장) vscode.postMessage({ type: 'model', value: selectedModel }); // Mirror the value to the *other* dropdown so both pickers reflect reality. - const inlineSel = document.getElementById('inlineModelSel'); + const inlineSel = document.getElementById('modelInlineSel'); if (originEl !== modelSel && modelSel.value !== selectedModel) modelSel.value = selectedModel; if (inlineSel && originEl !== inlineSel && inlineSel.value !== selectedModel) inlineSel.value = selectedModel; - updateInputPlaceholder(); - // 상태 레이블 즉시 업데이트 - statusLabel.innerText = `Model: ${selectedModel}`; }; modelSel.onchange = () => applyModelSelection(modelSel.value, modelSel); - const _inlineModelSelEl = document.getElementById('inlineModelSel'); + const _inlineModelSelEl = document.getElementById('modelInlineSel'); if (_inlineModelSelEl) { _inlineModelSelEl.onchange = () => applyModelSelection(_inlineModelSelEl.value, _inlineModelSelEl); } diff --git a/package.json b/package.json index 41b2a3e..ae43d92 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "astra", "displayName": "Astra", "description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.", - "version": "2.0.1", + "version": "2.0.2", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/features/projectArchitecture/index.ts b/src/features/projectArchitecture/index.ts index a9d34a5..423b56a 100644 --- a/src/features/projectArchitecture/index.ts +++ b/src/features/projectArchitecture/index.ts @@ -27,23 +27,72 @@ */ import * as fs from 'fs'; import * as path from 'path'; -import * as crypto from 'crypto'; import { logError, logInfo } from '../../utils'; +import { deepScan, DeepScanResult, ScanCache, RefreshStats } from './scanner'; +import { renderDirectoryTreeDiagram, renderModuleDependencyDiagram } from './mermaid'; /** Sub-folder under the project root where the architecture doc lives. */ const ARCH_DIR_REL = path.join('.astra', 'project-context'); const ARCH_FILE = 'architecture.md'; +const CACHE_FILE = 'scan-cache.json'; -/** Top-level directories we consider "code" worth listing under Main Modules. */ -const CODE_DIRS = ['src', 'media', 'core_py', 'lib', 'app', 'apps', 'packages', 'tests']; +/** + * Resolve the scan-cache path for a given project root. Sits next to the doc + * itself so anyone inspecting `.astra/project-context/` can see both the + * generated markdown and the per-file fingerprints that feed it. + */ +function _cachePathFor(projectRoot: string): string { + return path.join(projectRoot, ARCH_DIR_REL, CACHE_FILE); +} -/** Files at the project root worth highlighting under "Important Files". */ -const ROOT_IMPORTANT = [ - 'package.json', 'pnpm-workspace.yaml', 'tsconfig.json', - 'README.md', 'CHANGELOG.md', 'ARCHITECTURE.md', - 'pyproject.toml', 'requirements.txt', 'Cargo.toml', 'go.mod', - 'Dockerfile', 'docker-compose.yml', -]; +function _readScanCache(projectRoot: string): ScanCache | undefined { + const p = _cachePathFor(projectRoot); + if (!fs.existsSync(p)) return undefined; + try { + const parsed = JSON.parse(fs.readFileSync(p, 'utf8')); + if (parsed && parsed.version === 1 && parsed.files && typeof parsed.files === 'object') { + return parsed as ScanCache; + } + } catch (e: any) { + logError('projectArchitecture: cache read failed; starting fresh.', { + cachePath: p, error: e?.message ?? String(e), + }); + } + return undefined; +} + +function _writeScanCache(projectRoot: string, cache: ScanCache): void { + const p = _cachePathFor(projectRoot); + try { + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, JSON.stringify(cache, null, 2), 'utf8'); + } catch (e: any) { + logError('projectArchitecture: cache write failed.', { + cachePath: p, error: e?.message ?? String(e), + }); + } +} + +/** + * Normalize an absolute path for display. We don't want `/Volumes/Data/...` + * (or any other machine-specific prefix) leaking into the architecture doc or + * the prompt — the user works across multiple environments so absolute paths + * are noise at best, and outright wrong on the next machine. Anything that + * lives inside the workspace becomes workspace-relative; anything else falls + * back to just the basename. The function is exported so callers outside this + * module can apply the same policy consistently. + */ +export function toWorkspaceRelative(absPath: string, workspaceRoot?: string): string { + if (!absPath) return ''; + if (workspaceRoot) { + const wr = workspaceRoot.replace(/[\\/]+$/, ''); + const ap = absPath.replace(/\\/g, '/'); + const wrp = wr.replace(/\\/g, '/'); + if (ap === wrp) return '.'; + if (ap.startsWith(`${wrp}/`)) return ap.slice(wrp.length + 1); + } + return path.basename(absPath); +} const AUTO_START = ''; const AUTO_END = ''; @@ -74,125 +123,28 @@ export function architectureDocPathFor(projectRoot: string): string { } /** - * Scan a project root and return a structured summary. Pure, side-effect free - * (apart from reading the file system) so we can unit-test the signature/diff - * logic without writing any files. + * Backwards-compatible thin wrapper. The watcher / refresh path only needs the + * shape-signature to decide whether to re-emit the doc, so we expose `scanProject` + * with the legacy shape but delegate to the deep scanner internally. */ export function scanProject(projectRoot: string, projectName?: string): ArchitectureScanResult { - const safeRoot = projectRoot && fs.existsSync(projectRoot) ? projectRoot : ''; - const name = (projectName?.trim()) || (safeRoot ? path.basename(safeRoot) : 'Unknown Project'); - - // ── package.json ───────────────────────────────────────────────────────── - let description = ''; - let pkgJson: any = null; - const pkgPath = safeRoot ? path.join(safeRoot, 'package.json') : ''; - if (pkgPath && fs.existsSync(pkgPath)) { - try { - pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); - if (typeof pkgJson?.description === 'string') description = pkgJson.description.trim(); - } catch (e: any) { - logError('projectArchitecture: package.json parse failed.', { error: e?.message ?? String(e) }); - } - } - - // ── Runtime / framework fingerprint ───────────────────────────────────── - const runtimes: string[] = []; - if (safeRoot && fs.existsSync(path.join(safeRoot, 'tsconfig.json'))) runtimes.push('TypeScript'); - if (pkgJson) { - runtimes.push('Node.js'); - const deps = { ...(pkgJson.dependencies || {}), ...(pkgJson.devDependencies || {}) } as Record; - if (deps['@types/vscode'] || pkgJson.engines?.vscode) runtimes.push('VS Code Extension'); - if (deps['react']) runtimes.push('React'); - if (deps['next']) runtimes.push('Next.js'); - if (deps['express'] || deps['fastify']) runtimes.push('HTTP server'); - if (deps['@anthropic-ai/sdk']) runtimes.push('Anthropic SDK'); - if (deps['openai']) runtimes.push('OpenAI SDK'); - if (deps['@lmstudio/sdk']) runtimes.push('LM Studio SDK'); - } - if (safeRoot && fs.existsSync(path.join(safeRoot, 'pyproject.toml'))) runtimes.push('Python'); - if (safeRoot && fs.existsSync(path.join(safeRoot, 'Cargo.toml'))) runtimes.push('Rust'); - if (safeRoot && fs.existsSync(path.join(safeRoot, 'go.mod'))) runtimes.push('Go'); - - // ── Main modules (top-level code directories) ─────────────────────────── - const mainModules: ArchitectureScanResult['mainModules'] = []; - if (safeRoot) { - for (const candidate of CODE_DIRS) { - const dirAbs = path.join(safeRoot, candidate); - if (!_isDir(dirAbs)) continue; - const entries = _readDirSafe(dirAbs); - const fileCount = entries.filter((e) => _isFileLike(path.join(dirAbs, e))).length; - const subDirs = entries.filter((e) => _isDir(path.join(dirAbs, e))); - const desc = _describeModule(candidate, fileCount, subDirs); - mainModules.push({ dir: candidate, description: desc }); - } - } - - // ── Important files at the root ───────────────────────────────────────── - const importantFiles: string[] = []; - if (safeRoot) { - for (const f of ROOT_IMPORTANT) { - if (fs.existsSync(path.join(safeRoot, f))) importantFiles.push(f); - } - } - - // Signature: hash of the structural inputs only. We do NOT hash file - // *contents* — the goal is "did the shape of the project change" so the - // watcher doesn't re-render the doc for every keystroke in a TS file. - const signature = _hashSignature({ - name, - runtimes, - mainModules: mainModules.map((m) => `${m.dir}|${m.description}`), - importantFiles, - pkgVersion: pkgJson?.version || '', - pkgDeps: pkgJson ? Object.keys({ ...(pkgJson.dependencies || {}), ...(pkgJson.devDependencies || {}) }).sort().join(',') : '', - }); - + const deep = deepScan(projectRoot, projectName); return { - projectName: name, - projectRoot: safeRoot, - description, - runtimes, - mainModules, - importantFiles, - signature, + projectName: deep.projectName, + projectRoot: deep.projectRoot, + description: deep.description, + runtimes: deep.runtimes, + mainModules: deep.topModules.map((m) => ({ + dir: m.dir, + description: `${m.fileCount} files${m.subDirs.length > 0 + ? ` — ${m.subDirs.slice(0, 6).map((s) => s.name).join(', ')}${m.subDirs.length > 6 ? `, +${m.subDirs.length - 6} more` : ''}` + : ''}`, + })), + importantFiles: deep.entryPoints.map((e) => e.rel), + signature: deep.signature, }; } -function _describeModule(dir: string, fileCount: number, subDirs: string[]): string { - const subSummary = subDirs.length > 0 - ? ` — ${subDirs.slice(0, 6).join(', ')}${subDirs.length > 6 ? `, +${subDirs.length - 6} more` : ''}` - : ''; - const known: Record = { - src: 'Source code', - media: 'Webview assets (HTML/CSS/JS)', - core_py: 'Python utilities', - tests: 'Test suite', - lib: 'Library code', - app: 'Application entry', - apps: 'Application bundles', - packages: 'Monorepo packages', - }; - const label = known[dir] || 'Module'; - return `${label} (${fileCount} files${subSummary})`; -} - -function _isDir(p: string): boolean { - try { return fs.statSync(p).isDirectory(); } catch { return false; } -} -function _isFileLike(p: string): boolean { - try { return fs.statSync(p).isFile(); } catch { return false; } -} -function _readDirSafe(p: string): string[] { - try { - // Skip hidden + heavy noise dirs so the listing reads usefully. - return fs.readdirSync(p).filter((e) => !e.startsWith('.') && e !== 'node_modules' && e !== 'out' && e !== 'dist' && e !== '__pycache__'); - } catch { return []; } -} - -function _hashSignature(obj: unknown): string { - return crypto.createHash('sha1').update(JSON.stringify(obj)).digest('hex').slice(0, 16); -} - /** * Build or refresh the architecture doc. Idempotent: * • If the file doesn't exist: scaffold full doc with auto + user-owned blocks. @@ -203,7 +155,14 @@ export function buildOrRefreshArchitectureDoc( projectName?: string, nowIso: string = new Date().toISOString() ): BuildResult { - const scan = scanProject(projectRoot, projectName); + // Incremental scan: feed the previous per-file cache so unchanged files + // are reused instead of re-parsed. The cache lives alongside the doc and + // is rewritten at the end of every successful refresh. + const prevCache = _readScanCache(projectRoot); + const deep = deepScan(projectRoot, projectName, prevCache); + _writeScanCache(projectRoot, deep.newCache); + + const scan = scanProject(projectRoot, projectName); // shape-only wrapper for callers const docPath = architectureDocPathFor(projectRoot); const docDir = path.dirname(docPath); try { @@ -212,12 +171,16 @@ export function buildOrRefreshArchitectureDoc( logError('projectArchitecture: mkdir failed.', { docDir, error: e?.message ?? String(e) }); } - const autoBlock = _renderAutoBlock(scan, nowIso); + const autoBlock = _renderAutoBlock(deep, nowIso); if (!fs.existsSync(docPath)) { - const full = _renderFullDoc(scan, autoBlock); + const full = _renderFullDoc(deep, autoBlock); fs.writeFileSync(docPath, full, 'utf8'); - logInfo('projectArchitecture: created.', { docPath, signature: scan.signature }); + logInfo('projectArchitecture: created.', { + docPath, signature: deep.signature, files: deep.totalFiles, + newlyAnalyzed: deep.refreshStats.newlyAnalyzed, + cached: deep.refreshStats.cached, + }); return { docPath, created: true, scan }; } @@ -226,52 +189,182 @@ export function buildOrRefreshArchitectureDoc( const replaced = _replaceAutoBlock(existing, autoBlock); if (replaced !== existing) { fs.writeFileSync(docPath, replaced, 'utf8'); - logInfo('projectArchitecture: refreshed.', { docPath, signature: scan.signature }); + logInfo('projectArchitecture: refreshed.', { + docPath, signature: deep.signature, files: deep.totalFiles, + newlyAnalyzed: deep.refreshStats.newlyAnalyzed, + cached: deep.refreshStats.cached, + deleted: deep.refreshStats.deleted.length, + }); } return { docPath, created: false, scan }; } -function _renderAutoBlock(scan: ArchitectureScanResult, nowIso: string): string { - const modules = scan.mainModules.length > 0 - ? scan.mainModules.map((m) => `- \`${m.dir}/\` — ${m.description}`).join('\n') - : '_(no top-level code directories detected)_'; - const importantFiles = scan.importantFiles.length > 0 - ? scan.importantFiles.map((f) => `- \`${f}\``).join('\n') - : '_(none detected)_'; - const runtimes = scan.runtimes.length > 0 ? scan.runtimes.join(', ') : '_(unknown)_'; - return [ - AUTO_START, - '## Project Name', - scan.projectName, - '', - '## Project Root', - scan.projectRoot || '_(not set)_', - '', - '## Description', - scan.description || '_(no package.json description)_', - '', - '## Runtime / Stack', - runtimes, - '', - '## Main Modules', - modules, - '', - '## Important Files', - importantFiles, - '', - `_Last auto-scan: ${nowIso}_`, - AUTO_END, - ].join('\n'); +/** + * Render the auto-managed block. This is everything between + * `` and `` — overwritten on + * every refresh. The sections are kept compact (one line per file when + * possible) so the doc remains scannable; section headings use deterministic + * `##` levels so prompt-time truncation can prioritise correctly. + */ +function _renderAutoBlock(deep: DeepScanResult, nowIso: string): string { + const lines: string[] = [AUTO_START, '']; + + // ── Snapshot ---------------------------------------------------------------- + // Note: we deliberately do *not* emit the absolute project root here. The + // user works across multiple machines so a hardcoded macOS path is wrong + // on Linux/Windows and noisy everywhere else. The workspace name is + // sufficient — VS Code resolves the actual root at runtime. + lines.push('## Snapshot'); + lines.push(`- **Workspace**: \`${deep.projectName}\`${deep.version ? ` \`v${deep.version}\`` : ''} _(absolute path varies by environment; resolved from the active VS Code workspace)_`); + if (deep.description) lines.push(`- **Description**: ${deep.description}`); + lines.push(`- **Stack**: ${deep.runtimes.length ? deep.runtimes.join(', ') : '_(unknown)_'}`); + lines.push(`- **Stats**: ${deep.totalFiles} source files, ~${deep.totalLines.toLocaleString()} lines across ${deep.topModules.length} top-level modules.`); + lines.push(''); + + // ── Refresh stats ---------------------------------------------------------- + // Surfaces what the most recent refresh actually did — useful to confirm + // that incremental cache reuse is working as expected and to spot deletions. + const r = deep.refreshStats; + lines.push('## Last Refresh'); + lines.push(`- **Time**: ${nowIso}`); + lines.push(`- **Files newly analysed**: ${r.newlyAnalyzed}`); + lines.push(`- **Files reused from cache**: ${r.cached}`); + if (r.deleted.length > 0) { + const shown = r.deleted.slice(0, 10); + const more = r.deleted.length - shown.length; + lines.push(`- **Files deleted since last refresh** (${r.deleted.length}):`); + for (const d of shown) lines.push(` - \`${d}\``); + if (more > 0) lines.push(` - _…and ${more} more_`); + } + lines.push(''); + + // ── Directory mindmap ------------------------------------------------------ + const treeDiagram = renderDirectoryTreeDiagram(deep); + if (treeDiagram) { + lines.push('## Directory Map'); + lines.push(treeDiagram); + lines.push(''); + } + + // ── Module dependency flowchart ------------------------------------------- + const depDiagram = renderModuleDependencyDiagram(deep); + if (depDiagram && deep.topModules.some((m) => m.dependsOn.length > 0)) { + lines.push('## Module Dependencies'); + lines.push('> Arrows: which top-level module imports from which.'); + lines.push(depDiagram); + lines.push(''); + } + + // ── Entry points ---------------------------------------------------------- + if (deep.entryPoints.length > 0) { + lines.push('## Entry Points'); + lines.push('> Files to read first when learning the codebase.'); + for (const ep of deep.entryPoints) { + lines.push(`- \`${ep.rel}\`${ep.role ? ` — ${ep.role}` : ''}`); + } + lines.push(''); + } + + // ── Hub files ------------------------------------------------------------- + if (deep.hubs.length > 0) { + lines.push('## Hub Files'); + lines.push('> Imported by many other files — touching these has wide blast radius.'); + for (const h of deep.hubs) { + lines.push(`- \`${h.rel}\` — referenced by **${h.refsIn}** files${h.role ? ` · ${h.role}` : ''}`); + } + lines.push(''); + } + + // ── Per-module detail ------------------------------------------------------ + if (deep.topModules.length > 0) { + lines.push('## Modules'); + for (const mod of deep.topModules) { + lines.push(''); + lines.push(`### \`${mod.dir}/\` — ${mod.fileCount} files, ~${mod.totalLines.toLocaleString()} lines`); + if (mod.dependsOn.length > 0) { + lines.push(`*Depends on*: ${mod.dependsOn.map((d) => `\`${d}/\``).join(', ')}`); + } + if (mod.subDirs.length > 0) { + lines.push(''); + lines.push('**Sub-directories**'); + for (const sub of mod.subDirs) { + const desc = sub.description ? ` — ${sub.description}` : ''; + lines.push(`- \`${mod.dir}/${sub.name}/\` (${sub.fileCount})${desc}`); + } + } + if (mod.files.length > 0) { + lines.push(''); + lines.push('**Key files**'); + for (const f of mod.files) { + const role = f.role ? ` — ${f.role}` : ''; + lines.push(`- \`${f.rel}\` (${f.lines} lines)${role}`); + } + } + } + lines.push(''); + } + + // ── VS Code surface -------------------------------------------------------- + if (deep.vsCode && (deep.vsCode.commands.length || deep.vsCode.configurationProperties.length || deep.vsCode.activationEvents.length)) { + lines.push('## VS Code Extension Surface'); + if (deep.vsCode.extensionId) lines.push(`- **Extension ID**: \`${deep.vsCode.extensionId}\``); + if (deep.vsCode.activationEvents.length) { + lines.push(`- **Activation events**: ${deep.vsCode.activationEvents.map((e) => `\`${e}\``).join(', ')}`); + } + if (deep.vsCode.commands.length) { + lines.push(`- **Commands** (${deep.vsCode.commands.length}):`); + for (const c of deep.vsCode.commands.slice(0, 60)) { + lines.push(` - \`${c.command}\`${c.title ? ` — ${c.title}` : ''}`); + } + if (deep.vsCode.commands.length > 60) { + lines.push(` - _…and ${deep.vsCode.commands.length - 60} more_`); + } + } + if (deep.vsCode.configurationProperties.length) { + lines.push(`- **Configuration** (${deep.vsCode.configurationProperties.length} settings):`); + for (const c of deep.vsCode.configurationProperties.slice(0, 60)) { + const def = c.default === undefined ? '' : ` _(default: \`${JSON.stringify(c.default)}\`)_`; + lines.push(` - \`${c.key}\` *(${c.type})*${def}${c.description ? ` — ${c.description}` : ''}`); + } + if (deep.vsCode.configurationProperties.length > 60) { + lines.push(` - _…and ${deep.vsCode.configurationProperties.length - 60} more_`); + } + } + lines.push(''); + } + + // ── Dependencies ----------------------------------------------------------- + if (deep.deps.total > 0) { + lines.push('## Dependencies'); + lines.push(`- **Runtime** (${deep.deps.runtime.length}): ${deep.deps.runtime.length ? deep.deps.runtime.map((d) => `\`${d}\``).join(', ') : '_(none)_'}`); + if (deep.deps.dev.length > 0) { + lines.push(`- **Dev** (${deep.deps.dev.length}): ${deep.deps.dev.map((d) => `\`${d}\``).join(', ')}`); + } + lines.push(''); + } + + // ── README excerpt --------------------------------------------------------- + if (deep.readmeExcerpt) { + lines.push('## README Excerpt'); + lines.push('> Pulled from the project root README — first ~2 KB.'); + lines.push(''); + lines.push(deep.readmeExcerpt.trim()); + lines.push(''); + } + + lines.push(`_Last auto-scan: ${nowIso} · signature \`${deep.signature}\`_`); + lines.push(AUTO_END); + return lines.join('\n'); } -function _renderFullDoc(scan: ArchitectureScanResult, autoBlock: string): string { +function _renderFullDoc(deep: DeepScanResult, autoBlock: string): string { // User-owned sections start as placeholders so first-time activation gives // the user a clear "fill these in" surface without confusing the model. return [ - `# ${scan.projectName} — Project Architecture Context`, + `# ${deep.projectName} — Project Architecture Context`, '', '> Auto-managed sections (between the AUTO markers) are rewritten by Astra on every refresh.', - '> The rest is yours — Astra never touches it once this file exists.', + '> The rest below is yours — Astra never touches it once this file exists.', '', autoBlock, '', @@ -314,7 +407,7 @@ function _replaceAutoBlock(existing: string, autoBlock: string): string { * Purpose, Main Modules, Key Workflows, Current Constraints, Known Risks, * Active Decisions — and drop the long auto-listing of files first. */ -export function readArchitectureForPrompt(docPath: string, maxChars: number = 8000): string { +export function readArchitectureForPrompt(docPath: string, maxChars: number = 16000): string { if (!docPath || !fs.existsSync(docPath)) return ''; let raw: string; try { @@ -326,21 +419,29 @@ export function readArchitectureForPrompt(docPath: string, maxChars: number = 80 if (raw.length <= maxChars) return raw; // Section-aware trim: parse `## ` headers, prioritise the high-signal - // sections, drop the rest until we fit. Important Files is the longest - // auto section so it gets dropped first. + // sections, drop the rest until we fit. The verbose listings (per-module + // file enumerations, full dependency tables, README excerpt) are the + // first to go — they're useful when present but rarely changes the model's + // structural understanding of the project. const sections = _splitSections(raw); const priority = [ + // User-owned, irreplaceable. 'Purpose', - 'Project Name', - 'Description', 'Active Decisions', 'Current Constraints', 'Known Risks', 'Key Workflows', - 'Main Modules', - 'Runtime / Stack', - 'Project Root', - 'Important Files', // drop first + // Auto-managed high-signal. + 'Snapshot', + 'Entry Points', + 'Hub Files', + 'Module Dependencies', + 'Directory Map', + 'VS Code Extension Surface', + // Auto-managed long tail (dropped first). + 'Modules', + 'Dependencies', + 'README Excerpt', ]; sections.sort((a, b) => { const ai = priority.indexOf(a.title); const bi = priority.indexOf(b.title); @@ -390,15 +491,21 @@ function _splitSections(raw: string): { title: string; body: string }[] { export function formatArchitectureContextForPrompt(opts: { projectName: string; docPath: string; + /** When provided, `Source:` is emitted as a workspace-relative path. */ + projectRoot?: string; lastUpdated?: string; maxChars?: number; }): string { - const content = readArchitectureForPrompt(opts.docPath, opts.maxChars ?? 8000); + const content = readArchitectureForPrompt(opts.docPath, opts.maxChars ?? 16000); if (!content) return ''; const stamp = opts.lastUpdated ? `\nLast updated: ${opts.lastUpdated}` : ''; + // Surface the doc location as a workspace-relative path so the same prompt + // works regardless of which machine the user is on. The doc lives at + // `.astra/project-context/architecture.md` inside the workspace by design. + const sourceDisplay = toWorkspaceRelative(opts.docPath, opts.projectRoot); return [ '[ACTIVE PROJECT ARCHITECTURE CONTEXT]', - `Source: ${opts.docPath}`, + `Source: ${sourceDisplay}`, `Project: ${opts.projectName}${stamp}`, 'Use this as authoritative ground truth about the project structure, constraints, and active decisions. Do not contradict it without flagging the conflict.', '---', diff --git a/src/features/projectArchitecture/mermaid.ts b/src/features/projectArchitecture/mermaid.ts new file mode 100644 index 0000000..1337acb --- /dev/null +++ b/src/features/projectArchitecture/mermaid.ts @@ -0,0 +1,69 @@ +/** + * Mermaid diagram renderers for the architecture doc. + * + * Two diagrams: a directory mindmap so the user can see the *shape* of the + * project at a glance, and a flowchart that shows which top-level module + * imports from which. Both are small enough to fit in a single screenful so + * they remain useful inside the LLM context window — large mermaid blobs + * confuse smaller models, so we intentionally cap edges and nodes. + */ +import { DeepScanResult } from './scanner'; + +/** + * Mindmap of the top-level directory layout. We render one branch per scanned + * module and at most 6 sub-directories per module so the picture stays + * scannable. Names that would otherwise collide with mermaid keywords are + * defensively quoted. + */ +export function renderDirectoryTreeDiagram(scan: DeepScanResult): string { + if (scan.topModules.length === 0) return ''; + const lines: string[] = ['```mermaid', 'mindmap', ` root((${_safeId(scan.projectName)}))`]; + for (const mod of scan.topModules) { + lines.push(` ${_safeId(mod.dir)}/`); + for (const sub of mod.subDirs.slice(0, 6)) { + lines.push(` ${_safeId(sub.name)}/`); + } + } + lines.push('```'); + return lines.join('\n'); +} + +/** + * Module-level dependency flowchart. Edges are aggregated across all files in + * each top-level module so the result reads as "src/agents → src/core" rather + * than per-file noise. Edges are deduped and unidirectional. + */ +export function renderModuleDependencyDiagram(scan: DeepScanResult): string { + if (scan.topModules.length === 0) return ''; + const lines: string[] = ['```mermaid', 'flowchart LR']; + // Declare nodes first so layout is stable even when a module has no deps. + for (const mod of scan.topModules) { + lines.push(` ${_id(mod.dir)}["${_label(mod.dir, mod.fileCount)}"]`); + } + // Emit edges. Skip self-edges (already implicit) and dedupe. + const seen = new Set(); + for (const mod of scan.topModules) { + for (const dep of mod.dependsOn) { + if (dep === mod.dir) continue; + const key = `${mod.dir}>${dep}`; + if (seen.has(key)) continue; + seen.add(key); + lines.push(` ${_id(mod.dir)} --> ${_id(dep)}`); + } + } + lines.push('```'); + return lines.join('\n'); +} + +function _id(dir: string): string { + return dir.replace(/[^A-Za-z0-9_]/g, '_'); +} + +function _label(dir: string, fileCount: number): string { + return `${dir}/
${fileCount} files`; +} + +function _safeId(s: string): string { + // Strip characters mermaid mindmap can interpret as syntax. + return s.replace(/[()[\]{}|]/g, '').replace(/\s+/g, '_').slice(0, 40); +} diff --git a/src/features/projectArchitecture/scanner.ts b/src/features/projectArchitecture/scanner.ts new file mode 100644 index 0000000..afc20e9 --- /dev/null +++ b/src/features/projectArchitecture/scanner.ts @@ -0,0 +1,644 @@ +/** + * Deep static analyser for the Project Architecture Context generator. + * + * Walks the project tree (skipping the usual `node_modules` / `out` / `dist` + * noise), pulls the *role* of each interesting file from its leading + * JSDoc / docstring / H1, parses imports to build a directory-level + * dependency graph, and inspects `package.json` for the VS Code extension + * surface (commands, settings, activation events). + * + * Pure-ish — only file-system reads, no shell-outs, no LLM calls. A full scan + * over a project on the order of ConnectAI (~hundreds of source files) + * finishes in the low double-digit milliseconds, so the watcher can call this + * after every debounce window without warming up a fan. + */ +import * as fs from 'fs'; +import * as path from 'path'; + +/** Top-level directories we *always* recurse into. Everything else is ignored. */ +const SCAN_ROOTS = ['src', 'media', 'tests', 'core_py', 'lib', 'app', 'apps', 'packages', 'docs']; + +/** Directory names we never descend into, no matter where they appear. */ +const SKIP_DIRS = new Set([ + 'node_modules', 'out', 'dist', 'build', '.git', '.next', '.cache', + '__pycache__', '.pytest_cache', 'coverage', '.turbo', '.vercel', + '.astra', // our own scratch dir, would be self-referential +]); + +/** Files we treat as code worth annotating. Extension drives the parser used. */ +const CODE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.md', '.json', '.html', '.css']); + +/** Per-file analyser caps. Keeps a runaway file (huge generated source) cheap. */ +const READ_BYTE_CAP = 64 * 1024; +const ROLE_MAX_LEN = 200; + +export interface FileSummary { + /** Project-root-relative path. */ + rel: string; + /** Approximate line count (full file, but cheap because we read once). */ + lines: number; + /** Short human description: leading JSDoc / docstring / first H1, trimmed. */ + role: string; + /** Internal imports (project-relative paths the file references). */ + imports: string[]; +} + +/** + * Persisted cache so refreshes only re-read files whose mtime/size actually + * changed. Lives at `.astra/project-context/scan-cache.json`; safe to delete — + * the next refresh will rebuild it from scratch. + */ +export interface ScanCache { + version: 1; + generatedAt: string; + files: Record; +} + +export interface CachedFile { + /** File modification time in milliseconds since epoch. */ + mtimeMs: number; + /** File size in bytes (cheap second-line defence against mtime collisions). */ + size: number; + /** Approximate line count from the last full read. */ + lines: number; + /** Extracted role string. */ + role: string; + /** Resolved internal imports. */ + imports: string[]; +} + +/** Summary of work done by the last `deepScan` call — surfaced in the doc footer. */ +export interface RefreshStats { + /** Files that were freshly read and parsed this run. */ + newlyAnalyzed: number; + /** Files reused from cache because mtime + size matched. */ + cached: number; + /** Project-relative paths that disappeared since the previous scan. */ + deleted: string[]; +} + +export interface ModuleSummary { + /** Top-level directory name, e.g. "src". */ + dir: string; + /** Total files (including nested). */ + fileCount: number; + /** Total line count across all code files in this module. */ + totalLines: number; + /** Most informative files: hubs (high in-degree) and entry points come first. */ + files: FileSummary[]; + /** Immediate sub-directory summaries one level deeper. */ + subDirs: { name: string; fileCount: number; description: string }[]; + /** Other top-level modules this one imports from (dir-level edges). */ + dependsOn: string[]; +} + +export interface VsCodeContribution { + commands: { command: string; title: string }[]; + configurationProperties: { key: string; type: string; default: unknown; description: string }[]; + activationEvents: string[]; + extensionId?: string; +} + +export interface DeepScanResult { + projectName: string; + projectRoot: string; + description: string; + version: string; + runtimes: string[]; + /** Root README first ~2000 chars, useful as "what is this project". */ + readmeExcerpt: string; + /** Categorised npm dependencies. */ + deps: { runtime: string[]; dev: string[]; total: number }; + /** Common-knowledge entry files we detected. */ + entryPoints: { rel: string; role: string }[]; + /** Files referenced by many others — useful for "where do I start?". */ + hubs: { rel: string; refsIn: number; role: string }[]; + topModules: ModuleSummary[]; + vsCode?: VsCodeContribution; + /** Total source files across all SCAN_ROOTS. */ + totalFiles: number; + /** Total lines of code across all source files. */ + totalLines: number; + /** Cheap structural-shape hash — used by the watcher to skip no-op refreshes. */ + signature: string; + /** Per-file cache to persist for the next incremental refresh. */ + newCache: ScanCache; + /** What this scan actually did vs. reusing cache. */ + refreshStats: RefreshStats; +} + +/** + * Run the deep scan. All paths in the result are project-relative so the doc + * stays portable across machines. + * + * When `prevCache` is provided, files whose mtime *and* size are unchanged are + * reused from the cache instead of being re-read — this is what makes the + * "Refresh" button incremental on large projects. Files missing from the cache + * are analysed fresh; files in the cache but missing from disk are reported in + * `refreshStats.deleted`. + */ +export function deepScan(projectRoot: string, projectName?: string, prevCache?: ScanCache): DeepScanResult { + const safeRoot = projectRoot && fs.existsSync(projectRoot) ? projectRoot : ''; + const stats: RefreshStats = { newlyAnalyzed: 0, cached: 0, deleted: [] }; + // Track which previously-cached files we saw this pass. Anything left over + // at the end was deleted between runs. + const prevSeen = new Set(); + const newCacheFiles: Record = {}; + + // ── package.json + manifest ------------------------------------------------- + let pkg: any = null; + if (safeRoot && fs.existsSync(path.join(safeRoot, 'package.json'))) { + try { pkg = JSON.parse(fs.readFileSync(path.join(safeRoot, 'package.json'), 'utf8')); } catch { /* ignore */ } + } + const name = (projectName?.trim()) || pkg?.displayName || pkg?.name || (safeRoot ? path.basename(safeRoot) : 'Unknown Project'); + const description = (pkg?.description || '').trim(); + const version = (pkg?.version || '').trim(); + + // ── Runtimes --------------------------------------------------------------- + const runtimes: string[] = []; + if (safeRoot && fs.existsSync(path.join(safeRoot, 'tsconfig.json'))) runtimes.push('TypeScript'); + if (pkg) { + runtimes.push('Node.js'); + const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) } as Record; + if (allDeps['@types/vscode'] || pkg.engines?.vscode) runtimes.push('VS Code Extension'); + if (allDeps['react']) runtimes.push('React'); + if (allDeps['next']) runtimes.push('Next.js'); + if (allDeps['express'] || allDeps['fastify'] || allDeps['hono']) runtimes.push('HTTP server'); + if (allDeps['@anthropic-ai/sdk']) runtimes.push('Anthropic SDK'); + if (allDeps['openai']) runtimes.push('OpenAI SDK'); + if (allDeps['@lmstudio/sdk']) runtimes.push('LM Studio SDK'); + if (allDeps['ollama']) runtimes.push('Ollama SDK'); + if (allDeps['jest'] || allDeps['vitest'] || allDeps['mocha']) runtimes.push('Test runner'); + } + if (safeRoot && fs.existsSync(path.join(safeRoot, 'pyproject.toml'))) runtimes.push('Python'); + if (safeRoot && fs.existsSync(path.join(safeRoot, 'Cargo.toml'))) runtimes.push('Rust'); + if (safeRoot && fs.existsSync(path.join(safeRoot, 'go.mod'))) runtimes.push('Go'); + + // ── README excerpt --------------------------------------------------------- + let readmeExcerpt = ''; + if (safeRoot) { + for (const candidate of ['README.md', 'README.MD', 'Readme.md', 'readme.md']) { + const p = path.join(safeRoot, candidate); + if (fs.existsSync(p)) { + try { + const raw = fs.readFileSync(p, 'utf8'); + readmeExcerpt = raw.length > 2000 ? raw.slice(0, 2000) + '\n…(truncated)' : raw; + } catch { /* ignore */ } + break; + } + } + } + + // ── Dependency listing ---------------------------------------------------- + const runtimeDeps = pkg?.dependencies ? Object.keys(pkg.dependencies).sort() : []; + const devDeps = pkg?.devDependencies ? Object.keys(pkg.devDependencies).sort() : []; + + // ── Walk the file system -------------------------------------------------- + const collected: FileSummary[] = []; + const modules: ModuleSummary[] = []; + let totalLines = 0; + + if (safeRoot) { + for (const dir of SCAN_ROOTS) { + const abs = path.join(safeRoot, dir); + if (!_isDir(abs)) continue; + const moduleFiles: FileSummary[] = []; + const subDirAgg = new Map(); + _walk(abs, safeRoot, /*depth*/ 0, moduleFiles, subDirAgg, + /*immediateSubDir*/ undefined, + /*cacheCtx*/ { prevCache, prevSeen, newCacheFiles, stats }); + const totalLinesInMod = moduleFiles.reduce((acc, f) => acc + f.lines, 0); + totalLines += totalLinesInMod; + const subDirs = Array.from(subDirAgg.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 12) + .map(([sub, count]) => ({ + name: sub, + fileCount: count, + description: _describeSubDir(sub, moduleFiles, dir), + })); + + // dependsOn: aggregate imports out of this module to other SCAN_ROOTS. + const depSet = new Set(); + for (const f of moduleFiles) { + for (const imp of f.imports) { + const head = imp.split('/')[0]; + if (SCAN_ROOTS.includes(head) && head !== dir) depSet.add(head); + } + } + + // Sort files by "interest": hub-ness (others refs) descending, + // then size. Capped so the final doc stays readable. + const topFiles = moduleFiles + .map((f) => ({ f, score: _interestScore(f, moduleFiles) })) + .sort((a, b) => b.score - a.score) + .slice(0, 25) + .map((x) => x.f); + + modules.push({ + dir, + fileCount: moduleFiles.length, + totalLines: totalLinesInMod, + files: topFiles, + subDirs, + dependsOn: Array.from(depSet).sort(), + }); + collected.push(...moduleFiles); + } + } + + // ── Hubs (files imported by many others) ---------------------------------- + const refCount = new Map(); + for (const f of collected) { + for (const imp of f.imports) { + refCount.set(imp, (refCount.get(imp) ?? 0) + 1); + } + } + // Hub keys come from `_resolveRelImport`, which strips the file extension + // (e.g. `src/utils`). Real files live with extensions (`src/utils.ts`) or + // as folder/index pairs (`src/foo/index.ts`). Try a few suffixes when + // looking up the role so the displayed hub list is annotated, not bare. + const hubLookupSuffixes = ['.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js']; + const findFileForKey = (key: string): FileSummary | undefined => + collected.find((f) => f.rel === key) + ?? hubLookupSuffixes.map((suf) => collected.find((f) => f.rel === `${key}${suf}`)).find(Boolean); + const hubs = Array.from(refCount.entries()) + .map(([rel, count]) => { + const hit = findFileForKey(rel); + return { + rel: hit?.rel ?? rel, + refsIn: count, + role: hit?.role ?? '', + }; + }) + .filter((h) => h.refsIn >= 2) + .sort((a, b) => b.refsIn - a.refsIn) + .slice(0, 8); + + // ── Entry points (well-known files) --------------------------------------- + const entryCandidates = [ + 'src/extension.ts', 'src/index.ts', 'src/main.ts', 'extension.ts', 'index.ts', 'main.ts', + 'src/app.ts', 'src/server.ts', 'media/sidebar.html', 'package.json', + ]; + const entryPoints = entryCandidates + .map((rel) => { + const summary = collected.find((f) => f.rel === rel); + if (summary) return { rel, role: summary.role }; + if (safeRoot && fs.existsSync(path.join(safeRoot, rel))) { + return { rel, role: _peekFileRole(path.join(safeRoot, rel)) }; + } + return null; + }) + .filter((x): x is { rel: string; role: string } => !!x); + + // ── VS Code manifest ------------------------------------------------------ + let vsCode: VsCodeContribution | undefined; + if (pkg?.contributes || pkg?.activationEvents) { + vsCode = { + extensionId: pkg.publisher && pkg.name ? `${pkg.publisher}.${pkg.name}` : pkg.name, + commands: Array.isArray(pkg.contributes?.commands) + ? pkg.contributes.commands.map((c: any) => ({ + command: String(c?.command ?? ''), + title: String(c?.title ?? ''), + })).filter((c: any) => c.command) + : [], + configurationProperties: ((): VsCodeContribution['configurationProperties'] => { + const props = pkg.contributes?.configuration?.properties + || (Array.isArray(pkg.contributes?.configuration) + ? Object.fromEntries(pkg.contributes.configuration.flatMap((c: any) => Object.entries(c?.properties ?? {}))) + : {}); + if (!props || typeof props !== 'object') return []; + return Object.entries(props as Record).map(([key, val]: [string, any]) => ({ + key, + type: String(val?.type ?? ''), + default: val?.default, + description: String(val?.description ?? '').slice(0, 200), + })); + })(), + activationEvents: Array.isArray(pkg.activationEvents) ? pkg.activationEvents.slice(0, 30) : [], + }; + } + + // Signature: structural shape only — count + names of top-level subdirs, + // entry point list, dep set. We deliberately don't hash file *contents* + // because doing so would trigger a regen every keystroke. + const signature = _hash({ + name, version, + runtimes, + dirs: modules.map((m) => `${m.dir}:${m.fileCount}:${m.subDirs.map((s) => s.name).join('|')}`), + deps: [...runtimeDeps, ...devDeps], + entryPoints: entryPoints.map((e) => e.rel), + vsCodeCmds: vsCode?.commands.length ?? 0, + vsCodeCfg: vsCode?.configurationProperties.length ?? 0, + }); + + // Anything that lived in the previous cache but wasn't seen this pass was + // deleted (or moved). Report it so the user sees what disappeared and the + // next refresh starts from a tidy cache. + if (prevCache) { + for (const cachedRel of Object.keys(prevCache.files)) { + if (!prevSeen.has(cachedRel)) stats.deleted.push(cachedRel); + } + } + + const newCache: ScanCache = { + version: 1, + generatedAt: new Date().toISOString(), + files: newCacheFiles, + }; + + return { + projectName: name, + projectRoot: safeRoot, + description, + version, + runtimes: Array.from(new Set(runtimes)), + readmeExcerpt, + deps: { runtime: runtimeDeps, dev: devDeps, total: runtimeDeps.length + devDeps.length }, + entryPoints, + hubs, + topModules: modules, + vsCode, + totalFiles: collected.length, + totalLines, + signature, + newCache, + refreshStats: stats, + }; +} + +// ───────────────────────────── walkers ───────────────────────────────────── + +/** + * Cache plumbing carried through the recursion. Kept as a single context + * object so `_walk`'s signature doesn't balloon every time we add a counter. + */ +interface CacheCtx { + prevCache?: ScanCache; + prevSeen: Set; + newCacheFiles: Record; + stats: RefreshStats; +} + +function _walk( + abs: string, + projectRoot: string, + depth: number, + out: FileSummary[], + subDirAgg: Map, + /** Top-level sub-directory the recursion is currently inside (e.g. "agents"). */ + immediateSubDir: string | undefined, + cacheCtx: CacheCtx, +): void { + let entries: string[]; + try { entries = fs.readdirSync(abs); } catch { return; } + for (const entry of entries) { + if (entry.startsWith('.') && entry !== '.gitignore') continue; + if (SKIP_DIRS.has(entry)) continue; + const full = path.join(abs, entry); + let stat: fs.Stats; + try { stat = fs.statSync(full); } catch { continue; } + if (stat.isDirectory()) { + if (depth === 0) { + // Initialise sub-dir counter so we have an entry even if it ends up empty. + subDirAgg.set(entry, subDirAgg.get(entry) ?? 0); + } + // Once we step into a top-level child we keep its name as the tag + // for *every* nested file beneath it, so counts include depth > 1. + const nextTag = depth === 0 ? entry : immediateSubDir; + _walk(full, projectRoot, depth + 1, out, subDirAgg, nextTag, cacheCtx); + } else if (stat.isFile()) { + const ext = path.extname(entry).toLowerCase(); + if (!CODE_EXTS.has(ext)) continue; + // Heuristic: massive auto-generated files aren't useful as + // architectural signal. Skip the body extraction but still count. + if (stat.size > 2 * 1024 * 1024) continue; + const rel = path.relative(projectRoot, full).replace(/\\/g, '/'); + + // ── Cache lookup ──────────────────────────────────────────────── + // Same mtime *and* same size = file is structurally unchanged. We + // also require both because a few filesystems (notably tarballs + // extracted with --no-preserve) leave mtimes equal but bytes + // different; the size check rules those out cheaply. + const prev = cacheCtx.prevCache?.files[rel]; + const cacheable = prev + && Math.floor(prev.mtimeMs) === Math.floor(stat.mtimeMs) + && prev.size === stat.size; + let summary: FileSummary; + if (cacheable && prev) { + summary = { rel, lines: prev.lines, role: prev.role, imports: prev.imports.slice() }; + cacheCtx.stats.cached++; + cacheCtx.newCacheFiles[rel] = prev; + } else { + summary = _analyseFile(full, rel); + cacheCtx.stats.newlyAnalyzed++; + cacheCtx.newCacheFiles[rel] = { + mtimeMs: stat.mtimeMs, + size: stat.size, + lines: summary.lines, + role: summary.role, + imports: summary.imports.slice(), + }; + } + if (prev) cacheCtx.prevSeen.add(rel); + out.push(summary); + + if (immediateSubDir) { + // Nested file inside a tracked top-level sub-dir → bump its tally. + subDirAgg.set(immediateSubDir, (subDirAgg.get(immediateSubDir) ?? 0) + 1); + } + } + } +} + +function _analyseFile(full: string, rel: string): FileSummary { + let raw = ''; + let lines = 0; + try { + const stat = fs.statSync(full); + const size = Math.min(stat.size, READ_BYTE_CAP); + const buf = Buffer.alloc(size); + const fd = fs.openSync(full, 'r'); + try { fs.readSync(fd, buf, 0, size, 0); } finally { fs.closeSync(fd); } + raw = buf.toString('utf8'); + // Approximate lines: count newlines in the read window, then extrapolate + // when we hit the cap. Within ~5% for typical source files. + const seenLines = (raw.match(/\n/g) || []).length; + lines = stat.size > READ_BYTE_CAP ? Math.round(seenLines * (stat.size / READ_BYTE_CAP)) : seenLines; + } catch { /* ignore */ } + + const role = _extractRole(rel, raw); + const imports = _extractImports(rel, raw); + return { rel, lines, role, imports }; +} + +function _peekFileRole(full: string): string { + try { + const stat = fs.statSync(full); + const size = Math.min(stat.size, 8192); + const buf = Buffer.alloc(size); + const fd = fs.openSync(full, 'r'); + try { fs.readSync(fd, buf, 0, size, 0); } finally { fs.closeSync(fd); } + return _extractRole(path.basename(full), buf.toString('utf8')); + } catch { return ''; } +} + +// ───────────────────────────── extractors ────────────────────────────────── + +/** Pull a one-sentence "what is this file" from its header. Format depends on extension. */ +function _extractRole(rel: string, raw: string): string { + const ext = path.extname(rel).toLowerCase(); + if (!raw) return ''; + + if (ext === '.md') { + // First H1, or first non-blank line. + const h1 = /^#\s+(.+)$/m.exec(raw); + if (h1) return _clean(h1[1]); + const para = raw.split(/\n\s*\n/)[0]?.replace(/^>\s+/gm, '').trim(); + if (para) return _clean(para); + return ''; + } + + if (ext === '.json') { + if (rel.endsWith('package.json')) return 'npm package manifest'; + if (rel.endsWith('tsconfig.json')) return 'TypeScript compiler config'; + if (/system_schema/.test(rel)) return 'JSON schema'; + return 'JSON configuration'; + } + + if (ext === '.html') { + const title = /]*>([^<]+)<\/title>/i.exec(raw); + if (title) return _clean(title[1]); + return 'HTML document'; + } + + if (ext === '.css') return 'Stylesheet'; + + if (ext === '.py') { + // Triple-quoted docstring or top-of-file comment block. + const doc = /^\s*(?:#!.*\n)?(?:"""|''')([\s\S]*?)(?:"""|''')/.exec(raw); + if (doc) return _clean(doc[1]); + const hash = /^(?:#[^\n!][^\n]*\n){1,3}/.exec(raw); + if (hash) return _clean(hash[0].replace(/^#\s?/gm, '')); + return ''; + } + + // TS / JS — prefer the first /** … */ block at top-of-file. + const skipBom = raw.replace(/^/, ''); + const jsdoc = /^\s*\/\*\*([\s\S]*?)\*\//.exec(skipBom); + if (jsdoc) { + const cleaned = jsdoc[1].replace(/^\s*\*\s?/gm, '').trim(); + return _clean(cleaned); + } + // Fall back to leading single-line comments. + const lineCmt = /^(?:\/\/[^\n]*\n){1,4}/.exec(skipBom); + if (lineCmt) return _clean(lineCmt[0].replace(/^\/\/\s?/gm, '').trim()); + return ''; +} + +function _clean(s: string): string { + return s + // Strip ASCII-banner decoration lines like "================" or "----" — they're + // common in this codebase but read as visual noise once collapsed onto one line. + .replace(/[=\-_*~]{4,}/g, ' ') + // Drop markdown emphasis markers. + .replace(/[`*_]+/g, '') + // Collapse runs of whitespace. + .replace(/\s+/g, ' ') + .trim() + .slice(0, ROLE_MAX_LEN); +} + +/** + * Cheap import scanner — regex-based, project-relative only. We deliberately + * skip external packages because they're already covered by the deps section + * and would just clutter the module-dependency view. + */ +function _extractImports(rel: string, raw: string): string[] { + if (!raw) return []; + const ext = path.extname(rel).toLowerCase(); + const out: string[] = []; + if (ext === '.py') { + const pyRe = /^\s*(?:from\s+(\S+)\s+import|import\s+(\S+))/gm; + let m: RegExpExecArray | null; + while ((m = pyRe.exec(raw))) { + const spec = m[1] || m[2]; + if (spec && !spec.startsWith('.')) continue; // external + const resolved = _resolveRelImport(rel, spec.replace(/\./g, '/')); + if (resolved) out.push(resolved); + } + return out; + } + // TS/JS/MJS/CJS — covers: + // import x from './y' (with the very common space between `from` and the quote) + // import './side-effect' (no `from`) + // export { a } from './b' (re-exports still create a dep edge) + // export * from './b' + // require('./y') / import('./y') + // Earlier versions missed the space after `from`, so deps came back nearly empty. + const tsRe = /(?:\bfrom\s+|\brequire\s*\(\s*|\bimport\s*\(\s*|\bimport\s+)['"`]([^'"`\n]+)['"`]/g; + let m: RegExpExecArray | null; + while ((m = tsRe.exec(raw))) { + const spec = m[1]; + if (!spec || !spec.startsWith('.')) continue; + const resolved = _resolveRelImport(rel, spec); + if (resolved) out.push(resolved); + } + return Array.from(new Set(out)); +} + +function _resolveRelImport(fromRel: string, spec: string): string | null { + if (!fromRel) return null; + const fromDir = path.posix.dirname(fromRel); + let candidate = path.posix.normalize(path.posix.join(fromDir, spec)); + if (candidate.startsWith('../')) return null; // escaped project — ignore + // Trim trailing `/index` so paths line up with how files are usually written. + candidate = candidate.replace(/\/index$/, ''); + return candidate; +} + +// ───────────────────────────── scoring & helpers ─────────────────────────── + +/** Heuristic "how worth listing" score for ordering a module's file list. */ +function _interestScore(file: FileSummary, all: FileSummary[]): number { + let score = 0; + // Hub-ness: more imports = more central. + const refsIn = all.reduce((acc, other) => + acc + (other.imports.includes(file.rel) || other.imports.some((i) => file.rel.endsWith(`${i}.ts`) || file.rel.endsWith(`${i}.tsx`)) ? 1 : 0) + , 0); + score += refsIn * 4; + // Has a role string we extracted — bonus, less guessable name. + if (file.role) score += 2; + // Bigger files usually carry more responsibility (but cap so a single + // 5000-line file doesn't dominate). + score += Math.min(10, Math.floor(file.lines / 100)); + // Known entry / index files boost. + const base = path.basename(file.rel); + if (base === 'index.ts' || base === 'index.js' || base === 'extension.ts') score += 5; + return score; +} + +function _describeSubDir(name: string, allFiles: FileSummary[], parentDir: string): string { + // Find the first role-bearing file inside this sub-dir as a proxy description. + const prefix = `${parentDir}/${name}/`; + const inside = allFiles.filter((f) => f.rel.startsWith(prefix)); + const withRole = inside.find((f) => f.role); + if (withRole) return withRole.role.slice(0, 120); + if (inside.length === 0) return ''; + // Otherwise summarise by file types. + const exts = new Set(inside.map((f) => path.extname(f.rel).toLowerCase())); + return `${inside.length} files (${Array.from(exts).join(', ')})`; +} + +function _isDir(p: string): boolean { + try { return fs.statSync(p).isDirectory(); } catch { return false; } +} + +function _hash(obj: unknown): string { + // Stable, side-effect-free hash for the scan signature. + // Same approach as crypto.createHash but inlined to avoid a require here. + const s = JSON.stringify(obj); + let h = 5381; + for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0; + return (h >>> 0).toString(16); +} diff --git a/src/sidebarProvider.ts b/src/sidebarProvider.ts index 38b276e..6f68330 100644 --- a/src/sidebarProvider.ts +++ b/src/sidebarProvider.ts @@ -1150,6 +1150,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn return formatArchitectureContextForPrompt({ projectName: p.projectName, docPath: p.architectureDocPath, + // Pass the project root so the `Source:` header in the prompt is + // workspace-relative — keeps the prompt portable across machines. + projectRoot: p.projectRoot, lastUpdated: p.architectureLastUpdated, }); }