Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
be2dfac
docs: implementation plan to improve DwarfSpec CLI error reporting
dsisco11 Aug 3, 2026
547bbc2
[Phase 1]: Establish the probe response and diagnostic contracts
dsisco11 Aug 3, 2026
5847dad
[Phase 2]: Make the host probe safe and self-describing
dsisco11 Aug 3, 2026
d781136
[Phase 3]: Parse and classify probe results in the controller
dsisco11 Aug 3, 2026
3af3d02
[Phase 4]: Lock down controller and runner compatibility
dsisco11 Aug 3, 2026
c21276e
[Phase 5]: Document and package the improved diagnostics
dsisco11 Aug 3, 2026
8e477db
fix: cli dfhack detection
dsisco11 Aug 3, 2026
31e3194
docs: initial error reporting improvement implementation plan
dsisco11 Aug 3, 2026
225d405
docs: amend error reporting improvement implementation plan
dsisco11 Aug 3, 2026
8faee2c
[Phase 1]: Establish the additive rejection contract
dsisco11 Aug 3, 2026
d59bfde
[Phase 2]: Produce structured mismatch rejections in the host
dsisco11 Aug 3, 2026
048a43c
[Phase 3]: Validate and consume the structured rejection in the contr…
dsisco11 Aug 3, 2026
770edfb
[Phase 4]: Establish one shared adapter-error boundary
dsisco11 Aug 3, 2026
c945c40
[Phase 5]: Preserve bootstrap admission conflicts
dsisco11 Aug 3, 2026
78dca77
[Phase 6]: Structure mutation and recovery rejections
dsisco11 Aug 3, 2026
a852d58
[Phase 7]: Structure polling and event transport rejections
dsisco11 Aug 3, 2026
d788a4e
[Phase 8]: Lock down documentation, regression, and package integrity
dsisco11 Aug 3, 2026
114820c
chore: remove completed task list
dsisco11 Aug 3, 2026
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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
`Subject:search(query)` find literal final rendered text in the active mount
and return zero-based inclusive screen-cell bounds. Subject and rectangle
areas are spatial filters, not render-ownership claims.
- Structured `dwarfspec.error.v1` rejection codes now cover package admission,
scheduler conflicts, run mutation and recovery, polling, and event cursors
while preserving their existing runner classifications and exit codes.

### Changed

- Package-version mismatch diagnostics label the process-wide
`running_version` and current command's `requested_version` explicitly and
require a complete Dwarf Fortress/DFHack restart, removing the ambiguous
`expected` and `found` wording.

## [0.2.2] - 2026-08-03

### Changed

- Report connection preflight failures as distinct invocation, subprocess,
missing-response, malformed-response, protocol, core-context, and timeout
diagnostics with bounded captured output.
- Ship the updated controller and DFHack probe together while retaining probe
protocol version 2.

### Fixed

- Avoid attributing a failed DFHack connection to a selected specification
when test selection succeeded before the connection preflight.

## [0.2.1] - 2026-07-31

Expand Down
81 changes: 76 additions & 5 deletions docs/command-line.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,35 @@ complete terminal result without writing a file. A terminal service generation
is acknowledged only after its file replacement succeeds, or after successful
no-results validation.

### Connection preflight diagnostics

`dwarfspec run` completes discovery and selection before it starts the DFHack
connection preflight. A connection failure therefore does not implicate the
selected specification path. The path is included in a diagnostic only when
DFHack's subprocess output itself includes it.

Every connection preflight failure exits with code 4, but its message identifies
the failed boundary and the action needed to investigate it:

- an invocation failure reports the resolved runner path and the original
process-launch error;
- a nonzero probe exit reports the numeric exit code and bounded probe output;
- a missing response marker reports that no probe response was found and
includes bounded probe output;
- multiple response markers report the observed count and bounded probe output;
- a malformed response reports the first grammar error and the bounded
offending response line;
- a protocol mismatch reports the expected and observed protocol versions;
- a core-context failure reports that DFHack did not provide a healthy core Lua
context; and
- a timeout-capability failure reports that the required timeout support is
unavailable.

Captured subprocess output is length-bounded and sanitized before display. For
a protocol mismatch, reinstall or upgrade DwarfSpec from one package artifact
so that the external controller and bundled DFHack probe come from the same
DwarfSpec release. The probe protocol remains version 2 for this release.

