diff --git a/.claude/board/INTEGRATION_PLANS.md b/.claude/board/INTEGRATION_PLANS.md index 3a3623658..458374e17 100644 --- a/.claude/board/INTEGRATION_PLANS.md +++ b/.claude/board/INTEGRATION_PLANS.md @@ -1,3 +1,8 @@ +## 2026-11-10 — D-FOUNDRY-PARITY-1: DR × DT64 as an operational ontology → `.claude/plans/foundry-parity-v1.md` + +**Status:** ACTIVE; Level R lab landed (`crates/foundry-parity-lab`). Evidence matrix over 18 Foundry capability rows, missing-contract register G1–G6, roadmap P0–P5. [entry](entries/2026-11-10-foundry-parity-1-lab.md) · STATUS_BOARD D-FOUNDRY-*. +**Confidence:** lab MEASURED; every Foundry-side cell is documented capability only (no comparator). No row claims parity. + ## 2026-11-06 — D-LANESOCK: DR × DT64 property-to-lane socket and joint version witness → `.claude/plans/d-lanesock-dr-dt64-lane-socket-v1.md` **Status:** NORMATIVE SPEC (D-LANESOCK-0); implementation D-LANESOCK-1 is a separate draft PR. **Board:** [STATUS_BOARD](STATUS_BOARD.md) D-LANESOCK. Follows #1964/#1965 and OGAR#371. diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 9267decab..19b13af4e 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -4,6 +4,9 @@ Execution admitted = meaning valid (OGAR/DT64) ∧ representation compatible (DR | D-id | Deliverable | Status | |---|---|---| +| D-FOUNDRY-PARITY-1 | Foundry parity evidence matrix + 64-store read-only lab (`crates/foundry-parity-lab`): CanCook / CanFulfill / NetworkCapacity / network cost via DT64 `borrow_all` + mask-RISC, oracle + materialized null model, refusals, negative controls | In PR | +| D-FOUNDRY-G1 | Element-wise cross-lane arithmetic (per-store max / limiting resource / per-store cost) in an admitted operator | Queued (decision needed) | +| D-FOUNDRY-G2..G6 | IAM in the read path; scenario overlay frame; governed action gate; bound relations; method lowering from `MethodRef` | Queued | | D-SC-E1 | `MapTable::map_at` fenced as `diagnose_at` → `MapDiagnostic`; executable maps only via `map(frame)` | Shipped (#1981) | | D-SC-E2a | dt64 `CodeTable` (versioned code space, advances the lanes' FrameStore) + quack `bind_typed` / `TypedQuery::admit` (`StaleCodeSpace`, `Unwitnessed`) | In PR | | D-SC-E2b | per-owner adoption (`CamLabels`, dir-sim, sap): each real mutation path invalidates stale execution | Queued | diff --git a/.claude/board/entries/2026-11-10-foundry-parity-1-lab.md b/.claude/board/entries/2026-11-10-foundry-parity-1-lab.md new file mode 100644 index 000000000..93a8259db --- /dev/null +++ b/.claude/board/entries/2026-11-10-foundry-parity-1-lab.md @@ -0,0 +1,31 @@ +# 2026-11-10 — D-FOUNDRY-PARITY-1: 64-store operating network over DT64 + mask-RISC + +**Status:** MEASURED (lab, read-only Level R; not production admission). Crate: +`crates/foundry-parity-lab`. Plan + evidence matrix: +`.claude/plans/foundry-parity-v1.md`. + +**Question.** Can source-owned planes, one DT64 `LaneBinding` and mask-RISC +programs answer `CanCook` / `CanFulfill` / `NetworkCapacity` / network cost +over 64 stores × 64 categories with exact oracle parity and correct refusals? + +**Answer.** Yes for WHERE-shaped feasibility, fulfilment, network capacity and +network cost: 16 tests agree with an independent scalar oracle and a +materialized-object null model; 0 source bytes copied; stale frame, foreign +scope, other row order, unmapped field, unbound field and out-of-range +quantity are refused; unknown stock is never read as zero; the denied store +is reported NotAuthorized. Five disable runs turn the suite red. + +**No** for the per-store limiting resource and per-store cost: the admitted +algebra has no element-wise cross-lane arithmetic (register G1). The +per-store maximum is reachable only by a layer-cake sweep of monotone masks, +measured ~15 µs against 1.8 µs for walking already-built objects (which cost +21 µs and 51 968 B to build). One WHERE question: 470 ns, ~4× cheaper than +arm A. One VM (Xeon 2.30 GHz, 4 vCPU), median of 41, three runs. + +**Not established:** any Foundry parity (no Foundry comparator); production +IAM (lab stand-in predicate); scenarios through DR (oracle only; no overlay +frame); actions/writeback (no contract); bound relations (ADMISSION B). + +**OPEN:** G1 element-wise arithmetic decision; G2 IAM in the read path; G3 +overlay frame; G4 action gate; G5 bound relations; G6 method lowering from +`MethodRef`. Scaling past one mask word not measured. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 7d336cb7f..0ca6ef1cb 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,11 +25,12 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately opposite directions; the stranding this convention prevents shows up in exactly one of them, never both. -591 entries, 2026-08-06 .. 2026-11-10. +592 entries, 2026-08-06 .. 2026-11-10. | date | entry id | finding | file | |---|---|---|---| | 2026-11-10 | `lab-resolve-borrow-all` | | [2026-11-10-lab-resolve-borrow-all.md](2026-11-10-lab-resolve-borrow-all.md) | +| 2026-11-10 | `D-FOUNDRY-PARITY-1` | | [2026-11-10-foundry-parity-1-lab.md](2026-11-10-foundry-parity-1-lab.md) | | 2026-11-10 | `dt64-lane-borrow-all` | | [2026-11-10-dt64-lane-borrow-all.md](2026-11-10-dt64-lane-borrow-all.md) | | 2026-11-10 | `dt64-int8-facet-stride` | | [2026-11-10-dt64-int8-facet-stride.md](2026-11-10-dt64-int8-facet-stride.md) | | 2026-11-10 | `D-DT64-GEMM-SHAPE-1` | | [2026-11-10-dt64-gemm-shape.md](2026-11-10-dt64-gemm-shape.md) | diff --git a/.claude/plans/foundry-parity-v1.md b/.claude/plans/foundry-parity-v1.md new file mode 100644 index 000000000..0ebdcffa0 --- /dev/null +++ b/.claude/plans/foundry-parity-v1.md @@ -0,0 +1,226 @@ +# D-FOUNDRY-PARITY-1 — DR × DT64 as an operational ontology: evidence matrix and first lab + +> **Status:** ACTIVE (lab slice landed in this PR; everything past Level R is OPEN). +> **Lab:** `crates/foundry-parity-lab` (own workspace root, not a member). +> **Contract read first:** `.claude/knowledge/dr-dt64-locality-execution-contract.md` +> (its own evidence table grades Foundry parity HYPOTHESIS; this plan does not +> change that grade). +> **Verify:** `cargo test --release --manifest-path crates/foundry-parity-lab/Cargo.toml` +> and `cargo run --release --manifest-path crates/foundry-parity-lab/Cargo.toml --bin bench`. + +## 1. Overview + +The question is not "can we clone Foundry". It is: can source-owned SoA planes, +addressed through DR and interpreted through DT64, answer *operational* +questions about an organisation — what can it produce, where, at what cost, +under whose authorization — and how far that is from Foundry's documented +Ontology capabilities. Nothing here was compared against a running Foundry +instance; Foundry columns cite its public documentation only, as evidence of +*documented* capability, never of internal implementation or performance. + +## 2. Checklist + +- [x] Read the DR/DT64 locality contract, `lane.rs` (`LaneBinding`, + `borrow_all`), the #1987/#1989 lab, mask-RISC IR (`Pred`, `MaskOp`, + `Terminal`), `MethodBinding`/`MethodRef`, `contract::scenario`, `lance-graph-rbac`. +- [x] Evidence matrix (§4). +- [x] Lab: deterministic 64-store × 64-category fixture, source owner, 211-field class. +- [x] Independent scalar oracle + materialized-object null model (arm A). +- [x] `CanCook`, `CanFulfill`, `NetworkCapacity`, network ingredient cost through DT64 + mask-RISC (arm B). +- [x] Named cases 01–09, refusals, negative controls (§5). +- [x] Disable runs: five guards broken one at a time, suite red each time, tree restored. +- [x] Bench in ns/µs (§6). +- [ ] P1: element-wise cross-lane arithmetic in an admitted operator (per-store max, limiting resource, per-store cost) — see §7 G1. +- [ ] P3: scenario overlay frame (§7 G3); governed action / writeback gate (§7 G4). +- [ ] Production IAM in the read path instead of the lab stand-in (§7 G2). + +## 3. Architecture of the lab (what is where) + +``` +fixture (only value definition) + │ ingest (cold, once) + ▼ +Network ── source owner: 211 typed planes in ITS row order + its FrameStore + │ LaneSource::lane_of / borrow (addresses only; never a copy) + ▼ +World ── DT64: ClassView basis (211 fields), route, BindStore + │ bind_lanes(demanded fields only) → LaneBinding (cold, ~5.5 µs) + │ borrow_all (one check, N lookups) → pointer table (~0.26 µs) + ▼ +ItemQuery ── mask-RISC programs over the borrowed planes, per question + CanCook(q) = visible ∧ offered ∧ equip ∧ ⋀_k(known_k ∧ stock_k ≥ d_k·q) ∧ staff ≥ ⌈q/u⌉ + CanFulfill(q,t) = CanCook(q) ∧ slots ≥ 1 ∧ minutes ≤ t − prep + NetworkCapacity = Σ_q Count(CanCook(q)) (lawful by monotonicity, checked) + network cost(q) = Σ_k d_k·q · MaskedSumI32(CanCook(q), cost_k) + per-store max = layer-cake readback of CanCook(q) masks +``` + +X = 64 stores (rows), Y = 64 categories (stock / known / cost lanes), Z = +typed operational interpretation (staff skills, equipment, offered items, +delivery slots and minutes, the identity lane). Z is not one homogeneous axis +and nothing is materialized as a 64 × 64 × 64 tensor: the dense Cartesian +product never exists. The source holds 35 168 bytes; arm B copies 0 of them. + +Identity: positions become stores only by reading the bound `storeId` lane at +a result's set bits; results are keyed by global identity, so a source in +another row order answers identically (tested). + +Authorization: a **lab stand-in** — the reader is denied global id 1006, +expressed as `NeU32(storeId, 1006)` inside the program. DR's `Scope` is +checked only as a binding consistency witness (locality contract, boundary +5); it never authorizes. The stand-in is not IAM. + +## 4. Foundry parity evidence matrix + +Statuses: IMPLEMENTED (source + tested) · LAB (executable, oracle-validated) · +PARTIAL · PROPOSED · BLOCKED · ABSENT · UNKNOWN. Verdicts: PARITY-DEMONSTRATED · +FUNCTIONALLY-RELATED · LOWER-LEVEL-ADVANTAGE-HYPOTHESIS · MISSING · BLOCKED · +NOT-COMPARABLE · UNKNOWN. No row claims PARITY-DEMONSTRATED: nothing was +compared against Foundry. + +| # | Foundry capability (documented) | DR/DT64 counterpart | Source evidence | Status | Missing contract | Smallest falsifier | Verdict | +|---|---|---|---|---|---|---|---| +| 1 | Object types + typed properties | `ClassView` basis + `ClassSchema` + typed source lanes | `lance-graph-dt64/src/lib.rs` (`register`, `ClassSchema`), `lane.rs`; lab `bind.rs` (211 fields) | LAB | Property types are the source's, not declared in DT64 (lane kind checked only at use: `ArmRefusal::PlaneKind`) | Bind a mask field where an i32 lane is expected → refused | FUNCTIONALLY-RELATED | +| 2 | Stable identity + provenance | DR route + witnessed coordinate; global id lane | `RouteAt`, `CoordSpace`, `Frame`; lab reorder test | LAB | No per-value provenance (who wrote this stock, from which system) | Two sources disagree on a value; no witness says which wins | PARTIAL → MISSING for provenance | +| 3 | Link types / traversal (search-around) | Relation lanes; `graph_hop::OutgoingHop` | mask-RISC `graph_hop` is feature-gated OFF (value-directed gather banned, ADMISSION B) | BLOCKED | Admitted relation traversal over source-owned planes; recipe→ingredient is a fixture function here, not a bound relation | Bind recipe demand as a relation lane and traverse without Gather | BLOCKED | +| 4 | Object sets, filtering (WHERE) | Scoped WHERE population = mask program | lab `cook_mask`, `fulfill_mask` | LAB | — | Parity suite (8 items × 3 q × 64 stores) | FUNCTIONALLY-RELATED; LOWER-LEVEL-ADVANTAGE-HYPOTHESIS (0 bytes copied, 470 ns / question at 64 rows) | +| 5 | Computed properties / functions | `MethodBinding` (ON×WHEN×DO) + `MethodRef`; mask-RISC terminals | `lib.rs:215`, `:353`; DT64 "executes nothing" | PARTIAL | Executable lowering from a `MethodRef` to a mask-RISC program (the lab hand-writes the programs) | Register `CanCook` as a MethodBinding and execute it via `admit` | PARTIAL | +| 6 | Versioned method behaviour | `SigRev`, `GuardWitness`, `Stamp` | `lib.rs:345-370` | IMPLEMENTED (identity) / ABSENT (execution) | Same as 5 | Stale `SigRev` refused at execution | PARTIAL | +| 7 | Actions / state changes | none on this path | `MethodBinding::effects` is descriptive | ABSENT | Governed transition contract (§7 G4) | — | MISSING | +| 8 | Multiple sources + sync | `LaneSource` per owner, `FrameStore` per source | `lane.rs` `source_owns`; lab uses one source | PARTIAL | Cross-source composition; G2L across differing spaces is refused, not translated | Two sources, different orders, one question | BLOCKED (G2L OPEN in the locality contract) | +| 9 | Source ownership, logical materialization | One canonical owner; borrowed lanes | lab: arm B copies 0 bytes; arm A copies 51 968 | LAB | — | `only_demanded_fields_are_bound_and_borrowed_once` | LOWER-LEVEL-ADVANTAGE-HYPOTHESIS | +| 10 | Security / authorization | DR `Scope` (consistency only) + `lance-graph-rbac` (class/op grants) | `rbac/src/lib.rs`; lab stand-in `NeU32` | PARTIAL | Row-level authorization decided by IAM and applied in the read path; RBAC is not wired to DT64 | `bypassing_authorization_reveals_the_denied_store` | MISSING (production) | +| 11 | History / replay | `FrameStore` revisions; Lance versions | lab stale-frame refusal + rebind | LAB (invalidation) / UNKNOWN (replay at a past version through DT64) | Bind at a historical frame | Answer at frame N after the source moved to N+1 | PARTIAL | +| 12 | Derived state + invalidation | `LaneBinding::check` on every open | lab `a_stale_frame_is_refused_and_a_rebind_sees_the_restock` | LAB | Incremental recompute of only the changed subset (lab recomputes the whole question; it is 64 rows) | — | FUNCTIONALLY-RELATED | +| 13 | What-if scenarios (beta in Foundry docs) | `contract::scenario::ScenarioBranch` (facade) | `lance-graph-contract/src/scenario.rs` | PARTIAL; DR path BLOCKED | Overlay frame witness for DT64 bindings (§7 G3) | lab `the_scenario_is_oracle_only_and_lowers_capacity` (oracle only) | BLOCKED | +| 14 | Aggregations / analytics | mask-RISC `Count`, `MaskedSumI32`, group terminals | lab `network_capacity`, `network_cost` | LAB | Per-row cross-lane arithmetic (§7 G1) | Per-store cost through an admitted terminal | PARTIAL | +| 15 | Operational applications | a2ui-rs / consumer CustomObject | not exercised here | UNKNOWN | — | — | NOT-COMPARABLE here | +| 16 | Audit / decision provenance | Witnesses (route, frame, basis) per answer | `LaneBinding` fields | PARTIAL | Decision record: who asked, which witnesses, which answer | — | MISSING | +| 17 | Data connections / egress | Source adapters (OGAR, Lance) | outside this lab | UNKNOWN | — | — | NOT-COMPARABLE here | +| 18 | AI-assisted, governed decisions | none on this path | — | ABSENT | Depends on 7, 10, 16 | — | MISSING | + +Correspondence hypotheses (§4 of the brief), graded: `ClassView` ↔ object +type: FUNCTIONALLY-RELATED, but ClassView alone carries no security, search, +history or writeback. `MethodRef` ↔ Action Type: **rejected** — a `MethodRef` +names a read-only method revision; it is not a transaction, has no +preconditions, idempotency or commit. Object set ↔ WHERE mask: LAB-supported. + +## 5. The lab, case by case + +| Case | Expected | Arm B | Test | +|---|---|---|---| +| Store 01 | feasible, max 30, limit staff | ✓ (limit oracle/arm A only) | `the_named_cases_answer_as_designed` | +| Store 02 | max 0, cheese | ✓ | same | +| Store 03 | max 0, no cook | ✓ | same | +| Store 04 | max 0, oven | ✓ | same | +| Store 05 | q=1 yes, q=2 no | ✓ | same | +| Store 06 | no → yes after restock + rebind; stale binding refused | ✓ | `a_stale_frame_is_refused_and_a_rebind_sees_the_restock` | +| Store 07 | NotAuthorized, never "cannot cook" | ✓ | named cases + `bypassing_authorization_reveals_the_denied_store` | +| Store 08 | same answers in another row order, by identity; canonical selection refused | ✓ | `another_row_order_*` | +| Stale frame | refused (`SourceChanged`) | ✓ | stale test | +| Store 09 | unknown cheese ≠ zero; bypass reads the stale 1000 as stock | ✓ | `unknown_is_not_zero`, `bypassing_the_known_guard_*` | + +Also refused: a lane serving another scope; a field the source does not map; +a question needing a field that was not bound (`network_cost` without cost +lanes); quantity outside `1..=128`. Disable runs (each red, then restored): +staff predicate threshold → 0; denied mask → empty; ingredient threshold off +by one q; unknown mask → empty; fulfil deadline → `i32::MAX`. + +Not exercised: the `NotMonotone` and `Ceiling` guards of the layer-cake sweep +are defensive; no admitted fixture can reach them (feasibility predicates are +all `≥ threshold(q)` with thresholds non-decreasing in q). They are labelled, +not claimed as tested. + +## 6. Measurement (MEASURED) + +`cargo run --release --manifest-path crates/foundry-parity-lab/Cargo.toml --bin bench`; +one VM, Intel Xeon @ 2.30 GHz, 4 vCPU, single thread, median of 41 warm runs, +three runs, ranges across runs: + +| Stage | ns | +|---|---| +| cold: `World::new` (register + route) | 24 770–25 351 | +| cold: `bind_lanes`, 15 demanded of 211 fields | 5 500–5 511 | +| cold: `ItemQuery::open` (`borrow_all` + pointer table) | 254–272 | +| warm: `CanCook(q=1)`, one program, 64 stores | 470–474 | +| warm: `CanFulfill(q=2, 45 min)` | 569–577 | +| warm: per-store answers (layer-cake max, ~31 programs) | 15 146–15 199 | +| warm: `NetworkCapacity` (Σ_q Count) | 13 666–13 944 | +| warm: network cost q=3 (3 × MaskedSum) | 1 367–1 375 | +| arm A: build 64 objects (51 968 B second copy) | 20 818–23 216 | +| arm A: `CanCook(q=1)` over 64 objects | 1 792–1 844 | +| arm A: `NetworkCapacity` | 1 811–1 922 | + +Reading, not spin: a single WHERE-shaped question is ~4× cheaper in arm B and +copies nothing; but the per-store maximum and the network capacity are ~8× +**more** expensive in arm B than walking already-built objects, because the +admitted algebra has no element-wise min across lanes and must sweep q. Arm +A's build cost (21 µs) amortises after roughly two capacity questions. At 64 +rows every mask is one word, so mask-RISC's fixed per-program cost (program +construction + validation, not separated here) dominates; scaling is OPEN. + +## 7. Missing-contract register + +| Id | Missing operation | Why the current contract cannot express it | Lawful alternative today | Smallest test | Needs | +|---|---|---|---|---|---| +| G1 | Element-wise cross-lane arithmetic: `min_k ⌊stock_k / d_k⌋` per row, argmin (limiting resource), per-row weighted sum (per-store cost) | mask-RISC terminals reduce over rows; predicates compare a lane to a constant | Layer-cake over monotone masks (max only, ~30× programs); no alternative for argmin or per-row cost | Per-store max and cost in one program, equal to oracle | Decision + mask-RISC/Quack PR (H2) | +| G2 | Row-level authorization from IAM in the read path | DR `Scope` is per lane and consistency-only; RBAC grants are class/op, not wired to DT64 | Lab stand-in predicate on the identity lane | Same lab tests with the predicate supplied by `lance-graph-rbac` | Decision on where IAM evaluates (H3) | +| G3 | Scenario overlay over a binding | No frame witness for a proposed, non-canonical plane | Oracle only | Store 12 closed: overlay answer = oracle, canonical frame unchanged | Overlay frame contract (H3) | +| G4 | Governed action / writeback | No command identity, preconditions, idempotency, atomic commit or audit on this path; `MethodBinding::effects` is descriptive | none | Preview vs commit of "reassign order 12→17" with a stale-precondition refusal | Separate gate; not in a read-only lab | +| G5 | Bound relations (recipe → ingredient, menu → recipe) | Value-directed traversal is Gather, banned outside ADMISSION B | Relations as fixture functions (lab) | Bind recipe demand as lanes and answer without Gather | ADMISSION B decision | +| G6 | Methods executed from `MethodRef` | DT64 binds behaviour, executes nothing; programs are hand-written | Hand-written programs | `CanCook` registered as a `MethodBinding`, lowered and executed | Lowering PR | + +## 8. The three hypotheses + +- **H1 (semantic contract suffices for read-only):** *partly supported.* + WHERE-shaped feasibility, fulfilment, capacity and network cost are exact + through existing contracts with no copy. Falsified for anything needing a + per-row value combining several lanes. +- **H2 (missing algebra):** *supported by G1, G5.* The limiting resource and + per-store cost cannot be expressed; the maximum only by a costly sweep. +- **H3 (infrastructure):** *supported by G2, G3, G4, G6* but untested in + depth here: authorization, scenarios, actions and method execution all lack + a contract on this path. + +None is chosen over the others; all three hold in part. + +## 9. Roadmap + +- **P0 (this PR):** the lab and this matrix. +- **P1:** G1 — decide whether element-wise cross-lane arithmetic enters + mask-RISC/Quack, then re-run the per-store questions against the oracle. +- **P2:** G6 + G5 — `CanCook` as a registered method lowered from its + `MethodRef`; recipe demand as bound relation lanes. +- **P3:** G3 scenario overlay frame; G4 action gate (preview vs commit) as a + separate workstream; G2 IAM in the read path. +- **P4:** real source connectors, a2ui screens, deployment. +- **P5:** AI-assisted decisions, only after P3's governance exists. + +No effort estimates: there is no basis for them. + +## 10. Foundry documentation used + +Fetched 2026-11-10 (HTTP 200, no redirects); paraphrased: + +- `docs/foundry/ontology` — the Ontology is an operational layer over + datasets, virtual tables and models, with semantic elements (objects, + properties, links) and kinetic elements (actions, functions, dynamic + security). +- `docs/foundry/functions/api-object-sets` (TSv1) — an object set is an + unordered collection of objects of one type; supports filtering, Search + Arounds along link types, and aggregations. +- `docs/foundry/object-edits/overview` — an Action is a single transaction + changing properties of one or more objects; edits add/remove links and + create/delete objects. +- `docs/foundry/action-types/permissions` — view / edit / apply are separate + permissions; row and column controls filter what is read when invoking an + action and do not extend to the action's write. +- `docs/foundry/ontology/overview-ontology-scenario` — **Beta**; a sandbox of + edits applied by actions over Ontology data; not a data-versioning tool. +- `docs/foundry/object-backend/overview` — Object Storage v2 is the canonical + store backing the Ontology; v1 (Phonograph) marked for deprecation. + +Consequences for the matrix: Foundry scenarios are built *from actions*, so +row 13 depends on row 7 here too; Foundry separates read controls from write +authorization, which is the same split G2 / G4 keep apart. Nothing here claims +knowledge of Foundry internals or performance. diff --git a/Cargo.toml b/Cargo.toml index 214702aa4..d56fc05a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -230,6 +230,10 @@ exclude = [ # ndarray's strided BF16 GEMM entry. Verify via # `cargo test --release --manifest-path crates/dr-isa-stream-lab/Cargo.toml`. "crates/dr-isa-stream-lab", + # D-FOUNDRY-PARITY-1 LAB: 64-store operating network over DT64 + mask-RISC, + # read-only. Own workspace root. Verify via + # `cargo test --release --manifest-path crates/foundry-parity-lab/Cargo.toml`. + "crates/foundry-parity-lab", # ERA5 0.25-degree global grid baked onto the canonical 512-byte NodeRow -- # one cell = one node, one timestep = one Lance version. Standalone and # genuinely zero-dep (see the crate manifest for why helix is NOT an diff --git a/crates/foundry-parity-lab/Cargo.toml b/crates/foundry-parity-lab/Cargo.toml new file mode 100644 index 000000000..15014f8dc --- /dev/null +++ b/crates/foundry-parity-lab/Cargo.toml @@ -0,0 +1,31 @@ +# D-FOUNDRY-PARITY-1 LAB: a 64-store operating network bound through DT64 and +# answered by mask-RISC programs, against an independent scalar oracle and a +# materialized-object null model. Read-only operational evaluation only (Level +# R); no scenario overlay and no writeback. NOT a workspace member, nothing +# depends on it, no public contract. Verify with +# cargo test --release --manifest-path crates/foundry-parity-lab/Cargo.toml +# cargo run --release --manifest-path crates/foundry-parity-lab/Cargo.toml --bin bench +# Evidence matrix: .claude/plans/foundry-parity-v1.md +[package] +name = "foundry-parity-lab" +version = "0.0.0" +edition = "2021" +publish = false +license = "MIT OR Apache-2.0" +description = "D-FOUNDRY-PARITY-1 lab: can source-owned planes, a DT64 lane binding and mask-RISC programs answer CanCook / CanFulfill / NetworkCapacity over 64 stores with exact oracle parity and correct refusals?" + +# Own workspace root, like the other lab probes. +[workspace] + +[dependencies] +# Consumed read-only through their public APIs; none is changed. +lance-graph-dt64 = { path = "../lance-graph-dt64" } +lance-graph-mask-risc = { path = "../lance-graph-mask-risc" } +lance-graph-contract = { path = "../lance-graph-contract" } + +[[test]] +name = "integration" +path = "tests/integration/main.rs" + +[profile.release] +debug = false diff --git a/crates/foundry-parity-lab/src/arm_a.rs b/crates/foundry-parity-lab/src/arm_a.rs new file mode 100644 index 000000000..be4cc925e --- /dev/null +++ b/crates/foundry-parity-lab/src/arm_a.rs @@ -0,0 +1,112 @@ +//! ARM A: the conventional materialized null model. +//! +//! Builds one owned object per store from the fixture, then answers by +//! walking objects. This is a locally constructed comparator, NOT a claim +//! about how Foundry represents objects internally. It deliberately makes the +//! second copy the virtual arm avoids, and its byte count is reported. + +use crate::fixture::{self as fx, CATEGORIES, DENIED_STORE, EQUIPMENT, ITEMS, SKILLS, STORES}; +use crate::oracle::{Cook, Limit}; + +/// One materialized store. +#[derive(Debug, Clone)] +pub struct StoreObject { + pub id: u32, + /// `None` = unknown stock. + pub stock: Vec>, + pub cost: Vec, + pub staff: Vec, + pub equipment: Vec, + pub offered: Vec, + pub slots: i32, + pub minutes: i32, +} + +/// The materialized network. +pub struct Objects { + pub stores: Vec, +} + +impl Objects { + /// Build every object (the preparation cost of this arm). + pub fn build(restocked: bool) -> Objects { + let stores = (0..STORES) + .map(|s| StoreObject { + id: fx::store_id(s), + stock: (0..CATEGORIES) + .map(|k| fx::known(s, k).then(|| fx::stock(s, k, restocked))) + .collect(), + cost: (0..CATEGORIES).map(|k| fx::unit_cost(s, k)).collect(), + staff: (0..SKILLS).map(|j| fx::staff(s, j)).collect(), + equipment: (0..EQUIPMENT).map(|e| fx::equipment(s, e)).collect(), + offered: (0..ITEMS).map(|m| fx::offered(s, m)).collect(), + slots: fx::delivery_slots(s), + minutes: fx::delivery_minutes(s), + }) + .collect(); + Objects { stores } + } + + /// Heap bytes the objects hold (vectors' payloads only). + pub fn bytes(&self) -> usize { + self.stores + .iter() + .map(|o| { + o.stock.len() * 8 + + o.cost.len() * 4 + + o.staff.len() * 4 + + o.equipment.len() + + o.offered.len() + + 16 + }) + .sum() + } + + /// `CanCook` on one object. + pub fn can_cook(&self, s: usize, m: usize, q: i32) -> Cook { + let o = &self.stores[s]; + if o.id == DENIED_STORE { + return Cook::NotAuthorized; + } + let r = fx::recipe(m); + let mut stock = Vec::with_capacity(r.demand.len()); + for &(k, d) in &r.demand { + match o.stock[k] { + Some(v) => stock.push((k, v / d)), + None => return Cook::Unknown { category: k }, + } + } + let (max, limit) = if !o.offered[m] { + (0, Limit::NotOffered) + } else if !o.equipment[r.equipment] { + (0, Limit::Equipment(r.equipment)) + } else { + let mut best = (i32::MAX, Limit::NotOffered); + for (k, cap) in stock { + if cap < best.0 { + best = (cap, Limit::Ingredient(k)); + } + } + let cap = o.staff[r.skill] * r.per_staff; + if cap < best.0 { + best = (cap, Limit::Staff(r.skill)); + } + best + }; + Cook::Known { + feasible: max >= q, + max, + limit, + } + } + + /// `NetworkCapacity` on the objects. + pub fn network_capacity(&self, m: usize) -> i64 { + (0..STORES) + .map(|s| match self.can_cook(s, m, 1) { + Cook::Known { max, .. } => i64::from(max), + _ => 0, + }) + .sum() + } +} diff --git a/crates/foundry-parity-lab/src/arm_b.rs b/crates/foundry-parity-lab/src/arm_b.rs new file mode 100644 index 000000000..08d16ce6b --- /dev/null +++ b/crates/foundry-parity-lab/src/arm_b.rs @@ -0,0 +1,448 @@ +//! ARM B: the virtual operating model. +//! +//! `source-owned planes → DT64 LaneBinding (cold, once) → borrow_all (one +//! check) → mask-RISC programs (per question)`. Every answer is a mask +//! program over borrowed planes; nothing is copied and no per-store object +//! exists. Positions become store identities only by reading the bound +//! `storeId` lane at the set bits of a result mask. +//! +//! What this arm can NOT answer, by construction of the admitted algebra +//! (mask-RISC terminals reduce over rows; there is no element-wise cross-lane +//! arithmetic): the per-store limiting resource (an argmin across lanes) and +//! the per-store cost (a per-row weighted sum across lanes). It answers the +//! per-store maximum only through a layer-cake of feasibility masks, which is +//! lawful because feasibility is monotone in the quantity; the sweep checks +//! that monotonicity instead of assuming it. + +use std::cell::RefCell; + +use lance_graph_contract::class_view::ClassView; +use lance_graph_dt64::dr::FrameStore; +use lance_graph_dt64::{BindStore, LaneBinding, LaneRefusal}; +use lance_graph_mask_risc::{ + execute, tile_words_for, ExecError, LaneRef, MaskOp, Operand, Planes, Pred, Program, Scratch, + Terminal, Value, +}; + +use crate::bind::{COST0, EQUIP0, KNOWN0, MINUTES, OFFERED0, SLOTS, STAFF0, STOCK0, STORE_ID}; +use crate::fixture::{self as fx, Recipe, DENIED_STORE, STORES, STORE_ID_BASE}; +use crate::source::Plane; + +/// Quantities above this are refused rather than swept. +pub const Q_MAX: i32 = 128; + +/// The fields a question about item `m` demands. Nothing else is bound. +pub fn demanded_fields(m: usize, cost: bool, delivery: bool) -> Vec { + let r = fx::recipe(m); + let mut f = Vec::new(); + for &(k, _) in &r.demand { + f.push(STOCK0 + k); + f.push(KNOWN0 + k); + if cost { + f.push(COST0 + k); + } + } + f.extend([ + STAFF0 + r.skill, + EQUIP0 + r.equipment, + OFFERED0 + m, + STORE_ID, + ]); + if delivery { + f.extend([SLOTS, MINUTES]); + } + f.sort_unstable(); + f.into_iter().map(|x| x as u8).collect() +} + +/// The guards a program applies. Every guard is on in a real answer; the +/// negative controls turn exactly one off to show it is load-bearing. +#[derive(Debug, Clone, Copy)] +pub struct Guards { + /// Stores the reader is denied are excluded (IAM stand-in). + pub authorization: bool, + /// A demanded category with unknown stock excludes the store. + pub known: bool, +} + +impl Guards { + pub const ALL: Guards = Guards { + authorization: true, + known: true, + }; +} + +/// One store's answer as this arm produces it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Answer { + NotAuthorized, + Unknown, + Known { feasible: bool, max: i32 }, +} + +/// Why this arm could not answer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ArmRefusal { + Lane(LaneRefusal), + Exec(ExecError), + /// A bound plane had another kind than its field requires. + PlaneKind { + field: u8, + }, + /// A demanded field was not part of the binding. + NotBound { + field: u8, + }, + /// Feasibility at `q` was not a subset of feasibility at `q - 1`. + NotMonotone { + q: i32, + }, + /// Some store is still feasible at [`Q_MAX`]. + Ceiling, + /// `q` outside `1..=Q_MAX`. + Quantity { + q: i32, + }, +} + +impl From for ArmRefusal { + fn from(r: LaneRefusal) -> Self { + ArmRefusal::Lane(r) + } +} + +impl From for ArmRefusal { + fn from(e: ExecError) -> Self { + ArmRefusal::Exec(e) + } +} + +/// Where a bound field sits in the mask-RISC planes. +#[derive(Debug, Clone, Copy)] +enum Slot { + Mask(u16), + Lane(u16), +} + +/// The pointer table for one item: borrowed planes, made once per binding. +pub struct ItemQuery<'s> { + item: usize, + recipe: Recipe, + masks: Vec<&'s [u64]>, + lanes: Vec>, + slots: [Option; 256], + ids: &'s [u32], + scratch: RefCell>, +} + +impl<'s> ItemQuery<'s> { + /// Check the binding once and take every bound lane (`borrow_all`). + pub fn open( + item: usize, + binding: &LaneBinding, + store: &BindStore, + view: &V, + frames: &FrameStore, + source: &'s S, + ) -> Result, ArmRefusal> + where + S: for<'a> lance_graph_dt64::LaneSource = Plane<'a>>, + { + let mut masks = Vec::new(); + let mut lanes = Vec::new(); + let mut slots = [None; 256]; + let mut ids = None; + for lane in binding.borrow_all(store, view, frames, source)? { + let (field, plane) = lane?; + slots[usize::from(field)] = Some(match plane { + Plane::Mask(m) => { + masks.push(m); + Slot::Mask((masks.len() - 1) as u16) + } + Plane::I32(v) => { + lanes.push(LaneRef::I32(v)); + Slot::Lane((lanes.len() - 1) as u16) + } + Plane::U32(v) => { + if usize::from(field) == STORE_ID { + ids = Some(v); + } + lanes.push(LaneRef::U32(v)); + Slot::Lane((lanes.len() - 1) as u16) + } + }); + } + let ids = ids.ok_or(ArmRefusal::NotBound { + field: STORE_ID as u8, + })?; + Ok(ItemQuery { + item, + recipe: fx::recipe(item), + masks, + lanes, + slots, + ids, + scratch: RefCell::new(Scratch::new(tile_words_for(STORES), 2)), + }) + } + + fn mask(&self, f: usize) -> Result { + match self.slots[f] { + Some(Slot::Mask(i)) => Ok(Operand::Plane(i)), + Some(Slot::Lane(_)) => Err(ArmRefusal::PlaneKind { field: f as u8 }), + None => Err(ArmRefusal::NotBound { field: f as u8 }), + } + } + + fn lane(&self, f: usize) -> Result { + match self.slots[f] { + Some(Slot::Lane(i)) => Ok(i), + Some(Slot::Mask(_)) => Err(ArmRefusal::PlaneKind { field: f as u8 }), + None => Err(ArmRefusal::NotBound { field: f as u8 }), + } + } + + /// Ops leaving `CanCook(item, q)` in scratch 0. + fn cook_ops(&self, q: i32, g: Guards) -> Result, ArmRefusal> { + if !(1..=Q_MAX).contains(&q) { + return Err(ArmRefusal::Quantity { q }); + } + let r = &self.recipe; + let acc = Operand::Scratch(0); + let tmp = Operand::Scratch(1); + let and = |b| MaskOp::And { a: acc, b, dst: 0 }; + let mut ops = Vec::with_capacity(4 + 3 * r.demand.len()); + let offered = self.mask(OFFERED0 + self.item)?; + let equip = self.mask(EQUIP0 + r.equipment)?; + if g.authorization { + ops.push(self.visible(0)?); + ops.push(and(offered)); + } else { + ops.push(MaskOp::And { + a: offered, + b: offered, + dst: 0, + }); + } + ops.push(and(equip)); + for &(k, d) in &r.demand { + if g.known { + ops.push(and(self.mask(KNOWN0 + k)?)); + } + ops.push(MaskOp::Pred { + pred: Pred::GeI32 { + lane: self.lane(STOCK0 + k)?, + t: d * q, + }, + under: None, + dst: 1, + }); + ops.push(and(tmp)); + } + ops.push(MaskOp::Pred { + pred: Pred::GeI32 { + lane: self.lane(STAFF0 + r.skill)?, + t: (q + r.per_staff - 1) / r.per_staff, + }, + under: None, + dst: 1, + }); + ops.push(and(tmp)); + Ok(ops) + } + + /// The IAM stand-in: the reader is denied exactly [`DENIED_STORE`], + /// matched on the global identity lane, so it holds in any row order. + fn visible(&self, dst: u16) -> Result { + Ok(MaskOp::Pred { + pred: Pred::NeU32 { + lane: self.lane(STORE_ID)?, + v: DENIED_STORE, + }, + under: None, + dst, + }) + } + + fn run(&self, ops: Vec, terminal: Terminal) -> Result { + let program = Program::new(ops, terminal); + let planes = Planes { + n_rows: STORES, + masks: &self.masks, + lanes: &self.lanes, + }; + let mut scratch = self.scratch.borrow_mut(); + Ok(execute(&program, &planes, &mut scratch, None)?) + } + + fn keep(&self, ops: Vec) -> Result { + match self.run( + ops, + Terminal::Keep { + mask: Operand::Scratch(0), + }, + )? { + Value::Mask(_) => Ok(self.scratch.borrow().slot(0).map_or(0, |s| s[0])), + _ => unreachable!("Keep yields a mask"), + } + } + + /// `CanCook(item, q)` over the whole network: bit `r` = physical row `r`. + pub fn cook_mask(&self, q: i32, g: Guards) -> Result { + self.keep(self.cook_ops(q, g)?) + } + + /// Stores the reader is denied. + pub fn denied_mask(&self) -> Result { + // 64 rows fill the one mask word exactly (fixture asserts it). + Ok(!self.keep(vec![self.visible(0)?])?) + } + + /// Visible stores where some demanded category's stock is unknown. + pub fn unknown_mask(&self) -> Result { + let mut ops = vec![self.visible(0)?]; + let first = self.mask(KNOWN0 + self.recipe.demand[0].0)?; + ops.push(MaskOp::And { + a: first, + b: first, + dst: 1, + }); + for &(k, _) in &self.recipe.demand[1..] { + ops.push(MaskOp::And { + a: Operand::Scratch(1), + b: self.mask(KNOWN0 + k)?, + dst: 1, + }); + } + ops.push(MaskOp::AndNot { + a: Operand::Scratch(0), + b: Operand::Scratch(1), + dst: 0, + }); + self.keep(ops) + } + + /// The global identity at physical row `r`. + pub fn store_at(&self, r: usize) -> u32 { + self.ids[r] + } + + /// The per-store maximum by layer-cake: `max[s]` is the largest `q` whose + /// feasibility mask holds `s`. Indexed by store row (`id - BASE`), not by + /// physical row. Stops at the first empty mask. + pub fn max_quantities(&self, g: Guards) -> Result<[i32; STORES], ArmRefusal> { + let mut max = [0i32; STORES]; + let mut prev = u64::MAX; + for q in 1..=Q_MAX { + let m = self.cook_mask(q, g)?; + if m & !prev != 0 { + return Err(ArmRefusal::NotMonotone { q }); + } + if m == 0 { + return Ok(max); + } + let mut bits = m; + while bits != 0 { + let r = bits.trailing_zeros() as usize; + max[(self.ids[r] - STORE_ID_BASE) as usize] = q; + bits &= bits - 1; + } + prev = m; + } + Err(ArmRefusal::Ceiling) + } + + /// Every store's answer at quantity `q`, indexed by store row. + pub fn answers(&self, q: i32, g: Guards) -> Result<[Answer; STORES], ArmRefusal> { + // The sweep below runs its own valid quantities; the caller's `q` is + // only compared against the result, so it is checked here. + if !(1..=Q_MAX).contains(&q) { + return Err(ArmRefusal::Quantity { q }); + } + let denied = self.denied_mask()?; + let unknown = self.unknown_mask()?; + let max = self.max_quantities(g)?; + let mut out = [Answer::Unknown; STORES]; + for r in 0..STORES { + let s = (self.ids[r] - STORE_ID_BASE) as usize; + out[s] = if g.authorization && denied >> r & 1 == 1 { + Answer::NotAuthorized + } else if g.known && unknown >> r & 1 == 1 { + Answer::Unknown + } else { + Answer::Known { + feasible: max[s] >= q, + max: max[s], + } + }; + } + Ok(out) + } + + /// `NetworkCapacity(item)` = Σ_q Count(CanCook(item, q)): each term is + /// one mask-RISC `Count` program; monotonicity makes the sum Σ_s max_s. + pub fn network_capacity(&self) -> Result { + let mut total = 0i64; + for q in 1..=Q_MAX { + match self.run( + self.cook_ops(q, Guards::ALL)?, + Terminal::Count { + mask: Operand::Scratch(0), + }, + )? { + Value::Count(0) => return Ok(total), + Value::Count(c) => total += c as i64, + _ => unreachable!("Count yields a count"), + } + } + Err(ArmRefusal::Ceiling) + } + + /// Ingredient cost of making `q` at every store that can: + /// Σ_k d_k·q · MaskedSumI32(CanCook(q), cost_k). Needs the cost lanes. + pub fn network_cost(&self, q: i32) -> Result { + let mut total = 0i64; + for &(k, d) in &self.recipe.demand { + let lane = self.lane(COST0 + k)?; + match self.run( + self.cook_ops(q, Guards::ALL)?, + Terminal::MaskedSumI32 { + mask: Operand::Scratch(0), + lane, + }, + )? { + Value::SumI64(s) => total += i64::from(d * q) * s, + _ => unreachable!("MaskedSumI32 yields a sum"), + } + } + Ok(total) + } + + /// `CanFulfill(item, q, deadline)`: cook ∧ a free slot ∧ on time. + pub fn fulfill_mask(&self, q: i32, deadline: i32) -> Result { + let mut ops = self.cook_ops(q, Guards::ALL)?; + let acc = Operand::Scratch(0); + for pred in [ + Pred::GeI32 { + lane: self.lane(SLOTS)?, + t: 1, + }, + Pred::LeI32 { + lane: self.lane(MINUTES)?, + t: deadline - self.recipe.prep_minutes, + }, + ] { + ops.push(MaskOp::Pred { + pred, + under: None, + dst: 1, + }); + ops.push(MaskOp::And { + a: acc, + b: Operand::Scratch(1), + dst: 0, + }); + } + self.keep(ops) + } +} diff --git a/crates/foundry-parity-lab/src/bin/bench.rs b/crates/foundry-parity-lab/src/bin/bench.rs new file mode 100644 index 000000000..71ddbecf7 --- /dev/null +++ b/crates/foundry-parity-lab/src/bin/bench.rs @@ -0,0 +1,129 @@ +//! D-FOUNDRY-PARITY-1 bench: median of 41 warm runs per stage, nanoseconds. +//! Any stage at or above one millisecond is printed as a FAILED GATE, never +//! as a result (CLAUDE.md, "no millisecond results"). + +use std::hint::black_box; +use std::time::Instant; + +use foundry_parity_lab::arm_a::Objects; +use foundry_parity_lab::arm_b::{demanded_fields, Guards, ItemQuery}; +use foundry_parity_lab::bind::{World, READER, SPACE_A}; +use foundry_parity_lab::fixture::STORES; +use foundry_parity_lab::source::{Network, Order}; +use lance_graph_dt64::LaneSource; + +const RUNS: usize = 41; + +fn median(mut f: impl FnMut()) -> u128 { + f(); + let mut t: Vec = (0..RUNS) + .map(|_| { + let s = Instant::now(); + f(); + s.elapsed().as_nanos() + }) + .collect(); + t.sort_unstable(); + t[RUNS / 2] +} + +fn report(name: &str, ns: u128) { + if ns >= 1_000_000 { + println!("{name:<44} FAILED GATE (>= 1 ms; the path left the fold)"); + } else { + println!("{name:<44} {ns:>10} ns"); + } +} + +fn main() { + let net = Network::ingest(Order::Identity); + let fields = demanded_fields(0, true, true); + println!( + "item 0, {STORES} stores, {} demanded of 211 fields", + fields.len() + ); + println!("source resident bytes {}", net.resident_bytes()); + println!( + "arm A object bytes (second copy) {}", + Objects::build(false).bytes() + ); + println!("arm B bytes copied from source 0 (scratch: 2 words)"); + println!(); + + report( + "cold: World::new (register + route)", + median(|| { + black_box(World::new()); + }), + ); + let world = World::new(); + report( + "cold: bind_lanes (demanded fields)", + median(|| { + black_box(world.bind(&net, &fields, SPACE_A, READER).unwrap()); + }), + ); + let binding = world.bind(&net, &fields, SPACE_A, READER).unwrap(); + report( + "cold: open (borrow_all + pointer table)", + median(|| { + black_box( + ItemQuery::open(0, &binding, &world.store, &world.view, net.frames(), &net) + .unwrap(), + ); + }), + ); + let q = ItemQuery::open(0, &binding, &world.store, &world.view, net.frames(), &net).unwrap(); + report( + "warm: CanCook(q=1) one program, 64 stores", + median(|| { + black_box(q.cook_mask(black_box(1), Guards::ALL).unwrap()); + }), + ); + report( + "warm: CanFulfill(q=2, 45 min)", + median(|| { + black_box(q.fulfill_mask(2, 45).unwrap()); + }), + ); + report( + "warm: per-store answers (layer-cake max)", + median(|| { + black_box(q.answers(1, Guards::ALL).unwrap()); + }), + ); + report( + "warm: NetworkCapacity (Σ_q Count)", + median(|| { + black_box(q.network_capacity().unwrap()); + }), + ); + report( + "warm: network cost q=3 (Σ_k MaskedSum)", + median(|| { + black_box(q.network_cost(3).unwrap()); + }), + ); + println!(); + report( + "arm A: build 64 objects", + median(|| { + black_box(Objects::build(false)); + }), + ); + let objects = Objects::build(false); + report( + "arm A: CanCook(q=1) over 64 objects", + median(|| { + for s in 0..STORES { + black_box(objects.can_cook(s, 0, 1)); + } + }), + ); + report( + "arm A: NetworkCapacity", + median(|| { + black_box(objects.network_capacity(0)); + }), + ); +} diff --git a/crates/foundry-parity-lab/src/bind.rs b/crates/foundry-parity-lab/src/bind.rs new file mode 100644 index 000000000..cdd861cb8 --- /dev/null +++ b/crates/foundry-parity-lab/src/bind.rs @@ -0,0 +1,179 @@ +//! The store-network class: its field basis (the DT64 menu), its route, and +//! binding the fields a question demands through the DT64 lane socket. + +use lance_graph_contract::class_view::{ClassId, ClassView, WideFieldMask}; +use lance_graph_contract::ontology::{DisplayTemplate, FieldRef}; +use lance_graph_dt64::dr::{Extent, Scope}; +use lance_graph_dt64::{ + BindStore, ClassSchema, CoordSpace, LaneBinding, LaneRefusal, LaneRequest, LaneSource, RouteAt, + SchemaRev, +}; + +use crate::fixture::{CATEGORIES, EQUIPMENT, ITEMS, SKILLS}; +use crate::source::Network; + +/// The lab's class (lab-local id; no OGAR concept is minted for it). +pub const CLASS: u32 = 0x5116_0001; +/// The ClassView key of the class. +pub const VIEW_CLASS: ClassId = 0x5116; +/// DR position the class is routed at. +pub const POSITION: u16 = 7; +/// The scope the canonical lanes serve. A consistency witness, NOT an +/// authorization (locality contract, boundary 5). +pub const READER: Scope = Scope { id: 9 }; +/// The canonical row order. +pub const SPACE_A: CoordSpace = CoordSpace { + population: 64, + coordinates: 1, +}; +/// The same 64 stores in another row order. +pub const SPACE_REVERSED: CoordSpace = CoordSpace { + population: 64, + coordinates: 2, +}; + +pub const STOCK0: usize = 0; +pub const KNOWN0: usize = STOCK0 + CATEGORIES; +pub const COST0: usize = KNOWN0 + CATEGORIES; +pub const STAFF0: usize = COST0 + CATEGORIES; +pub const EQUIP0: usize = STAFF0 + SKILLS; +pub const OFFERED0: usize = EQUIP0 + EQUIPMENT; +pub const SLOTS: usize = OFFERED0 + ITEMS; +pub const MINUTES: usize = SLOTS + 1; +pub const STORE_ID: usize = MINUTES + 1; +/// Fields of the class: 211, so the class needs a 256-wide property axis. +pub const FIELD_COUNT: usize = STORE_ID + 1; + +/// What a field is. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Kind { + Stock(usize), + Known(usize), + Cost(usize), + Staff(usize), + Equipment(usize), + Offered(usize), + DeliverySlots, + DeliveryMinutes, + StoreId, +} + +/// The kind of field `f`. +pub fn field(f: u8) -> Kind { + let f = usize::from(f); + match f { + _ if f < KNOWN0 => Kind::Stock(f - STOCK0), + _ if f < COST0 => Kind::Known(f - KNOWN0), + _ if f < STAFF0 => Kind::Cost(f - COST0), + _ if f < EQUIP0 => Kind::Staff(f - STAFF0), + _ if f < OFFERED0 => Kind::Equipment(f - EQUIP0), + _ if f < SLOTS => Kind::Offered(f - OFFERED0), + SLOTS => Kind::DeliverySlots, + MINUTES => Kind::DeliveryMinutes, + _ => Kind::StoreId, + } +} + +fn predicate(k: Kind) -> String { + match k { + Kind::Stock(i) => format!("ops:stock/{i}"), + Kind::Known(i) => format!("ops:stockKnown/{i}"), + Kind::Cost(i) => format!("ops:unitCost/{i}"), + Kind::Staff(i) => format!("ops:staff/{i}"), + Kind::Equipment(i) => format!("ops:equipment/{i}"), + Kind::Offered(i) => format!("ops:offers/{i}"), + Kind::DeliverySlots => "ops:deliverySlots".into(), + Kind::DeliveryMinutes => "ops:deliveryMinutes".into(), + Kind::StoreId => "ops:storeId".into(), + } +} + +/// The ClassView of the store-network class. +pub struct View(Vec); + +impl ClassView for View { + fn fields(&self, class: ClassId) -> &[FieldRef] { + if class == VIEW_CLASS { + &self.0 + } else { + &[] + } + } + fn template(&self, _class: ClassId) -> DisplayTemplate { + DisplayTemplate::Detail + } + fn dolce_category_id(&self, _class: ClassId) -> u8 { + 0 + } +} + +/// Store, view and route of the class. +pub struct World { + pub store: BindStore, + pub view: View, + pub route: RouteAt, +} + +impl Default for World { + fn default() -> World { + World::new() + } +} + +impl World { + pub fn new() -> World { + let view = View( + (0..FIELD_COUNT) + .map(|f| FieldRef::new(predicate(field(f as u8)), format!("F{f}"))) + .collect(), + ); + let mut store = BindStore::new(); + store + .register( + &view, + CLASS, + ClassSchema { + schema_rev: SchemaRev(1), + field_count: FIELD_COUNT, + extent: Extent::DR256_SQ_X64, + }, + Vec::new(), + ) + .expect("the network class registers"); + let version = store.route(POSITION, CLASS).expect("the class routes"); + World { + store, + view, + route: RouteAt { + position: POSITION, + version, + }, + } + } + + /// Cold: bind exactly `fields` for `reader` under `selection`, at the + /// source's current frame. + pub fn bind( + &self, + source: &Network, + fields: &[u8], + selection: CoordSpace, + reader: Scope, + ) -> Result { + let mask = WideFieldMask::from_positions(fields); + let frame = source.frames().seal(); + self.store.bind_lanes( + &self.view, + source.frames(), + source, + LaneRequest { + route: self.route, + rev: SchemaRev(1), + fields: &mask, + frame: &frame, + scope: reader, + selection, + }, + ) + } +} diff --git a/crates/foundry-parity-lab/src/fixture.rs b/crates/foundry-parity-lab/src/fixture.rs new file mode 100644 index 000000000..1dda0131b --- /dev/null +++ b/crates/foundry-parity-lab/src/fixture.rs @@ -0,0 +1,192 @@ +//! The deterministic operating network. The only definition of its values: +//! the source, the oracle and the materialized null model all read it here, +//! and nothing else defines a value. +//! +//! All data is synthetic. Store rows `0..64` carry global identities +//! `STORE_ID_BASE + row`; rows `0..9` are hand-set for the named cases +//! (see [`case`]), every other row is derived from a hash of its coordinates. + +/// Stores (X). +pub const STORES: usize = 64; +// Every mask in this lab is exactly one u64 word. +const _: () = assert!(STORES == 64); +/// Inventory / resource categories (Y). +pub const CATEGORIES: usize = 64; +/// Equipment types. +pub const EQUIPMENT: usize = 4; +/// Staff skills. +pub const SKILLS: usize = 4; +/// Menu items. +pub const ITEMS: usize = 8; +/// Global identity of row 0. +pub const STORE_ID_BASE: u32 = 1000; + +/// Named rows (row index = store number - 1). +pub mod case { + /// Has all ingredients and sufficient staff for item 0. + pub const ALL_GOOD: usize = 0; + /// Lacks one critical ingredient (cheese = 0). + pub const NO_CHEESE: usize = 1; + /// Has inventory but no qualified cook. + pub const NO_COOK: usize = 2; + /// Has staff, but the oven is unavailable. + pub const NO_OVEN: usize = 3; + /// Stock for exactly one unit of item 0, not two. + pub const EXACTLY_ONE: usize = 4; + /// No cheese until an inventory update gives it 50. + pub const RESTOCKED: usize = 5; + /// Not visible to the lab reader (IAM stand-in denies it). + pub const HIDDEN: usize = 6; + /// Used by the reordered-source test; ordinary values. + pub const REORDERED: usize = 7; + /// Cheese stock is UNKNOWN; the slot holds a stale 1000. + pub const UNKNOWN_CHEESE: usize = 8; +} + +/// Category indices item 0 uses. +pub const FLOUR: usize = 0; +pub const CHEESE: usize = 1; +pub const SAUCE: usize = 2; +/// The stale value an unknown slot carries. +pub const STALE: i32 = 1000; + +/// A recipe: what one unit of a menu item consumes and needs. +#[derive(Debug, Clone)] +pub struct Recipe { + /// `(category, units per item)`, categories distinct and ascending. + pub demand: Vec<(usize, i32)>, + pub equipment: usize, + pub skill: usize, + /// Items one qualified employee can produce in the window. + pub per_staff: i32, + pub prep_minutes: i32, +} + +pub(crate) fn mix(mut x: u64) -> u64 { + x ^= x >> 33; + x = x.wrapping_mul(0xff51_afd7_ed55_8ccd); + x ^= x >> 33; + x = x.wrapping_mul(0xc4ce_b9fe_1a85_ec53); + x ^ (x >> 33) +} + +fn h(tag: u64, a: usize, b: usize) -> u64 { + mix((tag << 56) ^ ((a as u64) << 24) ^ b as u64) +} + +/// The recipe of item `m`. +pub fn recipe(m: usize) -> Recipe { + if m == 0 { + return Recipe { + demand: vec![(FLOUR, 2), (CHEESE, 1), (SAUCE, 1)], + equipment: 0, + skill: 0, + per_staff: 10, + prep_minutes: 15, + }; + } + let mut demand: Vec<(usize, i32)> = (0..3) + .map(|t| { + ( + (m * 7 + t * 11 + 3) % CATEGORIES, + 1 + (t as i32 + m as i32) % 3, + ) + }) + .collect(); + demand.sort_unstable(); + demand.dedup_by_key(|(k, _)| *k); + Recipe { + demand, + equipment: m % EQUIPMENT, + skill: m % SKILLS, + per_staff: 4 + (m as i32 % 5), + prep_minutes: 10 + 5 * (m as i32 % 4), + } +} + +/// Whether row `s` is one of the hand-set cases. +fn named(s: usize) -> bool { + s <= case::UNKNOWN_CHEESE +} + +/// Stock of category `k` at row `s`, at source version `restocked`. +pub fn stock(s: usize, k: usize, restocked: bool) -> i32 { + if named(s) && k <= SAUCE { + return match (s, k) { + (case::NO_CHEESE, CHEESE) => 0, + (case::EXACTLY_ONE, FLOUR) => 2, + (case::EXACTLY_ONE, _) => 1, + (case::RESTOCKED, CHEESE) => { + if restocked { + 50 + } else { + 0 + } + } + (case::UNKNOWN_CHEESE, CHEESE) => STALE, + _ => 100, + }; + } + 20 + (h(1, s, k) % 60) as i32 +} + +/// Whether the stock of `k` at `s` is known. +pub fn known(s: usize, k: usize) -> bool { + if named(s) { + return !(s == case::UNKNOWN_CHEESE && k == CHEESE); + } + !h(2, s, k).is_multiple_of(23) +} + +/// Unit cost of `k` at `s`, in cents. +pub fn unit_cost(s: usize, k: usize) -> i32 { + 50 + (h(3, s, k) % 200) as i32 +} + +/// Whether equipment `e` is available at `s`. +pub fn equipment(s: usize, e: usize) -> bool { + if named(s) { + return !(s == case::NO_OVEN && e == 0); + } + !h(4, s, e).is_multiple_of(9) +} + +/// Qualified staff with skill `j` at `s`. +pub fn staff(s: usize, j: usize) -> i32 { + if named(s) && j == 0 { + return if s == case::NO_COOK { 0 } else { 3 }; + } + (h(5, s, j) % 4) as i32 +} + +/// Whether item `m` is offered at `s`. +pub fn offered(s: usize, m: usize) -> bool { + if named(s) { + return true; + } + !h(6, s, m).is_multiple_of(7) +} + +/// Free delivery slots at `s` in the window. +pub fn delivery_slots(s: usize) -> i32 { + if named(s) { + return 2; + } + (h(7, s, 0) % 4) as i32 +} + +/// Delivery minutes from `s` to the order's destination. +pub fn delivery_minutes(s: usize) -> i32 { + if named(s) { + return 20; + } + 10 + (h(8, s, 0) % 40) as i32 +} + +/// Global identity of row `s`. +pub fn store_id(s: usize) -> u32 { + STORE_ID_BASE + s as u32 +} + +/// The IAM stand-in's single denial: the reader may not see this store. +pub const DENIED_STORE: u32 = STORE_ID_BASE + case::HIDDEN as u32; diff --git a/crates/foundry-parity-lab/src/lib.rs b/crates/foundry-parity-lab/src/lib.rs new file mode 100644 index 000000000..ccd8ee005 --- /dev/null +++ b/crates/foundry-parity-lab/src/lib.rs @@ -0,0 +1,26 @@ +//! D-FOUNDRY-PARITY-1 lab: a 64-store operating network. +//! +//! The question: can source-owned planes, one DT64 lane binding and +//! mask-RISC programs answer operational questions — `CanCook`, +//! `CanFulfill`, `NetworkCapacity`, network ingredient cost — over 64 stores +//! × 64 inventory categories with exact parity against an independent +//! oracle, and refuse correctly when a frame is stale, a row order differs, +//! a lane serves another scope, a store is not visible, or stock is unknown? +//! +//! * [`fixture`] is the only definition of values. +//! * [`source`] owns the planes and implements DT64's `LaneSource`. +//! * [`bind`] is the class: its 211-field basis, route and demanded binding. +//! * [`arm_b`] answers through `borrow_all` + mask-RISC programs. +//! * [`arm_a`] is the materialized-object null model. +//! * [`oracle`] answers from the fixture alone, and also evaluates the one +//! scenario (Level S) the DR path cannot: no overlay contract exists. +//! +//! Read-only (Level R). No writeback, no actions, no Foundry comparator: +//! nothing here measures or claims anything about Foundry itself. + +pub mod arm_a; +pub mod arm_b; +pub mod bind; +pub mod fixture; +pub mod oracle; +pub mod source; diff --git a/crates/foundry-parity-lab/src/oracle.rs b/crates/foundry-parity-lab/src/oracle.rs new file mode 100644 index 000000000..9935502ea --- /dev/null +++ b/crates/foundry-parity-lab/src/oracle.rs @@ -0,0 +1,115 @@ +//! The independent scalar oracle. Reads [`crate::fixture`] directly; it +//! knows nothing of DT64, DR, lanes, masks or mask-RISC. + +use crate::fixture::{self as fx, DENIED_STORE, STORES}; + +/// Why a store cannot make more of an item. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Limit { + NotOffered, + Equipment(usize), + Staff(usize), + Ingredient(usize), +} + +/// One store's answer to `CanCook(store, item, quantity)`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Cook { + /// The reader may not see this store. Never reported as "cannot cook". + NotAuthorized, + /// A demanded category's stock is unknown. Never read as zero. + Unknown { category: usize }, + /// `max` units can be made; `feasible` is `max >= quantity`. + Known { + feasible: bool, + max: i32, + limit: Limit, + }, +} + +/// The oracle's view of the network at one source version. +#[derive(Debug, Clone, Copy)] +pub struct Oracle { + pub restocked: bool, + /// Store rows the scenario closes (Level S); empty for the baseline. + pub closed: &'static [usize], +} + +impl Oracle { + pub const BASELINE: Oracle = Oracle { + restocked: false, + closed: &[], + }; + + /// `CanCook(row s, item m, quantity q)`. + pub fn can_cook(&self, s: usize, m: usize, q: i32) -> Cook { + if fx::store_id(s) == DENIED_STORE { + return Cook::NotAuthorized; + } + let r = fx::recipe(m); + if let Some(&(k, _)) = r.demand.iter().find(|(k, _)| !fx::known(s, *k)) { + return Cook::Unknown { category: k }; + } + let (max, limit) = if !fx::offered(s, m) || self.closed.contains(&s) { + (0, Limit::NotOffered) + } else if !fx::equipment(s, r.equipment) { + (0, Limit::Equipment(r.equipment)) + } else { + // Smallest cap wins; ties go to the earlier constraint + // (ingredients in category order, then staff). + let mut best = (i32::MAX, Limit::NotOffered); + for &(k, d) in &r.demand { + let cap = fx::stock(s, k, self.restocked) / d; + if cap < best.0 { + best = (cap, Limit::Ingredient(k)); + } + } + let staff_cap = fx::staff(s, r.skill) * r.per_staff; + if staff_cap < best.0 { + best = (staff_cap, Limit::Staff(r.skill)); + } + best + }; + Cook::Known { + feasible: max >= q, + max, + limit, + } + } + + /// `CanFulfill(row s, item m, quantity q, deadline minutes)`. + pub fn can_fulfill(&self, s: usize, m: usize, q: i32, deadline: i32) -> Option { + match self.can_cook(s, m, q) { + Cook::Known { feasible, .. } => Some( + feasible + && fx::delivery_slots(s) >= 1 + && fx::recipe(m).prep_minutes + fx::delivery_minutes(s) <= deadline, + ), + _ => None, + } + } + + /// `NetworkCapacity(item m)`: Σ max over visible stores with known stock. + pub fn network_capacity(&self, m: usize) -> i64 { + (0..STORES) + .map(|s| match self.can_cook(s, m, 1) { + Cook::Known { max, .. } => i64::from(max), + _ => 0, + }) + .sum() + } + + /// Ingredient cost of making `q` of `m` at every store that can. + pub fn network_cost(&self, m: usize, q: i32) -> i64 { + let r = fx::recipe(m); + (0..STORES) + .filter(|&s| matches!(self.can_cook(s, m, q), Cook::Known { feasible: true, .. })) + .map(|s| { + r.demand + .iter() + .map(|&(k, d)| i64::from(d * q) * i64::from(fx::unit_cost(s, k))) + .sum::() + }) + .sum() + } +} diff --git a/crates/foundry-parity-lab/src/source.rs b/crates/foundry-parity-lab/src/source.rs new file mode 100644 index 000000000..83106431b --- /dev/null +++ b/crates/foundry-parity-lab/src/source.rs @@ -0,0 +1,201 @@ +//! The source owner: one typed plane per field, in the source's own row +//! order, with the frame store that lives with them. It implements DT64's +//! [`LaneSource`]; it never hands out a copy. + +use std::cell::Cell; + +use lance_graph_contract::ontology::FieldRef; +use lance_graph_dt64::dr::{Frame, FrameRevisions, FrameStore, Scope}; +use lance_graph_dt64::{CoordSpace, LaneAddr, LaneSource, SchemaRev}; + +use crate::bind::{field, Kind, CLASS, FIELD_COUNT, READER, SPACE_A, SPACE_REVERSED}; +use crate::fixture::{self as fx, STORES}; + +/// One borrowed plane: a value lane or a row mask (bit `r` = row `r`). +#[derive(Debug, Clone, Copy)] +pub enum Plane<'a> { + I32(&'a [i32]), + U32(&'a [u32]), + Mask(&'a [u64]), +} + +#[derive(Debug)] +enum Owned { + I32(Box<[i32]>), + U32(Box<[u32]>), + Mask(Box<[u64]>), +} + +impl Owned { + fn bytes(&self) -> usize { + match self { + Owned::I32(v) => v.len() * 4, + Owned::U32(v) => v.len() * 4, + Owned::Mask(v) => v.len() * 8, + } + } +} + +/// Which fixture row sits at each physical row. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Order { + /// Physical row `r` is store row `r`. + Identity, + /// Physical row `r` is store row `63 - r`: same stores, another order. + Reversed, +} + +impl Order { + fn row(self, r: usize) -> usize { + match self { + Order::Identity => r, + Order::Reversed => STORES - 1 - r, + } + } + + /// The coordinate space this order is published under. + pub fn space(self) -> CoordSpace { + match self { + Order::Identity => SPACE_A, + Order::Reversed => SPACE_REVERSED, + } + } +} + +/// The owner of every plane of the network. +#[derive(Debug)] +pub struct Network { + planes: Vec, + order: Order, + frames: FrameStore, + restocked: bool, + /// The scope every lane serves. + pub scope: Scope, + /// A field the source declines to map (models a missing source lane). + pub unbound: Option, + /// Lane borrows handed out. + pub borrows: Cell, +} + +fn mask_from(pred: impl Fn(usize) -> bool) -> Box<[u64]> { + let mut w = 0u64; + for r in 0..STORES { + if pred(r) { + w |= 1 << r; + } + } + vec![w].into_boxed_slice() +} + +fn lane_i32(f: impl Fn(usize) -> i32) -> Box<[i32]> { + (0..STORES).map(f).collect() +} + +impl Network { + /// Ingest the fixture into planes in `order`. + pub fn ingest(order: Order) -> Network { + let mut n = Network { + planes: Vec::with_capacity(FIELD_COUNT), + order, + frames: FrameStore::new(FrameRevisions { + source: 1, + relation_map: 1, + coordinates: 1, + }), + restocked: false, + scope: READER, + unbound: None, + borrows: Cell::new(0), + }; + n.rebuild(); + n + } + + fn rebuild(&mut self) { + let o = self.order; + let rs = self.restocked; + self.planes = (0..FIELD_COUNT) + .map(|f| match field(f as u8) { + Kind::Stock(k) => Owned::I32(lane_i32(|r| fx::stock(o.row(r), k, rs))), + Kind::Known(k) => Owned::Mask(mask_from(|r| fx::known(o.row(r), k))), + Kind::Cost(k) => Owned::I32(lane_i32(|r| fx::unit_cost(o.row(r), k))), + Kind::Staff(j) => Owned::I32(lane_i32(|r| fx::staff(o.row(r), j))), + Kind::Equipment(e) => Owned::Mask(mask_from(|r| fx::equipment(o.row(r), e))), + Kind::Offered(m) => Owned::Mask(mask_from(|r| fx::offered(o.row(r), m))), + Kind::DeliverySlots => Owned::I32(lane_i32(|r| fx::delivery_slots(o.row(r)))), + Kind::DeliveryMinutes => Owned::I32(lane_i32(|r| fx::delivery_minutes(o.row(r)))), + Kind::StoreId => Owned::U32((0..STORES).map(|r| fx::store_id(o.row(r))).collect()), + }) + .collect(); + } + + /// The source-side change: store 06 receives cheese. The planes change in + /// place and the frame advances, so every binding sealed before is stale. + pub fn restock(&mut self) { + let r = (0..STORES) + .find(|&r| self.order.row(r) == fx::case::RESTOCKED) + .expect("the restocked store is in the population"); + let f = crate::bind::STOCK0 + fx::CHEESE; + if let Owned::I32(lane) = &mut self.planes[f] { + lane[r] = fx::stock(fx::case::RESTOCKED, fx::CHEESE, true); + } + self.restocked = true; + let next = self.frames.revisions().source + 1; + self.frames.advance(FrameRevisions { + source: next, + relation_map: 1, + coordinates: 1, + }); + } + + pub fn order(&self) -> Order { + self.order + } + + /// Bytes the source holds (the only canonical copy). + pub fn resident_bytes(&self) -> usize { + self.planes.iter().map(Owned::bytes).sum() + } +} + +impl LaneSource for Network { + type Lane<'a> + = Plane<'a> + where + Self: 'a; + + fn lane_of( + &self, + class: u32, + rev: SchemaRev, + field: u8, + _basis: &FieldRef, + _frame: &Frame, + ) -> Option { + if class != CLASS + || rev != SchemaRev(1) + || usize::from(field) >= self.planes.len() + || self.unbound == Some(field) + { + return None; + } + Some(LaneAddr { + space: self.order.space(), + lane: u16::from(field), + scope: self.scope, + }) + } + + fn borrow(&self, addr: &LaneAddr) -> Option> { + self.borrows.set(self.borrows.get() + 1); + Some(match self.planes.get(usize::from(addr.lane))? { + Owned::I32(v) => Plane::I32(v), + Owned::U32(v) => Plane::U32(v), + Owned::Mask(v) => Plane::Mask(v), + }) + } + + fn frames(&self) -> &FrameStore { + &self.frames + } +} diff --git a/crates/foundry-parity-lab/tests/integration/common.rs b/crates/foundry-parity-lab/tests/integration/common.rs new file mode 100644 index 000000000..9c2e8be3b --- /dev/null +++ b/crates/foundry-parity-lab/tests/integration/common.rs @@ -0,0 +1,32 @@ +use foundry_parity_lab::arm_b::{demanded_fields, Answer, ArmRefusal, ItemQuery}; +use foundry_parity_lab::bind::{World, READER}; +use foundry_parity_lab::oracle::Cook; +use foundry_parity_lab::source::Network; +use lance_graph_dt64::LaneSource; + +/// Bind the fields item `m` demands under the source's own row order, then +/// open the query (one `borrow_all`). +pub fn open<'s>( + world: &World, + net: &'s Network, + m: usize, + cost: bool, + delivery: bool, +) -> Result, ArmRefusal> { + let binding = world.bind( + net, + &demanded_fields(m, cost, delivery), + net.order().space(), + READER, + )?; + ItemQuery::open(m, &binding, &world.store, &world.view, net.frames(), net) +} + +/// The oracle's answer without the parts arm B cannot produce. +pub fn coarse(c: Cook) -> Answer { + match c { + Cook::NotAuthorized => Answer::NotAuthorized, + Cook::Unknown { .. } => Answer::Unknown, + Cook::Known { feasible, max, .. } => Answer::Known { feasible, max }, + } +} diff --git a/crates/foundry-parity-lab/tests/integration/controls.rs b/crates/foundry-parity-lab/tests/integration/controls.rs new file mode 100644 index 000000000..7b5957ea0 --- /dev/null +++ b/crates/foundry-parity-lab/tests/integration/controls.rs @@ -0,0 +1,109 @@ +//! Negative controls: bypass one load-bearing guard and the answer is wrong. + +use foundry_parity_lab::arm_b::{Answer, Guards}; +use foundry_parity_lab::bind::World; +use foundry_parity_lab::fixture::case; +use foundry_parity_lab::oracle::{Cook, Oracle}; +use foundry_parity_lab::source::{Network, Order}; + +use crate::common::{coarse, open}; + +#[test] +fn bypassing_the_known_guard_turns_unknown_stock_into_a_false_yes() { + let world = World::new(); + let net = Network::ingest(Order::Identity); + let query = open(&world, &net, 0, false, false).unwrap(); + let guarded = query.answers(1, Guards::ALL).unwrap(); + let bypass = query + .answers( + 1, + Guards { + known: false, + ..Guards::ALL + }, + ) + .unwrap(); + let truth = coarse(Oracle::BASELINE.can_cook(case::UNKNOWN_CHEESE, 0, 1)); + assert_eq!(guarded[case::UNKNOWN_CHEESE], truth); + assert!(matches!( + bypass[case::UNKNOWN_CHEESE], + Answer::Known { feasible: true, .. } + )); + assert_ne!( + bypass[case::UNKNOWN_CHEESE], + truth, + "the stale 1000 is read as stock" + ); + // Silent half: the guard changes nothing for stores whose stock is known. + for s in 0..64 { + if !matches!(Oracle::BASELINE.can_cook(s, 0, 1), Cook::Unknown { .. }) { + assert_eq!(bypass[s], guarded[s], "store {s}"); + } + } +} + +#[test] +fn bypassing_authorization_reveals_the_denied_store() { + let world = World::new(); + let net = Network::ingest(Order::Identity); + let query = open(&world, &net, 0, false, false).unwrap(); + let bypass = query + .answers( + 1, + Guards { + authorization: false, + ..Guards::ALL + }, + ) + .unwrap(); + assert_eq!( + Oracle::BASELINE.can_cook(case::HIDDEN, 0, 1), + Cook::NotAuthorized + ); + assert!( + matches!(bypass[case::HIDDEN], Answer::Known { .. }), + "{:?}", + bypass[case::HIDDEN] + ); + let guarded = query.answers(1, Guards::ALL).unwrap(); + let differing = (0..64).filter(|&s| bypass[s] != guarded[s]).count(); + assert_eq!(differing, 1, "authorization hides exactly the denied store"); +} + +#[test] +fn unknown_is_not_zero() { + // The unknown store's answer is neither "infeasible" nor "max 0". + let o = Oracle::BASELINE.can_cook(case::UNKNOWN_CHEESE, 0, 1); + assert!(matches!(o, Cook::Unknown { .. })); + let world = World::new(); + let net = Network::ingest(Order::Identity); + let b = open(&world, &net, 0, false, false) + .unwrap() + .answers(1, Guards::ALL) + .unwrap(); + assert_eq!(b[case::UNKNOWN_CHEESE], Answer::Unknown); +} + +#[test] +fn the_scenario_is_oracle_only_and_lowers_capacity() { + // Level S: closing store 12 for the window. The DR path has no overlay + // contract, so only the scalar reference answers; canonical data is not + // touched (the baseline is recomputed after and is unchanged). + let base = Oracle::BASELINE; + let closed = Oracle { + restocked: false, + closed: &[11], + }; + let before = base.network_capacity(0); + let after = closed.network_capacity(0); + let lost = match base.can_cook(11, 0, 1) { + Cook::Known { max, .. } => i64::from(max), + _ => 0, + }; + assert!( + lost > 0, + "store 12 must contribute for the scenario to be informative" + ); + assert_eq!(before - after, lost); + assert_eq!(base.network_capacity(0), before); +} diff --git a/crates/foundry-parity-lab/tests/integration/main.rs b/crates/foundry-parity-lab/tests/integration/main.rs new file mode 100644 index 000000000..6b09db93f --- /dev/null +++ b/crates/foundry-parity-lab/tests/integration/main.rs @@ -0,0 +1,7 @@ +//! D-FOUNDRY-PARITY-1 integration tests. +// Store rows index the oracle, arm A and arm B at once; a range loop is clearer. +#![allow(clippy::needless_range_loop)] +pub mod common; +pub mod controls; +pub mod parity; +pub mod refusals; diff --git a/crates/foundry-parity-lab/tests/integration/parity.rs b/crates/foundry-parity-lab/tests/integration/parity.rs new file mode 100644 index 000000000..2fa30a86d --- /dev/null +++ b/crates/foundry-parity-lab/tests/integration/parity.rs @@ -0,0 +1,165 @@ +//! Arm B (DT64 + mask-RISC) == oracle == arm A (materialized objects). + +use foundry_parity_lab::arm_a::Objects; +use foundry_parity_lab::arm_b::{demanded_fields, Answer, Guards}; +use foundry_parity_lab::bind::World; +use foundry_parity_lab::fixture::{case, CHEESE, FLOUR, ITEMS, STORES}; +use foundry_parity_lab::oracle::{Cook, Limit, Oracle}; +use foundry_parity_lab::source::{Network, Order}; + +use crate::common::{coarse, open}; + +#[test] +fn every_item_quantity_and_store_agrees_across_the_three_arms() { + let world = World::new(); + let net = Network::ingest(Order::Identity); + let objects = Objects::build(false); + let oracle = Oracle::BASELINE; + let (mut feasible, mut infeasible) = (0, 0); + for m in 0..ITEMS { + let query = open(&world, &net, m, false, false).expect("binds"); + for q in [1, 2, 5] { + let b = query.answers(q, Guards::ALL).expect("answers"); + for s in 0..STORES { + let o = oracle.can_cook(s, m, q); + assert_eq!( + objects.can_cook(s, m, q), + o, + "arm A, store {s} item {m} q {q}" + ); + assert_eq!(b[s], coarse(o), "arm B, store {s} item {m} q {q}"); + match b[s] { + Answer::Known { feasible: true, .. } => feasible += 1, + Answer::Known { + feasible: false, .. + } => infeasible += 1, + _ => {} + } + } + } + } + // Anti-vacuity: both outcomes are common, so agreement is informative. + let total = ITEMS * 3 * STORES; + assert!( + feasible * 5 > total && infeasible * 5 > total, + "{feasible}/{infeasible} of {total}" + ); +} + +#[test] +fn the_named_cases_answer_as_designed() { + let world = World::new(); + let net = Network::ingest(Order::Identity); + let o = Oracle::BASELINE; + let b = open(&world, &net, 0, false, false) + .expect("binds") + .answers(1, Guards::ALL) + .expect("answers"); + let known = |s, feasible, max, limit| { + assert_eq!( + o.can_cook(s, 0, 1), + Cook::Known { + feasible, + max, + limit + }, + "store row {s}" + ); + assert_eq!(b[s], Answer::Known { feasible, max }, "arm B store row {s}"); + }; + known(case::ALL_GOOD, true, 30, Limit::Staff(0)); + known(case::NO_CHEESE, false, 0, Limit::Ingredient(CHEESE)); + known(case::NO_COOK, false, 0, Limit::Staff(0)); + known(case::NO_OVEN, false, 0, Limit::Equipment(0)); + known(case::EXACTLY_ONE, true, 1, Limit::Ingredient(FLOUR)); + known(case::RESTOCKED, false, 0, Limit::Ingredient(CHEESE)); + assert_eq!( + o.can_cook(case::EXACTLY_ONE, 0, 2), + Cook::Known { + feasible: false, + max: 1, + limit: Limit::Ingredient(FLOUR) + } + ); + assert_eq!(o.can_cook(case::HIDDEN, 0, 1), Cook::NotAuthorized); + assert_eq!(b[case::HIDDEN], Answer::NotAuthorized); + assert_eq!( + o.can_cook(case::UNKNOWN_CHEESE, 0, 1), + Cook::Unknown { category: CHEESE } + ); + assert_eq!(b[case::UNKNOWN_CHEESE], Answer::Unknown); +} + +#[test] +fn network_capacity_and_cost_agree() { + let world = World::new(); + let net = Network::ingest(Order::Identity); + let objects = Objects::build(false); + let o = Oracle::BASELINE; + for m in 0..ITEMS { + let query = open(&world, &net, m, true, false).expect("binds"); + let cap = query.network_capacity().expect("capacity"); + assert_eq!(cap, o.network_capacity(m), "item {m}"); + assert_eq!(cap, objects.network_capacity(m), "item {m}"); + assert!(cap > 0, "item {m}: capacity must be informative"); + for q in [1, 3] { + let cost = query.network_cost(q).expect("cost"); + assert_eq!(cost, o.network_cost(m, q), "item {m} q {q}"); + } + } +} + +#[test] +fn can_fulfill_agrees_and_discriminates() { + let world = World::new(); + let net = Network::ingest(Order::Identity); + let o = Oracle::BASELINE; + let (mut yes, mut no) = (0, 0); + for m in 0..ITEMS { + let query = open(&world, &net, m, false, true).expect("binds"); + for deadline in [30, 45, 60] { + let mask = query.fulfill_mask(2, deadline).expect("fulfill"); + for r in 0..STORES { + let s = (query.store_at(r) - foundry_parity_lab::fixture::STORE_ID_BASE) as usize; + let b = mask >> r & 1 == 1; + // A store the oracle cannot judge (denied / unknown) is never + // reported as able to fulfil. + assert_eq!( + b, + o.can_fulfill(s, m, 2, deadline) == Some(true), + "{s} {m} {deadline}" + ); + if b { + yes += 1 + } else { + no += 1 + } + } + } + } + assert!(yes > 20 && no > 20, "{yes} yes / {no} no"); +} + +#[test] +fn only_demanded_fields_are_bound_and_borrowed_once() { + let world = World::new(); + let net = Network::ingest(Order::Identity); + let fields = demanded_fields(0, false, false); + let query = open(&world, &net, 0, false, false).expect("binds"); + // 3 ingredients x (stock, known) + staff + equipment + offered + storeId. + assert_eq!(fields.len(), 10); + assert_eq!( + net.borrows.get(), + 10, + "borrow_all: one borrow per bound field" + ); + for q in 1..=5 { + query.answers(q, Guards::ALL).expect("answers"); + } + query.network_capacity().expect("capacity"); + assert_eq!( + net.borrows.get(), + 10, + "questions reuse the binding; no further lookups" + ); +} diff --git a/crates/foundry-parity-lab/tests/integration/refusals.rs b/crates/foundry-parity-lab/tests/integration/refusals.rs new file mode 100644 index 000000000..fe1273c03 --- /dev/null +++ b/crates/foundry-parity-lab/tests/integration/refusals.rs @@ -0,0 +1,149 @@ +//! Refusals: what the binding must not answer. + +use foundry_parity_lab::arm_b::{demanded_fields, Answer, ArmRefusal, Guards, ItemQuery, Q_MAX}; +use foundry_parity_lab::bind::{World, COST0, READER, SPACE_A, STOCK0}; +use foundry_parity_lab::fixture::{case, store_id, CHEESE, ITEMS, STORES}; +use foundry_parity_lab::oracle::Oracle; +use foundry_parity_lab::source::{Network, Order}; +use lance_graph_dt64::dr::{FrameRefusal, Scope}; +use lance_graph_dt64::{LaneRefusal, LaneSource}; + +use crate::common::{coarse, open}; + +#[test] +fn a_stale_frame_is_refused_and_a_rebind_sees_the_restock() { + let world = World::new(); + let mut net = Network::ingest(Order::Identity); + let fields = demanded_fields(0, false, false); + let before = world.bind(&net, &fields, SPACE_A, READER).expect("binds"); + net.restock(); + let stale = ItemQuery::open(0, &before, &world.store, &world.view, net.frames(), &net); + assert!( + matches!( + stale, + Err(ArmRefusal::Lane(LaneRefusal::Frame( + FrameRefusal::SourceChanged + ))) + ), + "{:?}", + stale.err() + ); + let after = open(&world, &net, 0, false, false).expect("rebinds"); + let b = after.answers(1, Guards::ALL).expect("answers"); + let o = Oracle { + restocked: true, + closed: &[], + }; + assert_eq!( + b[case::RESTOCKED], + coarse(o.can_cook(case::RESTOCKED, 0, 1)) + ); + assert_eq!( + b[case::RESTOCKED], + Answer::Known { + feasible: true, + max: 30 + } + ); + assert_eq!(after.network_capacity().unwrap(), o.network_capacity(0)); + assert!(o.network_capacity(0) > Oracle::BASELINE.network_capacity(0)); +} + +#[test] +fn a_lane_serving_another_scope_is_refused() { + let world = World::new(); + let net = Network::ingest(Order::Identity); + let r = world.bind( + &net, + &demanded_fields(0, false, false), + SPACE_A, + Scope { id: 99 }, + ); + assert!(matches!(r, Err(LaneRefusal::Scope { .. })), "{r:?}"); +} + +#[test] +fn another_row_order_is_refused_under_the_canonical_selection() { + let world = World::new(); + let net = Network::ingest(Order::Reversed); + let r = world.bind(&net, &demanded_fields(0, false, false), SPACE_A, READER); + assert!( + matches!(r, Err(LaneRefusal::CoordinateSpace { .. })), + "{r:?}" + ); +} + +#[test] +fn another_row_order_answers_identically_by_global_identity() { + let world = World::new(); + let net = Network::ingest(Order::Reversed); + let o = Oracle::BASELINE; + for m in 0..ITEMS { + let query = open(&world, &net, m, false, false).expect("binds in its own space"); + // The ordinal moved; the identity did not. + assert_eq!( + query.store_at(STORES - 1 - case::REORDERED), + store_id(case::REORDERED) + ); + let b = query.answers(2, Guards::ALL).expect("answers"); + for s in 0..STORES { + assert_eq!(b[s], coarse(o.can_cook(s, m, 2)), "store {s} item {m}"); + } + assert_eq!(query.network_capacity().unwrap(), o.network_capacity(m)); + } +} + +#[test] +fn a_field_the_source_does_not_map_is_refused_not_read_as_empty() { + let world = World::new(); + let mut net = Network::ingest(Order::Identity); + net.unbound = Some((STOCK0 + CHEESE) as u8); + let r = world.bind(&net, &demanded_fields(0, false, false), SPACE_A, READER); + assert!(matches!(r, Err(LaneRefusal::Unbound { .. })), "{r:?}"); +} + +#[test] +fn a_question_needing_an_unbound_field_is_refused() { + let world = World::new(); + let net = Network::ingest(Order::Identity); + let query = open(&world, &net, 0, false, false).expect("binds without cost lanes"); + let r = query.network_cost(1); + assert!( + matches!(r, Err(ArmRefusal::NotBound { field }) if usize::from(field) >= COST0), + "{r:?}" + ); + assert!(matches!( + query.cook_mask(0, Guards::ALL), + Err(ArmRefusal::Quantity { q: 0 }) + )); +} + +#[test] +fn every_quantity_entry_point_refuses_out_of_range() { + // `answers` sweeps its own valid quantities, so it must check the + // caller's `q` itself; before this check `answers(0)` reported a store + // with no cheese as feasible (max 0 >= 0). + let world = World::new(); + let net = Network::ingest(Order::Identity); + let query = open(&world, &net, 0, true, true).expect("binds"); + for q in [0, -1, Q_MAX + 1] { + let r = query.answers(q, Guards::ALL); + assert!( + matches!(r, Err(ArmRefusal::Quantity { q: got }) if got == q), + "answers({q}): {r:?}" + ); + let r = query.fulfill_mask(q, 45); + assert!( + matches!(r, Err(ArmRefusal::Quantity { .. })), + "fulfill({q}): {r:?}" + ); + let r = query.network_cost(q); + assert!( + matches!(r, Err(ArmRefusal::Quantity { .. })), + "cost({q}): {r:?}" + ); + } + // Silent half: the bounds themselves are admitted. + assert!(query.answers(1, Guards::ALL).is_ok()); + assert!(query.answers(Q_MAX, Guards::ALL).is_ok()); +}