@bongos/core 1.21.151 → 1.21.153
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 +73 -58
- package/docs/copy-inventory.md +323 -317
- package/docs/copy-registry.json +423 -369
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +30 -0
- package/docs/page-readings.json +234 -227
- package/modules/agents/module.json +1 -0
- package/modules/autonomy/module.json +1 -0
- package/modules/builder-settings/module.json +1 -0
- package/modules/copy-desk/module.json +1 -0
- package/modules/design-styles/module.json +1 -0
- package/modules/discord/module.json +1 -0
- package/modules/economy/module.json +1 -0
- package/modules/government/module.json +1 -0
- package/modules/grading/module.json +1 -0
- package/modules/hall-ui/module.json +1 -0
- package/modules/hall-ui/public/modules.html +13 -0
- package/modules/hall-ui/public/modules.js +101 -4
- package/modules/hall-ui/public/modules.states.json +2 -1
- package/modules/hall-ui/records/catalog.md +2 -0
- package/modules/ideas/module.json +1 -0
- package/modules/lifecycle/github-failure.js +50 -0
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/routes/tasks.js +2 -2
- package/modules/memory/module.json +1 -0
- package/modules/npm-release/module.json +1 -0
- package/modules/onboarding/module.json +1 -0
- package/modules/page-notes/module.json +1 -0
- package/modules/pixel-art/module.json +1 -0
- package/modules/platform-identity/module.json +1 -0
- package/modules/provisioning/github-setup.js +51 -1
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/routes/provisioning.js +7 -2
- package/modules/public-landing/module.json +1 -0
- package/modules/public-landing/public/projects.html +33 -0
- package/modules/render-deploy/module.json +1 -0
- package/modules/security/module.json +1 -0
- package/modules/sessions/module.json +1 -0
- package/modules/specialities/module.json +1 -0
- package/modules/status-ui/module.json +1 -0
- package/modules/ui-design/module.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/scripts/gds/module.js +4 -2
- package/src/bongos/module-listing.js +76 -2
- package/src/module-api.js +1 -1
- package/src/module-loader/catalog.js +15 -1
- package/src/module-loader/manifest-schema.js +40 -1
- package/tests/github_failure.mjs +79 -0
- package/tests/github_install_nudge.mjs +219 -0
- package/tests/hall_catalog_world.mjs +23 -3
- package/tests/module_catalog.mjs +34 -0
- package/tests/module_manifest.mjs +36 -0
- package/tests/module_store_listing.mjs +78 -1
|
@@ -1140,6 +1140,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1140
1140
|
stripFor/errMarkup/canRetry/canRevive the list row reads -->
|
|
1141
1141
|
<div id="mStrip"></div>
|
|
1142
1142
|
<div class="projMsg err" id="mErr" hidden></div>
|
|
1143
|
+
<div class="projMsg" id="mGh" hidden></div>
|
|
1143
1144
|
<div class="projMsg" id="mMsg" role="status" aria-live="polite"></div>
|
|
1144
1145
|
<div class="actions" id="mActs" hidden></div>
|
|
1145
1146
|
</div>
|
|
@@ -8357,6 +8358,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
8357
8358
|
(renderCard) — a project the owner is being told about cannot sit under
|
|
8358
8359
|
Live. The intent-vs-status contract is deliberately left alone. */
|
|
8359
8360
|
if (inst.error_note && !inst.open_intent) return 'attention';
|
|
8361
|
+
/* an app nobody installed blocks every ship until the owner acts (task 1004763) */
|
|
8362
|
+
if (ghInstallRow(inst)) return 'attention';
|
|
8360
8363
|
return STATE_GROUP[inst.status] || 'all';
|
|
8361
8364
|
}
|
|
8362
8365
|
function findText(inst) {
|
|
@@ -8828,6 +8831,21 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
8828
8831
|
'">Say what it is</a> and it goes on.';
|
|
8829
8832
|
}
|
|
8830
8833
|
|
|
8834
|
+
/* the standing GitHub install nudge (task 1004763). A project whose app was made but never
|
|
8835
|
+
installed on its repo cannot have finished work added: the platform cannot read the
|
|
8836
|
+
repo, so every ship stops short. The wizard's checklist shows only while a project is
|
|
8837
|
+
being made, so the card and the project's own page say it, with GitHub's own install
|
|
8838
|
+
link, until the runner has seen the install. The click also asks for a fresh look
|
|
8839
|
+
(the a[data-gh-install] handler below). '' when there is nothing to install. */
|
|
8840
|
+
function ghInstallRow(inst) {
|
|
8841
|
+
var g = inst && inst.github_install;
|
|
8842
|
+
if (!g || !/^https:\/\/github\.com\/apps\//.test(String(g.install_url || ''))) return '';
|
|
8843
|
+
if (inst.status === 'torn_down' || inst.status === 'tearing_down') return '';
|
|
8844
|
+
return 'Finished work can’t be added to ' + (g.repo ? '<code>' + esc(g.repo) + '</code>' : 'its repo') +
|
|
8845
|
+
' until the GitHub App is installed on it. <a class="btn small" href="' + esc(g.install_url) +
|
|
8846
|
+
'" target="_blank" rel="noopener" data-gh-install="' + esc(String(inst.id)) + '">Install the GitHub App ↗</a>';
|
|
8847
|
+
}
|
|
8848
|
+
|
|
8831
8849
|
function renderCard(inst) {
|
|
8832
8850
|
/* iid is for MARKUP interpolation only (esc() = HTML escape); the DOM
|
|
8833
8851
|
lookup goes through liFor(), which CSS-escapes — the two escapes are
|
|
@@ -8851,6 +8869,8 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
8851
8869
|
if (addr) body += '<div class="addr">' + addr + '</div>';
|
|
8852
8870
|
var pub = publishRow(inst);
|
|
8853
8871
|
if (pub) body += '<div class="projMsg">' + pub + '</div>';
|
|
8872
|
+
var gh = ghInstallRow(inst);
|
|
8873
|
+
if (gh) body += '<div class="projMsg">' + gh + '</div>';
|
|
8854
8874
|
if ((inst.status === 'error' || inst.error_note) && !inst.open_intent) {
|
|
8855
8875
|
/* once a retry is queued the strip tells the story — the stale error
|
|
8856
8876
|
paragraph would just contradict it (owner walk, 2026-08-12). Also
|
|
@@ -9198,6 +9218,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
9198
9218
|
var snag = (inst.status === 'error' || inst.error_note) && !inst.open_intent;
|
|
9199
9219
|
$('mErr').innerHTML = snag ? errMarkup(inst) : '';
|
|
9200
9220
|
$('mErr').hidden = !snag;
|
|
9221
|
+
var gh = ghInstallRow(inst);
|
|
9222
|
+
$('mGh').innerHTML = gh;
|
|
9223
|
+
$('mGh').hidden = !gh;
|
|
9201
9224
|
var acts = [];
|
|
9202
9225
|
if (inst.status === 'active' && addressReady(inst)) acts.push('<a class="btn small ghost" href="https://' + esc(inst.domain) + '" target="_blank" rel="noopener">Open site →</a>');
|
|
9203
9226
|
if (canRetry(inst)) acts.push('<button type="button" class="btn small" data-m-retry>Try again</button>');
|
|
@@ -11231,6 +11254,16 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
11231
11254
|
return e.message || 'The restart could not be queued — try again in a moment.';
|
|
11232
11255
|
}
|
|
11233
11256
|
|
|
11257
|
+
/* the install nudge's click (task 1004763), on the card and the project's own page alike:
|
|
11258
|
+
GitHub opens in its own tab as the link says, and the platform is asked to look again.
|
|
11259
|
+
GitHub's hand-back from this link carries no attempt, so without the ask nothing would
|
|
11260
|
+
re-check and the nudge would never clear. Best-effort: a refused ask changes nothing. */
|
|
11261
|
+
document.addEventListener('click', function (ev) {
|
|
11262
|
+
var a = ev.target.closest && ev.target.closest('a[data-gh-install]');
|
|
11263
|
+
if (!a) return;
|
|
11264
|
+
fetch(API + '/provisioning/instances/' + encodeURIComponent(a.getAttribute('data-gh-install')) + '/github-app/pick-up', { method: 'POST', credentials: 'same-origin' }).catch(function () {});
|
|
11265
|
+
});
|
|
11266
|
+
|
|
11234
11267
|
/* the list's one delegated handler. A row holds no sender since task 1003355
|
|
11235
11268
|
— only its two doors — so the Manage link is all there is to upgrade: it is
|
|
11236
11269
|
a real anchor, a modified click keeps its browser meaning and a plain one
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"description": "The deploy page for a project whose app deploys through Render: the service's Render deploys, each tied to its commit and the Bongos tasks it carried, what is live now, and deploy and roll back behind a two-step confirm (task 1004270). Off in every hall until its owner turns it on.",
|
|
5
5
|
"version": "1.0.0",
|
|
6
6
|
"coreVersion": "^1.19.0",
|
|
7
|
+
"category": { "parent": "hosting" },
|
|
7
8
|
"default": false,
|
|
8
9
|
"author": "Cloud Bongos",
|
|
9
10
|
"origin": "Cloud Bongos core",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"description": "The red-team / vulnerability-report intake and the bounty payout system (rank-gated Metic+): the /builder-redteam skill files a report, the Archon dashboard adjudicates (confirm/dispute/fix/link-fix), and the env-aware bounty table sets the per-severity drachmae payout. A core-domain module carved out of the GDS core in BV1.R79 (ADR 0093 §1; 02-module-map §5 — security-the-FEATURE, NOT security-the-property, which stays kernel). default-on because every instance can take red-team reports. Registers the `security` kernel PORT the core /public/bounty-table resolves for the payout schedule. Also owns the scan-before-install surface (goal 1000055): the artifact-scan engine (quarantine/manifest/mirror) and the security_tool_scans record — POST Metic+, GET every rank.",
|
|
5
5
|
"version": "1.0.0",
|
|
6
6
|
"coreVersion": "^1.8.0",
|
|
7
|
+
"category": { "parent": "quality" },
|
|
7
8
|
"default": true,
|
|
8
9
|
"maintenance": { "status": "core-maintained" },
|
|
9
10
|
"contributes": { "routes": ["reports", "scans"], "migrations": true },
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"description": "The session-records corpus + its data plane (POST /sessions upload, the Archon-gated cross-builder /sessions/search + /sessions/:id deep-dive + /sessions/upload-health, the server-side signal that a builder's session uploads have silently stopped), the BFG 'read-the-room' cross-builder scorer (ADR 0026 §6B), and the session-pulse / stale-claim heartbeat helpers. A core-domain module carved out of the GDS core in BV1.R82 (ADR 0093 §1); default-on because every instance records its builders' sessions for the BFG. Like the memory module, sessions registers no port — core never calls back into it: the ship-time digest write is an HTTP POST to the module's own /sessions route, and the only outward dependency is the economy `reward` kernel PORT it resolves (session token reward + sumCreditsForTasks) at upload time.",
|
|
5
5
|
"version": "1.0.0",
|
|
6
6
|
"coreVersion": "^1.10.0",
|
|
7
|
+
"category": { "parent": "knowledge" },
|
|
7
8
|
"default": true,
|
|
8
9
|
"maintenance": { "status": "core-maintained" },
|
|
9
10
|
"contributes": { "routes": ["sessions"], "pollers": ["session-reward-reconcile", "session-upload-health"] },
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"description": "Role SUB-SPECIALITIES — a named bundle of knowledge, skills and an interaction contract that sits UNDER a craft (task 1003855; ADR 0296). A craft says how you work (engineer / ideator / artist / ui); a speciality says what you work ON and how a subject-matter expert wants to be engaged inside it. Deliberately a thin layer over three surfaces that already exist — discipline-modes.json for craft->playbook, behavior_prefs.interaction for the per-session contract (a speciality contract rides that SAME GET /me -> Conductor path rather than adding a second one), and the ADR 0083 manifest for declared skills; the only new machinery is the knowledge bundle. Training is two-fold per the owner's 2026-09-17 ruling: documents the builder supplied, plus learnings captured from sessions worked in the speciality (pointing at existing `learnings` rows, never a second corpus). Owned per-builder OR per-project, each carrying its owner's sharing decision (private / project / public) and an explicit sellable flag — the flag records intent only, because ADR 0107 defers the marketplace and there is nothing to list on. default-on: a speciality is how an instance's expertise enters the platform, so it ships like the hall and the status page.",
|
|
5
5
|
"version": "1.0.0",
|
|
6
6
|
"coreVersion": "^1.19.0",
|
|
7
|
+
"category": { "parent": "people" },
|
|
7
8
|
"default": true,
|
|
8
9
|
"maintenance": { "status": "core-maintained" },
|
|
9
10
|
"migrations": true,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"description": "The public build-status dashboard served at status.<apex> (status.example.com) — the live up/down banner, version-progress rollups, cost summary, recent ships, leaderboard, and the ecosystem fingerprint, all polling the read-only /api/bongos/public/* surface every 60s. A WEB-SURFACE core-domain module carved out of the GDS core in BV1.R84 (ADR 0093 §1; 02-module-map §6 — a web surface is first-class). default-on: a Cloud Bongos instance ships its status page like it ships its hall. Registers no port and contributes no routes — it declares a webSurface (host prefix + static dir) the loader hands serve-internal, which serves the module's public/ dir on the matching host exactly as it served public-status/ before. The public read endpoints it renders stay SHARED core in routes/public.js (the hall + the off-box mirror read them too).",
|
|
5
5
|
"version": "1.0.0",
|
|
6
6
|
"coreVersion": "^1.11.0",
|
|
7
|
+
"category": { "parent": "pages" },
|
|
7
8
|
"default": true,
|
|
8
9
|
"maintenance": { "status": "core-maintained" },
|
|
9
10
|
"contributes": {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
"description": "The platform capability that lets any builder on any instance create and design good-looking, functional UIs in line with that instance's world (scoped with the owner 2026-08-28; task 1003324, ADR 0197). It ships METHOD and never a world: the neutral design contract of ADR 0081 with its tool adapters (Claude Design, Figma), their sync CLIs and the validator; the /design playbook for ui-discipline work; the look-before-you-ship kit (kit/) and the style library (styles/). The thirteen design-style skills that lived in skills/ are the opt-in design-styles module since task 1004470; /design reaches them when that module is on. The instance's world — its branding pack, config/design-tokens.*, config/design-sources.json and its DESIGN.md — stays in config/ and at the repo root as the host boundary this module reads and never owns (ADR 0062 §7). Cloud Bongos's own front door is this module's first customer, not its owner. default-on because every instance has a face. No routes, no migrations, no port: declaration-only like character-anim, so disabling it withdraws only the skills it contributes and touches no served surface.",
|
|
5
5
|
"version": "1.0.0",
|
|
6
6
|
"coreVersion": "^1.19.0",
|
|
7
|
+
"category": { "parent": "pages" },
|
|
7
8
|
"default": true,
|
|
8
9
|
"maintenance": { "status": "core-maintained" },
|
|
9
10
|
"contributes": { "skills": ["design", "design-sync", "figma-design-sync"], "disciplines": ["ui"] },
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.153",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.153",
|
|
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.21.
|
|
3
|
+
"version": "1.21.153",
|
|
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
|
@@ -9920,5 +9920,25 @@
|
|
|
9920
9920
|
"id": "1004739",
|
|
9921
9921
|
"text": "The module test runner can now test against the private core: the server sends its own copy of the core into the private test repo, once per version."
|
|
9922
9922
|
}
|
|
9923
|
+
],
|
|
9924
|
+
"1.21.152": [
|
|
9925
|
+
{
|
|
9926
|
+
"id": "1003784",
|
|
9927
|
+
"text": "Every module now says which of the eight categories it belongs to, and the module list in the terminal and the hall's Modules tab both show it and let you search by it."
|
|
9928
|
+
}
|
|
9929
|
+
],
|
|
9930
|
+
"1.21.153": [
|
|
9931
|
+
{
|
|
9932
|
+
"id": "1004763",
|
|
9933
|
+
"text": "Project owners whose GitHub App was never installed now see an \"Install the GitHub App\" button on their project until GitHub confirms it."
|
|
9934
|
+
},
|
|
9935
|
+
{
|
|
9936
|
+
"id": "1003799",
|
|
9937
|
+
"text": "Two store modules with the same score no longer look identical: each now shows what it is strong at and weak at (tests, installs, reliability, testers, how-to), in the store listing and in the hall's Modules tab."
|
|
9938
|
+
},
|
|
9939
|
+
{
|
|
9940
|
+
"id": "1004766",
|
|
9941
|
+
"text": "When a project's server can't talk to GitHub, the land-status check now says which step failed and what GitHub answered, instead of a bare error."
|
|
9942
|
+
}
|
|
9923
9943
|
]
|
|
9924
9944
|
}
|
package/scripts/gds/module.js
CHANGED
|
@@ -452,14 +452,16 @@ function formatCatalog(rows, { catalogOnly = false, query = '', core = null, del
|
|
|
452
452
|
return out.join('\n');
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
out.push(` ${pad('', 5)}${pad('KEY', 20)}${pad('VERSION', 9)}${pad('SOURCE', 10)}${pad('AUTHOR', 20)}${pad('ORIGIN', 26)}${pad('LICENSE', 12)}UPKEEP`);
|
|
455
|
+
out.push(` ${pad('', 5)}${pad('KEY', 20)}${pad('VERSION', 9)}${pad('SOURCE', 10)}${pad('CATEGORY', 28)}${pad('AUTHOR', 20)}${pad('ORIGIN', 26)}${pad('LICENSE', 12)}UPKEEP`);
|
|
456
456
|
for (const r of shown) {
|
|
457
457
|
const mark = r.enabled ? '[x]' : '[ ]';
|
|
458
458
|
// An inferred credit is marked so it never reads as something the author
|
|
459
459
|
// typed — the catalog's whole job is honest attribution (ADR 0107 §2).
|
|
460
460
|
const suffix = r.credit.inferred ? '*' : '';
|
|
461
461
|
const upkeep = ((r.maintenance && r.maintenance.status) || 'undeclared') + (delisted.has(r.key) ? ' · DELISTED' : '');
|
|
462
|
-
|
|
462
|
+
// The parent key, plus the sub-category once one is approved (ADR 0342).
|
|
463
|
+
const cat = r.category ? r.category.parent + (r.category.sub ? `/${r.category.sub}` : '') : '—';
|
|
464
|
+
out.push(` ${pad(mark, 5)}${pad(clip(r.key, 19), 20)}${pad(clip(r.version || '—', 8), 9)}${pad(r.source, 10)}${pad(clip(cat, 27), 28)}${pad(clip((r.credit.author || '—') + suffix, 19), 20)}${pad(clip((r.credit.origin || '—') + suffix, 25), 26)}${pad(clip(r.license || '—', 11), 12)}${upkeep}`);
|
|
463
465
|
}
|
|
464
466
|
out.push('');
|
|
465
467
|
out.push(' [x] enabled here · [ ] available, not enabled');
|
|
@@ -35,6 +35,63 @@ function placeVersion(score, floor = QUALITY_FLOOR) {
|
|
|
35
35
|
return { placement: 'listed', reason: 'at_or_above_floor' };
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Strengths and weaknesses (task 1003799, ADR 0343 D4)
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
//
|
|
42
|
+
// The overall number sorts the listing; the parts explain it. Two modules that both
|
|
43
|
+
// score 74 can be good at different things, and a buyer who cares about reliability
|
|
44
|
+
// should be able to see which one is. So each listed entry carries its parts and a
|
|
45
|
+
// short reading of them:
|
|
46
|
+
//
|
|
47
|
+
// strength a part scored at or above STRONG_AT
|
|
48
|
+
// weakness a part scored below the floor — the same bar the listing uses
|
|
49
|
+
// unmeasured a part with no score yet (no installs, no testers, still running).
|
|
50
|
+
// Never a weakness: a missing part is never a zero (D3/D5).
|
|
51
|
+
//
|
|
52
|
+
// Security is not on the list: it is a pass/fail gate (D2), and every listed
|
|
53
|
+
// version has passed it. The thresholds are fixed numbers on purpose, so an author
|
|
54
|
+
// can check why a part reads as strong or weak, the same way the floor works.
|
|
55
|
+
const STRONG_AT = 80;
|
|
56
|
+
|
|
57
|
+
// The parts a buyer compares, in the order they are shown, with what their
|
|
58
|
+
// sample size counts. `docs` is shown but never averaged (ADR 0347).
|
|
59
|
+
const PARTS = [
|
|
60
|
+
{ part: 'tests', title: 'Tests', counts: 'test cases' },
|
|
61
|
+
{ part: 'install', title: 'Clean installs', counts: 'installs' },
|
|
62
|
+
{ part: 'reliability', title: 'Reliability', counts: 'installs' },
|
|
63
|
+
{ part: 'tester_feedback', title: 'Tester feedback', counts: 'testers' },
|
|
64
|
+
{ part: 'docs', title: 'How-to', counts: null },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// One reading of one version's current part results. `signals` is the newest
|
|
68
|
+
// module_assessment_signals row per part ({ part, outcome, score, sample_size });
|
|
69
|
+
// a part with no row is unmeasured. Pure.
|
|
70
|
+
function readParts(signals = [], { floor = QUALITY_FLOOR } = {}) {
|
|
71
|
+
const byPart = new Map(signals.map((s) => [s.part, s]));
|
|
72
|
+
const parts = {};
|
|
73
|
+
const strengths = [];
|
|
74
|
+
const weaknesses = [];
|
|
75
|
+
const unmeasured = [];
|
|
76
|
+
for (const { part, title, counts } of PARTS) {
|
|
77
|
+
const s = byPart.get(part);
|
|
78
|
+
const scored = s && s.outcome === 'scored' && Number.isFinite(Number(s.score));
|
|
79
|
+
parts[part] = s
|
|
80
|
+
? { outcome: s.outcome, score: scored ? Number(s.score) : null, sample_size: s.sample_size == null ? null : Number(s.sample_size) }
|
|
81
|
+
: { outcome: 'no_data', score: null, sample_size: null };
|
|
82
|
+
if (!scored) { unmeasured.push({ part, title }); continue; }
|
|
83
|
+
const score = Number(s.score);
|
|
84
|
+
const n = parts[part].sample_size;
|
|
85
|
+
const from = counts && n ? `, from ${n} ${n === 1 ? counts.replace(/s$/, '') : counts}` : '';
|
|
86
|
+
const item = { part, title, score, detail: `${title} scored ${score} out of 100${from}.` };
|
|
87
|
+
if (score >= STRONG_AT) strengths.push(item);
|
|
88
|
+
else if (score < floor) weaknesses.push(item);
|
|
89
|
+
}
|
|
90
|
+
strengths.sort((a, b) => b.score - a.score);
|
|
91
|
+
weaknesses.sort((a, b) => a.score - b.score);
|
|
92
|
+
return { parts, strengths, weaknesses, unmeasured };
|
|
93
|
+
}
|
|
94
|
+
|
|
38
95
|
// The newest version row per module key, in semver order (a version that does not
|
|
39
96
|
// parse is skipped, as latestVersions does in module-store.js).
|
|
40
97
|
function newestPerModule(rows) {
|
|
@@ -50,7 +107,8 @@ function newestPerModule(rows) {
|
|
|
50
107
|
}
|
|
51
108
|
|
|
52
109
|
// The catalog: { floor, listed: [...], new: [...] }, each entry
|
|
53
|
-
// { key, title, version, published_at, overall, is_new
|
|
110
|
+
// { key, title, version, published_at, overall, is_new, parts, strengths, weaknesses,
|
|
111
|
+
// unmeasured } (the last four from readParts). Excluded versions are left
|
|
54
112
|
// out entirely: a module below the bar is not listed at all. Both lists are in key
|
|
55
113
|
// order; ranking is a separate task (1003808) and does not happen here.
|
|
56
114
|
async function listCatalog({ db, floor = QUALITY_FLOOR } = {}) {
|
|
@@ -69,6 +127,21 @@ async function listCatalog({ db, floor = QUALITY_FLOOR } = {}) {
|
|
|
69
127
|
ORDER BY version_id, computed_at DESC, id DESC`, [newest.map((r) => r.version_id)]);
|
|
70
128
|
for (const s of rows) scores.set(String(s.version_id), s);
|
|
71
129
|
}
|
|
130
|
+
// Each version's current result per part: the newest row per (version, part),
|
|
131
|
+
// the read idx_module_assessment_signals_current exists for (core_265).
|
|
132
|
+
const signals = new Map();
|
|
133
|
+
if (newest.length) {
|
|
134
|
+
const { rows } = await pool.query(
|
|
135
|
+
`SELECT DISTINCT ON (version_id, part) version_id, part, outcome, score, sample_size
|
|
136
|
+
FROM module_assessment_signals
|
|
137
|
+
WHERE version_id = ANY($1)
|
|
138
|
+
ORDER BY version_id, part, measured_at DESC, id DESC`, [newest.map((r) => r.version_id)]);
|
|
139
|
+
for (const s of rows) {
|
|
140
|
+
const k = String(s.version_id);
|
|
141
|
+
if (!signals.has(k)) signals.set(k, []);
|
|
142
|
+
signals.get(k).push(s);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
72
145
|
const out = { floor, listed: [], new: [] };
|
|
73
146
|
for (const r of newest.sort((a, b) => a.module_key.localeCompare(b.module_key))) {
|
|
74
147
|
const score = scores.get(String(r.version_id)) || null;
|
|
@@ -77,9 +150,10 @@ async function listCatalog({ db, floor = QUALITY_FLOOR } = {}) {
|
|
|
77
150
|
out[placement].push({
|
|
78
151
|
key: r.module_key, title: r.title, version: r.version, published_at: r.published_at,
|
|
79
152
|
overall: placement === 'new' ? null : score.overall, is_new: placement === 'new',
|
|
153
|
+
...readParts(signals.get(String(r.version_id)) || [], { floor }),
|
|
80
154
|
});
|
|
81
155
|
}
|
|
82
156
|
return out;
|
|
83
157
|
}
|
|
84
158
|
|
|
85
|
-
module.exports = { QUALITY_FLOOR, placeVersion, listCatalog };
|
|
159
|
+
module.exports = { QUALITY_FLOOR, STRONG_AT, placeVersion, readParts, listCatalog };
|
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.21.
|
|
78
|
+
const CORE_VERSION = '1.21.153'; // 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');
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
const fs = require('node:fs');
|
|
31
31
|
const path = require('node:path');
|
|
32
32
|
const { deriveProvenance } = require('./provenance');
|
|
33
|
+
const { CATEGORY_PARENTS } = require('./manifest-schema');
|
|
33
34
|
|
|
34
35
|
// The core's own published + source package names. A checkout whose package.json
|
|
35
36
|
// says one of these IS the core, so its modules/ IS the catalog (see
|
|
@@ -99,6 +100,17 @@ function creditFor(manifest = {}, { source = 'instance', coreProjectName = CORE_
|
|
|
99
100
|
};
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
// Where a module is browsed (ADR 0342): its parent key, that parent's label, and its
|
|
104
|
+
// sub-category key — or null when the manifest declares none. The label is resolved
|
|
105
|
+
// HERE so the hall and the CLI never keep their own copy of the parent list. A parent
|
|
106
|
+
// outside the closed list cannot reach this (the validator refuses it), so an unknown
|
|
107
|
+
// one is null rather than shown under a label nobody chose.
|
|
108
|
+
function categoryFor(manifest = {}) {
|
|
109
|
+
const c = manifest.category;
|
|
110
|
+
if (!c || typeof c !== 'object' || !Object.hasOwn(CATEGORY_PARENTS, c.parent)) return null;
|
|
111
|
+
return { parent: c.parent, parentTitle: CATEGORY_PARENTS[c.parent], sub: c.sub || null };
|
|
112
|
+
}
|
|
113
|
+
|
|
102
114
|
// One catalog row per discovered module. `loaded` is the loader's
|
|
103
115
|
// discoverModules().loaded ([{ key, dir, manifest }]); `enabled` is the resolved
|
|
104
116
|
// enablement map (src/modules.js modules()).
|
|
@@ -138,6 +150,7 @@ function buildCatalog({
|
|
|
138
150
|
license: manifest.license || null,
|
|
139
151
|
maintainer: manifest.maintainer || null,
|
|
140
152
|
maintenance: manifest.maintenance || null,
|
|
153
|
+
category: categoryFor(manifest),
|
|
141
154
|
};
|
|
142
155
|
})
|
|
143
156
|
.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
@@ -160,7 +173,8 @@ function instanceRows(rows = []) {
|
|
|
160
173
|
function searchCatalog(rows = [], query = '') {
|
|
161
174
|
const q = String(query || '').trim().toLowerCase();
|
|
162
175
|
if (!q) return rows;
|
|
163
|
-
return rows.filter((r) => [r.key, r.title, r.description, r.credit.author, r.credit.origin, r.license
|
|
176
|
+
return rows.filter((r) => [r.key, r.title, r.description, r.credit.author, r.credit.origin, r.license,
|
|
177
|
+
r.category && r.category.parentTitle, r.category && r.category.sub]
|
|
164
178
|
.some((f) => f && String(f).toLowerCase().includes(q)));
|
|
165
179
|
}
|
|
166
180
|
|
|
@@ -53,6 +53,10 @@
|
|
|
53
53
|
// // actually declares in `contributes`, and every entry must be
|
|
54
54
|
// // one it actually lists there — a typo'd exemption is an error,
|
|
55
55
|
// // never a silent pass.
|
|
56
|
+
// "category": { // OPTIONAL. Where the module is browsed and ranked (ADR 0342).
|
|
57
|
+
// "parent": "money", // REQUIRED in-block: one of the eight CATEGORY_PARENTS keys.
|
|
58
|
+
// "sub": "stripe-payments" // OPTIONAL. An approved sub-category key — the comparable set
|
|
59
|
+
// }, // the store's cap of ten counts within; absent while pending.
|
|
56
60
|
// "howto": { // OPTIONAL. Extras beside the module's HOWTO.md (ADR 0347 D3).
|
|
57
61
|
// "artifactUrl": "https://claude.ai/…" // A Claude page with the same how-to, shown as an extra,
|
|
58
62
|
// }, // never graded and never required — the file is the how-to.
|
|
@@ -106,6 +110,24 @@ const SPEND_KEYS = ['requiresPayer'];
|
|
|
106
110
|
// The `howto` block (ADR 0347 D3) — closed for the same reason; not exported either.
|
|
107
111
|
const HOWTO_KEYS = ['artifactUrl'];
|
|
108
112
|
|
|
113
|
+
// The eight fixed parent categories (ADR 0342 D1), key → the label a catalog shows.
|
|
114
|
+
// A parent is for browsing only: the cap of ten counts within a SUB-category, never a
|
|
115
|
+
// parent (D2). The list is closed so a typo'd "monye" fails loud rather than filing
|
|
116
|
+
// the module under a ninth parent nobody browses; changing it needs a new ADR.
|
|
117
|
+
// `sub` is shape-checked only — which sub-categories are approved is ledger state
|
|
118
|
+
// (task 1004355), not something a pure validator can know.
|
|
119
|
+
const CATEGORY_PARENTS = Object.freeze({
|
|
120
|
+
work: 'Work & planning',
|
|
121
|
+
people: 'Team & people',
|
|
122
|
+
quality: 'Quality & review',
|
|
123
|
+
money: 'Money',
|
|
124
|
+
communication: 'Communication',
|
|
125
|
+
pages: 'Pages & design',
|
|
126
|
+
hosting: 'Hosting & deploy',
|
|
127
|
+
knowledge: 'Knowledge',
|
|
128
|
+
});
|
|
129
|
+
const CATEGORY_KEYS = ['parent', 'sub'];
|
|
130
|
+
|
|
109
131
|
// An https link on claude.ai (or a subdomain). Shape only: the store never fetches it.
|
|
110
132
|
function isClaudeArtifactUrl(v) {
|
|
111
133
|
if (!isStr(v)) return false;
|
|
@@ -320,6 +342,23 @@ function validateManifest(obj) {
|
|
|
320
342
|
}
|
|
321
343
|
}
|
|
322
344
|
|
|
345
|
+
// --- optional category (ADR 0342) ---
|
|
346
|
+
if ('category' in obj) {
|
|
347
|
+
const c = obj.category;
|
|
348
|
+
const parents = Object.keys(CATEGORY_PARENTS);
|
|
349
|
+
if (!isObj(c)) E('category: must be an object (e.g. { "parent": "money", "sub": "stripe-payments" })');
|
|
350
|
+
else {
|
|
351
|
+
for (const k of Object.keys(c)) {
|
|
352
|
+
if (!CATEGORY_KEYS.includes(k)) E(`category.${k}: unknown key (allowed: ${CATEGORY_KEYS.join(', ')}) — a module has exactly one sub-category (ADR 0342 D4)`);
|
|
353
|
+
}
|
|
354
|
+
if (!isStr(c.parent)) E(`category.parent: required — one of ${parents.join(', ')}`);
|
|
355
|
+
else if (!parents.includes(c.parent)) E(`category.parent: "${c.parent}" is not one of ${parents.join(', ')} (ADR 0342 D1)`);
|
|
356
|
+
if ('sub' in c && (!isStr(c.sub) || !KEY_RE.test(c.sub))) {
|
|
357
|
+
E('category.sub: must be a sub-category key (lowercase kebab-case) when present — leave it out while a new one is pending approval');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
323
362
|
// --- optional how-to extras (ADR 0347 D3) ---
|
|
324
363
|
if ('howto' in obj) {
|
|
325
364
|
const h = obj.howto;
|
|
@@ -370,5 +409,5 @@ function assertValidManifest(obj, label = 'module.json') {
|
|
|
370
409
|
|
|
371
410
|
module.exports = {
|
|
372
411
|
validateManifest, assertValidManifest, declaredEntries, entryLabel, KEY_RE, ALLOWED_CONTRIBUTES, ARRAY_OF_STRING_KEYS, ATTRIBUTION_KEYS,
|
|
373
|
-
MAINTENANCE_STATUSES, MAINTENANCE_KEYS, SUNSET_STATUSES,
|
|
412
|
+
MAINTENANCE_STATUSES, MAINTENANCE_KEYS, SUNSET_STATUSES, CATEGORY_PARENTS,
|
|
374
413
|
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// tests/github_failure.mjs — a failed GitHub call says WHICH step broke and what
|
|
2
|
+
// GitHub answered (task 1004766).
|
|
3
|
+
//
|
|
4
|
+
// On the toast instance every GET /tasks/:id/publish-status answered a bare 502
|
|
5
|
+
// `status_failed` with no message, and the server log carried one word. The thrown
|
|
6
|
+
// errors already held the step (code) and GitHub's reply (status, detail).
|
|
7
|
+
//
|
|
8
|
+
// Run: node tests/github_failure.mjs
|
|
9
|
+
|
|
10
|
+
import { strict as assert } from 'node:assert';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import url from 'node:url';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const ROOT = path.dirname(path.dirname(url.fileURLToPath(import.meta.url)));
|
|
18
|
+
const { describeGithubFailure } = require(path.join(ROOT, 'modules/lifecycle/github-failure.js'));
|
|
19
|
+
|
|
20
|
+
let passed = 0;
|
|
21
|
+
let failed = 0;
|
|
22
|
+
function t(name, fn) {
|
|
23
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
24
|
+
catch (e) { failed++; console.log(` FAIL ${name}`); console.log(` ${e.message}`); }
|
|
25
|
+
}
|
|
26
|
+
const thrown = (code, extra = {}) => Object.assign(new Error(code), { code }, extra);
|
|
27
|
+
|
|
28
|
+
t('an App token refusal names the step and GitHub\'s answer', () => {
|
|
29
|
+
const r = describeGithubFailure(thrown('APP_TOKEN_FAILED', { status: 401, detail: '{"message":"A JSON web token could not be decoded"}' }));
|
|
30
|
+
assert.match(r.message, /GitHub App token/);
|
|
31
|
+
assert.match(r.message, /HTTP 401: \{"message":"A JSON web token could not be decoded"\}/);
|
|
32
|
+
assert.equal(r.details.cause, 'APP_TOKEN_FAILED');
|
|
33
|
+
assert.equal(r.details.github_status, 401);
|
|
34
|
+
assert.match(r.log, /APP_TOKEN_FAILED \(GitHub HTTP 401/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
t('a refused repository read is told apart from a token failure', () => {
|
|
38
|
+
const r = describeGithubFailure(thrown('STATUS_FETCH_FAILED', { status: 403 }));
|
|
39
|
+
assert.match(r.message, /refused a read of the repository/);
|
|
40
|
+
assert.match(r.message, /HTTP 403\./);
|
|
41
|
+
assert.equal(r.details.github_detail, null);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
t('an unknown error still says something, and never invents a GitHub status', () => {
|
|
45
|
+
const r = describeGithubFailure(new Error('fetch failed'));
|
|
46
|
+
assert.match(r.message, /unexpected error/);
|
|
47
|
+
assert.doesNotMatch(r.message, /HTTP/);
|
|
48
|
+
assert.equal(r.details.cause, null);
|
|
49
|
+
assert.equal(r.log, 'fetch failed');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
t('GitHub\'s detail is capped', () => {
|
|
53
|
+
const r = describeGithubFailure(thrown('APP_TOKEN_FAILED', { status: 500, detail: 'x'.repeat(5000) }));
|
|
54
|
+
assert.ok(r.details.github_detail.length <= 300);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
t('publish-status sends the description, not a bare 502', () => {
|
|
58
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/routes/tasks.js'), 'utf8');
|
|
59
|
+
const i = src.indexOf("router.get('/tasks/:id/publish-status'");
|
|
60
|
+
const handler = src.slice(i, src.indexOf('router.', i + 20));
|
|
61
|
+
assert.match(handler, /failGithubStatus\(res, log, e\)/);
|
|
62
|
+
assert.doesNotMatch(handler, /res\.fail\('status_failed', 502\)/);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
t('the route answer carries the message and logs the step', () => {
|
|
66
|
+
const { failGithubStatus } = require(path.join(ROOT, 'modules/lifecycle/github-failure.js'));
|
|
67
|
+
let sent = null;
|
|
68
|
+
const logged = [];
|
|
69
|
+
const res = { fail: (code, opts) => { sent = { code, ...opts }; return 'sent'; } };
|
|
70
|
+
const out = failGithubStatus(res, { error: (...a) => logged.push(a.join(' ')) }, thrown('APP_TOKEN_FAILED', { status: 401 }));
|
|
71
|
+
assert.equal(out, 'sent');
|
|
72
|
+
assert.equal(sent.code, 'status_failed');
|
|
73
|
+
assert.equal(sent.status, 502);
|
|
74
|
+
assert.match(sent.message, /GitHub App token.*HTTP 401/);
|
|
75
|
+
assert.match(logged[0], /APP_TOKEN_FAILED \(GitHub HTTP 401\)/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
79
|
+
process.exit(failed ? 1 : 0);
|