Status
Accepted - March 2026
Context
templjs now needs consistent semantic behavior across authoring and runtime features:
- Hover, go-to-definition, completion, diagnostics in VS Code
- Template rendering semantics in core
- Schema and input-data alignment
- Template extraction (reverse rendering) flows and validation
The current design mixes feature-specific resolvers and context heuristics. That makes behavior inconsistent across features and difficult to evolve as new schema or host-language scenarios are introduced.
We need a model where multiple independently valid ASTs/providers can contribute context without direct dependencies between providers.
Decision
Adopt a Context Graph Platform with the following constraints:
- N-provider model: Support N providers where N >= 1.
- Independent validity: Each provider AST/model is independently valid and replaceable.
- No direct provider coupling: Providers communicate only by publishing facts/edges through the context graph API.
- Profile-based specialization: Text-location and editor semantics are implemented as profiles over a shared core model.
- TypeScript-first implementation: Implement v1 in TypeScript to maximize delivery speed and integration with current packages.
- Rust-ready contract from day 1: Public interfaces must remain stable and implementation-agnostic so the execution engine can move to Rust later.
- No dependency leakage in public API: Public contracts must not expose third-party package symbols or types.
- Transport-open architecture: Keep the contract transport-agnostic so future inter-process usage (for example protobuf, GraphQL, or IPC) can be added without breaking in-process consumers.
- Profile-first semantics:
profile is first-class in fact and query contracts.
- Query contract first, query language later: v1 standardizes a versioned query contract; a standalone query language is explicitly deferred.
Core Model
The context graph is a typed fact graph:
- Subject: opaque node or reference id
- Predicate: typed relationship/attribute key
- Object: value or reference id
Providers publish facts and relationships. Consumers query graph state and subscribe to graph deltas.
Profile model
- A profile identifies semantic scope (for example
editor-location, runtime, extraction, schema).
- Facts and edges are profile-scoped.
- Query contracts can filter by one or more profiles.
Query model
- v1 defines a versioned, transport-agnostic query contract (
request / response).
- Query results are deterministic and profile-aware.
- A separate textual query language is out of scope for v1.
This is an in-process architecture and intentionally excludes distributed-bus concerns (transport, service discovery, cross-process marshalling).
The model is intentionally transport-agnostic at the contract layer so optional inter-process adapters can be introduced later without changing core semantic contracts.
Public API Rules
Public APIs in @templjs/context-graph must follow these rules:
- Export only package-owned types/interfaces.
- Export only JSON-serializable or primitive-based payload shapes.
- Do not export third-party classes/generics/enums in signatures.
- Keep ids opaque (
NodeId, ProviderId, SnapshotId).
- Version all externally observable payload shapes.
- Provide deterministic query ordering.
- Do not encode transport-specific assumptions in core contracts.
- Include profile scoping in fact and query contracts.
- Standardize a versioned query request/response shape.
Rust-Ready-from-Day-1 Checklist
- Stable wire contract: Versioned payload schema for facts, queries, and deltas.
- Opaque identifiers: No object-identity assumptions.
- Boundary purity: No callbacks captured inside stored graph state.
- Serialization-safe values: Restrict to JSON-compatible data at public boundary.
- Deterministic results: Stable sorting for repeated identical queries.
- Error contract: Structured error codes and payloads (avoid class-instance error dependence).
- Async boundary isolation: Provider lifecycle and graph queries are interface-driven and runtime-agnostic.
- No dependency type leakage: Public
.d.ts remains free of external dependency symbols.
Consequences
Positive
- Aligns authoring/runtime semantics through a shared model.
- Enables incremental integration of new providers without cross-coupling.
- Keeps current TS velocity while preserving a future Rust migration path.
- Improves testability via deterministic graph snapshots and queries.
- Provides a shared semantic substrate for template extraction workflows.
Negative
- Adds a new core platform package and migration effort.
- Requires phased refactors of Volar and server integrations.
- Introduces governance requirements for API boundary discipline.
Neutral
- Future inter-process adoption remains an optional adapter concern and is explicitly out of immediate implementation scope.
Implementation Plan (Phased)
- Scaffold
@templjs/context-graph package with strict API boundaries.
- Implement minimal graph kernel + provider lifecycle + query interface.
- Add first-class profile support in fact and query contracts.
- Implement versioned query contract (
request / response) in public API.
- Add API boundary enforcement and contract tests.
- Add Volar adapter and migrate hover/definition/completion reads.
- Add diagnostics and schema-provider integrations.
- Add extraction-provider integration points to support reverse-rendering workflows.
- Evaluate performance; only then consider Rust engine replacement behind same API.
- If needed later, add optional inter-process adapters (protobuf/GraphQL/IPC) behind unchanged contracts.
Deferred
- A standalone query language (DSL) is deferred until concrete multi-consumer needs exceed the v1 query contract.
References