Focus diagnostics are nonfatal. They do not change test counts, terminal state,
cleanup confirmation, or the process result. A run whose tests pass and whose
cleanup is confirmed therefore exits with code 0 even when it retains focus
Expand All @@ -178,15 +207,57 @@ Exit codes are stable:
| 7 | Execution timeout or the distinct queue-timeout classification |
| 8 | Active abort or the distinct pre-activation cancellation classification |

When bootstrap rejects a command because the running DFHack process retained a
different DwarfSpec package version, the diagnostic identifies the running
service version and the current command version separately. Save and fully
exit Dwarf Fortress/DFHack, relaunch it, and retry the command. Returning to
the title screen or unloading the world does not unload the process-wide
DwarfSpec service. This remains a registration rejection with
`registration_error` result state and exit code 5.

### Structured host rejections

Expected service decisions use a validated `dwarfspec.error.v1` response. The
command selects guidance from its stable code and safe context fields; it does
not parse the human-readable message. These decisions preserve the existing
runner classification, persisted result state, and exit code 5.

| Code | When it is reported | Operator action |
|---|---|---|
| `package_version_mismatch` | DFHack retained a different process-wide DwarfSpec package version | Fully exit and relaunch Dwarf Fortress/DFHack, then retry with the current command package |
| `project_busy` | Another retained run owns the project | Wait for and consume the blocking run's result |
| `request_key_conflict` | A request identity was reused for different work | Retry the identical request or choose a new run identity |
| `result_path_busy` | Another retained run owns the result destination | Wait for result consumption or choose another result destination |
| `service_not_loaded` | The requested mutating, recovery, polling, or event operation has no loaded service | Start a run to load the service; for an in-flight command, treat the service as unavailable and retry only after checking its result |
| `run_not_found` | The named retained run does not exist | Verify the run identity; do not fabricate local run state |
| `generation_mismatch` | The run exists at a newer generation | Refresh authoritative run state and retry with its current generation |
| `invalid_run_state` | The operation is not legal in the run's current state | Inspect the reported state and choose the matching status, recovery, acknowledgement, or discard workflow |
| `owner_capability_rejected` | The caller no longer owns the requested run operation | Stop retrying with the rejected ownership context and recover or inspect through the command that created the run |
| `quarantine_mismatch` | Executor recovery targeted a different quarantined run generation | Refresh scheduler state and recover the reported blocking run |
| `clean_state_unverified` | Executor recovery could not prove native cleanup | Keep the executor quarantined and resolve the reported cleanup condition before retrying recovery |
| `event_cursor_ahead` | Polling requested events beyond the retained journal | Restart observation from the reported retained cursor without advancing the local cursor |

Normal read-only absence is not a rejection. Service-wide status reports
`service_loaded`, and history, inspection, and log queries report `found` when
the service or run is unavailable. A structured domain rejection means the
host understood a valid operation and declined it without performing the
forbidden state change. A bridge failure means the external `dfhack-run`
process failed, timed out, or returned no valid response; its diagnostic may
include only bounded, sanitized subprocess output. Malformed responses,
corrupt service state, impossible invariants, and unexpected host exceptions
remain internal faults instead of being mislabeled as operator-correctable
rejection codes.

On timeout, interruption, or malformed transport after bootstrap, the command
asks the service to recover from authoritative current state. A queued run is
cancelled without native cleanup; an active run is aborted with cleanup. If
recovery also fails, the original runner failure remains primary.

## Protocol compatibility

All bundled commands and adapters use `dwarfspec.transport.v2`, structured
`dwarfspec.event.v1` events, `dwarfspec.run.v2` snapshots, and
`dwarfspec.result.v2` results. Legacy `dwarfspec.run.v1` reports and formatted
progress lines are not accepted. Readers reject unknown schemas and protocols
instead of guessing.
All bundled commands and adapters use `dwarfspec.transport.v2`; expected
adapter rejections use `dwarfspec.error.v1`; structured events use
`dwarfspec.event.v1`; snapshots use `dwarfspec.run.v2`; and results use
`dwarfspec.result.v2`. Legacy `dwarfspec.run.v1` reports and formatted progress
lines are not accepted. Readers reject unknown schemas and protocols instead
of guessing.
180 changes: 180 additions & 0 deletions docs/connection-probe-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# DwarfSpec connection probe contract

