@intentius/behold 0.17.0 → 0.18.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/AGENTS.md +517 -2
- package/LICENSE +190 -0
- package/README.md +22 -2
- package/THIRD_PARTY.md +75 -0
- package/demos.json +78 -15
- package/dist/cli.js +4585 -1467
- package/example-choudoufu-estate/.behold.json +8 -0
- package/example-choudoufu-estate/README.md +48 -0
- package/example-choudoufu-estate/monolith/behaviour.live.json +495 -0
- package/example-choudoufu-estate/monolith/carve.json +12 -0
- package/example-choudoufu-estate/monolith/main.tf +127 -0
- package/example-choudoufu-estate/scripts/choudoufu-down.sh +8 -0
- package/example-choudoufu-estate/scripts/choudoufu-up.sh +62 -0
- package/example-choudoufu-estate/team-a/behaviour.live.json +179 -0
- package/example-choudoufu-estate/team-a/main.tf +58 -0
- package/example-choudoufu-estate/team-b/behaviour.live.json +179 -0
- package/example-choudoufu-estate/team-b/main.tf +73 -0
- package/example-choudoufu-estate/team-c/behaviour.live.json +179 -0
- package/example-choudoufu-estate/team-c/main.tf +51 -0
- package/licenses/cncf-artwork-LICENSE.md +13 -0
- package/licenses/iTerm2-Color-Schemes-LICENSE +31 -0
- package/licenses/kubernetes-icons-LICENSE +201 -0
- package/package.json +24 -7
- package/web/app.js +929 -20
- package/web/behaviour-scale.js +380 -0
- package/web/behaviour-scale.test.js +307 -0
- package/web/cross-env.js +58 -0
- package/web/cross-env.test.js +61 -0
- package/web/index.html +39 -1
package/web/cross-env.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// #234's last remainder — the cross-environment half of "is prod running what
|
|
2
|
+
// staging tested", the pure half (tested the way web/release.js and
|
|
3
|
+
// web/run-playhead.js are — no DOM). The payload is /api/components/compare's
|
|
4
|
+
// `{env, to, comparisons}`, each comparison chant's own CrossEnvComparison:
|
|
5
|
+
// `same` decided on the COMPARABLE identity (inputDigest ?? digest per side —
|
|
6
|
+
// a pinned helm deploy's bytes legitimately differ per cluster), `comparedOn`
|
|
7
|
+
// present when the input side spoke, digests always visible.
|
|
8
|
+
|
|
9
|
+
/** Tone vocabulary matches the component palette: good / warn / neutral. */
|
|
10
|
+
export function comparisonRow(c) {
|
|
11
|
+
const missingA = !c.digestA;
|
|
12
|
+
const missingB = !c.digestB;
|
|
13
|
+
if (missingA && missingB) {
|
|
14
|
+
return { component: c.component, tone: "neutral", verdict: "unrecorded", detail: `no release recorded in ${c.envA} or ${c.envB}` };
|
|
15
|
+
}
|
|
16
|
+
if (missingA || missingB) {
|
|
17
|
+
const where = missingA ? c.envA : c.envB;
|
|
18
|
+
return { component: c.component, tone: "neutral", verdict: "unrecorded", detail: `no release recorded in ${where}` };
|
|
19
|
+
}
|
|
20
|
+
if (c.same) {
|
|
21
|
+
// The honest wording: matching inputs with differing bytes is still
|
|
22
|
+
// "same" — that is chant#1243's contract, and saying "same bytes" here
|
|
23
|
+
// would claim more than the ledger does.
|
|
24
|
+
const inputDecided = !!c.comparedOn && c.comparedOn.a === c.comparedOn.b && c.digestA !== c.digestB;
|
|
25
|
+
return {
|
|
26
|
+
component: c.component,
|
|
27
|
+
tone: "good",
|
|
28
|
+
verdict: inputDecided ? "same inputs" : "same",
|
|
29
|
+
detail: inputDecided
|
|
30
|
+
? `built from the same inputs (${short(c.comparedOn.a)}); rendered bytes differ per cluster, as pinned deploys do`
|
|
31
|
+
: `same digest in both (${short(c.digestA)})`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
component: c.component,
|
|
36
|
+
tone: "warn",
|
|
37
|
+
verdict: "differs",
|
|
38
|
+
detail: `${c.envA}: ${short(c.digestA)} · ${c.envB}: ${short(c.digestB)}`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** One line for the section header: the counts, worst-tone-first. */
|
|
43
|
+
export function comparisonSummary(payload) {
|
|
44
|
+
const rows = (payload.comparisons || []).map(comparisonRow);
|
|
45
|
+
if (!rows.length) return { text: `no components recorded in ${payload.env} or ${payload.to}`, tone: "neutral" };
|
|
46
|
+
const differs = rows.filter((r) => r.tone === "warn").length;
|
|
47
|
+
const unrecorded = rows.filter((r) => r.tone === "neutral").length;
|
|
48
|
+
const same = rows.length - differs - unrecorded;
|
|
49
|
+
if (differs) return { text: `${differs} of ${rows.length} differ from ${payload.to}`, tone: "warn" };
|
|
50
|
+
if (same === rows.length) return { text: `all ${rows.length} running what ${payload.to} tested`, tone: "good" };
|
|
51
|
+
return { text: `${same} same as ${payload.to}, ${unrecorded} unrecorded`, tone: "neutral" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function short(digest) {
|
|
55
|
+
if (!digest) return "—";
|
|
56
|
+
const hex = digest.startsWith("sha256:") ? digest.slice(7) : digest;
|
|
57
|
+
return hex.slice(0, 12);
|
|
58
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { comparisonRow, comparisonSummary } from "./cross-env.js";
|
|
3
|
+
|
|
4
|
+
const cmp = (over) => ({ component: "web", envA: "prod", envB: "staging", digestA: "sha256:aaaa1111bbbb2222", digestB: "sha256:aaaa1111bbbb2222", same: true, ...over });
|
|
5
|
+
|
|
6
|
+
describe("comparisonRow (#234 — is prod running what staging tested)", () => {
|
|
7
|
+
it("same digest in both reads good, with the digest named", () => {
|
|
8
|
+
const r = comparisonRow(cmp({}));
|
|
9
|
+
expect(r).toMatchObject({ tone: "good", verdict: "same" });
|
|
10
|
+
expect(r.detail).toContain("aaaa1111bbbb");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("matching inputs with differing bytes says SAME INPUTS — never claims same bytes (pinned helm, chant#1243)", () => {
|
|
14
|
+
const r = comparisonRow(cmp({
|
|
15
|
+
digestA: "sha256:prod-bytes",
|
|
16
|
+
digestB: "sha256:staging-bytes",
|
|
17
|
+
same: true,
|
|
18
|
+
comparedOn: { a: "sha256:inputs", b: "sha256:inputs" },
|
|
19
|
+
}));
|
|
20
|
+
expect(r.tone).toBe("good");
|
|
21
|
+
expect(r.verdict).toBe("same inputs");
|
|
22
|
+
expect(r.detail).toContain("rendered bytes differ per cluster");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("differing identities read warn, both digests visible", () => {
|
|
26
|
+
const r = comparisonRow(cmp({ digestB: "sha256:other", same: false }));
|
|
27
|
+
expect(r).toMatchObject({ tone: "warn", verdict: "differs" });
|
|
28
|
+
expect(r.detail).toContain("prod");
|
|
29
|
+
expect(r.detail).toContain("staging");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("a missing side is UNRECORDED (neutral) and names where — never painted as drift", () => {
|
|
33
|
+
const r = comparisonRow(cmp({ digestB: undefined, same: false }));
|
|
34
|
+
expect(r).toMatchObject({ tone: "neutral", verdict: "unrecorded" });
|
|
35
|
+
expect(r.detail).toContain("staging");
|
|
36
|
+
expect(comparisonRow(cmp({ digestA: undefined, digestB: undefined, same: false })).detail).toContain("prod or staging");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("comparisonSummary", () => {
|
|
41
|
+
const payload = (comparisons) => ({ env: "prod", to: "staging", comparisons });
|
|
42
|
+
|
|
43
|
+
it("worst tone first: any differ makes the line warn with the count", () => {
|
|
44
|
+
const s = comparisonSummary(payload([cmp({}), cmp({ component: "api", digestB: "sha256:x", same: false })]));
|
|
45
|
+
expect(s).toEqual({ text: "1 of 2 differ from staging", tone: "warn" });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("all same is the good line the question wants answered", () => {
|
|
49
|
+
const s = comparisonSummary(payload([cmp({}), cmp({ component: "api" })]));
|
|
50
|
+
expect(s).toEqual({ text: "all 2 running what staging tested", tone: "good" });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("unrecorded components temper the claim instead of hiding in either count", () => {
|
|
54
|
+
const s = comparisonSummary(payload([cmp({}), cmp({ component: "api", digestB: undefined, same: false })]));
|
|
55
|
+
expect(s).toEqual({ text: "1 same as staging, 1 unrecorded", tone: "neutral" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("an empty ledger pair says so", () => {
|
|
59
|
+
expect(comparisonSummary(payload([]))).toEqual({ text: "no components recorded in prod or staging", tone: "neutral" });
|
|
60
|
+
});
|
|
61
|
+
});
|
package/web/index.html
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>behold</title>
|
|
7
|
+
<!-- #393: "nothing in the browser console" includes the 404 every page
|
|
8
|
+
load has always fetched for itself. behold ships no icon file and its
|
|
9
|
+
static export copies only web/, so the mark is a data URI rather than a
|
|
10
|
+
route or an asset: no request, and the exported page keeps it. -->
|
|
11
|
+
<link
|
|
12
|
+
rel="icon"
|
|
13
|
+
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6' fill='none' stroke='%23a6adc8' stroke-width='1.6'/%3E%3Ccircle cx='8' cy='8' r='2.2' fill='%23a6adc8'/%3E%3C/svg%3E"
|
|
14
|
+
/>
|
|
7
15
|
<style>
|
|
8
16
|
/* Pre-paint fallback (#229). These are the DEFAULT theme's tokens —
|
|
9
17
|
Catppuccin Mocha run through theme.js's tokensFor() — not a palette of
|
|
@@ -313,6 +321,25 @@
|
|
|
313
321
|
#panel .grow { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
314
322
|
#panel .tag { color: var(--muted); font: var(--t-caption)/1.5 var(--font-mono); white-space: nowrap; }
|
|
315
323
|
|
|
324
|
+
/* The provenance badge (#401, M4 of #397): who stated this figure, to
|
|
325
|
+
what tolerance, and whether it is modeled from list prices or
|
|
326
|
+
validated against a bill. Boxed rather than plain muted text, because
|
|
327
|
+
it rides BESIDE a number and has to read as a label on that number
|
|
328
|
+
rather than as more of it — and it sits on every figure the behaviour
|
|
329
|
+
overlay shows, in the panel and in the inspect pane alike, so it is
|
|
330
|
+
styled for both. `help` cursor: the hover carries "modeled, not
|
|
331
|
+
billed", which is the sentence #397 turns on. */
|
|
332
|
+
.behaviour-badge { color: var(--muted); font: var(--t-caption)/1.4 var(--font-mono);
|
|
333
|
+
border: 1px solid var(--line); border-radius: var(--r-ctl); padding: 0 5px;
|
|
334
|
+
white-space: nowrap; cursor: help; flex: none; }
|
|
335
|
+
#panel .behaviour-total { display: flex; flex-direction: column; gap: 1px; padding: 3px 0;
|
|
336
|
+
font: var(--t-caption)/1.5 var(--font-mono); }
|
|
337
|
+
#panel .behaviour-ramp { margin: 4px 0 2px; }
|
|
338
|
+
/* A card nothing priced keeps the drift overlay's neutral fill (#399) —
|
|
339
|
+
and says so, faintly, so the grey is legible as "not looked at" rather
|
|
340
|
+
than as the low end of the scale. */
|
|
341
|
+
#graph [data-unpriced="1"] { opacity: 0.82; }
|
|
342
|
+
|
|
316
343
|
/* Substrate readiness (M5, #54) — one row per substrate on the panel's
|
|
317
344
|
Substrates tab: status dot + name + state, with its actions (bring up /
|
|
318
345
|
reset / run pipeline) visible inline again. #73 moved those into ⌘K;
|
|
@@ -424,6 +451,13 @@
|
|
|
424
451
|
here", not "something is wrong". */
|
|
425
452
|
#graph [data-node-id].operator-home rect:first-of-type { stroke: var(--pending); stroke-width: 2; }
|
|
426
453
|
|
|
454
|
+
/* #404: a bound choudoufu card the plan would change in place. A DASHED
|
|
455
|
+
--degraded edge, deliberately not a fill: the fill is the ownership
|
|
456
|
+
verdict and attribute drift never moves it, so the two signals have to
|
|
457
|
+
live in different channels. Dashed rather than solid so it reads apart
|
|
458
|
+
from .carved's and .operator-home's solid strokes at a glance. */
|
|
459
|
+
#graph [data-node-id].plan-drifted rect:first-of-type { stroke: var(--degraded); stroke-width: 2; stroke-dasharray: 4 3; }
|
|
460
|
+
|
|
427
461
|
/* The step the playhead sits on, on the graph itself — the ops lens marks
|
|
428
462
|
it `_playhead` and the card wears a dashed edge, so "here" reads
|
|
429
463
|
differently from "this one is done". */
|
|
@@ -515,6 +549,10 @@
|
|
|
515
549
|
font: var(--t-body)/1.5 var(--font-mono);
|
|
516
550
|
cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
517
551
|
#pal-list .row.sel { background: var(--pending); color: var(--pal-sel-fg, var(--fg)); }
|
|
552
|
+
/* A node row's second line (#393): the member and the kind, which is what
|
|
553
|
+
tells two same-named addresses in two members apart. Inherits the row's
|
|
554
|
+
colour so the selected row stays readable on --pending. */
|
|
555
|
+
#pal-list .row .sub { font-size: var(--t-caption); opacity: 0.66; margin-top: 1px; }
|
|
518
556
|
#pal-list .empty { padding: 8px 11px; color: var(--muted); font-size: var(--t-body); }
|
|
519
557
|
</style>
|
|
520
558
|
</head>
|
|
@@ -577,7 +615,7 @@
|
|
|
577
615
|
<!-- Command palette (⌘K / Ctrl+K, #73) — the spicypath FG-036 shape
|
|
578
616
|
(../spicypath/src/index.html): input + filtered list, driven entirely
|
|
579
617
|
by app.js's paletteCommands()/palRender()/openPalette()/closePalette(). -->
|
|
580
|
-
<div id="palette"><div class="box"><input id="pal-input" placeholder="type a command… (Esc to close)" autocomplete="off"><div id="pal-list"></div></div></div>
|
|
618
|
+
<div id="palette"><div class="box"><input id="pal-input" placeholder="type a command, or an address… (Esc to close)" autocomplete="off"><div id="pal-list"></div></div></div>
|
|
581
619
|
<script type="module" src="./app.js"></script>
|
|
582
620
|
</body>
|
|
583
621
|
</html>
|