G V-7: the TypeScript finding writer owns kinds, keeps a views reader's findings in its statement and judges an owner's form
Builds: design 47 G §580, §452, §639, §678 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
+37
-2
@@ -81,6 +81,8 @@ export interface PendingKey {
|
||||
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[];
|
||||
}
|
||||
@@ -99,6 +101,19 @@ export interface Observed {
|
||||
pending: PendingKey[];
|
||||
}
|
||||
|
||||
/** The finding kinds a views reader writes through the part's writer (design 47 G §639). */
|
||||
export const VIEW_KINDS = ["view-waits", "first-view-unanswered", "view-unsaid", "view-too-large"];
|
||||
|
||||
const SESSION_FORM = /^[a-z0-9][a-z0-9._-]*@[a-z0-9][a-z0-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;
|
||||
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>)`);
|
||||
}
|
||||
|
||||
/** The key of a finding in the state `findings`: `<scope>.<id>.<kind>`. */
|
||||
export function findingKey(scope: string, id: string, kind: string): string {
|
||||
return `${scope}.${id}.${kind}`;
|
||||
@@ -109,6 +124,8 @@ export interface WriterOptions {
|
||||
module: string;
|
||||
/** The observer's own state `findings`. */
|
||||
state: State<FindingBody>;
|
||||
/** 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`. */
|
||||
@@ -142,6 +159,10 @@ export class Writer {
|
||||
this.closing = new Promise((r) => (this.close_ = r));
|
||||
}
|
||||
|
||||
private owns(kind: string): boolean {
|
||||
return !this.o.kinds?.length || this.o.kinds.includes(kind) || VIEW_KINDS.includes(kind);
|
||||
}
|
||||
|
||||
private log(line: string): void {
|
||||
this.o.log?.(line);
|
||||
}
|
||||
@@ -254,6 +275,7 @@ export class Writer {
|
||||
/** Delete the key and say `finding-cleared`, also of a key that does not exist, so a keeper holding it clears it. */
|
||||
clear(scope: string, id: string, kind: string, why: string): Promise<void> {
|
||||
return this.act(async () => {
|
||||
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 };
|
||||
@@ -266,7 +288,8 @@ export class Writer {
|
||||
return this.act(async () => {
|
||||
const obs = await this.o.observe();
|
||||
const keep = new Set<string>();
|
||||
const body: FindingsStated = { at: this.stamp(this.now()), ...(asked ? { asked } : {}), findings: [], pending: [] };
|
||||
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: [] };
|
||||
for (const f of obs.findings) {
|
||||
const [full] = await this.complete(f);
|
||||
await this.o.state.put(key(full), full);
|
||||
@@ -278,7 +301,16 @@ export class Writer {
|
||||
body.pending.push(p);
|
||||
}
|
||||
for (const k of await this.o.state.keys()) {
|
||||
if (!keep.has(k)) await this.o.state.delete(k);
|
||||
const kind = k.slice(k.lastIndexOf(".") + 1);
|
||||
// a views reader's open findings are stated beside observe's (design 47 G §639)
|
||||
if (!keep.has(k) && VIEW_KINDS.includes(kind)) {
|
||||
const e = await this.o.state.get(k);
|
||||
if (e) {
|
||||
keep.add(k);
|
||||
body.findings.push(e.value);
|
||||
}
|
||||
}
|
||||
if (!keep.has(k) && this.owns(kind)) await this.o.state.delete(k);
|
||||
}
|
||||
let out: unknown = body;
|
||||
if (this.o.send) {
|
||||
@@ -295,6 +327,9 @@ export class Writer {
|
||||
|
||||
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"]);
|
||||
const e = await this.o.state.get(key(f));
|
||||
const old = e ? e.value : null;
|
||||
const at = f.at ?? this.stamp(this.now());
|
||||
|
||||
@@ -933,3 +933,35 @@ test("the finding writer writes, clears and states findings as the Go writer doe
|
||||
await Promise.all([stating, writing]);
|
||||
assert.equal(store.has("module.during.k"), true);
|
||||
});
|
||||
|
||||
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 store = new Map<string, any>();
|
||||
const st: any = {
|
||||
get: async (k: string) => (store.has(k) ? { key: k, value: store.get(k), revision: 1 } : null),
|
||||
put: async (k: string, v: any) => (store.set(k, structuredClone(v)), 1),
|
||||
delete: async (k: string) => void store.delete(k),
|
||||
keys: async () => [...store.keys()].sort(),
|
||||
};
|
||||
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" },
|
||||
observe: async () => ({ findings: [], pending: [] }),
|
||||
publish: async (subject, body) => void sent.push({ subject, body }),
|
||||
listen: async () => () => {},
|
||||
});
|
||||
const f = { kind: "k", scope: "module", id: "a", severity: "warning" as const, summary: "one", source: "p", subject: { scope: "module", id: "a" } };
|
||||
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/);
|
||||
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" });
|
||||
store.set("module.x.other", f);
|
||||
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.ok(stated.body.kinds.includes("k") && stated.body.kinds.includes("view-waits"));
|
||||
assert.equal(stated.body.findings.length, 1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user