| title | Concepts and Terminology |
|---|
This document is the canonical reference for concepts and terminology used in this project. It focuses on the meta-concepts the project supports, not the content of specific frameworks modelled in schemas. Before naming things in code, tests, comments, or documentation, check definitions here for consistency, and update them here when the project's "world view" changes, avoiding blurry terms as much as possible.
A space is a named collection of nodes organised according to a schema. Spaces are the primary unit of organisation — a space has a backing format (a space directory or a space on a page file) and may be registered in a config file (.sctx.json or the global config.json) with a name for convenient access.
flowchart TD
subgraph dir [Space Directory]
direction TB
tp[Typed Page<br>type: goal in frontmatter]
en[Embedded Nodes<br>headings, bullets, or table rows<br>explicitly typed, relationship-implied,<br>or anchor-implied]
other[Other files<br>no frontmatter → skipped<br>no type field → nonSpace]
tp -->|body parsed for| en
end
subgraph soap [Space on a Page]
direction TB
sf[Single .md file<br>type: space_on_a_page]
hn[Heading nodes<br>depth → type via hierarchy<br>or relationship-implied]
bn[Bullet nodes<br>explicitly typed or relationship-implied]
tn[Table rows<br>typed via relationship heading<br>or first-column name]
sf -->|headings| hn
sf -->|typed bullets| bn
sf -->|typed tables| tn
end
pe[parse-embedded<br>extractEmbeddedNodes]
nodes[(Space Nodes)]
dir --> pe
soap --> pe
pe --> nodes
The term "space" is preferred over "OST" or "tree" because the tooling is not limited to a specific framework, and future schemas may not be strictly tree-shaped.
A space directory is a directory of markdown files that backs a space. Each file may represent a space node, embed child nodes in its body, or be an unrelated file that the tooling ignores.
Each .md file with a type frontmatter field is a typed page — it represents one node. Its body is also scanned for embedded nodes:
- Heading with
[type:: x]or anchor-implied type (e.g.## My Goal ^goal1) → becomes a child node. - Relationship headings (e.g.
### Assumptions) → when matched to a relationship definition in the schema, signals that following content (list items or table rows) should be typed as that relationship's child type, without requiring explicit inline annotations. - Untyped headings → update the depth stack for parent resolution but do not become nodes. They stay in the content of the node whose section contains them; see Node content.
- Typed bullet items (
- [type:: solution] Title) → become child nodes at any nesting depth. - Table rows under a relationship heading → become child nodes of the relationship type.
- YAML blocks and unbracketed
key:: valueparagraph fields → merged into the current node'sschemaData.
Parsing behaviour for a space directory:
- Files declaring a
space nodetype via frontmatter are included as nodes. - Such files may also contain
embedded nodesin their body, which are extracted and included. - Files declaring a
tooling type(e.g.space_on_a_page,dashboard) are excluded from the node set. - Files without frontmatter, or without a
typefield, are excluded from the node set (unless type inference is configured — see below). - Non-markdown files are not scanned.
When typeInference is configured on the markdown plugin, files without an explicit type field in frontmatter can have their type inferred from their folder path. Explicit type: in frontmatter always takes precedence.
Two modes are available:
-
folder-name(default) — the leaf directory name is matched case-insensitively against the schema's known type names and alias keys. For example, a file atconcept/page.mdis inferred as typeconcept; a file atstudy/page.mdis inferred assourceifstudyis an alias forsourcein the schema. A folder name that is neither a type name nor an alias key results in no inference. -
folderMap— an explicit map from folder path (relative to space root) to a type name or alias. Replaces auto-matching entirely; only folders listed in the map are inferred. Longest-prefix matching is used when folder paths overlap. An unresolvable value (not a known type or alias) is a hard error at parse time.
Space on a page is a single-file backing format for a space. An entire planning tree is represented in one markdown document, using heading hierarchy, bullet point annotations, and anchor syntax. No separate per-node files are used. This format is most useful for the early development stages of a space, keeping information together in one file with less "boilerplate".
A file in this format carries type: space_on_a_page in its frontmatter. It is not itself a space node — it is a container.
Key properties:
- Heading hierarchy determines node depth and infers
space nodetype (depth-based type inference). - Heading levels must not skip — each level must be exactly one deeper than its parent.
- Typed bullets work the same as in typed pages.
- A horizontal rule (
---) terminates parsing; headings below it are ignored.
Preamble is content in a space on a page document that appears before the first heading. It is parsed but discarded — not associated with any node, and not part of any node's content.
A space node (or node for short) is a single entity in a space — a named, typed item defined in the schema. Nodes are the primary content of a space.
Node types are defined by the schema in use and may vary across schemas. Examples from the default schema: vision, mission, goal, opportunity, solution. The tooling is not prescriptive about which types exist — schemas are designed to be extended and replaced.
Each node has an id (its unique key), a title (its display name) and linkTargets (the names wikilinks may use to reach it). See Node key.
Each node retains its physical source location (file, and line / column where available) so that validation diagnostics, edge errors, and downstream tooling pinpoint the exact location in the vault.
A node's content is the markdown source of its body; see Node content.
space_on_a_pageanddashboardare notspace nodetypes — they aretooling types.
Three values on every node do separate jobs:
| Value | Job | Unique? | Markdown source |
|---|---|---|---|
id |
Identifies the node: graph key, parent lookup, gating, duplicate detection, and the file shown in messages | Yes. Two nodes with the same id are a duplicate error |
Path relative to the space root, with extension (projects/alpha/Overview.md). Embedded nodes use <page id>#<heading>, with (2), (3) added to repeated headings on one page |
title |
Display name in show, rendered output and judge prompts |
No | title: frontmatter, else the containing folder's name for a folder index, else the file's basename |
linkTargets |
The names a wikilink may use to reach the node | Per name. A name shared by several nodes is reported as ambiguous where a link uses it | The explicit title:, the basename, and every path suffix without .md |
A wikilink is looked up in linkTargets, and the result is stored as the target node's id. Other values hold the rest of the picture: source is where the node is defined, resolvedParents lists parents by id, and resolvedLinks classifies content links. A parent field resolves exactly as a link does.
A validation result refers to a node by its id, in a nodeId field. Where the node is defined is a separate source (file, and line / column where available). A result may copy a node's identity and location, but not its content. A node can lack a source (for example when a plugin reads from a service rather than files), so treat source as optional.
Rule violations on a global rule have no nodeId.
A folder index is a page that stands for its folder: Overview.md or index.md inside projects/alpha/. Obsidian has no container pages, so a node with child pages is a folder plus an index page. Listing the index basenames in the markdown plugin's folderIndex config gives each folder index the folder's name as its title. The derived title is for display only and never a link target.
An embedded node is a space node defined within a containing document rather than as its own file. Embedded nodes are declared using markdown heading syntax with inline field annotations (e.g. [type:: goal]) or anchor-implied types, and are extracted at parse time.
A typed page may contain embedded nodes in its body. Those nodes become full members of the parsed node set, with parent references wired to their containing page or enclosing heading.
A node's content is the markdown of its body, copied unchanged from the file: blank lines, list looseness, line endings and code fences all stay as written. The content of a list item node is dedented by the item's content indent, so it reads as markdown on its own. Leading blank lines and trailing whitespace are trimmed.
A node owns the source from its heading or list item up to the next heading that ends its section, with these parts removed:
- Its own heading line or list marker text.
- The sections of nested embedded nodes.
- Field syntax that supplies data: frontmatter and other YAML blocks, and lines that hold only
key:: valueor[key:: value]fields. A field written inside a sentence stays in the prose, and its value is still read as a field.
An untyped heading belongs to the content of the node whose section contains it. An untyped heading at the same level as an embedded node's heading, or at a higher level (fewer #), ends that node's section, so the heading and what follows it go to the enclosing node, or to the page. An untyped heading at a deeper level stays inside the section. Inline fields below an untyped heading are assigned the same way.
In a space_on_a_page file, content before the first heading (the preamble) is not assigned to any node.
A type alias is an alternative name accepted in the type field for a given space node type. Aliases allow teams to use their own vocabulary while still receiving schema validation. For example, a schema might accept outcome as an alias for goal.
A space node's resolved type (resolvedType) is its canonical type after alias resolution. Prefer resolvedType over the raw type field for all comparisons in rules and hierarchy checks.
A typed page is a markdown file whose frontmatter declares a space node type (e.g. type: goal). The file itself represents one node, and its body may additionally contain embedded nodes. The page node's content is the body markdown outside any embedded node (the entire body when there are none).
Typed pages are distinct from space on a page files: a typed page is a space node; a space_on_a_page file is merely a container.
A schema defines the valid structure for nodes in a space: the fields, types, constraints, and descriptive rules for each entity type. A space uses the default schema unless a custom one is declared in its config.
The schema handles structural validation. Cross-node and workflow checks are handled by executable rules defined in $metadata.rules.
Schemas are composable. Structural definitions are reused with $ref (validation only). Metadata is reused only by listing schema $ids in $metadata.imports: imports merge transitively, in order, with the schema's own metadata last. Hierarchies, relationships, aliases and rules all follow one rule — a duplicate is an error unless the later entry sets override: true — and each declared hierarchy is kept as a separate named hierarchy.
Rules are descriptive, and potentially executable, checks applied to nodes beyond what structural schema validation can express. Rules encode qualitative guidance and best practices alongside the schema, making them available to both tooling and agent skills.
Rules may be:
- Descriptive — human-readable guidance, useful as documentation and as structured input to agent skills
- Executable — mechanically evaluable expressions (e.g. "no more than one
activenode of a given type at a time") - Quantitative — numeric thresholds or counts applied to node sets
- Stage-based — triggered only when a node's
statusmeets a condition - Qualitative — checks on content and framing (e.g. ensuring an opportunity is stated in the user's voice, not as a business goal)
- Cross-entity — checks spanning multiple nodes or levels of the tree
- Coherence — verifying that statements across related nodes credibly support one another
- Best-practice — guidance encoded as checks (e.g. flagging solution-framing in problem descriptions)
Rules are distinct from schema validation: the schema checks structure; rules check meaning and quality.
A rule's when is its applicability condition: a test that decides whether the rule applies to a node. Its expression is the test the node must pass.
Each executable rule has a category (validation, coherence, workflow, best-practice) that supplies its default severity (error, warning, info). A rule may override its category's severity, but categories are the preferred grouping. Only error severity violations fail validation.
See docs/rules.md for the rules reference, including JSONata expression syntax and the full $metadata field reference.
A semantic rule is a rule with a semantic part. It asks a model to evaluate a node, for example whether an opportunity is a customer need. The semantic object can carry a rubric, verdicts, a verdict severity map, a scope and includeContent. A rule can also have an expression, a JSONata test that runs before the model.
A model provider evaluates semantic rules: a decision model (Jev) or a chat model (Claude, OpenAI-compatible). It is a bundled provider or an entry in modelProviders. For each rule, the model gives one verdict from the rule's semantic.verdicts, for example customer-need, vague or solution. The rule's semantic.severity says which verdicts report a violation. Semantic rules cost money, so they are evaluated only when a run asks for it (--semantic); a rule's expression always runs. See docs/semantic-rules.md.
The evaluated node is the node a semantic rule is judged on. A rule's semantic.scope adds related nodes (parent, siblings, children, ancestors, descendants, or the whole branch) along the main hierarchy's edges, and semantic.includeContent chooses whose body is sent. See Related nodes and content.
A node ruleset group is the set of nodes that share the same semantic rules. It forms within one evaluation step of cascade gating: one hierarchy depth, or the set of non-hierarchy nodes. Nodes are keyed on the rules left after when and expression filtering. A model provider on the openrouter-decisions or typesafe-systemone API evaluates one group in as few decision requests as it can.
A decision request is one call to a decisions API. It carries the state of one or more nodes and a list of decision questions.
A decision question is one choice question about one node under one semantic rule, inside a decision request.
The per-node input cap limits the input for one evaluated node and its related nodes. See Input size and trimming.
Cascade gating is the mechanism by which a schema validation error, hierarchy structure violation, or gating rule violation on a node suppresses rule evaluation for that node's hierarchy subtree. It exists so downstream checks — especially costly semantic (model-judged) rules — aren't run against inputs already known to be invalid.
A coverage gate records one instance of this: the gating node, what kind of violation triggered it (schema, hierarchy, or a specific rule id), and the set of nodes whose rules were suppressed as a result. sctx validate reports coverage gates so both humans and agents can see the validation "edge" — where evaluation stopped and why. See docs/rules.md for the full model, including the per-rule gatesDescendants field and the --include-gated / --depth CLI options.
Tooling types are type values recognised by the schema and tooling but not treated as space nodes. They serve organisational or display purposes:
space_on_a_page— a container file for aspace on a page. Not itself a node.dashboard— a summary view for aspace directory. Conceptually similar tospace on a pagein that it presents a high-level, single-document view of a space — but rather than defining the space, it reflects it, querying and assembling information from the space's node files. Useful after a space has "graduated" from a singlespace on a pagefile to aspace directory, as a way to preserve that top-level overview. The dashboard concept may evolve to surface more operational information over time, but there is no concrete design for that yet.
The hierarchy is the ordered list of node types in a space, from root to leaf. It is defined in the schema's $metadata.hierarchy.levels array and drives depth-based type inference (for space on a page), tree rendering, and structural validation. The root type has no parent; every other type has parents in the level immediately above (unless $metadata.hierarchy.allowSkipLevels is set).
The hierarchy is modelled as a layered DAG: a non-root node may have zero parents (orphaned), one parent, or multiple parents. The show command renders this as an indented tree, marking repeated nodes with (*) where the subtree is already shown elsewhere.
A space may have several hierarchies, one for each imported schema that declares one. Each hierarchy has a name (defaulting to the declaring schema's file stem), and each type belongs to at most one hierarchy. Structural validation runs per hierarchy; relationships may connect types across hierarchies.
The main hierarchy is the one declared by the root schema; failing that, the only imported hierarchy; failing that, the one the root selects with "hierarchy": { "$ref": ... }. The main hierarchy drives space on a page parsing, hierarchy embedding, template sync, and default rendering. show, diagram and render take --hierarchy <name> to render another. See docs/schemas.md.
Each non-root level uses the shared field, fieldOn, and multiple edge options (see Graph edges). Hierarchy-specific options:
| Option | Default | Meaning |
|---|---|---|
selfRef |
false |
When true, a node may have a parent of the same resolved type, using field for both regular and same-type parents |
selfRefField |
undefined | When set, specifies a separate field for same-type parent relationships (always on the child). Requires selfRef: true. |
templateFormat |
undefined | Embedding hint ("list", "table", "heading"). When set alongside matchers, enables hierarchy embedding in typed pages |
matchers |
undefined | Heading patterns (strings or /regex/) that signal this level's embedding section |
embeddedTemplateFields |
undefined | Column names used when template-sync generates table stubs for this level |
Hierarchy levels with templateFormat and matchers support embedded parsing analogous to relationships: a heading in a typed page that matches the level's matchers signals that following content (list items or table rows) belongs to that type, without requiring explicit [type:: x] annotations.
Two sub-patterns:
- Child-level embedding — heading matches the next level in hierarchy → items create new child nodes.
- Parent-level referencing — heading matches the level above the current node's type → bare wikilink items (
- [[X]]) populate the current node's field pointing up to that parent type. This lets a node list its parent references inline without creating duplicate nodes.
Bare wikilink items (- [[X]]) in any embedding section always populate a field rather than creating new nodes.
Example: Activities listing Capabilities with sub-capabilities
"levels": [
"Activities",
{
"type": "Capabilities",
"field": "capabilities",
"fieldOn": "parent",
"multiple": true,
"selfRefField": "parent"
}
]This defines two edge types for Capabilities:
- Activities → Capabilities: Via
capabilitiesarray field on Activity nodes - Capability → Capability: Via
parentfield on Capability nodes (same-type, child-side)
A relationship is a link between a parent type and a child type that is not part of the primary structural hierarchy. For example, an opportunity might have a relationship with assumption (multiple) or problem_statement (single).
Relationships are defined in $metadata.relationships. Like hierarchy levels, they use the shared field, fieldOn, and multiple edge options (see Graph edges), but carry additional metadata used for parsing and template generation:
parent/type— the parent and child canonical types (required)templateFormat— parsing/generation hint:"heading","list","table", or"page"matchers— heading text patterns (strings or/regex/) used to detect relationship sections during embedded parsing
A heading in a typed page that matches a relationship's matchers signals to the parser that following content (single nodes, list items, or table rows) should be typed as that relationship's child type — without requiring explicit inline [type:: x] annotations.
Both hierarchy.levels and relationships define edges in a directed graph over the node set. All edges use the same three configuration options:
| Option | Default | Meaning |
|---|---|---|
field |
"parent" |
The frontmatter field holding the wikilink(s) |
fieldOn |
"child" |
"parent" means the field is on the parent node and points to children (reversed direction) |
multiple |
false |
When true, the field holds an array of wikilinks rather than a single one |
The fieldOn: "parent" pattern is used when the content model lists children on the parent node (e.g. tasks: ["[[Task A]]", "[[Task B]]"]). Embedded parsing then appends child wikilinks to the parent's field array rather than setting a parent field on each child.
Dangling wikilinks — edge field values that do not resolve to any known node — are reported as reference errors during validation.
A wikilink is the [[Title]] linking syntax (compatible with Obsidian) used in edge fields to reference other space nodes. Two forms are supported:
| Form | Example | Resolves to |
|---|---|---|
| Name or path | [[My Goal]], [[alpha/Overview]] |
The space node with that name in its linkTargets. A bare name shared by several nodes is ambiguous; add enough of the path to make it unique, as Obsidian does |
| Anchor ref | [[vision_page#^goal1]] |
The embedded node with anchor goal1 inside vision_page.md |
Resolved parents (resolvedParents) is the set of parent references derived from a node's edge fields at parse time. It is always an array (empty if unresolved or root-level). Both hierarchy.levels and relationships edges resolve into this single array — forming a unified labelled directed graph over the node set.
Each entry is a ResolvedParentRef object:
| Field | Type | Description |
|---|---|---|
id |
NodeId |
The parent node's id |
field |
string |
The frontmatter field that held the wikilink |
kind |
'hierarchy' | 'relationship' |
Whether the edge came from a hierarchy level or a relationship |
hierarchy |
string |
Name of the hierarchy the edge belongs to; present only when kind is 'hierarchy' |
selfRef |
boolean |
Whether the edge is a same-type (self-referential) parent link |
The kind and hierarchy labels let downstream consumers distinguish edge types and hierarchies without re-inspecting the schema. Validation routes hierarchy edges to structural checks within their own hierarchy (parent-type rules, skip-level detection) and relationship edges to field reference checks (type-match, missing-target). Tree rendering and rule evaluation use the full set.
The space graph format is a space's nodes and hierarchies as plain JSON. A program exports it on one side of a process or network boundary and builds a SpaceGraph from it on the other. See api.md.
Content links (resolvedLinks on SpaceNode) are all links extracted from a node's content at parse time — both from frontmatter non-edge fields and from the body text. They are distinct from graph edges: they are never used to infer structure, but are available to consumers for rendering, auditing, or navigation.
Each entry is a ResolvedContentLink object:
| Field | Type | Description |
|---|---|---|
text |
string |
Display text of the link |
target |
string |
Raw link target (URL or wikilink path) |
action |
'link' | 'embed' |
Whether the link navigates or transcludes |
anchor |
string? |
Fragment component (heading or block ref) if present |
location |
'node' | 'internal' | 'external' | 'system' | 'protocol' |
Resolved location classification |
Location classification:
node— wikilink resolved to a known space nodeinternal— wikilink or relative path to an in-source target that is not a space nodeexternal—http://orhttps://URLsystem—file://URLprotocol— any other scheme (e.g.obsidian://)
Sources: Both wikilink syntax ([[Target]], ![[Image]]) and markdown link syntax ([text](url), ) are extracted. Bare URLs in frontmatter string fields are also captured. Graph edge fields (hierarchy parent, relationship fields) are excluded — those are graph edges, not content links.
An anchor is a block anchor (e.g. ^goal1) appended to a heading in a typed page, using Obsidian block anchor syntax. Anchors serve two purposes:
- Cross-file references — other files can reference an
embedded nodeby[[filename#^anchor]]. - Anchor-implied type — if the anchor name matches a node type name or a node type name followed by digits (e.g.
^mission,^goal1), the node's type is inferred from the anchor, making an explicit inline annotation unnecessary.
A filter expression is a string that selects a subset of nodes from a space. Filter expressions are used with the --filter flag on the show and render commands and in named filter views in config.
WHERE {jsonata} — return nodes where the JSONata predicate is truthy
SELECT {spec} WHERE {jsonata} — filter by WHERE, then expand result via SELECT
SELECT {spec} — expand from all nodes (no WHERE filter)
{jsonata} — bare JSONata, treated as a WHERE predicate (convenience shorthand)
Keywords (WHERE, SELECT) are case-insensitive.
The WHERE predicate is a JSONata expression evaluated per node. Node fields (from schemaData) are accessible directly — e.g. resolvedType, status. id (e.g. "solutions/My Solution.md") and title are always available regardless of schema. resolvedParentIds lists the id of each parent and resolvedParentTitles their titles. Additionally, two pre-computed traversal arrays are available:
| Field | Description |
|---|---|
ancestors[] |
Flat array of ancestor nodes, nearest first, deduplicated by id |
descendants[] |
Flat array of descendant nodes, nearest first, deduplicated by id |
Each entry in ancestors[] or descendants[] includes all schema fields of the target node, plus edge metadata:
| Metadata field | Type | Description |
|---|---|---|
_field |
string |
The edge field name that connects to this ancestor/descendant |
_kind |
'hierarchy' | 'relationship' |
Whether the edge came from a hierarchy or a relationship |
_hierarchy |
string |
Hierarchy name; present only for hierarchy edges |
_selfRef |
boolean |
Whether the edge is a same-type (self-referential) link |
Dates and datetimes are the strings written in frontmatter, so compare them with $toMillis() rather than as strings (offsets and precision vary), e.g. WHERE $toMillis(updated) > $toMillis('2026-03-31T00:00:00Z'). See Rules: dates and times.
The SELECT clause expands the result set by walking the graph from matched nodes. The spec is a comma-separated list of directives:
| Directive | Meaning |
|---|---|
ancestors |
All ancestor nodes |
ancestors(type) |
Ancestors of the given resolved type |
descendants |
All descendant nodes |
descendants(type) |
Descendants of the given resolved type |
siblings |
Nodes sharing at least one parent with matched nodes |
relationships |
Nodes connected via a relationship (non-hierarchy) edge |
relationships(childType) |
Relationship-connected nodes of the given child type |
relationships(parentType:childType) |
As above, also filtering by parent type |
relationships(parentType:field:childType) |
Fully qualified: also filtering by edge field name |
Multiple directives may be combined with commas: SELECT ancestors(goal), siblings WHERE ...
WHERE resolvedType='solution' and status='active'
WHERE resolvedType='solution' and $exists(ancestors[resolvedType='opportunity' and status='active'])
WHERE $count(descendants[resolvedType='solution']) > 3
SELECT ancestors(opportunity) WHERE resolvedType='solution'
SELECT siblings WHERE resolvedType='solution' and status='active'
SELECT relationships(assumption) WHERE resolvedType='opportunity'
A filter view is a named filter expression defined in the space config. Views allow commonly used filters to be referenced by name rather than repeating the expression inline.
Views are defined in the space config under the views key:
{
"spaces": [
{
"name": "my-space",
"path": "/path/to/space",
"views": {
"active-solutions": {
"expression": "WHERE resolvedType='solution' and status='active'"
}
}
}
]
}Use a view by name with sctx show <space> --filter <view-name>. If no matching view name is found in the config, the value is treated as an inline filter expression.
Status is a lifecycle field on nodes indicating a node's current stage. The valid values and their semantics are defined by the schema in use. Examples from the default schema (in rough progression):
identified → wondering → exploring → active → paused → completed → archived
Status is required on all node types at validation time. Note however that currently the space on a page parser chooses to apply a default.
A plugin is a module that extends the tool's capabilities.
Plugins that support parsing produce raw SpaceNode[] results given a suitable configuration. Graph edge resolution (resolveGraphEdges) is called by the core afterwards.
The plugins field in config is a map from plugin name to plugin config object. All plugin names must start with sctx-, but the prefix is optional in config and normalised on load. Built-in plugins take precedence and all are loaded by default. External plugin names specified in config are then resolved in order:
- Config-adjacent:
{configDir}/plugins/{sctx-name} - npm: a package matching
sctx-*
Each plugin declares a configSchema JSON Schema; the loader validates the config block against it before invoking the plugin. Config fields annotated with format: 'path' are resolved relative to configDir by the loader.
Plugins are called in order (external plugins first, then built-ins). For each operation:
- A plugin that does not implement the hook is skipped.
- A plugin that implements the hook returns
T | null: non-null means "I handled it"; null means "not me, try the next plugin." - The first non-null result wins. If no plugin handles the operation, the command throws an error.
A plugin that can handle a space should do so and return a result. Conversely, a plugin should return null if it inspects the context and determines the operation is not meant for it (for example, a plugin that only handles a specific file format or URL).