Knowledge state (projections)
Epic #262 Phase 4 (#266).
Knowledge State is a projection of the Knowledge Model, not a set of dashboards. Every number a user sees — debt, balance, discoveries, open questions, the heatmap, the evolution timeline — is computed by a pure function of the model, and the Experience views (Home, Health, Discovery, Graph) consume those projections rather than computing anything themselves.
Metrics are consequences, not inventions (the manifesto): a metric exists because a projection derives it from the model, never because a view invented it inline.
The State surface
All projections are exposed through one import surface — architecture/knowledge/state — so the
Experience layer has a single door to "what the model says", and no view reaches into an individual
analysis by a deep path. This is the §XI boundary made concrete: the surface is pure and
Obsidian-free (guarded by test/architecture/knowledge/pure-is-obsidian-free.test.ts over state/),
and a structural test (test/architecture/components/core/experienceUsesStateSurface.test.ts) enforces
that components/core only imports from architecture/knowledge or architecture/knowledge/state.
Every projection follows one contract:
export type StateProjection<Params extends unknown[] = [], Result = unknown> =
(model: KnowledgeModel, ...params: Params) => Result;
The projection inventory
| Projection | Result | Consumed by |
|---|---|---|
buildHome |
HomeModel |
Home |
computeKnowledgeDebt |
KnowledgeDebt |
Health |
computeKnowledgeBalance |
KnowledgeBalance |
Health |
buildKnowledgeDashboard |
DashboardModel |
Dashboard |
findDiscoveries |
Discovery[] |
Discovery |
openQuestions / proposeAnswers |
OpenQuestion[] / answers |
Open questions |
buildEvidenceMap |
EvidenceMap |
Evidence map |
buildKnowledgeMap |
KnowledgeMap |
Knowledge map |
conceptNeighbors |
ConceptNeighbors |
Concept navigation |
computeWeeklyReview |
WeeklyReview |
Review |
buildHeatmapGrid |
HeatmapGrid |
Thinking heatmap |
deriveOutline |
Outline |
Projects / synthesis |
classifyHealth |
HealthResult |
Health (orphans / dead-ends, over the model's edges) |
deriveRecommendations |
KnowledgeRecommendation[] |
Home / Health / Discovery (via #268) |
The recommendation pipeline — Query → State → Recommendation → Command
Every metric proposes an action (the manifesto). The last leg of the pipeline is one primitive,
KnowledgeRecommendation (#267, architecture/knowledge/state/recommendation.ts):
interface KnowledgeRecommendation {
reason: RecommendationReason; // closed why-union (add-source, connect, resolve-contradiction, …)
target: string[]; // the note path(s) it concerns; empty = vault-wide
command: CommandActionId | null; // the kind:"command" action that resolves it, or null (no built-in yet)
priority: number; // urgency in [0,1]
}
deriveRecommendations(model) is a pure projection (offline, deterministic, §XI obsidian-free)
that composes the debt / balance / discovery / question / state signals into one prioritized list.
One primitive unifies six vocabularies. Before #267 each surface invented its own "next-step"
tokens — dashboard RecommendationToken, debt RemediationToken, balance BalanceSuggestion,
review ReviewAction, home NextSession.reason, and the action-layer NextMoveToken. Pure
from* mappers collapse every one of their cases onto a single RecommendationReason, proven by a
coverage test — so the whole system speaks one recommendation language.
command is a declarative pointer, not an invocation: it names a kind:"command" action id
(Phase 3) — add-source → attach-source, connect → create-semantic-relation — or null when no
built-in command applies yet. Actually running a recommendation's command from a view (via the
264 KnowledgeContext), and rendering recommendation widgets on Home/Health/Discovery, is deferred
to the view-collapse phase (#268). The dashboard keeps its current per-panel presentation unchanged; Phase 5 delivers the primitive + the unification, not new displayed output.
Health derives from the model (#274)
classifyHealth(model) classifies each idea over the model's typed edges — an orphan has no
outgoing edge, a dead-end has no incoming edge (self-edges excluded). Because the model's edges
include semantic relations (up::, supports, inline key:: [[X]]) on top of raw wikilinks, a note
connected only semantically is no longer a false orphan/dead-end. This is an intended number change
vs the earlier raw-resolvedLinks classifier: counts drop for vaults that use relations, and are
identical for pure-wikilink vaults.