@bongos/core 1.19.107 → 1.19.109
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 +26 -21
- package/docs/adr/0170-featured-projection-honest-facts.md +24 -0
- package/docs/adr/README.md +1 -0
- package/modules/platform-identity/platform-identity.js +6 -2
- package/modules/platform-identity/project-stats.js +22 -4
- package/modules/platform-identity/routes/projects.js +2 -0
- package/modules/platform-identity/tests/platform-identity.mjs +11 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +11 -0
- package/public-landing/projects.html +75 -10
- package/src/module-api.js +5 -1
- package/tests/landing_page.mjs +3 -2
- package/tests/module_loader.mjs +21 -3
- package/tests/project_stats.mjs +70 -0
- package/tests/projects_hub.mjs +53 -6
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.109",
|
|
6
|
+
"core_contract": "1.19.109",
|
|
7
|
+
"source_commit": "9e8a3444891adec77e52aecda57d21cd47e0e408",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-14T14:23:16.828Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 302,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
14
|
"functional_verbatim": 1236,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1559,
|
|
20
|
+
"tree_sha256": "68310c0ca0be28e5eff0b8b223ba78bf5edbd24e7dd79018d6ac283656eaeef1",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1324,10 +1324,15 @@
|
|
|
1324
1324
|
"mode": "0000644",
|
|
1325
1325
|
"sha256": "fe1c4b38b9b3eef60ecd00ecc35eb6086690350a022d2c376d1432c95c9452f7"
|
|
1326
1326
|
},
|
|
1327
|
+
{
|
|
1328
|
+
"path": "docs/adr/0170-featured-projection-honest-facts.md",
|
|
1329
|
+
"mode": "0000644",
|
|
1330
|
+
"sha256": "165324f1fe4fe9a532b5a068a2713bd127d1d9e5926d3964869808ee1af1db47"
|
|
1331
|
+
},
|
|
1327
1332
|
{
|
|
1328
1333
|
"path": "docs/adr/README.md",
|
|
1329
1334
|
"mode": "0000644",
|
|
1330
|
-
"sha256": "
|
|
1335
|
+
"sha256": "f9bba0779b60b4a9f3014e63d49f4d5518439021f02cbab2e08d02d574edd108"
|
|
1331
1336
|
},
|
|
1332
1337
|
{
|
|
1333
1338
|
"path": "docs/api-reference.md",
|
|
@@ -3747,7 +3752,7 @@
|
|
|
3747
3752
|
{
|
|
3748
3753
|
"path": "modules/platform-identity/platform-identity.js",
|
|
3749
3754
|
"mode": "0000644",
|
|
3750
|
-
"sha256": "
|
|
3755
|
+
"sha256": "2a0ee2667f79cbe8eaeb1dfb47b0c518bd969e80a60e42e0f709fa83365f908e"
|
|
3751
3756
|
},
|
|
3752
3757
|
{
|
|
3753
3758
|
"path": "modules/platform-identity/pollers/code-sweep.js",
|
|
@@ -3757,7 +3762,7 @@
|
|
|
3757
3762
|
{
|
|
3758
3763
|
"path": "modules/platform-identity/project-stats.js",
|
|
3759
3764
|
"mode": "0000644",
|
|
3760
|
-
"sha256": "
|
|
3765
|
+
"sha256": "f3d8b2ed8bc5a32dabcbdd628fd4a504a8d91e2e2ea282a2ad2c88083f88b4e8"
|
|
3761
3766
|
},
|
|
3762
3767
|
{
|
|
3763
3768
|
"path": "modules/platform-identity/routes/my-projects.js",
|
|
@@ -3767,7 +3772,7 @@
|
|
|
3767
3772
|
{
|
|
3768
3773
|
"path": "modules/platform-identity/routes/projects.js",
|
|
3769
3774
|
"mode": "0000644",
|
|
3770
|
-
"sha256": "
|
|
3775
|
+
"sha256": "3222e294691563874be9b19dda57fe879b29d0bd174edef06f6db0f483863b11"
|
|
3771
3776
|
},
|
|
3772
3777
|
{
|
|
3773
3778
|
"path": "modules/platform-identity/routes/scouting.js",
|
|
@@ -3782,7 +3787,7 @@
|
|
|
3782
3787
|
{
|
|
3783
3788
|
"path": "modules/platform-identity/tests/platform-identity.mjs",
|
|
3784
3789
|
"mode": "0000644",
|
|
3785
|
-
"sha256": "
|
|
3790
|
+
"sha256": "7766d918d22a2d73f9275ab07d6273b66fc0cdcdc6d2952986e01bab265344d2"
|
|
3786
3791
|
},
|
|
3787
3792
|
{
|
|
3788
3793
|
"path": "modules/platform-identity/ui/.gitkeep",
|
|
@@ -4147,12 +4152,12 @@
|
|
|
4147
4152
|
{
|
|
4148
4153
|
"path": "package-lock.json",
|
|
4149
4154
|
"mode": "0000644",
|
|
4150
|
-
"sha256": "
|
|
4155
|
+
"sha256": "0ad9780c25eeea40f124f750d54beeafdb191b1c6ae104754b482eae04766059"
|
|
4151
4156
|
},
|
|
4152
4157
|
{
|
|
4153
4158
|
"path": "package.json",
|
|
4154
4159
|
"mode": "0000644",
|
|
4155
|
-
"sha256": "
|
|
4160
|
+
"sha256": "0654f3a21beb49226093259215e4baaaeb1616230b7cb86223338d41b79cf546"
|
|
4156
4161
|
},
|
|
4157
4162
|
{
|
|
4158
4163
|
"path": "public-docs/index.html",
|
|
@@ -4392,7 +4397,7 @@
|
|
|
4392
4397
|
{
|
|
4393
4398
|
"path": "public-landing/assets/cosmos.css",
|
|
4394
4399
|
"mode": "0000644",
|
|
4395
|
-
"sha256": "
|
|
4400
|
+
"sha256": "caaae45a551f60c501f68d188b2163819fd93c4aff45e341c41098865e413c03"
|
|
4396
4401
|
},
|
|
4397
4402
|
{
|
|
4398
4403
|
"path": "public-landing/buddha.png",
|
|
@@ -4412,7 +4417,7 @@
|
|
|
4412
4417
|
{
|
|
4413
4418
|
"path": "public-landing/projects.html",
|
|
4414
4419
|
"mode": "0000644",
|
|
4415
|
-
"sha256": "
|
|
4420
|
+
"sha256": "11d478f2f60c4cafba0eed25e4d56a561fc9d2aef4edb4c7a568040109caf191"
|
|
4416
4421
|
},
|
|
4417
4422
|
{
|
|
4418
4423
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
@@ -5727,7 +5732,7 @@
|
|
|
5727
5732
|
{
|
|
5728
5733
|
"path": "src/module-api.js",
|
|
5729
5734
|
"mode": "0000644",
|
|
5730
|
-
"sha256": "
|
|
5735
|
+
"sha256": "5bda15ba1c10ee2642f8db5e3d1411eb4dfeefa1858eb74af2aa5f84b37d497f"
|
|
5731
5736
|
},
|
|
5732
5737
|
{
|
|
5733
5738
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6892,7 +6897,7 @@
|
|
|
6892
6897
|
{
|
|
6893
6898
|
"path": "tests/landing_page.mjs",
|
|
6894
6899
|
"mode": "0000644",
|
|
6895
|
-
"sha256": "
|
|
6900
|
+
"sha256": "89a9d21ee1c5b00ee09de2d888873fa13c1b2b6d15ab05a6bfeb7e7a5d016a03"
|
|
6896
6901
|
},
|
|
6897
6902
|
{
|
|
6898
6903
|
"path": "tests/lifecycle-card.mjs",
|
|
@@ -7052,7 +7057,7 @@
|
|
|
7052
7057
|
{
|
|
7053
7058
|
"path": "tests/module_loader.mjs",
|
|
7054
7059
|
"mode": "0000644",
|
|
7055
|
-
"sha256": "
|
|
7060
|
+
"sha256": "092c9d9ffeed7faed34cfb49ffec6de64d86e45a8fda129444345924313b8fd7"
|
|
7056
7061
|
},
|
|
7057
7062
|
{
|
|
7058
7063
|
"path": "tests/module_manifest.mjs",
|
|
@@ -7247,12 +7252,12 @@
|
|
|
7247
7252
|
{
|
|
7248
7253
|
"path": "tests/project_stats.mjs",
|
|
7249
7254
|
"mode": "0000644",
|
|
7250
|
-
"sha256": "
|
|
7255
|
+
"sha256": "6bd78c47b6dbbdd81f862df8afdc0b514e27f0ffb71d14161ac8feb19f9fbe19"
|
|
7251
7256
|
},
|
|
7252
7257
|
{
|
|
7253
7258
|
"path": "tests/projects_hub.mjs",
|
|
7254
7259
|
"mode": "0000644",
|
|
7255
|
-
"sha256": "
|
|
7260
|
+
"sha256": "9a4de4c40bf9272143e3098c80fa8d8473ae2b441a0f8f6746c50431bfb12d9f"
|
|
7256
7261
|
},
|
|
7257
7262
|
{
|
|
7258
7263
|
"path": "tests/provision.mjs",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# 0170 — The featured projection tells time honestly: `created_at` in, `updated_at` out, owner from the project's own archon
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-08-14
|
|
5
|
+
- **Task:** [#1002778](https://cloudbongos.com/builders#/task/1002778) (goal 1000061 — the front door and the project hub)
|
|
6
|
+
- **Extends:** the C6 honesty rule ([`docs/design/projects-hub-direction.md`](../design/projects-hub-direction.md) §7) and the self-report enrichment pattern of task 1002771 (`project-stats.js`).
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
Task 1002778 enriches the hub's project card with real metadata. Its brief listed `updated_at` under "already available, no new plumbing: add it to the public projection", and offered two owner options: "resolve it to a real login + avatar via the project's own API, or omit the field until it can be answered." Implementation surfaced facts that changed both calls, and this ADR records the deviations so they are decisions, not drift.
|
|
11
|
+
|
|
12
|
+
## Decision
|
|
13
|
+
|
|
14
|
+
**1. `created_at` joins the public `/projects/featured` projection, labelled "Listed" — never "Created".** The column is set once by `DEFAULT now()` on INSERT and never touched again: it is the moment the hub first catalogued the row, not when the project began (Example predates its row by months). The card wears the honest label, and the C6 card test bans the word "Created" outright.
|
|
15
|
+
|
|
16
|
+
**2. `updated_at` stays OUT of the projection, deviating from the task brief.** Reading the code showed the column is bumped by `upsertProject`'s ON CONFLICT branch — which fires on **every client re-registration and secret rotation** — and by `setProjectFeatured`, the Archon's own curation toggle. As a public "last updated" it would claim project activity while actually reporting curation noise: a project's timestamp would refresh because someone clicked "feature". Publishing it would be the same class of lie the em-dash rules exist to prevent. The projection test now pins the SELECT list itself (`updated_at` may sort, never project; `SELECT *` is banned — the route is CORS-open and rank:public, so a star projection would leak `owner_github_id` and `status`).
|
|
17
|
+
|
|
18
|
+
**3. The owner fact is the project's own archon row — `owner_github_id` stays unread.** The catalog's `owner_github_id` calls itself "advisory attribution (not an authority)" in its own migration, is NULL on most live rows, and the design contract forbids reading it. But the leaderboard payload `project-stats.js` already downloads for the builders count carries `rank`, `github_login`, and `avatar_url` per row — and this platform's own vocabulary defines the archon as the instance's owner (`selfRel()`: "Archon is the instance's owner"). So the enrichment surfaces the archon row's login + avatar: the project's own authority answering for itself, zero extra requests, the 2-fetch-per-origin pin intact. Values are type-checked and length-capped server-side (third-party payload), and the avatar URL is scheme-checked again client-side before it becomes an `<img>`.
|
|
19
|
+
|
|
20
|
+
## Consequences
|
|
21
|
+
|
|
22
|
+
- Every card fact is something some authority actually asserted; unknowns hide rather than dash. A "last updated" fact can return later if a project's **own** API ever reports real activity recency (e.g. newest `shipped_at` from `/public/recent-shipped`) — that would be a third fetch and needs the test pin moved deliberately.
|
|
23
|
+
- On a multi-archon instance the top-credits archon is named; a zero-credit archon on a page-capped leaderboard can fall off, which renders as **no owner shown**, never a wrong one.
|
|
24
|
+
- The hub republishes a login the source project already serves unauthenticated at `/api/bongos/public/leaderboard` — aggregation, not new disclosure (grader-reviewed).
|
package/docs/adr/README.md
CHANGED
|
@@ -243,5 +243,6 @@ This keeps the decision history honest and traceable.
|
|
|
243
243
|
| 0168 | [**The scaffold vendors the hub's RUNNING core (`npm pack` of the installed package); registry-default deferred to the go-public step** ([task 1002706](https://cloudbongos.com/builders#/task/1002706); gap 13 of the project-creation audit). Two collided facts: children were born pinned to a frozen `file:vendor/bongos-core-1.19.2.tgz` (installedCoreTarball reuses the hub's own file: pin — task 1002275 — and the hub's pin sat at 1.19.2), and the task-1002621 registry cutover (hub pin now `"@bongos/core": "1.19.56"`, verified on the box) made installedCoreTarball return null — dropping the scaffold into the package-core.js source rebuild that ABORTS on a consumer instance (empty core set → empty child repo, the 1002275 failure resurrected). Fix: a three-rung ladder — file:-pin reuse → NEW `installedCorePackDir()` + `npm pack` of the hub's installed core (child pins the SAME, CURRENT core, stays file:-pinned, owner needs no token) → source rebuild. The brief's "make registry the default now that the package is public" was verified FALSE (both npm scopes 404 anonymously; ADR 0134 keeps them private until go-public) — a registry-default child would be born UNINSTALLABLE for a tokenless owner. Registry-default is deferred to the go-public step (idea 1000700); the gap-13 "flip the box env" ops leg is explicitly a no-op until then; child freshness rides this ladder + the hub-pin lane (task 1002708). Rejected: registry-default now, dropping NPM_TOKEN, vendoring from the npm cacache.](<redacted>.md) | provisioning / distribution |
|
|
244
244
|
| 0167 | [**The /create gate trust link defaults to the AGPL-3.0 license text, not the npm package page** ([task 1002705](https://cloudbongos.com/builders#/task/1002705); gap 1 of the project-creation audit). The gate's "open source" link pointed at a nonexistent repo (404); the task brief's suggested default — the npm package page — is ALSO dead for the gate's actual audience (signed-out visitors): `@cloudbongos/core` and `@bongos/core` both 404 anonymously (private scopes, ADR 0134), and the ADR 0099 public mirror stays dormant until ~2026-11. The license text is the one live, instance-neutral artifact behind the claim today. The browsable-code upgrade is the owner's filed choice (idea 1000700): public repo now (forfeits the ADR 0065 §4 redaction window) / public npm package / wait for the mirror. Rejected: the npm link (verified dead), flipping the repo public (not a builder's call), plain unlinked text (less information, same cost).](<redacted>.md) | public landing / governance |
|
|
245
245
|
| 0169 | [**`bongos serve`: the `bongos dev` boot path, on a real host** ([task 1002398](https://cloudbongos.com/builders#/task/1002398), goal 1000052). [ADR 0149](<redacted>.md) gave an owner one command to stand an instance up on their laptop; "now put it on a server" had no equally good answer — Docker serves the *vanilla* brand on `:3000`, `bongos onboard` provisions a whole droplet through the control plane, and [`instance-deploy.md`](../recipes/instance-deploy.md) left the systemd unit as a hand-written prose step. The six steps (deps → Postgres → createdb → migrate → env → server → health) were already right for a host; only four *decisions inside them* were laptop-shaped. So: a `--server` **mode** on the same script (`bongos serve`), never a second boot path, so the two can't drift. (1) **Supervision** — a systemd unit (`Restart=always`, `WantedBy=multi-user.target`) instead of a pid file that dies on reboot, falling back to the pid-managed process where systemd is absent rather than refusing. (2) **Bind address, derived from the origin** — `https://…` means a proxy terminates TLS, so bind `127.0.0.1`; plain `http://` on a public host means nothing is in front, so bind `0.0.0.0`. The common misconfiguration (app publicly reachable on its raw port behind a proxy) becomes impossible by default. (3) **Peer-auth bootstrap** — a fresh `apt-get install postgresql` cluster has only the `postgres` role, so a deploy user's `createdb` fails with `role "deploy" does not exist`; that reads like a broken install and is really an unbootstrapped cluster, so we create the login role as the superuser. (4) **Health means the PUBLIC origin** — the loopback probe proves the app; DNS/proxy/TLS are where a host actually fails, so it probes the advertised origin too and names the missing layer (advisory — DNS may legitimately not be wired). Secrets go to a root-owned `0600` `EnvironmentFile`, never `Environment=` (`systemctl show` leaks those, and the laptop path's `buildEnv()` spreads the caller's whole shell). Every decision is a pure injectable in `dev-lib.js`, proved in `tests/dev_lib.mjs` with no host. Rejected: a separate `deploy.sh` (drift — the point is the *same* six steps), Docker-on-the-host (makes brand+port a compose override), always binding `0.0.0.0` (exposes the raw port), refusing without systemd (breaks containers/BSD).](<redacted>.md) | onboarding / deploy |
|
|
246
|
+
| 0170 | [**The featured projection tells time honestly: `created_at` in, `updated_at` out, owner from the project's own archon** ([task 1002778](https://cloudbongos.com/builders#/task/1002778), goal 1000061). The hub card's metadata pass added `created_at` to the public `/projects/featured` projection, labelled **Listed** — it is the moment the hub catalogued the row, never when the project began, and the C6 card test bans the word "Created". Two recorded deviations: (1) the task brief said `updated_at` was ready to add, but the column is bumped by every client re-registration AND the Archon's feature toggle, so as a public "last updated" it would report curation noise as project activity — it may sort, never project, and the test pins the SELECT list itself (`SELECT *` banned: the route is CORS-open rank:public and a star would leak `owner_github_id`/`status`). (2) The owner fact reads the project's OWN leaderboard archon row (login + avatar, type-checked and capped) from the payload project-stats already fetches — zero extra requests, the 2-fetch pin intact, the advisory `owner_github_id` column still unread per the design contract. Multi-archon: top-credits archon is named; a fallen-off-the-page archon renders as no owner shown, never a wrong one. Rejected: projecting `updated_at` as asked (it lies), reading `owner_github_id` (advisory, contract-banned), a third per-origin fetch for activity recency (moves the test pin for a fact nobody asked for yet).](<redacted>.md) | platform identity / public surface |
|
|
246
247
|
|
|
247
248
|
> ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
|
|
@@ -238,10 +238,14 @@ async function getProject(id, { pool = defaultPool } = {}) {
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
// The PUBLIC Explore listing — featured + active projects only, public projection
|
|
241
|
-
// (no owner_github_id, no status). Newest first.
|
|
241
|
+
// (no owner_github_id, no status). Newest first. created_at is the LISTED date —
|
|
242
|
+
// when the hub first catalogued the row, not when the project began — and the
|
|
243
|
+
// card labels it "Listed" for exactly that reason (task 1002778). updated_at may
|
|
244
|
+
// sort but is never projected: feature toggles and client re-registration bump
|
|
245
|
+
// it, so as a public "last updated" it would lie.
|
|
242
246
|
async function listFeaturedProjects({ pool = defaultPool } = {}) {
|
|
243
247
|
const { rows } = await pool.query(
|
|
244
|
-
`SELECT origin, name, tagline, art_url
|
|
248
|
+
`SELECT origin, name, tagline, art_url, created_at
|
|
245
249
|
FROM platform_identity_projects
|
|
246
250
|
WHERE featured = true AND status = 'active'
|
|
247
251
|
ORDER BY updated_at DESC`,
|
|
@@ -136,8 +136,26 @@ async function fetchStats(origin) {
|
|
|
136
136
|
]);
|
|
137
137
|
|
|
138
138
|
let builders = null;
|
|
139
|
+
let owner = null;
|
|
140
|
+
let ownerAvatar = null;
|
|
139
141
|
if (lb.status === 'fulfilled' && Array.isArray(lb.value?.leaderboard)) {
|
|
140
|
-
|
|
142
|
+
const people = lb.value.leaderboard.filter((b) => !isSystemAccount(b?.github_login));
|
|
143
|
+
builders = people.length;
|
|
144
|
+
// The archon row IS the instance's owner — the project's own authority
|
|
145
|
+
// answering for itself, from a payload already in hand (ADR 0170). The
|
|
146
|
+
// catalog's owner_github_id ("advisory attribution (not an authority)")
|
|
147
|
+
// stays unread. Caveat: the leaderboard is credits-ordered and page-capped,
|
|
148
|
+
// so a zero-credit archon on a very large instance can fall off it — that
|
|
149
|
+
// renders as no owner shown, never as a wrong one.
|
|
150
|
+
// The payload is third-party: accept only a plausible login string (GitHub
|
|
151
|
+
// caps logins at 39 chars; 64 leaves headroom without hosting a novel),
|
|
152
|
+
// and only an http(s) avatar URL — the client scheme-checks again before
|
|
153
|
+
// it becomes an <img>.
|
|
154
|
+
const arch = people.find((b) => b?.rank === 'archon');
|
|
155
|
+
const login = arch?.github_login;
|
|
156
|
+
owner = typeof login === 'string' && login ? login.slice(0, 64) : null;
|
|
157
|
+
const av = arch?.avatar_url;
|
|
158
|
+
ownerAvatar = owner && typeof av === 'string' && /^https?:\/\//.test(av) ? av.slice(0, 300) : null;
|
|
141
159
|
}
|
|
142
160
|
|
|
143
161
|
let tags = null;
|
|
@@ -149,7 +167,7 @@ async function fetchStats(origin) {
|
|
|
149
167
|
.sort();
|
|
150
168
|
}
|
|
151
169
|
|
|
152
|
-
return { builders, tags };
|
|
170
|
+
return { builders, tags, owner, owner_avatar: ownerAvatar };
|
|
153
171
|
}
|
|
154
172
|
|
|
155
173
|
function refresh(origin) {
|
|
@@ -163,7 +181,7 @@ function refresh(origin) {
|
|
|
163
181
|
// Remember the FAILURE for a full TTL too. Without this a permanently
|
|
164
182
|
// unreachable project is re-fetched on every request and pays the timeout
|
|
165
183
|
// every time — the exact way one dead project slows the whole page.
|
|
166
|
-
const stats = { builders: null, tags: null };
|
|
184
|
+
const stats = { builders: null, tags: null, owner: null, owner_avatar: null };
|
|
167
185
|
_cache.set(origin, { at: Date.now(), stats });
|
|
168
186
|
return stats;
|
|
169
187
|
})
|
|
@@ -197,7 +215,7 @@ async function getProjectStats(origins = []) {
|
|
|
197
215
|
// null-prototype: an origin literally equal to '__proto__' would otherwise
|
|
198
216
|
// retarget the object's prototype instead of adding a key
|
|
199
217
|
const out = Object.create(null);
|
|
200
|
-
for (const origin of origins) out[origin] = _cache.get(origin)?.stats || { builders: null, tags: null };
|
|
218
|
+
for (const origin of origins) out[origin] = _cache.get(origin)?.stats || { builders: null, tags: null, owner: null, owner_avatar: null };
|
|
201
219
|
return out;
|
|
202
220
|
}
|
|
203
221
|
|
|
@@ -119,6 +119,8 @@ module.exports = function projectsRoutes() {
|
|
|
119
119
|
...p,
|
|
120
120
|
builders: stats[p.origin]?.builders ?? null,
|
|
121
121
|
tags: stats[p.origin]?.tags ?? null,
|
|
122
|
+
owner: stats[p.origin]?.owner ?? null,
|
|
123
|
+
owner_avatar: stats[p.origin]?.owner_avatar ?? null,
|
|
122
124
|
})),
|
|
123
125
|
});
|
|
124
126
|
} catch (err) {
|
|
@@ -198,12 +198,22 @@ await ta('upsertProject upserts on normalized origin; update preserves tagline/a
|
|
|
198
198
|
});
|
|
199
199
|
|
|
200
200
|
await ta('listFeaturedProjects returns a PUBLIC projection (featured+active only, no owner/status)', async () => {
|
|
201
|
-
const pool = mockPool([{ rows: [{ origin: 'https://p', name: 'Proj', tagline: 't', art_url: null }] }]);
|
|
201
|
+
const pool = mockPool([{ rows: [{ origin: 'https://p', name: 'Proj', tagline: 't', art_url: null, created_at: '2026-08-01T00:00:00Z' }] }]);
|
|
202
202
|
const out = await P.listFeaturedProjects({ pool });
|
|
203
203
|
assert.equal(out.length, 1);
|
|
204
204
|
const c = pool.calls[0];
|
|
205
205
|
assert.match(c.text, /WHERE featured = true AND status = 'active'/);
|
|
206
|
+
// Pin the SELECT LIST itself, not the whole query — an ORDER BY mention must
|
|
207
|
+
// not satisfy a projection guard, and SELECT * must never sneak owner/status
|
|
208
|
+
// onto a CORS-open rank:public route.
|
|
209
|
+
const sel = (c.text.match(/SELECT([\s\S]*?)FROM/i) || [, ''])[1];
|
|
210
|
+
assert.ok(sel.trim(), 'the projection is an explicit SELECT list');
|
|
211
|
+
assert.doesNotMatch(sel, /\*/, 'no star projection on a public route');
|
|
212
|
+
assert.match(sel, /created_at/, 'the LISTED date (when the row was catalogued) is public');
|
|
206
213
|
assert.doesNotMatch(c.text, /owner_github_id/, 'public projection must not leak owner id');
|
|
214
|
+
// updated_at may sort (ORDER BY) but never project: it is bumped by feature
|
|
215
|
+
// toggles and client re-registration, so as a public fact it would lie.
|
|
216
|
+
assert.doesNotMatch(sel, /updated_at/, 'updated_at is curation noise — never projected');
|
|
207
217
|
});
|
|
208
218
|
|
|
209
219
|
await ta('setProjectFeatured flips the flag (Archon curation)', async () => {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.109",
|
|
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.109",
|
|
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.109",
|
|
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",
|
|
@@ -581,6 +581,17 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
581
581
|
border-radius:999px;background:var(--accent-soft);color:var(--accent-deep);margin-bottom:18px;}
|
|
582
582
|
:root[data-mode="dark"] .projCard .cardRel{color:var(--sun-lit);}
|
|
583
583
|
@media (prefers-color-scheme: dark){:root:not([data-mode="light"]) .projCard .cardRel{color:var(--sun-lit);}}
|
|
584
|
+
/* the facts strip (task 1002778): a label/value grid, centered as a block but
|
|
585
|
+
read left-aligned. A hidden pair leaves no gap — the grid row collapses. */
|
|
586
|
+
.projCard .cardMeta{display:grid;grid-template-columns:auto auto;justify-content:center;
|
|
587
|
+
gap:6px 14px;margin-bottom:18px;text-align:left;}
|
|
588
|
+
.projCard .cardMeta dt{font-family:var(--font-mono);font-size:11px;letter-spacing:.04em;
|
|
589
|
+
text-transform:uppercase;color:var(--ink-faint);align-self:center;}
|
|
590
|
+
.projCard .cardMeta dd{font-size:13.5px;color:var(--ink-soft);font-weight:600;}
|
|
591
|
+
.projCard .cardMeta .ownerAv{width:16px;height:16px;border-radius:50%;vertical-align:-3px;margin-right:6px;}
|
|
592
|
+
.projCard .cardMeta .tags{display:flex;gap:6px;flex-wrap:wrap;}
|
|
593
|
+
.projCard .cardMeta .tags span{font-family:var(--font-mono);font-size:10.5px;padding:2px 8px;
|
|
594
|
+
border-radius:999px;border:1px solid var(--line);color:var(--ink-faint);white-space:nowrap;}
|
|
584
595
|
.projCard .cardActions{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;}
|
|
585
596
|
.projCard .cardX{position:absolute;top:10px;right:12px;width:30px;height:30px;border-radius:50%;
|
|
586
597
|
border:none;background:none;cursor:pointer;font-size:14px;color:var(--ink-faint);line-height:1;}
|
|
@@ -213,6 +213,19 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
213
213
|
<h3 id="cardName"></h3>
|
|
214
214
|
<p class="cardTag" id="cardTag"></p>
|
|
215
215
|
<p class="cardRel" id="cardRel" hidden></p>
|
|
216
|
+
<!-- The facts strip. A definition list, never nested block wrappers, on
|
|
217
|
+
purpose: the C6 card test slices the card non-greedily up to the
|
|
218
|
+
first adjacent pair of closing wrapper tags, and a nested wrapper
|
|
219
|
+
would silently shorten what that test guards. A fact with no answer
|
|
220
|
+
hides, label and value together, because "Owner: dash" on the most
|
|
221
|
+
authoritative-looking element of the page reads as broken, not
|
|
222
|
+
honest (task 1002778). -->
|
|
223
|
+
<dl class="cardMeta" id="cardMeta" hidden>
|
|
224
|
+
<dt id="cardBuildersL" hidden>Builders</dt><dd id="cardBuilders" hidden></dd>
|
|
225
|
+
<dt id="cardRunsL" hidden>Runs</dt><dd id="cardRuns" hidden></dd>
|
|
226
|
+
<dt id="cardOwnerL" hidden>Owner</dt><dd id="cardOwner" hidden></dd>
|
|
227
|
+
<dt id="cardListedL" hidden>Listed</dt><dd id="cardListed" hidden></dd>
|
|
228
|
+
</dl>
|
|
216
229
|
<div class="cardActions" id="cardActions"></div>
|
|
217
230
|
</div>
|
|
218
231
|
</div>
|
|
@@ -511,8 +524,10 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
511
524
|
The public catalog: GET /projects/featured is the PUBLIC projection
|
|
512
525
|
(featured + active only, no auth), which is what lets a signed-out visitor
|
|
513
526
|
land on the map at all. The richer GET /projects needs project.curate and
|
|
514
|
-
would 401 them. It carries origin/name/tagline/art_url
|
|
515
|
-
|
|
527
|
+
would 401 them. It carries origin/name/tagline/art_url/created_at plus
|
|
528
|
+
the builders/tags/owner each project's OWN public API reports (task
|
|
529
|
+
1002778) — anything a project could not report renders as a dash or hides,
|
|
530
|
+
never as an invented value.
|
|
516
531
|
The orbs are placed on fixed slots: where things sit is this pass; how the
|
|
517
532
|
sky moves is goal 1000031. ══ */
|
|
518
533
|
|
|
@@ -604,11 +619,14 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
604
619
|
/* A tag is a module the project RUNS beyond the core set — a fact it declares
|
|
605
620
|
about itself, not a guess about what it is. Capped so one heavily-moduled
|
|
606
621
|
project cannot blow the column open; the overflow is counted, never hidden
|
|
607
|
-
silently. null = we could not ask (em dash); [] = asked, runs nothing
|
|
608
|
-
core
|
|
622
|
+
silently. null = we could not ask (em dash); [] = asked, runs nothing
|
|
623
|
+
beyond core — a real answer, voiced as "core" (the owner's call,
|
|
624
|
+
2026-08-14) so a vanilla instance stops reading as a fetch failure. One
|
|
625
|
+
formatter, two views: the table cell and the card must never disagree. */
|
|
609
626
|
var TAG_CAP = 3;
|
|
610
627
|
function tagsHtml(tags) {
|
|
611
|
-
if (!Array.isArray(tags)
|
|
628
|
+
if (!Array.isArray(tags)) return '—';
|
|
629
|
+
if (!tags.length) return 'core';
|
|
612
630
|
var shown = tags.slice(0, TAG_CAP).map(function (t) { return '<span>' + esc(t) + '</span>'; });
|
|
613
631
|
if (tags.length > TAG_CAP) shown.push('<span>+' + (tags.length - TAG_CAP) + '</span>');
|
|
614
632
|
return '<span class="tags">' + shown.join('') + '</span>';
|
|
@@ -791,11 +809,14 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
791
809
|
2026-08-13. The card is layered OVER the links rather than replacing them,
|
|
792
810
|
so ctrl/middle-click still opens a project's site directly.
|
|
793
811
|
|
|
794
|
-
It states
|
|
795
|
-
your relationship, a door —
|
|
796
|
-
and
|
|
797
|
-
|
|
798
|
-
|
|
812
|
+
It states what the catalog and the project ITSELF can answer: name,
|
|
813
|
+
tagline, your relationship, a door — and, when known, builders, runs,
|
|
814
|
+
owner and the Listed date (task 1002778). Builders/runs/owner are each
|
|
815
|
+
project's own public API answering for itself (project-stats.js); Listed
|
|
816
|
+
is created_at, when the hub catalogued the row — NOT when the project
|
|
817
|
+
began, which is why the label is never "Created". Liveness stays out: a
|
|
818
|
+
catalog row is never probed, and a chip here would be the C6 lie the
|
|
819
|
+
table's em dashes exist to avoid. An unanswerable fact hides. ══ */
|
|
799
820
|
|
|
800
821
|
var cardReturn = null; /* the element focus came from, to hand it back */
|
|
801
822
|
|
|
@@ -804,6 +825,26 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
804
825
|
return null;
|
|
805
826
|
}
|
|
806
827
|
|
|
828
|
+
/* The owner fact is a login and, when the project reported one, an avatar.
|
|
829
|
+
Both arrive from a third-party payload: the login is escaped, the avatar
|
|
830
|
+
URL scheme-checked (a javascript: URL must never become an img src). */
|
|
831
|
+
function ownerHtml(p) {
|
|
832
|
+
if (!p.owner) return '';
|
|
833
|
+
var av = safeHttpUrl(p.owner_avatar);
|
|
834
|
+
return (av ? '<img class="ownerAv" src="' + esc(av) + '" alt="" loading="lazy">' : '') +
|
|
835
|
+
'<span>' + esc(String(p.owner)) + '</span>';
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/* A fact either has an answer or takes no space: value and label hide as a
|
|
839
|
+
pair. Every field is re-written on every open — closeCard() clears nothing,
|
|
840
|
+
so a stale value would otherwise survive into the next project's card. */
|
|
841
|
+
function cardFact(id, value, isHtml) {
|
|
842
|
+
var dd = $(id);
|
|
843
|
+
if (isHtml) dd.innerHTML = value || ''; else dd.textContent = value || '';
|
|
844
|
+
dd.hidden = $(id + 'L').hidden = !value;
|
|
845
|
+
return !!value;
|
|
846
|
+
}
|
|
847
|
+
|
|
807
848
|
function openCard(p, rel, returnTo) {
|
|
808
849
|
var href = p.isSelf ? '/' : safeHttpUrl(p.origin);
|
|
809
850
|
$('cardDot').style.setProperty('--acc', p.isSelf ? 'var(--accent)' : accentOf(p));
|
|
@@ -812,6 +853,16 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
812
853
|
var label = rel === 'owner' ? 'yours' : rel === 'builder' ? 'you build here' : '';
|
|
813
854
|
$('cardRel').textContent = label;
|
|
814
855
|
$('cardRel').hidden = !label;
|
|
856
|
+
/* builders: 0 is a real answer ("nobody has joined yet"), null is "we could
|
|
857
|
+
not ask" and hides. runs: [] is a real answer too — the project declares
|
|
858
|
+
nothing beyond the core set, which the owner voiced as "core". */
|
|
859
|
+
var any = cardFact('cardBuilders', p.builders == null ? '' : String(p.builders));
|
|
860
|
+
any = cardFact('cardRuns', Array.isArray(p.tags) ? tagsHtml(p.tags) : '', true) || any;
|
|
861
|
+
/* when the rel chip already says "yours", naming the viewer as Owner is
|
|
862
|
+
noise — the fact answers strangers, not the person it describes */
|
|
863
|
+
any = cardFact('cardOwner', rel === 'owner' ? '' : ownerHtml(p), true) || any;
|
|
864
|
+
any = cardFact('cardListed', p.created_at ? listedDate(p.created_at) : '') || any;
|
|
865
|
+
$('cardMeta').hidden = !any;
|
|
815
866
|
/* the platform's door is same-tab: / is this site, and a new tab for the
|
|
816
867
|
page you are already on is a bug, not a courtesy */
|
|
817
868
|
$('cardActions').innerHTML = href
|
|
@@ -909,6 +960,15 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
909
960
|
if (!rows.length) return;
|
|
910
961
|
var real = rows.filter(function (b) { return !/-system$/.test(String(b.github_login || '')); });
|
|
911
962
|
$('selfBuilders').textContent = String(real.length);
|
|
963
|
+
/* the card reads these too: the same payload answers the platform's
|
|
964
|
+
builder count and its owner — the archon row — by the same rule the
|
|
965
|
+
server-side enrichment applies to every other project */
|
|
966
|
+
SELF_PROJECT.builders = real.length;
|
|
967
|
+
var arch = real.filter(function (b) { return b.rank === 'archon'; })[0];
|
|
968
|
+
if (arch && arch.github_login) {
|
|
969
|
+
SELF_PROJECT.owner = String(arch.github_login);
|
|
970
|
+
SELF_PROJECT.owner_avatar = arch.avatar_url || null;
|
|
971
|
+
}
|
|
912
972
|
})
|
|
913
973
|
.catch(function () { /* unreachable → the dash already in the markup stands */ });
|
|
914
974
|
}
|
|
@@ -1092,6 +1152,11 @@ try{var _m=localStorage.getItem('cb-color-mode');if(_m==='light'||_m==='dark')do
|
|
|
1092
1152
|
function shortDate(iso) {
|
|
1093
1153
|
return iso ? new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : '';
|
|
1094
1154
|
}
|
|
1155
|
+
/* the year matters on the card: catalogue entries sit years apart, and
|
|
1156
|
+
"Aug 13" with no year reads as recent activity, not a date of record */
|
|
1157
|
+
function listedDate(iso) {
|
|
1158
|
+
return iso ? new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) : '';
|
|
1159
|
+
}
|
|
1095
1160
|
/* "since 14:02" today, "since Aug 7" earlier — for the unreachable chip */
|
|
1096
1161
|
function clockOrDate(ts) {
|
|
1097
1162
|
var d = new Date(ts);
|
package/src/module-api.js
CHANGED
|
@@ -641,8 +641,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
641
641
|
// landed since 1.19.105 with no explicit bump. run 31759315328. (task 1002620)
|
|
642
642
|
// 1.19.107 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
643
643
|
// landed since 1.19.106 with no explicit bump. run 31762781737. (task 1002620)
|
|
644
|
+
// 1.19.108 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
645
|
+
// landed since 1.19.107 with no explicit bump. run 31803763842. (task 1002620)
|
|
646
|
+
// 1.19.109 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
647
|
+
// landed since 1.19.108 with no explicit bump. run 31809188532. (task 1002620)
|
|
644
648
|
// ---------------------------------------------------------------------------
|
|
645
|
-
const CORE_VERSION = '1.19.
|
|
649
|
+
const CORE_VERSION = '1.19.109'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
646
650
|
|
|
647
651
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
648
652
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/landing_page.mjs
CHANGED
|
@@ -262,8 +262,9 @@ test('the proof band ships hidden so a failed or empty feed leaves no orphan hea
|
|
|
262
262
|
const CODE = HTML.split('<script>').pop().replace(/\/\*[\s\S]*?\*\//g, '');
|
|
263
263
|
|
|
264
264
|
test('the catalog claims no liveness the public feed cannot back', () => {
|
|
265
|
-
// listFeaturedProjects projects {origin, name, tagline, art_url}
|
|
266
|
-
//
|
|
265
|
+
// listFeaturedProjects projects {origin, name, tagline, art_url, created_at}
|
|
266
|
+
// plus self-reported stats — still no status, so a verification chip here
|
|
267
|
+
// would be a claim nothing checked.
|
|
267
268
|
assert.doesNotMatch(CODE, /chip (live|maybe|down)/);
|
|
268
269
|
assert.doesNotMatch(CODE, /ago/);
|
|
269
270
|
});
|
package/tests/module_loader.mjs
CHANGED
|
@@ -170,16 +170,34 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
|
|
|
170
170
|
// then status-ui (R84) + hall-ui (R85) the two web-surface carves; and
|
|
171
171
|
// provisioning (ADR 0111 / task 1942) — a net-new feature module (instance hosting);
|
|
172
172
|
// platform-identity (ADR 0141) the hub IdP; and governance (ADR 0151 / BV1.R94) the
|
|
173
|
-
// configurable roles/permissions module
|
|
173
|
+
// configurable roles/permissions module; and agents (ADR 0083 / goal 1000038) the
|
|
174
|
+
// declarative agent registry — the first module to ship default:false, which is
|
|
175
|
+
// why it is DISCOVERED here (discovery is enablement-independent) while staying
|
|
176
|
+
// off in enabledModules(). All are discovered; none has validation
|
|
174
177
|
// errors or coreVersion mismatches. Order is the readdir sort.
|
|
175
178
|
//
|
|
176
179
|
// NB this is the CORE package tree: game / art-pipeline / character-anim / demo are
|
|
177
180
|
// instance/content-excluded from the published core (ADR 0098) so they are not
|
|
178
|
-
// present here — the discovered set is the
|
|
181
|
+
// present here — the discovered set is the 18 always-shipped core-domain + feature
|
|
179
182
|
// modules below.
|
|
183
|
+
//
|
|
184
|
+
// ⚠ ADDING A MODULE? This roster is HARDCODED and you must extend it — otherwise
|
|
185
|
+
// this test reds and your PR cannot land (task 1002787: the agents manifest hit
|
|
186
|
+
// exactly this). It stays hardcoded on purpose: unlike the fitness roster check,
|
|
187
|
+
// which compares two INDEPENDENT disk reads (tests/helpers.mjs onDiskModuleKeys vs
|
|
188
|
+
// fitness.js discoveredModuleKeys), this assertion pins what the PUBLISHED CORE
|
|
189
|
+
// ships (ADR 0098 content-exclusion). Deriving it from the same disk the loader
|
|
190
|
+
// just read would make it tautological — it could no longer catch the loader
|
|
191
|
+
// silently dropping or duplicating a module, which is the thing it exists to catch.
|
|
192
|
+
//
|
|
193
|
+
// ⚠ AND CHECK WHICH TESTS FAIL, NOT HOW MANY FILES. On Windows, origin/main already
|
|
194
|
+
// fails ~21 test FILES, this one among them (see the dedupeRoots test below — a path
|
|
195
|
+
// -resolution env issue). A before/after run compared at FILE granularity therefore
|
|
196
|
+
// reads 21 = 21 and looks clean while a NEW failure hides inside an already-red file.
|
|
197
|
+
// That is precisely how the agents regression reached an open PR (task 1002485).
|
|
180
198
|
loader._reset();
|
|
181
199
|
const snap = loader.loaderSnapshot();
|
|
182
|
-
assert.deepEqual(snap.loaded, ['autonomy', 'builder-settings', 'dev-box', 'discord', 'economy', 'governance', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui']);
|
|
200
|
+
assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'dev-box', 'discord', 'economy', 'governance', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui']);
|
|
183
201
|
assert.deepEqual(snap.errors, []);
|
|
184
202
|
assert.deepEqual(snap.skipped, []);
|
|
185
203
|
loader._reset();
|
package/tests/project_stats.mjs
CHANGED
|
@@ -58,6 +58,76 @@ test('system accounts are excluded — they are machines, not builders', async (
|
|
|
58
58
|
assert.equal(out['https://8.8.8.8'].builders, 2, 'four rows, two of them machines');
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
test("the owner is the project's own archon row, never the advisory catalog column", async () => {
|
|
62
|
+
// platform_identity_projects.owner_github_id is "advisory attribution (not an
|
|
63
|
+
// authority)" and stays out of every public projection. The project's own
|
|
64
|
+
// leaderboard — already fetched for the builders count — carries rank, and its
|
|
65
|
+
// archon row IS the instance's owner, reported by the one authority that knows.
|
|
66
|
+
stub({
|
|
67
|
+
'https://8.8.8.8': {
|
|
68
|
+
leaderboard: [
|
|
69
|
+
{ github_login: 'zed', rank: 'metic' },
|
|
70
|
+
{ github_login: 'amy', rank: 'archon', avatar_url: 'https://avatars.example.com/amy.png' },
|
|
71
|
+
],
|
|
72
|
+
enabled: {},
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
const out = await stats.getProjectStats(['https://8.8.8.8']);
|
|
76
|
+
assert.equal(out['https://8.8.8.8'].owner, 'amy');
|
|
77
|
+
assert.equal(out['https://8.8.8.8'].owner_avatar, 'https://avatars.example.com/amy.png');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('a non-http avatar from a third-party payload is dropped, the owner kept', async () => {
|
|
81
|
+
stub({
|
|
82
|
+
'https://8.8.8.8': {
|
|
83
|
+
leaderboard: [{ github_login: 'amy', rank: 'archon', avatar_url: 'javascript:alert(1)' }],
|
|
84
|
+
enabled: {},
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
const out = await stats.getProjectStats(['https://8.8.8.8']);
|
|
88
|
+
assert.equal(out['https://8.8.8.8'].owner, 'amy');
|
|
89
|
+
assert.equal(out['https://8.8.8.8'].owner_avatar, null);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('no archon on the page means no owner claimed', async () => {
|
|
93
|
+
stub({
|
|
94
|
+
'https://8.8.8.8': {
|
|
95
|
+
leaderboard: [{ github_login: 'zed', rank: 'metic' }],
|
|
96
|
+
enabled: {},
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
const out = await stats.getProjectStats(['https://8.8.8.8']);
|
|
100
|
+
assert.equal(out['https://8.8.8.8'].owner, null, 'unknown must stay unknown, not guessed');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('a machine account is never an owner, even wearing the archon rank', async () => {
|
|
104
|
+
stub({
|
|
105
|
+
'https://8.8.8.8': {
|
|
106
|
+
leaderboard: [{ github_login: 'bfg-system', rank: 'archon' }],
|
|
107
|
+
enabled: {},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
const out = await stats.getProjectStats(['https://8.8.8.8']);
|
|
111
|
+
assert.equal(out['https://8.8.8.8'].owner, null);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('a project whose fetches reject answers a null owner', async () => {
|
|
115
|
+
// allSettled means a rejecting fetch still travels the fulfilled path —
|
|
116
|
+
// owner stays its initialized null, and null is what reaches the wire
|
|
117
|
+
globalThis.fetch = () => Promise.reject(new Error('down'));
|
|
118
|
+
const out = await stats.getProjectStats(['https://9.9.9.9']);
|
|
119
|
+
assert.equal(out['https://9.9.9.9'].owner, null);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('every failure fallback carries the owner key — dead answers are null, never undefined', async () => {
|
|
123
|
+
// a bad-scheme origin makes publicUrl throw synchronously — the ONE path
|
|
124
|
+
// that reaches the cached-failure literal (a rejecting fetch never does)
|
|
125
|
+
const out = await stats.getProjectStats(['file:///etc/passwd']);
|
|
126
|
+
assert.ok('owner' in out['file:///etc/passwd'], 'the failure literal must carry the key');
|
|
127
|
+
assert.equal(out['file:///etc/passwd'].owner, null);
|
|
128
|
+
assert.ok('owner_avatar' in out['file:///etc/passwd'], 'both fallback literals share one shape');
|
|
129
|
+
});
|
|
130
|
+
|
|
61
131
|
test('a tag is a NON-core module — core modules carry no information', async () => {
|
|
62
132
|
// every instance runs the core set, so including it would print the same
|
|
63
133
|
// fifteen tags on every row
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -166,8 +166,9 @@ test('catalog origins are scheme-checked before they become hrefs', () => {
|
|
|
166
166
|
});
|
|
167
167
|
|
|
168
168
|
test('liveness is never claimed for a catalog row the hub did not probe (C6)', () => {
|
|
169
|
-
// the public feed carries
|
|
170
|
-
// chip saying "live" here
|
|
169
|
+
// the public feed carries identity, the Listed date and self-reported stats
|
|
170
|
+
// (builders/tags/owner) — still no liveness, so a chip saying "live" here
|
|
171
|
+
// would assert more than anything verified
|
|
171
172
|
assert.match(SCRIPT, /chip calm/, 'an unprobed catalog row gets the calm chip');
|
|
172
173
|
assert.doesNotMatch(SCRIPT, /chip live[^']*'\s*\+\s*esc\(p\./,
|
|
173
174
|
'no catalog row may be rendered as live');
|
|
@@ -360,12 +361,21 @@ test('focus enters the card and is handed back on close', () => {
|
|
|
360
361
|
assert.match(SCRIPT, /cardReturn\.focus\(\)/);
|
|
361
362
|
});
|
|
362
363
|
|
|
363
|
-
test('the card claims
|
|
364
|
+
test('the card claims only what the catalog and the project itself answer (C6)', () => {
|
|
364
365
|
const card = (HUB.match(/<div class="cardScrim"[\s\S]*?<\/div>\s*<\/div>/) || [''])[0];
|
|
365
|
-
// /projects/featured
|
|
366
|
-
|
|
366
|
+
// /projects/featured serves origin, name, tagline, art_url, created_at, and the
|
|
367
|
+
// builders/tags/owner enrichment each project's OWN public API reports about
|
|
368
|
+
// itself (task 1002778). The card may state those. It still may not claim
|
|
369
|
+
// liveness nothing probed, and the catalogued date must never masquerade as a
|
|
370
|
+
// creation date — created_at is when the hub first heard of the project.
|
|
371
|
+
for (const invented of [/\blive\b/i, /uptime/i, /\bCreated\b/]) {
|
|
367
372
|
assert.doesNotMatch(card, invented, `the card must not invent ${invented}`);
|
|
368
373
|
}
|
|
374
|
+
for (const id of ['cardBuilders', 'cardRuns', 'cardOwner', 'cardListed']) {
|
|
375
|
+
assert.match(card, new RegExp(`id="${id}"`), `the ${id} fact must be real markup (C7)`);
|
|
376
|
+
}
|
|
377
|
+
assert.match(card, />Listed</, 'the catalogue date wears its honest label');
|
|
378
|
+
assert.doesNotMatch(card, /—/, 'an unanswerable card fact hides; it never dashes');
|
|
369
379
|
});
|
|
370
380
|
|
|
371
381
|
test('the row keeps its own Open link working underneath the card', () => {
|
|
@@ -421,7 +431,8 @@ test('Type left no styling behind, and the live columns kept theirs', () => {
|
|
|
421
431
|
/* ── Real Builders + Runs (task 1002771) ────────────────────────────────────
|
|
422
432
|
The columns came back only once something could fill them: each project's
|
|
423
433
|
OWN public API. The distinction these pin is between "we could not ask"
|
|
424
|
-
(null → em dash) and "we asked, the answer is none" (0
|
|
434
|
+
(null → em dash) and "we asked, the answer is none" (0 builders; tags []
|
|
435
|
+
voiced as "core" since task 1002778 — the owner's call, 2026-08-14). ── */
|
|
425
436
|
|
|
426
437
|
test('unknown and zero are rendered as different things', () => {
|
|
427
438
|
// 0 builders is a real answer — "nobody has joined yet". null is not an
|
|
@@ -435,6 +446,33 @@ test('a project that cannot be reached degrades to a dash, not to a wrong number
|
|
|
435
446
|
assert.ok(fn, 'tagsHtml() must exist');
|
|
436
447
|
assert.match(fn, /Array\.isArray\(tags\)/, 'a non-array (null) must not be spread into the cell');
|
|
437
448
|
assert.match(fn, /return '—'/);
|
|
449
|
+
// [] is a different fact from null and reads differently: the project WAS
|
|
450
|
+
// asked and declares nothing beyond the core set. One formatter serves the
|
|
451
|
+
// table cell and the card, so the two views cannot disagree (task 1002778).
|
|
452
|
+
assert.match(fn, /return 'core'/, 'an empty list is a real answer, never a dash');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test('each card fact is wired to its data, not just present in the markup', () => {
|
|
456
|
+
// Mutation-found gap: the C6 test pins the markup ids, but nothing bound
|
|
457
|
+
// them to openCard's data — the whole facts strip could be deleted from the
|
|
458
|
+
// JS with every test green. These pin the wire, one fact at a time.
|
|
459
|
+
assert.match(SCRIPT, /function cardFact/, 'the card fact writer must exist');
|
|
460
|
+
assert.match(SCRIPT, /dd\.hidden = \$\(id \+ 'L'\)\.hidden = !value/,
|
|
461
|
+
'a fact with no answer hides label and value together — never a dash');
|
|
462
|
+
assert.match(SCRIPT, /cardFact\('cardBuilders', p\.builders == null \? '' : String\(p\.builders\)\)/,
|
|
463
|
+
'builders: 0 is a real answer, null hides');
|
|
464
|
+
assert.match(SCRIPT, /cardFact\('cardRuns', Array\.isArray\(p\.tags\) \? tagsHtml\(p\.tags\) : ''/,
|
|
465
|
+
'runs go through the one shared formatter');
|
|
466
|
+
assert.match(SCRIPT, /cardFact\('cardOwner', rel === 'owner' \? '' : ownerHtml\(p\), true\)/,
|
|
467
|
+
'owner comes from the enrichment and yields to the rel chip saying "yours"');
|
|
468
|
+
const oh = (SCRIPT.match(/function ownerHtml\(p\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
|
|
469
|
+
assert.ok(oh, 'ownerHtml() must exist');
|
|
470
|
+
assert.match(oh, /safeHttpUrl\(p\.owner_avatar\)/,
|
|
471
|
+
'a third-party avatar URL is scheme-checked before it becomes an img src');
|
|
472
|
+
assert.match(oh, /esc\(String\(p\.owner\)\)/, 'the login is escaped');
|
|
473
|
+
assert.match(SCRIPT, /cardFact\('cardListed', p\.created_at \? listedDate\(p\.created_at\) : ''\)/,
|
|
474
|
+
'listed is the catalogued date through the year-bearing formatter');
|
|
475
|
+
assert.match(SCRIPT, /year: 'numeric'/, 'a date of record carries its year');
|
|
438
476
|
});
|
|
439
477
|
|
|
440
478
|
test('the tag list is capped, and the overflow is counted rather than hidden', () => {
|
|
@@ -520,6 +558,15 @@ test('the table body is a sibling of the sky wrapper, not nested inside it', ()
|
|
|
520
558
|
'the table must not sit inside the sky wrapper. open: ' + ancestors.join(' > '));
|
|
521
559
|
});
|
|
522
560
|
|
|
561
|
+
test('the card metadata block lives inside the card, not a hideable sub-view', () => {
|
|
562
|
+
const ancestors = openAncestorsAt(HUB, 'id="cardMeta"');
|
|
563
|
+
assert.ok(ancestors.includes('projCard'), 'the facts belong to the card dialog');
|
|
564
|
+
for (const hidden of ['sub-sky-body', 'sub-table-body']) {
|
|
565
|
+
assert.ok(!ancestors.includes(hidden),
|
|
566
|
+
'#' + hidden + ' is toggled hidden — the facts cannot live inside it. open: ' + ancestors.join(' > '));
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
523
570
|
/* ── The self row's Open goes somewhere (task 1002777) ──────────────────────
|
|
524
571
|
Task 1002760's static self row shipped its action cell as href="#" with a
|
|
525
572
|
`data-self-open` hook nobody ever wired. The row-click handler deliberately
|