AI Initiative logoAI Initiative
Reading room / GraFOSa

GraFOSa Design Decisions — First Principles Analysis

Twelve first-principles challenges to graph-first analysis, persistence, layers, protocol adapters, tool design, classical ML, bandits, immutable simulations and deployment.

Architecture decision analysis
This is the retained source text, not a new validation of its claims. Numerical results and comparative claims require the accompanying source review before public reuse.

Read the connected case study or article →

Author not established in the source · 2026-03-13

Provenance, authorship and editorial notes

GraFOSa Design Decisions — First Principles Analysis

Id

grafosa-first-principles

Source path

grafosa/docs/DESIGN-DECISIONS-FIRST-PRINCIPLES.md

Source sha256

7bdf6f72832ff60a34671f23cb7a4188503875d447036b939aa050f30b9e6cb5

Bytes

35776

Repo root

grafosa

Repo head

388b50cfb34decee9d716f7f02d01ccf53305c03

Last git commit date

2026-03-14T00:46:21Z

Copy scope

Private full-text review candidate; not approved for public release.

Copy status

FULL_TEXT_BYTE_IDENTICAL

Metadata lines
  • **Date:** 2026-03-13
Author
Not recorded
Date

2026-03-13

Format

Markdown

Publication status

NOT_ESTABLISHED

Word count

5013

Disclosure concerns
  • Authorised private review copy. Preserve the original attribution and document status when selecting a public edition.
  • Document claims and numerical results are historical source assertions, not independently revalidated by this discovery.
  • Contains internal implementation plans, code examples and issue references; decisions/proposals must not become claims that every change shipped.
Author provenance

Not stated; not inferred from repository ownership

Date provenance

Explicit document date/update/capture field

Document status

Architecture decision analysis

Summary

Twelve first-principles challenges to graph-first analysis, persistence, layers, protocol adapters, tool design, classical ML, bandits, immutable simulations and deployment.

Recommended library type

Technical essay candidate

Attribution scope

GraFOSa professional-work source; retain explicit GraFOSa Research Team byline where present. Repository location alone does not identify individual authors.

Private review scan
Id

grafosa-first-principles

Scanned file

AI-Initiative-Architecture-Review-2026-10-26/research/publications/grafosa-first-principles.md

Sha256

7bdf6f72832ff60a34671f23cb7a4188503875d447036b939aa050f30b9e6cb5

Credential pattern counts
Private key

0

Provider key

0

Github token

0

Aws access id

0

Jwt

0

Quoted credential assignment

0

Email count

0

Contact classification

No email patterns found

Customer record findings

No customer-record content identified in targeted text inspection.

Limits

Targeted pattern scan and publication-text review, not exhaustive data-loss-prevention certification.

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

#DecisionEstablished RuleHeresyFinal Call
1Graph-first abstractionModel code as graphsCode is sequential, not structuralKeep graph — but acknowledge the map is not the territory
2In-memory kernelSpeed requires in-memoryMemory is the wrong constraintIn-memory for analysis, external DB for persistence
3Layer architecture (L0-L10)Higher layers compose lower11 layers is a bureaucracyCollapse to 4 tiers, keep layers as internal detail
4Single kernel, dual protocolCode + Workflow share one engineDifferent domains need different enginesKeep unified kernel — but extract protocol adapters
5MCP as primary interface156 tools = maximum capability156 tools = maximum confusionTiered tools: 12 essential, 40 standard, 104 advanced
6Pure Python MLZero heavy deps = universal installReinventing scikit-learn poorlyPure Python for embed, sklearn for standalone
7Thompson Sampling everywhereExplore-exploit is optimalBandits solve the wrong problemBandits for selection, deterministic for known-good
8Shadow graphs for simulationCopy-on-write is elegantSimulations should be functional, not statefulFunctional transforms + immutable snapshots
9Library, not a serviceEmbed like PostgreSQLLibraries can't scale horizontallyLibrary-first, service-optional — the SQLite model
10Spectral methods for healthEigenvalues reveal hidden structureMost users can't interpret eigenvaluesSpectral underneath, human language on top
11Scan-then-analyze pipelineIngest everything, then queryStale graphs are worse than no graphIncremental, event-driven — the LSP model
12Validation as enforcementGates prevent bad changesGates 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:

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:

What could have been built instead:


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:

TierPurposeInternal LayersUser Sees
GraphBuild and query the graphL0 (kernel), L1 (fs), L2 (modules), L3 (network)kernel.query(), kernel.add_node()
AnalysisUnderstand the graphL4 (process), L5 (execution), L9 (spectral), info_theoryanalyze.health(), analyze.impact()
DecisionDecide what to doL6 (scheduler), L7 (intel), L8 (strategy), L10 (optimizer)plan.deploy(), plan.optimize()
LearningImprove over timeML kernel, bandits, outcomes, co-changelearn.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:


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.

TierCountDescriptionWhen to Show
Essential12Core workflow: stats, lookup, impact, deps, health, predictAlways
Standard40Common analysis: trace, cycles, bottlenecks, plan, dead-codeWhen graph loaded
Advanced104Specialized: spectral, optimizer, MCTS, cortex, temporalOn 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:


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:

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:

What could have been built instead:


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, compared

The 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) │
              └────────┘ └──────┘ └───────┘

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 commits

The user sees: actions. The system computes: eigenvalues. The translation layer is where all the value lives.

What could have been built instead:


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:

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.

CategoryModeExample
New circular dependencyAdvisory (warn)"This creates a cycle between A→B→A. Consider extracting shared logic."
Breaking a public APIEnforced (block)"This removes endpoint /api/users. 3 consumers detected. Cannot proceed."
High fan-in increaseAdvisory (warn)"auth-service now has 42 dependents (was 38). Monitor for coupling risk."
Deploying to productionEnforced (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

PriorityDecisionCurrent StateChange NeededSprint
P0#5 Tool tiering156 flat toolsAdd tier metadata, filter by contextS2
P0#3 Tier model11 raw layers exposedCreate GrafosaStack facade with 4 tiersS2
P1#10 Human translationRaw metrics exposedScorecard + natural language layerS4
P1#11 Incremental analysisBatch scan onlyWire Sentinel → Delta → Kernel updatesFuture
P1#8 Functional scenariosShadow graphs (stateful)Add Scenario class with transform listsFuture
P2#12 Advisory modeEnforced by defaultMake advisory default, enforce for irreversibleS4
P2#2 SQLite persistenceJSON onlyAdd SQLite backend for GraphKernelFuture
P3#4 Protocol adaptersRaw kernel for bothAdd CodeAdapter + WorkflowAdapterFuture

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.

Continue exploring