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
208 changes: 39 additions & 169 deletions docs/REDSTONE_MODULES.md
Original file line number Diff line number Diff line change
@@ -1,187 +1,57 @@
# Redstone Modules

## Status

This document defines the first implementation boundary for functional redstone builds after CraftDAG v0.2.0.
# Redstone modules: evidence and boundary

Redstone support is a deterministic module system, not arbitrary circuit synthesis. Agents select and configure verified modules; module expanders own exact block positions and states.

## First target

- Edition: Minecraft Java Edition
- Version target: `java_1_21`
- First module: `ItemSorterSlice`
- First operating mode: manual filter initialization
- Output path: module config -> validated module -> VoxelPlan -> schematic

Bedrock behavior, cross-version equivalence, and free-form redstone generation are out of scope.

## Why modules are separate

Architectural ComponentPlan components describe bounded shape and composition. Redstone modules additionally require:
CraftDAG has two independently verified MinePilot modules: `ItemSorterSlice` and a Java quasi-connected `PistonDoor`. Their static layouts establish a small shared structural kernel. Their behavior remains module-specific and is verified downstream in the official runtime.

- exact directional block states;
- stable signal input and output locations;
- required air and clearance cells;
- module-specific connectivity invariants;
- version-specific behavior;
- initialization and troubleshooting metadata;
- real-game verification evidence.
## Comparison of the verified implementations

