@bongos/core 1.19.126 → 1.19.127
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 +13 -13
- package/modules/builder-settings/builder-needs.js +59 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/db-kernel.js +15 -0
- package/src/bongos/db.js +2 -1
- package/src/bongos/routes/me.js +11 -1
- package/src/module-api.js +3 -1
- package/tests/builder_needs.mjs +114 -0
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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.127",
|
|
6
|
+
"core_contract": "1.19.127",
|
|
7
|
+
"source_commit": "f4b67a76dc2ad911a7c9257ba4280de4ac7904c7",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-15T00:
|
|
9
|
+
"built_at": "2026-08-15T00:49:23.419Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 303,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 1569,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "deedfbf01298656cc039a9d567722f3689778d62d8aacc97a230ac4490a39c7a",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2367,7 +2367,7 @@
|
|
|
2367
2367
|
{
|
|
2368
2368
|
"path": "modules/builder-settings/builder-needs.js",
|
|
2369
2369
|
"mode": "0000644",
|
|
2370
|
-
"sha256": "
|
|
2370
|
+
"sha256": "3cb7478d9e347147e331f0954a9178a1770918067a678efdb8caef96241dfb5a"
|
|
2371
2371
|
},
|
|
2372
2372
|
{
|
|
2373
2373
|
"path": "modules/builder-settings/db.js",
|
|
@@ -4167,12 +4167,12 @@
|
|
|
4167
4167
|
{
|
|
4168
4168
|
"path": "package-lock.json",
|
|
4169
4169
|
"mode": "0000644",
|
|
4170
|
-
"sha256": "
|
|
4170
|
+
"sha256": "ecb64df7be6949adcb0d88b5f21ca3014724b598fa1a6d939a3a50d8f9696aa6"
|
|
4171
4171
|
},
|
|
4172
4172
|
{
|
|
4173
4173
|
"path": "package.json",
|
|
4174
4174
|
"mode": "0000644",
|
|
4175
|
-
"sha256": "
|
|
4175
|
+
"sha256": "5be9320e605a4b67ba2d63fa4ab831b99bda91a847c11338fd5e0ed6a65d0779"
|
|
4176
4176
|
},
|
|
4177
4177
|
{
|
|
4178
4178
|
"path": "public-docs/index.html",
|
|
@@ -5527,12 +5527,12 @@
|
|
|
5527
5527
|
{
|
|
5528
5528
|
"path": "src/bongos/db-kernel.js",
|
|
5529
5529
|
"mode": "0000644",
|
|
5530
|
-
"sha256": "
|
|
5530
|
+
"sha256": "f31dd847ed641bf49013c345b1f08f9e2794a875dad777de10daeda2a4ae1cf3"
|
|
5531
5531
|
},
|
|
5532
5532
|
{
|
|
5533
5533
|
"path": "src/bongos/db.js",
|
|
5534
5534
|
"mode": "0000644",
|
|
5535
|
-
"sha256": "
|
|
5535
|
+
"sha256": "d3dd606664855a94788751d13ab7fc62ccdc4a8df48f331ec97a5a81a8a6fa96"
|
|
5536
5536
|
},
|
|
5537
5537
|
{
|
|
5538
5538
|
"path": "src/bongos/github-repos.js",
|
|
@@ -5687,7 +5687,7 @@
|
|
|
5687
5687
|
{
|
|
5688
5688
|
"path": "src/bongos/routes/me.js",
|
|
5689
5689
|
"mode": "0000644",
|
|
5690
|
-
"sha256": "
|
|
5690
|
+
"sha256": "83657d8a2f8e1d81a80324350ddd983baf5346da3f88ce1f41c2211275f34440"
|
|
5691
5691
|
},
|
|
5692
5692
|
{
|
|
5693
5693
|
"path": "src/bongos/routes/modules.js",
|
|
@@ -5757,7 +5757,7 @@
|
|
|
5757
5757
|
{
|
|
5758
5758
|
"path": "src/module-api.js",
|
|
5759
5759
|
"mode": "0000644",
|
|
5760
|
-
"sha256": "
|
|
5760
|
+
"sha256": "80e087f2181512cfdf51030c27aeb7aaa4235421c4e06c9d7c4a000d418b0d0a"
|
|
5761
5761
|
},
|
|
5762
5762
|
{
|
|
5763
5763
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6147,7 +6147,7 @@
|
|
|
6147
6147
|
{
|
|
6148
6148
|
"path": "tests/builder_needs.mjs",
|
|
6149
6149
|
"mode": "0000644",
|
|
6150
|
-
"sha256": "
|
|
6150
|
+
"sha256": "49a8146cbb0ec5def5b48130ed61fdec6e9855f22fe5ad3a09b39ae2cb218ca8"
|
|
6151
6151
|
},
|
|
6152
6152
|
{
|
|
6153
6153
|
"path": "tests/bump_version.mjs",
|
|
@@ -90,13 +90,68 @@ function artKeyNeed(ctx) {
|
|
|
90
90
|
// pure (ctx) => need|null function here — every renderer picks it up for free.
|
|
91
91
|
const NEEDS = [artKeyNeed];
|
|
92
92
|
|
|
93
|
+
// ---- founding grace (task 1002739 / direction §5.6, idea 1000701) -----------
|
|
94
|
+
//
|
|
95
|
+
// A freshly-provisioned instance used to greet its founding owner with "your
|
|
96
|
+
// free art coverage has ended" — a demand as the payoff moment of the whole
|
|
97
|
+
// create flow. While the founding owner's first days are running, the roster
|
|
98
|
+
// leads with a welcome and every action_needed item is held at 'covered'
|
|
99
|
+
// (listed, honest, but not demanding). First-run is a time window, not a
|
|
100
|
+
// visit-counter: counting the first hall visit would end the grace on the very
|
|
101
|
+
// request that renders it.
|
|
102
|
+
//
|
|
103
|
+
// ctx.isFoundingOwner — is this builder the instance's first admitted builder?
|
|
104
|
+
// ctx.founderCreatedAt — that first builder's created_at (≈ the instance's
|
|
105
|
+
// birth on a newborn); the window anchors here.
|
|
106
|
+
// ctx.projectName — branding identity.productName, for the greeting.
|
|
107
|
+
// ctx.nowMs — injectable clock for tests; defaults to Date.now().
|
|
108
|
+
const FOUNDING_GRACE_DAYS = 7;
|
|
109
|
+
const FOUNDING_GRACE_MS = FOUNDING_GRACE_DAYS * 24 * 60 * 60 * 1000;
|
|
110
|
+
|
|
111
|
+
function foundingGraceActive(ctx = {}) {
|
|
112
|
+
if (!ctx.isFoundingOwner || !ctx.founderCreatedAt) return false;
|
|
113
|
+
const born = new Date(ctx.founderCreatedAt).getTime();
|
|
114
|
+
if (!Number.isFinite(born)) return false;
|
|
115
|
+
const now = typeof ctx.nowMs === 'number' ? ctx.nowMs : Date.now();
|
|
116
|
+
return now - born < FOUNDING_GRACE_MS;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function foundingWelcomeNeed(ctx) {
|
|
120
|
+
const name = ctx.projectName || 'your project';
|
|
121
|
+
return {
|
|
122
|
+
id: 'founding_welcome',
|
|
123
|
+
title: `Welcome to ${name}`,
|
|
124
|
+
what: 'Your project is standing, and this hall is where it gets built.',
|
|
125
|
+
learn_more_url: null,
|
|
126
|
+
state: 'covered',
|
|
127
|
+
severity: 'info',
|
|
128
|
+
message: `You made it — ${name} is up, and this hall is its workshop. ` +
|
|
129
|
+
'Look around at your own pace; if the platform ever needs something from you, ' +
|
|
130
|
+
'it will say so here once your first days are behind you.',
|
|
131
|
+
action: null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
93
135
|
// Compute the live list for a builder. `ctx` carries everything the need
|
|
94
136
|
// functions read (see each need's doc). Returns:
|
|
95
137
|
// { items: Need[], action_needed_count, has_action_needed }
|
|
96
138
|
// items is ordered: action_needed first (loudest), then covered, then satisfied,
|
|
97
|
-
// so any renderer that takes "the first one" gets the most urgent.
|
|
139
|
+
// so any renderer that takes "the first one" gets the most urgent. During the
|
|
140
|
+
// founding grace nothing is action_needed, so the welcome (prepended before the
|
|
141
|
+
// stable sort) is what that renderer shows.
|
|
98
142
|
function computeNeeds(ctx = {}) {
|
|
99
|
-
const
|
|
143
|
+
const grace = foundingGraceActive(ctx);
|
|
144
|
+
let items = NEEDS.map((fn) => fn(ctx)).filter(Boolean);
|
|
145
|
+
if (grace) {
|
|
146
|
+
items = items.map((n) => n.state !== 'action_needed' ? n : {
|
|
147
|
+
...n,
|
|
148
|
+
state: 'covered',
|
|
149
|
+
severity: 'info',
|
|
150
|
+
message: 'Nothing needed from you today — we’ll bring this up once your first days are behind you.',
|
|
151
|
+
action: null,
|
|
152
|
+
});
|
|
153
|
+
items.unshift(foundingWelcomeNeed(ctx));
|
|
154
|
+
}
|
|
100
155
|
const order = { action_needed: 0, covered: 1, satisfied: 2 };
|
|
101
156
|
items.sort((a, b) => (order[a.state] ?? 9) - (order[b.state] ?? 9));
|
|
102
157
|
const actionNeeded = items.filter((n) => n.state === 'action_needed');
|
|
@@ -111,5 +166,7 @@ module.exports = {
|
|
|
111
166
|
computeNeeds,
|
|
112
167
|
// exported for tests / direct reuse
|
|
113
168
|
artKeyNeed,
|
|
169
|
+
foundingGraceActive,
|
|
170
|
+
FOUNDING_GRACE_DAYS,
|
|
114
171
|
SETTINGS_PATH,
|
|
115
172
|
};
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.127",
|
|
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.127",
|
|
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.127",
|
|
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/bongos/db-kernel.js
CHANGED
|
@@ -62,6 +62,20 @@ async function getBuilderById(id) {
|
|
|
62
62
|
return rows[0] ?? null;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// getFoundingBuilder — the instance's first admitted HUMAN builder (task
|
|
66
|
+
// 1002739): the founding owner a newborn instance greets, and the anchor of
|
|
67
|
+
// the builder-needs founding-grace window. system_role IS NULL is load-bearing:
|
|
68
|
+
// core migrations 065/067 seed the BFG + Discord-bot system principals into
|
|
69
|
+
// builders on EVERY instance before anyone can sign in, so the lowest ids on a
|
|
70
|
+
// newborn DB are not the founder. First human row by id, regardless of status —
|
|
71
|
+
// the founder stays the founder even if later deactivated.
|
|
72
|
+
async function getFoundingBuilder() {
|
|
73
|
+
const { rows } = await pool.query(
|
|
74
|
+
`SELECT id, created_at FROM builders WHERE system_role IS NULL ORDER BY id ASC LIMIT 1`
|
|
75
|
+
);
|
|
76
|
+
return rows[0] ?? null;
|
|
77
|
+
}
|
|
78
|
+
|
|
65
79
|
// listActiveBuilders — the minimal, any-builder-readable roster (task 1877): an
|
|
66
80
|
// invite-picker needs a real name of every builder to invite, not just the
|
|
67
81
|
// leaderboard's top-100-by-credits window. Deliberately narrow columns — no
|
|
@@ -337,6 +351,7 @@ module.exports = {
|
|
|
337
351
|
withTx,
|
|
338
352
|
sessionPermsSelect,
|
|
339
353
|
getBuilderById,
|
|
354
|
+
getFoundingBuilder,
|
|
340
355
|
listActiveBuilders,
|
|
341
356
|
insertAuditLog,
|
|
342
357
|
LIVE_RANK_LADDER,
|
package/src/bongos/db.js
CHANGED
|
@@ -38,7 +38,7 @@ const seams = require('../module-seams');
|
|
|
38
38
|
// published doorway resolves through. Re-exported below so every caller is
|
|
39
39
|
// byte-identical; db-kernel depends only on ./pool, never ./db (one-way).
|
|
40
40
|
const {
|
|
41
|
-
withTx, getBuilderById, listActiveBuilders, insertAuditLog, LIVE_RANK_LADDER,
|
|
41
|
+
withTx, getBuilderById, getFoundingBuilder, listActiveBuilders, insertAuditLog, LIVE_RANK_LADDER,
|
|
42
42
|
// Auth/identity/admission + the rank vocabulary carved to db-kernel (ADR 0102,
|
|
43
43
|
// finishing ADR 0091 §1). Re-exported below so db.js's other importers + its own
|
|
44
44
|
// internal uses (setBuilderRank's RANKS.indexOf, etc.) stay byte-identical.
|
|
@@ -978,6 +978,7 @@ module.exports = {
|
|
|
978
978
|
upsertBuilder,
|
|
979
979
|
seatFirstAdminIfUnclaimed,
|
|
980
980
|
getBuilderById,
|
|
981
|
+
getFoundingBuilder,
|
|
981
982
|
listActiveBuilders,
|
|
982
983
|
linkAnthropicAccount,
|
|
983
984
|
getBuilderByGithubId,
|
package/src/bongos/routes/me.js
CHANGED
|
@@ -83,7 +83,7 @@ module.exports = function buildMeRouter() {
|
|
|
83
83
|
const [
|
|
84
84
|
builder, activeClaims, estVsActual, unlocked, lockedWithProgress, streakInfo,
|
|
85
85
|
recentGrades, builderAvg, onboardingRow, shipCount, recentKarma, karma30d, discordLink, boxConnected, cliAuthed,
|
|
86
|
-
behaviorPrefs, geminiKeyRow, needsRebaseTasks,
|
|
86
|
+
behaviorPrefs, geminiKeyRow, needsRebaseTasks, foundingBuilder,
|
|
87
87
|
] = await Promise.all([
|
|
88
88
|
db.getBuilderById(req.builder.id),
|
|
89
89
|
lc().getActiveClaimsForBuilder(req.builder.id),
|
|
@@ -144,6 +144,10 @@ module.exports = function buildMeRouter() {
|
|
|
144
144
|
// without a follow-up call. Through the `lifecycle` port; [] on a
|
|
145
145
|
// lifecycle-less instance (guarded — the port is resolveOptional).
|
|
146
146
|
lc() ? lc().needsRebaseTasksForBuilder(req.builder.id) : [],
|
|
147
|
+
// task 1002739: the instance's first admitted builder — anchors the
|
|
148
|
+
// builder-needs founding-grace window (a newborn instance's founder gets a
|
|
149
|
+
// welcome, not a nag). Best-effort: null just means no grace.
|
|
150
|
+
db.getFoundingBuilder().catch(() => null),
|
|
147
151
|
]);
|
|
148
152
|
// V3.R84 (#302), criterion C3: the builder's month-to-date spend vs their
|
|
149
153
|
// monthly budget. Surfaced here so the CLI (session-start ping) and the hall
|
|
@@ -274,6 +278,12 @@ module.exports = function buildMeRouter() {
|
|
|
274
278
|
rank: builder?.rank,
|
|
275
279
|
ownKeySet: !!(geminiKeyRow && geminiKeyRow.enc),
|
|
276
280
|
sharedKeyConfigured: artSharedKeyConfigured(),
|
|
281
|
+
// founding grace (task 1002739 / direction §5.6): a newborn instance's
|
|
282
|
+
// founder gets a welcome roster, not a coverage nag. The module owns the
|
|
283
|
+
// policy (window length, copy); we pass the raw facts.
|
|
284
|
+
isFoundingOwner: !!(foundingBuilder && String(foundingBuilder.id) === String(req.builder.id)),
|
|
285
|
+
founderCreatedAt: foundingBuilder ? foundingBuilder.created_at : null,
|
|
286
|
+
projectName: (() => { try { return branding().identity?.productName || null; } catch { return null; } })(),
|
|
277
287
|
}) : [],
|
|
278
288
|
};
|
|
279
289
|
// Module→core contribution seam (BV1.R42, ADR 0083). Let each enabled module
|
package/src/module-api.js
CHANGED
|
@@ -679,8 +679,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
679
679
|
// landed since 1.19.124 with no explicit bump. run 31849445746. (task 1002620)
|
|
680
680
|
// 1.19.126 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
681
681
|
// landed since 1.19.125 with no explicit bump. run 31853055269. (task 1002620)
|
|
682
|
+
// 1.19.127 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
683
|
+
// landed since 1.19.126 with no explicit bump. run 31854730552. (task 1002620)
|
|
682
684
|
// ---------------------------------------------------------------------------
|
|
683
|
-
const CORE_VERSION = '1.19.
|
|
685
|
+
const CORE_VERSION = '1.19.127'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
684
686
|
|
|
685
687
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
686
688
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/builder_needs.mjs
CHANGED
|
@@ -95,5 +95,119 @@ t('missing rank fails safe — not action_needed without an explicit rank', () =
|
|
|
95
95
|
assert.ok(['covered', 'action_needed', 'satisfied'].includes(n.state));
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
console.log('founding grace (task 1002739, direction §5.6):');
|
|
99
|
+
|
|
100
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
101
|
+
const bornNow = new Date().toISOString();
|
|
102
|
+
|
|
103
|
+
t('newborn founder: welcome item first, greeting by project name', () => {
|
|
104
|
+
const r = needs.computeNeeds({
|
|
105
|
+
rank: 'archon', ownKeySet: false,
|
|
106
|
+
isFoundingOwner: true, founderCreatedAt: bornNow, projectName: 'Mercury',
|
|
107
|
+
});
|
|
108
|
+
assert.equal(r.items[0].id, 'founding_welcome');
|
|
109
|
+
assert.match(r.items[0].title, /Mercury/);
|
|
110
|
+
assert.equal(r.items[0].state, 'covered');
|
|
111
|
+
assert.equal(r.items[0].action, null);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
t('newborn founder: zero action_needed — every nag is suppressed', () => {
|
|
115
|
+
const r = needs.computeNeeds({
|
|
116
|
+
rank: 'archon', ownKeySet: false, isFoundingOwner: true, founderCreatedAt: bornNow,
|
|
117
|
+
});
|
|
118
|
+
assert.equal(r.has_action_needed, false);
|
|
119
|
+
assert.equal(r.action_needed_count, 0);
|
|
120
|
+
for (const n of r.items) assert.notEqual(n.state, 'action_needed');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
t('a suppressed nag keeps its identity but loses the demand', () => {
|
|
124
|
+
const r = needs.computeNeeds({
|
|
125
|
+
rank: 'archon', ownKeySet: false, isFoundingOwner: true, founderCreatedAt: bornNow,
|
|
126
|
+
});
|
|
127
|
+
const artItem = r.items.find((n) => n.id === 'art_key');
|
|
128
|
+
assert.ok(artItem, 'the art_key item must still be listed, not hidden');
|
|
129
|
+
assert.equal(artItem.state, 'covered');
|
|
130
|
+
assert.equal(artItem.action, null);
|
|
131
|
+
assert.doesNotMatch(artItem.message, /coverage has ended/i);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
t('the grace window is seven days — the policy number is pinned, not self-referential', () => {
|
|
135
|
+
// A fixed injected clock, and 7 written as a literal on both sides: mutating
|
|
136
|
+
// FOUNDING_GRACE_DAYS (a "tune", a bad merge) must fail here, not slide.
|
|
137
|
+
assert.equal(needs.FOUNDING_GRACE_DAYS, 7);
|
|
138
|
+
const born = Date.parse('2026-01-01T00:00:00Z');
|
|
139
|
+
const ctx = { rank: 'archon', ownKeySet: false, isFoundingOwner: true, founderCreatedAt: '2026-01-01T00:00:00Z' };
|
|
140
|
+
const inWindow = needs.computeNeeds({ ...ctx, nowMs: born + 7 * DAY - 60_000 });
|
|
141
|
+
assert.equal(inWindow.has_action_needed, false);
|
|
142
|
+
assert.equal(inWindow.items[0].id, 'founding_welcome');
|
|
143
|
+
const expired = needs.computeNeeds({ ...ctx, nowMs: born + 7 * DAY + 60_000 });
|
|
144
|
+
assert.equal(expired.has_action_needed, true);
|
|
145
|
+
assert.ok(!expired.items.find((n) => n.id === 'founding_welcome'));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
t('grace expires: past FOUNDING_GRACE_DAYS the founder is nagged like anyone (real clock)', () => {
|
|
149
|
+
const old = new Date(Date.now() - (needs.FOUNDING_GRACE_DAYS * DAY + 60_000)).toISOString();
|
|
150
|
+
const r = needs.computeNeeds({
|
|
151
|
+
rank: 'archon', ownKeySet: false, isFoundingOwner: true, founderCreatedAt: old,
|
|
152
|
+
});
|
|
153
|
+
assert.equal(r.has_action_needed, true);
|
|
154
|
+
assert.ok(!r.items.find((n) => n.id === 'founding_welcome'));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
t('a non-founder on a newborn instance is not suppressed', () => {
|
|
158
|
+
const r = needs.computeNeeds({
|
|
159
|
+
rank: 'thetes', ownKeySet: false, isFoundingOwner: false, founderCreatedAt: bornNow,
|
|
160
|
+
});
|
|
161
|
+
assert.equal(r.has_action_needed, true);
|
|
162
|
+
assert.ok(!r.items.find((n) => n.id === 'founding_welcome'));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
t('satisfied stays satisfied during grace — no fake downgrades', () => {
|
|
166
|
+
const r = needs.computeNeeds({
|
|
167
|
+
rank: 'archon', ownKeySet: true, isFoundingOwner: true, founderCreatedAt: bornNow,
|
|
168
|
+
});
|
|
169
|
+
assert.equal(r.items.find((n) => n.id === 'art_key').state, 'satisfied');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
t('no founder facts at all → no grace, no welcome (vanilla ctx unchanged)', () => {
|
|
173
|
+
const r = needs.computeNeeds({ rank: 'thetes', ownKeySet: false });
|
|
174
|
+
assert.equal(r.has_action_needed, true);
|
|
175
|
+
assert.ok(!r.items.find((n) => n.id === 'founding_welcome'));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
console.log('the producing side (DB-free source pins — the policy is only as real as its facts):');
|
|
179
|
+
|
|
180
|
+
// The seven policy cases above hand computeNeeds its ctx. These pin the two
|
|
181
|
+
// places that PRODUCE that ctx, because both were wrong-or-unverified once:
|
|
182
|
+
// the founder query returned the migration-seeded BFG system principal (ids 1-2
|
|
183
|
+
// on every newborn DB — review panel, 2026-08-15), and a key rename in me.js
|
|
184
|
+
// would silently disable the grace while every policy case stayed green. A
|
|
185
|
+
// DB-backed newborn-instance test can't run in the unit gate, so the source is
|
|
186
|
+
// the checkable artifact.
|
|
187
|
+
import { readFileSync } from 'node:fs';
|
|
188
|
+
import { fileURLToPath } from 'node:url';
|
|
189
|
+
import path from 'node:path';
|
|
190
|
+
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
191
|
+
|
|
192
|
+
t('getFoundingBuilder skips the seeded system principals (system_role IS NULL)', () => {
|
|
193
|
+
const src = readFileSync(path.join(ROOT, 'src/bongos/db-kernel.js'), 'utf8');
|
|
194
|
+
const fn = src.slice(src.indexOf('function getFoundingBuilder'));
|
|
195
|
+
const sql = fn.slice(0, fn.indexOf('return'));
|
|
196
|
+
assert.match(sql, /FROM builders/);
|
|
197
|
+
assert.match(sql, /system_role IS NULL/,
|
|
198
|
+
'the founder query must exclude system principals — migrations 065/067 seed them at the lowest ids on every instance');
|
|
199
|
+
assert.match(sql, /ORDER BY id ASC/);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
t('the /me aggregator feeds computeNeeds the founder facts under their contract names', () => {
|
|
203
|
+
const src = readFileSync(path.join(ROOT, 'src/bongos/routes/me.js'), 'utf8');
|
|
204
|
+
const call = src.slice(src.indexOf('computeNeeds({'), src.indexOf('}) : []'));
|
|
205
|
+
for (const key of ['isFoundingOwner', 'founderCreatedAt', 'projectName']) {
|
|
206
|
+
assert.match(call, new RegExp(`${key}\\s*:`),
|
|
207
|
+
`me.js must pass ${key} into computeNeeds — a rename silently disables the founding grace`);
|
|
208
|
+
}
|
|
209
|
+
assert.match(src, /getFoundingBuilder\(\)/, 'me.js must resolve the founder row');
|
|
210
|
+
});
|
|
211
|
+
|
|
98
212
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
99
213
|
process.exit(failed ? 1 : 0);
|