docs(sdlc): convention for a slice whose intent rested on capabilities that do not exist

Discovered by 51-03. Its intent was scoped "compose-only" — authoring ten
scenarios — while silently depending on two runtime capabilities that turned
out not to exist: the `mutate` verb has no shipped runtime binding, and the TUI
control socket exposes no daemon truth to compare against ps/queue.

That is a DEPENDENCY error, not scope creep by the builder and not
over-ambition by the author: the ambition was right, the availability
assumption was wrong. Building the missing capabilities inside the slice would
have been the actual scope creep.

The convention records the honest response: revise the ambition down to what
reality supports, mark the slice PARTIAL, and write ONE document giving a
downstream reader the accurate story — locked intent, what shipped and how it
was verified by effect, what did not ship with file+line proving the capability
is absent, what the delivery does not cover, and follow-on slices scoped from
what is known at discovery time.

The last point is the load-bearing one: the agents holding the context when the
gap is found are the most knowledgeable readers that problem will ever have.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
v-openrig-buildandClaude Opus 5 committed 2026-08-11 21:55:09 +00:00
1 parent dcddb8a6ee
commit dc0fb35cd4
8 files changed
+57 -394

No files matched your search

+31 -3
View File
@@ -155,7 +155,35 @@ The **path + anchor is the AUTHORITY**; the whole-file hash **DATES** the
citation. That artifact is a living doc: the hash goes stale at the next
legitimate append, the anchor does not — the CURRENT-line convention applied to
a citation.
## A4. The elastic middle (proportionality — no minted ceremony)
## A4. When implementation finds the intent rested on something that does not exist
A slice's intent can only be honoured to the extent its assumptions hold. Sometimes implementation
discovers that a LOCKED intent depends on functionality that **is not there** — not a missing
detail, a missing capability.
**The honest response is to revise the ambition DOWN to what reality supports, and name the missing
functionality as its own work.** Mark the slice **PARTIAL**, and write one document that tells a
downstream reader the accurate story:
1. what the intent asked for (quote the locked text)
2. what actually shipped, and how it was verified BY EFFECT
3. **what did not ship and precisely why** — name each missing capability, at source, with the file
and line that proves it is absent
4. what the delivery does NOT cover, stated plainly — a scenario or feature that overstates its
coverage is worse than one that does not exist
5. **follow-on slices scoped from what you know NOW**, including which release you think they belong
in — the agents holding the context at discovery time are the most knowledgeable readers that
problem will ever have, and a downstream planner should not have to rediscover any of it
**What is NOT acceptable:** silently narrowing the claim; quietly expanding the slice to build the
missing capability; or declaring victory on a reduced surface while still using the original slice's
name. **Building the missing pieces inside the slice IS the scope creep.** Discovering them and
routing them is the slice working correctly — and a slice that reveals a real product gap has
produced something more valuable than the feature it set out to deliver.
Do not over-engineer this. The entire goal is an accurate story for whoever reads it next.
## A5. The elastic middle (proportionality — no minted ceremony)
The SDLC has exactly three fixed capture points: **intent** → a
**proportional structured requirement** → **proof**. Everything between is
@@ -164,7 +192,7 @@ mini-requirements may BE the whole PRD — the convention sections must be
present so the slice projects, but their contents scale to the work. Gates
are losslessness checks on the decompression from intent to delivery, not
paperwork. Scaffolding emits the sections; it must never mint ceremony.
## A5. The audit (advisory, fail-open — always)
## A6. The audit (advisory, fail-open — always)
`rig scope audit` (and the advisory rows in `rig workspace validate` /
`doctor`) checks these conventions: the section headings present, the proof
@@ -172,7 +200,7 @@ contract well-formed, `proof/` artifacts carrying valid C1 headers, UI slices
referencing a mockup. Every finding **records and advises — it never blocks a
write path and never changes exit semantics into a gate**. Unknown is
reported as unknown, not failure.
## A6. Where the knowledge lives (the four pointers)
## A7. Where the knowledge lives (the four pointers)
- **This document** — the SSOT.
- **Scaffold**: `rig scope slice create` emits the convention sections +
@@ -1,52 +1,27 @@
# Scenario #10 — one-view-state consistency (the 5.1 crowning assertion).
# Scenario #10 — one-view-state consistency (51-02 proof item 7, parse + interface).
#
# 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.
# 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.
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:
ps: { pluck: name }
queue: { pluck: destinationSession, rig: true }
- tui_socket
- ps
- queue
- down: {}
@@ -1,117 +0,0 @@
/**
* 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";
/**
* Thrown when a declared projection extracts NOTHING from a NON-EMPTY surface.
*
* Guard finding on a7b6b7c85: two surfaces both plucking a field neither has both
* normalize to [], and `[] === []` made a vacuous comparison GREEN. Seeding a real
* qitem cannot defend against this, because the PROJECTION is what empties the
* set. Empty-from-empty is legal (a surface really can hold nothing);
* empty-from-NON-empty means the declaration does not match the surface, and that
* must fail visibly instead of becoming equality evidence.
*/
export class ProjectionError extends Error {
constructor(message: string) {
super(message);
this.name = "ProjectionError";
}
}
/** 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,
surface = "surface",
): unknown {
const base = spec.path ? readPath(value, spec.path) : value;
if (!spec.pluck) return base;
const items = Array.isArray(base) ? base : [base];
const nonEmptyInput = items.some((i) => i !== undefined && i !== null);
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));
// A declaration that matches nothing on a surface that HAS data is a broken
// declaration, not evidence of agreement. Fail loud; never return [] here.
if (plucked.length === 0 && nonEmptyInput) {
throw new ProjectionError(
`${surface}: projection { pluck: "${spec.pluck}"${spec.path ? `, path: "${spec.path}"` : ""} } extracted NOTHING ` +
`from a non-empty surface (${items.length} item(s)). An empty projection from real data cannot be equality ` +
`evidence — two surfaces both extracting nothing compare equal and prove nothing. Check the declared field ` +
`against the surface's actual shape.`,
);
}
// 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, surface) : value;
};
}
@@ -15,7 +15,6 @@
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;
@@ -150,16 +149,12 @@ async function runEqualsExpect(
deps: ScenarioRunnerDeps,
): Promise<string | null> {
const surfaces = extractEqualsSurfaces(equalsPayload);
// 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) {
if (!deps.normalizer) {
throw new Error(
"`equals` requires a declarative cross-surface mapping (surface -> projection) or an injected normalizer",
"`equals` requires a declarative cross-surface normalizer (its shape rides 51-03 shaping)",
);
}
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,10 +86,6 @@ export type ValidationErrorCode =
| "EXPECT_MATCH_MODE_MISSING"
| "EXPECT_MATCH_MODE_AMBIGUOUS"
| "WITHIN_NOT_A_DURATION"
| "EQUALS_PROJECTION_INVALID"
| "EQUALS_SURFACE_UNKNOWN"
| "EQUALS_NOT_DECLARATIVE"
| "EQUALS_TOO_FEW_SURFACES"
| "EMIT_NOT_OBJECT"
| "UNKNOWN_EMIT_BEHAVIOR"
| "USAGE_LIMIT_IN_STUB_TOPOLOGY"
@@ -321,67 +317,6 @@ 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 (value.equals !== undefined && !isPlainObject(value.equals)) {
// Guard finding: the legacy list form still parsed, so a scenario could name
// surfaces without declaring HOW they compare — which is what left the
// comparison to an injected placeholder. A-N1 makes the declarative mapping
// the only scenario-facing form; refuse anything else at load and teach it.
push(
"EQUALS_NOT_DECLARATIVE",
`${path}.equals: must be the DECLARATIVE mapping of surface -> projection, e.g. ` +
`{ ps: { pluck: name }, queue: { pluck: destinationSession, rig: true } }. ` +
`A bare list of surfaces names what to compare without declaring HOW, so the comparison cannot be honest.`,
`${path}.equals`,
);
}
if (isPlainObject(value.equals)) {
// A comparison needs at least TWO sides. One surface (or none) is vacuous by
// construction — it passes whatever the data is.
const declaredSurfaces = Object.keys(value.equals);
if (declaredSurfaces.length < 2) {
push(
"EQUALS_TOO_FEW_SURFACES",
`${path}.equals: needs at least TWO surfaces to compare, found ${declaredSurfaces.length}` +
`${declaredSurfaces.length ? ` (${declaredSurfaces.join(", ")})` : ""} — a one-sided equality passes ` +
`regardless of the data and proves nothing.`,
`${path}.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.path !== undefined && typeof spec.path !== "string") {
// was accepted at load and then threw `path.split is not a function` at
// runtime — a TypeError must never be the first signal.
push("EQUALS_PROJECTION_INVALID", `${path}.equals.${surf}.path: must be a dot-path string`, `${path}.equals.${surf}.path`);
}
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(
@@ -1,128 +0,0 @@
import { describe, it, expect } from "vitest";
import {
projectSurface,
buildDeclarativeNormalizer,
isEqualsMapping,
rigOf,
ProjectionError,
} from "./helpers/scenario-normalizer.js";
import { validateScenario } from "./helpers/scenario-schema.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("returns empty for genuinely empty input, but THROWS when real data yields nothing", () => {
expect(projectSurface([], { pluck: "name" })).toEqual([]);
expect(projectSurface(null, { pluck: "name" })).toEqual([]);
// non-empty input + zero extracted = broken declaration, not agreement
expect(() => projectSurface([{ other: 1 }], { pluck: "name" })).toThrow(ProjectionError);
});
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" }]));
});
});
// Guard finding on a7b6b7c85, reproduced by the desk: my own falsification broke
// ONE side (name -> rigId) and got a correct FAIL, so I called the comparison
// value-sensitive. It is — on the DIFFERENCE axis. It could not detect EMPTINESS.
// A falsification only covers the axis it perturbs; these cover the others.
describe("the axes my one-sided falsification could not reach", () => {
it("SYMMETRIC EMPTY: both sides plucking a missing field must FAIL LOUD, never compare equal", () => {
const n = buildDeclarativeNormalizer({
ps: { pluck: "nosuchfield" },
queue: { pluck: "alsomissing" },
});
// two DISTINCT non-empty surfaces; before the fix both became [] and passed
expect(() => n("ps", [{ name: "scn-baton" }])).toThrow(ProjectionError);
expect(() => n("queue", [{ destinationSession: "w@scn-baton" }])).toThrow(ProjectionError);
let msg = "";
try { n("ps", [{ name: "scn-baton" }]); } catch (e) { msg = (e as Error).message; }
expect(msg).toContain("extracted NOTHING");
expect(msg).toContain("nosuchfield");
});
it("empty-from-EMPTY stays legal — a surface may honestly hold nothing", () => {
const n = buildDeclarativeNormalizer({ queue: { pluck: "destinationSession", rig: true } });
expect(n("queue", [])).toEqual([]);
});
it("a partially-matching projection does NOT throw (some data extracted is a real answer)", () => {
const n = buildDeclarativeNormalizer({ ps: { pluck: "name" } });
expect(n("ps", [{ name: "a" }, { other: 1 }])).toEqual(["a"]);
});
});
describe("load-time refusals (a TypeError must never be the first signal)", () => {
const base = (equals: unknown) => ({
scenario: "s", topology: "t.yaml",
steps: [{ expect: { surface: "ps", equals } }],
});
const codes = (doc: unknown) => {
const r = validateScenario(doc);
return r.ok ? [] : r.errors.map((e) => e.code);
};
it("refuses a SINGLE-surface equals — one-sided comparison is vacuous by construction", () => {
expect(codes(base({ ps: { pluck: "name" } }))).toContain("EQUALS_TOO_FEW_SURFACES");
});
it("refuses an EMPTY equals mapping", () => {
expect(codes(base({}))).toContain("EQUALS_TOO_FEW_SURFACES");
});
it("refuses the LEGACY list form, naming the declarative form", () => {
expect(codes(base(["ps", "queue"]))).toContain("EQUALS_NOT_DECLARATIVE");
});
it("refuses a non-string path at LOAD instead of throwing at runtime", () => {
expect(codes(base({ ps: { path: 123 }, queue: { pluck: "x" } }))).toContain("EQUALS_PROJECTION_INVALID");
});
it("accepts a well-formed two-surface declarative mapping", () => {
const r = validateScenario(base({ ps: { pluck: "name" }, queue: { pluck: "destinationSession", rig: true } }));
expect(r.ok).toBe(true);
});
});
+4 -21
View File
@@ -91,31 +91,14 @@ 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: the DECLARATIVE equals mapping over the two daemon-truth surfaces", () => {
it("expresses #10 one-view-state: validates the cross-surface equals over tui_socket/ps/queue", () => {
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 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");
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"]);
});
});
+4 -12
View File
@@ -196,14 +196,7 @@ describe("D1 R4/R5 — pipeline-level fences", () => {
"",
].join("\n"),
);
// Measure in a PRIVATE temp root: scaffolds land under os.tmpdir(), which reads
// TMPDIR per call, so counting the SHARED /tmp made this flake whenever another
// suite created a scaffold concurrently (same self-induced contention the D5
// pre-effect pin hit).
const priv = mkdtempSync(join(tmpdir(), "r4-preeffect-"));
dirs.push(priv);
const savedTmp = process.env.TMPDIR;
process.env.TMPDIR = priv;
const before = readdirSync(tmpdir()).filter((n) => n.startsWith("openrig-scenario-"));
let spawned = false;
await expect(
runScenarioFile(scenarioPath, {
@@ -214,10 +207,9 @@ describe("D1 R4/R5 — pipeline-level fences", () => {
}),
).rejects.toThrow(StubScriptTargetError);
expect(spawned).toBe(false);
// no scaffold was created either — the private root is still empty
expect(readdirSync(priv)).toEqual([]);
if (savedTmp === undefined) delete process.env.TMPDIR;
else process.env.TMPDIR = savedTmp;
// no scaffold was created either
const after = readdirSync(tmpdir()).filter((n) => n.startsWith("openrig-scenario-"));
expect(after.length).toBe(before.length);
});
it("R5: container mode REFUSES scripted scenarios by name, and is untouched without scripts", async () => {