G V-8 (TS), A-17, test: the TypeScript writer is generic over the contract's bodies and names its events; findings-asked is mesh-conditions' event

The TypeScript writer holds no FindingBody of its own and works on the wire form (checkFinding, views reader findings laid out by
viewFindingBody); both writers name findings-asked as the seat mesh-conditions' event. The view-waits test waits on the raise,
not on a count of yields.

Builds: design 47 G §686, §640, §687, A-3, A-13, A-17
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 21:45:07 +01:00
parent 43960cc925
commit 58bbc26219
6 changed files with 165 additions and 130 deletions
+7 -6
View File
@@ -10,9 +10,13 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
)
// FindingsAskedEvent is the event `findings-asked`, always the seat mesh-conditions' event, named with that seat (design 47 G
// A-17); a stdio module's writer subscribes to it by this name, and no module supplies it.
const FindingsAskedEvent = "mesh-conditions.findings-asked"
// NewStdioWriter makes the finding writer of a stdio module (design 47 G A-16, §461, §485, §503): it fills `Publish` from
// the module's own event publish (stdio.Emit, answered once the bus acknowledged it) and `Listen` from its subscription
// (stdio.SubscribeNew, only the asks published after it is made), so no module writes that adapter. A `Publish` or `Listen`
// (stdio.SubscribeNew of FindingsAskedEvent, only the asks published after it is made), so no module writes that adapter. A `Publish` or `Listen`
// the config already holds is kept. The runtime publishes and subscribes by the event's name on the subject the module's
// membership names for it, so each of the writer's four subjects (FindingSubjects) is told by its event.
func NewStdioWriter[F, C, S, A any](cfg WriterConfig[F, C, S, A]) (*Writer[F, C, S, A], error) {
@@ -32,11 +36,8 @@ func newStdioWriter[F, C, S, A any](cfg WriterConfig[F, C, S, A],
}
}
if cfg.Listen == nil {
cfg.Listen = func(_ context.Context, subject string, handle func([]byte)) (func(), error) {
event, err := eventOfSubject(subject)
if err != nil {
return nil, err
}
cfg.Listen = func(_ context.Context, _ string, handle func([]byte)) (func(), error) {
event := FindingsAskedEvent
var stopped atomic.Bool
// the runtime keeps a subscription until the process ends: a stopped listener hands on nothing
if err := subscribe(event, func(env stdio.Envelope) error {
+1 -1
View File
@@ -85,7 +85,7 @@ func TestStdioWriterPublishesAndListensThroughTheModulesOwnPlumbing(t *testing.T
}
var heard []string
stop, err := w.cfg.Listen(context.Background(), w.subj.Asked, func(b []byte) { heard = append(heard, string(b)) })
if err != nil || len(subscribed) != 1 || subscribed[0] != "findings-asked" {
if err != nil || len(subscribed) != 1 || subscribed[0] != "mesh-conditions.findings-asked" {
t.Fatalf("the listen: %v %v", subscribed, err)
}
_ = deliver(stdio.Envelope{Body: json.RawMessage(`{"ask":"x"}`)})
+6 -3
View File
@@ -310,8 +310,9 @@ func TestViews(t *testing.T) {
defer func() { waitBound = kept }()
var raised, cleared, said []Status
var mu sync.Mutex
raisedCh := make(chan struct{}, 4) // told after the part's raise, so the test reads it only once it is made
f := &Follower{Owner: "o", Tool: "o.revision", State: BranchState("catalog", "trunk"), Now: now,
RaiseWaits: func(s Status) { mu.Lock(); raised = append(raised, s); mu.Unlock() },
RaiseWaits: func(s Status) { mu.Lock(); raised = append(raised, s); mu.Unlock(); raisedCh <- struct{}{} },
ClearWaits: func(s Status) { mu.Lock(); cleared = append(cleared, s); mu.Unlock() },
Said: func(s Status) { mu.Lock(); said = append(said, s); mu.Unlock() }}
f.Holds("trunk.11")
@@ -321,8 +322,10 @@ func TestViews(t *testing.T) {
t.Fatalf("status = %+v", s)
}
ended <- true
for i := 0; i < 1000 && !f.Status().WaitsRaised; i++ {
runtime.Gosched()
select {
case <-raisedCh:
case <-time.After(10 * time.Second):
t.Fatal("the bound ended and view-waits was not raised")
}
mu.Lock()
if len(raised) != 1 || raised[0].WaitsFor != "trunk.12" {
+46
View File
@@ -482,6 +482,52 @@ export function viewWaitsFinding(part: string, s: Status, at: Date): Finding {
};
}
/** The kinds a views reader's findings written through the part's writer are of, and the one that is urgent (design 47 G §687). */
const TOO_LARGE_KIND = "view-too-large";
/** A name as a key's part (design 47 G §73): a character outside letters, digits, `-`, `/` and `_` is `=` and its two lowercase hex digits per byte. */
export function escapeName(name: string): string {
let out = "";
for (const b of new TextEncoder().encode(name)) {
const c = String.fromCharCode(b);
out += /[A-Za-z0-9\-/_]/.test(c) ? c : `=${b.toString(16).padStart(2, "0")}`;
}
return out;
}
/**
* A views reader's finding laid out in its wire form by mesh-sdk (design 47 G §687): `kind` the finding's, `scope` module,
* `id` the finding's id escaped as a key's name is, `severity` warning but urgent for view-too-large, `source` views,
* `subject` the part's module on its node, `summary` and `headline` the finding's texts. The Go side's `views.WriterFindings`.
*/
export function viewFindingBody(f: Finding, module: string, node: string): Record<string, unknown> {
return {
kind: f.kind,
scope: "module",
id: escapeName(f.id),
severity: f.kind === TOO_LARGE_KIND ? "urgent" : "warning",
source: "views",
subject: { scope: "module", id: module, ...(node ? { machine: node } : {}) },
summary: f.summary,
...(f.headline ? { headline: f.headline } : {}),
...(f.at ? { at: f.at } : {}),
};
}
/** The part of a Writer a views reader's findings use, whatever the contract's body types. */
export interface WireWriter {
write(finding: any): Promise<void>;
clear(scope: string, id: string, kind: string, why: string): Promise<void>;
}
/** A views reader's findings written through the part's writer, as kinds of the reader's own (design 47 G §639, §687). */
export function writerFindings(writer: WireWriter, module: string, node: string): { put(f: Finding): Promise<void>; delete(f: Finding): Promise<void> } {
return {
put: (f) => writer.write(viewFindingBody(f, module, node)),
delete: (f) => writer.clear("module", escapeName(f.id), f.kind, "the part no longer observes it"),
};
}
/** A Follower's `said` that keeps its status at statusKey in the reader's state STATUS_STATE. */
export function keepStatus(st: State<Status>): (s: Status) => Promise<void> {
return async (s) => {
+80 -111
View File
@@ -1,74 +1,44 @@
// The finding writer, TypeScript side (design 47 G §461, §485, §503): the same acts as the Go `views` writer, with the
// same bodies (§439) and rules, for the observers written in TypeScript. It keeps an observer's findings in its own state
// `findings` and says them as events (§383, §384). It names no seat and no module: the observer hands in the subjects of
// `finding-written`, `finding-cleared`, `findings-stated` and `findings-asked`, which the seat's generated contract makes.
// same rules, for the observers written in TypeScript, generic over the contract's body types (plain structures) and
// working on their wire form, with no layout of a finding of its own (design 47 G A-3, §640). It keeps an observer's findings in its own state
// `findings` and says them as events (§383, §384). It names the four events itself (design 47 G A-13, A-17): it says
// `finding-written`, `finding-cleared` and `findings-stated` by their names, which the runtime publishes on the subject the
// module's membership names for each, and hears `findings-asked` as the seat mesh-conditions' event; a module passes none.
// Its acts, `write`, `clear` and a statement, run one at a time in the order they are asked, on one chain of promises.
import type { State } from "../state/index.js";
/** The event `findings-asked` is always the seat mesh-conditions' (design 47 G A-17), named with that seat. */
export const FINDINGS_ASKED_EVENT = "mesh-conditions.findings-asked";
/** The events the writer says, by their names (design 47 G §383, A-13). */
export const FINDING_WRITTEN_EVENT = "finding-written";
export const FINDING_CLEARED_EVENT = "finding-cleared";
export const FINDINGS_STATED_EVENT = "findings-stated";
/** How long an event the bus did not acknowledge is sent again before it is logged as an error (§383). */
export const PUBLISH_RETRY_MS = 60 * 1000;
/** How often a failing `observe` is called again until a set is said (§485). */
export const OBSERVE_RETRY_MS = 60 * 1000;
/** What a finding is about. */
export interface FindingSubject {
scope: string;
id: string;
machine?: string;
also?: string[];
}
/** One observation, as it was said. */
export interface FindingEvidence {
at: string;
said: string;
}
/** One answer the operator may give from a notification. */
export interface FindingAction {
label: string;
verb: string;
machine?: string;
arguments?: Record<string, string>;
level?: "acknowledge" | "approve";
}
/**
* One finding as an observer says it: the layout whole of §439's list. It is the body of `finding-written`, of each entry
* of `findings-stated`, and the value of its key `<scope>.<id>.<kind>` in the state `findings`.
* What the writer needs of a contract's finding body (design 47 G A-3, §640): the fields it reads and sets by their
* contract names. The body types are the seat generator's, plain structures; a body type lacking one of these fields
* does not compile as the writer's `F`, and a value lacking its `subject` is refused at its first write.
*/
export interface FindingBody {
export interface FindingFields {
kind: string;
scope: string;
id: string;
subject: object;
at?: string;
since?: string;
severity: "urgent" | "warning";
summary: string;
headline?: string;
explanation?: string;
subject: FindingSubject;
evidence?: FindingEvidence[];
source: string;
observer?: string;
owner?: string;
"waiting-on"?: string;
actions?: FindingAction[];
needs?: string;
resolver?: string;
ref?: string;
run?: string;
}
/** The body of `finding-cleared`. */
export interface FindingCleared {
kind: string;
scope: string;
id: string;
at: string;
why: string;
}
/** A body in its wire form (design 47 G A-3). */
type Wire = Record<string, unknown>;
/** A key the observer cannot judge yet since its start: its `since` and its bound (a duration such as `5m`). */
export interface PendingKey {
@@ -77,27 +47,9 @@ export interface PendingKey {
bound: string;
}
/** The body of `findings-stated`. */
export interface FindingsStated {
at: string;
asked?: string;
/** The kinds this statement covers (design 47 G §580); none means every kind. */
kinds?: string[];
findings: FindingBody[];
pending: PendingKey[];
}
/** The body of `findings-asked`. */
export interface FindingsAsked {
ask: string;
at: string;
by: string;
module?: string;
}
/** What the observer's own truth says now. */
export interface Observed {
findings: FindingBody[];
/** What the observer's own truth says now: its findings, in the contract's finding type, and its pending keys. */
export interface Observed<F> {
findings: F[];
pending: PendingKey[];
}
@@ -109,8 +61,9 @@ const SEAT_FORM = /^[a-z][a-z0-9-]*$/;
const AUTHOR_FORM = /^forge:[A-Za-z0-9._-]+$/;
/** A finding's `owner` or `waiting-on` is a session (who@node), a seat's name or a commit's author (design 47 G §678). */
function formOfWho(field: string, v: string | undefined): void {
if (!v || SESSION_FORM.test(v) || SEAT_FORM.test(v) || AUTHOR_FORM.test(v)) return;
function formOfWho(field: string, v: unknown): void {
if (v === undefined || v === null || v === "") return;
if (typeof v === "string" && (SESSION_FORM.test(v) || SEAT_FORM.test(v) || AUTHOR_FORM.test(v))) return;
throw new Error(`the finding's ${field} "${v}" is none of a session (who@node), a seat's name or a commit's author (forge:<account>)`);
}
@@ -119,21 +72,36 @@ export function findingKey(scope: string, id: string, kind: string): string {
return `${scope}.${id}.${kind}`;
}
export interface WriterOptions {
/**
* Refuse a finding the keeper could not make a condition of, naming the field of the first rule it breaks: the key's three
* parts (each given, the kind with no dot, since the kind is the key's last part), the subject, and the forms of `owner`
* and `waiting-on` (design 47 G §678, §689, A-3). It judges the finding's wire form: the contract's generated type is one.
*/
export function checkFinding(finding: object): void {
const m = finding as Wire;
const given = (f: string): boolean => typeof m[f] === "string" && (m[f] as string).trim() !== "";
if (!given("scope")) throw new Error("the finding's scope is not given");
if (!given("id")) throw new Error("the finding's id is not given");
if (!given("kind")) throw new Error("the finding's kind is not given");
if ((m.kind as string).includes(".")) throw new Error(`the finding's kind "${m.kind}" holds a dot, and the kind is the last part of its key`);
if (!m.subject || typeof m.subject !== "object") throw new Error("the finding's subject is not given, and the contract's finding names it");
formOfWho("owner", m.owner);
formOfWho("waiting-on", m["waiting-on"]);
}
export interface WriterOptions<F extends FindingFields, C extends object, S extends object, A extends object> {
/** The observer's module name: it fills the `observer` of a finding and tells an ask for another module. */
module: string;
/** The observer's own state `findings`. */
state: State<FindingBody>;
state: State<F>;
/** The finding kinds this writer owns (design 47 G §580): a write or clear of another kind is refused, naming it, and a statement carries them as `kinds` and deletes only keys of them. None: every kind. The views reader's kinds are always its own besides. */
kinds?: string[];
/** The subjects of the four events. */
subjects: { written: string; cleared: string; stated: string; asked: string };
/** The findings the observer observes now and its pending keys. It is called inside the writer's one chain: it must not call `write` or `clear`. */
observe(): Promise<Observed>;
/** Say one message and resolve when the bus acknowledged it; `id` is the bus's de-duplication id. */
publish(subject: string, body: unknown, id: string): Promise<void>;
/** Make a consumer of `subject` that delivers only the messages published after it is made; resolves to its stop. */
listen(subject: string, handle: (body: unknown) => void): Promise<() => void>;
observe(): Promise<Observed<F>>;
/** Say one event, by its name, and resolve when the bus acknowledged it; `id` is the bus's de-duplication id. */
publish(subject: string, body: F | C | S, id: string): Promise<void>;
/** Make a consumer of the event `subject` names that delivers only the messages published after it is made; resolves to its stop. */
listen(subject: string, handle: (body: A) => void): Promise<() => void>;
/** Put a set larger than one bus message in the observer's sent store; resolves to what to publish instead. */
send?(label: string, body: unknown): Promise<unknown>;
log?(line: string): void;
@@ -142,9 +110,9 @@ export interface WriterOptions {
wait?(ms: number): Promise<boolean>;
}
/** The finding writer of an observer. */
export class Writer {
private readonly o: WriterOptions;
/** The finding writer of an observer: `F`, `C`, `S` and `A` are the contract's bodies of `finding-written`, `finding-cleared`, `findings-stated` and `findings-asked`. */
export class Writer<F extends FindingFields, C extends object, S extends object, A extends object> {
private readonly o: WriterOptions<F, C, S, A>;
private chain: Promise<unknown> = Promise.resolve();
private seq = 0;
private stop?: () => void;
@@ -154,7 +122,7 @@ export class Writer {
private readonly closing: Promise<void>;
private close_!: () => void;
constructor(o: WriterOptions) {
constructor(o: WriterOptions<F, C, S, A>) {
this.o = o;
this.closing = new Promise((r) => (this.close_ = r));
}
@@ -206,7 +174,7 @@ export class Writer {
try {
await this.listen();
} catch (e) {
this.log(`error: the finding writer could not make its consumer of ${this.o.subjects.asked} again: ${e}`);
this.log(`error: the finding writer could not make its consumer of ${FINDINGS_ASKED_EVENT} again: ${e}`);
}
this.request("");
}
@@ -220,13 +188,13 @@ export class Writer {
}
private async listen(): Promise<void> {
const stop = await this.o.listen(this.o.subjects.asked, (body) => this.heard(body));
const stop = await this.o.listen(FINDINGS_ASKED_EVENT, (body) => this.heard(body));
this.stop?.();
this.stop = stop;
}
private heard(body: unknown): void {
const a = body as FindingsAsked | null;
const a = body as Wire | null;
if (!a || typeof a.ask !== "string") {
this.log("an ask for findings could not be read");
return;
@@ -263,12 +231,12 @@ export class Writer {
}
/** Keep the finding in its key and say `finding-written` only when what it says changed; the key's `since` stays. */
write(f: FindingBody): Promise<void> {
write(f: F): Promise<void> {
return this.act(async () => {
const [full, old] = await this.complete(f);
await this.o.state.put(key(full), full);
await this.o.state.put(key(full), full as unknown as F);
if (old && unsaid(old) === unsaid(full)) return;
await this.say(this.o.subjects.written, "finding-written", key(full), full);
await this.say(FINDING_WRITTEN_EVENT, "finding-written", key(full), full as unknown as F);
});
}
@@ -278,8 +246,8 @@ export class Writer {
if (!this.owns(kind)) throw new Error(`the finding kind "${kind}" is not one this writer owns (${this.o.kinds})`);
const k = findingKey(scope, id, kind);
await this.o.state.delete(k);
const body: FindingCleared = { kind, scope, id, at: this.stamp(this.now()), why };
await this.say(this.o.subjects.cleared, "finding-cleared", k, body);
const body = { kind, scope, id, at: this.stamp(this.now()), why };
await this.say(FINDING_CLEARED_EVENT, "finding-cleared", k, body as unknown as C);
});
}
@@ -289,10 +257,10 @@ export class Writer {
const obs = await this.o.observe();
const keep = new Set<string>();
const kinds = this.o.kinds?.length ? [...new Set([...this.o.kinds, ...VIEW_KINDS])].sort() : undefined;
const body: FindingsStated = { at: this.stamp(this.now()), ...(asked ? { asked } : {}), ...(kinds ? { kinds } : {}), findings: [], pending: [] };
const body = { at: this.stamp(this.now()), ...(asked ? { asked } : {}), ...(kinds ? { kinds } : {}), findings: [] as Wire[], pending: [] as PendingKey[] };
for (const f of obs.findings) {
const [full] = await this.complete(f);
await this.o.state.put(key(full), full);
await this.o.state.put(key(full), full as unknown as F);
keep.add(key(full));
body.findings.push(full);
}
@@ -307,38 +275,38 @@ export class Writer {
const e = await this.o.state.get(k);
if (e) {
keep.add(k);
body.findings.push(e.value);
body.findings.push(e.value as unknown as Wire);
}
}
if (!keep.has(k) && this.owns(kind)) await this.o.state.delete(k);
}
let out: unknown = body;
let out = body as unknown as S;
if (this.o.send) {
try {
out = await this.o.send(`findings-stated.${this.o.module}`, body);
out = (await this.o.send(`findings-stated.${this.o.module}`, body)) as S;
} catch (e) {
this.log(`error: the findings-stated of ${this.o.module} could not be sent, the next statement covers it: ${e}`);
return;
}
}
await this.say(this.o.subjects.stated, "findings-stated", this.o.module, out);
await this.say(FINDINGS_STATED_EVENT, "findings-stated", this.o.module, out);
});
}
private async complete(f: FindingBody): Promise<[FindingBody, FindingBody | null]> {
if (!f.kind || !f.scope || !f.id) throw new Error(`a finding needs its kind, scope and id: ${f.scope}.${f.id}.${f.kind}`);
if (!this.owns(f.kind)) throw new Error(`the finding kind "${f.kind}" is not one this writer owns (${this.o.kinds})`);
formOfWho("owner", f.owner);
formOfWho("waiting-on", f["waiting-on"]);
private async complete(finding: F): Promise<[Wire, Wire | null]> {
const f: Wire = { ...(finding as object) };
checkFinding(f);
const kind = f.kind as string;
if (!this.owns(kind)) throw new Error(`the finding kind "${kind}" is not one this writer owns (${this.o.kinds})`);
const e = await this.o.state.get(key(f));
const old = e ? e.value : null;
const at = f.at ?? this.stamp(this.now());
const since = old?.since ?? f.since ?? at;
const old = e ? (e.value as unknown as Wire) : null;
const at = typeof f.at === "string" && f.at ? f.at : this.stamp(this.now());
const since = (typeof old?.since === "string" && old.since) || (typeof f.since === "string" && f.since) || at;
return [{ ...f, observer: f.observer || this.o.module, at, since }, old];
}
/** Publish awaiting the acknowledgement, again for a minute when unacknowledged, then log the error naming what it said. */
private async say(subject: string, event: string, what: string, body: unknown): Promise<void> {
private async say(subject: string, event: string, what: string, body: F | C | S): Promise<void> {
const id = `${this.o.module}-${event}-${this.now().getTime()}-${++this.seq}`;
const start = this.now().getTime();
for (let delay = 1000; ; delay = Math.min(delay * 2, 10000)) {
@@ -355,12 +323,13 @@ export class Writer {
}
}
function key(f: FindingBody): string {
return findingKey(f.scope, f.id, f.kind);
function key(f: object): string {
const m = f as Wire;
return findingKey(m.scope as string, m.id as string, m.kind as string);
}
/** A finding as compared to tell whether what it says changed: not the time it was last seen, nor what saw it. */
function unsaid(f: FindingBody): string {
function unsaid(f: Wire): string {
const { at: _at, run: _run, ...rest } = f;
return canonical(rest);
}
+25 -9
View File
@@ -861,7 +861,7 @@ test("the finding writer writes, clears and states findings as the Go writer doe
const w = new Writer({
module: "m",
state: st,
subjects: { written: "w", cleared: "c", stated: "s", asked: "a" },
observe: async () => {
if (++observes <= 2) throw new Error("not yet");
return { findings: [{ kind: "k", scope: "module", id: "a", severity: "warning", summary: "now", source: "p", subject: { scope: "module", id: "a" } }], pending: [{ key: "module.wait.k", since: "2026-11-11T11:00:00Z", bound: "5m" }] };
@@ -870,7 +870,7 @@ test("the finding writer writes, clears and states findings as the Go writer doe
if (refuse > 0) { refuse--; throw new Error("no ack"); }
sent.push({ subject, body });
},
listen: async (_s, h) => { consumers++; ask = h; return () => {}; },
listen: async (s, h) => { assert.equal(s, "mesh-conditions.findings-asked"); consumers++; ask = h; return () => {}; },
log: (l) => logs.push(l),
now: () => new Date(clock),
wait: async (ms) => { waits.push(ms); clock += ms; return true; },
@@ -881,11 +881,11 @@ test("the finding writer writes, clears and states findings as the Go writer doe
clock += 3600_000;
await w.write({ ...f, run: "other" }); // nothing it says changed
await w.write({ ...f, summary: "two" });
assert.deepEqual(sent.map((s) => s.subject), ["w", "w"]);
assert.deepEqual(sent.map((s) => s.subject), ["finding-written", "finding-written"]);
assert.equal(sent[1].body.since, first);
assert.equal(sent[1].body.observer, "m");
await w.clear("module", "never", "k", "held by the keeper");
assert.equal(sent[2].subject, "c");
assert.equal(sent[2].subject, "finding-cleared");
assert.equal(sent[2].body.why, "held by the keeper");
// an unacknowledged event is sent again for a minute, then logged naming the finding
refuse = 1000;
@@ -900,7 +900,7 @@ test("the finding writer writes, clears and states findings as the Go writer doe
await w.start();
for (let i = 0; i < 50 && sent.length < 4; i++) await new Promise((r) => setImmediate(r));
const stated = sent[3];
assert.equal(stated.subject, "s");
assert.equal(stated.subject, "findings-stated");
assert.deepEqual(waits.slice(0, 2), [OBSERVE_RETRY_MS, OBSERVE_RETRY_MS]);
assert.equal(stated.body.findings.length, 1);
assert.equal(stated.body.findings[0].since, first);
@@ -921,7 +921,7 @@ test("the finding writer writes, clears and states findings as the Go writer doe
let release!: () => void;
const gate = new Promise<void>((r) => (release = r));
const w2 = new Writer({
module: "m", state: st, subjects: { written: "w", cleared: "c", stated: "s", asked: "a" },
module: "m", state: st,
observe: async () => { await gate; return { findings: [], pending: [] }; },
publish: async () => {}, listen: async () => () => {},
});
@@ -935,7 +935,7 @@ test("the finding writer writes, clears and states findings as the Go writer doe
});
test("the finding writer owns its kinds, keeps a views reader's findings in its statement and refuses an owner of no form", async () => {
const { Writer, VIEW_KINDS } = await import("../dist/views/index.js");
const { Writer, VIEW_KINDS, checkFinding, escapeName, viewFindingBody, writerFindings, viewWaitsFinding } = await import("../dist/views/index.js");
const store = new Map<string, any>();
const st: any = {
get: async (k: string) => (store.has(k) ? { key: k, value: store.get(k), revision: 1 } : null),
@@ -945,7 +945,7 @@ test("the finding writer owns its kinds, keeps a views reader's findings in its
};
const sent: { subject: string; body: any }[] = [];
const w = new Writer({
module: "m", state: st, kinds: ["k"], subjects: { written: "w", cleared: "c", stated: "s", asked: "a" },
module: "m", state: st, kinds: ["k"],
observe: async () => ({ findings: [], pending: [] }),
publish: async (subject, body) => void sent.push({ subject, body }),
listen: async () => () => {},
@@ -954,6 +954,10 @@ test("the finding writer owns its kinds, keeps a views reader's findings in its
await assert.rejects(w.write({ ...f, kind: "other" }), /"other"/);
await assert.rejects(w.clear("module", "a", "other", "why"), /"other"/);
await assert.rejects(w.write({ ...f, owner: "Not A Form" }), /owner/);
// a body whose wire form lacks a field the contract names fails at its first write, naming it (design 47 G A-3)
const { subject: _s, ...noSubject } = f;
await assert.rejects(w.write(noSubject as any), /subject/);
assert.throws(() => checkFinding({ ...f, kind: "a.b" }), /dot/);
await assert.rejects(w.write({ ...f, "waiting-on": "x y" }), /waiting-on/);
await w.write({ ...f, owner: "me@node-1" });
await w.write({ ...f, kind: VIEW_KINDS[0], id: "m.o.s" });
@@ -961,7 +965,19 @@ test("the finding writer owns its kinds, keeps a views reader's findings in its
await w.state();
assert.deepEqual([...store.keys()].sort(), ["module.m.o.s.view-waits", "module.x.other"].sort());
const stated = sent.at(-1)!;
assert.equal(stated.subject, "s");
assert.equal(stated.subject, "findings-stated");
assert.ok(stated.body.kinds.includes("k") && stated.body.kinds.includes("view-waits"));
assert.equal(stated.body.findings.length, 1);
// a views reader's findings are laid out by the writer from the part's plain ones (design 47 G §687)
assert.equal(escapeName("a b.c"), "a=20b=2ec");
const vf = viewWaitsFinding("m", { owner: "o", state: "s", "waiting-since": "2026-11-11T12:00:00Z", "waits-for": "r7", view: "" } as any, new Date("2026-11-11T12:06:00Z"));
const laid = viewFindingBody(vf, "m", "n1");
assert.deepEqual([laid.kind, laid.scope, laid.id, laid.severity, laid.source], ["view-waits", "module", "m=2eo=2es", "warning", "views"]);
assert.deepEqual(laid.subject, { scope: "module", id: "m", machine: "n1" });
assert.equal(viewFindingBody({ ...vf, kind: "view-too-large" }, "m", "n1").severity, "urgent");
const vw = writerFindings(w, "m", "n1");
await vw.put(vf);
assert.equal(store.has("module.m=2eo=2es.view-waits"), true);
await vw.delete(vf);
assert.equal(store.has("module.m=2eo=2es.view-waits"), false);
});