From 8904ef30e29b598fcf32e689c79eb31665cd05ed Mon Sep 17 00:00:00 2001 From: Mike Lowis Date: Thu, 1 Oct 2026 23:52:22 -0400 Subject: [PATCH] documented design snag --- nl-shell-harness-spec.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/nl-shell-harness-spec.md b/nl-shell-harness-spec.md index d5504cd..d588d0b 100644 --- a/nl-shell-harness-spec.md +++ b/nl-shell-harness-spec.md @@ -329,18 +329,20 @@ Start conservative and tune from the audit log (`tier_used`, `confirmed`, and la --- -### 6.5 Control-flow tools (proposal, not yet implemented) +### 6.5 Control-flow tools (minimal slice implemented; full version still proposed) Collapses the former `Classifier` pre-filter for `conditional`/`loop` into the same `Ruleset` tool + Tier 1/2/3 mechanism every other action uses, instead of a separate node kind with its own bespoke matching. `query` already folds in this way with no further changes needed — a query is just a tool whose action looks something up instead of running a destructive command. Loops/conditionals need actual new capability, scoped below: -1. **DSL:** `ToolBuilder` gains a way to mark a slot as a *body* rather than a leaf value — e.g. `rule "if :condition then :body"` declares `:body` as recursive (a `body_slots :body` call, or a naming convention) — so `build` stops requiring every slot to bind one flat keyword to one leaf token. -2. **Extraction:** a body slot consumes *all* leftover tokens after the pattern's literal words are matched, not one leftover token per slot like today's `Tier1.match`/`Tier2.extract_slots` — a different extraction rule, specific to body slots. -3. **Recursive resolution:** the body span's tokens are re-split via `Lexer.clauses` and each resulting clause re-resolved via the same `Utterance.resolve` (Tier1→Tier2→Tier3) used for top-level clauses, producing a nested list of nodes — reusing the one pipeline rather than standing up a parallel one. -4. **Condition parsing:** `:condition` becomes a predicate struct (§7's `equals|contains|exists|greater_than|matches_pattern`, `left`/`right` as `$ref`/literal), not a leaf token — a parsing step beyond today's slot binding, needed regardless of the DSL change above. -5. **Executor:** `Executor.call` currently flattens every arg via `.text`; it needs a branch so a `:body`/`:then`/`:else` arg executes its nested nodes recursively instead of being passed as a string. -6. **Limits:** the depth cap (§7, 3) and loop iteration cap (§10, 20) must be enforced in this new recursive path, not just documented. +1. **DSL:** `ToolBuilder` gains a way to mark a slot as a *body* rather than a leaf value — `body_slots %i[body]` declares `:body` as recursive — so `build` stops requiring every slot to bind one flat keyword to one leaf token. **Implemented**, restricted to a trailing slot only (`build` raises if a declared body slot isn't last in every rule that uses it). +2. **Extraction:** a body slot consumes *all* leftover tokens after the pattern's literal words are matched, not one leftover token per slot like today's `Tier1.match`/`Tier2.extract_slots` — a different extraction rule, specific to body slots. **Implemented** as `Tier1.match_with_body`. +3. **Recursive resolution:** the body span's tokens are re-resolved via the same `Utterance.resolve` (Tier1→Tier2, Tier3 pending) used for top-level clauses, producing a nested node — reusing the one pipeline rather than standing up a parallel one. **Implemented**, but only as a single flat clause (no re-split via `Lexer.clauses` for a multi-clause body yet). +4. **Condition parsing:** `:condition` becomes a predicate struct (§7's `equals|contains|exists|greater_than|matches_pattern`, `left`/`right` as `$ref`/literal), not a leaf token. **Not implemented** — the minimal slice's `conditional` tool hardcodes a single `exists` check on a `:target` leaf slot; a real predicate struct is still needed for `equals`/`contains`/etc. +5. **Executor:** `Executor.call` currently flattens every arg via `.text`; it needs a branch so a `:body`/`:then`/`:else` arg executes its nested nodes recursively instead of being passed as a string. **Implemented**: `Executor.call` passes non-`Token` args through unchanged, and a new `Executor.run(node)` is the shared permission-checked entry point (confirms `:ask` tools, then calls) — used by both the REPL loop and the `conditional` tool's own action, so a destructive body still requires its own confirmation regardless of nesting. +6. **Limits:** the depth cap (§7, 3) and loop iteration cap (§10, 20) must be enforced in this new recursive path, not just documented. **Not implemented** — nesting depth is currently unbounded in practice (e.g. `if A exists do if B exists do archive B` resolves fine), and there is no loop tool yet to iterate. -Suggested first slice: a single `conditional` tool, a flat `:body` of one action, no `else`, no loops — validates the DSL/extraction/executor extensions before generalizing to full nesting and loops. +**Known gap, discovered during implementation — a connective word can't double as rule syntax.** `builtins`' `connectives` list (§5.5) makes `"then"` an unconditional clause separator at the `Lexer.clauses` stage. A rule pattern like `"if :target exists then :body"` can never match, because clause-splitting tears it into two clauses before Tier1/Tier2 ever see the full span — the pattern's literal `"then"` and the lexer's connective `"then"` collide. Worked around for the minimal slice by using `"do"` as the condition/body joiner instead (`"if :target exists do :body"`), but this is a generalizable problem: any future control-flow tool that wants a connective word (`"then"`, `"and"`, `"next"`, etc.) as literal syntax will hit the same wall, and loop syntax (`"for each :item in :list do :body"` or similar) needs to be designed around it too. Needs a real fix before generalizing past the minimal slice — e.g. scoping connective-splitting to happen only outside a span a control-flow tool's pattern would claim, or reserving a connective-free joiner vocabulary for rule authors. + +Suggested next slice: generalize from a single flat `:body` action to a list of actions (still no `else`, no loops), then add the predicate struct, then loops — validating one extension at a time. --- -- 2.55.0