@bongos/core 1.20.72 → 1.20.73
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 +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/tests/module_assess_price_parity.mjs +174 -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.20.
|
|
6
|
-
"core_contract": "1.20.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.20.73",
|
|
6
|
+
"core_contract": "1.20.73",
|
|
7
|
+
"source_commit": "801daee11330f1b0e8072594fd9d007194d82ff7",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-01T15:
|
|
9
|
+
"built_at": "2026-10-01T15:43:17.489Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 572,
|
|
13
13
|
"agent_docs_stubbed": 27,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2801,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3401,
|
|
20
|
+
"tree_sha256": "00438ba77e638f59bbf2b7e542f64a5651f030603d42887ba570dad90d40e7b0",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2802,7 +2802,7 @@
|
|
|
2802
2802
|
{
|
|
2803
2803
|
"path": "docs/module-api-changelog.md",
|
|
2804
2804
|
"mode": "0000644",
|
|
2805
|
-
"sha256": "
|
|
2805
|
+
"sha256": "a1158e3a1bb0e3daf32f0ec0592b3956071f4eaf712a567ddb702c52392e4b69"
|
|
2806
2806
|
},
|
|
2807
2807
|
{
|
|
2808
2808
|
"path": "docs/modules-contract.md",
|
|
@@ -9457,12 +9457,12 @@
|
|
|
9457
9457
|
{
|
|
9458
9458
|
"path": "package-lock.json",
|
|
9459
9459
|
"mode": "0000644",
|
|
9460
|
-
"sha256": "
|
|
9460
|
+
"sha256": "f383e88aef6668d55f149d2df43507dbbd0596d7d91f40b2027e29588a8b7bb6"
|
|
9461
9461
|
},
|
|
9462
9462
|
{
|
|
9463
9463
|
"path": "package.json",
|
|
9464
9464
|
"mode": "0000644",
|
|
9465
|
-
"sha256": "
|
|
9465
|
+
"sha256": "774a78ab94f32c857b8de59046bf54910f743c6e7c2e78be80d8019029dd4cb2"
|
|
9466
9466
|
},
|
|
9467
9467
|
{
|
|
9468
9468
|
"path": "public-docs/index.html",
|
|
@@ -9482,7 +9482,7 @@
|
|
|
9482
9482
|
{
|
|
9483
9483
|
"path": "release-notes.json",
|
|
9484
9484
|
"mode": "0000644",
|
|
9485
|
-
"sha256": "
|
|
9485
|
+
"sha256": "e63c13e0213b9008d28ee2c938a5a9ac2b280e2a20e8aec1fb8d9ce92791ed2b"
|
|
9486
9486
|
},
|
|
9487
9487
|
{
|
|
9488
9488
|
"path": "scripts/bongos-mcp.js",
|
|
@@ -11677,7 +11677,7 @@
|
|
|
11677
11677
|
{
|
|
11678
11678
|
"path": "src/module-api.js",
|
|
11679
11679
|
"mode": "0000644",
|
|
11680
|
-
"sha256": "
|
|
11680
|
+
"sha256": "73fdbcf183fb1dd260fbb16ea0a5f1dfe053d1f06d53b31d0af695f80ec28cc8"
|
|
11681
11681
|
},
|
|
11682
11682
|
{
|
|
11683
11683
|
"path": "src/module-loader/catalog.js",
|
|
@@ -14734,6 +14734,11 @@
|
|
|
14734
14734
|
"mode": "0000644",
|
|
14735
14735
|
"sha256": "c3c74387af6972760f6701efb80aaa588634965528d79d4ca881bca051e534e5"
|
|
14736
14736
|
},
|
|
14737
|
+
{
|
|
14738
|
+
"path": "tests/module_assess_price_parity.mjs",
|
|
14739
|
+
"mode": "0000644",
|
|
14740
|
+
"sha256": "78f36a8bbbd948c4485dfd80df55e1ee053af1b510b4d98607c0d2545bcfa66c"
|
|
14741
|
+
},
|
|
14737
14742
|
{
|
|
14738
14743
|
"path": "tests/module_assess_score.mjs",
|
|
14739
14744
|
"mode": "0000644",
|
|
@@ -2801,5 +2801,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2801
2801
|
landed since 1.20.70 with no explicit bump. run 36881290177. (task 1002620)
|
|
2802
2802
|
1.20.72 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2803
2803
|
landed since 1.20.71 with no explicit bump. run 36884685441. (task 1002620)
|
|
2804
|
+
1.20.73 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2805
|
+
landed since 1.20.72 with no explicit bump. run 36886320087. (task 1002620)
|
|
2804
2806
|
---------------------------------------------------------------------------
|
|
2805
2807
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.73",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.73",
|
|
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.20.
|
|
3
|
+
"version": "1.20.73",
|
|
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/release-notes.json
CHANGED
|
@@ -8561,5 +8561,11 @@
|
|
|
8561
8561
|
"id": "1004367",
|
|
8562
8562
|
"text": "Every module published to the store now gets its how-to guide read and scored by AI for clarity, with a short reason for each part so authors know what to fix. If the AI can't run, the guide shows 'not scored' rather than a ze"
|
|
8563
8563
|
}
|
|
8564
|
+
],
|
|
8565
|
+
"1.20.73": [
|
|
8566
|
+
{
|
|
8567
|
+
"id": "1003795",
|
|
8568
|
+
"text": "There is now an automatic check that free modules are scored exactly like paid ones. If anyone ever makes price affect a module's score, the check fails and points at the line."
|
|
8569
|
+
}
|
|
8564
8570
|
]
|
|
8565
8571
|
}
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.20.
|
|
78
|
+
const CORE_VERSION = '1.20.73'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// tests/module_assess_price_parity.mjs — free modules are assessed identically to
|
|
2
|
+
// paid ones: price neither buys nor excuses a score (task 1003795; owner decision
|
|
3
|
+
// on criterion wa5-quality-assessed; ADR 0343 D6).
|
|
4
|
+
//
|
|
5
|
+
// Two proofs, like the update-parity test proves its own criterion:
|
|
6
|
+
// 1. STATIC — no file on the assessment path (scripts/gds/module-assess-*.js)
|
|
7
|
+
// names a price, a currency amount, free/paid or an entitlement in its code
|
|
8
|
+
// (comments are allowed to explain the rule). A price-conditional branch
|
|
9
|
+
// added anywhere there fails this test, naming the file and line.
|
|
10
|
+
// 2. BEHAVIOUR — the same signals give the same score for a paid version and
|
|
11
|
+
// for the three ways a module can be free: a zero price, no price set, and a
|
|
12
|
+
// core-bundled module. The fake pool holds every case's price state and
|
|
13
|
+
// refuses any query that reads it.
|
|
14
|
+
//
|
|
15
|
+
// Run: node tests/module_assess_price_parity.mjs
|
|
16
|
+
|
|
17
|
+
import { strict as assert } from 'node:assert';
|
|
18
|
+
import { test } from 'node:test';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const { composeScore, recomposeScore } = require('../scripts/gds/module-assess-score.js');
|
|
27
|
+
const { assessVersion } = require('../scripts/gds/module-assess-version.js');
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// 1. static — the assessment path never reads price
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const PRICE_WORD = /\b(price\w*|isFree\w*|free|paid|cents|credits|store_module_prices|entitlement\w*|purchase\w*)\b/i;
|
|
34
|
+
|
|
35
|
+
// The code lines of a JS source, comments blanked (line numbers kept).
|
|
36
|
+
function codeLines(src) {
|
|
37
|
+
const noBlock = src.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
|
|
38
|
+
return noBlock.split('\n').map((l) => l.replace(/(^|[^:'"`\\])\/\/.*$/, '$1'));
|
|
39
|
+
}
|
|
40
|
+
function priceHits(src) {
|
|
41
|
+
return codeLines(src).flatMap((l, i) => (PRICE_WORD.test(l) ? [`${i + 1}: ${l.trim()}`] : []));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ASSESS_DIR = path.join(root, 'scripts', 'gds');
|
|
45
|
+
const assessFiles = fs.readdirSync(ASSESS_DIR).filter((f) => /^module-assess-.*\.js$/.test(f)).sort();
|
|
46
|
+
|
|
47
|
+
test('the scan sees the whole assessment path', () => {
|
|
48
|
+
for (const f of ['module-assess-score.js', 'module-assess-security.js', 'module-assess-tests.js', 'module-assess-version.js']) {
|
|
49
|
+
assert.ok(assessFiles.includes(f), `${f} is on the assessment path`);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('the scanner catches a price branch and ignores a comment about price', () => {
|
|
54
|
+
assert.equal(priceHits('// Nothing here reads price: free and paid are assessed identically\nconst x = 1;').length, 0);
|
|
55
|
+
assert.equal(priceHits('/* free modules\n are not excused */ const y = 2;').length, 0);
|
|
56
|
+
assert.deepEqual(priceHits('const a = 1;\nif (ver.price_cents === 0) score += 10;'), ['2: if (ver.price_cents === 0) score += 10;']);
|
|
57
|
+
assert.equal(priceHits("pool.query('SELECT * FROM store_module_prices')").length, 1);
|
|
58
|
+
assert.equal(priceHits('const bonus = isFreePrice(p) ? 0 : 5;').length, 1);
|
|
59
|
+
assert.equal(priceHits("const u = 'https://x.test/a'; if (paid) x();").length, 1, 'a URL in a string does not hide the rest of the line');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('no assessment file names a price in its code', () => {
|
|
63
|
+
const found = assessFiles.flatMap((f) => priceHits(fs.readFileSync(path.join(ASSESS_DIR, f), 'utf8')).map((h) => `scripts/gds/${f}:${h}`));
|
|
64
|
+
assert.deepEqual(found, [], `price must never gate or excuse a score (ADR 0343 D6):\n${found.join('\n')}`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// 2. behaviour — paid, zero-price, no-price and core-bundled score the same
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
const CASES = [
|
|
72
|
+
{ id: 1, module_key: 'paid-mod', version: '1.0.0', price: { price_cents: 500, price_credits: 50 } },
|
|
73
|
+
{ id: 2, module_key: 'zero-mod', version: '1.0.0', price: { price_cents: 0, price_credits: 0 } },
|
|
74
|
+
{ id: 3, module_key: 'unpriced-mod', version: '1.0.0', price: null },
|
|
75
|
+
{ id: 4, module_key: 'economy', version: '1.0.0', price: null, core_bundled: true },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
// core_265's append-only rows in memory, every case's price state alongside, and
|
|
79
|
+
// a refusal for any query that reads price.
|
|
80
|
+
function fakeDb() {
|
|
81
|
+
const signals = [];
|
|
82
|
+
const scores = [];
|
|
83
|
+
const sqlSeen = [];
|
|
84
|
+
let clock = 0;
|
|
85
|
+
const db = {
|
|
86
|
+
signals, scores, sqlSeen,
|
|
87
|
+
async query(sql, params = []) {
|
|
88
|
+
sqlSeen.push(sql);
|
|
89
|
+
if (PRICE_WORD.test(sql)) throw new Error(`the assessment read price: ${sql.slice(0, 120)}`);
|
|
90
|
+
if (/^SELECT id, module_key, version FROM store_module_versions WHERE id/.test(sql)) {
|
|
91
|
+
const v = CASES.find((c) => String(c.id) === String(params[0]));
|
|
92
|
+
return { rows: v ? [{ id: v.id, module_key: v.module_key, version: v.version }] : [] };
|
|
93
|
+
}
|
|
94
|
+
if (/SELECT DISTINCT ON \(part\)/.test(sql)) {
|
|
95
|
+
const cur = {};
|
|
96
|
+
for (const s of signals.filter((x) => String(x.version_id) === String(params[0]))) {
|
|
97
|
+
if (!cur[s.part] || s.t > cur[s.part].t || (s.t === cur[s.part].t && s.id > cur[s.part].id)) cur[s.part] = s;
|
|
98
|
+
}
|
|
99
|
+
return { rows: Object.values(cur) };
|
|
100
|
+
}
|
|
101
|
+
if (/FROM module_assessment_scores/.test(sql) && /^SELECT/.test(sql.trim())) {
|
|
102
|
+
const mine = scores.filter((x) => String(x.version_id) === String(params[0]) && x.kind === 'computed');
|
|
103
|
+
return { rows: mine.length ? [mine[mine.length - 1]] : [] };
|
|
104
|
+
}
|
|
105
|
+
if (/INSERT INTO module_assessment_scores/.test(sql)) {
|
|
106
|
+
const row = { id: scores.length + 1, version_id: params[0], kind: 'computed', overall: params[1], security_passed: params[2], is_new: params[3], formula: params[4], signal_ids: params[5].map(String) };
|
|
107
|
+
scores.push(row); return { rows: [row] };
|
|
108
|
+
}
|
|
109
|
+
if (/INSERT INTO module_assessment_signals/.test(sql) && /'pending'/.test(sql)) {
|
|
110
|
+
if (signals.some((s) => String(s.version_id) === String(params[0]) && s.part === 'tests')) return { rows: [] };
|
|
111
|
+
signals.push({ id: signals.length + 1, version_id: params[0], part: 'tests', outcome: 'pending', score: null, t: ++clock });
|
|
112
|
+
return { rows: [] };
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`unexpected SQL: ${sql.slice(0, 80)}`);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
db.add = (versionId, part, outcome, score = null) => {
|
|
118
|
+
const row = { id: signals.length + 1, version_id: versionId, part, outcome, score, t: ++clock };
|
|
119
|
+
signals.push(row); return row;
|
|
120
|
+
};
|
|
121
|
+
return db;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The answer with the version id taken out, so cases can be compared.
|
|
125
|
+
const shape = (s) => ({ overall: s.overall, security_passed: s.security_passed, is_new: s.is_new, formula: s.formula });
|
|
126
|
+
|
|
127
|
+
test('composeScore takes no price: the same signals give the same score whatever is attached', () => {
|
|
128
|
+
const signals = {
|
|
129
|
+
security: { id: 1, part: 'security', outcome: 'passed', score: null },
|
|
130
|
+
tests: { id: 2, part: 'tests', outcome: 'scored', score: 70 },
|
|
131
|
+
install: { id: 3, part: 'install', outcome: 'scored', score: 90 },
|
|
132
|
+
};
|
|
133
|
+
const base = composeScore(signals);
|
|
134
|
+
for (const c of CASES) {
|
|
135
|
+
// Price riding along on the rows must change nothing.
|
|
136
|
+
const withPrice = Object.fromEntries(Object.entries(signals).map(([k, r]) => [k, { ...r, ...(c.price || {}), core_bundled: !!c.core_bundled }]));
|
|
137
|
+
assert.deepEqual(composeScore(withPrice), base, c.module_key);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
for (const [label, signalsFor] of [
|
|
142
|
+
['scored parts', (db, id) => { db.add(id, 'security', 'passed'); db.add(id, 'tests', 'scored', 64); db.add(id, 'reliability', 'scored', 88); }],
|
|
143
|
+
['a failed gate', (db, id) => { db.add(id, 'security', 'failed'); db.add(id, 'tests', 'scored', 99); }],
|
|
144
|
+
['no data yet', (db, id) => { db.add(id, 'tests', 'no_data'); }],
|
|
145
|
+
]) {
|
|
146
|
+
test(`recompose: paid, zero-price, no-price and core-bundled score the same (${label})`, async () => {
|
|
147
|
+
const db = fakeDb();
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const c of CASES) {
|
|
150
|
+
signalsFor(db, c.id);
|
|
151
|
+
const res = await recomposeScore(c.id, { db });
|
|
152
|
+
out.push([c.module_key, shape(res.score)]);
|
|
153
|
+
}
|
|
154
|
+
for (const [key, s] of out.slice(1)) assert.deepEqual(s, out[0][1], `${key} scores exactly like the paid module`);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
test('assessVersion on publish runs the same steps for every case, and never reads price', async () => {
|
|
159
|
+
const db = fakeDb();
|
|
160
|
+
const steps = [];
|
|
161
|
+
const recordSecurity = async (id, { db: d }) => { steps.push(['security', id]); return { ok: true, signal: d.add(id, 'security', 'passed') }; };
|
|
162
|
+
const recordDocs = async (id, { db: d }) => { steps.push(['docs', id]); return { ok: true, signal: d.add(id, 'docs', 'scored', 50) }; };
|
|
163
|
+
const out = [];
|
|
164
|
+
for (const c of CASES) {
|
|
165
|
+
const res = await assessVersion(c.id, { db, recordSecurity, recordDocs });
|
|
166
|
+
assert.equal(res.ok, true, c.module_key);
|
|
167
|
+
out.push(shape(res.score));
|
|
168
|
+
}
|
|
169
|
+
for (const s of out.slice(1)) assert.deepEqual(s, out[0]);
|
|
170
|
+
const perCase = (id) => steps.filter(([, v]) => v === id).map(([k]) => k).join(',');
|
|
171
|
+
for (const c of CASES.slice(1)) assert.equal(perCase(c.id), perCase(CASES[0].id), `${c.module_key} goes through the same checks`);
|
|
172
|
+
assert.ok(db.sqlSeen.length > 0);
|
|
173
|
+
assert.ok(db.sqlSeen.every((q) => !PRICE_WORD.test(q)), 'no assessment query touched price');
|
|
174
|
+
});
|