@bongos/core 1.19.966 → 1.19.968
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/.bongos-core.json +16 -11
- package/docs/module-api-changelog.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/liveness_notify.mjs +204 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.968",
|
|
6
|
+
"core_contract": "1.19.968",
|
|
7
|
+
"source_commit": "8ccf04d976e2b856bc48cbfc6ad7d6563522bc51",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-22T23:25:06.335Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 515,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2365,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2906,
|
|
20
|
+
"tree_sha256": "4ccb083b8cb616dd5c4c9c49f498b566996a1ead6ea244f46e5344d7a726a322",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2697,7 +2697,7 @@
|
|
|
2697
2697
|
{
|
|
2698
2698
|
"path": "docs/module-api-changelog.md",
|
|
2699
2699
|
"mode": "0000644",
|
|
2700
|
-
"sha256": "
|
|
2700
|
+
"sha256": "aa4b0d91bfda4df6b776aa7ea600750b3f24be1e0ffee255ceb170184df90636"
|
|
2701
2701
|
},
|
|
2702
2702
|
{
|
|
2703
2703
|
"path": "docs/modules-contract.md",
|
|
@@ -8102,12 +8102,12 @@
|
|
|
8102
8102
|
{
|
|
8103
8103
|
"path": "package-lock.json",
|
|
8104
8104
|
"mode": "0000644",
|
|
8105
|
-
"sha256": "
|
|
8105
|
+
"sha256": "a4f60be63bc56da8ba3332e604d1fbb0b4b3288f9b3cf14a52d05227e040a3f4"
|
|
8106
8106
|
},
|
|
8107
8107
|
{
|
|
8108
8108
|
"path": "package.json",
|
|
8109
8109
|
"mode": "0000644",
|
|
8110
|
-
"sha256": "
|
|
8110
|
+
"sha256": "4d9fb5822b9ba10648ba9d9e0c2bd7c5659215e65bb8395455913b7b14c43998"
|
|
8111
8111
|
},
|
|
8112
8112
|
{
|
|
8113
8113
|
"path": "public-docs/index.html",
|
|
@@ -10112,7 +10112,7 @@
|
|
|
10112
10112
|
{
|
|
10113
10113
|
"path": "src/module-api.js",
|
|
10114
10114
|
"mode": "0000644",
|
|
10115
|
-
"sha256": "
|
|
10115
|
+
"sha256": "4f5478f0a18dbb167df7e86861ae757cceaf084a43436461af1fca4855256505"
|
|
10116
10116
|
},
|
|
10117
10117
|
{
|
|
10118
10118
|
"path": "src/module-loader/catalog.js",
|
|
@@ -12529,6 +12529,11 @@
|
|
|
12529
12529
|
"mode": "0000644",
|
|
12530
12530
|
"sha256": "cf0d082fad9bd7fdb3071016fac252fd59e5d24cec8280aab72c760c416d1482"
|
|
12531
12531
|
},
|
|
12532
|
+
{
|
|
12533
|
+
"path": "tests/liveness_notify.mjs",
|
|
12534
|
+
"mode": "0000644",
|
|
12535
|
+
"sha256": "8bd62820c232cae7346129c9d54aa9b56e3e50ed0d7488b238993c78c004c55e"
|
|
12536
|
+
},
|
|
12532
12537
|
{
|
|
12533
12538
|
"path": "tests/llm-cache-route.mjs",
|
|
12534
12539
|
"mode": "0000644",
|
|
@@ -2406,5 +2406,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2406
2406
|
landed since 1.19.964 with no explicit bump. run 35790478196. (task 1002620)
|
|
2407
2407
|
1.19.966 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2408
2408
|
landed since 1.19.965 with no explicit bump. run 35793400408. (task 1002620)
|
|
2409
|
+
1.19.967 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2410
|
+
landed since 1.19.966 with no explicit bump. run 35795807836. (task 1002620)
|
|
2411
|
+
1.19.968 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2412
|
+
landed since 1.19.967 with no explicit bump. run 35797263467. (task 1002620)
|
|
2409
2413
|
---------------------------------------------------------------------------
|
|
2410
2414
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.968",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.968",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.968",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.968'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// tests/liveness_notify.mjs
|
|
2
|
+
//
|
|
3
|
+
// The three rules that keep the liveness heartbeat from becoming a siren
|
|
4
|
+
// (task 1004151; the rules themselves are task 1002894's, from idea 1000703).
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS FILE EXISTS AT ALL. `modules/provisioning/pollers/liveness-notify.js` decides
|
|
7
|
+
// what reaches the owner's Discord when a project stops answering, and until now none of
|
|
8
|
+
// its rules had a test. Its CLAUDE.md row cited `tests/liveness-notify.mjs` as its proof
|
|
9
|
+
// and **that file had never existed** — `git log --all --diff-filter=A` for it is empty,
|
|
10
|
+
// and nothing under `tests/` named `DOWN_CONFIRM`, `FLEET_GUARD` or `noticeText`. Found
|
|
11
|
+
// while building the sibling poller under task 1004000.
|
|
12
|
+
//
|
|
13
|
+
// THE REASON IT MATTERS MORE THAN AN ORDINARY COVERAGE GAP: all three rules fail
|
|
14
|
+
// SILENTLY, and toward noise or silence rather than error. A broken edge check
|
|
15
|
+
// re-announces every outage on every control-plane restart. A broken confirm streak turns
|
|
16
|
+
// one refused connection into an owner-facing alarm. A broken collapse turns a
|
|
17
|
+
// prober-side blip into one message per project. Each of those reaches a human's Discord
|
|
18
|
+
// and none of them throws, so nothing but an assertion can catch them.
|
|
19
|
+
//
|
|
20
|
+
// EVERY CASE HERE WAS CHECKED BY DELETING THE RULE AND WATCHING IT GO RED — a guard is
|
|
21
|
+
// not finished until its test has been seen to fail. The mutation each case survives is
|
|
22
|
+
// named in its own comment, so a later reader can re-run the experiment instead of
|
|
23
|
+
// trusting this sentence.
|
|
24
|
+
//
|
|
25
|
+
// `planNotices` is pure over (observations, state) and mutates only the Map it is handed,
|
|
26
|
+
// so none of this needs a sweep, a database or a network.
|
|
27
|
+
//
|
|
28
|
+
// Run: node tests/liveness_notify.mjs
|
|
29
|
+
|
|
30
|
+
import { strict as assert } from 'node:assert';
|
|
31
|
+
import { createRequire } from 'node:module';
|
|
32
|
+
|
|
33
|
+
const require = createRequire(import.meta.url);
|
|
34
|
+
const { planNotices, noticeText, DOWN_CONFIRM, FLEET_GUARD } = require('../modules/provisioning/pollers/liveness-notify.js');
|
|
35
|
+
|
|
36
|
+
let passed = 0, failed = 0;
|
|
37
|
+
function t(name, fn) {
|
|
38
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
39
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// An observation as the sweep builds it: { id, slug, isPublic, up, prevUp }. `prevUp` is
|
|
43
|
+
// what recordLivenessProbe returned as the row's PRIOR verdict — the only place a fall is
|
|
44
|
+
// visible, which is what the edge rule is built on.
|
|
45
|
+
const obs = (over = {}) => ({ id: 1, slug: 'lantern', isPublic: true, up: false, prevUp: true, ...over });
|
|
46
|
+
|
|
47
|
+
// Drive N consecutive identical sweeps over one state Map and collect every notice.
|
|
48
|
+
function ticks(n, observations, state = new Map(), opts) {
|
|
49
|
+
const all = [];
|
|
50
|
+
for (let i = 0; i < n; i++) all.push(...planNotices(observations, state, opts).notices);
|
|
51
|
+
return { notices: all, state };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log('rule 1 — EDGE, not state:');
|
|
55
|
+
|
|
56
|
+
t('a project already down when the process started is NEVER announced, however long it stays down', () => {
|
|
57
|
+
// The rule: a fall is visible only in `prevUp`, so a row that was already down before
|
|
58
|
+
// this process began watching has no edge to see. Without it, every control-plane
|
|
59
|
+
// restart re-announces every ongoing outage — the exact siren this is built to avoid.
|
|
60
|
+
//
|
|
61
|
+
// MUTATION CHECKED: deleting the `else continue` arm (so an absent prev starts a streak
|
|
62
|
+
// of 1) makes this case fail on the second tick.
|
|
63
|
+
const { notices, state } = ticks(5, [obs({ prevUp: false })]);
|
|
64
|
+
assert.deepEqual(notices, [], 'no notice, no matter how many sweeps see it down');
|
|
65
|
+
assert.equal(state.size, 0, 'and it is not even tracked — there is nothing to announce later');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
t('a null prevUp — a row never probed before — is also not an edge', () => {
|
|
69
|
+
// recordLivenessProbe returns null for a row with no prior verdict. That is "unknown",
|
|
70
|
+
// not "was up", and reading it as a fall would announce every brand-new project that
|
|
71
|
+
// happens to be mid-standup.
|
|
72
|
+
const { notices } = ticks(3, [obs({ prevUp: null })]);
|
|
73
|
+
assert.deepEqual(notices, []);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
t('a fall OBSERVED in this process is announced — the edge rule is not just "stay quiet"', () => {
|
|
77
|
+
// The control for the two cases above: the same project, with prevUp true, does reach a
|
|
78
|
+
// notice. Without this, "announces nothing ever" would pass the edge tests too.
|
|
79
|
+
const { notices } = ticks(DOWN_CONFIRM, [obs({ prevUp: true })]);
|
|
80
|
+
assert.equal(notices.length, 1);
|
|
81
|
+
assert.equal(notices[0].kind, 'down');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
console.log('\nrule 2 — CONFIRM before speaking:');
|
|
85
|
+
|
|
86
|
+
t('one down probe followed by a recovery says NOTHING — a blip costs nothing', () => {
|
|
87
|
+
// The rule: wait DOWN_CONFIRM consecutive sweeps. One refused connection is not an
|
|
88
|
+
// outage, and a project that heals in between was never announced, so nothing needs
|
|
89
|
+
// unsaying.
|
|
90
|
+
//
|
|
91
|
+
// MUTATION CHECKED: lowering the gate to `streak >= 1` makes this case fail — it posts
|
|
92
|
+
// a down, and then an all-clear for an outage nobody experienced.
|
|
93
|
+
const state = new Map();
|
|
94
|
+
const first = planNotices([obs({ up: false, prevUp: true })], state);
|
|
95
|
+
assert.deepEqual(first.notices, [], 'the first down is not yet news');
|
|
96
|
+
const healed = planNotices([obs({ up: true })], state);
|
|
97
|
+
assert.deepEqual(healed.notices, [], 'and the recovery is not news either, because the fall never was');
|
|
98
|
+
assert.equal(state.size, 0, 'the streak is dropped, so a later fall starts clean');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
t('two consecutive downs post EXACTLY ONE message, and staying down adds none', () => {
|
|
102
|
+
// MUTATION CHECKED: dropping the `notified` latch makes the third tick post again.
|
|
103
|
+
const { notices, state } = ticks(6, [obs()]);
|
|
104
|
+
assert.equal(notices.length, 1, `one message for one outage, got ${notices.length}`);
|
|
105
|
+
assert.equal(notices[0].kind, 'down');
|
|
106
|
+
assert.equal(state.get(1).notified, true, 'the latch is what keeps it quiet afterwards');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
t('DOWN_CONFIRM is honoured as configured, not hardcoded at two', () => {
|
|
110
|
+
// The threshold is an env knob; a test that pinned "2" would pass on a deployment that
|
|
111
|
+
// had raised it and was therefore silent for longer than anyone expected.
|
|
112
|
+
const state = new Map();
|
|
113
|
+
for (let i = 1; i < DOWN_CONFIRM; i++) {
|
|
114
|
+
assert.deepEqual(planNotices([obs()], state).notices, [], `silent at ${i} of ${DOWN_CONFIRM}`);
|
|
115
|
+
}
|
|
116
|
+
assert.equal(planNotices([obs()], state).notices.length, 1, `speaks at ${DOWN_CONFIRM}`);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
console.log('\nrecovery is scoped to what was actually announced:');
|
|
120
|
+
|
|
121
|
+
t('an all-clear fires ONLY where a down notice really went out', () => {
|
|
122
|
+
// MUTATION CHECKED: pushing to `ups` without the `prev.notified` guard makes the blip
|
|
123
|
+
// case above emit an unpaired "is answering again" for an outage never reported.
|
|
124
|
+
const state = new Map();
|
|
125
|
+
ticks(DOWN_CONFIRM, [obs()], state); // announced
|
|
126
|
+
const back = planNotices([obs({ up: true })], state);
|
|
127
|
+
assert.equal(back.notices.length, 1);
|
|
128
|
+
assert.equal(back.notices[0].kind, 'up');
|
|
129
|
+
assert.equal(state.size, 0, 'and the row is forgotten, so a later fall is a fresh edge');
|
|
130
|
+
assert.deepEqual(planNotices([obs({ up: true })], state).notices, [], 'the all-clear is not repeated');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
console.log('\nrule 3 — COLLAPSE a fan-out:');
|
|
134
|
+
|
|
135
|
+
t('FLEET_GUARD simultaneous falls become ONE message that names no project', () => {
|
|
136
|
+
// The rule: when several projects fall in the same sweep the cause is almost always on
|
|
137
|
+
// this side — a control-plane restart, a prober-side blip — so one message says that,
|
|
138
|
+
// rather than N messages that read like N independent faults.
|
|
139
|
+
//
|
|
140
|
+
// MUTATION CHECKED: returning `rows.map(...)` unconditionally from collapse() makes this
|
|
141
|
+
// case fail with FLEET_GUARD separate notices.
|
|
142
|
+
const many = Array.from({ length: FLEET_GUARD }, (_, i) => obs({ id: i + 1, slug: `p${i + 1}` }));
|
|
143
|
+
const state = new Map();
|
|
144
|
+
let out;
|
|
145
|
+
for (let i = 0; i < DOWN_CONFIRM; i++) out = planNotices(many, state);
|
|
146
|
+
assert.equal(out.notices.length, 1, `one collapsed message, got ${out.notices.length}`);
|
|
147
|
+
assert.equal(out.notices[0].ids.length, FLEET_GUARD, 'it carries every id');
|
|
148
|
+
assert.equal(out.suppressed, FLEET_GUARD, 'and reports how many it stood in for');
|
|
149
|
+
for (const o of many) {
|
|
150
|
+
assert.ok(!out.notices[0].text.includes(o.slug), `a collapsed message must not name ${o.slug}`);
|
|
151
|
+
}
|
|
152
|
+
assert.match(out.notices[0].text, /usually the platform's own check/, 'it says where to look first');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
t('one BELOW the guard stays per-project, so the collapse is a threshold and not the default', () => {
|
|
156
|
+
const few = Array.from({ length: FLEET_GUARD - 1 }, (_, i) => obs({ id: i + 1, slug: `p${i + 1}` }));
|
|
157
|
+
const state = new Map();
|
|
158
|
+
let out;
|
|
159
|
+
for (let i = 0; i < DOWN_CONFIRM; i++) out = planNotices(few, state);
|
|
160
|
+
assert.equal(out.notices.length, FLEET_GUARD - 1, 'one message each');
|
|
161
|
+
assert.equal(out.suppressed, 0);
|
|
162
|
+
assert.ok(out.notices.every((n) => n.ids.length === 1));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
console.log('\nthe privacy rule (the one with a consequence):');
|
|
166
|
+
|
|
167
|
+
t('a project that is not public on the map is DESCRIBED, never named', () => {
|
|
168
|
+
// A private project's slug must not ride a Discord channel. This is the rule in this
|
|
169
|
+
// file whose failure is a disclosure rather than noise.
|
|
170
|
+
//
|
|
171
|
+
// MUTATION CHECKED: passing `r.slug` instead of `r.isPublic ? r.slug : null` in
|
|
172
|
+
// collapse() makes this case fail with the slug in the text.
|
|
173
|
+
const state = new Map();
|
|
174
|
+
let out;
|
|
175
|
+
for (let i = 0; i < DOWN_CONFIRM; i++) out = planNotices([obs({ isPublic: false, slug: 'secret-thing' })], state);
|
|
176
|
+
assert.equal(out.notices.length, 1);
|
|
177
|
+
assert.ok(!out.notices[0].text.includes('secret-thing'), 'the slug must never appear');
|
|
178
|
+
assert.match(out.notices[0].text, /A private project/, 'it is described instead');
|
|
179
|
+
// The recovery half has the same obligation — it is a second message about the same row.
|
|
180
|
+
const back = planNotices([obs({ isPublic: false, slug: 'secret-thing', up: true })], state);
|
|
181
|
+
assert.ok(!back.notices[0].text.includes('secret-thing'), 'and the all-clear must not leak it either');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
t('a public project IS named — the control for the rule above', () => {
|
|
185
|
+
const state = new Map();
|
|
186
|
+
let out;
|
|
187
|
+
for (let i = 0; i < DOWN_CONFIRM; i++) out = planNotices([obs({ isPublic: true, slug: 'lantern' })], state);
|
|
188
|
+
assert.match(out.notices[0].text, /lantern/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
console.log('\nthe copy:');
|
|
192
|
+
|
|
193
|
+
t('noticeText makes no wall-clock promise, and says what happens next', () => {
|
|
194
|
+
// The owner is being told something is wrong; a message that promises "back shortly"
|
|
195
|
+
// is a commitment nothing here can keep.
|
|
196
|
+
const down = noticeText({ kind: 'down', name: 'lantern' });
|
|
197
|
+
assert.doesNotMatch(down, /shortly|briefly|minute|second|soon\b/i);
|
|
198
|
+
assert.match(down, /say so here when the project answers again/, 'it says what the platform will do');
|
|
199
|
+
assert.match(noticeText({ kind: 'up', name: 'lantern' }), /answering again/);
|
|
200
|
+
assert.match(noticeText({ kind: 'up', count: 4 }), /All 4 projects/, 'the collapsed all-clear counts them');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
console.log(`\nliveness_notify: ${passed} passed, ${failed} failed`);
|
|
204
|
+
if (failed) process.exit(1);
|