mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 22:58:42 -10:00
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:
1 parent
dcddb8a6ee
commit
dc0fb35cd4
8 files changed
+57
-394
No files matched your search
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in new issue
Block a user