@grafema/util 0.3.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/dist/api/GraphAPI.d.ts +87 -0
- package/dist/api/GraphAPI.d.ts.map +1 -0
- package/dist/api/GraphAPI.js +212 -0
- package/dist/api/GraphAPI.js.map +1 -0
- package/dist/api/GuaranteeAPI.d.ts +147 -0
- package/dist/api/GuaranteeAPI.d.ts.map +1 -0
- package/dist/api/GuaranteeAPI.js +290 -0
- package/dist/api/GuaranteeAPI.js.map +1 -0
- package/dist/config/ConfigLoader.d.ts +214 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -0
- package/dist/config/ConfigLoader.js +441 -0
- package/dist/config/ConfigLoader.js.map +1 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +5 -0
- package/dist/config/index.js.map +1 -0
- package/dist/core/CoverageAnalyzer.d.ts +65 -0
- package/dist/core/CoverageAnalyzer.d.ts.map +1 -0
- package/dist/core/CoverageAnalyzer.js +199 -0
- package/dist/core/CoverageAnalyzer.js.map +1 -0
- package/dist/core/FileExplainer.d.ts +101 -0
- package/dist/core/FileExplainer.d.ts.map +1 -0
- package/dist/core/FileExplainer.js +140 -0
- package/dist/core/FileExplainer.js.map +1 -0
- package/dist/core/FileOverview.d.ts +124 -0
- package/dist/core/FileOverview.d.ts.map +1 -0
- package/dist/core/FileOverview.js +279 -0
- package/dist/core/FileOverview.js.map +1 -0
- package/dist/core/GrafemaUri.d.ts +66 -0
- package/dist/core/GrafemaUri.d.ts.map +1 -0
- package/dist/core/GrafemaUri.js +191 -0
- package/dist/core/GrafemaUri.js.map +1 -0
- package/dist/core/GraphBackend.d.ts +158 -0
- package/dist/core/GraphBackend.d.ts.map +1 -0
- package/dist/core/GraphBackend.js +85 -0
- package/dist/core/GraphBackend.js.map +1 -0
- package/dist/core/GraphFreshnessChecker.d.ts +33 -0
- package/dist/core/GraphFreshnessChecker.d.ts.map +1 -0
- package/dist/core/GraphFreshnessChecker.js +104 -0
- package/dist/core/GraphFreshnessChecker.js.map +1 -0
- package/dist/core/GuaranteeManager.d.ts +254 -0
- package/dist/core/GuaranteeManager.d.ts.map +1 -0
- package/dist/core/GuaranteeManager.js +447 -0
- package/dist/core/GuaranteeManager.js.map +1 -0
- package/dist/core/HashUtils.d.ts +24 -0
- package/dist/core/HashUtils.d.ts.map +1 -0
- package/dist/core/HashUtils.js +46 -0
- package/dist/core/HashUtils.js.map +1 -0
- package/dist/core/IncrementalReanalyzer.d.ts +33 -0
- package/dist/core/IncrementalReanalyzer.d.ts.map +1 -0
- package/dist/core/IncrementalReanalyzer.js +67 -0
- package/dist/core/IncrementalReanalyzer.js.map +1 -0
- package/dist/core/ResourceRegistry.d.ts +17 -0
- package/dist/core/ResourceRegistry.d.ts.map +1 -0
- package/dist/core/ResourceRegistry.js +32 -0
- package/dist/core/ResourceRegistry.js.map +1 -0
- package/dist/core/SemanticId.d.ts +159 -0
- package/dist/core/SemanticId.d.ts.map +1 -0
- package/dist/core/SemanticId.js +291 -0
- package/dist/core/SemanticId.js.map +1 -0
- package/dist/core/VersionManager.d.ts +166 -0
- package/dist/core/VersionManager.d.ts.map +1 -0
- package/dist/core/VersionManager.js +239 -0
- package/dist/core/VersionManager.js.map +1 -0
- package/dist/core/brandNodeInternal.d.ts +14 -0
- package/dist/core/brandNodeInternal.d.ts.map +1 -0
- package/dist/core/brandNodeInternal.js +4 -0
- package/dist/core/brandNodeInternal.js.map +1 -0
- package/dist/core/nodes/GuaranteeNode.d.ts +76 -0
- package/dist/core/nodes/GuaranteeNode.d.ts.map +1 -0
- package/dist/core/nodes/GuaranteeNode.js +118 -0
- package/dist/core/nodes/GuaranteeNode.js.map +1 -0
- package/dist/core/nodes/IssueNode.d.ts +73 -0
- package/dist/core/nodes/IssueNode.d.ts.map +1 -0
- package/dist/core/nodes/IssueNode.js +130 -0
- package/dist/core/nodes/IssueNode.js.map +1 -0
- package/dist/core/nodes/NodeKind.d.ts +104 -0
- package/dist/core/nodes/NodeKind.d.ts.map +1 -0
- package/dist/core/nodes/NodeKind.js +166 -0
- package/dist/core/nodes/NodeKind.js.map +1 -0
- package/dist/diagnostics/DiagnosticCollector.d.ts +103 -0
- package/dist/diagnostics/DiagnosticCollector.d.ts.map +1 -0
- package/dist/diagnostics/DiagnosticCollector.js +133 -0
- package/dist/diagnostics/DiagnosticCollector.js.map +1 -0
- package/dist/diagnostics/DiagnosticReporter.d.ts +122 -0
- package/dist/diagnostics/DiagnosticReporter.d.ts.map +1 -0
- package/dist/diagnostics/DiagnosticReporter.js +300 -0
- package/dist/diagnostics/DiagnosticReporter.js.map +1 -0
- package/dist/diagnostics/DiagnosticWriter.d.ts +31 -0
- package/dist/diagnostics/DiagnosticWriter.d.ts.map +1 -0
- package/dist/diagnostics/DiagnosticWriter.js +44 -0
- package/dist/diagnostics/DiagnosticWriter.js.map +1 -0
- package/dist/diagnostics/categories.d.ts +57 -0
- package/dist/diagnostics/categories.d.ts.map +1 -0
- package/dist/diagnostics/categories.js +71 -0
- package/dist/diagnostics/categories.js.map +1 -0
- package/dist/diagnostics/index.d.ts +17 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +15 -0
- package/dist/diagnostics/index.js.map +1 -0
- package/dist/errors/GrafemaError.d.ts +200 -0
- package/dist/errors/GrafemaError.d.ts.map +1 -0
- package/dist/errors/GrafemaError.js +209 -0
- package/dist/errors/GrafemaError.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/instructions/index.d.ts +8 -0
- package/dist/instructions/index.d.ts.map +1 -0
- package/dist/instructions/index.js +20 -0
- package/dist/instructions/index.js.map +1 -0
- package/dist/instructions/onboarding.md +133 -0
- package/dist/knowledge/KnowledgeBase.d.ts +113 -0
- package/dist/knowledge/KnowledgeBase.d.ts.map +1 -0
- package/dist/knowledge/KnowledgeBase.js +420 -0
- package/dist/knowledge/KnowledgeBase.js.map +1 -0
- package/dist/knowledge/SemanticAddressResolver.d.ts +59 -0
- package/dist/knowledge/SemanticAddressResolver.d.ts.map +1 -0
- package/dist/knowledge/SemanticAddressResolver.js +160 -0
- package/dist/knowledge/SemanticAddressResolver.js.map +1 -0
- package/dist/knowledge/git-ingest.d.ts +58 -0
- package/dist/knowledge/git-ingest.d.ts.map +1 -0
- package/dist/knowledge/git-ingest.js +301 -0
- package/dist/knowledge/git-ingest.js.map +1 -0
- package/dist/knowledge/git-queries.d.ts +86 -0
- package/dist/knowledge/git-queries.d.ts.map +1 -0
- package/dist/knowledge/git-queries.js +177 -0
- package/dist/knowledge/git-queries.js.map +1 -0
- package/dist/knowledge/index.d.ts +14 -0
- package/dist/knowledge/index.d.ts.map +1 -0
- package/dist/knowledge/index.js +10 -0
- package/dist/knowledge/index.js.map +1 -0
- package/dist/knowledge/parser.d.ts +39 -0
- package/dist/knowledge/parser.d.ts.map +1 -0
- package/dist/knowledge/parser.js +292 -0
- package/dist/knowledge/parser.js.map +1 -0
- package/dist/knowledge/types.d.ts +133 -0
- package/dist/knowledge/types.d.ts.map +1 -0
- package/dist/knowledge/types.js +8 -0
- package/dist/knowledge/types.js.map +1 -0
- package/dist/logging/Logger.d.ts +98 -0
- package/dist/logging/Logger.d.ts.map +1 -0
- package/dist/logging/Logger.js +274 -0
- package/dist/logging/Logger.js.map +1 -0
- package/dist/notation/archetypes.d.ts +36 -0
- package/dist/notation/archetypes.d.ts.map +1 -0
- package/dist/notation/archetypes.js +173 -0
- package/dist/notation/archetypes.js.map +1 -0
- package/dist/notation/fold.d.ts +25 -0
- package/dist/notation/fold.d.ts.map +1 -0
- package/dist/notation/fold.js +598 -0
- package/dist/notation/fold.js.map +1 -0
- package/dist/notation/index.d.ts +18 -0
- package/dist/notation/index.d.ts.map +1 -0
- package/dist/notation/index.js +16 -0
- package/dist/notation/index.js.map +1 -0
- package/dist/notation/lodExtractor.d.ts +32 -0
- package/dist/notation/lodExtractor.d.ts.map +1 -0
- package/dist/notation/lodExtractor.js +149 -0
- package/dist/notation/lodExtractor.js.map +1 -0
- package/dist/notation/nameShortener.d.ts +22 -0
- package/dist/notation/nameShortener.d.ts.map +1 -0
- package/dist/notation/nameShortener.js +24 -0
- package/dist/notation/nameShortener.js.map +1 -0
- package/dist/notation/perspectives.d.ts +11 -0
- package/dist/notation/perspectives.d.ts.map +1 -0
- package/dist/notation/perspectives.js +16 -0
- package/dist/notation/perspectives.js.map +1 -0
- package/dist/notation/renderer.d.ts +31 -0
- package/dist/notation/renderer.d.ts.map +1 -0
- package/dist/notation/renderer.js +315 -0
- package/dist/notation/renderer.js.map +1 -0
- package/dist/notation/traceRenderer.d.ts +39 -0
- package/dist/notation/traceRenderer.d.ts.map +1 -0
- package/dist/notation/traceRenderer.js +358 -0
- package/dist/notation/traceRenderer.js.map +1 -0
- package/dist/notation/types.d.ts +66 -0
- package/dist/notation/types.d.ts.map +1 -0
- package/dist/notation/types.js +10 -0
- package/dist/notation/types.js.map +1 -0
- package/dist/queries/NodeContext.d.ts +81 -0
- package/dist/queries/NodeContext.d.ts.map +1 -0
- package/dist/queries/NodeContext.js +196 -0
- package/dist/queries/NodeContext.js.map +1 -0
- package/dist/queries/findCallsInFunction.d.ts +62 -0
- package/dist/queries/findCallsInFunction.d.ts.map +1 -0
- package/dist/queries/findCallsInFunction.js +169 -0
- package/dist/queries/findCallsInFunction.js.map +1 -0
- package/dist/queries/findContainingFunction.d.ts +57 -0
- package/dist/queries/findContainingFunction.d.ts.map +1 -0
- package/dist/queries/findContainingFunction.js +91 -0
- package/dist/queries/findContainingFunction.js.map +1 -0
- package/dist/queries/index.d.ts +18 -0
- package/dist/queries/index.d.ts.map +1 -0
- package/dist/queries/index.js +14 -0
- package/dist/queries/index.js.map +1 -0
- package/dist/queries/traceDataflow.d.ts +65 -0
- package/dist/queries/traceDataflow.d.ts.map +1 -0
- package/dist/queries/traceDataflow.js +754 -0
- package/dist/queries/traceDataflow.js.map +1 -0
- package/dist/queries/traceValues.d.ts +70 -0
- package/dist/queries/traceValues.d.ts.map +1 -0
- package/dist/queries/traceValues.js +373 -0
- package/dist/queries/traceValues.js.map +1 -0
- package/dist/queries/types.d.ts +166 -0
- package/dist/queries/types.d.ts.map +1 -0
- package/dist/queries/types.js +10 -0
- package/dist/queries/types.js.map +1 -0
- package/dist/schema/GraphSchemaExtractor.d.ts +53 -0
- package/dist/schema/GraphSchemaExtractor.d.ts.map +1 -0
- package/dist/schema/GraphSchemaExtractor.js +125 -0
- package/dist/schema/GraphSchemaExtractor.js.map +1 -0
- package/dist/schema/InterfaceSchemaExtractor.d.ts +73 -0
- package/dist/schema/InterfaceSchemaExtractor.d.ts.map +1 -0
- package/dist/schema/InterfaceSchemaExtractor.js +113 -0
- package/dist/schema/InterfaceSchemaExtractor.js.map +1 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +3 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/storage/backends/RFDBServerBackend.d.ts +356 -0
- package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -0
- package/dist/storage/backends/RFDBServerBackend.js +748 -0
- package/dist/storage/backends/RFDBServerBackend.js.map +1 -0
- package/dist/storage/backends/typeValidation.d.ts +47 -0
- package/dist/storage/backends/typeValidation.d.ts.map +1 -0
- package/dist/storage/backends/typeValidation.js +141 -0
- package/dist/storage/backends/typeValidation.js.map +1 -0
- package/dist/utils/findRfdbBinary.d.ts +67 -0
- package/dist/utils/findRfdbBinary.d.ts.map +1 -0
- package/dist/utils/findRfdbBinary.js +261 -0
- package/dist/utils/findRfdbBinary.js.map +1 -0
- package/dist/utils/lazyDownload.d.ts +43 -0
- package/dist/utils/lazyDownload.d.ts.map +1 -0
- package/dist/utils/lazyDownload.js +175 -0
- package/dist/utils/lazyDownload.js.map +1 -0
- package/dist/utils/moduleResolution.d.ts +134 -0
- package/dist/utils/moduleResolution.d.ts.map +1 -0
- package/dist/utils/moduleResolution.js +189 -0
- package/dist/utils/moduleResolution.js.map +1 -0
- package/dist/utils/resolveNodeFile.d.ts +13 -0
- package/dist/utils/resolveNodeFile.d.ts.map +1 -0
- package/dist/utils/resolveNodeFile.js +18 -0
- package/dist/utils/resolveNodeFile.js.map +1 -0
- package/dist/utils/startRfdbServer.d.ts +63 -0
- package/dist/utils/startRfdbServer.d.ts.map +1 -0
- package/dist/utils/startRfdbServer.js +142 -0
- package/dist/utils/startRfdbServer.js.map +1 -0
- package/dist/validation/PathValidator.d.ts +80 -0
- package/dist/validation/PathValidator.d.ts.map +1 -0
- package/dist/validation/PathValidator.js +252 -0
- package/dist/validation/PathValidator.js.map +1 -0
- package/dist/version.d.ts +11 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +26 -0
- package/dist/version.js.map +1 -0
- package/package.json +50 -0
- package/src/api/GraphAPI.ts +307 -0
- package/src/api/GuaranteeAPI.ts +402 -0
- package/src/config/ConfigLoader.ts +653 -0
- package/src/config/index.ts +13 -0
- package/src/core/CoverageAnalyzer.ts +243 -0
- package/src/core/FileExplainer.ts +179 -0
- package/src/core/FileOverview.ts +397 -0
- package/src/core/GrafemaUri.ts +216 -0
- package/src/core/GraphBackend.ts +266 -0
- package/src/core/GraphFreshnessChecker.ts +145 -0
- package/src/core/GuaranteeManager.ts +684 -0
- package/src/core/HashUtils.ts +48 -0
- package/src/core/IncrementalReanalyzer.ts +106 -0
- package/src/core/ResourceRegistry.ts +39 -0
- package/src/core/SemanticId.ts +423 -0
- package/src/core/VersionManager.ts +405 -0
- package/src/core/brandNodeInternal.ts +16 -0
- package/src/core/nodes/GuaranteeNode.ts +162 -0
- package/src/core/nodes/IssueNode.ts +177 -0
- package/src/core/nodes/NodeKind.ts +192 -0
- package/src/diagnostics/DiagnosticCollector.ts +170 -0
- package/src/diagnostics/DiagnosticReporter.ts +395 -0
- package/src/diagnostics/DiagnosticWriter.ts +50 -0
- package/src/diagnostics/categories.ts +104 -0
- package/src/diagnostics/index.ts +30 -0
- package/src/errors/GrafemaError.ts +297 -0
- package/src/index.ts +261 -0
- package/src/instructions/index.ts +21 -0
- package/src/instructions/onboarding.md +133 -0
- package/src/knowledge/KnowledgeBase.ts +486 -0
- package/src/knowledge/SemanticAddressResolver.ts +191 -0
- package/src/knowledge/git-ingest.ts +402 -0
- package/src/knowledge/git-queries.ts +269 -0
- package/src/knowledge/index.ts +29 -0
- package/src/knowledge/parser.ts +294 -0
- package/src/knowledge/types.ts +146 -0
- package/src/logging/Logger.ts +303 -0
- package/src/notation/archetypes.ts +189 -0
- package/src/notation/fold.ts +696 -0
- package/src/notation/index.ts +27 -0
- package/src/notation/lodExtractor.ts +177 -0
- package/src/notation/nameShortener.ts +24 -0
- package/src/notation/perspectives.ts +18 -0
- package/src/notation/renderer.ts +394 -0
- package/src/notation/traceRenderer.ts +458 -0
- package/src/notation/types.ts +79 -0
- package/src/queries/NodeContext.ts +280 -0
- package/src/queries/findCallsInFunction.ts +249 -0
- package/src/queries/findContainingFunction.ts +124 -0
- package/src/queries/index.ts +44 -0
- package/src/queries/traceDataflow.ts +838 -0
- package/src/queries/traceValues.ts +531 -0
- package/src/queries/types.ts +191 -0
- package/src/schema/GraphSchemaExtractor.ts +177 -0
- package/src/schema/InterfaceSchemaExtractor.ts +173 -0
- package/src/schema/index.ts +5 -0
- package/src/storage/backends/RFDBServerBackend.ts +895 -0
- package/src/storage/backends/typeValidation.ts +154 -0
- package/src/utils/findRfdbBinary.ts +288 -0
- package/src/utils/lazyDownload.ts +206 -0
- package/src/utils/moduleResolution.ts +271 -0
- package/src/utils/resolveNodeFile.ts +18 -0
- package/src/utils/startRfdbServer.ts +197 -0
- package/src/validation/PathValidator.ts +334 -0
- package/src/version.ts +28 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Grafema Project Onboarding
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
Study the target project and create a `.grafema/config.yaml` that correctly
|
|
5
|
+
describes its services, entry points, and analysis configuration.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
- The project directory exists and contains source code
|
|
9
|
+
- Node.js 18+ installed
|
|
10
|
+
- Run `grafema init` to create `.grafema/` directory if it doesn't exist
|
|
11
|
+
|
|
12
|
+
## Step 1: Initial Reconnaissance
|
|
13
|
+
|
|
14
|
+
Use `read_project_structure` to get the directory tree.
|
|
15
|
+
|
|
16
|
+
Look for:
|
|
17
|
+
- `package.json` in root and subdirectories (indicates JS/TS packages)
|
|
18
|
+
- `pnpm-workspace.yaml`, `lerna.json`, root `package.json` with `workspaces`
|
|
19
|
+
field (workspace/monorepo indicators)
|
|
20
|
+
- `tsconfig.json` files (TypeScript project)
|
|
21
|
+
- `Dockerfile`, `docker-compose.yml`, `k8s/`, `apps.json` (deployment configs
|
|
22
|
+
that may reveal service boundaries)
|
|
23
|
+
- Directories named `apps/`, `packages/`, `services/`, `pkg/`, `modules/`
|
|
24
|
+
(common monorepo structures)
|
|
25
|
+
|
|
26
|
+
## Step 2: Identify Services
|
|
27
|
+
|
|
28
|
+
A "service" in Grafema is an independently analyzable unit of code with its
|
|
29
|
+
own entry point. Typically:
|
|
30
|
+
- A standalone application (API server, web app, CLI tool)
|
|
31
|
+
- A package in a monorepo that other packages depend on
|
|
32
|
+
- A microservice in a deployment configuration
|
|
33
|
+
|
|
34
|
+
For each potential service, determine:
|
|
35
|
+
1. **Name** — human-readable identifier (e.g., "backend", "dashboard")
|
|
36
|
+
2. **Path** — directory path relative to project root
|
|
37
|
+
3. **Entry point** — the main source file (prefer TypeScript source over
|
|
38
|
+
compiled output)
|
|
39
|
+
|
|
40
|
+
### How to find entry points
|
|
41
|
+
Check in order:
|
|
42
|
+
1. `package.json` "source" field (TypeScript source entry)
|
|
43
|
+
2. `package.json` "main" field, but look for `.ts` equivalent in `src/`
|
|
44
|
+
3. Common patterns: `src/index.ts`, `src/main.ts`, `src/app.ts`,
|
|
45
|
+
`src/server.ts`, `index.ts`
|
|
46
|
+
4. For React apps: `src/App.tsx`, `src/index.tsx`
|
|
47
|
+
5. Check `bin` field in package.json for CLI tools
|
|
48
|
+
|
|
49
|
+
### When to ask the user
|
|
50
|
+
- "I found [X] that looks like [description]. Should I include it as a
|
|
51
|
+
service?"
|
|
52
|
+
- "This directory has multiple potential entry points: [list].
|
|
53
|
+
Which should I use?"
|
|
54
|
+
- "I found deployment configuration mentioning services not visible in
|
|
55
|
+
the code structure. Should I investigate?"
|
|
56
|
+
|
|
57
|
+
## Step 3: Run Auto-Discovery (Optional)
|
|
58
|
+
|
|
59
|
+
Use `discover_services` to see what Grafema's built-in detection finds.
|
|
60
|
+
Compare with your own findings from Steps 1-2. Note discrepancies —
|
|
61
|
+
auto-discovery may miss services or misidentify entry points.
|
|
62
|
+
|
|
63
|
+
## Step 4: Configure File Patterns
|
|
64
|
+
|
|
65
|
+
The Rust orchestrator has a built-in analysis pipeline — no plugin
|
|
66
|
+
configuration needed. Focus on:
|
|
67
|
+
|
|
68
|
+
- **`include`** — glob patterns for source files to analyze
|
|
69
|
+
(e.g., `src/**/*.{ts,tsx,js,jsx}`)
|
|
70
|
+
- **`exclude`** — patterns to skip (tests, generated code, dist)
|
|
71
|
+
- **`services`** — for monorepos, explicit service definitions
|
|
72
|
+
|
|
73
|
+
The minimal config generated by `grafema init` works for most projects.
|
|
74
|
+
|
|
75
|
+
## Step 5: Write Configuration
|
|
76
|
+
|
|
77
|
+
Edit `.grafema/config.yaml` directly. Minimal format:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
version: "0.3"
|
|
81
|
+
root: ".."
|
|
82
|
+
include:
|
|
83
|
+
- "src/**/*.{ts,tsx,js,jsx}"
|
|
84
|
+
exclude:
|
|
85
|
+
- "**/*.test.*"
|
|
86
|
+
- "**/node_modules/**"
|
|
87
|
+
- "**/dist/**"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For monorepos, add services:
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
services:
|
|
94
|
+
- name: "api"
|
|
95
|
+
path: "packages/api"
|
|
96
|
+
entryPoint: "src/index.ts"
|
|
97
|
+
- name: "web"
|
|
98
|
+
path: "packages/web"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Note: `root` is relative to `.grafema/` directory, so `".."` points to
|
|
102
|
+
the project root.
|
|
103
|
+
|
|
104
|
+
## Step 6: Verify
|
|
105
|
+
|
|
106
|
+
Run `analyze_project` to build the graph. Then check:
|
|
107
|
+
- `get_stats` — are node/edge counts reasonable for the project size?
|
|
108
|
+
- `get_coverage` — are the expected files analyzed?
|
|
109
|
+
|
|
110
|
+
If coverage is low or results are unexpected, iterate:
|
|
111
|
+
revisit services, entry points, or include/exclude patterns.
|
|
112
|
+
|
|
113
|
+
## Common Patterns
|
|
114
|
+
|
|
115
|
+
### Monorepo with workspaces
|
|
116
|
+
Look for `pnpm-workspace.yaml` or `workspaces` in root `package.json`.
|
|
117
|
+
Each workspace package is typically a service. Use `read_project_structure`
|
|
118
|
+
with depth=2 to see the package layout.
|
|
119
|
+
|
|
120
|
+
### Legacy project with multiple entry points
|
|
121
|
+
Look for `scripts` in `package.json`, `bin` field, or multiple files
|
|
122
|
+
in `src/` that look like entry points (contain `app.listen()`,
|
|
123
|
+
`createServer()`, `express()`).
|
|
124
|
+
|
|
125
|
+
### Microservices with shared deployment
|
|
126
|
+
Look for `docker-compose.yml`, Kubernetes configs, or similar
|
|
127
|
+
deployment manifests that list services. Cross-reference with
|
|
128
|
+
code directories.
|
|
129
|
+
|
|
130
|
+
### Single-package project
|
|
131
|
+
If there is only one `package.json` at the root and no monorepo
|
|
132
|
+
structure, the project is likely a single service. The service path
|
|
133
|
+
is `.` (root), and the entry point is determined from `package.json`.
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KnowledgeBase — in-memory index over git-tracked knowledge files.
|
|
3
|
+
*
|
|
4
|
+
* Scans knowledge/ directory, parses markdown files with YAML frontmatter,
|
|
5
|
+
* builds an in-memory index for fast lookups. All mutations also write to disk.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { readdirSync, statSync, readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
10
|
+
import { parseFrontmatter, parseKBNode, serializeKBNode, parseEdgesFile, appendEdge as appendEdgeToFile, parseYamlArrayFile } from './parser.js';
|
|
11
|
+
import type { KBNode, KBNodeType, KBDecision, KBFact, KBEdge, KBStats, KBQueryFilter, KBLifecycle, ResolvedAddress, DanglingCodeRef } from './types.js';
|
|
12
|
+
import { SemanticAddressResolver } from './SemanticAddressResolver.js';
|
|
13
|
+
import type { ResolverBackend } from './SemanticAddressResolver.js';
|
|
14
|
+
|
|
15
|
+
/** Pluralized directory names for each node type */
|
|
16
|
+
const TYPE_DIR: Record<string, string> = {
|
|
17
|
+
DECISION: 'decisions',
|
|
18
|
+
FACT: 'facts',
|
|
19
|
+
SESSION: 'sessions',
|
|
20
|
+
COMMIT: 'commits',
|
|
21
|
+
FILE_CHANGE: 'file-changes',
|
|
22
|
+
AUTHOR: 'authors',
|
|
23
|
+
TICKET: 'tickets',
|
|
24
|
+
INCIDENT: 'incidents',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class KnowledgeBase {
|
|
28
|
+
private knowledgeDir: string;
|
|
29
|
+
private nodes: Map<string, KBNode> = new Map();
|
|
30
|
+
private edges: KBEdge[] = [];
|
|
31
|
+
private loaded = false;
|
|
32
|
+
private resolver: SemanticAddressResolver | null = null;
|
|
33
|
+
|
|
34
|
+
constructor(knowledgeDir: string) {
|
|
35
|
+
this.knowledgeDir = knowledgeDir;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Wire up a code graph backend for resolving semantic addresses.
|
|
40
|
+
* Creates a SemanticAddressResolver that lazily resolves `relates_to`
|
|
41
|
+
* and `applies_to` addresses to current code node IDs.
|
|
42
|
+
*/
|
|
43
|
+
setBackend(backend: ResolverBackend): void {
|
|
44
|
+
this.resolver = new SemanticAddressResolver(backend);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Bump the resolver's generation counter, marking all cached resolutions stale.
|
|
49
|
+
* Call after re-analysis so next resolve() re-queries the code graph.
|
|
50
|
+
*/
|
|
51
|
+
invalidateResolutionCache(): void {
|
|
52
|
+
this.resolver?.bumpGeneration();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve all code addresses in a node's relates_to (and applies_to for decisions).
|
|
57
|
+
* KB-internal addresses (kb:...) pass through without backend query.
|
|
58
|
+
* Returns empty array if no resolver is set.
|
|
59
|
+
*/
|
|
60
|
+
async resolveReferences(node: KBNode): Promise<ResolvedAddress[]> {
|
|
61
|
+
if (!this.resolver) return [];
|
|
62
|
+
|
|
63
|
+
const addresses: string[] = [];
|
|
64
|
+
if (node.relates_to) addresses.push(...node.relates_to);
|
|
65
|
+
if (node.type === 'DECISION') {
|
|
66
|
+
const d = node as KBDecision;
|
|
67
|
+
if (d.applies_to) addresses.push(...d.applies_to);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Filter to code addresses only (not kb: internal refs)
|
|
71
|
+
const codeAddresses = addresses.filter(a => !a.startsWith('kb:'));
|
|
72
|
+
if (codeAddresses.length === 0) return [];
|
|
73
|
+
|
|
74
|
+
return this.resolver.resolveAll(codeAddresses);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Find all KB nodes with code addresses that don't resolve to graph nodes.
|
|
79
|
+
* Returns pairs of (KB node ID, dangling address).
|
|
80
|
+
*/
|
|
81
|
+
async getDanglingCodeRefs(): Promise<DanglingCodeRef[]> {
|
|
82
|
+
if (!this.resolver) return [];
|
|
83
|
+
|
|
84
|
+
const results: DanglingCodeRef[] = [];
|
|
85
|
+
|
|
86
|
+
for (const node of this.nodes.values()) {
|
|
87
|
+
const resolved = await this.resolveReferences(node);
|
|
88
|
+
for (const r of resolved) {
|
|
89
|
+
if (r.status === 'dangling') {
|
|
90
|
+
results.push({ nodeId: node.id, address: r.address });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return results;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Scan knowledge directory recursively, parse all .md files, build index.
|
|
100
|
+
* Malformed files are skipped with console.warn, don't crash.
|
|
101
|
+
* Missing directory succeeds with empty index.
|
|
102
|
+
*/
|
|
103
|
+
async load(): Promise<void> {
|
|
104
|
+
this.nodes.clear();
|
|
105
|
+
this.edges = [];
|
|
106
|
+
|
|
107
|
+
if (!existsSync(this.knowledgeDir)) {
|
|
108
|
+
this.loaded = true;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Scan for .md files recursively
|
|
113
|
+
const mdFiles = this.scanFiles(this.knowledgeDir, '.md');
|
|
114
|
+
|
|
115
|
+
for (const filePath of mdFiles) {
|
|
116
|
+
try {
|
|
117
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
118
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
119
|
+
const node = parseKBNode(frontmatter, body, filePath);
|
|
120
|
+
|
|
121
|
+
if (this.nodes.has(node.id)) {
|
|
122
|
+
const existing = this.nodes.get(node.id)!;
|
|
123
|
+
throw new Error(
|
|
124
|
+
`ID collision: "${node.id}" exists in both ${existing.filePath} and ${filePath}`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.nodes.set(node.id, node);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// ID collisions should propagate — they're data integrity errors
|
|
131
|
+
if (error instanceof Error && error.message.startsWith('ID collision:')) {
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
console.warn(`[KnowledgeBase] Skipping malformed file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Scan for .yaml array files (derived data like commits, authors)
|
|
139
|
+
const yamlFiles = this.scanFiles(this.knowledgeDir, '.yaml');
|
|
140
|
+
for (const filePath of yamlFiles) {
|
|
141
|
+
// Skip edges.yaml and meta.yaml — they have special formats
|
|
142
|
+
const fileName = filePath.split('/').pop() || '';
|
|
143
|
+
if (fileName === 'edges.yaml' || fileName === 'meta.yaml') continue;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const nodes = parseYamlArrayFile(filePath);
|
|
147
|
+
for (const node of nodes) {
|
|
148
|
+
if (this.nodes.has(node.id)) {
|
|
149
|
+
// For derived data, silently skip duplicates (can happen with incremental ingest)
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
this.nodes.set(node.id, node);
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.warn(`[KnowledgeBase] Skipping malformed YAML file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Load edges
|
|
160
|
+
const edgesPath = join(this.knowledgeDir, 'edges.yaml');
|
|
161
|
+
try {
|
|
162
|
+
this.edges = parseEdgesFile(edgesPath);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.warn(`[KnowledgeBase] Failed to parse edges.yaml: ${error instanceof Error ? error.message : String(error)}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.loaded = true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get a node by its semantic ID.
|
|
172
|
+
*/
|
|
173
|
+
getNode(id: string): KBNode | undefined {
|
|
174
|
+
return this.nodes.get(id);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Query nodes with filters. All filters are AND-combined.
|
|
179
|
+
* When include_dangling_only is true, only returns nodes with dangling code refs.
|
|
180
|
+
* Note: include_dangling_only requires a resolver backend; without one it returns empty.
|
|
181
|
+
*/
|
|
182
|
+
async queryNodes(filter: KBQueryFilter): Promise<KBNode[]> {
|
|
183
|
+
let results = Array.from(this.nodes.values());
|
|
184
|
+
|
|
185
|
+
if (filter.type) {
|
|
186
|
+
results = results.filter(n => n.type === filter.type);
|
|
187
|
+
}
|
|
188
|
+
if (filter.projection) {
|
|
189
|
+
results = results.filter(n => n.projections.includes(filter.projection!));
|
|
190
|
+
}
|
|
191
|
+
if (filter.text) {
|
|
192
|
+
const lower = filter.text.toLowerCase();
|
|
193
|
+
results = results.filter(n => n.content.toLowerCase().includes(lower));
|
|
194
|
+
}
|
|
195
|
+
if (filter.status) {
|
|
196
|
+
results = results.filter(n => {
|
|
197
|
+
if (n.type === 'DECISION') return (n as KBDecision).status === filter.status;
|
|
198
|
+
return false;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (filter.relates_to) {
|
|
202
|
+
results = results.filter(n => n.relates_to?.includes(filter.relates_to!));
|
|
203
|
+
}
|
|
204
|
+
if (filter.include_dangling_only) {
|
|
205
|
+
if (!this.resolver) return [];
|
|
206
|
+
const danglingNodeIds = new Set<string>();
|
|
207
|
+
for (const node of results) {
|
|
208
|
+
const resolved = await this.resolveReferences(node);
|
|
209
|
+
if (resolved.some(r => r.status === 'dangling')) {
|
|
210
|
+
danglingNodeIds.add(node.id);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
results = results.filter(n => danglingNodeIds.has(n.id));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return results;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Find active decisions that apply to a given module/semantic address.
|
|
221
|
+
* Uses string includes matching on applies_to entries.
|
|
222
|
+
*/
|
|
223
|
+
async activeDecisionsFor(module: string): Promise<KBDecision[]> {
|
|
224
|
+
const decisions = await this.queryNodes({ type: 'DECISION', status: 'active' }) as KBDecision[];
|
|
225
|
+
return decisions.filter(d =>
|
|
226
|
+
d.applies_to?.some(addr => addr.includes(module) || module.includes(addr))
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Add a new node to the knowledge base.
|
|
232
|
+
* Creates the .md file on disk and updates the in-memory index.
|
|
233
|
+
*
|
|
234
|
+
* @param params - Node properties. `id` and `filePath` are auto-generated.
|
|
235
|
+
* @returns The created node.
|
|
236
|
+
*/
|
|
237
|
+
async addNode(params: {
|
|
238
|
+
type: KBNodeType;
|
|
239
|
+
content: string;
|
|
240
|
+
slug?: string;
|
|
241
|
+
subtype?: string;
|
|
242
|
+
scope?: 'global' | 'project' | 'module';
|
|
243
|
+
projections?: string[];
|
|
244
|
+
source?: string;
|
|
245
|
+
relates_to?: string[];
|
|
246
|
+
// Decision-specific
|
|
247
|
+
status?: KBDecision['status'];
|
|
248
|
+
effective_from?: string;
|
|
249
|
+
applies_to?: string[];
|
|
250
|
+
// Fact-specific
|
|
251
|
+
confidence?: KBFact['confidence'];
|
|
252
|
+
// Session-specific
|
|
253
|
+
task_id?: string;
|
|
254
|
+
session_path?: string;
|
|
255
|
+
produced?: string[];
|
|
256
|
+
}): Promise<KBNode> {
|
|
257
|
+
const slug = params.slug || this.generateSlug(params.content);
|
|
258
|
+
const typeLower = params.type.toLowerCase();
|
|
259
|
+
const id = `kb:${typeLower}:${slug}`;
|
|
260
|
+
|
|
261
|
+
// Collision check
|
|
262
|
+
if (this.nodes.has(id)) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`Slug collision: "${id}" already exists. Did you mean to supersede? Use supersedeFact() for facts.`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const typeDir = TYPE_DIR[params.type] || typeLower + 's';
|
|
269
|
+
const dir = join(this.knowledgeDir, 'declared', typeDir);
|
|
270
|
+
if (!existsSync(dir)) {
|
|
271
|
+
mkdirSync(dir, { recursive: true });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const fileName = `${slug}.md`;
|
|
275
|
+
const filePath = join(dir, fileName);
|
|
276
|
+
|
|
277
|
+
// Secondary collision guard — file on disk
|
|
278
|
+
if (existsSync(filePath)) {
|
|
279
|
+
throw new Error(`File already exists: ${filePath}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const created = new Date().toISOString().split('T')[0];
|
|
283
|
+
|
|
284
|
+
// Build frontmatter
|
|
285
|
+
const frontmatter: Record<string, unknown> = {
|
|
286
|
+
id,
|
|
287
|
+
type: params.type,
|
|
288
|
+
created,
|
|
289
|
+
};
|
|
290
|
+
if (params.subtype) frontmatter.subtype = params.subtype;
|
|
291
|
+
if (params.scope) frontmatter.scope = params.scope;
|
|
292
|
+
if (params.projections?.length) frontmatter.projections = params.projections;
|
|
293
|
+
if (params.source) frontmatter.source = params.source;
|
|
294
|
+
if (params.relates_to?.length) frontmatter.relates_to = params.relates_to;
|
|
295
|
+
|
|
296
|
+
// Decision fields
|
|
297
|
+
if (params.type === 'DECISION') {
|
|
298
|
+
frontmatter.status = params.status || 'proposed';
|
|
299
|
+
if (params.applies_to?.length) frontmatter.applies_to = params.applies_to;
|
|
300
|
+
if (params.effective_from) frontmatter.effective_from = params.effective_from;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Fact fields
|
|
304
|
+
if (params.type === 'FACT') {
|
|
305
|
+
if (params.confidence) frontmatter.confidence = params.confidence;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Session fields
|
|
309
|
+
if (params.type === 'SESSION') {
|
|
310
|
+
if (params.task_id) frontmatter.task_id = params.task_id;
|
|
311
|
+
if (params.session_path) frontmatter.session_path = params.session_path;
|
|
312
|
+
if (params.produced?.length) frontmatter.produced = params.produced;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Parse to get a proper node, then serialize
|
|
316
|
+
const node = parseKBNode(frontmatter, params.content, filePath);
|
|
317
|
+
const markdown = serializeKBNode(node);
|
|
318
|
+
writeFileSync(filePath, markdown, 'utf-8');
|
|
319
|
+
|
|
320
|
+
// Update index
|
|
321
|
+
this.nodes.set(id, node);
|
|
322
|
+
|
|
323
|
+
// Create edges for relates_to
|
|
324
|
+
if (params.relates_to?.length) {
|
|
325
|
+
const edgesPath = join(this.knowledgeDir, 'edges.yaml');
|
|
326
|
+
for (const target of params.relates_to) {
|
|
327
|
+
const edge: KBEdge = { type: 'RELATES_TO', from: id, to: target };
|
|
328
|
+
appendEdgeToFile(edgesPath, edge);
|
|
329
|
+
this.edges.push(edge);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return node;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Supersede an existing fact with a new version.
|
|
338
|
+
* Creates new fact, updates old fact with superseded_by.
|
|
339
|
+
*/
|
|
340
|
+
async supersedeFact(
|
|
341
|
+
oldId: string,
|
|
342
|
+
newContent: string,
|
|
343
|
+
newSlug?: string,
|
|
344
|
+
): Promise<{ old: KBFact; new: KBFact }> {
|
|
345
|
+
const oldNode = this.nodes.get(oldId);
|
|
346
|
+
if (!oldNode) {
|
|
347
|
+
throw new Error(`Fact not found: ${oldId}`);
|
|
348
|
+
}
|
|
349
|
+
if (oldNode.type !== 'FACT') {
|
|
350
|
+
throw new Error(`Node "${oldId}" is type ${oldNode.type}, not FACT. Only facts can be superseded.`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const oldFact = oldNode as KBFact;
|
|
354
|
+
const slug = newSlug || this.generateSlug(newContent);
|
|
355
|
+
const newId = `kb:fact:${slug}`;
|
|
356
|
+
|
|
357
|
+
// Create new fact
|
|
358
|
+
const newFact = await this.addNode({
|
|
359
|
+
type: 'FACT',
|
|
360
|
+
content: newContent,
|
|
361
|
+
slug,
|
|
362
|
+
projections: oldFact.projections,
|
|
363
|
+
source: oldFact.source,
|
|
364
|
+
confidence: oldFact.confidence,
|
|
365
|
+
}) as KBFact;
|
|
366
|
+
|
|
367
|
+
// Update old fact: set superseded_by and rewrite file
|
|
368
|
+
oldFact.superseded_by = newId;
|
|
369
|
+
const markdown = serializeKBNode(oldFact);
|
|
370
|
+
writeFileSync(oldFact.filePath, markdown, 'utf-8');
|
|
371
|
+
|
|
372
|
+
// Add supersedes edge
|
|
373
|
+
const edgesPath = join(this.knowledgeDir, 'edges.yaml');
|
|
374
|
+
const edge: KBEdge = { type: 'SUPERSEDES', from: newId, to: oldId };
|
|
375
|
+
appendEdgeToFile(edgesPath, edge);
|
|
376
|
+
this.edges.push(edge);
|
|
377
|
+
|
|
378
|
+
return { old: oldFact, new: newFact };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Add an edge to the knowledge graph.
|
|
383
|
+
*/
|
|
384
|
+
addEdge(edge: KBEdge): void {
|
|
385
|
+
const edgesPath = join(this.knowledgeDir, 'edges.yaml');
|
|
386
|
+
appendEdgeToFile(edgesPath, edge);
|
|
387
|
+
this.edges.push(edge);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Get edges, optionally filtered by node ID (from or to).
|
|
392
|
+
*/
|
|
393
|
+
getEdges(nodeId?: string): KBEdge[] {
|
|
394
|
+
if (!nodeId) return [...this.edges];
|
|
395
|
+
return this.edges.filter(e => e.from === nodeId || e.to === nodeId);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Get statistics about the knowledge base.
|
|
400
|
+
* Includes dangling code references if a resolver backend is available.
|
|
401
|
+
*/
|
|
402
|
+
async getStats(): Promise<KBStats> {
|
|
403
|
+
const byType: Partial<Record<KBNodeType, number>> = {};
|
|
404
|
+
const byLifecycle: Partial<Record<KBLifecycle, number>> = {};
|
|
405
|
+
|
|
406
|
+
for (const node of this.nodes.values()) {
|
|
407
|
+
byType[node.type] = (byType[node.type] || 0) + 1;
|
|
408
|
+
byLifecycle[node.lifecycle] = (byLifecycle[node.lifecycle] || 0) + 1;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const edgesByType: Record<string, number> = {};
|
|
412
|
+
for (const edge of this.edges) {
|
|
413
|
+
edgesByType[edge.type] = (edgesByType[edge.type] || 0) + 1;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Find dangling KB-internal refs
|
|
417
|
+
const nodeIds = new Set(this.nodes.keys());
|
|
418
|
+
const danglingRefs: string[] = [];
|
|
419
|
+
for (const edge of this.edges) {
|
|
420
|
+
if (!nodeIds.has(edge.from) && !danglingRefs.includes(edge.from)) {
|
|
421
|
+
danglingRefs.push(edge.from);
|
|
422
|
+
}
|
|
423
|
+
if (!nodeIds.has(edge.to) && !danglingRefs.includes(edge.to)) {
|
|
424
|
+
danglingRefs.push(edge.to);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Find dangling code references
|
|
429
|
+
const danglingCodeRefs = await this.getDanglingCodeRefs();
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
totalNodes: this.nodes.size,
|
|
433
|
+
byType,
|
|
434
|
+
byLifecycle,
|
|
435
|
+
totalEdges: this.edges.length,
|
|
436
|
+
edgesByType,
|
|
437
|
+
danglingRefs,
|
|
438
|
+
danglingCodeRefs,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// --- Private helpers ---
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Recursively scan directory for files with given extension.
|
|
446
|
+
*/
|
|
447
|
+
private scanFiles(dir: string, ext: string): string[] {
|
|
448
|
+
const results: string[] = [];
|
|
449
|
+
|
|
450
|
+
let entries: string[];
|
|
451
|
+
try {
|
|
452
|
+
entries = readdirSync(dir);
|
|
453
|
+
} catch {
|
|
454
|
+
return results;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
for (const entry of entries) {
|
|
458
|
+
const fullPath = join(dir, entry);
|
|
459
|
+
try {
|
|
460
|
+
const stat = statSync(fullPath);
|
|
461
|
+
if (stat.isDirectory()) {
|
|
462
|
+
results.push(...this.scanFiles(fullPath, ext));
|
|
463
|
+
} else if (entry.endsWith(ext)) {
|
|
464
|
+
results.push(fullPath);
|
|
465
|
+
}
|
|
466
|
+
} catch {
|
|
467
|
+
// Skip unreadable entries
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return results;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Generate a slug from content (first line, simplified).
|
|
475
|
+
*/
|
|
476
|
+
private generateSlug(content: string): string {
|
|
477
|
+
const firstLine = content.split('\n')[0].trim();
|
|
478
|
+
return firstLine
|
|
479
|
+
.toLowerCase()
|
|
480
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
481
|
+
.replace(/\s+/g, '-')
|
|
482
|
+
.replace(/-+/g, '-')
|
|
483
|
+
.replace(/^-|-$/g, '')
|
|
484
|
+
.slice(0, 60) || 'untitled';
|
|
485
|
+
}
|
|
486
|
+
}
|