## Purpose

The connection probe determines whether the resolved `dfhack-run` process can
execute the minimum DFHack core Lua API required by DwarfSpec. It runs after
project discovery and test selection but before bootstrap, admission, or test
execution.

The probe reports observed facts. The controller owns parsing, compatibility
decisions, failure classification, and user-facing diagnostics.

## Compatibility invariants

- The controller and probe protocol remains version 2.
- Every unsuccessful probe is a `RunnerFailureKind.CONNECTION` failure, maps to
the `connection_error` invocation result state, and exits with code 4.
- A diagnostic improvement must not change project-root resolution, test
discovery, selector glob semantics, runner lookup, registration, bootstrap,
admission, scheduler behavior, or cleanup behavior.
- A selected spec identity is not part of a connection diagnostic unless the
invoked subprocess independently emitted it.
- Protocol mismatches, non-core contexts, and missing required capabilities
remain fatal. More precise reporting must not make health validation more
permissive.
- The probe has no DwarfSpec module, project module, JSON, configuration, host
service, or third-party dependency.
- The distributed controller and probe must come from the same DwarfSpec
package. Shipping this contract requires a patch-version package release so
the two artifacts are updated together.

## Probe response grammar

The probe emits exactly one response line. Unrelated output from DFHack may
appear before or after it.

```text
DWARFSPEC_PROBE protocol=2 core=true timeout=function [dfhack=<version>]
```

A candidate response line begins with the exact ASCII marker
`DWARFSPEC_PROBE`, followed by the end of the line or one ASCII space. A bare
marker is therefore a malformed candidate with missing required fields. A
marker embedded later in a line is ordinary subprocess output.

After the marker, the response consists of one or more fields separated by one
or more ASCII spaces. Each field has the form `name=value`:

- `name` matches `[a-z][a-z0-9_]*`;
- `value` matches `[A-Za-z0-9._+-]+`;
- field names are unique;
- `protocol`, `core`, and `timeout` occur exactly once;
- `protocol` is a positive base-10 integer without a sign;
- `core` is `true`, `false`, or `unavailable`;
- `timeout` is one of the Lua `type()` names `nil`, `boolean`, `number`,
`string`, `function`, `userdata`, `thread`, or `table`, or the value
`unavailable`.

The optional `dfhack` field contains `dfhack.VERSION` only when its string form
matches the value grammar. The probe omits it otherwise. Its presence and value
are diagnostic only and never affect health.

Unknown well-formed fields are permitted and ignored by controllers that do not
recognize them. This permits additive diagnostics without weakening required
field validation. A duplicate field, including an unknown field, is malformed
because its meaning would be ambiguous.

The probe observes incomplete contexts without indexing an unavailable value:

- when the global `dfhack` value is not a table, it reports
`core=unavailable timeout=unavailable`;
- when `dfhack.is_core_context` is absent, `core=unavailable`;
- otherwise `core` is the string form of the boolean value, with any value
other than `true` or `false` normalized to `unavailable`;
- when `dfhack.timeout` is absent because no DFHack table is available,
`timeout=unavailable`;
- otherwise `timeout` is the result of Lua `type(dfhack.timeout)`, including
`nil` when the field is absent from an available table.

A response is healthy only when it contains `protocol=2`, `core=true`, and
`timeout=function`.

## Controller classification order

The controller classifies a probe result in this order so one subprocess result
has one deterministic primary cause:

1. Invocation exception: invoking the resolved runner did not return a result.
2. Nonzero exit: the subprocess returned an exit code other than zero. Marker
parsing is not attempted because process failure is primary.
3. Missing marker: a zero-exit result contains no candidate probe response.
4. Multiple markers: a zero-exit result contains more than one candidate.
5. Malformed response: the sole candidate violates the response grammar.
6. Protocol mismatch: the parsed protocol differs from 2.
7. Core-context failure: `core` is not `true`.
8. Timeout-capability failure: `timeout` is not `function`.
9. Healthy response: every required field has its accepted value.

The controller searches the complete output instead of requiring the probe to
be the final line. Unrelated output does not invalidate one otherwise healthy
response.

## Diagnostic catalog

