@bongos/core 1.19.114 → 1.19.116
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 +29 -29
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +1 -1
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -1
- package/docs/api/openapi.json +1 -1
- package/modules/provisioning/onboard-plan.js +2 -2
- package/modules/provisioning/routes/provisioning.js +3 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +9 -3
- package/public-landing/projects.html +1382 -38
- package/scripts/gds/audit-authorship.sh +0 -4
- package/scripts/gds/onboard.js +1 -1
- package/scripts/gds/seed-bv1-profile-page-tasks.js +161 -0
- package/src/module-api.js +5 -1
- package/src/platform-server.js +13 -5
- package/tests/cosmos_token_contrast.mjs +10 -7
- package/tests/module-scope-map.mjs +1 -1
- package/tests/onboard.mjs +20 -16
- package/tests/platform_boot.mjs +21 -8
- package/tests/projects_hub.mjs +79 -11
- package/tests/public_landing_projects.mjs +15 -10
- package/public-landing/create.html +0 -1505
|
@@ -202,10 +202,6 @@ ALLOWLIST=(
|
|
|
202
202
|
'tests/auth_anthropic_identity.mjs'
|
|
203
203
|
'modules/dev-box/app/test/unit.mjs'
|
|
204
204
|
|
|
205
|
-
# The public landing page links the open-source repo by URL — repo-hosting
|
|
206
|
-
# identity (the provision-staging.sh class), not a builder identity.
|
|
207
|
-
'public-landing/create.html'
|
|
208
|
-
|
|
209
205
|
# The mirror-redaction floor: PRIOR_INSTANCE_FLOOR carries the prior
|
|
210
206
|
# instance's owner literal precisely so packaging can SCRUB it from every
|
|
211
207
|
# published core — the anti-leak spec, not a runtime hardcode (task 1002640).
|
package/scripts/gds/onboard.js
CHANGED
|
@@ -23,7 +23,7 @@ const oauth = require(path.join(__dirname, 'oauth-secret.js')); // validateC
|
|
|
23
23
|
// ── PURE core (exported for tests) ───────────────────────────────────────────
|
|
24
24
|
|
|
25
25
|
// The canonical step sequence now lives in ONE place — the provisioning module — so the
|
|
26
|
-
// CLI (here)
|
|
26
|
+
// CLI (here) and the hub's ?view=new wizard can't drift (task 2085; goal
|
|
27
27
|
// 35 / criterion 165: "the canonical step sequence is the source of truth; skill/CLI/hall
|
|
28
28
|
// are forms over it"). The web forms read it over GET /provisioning/onboard-plan; the CLI
|
|
29
29
|
// requires it directly. Re-exported below so `bongos onboard` + tests keep the same API.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Seed the delta tasks the profile-page spike (task 1002783) surfaced, under
|
|
2
|
+
// goal 1000045 "Public builder profiles" (BONGOS-V1, internal).
|
|
3
|
+
//
|
|
4
|
+
// Source of truth: docs/specs/bongos-v1-builder-profile-page.md (status: final).
|
|
5
|
+
// The Phase-1 spine (R01-R06) was already seeded by seed-bv1-profiles-tasks.js;
|
|
6
|
+
// this file adds ONLY what that spec found on top of it, so the two seeders are
|
|
7
|
+
// disjoint and both stay re-runnable.
|
|
8
|
+
//
|
|
9
|
+
// Idempotent on (source, source_ref) — re-running resolves to the existing rows
|
|
10
|
+
// and re-asserts the dependency edges.
|
|
11
|
+
//
|
|
12
|
+
// Run: node scripts/gds/seed-bv1-profile-page-tasks.js
|
|
13
|
+
|
|
14
|
+
const { apiCall } = require('./cli-lib');
|
|
15
|
+
|
|
16
|
+
const VER = 'BONGOS-V1';
|
|
17
|
+
const GOAL = 1000045;
|
|
18
|
+
const SRC = 'planning-session';
|
|
19
|
+
const SPEC = 'docs/specs/bongos-v1-builder-profile-page.md';
|
|
20
|
+
|
|
21
|
+
// Tasks that already exist and that these depend on — referenced by id, since
|
|
22
|
+
// they were seeded by the sibling seeder (or by the wave task 1002781).
|
|
23
|
+
const R01 = 1002277; // spike · design profile↔rollup join + consent model
|
|
24
|
+
const R02 = 1002278; // feature · profile API returns cross-project record
|
|
25
|
+
const R03 = 1002279; // feature · hide flag
|
|
26
|
+
const R05 = 1002281; // feature · shareable-by-login profile URL
|
|
27
|
+
const SETTINGS_SPIKE = 1002784; // spike · account settings page — inventory + placement
|
|
28
|
+
const PRIVACY_SPIKE = 1002785; // spike · account privacy model + invite inbox
|
|
29
|
+
|
|
30
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
31
|
+
|
|
32
|
+
// Throttled + 429-aware wrapper. The seeder makes ~20 writes in a burst, which
|
|
33
|
+
// is exactly the shape the per-IP limiter is there to catch.
|
|
34
|
+
async function api(method, path, body, _tries = 0) {
|
|
35
|
+
await sleep(400);
|
|
36
|
+
const r = await apiCall(method, `/api/gds${path}`, body);
|
|
37
|
+
if (r.status === 429 && _tries < 8) {
|
|
38
|
+
const wait = (r.data?.error?.details?.retry_after_seconds || 30) + 1;
|
|
39
|
+
console.log(` …rate-limited, waiting ${wait}s`);
|
|
40
|
+
await sleep(wait * 1000);
|
|
41
|
+
return api(method, path, body, _tries + 1);
|
|
42
|
+
}
|
|
43
|
+
return r;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// deps entries are either a local ref ('P01') or a numeric id of an existing task.
|
|
47
|
+
const TASKS = [
|
|
48
|
+
{
|
|
49
|
+
ref: 'P01', kind: 'feature', disc: 'engineer', prio: 5, est: 180, crit: ['C1'], deps: [R01],
|
|
50
|
+
title: 'P01 · Builder handle on platform accounts (seed, dedupe, reserved list, rename)',
|
|
51
|
+
desc: `Add a \`handle\` column to platform_identity_accounts: seeded from github_login at creation, deduped with a numeric suffix on collision, validated against a small reserved list (me, new, edit, settings, admin, api, u), builder-editable at a low rate limit, previous handle retained for a 301 window. github_id stays the join key everywhere — the handle is presentation only. WHY: github_login is mutable, so hanging the canonical profile URL on it means every GitHub rename silently 404s that builder's shared links. Namespaced module migration (ADR 0083). Spec: ${SPEC} (Decision 3).`,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
ref: 'P02', kind: 'feature', disc: 'engineer', prio: 5, est: 180, crit: ['C1'], deps: [R01],
|
|
55
|
+
title: 'P02 · Profile fields: bio + typed links on the platform account',
|
|
56
|
+
desc: `Add \`bio\` (plain text, ~280 chars, NO markdown and no auto-linking) and \`links\` (a TYPED, capped set — website, X, LinkedIn, Discord; max 4) to platform_identity_accounts, plus the authenticated write. Server-side validation must enforce https-only AND reject non-http(s) schemes and userinfo/host tricks — do not rely on the client. Render with rel="nofollow ugc noopener". WHY typed-and-capped rather than a free-form link list: a public, indexable page with user-controlled links is an SEO-spam magnet, and this removes the incentive without a moderation queue. Spec: ${SPEC} (Decision 4).`,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
ref: 'P03', kind: 'decision', disc: 'engineer', prio: 5, est: 60, crit: ['C1'], deps: [], ready: true,
|
|
60
|
+
title: 'P03 · Owner decision: provisional vs active profile state for check-in-created accounts',
|
|
61
|
+
desc: `Goal 1000045 justifies "public by default" as tied to having a platform account, "which already carries platform T&C consent". That premise is not currently true for every account: POST /sso/membership/check-in and POST /sso/activity/rollup cause the hub to upsert an account for a builder who has never visited the hub and never saw a platform T&C. Under auto-create (see task 1002783) those people would get a public, indexable, cross-project profile from a sign-in they performed on someone else's instance. RECOMMENDATION: a profile_state of 'provisional' (created by check-in — still counted in aggregates, but not publicly listed, not at the handle URL, not in the sitemap, not in hub search) vs 'active' (has signed in at the hub at least once). One column, one branch. This is an owner product/consent call, and it changes the shape of P09. Spec: ${SPEC}.`,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
ref: 'P04', kind: 'feature', disc: 'ui', prio: 4, est: 180, crit: ['C1'], deps: ['P01', 'P02', SETTINGS_SPIKE],
|
|
65
|
+
title: 'P04 · Settings → Profile rail group (the single profile-edit surface)',
|
|
66
|
+
desc: `One edit surface for the profile, as a new rail group in the hall Settings page (the RAIL_GROUPS pattern in modules/hall-ui/public/settings.js) — not a separate edit page. Holds handle, bio, links alongside the display-name and disciplines controls that already live there. Account-global fields must be edited where they are stored; final hub-vs-per-project placement is task ${SETTINGS_SPIKE}'s call, which is why this waits on it. Spec: ${SPEC} (Decision 4).`,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
ref: 'P05', kind: 'verify', disc: 'engineer', prio: 5, est: 120, crit: ['C1'], deps: [R02],
|
|
70
|
+
title: 'P05 · Leak test for the hub profile read (mirror of tests/profile_route.mjs)',
|
|
71
|
+
desc: `The instance route GET /builders/:id/profile projects an explicit public column whitelist and tests/profile_route.mjs proves the private columns (anthropic_email, anthropic_user_id, monthly_budget_usd, system_role, box_blocked*, deactivated_reason) never appear anywhere in the serialized response. The new cross-project read needs its OWN mirror test over the account + rollup projection before it is public — same sentinel-string technique, same 404-on-unknown-id assertion. Never spread a raw row. Spec: ${SPEC} (Constraints §1).`,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
ref: 'P06', kind: 'feature', disc: 'ui', prio: 3, est: 120, crit: ['C1'], deps: ['P02'],
|
|
75
|
+
title: 'P06 · "Complete your profile" needs-attention inbox line item (dismissible)',
|
|
76
|
+
desc: `When bio or preferred_disciplines are empty, nudge the builder — as a DISMISSIBLE line item in the needs-attention inbox (goal-inbox.js / #attention-scroll), never a new Home card. That is the hall placement rule (modules/hall-ui/CLAUDE.md → Placement): an action awaiting the builder is an inbox line. This is the replacement for the profile-setup wizard that task 1002783 rejected. Spec: ${SPEC} (Decision 2).`,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
ref: 'P07', kind: 'feature', disc: 'engineer', prio: 3, est: 210, crit: ['C1'], deps: [R02],
|
|
80
|
+
title: 'P07 · Hub recent works via push — recent_ships[] on the activity rollup',
|
|
81
|
+
desc: `The hub has no tasks table for other projects, so "recent works" cannot render there today. The existing PULL pattern (modules/platform-identity/project-stats.js) fans out to each project's public API — right for a builder count on a catalog page, wrong per-profile-view, and it cannot serve a CLI-only project at all. Extend the POST /sso/activity/rollup payload (which already carries credits/ships/karma/rank/disciplines per project) with a capped recent_ships[] — 3 per project: title, id, shipped_at, and a link into that instance — plus the instance-side reporter in core auth.js. Reuses a table and a reporter that already exist; costs no fan-out read; stays descriptive-analytics-only. Spec: ${SPEC} (page spec, section 4).`,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
ref: 'P08', kind: 'feature', disc: 'ui', prio: 4, est: 150, crit: ['C1'], deps: ['P01', R05],
|
|
85
|
+
title: 'P08 · Canonical profile URL wiring (instance rel=canonical → hub; numeric id redirects)',
|
|
86
|
+
desc: `One canonical public profile, two render paths: the hub's server-rendered handle URL is canonical, and the hall's own profile page stays the LOCAL view and emits rel=canonical to it when the builder has a platform account. The numeric ?id= form never dies — it is stable and rename-proof, so keep it as a permanent fallback that redirects to the canonical handle URL. Coordinate with goal 1000048 R14 (public builder-profile SEO page) so only ONE page gets built. Spec: ${SPEC} (Decision 1).`,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
ref: 'P09', kind: 'feature', disc: 'engineer', prio: 3, est: 150, crit: ['C1'], deps: ['P01', PRIVACY_SPIKE],
|
|
90
|
+
title: 'P09 · noindex until first ship + sitemap consent gate for builder profiles',
|
|
91
|
+
desc: `A public, indexable profile that anyone can mint is a backlink-farming surface. Gate indexing on the builder having at least one shipped work, and gate sitemap inclusion on consent (private accounts and — if the owner accepts P03 — provisional accounts are excluded). Coordinates with goal 1000048 R15 (builder profiles in sitemap, consent-gated). Shape depends on the P03 decision. Spec: ${SPEC} (Visibility rules).`,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
ref: 'P10', kind: 'verify', disc: 'engineer', prio: 3, est: 90, crit: ['C2'], deps: [PRIVACY_SPIKE, R03],
|
|
95
|
+
title: 'P10 · PROOF: a private account\'s profile 404s to a stranger and renders to its owner',
|
|
96
|
+
desc: `Verify the profile page honours the privacy model: a private account's handle URL returns 404 — NOT 403, because a 403 confirms the handle exists — to everyone except the account itself, which sees its own profile with a "private" banner. Also assert the stats-hidden case renders no partial numbers (a count beside a hidden total is a leak by subtraction). The authoritative model is task ${PRIVACY_SPIKE}'s; if it lands a different rule, this proof follows it and the spec is the thing that is wrong. Spec: ${SPEC} (Visibility rules).`,
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
(async () => {
|
|
101
|
+
const idByRef = {};
|
|
102
|
+
let created = 0;
|
|
103
|
+
let existing = 0;
|
|
104
|
+
console.log(`Seeding ${TASKS.length} tasks under goal ${GOAL} (${VER})…`);
|
|
105
|
+
for (const t of TASKS) {
|
|
106
|
+
const r = await api('POST', '/tasks', {
|
|
107
|
+
version_id: VER,
|
|
108
|
+
goal_id: GOAL,
|
|
109
|
+
title: t.title,
|
|
110
|
+
description: t.desc,
|
|
111
|
+
kind: t.kind,
|
|
112
|
+
discipline: t.disc,
|
|
113
|
+
priority: t.prio,
|
|
114
|
+
est_minutes: t.est,
|
|
115
|
+
criterion_ids: t.crit,
|
|
116
|
+
source: SRC,
|
|
117
|
+
source_ref: `bv1-profile-page-${t.ref}`,
|
|
118
|
+
});
|
|
119
|
+
if (!r.ok) {
|
|
120
|
+
console.log(` ${t.ref}: FAILED ${r.status} ${JSON.stringify(r.data)}`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const task = r.data.task || r.data;
|
|
124
|
+
idByRef[t.ref] = task.id;
|
|
125
|
+
if (r.data.idempotent || r.data.existing) existing++; else created++;
|
|
126
|
+
console.log(` ${t.ref} → task ${task.id} (${t.kind}/${t.disc}, P${t.prio}, [${t.crit.join(',')}])`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log('Wiring dependencies…');
|
|
130
|
+
let edges = 0;
|
|
131
|
+
for (const t of TASKS) {
|
|
132
|
+
if (!t.deps.length) continue;
|
|
133
|
+
const id = idByRef[t.ref];
|
|
134
|
+
// A dep is either a local ref (resolved from this run) or a literal task id.
|
|
135
|
+
const depIds = t.deps.map((d) => (typeof d === 'number' ? d : idByRef[d])).filter(Boolean);
|
|
136
|
+
if (!id || depIds.length !== t.deps.length) {
|
|
137
|
+
console.log(` ${t.ref}: dep resolve gap, skipping`);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// PUT replaces the full set, so a re-run converges instead of erroring on
|
|
141
|
+
// an edge that already exists.
|
|
142
|
+
const r = await api('PUT', `/tasks/${id}/dependencies`, { depends_on_task_ids: depIds });
|
|
143
|
+
if (!r.ok) {
|
|
144
|
+
console.log(` ${t.ref}: dep FAILED ${r.status} ${JSON.stringify(r.data)}`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
edges += depIds.length;
|
|
148
|
+
console.log(` ${t.ref} depends on ${t.deps.join(',')}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log('Promoting zero-dep roots to ready…');
|
|
152
|
+
for (const t of TASKS.filter((x) => x.ready)) {
|
|
153
|
+
const id = idByRef[t.ref];
|
|
154
|
+
if (!id) continue;
|
|
155
|
+
const r = await api('POST', `/tasks/${id}/promote`, {});
|
|
156
|
+
console.log(` ${t.ref} (task ${id}): ${r.ok ? 'ready' : 'promote note ' + JSON.stringify(r.data).slice(0, 120)}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(`\nDONE. created=${created} existing=${existing} depEdges=${edges}.`);
|
|
160
|
+
console.log('ref→id:', JSON.stringify(idByRef));
|
|
161
|
+
})();
|
package/src/module-api.js
CHANGED
|
@@ -655,8 +655,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
655
655
|
// landed since 1.19.112 with no explicit bump. run 31816239246. (task 1002620)
|
|
656
656
|
// 1.19.114 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
657
657
|
// landed since 1.19.113 with no explicit bump. run 31816599678. (task 1002620)
|
|
658
|
+
// 1.19.115 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
659
|
+
// landed since 1.19.114 with no explicit bump. run 31825824596. (task 1002620)
|
|
660
|
+
// 1.19.116 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
661
|
+
// landed since 1.19.115 with no explicit bump. run 31827642395. (task 1002620)
|
|
658
662
|
// ---------------------------------------------------------------------------
|
|
659
|
-
const CORE_VERSION = '1.19.
|
|
663
|
+
const CORE_VERSION = '1.19.116'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
660
664
|
|
|
661
665
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
662
666
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/src/platform-server.js
CHANGED
|
@@ -184,13 +184,21 @@ function buildPlatformApp() {
|
|
|
184
184
|
if (!html) return next();
|
|
185
185
|
res.type('html').send(html);
|
|
186
186
|
});
|
|
187
|
-
// The
|
|
188
|
-
//
|
|
187
|
+
// The create wizard was absorbed into the hub's ?view=new view (task 1002763;
|
|
188
|
+
// direction §4 — "/create redirects to ?view=new"). This route only carries
|
|
189
|
+
// visitors — and any ?name= seed the old links still send — into it. 302, not
|
|
190
|
+
// 301: /create is an OAuth return target in the wild, and a browser-cached
|
|
191
|
+
// permanent redirect would outlive any future change of shape.
|
|
189
192
|
app.get('/create', (req, res, next) => {
|
|
190
193
|
if (!isApexHost(req)) return next();
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
// rebuilt, not spliced: an inbound ?view= must not ride along (the hub
|
|
195
|
+
// reads the FIRST view param — a duplicate would bet the route on that
|
|
196
|
+
// parser detail), and an empty query must not leave a bare trailing '&'
|
|
197
|
+
const qi = req.url.indexOf('?');
|
|
198
|
+
const rest = new URLSearchParams(qi >= 0 ? req.url.slice(qi + 1) : '');
|
|
199
|
+
rest.delete('view');
|
|
200
|
+
const tail = rest.toString();
|
|
201
|
+
res.redirect(302, '/projects?view=new' + (tail ? '&' + tail : ''));
|
|
194
202
|
});
|
|
195
203
|
// THE PROJECT HUB (task 1002743) — one surface holding every project-shaped
|
|
196
204
|
// action: the map of what exists (the orb sky the gate used to own, plus the
|
|
@@ -144,8 +144,9 @@ const PAIRS = [
|
|
|
144
144
|
['.btn.ghost:hover label', 'var(--accent-deep)', CARD, 4.5],
|
|
145
145
|
['.btn.danger label', 'var(--err)', CARD, 4.5],
|
|
146
146
|
// Inline links: cosmos.css recolors its own to --sun-lit in dark (:179-262), but
|
|
147
|
-
//
|
|
148
|
-
//
|
|
147
|
+
// the ?view=new wizard styles (absorbed from create.html, task 1002763) carry
|
|
148
|
+
// eleven --accent-deep consumers and zero dark rules of their own, so there the
|
|
149
|
+
// token itself must carry dark.
|
|
149
150
|
['inline link on a card', 'var(--accent-deep)', CARD, 4.5],
|
|
150
151
|
['inline link on the page ground', 'var(--accent-deep)', 'var(--bg)', 4.5],
|
|
151
152
|
['code chip on --bg-deep', 'var(--accent-deep)', 'var(--bg-deep)', 4.5],
|
|
@@ -172,11 +173,13 @@ const PAIRS = [
|
|
|
172
173
|
['.projMsg.ok', 'var(--ok)', CARD, 4.5],
|
|
173
174
|
['.projMsg.err / .confirmRow', 'var(--err)', CARD, 4.5],
|
|
174
175
|
// chrome: wordmark + footer sit on the page ground, not on a card
|
|
175
|
-
//
|
|
176
|
-
// darkest tints, so cosmos.css's pairs
|
|
177
|
-
|
|
178
|
-
['
|
|
179
|
-
['
|
|
176
|
+
// wizard grounds (projects.html ?view=new, absorbed from create.html in task
|
|
177
|
+
// 1002763) — its panels carry their own darkest tints, so cosmos.css's pairs
|
|
178
|
+
// alone do not cover them
|
|
179
|
+
['?view=new .ghrow .priv 11px', 'var(--ink-soft)', 'color-mix(in srgb,var(--ink) 8%,var(--bg-deep))', 4.5],
|
|
180
|
+
['--on-ok on an --ok fill', 'var(--on-ok)', 'var(--ok)', 4.5],
|
|
181
|
+
['?view=new input ::placeholder', 'var(--ink-faint)', CARD, 4.5],
|
|
182
|
+
['?view=new .ghlist .hint on deep', 'var(--ink-faint)', 'var(--bg-deep)', 4.5],
|
|
180
183
|
['--chrome-accent wordmark', 'var(--chrome-accent)', 'var(--page-bg)', 4.5],
|
|
181
184
|
['--chrome-ink', 'var(--chrome-ink)', 'var(--page-bg)', 4.5],
|
|
182
185
|
['--foot-ink footer copy', 'var(--foot-ink)', 'var(--page-bg)', 4.5],
|
|
@@ -127,7 +127,7 @@ test('platform-identity joined the roster (task 1002322) — the catalog territo
|
|
|
127
127
|
'modules/platform-identity/routes/projects.js', // the feed + invite routes
|
|
128
128
|
'modules/status-ui/public/explore.js', // orb render-type surface
|
|
129
129
|
'modules/hall-ui/public/my-projects.js', // owner settings / applications queue
|
|
130
|
-
'public-landing/
|
|
130
|
+
'public-landing/projects.html', // the creation step, ?view=new since task 1002763
|
|
131
131
|
'modules/provisioning/', // module-bundle selection at provision time
|
|
132
132
|
]) {
|
|
133
133
|
assert.equal(covers(touch), true, `wall does not cover ${touch}`);
|
package/tests/onboard.mjs
CHANGED
|
@@ -86,18 +86,19 @@ test('every render surface carries the terminal hall step (task 1002703)', () =>
|
|
|
86
86
|
const cli = read('scripts/gds/onboard.js');
|
|
87
87
|
assert.match(cli, /builders hall/i, 'the CLI walkthrough ends at the hall');
|
|
88
88
|
assert.match(cli, /bongos dev/, 'the CLI offers the local-hall one-liner when no domain');
|
|
89
|
-
const
|
|
90
|
-
const enterHall =
|
|
91
|
-
assert.ok(enterHall.length >= 1, '
|
|
92
|
-
assert.match(enterHall.join(''), /\/builders/, '
|
|
93
|
-
assert.match(enterHall.join(''), /bongos dev/, '
|
|
89
|
+
const hubHtml = read('public-landing/projects.html');
|
|
90
|
+
const enterHall = hubHtml.match(/data-onboard-step="enter-hall"[^]*?<\/li>/g) || [];
|
|
91
|
+
assert.ok(enterHall.length >= 1, 'the ?view=new wizard tags the enter-hall step');
|
|
92
|
+
assert.match(enterHall.join(''), /\/builders/, 'the wizard points at the hall address');
|
|
93
|
+
assert.match(enterHall.join(''), /bongos dev/, 'the wizard offers the local hall when no domain');
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
test('the
|
|
96
|
+
test('the wizard done panel is plan-driven, manifest-driven, and owner-honest (task 1002694)', () => {
|
|
97
97
|
// Gaps 2/3/4/14 of the project-creation audit (docs/audits/project-creation-gap-list.md).
|
|
98
|
+
// The wizard lives inside the hub page since task 1002763.
|
|
98
99
|
const { readFileSync } = require('node:fs');
|
|
99
100
|
const path = require('node:path');
|
|
100
|
-
const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/
|
|
101
|
+
const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/projects.html'), 'utf8');
|
|
101
102
|
// gap 2 (idea 1000675): the checklist renders FROM the canonical plan endpoint
|
|
102
103
|
assert.match(src, /provisioning\/onboard-plan/, 'the done panel fetches the canonical plan');
|
|
103
104
|
// gap 3 (idea 1000676): the ADR 0133 one-click manifest flow has a UI caller again
|
|
@@ -105,12 +106,14 @@ test('the /create done panel is plan-driven, manifest-driven, and owner-honest (
|
|
|
105
106
|
assert.match(src, /github-app\/status/, 'and polls its progress to reflect live state');
|
|
106
107
|
// gap 4: the operator-only control-plane command never reaches a new owner
|
|
107
108
|
assert.doesNotMatch(src, /oauth-secret place/, 'owners are never handed the control-plane placement command');
|
|
108
|
-
// gap 14 (idea 1000684): repo visibility is stated, with the private path named
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
// gap 14 (idea 1000684): repo visibility is stated, with the private path named.
|
|
110
|
+
// Landmarks unique to the wizard — bare /public/ and /adopt/i match hub copy
|
|
111
|
+
// that has nothing to do with it, which would leave this guard vacuous.
|
|
112
|
+
assert.match(src, /public stays public, private stays private/, 'visibility is stated');
|
|
113
|
+
assert.match(src, /id="c-adopt"/, 'the private path (adopt your own private repo) is named');
|
|
111
114
|
});
|
|
112
115
|
|
|
113
|
-
test('the
|
|
116
|
+
test('the wizard gate trust link points at a live artifact, not the dead private-repo URL (task 1002705)', () => {
|
|
114
117
|
// Gap 1 of the project-creation audit: the gate linked a nonexistent private repo
|
|
115
118
|
// (404 even authenticated — ADR 0167 names it). No public code artifact exists yet
|
|
116
119
|
// (the ADR 0099 mirror is dormant until ~2026-11, and both npm scopes 404 anonymously
|
|
@@ -118,7 +121,7 @@ test('the /create gate trust link points at a live artifact, not the dead privat
|
|
|
118
121
|
// "open source" is the license itself.
|
|
119
122
|
const { readFileSync } = require('node:fs');
|
|
120
123
|
const path = require('node:path');
|
|
121
|
-
const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/
|
|
124
|
+
const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/projects.html'), 'utf8');
|
|
122
125
|
assert.doesNotMatch(src, /Claude-OTB/, 'the dead repo link is gone');
|
|
123
126
|
assert.match(src, /https:\/\/www\.gnu\.org\/licenses\/agpl-3\.0/, 'the trust link resolves (the AGPL-3.0 license text)');
|
|
124
127
|
});
|
|
@@ -168,19 +171,20 @@ test('onboardPlan is the SAME function the provisioning module owns (single sour
|
|
|
168
171
|
assert.equal(ob.onboardPlan, canonical, 'scripts/gds/onboard.js must re-export the module’s onboardPlan');
|
|
169
172
|
});
|
|
170
173
|
|
|
171
|
-
test('the web form (
|
|
174
|
+
test('the web form (?view=new) declares EXACTLY the canonical human steps — drift guard', () => {
|
|
172
175
|
// goal 35 / criterion 165: "the canonical step sequence is the source of truth;
|
|
173
176
|
// skill/CLI/web are forms over it." The web form tags its human-step <li>s with
|
|
174
177
|
// data-onboard-step="<key>"; this asserts that set equals the plan's human-step keys,
|
|
175
178
|
// so adding/removing/renaming a human step in the single source fails CI until every
|
|
176
179
|
// form is updated (and vice-versa) — the sequence can no longer drift between forms.
|
|
177
|
-
// (The hall new-project wizard was removed in task 1002675
|
|
178
|
-
//
|
|
180
|
+
// (The hall new-project wizard was removed in task 1002675, and the apex /create
|
|
181
|
+
// page was absorbed into the hub's ?view=new view in task 1002763; the skill +
|
|
182
|
+
// CLI + the hub wizard remain the canonical forms.)
|
|
179
183
|
const { readFileSync } = require('node:fs');
|
|
180
184
|
const path = require('node:path');
|
|
181
185
|
const humanKeys = ob.onboardPlan().filter((s) => s.kind === 'human').map((s) => s.key).sort();
|
|
182
186
|
const surfaces = {
|
|
183
|
-
'/
|
|
187
|
+
'/projects?view=new wizard': 'public-landing/projects.html',
|
|
184
188
|
};
|
|
185
189
|
for (const [name, rel] of Object.entries(surfaces)) {
|
|
186
190
|
const src = readFileSync(path.join(import.meta.dirname, '..', rel), 'utf8');
|
package/tests/platform_boot.mjs
CHANGED
|
@@ -121,16 +121,29 @@ test('the apex host serves the landing page — not the game — on a game-less
|
|
|
121
121
|
assert.doesNotMatch(res.body, /Create a project/);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
test('
|
|
125
|
-
// The
|
|
126
|
-
//
|
|
124
|
+
test('/create redirects into the hub wizard, seed intact (task 1002763)', async () => {
|
|
125
|
+
// The wizard was absorbed into /projects?view=new (direction §4); /create
|
|
126
|
+
// survives only as a 302 so old links and OAuth returns keep working. The
|
|
127
|
+
// query string rides along — /create?name= was the seeding entry point.
|
|
127
128
|
const res = await request('/create', 'game.test.local');
|
|
128
|
-
assert.equal(res.status,
|
|
129
|
-
assert.
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
assert.equal(res.status, 302);
|
|
130
|
+
assert.equal(res.headers.location, '/projects?view=new');
|
|
131
|
+
// the query is rebuilt (URLSearchParams re-encodes space as '+', which the
|
|
132
|
+
// hub's URLSearchParams reading decodes right back), and an inbound view=
|
|
133
|
+
// never rides along to fight the one this route asserts
|
|
134
|
+
const seeded = await request('/create?name=Sky%20Farm&view=mine', 'game.test.local');
|
|
135
|
+
assert.equal(seeded.status, 302);
|
|
136
|
+
assert.equal(seeded.headers.location, '/projects?view=new&name=Sky+Farm');
|
|
137
|
+
const empty = await request('/create?', 'game.test.local');
|
|
138
|
+
assert.equal(empty.headers.location, '/projects?view=new');
|
|
139
|
+
// the branded wizard itself now ships inside the hub page
|
|
140
|
+
const hub = await request('/projects', 'game.test.local');
|
|
141
|
+
assert.equal(hub.status, 200);
|
|
142
|
+
assert.match(hub.headers['content-type'] || '', /text\/html/);
|
|
143
|
+
assert.match(hub.body, /window\.__BRANDING__=/);
|
|
144
|
+
assert.match(hub.body, /Create a project/);
|
|
132
145
|
// the wizard files the request against the provisioning API, nothing else
|
|
133
|
-
assert.match(
|
|
146
|
+
assert.match(hub.body, /provisioning\/instances/);
|
|
134
147
|
});
|
|
135
148
|
|
|
136
149
|
test('R51: an API-shaped 404 still gets a self-describing JSON body', async () => {
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -212,17 +212,85 @@ test('the gate is retired — no route, no files, no links into it', () => {
|
|
|
212
212
|
assert.doesNotMatch(HUB, /href="\/gate/);
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
-
test('/create
|
|
216
|
-
// Task 1002743
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
assert.
|
|
225
|
-
|
|
215
|
+
test('/create is absorbed — the wizard lives here and the route only redirects (task 1002763)', () => {
|
|
216
|
+
// Task 1002743 stopped at step 1: the hub named a project and navigated to
|
|
217
|
+
// /create?name=. This task carries the absorption the owner amended into it
|
|
218
|
+
// (direction §4: "/create redirects to ?view=new"): the wizard's six panels
|
|
219
|
+
// are real markup inside the new view (C7), create.html is deleted — the
|
|
220
|
+
// gate precedent above: express.static would keep serving the raw file —
|
|
221
|
+
// and the name handoff became an in-page step advance.
|
|
222
|
+
assert.equal(fs.existsSync(path.join(ROOT, 'public-landing', 'create.html')), false,
|
|
223
|
+
'create.html must be gone: express.static would keep serving it raw and unbranded');
|
|
224
|
+
assert.doesNotMatch(SCRIPT, /location\.assign\('\/create'/,
|
|
225
|
+
'naming a project advances the wizard in place — no navigation');
|
|
226
|
+
assert.doesNotMatch(HUB, /href="\/create"/, 'no link may point at the retired route');
|
|
227
|
+
assert.match(HUB, /href="\?view=new"/, 'in-page links carry the view query instead');
|
|
228
|
+
const server = fs.readFileSync(path.join(ROOT, 'src', 'platform-server.js'), 'utf8');
|
|
229
|
+
assert.match(server, /res\.redirect\(302, '\/projects\?view=new'/,
|
|
230
|
+
'/create must 302 into the hub (302, not 301: the URL is an OAuth return target in the wild)');
|
|
231
|
+
for (const id of ['panel-gate', 'panel-name', 'panel-home', 'panel-address', 'panel-review', 'panel-done']) {
|
|
232
|
+
assert.match(HUB, new RegExp(`id="${id}"`), `the wizard's ${id} ships as real markup (C7)`);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('the absorbed wizard sheds its two URL landmines — /gate/ and the whole-query strip', () => {
|
|
237
|
+
// create.html carried a document-level Escape that navigated to the retired
|
|
238
|
+
// /gate/ (a 404 the href-only guard above cannot see: it was a
|
|
239
|
+
// location.assign, not an href) and a replaceState that rewrote the URL to
|
|
240
|
+
// location.pathname — which on this page would destroy ?view=new on load.
|
|
241
|
+
assert.doesNotMatch(SCRIPT, /['"]\/gate\//,
|
|
242
|
+
'no code path may navigate into the retired gate');
|
|
243
|
+
assert.doesNotMatch(SCRIPT, /history\.replaceState\(null, '', location\.pathname\)/,
|
|
244
|
+
"the hub's '?view=' + v rewrite is the only URL writer on this page");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('no id is declared twice — the wizard merge had four collisions to clear', () => {
|
|
248
|
+
// modeToggle, who, signin, gateAlert all existed on BOTH pages; a duplicate
|
|
249
|
+
// id makes the second node unreachable by getElementById and the first
|
|
250
|
+
// silently wins. Scan the real markup only (JS builds ids like `dom-<iid>`
|
|
251
|
+
// from fragments that would false-positive here).
|
|
252
|
+
const afterHead = HUB.slice(HUB.indexOf('</script>') + '</script>'.length);
|
|
253
|
+
const markup = afterHead.slice(0, afterHead.indexOf('<script>'));
|
|
254
|
+
assert.ok(markup.includes('id="view-new"'), 'the markup slice must span the views');
|
|
255
|
+
const ids = [...markup.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]);
|
|
256
|
+
const seen = new Set();
|
|
257
|
+
const dupes = ids.filter((id) => (seen.has(id) ? true : (seen.add(id), false)));
|
|
258
|
+
assert.deepEqual(dupes, [], `duplicate ids in the hub markup: ${dupes}`);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('the new view balances its own tree — the map-only walkers never covered it', () => {
|
|
262
|
+
// div-balance (:535) counts view-map only, and every openAncestorsAt needle
|
|
263
|
+
// lives there too; a lopsided div in the wizard block would be caught by
|
|
264
|
+
// nothing else while silently reparenting the footer.
|
|
265
|
+
const start = HUB.indexOf('<section id="view-new"');
|
|
266
|
+
const end = HUB.indexOf('<div class="foot"');
|
|
267
|
+
assert.ok(start !== -1 && end > start, 'need the view-new span');
|
|
268
|
+
const wiz = HUB.slice(start, end);
|
|
269
|
+
assert.equal((wiz.match(/<div\b/g) || []).length, (wiz.match(/<\/div>/g) || []).length,
|
|
270
|
+
'every div opened in #view-new closes in #view-new');
|
|
271
|
+
assert.equal((wiz.match(/<section\b/g) || []).length, (wiz.match(/<\/section>/g) || []).length,
|
|
272
|
+
'every section opened in #view-new closes in #view-new');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('C8 — the wizard polls stop dead off the new view, like the sky off the map', () => {
|
|
276
|
+
// create.html's progress watch (5s) and manifest poll (4s/8s) had NO view
|
|
277
|
+
// guard — merged unguarded they would poll a hidden view for up to 33
|
|
278
|
+
// minutes. Both ticks must consult the same gate the sky consults.
|
|
279
|
+
assert.match(SCRIPT, /function wizPollActive\(\)/, 'the wizard needs its own stage guard');
|
|
280
|
+
assert.match(SCRIPT, /!document\.hidden && currentView === 'new'/,
|
|
281
|
+
'the guard is hidden-tab aware and view-scoped');
|
|
282
|
+
const gatedTicks = (SCRIPT.match(/if \(!wizPollActive\(\)\)/g) || []).length;
|
|
283
|
+
assert.ok(gatedTicks >= 2, `both wizard ticks (progress + manifest) must check the guard, found ${gatedTicks}`);
|
|
284
|
+
assert.match(SCRIPT, /if \(v === 'new'\) wizard\.enter\(focus\); else wizard\.leave\(\);/,
|
|
285
|
+
'setView hands the view to the wizard exactly like it starts/stops the sky');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('the wizard never steals focus on boot — focus moves only on deliberate steps', () => {
|
|
289
|
+
// create.html's show() unconditionally focused each panel's h1, and its boot
|
|
290
|
+
// always called show() — merged as-is, every hub load would rip focus into a
|
|
291
|
+
// hidden view. The hub rule (setView's `focus` flag) now reaches show() too.
|
|
292
|
+
assert.match(SCRIPT, /function show\(step, moveFocus\)/, 'show() must carry the focus flag');
|
|
293
|
+
assert.match(SCRIPT, /if \(moveFocus\)/, 'and only focus when it is set');
|
|
226
294
|
});
|
|
227
295
|
|
|
228
296
|
/* ── The platform's own entry (task 1002760) ────────────────────────────────
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
// intent_conflict, not_teardownable, bad_domain, domain_taken,
|
|
15
15
|
// domain_already_set, domain_not_attachable are all SAID, never swallowed)
|
|
16
16
|
// • an instance with no domain gets an HONEST no-address state, not a blank
|
|
17
|
-
// •
|
|
18
|
-
// has it" stop (the hall
|
|
17
|
+
// • the wizard's poll give-up points at the projects view — the false "the
|
|
18
|
+
// hall has it" stop (the hall card was removed in task 1002675) is gone.
|
|
19
|
+
// (create.html itself was absorbed into ?view=new in task 1002763.)
|
|
19
20
|
//
|
|
20
21
|
// Run: node --test tests/public_landing_projects.mjs
|
|
21
22
|
|
|
@@ -106,17 +107,20 @@ test('a domainless instance gets an honest no-address state', async () => {
|
|
|
106
107
|
assert.match(body, /[Nn]o address yet/);
|
|
107
108
|
});
|
|
108
109
|
|
|
109
|
-
test('the empty state points a new owner at
|
|
110
|
+
test('the empty state points a new owner at the wizard view — never at a hall', async () => {
|
|
110
111
|
const { body } = await request('/projects', 'game.test.local');
|
|
111
|
-
|
|
112
|
+
// /create was absorbed into ?view=new (task 1002763); the CTA moves in-page
|
|
113
|
+
assert.match(body, /href="\?view=new"/);
|
|
114
|
+
assert.doesNotMatch(body, /href="\/create"/, 'no link may point at the retired route');
|
|
112
115
|
// the hall my-projects card was REMOVED (task 1002675; hall my-projects is
|
|
113
116
|
// federation-only) — this page must not send an owner there
|
|
114
117
|
assert.doesNotMatch(body, /Enter the Hall/i);
|
|
115
118
|
});
|
|
116
119
|
|
|
117
|
-
test('
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
test('the watch give-up hands the owner their projects view — "the hall has it" is dead', async () => {
|
|
121
|
+
// the wizard (and its 33-minute watch cap) lives on /projects now
|
|
122
|
+
const { body } = await request('/projects', 'game.test.local');
|
|
123
|
+
assert.match(body, /your projects page/, 'the give-up path must hand the owner the projects view');
|
|
120
124
|
assert.doesNotMatch(body, /the hall has it/, 'the false poll-stop copy must be gone');
|
|
121
125
|
});
|
|
122
126
|
|
|
@@ -151,7 +155,8 @@ test('the stamp is derived from CONTENT, so it changes only when the file does',
|
|
|
151
155
|
});
|
|
152
156
|
|
|
153
157
|
test('every landing surface is stamped, not just the one that broke', async () => {
|
|
154
|
-
|
|
158
|
+
// /create dropped from the loop: it 302s since task 1002763 (no body to stamp)
|
|
159
|
+
for (const p of ['/', '/projects']) {
|
|
155
160
|
const res = await request(p);
|
|
156
161
|
if (res.status !== 200) continue;
|
|
157
162
|
assert.doesNotMatch(res.body, /(href|src)="\/assets\/[^"?]+"/,
|
|
@@ -163,8 +168,8 @@ test('routes and API paths are never mistaken for assets', async () => {
|
|
|
163
168
|
// stamping resolves the URL against public-landing/ — a route names no file,
|
|
164
169
|
// so it must come back untouched rather than acquiring a meaningless ?v=
|
|
165
170
|
const res = await request('/projects');
|
|
166
|
-
assert.match(res.body, /href="\/
|
|
167
|
-
assert.doesNotMatch(res.body, /href="
|
|
171
|
+
assert.match(res.body, /href="\/"/, "'/' is a route, not an asset");
|
|
172
|
+
assert.doesNotMatch(res.body, /href="\/\?v=/);
|
|
168
173
|
assert.doesNotMatch(res.body, /href="\/projects\?v=/);
|
|
169
174
|
assert.doesNotMatch(res.body, /\/api\/bongos\/[^"]*\?v=[0-9a-f]{8}/,
|
|
170
175
|
'API paths must never be stamped');
|