Files
stefan-gorules ffdfd807d1 feat: implement slots instead of natural language (#527)
* feat: implement slots instead of natural language

Replace the natural-language projection (`nl`) with engine-owned slot
completion: for a cursor position the engine classifies the slot being
edited (operand, operator, literal, member access, ...) and returns typed
options, literal facts and the expected type, for policies and graphs.

- zen-expression: new `slot` module (classification, literals, operators)
  and a lenient lexer mode for incomplete input; `nl` removed
- Intellisense: typed diagnostic codes with arguments, richer inspect for
  hover, dedicated parser error variants (messages unchanged)
- Engine: `Workspace::cursor_scope`, `slot`, `facts`; `rename_from` and
  `references_from`; editor spans in UTF-16 units; renames reach imported
  policies, `$nodes` reads, child graphs and output column heads; no `$root`
  completion in policies
- Node bindings: `slot`, `facts`, `cursorScope`, `slotBatch` replace
  `nl`, `nlTokenize`, `nlEncodeString`, `nlTokenizeBatch`

No change to expression evaluation: the VM, compiler, functions and the
default lexer path are untouched, and parser error messages keep their text.

* fix: harden slot classification, scopes and graph expectations

- restore master execution order; slot scopes hide writes of the edited
  block and its dependents instead of relying on block ranking
- token-start editing gated by slot state; operator, list and operand
  spans no longer splice into neighbouring tokens
- interval-aware bracket pairing for half-open and reversed intervals
- lenient lexer recovers from unknown characters (slots only)
- graph expected type follows switch and pass-through nodes and merges
  compatible output schemas
- sibling inference caps distinct values and analyses lazily
- deterministic overload return type display

* fix: order policy blocks by entity reads through closures and relationships

- record reads for `#.field` in closures so entity fields read through
  lists become dependencies
- link reads through derived lists (filter/map results) to the entity
  fields they carry and order the list after those fields
- evaluate entity blocks on single relationships, and demand entity
  paths for plain reads through relationships
- slot scopes keep the top-level variable when hiding written fields
2026-09-28 21:47:40 +02:00

126 lines
4.6 KiB
TOML

# Per-property dependency-lineage tests (`PolicyWorkspace::dependencies`).
#
# policies / content / policy: load policies into the workspace (same as
# diagnostics.toml — fixtures by filename, or inline `content`).
# target: the property whose dependency tree is built. May be a sub-path of a
# computed object/array-of-object (per-field lineage).
#
# Assertions check classification of any node in the tree by its dotted path:
# computed = paths that must resolve to a writer block (lineage continues)
# inputs = paths that must be plain inputs (no writer, resolvable)
# unresolved = paths the analyzer could not pin down (flagged, not silently
# treated as inputs)
[[test]]
name = "loan summary traces through per-account computed interest"
policies = ["loan_summary.json"]
target = "accountSummaries"
computed = ["account.interestDue"]
inputs = ["accounts.id", "account.balance", "account.apr"]
[[test]]
name = "per-field: projected computed field traces to its specific source"
policies = ["loan_summary.json"]
target = "accountSummaries.interest"
computed = ["account.interestDue"]
[[test]]
name = "per-field: projected input field traces to the input, not the computed sibling"
policies = ["loan_summary.json"]
target = "accountSummaries.id"
inputs = ["accounts.id"]
[[test]]
name = "per-field: ternary-of-objects unions both branches"
target = "summary.total"
computed = ["base", "bonus"]
content = '''
{
"blocks": [
{ "id": "dm", "type": "dataModel", "props": { "data": {
"name": "globals", "scope": "global",
"properties": [
{ "id": "g1", "name": "amount", "type": "number", "array": false, "optional": false },
{ "id": "g2", "name": "flag", "type": "boolean", "array": false, "optional": false }
]
}}},
{ "id": "s1", "type": "expression", "props": { "data": { "key": "base", "value": "amount * 2" } } },
{ "id": "s2", "type": "expression", "props": { "data": { "key": "bonus", "value": "amount * 3" } } },
{ "id": "s3", "type": "expression", "props": { "data": { "key": "summary", "value": "flag ? { total: base } : { total: bonus }" } } }
]
}
'''
[[test]]
name = "per-field: non-decomposable computed object field is flagged unresolved, not silent"
target = "wrapper.a"
unresolved = ["wrapper.a"]
content = '''
{
"blocks": [
{ "id": "dm", "type": "dataModel", "props": { "data": {
"name": "globals", "scope": "global",
"properties": [
{ "id": "g1", "name": "amount", "type": "number", "array": false, "optional": false }
]
}}},
{ "id": "s1", "type": "expression", "props": { "data": { "key": "base", "value": "{ a: amount }" } } },
{ "id": "s2", "type": "expression", "props": { "data": { "key": "wrapper", "value": "base" } } }
]
}
'''
[[test]]
name = "untrackable closure collection is flagged, resolvable operands stay inputs"
target = "ledgerTotals"
unresolved = ["t.amount"]
inputs = ["deposits", "withdrawals"]
content = '''
{
"blocks": [
{ "id": "dm-ledger", "type": "dataModel", "props": { "data": {
"name": "ledger", "scope": "global",
"properties": [
{ "id": "g1", "name": "deposits", "type": "number", "array": true, "optional": false },
{ "id": "g2", "name": "withdrawals", "type": "number", "array": true, "optional": false }
]
}}},
{ "id": "s1", "type": "expression", "props": { "data": { "key": "ledgerTotals", "value": "map(deposits or withdrawals as t, t.amount)" } } }
]
}
'''
[[test]]
name = "nested write parent unions its leaf writers' dependencies"
target = "loanSummary"
inputs = ["principal"]
content = '''
{
"blocks": [
{ "id": "dm", "type": "dataModel", "props": { "data": {
"name": "globals", "scope": "global",
"properties": [
{ "id": "g1", "name": "principal", "type": "number", "array": false, "optional": false }
]
}}},
{ "id": "s1", "type": "expression", "props": { "data": { "key": "loanSummary.byBucket.CURRENT.interestDue", "value": "principal * 0.04" } } },
{ "id": "s2", "type": "expression", "props": { "data": { "key": "loanSummary.grandTotal", "value": "principal * 1.04" } } }
]
}
'''
[[test]]
name = "dependencies follow the open policy when siblings write the same field (A)"
policies = ["merge_policy_a.json", "merge_policy_b.json", "merge_policy_c.json"]
policy = "merge_policy_a.json"
target = "customer.greeting"
inputs = ["customer.name"]
[[test]]
name = "dependencies follow the open policy when siblings write the same field (C)"
policies = ["merge_policy_a.json", "merge_policy_b.json", "merge_policy_c.json"]
policy = "merge_policy_c.json"
target = "customer.greeting"
inputs = ["customer.country"]