@bongos/core 1.19.424 → 1.19.426
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 +59 -34
- package/docs/adr/0210-scouting-graduates-on-an-instance-asserted-owner-claim.md +100 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +1 -1
- package/docs/copy-inventory.md +184 -184
- package/docs/copy-registry.json +235 -235
- package/docs/design/mocks/hub/wizard.html +421 -0
- package/docs/design/projects-hub-direction-v2.md +26 -0
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/people.js +5 -5
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/settings.js +12 -12
- package/modules/platform-identity/account-visibility.js +47 -1
- package/modules/platform-identity/platform-identity.js +14 -11
- package/modules/platform-identity/routes/scouting.js +61 -21
- package/modules/platform-identity/routes/sso.js +6 -0
- package/modules/platform-identity/scouting-authz.js +139 -0
- package/modules/ui-design/kit/serve.js +3 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +63 -0
- package/public-landing/projects.html +207 -199
- package/public-landing/projects.probes.json +7 -1
- package/public-landing/projects.states.json +7 -1
- package/src/bongos/auth.js +59 -3
- package/src/module-api.js +1 -1
- package/tests/account_privacy_flags.mjs +23 -13
- package/tests/effective_visibility_predicate.mjs +49 -4
- package/tests/projects_hub.mjs +56 -0
- package/tests/scouting_owner_access_and_reach.mjs +544 -0
- package/tests/scouting_page_gate.mjs +185 -0
package/src/bongos/auth.js
CHANGED
|
@@ -390,9 +390,54 @@ const PAGE_GATES = Object.freeze({
|
|
|
390
390
|
requireNonXenosPage: Object.freeze({ permission: 'page.view.builder', deniedTo: '/builders/not-ready' }),
|
|
391
391
|
requireGovernmentPage: Object.freeze({ permission: 'page.view.government', deniedTo: '/builders' }),
|
|
392
392
|
requireGovernmentManagePage: Object.freeze({ permission: 'government.manage', deniedTo: '/builders' }),
|
|
393
|
-
|
|
393
|
+
// `alsoAdmits` — a SECOND, non-rank audience this page's DATA ROUTE admits, so
|
|
394
|
+
// the page can follow it (ADR 0210, task 1002290). /people fronts GET /scouting,
|
|
395
|
+
// which since R14 admits a `project.curate` holder OR a PROJECT OWNER; a page
|
|
396
|
+
// gate that could only ever ask about a permission would leave the widened
|
|
397
|
+
// audience staring at a redirect. The value names a function on the
|
|
398
|
+
// `platform-identity.hub` port — the module that owns the ownership fact — and
|
|
399
|
+
// is consulted ONLY after the permission check has already refused, so it can
|
|
400
|
+
// widen and never narrow. Absent port, absent function, or a throw: no admit.
|
|
401
|
+
requireProjectCuratePage: Object.freeze({
|
|
402
|
+
permission: 'project.curate', deniedTo: '/builders', alsoAdmits: 'isProjectOwner',
|
|
403
|
+
}),
|
|
394
404
|
});
|
|
395
405
|
|
|
406
|
+
// The `alsoAdmits` door — "is this visitor the page's OTHER audience?", asked of
|
|
407
|
+
// the module that owns the answer, only for the one gate that declares one.
|
|
408
|
+
//
|
|
409
|
+
// FAIL-CLOSED, EVERY WAY IT CAN FAIL: a gate with no `alsoAdmits`, a hub module
|
|
410
|
+
// that is off (every non-hub instance), a port that never grew the function, or a
|
|
411
|
+
// resolver that throws all answer FALSE — the visitor is redirected exactly as
|
|
412
|
+
// before. The only outcome that admits is an explicit truthy answer.
|
|
413
|
+
//
|
|
414
|
+
// Not authority (ADR 0016): the answer gates a PAGE SHELL and nothing else, and
|
|
415
|
+
// every data endpoint behind that shell still carries its own server-side gate.
|
|
416
|
+
async function pageGateAlsoAdmits(name, session) {
|
|
417
|
+
const { alsoAdmits } = PAGE_GATES[name];
|
|
418
|
+
if (!alsoAdmits) return false;
|
|
419
|
+
// The WHOLE lookup is inside the try, not just the call: resolving a seam can
|
|
420
|
+
// throw too, and a throw there would 500 the page instead of taking the clean
|
|
421
|
+
// deny-redirect every other failure here takes. One catch, one outcome.
|
|
422
|
+
try {
|
|
423
|
+
const hub = seams.resolveOptional('platform-identity.hub');
|
|
424
|
+
// OWN property only. Every object inherits `constructor`, `valueOf` and
|
|
425
|
+
// `toString`, each of which is a function whose result is truthy — so a plain
|
|
426
|
+
// `hub[name]` lookup would let a mis-typed or mis-declared `alsoAdmits` admit
|
|
427
|
+
// EVERYONE. Unreachable today (the only value is a frozen literal), but a
|
|
428
|
+
// fail-OPEN default has no business sitting in an authorization path.
|
|
429
|
+
if (!hub || !Object.prototype.hasOwnProperty.call(hub, alsoAdmits)) return false;
|
|
430
|
+
const ask = hub[alsoAdmits];
|
|
431
|
+
if (typeof ask !== 'function') return false;
|
|
432
|
+
return !!(await ask(session.github_id));
|
|
433
|
+
} catch (err) {
|
|
434
|
+
// Message only — the answer came from a query that can carry row data.
|
|
435
|
+
require('./logger').logger.child({ src: 'auth' })
|
|
436
|
+
.error({ err: err && err.message }, `${name} ${alsoAdmits} check failed`);
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
396
441
|
// A builder's effective permission set, or NULL when authority cannot be
|
|
397
442
|
// ESTABLISHED (the `governance` port is absent or malformed, or the resolver
|
|
398
443
|
// threw). Every caller must read null as a DENIAL: admitting when the authority
|
|
@@ -446,6 +491,15 @@ async function runPageGate(name, req, res, next) {
|
|
|
446
491
|
}
|
|
447
492
|
const held = await resolveHeldPermissions(session.builder_id, session.raw_permission_keys);
|
|
448
493
|
if (held === null || !held.has(permission)) {
|
|
494
|
+
// The rank door said no. A page whose data route admits a SECOND audience
|
|
495
|
+
// asks that door before redirecting (ADR 0210) — consulted here and nowhere
|
|
496
|
+
// else, so it can only widen. Asked on the authority-outage branch too,
|
|
497
|
+
// because the ownership fact does not come from the authority system: not
|
|
498
|
+
// asking would make the page narrower than the route it fronts.
|
|
499
|
+
if (await pageGateAlsoAdmits(name, session)) {
|
|
500
|
+
next();
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
449
503
|
// N-1 (task 1002613): a bare deniedTo rendered nothing — every denial read as
|
|
450
504
|
// "I signed in and got dumped at the home page". Name the permission (and an
|
|
451
505
|
// authority outage, distinctly) in the query so the hall shell can say why.
|
|
@@ -488,8 +542,10 @@ async function requireGovernmentManagePage(req, res, next) {
|
|
|
488
542
|
// The /people builder-directory page (task 1002577) — gated on the
|
|
489
543
|
// `project.curate` ATOM, the same key its one data route (GET /scouting,
|
|
490
544
|
// modules/platform-identity) checks, so the page layer can never drift wider
|
|
491
|
-
// than the API it fronts.
|
|
492
|
-
//
|
|
545
|
+
// than the API it fronts. R14 (goal 1000045, ADR 0210) widened that route to a
|
|
546
|
+
// SECOND audience that is not a rank at all — a project owner — so this gate
|
|
547
|
+
// carries the same OR through `alsoAdmits` above: the two doors of the page are
|
|
548
|
+
// exactly the two doors of the route, and neither layer can drift from it.
|
|
493
549
|
async function requireProjectCuratePage(req, res, next) {
|
|
494
550
|
return runPageGate('requireProjectCuratePage', req, res, next);
|
|
495
551
|
}
|
package/src/module-api.js
CHANGED
|
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
33
33
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
34
34
|
// the entry to that file. Look for a version's history there, not here.
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
|
-
const CORE_VERSION = '1.19.
|
|
36
|
+
const CORE_VERSION = '1.19.426'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
37
37
|
|
|
38
38
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
39
39
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -284,23 +284,33 @@ test('the discoverable toggle names EXACTLY what it grants — the D3 sliver, na
|
|
|
284
284
|
'the section ships hidden — it reveals only where the hub module answers');
|
|
285
285
|
});
|
|
286
286
|
|
|
287
|
-
test('each toggle claims exactly what is enforced —
|
|
287
|
+
test('each toggle claims exactly what is enforced — and BOTH are now in force', () => {
|
|
288
288
|
const js = readFileSync(join(ROOT, 'modules/hall-ui/public/settings.js'), 'utf8');
|
|
289
289
|
const html = readFileSync(join(ROOT, 'modules/hall-ui/public/settings.html'), 'utf8');
|
|
290
|
-
// A toggle may promise only what a read path actually enforces
|
|
291
|
-
//
|
|
292
|
-
// public read, so its row moved to the present tense
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
290
|
+
// A toggle may promise only what a read path actually enforces. The two rows
|
|
291
|
+
// reached that point one task apart. Task 1002967 wired VISIBILITY into every
|
|
292
|
+
// public read, so its row moved to the present tense — and it must also say
|
|
293
|
+
// the page 404s to the OWNER, because that is the surprising half of D2 (the
|
|
294
|
+
// owner reads their record through the own-scope side door instead). Task
|
|
295
|
+
// 1002290 then wired RECRUITING REACH into listScoutingRollup, the one reader
|
|
296
|
+
// behind /scouting and the hall's People directory, so its row followed on the
|
|
297
|
+
// same terms. The assertions are unchanged in KIND — copy must track
|
|
298
|
+
// enforcement — only in which tense each row is now required to be in.
|
|
298
299
|
assert.match(js, /Private hides the page from everyone — including you/,
|
|
299
300
|
'visibility is enforced now, so the row says so — and names the 404-to-self');
|
|
300
301
|
assert.doesNotMatch(js, /Private will hide the page/,
|
|
301
302
|
'the future tense outlived its truth the moment resolveHandle joined the predicate');
|
|
302
|
-
assert.match(
|
|
303
|
-
'
|
|
304
|
-
assert.doesNotMatch(js, /
|
|
305
|
-
'
|
|
303
|
+
assert.match(js, /Recruiters looking for builders can find you\b/,
|
|
304
|
+
'recruiting reach is enforced now (task 1002290), so the row promises it in the present tense');
|
|
305
|
+
assert.doesNotMatch(js, /will be able to find you/,
|
|
306
|
+
'the future tense outlived its truth the moment listScoutingRollup joined the reach predicate');
|
|
307
|
+
assert.match(js, /takes you out of the People directory straight away/,
|
|
308
|
+
'and the row names the ONE surface that reads the flag today — a promise you can go and check');
|
|
309
|
+
assert.doesNotMatch(js, /every recruiting view/,
|
|
310
|
+
'never a promise over surfaces that do not exist yet: the D3 recruiter sliver is unbuilt, so no '
|
|
311
|
+
+ 'guard here could hold that claim, and the copy rule is that a toggle promises exactly what is enforced');
|
|
312
|
+
assert.match(html, /profile page setting applies now[\s\S]*Recruiter discoverability applies now too/,
|
|
313
|
+
'the section note now says both are live, rather than splitting them');
|
|
314
|
+
assert.doesNotMatch(html, /starts applying as the recruiting surfaces land/,
|
|
315
|
+
'the "recorded and waiting" note must not outlive the surface that started reading it');
|
|
306
316
|
});
|
|
@@ -77,6 +77,8 @@ import {
|
|
|
77
77
|
rollupVisibleSql,
|
|
78
78
|
isRollupVisible,
|
|
79
79
|
isRecruiterDiscoverable,
|
|
80
|
+
recruiterReachSql,
|
|
81
|
+
scoutingListableSql,
|
|
80
82
|
} from '../modules/platform-identity/account-visibility.js';
|
|
81
83
|
|
|
82
84
|
const require = createRequire(import.meta.url);
|
|
@@ -244,7 +246,14 @@ function blankComments(src) {
|
|
|
244
246
|
const SPELLINGS = [
|
|
245
247
|
[/\bprofile_state\s*(?:=|==|===|!=|!==|<>)\s*['"]/g, "profile_state compared to a literal"],
|
|
246
248
|
[/\baccount_visibility\s*(?:=|==|===|!=|!==|<>)\s*['"]/g, 'account_visibility compared to a literal'],
|
|
247
|
-
|
|
249
|
+
// The reach COALESCE, in every form a real query writes it. The unqualified
|
|
250
|
+
// spelling alone was a hole: the ONE production copy of this predicate is
|
|
251
|
+
// written `COALESCE(${q}recruiter_discoverable, …)` and emits
|
|
252
|
+
// `COALESCE(pa.recruiter_discoverable, …)`, and NEITHER matched — so the rule
|
|
253
|
+
// that is supposed to make this file the only home for the reach answer was
|
|
254
|
+
// catching only the spelling nobody uses. A drift copy would most likely be
|
|
255
|
+
// aliased too (task 1002290, found in this task's adversarial review).
|
|
256
|
+
[/\bCOALESCE\s*\(\s*(?:[A-Za-z_$][\w$]*|\$\{[^}]*\})?\s*\.?\s*recruiter_discoverable/gi, 'the recruiter-reach COALESCE'],
|
|
248
257
|
[/\bNOT\s+(?:[A-Za-z_][\w]*\.)?hide_stats\b/g, 'a SQL NOT hide_stats'],
|
|
249
258
|
[/\bhide_stats\s*(?:=|==|===)\s*(?:true|false)\b/g, 'hide_stats compared to a boolean'],
|
|
250
259
|
];
|
|
@@ -318,8 +327,14 @@ function accountStatements(code) {
|
|
|
318
327
|
return out;
|
|
319
328
|
}
|
|
320
329
|
|
|
321
|
-
/**
|
|
322
|
-
|
|
330
|
+
/**
|
|
331
|
+
* Does this function body route through one of the shared predicates?
|
|
332
|
+
* scoutingListableSql (task 1002290) is COMPOSED from rollupVisibleSql and
|
|
333
|
+
* recruiterReachSql inside the owner file, so a reader that joins it has adopted
|
|
334
|
+
* the shared rule — but it must be named here, or wiring the recruiting reach in
|
|
335
|
+
* would read as a reader that stopped adopting anything.
|
|
336
|
+
*/
|
|
337
|
+
const ADOPTS = /\b(?:accountVisibleSql|accountActiveSql|rollupVisibleSql|scoutingListableSql|isAccountVisible|isAccountActive|isRollupVisible)\s*\(/;
|
|
323
338
|
|
|
324
339
|
// ===========================================================================
|
|
325
340
|
// Self-tests — the machinery is the guard, so it gets proved too.
|
|
@@ -342,6 +357,17 @@ t('SPELLINGS catch a hand-written variant in either language, and leave writes a
|
|
|
342
357
|
assert.ok(caught("AND account_visibility = 'public'"), 'the existence axis in SQL');
|
|
343
358
|
assert.ok(caught("acct.account_visibility === 'private'"), 'the existence axis in JS');
|
|
344
359
|
assert.ok(caught('COALESCE(recruiter_discoverable, account_visibility)'), 'the recruiter COALESCE');
|
|
360
|
+
// The forms a REAL query carries — an alias, a template variable, and the two
|
|
361
|
+
// together. Testing only the bare spelling is how this rule came to match
|
|
362
|
+
// nothing that ships: the ONE production copy emits `COALESCE(pa.recruiter_-
|
|
363
|
+
// discoverable, …)` from source that reads `COALESCE(${q}recruiter_-
|
|
364
|
+
// discoverable, …)`, and the rule matched NEITHER (task 1002290, found in this
|
|
365
|
+
// task's adversarial review — and then again one form later, which is why all
|
|
366
|
+
// four are pinned rather than the one that failed first).
|
|
367
|
+
assert.ok(caught('COALESCE(pa.recruiter_discoverable, TRUE)'), 'the reach COALESCE, aliased');
|
|
368
|
+
assert.ok(caught('COALESCE(${q}recruiter_discoverable, x)'), 'the reach COALESCE, as the builder writes it');
|
|
369
|
+
assert.ok(caught('COALESCE(${alias}.recruiter_discoverable, TRUE)'), 'a template alias AND a dot — the drift copy most likely to be written');
|
|
370
|
+
assert.ok(!caught('COALESCE(hide_stats, false)'), 'and it is the REACH column it watches, not any COALESCE');
|
|
345
371
|
assert.ok(caught('AND NOT pa.hide_stats'), 'the hide half of a rollup predicate');
|
|
346
372
|
// A WRITE names the column but compares it to nothing — it decides no visibility.
|
|
347
373
|
assert.ok(!caught("profile_state = CASE WHEN $5::boolean THEN 'active' ELSE x END"), 'an upsert is not a predicate');
|
|
@@ -390,10 +416,24 @@ t('rollupVisibleSql is COMPOSED from accountVisibleSql, never restated', () => {
|
|
|
390
416
|
assert.match(rollupVisibleSql('pa'), /AND NOT pa\.hide_stats/, 'and add exactly the hide term');
|
|
391
417
|
});
|
|
392
418
|
|
|
393
|
-
t('
|
|
419
|
+
t('scoutingListableSql is COMPOSED from the rollup AND the reach halves (task 1002290)', () => {
|
|
420
|
+
// The recruiting directory asks BOTH questions; a leaderboard asks only the
|
|
421
|
+
// first. Composition is what keeps that distinction from becoming two rules.
|
|
422
|
+
assert.ok(scoutingListableSql('pa').includes(rollupVisibleSql('pa')),
|
|
423
|
+
'the directory predicate must literally contain the rollup predicate');
|
|
424
|
+
assert.ok(scoutingListableSql('pa').includes(recruiterReachSql('pa')),
|
|
425
|
+
'and the reach predicate — wiring reach in is the whole of task 1002290');
|
|
426
|
+
assert.ok(!rollupVisibleSql('pa').includes('recruiter_discoverable'),
|
|
427
|
+
'and reach must NOT leak back down into the rollup rule: the community spec D3 '
|
|
428
|
+
+ 'leaderboards join rollupVisibleSql and the opt-out must not empty them');
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
t('the alias is the one interpolated thing, and every builder refuses a non-identifier', () => {
|
|
394
432
|
const inject = "pa; DROP TABLE platform_identity_accounts --";
|
|
395
433
|
assert.throws(() => accountVisibleSql(inject), /bare SQL identifier/);
|
|
396
434
|
assert.throws(() => rollupVisibleSql(inject), /bare SQL identifier/);
|
|
435
|
+
assert.throws(() => recruiterReachSql(inject), /bare SQL identifier/);
|
|
436
|
+
assert.throws(() => scoutingListableSql(inject), /bare SQL identifier/);
|
|
397
437
|
});
|
|
398
438
|
|
|
399
439
|
t('the JS ports answer exactly what the SQL asks, row for row', () => {
|
|
@@ -655,6 +695,11 @@ function accountFilter(where, params) {
|
|
|
655
695
|
if (/\bprofile_state = 'active'/.test(where)) tests.push((r) => r.profile_state === 'active');
|
|
656
696
|
if (/\baccount_visibility = 'public'/.test(where)) tests.push((r) => r.account_visibility === 'public');
|
|
657
697
|
if (/\bNOT (?:[a-z_]+\.)?hide_stats\b/.test(where)) tests.push((r) => !r.hide_stats);
|
|
698
|
+
// The recruiting-reach COALESCE (task 1002290). Modelled through the shared JS
|
|
699
|
+
// twin, not re-implemented — an interpreter that read this term its own way
|
|
700
|
+
// could agree with a WRONG query. Unmodelled it would degrade to no filter,
|
|
701
|
+
// which is precisely what this interpreter exists to refuse.
|
|
702
|
+
if (/COALESCE\((?:[a-z_]+\.)?recruiter_discoverable,/.test(where)) tests.push(isRecruiterDiscoverable);
|
|
658
703
|
|
|
659
704
|
const byId = /\b(?:[a-z_]+\.)?github_id = \$(\d+)/.exec(where);
|
|
660
705
|
if (byId) { consumed.add(byId[1]); const v = params[Number(byId[1]) - 1]; tests.push((r) => String(r.github_id) === String(v)); }
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -1676,3 +1676,59 @@ test('one column grammar: the table lays every row on one set of fixed tracks (e
|
|
|
1676
1676
|
assert.match(COSMOS, /@media \(max-width:780px\)\{\.tbl \.colTags,\.tbl \.colType\{display:none;\}\.tbl\{min-width:600px;\}\}/, 'a phone scrolls the table inside its wrap');
|
|
1677
1677
|
assert.match(COSMOS, /#sub-table-body\{overflow-x:auto;\}/, 'the wrap scrolls, never the page');
|
|
1678
1678
|
});
|
|
1679
|
+
|
|
1680
|
+
/* ── the create wizard in the world (task 1003346; direction record §15, the pick: v1 the
|
|
1681
|
+
rail above, the bongo pair kept). Two sheets carry it: cosmos.css draws the FRAME by scope
|
|
1682
|
+
(#view-new), projects.html's own wizard block draws the wizard's structures. ── */
|
|
1683
|
+
test('the wizard takes the world by scope: no card ground, the six-step strip as the rail, the doors as pills (task 1003346)', () => {
|
|
1684
|
+
const at = COSMOS.indexOf('THE CREATE WIZARD, IN THE WORLD');
|
|
1685
|
+
assert.ok(at > -1, 'the #view-new block exists in cosmos.css');
|
|
1686
|
+
const block = COSMOS.slice(at);
|
|
1687
|
+
assert.match(block, /#view-new \.card\{background:none;border-radius:0;box-shadow:none;/, 'no card ground');
|
|
1688
|
+
assert.match(block, /#view-new \.steps\{display:grid;grid-template-columns:repeat\(6,1fr\);/, 'the strip lays its six steps on six tracks');
|
|
1689
|
+
assert.match(block, /#view-new \.steps li::before\{content:"";position:absolute;top:-4px;left:0;right:auto;width:7px;height:7px;/, 'the connector becomes the 7px dot');
|
|
1690
|
+
assert.match(block, /#view-new \.steps li\.now::before\{background:var\(--accent\);border-color:var\(--accent\);outline:4px solid/, 'the lit step carries the accent and an outline halo');
|
|
1691
|
+
assert.match(block, /#view-new \.panel h1\{font-weight:300;[^}]*text-transform:lowercase;/, 'the step title in the display voice, lowercase');
|
|
1692
|
+
assert.match(block, /#view-new \.btn\{[^}]*text-transform:lowercase;[^}]*box-shadow:none;/, 'the doors are pills without a shadow');
|
|
1693
|
+
assert.match(block, /#view-new \.btn\.ghost\{[^}]*border:1px solid var\(--rule\);/, 'back and skip are hairline pills');
|
|
1694
|
+
assert.match(block, /#view-new #wizSignin\{background:var\(--ink\);color:var\(--bg\);/, 'the GitHub sign-in is the inverted pill');
|
|
1695
|
+
assert.match(block, /#view-new \.bongos \.glow\{display:none;\}/, 'the bongo pair stays (the owner\'s one mark on every surface), its halo goes');
|
|
1696
|
+
assert.match(block, /#view-new \.panel \.lede a\{display:inline-block;padding:3px 0;min-height:24px;/, 'the done clause\'s link clears the 24px floor (the sheet\'s first problem)');
|
|
1697
|
+
assert.doesNotMatch(block, /box-shadow:(?!none)/, 'halos are outlines in the world, never box-shadow');
|
|
1698
|
+
assert.doesNotMatch(block, /#[0-9a-f]{3,8}\b/i, 'every colour is a token');
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
test('the wizard\'s own structures wear the world: hairline fields, cells of one band, the review as a ledger, the human steps as a numbered ledger, the standup strip as the rail (task 1003346)', () => {
|
|
1702
|
+
const at = STYLE.indexOf('the create wizard (task 1002763');
|
|
1703
|
+
assert.ok(at > -1, 'the wizard block is still the page\'s own');
|
|
1704
|
+
const block = STYLE.slice(at);
|
|
1705
|
+
assert.match(block, /#view-new input\[type=text\], #view-new textarea\{\s*width:100%;[^}]*border:0;border-bottom:1px solid var\(--rule\);border-radius:0;/, 'a field is one bottom hairline');
|
|
1706
|
+
assert.match(block, /#view-new input\[type=text\]:focus, #view-new textarea:focus\{border-bottom-color:var\(--ink\);box-shadow:none;\}/, 'focus takes ink, never a glow');
|
|
1707
|
+
assert.match(block, /\.typeCards\{display:grid;[^}]*border-top:1px solid var\(--rule\);border-bottom:1px solid var\(--rule\);/, 'the type and team choices are cells of one band');
|
|
1708
|
+
assert.match(block, /\.tpill\[aria-pressed=true\]::before\{background:var\(--accent\);/, 'the chosen cell carries the dot');
|
|
1709
|
+
assert.match(block, /\.choices\{display:grid;grid-template-columns:1fr 1fr;gap:0;border-top:1px solid var\(--rule\);/, 'start fresh / adopt are two cells of one band');
|
|
1710
|
+
assert.match(block, /\.choice svg\{display:none;\}/, 'the dot is the mark, the icon leaves');
|
|
1711
|
+
assert.match(block, /\.review\{list-style:none;display:grid;grid-template-columns:172px 1fr;/, 'the review is a ledger');
|
|
1712
|
+
assert.match(block, /\.review li\{display:contents;\}/, 'renderReview\'s li markup stands; the grid lays its k and v');
|
|
1713
|
+
assert.match(block, /\.humanSteps>li\{\s*display:grid;grid-template-columns:44px 1fr;/, 'the human steps are a numbered ledger');
|
|
1714
|
+
assert.match(block, /\.humanSteps>li>\*\{grid-column:2;/, 'every child of a step sits in the text column, never under the counter (the sheet\'s bug)');
|
|
1715
|
+
assert.match(block, /\.humanSteps>li\.sdone::before\{content:"done";/, 'a finished step reads DONE in the ok ink');
|
|
1716
|
+
assert.match(block, /\.prog\{background:none;border:0;border-radius:0;padding:0;/, 'the standup strip is drawn as the rail');
|
|
1717
|
+
assert.match(block, /\.prog \.stages li\.now \.sdot::after\{content:none;animation:none;\}/, 'no pulse on the rail');
|
|
1718
|
+
assert.match(block, /\.doneHead \.tick\{display:none;\}/, 'the tick leaves; the rail says filed');
|
|
1719
|
+
assert.match(block, /\.addrBig code\{display:block;font-family:var\(--font-display\);/, 'the address stands in the display voice');
|
|
1720
|
+
assert.doesNotMatch(block, /linear-gradient/, 'no tinted plates');
|
|
1721
|
+
assert.doesNotMatch(block, /box-shadow:(?!none)/, 'no box shadow in the wizard block');
|
|
1722
|
+
assert.match(HUB, /<div class="nav">\s*<button type="button" class="btn ghost" data-back="5">← Back<\/button>\s*<button type="button" class="btn big" id="doCreate">Create my project<\/button>/, 'the review\'s doors sit on one row, back then the primary');
|
|
1723
|
+
assert.match(HUB, /<h2 class="stepsHead">Your steps — the parts only you can play<\/h2>/, 'the steps heading is the label face by class, not an inline style');
|
|
1724
|
+
// what is enqueued is unchanged (ADR 0111): the filing body the control plane reads
|
|
1725
|
+
assert.match(SCRIPT, /body: JSON\.stringify\(\{\s*slug: state\.slug,/, 'the filing still carries the slug first');
|
|
1726
|
+
assert.match(SCRIPT, /hosting_shape: 'standalone',/, 'the shape is unchanged');
|
|
1727
|
+
assert.match(SCRIPT, /target_ref: state\.owner \+ '\/' \+ state\.repo,/, 'the repo ref is unchanged');
|
|
1728
|
+
assert.match(SCRIPT, /onboard_mode: state\.mode === 'adopt' \? 'adopt' : 'greenfield'/, 'the onboard mode is unchanged');
|
|
1729
|
+
// the kit walks every step and files against the stub
|
|
1730
|
+
const states = JSON.parse(fs.readFileSync(path.join(ROOT, 'public-landing', 'projects.states.json'), 'utf8')).states;
|
|
1731
|
+
for (const s of ['type', 'about', 'home', 'address', 'review', 'done']) assert.ok(states['in-new-' + s], `the ${s} step has a kit state`);
|
|
1732
|
+
const probes = JSON.parse(fs.readFileSync(path.join(ROOT, 'public-landing', 'projects.probes.json'), 'utf8')).probes;
|
|
1733
|
+
assert.ok(probes.some((p) => /the wizard driver \(task 1003346\)/.test(p.name) && p.steps.some((st) => st[1] === '#doCreate')), 'the wizard driver files a project end to end');
|
|
1734
|
+
});
|