yichus 0.2.0 → 0.3.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/README.md +45 -16
- package/dist/cli.js +1328 -259
- package/dist/cli.js.map +1 -1
- package/dist/studio-page/dom.js +22 -0
- package/dist/studio-page/label-fit.js +7 -0
- package/dist/studio-page/layer-diagram.js +458 -0
- package/dist/studio-page/layer-groups.js +129 -0
- package/dist/studio-page/source-link.js +17 -0
- package/dist/studio-page/studio-views.js +256 -0
- package/dist/studio-page/system-map-record.js +122 -8
- package/dist/studio-page/system-map.js +461 -85
- package/dist/studio-page/what-happens-record.js +217 -0
- package/dist/studio-page/what-happens.js +752 -0
- package/dist/studio-reference/deploy-platforms.md +79 -0
- package/dist/studio-reference/portable-programs.md +58 -0
- package/dist/studio-reference/porting.md +32 -0
- package/dist/studio-reference/refund-desk-one-round.bosatsu +196 -0
- package/dist/studio-reference/refund-desk.bosatsu +194 -0
- package/dist/studio-reference/refund-policy.bosatsu +97 -0
- package/dist/studio-reference/refund-test.bosatsu +75 -0
- package/dist/studio-reference/the-page.md +107 -0
- package/dist/studio-reference/worker-authority.md +159 -0
- package/package.json +2 -2
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// The what-happens record (schema what-happens-1, marks yichus-marks-3 plus
|
|
2
|
+
// the outcome leaf), built from the engine's own output. web/what-happens.js
|
|
3
|
+
// draws this record and nothing else; every element names the engine field
|
|
4
|
+
// it was copied or derived from (`from`). scripts/what-happens-examples.mjs
|
|
5
|
+
// records one per program.
|
|
6
|
+
//
|
|
7
|
+
// The record joins, by computed id:
|
|
8
|
+
// api_organize lens=structure (with requirements) scenarios[], claims
|
|
9
|
+
// api_access_check report.findings[]
|
|
10
|
+
// another what-happens record of the same program its red leaves (base)
|
|
11
|
+
// The author's words are copied as labels onto the ids they name and never
|
|
12
|
+
// decide anything: colour comes only from a claim's verdict or a check's
|
|
13
|
+
// witness, and structure only from the engine's trees.
|
|
14
|
+
|
|
15
|
+
import { MARKS_VERSION } from './system-map-record.js';
|
|
16
|
+
|
|
17
|
+
export const SCHEMA = 'what-happens-1';
|
|
18
|
+
|
|
19
|
+
export const VOCABULARY = {
|
|
20
|
+
version: MARKS_VERSION,
|
|
21
|
+
added: ['outcome'],
|
|
22
|
+
marks: {
|
|
23
|
+
entryHead: 'arrowhead where a request comes in; the rack above the tree has one per described entry, then one dashed line counting the entries no scenario describes, which opens to draw each of them struck <- entries[], ghosts[]',
|
|
24
|
+
guard: 'red diamond: the declared sign-in or role check, the first branch of every entry that has one; its failure stem ends in a refusal <- tree node with from.kind=guard',
|
|
25
|
+
fork: 'three prongs: a branch the code takes on the request, the caller, a value read from a table, or a call result; the arm each line takes is written where it leaves <- tree nodes',
|
|
26
|
+
station: 'ring on a line: an effect on that path, its verb and table beside it; filled = it writes <- arms[].effects, entry.effects',
|
|
27
|
+
modelRing: 'violet ring: the path reaches the model <- effects[resource=model]',
|
|
28
|
+
netRing: 'orange ring: the path reaches the network <- effects[resource=net]',
|
|
29
|
+
outcome: 'the end of a path: a cap with what the request gets back (a constructor, or a refusal with its status and code); red when a claim fails there or a check reports a witness that ends there, green when every obligation that failed there at the base is checked here and holds, hollow and dashed (unchecked) when a claim there is inconclusive or an obligation that failed at the base is no longer checked, grey otherwise <- leaves',
|
|
30
|
+
unchecked: 'a hollow dashed cap and a ? tick: the engine cannot say whether the claim holds on this path (a step it could not open, a table it could not name, an answer it could not decide), or what failed here at the base is not checked here; never counted as holding <- claims[verdict=inconclusive], leaves[].gaps, diff.unresolved',
|
|
31
|
+
struck: 'a struck ghost: something checked and found absent (an entry nobody described, a write a claim says a path must not make, "no base compared" when no earlier version was compared) <- ghosts[], claims holding with not',
|
|
32
|
+
bundle: 'a cable with a count: a subtree too large for this view; opening it draws that part with a breadcrumb <- derived from the tree',
|
|
33
|
+
compared: 'a line under the rack, always there: "compared with <base> · N fixed · M still failing", or "no base compared", struck <- diff',
|
|
34
|
+
hull: 'a shaded box on the line: a step that is not the handler\'s own work, folded (a wrapper\'s guard or short-circuit, the platform\'s declared check or the transaction it runs the plan in, work beside the wrapper, a wrapper\'s work after the handler); it keeps what the step answers instead of running the handler, one row per arm that skips the handler, read as a side stub (the arm, its struck writes, what the request gets, ticks), then "continue" (a transaction: "runs"), and its effects; opening it draws the step\'s own tree; drawn inline, the same hull is a light band behind the step and its exits, with its role and where it comes from above its test, so either way it reads as not the handler\'s own work <- tree nodes[concern.role], entries[].steps',
|
|
35
|
+
},
|
|
36
|
+
rule: 'every entry, branch, arm, effect and outcome the engine computes is drawn, a folded step\'s inside its box and an undescribed entry behind the rack\'s one line; the author\'s words appear only on ids the engine computed, in italic, with the code name beside them.',
|
|
37
|
+
concerns: 'where each branch and effect comes from in value flow (concern.layer, effects[].layer): core = the handler\'s own work, a function it hands its wrapper or its body when it has none; cross = the wrapper its route shape names, shared with other entries; beside = the handler\'s code outside the wrapper call; platform = the route\'s declared authority, or the transaction the platform runs a plan in (its arms that give up or cannot confirm skip the handler\'s work). Roles come from which arms reach the handler, never from names.',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const bare = id => String(id).split('::').pop();
|
|
41
|
+
const claimKey = (entry, c) => `${entry}\u0000${c.at}\u0000${c.parsed}`;
|
|
42
|
+
|
|
43
|
+
export function walk(tree, f, path = []) {
|
|
44
|
+
if (!tree) return;
|
|
45
|
+
f(tree, path);
|
|
46
|
+
if (tree.kind === 'node') for (const arm of tree.arms) { f(arm, path); walk(arm.next, f, [...path, arm]); }
|
|
47
|
+
}
|
|
48
|
+
export const leavesOf = tree => { const out = []; walk(tree, t => { if (t.kind === 'leaf') out.push(t); }); return out; };
|
|
49
|
+
// A leaf's effects are the entry's leading effects and those of each arm on
|
|
50
|
+
// its path; the record keeps them once, on the arms, and these rebuild them.
|
|
51
|
+
export function pathEffects(entry) {
|
|
52
|
+
const out = new Map();
|
|
53
|
+
const go = (t, acc) => {
|
|
54
|
+
if (!t) return;
|
|
55
|
+
if (t.kind === 'leaf') { out.set(t.id, acc); return; }
|
|
56
|
+
for (const a of t.arms) go(a.next, [...acc, ...a.effects]);
|
|
57
|
+
};
|
|
58
|
+
go(entry.tree, entry.effects || []);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
export const allEffects = entry => { const out = [...(entry.effects || [])]; walk(entry.tree, t => { if (!t.kind) out.push(...t.effects); }); return out; };
|
|
62
|
+
|
|
63
|
+
// The access check's findings that report a violation at an effect: the
|
|
64
|
+
// leaves whose path runs that effect, at that definition, on that table.
|
|
65
|
+
// A finding with a path condition (pathCondition.unlessHeld: the guards the
|
|
66
|
+
// write sits under no branch on) is witnessed only on the paths where none
|
|
67
|
+
// of those guards was tested and held: a path through a guard's True arm is
|
|
68
|
+
// the guarded case, even when the write ran before the test.
|
|
69
|
+
function heldOn(tree, guards) {
|
|
70
|
+
const out = new Set();
|
|
71
|
+
const go = (t, held) => {
|
|
72
|
+
if (!t) return;
|
|
73
|
+
if (t.kind === 'leaf') { if (held) out.add(t.id); return; }
|
|
74
|
+
const isGuard = t.from?.kind === 'call' && guards.includes(bare(t.from.callee));
|
|
75
|
+
for (const a of t.arms) go(a.next, held || (isGuard && a.arm === 'True'));
|
|
76
|
+
};
|
|
77
|
+
go(tree, false);
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
function witnessesOf(access, entries) {
|
|
81
|
+
if (!access) return [];
|
|
82
|
+
const findings = (access.report?.findings || []).filter(f => f.status === 'violated');
|
|
83
|
+
return findings.map((f, i) => {
|
|
84
|
+
const site = `${f.package}::${f.binding}`;
|
|
85
|
+
const guards = f.pathCondition?.unlessHeld || [];
|
|
86
|
+
const leaves = entries.flatMap(e => {
|
|
87
|
+
const held = heldOn(e.tree, guards);
|
|
88
|
+
return leavesOf(e.tree).filter(l => !held.has(l.id) && l.effects.some(x => x.site === site && bare(x.primitive) === f.operation && x.tables.includes(f.resource)));
|
|
89
|
+
}).map(l => l.id);
|
|
90
|
+
return { id: `witness:${i + 1}`, check: 'access', resource: f.resource, site, operation: f.operation, kind: f.kind, detail: f.detail, leaves,
|
|
91
|
+
...(guards.length ? { unlessHeld: guards } : {}),
|
|
92
|
+
from: guards.length ? 'api_access_check report.findings[status=violated], on paths where pathCondition.unlessHeld did not hold' : 'api_access_check report.findings[status=violated]' };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* engine: { structure, access? } structure = api_organize lens=structure
|
|
98
|
+
* output (with requirements), access = api_access_check output.
|
|
99
|
+
* base: the record of the program before a change, for the diff marks.
|
|
100
|
+
*/
|
|
101
|
+
export function buildWhatHappens(engine, { program, base = null, baseLabel = 'base' } = {}) {
|
|
102
|
+
for (const [k, v] of Object.entries(engine)) if (v && !v.ok) throw new Error(`${k}: ${v.error}`);
|
|
103
|
+
const st = engine.structure;
|
|
104
|
+
const claims = st.claims || { scenarios: [], undescribed: (st.scenarios || []).map(s => s.entry) };
|
|
105
|
+
const byEntry = new Map(claims.scenarios.filter(s => s.status === 'matched').map(s => [s.entry, s]));
|
|
106
|
+
const routes = new Map((st.entries || []).map(e => [e.entry, e]));
|
|
107
|
+
const entries = st.scenarios.map(s => {
|
|
108
|
+
const meta = routes.get(s.entry) || {};
|
|
109
|
+
const sc = byEntry.get(s.entry);
|
|
110
|
+
const names = new Map((sc?.names || []).filter(n => n.status === 'named').map(n => [n.id, n.words]));
|
|
111
|
+
// copy the tree, putting the author's words on the ids they name
|
|
112
|
+
const copy = t => {
|
|
113
|
+
if (!t) return null;
|
|
114
|
+
if (t.kind === 'leaf') return { ...t, words: names.get(t.id) || null, claims: [], witnesses: [], status: 'grey' };
|
|
115
|
+
return { ...t, words: names.get(t.id) || null, arms: t.arms.map(a => ({ ...a, words: names.get(a.id) || null, next: copy(a.next) })) };
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
entry: s.entry, handler: s.handler, label: s.entry.replace(/^route:/, ''),
|
|
119
|
+
authority: meta.authority || null, transport: meta.transport || null,
|
|
120
|
+
title: sc?.title || null, described: !!sc,
|
|
121
|
+
effects: s.effects, tree: copy(s.tree), leafCount: Number(s.leafCount), truncated: s.truncated, unsupported: s.unsupported || null,
|
|
122
|
+
wrapper: s.wrapper || null, steps: s.steps || [],
|
|
123
|
+
from: 'structure.scenarios[]',
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
const entryOf = new Map(entries.map(e => [e.entry, e]));
|
|
127
|
+
const leafById = new Map(entries.flatMap(e => leavesOf(e.tree).map(l => [l.id, l])));
|
|
128
|
+
|
|
129
|
+
// claims onto the leaves they are about
|
|
130
|
+
const checked = [], notes = [], refused = [], mismatches = [];
|
|
131
|
+
for (const sc of claims.scenarios) {
|
|
132
|
+
if (sc.status !== 'matched') { mismatches.push({ entry: sc.entry, title: sc.title || null, reason: sc.reason, from: 'structure.claims.scenarios[status=mismatch]' }); continue; }
|
|
133
|
+
for (const n of sc.names) if (n.status !== 'named') refused.push({ entry: sc.entry, id: n.id, words: n.words, reason: n.reason, from: 'structure.claims.scenarios[].names[status=refused]' });
|
|
134
|
+
for (const x of sc.expect) {
|
|
135
|
+
if (x.verdict === 'not-a-claim') { notes.push({ entry: sc.entry, at: x.at, text: x.claim, from: 'structure.claims.scenarios[].expect[verdict=not-a-claim]' }); continue; }
|
|
136
|
+
if (x.verdict === 'refused') { refused.push({ entry: sc.entry, id: x.at, words: x.claim, reason: x.reason, from: 'structure.claims.scenarios[].expect[verdict=refused]' }); continue; }
|
|
137
|
+
const open = new Map((x.inconclusive || []).map(o => [o.leaf, o.reasons]));
|
|
138
|
+
const c = { id: `claim:${checked.length + 1}`, entry: sc.entry, at: x.at, claim: x.claim, parsed: x.parsed, property: x.property, verdict: x.verdict, leaves: x.leaves, failing: x.failing,
|
|
139
|
+
inconclusive: x.inconclusive || [], from: 'structure.claims.scenarios[].expect[]' };
|
|
140
|
+
checked.push(c);
|
|
141
|
+
for (const id of x.leaves) {
|
|
142
|
+
const l = leafById.get(id);
|
|
143
|
+
if (!l) continue;
|
|
144
|
+
const verdict = x.failing.includes(id) ? 'fails' : open.has(id) ? 'inconclusive' : 'holds';
|
|
145
|
+
l.claims.push({ id: c.id, at: x.at, parsed: x.parsed, property: x.property, verdict, ...(verdict === 'inconclusive' ? { reasons: open.get(id) } : {}) });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const witnesses = witnessesOf(engine.access, entries);
|
|
150
|
+
// the base record keeps its leaves' effects on its arms
|
|
151
|
+
const basePaths = base ? new Map(base.entries.flatMap(e => [...pathEffects(e)])) : null;
|
|
152
|
+
for (const w of witnesses) for (const id of w.leaves) leafById.get(id)?.witnesses.push(w.id);
|
|
153
|
+
for (const l of leafById.values()) {
|
|
154
|
+
if (l.claims.some(c => c.verdict === 'fails') || l.witnesses.length) l.status = 'red';
|
|
155
|
+
else if (l.claims.some(c => c.verdict === 'inconclusive')) l.status = 'unchecked';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// the diff: a leaf red at the base is fixed only when every obligation
|
|
159
|
+
// that failed there (a claim, by entry, at and parsed claim; an access
|
|
160
|
+
// witness, by the check running here and naming no witness on the leaf)
|
|
161
|
+
// is checked here and holds; the base's red
|
|
162
|
+
// leaf travels with it, drawn faded beside it. An obligation that is gone
|
|
163
|
+
// or inconclusive here leaves the leaf unchecked, never green.
|
|
164
|
+
let diff = null;
|
|
165
|
+
if (base) {
|
|
166
|
+
const baseRed = new Map(base.entries.flatMap(e => leavesOf(e.tree).filter(l => l.status === 'red').map(l => [l.id, { entry: e.entry, leaf: l }])));
|
|
167
|
+
const baseWitness = new Map((base.witnesses || []).map(w => [w.id, w]));
|
|
168
|
+
const fixed = [], still = [], gone = [], unresolved = [];
|
|
169
|
+
for (const [id, { entry, leaf: bl }] of baseRed) {
|
|
170
|
+
const here = leafById.get(id);
|
|
171
|
+
if (!here) { gone.push(id); continue; }
|
|
172
|
+
if (here.status === 'red') { still.push(id); continue; }
|
|
173
|
+
const reasons = [
|
|
174
|
+
...bl.claims.filter(c => c.verdict === 'fails').flatMap(c => {
|
|
175
|
+
const now = here.claims.find(h => claimKey(entry, h) === claimKey(entry, c));
|
|
176
|
+
if (!now) return [`the claim ${c.parsed} at ${c.at} failed here at the base and is not checked here`];
|
|
177
|
+
return now.verdict === 'holds' ? [] : [`the claim ${c.parsed} at ${c.at} failed here at the base and is inconclusive here`];
|
|
178
|
+
}),
|
|
179
|
+
...(bl.witnesses.length && !engine.access ? [`the access check reported a witness here at the base and did not run here`] : []),
|
|
180
|
+
];
|
|
181
|
+
const baseClaims = bl.claims, baseWitnesses = bl.witnesses.map(w => baseWitness.get(w)).filter(Boolean);
|
|
182
|
+
here.base = { outcome: bl.outcome, effects: bl.effects || basePaths.get(id) || [], claims: baseClaims, witnesses: baseWitnesses };
|
|
183
|
+
if (reasons.length) { here.status = 'unchecked'; unresolved.push({ id, reasons }); continue; }
|
|
184
|
+
here.status = 'green';
|
|
185
|
+
fixed.push(id);
|
|
186
|
+
}
|
|
187
|
+
diff = { base: baseLabel, fixed, stillRed: still, goneAtHead: gone, unresolved,
|
|
188
|
+
from: 'derived: base leaves[status=red] matched by id; each failed claim matched by entry, at and parsed claim; a witness is gone when the access check ran here and reports none on the leaf' };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const ghosts = entries.filter(e => !e.described).map(e => ({ entry: e.entry, from: 'structure.claims.undescribed[]' }));
|
|
192
|
+
// a leaf's effects are kept once, on the arms of its path (pathEffects)
|
|
193
|
+
for (const l of leafById.values()) delete l.effects;
|
|
194
|
+
return {
|
|
195
|
+
schema: SCHEMA, marks: MARKS_VERSION, vocabulary: VOCABULARY, program,
|
|
196
|
+
entries, ghosts, claims: checked, witnesses, notes, refused, mismatches, diff,
|
|
197
|
+
counts: { entries: entries.length, described: entries.length - ghosts.length, leaves: leafById.size,
|
|
198
|
+
steps: entries.reduce((n, e) => n + e.steps.length, 0), wrapped: entries.filter(e => e.wrapper && e.wrapper.wraps).length,
|
|
199
|
+
red: [...leafById.values()].filter(l => l.status === 'red').length, green: [...leafById.values()].filter(l => l.status === 'green').length,
|
|
200
|
+
unchecked: [...leafById.values()].filter(l => l.status === 'unchecked').length },
|
|
201
|
+
evidence: { tools: ['api_organize:structure', ...(engine.access ? ['api_access_check'] : [])], derived: ['diff: base red leaves matched by id, their failed obligations by identity'] },
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Small helpers the renderer and tests share.
|
|
206
|
+
export const isWriteOp = op => !['read', 'runs', 'effect', 'random', 'trace', 'capture', 'init', 'mint id'].includes(op) && op !== 'call model' && op !== 'fetch article';
|
|
207
|
+
export function stationLabel(e) {
|
|
208
|
+
const verb = e.op === 'mint id' ? 'mint' : e.op;
|
|
209
|
+
if (e.tables.length) return `${verb} · ${e.tables.join(', ')}`;
|
|
210
|
+
if (e.resource === 'model' || e.resource === 'net') return verb;
|
|
211
|
+
return `${verb} · ${bare(e.primitive)}`;
|
|
212
|
+
}
|
|
213
|
+
export function outcomeLabel(o) {
|
|
214
|
+
if (o.status && !o.constructors.length) return o.shown;
|
|
215
|
+
const head = o.constructors.length ? o.constructors.join(' | ') : (o.value ? `the ${o.value}` : o.shown);
|
|
216
|
+
return head;
|
|
217
|
+
}
|