Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 99 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,29 @@ All plugin names must start with `ost-tools-` (the prefix is optional in config
- `templatePrefix` — filename prefix for templates (default blank)
- `fieldMap` — maps file/frontmatter field names to canonical schema field names (e.g. `{ "record_type": "type" }`)

**Filter views:** Named filter expressions can be defined per space under `views`. Each view has an `expression` field using the filter expression syntax:

```json
{
"spaces": [
{
"name": "my-space",
"path": "/path/to/space",
"views": {
"active-solutions": {
"expression": "WHERE resolvedType='solution' and status='active'"
},
"solutions-under-active-opportunity": {
"expression": "WHERE resolvedType='solution' and $exists(ancestors[resolvedType='opportunity' and status='active'])"
}
}
}
]
}
```

Use a view name with `ost-tools show <space> --filter <view-name>`.

### Spaces

A space is a named directory or single file registered in the config. Spaces let you reference content by name instead of path:
Expand Down Expand Up @@ -194,13 +217,88 @@ Validates markdown files against the JSON schema:
### Show space tree

```bash
ost-tools show <space>
ost-tools show <space> [--filter <view-or-expression>]
```

Prints the space as an indented hierarchy tree. Hierarchy roots are listed first, followed by orphans (nodes in the hierarchy but with no resolved parent) and non-hierarchy nodes.

When a node appears under multiple parents (DAG hierarchy), it is printed in full under its first parent. Subsequent appearances with children show a `(*)` marker indicating the subtree is omitted.

**Filtering:** The `--filter` flag accepts either a named view from the space config, or an inline filter expression. Only nodes matching the expression are shown.

```bash
# Inline expression
ost-tools show <space> --filter "WHERE resolvedType='solution' and status='active'"

# Named view from config
ost-tools show <space> --filter active-solutions
```

See [Filter expressions](#filter-expressions) below for expression syntax.

### Filter expressions

Filter expressions are used with `--filter` and in config `views`. They use a `SELECT ... WHERE ...` pseudo-DSL:

| Form | Meaning |
|------|---------|
| `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 via SELECT (no WHERE filter — returns all nodes, expanded per spec) |
| `{jsonata}` | Bare JSONata, treated as a WHERE predicate (convenience shorthand) |

The WHERE predicate is a [JSONata](https://docs.jsonata.org/overview) expression evaluated per node. Within the expression, each node's fields are accessible directly (e.g. `resolvedType`, `status`, any schema fields like `title`). Additionally, two pre-computed traversal arrays are available:

- **`ancestors[]`** — flat array of ancestor nodes, nearest first, deduplicated. Each entry includes all schema fields of the ancestor node, plus:
- `_field` — the edge field name that connects to the ancestor
- `_source` — `'hierarchy'` or `'relationship'`
- `_selfRef` — whether the edge is a same-type (self-referential) link
- **`descendants[]`** — same structure, for descendant nodes

**SELECT spec** 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` | All 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: `SELECT ancestors(goal), siblings WHERE ...`

**Examples:**

```jsonata
// All solutions
WHERE resolvedType='solution'

// Active solutions only
WHERE resolvedType='solution' and status='active'

// Solutions whose nearest opportunity ancestor is active
WHERE resolvedType='solution' and $exists(ancestors[resolvedType='opportunity' and status='active'])

// Nodes that have any ancestor goal
WHERE $exists(ancestors[resolvedType='goal'])

// Bare JSONata shorthand (no WHERE keyword)
resolvedType='solution' and status='active'

// Solutions + their opportunity ancestors
SELECT ancestors(opportunity) WHERE resolvedType='solution'

// Solutions + their siblings (other solutions under same opportunity)
SELECT siblings WHERE resolvedType='solution' and status='active'

// Opportunities + their related assumptions
SELECT relationships(assumption) WHERE resolvedType='opportunity'
```

### Generate Mermaid diagram

```bash
Expand Down
94 changes: 94 additions & 0 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,100 @@ An **anchor** is a block anchor (e.g. `^goal1`) appended to a heading in a `type

---

## Filter expressions

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` command and in named filter views in config.

### Syntax

```
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.

### Predicate context

The WHERE predicate is a [JSONata](https://docs.jsonata.org/overview) expression evaluated per node. Node fields (from `schemaData`) are accessible directly — e.g. `resolvedType`, `status`, `title`. Additionally, two pre-computed traversal arrays are available:

| Field | Description |
|-------|-------------|
| `ancestors[]` | Flat array of ancestor nodes, nearest first, deduplicated by title |
| `descendants[]` | Flat array of descendant nodes, nearest first, deduplicated by title |

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 |
| `_source` | `'hierarchy' \| 'relationship'` | Whether the edge came from the hierarchy or a relationship |
| `_selfRef` | `boolean` | Whether the edge is a same-type (self-referential) link |

### SELECT spec

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 ...`

### Examples

```jsonata
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'
```

---

## Filter views

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:

```json
{
"spaces": [
{
"name": "my-space",
"path": "/path/to/space",
"views": {
"active-solutions": {
"expression": "WHERE resolvedType='solution' and status='active'"
}
}
}
]
}
```

Use a view by name with `ost-tools 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

**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):
Expand Down
38 changes: 37 additions & 1 deletion skills/ost-tools/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ bunx ost-tools validate <space> --watch
## show

```bash
bunx ost-tools show <space>
bunx ost-tools show <space> [--filter <view-or-expression>]
```

Prints a hierarchical tree of all nodes, indented by parent→child relationships. Useful for
Expand All @@ -41,6 +41,39 @@ browsing structure, verifying parent links are correct, and spotting orphaned no
Uses hierarchy edge config from `$metadata.hierarchy.levels` (`field`, `fieldOn`, `multiple`).
If those are misconfigured for your content, output will appear flatter than expected.

**`--filter`** accepts either a named view from the space config (`views` key) or an inline filter
expression. Only matching nodes are shown in the tree.

```bash
# Inline expression
bunx ost-tools show <space> --filter "WHERE resolvedType='solution' and status='active'"

# Ancestor attribute filter (solutions under an active opportunity)
bunx ost-tools show <space> --filter "WHERE resolvedType='solution' and \$exists(ancestors[resolvedType='opportunity' and status='active'])"

# Named view from config
bunx ost-tools show <space> --filter my-view-name
```

**Filter expression syntax:** `WHERE {jsonata}` | `SELECT {spec} WHERE {jsonata}` | `SELECT {spec}` | bare JSONata.
Within the WHERE predicate, node fields (e.g. `resolvedType`, `status`) are directly accessible. Two
traversal arrays are also available per node:
- `ancestors[]` — ancestor nodes nearest-first, each with `_field`, `_source`, `_selfRef` edge metadata
- `descendants[]` — descendant nodes, same structure

The SELECT spec is a comma-separated list of directives that expand the result set:
`ancestors[(type)]`, `descendants[(type)]`, `siblings`,
`relationships[(childType | parentType:childType | parentType:field:childType)]`

**Named views** are defined in the space config:
```json5
{
views: {
"active-solutions": { expression: "WHERE resolvedType='solution' and status='active'" }
}
}
```

## dump

```bash
Expand Down Expand Up @@ -138,6 +171,9 @@ bunx ost-tools template-sync <space> --create-missing
},
miroBoardId: 'xxx',
miroFrameId: 'xxx', // auto-populated by --new-frame
views: {
'active-solutions': { expression: "WHERE resolvedType='solution' and status='active'" },
},
}
]
}
Expand Down
10 changes: 8 additions & 2 deletions src/commands/show.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { filterNodes } from '../filter/filter-nodes';
import { readSpace } from '../read/read-space';
import type { SpaceContext, SpaceNode } from '../types';
import { classifyNodes } from '../util/graph-helpers';

export async function show(context: SpaceContext) {
export async function show(context: SpaceContext, options?: { filter?: string }) {
const levels = context.schema.metadata.hierarchy?.levels ?? [];

const { nodes } = await readSpace(context);
let { nodes } = await readSpace(context);

if (options?.filter) {
const expression = context.space.views?.[options.filter]?.expression ?? options.filter;
nodes = await filterNodes(expression, nodes);
}

const { hierarchyRoots, orphans, nonHierarchy, children } = classifyNodes(nodes, levels);

Expand Down
11 changes: 11 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ const CONFIG_SCHEMA = {
miroBoardId: { type: 'string' },
miroFrameId: { type: 'string' },
plugins: { type: 'object', additionalProperties: { type: 'object' } },
views: {
type: 'object',
additionalProperties: {
type: 'object',
properties: { expression: { type: 'string', minLength: 1 } },
required: ['expression'],
additionalProperties: false,
},
},
},
required: ['name', 'path'],
additionalProperties: false,
Expand All @@ -40,6 +49,8 @@ export type SpaceConfig = {
miroFrameId?: string;
/** Plugin name → plugin config map. Overrides top-level plugins when set. */
plugins?: Record<string, Record<string, unknown>>;
/** Named filter views for this space. Keys are view names; values contain the filter expression. */
views?: Record<string, { expression: string }>;
};

export type Config = {
Expand Down
Loading
Loading