test(51-03): declarative cross-surface normalizer — scenario #10 GREEN, 3/3

The equals mode compared raw surface shapes through an identity placeholder, so
it could never hold: ps answers with rig records, queue with qitems whose
destinationSession is <pod>-<member>@<rig>. Comparing them requires DECLARING
which field of each carries the shared truth — that declaration is the
normalizer, and per A-N1 the declarative mapping is the only scenario-facing
form and LOWERS to the runner-internal seam. The seam signature is untouched;
this is additive.

  equals:
    ps:    { pluck: name }
    queue: { pluck: destinationSession, rig: true }

Projections are small and total: pluck a field per element, optionally reduce a
session name to its rig, then dedupe+sort so agreement on a SET is not defeated
by ordering or by N qitems naming one rig. Undeclared surfaces pass through
rather than silently emptying. Schema validates the mapping at load, so an
authoring error teaches instead of comparing nothing.

WHAT #10 NOW PROVES: ps and queue name the same rig set — a qitem addressed to a
rig ps does not know, or a rig whose queue work is invisible, fails the run.
FALSIFIED, not assumed: plucking rigId instead of name makes the sets disagree
(ULID vs scn-baton) and the scenario FAILS, so the comparison is value-sensitive
and both operands are non-empty. The scenario also seeds a real qitem — an
all-empty comparison would pass while proving nothing.

WHAT IT DOES NOT PROVE, measured at source and stated in the fixture: tui_socket
is asserted separately on its own coherence and deliberately NOT in the equality.
Both shipped OBSERVE verbs return UI/registry state (state -> {screen, drill,
viewTab}; commands -> the command registry), so the socket carries no daemon
truth; folding it in could only pass by comparing something trivially equal. The
gap is a TUI finding — the state query would need to expose the rig/queue view it
renders.

3/3 scenarios PASS against the real runtime. Matrix 170/170 across 21 files,
daemon tsc clean.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
v-openrig-buildandClaude Fable 5 committed 2026-08-11 21:10:46 +00:00
1 parent cd14bf98dc
commit fc3f766e44
6 files changed
+250 -22

No files matched your search

