@bongos/core 1.19.331 → 1.19.333

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.
Files changed (61) hide show
  1. package/.bongos-core.json +163 -43
  2. package/docs/api/openapi.json +2 -2
  3. package/docs/api-reference.md +1 -1
  4. package/docs/copy-inventory.md +202 -202
  5. package/docs/copy-registry.json +384 -384
  6. package/modules/grading/grader-config.js +98 -0
  7. package/modules/grading/grader-model.js +150 -0
  8. package/modules/grading/grader-prompt.js +139 -0
  9. package/modules/grading/grader-score.js +706 -0
  10. package/modules/grading/grader-subagent.js +328 -0
  11. package/modules/grading/grader.js +5 -1287
  12. package/modules/hall-ui/public/builders.js +19 -1182
  13. package/modules/hall-ui/public/goals-page.js +11 -997
  14. package/modules/hall-ui/public/goals-render.js +1032 -0
  15. package/modules/hall-ui/public/goals.html +4 -0
  16. package/modules/hall-ui/public/hall-render.js +1211 -0
  17. package/modules/hall-ui/public/index.html +6 -0
  18. package/modules/lifecycle/routes/goal-route-authz.js +400 -0
  19. package/modules/lifecycle/routes/goals.js +1 -367
  20. package/modules/lifecycle/routes/task-route-helpers.js +219 -0
  21. package/modules/lifecycle/routes/task-write-routes.js +542 -0
  22. package/modules/lifecycle/routes/tasks.js +3 -696
  23. package/package-lock.json +2 -2
  24. package/package.json +1 -1
  25. package/public-landing/assets/cosmos.css +4 -0
  26. package/public-landing/projects.html +103 -23
  27. package/scripts/gds/box-infra.js +280 -0
  28. package/scripts/gds/box.js +1 -245
  29. package/scripts/gds/fitness-checks-identity.js +320 -0
  30. package/scripts/gds/fitness-checks-vocabulary.js +230 -0
  31. package/scripts/gds/fitness-checks-write-validation.js +144 -0
  32. package/scripts/gds/fitness-lib.js +82 -0
  33. package/scripts/gds/fitness.js +55 -668
  34. package/scripts/gds/provision-config.js +204 -0
  35. package/scripts/gds/provision-net.js +221 -0
  36. package/scripts/gds/provision-repo.js +849 -0
  37. package/scripts/gds/provision-units.js +262 -0
  38. package/scripts/gds/provision.js +4 -1395
  39. package/src/bongos/auth-admission.js +264 -0
  40. package/src/bongos/auth-config.js +151 -0
  41. package/src/bongos/auth-discord.js +198 -0
  42. package/src/bongos/auth-github.js +336 -0
  43. package/src/bongos/auth-idp.js +330 -0
  44. package/src/bongos/auth.js +5 -1100
  45. package/src/module-api.js +5 -1
  46. package/tests/auth_admission_gate.mjs +3 -1
  47. package/tests/est_advisory.mjs +3 -1
  48. package/tests/goal_advisory.mjs +3 -2
  49. package/tests/goal_criterion_confirm_ui.mjs +6 -0
  50. package/tests/goal_subtitle.mjs +5 -2
  51. package/tests/goals_card_view.mjs +10 -3
  52. package/tests/grader_generated_provenance.mjs +3 -1
  53. package/tests/hall_palette.mjs +3 -1
  54. package/tests/helpers.mjs +58 -25
  55. package/tests/lib_sh_resolution.mjs +3 -1
  56. package/tests/membership_kind_lockstep.mjs +3 -1
  57. package/tests/migration_allocation.mjs +5 -2
  58. package/tests/patch_gate_ordering.mjs +3 -1
  59. package/tests/projects_hub_pre_uat.mjs +53 -0
  60. package/tests/rank_tier_single_source.mjs +16 -3
  61. package/tests/reward_gate.mjs +3 -2
package/src/module-api.js CHANGED
@@ -1089,8 +1089,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1089
1089
  // landed since 1.19.329 with no explicit bump. run 32789596635. (task 1002620)
1090
1090
  // 1.19.331 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1091
1091
  // landed since 1.19.330 with no explicit bump. run 32791877167. (task 1002620)
1092
+ // 1.19.332 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1093
+ // landed since 1.19.331 with no explicit bump. run 32793033453. (task 1002620)
1094
+ // 1.19.333 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1095
+ // landed since 1.19.332 with no explicit bump. run 32793666555. (task 1002620)
1092
1096
  // ---------------------------------------------------------------------------
