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
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,15 @@ jobs:
# (ubuntu-latest ships >=3.11).
- name: DW-code catalog matches the compiler reference (both directions)
run: python3 tools/check-dw-codes.py
# The bot tier's dsl_version allowlist (harness/src/critical-path.ts) must
# never trail the compiler's own SUPPORTED_DSL_VERSION ceiling
# (crates/dsl/src/envelope.rs) — nothing else in the repo relates the two
# files, so a compiler version bump landed with zero signal to the harness
# and the bot tier refused every campaign at the newest dsl_version, after
# the server booted and the bot connected (task #157). Deterministic,
# offline, stdlib-only python3.
- name: harness dsl_version allowlist covers the compiler ceiling
run: python3 tools/check-harness-dsl-version.py
# Merge artifacts in the docs tree: a table row duplicated under one key, a
# doubled heading, a stray conflict marker. Two branches appending rows at
# the same table anchor merge cleanly and silently, so this class is
Expand Down
1 change: 1 addition & 0 deletions docs/reference/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ Never shipped inside a delve.
| `tools/check-dw-codes.py` | CI | `python3 tools/check-dw-codes.py` — asserts the DW catalog in `compiler.md` matches `crates/**/*.rs` both ways, and that every code has a test |
| `tools/check-doc-dupes.py` | CI | `python3 tools/check-doc-dupes.py [path …]` — merge-artifact gate over `docs/**/*.md` + `README.md`: no two body rows in one markdown table share a first-cell key, no heading repeats within a file, no git conflict markers. Kills the class that put `shortcuts[]` in the stage-5 table twice (owner finding 2026-08-03). Same-key rows in *different* tables are fine; a genuine same-table collision means restructure the table, not allowlist it |
| `tools/check-worker-override.py` | CI | `python3 tools/check-worker-override.py` — worker-isolation coverage gate: every service in `validation/compose.yaml` that pins a `container_name` or publishes `ports` must be reset (`!reset`) in `validation/worker-override.yaml`. Container names and host ports are Docker-GLOBAL, so `-p dw-worker-<x>` does not isolate them; the omission cost a run twice (`server` #190, then `bot`) |
| `tools/check-harness-dsl-version.py` | CI | `python3 tools/check-harness-dsl-version.py` — sync gate: the compiler's `SUPPORTED_DSL_VERSION` (`crates/dsl/src/envelope.rs`) must be a member of the harness's `SUPPORTED_DSL_VERSIONS` allowlist (`harness/src/critical-path.ts`). Nothing else relates the two files; spec-0026 moved the compiler to `0.9.0` while the harness allowlist still ended at `0.8.0`, and the bot tier refused every campaign at the version gate after the server booted and the bot connected (task #157) |
| `tools/extract-sound-registry.py` | maintenance | `python3 tools/extract-sound-registry.py <registries/data.min.json> <out.json>` — regenerates the compiler's sound registry for a new MC pin (positional args only, no `--help`) |
| `tools/extract-item-stack-sizes.py` | maintenance | `python3 tools/extract-item-stack-sizes.py <item_components/data.min.json> <out.json>` — regenerates `crates/compiler/data/item-stack-sizes-1.21.11.json`, the item→`max_stack_size` table `DW0436` reads, for a new MC pin (positional args only). Pins and checks the source SHA-256; refuses to default a missing component rather than assuming 64 |
| `tools/extract-item-combat-stats.py` | maintenance | `python3 tools/extract-item-combat-stats.py <item_components/data.min.json> <out.json>` — regenerates `crates/compiler/data/item-combat-1.21.11.json`, the item→`attack_damage`/`attack_speed`/`armor`/`armor_toughness`/`nutrition` table the spec-0023 winnability arithmetic reads (`DW0472`, `DW0474`), for a new MC pin (positional args only). Pins the source SHA-256 and refuses any non-`add_value` modifier rather than mis-summing it |
Expand Down
11 changes: 10 additions & 1 deletion harness/src/critical-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@
* VALIDATION surface — `branch-plan.json`, the chronicles, the per-branch paths —
* and change no step the bot walks, so a v0.8 path is walked exactly as a v0.6
* one is. Without them here, the first campaign to declare a branch could not be
* run at all, branch tier or not.
* run at all, branch tier or not. v0.9 (spec-0026) adds the stage-1
* horizon-library surface (`horizon` object form, new base/shorthand names) —
* world-generation input the compiler consumes to build the map, not a change
* to the step contract, so a v0.9 path is walked exactly as a v0.8 one is.
*
* This allowlist must never trail the compiler's own `SUPPORTED_DSL_VERSION`
* ceiling (`crates/dsl/src/envelope.rs`) — `tools/check-harness-dsl-version.py`
* enforces that in CI (task #157: a v0.9.0 campaign was refused at this gate,
* server booted and bot connected, because this list still ended at 0.8.0).
*/
export const SUPPORTED_DSL_VERSIONS = [
"0.2.0",
Expand All @@ -41,6 +49,7 @@ export const SUPPORTED_DSL_VERSIONS = [
"0.6.0",
"0.7.0",
"0.8.0",
"0.9.0",
] as const;

/**
Expand Down
14 changes: 14 additions & 0 deletions harness/test/critical-path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,20 @@ test("accepts the 0.5.0 and 0.6.0 dsl versions (additive; same path contract)",
}
});

test("accepts the 0.7.0, 0.8.0, and 0.9.0 dsl versions (additive; same path contract)", () => {
// v0.9 (spec-0026) adds the stage-1 horizon-library surface (object-form
// `horizon`, new base/shorthand names) — world-generation input the compiler
// consumes to build the map, not a change to the critical-path step contract
// the bot walks. task #157: the allowlist lagged the compiler's dsl_version
// ceiling and refused every 0.9.0 campaign at the gate before the bot took a
// single step.
for (const v of ["0.7.0", "0.8.0", "0.9.0"]) {
const raw = validRaw();
raw["version"] = v;
assert.equal(parseCriticalPath(raw).version, v);
}
});

test("parses an optional ending_tail_ticks on assert-complete (task #125)", () => {
const raw = validRaw();
(raw["steps"] as Array<Record<string, unknown>>)[3]!["ending_tail_ticks"] = 250;
Expand Down
113 changes: 113 additions & 0 deletions tools/check-harness-dsl-version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Harness/compiler `dsl_version` sync gate (task #157).

The compiler's DSL surface and the harness's critical-path allowlist are two
independent files that must agree on one number: the newest `dsl_version` a
campaign may declare. `crates/dsl/src/envelope.rs` names it once
(`SUPPORTED_DSL_VERSION`, the crate's own "latest version" identity constant);
`harness/src/critical-path.ts` names the harness's whole accepted set
(`SUPPORTED_DSL_VERSIONS`), which every artifact the bot tier parses
(critical-path, waypoints, combat plan) gates on.

Nothing in the repo ties these together, so a compiler version bump lands with
zero signal to the harness. That happened for real: spec-0026 raised
`SUPPORTED_DSL_VERSION` to `0.9.0` while `harness/src/critical-path.ts` still
listed `0.2.0 … 0.8.0` — the server booted, the bot connected, and the gate
refused the campaign before it took a single step (hollow-vigil ladder run,
task #157). Every other CI job was green.

The rule: the compiler's latest `dsl_version` must be a member of the
harness's `SUPPORTED_DSL_VERSIONS`. The harness is allowed to lag behind on
older-but-still-supported versions (this checks membership of the ceiling, not
set equality) but must never fall behind the compiler's own idea of "current".

Deterministic, offline, no dependencies (Python 3 stdlib). Run from the repo
root:
python3 tools/check-harness-dsl-version.py
Exit 0 = in sync, 1 = the harness lags the compiler (see stderr), 2 = usage/IO
error (missing file or the source no longer matches the expected shape — fix
the regex, don't loosen the check).
"""

import pathlib
import re
import sys

REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
ENVELOPE_RS = REPO_ROOT / "crates" / "dsl" / "src" / "envelope.rs"
CRITICAL_PATH_TS = REPO_ROOT / "harness" / "src" / "critical-path.ts"

# `pub const SUPPORTED_DSL_VERSION: &str = "0.9.0";` — the compiler's single
# "latest dsl_version this crate implements" identity constant.
COMPILER_VERSION_RE = re.compile(
r'pub\s+const\s+SUPPORTED_DSL_VERSION\s*:\s*&str\s*=\s*"([^"]+)"\s*;'
)

# `export const SUPPORTED_DSL_VERSIONS = [ "0.2.0", ..., "0.8.0" ] as const;`
HARNESS_ARRAY_RE = re.compile(
r"export\s+const\s+SUPPORTED_DSL_VERSIONS\s*=\s*\[(.*?)\]\s*as\s+const\s*;",
re.DOTALL,
)
VERSION_LITERAL_RE = re.compile(r'"([^"]+)"')


def compiler_version(text: str) -> str | None:
m = COMPILER_VERSION_RE.search(text)
return m.group(1) if m else None


def harness_versions(text: str) -> list[str]:
m = HARNESS_ARRAY_RE.search(text)
if not m:
return []
return VERSION_LITERAL_RE.findall(m.group(1))


def main() -> int:
for p in (ENVELOPE_RS, CRITICAL_PATH_TS):
if not p.is_file():
sys.stderr.write(f"error: {p} not found (run from the repo root)\n")
return 2

compiler_max = compiler_version(ENVELOPE_RS.read_text(encoding="utf-8"))
if compiler_max is None:
sys.stderr.write(
'error: could not find `pub const SUPPORTED_DSL_VERSION: &str = "...";` '
f"in {ENVELOPE_RS} — the constant was renamed/reshaped; update "
"COMPILER_VERSION_RE in tools/check-harness-dsl-version.py\n"
)
return 2

harness = harness_versions(CRITICAL_PATH_TS.read_text(encoding="utf-8"))
if not harness:
sys.stderr.write(
"error: could not find `export const SUPPORTED_DSL_VERSIONS = [...] as "
f"const;` (or it parsed empty) in {CRITICAL_PATH_TS} — the declaration was "
"reshaped; update HARNESS_ARRAY_RE in tools/check-harness-dsl-version.py\n"
)
return 2

if compiler_max not in harness:
sys.stderr.write(
"harness dsl_version allowlist lags the compiler:\n"
f" {ENVELOPE_RS}: SUPPORTED_DSL_VERSION = {compiler_max!r}\n"
f" {CRITICAL_PATH_TS}: SUPPORTED_DSL_VERSIONS = {harness!r}\n"
f" {compiler_max!r} is not in the harness allowlist.\n\n"
"Every campaign at the compiler's current dsl_version would be refused "
"at the bot-tier version gate before it takes a single step (task #157). "
"Add the compiler's SUPPORTED_DSL_VERSION to harness/src/critical-path.ts's "
"SUPPORTED_DSL_VERSIONS (additive addition only — do not remove older "
"entries still in use), and confirm no version-conditional harness "
"behavior needs updating for the new version's artifacts.\n"
)
return 1

print(
f"harness dsl_version allowlist covers the compiler ceiling "
f"({compiler_max!r} in {harness!r})"
)
return 0


if __name__ == "__main__":
sys.exit(main())
94 changes: 94 additions & 0 deletions tools/tests/test_check_harness_dsl_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""The harness/compiler `dsl_version` sync gate (`tools/check-harness-dsl-version.py`).

The defect this pins, from the field (task #157): the compiler's
`SUPPORTED_DSL_VERSION` moved to `0.9.0` (spec-0026) while
`harness/src/critical-path.ts`'s `SUPPORTED_DSL_VERSIONS` allowlist still ended
at `0.8.0`. Every other CI job was green — nothing else in the repo relates the
two files — and the bot tier refused every 0.9.0 campaign at the version gate,
after the server booted and the bot connected.

These tests drive the detector over synthetic source pairs so it keeps failing
for the right reason as the real files grow. The live pair is checked by the
CI step itself.
"""

import importlib.util
import pathlib

import pytest

SCRIPT = pathlib.Path(__file__).resolve().parents[1] / "check-harness-dsl-version.py"

ENVELOPE_TEMPLATE = """\
//! doc
pub const SUPPORTED_DSL_VERSION: &str = "{version}";
pub const SUPPORTED_DSL_VERSIONS: &[&str] = &["0.2.0", "{version}"];
"""

CRITICAL_PATH_TEMPLATE = """\
// doc
export const SUPPORTED_DSL_VERSIONS = [
{entries}
] as const;
"""


@pytest.fixture
def gate(tmp_path, monkeypatch):
"""The script loaded as a module, re-rooted at synthetic source files."""
spec = importlib.util.spec_from_file_location("check_harness_dsl_version", SCRIPT)
module = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(module)
monkeypatch.setattr(module, "ENVELOPE_RS", tmp_path / "envelope.rs")
monkeypatch.setattr(module, "CRITICAL_PATH_TS", tmp_path / "critical-path.ts")
return module


def pair(gate, compiler_version: str, harness_versions: list[str]) -> int:
gate.ENVELOPE_RS.write_text(
ENVELOPE_TEMPLATE.format(version=compiler_version), encoding="utf-8"
)
entries = "\n".join(f' "{v}",' for v in harness_versions)
gate.CRITICAL_PATH_TS.write_text(
CRITICAL_PATH_TEMPLATE.format(entries=entries), encoding="utf-8"
)
return gate.main()


def test_harness_covering_the_compiler_ceiling_passes(gate):
assert pair(gate, "0.9.0", ["0.2.0", "0.8.0", "0.9.0"]) == 0


def test_harness_lagging_the_compiler_ceiling_fails(gate, capsys):
# The task #157 reproduction: compiler moved to 0.9.0, harness allowlist
# still ends at 0.8.0.
assert pair(gate, "0.9.0", ["0.2.0", "0.7.0", "0.8.0"]) == 1
err = capsys.readouterr().err
assert "0.9.0" in err
assert "0.8.0" in err


def test_harness_may_carry_older_versions_the_compiler_dropped(gate):
# Membership of the ceiling, not set equality — the harness is allowed to
# keep accepting versions the compiler still emits/documents even if this
# synthetic pair only names the current one.
assert pair(gate, "0.5.0", ["0.2.0", "0.3.0", "0.4.0", "0.5.0"]) == 0


def test_missing_compiler_constant_is_a_usage_error(gate, capsys):
gate.ENVELOPE_RS.write_text("// no constant here\n", encoding="utf-8")
gate.CRITICAL_PATH_TS.write_text(
CRITICAL_PATH_TEMPLATE.format(entries=' "0.2.0",'), encoding="utf-8"
)
assert gate.main() == 2
assert "SUPPORTED_DSL_VERSION" in capsys.readouterr().err


def test_missing_harness_declaration_is_a_usage_error(gate, capsys):
gate.ENVELOPE_RS.write_text(
ENVELOPE_TEMPLATE.format(version="0.9.0"), encoding="utf-8"
)
gate.CRITICAL_PATH_TS.write_text("// no allowlist here\n", encoding="utf-8")
assert gate.main() == 2
assert "SUPPORTED_DSL_VERSIONS" in capsys.readouterr().err
Loading