architecture

Forma View Query Model

Context

Forma views are managed Markdown configuration nodes under .forma/views/*.md by convention. They render structured knowledge from repository-backed Markdown pages without turning view definitions into ordinary domain notes.

The initial design previously leaned toward collection-bound views and then a workspace-file source model. The current starter-kit baseline is broader and more semantic: a view starts from pages in the Forma read model, then filters those pages by taxonomy membership or query predicates.

Goals

  • Keep view behavior explicit, file-backed, and reviewable.
  • Support space views, uncatalogued file views, and graph views with one source/query model.
  • Keep P0 query support small enough for robust diagnostics and GUI round-tripping.
  • Leave room for future external render inputs without treating them as durable knowledge truth.
  • Avoid executable scripts or arbitrary code in view definitions.

Non-Goals

  • P0 does not include a text query DSL.
  • P0 does not support DataviewJS-style or trusted JavaScript queries.
  • P0 does not implement full-text predicates, date comparisons, reference predicates, diagnostic filters, runtime temporary query controls, saved personal view controls, or cross-space table joins.
  • P0 does not make graph a global special feature outside the view system.

Current Starter-Kit Baseline

The current starter kit uses this shape:

---
kind: view
title: Notes
description: Configured table view over notes.
mode: table
display:
    order: 30

source:
    type: pages
    taxonomy:
        spaces:
            - notes

table:
    defaults:
        column:
            width: 16rem
            minWidth: 10rem
            maxWidth: 24rem
            overflow: wrap
    columns:
        - field: fields.title
          label: Title
          link:
              target: entry
          overflow: truncate
        - field: fields.summary
          label: Summary
        - field: fields.createdAt
          label: Created At

sort:
    - field: fields.createdAt
      direction: desc
---
# Notes

<!-- forma:content -->

Rules from the current baseline:

  • source.type is the canonical field name.
  • Ordinary projections use source.type: pages.
  • The global graph view can use only source.type: pages with no taxonomy filter.
  • Taxonomy filters use a map-to-list shape, even for one term.
  • Predicate and display field references use field, not target.
  • Table columns are objects so labels and future display options can be added without changing the column shape.
  • table.defaults.column may contain the same four presentation fields and is useful for concise equal-width Tables. defaults is the namespace for future default configuration; currently only column is defined. A valid field on a column overrides that default field; an absent or invalid column field inherits the valid default; an absent or invalid default falls back to the renderer's intrinsic behavior.
  • Table column width, minWidth, and maxWidth are optional positive length hints. A number means pixels; strings may use only px, rem, or em, including positive decimals, with a maximum numeric component of 4096. Percentages, viewport units, ch, functions, variables, keywords, and arbitrary CSS are rejected. ch is deferred because its value is ambiguous for zh-Hans and variable fonts without a proven code/date-column requirement. When both bounds use the same unit, minWidth must not exceed maxWidth.
  • Table column overflow accepts only wrap or truncate. When it is absent, each renderer keeps its established automatic presentation; wrap explicitly permits multiline content and truncate requests a single-line ellipsis.
  • A Table column may declare link.target: entry. Its non-empty ordinary-value cells open the source Page for that row. entry is the only supported explicit target in this first cut, so View definitions do not depend on Host-specific route paths or title-field inference.
  • view.render keeps resolved schema references typed in every View mode: value carries an ordinary frontmatter value, reference carries one { path, title } target, and referenceList carries ordered { path, title } targets. Hosts render the latter two as target-title links without separately matching raw frontmatter values against a reference map. An unresolved field remains an ordinary value; workspace diagnostics remain the authority for its resolution failure.
  • Invalid optional Table presentation hints produce a non-blocking view.tableColumnPresentationInvalid warning, are omitted from normalized render output, and must not generate HTML styles, classes, or data attributes.
  • Effective same-unit minWidth / maxWidth constraints are checked after merging a column with table.defaults.column; an inverted pair is warned and omitted without blocking the View.
  • Table and list sort stay view-level.
  • Kanban columns may define local sort because each column is a separate result group.
  • sort.order can define explicit enum order for fields such as priority.

The current examples use binding paths such as fields.title, fields.updatedAt, and fields.status. These paths document the current starter-kit baseline only. The full runtime object model still needs a separate design pass during the backend and WebApp refactor.

Proposed Architecture

Source Before Query

View evaluation has two layers:

  1. source selects candidate pages.
  2. query filters normalized page records derived from those pages.

Starter-kit views should use source.type: pages as the target source shape.

Taxonomy Filters

A view scoped to starter notes should filter through taxonomy membership:

source:
    type: pages
    taxonomy:
        spaces:
            - notes

This avoids making spaces a built-in runtime concept. Other taxonomies can use the same shape later.

Query AST

The query model remains a structured AST with boolean composition. The starter baseline uses field for leaf predicates:

query:
    all:
        - field: fields.status
          op: equals
          value: todo

The semantics are:

  • all: every child must match.
  • any: at least one child must match; an empty any should be neutral only when the query node is otherwise empty by implementation convention.
  • not: every child must not match.
  • Leaf predicates use field, op, and optional value.

P0 supports these operators:

  • equals
  • in
  • contains
  • exists

Additional operators can be introduced later when runtime typing and diagnostics justify them:

  • notEquals
  • notIn
  • notContains
  • intersects
  • before
  • beforeOrEqual
  • after
  • afterOrEqual

Mode-Specific Query Use

List and table views use the top-level query as their candidate filter.

Kanban views first apply the top-level query, then evaluate kanban.columns[].query in column order. The first matching column wins. Health checks should warn about overlapping column queries and unmatched items.

Graph views use source and optional query to define the graph scope, but their rendering semantics are graph-specific. A repository-wide graph is not a cross-space table join.

Example global graph view:

---
kind: view
title: Graph
description: Workspace page relationship graph.
mode: graph

source:
    type: pages

graph:
    presentation:
        nodes:
            colorBy:
                taxonomy: areas
    edges:
        - source: body
          intent: link
          label: links to
        - source: fields
          field: project
          label: belongs to
        - source: fields
          field: assignees
          label: assigned to
---

Graph node coloring is explicit and configuration-driven. When graph.presentation.nodes.colorBy.taxonomy is present, Core projects the matching configured term identity and presentation. A term color falls back to its taxonomy main color; unclassified and multi-term Pages remain neutral rather than receiving an inferred category. A missing taxonomy is a View diagnostic.

graph.presentation.nodes.colorBy.field is the mutually exclusive field-driven alternative. It reads a scalar frontmatter value through the standard fields.<path> target syntax. Valid #RRGGBB strings remain explicit colors; other non-empty strings, numbers, and booleans map deterministically to the Core-owned Graph palette. Missing, null, empty, list, and object values remain neutral and classify as Unclassified. Core owns this classification and color projection so RPC, WebApp, static export, and editor Hosts do not reinterpret frontmatter independently. Without colorBy, the Graph keeps Host-neutral node colors.

Graph should be opened through normal view navigation, tabs, or links. It is a view mode, not a separate global product surface. Relationship panels may show backlinks and outgoing links for the current page, but they are not the primary graph surface.

graph.edges configures which resolved references become graph edges. Wikilinks, embeds, and structured field references are all relation rules. Body edges declare at least source and intent. Field edges declare at least source and field; with source: fields, the field value is relative to the normalized fields object, such as assignees rather than fields.assignees. label is optional. A plain list of fields is not sufficient because field names such as assignees or blockedBy do not always define the edge label shown to users.

Health checks should validate that configured relation fields exist, are reference-typed according to the resolved space schema, and can produce workspace-relative target paths. view.render should include edge source kind, intent, field when present, resolved edge label, and semantic type in graph edge output.

The shared Graph renderer sizes nodes from their incoming and outgoing semantic reference count with a bounded logarithmic scale. One-hop selection still uses unique adjacent Pages, so repeated references may increase visual importance without duplicating focus neighbors.

Interfaces And Contracts

view.render should evaluate:

  • view parameters;
  • page source filters;
  • taxonomy filters;
  • normalized-page query definitions;
  • sort definitions;
  • display fields;
  • table configuration;
  • kanban column configuration;
  • render mount points.

The first public release does not use a committed persistent index. The serve process can build the read model in memory and expose valid view metadata, including page-source graph views without taxonomy filters.

check should report structured diagnostics for:

  • unsupported source.type;
  • invalid source globs;
  • invalid query fields;
  • unsupported operators;
  • incompatible operator/value combinations;
  • missing referenced spaces, fields, or parameters when the view requires them;
  • invalid kanban column queries;
  • overlapping or unmatched kanban items when enough information is available.

Invalid queries should produce diagnostics instead of panics or silent misrendering.

P0 Scope

P0 should implement the starter-kit baseline:

  • source.type: pages;
  • taxonomy filters using map-to-list values;
  • query.all;
  • query.any;
  • query.not;
  • field references over the currently supported starter bindings;
  • operators equals, in, contains, and exists;
  • table, list, kanban, and graph rendering over this model;
  • graph view discovery without making graph a special built-in route.

Later Scope

Later versions may add:

  • text query DSL compiled into the same AST;
  • reference-aware query targets;
  • full-text search predicates;
  • date and datetime comparisons;
  • runtime temporary filters and advanced table controls;
  • saved personal view controls;
  • embedded view parameters;
  • external source kinds;
  • write-capable kanban actions.