Phase 5: scaffold Crucible (sow-tools) — dispatcher + builder shims, container, PR-first CI, fail-closed (D11, D19).

This commit is contained in:
2026-06-11 20:36:13 +02:00
commit 0d6eb2aeaa
28 changed files with 987 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
# Crucible command surface
Crucible re-homes the single `nwn-tool` (a.k.a. `sow-toolkit`) command surface
into five builders plus a dispatcher (D11). This table maps every legacy command
to its Crucible home. It is the migration contract — keep it in sync with
`internal/dispatch.Registry`.
## Builders
| Builder | Legacy `nwn-tool` commands subsumed |
| ------------------ | ------------------------------------------------------------------------------------- |
| `crucible-module` | `build`, `build-module`, `extract`, `validate`, `compare`, `apply-hak-manifest`† |
| `crucible-topdata` | `validate-topdata`, `build-topdata`, `build-top-package`, `compare-topdata`, `convert-topdata` |
| `crucible-hak` | `build-haks`, `apply-hak-manifest` |
| `crucible-wiki` | `build-wiki`, `deploy-wiki` |
| `crucible-depot` | (new) depot blob verify/move — was shell `mc`/S3 in `sow-assets-manifest` |
`apply-hak-manifest` is HAK-list maintenance on a module; it is reachable from
both `crucible-module` and `crucible-hak`. Canonical home is `crucible-hak`.
## Folded / global commands
These legacy commands are **not** top-level builders:
- `music *` (`list-datasets`, `scan`, `build`, `credits`, `validate`, `manifest`,
`normalize`) → folds into the **module/hak build pipeline** (music BMUs are
packed into HAKs at build time). Exposed as `crucible-module music ...` /
`crucible-hak music ...` when wired. Needs `ffmpeg` at runtime.
- `config *` (`validate`, `effective`, `inspect`, `explain`, `sources`) → a
**global** concern shared by every builder; exposed as a global subcommand
group, not its own binary.
- `build-changelog` → release tooling; lands as a global `crucible changelog`
rather than a content builder.
## Global commands (implemented in the scaffold)
```text
crucible help | -h usage + builder list
crucible version | -V build version (git sha via -ldflags)
crucible list builders, one per line: name<TAB>bin<TAB>summary
```
`crucible list` is machine-readable so CI can enumerate builders without parsing
help text.
## Global flags (planned, at wiring time)
`--quiet`, `--verbose`, `--debug` (the legacy verbosity model), passed after the
builder name, e.g. `crucible topdata build-topdata --verbose`.
+50
View File
@@ -0,0 +1,50 @@
# Crucible consumer contract
How the artifact repos (`sow-module`, `sow-topdata`, `sow-assets-manifest`)
locate and invoke a Crucible builder. The goal: **no local-machine assumptions**
(Phase 5 exit criterion). A consumer either finds a Crucible binary or fails
closed — it never fakes an artifact.
## Resolution order (single-token command)
Each consumer wrapper resolves its builder to one executable token, in order:
1. **Explicit override**`$SOW_MODULE_BUILD` / `$SOW_TOPDATA_BUILD` /
`$CRUCIBLE` (a path or name), if set and executable. Back-compat with the
pre-Crucible skeletons.
2. **Standalone Crucible binary**`crucible-module` / `crucible-topdata` /
`crucible-hak` / `crucible-depot` on `PATH`.
3. **Legacy name**`sow-module-build` / `sow-topdata-build` on `PATH`
(pre-D11; kept so nothing breaks during cutover).
If none resolve, the wrapper exits non-zero with a Phase 5 message. The
`crucible` dispatcher (`crucible module …`) is for humans and CI introspection;
wrappers prefer the single-token `crucible-<name>` shim so `"$builder" args`
quoting stays correct.
## In CI (preferred)
Run the consumer job inside the pinned image and the binaries are on `PATH`:
```yaml
container:
image: registry.westgate.pw/sow/crucible:<sha> # pinned, immutable
```
No host install, no `$HOME` layout, no developer machine assumptions.
## `NWN_ROOT` rule
Crucible **never guesses `NWN_ROOT` from `$HOME`** (this was a deploy-notes
pain point). The NWN install/data root is passed explicitly:
- env `NWN_ROOT=/path/to/nwn`, or
- flag `--nwn-root /path/to/nwn`.
A builder that needs `NWN_ROOT` and receives neither must fail closed with a
clear message, not fall back to a home-directory default.
## Determinism
Same inputs → identical output bytes. Consumers may checksum artifacts across
runs; non-determinism is a builder bug.
+48
View File
@@ -0,0 +1,48 @@
# Migrating from `nwn-tool` to Crucible (operator cutover)
The legacy toolchain lives at `gitea/sow-tools` as a single `nwn-tool` binary
(entry `cmd/nwn-tool`, logic under `internal/`). This repo is the Crucible
rewrite. Per the migration hard rules, the `internal/` source is **not**
transplanted automatically — the operator migrates it. This is the checklist.
## Why a clean scaffold first
- The hard rules forbid copying old-repo source into the migration tree
(`build from scratch; reference, don't transplant`).
- The downstream skeletons (`sow-module`, `sow-topdata`) already fail closed
until they can resolve a Crucible binary; the scaffold satisfies that contract
without faking artifacts.
- The scaffold pins the **shape** (module path, dispatcher, multi-binary layout,
container, CI) so the migrated logic drops into a stable frame.
## Cutover steps
1. **Move the internal packages.** Bring `internal/{app,pipeline,project,erf,
gff,topdata,music,changelog,validator}` from `gitea/sow-tools` into this repo.
Re-home `internal/app` command wiring behind `internal/dispatch` instead of
the old flag parser in `cmd/nwn-tool`.
2. **Restore dependencies.** Add `golang.org/x/text` and `gopkg.in/yaml.v3` back
to `go.mod`; commit the resulting `go.sum`. (`docker/Dockerfile` already
tolerates an absent `go.sum` for the scaffold; it will be present after this.)
3. **Wire each builder.** In `internal/dispatch`, register a handler per builder
and remove it from the unwired fail-closed path. Map the legacy commands per
[`command-surface.md`](command-surface.md).
4. **Kill the home-dir / `NWN_ROOT` defaulting.** Honor only explicit
env/flag roots per [`consumer-contract.md`](consumer-contract.md).
5. **Drop the committed binary.** Delete `nwn-tool` / `tools/sow-toolkit` from
the source tree; `.gitignore` already excludes them. Binaries become CI
artifacts and image layers only.
6. **Carry the determinism test.** Port
`TestBuildNativeKeepsGeneratedIDsStableAcrossRepeatedBuilds` and keep it green.
7. **Runtime base for music.** If music conversion is wired, switch the final
Docker stage from `distroless/static` to a debian-slim base that includes
`ffmpeg`.
8. **Update `make smoke`.** Once a builder is wired, change the smoke
expectation for it from exit `70` to a real success path.
## Wrapper contract carried over
Consumer repos keep the `SCRIPT_WRAPPER_CONTRACT` resolution behavior, now
pointed at Crucible binaries — see [`consumer-contract.md`](consumer-contract.md).
The old `SOW_TOOLS_DEV_BINARY` dev override maps to `$CRUCIBLE` /
`$SOW_MODULE_BUILD` / `$SOW_TOPDATA_BUILD`.