Messages use these stable forms. Angle-bracketed terms are substituted with
observed, cleaned, and bounded values.

| Condition | Diagnostic |
| --- | --- |
| Invocation exception | `Could not invoke DFHack runner "<runner>": <error>` |
| Nonzero exit | `DFHack connection probe through "<runner>" exited with code <code>. Output: <excerpt>` |
| Missing marker | `DFHack responded through "<runner>", but emitted no DwarfSpec probe report. Output: <excerpt>` |
| Multiple markers | `DFHack emitted <count> DwarfSpec probe reports; expected exactly one. Output: <excerpt>` |
| Malformed response | `DFHack emitted a malformed DwarfSpec probe report: <reason>. Probe: <probe-line>` |
| Protocol mismatch | `DwarfSpec protocol mismatch: controller expects 2, probe reported <protocol>. Check for mixed installed DwarfSpec package versions.` |
| Core-context failure | `DFHack probe did not run in a healthy core Lua context: expected core=true, reported core=<core>.` |
| Timeout-capability failure | `DFHack core Lua context is missing the required dfhack.timeout function: reported timeout=<timeout>.` |

Invocation exceptions include the resolved runner path and the cleaned exception
text. They do not claim that DFHack accepted a connection.

A malformed-response reason identifies the first grammar violation in parsing
order: invalid token, invalid field name, empty value, duplicate field, missing
required field, invalid protocol, invalid core value, or invalid timeout value.

## Bounded output excerpts

Diagnostics may include subprocess output for nonzero exits, missing markers,
multiple markers, and malformed responses. Formatting is deterministic:

1. Convert each supplied line to a safe string under a protected call so a
failing `tostring` metamethod cannot raise a secondary formatting error. Use
`<unprintable output>` when conversion fails.
2. Replace tab characters with one ASCII space. Replace ASCII control bytes
`0x00` through `0x1f` and `0x7f` with `?`, then trim surrounding ASCII
whitespace.
3. Discard empty normalized lines.
4. Limit each line to 512 bytes, including the suffix
`...<line truncated>` when truncation occurs.
5. Retain the final eight non-empty lines in their original order. When earlier
lines were omitted, prepend `<N earlier lines omitted>`.
6. Join rendered entries with ` | `.
7. Limit the complete excerpt to 2,048 bytes. If necessary, preserve the most
recent output and prefix it with `<output truncated> ` within that limit.
8. Render `<no output>` when no non-empty content remains.

The limits are byte limits because Lua strings and the current subprocess
surface are byte-oriented. An implementation must not split a valid UTF-8 code
point when it truncates otherwise valid UTF-8 output.

The formatter may reproduce paths or other text already emitted by the
subprocess. It must not add command arguments, environment variables, or secret
values from controller state. Existing result persistence rules determine
whether the resulting connection failure message is written to a result file.

## Ownership boundaries

- `src/dwarfspec/host/entrypoints/probe.lua` owns safe observation and one-line
response emission.
- `src/dwarfspec/controller/execution/transport_client.lua`, or a focused
controller helper extracted from it, owns response parsing, output bounding,
health checks, and connection failure construction.
- `src/dwarfspec/controller/execution/runner.lua` continues to orchestrate
preflight before bootstrap and does not interpret probe fields.
- Test discovery continues to own canonical identity selection before runner
orchestration. It does not diagnose DFHack connectivity.

## Verification obligations

Later implementation work must independently prove:

- probe behavior for healthy, absent, and incomplete DFHack globals;
- parser behavior for noise, missing and multiple markers, malformed fields,
protocol mismatch, unhealthy capabilities, and unknown fields;
- exact bounded-output behavior at line-count, per-line, total-byte, control
character, empty-output, and UTF-8 boundaries;
- preservation of connection failure kind, result state, and exit code;
- absence of bootstrap after any failed probe;
- package co-location of the controller and probe; and
- installed live success plus terminal cleanup evidence.
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ For a local release candidate, build and install the generated rock instead of
loading files from a sibling checkout:

```powershell
luarocks install .\dist\dwarfspec-0.2.1-1.all.rock
luarocks install .\dist\dwarfspec-0.2.2-1.all.rock
```

The VS Code `Publish` task, or `tools/Publish.ps1`, produces that portable
Expand Down
Loading
Loading