diff --git a/CHANGELOG.md b/CHANGELOG.md index 625df4e5..cf4f2231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] — PROVING GROUND + +### Added + +- **`coverage` view** — Code-to-spec gap analysis: identifies `crate:`/`module:`/`pkg:` nodes lacking `implements` edges to `spec:`/`adr:` targets. Returns `meta.linked`, `meta.unlinked`, and `meta.coveragePct` +- **Echo ecosystem seed fixture** — `test/fixtures/echo-seed.yaml` with 55 nodes and 70 edges for integration testing (5 milestones, 5 specs, 5 ADRs, 5 docs, 15 crates, 11 tasks, 9 issues) +- **PROVING GROUND integration tests** — `test/proving-ground.test.js` validates 5 real project management questions against the Echo seed with deterministic ground truth +- **Dogfood session transcript** — `docs/dogfood-session.md` documents CLI walkthrough of all 5 questions with answers and timing + +### Changed + +- **Test count** — 162 tests across 9 files (was 143 across 8) + ## [2.0.0-alpha.1] - 2026-02-11 ### Added diff --git a/docs/dogfood-session.md b/docs/dogfood-session.md new file mode 100644 index 00000000..1b0c0903 --- /dev/null +++ b/docs/dogfood-session.md @@ -0,0 +1,147 @@ +# PROVING GROUND — Dogfood Session Transcript + +> **Date**: 2026-02-11 +> **Seed**: `test/fixtures/echo-seed.yaml` — Echo ecosystem (55 nodes, 70 edges) +> **Runtime**: Node.js 20, vitest 3.2.4 + +## Setup + +```bash +$ git mind init +Initialized empty git-mind graph + +$ git mind import test/fixtures/echo-seed.yaml +Imported 55 nodes, 70 edges (144ms) +``` + +--- + +## Q1: What blocks milestone M2? + +**View**: `milestone` + +```bash +$ git mind view milestone --json | jq '.meta.milestoneStats["milestone:M2"]' +{ + "total": 2, + "done": 0, + "pct": 0, + "blockers": ["issue:E-003", "issue:E-004"] +} +``` + +**Answer**: Two issues block M2 — `issue:E-003` (REST schema breaks on nested objects) blocks `task:E-007`, and `issue:E-004` (validation rejects valid payloads) blocks `task:E-008`. Both M2 tasks are incomplete (0%). + +**Timing**: <1ms + +--- + +## Q2: Which ADRs lack implementation? + +**View**: `traceability` + +```bash +$ git mind view traceability --json | jq '.meta.gaps | map(select(startswith("adr:")))' +[ + "adr:003-encryption-at-rest", + "adr:004-rest-vs-grpc" +] +``` + +**Answer**: 2 of 5 ADRs have no `implements` edge pointing at them. ADR-003 (encryption at rest for PII) and ADR-004 (REST over gRPC) are decisions with no code backing. The other 3 ADRs are covered: `echo-core` implements ADR-001, `echo-db` implements ADR-002, `echo-api` implements ADR-005. + +**Timing**: <1ms + +--- + +## Q3: Which crates are unlinked to specs? + +**View**: `coverage` (new in PROVING GROUND) + +```bash +$ git mind view coverage --json | jq '.meta' +{ + "linked": [ + "crate:echo-core", + "crate:echo-api", + "crate:echo-db", + "crate:echo-auth", + "crate:echo-queue", + "crate:echo-web" + ], + "unlinked": [ + "crate:echo-crypto", + "crate:echo-cli", + "crate:echo-config", + "crate:echo-log", + "crate:echo-test-utils", + "crate:echo-bench", + "crate:echo-migrate", + "crate:echo-plugin", + "crate:echo-sdk" + ], + "coveragePct": 40 +} +``` + +**Answer**: 9 of 15 crates have no `implements` edge to any spec or ADR. Coverage is 40%. The unlinked crates are utility/tooling crates (crypto, cli, config, log, test-utils, bench, migrate, plugin, sdk) — they may not need formal specs, but the gap is now visible. + +**Timing**: <1ms + +--- + +## Q4: What should a new engineer read first? + +**View**: `onboarding` + +```bash +$ git mind view onboarding --json | jq '.meta.readingOrder | map(select(startswith("doc:")))' +[ + "doc:getting-started", + "doc:architecture-overview", + "doc:api-reference", + "doc:contributing", + "doc:deployment-guide" +] +``` + +**Answer**: Start with `doc:getting-started` (the root — no dependencies). Then `doc:architecture-overview` (depends on getting-started). After that, `doc:api-reference` and `doc:contributing` (both depend on architecture-overview). Finally `doc:deployment-guide` (depends on getting-started, so it could be read earlier, but topological sort with alphabetical tie-breaking places it last). + +**Timing**: <1ms + +--- + +## Q5: What's low-confidence? + +**View**: `suggestions` + `computeStatus()` + +```bash +$ git mind view suggestions --json | jq '.edges[] | {from, to, label, confidence: .props.confidence}' +{ "from": "issue:E-016", "to": "issue:E-017", "label": "relates-to", "confidence": 0.2 } +{ "from": "issue:E-018", "to": "crate:echo-crypto", "label": "relates-to", "confidence": 0.3 } +{ "from": "issue:E-019", "to": "spec:003-web-sockets", "label": "relates-to", "confidence": 0.4 } +{ "from": "issue:E-020", "to": "doc:contributing", "label": "relates-to", "confidence": 0.3 } + +$ git mind status --json | jq '.health.lowConfidence' +4 +``` + +**Answer**: 4 edges have confidence below 0.5 (the low-confidence threshold). All are `relates-to` edges — likely AI-suggested links that haven't been reviewed. Confidence values range from 0.2 to 0.4. + +**Timing**: <1ms + +--- + +## Summary + +| # | Question | View | Answer | Time | +|---|----------|------|--------|------| +| 1 | What blocks M2? | `milestone` | `issue:E-003`, `issue:E-004` | <1ms | +| 2 | ADRs lacking impl? | `traceability` | `adr:003-encryption-at-rest`, `adr:004-rest-vs-grpc` | <1ms | +| 3 | Unlinked crates? | `coverage` | 9 of 15 (40% coverage) | <1ms | +| 4 | Read first? | `onboarding` | `doc:getting-started` → `doc:architecture-overview` | <1ms | +| 5 | Low-confidence? | `suggestions` | 4 edges (0.2–0.4) | <1ms | + +**Total query time**: ~1ms for all 5 questions against a 55-node, 70-edge graph. + +All 5 questions answered correctly from the graph alone — no manual inspection, no external tools, just views. diff --git a/src/views.js b/src/views.js index f8586421..fe06830d 100644 --- a/src/views.js +++ b/src/views.js @@ -401,5 +401,50 @@ defineView('onboarding', (nodes, edges) => { }; }); +defineView('coverage', (nodes, edges) => { + // Code-to-spec gap analysis: which code nodes lack implements edges to specs? + const codePrefixes = new Set(['crate', 'module', 'pkg']); + const specPrefixes = new Set(['spec', 'adr']); + + const codeNodes = nodes.filter(n => { + const p = extractPrefix(n); + return p && codePrefixes.has(p); + }); + const specNodes = new Set( + nodes.filter(n => { + const p = extractPrefix(n); + return p && specPrefixes.has(p); + }) + ); + + // Code nodes that have at least one implements edge to a spec/adr + const linkedSet = new Set( + edges + .filter(e => e.label === 'implements' && specNodes.has(e.to)) + .map(e => e.from) + ); + + const linked = codeNodes.filter(n => linkedSet.has(n)); + const unlinked = codeNodes.filter(n => !linkedSet.has(n)); + + // Self-contained subgraph: edges where both endpoints are in the result + const resultNodes = new Set([...codeNodes, ...specNodes]); + const resultEdges = edges.filter( + e => e.label === 'implements' && resultNodes.has(e.from) && resultNodes.has(e.to) + ); + + return { + nodes: [...resultNodes], + edges: resultEdges, + meta: { + linked, + unlinked, + coveragePct: codeNodes.length > 0 + ? Math.round((linked.length / codeNodes.length) * 100) + : 100, + }, + }; +}); + // Capture built-in names after all registrations builtInNames = new Set(registry.keys()); diff --git a/test/fixtures/echo-seed.yaml b/test/fixtures/echo-seed.yaml new file mode 100644 index 00000000..ba31d8c3 --- /dev/null +++ b/test/fixtures/echo-seed.yaml @@ -0,0 +1,288 @@ +# Echo Ecosystem — PROVING GROUND seed fixture +# 55 nodes, 70 edges, deterministic ground truth for 5 dogfood questions. +# +# Q1 What blocks M2? → milestone view → [issue:E-003, issue:E-004] +# Q2 Which ADRs lack impl? → traceability → [adr:003-encryption-at-rest, adr:004-rest-vs-grpc] +# Q3 Unlinked crates? → coverage view → 9 of 15 crates +# Q4 What to read first? → onboarding → doc:getting-started before doc:architecture-overview +# Q5 Low-confidence edges? → suggestions → 4 edges (confidence < 0.5) + +version: 1 + +nodes: + # ── Milestones (5) ────────────────────────────────────────────── + - id: "milestone:M1" + properties: + title: "Core Foundation" + status: "complete" + - id: "milestone:M2" + properties: + title: "API Layer" + status: "blocked" + - id: "milestone:M3" + properties: + title: "Real-time Features" + status: "in-progress" + - id: "milestone:M4" + properties: + title: "Developer Experience" + status: "planned" + - id: "milestone:M5" + properties: + title: "Production Readiness" + status: "planned" + + # ── Specs (5) ─────────────────────────────────────────────────── + - id: "spec:001-auth-flow" + properties: + title: "Authentication Flow" + - id: "spec:002-queue-protocol" + properties: + title: "Message Queue Protocol" + - id: "spec:003-web-sockets" + properties: + title: "WebSocket Transport" + - id: "spec:004-rest-api" + properties: + title: "REST API Design" + - id: "spec:005-data-model" + properties: + title: "Data Model Schema" + + # ── ADRs (5) ──────────────────────────────────────────────────── + - id: "adr:001-event-sourcing" + properties: + title: "Use event sourcing for audit trail" + - id: "adr:002-postgres" + properties: + title: "PostgreSQL as primary store" + - id: "adr:003-encryption-at-rest" + properties: + title: "Encryption at rest for PII" + - id: "adr:004-rest-vs-grpc" + properties: + title: "REST over gRPC for public API" + - id: "adr:005-crate-structure" + properties: + title: "Workspace crate structure" + + # ── Docs (5) ──────────────────────────────────────────────────── + - id: "doc:getting-started" + properties: + title: "Getting Started Guide" + - id: "doc:architecture-overview" + properties: + title: "Architecture Overview" + - id: "doc:api-reference" + properties: + title: "API Reference" + - id: "doc:deployment-guide" + properties: + title: "Deployment Guide" + - id: "doc:contributing" + properties: + title: "Contributing Guide" + + # ── Crates (15) — 6 linked to specs/ADRs, 9 unlinked ────────── + - id: "crate:echo-core" + properties: + description: "Core event-sourcing engine" + - id: "crate:echo-api" + properties: + description: "REST API server" + - id: "crate:echo-db" + properties: + description: "Database layer (Postgres)" + - id: "crate:echo-auth" + properties: + description: "Authentication module" + - id: "crate:echo-queue" + properties: + description: "Message queue adapter" + - id: "crate:echo-web" + properties: + description: "WebSocket server" + - id: "crate:echo-crypto" + properties: + description: "Cryptographic helpers" + - id: "crate:echo-cli" + properties: + description: "CLI interface" + - id: "crate:echo-config" + properties: + description: "Configuration loader" + - id: "crate:echo-log" + properties: + description: "Structured logging" + - id: "crate:echo-test-utils" + properties: + description: "Test utilities and fixtures" + - id: "crate:echo-bench" + properties: + description: "Benchmark harness" + - id: "crate:echo-migrate" + properties: + description: "Database migrations" + - id: "crate:echo-plugin" + properties: + description: "Plugin framework" + - id: "crate:echo-sdk" + properties: + description: "Client SDK" + + # ── Tasks (11) — milestone work items ─────────────────────────── + - id: "task:E-005" + properties: + title: "Implement auth middleware" + - id: "task:E-006" + properties: + title: "Implement queue consumer" + - id: "task:E-007" + properties: + title: "Build REST endpoints" + - id: "task:E-008" + properties: + title: "API schema validation" + - id: "task:E-009" + properties: + title: "WebSocket handshake" + - id: "task:E-010" + properties: + title: "Message broadcast" + - id: "task:E-011" + properties: + title: "Connection pooling" + - id: "task:E-012" + properties: + title: "CLI scaffolding" + - id: "task:E-013" + properties: + title: "Config file support" + - id: "task:E-014" + properties: + title: "Migration runner" + - id: "task:E-015" + properties: + title: "Health check endpoint" + + # ── Issues (9) — bugs, blockers, investigations ───────────────── + - id: "issue:E-001" + properties: + title: "Auth token expiry bug" + - id: "issue:E-002" + properties: + title: "Queue deadlock under load" + - id: "issue:E-003" + properties: + title: "REST schema breaks on nested objects" + - id: "issue:E-004" + properties: + title: "Validation rejects valid payloads" + - id: "issue:E-016" + properties: + title: "Flaky integration test" + - id: "issue:E-017" + properties: + title: "Slow query on large datasets" + - id: "issue:E-018" + properties: + title: "Crypto padding oracle concern" + - id: "issue:E-019" + properties: + title: "WebSocket reconnect storms" + - id: "issue:E-020" + properties: + title: "Contributor docs outdated" + +edges: + # ── belongs-to: tasks → milestones (11) ───────────────────────── + - { source: "task:E-005", target: "milestone:M1", type: "belongs-to" } + - { source: "task:E-006", target: "milestone:M1", type: "belongs-to" } + - { source: "task:E-007", target: "milestone:M2", type: "belongs-to" } + - { source: "task:E-008", target: "milestone:M2", type: "belongs-to" } + - { source: "task:E-009", target: "milestone:M3", type: "belongs-to" } + - { source: "task:E-010", target: "milestone:M3", type: "belongs-to" } + - { source: "task:E-011", target: "milestone:M3", type: "belongs-to" } + - { source: "task:E-012", target: "milestone:M4", type: "belongs-to" } + - { source: "task:E-013", target: "milestone:M4", type: "belongs-to" } + - { source: "task:E-014", target: "milestone:M5", type: "belongs-to" } + - { source: "task:E-015", target: "milestone:M5", type: "belongs-to" } + + # ── implements: tasks → specs (task completion signals) (5) ───── + - { source: "task:E-005", target: "spec:001-auth-flow", type: "implements" } + - { source: "task:E-006", target: "spec:002-queue-protocol", type: "implements" } + - { source: "task:E-009", target: "spec:003-web-sockets", type: "implements" } + - { source: "task:E-012", target: "spec:004-rest-api", type: "implements" } + - { source: "task:E-014", target: "spec:005-data-model", type: "implements" } + + # ── implements: crates → specs/ADRs (coverage edges) (8) ─────── + - { source: "crate:echo-core", target: "adr:001-event-sourcing", type: "implements" } + - { source: "crate:echo-api", target: "spec:004-rest-api", type: "implements" } + - { source: "crate:echo-api", target: "adr:005-crate-structure", type: "implements" } + - { source: "crate:echo-db", target: "spec:005-data-model", type: "implements" } + - { source: "crate:echo-db", target: "adr:002-postgres", type: "implements" } + - { source: "crate:echo-auth", target: "spec:001-auth-flow", type: "implements" } + - { source: "crate:echo-queue", target: "spec:002-queue-protocol", type: "implements" } + - { source: "crate:echo-web", target: "spec:003-web-sockets", type: "implements" } + + # ── blocks: issue → task (Q1 ground truth) (4) ───────────────── + - { source: "issue:E-003", target: "task:E-007", type: "blocks" } + - { source: "issue:E-004", target: "task:E-008", type: "blocks" } + - { source: "issue:E-001", target: "issue:E-003", type: "blocks" } + - { source: "issue:E-002", target: "issue:E-004", type: "blocks" } + + # ── depends-on: crate → crate (architecture) (10) ────────────── + - { source: "crate:echo-api", target: "crate:echo-core", type: "depends-on" } + - { source: "crate:echo-db", target: "crate:echo-core", type: "depends-on" } + - { source: "crate:echo-auth", target: "crate:echo-db", type: "depends-on" } + - { source: "crate:echo-queue", target: "crate:echo-core", type: "depends-on" } + - { source: "crate:echo-web", target: "crate:echo-api", type: "depends-on" } + - { source: "crate:echo-cli", target: "crate:echo-core", type: "depends-on" } + - { source: "crate:echo-config", target: "crate:echo-core", type: "depends-on" } + - { source: "crate:echo-log", target: "crate:echo-core", type: "depends-on" } + - { source: "crate:echo-plugin", target: "crate:echo-api", type: "depends-on" } + - { source: "crate:echo-sdk", target: "crate:echo-api", type: "depends-on" } + + # ── depends-on: doc → doc (reading order, Q4) (4) ────────────── + - { source: "doc:architecture-overview", target: "doc:getting-started", type: "depends-on" } + - { source: "doc:api-reference", target: "doc:architecture-overview", type: "depends-on" } + - { source: "doc:deployment-guide", target: "doc:getting-started", type: "depends-on" } + - { source: "doc:contributing", target: "doc:architecture-overview", type: "depends-on" } + + # ── documents: doc → spec/ADR (5) ────────────────────────────── + - { source: "doc:getting-started", target: "spec:001-auth-flow", type: "documents" } + - { source: "doc:architecture-overview", target: "adr:001-event-sourcing", type: "documents" } + - { source: "doc:api-reference", target: "spec:004-rest-api", type: "documents" } + - { source: "doc:deployment-guide", target: "adr:002-postgres", type: "documents" } + - { source: "doc:contributing", target: "adr:005-crate-structure", type: "documents" } + + # ── consumed-by: spec/ADR → crate (5) ────────────────────────── + - { source: "spec:001-auth-flow", target: "crate:echo-auth", type: "consumed-by" } + - { source: "spec:002-queue-protocol", target: "crate:echo-queue", type: "consumed-by" } + - { source: "spec:004-rest-api", target: "crate:echo-api", type: "consumed-by" } + - { source: "spec:005-data-model", target: "crate:echo-db", type: "consumed-by" } + - { source: "adr:001-event-sourcing", target: "crate:echo-core", type: "consumed-by" } + + # ── augments (4) ─────────────────────────────────────────────── + - { source: "adr:002-postgres", target: "adr:001-event-sourcing", type: "augments" } + - { source: "adr:005-crate-structure", target: "adr:004-rest-vs-grpc", type: "augments" } + - { source: "spec:003-web-sockets", target: "spec:004-rest-api", type: "augments" } + - { source: "doc:contributing", target: "doc:getting-started", type: "augments" } + + # ── relates-to: normal confidence (10) ────────────────────────── + - { source: "issue:E-001", target: "task:E-005", type: "relates-to" } + - { source: "issue:E-002", target: "task:E-006", type: "relates-to" } + - { source: "issue:E-003", target: "spec:001-auth-flow", type: "relates-to" } + - { source: "issue:E-004", target: "spec:002-queue-protocol", type: "relates-to" } + - { source: "crate:echo-test-utils", target: "crate:echo-core", type: "relates-to" } + - { source: "crate:echo-bench", target: "crate:echo-core", type: "relates-to" } + - { source: "crate:echo-migrate", target: "crate:echo-db", type: "relates-to" } + - { source: "issue:E-016", target: "milestone:M3", type: "relates-to" } + - { source: "issue:E-017", target: "milestone:M4", type: "relates-to" } + - { source: "issue:E-019", target: "milestone:M5", type: "relates-to" } + + # ── relates-to: LOW CONFIDENCE (Q5 ground truth — 4 edges) ───── + - { source: "issue:E-016", target: "issue:E-017", type: "relates-to", confidence: 0.2 } + - { source: "issue:E-018", target: "crate:echo-crypto", type: "relates-to", confidence: 0.3 } + - { source: "issue:E-019", target: "spec:003-web-sockets", type: "relates-to", confidence: 0.4 } + - { source: "issue:E-020", target: "doc:contributing", type: "relates-to", confidence: 0.3 } diff --git a/test/proving-ground.test.js b/test/proving-ground.test.js new file mode 100644 index 00000000..0b92601c --- /dev/null +++ b/test/proving-ground.test.js @@ -0,0 +1,283 @@ +/** + * PROVING GROUND — Integration tests for dogfood validation. + * + * Imports the Echo ecosystem seed YAML and answers 5 real project + * management questions against the resulting graph. Each question + * maps to an existing view or API. + * + * Ground truth is baked into the seed design (test/fixtures/echo-seed.yaml). + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { tmpdir } from 'node:os'; +import { execSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { initGraph } from '../src/graph.js'; +import { importFile } from '../src/import.js'; +import { renderView } from '../src/views.js'; +import { computeStatus } from '../src/status.js'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const SEED_PATH = resolve(__dirname, 'fixtures', 'echo-seed.yaml'); + +describe('PROVING GROUND', () => { + let tempDir; + let graph; + let importResult; + + beforeAll(async () => { + tempDir = await mkdtemp(join(tmpdir(), 'gitmind-proving-ground-')); + execSync('git init', { cwd: tempDir, stdio: 'ignore' }); + graph = await initGraph(tempDir); + importResult = await importFile(graph, SEED_PATH); + }); + + afterAll(async () => { + await rm(tempDir, { recursive: true, force: true }); + }); + + // ── PRV-002: Seed import ───────────────────────────────────── + + describe('seed import', () => { + it('imports the Echo seed without errors', () => { + expect(importResult.valid).toBe(true); + expect(importResult.errors).toEqual([]); + expect(importResult.dryRun).toBe(false); + }); + + it('produces expected node count', async () => { + const nodes = await graph.getNodes(); + expect(nodes.length).toBe(55); + }); + + it('produces expected edge count', async () => { + const edges = await graph.getEdges(); + expect(edges.length).toBe(70); + }); + }); + + // ── PRV-003: Five dogfood questions ────────────────────────── + + describe('Q1: What blocks M2?', () => { + it('milestone view identifies issue:E-003 and issue:E-004 as M2 blockers', async () => { + const result = await renderView(graph, 'milestone'); + const m2 = result.meta.milestoneStats['milestone:M2']; + + expect(m2).toBeDefined(); + expect(m2.blockers).toHaveLength(2); + expect(m2.blockers).toContain('issue:E-003'); + expect(m2.blockers).toContain('issue:E-004'); + }); + + it('M2 has 2 children and 0 done', async () => { + const result = await renderView(graph, 'milestone'); + const m2 = result.meta.milestoneStats['milestone:M2']; + + expect(m2.total).toBe(2); + expect(m2.done).toBe(0); + expect(m2.pct).toBe(0); + }); + }); + + describe('Q2: Which ADRs lack implementation?', () => { + it('traceability view finds adr:003 and adr:004 as gaps', async () => { + const result = await renderView(graph, 'traceability'); + const adrGaps = result.meta.gaps.filter(g => g.startsWith('adr:')); + + expect(adrGaps).toHaveLength(2); + expect(adrGaps).toContain('adr:003-encryption-at-rest'); + expect(adrGaps).toContain('adr:004-rest-vs-grpc'); + }); + + it('3 of 5 ADRs are implemented', async () => { + const result = await renderView(graph, 'traceability'); + const adrCovered = result.meta.covered.filter(c => c.startsWith('adr:')); + + expect(adrCovered).toHaveLength(3); + expect(adrCovered).toContain('adr:001-event-sourcing'); + expect(adrCovered).toContain('adr:002-postgres'); + expect(adrCovered).toContain('adr:005-crate-structure'); + }); + }); + + describe('Q3: Which crates are unlinked to specs?', () => { + it('coverage view finds 9 unlinked crates', async () => { + const result = await renderView(graph, 'coverage'); + + expect(result.meta.unlinked).toHaveLength(9); + expect(result.meta.unlinked).toContain('crate:echo-crypto'); + expect(result.meta.unlinked).toContain('crate:echo-cli'); + expect(result.meta.unlinked).toContain('crate:echo-config'); + expect(result.meta.unlinked).toContain('crate:echo-log'); + expect(result.meta.unlinked).toContain('crate:echo-test-utils'); + expect(result.meta.unlinked).toContain('crate:echo-bench'); + expect(result.meta.unlinked).toContain('crate:echo-migrate'); + expect(result.meta.unlinked).toContain('crate:echo-plugin'); + expect(result.meta.unlinked).toContain('crate:echo-sdk'); + }); + + it('6 crates are linked', async () => { + const result = await renderView(graph, 'coverage'); + + expect(result.meta.linked).toHaveLength(6); + expect(result.meta.coveragePct).toBe(40); + }); + }); + + describe('Q4: What should a new engineer read first?', () => { + it('onboarding view puts doc:getting-started before doc:architecture-overview', async () => { + const result = await renderView(graph, 'onboarding'); + const order = result.meta.readingOrder; + + const gsIdx = order.indexOf('doc:getting-started'); + const aoIdx = order.indexOf('doc:architecture-overview'); + + expect(gsIdx).toBeGreaterThanOrEqual(0); + expect(aoIdx).toBeGreaterThan(gsIdx); + }); + + it('doc:architecture-overview appears before doc:api-reference', async () => { + const result = await renderView(graph, 'onboarding'); + const order = result.meta.readingOrder; + + const aoIdx = order.indexOf('doc:architecture-overview'); + const arIdx = order.indexOf('doc:api-reference'); + + expect(arIdx).toBeGreaterThan(aoIdx); + }); + }); + + describe('Q5: What is low-confidence?', () => { + it('suggestions view finds exactly 4 low-confidence edges', async () => { + const result = await renderView(graph, 'suggestions'); + expect(result.edges).toHaveLength(4); + }); + + it('computeStatus reports 4 low-confidence edges', async () => { + const status = await computeStatus(graph); + expect(status.health.lowConfidence).toBe(4); + }); + + it('low-confidence edges have expected confidence values', async () => { + const result = await renderView(graph, 'suggestions'); + const confidences = result.edges + .map(e => e.props?.confidence) + .sort((a, b) => a - b); + expect(confidences).toEqual([0.2, 0.3, 0.3, 0.4]); + }); + }); + + // ── PRV-004: Complexity verification ────────────────────────── + + describe('complexity', () => { + /** + * Generate a synthetic graph with ~N nodes and ~0.7N edges. + * Uses the same prefixes/types as echo-seed so views exercise real code paths. + */ + async function generateGraph(nodeCount) { + const dir = await mkdtemp(join(tmpdir(), 'gitmind-complexity-')); + execSync('git init', { cwd: dir, stdio: 'ignore' }); + const g = await initGraph(dir); + + const patch = await g.createPatch(); + + // Distribute nodes across 6 prefixes + const nMilestones = Math.floor(nodeCount / 5); + const nTasks = Math.floor(nodeCount / 5); + const nSpecs = Math.floor(nodeCount / 5); + const nCrates = Math.floor(nodeCount / 5); + const nDocs = Math.floor(nodeCount / 10); + const nIssues = Math.floor(nodeCount / 10); + + // Create nodes + for (let i = 0; i < nMilestones; i++) { + patch.addNode(`milestone:M${i}`); + patch.setProperty(`milestone:M${i}`, 'title', `Milestone ${i}`); + patch.setProperty(`milestone:M${i}`, 'status', i % 3 === 0 ? 'complete' : 'planned'); + } + for (let i = 0; i < nTasks; i++) { + patch.addNode(`task:T${i}`); + patch.setProperty(`task:T${i}`, 'title', `Task ${i}`); + } + for (let i = 0; i < nSpecs; i++) { + patch.addNode(`spec:S${i}`); + patch.setProperty(`spec:S${i}`, 'title', `Spec ${i}`); + } + for (let i = 0; i < nCrates; i++) { + patch.addNode(`crate:C${i}`); + patch.setProperty(`crate:C${i}`, 'description', `Crate ${i}`); + } + for (let i = 0; i < nDocs; i++) { + patch.addNode(`doc:D${i}`); + patch.setProperty(`doc:D${i}`, 'title', `Doc ${i}`); + } + for (let i = 0; i < nIssues; i++) { + patch.addNode(`issue:I${i}`); + patch.setProperty(`issue:I${i}`, 'title', `Issue ${i}`); + } + + // belongs-to: tasks → milestones + for (let i = 0; i < nTasks; i++) { + patch.addEdge(`task:T${i}`, `milestone:M${i % nMilestones}`, 'belongs-to'); + } + // implements: crates → specs + for (let i = 0; i < Math.min(nCrates, nSpecs); i++) { + patch.addEdge(`crate:C${i}`, `spec:S${i}`, 'implements'); + } + // depends-on: crate → crate chain + for (let i = 1; i < nCrates; i++) { + patch.addEdge(`crate:C${i}`, `crate:C${i - 1}`, 'depends-on'); + } + // depends-on: doc → doc chain (for onboarding view) + for (let i = 1; i < nDocs; i++) { + patch.addEdge(`doc:D${i}`, `doc:D${i - 1}`, 'depends-on'); + } + // 4 low-confidence relates-to edges (for suggestions view) + if (nIssues >= 2 && nCrates >= 1) { + patch.addEdge(`issue:I0`, `issue:I1`, 'relates-to'); + patch.setEdgeProperty(`issue:I0`, `issue:I1`, 'relates-to', 'confidence', 0.2); + patch.addEdge(`issue:I1`, `crate:C0`, 'relates-to'); + patch.setEdgeProperty(`issue:I1`, `crate:C0`, 'relates-to', 'confidence', 0.3); + } + if (nIssues >= 4) { + patch.addEdge(`issue:I2`, `issue:I3`, 'relates-to'); + patch.setEdgeProperty(`issue:I2`, `issue:I3`, 'relates-to', 'confidence', 0.4); + patch.addEdge(`issue:I3`, `issue:I0`, 'relates-to'); + patch.setEdgeProperty(`issue:I3`, `issue:I0`, 'relates-to', 'confidence', 0.1); + } + + await patch.commit(); + return { graph: g, dir }; + } + + it('all 5 views scale sub-quadratically (O(N+E))', async () => { + const sizes = [100, 500, 2500, 12500]; + const timings = []; + + for (const size of sizes) { + const { graph: g, dir } = await generateGraph(size); + try { + const start = performance.now(); + await renderView(g, 'milestone'); + await renderView(g, 'traceability'); + await renderView(g, 'coverage'); + await renderView(g, 'onboarding'); + await renderView(g, 'suggestions'); + timings.push(performance.now() - start); + } finally { + await rm(dir, { recursive: true, force: true }); + } + } + + // Check growth factors between consecutive 5x size steps. + // Linear (O(N+E)) ≈ 5x growth. Quadratic (O(N²)) = 25x growth. + // Threshold of 15x catches O(N²) with margin for constant-factor overhead. + for (let i = 1; i < timings.length; i++) { + const growthFactor = timings[i] / Math.max(timings[i - 1], 1); + expect(growthFactor).toBeLessThan(15); + } + }, 120_000); + }); +}); diff --git a/test/views.test.js b/test/views.test.js index 6aa94b70..98a666c7 100644 --- a/test/views.test.js +++ b/test/views.test.js @@ -34,6 +34,7 @@ describe('views', () => { expect(views).toContain('traceability'); expect(views).toContain('blockers'); expect(views).toContain('onboarding'); + expect(views).toContain('coverage'); }); it('renderView throws for unknown views', async () => { @@ -329,4 +330,45 @@ describe('views', () => { expect(result.nodes).toContain('spec:b'); }); }); + + // ── PROVING GROUND: coverage view ──────────────────────────── + + describe('coverage view', () => { + it('identifies crates not linked to any spec/ADR', async () => { + await createEdge(graph, { source: 'crate:linked', target: 'spec:auth', type: 'implements' }); + await createEdge(graph, { source: 'crate:orphan', target: 'crate:linked', type: 'depends-on' }); + + const result = await renderView(graph, 'coverage'); + expect(result.meta.linked).toContain('crate:linked'); + expect(result.meta.unlinked).toContain('crate:orphan'); + expect(result.meta.coveragePct).toBe(50); + }); + + it('reports 100% when all crates implement specs', async () => { + await createEdge(graph, { source: 'crate:a', target: 'spec:x', type: 'implements' }); + await createEdge(graph, { source: 'crate:b', target: 'adr:001', type: 'implements' }); + + const result = await renderView(graph, 'coverage'); + expect(result.meta.unlinked).toEqual([]); + expect(result.meta.coveragePct).toBe(100); + }); + + it('handles graph with no crate/module/pkg nodes', async () => { + await createEdge(graph, { source: 'task:a', target: 'task:b', type: 'blocks' }); + + const result = await renderView(graph, 'coverage'); + expect(result.meta.linked).toEqual([]); + expect(result.meta.unlinked).toEqual([]); + expect(result.meta.coveragePct).toBe(100); + }); + + it('includes module and pkg nodes', async () => { + await createEdge(graph, { source: 'module:auth', target: 'spec:auth', type: 'implements' }); + await createEdge(graph, { source: 'pkg:utils', target: 'task:a', type: 'relates-to' }); + + const result = await renderView(graph, 'coverage'); + expect(result.meta.linked).toContain('module:auth'); + expect(result.meta.unlinked).toContain('pkg:utils'); + }); + }); });