@githolon/dsl 0.49.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/check_package.mjs +46 -0
- package/dart/lib/src/webview_bridge.dart +160 -6
- package/package.json +3 -2
- package/src/check_package_main.ts +83 -0
- package/src/codegen_dart_client.ts +103 -8
- package/src/codegen_proof.ts +1 -1
- package/src/codegen_ts.ts +289 -14
- package/src/compile_package_main.ts +19 -0
- package/src/law_check.ts +375 -0
package/src/law_check.ts
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
|
+
// QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* THE AUTHOR-TIME GATE — `githolon check` (architecture/author_time_gate.md, slice 1).
|
|
8
|
+
*
|
|
9
|
+
* Every DX bug the tenant teams reported was the same disease: the kernel gate refused
|
|
10
|
+
* something at RUNTIME that was STATICALLY VISIBLE at compile. The cure is to pre-fire
|
|
11
|
+
* the refusal at author time, with the FIX inside the message. This module is the
|
|
12
|
+
* unified surface: it runs the STATIC TWINS of the gate's refusal vocabulary over the
|
|
13
|
+
* composed domain modules and returns typed {@link Finding}s.
|
|
14
|
+
*
|
|
15
|
+
* Three verdict levels:
|
|
16
|
+
* - **refuse** — the fault is PAYLOAD-INDEPENDENT (return shape, schema/driver mismatch),
|
|
17
|
+
* so the runtime gate WILL refuse it. `githolon check` exits non-zero; `compile` refuses.
|
|
18
|
+
* - **warn** — the probe SAW something suspicious but can't PROVE it (a plan that threw on
|
|
19
|
+
* the one synthesized sample — a conditional/payload-dependent path). Names the payload.
|
|
20
|
+
* - **note** — an auto-repair was applied (e.g. the auto-declared secondary ensure).
|
|
21
|
+
*
|
|
22
|
+
* FALSE-POSITIVE DISCIPLINE (the hard rule, from the design doc): the probe runs ONE
|
|
23
|
+
* synthesized payload — a conditional branch it doesn't take proves nothing. A twin may
|
|
24
|
+
* REFUSE only when the fault is payload-independent; everything else is a WARN naming the
|
|
25
|
+
* payload that exhibited it. The runtime gate remains the arbiter; `check` never claims
|
|
26
|
+
* completeness, only that what it flags is real.
|
|
27
|
+
*/
|
|
28
|
+
import type { z } from "zod";
|
|
29
|
+
|
|
30
|
+
import type { AggregateHandle } from "./aggregate.js";
|
|
31
|
+
import type { Directive } from "./directive.js";
|
|
32
|
+
import type { DomainModule } from "./codegen_dart.js";
|
|
33
|
+
import type { Field } from "./fields.js";
|
|
34
|
+
import type { FieldOp, PlannedOp } from "./ops.js";
|
|
35
|
+
import { deterministicPorts } from "./ctx.js";
|
|
36
|
+
import { synthesizePayload, withBirthProbeGlobals, type SynthesizedPayload } from "./codegen_proof.js";
|
|
37
|
+
import { __drainAuthoring, __resetAuthoring } from "./authoring.js";
|
|
38
|
+
import { __drainReads, __resetReads } from "./read.js";
|
|
39
|
+
import { __drainBirths, __resetBirths } from "./births.js";
|
|
40
|
+
import { proofLegsIndex } from "./codegen_proof.js";
|
|
41
|
+
import { autoStampFields, zodKind, zodObjectShape } from "./codegen_ts.js";
|
|
42
|
+
|
|
43
|
+
/** One author-time finding — the gate refusal pre-fired at compile, or a courtesy note. */
|
|
44
|
+
export interface Finding {
|
|
45
|
+
/** The domain key the directive lives in. */
|
|
46
|
+
readonly domain: string;
|
|
47
|
+
/** The directive the finding is about (absent for a domain-wide note). */
|
|
48
|
+
readonly directiveId?: string;
|
|
49
|
+
/** `refuse` — provably refuses at the gate · `warn` — suspicious, can't prove · `note` — auto-repair. */
|
|
50
|
+
readonly level: "refuse" | "warn" | "note";
|
|
51
|
+
/** What is wrong (or, for a note, what was auto-repaired). */
|
|
52
|
+
readonly what: string;
|
|
53
|
+
/** How to fix it (or, for a note, how the repair keeps the law valid). */
|
|
54
|
+
readonly fix: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Payload field names the FRAMEWORK consumes off the intent (never the plan) — excluded from the
|
|
58
|
+
* unread-payload-field warn so a birth/timestamp convention field is never falsely flagged as dead.
|
|
59
|
+
* Sources: `autoStampFields` (the `…At` clock convention) + `birthMeterSubject`/`birthMeterBornAt`. */
|
|
60
|
+
const FRAMEWORK_CONSUMED_PAYLOAD_FIELDS: ReadonlySet<string> = new Set([
|
|
61
|
+
"principal",
|
|
62
|
+
"owner",
|
|
63
|
+
"ownerSubject",
|
|
64
|
+
"bornAt",
|
|
65
|
+
"requestedAt",
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/** The outcome of running one directive's plan on its synthesized sample under the probe globals. */
|
|
69
|
+
interface ProbeResult {
|
|
70
|
+
readonly synth: SynthesizedPayload;
|
|
71
|
+
/** The stamped-and-filled sample the plan was run with (what a WARN names). */
|
|
72
|
+
readonly probePayload: Record<string, unknown>;
|
|
73
|
+
/** The plan's RETURN value (whatever it was — checked for array-ness by the return twin). */
|
|
74
|
+
readonly returned: unknown;
|
|
75
|
+
/** True when the plan THREW on the sample (payload-dependent — everything degrades to a WARN). */
|
|
76
|
+
readonly threw: boolean;
|
|
77
|
+
readonly error?: unknown;
|
|
78
|
+
/** The FIELD ops the plan emitted (authoring sink + returned ops), when it didn't throw. */
|
|
79
|
+
readonly ops: FieldOp[];
|
|
80
|
+
/** The top-level payload keys the plan actually READ (via the tracking proxy). */
|
|
81
|
+
readonly readFields: ReadonlySet<string>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Run a directive's plan ONCE on a tracking-proxy sample under the birth-probe globals, capturing the
|
|
85
|
+
* return value, whether it threw, the emitted field ops, and which payload fields it read. `undefined`
|
|
86
|
+
* ⇒ the payload is unsynthesizable (an exotic zod shape) — the directive can't be probed at all. */
|
|
87
|
+
function probeDirective(dir: Directive<unknown>, where: string): ProbeResult | undefined {
|
|
88
|
+
let synth: SynthesizedPayload;
|
|
89
|
+
try {
|
|
90
|
+
synth = synthesizePayload(dir.payloadSchema as z.ZodTypeAny, where);
|
|
91
|
+
} catch {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
// Build the sample: required non-stamped fields from the synth, plus the auto-stamped `…At` fields
|
|
95
|
+
// filled with a fixed ISO stamp (the client stamps them at dispatch; the plan may read them here).
|
|
96
|
+
const probePayload: Record<string, unknown> = { ...synth.value };
|
|
97
|
+
for (const f of synth.autoStamped) probePayload[f] = "2026-01-01T00:00:00Z";
|
|
98
|
+
|
|
99
|
+
const shapeKeys = new Set(Object.keys(safeShape(dir.payloadSchema as z.ZodTypeAny)));
|
|
100
|
+
const readFields = new Set<string>();
|
|
101
|
+
const tracked = new Proxy(probePayload, {
|
|
102
|
+
get(target, prop, recv) {
|
|
103
|
+
if (typeof prop === "string" && shapeKeys.has(prop)) readFields.add(prop);
|
|
104
|
+
return Reflect.get(target, prop, recv);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
let returned: unknown;
|
|
109
|
+
let threw = false;
|
|
110
|
+
let error: unknown;
|
|
111
|
+
let ops: FieldOp[] = [];
|
|
112
|
+
withBirthProbeGlobals(() => {
|
|
113
|
+
__resetAuthoring();
|
|
114
|
+
__resetReads();
|
|
115
|
+
__resetBirths();
|
|
116
|
+
try {
|
|
117
|
+
returned = dir.plan(tracked as never, deterministicPorts({ physical: 1, replica: 1, principal: "principal:probe" }));
|
|
118
|
+
const drained: PlannedOp[] = [...__drainAuthoring(), ...(Array.isArray(returned) ? returned : [])];
|
|
119
|
+
ops = drained.filter((o): o is FieldOp => (o as { kind?: string }).kind !== "strike");
|
|
120
|
+
} catch (e) {
|
|
121
|
+
threw = true;
|
|
122
|
+
error = e;
|
|
123
|
+
} finally {
|
|
124
|
+
// Drain the sinks on every path so a throwing probe never leaks state into the next.
|
|
125
|
+
try { __drainAuthoring(); } catch { /* drained */ }
|
|
126
|
+
try { __drainReads(); } catch { /* drained */ }
|
|
127
|
+
try { __drainBirths(); } catch { /* drained */ }
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
return { synth, probePayload, returned, threw, error, ops, readFields };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** `zodObjectShape` with a swallow — a non-object payload yields `{}` (the return twin handles it). */
|
|
134
|
+
function safeShape(schema: z.ZodTypeAny): Record<string, z.ZodTypeAny> {
|
|
135
|
+
try {
|
|
136
|
+
if (zodKind(schema) !== "object") return {};
|
|
137
|
+
return zodObjectShape(schema);
|
|
138
|
+
} catch {
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The REQUIRED top-level payload fields (optional/defaulted omitted — an unread optional proves nothing). */
|
|
144
|
+
function requiredPayloadFields(schema: z.ZodTypeAny): string[] {
|
|
145
|
+
const out: string[] = [];
|
|
146
|
+
for (const [name, f] of Object.entries(safeShape(schema))) {
|
|
147
|
+
const k = zodKind(f);
|
|
148
|
+
if (k === "optional" || k === "default") continue;
|
|
149
|
+
out.push(name);
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A short, safe render of the sample payload a WARN names (bounded so a huge payload doesn't flood). */
|
|
155
|
+
function samplePayloadText(payload: Record<string, unknown>): string {
|
|
156
|
+
let s: string;
|
|
157
|
+
try {
|
|
158
|
+
s = JSON.stringify(payload);
|
|
159
|
+
} catch {
|
|
160
|
+
s = "<unserializable>";
|
|
161
|
+
}
|
|
162
|
+
return s.length > 200 ? `${s.slice(0, 197)}…` : s;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── TWIN A — the UNIVERSAL plan-return probe ──────────────────────────────────────
|
|
166
|
+
// The engine assembles a plan's output with `[...returned]` (wire_encode.ts). A plan whose
|
|
167
|
+
// `plan` returns a NON-ITERABLE (undefined — a forgotten `return`; a builder — a forgotten
|
|
168
|
+
// `return []` after `create(...).set(...)`; a plain object) throws "not iterable" at the FIRST
|
|
169
|
+
// dispatch on every peer. This is payload-INDEPENDENT (the return TYPE is fixed) ⇒ REFUSE.
|
|
170
|
+
// A plan that THREW on the sample is payload-DEPENDENT (can't prove) ⇒ WARN naming the sample.
|
|
171
|
+
function planReturnFindings(domain: string, dir: Directive<unknown>, probe: ProbeResult): Finding[] {
|
|
172
|
+
if (probe.threw) {
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
domain,
|
|
176
|
+
directiveId: dir.id,
|
|
177
|
+
level: "warn",
|
|
178
|
+
what: `the plan THREW on the synthesized sample (${(probe.error as Error)?.message ?? String(probe.error)}) — can't prove the return shape`,
|
|
179
|
+
fix: `run it on the sample ${samplePayloadText(probe.probePayload)} — if the throw is real, the runtime gate refuses; if it's a payload-dependent branch, ignore`,
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
if (!Array.isArray(probe.returned)) {
|
|
184
|
+
const got =
|
|
185
|
+
probe.returned === undefined
|
|
186
|
+
? "undefined (a missing `return`)"
|
|
187
|
+
: typeof probe.returned === "object" && probe.returned !== null
|
|
188
|
+
? "an object/builder (a forgotten `return []`)"
|
|
189
|
+
: `a ${typeof probe.returned}`;
|
|
190
|
+
return [
|
|
191
|
+
{
|
|
192
|
+
domain,
|
|
193
|
+
directiveId: dir.id,
|
|
194
|
+
level: "refuse",
|
|
195
|
+
what: `the plan returns ${got}, not an array of ops — the engine assembles plan output with [...returned] and would throw "not iterable" at the first dispatch`,
|
|
196
|
+
fix: `return an array of ops — the fluent surface (create/set/add) records ops as a side-effect, so end the plan with \`return [];\` (or return the typed ops directly)`,
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── TWIN B — the UNREAD-PAYLOAD-FIELD warn ────────────────────────────────────────
|
|
204
|
+
// A REQUIRED payload field the plan never reads on the sample path is usually a bug (dead field
|
|
205
|
+
// or missing logic). WARN only (a conditional read the sample didn't reach proves nothing), and
|
|
206
|
+
// only for a plan that ran cleanly (a throwing plan read nothing — twin A already flagged it).
|
|
207
|
+
// Framework-consumed convention fields (auto-stamped `…At`, birth subject/time) are excluded.
|
|
208
|
+
function unreadPayloadFindings(domain: string, dir: Directive<unknown>, probe: ProbeResult): Finding[] {
|
|
209
|
+
if (probe.threw) return [];
|
|
210
|
+
const stamped = new Set(autoStampFields(dir.payloadSchema as z.ZodTypeAny));
|
|
211
|
+
// The GATE reads some payload fields the PLAN never does: `.requires(rel, { objectFrom })` derives
|
|
212
|
+
// the gate object from a named payload id field (per-record authz). Excluding it keeps a real
|
|
213
|
+
// gate-consumed field from being flagged dead (the todo-pro `shareTodo.listId` case).
|
|
214
|
+
const gateConsumed = new Set<string>();
|
|
215
|
+
const objectFromField = (dir as { requiresRelationGate?: { objectFrom?: { idField?: string } } })
|
|
216
|
+
.requiresRelationGate?.objectFrom?.idField;
|
|
217
|
+
if (typeof objectFromField === "string") gateConsumed.add(objectFromField);
|
|
218
|
+
const out: Finding[] = [];
|
|
219
|
+
for (const f of requiredPayloadFields(dir.payloadSchema as z.ZodTypeAny)) {
|
|
220
|
+
if (probe.readFields.has(f)) continue;
|
|
221
|
+
if (stamped.has(f)) continue; // the client stamps it; the plan needn't read it
|
|
222
|
+
if (FRAMEWORK_CONSUMED_PAYLOAD_FIELDS.has(f)) continue; // the meter reads it off the intent
|
|
223
|
+
if (gateConsumed.has(f)) continue; // the authz gate derives its object from it
|
|
224
|
+
out.push({
|
|
225
|
+
domain,
|
|
226
|
+
directiveId: dir.id,
|
|
227
|
+
level: "warn",
|
|
228
|
+
what: `payload field '${f}' is never read by the plan (on the sampled path) — dead field or missing logic?`,
|
|
229
|
+
fix: `read it in the plan (e.g. \`.set("${f}", p.${f})\`), make it \`.optional()\` if it is genuinely optional, or drop it from the payload`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── TWIN C — the OP-VS-SCHEMA / driver twin ───────────────────────────────────────
|
|
236
|
+
// Walk the probed ops against the target aggregate's schema. An op on a field the aggregate
|
|
237
|
+
// does NOT declare is a hard mismatch (REFUSE, naming the real fields). A `set` on an additive
|
|
238
|
+
// set (AddWins/RemoveWins) OVERWRITES the CRDT instead of merging — REFUSE naming addToSet/
|
|
239
|
+
// removeFromSet. A `setEntry` on a non-map field is likewise a driver mismatch. Conservative:
|
|
240
|
+
// only ops targeting an aggregate THIS domain declares are judged (a foreign/framework tuple is
|
|
241
|
+
// left to the runtime gate); every refusal here is a combination the engine provably rejects.
|
|
242
|
+
function opVsSchemaFindings(
|
|
243
|
+
domain: string,
|
|
244
|
+
dir: Directive<unknown>,
|
|
245
|
+
probe: ProbeResult,
|
|
246
|
+
aggById: ReadonlyMap<string, AggregateHandle>,
|
|
247
|
+
): Finding[] {
|
|
248
|
+
if (probe.threw) return [];
|
|
249
|
+
const out: Finding[] = [];
|
|
250
|
+
const seen = new Set<string>(); // dedupe identical (agg, field, kind) findings
|
|
251
|
+
for (const op of probe.ops) {
|
|
252
|
+
const aggId = op.aggregateType;
|
|
253
|
+
if (typeof aggId !== "string") continue;
|
|
254
|
+
const agg = aggById.get(aggId);
|
|
255
|
+
if (agg === undefined) continue; // foreign / framework aggregate — the runtime gate arbitrates
|
|
256
|
+
const key = `${aggId}.${op.field}.${op.kind}`;
|
|
257
|
+
if (seen.has(key)) continue;
|
|
258
|
+
seen.add(key);
|
|
259
|
+
const field = agg.fields[op.field] as Field | undefined;
|
|
260
|
+
if (field === undefined) {
|
|
261
|
+
const real = Object.keys(agg.fields).sort();
|
|
262
|
+
out.push({
|
|
263
|
+
domain,
|
|
264
|
+
directiveId: dir.id,
|
|
265
|
+
level: "refuse",
|
|
266
|
+
what: `the plan writes '${op.field}' on '${aggId}', which is not a declared field of that aggregate`,
|
|
267
|
+
fix: `write one of ${aggId}'s fields — ${real.length ? real.join(", ") : "(none)"} — or add '${op.field}' to the aggregate`,
|
|
268
|
+
});
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const driverKind = (field.driver as { kind?: string }).kind;
|
|
272
|
+
if (op.kind === "set" && driverKind === "AddWins") {
|
|
273
|
+
out.push({
|
|
274
|
+
domain,
|
|
275
|
+
directiveId: dir.id,
|
|
276
|
+
level: "refuse",
|
|
277
|
+
what: `the plan \`set\`s '${op.field}' on '${aggId}', an AddWins set — set() OVERWRITES the merge (concurrent adds would be lost)`,
|
|
278
|
+
fix: `use \`addToSet(instance(${aggId}, id), "${op.field}", values)\` — the additive write that commutes`,
|
|
279
|
+
});
|
|
280
|
+
} else if (op.kind === "set" && driverKind === "RemoveWins") {
|
|
281
|
+
out.push({
|
|
282
|
+
domain,
|
|
283
|
+
directiveId: dir.id,
|
|
284
|
+
level: "refuse",
|
|
285
|
+
what: `the plan \`set\`s '${op.field}' on '${aggId}', a RemoveWins set — set() OVERWRITES the 2P-set CRDT`,
|
|
286
|
+
fix: `use \`addToSet(...)\` / \`removeFromSet(instance(${aggId}, id), "${op.field}", values)\` — the additive/tombstone writes`,
|
|
287
|
+
});
|
|
288
|
+
} else if (op.kind === "setEntry" && field.kind !== "map") {
|
|
289
|
+
out.push({
|
|
290
|
+
domain,
|
|
291
|
+
directiveId: dir.id,
|
|
292
|
+
level: "refuse",
|
|
293
|
+
what: `the plan \`setEntry\`s '${op.field}' on '${aggId}', a '${field.kind}' field — setEntry targets a map (MapOf) field only`,
|
|
294
|
+
fix: `write a t.map(...) field with setEntry, or use \`set(...)\` for the '${field.kind}' field '${op.field}'`,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Run every author-time twin over the composed domain modules and return the findings, ordered
|
|
303
|
+
* refuse → warn → note (stable within a level, by domain/directive appearance). This is the ONE
|
|
304
|
+
* entry `githolon check` and `compile`'s summary both call.
|
|
305
|
+
*/
|
|
306
|
+
export function checkDomainModules(modules: readonly DomainModule[]): Finding[] {
|
|
307
|
+
const findings: Finding[] = [];
|
|
308
|
+
|
|
309
|
+
for (const mod of modules) {
|
|
310
|
+
const domain = mod.domain ?? mod.name;
|
|
311
|
+
const aggById = new Map<string, AggregateHandle>();
|
|
312
|
+
for (const a of mod.aggregates) aggById.set(a.id, a);
|
|
313
|
+
|
|
314
|
+
// The NOTE twin (unify): the auto-declared secondary ensures composeDomainModule injected.
|
|
315
|
+
for (const ty of mod.autoDeclaredEnsures ?? []) {
|
|
316
|
+
findings.push({
|
|
317
|
+
domain,
|
|
318
|
+
level: "note",
|
|
319
|
+
what: `auto-declared Ensure→${ty} from a plan's secondary ensure(...) — the id-mint gate was opened for it`,
|
|
320
|
+
fix: `no action needed; to make it explicit, export a directive that \`.ensures(${ty})\``,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const dir of mod.directives as Directive<unknown>[]) {
|
|
325
|
+
const where = `${domain}/${dir.id}`;
|
|
326
|
+
const probe = probeDirective(dir, where);
|
|
327
|
+
if (probe === undefined) continue; // unsynthesizable payload — proven by hand, not by check
|
|
328
|
+
findings.push(...planReturnFindings(domain, dir, probe));
|
|
329
|
+
findings.push(...unreadPayloadFindings(domain, dir, probe));
|
|
330
|
+
findings.push(...opVsSchemaFindings(domain, dir, probe, aggById));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// The BIRTH-RECIPE twin (unify): fold the scattered `.births()` recipe diagnostics into the
|
|
335
|
+
// one report as WARNs (a discoverable-but-suspect recipe — missing seed, uncomposed child domain).
|
|
336
|
+
try {
|
|
337
|
+
const idx = proofLegsIndex(modules as DomainModule[]);
|
|
338
|
+
if (idx !== null) {
|
|
339
|
+
for (const [domain, legs] of Object.entries(idx.domains)) {
|
|
340
|
+
for (const diag of legs.birthDiagnostics ?? []) {
|
|
341
|
+
findings.push({
|
|
342
|
+
domain,
|
|
343
|
+
level: "warn",
|
|
344
|
+
what: `birth recipe: ${diag}`,
|
|
345
|
+
fix: `see architecture/g3_kernel_fulfils_birth.md / the births DX — the recipe must install both framework + child law and seed genesis`,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
/* birth discovery is best-effort — its absence never blocks the check */
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const rank = { refuse: 0, warn: 1, note: 2 } as const;
|
|
355
|
+
return findings
|
|
356
|
+
.map((f, i) => ({ f, i }))
|
|
357
|
+
.sort((a, b) => rank[a.f.level] - rank[b.f.level] || a.i - b.i)
|
|
358
|
+
.map(({ f }) => f);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Does the finding set carry a hard refusal (⇒ `githolon check` exits non-zero, `compile` refuses)? */
|
|
362
|
+
export function hasRefusal(findings: readonly Finding[]): boolean {
|
|
363
|
+
return findings.some((f) => f.level === "refuse");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Format one finding as the unified report line: `<domain>/<directive>: <LEVEL> — <what> — fix: <fix>`. */
|
|
367
|
+
export function formatFinding(f: Finding): string {
|
|
368
|
+
const subject = f.directiveId !== undefined ? `${f.domain}/${f.directiveId}` : f.domain;
|
|
369
|
+
return `${subject}: ${f.level.toUpperCase()} — ${f.what} — fix: ${f.fix}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** The full `check` report body (one line per finding), ordered refuse → warn → note. */
|
|
373
|
+
export function formatFindings(findings: readonly Finding[]): string[] {
|
|
374
|
+
return findings.map(formatFinding);
|
|
375
|
+
}
|