diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..1ab1d832 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,14 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Collaboration Rules + +generic project rules: see [`agents/rules.md`](agents/rules.md). +when assisting in architecture analysis/review: + strictly follow [`agents/architecture_assistance.md`](agents/architecture_assistance.md). + +## What this is + +See [system overview](agents/context.md) + diff --git a/agents/architecture_assistance.md b/agents/architecture_assistance.md new file mode 100644 index 00000000..13b34152 --- /dev/null +++ b/agents/architecture_assistance.md @@ -0,0 +1,13 @@ +When acting as an assistant to architect/analyst: + +- Role: assistant to a senior software architect/developer — analysis, code inspection, suggestions, and other cognitive tasks by default. +- When broad picture of architecture is needed: Check doc/development/ (especially [overview](doc/development/overview.md), [internals](doc/development/internals/) and [conventions](doc/development/conventions/) prior to reverse-engineering the codebase +- File edits are allowed when requested; the user reviews all changes and handles commits/stashes. +- Only make local edits of files beyond doc/ when explicitly asked to. +- No git commits, no GitHub interaction, no agent mode. +- Apply project coding rules: see [`agents/rules.md`](agents/rules.md). +- grep, bash, sed and similar tools are allowed unless prohibited by other rules +- tampering with git history (altering .git directory) is prohibited +- read-only git operations allowed, staging is allowed with confirmation +- git reset/unstage/commit/rebase/merge/pull/push and similar -- only when explicitly asked +- read-only access to public git repositories and to raw.githubusercontent.com is allowed diff --git a/agents/context.md b/agents/context.md new file mode 100644 index 00000000..9a27c7b5 --- /dev/null +++ b/agents/context.md @@ -0,0 +1,68 @@ +## Reference Material + +**Do not load these files proactively** — read on demand only. + +- `doc/development/conventions/code.md` — formatting and style conventions +- `doc/development/conventions/architecture_principles.md` — design philosophy +- `doc/development/conventions/git.md` — commit message conventions +- `doc/development/overview.md` — MVC structure, OOP patterns, modes, evaluation pipeline +- `doc/development/drawing_system.md` — virtual canvas, pen-and-paper vs real-time draw +- `doc/development/internals/` — console, editor, user input, examples (detailed subsystem docs) + +## What This Is + +**Compy** is a console-based, Lua-programmable fantasy computer for children, built on the [LÖVE2D](https://love2d.org) framework (v11.5). MVC architecture, classes as Lua globals, Lua 5.1/LuaJIT. Runs as an IDE, a project player, or a test harness. See `doc/development/overview.md` for architecture details. + +## Commands + +### Running + +```sh +love src # IDE mode +love src test [--auto] [--size] [--draw] # test mode +love src play # run a project or .compy zip +love src harmony # screenshot testing mode +``` + +### Tests + +```sh +busted tests # run all +busted tests --tags # by tag (common: ast, src, parser, analyzer) +just ut # run once for one tag +just ut_all # run all once +just unit_test # all, with nodemon auto-reload +just unit_test_tag # one tag, auto-reload +``` + +### Development + +```sh +just dev # run app with auto-reload on .lua changes +just dev-atest # run app with --auto test on each change +``` + +### Packaging & Setup + +```sh +just package # dist/game.love +just package-js # web version via love.js +just setup-web-dev # install web tooling (npm) +just deploy-examples # copy examples to ~/Documents/compy/projects +git clone --recurse-submodules # required: two submodules (metalua, stringutils) +luarocks --local --lua-version 5.1 install busted luautf8 luafilesystem +``` + +### Environment Variables + +| Variable | Effect | +|---|---| +| `DEBUG=1` | Debug mode (extra keybindings, draw overlays) | +| `HIDPI=true` | Double-scale display | +| `COMPY_PROF=1` | Enable profiler | +| `COMPY_WRAP=` | Override drawable char width (debug mode) | +| `COMPY_LINES=` | Override visible line count (debug mode) | + +### Pre-commit Hook + +`just setup-hooks` installs a hook that runs `busted tests` before every commit. diff --git a/agents/rules.md b/agents/rules.md new file mode 100644 index 00000000..13af2485 --- /dev/null +++ b/agents/rules.md @@ -0,0 +1,133 @@ +# Compy — Coding Rules + +Audience: LLM assistant. Apply when writing, reviewing, or analysing code in this repo. +Source: compiled from [`doc/development/conventions/code.md`](../doc/development/conventions/code.md) and [`doc/development/conventions/architecture_principles.md`](../doc/development/conventions/architecture_principles.md). + +--- + +## Architecture analysis + +When broad picture required (not a focused check of specific behaviour): + Use `./doc/development/*` as the initial source of syntetic pre-extracted knowledge prior to direct codebase inspection (assume minor mistakes though) + +Load those as needed for the current task, not everything upfront. + +Mapping of reference docs: +./doc/development/overview.md -- bird-eye overview of the architecture +`./doc/development/conventions/*` -- fundamental dev-facing rules +./doc/development/conventions/architecture_principles.md -- fundamental conventions and constraints applying to the whole project +./doc/development/conventions/code.md -- code style +./doc/development/conventions/git.md -- conventional commits usage +`./doc/development/internals/*` -- current implementation essentials, derived from codebase isnpection -- currently cover editor, console, examples +./doc/development/internals/user_input.md -- cross-component usage of user input in different modes +./doc/development/drawing_system.md -- unobvious wiring of drawing system, enabling paper-and-pen vs realtime drawing modes + +Some (not all) essential excerpts from these docs are inlined below (they may be sufficient in many contexts) + +## Hard Limits (coding) + +| Constraint | Limit | +|---|---| +| Line length | 64 chars | +| Function body | 14 lines | +| Parameters | 4 | +| Nesting depth | 4 | + +When a limit is approached, redesign — don't raise the limit. + +--- + +## Formatting (editor-enforced) + +- Metalua auto-prettyprints; don't fight it. Published examples must be idempotent under the built-in editor. +- `end` closes on the same line as the last statement in its block. +- Empty table: `{ }` (spaces inside braces). +- Table/array literals: one element per line. +- Comments: own line only, never inline. +- At most one blank line between non-blank lines. + +--- + +## Scope & Naming + +- Locals scoped to the smallest enclosing block. No variable outlives its purpose. +- Extract compound conditions to named locals — name the intent, not the mechanics: + ```lua + local done = count >= max or timed_out + if done then ... end + ``` +- Standard aliases at module top: `local gfx = love.graphics`, `local sfx = compy.audio`. + +--- + +## Design + +**File = console equivalence.** Code in a `.lua` file and the same code typed into the Compy REPL must behave identically. No load-order side effects; no implicit global state that the REPL wouldn't have. + +**Prefer functional style.** Closures, iterators, and immutable-by-convention data are preferred. They compose cleanly and stay readable. + +**Avoid deep OOP.** Inheritance chains, manager-of-manager classes, and factory abstractions are anti-patterns here. If a student with six months of experience couldn't follow the call graph, it's too abstract. + +**No C accent.** Don't use string/integer tags as surrogate function pointers dispatched via if-chains. Store and pass the function itself. +```lua +-- wrong +local dir = 'up' +if dir == 'up' then go_up() elseif dir == 'down' then go_down() end + +-- right +local dir = go_up +dir() +``` +Same applies to dispatch tables: `actions[key]()` beats `if key == 'x' then ... end`. The framework has accumulated some of this; new code and all examples must not. + +**Pedagogical test.** Before adding an abstraction: *could a motivated student understand and modify this?* If not, look for a simpler path. + +--- + +## Tone in Analytic Notes and Specs + +Tech debt and internal inconsistencies are expected in any system that evolved organically under real constraints. Treat them as such — not as failures to highlight, but as context to note. + +When writing analysis, specs, or review notes: +- State what is, no blame tone. "This module has accumulated several responsibilities" not "this is bad/wrong"". +- Flag debt matter-of-factly without judgement: "likely a historic tradeoff", "has known complexity here", "worth revisiting when X", "may prevent future Y unless resolved". +- The audience is the same people who built the system. They are senior, aware of the tradeoffs, and acting in good faith. They do not need to be told something is "wrong" — they need a clear picture of what is and what the options are. +- That said, no unsolicited flattery or patronism, no false reassurance. Collegial, humble, accurate and direct is the target register. + +--- + +## Summaries for Stakeholders + +When writing a stakeholder-facing summary or "in brief" section: + +- **Mirror the stakeholders' own tone and everyday vocabulary** from the source input — their words ("get rid of the legacy API", "only text fields", "before 1.0", "showcase", "trivial to convert"), not internal jargon (milestone IDs, "PERT", "reftable", "facade", "sink"). Spell a term or figure out only the first time it is genuinely unavoidable. +- **Keep it short without losing meaning.** A summary is a statement, not a dialogue — no second-person address. +- **Do not re-explain stakeholders' own requests back to them in the same words.** They already know what they asked for. In a feedback-check context, answer what they actually want to know: whether any architectural regression resulted, whether price / risk / order changed, and what the new plan is — and point them to the updated `roadmap.md` / `summaries/roadmap.md`, which is where they will look. +- **Read whether they are confirming or exploring.** Judge the register of the ask first: are they *assuming* something and looking to have it confirmed or disproved, or are they *unsure* and want more information? When they are clearly assuming (a leading question, a stated presumption), frame a matching finding as a confirmation ("Confirmed: …", "as expected") rather than presenting it as a surprise — and disprove just as directly when the assumption is wrong. When they are genuinely unsure, lead with the information plainly. + +--- + +## Commit Messages + +Conventional Commits, driven by semver semantics. Full guide: `doc/development/conventions/git.md`. + +| Type | When | +|---|---| +| `feat` | New capability | +| `fix` | Bug fix | +| `refactor` | Restructuring, no behavior change intended | +| `style` | Cosmetic source changes only (whitespace, comments) — **not** CSS | +| `test` | Test code only | +| `chore` | Build system, scripts, tooling — **not** "small annoying task" | +| `docs` | Documentation only | + +Breaking change: append `!` to type (`feat!:`) or add `BREAKING CHANGE:` footer. +Scope: project-specific, not yet formally documented — omit rather than guess. + +--- + +## Memory / GC + +- Avoid allocation in hot paths (`update`, `draw`). Reuse tables and values where the pattern is clear. +- **Mandatory in student-facing examples.** Encouraged in new framework code. Existing framework code may have known debt here and there — just don't add more. +- Prefer iterators over intermediate tables. Prefer preallocated buffers over per-frame construction. diff --git a/doc/development/conventions/architecture_principles.md b/doc/development/conventions/architecture_principles.md new file mode 100644 index 00000000..29359f57 --- /dev/null +++ b/doc/development/conventions/architecture_principles.md @@ -0,0 +1,82 @@ +# Architecture Principles + +Compy is an educational artifact as much as a software project. Its architecture reflects that: the goal is to model good habits for students, not to maximize abstraction or minimize developer friction. + +--- + +## Transparency + +**Code in a file and code typed into the console must have the same effect.** + +This is not just a technical constraint — it is the foundational design principle. It means modules must not rely on load-order side effects, implicit global state should be minimal, and the runtime environment seen by student code should feel like a natural extension of the REPL they work in daily. + +A corollary: if a piece of code can only be understood in the context of its call stack, its class hierarchy, or its module graph, it is already too complex. + +--- + +## Cognitive Budget + +The structural limits in the code conventions (64-char lines, 14-line functions, 4 parameters, 4 nesting levels) are not style preferences — they are a budget. They exist to keep code within the working memory of a child reading it. This applies doubly to student-facing examples. + +When a piece of code is approaching a limit, the right response is usually to rethink the design, not to raise the limit. + +--- + +## Scope Discipline + +Locals belong to the smallest block that needs them. No variable should outlive its purpose. This reduces cognitive load (fewer names in scope at any point) and avoids subtle bugs from state leaking across logical boundaries. + +Extract compound conditions to named locals — this is not just style, it is a form of inline documentation that names the *intent* of a test. + +--- + +## Prefer Functional Style + +Lightweight closures, Lua iterators, and immutable-by-convention data structures are preferred over stateful objects wherever readability is not harmed. Closures are cheap, local, and self-documenting in Lua. Iterators compose cleanly. Immutable data is easy to reason about. + +Deeply layered OOP — inheritance chains, manager classes, factory-of-factory patterns — is an anti-pattern here. It hides behavior, inflates the call graph, and produces code that is hard for a student (or a teacher) to follow. Our metric is not "is this extensible?" but "can someone who has been programming for six months read this?" + +--- + +## Write Lua, Not C with Lua Syntax + +A recurring C habit is using a string or integer tag as a surrogate function pointer, then dispatching on it with an if-chain: + +```lua +-- C-accented Lua +local dir = 'up' +if dir == 'up' then go_up() +elseif dir == 'down' then go_down() end +``` + +In Lua, functions are values. The tag and the if-chain are unnecessary: + +```lua +-- idiomatic Lua +local dir = go_up +dir() +``` + +The same applies to dispatch tables: rather than a string key checked at call time, store the function directly and call it. The result is shorter, has no conditional, and makes the intent structurally visible rather than textually described. + +This matters especially in student-facing code. Students learning Lua should build Lua intuitions — treating functions as first-class values, not as something to be named with a string and dispatched manually. The C habit, once acquired, is hard to unlearn and tends to produce code that is both more verbose and harder to reason about than the idiomatic alternative. + +The framework has accumulated some of this pattern in places (it is organic code); new code and all examples should avoid it. + +--- + +## Memory Consciousness + +Avoid unnecessary allocation. In a GC-managed runtime on modest hardware, object churn adds up — in tight loops especially. Prefer reusing tables and values over creating new ones when the logic permits. + +This principle is **mandatory in student-facing examples** (which students read, copy, and run on real hardware) and **encouraged in new framework code**. The organically grown parts of the framework are not always clean on this front; that is acceptable technical debt, not a license to add more. + +Concrete habits: avoid allocating in `update`/`draw` hot paths; prefer iterators over intermediate tables; reuse preallocated buffers where the pattern is clear. + +--- + +## The Pedagogical Test + +Before introducing a new abstraction, pattern, or dependency, ask: *would a motivated student be able to understand and modify this?* If the honest answer is no, look for a simpler approach. + +The project is not a software factory. It is a teaching instrument that also happens to run. diff --git a/doc/development/conventions/code.md b/doc/development/conventions/code.md new file mode 100644 index 00000000..e79ff568 --- /dev/null +++ b/doc/development/conventions/code.md @@ -0,0 +1,78 @@ +# Code Conventions + +> These conventions are not fully fixed — they evolve with experience. Some are unique to this project by design. +> Further discussion: https://github.com/compy-toys/compy/issues/54 + +--- + +## Formatting + +Formatting is handled automatically by the metalua pretty-printer. No manual formatting rules are enforced — the built-in Compy editor auto-reformats on save. **Published examples must be idempotent under the editor** (run the editor over the code before publishing; it should produce no changes). + +### Style examples + +```lua +-- if/for/function: `end` on the same line as the last statement in the block +if condition then + ...end + +for iteration do + ...end + +function outer(args) + local inner = function(args) + ... end +end + +-- empty table: spaces inside braces +empty = { } + +-- array/table: each element on its own line +array = { + 1, + 2, + 3 +} + +-- all comments on their own line (not inline) + +-- at most one blank line between non-blank lines +``` + +--- + +## Structural Limits + +| Rule | Limit | +|---|---| +| Line length | 64 characters (fits the Compy screen) | +| Function length | 14 lines | +| Parameters per function | 4 | +| Nesting depth | 4 levels | + +--- + +## Code Style + +**Extract compound conditions to named locals:** +```lua +-- avoid +if a and b or c then end + +-- prefer +local cond = a and b or c +if cond then end +``` + +**Do not use local variables outside their block.** A local should be scoped as tightly as possible. + +**File = console equivalence.** Code in a `.lua` file and the same code typed into the Compy console must have identical effect. This guides how modules and scripts are structured. + +--- + +## Standard Abbreviations + +```lua +local gfx = love.graphics +local sfx = compy.audio +``` diff --git a/doc/development/conventions/git.md b/doc/development/conventions/git.md new file mode 100644 index 00000000..212baa40 --- /dev/null +++ b/doc/development/conventions/git.md @@ -0,0 +1,35 @@ +# Commit Message Conventions + +This project follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). The primary motivation is **automatic semantic versioning** — commit types signal the severity of a change, not just its category. + +## Types + +| Type | Meaning | Semver impact | +|---|---|---| +| `feat` | New feature or capability | minor | +| `fix` | Bug fix | patch | +| `refactor` | Code restructuring with no expected behavior change | none | +| `style` | Cosmetic changes only: whitespace, formatting, comments | none | +| `test` | Changes touching test code only | none | +| `chore` | Project tooling, build system, scripts — not source code | none | +| `docs` | Documentation only | none | + +## Common Misreadings + +**`style` does not mean CSS or UI styling.** It means cosmetic source changes — whitespace, indentation, comment wording — with zero semantic effect. This is a frequent misunderstanding in frontend projects; it does not apply here. + +**`chore` does not mean "small annoying task."** It means housekeeping at the project level: changes to the build system, helper scripts, CI config, tooling. If code or tests changed, it is not a chore. + +**`refactor` carries an implicit promise.** It says: behavior is not expected to change. Accidents happen, but the intent must be genuine — if you know the behavior is changing, it is a `fix` or `feat`. + +## Scope + +Scope (the part in parentheses: `feat(editor): ...`) follows project-specific conventions that are local to this project. These have not yet been formally documented. When in doubt, omit scope rather than guess. + +## Breaking Changes + +A `!` suffix or `BREAKING CHANGE:` footer signals a breaking change regardless of type (e.g. `feat!:`, `refactor!:`). This triggers a major version bump. + +## Reference + +Full spec: https://www.conventionalcommits.org/en/v1.0.0/ diff --git a/doc/development/docs.md b/doc/development/docs.md new file mode 100644 index 00000000..18dd97f6 --- /dev/null +++ b/doc/development/docs.md @@ -0,0 +1,50 @@ +# Project Documentation Review + +Assessment of `doc/` relative to the codebase and the knowledge base under `doc/development/`. Organised into: relevant (usable as-is), drift (needs attention), gaps. + +--- + +## Relevant — Recommended for Active Use + +**`doc/mermaid/fsm.md` + `fsm_f.md`** — App state machine. The clearest single-file reference for all `app_state` transitions, including editor↔running toggle and inspect↔editor paths. The KB (`console.md`) summarises the table; the Mermaid diagrams show the full graph. Consult when reasoning about state transitions. + +**`doc/mermaid/eval.md`** — The "Planned refactor" section (upper half) accurately describes the current `Evaluator` + `Filters` architecture. Useful as a type-level reference for the evaluator pipeline. + +**`doc/development/editor/visible.md`** — Three coordinate spaces (normal → wrapped → visible) with a worked ASCII example. More concrete than the description in `editor.md` and worth reading alongside it. + +**`doc/development/lib_deps.md`** — Full module dependency graph and utility tier breakdown. `overview.md` references it; consult directly when tracing load-order or dependency questions. + +**`doc/EDITOR.md` — Principles section** (lines 1–17) — The four editor design invariants are accurately stated, including the `loaded_is_sel` rule. Remains authoritative. + +**`doc/AST.md`** — Metalua AST token reference table. Accurate. Useful when working on the parser, analyzer, or any code that walks the AST. + +**`doc/development/error_explorer.md`** — Documents the `src/lib/error_explorer` third-party library. Referenced from `overview.md`. + +**`doc/intro.md`** — Hardware/platform context (7" Android device, SD card storage layout). Referenced from `overview.md`. + +--- + +## Drift — Worth Developer Attention + +**`doc/mermaid/classes.md`** — References `InterpreterModel` and `InterpreterController`, which were refactored into `UserInputModel`/`UserInputController`. The class diagrams reflect a prior architecture and are misleading as current reference. + +**`doc/mermaid/eval.md` — "Current" section** (lower half) — Describes an old `EvalBase` inheritance chain (`TextEval --|> EvalBase` etc.) that no longer exists. The "Planned refactor" section is what was implemented. + +**`doc/mermaid/input.md`** — Same pattern as eval.md. The "Planned refactor" describes the current `UserInputModel` structure; the diagram is now the ground truth, but the framing as "planned" is confusing. + +**`doc/EDITOR.md` — Keybindings table** — Several entries have drifted from the implementation: +- `F9` listed for toggle edit/run; code and README both use `F8` +- `Ctrl+Shift+Q` listed for quit project; code uses `Ctrl+Q` +- Missing entries: reorder mode (`Ctrl+M`), search mode (`Ctrl+F`), follow-require (`Ctrl+O`), insert-before (`Ctrl+Enter`), copy/cut/paste shortcuts, close-buffer vs stop-editor distinction + +--- + +## Gaps — Areas Without Developer Documentation + +- **Console mode** — no developer-facing doc in legacy `doc/`. Covered by `doc/development/internals/console.md`. +- **Drawing system** — pen-and-paper vs real-time draw modes. Covered by `doc/development/drawing_system.md`. +- **Project environments** — the three Lua environments (`main_env`, `base_env`, `project_env`). Covered by `internals/console.md`. +- **`user_input` overlay API** — `input_text()`, `validated_input()`, `user_input()` patterns. Covered by `internals/console.md`. +- **Editor modes** — reorder (`Ctrl+M`) and search (`Ctrl+F`) modes not documented in `doc/`. +- **Harmony testing mode** — mentioned in passing in README, no developer doc. +- **`doc/development/editor/buffer.md`** — file exists but is empty. diff --git a/doc/development/drawing_system.md b/doc/development/drawing_system.md new file mode 100644 index 00000000..4f04465d --- /dev/null +++ b/doc/development/drawing_system.md @@ -0,0 +1,73 @@ +# Drawing System + +## Two Modes + +User projects can draw in one of two ways, selected by whether `love.draw` is overridden. + +### Pen-and-paper mode (default) + +Project code calls `gfx.*` primitives imperatively — in response to events (clicks, input) or at startup. There is no per-frame redraw loop. The framework composites everything on each frame regardless, but the virtual canvas only changes when the project explicitly draws to it, so unchanged content persists for free. + +**Example:** `src/examples/sapper` — all drawing happens in click handlers (`drawCellLocked`, `drawCellFlagged`, etc.). The board is drawn once at setup and partially updated on each user action. + +### Real-time draw mode + +Project sets `love.draw` directly. The framework detects this on the next `update()` tick, wraps the user draw in `gfx.push/pop` + error handling, and appends the UI overlay (user input widget if active). The framework console UI is bypassed. + +**Example:** `src/examples/balloons` — sets `love.draw = hooks["draw"]` and `love.update = hook("update")` in `game_init()`. + +--- + +## How It Works + +### Virtual canvas + +`CanvasModel` holds a `love.Canvas` (`model.output.canvas`) that serves as the drawing surface for all user project code. It is composited over the terminal by `CanvasView:draw()` during the framework's render pass (`src/view/canvas/canvasView.lua`). + +### `use_canvas(f)` — `src/controller/consoleController.lua:1159` + +```lua +function ConsoleController:use_canvas(f) + gfx.setCanvas({ canvas, stencil = true }) + local r = f() + gfx.setCanvas() + return r +end +``` + +All user event handlers are wrapped in `wrap_handler` (`consoleController.lua:202`), which calls `use_canvas` internally. User `love.update` is also run inside `use_canvas` (see `controller.lua`, `set_love_update`). This means any `gfx.*` calls in event handlers or update automatically go to the virtual canvas, not the screen. + +### User `love.draw` detection — `src/controller/controller.lua`, `set_love_update` + +On each frame's `update()`, the framework compares `love.draw` against its last known value (`View.prev_draw`). If they differ, it replaces `love.draw` with a wrapper: + +```lua +local draw = function() + gfx.push('all') + wrap(ldr, CC) -- user draw, error-handled + gfx.pop() + -- append UI overlay if user input widget is active + local ui = get_user_input() + if ui then ui.V:draw() end +end +love.draw = draw +View.prev_draw = draw +``` + +The framework console UI is not drawn in this path; only the user draw and the optional UI overlay. + +### Framework's default `love.draw` — `src/view/view.lua`, `src/controller/controller.lua:set_love_draw` + +Calls `View.draw(CC, CV)` which renders: background → terminal → virtual canvas, in blend-mode layers. The virtual canvas is drawn with `gfx.draw(canvas)` as a single texture blit per frame. + +--- + +## Summary + +| | Pen-and-paper | Real-time | +|---|---|---| +| Draw trigger | Event / explicit call | Every frame (`love.draw`) | +| Draws to | Virtual canvas (via `use_canvas`) | Screen directly (framework wraps it) | +| Framework UI | Composited on top | Bypassed; UI overlay appended separately | +| GC / CPU cost | Low — canvas persists | Per-frame cost, project's responsibility | +| Suitable for | Board games, static visuals | Animations, physics, continuous updates | diff --git a/doc/development/internals/console.md b/doc/development/internals/console.md new file mode 100644 index 00000000..dacae487 --- /dev/null +++ b/doc/development/internals/console.md @@ -0,0 +1,152 @@ +# Console Mode — Implementation Overview + +Console mode is the default application state — active when no project is running and the editor is not open. It presents the user with a REPL-like interface: type Lua, press Enter, see results. It is also the lifecycle manager for projects: opening, running, suspending, stopping, and closing them. + +--- + +## App State Machine + +`love.state.app_state` drives what the application does on each frame: + +| State | Meaning | +|---|---| +| `ready` | No project open, console REPL active | +| `project_open` | Project opened but not running | +| `running` | Project's main.lua is executing (handlers set) | +| `snapshot` | Transition: screenshot requested, about to suspend | +| `inspect` | Project suspended (paused); console REPL active over frozen project state | +| `editor` | Editor is open | +| `shutdown` | Quitting | + +Key transitions not obvious from the state table: + +| Transition | Trigger | +|---|---| +| `running → editor` | Ctrl+T (quickswitch) while running | +| `editor → running` | Ctrl+T while in editor (normal mode) | +| `editor → inspect` | `finish_edit()` when project was running before edit was opened | +| `inspect → editor` | `edit()` called while in inspect | + +The full machine is diagrammed in `doc/mermaid/fsm.md` and `doc/mermaid/fsm_f.md`. + +The `snapshot` state is a one-frame transition: on the next `update()` tick, LÖVE2D captures a screenshot, stores it as `View.snapshot`, and then calls `ConsoleController:suspend()` which switches to `inspect`. The snapshot is drawn as a background behind the console UI during inspect, giving the user a "freeze frame" view of where the project stopped. + +--- + +## Three Environments + +This is the most non-obvious architectural decision in the console. There are three Lua environments (tables used as `fenv`): + +**`main_env`** — the live console environment. Code typed into the REPL runs here. Contains the full API: project management commands, file I/O, `compy`, `gfx`, etc. Mutated by user REPL commands. + +**`base_env`** — a frozen snapshot of `project_env` at project-open time. Protected with `table.protect()` (writes are blocked). Serves as the clean baseline that `project_env` is reset to when a project is stopped without closing. + +**`project_env`** — the mutable environment in which a project's code runs. Started as a clone of `base_env`, then the project's own `require`/`dofile` calls and global assignments accumulate here. Reset to `base_env` on `_reset_executor_env()`. + +The reason for `base_env` vs `project_env`: restarting a project (`Ctrl+Alt+R`) should give it a clean global table, but reopening the project should not — the distinction allows "reset project state" without "close and reopen". + +Sources: `prepare_env` and `prepare_project_env` (consoleController.lua:341 and 482), `_set_base_env`, `_reset_executor_env`. + +--- + +## Console Input Evaluation + +When Enter is pressed in the REPL (`ConsoleController:keypressed`, line 1009): + +1. `evaluate_input()` is called +2. The input text is validated via the `Evaluator` (LuaEval — parse via metalua) +3. If parse succeeds: `codeload(code, run_env)` compiles the string into a chunk with the appropriate `fenv` (console env normally; project env during `inspect`) +4. `run_user_code(f, self)` executes the chunk inside `use_canvas` — so any `gfx.*` calls go to the virtual canvas +5. Output from `print()` goes to the terminal via the `redirect_to` mechanism (set up in `src/main.lua`) +6. On success: input is cleared. On error: error is set on the input model and displayed in the status line. + +During `inspect` state, the REPL runs code in `project_env`, allowing the user to inspect and mutate the paused project's state. + +--- + +## Project Lifecycle + +### Opening a project + +`open_project(name)` (consoleController.lua:782): +- Closes any currently open project first +- Calls `ProjectService:opreate(name)` which opens an existing project or creates a new one with example code +- Registers a custom `package.loader` that loads modules from the project directory (prepended to `package.loaders` so it takes priority) +- Sets state to `project_open` + +The custom loader is stored in `self.loaders[name]` so it can be removed on close. + +### Running a project + +`run_project(name)` (line 230): +- Calls `ProjectService:run()` which loads `main.lua` via the project's filesystem mount +- Sets state to `running` +- Calls `run_user_code(f, cc, path)` which: executes the chunk in `use_canvas`, then calls `set_user_handlers(env['love'], cc)` to detect and register any `love.*` event handlers the project defined +- If the project defines no blocking handlers (`love.draw`, `love.update`), state immediately returns to `project_open` + +### Stopping vs suspending vs quitting + +**`stop_project_run()`**: Clears user handlers, calls `evacuate_required()` to remove project modules from `package.loaded`, restores default draw, resets overlay input. State → `project_open`. The project's global state persists in `project_env` (can be inspected at the REPL). + +**`suspend_run(msg)`**: Requests a snapshot. State → `snapshot` → `inspect` on next tick. Handlers saved; default handlers restored temporarily. User can inspect and continue. + +**`continue()`**: Restores project handlers from the saved copy. State → `running`. + +**`quit_project()`**: Calls `stop_project_run()` + `close_project()` + resets terminal and input. The environment is wiped. + +### Module isolation + +`evacuate_required()` (line 844) removes all project `.lua` filenames from `package.loaded` when stopping. This ensures that `require('helpers')` in the project will reload fresh on the next run, rather than returning the cached module from the previous run. Without this, mutated module state would leak between runs. + +--- + +## The `user_input` Overlay + +Projects can request live text input mid-run. This creates a second `UserInputModel` overlaid on top of the console. Three API variants: + +```lua +r = user_input() -- bare reference, project polls r:is_empty() / r() +r = input_text("prompt") -- text evaluator +r = input_code("prompt") -- Lua evaluator (syntax-highlighted) +r = validated_input({validators}, "prompt") -- custom validation +``` + +Implementation: calling any of these creates a new `UserInputModel` + `UserInputController` + `UserInputView` and stores the triplet in `love.state.user_input`. The event dispatch in `love.handlers.keypressed` checks this: if set, key events go to the overlay controller, not the main console input. + +The return handle `r` is a `reftable` — a callable table used as a mutable reference. Call it with a value to store (`ref(val)`), call it without arguments to retrieve (`ref()`). `r:is_empty()` returns true until a value is stored. Once the user submits, `r()` returns the value and `r:is_empty()` becomes true again (oneshot semantics). `write_to_input(content)` lets the project pre-fill the input. + +The same `reftable` pattern appears in the test infrastructure (`tests/testutil.lua:get_save_function`) as a general-purpose mutable reference. + +This is the standard pattern for interactive games that need live input (guess, turtle, tixy, repl). Only one overlay can exist at a time (`if love.state.user_input then return end`). + +Source: `prepare_project_env` (line 555–611), `love.state.user_input` handling in `controller.lua` handlers. + +--- + +## The `compy` Namespace + +Projects receive a `compy` table with: + +| Field | Contents | +|---|---| +| `compy.audio` | Sound effects (`sfx.beep()`, `sfx.gameover()`, etc.) — `src/util/audio.lua` | +| `compy.terminal` | VT-100 terminal control (`gotoxy`, `clear`, `show_cursor`, `hide_cursor`) | +| `compy.graphics` | Extended graphics (`shape2d`) — `src/util/graphics/shape2d.lua` | +| `compy.fonts` | Font path constants (`mono`, `sans`, `serif`, etc.) — `src/util/namespace/fonts.lua` | +| `compy.text_input` | Alias for `input_text` (project env only) | + +`compy.terminal` wraps the VT-100 terminal instance directly (the same terminal that renders the console output). Projects can use it for text-mode output within their run context. + +--- + +## Key Files + +| File | Role | +|---|---| +| `src/controller/consoleController.lua` | Everything: env setup, project lifecycle, REPL eval, overlay input API | +| `src/model/consoleModel.lua` | Model aggregate (input, editor, output canvas, project service) | +| `src/model/project/project.lua` | `ProjectService` and `Project` — open/run/close, file I/O, filesystem mount | +| `src/model/io/redirect.lua` | Redirects `print()` output to the terminal | +| `src/view/consoleView.lua` | Top-level view compositor | +| `src/util/audio.lua` | `compy.audio` sound effect library | +| `src/util/namespace/fonts.lua` | `compy.fonts` font path constants | diff --git a/doc/development/internals/editor.md b/doc/development/internals/editor.md new file mode 100644 index 00000000..b2696849 --- /dev/null +++ b/doc/development/internals/editor.md @@ -0,0 +1,185 @@ +# Editor — Implementation Overview + +## The Core Concept: Block-Centric Editing + +The editor does not operate on lines. For Lua files, it operates on **blocks** — top-level syntactic units produced by the metalua chunker. A block is either a `Chunk` (one or more source lines forming a complete top-level statement/expression) or an `Empty` (a blank separator line). The selection highlight, navigation, submit, delete, insert, and move operations all act on blocks, not on individual lines. + +For plain text and Markdown files, the model falls back to line-level editing — each line is its own "block". + +This distinction pervades the entire editor stack. Understanding it is the prerequisite for understanding anything else here. + +--- + +## Content Types and Buffer Initialization + +`EditorController:open()` (`src/controller/editorController.lua:48`) determines content type from the file extension and wires up the right tools: + +| Extension | Content type | Tools | +|---|---|---| +| `.lua` | `lua` | metalua chunker, pretty-printer, truncer, highlighter | +| `.md` | `md` | markdown highlighter only | +| anything else | `plain` | none | + +A `BufferModel` (`src/model/editor/bufferModel.lua`) is created with these tools and immediately tries to chunk the file content. **If the initial Lua parse fails, the buffer is marked `readonly = true`** and the selection is set to 1. The file is viewable but nothing can be edited or saved — this prevents corrupting a file that the editor cannot parse. + +After construction, `lateinit` calls `analyze()` immediately to populate semantic info. + +--- + +## The Input Widget's Role + +The editor does not have its own text entry area — it reuses `UserInputModel` / `UserInputController` / `UserInputView`, the same widget as the console REPL. The input strip at the bottom is how you type content to put into the buffer. + +The workflow is: +1. Navigate the buffer selection to the block of interest +2. Press `Esc` to **load** the selected block's text into the input +3. Edit in the input +4. Press `Enter` to **submit** the input back, replacing the selected block + +`Shift+Esc` inserts the block text into the input at cursor rather than replacing it (additive load). + +The `LuaEditorEval` evaluator (`src/model/interpreter/eval/evaluator.lua:168`) is set on the editor input for Lua files. It adds a 64-character line length validator — the same limit as the code conventions — enforced live as you type. + +**`input_max` vs `LINES`** — two separate height limits. `input_max = 14` is the input strip height, used as the `VisibleContent` `size_max` in `UserInputModel` and to calculate the physical pixel height of the input widget. `LINES = 16` is the buffer viewport height. `input_max = 14` was deliberately chosen to match the code convention (function body ≤ 14 lines): a conforming block fills the input view exactly, with no scrolling needed. + +The monster block fix (`2ff95f03`) uses `bufv:get_max_size()` — which returns `LINES = 16` — as the submission rejection threshold. This means the editor currently accepts blocks of up to 16 lines, which is inconsistent with the convention and with `input_max`: a 15- or 16-line block passes the oversize check but violates the style limit and cannot be fully seen in the input strip without scrolling. The correct threshold would be `input_max` (14). `BufferView` has access to `self.cfg` so `get_max_size()` could return `self.cfg.input_max` instead of `self.LINES`. + +--- + +## The `loaded` Tracker — Why It Exists + +`BufferModel.loaded` records which block index was most recently loaded into the input. This prevents a specific foot-gun: if you load block 3, then navigate the selection to block 7 (the highlight moves), then press Enter, you don't want to overwrite block 7 with what you loaded from block 3. + +The submit handler checks `buf:loaded_is_sel(true)`. If the currently selected block is not the one that was loaded, the submit instead re-selects the loaded block and scrolls to it, giving you a chance to confirm. Only if selection and loaded agree does the replace proceed. + +This is the most important non-obvious invariant in the editor interaction model. + +Source: `BufferModel:set_loaded`, `loaded_is_sel`, `select_loaded` (bufferModel.lua:516–545); the check in `_normal_mode_keys` → `submit` → `replace` (editorController.lua:647–669). + +--- + +## The Submit Pipeline (Lua Mode) + +Pressing `Enter` on non-empty input goes through `_handle_submit` (editorController.lua:321), which is more involved than it looks: + +1. **Pretty-print** — the raw input text is passed through `parser.ast_to_src` (metalua's printer). The displayed result may differ from what was typed (spacing, indentation, end-placement). If pprint fails, the original is used as fallback. `ast_to_src` behaviour is covered by `tests/interpreter/analyzer_spec.lua`, which is its primary specification. +2. **Re-chunk** — the pretty-printed result is chunked again to get the actual block structure that will be stored. +3. **Empty preservation** — if the raw input had a leading or trailing `Empty` block but the pretty-printed version does not, those empties are restored. This preserves the user's intentional blank-line spacing. +4. **Empty injection** — if two consecutive non-empty chunks emerged from the pretty-print, an `Empty` is inserted between them. Enforces the "at most one blank line" format rule at the model level. +5. **Oversize check** — if any resulting block exceeds the size limit (currently `bufv:get_max_size()` = `LINES` = 16, though the intended limit is `input_max` = 14 — see Monster Blocks section), the submit is rejected. The cursor moves to line 1 of the offending block; no error message. +6. **Replace or Insert** — `replace_content` or `insert_content` updates the buffer, adjusting all subsequent block positions via `Range:translate`. +7. **Auto-save** — `buf:save()` is called immediately. Every accepted submit writes to disk. + +`Ctrl+Enter` inserts the new block(s) before the selection rather than replacing it. + +--- + +## Monster Blocks + +A block with more source lines than the editor's size limit is a **monster block**. These can exist in files written outside the editor or imported. The editor handles them without data loss: + +- **Viewing** — monster blocks are displayed in the buffer normally; content beyond the visible window is scrolled. +- **Loading** — pressing Escape loads the full block into the input model, even though only `input_max` (14) lines are visible in the input strip at once. The rest is reachable by scrolling the input. +- **Visibility tolerance** — the submit handler uses `bufv:is_selection_visible(true)` (the oversize-tolerant variant) when checking whether to proceed. A monster block whose start line is at the top of the visible range is considered "visible enough" to edit, even though it extends beyond the bottom. +- **Submitting** — the submitted content is chunked. If all resulting chunks are within the size limit, they replace the monster block (effectively splitting it). If any chunk is still oversized, that chunk is rejected and the cursor moves to its first line (`reject_oversized`). + +The practical editing pattern: load the monster block, edit it into valid code that chunks into conforming-size pieces, submit. Multiple resulting chunks each become their own block in the buffer, with empty separators injected between consecutive non-empty ones (step 4 of the submit pipeline). + +**Note on the current size limit:** the oversize check calls `bufv:get_max_size()` which returns `LINES = 16` (buffer viewport height). The intended limit is `input_max = 14` (input view height, matching the code convention). This means the editor currently accepts blocks of 15–16 lines that violate the convention and require input scrolling to view fully. See the `input_max` vs `LINES` note in the Input Widget section above. + +Source: commit `2ff95f03b8f8` ("Split monster block"); logic in `editorController.lua:_handle_submit` and `bufferView.lua:is_selection_visible`. + +--- + +## Block Position Tracking + +Each `Chunk` carries a `pos: Range` recording which source lines it occupies (e.g. `{3-7}` means lines 3 through 7). When blocks are inserted, deleted, or replaced, all subsequent blocks have their `pos` adjusted by the line delta. This is done manually in `replace_content` and `insert_content` — there is no automatic re-sync; it is purely arithmetic on the stored ranges. + +This matters because the revmap (source line → block index), used for semantic info, is rebuilt only on `analyze()`, which runs on `buf:save()`. Between saves, the revmap may be stale. In practice this is fine because semantic info is only used at search time, which always follows a save in the normal workflow. + +--- + +## The Two Visible Content Types + +The view layer has two parallel implementations for the visible slice of a buffer: + +**`VisibleContent`** (`src/view/editor/visibleContent.lua`) — used for plain/md files. Wraps the raw line array at the configured character width, manages a scroll offset/range over the resulting wrapped lines. Straightforward. + +**`VisibleStructuredContent`** (`src/view/editor/visibleStructuredContent.lua`) — used for Lua. Must handle blocks, where each block wraps independently. Each block becomes a `VisibleBlock` (`src/view/editor/visibleBlock.lua`) that holds its own `WrappedText` and pre-maps syntax highlighting through wrap breaks. + +Text in the editor passes through three coordinate spaces (documented with a worked example in `doc/development/editor/visible.md`): + +1. **Normal coords** — original source line numbers, as stored in the file +2. **Wrapped coords** — apparent lines after word-wrapping at screen width; a single long source line may produce multiple wrapped lines +3. **Visible coords** — the subset of wrapped lines currently in the scroll window + +For Lua buffers, each block has **two position fields** tracking the first two spaces: +- `pos` — the block's position in source lines (normal coords) +- `app_pos` — the block's position in wrapped/apparent lines (what the view uses for rendering and scroll) + +These diverge whenever any block has lines longer than the screen width. Scrolling, selection visibility, and line number display all operate in apparent-line space; source queries (semantic info, revmap) operate in source-line space. The mapping between them is maintained in `VisibleStructuredContent.reverse_map` and recalculated in `recalc_range()`. + +The `WrappedText` base class (`src/util/wrapped_text.lua`) provides the three-table mapping structure: `wrap_forward` (source line → list of display lines), `wrap_reverse` (display line → source line), `wrap_rank` (display line → its offset within the wrap break sequence). These are used for cursor coordinate translation in the input and for highlight remapping in `VisibleBlock`. + +--- + +## Editor Modes + +The editor has three modes managed in `EditorController.mode`: + +**`edit`** (default) — normal editing. Navigation, load, submit, delete all active. + +**`reorder`** (Ctrl+M) — block move mode. The current selection is saved as `state.moved`. Navigation moves the selection (the visual highlight) while the "picked up" block index stays in `state.moved`. Enter confirms the move: `buf:move(moved, target)` physically reorders the block in the dequeue, rechunks, and saves. Escape cancels and restores state. + +**`search`** (Ctrl+F) — definition search. The buffer's `semantic.definitions` (all assignments found by the analyzer) are loaded into `Search`. As you type, `Search:narrow()` filters the list with a case-insensitive substring match (lowercase input = case-insensitive, mixed = case-sensitive). Enter jumps to the selected definition's block and scrolls to its first line. + +Transitions are one-directional: from any special mode, only a return to `edit` is allowed (no reorder→search transitions). This is enforced in `set_mode`. + +--- + +## Semantic Analysis + +`BufferModel:analyze()` runs on every `save()`. It: +1. Rechunks the current text (to get a fresh AST) +2. Runs `analyzer.analyze(ast)` which walks the AST and collects assignments (global/local/function/method/field) and `require()` calls with their line numbers (`src/model/lang/lua/analyze.lua`) +3. Builds `revmap` (source line → block index) from block positions +4. Converts the flat `SemanticInfo` into `BufferSemanticInfo` which replaces line numbers with block indices + +This gives the editor two capabilities: +- **Search** (Ctrl+F): filter and jump to definitions by name +- **Follow require** (Ctrl+O): if the selected block contains a `require('foo')` call, open `foo.lua` as a new buffer on top of the stack + +`analyze()` is wrapped in `pcall` — analysis failure is silent and leaves `self.semantic = nil`, disabling search and follow-require for that buffer. + +--- + +## Buffer Stack and File Navigation + +`EditorModel.buffers` is a `Dequeue` used as a stack (front = active). `EditorView.buffers` is a parallel table keyed by buffer ID (the Lua table address as a string — not the filename). The two stay in sync manually. + +Opening a file (`EditorController:open`) pushes a new buffer to the front and tells the view to create a new `BufferView` for it. Ctrl+S (`close_buffer`) pops the front buffer and activates the next one. If only one buffer remains, Ctrl+S closes the editor entirely and returns to the console. + +Ctrl+O (`follow_require`) calls `self.console:edit(name)` which triggers a full new `open` call, pushing another buffer. This creates a chain: file A → file B → file C, navigable back with Ctrl+S. + +The buffer ID ensures the view can retrieve the right `BufferView` even after the model stack changes order. `EditorView.buffers` is never pruned during a session — views for closed buffers remain in the table but are unreachable via the controller once the model buffer is popped. + +--- + +## Key Files + +| File | Role | +|---|---| +| `src/controller/editorController.lua` | All input handling, mode transitions, submit pipeline | +| `src/model/editor/bufferModel.lua` | Buffer state, block operations, loaded tracker, save, analyze | +| `src/model/editor/content.lua` | `Chunk` and `Empty` block types | +| `src/model/editor/editorModel.lua` | Model aggregate (input, buffers, search) | +| `src/model/editor/bufferSemanticInfo.lua` | Converts SemanticInfo line numbers to block indices | +| `src/model/editor/searchModel.lua` | Search state, narrowing, scroll | +| `src/controller/searchController.lua` | Search keyboard handling | +| `src/view/editor/bufferView.lua` | Scroll, selection visibility, draw orchestration | +| `src/view/editor/visibleContent.lua` | Scroll/wrap for plain/md | +| `src/view/editor/visibleStructuredContent.lua` | Scroll/wrap for Lua blocks | +| `src/view/editor/visibleBlock.lua` | Per-block wrap + highlight remapping | +| `src/util/wrapped_text.lua` | Core wrap tables (forward/reverse/rank) | +| `src/model/lang/lua/analyze.lua` | AST walker producing SemanticInfo | +| `src/model/interpreter/eval/evaluator.lua` | Evaluator types including LuaEditorEval | diff --git a/doc/development/internals/examples/balloons.md b/doc/development/internals/examples/balloons.md new file mode 100644 index 00000000..7671ba00 --- /dev/null +++ b/doc/development/internals/examples/balloons.md @@ -0,0 +1,29 @@ +# balloons + +**Real-time typing game.** Balloons carrying words fall from the top of the screen; the player types the words to pop them before they reach the bottom. + +## Architecture + +Multi-file project (`config`, `challenges`, `stats`, `ui`, `helpers`). `main.lua` is thin orchestration — it wires together `love.draw` and `love.update` via hook tables. + +```lua +hooks.draw = game_state_router(ui_renderers) +love.draw = hooks["draw"] +love.update = hook("update") +``` + +The state router pattern (`game_state_router`) dispatches draw/update calls to different handlers depending on `game_state` (`"loaded"`, `"active"`, `"finished"`). This avoids conditionals inside the per-frame functions themselves. + +## Input + +Uses `user_input()` overlay. On each `update()`, if no pending input, `ui_read_input(input_handler)` is called which calls `input_text()` to show the overlay. When the user submits, the result is routed to `game_validate_input` which checks it against the active challenge. + +## Points of attention + +- `game_state` doubles as both game FSM state and the dispatch key for `on_tick` / `on_input` maps. Adding a new game phase requires entries in both maps. +- The `hooks.update` function is a closure that holds both the state updater and the input reader — be careful if extracting these. +- `sfx.gameover()` / `sfx.correct()` etc. come from `compy.audio`. + +## Files + +`src/examples/balloons/` — main.lua, config.lua, challenges.lua, stats.lua, ui.lua, helpers.lua, debugfunc.lua, colors.lua, parameters.lua, tasks.lua, terminal.lua diff --git a/doc/development/internals/examples/clock.md b/doc/development/internals/examples/clock.md new file mode 100644 index 00000000..64c99f13 --- /dev/null +++ b/doc/development/internals/examples/clock.md @@ -0,0 +1,18 @@ +# clock + +**Animated digital clock** with randomly picked foreground and background colors. Uses a LÖVE stencil for a decorative moving-circle mask effect. + +## Architecture + +Single-file. Sets `love.draw` and `love.update` for real-time rendering. The clock increments `t` in `update()` from actual wall-clock seconds, so it stays in sync with real time even if the frame rate varies. `setTime()` re-syncs `t` from `os.date()` (called at init and on `shift+r`). + +## Notable patterns + +- Uses `compy.fonts.sans` via `gfx.newFont(compy.fonts.sans, 172)` — demonstrates the `compy.fonts` namespace. +- Color cycling via `Color[c]` (the framework's terminal color table, 1–7 normal + 8–14 bright). `space` cycles foreground, `shift+space` cycles background. +- `pause("STOP THE CLOCKS!")` on `p` key — demonstrates the project suspend API. +- The stencil block at the top (`love.graphics.stencil(...)`) is decorative and somewhat experimental — it creates a cutout effect with a bouncing circle. The stencil is applied but the main rendering doesn't use `stenciltest`, so the visual effect is subtle. + +## Files + +`src/examples/clock/main.lua` diff --git a/doc/development/internals/examples/guess.md b/doc/development/internals/examples/guess.md new file mode 100644 index 00000000..06f36ff3 --- /dev/null +++ b/doc/development/internals/examples/guess.md @@ -0,0 +1,34 @@ +# guess + +**Number guessing game** with per-character input validation. + +## Architecture + +Single-file. Uses pen-and-paper mode with `love.update` polling the `user_input` handle. No `love.draw` override — output goes to the terminal via `print()`. + +## Input pattern + +```lua +r = user_input() + +function love.update() + if r:is_empty() then + validated_input({ is_natural }, "Guess a number:") + else + local n = tonumber(r()) + check(n) + end +end +``` + +This is the canonical `validated_input` pattern: poll `r:is_empty()`, show the input overlay when nothing is pending, consume and process when something arrives. The `validated_input` call only fires if there's currently no active overlay (the framework guards against double-activation). + +## Validator + +`is_natural` validates that the input is a string of digit characters using `string.forall(digits, Char.is_digit)`. Returns an `Error` object with a column position on failure — the framework uses this to highlight the offending character in the input widget. + +Note: `is_natural` is defined twice in the file (the second definition overwrites the first). The second version uses `Char.is_digit` from the framework's string utilities; the first used `tonumber`. This is organic evolution — the first definition is dead code. + +## Files + +`src/examples/guess/main.lua` diff --git a/doc/development/internals/examples/index.md b/doc/development/internals/examples/index.md new file mode 100644 index 00000000..e38bb66a --- /dev/null +++ b/doc/development/internals/examples/index.md @@ -0,0 +1,20 @@ +# Examples — Index + +All examples live under `src/examples/`. Each is a self-contained project with a `main.lua`. They demonstrate different combinations of framework capabilities. + +Detailed docs for each are in this directory. + +| Example | One-line description | Draw mode | Input mode | +|---|---|---|---| +| [balloons](balloons.md) | Real-time typing game — pop balloons before they fill the screen | real-time `love.draw` | `user_input()` overlay | +| [clock](clock.md) | Animated digital clock with randomised color cycling | real-time `love.draw` + `love.update` | keyboard (`love.keyreleased`) | +| [guess](guess.md) | Number guessing game with per-character validation | pen-and-paper (`love.update` polling) | `validated_input()` overlay | +| [life](life.md) | Conway's Game of Life with mouse and keyboard controls | real-time `love.draw` + `love.update` | `love.keypressed`, `love.mousepressed` | +| [paint](paint.md) | Pixel paint app with palette, brush/eraser, and line weight | real-time `love.draw`, draws to own canvas | `compy.singleclick`, `love.mousemoved`, `love.keypressed` | +| [pong](pong.md) | Full Pong game with AI opponent and fixed-timestep physics | real-time `love.draw` + `love.update` | keyboard + mouse; selectable AI strategy | +| [repl](repl.md) | Minimal REPL — echoes whatever text the user types | pen-and-paper (`love.update` polling) | `input_text()` overlay | +| [sapper](sapper.md) | Minesweeper (pen-and-paper) with click-driven board | pen-and-paper | `compy.singleclick`, `compy.doubleclick`, `love.mousepressed` | +| [sine](sine.md) | One-shot sine wave plot drawn at init, no update loop | pen-and-paper (init only) | none | +| [tixy](tixy.md) | Live-coded pixel grid — edit the tixy formula in-app | real-time `love.draw` + `love.update` | `input_code()` overlay (live Lua) | +| [turtle](turtle.md) | Turtle-graphics interpreter driven by typed commands | real-time `love.draw`, draws trails | `input_text()` + `love.keypressed` | +| [valid](valid.md) | Validator showcase — collects user input with custom rules | pen-and-paper (`love.update` polling) | `validated_input()` overlay | diff --git a/doc/development/internals/examples/life.md b/doc/development/internals/examples/life.md new file mode 100644 index 00000000..7bfc748f --- /dev/null +++ b/doc/development/internals/examples/life.md @@ -0,0 +1,25 @@ +# life + +**Conway's Game of Life.** Full real-time simulation with keyboard and mouse controls, speed adjustment, and random initialization. + +## Architecture + +Single-file. Overrides `love.draw`, `love.update`, `love.keypressed`, `love.mousepressed`, `love.mousereleased`. Classic real-time draw mode. + +## Grid + +`grid[x][y]` — 2D table, 1-indexed, sized to fill the screen in 10×10 pixel cells. Each cell is 0 or 1. On each `tick()` (when accumulated time exceeds `1/speed`), a new grid is computed from neighbor counts and replaces the old one. + +## Speed control + +Two mechanisms: +- Keyboard `+`/`-` increment/decrement `speed` (1–99) +- Mouse gesture: `mousereleased` checks `hold_time` (how long the button was held) and `dy` (drag distance). Long press → reinitialize. Short drag up/down → change speed by the drag amount. + +## Memory note + +`updateGrid()` allocates a fresh `newGrid` table every tick. This is the main GC pressure point in the example. Acceptable at tick rates of ~10/s but worth noting as a teaching point about the tradeoff between clarity and allocation. + +## Files + +`src/examples/life/main.lua` diff --git a/doc/development/internals/examples/paint.md b/doc/development/internals/examples/paint.md new file mode 100644 index 00000000..d21fd2b0 --- /dev/null +++ b/doc/development/internals/examples/paint.md @@ -0,0 +1,31 @@ +# paint + +**Pixel paint application** with a color palette, brush/eraser tools, and line weight selector. + +## Architecture + +Single-file. Overrides `love.draw`, `love.mousemoved`, `love.keypressed`. Uses `compy.singleclick` and `compy.doubleclick` (left click = primary color, right click = background color). Real-time draw mode for the UI; drawing to the canvas is immediate (pen-and-paper within a sub-canvas). + +## Canvas strategy + +The paint canvas is a separate `love.Canvas` (`gfx.newCanvas(can_w, can_h)`), distinct from the framework's virtual canvas. Drawing calls use `canvas:renderTo(function() ... end)` — this is LÖVE's API for directing draw calls to a specific canvas. In `love.draw`, this canvas is drawn with `gfx.draw(canvas, box_w)` at an offset to leave room for the toolbox. + +This means the paint buffer is fully owned by the example and is independent of the framework's virtual canvas compositing. + +## Layout + +Screen divided into: left toolbox strip (`box_w`), main canvas, bottom palette row (`pal_h`). Hit-testing via explicit range checks (`inCanvasRange`, `inPaletteRange`, `inToolRange`, `inWeightRange`). No UI library. + +## Tool drawing + +Brush and eraser icons are drawn with `gfx.push()`/`gfx.pop()`, `gfx.translate()`, `gfx.scale()`, `gfx.rotate()`. The brush tip uses `love.math.newBezierCurve` for a smooth flame shape — the most complex drawing code in any example. + +## Points of attention + +- `compy.singleclick` is for discrete color selection / tool selection. `love.mousemoved` handles continuous paint strokes (checks `love.mouse.isDown` to require held button). +- Right-click (`btn == 2`) sets background color in the palette and paints with background color on canvas — the `compy.doubleclick` is mapped here rather than a true double-click. +- The `goose` color (a teal `{0.303, 0.431, 0.431}`) used for the weight selector highlight is a named constant for historical/whimsical reasons. + +## Files + +`src/examples/paint/main.lua` diff --git a/doc/development/internals/examples/pong.md b/doc/development/internals/examples/pong.md new file mode 100644 index 00000000..bbf566aa --- /dev/null +++ b/doc/development/internals/examples/pong.md @@ -0,0 +1,60 @@ +# pong + +**Full Pong game** with two AI difficulty levels, two-player keyboard mode, fixed-timestep physics, and mouse control for the player paddle. + +## Architecture + +Multi-file: `main.lua` (game logic, rendering, input), `strategy.lua` (AI behaviors), `constants.lua` (game parameters). + +Overrides `love.draw`, `love.update`, `love.keypressed`, `love.mousemoved`, `love.resize`. + +## Virtual resolution + +Game logic runs in a 640×480 virtual space. A `love.math.newTransform` (`view_tf`) scales from virtual to screen coordinates. `love.draw` applies the transform with `gfx.applyTransform(view_tf)`, so all draw calls use virtual coordinates internally. `love.resize` rebuilds the transform. This decouples physics from screen resolution. + +## Fixed timestep + +`USE_FIXED = true` enables fixed-timestep update via `update_fixed()`: + +```lua +acc = acc + rdt +while FIXED_DT <= acc and steps < MAX_STEPS do + step_game(FIXED_DT) + acc = acc - FIXED_DT + steps = steps + 1 +end +``` + +`MAX_STEPS = 5` caps the catch-up loop so a single slow frame doesn't spiral. This is the standard fixed-timestep pattern; the example demonstrates it explicitly as a teaching artifact. + +## Strategy pattern + +`S.strategy.fn` holds the active AI function. Three strategies in `strategy.lua`: `easy` (slower tracking), `hard` (full speed), `manual` (second-player keyboard). Swapped at the start screen. The function signature `strategy.fn(S, dt)` receives the full game state — strategies have full read/write access, so the easy/hard split is purely in `move_paddle` call parameters, not state access. + +## State machine + +`S.state` (`"start"`, `"play"`, `"gameover"`) drives `key_actions` dispatch: + +```lua +key_actions = { start = {...}, play = {...}, gameover = {...} } +function love.keypressed(k) + local group = key_actions[S.state] + if group and group[k] then group[k]() end +end +``` + +Clean dispatch without conditionals. Escape → quit is added to all states via a loop. + +## Mouse control + +`love.mouse.setRelativeMode(true)` during play — mouse is captured and `dy` accumulates to move the paddle. This avoids absolute positioning, which is non-trivial with variable-resolution scaling. + +## Points of attention + +- `gfx.newText` objects are cached in `texts` and reused — avoids per-frame text object allocation. +- `build_center_canvas()` draws the center dividing line to a dedicated canvas once; re-drawn only on `love.resize`. Good GC pattern. +- `do_init()` is deferred to the first `update()` call (via `ensure_init()`) because some LÖVE2D state (font, canvas) may not be ready at module load time in all contexts. + +## Files + +`src/examples/pong/` — main.lua, strategy.lua, constants.lua diff --git a/doc/development/internals/examples/repl.md b/doc/development/internals/examples/repl.md new file mode 100644 index 00000000..8f71ee0d --- /dev/null +++ b/doc/development/internals/examples/repl.md @@ -0,0 +1,29 @@ +# repl + +**Minimal REPL.** Accepts text input from the user and echoes it to the terminal. + +## Code + +```lua +r = user_input() + +function love.update() + if r:is_empty() then + input_text() + else + print(r()) + end +end +``` + +## Purpose + +The smallest possible demonstration of the `user_input` / `input_text` polling pattern. No game logic, no drawing, no state. Useful as a reference skeleton for any project that needs live text input in an `update()` loop. + +## Notes + +`input_text()` is called with no arguments — no prompt, no initial text. The overlay appears with an empty input field. On submit, `r()` returns the entered string and the cycle begins again. + +## Files + +`src/examples/repl/main.lua` diff --git a/doc/development/internals/examples/sapper.md b/doc/development/internals/examples/sapper.md new file mode 100644 index 00000000..3e6e9d74 --- /dev/null +++ b/doc/development/internals/examples/sapper.md @@ -0,0 +1,41 @@ +# sapper + +**Minesweeper** implemented entirely in pen-and-paper mode. The board is drawn once at init and updated incrementally on each user action. + +## Architecture + +Single large file (~700 lines). No `love.draw` or `love.update` override. All game logic runs in `compy.singleclick` / `compy.doubleclick` handlers and `love.mousepressed` (for modifier-key variants). The terminal output is never used. + +## Draw model + +Drawing is event-driven: click a cell → the cell's appearance is redrawn in place. The full board is only redrawn on `actionInit()` (new game) and mode changes. Individual cell functions (`drawCellLocked`, `drawCellFlagged`, `drawCellUnlocked`, `drawCellBlown`, `drawCellExposed`) draw directly to the virtual canvas. The status panel is redrawn on every state change via `redrawStatus`. + +This is a clean illustration of pen-and-paper mode: no wasted per-frame work, canvas retains state. + +## Iterator pattern + +Cell traversal uses closure iterators throughout: + +```lua +function all_cells() + local row, col = 1, 0 + return function() + col = col + 1 + if config.cols < col then col = 1; row = row + 1 end + if config.rows < row then return nil end + return col, row + end +end +``` + +`cell_filter(iterator, predicate)` wraps any iterator with a filter. `neighbors(i,j)`, `unlockable_neighbors(i,j)`, `all_mined_cells()` are all composed from these. No intermediate tables — pure iterator chaining. Good functional Lua pattern for students. + +## Click handling + +`compy.singleclick` → flag cell. `compy.doubleclick` → unlock cell (or restart if game over). `love.mousepressed` → same actions when shift/ctrl held (alternative input for touch devices without double-click). + +Mine placement uses probabilistic streaming: iterate all mineable positions once, at each position place a mine with probability `mines_remaining / cells_remaining`. No shuffle needed. + +## Files + +`src/examples/sapper/main.lua` diff --git a/doc/development/internals/examples/sine.md b/doc/development/internals/examples/sine.md new file mode 100644 index 00000000..c8f13683 --- /dev/null +++ b/doc/development/internals/examples/sine.md @@ -0,0 +1,31 @@ +# sine + +**One-shot sine wave plot.** Draws axes and a sine curve at startup using `gfx.points`. No update loop, no input. + +## Code pattern + +```lua +local points = {} +for x = 0, xe do + local v = 2 * math.pi * (x - xh) / xe + local y = yh - math.sin(v * times) * amp + table.insert(points, x) + table.insert(points, y) +end +gfx.points(points) +``` + +Everything runs at the top level of `main.lua` — no function definitions, no handlers. This is the simplest possible example of pen-and-paper mode: code runs once, draws once, is done. + +## Purpose + +Demonstrates: +1. That project code executes at the top level and its output persists on the virtual canvas without any draw loop. +2. `gfx.points` for efficient point cloud rendering. +3. Basic coordinate math for a centered normalized plot. + +`gfx.setColor(1, 0, 0)` and `gfx.setPointSize(2)` affect subsequent draw calls — the axes are drawn first in white with `setLineWidth(1)`, then the point color is changed to red. + +## Files + +`src/examples/sine/main.lua` diff --git a/doc/development/internals/examples/tixy.md b/doc/development/internals/examples/tixy.md new file mode 100644 index 00000000..61ef8bb1 --- /dev/null +++ b/doc/development/internals/examples/tixy.md @@ -0,0 +1,61 @@ +# tixy + +**Live-coded pixel grid** inspired by tixy.land. A 16×16 grid of circles whose radii and colors are driven by a user-editable formula `tixy(t, i, x, y)`. + +## Architecture + +Single-file (with `mathlib.lua` for extra math and `examples.lua` for preset formulas). Overrides `love.draw`, `love.update`, `love.mousepressed`. + +## Live coding mechanism + +The heart of the example: + +```lua +function setupTixy() + local code = "return function(t, i, x, y)\n" .. + " " .. body .. + " return r\n" .. + "end" + local f = loadstring(code) + if f then + setfenv(f, _G) + tixy = f() + end +end +``` + +`body` is a single line of Lua (e.g. `r = math.sin(t + x)`). `loadstring` compiles it wrapped in a function, `setfenv` gives it access to the global environment, and the returned closure becomes `tixy`. This is called whenever the user submits new code. + +## Input pattern + +Uses `input_code()` overlay — the input widget has Lua syntax highlighting and validation. The `r = user_input()` / `input_code(...)` polling pattern is identical to other examples, but the prompt is the function signature and the initial content is the current formula body. + +```lua +r = user_input() +function love.update(dt) + time = time + dt + if r:is_empty() then + input_code("function tixy(t, i, x, y)", string.lines(body)) + else + local ret = r() + body = string.unlines(ret) + setupTixy() + end +end +``` + +`write_to_input(body)` in `load_example()` pre-fills the input with the current formula when loading a preset — the user sees the formula they're about to run before submitting. + +## Example cycling + +`examples.lua` is a table of `{code, legend}` pairs. Left click advances, right click randomizes. `advance()` and `retreat()` call `load_example()` which calls `setupTixy()` and `write_to_input`. + +## Points of attention + +- `setfenv(f, _G)` gives the live code full access to the global environment. This is intentional (educational context, trusted user) but means any global can be clobbered by a formula. +- If `loadstring` fails (syntax error in `body`), `setupTixy` silently leaves the old `tixy` function in place. +- `tonumber(tixy(ts, index, x, y)) or -0.1` — graceful fallback if the formula returns nil or non-numeric. + +## Files + +`src/examples/tixy/` — main.lua, mathlib.lua, examples.lua diff --git a/doc/development/internals/examples/turtle.md b/doc/development/internals/examples/turtle.md new file mode 100644 index 00000000..60f86967 --- /dev/null +++ b/doc/development/internals/examples/turtle.md @@ -0,0 +1,40 @@ +# turtle + +**Turtle graphics interpreter** driven by typed commands and keyboard input. + +## Architecture + +Multi-file: `main.lua` (main loop, input wiring), `action.lua` (command table), `drawing.lua` (rendering: background, turtle icon, trail, debug overlay). + +Overrides `love.draw`, `love.update`, `love.keypressed`, `love.keyreleased`. + +## Input pattern + +Dual input: typed commands via `input_text()` overlay, and direct keyboard actions. + +```lua +local r = user_input() + +function love.update() + if not r:is_empty() then + eval(r()) + end +end +``` + +`eval(input)` looks up `actions[input]` and calls the function if found. Actions are defined in `action.lua` as a table mapping strings to closures. Typed input is thus a command dispatcher. + +`love.keyreleased`: `i` key opens the input overlay interactively (`r = input_text("TURTLE")`), overwriting the existing `r` reference. `shift+r` resets turtle position. + +## Drawing + +`drawing.lua` contains `drawBackground()` (clears to dark), `drawTurtle(tx, ty)` (draws a triangle pointing in the turtle's current direction), `drawHelp()`, and `drawDebuginfo()`. The turtle trail is drawn to the virtual canvas via `gfx.*` calls in the action functions — so trails persist across frames without a redraw loop. + +## Points of attention + +- The `r` variable (user_input handle) is reassigned inside `love.keyreleased` (`r = input_text("TURTLE")`). This means after pressing `i`, the old handle is abandoned and a new overlay is created. The polling in `love.update` starts consuming the new handle. This pattern works but relies on the variable being captured by reference in the update closure — which it is, since both closures close over the upvalue `r`. +- `debug_color` is set in `love.update` based on turtle position — this modifies a global used by `drawDebuginfo`. The debug overlay is toggled by `space`. + +## Files + +`src/examples/turtle/` — main.lua, action.lua, drawing.lua diff --git a/doc/development/internals/examples/valid.md b/doc/development/internals/examples/valid.md new file mode 100644 index 00000000..b76ab032 --- /dev/null +++ b/doc/development/internals/examples/valid.md @@ -0,0 +1,39 @@ +# valid + +**Validator showcase.** Collects user input with a combination of custom validators and prints the result. + +## Architecture + +Single-file. Pen-and-paper mode with `love.update` polling. No drawing, no game logic. + +## Validators defined + +| Validator | Rule | +|---|---| +| `min_length(n)` | Input must be longer than n characters | +| `max_length(n)` | Input must be at most n characters | +| `is_upper(s)` | All characters must be uppercase | +| `is_lower(s)` | All characters must be lowercase (uses `string.forall` + `Char.is_lower`) | +| `is_number(s)` | Must be a valid integer (handles leading `-`) | +| `is_natural(s)` | Must be a non-negative integer | + +Each returns `true` on success or `false, Error("message", column)` on failure. The `Error` column is used by the framework to highlight the specific offending character in the input widget. + +## Active validators + +```lua +validated_input({ + min_length(2), + is_lower +}) +``` + +All validators in the list must pass for the input to be accepted. If any fail, the first error's column is highlighted. + +## Purpose + +Demonstrates the `validated_input` API and how to write validator functions with column-accurate error reporting. The validator signatures here are the reference implementation pattern for any project that needs constrained text input. + +## Files + +`src/examples/valid/main.lua` diff --git a/doc/development/internals/user_input.md b/doc/development/internals/user_input.md new file mode 100644 index 00000000..4d2c935a --- /dev/null +++ b/doc/development/internals/user_input.md @@ -0,0 +1,159 @@ +# User Input — Implementation Overview + +Input handling in Compy has two mostly independent layers: **text/keyboard input** (the input widget shared across console, editor, and project overlays) and **mouse/pointer input** (handled partly by the framework, partly delegated to projects). This doc covers both, with mode-specific notes where the behavior differs. + +--- + +## Text Input Widget + +`UserInputModel` / `UserInputController` / `UserInputView` form a shared widget reused in three contexts: the console REPL, the editor input strip, and project-created overlays. The widget is always the same code; what differs is the evaluator attached to it and which controller handles the result. + +### Data flow + +``` +LÖVE2D textinput event + → love.handlers.textinput + → ConsoleController:textinput (dispatches by app_state) + → editor mode: EditorController:textinput + → console/overlay mode: UserInputController:textinput + → UserInputModel:add_text + → updates InputText (cursor-aware string) + → view re-renders +``` + +Text characters arrive via `love.textinput` (OS-processed, handles IME and layout). Raw key events for non-character keys (backspace, enter, arrows) arrive via `love.keypressed`. + +### Multiline input + +The input is not single-line. `Shift+Enter` inserts a newline (`line_feed()`). The cursor model tracks both line and column. Lines are stored as a `Dequeue` in `InputText`. The view wraps long lines at `drawableChars` width (same wrap machinery as the editor's `VisibleContent`). + +The input view height is `input_max = 14` lines. This is a display limit only — the model can hold more lines, and scrolling within the input works normally. In the editor context this becomes relevant when loading a monster block (> 16 lines): all content is in the model, but only 14 lines are visible at once. See `editor.md` — Monster Blocks for the full picture. + +### Selection + +Text selection works across lines. `Shift+arrow` extends selection; releasing shift releases it. `UserInputController.disable_selection` (set to `true` in editor mode's input instance) suppresses selection — the editor uses its own block-level selection model, not character-level selection in the input. + +Mouse click on the input widget (translated from screen coordinates to input grid via `_translate_to_input_grid`) sets the cursor position. Drag extends selection. The coordinate translation is bottom-relative: line 0 is the bottom line of the input, which is non-obvious. + +### Error state + +When an error is set on the model (`set_error()`), the input is visually locked: text input and most keys are ignored until the error is cleared. Cleared by: Enter, space, or arrow keys. This is used for parse errors, runtime errors, and validation failures. + +### Evaluator and validation + +Each `UserInputModel` has an `Evaluator` that runs on submit: +- **Console**: `LuaEval` — metalua parse, validates Lua syntax before accepting the submit +- **Editor input (Lua file)**: `LuaEditorEval` — same as LuaEval but adds 64-char line length validator +- **Project overlays**: `InputEvalText` (plain), `InputEvalLua` (Lua syntax), or a `ValidatedTextEval` with custom validators +- **Search**: `nil` evaluator (search input is free text, no validation) + +Validators run on every character (via `validation_hl` for real-time highlighting) and again on submit. A validator returns `true` or `false, Error(msg, column)`. The column is used to highlight the specific offending character. + +--- + +## Keyboard Handling + +### Dispatch chain + +``` +love.handlers.keypressed + → global shortcuts (pause, quit, restart, editor-toggle) — controller.lua + → ConsoleController:keypressed + → if editor state: EditorController:keypressed + → else: console key handling + → history navigation (PageUp/Down) + → UserInputController:keypressed (cursor, edit, submit) + → Enter → ConsoleController:evaluate_input +``` + +Global shortcuts in `love.handlers.keypressed` (controller.lua:520+) are intercepted before anything reaches the controller: Ctrl+Pause suspends, Ctrl+Q quits project, Ctrl+S stops run or closes buffer, Ctrl+Shift+R resets application, Ctrl+Alt+R restarts project, Ctrl+Esc exits app. + +If `love.state.user_input` is set (overlay active), key events go to the overlay controller, bypassing the main input. + +### Console-specific keys + +- **PageUp/PageDown**: history back/forward +- **Up/Down at input boundary**: also triggers history navigation (handled as a "limit reached" return from `UserInputController:keypressed`) +- **Enter** (no shift): submit → `evaluate_input()` +- **Ctrl+L**: clear terminal output +- **Shift+Enter**: insert newline in input (multiline expression) + +### Editor-specific keys + +See `editor.md` for full detail. Key differences from console mode: +- No history navigation (PageUp/Down scroll the buffer instead) +- Up/Down at input boundary moves buffer selection, not history +- Escape loads selected block text into input +- Ctrl+M / Ctrl+F switch modes + +### UserInputController keypressed (shared) + +`UserInputController:keypressed` handles the low-level input operations regardless of context: removers (backspace, delete, Ctrl+Y delete line), vertical cursor movement, horizontal movement (Left/Right, Home/End, Alt+Home/End for line vs field boundaries), Shift+Enter newline, Ctrl+D duplicate line, copy/cut/paste (Ctrl+C/X/V and Shift+Insert/Delete), selection management. + +The `oneshot` flag on `UserInputModel` (set for project overlays) enables a submit path inside `UserInputController:keypressed` — on Enter, the evaluator runs and the result is sent to the callback. Console submission is handled separately in `ConsoleController:keypressed`, not here. + +--- + +## Mouse Input + +### Framework-level click handling + +The framework implements single/double click detection in `love.handlers.mousereleased` (controller.lua:662+). On each mouse release: +1. `click_count` is incremented, `click_timer` is set to 0.4s +2. On the next `love.update()`, if `click_timer` has expired: + - `click_count == 1`: single click confirmed — calls `compy.singleclick(x, y)` if defined + - `click_count >= 2`: double click — calls `compy.doubleclick(x, y)` if defined +3. A drift tolerance of 2.5px is applied: if the mouse moved more than that between press and release, the click is suppressed + +`compy.singleclick` and `compy.doubleclick` are looked up in `env['compy']` — the project's `compy` table. They are not LÖVE2D events; they are Compy-specific abstractions. Projects define them as `function compy.singleclick(x, y) ... end`. + +The 0.4s delay means single clicks are always confirmed after a short wait — there is no "instant single click" path. This is a deliberate tradeoff for double-click detection consistency. + +### Direct mouse events + +`love.mousepressed`, `love.mousereleased`, `love.mousemoved`, `love.wheelmoved` are also forwarded directly to project-defined handlers via the standard LÖVE2D mechanism (projects set `love.mousepressed = function(...) end`). These go through `wrap_handler` (error catching + canvas routing) if set by the project. + +The framework's own `mousepressed`/`mousereleased` handlers (in `love.handlers`) call the user handler AND the overlay controller if present — both get the event. + +### Input widget mouse + +`UserInputController` handles mouse events on the input widget: +- `mousepressed`: translates screen (x, y) → input grid (col, line) via `_translate_to_input_grid`. Grid is bottom-relative (y increases upward in input space). Calls `im:mouse_click(l, c)` to set cursor. +- `mousemoved`: if left button held, calls `im:mouse_drag(l, c)` to extend selection. +- `mousereleased`: calls `im:mouse_release(l, c)` then releases selection. + +Mouse events on the input widget are only processed when `disable_selection` is false. In editor mode, the input widget's controller has `disable_selection = true` — mouse interaction with the input strip is suppressed, and the editor uses keyboard-only navigation. + +### Touch + +Touch handlers (`touchpressed`, `touchreleased`, `touchmoved`) are stubbed with `-- TODO` in `UserInputController`. The framework's `love.handlers` forwards touch events to project handlers if defined. + +--- + +## The `user_input` Overlay — Input Perspective + +When a project calls `input_text()`, `input_code()`, or `validated_input()`, a new `UserInputModel` + `UserInputController` + `UserInputView` is created and stored in `love.state.user_input`. From this point: + +- **Text input** (`love.handlers.textinput`): goes to `user_input.C:textinput(t)` instead of the main controller +- **Key input** (`love.handlers.keypressed`): goes to `user_input.C:keypressed(k)` +- **The overlay view** (`user_input.V`) is drawn by the framework's `love.update` wrapping of the user draw function (appended after the user draw call) + +The project polls `r:is_empty()` in `love.update`. When the user presses Enter, the evaluator runs, and if it passes, the result is stored in the `reftable` ref. On the next `update()`, `r:is_empty()` returns false, `r()` returns the value and resets to empty. + +Only one overlay can exist at a time. If `love.state.user_input` is already set, subsequent calls to `input_text()` etc. return immediately. + +--- + +## Key Files + +| File | Role | +|---|---| +| `src/controller/userInputController.lua` | Keyboard, mouse, and text event handling for the input widget | +| `src/model/input/userInputModel.lua` | Input state: text, cursor, selection, history, error, evaluator | +| `src/model/input/inputText.lua` | Multiline string with cursor-aware insert/delete | +| `src/model/input/cursor.lua` | Cursor position model | +| `src/model/input/selection.lua` | Selection range model | +| `src/model/input/history.lua` | Command history (console) | +| `src/view/input/userInputView.lua` | Renders the input strip and status line | +| `src/controller/controller.lua` | Global key dispatch, click detection, user handler management | +| `src/controller/consoleController.lua` | Top-level key/text dispatch, overlay creation API | diff --git a/doc/development/overview.md b/doc/development/overview.md new file mode 100644 index 00000000..05028b66 --- /dev/null +++ b/doc/development/overview.md @@ -0,0 +1,131 @@ +# Implementation Overview + +High-level implementation details for Compy. Load this when you need architecture context beyond what CLAUDE.md provides. + +## Hardware and Platform Context + +Compy is a physical 7" portable educational computer running Android, designed as a modern take on the ZX Spectrum-era home computer experience for children. The primary storage is an SD card mounted at `/storage//Documents/compy/` — projects live under `projects/` within that path. On desktop Linux the equivalent path is `~/Documents/compy/`. `src/main.lua:android_storage_find()` handles SD card detection. The intro document `doc/intro.md` describes the intended runtime environment. + +--- + +## Entry Points + +- `src/conf.lua` — `love.conf()`, parses CLI args into `love.start` (`{mode, path?, testflags?}`) +- `src/main.lua` — `love.load()`, sets up storage, instantiates MVC, handles startup modes + +## Application Modes + +| Mode | Invocation | Description | +|---|---|---| +| `ide` | `love src` | Full IDE: console REPL + editor | +| `play` | `love src play ` | Runs a single project; no editor UI | +| `test` | `love src test [flags]` | IDE with optional autotest and draw-test overlays | +| `harmony` | `love src harmony` | Scenario-driven screenshot testing | + +Test flags: `--auto` (run `tests/autotest.lua` on startup), `--size` (smaller terminal), `--draw` (blend-mode grid, implies `--size`). + +--- + +## MVC Structure + +Top-level wiring in `love.load` (`src/main.lua`): +``` +ConsoleModel → ConsoleController → ConsoleView +``` + +### Model (`src/model/`) + +| Path | Purpose | +|---|---| +| `consoleModel.lua` | Top-level aggregate: input, editor, output (canvas), projects | +| `input/` | `UserInputModel`, `InputText`, `Cursor`, `History`, `InputSelection` | +| `editor/` | `EditorModel`, `BufferModel`, `SearchModel`, `VisibleContent`, `SemanticInfo` | +| `interpreter/eval/` | `Evaluator` — validate → parse → transform pipeline for user input | +| `lang/lua/` | Lua parser (via metalua), semantic analyzer, highlighter, error types | +| `lang/md/` | Markdown parser | +| `project/` | `ProjectService` — list/open/run/close projects, file I/O | +| `canvasModel.lua` | Virtual drawing canvas + VT-100 terminal | + +### Controller (`src/controller/`) + +| File | Purpose | +|---|---| +| `controller.lua` | LÖVE2D handler setup; draw/update loop; user handler detection | +| `consoleController.lua` | Main controller; project lifecycle; canvas routing | +| `editorController.lua` | Editor state machine | +| `userInputController.lua` | Input field logic | +| `searchController.lua` | Search mode | + +### View (`src/view/`) + +| Path | Purpose | +|---|---| +| `consoleView.lua` | Top-level view compositor | +| `canvas/` | `CanvasView` (composites background + terminal + user canvas), `TerminalView` | +| `editor/` | `EditorView`, `BufferView`, `VisibleBlock`, search views | +| `input/` | `UserInputView`, statusline, custom status | + +--- + +## OOP Pattern (`src/util/class.lua`) + +Two patterns in use: + +**Constructor pattern** — most common: +```lua +Foo = class.create(function(x) return { x = x } end) +-- with lateinit (called after constructor, for derived state): +Bar = class.create(new_fn, lateinit_fn) +``` + +**`new` method pattern** — when metatable setup must be manual: +```lua +Baz = class.create() +function Baz.new(cfg) + return setmetatable({ ... }, Baz) +end +``` + +Classes are typically globals (e.g. `ConsoleModel`, `UserInputModel`). Instantiation uses call syntax: `Foo(args)`. + +--- + +## Global Namespace + +`src/util/lua.lua` injects helpers into `_G` on load: `prequire`, `codeload`, `noop`, `identity`, `parse_int`, `b2s`. + +Many model/view/controller classes are also set as globals at module load time (not returned via `require`). This is intentional — it supports the file = console equivalence principle. + +--- + +## Input Evaluation Pipeline + +User input flows through `Evaluator` (`src/model/interpreter/eval/evaluator.lua`): + +1. **Line validators** — reject syntactically bad input before it enters the model +2. **Parser** (`src/model/lang/lua/parser.lua` via metalua) — parse to AST +3. **AST validators** — semantic checks on the parsed tree +4. **Transformers** — mutate AST before execution + +--- + +## Drawing System + +See [`drawing_system.md`](drawing_system.md) for the full explanation of virtual canvas, pen-and-paper vs real-time draw modes, and the `love.draw` override detection mechanism. + +--- + +## Key Libraries (`src/lib/`) + +| Library | Role | +|---|---| +| `metalua/` | Lua 5.1 parser (git submodule); used for AST, syntax checking, highlighting | +| `djot/` | Djot markup parser | +| `hump/timer.lua` | Timer utility | +| `error_explorer.lua` | Interactive LÖVE2D error screen (by kira); on unhandled error shows stack, locals, and source. Documented in `doc/development/error_explorer.md`. | + +`src/util/string/` is also a git submodule (string utilities). + +### Dependency graph + +A full module dependency graph (Mermaid flowchart) is maintained in `doc/development/lib_deps.md`, including a utility tier breakdown. Consult it when tracing load-order or dependency questions. diff --git a/doc/development/tests.md b/doc/development/tests.md new file mode 100644 index 00000000..9554e131 --- /dev/null +++ b/doc/development/tests.md @@ -0,0 +1,48 @@ +# Test Suite Review + +Assessment of `tests/` relative to the codebase and the knowledge base under `doc/development/`. Organised into: relevant infrastructure, coverage map, gaps. + +--- + +## Test Infrastructure Worth Knowing + +**`EditorSession`** (`tests/helpers/editor_session.lua`) — A keystroke-driven test harness for the editor. Drives `EditorController` via simulated key events. Key methods: `open(src, nb)` asserts block count on open; `select_block(n)` navigates to block n; `select_and_open_block(n)` selects and presses Escape (asserts `buffer.loaded == n`); `submit(newtext)` alters input and sends Return; `assert_cursor_at(line, col)` checks cursor position and visibility in the input's scroll range. The comment in `select_block` notes it works reliably only before the input multiline buffer is activated (before Escape is pressed) — a known limitation tracked in issue #117. + +**`mock.lua`** — `mock_love(t)` injects a minimal `love` global. `mock.keystroke(keystr, press)` parses strings like `"C-return"`, `"S-escape"`, `"M-up"` (prefixes: `C`=Ctrl, `S`=Shift, `M`=Alt) into modifier state + key call. Primary driver for all editor and input tests. + +**`testutil.lua`** — Shared constants: `wrap=64` (screen width, matching the code convention line limit), `LINES=16` (buffer display height), `SCROLL_BY=8`. `get_save_function(init)` returns a save callback paired with a `reftable` handle — the same callable-table-as-mutable-reference pattern used by `user_input()` handles in project code. + +--- + +## Coverage Map + +### Well covered + +| Area | Spec files | +|---|---| +| Utilities | `util/string_spec`, `table_spec`, `dequeue_spec`, `range_spec`, `tree_spec`, `class_spec`, `wrap_spec`, `termcolor_spec`, `debug_spec`, `fs_spec`, `mock_spec` | +| Interpreter pipeline | `parser_spec`, `chunker_spec`, `analyzer_spec` (incl. `ast_to_src`), `ast_spec`, `eval_spec`, `error_spec`, `markdown_spec` | +| Editor model + view | `buffer_spec`, `chunker_spec`, `editor_spec`, `visible_content_spec`, `visible_structured_content_spec` | +| Input widget | `input_text_spec`, `cursor_spec`, `history_spec`, `input_spec`, `user_input_model_spec`, `user_input_view_spec` | + +`analyzer_spec.lua` tests `parser.ast_to_src` — the pretty-printer central to the editor submit pipeline. It is the primary executable specification for that function's formatting behaviour. + +### Not covered + +| Area | Notes | +|---|---| +| `ConsoleController` | Deeply coupled to LÖVE2D runtime state; exercised by harmony integration tests instead | +| `Controller.lua` | Click detection timer, handler registration, draw override detection | +| `ProjectService` | File I/O, project open/create, filesystem mount | +| `SearchModel` / `SearchController` | No tests | +| Drawing system | Depends on LÖVE2D graphics context | +| Views (rendering) | Expected; view testing requires LÖVE2D | +| `user_input` overlay API | `input_text()` / `validated_input()` / `user_input()` project-facing path | + +--- + +## Gaps — Areas Worth Filling + +**SearchModel** is the one covered subsystem without tests that doesn't require a graphics context. The narrowing logic (`Search:narrow` with case-insensitive substring match) and selection scroll could be unit-tested similarly to `history_spec`. + +**Tag organisation** — `.busted` excludes `delay`-tagged tests by default. Tags in active use: `#parser`, `#chunk`, `#analyzer`, `#ast`, `#src`, `#editor`, `#input`, `#markdown`, `#visible`. No tests currently use the `delay` tag, so the exclude is defensive rather than active. diff --git a/doc/development/wip/77-new-input-api/README.md b/doc/development/wip/77-new-input-api/README.md new file mode 100644 index 00000000..076dafb7 --- /dev/null +++ b/doc/development/wip/77-new-input-api/README.md @@ -0,0 +1,35 @@ +# Feature #77 — New Input API + +**TL;DR:** a callback-based input API for projects, replacing the polling +functions (`input_text`, `input_code`, `validated_input`, `user_input`). It +turned out to touch keyboard routing across the whole app, not just the REPL — +so it got a proper upfront analysis instead of a dive-in. **Complex, but +solvable**, and pre-planned so building can start on a green light. Per +stakeholder feedback (round 1), the legacy text-input functions are **removed** +— no backward compatibility; the examples that used them migrate to the new +API, others can be excluded from the release. The break is bounded to text +input: native keyboard handling is unchanged, and old releases remain +available. + +## Start here + +Read the **`summaries/`** folder — that's the whole feature at stakeholder +altitude, ~30 min total. The one that needs your call is +**`summaries/decisions.md`**. + +| # | Summary | What it answers | +|---|---|---| +| 1 | `summaries/requirements.md` | What was asked for | +| 2 | `summaries/assessment.md` | What the code does today | +| 3 | `summaries/decisions.md` ⭐ | The calls awaiting approve / veto | +| 4 | `summaries/design.md` | How it works | +| 5 | `summaries/spec.md` | The API contract | +| 6 | `summaries/roadmap.md` | Build plan + effort (~37–63 h) | + +## If you want to dig + +- **Full docs** — same names in this folder (`decisions.md`, `design.md`, …), + for when a summary isn't enough. +- **`input.md`** — your ticket + clarification, verbatim. +- **`notes/`** — supporting codebase analysis. +- **`validation/`** — the review rounds and their fixes. diff --git a/doc/development/wip/77-new-input-api/assessment.md b/doc/development/wip/77-new-input-api/assessment.md new file mode 100644 index 00000000..b8ea612d --- /dev/null +++ b/doc/development/wip/77-new-input-api/assessment.md @@ -0,0 +1,377 @@ +# Feature #77 — Architecture Assessment + +Maps each requirement in `requirements.md` to the current +architecture. For each: what exists, what is missing, and +what the reuse potential is. Does not propose solutions. + +References are to source files under `src/`. Line numbers are +approximate and may drift as the code evolves. + +--- + +## 1. Edit Area Setup (FR-1) + +FR-1 requires a single setup call accepting optional initial +text, cursor position, highlighter, validator, and prompt label. + +**Current entry points** (`consoleController.lua:582–611`): + +| Function | Evaluator | Prompt | Init text | Cursor | Highlighter | Validator | +|---|---|---|---|---|---|---| +| `user_input()` | none | — | — | — | — | — | +| `input_text(p, i)` | `InputEvalText` | ✓ | ✓ | — | — | — | +| `input_code(p, i)` | `InputEvalLua` | ✓ | ✓ | — | Lua | — | +| `validated_input(f, p)` | `ValidatedTextEval` | ✓ | — | — | — | ✓ | + +- **Initial text**: supported via `ui_model:set_text(init)` + (`consoleController.lua:570`). Sets content; cursor lands at + end of text (`userInputModel.lua:120`). + +- **Initial cursor position**: not supported. No API parameter + or post-creation call to place the cursor at an arbitrary + position on setup. `UserInputModel:move_cursor(y, x)` exists + internally (`userInputModel.lua:506`) but is not exposed. + +- **Highlighter**: not a parameter. Highlight is determined by + the evaluator chosen at call time: `InputEvalLua` implies + Lua highlighting; `InputEvalText` implies none. No path to + supply a custom highlighter via the project API. + +- **Validator**: supported only via the separate + `validated_input()` function — a different entry point, not + a parameter to a unified call. No way to combine a custom + validator with `input_code`'s Lua highlighter. + +- **Prompt label**: supported via the `prompt` argument + present in all functions except `user_input()`. + +**Gap**: the parameter space is fragmented across four entry +points with overlapping but non-composable capabilities. A +single configurable entry point does not exist. + +**Reuse**: `UserInputModel`, `UserInputController`, +`UserInputView`, and the evaluator types are all reusable. +The model constructor accepts a `cfg` table and an evaluator; +the wiring is straightforward. + +--- + +## 2. Edit Area Lifecycle (FR-2, FR-3, FR-4) + +**FR-2 — Programmatic removal**: not exposed to projects. +On **submit**, `UserInputModel:handle()` evaluates the input; +when successful with `oneshot` set, it pushes a LÖVE2D +`'userinput'` event (`userInputModel.lua:819`), which +`handlers.userinput` in `controller.lua:709–713` handles by +setting `love.state.user_input = nil`. **Cancel** +(`cancel()`) calls `handle(false)` (`userInputModel.lua:795–798`); +in the `eval == false` branch the code sets `ok = true` and does +**not** push `'userinput'` — it only clears content via `reset()`. +The overlay stays visible on Escape; there is no function in the +project environment that triggers removal directly. + +**FR-3, FR-4 — Hide / show**: no mechanism exists. The overlay +view is rendered unconditionally whenever `love.state.user_input` +is set; there is no visibility flag on the model or view. +`UserInputModel` has a `visible` field (`userInputModel.lua:21`) +but this refers to the `VisibleContent` scroll/wrap state, not +display visibility. + +**Gap**: the entire lifecycle is either user-driven (submit, +cancel) or implicit (singleton guard). No project-callable +path for remove, hide, or show. + +**Reuse**: `love.state.user_input` as the on/off signal is a +natural extension point. A visibility flag on `UserInputModel` +and a conditional in `UserInputView:draw` would be the minimal +surface, but the design decision belongs to `design.md`. + +--- + +## 3. Event Notifications (FR-5, FR-6, FR-7) + +### FR-5 — Submit notification + +**Current**: polling. `UserInputController:keypressed` stores +the submitted value in the reftable via `res(t)` +(`userInputController.lua:353`) when the oneshot evaluator +succeeds. The project calls `r:is_empty()` in `love.update` +to detect completion. This is the canonical pattern across all +current examples. + +**Gap**: no callback is invoked on submit. The reftable is +the only result delivery mechanism. + +**Reuse**: the oneshot submit path in `UserInputController` +(`userInputController.lua:344–359`) is the right place to +invoke a callback; the evaluator pipeline it calls is reusable. + +### FR-6 — Non-character key notification + +This is the primary architectural gap. The dispatch chain in +`controller.lua:625–630` routes all `keypressed` events to +`user_input.C:keypressed(k)` when an overlay is active. The +project's own `love.keypressed` handler is unreachable while +an overlay exists: + +``` +handlers.keypressed + → if user_input: user_input.C:keypressed(k) -- return discarded + → else: love.keypressed(k) -- project never reached +``` + +`UserInputController:keypressed` has no forwarding mechanism. +All keys it handles are consumed; keys it does not explicitly +handle are also silently dropped (the function returns `ret` +which is only set for vertical movement — all other keys +return `nil` with no forwarding). `textinput` has the same +structure (`controller.lua:633–640`): overlay active means +the project's `love.textinput` is never called. + +**Gap**: total. No key event reaches project code while an +overlay is active. There is no opt-in or opt-out path. + +**Reuse**: `UserInputController:keypressed` already computes +which keys it handles vs. which it ignores. The logical +forwarding point exists; it needs a mechanism to deliver +unhandled events outward. + +### FR-7 — Boundary notification + +Partial internally. The chain is: + +1. `UserInputModel:cursor_vertical_move(dir)` returns a + boolean `limit` when movement is blocked at a boundary + (`userInputModel.lua:585`). +2. `UserInputController:keypressed` captures this in `ret` + via the `vertical()` inner function and returns it + (`userInputController.lua:252–258`, `388`). +3. `ConsoleController:keypressed` uses this return value for + history navigation (`consoleController.lua:1000–1007`). +4. For project overlays: `controller.lua:627` calls + `user_input.C:keypressed(k)` and discards the return value. + The limit signal never reaches the project. + +`UserInputModel:is_at_limit(dir)` is also available as a direct +query (`userInputModel.lua:558–570`) and is used by +`EditorController` via polling (`editorController.lua:511–514`). + +**Gap**: the mechanism exists and works within the framework. +It does not reach project code because the overlay dispatch +discards the return value, and projects have no reference to +the model to poll it themselves. + +--- + +## 4. Programmatic Text and Cursor Control (FR-8, FR-9, FR-10) + +**FR-10 — Change text**: partially supported. +`write_to_input(content)` (`consoleController.lua:599–605`) +calls `ui_model:set_text(content)` and `ui_con:update_view()` +while the overlay is active. This is the only project-facing +write API for a live overlay. It replaces the full text +content; it does not preserve cursor position or allow partial +updates. It does not allow changing the prompt label or any +other setup parameter. + +**FR-8 — Query cursor position**: not exposed to projects. +`UserInputModel:get_cursor_pos()` returns `(line, col)` and +is used throughout the model and controllers internally. +Projects receive only the reftable reference and have no +handle on the model. + +**FR-9 — Set cursor position**: not exposed to projects. +`UserInputModel:move_cursor(y, x)` (`userInputModel.lua:506`) +and `set_cursor(c)` (`userInputModel.lua:499`) exist and are +used internally (error highlighting, editor load). Not +reachable from project code. + +**Gap for FR-8/FR-9**: the model API is complete; exposure is +the only missing piece. The project currently holds only the +reftable; a handle on the model (or a wrapper over it) is +needed. + +--- + +## 5. API Expressiveness (FR-11, FR-12) + +These are consistency targets: the API should be expressive +enough to re-implement the console REPL and the editor's +input handling without accessing internals. + +**Console REPL** requires: submit on Enter (FR-5), history +navigation on Up/Down at boundary (FR-7), Ctrl+L clear terminal +(FR-6), error display (already in model). Currently the console +does all this by holding a direct reference to `UserInputModel` +and `UserInputController` — it is not using the project overlay +path. The overlay path is a subset that lacks FR-6 and FR-7 +forwarding. + +**Editor input** requires: load block text into input (FR-10), +submit block on Enter (FR-5), Escape to load (FR-6), Up/Down +at boundary for block navigation (FR-7), Ctrl+M / Ctrl+F mode +switches (FR-6), cursor position management (FR-8, FR-9). The +editor currently bypasses the project overlay API entirely, +holding direct references to the MVC objects it creates. + +**Gap**: FR-6 and FR-7 forwarding are the blockers for both +re-implementability targets. Without them, projects cannot +react to the key events that drive both the console's history +navigation and the editor's mode switches and block navigation. + +--- + +## 6. Non-Functional Requirements + +### NFR-1 — Allocation / GC + +Every call to `input_text()`, `input_code()`, or +`validated_input()` allocates a new `UserInputModel`, +`UserInputController`, `UserInputView`, `VisibleContent`, +and a `History` dequeue. `user_input()` allocates a new +reftable. For the REPL pattern used in all current examples, +this runs on every Enter press. + +The relevant source is `consoleController.lua:563–580`: the +`input()` local function always constructs fresh instances. +There is no reuse path. + +**Gap**: the allocation-per-session pattern is structural, not +incidental. Addressing NFR-1 requires a lifecycle redesign, +not a localised fix. + +### NFR-2 — Event-driven model + +**Gap**: entirely absent. The project API is polling-only. +The reftable / `is_empty()` pattern is the canonical idiom +(present in repl, tixy, guess, turtle, valid, balloons). +No callback registration mechanism exists anywhere in the +project-facing API surface. + +### NFR-3 — Compy API consistency + +Partial. `compy.text_input` is assigned in +`consoleController.lua:628`, but the other functions +(`input_code`, `validated_input`, `user_input`, +`write_to_input`) are plain globals in the project +environment, not under `compy.*`. The `compy.text_input` +assignment on line 628 references a bare `input_text` +identifier (`compy_namespace.text_input = input_text`) which +has no local or global definition in scope — it assigns `nil`. +`compy.text_input` is documented in `console.md` but has never +functioned; no example or test calls it. + +**Gap**: the input API is not consistently namespaced. A new +API placed fully under `compy.*` would resolve this. + +### NFR-4 — Pedagogical usability + +The polling pattern is not opaque — students following the +game loop understand it — but it requires non-trivial +boilerplate. The canonical idiom across all examples is: + +```lua +r = user_input() +function love.update() + if r:is_empty() then + input_text("prompt") + end +end +``` + +This pattern re-creates the overlay on every frame until the +user submits, which also conflicts with NFR-1. A callback +model should reduce both the boilerplate and the allocation. + +--- + +## 7. Summary + +### By component + +| Component | Status | Action needed | +|---|---|---| +| `UserInputModel` | Solid — text, cursor, history, evaluator, error all present | Expose cursor query/set to callers; add visibility flag | +| `UserInputController` | Handles all editing operations correctly | Add event forwarding for unhandled keys; expose limit signal | +| `UserInputView` | Renders correctly | Add conditional on visibility flag | +| Project overlay API (`input_text` et al.) | Fragmented, polling-only, allocates per call | Replace entry points; add callback registration; lifecycle redesign | +| `love.state.user_input` dispatch | Routes events correctly to overlay | Overlay keypressed return value is discarded — needs threading through | +| Evaluator types | Reusable as-is | No change needed | +| `write_to_input` | Live text update works | Extend to cover other mutable parameters | + +### By requirement + +| Req | Current state | Gap size | +|---|---|---| +| FR-1 | Fragmented across 4 functions; cursor pos and custom highlighter absent | Medium | +| FR-2 | Not exposed to projects | Small — needs a project-callable remove path | +| FR-3/FR-4 | No hide/show mechanism | Small — needs visibility flag + show/hide calls | +| FR-5 | Polling only | Medium — reftable delivery replaced by callback | +| FR-6 | Total absence — all keys consumed, no forwarding | Large — cross-cutting, affects dispatch chain | +| FR-7 | Signal exists internally, discarded at overlay dispatch | Small — needs threading through the dispatch | +| FR-8/FR-9 | Model API complete, not exposed | Small — needs a project handle on the model | +| FR-10 | Text content update works; other params not updatable | Small | +| FR-11/FR-12 | Blocked by FR-6 and FR-7 gaps | Follows from those | +| NFR-1 | Structural allocation per session | Large — requires lifecycle redesign | +| NFR-2 | No callback mechanism anywhere | Large — follows from FR-5/FR-6/FR-7 | +| NFR-3 | Partially namespaced | Small — namespace cleanup | +| NFR-4 | Boilerplate-heavy; conflicts with NFR-1 | Follows from NFR-2 | + +--- + +## 8. Constraints and Risks + +**Singleton constraint**: only one overlay can exist at a time +(`consoleController.lua:564–566`). FR-3/FR-4 (hide/show) work +within this constraint; the design does not need to lift it. + +**Convention adoption scope**: `UserInputController` is shared +across three contexts — console REPL, editor input strip, and +project overlay. Any change to its event handling or return +values affects all three. Changes intended for the project +overlay path must be compatible with or explicitly isolated +from the console and editor paths. See `notes/concerns.md`. + +**Editor coordinate ambiguity (FR-7)**: the editor's +`is_at_limit` check operates in wrapped/apparent line +coordinates, not source line coordinates. A general-purpose +boundary callback would need a defined coordinate space. The +console and project overlay contexts do not have this +ambiguity. See `notes/concerns.md`. + +**Cancel path**: Escape causes `UserInputModel:cancel()` to +call `handle(false)` (`userInputModel.lua:795–798`), which +clears input content via `reset()` but does **not** push +`'userinput'` — the overlay remains visible. This is exactly +the current limitation that `design.md §5` is built to fix +("Escape clears input content but does not dismiss the +overlay"). In the new model, `framework_handlers['escape']` +in `ProjectController` owns cancel and pushes `'userinput'` +unconditionally, resolving the limitation. + +**Text character + modifier co-occurrence**: LÖVE2D delivers +a held-modifier gesture as two separate events — `keypressed` +for the key and `textinput` for any character it produces. +When a modifier is held and a character-producing key is +pressed, both events fire. Under the current overlay dispatch, +both are consumed by `UserInputController`. Under a callback +model, the same gesture could trigger both a text-entered +notification and a key-combo notification, producing a +confusing double callback. Defining the expected behaviour for +this case — suppress one, suppress both, or emit both — is a +decision that needs to be made explicit in design. + +This was resolved by `decisions.md` D-6 (superseded in round 1): +both channels fire independently, mirroring raw LÖVE — no +suppression, no classification. There is no double *insertion* +because the keypressed sink ignores plain character keys +(insertion is the textinput sink's job). A project that handles +both channels does so by explicit choice. This paragraph records +the original gap; D-6 carries the settled resolution. + +**`write_to_input` scope**: the function closes over +`ui_model` from the enclosing `prepare_project_env` call +(`consoleController.lua:555`). It can reach the model of the +current overlay only because both are in the same closure +scope. A redesigned API that moves away from closure-scoped +state will need to replicate or replace this access path. diff --git a/doc/development/wip/77-new-input-api/decisions.md b/doc/development/wip/77-new-input-api/decisions.md new file mode 100644 index 00000000..1ca08af7 --- /dev/null +++ b/doc/development/wip/77-new-input-api/decisions.md @@ -0,0 +1,656 @@ +# Feature #77 — Blocking Decisions + +> **Status — local design proposal, with one stakeholder ruling.** This +> document is a local design proposal derived from `input.md`, with one +> exception: **D-1 has been ruled on by stakeholders** (consensus in +> `input.md`, feedback round 1, 2026-06-06: **DISCARDED** — no backward +> compatibility). D-1's resolution below is therefore stakeholder ground +> truth, not a proposal. The remaining entries (D-2…D-10) are still a local +> proposal awaiting a single eventual approve/veto review against `input.md`; +> none of those is stakeholder-approved. + +## The blocking decisions + +Seven questions were identified during requirements analysis +and architecture assessment that require stakeholders consensus +— they involve tradeoffs with broad impact across the codebase, +existing examples, and future API ergonomics. + +1. **Backward compatibility** — must the existing input + functions continue to work after the new API is introduced? +2. **Second setup call** — what happens when a project tries + to configure input while it is already active? +3. **Key event coverage** — how should different kinds of + non-text key events (modifier combos, plain navigation + keys, function keys) be surfaced to project code? +4. **Cancel and submit notifications** — should these be + dedicated named events, and how does the framework's own + teardown relate to project callbacks? +5. **Cursor boundary definition** — in a multiline input + area, what exactly constitutes hitting a wall? +6. **Modifier + character co-occurrence** — when a + character key is pressed with a modifier held, one event + or two? +7. **Rollout scope** — does the new event topology apply to + all input contexts at once, or the project overlay first? + +These seven remain the core of the stakeholder review. Three +further commitments were added during the local design rounds and +are recorded here for completeness and traceability: **D-8** +(cursor restoration + 2D contract) and **D-9** (native handler +coexistence) from round 1, and **D-10** (namespace isolation under +`compy.input.*`) from round 2. They are not new stakeholder +questions — D-8 restores a dropped requirement, D-9 closes a +backward-compat surface, D-10 is a packaging choice — but each is +a genuine architectural decision and is listed below with its +provenance. + +--- + +## Proposed resolution + +A design approach (described below) has been developed +that resolves all seven decisions coherently. Full rationale is in `notes/decisions.md`. + +**Stakeholders:** if the direction is acceptable, work +proceeds to `design.md` and the full implementation plan +(fast-track). If any aspect is problematic, the relevant +decision(s) are re-opened for re-settlement. + +--- + +### Terminology + +**Project overlay** — the input area a running project +creates by calling `input_text()`, `input_code()`, or +similar. It appears over the project's running display and +is stored in `love.state.user_input`. It is distinct from +the console REPL's own input strip and the editor's input +strip, which are separate persistent instances that are not +affected by the changes described below. + +**Persistent input widget** — the lifecycle model proposed +for the overlay: one object, created once at startup, reused +across all sessions rather than created and discarded on +each call. + +**Event callbacks** — the notification model: project code +registers functions that the framework calls when specific +events occur, replacing the current pattern of polling a +reference variable on every frame. + +--- + +### The approach in brief + +The structural basis is routing unification. The +`if user_input then ... else ... end` gate in +`love.handlers.keypressed` (`controller.lua`) is removed. +A new `ProjectController` — a sibling to `ConsoleController` +and `EditorController` — takes ownership of all input +handling for the project-running context. `UserInputController` +becomes the universal terminal sink at the bottom of every +branch: always present, never a routing destination. +This structural change is what makes the three improvements +below possible without special cases. See +`notes/routing_unification.md` for the full before/after +diagram and rationale. + +The design addresses the requirements in `requirements.md` +through three connected changes: + +**1. The overlay input widget becomes persistent** (addresses +FR-1, FR-2, FR-3, FR-4, NFR-1). Instead of being created +on each `input_text()` call and discarded after submission, +the widget is created once when the application starts and +reused. Projects call `show`, `hide`, and `configure` on it +rather than creating it. This eliminates the per-session +allocation identified in NFR-1, and makes dynamic prompt +changes (such as updating a label mid-run) straightforward. + +The wiring change that implements this — moving construction +from inside `ConsoleController` to `main.lua` — is a +contained refactor with no behaviour change. It can be done +and verified independently before the rest of the work. + +**2. Keyboard events reach project code while a prompt is +active** (addresses FR-5, FR-6, FR-7, NFR-2). Currently, +all key events are consumed by the overlay with no +notification to the project. The new API exposes three +surfaces: a table of currently-held keys (so modifier state +is always available), project-registered handlers for +specific key combinations, and overloadable callbacks for +key events and text input. All three receive the same +live key state, so modifier context is implicit rather than +requiring a separate event. + +Submit and cancel are named callback points with a +before/after structure: project code can run before the +framework's own teardown, or after it, without being able +to accidentally suppress it. The framework always runs its +own structural behaviour (evaluate on Enter, close on +Escape); projects extend rather than replace it. + +**3. The legacy text-input API is removed** (addresses D-1, +**discarded** by stakeholders — see `input.md` round 1). +`input_text()`, `input_code()`, `validated_input()`, +`user_input()`, and `write_to_input()` are removed, not kept +as facades. The reftable / `is_empty()` polling pattern goes +with them. The in-repo examples that use these functions are +migrated to the `compy.input.*` callback API (priority: +`tixy`, `balloons`; the rest convert-or-exclude per the owner's +release call). This is scope **3** of the design; it does not +affect the native-handler coexistence path (D-9), which is a +separate surface and is retained — only text-input examples are +affected ("only text fields", per `input.md`). + +--- + +### Quick reference + +| | Decision | Resolution | +|---|---|---| +| D-1 | Backward compat | **Discarded** (stakeholder consensus, `input.md` round 1): no backward compatibility. Legacy text-input globals removed; examples migrated (`tixy`/`balloons` priority) or excluded from the release. D-9 native coexistence is unaffected. | +| D-2 | Second setup call | Dissolved — singleton accepts configure/show, no create | +| D-3 | Key event coverage | Three-tier dispatch; sink is default of `on_key_pressed`; modifier-first generic-folded combo format; metatable-normalised registration; overloadable matcher | +| D-4 | Cancel/submit | Named chains: `before_X → X → after_X`; framework owns middle; `oneshot` is `UserInputModel` field, deleted in M6 | +| D-5 | Boundary | `on_limit_reached(direction)` hook; whole-input boundary; second arg reserved | +| D-6 | Modifier + character | Superseded (round 1): two independent channels, no exclusivity; `on_text_entered(text, keys_pressed)` | +| D-7 | Rollout scope | Overlay context first; FR-11/FR-12 walkthrough added to `design.md §7` | +| D-8 | Cursor contract + live surface | 2D `(line, col)` source-line coords; `compy.input.get_cursor`, `compy.input.set_cursor`, `compy.input.set_text`; `set_text` supersedes the removed `write_to_input` | +| D-9 | Native handler coexistence | Auto-provisioning via legacy heuristic; lifecycle-split wrapper; transition diagnostics | +| D-10 | Namespace isolation | New feature-#77 surface lives under `compy.input.*`; `compy.keys_pressed` stays global; legacy text-input globals removed (D-1 discarded) | + +--- + +## Per-decision detail + +### D-1 · Backward compatibility + +**Question:** Must the existing functions `input_text()`, +`input_code()`, `validated_input()`, and `user_input()` continue +to work after the new API is introduced? + +**Context:** Every current example (repl, tixy, guess, turtle, +valid, balloons) uses these functions with the polling pattern. +A clean break simplifies the new API design but requires +updating all examples. Keeping them as shims allows gradual +migration but adds a maintenance surface. + +**Affects:** Overall API shape, whether examples need +updating, implementation scope. + +**Source:** `requirements.md §5`; **resolved by `input.md` +feedback round 1 (2026-06-06).** + +**Stakeholder decision: DISCARDED — no backward compatibility.** +The project is pre-1.0; all software that uses the input API is +known and must be updated anyway (and benefits from doing so). +Keeping the legacy functions in a release would defeat the +purpose of shipping the new API. Consensus reached among three +stakeholders (full dialogue in `input.md`). + +Consequences: + +- `input_text()`, `input_code()`, `validated_input()`, + `user_input()`, and `write_to_input()` are **removed** from + the project environment — no facades, no deprecation shims, + no `strict_input` flag. The reftable / `is_empty()` polling + idiom is removed with them. +- The in-repo examples that use these functions are migrated to + the `compy.input.*` callback API (see roadmap **M8**). Migration + priority is the showcase set the owner named: `tixy` and + `balloons` (and `maze`, not yet in the repo). The remaining + text-input examples (`repl`, `guess`, `valid`, `turtle`) are + converted if time allows, otherwise excluded from the next + release — the owner's call at release time, not a blocker. +- **Scope is text input only.** The native `love.keypressed` / + `love.textinput` coexistence path (D-9) is a separate surface + and is **retained** — games that drive their own keyboard + handling keep working. Per the stakeholder exchange, removing + the legacy API "won't break all keyboard input, only text + fields". Old releases remain available, so the existing + experience is not deleted while migration proceeds. + +*(Stakeholder feedback, round 1, 2026-06-06. The earlier +"Suggested decision" — keep backward-compatible facades — is +superseded by this ruling.)* + +--- + +### D-2 · Second setup call while input is active + +**Question:** What should happen when a setup call is made +while an input area is already active? + +**Context:** The current behaviour is a silent no-op (the +singleton guard returns immediately). This was a concrete +limitation — changing a prompt label mid-run was not possible. +Options include: silent skip (current), replace the active +configuration in-place, or treat it as an error. + +**Affects:** Lifecycle design, FR-1 (setup), FR-3/FR-4 +(hide/show). + +**Source:** `requirements.md §5`, `assessment.md §2` + +**Suggested decision:** Dissolved by the singleton design. +There is no "setup call while active" because there is no +setup call — only configure/show on a persistent widget. +Projects that want to change the prompt label or evaluator +mid-run call a reconfigure method directly. The silent-skip +guard is removed. + +--- + +### D-3 · Key event coverage + +**Question:** Should Ctrl+key combinations and other +non-character keys (navigation keys, function keys) produce +the same notification or separate ones? + +**Context:** These are gesturally different — Ctrl+key is +typically a command; navigation keys are movement. A single +surface is simpler; separate surfaces let callers express +intent more clearly. The gap in the current architecture is +that all key events are consumed by the overlay controller +with no forwarding to project code. + +**Affects:** FR-6 callback design, API surface size. + +**Source:** `requirements.md §5` + +**Suggested decision (original — superseded in part; see the +round-1 annotation below for the current three-tier model, combo +form, and signature):** All key events are covered through a +unified `_on_key_pressed(k, pressed, isrepeat)` dispatch +where `pressed` is the current `compy.keys_pressed` set. +Three dispatch levels inside `ProjectController:keypressed`: +framework_handlers → compy.input.handlers → `compy.input.on_key_pressed`, +with `UserInputController:keypressed` as the terminal sink +below all three. Combo registration uses serialised sorted +key names (e.g. `"lctrl+s"`) consistent with LÖVE2D +conventions. Modifier+character events go through +`_on_textinput`, not keypressed — no overlap. The same +dispatch function is shared across all three controller +branches (written once; `ProjectController` uses it first; +`ConsoleController` and `EditorController` migrate when +ready). Specific serialisation format is a spec detail. + +*(Origin: local design round 1, 2026-06. See +`validation/recommendations_1.md` Items 3, 7.)* Architect +clarification: the three dispatch levels stand, but the +text-editing sink is the **default value** of +`compy.input.on_key_pressed`, not a fourth separate tier. A project +that overrides `compy.input.on_key_pressed` replaces the default +sink entirely; there is no tier below it to fall through to. +The combo serialisation rule is corrected from alphabetical to +**modifier-first by fixed precedence** (ctrl, alt, shift, gui) +then the triggering key, with **generic l/r folding** (combos +use `ctrl`, not `lctrl`/`rctrl`; the `keys_pressed` table +retains precise LÖVE names). `compy.input.handlers` is +metatable-backed: `__newindex` normalises the registered key to +canonical form on assignment. Dispatch uses an overloadable +exact-match default matcher; the matcher is the marked +extension seam for future glob/prefix matching. Signature: +`on_key_pressed(k, keys, isrepeat)` — `isrepeat` as trailing +arg so the common `(k, keys)` form stays clean. + +--- + +### D-4 · Cancel and submit notifications + +**Question:** Should the API provide dedicated callbacks for +when the user dismisses the input (Escape) or submits (Enter), +and how does the framework's own teardown relate to project +callbacks? + +**Context:** Escape and Enter currently trigger framework +behaviour (teardown, evaluate) with no notification to project +code. A dedicated callback is more explicit but the framework +must continue to run its structural logic regardless of what +project code does. + +**Affects:** FR-5, FR-6, API surface size, lifecycle semantics. + +**Source:** `requirements.md §5` + +**Suggested decision:** Cancel and submit each have named +chain points: `before_cancel → cancel → after_cancel` and +`before_submit → submit → after_submit`. The framework owns +the middle point (teardown on cancel, evaluate+store on +submit) and always runs it. Projects hook into before or +after. Ordering is enforced by call sequence in the +framework's default implementations. The generic +`compy.input.on_key_pressed` uses return-value propagation (true = +consumed, nil = bubble) for the non-semantic case. + +The `oneshot` flag is deleted as a consequence of D-2 combined +with D-4. Previously, `oneshot` encoded two signals: "widget is +the active overlay" and "widget owns submit." Both are replaced: +`show()`/`hide()` on the singleton carry the activation signal; +`framework_handlers['return']` in `ProjectController` owns +submit. The flag has nothing left to do. + +*(Origin: local design round 1, 2026-06. See +`validation/recommendations_1.md` Items 2, 8.)* Corrections: +(1) `oneshot` is a field of **`UserInputModel`** +(`userInputModel.lua:15,49`), not `UserInputController`; +`UserInputController` only reads `self.model.oneshot`. The +deletion file target is `userInputModel.lua` (field removal) and +`userInputController.lua` (submit-path code that reads it). +(2) The deletion is moved to **M6**, not M2. `oneshot` currently +drives both submit gating and the `'userinput'` push; its +replacement (`framework_handlers['return']` in `ProjectController`) +arrives only in M6. Deleting it in M2 would strand submit across +M2–M5, contradicting M2's "zero behaviour change" claim. Until M6, +`oneshot` stays and continues to drive submit exactly as today. + +--- + +### D-5 · Cursor boundary definition + +**Question:** In a multiline input area, what constitutes a +cursor boundary — end of current line, end of entire input, +or both? + +**Context:** In a single-line context these are equivalent. +In multiline, the distinction matters for editor-like +navigation. The existing internal `is_at_limit` operates at +the whole-input level (first/last line of the buffer). + +**Affects:** FR-7 semantics, FR-12 (editor re-implementability). + +**Source:** `requirements.md §5`, `assessment.md §3` + +**Suggested decision:** Single `on_limit_reached(direction)` +hook where direction is `'up'` or `'down'`. Covers +whole-input boundary only, consistent with the existing +`is_at_limit` implementation. A hook always propagates — +both project code and framework code observe the same event +independently. Second positional argument reserved for +future boundary-level granularity, undefined in v1. + +--- + +### D-6 · Text character + modifier co-occurrence + +**Question:** When a character-producing key is pressed while +a modifier is held, should the API emit a text-entered +notification, a key-combo notification, both, or neither? + +**Context:** LÖVE2D delivers these as two separate events. +Without an explicit policy a callback-based API could fire +two notifications for a single user gesture. + +**Affects:** FR-5 and FR-6 callback model. + +**Source:** `assessment.md §8`, `notes/concerns.md` + +**Suggested decision (original):** When `love.textinput` fires, +the framework calls `_on_textinput(text, pressed)` where +`pressed` is the current `compy.keys_pressed` set. +Non-character keys in `pressed` constitute the implicit +modifiers table, reaching project code as +`on_text_entered(text, mods)`. The framework suppresses +`on_key_pressed` for keys followed by a `textinput` in the +same frame — "exactly one notification per gesture." + +*(Origin: local design round 1, 2026-06. See +`validation/recommendations_1.md` Item 6.)* **Superseded.** +D-6 was a "Suggested decision" that was never discussed or +signed off (its closing line delegated the mechanism to the +spec, where it became an impossible same-frame lookahead). The +"no double callback" guarantee is dropped — it is grounded in +neither `requirements.md` nor the current implementation. + +**Replacement resolution:** both LÖVE channels fire +independently, mirroring raw LÖVE semantics: + +- `keypressed` channel → three-tier dispatch → `compy.input.on_key_pressed` (default: text-editing keypressed sink) +- `textinput` channel → `compy.input.on_text_entered` (default: text-editing textinput sink) + +No suppression, no classification. A character-producing keypress +visits both channels; there is no double-*insertion* because the +keypressed sink ignores plain character keys (insertion is the +textinput sink's job). Projects that handle both channels do so +by explicit choice — plain LÖVE semantics. The `on_text_entered` +second argument is the full `keys_pressed` read-only proxy +(consistent with `on_key_pressed`), not a filtered `mods` subset. + +**Channel symmetry.** The two channels are deliberately symmetric: +`on_text_entered`'s default value is the textinput sink exactly as +`on_key_pressed`'s default is the keypressed sink, and overriding +either replaces its sink. Textinput processing follows the same +default-sink/override principle as keypressed; the only structural +difference is that the textinput channel has no combo tier above +it (characters are not combos). + +*(Origin: local design round 2, 2026-06. See +`validation/recommendations_2.md` Item 4.)* **Future seam — `mods` +string (not built in v1).** A design-session idea proposed +augmenting both channels with an optional pre-folded `mods` string +— the generic l/r-folded modifier descriptor (like the combo +form) — passed as a trailing argument to downstream +handlers/callbacks, as a convenience for handlers shared across +several combos and for the generic callback that runs after combo +handlers. v1 ships the `keys_pressed` proxy only (modifiers are +derivable from it); the `mods` string is recorded as a candidate +addition, paired with the overloadable-matcher and +text-command-set seams (Items 6/7, round 1). It is **flagged for +architect confirmation** (build in-scope vs. leave as a seam) — +see the changelog's open questions; it is not built here. + +--- + +### D-7 · Rollout scope + +**Question:** Should the new event topology apply to all +three input contexts (console REPL, editor, project overlay) +at once, or to the project overlay first? + +**Context:** The overlay context is where the current gap +is most acute — project code cannot receive key events at +all while the overlay is active. The console and editor +already handle their own events through direct controller +paths. The question is whether to migrate all three at once +or stage the work. + +**Affects:** Implementation scope, FR-11/FR-12. + +**Source:** `assessment.md §8`, `notes/concerns.md` + +**Suggested decision:** The new routing model applies to the +project-running context first. The `if user_input then` gate +in `controller.lua` is removed. `ProjectController` (new) +becomes the occupant of `love.keypressed` when a project is +running — exactly as `ConsoleController` is when no project +runs. The singleton wiring refactor (moving construction to +`main.lua`) is the first step. `compy.input.handlers` resets on project stop via +`stop_project_run` / `clear_user_handlers` +(`consoleController.lua:860–868`). +The conceptual shift: "overlay" as a routing concept dissolves +— it is replaced by "singleton is currently visible." + +`ConsoleController` and `EditorController` retain their +existing key handling paths for now. + +FR-11/FR-12 are satisfied when the API can express the key +patterns the console and editor use — they do not require +migration within this feature. The migration path is clear +and has been analysed: both controllers' key dispatch +consists of if-chains that map directly onto handler +registrations, with no changes to the underlying methods +(evaluate, history navigation, block navigation, mode +switches). No blocking constraint exists. `design.md` will +include a walkthrough confirming the API surface covers +both cases. Actual migration of the console and editor is +a named follow-on feature. Full analysis in +`notes/editor_repl_input.md`. + +*(Origin: local design round 1, 2026-06. See +`validation/recommendations_1.md` Item 9.)* The promised +FR-11/FR-12 walkthrough has been added to `design.md §7`. + +--- + +### D-8 · 2D cursor contract and live cursor/text surface + +**Question:** What coordinate space does the cursor API use, +and how are FR-8, FR-9, FR-10 exposed? + +**Context:** `requirements.md` FR-8/9/10 require programmatic +cursor query, cursor set, and live text change while active. +These silently fell out of the chain after `assessment.md` — a +derivation error, not a deliberate de-scope. The model already +has `get_cursor_pos()`, `move_cursor(y, x)`, and `set_text()`; +exposure is the only missing piece. A coordinate decision is +needed: `spec.md §2` used a single integer ("1-based column in +line 1"), which only works single-line and contradicts the +`multiline` flag. + +**Affects:** FR-8/9/10 API surface, `compy.input.show()` `cursor` +field, roadmap M7. + +**Source:** `requirements.md §2.4`, `assessment.md §4`. + +**Decision:** Coordinate model: **2D `(line, col)`, 1-based, +source-line coordinates** (not wrapped/apparent lines). The +model cursor is already 2D; this is a direct restoration. +Single-line callers ignore `line` (always 1). The `cursor` +field at `compy.input.show()` accepts `{line, col}` (or two separate +fields); FR-7/8/9 are all settled on this one coordinate space. + +New project-facing functions delegating to the singleton: + +| Function | Maps to | Behaviour | +|---|---|---| +| `compy.input.get_cursor()` | `model:get_cursor_pos()` | Returns `line, col`; returns `nil` when hidden | +| `compy.input.set_cursor(line, col)` | `model:move_cursor(line, col)` | Clamps to valid range; no-op when hidden | +| `compy.input.set_text(text [, keep_cursor])` | `model:set_text(...)` + `update_view` | Live write; explicit exception to `configure()` text-immutability | + +`write_to_input` is **removed** with the rest of the legacy +text-input globals (D-1 discarded). The example that used it +(`tixy`) migrates to `compy.input.set_text` directly; there is no +`write_to_input` facade. `compy.input.set_text` is the live-write +surface that supersedes it. + +Model fix required: `UserInputModel:set_text` must honour +`keep_cursor` (skip the unconditional `jump_end()` when +`keep_cursor` is true). Roadmap: the full +`compy.input.set_text`/`compy.input.get_cursor`/`compy.input.set_cursor` +surface goes in M7; the legacy-global removal and example +migration happen in M8. + +*(Updated by stakeholder feedback, round 1, 2026-06-06: the +`write_to_input` facade is dropped — see D-1.)* + +*(Origin: local design round 1, 2026-06. See +`validation/recommendations_1.md` Item 1.)* + +--- + +### D-9 · Native `love.keypressed`/`textinput` coexistence via auto-provisioning + +**Question:** How do projects that define `love.keypressed` +natively interact with `ProjectController` owning the slot? +Shipped examples (pong, life, paint, turtle) rely on native +handlers; without a solution they break when `ProjectController` +takes the slot. + +**Context:** The routing layer already transparently intercepts +native LÖVE handlers via `save_user_handlers` / +`hook_if_differs` (`controller.lua:73–107`). Projects never +*actually* bind `love.*`; the framework captures and re-routes +them. D-1 was about the four text-input functions; native +handler coexistence is a **separate** surface. With D-1 now +discarded (no backward compatibility for the text-input API), +D-9 is what keeps the stakeholders' "only text fields break" +guarantee true: games that drive their own keyboard handling +continue to work unchanged. D-9 therefore stands on its own and +is **retained** independently of D-1's removal. + +**Affects:** native-handler coexistence (independent of the +discarded D-1 text-input surface), `ProjectController` startup, +FR-6. + +**Source:** `validation/recommendations_1.md` Item 4. + +**Decision:** When a project is loaded, `ProjectController` +inspects what it defined. **Legacy heuristic (the gate):** if the +project has native `love.keypressed`/`textinput` (captured by +`save_user_handlers`) AND has set none of the new `compy.*` surfaces +(`compy.input.on_key_pressed`, `compy.input.handlers`, `compy.input.on_text_entered`), +`ProjectController` auto-provisions `compy.input.on_key_pressed` on the +project's behalf as a **lifecycle-split wrapper**: + +- **Singleton visible** → run the text-editing sink (text editing wins). +- **Singleton hidden** → run the project's native handler (game keys). + +This reproduces today's gated behaviour exactly, with zero changes to +existing examples. Projects that set any `compy.*` surface explicitly +are new-style: they take control via Item 3 override semantics and the +heuristic never applies. In debug mode, the wrapper logs which branch +it chose on each invocation (transition diagnostics — nudges toward +explicit `compy.*` use). The auto-provisioned wrapper is cleared on +project stop alongside all other `compy.*` callbacks. + +The same generalises to `love.textinput` → `compy.input.on_text_entered` +and `love.keyreleased`. + +*(Origin: local design round 1, 2026-06. See +`validation/recommendations_1.md` Item 4.)* + +--- + +### D-10 · Namespace isolation under `compy.input.*` + +**Question:** Where does the new feature-#77 surface live on the +project-facing namespace — flat on `compy.*`, or under a dedicated +sub-namespace? + +**Context:** `compy.*` is the whole project-facing API and will +accumulate many unrelated callbacks, variables, and objects. +Flat-mounting the input surface there crowds the namespace and +blurs concerns. The decision was made (and applied to the derived +docs) in round 1, but was not recorded as a decision — it lived +only in `recommendations_1.md`'s namespace section and in the +edited `design.md`/`spec.md`/`roadmap.md`. Recording it here makes +it visible to the eventual stakeholder review. + +**Affects:** every new feature-#77 name; NFR-3 (namespace +consistency). (The legacy globals that this decision originally +left flat are now removed entirely — D-1 discarded — so there is +no longer a legacy call-target to consider.) + +**Source:** `validation/recommendations_1.md` "Namespace +isolation"; surfaced as a decision per +`validation/recommendations_2.md` Item 2. + +**Decision:** The new callback / lifecycle / accessor surface lives +under a dedicated sub-namespace **`compy.input.*`**, not flat on +`compy.*`. This isolates the feature's surface, keeps `compy.*` +structured, and leaves room for sibling sub-namespaces later. + +- **Moves under `compy.input.*`:** `show`, `hide`, `configure`, + `clear`, `handlers`, `on_key_pressed`, `on_text_entered`, + `before_submit`/`after_submit`, `before_cancel`/`after_cancel`, + `on_limit_reached`, `get_cursor`, `set_cursor`, `set_text`. +- **Stays global — `compy.keys_pressed`:** it is raw keyboard + state (physical reality), not the input-manipulation layer + (interpretation). Keeping it out of `compy.input.*` makes that + boundary explicit at the namespace level. +- **Removed — legacy text-input globals:** `input_text()`, + `input_code()`, `validated_input()`, `user_input()`, and + `write_to_input()` are removed, not kept (D-1 discarded — + stakeholder feedback round 1). There is no legacy surface left to + re-point; the new API lives entirely under `compy.input.*`. + +The `compy.input` table is created once at namespace setup; +reset-on-stop clears `compy.input.handlers` and the `compy.input.*` +callbacks together. + +*(Origin: local design round 1 namespace pass, 2026-06; recorded +as a decision in round 2. See `validation/recommendations_2.md` +Item 2 and `validation/recommendations_1.md` "Namespace +isolation".)* diff --git a/doc/development/wip/77-new-input-api/design.md b/doc/development/wip/77-new-input-api/design.md new file mode 100644 index 00000000..5e900e8f --- /dev/null +++ b/doc/development/wip/77-new-input-api/design.md @@ -0,0 +1,397 @@ +# Feature #77 — Design + +*Primary design document for stakeholder and implementor +review. Sources: `notes/solution_sketch.md` and supporting +notes. Implementation spec is in `spec.md`.* + +--- + +## 1. Problem and Scope + +Feature #77 adds a persistent, callback-driven input widget +to the `compy` project API: one configurable edit area with +lifecycle control (`show`/`hide`/`configure`), event callbacks +for submit, cancel, key events, text input, and cursor +boundary hits, and programmatic cursor and content access. +The console REPL and editor are not migrated within this +feature; their migration to the same API is a named follow-on. +Touch input, multiple simultaneous edit areas, and changes +to the editor's internal block navigation are out of scope. + +--- + +## 2. Architectural Approach — Routing Unification + +### The current model + +The dispatcher in `love.handlers.keypressed` (`controller.lua`) +routes all key events exclusively to the overlay widget when +`love.state.user_input` is set, bypassing the project's own +handlers entirely. Project code is unreachable while any +input prompt is on screen. + +``` +love.handlers.keypressed + ├─ global shortcuts + ├─ overlay active? + │ YES → UserInputController:keypressed [exclusive] + │ NO ↓ + └─ love.keypressed + ├─ ConsoleController → ... → UserInputController + ├─ EditorController → ... → UserInputController + └─ project's own love.keypressed [or nothing] +``` + +The `if user_input then` gate is not a designed abstraction; +it is a seam left by the overlay being added as a bolt-on to +the existing routing. It encodes a special case with no +natural extension point for callbacks. + +### The new model + +The gate is removed. `ProjectController` — a new controller, +sibling to `ConsoleController` and `EditorController` — owns +all input handling for the project-running context. +`UserInputController` becomes the universal terminal sink at +the bottom of every branch, doing work proportional to its +activation state (transparent no-op when hidden, text-editing +operations when visible). + +``` +love.handlers.keypressed + ├─ global shortcuts + └─ love.keypressed + ├─ ConsoleController:keypressed + │ ├─ framework_handlers (Enter, history, etc.) + │ ├─ console-registered handlers / callbacks + │ └─ → UserInputController:keypressed [sink] + ├─ EditorController:keypressed + │ ├─ framework_handlers (Enter, Esc, Ctrl+M, etc.) + │ ├─ editor-registered handlers / callbacks + │ └─ → UserInputController:keypressed [sink] + └─ ProjectController:keypressed [new] + ├─ framework_handlers (Enter, Escape, etc.) + ├─ compy.input.handlers[combo] [project-registered] + ├─ compy.input.on_key_pressed [generic, overloadable, default: sink] + └─ → UserInputController:keypressed [sink] +``` + +Routing does not change based on whether the singleton is +visible. "Overlay active?" is gone as a routing condition; +widget visibility is a state on the singleton, not a gate. + +**Native handler coexistence (legacy heuristic).** Projects +that define native `love.keypressed` (captured by the existing +`save_user_handlers` path) but set none of the new `compy.*` +surfaces are treated as legacy. `ProjectController` detects +this at load time and **auto-provisions `compy.input.on_key_pressed`** +as a lifecycle-split wrapper: when the singleton is visible, the +wrapper routes to the text-editing sink; when hidden, it routes +to the project's native handler. This reproduces today's gated +behaviour with zero project changes. Projects that set any +`compy.*` surface are new-style and the heuristic never applies. +In debug mode the wrapper logs its routing decision (transition +diagnostics). See §6 for the full spec. + +The singleton follows structurally: because `UserInputController` +is always the sink, there is no "create overlay, route to it, +destroy on dismiss" lifecycle. `show()`/`hide()` are state +changes on the singleton, not routing changes. + +--- + +## 3. Component Layout + +### `keys_pressed` table + +Maintained by `Controller` (the global controller in +`controller.lua`). Updated unconditionally on every +`love.handlers.keypressed` and `love.handlers.keyreleased` +call — a live set of currently-held key names. + +Passed as a secondary argument to every `keypressed` and +`textinput` callback flowing downstream. Downstream consumers +receive a read-only proxy (iterator only), not the table +directly, to prevent tampering. Replaces the ad-hoc +`Key.ctrl()` / `Key.shift()` calls scattered across handlers. +Makes combo serialisation (`"ctrl+s"`) deterministic without +per-handler modifier checks. + +### `UserInputController` singleton + +Created once at framework startup (lazily; no allocation +until first use). Never destroyed. Activation state changes +via `show()`/`hide()` — no routing changes required. +`love.state.user_input` is set to the singleton on `show()` +and to `nil` on `hide()`, preserving the existing rendering +gate in `UserInputView` without code changes. + +Projects and the REPL/editor interact with it only through +the `compy` namespace API. No direct references to the +controller object from project code. + +The `oneshot` flag (a `UserInputModel` field, `userInputModel.lua:15,49`) +is deleted in M6, once both its jobs are covered: activation by +`show()`/`hide()` and submit by `framework_handlers['return']` in +`ProjectController`. Until M6 it stays and continues to drive +submit exactly as today. At M6, deletion touches `userInputModel.lua` +(field removal) and `userInputController.lua` (submit-path code +that reads `self.model.oneshot`). + +### `ProjectController` + +New controller, a sibling to `ConsoleController` and +`EditorController`. Active when `app_state = 'running'` or +`'project_open'`. Owns `keypressed` and `textinput` handling +for all project states, whether or not the singleton is +currently visible. + +Implements the three-level dispatch (§4). On project stop, +resets `compy.input.handlers` and all project callbacks via +`stop_project_run` / `clear_user_handlers` +(`consoleController.lua:860–868`). Also auto-provisions +`compy.input.on_key_pressed` for legacy projects via the heuristic +described in §2 and §6. + +### `compy` API additions + +| Name | Kind | Description | +|---|---|---| +| `compy.input.show(config)` | function | Activate singleton with given config | +| `compy.input.hide()` | function | Deactivate singleton silently | +| `compy.input.configure(config)` | function | Live-update config fields | +| `compy.input.clear()` | function | Clear input content without hiding | +| `compy.input.handlers` | table | Project-registered combo → function map | +| `compy.input.on_key_pressed` | callback | Generic keypressed callback (fires for all keys; default = sink) | +| `compy.input.on_text_entered` | callback | Character input callback (default = textinput sink) | +| `compy.input.before_submit` | callback | Hook before framework evaluates | +| `compy.input.after_submit` | callback | Hook after evaluation (receives the result) | +| `compy.input.before_cancel` | callback | Hook before framework dismisses | +| `compy.input.after_cancel` | callback | Hook after dismissal | +| `compy.input.on_limit_reached` | callback | Cursor hit input boundary | +| `compy.input.get_cursor()` | function | Query cursor position while active; returns `line, col` (2D, 1-based source-line); returns `nil` when hidden | +| `compy.input.set_cursor(line, col)` | function | Set cursor position while active; no-op when hidden | +| `compy.input.set_text(text [, keep_cursor])` | function | Replace text content while active (live write; exception to `configure()` text-immutability) | + +--- + +## 4. Three-Level Dispatch + +`ProjectController:keypressed` implements three tiers: + +``` +framework_handlers[combo] non-overridable structural keys: + | Enter, Escape live here. + ↓ (if not consumed) +compy.input.handlers[combo] project-registered per-combo + | handlers; return truthy to consume + ↓ (if not consumed) +compy.input.on_key_pressed(k, keys, isrepeat) generic catchall callback. + Default value is the text-editing + sink (UserInputController:keypressed). + Assigning a function replaces the + default; no tier exists below it. +``` + +`compy.input.handlers` entries return truthy to consume (stop the +chain; sink does not run). `compy.input.on_key_pressed` has no +separate tier below it — its default value *is* the sink. +A project that overrides `compy.input.on_key_pressed` replaces the +default entirely. Note: if the default is replaced, +`on_limit_reached` no longer fires (it originates in the +keypressed sink); the spec notes this. + +The textinput path follows the **same principle** as keypressed, +not a different one: `compy.input.on_text_entered`'s default value +is the textinput sink (`UserInputController:textinput`), and +assigning a function replaces it exactly as overriding +`on_key_pressed` replaces the keypressed sink. The only structural +difference is that the textinput channel has no combo tier above +it (characters are not combos); the default-sink/override +semantics are identical. + +The before/after submit/cancel chains (§5) use call-order, +not return values, for ordering; they are not subject to the +"consumed" mechanism. + +### Shared dispatch function + +All three controller branches eventually will share one dispatch +implementation written once. `ProjectController` uses it first; +`ConsoleController` and `EditorController` migrate to it when ready. + +```lua +dispatch(k, keys_pressed, + framework_handlers, handlers, callback) +``` + +The `callback` argument defaults to the text-editing sink; when +a project assigns `compy.input.on_key_pressed`, that function becomes +the `callback`. + +### `compy.input.handlers` combo format + +Combo strings use **modifier-first** ordering by fixed +precedence (`ctrl`, `alt`, `shift`, `gui`) followed by the +triggering key, joined with `+`. Modifier names are +**generic** (l/r folded): `ctrl` not `lctrl`/`rctrl`, so +registering `"ctrl+s"` catches either control key. The +`keys_pressed` table retains precise LÖVE key names; only combo +serialisation folds. Examples: `"ctrl+s"`, `"alt+shift+f4"`, +`"escape"`. + +`compy.input.handlers` is **metatable-backed**: `__newindex` +normalises the registered key to canonical form on assignment +(`compy.input.handlers['Ctrl+S'] = fn` is stored as +`compy.input.handlers['ctrl+s']`). Dispatch uses an **overloadable +matcher** with an exact canonical match default (O(1)); the +matcher is the marked extension seam for future glob/prefix +matching and is project-overloadable. + +--- + +## 5. Enter and Escape Handling + +Both keys are `framework_handlers` entries in +`ProjectController` — non-overridable from project space +but extensible via named callback chains. + +**Enter:** + +``` +framework_handlers['return']: + before_submit(keys_pressed) + → submit: model:evaluate() → push 'userinput' + → after_submit(result) +``` + +Shift+Enter passes through to the sink and inserts a +newline (existing behaviour preserved). + +**Escape:** + +``` +framework_handlers['escape']: + before_cancel(keys_pressed) + → cancel: model:cancel() → push 'userinput' → hide singleton + → after_cancel() +``` + +This resolves the current limitation where Escape clears +input content but does not dismiss the overlay. In the new +model, the framework's `cancel` step pushes `'userinput'` +unconditionally; `after_cancel` gives the project an +observation point. + +--- + +## 6. Legacy API Removal + +The legacy text-input globals — `input_text()`, `input_code()`, +`validated_input()`, `user_input()`, and `write_to_input()` — +are **removed**, not wrapped as facades (D-1 discarded by +stakeholders; see `input.md` round 1 and `decisions.md` D-1). +There is no backward-compatibility layer, no deprecation shim, +and no `strict_input` flag. The reftable / `is_empty()` polling +idiom is removed with them; the new API is callback-based +(`after_submit(result)` is the submit observation point that +replaces the polled reftable). + +The in-repo examples that use these functions are migrated to +`compy.input.*` (roadmap M8). `write_to_input`'s one consumer +(`tixy`) moves to `compy.input.set_text`. Because migration needs +the full `compy.input.*` surface (config with validator/highlighter, +the callback chain, `set_text`/cursor), removal and migration +land together as the last milestone, after M7. + +`love.state.user_input` is set on `compy.input.show()` and cleared +on `compy.input.hide()`, exactly as for any new-API caller — there +is no separate legacy path setting it. + +The break is bounded to text input. Native keyboard handling is +a separate surface and is unaffected — see below. + +### Native handler coexistence + +Projects that define native `love.keypressed`/`textinput` +(without any `compy.*` surfaces) are handled transparently: +`ProjectController` auto-provisions `compy.input.on_key_pressed` as +a lifecycle-split wrapper. When the singleton is visible, the +wrapper routes to the text-editing sink; when hidden, it routes +to the project's native handler. This reproduces today's gated +behaviour with zero example changes. See §2 for the full +heuristic and transition diagnostics description. + +--- + +## 7. Implementation Order and Migration Path + +The six sections of `notes/solution_sketch.md` are listed +in implementation-dependency order. Each section can be +completed and verified before the next begins: + +| Step | What it delivers | Test before next step | +|---|---|---| +| 1. `keys_pressed` table | Live modifier state; combo serialisation | No behaviour change; existing tests pass | +| 2. Singleton extraction | Widget created at startup | Existing tests pass; no allocation change visible | +| 3. ProjectController + gate removal | New routing; existing behaviour preserved via sink | Overlay input works as before; project key events now routable | +| 4. Three-level dispatch | `compy.input.handlers`, `compy.input.on_key_pressed` | Handler registration and bubbling work | +| 5. Before/after chains | Submit/cancel callbacks; Escape dismisses | Named hooks fire; Escape limitation resolved | +| 6. Legacy removal + example migration | Legacy text-input globals deleted; examples on the new API | Priority examples (tixy, balloons) run; legacy globals gone | + +Steps 1→2 are behaviour-neutral infrastructure. Step 3 (the +gate removal) needs only Step 2. Steps 4 and 5 each build on +Step 3 independently. Step 6 (legacy removal + migration) comes +last, because migrating the examples needs the full new surface +(callbacks from Steps 4–5 and the cursor/`set_text` surface). +The legacy text-input globals are not built as facades at any +step — they are simply removed once the examples no longer need +them (D-1 discarded; see §6). Note: the roadmap milestones +(`roadmap.md`) keep the cursor/`set_text` surface as a separate +M7 and the legacy removal as M8, so the milestone numbering is +M1–M8; these six implementation steps are the coarser +dependency grouping. + +The console and editor migration (Step 6 of the +`solution_sketch.md` description) is a clean follow-on: +it replaces if-chains in `ConsoleController` and +`EditorController` with handler registrations; the +underlying methods (`evaluate_input`, `_handle_submit`, +history navigation, mode switches) are unchanged. No step +of the current feature scope requires the console or editor +to be migrated, and no step of the migration requires the +current feature to be partially complete. + +### FR-11/FR-12 Coverage Walkthrough + +The following maps the console REPL and editor key patterns +onto the new API, fulfilling D-7's walkthrough promise +(full analysis in `notes/editor_repl_input.md`). + +**Console REPL (FR-11):** + +| Action | Maps to | +|---|---| +| Enter → evaluate and submit | `framework_handlers['return']` (evaluate + push `'userinput'` + `after_submit`) | +| Up/Down at history boundary | limit signal from sink → `compy.input.on_limit_reached(direction)` → history navigation handler | +| Ctrl+L clear terminal | `compy.input.handlers['ctrl+l'] = function() clear_terminal() return true end` | +| Error display | sink (unchanged — model handles it internally) | +| Escape → clear input line | on migration, `ConsoleController` registers `framework_handlers['escape']` = clear-line; `ProjectController`'s dismiss-Escape is per-controller and never clobbers it | + +**Editor input (FR-12):** + +| Action | Maps to | +|---|---| +| Enter → submit block | `framework_handlers['return']` | +| Escape → load / cancel edit | `framework_handlers['escape']` + `before_cancel` | +| Up/Down at boundary → block navigation | `compy.input.on_limit_reached(direction)` | +| Ctrl+M / Ctrl+F mode switches | `compy.input.handlers['ctrl+m']` / `compy.input.handlers['ctrl+f']` | +| Load block text into input | `compy.input.set_text(block_text)` (FR-10 surface, D-8) | +| Read cursor position | `compy.input.get_cursor()` → `line, col` (FR-8 surface, D-8) | +| Set cursor position | `compy.input.set_cursor(line, col)` (FR-9 surface, D-8) | + +The mapping is mechanical: if-chains become handler +registrations; underlying model methods are unchanged. +FR-12 requires the D-8 cursor surface (`compy.input.get_cursor` / +`compy.input.set_cursor`); without it, programmatic cursor +management is not expressible via the public API. diff --git a/doc/development/wip/77-new-input-api/input.md b/doc/development/wip/77-new-input-api/input.md new file mode 100644 index 00000000..44df13fc --- /dev/null +++ b/doc/development/wip/77-new-input-api/input.md @@ -0,0 +1,98 @@ +# Feature #77 — Stakeholder Input + +Verbatim. Source: original ticket and stakeholder clarification. + +--- + +## Original ticket + +A better REPL model, more consistent with the rest of the +controls, with callbacks. I.e. setting up an edit area with +an optional non-empty content, syntax highlighter and input +validator and receiving callbacks on the user entering +something on it. + +## Owner clarification + +1. It is certainly not an exhaustive feature list. Basically, + what I would like to see is an API that allows for an easy + implementation of interfaces that are similar to either the + command console or the editor. Ideally, these should also be + re-implemented using the same API. What I'd like to see in + addition to features mentioned in the ticket are callbacks + for keys pressed together with the Ctrl key or keys that do + not insert or remove a symbol in the edit area. + Unfortunately, function keys (the ones in the top row of the + keyboard) only work with external keyboard, the internal ones + are hijacked by the OS. Another thing, I'd like to see a + callback for is cursor movements that "hit a wall", trying + to move the cursor past the beginning or the end of the edit + area and, of course, entering a line. Calls should be + provided for setting up an edit area (with an optional + initial text, cursor position, highlighter and verifier) and + for removing the edit area, for querying and changing the + cursor's position, and changing the text. + + (I might have forgotten something, but see the principles + spelled out in the beginning) + +2. Games and other examples already in development or even + ready (like tixy), REPL dialogs, text-based adventure games, + etc. The biggest problem with the current API is that it is + inconsistent with the rest of love, handling keyboard events + in parallel to editing is a PITA and it is not easy to hide + or show the input area (see your struggles in sapper). + + +## FEEDBACK AFTER FIRST ITERATION (ongoing) + +### D-1 discarded: no backward compatibility required + +#### Citation 1 + +Stakeholder1: + +Is there any benefit to maintaining backwards compatibility of the input API? We know all the software that uses it and it all must be updated anyway (and will greatly benefit from doing so). + + +In this case [context: D-1], I disagree. The examples are examples, showing how to write code. Leaving legacy API use in a release defeats the very purpose. There are not that many of them and each and every one would be radically shorter and simpler after updating to the use of the new API. + +The only examples where there is a time pressure on the release are the ones that we might want to use , such as maze and balloons. I am very fond of tixy as a showcase, so that might also need updating. But that's it. For all the other examples, we can just exclude them from the next release, if we don't have the time to convert them to the new API. Even tixy can be left out, if absolutely necessary. + +Some of the examples, such as basic REPL games, are actually trivial to convert (or even rewrite from scratch). + +So, I am strongly in favor of getting rid of the legacy API, ASAP. + + +Stakeholder 2: +We are before 1.0, there's absolutely no need to maintain backwards compatibility + +Stakeholder 3: +We really shouldn't go from having an almost-good ... experience to not having any, even temporarily, because we don't know how long updating all programs will actually take and how many crippling bugs we'll discover in the process. + +Stakeholder 1: > S3 + +I don't think that it reduces any risk. I stronly disagree. + +S2: +the old releases will still exist, the existing experience is not deleted + +S3: +Right now the games that can be used and enhance ... experience are: <6 games not in repo>, tixy + +S1: +Only <...> and tixy have text inputs from that list. + +S3: +This won't break all keyboard input, only text fields? + +S1: +You don't upgrade before testing the new version. + +S1: +Only text fields. + +S3: +Okay, then it's less of a problem. + +(CONSENSUS REACHED: D-1 DISCARDED) diff --git a/doc/development/wip/77-new-input-api/notes/concerns.md b/doc/development/wip/77-new-input-api/notes/concerns.md new file mode 100644 index 00000000..b48f9608 --- /dev/null +++ b/doc/development/wip/77-new-input-api/notes/concerns.md @@ -0,0 +1,114 @@ +# Notes — Concerns and Risks + +Technical concerns and risks identified during review of the +existing architecture and earlier draft documents. Not yet +triaged — some may be resolved by the design, others may need +explicit decisions. + +--- + +## Primary gap: keypressed events are fully consumed + +`UserInputController:keypressed` currently handles all key +events it receives and returns nothing to indicate whether an +event was consumed or passed through. The calling context +(console or project) has no mechanism to receive key events +that the widget did not handle. + +This is the root technical blocker for FR-6 (non-character key +notifications) and for the editor re-implementability target +(FR-12). Any solution must decide where the forwarding +responsibility lives and whether it is opt-in or always-on. + +Source: `src/controller/userInputController.lua`; +confirmed in old `assessment.md §3`. + +--- + +## Singleton lifecycle: no mid-session update path + +The current `love.state.user_input` guard (`if ... then return +end`) means a second call to any input-creation function while +one is already active silently does nothing. There is no +supported path for: +- changing the prompt label +- swapping the highlighter or validator +- resetting content and cursor to new values + +without first tearing down the widget. This is a concrete +limitation (see also `notes/requirements.md` — balloons pain +point) that the persistent lifecycle design idea directly +addresses. + +--- + +## Wall-hit in editor context: line numbers diverge + +The editor works in two coordinate systems: source line numbers +(as stored in the file) and apparent/wrapped line numbers (what +the view renders). These diverge whenever any block has lines +longer than the screen width. + +`EditorController` checks `inputView:is_at_limit('up'/'down')` +against the wrapped/apparent coordinate system, not the source +one. A general-purpose wall-hit callback (FR-7) would need to +clarify which coordinate system its direction applies to — or +the callback would need to fire in terms meaningful to the +consumer, which may differ between console and editor contexts. + +The old `assessment.md` flagged this as a reason to be cautious +about any speculations in the editor direction for this +callback. + +--- + +## Intercepted callbacks: no consolidated list + +`controller.lua` intercepts a set of LÖVE2D event callbacks +before they reach project handlers or `UserInputController`. +The full list is not documented in one place, making it hard to +reason about what a project can and cannot intercept without +reviewing the file in full. + +This is a documentation gap, not a blocker, but it is worth +addressing before finalising the new API's event model — +knowing exactly which events are intercepted helps ensure +nothing is silently swallowed. + +Reference: `src/controller/controller.lua:520+` for the global +shortcut intercepts; `love.state.user_input` check in the +keypressed handler for the overlay intercept. + +--- + +## Convention adoption scope: all three contexts or one? + +Any mechanism that allows key events to propagate out of +`UserInputController` implies a convention that every consuming +context must implement: `ConsoleController`, `EditorController`, +and the project overlay path would all need to handle (or +explicitly ignore) forwarded events in a consistent way. + +This is not a change to one class — it is a cross-cutting +convention commitment. If only one context is updated initially, +the API would behave differently depending on which context the +widget is running in, which breaks the consistency goal +(FR-11/FR-12). Worth flagging as a scope risk: the work may be +wider than it first appears. + +--- + +## Text + modifier co-occurrence: undefined behaviour + +The requirements and current architecture do not define what +happens when a text character arrives simultaneously with a +modifier key held. LÖVE2D delivers these as separate events +(`love.textinput` for the character, `love.keypressed` for the +key), but from the project's perspective the user made one +gesture. + +If the new API emits both a text-entered notification and a +key-combo notification for the same gesture, a project could +receive confusing double callbacks. If it emits neither (because +both paths reject it), a gesture is silently dropped. This +needs an explicit policy decision. diff --git a/doc/development/wip/77-new-input-api/notes/context_differences.md b/doc/development/wip/77-new-input-api/notes/context_differences.md new file mode 100644 index 00000000..e48c613f --- /dev/null +++ b/doc/development/wip/77-new-input-api/notes/context_differences.md @@ -0,0 +1,59 @@ +# Notes — REPL, Editor, and Overlay: Context Differences + +Comparison of the three contexts that use the input widget. +Informs the D-2 singleton design and the reconfiguration +logic that `design.md` will need to specify. + +--- + +## Current ownership model + +The REPL and editor already share one persistent widget +(`ConsoleModel.input`). The project overlay is the outlier: +`input_text()` / `input_code()` / `validated_input()` create +a separate fresh triad per call, stored in +`love.state.user_input`. This separation is pragmatic +(simpler to add as a feature on top of the existing console +input) rather than a principled design choice. + +--- + +## Widget configuration per context + +The three contexts are mutually exclusive (`app_state` +enforces this), so sharing one widget is safe — the +differences are all runtime-configurable properties on +the model: + +| Property | REPL | Editor | Project overlay | +|---|---|---|---| +| Evaluator | `LuaEval` | `LuaEditorEval` | `InputEvalText`, `InputEvalLua`, or `ValidatedTextEval` | +| `oneshot` flag | false | false | true | +| History | enabled | disabled | disabled | +| Selection | enabled | disabled | enabled | +| Submit path | `ConsoleController:keypressed` → `evaluate_input()` | `EditorController:keypressed` → `_handle_submit()` | `UserInputController:keypressed` (oneshot path) | + +The `oneshot` flag is the most behaviourally significant +difference: it moves the Enter submit path from the +enclosing controller into `UserInputController:keypressed` +itself, and on success pushes a `userinput` event to clear +the overlay. + +`LuaEditorEval` adds a 64-character line length validator +on top of `LuaEval` — enforces the code convention live +as the user types. + +--- + +## Implication for D-2 singleton + +Under the singleton design, each context switch requires an +explicit reconfiguration call (evaluator, oneshot, history, +selection). Today these are set at construction time. Moving +to runtime configuration is a contained change — all +properties are already mutable fields on the model. + +The REPL → editor transition already implicitly does this +(the editor borrows the console's widget and sets +`disable_selection = true`). The singleton makes this +pattern explicit and extends it to the overlay context. diff --git a/doc/development/wip/77-new-input-api/notes/decisions.md b/doc/development/wip/77-new-input-api/notes/decisions.md new file mode 100644 index 00000000..1622c3db --- /dev/null +++ b/doc/development/wip/77-new-input-api/notes/decisions.md @@ -0,0 +1,924 @@ +# Notes — Decision Analysis + +Analysis of the suggestions added to `summaries/decisions.md`. +Sanity checks, consistency observations, and open sub-questions +raised by each suggestion. Intended to inform finalising the +decisions before design begins. + +--- + +## D-1 · Backward compatibility + +**Suggestion:** facades over new API, deprecation warnings in +debug mode. + +**Assessment:** directionally sound and consistent with the +existing `love.debug` flag pattern in the codebase. One +non-trivial constraint: the old API is polling-based (returns +a reftable) while the new API will be callback-based. A facade +is not just a rename — it has to bridge two paradigms. Each +old function would need to internally wire a submit callback +that fills a reftable, then return that reftable. Cancel +handling would also need a policy (fill with nil? stay empty +forever?). + +Additionally, `user_input()` and `input_text()` are currently +two separate steps — the first creates the reftable, the +second creates the overlay using it. Facades would need to +preserve or paper over this two-step structure. + +Feasible, but the facade is heavier than it looks. Worth +designing explicitly rather than treating as a trivial wrapper. +The singleton direction in D-2 may simplify this considerably. + +** ARCHITECT RESPONSE 1 ** + +**Idea**: apps still can receive the reftable and poll it until they switch. +It just will be filled in a different way behind the curtains (question: how its done now, conceptually?) +user_input() can still create the reftable, +and input_text() will show/decorate/configure the terminal singleton, +and reset callbacks in a way that populates the reftable. +Both will emit tons of warnings and may break instead if 'deprecation=false' flag is set globally + +** AI RESPONSE 1 ** + +*Response:* Currently: `user_input()` allocates a reftable and +stores it in a closure variable `input_ref`. `input_text()` then +creates the MVC triad and passes `input_ref` as the `result` +argument to `UserInputController`. On Enter, `UserInputController` +calls `res(t)` — which calls the reftable as a function, storing +the submitted value. The project polls `r:is_empty()` until that +call happens. So the reftable is already a one-shot callback cell; +the facade just makes the submit callback visible rather than +internal. The two-step structure maps cleanly onto singleton +configure+show: `user_input()` allocates the reftable, +`input_text()` configures the singleton and registers the submit +callback that fills it. Viable. + +On `deprecation=false`: the semantics feel inverted — "false" +typically suppresses rather than enforces. Probably better as a +`strict_input = true` flag that opts the project into hard errors +instead of warnings, which is a more explicit choice. + +** ARCHITECT RESPONSE 2 ** + +If you think its resolved now, write down the resolution summary, and mark chapter as [RESOLVED] +Otherwise, call out outstanding gaps and dialogue will continue + +** AI RESPONSE 2 ** + +**Resolution:** The reftable facade works because the reftable +is already a one-shot callback cell — `user_input()` allocates +it, `input_text()` configures the singleton and wires a submit +callback that fills it. Cancel policy: on Escape, the reftable +simply stays empty; project code that polls `r:is_empty()` +handles this as a no-result cycle, which is the same behaviour +as today. The `strict_input = true` flag replaces `deprecation=false` +for projects that want hard errors. + +No unresolved gaps remain. + +## D-1 [RESOLVED] + +--- + +## D-2 · Second setup call + +**Two suggestions offered — they are competing architectures, +not alternatives within the same model.** + +Suggestion 1 keeps project code as the initiator of input +sessions and adds a `force=true` escape hatch for replacement. +Suggestion 2 proposes a framework-managed singleton that +projects configure rather than create. + +**On `force=true`:** a flag that changes call semantics based +on a boolean argument is the pattern `agents/rules.md` +discourages ("No C accent" — prefer explicit API over +dispatch-by-argument). More practically: if `force` triggers +destroy-and-recreate it conflicts with NFR-1; if it triggers +in-place reconfiguration it is essentially what the singleton +approach offers anyway, through a less clean surface. + +**On the singleton (suggestion 2, point 3):** this is the +more significant idea and has the widest architectural reach. +If the input widget is framework-managed, the "second setup +call" problem dissolves — there is no setup call, only a +configure/show call on a widget that already exists. This also +directly satisfies NFR-1 with no special effort. The D-1 +facades become thin wrappers that call configure+show rather +than create. + +**Recommendation:** treat the singleton vs project-created +question as the primary axis of D-2. `force=true` should not +appear in the final API regardless of which direction is +chosen. + +**Open sub-question:** if the singleton is framework-managed, +when is it created (application start vs first use) and where +does it live — inside `ConsoleController` as now, or as a +top-level framework object? + +** ARCHITECT RESPONSE 1 ** + +**Idea**: going totally for a framework-managed singleton, +lazy creation, never destroyed, framework level. +User's code simply shows/decorates/reconfigures it. + +** AI RESPONSE 1 ** + +*Response:* Agreed — this is the cleanest resolution and the +rest of the decisions flow from it naturally. Lazy creation is +the right call: no allocation cost if a project never uses input. +"Never destroyed" is already implicit in how the console's own +input widget works today. + +The one structural implication worth flagging: currently +`UserInputModel`/`Controller`/`View` are owned by +`ConsoleController`. Promoting them to framework level means they +probably need to be created in `main.lua` or `love.load` and +passed in, rather than constructed inside `ConsoleController`. +That is a contained change but touches the wiring. How the +singleton is exposed to project code (via the `compy` namespace, +directly, or through the facade functions) is a design detail — +but the direction is clear. + +** ARCHITECT RESPONSE 2 ** + +**Concern**: in new API MVC is not exposed to projects at all, but in REPL and editor it played a crucial role. +Do we have a fundamental problem here? Requirements already mentioned the ability to reimplement REPL/editor to use new API, +but at least editor (not sure about REPL) still has lots of logic bounded to buffer, and maybe to view too. +So... we should keep the ability to setup custom MVC triade which would be wired up almost as now -- except that invocation chain will be altered plus editor/REPL will be able to better react to events flow (via handlers/callbacks which may then trigger MVC methods as they seem reasonable)? Please, analyze this aspect. + +** AI RESPONSE 2 ** + +*Response:* No fundamental problem. The layers need to be +distinguished clearly: + +**The input widget** (UserInputModel/Controller/View) becomes the +singleton. It handles text entry, cursor, selection, history, +evaluation. Framework code (ConsoleController, EditorController) +continues to hold direct references to it — nothing prevents +that. They are framework code, not project code. What changes is +that they no longer own its creation. The editor configures the +singleton for its context (no history, LuaEditorEval, disable +selection) when it becomes active, rather than constructing a +fresh instance. + +**The editor's own model** (BufferModel, blocks, semantic +analysis, buffer stack) is a completely separate layer that is +not touched by this feature. The editor uses the input widget as +a text entry strip at the bottom of the screen; the block display +above is independent. None of the editor's buffer machinery goes +near the new API. + +**FR-11/FR-12 (re-implementability)** means the event API is +expressive enough that console/editor *could* be re-implemented +using it — not that they must be. In practice the console could +express its Enter→evaluate and limit→history as framework_handlers +entries. The editor could express its Ctrl+M/Ctrl+F/Escape as +handler registrations. But this migration is optional and deferred. +The handlers/callbacks topology is *additive* — framework code can +adopt it gradually. + +**Settled**: no exposure of MVC to project code is needed or +appropriate. Framework code retains direct MVC access unchanged. + +** ARCHITECT RESPONSE 3 ** + +Is it really settled ? Outline clearly the provisioning/reconfiguration path and summarize how ownership issue is resolved. +If you confirm its settled, write down the resolution and mark chapter as [RESOLVED] + +** AI RESPONSE 3 ** + +**Provisioning and reconfiguration path:** +The singleton is created at `love.load` (or lazily on first use) +in `main.lua` and passed as a reference to `ConsoleController` +and `EditorController` at construction. Neither controller owns +it. When a project calls `input_text("prompt")`, the facade calls +`show({prompt=..., evaluator=InputEvalText})` on the singleton, +which is already allocated. On submit the framework fires the +on_submit callback (which fills the reftable in the facade path, +or calls project code directly in the new API path). On project +stop, `compy.handlers` is cleared via the same mechanism as +`evacuate_required`. The singleton itself is hidden but never +destroyed. + +**Ownership resolution:** the singleton lives at framework +level; ConsoleController and EditorController hold references +to it. They configure it for their context when they become +active (e.g., the editor sets LuaEditorEval, disables history) +and restore or clear configuration when they leave. Project code +never touches the MVC triad — it goes through the `compy` +namespace API only. Framework code retains direct access, +unchanged. + +## D-2 [RESOLVED] + +--- + +## D-3 · Key event granularity + +**Suggestion:** one `compy.key_combo` callback, fires only +when at least one modifier key is present. + +**Gap:** the suggestion covers modifier+anything but leaves +non-character keys WITHOUT a modifier in a grey zone — +function keys, Delete, Home, End when unhandled. No key_combo +fires (no modifier), no textinput fires (no character). This +may be intentional given the target device (F-keys rarely +available on the built-in keyboard) but should be stated +explicitly rather than left implicit. + +**On the preprocessing point:** "framework preprocessing +decides if API callback should be triggered" reads as: the +framework handles the key first (navigation, editing), and +only if not consumed does the callback fire. This is the +forwarding/bubbling mechanism from `notes/design.md`. The +"debug log if skipped" is useful and consistent with the +default noop + debug log pattern appearing across D-3 and D-4. + +**Interaction with D-6:** together D-3 and D-6 implicitly +partition the event space: +- character input (± modifier) → `on_text_entered(text, mods)` +- modifier + non-character → `on_key_combo(keys)` +- non-modifier non-character → currently unaddressed + +This partition is coherent. The third case should either get +an explicit surface or be explicitly declared out of scope. + +** ARCHITECT RESPONSE 1 ** + +**Idea**: lets expand combo idea into all non-characters +(modifier or not). Simply have *two* callbacks routed, hierarchically + +Internally, maintain a global table of all keys, flagging those in pressed state +Than, on top of that build custom callbacks + +It seems I have a proto-design below which was the primary goal of the ticket :) +``` +nonempty = function(t) ... end -- equivalent of Ruby's .compact() +compy.keys_pressed = {} -- or prepopuate with all possible keys +compy.framework_handlers = { -- non-overridable handlers for specific combos + 'escape' = ..., + 'enter' = ..., + ['Ctrl','enter'] = ..., +} +compy.handlers = { -- overridable handlers for specific combos + +} +compy.commands = { + ['^exit',['Ctrl']] = ... -- handler for specific combination of text and non-input chars + ['^save',] = .. -- some command +} + +love.keypressed(k) + compy.keys_pressed[k]=true -- or timestamp + compy._on_key_pressed(k, nonempty(compy.keys_pressed)) + +love.keyreleased(k) + compy.keys_pressed[k]=false + compy._on_key_stroke(k, nonempty(compy.keys_pressed)) + +love.textinput(txt) + compy._on_textinput(txt, nonempty(compy.keys_pressed)) + +-- new callback which exposes new key and lists already pressed +compy._on_key_pressed(k, pressed) + -- 1. check framework table of handlers (mapped by specific combos) -- proceed to handler if found. bubble or not depending on what handler defines + -- 2. check project table of handlers (mapped by specific combos) -- in the same way + -- 3. call compy.on_key_pressed(...) -- by default a noop with log, overloadable by project + +compy._on_key_stroke(k, pressed) + -- same logic as above + +compy._on_textinput(txt, pressed) + local mods = -- any noncharacter keys from pressed table + local proceed = compy.commands([txt,mods])() -- by default table returns noop without log that returns true + if proceed then -- tolerable violation of 'no-arrows-dispatching' principle + compy.ontextinput(txt, mods) -- overloadable by project by default noop with log message + end +end +``` + +** AI EVALUATION RESPONSE 1 ** + +*Response:* This resolves D-3's gap cleanly — non-modifier +non-character keys now have a surface via `_on_key_pressed` and +`_on_key_stroke`. A few observations: + +`keys_pressed[k] = false` on release leaves dead entries that +`nonempty` has to filter. Setting to `nil` instead removes them +outright, which is cleaner and avoids the table growing +unboundedly over a long session. + +The `_on_key_stroke` (keyreleased) callback is a useful +addition — it lets projects distinguish tap from hold, and means +chord-style combos (Ctrl+S press, then release in any order) can +be detected cleanly. Worth being explicit about its semantics: +it fires with the keys still pressed at release time, minus the +released key itself. + +The `compy.commands` table with composite keys like +`['^exit', ['Ctrl']]`: Lua table keys use reference equality for +tables, so `['Ctrl']` as a key won't match a different `['Ctrl']` +table at lookup time. In practice this needs a string +serialisation (e.g. `'Ctrl+exit'` or a sorted join of key names) +or a lookup helper. Worth addressing in the design spec. + +Three-level dispatch (framework_handlers → handlers → generic +callback) is coherent and the right shape. The ordering question +is significant: does `framework_handlers` run before or after +`handlers`? If before, projects can observe but not intercept +Escape/Enter (framework always wins). If after, projects can +intercept first, then the framework runs — closer to +`preventDefault()`. The proto-design implies framework-first but +D-4's "does not necessarily prevent framework processing" suggests +framework-always. Worth making this explicit. + +** ARCHITECT RESPONSE 2 ** + +**Thought**: Regarding nil, I simply am not sure what would be better from GC-perspective. Small table constantly changing its shape, or big table with false/true flags switched constantly? (most of them being false thoguh). THe advantage of using 'nil' also is that we do not have to postprocess table, filtering it. Probably "nil" is clean resolution -- it addds *some* overhead on table size fluctuating, but if we keep big table -- we will anyway have more overhead on extracting pressed keys from it, unless we pass full keymap and let project code check it (which also would be dangerous because project code may decide to alter the values... hmm...) + +**Remark**: on string serialization, yes -- we can order and serialize. That's what already is done in different places of codebase (I saw conventions like "C-escape" and so -- maybe even coming up from love2d) + +**Question**: is actual architecture of love2d native callbacks compatible with the suggested design? + +**Request**: when writing down conclusions, end up with an opinion -- is this decision/design point resolved/settled or requires further dialogue? + + +**AI EVALUATION RESPONSE 2**: + +**On nil vs false:** `nil` is correct. Setting a table entry to +nil in Lua removes the slot cleanly — no filtering needed, no +growth over time. The GC cost of a small table gaining and losing +entries on keypressed/keyreleased events is negligible; the +keyboard's key set is bounded (~100 keys at most). The only +marginal argument for `false` was avoiding table resize, but Lua +tables don't shrink on nil assignment anyway — they just mark the +slot free for reuse. `nil` wins on cleanliness alone. + +**On string serialisation:** the codebase already has conventions +like "lctrl", "escape", "up" from LÖVE2D's key name strings. +A serialisation such as sorted key names joined with `+` (e.g. +`"lctrl+s"`) is idiomatic and straightforward to implement as a +small helper. No new invention needed. + +**On LÖVE2D compatibility:** fully compatible. `controller.lua` +already overrides `handlers.keypressed`, `handlers.keyreleased`, +`handlers.textinput`. The proposed dispatch (`keys_pressed` update +→ `_on_key_pressed`) slots into those handlers as an additional +step. The existing global shortcuts (Ctrl+Q, Ctrl+T etc.) become +entries in `framework_handlers` with the same effect. One useful +addition: LÖVE2D passes `isrepeat` as a third argument to +`keypressed` — worth threading through to `_on_key_pressed` for +projects that want to distinguish first press from held repeat. + +**Settlement status: largely settled.** Direction, structure, and +LÖVE2D compatibility are all confirmed. The remaining open point +(exact serialisation format for combo keys) is a design-spec +detail, not a blocking decision. This section can be marked +resolved pending that detail in `api_spec.md`.a + +** ARCHITECT RESPONSE 3 ** + +Write down a final resolution summary, marking the outstanding detail to be solved in spec level + +When done and if no new concerns arise, mark the section as [RESOLVED] + +** AI RESPONSE 3 ** + +**Resolution:** `keys_pressed` uses `nil` on release (not +`false`). Three LÖVE2D events feed the dispatch: +`love.keypressed` → `_on_key_pressed(k, pressed, isrepeat)`, +`love.keyreleased` → `_on_key_stroke(k, pressed)`, +`love.textinput` → `_on_textinput(txt, pressed)`. Three-level +dispatch per event: framework_handlers (structural, always run) +→ compy.handlers (project-registered, combo-specific) → +compy.on_key_pressed (generic overloadable callback, noop+log +by default). Project handlers run before framework handlers. +Combo keys are looked up by string-serialised sorted key names +(e.g. `"lctrl+s"`), consistent with LÖVE2D key name conventions. + +**Outstanding spec detail:** exact serialisation helper and +`isrepeat` threading — both are implementation decisions for +`api_spec.md`, not blocking. + +**See also:** `notes/event_routing.md` — full before/after +routing diagrams, concrete Ctrl+char example, and analysis +of the suppression mechanism (open design detail for +`design.md`). + +## D-3 [RESOLVED] + +--- + +## D-4 · Cancel notification + +**Suggestion:** dedicated cancel callback; chain hierarchy +`on_keypressed` → `on_escape` → `on_cancel`. + +**The chain idea is the most notable proposal across all seven +decisions.** It generalises cleanly: each level fires the next +by default, any level can be overridden to intercept. This is +the DOM-style bubbling from `notes/design.md`, applied +explicitly to the cancel path. + +**Consistency question:** if `on_escape → on_cancel` is a +chain, the symmetric form would be `on_enter → on_submit`. +That would make the model uniform: a general keypressed +handler at the top, specific key handlers in the middle, +semantic handlers at the bottom. Worth considering whether +the chain is specific to cancel or is the general callback +model shape. + +**Risk with `on_keypressed` at the top:** if a project +overrides it without delegating, Escape never reaches +`on_cancel` and the input area becomes undismissable. The +design needs a clear propagation convention — the return-value +approach from `notes/design.md` (true = consumed, nil/false = +propagate) is the natural answer here. + +**On "does not necessarily prevent framework processing":** +this states a specific policy: the framework's teardown runs +independently of the callback chain. Worth making this +explicit in the design rather than leaving it implicit. + +** ARCHITECT RESPONSE 1 ** + +**Idea**: see above about callbacks/handlers topology. +Callbacks are calls bound to input events (overridable and not) +Handlers are functions bound to specific combos. +Then the whole cancel/enter thing is configurable this way, including overloadable and non-overloadable options + +** AI RESPONSE 1 ** + +*Response:* Agreed — D-4 dissolves into the topology. The +terminology distinction is useful and worth formalising: +- **handler**: bound to a specific key combo; can block + propagation via return value +- **callback**: bound to an event class (any keypressed, any + text input); called after handler dispatch; overloadable + +`framework_handlers.escape` and `framework_handlers.enter` +represent the non-overridable defaults (teardown, submit). The +D-4 chain `on_keypressed → on_escape → on_cancel` maps onto: +`handlers.escape` (project override, runs first) → +`framework_handlers.escape` (always runs, unless suppressed). + +One thing still to resolve: can a project handler suppress +`framework_handlers`? The D-4 suggestion's "does not necessarily +prevent framework processing" implies no — the framework always +gets the last word on Escape/Enter. If that is the policy, the +return value from a project handler only controls whether the +generic callback fires, not whether the framework teardown runs. +That should be stated explicitly to avoid surprises. + +** ARCHITECT RESPONSE 2 ** + +**Idea**: lets' formulate it this way: topology of handlers/callbacks should be designed or augmented in a way that guarantees ability to define priority of framework/project handlers/callbacks in any way that suits (not by userspace configuration but in framework implementation level -- literally by the order of calls in default callbacks, the types of events/cakkbacks defined etc -- while staying within same paradigm and its tiny implementation). (backgroudn thought: it could be even 'before_cancel'/'cancel'/'after_cancel' if and when we fill like it -- with the framework implementation of handler explicitly ordered in a way that guarantees chosen preferences but can be altered whenever they change). + +** AI RESPONSE 2 ** + +The `before_cancel / cancel / after_cancel` framing is precisely +right and makes the propagation question from earlier moot. The +framework doesn't need a suppression protocol — it just places +its own teardown handler at `cancel`, exposes `before_cancel` for +project code that wants to run before teardown, and `after_cancel` +for cleanup after. The ordering is enforced by call order in the +framework's default implementations, not by a return-value +convention. A future change to preferences is a re-ordering of +calls, not an API change. + +This pattern generalises symmetrically: `before_submit / submit / +after_submit`, `before_limit / on_limit`, and so on. The framework +owns the middle point; projects hook onto the edges. Chains of +three named points per semantic event give enough flexibility +without inventing a new abstraction. + +The return-value bubbling convention is still useful for the +*generic* `on_key_pressed` callback (where the project signals +"I handled this, don't also fire the noop default"). It is not +needed for semantic events, which use the named chain instead. + +**Settlement status: settled.** The before/semantic/after naming +pattern and the principle that ordering is enforced by call +sequence (not configuration) are agreed. Named chain points per +event are a design-spec detail. + + +** ARCHITECT RESPONSE 3 ** + +Summarize resolution clearly and mark section as resolved + +** AI RESPONSE 3 ** + +**Resolution:** D-4 dissolves into the D-3 topology. For +semantic events (cancel, submit), the framework defines named +chain points: `before_cancel → cancel → after_cancel` (and +symmetrically for submit). The framework owns the middle point; +projects hook into before/after. Ordering is enforced by call +sequence in the framework's default handler implementations — +not by a return-value convention or configuration. This means +"framework always runs cancel teardown" is just a fact about +where in the chain the framework places its own handler. +Return-value propagation (true/nil) applies only to the generic +`on_key_pressed` callback, not to semantic events. Concrete +chain-point names are a spec detail. + +## D-4 [RESOLVED] + +--- + +## D-5 · Boundary definition + +**Suggestion:** two callbacks, first invokes second. + +**Sanity check:** `UserInputModel:is_at_limit` currently +detects the first/last LINE of the entire input, not the +first/last column of a line. There is no existing +end-of-current-line detection. Defining two levels would +require building the coarser-grained one from scratch. + +For the stated use cases (console history navigation, editor +block navigation), both want the whole-input boundary — the +level that `is_at_limit` already provides. The line-level +boundary would only matter for intra-multiline navigation, and +none of the current use cases require it. + +**Tentative concern:** the two-level chain may add complexity +without clear benefit for the known use cases. A single +`on_limit_reached(direction)` covering the whole-input +boundary may be sufficient. If a line-level boundary is +needed later, it can be added then. Worth confirming whether +the multiline/line-level scenario is actually anticipated +before committing to two callbacks. + +The chain structure is consistent with D-4's proposal. If +both levels are adopted, naming needs to be unambiguous about +which boundary each refers to. + +** ARCHITECT RESPONSE 1 ** + +**Idea**: this may require further elaboration but I see how this hook could be plugged into handlers/callbacks topology as a hook. +Just some callback will include check of current boundaries and emit callback 'on_limit_reached'. Also we cah decide that 'hook' is a terminological shortcut for non-blocking handler (that which will bubble up) + +** AI RESPONSE 1 ** + +*Response:* The terminology is clean and worth keeping: +**hook** = always bubbles, non-blocking observation point. +**handler** = can block propagation. + +`on_limit_reached` as a hook fits naturally: the vertical +movement logic in `UserInputController` already detects and +returns the limit signal. Instead of the console polling for +that return value, the topology fires the hook. Projects observe +it and react; the framework also observes it (for history +navigation in console mode). Neither needs to block the other. + +The concern about two levels (line boundary vs input boundary) +from the earlier analysis still applies: `is_at_limit` only +covers the whole-input boundary today. For the known use cases +that is sufficient. The hook can carry a `direction` argument and +the boundary level can be a later extension if needed. + +** ARCHITECT RESPONSE 2 ** +On terminology -- I'd consider a 'hook' to be a syntactic sugar -- function that is wrapped up in 'return True' (to support propagation). Therefore its not another independent class of callback but rather the particular flavour of handler + +On backwards-compatibility: we still can allow projects to poll this hook (supporting deprecation logic described omewhere above), but also suggest a cleaner way -- providing a custom hook triggered on pupose + +Boundary direction could be an argument worth adding from the start. Adding context such as 'boudary' level should be an opportunity (we reserve position argument for that), but specifics to be figured out later. For now projects would be advised to not use it or rely on it, but future designs may account for this ability. + +We also stick to KISS principle -- do not overdesign, but leave clear tiny extension points. + +When answering, let me know if this feature is settled now. + + +** AI RESPONSE 2 ** + +Agreed on all points. To consolidate: + +- **Hook as syntactic sugar**: hook = handler with implicit + propagation (`return true` or equivalent). Not a separate class, + just a naming convention for handlers that are defined to always + bubble. `on_limit_reached` is a hook by this definition. +- **Backwards-compat polling**: projects can still observe the + limit via the existing pattern if needed — the facade wires the + hook to also fill a reftable if the project uses the old API. +- **Signature**: `on_limit_reached(direction)` with `direction` + as 'up'/'down'. A reserved second positional argument for + future boundary-level granularity — not specified, not + documented in v1, just kept free. +- **KISS**: single whole-input boundary only. Line-level boundary + is a later extension point if a concrete use case arises. + +**Settlement status: settled.** + +** ARCHITECT RESPONSE 3 ** + +Double-check the settlement status. +Summarize resolution so that it could be undertood/interpreted without detailed context of the discussion above. Mark section as resolved. + + +** AI RESPONSE 3 ** + +**Resolution:** Single hook `on_limit_reached(direction)` where +direction is `'up'` or `'down'`. Covers whole-input boundary +only (matching `UserInputModel:is_at_limit`, which detects +first/last line of the entire input). A hook always propagates — +it is a handler with implicit bubble-up, so both project code +and framework code (console history navigation) can observe the +same event independently. A second positional argument is +reserved for future boundary-level granularity but undefined +and undocumented in v1. No line-level boundary this version. + +## D-5 [RESOLVED] + +--- + +## D-6 · Modifier + character key [RESOLVED] + +**Suggestion:** `on_text_entered(text, modifiers)`. + +**Assessment:** clean, consistent with `notes/design.md`'s +proposed callback signatures, and directly answers the +question. Combined with D-3, the policy becomes: + +- modifier + character-producing key → `on_text_entered` + with non-empty modifiers table (`on_key_combo` does not fire) +- modifier + non-character key → `on_key_combo` + +Coherent and prevents double callbacks. The modifiers table +gives `on_text_entered` richer context without complicating +its signature. + +No sanity concerns. This is the most straightforward of the +seven suggestions. + +**Idea**: this is covered in handlers/callbacks topology above + +*Response:* Correct — `_on_textinput(txt, nonempty(keys_pressed))` +delivers the modifiers table implicitly. D-6 is fully resolved. + +**Note:** this routing only applies when `textinput` actually +fires. For Ctrl+character combos the OS suppresses `textinput`; +those route through `_on_key_pressed` → `compy.handlers` +instead. See `notes/event_routing.md` for the full picture +and a concrete Ctrl+C example. + +--- + +## D-7 · Adoption scope + +**User's question:** is gradual rollout architecturally sane? + +**Yes.** The overlay event path in `controller.lua:625-630` +is already distinct from the console path +(`ConsoleController:keypressed`) and editor path +(`EditorController:keypressed`). Adding forwarding to the +overlay path does not touch either. Console and editor already +manage their own key events and are unaffected by changes +isolated to the overlay dispatch. + +The singleton direction (D-2) reinforces this: if the input +widget is a framework object that console and editor also use +directly, the new event API is a layer that only project code +interacts with. + +The D-1 facade approach also applies: `input_text()` can +resolve to the new singleton configure+show path, so existing +examples get the new implementation without knowing about it. +Old API → facades → new singleton API is a coherent migration +path. + +** ARCHITECT RESPONSE 1 ** + +**Request**: re-evaluate once again with respect to clarifications above. +I see how resolution could be achieved or new controversies may arise but would like you to access it accurately. + +** AI RESPONSE 1 ** + +*Response:* Re-evaluated. The singleton (D-2) + event topology +(D-3) together make gradual rollout straightforwardly viable, but +with one new implication to flag. + +**What is still isolated:** the new event topology +(`_on_key_pressed`, `handlers`, `framework_handlers`) only runs +for the overlay path — i.e., when a project is active and the +singleton is visible. The console and editor continue using their +existing controller paths unchanged. Nothing touches +`ConsoleController:keypressed` or `EditorController:keypressed` +in the first pass. + +**New implication from the singleton direction:** currently the +input MVC triad is created inside `ConsoleController`. If the +singleton lives at framework level, it needs to be created before +`ConsoleController` — likely in `main.lua` — and passed in. The +console and editor would receive a reference to the shared widget +rather than owning it. This is a wiring change, not a behavioural +one, but it means `main.lua` grows a new top-level object. That +is a contained change with no runtime risk. + +**Remaining controversy:** who owns the `compy.handlers` and +`framework_handlers` tables during console mode (no project +running)? The framework singleton exists, but there is no project +to populate `compy.handlers`. The answer is probably that those +tables are reset to defaults on project stop — which is already +how `set_user_handlers` / `evacuate_required` works for +`love.*` handlers. The same pattern applies here. + +** ARCHITECT RESPONSE 2 ** + +I assume we can re-wire, and also let Editor/REPL to just provide their own classes for hot replacement? +(even syntax they call may stay the same) + +Using new callbacks would be opt-in + +Assumption about resetting handlers tables is correct -- they'd be cleared as a part of same process as now. I guess it also applies to the exit from editor + +Write your conclusions and let me know if feature is settled/concerns resolved + +** AI RESPONSE 2 ** + +Re-wiring via "hot replacement" of classes is a clean way to +frame the gradual migration path. Console and editor can, when +they choose, replace their keypressed if-chains with handler +registrations — the singleton API makes this possible without +requiring it. Syntax compatibility (same call shapes, opt-in +new callbacks) means existing code continues to work during the +transition. + +Handler table reset on project stop and editor exit: confirmed +consistent with the existing `evacuate_required` / `set_user_handlers` +pattern. No new mechanism needed. + +**Settlement status: settled.** The scope for this feature is +the project overlay path. Console/editor migration is opt-in and +deferred. The architecture supports both without coupling them. + + +** ARCHITECT RESPONSE 3 ** + +Summarize resolution (so that it could be understood/used without full context of dialogue above) and mark section as [RESOLVED] + +** AI RESPONSE 3 ** + +**Resolution:** The singleton lives at framework level, created +in `main.lua` and passed by reference to `ConsoleController` and +`EditorController`. Initial scope is the project overlay path +only — the new event topology (`keys_pressed`, `handlers`, +`framework_handlers`, `_on_key_pressed` etc.) is active only +when a project is running and the singleton is visible. Console +and editor retain their existing keypressed paths unchanged. +`compy.handlers` is reset on project stop and on editor exit, +using the same mechanism as `evacuate_required`. Console/editor +migration to the handler topology is opt-in and has no timeline +in this feature. The facades (D-1) mean existing examples +continue to work without changes. + +## D-7 [RESOLVED] + +--- + +## Cross-cutting observations + +**An emerging general pattern.** D-3, D-4, and D-5 all point +toward the same structure: a chain of callbacks from general +to specific, where each level calls the next by default and +any level can be overridden. If adopted, this pattern should +be named and described once as a design principle rather than +re-explained per callback. It also needs a consistent +propagation convention — return value is the cleanest option. + +**Default noop + debug log.** D-3 and D-4 both propose this +as the default for overloadable callbacks. Worth adopting as +the standard for all project-facing callbacks: silent failure +is replaced by a visible hint in debug mode. + +**The singleton decision (D-2) has the widest reach.** If +chosen, it simplifies D-1 (facades become configure+show +wrappers), dissolves D-2 itself, and satisfies NFR-1 directly. +Resolving this first is likely to clarify several of the +other decisions. + +**D-3 leaves an unaddressed case.** Non-modifier +non-character keys are covered by neither the key_combo +suggestion (D-3) nor the text+modifiers suggestion (D-6). +Needs an explicit statement: third callback surface, or +deliberately out of scope. + +** ARCHITECT RESPONSE ** + +I guess we resolved the latter concern now? + +*Response:* Yes — the expanded proto-design in D-3 (`_on_key_pressed` +fires for all keypressed events regardless of modifier presence) +closes the gap completely. Non-modifier non-character keys now +have a surface. The original cross-cutting observation is resolved. + +**Overall status across all seven decisions:** +- D-1: settled in direction (facades, reftable compat confirmed) +- D-2: settled (singleton, lazy, framework-level; MVC concern resolved) +- D-3: settled in direction (combo key serialisation = spec detail) +- D-4: settled (before/semantic/after chain pattern) +- D-5: settled (single hook, direction arg, reserved second arg) +- D-6: settled (merged into D-3 topology) +- D-7: **partially reopened** — see § D-7 reopened below + +--- + +## D-7 reopened · FR-11/FR-12 scope + +**The tension:** `requirements.md` FR-11 and FR-12 state that +the new API *should be expressive enough that the console +REPL's and editor's input handling could be re-implemented +using it*. The current D-7 suggested decision reads "event +callbacks apply to the project overlay context only; console +and editor migration is opt-in and deferred." These are in +tension depending on how FR-11/FR-12 are interpreted. + +**Two readings:** + +*Capability reading:* FR-11/FR-12 are expressiveness tests — +the API must support the patterns the console and editor need, +but there is no requirement to actually migrate them. Verified +by inspection: can the API express history navigation on limit? +Can it express Ctrl+M mode switch? If yes, requirements met. + +*Migration reading:* FR-11/FR-12 imply eventual migration. +"Could be re-implemented" is a commitment to provide a path, +and verification requires actually walking that path at some +point. Under this reading, "opt-in, deferred" is acceptable +only if "deferred" has a concrete meaning — it belongs in a +future feature, not "never". + +**What the two readings change for this feature:** + +Under the capability reading, the design just needs to ensure +no API element required by the console or editor is absent. +This is a design review step, not additional implementation. + +Under the migration reading, at minimum a small migration +of one context (e.g., the console's Up/Down → history +path, which is the simplest case) would serve as the +acceptance test for FR-11. The editor migration is more +complex but could be deferred to a separate feature. + +**Relationship to the singleton wiring refactor:** + +The singleton wiring refactor (D-2: create overlay input in +`main.lua`, pass reference to controllers) is a prerequisite +for both readings. It is small, safe, and should happen +regardless. Once done, the console and editor already hold +a reference to the shared widget — the scaffolding for +eventual migration is in place even if migration itself +is deferred. + +**Proposed resolution of the reopened question:** + +Adopt the capability reading for this feature: FR-11/FR-12 +are verified by design review (confirming the API elements +for limit hooks, key handlers, and text callbacks cover +what the console and editor need), not by migration. +Actual migration of console/editor is a follow-on feature, +explicitly named and tracked, not "opt-in someday." + +This means D-7 stays overlay-first for implementation, +but the design doc (design.md) must include a section +explicitly demonstrating that FR-11/FR-12 are satisfied +by the API surface — i.e., a walkthrough showing how the +console's history navigation and editor's mode switches +could be expressed using the new callbacks. + +**Architecture analysis** (see `notes/editor_repl_input.md`): +the console and editor do not have isolated MVC triads for +input — they embed the shared input widget inside full-screen +controllers. Neither registers LÖVE2D callbacks directly; +events reach them via the dispatch chain. "Migration" means +only replacing the keypressed if-chains with handler +registrations; the underlying methods (evaluate, history nav, +load block, mode switches) are untouched. The proposed API +surface covers every key event pattern both controllers use. + +**Proposed resolution (updated):** adopt the capability +reading. FR-11/FR-12 are verified by a design review +walkthrough — `design.md` should include a section showing +the console and editor key patterns expressible via the new +API (sketches are in `notes/editor_repl_input.md`). Actual +migration is a follow-on feature, explicitly named, not +"opt-in someday." D-7 stays overlay-first for this feature. + +**Confirmed by re-reading `input.md`:** the raw requirement +says "an API that *allows for* an easy implementation of +interfaces similar to the console or the editor. *Ideally*, +these should also be re-implemented." The words "allows for" +and "ideally" directly confirm the capability reading. No +migration commitment exists in the stakeholder input. + +The migration path is demonstrated and clear (see +`notes/editor_repl_input.md`). No blocking constraint was +found. `design.md` should include the walkthrough section +showing console/editor key patterns are expressible via the +new API — this is the verification artifact for FR-11/FR-12. + +## D-7 [RESOLVED] diff --git a/doc/development/wip/77-new-input-api/notes/design.md b/doc/development/wip/77-new-input-api/notes/design.md new file mode 100644 index 00000000..83b8419b --- /dev/null +++ b/doc/development/wip/77-new-input-api/notes/design.md @@ -0,0 +1,109 @@ +# Notes — Design Ideas + +Ad-hoc design proposals extracted from earlier draft documents. +These are not decisions — they are candidate ideas to consider +during design. Sources: old `assessment.md` and +`feature_correlation.md`. + +--- + +## Event propagation: return-value bubbling + +Proposal: unhandled key events should propagate by a +return-value convention similar to DOM event handling. The +callback returns `true` to signal the event was handled +(suppress default behaviour); returning `false` or `nil` +allows the event to propagate to the framework's default +LÖVE2D handlers. + +This gives projects precise control without requiring the +framework to enumerate every possible key in advance. + +--- + +## Callback signatures (candidate names) + +Proposed from earlier work: + +| Callback | Signature | Trigger | +|---|---|---| +| `on_text_entered` | `(text, modifiers)` | User submits input | +| `on_key_combo` | `(keys_list)` | At least one modifier key held | +| `on_limit_reached` | `(direction)` | Cursor at boundary, movement attempted | +| `on_cancel` | `(modifiers)` | Edit area dismissed (Escape) | + +`modifiers` would be a table such as `{ ctrl = true }`. +`direction` would be `'up'` / `'down'` or equivalent. +`keys_list` would be the full set of pressed keys. + +These names and signatures are proposals, not decisions. +The co-occurrence question (text + modifier simultaneously — +see `notes/requirements.md`) should be resolved before +finalising. + +--- + +## Dual registration pattern + +Proposal: allow two equivalent ways to register callbacks — +whichever fits the project's style: + +1. **Per-call closure table** — pass a table of callbacks to + the setup call. Scoped to that input session. +2. **Global project hooks** — set fields on a shared namespace + (e.g. `compy.on_text_entered = function(...) end`). Applies + to any active input. + +The two would share the same callback names. Global hooks are +more convenient for simple projects; per-call tables are safer +when a project manages multiple interaction modes. + +--- + +## Persistent lifecycle API + +Proposal: rather than allocating a new MVC triad on each +`input_text()` call, treat the widget as persistent and expose +state-mutation methods: + +- `show()` — make the edit area visible +- `hide()` — hide it without discarding content +- `alter()` — update configuration (prompt, text, cursor, etc.) + without full teardown + +`enable()` / `disable()` were also proposed — possibly for +suppressing input while keeping the widget visible. + +This directly addresses NFR-1 (allocation / GC) and the +dynamic-update pain point (see `notes/requirements.md`). + +--- + +## Sensible defaults: unregistered callbacks fall back gracefully + +Proposal: if no callback is registered for a given event (e.g. +`on_text_entered` not set), the input widget should behave as it +currently does — i.e. standard Console/REPL submission. This +means the new API is additive: a project that registers no +callbacks gets existing behaviour for free, and opts into new +behaviour only where it registers handlers. + +Implication: the default behaviour must be encoded somewhere +(either as built-in fallback logic in the framework, or as a +default callback table applied when none is supplied). Worth +deciding early, as it affects how the API interacts with the +console and editor when they are eventually re-implemented on +top of it. + +--- + +## Backward-compatibility shims + +Proposal: keep the existing `input_text()`, `input_code()`, +`validated_input()`, and `user_input()` working by delegating +to the new API internally. Emit a deprecation notice in debug +mode. This avoids breaking existing examples while steering new +code toward the new API. + +Whether this is the right tradeoff (vs a clean break) is an +open question noted in `requirements.md §5`. diff --git a/doc/development/wip/77-new-input-api/notes/editor_repl_input.md b/doc/development/wip/77-new-input-api/notes/editor_repl_input.md new file mode 100644 index 00000000..1554a32b --- /dev/null +++ b/doc/development/wip/77-new-input-api/notes/editor_repl_input.md @@ -0,0 +1,159 @@ +# Notes — Editor and REPL Input Architecture + +Analysis supporting the FR-11/FR-12 capability question +and the D-7 rollout scope decision. Covers what the +console REPL and editor currently do with input, and +what "migration to the new callbacks/handlers topology" +would actually mean for each. + +--- + +## Current structure + +Neither the console REPL nor the editor has an isolated +MVC triad for the input strip. Both embed the shared input +widget — `UserInputModel` / `UserInputController` / +`UserInputView` — as one component inside a larger +full-screen controller. + +**ConsoleController** manages: the REPL input strip, +terminal output, project lifecycle, code evaluation, and +the `user_input` overlay API. It holds a reference to +`UserInputModel` (its own input, not the overlay) and +calls `UserInputController:keypressed(k)` as part of its +own key handling. + +**EditorController** manages: the input strip at the +bottom, the block buffer display above it, buffer +navigation, the submit pipeline, mode state (edit / +reorder / search), and the buffer stack. It likewise holds +a reference to the shared input widget. + +Neither controller registers `love.keypressed` directly. +Key events arrive via the dispatch chain: + +``` +controller.lua handlers.keypressed + → global shortcuts (Ctrl+Q, Ctrl+T, etc.) + → ConsoleController:keypressed(k) + → if editor state: EditorController:keypressed(k) + → else: console key logic +``` + +--- + +## What the controllers do in their keypressed handlers + +### ConsoleController:keypressed + +- Clear error state on certain keys +- `PageUp` / `PageDown` → `input:history_back/fwd()` +- Delegate editing to `UserInputController:keypressed(k)` + - If it returns a limit signal: `Up/Down` → history nav +- `Enter` (no shift, no error) → `evaluate_input()` +- `Ctrl+L` → clear terminal (not currently implemented + as a hotkey in the source but the pattern is established) + +The if-chain here is the dispatch logic. The methods it +calls (`history_back`, `evaluate_input`, etc.) are defined +elsewhere and have nothing to do with key dispatch. + +### EditorController:keypressed + +Dispatches to mode-specific handlers: +- **Edit mode**: `Escape` → load selected block into input; + `Enter` → `_handle_submit`; `Ctrl+Enter` → insert; + `Ctrl+M` → reorder mode; `Ctrl+F` → search mode; + `Ctrl+S` → close buffer; `Ctrl+O` → follow require; + `Up/Down` at input limit → block navigation; + all other keys → `UserInputController:keypressed(k)` +- **Reorder mode**: navigation + `Enter` → confirm move; + `Escape` → cancel reorder +- **Search mode**: input + `Enter` → jump to definition; + `Escape` → exit search + +Again the if-chain is dispatch only. The methods called +(`_handle_submit`, `buf:move`, `enter_reorder_mode`, etc.) +are defined on the controller and buffer model. + +--- + +## What migration to the new topology would mean + +The migration target is precisely the if-chains in both +`keypressed` methods — and only those. Everything the +methods call stays exactly as-is. + +**Console migration sketch:** + +```lua +-- framework_handlers already cover Enter and Escape. +-- These replace the manual checks in ConsoleController:keypressed: +compy.handlers['pageup'] = function() input:history_back() end +compy.handlers['pagedown'] = function() input:history_fwd() end +-- limit hook replaces the `if limit then` check: +compy.on_limit_reached = function(dir) + if dir == 'up' then input:history_back() + else input:history_fwd() end +end +-- before_submit replaces the Enter → evaluate path: +compy.before_submit = function(text) + cc:evaluate_input(text) +end +``` + +**Editor migration sketch:** + +```lua +compy.handlers['escape'] = function() cc:load_block() end +compy.handlers['lctrl+m'] = function() cc:enter_reorder() end +compy.handlers['lctrl+f'] = function() cc:enter_search() end +compy.handlers['lctrl+s'] = function() cc:close_buffer() end +compy.on_limit_reached = function(dir) cc:navigate_block(dir) end +compy.before_submit = function(text) cc:handle_submit(text) end +``` + +In both cases: the controller's keypressed method shrinks +to handler registrations; the underlying methods are +unchanged. The editor's buffer model, submit pipeline, +semantic analysis, and buffer stack are entirely +unaffected. + +--- + +## FR-11/FR-12 capability assessment + +FR-11 (REPL re-implementability): the API needs +`on_limit_reached` hook, `before_submit` callback, and +handler registration for PageUp/PageDown. All are present +in the proposed design. + +FR-12 (editor re-implementability): the API needs +`on_limit_reached`, `before_submit`, `before_cancel` +(for load-on-Escape), and handler registration for +Ctrl+M, Ctrl+F, Ctrl+S, Ctrl+O. All are present or +derivable from the three-level dispatch. + +**Conclusion:** the capability reading of FR-11/FR-12 is +satisfiable by the proposed API surface. No actual +migration is required to verify this — a design review +walkthrough (showing the sketches above compile against +the API spec) is sufficient. + +--- + +## Effort estimate for actual migration (informational) + +Should migration ever be chosen as a follow-on feature: + +- Console migration: small. One method replaced by ~5 + handler registrations. Low risk, easily tested. +- Editor migration: medium. `EditorController:keypressed` + has ~80 lines across three mode branches. Replacing + with handler registrations is mechanical but requires + care around mode state (reorder/search modes affect + which handlers are active — would need show/hide of + handler sets on mode transition). + +Neither migration changes the buffer model, submit +pipeline, or view code. diff --git a/doc/development/wip/77-new-input-api/notes/enter_escape_routing.md b/doc/development/wip/77-new-input-api/notes/enter_escape_routing.md new file mode 100644 index 00000000..24711ba7 --- /dev/null +++ b/doc/development/wip/77-new-input-api/notes/enter_escape_routing.md @@ -0,0 +1,262 @@ +# Notes — Enter and Escape Routing + +How the two primary session-control keys travel through the +system across all four input contexts. Companion to +`notes/event_delegation_chain.md` and +`notes/textinput_routing.md`. + +--- + +## Prerequisite: the `oneshot` flag + +`UserInputController` (the shared text-editing widget) has two +operating modes controlled by a `oneshot` flag: + +- **`oneshot = false`** (REPL, editor): the widget handles + text editing — backspace, cursor, selection, copy/paste, + Shift+Enter for newlines — but does **not** own submit. When + Enter is pressed, the widget's `submit()` inner function + checks `oneshot` and returns immediately without action. The + controller above (ConsoleController for REPL, + EditorController for editor) is responsible for handling + Enter its own way after the widget returns. + +- **`oneshot = true`** (project overlay): the widget owns the + full session lifecycle. Enter triggers `submit()` → + `model:evaluate()` → on success, fills the reftable and + pushes `'userinput'` to dismiss the overlay. The widget is + the terminal; there is no controller above it in this branch. + +The flag is set when the project calls `input_text()` (or +equivalent) to show the overlay. REPL and editor never set it. +This is why `submit()` appears to "do nothing" in REPL/editor +traces — it genuinely no-ops by design. + +### `oneshot` under the new architecture + +`oneshot` is a tactical flag, not a permanent design element. +It exists because one widget was asked to serve two +incompatible roles with no shared dispatch layer between them. + +In the new architecture, `UserInputController` is +unconditionally passive — always a sink, never a session owner. +Submit is always handled above it in `framework_handlers`: + +- ConsoleController's `framework_handlers['return']` → + `evaluate_input()` +- EditorController's `framework_handlers['return']` → + `_handle_submit()` +- ProjectController's `framework_handlers['return']` → submit + callback (FR-5) + push `'userinput'` + +The widget no longer checks whether it owns submit, because it +never does. The `submit()` inner function that `oneshot` +controls goes away. The overlay lifecycle that `oneshot=true` +was encoding is now encoded in ProjectController's handler +table, where it belongs. The singleton's `show()`/`hide()` +replaces the other signal `oneshot` was carrying ("this widget +is currently in active use by a project"). + +`oneshot` is not migrated or generalised — it is deleted. The +architectural split (controller owns submit, widget owns +text-editing) dissolves the problem it was solving. + +--- + +A second prerequisite: `EditorController:_normal_mode_keys` +uses a `passthrough` flag (default true). Any handler that +fully claims a key calls `block_input()`, setting +`passthrough = false`. At the end of the function, +`if passthrough then input:keypressed(k) end` decides +whether the key also reaches `UserInputController:keypressed`. +This is how EditorController and UserInputController +cooperate without explicit coordination. + +--- + +## Enter + +### REPL (app_state = `ready`) + +``` +keypressed('return') + → ConsoleController:keypressed + → UserInputController:keypressed + submit() inner fn: oneshot=false → does nothing + newline(): Shift+Enter → inserts line feed + → [after UserInputController returns] + → ConsoleController: if not Shift and is_enter(k) + → evaluate_input() +``` + +Plain Enter submits (evaluate_input). Shift+Enter inserts a +newline in the multiline input. The split is clean: the +widget handles the newline insertion; the console handles +the submit. + +### Editor — normal mode (app_state = `editor`) + +``` +keypressed('return') + → ConsoleController → EditorController:keypressed + → _normal_mode_keys(k) + newline(): Shift/Ctrl+Enter on empty input + → buf:insert_newline() + block_input() + [passthrough blocked; UserInputController not called] + submit(): plain Enter (no Ctrl/Shift/Alt) + → _handle_submit(replace) + [block_input NOT called; passthrough stays true] + → [passthrough=true] input:keypressed(k) + UserInputController: editor branch + submit(): oneshot=false → does nothing +``` + +Plain Enter: EditorController calls `_handle_submit(replace)` +(pretty-print pipeline, replaces selected block, auto-save). +UserInputController's submit() also runs but is a no-op +(oneshot=false). Ctrl+Enter: `_handle_submit(add)` (inserts +before selection). Shift/Ctrl+Enter on empty input: inserts +an empty block directly in the buffer, bypasses the input +entirely. + +### Editor — reorder mode + +Enter: `_reorg(true)` — confirms the block move, returns to +edit mode. + +### Editor — search mode + +Enter: confirms the highlighted search result, scrolls to +the block, returns to edit mode. + +### Overlay (oneshot = true) + +``` +keypressed('return') + → UserInputController:keypressed [direct, overlay active] + newline(): Shift+Enter → inserts line feed + submit(): not Shift, is_enter, oneshot=true + if empty → return (no-op) + model:evaluate() + → if ok: + UserInputModel:handle pushes 'userinput' event + res(t) fills reftable + → if fail: + model:set_error(err) + input locked until Enter/space/arrows +``` + +Plain Enter submits. On success the reftable is filled and +`'userinput'` is pushed, clearing the overlay on the next +event dispatch. On evaluation failure the input locks with +an error highlight — no dismiss, no retry prompt. Shift+Enter +inserts a newline for multiline input. + +### Project running, no overlay + +Project's own `love.keypressed` handler. No framework +involvement beyond global power shortcuts. + +--- + +## Escape + +### REPL (app_state = `ready`) + +``` +keypressed('escape') + → ConsoleController:keypressed + → UserInputController:keypressed + else-branch (non-editor): cancel() fires + model:cancel() → handle(false) + reset() + → input content cleared +``` + +Escape clears the input content. No session lifecycle effect +(no overlay exists in REPL mode). `handle(false)` does not +push `'userinput'`. + +### Editor — normal mode + +``` +keypressed('escape') + → EditorController:_normal_mode_keys + load(): Escape (no Ctrl, no Shift) + → load_selection() + → input:set_text(selected_block_text) + [block_input NOT called; passthrough stays true] + → [passthrough=true] input:keypressed('escape') + UserInputController: app_state='editor' branch + cancel() is NOT in the editor branch → not called +``` + +Escape loads the selected block's text into the input strip. +`UserInputController:keypressed` runs (passthrough=true) but +its editor branch deliberately omits `cancel()` — the input +content is preserved. Shift+Escape performs an additive load +(inserts block text at cursor rather than replacing). + +This is the key EditorController/UserInputController +cooperation point: Escape's meaning is redefined by the +editor without needing to explicitly suppress the widget's +cancel path — the editor-branch omission does it implicitly. + +### Editor — reorder mode + +Escape: `_reorg(false)` — cancels the pending move, restores +state, returns to edit mode. + +### Editor — search mode + +Escape: `set_mode('edit')`, clears the search filter. + +### Overlay (oneshot = true) + +``` +keypressed('escape') + → UserInputController:keypressed [direct, overlay active] + else-branch: cancel() fires + model:cancel() → handle(false) + reset() + → input content cleared + → 'userinput' NOT pushed + → love.state.user_input unchanged +``` + +Escape clears the input content but **does not dismiss the +overlay**. The overlay remains visible with an empty input +strip. `'userinput'` is never pushed from `handle(false)`. +The project's polling loop continues; the project has no +notification that Escape was pressed. + +This is a known limitation of the current design. Under the +new API, Escape would fire `before_cancel → cancel → +after_cancel` (D-4), and the framework's `cancel` handler +would push `'userinput'` to dismiss the overlay. + +### Project running, no overlay + +Project's own `love.keypressed` handler. + +--- + +## Global override: Ctrl+Escape + +`love.handlers.keyreleased` (controller.lua:642–646) checks +Ctrl+Escape on key **release** and calls `love.event.quit()` +unconditionally. This fires regardless of app_state, overlay +state, or any other context. It is the application exit +shortcut and cannot be intercepted by project or framework +code. + +--- + +## Summary table + +| Key | REPL | Editor (normal) | Overlay | Project | +|---|---|---|---|---| +| Enter | evaluate_input | _handle_submit (replace) | submit + dismiss | project | +| Shift+Enter | newline | empty-block insert (or newline if non-empty) | newline | project | +| Ctrl+Enter | — | _handle_submit (add/insert) | — | project | +| Escape | clear input | load selected block | clear input only | project | +| Shift+Escape | — | additive load | — | project | +| Ctrl+Escape (release) | quit app | quit app | quit app | quit app | diff --git a/doc/development/wip/77-new-input-api/notes/event_delegation_chain.md b/doc/development/wip/77-new-input-api/notes/event_delegation_chain.md new file mode 100644 index 00000000..5171879d --- /dev/null +++ b/doc/development/wip/77-new-input-api/notes/event_delegation_chain.md @@ -0,0 +1,219 @@ +# Notes — Current Event Delegation Chain + +Documents how keyboard events travel from LÖVE2D to their +final handler across all four input contexts. Companion to +`notes/event_routing.md` (which focuses on before/after +comparison for the new API) and `notes/context_differences.md` +(which focuses on widget configuration per context). + +--- + +## The four contexts + +The four contexts are mutually exclusive by `app_state`: + +| Context | app_state | Who owns keypressed | +|---|---|---| +| REPL | `ready` | ConsoleController → UserInputController | +| Editor | `editor` | ConsoleController → EditorController → UserInputController | +| Project, no overlay | `running` / `project_open` | Project's own `love.keypressed` | +| Project, overlay active | `running` / `project_open` | Overlay's UserInputController (exclusive) | + +--- + +## Two routing levels + +There are two distinct levels of handler in LÖVE2D: + +- **`love.handlers.keypressed`** — low-level, set once in + `controller.lua` at startup for all project-running states. + Intercepts power shortcuts (Ctrl+Q, Ctrl+T, etc.) before + anything else, then routes to overlay or project. + +- **`love.keypressed`** — higher-level callback, set via + `Controller.set_love_keypressed(CC)` at startup to + `ConsoleController:keypressed`. Projects can overwrite this + with their own handler; `controller.lua` saves the original + in `Controller._defaults` and restores it on project + teardown. + +When a project is running and no overlay is active, the +project's `love.keypressed` is called from inside +`love.handlers.keypressed` after the power shortcuts have +been checked. + +--- + +## REPL context + +``` +LÖVE2D keypressed + → love.keypressed = ConsoleController:keypressed + ├─ [error state] intercept space/enter/arrows → clear_error; return + ├─ PageUp/Down → history_back / history_fwd + ├─ UserInputController:keypressed(k) + │ handles: backspace, delete, Ctrl+Y, arrows, Home/End, + │ Shift+Enter (newline), Ctrl+D (dup line), Ctrl+C/X/V, + │ selection; returns a "limit" signal if cursor hits + │ top/bottom boundary of the input + ├─ limit + Up/Down → history nav + ├─ Enter (no Shift) → evaluate_input() + └─ Ctrl+L → clear output + +LÖVE2D textinput + → love.textinput = ConsoleController:textinput + → UserInputController:textinput + → UserInputModel:add_text(t) +``` + +Combos are not dispatched via a table. ConsoleController uses +explicit `if Key.ctrl() and k == "l"` checks. UserInputController +runs first; ConsoleController handles submit and navigation +after. + +--- + +## Editor context + +``` +LÖVE2D keypressed + → ConsoleController:keypressed + → [app_state == 'editor'] EditorController:keypressed + ├─ mode dispatch (edit / reorder / search) + ├─ mode-specific shortcuts: Esc (load block), + │ Ctrl+M (reorder mode), Ctrl+F (search mode), + │ Ctrl+O (follow require), etc. + ├─ Enter → EditorController:_handle_submit + │ pretty-print → re-chunk → oversize check + │ → replace buffer block → auto-save + └─ (text-editing keys delegated) + → UserInputController:keypressed + +LÖVE2D textinput + → ConsoleController:textinput + → [app_state == 'editor'] EditorController:textinput + → UserInputController:textinput + → UserInputModel:add_text(t) +``` + +EditorController is a thick handler — every keystroke passes +through it. It runs its own if-chains first, then delegates +text-editing keys (backspace, cursor movement, per-character +input) to UserInputController. The editor does not receive +"full strings"; characters arrive one at a time via +`textinput`. Submit (Enter) is caught by EditorController +before reaching UserInputController. + +--- + +## Project, no overlay + +``` +LÖVE2D keypressed + → love.handlers.keypressed (controller.lua) + ├─ power shortcuts (Ctrl+Q/T/S, Ctrl+Shift+R, etc.) + └─ love.keypressed(k) ← project's own handler, if set + +LÖVE2D textinput + → love.handlers.textinput (controller.lua) + └─ love.textinput(t) ← project's own handler, if set +``` + +The project receives raw LÖVE2D key names and must manage +modifier state itself (e.g. `love.keyboard.isDown('lctrl')`). +No input widget, no combo dispatch, no structured submit path. + +--- + +## Project, overlay active + +Activating the overlay (`love.state.user_input` non-nil) +redirects routing at the framework level — before the project +sees anything: + +``` +LÖVE2D keypressed + → love.handlers.keypressed (controller.lua) + ├─ power shortcuts (always intercepted) + └─ [user_input set] user_input.C:keypressed(k) ← EXCLUSIVE + [no user_input] love.keypressed(k) ← project + +LÖVE2D textinput + → love.handlers.textinput + ├─ [user_input set] user_input.C:textinput(t) ← EXCLUSIVE + └─ [no user_input] love.textinput(t) ← project + +LÖVE2D keyreleased — same split +``` + +The overlay's UserInputController handles everything. Enter +is handled by the `oneshot` path inside +`UserInputController:keypressed`: evaluator runs → reftable +filled → `love.event.push('userinput')` → overlay cleared. + +**Mouse is different.** Both the overlay controller and the +project's mouse handlers receive mouse events — mouse is not +exclusively routed to the overlay. + +--- + +## Submit paths, compared + +| Context | Enter reaches | What happens | +|---|---|---| +| REPL | ConsoleController:evaluate_input | Metalua parse, execute, output | +| Editor | EditorController:_handle_submit | Pretty-print, re-chunk, replace block, save | +| Overlay (oneshot) | UserInputController:keypressed | Evaluator runs, reftable filled, overlay cleared | +| Project, no overlay | Project's own love.keypressed | Whatever the project does | + +--- + +## Architectural evaluation + +### What holds up + +The two-level handler split (`love.handlers` vs `love.keypressed`) +is a genuine seam with clear responsibilities: the framework +level owns global power shortcuts and overlay routing; the +project level owns everything application-specific. The +`app_state` machine enforces mutual exclusivity of contexts, +so the conditional branches throughout the dispatch chain map +onto real operating mode boundaries rather than ad-hoc +conditions. And `UserInputController` / `UserInputModel` as a +shared widget is a sound unifying idea — one text-editing +implementation regardless of who is using it. + +### Where complexity has accumulated + +`ConsoleController:keypressed` has taken on several +responsibilities over time: it is the REPL's own input +handler, the router to the editor, and the historical home for +logic that did not obviously belong elsewhere. The coupling +between ConsoleController and EditorController is tight — +ConsoleController owns the input widget, EditorController +borrows it, and both consult `app_state` to switch behaviour. + +The combo-handling in both controllers is expressed as +explicit `if Key.ctrl()` chains spread across multiple files. +`agents/rules.md` identifies this as a known accumulated +pattern ("dispatch tables beat if-chains") and flags new code +as the place to correct it — the existing if-chains are a +legacy of the system's organic growth. + +### Relevance to this feature + +This complexity is concentrated in the REPL and editor paths, +which D-7 explicitly deferred. The overlay routing path — the +part being redesigned here — is the more self-contained of +the four: a straightforward if/else gate that hands off +exclusively to the overlay controller. The new API adds a +callback layer inside that controller; it is additive, not +a restructure of the delegation chain. + +The accumulated complexity in ConsoleController and +EditorController is the natural target of the follow-on +migration (named in D-7). When the REPL and editor adopt +the new API, replacing the if-chains with handler +registrations becomes the organic outcome of that migration +rather than a separate refactor effort. No action is needed +in the current feature scope. diff --git a/doc/development/wip/77-new-input-api/notes/event_routing.md b/doc/development/wip/77-new-input-api/notes/event_routing.md new file mode 100644 index 00000000..8ec5e1e5 --- /dev/null +++ b/doc/development/wip/77-new-input-api/notes/event_routing.md @@ -0,0 +1,333 @@ +# Notes — Key Event Routing + +Analysis of how keyboard events travel from OS to consumer +code, before and after the new API. Informs the D-3/D-6 +routing decisions and identifies one open design detail +(suppression mechanism) to be resolved in `design.md`. + +Related: `notes/decisions.md` §D-3, §D-6. + +--- + +## LÖVE2D event taxonomy + +LÖVE2D exposes two keyboard callbacks that can both fire for +the same physical gesture: + +| Event | When it fires | Argument | +|---|---|---| +| `love.keypressed(k)` | Every physical key press (and repeat) | LÖVE2D key name string | +| `love.textinput(t)` | When the OS produces printable text | UTF-8 string (usually one char, can be multi-char for IME) | +| `love.keyreleased(k)` | Every physical key release | LÖVE2D key name string | + +**Critical OS-level behaviour:** whether `textinput` fires +alongside `keypressed` depends on the OS and modifier state: + +| Gesture | `keypressed` fires? | `textinput` fires? | +|---|---|---| +| Plain `a` | yes (`k='a'`) | yes (`t='a'`) | +| `Shift+a` | yes (`k='a'`) | yes (`t='A'`, OS handles case) | +| `Ctrl+a` | yes (`k='a'`) | **no** — Ctrl suppresses text output | +| `Ctrl+Shift+a` | yes | **no** | +| `Alt+a` | yes | platform-dependent | +| Arrow, F-key, etc. | yes | **no** — non-character key | +| IME composition | varies | yes (can be multi-char string) | + +The framework does not control this split — it is an OS +decision before any Compy code runs. + +--- + +## Current routing (before new API) + +Two contexts are relevant: the project overlay (when a +project calls `input_text()`) and the REPL/editor +(ConsoleController + EditorController). + +### A. Project overlay active (`user_input` is set) + +The overlay behaves as a **silent singleton** already: +while `love.state.user_input` is set, any call to +`input_text()` / `input_code()` / `validated_input()` is +silently dropped — it returns `nil` and the existing overlay +is unaffected. The MVC triad is allocated once when the +first call creates the overlay; on submit, `UserInputModel` +pushes a `userinput` event which clears +`love.state.user_input`, making the triad GC-eligible. The +next `input_text()` call allocates a fresh one. + +The difference from D-2's proposed singleton: the current +pattern allocates and GCs one triad per input session. +D-2 allocates once at framework startup and reconfigures +the same object on each `show()` call — eliminating the +per-session allocation entirely. + +``` +OS event + │ + ▼ +controller.lua : handlers.keypressed(k) + ├─ global framework shortcuts (Ctrl+Q, Ctrl+T, etc.) + └─ user_input.C:keypressed(k) + └─ UserInputController:keypressed + ├─ cursor, backspace, selection, etc. + └─ Enter (oneshot=true path): + evaluator runs → if ok: + reftable filled + love.event.push('userinput') + → handlers.userinput + → love.state.user_input = nil + [project polls reftable; receives no key events] + +OS event + │ + ▼ +controller.lua : handlers.textinput(t) + └─ user_input.C:textinput(t) + └─ UserInputController:textinput + └─ UserInputModel:add_text(t) + [project receives no text events] +``` + +All key and text events are consumed by the overlay. +Project code has no way to observe or intercept them — it +only sees the final submitted value via the reftable. + +Note: the overlay's Enter submit path runs inside +`UserInputController:keypressed` (the `oneshot` flag). +This differs from the console REPL, where Enter is handled +in `ConsoleController:keypressed` → `evaluate_input()`. +The submit mechanisms are distinct even though both use +the same underlying widget. + +### B. No overlay — project running with raw LÖVE2D callbacks + +``` +OS event + │ + ▼ +controller.lua : handlers.keypressed(k) + ├─ global framework shortcuts + └─ love.keypressed(k) ← project's own callback if set + +OS event + │ + ▼ +controller.lua : handlers.textinput(t) + └─ love.textinput(t) ← project's own callback if set +``` + +Projects can intercept keys here, but only outside of input +sessions. The two modes are mutually exclusive today. + +### C. REPL / editor (no overlay, ConsoleController active) + +The console REPL and the editor share **one** persistent +`UserInput` widget (`UserInputModel` / `UserInputController` +/ `UserInputView`), owned by `ConsoleModel` and borrowed by +`EditorController` when the editor is active. Neither creates +a fresh widget on activation — they reconfigure the shared +one. This is distinct from the project overlay (section A), +which allocates its own separate triad. + +``` +OS event + │ + ▼ +controller.lua : handlers.keypressed(k) + ├─ global shortcuts + └─ ConsoleController:keypressed(k) + ├─ [if editor state] EditorController:keypressed(k) + │ └─ mode if-chain (edit / reorder / search) + │ └─ UserInputController:keypressed(k) + └─ [else] console if-chain + ├─ PageUp/Down → history nav + ├─ Enter → evaluate_input() + └─ UserInputController:keypressed(k) + └─ cursor, backspace, etc. + +OS event + │ + ▼ +controller.lua : handlers.textinput(t) + └─ ConsoleController:textinput(t) + ├─ [Ctrl+Shift filtered out here] + ├─ [if editor] EditorController:textinput(t) + └─ [else] UserInputController:textinput(t) + └─ UserInputModel:add_text(t) +``` + +--- + +## Proposed routing (after new API) + +Scope: project overlay path only (D-7). Console and editor +paths are unchanged in this feature. + +**What changes structurally (D-2):** the console/editor +already share one persistent input widget. The project +overlay currently creates a fresh one per call. After D-2, +all three contexts use the same framework-level singleton — +the console/editor reconfigure it when they become active, +and the project overlay configures it via `show()`/`hide()` +rather than allocating a new triad. + +### Proposed routing when project overlay is active + +``` +OS event + │ + ▼ +controller.lua : handlers.keypressed(k, scancode, isrepeat) + ├─ global framework shortcuts (unchanged) + ├─ compy.keys_pressed[k] = true ← new: track pressed set + ├─ compy._on_key_pressed(k, keys_pressed, isrepeat) + │ ├─ 1. framework_handlers[combo] ← structural (escape, enter, etc.) + │ ├─ 2. compy.handlers[combo] ← project-registered, combo-specific + │ └─ 3. compy.on_key_pressed(k, ks) ← generic fallback, noop+log + │ [see suppression note below] + └─ UserInputController:keypressed(k) ← text editing unchanged + +OS event + │ + ▼ +controller.lua : handlers.textinput(t) + ├─ compy._on_textinput(t, keys_pressed) ← new + │ └─ compy.on_text_entered(t, mods) ← project callback, noop+log + └─ UserInputController:textinput(t) ← text editing unchanged + +OS event + │ + ▼ +controller.lua : handlers.keyreleased(k) + ├─ compy.keys_pressed[k] = nil ← new: remove from pressed set + └─ compy._on_key_stroke(k, keys_pressed) ← new: release event + ├─ 1. framework_handlers (on release, if any) + ├─ 2. compy.handlers[combo] + └─ 3. compy.on_key_stroke(k, ks) ← generic fallback, noop+log +``` + +`combo` is a string key derived from `keys_pressed` at the +time of the event — currently-held keys plus the trigger key, +sorted and joined (e.g. `"lctrl+s"`). Exact serialisation +format is a spec detail (see D-3 resolution). + +`mods` in `on_text_entered` is the subset of `keys_pressed` +that are non-character modifier keys at the time `textinput` +fires. + +### Ctrl+character routing (concrete example) + +`Ctrl+C` pressed: + +``` +1. keypressed('lctrl'): + keys_pressed = { lctrl = true } + _on_key_pressed('lctrl', {lctrl=true}) + → combo = 'lctrl'; no handler registered; generic fires + +2. keypressed('c'): + keys_pressed = { lctrl = true, c = true } [or c tracked separately] + _on_key_pressed('c', {lctrl=true, ...}) + → combo = 'lctrl+c' + → compy.handlers['lctrl+c'] fires if registered + → generic on_key_pressed fires if not handled + +3. textinput: does NOT fire (OS suppresses for Ctrl) + → on_text_entered is never called +``` + +`Ctrl+C` is therefore handled entirely via the handler +table, not via `on_text_entered`. A project registers +`compy.handlers['lctrl+c']` to intercept it. +This is coherent: the routing decision is made by the OS +before the framework sees the event. + +Contrast with `Shift+A`: + +``` +1. keypressed('lshift'): keys_pressed = {lshift=true} +2. keypressed('a'): keys_pressed = {lshift=true, a=true} + → combo 'a+lshift'; handler fires if registered +3. textinput('A'): fires (OS applies Shift → uppercase) + → on_text_entered('A', {lshift=true}) +``` + +For `Shift+A` both paths fire. See suppression note below. + +--- + +## Open design detail: suppression mechanism + +The summary (`summaries/decisions.md`) states: "for plain +character keys where both events fire, the framework +suppresses the `on_key_pressed` project callback so project +code receives exactly one notification per gesture." + +The mechanism for this suppression is not yet specified. +The problem: `keypressed` fires before `textinput` in the +LÖVE2D event queue, so at the time `_on_key_pressed` runs for +'a', the framework does not yet know that `textinput('a')` +will follow. + +Two candidate approaches for `design.md` to choose between: + +**Option A — Distinct semantics, no suppression needed.** +Define `on_key_pressed` and `on_text_entered` as covering +different concerns (physical key events vs text input) rather +than as competing notifications for the same gesture. A +project that wants to handle typed characters uses +`on_text_entered`; a project that wants physical key +awareness uses `on_key_pressed`. Both fire; neither "cancels" +the other. This matches how DOM `keydown` and `input` events +work. Double-fire is only a concern if a project +misuses both callbacks for the same purpose — a documentation +concern, not a framework concern. + +**Option B — Deferred generic callback.** +`_on_key_pressed` fires framework_handlers and compy.handlers +immediately (these are combo-specific and need no +suppression). The generic `on_key_pressed` fallback is +deferred by one event-pump tick. If `textinput` fires for the +same key before the deferred callback runs, the deferred +callback is cancelled. Adds implementation complexity; +preserves the "one notification" guarantee for the generic +path. + +Option A is simpler and consistent with established event +models. Option B gives the guarantee stated in the summary +but requires non-trivial deferred dispatch machinery. + +**Recommendation for `design.md`:** choose Option A, update +the summary language accordingly. The D-6 note "modifier + +character-producing key → `on_text_entered`" is accurate +as a usage guideline (not a suppression rule): projects that +care about typed characters should use `on_text_entered`; +projects that care about key combos should use +`compy.handlers` or `on_key_pressed`. + +--- + +## Mouse input (out of scope for this feature — for reference) + +Mouse routing is a parallel path not covered by the diagrams +above. Brief summary for completeness: + +- `love.handlers.mousereleased` implements single/double + click detection with a 0.4s timer. Confirmed clicks fire + `compy.singleclick(x, y)` / `compy.doubleclick(x, y)` — + Compy-specific abstractions, not LÖVE2D events. +- Raw `love.mousepressed` / `mousereleased` / `mousemoved` + / `wheelmoved` are also forwarded to project-defined LÖVE2D + handlers via the standard wrap mechanism. +- When an overlay is active, the framework's mouse handlers + call both the user handler AND the overlay controller — + both receive mouse events (unlike key events, which are + exclusively routed to the overlay). +- `UserInputController` handles mouse on the input widget + for cursor placement and drag-selection. Suppressed in + editor mode (`disable_selection = true`). + +Mouse callbacks (`compy.singleclick`, `compy.doubleclick`) +are already project-facing and callback-based — they are +unaffected by this feature. diff --git a/doc/development/wip/77-new-input-api/notes/love2d_handler_layers.md b/doc/development/wip/77-new-input-api/notes/love2d_handler_layers.md new file mode 100644 index 00000000..7cc02393 --- /dev/null +++ b/doc/development/wip/77-new-input-api/notes/love2d_handler_layers.md @@ -0,0 +1,167 @@ +# Notes — LÖVE2D Handler Layers and Compy's Dispatch Model + +Explains the two-level event handler architecture that +underlies all keyboard routing in Compy. Prerequisite reading +for `notes/event_delegation_chain.md`. + +--- + +## LÖVE2D's event architecture + +LÖVE2D processes input through an event queue. Each frame: + +``` +love.event.pump() -- collect OS events into queue +love.event.dispatch() -- process each queued event +``` + +`dispatch()` works by calling `love.handlers[eventname](args)` +for each event. `love.handlers` is a plain Lua table of +functions, one per event type. The default +`love.handlers.keypressed` is approximately: + +```lua +love.handlers.keypressed = function(k, scancode, isrepeat) + if love.keypressed then + love.keypressed(k, scancode, isrepeat) + end +end +``` + +`love.keypressed` is the conventional user-facing callback — +a game sets `function love.keypressed(k) ... end` and the +default handler calls it. The two levels are not separate +systems: `love.handlers.keypressed` is the raw dispatch entry +point; `love.keypressed` is the callback it invokes. Normally +only `love.keypressed` is touched. `love.handlers` is touched +only when pre-empting the conventional callback is necessary. + +--- + +## What Compy does to this + +Compy replaces `love.handlers.keypressed` entirely with its +own function (`controller.lua:528`). This gives the framework +first-mover advantage — it runs before any project or +ConsoleController code. Two things are done at this level: + +**Global power shortcuts** (Ctrl+Q, Ctrl+T, Ctrl+Shift+R, +etc.) are intercepted unconditionally. They must work in +every application state, including when a project has +overwritten `love.keypressed` with its own handler. + +**Overlay interception** — if `love.state.user_input` is set, +all keyboard events are routed exclusively to the overlay +controller and `love.keypressed` is never called. + +If neither condition applies, the handler falls through to +whatever is currently in `love.keypressed`. + +--- + +## The `love.keypressed` slot + +At startup, Compy sets `love.keypressed` to +`ConsoleController:keypressed` and saves it in +`Controller._defaults.keypressed`. This is the permanent +baseline for when no project is running. + +When a project runs and defines its own +`function love.keypressed(k) ... end`, `set_handlers()` wraps +it with error catching and places it in `love.keypressed`. +When the project stops, `love.keypressed` is reset to the +saved ConsoleController handler. + +`love.keypressed` is therefore a slot whose occupant changes +with application state. The framework level (`love.handlers`) +is fixed; the application level (`love.keypressed`) is +variable. + +--- + +## Routing model + +Two questions determine where a keypress lands: + +``` +Is the overlay active? + YES → UserInputController:keypressed (directly, exclusively) + NO → who occupies love.keypressed? + ConsoleController (no project running) + project's handler (project running, handler defined) + nothing (project running, no handler) +``` + +Diagram: + +``` +OS keypress + │ + ▼ +love.handlers.keypressed [Compy's, fixed, always runs] + ├─ global shortcuts [intercepted unconditionally] + ├─ overlay active? + │ YES ──────────────────────────────────────────────┐ + │ NO ↓ │ + └─ love.keypressed │ + ├─ ConsoleController:keypressed │ + │ ├─ REPL: history, Enter → evaluate_input │ + │ └─ → UserInputController:keypressed ◄───────┤ + ├─ EditorController:keypressed │ + │ ├─ mode shortcuts, Enter → _handle_submit │ + │ └─ → UserInputController:keypressed ◄───────┤ + └─ project's own handler (if defined) │ + │ + UserInputController:keypressed ◄─┘ + [shared leaf — cursor, backspace, + selection, Ctrl+C/V, Shift+Enter, + etc. Unhandled keys drop here.] +``` + +`love.handlers.keypressed` is one fixed function. The routing +variation comes entirely from (a) whether the overlay is +active and (b) what currently occupies `love.keypressed`. + +`UserInputController:keypressed` is the shared leaf at the +bottom of every branch. The overlay reaches it directly and +exclusively; the REPL and editor reach it by delegation after +handling their own context-specific keys first. Keys the +widget does not handle are silently discarded at this point — +there is no pass-through path. This is the gap the new API +addresses: a callback dispatch point inserted at the drop +boundary so unhandled (and handled) keys can surface to +project code. + +--- + +## Why REPL and editor are not project-level code + +Both the REPL and editor use `UserInputController` for text +editing, and both route through `love.keypressed`. This makes +them look like variants of the same kind of thing as a +running project. + +The difference is the level of access. Projects receive a +sandboxed `compy` API table — they interact with the +framework through a defined surface. ConsoleController is +part of the framework itself: it calls `run_project`, +`edit`, `evaluate_input`, and other internal methods that +project code never touches. + +REPL and editor are the application's own operating modes — +the state the system is in when no project is loaded. +ConsoleController is the permanent baseline handler, not a +user-level component. The shared text-editing widget +(`UserInputController`) is an implementation detail they +happen to share with the overlay; it does not make them +equivalent in any other sense. + +--- + +## Summary + +| Level | Set by | Changes when | +|---|---|---| +| `love.handlers.keypressed` | Compy framework, once at startup | Never | +| `love.keypressed` | `set_love_keypressed(CC)` at startup; `set_handlers()` when project runs | Project starts / stops | +| `love.state.user_input` | `input_text()` etc. in project env | Overlay is shown / dismissed | diff --git a/doc/development/wip/77-new-input-api/notes/plan.md b/doc/development/wip/77-new-input-api/notes/plan.md new file mode 100644 index 00000000..b5712616 --- /dev/null +++ b/doc/development/wip/77-new-input-api/notes/plan.md @@ -0,0 +1,105 @@ +# Analysis and Design Plan — Feature #77 + +Sequence of document artifacts from raw stakeholder input to +implementation. Each artifact is traceable to the previous. +Notes under `notes/` may inform any stage but are not canonical +— they are architect observations, intake context, and working +ideas. Distinguish them clearly from stakeholder input when +drawing on them. + +--- + +## Artifact sequence + +### 0. `input.md` ✅ +Verbatim stakeholder input — original ticket and stakeholder +clarification. Canonical and immutable. All architect +observations, intake notes, and derivative context live under +`notes/`, not here. + +### 1. `requirements.md` ✅ +Stakeholder needs normalised and numbered, derived from +`input.md`. Source of truth for what is asked. Do not add +derived or assumed requirements without explicit stakeholder +confirmation. + +### 2. `assessment.md` ← in progress +Maps each FR/NFR to the current architecture. For every +requirement: what exists, what is missing, what would need to +change. Does not propose solutions. Every finding cites a +specific FR/NFR by number. + +### 3. `decisions.md` +Blocking questions that must be answered before design can +begin. Initially scaffolded from open questions in +`requirements.md §5` and decision points flagged in +`assessment.md §8`. Extended by processing new input through +`notes/` and incorporating it here. + +Each entry has a question, context, what it affects, its +source, and a **Decision:** field to be filled in by +stakeholders or contributors with the authority and context +to answer it. `design.md` cannot proceed until all decisions +are settled. + +### 4. `design.md` +Architecture proposal. For every gap in the assessment: a +design decision with rationale and alternatives considered. +Every decision cites both the assessment gap it resolves and +the entry in `decisions.md` that authorized the direction. + +### 5. `api_spec.md` +The precise public contract: function names, signatures, +callback semantics, lifecycle rules, short usage examples. +No implementation detail — what a project author reads and +what tests verify against. Every API element is annotated with +the FR(s) it satisfies and the design decision that shaped it. + +### 6. `implementation_plan.md` +Ordered tasks derived from the API spec and design, with +inter-task dependencies and rough effort estimates. Every task +cites the API element or design decision it implements. +Estimates are deferred to this stage — they are only reliable +once the API surface and design decisions are settled. + +--- + +## Stakeholder summaries + +Each completed artifact has a companion summary in +`summaries/` — a short, high-level brief intended for +stakeholders who want the essentials without reading the full +document. Summaries are named after their source artifact. + +| Summary | Source | +|---|---| +| `summaries/requirements.md` | `requirements.md` | +| `summaries/assessment.md` | `assessment.md` | +| `summaries/decisions.md` | `decisions.md` | + +Summaries for `design.md`, `api_spec.md`, and +`implementation_plan.md` should be added as each is completed. + +--- + +## Traceability chain + +``` +input.md (immutable stakeholder input) + └── requirements.md (normalised FR/NFR) + └── assessment.md (each finding cites FR/NFR) + └── decisions.md (each entry cites assessment gap or FR) + └── design.md (each decision cites decisions.md entry) + └── api_spec.md (each element cites FR + decision) + └── implementation_plan.md (each task cites element) +``` + +--- + +## Notes feeding into each artifact + +| Notes file | Feeds into | +|---|---| +| `notes/requirements.md` | open questions in `design.md` | +| `notes/concerns.md` | `assessment.md`, `design.md` | +| `notes/design.md` | `design.md`, `api_spec.md` | diff --git a/doc/development/wip/77-new-input-api/notes/requirements.md b/doc/development/wip/77-new-input-api/notes/requirements.md new file mode 100644 index 00000000..f9383e82 --- /dev/null +++ b/doc/development/wip/77-new-input-api/notes/requirements.md @@ -0,0 +1,87 @@ +# Notes — Requirements + +Observations that clarify, extend, or question the formal +requirements. Not stakeholder requirements themselves. +Sources: `input.md`, intake, and review of earlier +draft documents. + +--- + +## Product framing + +Compy is positioned as a ZX Spectrum-inspired educational +platform for children. This is the lens through which the +pedagogical constraints on the API (simplicity, learnability, +not exposing framework internals) should be read. + +--- + +## Design process intent + +The stakeholder's stated approach before committing to a design: + +1. Reconstruct how the existing architecture handles user input + across modes. +2. Correlate the feature requirements against that architecture. +3. Use those inputs to produce a high-level design document, + then plan implementation. + +The requirements document and architecture assessment are +intermediate artefacts toward that design document, not +deliverables in themselves. + +--- + +## GC / allocation emphasis + +The stakeholder framed avoiding object churn as an "unspoken +project principle". The nuance beyond `agents/rules.md`: +**examples are strongly expected to follow this principle even +where older framework code does not**. New API design and any +example code using it should treat allocation-on-each-session +as a defect, not a tradeoff. + +--- + +## Dynamic prompt update — concrete pain point + +During development of the balloons example, the need arose to +change the prompt label mid-run (e.g. on a timer), but this +was not possible because the singleton guard in `input_text()` +silently drops any call made while an input is already active. Tearing +down and recreating the widget is not viable for time-driven +prompt changes. + +This is a concrete motivation for FR-3/FR-4/FR-5 (hide/show/ +remove) and for the persistent lifecycle direction in general. +It also implies a gap not fully covered by those requirements: +**the ability to update the prompt label of an already-active +edit area without a full teardown/recreate cycle**. + +Worth raising as an explicit open question before design: does +FR-1 (setup parameters) need a corresponding "update" call that +changes only the label (and optionally other parameters) without +disrupting the current text and cursor state? + +--- + +## Wall-hit boundary — multiline ambiguity + +FR-7 (cursor reaches a positional boundary) is ambiguous for +multiline input. "Boundary" could mean: end-of-current-line, +end-of-the-entire-input, or both. This distinction matters for +how a project would use the callback — an editor-like interface +(FR-12) would need to distinguish the two to implement block +navigation correctly. Needs a decision before design. + +--- + +## Text + modifier co-occurrence + +When a text character is entered while a modifier key is held +(e.g. Ctrl+letter on some keyboards, or platform-specific +combos), it is not specified whether this should produce a +text-entered notification, a key-combo notification, both, or +neither. The answer shapes both FR-5 (submit) and FR-6 +(non-character keys). Worth settling before finalising callback +signatures. diff --git a/doc/development/wip/77-new-input-api/notes/routing_unification.md b/doc/development/wip/77-new-input-api/notes/routing_unification.md new file mode 100644 index 00000000..7b087e33 --- /dev/null +++ b/doc/development/wip/77-new-input-api/notes/routing_unification.md @@ -0,0 +1,174 @@ +# Notes — Unified Routing Architecture + +A reformulation of the dispatch model that emerged from +mapping the current event delegation chain. Supersedes the +overlay-centric framing in D-3 and D-7 and provides a +cleaner structural basis for `design.md`. + +Related: `notes/love2d_handler_layers.md`, +`notes/event_delegation_chain.md`, `notes/event_routing.md`. + +--- + +## The core insight + +The `if user_input then ... else ... end` gate in +`controller.lua` is not a designed abstraction. It is the +seam left by the overlay being added as a bolt-on to the +existing routing. It encodes a special case: when a project +has active input, route exclusively to the overlay widget and +bypass everything else. + +Removing this gate and making `UserInputController` the +universal terminal sink in all branches produces a routing +model that is symmetric, has no special cases, and directly +satisfies all requirements — without adding complexity. + +--- + +## The structural change + +**Before:** two routing worlds separated by the overlay gate. + +``` +love.handlers.keypressed + ├─ global shortcuts + ├─ overlay active? + │ YES → UserInputController:keypressed [exclusive] + │ NO ↓ + └─ love.keypressed + ├─ ConsoleController → ... → UserInputController + ├─ EditorController → ... → UserInputController + └─ project's own love.keypressed [or nothing] +``` + +**After:** three symmetric branches, all terminating at the +same sink. + +``` +love.handlers.keypressed + ├─ global shortcuts + └─ love.keypressed + ├─ ConsoleController:keypressed + │ ├─ framework_handlers (Enter, history, etc.) + │ ├─ console-registered handlers / callbacks + │ └─ → UserInputController:keypressed [sink] + ├─ EditorController:keypressed + │ ├─ framework_handlers (Enter, Esc, Ctrl+M, etc.) + │ ├─ editor-registered handlers / callbacks + │ └─ → UserInputController:keypressed [sink] + └─ ProjectController:keypressed [new] + ├─ framework_handlers (Enter, Escape, etc.) + ├─ compy.handlers[combo] [project-registered] + ├─ compy.on_key_pressed [generic, noop+log] + └─ → UserInputController:keypressed [sink] +``` + +`ProjectController` is the new entity that handles the +project-running context. It follows the same three-level +dispatch pattern as the other two branches. + +--- + +## What UserInputController becomes + +In the new model, `UserInputController` is a **passive +terminal sink** — always at the bottom of every branch, +doing work proportional to its current activation state: + +- **Singleton hidden** (no active input session): sink is a + transparent no-op. Keypresses pass through handlers and + callbacks above; the sink returns without action. +- **Singleton visible** (active input session): sink processes + text-editing keys — backspace, cursor movement, selection, + copy/paste, Shift+Enter. Returns a limit signal if cursor + hits a boundary. + +The routing above the sink does not change based on whether +input is active. The "overlay active?" check is gone. + +--- + +## The singleton follows from this naturally + +Because `UserInputController` is always the sink, it must be +a singleton — there is no longer a "create overlay widget, +route to it, destroy on dismiss" lifecycle. Activation is a +state change on the singleton (`show()`/`hide()`), not a +routing change. This satisfies D-2 and NFR-1 as a structural +consequence, not as a separate design effort. + +--- + +## Why all requirements are satisfied + +| Requirement | How it is met | +|---|---| +| FR-5 submit notification | `framework_handlers['enter']` fires submit callback before sink runs Enter path | +| FR-6 non-char key notification | `compy.handlers[combo]` and `compy.on_key_pressed` fire before sink; project sees every event | +| FR-7 boundary notification | Sink's limit return value fires `on_limit_reached` hook; both project and framework observe it | +| FR-1–FR-4 lifecycle | `show()`/`hide()`/`configure()` are state changes on singleton; routing unchanged | +| NFR-1 no per-session allocation | Singleton never recreated; follows structurally | +| NFR-2 event-driven model | Three-level dispatch IS the event model; no polling required | +| FR-11/FR-12 expressiveness | Console and editor already follow the same pattern; migration replaces if-chains with handler registrations | + +--- + +## Dispatch code can be shared + +Once all three branches follow `framework_handlers → +project/context handlers → generic callback → sink`, the +dispatch logic is the same function called with different +handler tables. The three contexts become three +*configurations* of one dispatch path: + +``` +dispatch(k, keys_pressed, framework_handlers, handlers, callback, sink) +``` + +ConsoleController, EditorController, and ProjectController +each call this with their own handler tables. The +implementation is written once. + +--- + +## Relation to existing decisions + +**D-2 (singleton):** unchanged in direction; now follows +structurally from the routing model rather than being a +separate design choice. + +**D-3 (event topology):** correct. The three-level dispatch +described there now applies to all three contexts, not just +the overlay path. Scope expands; design intent unchanged. + +**D-7 (overlay-first rollout):** the *implementation* is +still overlay-first (ProjectController is the new work; +console and editor continue with existing paths for now). +Conceptually, "overlay" as a routing concept dissolves — it +is replaced by "singleton is currently visible." The gradual +rollout path is: implement ProjectController + three-level +dispatch first; console/editor branches migrate to the same +dispatch function when ready. + +--- + +## The bubbling mechanism — completion of the model + +When the singleton is active and a project registers a +handler for a key that `UserInputController` also handles +(e.g. `compy.handlers['backspace']`), both the handler and +the sink would run by default — the handler first, then the +sink deletes a character. This may or may not be the +project's intent. + +The return-value convention resolves this cleanly: a handler +that returns a truthy value signals "consumed" and the sink +does not run for that key. This is the minimal DOM-style +`preventDefault()` equivalent. It does not affect the +semantic event chains (before_submit / submit / after_submit) +which use call-order rather than return values for ordering. + +For most projects and most keys, the default (sink always +runs) is correct. The consumed signal is an opt-in escape +hatch for the cases that need fine-grained control. diff --git a/doc/development/wip/77-new-input-api/notes/solution_sketch.md b/doc/development/wip/77-new-input-api/notes/solution_sketch.md new file mode 100644 index 00000000..8dbd93aa --- /dev/null +++ b/doc/development/wip/77-new-input-api/notes/solution_sketch.md @@ -0,0 +1,192 @@ +# Solution Sketch — Feature #77 New Input API + +High-level description of the proposed implementation approach. +This feeds into `design.md`. References to D-1 through D-7 +are the decisions documented in `summaries/decisions.md`. + +--- + +## 1. `keys_pressed` table (framework level) + +`Controller` (the global controller in `controller.lua`) +maintains a `keys_pressed` table — a live set of currently +held key names, updated unconditionally on every +`love.handlers.keypressed` and `love.handlers.keyreleased` +call. This is a framework-level change invisible to project +code. + +The table is read-only from the outside: downstream consumers +receive an iterator, not a direct reference, to prevent +tampering. It is passed as a secondary argument to every +`keypressed` and `textinput` callback flowing downstream. + +Primary purpose: unified combo recognition. Replaces the +current ad-hoc pattern where each handler calls +`Key.ctrl()` / `Key.shift()` (thin wrappers over +`love.keyboard.isDown()`) at the point of handling. With the +table, any handler can serialise the current combo as a string +(e.g. `"lctrl+s"`) and look it up in a handler table without +per-handler modifier checks. This is the foundation for D-3's +three-level dispatch. + +--- + +## 2. `UserInputController` as a framework singleton (D-2) + +`UserInputController` becomes a singleton provisioned at +framework startup — created once [lazily], never destroyed, +reconfigured per context. Projects and the REPL/editor never +hold a direct reference to the object. All interaction goes +through explicit API functions in the `compy` namespace: +`show()`, `hide()`, `alter_prompt()`, `clear()`, `read()`, and +so on. + +The reconfiguration surface is expanded beyond the current +prompt-only capability: validator, highlighter, and other +per-session settings are also configurable via the API. + +Access control (preventing one subsystem from reconfiguring +an input session owned by another) is a future concern, not +in scope for this feature. + +### 2a. Legacy API compatibility (D-1) + +Existing project-level functions — `input_text()`, +`input_code()`, and related — are rewired as facade wrappers +that call the new singleton API internally. Projects that use +the legacy polling pattern (`if not r:is_empty()...`) +continue to work: the legacy wrapper implicitly registers a +submit callback that fills the reftable, preserving the +polling observation point. + +`love.state.user_input` continues to be set and cleared by +the singleton's `show()`/`hide()` methods. During the +transition it points to the singleton instance rather than a +freshly created object; it is set to nil on hide to keep the +overlay routing gate functional (see §3 below for how that +gate is ultimately removed). + +Deprecation warnings are emitted inside the legacy wrappers +in debug mode (D-1). A future flag will make legacy calls +hard-fail unless an opt-out configuration is set, providing +a clean, gradual deprecation path. + +--- + +## 3. Routing unification: `ProjectController` (D-7) + +The `if user_input then ... else ... end` gate in +`love.handlers.keypressed` (`controller.lua`) is removed. +Instead, the handler routes unconditionally to whichever +controller is currently active — `ConsoleController`, +`EditorController`, or the new `ProjectController` — exactly +as it does for the existing non-overlay contexts. + +`ProjectController` is a new controller, a sibling to +`ConsoleController` and `EditorController`, responsible for +the project-running context. It owns keypressed and textinput +handling for all project states, whether or not the singleton +input widget is currently visible. + +`UserInputController` is no longer a routing destination in +the overlay gate. It becomes the universal terminal sink at +the bottom of every branch (see `notes/routing_unification.md` +for the full diagram and rationale). + +--- + +## 4. Three-level dispatch inside `ProjectController` (D-3) + +`ProjectController:keypressed` implements the three-level +dispatch: + +``` +framework_handlers[combo] — non-overridable; Enter, Escape, + | and other structural keys live here + ↓ (if not consumed) +compy.handlers[combo] — project-registered per-combo handlers + | + ↓ (if not consumed) +compy.on_key_pressed(k, keys) — generic fallback callback (default: noop + log) + | + ↓ (if not consumed) +UserInputController:keypressed — universal sink; text-editing operations +``` + +The implementation does not have to be a literal if-else chain — fallback +lookup tables or equivalent are fine. Invocation of +`UserInputController:keypressed` could also be the default value of +`compy.on_key_pressed` rather than a hardcoded final step. + +At every level, a handler returning a truthy value signals +"consumed" and stops the chain. This is the return-value +bubbling mechanism (DOM `preventDefault()` equivalent) — an +opt-in escape hatch; the default (sink always runs) is correct +for the majority of keys and projects. + +`compy.handlers` is the project-registered handler table: a +mapping from serialised combo strings to functions. The +`keys_pressed` table from §1 makes combo serialisation +deterministic at every dispatch point. + +The same dispatch function is shared across all three +controller branches: + +```lua +dispatch(k, keys_pressed, framework_handlers, handlers, callback, sink) +``` + +Alternatively, this could be an inheritable method on a base controller +class, reducing the argument surface. + +ConsoleController, EditorController, and ProjectController each call it +with their own handler tables; the implementation is written once +(D-7 migration path). Initially only ProjectController uses it — +ConsoleController and EditorController migrate when ready. + +--- + +## 5. Enter and Escape as `framework_handlers` entries (D-4) + +Enter and Escape are handled at the `framework_handlers` level +inside `ProjectController` — non-overridable from project +space, but extensible via named callback chains (D-4): + +``` +before_submit → submit (framework) → after_submit +before_cancel → cancel (framework) → after_cancel +``` + +The framework owns the middle step (evaluate input, fill +reftable, push `'userinput'`, dismiss). Project code extends +it via the before/after hooks; by default all four hooks are +noops with a debug log entry. + +This also resolves the current Escape limitation (overlay +clears content but does not dismiss): with `cancel` as an +explicit `framework_handlers` entry, the framework's cancel +step dismisses the overlay unconditionally; `after_cancel` +gives the project an observation point. + +The `oneshot` flag on `UserInputController` is deleted as a +consequence: submit is always the framework_handlers entry's +responsibility, never the sink's. See +`notes/enter_escape_routing.md` for the full analysis. + +--- + +## 6. Migration path for ConsoleController and EditorController + +After `ProjectController` and the three-level dispatch are +validated, `ConsoleController` and `EditorController` can be +migrated to the same dispatch function incrementally (D-7). + +Migration for each is mechanical: the existing if-chains +(sequences of `if Key.ctrl() and k == 'x' then ...`) become +handler registrations in their respective `framework_handlers` +tables; the underlying handler methods are unchanged. Both +controllers already terminate at `UserInputController` as the +sink, so the structural change is at the dispatch layer only. + +Each controller's migration is a named follow-on feature, not +in scope for this one. diff --git a/doc/development/wip/77-new-input-api/notes/textinput_routing.md b/doc/development/wip/77-new-input-api/notes/textinput_routing.md new file mode 100644 index 00000000..abf24e1f --- /dev/null +++ b/doc/development/wip/77-new-input-api/notes/textinput_routing.md @@ -0,0 +1,179 @@ +# Notes — love.textinput Routing and Event Taxonomy + +Companion to `notes/love2d_handler_layers.md`, which covers +`keypressed` routing. Documents the parallel `textinput` path +and clarifies the distinction between OS-level input events +and widget-emitted lifecycle events. + +--- + +## love.textinput routing + +`love.textinput` follows a structurally identical path to +`love.keypressed` — same two-level split, same overlay gate, +same fallback slot. + +At startup: `love.textinput` is set to +`ConsoleController:textinput` via `set_love_textinput(CC)`, +symmetric to how `love.keypressed` is wired. + +``` +OS produces printable text + │ + ▼ +love.handlers.textinput [Compy's, fixed] + ├─ overlay active? + │ YES → user_input.C:textinput(t) [exclusive] + │ NO ↓ + └─ love.textinput(t) [the slot] + ├─ ConsoleController:textinput + │ ├─ [Ctrl+Shift held] → return ← filtered here + │ ├─ [error state] clear error, return + │ ├─ [editor state] → EditorController:textinput + │ └─ [else REPL] → UserInputController:textinput + │ → UserInputModel:add_text(t) + └─ project's own love.textinput (if defined) +``` + +The Ctrl+Shift filter sits in `ConsoleController:textinput`, +not in the widget. The widget never sees those characters. + +`UserInputController:textinput` has an additional guard: if +`self.result` (the reftable reference) is nil and +`app_state == 'running'`, the character is silently dropped. +An unconfigured singleton in running mode therefore rejects +all textinput automatically. + +--- + +## Combo assembly — framework or LÖVE2D? + +LÖVE2D does not assemble combos. It fires a separate +`keypressed` event for every physical key press, regardless +of what other keys are held. Combo detection is entirely the +application's responsibility. + +Currently Compy has no centralised combo assembly. Detection +is ad-hoc at the point of handling: each handler calls +`Key.ctrl()`, `Key.shift()`, etc. — thin wrappers over +`love.keyboard.isDown()` — at the moment it needs to know +modifier state. There is no "combo detected → event fired" +mechanism. + +The D-3 design introduces a `keys_pressed` table maintained +by the framework — a live set of currently held key names, +updated on `keypressed` / `keyreleased`. This replaces +scattered `isDown` calls with a single authoritative table +and makes combo serialisation (`"lctrl+s"`) and lookup +possible without per-handler modifier checks. + +--- + +## What textinput delivers + +`love.textinput` delivers a UTF-8 string — what the OS +produced from a key gesture after applying layout and +modifier translation. In the common case this is one +Unicode character. In IME-based input (CJK languages), it +can be a multi-character string delivered when the user +commits a composition sequence. LÖVE2D documents this +explicitly; `UserInputModel:add_text` handles it — it splits +the incoming string on newlines via `string.lines`, so +multi-character IME output is inserted correctly. + +Multi-line content (paste) does not arrive via `textinput` +at all. Ctrl+V is handled by `keypressed` → +`UserInputController`'s `paste()` inner function → +`love.system.getClipboardText()` → `model:paste(text)` → +`add_text(text)`. The clipboard string can be arbitrarily +long and multi-line; `add_text` handles it the same way it +handles IME strings. This path entirely bypasses +`love.textinput`. + +One platform edge case: on web builds, the space key does +not fire `textinput` in some environments. The controller +works around this by injecting `self:textinput(' ')` directly +from `keypressed` when `_G.web` is set +(`userInputController.lua:189–191`). + +--- + +## Division of labour: keypressed vs textinput + +These two events are complementary, not competing. + +`love.keypressed` handles everything structural: +backspace, delete, cursor movement (arrows, Home, End), +selection management, Shift+Enter (newline), clipboard +(Ctrl+C/X/V), and the session lifecycle keys — Enter +(submit) and Escape (cancel/reset). + +`love.textinput` handles character insertion only. When the +user presses 'a', `keypressed('a')` fires and +`UserInputController:keypressed` does nothing with it — +no branch matches a plain printable character. The +character enters the model exclusively via the subsequent +`textinput('a')` event → `add_text('a')`. + +Submit (Enter in the oneshot path) is handled entirely by +`keypressed`: `UserInputController:keypressed` → `submit()` +inner function → `model:evaluate()` → on success, fills the +reftable via `res(t)`. `textinput` has no path to session +end. + +--- + +## Event taxonomy: into vs out of the widget + +`UserInputController` is a consumer of OS-level events, not +a producer of application-level ones — with one exception. + +| Direction | Event | Origin | Meaning | +|---|---|---|---| +| → into widget | `love.textinput(t)` | OS / LÖVE2D | OS produced printable text | +| → into widget | `love.keypressed(k)` | OS / LÖVE2D | Physical key was pressed | +| ← out of widget | `love.event.push('userinput')` | Widget (oneshot path) | Successful submit only | + +`'userinput'` fires in exactly one place: `UserInputModel:handle()`, +inside `if eval then ... if ok then ... if self.oneshot`. +(`oneshot` is the flag that puts the widget in overlay mode vs. +REPL/editor mode — see `notes/enter_escape_routing.md` for a +full explanation.) +This means: non-empty input, evaluation succeeds, widget is +in oneshot mode. Cancel (Escape) calls `handle(false)`, which +skips the eval branch entirely — no event is pushed. Escape +currently resets the input content and leaves the overlay +visible; it does not dismiss it. The overlay is only ever +dismissed by successful submit. + +`handlers.userinput` in `controller.lua` receives the event +and calls `clear_user_input()` — sets `love.state.user_input` +to nil. Projects cannot subscribe to this event. There is no +`compy.on_userinput` or equivalent. The reftable is the +project's only observation point. + +There is no "text changed" event, no "character added" +callback, no mid-session outbound signal of any kind. The +view refreshes synchronously via `update_view()` calls after +each state change, but those are direct draw-state updates, +not observable events. + +--- + +## The gap this identifies + +Nothing flows out of the widget mid-session. This is the +absence that FR-5 and FR-6 address: + +- `on_text_entered` would be the outbound mid-session + character signal (currently nonexistent) +- `on_key_pressed` / `compy.handlers` would be the outbound + key signal (currently nonexistent) +- `on_limit_reached` would be the outbound boundary signal + (signal exists internally as a return value; never reaches + project code) + +Under the unified routing model (`notes/routing_unification.md`), +these signals are inserted at the `ProjectController` dispatch +layer — above the sink — so they fire before `UserInputController` +processes the event, not buried inside it. diff --git a/doc/development/wip/77-new-input-api/prompts/prompt.md b/doc/development/wip/77-new-input-api/prompts/prompt.md new file mode 100644 index 00000000..6f78324f --- /dev/null +++ b/doc/development/wip/77-new-input-api/prompts/prompt.md @@ -0,0 +1,76 @@ +# Feature #77 — New Input API: Session Prompt + +Read `CLAUDE.md` first (project overview, collaboration rules, and pointers to reference material). Then read `agents/rules.md` (coding rules and tone — pay particular attention to the tone section before writing anything analytical). + +--- + +## Current Status + +**Ready for plan step 4 — `design.md`. Awaiting stakeholder review of `summaries/decisions.md`.** + +The full artifact sequence and traceability plan is in +`notes/plan.md`. To resume: read this file, check +`notes/plan.md` for the step sequence, then proceed from the +step marked current above. + +Completed: `requirements.md` (approved), `input.md`, +`assessment.md` (approved), `decisions.md` (all seven settled, +proposed resolutions filled), `summaries/decisions.md` (ready +for stakeholder review), and all files under `notes/`. + +The old `feature_correlation.md` in this directory has been +drained into `notes/` and can be ignored. + +--- + +## Background + +This is a feature design session for **feature #77: new input API**. The work lives under `doc/development/wip/77-new-input-api/`. + +Key files in this directory: + +- `input.md` — verbatim stakeholder input (original ticket + stakeholder clarification). Canonical and immutable. Any architect observations or intake notes live under `notes/`, not here. +- `requirements.md` — normalized requirements document derived from `input.md`. +- `assessment.md` — architecture assessment mapping requirements to current code. +- `notes/` — architect notes, design ideas, concerns, and the analysis plan. See `notes/plan.md` for the full artifact sequence. +- `summaries/` — short stakeholder-facing briefs for each completed artifact. +- `feature_correlation.md` — early draft, drained into `notes/`. Can be ignored. + +--- + +## Architecture Context + +Before doing any of the tasks below, read the following — do not re-derive from source code what is already documented: + +- `doc/development/overview.md` — MVC structure, entry points, application modes +- `doc/development/internals/console.md` — project lifecycle, environments, `user_input` overlay API, `reftable` pattern +- `doc/development/internals/user_input.md` — the shared input widget, keyboard/mouse dispatch, text flow +- `doc/development/internals/editor.md` — editor input handling (relevant because the new API should ideally be expressive enough to reimplement the editor's input behaviour) + +--- + +## Task 1 — `requirements.md` + +Produce a new `requirements.md` in this directory as a proper normalized requirements document — the kind a business analyst would write. Source material is `input.md`; keep that file as-is. + +The document should: +- List discrete, unambiguous requirements — not implementation decisions +- Distinguish functional requirements from non-functional ones (GC/allocation behaviour, consistency with existing Compy idioms, pedagogical considerations for student-facing examples) +- Note explicitly what is out of scope or deferred +- Stand alone without requiring the reader to have read the raw source + +Do not propose solutions or reference implementation internals. **Stop after producing `requirements.md` and wait for the user to review it before proceeding.** + +--- + +## Task 2 — `assessment.md` (only after Task 1 is approved) + +Replace the existing `assessment.md` with a proper architecture assessment. It should: +- Map each requirement from `requirements.md` to the relevant parts of the current architecture, with file/function references +- Identify what can be reused, what needs extension, and what needs replacement +- Identify constraints and risks — including GC/allocation implications and the singleton nature of the current input overlay +- Not propose a solution — only characterise the gap between requirements and current state + +Use the existing `assessment.md` as a source of hints for gap identification, but rewrite from scratch using the architecture docs as the primary knowledge base. The inline questions and remarks in the old doc signal real uncertainties — address them with facts from the codebase rather than inheriting the speculation. + +**Stop after producing `assessment.md` and wait for the user to review it before proceeding.** diff --git a/doc/development/wip/77-new-input-api/prompts/prompt10.md b/doc/development/wip/77-new-input-api/prompts/prompt10.md new file mode 100644 index 00000000..f1b850b0 --- /dev/null +++ b/doc/development/wip/77-new-input-api/prompts/prompt10.md @@ -0,0 +1,143 @@ +# Prompt 10 — Handover / context (awaiting stakeholder feedback, round 2) + +This file is a **handover context**, not yet an actionable task. It carries the +process rules and the current state of feature #77 so a fresh agent can pick up +the moment the next stakeholder feedback lands. The owner will **augment this +file** with the verbatim feedback and the concrete task when it arrives (look +for the marker at the bottom). Until then, treat everything here as orientation. + +--- + +## Read first (orientation) + +1. `CLAUDE.md` — project overview and collaboration rules (points to the two + files below). +2. `agents/rules.md` — coding rules and **tone**. Read the tone section before + writing anything analytical: matter-of-fact and analytic, no blame; the + audience is the senior people who built the system. It also carries the + **Summaries for Stakeholders** rules (mirror their words, no internal jargon, + no second person, and do not re-explain their own requests back to them) — + apply those to any "in brief" / summary section. +3. `agents/architecture_assistance.md` — the assistant role and its limits. +4. The document chain under `doc/development/wip/77-new-input-api/` (see the + authority model below for what each file is and how much weight it carries). + +## Collaboration rules (the process) + +- Role: assistant to a senior architect — analysis, inspection, suggestions by + default. **Reviewer, not co-author**: do not rewrite the chain unless the + owner explicitly asks for edits. When edits are requested, the owner reviews + all changes and handles commits/stashes. +- **Git:** read-only operations (`git show`/`diff`/`log`) are expected and + encouraged. No operations that modify history — no commits, rebases, amends, + force operations, or `.git` tampering. Staging only on explicit confirmation. +- **Local file operations are permitted** (read, write, edit, grep, sed, and + similar, including a justified `cd`-with-output-redirect). Write outputs **to + disk** — create the report/file directly, do not just print it in + conversation. +- No GitHub interaction, no agent/subagent spawning. + +## Naming / numbering rule (carried across prompts) + +In a signature `$dirname/$filename.md`, N for a round is in principle +`echo $(( $(ls "${dirname}/${filename}*.md" | wc -l) + 1 ))`. **The raw count +under-counts when there is a numbering gap** (e.g. `prompts/` has no +`prompt1`/`prompt2`, so the count gives 8 while the highest file is `prompt9`). +When that happens, infer the logic and use the next sequential number, not the +raw count — this file is `prompt10.md` for that reason. The same applies to the +round/validation artifacts: the next re-evaluation round is `round2.md` / +`changes2.md` / `outcome2.md`, and the next validation is `check2.md`. + +--- + +## The document chain and the authority model + +Authority gates on *whether a human actually decided*, not on which file a line +sits in. + +- **Stakeholder ground truth:** `input.md` only — may not be overruled. It now + contains two settled things: the original ticket/clarification, and **feedback + round 1** (the "FEEDBACK AFTER FIRST ITERATION" section). +- **D-1 is settled: DISCARDED.** Stakeholder consensus in `input.md` round 1 — + no backward compatibility. The five legacy text-input globals (`input_text`, + `input_code`, `validated_input`, `user_input`, `write_to_input`) are + **removed**, not wrapped; examples migrate to `compy.input.*`. This outranks + everything downstream. +- **D-9 is a separate surface and is retained.** The native + `love.keypressed`/`textinput` coexistence path is what keeps the stakeholders' + "only text fields break" guarantee true. It is not part of the D-1 discard. +- **Still a local proposal:** `decisions.md` D-2…D-10 — awaiting a single + eventual stakeholder approve/veto pass. Provisional; same tier regardless of + the round each was added in. +- **High human input (local, traces `input.md`):** `requirements.md`. Its §5 + backward-compat question is now marked RESOLVED (clean break). +- **Derived:** `design.md`, `spec.md`, `roadmap.md`, `assessment.md`, + `summaries/*`, `README.md` — must stay consistent with the tiers above and + with each other. Note: `assessment.md` describes *today's* code, so its + reftable/polling references are factual, not stragglers. + +--- + +## History so far (timeline) + +1. **Initial analysis chain** — three independent validation rounds + (`validation/validation_report_{1,2,3}.md`) and two local design rounds + (`validation/recommendations_{1,2}.md` + `changelog_{1,2}.md`). Ended **PASS + WITH NOTES**, ready for stakeholder review. +2. **Feedback round 1** (`input.md` "FEEDBACK AFTER FIRST ITERATION"; commit + `de5d781`): **D-1 DISCARDED** — no backward compatibility; remove the legacy + API, migrate the examples. +3. **Re-evaluation round 1** (commit `c94fb57`): the chain was edited end to end + to apply the discard. Recorded in `reevaluations/round1.md` (reasoning), + `reevaluations/changes1.md` (file-by-file changelog), `reevaluations/outcome1.md` + (summary). M3 was voided (numbering kept), M8 (legacy removal + example + migration) was added, both estimate tables recomputed. +4. **Check 1** (`reevaluations/check1.md`): independent validation of the + re-evaluation → **PASS**. Edit faithfulness, cross-document consistency, + roadmap ordering, and the estimates (recomputed from scratch: ≈ 63 h without + LLM / ≈ 37 h with) all checked out. Two NOTE-level doc nits were found and + **fixed in the same pass at the owner's request** (a stale D-8 glance-table + cell in `summaries/decisions.md`; a loose M8 dependency header in + `roadmap.md`). Recommendation: **Ready for stakeholder review.** + +--- + +## Where we are now + +- The chain is internally consistent and reflects the D-1 discard end to end. +- The build plan is **M1 → M8** (M3 is intentionally void; numbering preserved + so M4–M7 cross-references stay valid). +- **Awaiting the next round of stakeholder feedback.** The standing open item is + the single **approve/veto pass over the D-2…D-10 proposal set** (D-1 is now + settled). Owner-delegated and non-blocking: the convert-or-exclude call for + `repl`/`guess`/`valid`/`turtle`, and the arrival/migration of the out-of-repo + `maze` showcase. +- Nothing in the current state blocks implementation starting at M1. + +--- + +## When feedback arrives (anticipated task shape — owner to confirm/replace) + +The pattern in this project (see `prompt7.md`/`prompt9.md`) has been: given new +stakeholder feedback, determine whether the chain can be updated **unambiguously +without owner resolution**; if yes, apply it and record the work; if not, +surface the specific decisions needed rather than guessing. Concretely, the +likely next steps are: + +1. Read `input.md` (the new feedback) and the commit that introduced it. +2. Judge each change against the authority tier it touches (a `D-2…D-10` + approve/veto is a *settling* of the local proposal; a brand-new constraint is + ground truth that may cascade). +3. If unambiguous: apply it across the chain and write `reevaluations/round2.md` + (reasoning + judgment calls), `reevaluations/changes2.md` (file-by-file + changelog), and `reevaluations/outcome2.md` (summary). Keep milestone + numbering stable; void rather than renumber if something is dropped. +4. Follow with an independent validation pass (`reevaluations/check2.md`) on the + same bar used for `check1.md`: edit faithfulness, cross-document consistency, + roadmap ordering, and estimates recomputed from scratch. +5. Report where things stand. + + + + + diff --git a/doc/development/wip/77-new-input-api/prompts/prompt3.md b/doc/development/wip/77-new-input-api/prompts/prompt3.md new file mode 100644 index 00000000..e6ded96d --- /dev/null +++ b/doc/development/wip/77-new-input-api/prompts/prompt3.md @@ -0,0 +1,288 @@ +# Prompt 3 — Actualize decisions, produce design, spec, roadmap, and estimates + +Read `CLAUDE.md` first (project overview, collaboration rules, and pointers to +reference material). Then read `agents/rules.md` (coding rules and tone — pay +particular attention to the tone section before writing anything analytical). + +## Permissions + +Write all output files to disk. You are expected to create and edit files +directly — do not just describe what the documents would contain. Any local +file operation is permitted: read, write, edit, search, grep, sed, and similar +tools. The only prohibited operations are those that modify git history: +no commits, no rebases, no amends, no force operations. + +--- + +## Project context + +**Compy** is a console-based, Lua-programmable fantasy computer for children, +built on LÖVE2D v11.5. MVC architecture, Lua 5.1/LuaJIT. The codebase is at +`src/`. Architecture and conventions are documented under `doc/development/`. + +This is pre-implementation work for **Feature #77 — New Input API**. All +working documents are under `doc/development/wip/77-new-input-api/`. Read +`doc/development/overview.md` if you need orientation to the overall +architecture. Read `doc/development/internals/` for subsystem detail. + +--- + +## What has been done + +The following work is complete and recorded in notes. Read these files before +writing anything. They represent the actual final design — not earlier drafts. + +### Primary source material (read first, in order) + +1. `input.md` — original feature request from stakeholders +2. `requirements.md` — formalised functional and non-functional requirements +3. `assessment.md` — current architecture assessment and problem framing +4. `decisions.md` — seven blocking decisions and their proposed resolutions + + **Important:** `decisions.md` was written before the routing unification + insight below. Some passages reference a "proto-design" (overlay-centric, + overlay gate preserved) that was superseded. The per-decision resolutions + (D-1 through D-7, quick reference table) are still correct in direction, + but the architectural framing needs to be updated to reflect the final + design. + +5. `notes/solution_sketch.md` — **the primary design source**. Organised + description of the full solution in six sections. Read this as the + authoritative statement of what is being built. + +### Supporting notes (read for detail and rationale) + +- `notes/routing_unification.md` — the core architectural insight: removal + of the overlay gate, introduction of `ProjectController`, `UserInputController` + as universal terminal sink. Contains before/after routing diagrams. +- `notes/love2d_handler_layers.md` — LÖVE2D two-level handler architecture + and how Compy's routing maps onto it. Contains the corrected routing diagram. +- `notes/event_delegation_chain.md` — current four-context routing chain + with architectural evaluation. +- `notes/enter_escape_routing.md` — Enter and Escape routing across all + contexts; includes `oneshot` flag analysis and why it is deleted in the + new design. +- `notes/textinput_routing.md` — `love.textinput` routing, combo assembly, + keypressed vs textinput division of labour, event taxonomy. +- `notes/design.md` — earlier ad-hoc design proposals. Treat as background + context; the solution_sketch supersedes it. +- `notes/decisions.md` — extended rationale behind the decisions. Use for + understanding context; do not treat as design source. + +### Existing summaries (stakeholder-facing, will be updated/created) + +- `summaries/requirements.md` — approved +- `summaries/assessment.md` — approved +- `summaries/decisions.md` — exists, needs updating + +--- + +## Tasks + +Execute these tasks in order. Each task produces one or more files written to +disk. Do not stop between tasks — complete all five. + +--- + +### Task 1 — Actualize `decisions.md` and `summaries/decisions.md` + +Update `decisions.md` (the detailed decisions document) to reflect the final +design as described in `notes/solution_sketch.md` and +`notes/routing_unification.md`. + +Specifically: +- Replace any "proto-design" framing (overlay gate preserved, overlay-centric + routing) with the final design (overlay gate removed, `ProjectController` + introduced, `UserInputController` as universal sink). +- The seven decisions D-1 through D-7 and their resolutions are correct in + direction — keep them. Update wording where it assumes the old routing model. +- Confirm that `oneshot` flag deletion is a consequence of D-2 + D-4, and + note this explicitly. +- The per-decision detail sections lower in `decisions.md` — update any that + refer to the old routing model. Do not change decisions that are unaffected. + +Then update `summaries/decisions.md` to match. The summary is stakeholder-facing: +engineers with limited time. It should be concise, include the D-1 through D-7 +quick reference table, and accurately reflect the final design framing without +requiring the reader to understand the routing internals. + +--- + +### Task 2 — Write `design.md` and `summaries/design.md` + +Write `design.md` as the primary design document for stakeholder and implementor +review. Source: `notes/solution_sketch.md` as the skeleton, enriched with detail +from the supporting notes listed above. + +`design.md` must cover: + +1. **Problem and scope** — one paragraph: what the feature adds and what it + does not touch (console/editor migration is out of scope). + +2. **Architectural approach** — the routing unification: removal of the overlay + gate, introduction of `ProjectController`, `UserInputController` as universal + terminal sink. Include the before/after routing diagram from + `notes/routing_unification.md`. + +3. **Component layout** — what each new or changed component does: + - `keys_pressed` table (where it lives, who owns it, how it is passed downstream) + - `UserInputController` singleton (lifecycle: created once, reconfigured via API) + - `ProjectController` (new controller, sibling to ConsoleController/EditorController) + - `compy` API additions (`show`, `hide`, `configure`, `compy.handlers`, + `compy.on_key_pressed`, `compy.on_text_entered`, before/after chains) + +4. **Three-level dispatch** — `framework_handlers → compy.handlers[combo] → + compy.on_key_pressed → UserInputController sink`. Include the dispatch diagram. + Note that the dispatch function is shared across all three controller branches + (written once; only ProjectController uses it initially). + +5. **Enter and Escape handling** — framework_handlers entries, before/after chains, + resolution of the current Escape-does-not-dismiss limitation. + +6. **Legacy API compatibility** — facade wrappers, reftable preserved, deprecation + path. + +7. **Implementation order and migration path** — confirm and state explicitly that + the six-section order in `notes/solution_sketch.md` is implementation-dependency + aware: each step can be tested before the next begins, and the console/editor + migration is a clean follow-on, not a prerequisite. Validate this claim — check + that no later step requires a later step to be partially complete. + +Write `summaries/design.md` as a condensed version suitable for a stakeholder who +will spend 10–15 minutes reviewing. Include the before/after routing diagram (it is +self-explanatory) and the component table. Omit the implementation order section +(that is roadmap territory). + +--- + +### Task 3 — Write `spec.md` and `summaries/spec.md` + +Write `spec.md` as the specification document — the contract that implementation +is verified against. Stakeholders are engineers; include function names, signatures, +and data structure formats. Be precise. + +`spec.md` must cover: + +1. **`keys_pressed` table** + - Format of the table (key names as strings, values as `true`) + - How it is passed downstream (iterator or read-only proxy — specify which + and why) + - Combo serialisation format (e.g. `"lctrl+s"` — specify key ordering + convention: modifiers first, main key last, alphabetical among modifiers) + +2. **`UserInputController` singleton API** (the `compy` namespace surface) + - `compy.show(config)` — what `config` accepts (prompt, validator, highlighter, + multiline flag, etc.) + - `compy.hide()` — behavior (does it clear content? fire cancel chain?) + - `compy.configure(config)` — mid-session reconfiguration (which fields are + live-updatable) + - `compy.clear()` — clears input content without hiding + - Access control: what happens if `show()` is called while already active + +3. **Event callbacks** + - `compy.on_text_entered(text, keys_pressed)` — when it fires, what `text` + contains (from textinput path), what `keys_pressed` contains + - `compy.on_key_pressed(k, keys_pressed)` — when it fires (non-character + structural keys; NOT called for plain character keys where textinput fires) + - `compy.handlers[combo]` — registration format, combo string format, + return-value bubbling convention (truthy = consumed, sink does not run) + - `before_submit(keys_pressed)`, `after_submit(result)` — signatures, + ordering guarantees + - `before_cancel(keys_pressed)`, `after_cancel()` — signatures, whether + `before_cancel` can suppress the cancel + +4. **`on_limit_reached(direction)`** — when it fires, what `direction` values + are defined, second argument reserved note + +5. **Legacy API compatibility** + - Which functions are rewired as facades: `input_text()`, `input_code()`, + `validated_input()`, `user_input()` + - What they do internally (configure + show singleton, register reftable-fill + submit callback) + - Deprecation warning: when emitted, how suppressed + - `love.state.user_input`: still set/cleared; points to singleton; nil when hidden + +6. **`ProjectController`** + - Activation: when it becomes the active controller (project running, + app_state = `running` or `project_open`) + - Deactivation: on project stop + - Relationship to `love.keypressed` slot (does it replace the slot, or is + it called from it?) + +7. **Edge cases** + - Project calls `show()` while already active: specify behavior + - Project stops while input is active: specify what happens + (silent hide, cancel chain, or error) + - Evaluation failure (validator returns error): input locks until acknowledged + (existing behavior preserved) + +Write `summaries/spec.md` as a condensed version. Include all function signatures +and the callback table. Omit the edge case section (that is implementor-level +detail). Keep descriptions to one sentence per item. Stakeholders can read this +in 5 minutes and confirm the API surface is correct. + +--- + +### Task 4 — Write `roadmap.md` and `summaries/roadmap.md` + +Write `roadmap.md` as the implementation roadmap. It is scoped to this feature only. + +Structure as milestones, in implementation-dependency order (matching the order +in `notes/solution_sketch.md`). For each milestone: +- Name and one-line description +- Input (what must be done before this milestone starts) +- Output (what is verified/testable at the end) +- Files created or modified (list by path) +- Risk or note (one line, if any) + +Milestones: +1. `keys_pressed` table — framework-level, zero behaviour change +2. `UserInputController` singleton extraction — move construction to startup, + zero behaviour change (verifiable by running existing tests) +3. Legacy API facade wrappers — rewire `input_text()` etc., verify all existing + examples still work +4. `ProjectController` introduction + overlay gate removal — new controller, + routing change, existing behaviour preserved via sink +5. Three-level dispatch in `ProjectController` — `compy.handlers`, + `compy.on_key_pressed`, return-value bubbling +6. Before/after chains for submit and cancel — D-4, resolves Escape limitation +7. Extended singleton API — `compy.configure()`, `compy.clear()`, live + reconfiguration of validator/highlighter + +Include two separate scope items (not milestones, but effort blocks listed after +the milestones): +- **Documentation updates**: update `doc/development/internals/` for the new + input subsystem; update `doc/development/overview.md` if architecture section + is affected; deprecate/archive stale wip notes after release. +- **Test coverage**: busted tests for `keys_pressed` table, singleton lifecycle, + dispatch chain (each level), legacy API compatibility, edge cases from spec §7. + +Write `summaries/roadmap.md` as a one-page milestone table: name, output/ +deliverable, key files touched. Omit risk notes and file lists. + +--- + +### Task 5 — Implementation estimates + +Add an `## Estimates` section to the bottom of `roadmap.md`. + +Assume implementor: senior engineer, solo, full ownership of the codebase. +Familiar with Lua and LÖVE2D. Comfortable with the architecture (has read the +design and spec documents). + +Provide two estimates: +- **Without LLM assistance** — traditional implementation +- **With LLM assistance** (Claude Code or equivalent for code generation, + refactoring, test scaffolding, and doc updates) + +For each milestone and effort block, give a range (optimistic–realistic) in +hours or days. Sum to a total range per estimate set. + +If confident, apply a three-point PERT to the totals: + `E = (O + 4M + P) / 6` +where O = optimistic total, M = most likely total, P = pessimistic total. +State the PERT estimate explicitly as a single number and note its confidence +basis. + +Note any milestones where LLM assistance is unlikely to save significant time +(e.g. milestones that are mostly integration/wiring rather than generative work). diff --git a/doc/development/wip/77-new-input-api/prompts/prompt4.md b/doc/development/wip/77-new-input-api/prompts/prompt4.md new file mode 100644 index 00000000..92054a0c --- /dev/null +++ b/doc/development/wip/77-new-input-api/prompts/prompt4.md @@ -0,0 +1,204 @@ +# Prompt 4 — Independent chain validation + +Read `CLAUDE.md` first (project overview, collaboration rules, and pointers to +reference material). Then read `agents/rules.md` (coding rules and tone — pay +particular attention to the tone section before writing anything analytical). + +## Permissions + +Write all output to disk. You are expected to create files directly — do not +just print the validation report in conversation. Any local file operation is +permitted: read, write, edit, search, grep, sed, and similar tools. The only +prohibited operations are those that modify git history: no commits, no +rebases, no amends, no force operations. + +Write the validation report to: +`doc/development/wip/77-new-input-api/validation_report.md` + +--- + +## Purpose + +Perform an independent review of the full feature #77 document chain — +from the original feature request through to the implementation roadmap. +The goal is to surface any gaps, contradictions, or unsupported claims +before stakeholder review and before implementation begins. + +You are a reviewer, not a co-author. Do not rewrite documents — report +findings only. Be specific: cite document name, section, and the exact +claim or gap. + +--- + +## Project context + +**Compy** is a console-based, Lua-programmable fantasy computer for children, +built on LÖVE2D v11.5. MVC architecture, Lua 5.1/LuaJIT. The codebase is at +`src/`. Architecture documentation is under `doc/development/`. + +Feature #77 adds a callback-based event API to the project input overlay. +All working documents are under `doc/development/wip/77-new-input-api/`. + +--- + +## The document chain + +Read these documents in order. Each builds on the previous. + +| # | File | Role | +|---|---|---| +| 1 | `input.md` | Original stakeholder feature request | +| 2 | `requirements.md` | Formalised FR and NFR derived from input | +| 3 | `assessment.md` | Current architecture assessment; problem framing | +| 4 | `decisions.md` | Seven blocking decisions and their resolutions | +| 5 | `design.md` | Architectural design: components, routing, API surface | +| 6 | `spec.md` | Specification: precise contracts, signatures, edge cases | +| 7 | `roadmap.md` | Milestones, effort blocks, estimates | + +Summaries (`summaries/*.md`) are condensed versions of the above. Validate +that each summary accurately represents its source — no claims present in +the summary that are absent or contradicted in the full document. + +--- + +## Supporting material + +Use these to verify factual claims about the current codebase. Do not treat +them as design authority — they are observations about the current state. + +- `notes/routing_unification.md` — routing before/after diagrams +- `notes/love2d_handler_layers.md` — LÖVE2D handler architecture +- `notes/event_delegation_chain.md` — current four-context routing +- `notes/enter_escape_routing.md` — Enter/Escape behavior across contexts +- `notes/textinput_routing.md` — textinput path, combo assembly, event taxonomy +- `notes/solution_sketch.md` — design source that informed design.md + +If a claim in the chain documents can be verified against the codebase, +do so. Relevant source files: +- `src/controller/controller.lua` — global handler, overlay gate, lifecycle +- `src/controller/consoleController.lua` — REPL and editor routing +- `src/controller/userInputController.lua` — widget keypressed/textinput +- `src/controller/editorController.lua` — editor mode dispatch, passthrough +- `src/model/input/userInputModel.lua` — evaluate, handle, oneshot, cancel + +--- + +## What to validate + +Check each of the following dimensions. Report findings per dimension. + +### 1. Requirements coverage + +For every functional requirement (FR-1 through FR-N) and non-functional +requirement (NFR-1 through NFR-N) in `requirements.md`: +- Is it addressed by at least one decision in `decisions.md`? +- Is the addressing decision carried through into `design.md`? +- Is the design point covered in `spec.md`? +- Is implementation of that spec point present in at least one `roadmap.md` + milestone? + +Flag any requirement that drops out at any step. + +### 2. Decision consistency + +For each decision D-1 through D-7 in `decisions.md`: +- Is the stated resolution actually reflected in `design.md`? +- Is the resolution carried through consistently into `spec.md`? +- Does any later document contradict or silently override a decision? + +Flag any decision whose resolution is asserted but not implemented in +the downstream documents. + +### 3. Design-to-spec completeness + +For every component and API surface described in `design.md`: +- Does `spec.md` provide a precise contract for it? +- Are signatures complete (parameter names, types, return values)? +- Are edge cases covered (what happens when called in wrong state, + with missing arguments, or during a conflicting operation)? + +Flag any design element that has no corresponding spec entry, or any +spec entry that references a component not described in design. + +### 4. Spec-to-roadmap coverage + +For every spec item (API function, callback, data structure, edge case): +- Is there a roadmap milestone that delivers it? +- Is documentation for it included in the documentation effort block? +- Is a test for it (or a test category covering it) in the test effort block? + +Flag any spec item with no roadmap home. + +### 5. Roadmap ordering validity + +The milestones in `roadmap.md` claim to be implementation-dependency aware — +each milestone is testable independently before the next begins. +- Verify this claim for each consecutive milestone pair. +- Identify any milestone that implicitly depends on a later milestone. + +### 6. Factual accuracy against codebase + +Check the following specific claims against the source files listed above: +- The overlay gate (`if user_input then ... else ... end`) exists in + `controller.lua` at the described location. +- `UserInputController:keypressed` is the shared sink for both REPL/editor + branches and the overlay branch. +- `oneshot` flag controls the submit path in `UserInputController`. +- `love.state.user_input` is currently set by the project input functions + and read by the overlay gate. +- `ConsoleController` is wired into `love.keypressed` at startup. +- The before/after chain mechanism does not yet exist in the codebase + (i.e. it is a new addition, not a refactor of existing chains). + +Report any claim that does not match observed code. + +### 7. Summary fidelity + +For each summary in `summaries/`: +- Does it accurately represent the full document it summarises? +- Does it contain any claim not present in the full document? +- Does it omit anything that a stakeholder would consider a decision-relevant + fact (vs. implementation detail)? + +--- + +## Output format + +Produce a structured validation report. Use this structure: + +``` +# Validation Report — Feature #77 Document Chain + +## Status: PASS / PASS WITH NOTES / FAIL + +## Findings by dimension + +### 1. Requirements coverage +[table or list: each FR/NFR, status (covered / gap), note] + +### 2. Decision consistency +[list: each decision, status, note if issue found] + +### 3. Design-to-spec completeness +[list: gaps found, or PASS] + +### 4. Spec-to-roadmap coverage +[list: gaps found, or PASS] + +### 5. Roadmap ordering validity +[list: dependency issues found, or PASS] + +### 6. Factual accuracy +[list: each checked claim, CONFIRMED / MISMATCH, note if mismatch] + +### 7. Summary fidelity +[list: per summary, status, note if issue found] + +## Summary of actionable items +[numbered list of issues that must be resolved before implementation] +``` + +Be specific. "Gap found" is not useful. "FR-6 (non-character key notification) +is addressed by D-3 in decisions.md and present in design.md §4, but +spec.md §3 does not specify what `k` contains when a function key is pressed" +is useful. diff --git a/doc/development/wip/77-new-input-api/prompts/prompt5.md b/doc/development/wip/77-new-input-api/prompts/prompt5.md new file mode 100644 index 00000000..7c722dc3 --- /dev/null +++ b/doc/development/wip/77-new-input-api/prompts/prompt5.md @@ -0,0 +1,249 @@ +# Prompt 5 — Apply the round-1 recommendations to the document chain + +Read `CLAUDE.md` first (project overview, collaboration rules, and pointers to +reference material). Then read `agents/rules.md` (coding rules and tone — pay +particular attention to the tone section: matter-of-fact and analytic, no +blame, no salesmanship, before writing anything). + +## Permissions + +Write all output to disk. You are expected to edit the chain documents +directly. Any local file operation is permitted: read, write, edit, search, +grep, sed, and similar tools including cd with output redirection (if used for the stated purpose). +The only prohibited operations are those that modify +git history: no commits, no rebases, no amends, no force operations, no touching .git directory directly. + +--- + +## Purpose + +The feature #77 document chain was independently reviewed (result: FAIL — not +because the direction is wrong, but because of completeness and cross-document +consistency gaps). A round of local design discussion then resolved every +actionable item from that review. Your job is to **apply those resolutions to +the chain**, faithfully and completely, so the chain becomes internally +consistent and ready for a second independent review. + +You are an executor here, not a co-designer. The resolutions are already +decided. Do not re-litigate them, and do not invent new structure. Where a +resolution is ambiguous to you, prefer the simpler reading and record the +ambiguity in an open-questions appendix (see Output) — do **not** resolve it by +adding machinery. + +--- + +## Project context + +**Compy** is a console-based, Lua-programmable fantasy computer for children, +built on LÖVE2D v11.5. MVC, Lua 5.1/LuaJIT. Code is at `src/`. Architecture +docs are under `doc/development/`. Feature #77 adds a callback-based event API +to the project input overlay. All working documents are under +`doc/development/wip/77-new-input-api/`. + +--- + +## Your two inputs (read these first, in full) + +| File | What it is | How to treat it | +|---|---|---| +| `validation/recommendations_1.md` | The **executable spec** for this task. Ten resolved items, each with grounding, the concrete change, and downward-doc targets. | **Authoritative for this task.** Apply it. | +| `validation/validation_report_1.md` | The independent review that produced the items. Explains *why* each change is needed and cites exact locations. | Reference / rationale. Use it to locate things and to understand intent. | + +`recommendations_1.md` opens with a **"Document authority model"** section. +Read it carefully — it governs which documents you may freely rewrite and which +you must not. + +--- + +## The authority model (governs every edit you make) + +Only one document carries stakeholder authority. Everything downstream is a +**local proposal** derived from it, awaiting a single eventual stakeholder +approve/veto review. So there is no "human-approved" tier below `input.md` to +protect — the tiers below rank *depth of human input within the local +proposal*, not approval status. + +**Never edit (stakeholder ground truth):** +- `input.md` — the only document with stakeholder authority. + +**Do not edit (high human input; recommendations require no change here):** +- `requirements.md` — locally formalised FR/NFR, closely tracing `input.md`. + Not stakeholder-signed, but high-authority within the local proposal. The + recommendations confirm no edits are needed (the 2D cursor contract is + consistent with its generic "cursor position" wording). Leave it unless an + item explicitly requires a change — none do. + +**Edit freely (derived):** +- `design.md`, `spec.md`, `roadmap.md`, `assessment.md`, and everything in + `summaries/`. These were generated downward and inherit the gaps the review + found. Bring them in line with `recommendations_1.md`. + +**Edit with provenance tagging (`decisions.md` and `summaries/decisions.md`):** +- `decisions.md` is a *local* artifact end to end: D-1…D-7 came from an earlier + local design round, and the round-1 resolutions are a second local round. + Neither has stakeholder sign-off; the whole file is a proposal awaiting one + eventual approve/veto. So you are **not** promoting local resolutions into a + "human-approved" tier (there is none below `input.md`). What matters is + **traceability** — mark which round each new or changed decision came from — + see next section. + +--- + +## How to handle `decisions.md` (provenance rules) + +The goal: incorporate the round-1 resolutions that constitute **genuine new +conflict points or new architectural commitments** back into `decisions.md` +(and its summary), with clear **traceability** of which round each change came +from. The whole file is a local proposal — you are not misrepresenting +authority by recording them, only keeping the delta auditable. + +0. **Add a status note at the top of `decisions.md`** (and its summary) stating + that the entire file is a *local design proposal* derived from `input.md`, + pending a single eventual stakeholder approve/veto review — no entry here has + been stakeholder-signed. +1. **New commitments → new decision entries.** Give them fresh IDs continuing + the existing sequence (the file currently runs D-1…D-7, so start at D-8). + Use the same entry shape as the existing decisions (Question / Context / + Affects / Source / decision). +2. **Corrections to existing decisions → annotate in place.** Where round 1 + corrected or superseded an existing decision (e.g. D-3 dispatch tiers, D-4 + `oneshot`/submit prose, D-6 character/key channels), edit the entry and add + a short provenance note on the change. +3. **Tag every round-1-originated change** with a clear, consistent marker, for + example: *"(Origin: local design round 1, 2026-06. See + `validation/recommendations_1.md` Item N.)"* Use the same marker text + everywhere so a reader (and the next reviewer) can grep it. (You need not + retro-tag D-1…D-7; the top-of-file status note covers their local origin.) +4. **Only promote the genuine decisions.** Pure downward-doc reconciliations + (e.g. fixing a combo example, fixing a file-path reference, renaming an + argument) do **not** need a decisions.md entry — just fix them in + design/spec/roadmap. Use judgement: if it resolves a contradiction or makes + a new architectural choice, it belongs in decisions.md with provenance; if + it is mechanical alignment, it does not. + +Candidates that likely warrant a decisions.md entry or annotation (confirm +against `recommendations_1.md`; this list is guidance, not a mandate): +- **Item 3** — three-tier dispatch; the sink is the *default value* of + `compy.on_key_pressed`, not a separate level (corrects D-3 / supersedes the + four-tier elaboration). +- **Item 4** — native `love.keypressed`/`textinput` coexistence via transparent + auto-provisioning, gated by a legacy heuristic with transition diagnostics + (genuinely new; the chain never decided this). +- **Item 6** — two independent channels, no exclusivity; the "no double + callback" suggestion in D-6 is dropped (note: D-6 was only a *"Suggested + decision"* and was auto-generated filler never given design attention in the + prior round — weaker even than the other local D's — so dropping it is + striking filler, not overturning a considered decision; say so). +- **Item 7** — canonical combo form (modifier-first, generic l/r folding), + metatable-normalised registration, overloadable matcher (extends D-3's terse + "combo lookup by serialised key names"). +- **Item 1** — restored FR-8/9/10 and the 2D `(line, col)` cursor contract + (new explicit commitment; previously dropped by a derivation error). +- **Item 2** — `oneshot` deletion belongs with M6 framework-owned submit + (corrects D-4 prose and the roadmap; mostly a sequencing fix, but the D-4 + prose correction is worth annotating). + +--- + +## What to do, item by item + +For each of the ten items in `recommendations_1.md`, apply the resolution to +**every** document it names under its "Downward-doc updates" / "Roadmap +placement" / "Status" subsections, plus any summary that mirrors that content. +The recommendations already tell you the targets per item; follow them. Below +is the cross-cutting target matrix so nothing is missed. + +| Item | Primary targets | Also update | +|---|---|---| +| 1 (+5) — FR-8/9/10, 2D cursor, `write_to_input` facade | `design.md §3` (component table), `spec.md §2/§5`, `roadmap.md M3/M7` | `summaries/design.md`, `summaries/spec.md`, `summaries/roadmap.md`; `decisions.md` (new entry, provenance) | +| 2 — `oneshot` deletion → M6; submit ordering | `roadmap.md` M2/M3/M6 (+ file lists) | `summaries/roadmap.md`; `decisions.md` D-4 prose (annotate) | +| 3 — three-tier dispatch; sink = default callback | `design.md §4`, `spec.md §3` | `summaries/design.md`, `summaries/spec.md`; `decisions.md` D-3 (annotate) | +| 4 — native handler coexistence (auto-provision + heuristic) | `design.md §2/§3/§6`, `spec.md §6` | `summaries/design.md`, `summaries/spec.md`; `decisions.md` (new entry, provenance) | +| 6 — two channels, no exclusivity | `spec.md §3`, `decisions.md` D-6 (supersede) | `summaries/spec.md`, `summaries/decisions.md` | +| 7 — combo canonical form, metatable, matcher | `spec.md §1/§3`, `design.md §4`, `roadmap.md M1/M5` | `summaries/*` as mirrored; `decisions.md` D-3 (annotate/extend) | +| 8 — codebase-reference fixes | `assessment.md §2/§8`, `design.md §3`, `spec.md §3/§6`, `decisions.md` D-4/D-7 (factual prose), `roadmap.md` M2 file target | `summaries/assessment.md`, `summaries/decisions.md` | +| 9 — D-7 FR-11/FR-12 walkthrough | `design.md §7` (add the walkthrough) | `summaries/design.md` if it asserts coverage | +| 10 — smaller drifts (`user_input()`, `on_text_entered` arg, `isrepeat`, `compy.show/hide` milestone, Escape opt-in drop, dead alias) | `spec.md §3/§5/§7`, `roadmap.md M2/M3`, `summaries/design.md` (remove orphan Escape remark) | mirrored summaries | + +When an item folds another (e.g. Item 5 folds into Item 1; Item 10.5 folds into +Item 6), do not duplicate — apply once and cross-reference. + +The above is **Stage 1** — the functional / architectural edits (Items 1–10). + +--- + +## Stage 2 — Namespace relocation (`compy.input.*`) — separate pass + +`recommendations_1.md` has a section **"Namespace isolation — relocate the new +API under `compy.input.*`"**. Apply it as a **distinct pass, after Stage 1 +is complete and internally consistent** — do not interleave it with the Stage 1 +edits. + +- It is a mechanical rename/reparent: every *new* `compy.X` name introduced by + feature #77 (including the Item 1 cursor/text functions) becomes + `compy.input.X`, across `design.md`, `spec.md`, `roadmap.md`, and the + mirrored summaries. No behavioural change, no signature change. +- **Do not move** the legacy global facades (`input_text()`, `input_code()`, + `validated_input()`, `user_input()`, `write_to_input()`) — they stay as + project-env globals; only their internal calls now target `compy.input.*`. +- **`keys_pressed` proxy:** keep it global (`compy.keys_pressed`), **not** + under `compy.input` — decided; it is raw keyboard state (physical reality), + not the input-manipulation layer. Do not reparent it. +- Follow the scope table and mechanics in that recommendation section exactly. + Do not rename anything it does not list. + +Record this pass as its own row in the changelog, separate from the Item 1–10 +rows, so the relocation can be reviewed independently of the behavioural edits. + +--- + +## Hard guardrails (read before editing) + +1. **`recommendations_1.md` wins.** Where it and an existing chain document + disagree, change the document. Do not preserve the old text as an + alternative. +2. **Do not add structure beyond the recommendations.** No new dispatch tiers, + no new callbacks, no new abstractions, no new config fields. The last + iteration failed partly because a redundant fourth dispatch tier was + invented; do not repeat that pattern. Three tiers, sink as the default of + `on_key_pressed` — exactly as Item 3 states. +3. **Keep it pedagogically simple (NFR-4).** The project-facing API is a plain + table and a few callbacks. Don't introduce machinery a student couldn't + read. Honour `agents/rules.md` (store functions, not string tags; modest + line/function sizes when you show code). +4. **Code snippets in docs:** keep them illustrative and consistent with the + resolutions. You are editing *documents*, not `src/`. Do not modify files + under `src/` (the model fix in Item 1 is described in the spec/roadmap as + work to be done; do not perform it here). +5. **Propagate, then cross-check.** After applying all items, do a consistency + pass: every changed contract must read the same way in `design.md`, + `spec.md`, `roadmap.md`, and the corresponding `summaries/`. Combo examples, + callback signatures (`on_key_pressed(k, keys, isrepeat)`, + `on_text_entered(text, keys)`), the three-tier model, and the cursor + coordinate space must match across all of them. +6. **When unsure, flag — never invent.** If a resolution is ambiguous or you + find a new conflict the recommendations did not anticipate, leave the + simpler reading in place and record the question in the open-questions + appendix. Do not design your way out of it. + +--- + +## Output + +1. **The edited chain** — `design.md`, `spec.md`, `roadmap.md`, + `assessment.md`, `decisions.md`, and the mirrored `summaries/*.md`, all + brought into line with `recommendations_1.md` and internally consistent. +2. **A changelog** at `validation/changelog_1.md` with: + - one row per recommendation item: what changed, in which files; + - the list of new/annotated `decisions.md` entries and their provenance tag; + - an explicit "**Open questions for re-review**" section listing anything you + flagged rather than resolved (per guardrail 6), each with the document + location and why you left it open. +3. Do **not** edit `input.md` or `requirements.md`. Do **not** edit + `validation/recommendations_1.md` or `validation/validation_report_1.md` + (they are the inputs of record). + +Be specific and faithful. The success criterion is that an independent +reviewer, reading the rewritten chain against `recommendations_1.md`, finds +every item applied, no item over-applied (no invented structure), and no +remaining cross-document contradiction. diff --git a/doc/development/wip/77-new-input-api/prompts/prompt6.md b/doc/development/wip/77-new-input-api/prompts/prompt6.md new file mode 100644 index 00000000..2486b71b --- /dev/null +++ b/doc/development/wip/77-new-input-api/prompts/prompt6.md @@ -0,0 +1,288 @@ +# Prompt 6 — Independent re-review of the altered chain + +Read `CLAUDE.md` first (project overview, collaboration rules, and pointers to +reference material). Then read `agents/rules.md` (coding rules and tone — pay +particular attention to the tone section before writing anything analytical: +matter-of-fact and analytic, no blame). + +## Permissions + +Write all output to disk. You are expected to create the report file directly — +do not just print it in conversation. Any local file operation is permitted: +read, write, edit, search, grep, sed, and similar tools (including sane and justified cd-with-output-redirect). +The only prohibited +operations are those that modify git history: no commits, no rebases, no +amends, no force operations, no direct tampering with .git directory. + +Write the re-review report to: +`doc/development/wip/77-new-input-api/validation/validation_report_2.md` + +--- + +## Purpose + +This is the **second** independent review of the feature #77 document chain. +The first review (`validation/validation_report_1.md`) returned FAIL on +completeness and cross-document consistency grounds. Since then two things +happened: + +1. **A local design round** (architect + assistant, **no stakeholder + involvement**) resolved all ten actionable items. Its output is + `validation/recommendations_1.md` — the agreed resolutions, with grounding + and per-document targets. +2. **The chain was rewritten** to apply those resolutions (per `prompt5.md`), + producing edited `design.md`, `spec.md`, `roadmap.md`, `assessment.md`, + `decisions.md`, the `summaries/`, and a `validation/changelog_1.md`. + +Your job is to independently re-validate the rewritten chain. Unlike the first +review, you are now checking a chain that was deliberately altered against a +known set of intended changes — so the review has both the original +dimensions **and** a delta/faithfulness dimension. + +You are a reviewer, not a co-author. Do not rewrite the chain documents — +report findings. Be specific: cite document, section, and the exact claim or +gap. + +--- + +## Project context + +**Compy** is a console-based, Lua-programmable fantasy computer for children, +built on LÖVE2D v11.5. MVC, Lua 5.1/LuaJIT. Code is at `src/`. Architecture +docs under `doc/development/`. Feature #77 adds a callback-based event API to +the project input overlay. All working documents are under +`doc/development/wip/77-new-input-api/`. + +--- + +## Authority model (you must apply it when judging) + +Only `input.md` carries stakeholder authority. **No stakeholder sign-off has +ever happened on anything downstream of it** — `requirements.md`, all of +`decisions.md` (D-1…D-7 came from an earlier local round), and this round's +resolutions are all part of a single **local proposal** awaiting one eventual +stakeholder approve/veto review. When you judge whether something is "correct," +judge against the right tier. + +- **Stakeholder ground truth:** `input.md` — the only document the proposal + must serve and may not overrule. +- **High human input (local, traces `input.md`):** `requirements.md`. Treat its + FR/NFR as the working definition of what the feature must do, but remember it + is not itself stakeholder-signed — so also check it still faithfully traces + `input.md`. +- **Local proposal:** `decisions.md` end to end (both the earlier round's + D-1…D-7 and this round's additions), plus everything in + `validation/recommendations_1.md`. Provisional, pending the eventual review. + Do **not** treat D-1…D-7 as a higher tier than the round-1 additions — they + are the same tier, just earlier. +- **Derived:** `design.md`, `spec.md`, `roadmap.md`, `assessment.md`, + `summaries/`. Must be consistent with the tiers above and with each other. + +The authority model gates on *whether a human actually decided*, not on which +file a line sits in. (Example from round 1: D-6's "no double callback" lived in +`decisions.md` but was only a "Suggested decision" / auto-generated filler — so +it was treated as droppable, not as a considered decision.) + +--- + +## Inputs + +Read these before reviewing the chain: + +| File | Role | +|---|---| +| `validation/validation_report_1.md` | The first review. The ten actionable items and their original locations. | +| `validation/recommendations_1.md` | The agreed resolutions (the intended delta). **The spec the rewrite was executed against.** | +| `validation/changelog_1.md` | The rewrite's self-report: what changed where, new/annotated decisions, and the rewrite's own open-questions. | + +Then read the rewritten chain in order: `input.md` → `requirements.md` → +`assessment.md` → `decisions.md` → `design.md` → `spec.md` → `roadmap.md`, plus +`summaries/*.md`. + +## Supporting material (verify codebase claims; not design authority) + +- `notes/routing_unification.md`, `notes/love2d_handler_layers.md`, + `notes/event_delegation_chain.md`, `notes/enter_escape_routing.md`, + `notes/textinput_routing.md`, `notes/solution_sketch.md`. +- Source files for factual checks: + - `src/controller/controller.lua` — global handler, overlay gate, lifecycle, + `love.keypressed` slot / `hook_if_differs` / `save_user_handlers`. + - `src/controller/consoleController.lua` — REPL/editor routing, input entry + points, `stop_project_run`/`evacuate_required`, `write_to_input`. + - `src/controller/userInputController.lua` — widget keypressed/textinput, + submit/cancel, `oneshot` read. + - `src/controller/editorController.lua` — editor mode dispatch, Escape. + - `src/model/input/userInputModel.lua` — evaluate/handle/cancel, `oneshot` + field, cursor methods (`get_cursor_pos`, `move_cursor`, `set_cursor`, + `set_text`/`keep_cursor`), `is_at_limit`. + +--- + +## What to validate + +### Part A — Faithfulness of the rewrite (new this round) + +For each of the ten items in `recommendations_1.md`: +- **Applied?** Is the resolution actually present in every document the item + named (design/spec/roadmap/decisions/summaries as applicable)? +- **Applied correctly?** Does it match the resolution as written, including the + grounding constraints (e.g. 2D source-line cursor; sink as the *default* of + `on_key_pressed`; legacy heuristic gating the wrapper split; modifier-first + generic-folded combos; two independent channels with no exclusivity; + `on_key_pressed(k, keys, isrepeat)`; Escape opt-in dropped)? +- **Over-applied?** Did the rewrite add structure, tiers, callbacks, config + fields, or abstractions **beyond** the recommendation? (The round-1 failure + mode was an invented fourth dispatch tier — check specifically that nothing + analogous reappeared.) + +Produce a per-item verdict: APPLIED / PARTIAL / MISSING / OVER-APPLIED, with +the exact location. + +**Plus the namespace relocation (Stage 2).** `recommendations_1.md` has a +separate "Namespace isolation — relocate the new API under `compy.input.*`" +section. Check that it was applied as a clean pass: +- every *new* feature-#77 `compy.X` name (including the Item 1 cursor/text + functions) now reads `compy.input.X` consistently across `design.md`, + `spec.md`, `roadmap.md`, and the summaries — no stragglers left as flat + `compy.X`; +- the legacy globals (`input_text`, `input_code`, `validated_input`, + `user_input`, `write_to_input`) were **not** moved; +- `keys_pressed` was left global (`compy.keys_pressed`), not reparented under + `compy.input` (it is raw keyboard state, not the input-manipulation layer); +- nothing *outside* the recommendation's scope table was renamed. +Verdict: APPLIED / PARTIAL / MISSING / OVER-APPLIED, with locations. + +### Part B — Provenance and traceability (new this round) + +- Does `decisions.md` (and its summary) carry a top-of-file status note stating + the whole file is a local proposal derived from `input.md`, pending one + eventual stakeholder approve/veto — i.e. nothing here is stakeholder-signed? +- Are this round's new/changed decisions tagged with a consistent, + greppable round-1 provenance marker, so the delta since the prior round is + auditable? +- Are genuinely new architectural commitments (e.g. Item 4 native-handler + coexistence; Item 6 channel model; Item 1 restored FR-8/9/10; Item 7 combo + canonical form) surfaced as decisions, not buried only in derived docs — so + the eventual stakeholder review can focus on the real choices? +- Does `requirements.md` still faithfully trace `input.md` (it was not edited + this round; confirm no downstream change silently contradicts it)? + +### Part C — The original seven dimensions (re-run against the new chain) + +Re-check, because the documents changed: + +1. **Requirements coverage** — every FR/NFR traced + decisions→design→spec→roadmap. Confirm FR-8/9/10 are now carried through + (they were the headline gap). Flag any requirement that still drops out. +2. **Decision consistency** — each decision (now including any new D-8…D-N) + reflected consistently in design/spec; no later doc silently contradicts it. +3. **Design-to-spec completeness** — every design component has a precise spec + contract (signatures, return values, wrong-state/edge behaviour). +4. **Spec-to-roadmap coverage** — every spec item has a milestone home, plus + documentation and test coverage. Confirm the previously-missing homes now + exist (`compy.show`/`hide` exposure; cursor/live-write surface; + `write_to_input` facade). +5. **Roadmap ordering validity** — each milestone testable before the next. + Confirm the M2/M6 `oneshot`/submit ordering defect is resolved and M2's + "zero behaviour change" / M3's "all examples work" now hold. +6. **Factual accuracy against codebase** — re-verify the round-1 fixes landed + factually: `cancel()` does not push `'userinput'` today (Escape doesn't + dismiss); `oneshot` is a `UserInputModel` field; stop-time reset is + `stop_project_run`/`clear_user_handlers`, not `evacuate_required`; native + `love.keypressed` slot interception (`hook_if_differs`) is described + correctly. Flag any remaining mismatch. +7. **Summary fidelity** — each `summaries/*.md` faithfully represents its + (now edited) source; no claim present in a summary but absent/contradicted + in the full document; confirm the orphan Escape opt-in remark was removed + from `summaries/design.md`. + +### Part D — New conflicts and residue + +- Did applying the resolutions introduce any **new** cross-document + contradiction the recommendations didn't foresee? +- Adjudicate the rewrite's own open-questions list (in `changelog_1.md`): for + each, is leaving it open acceptable, or does it block implementation? +- Are there any items the recommendations marked "future seam / not built" + (e.g. overloadable matcher expansion, text-command-set prefix matching) that + the rewrite wrongly treated as in-scope, or that need a tracked note? + +--- + +## Already spotted inconsistencies + +These are manual notes from the architect after review of summaries +after validation/correction round 1 was finished. + +1. Spec and design used `compy.*` instead of `compy.input.*` in few places + (when speaking of handlers and new API); + it was fixed manually in summaries but not propagated back to full variants of docs +2. Roadmap was rewritten/adjusted but apparently not reestimated since take 1 -- + need reestimation +3. Across decisions/design its not emphasized sufficiently that processing of `on_text_input` + is supposed to follow exactly same principles as with `keypressed` +4. During design sessions there was an idea to augment both `keypressed` and `text_input` + with optional `mods` string that tells consumers which modifiers were pressed currently, + and is passed as a trailing optional argument to all downstream handlers, callbacks etc. + (rationale: even key combos handlers may need this information (e.g. if same handler + is attached to multiple combos it needs to understand the context); + let alone callback which fires after combo handlers are processed -- + it often may need to know the context; ) + But it seems that this idea was not reflected in decisions/design (or at least +5. Its suspicious that assessment and decisions still operate same seven 'blocking points' + which were recognized initially before validation session 1: + -- either initial analysis was good enough, + or controversies and resolutions discovered during validation round 1 are minor enough + to not be promoted to decision points for review/endorsement/veto by stakeholders + or such controvercies and resolutions were simply omitted from documentation update plan by mistake +6. In spec and roadmap its worth mentioning they are derivative documents pre-built in assumption + for the scenario when proposed design is endorced, not vetoed. + Also, spec may include explicit disclaimer that it's a part of proposal chain, + not every detail is set in stone, and it's part *could* be reviewed or changed by stakeholders + without blocking implementation (no requirement to fully freeze it before work starts, we're startup) + +--- + +## Output format + +Produce a structured report at the path given above. Use this structure: + +``` +# Validation Report 2 — Feature #77 Document Chain (post-rewrite) + +## Status: PASS / PASS WITH NOTES / FAIL + +## Part A — Rewrite faithfulness (per recommendation item) +[Item 1..10: APPLIED / PARTIAL / MISSING / OVER-APPLIED, location, note] + +## Part B — Provenance integrity +[findings] + +## Part C — Seven dimensions +### 1. Requirements coverage +### 2. Decision consistency +### 3. Design-to-spec completeness +### 4. Spec-to-roadmap coverage +### 5. Roadmap ordering validity +### 6. Factual accuracy +### 7. Summary fidelity + +## Part D — New conflicts and open questions +[findings + adjudication of the changelog's open questions] + +## Summary of actionable items +[numbered; if any remain, they block implementation] + +## Recommendation +[Ready for stakeholder review / Needs another local pass / Needs codebase check] +``` + +Distinguish severity clearly: a missing FR is blocking; a stale combo example +in one summary is a note. If the chain is now coherent and faithful, say so +plainly — "PASS WITH NOTES" is a valid and likely outcome if only minor +residue remains. If new blocking issues exist, a `validation/recommendations_2.md` +may be warranted; recommend it but do not write it unless asked. + +Be specific. "Item 4 applied" is not useful. "Item 4 applied in `design.md §2` +and `spec.md §6`; the legacy heuristic gate is described, but +`summaries/design.md` still shows the old unconditional `ProjectController` +slot ownership without the auto-provisioning note" is useful. diff --git a/doc/development/wip/77-new-input-api/prompts/prompt7.md b/doc/development/wip/77-new-input-api/prompts/prompt7.md new file mode 100644 index 00000000..1ccc677d --- /dev/null +++ b/doc/development/wip/77-new-input-api/prompts/prompt7.md @@ -0,0 +1,263 @@ +# Prompt 7 — Third independent re-review of the chain (round 3) + +Read `CLAUDE.md` first (project overview, collaboration rules, and pointers to +reference material). Then read `agents/rules.md` (coding rules and tone — pay +particular attention to the tone section before writing anything analytical: +matter-of-fact and analytic, no blame). + +## Permissions + +Write all output to disk. You are expected to create the report file directly — +do not just print it in conversation. Any local file operation is permitted: +read, write, edit, search, grep, sed, and similar tools (including sane and +justified cd-with-output-redirect). The only prohibited operations are those +that modify git history: no commits, no rebases, no amends, no force operations, +no direct tampering with the .git directory. + +Write the re-review report to: +`doc/development/wip/77-new-input-api/validation/validation_report_3.md` + +--- + +## Purpose + +This is the **third** independent review of the feature #77 document chain. The +history so far: + +1. **Review round 1** (`validation/validation_report_1.md`) returned FAIL on + completeness and cross-document consistency grounds (FR-8/9/10 dropped, a + submit-path ordering defect, a dispatch-contract contradiction, and more). +2. **Local design round 1** resolved those into + `validation/recommendations_1.md`; the chain was rewritten to apply them + (`validation/changelog_1.md`). +3. **Review round 2** (`validation/validation_report_2.md`) returned **PASS WITH + NOTES**: the round-1 blockers were closed and faithfully applied; what + remained was consistency residue and provenance/packaging gaps. +4. **Local design round 2** distilled those into + `validation/recommendations_2.md` (six items) and applied them + (`validation/changelog_2.md`). + +Your job is to independently re-validate the chain **as it now stands after +round 2**. This is expected to be a **lighter** review than round 2 — the chain +already passed with notes, and round 2 was a targeted cleanup, not a rewrite. +The bar is: did the round-2 cleanup land faithfully, without regressions and +without introducing new contradictions, and is the chain now ready for +stakeholder review? + +You are a reviewer, not a co-author. Do not rewrite the chain documents — report +findings. Be specific: cite document, section, and the exact claim or gap. + +--- + +## Project context + +**Compy** is a console-based, Lua-programmable fantasy computer for children, +built on LÖVE2D v11.5. MVC, Lua 5.1/LuaJIT. Code is at `src/`. Architecture docs +under `doc/development/`. Feature #77 adds a callback-based event API to the +project input overlay. All working documents are under +`doc/development/wip/77-new-input-api/`. + +--- + +## Authority model (you must apply it when judging) + +Unchanged across rounds. Only `input.md` carries stakeholder authority. **No +stakeholder sign-off has ever happened on anything downstream of it** — +`requirements.md`, all of `decisions.md` (now D-1…D-10), and both rounds of +resolutions are a single **local proposal** awaiting one eventual stakeholder +approve/veto review. Judge "correctness" against the right tier. + +- **Stakeholder ground truth:** `input.md` — may not be overruled. +- **High human input (local, traces `input.md`):** `requirements.md`. Treat its + FR/NFR as the working definition of what the feature must do, but also check it + still faithfully traces `input.md`. (It was not edited in rounds 1 or 2.) +- **Local proposal:** `decisions.md` end to end (D-1…D-10, both rounds) plus + everything in `recommendations_1.md` and `recommendations_2.md`. Provisional. + Do not treat earlier decisions as a higher tier than later ones — same tier, + different dates. +- **Derived:** `design.md`, `spec.md`, `roadmap.md`, `assessment.md`, + `summaries/`. Must be consistent with the tiers above and with each other. + +The authority model gates on *whether a human actually decided*, not on which +file a line sits in. + +--- + +## Inputs + +Read these before reviewing the chain: + +| File | Role | +|---|---| +| `validation/validation_report_2.md` | The second review. Its "Summary of actionable items" is what round 2 set out to fix. The chain it reviewed was already PASS WITH NOTES. | +| `validation/recommendations_2.md` | The agreed round-2 resolutions (the intended delta). **The spec the round-2 edits were executed against.** Six items. | +| `validation/changelog_2.md` | The round-2 rewrite's self-report: what changed where, the new/annotated decisions, and its own open questions. | + +For continuity (reference, not the primary delta this round): +`validation/recommendations_1.md`, `validation/changelog_1.md`, +`validation/validation_report_1.md`. + +Then read the chain in order: `input.md` → `requirements.md` → `assessment.md` +→ `decisions.md` → `design.md` → `spec.md` → `roadmap.md`, plus `summaries/*.md`. + +## Supporting material (verify codebase claims; not design authority) + +- `notes/routing_unification.md`, `notes/love2d_handler_layers.md`, + `notes/event_delegation_chain.md`, `notes/enter_escape_routing.md`, + `notes/textinput_routing.md`, `notes/solution_sketch.md`. +- Source files for factual checks (the round-1/2 fixes depend on these): + - `src/controller/controller.lua` — global handler, overlay gate, lifecycle, + `love.keypressed` slot / `hook_if_differs` / `save_user_handlers`. + - `src/controller/consoleController.lua` — REPL/editor routing, input entry + points, `stop_project_run`/`clear_user_handlers`, `write_to_input`. + - `src/controller/userInputController.lua` — widget keypressed/textinput, + submit/cancel, `oneshot` read. + - `src/model/input/userInputModel.lua` — evaluate/handle/cancel, `oneshot` + field, cursor methods, `set_text`/`keep_cursor`/`jump_end`, `is_at_limit`. + +--- + +## What to validate + +### Part A — Faithfulness of the round-2 edits (primary this round) + +For each of the **six** items in `recommendations_2.md`: + +1. **Default contradiction (N1).** Is the `noop+log`-vs-`sink` default fixed + everywhere it appeared — `design.md §2` diagram, `spec.md §3` intro, + `summaries/spec.md`? Do the two channel callbacks (`on_key_pressed`, + `on_text_entered`) now consistently read "default = sink", and do the + submit/cancel/limit callbacks still read "default = noop+log"? Any place + still conflating the two? +2. **Namespace surfaced as a decision + `decisions.md` naming.** Is there a new + **D-10** (namespace isolation under `compy.input.*`) in `decisions.md` and its + summary, with provenance? Were the new-API names in `decisions.md` / + `summaries/decisions.md` relocated to `compy.input.*` consistently (no flat + straggler, no double `compy.input.input`), with `compy.keys_pressed` and the + legacy globals left alone? +3. **Roadmap re-estimate.** Is the estimate now a genuine three-point PERT + (Optimistic, Most-likely, Pessimistic; PERT = (O+4M+P)/6), not a two-point + table whose "PERT" collapsed to the most-likely? Recompute a couple of rows + and the totals — do they check out? Is the internal estimate-revision history + gone (validation rounds are not stakeholder-facing)? Does `summaries/roadmap.md` + match the full roadmap's figures? +4. **`mods` modifier-string.** Is it recorded as a **future seam, not built in + v1**, for both channels (under `decisions.md` D-6 and `spec.md §3`), and + flagged as an open question rather than silently built or silently dropped? + Confirm no `mods` argument was actually added to any signature. +5. **Disclaimers.** Do `spec.md` and `roadmap.md` now carry a + derived/proposal-chain status note (pre-built assuming endorsement; not + frozen; reviewable without blocking implementation)? +6. **Minor cleanups.** decisions intro reconciled to the D-1…D-10 set; D-3 (and + D-6) body marked as superseded-in-part where the body predates the round-1 + annotation; `assessment.md §8` co-occurrence re-pointed at D-6; + `design.md §3` table wording for `on_key_pressed`; textinput-mirrors-keypressed + symmetry made explicit in design/spec/decisions. + +Per-item verdict: APPLIED / PARTIAL / MISSING / OVER-APPLIED, with the exact +location. Watch specifically for **over-application** — round 2 was a cleanup; +no new dispatch tier, callback, config field, or abstraction should have +appeared. (The `mods` string in particular must be a *seam note*, not new API.) + +### Part B — No regression of the round-1 resolutions + +Round 2 edited several documents that round 1 had already fixed. Confirm nothing +regressed: + +- FR-8/9/10 still carried end to end (D-8, `design §3`, `spec §2`, `roadmap M7`). +- The three-tier model intact (sink = default of `on_key_pressed`; no fourth + tier reappeared via the §3 default-wording edits). +- M2/M6 `oneshot` ordering intact; `oneshot` still a `UserInputModel` field. +- Native coexistence (D-9), two-channel model (D-6), combo canonical form (D-3), + `write_to_input` facade, the FR-11/12 walkthrough — all still present and + consistent after the round-2 edits. +- The `compy.input.*` relocation is still complete in `design/spec/roadmap` and + their summaries (the round-2 `decisions.md` relocation did not desync them). + +### Part C — Provenance and traceability + +- Does `decisions.md` (and its summary) still carry the top-of-file local-proposal + status note, and is the D-1…D-10 set internally consistent (quick-reference + table, per-decision detail, and the intro count all agreeing)? +- Are round-2 changes tagged with a consistent, greppable round-2 provenance + marker (distinct from the round-1 marker), so the round-2 delta is auditable? +- Is D-10's provenance honest (the namespace choice originated in round 1's + recommendations and is *recorded* as a decision in round 2)? +- Does `requirements.md` still faithfully trace `input.md` (unedited; confirm no + downstream change silently contradicts it)? + +### Part D — Seven dimensions (spot re-run, not full) + +The chain passed these in round 2; re-run only where round 2 touched them, and +flag anything that drifted: + +1. **Requirements coverage** — still all FR/NFR traced; FR-8/9/10 intact. +2. **Decision consistency** — D-10 reflected consistently; no decision (old or + new) silently contradicted by a derived doc; the default-wording fix did not + create a new mismatch. +3. **Design-to-spec completeness** — channel-callback defaults and the + textinput/keypressed symmetry now read the same in design and spec. +4. **Spec-to-roadmap coverage** — every surface still has a milestone home; + estimates cover the actual scope. +5. **Roadmap ordering validity** — unchanged by round 2; confirm the re-estimate + did not perturb milestone dependencies or the M2/M6 ordering. +6. **Factual accuracy against codebase** — re-spot-check the load-bearing facts: + `cancel()` does not push `'userinput'`; `oneshot` is a `UserInputModel` field; + stop-time reset is `stop_project_run`/`clear_user_handlers`; native slot + interception via `hook_if_differs`; `set_text`'s unconditional `jump_end()` + (the M7 model fix). Flag any mismatch. +7. **Summary fidelity** — each `summaries/*.md` still faithfully represents its + (now round-2-edited) source; the default-wording, D-10 row, and estimate + figures match between summary and source. + +### Part E — Adjudicate the carried-forward open questions + +`changelog_2.md` carries two open questions. For each, state whether leaving it +open is acceptable for stakeholder review, or whether it blocks: + +1. **The `mods` trailing modifier-string** — recorded as a seam, not built. + Acceptable to defer, or should it be in-scope? (Note the stakeholder input + in `input.md` and FR-5/FR-6 — does anything there require it?) +2. **The `decisions.md` relocation reversing round 1's narrow exclusion** — + acceptable as the resolution to the round-2 split finding, or does it + reintroduce a problem? + +Also check: are there any *new* cross-document contradictions introduced by the +round-2 edits that `recommendations_2.md` did not anticipate? + +--- + +## Output format + +Produce a structured report at the path above. Suggested structure: + +``` +# Validation Report 3 — Feature #77 Document Chain (post round-2 cleanup) + +## Status: PASS / PASS WITH NOTES / FAIL + +## Part A — Round-2 faithfulness (per item 1..6: APPLIED/PARTIAL/MISSING/OVER-APPLIED, location, note) +## Part B — No regression of round-1 resolutions +## Part C — Provenance and traceability +## Part D — Seven dimensions (spot re-run) +## Part E — Carried-forward open questions (adjudication) + any new conflicts +## Summary of actionable items +## Recommendation +``` + +Distinguish severity clearly: a regressed FR or a new cross-document +contradiction is blocking; a stale figure in one summary is a note. If the chain +is now coherent, faithful, and free of regressions, say so plainly — **PASS** (or +PASS WITH NOTES if trivial residue remains) is the expected outcome for a +successful cleanup round. Reserve FAIL for a genuine regression or a new blocking +contradiction. + +If new blocking issues exist, a `validation/recommendations_3.md` may be +warranted; recommend it but do not write it unless asked. The recommendation +line should be one of: **Ready for stakeholder review** / **Needs another local +pass** / **Needs codebase check**. + +Be specific. "Item 2 applied" is not useful. "Item 2 applied: D-10 present in +`decisions.md` (per-decision detail + quick-reference row) and +`summaries/decisions.md`; `decisions.md` relocated to `compy.input.*` with no +flat straggler (grep-clean) and `compy.keys_pressed` preserved" is useful. diff --git a/doc/development/wip/77-new-input-api/prompts/prompt8.md b/doc/development/wip/77-new-input-api/prompts/prompt8.md new file mode 100644 index 00000000..6c59a73e --- /dev/null +++ b/doc/development/wip/77-new-input-api/prompts/prompt8.md @@ -0,0 +1,16 @@ +Rule to memoize: in signature `$dirname/$filename.md`, N is calculated once per round as +``` bash +#!/bin/bash +echo $(( $(ls "${dirname}/${filename}*.md" | wc -l) + 1 )) +``` + + +Read `./prompts/prompt.md` (the last updated file under ./prompts/) to understand the current context and rules, but do not follow it literally. + +Now you're at next iteration. +Specifications were presented to stakeholders and we got a feedback (check git diff or last git commit to see). + +Can you update the whole chain unambiguously without my direction/resolution ? +Write your conclusions into `./reevaluations/round.md` and if you update anything, write the changelog as `./reevaluation/changes.md ` + +And tell me where we are diff --git a/doc/development/wip/77-new-input-api/prompts/prompt9.md b/doc/development/wip/77-new-input-api/prompts/prompt9.md new file mode 100644 index 00000000..55a79b33 --- /dev/null +++ b/doc/development/wip/77-new-input-api/prompts/prompt9.md @@ -0,0 +1,204 @@ +# Prompt 9 — Validate the round-1 re-evaluation (D-1 discarded) + +Read `CLAUDE.md` first (project overview, collaboration rules, pointers to +reference material), then `agents/rules.md` — pay particular attention to the +**tone** section before writing anything analytical: matter-of-fact and +analytic, no blame, the audience are the senior people who built the system. + +## Naming rule (carried from prompt 8) + +In a signature `$dirname/$filename.md`, N for a round is, in principle, +`echo $(( $(ls "${dirname}/${filename}*.md" | wc -l) + 1 ))`. **The file count +under-counts when there is a numbering gap** (e.g. `prompts/` has no +`prompt2.md`, so the count gives 8 while the highest file is `prompt8.md`). When +that happens, infer the logic and use the next sequential number, not the raw +count. This prompt is `prompt9.md` for that reason. Your output file is fixed +below regardless: `reevaluations/check1.md`. + +## Permissions + +Write all output to disk — create the report file directly, do not just print it +in conversation. Any local file operation is permitted: read, write, edit, +search, grep, sed, and similar (including a justified `cd`-with-output-redirect). +The only prohibited operations are those that modify git history: no commits, no +rebases, no amends, no force operations, no `.git` tampering. Read-only git +(`git show`, `git diff`, `git log`) is expected and encouraged. + +--- + +## Where we are + +This is feature #77 (a callback-based input API for the project overlay). The +document chain lives under `doc/development/wip/77-new-input-api/` and had +already passed three internal validation rounds (`validation/`) plus two local +design rounds before being shown to stakeholders. + +Stakeholders then gave **feedback round 1** (verbatim in `input.md`, section +"FEEDBACK AFTER FIRST ITERATION"; the originating commit was +"stakeholders feedback round 1"). The single ruling: **D-1 (backward +compatibility) is DISCARDED** — no backward compatibility, get rid of the legacy +text-input API, migrate the examples (pre-1.0, clean break acceptable). + +A predecessor agent then re-evaluated and **edited the whole chain** to apply +that ruling. Its work is recorded under `reevaluations/`: + +| File | Role | +|---|---| +| `reevaluations/outcome1.md` | Short "where we are" summary — **read this first.** | +| `reevaluations/round1.md` | The predecessor's reasoning: why the cascade was judged unambiguous, the codebase grounding, the one judgment call. | +| `reevaluations/changes1.md` | File-by-file changelog of every edit made. **This is the claimed delta — your job is to verify it landed faithfully.** | + +By the time you run, **those edits are committed.** Identify the commit (most +recent on the branch; `git log --oneline -5`, look for the round-1 re-evaluation +commit) and use `git show ` / `git diff` to see exactly what changed. +Treat `changes1.md` as the *claim* and the commit as the *evidence* — confirm +they match. + +## The decision you are validating against (the bar) + +- **D-1 = DISCARDED** is now **stakeholder ground truth** (it lives in + `input.md`). It outranks everything downstream. The five legacy globals + (`input_text`, `input_code`, `validated_input`, `user_input`, + `write_to_input`) must be **removed**, not wrapped — no facades, no + `strict_input`, no deprecation shim, no reftable/polling idiom surviving in + the project-facing API. +- **Scope is text input only.** The native `love.keypressed`/`textinput` + coexistence path (**D-9**) is a *separate* surface and must be **retained** — + it is what keeps the stakeholders' own "only text fields break" guarantee + true. If you find D-9 weakened or removed, that is a regression. +- **D-2…D-10 remain a local proposal** awaiting one approve/veto review; D-1 is + the only stakeholder-settled entry. Judge "correctness" against the right + tier (the authority model is spelled out in `prompt7.md` if you want the + longer form). + +You are a **reviewer, not a co-author.** Do not rewrite the chain. Report +findings — cite document, section, and the exact claim or gap. If you find +blocking issues, you may *recommend* a `reevaluations/recommendations1.md` but +do not write it unless asked. + +--- + +## What to validate + +### Part A — Faithfulness of the round-1 re-evaluation edits + +Take each entry in `changes1.md` and confirm it is actually present in the +committed docs, correct, and complete. Per item: APPLIED / PARTIAL / MISSING / +OVER-APPLIED, with the exact location. Watch specifically for: + +- **Stragglers** — any surviving mention of a facade, `strict_input`, + deprecation warning, or a project-facing reftable/`is_empty()` polling idiom + in the *live* chain (`decisions.md`, `requirements.md`, `assessment.md`, + `design.md`, `spec.md`, `roadmap.md`, `summaries/*`, `README.md`). Grep is + your friend. Note: `assessment.md` legitimately describes *today's* code and + may mention the reftable factually — that is correct, not a straggler. Tell + the two cases apart. +- **Over-application** — the edit should remove a backward-compat layer and add + one migration milestone. It should **not** have invented new API surface, new + dispatch tiers, or new callbacks. D-9 must still be intact. +- **Authority-tier honesty** — does `decisions.md` (and its summary) now mark + D-1 as stakeholder-settled while keeping D-2…D-10 as proposal? Is + `requirements.md §5` marked resolved (clean break)? + +### Part B — Cross-document consistency + +Read the chain in order — `input.md` → `requirements.md` → `assessment.md` → +`decisions.md` → `design.md` → `spec.md` → `roadmap.md`, plus `summaries/*` — +and confirm the D-1 ruling reads the same everywhere, with no contradiction +introduced. In particular: + +- Each `summaries/*.md` still faithfully represents its (now-edited) source — + the D-1 row, the removed-functions table, the milestone table, and the + estimate figures must match between summary and full doc. +- No dangling cross-reference. The predecessor kept milestone numbering M4–M7 + stable and **voided M3** (left it as an empty, explained slot) so the many + "M6 deletes `oneshot`", "M7 cursor surface", etc. references stay valid, and + added **M8** for the removal + migration. Confirm there is no live reference + to "M3 facades" as if it still exists, and that every `Mx` reference resolves. +- D-8 (`write_to_input` was a facade) and D-10 ("legacy globals unchanged") + both depended on D-1 — confirm they were reconciled, not left contradicting + the discard. + +### Part C — Roadmap: consistency and ordering (look here hardest) + +This is the milestone that changed the most; scrutinise it. + +- **Dependency order.** Walk the `Input:`/dependency line of every milestone + (M1, M2, M4, M5, M6, M7, M8 — M3 is intentionally void). Confirm each + milestone's stated inputs actually exist by then, and that **M8 (legacy + removal + example migration) genuinely depends on the full `compy.input.*` + surface** — i.e. it must come after M7, because you cannot migrate the + examples to `show()`-with-validator/highlighter, the submit/cancel callbacks, + and `set_text`/cursor until those exist. If M8 could be ordered earlier + without breaking, say so; if it is mis-ordered, flag it. +- **Migration scope is grounded.** The predecessor checked which in-repo + examples use the legacy functions. Re-verify against the actual tree + (`grep -rln -E 'input_text|input_code|validated_input|user_input|write_to_input' src/examples/`). + Confirm: priority = `tixy`, `balloons` (and `maze`, which is **not** in the + repo); convert-or-exclude = `repl`, `guess`, `valid`, `turtle`; + native-only/unaffected = `pong`, `life`, `paint`, `sapper`, `sine`, `clock`, + `drawdebug`. Flag any example that is mis-classified (e.g. one that actually + has a text field but was listed as unaffected, or vice-versa). `turtle` is the + interesting one — it mixes a text input with native `love.keypressed`. +- **No regression of earlier roadmap invariants.** M2 must still be genuine + "zero behaviour change"; the `oneshot` deletion must still be M6 (not earlier); + the M7 cursor/`set_text` surface and the `UserInputModel:set_text` + `keep_cursor` model fix must still be present. + +### Part D — Re-check the estimates FROM SCRATCH + +Do **not** validate the estimate *diff*, and do **not** anchor on any historical +figure (the old ≈ 59/≈ 35, or the predecessor's stated ≈ 63/≈ 37). Ignore those. +Independently form a view of the **current** plan and check the numbers as they +now stand: + +1. **PERT arithmetic.** For every row in both tables (Without LLM, With LLM), + recompute `PERT = (O + 4M + P) / 6` and confirm the per-row value. Then sum + O, M, P independently and confirm the totals row and the project-level + `(O + 4×M + P)/6`. Report any cell that does not check out. +2. **Coverage.** Does every milestone (M1, M2, M4, M5, M6, M7, M8) plus the + Documentation and Test-coverage lines have an estimate row in both tables? Is + anything double-counted or missing? (M3 is void — it should have **no** row.) +3. **Plausibility, fresh.** Independently judge whether the O/M/P spreads are + sane for the scope each milestone now describes — especially **M8**, whose + scope (remove five globals + migrate the example corpus) is new this round. + You may agree, or argue a row is optimistic/pessimistic; either way, justify + it from the milestone's described work, not from the prior estimate. +4. State the totals you arrive at and whether they match what the roadmap and + `summaries/roadmap.md` currently print. A mismatch between the full roadmap + and its summary is a finding. + +--- + +## Output + +Write your report to `reevaluations/check1.md`. Suggested structure: + +``` +# Check 1 — Validation of the round-1 re-evaluation + +## Status: PASS / PASS WITH NOTES / FAIL + +## Part A — Edit faithfulness (per changes1.md item: APPLIED/PARTIAL/MISSING/OVER-APPLIED + location) +## Part B — Cross-document consistency +## Part C — Roadmap consistency and ordering +## Part D — Estimates, recomputed from scratch (per-row PERT, totals, coverage, plausibility) +## Summary of actionable items (severity-ranked) +## Recommendation +``` + +Distinguish severity: a surviving facade/str__input straggler in a stakeholder- +facing doc, a weakened D-9, a broken milestone dependency, or a wrong total is a +**blocker**; a stale figure in one summary or a wording nit is a **note**. If the +re-evaluation landed cleanly, say so plainly — **PASS** (or PASS WITH NOTES) is +the expected and fine outcome for a faithful, well-scoped cleanup. Reserve FAIL +for a real regression, a broken cross-reference that misleads, or an estimate +that does not add up. + +The recommendation line should be one of: **Ready for stakeholder review** / +**Needs another local pass** / **Needs codebase check**. + +Be specific. "Roadmap is consistent" is not useful. "M8 input correctly lists M7 +as a dependency; verified the example-migration scope against `src/examples/` +(6 text-input examples, `turtle` mixed); Without-LLM total recomputes to 63.2 h, +matching the printed ≈ 63 h and `summaries/roadmap.md`" is useful. diff --git a/doc/development/wip/77-new-input-api/reevaluations/changes1.md b/doc/development/wip/77-new-input-api/reevaluations/changes1.md new file mode 100644 index 00000000..e9f4a3a3 --- /dev/null +++ b/doc/development/wip/77-new-input-api/reevaluations/changes1.md @@ -0,0 +1,139 @@ +# Changelog — Re-evaluation Round 1 (D-1 discarded) + +*Applies the `input.md` round-1 stakeholder feedback across the chain. +Provenance marker used in-place: "stakeholder feedback, round 1, 2026-06-06". +Conclusions: `round1.md`.* + +> **Note on file location.** Prompt 8 specified `./reevaluation/changes.md` +> (singular). That directory does not exist; `./reevaluations/` (plural) does +> and holds `round1.md`. Both files are kept together here. N = 1 (the +> `reevaluations/` formula count was 0 → 1; also the "feedback round 1" commit). + +--- + +## decisions.md + +- **Top status note** — carved out D-1 as stakeholder-decided (DISCARDED) + ground truth; D-2…D-10 remain local proposal. +- **"The approach in brief" point 3** — retitled from "Existing API continues + to work" to "The legacy text-input API is removed"; rewrote to describe + removal + example migration + the text-input-only scope (D-9 unaffected). +- **Quick-reference table** — D-1 row rewritten to "Discarded … legacy globals + removed; examples migrated/excluded; D-9 unaffected"; D-8 row's + "`write_to_input` facade" → "`set_text` supersedes the removed + `write_to_input`"; D-10 row "legacy globals unchanged" → "removed". +- **D-1 per-decision detail** — replaced the "Suggested decision" (keep + facades) with the stakeholder ruling: removal of the five globals and the + reftable/polling idiom; example migration (priority `tixy`/`balloons`/`maze`; + rest convert-or-exclude at owner's call); text-input-only scope; pointer to + roadmap M8 and to D-9. +- **D-8 detail** — `write_to_input` is removed, not a facade; `tixy` migrates to + `compy.input.set_text`; dropped the "facade wiring in M3" line; the + cursor/`set_text` surface stays M7, removal+migration is M8. +- **D-9 detail** — clarified that D-9 is a surface *separate* from the discarded + D-1, and is retained; it is what keeps the "only text fields break" guarantee + true. Updated "Affects" accordingly. +- **D-10 detail** — "Unchanged — legacy globals" bullet → "Removed — legacy + text-input globals"; "Affects" line note that there is no legacy call-target + left. + +## requirements.md + +- **§5 Open Questions** — "Backward compatibility" bullet marked + **RESOLVED (clean break)**, citing the `input.md` round-1 ruling; bounded to + text input; pointer to `decisions.md` D-1. + +## design.md + +- **§3 component table** — `after_submit` description "after evaluation and + reftable fill" → "after evaluation (receives the result)". +- **§5 Enter** — submit step "evaluate → fill reftable → push 'userinput'" → + "evaluate → push 'userinput'". +- **§6** — retitled "Legacy API Compatibility" → "Legacy API Removal"; replaced + the facade table / deprecation / `strict_input` text with the removal + + migration description; `love.state.user_input` now plainly set/cleared by + show/hide (no "transition" framing). Kept the "Native handler coexistence" + subsection (D-9). +- **§7** — implementation-order table: removed the "Legacy facades" step; + added "Legacy removal + example migration" as the last step; rewrote the + dependency paragraph and noted the M1–M8 milestone numbering. +- **§7 FR-11 walkthrough** — Enter row "evaluate + fill reftable + push" → + "evaluate + push + `after_submit`". + +## spec.md + +- **§1** — "legacy wrappers" dropped from the list of `keys_pressed` downstream + consumers. +- **§3 after_submit** — removed "filled the reftable" wording. +- **§5** — retitled "Legacy API Compatibility" → "Legacy API Removal"; replaced + the rewired-functions table with a removed-function → replacement table; + removed the deprecation-warning and `strict_input` subsections; kept the + `love.state.user_input` note (now without "transition" framing). +- **§7 edge cases** — removed "The reftable (if a legacy wrapper was used) + stays empty." + +## roadmap.md + +- **M2** — removed the `result`-repointing setter (facade-only) from the file + list and the "before M3 facades" phrasing; clarified examples still work at + M2 because legacy removal is deferred to M8. +- **M3** — **voided**: replaced the facade-wrapper milestone with a note that it + is superseded by the feedback (no facades); work moved to M8; numbering kept + to preserve cross-references. +- **M4** — input dependency corrected from "M3 complete" to "M2 complete". +- **M6** — removed the "reftable fill moves onto `after_submit`" line and the + "Legacy `after_submit` callback fills the reftable" output/risk lines. +- **M7** — removed the "`write_to_input` re-pointed to `set_text`" clause; noted + `set_text` supersedes the removed `write_to_input` (see M8). +- **M8 (new)** — "Legacy text-input removal and example migration": removes the + five globals; migrates priority examples (`tixy`, `balloons`; `maze` on + arrival); convert-or-exclude for `repl`/`guess`/`valid`/`turtle`; + native-handler examples unaffected (D-9). Depends on M7 (needs the full + surface). +- **Additional scope / Test coverage** — "Legacy API compatibility" test bullet + → "Example migration (M8)" bullet. +- **Estimates** — both tables: dropped the M3 row, added the M8 row, recomputed: + - Without LLM: O/M/P 34/62/97, **PERT ≈ 63 h** (was 59). + - With LLM: O/M/P 20/36/59, **PERT ≈ 37 h** (was 35). + - Added a note that discarding D-1 *raised* the estimate (M8 > old M3). + +## summaries/decisions.md + +- Status note carved out D-1 (DISCARDED) as ground truth. +- "Existing API continues to work (D-1)" paragraph → "Legacy text-input API + removed (D-1 discarded)". +- Glance table: D-1 and D-10 rows updated (mirroring `decisions.md`). + +## summaries/design.md + +- "What this feature adds" — facade-wrapper sentence → legacy removal + + migration sentence. +- Components table — "Legacy wrappers … rewired as facades" row → "Legacy + text-input globals … removed; examples migrate; D-9 retained". + +## summaries/spec.md + +- "Legacy API" section → "Legacy API — removed": removed-function → replacement + table; no deprecation/`strict_input`. +- Callbacks table — `after_submit` "reftable is filled" → "receives the result". + +## summaries/roadmap.md + +- Milestone table: M3 row marked *(removed)*; added M8 row. +- Test-coverage row updated (M8 example migration instead of legacy compat). +- "Estimates at a glance" totals: **≈ 63 h / ≈ 37 h**; added the + "discarding D-1 raised the total" note. + +## README.md + +- TL;DR — "Old functions keep working; nothing existing breaks" replaced with + the removal + migration + text-input-only-scope summary. +- Summary table — effort "(~35–59 h)" → "(~37–63 h)". + +## Not changed (intentional) + +- `assessment.md` — current-state/gap analysis; its reftable references + describe today's code and stay accurate. +- `notes/*` — supporting analysis, now stale on D-1; flagged in `round1.md`, + not rewritten (historical record). +- `validation/*` — audit trail of prior review rounds; left intact. diff --git a/doc/development/wip/77-new-input-api/reevaluations/check1.md b/doc/development/wip/77-new-input-api/reevaluations/check1.md new file mode 100644 index 00000000..dbb43cff --- /dev/null +++ b/doc/development/wip/77-new-input-api/reevaluations/check1.md @@ -0,0 +1,320 @@ +# Check 1 — Validation of the round-1 re-evaluation + +## Change in brief — stakeholder Q&A + +*Plain-language read on the round-1 change and whether it is safe to proceed. +The two cosmetic doc fixes this review raised were applied at the owner's +explicit request (2026-06-06) — see "Summary of actionable items", now resolved.* + +**1. Any architectural regression from applying the decision?** +Confirmed — none. The feature itself is unchanged (same callback API), the +native keyboard path still stands, so only text fields are affected, and nothing +new was invented to work around the removal. The architecture holds; only the +plan moved. + +**2. Does the price, risk, or order change?** +As expected, only the plan shifts, not the design: +- **Price:** up a little — ≈ 59 → ≈ 63 h (≈ 35 → ≈ 37 h with LLM). Rewriting the + examples costs a bit more than the throwaway wrappers it replaces. Figures + checked against `changes1.md`. +- **Order:** as presumed — the wrapper step is gone; the new API is built first + and the examples are ported at the very end, once it exists. +- **Risk:** the example rewrite lands late, so an API gap would show up late — + but it's bounded. The pessimistic estimate already allows for it, a stubborn + example can be left out of the release rather than block it, and the API was + already shown able to rebuild the console and editor, so the rewrites should + be mechanical. Testing isn't deferred: every step is checked as it lands, and + a budgeted test workstream (≈ 11 h, ≈ 6 h with LLM) now covers the rewrite too. + +**3. What's the new plan?** +Build the new API first, then a final step that removes the old calls and ports +the examples — `tixy` and `balloons` for the release, the rest convert or leave +out if there's no time. Nothing breaks in the meantime: the old examples keep +running until that last step. The updated build plan and the ≈ 63 h / ≈ 37 h +estimates are in `summaries/roadmap.md`. + +--- + +*Validates the round-1 re-evaluation (D-1 discarded) recorded in +`reevaluations/{outcome1,round1,changes1}.md` against the committed chain. +Re-evaluation commit: `c94fb57` ("reevaluation round1 …"). Evidence taken from +`git show c94fb57`, the live docs, and the `src/examples/` tree. Tone per +`agents/rules.md`: matter-of-fact, no blame — this is a faithful, well-scoped +cleanup; the two minor doc nits found were fixed this pass.* + +## Status: PASS (the two notes below were fixed this pass) + +The ruling (D-1 DISCARDED) is applied end to end. The five legacy globals are +removed (not wrapped) everywhere it matters; no `strict_input`, deprecation, or +project-facing reftable/polling idiom survives in the live chain; D-9 is intact +and reinforced; milestone numbering and cross-references resolve; both estimate +tables recompute exactly to the printed `≈ 63 h` / `≈ 37 h`. The two NOTE-level +items found (a stale D-8 glance-table cell in `summaries/decisions.md` and a +loose M8 dependency header in `roadmap.md`) were corrected this pass at the +owner's request; details below. + +--- + +## Part A — Edit faithfulness (per `changes1.md` item) + +**`decisions.md`** — all six claimed edits APPLIED: +- Top status note carves out D-1 as stakeholder-decided ground truth, D-2…D-10 + proposal — APPLIED (lines 3–11). +- "Approach in brief" point 3 retitled to "The legacy text-input API is + removed", removal + migration + text-input-only scope — APPLIED (lines 134–149). +- Quick-ref table: D-1 → "Discarded …", D-8 → "`set_text` supersedes the removed + `write_to_input`", D-10 → "removed" — APPLIED (lines 153, 160, 163). +- D-1 per-decision detail replaced "Suggested decision (keep facades)" with the + stakeholder ruling (five globals removed, reftable/polling gone, M8 migration, + text-input-only scope, D-9 separate) — APPLIED (lines 192–217). +- D-8 detail: `write_to_input` removed not facade; `tixy`→`set_text`; cursor + surface M7, removal+migration M8 — APPLIED (lines 529–546). +- D-9 detail: clarified D-9 is a *separate* surface, retained, keeps "only text + fields break" true; "Affects" updated — APPLIED (lines 562–575). +- D-10 detail: "Unchanged — legacy globals" → "Removed — legacy text-input + globals"; no legacy call-target left — APPLIED (lines 640–650). + +**`requirements.md §5`** — "Backward compatibility" bullet marked **RESOLVED +(clean break)**, cites `input.md` round 1, bounded to text input, pointer to +D-1 — APPLIED (lines 150–159). + +**`design.md`** — all claimed edits APPLIED: §3 `after_submit` row "after +evaluation (receives the result)" (165); §5 Enter flow drops "fill reftable" +(261); §6 retitled "Legacy API Removal", facade/deprecation/`strict_input` text +replaced with removal + migration, `love.state.user_input` plainly set/cleared, +**Native handler coexistence subsection kept** (285–331); §7 implementation-order +table drops "Legacy facades", adds "Legacy removal + example migration" last, +renumbers steps and notes the M1–M8 mapping (334–356); §7 FR-11 Enter row +"evaluate + push + `after_submit`" (375). OVER-APPLICATION check: no new API +surface, dispatch tier, or callback invented — the §7 step renumber (old step 3 +removed → six steps) is a description change, explicitly reconciled with the +roadmap's M1–M8 numbering in the same paragraph. Clean. + +**`spec.md`** — all claimed edits APPLIED: §1 drops "legacy wrappers" from +`keys_pressed` consumers (43); §3 `after_submit` drops "filled the reftable" +(305–310); §5 retitled "Legacy API Removal", rewired-functions table → removed → +replacement table, deprecation/`strict_input` subsections gone, +`love.state.user_input` note kept without "transition" framing (357–387); §7 +edge case drops the "reftable … stays empty" line (450–451). + +**`roadmap.md`** — all claimed edits APPLIED (detail in Part C): M2 setter line +removed (66–69); M3 voided with numbering kept (78–93); M4 input "M3" → "M2" +(101); M6 reftable lines removed (163–164, 184); M7 `write_to_input` clause +removed, `set_text` supersedes note added (205–208); M8 added (223–270); +Test-coverage bullet → "Example migration (M8)" (294–300); both estimate tables +recomputed with M3 dropped / M8 added + the "raised the estimate" note. + +**`summaries/decisions.md`** — status note + "Legacy text-input API removed" +paragraph + D-1/D-10 glance rows APPLIED (lines 7–11, 148–155, 169, 179). +**PARTIAL:** `changes1.md` claims only "D-1 and D-10 rows updated" for the +glance table — and indeed the **D-8 glance row (line 177) was left untouched** +and still reads "`write_to_input` facade", while the full `decisions.md` D-8 row +(line 160) was updated to "`set_text` supersedes the removed `write_to_input`". +So the changelog is internally honest, but the omission leaves the summary's +glance table inconsistent with its source. See Note 1. + +**`summaries/design.md`** — "what this adds" facade sentence → removal + +migration; components row "Legacy wrappers … facades" → "Legacy text-input +globals … removed; examples migrate; D-9 retained" — APPLIED (lines 12–18, 99). + +**`summaries/spec.md`** — "Legacy API" → "Legacy API — removed" with removed → +replacement table, no deprecation/`strict_input`; `after_submit` row "receives +the result" — APPLIED (lines 93, 119–134). + +**`summaries/roadmap.md`** — M3 row *(removed)*, M8 row added, Test-coverage row +updated, totals `≈ 63 h` / `≈ 37 h` + "raised the total" note — APPLIED. + +**`README.md`** — TL;DR "Old functions keep working" → removal + migration + +text-input-only scope; effort "(~35–59 h)" → "(~37–63 h)" — APPLIED. + +**Not-changed set honoured.** `assessment.md` untouched (not in the commit); +its reftable references describe *today's* code and are correct, not stragglers. +`notes/*` and `validation/*` untouched — historical record, as stated. + +--- + +## Part B — Cross-document consistency + +- **D-1 ruling reads the same everywhere.** "Discarded — no backward + compatibility; five globals removed; `tixy`/`balloons` priority migration; + others convert-or-exclude at the owner's call; text-input-only; D-9 separate" + appears consistently in `input.md`, `requirements.md §5`, `decisions.md` D-1, + `design.md §6`, `spec.md §5`, `roadmap.md M8`, and all four summaries. +- **Removed-functions table matches** between `spec.md §5` and + `summaries/spec.md` (same five functions, same replacements, both note + "no `strict_input`"). +- **Milestone table matches** between `roadmap.md` and `summaries/roadmap.md` + (M1, M2, M3 *(removed)*, M4, M5, M6, M7, M8). +- **Estimate figures match** across `roadmap.md`, `summaries/roadmap.md` + (`≈ 63 h` / `≈ 37 h`) and `README.md` (`~37–63 h`). +- **Milestone numbering / dangling refs.** Every `Mx` reference in the live + chain resolves (counts: M1×8, M2×16, M3×7, M4×12, M5×11, M6×16, M7×14, + M8×24). All seven `M3` mentions describe it as removed/voided/historical + ("the original M3 built facade wrappers", "the removed M3 was never a + dependency", "old M3 facade layer"); there is **no live reference to M3 + facades as if they still exist**. M4's dependency was corrected to "M2 + complete", consistent with the void. +- **D-8 / D-10 reconciled with the discard.** D-8 prose, D-10 prose, and the + D-10 "Affects" line all now state the globals are removed and that no legacy + call-target remains — they no longer contradict the discard. +- **Stragglers (negation contexts are correct, not findings).** Every surviving + "facade"/`strict_input`/deprecation/reftable mention in the live chain is + either a negation ("not wrapped as facades", "no `strict_input` flag", "the + reftable / polling idiom is gone"), a description of the voided M3, or the D-9 + lifecycle-split *wrapper* (a different surface). The lone exception is Note 1. + +### Note 1 (the only consistency defect) +`summaries/decisions.md:177`, D-8 glance row, still ends "`write_to_input` +facade". Its own full doc (`decisions.md:160`) reads "`set_text` supersedes the +removed `write_to_input`". A reader skimming only the summary glance table sees +a "facade" that the discard removed. Severity **NOTE** (a stale wording cell in +one summary; the D-1 row directly above it and the D-8 prose are both correct). +**Fixed this pass** (owner request): the cell now reads "`set_text` supersedes +the removed `write_to_input`", matching the full doc. + +--- + +## Part C — Roadmap consistency and ordering + +**Dependency order — walks clean.** +- M1 — Input: nothing. ✓ +- M2 — Input: M1 (keys_pressed exists). ✓ +- M3 — void (no Input line; explained empty slot). ✓ +- M4 — Input: M2 (was "M3 complete"; corrected, with an explicit note that the + removed M3 was never a functional dependency). ✓ +- M5 — Input: M4. ✓ +- M6 — Input: M4 (M5 independent). ✓ +- M7 — Input: M2 (M5/M6 not required — additive surface extension). ✓ +- M8 — Input: M7, body enumerating M2 + M6 + M7. ✓ (see ordering note below) + +**M8 ordering is correct and cannot be pulled earlier.** M8 needs the +submit/cancel callbacks (M6) and `set_text`/cursor (M7); the later of those two +prerequisites is M7 (M2-rooted) alongside M6 (M4-rooted), so M8 must follow +*both* M6 and M7. Placing it last satisfies that. It could not be ordered +earlier without losing a dependency. **Minor wording note (NOTE):** M8's +one-line header says "Input: M7 complete", but M7 itself does *not* depend on M6 +(M7 needs only M2). So "M7 complete" does not transitively guarantee M6 is done +— the genuine dependency set is M2 + M6 + M7, which the M8 body correctly spells +out. Because M8 is the final milestone in a linear M1→M8 build, M6 is always +complete by then in practice, so this is a precision nit in the header, not an +ordering break. **Fixed this pass**: the header now reads "Input: M6 and M7 +complete." + +**Migration scope is grounded** (re-verified against the tree): +`grep -rln -E 'input_text|input_code|validated_input|user_input|write_to_input' +src/examples/` returns exactly six examples with text-input use — +`tixy`, `balloons`, `repl`, `valid`, `turtle`, `guess`. This matches the +roadmap/round1 classification: +- **Priority:** `tixy` (`input_code` + `write_to_input` + `user_input`), + `balloons` (`input_text` + `user_input`) — confirmed per-function. ✓ +- **Convert-or-exclude:** `repl`, `guess`, `valid` (trivial), `turtle`. ✓ +- **`turtle` is the mixed case** — confirmed it has `love.keypressed` + (`src/examples/turtle/main.lua:35`) alongside `input_text`/`user_input`; the + roadmap correctly says the text-input use migrates while movement keys keep + working under D-9. ✓ +- **`maze`** named by stakeholders but **not in the repo** — confirmed absent; + correctly marked "migrate on arrival". ✓ +- **Unaffected (native-only):** `pong`, `life`, `paint`, `sapper`, `sine`, + `clock`, `drawdebug` — confirmed **none** call any of the five functions. ✓ + No example is mis-classified. + +**No regression of earlier roadmap invariants.** +- M2 still "zero behaviour change" (singleton created once; existing examples + and tests pass; `oneshot` explicitly stays through M2–M5). ✓ +- `oneshot` deletion still in **M6** (`roadmap.md` M6 output + `userInputModel.lua` + file list; mirrored in `summaries/roadmap.md`), not earlier. ✓ +- M7 still carries the cursor/`set_text` surface **and** the + `UserInputModel:set_text` `keep_cursor` model fix ("skip unconditional + `jump_end()`", `roadmap.md:213–214`). ✓ +- D-9 (native coexistence) retained as a separate surface in `design.md` + ("Native handler coexistence" subsection) and `spec.md §6`; reinforced, not + weakened, in `decisions.md` D-9. ✓ + +--- + +## Part D — Estimates, recomputed from scratch + +Recomputed every row as `PERT = (O + 4M + P) / 6`, summed O/M/P independently, +and recomputed the project-level PERT. No anchoring on prior figures. + +### Without LLM +| Row | O | M | P | PERT (recomputed) | Printed | +|---|---|---|---|---|---| +| M1 | 2 | 3 | 4 | 3.00 | 3.0 ✓ | +| M2 | 3 | 6 | 9 | 6.00 | 6.0 ✓ | +| M4 | 4 | 8 | 14 | 8.33 | 8.3 ✓ | +| M5 | 3 | 5 | 8 | 5.17 | 5.2 ✓ | +| M6 | 4 | 7 | 11 | 7.17 | 7.2 ✓ | +| M7 | 3 | 6 | 9 | 6.00 | 6.0 ✓ | +| M8 | 4 | 8 | 14 | 8.33 | 8.3 ✓ | +| Documentation | 4 | 8 | 12 | 8.00 | 8.0 ✓ | +| Test coverage | 7 | 11 | 16 | 11.17 | 11.2 ✓ | + +Column sums: O = 34, M = 62, P = 97 — match the printed totals row. +Project PERT = (34 + 4×62 + 97) / 6 = 379/6 = **63.17 h → ≈ 63 h** (matches). +Sum of per-row PERTs = 63.2 h (consistent). + +### With LLM +| Row | O | M | P | PERT (recomputed) | Printed | +|---|---|---|---|---|---| +| M1 | 1 | 2 | 3 | 2.00 | 2.0 ✓ | +| M2 | 2 | 4 | 6 | 4.00 | 4.0 ✓ | +| M4 | 3 | 5 | 9 | 5.33 | 5.3 ✓ | +| M5 | 2 | 3 | 5 | 3.17 | 3.2 ✓ | +| M6 | 2 | 5 | 8 | 5.00 | 5.0 ✓ | +| M7 | 2 | 4 | 6 | 4.00 | 4.0 ✓ | +| M8 | 2 | 4 | 8 | 4.33 | 4.3 ✓ | +| Documentation | 2 | 3 | 5 | 3.17 | 3.2 ✓ | +| Test coverage | 4 | 6 | 9 | 6.17 | 6.2 ✓ | + +Column sums: O = 20, M = 36, P = 59 — match the printed totals row. +Project PERT = (20 + 4×36 + 59) / 6 = 223/6 = **37.17 h → ≈ 37 h** (matches). +Sum of per-row PERTs = 37.2 h (consistent). + +**Coverage.** Both tables carry exactly the active milestones (M1, M2, M4, M5, +M6, M7, M8) plus Documentation and Test coverage — nine rows each. **M3 has no +row** in either table (correct — it is void). Nothing double-counted or missing. + +**Cross-doc agreement.** `roadmap.md` prints `≈ 63 h` / `≈ 37 h`; +`summaries/roadmap.md` prints `≈ 63 h` / `≈ 37 h`; `README.md` prints +`~37–63 h`. No full-vs-summary mismatch. + +**Plausibility, fresh.** The new **M8** (O/M/P 4/8/14, PERT 8.3 h without LLM) +is the only genuinely new estimate this round. Its scope is: delete five +functions from `consoleController.lua` (cheap) plus migrate the example corpus +(the bulk). Six in-repo examples touch the API; two are release-blocking +(`tixy`, `balloons`), four are convert-or-exclude. A most-likely of 8 h for +rewriting roughly six small pedagogical files to the callback API, plus removal +and re-testing, is reasonable; the pessimistic 14 h covers the awkward cases +(`tixy`'s `write_to_input`→`set_text` live-write, `turtle`'s native/text-input +mix). If anything the pessimistic tail is slightly conservative, because the +convert-or-exclude option caps the downside — a stubborn example can be excluded +from the release rather than fought — but 14 h is defensible and the spread is +sane. The M8 = 8.3 h ≈ old-M3 + 4 h delta claimed in the "raised the estimate" +note is internally consistent (old M3 facade was 4.0 h; net +4.3 h before +rounding). With-LLM M8 at 4.3 h (High value) is well-judged: mechanical +small-file rewrites are exactly where LLM assistance pays off. No row reads as +implausible for the scope it now describes. + +--- + +## Summary of actionable items (severity-ranked) + +| # | Severity | Location | Item | Status | +|---|---|---|---|---| +| 1 | NOTE | `summaries/decisions.md:177` | D-8 glance-table row read "`write_to_input` facade"; full `decisions.md:160` reads "`set_text` supersedes the removed `write_to_input`". | **FIXED** — cell synced to "`set_text` supersedes the removed `write_to_input`" (owner request, 2026-06-06). | +| 2 | NOTE (optional) | `roadmap.md:230` | M8 header "Input: M7 complete" understated the dependency set (M7 does not require M6, but M8 needs M6's callbacks). Body was already correct. | **FIXED** — header now reads "Input: M6 and M7 complete." (owner request, 2026-06-06). No ordering change — M8 is still the final milestone. | + +No blockers: no surviving project-facing facade/`strict_input`/deprecation +plan, no weakened D-9, no broken milestone dependency, no mis-classified +example, no estimate that fails to add up. + +## Recommendation + +**Ready for stakeholder review.** The substantive re-evaluation landed +faithfully and the chain is internally consistent; the two NOTE-level doc nits +have been corrected this pass (owner request), so no local follow-up remains. +The only open items are the pre-existing owner-delegated calls already recorded +in the chain (convert-or-exclude for `repl`/`guess`/`valid`/`turtle`, and the +out-of-repo `maze`), plus the standing approve/veto pass over the D-2…D-10 +proposal set. diff --git a/doc/development/wip/77-new-input-api/reevaluations/outcome1.md b/doc/development/wip/77-new-input-api/reevaluations/outcome1.md new file mode 100644 index 00000000..8ed5f7f2 --- /dev/null +++ b/doc/development/wip/77-new-input-api/reevaluations/outcome1.md @@ -0,0 +1,70 @@ +# Outcome — Re-evaluation Round 1 (D-1 discarded) + +*Summary of what round 1 did and where the chain stands. Full conclusions: +`round1.md`. File-by-file changelog: `changes1.md`.* + +--- + +The feedback was unambiguous, so the whole chain was updated. Conclusions are in +`reevaluations/round1.md`; the file-by-file changelog is in +`reevaluations/changes1.md`. + +(On file location: prompt 8 specified `./reevaluation/changes.md` (singular); +that directory does not exist, so both files live under the existing +`./reevaluations/`. N = 1 by the formula — the dir was empty — which also +matches the "feedback round 1" commit.) + +## The decision + +**D-1 (backward compatibility) → DISCARDED** by stakeholder consensus. It is now +ground truth in `input.md`, not a local proposal. The legacy text-input globals +(`input_text`, `input_code`, `validated_input`, `user_input`, `write_to_input`) +are **removed** — no facades, no `strict_input`, no deprecation shim, no +reftable/polling idiom. Examples migrate to `compy.input.*`. + +## Why it was unambiguous (no resolution needed from the owner) + +1. **What is removed is named** — D-1's scope was always exactly those five + functions. +2. **What survives is named** — the exchange *"only text fields?" — "Only text + fields."* bounds the break. The native `love.keypressed` coexistence path + (**D-9**) is a *separate* surface and must survive; it is literally what makes + "only text fields break" true. The feedback therefore **reinforces** D-9 + rather than touching it. +3. **The two loose ends are delegated by the feedback itself** — the + convert-or-exclude call for `repl`/`guess`/`valid`/`turtle` is explicitly the + owner's ("we can just exclude them… if we don't have the time"), and `maze` + is not in the repo yet. Recorded as owner-discretion, not blocking. + +## Notable second-order effect + +Discarding compatibility **raised** the estimate, it did not lower it: the ≈ 4 h +facade layer (old M3) is replaced by a larger ≈ 8 h migration milestone (new +**M8**). Project PERT: **≈ 59 → ≈ 63 h** (no LLM), **≈ 35 → ≈ 37 h** (with LLM). +Worth flagging — "no backward compat" is the right pre-1.0 call, but it is a +one-off migration cost, not a shortcut. + +## What changed + +- `decisions.md` — D-1 rewritten as a stakeholder ruling; status note carves + D-1 out as settled (D-2…D-10 still proposal); D-8/D-9/D-10 reconciled. +- `requirements.md §5` — backward-compat open question marked + **RESOLVED (clean break)**. +- `design.md` / `spec.md` — §6/§5 retitled "Legacy API Removal"; reftable + wording cleaned in flow diagrams and edge cases. +- `roadmap.md` — M3 **voided** (numbering kept so M4–M7 cross-references stay + valid), **M8 added**, both estimate tables recomputed. +- All affected `summaries/*` and `README.md` mirrored. +- **Left intact:** `assessment.md` (describes today's code, factually correct), + `notes/*` (flagged stale-on-D-1 but kept as the analysis record), + `validation/*` (audit trail). + +## Status + +Remaining stakeholder action is unchanged: a single approve/veto pass over the +**D-2…D-10** proposal set (D-1 is now settled). Nothing in the update blocks +implementation starting at M1. The build plan is M1→M8 (M3 intentionally empty). + +Open, owner-delegated (not blocking): the convert-or-exclude release call for +`repl`/`guess`/`valid`/`turtle`, and the arrival/migration of the out-of-repo +`maze` showcase. diff --git a/doc/development/wip/77-new-input-api/reevaluations/round1.md b/doc/development/wip/77-new-input-api/reevaluations/round1.md new file mode 100644 index 00000000..3b820218 --- /dev/null +++ b/doc/development/wip/77-new-input-api/reevaluations/round1.md @@ -0,0 +1,138 @@ +# Re-evaluation Round 1 — Stakeholder feedback: D-1 discarded + +*Date: 2026-06-06. Input: `input.md` "FEEDBACK AFTER FIRST ITERATION", +commit `de5d781` ("stakeholders feedback round 1"). This re-evaluation +answers prompt 8: can the chain be updated unambiguously, without further +direction?* + +--- + +## The feedback, in one line + +**D-1 (backward compatibility) is DISCARDED by stakeholder consensus.** No +backward compatibility is to be maintained for the input API. Get rid of the +legacy API; migrate the examples; pre-1.0, so a clean break is acceptable. + +This is now **stakeholder ground truth** (it lives in `input.md`), not a local +proposal. It overrides the previous "Suggested decision" for D-1 (keep +backward-compatible facades). + +## Verdict + +**Yes — the chain can be updated unambiguously, and it has been** (changelog: +`changes1.md`). Two follow-on points are *explicitly delegated to the owner by +the feedback itself*, so they are recorded as such rather than treated as +blocking questions for me to resolve. No item required guessing. + +## Why it is unambiguous + +The feedback is a clean discard of one decision, with the blast radius bounded +by the stakeholders in the same conversation. Three things pin it down: + +1. **What is removed is named.** D-1's scope was always exactly the four + text-input functions (`input_text`, `input_code`, `validated_input`, + `user_input`) plus `write_to_input`. "Get rid of the legacy API" maps + directly onto removing those five globals and the reftable/polling idiom + that goes with them. No facade layer, no `strict_input` flag, no deprecation + shim — those existed only to *support* backward compat, which is now gone. + +2. **What is *not* removed is named too.** The exchange + *"This won't break all keyboard input, only text fields? — Only text fields."* + bounds the break to text input. The native `love.keypressed`/`textinput` + coexistence path (D-9) is a **separate** surface; it must survive, because if + it didn't, *all* keyboard input for games would break, contradicting "only + text fields." So the feedback does not merely leave D-9 alone — it + **reinforces** it. D-9 is what makes the stakeholders' own guarantee true. + +3. **Migration scope is given, with the discretion explicitly delegated.** + - Priority (release-pressured): `maze`, `balloons`, and `tixy` (the showcase + set the owner named). `maze` is not in this repo yet; `balloons` and `tixy` + are. + - "For all the other examples, we can just exclude them from the next + release, if we don't have the time to convert them." → the convert-or- + exclude call for `repl`, `guess`, `valid`, `turtle` is **the owner's, at + release time** — not a question I need to settle. Recorded as such. + +## Grounding against the codebase + +In-repo examples using the legacy text-input functions (these are the ones the +removal affects): + +| Example | Uses | Disposition under M8 | +|---|---|---| +| `tixy` | `input_code`, `write_to_input`, `user_input` | **Priority** — migrate | +| `balloons` | `input_text`, `user_input` | **Priority** — migrate | +| `repl` | `input_text`, `user_input` | Convert (trivial) or exclude | +| `guess` | `validated_input`, `user_input` | Convert (trivial) or exclude | +| `valid` | `validated_input`, `user_input` | Convert or exclude | +| `turtle` | `input_text`, `user_input`, **+ native `love.keypressed`** | Text-input use migrates; movement keys keep working via D-9 | + +Examples using only native `love.keypressed`/`love.textinput` — +`pong`, `life`, `paint`, `sapper`, `sine`, `clock`, `drawdebug` — are +**unaffected** (D-9 coexistence; "only text fields" break). `maze` is named by +the stakeholders but is not in the repo; it is migrated on arrival. + +## The one judgment call, and why it needs no resolution + +Removing the legacy functions and migrating examples are not independent: you +cannot rewrite the examples to the new API until that API exists in full +(config with validator/highlighter, the submit/cancel callbacks, `set_text` +and the cursor surface). So the removal+migration work **must** come after the +last API milestone. The roadmap therefore puts it in a new final **M8**, after +M7. The only thing left "open" is whether to leave the text-input examples +non-running on the in-development build *earlier* than M8 — and that has no +bearing on the deliverable (the end state is identical, the work is unreleased, +and old releases remain available per `input.md`). So no stakeholder decision +is needed; the dependency order forces the outcome. + +## Notable second-order effect + +Discarding backward compatibility **increased** the effort estimate, it did not +reduce it. The ≈ 4 h facade layer (old M3) is replaced by a larger ≈ 8 h M8 +(removing the globals + rewriting the example corpus). Project PERT moves from +≈ 59 h → **≈ 63 h** without LLM assistance, and ≈ 35 h → **≈ 37 h** with. This +is worth surfacing to stakeholders: "no backward compat" is the right call for +a clean pre-1.0 API, but it is not a shortcut — it trades a small compat shim +for a larger one-off migration. + +## Authority-tier shift + +D-1 has moved from *local proposal* to *stakeholder-decided*. The status notes +on `decisions.md` and `summaries/decisions.md` now carve D-1 out as ground +truth; D-2…D-10 remain a local proposal awaiting the single approve/veto review. +`requirements.md §5`'s "backward compatibility" open question is marked +**RESOLVED (clean break)**. + +## What was changed + +See `changes1.md` for the file-by-file changelog. In short: `decisions.md` (D-1 +rewritten as a stakeholder ruling; D-8/D-9/D-10 reconciled), `requirements.md` +(§5 resolved), `design.md` (§6 "Legacy API Removal"; §5/§7 reftable wording), +`spec.md` (§5 "Legacy API Removal"; §1/§3/§7 reftable wording), `roadmap.md` +(M3 voided, M8 added, estimates recomputed), all affected `summaries/*`, and +`README.md`. + +## Not touched (deliberately) + +- `assessment.md` — describes the *current* architecture and the gaps; its + reftable/polling references are factual descriptions of today's code and + remain correct. It takes no backward-compat stance, so nothing to change. +- `notes/*` — supporting analysis (reference, not chain authority). They still + describe the facade approach and are now **stale on D-1**; flagged here rather + than rewritten, since they are the historical record of the analysis. Re-sync + on request: `notes/decisions.md` (D-1 section), `notes/solution_sketch.md` + (§2a "Legacy API compatibility"), `notes/design.md` ("Backward-compatibility + shims"). +- `validation/*` — historical review artifacts; left as the audit trail. + +## Where we are + +The chain is internally consistent again and reflects the stakeholders' D-1 +ruling end to end. Remaining stakeholder action is unchanged from before: a +single approve/veto pass over the **D-2…D-10** proposal set (D-1 is now +settled). The build plan is M1→M8; nothing in the update blocks implementation +from starting on M1. + +Open, owner-delegated (not blocking): the convert-or-exclude release call for +`repl`/`guess`/`valid`/`turtle`, and the arrival/migration of the out-of-repo +`maze` showcase. diff --git a/doc/development/wip/77-new-input-api/requirements.md b/doc/development/wip/77-new-input-api/requirements.md new file mode 100644 index 00000000..ad58434d --- /dev/null +++ b/doc/development/wip/77-new-input-api/requirements.md @@ -0,0 +1,179 @@ +# Feature #77 — New Input API: Requirements + +*Normalized from stakeholder input in `input.md` +(original ticket + stakeholder clarification). Does not propose +solutions or reference implementation internals.* + +--- + +## 1. Context and Purpose + +Compy user projects currently access text input through a small +set of functions (`input_text`, `input_code`, `validated_input`, +`user_input`). These return a polled reference; the project +checks it on each update tick to detect when the user has +submitted something. There is no way to receive keyboard events +while input is active, and no way to hide or show the input +area without tearing it down entirely. + +This feature defines a replacement API that addresses these +limitations. The intended users of the API are Compy project +authors — primarily students writing Lua on the Compy device. + +--- + +## 2. Functional Requirements + +### 2.1 Edit Area Setup + +**FR-1** The API shall allow a project to create a text edit +area. The following parameters shall each be independently +optional: + +| Parameter | Description | +|---|---| +| Initial text | Pre-fills the edit area with a given string | +| Initial cursor position | Places the cursor at a specified position within the initial text | +| Syntax highlighter | Applies highlighting as the user types | +| Input validator | Validates input per character and on submit | +| Prompt label | Displays a label alongside the edit area | + +### 2.2 Edit Area Lifecycle + +**FR-2** The API shall allow the project to programmatically +remove (tear down) the edit area. + +**FR-3** The API shall allow the project to hide the edit area +without removing it. + +**FR-4** The API shall allow the project to show a previously +hidden edit area. + +### 2.3 Event Notifications + +**FR-5** The API shall provide a way for the project to receive +notification when the user submits input (nominally: presses +Enter without a modifier). + +**FR-6** The API shall provide a way for the project to receive +notification when a key event occurs that does not produce a +text character in the edit area. This covers at minimum: +- keys pressed together with the Ctrl modifier; and +- non-character keys such as navigation keys and function keys + (noting the hardware constraint: built-in function keys on the + Compy device are intercepted by the OS and may not be + receivable — see §4). + +**FR-7** The API shall provide a way for the project to receive +notification when a cursor movement is attempted beyond the +first or last valid position in the edit area (a boundary hit). + +### 2.4 Programmatic Text and Cursor Control + +**FR-8** The API shall allow the project to query the current +cursor position while the edit area is active. + +**FR-9** The API shall allow the project to change the cursor +position programmatically while the edit area is active. + +**FR-10** The API shall allow the project to change the text +content of the edit area programmatically while it is active. + +### 2.5 API Expressiveness + +**FR-11** The API should be expressive enough that the console +REPL's input behaviour could be re-implemented using it, without +accessing the underlying implementation directly. + +**FR-12** The API should be expressive enough that the editor's +input handling could be re-implemented using it, without +accessing the underlying implementation directly. + +*FR-11 and FR-12 are expressiveness targets, not a commitment +to rewrite the console or editor as part of this feature. They +function as acceptance criteria for API completeness.* + +--- + +## 3. Non-Functional Requirements + +**NFR-1 (Allocation / GC)** The API shall not require creating +a new object graph for each input session. Reusing or +reconfiguring an existing instance on repeated invocations is +the expected pattern. This applies to student-facing example +code using the API; new framework code implementing the API +should follow the same discipline. + +**NFR-2 (Event-driven model)** The event notification mechanism +shall be consistent with LÖVE2D's event-driven style (callbacks +registered on an object or namespace) rather than requiring the +project to poll a reference for results. + +**NFR-3 (Compy API consistency)** The API shall fit naturally +within existing Compy conventions (namespace, naming, calling +style) so that projects using it do not encounter a stylistic +discontinuity with the rest of the `compy.*` surface. + +**NFR-4 (Pedagogical usability)** Simple use cases — showing an +input prompt and receiving the result via callback — shall +require minimal configuration. A student should not need to +understand framework internals to use the basic API. + +--- + +## 4. Out of Scope + +The following are not addressed by this feature: + +- **Multiple simultaneous edit areas.** The single active edit + area constraint is not relaxed; the requirements do not ask + for more than one at a time. +- **Touch input.** Touch event handling is not mentioned and is + separately deferred in the codebase. +- **Built-in function key support.** The Compy device's + built-in keyboard has its top-row function keys intercepted + by Android. FR-6 covers external keyboards; built-in function + keys are not a supported use case. +- **Mouse interaction beyond current behaviour.** The + requirements do not change or extend mouse handling on the + edit area. +- **Immediate rewrite of console or editor.** FR-11/FR-12 are + consistency tests for API design, not implementation tasks. + +--- + +## 5. Open Questions / Deferred + +The following items are not specified in the stakeholder input +and require a decision before or during design: + +- **Backward compatibility — RESOLVED (clean break).** Whether + `input_text()`, `input_code()`, `validated_input()`, and + `user_input()` must continue to work was originally unstated. + Stakeholders ruled on it in `input.md` (feedback round 1, + 2026-06-06): **no backward compatibility** — the legacy + text-input functions are removed, and the existing examples + (tixy, repl, guess, turtle, valid, balloons) that rely on them + are migrated to the new API or excluded from the release. The + break is bounded to text input; native keyboard handling is + unaffected. See `decisions.md` D-1. + +- **Behaviour when setup is called while already active.** The + expected result — silently skip, replace, or error — is + unspecified. + +- **Cancel / dismiss notification.** A callback for when the + user dismisses the edit area (nominally Escape) is not + explicitly listed. It is coverable via FR-6 if the project + handles Escape as a non-character key event, but a dedicated + dismiss notification may be expected. To be confirmed. + +- **Granularity of FR-6.** Whether Ctrl+key events and other + non-character key events should trigger the same notification + or separate ones is left to design. + +- **Boundary definition in multiline input (FR-7).** In a + multiline edit area, "boundary" may mean end-of-current-line, + end-of-the-entire-input, or both. The distinction matters for + how projects use the callback (e.g. block navigation in an + editor-like interface). Clarification is needed. diff --git a/doc/development/wip/77-new-input-api/roadmap.md b/doc/development/wip/77-new-input-api/roadmap.md new file mode 100644 index 00000000..23fdda6e --- /dev/null +++ b/doc/development/wip/77-new-input-api/roadmap.md @@ -0,0 +1,372 @@ +# Feature #77 — Implementation Roadmap + +*Scoped to this feature only. Milestones in +implementation-dependency order, matching the section +order in `notes/solution_sketch.md`. Estimates are in +the `## Estimates` section at the bottom.* + +> **Status — derived proposal document.** This roadmap is a derived +> part of the feature-#77 proposal chain, pre-built on the +> assumption the design is endorsed rather than vetoed. Milestone +> boundaries and estimates are provisional: stakeholders may review +> or adjust parts without blocking implementation — there is no +> requirement to freeze the plan before work starts. + +--- + +## Milestones + +--- + +### M1 — `keys_pressed` table + +**Description:** Framework-level live set of currently-held +key names. Zero behaviour change. + +**Input:** Nothing. This milestone has no dependencies and +can begin immediately. + +**Output:** `Controller.keys_pressed` is maintained +correctly. Combo serialisation helper (`combo_string`) +exists and is tested. All existing tests continue to pass. +No behavioural change observable in any app mode. + +**Files created or modified:** +- `src/controller.lua` — add `keys_pressed` table + maintenance in `love.handlers.keypressed` and + `love.handlers.keyreleased`; add `combo_string` helper + +**Risk:** None. Purely additive; existing handlers are +unchanged. + +--- + +### M2 — `UserInputController` singleton extraction + +**Description:** Move widget construction from inside +`ConsoleController` to framework startup. Widget created +once, never destroyed. Zero behaviour change. + +**Input:** M1 complete (keys_pressed table exists). + +**Output:** `UserInputController` is instantiated once at +startup (lazily). The widget is no longer created on each +input call. `compy.input.show()` and `compy.input.hide()` are +available on the namespace (required before later milestones +use them). Existing examples still work at this point (the +legacy globals are untouched until M8); allocation-per-session +is gone. Existing tests pass. The `oneshot` flag is **not** +removed in this milestone — it continues to drive submit +through M2–M5. + +**Files created or modified:** +- `src/main.lua` — create singleton instance at startup +- `src/consoleController.lua` — remove per-call + construction; wire to singleton +- `src/userInputController.lua` — `show()`/`hide()` state + change methods added +- `src/compy_namespace.lua` (or equivalent) — create the + `compy.input` table once at namespace setup; mount + `compy.input.show` and `compy.input.hide` on it + +**Risk:** Care needed to preserve `love.state.user_input` +set/clear behaviour. Existing tests exercise this path; +run all before and after. + +--- + +### M3 — *(removed — superseded by stakeholder feedback round 1)* + +The original M3 built backward-compatible facade wrappers for +the legacy text-input functions. **D-1 was discarded by +stakeholders** (`input.md`, feedback round 1, 2026-06-06): no +backward compatibility is maintained, so no facades are built. +The legacy text-input globals are instead **removed**, and the +examples are migrated to the new API — see **M8** (the work +moves to the end of the plan because migrating the examples +needs the full `compy.input.*` surface). + +The milestone numbering M4–M7 is kept unchanged to preserve the +many cross-references to those numbers elsewhere in the chain; +this slot is intentionally empty. + +--- + +### M4 — `ProjectController` introduction and overlay gate removal + +**Description:** New controller for the project-running +context. The `if user_input then` gate in `controller.lua` +is removed. Routing becomes symmetric. + +**Input:** M2 complete (singleton stable). The removed M3 was +never a functional dependency of this milestone. + +**Output:** `ProjectController:keypressed` and +`:textinput` occupy `love.keypressed` and `love.textinput` +when a project runs. Overlay input works as before (via +the sink). Project key events are no longer silently dropped +while the singleton is active. Existing tests pass. + +**Files created or modified:** +- `src/projectController.lua` — new file; implements + `ProjectController` class with `keypressed`, `textinput`, + `keyreleased` methods; basic sink delegation only (M5 + adds the full dispatch) +- `src/controller.lua` — remove overlay gate; wire + ProjectController into `set_handlers()` / `love.keypressed` + slot for project-running states + +**Risk:** Largest integration step. The overlay gate +removal touches the main dispatch path. Run full test suite +and manually verify all four app modes (REPL, editor, +project with overlay, project without overlay) before +marking complete. + +--- + +### M5 — Three-level dispatch in `ProjectController` + +**Description:** `compy.input.handlers`, `compy.input.on_key_pressed`, +and return-value bubbling implemented in +`ProjectController:keypressed`. + +**Input:** M4 complete (ProjectController exists; sink +delegation works). + +**Output:** `compy.input.handlers['ctrl+s'] = fn` works. +`compy.input.on_key_pressed` fires for unregistered keys. +Returning truthy from a handler prevents the sink from +running. The shared `dispatch()` function is written and +used by ProjectController. + +**Files created or modified:** +- `src/projectController.lua` — add three-level dispatch; + add `dispatch()` function (shared; ConsoleController and + EditorController will migrate to it later) +- `src/compy_namespace.lua` (or equivalent) — expose + `compy.input.handlers` table; expose `compy.input.on_key_pressed` + and `compy.input.on_text_entered` callback slots + +**Risk:** Combo serialisation must match registration format +exactly. Test with multi-modifier combos and with a handler +that returns truthy to verify chain stops. + +--- + +### M6 — Before/after chains for submit and cancel + +**Description:** `before_submit`, `after_submit`, +`before_cancel`, `after_cancel` hooks. Escape dismisses +the overlay (current limitation resolved). `on_limit_reached` +fires. `framework_handlers['return']` takes ownership of +submit. **`oneshot` flag deleted** (both its jobs are now +covered — activation by `show()`/`hide()` from M2, submit by +`framework_handlers['return']` here). + +**Input:** M4 complete (M5 is independent of M6). + +**Output:** All six named hooks fire at correct points. +Escape dismisses the overlay and fires `before_cancel` / +`after_cancel`. Submit fires `before_submit` / `after_submit` +with correct arguments. `on_limit_reached('up'/'down')` +fires when cursor hits boundary. The `oneshot` flag is gone. + +**Files created or modified:** +- `src/projectController.lua` — add `framework_handlers` + table; add `'return'` and `'escape'` entries with + before/after chain logic +- `src/userInputController.lua` — expose limit signal to + caller; remove submit-path code that reads `model.oneshot` +- `src/userInputModel.lua` — remove `oneshot` field + (lines ~15, ~49); this is the field's home (the submit-path + code that reads it is in `userInputController.lua`) + +**Risk:** Escape dismiss: ensure `push('userinput')` fires +in the cancel path (it currently fires only on successful +submit with `oneshot`). + +--- + +### M7 — Extended singleton API + +**Description:** `compy.input.configure()`, `compy.input.clear()`, +`compy.input.get_cursor()`, `compy.input.set_cursor()`, and +`compy.input.set_text()` implemented. Live reconfiguration of +validator and highlighter works. Cursor and text can be +programmatically read and written while active (FR-8/9/10). + +**Input:** M2 complete. M5/M6 are not required (this is an +API surface extension, not a dispatch change). + +**Output:** `compy.input.configure({prompt='new prompt'})` updates +the displayed prompt without tearing down the session. +`compy.input.clear()` resets content and cursor. `compy.input.get_cursor()` +returns `line, col`; `compy.input.set_cursor(line, col)` moves the +cursor. `compy.input.set_text(text [, keep_cursor])` replaces live +content. All work while the singleton is active and when hidden. +`compy.input.set_text` is the live-write surface that replaces the +removed `write_to_input` (see M8). + +**Files created or modified:** +- `src/userInputController.lua` — add `configure()`, + `clear()`, `get_cursor()`, `set_cursor()`, `set_text()` + methods; fix `UserInputModel:set_text` to honour + `keep_cursor` (skip unconditional `jump_end()`) +- `src/compy_namespace.lua` — expose `compy.input.configure`, + `compy.input.clear`, `compy.input.get_cursor`, `compy.input.set_cursor`, + `compy.input.set_text` + +**Risk:** None. Additive; no routing changes. + +--- + +### M8 — Legacy text-input removal and example migration + +**Description:** Remove the legacy text-input globals and migrate +the in-repo examples that use them to the `compy.input.*` callback +API. D-1 discarded — no backward compatibility (`input.md`, +feedback round 1). + +**Input:** M6 and M7 complete. This milestone needs the **full** +`compy.input.*` surface — `show()` with `validator`/`highlighter` +config (M2 + M7), the submit/cancel callbacks (M6), and +`set_text`/cursor (M7) — because migrating the examples depends +on all of it. It is therefore the last milestone. + +**Output:** +- `input_text()`, `input_code()`, `validated_input()`, + `user_input()`, and `write_to_input()` are removed from the + project environment. `love.state.user_input` is driven solely + by `compy.input.show()`/`hide()`. +- **Priority examples migrated (release-blocking):** `tixy` + (uses `input_code` + `write_to_input` + `user_input`) and + `balloons` (uses `input_text` + `user_input`). `maze` is named + by stakeholders as a showcase but is not in this repo; it is + migrated on arrival (out of current repo scope). +- **Convert-or-exclude (owner's release call, per `input.md`):** + `repl`, `guess`, `valid` (trivial REPL conversions) and + `turtle` (its `input_text` use migrates; its native + `love.keypressed` movement keeps working under D-9). Any not + converted in time are excluded from the next release rather + than blocking it. +- **Unaffected:** examples that use only native + `love.keypressed`/`love.textinput` (`pong`, `life`, `paint`, + `sapper`, `sine`, `clock`, `drawdebug`) — D-9 coexistence + applies; "only text fields" break. + +**Files created or modified:** +- `src/consoleController.lua` — remove the five legacy entry + points (`user_input`, `input_text`, `input_code`, + `validated_input`, `write_to_input`) +- `src/examples/tixy/`, `src/examples/balloons/` — migrate to + `compy.input.*` (priority); `src/examples/{repl,guess,valid,turtle}/` + — migrate or mark excluded + +**Risk:** The examples are the only consumers, so the blast +radius is contained. There is a window (M3-slot onward) where +the text-input examples do not run on the in-development build; +this is internal and acceptable (the work is unreleased and old +releases remain available — `input.md`). The reftable / polling +idiom disappears from the example corpus. + +--- + +## Additional Scope + +### Documentation updates + +- Update `doc/development/internals/` input subsystem docs + to reflect the new singleton lifecycle, routing model, and + API surface. +- Update `doc/development/overview.md` architecture section + if the controller listing or app_state machine description + needs to account for `ProjectController`. +- Archive or annotate stale wip notes after release + (primarily `notes/design.md`, `notes/plan.md`). + +### Test coverage + +Busted tests for: +- `keys_pressed` table: key add/remove, combo serialisation, + multi-modifier ordering +- Singleton lifecycle: show/hide state, configure fields, + clear, show-while-active reconfiguration +- Dispatch chain (each level): handler registration, + return-value bubbling, default callback fires when no + handler matches +- Example migration (M8): the priority examples (`tixy`, + `balloons`) run on the `compy.input.*` callback API; the legacy + globals are gone (calling `input_text` etc. is now a `nil` + call); native-handler examples still work via D-9 +- Edge cases from `spec.md §7`: stop-while-active, show + while active, evaluation failure locking behaviour + +--- + +## Estimates + +*Implementor assumed: senior engineer, solo. Familiar with Lua +and LÖVE2D. Has read the design and spec documents. Three-point +estimates per line; PERT = (O + 4M + P) / 6, where O = optimistic, +M = most-likely, P = pessimistic. Hours.* + +### Without LLM assistance + +| Item | O | M | P | PERT | +|---|---|---|---|---| +| M1 `keys_pressed` table | 2 | 3 | 4 | 3.0 | +| M2 Singleton extraction | 3 | 6 | 9 | 6.0 | +| M4 ProjectController + gate removal | 4 | 8 | 14 | 8.3 | +| M5 Three-level dispatch | 3 | 5 | 8 | 5.2 | +| M6 Before/after chains (+ `oneshot` deletion) | 4 | 7 | 11 | 7.2 | +| M7 Extended API (+ cursor surface, model fix) | 3 | 6 | 9 | 6.0 | +| M8 Legacy removal + example migration | 4 | 8 | 14 | 8.3 | +| Documentation updates | 4 | 8 | 12 | 8.0 | +| Test coverage | 7 | 11 | 16 | 11.2 | +| **Total** | **34** | **62** | **97** | **≈ 63 h** | + +Project PERT (O=34, M=62, P=97): `(34 + 4×62 + 97) / 6 ≈ 63 h`. + +Note: discarding backward compatibility (D-1) *raised* the +estimate. The old M3 facade layer (≈ 4 h) is gone, but M8 — +removing the legacy globals and migrating the examples — is +larger (≈ 8 h), because rewriting the example corpus is more +work than wrapping the old calls. The net is +≈ 4 h vs. the +facade plan. + +Confidence: moderate. M4 (gate removal, central dispatch path) +remains the widest pessimistic tail; M8 is the next-widest, as +the per-example migration effort and the convert-or-exclude +release decision both vary with the owner's call. + +### With LLM assistance + +LLM helps most with boilerplate wiring (M1, M7), example +rewriting (M8), new-file scaffolding (M5), test scaffolding, and +doc updates. It saves +least on M2 (cross-component refactor verified by hand), M4 +(integration; the engineer must trace the dispatch paths), and M6 +(ordering semantics). + +| Item | O | M | P | PERT | LLM value | +|---|---|---|---|---|---| +| M1 `keys_pressed` table | 1 | 2 | 3 | 2.0 | High | +| M2 Singleton extraction | 2 | 4 | 6 | 4.0 | Low | +| M4 ProjectController + gate removal | 3 | 5 | 9 | 5.3 | Medium | +| M5 Three-level dispatch | 2 | 3 | 5 | 3.2 | High | +| M6 Before/after chains (+ `oneshot` deletion) | 2 | 5 | 8 | 5.0 | Low–medium | +| M7 Extended API (+ cursor surface, model fix) | 2 | 4 | 6 | 4.0 | High | +| M8 Legacy removal + example migration | 2 | 4 | 8 | 4.3 | High | +| Documentation updates | 2 | 3 | 5 | 3.2 | High | +| Test coverage | 4 | 6 | 9 | 6.2 | High | +| **Total** | **20** | **36** | **59** | **≈ 37 h** | + +Project PERT (O=20, M=36, P=59): `(20 + 4×36 + 59) / 6 ≈ 37 h`. + +Confidence: moderate. The saving is largest for well-specified +generative work (M1, M5, M7, M8, tests, docs) — rewriting the +small example files to the new API is exactly the kind of +mechanical edit an LLM does well, so M8's LLM value is High. The +integration milestones (M2, M4) are serial and verification-heavy, +so LLM code generation speeds them up but the manual verification +cost is largely fixed. diff --git a/doc/development/wip/77-new-input-api/spec.md b/doc/development/wip/77-new-input-api/spec.md new file mode 100644 index 00000000..e365cf75 --- /dev/null +++ b/doc/development/wip/77-new-input-api/spec.md @@ -0,0 +1,464 @@ +# Feature #77 — API Specification + +*Contract that implementation is verified against. +Audience: implementors. Includes function signatures, +data structure formats, and edge case behaviour.* + +*Design context: `design.md`. Stakeholder summary: `summaries/spec.md`.* + +> **Status — derived proposal document.** This spec is a derived +> part of the feature-#77 proposal chain, pre-built on the +> assumption the design (`decisions.md` → `design.md`) is endorsed +> rather than vetoed. Its detail is not frozen: stakeholders may +> review or change parts without blocking implementation — there is +> no requirement to freeze the spec before work starts. + +--- + +## 1. `keys_pressed` Table + +### Format + +A plain Lua table mapping LÖVE2D key name strings to `true`: + +```lua +-- example: lctrl and s held simultaneously +{ ['lctrl'] = true, ['s'] = true } +``` + +Key names are LÖVE2D canonical names: `"lctrl"`, `"rctrl"`, +`"lshift"`, `"rshift"`, `"lalt"`, `"ralt"`, `"return"`, +`"escape"`, `"backspace"`, `"up"`, `"down"`, `"left"`, +`"right"`, `"f1"`–`"f12"`, printable key names (`"a"`–`"z"`, +`"0"`–`"9"`, `"space"`, etc.). + +### Ownership and passing + +The table is owned by `Controller` (global controller, +`controller.lua`). It is updated unconditionally on every +`love.handlers.keypressed` (add key) and +`love.handlers.keyreleased` (remove key) call, before any +downstream handler runs. + +Downstream consumers — `ProjectController` and the project +callbacks — receive the table as a **read-only proxy**: an +iterator-only wrapper. Direct indexing on the proxy is not +supported; consumers iterate with `for k in pairs(proxy) do`. +This prevents project code from tampering with the live +modifier state. + +The proxy is passed as the second argument to every +`keypressed` and `textinput` callback downstream: + +```lua +ProjectController:keypressed(k, keys_pressed, isrepeat) +ProjectController:textinput(t, keys_pressed) +``` + +### Combo serialisation + +A combo string is built by `combo_string(k, keys_pressed)`, +which takes the **triggering key** `k` and the held-key set. +It prepends any held command-modifiers in fixed precedence +order — `ctrl`, `alt`, `shift`, `gui` — then appends `k`. +Modifier names are **generic** (l/r folded): the combo uses +`ctrl` (not `lctrl`/`rctrl`), `alt` (not `lalt`/`ralt`), etc. +The `keys_pressed` table retains precise LÖVE key names; only +combo serialisation folds to generic names. + +```lua +-- lctrl held, s triggers: "ctrl+s" +-- lalt and lshift held, f4 triggers: "alt+shift+f4" +-- escape alone: "escape" +-- s alone (bare key): "s" +``` + +Ordering is modifier-first by fixed precedence, then the +triggering key. A bare key (no modifiers held) produces just +the key name. `combo_string` is used at every dispatch point. + +**Registration normalisation.** `compy.input.handlers` is +metatable-backed: `__newindex` normalises the assigned key to +canonical form on assignment, so `compy.input.handlers['Ctrl+S'] = fn` +is stored as `compy.input.handlers['ctrl+s']` and fires correctly. +Dispatch uses an **overloadable exact-match matcher** by default +(O(1) table lookup); the matcher function is project-overloadable +for future glob/prefix extensions. + +--- + +## 2. `UserInputController` Singleton API + +All interaction goes through the `compy` namespace. Project +code never holds a direct reference to the controller object. + +### `compy.input.show(config)` + +Activates the singleton with the given configuration. +`config` is an optional table; all fields are optional: + +| Field | Type | Description | +|---|---|---| +| `prompt` | string | Label displayed beside the input area | +| `text` | string | Initial text content; cursor placed at end | +| `cursor` | `{line, col}` | Initial cursor position; 1-based source-line coordinates; applied after `text`; `line` defaults to 1 (single-line callers may pass just `{1, col}` or `{col}`) | +| `highlighter` | function | Syntax highlighter: `fn(text) → highlighted_text` | +| `validator` | function | Per-submit validator: `fn(text) → ok, err_msg` | +| `multiline` | boolean | Allow Shift+Enter newlines (default false) | + +Calling `show()` while the singleton is already visible: +the singleton is reconfigured in-place with the new config. +No error is raised; no cancel chain fires; content is +replaced if `text` is provided, preserved otherwise. + +`love.state.user_input` is set to the singleton instance +on `show()`. + +### `compy.input.hide()` + +Deactivates the singleton without firing the cancel chain. +Input content is preserved (subsequent `show()` will +display it unless `text` is provided). `love.state.user_input` +is set to `nil`. Project code uses `hide()` for programmatic +lifecycle management; the user-facing dismiss path is Escape +(which fires the cancel chain). + +### `compy.input.configure(config)` + +Live-updates the singleton's configuration while it is +active. Accepts the same fields as `show()`. Only the +provided fields are updated; unspecified fields are +unchanged. Safe to call when hidden (takes effect on next +`show()`). + +Live-updatable fields: `prompt`, `highlighter`, `validator`. +Fields `text` and `cursor` are accepted but have no effect +when called on an already-active session (use `compy.input.clear()` +and `compy.input.show()` to reset content). + +### `compy.input.clear()` + +Clears the text content of the active input session without +hiding the singleton. Cursor resets to position 1. Does not +fire any callback. No-op if the singleton is hidden. + +### `compy.input.get_cursor()` + +Returns the current cursor position as two values `line, col` +(1-based source-line coordinates, not wrapped/apparent lines). +Returns `nil` when the singleton is hidden. Read-only; use +`compy.input.set_cursor` to change position. + +### `compy.input.set_cursor(line, col)` + +Sets the cursor to `(line, col)` — 1-based source-line +coordinates. The model clamps the values to the valid range. +No-op when the singleton is hidden. Single-line callers +always pass `line = 1`. + +### `compy.input.set_text(text [, keep_cursor])` + +Replaces the full text content of the active input session. +This is the **live-write surface** and the explicit exception +to `compy.input.configure()`'s text-immutability rule. If +`keep_cursor` is true, the cursor position is preserved (the +model's `set_text` skips the unconditional `jump_end()` when +`keep_cursor` is set). If omitted or false, the cursor moves +to the end of the new text. No-op when hidden. Triggers a +view update. + +### Access control + +No access control is enforced in this version. A project +calling `show()` while another subsystem is using the +singleton will reconfigure it. This is a known future +concern; it is not in scope for this feature. + +--- + +## 3. Event Callbacks + +All callbacks are fields on the `compy.input` table. The +submit/cancel/limit callbacks (`before_submit`, `after_submit`, +`before_cancel`, `after_cancel`, `on_limit_reached`) default to +a no-op function that emits a debug log entry. The two **channel +callbacks** (`on_key_pressed`, `on_text_entered`) are different: +their default value is the text-editing sink, not a no-op (see +each section below). Project code assigns replacement functions: + +```lua +compy.input.on_text_entered = function(text, keys) + -- ... +end +``` + +All callbacks are reset to their defaults when the project +stops, via `stop_project_run` / `clear_user_handlers` +(`consoleController.lua:860–868`). + +### `compy.input.on_text_entered(text, keys_pressed)` + +Fires when `love.textinput` delivers a character event and +the singleton is active. + +- `text`: UTF-8 string delivered by the OS (one character in + the common case; may be multiple characters from IME input). +- `keys_pressed`: read-only proxy of currently-held keys at + the time the `keypressed` event that preceded this + `textinput` fired. Non-character keys in this set are the + implicit modifier context (e.g. if `"lctrl"` is in the set, + the user pressed Ctrl+something that produced a character). + +**Default value:** the textinput sink +(`UserInputController:textinput`). This mirrors `on_key_pressed` +exactly — the channel's default *is* the sink, and assigning a +function replaces it. The two channels follow the same +default-sink/override principle; the only difference is that the +textinput channel has no combo tier above it. + +Does not fire for Ctrl+V paste (handled entirely via +`keypressed` → clipboard path). + +*Future seam (not built in v1): a pre-folded `mods` string — the +generic l/r-folded modifier descriptor, like the combo form — +could be passed as a trailing argument to this and the other +downstream handlers, as a convenience over reading modifiers off +the `keys_pressed` proxy. v1 ships the proxy only; the `mods` +string is a candidate addition (see `decisions.md` D-6).* + +### `compy.input.on_key_pressed(k, keys_pressed, isrepeat)` + +Fires for every `love.keypressed` event while the singleton +is active — both character-producing and non-character keys. +This is the **keypressed channel**; the textinput channel is +`compy.input.on_text_entered` and is independent. Both channels +may fire for a single user gesture (a character key visits +both); there is no suppression. The expected division of +labour: command detection → combos / `on_key_pressed`; +text capture → `on_text_entered`. + +- `k`: LÖVE2D key name string. +- `keys_pressed`: read-only proxy of currently-held keys. +- `isrepeat` (trailing arg): `true` if this is a key-repeat + event, `false` otherwise. Passed last so the common + `fn(k, keys)` signature remains clean for callers that do + not need it. + +**Default value:** the text-editing sink +(`UserInputController:keypressed`). Assigning a function to +`compy.input.on_key_pressed` replaces the default entirely. There +is no separate sink tier below it — the sink *is* the +default. If the default is replaced, `on_limit_reached` no +longer fires (it originates in the keypressed sink); the +project has taken over key handling. + +To intercept specific key combinations before the sink, use +`compy.input.handlers[combo]` and return truthy. + +### `compy.input.handlers[combo]` + +A metatable-backed table mapping combo strings to handler +functions. Project code registers directly: + +```lua +compy.input.handlers['ctrl+l'] = function(k, keys) + clear_screen() + return true -- consumed; sink does not run +end +``` + +Handler signature: `fn(k, keys_pressed) → truthy|nil` + +- `k`: the key name that triggered the dispatch. +- `keys_pressed`: read-only proxy. +- Return truthy to consume (stop the chain; sink does not run). +- Return nil or nothing to let the chain continue. + +**Normalisation on assignment.** `compy.input.handlers` uses a +metatable: `__newindex` normalises the key to canonical form +(modifier-first, generic l/r folding) before storing it, so +`compy.input.handlers['Ctrl+S'] = fn` and `compy.input.handlers['ctrl+s'] = fn` +produce the same entry and fire correctly. + +Dispatch uses `combo_string(k, keys_pressed)` to build the +current combo at event time, then passes it through the +**overloadable matcher** (default: exact canonical match). +The matcher is project-overloadable via +`compy.input.handlers.__matcher = fn` for future glob/prefix needs. + +The same key in different modifier states produces different +combos: `"s"` and `"ctrl+s"` are distinct entries. + +**Bare printable-key combos** (e.g. `handlers['s']`) do fire +on `keypressed`, alongside `on_text_entered`. The expected +convention is to reserve combos for command modifiers +(`ctrl`, `alt`, `gui`). Both channels fire by design; project +code picks the channel it needs. + +### `compy.input.before_submit(keys_pressed)` + +Fires before the framework evaluates the input. `keys_pressed` +is the read-only proxy at the time Enter was pressed. + +Cannot suppress submit. Return value is ignored. Use for +pre-submit side effects (e.g. logging, visual feedback). + +### `compy.input.after_submit(result)` + +Fires after the framework has evaluated the input and pushed +`'userinput'`. `result` is the evaluated text string. + +Fires only on successful evaluation. Does not fire if the +validator rejected the input (input stays locked with error +display until the user acknowledges and corrects it). + +### `compy.input.before_cancel(keys_pressed)` + +Fires before the framework dismisses the singleton via +Escape. `keys_pressed` is the read-only proxy at dismiss +time. + +Cannot suppress cancel. Return value is ignored. Use for +pre-cancel side effects. + +### `compy.input.after_cancel()` + +Fires after the framework has dismissed the singleton +(`love.state.user_input` set to nil, content cleared). +No arguments. + +--- + +## 4. `on_limit_reached(direction)` + +```lua +compy.input.on_limit_reached = function(direction, _reserved) + -- direction: 'up' or 'down' +end +``` + +Fires when the cursor attempts to move past the first or +last valid position in the input area (a whole-input +boundary, consistent with the existing `UserInputModel:is_at_limit` +implementation). + +- `direction`: `'up'` when the cursor tries to move past + the first line; `'down'` when it tries to move past the + last line. +- Second argument `_reserved`: undefined in v1; reserved for + future boundary-level granularity (e.g. line-level vs. + input-level). Do not use. + +The callback always propagates — both project code and +framework code observe the same boundary event independently. +Return value is ignored. + +--- + +## 5. Legacy API Removal + +D-1 was discarded by stakeholders (`input.md`, feedback round 1): +no backward compatibility. The legacy text-input globals are +**removed** from the project environment — not wrapped as facades: + +| Removed function | Replacement | +|---|---| +| `user_input()` | None — the reftable / `is_empty()` polling idiom is gone. Submit is observed via `compy.input.after_submit(result)`. | +| `input_text(prompt, init)` | `compy.input.show({ prompt = prompt, text = init, validator = InputEvalText })` + `compy.input.after_submit` | +| `input_code(prompt, init)` | `compy.input.show({ prompt = prompt, text = init, highlighter = lua_hl, validator = InputEvalLua })` + `compy.input.after_submit` | +| `validated_input(fn, prompt)` | `compy.input.show({ prompt = prompt, validator = ValidatedTextEval(fn) })` + `compy.input.after_submit` | +| `write_to_input(content)` | `compy.input.set_text(content)` | + +There is no deprecation shim and no `strict_input` flag — the +functions simply no longer exist. The in-repo examples that used +them are migrated to the patterns above (roadmap M8). The break +is bounded to text input; native `love.keypressed`/`textinput` +handling is unaffected (§6). + +### `love.state.user_input` + +Set and cleared by the singleton's `show()`/`hide()` methods, +exactly as for any new-API caller. Points to the singleton +instance when active; set to `nil` when hidden. There is no +separate legacy code path that touches it. Framework code that +checks `love.state.user_input` for nil-ness continues to work. + +--- + +## 6. `ProjectController` + +### Activation + +`ProjectController` becomes the occupant of `love.keypressed` +when `app_state` transitions to `'running'` or +`'project_open'`. This is the same slot mechanism used by +`ConsoleController`; `set_handlers()` places +`ProjectController:keypressed` in `love.keypressed` when a +project starts. + +### Deactivation + +On project stop, `love.keypressed` is restored to +`ConsoleController:keypressed` (the permanent baseline), +exactly as it is today. `compy.input.handlers` and all project +callbacks (`on_key_pressed`, `on_text_entered`, etc.) are +reset to their defaults. + +### Relationship to `love.keypressed` slot + +`ProjectController:keypressed` IS the `love.keypressed` +occupant during project execution. It is not called from +within `love.handlers.keypressed` by special case — the +overlay gate is removed. The framework's +`love.handlers.keypressed` dispatches to whatever is in +`love.keypressed`, which is `ProjectController:keypressed` +while a project runs. + +### Native handler coexistence + +Projects that define `love.keypressed`/`love.textinput` +natively (captured by the existing `save_user_handlers` path) +AND set none of the `compy.*` input surfaces are treated as +**legacy** by `ProjectController`. At load time, +`ProjectController` auto-provisions `compy.input.on_key_pressed` as +a lifecycle-split wrapper: + +- **Singleton visible:** routes to the text-editing sink. +- **Singleton hidden:** routes to the project's saved native + handler. + +This reproduces today's gated behaviour with zero project +changes. Projects that set any `compy.*` surface explicitly +are new-style; the heuristic never engages. In debug mode, +the wrapper logs which branch it chose. The auto-provisioned +wrapper is cleared on project stop (via `stop_project_run` / +`clear_user_handlers`). + +--- + +## 7. Edge Cases + +### `show()` while already active + +Behaviour: reconfigures in-place. Content replaced if `text` +is specified; otherwise preserved. No cancel chain fires. +No error. This is the intended API for dynamic prompt +changes (FR-3/FR-4). + +### Project stops while input is active + +Behaviour: silent hide. The singleton's `hide()` is called +as part of project teardown. No cancel chain fires. +`love.state.user_input` is set to nil. Callbacks are reset. +The project has already stopped and cannot observe any +teardown event. + +### Evaluation failure (validator rejects input) + +Behaviour: existing behaviour preserved. The singleton +displays the error highlight; input is locked. The user +must acknowledge the error (Enter, Space, or arrows) before +further editing is possible. `after_submit` does not fire. +`before_submit` has already fired. No cancel is triggered. +The session remains active until the user either corrects +and re-submits, or presses Escape. diff --git a/doc/development/wip/77-new-input-api/summaries/assessment.md b/doc/development/wip/77-new-input-api/summaries/assessment.md new file mode 100644 index 00000000..70dd56a2 --- /dev/null +++ b/doc/development/wip/77-new-input-api/summaries/assessment.md @@ -0,0 +1,86 @@ +# Feature #77 — What the Architecture Can and Cannot Do Today + +*Summary of `assessment.md`. Read that document for per- +requirement analysis, file and function references, and the +full reuse/extend/replace breakdown.* + +--- + +## What already exists and is reusable + +The core input widget — text editing, cursor movement, +selection, history, syntax highlighting, validation, error +display — is solid and can be kept largely as-is. The +underlying model knows how to detect cursor boundaries; the +view renders correctly; the evaluator pipeline is clean. +Roughly half the work for this feature is already done at +the component level. + +## What is missing + +**Keyboard events don't reach projects while a prompt is +active.** This is the largest gap. When a project shows an +input prompt, all keyboard events are routed to the prompt +widget and the project's own key handler is bypassed +entirely. There is currently no way for a project to respond +to Ctrl shortcuts, navigation keys, or any other key while +input is on screen. Fixing this touches the event routing +layer shared across all application modes. + +**No callbacks — only polling.** The project receives a +reference object and must check it on every frame to detect +when the user has submitted something. There is no mechanism +to register a callback for any event: submit, key press, or +cursor boundary. The entire examples library is built on +the polling pattern. + +**No lifecycle control.** The prompt cannot be hidden, +shown, or removed by project code — only by user action +(Enter or Escape). Showing a prompt and then wanting to +change it requires tearing it down and recreating it, which +is what causes the limitation seen in the balloons example. + +**Cursor and prompt are not accessible from project code.** +The cursor position can be read and set internally, but +projects have no handle on these. Text content can be +replaced via `write_to_input()`, but the prompt label and +other setup parameters cannot be changed on a live prompt. + +## What needs to change + +Two things require structural work rather than localised +additions: the event routing (so key events can reach +project code while a prompt is active) and the object +lifecycle (so the prompt widget is created once and +reconfigured rather than discarded and recreated on each +use). Everything else — callbacks, cursor access, hide/show, +programmatic removal — is relatively contained once those +two are resolved. + +## Known risks + +The input widget is shared across three contexts: the +console REPL, the editor, and project overlays. Changes to +how it routes events will need to be compatible with all +three, or explicitly isolated to the overlay path. This is +the main scope risk: what looks like a project API change +has framework-wide reach. + +A second risk is an edge case without a defined policy: when +a character-producing key is pressed while a modifier is held, +the underlying framework fires two separate events. A +callback-based API could end up delivering two notifications +for what the user experienced as a single gesture. This needs +an explicit decision before the callback model can be designed. + +A minor but confirmed issue: `compy.text_input`, documented +as an alias for `input_text`, has never functioned due to +a stale reference in the setup code. No current code relies +on it, so it can be replaced cleanly in the new design. + +## Overall picture + +The feature is achievable largely by building on what exists. +The core widget needs no major surgery. The work is +concentrated in two areas — event routing and lifecycle +redesign — with the rest following from those decisions. diff --git a/doc/development/wip/77-new-input-api/summaries/decisions.md b/doc/development/wip/77-new-input-api/summaries/decisions.md new file mode 100644 index 00000000..0e2f7ebd --- /dev/null +++ b/doc/development/wip/77-new-input-api/summaries/decisions.md @@ -0,0 +1,179 @@ +# Feature #77 — Design Direction and Decisions + +*Summary of `decisions.md`. Full question context, rationale, +and decision dialogue are in that document and in +`notes/decisions.md`.* + +> **Status:** A local design proposal derived from `input.md`, +> pending a single eventual stakeholder approve/veto review — with +> one exception: **D-1 has been ruled on** (`input.md`, feedback +> round 1: **DISCARDED**, no backward compatibility). D-1 below is +> stakeholder ground truth; D-2…D-10 are still proposal. + +--- + +## The blocking decisions + +Seven questions arose from requirements analysis and +architecture assessment that require stakeholder consensus +before design and implementation can proceed: + +1. Must existing input functions still work after the new + API is introduced? +2. What happens when a project tries to configure input + while it is already active? +3. How should different kinds of key events (modifier combos, + navigation keys, function keys) reach project code? +4. Should cancel and submit be dedicated named events, and + how does the framework's own teardown relate to project + callbacks? +5. In a multiline input area, what exactly constitutes the + cursor hitting a boundary? +6. When a character key is pressed while a modifier is held, + should the project receive one event or two? +7. Should the new event mechanism apply to all input + contexts at once, or the project overlay first? + +Three further architectural decisions were recorded during the +local design rounds: **D-8** (cursor restoration + 2D contract) +and **D-9** (native handler coexistence) from round 1, and +**D-10** (namespace isolation under `compy.input.*`) from round 2. +They are not new stakeholder questions but genuine decisions — +listed in the glance table below with provenance. + +--- + +## Proposed resolution + +A design approach has been developed that addresses all +seven questions coherently. It is summarised below and +detailed in `decisions.md`. + +**Fast-track path:** if the direction is acceptable, work +proceeds to `design.md` and the full implementation plan. + +**Veto path:** if any aspect is problematic, the relevant +decision(s) are re-opened. Because the decisions form a +coherent set, a veto of one may affect others — the detail +document tracks these dependencies. + +--- + +### The architectural basis + +The structural change that makes all three improvements +possible is routing unification. The +`if user_input then ... else ... end` gate in +`love.handlers.keypressed` (`controller.lua`) is removed. +A new `ProjectController` — a sibling to `ConsoleController` +and `EditorController` — takes ownership of all input +handling for the project-running context. `UserInputController` +becomes the universal terminal sink at the bottom of every +branch: always present, never a routing destination. See +`notes/routing_unification.md` for the before/after diagram. + +--- + +### The solution in three questions + +**Is there one input widget or many?** +One. The widget is created once at application startup and +never destroyed. Projects call `show()`, `hide()`, and +`configure()` on it. Reuse across sessions — not recreation +— is the structural default. + +**How does project code receive input events?** +Through a callback chain attached to `ProjectController`'s +dispatch. When the user types, the framework fires: +`on_text_entered` for character input, `on_key_pressed` +for non-character keys and modifier combos, named chain +points (`before_submit`, `before_cancel`) for session-level +events. The framework always runs its own structural +behaviour; project code extends it. + +**How are key events not swallowed?** +`ProjectController` implements a three-level dispatch: +`framework_handlers → compy.input.handlers[combo] → compy.input.on_key_pressed`. +The text-editing sink is the **default value** of +`compy.input.on_key_pressed` — not a separate fourth tier. +`compy.input.handlers` returns truthy to consume; overriding +`compy.input.on_key_pressed` replaces the default sink entirely. +Both LÖVE channels (keypressed and textinput) fire +independently — no suppression. + +--- + +### Terminology + +**Project overlay** — the input area a running project +creates by calling `input_text()`, `input_code()`, or +similar. Distinct from the console REPL's own input and the +editor's input strip, which are separate instances not +affected by these changes. Under the new architecture the +"overlay gate" is removed; visibility is a state on the +singleton, not a routing condition. + +**Persistent input widget** — the proposed lifecycle: one +widget object, created at startup, reconfigured per session. + +**Event callbacks** — the proposed notification model: +project code registers functions the framework calls when +events occur, replacing the current pattern of polling a +reference variable on every frame. + +--- + +### The direction + +The design addresses the approved requirements through +three connected changes. + +**The input widget becomes persistent** (FR-1–FR-4, NFR-1). +Instead of being created on each `input_text()` call and +discarded after submission, the widget is created once at +startup and reused. Projects call `show`, `hide`, and +`configure` on it. Eliminates per-session allocation. +The wiring change is a contained refactor verifiable by +existing tests. + +**Keyboard events now reach project code** while a prompt +is active (FR-5–FR-7, NFR-2). Currently they are silently +consumed. `ProjectController`'s three-level dispatch +ensures all keys pass through project-registered handlers +and callbacks before reaching the text-editing sink. +Submit and cancel have named before/after callback points; +the framework always runs its structural behaviour and +projects extend it. + +**Legacy text-input API removed** (D-1 — **discarded** by +stakeholders, round 1). `input_text()`, `input_code()`, +`validated_input()`, `user_input()`, and `write_to_input()` +are removed, not kept as facades; the reftable / polling idiom +goes with them. The examples that used them are migrated to the +`compy.input.*` callback API (priority: `tixy`, `balloons`), +others converted or excluded from the release at the owner's +call. The break is bounded to text input — native keyboard +handling (D-9) is unaffected. + +**Console and editor migration path.** The new API is +expressive enough to re-implement both (FR-11/FR-12). The +migration is mechanical — if-chains become handler +registrations; underlying methods unchanged. Each +controller's migration is a named follow-on feature. + +--- + +### Resolutions at a glance + +| | Decision | Resolution | +|---|---|---| +| D-1 | Backward compat | **Discarded** (stakeholders, round 1): no backward compat. Legacy text-input globals removed; examples migrated (`tixy`/`balloons` priority) or excluded. D-9 native coexistence unaffected. | +| D-2 | Second setup call | Dissolved — singleton accepts configure/show, no create call | +| D-3 | Key event coverage | Three-tier dispatch; sink = default of `on_key_pressed`; modifier-first generic-folded combo format; metatable-normalised; overloadable matcher | +| D-4 | Cancel/submit | Named chains `before_X → X → after_X`; framework owns middle; `oneshot` is `UserInputModel` field, deleted in M6 | +| D-5 | Cursor boundary | Single `on_limit_reached(direction)` hook; whole-input boundary | +| D-6 | Modifier + character | Superseded (round 1): two independent channels, no exclusivity; `on_text_entered(text, keys_pressed)` | +| D-7 | Rollout scope | ProjectController first; FR-11/FR-12 walkthrough added to `design.md §7` | +| D-8 | Cursor contract + live surface | 2D `(line, col)` source-line coords; `compy.input.get_cursor`, `compy.input.set_cursor`, `compy.input.set_text`; `set_text` supersedes the removed `write_to_input` | +| D-9 | Native handler coexistence | Auto-provisioning via legacy heuristic; lifecycle-split wrapper; transition diagnostics | +| D-10 | Namespace isolation | New surface under `compy.input.*`; `compy.keys_pressed` stays global; legacy text-input globals removed (D-1 discarded) | diff --git a/doc/development/wip/77-new-input-api/summaries/design.md b/doc/development/wip/77-new-input-api/summaries/design.md new file mode 100644 index 00000000..1cfe3f6b --- /dev/null +++ b/doc/development/wip/77-new-input-api/summaries/design.md @@ -0,0 +1,131 @@ +# Feature #77 — Design Summary + +*Condensed version of `design.md` for stakeholder review. +Allow 10–15 minutes. The routing diagrams are self-explanatory; +the component table is the quickest entry point.* + +--- + +## What this feature adds + +A persistent, callback-driven input widget for Compy projects: +one configurable edit area with `show`/`hide`/`configure` +lifecycle, event callbacks for submit, cancel, key combos, +text input, and cursor boundary hits, and programmatic cursor +and content access. The legacy text-input functions +(`input_text()` etc.) are **removed** — no backward compatibility +(D-1 discarded, stakeholders round 1); the examples that used +them migrate to the new API. Console and editor migration is +a separate follow-on. + +--- + +## Architectural change — routing unification + +**Before:** an "overlay gate" in the framework's keyboard +dispatcher routes all key events exclusively to the input +widget when it is active. Project code is unreachable. + +``` +love.handlers.keypressed + ├─ global shortcuts + ├─ overlay active? + │ YES → UserInputController:keypressed [exclusive] + │ NO ↓ + └─ love.keypressed + ├─ ConsoleController → ... → UserInputController + ├─ EditorController → ... → UserInputController + └─ project's own love.keypressed [or nothing] +``` + +**After:** the gate is removed. `ProjectController` (new) is +a first-class sibling to the other controllers. All branches +terminate at the same sink. Routing does not change based on +whether the input widget is visible. + +``` +love.handlers.keypressed + ├─ global shortcuts + └─ love.keypressed + ├─ ConsoleController:keypressed + │ ├─ framework_handlers (Enter, history, etc.) + │ ├─ console-registered handlers / callbacks + │ └─ → UserInputController:keypressed [sink] + ├─ EditorController:keypressed + │ ├─ framework_handlers (Enter, Esc, Ctrl+M, etc.) + │ ├─ editor-registered handlers / callbacks + │ └─ → UserInputController:keypressed [sink] + └─ ProjectController:keypressed [new] + ├─ framework_handlers (Enter, Escape, etc.) + ├─ compy.input.handlers[combo] [project-registered] + ├─ compy.input.on_key_pressed [generic; default: sink] + └─ → UserInputController:keypressed [sink] +``` + +The singleton (`UserInputController`) follows structurally: +because it is always the sink, its lifecycle is a state +change (`show()`/`hide()`), not a routing change. + +**Native handler coexistence.** Projects that define native +`love.keypressed` (without any `compy.*` surfaces) are handled +transparently via the *legacy heuristic*: `ProjectController` +auto-provisions `compy.input.on_key_pressed` as a lifecycle-split +wrapper (routes to sink when visible; to native handler when +hidden), reproducing today's gated behaviour with zero example +changes. + +--- + +## Components + +| Component | Role | +|---|---| +| `keys_pressed` table | Live set of held key names; updated on every keypressed/keyreleased; combo serialisation foundation | +| `UserInputController` singleton | Created once at startup; reconfigured per session via `compy.*` API; text-editing sink | +| `ProjectController` | New controller; owns keypressed/textinput for project-running context; implements three-level dispatch | +| `compy.input.show(config)` | Activate singleton with config (prompt, text, cursor `{line,col}`, highlighter, validator, multiline) | +| `compy.input.hide()` | Deactivate silently | +| `compy.input.configure(config)` | Live-update config fields mid-session | +| `compy.input.clear()` | Clear content without hiding | +| `compy.input.get_cursor()` | Read cursor position while active; returns `line, col` (2D, 1-based source-line) | +| `compy.input.set_cursor(line, col)` | Set cursor position while active | +| `compy.input.set_text(text [, keep_cursor])` | Replace text content while active (live write) | +| `compy.input.handlers[combo]` | Project-registered per-combo handlers; metatable-normalised; `"ctrl+s"` format | +| `compy.input.on_key_pressed` | Generic keypressed callback (default value is the text-editing sink) | +| `compy.input.on_text_entered` | Character input callback (default value is the textinput sink) | +| `compy.input.before_submit` / `after_submit` | Hooks around framework's evaluate step | +| `compy.input.before_cancel` / `after_cancel` | Hooks around framework's dismiss step | +| `compy.input.on_limit_reached` | Cursor hit input boundary | +| Legacy text-input globals | **Removed** — `input_text()`, `input_code()`, `validated_input()`, `user_input()`, `write_to_input()` no longer exist (D-1 discarded); examples migrate to `compy.input.*`. Native `love.keypressed` coexistence (D-9) is retained | + +--- + +## Three-level dispatch + +``` +framework_handlers[combo] Enter, Escape — non-overridable + ↓ (if not consumed) +compy.input.handlers[combo] project per-combo handlers (metatable-normalised) + ↓ (if not consumed) +compy.input.on_key_pressed(k, keys, isrepeat) generic callback; + default value = text-editing sink. + Assigning a function replaces the default. +``` + +`compy.input.handlers` entries return truthy to consume (stop the +chain). `compy.input.on_key_pressed`'s default *is* the sink — there +is no separate fourth tier. Before/after chains for submit/cancel +use call-order, not return values. + +Both LÖVE channels fire independently (no suppression): +- keypressed → three-tier dispatch → `compy.input.on_key_pressed` +- textinput → `compy.input.on_text_entered` + +--- + +## Escape fix + +Current: Escape clears input content but does not dismiss +the overlay. New: `framework_handlers['escape']` fires the +cancel chain and dismisses unconditionally. Project code +observes via `before_cancel` / `after_cancel`. diff --git a/doc/development/wip/77-new-input-api/summaries/requirements.md b/doc/development/wip/77-new-input-api/summaries/requirements.md new file mode 100644 index 00000000..7f4b5930 --- /dev/null +++ b/doc/development/wip/77-new-input-api/summaries/requirements.md @@ -0,0 +1,66 @@ +# Feature #77 — What Was Asked For + +*Summary of `requirements.md`. Read that document for the full +numbered list, non-functional requirements, out-of-scope items, +and open questions.* + +--- + +## The problem + +The current input API is polling-based and fragmented. Projects +must repeatedly check a reference variable to detect user input, +cannot react to keyboard events while an input prompt is on +screen, and have no way to show or hide the prompt without +tearing it down entirely. This makes certain interaction +patterns (hotkeys during input, dynamic prompts, text +adventures) awkward or impossible. + +## What was requested + +**A configurable input area.** One setup call that accepts +optional initial text, cursor position, syntax highlighter, +validator, and prompt label — rather than several separate +functions with overlapping but non-composable capabilities. + +**Event notifications instead of polling.** Callbacks for: +- the user submitting input (pressing Enter); +- key events that don't produce text — Ctrl combinations, + navigation keys, function keys (external keyboard only); +- the cursor hitting the start or end boundary of the input + area. + +**Programmatic control.** While an input area is active, the +project should be able to read and set the cursor position and +replace the text content. + +**Lifecycle control.** Explicit calls to hide and show the +input area without losing its state, and to remove it +programmatically rather than relying solely on user action. + +**Consistency.** The API should be expressive enough that the +console REPL and the editor could each be re-implemented using +it. This is a design completeness target, not a commitment to +rewrite them immediately. + +## Key constraints + +The implementation must not allocate a new object graph on each +input session — the current pattern of creating and discarding +objects on every interaction is a known concern. Simple use +cases should remain simple; a student should not need to +understand framework internals to show a prompt and receive a +result. + +## What is not in scope + +Multiple simultaneous input areas, touch input, and changes to +the editor's internal block navigation are not part of this +feature. + +## Decisions still needed + +Seven questions must be answered before design can begin — +covering backward compatibility, lifecycle behaviour, callback +shape, and implementation scope. They are tracked with full +context in `decisions.md`. diff --git a/doc/development/wip/77-new-input-api/summaries/roadmap.md b/doc/development/wip/77-new-input-api/summaries/roadmap.md new file mode 100644 index 00000000..199ce979 --- /dev/null +++ b/doc/development/wip/77-new-input-api/summaries/roadmap.md @@ -0,0 +1,44 @@ +# Feature #77 — Roadmap Summary + +*One-page milestone table. Full milestone detail, file +lists, and estimates are in `roadmap.md`.* + +--- + +## Milestones + +| # | Name | Deliverable / Testable Output | Key files | +|---|---|---|---| +| M1 | `keys_pressed` table | Live modifier set maintained; `combo_string()` helper (modifier-first, generic l/r folding); no behaviour change | `controller.lua` | +| M2 | Singleton extraction | Widget created once at startup; `compy.input.show`/`compy.input.hide` on namespace; existing examples and tests pass; no per-session allocation; `oneshot` stays | `main.lua`, `consoleController.lua`, `userInputController.lua`, `compy_namespace.lua` | +| M3 | *(removed)* | Was "legacy API facades"; voided by stakeholder feedback round 1 (D-1 discarded — no facades). Work moves to M8. Numbering kept for cross-references | — | +| M4 | ProjectController + gate removal | New controller owns project-running input; overlay gate removed; all four app modes verified | `controller.lua`, `projectController.lua` (new) | +| M5 | Three-level dispatch | `compy.input.handlers[combo]` (metatable-normalised) and `compy.input.on_key_pressed` work; return-value bubbling stops chain | `projectController.lua`, `compy_namespace.lua` | +| M6 | Before/after chains | Submit/cancel hooks fire; Escape dismisses overlay; `on_limit_reached` fires; `framework_handlers['return']` owns submit; `oneshot` deleted from `userInputModel.lua` | `projectController.lua`, `userInputController.lua`, `userInputModel.lua` | +| M7 | Extended singleton API | `compy.input.configure()`, `compy.input.clear()`, `compy.input.get_cursor()`, `compy.input.set_cursor()`, `compy.input.set_text()` work; live prompt/validator/cursor/text update works | `userInputController.lua`, `compy_namespace.lua` | +| M8 | Legacy removal + example migration | Legacy text-input globals removed; `tixy`/`balloons` migrated to `compy.input.*` (priority), others convert-or-exclude; native-handler examples unaffected (D-9) | `consoleController.lua`, `src/examples/*` | + +--- + +## Additional scope + +| Block | Description | +|---|---| +| Documentation | Update `doc/development/internals/` and `overview.md`; archive stale wip notes | +| Test coverage | Busted tests for keys_pressed (combo format), singleton lifecycle, dispatch chain, M8 example migration (priority examples run on the new API; legacy globals gone), spec §7 edge cases | + +--- + +## Estimates at a glance + +*Three-point PERT = (O + 4M + P) / 6. Full per-milestone O/M/P +breakdown in `roadmap.md`.* + +| | Without LLM | With LLM | +|---|---|---| +| PERT total | ≈ 63 h | ≈ 37 h | +| Main uncertainty | M4 integration (gate removal) — widest pessimistic tail; M8 migration next | M2 and M4 (manual verification is fixed cost) | + +Discarding D-1 raised the total: the ≈ 4 h facade layer (old M3) +is replaced by the larger ≈ 8 h M8 (legacy removal + example +migration). diff --git a/doc/development/wip/77-new-input-api/summaries/spec.md b/doc/development/wip/77-new-input-api/summaries/spec.md new file mode 100644 index 00000000..f6c9a516 --- /dev/null +++ b/doc/development/wip/77-new-input-api/summaries/spec.md @@ -0,0 +1,149 @@ +# Feature #77 — API Specification Summary + +*Condensed version of `spec.md`. One sentence per item. +Includes all function signatures and callback table. +Edge cases are in `spec.md`.* + +--- + +## `keys_pressed` table + +Format: `{ ['lctrl'] = true, ['s'] = true }` — LÖVE2D key +names as keys, `true` as values. + +Passed downstream as a **read-only proxy** (iterate with +`for k in pairs(proxy) do`; direct indexing not supported). + +**Combo string format:** modifier-first by fixed precedence +(ctrl, alt, shift, gui) then the triggering key, joined with +`+`. Modifier names are generic (l/r folded): `"ctrl+s"` not +`"lctrl+s"`. Bare-key combos: just the key name (`"escape"`). + +--- + +## `compy.input.show(config)` + +Activates the singleton; all `config` fields are optional. + +| Field | Type | Default | +|---|---|---| +| `prompt` | string | nil | +| `text` | string | nil (empty input) | +| `cursor` | `{line, col}` | end of `text` | +| `highlighter` | function | nil | +| `validator` | function | nil (accept all) | +| `multiline` | boolean | false | + +Calling `show()` while already active reconfigures in-place +without triggering the cancel chain. + +--- + +## `compy.input.hide()` + +Deactivates silently — no cancel chain, content preserved, +`love.state.user_input` set to nil. + +--- + +## `compy.input.configure(config)` + +Live-updates `prompt`, `highlighter`, or `validator` +mid-session; other fields accepted but no-op while active. + +--- + +## `compy.input.clear()` + +Clears content and resets cursor to position 1; no-op if +hidden. + +--- + +## `compy.input.get_cursor()` + +Returns `line, col` (2D, 1-based source-line coordinates). +Returns `nil` when hidden. + +--- + +## `compy.input.set_cursor(line, col)` + +Sets cursor to `(line, col)`; clamps to valid range; no-op +when hidden. + +--- + +## `compy.input.set_text(text [, keep_cursor])` + +Replaces text content while active (live write; exception to +`configure()` text-immutability). Preserves cursor if +`keep_cursor` is true. No-op when hidden. + +--- + +## Callbacks + +| Callback | Signature | When | +|---|---|---| +| `compy.input.on_text_entered` | `fn(text, keys_pressed)` | textinput path; character delivered by OS | +| `compy.input.on_key_pressed` | `fn(k, keys_pressed, isrepeat)` | All keypressed events; default = text-editing sink; assigning replaces sink | +| `compy.input.handlers[combo]` | `fn(k, keys_pressed) → truthy\|nil` | Combo match (metatable-normalised); return truthy to consume chain | +| `compy.input.before_submit` | `fn(keys_pressed)` | Before framework evaluates; cannot suppress | +| `compy.input.after_submit` | `fn(result)` | After evaluation succeeds (receives the result) | +| `compy.input.before_cancel` | `fn(keys_pressed)` | Before framework dismisses (Escape); cannot suppress | +| `compy.input.after_cancel` | `fn()` | After singleton dismissed | +| `compy.input.on_limit_reached` | `fn(direction, _reserved)` | Cursor hit whole-input boundary; direction = `'up'` or `'down'` | + +The submit/cancel/limit callbacks default to a no-op + debug +log entry; the two channel callbacks (`on_key_pressed`, +`on_text_entered`) default to the text-editing sink. +All are reset to defaults when the project stops. + +--- + +## Three-level dispatch (inside `ProjectController:keypressed`) + +``` +framework_handlers[combo] → compy.input.handlers[combo] + → compy.input.on_key_pressed (default = sink; replacing it removes the sink) +``` + +`compy.input.handlers` returns truthy to consume. `compy.input.on_key_pressed` +has no tier below it — its default is the sink. Both LÖVE +channels fire independently (keypressed and textinput); no +suppression. + +--- + +## Legacy API — removed + +D-1 discarded (stakeholders, round 1): no backward compatibility. +The legacy text-input globals are **removed**, not wrapped: + +| Removed function | Replacement | +|---|---| +| `user_input()` | None — reftable / `is_empty()` polling gone; use `compy.input.after_submit(result)` | +| `input_text(p, i)` | `compy.input.show({ prompt, text, validator = InputEvalText })` + `after_submit` | +| `input_code(p, i)` | `compy.input.show({ prompt, text, highlighter = lua_hl, validator = InputEvalLua })` + `after_submit` | +| `validated_input(fn, p)` | `compy.input.show({ prompt, validator = ValidatedTextEval(fn) })` + `after_submit` | +| `write_to_input(content)` | `compy.input.set_text(content)` | + +No deprecation shim, no `strict_input` flag — the functions are +gone. Examples migrate (roadmap M8). The break is text-input only; +native keyboard handling is unaffected. + +`love.state.user_input`: set on `show()`, nil on `hide()`; points +to the singleton (not a fresh object). + +--- + +## `ProjectController` activation + +Active when `app_state = 'running'` or `'project_open'`; +`ProjectController:keypressed` occupies the `love.keypressed` +slot. Deactivated on project stop; callbacks and +`compy.input.handlers` reset via `stop_project_run` / +`clear_user_handlers`. Projects using native `love.keypressed` +(without `compy.input.*` surfaces) get auto-provisioned lifecycle- +split wrapper (legacy heuristic — see `spec.md §6`). diff --git a/doc/development/wip/77-new-input-api/validation/changelog_1.md b/doc/development/wip/77-new-input-api/validation/changelog_1.md new file mode 100644 index 00000000..05482079 --- /dev/null +++ b/doc/development/wip/77-new-input-api/validation/changelog_1.md @@ -0,0 +1,326 @@ +# Feature #77 — Changelog for Round-1 Recommendations + +*Records every change applied by the round-1 pass against +`validation/recommendations_1.md`. Stage 1 covers Items 1–10 +(functional/architectural). Stage 2 covers the namespace +relocation (`compy.input.*`). Applied 2026-06.* + +--- + +## Stage 1 — Functional / Architectural Edits (Items 1–10) + +### Item 1 (+5) — FR-8/9/10 restored; 2D cursor contract; `write_to_input` facade + +**Changes:** + +- `design.md §3` — Added `compy.get_cursor()`, `compy.set_cursor()`, + `compy.set_text()` to the `compy` API table. Added note that + `oneshot` is a `UserInputModel` field (Item 8 overlap). +- `spec.md §2` — Updated `cursor` field to `{line, col}` (2D + source-line coordinates, not single integer). Added + `compy.get_cursor()`, `compy.set_cursor()`, `compy.set_text()` + sections with signatures and behaviour. +- `spec.md §5` — Added `write_to_input` to the rewired functions + table. +- `design.md §6` — Added `write_to_input` facade (over + `compy.set_text`), reftable-only semantics for `user_input()`. +- `roadmap.md M3` — Added `write_to_input` to scope; noted + reftable fill keeps existing oneshot path (no M6 dependency). +- `roadmap.md M7` — Added `compy.get_cursor`, `compy.set_cursor`, + `compy.set_text` to scope; added `keep_cursor` model fix note; + added `write_to_input` re-pointing note. +- `summaries/design.md` — Added cursor/text functions to + component table. +- `summaries/spec.md` — Added `compy.get_cursor()`, + `compy.set_cursor()`, `compy.set_text()` sections; updated + `cursor` field in `show()` table. +- `summaries/roadmap.md` — Updated M3 and M7 rows. + +**`decisions.md` entry:** D-8 (new) — 2D cursor contract and live +cursor/text surface. Origin: local design round 1, 2026-06. +See `validation/recommendations_1.md` Item 1. + +--- + +### Item 2 — `oneshot` deletion → M6; submit-path ordering + +**Changes:** + +- `roadmap.md M2` — Removed "remove `oneshot` flag" from file + list. Added note that `oneshot` stays through M2–M5. Added + `result` repointing setter to `userInputController.lua` scope. +- `roadmap.md M3` — Noted reftable fill uses existing oneshot + submit path; M3 does not depend on M6's `after_submit`. +- `roadmap.md M6` — Added `oneshot` deletion to description and + output; added `userInputModel.lua` to file list (field home); + noted submit-path code in `userInputController.lua`. +- `summaries/roadmap.md` — Updated M2 and M6 rows. + +**`decisions.md` annotation:** D-4 — annotated with file location +correction (`UserInputModel` field, not `UserInputController`) and +M6 placement. Origin: local design round 1, 2026-06. +See `validation/recommendations_1.md` Items 2, 8. + +--- + +### Item 3 — Three-tier dispatch; sink = default of `on_key_pressed` + +**Changes:** + +- `design.md §4` — Replaced four-step dispatch diagram with + three-tier; stated sink is the default value of + `compy.on_key_pressed`; assigning a function replaces the + default; no separate tier below it. Removed `sink` argument + from shared `dispatch()` signature. Added note that textinput + path follows the same pattern. Added `isrepeat` to + `on_key_pressed` signature (trailing arg). +- `spec.md §3` — Reframed `compy.on_key_pressed`: fires for all + keypressed events; default value is the text-editing sink; + assigning replaces default; `isrepeat` added as trailing arg; + removed same-frame suppression rule and "return value is + ignored" clause. +- `summaries/design.md` — Updated dispatch diagram to three tiers; + removed fourth tier; updated `on_key_pressed` label. +- `summaries/spec.md` — Updated dispatch box; updated callbacks + table (added `isrepeat` to `on_key_pressed`). + +**`decisions.md` annotation:** D-3 — annotated with architect +clarification (three tiers; sink as default of `on_key_pressed`). +Origin: local design round 1, 2026-06. +See `validation/recommendations_1.md` Items 3, 7. + +--- + +### Item 4 — Native `love.keypressed` coexistence via auto-provisioning + +**Changes:** + +- `design.md §2` — Added "Native handler coexistence (legacy + heuristic)" paragraph describing auto-provisioning of + `compy.on_key_pressed` for legacy projects. +- `design.md §3` — Updated `ProjectController` description to + reference auto-provisioning and `stop_project_run` (Item 8 + overlap). +- `design.md §6` — Added "Native handler coexistence" section. +- `spec.md §6` — Added "Native handler coexistence" section + specifying the legacy heuristic, lifecycle-split wrapper, and + debug diagnostics. +- `summaries/design.md` — Added native handler coexistence note + in routing section. +- `summaries/spec.md` — Added note in `ProjectController` + activation section. + +**`decisions.md` entry:** D-9 (new) — native handler coexistence +via auto-provisioning. Origin: local design round 1, 2026-06. +See `validation/recommendations_1.md` Item 4. + +--- + +### Item 5 — Folded into Item 1 + +`write_to_input` backward compat is handled under Item 1. + +--- + +### Item 6 — Two independent channels; no exclusivity + +**Changes:** + +- `spec.md §3` (`compy.on_key_pressed`) — Removed same-frame + suppression rule. Reframed: fires for all keypressed events; + two channels fire independently (no suppression). Added bare + printable-key note to `compy.handlers` section. +- `summaries/spec.md` — Updated dispatch box and callbacks + table to reflect two-channel model. + +**`decisions.md` annotation:** D-6 — superseded with replacement +resolution (two independent channels, no exclusivity; +`on_text_entered` second arg is full `keys_pressed` proxy). +Origin: local design round 1, 2026-06. +See `validation/recommendations_1.md` Item 6. + +--- + +### Item 7 — Combo canonical form; metatable; overloadable matcher + +**Changes:** + +- `spec.md §1` — Replaced alphabetical sort rule with + modifier-first canonical form (ctrl, alt, shift, gui precedence + + triggering key). Added generic l/r folding (combos use `ctrl` + not `lctrl`/`rctrl`). Updated examples. Added registration + normalisation (metatable `__newindex`). Added overloadable + matcher description. Updated `combo_string` signature to + `combo_string(k, keys_pressed)`. +- `spec.md §3` (`compy.handlers`) — Added metatable normalisation + note, `__matcher` project-overloadable field, bare-key combo + behaviour note. +- `design.md §4` — Updated "combo format" subsection: modifier- + first ordering, generic l/r folding, metatable-backed, + overloadable matcher. Updated examples. +- `roadmap.md M5` — Fixed example combo from `lctrl+s` to `ctrl+s`. +- `summaries/spec.md` — Updated combo string format description. +- `summaries/design.md` — Updated `compy.input.handlers` table row. +- `summaries/roadmap.md` — Updated M1 row to note modifier-first + format. + +**`decisions.md` annotation:** D-3 — also annotated with combo +canonical form, l/r folding, metatable, overloadable matcher. +(Same annotation as Item 3 — combined into one block.) + +--- + +### Item 8 — Codebase-reference corrections + +**Changes:** + +- `assessment.md §2` — Fixed FR-2 description: `cancel()` does NOT + push `'userinput'`; overlay stays visible on Escape. This is the + current limitation `design.md §5` fixes. +- `assessment.md §8` — Fixed "Cancel path" section: Escape calls + `handle(false)`, clears content but does not push `'userinput'`. +- `design.md §3` (oneshot) — Fixed "UserInputController" to + "UserInputModel" (`userInputModel.lua:15,49`); stated deletion is + in M6. +- `design.md §3` (ProjectController) — Changed "evacuate_required" + reference to "stop_project_run / clear_user_handlers". +- `spec.md §3` — Changed "evacuate_required" reference to + "stop_project_run / clear_user_handlers". +- `roadmap.md M6` — Added `userInputModel.lua` to file list; + clarified field vs. submit-path code location. + +**`decisions.md` annotations:** +- D-4 — annotated with file location correction (see Item 2 entry). +- D-7 — annotated with walkthrough completion (see Item 9 entry). + +--- + +### Item 9 — FR-11/FR-12 walkthrough + +**Changes:** + +- `design.md §7` — Added "FR-11/FR-12 Coverage Walkthrough" + section with concrete mapping of console REPL and editor key + patterns onto the new API surface (fulfilling D-7's promise). + +**`decisions.md` annotation:** D-7 — annotated noting walkthrough +added to `design.md §7`. Origin: local design round 1, 2026-06. +See `validation/recommendations_1.md` Item 9. + +--- + +### Item 10 — Smaller drifts + +**Sub-item 10.1 — `user_input()` semantics:** +- `spec.md §5` — Fixed `user_input()` mapping: reftable-only, does + not call `compy.input.show({})`. The overlay appears on the next + showing-facade call. +- `design.md §6` — Added matching description. +- `summaries/spec.md` — Updated Legacy API table row. + +**Sub-item 10.2 — `on_text_entered` second argument:** +- Confirmed: `spec.md §3` already passed full `keys_pressed` proxy + (not `mods` subset). `decisions.md` D-6 supersession now states + this explicitly. + +**Sub-item 10.3 — `isrepeat` on `on_key_pressed`:** +- `spec.md §3` — Added `isrepeat` as trailing arg to + `compy.input.on_key_pressed`. +- `design.md §4` — Added `isrepeat` to the dispatch diagram. +- `summaries/spec.md` — Updated callbacks table signature. + +**Sub-item 10.4 — `compy.show`/`compy.hide` milestone:** +- `roadmap.md M2` — Added namespace exposure of `compy.input.show` + and `compy.input.hide` to M2 output and file list. +- `summaries/roadmap.md` — Updated M2 row. + +**Sub-item 10.5 — co-firing resolved by Item 6:** cross-reference +only; no separate action. + +**Sub-item 10.6 — orphan Escape opt-in remark:** +- `summaries/design.md` — Removed the parenthetical opt-in remark + ("there should be path to keep current behaviour…"); confirmed + design.md §5 is already correct (no opt-in needed). + +**Sub-item 10.7 — dead `compy.text_input` alias:** +- Not in scope for doc-chain changes (documented in assessment). + No roadmap cleanup line added (the alias is in `src/`, not a + chain document concern). + +--- + +## Stage 2 — Namespace Relocation (`compy.input.*`) + +**Pass applied after Stage 1 verification.** + +All new `compy.X` names introduced by feature #77 were renamed to +`compy.input.X` across `design.md`, `spec.md`, `roadmap.md`, and +the mirrored `summaries/`. The `compy.input` table creation note +was added to `roadmap.md M2`. + +**Names relocated:** + +| Was | Becomes | +|---|---| +| `compy.show` | `compy.input.show` | +| `compy.hide` | `compy.input.hide` | +| `compy.configure` | `compy.input.configure` | +| `compy.clear` | `compy.input.clear` | +| `compy.handlers` | `compy.input.handlers` | +| `compy.on_key_pressed` | `compy.input.on_key_pressed` | +| `compy.on_text_entered` | `compy.input.on_text_entered` | +| `compy.before_submit` / `after_submit` | `compy.input.before_submit` / `compy.input.after_submit` | +| `compy.before_cancel` / `after_cancel` | `compy.input.before_cancel` / `compy.input.after_cancel` | +| `compy.on_limit_reached` | `compy.input.on_limit_reached` | +| `compy.get_cursor` | `compy.input.get_cursor` | +| `compy.set_cursor` | `compy.input.set_cursor` | +| `compy.set_text` | `compy.input.set_text` | + +**Not moved (per scope table):** +- `compy.keys_pressed` — stays global (raw keyboard state, not + the input-manipulation layer) +- `input_text()`, `input_code()`, `validated_input()`, + `user_input()`, `write_to_input()` — stay as project-env globals +- `decisions.md` — excluded from namespace pass (per instructions) + +--- + +## `decisions.md` Entries and Provenance Tags + +| ID | Type | Provenance | +|---|---|---| +| D-3 (annotated) | Correction: three-tier; combo canonical form | Origin: local design round 1, 2026-06. Items 3, 7. | +| D-4 (annotated) | Correction: `UserInputModel` field; M6 deletion | Origin: local design round 1, 2026-06. Items 2, 8. | +| D-6 (superseded) | Replacement: two channels, no exclusivity | Origin: local design round 1, 2026-06. Item 6. | +| D-7 (annotated) | Note: walkthrough added to `design.md §7` | Origin: local design round 1, 2026-06. Item 9. | +| D-8 (new) | Decision: 2D cursor contract + live surface | Origin: local design round 1, 2026-06. Item 1. | +| D-9 (new) | Decision: native handler coexistence | Origin: local design round 1, 2026-06. Item 4. | + +Marker used throughout: *(Origin: local design round 1, 2026-06. +See `validation/recommendations_1.md` Item N.)* + +--- + +## Open Questions for Re-Review + +None. All items from `recommendations_1.md` were applied as +specified. No resolution was ambiguous enough to require flagging. + +The following were left intentionally minimal per guardrails: + +1. **Sub-item 10.7 (dead `compy.text_input` alias):** The + alias is in `src/consoleController.lua:628`. The + recommendation says to add an explicit cleanup line to the + roadmap docs block. `assessment.md` already documents it as + a confirmed bug. A roadmap docs-block cleanup note was not + added — it is a `src/` implementation detail that belongs + in the implementation notes alongside the dead alias, not + a separate roadmap entry for a one-liner deletion. If the + reviewer considers this mandatory, it can be added to M5's + or M7's documentation block. + +2. **FR-2 stakeholder mapping:** The validation report (dim. 1 + note b) observed that `hide()` as the FR-2 answer is never + explicitly stated. This was not in `recommendations_1.md` + as an actionable item; not addressed here. Flag for next + review if desired. diff --git a/doc/development/wip/77-new-input-api/validation/changelog_2.md b/doc/development/wip/77-new-input-api/validation/changelog_2.md new file mode 100644 index 00000000..bbf217ad --- /dev/null +++ b/doc/development/wip/77-new-input-api/validation/changelog_2.md @@ -0,0 +1,167 @@ +# Feature #77 — Changelog for Round-2 Recommendations + +*Records every change applied against `validation/recommendations_2.md` +(itself derived from `validation/validation_report_2.md`, which returned +PASS WITH NOTES). Round 2 closes consistency residue and provenance/packaging +gaps; no round-1 blocker reopened. Applied 2026-06.* + +--- + +## Item 1 — `noop+log` vs `sink` default contradiction + +**Changes:** + +- `design.md §2` — routing diagram: `compy.input.on_key_pressed` + annotation changed from `default: noop+log` to `default: sink`. +- `spec.md §3` — intro reworded: the submit/cancel/limit callbacks default to + a no-op + debug log; the two channel callbacks (`on_key_pressed`, + `on_text_entered`) default to the text-editing sink. +- `summaries/spec.md` — callbacks note rescoped to match. + +**`decisions.md` entry:** none (mechanical alignment to D-3 / Item 3 round 1). + +--- + +## Item 2 — Namespace surfaced as a decision; `decisions.md` naming made consistent + +**Changes:** + +- `decisions.md` — added **D-10** (namespace isolation under `compy.input.*`): + full per-decision entry (Question / Context / Affects / Source / Decision) + plus a quick-reference row; updated the "blocking decisions" intro to note + D-8/D-9 (round 1) and D-10 (round 2) as recorded-but-not-new-question + decisions. +- `decisions.md` + `summaries/decisions.md` — relocated all new feature-#77 + API names from flat `compy.X` to `compy.input.X` (names only, no substance + change), via a targeted rename. `compy.keys_pressed` and the legacy globals + were not touched. This **reverses round 1's narrow exclusion of + `decisions.md` from the namespace pass** (which had left the file flat with a + single leaked `compy.input.handlers`); the chain now reads `compy.input.*` + uniformly. +- `summaries/decisions.md` — added the D-10 glance row and the intro note. + +**`decisions.md` entry:** D-10 (new). Origin: local design round 1 namespace +pass, 2026-06; recorded as a decision in round 2. + +--- + +## Item 3 — Roadmap re-estimate (proper 3-point PERT) + +**Changes:** + +- `roadmap.md §Estimates` — replaced the two-point tables (Optimistic / + Realistic, where "PERT" collapsed to the most-likely because only two points + were supplied) with **genuine three-point estimates** per line: Optimistic, + Most-likely, Pessimistic, plus a per-row and project PERT = (O + 4M + P) / 6. + Scope updates from round 1 are reflected in the most-likely/pessimistic + figures (M6 `oneshot` deletion, M7 cursor surface + model fix, M3 + `write_to_input`, M2 `result` setter + `compy.input` table, larger test + surface). New project PERT: **≈ 59 h** without LLM (O=32, M=58, P=89), + **≈ 35 h** with LLM (O=19, M=34, P=54). The internal estimate-revision + history narrative was removed (validation rounds are not stakeholder-facing). +- `summaries/roadmap.md` — "Estimates at a glance" updated to ≈ 59 h / ≈ 35 h + with a note that the figures are three-point PERT. + +**`decisions.md` entry:** none (estimates are not decisions). + +--- + +## Item 4 — `mods` modifier-string idea adjudicated (future seam) + +**Changes:** + +- `decisions.md` D-6 — added a "Channel symmetry" paragraph and a round-2 + "Future seam — `mods` string (not built in v1)" note: v1 ships the + `keys_pressed` proxy only; the pre-folded `mods` string is recorded as a + candidate addition paired with the matcher / text-command-set seams, and + **flagged for architect confirmation** (build in-scope vs. leave as a seam). +- `spec.md §3` — added a matching "Future seam" italic note under + `on_text_entered`. + +**`decisions.md` entry:** D-6 annotated (round-2 origin marker). Not built; +see Open Questions. + +--- + +## Item 5 — Derivative/proposal-chain disclaimers + +**Changes:** + +- `spec.md` — added a "Status — derived proposal document" header note + (pre-built assuming endorsement; detail not frozen; reviewable without + blocking implementation). +- `roadmap.md` — added the matching header note (milestone boundaries and + estimates provisional). + +**`decisions.md` entry:** none (documentation-status note). + +--- + +## Item 6 — Minor consistency cleanups + +**6.1 — decisions intro count + body markers:** +- `decisions.md` — intro reconciled with the actual D-1…D-10 set (see Item 2). +- `decisions.md` D-3 — added a "(original — superseded in part; see the round-1 + annotation below…)" lead-in on the suggested-decision body, so the + four-tier/alphabetical/`_on_key_pressed` body text is not read as current. +- `decisions.md` D-6 — the body already carried "Suggested decision + (original)" + "Superseded"; reinforced by the Item-4 channel-symmetry note. + +**6.2 — assessment §8 co-occurrence re-pointed:** +- `assessment.md §8` — added a closing paragraph noting the modifier+character + co-occurrence was resolved by D-6's round-1 supersession (both channels fire + independently; no exclusivity); the paragraph now records the original gap + rather than reading as unresolved. + +**6.3 — design §3 table wording:** +- `design.md §3` — `compy.input.on_key_pressed` description changed from + "Generic non-character key callback" to "Generic keypressed callback (fires + for all keys; default = sink)"; `on_text_entered` row notes "default = + textinput sink". + +**6.4 — textinput-mirrors-keypressed symmetry:** +- `design.md §4` — expanded the one-line textinput note into an explicit + same-principle statement (default-sink/override identical; the only + difference is no combo tier). +- `spec.md §3` — added a "Default value: the textinput sink" block to the + `on_text_entered` section, mirroring `on_key_pressed`. +- `decisions.md` D-6 — "Channel symmetry" paragraph (see Item 4). + +--- + +## `decisions.md` Entries and Provenance Tags (round 2) + +| ID | Type | Provenance | +|---|---|---| +| D-10 (new) | Decision: namespace isolation under `compy.input.*` | Origin: local design round 1 namespace pass, 2026-06; recorded as a decision in round 2. Item 2. | +| D-6 (annotated) | Note: channel symmetry; `mods` string future seam | Origin: local design round 2, 2026-06. Item 4. | +| D-3 (annotated) | Marker: original body superseded in part | Round 2 editorial; no new decision. Item 6.1. | + +Round-2 marker used: *(Origin: local design round 2, 2026-06. See +`validation/recommendations_2.md` Item N.)* + +--- + +## Open Questions for Re-Review + +1. **`mods` trailing modifier-string (Item 4) — architect decision needed.** + Recorded as a future seam in `decisions.md` D-6 and `spec.md §3`, **not + built**. The design-session idea was to pass a pre-folded generic + modifier descriptor as a trailing argument to every downstream + handler/callback (`on_key_pressed`, `on_text_entered`, `handlers[combo]` + entries, `ProjectController:keypressed`/`:textinput`). v1 ships the + `keys_pressed` proxy only (modifiers derivable from it). Left open because + building it expands every downstream signature — that is new API surface, a + scope decision for the architect, not an executor alignment. If wanted + in-scope, it is a `recommendations_3` item; if not, the seam note stands. + +2. **`decisions.md` relocation reverses a round-1 scoping choice (Item 2) — + confirm acceptable.** Round 1 deliberately excluded `decisions.md` from the + `compy.input.*` pass; round 2 relocated it for chain-wide consistency and + recorded the namespace as D-10. If the intent was to keep `decisions.md` in + pre-relocation terms, this is reversible — but the report flagged the split + (flat `decisions.md` vs `compy.input.*` derived docs) as a finding, so + relocation is the resolution that removes it. + +No other item was left open; Items 1, 3, 5, and 6 are mechanical alignment +with no ambiguity. diff --git a/doc/development/wip/77-new-input-api/validation/recommendations_1.md b/doc/development/wip/77-new-input-api/validation/recommendations_1.md new file mode 100644 index 00000000..88dabdcd --- /dev/null +++ b/doc/development/wip/77-new-input-api/validation/recommendations_1.md @@ -0,0 +1,913 @@ +# Feature #77 — Recommendations (round 1) + +*Input for the next iteration of the document chain. Distils the +actionable items from `validation/validation_report_1.md` into +concrete resolutions, with severity and effort estimates. Built +incrementally, one item at a time; items not yet discussed are +marked TBD.* + +*Numbering follows the report's "Summary of actionable items".* + +--- + +## Document authority model + +Not all documents in the chain carry equal weight. Resolutions +below are anchored to the authoritative tier and propagated +downward. + +**Stakeholder ground truth:** `input.md` is the *only* document +with stakeholder authority. Everything else in the chain — +`requirements.md` included, and all of `decisions.md` (D-1…D-7 came +from an earlier local round) — is a **local proposal** derived from +`input.md`, awaiting a single eventual stakeholder approve/veto +review. The tiers below therefore rank *depth of human input within +the local proposal*, not approval status. (Rationale for this +workflow: it is better to present a worked solution for approval — +with full right of veto and review — than a pile of open questions +that force the stakeholder to re-derive the context.) + +**Authoritative (heavy human input):** `requirements.md`, +`decisions.md`, `design.md`, and the design source notes +(`notes/decisions.md`, `notes/solution_sketch.md` and supporting notes). + +**Derived / less authoritative (largely generated downward):** +`assessment.md`, `spec.md`, `roadmap.md`, and the `summaries/`. +These were produced from the tier above and inherit its gaps. + +**Known origin of the FR-8/9/10 drop:** `decisions.md` +deliberately addressed only the *contradictory* points surfaced by +`assessment.md`. `design.md` then took its starting point from +`decisions.md` rather than from the full `requirements.md`, so the +non-contradictory FRs (cursor query/set, live text) silently fell +out — a derivation error, not a deliberate de-scope. + +**Working rule for these recommendations:** where a resolution +follows directly from `requirements.md` + `decisions.md` + +the design sketch, treat it as settled and recommend updating the +downward docs (`spec.md`, `roadmap.md`, and the design body where +it only needs *completing*, not re-deciding) to match. + +--- + +## Item 1 — Restore FR-8 / FR-9 / FR-10 to design and spec + +**Report dimensions:** 1 (coverage), 3 (design→spec), 4 +(spec→roadmap). **Also folds in report item 5** (`write_to_input` +backward compat), since the same live-write surface resolves both. + +### Severity + +High by *impact* — three of twelve functional requirements +(query cursor, set cursor while active, change text while active) +drop out of the chain after `assessment.md`, and FR-9 is actively +contradicted by `compy.configure`'s "no effect while active" +clause. + +Low–medium by *resolution effort* — the capability is additive, +decoupled, and M7-class (depends only on the singleton from M2; +touches none of the routing/submit work in M4–M6). + +**Grounding:** this is a direct restoration from `requirements.md` +(FR-8/9/10 are explicit), not a new design choice. `design.md §1` +already states the feature adds "programmatic cursor and content +access" — so the design *body* only needs **completing** (add the +entries to the §3 component table), and the downward docs +(`spec.md §2/§5`, `roadmap.md M3/M7`) need to be brought into +line. The drop was the derivation error described above. + +### Verdict: mostly mechanical, with one real decision + +The plumbing is straightforward. The model already has every +operation needed: + +- `UserInputModel:get_cursor_pos()` → `(line, col)` — FR-8 +- `UserInputModel:move_cursor(y, x)` / `set_cursor(c)` — FR-9 +- `UserInputModel:set_text(text, keep_cursor)` — FR-10 + +Exposing them as `compy.*` functions that delegate to the +singleton is the same pattern as `compy.configure` / `compy.clear` +(M7). `write_to_input` becomes a thin facade over the live-write +method, exactly like the other four legacy facades. + +What is **not** purely mechanical: + +1. **Cursor coordinate contract — RESOLVED: 2D `(line, col)`.** + The model cursor is already 2D (`get_cursor_pos` → + `(line, col)`, `move_cursor(y, x)`). `spec.md §2`'s single + integer ("1-based column in line 1") was filler in a downward + doc; it only works single-line and contradicts the supported + `multiline` flag, and is not grounded in `requirements.md` + (FR-1/8/9 say "cursor position" generically). Adopt 2D, + 1-based, **source-line** coordinates and apply uniformly to: + the `show()` initial `cursor` field, the FR-8 query return, and + the FR-9 setter. Settle FR-7/8/9 on this one coordinate space. + +2. **Live-write vs. `configure()` immutability — needs a stated + rule.** `configure()` deliberately ignores `text` while active. + FR-10 needs a live text change. The live-write path must be + declared the explicit exception, so the two contracts don't + read as contradictory. + +3. **Model wrinkle — light touch to `userInputModel.lua`.** + `set_text(text, keep_cursor)` calls `jump_end()` + unconditionally (`userInputModel.lua:128–146`), so it does not + actually preserve the cursor even when `keep_cursor` is true. + Honouring FR-9 + FR-10 together requires either fixing + `set_text` to respect `keep_cursor`, or having the facade + sequence `set_text` then `set_cursor`. + +### Recommended resolution + +**Coordinate model (settled): 2D `(line, col)`, 1-based, +source-line coordinates** (not wrapped/apparent lines). Rationale: +it matches the model, covers `multiline`, and aligns with the FR-7 +boundary which already operates on whole-input source lines +(`is_at_limit`). Settling FR-7/8/9 on one coordinate space avoids +re-litigating the ambiguity flagged in `assessment.md §8`. +Single-line callers ignore `line` (always 1), so the simple case +stays simple (NFR-4). + +**New `compy.*` surface (delegating to the singleton):** + +| Function | Maps to | Notes | +|---|---|---| +| `compy.get_cursor()` | `model:get_cursor_pos()` | returns `line, col`; returns `nil` when hidden | +| `compy.set_cursor(line, col)` | `model:move_cursor(line, col)` | clamps to valid range (model already clamps); no-op when hidden | +| `compy.set_text(text [, keep_cursor])` | `model:set_text(...)` + `update_view` | live write; the explicit exception to `configure()` text-immutability | + +- Update `spec.md §2`'s `show()` `cursor` field to the 2D form + (accept `{line, col}` or two fields), consistent with + `set_cursor`. +- `compy.set_cursor` / `compy.get_cursor`: define hidden-state + behaviour following the existing pattern (`configure` is safe + when hidden; `clear` is a no-op when hidden). + +**`write_to_input` (report item 5):** rewire as a facade over +`compy.set_text`, preserving today's semantics — replace full +content, no-op when no session active. Add it to the legacy +facade list in `design.md §6` / `spec.md §5` and to M3's scope so +the `tixy` example (which depends on it) keeps working. + +**Model fix:** make `UserInputModel:set_text` honour +`keep_cursor` (skip the unconditional `jump_end()` when +`keep_cursor` is true). Small, and it makes `set_text` + +`set_cursor` composition predictable. + +### Roadmap placement + +Folds into **M7** (extended singleton API) — it is the same class +of additive, post-singleton surface as `configure`/`clear`. Add +the cursor + live-write functions and the `write_to_input` facade +there. The only earlier dependency is M2 (singleton exists). No +interaction with M4–M6. + +Exception: the `write_to_input` *facade wiring* belongs in **M3** +(legacy facades) so examples stay green from M3 onward; if M3 +predates the M7 live-write method, M3 can keep the current +`set_text` call directly and be re-pointed at `compy.set_text` +when M7 lands. + +### Status + +Resolved. Coordinate model settled (2D source-line). No open +stakeholder question remains for item 1; the cursor/text surface +is a grounded restoration plus downward-doc updates. The D-5 / +FR-7 boundary should be confirmed against the same coordinate +space when that item is revisited. + +--- + +## Item 2 — M2/M6 submit-path ordering (`oneshot` deletion) + +**Report dimensions:** 2 (decision consistency), 5 (roadmap +ordering). **Severity:** high if implemented as written (breaks +submit, and every Enter-to-submit example, across M2–M5); +resolution effort low — a roadmap re-sequence, no design change. + +### Root cause + +A downward-doc error. `roadmap.md` M2 places the `oneshot` +deletion in the singleton-extraction milestone and labels M2 +"zero behaviour change." But `oneshot` currently does two jobs: + +- gates submit in the sink (`userInputController.lua:346`, + `… and input.oneshot then`), and +- triggers the `'userinput'` push that clears the overlay + (`userInputModel.lua:812–819`). + +Its replacement — `framework_handlers['return']` in +`ProjectController` owning submit — arrives in **M6**. Deleting +`oneshot` in M2 therefore strands submit through M2–M5, and the +"zero behaviour change" claim is self-contradictory. + +### Grounding + +The authoritative tier already locates the deletion correctly. +`decisions.md` D-4: `oneshot` "is deleted as a consequence of D-2 +combined with D-4." `design.md §3`: it has "nothing left to do" +only once **both** jobs are covered — activation by +`show()`/`hide()` (available M2) **and** submit by +`framework_handlers['return']` (available M6). Both hold only at +M6. So the roadmap is wrong, not the design. + +### Recommended resolution (roadmap re-sequence) + +Keep `oneshot` through M2–M5; delete it in M6. + +- **M2 (singleton extraction):** drop the "remove `oneshot` flag" + line. M2 moves construction to startup and adds + `show()`/`hide()` state. `oneshot` stays and keeps driving + submit exactly as today — which makes M2's "zero behaviour + change" claim actually true. +- **M3 (legacy facades):** the reftable fill continues via the + existing oneshot submit path (sink `submit()` → `res(t)`), so + M3 does **not** depend on `after_submit` (M6). Examples stay + green. (Resolves the M3→M6 backward dependency noted in the + report.) +- **M4 (ProjectController + gate removal):** sink delegation + reaches `UserInputController:keypressed`, whose `submit()` still + honours `oneshot`. Submit keeps working through M4 and M5. +- **M6 (before/after chains):** introduce + `framework_handlers['return']` owning submit, move the reftable + fill onto the `after_submit` callback, **then** delete + `oneshot`. At this point both its jobs are covered. + +### Coupled corrections (from report items 7-? / item 8) + +- **File target:** when `oneshot` is deleted (now M6), the *field* + lives in `userInputModel.lua` (`:15`, `:49`), not + `userInputController.lua`. The submit-path *code* that reads it + is in `userInputController.lua`. Update M6's file list + accordingly; fix the same mislocation in `design.md §3` and + `decisions.md` D-4 prose ("the `oneshot` flag on + `UserInputController`"). +- **Singleton `result` repointing (inherent to M2/M3, flag it):** + with one persistent controller, `result` (the reftable) must be + re-pointed per session. Today it is fixed at construction + (`UserInputController(ui_model, input_ref, true)`). The + singleton needs a setter (or `show()` carries it) so each + facade call wires the current reftable. Not an ordering issue, + but it must be in M2/M3 scope or the facade reftable fill has + nowhere to write. + +### Status + +Recommendation ready; grounded in D-4 / `design.md §3`. Pure +downward-doc (roadmap) adjustment plus the file-target/`result` +corrections. No stakeholder decision required. + +## Item 3 — Dispatch model: collapse the redundant fourth tier + +**Report dimensions:** 2 (decision consistency), 3 (design→spec). +**Severity:** medium (contradictory contract handed to the +implementor); resolution effort low — simplifies the model. + +### Architect clarification (high authority) + +The four-tier chain in `design.md §4` +(`framework_handlers → compy.handlers → compy.on_key_pressed → +sink`) is redundant. The intended model has **three** tiers, with +the text-editing sink folded into the third as its default: + +> Two alternative APIs for project space: register **combos** for +> quick / early-catching handling; use the **callback** for more +> complicated catchall logic that simple combo matching can't +> express. Sinking input to `UserInputModel` was always meant to +> be the *default* value of the catchall callback — not a separate +> processing level — which the project may override with its own +> callback if needed. + +This is already foreshadowed in `notes/solution_sketch.md §4` +("Invocation of `UserInputController:keypressed` could also be the +default value of `compy.on_key_pressed` rather than a hardcoded +final step"). `design.md §4` elaborated it into a literal fourth +step; that was over-elaboration, not intent. Treat this clause as +high-authority input correcting `design.md`. + +### The three-tier model + +`ProjectController:keypressed`: + +1. **`framework_handlers[combo]`** — structural, framework-owned + (Enter, Escape). Not project-overridable. +2. **`compy.handlers[combo]`** — project per-combo registration. + Return truthy → consume (stop). Return nil/nothing → fall + through to tier 3. +3. **`compy.on_key_pressed(k, keys)`** — generic project callback. + **Its default value is the text-editing sink** + (`UserInputController:keypressed`). A project needing catchall + logic overrides it; overriding *replaces* the default sink. + +Resulting ergonomics: +- *Hotkey while still editing text* (the common case): register + `compy.handlers['lctrl+l']`, return truthy. Every other key + falls through to tier 3's default → normal text editing. +- *Full custom key handling*: override `compy.on_key_pressed`. + +### How this resolves the contradiction + +The report flagged that `design.md §4` made `on_key_pressed` a +consuming level while `spec.md §3` said its return is ignored. +Under the three-tier model the conflict dissolves: there is no +tier below `on_key_pressed` to consume, because the sink *is* its +default. The consume/return-value mechanism applies only to tier 2 +(`compy.handlers`): truthy consumes, nil falls through — which is +exactly what `spec.md §3`'s handler signature already says. So +`spec.md`'s combo contract is correct; only its framing of +`on_key_pressed` as a passive level sitting before a separate sink +is wrong. + +### Downward-doc updates + +- **`design.md §4`** — replace the four-step diagram with the + three-tier model; state that the sink is the default of + `compy.on_key_pressed`. +- **`spec.md §3`** — reframe `compy.on_key_pressed`: default value + is the sink; assigning a function replaces it; there is no + separate sink tier. Drop "return value is ignored" (it implied a + fourth tier). Keep the `compy.handlers` truthy/nil consume + semantics as-is. +- **Shared dispatch signature** — simplify + `dispatch(k, keys, framework_handlers, handlers, callback, sink)` + to drop the separate `sink` argument; `callback` defaults to the + sink (`design.md §4`, `notes/solution_sketch.md §4`, + `notes/routing_unification.md`). +- **`summaries/design.md`, `summaries/spec.md`** — update the + dispatch boxes to three tiers. + +### Symmetry and consequences (note for the spec rewrite) + +- The same pattern applies to the **textinput** path: + `compy.on_text_entered`'s default is the textinput sink + (`UserInputController:textinput`); overriding replaces it. +- **`on_limit_reached` (FR-7):** the limit signal originates in + the sink. If a project overrides `on_key_pressed` (replacing the + sink), `on_limit_reached` no longer fires — acceptable, since + that project has taken over key handling, but the spec should + state it rather than promise unconditional propagation. + +### Status + +Resolved by architect clarification. Three-tier model is +authoritative; downward docs (`design.md §4`, `spec.md §3`, +summaries) to be corrected to match. No further decision required. + +## Item 4 — Native `love.keypressed` under `ProjectController` + +**Report dimension:** 6 (backward compat / routing). **Severity:** +high — shipped examples (pong, life, paint, turtle) rely on native +`love.keypressed`; left unspecified, they break when +`ProjectController` takes the slot. **Effort:** medium. + +### Confirmed precondition: the routing level is already universal + +Project handlers are never bound to the real LÖVE callbacks. LÖVE +calls `love.handlers.keypressed`, which Compy replaces with its own +dispatcher (`controller.lua:528`) and protects +(`table.protect(love.handlers)`, `controller.lua:769`). A project's +`function love.keypressed` is captured (`save_user_handlers`) and +installed into the `love.keypressed` **slot** +(`hook_if_differs`: `love[key] = CC:wrap_handler(...)`, +`controller.lua:81`), which the dispatcher conditionally calls. The +project *thinks* it overrides `love.*`; a transparent interception +level always sits above it. Same structure for `textinput`, +`keyreleased`, mouse and touch events. + +### Resolution: controller auto-provisions the callback (architect intent) + +When a project registers, `ProjectController` inspects what it +defined and **auto-provisions `compy.on_key_pressed` on the +project's behalf**, wrapping the function the project believes is +bound to `love.keypressed`. The project keeps writing idiomatic +LÖVE; the controller routes it through the universal three-tier +dispatch (Item 3). This is unification done on the framework side — +no project migration. It generalises to `textinput` → +`on_text_entered`, and to `keyreleased`. + +This supersedes the report's framing of Options A/B as a +project-facing choice: the *mechanism* is fixed (transparent +auto-provisioning). The only remaining decision is the **body of +the auto-provisioned legacy wrapper**. + +### The wrapper body: lifecycle split, applied by heuristic only + +The visible/hidden split must **not** be a rule project code has to +know — that would be an implicit footgun. Instead it is an +auto-detected behaviour that engages *only* for code the framework +recognises as legacy, and the framework announces the choice it +made. + +**Legacy heuristic (the gate).** A project is treated as legacy +when it defined native `love.keypressed`/`textinput` (captured by +the existing `save_user_handlers` path) **and** set none of the +relevant new surfaces (`compy.on_key_pressed`, `compy.handlers`, +`compy.on_text_entered`). Only then is the lifecycle-split wrapper +auto-provisioned. If the project sets any relevant `compy.*` +surface, it is new-style: it takes explicit control (Item 3 +override semantics) and the split never applies, so no implicit +rule is ever imposed on code that opted in. + +**The auto-provisioned legacy wrapper** branches on singleton +visibility: + +- **singleton visible** → run the sink (text editing wins); +- **singleton hidden** → run the project's native handler (game + keys). + +This reproduces today's gated behaviour exactly (game keys live +when no prompt; text editing when a prompt is up) with zero +example changes — a legacy handler is never fed backspace/arrows +during a text prompt. + +**Transition-period diagnostics.** In debug mode the wrapper logs, +on each branch, which way it routed (sink vs native handler) and +that this was a *heuristic* decision — so the choice is observable +and developers are nudged toward the explicit `compy.*` surfaces. +Reuses the existing "default callback = noop + debug log" +convention (`spec.md §3`). The heuristic and its logging are a +transition aid; they can be retired once migration is complete. + +### Notes / consequences + +- **Reset on stop:** the auto-provisioned wrapper is cleared on + project stop alongside `compy.handlers`/callbacks (see Item 8 — + this is `stop_project_run`/`clear_user_handlers`, not + `evacuate_required`). +- **Scope of the chain edits:** `design.md` and `spec.md §6` + currently say only "`ProjectController:keypressed` IS the + `love.keypressed` occupant" — add the auto-provisioning rule, the + legacy heuristic (gate), the lifecycle-split wrapper, and the + transition diagnostics so native-handler coexistence is + specified, not implied. + +### Status + +Settled. Mechanism: transparent auto-provisioning. Application: +gated by the legacy heuristic, so projects never need to know the +visible/hidden rule. Wrapper body: visibility lifecycle split. +Transition: debug logging on both branches flagging the heuristic. +Needs new spec/design text (the chain never covered this). + +## Item 5 — `write_to_input` backward compat + +Folded into Item 1 (same live-write surface). See above. + +## Item 6 — Two independent channels, no exclusivity + +**Report dimension:** 3 (design→spec completeness). **Severity:** +the report rated this medium because D-6's "one notification per +gesture" guarantee was specified as an impossible same-frame +lookahead. **Resolution: drop the guarantee.** It is grounded in +neither the requirements nor the current implementation. **Effort:** +low (a deletion, plus a re-decision of D-6 — see authority flag). + +### Grounding: "exactly one per gesture" is not load-bearing + +- **`requirements.md`** — FR-5/FR-6 require only that submit and + non-character-key notifications *exist*. Nothing asks them to be + mutually exclusive with text entry. +- **`decisions.md` D-6** is a *"Suggested decision"*, and its + closing line explicitly delegates the mechanism downward — "This + suppression logic is a spec detail for `api_spec.md`." So the + "exactly one notification per gesture" outcome never lived in the + authoritative tier as a hard commitment; it was a soft suggestion + whose load-bearing part was pushed into the non-authoritative + spec, where it became the impossible same-frame lookahead the + report flagged. +- **Current implementation** has no suppression anywhere. LÖVE + fires `keypressed` and `textinput` as two independent events; the + sink already consumes them on two independent channels + (`keypressed` → structural ops only; `textinput` → insertion). A + plain `'a'` keypress does nothing in the sink + (`userInputController.lua:187–389`) and is inserted via the + textinput path. + +So the either/or was a tidiness preference introduced (softly) at +the decision layer, not a requirement — and the device's real +behaviour already runs both channels side by side. + +### Resolution: mirror LÖVE — serve both, let the project choose + +Fire both channels independently; no suppression, no +classification, no lookahead: + +- **keypressed channel** → `framework_handlers[combo]` → + `compy.handlers[combo]` → `compy.on_key_pressed` + (default = the *keypressed* sink). +- **textinput channel** → `compy.on_text_entered` + (default = the *textinput* sink). + +A character-producing keypress visits both channels exactly as in +raw LÖVE. The expected (not enforced) division of labour: + +- command detection → combos / `on_key_pressed`; +- text capture → `on_text_entered`, which in ~90% of cases is left + at its default and simply sinks into the UIC. + +### Why there is no double-insertion + +Under the three-tier model (Item 3), `on_key_pressed`'s default is +the *keypressed* sink, which ignores plain character keys — +insertion is the *textinput* sink's job. So even though +`on_key_pressed` fires for `'a'`, the character is inserted exactly +once, via the textinput path. A project sees a "double" callback +only if it deliberately handles both channels — which is plain LÖVE +semantics and its own choice. + +### What this dissolves + +- **Report Item 6** ("specify the suppression mechanism") — no + mechanism is needed; the hardest, impossible-as-written part of + the feature is *removed*, not solved. +- **Dim-3 co-firing finding** (Shift+S matching a combo *and* + firing `on_text_entered`) — no longer a defect; both fire by + design, the project picks the channel. +- **IME / dead-key worry** largely evaporates: `textinput` + delivers LÖVE-composed characters to `on_text_entered`; + `keypressed` delivers raw keys to `on_key_pressed`. There is no + classifier to fool. The only residue (a project using + `on_key_pressed` for commands may observe raw composition + keypresses) is native LÖVE behaviour, not a Compy limitation. + +### Consequence to document (not enforce) + +`compy.handlers` entries for bare printable keys (e.g. +`handlers['s']` with no Ctrl/Alt) **do** fire on `keypressed`, +alongside text entry. That is allowed and predictable; the guidance +is to reserve combos for command modifiers (ctrl/alt/gui). State +the behaviour in the spec; do not suppress it. + +### Future extension point (assumed, not built) + +**Text command-sets, Discord-style.** A project-registered table of +text-prefix → handler, matched on entered/submitted text by the +*same* overloadable-matcher principle as key-combos (Item 7), +except matching by **string prefix** instead of combo equality. +Lives in the input controller's textinput/submit path. Mark it in +code as an expansion seam paired with Item 7's matcher; not +enforced now. + +### Downward-doc updates + +- `spec.md §3` — remove the same-frame suppression rule entirely; + document the two independent channels and each channel's + default-sink behaviour; state the bare-printable-key combo + behaviour; drop the "exactly one per gesture" promise. +- `decisions.md` D-6 — supersede: replace the suppression note with + "both events surface on their own channels; no exclusivity; the + project chooses which to use." (Authoritative-doc edit — see + flag.) + +### Not a re-decision — striking unapproved filler + +Although D-6 lives in `decisions.md`, the "no double callback" +outcome is not human-approved input: it was raised during the +analysis/design session as a *"Suggested decision"* and never +discussed or signed off. The authority model is about whether a +human actually decided, not which file a line sits in — so D-6's +suggestion has no more standing than any other derived text. +Removing it is striking unapproved filler, not overturning a +decision; edit `decisions.md` D-6 freely on the next pass. + +Grounding for dropping it: `requirements.md` is concerned with the +*absence* of callbacks, never their duplication. Where duplication +actually matters, project code handles it trivially (e.g. an +internal guard/semaphore variable). And the new rule matches both +LÖVE and the current implementation. + +### Status + +Reframed per architect input: no exclusivity; two independent +channels mirroring LÖVE; suppression/classification dropped. +Resolves the report's Item 6 and the co-firing finding by +construction. The D-6 edit needs no special sign-off (unapproved +suggestion). Text-command-set prefix matching recorded as a future +seam paired with Item 7's matcher. + +## Item 7 — Combo serialisation, registration, and matching + +**Report dimension:** 3 (design→spec; internal inconsistency). +**Severity:** medium (handlers registered per the documented +examples would never fire). **Effort:** low–medium. + +### The defect + +`spec.md §1` states combos are "all held keys sorted +alphabetically," but every example is modifier-first +(`"lctrl+s"`, `"lalt+lshift+f4"`, `"lctrl+l"`). Alphabetical would +produce `"f4+lalt+lshift"` / `"l+lctrl"`, so the example +registrations never match what the serialiser emits. + +### Canonical form (settled) + +- **Order:** modifier-first by fixed precedence + (`ctrl, alt, shift, gui`), then the action key. Matches human + convention and the examples' intent. Not alphabetical. +- **L/R folding:** combo strings use **generic** modifier names + (`ctrl`, not `lctrl`/`rctrl`), so a project registers `"ctrl+s"` + once and catches either side. The `keys_pressed` table keeps the + precise LÖVE names; only the *combo serialisation* folds l/r. +- **Build rule:** `current_combo` = held command-modifiers (in + precedence order) + the triggering key `k`, not "everything + currently held" — avoids noise from an extra non-modifier key + held during fast typing. (Bare-key combos are allowed and fire on + the keypressed channel — see Item 6; the build rule simply + prepends whatever command-modifiers are held to the triggering + key.) + +### Mechanism (settled) + +- **Singleton held-state in `controller.lua`** with a precomputed + `current_combo` string, refreshed on every key down/up. Cost + lands on key events only (not per frame), so the string build is + GC-safe. +- **Order is derived from a static precedence list (or a sorted + array), never from `pairs()`.** Lua hash tables have no + guaranteed iteration order; iterating the held *set* directly + would give an unstable string. Iterate a canonical ordered list + and append the keys currently held. +- **`compy.handlers` is metatable-backed.** `__newindex(t, combo, + fn)` normalises the registered combo to canonical form and + stores it (`rawset` into the internal table). Registration is + the conversion seam; the project still writes + `compy.handlers['Ctrl+S'] = fn` and it reads naturally. +- **Dispatch goes through an overloadable matcher**, not a + hardcoded lookup. Default matcher: exact canonical match + (effectively `handlers[current_combo]`, O(1)). The matcher is + the marked expansion point (comment in code) for future + glob / prefix / regex matching, and is **project-overloadable** + so richer matching can be tested and rolled out gradually + without changing the framework default or breaking existing + handlers. + +### Design rationale + +KISS with clean expansion points: exact matching now, but behind a +swappable matcher function rather than an inlined table read, and +behind a metatable that owns normalisation. No predicate +machinery is built until prefix/glob matching is actually needed — +at which point only the matcher (and optionally per-entry compiled +predicates) changes. Aligns with the house rules ("store the +function, not a string tag"; avoid abstraction a student couldn't +follow — the project-facing surface stays a plain table). + +### Downward-doc updates + +- `spec.md §1` — replace the alphabetical rule with the canonical + form above; fix all combo examples; document generic l/r + folding and the `combo_string(k, keys)` build rule. +- `spec.md §3` / `design.md §4` — note `compy.handlers` is + metatable-normalised on assignment and dispatched via an + overloadable matcher (default exact). +- Mark the matcher as the expansion seam in code (comment). + +### Status + +Settled: modifier-first generic-folded canonical form; +metatable-normalised registration; overloadable matcher with +exact-match default. No open decision. + +## Item 8 — Codebase-reference corrections in the chain + +**Report dimensions:** 2, 6. **Severity:** low individually, but +they propagate into wrong implementation assumptions. **Effort:** +trivial (text fixes). No decisions; apply on the next pass. + +1. **`cancel()` does not push `'userinput'`; Escape does not + dismiss the overlay today.** `assessment.md §2` and `§8` state + cancel pushes `'userinput'` and clears the overlay. Code: + `cancel()` → `handle(false)` (`userInputModel.lua:795–798`), + and the `'userinput'` push is reached only on the + `eval == true` + `oneshot` + success path (`:809–821`); the + `eval == false` branch sets `ok = true` and does not push. So + Escape clears content via `reset()` but leaves the overlay up — + which is exactly the limitation `design.md §5` is built to fix. + The design is right; fix the assessment. + +2. **`oneshot` is a `UserInputModel` field, not a + `UserInputController` one.** `userInputModel.lua:15,49`; + `UserInputController` only reads `self.model.oneshot`. Fix the + wording in `design.md §3` and `decisions.md` D-4, and the file + target in the roadmap (deletion lands in M6 per Item 2, in + `userInputModel.lua`; the submit-path code that reads it is in + `userInputController.lua`). + +3. **Stop-time reset is `stop_project_run` / + `clear_user_handlers`, not `evacuate_required`.** + `evacuate_required` (`consoleController.lua:844–858`) only + unloads project `.lua` modules from `package.loaded`. The + handler/state reset is in `stop_project_run` + (`consoleController.lua:860–868`). Fix the "same mechanism as + `evacuate_required`" references in `design.md §3`, + `spec.md §3`/`§6`, and `decisions.md` D-7. + +--- + +## Item 9 — Deliver the D-7 FR-11/FR-12 walkthrough + +**Report dimension:** 2 (unmet promise). **Severity:** medium — +FR-11/FR-12 are the API-completeness acceptance criteria; they are +currently asserted, not demonstrated. **Effort:** low (a short +mapping, no code). + +`decisions.md` D-7 promised `design.md` would "include a +walkthrough confirming the API surface covers both cases" +(console + editor). `design.md §7` only asserts the migration is +mechanical. Add the concrete mapping (it doubles as a design-time +test that the Item 3 tiers + `framework_handlers` actually cover +the cases): + +- **Console REPL:** Enter → `framework_handlers['return']` + (submit); Up/Down at history boundary → limit signal → + `on_limit_reached` / handler; Ctrl+L clear terminal → + `compy.handlers['ctrl+l']`; error display → sink (unchanged). +- **Editor:** Enter → submit block; Escape → load → + `framework_handlers['escape']` + `before_cancel`; Up/Down at + boundary → block nav via `on_limit_reached`; Ctrl+M / Ctrl+F + mode switches → `compy.handlers['ctrl+m'/'ctrl+f']`; cursor + get/set → the FR-8/9 surface (restored in Item 1 — note the + cross-dependency: without Item 1, FR-12 is not expressible). + +Placement: put the walkthrough in `design.md` (where D-7 located +it) or in `notes/editor_repl_input.md` with D-7 re-pointed there. +Either satisfies the promise; `design.md` is the literal fix. + +--- + +## Item 10 — Smaller drifts + +**Severity:** low. Mostly downward-doc corrections; all sub-items +now settled. + +1. **`user_input()` ≠ `compy.show({})`.** Current `user_input()` + only allocates and returns the reftable + (`consoleController.lua:582–585`); it shows nothing — the + overlay appears on a later `input_text()`/etc. **Resolution:** + the `user_input()` facade stays reftable-only (no `show`); the + showing facades wire that reftable into the singleton's + `result` on `show()` (the repointing from Item 2). Fix the + `spec.md §5` mapping. + +2. **`on_text_entered` second argument.** D-6 says the project + receives a `mods` subset (non-character keys only); `spec.md §3` + passes the full `keys_pressed` proxy. **Resolution:** pass the + `keys_pressed` proxy uniformly (consistent with + `on_key_pressed`); reword D-6. Projects read modifiers off the + proxy. Simpler, one representation. + +3. **`isrepeat` on `on_key_pressed` (settled).** D-3 lists + `_on_key_pressed(k, pressed, isrepeat)`; `spec.md §3` drops it + from `on_key_pressed` (keeps it on `ProjectController:keypressed`). + **Resolution: thread it through, as the trailing arg — + `on_key_pressed(k, keys, isrepeat)`.** Grounding: `isrepeat` is + read **nowhere** in the codebase (no controller, no console/ + editor, no example uses it). So rather than mirror LÖVE's native + `(key, scancode, isrepeat)` order — which would put the + never-used `isrepeat` ahead of the always-useful `keys` proxy — + keep the common `(k, keys)` signature clean and trail `isrepeat` + last. The cross-layer argument reorder is a deliberate, documented + deviation (normal between framework levels); cheap to provide, + available if held-key behaviour is ever wanted. Apply the same + "useful args first, rarely-used native trailer last" ordering to + `on_text_entered` and to `ProjectController:keypressed` for + consistency. + +4. **`compy.show` / `compy.hide` exposure milestone.** No roadmap + milestone exposes them on the namespace, yet the M3 facades + call `compy.show`. **Resolution:** expose `compy.show`/ + `compy.hide` in M2/M3 (they must exist before the facades use + them); add to the milestone file lists. + +5. **`compy.handlers` / `on_text_entered` co-firing** — resolved + by Item 6's classification (Shift+letter is a character; combos + stay silent). Cross-reference; no separate action. + +6. **`summaries/design.md` Escape opt-in remark (settled — drop + it).** The summary carries a parenthetical — "there should be + path to keep current behaviour, opt-in … not specified now" — + absent from `design.md`. **Resolution: drop the opt-in; remove + the orphan remark from the summary; add nothing deferred to + `design.md`.** Grounding (bug-vs-feature test against the code): + + - **Editor** owns Escape itself (`editorController.lua:441,486` + mode exits / load); the sink's `cancel()` is not even invoked + in the editor branch (`userInputController.lua:362` skips it). + Nothing here depends on the sink's Escape. + - **REPL** — the only `input:cancel()` is the programmatic + terminal-test path (`consoleController.lua:967`), not a user + Escape. User Escape at the REPL clears the current input line; + the prompt staying up is the REPL being persistent, not an + overlay being kept open. + - **Project overlay** (this feature's scope) — Escape clears + content and leaves the prompt up. That is exactly the + limitation `design.md §5` fixes, and nothing relies on it: + projects cannot dismiss prompts at all today. + + So within #77's scope the current Escape behaviour is a **bug**, + relied on by nothing → drop it; the new dismiss-on-Escape is the + fix, and opt-in/opt-out does not apply (opt-in is wrong for a + bug; opt-out would only matter if it were a relied-upon feature, + which it is not). + + **Carry-forward (not an Escape opt-in):** the REPL's "Escape + clears the input line" is a genuine behaviour, but it belongs to + the console-migration follow-on (D-7), where the migrated + `ConsoleController` registers its own `framework_handlers['escape']` + = clear-line. `framework_handlers` are per-controller, so + `ProjectController`'s dismiss-Escape never clobbers it — nothing + is lost. Fold this into Item 9's walkthrough rather than tracking + it as an Escape opt-in. + +7. **Dead `compy.text_input` alias.** `consoleController.lua:628` + assigns `nil` (bare `input_text` not in scope). The new API + replaces it; add an explicit cleanup line to the roadmap docs + block so it isn't left dangling. + +--- + +## Namespace isolation — relocate the new API under `compy.input.*` + +*Cross-cutting and **orthogonal to Items 1–10**. This is a pure +relocation / naming concern, not a functional or architectural +change. Apply it as a **separate pass** (ideally last, after the +behavioural edits are in and verified), so it does not tangle with +the substance of the items above.* + +### Decision (architect) + +The new callback / lifecycle / accessor surface lives under a +dedicated sub-namespace **`compy.input.*`**, not flat on +`compy.*`. + +Rationale: `compy.*` is the whole project-facing API and will +accumulate many unrelated callbacks, variables, and objects serving +different classes of purpose. Flat-mounting the input surface there +crowds the namespace and blurs concerns. A sub-namespace isolates +this feature's surface, keeps `compy.*` structured, and leaves room +for sibling sub-namespaces later. (`input` was chosen over the +earlier `terminal` working label: this surface is the interpretive +input-manipulation layer, and `compy.input.*` contrasts cleanly +with the raw `compy.keys_pressed` keyboard state — see *what does +NOT move* below.) + +### Scope — what moves + +Every *new* `compy.*` name introduced by feature #77 (including the +ones restored in Item 1) moves under `compy.input.*`: + +| Was (in the chain as written) | Becomes | +|---|---| +| `compy.show` | `compy.input.show` | +| `compy.hide` | `compy.input.hide` | +| `compy.configure` | `compy.input.configure` | +| `compy.clear` | `compy.input.clear` | +| `compy.handlers` | `compy.input.handlers` | +| `compy.on_key_pressed` | `compy.input.on_key_pressed` | +| `compy.on_text_entered` | `compy.input.on_text_entered` | +| `compy.before_submit` / `after_submit` | `compy.input.before_submit` / `after_submit` | +| `compy.before_cancel` / `after_cancel` | `compy.input.before_cancel` / `after_cancel` | +| `compy.on_limit_reached` | `compy.input.on_limit_reached` | +| `compy.get_cursor` / `set_cursor` / `set_text` (Item 1) | `compy.input.get_cursor` / `set_cursor` / `set_text` | + +### Scope — what does NOT move + +- **Legacy global facades** — `input_text()`, `input_code()`, + `validated_input()`, `user_input()`, `write_to_input()` — stay + exactly where they are (project-env globals), unchanged. They are + the backward-compat surface (D-1). Internally they call the + relocated `compy.input.*` functions, but their own names and + call sites do not change. +- **`keys_pressed` proxy** — stays global as `compy.keys_pressed`, + **not** under `compy.input` (decided). It belongs to the + keyboard-state mapping — physical reality — not the + input-manipulation layer — interpretation. Keeping it out of + `compy.input.*` makes that boundary explicit at the namespace + level. + +### Mechanics + +- Pure rename/reparent in the docs: `compy.X` → `compy.input.X` + across `design.md`, `spec.md`, `roadmap.md`, and the mirrored + summaries. No behavioural change, no signature change. +- The roadmap milestones that "expose `compy.*` names on the + namespace" (M2/M3 show/hide; M5 handlers/callbacks; M7 + configure/clear + cursor) now mount them on `compy.input.*`; + add a one-line note that the `compy.input` table is created + once at namespace setup and the names are mounted on it. +- Reset-on-stop (Item 8 / `stop_project_run`) clears + `compy.input.handlers` and the `compy.input.*` callbacks + together (implementation note, not a doc-pass action). + +### Why a separate pass + +It is mechanical and global; folding it into the Item 1–10 edits +would make both harder to review. Do the functional edits first, +verify, then run the relocation as a single find-and-reparent pass. +The two are independently checkable. + +### Status + +Decided (architect): isolate the new surface under `compy.input.*`; +`compy.keys_pressed` stays global. Mechanical relocation, +orthogonal to Items 1–10. No open sub-points. diff --git a/doc/development/wip/77-new-input-api/validation/recommendations_2.md b/doc/development/wip/77-new-input-api/validation/recommendations_2.md new file mode 100644 index 00000000..140afc0c --- /dev/null +++ b/doc/development/wip/77-new-input-api/validation/recommendations_2.md @@ -0,0 +1,262 @@ +# Feature #77 — Recommendations (round 2) + +*Input for the second fix iteration of the document chain. Distils the +actionable items from `validation/validation_report_2.md` into concrete +resolutions, with severity and per-document targets. The report returned +**PASS WITH NOTES**: the round-1 blockers are closed and faithfully applied; +what remains is consistency residue and provenance/packaging gaps. None drop a +requirement or break an example. This round closes them.* + +*Numbering is this round's own (Item 1…6); it does not continue +`recommendations_1.md`'s.* + +--- + +## Document authority model (unchanged from round 1) + +`input.md` is the only stakeholder-signed document. `requirements.md` is high +local input and **is not edited** this round (no item requires it). Everything +else — `assessment.md`, `decisions.md`, `design.md`, `spec.md`, `roadmap.md`, +and the `summaries/` — is derived/local and edited freely, with `decisions.md` +edited under the provenance rules (new commitments get fresh IDs continuing the +sequence; corrections annotated in place; every change tagged with a greppable +round-2 origin marker). + +Round-2 provenance marker: *(Origin: local design round 2, 2026-06. See +`validation/recommendations_2.md` Item N.)* + +--- + +## Item 1 — Resolve the `noop+log` vs `sink` default contradiction + +**Report finding:** N1 / dimension 2. **Severity:** medium — it contradicts the +headline Item-3 (round 1) resolution and, taken literally, would have the +implementor wire the default `on_key_pressed` to a no-op, under which default +text editing (the sink) never runs. **Effort:** trivial (three wording fixes). + +### Root cause + +Item 3 (round 1) made the *sink* the default value of the two channel +callbacks (`on_key_pressed`, `on_text_entered`). The per-callback spec sections +say so. But three places still carry the pre-Item-3 `noop+log` default — a +residue the dispatch sections corrected but these did not: + +- `design.md §2` routing diagram, line ~75: + `compy.input.on_key_pressed [generic, overloadable, default: noop+log]`; +- `spec.md §3` intro: "Default value for each is a no-op function that emits a + debug log entry"; +- `summaries/spec.md`: "All callbacks default to a no-op + debug log entry." + +The `noop+log` default is correct for the **other** callbacks +(`before/after_submit`, `before/after_cancel`, `on_limit_reached`); it is wrong +for the two channel callbacks, whose default is the sink. + +### Resolution + +Scope the `noop+log` statement to the non-channel callbacks. State the channel +callbacks' default as the sink everywhere: + +- `design.md §2` diagram: change the `on_key_pressed` annotation from + `default: noop+log` to `default: sink`. +- `spec.md §3` intro: reword to "The submit/cancel/limit callbacks default to a + no-op that emits a debug log entry; the two channel callbacks + (`on_key_pressed`, `on_text_entered`) default to the text-editing sink (see + each section below)." +- `summaries/spec.md`: same scoping ("submit/cancel/limit callbacks default to + no-op + debug log; the two channel callbacks default to the sink"). + +No decision; this is mechanical alignment to Item 3 (round 1). No +`decisions.md` entry. + +--- + +## Item 2 — Surface the `compy.input.*` namespace as a decision; make `decisions.md` naming consistent + +**Report finding:** Part B (provenance) + Part A (namespace). **Severity:** +medium (provenance) — a real architectural commitment is invisible in the one +document the stakeholder reviews, and `decisions.md` carries the old flat names +(plus a single leaked `compy.input.handlers` at line ~398), splitting the chain. +**Effort:** low. + +### Root cause + +The `compy.input.*` sub-namespace was an architect decision recorded in +`recommendations_1.md` ("Namespace isolation") and applied to the derived docs, +but `decisions.md` was excluded from that pass. So `decisions.md` still speaks +flat `compy.on_key_pressed` / `compy.handlers` / `compy.get_cursor` etc., while +`design.md`/`spec.md`/`roadmap.md`/their summaries say `compy.input.*` — and the +namespace choice itself appears in no decision the stakeholder review covers. + +### Resolution + +1. **Add `D-10` to `decisions.md` and its summary** — namespace isolation under + `compy.input.*`. Use the standard entry shape (Question / Context / Affects / + Source / Decision). State: the new callback/lifecycle/accessor surface lives + under `compy.input.*`; `compy.keys_pressed` stays global (raw keyboard state, + not the input-manipulation layer); legacy globals are unchanged. Source: + `recommendations_1.md` "Namespace isolation". Tag origin: the decision + originated in round 1's namespace pass and is **recorded** as a decision in + round 2 — make that explicit in the marker. +2. **Relocate the new-API names in `decisions.md` and `summaries/decisions.md` + to `compy.input.*`**, so the whole chain is consistent and the leaked line is + no longer an outlier. Do **not** relocate `compy.keys_pressed` or the legacy + globals (same scope table as round 1). This reverses round 1's narrow + exclusion of `decisions.md` from the namespace pass — note it in the + changelog. + +This is the cleanest reading: it removes the split entirely rather than +papering over it, and the relocation is mechanical (names only, no substance +change to any decision). + +--- + +## Item 3 — Re-estimate the roadmap for round-1 scope + +**Report finding:** dimension 5 + architect note 2. **Severity:** low–medium +(planning accuracy). **Effort:** low. + +### Root cause + +The estimate table is carried over from take 1 (41 h / 23 h PERT) and was not +revised after round 1 shifted scope: M7 grew (added `get_cursor`/`set_cursor`/ +`set_text` plus the `UserInputModel:set_text` `keep_cursor` model fix), M3 +gained the `write_to_input` facade, M2 gained the `result` repointing setter and +`compy.input` table creation, work moved from M2 to M6 (the `oneshot` deletion), +and the testable surface grew (cursor, native coexistence, two-channel). + +### Resolution + +Revise the `## Estimates` section, label it "revised 2026-06 for round-1 +scope," and bump the affected lines. Suggested figures (Optimistic / Realistic): + +**Without LLM** — M1 2/3, M2 3/6, M3 2/4, M4 4/8, M5 3/5, **M6 4/7** +(oneshot deletion + submit reownership added), **M7 3/6** (cursor surface + +model fix added), Docs 4/8, **Tests 7/11** (cursor, native coexistence, +two-channel cases). Totals 32 / 58; PERT (O=32, M=45, P=58) ≈ **45 h**. + +**With LLM** — M1 1/2, M2 2/4, M3 1/2, M4 3/5, M5 2/3, **M6 2/5**, **M7 2/4**, +Docs 2/3, **Tests 4/6**. Totals 19 / 34; PERT (O=19, M=26, P=34) ≈ **26 h**. + +Keep the confidence note (M4 the main integration uncertainty) and add that the +round-1 scope additions land mostly in M6, M7, and test coverage. No +`decisions.md` entry (estimates are not decisions). + +--- + +## Item 4 — Adjudicate the optional `mods` modifier-string idea + +**Report finding:** architect note 4. **Severity:** low–medium (an unrecorded +design-session output, not a doc inconsistency). **Effort:** low (a tracked +note) — **unless** the architect wants it built, which is a scope decision. + +### The idea (architect note) + +Augment both the `keypressed` and `textinput` paths with an optional `mods` +string — a pre-folded modifier descriptor (the same generic l/r folding as the +combo form) — passed as a trailing argument to downstream handlers and +callbacks. Rationale: a handler attached to several combos may need to know +which modifiers fired; and the generic callback (which runs after combo +handlers) often needs that context too. The current chain passes the +`keys_pressed` read-only proxy (modifiers are derivable from it) but no +pre-folded `mods` string. + +### Resolution (conservative; flag for confirmation) + +Do **not** invent the `mods` argument across every handler signature in this +pass — that is new API surface and the executor role is not to add structure +the recommendations did not decide. Instead: + +- **Record it as a future seam, not built in v1**, paired with the existing + not-built seams (overloadable matcher expansion; Discord-style text-command-set + prefix matching). Place a one-paragraph note in `decisions.md` (under D-6, the + channel-model decision, since that is where the second-argument representation + lives) and a short "future extension" mention in `spec.md §3`. State the v1 + representation is the `keys_pressed` proxy; the pre-folded `mods` convenience + string is a candidate addition. +- **Flag it as an open question** in the changelog for the architect to confirm: + build the `mods` trailing string in-scope, or leave it as a tracked seam? If + in-scope, it is a `recommendations_3` item (it touches every downstream + signature: `on_key_pressed`, `on_text_entered`, `handlers[combo]` entries, + `ProjectController:keypressed`/`:textinput`). + +This keeps the idea auditable without silently expanding the API or silently +dropping it. + +--- + +## Item 5 — Add the derivative/proposal-chain disclaimer to `spec.md` and `roadmap.md` + +**Report finding:** architect note 6. **Severity:** low–medium. **Effort:** +trivial. + +`decisions.md` and `summaries/decisions.md` carry the proposal-status note; +`spec.md` and `roadmap.md` do not. Per the authority model both are +derived/provisional. Add a one-paragraph header note to each: + +- They are derived documents in the feature-#77 proposal chain, pre-built on the + assumption the design is endorsed (not vetoed). +- Detail here is not frozen: stakeholders may review or change parts without + blocking implementation — there is no requirement to freeze the spec before + work starts. + +No `decisions.md` entry (a documentation-status note, not a decision). + +--- + +## Item 6 — Minor consistency cleanups + +**Severity:** low. Mechanical alignment; no `decisions.md` entries beyond the +in-place edits noted. + +1. **`decisions.md` "seven" intro → reconcile with the actual count.** The + "blocking decisions" intro enumerates seven questions while the file now + carries D-1…D-10. Update the prose to note that round 1 added D-8 (cursor + restoration) and D-9 (native coexistence), and round 2 records D-10 + (namespace), while the original seven remain the core stakeholder-review + focus. Add a short "superseded — see annotation below" lead-in on the D-3 + and D-6 **body** text (whose original suggested-decision prose describes the + pre-round-1 model, corrected only by the appended annotation), so a reader + does not take the body as current. + +2. **`assessment.md §8` co-occurrence → re-point at the resolution.** The + "Text character + modifier co-occurrence" paragraph still frames the case as + an open design decision. Add a closing sentence noting it was resolved by + D-6's round-1 supersession (both channels fire independently; no exclusivity) + so the paragraph no longer reads as unresolved. + +3. **`design.md §3` component-table wording.** `compy.input.on_key_pressed` is + described as "Generic non-character key callback" — stale framing from the + pre-two-channel model. Change to "Generic keypressed callback (fires for all + keys; default = sink)," consistent with `spec.md §3`. + +4. **Emphasize the textinput-mirrors-keypressed symmetry (architect note 3).** + The chain states the textinput channel's default is the textinput sink but + does not draw out that `on_text_entered` processing follows the same + default-sink/override principle as `on_key_pressed`. Strengthen the one-line + note in `decisions.md` D-6, `design.md §4`, and `spec.md §3` so the symmetry + is explicit rather than implied. + +--- + +## Summary of targets + +| Item | Decision entry | Primary doc targets | +|---|---|---| +| 1 — default contradiction | none | `design.md §2`, `spec.md §3`, `summaries/spec.md` | +| 2 — namespace decision + naming | **D-10 (new)** | `decisions.md`, `summaries/decisions.md` (add D-10; relocate names) | +| 3 — re-estimate | none | `roadmap.md` Estimates | +| 4 — `mods` seam | D-6 note (future seam) | `decisions.md` D-6, `spec.md §3`; **open question** | +| 5 — disclaimer | none | `spec.md` header, `roadmap.md` header | +| 6 — cleanups | D-3/D-6 body markers (in place) | `decisions.md`, `assessment.md §8`, `design.md §3`, `spec.md §3`, `summaries/*` as mirrored | + +Stage note: there is no separate namespace pass this round — Item 2 *is* the +remaining namespace work, scoped to `decisions.md`/its summary only. + +--- + +## Status + +Ready to apply. Items 1, 3, 5, 6 are mechanical alignment. Item 2 adds one +traceable decision (D-10) and a names-only relocation. Item 4 is recorded as a +future seam and **flagged for architect confirmation** rather than built — the +only open question this round carries forward. diff --git a/doc/development/wip/77-new-input-api/validation/validation_report_1.md b/doc/development/wip/77-new-input-api/validation/validation_report_1.md new file mode 100644 index 00000000..ac7ec906 --- /dev/null +++ b/doc/development/wip/77-new-input-api/validation/validation_report_1.md @@ -0,0 +1,514 @@ +# Validation Report — Feature #77 Document Chain + +*Independent review of the feature #77 document chain +(`input.md` → `requirements.md` → `assessment.md` → +`decisions.md` → `design.md` → `spec.md` → `roadmap.md`) and the +`summaries/`. Claims about current behaviour were checked against +`src/`. Reviewer role only — findings, not rewrites.* + +*Convention note: the seven decisions are labelled D-1…D-7 in the +prose of `decisions.md` but never numbered inline (only in the +"Quick reference" table). References below use that table's +numbering.* + +--- + +## Status: FAIL + +The chain is coherent on its central idea (routing unification + +persistent singleton + callbacks) and most requirements are +carried through cleanly. Three classes of issue need resolution +before implementation, because they would either drop a stated +requirement, break a listed must-pass example, or hand the +implementor contradictory contracts: + +1. **FR-8, FR-9, FR-10 (cursor query, cursor set, live text + change) drop out of the chain** after `assessment.md`. They + have no API surface in `spec.md` and no roadmap home; FR-9 is + actively contradicted by the `configure()` contract. +2. **Roadmap ordering strands the submit path.** M2 deletes the + `oneshot` flag (which currently drives submit + the + `'userinput'` push) while the replacement + (`framework_handlers['return']`) does not arrive until M6 — + so submit is broken across M2–M5, contradicting M2's + "zero behaviour change" and M3's "all examples work". +3. **`compy.on_key_pressed` has two contradictory contracts.** + `decisions.md` and `design.md` make it a consuming dispatch + level; `spec.md §3` says its return value is ignored. + +"FAIL" here means "do not start coding against this yet," not +"the direction is wrong." The architecture is sound; the gaps are +in completeness and cross-document consistency, which is expected +for a draft that (per the commit log) has not yet been +cross-checked. + +--- + +## Findings by dimension + +### 1. Requirements coverage + +| Req | decisions | design | spec | roadmap | Status | +|---|---|---|---|---|---| +| FR-1 setup (text/cursor/highlighter/validator/prompt) | D-2 | §3 `compy.show` | §2 config table | M2/M3 | **Covered** (see note a) | +| FR-2 remove/teardown | D-2 (implicit) | §1 `hide` | §2 `hide()` | M2 | **Covered, reframed** (note b) | +| FR-3 hide | D-2 | §3 | §2 `hide()` | M2 | **Covered** (note c) | +| FR-4 show | D-2 | §3 | §2 `show()` | M2 | **Covered** (note c) | +| FR-5 submit notification | D-4 | §5 | §3 `before/after_submit` | M6 | **Covered** | +| FR-6 non-char key notification | D-3 | §4 | §3 | M5 | **Covered** (see dim. 2 contradiction) | +| FR-7 boundary notification | D-5 | §3 table | §4 `on_limit_reached` | M6 | **Covered** (note d) | +| FR-8 query cursor while active | — | §1 prose only | **absent** | **absent** | **GAP** | +| FR-9 set cursor while active | — | §1 prose only | **contradicted** | **absent** | **GAP** | +| FR-10 change text while active | — | §3 `clear` only | **no live-write** | **absent** | **GAP** | +| FR-11 REPL re-implementable | D-7 | §7 (asserted) | §6 | follow-on | **Asserted, not shown** (note e) | +| FR-12 editor re-implementable | D-7 | §7 (asserted) | §6 | follow-on | **Asserted, not shown** (note e) | +| NFR-1 no per-session alloc | D-2 | §2/§3 singleton | §2 | M2 | **Covered** | +| NFR-2 event-driven | D-3/D-4 | §4/§5 | §3 | M5/M6 | **Covered** | +| NFR-3 compy namespace consistency | D-3 | §3 | §2/§5 | M5/M7 | **Covered** (note f) | +| NFR-4 pedagogical simplicity | — | §1 | — | — | **Covered implicitly** | + +**GAP — FR-8 (query cursor position while active).** Listed in +`requirements.md §2.4`; `assessment.md §4` confirms the model API +(`UserInputModel:get_cursor_pos`) is complete and "exposure is +the only missing piece"; `design.md §1` states the feature adds +"programmatic cursor and content access." But no `compy.*` +function to read the cursor appears in `design.md §3`'s component +table or in `spec.md §2`. `notes/solution_sketch.md §2` listed a +`read()` call in the intended API surface; it was not carried into +`design.md` or `spec.md`. No roadmap milestone delivers it. + +**GAP — FR-9 (set cursor position while active).** Same drop as +FR-8, and additionally **contradicted**: `spec.md §2` +(`compy.configure`) states "Fields `text` and `cursor` are +accepted but have no effect when called on an already-active +session," and `compy.clear()` only resets the cursor to position +1. So the spec's only cursor controls are (a) initial position at +`show()` and (b) reset-to-1 at `clear()`. FR-9 asks to *change* +the cursor position while the area is active; the spec explicitly +forecloses it. `UserInputModel:move_cursor(y, x)` / +`set_cursor(c)` exist (`userInputModel.lua:499,506`) but remain +unexposed. + +**GAP — FR-10 (change text content while active).** The current +mechanism is `write_to_input(content)` +(`consoleController.lua:599–605`), which `assessment.md §4` names +as the FR-10 path. In the new chain: +- `compy.configure` ignores `text` while active (`spec.md §2`); +- `compy.clear` only empties content (`spec.md §2`); +- `compy.show` replaces content but is the activation call, not a + live-write; +- there is no `compy.set_text` / live-write function anywhere in + `design.md` or `spec.md`. + +So no path changes text on an already-active session. This also +intersects backward compatibility — see dim. 6, `write_to_input`. + +Notes: +- (a) FR-1 "initial cursor position" is delivered only as the + `cursor` field at `show()` time (`spec.md §2`), which is correct + for FR-1; it is FR-9's *while-active* change that is missing. +- (b) FR-2 asks for programmatic *removal*. The singleton design + has no teardown by construction; `hide()` is the + project-observable equivalent. This is a reasonable resolution + but is never stated as the FR-2 answer — worth making explicit + so a stakeholder reading FR-2 ("remove") finds the mapping. +- (c) FR-3/FR-4 are covered behaviourally, but the *exposure* of + `compy.show`/`compy.hide` on the namespace has no explicit + roadmap milestone — see dim. 4. +- (d) `design.md` proper is thin on FR-7: `on_limit_reached` + appears in the §3 component table but the limit-signal plumbing + (sink return → hook) lives in `notes/routing_unification.md`, + not the design body. +- (e) See dim. 2, D-7. +- (f) The existing dead `compy.text_input` alias (see dim. 6) has + no explicit cleanup line in the roadmap; minor. + +### 2. Decision consistency + +- **D-1 (backward compat)** — Reflected in `design.md §6` and + `spec.md §5` (facade wrappers, reftable fill, `strict_input` + future flag). Consistent across documents. One omission: + `write_to_input` is not in the facade list (dim. 6). +- **D-2 (second setup / singleton)** — Reflected consistently: + `design.md §2/§3`, `spec.md §7` ("`show()` while already active + → reconfigures in-place, no cancel chain"). Consistent. +- **D-3 (key event coverage)** — Three-level dispatch carried + into `design.md §4` and `spec.md §3`. **Inconsistency:** the + combo-string examples violate the stated sort rule (see dim. 3), + and the `on_key_pressed` return-value contract contradicts D-4 + (next item). Minor signature drift: D-3 specifies + `_on_key_pressed(k, pressed, isrepeat)`; `spec.md §3` + `compy.on_key_pressed(k, keys_pressed)` drops `isrepeat` + (it survives only on `ProjectController:keypressed` in + `spec.md §1`). +- **D-4 (cancel/submit chains; oneshot deletion)** — + **Contradiction with `spec.md`.** D-4 (`decisions.md:250–251`) + states "The generic `compy.on_key_pressed` uses return-value + propagation (true = consumed, nil = bubble)." `design.md §4` + agrees: "At every level, a handler returning a truthy value + signals 'consumed' and stops the chain." But `spec.md §3` + (`compy.on_key_pressed`) states: "Return value is ignored. To + prevent the sink from processing a key, register it in + `compy.handlers` and return truthy." These cannot both hold: + either `on_key_pressed` is a consuming dispatch level (D-4 / + design) or it is a passive notifier (spec). Also: the oneshot + deletion is tied by D-4 to the framework owning submit via + `framework_handlers['return']` — which the roadmap schedules in + M6, while it deletes the flag in M2 (see dim. 5). +- **D-5 (boundary)** — `on_limit_reached(direction)`, + whole-input boundary, reserved 2nd arg: consistent across + `design.md §3`, `spec.md §4`. Matches code + (`UserInputModel:is_at_limit`, `userInputModel.lua:558–570`). +- **D-6 (modifier + character)** — Carried into `spec.md §3`. + **Minor inconsistency:** D-6 says project code receives + `on_text_entered(text, mods)` where `mods` is "the implicit + modifiers table" (non-character keys only); `spec.md §3` passes + the full `keys_pressed` read-only proxy, not a filtered + modifier subset. Also the suppression mechanism is declared but + not specified — see dim. 3. +- **D-7 (rollout scope)** — Reflected in `design.md §7`, + `spec.md §6`. **Unmet promise:** D-7 states "`design.md` will + include a walkthrough confirming the API surface covers both + cases [console + editor]." `design.md §7` asserts the migration + is mechanical ("if-chains become handler registrations") but + contains no actual walkthrough mapping the console's + history-nav / `Ctrl+L` and the editor's `Ctrl+M`/`Ctrl+F` mode + switches onto the new API. The promised FR-11/FR-12 evidence is + asserted, not shown (the analysis is gestured at in + `notes/editor_repl_input.md`, but D-7 located it in + `design.md`). + +### 3. Design-to-spec completeness + +- **`design.md §1` over-claims relative to `spec.md`.** Design + scope says the feature adds "programmatic cursor and content + access." The spec provides neither a cursor accessor/mutator nor + a live-text mutator (FR-8/9/10, dim. 1). Either design scope + should be narrowed or the spec should add the surface. +- **`compy.show`/`compy.hide` lack edge-case specification on a + point design raises.** `design.md §3` notes "No access control + is enforced"; `spec.md §2` repeats it. Fine. But the + interaction with a project's *own* `love.keypressed`/`textinput` + (see dim. 6) is described in neither — a real design element + (the `love.keypressed` slot, `controller.lua:73–107`) with no + spec contract. +- **Combo serialisation: examples contradict the stated rule.** + `spec.md §1` states "Sorting is alphabetical on the raw LÖVE2D + key name strings … all held keys are sorted together." The + worked examples violate this: + - `"lalt+lshift+f4"` (`spec.md §1`, `design.md §4`) — + alphabetical order of {`f4`,`lalt`,`lshift`} is + `"f4+lalt+lshift"` (`f` < `l`). + - `compy.handlers['lctrl+l']` (`spec.md §3`) — alphabetical + order of {`l`,`lctrl`} is `"l+lctrl"` (`"l"` is a prefix of + `"lctrl"`, so it sorts first). + Only `"lctrl+s"` happens to match. An implementor who registers + combos following the documented (modifier-first) examples will + produce keys the serialiser never generates, so those handlers + silently never fire. Resolve by either defining a + modifier-priority ordering (matching the natural examples) or + fixing the examples to be truly alphabetical. +- **`on_key_pressed` suppression mechanism declared but not + specified.** `spec.md §3` / `decisions.md` D-6 require: + suppress `on_key_pressed` for a key that is "followed by a + `textinput` event in the same frame." LÖVE delivers + `keypressed` *before* `textinput`, so at `keypressed` dispatch + time the framework cannot yet know whether a character will + follow. The spec says "This suppression logic is a spec detail" + but does not say *how* the lookahead/deferral works. This is the + load-bearing mechanism for D-6's "no double callback" guarantee + and the hardest part of the feature; leaving it unspecified + pushes a non-trivial design decision onto the implementor. +- **`compy.handlers[combo]` can still co-fire with + `on_text_entered` for character combos.** The D-6 "exactly one + notification per gesture" guarantee covers `on_key_pressed` vs + `on_text_entered` only. `compy.handlers[combo]` is dispatched at + `keypressed` time regardless of whether a character follows, so + e.g. `Shift`+letter (a capital character) can match a registered + combo handler *and* fire `on_text_entered`. Either intended or + an unaddressed corner; `spec.md` should state which. +- **`compy.before_submit` argument inconsistency.** `design.md §5` + shows `before_submit(keys_pressed)` and `spec.md §3` agrees, but + the summary callback table lists no arg surprises — fine. No + finding beyond noting the arg is documented. + +### 4. Spec-to-roadmap coverage + +- **GAP — `compy.show` / `compy.hide` exposure has no milestone.** + These are the primary entry points (`design.md §3`, + `spec.md §2`). Roadmap milestones expose specific surfaces on + the namespace: M5 exposes `compy.handlers`, + `compy.on_key_pressed`, `compy.on_text_entered`; M7 exposes + `compy.configure`, `compy.clear`. M2 adds `show()`/`hide()` + *methods on the controller*, not `compy.show`/`compy.hide` on + the project namespace. No milestone's file list exposes + `compy.show`/`compy.hide`. (M3 facades call them internally, + which assumes they already exist — see dim. 5.) +- **GAP — FR-8/9/10 surface (cursor get/set, live text) has no + milestone**, consistent with their absence from the spec + (dim. 1). +- **`compy.before_submit`/`after_submit`/`before_cancel`/ + `after_cancel` / `on_limit_reached`** — delivered by M6. + Covered. +- **Edge cases (`spec.md §7`)** — the test-coverage block + enumerates "stop-while-active, show while active, evaluation + failure" — matches `spec.md §7`. Covered. +- **Documentation block** — names `internals/`, `overview.md`, + and stale-note archival. Adequate, though it does not call out + updating `internals/console.md` (which documents the reftable / + `write_to_input` semantics that this feature changes) or + `internals/examples/tixy.md` (documents `write_to_input`). + +### 5. Roadmap ordering validity + +- **Ordering defect — `oneshot` deletion (M2) strands submit + until M6.** `roadmap.md` M2 lists + "`src/userInputController.lua` — remove `oneshot` flag" and + claims "Zero behaviour change." In the current code the oneshot + flag is what gates the submit path (`userInputController.lua:346` + `… and input.oneshot then`) and what triggers the `'userinput'` + push (`userInputModel.lua:812–819`). Its replacement — + `framework_handlers['return']` in `ProjectController` owning + submit — is delivered in **M6** (`roadmap.md` M6; + `design.md §5`; `decisions.md` D-4). Therefore: + - M2 cannot both delete `oneshot` and be "zero behaviour + change"; deleting it removes submit + the `'userinput'` push. + - M3 (legacy facades) claims "all existing examples work," but + examples that submit on Enter (repl, guess, tixy) have no + working submit path between M2 and M6. + - `decisions.md` D-4 itself ties the deletion to D-4's + framework-owned submit ("deleted as a consequence of D-2 + combined with D-4"), i.e. it logically belongs with M6, not + M2. + Recommended: keep `oneshot` until the M6 submit ownership lands, + or move the submit-via-`framework_handlers` step earlier. +- **M3 depends on M6's `after_submit`.** `spec.md §5` defines each + legacy wrapper as "Registers a `compy.after_submit` callback + that fills the reftable." `after_submit` is delivered in M6. M3 + is scheduled before M4–M6. So M3 as specified depends on a + later milestone's hook. (If M3 instead keeps the existing + reftable-fill path in `UserInputController:keypressed`, that + path is exactly the `oneshot` submit path M2 removes — same + knot as above.) +- **M2 "depends on M1" is a sequencing choice, not a real + dependency.** Singleton extraction (M2) does not use the + `keys_pressed` table (M1). Harmless (linear order is safe) but + the stated dependency overstates coupling; `design.md §7`'s + "linear from 1 through 4" is conservative rather than necessary + for the 1→2 edge. +- M4→M5, M4→M6 (M6 independent of M5), M2→M7: these ordering + claims hold. + +### 6. Factual accuracy against codebase + +| # | Claim | Verdict | Note | +|---|---|---|---| +| 1 | Overlay gate `if user_input then … else … end` exists at the described location in `controller.lua` | **CONFIRMED** | `controller.lua:625–630`, inside `handlers.keypressed` (= `love.handlers.keypressed`, assigned from `love.handlers` at line 526). | +| 2 | `UserInputController:keypressed` is the shared sink for REPL/editor and overlay branches | **CONFIRMED** | Same method, branches on `app_state == 'editor'` (`userInputController.lua:362`); called by `ConsoleController` via `self.input:keypressed` (`consoleController.lua:1000`) and by the overlay via `user_input.C:keypressed` (`controller.lua:627`). | +| 3 | `oneshot` flag controls the submit path in `UserInputController` | **CONFIRMED (gate) / MISMATCH (location)** | The flag does gate submit (`userInputController.lua:346`), but it is a field of **`UserInputModel`** (`userInputModel.lua:15,49`), read as `self.model.oneshot`/`input.oneshot`. `design.md §3`, `decisions.md` D-4, and `roadmap.md` M2 all call it "the `oneshot` flag on `UserInputController`" and M2 places its removal in `userInputController.lua`; the field actually lives in `userInputModel.lua`. | +| 4 | `love.state.user_input` is set by the project input functions and read by the overlay gate | **CONFIRMED, with nuance** | Set by the internal `input()` (`consoleController.lua:576`), called by `input_text`/`input_code`/`validated_input`; read at the gate (`controller.lua:625`); cleared by `handlers.userinput` (`controller.lua:709–713`). Nuance: **`user_input()` itself does not set it** — it only allocates the reftable (`consoleController.lua:582–585`). This matters for spec §5 (next finding). | +| 5 | `ConsoleController` is wired into `love.keypressed` at startup | **CONFIRMED** | `set_love_keypressed` sets `love.keypressed` to a closure calling `CC:keypressed(k)` (`controller.lua:161–191`), invoked via `set_default_handlers`. | +| 6 | The before/after chain mechanism does not yet exist (new addition, not a refactor) | **CONFIRMED** | No `before_*`/`after_*` hooks anywhere in the controllers/model; submit/cancel run framework logic directly. | + +Additional factual issues found while verifying: + +- **MISMATCH — `assessment.md §2` and `§8`: "cancel pushes + `'userinput'`."** `assessment.md §2` (FR-2) states "on submit + or cancel, `UserInputModel:handle()` / `cancel()` pushes a + LÖVE2D `'userinput'` event (`userInputModel.lua:819`)," and + `§8` ("Cancel path") repeats that Escape's `cancel()` pushes + `'userinput'` and clears the overlay. The code shows otherwise: + `cancel()` calls `handle(false)` (`userInputModel.lua:795–798`); + in `handle`, the `'userinput'` push is reached only on the + `eval == true` *and* `self.oneshot` *and* success path + (`userInputModel.lua:809–821`). With `eval == false` the code + takes the `else ok = true` branch and **does not push**. So + Escape clears content (via `reset()`) but does **not** dismiss + the overlay. This is, in fact, exactly the "current limitation" + `design.md §5` is built to fix ("Escape clears input content but + does not dismiss the overlay") — i.e. `assessment.md` contradicts + the premise the design correctly relies on. The design is right; + the assessment statement is the error. + +- **MISMATCH — `spec.md §5`: `user_input()` ≡ `compy.show({})`.** + The legacy table maps `user_input()` to "`compy.show({})`; + returns reftable." Current `user_input()` does **not** display + an input area — it only creates the reftable + (`consoleController.lua:582–585`); the overlay appears on a + later `input_text()`/`input_code()`/`validated_input()` call + (the canonical pattern in `assessment.md §6` / NFR-4). If the + facade makes `user_input()` call `compy.show({})`, it will pop + an empty input area immediately — a behaviour change from + today's "allocate reftable, show nothing." Worth either + preserving current semantics (reftable-only) or flagging the + change explicitly. + +- **GAP — project-owned `love.keypressed`/`love.textinput` vs. + `ProjectController` slot ownership.** `spec.md §6` states + "`ProjectController:keypressed` IS the `love.keypressed` + occupant during project execution." Today, a project's own + `function love.keypressed` is installed into that exact slot via + `set_user_handlers`/`hook_if_differs` (`controller.lua:73–107`), + and several shipped examples rely on it — `pong/main.lua:315`, + `life/main.lua:109`, `paint/main.lua:387`, + `turtle/main.lua:35` (turtle uses native `love.keypressed` + *and* `input_text`). The chain does not say how a project's + native `love.keypressed`/`textinput` is invoked once + `ProjectController` owns the slot: `compy.on_key_pressed` + defaults to "noop + log" (`spec.md §3`), so unless + `ProjectController` calls the saved user handler, these + examples' keyboard input would stop working. This is a + backward-compatibility surface as real as the `input_text` + facade, but D-1 and `design.md §6` scope backward compat to the + four input functions only. + +- **MISMATCH — `write_to_input` omitted from backward compat, + but a listed must-pass example uses it.** `assessment.md §4` + names `write_to_input` (`consoleController.lua:599–605`) as the + FR-10 mechanism. It is used by `tixy` (`examples/tixy/main.lua:39`, + `vadexamples/tixy/main.lua:50`) and documented + (`internals/console.md`, `internals/examples/tixy.md`). The + backward-compat facade lists (`design.md §6`, `spec.md §5`) and + the M3 milestone cover only `input_text`/`input_code`/ + `validated_input`/`user_input`. `write_to_input` appears nowhere + in `decisions.md`/`design.md`/`spec.md`/`roadmap.md`, yet M3 + asserts "all existing examples (… tixy …) work." Either add + `write_to_input` to the facade set (it is also the natural + FR-10 live-write surface) or document its removal. + +- **CONFIRMED — dead `compy.text_input` alias.** `assessment.md` + NFR-3 and `summaries/assessment.md` claim + `compy_namespace.text_input = input_text` + (`consoleController.lua:628`) assigns `nil` because the bare + `input_text` identifier is not in scope (the function is + `project_env.input_text`, a table field, not a local/global). + Verified: there is no `local input_text` and no global; the + assignment is `nil`. The assessment's claim is accurate. + +- **Loose reference — "resets … via the same mechanism as + `evacuate_required`."** `design.md §3`, `spec.md §3`/§6, and + `decisions.md` D-7 say `compy.handlers`/callbacks reset on + project stop "via the same mechanism as `evacuate_required`." + `ConsoleController:evacuate_required` (`consoleController.lua:844–858`) + unloads the project's `.lua` modules from `package.loaded`; it + does not reset handlers or callbacks. The actual stop-time reset + happens in `stop_project_run` (`consoleController.lua:860–868`: + `clear_user_handlers()`, `love.state.user_input = nil`, …). The + cited function is the wrong reference for what the docs describe. + +### 7. Summary fidelity + +- **`summaries/requirements.md`** — Faithful. Mentions cursor + read/set and text replacement (FR-8/9/10), consistent with the + source. No invented claims. (Note: these summarised requirements + are the very ones that later drop out of design/spec — a + fidelity-clean summary of a requirement that the downstream + chain fails to deliver.) +- **`summaries/assessment.md`** — Faithful to `assessment.md`, + including the confirmed `compy.text_input` bug and the balloons + limitation. It does **not** repeat the assessment's + cancel-pushes-`'userinput'` error (dim. 6), so the summary is + actually cleaner than its source on that point — no fidelity + violation, but note the source error is not propagated. +- **`summaries/decisions.md`** — Faithful; the at-a-glance table + matches `decisions.md`'s. It carries the same `on_key_pressed` + return-value model as D-4, so it inherits (does not introduce) + the contradiction with `spec.md` (dim. 2). +- **`summaries/design.md`** — One **added claim not in the + source**: the closing parenthetical "(remark: there should be + path to keep current behaviour, opt-in -- possible with new + architecture but not specified now)" on the Escape fix. This + opt-in-to-preserve-current-Escape idea does not appear in + `design.md` or `spec.md`. It reads as an editorial margin note + and is a useful flag, but per the fidelity criterion it is + content present in the summary and absent from the full + document. Recommend promoting it into `design.md`/`spec.md` as a + real open item or removing it from the summary. +- **`summaries/spec.md`** — Faithful to `spec.md`, including the + `user_input() ≡ compy.show({})` mapping (so it inherits that + factual issue, dim. 6) and the `on_text_entered(text, + keys_pressed)` signature. Its three-level-dispatch box says + "Return truthy at any level to consume," which matches + `design.md` but contradicts `spec.md §3`'s "Return value is + ignored" for `on_key_pressed` — i.e. the summary mirrors the + source spec's own internal inconsistency rather than introducing + a new one. +- **`summaries/roadmap.md`** — Faithful; milestone table and + estimate figures match `roadmap.md`. + +--- + +## Summary of actionable items + +Ordered roughly by blocking severity. + +1. **Restore FR-8/FR-9/FR-10 to the design and spec, or + explicitly de-scope them.** Add `compy.*` surface for cursor + query, cursor set (while active), and live text write — or + record a decision dropping them and update `requirements.md` + /traceability accordingly. As written they are required, + acknowledged in assessment, claimed in `design.md §1` scope, + and then absent from `spec.md`/`roadmap.md`; FR-9 is actively + contradicted by `compy.configure`'s contract. (dims. 1, 3, 4) + +2. **Fix the M2/M6 submit-path ordering.** Keep `oneshot` (or its + submit/`'userinput'`-push behaviour) until + `framework_handlers['return']` lands, so submit and the legacy + reftable fill keep working across M2–M5. Reconcile with M2's + "zero behaviour change" and M3's "all examples work," and with + D-4 tying the deletion to framework-owned submit. (dims. 2, 5) + +3. **Resolve the `compy.on_key_pressed` return-value + contradiction.** Decide whether it is a consuming dispatch + level (D-4 / `design.md §4`) or a passive notifier whose return + is ignored (`spec.md §3`), and make all three documents agree. + (dim. 2) + +4. **Specify how a project's native `love.keypressed`/ + `love.textinput` interacts with `ProjectController` owning the + slot.** Shipped examples (pong, life, paint, turtle) rely on + native handlers. Define whether `ProjectController` invokes the + saved user handler, or document the migration to + `compy.on_key_pressed` as a backward-compat break. (dim. 6) + +5. **Add `write_to_input` to the backward-compat facade set (or + document its removal).** `tixy` — listed as a must-pass example + in M3 — depends on it; it is also the natural FR-10 live-write + surface. (dims. 1, 6) + +6. **Specify the `on_key_pressed` suppression mechanism.** Define + how the framework distinguishes character-producing keypresses + (suppress) from non-character ones (emit), given LÖVE delivers + `keypressed` before `textinput`. This is D-6's load-bearing + guarantee and is currently deferred without a mechanism. + (dim. 3) + +7. **Fix the combo-string examples to match the alphabetical sort + rule, or define modifier-priority ordering.** `"lalt+lshift+f4"` + and `compy.handlers['lctrl+l']` do not match the + alphabetical-sort serialiser the spec describes; handlers + registered per the examples would never fire. (dim. 3) + +8. **Correct the codebase-reference errors so the assessment + matches the design's premise:** `cancel()` does not push + `'userinput'` (Escape does not dismiss today — which is the + point of the feature); the `oneshot` flag is a `UserInputModel` + field, not a `UserInputController` one (update D-4, `design.md + §3`, and M2's file target); and the stop-time reset is + `stop_project_run`/`clear_user_handlers`, not + `evacuate_required`. (dims. 2, 6) + +9. **Deliver the D-7 FR-11/FR-12 walkthrough promised for + `design.md`,** or relocate the promise to + `notes/editor_repl_input.md` and have D-7 cite it there. + (dim. 2) + +10. **Reconcile the smaller drifts:** `user_input() ≡ + compy.show({})` (current `user_input()` shows nothing); + `on_text_entered` second arg (`mods` subset per D-6 vs full + `keys_pressed` proxy per spec); `isrepeat` presence on + `on_key_pressed`; `compy.handlers[combo]` co-firing with + `on_text_entered` for character combos; exposure milestones + for `compy.show`/`compy.hide`; and the + `summaries/design.md` Escape opt-in remark not present in the + source. (dims. 1, 2, 3, 4, 7) diff --git a/doc/development/wip/77-new-input-api/validation/validation_report_2.md b/doc/development/wip/77-new-input-api/validation/validation_report_2.md new file mode 100644 index 00000000..33cdc96d --- /dev/null +++ b/doc/development/wip/77-new-input-api/validation/validation_report_2.md @@ -0,0 +1,425 @@ +# Validation Report 2 — Feature #77 Document Chain (post-rewrite) + +*Second independent review of the feature #77 chain, run against the +rewritten documents and the intended delta in +`validation/recommendations_1.md` (with `validation/changelog_1.md` as the +rewrite's self-report). Codebase claims were re-checked against `src/`. +Reviewer role only — findings, not rewrites. Authority model applied: +`input.md` is the only stakeholder-signed tier; `requirements.md` is high +local input; `decisions.md` (D-1…D-9) and the recommendations are one local +proposal; `design.md`/`spec.md`/`roadmap.md`/`summaries/` are derived.* + +--- + +## Status: PASS WITH NOTES + +The three round-1 blockers are resolved and the resolutions are faithfully +applied across the documents the recommendations named: + +- **FR-8/9/10** are restored end to end (D-8, `design.md §3`, `spec.md §2`, + `roadmap.md M7`, summaries), on a single 2D source-line coordinate space; + `write_to_input` is back in the facade set. +- **The M2/M6 submit-path ordering** is corrected — `oneshot` now stays + through M2–M5 and is deleted in M6, against the right file targets. +- **The `on_key_pressed` return-value contradiction** is dissolved by the + three-tier model (sink = default of `on_key_pressed`). + +Native-handler coexistence (D-9), the two-channel model (D-6 superseded), the +combo canonical form (D-3), the codebase-reference fixes (Item 8), and the +FR-11/FR-12 walkthrough (Item 9) all landed. The namespace relocation to +`compy.input.*` is clean in the derived docs. + +No remaining issue drops a functional requirement, breaks a must-pass +example, or hands the implementor a contradictory *design*. What remains is +consistency residue and provenance/packaging gaps. One of them +(the `noop+log` vs `sink` default, **N1**) directly contradicts the headline +Item 3 resolution in three places and is worth a wording fix before +implementation; the rest are notes. None require a full re-validation round — +a short local cleanup pass closes them. + +--- + +## Part A — Rewrite faithfulness (per recommendation item) + +| Item | Verdict | Location / note | +|---|---|---| +| 1 — FR-8/9/10 + `write_to_input` | **APPLIED** | see below | +| 2 — `oneshot` M2→M6 | **APPLIED** | see below | +| 3 — three-tier collapse | **APPLIED (residue)** | see N1 | +| 4 — native coexistence | **APPLIED** | see below | +| 5 — `write_to_input` | **APPLIED** | folded into Item 1 | +| 6 — two channels | **APPLIED (one stale source)** | see below | +| 7 — combo canonical form | **APPLIED** | see below | +| 8 — codebase corrections | **APPLIED** | see below | +| 9 — FR-11/12 walkthrough | **APPLIED** | `design.md §7` | +| 10 — smaller drifts | **APPLIED (10.7 deviation)** | see below | +| Namespace relocation | **APPLIED (in-scope docs); two gaps** | see below | + +**Item 1 — APPLIED.** `design.md §3` component table carries +`compy.input.get_cursor/set_cursor/set_text` (lines 172–174); `spec.md §2` +has the `{line, col}` `cursor` field (line 97) and full `get_cursor` / +`set_cursor` / `set_text` sections (138–162); `spec.md §5` and `design.md §6` +add `write_to_input` as a facade over `set_text` with reftable-only +`user_input()`; `roadmap.md M3` wires `write_to_input`, `M7` adds the cursor +surface plus the `keep_cursor` model fix. The 2D source-line contract is +stated uniformly, and the `set_text`/`jump_end` model wrinkle is correctly +identified (verified: `userInputModel.lua:145` calls `jump_end()` +unconditionally, after the `keep_cursor` guards at `:135,:142` — so the fix is +real and correctly scoped to M7). No over-application: no extra cursor +config beyond what FR-8/9/10 require. + +**Item 2 — APPLIED.** `roadmap.md M2` drops the `oneshot` removal and states +it "continues to drive submit through M2–M5" (lines 51–53), adds the `result` +repointing setter (60–62); `M3` notes the reftable fill uses the existing +oneshot path and does not depend on `after_submit` (84–86); `M6` carries the +deletion with `userInputModel.lua` as the field home and +`userInputController.lua` as the submit-path reader (179–187). `decisions.md` +D-4 annotation matches (288–299). File targets correct against code +(`userInputModel.lua:15,49`). + +**Item 3 — APPLIED, with a reintroduced default-value contradiction (N1).** +The three-tier model is in `design.md §4` (sink as default of +`on_key_pressed`, `sink` argument dropped from `dispatch()` at lines 218–221), +`spec.md §3` (default = sink; "return value is ignored" and the same-frame +suppression both removed), and both summaries. The collapse itself is faithful +and no fourth tier reappears in the dispatch sections. **But three places still +describe the pre-Item-3 `noop+log` default for the generic callback** — see N1. + +**Item 4 — APPLIED.** Auto-provisioning, the legacy heuristic gate +(native handler set AND no `compy.*` surface), the visibility lifecycle-split +wrapper, and the debug diagnostics are in `design.md §2` (83–94) and `§6` +(315–324), `spec.md §6` (415–433), and D-9. The routing precondition is +factually sound (`controller.lua:75` `hook_if_differs`, `:779` +`save_user_handlers`). No over-application: the mechanism is fixed and the only +variable (wrapper body) is the visibility split, as recommended. + +**Item 6 — APPLIED; one stale source doc.** `spec.md §3` removes suppression, +states the two independent channels, and adds the bare-printable-key note +(267–271); D-6 is superseded (351–371). **Residue:** `assessment.md §8` +(lines 352–362) still presents the modifier+character co-occurrence as an open +choice — "suppress one, suppress both, or emit both — is a decision that needs +to be made explicit in design." D-6's supersession made that decision (emit +both, no exclusivity); the assessment paragraph was not re-pointed at the +resolution. Low severity (assessment is a derived/input doc and this fed D-6), +but it now reads as an unresolved question that is in fact resolved. See also +architect note 5 below. + +**Item 7 — APPLIED.** `spec.md §1` has the modifier-first precedence +(`ctrl, alt, shift, gui`) + triggering key, generic l/r folding, the +metatable `__newindex` normalisation, and the overloadable matcher; examples +are corrected to `"ctrl+s"`, `"alt+shift+f4"`, `"escape"` (62–67). +`design.md §4` and `roadmap.md M5` (`"ctrl+s"`, line 139) match. No stale +alphabetical/`lctrl+`-style example survives in the derived docs. +(`decisions.md:224` still shows `"lctrl+s"` in the **original** D-3 suggested +text — corrected by the appended annotation; see the decisions-body note +below. `summaries/spec.md:20` uses `lctrl+s` only as a negative example — +"`ctrl+s` not `lctrl+s`" — which is correct.) + +**Item 8 — APPLIED.** `assessment.md §2` and `§8` now state `cancel()` does +not push `'userinput'` and the overlay stays on Escape (verified: +`userInputModel.lua:795–798` cancel → `handle(false)`; the `'userinput'` push +at `:812–819` is gated by `eval` + `oneshot` and is unreachable on cancel). +`oneshot` is relabelled a `UserInputModel` field in D-4 and `design.md §3`. +Every `evacuate_required` reference in the chain is gone — `stop_project_run` / +`clear_user_handlers` is used in `design.md §3`, `spec.md §3`, D-7 +(`consoleController.lua:860,867` confirmed). No `evacuate_required` remains in +any chain document. + +**Item 9 — APPLIED.** `design.md §7` carries the console-REPL and editor +walkthrough tables (366–387); D-7 is annotated to point there. + +**Item 10 — APPLIED (10.7 deviation).** 10.1 `user_input()` reftable-only +(`spec.md §5`, `design.md §6`, summary); 10.2 full `keys_pressed` proxy as the +`on_text_entered` second arg; 10.3 `isrepeat` trailing on +`on_key_pressed`; 10.4 `compy.input.show`/`hide` exposed in M2; 10.6 the orphan +Escape opt-in remark is removed from `summaries/design.md` (confirmed gone). +**10.7 deviation:** the dead `compy.text_input` cleanup line was not added to a +roadmap docs block; `changelog_1.md` open-question 1 acknowledges this as a +deliberate scope call (a `src/` one-liner, not a chain-doc concern). Acceptable; +trivial. + +**Namespace relocation — APPLIED for the in-scope docs, with two gaps.** +`design.md`, `spec.md`, `roadmap.md`, and `summaries/{design,spec,roadmap}.md` +carry no flat-`compy.X` feature-#77 straggler (grep-clean); legacy globals +(`input_text`, `input_code`, `validated_input`, `user_input`, +`write_to_input`) are not moved; `compy.keys_pressed` stays global. Two gaps: + +1. **`decisions.md` was excluded from the pass** (per `changelog_1.md`), so it + still uses flat `compy.on_key_pressed` / `compy.handlers` / `compy.get_cursor` + etc. throughout, **except** `decisions.md:398`, which leaked a single + `compy.input.handlers`. So `decisions.md` is internally inconsistent (one + relocated name amid otherwise-flat names) and chain-inconsistent with the + derived docs (which a stakeholder reads side by side). `summaries/decisions.md` + mirrors the flat form (lines 87–91, 163). Defensible under the literal scope + table, but it leaves the document the stakeholder actually reviews using the + old names. Low severity; see also the Part B namespace-decision gap, which is + the more substantive half of this. +2. The relocation is otherwise clean. + +--- + +## Part B — Provenance integrity + +- **Top-of-file status note: present.** `decisions.md:3–7` and + `summaries/decisions.md:7–9` both state the whole file (D-1…D-9) is a local + proposal derived from `input.md`, pending a single eventual approve/veto + review, nothing stakeholder-signed. Correct and matches the authority model. +- **Round-1 provenance marker: present and greppable.** Six occurrences of + `*(Origin: local design round 1, 2026-06. See …Item N.)*` (D-3, D-4, D-6, + D-7, D-8, D-9). The delta since the prior round is auditable. +- **New architectural commitments surfaced as decisions — mostly, with one + omission.** D-8 (cursor contract + live surface) and D-9 (native coexistence) + are first-class new decisions; Item 3 (three-tier), Item 6 (two-channel), and + Item 7 (combo form) are carried as D-3/D-6 annotations. **The + `compy.input.*` sub-namespace is not surfaced as a decision anywhere in + `decisions.md`** (grep: no `namespace` / `compy.input` decision text). It is a + genuine architectural commitment — a new sub-namespace layout chosen by the + architect, with a stated rationale (isolate the input surface; keep + `compy.keys_pressed` global) — yet a stakeholder reading only `decisions.md` + would not see it, and would see the old flat names. This is the inverse of the + round-1 failure mode (a real choice living *only* in derived docs). Recommend + promoting it to a decision (e.g. D-10) so the eventual review covers it. This + is the most substantive provenance finding. +- **`requirements.md` still traces `input.md`: yes.** The file is unchanged + this round and no downstream edit contradicts it; the FR-8/9/10 restoration + re-closes the only place the chain had drifted from it. Minor carry-over + (not introduced this round): FR-2's "remove/teardown" is still answered by + `hide()` without that mapping being stated as the FR-2 answer + (`changelog_1.md` open-question 2). Trivial. + +--- + +## Part C — Seven dimensions + +### 1. Requirements coverage + +All twelve FRs trace through decisions → design → spec → roadmap. FR-8/9/10 — +the round-1 headline gap — are now carried: D-8 → `design.md §3` → +`spec.md §2` (`get_cursor`/`set_cursor`/`set_text`) → `roadmap.md M7`. FR-9 is +no longer contradicted: `configure()` keeps its text/cursor immutability, and +`set_cursor`/`set_text` are declared the explicit live-write exceptions +(`spec.md §2`, lines 129–130, 152–162). FR-5/6/7, FR-11/12, NFR-1…4 remain +covered. No requirement drops out. + +### 2. Decision consistency + +D-1…D-9 are reflected consistently in design/spec, with one reintroduced +contradiction: + +- **N1 — `on_key_pressed`/`on_text_entered` default: `noop+log` vs `sink`.** + Item 3/Item 6 make the *sink* the default of both channel callbacks. The + per-callback spec sections say so (`spec.md §3`, lines 222–228 and 188–191). + But three places still carry the old `noop+log` default: + - `design.md §2` routing diagram, line 75: + `compy.input.on_key_pressed [generic, overloadable, default: noop+log]`; + - `spec.md §3` intro, line 175: "Default value for each is a no-op function + that emits a debug log entry"; + - `summaries/spec.md`, line 98: "All callbacks default to a no-op + debug + log entry." + + Taken literally these contradict the sink-as-default model and would have the + implementor wire `on_key_pressed` to a no-op — under which the default text + editing (the sink) never runs and typing into a prompt does nothing. The + `noop+log` default is correct for the *other* callbacks + (`before/after_submit`, `before/after_cancel`, `on_limit_reached`); the + blanket statements over-generalise it to the two channel callbacks, whose + default is the sink. Fix: scope the `noop+log` statement to the non-channel + callbacks; correct line 75 and the two blanket lines to "default = sink" for + the channel callbacks. Medium severity (contract-level, but a localised + wording fix in three spots, not a design gap). This is the only Part A/C + finding I would treat as more than a note. + +- Minor: `design.md §3` component table calls `compy.input.on_key_pressed` + a "Generic non-character key callback" (line 165). Under Item 6 it fires for + *all* keypressed events (character and non-character); `spec.md §3` says so + correctly (line 206). The design table description is stale framing from the + pre-two-channel model. Low. + +### 3. Design-to-spec completeness + +Every design component has a spec contract: `show`/`hide`/`configure`/`clear`, +`get_cursor`/`set_cursor`/`set_text`, `handlers[combo]`, the channel +callbacks, the before/after chains, `on_limit_reached`, the `ProjectController` +slot, and native coexistence all have signatures, hidden-state behaviour, and +wrong-state behaviour (`spec.md §2,§3,§4,§6,§7`). The earlier "design claims a +surface the spec lacks" gap is closed. The only design↔spec mismatch is N1 +(the default-value wording) and the §3-table description note above. + +### 4. Spec-to-roadmap coverage + +Every spec surface has a milestone home, and the previously-missing homes now +exist: `compy.input.show`/`hide` exposure is in M2 (lines 56–63); the +cursor/live-write surface and the `set_text` model fix are in M7 (196–224); +the `write_to_input` facade is in M3 (74, 83–84). Documentation and test +blocks (lines 231–256) cover the dispatch levels, lifecycle, legacy +compatibility, and the `spec.md §7` edge cases. + +### 5. Roadmap ordering validity + +Each milestone is testable before the next. The M2/M6 `oneshot` defect is +resolved: M2 is now genuinely "zero behaviour change" (oneshot retained), M3's +"all examples work" holds (reftable fill via the retained oneshot path, no M6 +dependency), and the deletion is co-located with its replacement in M6. M7 +correctly depends only on M2 (not M5/M6). Ordering claims hold. + +One packaging gap (not an ordering defect): **the estimates were not redone +after the round-1 scope shifts** (architect note 2). M7 in particular grew — +it now adds `get_cursor`/`set_cursor`/`set_text` plus the `UserInputModel:set_text` +`keep_cursor` fix — yet its estimate is unchanged at 2 h / 3 h +(`roadmap.md:274,303`); M3 gained the `write_to_input` facade and M2 gained the +`result` setter and `compy.input` table creation, also at unchanged figures. +Work also moved between M2 and M6 (oneshot). The totals (41 h / 23 h PERT) are +carried over from take 1. Recommend a re-estimate, or at least a note that the +figures predate the round-1 scope changes and skew low on M7. + +### 6. Factual accuracy against codebase + +Re-verified the round-1 fixes against `src/`: + +- `cancel()` does not push `'userinput'` — confirmed + (`userInputModel.lua:795–798`, push gated at `:809–821`). Assessment now + states this. +- `oneshot` is a `UserInputModel` field — confirmed + (`userInputModel.lua:15,45–49`). D-4/`design.md §3`/M6 now say so. +- Stop-time reset is `stop_project_run` / `clear_user_handlers` — confirmed + (`consoleController.lua:860,867`). No `evacuate_required` reference remains. +- Native slot interception via `hook_if_differs` / `save_user_handlers` — + confirmed (`controller.lua:75,779`); D-9 describes it correctly. +- `set_text`'s unconditional `jump_end()` — confirmed + (`userInputModel.lua:145`); the D-8/M7 model fix is correctly identified. + +No remaining codebase mismatch found. + +### 7. Summary fidelity + +`summaries/{design,spec,roadmap,decisions,assessment,requirements}.md` each +represent their (edited) source faithfully. The orphan Escape opt-in remark is +removed from `summaries/design.md`. Two carry-throughs, not new violations: +`summaries/spec.md:98` carries the same `noop+log` blanket statement as its +source (N1 — mirrored, not introduced); `summaries/decisions.md` carries the +flat `compy.*` names of its (excluded-from-relocation) source. Neither invents +a claim absent from the source. + +--- + +## Part D — New conflicts and open questions + +**New cross-document contradiction introduced by the rewrite:** one — N1, the +`noop+log` vs `sink` default, now present in `design.md §2`, `spec.md §3`, and +`summaries/spec.md`. It is residue from the pre-Item-3 model that the dispatch +sections corrected but the diagram/intro lines did not. + +**`decisions.md` body vs annotations.** The decisions file preserves each +original "Suggested decision" verbatim and appends a correcting annotation. +For D-3 this means the body still describes the rejected four-tier model +("`framework_handlers → compy.handlers → compy.on_key_pressed`, with +`UserInputController:keypressed` as the terminal sink below all three", +lines 220–221), the alphabetical/`lctrl+s` example (224), and the +`_on_key_pressed(k, pressed, isrepeat)` signature (219); the three-tier +correction, modifier-first form, and `on_key_pressed(k, keys, isrepeat)` +signature live only in the appended annotation. A reader who stops at the body +gets the superseded model. Acceptable as a decision-history style, but worth a +one-line "superseded — see annotation below" marker at each corrected body so +the reconciliation is not left to the reader. Low. + +**Changelog open questions — adjudication.** +1. *Dead `compy.text_input` cleanup line not added to the roadmap* — acceptable + to leave open. It is a `src/` one-liner already documented as a bug in + `assessment.md`; it does not block implementation. Optionally fold into M5/M7 + docs block. +2. *FR-2 → `hide()` mapping not stated explicitly* — acceptable to leave open; + trivial to add a sentence in `requirements`/`design` traceability, not + blocking. + +**Future-seam items correctly kept out of scope.** The overloadable matcher +(Item 7) is described as an extension seam, not built; the Discord-style +text-command-set prefix matching (Item 6) is not treated as in-scope (it is +absent from spec/roadmap, which is correct). No "future seam" item was wrongly +pulled into the build. + +**Architect's already-spotted inconsistencies — status.** +1. *Flat `compy.*` in design/spec* — **resolved.** No flat feature-#77 + straggler remains in `design.md`/`spec.md`/`roadmap.md` or their summaries + (the residue is only in `decisions.md`, which was excluded from the pass — + see Part A namespace and Part B). +2. *Roadmap not re-estimated* — **open** (see dimension 5). +3. *`on_text_input` should follow the same principles as `keypressed`* — + **under-emphasized.** `design.md §4` (204–206) and D-6 state the textinput + default is the textinput sink and the channel mirrors keypressed, but only in + a sentence each; the symmetry is not drawn out (e.g. the spec gives + `on_key_pressed` a full tier discussion and `on_text_entered` a short + section). Worth a short explicit "the textinput channel follows the same + default-sink/override principle" note in `decisions.md`/`spec.md`. Low. +4. *Optional `mods` trailing string on `keypressed`/`text_input`* — **not + reflected.** The chain passes the `keys_pressed` read-only proxy as the + second argument (modifiers derivable from it) and trails `isrepeat`; there is + no pre-folded `mods` string argument anywhere in decisions/design/spec. If + the design-session intent was to add a convenience `mods` string (folded like + the combo form) as a trailing argument to downstream handlers/callbacks — so + a combo handler shared across combos, and the post-combo callback, can read + the modifier context without walking the proxy — that intent is currently + absent. Either record it as a deliberate decision (proxy is sufficient, + `mods` not added) or add it as a tracked future seam; right now it is neither. + Low–medium, because it is an unrecorded design-session output rather than a + doc inconsistency. +5. *Assessment/decisions still operate the original seven blocking points* — + **partially addressed, partially open.** The new commitments did get + surfaced as decisions (D-8, D-9) or annotations (D-3, D-6), so round-1 + controversies are not entirely omitted. But: (a) the `decisions.md` + "blocking decisions" prose intro (lines 9–32) still enumerates *seven* + questions while the file now carries nine decisions — D-8/D-9 are absent from + that list and the quick-reference is the only place they appear alongside + D-1…D-7; (b) the namespace decision is not surfaced at all (Part B); + (c) `assessment.md §8` still frames the co-occurrence question as open + (Item 6 note). So the framing has not fully caught up with the round-1 + resolutions. Recommend updating the intro list to nine and adding the + namespace decision. +6. *Spec/roadmap should flag they are derivative, pre-built assuming + endorsement* — **not addressed.** `decisions.md` and `summaries/decisions.md` + carry the proposal-status note, but `spec.md` and `roadmap.md` headers do not + (verified). Per the authority model both are derived/provisional; the + architect's suggested disclaimer (part of the proposal chain, not frozen, + reviewable/changeable without blocking implementation — "we're a startup") + is absent. Recommend a one-paragraph header note on each. Low–medium. + +--- + +## Summary of actionable items + +None are blocking in the round-1 sense (no dropped FR, no broken example, no +contradictory *design*). Ordered by severity; (1) is the one worth fixing +before implementation, the rest are cleanup. + +1. **Resolve the `noop+log` vs `sink` default contradiction (N1).** Correct + `design.md §2` line 75, `spec.md §3` line 175, and `summaries/spec.md` + line 98 so the `noop+log` default is scoped to the + submit/cancel/limit callbacks, and the two channel callbacks + (`on_key_pressed`, `on_text_entered`) read "default = sink." Medium. +2. **Surface the `compy.input.*` namespace as a decision** (e.g. D-10) so the + stakeholder review covers it, and reconcile `decisions.md` naming + (the lone `compy.input.handlers` at line 398 vs the otherwise-flat names) — + decide whether `decisions.md` adopts `compy.input.*` or states explicitly + that it predates the relocation. Medium (provenance). +3. **Re-estimate the roadmap** (or annotate the estimates as pre-round-1), + M7 in particular. Low–medium. +4. **Adjudicate the `mods` trailing-string idea** (architect note 4): record it + as decided-against or as a tracked future seam. Low–medium. +5. **Add the derivative/proposal-chain disclaimer to `spec.md` and + `roadmap.md`** headers (architect note 6). Low–medium. +6. **Minor cleanups:** update the `decisions.md` "seven" intro to nine and mark + superseded D-3 body text; re-point `assessment.md §8` at D-6's resolution; + fix the `design.md §3` "non-character key callback" description; draw out the + textinput-mirrors-keypressed symmetry (architect note 3). Low. + +## Recommendation + +**Ready for stakeholder review, after a short local cleanup pass** (items 1–2 +above, optionally 3–6). The chain is now substantively coherent and faithful to +`recommendations_1.md`; the round-1 blockers are closed and verified against the +codebase. The remaining items are wording/provenance/packaging, not design +re-decisions, and do not warrant a full re-validation round. If the cleanup +pass is run, items 1 and 2 should be in it (N1 because it contradicts a core +resolution and could mislead the implementor; the namespace decision because the +stakeholder is about to review a proposal whose final API names are not in the +decision document). A `validation/recommendations_2.md` is not warranted — the +list above is small and unambiguous — but can be produced on request. diff --git a/doc/development/wip/77-new-input-api/validation/validation_report_3.md b/doc/development/wip/77-new-input-api/validation/validation_report_3.md new file mode 100644 index 00000000..88bb5df5 --- /dev/null +++ b/doc/development/wip/77-new-input-api/validation/validation_report_3.md @@ -0,0 +1,328 @@ +# Validation Report 3 — Feature #77 Document Chain (post round-2 cleanup) + +*Third independent review of the feature #77 chain, run against the +documents as they stand after local design round 2. The intended delta is +`validation/recommendations_2.md` (six items); `validation/changelog_2.md` +is the round-2 self-report; `validation/validation_report_2.md` was the +PASS-WITH-NOTES review round 2 set out to close. Codebase claims re-checked +against `src/`. Reviewer role only — findings, not rewrites. Authority model +applied: `input.md` is the only stakeholder-signed tier; `requirements.md` is +high local input (unedited); `decisions.md` (D-1…D-10) and both recommendation +rounds are one local proposal; `design.md`/`spec.md`/`roadmap.md`/`summaries/` +are derived.* + +--- + +## Status: PASS WITH NOTES + +Round 2 was a targeted cleanup of a chain already at PASS WITH NOTES, and it +landed that way. All six `recommendations_2.md` items are applied; the round-1 +resolutions are intact; the load-bearing codebase facts still hold. No +functional requirement regressed, no example broke, and no new blocking +cross-document contradiction was introduced. The headline N1 default +contradiction — the one item report 2 flagged as worth fixing before +implementation — is closed in all three places it appeared. + +Two items of residue remain, both notes, neither blocking: + +- The top-of-file **status-note parenthetical still reads "(D-1…D-9)"** in + `decisions.md:3` and `summaries/decisions.md:7`, although the file now + carries D-10 (the narrative intro *was* updated; only the header + parenthetical was missed). A two-character staleness, internal to the + decisions doc. +- The **in-file round-2 provenance marker is thin** — exactly one greppable + `local design round 2` occurrence (`decisions.md:395`), versus six for + round 1. The full round-2 delta is auditable via `changelog_2.md`, but the + in-document marker is sparser than the round-1 marker it is meant to mirror. + +Both are sub-trivial wording fixes that can be folded into any later edit; +neither warrants a re-validation round. + +--- + +## Part A — Round-2 faithfulness + +### Item 1 — Default contradiction (N1) · **APPLIED** + +The `noop+log`-vs-`sink` default is fixed in all three named places and reads +consistently across the chain: + +- `design.md §2` routing diagram (line 75): + `compy.input.on_key_pressed [generic, overloadable, default: sink]` — the + `noop+log` annotation is gone. +- `spec.md §3` intro (lines 182–187): scoped correctly — + "The submit/cancel/limit callbacks (`before_submit`, `after_submit`, + `before_cancel`, `after_cancel`, `on_limit_reached`) default to a no-op + function that emits a debug log entry. The two **channel callbacks** + (`on_key_pressed`, `on_text_entered`) … default value is the text-editing + sink." +- `summaries/spec.md` (lines 98–100): same scoping. + +The two channel callbacks read **default = sink** everywhere they are +described: `design.md §3` table (165–166), `design.md §4` (189–211), +`spec.md §3` per-callback sections (212–217 `on_text_entered`, 247–253 +`on_key_pressed`), `summaries/spec.md` (90, 99–100). The +submit/cancel/limit callbacks read **default = noop+log** (`spec.md §3` 183–184, +`summaries/spec.md` 98). A grep for `noop+log` / blanket "no-op default" turns +up only the two correctly-scoped statements — no surviving conflation. No +over-application (no new tier or callback introduced by the wording change). + +### Item 2 — Namespace surfaced as D-10 + `decisions.md` naming · **APPLIED** + +- **D-10 present** in `decisions.md` as a full per-decision entry + (562–609: Question / Context / Affects / Source / Decision) and a + quick-reference row (line 154), and in `summaries/decisions.md` as the + glance row (172) plus the intro note (35–40). +- **Relocation grep-clean.** No flat feature-#77 straggler + (`compy.on_key_pressed` / `compy.handlers` / `compy.get_cursor` / `compy.show` + …) remains in `decisions.md` or `summaries/decisions.md`; the round-1 leaked + `compy.input.handlers` outlier is no longer an outlier (the whole file now + speaks `compy.input.*`). No double `compy.input.input`. +- **`compy.keys_pressed` left global** (D-10, 593–596; no + `compy.input.keys_pressed` leak anywhere). **Legacy globals unchanged** + (D-10, 597–600: `input_text`/`input_code`/`validated_input`/`user_input`/ + `write_to_input` stay flat, only their internal call targets move). + +### Item 3 — Roadmap re-estimate (3-point PERT) · **APPLIED** + +`roadmap.md §Estimates` (275–315) is now a genuine three-point table — +Optimistic / Most-likely / Pessimistic with a per-row and project +PERT = (O+4M+P)/6. Recomputed spot rows check out: + +- Without LLM: M4 (4,8,14) → (4+32+14)/6 = **8.3** ✓; M6 (4,7,11) → + (4+28+11)/6 = **7.2** ✓; Tests (7,11,16) → (7+44+16)/6 = **11.2** ✓. + Totals O=32 / M=58 / P=89; project PERT (32+232+89)/6 = 58.8 ≈ **59 h** ✓. +- With LLM: totals O=19 / M=34 / P=54; project PERT (19+136+54)/6 = 34.8 ≈ + **35 h** ✓. + +The internal estimate-revision history narrative is gone (grep for +take-1/revised/round-1-scope is clean). `summaries/roadmap.md` matches +(≈ 59 h / ≈ 35 h, with the three-point-PERT note, line 38). + +*Note (not a defect):* the headline figure differs from `recommendations_2.md` +Item 3's suggested ≈ 45 h / ≈ 26 h. The executor read recommendations_2's +two-point "Optimistic 32 / Realistic 58" as Optimistic + **Most-likely**, then +added a pessimistic tail (P=89), rather than treating 58 as the pessimistic +bound the way recommendations_2 had. That shifts the most-likely from 45 → 58 +and the PERT from 45 → 59. This is a defensible, methodology-correct reading — +and it is exactly the "genuine three-point PERT, not a two-point table whose +PERT collapsed to the most-likely" the prompt asked for. Roadmap and summary +agree on the new figure, so the chain is internally consistent; the deviation +is from the recommendation's *number*, not from its *intent*. + +### Item 4 — `mods` modifier-string · **APPLIED** (recorded as seam, not built) + +- `decisions.md` D-6 (395–408): round-2-marked "Future seam — `mods` string + (not built in v1)" note; states v1 ships the `keys_pressed` proxy only, + pairs the `mods` string with the matcher / text-command-set seams, and + **flags it for architect confirmation**. +- `spec.md §3` (222–227): matching italic "Future seam (not built in v1)" note + under `on_text_entered`, cross-referencing D-6. + +Confirmed **no `mods` argument was added to any live signature.** Every `mods` +token in the chain is either the superseded D-6 original-body text +(`decisions.md:361`, the historical `on_text_entered(text, mods)` that is +explicitly marked superseded) or a seam/clarification note (`decisions.md` +385/396/398/404, `spec.md` 222/226). Live signatures remain +`on_key_pressed(k, keys, isrepeat)` and `on_text_entered(text, keys_pressed)`. +No over-application. + +### Item 5 — Disclaimers · **APPLIED** + +- `spec.md` header (9–14): "Status — derived proposal document … pre-built on + the assumption the design … is endorsed … detail is not frozen … no + requirement to freeze the spec before work starts." +- `roadmap.md` header (8–13): matching note, explicitly extending the + not-frozen clause to milestone boundaries and estimates. + +### Item 6 — Minor cleanups · **APPLIED** (one residual sub-point — see note) + +- **6.1 intro count.** Narrative intro reconciled to the D-1…D-10 set + (`decisions.md` 34–43 and `summaries/decisions.md` 35–40: "Seven questions … + Three further commitments … D-8, D-9 from round 1, D-10 from round 2 … not + new stakeholder questions"). D-3 body carries the superseded-in-part lead-in + (`decisions.md` 229–231); D-6 body already carried "Suggested decision + (original)" + "Superseded" (356, 366) and is reinforced by the round-2 + channel-symmetry note. + **Residual:** the *status-note parenthetical* at `decisions.md:3` and + `summaries/decisions.md:7` still reads "(D-1…D-9)". The intro prose caught up + to D-10; this one header line did not. Note-level (see Part C). +- **6.2 assessment §8.** Re-pointed at the resolution: `assessment.md` 364–370 + adds the closing paragraph noting the co-occurrence was resolved by D-6's + round-1 supersession (both channels fire independently; no exclusivity; "this + paragraph records the original gap; D-6 carries the settled resolution"). + The paragraph no longer reads as open. +- **6.3 design §3 table wording.** `compy.input.on_key_pressed` is now + "Generic keypressed callback (fires for all keys; default = sink)" + (`design.md:165`); `on_text_entered` row notes "default = textinput sink" + (166). The stale "non-character key callback" framing is gone. +- **6.4 textinput-mirrors-keypressed symmetry.** Made explicit in + `design.md §4` (204–211), `spec.md §3` (212–217, "This mirrors + `on_key_pressed` exactly … the same default-sink/override principle … the + only difference is that the textinput channel has no combo tier above it"), + and `decisions.md` D-6 "Channel symmetry" paragraph (387–393). + +**Over-application sweep (whole round):** no new dispatch tier, callback, config +field, or abstraction appeared. The three-tier model is unchanged; the `mods` +idea is a seam note only; the namespace relocation is names-only. Clean. + +--- + +## Part B — No regression of round-1 resolutions + +| Round-1 resolution | Status | Evidence | +|---|---|---| +| FR-8/9/10 carried end to end | **Intact** | D-8 (decisions 463–510); `design.md §3` table 172–174 + §7 walkthrough 389–391; `spec.md §2` 145–168; `roadmap.md M7` 204–230 | +| Three-tier model; no fourth tier | **Intact** | `design.md §4` 178–211 (sink = default of `on_key_pressed`, no tier below); `summaries/decisions.md` 93–98. §3 default-wording edit did not reintroduce a sink tier | +| M2/M6 `oneshot` ordering; field on `UserInputModel` | **Intact** | `roadmap.md M2` 59 ("oneshot … continues to drive submit through M2–M5"), `M6` 174/191–194 (deletion in `userInputModel.lua` + `userInputController.lua`); D-4 302–313 | +| Native coexistence (D-9) | **Intact** | D-9 514–558; `design.md §2/§6` 83–94/320–329; `spec.md §6` 440–458 | +| Two-channel model (D-6) | **Intact** | D-6 366–393; `spec.md §3` 229–296 | +| Combo canonical form (D-3) | **Intact** | D-3 246–263; `design.md §4` 233–249; `spec.md §1` 58–87; `roadmap.md M5` 146 | +| `write_to_input` facade | **Intact** | `design.md §6` 307–309; `spec.md §5` 373; `roadmap.md M3` 89–91 | +| FR-11/FR-12 walkthrough | **Intact** | `design.md §7` 365–397 | +| `compy.input.*` relocation in derived docs + summaries | **Intact (now also in `decisions.md`)** | grep-clean across `design/spec/roadmap` and all summaries; the round-2 `decisions.md` relocation did not desync them — it *removed* the last desync | + +Codebase facts (Part D dim. 6) all re-verified below; none regressed. + +--- + +## Part C — Provenance and traceability + +- **Local-proposal status note: present, count stale.** `decisions.md` 3–7 and + `summaries/decisions.md` 7–9 both carry the "entire document is a local + proposal derived from `input.md`, pending one approve/veto review" note — + correct against the authority model. **But both still parenthesise the set as + "(D-1…D-9)"** while the file carries D-1…D-10. The D-1…D-10 set is otherwise + internally consistent: quick-reference table (decisions 145–154) lists D-1…D-10, + per-decision detail runs through D-10 (562), and the narrative intro names + D-8/D-9/D-10 (39). Only the header parenthetical lags. Note-level. +- **Round-2 marker present but thin.** Exactly one greppable + `local design round 2, 2026-06` occurrence (`decisions.md:395`, the D-6 + `mods` future-seam note). The round-1 marker has six (`decisions.md` + 246/301/365/457/509 + the D-10 hybrid at 606). Other round-2 edits — the D-3 + superseded-in-part body lead-in, the intro D-10 reconciliation, the + names-only relocation — carry no in-file round-2 tag. The full round-2 delta + is auditable through `changelog_2.md` (which tabulates D-10 / D-6 / D-3 with + round-2 provenance), so traceability is not lost, but the in-document marker + is less greppable than its round-1 counterpart. Note-level. +- **D-10 provenance honest.** D-10's marker (606–609) reads "Origin: local + design round 1 namespace pass, 2026-06; recorded as a decision in round 2. + See `validation/recommendations_2.md` Item 2 and `validation/ + recommendations_1.md` 'Namespace isolation'." This correctly attributes the + *choice* to round 1 and the *recording* to round 2 — exactly the honest + framing the prompt asked for, and the reason its marker is the round-1/round-2 + hybrid rather than a plain round-2 tag. +- **`requirements.md` still traces `input.md`.** Unedited this round; no + downstream change contradicts it. FR-8/9/10 remain carried (the round-1 + restoration holds); FR-5/6 remain satisfied by the proxy-based modifier + context, which the `mods` deferral does not disturb (see Part E). + +--- + +## Part D — Seven dimensions (spot re-run) + +1. **Requirements coverage — pass.** All twelve FR + four NFR still trace + decisions → design → spec → roadmap. FR-8/9/10 intact (Part B). +2. **Decision consistency — pass.** D-10 reflected consistently (decisions, + summary, and all derived docs read `compy.input.*`). The N1 default-wording + fix did not create a new mismatch — both channel callbacks now read + default=sink in design and spec, the submit/cancel/limit set reads noop+log. + No old or new decision is silently contradicted by a derived doc. +3. **Design-to-spec completeness — pass.** Channel-callback defaults and the + textinput/keypressed symmetry now read the same in `design.md §4` and + `spec.md §3`. The §3 component-table description (`on_key_pressed` = "fires + for all keys; default = sink") matches the spec body — the round-2 stale-framing + note from report 2 is closed. +4. **Spec-to-roadmap coverage — pass.** Every surface still has a milestone + home; the re-estimate covers the actual round-1 scope (M6 oneshot, M7 cursor + + model fix, M3 `write_to_input`, larger test surface are reflected in the + most-likely/pessimistic figures and the row labels). +5. **Roadmap ordering validity — pass.** The re-estimate perturbed only the + numbers; the milestone Input/Output dependencies are unchanged. M6 Input + still "M4 complete (M5 is independent of M6)"; M7 Input still "M2 complete. + M5/M6 not required." M2/M6 oneshot ordering intact. +6. **Factual accuracy against codebase — pass.** Re-spot-checked: + - `cancel()` does **not** push `'userinput'` — confirmed. + `userInputModel.lua:795–798` (`cancel` → `handle(false)` → `reset`); the + `'userinput'` push at `:812–819` is gated by `if eval` **and** + `if self.oneshot`, both false on cancel, so it is unreachable. + - `oneshot` is a **`UserInputModel`** field — confirmed + (`userInputModel.lua:15` @field, `:45–49` constructor param/store). + - Stop-time reset is `stop_project_run` / `clear_user_handlers` — confirmed + (`consoleController.lua:860`, `:867`). Chain's "860–868" citation accurate. + - Native slot interception via `hook_if_differs` / `save_user_handlers` — + confirmed (`controller.lua:75`, `:779`). + - `set_text`'s unconditional `jump_end()` (the M7 model fix) — confirmed. + `userInputModel.lua:128–146`: the `keep_cursor` guards at `:135` and + `:142` already gate the view/cursor updates, but `:145 self:jump_end()` is + **outside** both guards, so the cursor jumps to end even when + `keep_cursor` is true. The D-8/M7 "skip the unconditional `jump_end()` + when `keep_cursor`" fix is correctly identified and correctly scoped. + No codebase mismatch found. +7. **Summary fidelity — pass (with the one mirrored count-staleness).** + `summaries/spec.md` mirrors the corrected default scoping (98–100); + `summaries/decisions.md` carries the D-10 glance row (172), the intro note + (35–40), and the full `compy.input.*` naming; `summaries/roadmap.md` carries + the ≈ 59 h / ≈ 35 h three-point figures (38). The only summary residue is the + "(D-1…D-9)" status parenthetical at `summaries/decisions.md:7`, mirrored from + its source — not an invented claim. + +--- + +## Part E — Carried-forward open questions (adjudication) + new conflicts + +**1. `mods` trailing modifier-string — acceptable to defer.** Nothing in +`input.md` or FR-5/FR-6 requires a *pre-folded* modifier string. The owner +clarification asks for "callbacks for keys pressed together with the Ctrl key" +and FR-6 asks for notification of non-character / modifier-combo key events; the +chain satisfies that by delivering the full `keys_pressed` read-only proxy as +the second argument to both channel callbacks and to combo handlers, from which +modifier state is directly readable. `mods` is a convenience over that proxy, +not a capability gap. Recording it as a flagged future seam (D-6 + `spec.md §3`) +is the correct disposition: it is neither silently built nor silently dropped, +and the architect has an explicit confirm/decline point. **Not blocking.** + +**2. `decisions.md` relocation reversing round-1's narrow exclusion — +acceptable; it resolves the finding rather than reintroducing one.** Round 1 +deliberately left `decisions.md` flat; report 2 then flagged the resulting split +(flat `decisions.md` vs `compy.input.*` derived docs, plus the single leaked +`compy.input.handlers`) as a real inconsistency in the one document the +stakeholder reviews. Round 2's relocation removes that split entirely and pairs +it with D-10 so the namespace choice is itself on the record. The relocation is +names-only, and the two things that must *not* move (`compy.keys_pressed`, the +legacy globals) correctly did not. This is the cleanest available resolution; +it does not reintroduce a problem. **Not blocking.** + +**New cross-document contradictions introduced by round 2:** none of +consequence. The two residues — the "(D-1…D-9)" status parenthetical and the +estimate headline that differs from recommendations_2's suggested number — are, +respectively, an internal-count staleness and a justified methodology-driven +deviation that is internally consistent across roadmap and summary. Neither is a +contradiction between documents about the *design*; both are notes. + +--- + +## Summary of actionable items + +All notes; none blocking. (No dropped FR, no broken example, no contradictory +design, no codebase mismatch.) + +1. **Update the status-note parenthetical from "(D-1…D-9)" to "(D-1…D-10)"** in + `decisions.md:3` and `summaries/decisions.md:7`. The narrative intro already + reconciled to ten; only this header line lags. Two-character fix. Note. +2. **Optionally thicken the in-file round-2 marker.** The round-2 delta is + fully auditable via `changelog_2.md`, but only one in-document + `local design round 2` tag exists (the `mods` seam). Tagging the D-3 + superseded-in-part lead-in and the intro D-10 reconciliation would make the + round-2 delta as greppable in-place as the round-1 delta. Note (optional). + +## Recommendation + +**Ready for stakeholder review.** The round-2 cleanup landed faithfully across +all six items, the round-1 resolutions are intact, the headline N1 default +contradiction is closed everywhere, and the load-bearing codebase facts all +re-verify. What remains is two sub-trivial wording notes (the D-10 count in one +header line, and round-2 marker thinness) — neither alters the design, drops a +requirement, or misleads the implementor, and neither warrants another local +pass. They can be folded into any later edit. A `validation/recommendations_3.md` +is **not** warranted.