@bongos/core 1.19.478 → 1.19.480
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 +100 -35
- package/.claude/skills/design/SKILL.md +9 -3
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +4 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0230-a-look-skill-is-a-composition-grammar-the-palette-is-the-pack.md +36 -0
- package/docs/adr/0231-the-style-session-an-owner-authors-a-look-inside-the-contract.md +32 -0
- package/docs/adr/README.md +2 -0
- package/docs/api/openapi.json +80 -1
- package/docs/api-reference.md +5 -3
- package/docs/copy-inventory.md +6 -6
- package/docs/copy-registry.json +6 -6
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
- package/modules/economy/credits.js +78 -0
- package/modules/economy/module.json +1 -1
- package/modules/economy/reward.js +4 -0
- package/modules/economy/routes/credits.js +151 -0
- package/modules/hall-ui/public/md-reader.js +24 -1
- package/modules/ui-design/module.json +1 -1
- package/modules/ui-design/skills/README.md +14 -2
- package/modules/ui-design/skills/gpt-taste/SKILL.md +110 -0
- package/modules/ui-design/skills/industrial-brutalist-ui/SKILL.md +110 -0
- package/modules/ui-design/skills/minimalist-ui/SKILL.md +103 -0
- package/modules/ui-design/skills/stitch-design-taste/SKILL.md +91 -0
- package/modules/ui-design/skills/style/SKILL.md +112 -0
- package/modules/ui-design/styles/README.md +3 -0
- package/modules/ui-design/styles/blueprint/DESIGN.md +152 -0
- package/modules/ui-design/styles/blueprint/mock.html +308 -0
- package/modules/ui-design/styles/blueprint/mock.states.json +11 -0
- package/modules/ui-design/styles/blueprint/pack.json +33 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/credit_ledger_read.mjs +347 -0
- package/tests/hall_reading_world.mjs +80 -0
- package/tests/own_scope_guard.mjs +24 -2
- package/tests/ui_design_skills.mjs +102 -11
package/docs/api/openapi.json
CHANGED
|
@@ -2625,6 +2625,54 @@
|
|
|
2625
2625
|
]
|
|
2626
2626
|
}
|
|
2627
2627
|
},
|
|
2628
|
+
"/builders/{builderId}/credits": {
|
|
2629
|
+
"get": {
|
|
2630
|
+
"operationId": "get_builders_builderId_credits",
|
|
2631
|
+
"tags": [
|
|
2632
|
+
"builders"
|
|
2633
|
+
],
|
|
2634
|
+
"summary": "GET /builders/:builderId/credits",
|
|
2635
|
+
"description": "GET /builders/:builderId/credits — another builder's ledger. rank: metic — gated on the `builder.roster.read` ATOM, the same gate /builders/:id/active-claims carries, because this is the same act: reading another builder's record off the roster. ADR 0157 delegated that atom archon→metic. Note this exposes NO new secret — the leaderboard already publishes total_credits and /builders/:id/profile already publishes per-task awards, both unauthenticated. What this adds is the itemisation, which is precisely what makes the ledger auditable. The param is `:builderId`, not the `:id` its neighbours in src/bongos/routes/builders.js use, and deliberately: tests/own_scope_guard.mjs classifies a route as cross-owner by whether a path param NAMES a builder, and a bare `:id` reads as own-scoped. Since this file also mounts /me/credits it is held to that guard — so the explicit name is what lets the guard see this route for what it is and check that its gate is really there.\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `builder.roster.read` (all required).",
|
|
2636
|
+
"x-rank": "metic+archon",
|
|
2637
|
+
"x-source": "modules/economy/routes/credits.js",
|
|
2638
|
+
"x-permissions": [
|
|
2639
|
+
"builder.roster.read"
|
|
2640
|
+
],
|
|
2641
|
+
"parameters": [
|
|
2642
|
+
{
|
|
2643
|
+
"name": "builderId",
|
|
2644
|
+
"in": "path",
|
|
2645
|
+
"required": true,
|
|
2646
|
+
"schema": {
|
|
2647
|
+
"type": "string"
|
|
2648
|
+
},
|
|
2649
|
+
"description": "Path parameter `builderId`."
|
|
2650
|
+
}
|
|
2651
|
+
],
|
|
2652
|
+
"responses": {
|
|
2653
|
+
"200": {
|
|
2654
|
+
"description": "Success."
|
|
2655
|
+
},
|
|
2656
|
+
"400": {
|
|
2657
|
+
"$ref": "#/components/responses/BadRequest"
|
|
2658
|
+
},
|
|
2659
|
+
"401": {
|
|
2660
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
2661
|
+
},
|
|
2662
|
+
"403": {
|
|
2663
|
+
"$ref": "#/components/responses/Forbidden"
|
|
2664
|
+
},
|
|
2665
|
+
"404": {
|
|
2666
|
+
"$ref": "#/components/responses/NotFound"
|
|
2667
|
+
}
|
|
2668
|
+
},
|
|
2669
|
+
"security": [
|
|
2670
|
+
{
|
|
2671
|
+
"builderSession": []
|
|
2672
|
+
}
|
|
2673
|
+
]
|
|
2674
|
+
}
|
|
2675
|
+
},
|
|
2628
2676
|
"/builders/{id}/active-claims": {
|
|
2629
2677
|
"get": {
|
|
2630
2678
|
"operationId": "get_builders_id_active_claims",
|
|
@@ -8280,6 +8328,37 @@
|
|
|
8280
8328
|
]
|
|
8281
8329
|
}
|
|
8282
8330
|
},
|
|
8331
|
+
"/me/credits": {
|
|
8332
|
+
"get": {
|
|
8333
|
+
"operationId": "get_me_credits",
|
|
8334
|
+
"tags": [
|
|
8335
|
+
"me"
|
|
8336
|
+
],
|
|
8337
|
+
"summary": "GET /me/credits",
|
|
8338
|
+
"description": "GET /me/credits — the caller's own ledger. rank: any-builder — own-scoped by construction: the builder id comes from the authenticated session and is never read off the request, so there is no parameter here to tamper with. A builder may always read their own ledger; gating this any higher would mean a Xenos cannot see what they were paid.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
8339
|
+
"x-rank": "any-builder",
|
|
8340
|
+
"x-source": "modules/economy/routes/credits.js",
|
|
8341
|
+
"responses": {
|
|
8342
|
+
"200": {
|
|
8343
|
+
"description": "Success."
|
|
8344
|
+
},
|
|
8345
|
+
"400": {
|
|
8346
|
+
"$ref": "#/components/responses/BadRequest"
|
|
8347
|
+
},
|
|
8348
|
+
"401": {
|
|
8349
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
8350
|
+
},
|
|
8351
|
+
"403": {
|
|
8352
|
+
"$ref": "#/components/responses/Forbidden"
|
|
8353
|
+
}
|
|
8354
|
+
},
|
|
8355
|
+
"security": [
|
|
8356
|
+
{
|
|
8357
|
+
"builderSession": []
|
|
8358
|
+
}
|
|
8359
|
+
]
|
|
8360
|
+
}
|
|
8361
|
+
},
|
|
8283
8362
|
"/me/cross-project": {
|
|
8284
8363
|
"get": {
|
|
8285
8364
|
"operationId": "get_me_cross_project",
|
|
@@ -21426,7 +21505,7 @@
|
|
|
21426
21505
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
21427
21506
|
}
|
|
21428
21507
|
},
|
|
21429
|
-
"x-endpoint-count":
|
|
21508
|
+
"x-endpoint-count": 341,
|
|
21430
21509
|
"x-schema-count": 374,
|
|
21431
21510
|
"x-undocumented-bodies": 9,
|
|
21432
21511
|
"x-response-schemas": 255,
|
package/docs/api-reference.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Bongos API reference
|
|
4
4
|
|
|
5
|
-
> **Generated from the live route files** — the route file is authoritative.
|
|
5
|
+
> **Generated from the live route files** — the route file is authoritative. 341 endpoints across 55 route files.
|
|
6
6
|
> Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
|
|
7
7
|
|
|
8
8
|
Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>.md)): `public` < `any-builder` < `metic+archon` < `archon`.
|
|
@@ -119,10 +119,11 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
119
119
|
|---|---|---|---|---|
|
|
120
120
|
| GET | `/api/bongos/bug-attachments/:name` | `any-builder` | — | GET /api/bongos/bug-attachments/:name — stream one stored image. |
|
|
121
121
|
|
|
122
|
-
## `builders` (
|
|
122
|
+
## `builders` (11)
|
|
123
123
|
|
|
124
124
|
| Method | Path | Rank | Body | Description |
|
|
125
125
|
|---|---|---|---|---|
|
|
126
|
+
| GET | `/api/bongos/builders/:builderId/credits` | `metic+archon` | — | GET /builders/:builderId/credits — another builder's ledger. |
|
|
126
127
|
| GET | `/api/bongos/builders/:id/active-claims` | `metic+archon` | — | GET /builders/:id/active-claims — V3.R14 (task 238). |
|
|
127
128
|
| GET | `/api/bongos/builders/:id/activity` | `any-builder` | — | GET /builders/:id/activity — the profile's PER-CRAFT activity log (task 1003281, goal 1000072, criterion legibility-activity-by-role). |
|
|
128
129
|
| PATCH | `/api/bongos/builders/:id/budget` | `metic+archon` | `monthly_budget_usd` | PATCH /builders/:id/budget — set or clear a builder's monthly spend ceiling (V3.R84 task 302, criterion C3). |
|
|
@@ -345,13 +346,14 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
345
346
|
| POST | `/api/bongos/llm-cache/lookup` | `metic+archon` | `cache_key`, `tokens_saved_est` | POST /llm-cache/lookup — hit/miss + atomic hit_count bump. |
|
|
346
347
|
| POST | `/api/bongos/llm-cache/store` | `metic+archon` | `spec`, `result_stdout`, `result_cost_usd` | POST /llm-cache/store — upsert a freshly-produced result. |
|
|
347
348
|
|
|
348
|
-
## `me` (
|
|
349
|
+
## `me` (31)
|
|
349
350
|
|
|
350
351
|
| Method | Path | Rank | Body | Description |
|
|
351
352
|
|---|---|---|---|---|
|
|
352
353
|
| GET | `/api/bongos/me` | `any-builder` | — | PMS-V2 task 188: response now also carries `unlocked` (server-joined achievement metadata) + `locked_with_progress` (server-computed huma… |
|
|
353
354
|
| GET | `/api/bongos/me/achievements` | `any-builder` | — | The builder's unlocked achievements (joined to achievement metadata). |
|
|
354
355
|
| POST | `/api/bongos/me/anthropic-link` | `any-builder` | `anthropic_email`, `anthropic_user_id` | V3.R10 (task 235), criterion C3 — link the authed builder's Anthropic account. |
|
|
356
|
+
| GET | `/api/bongos/me/credits` | `any-builder` | — | GET /me/credits — the caller's own ledger. |
|
|
355
357
|
| GET | `/api/bongos/me/cross-project` | `any-builder` | — | the caller's OWN cross-project record + privacy flags (own data). |
|
|
356
358
|
| PATCH | `/api/bongos/me/cross-project` | `any-builder` | `hide_stats`, `account_visibility`, `recruiter_discoverable` | flip the caller's OWN privacy flags (C2's self-serve opt-out plus the task-1002966 pair; own data, keyed on the authenticated github_id —… |
|
|
357
359
|
| PATCH | `/api/bongos/me/disciplines` | `any-builder` | `preferred_disciplines` | Self-service edit of the authenticated builder's preferred disciplines (migrations 027 + 086 / V3.R80). |
|
package/docs/copy-inventory.md
CHANGED
|
@@ -84,11 +84,11 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
84
84
|
|---|---|---|---|
|
|
85
85
|
| `a8813c34d20d` | certain | “Host denied” | `modules/hall-ui/public/box-panel.js:160` |
|
|
86
86
|
| `a8813c34d20d` | certain | “Host denied” | `modules/hall-ui/public/box-panel.js:340` |
|
|
87
|
-
| `b585019e3e1d` | certain | ' lines collapse into one | `modules/hall-ui/public/md-reader.js:
|
|
88
|
-
| `44e29d37ddf6` | certain | '); out = out.replace(/(^\|[^*])\*([^*]+)\*(?!\*)/g, '$1 | `modules/hall-ui/public/md-reader.js:
|
|
89
|
-
| `66cea7cefa4e` | certain | '; for (const row of bodyRows) { html += ' | `modules/hall-ui/public/md-reader.js:
|
|
90
|
-
| `53e4ac514458` | certain | '; out.push(html); continue; } // Lists (unordered '- ' or ordered '1. '); consecutive items collapse. if (/^( | `modules/hall-ui/public/md-reader.js:
|
|
91
|
-
| `87fa099f5068` | certain | '; } html += ' | `modules/hall-ui/public/md-reader.js:
|
|
87
|
+
| `b585019e3e1d` | certain | ' lines collapse into one | `modules/hall-ui/public/md-reader.js:186` |
|
|
88
|
+
| `44e29d37ddf6` | certain | '); out = out.replace(/(^\|[^*])\*([^*]+)\*(?!\*)/g, '$1 | `modules/hall-ui/public/md-reader.js:140` |
|
|
89
|
+
| `66cea7cefa4e` | certain | '; for (const row of bodyRows) { html += ' | `modules/hall-ui/public/md-reader.js:217` |
|
|
90
|
+
| `53e4ac514458` | certain | '; out.push(html); continue; } // Lists (unordered '- ' or ordered '1. '); consecutive items collapse. if (/^( | `modules/hall-ui/public/md-reader.js:221` |
|
|
91
|
+
| `87fa099f5068` | certain | '; } html += ' | `modules/hall-ui/public/md-reader.js:221` |
|
|
92
92
|
| `cb327c50b079` | certain | 's dev box (state: {…}). Any uncommitted work on that box is lost. The box is destroyed; the builder can open | `modules/hall-ui/public/harbor.js:125` |
|
|
93
93
|
| `544794f1ec8f` | certain | (later in | `modules/hall-ui/public/index.html:9` |
|
|
94
94
|
| `3ac2eb36ba97` | certain | (no sections) | `modules/hall-ui/public/drachmae.js:128` |
|
|
@@ -1001,7 +1001,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
|
|
|
1001
1001
|
| `3c148c7b9539` | certain | Your work runs on a dev box we host for you — there's almost nothing to install. Choose how to connect: | `modules/hall-ui/public/hall-render.js:653` |
|
|
1002
1002
|
| `137b07b42203` | certain | Zoom in | `modules/hall-ui/public/diagram-viewer.js:98` |
|
|
1003
1003
|
| `0d0bfa651b36` | certain | Zoom out | `modules/hall-ui/public/diagram-viewer.js:96` |
|
|
1004
|
-
| `cc361cc7333a` | certain | \s?/.test(raw)) { const quoteLines = []; while (i | `modules/hall-ui/public/md-reader.js:
|
|
1004
|
+
| `cc361cc7333a` | certain | \s?/.test(raw)) { const quoteLines = []; while (i | `modules/hall-ui/public/md-reader.js:186` |
|
|
1005
1005
|
| `44e6e402db94` | certain | account, then open | `modules/hall-ui/public/hall-render.js:653` |
|
|
1006
1006
|
| `ae43396446ed` | certain | account. | `modules/hall-ui/public/hall-render.js:653` |
|
|
1007
1007
|
| `9612c983451f` | certain | achieved · | `modules/hall-ui/public/roadmap.js:263` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -1631,7 +1631,7 @@
|
|
|
1631
1631
|
"surface": "builders-hall",
|
|
1632
1632
|
"text": "' lines collapse into one",
|
|
1633
1633
|
"file": "modules/hall-ui/public/md-reader.js",
|
|
1634
|
-
"line":
|
|
1634
|
+
"line": 186,
|
|
1635
1635
|
"origin": "js-markup",
|
|
1636
1636
|
"confidence": "certain"
|
|
1637
1637
|
},
|
|
@@ -1640,7 +1640,7 @@
|
|
|
1640
1640
|
"surface": "builders-hall",
|
|
1641
1641
|
"text": "'); out = out.replace(/(^|[^*])\\*([^*]+)\\*(?!\\*)/g, '$1",
|
|
1642
1642
|
"file": "modules/hall-ui/public/md-reader.js",
|
|
1643
|
-
"line":
|
|
1643
|
+
"line": 140,
|
|
1644
1644
|
"origin": "js-markup",
|
|
1645
1645
|
"confidence": "certain"
|
|
1646
1646
|
},
|
|
@@ -1649,7 +1649,7 @@
|
|
|
1649
1649
|
"surface": "builders-hall",
|
|
1650
1650
|
"text": "'; for (const row of bodyRows) { html += '",
|
|
1651
1651
|
"file": "modules/hall-ui/public/md-reader.js",
|
|
1652
|
-
"line":
|
|
1652
|
+
"line": 217,
|
|
1653
1653
|
"origin": "js-markup",
|
|
1654
1654
|
"confidence": "certain"
|
|
1655
1655
|
},
|
|
@@ -1658,7 +1658,7 @@
|
|
|
1658
1658
|
"surface": "builders-hall",
|
|
1659
1659
|
"text": "'; out.push(html); continue; } // Lists (unordered '- ' or ordered '1. '); consecutive items collapse. if (/^(-|\\d+\\.)\\s+/.test(raw)) { const ordered = /^\\d+\\.\\s+/.test(raw); const items = []; while (i",
|
|
1660
1660
|
"file": "modules/hall-ui/public/md-reader.js",
|
|
1661
|
-
"line":
|
|
1661
|
+
"line": 221,
|
|
1662
1662
|
"origin": "js-markup",
|
|
1663
1663
|
"confidence": "certain"
|
|
1664
1664
|
},
|
|
@@ -1667,7 +1667,7 @@
|
|
|
1667
1667
|
"surface": "builders-hall",
|
|
1668
1668
|
"text": "'; } html += '",
|
|
1669
1669
|
"file": "modules/hall-ui/public/md-reader.js",
|
|
1670
|
-
"line":
|
|
1670
|
+
"line": 221,
|
|
1671
1671
|
"origin": "js-markup",
|
|
1672
1672
|
"confidence": "certain"
|
|
1673
1673
|
},
|
|
@@ -9884,7 +9884,7 @@
|
|
|
9884
9884
|
"surface": "builders-hall",
|
|
9885
9885
|
"text": "\\s?/.test(raw)) { const quoteLines = []; while (i",
|
|
9886
9886
|
"file": "modules/hall-ui/public/md-reader.js",
|
|
9887
|
-
"line":
|
|
9887
|
+
"line": 186,
|
|
9888
9888
|
"origin": "js-markup",
|
|
9889
9889
|
"confidence": "certain"
|
|
9890
9890
|
},
|
package/docs/file-map.md
CHANGED
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
│ ├── hall-ui/ ← CORE-DOMAIN WEB-SURFACE carve (BV1.R85, default-on): the authenticated builders' hall at builders.<apex>, moved from public-builders/ (~13k LOC, the largest). NO routes/db/port — declares contributes.webSurfaces:[{host:"builders.",dir:"public"}] for discovery, but UNLIKE status-ui keeps its DEDICATED serving block in serve-internal (the /builders shim + canonical redirects + citizen/Archon page gates + asset-stamping + hall-widget injection), partitioned out of the generic web-surface loop and served from BUILDERS_DIR. CLAUDE.md, module.json, public/{index.html,builders.js,style.css,work,watch,harbor,gate,pair,settings,ranks,sessions,primer,diagrams,atlas,...}.{html,js},cursors/,chime.wav. Three ARCHETYPE sheets sit beside the per-page ones: `record.css` (the task + idea records, task 1003305), `room.css` (the READING ROOMS — the primer + the diagrams index, task 1003306: the reading measure and rhythm, the table of contents, the progress hairline and the framed figure diagram-viewer.js upgrades on both pages) and `oversight.css` (the OVERSIGHT family — government, watch, harbor, gate, task 1003469: the row, the figure band inside a panel, the group head, the inline form, the grant toggle and the shared confirm modal; named oversight because the vocabulary wall bans "governance" on the government surface). A page sheet then holds only what one page has and the others do not — `primer.css` the acknowledge gate, `diagrams.css` the legend strip, `gate.css` the hold reasons and file chips, `watch.css` the severities, the report reader and the grade trend, `people.css` the directory's identity cell (task 1003470 made the legacy door `/scouting` ship the same body and script as `/people`; task 1003440 then deleted the door — `scouting.html` and `scouting.states.json` are gone and the URL 301s to `/people`).
|
|
134
134
|
│ ├── lifecycle/ ← CORE-DOMAIN carve (BV1.R86, default-on): the build state machine — tasks · claims · versions · done-when · goals · dependencies · claim→ship→grade→credit · github-push + merge-lock + conflict-resolve + publish-reconciler. The LAST, most-coupled carve; registers the `lifecycle` kernel port (createTask, classifyTaskKind, tallyPeerVotes, work-tracking reads). CLAUDE.md, module.json, routes/{tasks,claims,versions,done-when,goals,dependencies,gate-approvals,analytics}.js, lifecycle.js, github-push.js, merge-lock.js, conflict-resolve.js, publish-reconciler.js, task-visuals.js + task-visual-db.js + routes/visuals.js (the optional ship-time visual, task 1003109 — the route file is NOT named task-*.js on purpose; see the module CLAUDE.md).
|
|
135
135
|
│ ├── copy-desk/ ← CORE-DOMAIN module (task 1003113 / R02 of goal 1000074, default-on): the COPY half of the design contract — ADR 0081 gave design TOKENS a repo-owned source of truth, text had none. Owns the FLAG (anyone in any role marks a string or a whole surface as needing an artist's attention, with a reason required in BOTH the route and a CHECK constraint) and the QUEUE an artist reads it from. READS R01's committed registry (docs/copy-registry.json, generated by scripts/gds/copy-inventory.js) and never writes it. NO LIVE CMS is the hard non-goal: no column here could hold replacement text, and copy_no_cms.mjs pins that. Key is `copy-desk`, not `copy` — the branding contract already owns a `copy` block, so the bare word reds the "core carries no module code" fitness check. CLAUDE.md, module.json, registry.js, flags.js, routes/copy-desk.js, migrations/copy_desk_001_flags.sql, tests/{copy_flags,copy_queue,copy_no_cms}.mjs. Its hall surface lives with hall-ui (public/copy-desk.{html,js,css}), per the one-key-per-web-surface precedent.
|
|
136
|
-
│ ├── ui-design/ ← CORE-DOMAIN module (task 1003324 / ADR 0197, default-on): the design capability every builder on every instance gets — ships METHOD, never a world. Absorbs ADR 0081's layer: adapters/{claude-design,figma}/index.js (moved from src/ui/adapters/), scripts/{design-sync,figma-design-sync,validate-design}.js (moved from scripts/gds/, which keeps thin shims), docs/design-contract.md (moved from docs/). Declaration-only: contributes.skills design (the /design playbook, new) + design-sync + figma-design-sync; no routes/port/migrations, so disabling it touches no served surface. CLAUDE.md separates the platform FLOORS (the Fifteen Rule, the dark twins, no page :root, the 24px floor, AA in both modes, the kill switch) from the instance's WORLD (its packs + DESIGN.md). kit/ (task 1003320) is the look-before-you-ship kit: lib.js (the floors, the states/probes contracts, the action + expectation vocabularies, the WCAG maths, the stub spawner), serve.js (any surface through the doorway's real transforms + fixtures/), render.js (states × 1440/390/320 × dark/light, audited), probe.js (PASS/FAIL contracts), check-mock.js (platform rules + the instance's DESIGN.md taste bans, then the detector), tells.js (task 1003329 / ADR 0223: the anti-pattern detector as two tiers — floors FAIL, craft tells WARN, kit-ignore waivers per file; the rendered twins live in lib.js factsInPage), contrast.js + png.js; a page's <page>.states.json / <page>.probes.json sit BESIDE the surface (modules/public-landing/public/, modules/hall-ui/public/). Recipe: docs/recipes/ui-look-before-you-ship.md. skills/ (task 1003321 / ADR 0198) is the vendoring policy: policy.json (licence allowlist, verdicts that may land, sidecar fields, preamble markers), PREAMBLE.md (the platform preamble every vendored SKILL.md opens with), README.md (the roster — both origins scanned dangerous on 2026-08-28, landed as rebuild specs; nothing vendored — and the landing recipe); a landable copy goes under skills/vendor/<name>/ with PROVENANCE.md + LICENSE and is materialised into an instance's .claude/skills/ by claude-materialize.js step 1b. skills/<name>/ (task 1003325 / ADR 0220) are the in-house rebuilds from the specs, first-party and counted: design-taste-frontend (the default taste skill), design-taste-frontend-v1 (the earlier generation, kept distinct), high-end-visual-design (the polish pass), redesign-existing-projects (the in-place upgrade); each opens with the same preamble and carries a "Rebuilt from" section instead of a sidecar. impeccable/ (task 1003326 / ADR 0221) is the design-craft method as ONE skill with sub-commands: SKILL.md (the router, the menu, the craft floor, the bounded finish) + reference/<sub-command>.md (eighteen playbooks, one loaded per request); no hooks, no bypass agent, no telemetry, no live server. image-to-code/, imagegen-frontend-web/, imagegen-frontend-mobile/, brandkit/ (task 1003328 / ADR 0222) are the image family under the hero-only rule: plates and hero objects only, the pair (JPEG dark plate + WebP alpha cutout) with a <file>.json sidecar beside each, through the instance's art pipeline key; the grove specimen's styles/grove/assets/ is the worked example. styles/ (task 1003323 / ADR 0219) is the style library: <name>/pack.json (the theme.ui overlay, exactly the fifteen) + DESIGN.md (the look's derived recipes, thirteen in lockstep with the pack) + mock.html (the specimen); chrome-world (pinned to the neutral pack), expedition, grove; STYLE=<name> on the kit lays a look over the instance's pack through GDS_BRANDING_FILE; tests/ui_design_styles.mjs runs the world/cosmos/hall token suites once per alternate (UI_DESIGN_PACK). config/design-tokens.* + design-sources.json STAY in config/ as the host boundary; public-landing keeps its own key (first customer, not owner).
|
|
136
|
+
│ ├── ui-design/ ← CORE-DOMAIN module (task 1003324 / ADR 0197, default-on): the design capability every builder on every instance gets — ships METHOD, never a world. Absorbs ADR 0081's layer: adapters/{claude-design,figma}/index.js (moved from src/ui/adapters/), scripts/{design-sync,figma-design-sync,validate-design}.js (moved from scripts/gds/, which keeps thin shims), docs/design-contract.md (moved from docs/). Declaration-only: contributes.skills design (the /design playbook, new) + design-sync + figma-design-sync; no routes/port/migrations, so disabling it touches no served surface. CLAUDE.md separates the platform FLOORS (the Fifteen Rule, the dark twins, no page :root, the 24px floor, AA in both modes, the kill switch) from the instance's WORLD (its packs + DESIGN.md). kit/ (task 1003320) is the look-before-you-ship kit: lib.js (the floors, the states/probes contracts, the action + expectation vocabularies, the WCAG maths, the stub spawner), serve.js (any surface through the doorway's real transforms + fixtures/), render.js (states × 1440/390/320 × dark/light, audited), probe.js (PASS/FAIL contracts), check-mock.js (platform rules + the instance's DESIGN.md taste bans, then the detector), tells.js (task 1003329 / ADR 0223: the anti-pattern detector as two tiers — floors FAIL, craft tells WARN, kit-ignore waivers per file; the rendered twins live in lib.js factsInPage), contrast.js + png.js; a page's <page>.states.json / <page>.probes.json sit BESIDE the surface (modules/public-landing/public/, modules/hall-ui/public/). Recipe: docs/recipes/ui-look-before-you-ship.md. skills/ (task 1003321 / ADR 0198) is the vendoring policy: policy.json (licence allowlist, verdicts that may land, sidecar fields, preamble markers), PREAMBLE.md (the platform preamble every vendored SKILL.md opens with), README.md (the roster — both origins scanned dangerous on 2026-08-28, landed as rebuild specs; nothing vendored — and the landing recipe); a landable copy goes under skills/vendor/<name>/ with PROVENANCE.md + LICENSE and is materialised into an instance's .claude/skills/ by claude-materialize.js step 1b. skills/<name>/ (task 1003325 / ADR 0220) are the in-house rebuilds from the specs, first-party and counted: design-taste-frontend (the default taste skill), design-taste-frontend-v1 (the earlier generation, kept distinct), high-end-visual-design (the polish pass), redesign-existing-projects (the in-place upgrade); each opens with the same preamble and carries a "Rebuilt from" section instead of a sidecar. impeccable/ (task 1003326 / ADR 0221) is the design-craft method as ONE skill with sub-commands: SKILL.md (the router, the menu, the craft floor, the bounded finish) + reference/<sub-command>.md (eighteen playbooks, one loaded per request); no hooks, no bypass agent, no telemetry, no live server. minimalist-ui/, industrial-brutalist-ui/, gpt-taste/, stitch-design-taste/ (task 1003327 / ADR 0230) are the four looks, composition grammars with no colours of their own: the palette is the instance's pack and the texture is a named entry in styles/; a substrate is an archetype that still ships both modes, gpt-taste's selection is derived from the route rather than rolled, and stitch-design-taste writes a DESIGN.md-shaped contract document whose thirteen are transcribed from the pack. image-to-code/, imagegen-frontend-web/, imagegen-frontend-mobile/, brandkit/ (task 1003328 / ADR 0222) are the image family under the hero-only rule: plates and hero objects only, the pair (JPEG dark plate + WebP alpha cutout) with a <file>.json sidecar beside each, through the instance's art pipeline key; the grove specimen's styles/grove/assets/ is the worked example. style/ (task 1003475 / ADR 0231) is the module's first FIRST-PARTY skill (no upstream, so a `## First party` section instead of `## Rebuilt from`): the /style session in which an owner authors their own look in four bounded steps, rendering the nearest library entries before asking, drafting inside the contract, retuning by measurement, and handing over the one-step adoption without ever writing config/branding.json. styles/ (task 1003323 / ADR 0219) is the style library: <name>/pack.json (the theme.ui overlay, exactly the fifteen) + DESIGN.md (the look's derived recipes, thirteen in lockstep with the pack) + mock.html (the specimen); chrome-world (pinned to the neutral pack), expedition, grove, blueprint (the first cool look, authored through /style at task 1003475); STYLE=<name> on the kit lays a look over the instance's pack through GDS_BRANDING_FILE; tests/ui_design_styles.mjs runs the world/cosmos/hall token suites once per alternate (UI_DESIGN_PACK). config/design-tokens.* + design-sources.json STAY in config/ as the host boundary; public-landing keeps its own key (first customer, not owner).
|
|
137
137
|
│ ├── character-anim/ ← consistent-character generative animation (ADR 0088) — graded painterly frame sets + manifest, the otb-character-review skill. CLAUDE.md, module.json.
|
|
138
138
|
│ ├── provisioning/ ← stands up + operates a Cloud Bongos instance's own infra (server/DB/domain/DNS/TLS) via the API (ADR 0111); web-tier request+state only, no cloud tokens — a separate control-plane runner drains the intents. Distinct from the `hosting` module (ADR 0092), which brokers compute workloads. CLAUDE.md, module.json.
|
|
139
139
|
│ └── demo/ ← BV1.R52 acceptance-proof module: one rank-gated write route + one namespaced migration proving the loader mounts a scaffolded module with zero core edits. CLAUDE.md, module.json.
|
|
@@ -1393,5 +1393,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1393
1393
|
landed since 1.19.476 with no explicit bump. run 33662059275. (task 1002620)
|
|
1394
1394
|
1.19.478 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1395
1395
|
landed since 1.19.477 with no explicit bump. run 33664027105. (task 1002620)
|
|
1396
|
+
1.19.479 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1397
|
+
landed since 1.19.478 with no explicit bump. run 33665076054. (task 1002620)
|
|
1398
|
+
1.19.480 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1399
|
+
landed since 1.19.479 with no explicit bump. run 33668172411. (task 1002620)
|
|
1396
1400
|
---------------------------------------------------------------------------
|
|
1397
1401
|
```
|
|
@@ -220,6 +220,8 @@ function createClient(opts = {}) {
|
|
|
220
220
|
getBugAttachmentsName: (args) => request("GET", "/bug-attachments/{name}", { hasBody: false }, args),
|
|
221
221
|
},
|
|
222
222
|
"builders": {
|
|
223
|
+
// GET /builders/{builderId}/credits — rank: metic+archon — GET /builders/:builderId/credits
|
|
224
|
+
getBuildersBuilderIdCredits: (args) => request("GET", "/builders/{builderId}/credits", { hasBody: false }, args),
|
|
223
225
|
// GET /builders/{id}/active-claims — rank: metic+archon — GET /builders/:id/active-claims
|
|
224
226
|
getBuildersIdActiveClaims: (args) => request("GET", "/builders/{id}/active-claims", { hasBody: false }, args),
|
|
225
227
|
// GET /builders/{id}/activity — rank: any-builder — GET /builders/:id/activity
|
|
@@ -502,6 +504,8 @@ function createClient(opts = {}) {
|
|
|
502
504
|
getMeAchievements: (args) => request("GET", "/me/achievements", { hasBody: false }, args),
|
|
503
505
|
// POST /me/anthropic-link — rank: any-builder — POST /me/anthropic-link
|
|
504
506
|
postMeAnthropicLink: (args) => request("POST", "/me/anthropic-link", { hasBody: true }, args),
|
|
507
|
+
// GET /me/credits — rank: any-builder — GET /me/credits
|
|
508
|
+
getMeCredits: (args) => request("GET", "/me/credits", { hasBody: false }, args),
|
|
505
509
|
// GET /me/cross-project — rank: any-builder — GET /me/cross-project
|
|
506
510
|
getMeCrossProject: (args) => request("GET", "/me/cross-project", { hasBody: false }, args),
|
|
507
511
|
// PATCH /me/cross-project — rank: any-builder — PATCH /me/cross-project
|
|
@@ -921,6 +921,81 @@ async function sumCreditsForTasks(builderId, taskIds) {
|
|
|
921
921
|
return rows[0] ? rows[0].total : 0;
|
|
922
922
|
}
|
|
923
923
|
|
|
924
|
+
// The three ideator streams (ADR 0172) each book under a reason key that EMBEDS
|
|
925
|
+
// the idea (and, for stream 1, the task) — that string IS the idempotency
|
|
926
|
+
// predicate, which is why it can't be a bare constant:
|
|
927
|
+
//
|
|
928
|
+
// stream 1 (trailing) idea.credit.task_ship:<ideaId>:<taskId>
|
|
929
|
+
// stream 2 (grade) idea.credit.full_pass:<ideaId>
|
|
930
|
+
// stream 3 (board room) idea.credit.governance_pass:<ideaId>
|
|
931
|
+
//
|
|
932
|
+
// So "show me everything stream 2 ever paid me" is a PREFIX question, never an
|
|
933
|
+
// equality one — an exact-match filter over `reason` can only ever return the
|
|
934
|
+
// single row for one idea. These are the prefixes `reasonPrefix` is meant to be
|
|
935
|
+
// given; exported so a reader never hand-builds the string and drifts from the
|
|
936
|
+
// writers above.
|
|
937
|
+
const IDEA_STREAM_REASON_PREFIXES = {
|
|
938
|
+
task_ship: 'idea.credit.task_ship:',
|
|
939
|
+
full_pass: 'idea.credit.full_pass:',
|
|
940
|
+
governance_pass: 'idea.credit.governance_pass:',
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
// creditLedger — the per-row read over credit_log for ONE builder. The ledger is
|
|
944
|
+
// append-only and builders.total_credits is only its cache, so this is the
|
|
945
|
+
// authoritative answer to "what was I actually paid, and for what".
|
|
946
|
+
//
|
|
947
|
+
// It exists because until now credit_log had NO read surface anywhere in the API
|
|
948
|
+
// (blocker 1000127): every other reader in the tree is an internal aggregate or
|
|
949
|
+
// an existence probe, so the ideator lane could not be audited without direct
|
|
950
|
+
// database access, and the hall had no surface that could show an author what an
|
|
951
|
+
// idea earned them.
|
|
952
|
+
//
|
|
953
|
+
// Filters are AND-ed. `reason` is exact — right for the unparameterized keys
|
|
954
|
+
// (task.shipped, session.token_reward, idea_promotion_bonus); `reasonPrefix` is a
|
|
955
|
+
// literal prefix match for the parameterized ones above. LIKE wildcards in the
|
|
956
|
+
// caller's input are escaped, so a `%` in a query string matches a literal `%`
|
|
957
|
+
// and cannot widen the scan. Newest-first; `total` is the UNPAGINATED count, so a
|
|
958
|
+
// caller can tell whether more rows exist.
|
|
959
|
+
async function creditLedger(builderId, { reason = null, reasonPrefix = null, taskId = null, since = null, limit = 50, offset = 0 } = {}) {
|
|
960
|
+
const id = Number(builderId);
|
|
961
|
+
if (!Number.isInteger(id) || id <= 0) return { rows: [], total: 0 };
|
|
962
|
+
|
|
963
|
+
const where = ['builder_id = $1'];
|
|
964
|
+
const params = [id];
|
|
965
|
+
if (reason) {
|
|
966
|
+
params.push(reason);
|
|
967
|
+
where.push(`reason = $${params.length}`);
|
|
968
|
+
}
|
|
969
|
+
if (reasonPrefix) {
|
|
970
|
+
params.push(reasonPrefix.replace(/([\\%_])/g, '\\$1') + '%');
|
|
971
|
+
where.push(`reason LIKE $${params.length} ESCAPE '\\'`);
|
|
972
|
+
}
|
|
973
|
+
if (taskId != null) {
|
|
974
|
+
params.push(Number(taskId));
|
|
975
|
+
where.push(`task_id = $${params.length}`);
|
|
976
|
+
}
|
|
977
|
+
if (since) {
|
|
978
|
+
params.push(since);
|
|
979
|
+
where.push(`recorded_at >= $${params.length}::timestamptz`);
|
|
980
|
+
}
|
|
981
|
+
const whereSql = where.join(' AND ');
|
|
982
|
+
|
|
983
|
+
// Count and page in one round trip. reward_basis is deliberately NOT selected:
|
|
984
|
+
// it is session top-up bookkeeping (core_196), not a ledger fact a reader needs,
|
|
985
|
+
// and exposing it would invite its being read as money.
|
|
986
|
+
const { rows } = await pool.query(
|
|
987
|
+
`SELECT id, delta, reason, description, task_id, session_id, recorded_at,
|
|
988
|
+
COUNT(*) OVER ()::int AS total_count
|
|
989
|
+
FROM credit_log
|
|
990
|
+
WHERE ${whereSql}
|
|
991
|
+
ORDER BY recorded_at DESC, id DESC
|
|
992
|
+
LIMIT $${params.length + 1} OFFSET $${params.length + 2}`,
|
|
993
|
+
[...params, limit, offset]
|
|
994
|
+
);
|
|
995
|
+
const total = rows.length > 0 ? Number(rows[0].total_count) : 0;
|
|
996
|
+
return { rows: rows.map(({ total_count, ...r }) => r), total };
|
|
997
|
+
}
|
|
998
|
+
|
|
924
999
|
module.exports = {
|
|
925
1000
|
leaderboard,
|
|
926
1001
|
KIND_MULTIPLIERS,
|
|
@@ -953,4 +1028,7 @@ module.exports = {
|
|
|
953
1028
|
netNegativeBonusAmount,
|
|
954
1029
|
awardNetNegativeBonus,
|
|
955
1030
|
sumCreditsForTasks,
|
|
1031
|
+
// the ledger read surface (task 1003484, blocker 1000127)
|
|
1032
|
+
creditLedger,
|
|
1033
|
+
IDEA_STREAM_REASON_PREFIXES,
|
|
956
1034
|
};
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"coreVersion": "^1.7.0",
|
|
6
6
|
"default": true,
|
|
7
7
|
"maintenance": { "status": "core-maintained" },
|
|
8
|
-
"contributes": { "routes": ["cost", "leaderboard", "reward"] },
|
|
8
|
+
"contributes": { "routes": ["cost", "credits", "leaderboard", "reward"] },
|
|
9
9
|
"provides": ["reward"],
|
|
10
10
|
"consumes": []
|
|
11
11
|
}
|
|
@@ -73,6 +73,10 @@ module.exports = {
|
|
|
73
73
|
getBuilderAchievements: (...a) => achievements.getBuilderAchievements(...a),
|
|
74
74
|
getLockedAchievementsProgress: (...a) => achievements.getLockedAchievementsProgress(...a),
|
|
75
75
|
sumCreditsForTasks: (...a) => credits.sumCreditsForTasks(...a),
|
|
76
|
+
// The per-row ledger read (task 1003484). On the port because the surface core
|
|
77
|
+
// most wants it for is a HALL view — "what did this idea earn me" — and a core
|
|
78
|
+
// route must reach economy through here, never by importing the module.
|
|
79
|
+
creditLedger: (...a) => credits.creditLedger(...a),
|
|
76
80
|
computeStreak: (...a) => streak.computeStreak(...a),
|
|
77
81
|
wouldExtendStreak: (...a) => streak.wouldExtendStreak(...a),
|
|
78
82
|
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// modules/economy/routes/credits.js — GET /me/credits + GET /builders/:builderId/credits:
|
|
2
|
+
// the credit ledger, readable at last (task 1003484, blocker 1000127).
|
|
3
|
+
//
|
|
4
|
+
// WHY. `credit_log` is the append-only record of every drachma this instance has
|
|
5
|
+
// ever paid, and `builders.total_credits` is only its cache — yet until this file
|
|
6
|
+
// there was NO read surface for it anywhere in the API. Every other reader in the
|
|
7
|
+
// tree is an internal aggregate or an existence probe. The consequences were not
|
|
8
|
+
// abstract: nobody could answer "was this ideator actually paid, and under which
|
|
9
|
+
// stream" without direct database access, the three ideator streams (ADR 0172)
|
|
10
|
+
// could not be audited from outside at all, and the builders' hall had no surface
|
|
11
|
+
// that could ever show an author what one of their ideas earned them.
|
|
12
|
+
//
|
|
13
|
+
// A balance you cannot itemise is a number you have to take on faith. This route
|
|
14
|
+
// is what turns it back into a ledger.
|
|
15
|
+
//
|
|
16
|
+
// The route is only the doorway; `../credits.js` owns `creditLedger` and every
|
|
17
|
+
// other credit_log query. Keep it that way — this file must never grow SQL.
|
|
18
|
+
//
|
|
19
|
+
// MODULE BOUNDARY: reaches core only through the published doorway
|
|
20
|
+
// `../../../src/module-api`; `../credits` is an intra-module sibling.
|
|
21
|
+
|
|
22
|
+
const express = require('express');
|
|
23
|
+
const api = require('../../../src/module-api');
|
|
24
|
+
const credits = require('../credits');
|
|
25
|
+
|
|
26
|
+
// Cap the page. The ledger is unbounded and grows forever, so an uncapped read is
|
|
27
|
+
// a slow query waiting to happen on the oldest, busiest builder.
|
|
28
|
+
const DEFAULT_LIMIT = 50;
|
|
29
|
+
const MAX_LIMIT = 200;
|
|
30
|
+
const MAX_REASON_LEN = 200;
|
|
31
|
+
|
|
32
|
+
// Pull the shared filter/paging shape out of the query string. Returns
|
|
33
|
+
// { opts } on success or { error } with a res.fail code — both routes read the
|
|
34
|
+
// SAME filters, and a filter that behaves differently depending on whose ledger
|
|
35
|
+
// you are reading is exactly the drift this shared parser exists to prevent.
|
|
36
|
+
function parseLedgerQuery(req) {
|
|
37
|
+
const q = req.query || {};
|
|
38
|
+
const { limit, offset } = api.parsePagination(req, { defaultLimit: DEFAULT_LIMIT, maxLimit: MAX_LIMIT });
|
|
39
|
+
|
|
40
|
+
for (const key of ['reason', 'reason_prefix']) {
|
|
41
|
+
const v = q[key];
|
|
42
|
+
if (v !== undefined && (typeof v !== 'string' || v.length > MAX_REASON_LEN)) {
|
|
43
|
+
return { error: 'bad_reason_filter' };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let taskId = null;
|
|
48
|
+
if (q.task_id !== undefined) {
|
|
49
|
+
const n = Number(q.task_id);
|
|
50
|
+
if (!Number.isInteger(n) || n <= 0) return { error: 'bad_task_id' };
|
|
51
|
+
taskId = n;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let since = null;
|
|
55
|
+
if (q.since !== undefined) {
|
|
56
|
+
// Validate here rather than letting Postgres raise: an unparseable timestamp
|
|
57
|
+
// would otherwise surface as a 500 on what is a plain bad request.
|
|
58
|
+
if (typeof q.since !== 'string' || Number.isNaN(Date.parse(q.since))) {
|
|
59
|
+
return { error: 'bad_since' };
|
|
60
|
+
}
|
|
61
|
+
since = q.since;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
opts: {
|
|
66
|
+
reason: q.reason || null,
|
|
67
|
+
reasonPrefix: q.reason_prefix || null,
|
|
68
|
+
taskId,
|
|
69
|
+
since,
|
|
70
|
+
limit,
|
|
71
|
+
offset,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Shared responder — one shape for both routes, so a client (or the hall) can
|
|
77
|
+
// read either without branching. `streams` echoes the ideator-stream reason
|
|
78
|
+
// PREFIXES: the stream keys embed the idea id, so a caller that hand-built
|
|
79
|
+
// 'idea.credit.full_pass:' would silently drift from the writer. Serving them
|
|
80
|
+
// beside the rows means the audit filter is discoverable from the response.
|
|
81
|
+
async function respondWithLedger(res, builderId, opts) {
|
|
82
|
+
const { rows, total } = await credits.creditLedger(builderId, opts);
|
|
83
|
+
// A builder's own balance, and cheap to recompute — never cached.
|
|
84
|
+
res.set('Cache-Control', 'no-store');
|
|
85
|
+
res.json({
|
|
86
|
+
builder_id: builderId,
|
|
87
|
+
rows,
|
|
88
|
+
page: api.pageMeta(opts.limit, opts.offset, total),
|
|
89
|
+
streams: credits.IDEA_STREAM_REASON_PREFIXES,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = function buildCreditsRouter() {
|
|
94
|
+
const router = express.Router();
|
|
95
|
+
|
|
96
|
+
// Route ORDER is deliberate, and the same rule modules/dev-box/routes/box.js
|
|
97
|
+
// documents: the route-rank auditor classifies from a lookahead window that
|
|
98
|
+
// checks requireRank/requirePermission before requireBuilder, so the
|
|
99
|
+
// PRIVILEGED route is declared FIRST. Below the own-scoped one, its gate
|
|
100
|
+
// would bleed upward and mislabel that route as privileged.
|
|
101
|
+
|
|
102
|
+
// GET /builders/:builderId/credits — another builder's ledger.
|
|
103
|
+
//
|
|
104
|
+
// rank: metic — gated on the `builder.roster.read` ATOM, the same gate
|
|
105
|
+
// /builders/:id/active-claims carries, because this is the same act: reading
|
|
106
|
+
// another builder's record off the roster. ADR 0157 delegated that atom
|
|
107
|
+
// archon→metic. Note this exposes NO new secret — the leaderboard already
|
|
108
|
+
// publishes total_credits and /builders/:id/profile already publishes per-task
|
|
109
|
+
// awards, both unauthenticated. What this adds is the itemisation, which is
|
|
110
|
+
// precisely what makes the ledger auditable.
|
|
111
|
+
//
|
|
112
|
+
// The param is `:builderId`, not the `:id` its neighbours in
|
|
113
|
+
// src/bongos/routes/builders.js use, and deliberately: tests/own_scope_guard.mjs
|
|
114
|
+
// classifies a route as cross-owner by whether a path param NAMES a builder, and
|
|
115
|
+
// a bare `:id` reads as own-scoped. Since this file also mounts /me/credits it is
|
|
116
|
+
// held to that guard — so the explicit name is what lets the guard see this route
|
|
117
|
+
// for what it is and check that its gate is really there.
|
|
118
|
+
router.get('/builders/:builderId/credits', api.requireBuilder, api.requirePermission('builder.roster.read'), async (req, res) => {
|
|
119
|
+
const targetId = api.parseId(req, res, { param: 'builderId', code: 'bad_builder_id', positiveInt: true });
|
|
120
|
+
if (targetId === null) return;
|
|
121
|
+
const { opts, error } = parseLedgerQuery(req);
|
|
122
|
+
if (error) return res.fail(error, 400);
|
|
123
|
+
try {
|
|
124
|
+
const b = await api.getBuilderById(targetId);
|
|
125
|
+
if (!b) return res.fail('builder_not_found', 404);
|
|
126
|
+
await respondWithLedger(res, targetId, opts);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error('[gds] GET /builders/:builderId/credits', err);
|
|
129
|
+
res.fail('credit_ledger_read_failed', { status: 500, message: 'internal error' });
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// GET /me/credits — the caller's own ledger.
|
|
134
|
+
//
|
|
135
|
+
// rank: any-builder — own-scoped by construction: the builder id comes from the
|
|
136
|
+
// authenticated session and is never read off the request, so there is no
|
|
137
|
+
// parameter here to tamper with. A builder may always read their own ledger;
|
|
138
|
+
// gating this any higher would mean a Xenos cannot see what they were paid.
|
|
139
|
+
router.get('/me/credits', api.requireBuilder, async (req, res) => {
|
|
140
|
+
const { opts, error } = parseLedgerQuery(req);
|
|
141
|
+
if (error) return res.fail(error, 400);
|
|
142
|
+
try {
|
|
143
|
+
await respondWithLedger(res, Number(req.builder.id), opts);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
console.error('[gds] GET /me/credits', err);
|
|
146
|
+
res.fail('credit_ledger_read_failed', { status: 500, message: 'internal error' });
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return router;
|
|
151
|
+
};
|
|
@@ -56,9 +56,28 @@
|
|
|
56
56
|
return out.join('/') + (p.endsWith('/') ? '/' : '');
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Which link targets the reader will write into an href (task 1003482).
|
|
60
|
+
// escapeHtml stops markup, not URI schemes: a `javascript:` target would run
|
|
61
|
+
// on click. Vouched for: http(s), mailto, tel, an in-page #fragment, and a
|
|
62
|
+
// repo-relative path (which the resolver rewrites onto the repo bases).
|
|
63
|
+
// Everything else — javascript:, data:, vbscript:, file:, any unlisted
|
|
64
|
+
// scheme, a protocol-relative //host — renders as text. Tab/newline and
|
|
65
|
+
// leading controls are dropped before the scheme is read, because a
|
|
66
|
+
// browser's URL parser drops them too (`java\tscript:` runs).
|
|
67
|
+
const ALLOWED_SCHEMES = ['http', 'https', 'mailto', 'tel'];
|
|
68
|
+
function isAllowedLinkHref(href) {
|
|
69
|
+
if (href == null) return false;
|
|
70
|
+
const probe = String(href).replace(/[\t\n\r]/g, '').replace(/^[\x00-\x20]+/, '');
|
|
71
|
+
if (probe.startsWith('#')) return true;
|
|
72
|
+
if (/^[\/\\]{2}/.test(probe)) return false;
|
|
73
|
+
const scheme = probe.match(/^([a-z][a-z0-9+.-]*):/i);
|
|
74
|
+
return !scheme || ALLOWED_SCHEMES.includes(scheme[1].toLowerCase());
|
|
75
|
+
}
|
|
76
|
+
|
|
59
77
|
// Build a link resolver bound to where THIS doc lives in the repo. Relative
|
|
60
78
|
// repo paths (../adr/…, diagrams/…) are rewritten to something that resolves
|
|
61
79
|
// in the browser instead of 404-ing under /builders/*:
|
|
80
|
+
// - a target the allowlist refuses → null (renderInline drops the <a>)
|
|
62
81
|
// - http(s):// or #anchor or mailto:/tel: → unchanged
|
|
63
82
|
// - the diagrams dir → the in-hall diagrams route
|
|
64
83
|
// - everything else → GitHub blob/tree (bases fetched
|
|
@@ -72,6 +91,7 @@
|
|
|
72
91
|
const diagramsHref = (opts && opts.diagramsHref) || '/builders/diagrams';
|
|
73
92
|
return function resolveLinkHref(href) {
|
|
74
93
|
if (!href) return href;
|
|
94
|
+
if (!isAllowedLinkHref(href)) return null;
|
|
75
95
|
if (/^https?:\/\//i.test(href)) return href;
|
|
76
96
|
if (href.startsWith('#')) return href;
|
|
77
97
|
if (href.startsWith('mailto:') || href.startsWith('tel:')) return href;
|
|
@@ -108,6 +128,9 @@
|
|
|
108
128
|
out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, text, url) => {
|
|
109
129
|
const safeUrl = url.replace(/&/g, '&').replace(/'/g, "'");
|
|
110
130
|
const resolved = resolve(safeUrl);
|
|
131
|
+
// The reader never writes an href it did not vouch for — checked on the
|
|
132
|
+
// RESOLVED value, so a page's own resolver cannot reopen the door.
|
|
133
|
+
if (resolved == null || !isAllowedLinkHref(resolved)) return text;
|
|
111
134
|
let linkHost = '';
|
|
112
135
|
try { linkHost = new URL(resolved, location.href).hostname; } catch (_) { /* relative */ }
|
|
113
136
|
const instApex = location.hostname.split('.').slice(-2).join('.');
|
|
@@ -236,5 +259,5 @@
|
|
|
236
259
|
return { html: out.join('\n'), headings };
|
|
237
260
|
}
|
|
238
261
|
|
|
239
|
-
window.OTBMdReader = { escapeHtml, slugify, normalizePath, makeResolveLinkHref, renderInline, splitTableRow, parseMarkdown };
|
|
262
|
+
window.OTBMdReader = { escapeHtml, slugify, normalizePath, isAllowedLinkHref, makeResolveLinkHref, renderInline, splitTableRow, parseMarkdown };
|
|
240
263
|
})();
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"coreVersion": "^1.19.0",
|
|
6
6
|
"default": true,
|
|
7
7
|
"maintenance": { "status": "core-maintained" },
|
|
8
|
-
"contributes": { "skills": ["design", "design-sync", "figma-design-sync", "design-taste-frontend", "design-taste-frontend-v1", "high-end-visual-design", "redesign-existing-projects", "impeccable", "image-to-code", "imagegen-frontend-web", "imagegen-frontend-mobile", "brandkit"] },
|
|
8
|
+
"contributes": { "skills": ["design", "design-sync", "figma-design-sync", "design-taste-frontend", "design-taste-frontend-v1", "high-end-visual-design", "redesign-existing-projects", "impeccable", "style", "minimalist-ui", "industrial-brutalist-ui", "gpt-taste", "stitch-design-taste", "image-to-code", "imagegen-frontend-web", "imagegen-frontend-mobile", "brandkit"] },
|
|
9
9
|
"provides": [],
|
|
10
10
|
"consumes": []
|
|
11
11
|
}
|