@bongos/core 1.19.1000 → 1.19.1002
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 +27 -17
- package/docs/api/openapi.json +1 -1
- package/docs/api-reference.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/modules/provisioning/cost-ledger.js +74 -0
- package/modules/provisioning/provisioning.js +5 -28
- package/modules/provisioning/routes/provisioning.js +4 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/gen-file-map.js +22 -3
- package/src/module-api.js +1 -1
- package/tests/file_map.mjs +64 -12
- package/tests/provisioning_cost_ledger.mjs +98 -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.1002",
|
|
6
|
+
"core_contract": "1.19.1002",
|
|
7
|
+
"source_commit": "e0937954f00712025cbc3f2c9b8a400a35654037",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-24T15:24:05.587Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 521,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2410,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2957,
|
|
20
|
+
"tree_sha256": "8a53ccc546dbeb0297ea0095d653db33cedcebd26e566f28b616cbd0e7cf980c",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2157,12 +2157,12 @@
|
|
|
2157
2157
|
{
|
|
2158
2158
|
"path": "docs/api-reference.md",
|
|
2159
2159
|
"mode": "0000644",
|
|
2160
|
-
"sha256": "
|
|
2160
|
+
"sha256": "20ab80e06f03ad1c1f1f33e3f804d402d011bca4c7b2b9df0090269958009d92"
|
|
2161
2161
|
},
|
|
2162
2162
|
{
|
|
2163
2163
|
"path": "docs/api/openapi.json",
|
|
2164
2164
|
"mode": "0000644",
|
|
2165
|
-
"sha256": "
|
|
2165
|
+
"sha256": "51f4a9dd2fcd6d34bd3eaa08285acf9b7c9c1ccc294d1e616c1338607701439e"
|
|
2166
2166
|
},
|
|
2167
2167
|
{
|
|
2168
2168
|
"path": "docs/architecture.md",
|
|
@@ -2722,7 +2722,7 @@
|
|
|
2722
2722
|
{
|
|
2723
2723
|
"path": "docs/module-api-changelog.md",
|
|
2724
2724
|
"mode": "0000644",
|
|
2725
|
-
"sha256": "
|
|
2725
|
+
"sha256": "9fa60f085e9a51dea701f575ca3f5ed132c2deec0c5a311b46d8bc5dec1079e0"
|
|
2726
2726
|
},
|
|
2727
2727
|
{
|
|
2728
2728
|
"path": "docs/modules-contract.md",
|
|
@@ -6634,6 +6634,11 @@
|
|
|
6634
6634
|
"mode": "0000644",
|
|
6635
6635
|
"sha256": "d2ff6733aaab0c009cfe2e90e756fa9742193dba7c5078c5e1d5fa2fd6ef6e8a"
|
|
6636
6636
|
},
|
|
6637
|
+
{
|
|
6638
|
+
"path": "modules/provisioning/cost-ledger.js",
|
|
6639
|
+
"mode": "0000644",
|
|
6640
|
+
"sha256": "0f6a25220ce3786ba3d813bcba2e39ef902aa25ed4420264efaca36b9ddb3432"
|
|
6641
|
+
},
|
|
6637
6642
|
{
|
|
6638
6643
|
"path": "modules/provisioning/credential-preflight.js",
|
|
6639
6644
|
"mode": "0000644",
|
|
@@ -6827,7 +6832,7 @@
|
|
|
6827
6832
|
{
|
|
6828
6833
|
"path": "modules/provisioning/provisioning.js",
|
|
6829
6834
|
"mode": "0000644",
|
|
6830
|
-
"sha256": "
|
|
6835
|
+
"sha256": "fba7da2883254cc0f7f2eafee6c437aeac00f97fb1f8a35acade67e48c6a6eff"
|
|
6831
6836
|
},
|
|
6832
6837
|
{
|
|
6833
6838
|
"path": "modules/provisioning/rate-limit.js",
|
|
@@ -6872,7 +6877,7 @@
|
|
|
6872
6877
|
{
|
|
6873
6878
|
"path": "modules/provisioning/routes/provisioning.js",
|
|
6874
6879
|
"mode": "0000644",
|
|
6875
|
-
"sha256": "
|
|
6880
|
+
"sha256": "6f123553f9fd1f3d48f5356dcddb628c78660c10615f3d7e137f71bf2e7d0750"
|
|
6876
6881
|
},
|
|
6877
6882
|
{
|
|
6878
6883
|
"path": "modules/provisioning/routes/repo-private.js",
|
|
@@ -8202,12 +8207,12 @@
|
|
|
8202
8207
|
{
|
|
8203
8208
|
"path": "package-lock.json",
|
|
8204
8209
|
"mode": "0000644",
|
|
8205
|
-
"sha256": "
|
|
8210
|
+
"sha256": "ce35548a4758316756e143ba9b49e1d860899d125a16a4b92ab1569174f5f3f0"
|
|
8206
8211
|
},
|
|
8207
8212
|
{
|
|
8208
8213
|
"path": "package.json",
|
|
8209
8214
|
"mode": "0000644",
|
|
8210
|
-
"sha256": "
|
|
8215
|
+
"sha256": "eb8f4069bbf120c7d6322a857238a6c5fba334c8c2112738fcc97460e29bcbef"
|
|
8211
8216
|
},
|
|
8212
8217
|
{
|
|
8213
8218
|
"path": "public-docs/index.html",
|
|
@@ -8857,7 +8862,7 @@
|
|
|
8857
8862
|
{
|
|
8858
8863
|
"path": "scripts/gds/gen-file-map.js",
|
|
8859
8864
|
"mode": "0000644",
|
|
8860
|
-
"sha256": "
|
|
8865
|
+
"sha256": "ac2308077104c2ff899f4b12ff9da7baa965a9c0d3e4ef2f92e6bdf9d3613fb6"
|
|
8861
8866
|
},
|
|
8862
8867
|
{
|
|
8863
8868
|
"path": "scripts/gds/gen-freshness.js",
|
|
@@ -10252,7 +10257,7 @@
|
|
|
10252
10257
|
{
|
|
10253
10258
|
"path": "src/module-api.js",
|
|
10254
10259
|
"mode": "0000644",
|
|
10255
|
-
"sha256": "
|
|
10260
|
+
"sha256": "7be77918a0d109201d7c85c399d36f8f88473314c8c9a433a8d08ff7db44776a"
|
|
10256
10261
|
},
|
|
10257
10262
|
{
|
|
10258
10263
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11517,7 +11522,7 @@
|
|
|
11517
11522
|
{
|
|
11518
11523
|
"path": "tests/file_map.mjs",
|
|
11519
11524
|
"mode": "0000644",
|
|
11520
|
-
"sha256": "
|
|
11525
|
+
"sha256": "c6a7da467336246652fae6db5b5d7faa147e71ac1db89a66ec143dcf03bf8013"
|
|
11521
11526
|
},
|
|
11522
11527
|
{
|
|
11523
11528
|
"path": "tests/filing_abuse_matrix.mjs",
|
|
@@ -13434,6 +13439,11 @@
|
|
|
13434
13439
|
"mode": "0000644",
|
|
13435
13440
|
"sha256": "406ea4d32fe1bbb7aa1f69e0c4dc967feb2fc43c8bf0f70848547fdc6faf8762"
|
|
13436
13441
|
},
|
|
13442
|
+
{
|
|
13443
|
+
"path": "tests/provisioning_cost_ledger.mjs",
|
|
13444
|
+
"mode": "0000644",
|
|
13445
|
+
"sha256": "a0176f26c37067860379df4da0274646771c7d6c427d84fad0145763c7366152"
|
|
13446
|
+
},
|
|
13437
13447
|
{
|
|
13438
13448
|
"path": "tests/provisioning_credential_preflight.mjs",
|
|
13439
13449
|
"mode": "0000644",
|
package/docs/api/openapi.json
CHANGED
|
@@ -12819,7 +12819,7 @@
|
|
|
12819
12819
|
"provisioning"
|
|
12820
12820
|
],
|
|
12821
12821
|
"summary": "GET /provisioning/fleet/cost-ledger",
|
|
12822
|
-
"description": "GET /provisioning/fleet/cost-ledger — per-instance monthly
|
|
12822
|
+
"description": "GET /provisioning/fleet/cost-ledger — per-instance monthly cost to us + estimated cost to its owner, with totals. rank: archon — cost oversight across the fleet (the /boxes/cost-ledger precedent). Declared among the archon routes (archon-first) for correct auditor classification.\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `provisioning.fleet.manage` (all required).",
|
|
12823
12823
|
"x-rank": "metic+archon",
|
|
12824
12824
|
"x-source": "modules/provisioning/routes/provisioning.js",
|
|
12825
12825
|
"x-permissions": [
|
package/docs/api-reference.md
CHANGED
|
@@ -515,7 +515,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
515
515
|
|---|---|---|---|---|
|
|
516
516
|
| GET | `/api/bongos/provisioning/control-plane` | `archon` | — | GET /provisioning/control-plane — "which row is this platform, and where does it stand?" The deploy page's first call: the self row is cr… |
|
|
517
517
|
| GET | `/api/bongos/provisioning/fleet` | `metic+archon` | — | GET /provisioning/fleet — the full instance roster across all owners. |
|
|
518
|
-
| GET | `/api/bongos/provisioning/fleet/cost-ledger` | `metic+archon` | — | GET /provisioning/fleet/cost-ledger — per-instance monthly
|
|
518
|
+
| GET | `/api/bongos/provisioning/fleet/cost-ledger` | `metic+archon` | — | GET /provisioning/fleet/cost-ledger — per-instance monthly cost to us + estimated cost to its owner, with totals. |
|
|
519
519
|
| GET | `/api/bongos/provisioning/github-app/callback` | `public` | — | GET /provisioning/github-app/callback?code=&state= — GitHub redirects the owner's BROWSER here after they approve the App Manifest (task … |
|
|
520
520
|
| GET | `/api/bongos/provisioning/github-app/status` | `public` | — | GET /provisioning/github-app/status?state=<> — the PUBLIC progress read the onboarding callback page polls (task 2096) to know when setup… |
|
|
521
521
|
| GET | `/api/bongos/provisioning/health` | `public` | — | GET /provisioning/health — liveness + the core version this module mounted against. |
|
|
@@ -2474,5 +2474,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2474
2474
|
landed since 1.19.998 with no explicit bump. run 35942048393. (task 1002620)
|
|
2475
2475
|
1.19.1000 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2476
2476
|
landed since 1.19.999 with no explicit bump. run 35943151697. (task 1002620)
|
|
2477
|
+
1.19.1001 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2478
|
+
landed since 1.19.1000 with no explicit bump. run 36016738965. (task 1002620)
|
|
2479
|
+
1.19.1002 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2480
|
+
landed since 1.19.1001 with no explicit bump. run 36019839845. (task 1002620)
|
|
2477
2481
|
---------------------------------------------------------------------------
|
|
2478
2482
|
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// The fleet cost ledger (GET /provisioning/fleet/cost-ledger) — VISIBILITY only (ADR 0111 §6).
|
|
3
|
+
//
|
|
4
|
+
// It asks two questions, one column each (task 1004148, ADR 0327). Before, one column
|
|
5
|
+
// asked both and reported a confident $0 for every project:
|
|
6
|
+
// platform_monthly_usd — what a row costs US. A cloud-host runs on its owner's own
|
|
7
|
+
// account, so $0 is correct; the one thing we pay for is the control plane's box.
|
|
8
|
+
// owner_monthly_usd — an ESTIMATE of the owner's own hosting bill, never ours.
|
|
9
|
+
// null in either column means UNPRICED — the row names no plan we can price — and is
|
|
10
|
+
// never folded into a total as 0.
|
|
11
|
+
|
|
12
|
+
const { HOSTING_SHAPES, TIER_MONTHLY_USD } = require('./provisioning');
|
|
13
|
+
|
|
14
|
+
// What a cloud-host's owner pays Render per month, keyed by the plan stored in `tier`:
|
|
15
|
+
// web service + Postgres, ~$13 at the default plans (ADR 0327). No row names a plan yet;
|
|
16
|
+
// the Render standup (task 1004183) writes one, and the pilot (task 1004142) confirms
|
|
17
|
+
// these names and prices.
|
|
18
|
+
const RENDER_PLAN_MONTHLY_USD = Object.freeze({
|
|
19
|
+
'starter+basic-256mb': 13,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function priceOf(table, key) {
|
|
23
|
+
return typeof key === 'string' && Object.hasOwn(table, key) ? table[key] : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function platformMonthlyUsd({ hostingShape, tier } = {}) {
|
|
27
|
+
// The box the projects share today is priced once, here, by its droplet size.
|
|
28
|
+
if (hostingShape === 'control-plane') return priceOf(TIER_MONTHLY_USD, tier);
|
|
29
|
+
return HOSTING_SHAPES.includes(hostingShape) ? 0 : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ownerMonthlyUsd({ hostingShape, tier } = {}) {
|
|
33
|
+
if (hostingShape === 'cloud-host') return priceOf(RENDER_PLAN_MONTHLY_USD, tier);
|
|
34
|
+
// co-tenant shares our box and the control plane is ours: no owner is billed for either.
|
|
35
|
+
if (hostingShape === 'co-tenant' || hostingShape === 'control-plane') return 0;
|
|
36
|
+
return null; // byo-host runs on the owner's own machine, whose bill we cannot see
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function columnTotal(instances, key) {
|
|
40
|
+
const priced = instances.map((i) => i[key]).filter((v) => v !== null);
|
|
41
|
+
const sum = priced.reduce((a, v) => a + v, 0);
|
|
42
|
+
return { monthly_usd: Number(sum.toFixed(4)), unpriced: instances.length - priced.length };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Per-instance costs + a total per column, over the instances not torn down.
|
|
46
|
+
async function costLedger(db, { limit = 500 } = {}) {
|
|
47
|
+
const { rows } = await db.query(
|
|
48
|
+
`SELECT pi.id, pi.slug, pi.hosting_shape, pi.tier, pi.status, b.github_login
|
|
49
|
+
FROM provisioning_instances pi
|
|
50
|
+
JOIN builders b ON b.id = pi.owner_builder_id
|
|
51
|
+
WHERE pi.status <> 'torn_down'
|
|
52
|
+
ORDER BY pi.updated_at DESC
|
|
53
|
+
LIMIT $1`,
|
|
54
|
+
[Math.min(Math.max(Number(limit) || 500, 1), 2000)]
|
|
55
|
+
);
|
|
56
|
+
const instances = rows.map((r) => {
|
|
57
|
+
const shape = { hostingShape: r.hosting_shape, tier: r.tier };
|
|
58
|
+
return {
|
|
59
|
+
id: r.id, slug: r.slug, owner: r.github_login,
|
|
60
|
+
hosting_shape: r.hosting_shape, tier: r.tier, status: r.status,
|
|
61
|
+
platform_monthly_usd: platformMonthlyUsd(shape),
|
|
62
|
+
owner_monthly_usd: ownerMonthlyUsd(shape),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
instances,
|
|
67
|
+
totals: {
|
|
68
|
+
platform: columnTotal(instances, 'platform_monthly_usd'),
|
|
69
|
+
owner: columnTotal(instances, 'owner_monthly_usd'),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { RENDER_PLAN_MONTHLY_USD, platformMonthlyUsd, ownerMonthlyUsd, costLedger };
|
|
@@ -307,9 +307,10 @@ function isValidSlug(slug) {
|
|
|
307
307
|
// 'dedicated' was the only shape that priced above zero and provisioning_027 folded it into
|
|
308
308
|
// 'cloud-host', so no row can carry it. ADR 0327 says a ledger of OUR spend "should read
|
|
309
309
|
// ~$0 per cloud-host and be *correct*"; what an owner is worth telling is THEIR platform
|
|
310
|
-
// cost, a different column —
|
|
311
|
-
//
|
|
312
|
-
//
|
|
310
|
+
// cost, a different column — ownerMonthlyUsd in cost-ledger.js, which the fleet ledger
|
|
311
|
+
// reads instead of this stored value. The 'dedicated' arm and the tier table stay because
|
|
312
|
+
// this is pure and its tests call it with that value; the droplet code path is task
|
|
313
|
+
// 1004185's to delete.
|
|
313
314
|
function costEstimateUsd({ hostingShape, tier } = {}) {
|
|
314
315
|
if (hostingShape !== 'dedicated') return 0;
|
|
315
316
|
// An ABSENT tier is not an unknown one. The runner substitutes its own default
|
|
@@ -1434,30 +1435,6 @@ async function planetChoicesForHosts(db, hosts) {
|
|
|
1434
1435
|
return out;
|
|
1435
1436
|
}
|
|
1436
1437
|
|
|
1437
|
-
// Fleet cost ledger (Archon): per-instance monthly estimate + a total, over the
|
|
1438
|
-
// instances that still cost something (not torn_down). VISIBILITY only (ADR 0111 §6).
|
|
1439
|
-
async function costLedger(db, { limit = 500 } = {}) {
|
|
1440
|
-
const { rows } = await db.query(
|
|
1441
|
-
`SELECT pi.id, pi.slug, pi.hosting_shape, pi.tier, pi.status,
|
|
1442
|
-
pi.cost_estimate_usd, b.github_login
|
|
1443
|
-
FROM provisioning_instances pi
|
|
1444
|
-
JOIN builders b ON b.id = pi.owner_builder_id
|
|
1445
|
-
WHERE pi.status <> 'torn_down'
|
|
1446
|
-
ORDER BY pi.cost_estimate_usd DESC, pi.updated_at DESC
|
|
1447
|
-
LIMIT $1`,
|
|
1448
|
-
[Math.min(Math.max(Number(limit) || 500, 1), 2000)]
|
|
1449
|
-
);
|
|
1450
|
-
const totalMonthlyUsd = rows.reduce((a, r) => a + Number(r.cost_estimate_usd || 0), 0);
|
|
1451
|
-
return {
|
|
1452
|
-
instances: rows.map((r) => ({
|
|
1453
|
-
id: r.id, slug: r.slug, owner: r.github_login,
|
|
1454
|
-
hosting_shape: r.hosting_shape, tier: r.tier, status: r.status,
|
|
1455
|
-
cost_estimate_usd: Number(r.cost_estimate_usd || 0),
|
|
1456
|
-
})),
|
|
1457
|
-
total_monthly_usd: Number(totalMonthlyUsd.toFixed(4)),
|
|
1458
|
-
};
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
1438
|
module.exports = {
|
|
1462
1439
|
// constants
|
|
1463
1440
|
INSTANCE_STATES, HOSTING_SHAPES, ONBOARD_MODES, PROJECT_TYPES, INTENT_ACTIONS, TEARDOWN_FROM,
|
|
@@ -1488,7 +1465,7 @@ module.exports = {
|
|
|
1488
1465
|
recordSettingsPushOwed,
|
|
1489
1466
|
createInstance, setInstanceStatus, updateInstanceDomain, clearReleasedDomains, updateInstanceSettings, updateInstanceDetail,
|
|
1490
1467
|
enqueueIntent, enqueueTeardown, enqueueSettingsApply, enqueueRestart, getOpenIntent, claimNextIntent, resolveIntent,
|
|
1491
|
-
recordEvent, instanceStateForSlug, planetChoicesForHosts, projectOwnedBy,
|
|
1468
|
+
recordEvent, instanceStateForSlug, planetChoicesForHosts, projectOwnedBy,
|
|
1492
1469
|
// data access — GitHub App Manifest attempts (task 2080)
|
|
1493
1470
|
createOAuthManifest, getOAuthManifestByState, attachManifestCode,
|
|
1494
1471
|
getLatestOAuthManifestForInstance, claimNextOAuthManifest, resolveOAuthManifest,
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// GET /provisioning/instances/:id/invite-suggestions — who is worth inviting (R28, ADR 0251)
|
|
30
30
|
// Archon-scoped (requireRank('archon') — the fleet, the /boxes precedent):
|
|
31
31
|
// GET /provisioning/fleet — the full instance roster + the box's slot `capacity` (ADR 0285)
|
|
32
|
-
// GET /provisioning/fleet/cost-ledger — per-instance +
|
|
32
|
+
// GET /provisioning/fleet/cost-ledger — per-instance cost to us + to its owner, with totals
|
|
33
33
|
// POST /provisioning/instances/:id/force-teardown — tear down ANY instance
|
|
34
34
|
// public:
|
|
35
35
|
// GET /provisioning/health — liveness + core version
|
|
@@ -49,6 +49,7 @@ const express = require('express');
|
|
|
49
49
|
const api = require('../../../src/module-api');
|
|
50
50
|
const auth = api, { pool, validateOrRespond, parseId } = api;
|
|
51
51
|
const provisioning = require('../provisioning');
|
|
52
|
+
const { costLedger } = require('../cost-ledger');
|
|
52
53
|
const { onboardPlan, stepsForShape } = require('../onboard-plan');
|
|
53
54
|
const { ALWAYS_ON_CORE, OPTIONAL_MODULES, starterBundleAnswerTable, optionalModules, effectiveModules } = require('../starter-bundles');
|
|
54
55
|
const { recommendationsForDetails } = require('../recommendations');
|
|
@@ -214,14 +215,14 @@ module.exports = function provisioningRoutes() {
|
|
|
214
215
|
}
|
|
215
216
|
});
|
|
216
217
|
|
|
217
|
-
// GET /provisioning/fleet/cost-ledger — per-instance monthly
|
|
218
|
+
// GET /provisioning/fleet/cost-ledger — per-instance monthly cost to us + estimated cost to its owner, with totals.
|
|
218
219
|
// rank: archon — cost oversight across the fleet (the /boxes/cost-ledger precedent).
|
|
219
220
|
// Declared among the archon routes (archon-first) for correct auditor classification.
|
|
220
221
|
router.get('/provisioning/fleet/cost-ledger', auth.requireBuilder, auth.requirePermission('provisioning.fleet.manage'), async (req, res) => {
|
|
221
222
|
try {
|
|
222
223
|
// R14 (#2001 / ADR 0119): shared helper; per-endpoint cap preserved (default 500, max 2000).
|
|
223
224
|
const { limit } = api.parsePagination(req, { defaultLimit: 500, maxLimit: 2000 });
|
|
224
|
-
const out = await
|
|
225
|
+
const out = await costLedger(pool, { limit });
|
|
225
226
|
res.json(out);
|
|
226
227
|
} catch (err) {
|
|
227
228
|
log.error('[provisioning] GET /provisioning/fleet/cost-ledger', err);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.1002",
|
|
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.1002",
|
|
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.1002",
|
|
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",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// and .claude/scheduled-tasks/ — are now GENERATED from what is on disk. The per-entry one-line
|
|
14
14
|
// notes (the prose after the ←, which no directory walk can know) live in a sidecar,
|
|
15
15
|
// docs/file-map.notes.json, the ONE place a human writes them. The contract enforced by --check:
|
|
16
|
-
// • every skill/routine dir
|
|
16
|
+
// • every TRACKED skill/routine dir MUST have a note → a new skill can't land undescribed;
|
|
17
17
|
// • every note MUST map to a dir that exists → the map can't carry ghosts;
|
|
18
18
|
// • the rendered block MUST match what's committed → no stale tree.
|
|
19
19
|
// MODULE COVERAGE (task 1004066). The same honor system that let a skill land unmapped let a whole
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
const fs = require('node:fs');
|
|
43
43
|
const path = require('node:path');
|
|
44
|
+
const { execFileSync } = require('node:child_process');
|
|
44
45
|
const { resolveInstanceRoot } = require('../../src/instance-config'); // ADR 0108 §1: configurable-root
|
|
45
46
|
const { sameContent } = require('./gen-freshness'); // eol-insensitive freshness (CRLF checkouts)
|
|
46
47
|
|
|
@@ -87,6 +88,21 @@ function loadNotes() {
|
|
|
87
88
|
const { readModuleSkillsManifest } = require('./claude-materialize.js');
|
|
88
89
|
function materializedNames(absDir) { return new Set(Object.keys(readModuleSkillsManifest(absDir).skills)); }
|
|
89
90
|
|
|
91
|
+
// The subdirs of `relDir` holding at least one TRACKED file (task 1004213). An untracked one is the
|
|
92
|
+
// builder's own — a locally-installed plugin skill, or the empty husk a plugin extract leaves — and
|
|
93
|
+
// listing it would publish one machine's tooling into a shared doc on every post-merge regen.
|
|
94
|
+
// null when `root` is not the top of a git work tree (a packaged install, ADR 0108 §1): nothing to ask.
|
|
95
|
+
function trackedSubdirs(root, relDir) {
|
|
96
|
+
const git = (args) => execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
97
|
+
try {
|
|
98
|
+
if (git(['rev-parse', '--show-prefix']).trim() !== '') return null;
|
|
99
|
+
return new Set(git(['ls-files', '-z', '--', relDir]).split('\0')
|
|
100
|
+
.map((p) => p.slice(relDir.length + 1).split('/'))
|
|
101
|
+
.filter((segs) => segs.length > 1)
|
|
102
|
+
.map((segs) => segs[0]));
|
|
103
|
+
} catch { return null; }
|
|
104
|
+
}
|
|
105
|
+
|
|
90
106
|
// Immediate subdirectories of an absolute dir, sorted by name (deterministic); materialised
|
|
91
107
|
// module copies excluded.
|
|
92
108
|
function listSubdirs(absDir) {
|
|
@@ -103,7 +119,9 @@ function listSubdirs(absDir) {
|
|
|
103
119
|
// classes the freshness contract enforces (a dir with no note; a note with no dir).
|
|
104
120
|
function buildSection(section, notes) {
|
|
105
121
|
const absDir = path.join(REPO_ROOT, section.dir);
|
|
106
|
-
const
|
|
122
|
+
const present = listSubdirs(absDir);
|
|
123
|
+
const tracked = trackedSubdirs(REPO_ROOT, section.dir);
|
|
124
|
+
const names = tracked ? present.filter((n) => tracked.has(n)) : present;
|
|
107
125
|
const sectionNotes = (notes && notes[section.notesKey]) || {};
|
|
108
126
|
const entries = names.map((name) => {
|
|
109
127
|
// Skills + scheduled-tasks each carry a SKILL.md; label it explicitly when present (matches the
|
|
@@ -114,7 +132,8 @@ function buildSection(section, notes) {
|
|
|
114
132
|
return { name, label, note };
|
|
115
133
|
});
|
|
116
134
|
const missing = entries.filter((e) => e.note == null).map((e) => `${section.dir}/${e.name}`);
|
|
117
|
-
|
|
135
|
+
// A note is a ghost only when its dir is gone from disk — one not yet `git add`ed is not.
|
|
136
|
+
const onDisk = new Set(present);
|
|
118
137
|
const stale = Object.keys(sectionNotes).filter((k) => !onDisk.has(k)).map((k) => `${section.notesKey}.${k}`);
|
|
119
138
|
return { ...section, absDir, entries, missing, stale };
|
|
120
139
|
}
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.1002'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/file_map.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import assert from 'node:assert/strict';
|
|
4
4
|
import { test } from 'node:test';
|
|
5
5
|
import { execFileSync } from 'node:child_process';
|
|
6
|
-
import { readFileSync, existsSync, readdirSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
6
|
+
import { readFileSync, existsSync, readdirSync, mkdirSync, writeFileSync, rmSync, mkdtempSync } from 'node:fs';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
7
8
|
import { createRequire } from 'node:module';
|
|
8
9
|
import { fileURLToPath } from 'node:url';
|
|
9
10
|
import path from 'node:path';
|
|
@@ -40,8 +41,11 @@ test('EVERY skill + scheduled-task dir on disk is listed AND has a note — no s
|
|
|
40
41
|
// A module skill materialised into this checkout (task 1003330) is an untracked copy the
|
|
41
42
|
// module documents; the generator skips it by the manifest beside it, and so does this.
|
|
42
43
|
const materialised = gen.materializedNames(path.join(ROOT, section.dir));
|
|
44
|
+
// An untracked dir is the builder's own (a plugin install, an empty husk — task 1004213).
|
|
45
|
+
const tracked = new Set(execFileSync('git', ['ls-files', '-z', '--', section.dir], { cwd: ROOT, encoding: 'utf8' })
|
|
46
|
+
.split('\0').map((p) => p.slice(section.dir.length + 1).split('/')).filter((s) => s.length > 1).map((s) => s[0]));
|
|
43
47
|
const onDisk = readdirSync(path.join(ROOT, section.dir), { withFileTypes: true })
|
|
44
|
-
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && !materialised.has(e.name))
|
|
48
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && !materialised.has(e.name) && tracked.has(e.name))
|
|
45
49
|
.map((e) => e.name)
|
|
46
50
|
.sort();
|
|
47
51
|
const listed = section.entries.map((e) => e.name).sort();
|
|
@@ -69,20 +73,68 @@ test('the generated blocks live between markers and carry one ← note per entry
|
|
|
69
73
|
}
|
|
70
74
|
});
|
|
71
75
|
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
// A throwaway instance root (task 1004213). `new-skill` is staged, so git tracks it; `local-plugin`
|
|
77
|
+
// is untracked with a SKILL.md, as a real plugin install is; `empty-husk` is the bare dir a plugin
|
|
78
|
+
// extract leaves behind. The generator falls back to cwd for its root, so no env is needed.
|
|
79
|
+
function fixtureRoot({ git }) {
|
|
80
|
+
const root = mkdtempSync(path.join(tmpdir(), 'file-map-'));
|
|
81
|
+
const skills = path.join(root, '.claude', 'skills');
|
|
82
|
+
for (const n of ['new-skill', 'local-plugin']) {
|
|
83
|
+
mkdirSync(path.join(skills, n), { recursive: true });
|
|
84
|
+
writeFileSync(path.join(skills, n, 'SKILL.md'), `---\nname: ${n}\n---\n`);
|
|
85
|
+
}
|
|
86
|
+
mkdirSync(path.join(skills, 'empty-husk'));
|
|
87
|
+
if (git) {
|
|
88
|
+
execFileSync('git', ['init', '-q'], { cwd: root, stdio: 'ignore' });
|
|
89
|
+
execFileSync('git', ['add', '.claude/skills/new-skill'], { cwd: root, stdio: 'ignore' });
|
|
90
|
+
}
|
|
91
|
+
return root;
|
|
92
|
+
}
|
|
93
|
+
const skillsSectionAt = (cwd) => JSON.parse(execFileSync(process.execPath,
|
|
94
|
+
['-e', `process.stdout.write(JSON.stringify(require(${JSON.stringify(GEN)}).buildModel()))`],
|
|
95
|
+
{ cwd, encoding: 'utf8' })).find((s) => s.id === 'skills');
|
|
96
|
+
const skillNamesAt = (cwd) => skillsSectionAt(cwd).entries.map((e) => e.name);
|
|
97
|
+
|
|
98
|
+
test('--check FAILS (exit 2) when a new tracked skill has no note — a skill can no longer land undescribed', () => {
|
|
99
|
+
const root = fixtureRoot({ git: true });
|
|
74
100
|
try {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
101
|
+
let r;
|
|
102
|
+
try { execFileSync(process.execPath, [GEN, '--check'], { cwd: root, encoding: 'utf8', stdio: 'pipe' }); r = { code: 0, stderr: '' }; }
|
|
103
|
+
catch (e) { r = { code: e.status, stderr: String(e.stderr) }; }
|
|
78
104
|
assert.equal(r.code, 2, 'an undescribed skill must fail the gate');
|
|
79
|
-
assert.match(r.stderr, /notes MISSING
|
|
80
|
-
assert.
|
|
105
|
+
assert.match(r.stderr, /notes MISSING for: \.claude\/skills\/new-skill(?! *,)/, 'the tracked skill, and only it, is missing a note');
|
|
106
|
+
assert.doesNotMatch(r.stderr, /local-plugin|empty-husk/);
|
|
81
107
|
} finally {
|
|
82
|
-
rmSync(
|
|
108
|
+
rmSync(root, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('an untracked skill dir is the builder\'s own — a plugin install and an empty husk never reach the map', () => {
|
|
113
|
+
const root = fixtureRoot({ git: true });
|
|
114
|
+
try {
|
|
115
|
+
assert.deepEqual(skillNamesAt(root), ['new-skill']);
|
|
116
|
+
// A note written before `git add` is not a ghost: its dir is on disk.
|
|
117
|
+
mkdirSync(path.join(root, 'docs'));
|
|
118
|
+
writeFileSync(path.join(root, 'docs', 'file-map.notes.json'), JSON.stringify({ skills: { 'local-plugin': 'x', gone: 'y' } }));
|
|
119
|
+
assert.deepEqual(skillsSectionAt(root).stale, ['skills.gone']);
|
|
120
|
+
} finally {
|
|
121
|
+
rmSync(root, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('outside a git work tree (a packaged install) every dir on disk is still listed', () => {
|
|
126
|
+
const root = fixtureRoot({ git: false });
|
|
127
|
+
const parent = fixtureRoot({ git: true });
|
|
128
|
+
const nested = path.join(parent, 'instance');
|
|
129
|
+
try {
|
|
130
|
+
assert.deepEqual(skillNamesAt(root), ['empty-husk', 'local-plugin', 'new-skill']);
|
|
131
|
+
// A root that is a SUBDIR of some repo is not that repo's top: its files are not the repo's to judge.
|
|
132
|
+
mkdirSync(path.join(nested, '.claude', 'skills', 'inner'), { recursive: true });
|
|
133
|
+
assert.deepEqual(skillNamesAt(nested), ['inner']);
|
|
134
|
+
} finally {
|
|
135
|
+
rmSync(root, { recursive: true, force: true });
|
|
136
|
+
rmSync(parent, { recursive: true, force: true });
|
|
83
137
|
}
|
|
84
|
-
// and clean again once removed
|
|
85
|
-
assert.equal(run(['--check']).code, 0, 'gate is clean after the probe dir is removed');
|
|
86
138
|
});
|
|
87
139
|
|
|
88
140
|
test('generation is deterministic + idempotent (a second run is a no-op)', () => {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// tests/provisioning_cost_ledger.mjs — the fleet cost ledger asks two questions, one
|
|
2
|
+
// column each (task 1004148, ADR 0327): what a row costs US, and an estimate of what it
|
|
3
|
+
// costs its OWNER. A row neither can price reads null (unpriced), never 0 — the old
|
|
4
|
+
// single column reported a confident $0 for every project, with no way to tell
|
|
5
|
+
// "costs nothing" from "we don't know".
|
|
6
|
+
//
|
|
7
|
+
// DB-free: CI has no Postgres, so costLedger runs against a recording fake.
|
|
8
|
+
//
|
|
9
|
+
// Run: node tests/provisioning_cost_ledger.mjs
|
|
10
|
+
import { strict as assert } from 'node:assert';
|
|
11
|
+
import { makeRunner } from './helpers.mjs';
|
|
12
|
+
import {
|
|
13
|
+
platformMonthlyUsd, ownerMonthlyUsd, costLedger, RENDER_PLAN_MONTHLY_USD,
|
|
14
|
+
} from '../modules/provisioning/cost-ledger.js';
|
|
15
|
+
|
|
16
|
+
const { test, summary } = makeRunner();
|
|
17
|
+
|
|
18
|
+
const PLAN = Object.keys(RENDER_PLAN_MONTHLY_USD)[0];
|
|
19
|
+
|
|
20
|
+
await test('a cloud-host with no plan named is unpriced for its owner, not $0', () => {
|
|
21
|
+
assert.equal(ownerMonthlyUsd({ hostingShape: 'cloud-host', tier: null }), null);
|
|
22
|
+
assert.equal(ownerMonthlyUsd({ hostingShape: 'cloud-host' }), null);
|
|
23
|
+
assert.equal(ownerMonthlyUsd({ hostingShape: 'cloud-host', tier: '' }), null);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await test('a cloud-host on a named plan is priced from the plan table', () => {
|
|
27
|
+
assert.equal(ownerMonthlyUsd({ hostingShape: 'cloud-host', tier: PLAN }), RENDER_PLAN_MONTHLY_USD[PLAN]);
|
|
28
|
+
assert.ok(RENDER_PLAN_MONTHLY_USD[PLAN] > 0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await test('an unrecognised plan is unpriced, including an Object.prototype key', () => {
|
|
32
|
+
assert.equal(ownerMonthlyUsd({ hostingShape: 'cloud-host', tier: 'pro-max+pro-64gb' }), null);
|
|
33
|
+
assert.equal(ownerMonthlyUsd({ hostingShape: 'cloud-host', tier: 'constructor' }), null);
|
|
34
|
+
assert.equal(platformMonthlyUsd({ hostingShape: 'control-plane', tier: 'toString' }), null);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await test('a cloud-host costs US a correct $0: it runs on its owner\'s own account', () => {
|
|
38
|
+
assert.equal(platformMonthlyUsd({ hostingShape: 'cloud-host', tier: PLAN }), 0);
|
|
39
|
+
assert.equal(platformMonthlyUsd({ hostingShape: 'cloud-host', tier: null }), 0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await test('byo-host: $0 to us, and unpriced for the owner (their own machine)', () => {
|
|
43
|
+
assert.equal(platformMonthlyUsd({ hostingShape: 'byo-host' }), 0);
|
|
44
|
+
assert.equal(ownerMonthlyUsd({ hostingShape: 'byo-host' }), null);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await test('co-tenant shares our box: no owner is billed, and the box is priced once', () => {
|
|
48
|
+
assert.equal(platformMonthlyUsd({ hostingShape: 'co-tenant' }), 0);
|
|
49
|
+
assert.equal(ownerMonthlyUsd({ hostingShape: 'co-tenant' }), 0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await test('the control plane is OUR cost, priced by its droplet size; no size is unpriced', () => {
|
|
53
|
+
assert.equal(platformMonthlyUsd({ hostingShape: 'control-plane', tier: 's-1vcpu-2gb' }), 12);
|
|
54
|
+
assert.equal(platformMonthlyUsd({ hostingShape: 'control-plane', tier: null }), null);
|
|
55
|
+
assert.equal(ownerMonthlyUsd({ hostingShape: 'control-plane', tier: 's-1vcpu-2gb' }), 0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await test('an unknown shape is unpriced in both columns', () => {
|
|
59
|
+
assert.equal(platformMonthlyUsd({ hostingShape: 'dedicated', tier: 's-2vcpu-4gb' }), null);
|
|
60
|
+
assert.equal(ownerMonthlyUsd({ hostingShape: 'dedicated', tier: 's-2vcpu-4gb' }), null);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function recordingDb(rows) {
|
|
64
|
+
const calls = [];
|
|
65
|
+
return { calls, query: async (sql, params) => { calls.push({ sql, params }); return { rows }; } };
|
|
66
|
+
}
|
|
67
|
+
const row = (id, hosting_shape, tier = null) =>
|
|
68
|
+
({ id, slug: `p${id}`, hosting_shape, tier, status: 'active', github_login: 'owner' });
|
|
69
|
+
|
|
70
|
+
await test('the live fleet today: every total says how many rows it could not price', async () => {
|
|
71
|
+
// Measured 2026-09-24: the control plane (no size recorded) + five cloud-hosts (no plan).
|
|
72
|
+
const db = recordingDb([row(1, 'control-plane'), ...[2, 3, 4, 5, 6].map((id) => row(id, 'cloud-host'))]);
|
|
73
|
+
const out = await costLedger(db);
|
|
74
|
+
assert.deepEqual(out.totals.platform, { monthly_usd: 0, unpriced: 1 });
|
|
75
|
+
assert.deepEqual(out.totals.owner, { monthly_usd: 0, unpriced: 5 });
|
|
76
|
+
assert.equal(out.instances[0].platform_monthly_usd, null);
|
|
77
|
+
assert.equal(out.instances[1].owner_monthly_usd, null);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await test('totals sum only priced rows, per column', async () => {
|
|
81
|
+
const db = recordingDb([
|
|
82
|
+
row(1, 'control-plane', 's-1vcpu-2gb'), row(2, 'cloud-host', PLAN), row(3, 'cloud-host', PLAN),
|
|
83
|
+
row(4, 'cloud-host'), row(5, 'byo-host'),
|
|
84
|
+
]);
|
|
85
|
+
const out = await costLedger(db);
|
|
86
|
+
assert.deepEqual(out.totals.platform, { monthly_usd: 12, unpriced: 0 });
|
|
87
|
+
assert.deepEqual(out.totals.owner, { monthly_usd: 2 * RENDER_PLAN_MONTHLY_USD[PLAN], unpriced: 2 });
|
|
88
|
+
assert.equal('cost_estimate_usd' in out.instances[0], false, 'the one-column-two-questions field is gone');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await test('the query skips torn-down rows and clamps the limit', async () => {
|
|
92
|
+
const db = recordingDb([]);
|
|
93
|
+
await costLedger(db, { limit: 99999 });
|
|
94
|
+
assert.match(db.calls[0].sql, /status <> 'torn_down'/);
|
|
95
|
+
assert.deepEqual(db.calls[0].params, [2000]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
summary();
|