These rules should not be spread across general architectural component validation.
The evidence sources are MinePilot [PR #134](https://github.com/i365dev/MinePilot/pull/134) for ItemSorterSlice and [PR #136](https://github.com/i365dev/MinePilot/pull/136) for PistonDoor (merged MinePilot commit `171926a535389a3446dcc9a1f4ca52f0a38e8afc`). CraftDAG's compact fixtures retain the static topology and four expected orientations, with provenance back to those proofs.

## Proposed public contract
| Concept | Shared structural evidence | Module-specific or external evidence |
| --- | --- | --- |
| Identity/version | Both have a stable module id and version. | The ids and versioning schemes are independent. |
| Canonical layout | Both define integer local coordinates, canonical bounds/facing, and role-tagged authored blocks. | Bounds and authored roles differ. |
| Block states | Both store concrete Minecraft block IDs and exact string properties. | Sorter uses hopper/comparator/repeater/wire/torch states; PistonDoor uses lever/piston states and relies on quasi-connectivity. |
| Orientation | Both rotate local coordinates, bounds, and horizontal `facing` deterministically. | ItemSorter also uses compass-key wire properties, which the kernel rotates. During non-zero turns only those directions and verified invariant properties are accepted; unknown properties fail closed. |
| Generic structure checks | Both use bounded coordinates and non-colliding occupied cells; roles identify authored blocks. | The kernel also requires unique valid role IDs. Required-role relationships and topology invariants remain module-specific. |
| VoxelPlan/provenance | Both lower to ordinary VoxelPlan blocks and carry roles through `sourceNodeId`. Identity/version is reflected in the plan name. | Runtime receipts, artifact digests, references, and behavioral claims remain in downstream evidence. |
| Reference conformance | Both were compared with an external reference. | The sorter has a reference block-table check; the door has an independent six-block reference gate and candidate/reference mechanical check. |
| Runtime semantics | Both have exact external runtime verification evidence. | Sorter initialization, item routing, threshold behavior, and inventory assertions are sorter-specific. Piston movement, reset/pullback, and quasi-connectivity are door-specific. |
| Ports and timing | No repeated public contract was proved. | Ports, signal graphs, timing semantics, and a generic scenario model remain unsupported. |

```ts
type RedstoneFacing = "north" | "east" | "south" | "west";
type RedstoneVersionTarget = "java_1_21";
## Proven shared kernel

type RedstoneModulePlan = {
version: "0.1";
name: string;
versionTarget: RedstoneVersionTarget;
bounds: { width: number; height: number; length: number };
modules: RedstoneModuleInstance[];
};
The internal experimental helper in `packages/core/src/redstone/moduleKernel.ts` accepts a canonical bounded static topology with:

type RedstoneModuleInstance = {
id: string;
type: "ItemSorterSlice";
placement: {
anchor: { x: number; y: number; z: number };
facing: RedstoneFacing;
};
options: {
filterItem: string;
fillerItem?: string;
initializeFilter?: false;
};
};
```
- module identity and version;
- canonical facing and positive integer bounds;
- unique role-tagged blocks at local integer coordinates;
- concrete `minecraft:*` block IDs and exact string properties.

`initializeFilter: true` is intentionally unsupported until VoxelPlan and the schematic exporter have an explicit block-entity contract.
It validates bounds, roles, collisions, and requested facings; rotates coordinates, bounds, horizontal `facing`, and compass-key properties for north/east/south/west; and lowers to the existing VoxelPlan shape. During non-zero rotations, `face`, `enabled`, `extended`, `lit`, `locked`, `mode`, `power`, `powered`, `type`, `waterlogged`, and `delay` are preserved because they occur in the verified fixtures as non-directional state. Any other property fails closed; with zero rotation properties remain unchanged. Block roles are preserved in `sourceNodeId`. Output blocks retain concrete IDs and states. The helper is experimental and is not exported from the public package entry point.

## Expansion rules
Compact static regression fixtures at `packages/core/test/fixtures/redstone/` preserve both canonical topologies and all four expected facing outputs. Tests compare kernel output against these checked-in values; expected transforms are not generated by the code under test. Fixtures contain no GameTest code, server packs, reports, logs, or artifact trees.

Each module has one canonical south-facing local layout. Expansion must:
## Module-specific semantics and verification

1. validate the module config;
2. rotate local coordinates and directional block states for the requested facing;
3. shift the layout to the anchor;
4. validate global bounds and clearance;
5. emit ordinary VoxelPlan blocks;
6. attach module metadata, initialization steps, and diagnostics.
The kernel does not validate circuit behavior or infer topology. Keep these rules with each module and its downstream verifier:

Rotation must use explicit state transforms. Coordinate rotation without updating properties such as `facing`, repeater delay, hopper direction, or comparator mode is invalid.
- **ItemSorterSlice:** filter inventory initialization, transport/overflow path, hopper and comparator relationships, threshold/repeater/torch behavior, and matching/non-matching item routing.
- **PistonDoor:** piston arrangement, moved blocks, activation/reset order, sticky pullback, and Java quasi-connectivity.
- **Verification:** MinePilot's official runtime receipts, state-exact schematic round trips, GameTest assertions, and module-specific reference comparisons remain external evidence. CraftDAG's regression tests prove static compilation and state preservation; they do not rerun Minecraft behavior.

## Module metadata
## Runtime-target boundary

The compiled result needs metadata alongside VoxelPlan:
Current VoxelPlan output contains concrete Minecraft block IDs and block-state properties. It is not edition-neutral. The completed #131 boundary remains in force: shared logical intent may lower to target-specific Redstone topology. The verified PistonDoor depends on Java quasi-connectivity, so its topology cannot be claimed portable to Bedrock. No Java-to-Bedrock mapping or public RuntimeTarget API is introduced here.

```ts
type CompiledRedstoneModule = {
plan: VoxelPlan;
moduleId: string;
moduleType: "ItemSorterSlice";
versionTarget: "java_1_21";
verified: boolean;
requiresManualInitialization: boolean;
ports: Array<{
id: string;
kind: "item_input" | "item_output" | "overflow_output" | "signal_output";
pos: Vec3;
facing: RedstoneFacing;
}>;
criticalBlocks: Array<{ role: string; pos: Vec3 }>;
initializationSteps: string[];
troubleshooting: string[];
};
```
## Explicitly deferred

This metadata must not be encoded as decorative blocks or inferred again from the compiled voxel set.
The two proofs do not establish support for:

## Validation stages
- public ports or generic signal inputs/outputs;
- signal graphs, automatic Redstone routing, or arbitrary circuit synthesis;
- timing, tick, or generic runtime-scenario DSLs;
- module composition or repetition semantics;
- Bedrock topology mapping or edition-neutral VoxelPlan;
- a public `RedstoneModulePlan` API.

### Schema validation

- known module type and version target;
- integer, non-negative anchor;
- supported facing;
- valid namespaced item identifiers;
- unsupported initialization options rejected explicitly.

### Spatial validation

- rotated module bounds stay inside the plan;
- required air cells remain clear;
- module instances do not collide unless a declared port connection permits it;
- input, output, and maintenance clearance remain accessible.

### Static redstone validation

- filter hopper exists and faces the expected output direction;
- comparator reads the filter hopper;
- repeater/comparator properties match the rotated facing;
- torch lock path and dust path contain every required block;
- output hopper/container path exists;
- all critical positions contain the expected block and state.

### Verification status

Unit tests prove deterministic expansion and invariants. They do not prove Minecraft runtime behavior. A module becomes `verified: true` only after an in-game Java 1.21 smoke test is recorded with the fixture version.

## ItemSorterSlice stages

### Stage 0: Manual initialization fixture

- deterministic single-slice layout;
- exact hopper/comparator/repeater/torch/dust states;
- material and layer output;
- schematic export;
- initialization and troubleshooting instructions;
- static invariant tests;
- `requiresManualInitialization: true`.

### Stage 1: Block entities

Add a typed block-entity payload to VoxelPlan and schematic export before supporting initialized hopper inventories. The contract must preserve coordinates, NBT payloads, and version targeting through round trips.

### Stage 2: Arrays

Build `ItemSorterArray` by repeating verified slices through declared ports and spacing rules. Do not duplicate the single-slice layout by hand.

## Package layout

Start inside core while the API is experimental:

```text
packages/core/src/redstone/
types.ts
schema.ts
compileRedstoneModulePlan.ts
orientation.ts
modules/itemSorterSlice.ts
validation/itemSorterSlice.ts
```

Split a package only when versioning or dependency pressure justifies it.

## Required tests for the first fixture

- all four facings produce rotated coordinates and block states;
- repeated compilation is byte-for-byte deterministic;
- every critical invariant has a failing negative test;
- bounds and clearance failures return structured diagnostics;
- material and layer guides include redstone blocks correctly;
- schematic export/import preserves directional block states;
- unsupported automatic initialization fails clearly;
- the fixture includes a documented Java 1.21 in-game smoke-test checklist.

## Non-goals

- arbitrary redstone coordinates authored by an LLM;
- a browser tick simulator;
- Bedrock redstone;
- automatic support for every Java version;
- block-entity initialization hidden behind untyped objects;
- claims of working behavior without an in-game verification record.
Add such abstractions only when further real evidence justifies them. Do not infer them from these two modules.
133 changes: 133 additions & 0 deletions packages/core/src/redstone/moduleKernel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import type { Vec3, VoxelPlan } from "../types.js";

/** Experimental structural core for already-authored, concrete module layouts. */
export type HorizontalFacing = "north" | "east" | "south" | "west";

export interface StaticModuleBlock {
role: string;
pos: Vec3;
block: string;
properties: Record<string, string>;
}

export interface StaticModuleDefinition {
identity: { id: string; version: string };
canonicalFacing: HorizontalFacing;
bounds: Vec3;
blocks: StaticModuleBlock[];
}

const FACINGS: HorizontalFacing[] = ["north", "east", "south", "west"];
const FACING_INDEX = new Map(FACINGS.map((facing, index) => [facing, index]));
// These properties occur in the two verified topologies and do not encode a
// horizontal direction. Unknown properties must not silently survive rotation.
const INVARIANT_PROPERTIES = new Set([
"delay",
"enabled",
"extended",
"face",
"lit",
"locked",
"mode",
"power",
"powered",
"type",
"waterlogged",
]);

function assertFacing(value: string): asserts value is HorizontalFacing {
if (!FACING_INDEX.has(value as HorizontalFacing)) throw new Error(`unsupported horizontal facing: ${value}`);
}

function assertDefinition(module: StaticModuleDefinition): void {
if (!module.identity.id.trim() || !module.identity.version.trim()) throw new Error("module identity and version must be non-empty");
if (!Array.isArray(module.bounds) || module.bounds.length !== 3 || module.bounds.some((value) => !Number.isInteger(value) || value <= 0)) {
throw new Error("module bounds must be three positive integers");
}
assertFacing(module.canonicalFacing);
if (!module.blocks.length) throw new Error("module must contain at least one authored block");

const roles = new Set<string>();
const positions = new Set<string>();
for (const entry of module.blocks) {
if (!/^[a-z][a-z0-9_]*$/.test(entry.role)) throw new Error(`invalid block role: ${entry.role}`);
if (roles.has(entry.role)) throw new Error(`duplicate block role: ${entry.role}`);
roles.add(entry.role);
if (!Array.isArray(entry.pos) || entry.pos.length !== 3 || entry.pos.some((value) => !Number.isInteger(value))) {
throw new Error(`block ${entry.role} position must contain three integers`);
}
const [x, y, z] = entry.pos;
const [width, height, depth] = module.bounds;
if (x < 0 || y < 0 || z < 0 || x >= width || y >= height || z >= depth) {
throw new Error(`block ${entry.role} is outside module bounds`);
}
const coordinate = entry.pos.join(",");
if (positions.has(coordinate)) throw new Error(`occupied-coordinate collision at ${coordinate}`);
positions.add(coordinate);
if (!/^minecraft:[a-z0-9_./-]+$/.test(entry.block)) throw new Error(`invalid concrete Minecraft block id: ${entry.block}`);
for (const [property, value] of Object.entries(entry.properties)) {
if (typeof value !== "string") throw new Error(`block property ${property} on ${entry.role} must be a string`);
if (property === "facing") assertFacingOrVertical(value);
}
}
}

function assertFacingOrVertical(value: string): void {
if (value === "up" || value === "down") return;
assertFacing(value);
}

function rotateState(properties: Record<string, string>, turns: number): Record<string, string> {
const output: Record<string, string> = {};
for (const property of Object.keys(properties).sort()) {
const value = properties[property];
if (property === "facing") {
output[property] = value === "up" || value === "down" ? value : rotateFacing(value as HorizontalFacing, turns);
} else if (FACING_INDEX.has(property as HorizontalFacing)) {
output[rotateFacing(property as HorizontalFacing, turns)] = value;
} else if (turns === 0 || INVARIANT_PROPERTIES.has(property)) {
output[property] = value;
} else {
throw new Error(`unsupported block-state property during rotation: ${property}`);
}
}
return output;
}

function rotateFacing(facing: HorizontalFacing, turns: number): HorizontalFacing {
const index = FACING_INDEX.get(facing);
if (index === undefined) throw new Error(`unsupported horizontal facing: ${facing}`);
return FACINGS[(index + turns) % FACINGS.length];
}

function rotatePosition(position: Vec3, bounds: Vec3, turns: number): Vec3 {
let [x, , z] = position;
const y = position[1];
let [width, , depth] = bounds;
for (let turn = 0; turn < turns; turn += 1) {
[x, z] = [depth - 1 - z, x];
[width, depth] = [depth, width];
}
return [x, y, z];
}

/** Compile concrete authored topology into the existing ordinary VoxelPlan shape. */
export function compileStaticModule(module: StaticModuleDefinition, facing: HorizontalFacing): VoxelPlan {
assertDefinition(module);
assertFacing(facing);
const turns = (FACING_INDEX.get(facing)! - FACING_INDEX.get(module.canonicalFacing)! + FACINGS.length) % FACINGS.length;
const [width, height, depth] = module.bounds;
const size: Vec3 = turns % 2 === 0 ? [width, height, depth] : [depth, height, width];

return {
version: "0.1",
name: `${module.identity.id}@${module.identity.version} (${facing})`,
size,
origin: [0, 0, 0],
blocks: module.blocks.map((entry) => ({
pos: rotatePosition(entry.pos, module.bounds, turns),
block: { name: entry.block, properties: rotateState(entry.properties, turns) },
sourceNodeId: entry.role,
})),
};
}
Loading
Loading