Skip to content
Draft
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
2 changes: 2 additions & 0 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ Rules may be:

Rules are distinct from schema validation: the schema checks structure; rules check meaning and quality.

A **rule template** is a rule with `abstract: true`. It never runs; a rule that names it in `extends` takes its fields and sets its own.

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.
Expand Down
4 changes: 4 additions & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ Example override:
}
```

### Rule templates

A rule can reuse a rule template with `extends`. See [Rule templates](semantic-rules.md#rule-templates).

## Cascade gating

When a schema uses `$metadata.hierarchy`, rule evaluation cascades down the hierarchy, one level at a time from roots and orphans. A node becomes a **gate** when it fails in a way that makes its descendants' rules untrustworthy or wasteful to run. The nodes below a gate are **gated**: their rules are not evaluated.
Expand Down
2 changes: 1 addition & 1 deletion docs/schemas.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ Every schema `$id` uses the `sctx://` scheme. The shape of the `$id` says where
- `$id` values follow the [namespace rules](#schema-id-namespaces) and must not collide across bundled, plugin and local schemas.
- `$ref` resolution is transitive across files.
- Partials with no `$metadata` should prefer `$schema: "https://json-schema.org/draft/2020-12/schema"` so they validate standalone as plain JSON Schema fragments.
- **Bundled partials as entity libraries**: `_sctx_base.json`, `_strategy_general.json`, `_knowledge_wiki.json`, `_okf.json`, and `_ost_strict.json` provide reusable entity definitions and metadata. Composing schemas can reference their entity types via `$ref` rather than redefining them.
- **Bundled partials as entity libraries**: `_sctx_base.json`, `_strategy_general.json`, `_knowledge_wiki.json`, `_okf.json`, and `_ost_strict.json` provide reusable entity definitions and metadata. Composing schemas can reference their entity types via `$ref` rather than redefining them. `_semantic_patterns.json` provides [rule templates](semantic-rules.md#rule-templates).
- **Partials can carry metadata**: Partials may include `$metadata` (hierarchy, aliases, relationships, rules). A schema gets it only by listing the partial in `$metadata.imports`; `$ref` alone brings none.

## Plugin-contributed schemas
Expand Down
26 changes: 19 additions & 7 deletions docs/semantic-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ A semantic rule has the [common rule fields](rules.md) and a `semantic` object.
| Field | Required | Description |
|---|---|---|
| `semantic.rubric` | no | Tells the model what to assess. Default: `description` |
| `semantic.criterion` | no | Text the model reads after the rubric. A rule that extends a [template](#rule-templates) uses it to make the template's rubric specific |
| `semantic.verdicts` | no | The answers the model can give. Default: `pass` and `fail`. See [Verdicts](#verdicts) |
| `semantic.severity` | if `semantic.verdicts` is set | The severity of each verdict. See [Severity](#severity) |
| `semantic.scope` | no | The related nodes to send with the evaluated node. Default: none. See [Related nodes and content](#related-nodes-and-content) |
Expand Down Expand Up @@ -146,15 +147,26 @@ Scope sends node data only, never the verdicts of other rules, and [gating](rule

A global rule cannot have a `semantic` part, so it has no scope.

The bundled schemas use scope in three ways. We recommend them as models for your own rules:
## Rule templates

| Check | Attach the rule to | Scope | Bundled example |
|---|---|---|---|
| The children are distinct, or cover the node | The parent, with a `when` that needs two or more children | `children` | `opportunities-distinct`, `goals-cover-parent` |
| The node fits its parent | The child | `parent` | `opportunity-refines-parent`, `solution-addresses-opportunity` |
| The node serves the intent of the levels above | The child | `ancestors` | `solution-serves-outcome`, `opportunity-serves-ancestors` |
A **rule template** is a rule with `"abstract": true`, which never runs. A rule that names it in `extends` takes all of its fields, then sets its own. Top-level fields replace the template's. Fields inside `semantic` replace the template's one at a time, so a rule can add a `criterion` and keep the template's rubric, scope and verdicts.

The rules are in the bundled partials, such as `schemas/_ost_strict.json`.
The bundled partial `sctx://core/_semantic_patterns` holds four templates. To use one, add the partial to `$metadata.imports`:

| Template | Checks | Attach the rule to | Scope | Bundled rules that extend it |
|---|---|---|---|---|
| `children-distinct` | The children are distinct | The parent, with a `when` that needs two or more children | `children` | `opportunities-distinct`, `goals-distinct` |
| `children-cover-parent` | The children cover the parent | The parent, with the same `when` | `children` | `goals-cover-parent` |
| `refines-parent` | The node is one part, cause, case or means of its parent | The child | `parent` | `opportunity-refines-parent`, `goal-advances-parent` |
| `serves-ancestors` | The node serves the intent of the levels above | The child | `ancestors` | `solution-serves-outcome`, `opportunity-serves-ancestors` |

```json

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considerations as part of the review of this PR and the one it's stacked on:

  • Is criterion needed?
  • Is criterion a good name? Is "criterion gets tacked onto rubric" a valid and reasonably understandable statement to a user?
  • How does a custom rule (no template, hand-coded in top-level schema) vs a template rule with criterion vs a template rule without criterion compare in evals, for the same intent? That informs where we might focus attention on the shape, composition and wording used in rules.

{ "id": "goals-distinct", "extends": "children-distinct", "description": "Sibling goals are distinct",

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't feel like a great example yet. We need to prove that criterion is useful vs not having one at all for a given use of a bundled template.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a good example might emerge where the criterion clarifies the interpretation of the language in a rule. eg "refines" means {blah} in this context

"when": "$count(nodes[$$.current.id in resolvedParentIds and resolvedType = 'goal']) >= 2",
"semantic": { "criterion": "Judge the child goals. Each should be a different result." } }
```

A rule must extend a template, not another rule, and a template cannot extend a template. The rule is checked as a full rule once it takes the template's fields.

## Input size and trimming

Expand Down
20 changes: 10 additions & 10 deletions evals/semantic/goal-advances-parent.json5
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,32 @@
space: 'strategy-trees',
expected: {
'goal-advances-parent': {
'Launch online ordering for 100 bookshops.md': 'advances',
'Help bookshops show live stock on their own websites.md': 'advances',
'Launch online ordering for 100 bookshops.md': 'refines',
'Help bookshops show live stock on their own websites.md': 'refines',
// About bookshops, but not tools for them.
'Open our own flagship bookshop.md': ['weak-link', 'misplaced'],
'Grow our social media following.md': 'weak-link',
// Restates the mission.
'Give independent bookshops online tools.md': 'weak-link',
// Sells through a rival of the bookshops, not for them.
'Sell our own e-books on Kindle.md': ['weak-link', 'misplaced'],
'Sign 100 bookshops to the ordering pilot.md': 'advances',
'Build a checkout that works on phones.md': 'advances',
'Sign 100 bookshops to the ordering pilot.md': 'refines',
'Build a checkout that works on phones.md': 'refines',
// Restates the parent goal.
'Get 100 bookshops ordering online.md': 'weak-link',
'Run a reading festival.md': 'misplaced',
// Support could help the launch, but the goal does not say how.
'Improve our customer support.md': 'weak-link',
'Cook dinner together three nights a week.md': 'advances',
'Cook dinner together three nights a week.md': 'refines',
'Get a promotion this year.md': ['weak-link', 'misplaced'],
'Take the kids hiking every month.md': 'advances',
'Take the kids hiking every month.md': 'refines',
// Restates half of the mission.
'Be healthier.md': ['advances', 'weak-link'],
'Build to 80 km weeks by January.md': 'advances',
'Be healthier.md': ['refines', 'weak-link'],
'Build to 80 km weeks by January.md': 'refines',
// A means to the goal more than a part of it.
'Buy new trail shoes.md': ['advances', 'weak-link'],
'Buy new trail shoes.md': ['refines', 'weak-link'],
// Supports the race more than it is part of it.
'Lose 5 kg before race day.md': ['advances', 'weak-link'],
'Lose 5 kg before race day.md': ['refines', 'weak-link'],
},
},
}
2 changes: 1 addition & 1 deletion plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "structured-context",
"version": "0.23.14",
"version": "0.23.15",
"description": "Structured content authoring for Obsidian — automatic validation hooks, slash commands, and agent skills to keep markdown files compliant with your schema.",
"homepage": "https://github.com/mindsocket/structured-context",
"repository": "https://github.com/mindsocket/structured-context",
Expand Down
132 changes: 132 additions & 0 deletions plugin/hooks/projection.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions plugin/skills/structured-context/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ what the rule actually sees in the `current` object, then adjust the rule in the
| `Link target "X" in field "parent" not found` | Broken wikilink | Fix the link, or ensure the target file exists and has that name |
| `Link target "X" in field "parent" is ambiguous (matches multiple nodes)` | Several nodes share that name, for example `Overview.md` in different folders | Link with enough of the path to be unique, such as `[[alpha/Overview]]` |
| `[rule-id] ... (Rule error: ...)` | A rule's `expression` failed to evaluate (syntax or runtime error) | Verify the expression with `dump` and a JSONata tester |
| `Rule "X" extends "Y", but no rule template ... has that id` | `extends` names a rule that is missing or lacks `"abstract": true` | Import the partial that holds the template (bundled ones are in `sctx://core/_semantic_patterns`), or fix the id |
| `[rule-id] ... (Semantic evaluation error: ...)` | The model provider call failed, or input is still over the per-node input cap after trimming | Check `sctx providers` and the API key; narrow `semantic.scope` or drop `includeContent` |

## Plugins
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ Other rule fields:
- `includeContent` — `nodeOnly` (the evaluated node's body) or `fullScope`/`true` (every body in scope). Without it, no body is sent.
- A rule with `scope` or `includeContent` may return `missing-context` (default `warning`) when its input was trimmed to fit.
- Semantic rules send the schema's type and field descriptions to the model with the rubric. Descriptions that say what a good node looks like save repeating that detail in each rubric, and let more general rules draw on the schema.
- To check a set of children, a fit with the parent, or the intent of the levels above, copy a bundled rule (`opportunities-distinct`, `opportunity-refines-parent`, `solution-serves-outcome` in `_ost_strict.json`). `sctx docs semantic-rules` shows which scope each uses.
- To check a set of children, a fit with the parent, or the intent of the levels above, import `sctx://core/_semantic_patterns` and extend one of its rule templates: `{ "id": "…", "extends": "children-distinct", "type": "…", "semantic": { "criterion": "…" } }`. `sctx docs semantic-rules` lists the templates.

## Metadata composition semantics

Expand Down
Loading
Loading