@@ -1,27 +1,52 @@
# Scenario #10 — one-view-state consistency (51-02 proof item 7, parse + interface).
# Scenario #10 — one-view-state consistency (the 5.1 crowning assertion).
#
# INTENT: a single point in time must read consistently across surfaces — the TUI
# control socket, `rig ps`, and `rig queue` should agree on the one view state
# (the cross-surface `equals` assertion). This scenario exercises the equals PARSE
# + the declarative-normalizer INTERFACE; the normalizer's final shape (which fields
# of each surface map onto the shared comparison form) rides 51-03 shaping, so the
# runner takes an injected normalizer here. The locked schema requires a nominal
# `surface` on every expect, so `surface: ps` names the anchor; `equals` lists the
# surfaces compared.
# INTENT: at a single point in time the shipped surfaces must AGREE about one
# world. This is the automated form of the failure class that costs real time —
# one surface confidently reporting a world another surface cannot see.
#
# WHAT IS COMPARED, and why these two: `rig ps` and `rig queue` are both
# daemon-truth surfaces, so the rigs ps reports and the rigs the queue's items are
# destined for must be the SAME SET. A qitem addressed to a rig ps does not know
# about — or a rig running with its queue work invisible — breaks this and fails
# the scenario. The declarative mapping below is what makes the comparison
# possible: the surfaces answer in different shapes (rig records vs qitems whose
# destinationSession is <pod>-<member>@<rig>), so each DECLARES the field carrying
# the shared truth. Per A-N1 the mapping is the only scenario-facing form; it
# lowers to the runner-internal seam.
#
# WHAT tui_socket CONTRIBUTES, stated honestly: its own coherence, asserted
# separately below — NOT membership in the equality. MEASURED at source: both of
# the control socket's OBSERVE verbs return UI/registry state
# (`state` -> {screen, drill, viewTab}; `commands` -> the command registry), so
# the shipped socket carries NO daemon truth to compare. Folding it into the
# equality could only pass by comparing something trivially equal, which would be
# a false green. The gap is a TUI finding (the state query would need to expose
# the rig/queue view it renders), not something this scenario can paper over.
scenario: one-view-state-consistency
topology: ./topo-stub-baton.yaml
env:
# D7 (51-02 delta): tui_socket expects require the TUI opt-in — the pipeline
# provisions the shipped TUI (isolated tmux server + scaffold socket) only when
# declared, so the surface read never races a socket that never listens.
tui: true
# give the queue a real item so both surfaces have a rig to agree about; an
# all-empty comparison would pass without proving anything
queue:
- id: view-1
source: harness@scn-baton
destination: dev-worker@scn-baton
summary: "#10 one-view-state — the queue's rig must be a rig ps knows"
claim: true
steps:
- up: {}
# the TUI is live and coherent at this instant (its own state, its own claim)
- expect:
surface: tui_socket
within: 20s
match:
ok: true
# THE INVARIANT: ps and queue name the same rig set
- expect:
surface: ps
within: 20s
equals:
- tui_socket
- ps
- queue
ps: { pluck: name }
queue: { pluck: destinationSession, rig: true }
- down: {}
@@ -0,0 +1,85 @@
/**
* Slice 51-03 — the DECLARATIVE cross-surface normalizer (the `equals` mapping).
*
* A-N1 fixed the shape of this: the declarative mapping is the ONLY scenario-facing
* form, and it LOWERS to the runner-internal seam
* (`normalizer?: (surface, value) => unknown`) rather than replacing it. So this
* module is purely additive — it builds that callback from YAML. The seam
* signature is untouched and the runner's equals branch is unchanged.
*
* The problem it solves: the shipped surfaces answer in structurally different
* shapes, so raw comparison always differs. `rig ps --json` is an array of rig
* records; `rig queue list --json` is an array of qitems whose destinationSession
* is `<pod>-<member>@<rig>`. Comparing them requires DECLARING which field of each
* carries the shared truth — that declaration is what a normalizer is.
*
* equals:
* ps: { pluck: name } # -> ["scn-baton"]
* queue: { pluck: destinationSession, rig: true } # -> ["scn-baton"]
*
* Projections are deliberately tiny and total: pluck a field from each element of
* an array surface, optionally reduce a session name to its rig, then dedupe and
* sort so the comparison is order-insensitive (two surfaces agreeing on a SET
* should not fail because they enumerate it differently).
*/
import type { ExpectSurface } from "./scenario-schema.js";
/** One surface's declared projection onto the shared comparison form. */
export interface SurfaceProjection {
/** Field to take from each element of an array surface (e.g. `name`). */
pluck?: string;
/** Reduce a canonical session name (`<pod>-<member>@<rig>`) to its rig part. */
rig?: boolean;
/** Read a nested field from an OBJECT surface before projecting (dot path). */
path?: string;
}
/** The declarative `equals` mapping: surface -> projection. */
export type EqualsMapping = Partial<Record<ExpectSurface, SurfaceProjection>>;
/** True when an `equals` payload is the declarative MAPPING form (not the legacy list). */
export function isEqualsMapping(payload: unknown): payload is EqualsMapping {
return !!payload && typeof payload === "object" && !Array.isArray(payload);
}
function readPath(value: unknown, path: string): unknown {
return path.split(".").reduce<unknown>((acc, key) => {
if (acc && typeof acc === "object") return (acc as Record<string, unknown>)[key];
return undefined;
}, value);
}
/** `dev-worker@scn-baton` -> `scn-baton`. Left unchanged when there is no `@`. */
export function rigOf(session: string): string {
const at = session.lastIndexOf("@");
return at < 0 ? session : session.slice(at + 1);
}
/** Apply one surface's declared projection. Total: never throws on a shape it did not expect. */
export function projectSurface(value: unknown, spec: SurfaceProjection): unknown {
const base = spec.path ? readPath(value, spec.path) : value;
if (!spec.pluck) return base;
const items = Array.isArray(base) ? base : [base];
const plucked = items
.map((item) => (item && typeof item === "object" ? (item as Record<string, unknown>)[spec.pluck!] : undefined))
.filter((v): v is string => typeof v === "string" && v.length > 0)
.map((v) => (spec.rig ? rigOf(v) : v));
// Set semantics: dedupe + sort, so agreement on the SET is not defeated by order
// or by one surface listing the same rig twice (N qitems, one rig).
return [...new Set(plucked)].sort();
}
/**
* Lower a declarative mapping onto the runner-internal seam (A-N1). Surfaces the
* mapping does not mention pass through unchanged, so a partially-declared
* comparison still behaves predictably rather than silently emptying.
*/
export function buildDeclarativeNormalizer(
mapping: EqualsMapping,
): (surface: ExpectSurface, value: unknown) => unknown {
return (surface, value) => {
const spec = mapping[surface];
return spec ? projectSurface(value, spec) : value;
};
}
@@ -15,6 +15,7 @@
import type { ExpectSurface, ValidatedScenario } from "./scenario-schema.js";
import { structuralSubsetMatch, containsMatch, pollUntilMatch } from "./scenario-expect.js";
import type { RunRecord } from "./scenario-run-record.js";
import { buildDeclarativeNormalizer, isEqualsMapping } from "./scenario-normalizer.js";
export interface ActionResult {
code: number;
@@ -149,12 +150,16 @@ async function runEqualsExpect(
deps: ScenarioRunnerDeps,
): Promise<string | null> {
const surfaces = extractEqualsSurfaces(equalsPayload);
if (!deps.normalizer) {
// 51-03: the DECLARATIVE mapping is the scenario-facing form and LOWERS to the
// runner-internal seam (A-N1) — the seam signature is unchanged, and a scenario
// that declares its mapping no longer depends on an injected normalizer.
const declared = isEqualsMapping(equalsPayload) ? buildDeclarativeNormalizer(equalsPayload) : undefined;
const normalizer = declared ?? deps.normalizer;
if (!normalizer) {
throw new Error(
"`equals` requires a declarative cross-surface normalizer (its shape rides 51-03 shaping)",
"`equals` requires a declarative cross-surface mapping (surface -> projection) or an injected normalizer",
);
}
const normalizer = deps.normalizer;
const observeAllEqual = async (): Promise<{ equal: boolean; snapshot: unknown[] }> => {
const snapshot = await Promise.all(
surfaces.map(async (s) => normalizer(s, await deps.observe(s, {}))),
@@ -86,6 +86,8 @@ export type ValidationErrorCode =
| "EXPECT_MATCH_MODE_MISSING"
| "EXPECT_MATCH_MODE_AMBIGUOUS"
| "WITHIN_NOT_A_DURATION"
| "EQUALS_PROJECTION_INVALID"
| "EQUALS_SURFACE_UNKNOWN"
| "EMIT_NOT_OBJECT"
| "UNKNOWN_EMIT_BEHAVIOR"
| "USAGE_LIMIT_IN_STUB_TOPOLOGY"
@@ -317,6 +319,37 @@ function validateExpect(
} else if (modes.length > 1) {
push("EXPECT_MATCH_MODE_AMBIGUOUS", `${path}: only one of match | contains | equals is allowed, found [${modes.join(", ")}]`, path);
}
// 51-03: the declarative `equals` mapping (surface -> projection). Validated
// here so an authoring error is a load-time teaching failure, not a scenario
// that runs and compares nothing.
if (isPlainObject(value.equals)) {
for (const [surf, spec] of Object.entries(value.equals)) {
if (!(EXPECT_SURFACES as readonly string[]).includes(surf)) {
push(
"EQUALS_SURFACE_UNKNOWN",
`${path}.equals.${surf}: not a readable surface — the shipped-observable set is: ${EXPECT_SURFACES.join(", ")}`,
`${path}.equals.${surf}`,
);
continue;
}
if (!isPlainObject(spec)) {
push("EQUALS_PROJECTION_INVALID", `${path}.equals.${surf}: must be a projection mapping, e.g. { pluck: name }`, `${path}.equals.${surf}`);
continue;
}
for (const key of Object.keys(spec)) {
if (!["pluck", "rig", "path"].includes(key)) {
push("EQUALS_PROJECTION_INVALID", `${path}.equals.${surf}.${key}: unknown projection key — allowed: pluck, rig, path`, `${path}.equals.${surf}.${key}`);
}
}
if (spec.pluck !== undefined && typeof spec.pluck !== "string") {
push("EQUALS_PROJECTION_INVALID", `${path}.equals.${surf}.pluck: must be a field name string`, `${path}.equals.${surf}.pluck`);
}
if (spec.rig !== undefined && typeof spec.rig !== "boolean") {
push("EQUALS_PROJECTION_INVALID", `${path}.equals.${surf}.rig: must be a boolean`, `${path}.equals.${surf}.rig`);
}
}
}
if (value.within !== undefined) {
if (typeof value.within !== "string" || !DURATION_RE.test(value.within)) {
push(
@@ -0,0 +1,63 @@
import { describe, it, expect } from "vitest";
import {
projectSurface,
buildDeclarativeNormalizer,
isEqualsMapping,
rigOf,
} from "./helpers/scenario-normalizer.js";
// 51-03 — the declarative cross-surface mapping. A-N1: this is the only
// scenario-facing form and it LOWERS to the runner-internal seam.
describe("projectSurface — declares which field carries the shared truth", () => {
it("plucks a field from an array surface, deduped and sorted (set semantics)", () => {
const ps = [{ name: "b-rig" }, { name: "a-rig" }, { name: "a-rig" }];
expect(projectSurface(ps, { pluck: "name" })).toEqual(["a-rig", "b-rig"]);
});
it("reduces canonical session names to their rig — N qitems on one rig collapse to one", () => {
const queue = [
{ destinationSession: "dev-worker@scn-baton" },
{ destinationSession: "dev-qa@scn-baton" },
];
expect(projectSurface(queue, { pluck: "destinationSession", rig: true })).toEqual(["scn-baton"]);
});
it("is total on shapes it did not expect (no throw, no fabricated value)", () => {
expect(projectSurface([], { pluck: "name" })).toEqual([]);
expect(projectSurface(null, { pluck: "name" })).toEqual([]);
expect(projectSurface([{ other: 1 }], { pluck: "name" })).toEqual([]);
});
it("reads a nested path on an object surface before projecting", () => {
expect(projectSurface({ state: { screen: "topology" } }, { path: "state.screen" })).toBe("topology");
});
it("rigOf takes the part after the LAST @ and passes through a bare name", () => {
expect(rigOf("dev-worker@scn-baton")).toBe("scn-baton");
expect(rigOf("bare")).toBe("bare");
});
});
describe("buildDeclarativeNormalizer — lowering onto the seam", () => {
it("applies each surface's declared projection and passes undeclared surfaces through", () => {
const n = buildDeclarativeNormalizer({
ps: { pluck: "name" },
queue: { pluck: "destinationSession", rig: true },
});
expect(n("ps", [{ name: "scn-baton" }])).toEqual(["scn-baton"]);
expect(n("queue", [{ destinationSession: "dev-worker@scn-baton" }])).toEqual(["scn-baton"]);
// a surface with no declared projection is NOT silently emptied
expect(n("stream", [{ id: 1 }])).toEqual([{ id: 1 }]);
});
it("distinguishes the declarative mapping from the legacy surface list", () => {
expect(isEqualsMapping({ ps: { pluck: "name" } })).toBe(true);
expect(isEqualsMapping(["tui_socket", "ps", "queue"])).toBe(false);
});
it("two surfaces that genuinely disagree do NOT normalize equal (the pin has teeth)", () => {
const n = buildDeclarativeNormalizer({ ps: { pluck: "name" }, queue: { pluck: "destinationSession", rig: true } });
expect(n("ps", [{ name: "scn-baton" }])).not.toEqual(n("queue", [{ destinationSession: "w@other-rig" }]));
});
});
+21 -4
View File
@@ -91,14 +91,31 @@ describe("authored evidence scenarios (expressibility)", () => {
expect(pre[0]).toMatchObject({ id: "baton-1", destination: "dev-worker@scn-baton", claim: true });
});
it("expresses #10 one-view-state: validates the cross-surface equals over tui_socket/ps/queue", () => {
it("expresses #10 one-view-state: the DECLARATIVE equals mapping over the two daemon-truth surfaces", () => {
const res = loadScenarioFile(join(FIXTURES, "scenario-10-one-view-state.yaml"));
expect(res.ok).toBe(true);
if (!res.ok) return;
expect(res.loaded.scenario.scenario).toBe("one-view-state-consistency");
const equalsStep = res.loaded.scenario.steps.find((s) => "expect" in s)!;
const exp = (equalsStep as { expect: Record<string, unknown> }).expect;
expect(exp.equals).toEqual(["tui_socket", "ps", "queue"]);
const expects = res.loaded.scenario.steps
.filter((s) => "expect" in s)
.map((s) => (s as { expect: Record<string, unknown> }).expect);
// 51-03: equals is now the declarative MAPPING (A-N1's scenario-facing form),
// each surface declaring the field that carries the shared truth.
const equalsStep = expects.find((e) => e.equals)!;
expect(equalsStep.equals).toEqual({
ps: { pluck: "name" },
queue: { pluck: "destinationSession", rig: true },
});
// tui_socket is asserted SEPARATELY on its own coherence, deliberately not
// folded into the equality: both of the shipped control socket's OBSERVE
// verbs return UI/registry state, so it carries no daemon truth to compare
// and including it could only pass by comparing something trivially equal.
const tuiStep = expects.find((e) => e.surface === "tui_socket")!;
expect(tuiStep).toBeDefined();
expect(tuiStep.match).toEqual({ ok: true });
expect(equalsStep.equals).not.toHaveProperty("tui_socket");
});
});