1093
- const CORE_VERSION = '1.19.331'; // CI auto-patch carrier (ADR 0161); see changelog above
1097
+ const CORE_VERSION = '1.19.333'; // CI auto-patch carrier (ADR 0161); see changelog above
1094
1098
 
1095
1099
  // A namespaced logger so a module's log lines are attributable + consistent.
1096
1100
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -13,6 +13,7 @@
13
13
  import { strict as assert } from 'node:assert';
14
14
  import { createRequire } from 'node:module';
15
15
  import { readFileSync } from 'node:fs';
16
+ import { authSource } from './helpers.mjs';
16
17
 
17
18
  const require = createRequire(import.meta.url);
18
19
  const auth = require('../src/bongos/auth.js');
@@ -150,7 +151,8 @@ t('no login passed (legacy callers) behaves exactly as before — BLOCKED', () =
150
151
  console.log('\nfirstTimeAdmissionBlocked — every call-site passes `login` (SR-19 non-drift, #1970):');
151
152
 
152
153
  t('completeWebFlow + pollDeviceFlow both pass `login` to the gate', () => {
153
- const src = readFileSync(new URL('../src/bongos/auth.js', import.meta.url), 'utf8');
154
+ // task 1003206: the auth flows are auth.js + auth-*.js now — read the family.
155
+ const src = authSource();
154
156
  const re = /firstTimeAdmissionBlocked\s*\(\s*\{[^}]*\}/g;
155
157
  const callSites = [];
156
158
  for (let m = re.exec(src); m; m = re.exec(src)) {
@@ -30,6 +30,7 @@
30
30
  import { strict as assert } from 'node:assert';
31
31
  import fs from 'node:fs';
32
32
  import { createRequire } from 'node:module';
33
+ import { taskRoutesSource } from './helpers.mjs';
33
34
 
34
35
  const require = createRequire(import.meta.url);
35
36
  const { adviseEstimate, pickRatio, buildEstAdvisory, MIN_MATERIAL_DELTA, SCALE_OK_RATIO } =
@@ -171,7 +172,8 @@ await t('the route attaches est_advisory to the 201 body, non-blocking', () => {
171
172
  // literal because modules/lifecycle/routes/tasks.js is over the size ratchet's
172
173
  // cap and main sits exactly AT the baseline — so this feature had to land at
173
174
  // net-zero lines in that file. Asserting the shape keeps the fold honest.
174
- const src = fs.readFileSync(new URL('../modules/lifecycle/routes/tasks.js', import.meta.url), 'utf8');
175
+ // task 1003206: the task router is tasks.js + task-*.js now — read the family.
176
+ const src = taskRoutesSource();
175
177
  const body201 = (src.match(/res\.status\(201\)\.json\(\{[\s\S]*?\}\);/) || [''])[0];
176
178
  assert.ok(body201, 'found the 201 response body');
177
179
  assert.match(body201, /est_advisory:/, 'est_advisory rides the create response');
@@ -17,14 +17,15 @@ import { strict as assert } from 'node:assert';
17
17
  import { createRequire } from 'node:module';
18
18
  import { readFileSync } from 'node:fs';
19
19
  import http from 'node:http';
20
- import { makeRunner, lifecycleDbSource } from './helpers.mjs';
20
+ import { makeRunner, lifecycleDbSource, taskRoutesSource } from './helpers.mjs';
21
21
 
22
22
  process.env.NODE_ENV = 'test';
23
23
 
24
24
  const require = createRequire(import.meta.url);
25
25
  const { adviseGoal, candidateGoals, generalGoalTitle, buildGoalAdvisory } = require('../modules/lifecycle/goal-advisory.js');
26
26
 
27
- const tasksSrc = readFileSync(new URL('../modules/lifecycle/routes/tasks.js', import.meta.url), 'utf8');
27
+ // task 1003206: the task router is tasks.js + task-*.js now — read the family.
28
+ const tasksSrc = taskRoutesSource();
28
29
  // task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
29
30
  const dbSrc = lifecycleDbSource();
30
31
  const { test, summary } = makeRunner();
@@ -40,6 +40,11 @@ const TASK_LIB_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/tas
40
40
  // task 1003135: the page's pure presentational helpers moved to goals-lib.js
41
41
  // (window.OTBGoalsLib) — run the REAL file too, same reasoning as task-lib above.
42
42
  const GOALS_LIB_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals-lib.js'), 'utf8');
43
+ // task 1003206: the page's view painters moved to goals-render.js
44
+ // (window.OTBGoalsRender) — the second half of the same carve. Run the REAL file
45
+ // too, for the same reason: this is the only test that executes either board, so
46
+ // stubbing the renderers would let them rot unwatched.
47
+ const GOALS_RENDER_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals-render.js'), 'utf8');
43
48
 
44
49
  const { test, summary } = makeRunner();
45
50
 
@@ -136,6 +141,7 @@ async function render({ rank = 'metic', goalStatus = 'open', criteria = Object.v
136
141
  vm.createContext(sandbox);
137
142
  vm.runInContext(TASK_LIB_SRC, sandbox, { filename: 'task-lib.js' });
138
143
  vm.runInContext(GOALS_LIB_SRC, sandbox, { filename: 'goals-lib.js' });
144
+ vm.runInContext(GOALS_RENDER_SRC, sandbox, { filename: 'goals-render.js' });
139
145
  vm.runInContext(SRC, sandbox, { filename: 'goals-page.js' });
140
146
 
141
147
  // Let boot()'s promise chain settle (me → goals → details → route → render).
@@ -18,6 +18,7 @@ import { createRequire } from 'node:module';
18
18
  import fs from 'node:fs';
19
19
  import path from 'node:path';
20
20
  import { fileURLToPath } from 'node:url';
21
+ import { goalsPageSource } from './helpers.mjs';
21
22
 
22
23
  const require = createRequire(import.meta.url);
23
24
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -179,9 +180,11 @@ test('an unset subtitle is hidden; a set one is shown trimmed', () => {
179
180
  });
180
181
 
181
182
  test('on a LIST the subtitle is a tooltip, never a second visible line', () => {
182
- const js = read('modules', 'hall-ui', 'public', 'goals-page.js');
183
+ // task 1003206: the row/card painters moved to goals-render.js — the second half
184
+ // of the carve goals-lib.js began — so read the family, not one filename.
185
+ const js = goalsPageSource();
183
186
  // The helper lives in goals-lib.js — the file that exists because
184
- // goals-page.js is over the excess-lines ratchet (task 1003135).
187
+ // goals-page.js was over the excess-lines ratchet (task 1003135).
185
188
  const lib = read('modules', 'hall-ui', 'public', 'goals-lib.js');
186
189
  assert.match(lib, /function subtitleAttr/);
187
190
  assert.match(lib, /escapeHtml\(t\)/, 'escaped, since it lands in an attribute');
@@ -12,10 +12,11 @@ import { strict as assert } from 'node:assert';
12
12
  import fs from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
- import { makeRunner } from './helpers.mjs';
15
+ import { makeRunner, goalsPageSource } from './helpers.mjs';
16
16
 
17
17
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
- const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals-page.js'), 'utf8');
18
+ // task 1003206: the view painters moved to goals-render.js — read the family.
19
+ const SRC = goalsPageSource();
19
20
  const CSS = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals.css'), 'utf8');
20
21
  const { test, summary } = makeRunner();
21
22
 
@@ -28,7 +29,13 @@ await test('both views render from ONE fact source (goalRenderFacts) so they can
28
29
  });
29
30
 
30
31
  await test('renderList forks on the f-goal-view toggle AFTER grouping/filter/sort (both views share them)', () => {
31
- const body = SRC.slice(SRC.indexOf('function renderList'), SRC.indexOf('drag-to-reorder: RETIRED'));
32
+ // task 1003206: renderList moved to goals-render.js while the 'drag-to-reorder:
33
+ // RETIRED' marker that used to bound it stayed in goals-page.js, so slicing
34
+ // between the two now spans two files (or inverts). Bound the body by the next
35
+ // top-level function instead — that IS what 'renderList's body' means.
36
+ const start = SRC.indexOf('function renderList');
37
+ const nextFn = SRC.indexOf('\n function ', start + 1);
38
+ const body = SRC.slice(start, nextFn > start ? nextFn : undefined);
32
39
  assert.match(body, /#f-goal-view/, 'the fork reads the toggle');
33
40
  const sortIdx = body.indexOf('sortGoals(');
34
41
  const forkIdx = body.indexOf('#f-goal-view');
@@ -36,6 +36,7 @@ import { createRequire } from 'node:module';
36
36
  import fs from 'node:fs';
37
37
  import path from 'node:path';
38
38
  import { fileURLToPath } from 'node:url';
39
+ import { fitnessSource } from './helpers.mjs';
39
40
 
40
41
  const require = createRequire(import.meta.url);
41
42
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -44,7 +45,8 @@ const prov = require(path.join(ROOT, 'modules/grading/generated-provenance.js'))
44
45
  // ---- 1. lockstep with the fitness gate ----
45
46
 
46
47
  test('the generator roster matches fitness.js checkGeneratedArtifactsFresh exactly', () => {
47
- const src = fs.readFileSync(path.join(ROOT, 'scripts/gds/fitness.js'), 'utf8');
48
+ // task 1003206: the gate is fitness.js + fitness-*.js now — read the family.
49
+ const src = fitnessSource();
48
50
  const fn = src.slice(src.indexOf('function checkGeneratedArtifactsFresh'));
49
51
  const body = fn.slice(0, fn.indexOf('\nfunction ', 1));
50
52
 
@@ -18,6 +18,7 @@ import fs from 'node:fs';
18
18
  import path from 'node:path';
19
19
  import vm from 'node:vm';
20
20
  import { fileURLToPath } from 'node:url';
21
+ import { hallSource } from './helpers.mjs';
21
22
 
22
23
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
23
24
  const HALL = path.join(ROOT, 'modules/hall-ui/public');
@@ -90,7 +91,8 @@ function declaredHrefs(prefixTest) {
90
91
  }
91
92
 
92
93
  test('Standing section anchors exist in builders.js and are routable in shell.js', () => {
93
- const groups = [...read('builders.js').matchAll(/\{ key: '([a-z]+)', label: '[^']+' \}/g)].map((m) => m[1]);
94
+ // task 1003206: STANDING_GROUPS moved to hall-render.js — read the family.
95
+ const groups = [...hallSource().matchAll(/\{ key: '([a-z]+)', label: '[^']+' \}/g)].map((m) => m[1]);
94
96
  assert.ok(groups.length >= 4, `sanity: found STANDING_GROUPS (${groups})`);
95
97
  const declared = declaredHrefs((h) => h.startsWith('/#standing-')).map((h) => h.replace('/#', ''));
96
98
  assert.ok(declared.length, 'palette declares Standing sections');
package/tests/helpers.mjs CHANGED
@@ -124,34 +124,67 @@ export function requireHostModule(require, spec, note) {
124
124
  }
125
125
  }
126
126
 
127
- // lifecycleDbSource — the LIFECYCLE data layer as one source string.
127
+ // ── carved-family source readers (tasks 1003204–1003206, goal 1000079) ────────
128
128
  //
129
- // task 1003204 (goal 1000079) split the 5,748-line modules/lifecycle/db.js into
130
- // db.js (a pure re-export facade) + twelve cohesive db-*.js units. A pile of
131
- // tests assert things about "the data layer" by regexing its source — that the
132
- // claim paths gate on requires_rank, that a literal appears in exactly one
133
- // place, that dependency CRUD points at the right table. Those assertions were
134
- // always about the LAYER, not about one filename, so they read the whole family
135
- // here. Reading the family (rather than repointing each test at whichever db-*
136
- // unit happens to hold the code today) also means a future re-carve moves code
137
- // between units without silently disarming the assertion.
129
+ // The de-monolith work split four big files into a facade plus siblings:
130
+ // modules/lifecycle/db.js, scripts/gds/ship.js, scripts/gds/fitness.js and
131
+ // modules/lifecycle/routes/tasks.js. A pile of tests assert things about one of
132
+ // those SUBJECTS by regexing its source — that every claim path gates on
133
+ // requires_rank, that a literal appears in exactly one place, that a gate
134
+ // precedes the first write. Those assertions were always about the subject, not
135
+ // about a filename, so they read the whole family.
136
+ //
137
+ // Reading the family (rather than repointing each test at whichever sibling
138
+ // happens to hold the code today) also means a future re-carve can move code
139
+ // between siblings without silently disarming the assertion.
140
+ function familySource(dir, matches) {
141
+ return fs.readdirSync(dir).filter(matches).sort()
142
+ .map((f) => fs.readFileSync(path.join(dir, f), 'utf8')).join('\n');
143
+ }
144
+
145
+ // The LIFECYCLE data layer: db.js (a re-export facade) + the twelve db-*.js units.
138
146
  export function lifecycleDbSource(modulesDir = path.join(ROOT, 'modules')) {
139
- const dir = path.join(modulesDir, 'lifecycle');
140
- const files = fs.readdirSync(dir)
141
- .filter((f) => f === 'db.js' || (f.startsWith('db-') && f.endsWith('.js')))
142
- .sort();
143
- return files.map((f) => fs.readFileSync(path.join(dir, f), 'utf8')).join('\n');
147
+ return familySource(path.join(modulesDir, 'lifecycle'),
148
+ (f) => f === 'db.js' || (f.startsWith('db-') && f.endsWith('.js')));
144
149
  }
145
150
 
146
- // shipCliSource — the ship CLI as one source string.
147
- //
148
- // The companion to lifecycleDbSource above, for the same reason: task 1003205
149
- // carved the 4,613-line scripts/gds/ship.js into ship.js (a re-export facade +
150
- // the CLI entry point) plus ship-*.js phase modules. Tests that assert "the ship
151
- // CLI does X" by regexing its source mean the CLI as a whole, not one file.
151
+ // The SHIP CLI: ship.js (facade + entry point) + the ship-*.js phase modules.
152
152
  export function shipCliSource(scriptsDir = path.join(ROOT, 'scripts', 'gds')) {
153
- const files = fs.readdirSync(scriptsDir)
154
- .filter((f) => f === 'ship.js' || (f.startsWith('ship-') && f.endsWith('.js')))
155
- .sort();
156
- return files.map((f) => fs.readFileSync(path.join(scriptsDir, f), 'utf8')).join('\n');
153
+ return familySource(scriptsDir,
154
+ (f) => f === 'ship.js' || (f.startsWith('ship-') && f.endsWith('.js')));
155
+ }
156
+
157
+ // The architecture-fitness GATE: fitness.js (registry + runner), fitness-lib.js
158
+ // and the fitness-checks-*.js clusters. fitness-ratchets.js is deliberately in —
159
+ // it has always been a registered check module of the same gate.
160
+ export function fitnessSource(scriptsDir = path.join(ROOT, 'scripts', 'gds')) {
161
+ return familySource(scriptsDir,
162
+ (f) => f === 'fitness.js' || (f.startsWith('fitness-') && f.endsWith('.js')));
163
+ }
164
+
165
+ // The BUILDERS' HALL page script: builders.js (state, data loading, wiring) plus
166
+ // hall-render.js, the view painters task 1003206 carved out of it.
167
+ export function hallSource(pubDir = path.join(ROOT, 'modules', 'hall-ui', 'public')) {
168
+ return familySource(pubDir, (f) => f === 'builders.js' || f === 'hall-render.js');
169
+ }
170
+
171
+ // The GOALS page script: goals-page.js plus its two carved siblings — goals-lib.js
172
+ // (pure helpers, task 1003135) and goals-render.js (view painters, task 1003206).
173
+ export function goalsPageSource(pubDir = path.join(ROOT, 'modules', 'hall-ui', 'public')) {
174
+ return familySource(pubDir, (f) => ['goals-page.js', 'goals-lib.js', 'goals-render.js'].includes(f));
175
+ }
176
+
177
+ // The KERNEL IDENTITY surface: auth.js (session resolution, the middleware and the
178
+ // page gates) plus the auth-*.js flow modules (config, admission, GitHub, IdP,
179
+ // Discord).
180
+ export function authSource(srcDir = path.join(ROOT, 'src', 'bongos')) {
181
+ return familySource(srcDir,
182
+ (f) => f === 'auth.js' || (f.startsWith('auth-') && f.endsWith('.js')));
183
+ }
184
+
185
+ // The TASK ROUTER: routes/tasks.js plus the task-*.js route modules it registers
186
+ // (task-write-routes.js holds POST /tasks and PATCH /tasks/:id).
187
+ export function taskRoutesSource(modulesDir = path.join(ROOT, 'modules')) {
188
+ return familySource(path.join(modulesDir, 'lifecycle', 'routes'),
189
+ (f) => f === 'tasks.js' || (f.startsWith('task-') && f.endsWith('.js')));
157
190
  }
@@ -28,6 +28,7 @@ import { spawnSync } from 'node:child_process';
28
28
  import fs from 'node:fs';
29
29
  import path from 'node:path';
30
30
  import { fileURLToPath } from 'node:url';
31
+ import { fitnessSource } from './helpers.mjs';
31
32
 
32
33
  const require = createRequire(import.meta.url);
33
34
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -122,7 +123,8 @@ test('gds_api_base falls back to the instance, never a foreign host', (t) => {
122
123
  // ---- 3. the guard can now SEE a .sh re-hardcode ----------------------------
123
124
 
124
125
  test('the host-identity fitness scan covers .sh', () => {
125
- const src = fs.readFileSync(path.join(GDS, 'fitness.js'), 'utf8');
126
+ // task 1003206: the gate is fitness.js + fitness-*.js now — read the family.
127
+ const src = fitnessSource();
126
128
  assert.match(src, /IDENTITY_SCAN_EXTS = new Set\(\[[^\]]*'\.sh'/,
127
129
  'the scan extension set must include .sh, or no shell script is ever checked');
128
130
  });
@@ -36,6 +36,7 @@ import { test } from 'node:test';
36
36
  import fs from 'node:fs';
37
37
  import path from 'node:path';
38
38
  import { fileURLToPath } from 'node:url';
39
+ import { authSource } from './helpers.mjs';
39
40
 
40
41
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
41
42
  const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
@@ -101,7 +102,8 @@ test('the value mapping is one shared function, not repeated inline', () => {
101
102
  });
102
103
 
103
104
  test('the sender writes BOTH spellings, computing the kind once (the cross-version window)', () => {
104
- const auth = read('src/bongos/auth.js');
105
+ // task 1003206: the auth flows are auth.js + auth-*.js now — read the family.
106
+ const auth = authSource();
105
107
  const body = auth.slice(auth.indexOf('async function reportMembershipCheckIn'), auth.indexOf('async function reportMembershipCheckIn') + 2400);
106
108
  assert.match(body, /membership_kind: kind/, 'the new field goes on the wire');
107
109
  assert.match(body, /role: kind === 'member' \? 'builder' : kind/,
@@ -28,7 +28,7 @@ import { readFileSync } from 'node:fs';
28
28
  import { fileURLToPath } from 'node:url';
29
29
  import { dirname, join } from 'node:path';
30
30
  import { createRequire } from 'node:module';
31
- import { lifecycleDbSource } from './helpers.mjs';
31
+ import { lifecycleDbSource, taskRoutesSource } from './helpers.mjs';
32
32
 
33
33
  const __dirname = dirname(fileURLToPath(import.meta.url));
34
34
  const repoRoot = join(__dirname, '..');
@@ -52,7 +52,8 @@ const migrationSql = read('migrations', '081_tasks_needs_migration.sql');
52
52
  // the tasks db slice + the tasks route carved to modules/lifecycle/ at BV1.R86.
53
53
  // task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
54
54
  const dbSrc = lifecycleDbSource();
55
- const routesSrc = read('modules', 'lifecycle', 'routes', 'tasks.js');
55
+ // task 1003206: the task router is tasks.js + task-*.js now — read the family.
56
+ const routesSrc = taskRoutesSource();
56
57
  const captureSrc = read('scripts', 'gds', 'capture.js');
57
58
 
58
59
  // The allocator's trigger, reproduced as a pure predicate. This is the exact
@@ -148,6 +149,8 @@ function shouldReserveMigrationNumber(task) {
148
149
  // "migration"/"schema" compounds, and a wrong verdict must be correctable
149
150
  // via PATCH. Behavioral tests use the real exported detectsMigration.
150
151
  const require = createRequire(import.meta.url);
152
+ // task 1003206: detectsMigration moved to task-route-helpers.js; tasks.js still
153
+ // re-exports it, so this keeps exercising the router's own public surface.
151
154
  const { detectsMigration } = require('../modules/lifecycle/routes/tasks.js');
152
155
 
153
156
  await test('classifier ignores code-migration and API-schema phrasing (task 1002648)', () => {
@@ -18,8 +18,10 @@
18
18
  import assert from 'node:assert/strict';
19
19
  import { test } from 'node:test';
20
20
  import { readFileSync } from 'node:fs';
21
+ import { taskRoutesSource } from './helpers.mjs';
21
22
 
22
- const src = readFileSync(new URL('../modules/lifecycle/routes/tasks.js', import.meta.url), 'utf8');
23
+ // task 1003206: PATCH /tasks/:id now lives in task-write-routes.js — read the family.
24
+ const src = taskRoutesSource();
23
25
 
24
26
  // The PATCH handler slice: router.patch('/tasks/:id' … up to the next route.
25
27
  function patchSlice(source) {
@@ -309,3 +309,56 @@ test('decision 10 — both footers carry the open-source (AGPL-3.0) link, in lin
309
309
  assert.ok(indexFoot.indexOf('<!--apex-only:end-->') < indexFoot.search(AGPL), 'index.html: after the apex-only block');
310
310
  assert.match(COSMOS, /\.foot a\{color:var\(--link-ink\);font-weight:700;text-decoration:none;\}/, 'never the browser’s blue');
311
311
  });
312
+
313
+ /* ── the should-fix remainder (task 1003244) ── */
314
+ test('an unmapped create failure gets a plain sentence with the raw text folded under Technical details', () => {
315
+ const friendly = fn('friendlyCreateError');
316
+ assert.match(friendly, /return '';\n\s*\}/, 'unmapped codes return nothing — the caller folds them');
317
+ assert.doesNotMatch(friendly, /Couldn’t file the request: ' \+/, 'the raw message is no longer the headline');
318
+ for (const code of ['slug_taken', 'bad_slug', 'domain_reserved', 'domain_taken', 'bad_domain', 'repo_scope_required', 'request_failed']) {
319
+ assert.match(friendly, new RegExp(`code === '${code}'`), `${code} stays headline-only`);
320
+ }
321
+ assert.match(friendly, /belongs to the platform’s own domain/, 'domain_reserved covers both server branches');
322
+ assert.match(fn('createFailureMarkup'), /Nothing was created — you can try again\.' \+\n\s*'<details><summary>Technical details<\/summary><p>' \+ esc\(err && err\.message/);
323
+ assert.match(SCRIPT, /if \(friendly\) alertBox\.textContent = friendly;\n\s*else alertBox\.innerHTML = createFailureMarkup\(err\);/);
324
+ assert.match(COSMOS, /\.alert details p\{[^}]*font-family:var\(--font-mono\)/, 'the raw text reads as code, inside the alert');
325
+ });
326
+
327
+ test('a manifest error says a plain sentence, folds the route text, and sends a domainless project to its address', () => {
328
+ const steps = fn('renderStepOauthApp');
329
+ assert.doesNotMatch(steps, /'GitHub setup hit a snag' \+ \(live\.manifestMsg \? ': ' \+ esc\(live\.manifestMsg\)/, 'the raw route text is no longer the headline');
330
+ assert.match(steps, /else if \(live\.manifestCode === 'domain_required'\) snag = 'This project needs an address first — attach one from <a href="\?view=mine">your projects page<\/a>/);
331
+ assert.match(steps, /\(raw \? '<details><summary>Technical details<\/summary><p>' \+ esc\(raw\) \+ '<\/p><\/details>' : ''\)/);
332
+ assert.match(SCRIPT, /e\.code = \(d\.error && d\.error\.code\) \|\| '';/, 'the route code travels with the error');
333
+ assert.match(SCRIPT, /live\.manifestCode = String\(\(err && err\.code\) \|\| ''\);/);
334
+ assert.equal((SCRIPT.match(/manifestMsg: '', manifestCode: '', appSlug: null/g) || []).length, 2, 'both resets carry the code');
335
+ });
336
+
337
+ test('the rail at the done panel keeps its marker on Create until the project answers', () => {
338
+ const rail = fn('paintRail');
339
+ assert.match(rail, /var landed = step === 6 && !!\(live && live\.instStatus === 'active'\);/);
340
+ assert.match(rail, /var cur = step === 6 \? 5 : step;/);
341
+ assert.match(rail, /li\.classList\.toggle\('done', landed \|\| n < cur\);/);
342
+ assert.match(rail, /if \(now\) li\.setAttribute\('aria-current', 'step'\); else li\.removeAttribute\('aria-current'\);/);
343
+ assert.match(fn('show'), /paintRail\(step\);/, 'show() paints through it');
344
+ assert.doesNotMatch(fn('show'), /classList\.toggle\('done'/, 'and no longer marks the rail on its own');
345
+ assert.match(SCRIPT, /live\.instStatus = inst\.status; renderSteps\(\);\n\s*if \(state\.step === 6\) paintRail\(6\);/, 'the done-panel poll repaints when the status moves');
346
+ assert.match(HUB, /<li class="now" data-step="1" aria-current="step">/, 'the static rail starts with the same marker');
347
+ });
348
+
349
+ test('card action results announce, nothing under the pointer is deleted, and focus stays on the card', () => {
350
+ assert.match(fn('renderCard'), /id="msg-' \+ iid \+ '" role="status" aria-live="polite">/, 'the card slot is a live region');
351
+ assert.match(HUB, /<div class="projMsg" id="mMsg" role="status" aria-live="polite"><\/div>/, 'so is the manage page’s');
352
+ assert.doesNotMatch(SCRIPT, /act\.innerHTML = '<span class="projMsg">Asking…<\/span>'/, 'no action replaces the row it was pressed in');
353
+ assert.match(fn('doTeardown'), /quietActions\(id\);\n\s*msg\(id, 'Asking…'\);/);
354
+ assert.match(fn('reProvision'), /quietActions\(id\);\n\s*msg\(id, 'Asking…'\);/);
355
+ assert.match(fn('quietActions'), /act\.querySelectorAll\('button'\)\.forEach\(function \(b\) \{ b\.disabled = true; \}\);/);
356
+ assert.match(fn('quietActions'), /li\.setAttribute\('data-focus-back', ''\)/, 'a disabled button that held focus leaves a note');
357
+ assert.match(fn('askTeardown'), /<div class="confirmRow" tabindex="-1">/);
358
+ assert.match(fn('askTeardown'), /if \(row && row\.focus\) row\.focus\(\);/, 'the question takes focus when the button goes');
359
+ const card = fn('renderCard');
360
+ assert.match(card, /var wasInside = li\.contains\(document\.activeElement\) && document\.activeElement !== li;/);
361
+ assert.match(card, /else if \(focusBack\) focusCardControl\(inst\.id, prefer\);/);
362
+ assert.match(fn('focusKeyOf'), /data-teardown-yes'\) \|\| el\.hasAttribute\('data-teardown-no'\)\) return '\[data-teardown\]'/, 'Yes / Keep land back on the door that opened them');
363
+ assert.match(fn('focusCardControl'), /li\.querySelector\('\.actions a, \.actions button'\) \|\|\n\s*li\.querySelector\('details\.attachFold summary'\)/, 'nearest control as the fallback');
364
+ });
@@ -124,9 +124,18 @@ test('static ladder copies carry the exact canonical ladder', () => {
124
124
  }
125
125
  });
126
126
 
127
- const STATIC_MAP_FILES = ['modules/hall-ui/public/builders.js', 'modules/hall-ui/public/goals-page.js'];
127
+ // task 1003206 carved the hall page scripts up; the rank tier maps travelled with
128
+ // the renderers that read them, so the pin and the allowlist follow the code.
129
+ const HALL_SCRIPTS = ['modules/hall-ui/public/builders.js', 'modules/hall-ui/public/goals-page.js',
130
+ 'modules/hall-ui/public/hall-render.js', 'modules/hall-ui/public/goals-render.js'];
131
+ // Filtered INSIDE the test, not at module scope: extractRankMaps is declared
132
+ // further down, so calling it up here is a temporal-dead-zone crash.
133
+ const staticMapFiles = () => HALL_SCRIPTS.filter(
134
+ (rel) => extractRankMaps(fs.readFileSync(path.join(ROOT, rel), 'utf8')).length > 0);
128
135
  test('hall static tier maps match the kernel map', () => {
129
- for (const rel of STATIC_MAP_FILES) {
136
+ const files = staticMapFiles();
137
+ assert.ok(files.length > 0, 'at least one hall script must carry the tier map');
138
+ for (const rel of files) {
130
139
  const src = fs.readFileSync(path.join(ROOT, rel), 'utf8');
131
140
  const maps = extractRankMaps(src);
132
141
  assert.ok(maps.length > 0, `${rel}: expected a rank tier map`);
@@ -146,7 +155,11 @@ const MAP_ALLOWLIST = new Set([
146
155
  'scripts/gds/box-connect-lib.js',
147
156
  'modules/dev-box/boxes.js',
148
157
  'modules/hall-ui/public/builders.js',
149
- 'modules/hall-ui/public/goals-page.js',
158
+ // task 1003206 carved the hall page scripts up. builders.js KEPT its map (the
159
+ // renderers read it through the injected ctx), while goals-page.js's travelled
160
+ // into goals-render.js — so goals-page.js is pruned and hall-render.js is never
161
+ // added. The allowlist fails on a stale entry, which is the check working.
162
+ 'modules/hall-ui/public/goals-render.js',
150
163
  ]);
151
164
  // Where a rank-ladder array literal is allowed to exist.
152
165
  const LADDER_ALLOWLIST = new Set([
@@ -25,7 +25,7 @@ const db = require('../modules/lifecycle/db.js');
25
25
  // the SAME singleton object db.js captured, so mutating its .query stubs the INSERT
26
26
  // createTask runs — no database needed.
27
27
  const sharedPool = require('../src/module-api.js').pool;
28
- import { lifecycleDbSource } from './helpers.mjs';
28
+ import { lifecycleDbSource, taskRoutesSource } from './helpers.mjs';
29
29
 
30
30
  const __dir = dirname(fileURLToPath(import.meta.url));
31
31
  const root = join(__dir, '..');
@@ -132,7 +132,8 @@ t('null task is a no-op (no throw, no assignment)', () => {
132
132
  // ---------------------------------------------------------------------------
133
133
  console.log('\nenforcement points (source-wiring — AUTO-ASSIGN):');
134
134
 
135
- const tasksRouteSrc = readFileSync(join(root, 'modules/lifecycle/routes/tasks.js'), 'utf8');
135
+ // task 1003206: POST /tasks now lives in task-write-routes.js — read the family.
136
+ const tasksRouteSrc = taskRoutesSource();
136
137
  // task 1003204: the lifecycle data layer is db.js + db-*.js now — read the family.
137
138
  const dbSrc = lifecycleDbSource();
138
139
  const migration178 = readFileSync(join(root, 'migrations/178_reward_gate_auto_assign_revert_autopromote.sql'), 'utf8');