GraFOSa Design Decisions — First Principles Analysis
Date: 2026-03-13
Source audit: System review (Track 1 + Track 2), architecture docs, 62,982 LOC
JIRA epic: MD-8630 (System Review Remediation)
Method: Challenge every established rule from first principles. The heresy must be heard before any decision is conscious.
Decision Matrix
| # | Decision | Established Rule | Heresy | Final Call |
|---|---|---|---|---|
| 1 | Graph-first abstraction | Model code as graphs | Code is sequential, not structural | Keep graph — but acknowledge the map is not the territory |
| 2 | In-memory kernel | Speed requires in-memory | Memory is the wrong constraint | In-memory for analysis, external DB for persistence |
| 3 | Layer architecture (L0-L10) | Higher layers compose lower | 11 layers is a bureaucracy | Collapse to 4 tiers, keep layers as internal detail |
| 4 | Single kernel, dual protocol | Code + Workflow share one engine | Different domains need different engines | Keep unified kernel — but extract protocol adapters |
| 5 | MCP as primary interface | 156 tools = maximum capability | 156 tools = maximum confusion | Tiered tools: 12 essential, 40 standard, 104 advanced |
| 6 | Pure Python ML | Zero heavy deps = universal install | Reinventing scikit-learn poorly | Pure Python for embed, sklearn for standalone |
| 7 | Thompson Sampling everywhere | Explore-exploit is optimal | Bandits solve the wrong problem | Bandits for selection, deterministic for known-good |
| 8 | Shadow graphs for simulation | Copy-on-write is elegant | Simulations should be functional, not stateful | Functional transforms + immutable snapshots |
| 9 | Library, not a service | Embed like PostgreSQL | Libraries can't scale horizontally | Library-first, service-optional — the SQLite model |
| 10 | Spectral methods for health | Eigenvalues reveal hidden structure | Most users can't interpret eigenvalues | Spectral underneath, human language on top |
| 11 | Scan-then-analyze pipeline | Ingest everything, then query | Stale graphs are worse than no graph | Incremental, event-driven — the LSP model |
| 12 | Validation as enforcement | Gates prevent bad changes | Gates prevent ALL changes (including good ones) | Advisory by default, enforced only for irreversible ops |
Decision 1: Graph-First Abstraction
Established rule: Codebases are graphs. Files import files. Functions call functions. Services depend on services. Model everything as nodes and edges, answer structural questions through traversal.
Heresy: Code is fundamentally sequential, not structural. Developers think in execution order, not dependency graphs. A call graph is a lossy abstraction — it captures "who calls whom" but not "why" or "when" or "what state." By forcing everything into graph form, you lose the temporal, conditional, and contextual dimensions that make code comprehensible.
Deeper heresy: The graph is always stale. The moment you scan a codebase, it's already changing. A graph that's 5 minutes old can lie about dependencies that were just added. Developers who trust the graph more than git diff will make worse decisions, not better ones.
Counter-argument: Graphs compress combinatorial complexity. A 67K-node graph with 95K edges encodes relationships that no human could hold in working memory. You don't read code by tracing every import chain — but your CI/CD should. The graph isn't for reading; it's for automated reasoning about structural properties that humans can't see.
Conscious call: Keep the graph-first abstraction, but be honest about what it captures and what it doesn't. The map is not the territory. Three rules:
1. Never claim the graph IS the codebase. It's a structural projection. Semantic meaning, business logic, runtime behavior, and developer intent are NOT in the graph.
2. Always display staleness. Every graph query should show "analyzed at: X, Y minutes ago." Never present a graph as "the truth" — present it as "the truth as of."
3. Combine with runtime signals. A graph alone can't tell you if code is fast or slow, correct or broken, secure or vulnerable. Spectral metrics + static analysis + runtime telemetry = useful. Graph alone = half the story.
What could have been built instead:
- AST-first analysis (tree-sitter/semgrep model): Operate directly on syntax trees. More precise, captures control flow, but limited to single-file scope.
- Trace-first analysis (OpenTelemetry model): Instrument runtime, capture actual call paths. Perfectly accurate for what ran, blind to dead code.
- Diff-first analysis (Graphite/Reviewbot model): Only analyze what changed. Fast, relevant, but no big-picture view.
The graph-first approach trades precision for scope. This is the right trade-off for architectural reasoning but the wrong one for line-level correctness.
Decision 2: In-Memory Kernel
Established rule: Graph queries must be fast. Sub-second PageRank, instant BFS. The only way to achieve this is to hold the entire graph in Python dicts with indexed lookups.
Heresy: Memory is the wrong constraint to optimize for. A 67K-node graph fits in memory today. A 1M-node graph (monorepo scale) does not — it needs 2-3 GB for nodes alone, plus indices, plus cached algorithm results, plus transaction snapshots. The in-memory model works at current scale and breaks at the next order of magnitude.
Deeper heresy: The in-memory model forces serialization bottlenecks. Only one process can hold the graph. MCP server is single-threaded with global state. CI/CD can't parallelize analysis across workers. The "fast" in-memory approach creates a single point of serialization that ultimately makes the SYSTEM slower, even if individual QUERIES are faster.
Counter-argument: External graph databases (Neo4j, DGraph, TigerGraph) add network latency, deployment complexity, and version management. For a tool that runs as a library embedded in developer workflows, requiring a database server is a non-starter. The in-memory model's limitation is precisely its virtue: zero infrastructure.
Conscious call: In-memory for analysis, external persistence for scale. The SQLite model: the library holds the working set in memory, persists to disk (JSON, SQLite, or Parquet), and external databases are optional adapters.
Refined architecture:
┌──────────────┐
│ GraphKernel │ ← Always in-memory, always fast
│ (working │
│ set) │
└──────┬───────┘
│ sync on demand
┌────────────┼────────────┐
│ │ │
┌─────┴─────┐ ┌───┴────┐ ┌────┴────┐
│ JSON file │ │ SQLite │ │ Neo4j │
│ (portable) │ │ (fast) │ │ (scale) │
└───────────┘ └────────┘ └─────────┘What should change:
- Default persistence: SQLite (not JSON). JSON is O(V+E) to read/write; SQLite enables incremental updates.
- Working set: Keep in-memory kernel for the hot graph (current project). Cold data stays on disk.
- Scale path: For monorepo-scale (1M+ nodes), partition by service. Each service gets its own kernel. Cross-service queries go through a thin federation layer.
What could have been built instead:
- Neo4j-native (CodeIntelligence, Sourcegraph): External DB handles all storage and queries. Pro: infinite scale, Cypher queries. Con: requires infrastructure, 10-50ms per query vs 0.1ms.
- SQLite-first (Fossil, DuckDB model): Everything in a single file. Pro: portable, no server. Con: no in-memory speed for algorithms.
- Append-only log (event sourcing): Store mutations, reconstruct graph on demand. Pro: full audit trail, time-travel. Con: reconstruction latency.
Decision 3: Layer Architecture (L0-L10)
Established rule: Each layer builds on the previous one. L0 (kernel) provides primitives. L1 (filesystem) adds file semantics. L2 (modules) adds import resolution. All the way to L10 (optimizer). This layered composition provides clean separation of concerns.
Heresy: 11 layers is not clean separation — it's architectural anxiety. Most users need exactly 3 things: "what depends on what", "what breaks if I change this", and "how healthy is my codebase." They don't care whether the answer comes from L5 (execution) or L7 (intelligence). The layer model serves the implementor's mental model, not the user's problem.
Deeper heresy: The layers aren't actually independent. L7 (intel) imports L1-L6. L8 (strategy) imports L0-L7. L6 (scheduler) takes 6 constructor arguments. The "clean layering" creates a dependency tree that's actually a dependency web — and the constructor signature of GraphStrategy (9 positional args) is proof.
Counter-argument: The layers emerged from bottom-up construction, not top-down design. Each layer was added when a new capability was needed. The alternative — a monolithic analyzer — would be a 10,000-line god object. At least with layers, you can test L1 without L7.
Conscious call: Collapse the 11 layers into 4 user-facing tiers. Keep layers as internal implementation detail.
Proposed tier model:
| Tier | Purpose | Internal Layers | User Sees |
|---|---|---|---|
| Graph | Build and query the graph | L0 (kernel), L1 (fs), L2 (modules), L3 (network) | kernel.query(), kernel.add_node() |
| Analysis | Understand the graph | L4 (process), L5 (execution), L9 (spectral), info_theory | analyze.health(), analyze.impact() |
| Decision | Decide what to do | L6 (scheduler), L7 (intel), L8 (strategy), L10 (optimizer) | plan.deploy(), plan.optimize() |
| Learning | Improve over time | ML kernel, bandits, outcomes, co-change | learn.predict(), learn.record() |
The user thinks: "Build graph → Analyze it → Make decisions → Learn from outcomes."
The implementation knows: L0→L1→L2→L3→L4→L5→L6→L7→L8→L9→L10.
The user never needs to know which numbered layer answered their question.
What this changes: The LayerFactory (Sprint 2 task) becomes essential. Users create a GrafosaStack(kernel) and get back four tier objects, not 11 layers. The 11-layer model stays inside as an implementation detail, invisible to the public API.
What could have been built instead:
- Plugin architecture (LSP model): Core engine + plugins. Each plugin registers capabilities. Pro: extensible, decoupled. Con: plugin discovery/loading complexity.
- Pipeline model (Unix pipes): Each stage transforms data.
scan | resolve | trace | score | report. Pro: composable, parallelizable. Con: hard to share state between stages. - Single-class model (SonarQube): One analyzer, many rules. Pro: simple API. Con: 10,000-line god object.
Decision 4: Single Kernel, Dual Protocol
Established rule: The same GraphKernel handles both Code Protocol (files, symbols, imports) and Workflow Protocol (services, teams, DEPENDS_ON). One engine, two vocabularies.
Heresy: Code graphs and workflow graphs have fundamentally different properties. Code graphs are dense, hierarchical, and computed (from source). Workflow graphs are sparse, flat, and authored (by humans). Code graph queries are structural ("what calls this function?"). Workflow graph queries are relational ("what depends on auth-service?"). Cramming both into the same kernel optimizes neither.
Counter-argument: The power of a unified kernel is cross-protocol queries. "Which services are affected if this file changes?" requires traversing from Code Protocol (file → symbol → endpoint) to Workflow Protocol (endpoint → service → DEPENDS_ON → service). Two separate engines can't answer this without a join layer.
Conscious call: Keep the unified kernel. The cross-protocol query capability is too valuable to sacrifice. But extract protocol-specific adapters:
# Instead of:
kernel.add_node("File", {"path": "auth.py"}) # Code protocol
kernel.add_node("Service", {"name": "auth"}) # Workflow protocol
# Use adapters:
code = CodeAdapter(kernel) # Type-safe code operations
code.add_file("auth.py", language="python")
workflow = WorkflowAdapter(kernel) # Type-safe workflow operations
workflow.add_service("auth", tier="critical")
# Cross-protocol queries go through kernel directly:
kernel.trace_path("file:auth.py", "service:auth", via=["DEFINES", "SERVES"])What this preserves: Cross-protocol blast radius analysis, unified PageRank across both protocols, single graph visualization.
What this adds: Type safety, protocol-specific validation, clear API boundaries.
Decision 5: 156 MCP Tools
Established rule: More tools = more capability. Every graph operation should be directly invocable by an AI agent. 156 tools covers everything from graph_stats to restructuring_proposals.
Heresy: 156 tools is a maze, not an API. No human or AI can hold 156 tool signatures in working memory. The tool list itself requires a search tool to navigate. When your API needs its own search engine, your API is too large.
Deeper heresy: Most of the 156 tools exist because each was easy to add (a decorated function). Nobody asked "does the user need this as a separate tool, or would it be a parameter on an existing tool?" The result: spectral_profile, spectral_health, spectral_radius_metric, pareto_score — 4 tools that could be 1 tool with a metric parameter.
Counter-argument: AI agents (Claude, GPT) can handle large tool sets because they have full context window access to descriptions. Collapsing tools into fewer with more parameters actually makes discovery harder for AI — a tool named spectral_health is more discoverable than spectral(metric="health").
Conscious call: Tiered tools with progressive disclosure.
| Tier | Count | Description | When to Show |
|---|---|---|---|
| Essential | 12 | Core workflow: stats, lookup, impact, deps, health, predict | Always |
| Standard | 40 | Common analysis: trace, cycles, bottlenecks, plan, dead-code | When graph loaded |
| Advanced | 104 | Specialized: spectral, optimizer, MCTS, cortex, temporal | On demand |
Implementation:
@mcp.tool(tier="essential") # Always visible
def project_health(): ...
@mcp.tool(tier="standard") # Visible when graph loaded
def blast_radius(entity): ...
@mcp.tool(tier="advanced") # On demand
def spectral_profile(): ...The AI agent starts with 12 tools. As it learns the graph is loaded and the user asks deeper questions, standard and advanced tools become visible. This is not hiding — it's progressive disclosure with context-sensitive availability.
What could have been built instead:
- Natural language interface only (ChatGPT model): One tool —
ask(question). The NL engine routes to the right analysis. Pro: zero learning curve. Con: ambiguous, unpredictable. - Query language (GraphQL/Cypher model): One tool —
query(cypher_expression). Pro: unlimited expressiveness. Con: requires learning a query language. - Composite actions (GitHub Actions model): Workflows that chain multiple tools. Pro: reusable recipes. Con: workflow authoring complexity.
Decision 6: Pure Python ML Models
Established rule: GraFOSa's ML kernel implements Random Forest, Logistic Regression, Decision Trees, and Z-Score anomaly detection in pure Python. This enables zero-dependency embedding — any service can from grafosa.ml import LearningKernel without installing numpy/sklearn.
Heresy: Reimplementing sklearn in pure Python is academic hubris. The pure Python Random Forest will always be slower, less tested, and less accurate than sklearn's C-optimized implementation. Every bug in the pure Python RF is a bug that sklearn fixed 10 years ago.
Deeper heresy: The "zero dependency" goal is a false economy. Any serious ML use requires numpy for feature engineering anyway. And the Learning Kernel's value isn't in the model implementation — it's in the bandit-based model SELECTION. The selection framework works regardless of whether the selected model is pure Python or sklearn.
Counter-argument: The Three Pillars architecture requires GraFOSa to be embeddable in Nous (Python), Symposium (Python), and potentially TypeScript services. A heavy dependency chain (numpy → scipy → sklearn) makes embedding painful. The pure Python models are intentionally simple — they exist as fallbacks, not as the primary model.
Conscious call: Keep pure Python models for embedding, use sklearn for standalone analysis. The model registry already supports both:
# When embedded in another service (no sklearn):
registry.register("rf_pure", PurePythonRF())
# When running standalone (sklearn available):
registry.register("rf_sklearn", SklearnRFWrapper())
# Bandit selects the best performer regardless of implementation:
best_model = bandit.select()What matters is not the model — it's the selection loop. The Thompson Sampling bandit converges on the best available model. If sklearn is available, it wins. If only pure Python is available, the system still works. This is correct design.
What should change: Don't invest more in pure Python model accuracy. If sklearn is absent, the models are "good enough" for basic prediction. If sklearn is present, it will always win the bandit tournament. Accept this.
Decision 7: Thompson Sampling for Model Selection
Established rule: Use Thompson Sampling (Beta posterior, explore-exploit) for every decision: which ML model to use, which analysis to run, which threshold to apply.
Heresy: Bandits solve the wrong problem. Bandit algorithms are designed for problems where (a) you don't know which arm is best, and (b) each pull gives you a noisy reward signal. Model selection fits this. But many GraFOSa decisions are NOT bandit problems:
- "Should I use sparse or dense eigenvalue decomposition?" This is deterministic — if n > 5000, use sparse. There's nothing to explore.
- "What threshold should I use for anomaly detection?" This is a calibration problem, not an exploration problem.
- "Which community detection algorithm is best?" Label propagation vs Louvain is a research question, not a bandit question.
Counter-argument: The bandit framework is a clean abstraction for "try things, measure results, converge." It naturally handles the cold-start problem (explore when uncertain) and the non-stationarity problem (re-explore when things change). Even for deterministic decisions, framing them as bandits enables automatic adaptation when the underlying assumptions change.
Conscious call: Bandits for selection, deterministic for known-good. Use bandits ONLY where:
1. There are genuinely competing alternatives with unknown performance
2. Reward signals are available (predictions can be verified)
3. The exploration cost is low (trying the "wrong" model doesn't break anything)
Not bandits for:
- Algorithm parameter selection (sparse/dense cutoff) — use measured heuristics
- Threshold calibration — use AccuracyTracker + statistical tests
- Architecture decisions — use deterministic rules
What could have been built instead:
- AutoML-style search (Optuna, Ray Tune): Bayesian optimization over hyperparameter space. Pro: finds optimal configs. Con: expensive, overkill for small model sets.
- Ensemble (voting/stacking): Run all models, combine predictions. Pro: robust. Con: N× compute cost.
- Fixed model + confidence intervals: Pick the theoretically best model, track calibration. Pro: simple, interpretable. Con: no adaptation.
Decision 8: Shadow Graphs for Simulation
Established rule: Shadow graphs (copy-on-write overlays) enable "what-if" analysis. Create a shadow, mutate it, observe the effects, discard or merge.
Heresy: Simulations should be functional, not stateful. A shadow graph is a mutable copy that accumulates state changes. If the simulation is complex (multi-step what-if), the shadow becomes hard to reason about — which mutations happened? In what order? Can you replay them? The stateful approach makes it hard to compare scenarios because each shadow is a unique snowflake of accumulated changes.
Counter-argument: The shadow graph exists because the kernel is mutable. If the kernel were immutable (persistent data structure), you wouldn't need shadows — every operation would return a new kernel. But immutable kernels in Python are expensive (copying 67K nodes per operation).
Conscious call: Functional transforms + immutable snapshots. The right model:
# Instead of:
shadow = kernel.create_shadow()
shadow.remove_node("auth-service") # Mutate shadow
impact = analyze(shadow) # Analyze mutated state
shadow.discard() # Throw away
# Prefer:
scenario = Scenario(kernel)
scenario.remove("auth-service") # Record the transform
scenario.remove("user-service") # Record another transform
impact = scenario.evaluate() # Apply ALL transforms, analyze result
# Scenario is immutable — can be stored, replayed, comparedThe scenario object is a RECIPE, not a RESULT. You can compare two scenarios by comparing their transform lists. You can replay a scenario on a different kernel. You can serialize it, share it, version it.
What the shadow graph is good for: The optimizer (SA, NSGA-II) needs rapid mutation-test-undo cycles. For this, shadows + copy-on-write is correct. The problem (flagged in the system review) is that the optimizer uses copy.deepcopy() instead of the existing shadow API. Fix: use shadows in the optimizer.
Decision 9: Library, Not a Service
Established rule: GraFOSa is a library (the PostgreSQL model). Each consumer embeds its own instance in-process. No central server, no network calls, no deployment to manage.
Heresy: Libraries can't scale horizontally. If Nous, Symposium, and Conductor each embed their own GraFOSa instance, they each have their own graph. Three independent copies of the same codebase graph, none aware of the others' mutations. The "library" model trades operational simplicity for data consistency.
Deeper heresy: The MCP server contradicts the "library" model. It IS a service — it runs as a process, accepts tool calls over stdio/SSE, maintains global state. GraFOSa already has a service form; it just doesn't admit it.
Counter-argument: The library model's strength is zero-coordination. Each consumer's graph is built from the same source code, so they converge to the same state. There's no consistency problem because the source code is the single source of truth. The graph is a CACHE of structural analysis, not a database of record.
Conscious call: Library-first, service-optional. The SQLite model:
┌──────────────────────────────────────┐
│ Source Code (ground truth) │
└────────────────┬─────────────────────┘
│ scan
┌────────┴────────┐
│ Shared Graph │ ← SQLite file or shared JSON
│ (on disk) │
└───┬─────┬───┬───┘
│ │ │ embed
┌────┘ │ └────┐
│ │ │
┌────┴───┐ ┌───┴──┐ ┌───┴───┐
│ Nous │ │ MCP │ │ CI │
│ (lib) │ │(svc) │ │ (lib) │
└────────┘ └──────┘ └───────┘- Embed mode (library):
from grafosa import GraphKernel; kernel = GraphKernel.load("graph.sqlite"). In-process, fast, no server. - Server mode (MCP):
python3 -m grafosa.mcp_server. Runs as a process, exposes 156 tools. For Claude Code integration. - CI mode (library):
python3 -m grafosa.validate --graph graph.sqlite. Runs in pipeline, validates changes.
All three modes read the same graph file. The file is the coordination mechanism, not a server. This is how SQLite works, and it scales to millions of users.
Decision 10: Spectral Methods for Graph Health
Established rule: Eigenvalue-based metrics (Fiedler value, spectral radius, eigengap, von Neumann entropy) reveal structural properties invisible to simpler heuristics. The Fiedler value measures graph connectivity. The spectral gap indicates community structure. Heat kernel signatures detect anomalies.
Heresy: Most developers can't interpret eigenvalues. "Your Fiedler value is 0.0023" means nothing to someone debugging a deployment. Spectral methods are mathematically elegant but communicationally opaque. If the user can't act on a metric, the metric serves the tool's ego, not the user's need.
Counter-argument: The value of spectral methods is not in the raw numbers — it's in the DERIVED insights. "Your codebase has a bottleneck at auth-service" is actionable. "Your Fiedler value dropped 15% since last week" indicates structural degradation. The user doesn't need to understand eigenvalues; they need to understand the CONCLUSIONS.
Conscious call: Spectral underneath, human language on top. The scorecard pattern (already in spectral.py) is correct — translate metrics to traffic lights (green/yellow/red) with plain-language explanations.
The ideal output:
Codebase Health: B+ (improved from B-)
Strengths:
✓ No circular dependencies in core modules
✓ Service isolation is strong (Fiedler: 0.45, healthy range)
Concerns:
⚠ auth-service is a single point of failure (betweenness: 0.23, threshold: 0.15)
⚠ Modularity declining — 3 cross-cutting concerns detected this week
Actions:
1. Extract auth validation into a shared library (reduces betweenness by ~40%)
2. Review the 3 new cross-module imports added in last 5 commitsThe user sees: actions. The system computes: eigenvalues. The translation layer is where all the value lives.
What could have been built instead:
- Rule-based heuristics only (ESLint/SonarQube model): "If fan-in > 20, flag as coupling risk." Pro: interpretable, tunable. Con: can't detect emergent structural properties.
- ML-based scoring (CodeClimate model): Train on "good" vs "bad" codebases. Pro: learns what matters. Con: opaque, needs training data.
- Diff-based trending (only): Don't score the graph. Score the CHANGES. "This PR increases coupling by 5%." Pro: always actionable. Con: no baseline.
Decision 11: Scan-Then-Analyze Pipeline
Established rule: GraFOSa scans the entire codebase, builds a complete graph, then runs analysis. PythonScanner scans all .py files. TypeScript scanner scans all .ts files. The full graph is the input to every query.
Heresy: Full scans are expensive and produce stale graphs. A maestro-platform scan produces 67K nodes — how long does that take? If it takes 30 seconds, every analysis starts with 30 seconds of waiting. And the graph is immediately stale — any edit during analysis invalidates it. The batch-scan model is a holdover from static analysis tools designed for nightly runs, not interactive developer workflows.
Counter-argument: Full scans provide a complete picture. Incremental analysis can miss transitive effects — if file A imports file B which imports file C, changing C affects A, but incremental analysis focused on C might not trace to A unless the full import graph exists.
Conscious call: Incremental, event-driven analysis. The LSP (Language Server Protocol) model:
1. Initial scan: Full scan on first load (cold start). Build complete graph. Cache to disk.
2. Incremental updates: On file save, re-scan only changed files. Update affected nodes/edges. Invalidate cached metrics for affected subgraph.
3. Event-driven: Sentinel already watches for file/git changes. Wire it to incremental graph updates instead of full re-scans.
4. Lazy expansion: When a query touches a file not yet scanned (e.g., external dependency), scan it on demand and add to graph.
What exists today:
GraphDeltaandGitGraphDeltaindelta.py— designed for exactly this purposeSentinelwatches for file changes and can trigger updatesAccuracyTrackercan detect when graph-based predictions diverge from reality
What's missing: The wiring. Sentinel events → delta computation → kernel update → cache invalidation → metric recomputation. The pieces exist but aren't connected into a live pipeline.
Decision 12: Validation as Enforcement
Established rule: The validation gate (python3 -m grafosa.validate) exits non-zero when issues exceed the max-severity threshold. The stop hook runs validation automatically when Claude Code finishes editing. This enforces structural quality.
Heresy: Gates that block changes kill velocity. Every false positive is a developer reaching for --no-verify. The stricter the gate, the more developers learn to bypass it. And structural analysis has high false-positive rates — is a new circular dependency always bad? (Sometimes it's intentional.) Is high fan-in always bad? (Sometimes it's a well-designed utility.) Enforcement without context creates adversarial relationships between developers and their tools.
Counter-argument: Without enforcement, analysis is ignored. Every advisory-only tool eventually becomes a dashboard nobody checks. The value of the stop hook is that it can't be ignored.
Conscious call: Advisory by default, enforced only for irreversible operations.
| Category | Mode | Example |
|---|---|---|
| New circular dependency | Advisory (warn) | "This creates a cycle between A→B→A. Consider extracting shared logic." |
| Breaking a public API | Enforced (block) | "This removes endpoint /api/users. 3 consumers detected. Cannot proceed." |
| High fan-in increase | Advisory (warn) | "auth-service now has 42 dependents (was 38). Monitor for coupling risk." |
| Deploying to production | Enforced (block) | "Blast radius: 12 services, 40% of traffic. Requires explicit approval." |
The question is "can I undo this?", not "is this a best practice?" (Echoing Decision #8 from the Conduir first principles document.)
Cross-Cutting Themes
1. The map is not the territory. A graph is a structural projection of code. It captures topology but not semantics, intent, or runtime behavior. Always present graph insights as one perspective, not the truth.
2. Progressive disclosure, not progressive complexity. 156 tools is a maze. 12 essential tools that expand to 40 then 104 is a path. Layer complexity for the consumer, not the implementor.
3. Functional over stateful for analysis. Scenarios as recipes (transform lists), not as mutated copies. Immutable snapshots that can be compared, serialized, replayed.
4. Human language on top, math underneath. Eigenvalues and betweenness centrality are powerful. "Your auth service is a single point of failure — here are 3 ways to fix it" is useful. Never expose the math without the translation.
5. Incremental over batch. Full scans are for cold starts. Everything after that should be event-driven deltas. The LSP model, not the nightly-build model.
6. Advisory over enforcement (with exceptions). Block irreversible actions. Warn about everything else. Developers who trust the tool will follow warnings. Developers who don't trust it will find ways around enforcement anyway — and lose the warnings too.
7. Library-first, service-optional. Embedding in-process is always simpler than calling across a network. The MCP server is one interface, not the architecture. The graph file is the coordination mechanism, not a server.
Implementation Priority
| Priority | Decision | Current State | Change Needed | Sprint |
|---|---|---|---|---|
| P0 | #5 Tool tiering | 156 flat tools | Add tier metadata, filter by context | S2 |
| P0 | #3 Tier model | 11 raw layers exposed | Create GrafosaStack facade with 4 tiers | S2 |
| P1 | #10 Human translation | Raw metrics exposed | Scorecard + natural language layer | S4 |
| P1 | #11 Incremental analysis | Batch scan only | Wire Sentinel → Delta → Kernel updates | Future |
| P1 | #8 Functional scenarios | Shadow graphs (stateful) | Add Scenario class with transform lists | Future |
| P2 | #12 Advisory mode | Enforced by default | Make advisory default, enforce for irreversible | S4 |
| P2 | #2 SQLite persistence | JSON only | Add SQLite backend for GraphKernel | Future |
| P3 | #4 Protocol adapters | Raw kernel for both | Add CodeAdapter + WorkflowAdapter | Future |
Appendix: Alternative Architectures That Were Not Built
A. The Language Server Model
GraFOSa as an LSP-compatible language server. Runs as a background process, provides real-time graph insights as you edit. Integrates with VS Code, Neovim, etc. via standard LSP protocol.
Why it wasn't built: MCP was the chosen protocol (Claude Code integration). LSP serves a different consumer (editors, not AI agents).
Why it might have been better: Reaches 100% of developers, not just those using AI coding assistants. Provides real-time feedback, not batch analysis.
B. The Git Hook Model
GraFOSa as a git hook that runs on every commit. No interactive mode. Pure enforcement. Produces a structured report in .grafosa/report.json that CI reads.
Why it wasn't built: Too limiting. Interactive analysis (what-if, trace, explain) requires a query interface.
Why it might have been better: Simpler, more focused. Every developer already has git. No MCP, no server, no 156 tools. One tool: grafosa check.
C. The Database View Model
GraFOSa as SQL views over a code database. Scan populates tables (files, symbols, imports, endpoints). Queries are SQL. Analysis is GROUP BY + JOIN + window functions.
Why it wasn't built: Graph algorithms (PageRank, SCC, betweenness) don't map well to SQL. Recursive CTEs exist but are slow for deep traversals.
Why it might have been better: SQL is the most widely known query language. Every team has someone who can write SQL. Nobody outside GraFOSa can write kernel.query_nodes(NodePattern(label="File", properties={"service": "auth"})).
D. The Notebook Model
GraFOSa as a Jupyter notebook library. Import, scan, explore interactively. Visualize with matplotlib/plotly. Share notebooks as analysis artifacts.
Why it wasn't built: The target user (AI coding assistant) doesn't use notebooks.
Why it might have been better: Data scientists and architects DO use notebooks. The spectral analysis features are perfect for interactive exploration. This could be a secondary interface alongside MCP.