Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 26 additions & 20 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ drivers and an optional Python/CVXPY optimizer.

The repository has three explicit modules:

- **Core** — `go/cmd/ftw`, `go/internal` and `web`. Core owns state,
telemetry, control, safety, API and UI.
- **Drivers** — `drivers/*.lua`, hosted by `go/internal/drivers`. Drivers own
- **Core** — [`go/cmd/ftw`](go/cmd/ftw), [`go/internal`](go/internal) and
[`web`](web). Core owns state, telemetry, control, safety, API and UI.
- **Drivers** — `drivers/*.lua`, hosted by
[`go/internal/drivers`](go/internal/drivers). Drivers own
vendor protocols and are the only place power signs are converted. This tree
is a generated snapshot of `srcfl/device-drivers`, pinned in
`drivers/BUNDLED_SOURCE.json`; change a driver there, not here.
- **Optimizer** — `optimizer`, behind the contract in `go/internal/mpc`.
is a generated snapshot of
[`srcfl/device-drivers`](https://github.com/srcfl/device-drivers), pinned in
[`drivers/BUNDLED_SOURCE.json`](drivers/BUNDLED_SOURCE.json); change a driver
there, not here.
- **Optimizer** — [`optimizer`](optimizer), behind the contract in
[`go/internal/mpc`](go/internal/mpc).
It proposes plans; core validates them and retains a Go fallback.

Keep new functionality in core unless it has a narrow versioned contract,
Expand All @@ -32,11 +36,12 @@ Read [docs/architecture.md](docs/architecture.md) for the system map and
- A failed/stale driver receives its autonomous default mode.
- Every clamp protects a quantified hardware or control risk.
- Persistent device state is keyed by stable hardware identity, not a YAML name.
- SQLite queries stay in `go/internal/state`.
- SQLite queries stay in [`go/internal/state`](go/internal/state).

## Drivers

`go/internal/drivers/lua.go` is the source of truth for the Lua host API.
[`go/internal/drivers/lua.go`](go/internal/drivers/lua.go) is the source of
truth for the Lua host API.
Every driver implements `driver_init`, `driver_poll`, `driver_command` and a
safe `driver_default_mode`; `driver_cleanup` is optional. Declare catalog
metadata in the driver's `DRIVER` block and request only required capabilities.
Expand All @@ -51,7 +56,7 @@ Drivers are hot-editable and ship without a compilation step. See
- Prefer explicit mutexes and simple ownership over clever atomics.
- Keep packages cohesive; depend on narrow interfaces.
- Put tests beside Go code as `_test.go`.
- Keep full-stack tests in `go/test/e2e`.
- Keep full-stack tests in [`go/test/e2e`](go/test/e2e).
- Treat code, types, tests and driver metadata as the detailed documentation.
- Add prose only for architecture, safety invariants or operator steps the code
cannot explain.
Expand Down Expand Up @@ -80,7 +85,7 @@ landed on somebody else's finished work.
area, whoever — or whatever — wrote it.

Planning documents, design specs, task breakdowns and agent scratch notes
stay out of the repository; `.github/check-no-planning-docs.sh` enforces
stay out of the repository; [`.github/check-no-planning-docs.sh`](.github/check-no-planning-docs.sh) enforces
this. Commit the change, its tests and a changeset; put the reasoning in the
PR description, where it is read during review and then archived.

Expand All @@ -102,8 +107,9 @@ Lua drivers have syntax and contract checks in the Go test suite.
## Releases

Changesets drive versioning. Every user-visible code change needs a
`.changeset/*.md` entry; documentation- and CI-only changes are auto-exempt.
Do not edit `package.json` version or `CHANGELOG.md` manually.
[`.changeset/*.md`](.changeset/) entry; documentation- and CI-only changes are
auto-exempt. Do not edit [`package.json`](package.json) version or
[`CHANGELOG.md`](CHANGELOG.md) manually.

Only two release channels exist:

Expand All @@ -118,14 +124,14 @@ independently but follow the same beta-to-stable progression. See

| Concern | Source |
|---|---|
| Process wiring and control tick | `go/cmd/ftw/main.go` |
| Configuration schema | `go/internal/config`, `config.example.yaml` |
| HTTP routes | `go/internal/api/api.go` |
| Lua host and registry | `go/internal/drivers` |
| Safety/control | `go/internal/control`, `go/internal/telemetry` |
| Persistence | `go/internal/state` |
| Planner contract/fallback | `go/internal/mpc` |
| Optional optimizer | `optimizer` |
| Process wiring and control tick | [`go/cmd/ftw/main.go`](go/cmd/ftw/main.go) |
| Configuration schema | [`go/internal/config`](go/internal/config), [`config.example.yaml`](config.example.yaml) |
| HTTP routes | [`go/internal/api/api.go`](go/internal/api/api.go) |
| Lua host and registry | [`go/internal/drivers`](go/internal/drivers) |
| Safety/control | [`go/internal/control`](go/internal/control), [`go/internal/telemetry`](go/internal/telemetry) |
| Persistence | [`go/internal/state`](go/internal/state) |
| Planner contract/fallback | [`go/internal/mpc`](go/internal/mpc) |
| Optional optimizer | [`optimizer`](optimizer) |
| Driver catalog | `DRIVER` blocks in `drivers/*.lua` |

When behavior looks wrong, inspect the source and its tests before adding a new
Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

Thanks for helping build the local energy coordination layer. This document
covers the legal bits — for how the code is organized and how to add a driver,
start with `AGENTS.md` and `docs/writing-a-driver.md`.
start with [`AGENTS.md`](AGENTS.md) and
[`docs/writing-a-driver.md`](docs/writing-a-driver.md).

## Website

Expand Down Expand Up @@ -80,4 +81,4 @@ By making a contribution to this project, I certify that:
- Keep PRs focused on one logical change.
- New code needs tests; `make verify` must pass before review.
- User-visible changes need a Changeset entry (`npx changeset`) — see the
Release Process section in `README.md`.
Release Process section in [`README.md`](README.md).
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ workflows.

## Configuration

`config.example.yaml` and the validation types in `go/internal/config` are
the configuration reference. Copy the example for a native development setup:
[`config.example.yaml`](config.example.yaml) and the validation types in
[`go/internal/config`](go/internal/config) are the configuration reference. Copy the example for a native development setup:

```bash
cp config.local.example.yaml config.local.yaml
Expand Down Expand Up @@ -194,8 +194,8 @@ metadata are the detailed reference.
- [Home Assistant](docs/ha-integration.md)
- [CalDAV](docs/caldav-integration.md)

Other files under `docs/` are focused installation or external-integration
guides.
Other files under [`docs/`](docs/) are focused installation or
external-integration guides.

## Contributing

Expand Down
36 changes: 21 additions & 15 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ make dispatch unsafe.

| Module | Source | Runtime | Responsibility |
|---|---|---|---|
| Core | `go/cmd/ftw`, `go/internal`, `web` | One Go binary | Configuration, telemetry, state, API/UI, safety, control and fallback planning |
| Drivers | Editable source in `srcfl/device-drivers`; bundled recovery in `drivers/*.lua`; host in `go/internal/drivers` | One sandboxed Lua VM per configured device | Vendor protocol, sign conversion and device commands |
| Optimizer | `optimizer`, contract in `go/internal/mpc` | Optional Python service/process | Solve the long-horizon mathematical plan |
| Core | [`go/cmd/ftw`](../go/cmd/ftw), [`go/internal`](../go/internal), [`web`](../web) | One Go binary | Configuration, telemetry, state, API/UI, safety, control and fallback planning |
| Drivers | Editable source in [`srcfl/device-drivers`](https://github.com/srcfl/device-drivers); bundled recovery in `drivers/*.lua`; host in [`go/internal/drivers`](../go/internal/drivers) | One sandboxed Lua VM per configured device | Vendor protocol, sign conversion and device commands |
| Optimizer | [`optimizer`](../optimizer), contract in [`go/internal/mpc`](../go/internal/mpc) | Optional Python service/process | Solve the long-horizon mathematical plan |

Core can run without the optimizer. Hardware cannot be accessed without a
driver, but one failed driver is isolated from the others. Optional
Expand All @@ -38,9 +38,10 @@ changing power math.

## Core

`go/cmd/ftw/main.go` is the composition root. It wires configuration, driver
registry, telemetry, persistent state, control, planning, API and integrations.
Packages under `go/internal` should stay cohesive and communicate through
[`go/cmd/ftw/main.go`](../go/cmd/ftw/main.go) is the composition root. It wires
configuration, driver registry, telemetry, persistent state, control, planning,
API and integrations.
Packages under [`go/internal`](../go/internal) should stay cohesive and communicate through
narrow Go interfaces or data types instead of reaching into one another's
storage.

Expand All @@ -57,7 +58,8 @@ telemetry → control/planner → core validation and safety → driver command

The in-memory telemetry store owns latest readings and driver health. SQLite
owns durable configuration state, history, forecasts, prices, device identity
and learned model state. Database access stays in `go/internal/state`.
and learned model state. Database access stays in
[`go/internal/state`](../go/internal/state).

The control loop computes a site target, allocates it across capable assets,
applies safety constraints, then sends commands through the driver registry.
Expand All @@ -73,7 +75,8 @@ may later consume an exact public commit for other products or a higher support
level.

Each Lua artifact still contains its own `DRIVER` metadata and implements the
FTW lifecycle. `go/internal/drivers/lua.go` is the source of truth for FTW's
FTW lifecycle. [`go/internal/drivers/lua.go`](../go/internal/drivers/lua.go) is
the source of truth for FTW's
host API and capability sandbox. Network and protocol capabilities must be
granted in configuration.

Expand Down Expand Up @@ -107,7 +110,8 @@ Drivers and the optimizer release on their own schedules, so core cannot assume
the version on the other side of either contract. Both use the same rule.

Each side declares the **window** of contract versions it speaks — core in
`go/internal/components` and `go/internal/optimizercontract`, a driver in its
[`go/internal/components`](../go/internal/components) and
[`go/internal/optimizercontract`](../go/internal/optimizercontract), a driver in its
`host_api_min`/`host_api_max` metadata, the optimizer in its handshake reply.
An overlap of one version is enough. Declaring a single version means a window
of one.
Expand Down Expand Up @@ -140,19 +144,21 @@ relevant code are the detailed executable specification.

## Configuration and interfaces

`config.example.yaml` and the structs plus validation in
`go/internal/config` define the configuration schema. The handlers registered
in `go/internal/api/api.go` define the HTTP surface. Driver metadata defines
[`config.example.yaml`](../config.example.yaml) and the structs plus validation
in [`go/internal/config`](../go/internal/config) define the configuration
schema. The handlers registered in
[`go/internal/api/api.go`](../go/internal/api/api.go) define the HTTP surface. Driver metadata defines
the device catalog. These sources replace manually duplicated reference docs.

Some startup bindings cannot be hot-reloaded, including state paths, API
listener and selected integration transports. Normal device and control
configuration is reloaded through `go/internal/configreload`.
configuration is reloaded through
[`go/internal/configreload`](../go/internal/configreload).

## Future remote access boundary

Remote Sourceful access is not implemented by the LAN/API hardening layer. The
central request policy in `go/internal/api` is the expansion point: a future
central request policy in [`go/internal/api`](../go/internal/api) is the expansion point: a future
protected remote request must present a locally verifiable principal and access
proof there before an API handler runs.

Expand Down Expand Up @@ -234,7 +240,7 @@ There is no edge channel. See [self-update.md](self-update.md).

1. [site-convention.md](site-convention.md)
2. this document
3. `go/cmd/ftw/main.go`
3. [`go/cmd/ftw/main.go`](../go/cmd/ftw/main.go)
4. the package or driver being changed and its colocated tests
5. [writing-a-driver.md](writing-a-driver.md) for hardware support
6. [operations.md](operations.md) for deployment and recovery
10 changes: 5 additions & 5 deletions docs/caldav-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ network.
FTW **hosts its own CalDAV server**, in-process, and also runs a CalDAV
**client** against it:

- The **server** (`go/internal/caldavserver`) is pure-Go, built on
- The **server** ([`go/internal/caldavserver`](../go/internal/caldavserver)) is pure-Go, built on
[`emersion/go-webdav`](https://github.com/emersion/go-webdav) (MIT). It ships
inside the single FTW binary — no sidecar, no second container — and persists
calendar objects in `state.db`. It binds `:5232` on your LAN so your phone or
desktop calendar app can subscribe. Because it's in-process it runs everywhere
FTW does, **including a single-container Home Assistant add-on**.
- The **client** (`go/internal/calendar`) polls a collection on that server and
- The **client** ([`go/internal/calendar`](../go/internal/calendar)) polls a collection on that server and
maps events onto planner machinery.

```
Expand Down Expand Up @@ -87,7 +87,7 @@ languages. What FTW parsed is visible at `GET /api/caldav/status`.

**Recurring events work fully.** A weekly *Away* or a daily *Charge car* expands
into its individual occurrences server-side (RFC 4791 `CALDAV:expand`, via
`caldavserver/expand.go`), so the planner sees every occurrence inside its
[`caldavserver/expand.go`](../go/internal/caldavserver/expand.go)), so the planner sees every occurrence inside its
horizon — not just the first. RRULE, RDATE and EXDATE are all honoured, and if
you edit or delete a single occurrence in your calendar app (a per-instance
`RECURRENCE-ID` override or cancellation) that one occurrence is updated/removed
Expand Down Expand Up @@ -121,7 +121,7 @@ collections are kept distinct so FTW never re-reads its own output as input.

## Config

See the `caldav:` block in `config.example.yaml`. The password is stored in
See the `caldav:` block in [`config.example.yaml`](../config.example.yaml). The password is stored in
`state.db` (key `caldav_password`), never written to `config.yaml`. URL,
credentials, keywords and intervals hot-reload; toggling `enabled` needs a
restart. `listen` (default `:5232`) sets the server's bind address.
Expand All @@ -135,7 +135,7 @@ deploy mode with nothing extra to install:
server binds `:5232` directly on the host. Subscribe at
`http://<host-ip>:5232/…`.
- **docker-compose on macOS (bridge networking):** the main service publishes
`5232:5232` (see `docker-compose.macos.yml`) so phones reach it; keep
`5232:5232` (see [`docker-compose.macos.yml`](../docker-compose.macos.yml)) so phones reach it; keep
`caldav.url: http://localhost:5232` (the in-container loopback).
- **Home Assistant add-on (single container):** it just works — there is no
sidecar at all, so no deploy-mode is gated off.
Expand Down
3 changes: 2 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ Set `FTW_PROXY_READONLY=0` only for an intentional live write session.
## Containers

`docker compose up -d` mirrors the Linux production topology: core, optimizer,
updater and MQTT broker. Use `docker-compose.macos.yml` on macOS. Local Compose
updater and MQTT broker. Use
[`docker-compose.macos.yml`](../docker-compose.macos.yml) on macOS. Local Compose
overrides are machine-specific and untracked.

## Generated files
Expand Down
2 changes: 1 addition & 1 deletion docs/ha-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ homeassistant:

After FTW connects, Home Assistant discovers site power/energy, mode, planner,
driver health, structured DER readings and emitted diagnostic metrics. The
exact entity set is generated by `go/internal/ha`; it is not maintained
exact entity set is generated by [`go/internal/ha`](../go/internal/ha); it is not maintained
manually in this document.

Power values keep FTW's site convention:
Expand Down
3 changes: 2 additions & 1 deletion docs/nibe-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ thermal/load models consume `hp_power_w` etc. as twins.
## Testing

A live integration test exercises the driver against a real pump
(`go/internal/drivers/nibe_local_test.go`, `TestNibeLocalLive`), skipped unless
([`go/internal/drivers/nibe_local_test.go`](../go/internal/drivers/nibe_local_test.go),
`TestNibeLocalLive`), skipped unless
`NIBE_LIVE=1`:

```bash
Expand Down
2 changes: 1 addition & 1 deletion docs/nova-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This bridge is an engineering integration, not a public remote-access path.

## Boundary

`go/internal/nova` snapshots registered DER telemetry, maps it to the selected
[`go/internal/nova`](../go/internal/nova) snapshots registered DER telemetry, maps it to the selected
Nova schema and publishes over MQTT. An ES256 identity signs short-lived MQTT
JWTs. Claim and provisioning use HTTPS.

Expand Down
11 changes: 7 additions & 4 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,15 @@ changing ownership or deleting any SQLite sidecar files.

## Configuration

`config.example.yaml` and `go/internal/config` define the current schema.
[`config.example.yaml`](../config.example.yaml) and
[`go/internal/config`](../go/internal/config) define the current schema.
Edits are validated before application. A rejected hot reload leaves the
previous live configuration intact.

Driver set and most control values reload live. Listener addresses, state
paths and some integration transports are startup bindings; restart after
changing them. When unsure, inspect the restart classification in
`go/internal/config/restart_required.go`.
[`go/internal/config/restart_required.go`](../go/internal/config/restart_required.go).

## LAN and API access

Expand Down Expand Up @@ -181,7 +182,8 @@ driver configuration.

### Configuration rejected

Read the validation error, compare with `config.example.yaml`, fix the file and
Read the validation error, compare with
[`config.example.yaml`](../config.example.yaml), fix the file and
save again. Do not delete `state.db` to resolve a YAML error.

### Port already in use
Expand All @@ -196,7 +198,8 @@ Stop the conflicting service or change the configured API port, then restart.

`make build-arm64` and `make build-amd64` produce static Core and `ftw-backup`
binaries.
`deploy/ftw.service` is the reference systemd unit. A conventional layout is:
[`deploy/ftw.service`](../deploy/ftw.service) is the reference systemd unit. A
conventional layout is:

```text
/opt/ftw/ binary, web, bundled drivers, optional optimizer
Expand Down
2 changes: 1 addition & 1 deletion docs/rpi-image.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ beta/stable updater.

## Build the image

Image provisioning lives under `deploy/pi-gen`:
Image provisioning lives under [`deploy/pi-gen`](../deploy/pi-gen):

```bash
deploy/pi-gen/build.sh
Expand Down
9 changes: 5 additions & 4 deletions docs/safety.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ normal dispatch resumes only after the required readings recover.

Source and executable specification:

- `go/internal/telemetry`
- the control tick in `go/cmd/ftw/main.go`
- [`go/internal/telemetry`](../go/internal/telemetry)
- the control tick in [`go/cmd/ftw/main.go`](../go/cmd/ftw/main.go)
- watchdog and stale-meter tests beside those packages

## Dispatch limits
Expand All @@ -49,8 +49,9 @@ The reactive fuse saver may bypass normal slew limiting because preventing a
fuse trip is the higher-priority quantified risk. It must still respect
available battery energy and hardware power capability.

The implementation and tests in `go/internal/control` are authoritative.
Planner-side limits in `go/internal/mpc` reduce infeasible plans but do not
The implementation and tests in [`go/internal/control`](../go/internal/control)
are authoritative. Planner-side limits in [`go/internal/mpc`](../go/internal/mpc)
reduce infeasible plans but do not
replace runtime enforcement.

## Planner and model containment
Expand Down
7 changes: 4 additions & 3 deletions docs/self-update.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ migrated to `beta`; no edge releases are published or accepted.
## Release progression

User-visible changes land with a Changeset. The Changesets workflow opens the
Version Packages PR and updates `package.json` plus `CHANGELOG.md`.
Version Packages PR and updates [`package.json`](../package.json) plus
[`CHANGELOG.md`](../CHANGELOG.md).

After that PR merges:

1. run `beta.yml` with `vX.Y.Z-beta.N`;
1. run [`beta.yml`](../.github/workflows/beta.yml) with `vX.Y.Z-beta.N`;
2. validate that immutable build on real sites;
3. manually dispatch `release.yml` from that same commit;
3. manually dispatch [`release.yml`](../.github/workflows/release.yml) from that same commit;
4. stable promotion verifies that a matching beta tag resolves to the exact
stable candidate commit;
5. release assets publish `vX.Y.Z` and move the stable aliases.
Expand Down
Loading