@bongos/core 1.19.574 → 1.19.576
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 +17 -12
- package/docs/module-api-changelog.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +104 -61
- package/tests/logger.mjs +13 -1
- package/tests/module_api_lazy.mjs +111 -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.576",
|
|
6
|
+
"core_contract": "1.19.576",
|
|
7
|
+
"source_commit": "a97463936d818b3a6be556837447748b3a2abaa1",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-07T14:24:36.180Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 450,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2044,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2518,
|
|
20
|
+
"tree_sha256": "9b56f557f5e1d8d63c314cb2990df89c75d7230c3f16bdd41416396d95e2b6bd",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -2692,7 +2692,7 @@
|
|
|
2692
2692
|
{
|
|
2693
2693
|
"path": "docs/module-api-changelog.md",
|
|
2694
2694
|
"mode": "0000644",
|
|
2695
|
-
"sha256": "
|
|
2695
|
+
"sha256": "8f122cc3f3ca35dcc6569b98bad34967f2a4bbf9ebcbc0e645ca2b1016245fbc"
|
|
2696
2696
|
},
|
|
2697
2697
|
{
|
|
2698
2698
|
"path": "docs/modules-contract.md",
|
|
@@ -7552,12 +7552,12 @@
|
|
|
7552
7552
|
{
|
|
7553
7553
|
"path": "package-lock.json",
|
|
7554
7554
|
"mode": "0000644",
|
|
7555
|
-
"sha256": "
|
|
7555
|
+
"sha256": "8a58247d11bee28e30582d0860bd54cac15c8f3cb30753111063711a9369c390"
|
|
7556
7556
|
},
|
|
7557
7557
|
{
|
|
7558
7558
|
"path": "package.json",
|
|
7559
7559
|
"mode": "0000644",
|
|
7560
|
-
"sha256": "
|
|
7560
|
+
"sha256": "e2f634e2e1deae3517a6b545c91a2b4818c51b04cb56daa4f1528b822cc1d14e"
|
|
7561
7561
|
},
|
|
7562
7562
|
{
|
|
7563
7563
|
"path": "public-docs/index.html",
|
|
@@ -9257,7 +9257,7 @@
|
|
|
9257
9257
|
{
|
|
9258
9258
|
"path": "src/module-api.js",
|
|
9259
9259
|
"mode": "0000644",
|
|
9260
|
-
"sha256": "
|
|
9260
|
+
"sha256": "fb020c72dfb07b789b7e281ba766fe8f0f9c4fe67c359c8b5c9928c8e86b85da"
|
|
9261
9261
|
},
|
|
9262
9262
|
{
|
|
9263
9263
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11157,7 +11157,7 @@
|
|
|
11157
11157
|
{
|
|
11158
11158
|
"path": "tests/logger.mjs",
|
|
11159
11159
|
"mode": "0000644",
|
|
11160
|
-
"sha256": "
|
|
11160
|
+
"sha256": "7450580274562f784122e5e9eefe458669fb9083adddb7268364de7139f94cb5"
|
|
11161
11161
|
},
|
|
11162
11162
|
{
|
|
11163
11163
|
"path": "tests/main_audit.mjs",
|
|
@@ -11269,6 +11269,11 @@
|
|
|
11269
11269
|
"mode": "0000644",
|
|
11270
11270
|
"sha256": "39812c560bcd88e3b3a7938ed2447fbd4ab24b0f925e9662b375910bcbe7f1da"
|
|
11271
11271
|
},
|
|
11272
|
+
{
|
|
11273
|
+
"path": "tests/module_api_lazy.mjs",
|
|
11274
|
+
"mode": "0000644",
|
|
11275
|
+
"sha256": "40c396fda3a618bf9fa2d2ae3d8d30b50e62c4210199d70b5e29cb82a046d037"
|
|
11276
|
+
},
|
|
11272
11277
|
{
|
|
11273
11278
|
"path": "tests/module_catalog.mjs",
|
|
11274
11279
|
"mode": "0000644",
|
|
@@ -1597,5 +1597,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1597
1597
|
landed since 1.19.572 with no explicit bump. run 34072921884. (task 1002620)
|
|
1598
1598
|
1.19.574 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1599
1599
|
landed since 1.19.573 with no explicit bump. run 34075598213. (task 1002620)
|
|
1600
|
+
1.19.575 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1601
|
+
landed since 1.19.574 with no explicit bump. run 34076465465. (task 1002620)
|
|
1602
|
+
1.19.576 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1603
|
+
landed since 1.19.575 with no explicit bump. run 34132732488. (task 1002620)
|
|
1600
1604
|
---------------------------------------------------------------------------
|
|
1601
1605
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.576",
|
|
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.576",
|
|
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.576",
|
|
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
|
@@ -9,32 +9,38 @@
|
|
|
9
9
|
// (task 1406); seams in src/module-seams.js (BV1.R39 / task 1408); the loader that
|
|
10
10
|
// mounts modules behind kernel-composed auth is BV1.R41 / task 1410.
|
|
11
11
|
|
|
12
|
-
const auth = require('./bongos/auth');
|
|
13
12
|
const util = require('node:util');
|
|
14
|
-
// task 1003208: api.logger() returns a real pino child from here (see below).
|
|
15
|
-
const { logger: coreLogger } = require('./bongos/logger');
|
|
16
|
-
const { pool, instanceDbName } = require('./bongos/pool');
|
|
17
13
|
const branding = require('./branding');
|
|
18
|
-
|
|
14
|
+
|
|
15
|
+
// LAZY BY CONSTRUCTION (task 1003677). Every kernel capability below that costs
|
|
16
|
+
// something to load — auth, the Postgres pool, the route helpers, project
|
|
17
|
+
// settings, the logger — is reached through a getter that `require()`s it on
|
|
18
|
+
// first READ, never at module load. Node's module cache makes the repeat cost a
|
|
19
|
+
// map lookup, and this file already used the idiom for db-kernel and the LLM
|
|
20
|
+
// helpers; this extends it to the rest.
|
|
21
|
+
//
|
|
22
|
+
// Why it matters beyond tidiness: `require('src/module-api')` used to open a
|
|
23
|
+
// Postgres pool and load the whole auth stack as a side effect of being
|
|
24
|
+
// imported. Modules are required by the CLI too — `bongos claim` reads
|
|
25
|
+
// modules/lifecycle/ship-card, `bongos start` reads task-classifier, and each
|
|
26
|
+
// needs `branding` and nothing else — so importing one dragged 22 server files
|
|
27
|
+
// into every CLI subcommand's require-closure. That is what made the CLI
|
|
28
|
+
// impossible to ship as a public, client-only npx package (goal 1000054), and
|
|
29
|
+
// with it what kept a builder holding no private-registry credential from
|
|
30
|
+
// claiming any task at all.
|
|
31
|
+
//
|
|
32
|
+
// The contract is unchanged: ADR 0083's doorway is still the one file a module
|
|
33
|
+
// may import, and every export still resolves to exactly what it did before —
|
|
34
|
+
// it just resolves when read instead of when loaded. Adding an export stays a
|
|
35
|
+
// MINOR bump; a getter is not a breaking change to a consumer that reads it.
|
|
36
|
+
//
|
|
37
|
+
// Keep it this way. A plain `name: require('./bongos/x').y` entry in the object
|
|
38
|
+
// below is EAGER — it evaluates while the literal is built — so a new export
|
|
39
|
+
// that costs anything belongs behind `get name() { return require(...); }`.
|
|
19
40
|
const instanceConfig = require('./instance-config');
|
|
20
41
|
const staleTimer = require('./stale-timer');
|
|
21
|
-
// Destructured, not namespaced, deliberately — the same form src/bongos/routes/
|
|
22
|
-
// builders.js uses for stale-timer.js. knip attributes a named export to a
|
|
23
|
-
// destructured binding but not to a property read off a required namespace, so the
|
|
24
|
-
// namespace form reports every export here as dead and regresses the ratchet.
|
|
25
|
-
const {
|
|
26
|
-
resolveRotDays,
|
|
27
|
-
SETTING_KEYS: PROJECT_SETTING_KEYS,
|
|
28
|
-
getSetting: getProjectSetting,
|
|
29
|
-
setSetting: setProjectSetting,
|
|
30
|
-
describeRotTimer,
|
|
31
|
-
parseDays: parseRotDays,
|
|
32
|
-
MIN_ROT_DAYS,
|
|
33
|
-
MAX_ROT_DAYS,
|
|
34
|
-
} = require('./bongos/project-settings');
|
|
35
42
|
const seams = require('./module-seams');
|
|
36
43
|
const { buildInfo } = require('./build-info');
|
|
37
|
-
const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePagination, pageMeta, PAGINATION } = require('./bongos/routes/_helpers');
|
|
38
44
|
|
|
39
45
|
// ---------------------------------------------------------------------------
|
|
40
46
|
// CORE_VERSION — the version of THIS published surface (semver).
|
|
@@ -49,7 +55,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
49
55
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
50
56
|
// the entry to that file. Look for a version's history there, not here.
|
|
51
57
|
// ---------------------------------------------------------------------------
|
|
52
|
-
const CORE_VERSION = '1.19.
|
|
58
|
+
const CORE_VERSION = '1.19.576'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
53
59
|
|
|
54
60
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
55
61
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -74,7 +80,7 @@ const CORE_VERSION = '1.19.574'; // CI auto-patch carrier (ADR 0161); changelog:
|
|
|
74
80
|
// A secret INTERPOLATED into a message string is still not redactable here; that
|
|
75
81
|
// is the scrubber's and the prevent hook's job, as logger.js's own header says.
|
|
76
82
|
function logger(moduleKey) {
|
|
77
|
-
const child =
|
|
83
|
+
const child = require('./bongos/logger').logger.child({ module: moduleKey || 'module' });
|
|
78
84
|
const emit = (level) => (...args) => {
|
|
79
85
|
if (args.length === 0) return child[level]('');
|
|
80
86
|
const bindings = {};
|
|
@@ -98,21 +104,21 @@ module.exports = {
|
|
|
98
104
|
|
|
99
105
|
// --- auth & rank (ADR 0016) — the gates the loader composes around a module's
|
|
100
106
|
// routes; a module declares the rank it needs and never wires auth itself.
|
|
101
|
-
requireBuilder
|
|
102
|
-
requireRank
|
|
107
|
+
get requireBuilder() { return require('./bongos/auth').requireBuilder; },
|
|
108
|
+
get requireRank() { return require('./bongos/auth').requireRank; },
|
|
103
109
|
// requirePermission(...atoms) — the AUTHORITY-ATOM gate (ADR 0151), mounted
|
|
104
110
|
// AFTER requireBuilder exactly like requireRank. Async, AND-semantics,
|
|
105
111
|
// fail-closed; denies with `permission_forbidden` rather than
|
|
106
112
|
// `rank_forbidden`. The rank-role compat seed makes it admit exactly who
|
|
107
113
|
// the equivalent requireRank admitted, so a migrated module route is
|
|
108
114
|
// behaviour-identical (BV1.R105).
|
|
109
|
-
requirePermission
|
|
110
|
-
rankMeetsThreshold
|
|
111
|
-
requireBfgPrincipal
|
|
115
|
+
get requirePermission() { return require('./bongos/auth').requirePermission; },
|
|
116
|
+
get rankMeetsThreshold() { return require('./bongos/auth').rankMeetsThreshold; },
|
|
117
|
+
get requireBfgPrincipal() { return require('./bongos/auth').requireBfgPrincipal; },
|
|
112
118
|
// allowBoxScope — opt a route in to box-scoped (dev-box bootstrap) session
|
|
113
119
|
// tokens. A module's own bootstrap routes flag themselves; everything else
|
|
114
120
|
// stays closed to box tokens (the ADR 0016 trust boundary).
|
|
115
|
-
allowBoxScope
|
|
121
|
+
get allowBoxScope() { return require('./bongos/auth').allowBoxScope; },
|
|
116
122
|
// resolveSession(req) — resolve the request to a live session row (or null)
|
|
117
123
|
// WITHOUT the 401-or-pass behaviour of requireBuilder. A module route that
|
|
118
124
|
// must make its own auth decision — e.g. a browser page that REDIRECTS to
|
|
@@ -121,13 +127,13 @@ module.exports = {
|
|
|
121
127
|
// Returns the same shape lookupSession does (builder_id, github_id,
|
|
122
128
|
// github_login, display_name, avatar_url, rank, source, status); rank is
|
|
123
129
|
// still never an authority a module should trust for cross-instance identity.
|
|
124
|
-
resolveSession: (...a) => auth.resolveSession(...a),
|
|
130
|
+
resolveSession: (...a) => require('./bongos/auth').resolveSession(...a),
|
|
125
131
|
|
|
126
132
|
// --- data — the shared Postgres pool. A module owns its namespaced tables
|
|
127
133
|
// (<key>_*) via its own migrations (BV1.R46); it reaches them through this.
|
|
128
|
-
pool,
|
|
129
|
-
getPool: () => pool,
|
|
130
|
-
instanceDbName,
|
|
134
|
+
get pool() { return require('./bongos/pool').pool; },
|
|
135
|
+
getPool: () => require('./bongos/pool').pool,
|
|
136
|
+
get instanceDbName() { return require('./bongos/pool').instanceDbName; },
|
|
131
137
|
// withTx(fn, {pool}) — run fn(client) inside BEGIN/COMMIT (ROLLBACK + rethrow
|
|
132
138
|
// on error, always release). The KERNEL transaction primitive a carved
|
|
133
139
|
// core domain needs for a multi-statement write (BV1.R68 / ADR 0091 §3;
|
|
@@ -157,7 +163,7 @@ module.exports = {
|
|
|
157
163
|
// enumerator alternates between them for double the rate (task 1003339,
|
|
158
164
|
// ADR 0209). Vendoring a second copy in a module is the failure this
|
|
159
165
|
// export exists to prevent.
|
|
160
|
-
accountExistenceReadRateLimit
|
|
166
|
+
get accountExistenceReadRateLimit() { return require('./bongos/middleware/rate-limit').accountExistenceReadRateLimit; },
|
|
161
167
|
// publicProjectFeedRateLimit — the per-IP ceiling (120 reads/60s) on the PUBLIC
|
|
162
168
|
// projects feed (task 1002327, ADR 0252 §5.3). Same reasoning as its
|
|
163
169
|
// neighbour, and the same shape — ONE middleware instance, never a factory,
|
|
@@ -168,20 +174,20 @@ module.exports = {
|
|
|
168
174
|
// much as ours. ADR 0252 made this a condition of auto-appear, not a
|
|
169
175
|
// follow-up — the straight-through path and its abuse guard ship together
|
|
170
176
|
// (ADR 0047).
|
|
171
|
-
publicProjectFeedRateLimit
|
|
177
|
+
get publicProjectFeedRateLimit() { return require('./bongos/middleware/rate-limit').publicProjectFeedRateLimit; },
|
|
172
178
|
|
|
173
179
|
// --- per-builder secrets at rest (AES-256-GCM box) — a kernel trust
|
|
174
180
|
// primitive; the full secretBox object is exposed so a module can call
|
|
175
181
|
// isConfigured/encrypt/decrypt/provisionMasterKey without reaching core.
|
|
176
|
-
secretBox
|
|
182
|
+
get secretBox() { return require('./bongos/secret-box'); },
|
|
177
183
|
|
|
178
184
|
// --- KERNEL trust classifiers (ADR 0091 §1, KERNEL_FILES) — the deterministic
|
|
179
185
|
// route→rank + protected-path matchers. They are KERNEL (the trust
|
|
180
186
|
// boundary, never "called" by a domain), exposed so a module that runs the
|
|
181
187
|
// same deterministic pre-passes (the grading panel, BV1.R73) reaches them
|
|
182
188
|
// through the doorway instead of a deep core require. PERMANENT.
|
|
183
|
-
routeRankCheck
|
|
184
|
-
permissionPathCheck
|
|
189
|
+
get routeRankCheck() { return require('./bongos/route-rank-check'); },
|
|
190
|
+
get permissionPathCheck() { return require('./bongos/permission-path-check'); },
|
|
185
191
|
// --- KERNEL repo + scope-map helpers (ADR 0091 §1, KERNEL_FILES) — exposed for
|
|
186
192
|
// the carved lifecycle module (BV1.R86): repoInfo (the git-remote/slug
|
|
187
193
|
// resolver github-push.js + conflict-resolve.js read) and moduleScopeMap
|
|
@@ -189,13 +195,13 @@ module.exports = {
|
|
|
189
195
|
// module-key validation). Both are KERNEL files (the scope-map IS the trust
|
|
190
196
|
// wall, repo-info is the publish-target identity); a module reaches them
|
|
191
197
|
// through the doorway instead of a deep core require. PERMANENT.
|
|
192
|
-
repoInfo
|
|
193
|
-
moduleScopeMap
|
|
198
|
+
get repoInfo() { return require('./bongos/repo-info'); },
|
|
199
|
+
get moduleScopeMap() { return require('./bongos/module-scope-map'); },
|
|
194
200
|
// pathMatch — the pure touches[]-overlap matcher (src/bongos/path-match.js; no
|
|
195
201
|
// domain logic — already a grandfathered kernel-adjacent infra util). The
|
|
196
202
|
// carved lifecycle module's claim/optimizer overlap checks reach it here
|
|
197
203
|
// instead of requiring the core file (BV1.R86).
|
|
198
|
-
pathMatch
|
|
204
|
+
get pathMatch() { return require('./bongos/path-match'); },
|
|
199
205
|
// goalScopeCheck — the pure scope wall for goal-authored tasks (src/bongos/goal-
|
|
200
206
|
// scope-check.js, BV1.R59 / ADR 0086 §3). The lifecycle module's POST /goals/:id/
|
|
201
207
|
// tasks create gate (BV1.R60) calls check(goalScopeModules, touches, builderRank)
|
|
@@ -204,7 +210,7 @@ module.exports = {
|
|
|
204
210
|
// moduleScopeMap + pathMatch + permissionPathCheck matchers above — never a
|
|
205
211
|
// second protected list — so a module reaches it via the doorway, not a deep
|
|
206
212
|
// core require (which the module-boundary fitness check would red). PERMANENT.
|
|
207
|
-
goalScopeCheck
|
|
213
|
+
get goalScopeCheck() { return require('./bongos/goal-scope-check'); },
|
|
208
214
|
|
|
209
215
|
// === DOMAIN capabilities still on the doorway — SCHEDULED TO RELOCATE ======
|
|
210
216
|
// NOT kernel: these leak a domain's queries through the doorway (ADR 0091
|
|
@@ -237,14 +243,14 @@ module.exports = {
|
|
|
237
243
|
// it can't be grading-owned. A tracked domain leak (the R70 ratchet flags
|
|
238
244
|
// module-api → llm-cache as WARN); it relocates behind a port if/when an
|
|
239
245
|
// llm-infra domain is carved (tranche 2).
|
|
240
|
-
llmCache
|
|
246
|
+
get llmCache() { return require('./bongos/llm-cache'); },
|
|
241
247
|
// • llmPricing (BV1.R77) → the shared token->USD pricing lib (the locked
|
|
242
248
|
// PRICES table + priceModelUsage/componentsFromModelUsage). NOT kernel;
|
|
243
249
|
// consumed by BOTH the carved economy module (reward/cost paths) AND core
|
|
244
250
|
// cost scripts (ship.js, session-cost.js), so it can't be economy-owned —
|
|
245
251
|
// a DOMAIN leak like llmCache. Relocates behind a port if/when an llm-infra
|
|
246
252
|
// domain is carved (tranche 2).
|
|
247
|
-
llmPricing
|
|
253
|
+
get llmPricing() { return require('./bongos/llm-pricing'); },
|
|
248
254
|
|
|
249
255
|
// --- host context — config / branding / identity (a module reads, never
|
|
250
256
|
// hardcodes, instance strings).
|
|
@@ -256,7 +262,7 @@ module.exports = {
|
|
|
256
262
|
// open-enrolment flag and the fail-closed default live in this one
|
|
257
263
|
// reader, and the sign-in flows and the public access-request route
|
|
258
264
|
// must answer with the same door (SR-19).
|
|
259
|
-
joinabilityMode
|
|
265
|
+
get joinabilityMode() { return require('./bongos/auth').joinabilityMode; },
|
|
260
266
|
// projectJoinDoor() — the project's COMPOSED join/apply door: 'open' | 'apply' |
|
|
261
267
|
// 'invite_only' | 'locked' (BV1.R12, task 1002331; ADR 0182 D6, ADR 0247).
|
|
262
268
|
// `joinabilityMode` above answers one knob; this answers the whole door,
|
|
@@ -266,7 +272,7 @@ module.exports = {
|
|
|
266
272
|
// surface calls THIS; a module that read a single knob would re-open a door
|
|
267
273
|
// the other two had closed. Optional `{ publishable }` for a caller that
|
|
268
274
|
// holds a publish verdict — see src/bongos/project-door.js.
|
|
269
|
-
projectJoinDoor: (opts) =>
|
|
275
|
+
projectJoinDoor: (opts) => require('./bongos/project-door').effectiveJoinDoor(opts),
|
|
270
276
|
// userAgent — the instance's HTTP User-Agent string (branding-derived). The
|
|
271
277
|
// carved lifecycle module's github-push.js stamps every GitHub API request with
|
|
272
278
|
// it instead of reaching into src/branding.js directly (BV1.R86).
|
|
@@ -296,21 +302,21 @@ module.exports = {
|
|
|
296
302
|
// is about work nobody picked up, and it only ever ASKS a human. The rot read
|
|
297
303
|
// deliberately skips tasks at 'active' so the two never report the same rot
|
|
298
304
|
// with two different numbers. See src/bongos/project-settings.js.
|
|
299
|
-
resolveRotDays,
|
|
305
|
+
get resolveRotDays() { return require('./bongos/project-settings').resolveRotDays; },
|
|
300
306
|
projectSettings: {
|
|
301
|
-
KEYS
|
|
302
|
-
get
|
|
303
|
-
set
|
|
304
|
-
describeRotTimer,
|
|
307
|
+
get KEYS() { return require('./bongos/project-settings').SETTING_KEYS; },
|
|
308
|
+
get get() { return require('./bongos/project-settings').getSetting; },
|
|
309
|
+
get set() { return require('./bongos/project-settings').setSetting; },
|
|
310
|
+
get describeRotTimer() { return require('./bongos/project-settings').describeRotTimer; },
|
|
305
311
|
// The RANGE CHECK, shared rather than re-typed. The settings page validates a
|
|
306
312
|
// submitted timer with the very parser the resolver reads it back through, so the
|
|
307
313
|
// form and the reader can never disagree about what 1..365 means (task 1003280).
|
|
308
|
-
parseDays
|
|
314
|
+
get parseDays() { return require('./bongos/project-settings').parseDays; },
|
|
309
315
|
// The bounds themselves, so a surface that must SHOW them (the settings page's rot
|
|
310
316
|
// row) reads the single source instead of re-typing 1/365 — the very drift the
|
|
311
317
|
// page's own anti-drift rule forbids, caught one layer up on task 1003280's grade.
|
|
312
|
-
MIN_ROT_DAYS,
|
|
313
|
-
MAX_ROT_DAYS,
|
|
318
|
+
get MIN_ROT_DAYS() { return require('./bongos/project-settings').MIN_ROT_DAYS; },
|
|
319
|
+
get MAX_ROT_DAYS() { return require('./bongos/project-settings').MAX_ROT_DAYS; },
|
|
314
320
|
},
|
|
315
321
|
readConfigFileSync: instanceConfig.readConfigFileSync,
|
|
316
322
|
configHome: instanceConfig.configHome,
|
|
@@ -351,18 +357,55 @@ module.exports = {
|
|
|
351
357
|
// own. asyncHandler + corsPublicGet added at BV1.R86 for the carved
|
|
352
358
|
// lifecycle routes (versions/done-when/analytics).
|
|
353
359
|
buildInfo,
|
|
354
|
-
validateOrRespond,
|
|
355
|
-
LIMITS,
|
|
356
|
-
parseId,
|
|
357
|
-
asyncHandler,
|
|
358
|
-
corsPublicGet,
|
|
360
|
+
get validateOrRespond() { return require('./bongos/routes/_helpers').validateOrRespond; },
|
|
361
|
+
get LIMITS() { return require('./bongos/routes/_helpers').LIMITS; },
|
|
362
|
+
get parseId() { return require('./bongos/routes/_helpers').parseId; },
|
|
363
|
+
get asyncHandler() { return require('./bongos/routes/_helpers').asyncHandler; },
|
|
364
|
+
get corsPublicGet() { return require('./bongos/routes/_helpers').corsPublicGet; },
|
|
359
365
|
// R14 (#2001 / ADR 0119): the shared pagination contract, so a MODULE list
|
|
360
366
|
// route pages identically to core (parsePagination clamps ?limit=&offset=;
|
|
361
367
|
// pageMeta builds the { limit, offset, total? } block).
|
|
362
|
-
parsePagination,
|
|
363
|
-
pageMeta,
|
|
364
|
-
PAGINATION,
|
|
368
|
+
get parsePagination() { return require('./bongos/routes/_helpers').parsePagination; },
|
|
369
|
+
get pageMeta() { return require('./bongos/routes/_helpers').pageMeta; },
|
|
370
|
+
get PAGINATION() { return require('./bongos/routes/_helpers').PAGINATION; },
|
|
365
371
|
|
|
366
372
|
// --- logging
|
|
367
373
|
logger,
|
|
368
374
|
};
|
|
375
|
+
|
|
376
|
+
// Keep every lazy export ASSIGNABLE (task 1003677). The getters above turned what
|
|
377
|
+
// used to be plain data properties into accessors, and a getter-only property
|
|
378
|
+
// THROWS on assignment — `TypeError: Cannot set property requireBuilder of #<Object>
|
|
379
|
+
// which has only a getter`. Callers do assign: a test stubs `api.requireBuilder` to
|
|
380
|
+
// bypass auth, and the module loader composes gates the same way. That is a real
|
|
381
|
+
// part of this surface's contract, not an accident, so laziness must not cost it.
|
|
382
|
+
//
|
|
383
|
+
// Each accessor gains a setter that REPLACES itself with the assigned value — the
|
|
384
|
+
// property becomes an ordinary writable data property from then on, exactly as it
|
|
385
|
+
// was before, and the lazy require is simply never reached. Applied in a loop so a
|
|
386
|
+
// future lazy export inherits it without anyone remembering to.
|
|
387
|
+
function makeLazyPropsWritable(target) {
|
|
388
|
+
for (const key of Object.keys(target)) {
|
|
389
|
+
const d = Object.getOwnPropertyDescriptor(target, key);
|
|
390
|
+
if (!d) continue;
|
|
391
|
+
if (d.get && !d.set) {
|
|
392
|
+
Object.defineProperty(target, key, {
|
|
393
|
+
get: d.get,
|
|
394
|
+
set(value) {
|
|
395
|
+
Object.defineProperty(this, key, { value, writable: true, configurable: true, enumerable: d.enumerable });
|
|
396
|
+
},
|
|
397
|
+
configurable: true,
|
|
398
|
+
enumerable: d.enumerable,
|
|
399
|
+
});
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
// One level down, and ONLY through a data descriptor — reading `d.value` is
|
|
403
|
+
// free, whereas reading the property would fire a lazy getter and defeat the
|
|
404
|
+
// point. `projectSettings` is a nested namespace whose members are lazy too,
|
|
405
|
+
// and a settings test assigns `api.projectSettings.set`.
|
|
406
|
+
if (!d.get && d.value && typeof d.value === 'object' && Object.getPrototypeOf(d.value) === Object.prototype) {
|
|
407
|
+
makeLazyPropsWritable(d.value);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
makeLazyPropsWritable(module.exports);
|
package/tests/logger.mjs
CHANGED
|
@@ -11,8 +11,20 @@ import { fileURLToPath } from 'node:url';
|
|
|
11
11
|
|
|
12
12
|
const require = createRequire(import.meta.url);
|
|
13
13
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
14
|
+
|
|
15
|
+
// pino resolves as a BARE specifier, never as ROOT/node_modules/pino (task 1003616).
|
|
16
|
+
// An absolute path demands the directory exist at exactly that spot, so a fresh
|
|
17
|
+
// per-claim worktree with no install of its own reported a phantom FAIL; the bare
|
|
18
|
+
// form walks parent directories and finds the checkout the worktree lives under.
|
|
19
|
+
// Where nothing is installed at all — a package-only consumer — say so and skip,
|
|
20
|
+
// the same contract tests/hooks_drain.mjs uses for its own `pg` gate.
|
|
21
|
+
try { require.resolve('pino'); } catch {
|
|
22
|
+
console.log('logger: SKIP — pino is not installed in or above this checkout (run npm install).');
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
const log = require(path.join(ROOT, 'src', 'bongos', 'logger.js'));
|
|
15
|
-
const pino = require(
|
|
27
|
+
const pino = require('pino');
|
|
16
28
|
const { Writable } = require('node:stream');
|
|
17
29
|
|
|
18
30
|
// pino writes through sonic-boom directly to the fd, so we can't intercept the
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// tests/module_api_lazy.mjs — requiring the module doorway must not boot the
|
|
2
|
+
// server (task 1003677).
|
|
3
|
+
//
|
|
4
|
+
// src/module-api.js is the ONE file a module may import (ADR 0083). It used to
|
|
5
|
+
// require auth, the Postgres pool, the route helpers and project settings at
|
|
6
|
+
// module scope — so merely IMPORTING it opened a pool and loaded the whole auth
|
|
7
|
+
// stack as a side effect. Modules are imported by the CLI too (`bongos claim`
|
|
8
|
+
// reads modules/lifecycle/ship-card, `bongos start` reads task-classifier, each
|
|
9
|
+
// wanting `branding` and nothing else), so that side effect dragged 22 server
|
|
10
|
+
// files into every CLI subcommand — which is what made the CLI impossible to
|
|
11
|
+
// publish as a client-only npx package (goal 1000054) and left a builder with no
|
|
12
|
+
// private-registry credential unable to claim anything.
|
|
13
|
+
//
|
|
14
|
+
// The invariant: a kernel capability loads when it is READ, not when the doorway
|
|
15
|
+
// is imported. Measured against the real require.cache, because a static scan
|
|
16
|
+
// cannot tell an eager `require()` from one inside a getter — the text is
|
|
17
|
+
// identical, only the timing differs. Each case runs in its OWN child process:
|
|
18
|
+
// require.cache is per-process, and one eager import anywhere would poison the
|
|
19
|
+
// rest of the run.
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import { test } from 'node:test';
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
|
+
const HEAVY = /^src\/bongos\/(pool|auth|db|db-kernel|auth-[^/]*|routes\/)/;
|
|
28
|
+
|
|
29
|
+
// Load `spec` in a fresh process and report which src/bongos files ended up in
|
|
30
|
+
// the require cache. `read` optionally destructures a property first, proving the
|
|
31
|
+
// getter resolves without dragging its neighbours in.
|
|
32
|
+
function loadedAfter(spec, read = null) {
|
|
33
|
+
const script = `
|
|
34
|
+
const path = require('node:path');
|
|
35
|
+
const ROOT = ${JSON.stringify(ROOT)};
|
|
36
|
+
const m = require(path.join(ROOT, ${JSON.stringify(spec)}));
|
|
37
|
+
${read ? `void m[${JSON.stringify(read)}];` : ''}
|
|
38
|
+
const files = Object.keys(require.cache)
|
|
39
|
+
.map((f) => path.relative(ROOT, f))
|
|
40
|
+
.filter((f) => f.startsWith('src/bongos/'));
|
|
41
|
+
process.stdout.write(JSON.stringify(files));
|
|
42
|
+
`;
|
|
43
|
+
// spawnSync, not execFileSync: several subcommands print usage and exit
|
|
44
|
+
// non-zero when required with no arguments. That is the script behaving
|
|
45
|
+
// correctly, and the measurement (already written to stdout) is still valid —
|
|
46
|
+
// only a MISSING/unparseable payload is a real failure.
|
|
47
|
+
const r = spawnSync(process.execPath, ['-e', script], { encoding: 'utf8' });
|
|
48
|
+
const out = (r.stdout || '').trim();
|
|
49
|
+
if (!out) throw new Error(`probe produced nothing for ${spec} (exit ${r.status}): ${(r.stderr || '').slice(0, 200)}`);
|
|
50
|
+
return JSON.parse(out);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test('importing the doorway loads no server: no pool, no auth, no routes', () => {
|
|
54
|
+
const files = loadedAfter('src/module-api.js');
|
|
55
|
+
const heavy = files.filter((f) => HEAVY.test(f));
|
|
56
|
+
assert.deepEqual(heavy, [],
|
|
57
|
+
`require('src/module-api') pulled in ${heavy.join(', ')}. A new export whose value is a bare `
|
|
58
|
+
+ "`require('./bongos/x')` is EAGER — the object literal evaluates it at load. Put it behind "
|
|
59
|
+
+ '`get name() { return require(...); }` instead.');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('reading `branding` — what a CLI-imported module actually wants — stays cheap', () => {
|
|
63
|
+
const files = loadedAfter('src/module-api.js', 'branding');
|
|
64
|
+
assert.deepEqual(files.filter((f) => HEAVY.test(f)), [],
|
|
65
|
+
'reading one light export must not resolve the heavy ones');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('reading `pool` DOES load it — laziness must not mean broken', () => {
|
|
69
|
+
const files = loadedAfter('src/module-api.js', 'pool');
|
|
70
|
+
assert.ok(files.some((f) => f === 'src/bongos/pool.js'),
|
|
71
|
+
'the pool getter must still resolve the real pool when read');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// The getters replaced plain data properties, and a getter-only property THROWS on
|
|
75
|
+
// assignment. Callers do assign — a test stubs api.requireBuilder to bypass auth,
|
|
76
|
+
// and two suites (catalog_only_client, government_abuse_matrix) failed exactly that
|
|
77
|
+
// way on the first cut. Laziness must not cost assignability.
|
|
78
|
+
test('a lazy export is still assignable, and the stub sticks', async () => {
|
|
79
|
+
const { createRequire } = await import('node:module');
|
|
80
|
+
const require_ = createRequire(import.meta.url);
|
|
81
|
+
const api = require_(path.join(ROOT, 'src/module-api.js'));
|
|
82
|
+
const before = api.requireBuilder;
|
|
83
|
+
assert.equal(typeof before, 'function', 'the getter resolves the real thing first');
|
|
84
|
+
const stub = function stubbed() {};
|
|
85
|
+
api.requireBuilder = stub;
|
|
86
|
+
assert.equal(api.requireBuilder, stub, 'assignment must replace the accessor, not throw or no-op');
|
|
87
|
+
api.requireBuilder = before;
|
|
88
|
+
|
|
89
|
+
// And one level down: projectSettings is a nested namespace whose members are
|
|
90
|
+
// lazy too, and tests/project_settings_page.mjs assigns `.set`. The first fix
|
|
91
|
+
// walked only the top level and that suite failed nine ways.
|
|
92
|
+
const prevSet = api.projectSettings.set;
|
|
93
|
+
const stubSet = function stubbedSet() {};
|
|
94
|
+
api.projectSettings.set = stubSet;
|
|
95
|
+
assert.equal(api.projectSettings.set, stubSet, 'a nested lazy export must be assignable too');
|
|
96
|
+
api.projectSettings.set = prevSet;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('the two lifecycle modules the CLI imports boot no server', () => {
|
|
100
|
+
for (const spec of ['modules/lifecycle/ship-card.js', 'modules/lifecycle/task-classifier.js']) {
|
|
101
|
+
const heavy = loadedAfter(spec).filter((f) => HEAVY.test(f));
|
|
102
|
+
assert.deepEqual(heavy, [], `${spec} booted ${heavy.join(', ')} — the CLI imports this`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('every CLI subcommand loads without the server', () => {
|
|
107
|
+
for (const cmd of ['start', 'claim', 'ship', 'task', 'status', 'login', 'setup', 'release', 'cost', 'recall']) {
|
|
108
|
+
const heavy = loadedAfter(`scripts/gds/${cmd}.js`).filter((f) => HEAVY.test(f));
|
|
109
|
+
assert.deepEqual(heavy, [], `bongos ${cmd} booted ${heavy.join(', ')}`);
|
|
110
|
+
}
|
|
111
|
+
});
|