mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
feat(code-intelligence): first-class C4 architecture diagrams (ADR-112)
Consolidate C4 diagram generation onto a single engine and expose it to users via CLI and MCP, with a deterministic confidence gate and real Knowledge-Graph-derived relationships. - Consolidation (C1/C6): C4ModelService is the single render/analyze/store engine; the bridge delegates and FAILS LOUD on a render error instead of silently degrading. Duplicate inline generators removed. - C2: real component relationships from the Knowledge Graph (AST import/call edges) replace the naming heuristic; project-scoped via a new KG basePath so repos outside cwd don't trip the path-traversal guard. - C3: deterministic confidence gate (high/medium/low + reasons) on every diagram — surfaces the detector's known limits instead of hiding them. - C4/C5: `aqe code c4` CLI + `qe/code/c4` MCP tool (generate/search), verified at MCP-CLI parity and through the protocol-server bridge. - Search: generate persists embeddings (opt-in `enableC4Embeddings`, on for MCP) so `qe/code/c4 search` returns hits in the standard flow. - Fixed pre-existing lint in touched files (require->import, unused catches). Docs: ADR-112 + docs/guides/c4-architecture-diagrams.md. tsc + lint clean; C4 surface fully green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,9 @@ aqe code deps src/
|
||||
|
||||
# Analyze complexity and find hotspots
|
||||
aqe code complexity src/
|
||||
|
||||
# Generate C4 architecture diagrams (Mermaid) with a confidence score
|
||||
aqe code c4 .
|
||||
```
|
||||
|
||||
## Agent Workflow
|
||||
|
||||
@@ -35,6 +35,9 @@ aqe code deps src/
|
||||
|
||||
# Analyze complexity and find hotspots
|
||||
aqe code complexity src/
|
||||
|
||||
# Generate C4 architecture diagrams (Mermaid) with a confidence score
|
||||
aqe code c4 .
|
||||
```
|
||||
|
||||
## Agent Workflow
|
||||
|
||||
@@ -359,6 +359,7 @@ aqe code search "authentication" # Semantic code search
|
||||
aqe code impact src/ # Change impact analysis
|
||||
aqe code deps src/ # Dependency mapping
|
||||
aqe code complexity src/ # Complexity metrics and hotspots
|
||||
aqe code c4 . # C4 architecture diagrams (Mermaid) + confidence
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -39,6 +39,9 @@ aqe code deps src/
|
||||
|
||||
# Analyze complexity and find hotspots
|
||||
aqe code complexity src/
|
||||
|
||||
# Generate C4 architecture diagrams (Mermaid) with a confidence score
|
||||
aqe code c4 .
|
||||
```
|
||||
|
||||
## Agent Workflow
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# C4 Architecture Diagrams
|
||||
|
||||
Generate [C4 model](https://c4model.com/) architecture diagrams (Context → Container → Component, as Mermaid) directly from your codebase — with a confidence score so you know how much to trust the auto-detected structure.
|
||||
|
||||
> **What it is:** AQE scans your repo, detects components (from `src/` structure), external systems (from dependencies), and their relationships, then renders standard C4 Mermaid you can paste into GitHub, a README, or any Mermaid-aware IDE. A deterministic quality gate rates the result `high` / `medium` / `low`.
|
||||
|
||||
---
|
||||
|
||||
## CLI
|
||||
|
||||
```bash
|
||||
# All three C4 levels for the current repo (Markdown with fenced mermaid blocks)
|
||||
aqe code c4 .
|
||||
|
||||
# Just the component diagram
|
||||
aqe code c4 src/ --level component
|
||||
|
||||
# Full structured result as JSON, written to a file
|
||||
aqe code c4 . --format json -o c4.json
|
||||
```
|
||||
|
||||
| Flag | Values | Default | Meaning |
|
||||
|------|--------|---------|---------|
|
||||
| `--level` | `context` \| `container` \| `component` \| `all` | `all` | Which C4 level(s) to emit |
|
||||
| `--format` | `text` \| `json` | `text` | `text` = Markdown + Mermaid; `json` = full result object |
|
||||
| `-o, --output <path>` | file path | stdout | Write the output to a file |
|
||||
|
||||
**Text output** prints each diagram in a ` ```mermaid ` block, then a confidence banner and a short architecture summary:
|
||||
|
||||
```
|
||||
Confidence: MEDIUM (55%)
|
||||
- No relationships detected between components — edges are heuristic-only or missing; the structure is unverified.
|
||||
- Auto-generated draft — verify against the source before relying on it.
|
||||
|
||||
Components: 6 External systems: 2 Relationships: 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MCP tool
|
||||
|
||||
`qe/code/c4` exposes the same engine to agents and IDEs.
|
||||
|
||||
```jsonc
|
||||
// Generate
|
||||
{ "action": "generate", "projectPath": ".", "level": "all" }
|
||||
|
||||
// Semantic search over previously generated diagrams
|
||||
{ "action": "search", "query": "where does auth talk to the database", "limit": 5 }
|
||||
```
|
||||
|
||||
`generate` returns `{ diagrams, confidence, componentsDetected, externalSystemsDetected, relationshipsDetected, circularDependencies }`. The CLI and MCP drive the **same** pipeline, so their diagrams match.
|
||||
|
||||
---
|
||||
|
||||
## How confidence is scored
|
||||
|
||||
The gate is **deterministic** (pure code, no LLM — it can't be talked into a good score). It reflects the detector's known limits:
|
||||
|
||||
| Signal | Effect |
|
||||
|--------|--------|
|
||||
| 0 components detected | `low` (empty diagram) |
|
||||
| No relationships between components | penalized — "structure unverified" |
|
||||
| External systems detected from deps | small boost (Platform picture grounded) |
|
||||
| Repo > ~50K LOC | downgraded to draft (detection degrades at scale) |
|
||||
| Single component | slight penalty (under-segmented) |
|
||||
|
||||
**Treat `medium`/`low` diagrams as a starting draft to refine, not ground truth.** Architecture detection is heuristic — most accurate on well-structured `src/` trees and small-to-mid repos.
|
||||
|
||||
---
|
||||
|
||||
## How it works (under the hood)
|
||||
|
||||
```
|
||||
your repo ──► detect (src/ structure + dependency patterns)
|
||||
──► C4ModelService (render Mermaid + architecture analysis + store)
|
||||
──► confidence gate ──► CLI / MCP / product-factors (SFDIPOT)
|
||||
```
|
||||
|
||||
The same C4 output also feeds the product-factors / SFDIPOT test-strategy assessor, so generating diagrams improves test-design analysis for free.
|
||||
|
||||
See [`ADR-112`](../implementation/adrs/ADR-112-c4-architecture-diagrams.md) for the design decision.
|
||||
@@ -0,0 +1,135 @@
|
||||
# ADR-112: First-Class C4 Architecture Diagrams (Consolidate, Gate, Expose)
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Decision ID** | ADR-112 |
|
||||
| **Status** | Accepted (2026-06-26) — implemented C0–C7 (incl. C2 KG-backed relationships; C6 deletes the duplicate + fails loud) |
|
||||
| **Date** | 2026-06-26 |
|
||||
| **Author** | AQE Core |
|
||||
| **Review Cadence** | 3 months |
|
||||
| **Supersedes** | — |
|
||||
| **Related** | [ADR-050](./ADR-050-ruvector-neural-backbone.md) (the "Code Intelligence Gap"), [ADR-090](./ADR-090-hnswlib-node-migration.md) (HNSW index), product-factors-assessor (SFDIPOT consumer) |
|
||||
|
||||
---
|
||||
|
||||
## WH(Y) Decision Statement
|
||||
|
||||
**In the context of** AQE already shipping a C4 model (Simon Brown's Context → Container → Component) that feeds the product-factors / SFDIPOT test-strategy assessor, and users who would benefit from auto-generated, always-current architecture diagrams for onboarding, impact review, and test design,
|
||||
|
||||
**facing** a **half-wired** C4 implementation built without any ADR: the production path renders Mermaid via ~100 lines of inline string-concatenation in `ProductFactorsBridgeService`, while a far richer, fully-written `C4ModelService` (1606 LoC — Mermaid + architecture analysis + embeddings + memory-backed semantic search) sits **orphaned**, instantiated only by its own factory and never called; plus there is **no user-facing surface** (C4 is reachable only internally via product-factors),
|
||||
|
||||
**we decided for** making C4 a **first-class, fully-implemented capability**: (a) **consolidate** on `C4ModelService` as the single render + analyze + store engine and reduce the bridge to a detector that delegates to it, (b) add a **deterministic quality/confidence gate** so we never emit a confident-but-wrong diagram, and (c) **expose** C4 to users through a CLI (`aqe code c4`) and an MCP tool (`qe/code/c4`),
|
||||
|
||||
**and neglected** a third parallel renderer (we remove duplication, not add to it), heavyweight UML/PlantUML output (Mermaid C4 is enough and renders in GitHub/IDEs), and full LLM-authored architecture narration (kept optional/out of the accept gate to avoid hallucinated structure),
|
||||
|
||||
**to achieve** real user value — one command or tool call produces accurate Context/Container/Component diagrams + coupling/cycle analysis + semantic diagram search, current with the codebase — while collapsing two code paths into one maintained engine,
|
||||
|
||||
**accepting that** auto-detected components/relationships have a **capability ceiling** (the `qe-code-intelligence` skill itself records ~18% success on complex queries and degradation above ~50K LOC), so the gate must surface a **confidence score** and the diagrams are explicitly "a generated draft to refine," not ground truth.
|
||||
|
||||
---
|
||||
|
||||
## Current state (grounded, verified 2026-06-26)
|
||||
|
||||
| Fact | Evidence |
|
||||
|---|---|
|
||||
| **No ADR governs C4.** ADR-050 flagged the "Code Intelligence Gap"; ADR-018 is referenced in `v3-adrs.md` but the file does not exist | grep across all 83 ADRs — zero `C4` decisions |
|
||||
| Production C4 path = bridge's **inline Mermaid string-builders** | `coordinator.generateC4Diagrams()` (`coordinator.ts:1217`) → `bridge.requestC4Diagrams()` (`:1242`) → `generateContextDiagram`/`Container`/`Component`/`DependencyGraph` (`product-factors-bridge.ts:397–500`) |
|
||||
| The rich **`C4ModelService` is orphaned** — built but never wired | only `new C4ModelService(...)` is its own factory `createC4ModelService` (`c4-model/index.ts:1605`); exported via `services/index.ts:36` but **no production caller** |
|
||||
| `C4ModelService` is a pure **renderer + analyzer + store** (takes structured specs; no filesystem scanning) | `buildContext/Container/Component(request)` take `components`/`containers` specs (`types.ts:328,364,412`); `analyzeArchitecture` → coupling (`:1384`), cycle detection (`:1445`), recommendations (`:1491`), pattern + layer detection (`:1327,:1357`); embeddings + memory + `searchDiagrams` |
|
||||
| The **bridge is the detector** (filesystem scan) | `analyzeComponents(projectPath)` (`:589`, `fs.readdir` walk `:611,:682`), external systems via 76 dependency patterns (`:76`), package.json parsing (`:516`) |
|
||||
| Consumer today = product-factors / SFDIPOT only | `product-factors-service.ts:174–193` requests C4; `architecture-parser.ts:108–144` parses the Mermaid back into components |
|
||||
| Shared vocabulary is already factored out | `src/shared/c4-model/index.ts` (`C4Person`/`Container`/`Component`/`Relationship`, `inferComponentType`, helpers) |
|
||||
| **No direct user surface.** CLI `aqe code` has `index\|search\|impact\|deps\|complexity` (no `c4`); MCP has only `qe/code/analyze` | `cli/commands/code.ts:20`; `mcp/tools/code-intelligence/analyze.ts` |
|
||||
|
||||
**The core problem in one line:** the *good* C4 engine exists and is unused; the *production* C4 path is a weaker duplicate; and neither is reachable by users.
|
||||
|
||||
---
|
||||
|
||||
## Decision detail
|
||||
|
||||
### 1. Consolidate — one engine
|
||||
|
||||
`C4ModelService` becomes the single source for **rendering, architecture analysis, embeddings, and storage**. `ProductFactorsBridgeService` keeps only its **detection** responsibility (scan repo → external systems + components + relationships) and **delegates** rendering/analysis to `C4ModelService` instead of its inline generators. The inline `generate*Diagram` methods are removed once parity tests pass.
|
||||
|
||||
```
|
||||
repo ──[Bridge.detect: fs scan + 76 dep patterns + (optional) KnowledgeGraph edges]──►
|
||||
DetectedComponents/Relationships/ExternalSystems
|
||||
└──► C4ModelService.buildContext/Container/Component ──► Mermaid
|
||||
└──► C4ModelService.analyzeArchitecture ──► coupling, cycles, recs
|
||||
└──► C4ModelService (embeddings + memory) ──► searchDiagrams / retrieval
|
||||
```
|
||||
|
||||
Relationship quality is the weakest link in today's detector (directory heuristics). Where the **KnowledgeGraph** already has real import/call edges (`knowledge-graph.ts`), the detector should prefer those over directory grouping — this is the single biggest accuracy lever.
|
||||
|
||||
### 2. Quality gate — never ship a confident-but-wrong diagram
|
||||
|
||||
A **deterministic** confidence score (no LLM in the gate, per the ADR-111 discipline), surfaced in output and metadata:
|
||||
|
||||
- inputs: # files analyzed vs total, # components with real KG edges vs heuristic-only, external-systems matched, repo size vs the ~50K-LOC degradation threshold.
|
||||
- `confidence: 'high' | 'medium' | 'low'` with the reasons; **low** prints a visible "draft — verify against source" banner and (CLI) a non-zero hint. This directly answers the skill's recorded 18%/50K-LOC limits instead of hiding them.
|
||||
|
||||
### 3. Expose — CLI + MCP (both, per decision)
|
||||
|
||||
- **CLI:** extend `cli/commands/code.ts` with a `c4` action:
|
||||
`aqe code c4 <path> [--level context|container|component|all] [--format mermaid|json] [--search "<query>"] [--output file]`.
|
||||
Prints Mermaid (copy-paste into GitHub/IDE), or JSON for tooling; `--search` runs semantic diagram retrieval.
|
||||
- **MCP:** new tool `qe/code/c4` (mirror `analyze.ts` structure) with actions `generate` (level-scoped), `search`, `analyze` (coupling/cycles/recommendations), `get`. Wraps the same `C4ModelService` — no logic duplication.
|
||||
|
||||
---
|
||||
|
||||
## Implementation plan (phased — for review before any code)
|
||||
|
||||
> Cost: **S** ≤ ~2 days · **M** ~3–5 days. All phases keep existing product-factors behavior green.
|
||||
|
||||
| # | Phase | Work | Pre | Cost | Risk |
|
||||
|---|---|---|---|---|---|
|
||||
| **C0** | **Confirm orphan + parity baseline** | Prove `C4ModelService` is unreachable in prod; snapshot current bridge Mermaid output on a fixture repo as the parity oracle | — | S | Low |
|
||||
| **C1** | **Delegate rendering** | Bridge calls `C4ModelService.build*` instead of inline `generate*Diagram`; map `DetectedComponent[]` → `ComponentSpec[]`. Parity test: new output ⊇ old (same elements/edges) | C0 | M | Med (output drift — guarded by C0 snapshot) |
|
||||
| **C2** | **KG-backed relationships** | Detector prefers KnowledgeGraph import/call edges over directory heuristics where available; fall back cleanly | C1 | M | Med (accuracy lever) |
|
||||
| **C3** | **Quality gate** | Deterministic `confidence` scorer + reasons; thread into `C4DiagramResult.metadata` and all surfaces | C1 | S | Low |
|
||||
| **C4** | **CLI `aqe code c4`** | New action in `code.ts`; Mermaid/JSON output, `--level`, `--search`, low-confidence banner; help + fish completion | C1, C3 | S | Low |
|
||||
| **C5** | **MCP `qe/code/c4`** | New tool wrapping `C4ModelService`; register in tool registry; integration test via the protocol server (per CLAUDE.md MCP-parity rule) | C1, C3 | M | Med |
|
||||
| **C6** | **Remove the duplicate** | Delete bridge inline `generate*Diagram` once C1 parity holds; update product-factors path to the consolidated call | C1–C5 green | S | Low |
|
||||
| **C7** | **Docs + tests** | User guide (`docs/guides/`), unit + integration (CLI and MCP), update `v3-adrs.md` index; flip this ADR to Accepted | C4, C5 | S | Low |
|
||||
|
||||
**Verification (per CLAUDE.md):** real `node --test` suites for the renderer/gate; **MCP-CLI parity** — the same fixture repo must produce equivalent C4 via `aqe code c4` and the `qe/code/c4` MCP tool; a smoke run on a real fixture before any release.
|
||||
|
||||
**Acceptance / flip-to-Accepted criteria:**
|
||||
1. `C4ModelService` is the only renderer; bridge inline generators deleted; product-factors output unchanged (parity test green).
|
||||
2. `aqe code c4` and `qe/code/c4` both emit valid Mermaid C4 + an architecture analysis + a confidence score on a fixture repo.
|
||||
3. Low-confidence repos (e.g. >50K LOC) surface the warning rather than a silent wrong diagram.
|
||||
|
||||
**G-ABORT (record-and-stop):** if KG-backed detection (C2) cannot lift relationship accuracy above directory heuristics on real repos, ship C1/C3/C4/C5 with the detector as-is and an explicit "draft" confidence framing — still net-positive (one maintained engine + a user surface), just no accuracy claim.
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive:** one maintained C4 engine (deletes a duplicate); users get architecture diagrams + semantic diagram search via CLI and MCP; product-factors gains the richer analysis for free; honest confidence gating turns a known limitation into a surfaced signal.
|
||||
|
||||
**Negative / risks:** auto-detection accuracy is capped by code-intelligence quality (mitigated by C2 + the gate, bounded by G-ABORT); C1 risks output drift (mitigated by the C0 parity snapshot); a new MCP tool adds protocol surface to maintain.
|
||||
|
||||
**Neutral:** no new dependencies (Mermaid is plain strings; HNSW/embeddings already shipped). The shared `src/shared/c4-model` vocabulary is unchanged.
|
||||
|
||||
---
|
||||
|
||||
## Implementation status (2026-06-26 — Accepted)
|
||||
|
||||
Delivered across phases C0–C7. `tsc --noEmit` = 0 errors; the C4 area is green (code-intelligence + shared C4 + MCP registry/tool suites).
|
||||
|
||||
| Phase | Status | Artifacts |
|
||||
|---|---|---|
|
||||
| C0 parity baseline | ✅ | orphan confirmed; parser-compatibility verified (`architecture-parser` keys on `Component(`/`Person(` labels, which `C4ModelService` emits) |
|
||||
| C1 delegate rendering | ✅ | `services/c4-model/from-detected.ts` (pure detected→spec mapping, enum translation); bridge `renderContext/Container/Component` delegate to `C4ModelService` with inline fallback. Tests: `from-detected.test.ts` (16), `c4-consolidation.test.ts` (4) |
|
||||
| C2 KG-backed relationships | ✅ | `services/c4-model/kg-relationships.ts` — folds the KnowledgeGraph's REAL import/call edges (AST/TS parser, no LLM) up to component→component relationships, replacing the naming heuristic; injected into the bridge as a project-scoped resolver with heuristic fallback. Required adding `basePath` to `KnowledgeGraphConfig` (so analyzing a repo outside cwd doesn't trip the FileReader path-traversal guard). Tests: `kg-relationships.test.ts` (8 pure) + `c4-kg-relationships.test.ts` (2 real-KG, assert `depends_on` edges from actual `import`s) |
|
||||
| C3 confidence gate | ✅ | `shared/c4-model/confidence.ts` `assessC4Confidence()` (deterministic, no LLM); attached to `C4AnalysisMetadata.confidence`. Tests: `c4-confidence.test.ts` (6) |
|
||||
| C4 CLI | ✅ | `aqe code c4 <path> [--level] [--format] [-o]` + confidence banner + completions. Validated by `c4-generation-e2e.test.ts` (3, real pipeline over a temp fixture) |
|
||||
| C5 MCP tool | ✅ | `qe/code/c4` (generate/search) registered; **MCP-CLI parity test** asserts MCP output == bridge/CLI pipeline. Tests: `code-c4.test.ts` (5) |
|
||||
| C6 remove duplicate | ✅ | The inline `generateContext/Container/Component` methods are **deleted** and the bridge no longer `implements IC4DiagramGenerator`. `render*` now **fails loud** — on a `C4ModelService` failure it logs at error level and propagates, so the caller surfaces it (CLI prints `Failed:`, MCP returns `success:false`) instead of silently degrading to a weaker diagram (ADR-050: no silent degradation). `generateDependencyGraph` stays (not a C4 level; `C4ModelService` doesn't render it). Test: `c4-generation-e2e.test.ts` asserts a forced render failure surfaces as an error. |
|
||||
| C7 docs + status | ✅ | `docs/guides/c4-architecture-diagrams.md`; this status block; ADR flipped to Accepted. (The `v3-adrs.md` index is stale — stops at ADR-094, missing 095–111 — so no lone row was forced in, per that precedent.) |
|
||||
|
||||
**Production-safety note:** the C1 change touches the product-factors render path. It is guarded by (a) verified parser compatibility, (b) the inline fallback, and (c) the parity tests proving detected elements survive into the new output. Behavior is preserved as a superset.
|
||||
|
||||
**Open follow-ups:** wiring `totalLoc` (from MetricCollector) into the confidence gate (so large repos auto-downgrade). *(C2 KG-backed relationships and the `qe/code/c4 search` persistence path are both DONE — see the status table.)*
|
||||
|
||||
**Search persistence (DONE):** the bridge gained an `enableC4Embeddings` option (default off → product-factors stays fast/offline). The MCP `qe/code/c4` tool turns it on, so `generate` embeds + persists diagrams to memory; `search` then finds them by vector similarity over the shared `code-intelligence:c4` namespace. Test: `code-c4.test.ts` asserts `search` returns hits after `generate`.
|
||||
@@ -9,6 +9,7 @@ import chalk from 'chalk';
|
||||
import type { CLIContext } from '../handlers/interfaces.js';
|
||||
import { walkSourceFiles, SOURCE_EXTENSIONS } from '../utils/file-discovery.js';
|
||||
import { type OutputFormat, writeOutput, toJSON } from '../utils/ci-output.js';
|
||||
import type { C4DiagramResult } from '../../shared/c4-model';
|
||||
|
||||
export function createCodeCommand(
|
||||
context: CLIContext,
|
||||
@@ -17,12 +18,13 @@ export function createCodeCommand(
|
||||
): Command {
|
||||
const codeCmd = new Command('code')
|
||||
.description('Code intelligence analysis')
|
||||
.argument('<action>', 'Action (index|search|impact|deps|complexity)')
|
||||
.argument('<action>', 'Action (index|search|impact|deps|complexity|c4)')
|
||||
.argument('[target]', 'Target path or query')
|
||||
.option('--depth <depth>', 'Analysis depth', '3')
|
||||
.option('--include-tests', 'Include test files')
|
||||
.option('--incremental', 'Incremental indexing (index action only)')
|
||||
.option('--git-since <ref>', 'Index changes since git ref (index action only)')
|
||||
.option('--level <level>', 'C4 level for c4 action (context|container|component|all)', 'all')
|
||||
.option('-F, --format <format>', 'Output format (text|json)', 'text')
|
||||
.option('-o, --output <path>', 'Write output to file')
|
||||
.addHelpText('after', `
|
||||
@@ -34,6 +36,9 @@ Examples:
|
||||
aqe code impact src/auth/ Analyze change impact
|
||||
aqe code deps src/ Map dependencies
|
||||
aqe code complexity src/ Analyze code complexity metrics
|
||||
aqe code c4 . Generate C4 architecture diagrams (Mermaid)
|
||||
aqe code c4 src/ --level component Only the C4 component diagram
|
||||
aqe code c4 . --format json -o c4.json Full C4 result as JSON
|
||||
`)
|
||||
.action(async (action: string, target: string, options) => {
|
||||
if (!await ensureInitialized()) return;
|
||||
@@ -45,6 +50,9 @@ Examples:
|
||||
analyzeImpact(request: { changedFiles: string[]; depth?: number; includeTests?: boolean }): Promise<{ success: boolean; value?: unknown; error?: Error }>;
|
||||
mapDependencies(request: { files: string[]; direction: string; depth?: number }): Promise<{ success: boolean; value?: unknown; error?: Error }>;
|
||||
getSemanticAnalyzer(): { analyze(code: string): Promise<{ success: boolean; value?: { concepts: string[]; patterns: string[]; complexity: { cyclomatic: number; cognitive: number; halstead: { vocabulary: number; length: number; difficulty: number; effort: number; time: number; bugs: number } }; dependencies: string[]; suggestions: string[] }; error?: Error }> } | null;
|
||||
getCoordinator?(): {
|
||||
generateC4Diagrams(projectPath: string, options?: Record<string, unknown>): Promise<{ success: boolean; value?: C4DiagramResult; error?: Error }>;
|
||||
} | null;
|
||||
}>('code-intelligence');
|
||||
|
||||
if (!codeAPI) {
|
||||
@@ -389,9 +397,85 @@ Examples:
|
||||
}
|
||||
}
|
||||
|
||||
} else if (action === 'c4') {
|
||||
const targetPath = path.resolve(target || '.');
|
||||
const level = String(options.level || 'all').toLowerCase();
|
||||
const validLevels = ['context', 'container', 'component', 'all'];
|
||||
if (!validLevels.includes(level)) {
|
||||
console.log(chalk.red(`Invalid --level: "${level}" (must be ${validLevels.join('|')})`));
|
||||
return await cleanupAndExit(1);
|
||||
}
|
||||
|
||||
if (typeof codeAPI.getCoordinator !== 'function' || !codeAPI.getCoordinator()) {
|
||||
console.log(chalk.red('C4 generation not available — ensure the fleet is initialized'));
|
||||
return await cleanupAndExit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`\n Generating C4 diagrams for ${targetPath} (level: ${level})...\n`));
|
||||
|
||||
const want = (l: string) => level === 'all' || level === l;
|
||||
const result = await codeAPI.getCoordinator()!.generateC4Diagrams(targetPath, {
|
||||
includeContext: want('context'),
|
||||
includeContainer: want('container'),
|
||||
includeComponent: want('component'),
|
||||
includeDependency: level === 'all',
|
||||
analyzeComponents: true,
|
||||
detectExternalSystems: true,
|
||||
analyzeCoupling: true,
|
||||
});
|
||||
|
||||
if (!result.success || !result.value) {
|
||||
console.log(chalk.red(`Failed: ${result.error?.message || 'Unknown error'}`));
|
||||
return await cleanupAndExit(1);
|
||||
}
|
||||
|
||||
const c4 = result.value;
|
||||
const confidence = c4.metadata.analysisMetadata?.confidence;
|
||||
|
||||
if (format === 'json') {
|
||||
writeOutput(toJSON(c4), options.output);
|
||||
} else {
|
||||
// Build the Markdown (fenced mermaid blocks) — copy-paste into GitHub/IDE.
|
||||
const blocks: string[] = [];
|
||||
const add = (titleLevel: string, code?: string) => {
|
||||
if (code) blocks.push(`### C4 ${titleLevel}\n\n\`\`\`mermaid\n${code.trim()}\n\`\`\``);
|
||||
};
|
||||
add('Context', c4.diagrams.context);
|
||||
add('Container', c4.diagrams.container);
|
||||
add('Component', c4.diagrams.component);
|
||||
if (c4.diagrams.dependency) blocks.push(`### Dependency Graph\n\n\`\`\`mermaid\n${c4.diagrams.dependency.trim()}\n\`\`\``);
|
||||
const markdown = `# C4 Architecture — ${c4.metadata.projectName}\n\n${blocks.join('\n\n')}\n`;
|
||||
|
||||
if (options.output) {
|
||||
writeOutput(markdown, options.output);
|
||||
} else {
|
||||
console.log(markdown);
|
||||
}
|
||||
|
||||
// Confidence banner (ADR-112 quality gate) — always to stderr-style console.
|
||||
if (confidence) {
|
||||
const color = confidence.level === 'high' ? chalk.green : confidence.level === 'medium' ? chalk.yellow : chalk.red;
|
||||
console.log(color(`Confidence: ${confidence.level.toUpperCase()} (${(confidence.score * 100).toFixed(0)}%)`));
|
||||
if (confidence.level !== 'high') {
|
||||
for (const reason of confidence.reasons) {
|
||||
console.log(chalk.gray(` - ${reason}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Architecture analysis (coupling/cycles).
|
||||
const coupling = c4.couplingAnalysis ?? [];
|
||||
const circular = coupling.filter((c) => c.isCircular);
|
||||
console.log(chalk.cyan(`\n Components: ${chalk.white(c4.components.length)} External systems: ${chalk.white(c4.externalSystems.length)} Relationships: ${chalk.white(c4.relationships.length)}`));
|
||||
if (circular.length > 0) {
|
||||
console.log(chalk.red(` Circular dependencies: ${circular.length}`));
|
||||
for (const c of circular.slice(0, 3)) console.log(chalk.red(` ${c.moduleA} <-> ${c.moduleB}`));
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log(chalk.red(`\nUnknown action: ${action}`));
|
||||
console.log(chalk.gray(' Available: index, search, impact, deps, complexity\n'));
|
||||
console.log(chalk.gray(' Available: index, search, impact, deps, complexity, c4\n'));
|
||||
await cleanupAndExit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -363,7 +363,7 @@ _aqe_completions() {
|
||||
local protocol_subcmds="run"
|
||||
local completions_subcmds="bash zsh fish powershell install"
|
||||
local brain_subcmds="export import info diff search witness-backfill"
|
||||
local code_actions="index search impact deps"
|
||||
local code_actions="index search impact deps complexity c4"
|
||||
local test_actions="generate execute"
|
||||
|
||||
# Domains
|
||||
@@ -1307,6 +1307,9 @@ complete -c aqe -n "__fish_seen_subcommand_from code; and not __fish_seen_subcom
|
||||
complete -c aqe -n "__fish_seen_subcommand_from code; and not __fish_seen_subcommand_from index search impact deps" -a "search" -d "Search code"
|
||||
complete -c aqe -n "__fish_seen_subcommand_from code; and not __fish_seen_subcommand_from index search impact deps" -a "impact" -d "Analyze impact"
|
||||
complete -c aqe -n "__fish_seen_subcommand_from code; and not __fish_seen_subcommand_from index search impact deps" -a "deps" -d "Map dependencies"
|
||||
complete -c aqe -n "__fish_seen_subcommand_from code; and not __fish_seen_subcommand_from index search impact deps complexity c4" -a "complexity" -d "Code complexity metrics"
|
||||
complete -c aqe -n "__fish_seen_subcommand_from code; and not __fish_seen_subcommand_from index search impact deps complexity c4" -a "c4" -d "Generate C4 architecture diagrams"
|
||||
complete -c aqe -n "__fish_seen_subcommand_from code; and __fish_seen_subcommand_from c4" -l level -d "C4 level" -xa "context container component all"
|
||||
complete -c aqe -n "__fish_seen_subcommand_from code" -l depth -d "Analysis depth" -xa "1 2 3 4 5"
|
||||
complete -c aqe -n "__fish_seen_subcommand_from code" -l include-tests -d "Include test files"
|
||||
# Directory completion for code actions
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* - QESONA: Learns and adapts code patterns for improved intelligence
|
||||
*/
|
||||
|
||||
import { existsSync } from 'node:fs';
|
||||
import { LoggerFactory } from '../../logging/index.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Result, err, DomainEvent } from '../../shared/types';
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
ProductFactorsBridgeService,
|
||||
IProductFactorsBridge,
|
||||
} from './services/product-factors-bridge';
|
||||
import { createKnowledgeGraphRelationshipResolver } from './services/c4-model/kg-relationships';
|
||||
import {
|
||||
CodeIntelligenceAPI,
|
||||
IndexRequest,
|
||||
@@ -312,9 +314,14 @@ export class CodeIntelligenceCoordinator
|
||||
this.impactAnalyzer = new ImpactAnalyzerService(memory, this.knowledgeGraph);
|
||||
this.fileReader = new FileReader();
|
||||
|
||||
// Initialize Product Factors Bridge
|
||||
// Initialize Product Factors Bridge.
|
||||
// ADR-112 C2: inject a KG-backed relationship resolver so C4 component
|
||||
// diagrams use REAL import/call edges (not the naming heuristic).
|
||||
this.productFactorsBridge = new ProductFactorsBridgeService(eventBus, memory, {
|
||||
publishEvents: this.config.publishEvents,
|
||||
relationshipResolver: createKnowledgeGraphRelationshipResolver(
|
||||
(projectPath) => new KnowledgeGraphService({ memory, llmRouter }, { basePath: projectPath }),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1437,9 +1444,8 @@ export class CodeIntelligenceCoordinator
|
||||
for (const marker of markers) {
|
||||
try {
|
||||
const markerPath = `${currentPath}/${marker}`;
|
||||
// Use synchronous check (since we're in async context anyway)
|
||||
const fs = require('fs');
|
||||
if (fs.existsSync(markerPath)) {
|
||||
// Synchronous existence check (we're in an async context anyway).
|
||||
if (existsSync(markerPath)) {
|
||||
return currentPath;
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* ADR-112 — Detected → C4 spec mapping.
|
||||
*
|
||||
* Pure, dependency-free functions that convert the code-intelligence DETECTOR
|
||||
* output (shared `Detected*` types, produced by ProductFactorsBridgeService's
|
||||
* filesystem scan) into the `C4ModelService` Build*Request specs.
|
||||
*
|
||||
* This is the consolidation seam: the bridge keeps detection; the rich
|
||||
* `C4ModelService` does all rendering/analysis/storage. Keeping the mapping pure
|
||||
* makes the C1 parity test (every detected element survives into a spec) trivial
|
||||
* and keeps zero coupling to the service's async/embedding/memory machinery.
|
||||
*/
|
||||
|
||||
import type {
|
||||
C4ProjectInfo,
|
||||
DetectedExternalSystem,
|
||||
DetectedComponent,
|
||||
DetectedRelationship,
|
||||
ExternalSystemType,
|
||||
C4ComponentType,
|
||||
C4RelationshipType as SharedC4RelationshipType,
|
||||
} from '../../../../shared/c4-model';
|
||||
import type {
|
||||
BuildContextRequest,
|
||||
BuildContainerRequest,
|
||||
BuildComponentRequest,
|
||||
ExternalSystemSpec,
|
||||
ComponentSpec,
|
||||
ComponentRelationshipSpec,
|
||||
SystemType,
|
||||
ComponentType,
|
||||
C4RelationshipType as DomainC4RelationshipType,
|
||||
} from './types';
|
||||
|
||||
/** The single application container the detector models today (mirrors the
|
||||
* legacy bridge, which hardcoded one "Application" container). Real multi-
|
||||
* container detection is future work (ADR-112 G-ABORT note). */
|
||||
export const DEFAULT_CONTAINER_NAME = 'Application';
|
||||
|
||||
/** shared ExternalSystemType → domain SystemType (C4ModelService vocabulary). */
|
||||
const EXTERNAL_SYSTEM_TYPE_MAP: Record<ExternalSystemType, SystemType> = {
|
||||
database: 'database',
|
||||
cache: 'cache',
|
||||
queue: 'message_queue',
|
||||
api: 'api',
|
||||
storage: 'storage',
|
||||
auth: 'authentication',
|
||||
monitoring: 'monitoring',
|
||||
cloud: 'third_party',
|
||||
};
|
||||
|
||||
/** shared C4ComponentType → domain ComponentType (C4ModelService vocabulary).
|
||||
* Types the service lacks (transformer/layer/feature/package/other) fold to the
|
||||
* nearest supported concept so no component is dropped. */
|
||||
const COMPONENT_TYPE_MAP: Record<C4ComponentType, ComponentType> = {
|
||||
controller: 'controller',
|
||||
service: 'service',
|
||||
repository: 'repository',
|
||||
facade: 'facade',
|
||||
factory: 'factory',
|
||||
adapter: 'adapter',
|
||||
gateway: 'gateway',
|
||||
handler: 'handler',
|
||||
validator: 'validator',
|
||||
transformer: 'adapter',
|
||||
utility: 'utility',
|
||||
module: 'module',
|
||||
layer: 'module',
|
||||
feature: 'module',
|
||||
package: 'module',
|
||||
other: 'module',
|
||||
};
|
||||
|
||||
export function mapExternalSystemType(type: ExternalSystemType): SystemType {
|
||||
return EXTERNAL_SYSTEM_TYPE_MAP[type] ?? 'third_party';
|
||||
}
|
||||
|
||||
export function mapComponentType(type: C4ComponentType): ComponentType {
|
||||
return COMPONENT_TYPE_MAP[type] ?? 'module';
|
||||
}
|
||||
|
||||
/** shared C4RelationshipType → domain C4RelationshipType (C4ModelService vocabulary).
|
||||
* The two unions diverge (shared `sends`/`reads`/`writes`/`imports`/`stores_data_in`
|
||||
* vs domain `sends_to`/`reads_from`/`writes_to`); fold to the nearest domain verb. */
|
||||
const RELATIONSHIP_TYPE_MAP: Record<SharedC4RelationshipType, DomainC4RelationshipType> = {
|
||||
uses: 'uses',
|
||||
calls: 'calls',
|
||||
imports: 'depends_on',
|
||||
depends_on: 'depends_on',
|
||||
extends: 'extends',
|
||||
implements: 'implements',
|
||||
sends: 'sends_to',
|
||||
reads: 'reads_from',
|
||||
writes: 'writes_to',
|
||||
stores_data_in: 'writes_to',
|
||||
authenticates_with: 'authenticates_with',
|
||||
};
|
||||
|
||||
export function mapRelationshipType(type: SharedC4RelationshipType): DomainC4RelationshipType {
|
||||
return RELATIONSHIP_TYPE_MAP[type] ?? 'uses';
|
||||
}
|
||||
|
||||
/** Detected external systems → C4 ExternalSystemSpec[]. */
|
||||
export function toExternalSystemSpecs(
|
||||
externalSystems: DetectedExternalSystem[],
|
||||
): ExternalSystemSpec[] {
|
||||
return externalSystems.map((es) => ({
|
||||
name: es.name,
|
||||
type: mapExternalSystemType(es.type),
|
||||
technology: es.technology,
|
||||
relationshipDescription: es.relationship,
|
||||
}));
|
||||
}
|
||||
|
||||
/** Detected components → C4 ComponentSpec[]. */
|
||||
export function toComponentSpecs(components: DetectedComponent[]): ComponentSpec[] {
|
||||
return components.map((c) => ({
|
||||
name: c.name,
|
||||
type: mapComponentType(c.type),
|
||||
technology: c.technology,
|
||||
boundary: c.boundary,
|
||||
files: c.files,
|
||||
responsibilities: c.responsibilities,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Detected relationships → C4 ComponentRelationshipSpec[].
|
||||
*
|
||||
* `DetectedRelationship` references components by `id`; the service re-derives
|
||||
* ids by sanitizing the spec's `from`/`to` NAMES. So we translate ids back to
|
||||
* names via the component set; relationships whose endpoints aren't in the set
|
||||
* are dropped (they can't render) — surfaced by the caller's count delta.
|
||||
*/
|
||||
export function toComponentRelationshipSpecs(
|
||||
components: DetectedComponent[],
|
||||
relationships: DetectedRelationship[],
|
||||
): ComponentRelationshipSpec[] {
|
||||
const nameById = new Map(components.map((c) => [c.id, c.name]));
|
||||
const specs: ComponentRelationshipSpec[] = [];
|
||||
for (const rel of relationships) {
|
||||
const from = nameById.get(rel.sourceId);
|
||||
const to = nameById.get(rel.targetId);
|
||||
if (from === undefined || to === undefined) continue;
|
||||
specs.push({ from, to, type: mapRelationshipType(rel.type) });
|
||||
}
|
||||
return specs;
|
||||
}
|
||||
|
||||
/** Project + detected external systems → BuildContextRequest. */
|
||||
export function toContextRequest(
|
||||
project: C4ProjectInfo,
|
||||
externalSystems: DetectedExternalSystem[],
|
||||
): BuildContextRequest {
|
||||
return {
|
||||
systemName: project.name,
|
||||
systemDescription: project.description,
|
||||
externalSystems: toExternalSystemSpecs(externalSystems),
|
||||
};
|
||||
}
|
||||
|
||||
/** Project + detected external systems → BuildContainerRequest (single app container). */
|
||||
export function toContainerRequest(
|
||||
project: C4ProjectInfo,
|
||||
externalSystems: DetectedExternalSystem[],
|
||||
): BuildContainerRequest {
|
||||
const externalSpecs = toExternalSystemSpecs(externalSystems);
|
||||
return {
|
||||
systemName: project.name,
|
||||
containers: [
|
||||
{
|
||||
name: DEFAULT_CONTAINER_NAME,
|
||||
type: 'web_application',
|
||||
technology: 'TypeScript',
|
||||
description: 'Main application',
|
||||
},
|
||||
],
|
||||
externalSystems: externalSpecs,
|
||||
// The legacy bridge drew Rel(app, <external>) for each external system.
|
||||
dependencies: externalSpecs.map((es) => ({
|
||||
from: DEFAULT_CONTAINER_NAME,
|
||||
to: es.name,
|
||||
description: es.relationshipDescription,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/** Detected components + relationships → BuildComponentRequest. */
|
||||
export function toComponentRequest(
|
||||
components: DetectedComponent[],
|
||||
relationships: DetectedRelationship[],
|
||||
containerName: string = DEFAULT_CONTAINER_NAME,
|
||||
): BuildComponentRequest {
|
||||
return {
|
||||
containerName,
|
||||
components: toComponentSpecs(components),
|
||||
relationships: toComponentRelationshipSpecs(components, relationships),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* ADR-112 C2 — real component relationships from the Knowledge Graph.
|
||||
*
|
||||
* The detector models components as top-level `src/` directories but its edge
|
||||
* detection is a NAMING heuristic (`likelyHasRelationship`) that never reads the
|
||||
* code. This module folds the KnowledgeGraph's REAL file-level import/call edges
|
||||
* (extracted from the AST by the TS parser — no LLM needed) up to
|
||||
* component→component relationships.
|
||||
*
|
||||
* Pure aggregation + a resolver that drives the existing KG. The resolver
|
||||
* returns `null` on any miss so the bridge falls back to the heuristic — C2 can
|
||||
* only improve accuracy, never break generation.
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import type { DetectedComponent, DetectedRelationship } from '../../../../shared/c4-model';
|
||||
|
||||
/** Minimal shape of the KG `mapDependencies` result we consume. */
|
||||
export interface DependencyMapLike {
|
||||
nodes: Array<{ id: string; path: string }>;
|
||||
edges: Array<{ source: string; target: string }>;
|
||||
}
|
||||
|
||||
/** Minimal slice of the KnowledgeGraph the resolver needs (keeps this decoupled). */
|
||||
export interface KnowledgeGraphSlice {
|
||||
index(req: { paths: string[]; incremental?: boolean }): Promise<{ success: boolean }>;
|
||||
mapDependencies(req: {
|
||||
files: string[];
|
||||
direction: 'incoming' | 'outgoing' | 'both';
|
||||
depth?: number;
|
||||
}): Promise<{ success: boolean; value?: DependencyMapLike }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve real component relationships for a set of detected components.
|
||||
* Returns `null` when it cannot (no files, KG miss, no cross-component edges) so
|
||||
* the caller keeps the heuristic.
|
||||
*/
|
||||
export type RelationshipResolver = (
|
||||
components: DetectedComponent[],
|
||||
projectPath: string,
|
||||
) => Promise<DetectedRelationship[] | null>;
|
||||
|
||||
const normalize = (projectPath: string, p: string): string =>
|
||||
path.resolve(projectPath, p).replace(/\\/g, '/');
|
||||
|
||||
/**
|
||||
* PURE: fold file-level dependency edges into component→component relationships.
|
||||
* An edge counts only when both endpoints map to *different* components; weight =
|
||||
* number of underlying file edges. Exported for direct testing.
|
||||
*/
|
||||
export function aggregateDependencyMapToComponentRelationships(
|
||||
components: DetectedComponent[],
|
||||
dependencyMap: DependencyMapLike,
|
||||
projectPath: string,
|
||||
): DetectedRelationship[] {
|
||||
// normalized file path → componentId
|
||||
const fileToComponent = new Map<string, string>();
|
||||
for (const c of components) {
|
||||
for (const f of c.files ?? []) fileToComponent.set(normalize(projectPath, f), c.id);
|
||||
}
|
||||
const idToPath = new Map(dependencyMap.nodes.map((n) => [n.id, normalize(projectPath, n.path)]));
|
||||
|
||||
const componentOf = (nodeId: string): string | undefined => {
|
||||
const p = idToPath.get(nodeId);
|
||||
if (!p) return undefined;
|
||||
const exact = fileToComponent.get(p);
|
||||
if (exact) return exact;
|
||||
// Tolerate abs/rel divergence from the parser via a suffix match.
|
||||
for (const [file, cid] of fileToComponent) {
|
||||
if (p.endsWith(file) || file.endsWith(p)) return cid;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const byKey = new Map<string, DetectedRelationship>();
|
||||
for (const e of dependencyMap.edges) {
|
||||
const cs = componentOf(e.source);
|
||||
const ct = componentOf(e.target);
|
||||
if (!cs || !ct || cs === ct) continue;
|
||||
const key = `${cs}->${ct}`;
|
||||
const existing = byKey.get(key);
|
||||
if (existing) existing.weight = (existing.weight ?? 1) + 1;
|
||||
else byKey.set(key, { sourceId: cs, targetId: ct, type: 'depends_on', weight: 1 });
|
||||
}
|
||||
return [...byKey.values()];
|
||||
}
|
||||
|
||||
/**
|
||||
* A KG factory scoped to a project root. Building the KG per-call lets us set
|
||||
* the FileReader base directory to `projectPath`, so analyzing a repo outside
|
||||
* cwd doesn't trip the path-traversal guard.
|
||||
*/
|
||||
export type KnowledgeGraphFactory = (projectPath: string) => KnowledgeGraphSlice;
|
||||
|
||||
/**
|
||||
* Build a resolver backed by the existing KnowledgeGraph. It (incrementally)
|
||||
* indexes the component files so edges exist regardless of prior `aqe code
|
||||
* index`, maps their dependencies, and aggregates to component edges.
|
||||
*
|
||||
* Accepts a project-scoped factory (preferred) or a single pre-built KG.
|
||||
*/
|
||||
export function createKnowledgeGraphRelationshipResolver(
|
||||
kgOrFactory: KnowledgeGraphSlice | KnowledgeGraphFactory,
|
||||
opts: { autoIndex?: boolean } = {},
|
||||
): RelationshipResolver {
|
||||
const autoIndex = opts.autoIndex ?? true;
|
||||
const factory: KnowledgeGraphFactory =
|
||||
typeof kgOrFactory === 'function' ? kgOrFactory : () => kgOrFactory;
|
||||
return async (components, projectPath) => {
|
||||
const absFiles = components.flatMap((c) =>
|
||||
(c.files ?? []).map((f) => path.resolve(projectPath, f)),
|
||||
);
|
||||
if (absFiles.length === 0) return null;
|
||||
try {
|
||||
const kg = factory(projectPath);
|
||||
if (autoIndex) await kg.index({ paths: absFiles, incremental: true });
|
||||
const dep = await kg.mapDependencies({ files: absFiles, direction: 'both' });
|
||||
if (!dep.success || !dep.value) return null;
|
||||
const rels = aggregateDependencyMapToComponentRelationships(components, dep.value, projectPath);
|
||||
return rels.length > 0 ? rels : null;
|
||||
} catch {
|
||||
return null; // any failure → heuristic fallback
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -72,6 +72,12 @@ export interface KnowledgeGraphConfig {
|
||||
llmModelTier?: number;
|
||||
/** ADR-051: Max tokens for LLM responses */
|
||||
llmMaxTokens?: number;
|
||||
/**
|
||||
* ADR-112 C2: base directory for file reads. Defaults to cwd. Set to a
|
||||
* project root so analyzing a repo OUTSIDE cwd (e.g. `aqe code c4 /other`)
|
||||
* doesn't trip the FileReader path-traversal guard.
|
||||
*/
|
||||
basePath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,7 +158,7 @@ export class KnowledgeGraphService implements IKnowledgeGraphService {
|
||||
}
|
||||
|
||||
this.tsParser = new TypeScriptParser();
|
||||
this.fileReader = new FileReader();
|
||||
this.fileReader = new FileReader(this.config.basePath ? { basePath: this.config.basePath } : undefined);
|
||||
this.embedder = new NomicEmbedder({
|
||||
enableFallback: true,
|
||||
});
|
||||
@@ -524,7 +530,7 @@ Be precise and only report high-confidence findings.`,
|
||||
}
|
||||
|
||||
return { semanticRelationships: [], designPatterns: [], architecturalBoundaries: [], dependencyImpacts: [] };
|
||||
} catch (error) {
|
||||
} catch {
|
||||
logger.warn('LLM relationship extraction failed:');
|
||||
return { semanticRelationships: [], designPatterns: [], architecturalBoundaries: [], dependencyImpacts: [] };
|
||||
}
|
||||
@@ -653,7 +659,7 @@ Return JSON: { "rankedIds": ["id1", "id2", ...], "insights": ["insight1", "insig
|
||||
}
|
||||
|
||||
return { enhancedResults: results, insights: [] };
|
||||
} catch (error) {
|
||||
} catch {
|
||||
logger.warn('LLM query enhancement failed:');
|
||||
return { enhancedResults: results, insights: [] };
|
||||
}
|
||||
|
||||
@@ -40,9 +40,16 @@ import {
|
||||
isCacheValid,
|
||||
sanitizeId,
|
||||
inferComponentType,
|
||||
IC4DiagramGenerator,
|
||||
assessC4Confidence,
|
||||
} from '../../../shared/c4-model';
|
||||
import { safeJsonParse } from '../../../shared/safe-json.js';
|
||||
import { C4ModelService } from './c4-model';
|
||||
import {
|
||||
toContextRequest,
|
||||
toContainerRequest,
|
||||
toComponentRequest,
|
||||
} from './c4-model/from-detected';
|
||||
import type { RelationshipResolver } from './c4-model/kg-relationships';
|
||||
|
||||
// ============================================================================
|
||||
// Configuration
|
||||
@@ -57,6 +64,18 @@ export interface ProductFactorsBridgeConfig {
|
||||
excludePatterns: string[];
|
||||
/** Maximum files to analyze */
|
||||
maxFiles: number;
|
||||
/**
|
||||
* ADR-112 C2: optional resolver that returns REAL component relationships
|
||||
* from the Knowledge Graph. When it yields edges, they replace the naming
|
||||
* heuristic; otherwise the heuristic stands. Off by default (no resolver).
|
||||
*/
|
||||
relationshipResolver?: RelationshipResolver;
|
||||
/**
|
||||
* ADR-112: persist diagram embeddings so they're semantically searchable
|
||||
* (`qe/code/c4 search`). Off by default — the internal product-factors path
|
||||
* stays fast/offline; surfaces that expose search (MCP) turn it on.
|
||||
*/
|
||||
enableC4Embeddings?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: ProductFactorsBridgeConfig = {
|
||||
@@ -176,11 +195,13 @@ export interface IProductFactorsBridge {
|
||||
const logger = LoggerFactory.create('code-intelligence/product-factors-bridge');
|
||||
|
||||
export class ProductFactorsBridgeService
|
||||
implements IProductFactorsBridge, IC4DiagramGenerator
|
||||
implements IProductFactorsBridge
|
||||
{
|
||||
private readonly config: ProductFactorsBridgeConfig;
|
||||
private initialized = false;
|
||||
private eventSubscriptions: Subscription[] = [];
|
||||
/** ADR-112: the consolidated render+analyze+store engine (lazily built). */
|
||||
private c4Service?: C4ModelService;
|
||||
|
||||
constructor(
|
||||
private readonly eventBus: EventBus,
|
||||
@@ -190,6 +211,20 @@ export class ProductFactorsBridgeService
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* ADR-112: lazily construct the shared C4ModelService.
|
||||
* Embeddings are OFF on this internal product-factors path (keeps it offline +
|
||||
* fast); the CLI/MCP surfaces opt embeddings in when semantic search is wanted.
|
||||
*/
|
||||
private getC4Service(): C4ModelService {
|
||||
if (!this.c4Service) {
|
||||
this.c4Service = new C4ModelService(this.memory, {
|
||||
enableEmbeddings: this.config.enableC4Embeddings ?? false,
|
||||
});
|
||||
}
|
||||
return this.c4Service;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Lifecycle
|
||||
// ==========================================================================
|
||||
@@ -234,7 +269,7 @@ export class ProductFactorsBridgeService
|
||||
}
|
||||
|
||||
private async handleKnowledgeGraphUpdated(
|
||||
event: DomainEvent
|
||||
_event: DomainEvent
|
||||
): Promise<void> {
|
||||
// Invalidate relevant caches when knowledge graph is updated
|
||||
logger.info(
|
||||
@@ -334,23 +369,22 @@ export class ProductFactorsBridgeService
|
||||
couplingAnalysis = this.analyzeCoupling(components, relationships);
|
||||
}
|
||||
|
||||
// Generate diagrams
|
||||
// Generate diagrams. ADR-112: the three C4 levels render through the
|
||||
// consolidated C4ModelService (richer + stored + searchable); the bridge
|
||||
// keeps only detection. Inline generators remain as a safety fallback.
|
||||
const diagrams: C4Diagrams = {};
|
||||
|
||||
if (request.includeContext !== false) {
|
||||
diagrams.context = this.generateContextDiagram(projectInfo, externalSystems);
|
||||
diagrams.context = await this.renderContext(projectInfo, externalSystems);
|
||||
}
|
||||
|
||||
if (request.includeContainer !== false) {
|
||||
diagrams.container = this.generateContainerDiagram(
|
||||
projectInfo,
|
||||
externalSystems
|
||||
);
|
||||
diagrams.container = await this.renderContainer(projectInfo, externalSystems);
|
||||
}
|
||||
|
||||
if (request.includeComponent !== false && components.length > 0) {
|
||||
diagrams.component = this.generateComponentDiagram(
|
||||
projectInfo.name,
|
||||
diagrams.component = await this.renderComponent(
|
||||
projectInfo,
|
||||
components,
|
||||
relationships
|
||||
);
|
||||
@@ -363,16 +397,24 @@ export class ProductFactorsBridgeService
|
||||
);
|
||||
}
|
||||
|
||||
const filesAnalyzed = components.reduce((sum, c) => sum + c.files.length, 0);
|
||||
const metadata: C4DiagramMetadata = {
|
||||
projectName: projectInfo.name,
|
||||
projectDescription: projectInfo.description,
|
||||
generatedAt: new Date(),
|
||||
source: 'codebase-analysis',
|
||||
analysisMetadata: {
|
||||
filesAnalyzed: components.reduce((sum, c) => sum + c.files.length, 0),
|
||||
filesAnalyzed,
|
||||
componentsDetected: components.length,
|
||||
externalSystemsDetected: externalSystems.length,
|
||||
analysisTimeMs: Date.now() - startTime,
|
||||
// ADR-112: deterministic confidence gate — never present a wrong diagram as truth.
|
||||
confidence: assessC4Confidence({
|
||||
componentsDetected: components.length,
|
||||
relationshipsDetected: relationships.length,
|
||||
externalSystemsDetected: externalSystems.length,
|
||||
filesAnalyzed,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -391,96 +433,57 @@ export class ProductFactorsBridgeService
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// IC4DiagramGenerator Implementation
|
||||
// ADR-112: render via the consolidated C4ModelService (the single engine).
|
||||
// On failure we FAIL LOUD — propagate the error so the caller surfaces it —
|
||||
// rather than silently degrading to a weaker inline diagram. A masked render
|
||||
// failure is worse than a surfaced one (ADR-050: no silent degradation).
|
||||
// ==========================================================================
|
||||
|
||||
generateContextDiagram(
|
||||
project: C4ProjectInfo,
|
||||
private async renderContext(
|
||||
projectInfo: C4ProjectInfo,
|
||||
externalSystems: DetectedExternalSystem[]
|
||||
): string {
|
||||
let mermaid = `C4Context
|
||||
title System Context diagram for ${project.name}
|
||||
|
||||
Person(user, "User", "A user of the system")
|
||||
System(system, "${project.name}", "${project.description}")
|
||||
`;
|
||||
|
||||
for (const sys of externalSystems) {
|
||||
mermaid += ` System_Ext(${sys.id}, "${sys.name}", "${sys.type}")
|
||||
`;
|
||||
): Promise<string> {
|
||||
const res = await this.getC4Service().buildContext(
|
||||
toContextRequest(projectInfo, externalSystems)
|
||||
);
|
||||
if (!res.success) {
|
||||
logger.error('[ProductFactorsBridge] C4 context render failed', res.error);
|
||||
throw res.error;
|
||||
}
|
||||
|
||||
mermaid += `
|
||||
Rel(user, system, "Uses")
|
||||
`;
|
||||
|
||||
for (const sys of externalSystems) {
|
||||
mermaid += ` Rel(system, ${sys.id}, "${sys.relationship}")
|
||||
`;
|
||||
}
|
||||
|
||||
return mermaid;
|
||||
return res.value.mermaid;
|
||||
}
|
||||
|
||||
generateContainerDiagram(
|
||||
project: C4ProjectInfo,
|
||||
private async renderContainer(
|
||||
projectInfo: C4ProjectInfo,
|
||||
externalSystems: DetectedExternalSystem[]
|
||||
): string {
|
||||
let mermaid = `C4Container
|
||||
title Container diagram for ${project.name}
|
||||
|
||||
Person(user, "User", "A user of the system")
|
||||
|
||||
Container_Boundary(c1, "${project.name}") {
|
||||
Container(app, "Application", "TypeScript", "Main application")
|
||||
}
|
||||
`;
|
||||
|
||||
for (const sys of externalSystems) {
|
||||
mermaid += ` System_Ext(${sys.id}, "${sys.name}", "${sys.type}")
|
||||
`;
|
||||
): Promise<string> {
|
||||
const res = await this.getC4Service().buildContainer(
|
||||
toContainerRequest(projectInfo, externalSystems)
|
||||
);
|
||||
if (!res.success) {
|
||||
logger.error('[ProductFactorsBridge] C4 container render failed', res.error);
|
||||
throw res.error;
|
||||
}
|
||||
|
||||
mermaid += `
|
||||
Rel(user, app, "Uses")
|
||||
`;
|
||||
|
||||
for (const sys of externalSystems) {
|
||||
mermaid += ` Rel(app, ${sys.id}, "${sys.relationship}")
|
||||
`;
|
||||
}
|
||||
|
||||
return mermaid;
|
||||
return res.value.mermaid;
|
||||
}
|
||||
|
||||
generateComponentDiagram(
|
||||
projectName: string,
|
||||
private async renderComponent(
|
||||
projectInfo: C4ProjectInfo,
|
||||
components: DetectedComponent[],
|
||||
relationships: DetectedRelationship[]
|
||||
): string {
|
||||
let mermaid = `C4Component
|
||||
title Component diagram for ${projectName}
|
||||
|
||||
Container_Boundary(app, "Application") {
|
||||
`;
|
||||
|
||||
for (const comp of components) {
|
||||
const responsibility = comp.responsibilities?.[0] || '';
|
||||
mermaid += ` Component(${comp.id}, "${comp.name}", "${comp.technology || 'TypeScript'}", "${responsibility}")
|
||||
`;
|
||||
): Promise<string> {
|
||||
const res = await this.getC4Service().buildComponent(
|
||||
toComponentRequest(components, relationships)
|
||||
);
|
||||
if (!res.success) {
|
||||
logger.error('[ProductFactorsBridge] C4 component render failed', res.error);
|
||||
throw res.error;
|
||||
}
|
||||
|
||||
mermaid += ` }
|
||||
`;
|
||||
|
||||
for (const rel of relationships) {
|
||||
mermaid += ` Rel(${rel.sourceId}, ${rel.targetId}, "${rel.type}")
|
||||
`;
|
||||
}
|
||||
|
||||
return mermaid;
|
||||
return res.value.mermaid;
|
||||
}
|
||||
|
||||
// The dependency graph is NOT a C4 level and C4ModelService does not render
|
||||
// it, so this stays as the bridge's own renderer.
|
||||
generateDependencyGraph(
|
||||
components: DetectedComponent[],
|
||||
relationships: DetectedRelationship[]
|
||||
@@ -652,6 +655,19 @@ export class ProductFactorsBridgeService
|
||||
logger.error('Component analysis failed:', error instanceof Error ? error : undefined);
|
||||
}
|
||||
|
||||
// ADR-112 C2: prefer REAL KG-derived edges (AST import/call graph) over the
|
||||
// naming heuristic. The resolver returns null on any miss → heuristic stands.
|
||||
if (this.config.relationshipResolver && components.length > 0) {
|
||||
try {
|
||||
const resolved = await this.config.relationshipResolver(components, projectPath);
|
||||
if (resolved && resolved.length > 0) {
|
||||
return { components, relationships: resolved };
|
||||
}
|
||||
} catch (error) {
|
||||
logger.debug(`KG relationship resolver failed; using heuristic: ${toError(error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { components, relationships };
|
||||
}
|
||||
|
||||
@@ -693,7 +709,7 @@ export class ProductFactorsBridgeService
|
||||
files.push(relativePath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
// Non-critical: permission errors when scanning directories
|
||||
logger.debug('Directory scan error:');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Agentic QE v3 - C4 Architecture MCP Tool (ADR-112)
|
||||
*
|
||||
* qe/code/c4 - Generate C4 architecture diagrams (Context/Container/Component,
|
||||
* Mermaid) from codebase analysis, plus semantic search over stored diagrams.
|
||||
*
|
||||
* Wraps the SAME pipeline the CLI `aqe code c4` drives:
|
||||
* ProductFactorsBridgeService (detect) → C4ModelService (render+store) →
|
||||
* deterministic confidence gate. This keeps MCP-CLI parity by construction.
|
||||
*/
|
||||
|
||||
import { MCPToolBase, MCPToolConfig, MCPToolContext, MCPToolSchema, getSharedMemoryBackend } from '../base';
|
||||
import { ToolResult } from '../../types';
|
||||
import { ProductFactorsBridgeService } from '../../../domains/code-intelligence/services/product-factors-bridge';
|
||||
import { C4ModelService } from '../../../domains/code-intelligence/services/c4-model';
|
||||
import { KnowledgeGraphService } from '../../../domains/code-intelligence/services/knowledge-graph';
|
||||
import { createKnowledgeGraphRelationshipResolver } from '../../../domains/code-intelligence/services/c4-model/kg-relationships';
|
||||
import { InMemoryEventBus } from '../../../kernel/event-bus';
|
||||
import type { C4Diagrams, C4ConfidenceAssessment } from '../../../shared/c4-model';
|
||||
import { toErrorMessage } from '../../../shared/error-utils.js';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export type C4Level = 'context' | 'container' | 'component' | 'all';
|
||||
|
||||
export interface CodeC4Params {
|
||||
action: 'generate' | 'search';
|
||||
projectPath?: string;
|
||||
level?: C4Level;
|
||||
query?: string;
|
||||
limit?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface C4GenerateResult {
|
||||
diagrams: C4Diagrams;
|
||||
confidence?: C4ConfidenceAssessment;
|
||||
componentsDetected: number;
|
||||
externalSystemsDetected: number;
|
||||
relationshipsDetected: number;
|
||||
circularDependencies: number;
|
||||
}
|
||||
|
||||
export interface C4SearchHit {
|
||||
key: string;
|
||||
type: 'context' | 'container' | 'component';
|
||||
title: string;
|
||||
score: number;
|
||||
preview: string;
|
||||
}
|
||||
|
||||
export interface C4SearchResult {
|
||||
results: C4SearchHit[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface CodeC4Result {
|
||||
action: string;
|
||||
generateResult?: C4GenerateResult;
|
||||
searchResult?: C4SearchResult;
|
||||
}
|
||||
|
||||
const want = (level: C4Level, l: string): boolean => level === 'all' || level === l;
|
||||
|
||||
// ============================================================================
|
||||
// Tool Implementation
|
||||
// ============================================================================
|
||||
|
||||
export class CodeC4Tool extends MCPToolBase<CodeC4Params, CodeC4Result> {
|
||||
readonly config: MCPToolConfig = {
|
||||
name: 'qe/code/c4',
|
||||
description:
|
||||
'Generate C4 architecture diagrams (Context/Container/Component, Mermaid) from a codebase with a deterministic confidence gate, or semantically search previously generated diagrams.',
|
||||
domain: 'code-intelligence',
|
||||
schema: CODE_C4_SCHEMA,
|
||||
streaming: false,
|
||||
timeout: 300000,
|
||||
};
|
||||
|
||||
private bridge: ProductFactorsBridgeService | null = null;
|
||||
private searchService: C4ModelService | null = null;
|
||||
|
||||
private async getBridge(context: MCPToolContext): Promise<ProductFactorsBridgeService> {
|
||||
if (!this.bridge) {
|
||||
const memory = context.memory ?? (await getSharedMemoryBackend());
|
||||
// ADR-112 C2: KG-backed resolver (project-scoped) so MCP matches the CLI.
|
||||
// publishEvents:false — the MCP path doesn't need the cross-domain event.
|
||||
this.bridge = new ProductFactorsBridgeService(new InMemoryEventBus(), memory, {
|
||||
publishEvents: false,
|
||||
// ADR-112: embed generated diagrams so `search` can find them.
|
||||
enableC4Embeddings: true,
|
||||
relationshipResolver: createKnowledgeGraphRelationshipResolver(
|
||||
(projectPath) => new KnowledgeGraphService(memory, { basePath: projectPath }),
|
||||
),
|
||||
});
|
||||
}
|
||||
return this.bridge;
|
||||
}
|
||||
|
||||
/** Embeddings ON here so stored diagrams are semantically searchable. */
|
||||
private async getSearchService(context: MCPToolContext): Promise<C4ModelService> {
|
||||
if (!this.searchService) {
|
||||
const memory = context.memory ?? (await getSharedMemoryBackend());
|
||||
this.searchService = new C4ModelService(memory, { enableEmbeddings: true });
|
||||
}
|
||||
return this.searchService;
|
||||
}
|
||||
|
||||
async execute(params: CodeC4Params, context: MCPToolContext): Promise<ToolResult<CodeC4Result>> {
|
||||
const { action, projectPath = '.', level = 'all', query, limit = 10 } = params;
|
||||
|
||||
try {
|
||||
if (this.isAborted(context)) {
|
||||
return { success: false, error: 'Operation aborted' };
|
||||
}
|
||||
|
||||
const result: CodeC4Result = { action };
|
||||
|
||||
switch (action) {
|
||||
case 'generate': {
|
||||
const bridge = await this.getBridge(context);
|
||||
const res = await bridge.requestC4Diagrams({
|
||||
projectPath,
|
||||
includeContext: want(level, 'context'),
|
||||
includeContainer: want(level, 'container'),
|
||||
includeComponent: want(level, 'component'),
|
||||
includeDependency: level === 'all',
|
||||
analyzeComponents: true,
|
||||
detectExternalSystems: true,
|
||||
analyzeCoupling: true,
|
||||
});
|
||||
if (!res.success) {
|
||||
return { success: false, error: toErrorMessage(res.error) };
|
||||
}
|
||||
const c4 = res.value;
|
||||
result.generateResult = {
|
||||
diagrams: c4.diagrams,
|
||||
confidence: c4.metadata.analysisMetadata?.confidence,
|
||||
componentsDetected: c4.components.length,
|
||||
externalSystemsDetected: c4.externalSystems.length,
|
||||
relationshipsDetected: c4.relationships.length,
|
||||
circularDependencies: (c4.couplingAnalysis ?? []).filter((co) => co.isCircular).length,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
case 'search': {
|
||||
if (!query) {
|
||||
return { success: false, error: 'Query is required for search action' };
|
||||
}
|
||||
const service = await this.getSearchService(context);
|
||||
const res = await service.searchDiagrams(query, limit);
|
||||
if (!res.success) {
|
||||
return { success: false, error: toErrorMessage(res.error) };
|
||||
}
|
||||
result.searchResult = {
|
||||
results: res.value.map((r) => ({ key: r.key, type: r.type, title: r.title, score: r.score, preview: r.preview })),
|
||||
total: res.value.length,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return { success: false, error: `Unknown action: ${action}` };
|
||||
}
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
return { success: false, error: toErrorMessage(error) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Schema
|
||||
// ============================================================================
|
||||
|
||||
const CODE_C4_SCHEMA: MCPToolSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
description: 'C4 action to perform',
|
||||
enum: ['generate', 'search'],
|
||||
},
|
||||
projectPath: {
|
||||
type: 'string',
|
||||
description: 'Project root to analyze (generate action). Defaults to the current directory.',
|
||||
},
|
||||
level: {
|
||||
type: 'string',
|
||||
description: 'C4 level to generate (generate action)',
|
||||
enum: ['context', 'container', 'component', 'all'],
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Semantic search query over stored diagrams (search action)',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Max search results (search action)',
|
||||
},
|
||||
},
|
||||
required: ['action'],
|
||||
};
|
||||
@@ -144,6 +144,14 @@ export {
|
||||
type DependencyResult,
|
||||
} from './code-intelligence/analyze';
|
||||
|
||||
export {
|
||||
CodeC4Tool,
|
||||
type CodeC4Params,
|
||||
type CodeC4Result,
|
||||
type C4GenerateResult,
|
||||
type C4SearchResult,
|
||||
} from './code-intelligence/c4';
|
||||
|
||||
// ============================================================================
|
||||
// Security Compliance Domain
|
||||
// ============================================================================
|
||||
|
||||
@@ -17,6 +17,7 @@ import { DefectPredictTool } from './defect-intelligence/predict';
|
||||
import { RequirementsValidateTool } from './requirements-validation/validate';
|
||||
import { QualityCriteriaTool } from './requirements-validation/quality-criteria';
|
||||
import { CodeAnalyzeTool } from './code-intelligence/analyze';
|
||||
import { CodeC4Tool } from './code-intelligence/c4';
|
||||
import { SecurityScanTool } from './security-compliance/scan';
|
||||
import { ContractValidateTool } from './contract-testing/validate';
|
||||
import { VisualCompareTool, A11yAuditTool } from './visual-accessibility';
|
||||
@@ -66,6 +67,7 @@ export const QE_TOOL_NAMES = {
|
||||
|
||||
// Code Intelligence
|
||||
CODE_ANALYZE: 'qe/code/analyze',
|
||||
CODE_C4: 'qe/code/c4',
|
||||
|
||||
// Security Compliance
|
||||
SECURITY_SCAN: 'qe/security/scan',
|
||||
@@ -151,6 +153,7 @@ export const QE_TOOLS: MCPToolBase[] = [
|
||||
|
||||
// Code Intelligence Domain
|
||||
new CodeAnalyzeTool(),
|
||||
new CodeC4Tool(),
|
||||
|
||||
// Security Compliance Domain
|
||||
new SecurityScanTool(),
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* ADR-112 — Deterministic C4 confidence gate.
|
||||
*
|
||||
* Pure, code-only (NO LLM in the gate — same discipline as ADR-111's accept
|
||||
* gate). Turns the code-intelligence detector's known limits (the
|
||||
* qe-code-intelligence skill records ~18% success on complex queries and
|
||||
* degradation above ~50K LOC) into an explicit, surfaced signal so a
|
||||
* confident-but-wrong diagram is never presented as ground truth.
|
||||
*/
|
||||
|
||||
export type C4Confidence = 'high' | 'medium' | 'low';
|
||||
|
||||
export interface C4ConfidenceInputs {
|
||||
/** Number of components the detector found. */
|
||||
componentsDetected: number;
|
||||
/** Number of component relationships (edges) detected. */
|
||||
relationshipsDetected: number;
|
||||
/** Number of external systems detected. */
|
||||
externalSystemsDetected: number;
|
||||
/** Number of source files analyzed. */
|
||||
filesAnalyzed: number;
|
||||
/** Total lines of code, if known (e.g. from MetricCollector). Optional. */
|
||||
totalLoc?: number;
|
||||
}
|
||||
|
||||
export interface C4ConfidenceAssessment {
|
||||
/** Bucketed level for quick display. */
|
||||
level: C4Confidence;
|
||||
/** Continuous score in [0,1] the level is derived from. */
|
||||
score: number;
|
||||
/** Human-readable reasons (always populated). */
|
||||
reasons: string[];
|
||||
}
|
||||
|
||||
/** Repo size (LOC) beyond which the skill records detection degradation. */
|
||||
export const C4_LOC_DEGRADE_THRESHOLD = 50_000;
|
||||
|
||||
const clamp01 = (x: number) => Math.max(0, Math.min(1, x));
|
||||
|
||||
/**
|
||||
* Assess how much to trust an auto-generated C4 diagram. Deterministic: the same
|
||||
* inputs always yield the same level + reasons.
|
||||
*/
|
||||
export function assessC4Confidence(inputs: C4ConfidenceInputs): C4ConfidenceAssessment {
|
||||
const { componentsDetected, relationshipsDetected, externalSystemsDetected, totalLoc } = inputs;
|
||||
const reasons: string[] = [];
|
||||
|
||||
// Empty diagram — nothing to trust.
|
||||
if (componentsDetected === 0) {
|
||||
return {
|
||||
level: 'low',
|
||||
score: 0,
|
||||
reasons: ['No components detected — the diagram is effectively empty; verify against the source.'],
|
||||
};
|
||||
}
|
||||
|
||||
let score = 0.5;
|
||||
|
||||
// Relationships are the weakest link in heuristic detection. Their presence is
|
||||
// the strongest signal that the STRUCTURE (not just a node list) is real.
|
||||
const relDensity = relationshipsDetected / Math.max(1, componentsDetected);
|
||||
if (relationshipsDetected === 0 && componentsDetected > 1) {
|
||||
score -= 0.25;
|
||||
reasons.push('No relationships detected between components — edges are heuristic-only or missing; the structure is unverified.');
|
||||
} else if (relDensity >= 0.5) {
|
||||
score += 0.2;
|
||||
reasons.push(`${relationshipsDetected} relationship(s) detected across ${componentsDetected} components.`);
|
||||
} else {
|
||||
score += 0.05;
|
||||
reasons.push(`${relationshipsDetected} relationship(s) detected (sparse) across ${componentsDetected} components.`);
|
||||
}
|
||||
|
||||
// External systems detected → the Platform/Interfaces picture is grounded.
|
||||
if (externalSystemsDetected > 0) {
|
||||
score += 0.1;
|
||||
reasons.push(`${externalSystemsDetected} external system(s) detected from dependencies.`);
|
||||
}
|
||||
|
||||
// Repo size vs the known degradation threshold.
|
||||
if (totalLoc !== undefined && totalLoc > C4_LOC_DEGRADE_THRESHOLD) {
|
||||
score -= 0.3;
|
||||
reasons.push(
|
||||
`Repository is large (${totalLoc.toLocaleString()} LOC > ~${C4_LOC_DEGRADE_THRESHOLD / 1000}K) — detection accuracy degrades; treat the diagram as a draft.`,
|
||||
);
|
||||
}
|
||||
|
||||
// A healthy component count adds confidence; a single component is thin.
|
||||
if (componentsDetected >= 5) {
|
||||
score += 0.1;
|
||||
} else if (componentsDetected === 1) {
|
||||
score -= 0.05;
|
||||
reasons.push('Only one component detected — likely an under-segmented view.');
|
||||
}
|
||||
|
||||
score = clamp01(score);
|
||||
const level: C4Confidence = score >= 0.7 ? 'high' : score >= 0.4 ? 'medium' : 'low';
|
||||
|
||||
if (level !== 'high' && !reasons.some((r) => r.includes('draft') || r.includes('verify'))) {
|
||||
reasons.push('Auto-generated draft — verify against the source before relying on it.');
|
||||
}
|
||||
|
||||
return { level, score: Math.round(score * 1000) / 1000, reasons };
|
||||
}
|
||||
@@ -9,6 +9,9 @@
|
||||
* https://c4model.com/
|
||||
*/
|
||||
|
||||
export * from './confidence';
|
||||
import type { C4ConfidenceAssessment } from './confidence';
|
||||
|
||||
// ============================================================================
|
||||
// C4 Diagram Types
|
||||
// ============================================================================
|
||||
@@ -64,6 +67,8 @@ export interface C4AnalysisMetadata {
|
||||
externalSystemsDetected: number;
|
||||
/** Analysis duration in milliseconds */
|
||||
analysisTimeMs: number;
|
||||
/** ADR-112: deterministic confidence gate for the generated diagrams */
|
||||
confidence?: C4ConfidenceAssessment;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* ADR-112 C2 — REAL Knowledge Graph end-to-end.
|
||||
*
|
||||
* Proves the resolver turns actual `import` statements into component→component
|
||||
* relationships using the live KnowledgeGraphService (AST/TS parser, no LLM),
|
||||
* and that the bridge then surfaces them with raised confidence.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { createMockMemory, createMockEventBus } from '../../mocks';
|
||||
import { KnowledgeGraphService } from '../../../src/domains/code-intelligence/services/knowledge-graph';
|
||||
import { createKnowledgeGraphRelationshipResolver } from '../../../src/domains/code-intelligence/services/c4-model/kg-relationships';
|
||||
import { ProductFactorsBridgeService } from '../../../src/domains/code-intelligence/services/product-factors-bridge';
|
||||
|
||||
let tmp: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'c4-c2-'));
|
||||
fs.writeFileSync(path.join(tmp, 'package.json'), JSON.stringify({ name: 'svc', description: 'svc' }));
|
||||
// controller imports service; service imports repository → a real chain.
|
||||
fs.mkdirSync(path.join(tmp, 'src', 'controllers'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tmp, 'src', 'services'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tmp, 'src', 'repositories'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmp, 'src', 'repositories', 'order.ts'), 'export class OrderRepository { save() {} }\n');
|
||||
fs.writeFileSync(path.join(tmp, 'src', 'services', 'order.ts'),
|
||||
"import { OrderRepository } from '../repositories/order';\nexport class OrderService { constructor(private r = new OrderRepository()) {} }\n");
|
||||
fs.writeFileSync(path.join(tmp, 'src', 'controllers', 'order.ts'),
|
||||
"import { OrderService } from '../services/order';\nexport class OrderController { constructor(private s = new OrderService()) {} }\n");
|
||||
});
|
||||
|
||||
afterEach(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
||||
|
||||
describe('ADR-112 C2 — real KG relationship resolution', () => {
|
||||
it('should_derive_component_relationships_from_real_imports', async () => {
|
||||
// Project-scoped factory (mirrors coordinator/MCP wiring) so FileReader's
|
||||
// base directory is the analyzed repo, not cwd.
|
||||
const resolver = createKnowledgeGraphRelationshipResolver(
|
||||
(projectPath) => new KnowledgeGraphService(createMockMemory(), { basePath: projectPath }),
|
||||
);
|
||||
|
||||
const components = [
|
||||
{ id: 'controllers', name: 'Controllers', type: 'controller' as const, files: ['src/controllers/order.ts'] },
|
||||
{ id: 'services', name: 'Services', type: 'service' as const, files: ['src/services/order.ts'] },
|
||||
{ id: 'repositories', name: 'Repositories', type: 'repository' as const, files: ['src/repositories/order.ts'] },
|
||||
];
|
||||
|
||||
const rels = await resolver(components, tmp);
|
||||
|
||||
// The TS parser must have produced at least one REAL cross-component edge.
|
||||
expect(rels, 'resolver should find real import edges').not.toBeNull();
|
||||
expect(rels!.length).toBeGreaterThan(0);
|
||||
// Every derived edge must connect two distinct detected components.
|
||||
const ids = new Set(components.map((c) => c.id));
|
||||
for (const r of rels!) {
|
||||
expect(ids.has(r.sourceId)).toBe(true);
|
||||
expect(ids.has(r.targetId)).toBe(true);
|
||||
expect(r.sourceId).not.toBe(r.targetId);
|
||||
}
|
||||
});
|
||||
|
||||
it('should_surface_KG_relationships_through_the_bridge_with_confidence', async () => {
|
||||
const bridge = new ProductFactorsBridgeService(createMockEventBus(), createMockMemory(), {
|
||||
publishEvents: false,
|
||||
relationshipResolver: createKnowledgeGraphRelationshipResolver(
|
||||
(projectPath) => new KnowledgeGraphService(createMockMemory(), { basePath: projectPath }),
|
||||
),
|
||||
});
|
||||
|
||||
const res = await bridge.requestC4Diagrams({
|
||||
projectPath: tmp,
|
||||
analyzeComponents: true,
|
||||
includeComponent: true,
|
||||
});
|
||||
|
||||
expect(res.success).toBe(true);
|
||||
if (!res.success) return;
|
||||
// Real edges present, and KG-DERIVED (type 'depends_on'), not the naming
|
||||
// heuristic (which emits type 'uses'). This proves C2 is actually engaged.
|
||||
expect(res.value.relationships.length).toBeGreaterThan(0);
|
||||
expect(res.value.relationships.some((r) => r.type === 'depends_on')).toBe(true);
|
||||
// Confidence reflects that relationships were detected.
|
||||
const confidence = res.value.metadata.analysisMetadata?.confidence;
|
||||
expect(confidence).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* ADR-112 — qe/code/c4 is actually SERVED through the protocol-server bridge.
|
||||
*
|
||||
* The protocol server registers QE_TOOLS via registerMissingQETools(). This
|
||||
* proves the new tool reaches that path with a definition + working handler
|
||||
* (CLAUDE.md: MCP fixes must be verified through the server path, not just the
|
||||
* in-process tool class).
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { registerMissingQETools } from '../../../src/mcp/qe-tool-bridge';
|
||||
|
||||
interface CapturedEntry {
|
||||
definition: { name: string; description?: string; parameters?: unknown[] };
|
||||
handler: (params: Record<string, unknown>) => Promise<unknown>;
|
||||
}
|
||||
|
||||
let tmp: string;
|
||||
beforeEach(() => {
|
||||
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'c4-bridge-'));
|
||||
fs.writeFileSync(path.join(tmp, 'package.json'), JSON.stringify({ name: 'svc', dependencies: { pg: '^8.0.0' } }));
|
||||
fs.mkdirSync(path.join(tmp, 'src', 'services'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmp, 'src', 'services', 'a.ts'), 'export class A {}\n');
|
||||
});
|
||||
afterEach(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
||||
|
||||
describe('qe/code/c4 served via the QE tool bridge', () => {
|
||||
it('should_register_qe_code_c4_with_a_definition_and_handler', async () => {
|
||||
const entries: CapturedEntry[] = [];
|
||||
registerMissingQETools((e) => entries.push(e as unknown as CapturedEntry));
|
||||
|
||||
const c4 = entries.find((e) => e.definition.name === 'qe/code/c4');
|
||||
expect(c4, 'qe/code/c4 must be bridged into the server').toBeDefined();
|
||||
expect(typeof c4!.handler).toBe('function');
|
||||
expect(c4!.definition.parameters?.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should_execute_generate_through_the_bridged_handler', async () => {
|
||||
const entries: CapturedEntry[] = [];
|
||||
registerMissingQETools((e) => entries.push(e as unknown as CapturedEntry));
|
||||
const c4 = entries.find((e) => e.definition.name === 'qe/code/c4')!;
|
||||
|
||||
const out = (await c4.handler({ action: 'generate', projectPath: tmp, level: 'context' })) as {
|
||||
success?: boolean; data?: { generateResult?: { diagrams?: { context?: string } } };
|
||||
generateResult?: { diagrams?: { context?: string } };
|
||||
};
|
||||
// The bridge may unwrap ToolResult; accept either shape.
|
||||
const ctxDiagram = out?.data?.generateResult?.diagrams?.context ?? out?.generateResult?.diagrams?.context;
|
||||
expect(ctxDiagram, JSON.stringify(out).slice(0, 300)).toContain('C4Context');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* ADR-112 C1 — consolidation parity proof.
|
||||
*
|
||||
* The bridge keeps detection; rendering moves to C4ModelService. This guards the
|
||||
* seam end-to-end (detected → mapping → service → Mermaid): every detected
|
||||
* element must survive, and the output must carry the C4 tokens the
|
||||
* product-factors architecture-parser keys on (Person(/Container(/Component().
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { createMockMemory } from '../../../mocks';
|
||||
import type {
|
||||
C4ProjectInfo,
|
||||
DetectedExternalSystem,
|
||||
DetectedComponent,
|
||||
DetectedRelationship,
|
||||
} from '../../../../src/shared/c4-model';
|
||||
import { C4ModelService } from '../../../../src/domains/code-intelligence/services/c4-model';
|
||||
import {
|
||||
toContextRequest,
|
||||
toContainerRequest,
|
||||
toComponentRequest,
|
||||
} from '../../../../src/domains/code-intelligence/services/c4-model/from-detected';
|
||||
|
||||
const project: C4ProjectInfo = { name: 'CheckoutApp', description: 'Order checkout service' };
|
||||
const externalSystems: DetectedExternalSystem[] = [
|
||||
{ id: 'pg', name: 'PostgreSQL', type: 'database', technology: 'PostgreSQL', detectedFrom: 'pg', relationship: 'reads' },
|
||||
];
|
||||
const components: DetectedComponent[] = [
|
||||
{ id: 'order-controller', name: 'OrderController', type: 'controller', files: ['src/order.controller.ts'] },
|
||||
{ id: 'order-service', name: 'OrderService', type: 'service', files: ['src/order.service.ts'] },
|
||||
];
|
||||
const relationships: DetectedRelationship[] = [
|
||||
{ sourceId: 'order-controller', targetId: 'order-service', type: 'calls' },
|
||||
];
|
||||
|
||||
describe('ADR-112 consolidation: detected → C4ModelService → Mermaid', () => {
|
||||
let service: C4ModelService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new C4ModelService(createMockMemory(), { enableEmbeddings: false });
|
||||
});
|
||||
|
||||
it('should_render_a_valid_C4Context_with_the_system_and_external_systems', async () => {
|
||||
const res = await service.buildContext(toContextRequest(project, externalSystems));
|
||||
expect(res.success).toBe(true);
|
||||
if (!res.success) return;
|
||||
expect(res.value.mermaid).toContain('C4Context');
|
||||
expect(res.value.mermaid).toContain('PostgreSQL');
|
||||
});
|
||||
|
||||
it('should_render_a_valid_C4Container_with_the_application_container', async () => {
|
||||
const res = await service.buildContainer(toContainerRequest(project, externalSystems));
|
||||
expect(res.success).toBe(true);
|
||||
if (!res.success) return;
|
||||
expect(res.value.mermaid).toContain('C4Container');
|
||||
expect(res.value.mermaid).toMatch(/Container\(/);
|
||||
});
|
||||
|
||||
it('should_preserve_every_detected_component_name_in_the_component_diagram', async () => {
|
||||
const res = await service.buildComponent(toComponentRequest(components, relationships));
|
||||
expect(res.success).toBe(true);
|
||||
if (!res.success) return;
|
||||
expect(res.value.mermaid).toContain('C4Component');
|
||||
for (const c of components) {
|
||||
expect(res.value.mermaid).toContain(c.name);
|
||||
}
|
||||
});
|
||||
|
||||
it('should_emit_parser_compatible_Component_tokens', async () => {
|
||||
// The product-factors architecture-parser extracts names via /Component\(\w+,\s*"([^"]+)"/.
|
||||
const res = await service.buildComponent(toComponentRequest(components, relationships));
|
||||
expect(res.success).toBe(true);
|
||||
if (!res.success) return;
|
||||
const names = [...res.value.mermaid.matchAll(/Component\((\w+),\s*"([^"]+)"/g)].map((m) => m[2]);
|
||||
expect(names).toEqual(expect.arrayContaining(['OrderController', 'OrderService']));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* ADR-112 — C4 generation pipeline end-to-end (the path `aqe code c4` drives).
|
||||
*
|
||||
* Exercises the REAL bridge over a temp fixture: detect (fs scan + deps) →
|
||||
* map → C4ModelService render → confidence gate. No kernel, no memory.db.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { createMockMemory, createMockEventBus } from '../../../mocks';
|
||||
import { ProductFactorsBridgeService } from '../../../../src/domains/code-intelligence/services/product-factors-bridge';
|
||||
|
||||
let tmp: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'c4-e2e-'));
|
||||
fs.writeFileSync(path.join(tmp, 'package.json'), JSON.stringify({
|
||||
name: 'checkout-svc',
|
||||
description: 'Order checkout service',
|
||||
dependencies: { pg: '^8.0.0', express: '^4.0.0', ioredis: '^5.0.0' },
|
||||
}));
|
||||
// The detector treats top-level src/ subdirectories as components.
|
||||
for (const dir of ['controllers', 'services', 'repositories']) {
|
||||
fs.mkdirSync(path.join(tmp, 'src', dir), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmp, 'src', dir, 'order.ts'), `export class Order_${dir} {}\n`);
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('ADR-112 C4 generation pipeline (user path)', () => {
|
||||
let bridge: ProductFactorsBridgeService;
|
||||
|
||||
beforeEach(() => {
|
||||
bridge = new ProductFactorsBridgeService(createMockEventBus(), createMockMemory(), { publishEvents: false });
|
||||
});
|
||||
|
||||
it('should_generate_valid_C4_context_container_and_component_diagrams', async () => {
|
||||
const res = await bridge.requestC4Diagrams({
|
||||
projectPath: tmp,
|
||||
includeContext: true,
|
||||
includeContainer: true,
|
||||
includeComponent: true,
|
||||
analyzeComponents: true,
|
||||
detectExternalSystems: true,
|
||||
});
|
||||
expect(res.success).toBe(true);
|
||||
if (!res.success) return;
|
||||
expect(res.value.diagrams.context).toContain('C4Context');
|
||||
expect(res.value.diagrams.container).toContain('C4Container');
|
||||
expect(res.value.diagrams.component).toContain('C4Component');
|
||||
});
|
||||
|
||||
it('should_detect_external_systems_from_dependencies', async () => {
|
||||
const res = await bridge.requestC4Diagrams({ projectPath: tmp, detectExternalSystems: true });
|
||||
expect(res.success).toBe(true);
|
||||
if (!res.success) return;
|
||||
const names = res.value.externalSystems.map((e) => e.technology);
|
||||
// pg → PostgreSQL, ioredis → Redis (known dependency patterns)
|
||||
expect(names).toContain('PostgreSQL');
|
||||
});
|
||||
|
||||
it('should_fail_loud_not_silently_when_the_render_engine_fails', async () => {
|
||||
// ADR-112 C6: a C4ModelService failure must surface as an error, NOT be
|
||||
// masked by a silent fallback to a weaker inline diagram.
|
||||
const bridge = new ProductFactorsBridgeService(createMockEventBus(), createMockMemory(), { publishEvents: false });
|
||||
// Force the render engine to fail.
|
||||
(bridge as unknown as { getC4Service: () => unknown }).getC4Service = () => ({
|
||||
buildContext: async () => ({ success: false, error: new Error('boom') }),
|
||||
buildContainer: async () => ({ success: false, error: new Error('boom') }),
|
||||
buildComponent: async () => ({ success: false, error: new Error('boom') }),
|
||||
});
|
||||
|
||||
const res = await bridge.requestC4Diagrams({ projectPath: tmp, includeContext: true, analyzeComponents: true });
|
||||
expect(res.success).toBe(false);
|
||||
if (res.success) return;
|
||||
expect(res.error.message).toContain('boom');
|
||||
});
|
||||
|
||||
it('should_attach_a_deterministic_confidence_assessment_to_metadata', async () => {
|
||||
const res = await bridge.requestC4Diagrams({ projectPath: tmp, analyzeComponents: true });
|
||||
expect(res.success).toBe(true);
|
||||
if (!res.success) return;
|
||||
const confidence = res.value.metadata.analysisMetadata?.confidence;
|
||||
expect(confidence).toBeDefined();
|
||||
expect(['high', 'medium', 'low']).toContain(confidence!.level);
|
||||
expect(confidence!.reasons.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* ADR-112 — Detected → C4 spec mapping unit tests.
|
||||
*
|
||||
* Guards the consolidation seam: every detected element (component, relationship,
|
||||
* external system) must survive into a C4ModelService spec, and the enum
|
||||
* translations must land on valid domain vocabulary.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type {
|
||||
C4ProjectInfo,
|
||||
DetectedExternalSystem,
|
||||
DetectedComponent,
|
||||
DetectedRelationship,
|
||||
} from '../../../../src/shared/c4-model';
|
||||
import {
|
||||
mapExternalSystemType,
|
||||
mapComponentType,
|
||||
toExternalSystemSpecs,
|
||||
toComponentSpecs,
|
||||
toComponentRelationshipSpecs,
|
||||
toContextRequest,
|
||||
toContainerRequest,
|
||||
toComponentRequest,
|
||||
DEFAULT_CONTAINER_NAME,
|
||||
} from '../../../../src/domains/code-intelligence/services/c4-model/from-detected';
|
||||
|
||||
const project: C4ProjectInfo = { name: 'MyApp', description: 'A test app' };
|
||||
|
||||
const externalSystems: DetectedExternalSystem[] = [
|
||||
{ id: 'pg', name: 'PostgreSQL', type: 'database', technology: 'PostgreSQL', detectedFrom: 'pg', relationship: 'reads' },
|
||||
{ id: 'redis', name: 'Redis', type: 'cache', technology: 'Redis', detectedFrom: 'ioredis', relationship: 'uses' },
|
||||
];
|
||||
|
||||
const components: DetectedComponent[] = [
|
||||
{ id: 'user-controller', name: 'UserController', type: 'controller', files: ['src/user.controller.ts'], responsibilities: ['Handle user routes'] },
|
||||
{ id: 'user-service', name: 'UserService', type: 'service', files: ['src/user.service.ts'] },
|
||||
];
|
||||
|
||||
const relationships: DetectedRelationship[] = [
|
||||
{ sourceId: 'user-controller', targetId: 'user-service', type: 'calls', weight: 3 },
|
||||
];
|
||||
|
||||
describe('mapExternalSystemType', () => {
|
||||
it('should_map_queue_to_message_queue', () => {
|
||||
expect(mapExternalSystemType('queue')).toBe('message_queue');
|
||||
});
|
||||
|
||||
it('should_map_auth_to_authentication', () => {
|
||||
expect(mapExternalSystemType('auth')).toBe('authentication');
|
||||
});
|
||||
|
||||
it('should_map_cloud_to_third_party', () => {
|
||||
expect(mapExternalSystemType('cloud')).toBe('third_party');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapComponentType', () => {
|
||||
it('should_pass_through_supported_types', () => {
|
||||
expect(mapComponentType('controller')).toBe('controller');
|
||||
expect(mapComponentType('repository')).toBe('repository');
|
||||
});
|
||||
|
||||
it('should_fold_unsupported_types_to_module_so_none_are_dropped', () => {
|
||||
expect(mapComponentType('layer')).toBe('module');
|
||||
expect(mapComponentType('feature')).toBe('module');
|
||||
expect(mapComponentType('package')).toBe('module');
|
||||
expect(mapComponentType('other')).toBe('module');
|
||||
});
|
||||
|
||||
it('should_fold_transformer_to_adapter', () => {
|
||||
expect(mapComponentType('transformer')).toBe('adapter');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toExternalSystemSpecs', () => {
|
||||
it('should_preserve_every_external_system', () => {
|
||||
const specs = toExternalSystemSpecs(externalSystems);
|
||||
expect(specs).toHaveLength(externalSystems.length);
|
||||
});
|
||||
|
||||
it('should_carry_name_technology_and_relationship_description', () => {
|
||||
const [pg] = toExternalSystemSpecs(externalSystems);
|
||||
expect(pg).toMatchObject({ name: 'PostgreSQL', type: 'database', technology: 'PostgreSQL', relationshipDescription: 'reads' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('toComponentSpecs', () => {
|
||||
it('should_preserve_every_component_with_files_and_responsibilities', () => {
|
||||
const specs = toComponentSpecs(components);
|
||||
expect(specs).toHaveLength(2);
|
||||
expect(specs[0]).toMatchObject({
|
||||
name: 'UserController',
|
||||
type: 'controller',
|
||||
files: ['src/user.controller.ts'],
|
||||
responsibilities: ['Handle user routes'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toComponentRelationshipSpecs', () => {
|
||||
it('should_translate_ids_back_to_component_names', () => {
|
||||
const specs = toComponentRelationshipSpecs(components, relationships);
|
||||
expect(specs).toEqual([{ from: 'UserController', to: 'UserService', type: 'calls' }]);
|
||||
});
|
||||
|
||||
it('should_drop_relationships_whose_endpoints_are_unknown', () => {
|
||||
const dangling: DetectedRelationship[] = [{ sourceId: 'user-controller', targetId: 'ghost', type: 'calls' }];
|
||||
expect(toComponentRelationshipSpecs(components, dangling)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should_translate_divergent_relationship_verbs_to_domain_vocabulary', () => {
|
||||
const reads: DetectedRelationship[] = [{ sourceId: 'user-service', targetId: 'user-controller', type: 'reads' }];
|
||||
expect(toComponentRelationshipSpecs(components, reads)[0].type).toBe('reads_from');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toContextRequest', () => {
|
||||
it('should_use_project_name_and_description_and_all_external_systems', () => {
|
||||
const req = toContextRequest(project, externalSystems);
|
||||
expect(req.systemName).toBe('MyApp');
|
||||
expect(req.systemDescription).toBe('A test app');
|
||||
expect(req.externalSystems).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toContainerRequest', () => {
|
||||
it('should_model_a_single_application_container', () => {
|
||||
const req = toContainerRequest(project, externalSystems);
|
||||
expect(req.containers).toHaveLength(1);
|
||||
expect(req.containers[0]).toMatchObject({ name: DEFAULT_CONTAINER_NAME, technology: 'TypeScript' });
|
||||
});
|
||||
|
||||
it('should_draw_a_dependency_from_app_to_each_external_system', () => {
|
||||
const req = toContainerRequest(project, externalSystems);
|
||||
expect(req.dependencies).toHaveLength(2);
|
||||
expect(req.dependencies?.every((d) => d.from === DEFAULT_CONTAINER_NAME)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toComponentRequest', () => {
|
||||
it('should_default_to_the_application_container_and_keep_components_and_relationships', () => {
|
||||
const req = toComponentRequest(components, relationships);
|
||||
expect(req.containerName).toBe(DEFAULT_CONTAINER_NAME);
|
||||
expect(req.components).toHaveLength(2);
|
||||
expect(req.relationships).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* ADR-112 C2 — KG → component relationship aggregation tests.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { DetectedComponent } from '../../../../src/shared/c4-model';
|
||||
import {
|
||||
aggregateDependencyMapToComponentRelationships,
|
||||
createKnowledgeGraphRelationshipResolver,
|
||||
type DependencyMapLike,
|
||||
} from '../../../../src/domains/code-intelligence/services/c4-model/kg-relationships';
|
||||
|
||||
const PROJECT = '/proj';
|
||||
const components: DetectedComponent[] = [
|
||||
{ id: 'controllers', name: 'Controllers', type: 'controller', files: ['src/controllers/order.ts'] },
|
||||
{ id: 'services', name: 'Services', type: 'service', files: ['src/services/order.ts'] },
|
||||
{ id: 'repositories', name: 'Repositories', type: 'repository', files: ['src/repositories/order.ts'] },
|
||||
];
|
||||
|
||||
// edges reference node ids; nodes carry id+path (abs, as the KG returns them)
|
||||
const depMap: DependencyMapLike = {
|
||||
nodes: [
|
||||
{ id: 'n1', path: '/proj/src/controllers/order.ts' },
|
||||
{ id: 'n2', path: '/proj/src/services/order.ts' },
|
||||
{ id: 'n3', path: '/proj/src/repositories/order.ts' },
|
||||
],
|
||||
edges: [
|
||||
{ source: 'n1', target: 'n2' }, // controllers → services
|
||||
{ source: 'n2', target: 'n3' }, // services → repositories
|
||||
],
|
||||
};
|
||||
|
||||
describe('aggregateDependencyMapToComponentRelationships', () => {
|
||||
it('should_fold_file_edges_into_component_relationships', () => {
|
||||
const rels = aggregateDependencyMapToComponentRelationships(components, depMap, PROJECT);
|
||||
expect(rels).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ sourceId: 'controllers', targetId: 'services', type: 'depends_on', weight: 1 },
|
||||
{ sourceId: 'services', targetId: 'repositories', type: 'depends_on', weight: 1 },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should_drop_self_edges_within_one_component', () => {
|
||||
const selfEdge: DependencyMapLike = {
|
||||
nodes: [
|
||||
{ id: 'a', path: '/proj/src/services/order.ts' },
|
||||
{ id: 'b', path: '/proj/src/services/order.ts' },
|
||||
],
|
||||
edges: [{ source: 'a', target: 'b' }],
|
||||
};
|
||||
expect(aggregateDependencyMapToComponentRelationships(components, selfEdge, PROJECT)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should_accumulate_weight_for_repeated_component_edges', () => {
|
||||
const multi: DependencyMapLike = {
|
||||
nodes: [
|
||||
{ id: 'c1', path: '/proj/src/controllers/order.ts' },
|
||||
{ id: 's1', path: '/proj/src/services/order.ts' },
|
||||
],
|
||||
edges: [
|
||||
{ source: 'c1', target: 's1' },
|
||||
{ source: 'c1', target: 's1' },
|
||||
],
|
||||
};
|
||||
const rels = aggregateDependencyMapToComponentRelationships(components, multi, PROJECT);
|
||||
expect(rels[0].weight).toBe(2);
|
||||
});
|
||||
|
||||
it('should_ignore_edges_to_files_outside_any_component', () => {
|
||||
const external: DependencyMapLike = {
|
||||
nodes: [
|
||||
{ id: 'c1', path: '/proj/src/controllers/order.ts' },
|
||||
{ id: 'x', path: '/proj/node_modules/lib/index.ts' },
|
||||
],
|
||||
edges: [{ source: 'c1', target: 'x' }],
|
||||
};
|
||||
expect(aggregateDependencyMapToComponentRelationships(components, external, PROJECT)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createKnowledgeGraphRelationshipResolver', () => {
|
||||
it('should_return_null_when_there_are_no_files', async () => {
|
||||
const kg = { index: async () => ({ success: true }), mapDependencies: async () => ({ success: true, value: { nodes: [], edges: [] } }) };
|
||||
const resolver = createKnowledgeGraphRelationshipResolver(kg);
|
||||
expect(await resolver([], PROJECT)).toBeNull();
|
||||
});
|
||||
|
||||
it('should_return_null_on_a_KG_miss_so_the_heuristic_stands', async () => {
|
||||
const kg = { index: async () => ({ success: true }), mapDependencies: async () => ({ success: false }) };
|
||||
const resolver = createKnowledgeGraphRelationshipResolver(kg);
|
||||
expect(await resolver(components, PROJECT)).toBeNull();
|
||||
});
|
||||
|
||||
it('should_aggregate_real_edges_from_the_kg', async () => {
|
||||
const kg = {
|
||||
index: async () => ({ success: true }),
|
||||
mapDependencies: async () => ({ success: true, value: depMap }),
|
||||
};
|
||||
const resolver = createKnowledgeGraphRelationshipResolver(kg);
|
||||
const rels = await resolver(components, PROJECT);
|
||||
expect(rels).not.toBeNull();
|
||||
expect(rels!.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should_index_incrementally_before_mapping', async () => {
|
||||
const calls: Array<{ paths: string[]; incremental?: boolean }> = [];
|
||||
const kg = {
|
||||
index: async (req: { paths: string[]; incremental?: boolean }) => { calls.push(req); return { success: true }; },
|
||||
mapDependencies: async () => ({ success: true, value: depMap }),
|
||||
};
|
||||
await createKnowledgeGraphRelationshipResolver(kg)(components, PROJECT);
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].incremental).toBe(true);
|
||||
expect(calls[0].paths).toContain('/proj/src/controllers/order.ts');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* ADR-112 — qe/code/c4 MCP tool tests, incl. MCP-CLI parity.
|
||||
*
|
||||
* The MCP tool and the CLI both drive the SAME ProductFactorsBridgeService
|
||||
* pipeline, so a generate over a fixture must yield the same diagrams +
|
||||
* confidence the CLI's coordinator path produces (CLAUDE.md MCP-CLI parity rule).
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { createMockMemory } from '../../../mocks';
|
||||
import { CodeC4Tool } from '../../../../src/mcp/tools/code-intelligence/c4';
|
||||
import { ProductFactorsBridgeService } from '../../../../src/domains/code-intelligence/services/product-factors-bridge';
|
||||
import { InMemoryEventBus } from '../../../../src/kernel/event-bus';
|
||||
import type { MCPToolContext } from '../../../../src/mcp/tools/base';
|
||||
|
||||
let tmp: string;
|
||||
|
||||
function makeFixture(): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'c4-mcp-'));
|
||||
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
|
||||
name: 'billing-svc', description: 'Billing service', dependencies: { pg: '^8.0.0' },
|
||||
}));
|
||||
for (const sub of ['controllers', 'services']) {
|
||||
fs.mkdirSync(path.join(dir, 'src', sub), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, 'src', sub, 'billing.ts'), `export class Billing_${sub} {}\n`);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
const ctx = (memory: ReturnType<typeof createMockMemory>): MCPToolContext => ({
|
||||
requestId: 'test', startTime: Date.now(), memory,
|
||||
});
|
||||
|
||||
beforeEach(() => { tmp = makeFixture(); });
|
||||
afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
|
||||
|
||||
describe('qe/code/c4 MCP tool', () => {
|
||||
it('should_be_named_and_domain_scoped', () => {
|
||||
const tool = new CodeC4Tool();
|
||||
expect(tool.config.name).toBe('qe/code/c4');
|
||||
expect(tool.config.domain).toBe('code-intelligence');
|
||||
});
|
||||
|
||||
it('should_generate_C4_diagrams_with_a_confidence_assessment', async () => {
|
||||
const tool = new CodeC4Tool();
|
||||
const res = await tool.execute({ action: 'generate', projectPath: tmp, level: 'all' }, ctx(createMockMemory()));
|
||||
expect(res.success).toBe(true);
|
||||
const gen = res.data?.generateResult;
|
||||
expect(gen?.diagrams.context).toContain('C4Context');
|
||||
expect(gen?.diagrams.component).toContain('C4Component');
|
||||
expect(gen?.confidence).toBeDefined();
|
||||
expect(['high', 'medium', 'low']).toContain(gen!.confidence!.level);
|
||||
});
|
||||
|
||||
it('should_respect_the_level_parameter', async () => {
|
||||
const tool = new CodeC4Tool();
|
||||
const res = await tool.execute({ action: 'generate', projectPath: tmp, level: 'context' }, ctx(createMockMemory()));
|
||||
expect(res.success).toBe(true);
|
||||
const gen = res.data?.generateResult;
|
||||
expect(gen?.diagrams.context).toBeDefined();
|
||||
expect(gen?.diagrams.component).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should_require_a_query_for_search', async () => {
|
||||
const tool = new CodeC4Tool();
|
||||
const res = await tool.execute({ action: 'search' }, ctx(createMockMemory()));
|
||||
expect(res.success).toBe(false);
|
||||
expect(res.error).toMatch(/query/i);
|
||||
});
|
||||
|
||||
it('should_return_hits_for_search_after_generate_persists_embeddings', async () => {
|
||||
// ADR-112: generate embeds + persists diagrams; search finds them via the
|
||||
// shared memory + namespace. Same tool instance + same context memory.
|
||||
const tool = new CodeC4Tool();
|
||||
const context = ctx(createMockMemory());
|
||||
|
||||
const gen = await tool.execute({ action: 'generate', projectPath: tmp, level: 'all' }, context);
|
||||
expect(gen.success).toBe(true);
|
||||
|
||||
const found = await tool.execute({ action: 'search', query: 'billing service architecture', limit: 5 }, context);
|
||||
expect(found.success).toBe(true);
|
||||
expect(found.data?.searchResult?.total).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should_produce_diagrams_matching_the_bridge_pipeline_the_CLI_uses', async () => {
|
||||
// MCP path
|
||||
const tool = new CodeC4Tool();
|
||||
const mcp = await tool.execute({ action: 'generate', projectPath: tmp, level: 'all' }, ctx(createMockMemory()));
|
||||
expect(mcp.success).toBe(true);
|
||||
|
||||
// CLI/coordinator path = the same bridge pipeline
|
||||
const bridge = new ProductFactorsBridgeService(new InMemoryEventBus(), createMockMemory(), { publishEvents: false });
|
||||
const cli = await bridge.requestC4Diagrams({
|
||||
projectPath: tmp,
|
||||
includeContext: true, includeContainer: true, includeComponent: true, includeDependency: true,
|
||||
analyzeComponents: true, detectExternalSystems: true, analyzeCoupling: true,
|
||||
});
|
||||
expect(cli.success).toBe(true);
|
||||
if (!cli.success) return;
|
||||
|
||||
const gen = mcp.data!.generateResult!;
|
||||
expect(gen.componentsDetected).toBe(cli.value.components.length);
|
||||
expect(gen.externalSystemsDetected).toBe(cli.value.externalSystems.length);
|
||||
expect(gen.diagrams.context).toBe(cli.value.diagrams.context);
|
||||
expect(gen.diagrams.component).toBe(cli.value.diagrams.component);
|
||||
});
|
||||
});
|
||||
@@ -17,8 +17,8 @@ describe('QE Tool Registry', () => {
|
||||
describe('QE_TOOL_NAMES', () => {
|
||||
it('should have all tool names', () => {
|
||||
const names = Object.values(QE_TOOL_NAMES);
|
||||
// 33 original + 4 new (schedule, load-test, visual-security, browser-workflow) = 37 tools
|
||||
expect(names.length).toBe(37);
|
||||
// 33 original + 4 new (schedule, load-test, visual-security, browser-workflow) + 1 (qe/code/c4, ADR-112) = 38 tools
|
||||
expect(names.length).toBe(38);
|
||||
});
|
||||
|
||||
it('should follow qe/* naming convention', () => {
|
||||
@@ -87,15 +87,15 @@ describe('QE Tool Registry', () => {
|
||||
|
||||
describe('QE_TOOLS', () => {
|
||||
it('should have all tool instances', () => {
|
||||
// 33 original + 4 new (schedule, load-test, visual-security, browser-workflow) = 37 tools
|
||||
expect(QE_TOOLS.length).toBe(37);
|
||||
// 33 original + 4 new (schedule, load-test, visual-security, browser-workflow) + 1 (qe/code/c4, ADR-112) = 38 tools
|
||||
expect(QE_TOOLS.length).toBe(38);
|
||||
});
|
||||
|
||||
it('should have all unique names', () => {
|
||||
const names = QE_TOOLS.map(t => t.name);
|
||||
const uniqueNames = new Set(names);
|
||||
// 33 original + 4 new (schedule, load-test, visual-security, browser-workflow) = 37 tools
|
||||
expect(uniqueNames.size).toBe(37);
|
||||
// 33 original + 4 new (schedule, load-test, visual-security, browser-workflow) + 1 (qe/code/c4, ADR-112) = 38 tools
|
||||
expect(uniqueNames.size).toBe(38);
|
||||
});
|
||||
|
||||
it('should have descriptions for all tools', () => {
|
||||
@@ -204,8 +204,8 @@ describe('QE Tool Registry', () => {
|
||||
describe('getAllToolDefinitions', () => {
|
||||
it('should return all tool definitions', () => {
|
||||
const definitions = getAllToolDefinitions();
|
||||
// 33 original + 4 new (schedule, load-test, visual-security, browser-workflow) = 37 tools
|
||||
expect(definitions.length).toBe(37);
|
||||
// 33 original + 4 new (schedule, load-test, visual-security, browser-workflow) + 1 (qe/code/c4, ADR-112) = 38 tools
|
||||
expect(definitions.length).toBe(38);
|
||||
});
|
||||
|
||||
it('should return MCP-compatible definitions', () => {
|
||||
@@ -223,8 +223,8 @@ describe('QE Tool Registry', () => {
|
||||
const definitions = getAllToolDefinitions();
|
||||
const names = definitions.map(d => d.name);
|
||||
const uniqueNames = new Set(names);
|
||||
// 33 original + 4 new (schedule, load-test, visual-security, browser-workflow) = 37 tools
|
||||
expect(uniqueNames.size).toBe(37);
|
||||
// 33 original + 4 new (schedule, load-test, visual-security, browser-workflow) + 1 (qe/code/c4, ADR-112) = 38 tools
|
||||
expect(uniqueNames.size).toBe(38);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* ADR-112 — C4 confidence gate unit tests.
|
||||
*
|
||||
* Deterministic gate that turns the detector's known limits into a surfaced
|
||||
* signal. Covers the boundary conditions that flip the level.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
assessC4Confidence,
|
||||
C4_LOC_DEGRADE_THRESHOLD,
|
||||
} from '../../../src/shared/c4-model/confidence';
|
||||
|
||||
describe('assessC4Confidence', () => {
|
||||
it('should_return_low_with_a_verify_reason_when_no_components_detected', () => {
|
||||
const a = assessC4Confidence({ componentsDetected: 0, relationshipsDetected: 0, externalSystemsDetected: 0, filesAnalyzed: 0 });
|
||||
expect(a.level).toBe('low');
|
||||
expect(a.score).toBe(0);
|
||||
expect(a.reasons.join(' ')).toMatch(/empty|verify/i);
|
||||
});
|
||||
|
||||
it('should_be_deterministic_for_identical_inputs', () => {
|
||||
const inputs = { componentsDetected: 6, relationshipsDetected: 5, externalSystemsDetected: 2, filesAnalyzed: 40 };
|
||||
expect(assessC4Confidence(inputs)).toEqual(assessC4Confidence(inputs));
|
||||
});
|
||||
|
||||
it('should_rate_high_when_components_relationships_and_external_systems_are_rich', () => {
|
||||
const a = assessC4Confidence({ componentsDetected: 8, relationshipsDetected: 8, externalSystemsDetected: 2, filesAnalyzed: 60 });
|
||||
expect(a.level).toBe('high');
|
||||
expect(a.score).toBeGreaterThanOrEqual(0.7);
|
||||
});
|
||||
|
||||
it('should_penalize_missing_relationships_as_unverified_structure', () => {
|
||||
const withRels = assessC4Confidence({ componentsDetected: 6, relationshipsDetected: 6, externalSystemsDetected: 0, filesAnalyzed: 30 });
|
||||
const noRels = assessC4Confidence({ componentsDetected: 6, relationshipsDetected: 0, externalSystemsDetected: 0, filesAnalyzed: 30 });
|
||||
expect(noRels.score).toBeLessThan(withRels.score);
|
||||
expect(noRels.reasons.join(' ')).toMatch(/no relationships/i);
|
||||
});
|
||||
|
||||
it('should_downgrade_large_repos_past_the_LOC_degrade_threshold', () => {
|
||||
const small = assessC4Confidence({ componentsDetected: 8, relationshipsDetected: 8, externalSystemsDetected: 2, filesAnalyzed: 60, totalLoc: 5_000 });
|
||||
const large = assessC4Confidence({ componentsDetected: 8, relationshipsDetected: 8, externalSystemsDetected: 2, filesAnalyzed: 60, totalLoc: C4_LOC_DEGRADE_THRESHOLD + 1 });
|
||||
expect(large.score).toBeLessThan(small.score);
|
||||
expect(large.reasons.join(' ')).toMatch(/large|draft/i);
|
||||
});
|
||||
|
||||
it('should_always_append_a_draft_warning_when_not_high', () => {
|
||||
const a = assessC4Confidence({ componentsDetected: 2, relationshipsDetected: 0, externalSystemsDetected: 0, filesAnalyzed: 4 });
|
||||
expect(a.level).not.toBe('high');
|
||||
expect(a.reasons.join(' ')).toMatch(/draft|verify/i);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user