@bongos/core 1.17.2 → 1.17.3
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 +292 -32
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -2
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +65 -26
- package/docs/api-reference.md +3 -2
- package/modules/hall-ui/public/builders.js +96 -646
- package/modules/hall-ui/public/goal-inbox.js +67 -26
- package/modules/hall-ui/public/goals-page.js +4 -1
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/hall-kit.js +474 -0
- package/modules/hall-ui/public/hall-widgets.js +11 -15
- package/modules/hall-ui/public/instances-panel.js +24 -3
- package/modules/hall-ui/public/new-project-wizard.js +6 -5
- package/modules/hall-ui/public/style.css +125 -0
- package/modules/hall-ui/public/work.css +194 -0
- package/modules/hall-ui/public/work.html +46 -374
- package/modules/hall-ui/public/work.js +137 -102
- package/modules/lifecycle/criterion-suggest.js +80 -0
- package/modules/lifecycle/db.js +42 -5
- package/modules/lifecycle/routes/goals.js +26 -0
- package/modules/lifecycle/routes/tasks.js +21 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/hall-preview/README.md +103 -0
- package/scripts/hall-preview/fixtures/access-requests.json +1 -0
- package/scripts/hall-preview/fixtures/achievements.json +76 -0
- package/scripts/hall-preview/fixtures/analytics__builder.json +251 -0
- package/scripts/hall-preview/fixtures/audit-log.json +1 -0
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +1 -0
- package/scripts/hall-preview/fixtures/builders__active-claims.json +1 -0
- package/scripts/hall-preview/fixtures/builders__directory.json +81 -0
- package/scripts/hall-preview/fixtures/builders__roster.json +1 -0
- package/scripts/hall-preview/fixtures/goals.json +502 -0
- package/scripts/hall-preview/fixtures/goals__25.json +96 -0
- package/scripts/hall-preview/fixtures/goals__26.json +67 -0
- package/scripts/hall-preview/fixtures/goals__3.json +103 -0
- package/scripts/hall-preview/fixtures/goals__35.json +49 -0
- package/scripts/hall-preview/fixtures/goals__conflicts.json +154 -0
- package/scripts/hall-preview/fixtures/goals__detail.json +96 -0
- package/scripts/hall-preview/fixtures/goals__inbox.json +5 -0
- package/scripts/hall-preview/fixtures/goals__requests.json +3 -0
- package/scripts/hall-preview/fixtures/grades__by-builder.json +1 -0
- package/scripts/hall-preview/fixtures/inbox.json +330 -0
- package/scripts/hall-preview/fixtures/instance.json +124 -0
- package/scripts/hall-preview/fixtures/leaderboard.json +184 -0
- package/scripts/hall-preview/fixtures/me.json +476 -0
- package/scripts/hall-preview/fixtures/me__recent-ships.json +304 -0
- package/scripts/hall-preview/fixtures/override-requests.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__instances.json +1 -0
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +516 -0
- package/scripts/hall-preview/fixtures/public__bounty-table.json +37 -0
- package/scripts/hall-preview/fixtures/public__cost-summary.json +742 -0
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +41 -0
- package/scripts/hall-preview/fixtures/public__grades.json +50 -0
- package/scripts/hall-preview/fixtures/public__progress.json +124 -0
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +426 -0
- package/scripts/hall-preview/fixtures/public__repo-health.json +160 -0
- package/scripts/hall-preview/fixtures/public__repo-info.json +5 -0
- package/scripts/hall-preview/fixtures/security__docs.json +1 -0
- package/scripts/hall-preview/fixtures/security__reports.json +1 -0
- package/scripts/hall-preview/fixtures/sessions__search.json +1 -0
- package/scripts/hall-preview/fixtures/tasks.json +2420 -0
- package/scripts/hall-preview/fixtures/tasks__claimable.json +2555 -0
- package/scripts/hall-preview/fixtures/tasks__detail.json +111 -0
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +330 -0
- package/scripts/hall-preview/fixtures/versions.json +89 -0
- package/scripts/hall-preview/refresh-fixtures.js +77 -0
- package/scripts/hall-preview/server.js +178 -0
- package/scripts/migrate.sh +29 -4
- package/src/bongos/optimizer.js +37 -2
- package/tests/criterion_suggest.mjs +106 -0
- package/tests/migrate_root_resolution.mjs +97 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/hall-preview/server.js — the hall preview harness (task 2167).
|
|
3
|
+
//
|
|
4
|
+
// The dev-box sandbox previews the GAME only (/builders 404s there, idea 490),
|
|
5
|
+
// so hall-ui changes are reviewed HERE: a tiny express app that serves the real
|
|
6
|
+
// modules/hall-ui/public files through the REAL serving seams (boot-time ?v=
|
|
7
|
+
// stamping, {{token}} replacement, __BRANDING__/__MODULES__ injection, theme
|
|
8
|
+
// markup) with the /api/gds|bongos reads answered from checked-in fixtures.
|
|
9
|
+
// Boots DB-less (pg pools connect lazily). Proven recipe from tasks 1723/1742,
|
|
10
|
+
// finally checked in.
|
|
11
|
+
//
|
|
12
|
+
// node scripts/hall-preview/server.js [--port 4600] [--brand <file>]
|
|
13
|
+
// [--modules-off dev-box,provisioning] [--empty]
|
|
14
|
+
//
|
|
15
|
+
// --port listen port (default 4600)
|
|
16
|
+
// --brand branding pack file — default config/branding.neutral.json
|
|
17
|
+
// (vanilla); pass config/branding.json for the instance pack
|
|
18
|
+
// --modules-off comma list of module keys to report disabled in
|
|
19
|
+
// window.__MODULES__ (verifies moduleOn() gating)
|
|
20
|
+
// --empty serve every fixture with its arrays emptied — the
|
|
21
|
+
// empty-state screenshot pass
|
|
22
|
+
//
|
|
23
|
+
// Browse http://builders.localhost:<port>/ — Chromium resolves *.localhost,
|
|
24
|
+
// and the `builders.` host prefix exercises the real subdomain serving path.
|
|
25
|
+
// Refresh fixtures from live data: node scripts/hall-preview/refresh-fixtures.js
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
|
|
31
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
32
|
+
const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
33
|
+
const FIXTURES = path.join(__dirname, 'fixtures');
|
|
34
|
+
|
|
35
|
+
// ---- flags (before any src/ require: --brand must win the branding resolve) --
|
|
36
|
+
function arg(name) {
|
|
37
|
+
const i = process.argv.indexOf(name);
|
|
38
|
+
return i > -1 ? process.argv[i + 1] : null;
|
|
39
|
+
}
|
|
40
|
+
const PORT = Number(arg('--port')) || 4600;
|
|
41
|
+
const EMPTY = process.argv.includes('--empty');
|
|
42
|
+
const MODULES_OFF = String(arg('--modules-off') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
43
|
+
// --modules-on: force-report modules as enabled — a dev box's local config
|
|
44
|
+
// often lacks prod's instance modules (provisioning, dev-box), and their
|
|
45
|
+
// widgets can't be reviewed without it (task 2169).
|
|
46
|
+
const MODULES_ON = String(arg('--modules-on') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
47
|
+
const BRAND_FILE = arg('--brand') || 'config/branding.neutral.json';
|
|
48
|
+
process.env.GDS_BRANDING_FILE = path.resolve(ROOT, BRAND_FILE);
|
|
49
|
+
|
|
50
|
+
/* eslint-disable import/no-dynamic-require */
|
|
51
|
+
// express comes from this tree's node_modules when installed, else node's
|
|
52
|
+
// normal upward walk finds the main checkout's copy (a git worktree under
|
|
53
|
+
// .claude/worktrees/ has no node_modules of its own). The src/ seams below are
|
|
54
|
+
// ALWAYS taken from THIS tree so a worktree's edits are what gets previewed.
|
|
55
|
+
let express;
|
|
56
|
+
try { express = require(path.join(ROOT, 'node_modules', 'express')); }
|
|
57
|
+
catch (_) { express = require('express'); }
|
|
58
|
+
const { buildStampedHtml } = require(path.join(ROOT, 'src', 'asset-version.js'));
|
|
59
|
+
const { injectBrandingGlobal, mountInternalSurfaces } = require(path.join(ROOT, 'src', 'bongos', 'serve-internal.js'));
|
|
60
|
+
const { clientModules } = require(path.join(ROOT, 'src', 'modules.js'));
|
|
61
|
+
|
|
62
|
+
// ---- fixtures ---------------------------------------------------------------
|
|
63
|
+
// File naming: the API path with slashes as "__" — /me/recent-ships →
|
|
64
|
+
// me__recent-ships.json. Query strings are ignored when matching. Id-bearing
|
|
65
|
+
// paths fall back to the generic names below so one fixture serves any id.
|
|
66
|
+
const FIXTURE_FALLBACKS = [
|
|
67
|
+
[/^goals\/\d+\/conflicts$/, 'goals__conflicts'],
|
|
68
|
+
[/^goals\/\d+\/requests$/, 'goals__requests'],
|
|
69
|
+
[/^goals\/\d+$/, 'goals__detail'],
|
|
70
|
+
[/^tasks\/\d+$/, 'tasks__detail'],
|
|
71
|
+
[/^analytics\/builder\/\d+$/, 'analytics__builder'],
|
|
72
|
+
[/^builders\/\d+\/active-claims$/, 'builders__active-claims'],
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
function loadFixture(name) {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(fs.readFileSync(path.join(FIXTURES, `${name}.json`), 'utf8'));
|
|
78
|
+
} catch (_) { return undefined; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --empty: the same pages, with nothing in them — every array drains so each
|
|
82
|
+
// page's empty states render. Scalars/strings survive (identity stays real).
|
|
83
|
+
function emptied(v) {
|
|
84
|
+
if (Array.isArray(v)) return [];
|
|
85
|
+
if (v && typeof v === 'object') {
|
|
86
|
+
const out = {};
|
|
87
|
+
for (const [k, val] of Object.entries(v)) out[k] = emptied(val);
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
return v;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function fixtureFor(apiPath) {
|
|
94
|
+
const exact = loadFixture(apiPath.replace(/\//g, '__'));
|
|
95
|
+
if (exact !== undefined) return exact;
|
|
96
|
+
for (const [re, name] of FIXTURE_FALLBACKS) {
|
|
97
|
+
if (re.test(apiPath)) {
|
|
98
|
+
const hit = loadFixture(name);
|
|
99
|
+
if (hit !== undefined) return hit;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- app ----------------------------------------------------------------------
|
|
106
|
+
const app = express();
|
|
107
|
+
|
|
108
|
+
// The API stub — intercepts every /api/gds/*, /api/bongos/*, /api/bongos/v1/*
|
|
109
|
+
// request BEFORE mountInternalSurfaces can reach the real routes (which would
|
|
110
|
+
// touch Postgres — the harness is DB-less by design). A plain function
|
|
111
|
+
// middleware, not a path mount: regex-mount semantics differ across express
|
|
112
|
+
// majors and a silent non-match here means a pg crash.
|
|
113
|
+
app.use((req, res, next) => {
|
|
114
|
+
const m = req.path.match(/^\/api\/(?:gds|bongos)(?:\/v1)?\/(.+)$/);
|
|
115
|
+
if (!m) return next();
|
|
116
|
+
const apiPath = m[1].replace(/\/$/, '');
|
|
117
|
+
|
|
118
|
+
// SSE live channel: emit one nudge (so "live refresh" paths run once), then
|
|
119
|
+
// keep-alive comments. Pages treat this exactly like prod's stream.
|
|
120
|
+
if (apiPath === 'live') {
|
|
121
|
+
res.set({ 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
|
|
122
|
+
res.flushHeaders();
|
|
123
|
+
setTimeout(() => res.write('event: nudge\ndata: {}\n\n'), 1200);
|
|
124
|
+
const tick = setInterval(() => res.write(': keep-alive\n\n'), 25000);
|
|
125
|
+
req.on('close', () => clearInterval(tick));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (req.method !== 'GET') {
|
|
130
|
+
return res.status(501).json({ error: { code: 'preview_readonly', message: 'the preview harness serves fixtures; writes are not wired' } });
|
|
131
|
+
}
|
|
132
|
+
const fix = fixtureFor(apiPath);
|
|
133
|
+
if (fix === undefined) {
|
|
134
|
+
console.log(` no fixture: ${apiPath} (add scripts/hall-preview/fixtures/${apiPath.replace(/\//g, '__')}.json)`);
|
|
135
|
+
return res.status(404).json({ error: { code: 'no_fixture', message: apiPath } });
|
|
136
|
+
}
|
|
137
|
+
res.json(EMPTY ? emptied(fix) : fix);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// The hall pages, pre-auth-gate, through the real seams: boot-time stamped
|
|
141
|
+
// (?v= content hashes), {{token}}-replaced, __BRANDING__/__MODULES__ injected,
|
|
142
|
+
// theme markup appended — byte-for-byte what prod serves after its gates.
|
|
143
|
+
const stamped = buildStampedHtml(PUB, '/builders/');
|
|
144
|
+
const mods = JSON.parse(JSON.stringify(clientModules()));
|
|
145
|
+
for (const key of MODULES_OFF) {
|
|
146
|
+
if (mods.enabled && key in mods.enabled) mods.enabled[key] = false;
|
|
147
|
+
else (mods.enabled = mods.enabled || {})[key] = false;
|
|
148
|
+
}
|
|
149
|
+
for (const key of MODULES_ON) {
|
|
150
|
+
(mods.enabled = mods.enabled || {})[key] = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sendPage(res, name) {
|
|
154
|
+
const html = stamped[name];
|
|
155
|
+
if (!html) return false;
|
|
156
|
+
res.type('html').send(injectBrandingGlobal(html, undefined, mods));
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
app.get('/', (req, res, next) => { if (!sendPage(res, 'index.html')) next(); });
|
|
161
|
+
app.get(/^\/task\/\d+$/, (req, res, next) => { if (!sendPage(res, 'task.html')) next(); });
|
|
162
|
+
app.get(/^\/([a-z-]+)$/, (req, res, next) => {
|
|
163
|
+
if (!sendPage(res, `${req.params[0]}.html`)) next();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Everything else — /builders/*.css|js assets, /lib/bongos-client.js, diagram
|
|
167
|
+
// PNGs — falls through to the real internal-surface mounting. Our handlers
|
|
168
|
+
// above registered first, so the hall's auth gates in there never fire for
|
|
169
|
+
// the page routes we already served.
|
|
170
|
+
mountInternalSurfaces(app);
|
|
171
|
+
|
|
172
|
+
// Loopback only — this is a pre-auth preview of hall pages with fixture data;
|
|
173
|
+
// *.localhost resolves to 127.0.0.1 so nothing else is needed.
|
|
174
|
+
app.listen(PORT, '127.0.0.1', () => {
|
|
175
|
+
console.log(`hall-preview: http://builders.localhost:${PORT}/`);
|
|
176
|
+
console.log(` brand: ${BRAND_FILE}${EMPTY ? ' (EMPTY fixtures)' : ''}${MODULES_OFF.length ? ` modules off: ${MODULES_OFF.join(',')}` : ''}`);
|
|
177
|
+
console.log(' pages:', Object.keys(stamped).map((f) => f.replace(/\.html$/, '')).sort().join(' '));
|
|
178
|
+
});
|
package/scripts/migrate.sh
CHANGED
|
@@ -12,18 +12,43 @@ set -euo pipefail
|
|
|
12
12
|
# alongside the instance, so both resolve to the SAME dir and this is byte-identical to the
|
|
13
13
|
# old `cd "$(dirname "$0")/.."`. Once the core installs from node_modules, resolveCoreRoot()
|
|
14
14
|
# points into the package (core migrations + src/) while resolveInstanceRoot() stays the host
|
|
15
|
-
# repo (env → bongos.config → cwd).
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
# repo (env → bongos.config → cwd).
|
|
16
|
+
#
|
|
17
|
+
# CORE root is resolved SCRIPT-relative (the package's own files sit next to this script), so it
|
|
18
|
+
# never depends on the caller's cwd. The INSTANCE root is the OPPOSITE: it MUST come from where
|
|
19
|
+
# migrate was INVOKED. `npm run migrate` from the instance sets INIT_CWD (and $PWD) to the
|
|
20
|
+
# instance dir, where bongos.config.json lives. Resolving the instance root script-relative would
|
|
21
|
+
# `cd` into the core PACKAGE first — whose dir has no bongos.config.json — so with no
|
|
22
|
+
# OTB_INSTANCE_ROOT env it falls back to the package dir as the "instance" and reads the core's
|
|
23
|
+
# VANILLA module config, SILENTLY skipping every host + flag-gated module migration (R89 rehearsal
|
|
24
|
+
# F6, task 2180: literal `npm run migrate` applied 115 rows and created no game schema; from the
|
|
25
|
+
# instance cwd it applied 121). So resolve the instance root from the invocation cwd, preferring an
|
|
26
|
+
# EXPLICIT pointer (OTB_INSTANCE_ROOT env, else the invocation cwd's bongos.config.json).
|
|
27
|
+
# resolveInstanceRootExplicit() returns '' (NOT process.cwd()) when neither is present, so we fall
|
|
28
|
+
# back to the script-relative DEFAULT_ROOT — the monolith case (cwd==core==instance), byte-identical
|
|
29
|
+
# to the old behavior, and a stray cwd can never masquerade as the instance. Fail open to
|
|
30
|
+
# DEFAULT_ROOT if node is unavailable so a deploy never breaks.
|
|
31
|
+
INVOKE_CWD="${INIT_CWD:-$PWD}"
|
|
18
32
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
19
33
|
DEFAULT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
20
34
|
CORE_ROOT="$(cd "$DEFAULT_ROOT" && node -e "process.stdout.write(require('./src/instance-config').resolveCoreRoot())" 2>/dev/null || echo "$DEFAULT_ROOT")"
|
|
21
|
-
INSTANCE_ROOT="$(cd "$
|
|
35
|
+
INSTANCE_ROOT="$(cd "$INVOKE_CWD" && node -e "process.stdout.write(require('$DEFAULT_ROOT/src/instance-config').resolveInstanceRootExplicit() || '')" 2>/dev/null || true)"
|
|
36
|
+
INSTANCE_ROOT="${INSTANCE_ROOT:-$DEFAULT_ROOT}"
|
|
22
37
|
# Run from the instance root so instance-relative paths resolve (the post-deploy art check below)
|
|
23
38
|
# AND so the enabledModules() read finds the instance's config/modules.json (resolveInstanceRoot
|
|
24
39
|
# falls back to cwd).
|
|
25
40
|
cd "$INSTANCE_ROOT"
|
|
26
41
|
|
|
42
|
+
# Test/ops seam (task 2180): print the resolved roots and exit BEFORE any DB work, so a DB-free
|
|
43
|
+
# unit test (tests/migrate_root_resolution.mjs) can assert the INSTANCE root resolves from the
|
|
44
|
+
# invocation cwd (a consumer's bongos.config.json), not script-relative into the core package —
|
|
45
|
+
# the F6 regression. Also handy in the field: `npm run migrate -- --print-roots` shows what
|
|
46
|
+
# migrate sees.
|
|
47
|
+
if [ "${1:-}" = "--print-roots" ]; then
|
|
48
|
+
printf 'INVOKE_CWD=%s\nCORE_ROOT=%s\nINSTANCE_ROOT=%s\n' "$INVOKE_CWD" "$CORE_ROOT" "$INSTANCE_ROOT"
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
27
52
|
DB="${PGDATABASE:-amazonprimea}"
|
|
28
53
|
|
|
29
54
|
# --- Instance-seed policy (task 1704 / ADR 0105) --------------------------
|
package/src/bongos/optimizer.js
CHANGED
|
@@ -53,10 +53,45 @@ async function optimizeSession({ builderId, mode, timeBudgetMin, maxParallel })
|
|
|
53
53
|
// tasks_session_fit (derived fits_session_modes[] + credits_per_minute).
|
|
54
54
|
// Also exclude tasks this builder already has an active claim on, mirroring
|
|
55
55
|
// listClaimableTasks in db.js.
|
|
56
|
+
// task 2181 (incident, blocker 74): the view bodies (migration core_190) are
|
|
57
|
+
// inlined so this query takes only ACCESS SHARE on base tables — a detached
|
|
58
|
+
// backend queued an ACCESS EXCLUSIVE DDL on the views and parked every view
|
|
59
|
+
// SELECT behind it. Plan-identical (Postgres inlines simple views); change in
|
|
60
|
+
// lockstep with the views / listClaimableTasks if a migration alters them.
|
|
56
61
|
const { rows } = await pool.query(
|
|
57
62
|
`SELECT ct.*, sf.fits_session_modes, sf.credits_per_minute
|
|
58
|
-
FROM
|
|
59
|
-
|
|
63
|
+
FROM (
|
|
64
|
+
SELECT t.*, pms_calibrated_minutes(t.est_minutes, t.kind) AS est_minutes_calibrated
|
|
65
|
+
FROM tasks t
|
|
66
|
+
WHERE t.status = 'ready'
|
|
67
|
+
AND t.title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'
|
|
68
|
+
AND NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)
|
|
69
|
+
AND NOT EXISTS (
|
|
70
|
+
SELECT 1 FROM claims c
|
|
71
|
+
JOIN tasks t2 ON t2.id = c.task_id
|
|
72
|
+
WHERE c.released_at IS NULL
|
|
73
|
+
AND t2.touches && t.touches
|
|
74
|
+
)
|
|
75
|
+
) ct
|
|
76
|
+
JOIN (
|
|
77
|
+
SELECT t.id,
|
|
78
|
+
CASE
|
|
79
|
+
WHEN t.automation_tag = 'overnight-safe' AND COALESCE(t.manual_degree, 5) <= 2
|
|
80
|
+
AND NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)
|
|
81
|
+
THEN ARRAY['autonomous','check-in','active']
|
|
82
|
+
WHEN t.automation_tag = 'live-only' OR COALESCE(t.manual_degree, 5) >= 4
|
|
83
|
+
THEN ARRAY['active']
|
|
84
|
+
ELSE ARRAY['check-in','active']
|
|
85
|
+
END AS fits_session_modes,
|
|
86
|
+
CASE
|
|
87
|
+
WHEN t.credits_reward > 0
|
|
88
|
+
AND pms_calibrated_minutes(t.est_minutes, t.kind) IS NOT NULL
|
|
89
|
+
AND pms_calibrated_minutes(t.est_minutes, t.kind) > 0
|
|
90
|
+
THEN ROUND(t.credits_reward::numeric / pms_calibrated_minutes(t.est_minutes, t.kind), 3)
|
|
91
|
+
ELSE NULL
|
|
92
|
+
END AS credits_per_minute
|
|
93
|
+
FROM tasks t
|
|
94
|
+
) sf ON sf.id = ct.id
|
|
60
95
|
WHERE $1 = ANY(sf.fits_session_modes)
|
|
61
96
|
AND NOT EXISTS (
|
|
62
97
|
SELECT 1 FROM claims c
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// tests/criterion_suggest.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit tests for suggestCriteria() — the pure criterion RECOMMENDER
|
|
4
|
+
// (modules/lifecycle/criterion-suggest.js, task 2165). Given a goal's criteria
|
|
5
|
+
// and a candidate task's title/description, it ranks the criteria by weighted
|
|
6
|
+
// lexical overlap so task-create can suggest which done-when criterion to link
|
|
7
|
+
// (the author confirms — nothing is auto-linked). No DB, so it runs in the
|
|
8
|
+
// DB-free suite.
|
|
9
|
+
//
|
|
10
|
+
// Run: node tests/criterion_suggest.mjs
|
|
11
|
+
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const { suggestCriteria, tokenize } = require('../modules/lifecycle/criterion-suggest.js');
|
|
17
|
+
|
|
18
|
+
let passed = 0;
|
|
19
|
+
let failed = 0;
|
|
20
|
+
function test(name, fn) {
|
|
21
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
22
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const crit = (id, criterion_id, criterion_md, satisfied = false) =>
|
|
26
|
+
({ id, criterion_id, criterion_md, satisfied });
|
|
27
|
+
|
|
28
|
+
// A small, realistic goal-25-flavoured criterion set.
|
|
29
|
+
const CRITERIA = [
|
|
30
|
+
crit(138, 'bx-hall', 'Core task actions work in the builders hall without invoking the LLM.'),
|
|
31
|
+
crit(139, 'bx-identity', 'A builder can view and edit their own profile and read a leaderboard.'),
|
|
32
|
+
crit(140, 'bx-onramp', 'Newcomers never dead-end: auth and re-auth are browser-clickable.'),
|
|
33
|
+
crit(141, 'bx-box', 'A builder dev box and sandbox is robust: uncommitted work is not lost.'),
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
test('ranks the most lexically-relevant criterion first', () => {
|
|
37
|
+
const out = suggestCriteria(CRITERIA, {
|
|
38
|
+
title: 'In-hall confirm button for criteria',
|
|
39
|
+
description: 'Let builders confirm a criterion in the hall without the LLM.',
|
|
40
|
+
});
|
|
41
|
+
assert.ok(out.length >= 1, 'expected at least one suggestion');
|
|
42
|
+
assert.equal(out[0].criterion_id, 'bx-hall', `expected bx-hall first, got ${out[0].criterion_id}`);
|
|
43
|
+
assert.ok(out[0].matched_terms.includes('hall'), 'should report the matched term');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('title terms weigh more than description terms', () => {
|
|
47
|
+
// "profile" only in the title → should beat a body-only "hall" match.
|
|
48
|
+
const out = suggestCriteria(CRITERIA, {
|
|
49
|
+
title: 'Edit your profile',
|
|
50
|
+
description: 'Shown in the hall.',
|
|
51
|
+
});
|
|
52
|
+
assert.equal(out[0].criterion_id, 'bx-identity', `title term should win, got ${out[0].criterion_id}`);
|
|
53
|
+
const identity = out.find((c) => c.criterion_id === 'bx-identity');
|
|
54
|
+
const hall = out.find((c) => c.criterion_id === 'bx-hall');
|
|
55
|
+
assert.ok(hall === undefined || identity.score > hall.score, 'title match must outscore body match');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('returns [] when nothing overlaps (no low-confidence noise)', () => {
|
|
59
|
+
const out = suggestCriteria(CRITERIA, {
|
|
60
|
+
title: 'Migrate the Postgres backup vendor to object storage',
|
|
61
|
+
description: 'Nightly encrypted dumps to a bucket.',
|
|
62
|
+
});
|
|
63
|
+
assert.deepEqual(out, [], 'no shared vocabulary → no suggestion');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('never suggests an already-satisfied criterion', () => {
|
|
67
|
+
const satisfiedHall = [crit(138, 'bx-hall', 'Core task actions work in the builders hall.', true)];
|
|
68
|
+
const out = suggestCriteria(satisfiedHall, { title: 'hall task actions' });
|
|
69
|
+
assert.deepEqual(out, [], 'satisfied criteria are excluded');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('respects the limit and is stable (id tiebreak on equal scores)', () => {
|
|
73
|
+
const tied = [
|
|
74
|
+
crit(20, 'b', 'builder profile'),
|
|
75
|
+
crit(10, 'a', 'builder profile'),
|
|
76
|
+
crit(30, 'c', 'builder profile'),
|
|
77
|
+
];
|
|
78
|
+
const out = suggestCriteria(tied, { title: 'builder profile' }, { limit: 2 });
|
|
79
|
+
assert.equal(out.length, 2, 'limit caps the result');
|
|
80
|
+
assert.deepEqual(out.map((c) => c.id), [10, 20], 'equal scores break ties by ascending id');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('includeZero returns all unsatisfied criteria in score order', () => {
|
|
84
|
+
const out = suggestCriteria(CRITERIA, { title: 'profile' }, { includeZero: true, limit: 10 });
|
|
85
|
+
assert.equal(out.length, 4, 'includeZero keeps the zero-overlap criteria');
|
|
86
|
+
assert.equal(out[0].criterion_id, 'bx-identity', 'best match still leads');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('empty / malformed inputs are safe', () => {
|
|
90
|
+
assert.deepEqual(suggestCriteria([], { title: 'x' }), []);
|
|
91
|
+
assert.deepEqual(suggestCriteria(null, { title: 'x' }), []);
|
|
92
|
+
assert.deepEqual(suggestCriteria(CRITERIA, {}), [], 'no candidate terms → no suggestion');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('tokenize drops stopwords, short tokens, and singularizes', () => {
|
|
96
|
+
const toks = tokenize('The builders can edit profiles and tasks.');
|
|
97
|
+
assert.ok(!toks.includes('the'), 'stopword dropped');
|
|
98
|
+
assert.ok(!toks.includes('can'), 'stopword dropped');
|
|
99
|
+
assert.ok(toks.includes('builder'), 'builders → builder');
|
|
100
|
+
assert.ok(toks.includes('profile'), 'profiles → profile');
|
|
101
|
+
assert.ok(toks.includes('task'), 'tasks → task');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(`criterion-suggest recommender tests: ${passed} passed, ${failed} failed`);
|
|
106
|
+
if (failed > 0) process.exit(1);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// tests/migrate_root_resolution.mjs
|
|
2
|
+
//
|
|
3
|
+
// R89 rehearsal F6 (task 2180): after the core→consumer split, scripts/migrate.sh
|
|
4
|
+
// must resolve the INSTANCE root from the INVOCATION cwd (OTB_INSTANCE_ROOT env,
|
|
5
|
+
// else the invocation cwd's bongos.config.json), NOT script-relative into the
|
|
6
|
+
// installed @bongos/core package. The old script-relative resolve `cd`'d into the
|
|
7
|
+
// package dir first — which has no bongos.config.json — so it silently picked the
|
|
8
|
+
// package as the "instance" and read the core's VANILLA module config, skipping
|
|
9
|
+
// every host + flag-gated module migration (rehearsal: `npm run migrate` applied
|
|
10
|
+
// 115 rows and created NO game schema; from the instance cwd it applied 121).
|
|
11
|
+
//
|
|
12
|
+
// This exercises migrate.sh's `--print-roots` seam (DB-free: it prints the resolved
|
|
13
|
+
// roots and exits before any DB work), so no Postgres is needed. CORE_ROOT must stay
|
|
14
|
+
// script-relative in every case; INSTANCE_ROOT must follow the invocation cwd.
|
|
15
|
+
//
|
|
16
|
+
// Run: node tests/migrate_root_resolution.mjs
|
|
17
|
+
|
|
18
|
+
import { strict as assert } from 'node:assert';
|
|
19
|
+
import { execFileSync } from 'node:child_process';
|
|
20
|
+
import { mkdtempSync, mkdirSync, writeFileSync, realpathSync } from 'node:fs';
|
|
21
|
+
import { tmpdir } from 'node:os';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
|
|
25
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const MIGRATE = path.join(REPO_ROOT, 'scripts', 'migrate.sh');
|
|
27
|
+
const REPO_REAL = realpathSync(REPO_ROOT);
|
|
28
|
+
|
|
29
|
+
let passed = 0;
|
|
30
|
+
let failed = 0;
|
|
31
|
+
function t(name, fn) {
|
|
32
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
33
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Run `migrate.sh --print-roots` with a controlled cwd + INIT_CWD, and an env that
|
|
37
|
+
// carries NO *_INSTANCE_ROOT override, so we test cwd-based resolution alone.
|
|
38
|
+
function printRoots({ cwd, initCwd } = {}) {
|
|
39
|
+
const env = { ...process.env };
|
|
40
|
+
for (const k of Object.keys(env)) if (/_INSTANCE_ROOT$/.test(k)) delete env[k];
|
|
41
|
+
delete env.INIT_CWD;
|
|
42
|
+
if (initCwd) env.INIT_CWD = initCwd;
|
|
43
|
+
const out = execFileSync('bash', [MIGRATE, '--print-roots'], { cwd, env, encoding: 'utf8' });
|
|
44
|
+
const roots = {};
|
|
45
|
+
for (const line of out.split('\n')) {
|
|
46
|
+
const m = /^(INVOKE_CWD|CORE_ROOT|INSTANCE_ROOT)=(.*)$/.exec(line);
|
|
47
|
+
if (m) roots[m[1]] = m[2];
|
|
48
|
+
}
|
|
49
|
+
return roots;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A synthetic consumer: a dir with a bongos.config.json pointing at itself, exactly
|
|
53
|
+
// as the post-split amazonprimea repo carries (bongos.config.json { instanceRoot: "." }).
|
|
54
|
+
function makeConsumer() {
|
|
55
|
+
const dir = realpathSync(mkdtempSync(path.join(tmpdir(), 'r89-consumer-')));
|
|
56
|
+
writeFileSync(path.join(dir, 'bongos.config.json'), JSON.stringify({ instanceRoot: '.' }));
|
|
57
|
+
return dir;
|
|
58
|
+
}
|
|
59
|
+
// A dir with NO bongos.config.json — stands in for the monolith / a stray cwd.
|
|
60
|
+
function makeBare() {
|
|
61
|
+
return realpathSync(mkdtempSync(path.join(tmpdir(), 'r89-bare-')));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---- the F6 regression: instance root follows the invocation cwd ----------
|
|
65
|
+
t('consumer run from its own dir: INSTANCE_ROOT resolves to the consumer (not the core)', () => {
|
|
66
|
+
const consumer = makeConsumer();
|
|
67
|
+
const r = printRoots({ cwd: consumer });
|
|
68
|
+
assert.equal(realpathSync(r.INSTANCE_ROOT), consumer, 'instance root must be the consumer dir');
|
|
69
|
+
assert.notEqual(realpathSync(r.INSTANCE_ROOT), REPO_REAL, 'must NOT fall back to the script-relative core root');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
t('consumer via npm from elsewhere: INIT_CWD wins over the spawn cwd', () => {
|
|
73
|
+
const consumer = makeConsumer();
|
|
74
|
+
const elsewhere = makeBare();
|
|
75
|
+
// npm sets INIT_CWD to where `npm run` was invoked (the instance), even if a
|
|
76
|
+
// wrapper cd'd somewhere else first.
|
|
77
|
+
const r = printRoots({ cwd: elsewhere, initCwd: consumer });
|
|
78
|
+
assert.equal(realpathSync(r.INSTANCE_ROOT), consumer, 'INIT_CWD (the instance) must win');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ---- the monolith / no-pointer fallback stays script-relative -------------
|
|
82
|
+
t('no bongos.config + no env: INSTANCE_ROOT falls back to the script-relative repo root', () => {
|
|
83
|
+
const bare = makeBare();
|
|
84
|
+
const r = printRoots({ cwd: bare });
|
|
85
|
+
assert.equal(realpathSync(r.INSTANCE_ROOT), REPO_REAL, 'a stray cwd must NOT masquerade as the instance');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ---- CORE root is always script-relative, regardless of cwd ---------------
|
|
89
|
+
t('CORE_ROOT is always the script-relative repo root, independent of the invocation cwd', () => {
|
|
90
|
+
const consumer = makeConsumer();
|
|
91
|
+
const bare = makeBare();
|
|
92
|
+
assert.equal(realpathSync(printRoots({ cwd: consumer }).CORE_ROOT), REPO_REAL);
|
|
93
|
+
assert.equal(realpathSync(printRoots({ cwd: bare }).CORE_ROOT), REPO_REAL);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.log(`\nmigrate_root_resolution: ${passed} passed, ${failed} failed`);
|
|
97
|
+
process.exit(failed ? 1 : 0);
|