diff --git a/README.md b/README.md index f5ed84a..2eb753e 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,15 @@ bounded effect ## Applications Built With HowlFrame +* **[HowlBoard](https://github.com/howlcipher/howlboard)** — the canonical + HowlFrame reference application and human-facing AI engineering operations + interface. A full-stack mission-control console for governed autonomous work: + `http_server` compiled to standalone bytecode, `web_app` compiled through the + JavaScript backend, native-store persistence, and no hand-written server or + client code. Live at . Its + [dogfooding journal](https://github.com/howlcipher/howlboard/blob/main/docs/dogfooding.md) + is the most detailed record of where this language helps and where it gets in + the way. * [Status API](apps/status_api/README.md) — Proves HTTP serving, deterministic routing, and environment inspection. * [Log Analyzer](apps/log_analyzer/README.md) — Proves file parsing, deterministic string logic, and graceful capability denial. * [KV CLI](apps/kv_cli/README.md) — Proves in-memory store functionality and sequential deterministic state. @@ -281,7 +290,20 @@ go run howlframe.go -o build examples/wasm_math.howl ### Reference Application -[HowlFrame Repo Analyst](examples/repo_analyst/README.md) is a deterministic, five-module application that compiles to HowlFrame bytecode and analyzes repositories without generated Go or JavaScript. Its dogfooding tests prove both the unchanged default instruction ceiling and a larger finite budget explicitly authorized by the trusted runner. +[HowlBoard](https://github.com/howlcipher/howlboard) is the canonical reference +application: a full-stack mission-control interface for governed autonomous +engineering work, with both tiers written in HowlFrame. It is the largest +program built on this toolchain and the one that most exercises it — the +`store_keys` primitive, heterogeneous dict records, fail-closed HTTP handlers, +and several `web_app` codegen fixes all exist because building it required +them. + +[HowlFrame Repo Analyst](examples/repo_analyst/README.md) remains the reference +for the in-repository bytecode path: a deterministic, five-module application +that compiles to HowlFrame bytecode and analyzes repositories without generated +Go or JavaScript. Its dogfooding tests prove both the unchanged default +instruction ceiling and a larger finite budget explicitly authorized by the +trusted runner. ## Common Language Features diff --git a/apps/task_api/DEVELOPMENT_NOTES.md b/apps/task_api/DEVELOPMENT_NOTES.md index 5a86f68..5e219b7 100644 --- a/apps/task_api/DEVELOPMENT_NOTES.md +++ b/apps/task_api/DEVELOPMENT_NOTES.md @@ -4,6 +4,36 @@ Phase 3 dogfooding: a genuine stateful HTTP CRUD service, running as standalone bytecode, exercised across multiple independent HTTP requests against one long-lived server process. +## Corrections (recorded during the HowlBoard dogfood pass) + +Three claims below were accurate when written and are no longer true. They are +left in place for the record, with the correction stated here rather than +silently edited into the original text. + +* **"No opcode exposes method, query string, headers, or path segments" — partly + stale.** `OpHttpReqMethod` exists and `(req_method req)` returns the HTTP + method; HowlBoard uses it for OPTIONS preflight in every route. Query strings, + headers and path segments remain genuinely unavailable, and `req_header` is + documented in the app-development skill despite having no opcode. + +* **"`defun` return-type annotations compile but crash at runtime" and "there is + currently no working path to a `defun` that returns a dict" — stale.** The + supported form is the `type_hint` annotation, not a positional type symbol: + `(defun make_rec (id) (type_hint return "dict") ...)` compiles and returns a + dict that `map_get` consumes correctly at the call site. `type_hint` is + compiled away as a pure annotation. HowlBoard's backend and interface are both + decomposed into dict-returning helpers on this basis. + +* **"An unhandled panic inside a route handler is silently swallowed as a + successful response" — fixed.** A handler that fails before writing now + returns 500 carrying the structured `VMError` JSON with its code preserved, + logged to the VM error stream rather than process stdout. See + `TestHTTPHandlerFailuresFailClosed`. + +Separately, the note that `store_keys`-style enumeration is "adequate friction, +not a blocker" no longer applies: `store_keys` exists, returns sorted keys, and +removes the `next_id` scan workaround entirely. + ## What worked well * **State sharing across HTTP requests.** Every `route` handler runs in a diff --git a/apps/task_api/task_api_test.go b/apps/task_api/task_api_test.go index 2f461cf..94fd526 100644 --- a/apps/task_api/task_api_test.go +++ b/apps/task_api/task_api_test.go @@ -399,13 +399,19 @@ func TestTaskAPICapabilities(t *testing.T) { t.Run("network only starts but store access is denied", func(t *testing.T) { startServer(t, srcDir, bcPath, "network") - // Documented runtime finding: an unhandled capability-denied panic - // inside a route handler is swallowed by the VM's own recover, and - // the client observes a 200 with an empty body rather than an - // error status. This subtest asserts that real, current behavior. + // This subtest previously asserted the opposite: that a + // capability-denied panic inside a route handler was swallowed and the + // client saw a 200 with an empty body. That made the platform's core + // safety mechanism indistinguishable from success at the client, and + // the VM now fails closed instead, surfacing the structured VMError + // with its code intact. See TestHTTPHandlerFailuresFailClosed in + // internal/vm for the unit-level regression test. status, data := doRaw(t, "POST", "/tasks/create", `{"title":"x"}`) - if status != 200 || len(data) != 0 { - t.Errorf("expected silent 200/empty-body on capability-denied store access (documented runtime finding), got %d body=%q", status, data) + if status != 500 { + t.Errorf("expected 500 on capability-denied store access, got %d body=%q", status, data) + } + if !strings.Contains(string(data), "CAPABILITY_DENIED") { + t.Errorf("denial response does not carry its code: %q", data) } }) diff --git a/change_log.md b/change_log.md index 7c67cea..ed6e36b 100644 --- a/change_log.md +++ b/change_log.md @@ -3,6 +3,16 @@ ## Unreleased ### Added +* `time_now` in the JavaScript backend. It was supported by the bytecode VM and + the Go backend but rejected as an unknown statement for `web_app` programs, so + a browser interface had no way to read the clock and render relative times. +* `store_keys` construct and `STORE_KEYS` bytecode instruction, returning every + record key in a native store as a sorted list. Go randomizes map iteration, so + enumeration is sorted to keep listing deterministic. Every prior HowlFrame + application (`kv_cli`, `todo_cli`, `task_api`, HowlBoard) had to maintain a + parallel index record that could silently diverge from the records it indexed; + `store_keys` removes that workaround. Requires the `database` capability, and + `filesystem` additionally for `file://` stores. * Runner-sealed, bounded negative map-state provenance for the internal direct HFIR experiment. It records completed map mutations and reads by backing-map identity, proves never-present versus effectively deleted keys, and fails @@ -65,6 +75,13 @@ diverge silently. ### Changed +* Dict values may now mix types. Dicts are the language's record literal, and the + VM and native store both carry `map[string]any`, so a record combining strings, + ints, lists, and nested dicts already executed correctly; only the analyzer + rejected it. Heterogeneous dict literals and `map_set` writes now widen the + element type to `any` through the existing `join` helper instead of reporting + `dict value N has type X, want Y`. Key checks, target-kind checks, and list + element homogeneity are unchanged. * Documented the existing standalone HTTP JSON request composition (`parse_json ... req.body` with `try_let`), its bounded scope, and the @@ -104,6 +121,26 @@ are classified separately and keep compiling unchanged. ### Fixed +* `for` over an expression no longer silently miscompiles in the JavaScript and + Go backends. Both read the iterable's raw node value, which is empty for + anything but a bound symbol, so `(for m (map_get d "missions") ...)` emitted + `for (let m of )` and `for _, m := range {` - invalid output produced with no + diagnostic, in a toolchain whose contract is to fail closed. +* `on_event` now terminates its statement. Automatic semicolon insertion does + not apply before `(`, so any following top-level statement was parsed as a + call of the `addEventListener` result. +* A `web_app`'s top-level statements are wrapped in an async IIFE. They routinely + contain awaited calls, and a classic `