Skip to content

feat: executable rules and hierarchy validation#29

Merged
mindsocket merged 3 commits into
mainfrom
feat/rules
Mar 1, 2026
Merged

feat: executable rules and hierarchy validation#29
mindsocket merged 3 commits into
mainfrom
feat/rules

Conversation

@mindsocket
Copy link
Copy Markdown
Owner

@mindsocket mindsocket commented Mar 1, 2026

Summary

  • Adds hierarchy validation that checks parent-child type relationships against the hierarchy array in schema metadata, with support for allowSelfRef (e.g. nested opportunities) and allowSkipLevels flags
  • Adds an executable rules system using JSONata expressions embedded in schema _metadata.rules, organised into validation, coherence, and bestPractice categories
  • Extends schema metadata loading (loadMetadata) to return aliases, allowSelfRef, and rules alongside the hierarchy — used by all read paths and the validate command
  • Adds resolvedType to SpaceNode, populated at parse time by resolving type aliases from schema metadata
  • Ships initial rules in _strict.json: active outcome/opportunity count (coherence) and solution quantity per opportunity (best-practice)

Closes #23, Closes #16

Test plan

  • bun run test — all unit tests pass (evaluate-rule, validate-hierarchy, validate-rules, validate-strict)
  • bun run test:smoke — smoke tests pass against configured spaces
  • bun run src/index.ts validate <strict-ost-space> — rule violations and hierarchy violations reported correctly in output
  • Verify a solution with a solution parent triggers a hierarchy violation
  • Verify two active outcomes triggers a coherence rule violation

Comment thread src/validate-rules.ts Outdated
Add two new validation passes that run after JSON schema validation:

- **Hierarchy validation** (`validate-hierarchy.ts`): checks parent-child
  type relationships against the schema's `hierarchy` array, with support
  for `allowSelfRef` and `allowSkipLevels` metadata flags.

- **Executable rules** (`validate-rules.ts`, `evaluate-rule.ts`): evaluates
  JSONata expressions stored in schema `_metadata.rules` against the node
  set. Rules are organised into `validation`, `coherence`, and `bestPractice`
  categories. Violations are grouped by category in the output.

Schema changes:
- `_strict.json`: coherence rules (active outcome/opportunity count) and a
  best-practice rule (solution quantity per opportunity) with JSONata checks.
- `general.json`: adds `aliases` and `allowSelfRef` metadata.
- `strict_ost.json`: removes redundant local `_metadata` (now inherited from
  `_strict`), tightens `assumption_test` type to `const`.
- `loadHierarchy` replaced by `loadMetadata` which returns the full metadata
  object including aliases, allowSelfRef, and rules.
- `resolveNodeType` added for alias resolution; `resolvedType` field added to
  `SpaceNode` and populated at parse time.

Closes #21, #16
Comment thread schemas/_strict.json
"Only one target opportunity should be active at a time"
"coherence": [
{
"id": "active-outcome-count",
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 isn't coherence. Add a workflow category for both of these rules.

Coherence is more for things like "the goal is to make $10M" when "the opportunity is to rebuild the design system". Coherence rules are likely to be more qualitative.

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.

Also add a workflow check that an active (status) node should have an active (status) parent.

Ideally this goes into _shared but we don't have a way to merge rules yet - now we have a reason to want #17 (comment)

Comment thread schemas/_strict.json Outdated
"id": "solution-quantity",
"description": "Explore multiple candidate solutions (aim for at least three) for the target opportunity",
"type": "opportunity",
"check": "$count(nodes[resolvedParentTitle=$$.current.title and resolvedType='solution']) >= 3"
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 should only apply when status in ['exploring', 'active']

Comment thread src/validate-hierarchy.ts
}
}

for (const node of nodes) {
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.

Also check that allowSelfRef is strictly a subset of hierarchy. If not -> violation

…g validation

- Add `workflow` rule category for process discipline checks; move
  active-outcome/opportunity-count rules from coherence to workflow
- Add `active-node-parent-active` workflow rule: an active node's parent
  must also be active
- Update `solution-quantity` best-practice to only apply when opportunity
  status is exploring or active
- Check `allowSelfRef ⊆ hierarchy` in validate command, reporting
  mismatches as config errors with a new configErrors result field
- Rename schemaErrors/schemaValidCount → nodeErrors/validCount for clarity
- Update docs/rules.md: add workflow category, clarify scope is
  orthogonal to category, update examples
Comment on lines +103 to +104
const context = buildEvalContext(mockNode, allNodes, mockNodeIndex);
const result = await evaluateExpression('invalid.syntax.('!, context);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The string literal closes earlier than intended. In JavaScript/TypeScript the tokenizer closes the string at the second ' (after (), so this is parsed as:

evaluateExpression('invalid.syntax.(', !context)

!context evaluates to false (boolean), which is passed as the second argument instead of the EvalContext object. TypeScript should flag this as a type error. The test passes by coincidence because the function's catch block returns false regardless of whether the failure was an invalid expression or an invalid context argument.

Suggested change
const context = buildEvalContext(mockNode, allNodes, mockNodeIndex);
const result = await evaluateExpression('invalid.syntax.('!, context);
const result = await evaluateExpression("invalid.syntax.('!", context);

Comment thread src/validate-hierarchy.ts
Comment thread src/types.ts
@mindsocket mindsocket merged commit 7975f83 into main Mar 1, 2026
1 of 2 checks passed
@mindsocket mindsocket deleted the feat/rules branch March 1, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: enforce parent-child hierarchy rules in validation Support descriptive and executable rules as schema metadata

1 participant