@bongos/core 1.20.5 → 1.20.6
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 +102 -52
- package/.claude/skills/goal-review/SKILL.md +16 -24
- package/.claude/skills/goal-uat/SKILL.md +84 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +12 -0
- package/clients/bongos-client/index.cjs +12 -0
- package/clients/bongos-client/index.d.ts +18 -1
- package/clients/bongos-client/index.mjs +12 -0
- package/docs/adr/0183-criteria-close-themselves.md +1 -1
- package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
- package/docs/adr/0351-a-criterion-closes-on-a-uat.md +73 -0
- package/docs/adr/README.md +28 -0
- package/docs/api/openapi.json +385 -5
- package/docs/api-reference.md +14 -4
- package/docs/architecture.md +7 -0
- package/docs/copy-inventory.md +22 -22
- package/docs/copy-registry.json +23 -23
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +3 -1
- package/docs/page-readings.json +3 -3
- package/modules/hall-ui/public/goals-page.js +8 -3
- package/modules/hall-ui/public/tweak-editor.css +4 -7
- package/modules/hall-ui/public/tweak-editor.html +3 -3
- package/modules/hall-ui/public/tweak-editor.js +3 -0
- package/modules/lifecycle/criterion-uat-db.js +267 -0
- package/modules/lifecycle/criterion-uat.js +303 -0
- package/modules/lifecycle/db-goals.js +25 -12
- package/modules/lifecycle/done-when.js +84 -17
- package/modules/lifecycle/migrations/lifecycle_015_criterion_uat.sql +73 -0
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/routes/criterion-uat.js +169 -0
- package/modules/lifecycle/routes/done-when.js +25 -1
- package/modules/npm-release/module.json +3 -1
- package/modules/npm-release/routes/task-where.js +18 -1
- package/modules/npm-release/work.js +38 -7
- package/modules/specialities/routes/specialities.js +8 -1
- package/modules/specialities/specialities.js +26 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +18 -0
- package/scripts/gds/cli-lib.js +4 -1
- package/scripts/gds/fitness-checks-write-validation.js +4 -0
- package/scripts/gds/status.js +14 -3
- package/scripts/gds/uat.js +120 -0
- package/src/bongos/route-rank-check.js +9 -0
- package/src/module-api.js +1 -1
- package/tests/auto_satisfy_criteria.mjs +4 -2
- package/tests/criterion_uat.mjs +467 -0
- package/tests/criterion_uat_routes.mjs +232 -0
- package/tests/fitness.mjs +3 -1
- package/tests/goal_achievement.mjs +4 -2
- package/tests/goal_routes.mjs +4 -2
- package/tests/ideator_full_idea_shapes_space_proof.mjs +3 -2
- package/tests/npm_release_where.mjs +18 -2
- package/tests/speciality_session_skills.mjs +150 -0
|
@@ -204,7 +204,23 @@ function canAdopt(speciality, viewer) {
|
|
|
204
204
|
// characters and a session cannot carry that; what the contract carries is the
|
|
205
205
|
// speciality's terms plus an inventory of what it knows, so the agent knows to
|
|
206
206
|
// GO AND SEARCH rather than believing the subject is undocumented.
|
|
207
|
-
|
|
207
|
+
// The one sentence naming the enabled skills, or null — SILENT when nothing is on
|
|
208
|
+
// (the common state after part 2), never an empty list. Only a skill still both
|
|
209
|
+
// OFFERED and (when `installed` is known) INSTALLED is named: the PUT checked
|
|
210
|
+
// both, but an author can shrink the offer and a deploy can drop a skill later,
|
|
211
|
+
// and advising a session to reach for a skill it cannot find is noise.
|
|
212
|
+
function enabledSkillsLine(active, installed = null) {
|
|
213
|
+
const enabled = Array.isArray(active && active.enabled_skills) ? active.enabled_skills : [];
|
|
214
|
+
const offered = new Set(Array.isArray(active && active.skills) ? active.skills : []);
|
|
215
|
+
const names = [...new Set(enabled)].filter((n) => typeof n === 'string' && SKILL_NAME_RE.test(n)
|
|
216
|
+
&& offered.has(n) && (!installed || installed.has(n)));
|
|
217
|
+
if (!names.length) return null;
|
|
218
|
+
return `In this speciality the builder chose to lean on these skills: ${names.map((n) => `/${n}`).join(', ')}. `
|
|
219
|
+
+ 'Reach for them when the work calls for one. This is advice, not permission — '
|
|
220
|
+
+ 'each still runs only if the builder\'s rank allows it.';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function describeForApi(active, { documentCount = 0, learningCount = 0, selfAuthored = true, authorLabel = null, installed = null } = {}) {
|
|
208
224
|
if (!active) return null;
|
|
209
225
|
const parts = [];
|
|
210
226
|
// Conditional, not unconditional: an unnamed speciality has nothing to announce,
|
|
@@ -243,6 +259,15 @@ function describeForApi(active, { documentCount = 0, learningCount = 0, selfAuth
|
|
|
243
259
|
);
|
|
244
260
|
}
|
|
245
261
|
|
|
262
|
+
// THE ENABLED SKILLS (task 1004074, ADR 0310 part 3). What THIS builder turned
|
|
263
|
+
// on in the walkthrough, so the speciality changes what the session reaches for.
|
|
264
|
+
// After the provenance fence on purpose: the list is the adopter's own choice,
|
|
265
|
+
// not the author's prose. Advice only — naming a skill grants nothing, and the
|
|
266
|
+
// line says so, because a session told "use X" by a builder who cannot run X
|
|
267
|
+
// should hit the rank gate, not believe the speciality opened it.
|
|
268
|
+
const skillsLine = enabledSkillsLine(active, installed);
|
|
269
|
+
if (skillsLine) parts.push(skillsLine);
|
|
270
|
+
|
|
246
271
|
const contract = parts.filter(Boolean).join('\n');
|
|
247
272
|
// inject:false for an empty contract, matching how the wandering knob stays
|
|
248
273
|
// silent on its permissive levels rather than spending context saying nothing.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.6",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.6",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.6",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8066,9 +8066,27 @@
|
|
|
8066
8066
|
}
|
|
8067
8067
|
],
|
|
8068
8068
|
"1.20.5": [
|
|
8069
|
+
{
|
|
8070
|
+
"id": "1002456",
|
|
8071
|
+
"text": "The list of decision records now says plainly which 20 numbers are shared by two documents, and tells everyone to cite a record by its file name so nobody opens the wrong one."
|
|
8072
|
+
},
|
|
8073
|
+
{
|
|
8074
|
+
"id": "1004394",
|
|
8075
|
+
"text": "Code tidy-up in the tweak editor: an out-of-date comment fixed. Nothing changes for artists."
|
|
8076
|
+
},
|
|
8077
|
+
{
|
|
8078
|
+
"id": "1004074",
|
|
8079
|
+
"text": "The skills you switch on for a speciality now follow you into your work sessions. Your assistant is told which ones you chose to lean on, and only in sessions for that craft. Switch nothing on and nothing extra is said. It is"
|
|
8080
|
+
},
|
|
8069
8081
|
{
|
|
8070
8082
|
"id": "1004391",
|
|
8071
8083
|
"text": "Wrote down a rule: when several linked tasks are claimed together, whoever claims them must finish them in order. Nothing enforces it yet, by the owner's choice."
|
|
8072
8084
|
}
|
|
8085
|
+
],
|
|
8086
|
+
"1.20.6": [
|
|
8087
|
+
{
|
|
8088
|
+
"id": "1004392",
|
|
8089
|
+
"text": "A goal's checklist item no longer ticks itself off just because some linked work shipped. Once the work is done it now shows \"Awaiting UAT\", and it closes only when someone who did not build it tries it on the live site and up"
|
|
8090
|
+
}
|
|
8073
8091
|
]
|
|
8074
8092
|
}
|
package/scripts/gds/cli-lib.js
CHANGED
|
@@ -680,10 +680,13 @@ async function apiCall(method, urlPath, body = null, opts = {}) {
|
|
|
680
680
|
// account billing, so identity is not an auth boundary. anthropic_user_id is
|
|
681
681
|
// still kept in the session as cosmetic attribution; it is simply not
|
|
682
682
|
// transmitted as a header.
|
|
683
|
+
// opts.rawBody (a Buffer) + opts.contentType send bytes instead of JSON — a
|
|
684
|
+
// UAT recording upload (task 1004392), whose route reads a raw video body.
|
|
685
|
+
if (opts.rawBody) headers['Content-Type'] = opts.contentType || 'application/octet-stream';
|
|
683
686
|
const res = await dispatchedFetch(`${base}${urlPath}`, {
|
|
684
687
|
method,
|
|
685
688
|
headers,
|
|
686
|
-
body: body ? JSON.stringify(body) : null,
|
|
689
|
+
body: opts.rawBody ? opts.rawBody : (body ? JSON.stringify(body) : null),
|
|
687
690
|
dispatcher: noKeepAliveDispatcher(),
|
|
688
691
|
});
|
|
689
692
|
const text = await res.text();
|
|
@@ -50,6 +50,10 @@ const WRITE_VALIDATION_EXEMPT = new Set([
|
|
|
50
50
|
// the route's express.raw parser and read by module-artifact.verifyModuleArtifact, which
|
|
51
51
|
// recomputes every hash and refuses anything that is not a well-formed module version.
|
|
52
52
|
'POST /store/modules/:key/versions',
|
|
53
|
+
// Not a JSON body: the raw UAT recording (task 1004392, ADR 0351), capped by the
|
|
54
|
+
// route's express.raw parser and read by criterion-uat.screenRecording, which
|
|
55
|
+
// refuses anything that is not a bounded mp4/webm whose bytes match its type.
|
|
56
|
+
'POST /done-when/:criterionId/uat/recording',
|
|
53
57
|
]);
|
|
54
58
|
|
|
55
59
|
// KNOWN GAPS, NOT EXEMPTIONS (task 1002551 / BV1.R109b → tracked by task 1002552).
|
package/scripts/gds/status.js
CHANGED
|
@@ -37,6 +37,17 @@
|
|
|
37
37
|
const { API_BASE, cliClient, assertInstanceMatch, cliExit } = require('./cli-lib');
|
|
38
38
|
const { resolveEnv } = require('../../src/instance-config');
|
|
39
39
|
const { resolveDefaultVersion } = require('./version-select.js');
|
|
40
|
+
// task 1004392 (ADR 0351): the criterion's UAT state, in the same words the
|
|
41
|
+
// server and /goal-uat use. Dependency-free, so this public CLI pays nothing.
|
|
42
|
+
const { stateLabel } = require('../../modules/lifecycle/criterion-uat.js');
|
|
43
|
+
|
|
44
|
+
// One criterion's state as a short phrase. A server older than the UAT rule
|
|
45
|
+
// sends no uat_state, and then the old satisfied/not answer is the truth.
|
|
46
|
+
function criterionStateText(c) {
|
|
47
|
+
if (!c.uat_state) return c.satisfied ? 'satisfied ✓' : 'not satisfied';
|
|
48
|
+
const label = stateLabel(c.uat_state);
|
|
49
|
+
return c.satisfied ? `${label} ✓` : label;
|
|
50
|
+
}
|
|
40
51
|
const {
|
|
41
52
|
looksLikeVersionId,
|
|
42
53
|
resolveCriterionIn,
|
|
@@ -270,7 +281,7 @@ function renderStatusWidget(data, criterion) {
|
|
|
270
281
|
return `${STATUS_WIDGET_STYLE}
|
|
271
282
|
<h2 class="st-sr">${sesc(data.version_id)} criterion ${sesc(criterionLabel(c))} ${c.satisfied ? 'is satisfied' : 'is not yet satisfied'}: ${c.counts.shipped} of ${c.counts.total} gating tasks shipped.</h2>
|
|
272
283
|
<div class="st-card">
|
|
273
|
-
<div class="st-head"><span class="st-title"><i class="ti ti-checklist" aria-hidden="true"></i>${sesc(data.version_id)} · ${sesc(criterionLabel(c))}</span><span class="st-meta">${c.satisfied ?
|
|
284
|
+
<div class="st-head"><span class="st-title"><i class="ti ti-checklist" aria-hidden="true"></i>${sesc(data.version_id)} · ${sesc(criterionLabel(c))}</span><span class="st-meta">${c.satisfied ? `<span class="st-ok">${sesc(criterionStateText(c))}</span>` : sesc(criterionStateText(c))} · ${c.counts.shipped}/${c.counts.total} shipped</span></div>
|
|
274
285
|
<div class="st-crit">${sesc(c.criterion_md.replace(/\*\*/g, '').slice(0, 160))}</div>
|
|
275
286
|
${body}
|
|
276
287
|
<div class="st-foot">remaining: ${sesc(remainingSummary(c.remaining))}${claimHint}</div>
|
|
@@ -314,7 +325,7 @@ function renderStatusMarkdown(data, criterion) {
|
|
|
314
325
|
const lines = [];
|
|
315
326
|
if (criterion) {
|
|
316
327
|
const c = criterion;
|
|
317
|
-
lines.push(`**/status · ${data.version_id} · ${criterionLabel(c)}** — ${c
|
|
328
|
+
lines.push(`**/status · ${data.version_id} · ${criterionLabel(c)}** — ${criterionStateText(c)}`);
|
|
318
329
|
lines.push(`_${c.criterion_md.replace(/\*\*/g, '').slice(0, 160)}_`);
|
|
319
330
|
lines.push('');
|
|
320
331
|
if (c.tasks.length) {
|
|
@@ -405,7 +416,7 @@ function printAll(data) {
|
|
|
405
416
|
}
|
|
406
417
|
|
|
407
418
|
function printOne(data, c) {
|
|
408
|
-
console.log(`\n${data.version_id} · ${c.criterion_id} (C${c.cnum}) [${c
|
|
419
|
+
console.log(`\n${data.version_id} · ${c.criterion_id} (C${c.cnum}) [${criterionStateText(c)}]`);
|
|
409
420
|
console.log(`"${c.criterion_md.replace(/\*\*/g, '').slice(0, 120)}"\n`);
|
|
410
421
|
if (c.tasks.length === 0) {
|
|
411
422
|
console.log(' (no tasks linked — this criterion has no structured gating tasks yet)\n');
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/uat.js — see, and sign off, a criterion's UAT from a session
|
|
3
|
+
// (task 1004392, goal 1000089; ADR 0351). The CLI half of /goal-uat.
|
|
4
|
+
//
|
|
5
|
+
// WHY. A criterion no longer closes because its linked tasks shipped. It reads
|
|
6
|
+
// "Awaiting UAT" until someone who did not ship the work performs it on the live
|
|
7
|
+
// site and signs it off with a recording (or, for a criterion marked
|
|
8
|
+
// backend-only, a recording-free sign-off). This is how a session does that
|
|
9
|
+
// before the hall has buttons for it (task 1004402), and what /goal-uat drives.
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// node scripts/gds/uat.js --queue [--version V] every criterion awaiting UAT
|
|
13
|
+
// node scripts/gds/uat.js <criterion-id> its three checks + sign-offs
|
|
14
|
+
// node scripts/gds/uat.js <criterion-id> --recording <file.mp4|.webm> [--live-attested] [--note "..."]
|
|
15
|
+
// node scripts/gds/uat.js <criterion-id> --backend-signoff [--live-attested] [--note "..."]
|
|
16
|
+
// node scripts/gds/uat.js <criterion-id> --backend-only on|off
|
|
17
|
+
//
|
|
18
|
+
// <criterion-id> is the criterion's numeric id (the `id` /status and the queue
|
|
19
|
+
// print), not its slug. --live-attested is only needed where this site cannot
|
|
20
|
+
// tell on its own whether the work is live; the server says so when it is.
|
|
21
|
+
|
|
22
|
+
const { apiCall, arg, argText, hasFlag, cliExit } = require('./cli-lib');
|
|
23
|
+
const uat = require('../../modules/lifecycle/criterion-uat');
|
|
24
|
+
|
|
25
|
+
const API = '/api/bongos';
|
|
26
|
+
|
|
27
|
+
function refusalText(data) {
|
|
28
|
+
const e = data && data.error;
|
|
29
|
+
if (!e) return 'the server refused it.';
|
|
30
|
+
const msg = typeof e === 'string' ? e : (e.message || e.code);
|
|
31
|
+
const extra = [];
|
|
32
|
+
const details = (typeof e === 'object' && e.details) || data;
|
|
33
|
+
if (details && Array.isArray(details.remaining) && details.remaining.length) {
|
|
34
|
+
extra.push(`still open: ${details.remaining.map((t) => `task ${t.id} (${t.status})`).join(', ')}`);
|
|
35
|
+
}
|
|
36
|
+
if (details && Array.isArray(details.not_live) && details.not_live.length) {
|
|
37
|
+
extra.push(`not live yet: ${details.not_live.map((t) => `task ${t.task_id}${t.stage ? ` (${t.stage})` : ''}`).join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
return [msg, ...extra].join('\n ');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function showQueue() {
|
|
43
|
+
const version = arg('--version');
|
|
44
|
+
const q = version ? `?version=${encodeURIComponent(version)}` : '';
|
|
45
|
+
const r = await apiCall('GET', `${API}/done-when/pending-review${q}`);
|
|
46
|
+
if (!r.ok) { console.error(`Could not read the queue: ${refusalText(r.data)}`); return 1; }
|
|
47
|
+
const rows = (r.data.criteria || []).filter((c) => c.awaiting_uat);
|
|
48
|
+
if (!rows.length) { console.log('Nothing is awaiting UAT.'); return 0; }
|
|
49
|
+
console.log(`${rows.length} criteri${rows.length === 1 ? 'on' : 'a'} awaiting UAT:\n`);
|
|
50
|
+
for (const c of rows) {
|
|
51
|
+
const tag = c.backend_only ? ' [backend-only]' : '';
|
|
52
|
+
console.log(` criterion ${c.id} ${c.criterion_id}${tag}`);
|
|
53
|
+
console.log(` goal ${c.goal_id ?? '-'}${c.goal_title ? ` "${c.goal_title}"` : ''}${c.is_last_in_goal ? ' (last open criterion: signing it off can achieve the goal)' : ''}`);
|
|
54
|
+
}
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function showOne(id) {
|
|
59
|
+
const r = await apiCall('GET', `${API}/done-when/${id}/uat`);
|
|
60
|
+
if (!r.ok) { console.error(`Could not read criterion ${id}: ${refusalText(r.data)}`); return 1; }
|
|
61
|
+
const v = r.data;
|
|
62
|
+
console.log(`criterion ${v.criterion.id} ${v.criterion.criterion_id} — ${v.uat_label}${v.backend_only ? ' [backend-only]' : ''}\n`);
|
|
63
|
+
console.log(String(v.criterion.criterion_md || '').trim());
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log(` Code ${v.checks.code ? 'passed' : 'not yet'} (${v.linked_tasks.length} linked: ${v.linked_tasks.map((t) => `${t.id} ${t.status}`).join(', ') || 'none'})`);
|
|
66
|
+
const live = v.checks.live;
|
|
67
|
+
console.log(` Live ${live == null ? '-' : !live.available ? 'this site cannot tell; the signer confirms it' : live.live ? 'running on the site' : `not yet: ${(live.not_live || []).map((t) => t.task_id).join(', ')}`}`);
|
|
68
|
+
const s = v.checks.uat;
|
|
69
|
+
console.log(` UAT ${s ? `${s.kind === 'uat' ? 'recorded' : 'backend sign-off'} by ${s.signed_off_by_login || s.signed_off_by} on ${String(s.created_at).slice(0, 10)}` : 'not yet'}`);
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function signOff(id, kind) {
|
|
74
|
+
let recording = null;
|
|
75
|
+
if (kind === 'uat') {
|
|
76
|
+
const file = arg('--recording');
|
|
77
|
+
const read = uat.readRecordingFile(file);
|
|
78
|
+
if (!read.ok) { console.error(`Recording refused: ${read.message}`); return 1; }
|
|
79
|
+
const up = await apiCall('POST', `${API}/done-when/${id}/uat/recording`, null, { rawBody: read.buf, contentType: read.contentType });
|
|
80
|
+
if (!up.ok) { console.error(`Upload refused: ${refusalText(up.data)}`); return 1; }
|
|
81
|
+
recording = up.data.recording;
|
|
82
|
+
console.log(`Uploaded the recording (${Math.round(up.data.bytes / 1024 / 1024 * 10) / 10} MB).`);
|
|
83
|
+
}
|
|
84
|
+
const body = { kind };
|
|
85
|
+
if (recording) body.recording = recording;
|
|
86
|
+
if (hasFlag('--live-attested')) body.live_attested = true;
|
|
87
|
+
const note = argText('--note');
|
|
88
|
+
if (note) body.note = note;
|
|
89
|
+
const r = await apiCall('POST', `${API}/done-when/${id}/uat`, body);
|
|
90
|
+
if (!r.ok) { console.error(`Sign-off refused: ${refusalText(r.data)}`); return 1; }
|
|
91
|
+
console.log(r.data.criterion_closed ? `Signed off. Criterion ${id} is now closed.` : `Signed off, but criterion ${id} did not close yet.`);
|
|
92
|
+
for (const g of r.data.achieved_goals || []) console.log(` That was the goal's last criterion: goal ${g} is achieved.`);
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function setBackendOnly(id, value) {
|
|
97
|
+
if (value !== 'on' && value !== 'off') { console.error('--backend-only takes on or off.'); return 1; }
|
|
98
|
+
const r = await apiCall('PUT', `${API}/done-when/${id}/backend-only`, { backend_only: value === 'on' });
|
|
99
|
+
if (!r.ok) { console.error(`Refused: ${refusalText(r.data)}`); return 1; }
|
|
100
|
+
console.log(`Criterion ${id} is ${r.data.backend_only ? 'now marked backend-only' : 'no longer backend-only'}.`);
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function main() {
|
|
105
|
+
if (hasFlag('--queue')) return showQueue();
|
|
106
|
+
const id = process.argv[2];
|
|
107
|
+
if (!id || !/^\d{1,12}$/.test(id)) {
|
|
108
|
+
console.error('usage: node scripts/gds/uat.js <criterion-id> [--recording f | --backend-signoff | --backend-only on|off] | --queue');
|
|
109
|
+
return 2;
|
|
110
|
+
}
|
|
111
|
+
if (arg('--backend-only')) return setBackendOnly(id, arg('--backend-only'));
|
|
112
|
+
if (hasFlag('--backend-signoff')) return signOff(id, 'backend_signoff');
|
|
113
|
+
if (arg('--recording')) return signOff(id, 'uat');
|
|
114
|
+
return showOne(id);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
main().then((code) => cliExit(code), (err) => {
|
|
118
|
+
console.error(`uat: ${err && err.message}`);
|
|
119
|
+
cliExit(1);
|
|
120
|
+
});
|
|
@@ -425,6 +425,15 @@ const MODULE_EXPECTED_RANKS = {
|
|
|
425
425
|
// — but it is scope disposition, not an authority grant, so it delegates with the
|
|
426
426
|
// rest. Pinned by atom so the guarded invariant is the permission, not its floor.
|
|
427
427
|
'POST /done-when/:criterionId/unsatisfy': 'perm:criterion.unsatisfy',
|
|
428
|
+
// task 1004392 (ADR 0351): a criterion closes on a UAT sign-off. Uploading
|
|
429
|
+
// the recording and signing off ride `criterion.review`, the review queue's
|
|
430
|
+
// own atom, because the sign-off IS that review now; the wall that matters is
|
|
431
|
+
// in-handler (nobody who shipped the linked work may sign it off). Marking a
|
|
432
|
+
// criterion backend-only decides whether a recording is needed at all, so it
|
|
433
|
+
// is an AUTHORING act: `criterion.create` plus authorizeCriterionCreate.
|
|
434
|
+
'POST /done-when/:criterionId/uat/recording': 'perm:criterion.review',
|
|
435
|
+
'POST /done-when/:criterionId/uat': 'perm:criterion.review',
|
|
436
|
+
'PUT /done-when/:criterionId/backend-only': 'perm:criterion.create',
|
|
428
437
|
// gate-surface PR approval from the hall (task 1179, ADR 0058 amendment). ADR 0157
|
|
429
438
|
// SPLIT these two: the list route is `gate_approval.read` (metic — the Gate page
|
|
430
439
|
// opens to the working rank), while APPROVING still clears the gate-review hard
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.20.
|
|
78
|
+
const CORE_VERSION = '1.20.6'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -139,12 +139,14 @@ await test('EXEC: shipTask passes its transaction CLIENT, and the reconciler pas
|
|
|
139
139
|
await test('SCOPED vs UNSCOPED: the taskId parameter is what narrows the candidate set', async () => {
|
|
140
140
|
const scoped = fakeExec({ rows: [] });
|
|
141
141
|
await autoSatisfyShippedCriteria(scoped, { taskId: 42 });
|
|
142
|
-
|
|
142
|
+
// task 1004392 added a second scope ($2, the sign-off route's criterionId); the ship
|
|
143
|
+
// hook leaves it null.
|
|
144
|
+
assert.deepEqual(scoped.calls[0].params, [42, null]);
|
|
143
145
|
assert.match(scoped.calls[0].sql, /\$1::bigint IS NULL OR c\.id IN/, 'one SQL serves both shapes');
|
|
144
146
|
|
|
145
147
|
const sweep = fakeExec({ rows: [] });
|
|
146
148
|
await autoSatisfyShippedCriteria(sweep, {});
|
|
147
|
-
assert.deepEqual(sweep.calls[0].params, [null], 'unscoped passes NULL, which disables the narrowing clause');
|
|
149
|
+
assert.deepEqual(sweep.calls[0].params, [null, null], 'unscoped passes NULL, which disables the narrowing clause');
|
|
148
150
|
});
|
|
149
151
|
|
|
150
152
|
await test('NO-OP: nothing closed means no cascade query at all', async () => {
|