@bongos/core 1.19.893 → 1.19.894
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 +27 -17
- package/.claude/skills/status/SKILL.md +5 -5
- package/docs/module-api-changelog.md +2 -0
- package/docs/onboarding/slash-commands.md +1 -1
- package/modules/lifecycle/criterion-ref.js +107 -0
- package/modules/lifecycle/db-deps-criteria.js +11 -11
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/build-cli-package.js +5 -0
- package/scripts/gds/status.js +107 -28
- package/src/module-api.js +1 -1
- package/tests/criterion_ref_status.mjs +202 -0
- package/tests/session-cards.mjs +36 -3
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.894",
|
|
6
|
+
"core_contract": "1.19.894",
|
|
7
|
+
"source_commit": "a62eeb34dfae8175f0ff3586f38460f044b6bc2b",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-19T22:
|
|
9
|
+
"built_at": "2026-09-19T22:40:59.707Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 498,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2274,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2798,
|
|
20
|
+
"tree_sha256": "22cc76ae6f3b3fe04849fa744d2d7b43b859f159332e23bee866c374a82b15a7",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -242,7 +242,7 @@
|
|
|
242
242
|
{
|
|
243
243
|
"path": ".claude/skills/status/SKILL.md",
|
|
244
244
|
"mode": "0000644",
|
|
245
|
-
"sha256": "
|
|
245
|
+
"sha256": "2613bd488e5ec83681effe72b40d7b9c9152bfae401c9ca484c6b84f6d349373"
|
|
246
246
|
},
|
|
247
247
|
{
|
|
248
248
|
"path": ".claude/skills/strand-fix/SKILL.md",
|
|
@@ -2612,7 +2612,7 @@
|
|
|
2612
2612
|
{
|
|
2613
2613
|
"path": "docs/module-api-changelog.md",
|
|
2614
2614
|
"mode": "0000644",
|
|
2615
|
-
"sha256": "
|
|
2615
|
+
"sha256": "6fb5bea1a5ba196287323307cc7272e869defbaa91d4f5ef522a92436064847f"
|
|
2616
2616
|
},
|
|
2617
2617
|
{
|
|
2618
2618
|
"path": "docs/modules-contract.md",
|
|
@@ -2682,7 +2682,7 @@
|
|
|
2682
2682
|
{
|
|
2683
2683
|
"path": "docs/onboarding/slash-commands.md",
|
|
2684
2684
|
"mode": "0000644",
|
|
2685
|
-
"sha256": "
|
|
2685
|
+
"sha256": "92ce9c4de763330c4ac877a1ba79d0b11d88797c711b3517adebe8604bdf59ea"
|
|
2686
2686
|
},
|
|
2687
2687
|
{
|
|
2688
2688
|
"path": "docs/packs/artist.md",
|
|
@@ -5409,6 +5409,11 @@
|
|
|
5409
5409
|
"mode": "0000644",
|
|
5410
5410
|
"sha256": "107dec0deb790b7439da27761235631ab79a29a6cfcea58db9a57460e04df1bd"
|
|
5411
5411
|
},
|
|
5412
|
+
{
|
|
5413
|
+
"path": "modules/lifecycle/criterion-ref.js",
|
|
5414
|
+
"mode": "0000644",
|
|
5415
|
+
"sha256": "c79a34e577df14c4c54d426e60789c5e30bc2e81fd4005a283f21936b9f0aba7"
|
|
5416
|
+
},
|
|
5412
5417
|
{
|
|
5413
5418
|
"path": "modules/lifecycle/criterion-suggest.js",
|
|
5414
5419
|
"mode": "0000644",
|
|
@@ -5432,7 +5437,7 @@
|
|
|
5432
5437
|
{
|
|
5433
5438
|
"path": "modules/lifecycle/db-deps-criteria.js",
|
|
5434
5439
|
"mode": "0000644",
|
|
5435
|
-
"sha256": "
|
|
5440
|
+
"sha256": "e921d48860af27f836252fa7e44e1bdae25be5b940c25e9272f1cf368805d132"
|
|
5436
5441
|
},
|
|
5437
5442
|
{
|
|
5438
5443
|
"path": "modules/lifecycle/db-executor.js",
|
|
@@ -7712,12 +7717,12 @@
|
|
|
7712
7717
|
{
|
|
7713
7718
|
"path": "package-lock.json",
|
|
7714
7719
|
"mode": "0000644",
|
|
7715
|
-
"sha256": "
|
|
7720
|
+
"sha256": "5a7c7abe37561006b741b013bd299d254bac0267a5da69e05c15bd5df7f804ba"
|
|
7716
7721
|
},
|
|
7717
7722
|
{
|
|
7718
7723
|
"path": "package.json",
|
|
7719
7724
|
"mode": "0000644",
|
|
7720
|
-
"sha256": "
|
|
7725
|
+
"sha256": "78a3e6e4967ac1ffcd9fef0be950247551d3c739763d30cfc98aad8dd0dd5168"
|
|
7721
7726
|
},
|
|
7722
7727
|
{
|
|
7723
7728
|
"path": "public-docs/index.html",
|
|
@@ -8022,7 +8027,7 @@
|
|
|
8022
8027
|
{
|
|
8023
8028
|
"path": "scripts/gds/build-cli-package.js",
|
|
8024
8029
|
"mode": "0000644",
|
|
8025
|
-
"sha256": "
|
|
8030
|
+
"sha256": "fcb5bdfe3341b177018a080c2cd7295bad3b1772696e61bef6154ec1e5767300"
|
|
8026
8031
|
},
|
|
8027
8032
|
{
|
|
8028
8033
|
"path": "scripts/gds/bump-version.js",
|
|
@@ -9212,7 +9217,7 @@
|
|
|
9212
9217
|
{
|
|
9213
9218
|
"path": "scripts/gds/status.js",
|
|
9214
9219
|
"mode": "0000644",
|
|
9215
|
-
"sha256": "
|
|
9220
|
+
"sha256": "cdf231a4804965b5cd4ca6218893573319d318b51356bf87c2cddeeccd523dd2"
|
|
9216
9221
|
},
|
|
9217
9222
|
{
|
|
9218
9223
|
"path": "scripts/gds/strand-watch.js",
|
|
@@ -9722,7 +9727,7 @@
|
|
|
9722
9727
|
{
|
|
9723
9728
|
"path": "src/module-api.js",
|
|
9724
9729
|
"mode": "0000644",
|
|
9725
|
-
"sha256": "
|
|
9730
|
+
"sha256": "61c6a9f0360af4f9f936786eacffe6b231db6f0f0339b7ba206511136823d2ee"
|
|
9726
9731
|
},
|
|
9727
9732
|
{
|
|
9728
9733
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10719,6 +10724,11 @@
|
|
|
10719
10724
|
"mode": "0000644",
|
|
10720
10725
|
"sha256": "80ce711abf62a78dc4f36b513b852a5396173a2ff50e5deab2460531d17911e4"
|
|
10721
10726
|
},
|
|
10727
|
+
{
|
|
10728
|
+
"path": "tests/criterion_ref_status.mjs",
|
|
10729
|
+
"mode": "0000644",
|
|
10730
|
+
"sha256": "a441876b05922aeebb5fa122a679a836ccf322e8d96e8a28acf0c7c076283741"
|
|
10731
|
+
},
|
|
10722
10732
|
{
|
|
10723
10733
|
"path": "tests/criterion_suggest.mjs",
|
|
10724
10734
|
"mode": "0000644",
|
|
@@ -13167,7 +13177,7 @@
|
|
|
13167
13177
|
{
|
|
13168
13178
|
"path": "tests/session-cards.mjs",
|
|
13169
13179
|
"mode": "0000644",
|
|
13170
|
-
"sha256": "
|
|
13180
|
+
"sha256": "62dbeb24b93c9680ee2cf4427ca64da2532588cdfb75deaa329c1cfc9c122085"
|
|
13171
13181
|
},
|
|
13172
13182
|
{
|
|
13173
13183
|
"path": "tests/session_506_regression.mjs",
|
|
@@ -12,19 +12,19 @@ Rolls up a version's done-when criteria → gating tasks → live statuses → w
|
|
|
12
12
|
|
|
13
13
|
## How to render it
|
|
14
14
|
|
|
15
|
-
Delivery is deterministic (task 1288, same methodology as `/builder-start`). The **card-delivery hook** (`.claude/hooks/session-cards.js`) runs `status.js` for you on a `/status` (or "what's left for C8" / "status of GDS-V4") prompt — pulling any version + `Cn` out of the prompt — and injects the **finished card + a one-step directive** into your context, a block beginning `[otb card — the status card below was pre-rendered …]`.
|
|
15
|
+
Delivery is deterministic (task 1288, same methodology as `/builder-start`). The **card-delivery hook** (`.claude/hooks/session-cards.js`) runs `status.js` for you on a `/status` (or "what's left for C8" / "status of GDS-V4") prompt — pulling any version + criterion ref (a slug or a positional `Cn`) out of the prompt — and injects the **finished card + a one-step directive** into your context, a block beginning `[otb card — the status card below was pre-rendered …]`.
|
|
16
16
|
|
|
17
17
|
1. **Normal path — follow the injected directive.** If that `[otb card …]` block is present, do exactly what it says: `show_widget` with the fenced HTML if you have `mcp__visualize` (call `mcp__visualize__read_me` once first, silently), otherwise output the fenced markdown **verbatim**. Render **once** — don't run `status.js`, don't lead with your own headline, don't rebuild the table, don't add a recommendation or recap. The card now owns the headline, the remaining-task list, and the unattributed-tasks note (this replaces the old freeform "interpret + relay" summary).
|
|
18
18
|
|
|
19
19
|
2. **Fallback — no directive present** (hook disabled/failed, or an older checkout). Run it yourself:
|
|
20
|
-
- **If you have `show_widget`**: `node scripts/gds/status.js --widget [--version ID] [
|
|
21
|
-
- **No widget renderer**: `node scripts/gds/status.js [--version ID] [
|
|
22
|
-
-
|
|
20
|
+
- **If you have `show_widget`**: `node scripts/gds/status.js --widget [--version ID] [--criterion REF]` → render the HTML verbatim with `title: "status_card"` (call `read_me` once first).
|
|
21
|
+
- **No widget renderer**: `node scripts/gds/status.js [--version ID] [--criterion REF]` → print its stdout verbatim.
|
|
22
|
+
- A criterion is named by its **slug** (`/status wa6-kernel-and-packs`) or, as an affordance, by position (`/status C8`); `--criterion REF` is the exact form for either. `--version` defaults to the live internal version; `--json` is the machine interface; `--hook` is the envelope the hook consumes — don't render those for the user unless asked.
|
|
23
23
|
|
|
24
24
|
## Constraints
|
|
25
25
|
|
|
26
26
|
- **Read-only.** This skill never claims, ships, or mutates anything. If the user then wants to act on a remaining task, hand off to `/builder-claim N`.
|
|
27
|
-
-
|
|
27
|
+
- **The SLUG is the criterion's identity; `Cn` is a position, and positions move** (task 1004069). `Cn` is `ROW_NUMBER()` over a reorderable `sort_order`, recomputed on every read: adding `wa6-noise-limited` on 2026-09-19 moved `<redacted>` from C11 to C13, and everything still quoting C11 silently began naming a different criterion. The write path (`resolveCriterionIds`) **refuses** a positional ref on a multi-goal version for that reason. So: **quote the slug** in anything durable — a note, a task description, a handoff, a card you expect someone to read tomorrow. Repeat a `Cn` back only alongside the slug it resolved to; `status.js` prints that line for you whenever a drill-in came in positionally.
|
|
28
28
|
- If `--version X` returns no criteria, say so plainly (that version may not have done-when criteria seeded) rather than implying 0% done.
|
|
29
29
|
|
|
30
30
|
## Files this skill touches
|
|
@@ -2260,5 +2260,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2260
2260
|
landed since 1.19.891 with no explicit bump. run 35471773244. (task 1002620)
|
|
2261
2261
|
1.19.893 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2262
2262
|
landed since 1.19.892 with no explicit bump. run 35473299957. (task 1002620)
|
|
2263
|
+
1.19.894 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2264
|
+
landed since 1.19.893 with no explicit bump. run 35473999345. (task 1002620)
|
|
2263
2265
|
---------------------------------------------------------------------------
|
|
2264
2266
|
```
|
|
@@ -32,7 +32,7 @@ Every one of these talks to the build system. They're grouped by when you'd reac
|
|
|
32
32
|
| `/builder-release` | Cancel a claim — the task returns to `ready`, no credits |
|
|
33
33
|
| `/builder-stage` | Preview your work-in-progress on your own sandbox before shipping |
|
|
34
34
|
| `/builder-end` | Close out the session safely — resolve the claim, seal the log |
|
|
35
|
-
| `/status` | Version / criterion progress — "what's left for
|
|
35
|
+
| `/status` | Version / criterion progress — "what's left for `wa6-kernel-and-packs`?" (a criterion's slug is its name; `C8` is its current position and moves) |
|
|
36
36
|
| `/recall` | Search the docs + project knowledge before re-solving something |
|
|
37
37
|
|
|
38
38
|
### Your account and your machine
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// modules/lifecycle/criterion-ref.js — the ONE vocabulary for naming a done-when
|
|
2
|
+
// criterion, shared by the write path and the read path (task 1004069).
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS FILE EXISTS. `parseCriterionRef` lived inside db-deps-criteria.js,
|
|
5
|
+
// which requires ../../src/module-api and therefore a database pool. The read
|
|
6
|
+
// side — scripts/gds/status.js, which is public, unauthenticated, and shipped in
|
|
7
|
+
// the @cloudbongos/cli package with no server present — could not require it, so
|
|
8
|
+
// it grew its own parser: `Number(ref.slice(1))`, positional only. The system
|
|
9
|
+
// ended up PRINTING an identifier its own write path refuses as unsafe, because
|
|
10
|
+
// the two sides never shared a definition of what a ref is. Carving the pure
|
|
11
|
+
// half out is what stops that from happening again; this file requires nothing,
|
|
12
|
+
// touches no I/O, and both sides import it.
|
|
13
|
+
//
|
|
14
|
+
// THE VOCABULARY. A ref is one of three things:
|
|
15
|
+
// - a positional "Cn" token → { kind: 'cnum', n } C8 = the 8th criterion by
|
|
16
|
+
// (sort_order, criterion_id) WITHIN one version.
|
|
17
|
+
// - bare digits → { kind: 'id', id } done_when_criteria.id.
|
|
18
|
+
// - anything else → { kind: 'slug', slug } the criterion_id slug.
|
|
19
|
+
//
|
|
20
|
+
// WHICH ONE IS THE IDENTITY. The slug. `Cn` is a presentation ordinal computed
|
|
21
|
+
// live (ROW_NUMBER over a user-reorderable sort_order), so inserting a criterion
|
|
22
|
+
// renumbers every criterion after it, version-wide, with no record: creating
|
|
23
|
+
// `wa6-noise-limited` on 2026-09-19 moved `wa10-execution-integrity` from C11 to
|
|
24
|
+
// C13 while every doc, card and click handler quoting C11 kept pointing at a row
|
|
25
|
+
// that had moved. `resolveCriterionIds` already REFUSES a positional ref on any
|
|
26
|
+
// version holding more than one goal, for the same reason (a positional ref
|
|
27
|
+
// mislinked all 15 tasks of goal 1000071 — learning 1000188). So `Cn` is an
|
|
28
|
+
// affordance for a human reading a numbered list, never the thing to quote or
|
|
29
|
+
// store. Do not "fix" the instability by persisting cnum as a column: that is a
|
|
30
|
+
// second source of truth over a reorderable ordering, and it drifts the first
|
|
31
|
+
// time someone reorders.
|
|
32
|
+
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
// PURE: classify a single criterion ref. Returns { kind:'cnum', n } |
|
|
36
|
+
// { kind:'id', id } | { kind:'slug', slug }.
|
|
37
|
+
// A leading C/c + digits is positional; bare digits are a numeric id; anything
|
|
38
|
+
// else is treated as a criterion_id slug. Anchored on purpose — prose like
|
|
39
|
+
// "see C8 above" is a slug, never a silent position match.
|
|
40
|
+
|
|
41
|
+
function parseCriterionRef(raw) {
|
|
42
|
+
const ref = String(raw == null ? '' : raw).trim();
|
|
43
|
+
const cMatch = /^C(\d+)$/i.exec(ref);
|
|
44
|
+
if (cMatch) return { kind: 'cnum', n: Number(cMatch[1]) };
|
|
45
|
+
if (/^\d+$/.test(ref)) return { kind: 'id', id: Number(ref) };
|
|
46
|
+
return { kind: 'slug', slug: ref };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A version id as this project writes them: an uppercase prefix (possibly none)
|
|
50
|
+
// followed by V + digits — V1, PMS-V2, GDS-V4, BONGOS-V2, CB-V1.
|
|
51
|
+
//
|
|
52
|
+
// Used ONLY to tell a bare CLI token apart from a criterion ref, because
|
|
53
|
+
// `status.js BONGOS-V2` and `status.js wa6-kernel-and-packs` are both legal and
|
|
54
|
+
// positional. Deliberately case-SENSITIVE: every version id in the versions
|
|
55
|
+
// table is uppercase and every criterion_id slug is lowercase kebab, so casing
|
|
56
|
+
// is the one signal that does not require a round trip — and a lowercase
|
|
57
|
+
// `bongos-v2` was never a working version id anyway (the progress endpoint
|
|
58
|
+
// path-matches the id verbatim). A caller that does not want to be guessed at
|
|
59
|
+
// passes the exact form: `--version ID` and `--criterion REF` skip this entirely.
|
|
60
|
+
const VERSION_ID_RE = /^(?:[A-Z][A-Z0-9]*-)*V\d+(?:\.\d+)?$/;
|
|
61
|
+
|
|
62
|
+
function looksLikeVersionId(token) {
|
|
63
|
+
return VERSION_ID_RE.test(String(token == null ? '' : token).trim());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Resolve a ref against criteria rows already fetched for ONE version. Rows need
|
|
67
|
+
// `cnum`, `criterion_id`, and (for the numeric form) `id` — the shape
|
|
68
|
+
// GET /versions/:id/progress returns.
|
|
69
|
+
//
|
|
70
|
+
// Version scope is the CALLER's job and is not optional: `Cn` numbers
|
|
71
|
+
// independently per version (BONGOS-V1 C8 is `money-one-unit`, BONGOS-V2 C8 is
|
|
72
|
+
// `wa6-kernel-and-packs`), so resolving a bare `Cn` without a version in hand
|
|
73
|
+
// answers a different question than the one asked. Passing rows from a single
|
|
74
|
+
// version's payload is what makes that structurally impossible here.
|
|
75
|
+
//
|
|
76
|
+
// Returns { criterion, kind } on a hit, or { criterion: null, kind } on a miss.
|
|
77
|
+
// Slug matching is case-insensitive so a slug pasted from a title still lands.
|
|
78
|
+
function resolveCriterionIn(criteria, ref) {
|
|
79
|
+
const rows = Array.isArray(criteria) ? criteria : [];
|
|
80
|
+
const p = parseCriterionRef(ref);
|
|
81
|
+
let criterion = null;
|
|
82
|
+
if (p.kind === 'cnum') {
|
|
83
|
+
criterion = rows.find((c) => Number(c.cnum) === p.n) || null;
|
|
84
|
+
} else if (p.kind === 'id') {
|
|
85
|
+
criterion = rows.find((c) => String(c.id) === String(p.id)) || null;
|
|
86
|
+
} else {
|
|
87
|
+
const want = p.slug.toLowerCase();
|
|
88
|
+
criterion = rows.find((c) => String(c.criterion_id || '').toLowerCase() === want) || null;
|
|
89
|
+
}
|
|
90
|
+
return { criterion, kind: p.kind };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// How a criterion is NAMED in output: the slug leads, the position trails in
|
|
94
|
+
// parentheses as the affordance it is. Never "C8" alone — that is the form that
|
|
95
|
+
// goes stale between the printing and the reading.
|
|
96
|
+
function criterionLabel(c) {
|
|
97
|
+
if (!c) return '';
|
|
98
|
+
const slug = String(c.criterion_id || '');
|
|
99
|
+
return c.cnum == null ? slug : `${slug} (C${c.cnum})`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
parseCriterionRef,
|
|
104
|
+
looksLikeVersionId,
|
|
105
|
+
resolveCriterionIn,
|
|
106
|
+
criterionLabel,
|
|
107
|
+
};
|
|
@@ -278,17 +278,17 @@ async function removeDependency(fromKind, fromId, toKind, toId) {
|
|
|
278
278
|
// ---------------------------------------------------------------------------
|
|
279
279
|
|
|
280
280
|
// PURE: classify a single criterion ref. Returns { kind:'cnum', n } |
|
|
281
|
-
// { kind:'id', id } | { kind:'slug', slug }.
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
281
|
+
// { kind:'id', id } | { kind:'slug', slug }. Re-exported below for unit tests.
|
|
282
|
+
//
|
|
283
|
+
// The DEFINITION lives in ./criterion-ref.js, which requires nothing at all —
|
|
284
|
+
// because the READ side needs the same vocabulary and cannot pay for a pool.
|
|
285
|
+
// scripts/gds/status.js is public, unauthenticated and shipped inside the
|
|
286
|
+
// server-free @cloudbongos/cli, so while this classifier sat behind
|
|
287
|
+
// ../../src/module-api it was unreachable from there and the CLI grew its own
|
|
288
|
+
// positional-only parser (`Number(ref.slice(1))`). The system ended up PRINTING
|
|
289
|
+
// and REQUIRING the very ref shape resolveCriterionIds below refuses as unsafe
|
|
290
|
+
// (task 1004069). One vocabulary, two importers, no second definition.
|
|
291
|
+
const { parseCriterionRef } = require('./criterion-ref.js');
|
|
292
292
|
|
|
293
293
|
// Resolve an array of refs to done_when_criteria.id values within a version.
|
|
294
294
|
// Returns { ids: [unique numeric ids, first-appearance order], unresolved: [refs],
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.894",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.894",
|
|
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.19.
|
|
3
|
+
"version": "1.19.894",
|
|
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",
|
|
@@ -85,6 +85,11 @@ const FILES = [
|
|
|
85
85
|
// Module files the above touch. Each imports nothing from the kernel at load time.
|
|
86
86
|
'modules/lifecycle/task-classifier.js',
|
|
87
87
|
'modules/lifecycle/task-visuals.js',
|
|
88
|
+
// The criterion-ref vocabulary status.js resolves a `Cn` or a slug through
|
|
89
|
+
// (task 1004069). It is in modules/ because the WRITE path uses the same
|
|
90
|
+
// definition; it requires nothing, which is the whole reason it could be
|
|
91
|
+
// carved out of db-deps-criteria.js (that file needs a pool) and shipped here.
|
|
92
|
+
'modules/lifecycle/criterion-ref.js',
|
|
88
93
|
'modules/economy/reward-policy.js',
|
|
89
94
|
|
|
90
95
|
// The only core files in the package: config/branding readers and one constant table.
|
package/scripts/gds/status.js
CHANGED
|
@@ -8,11 +8,26 @@
|
|
|
8
8
|
// GET /api/bongos/versions/:id/progress.
|
|
9
9
|
//
|
|
10
10
|
// Usage:
|
|
11
|
-
// node scripts/gds/status.js
|
|
12
|
-
// node scripts/gds/status.js
|
|
13
|
-
// node scripts/gds/status.js
|
|
11
|
+
// node scripts/gds/status.js # all criteria, default version
|
|
12
|
+
// node scripts/gds/status.js wa6-kernel-and-packs # drill in by SLUG (the identity)
|
|
13
|
+
// node scripts/gds/status.js C8 # …or by position (an affordance)
|
|
14
|
+
// node scripts/gds/status.js --criterion C8 # exact form, no guessing
|
|
15
|
+
// node scripts/gds/status.js --version V1 # another version
|
|
14
16
|
// node scripts/gds/status.js --version GDS-V3 C8
|
|
15
|
-
// node scripts/gds/status.js --json
|
|
17
|
+
// node scripts/gds/status.js --json # machine-readable
|
|
18
|
+
//
|
|
19
|
+
// WHICH NAME IS THE IDENTITY (task 1004069). The SLUG. `Cn` is a presentation
|
|
20
|
+
// ordinal recomputed live from a reorderable sort_order, so inserting one
|
|
21
|
+
// criterion renumbers every criterion after it, version-wide, silently — and
|
|
22
|
+
// the write path (`resolveCriterionIds`) already REFUSES a positional ref on any
|
|
23
|
+
// multi-goal version for exactly that reason. This CLI used to print `Cn` as
|
|
24
|
+
// each row's identity and accept nothing else, so the one documented way to
|
|
25
|
+
// drill in was the one form the rest of the system calls unsafe. Now the slug
|
|
26
|
+
// leads in every output mode and BOTH forms resolve on input; a positional ref
|
|
27
|
+
// answers with the slug it landed on, so the reader can quote the stable name.
|
|
28
|
+
// The ref vocabulary itself is ../../modules/lifecycle/criterion-ref.js — one
|
|
29
|
+
// definition, shared with the write path, which is what stops the two drifting
|
|
30
|
+
// apart again.
|
|
16
31
|
//
|
|
17
32
|
// Read-only and UNAUTHENTICATED (the endpoint is public, same posture as
|
|
18
33
|
// /versions/:id/done-when), so it works before /builder-setup too.
|
|
@@ -22,6 +37,11 @@
|
|
|
22
37
|
const { API_BASE, cliClient, assertInstanceMatch, cliExit } = require('./cli-lib');
|
|
23
38
|
const { resolveEnv } = require('../../src/instance-config');
|
|
24
39
|
const { resolveDefaultVersion } = require('./version-select.js');
|
|
40
|
+
const {
|
|
41
|
+
looksLikeVersionId,
|
|
42
|
+
resolveCriterionIn,
|
|
43
|
+
criterionLabel,
|
|
44
|
+
} = require('../../modules/lifecycle/criterion-ref.js');
|
|
25
45
|
|
|
26
46
|
// The active internal version is RESOLVED, never named here. Override with
|
|
27
47
|
// <PREFIX>_STATUS_DEFAULT_VERSION or --version.
|
|
@@ -53,20 +73,32 @@ function parseArgs(argv) {
|
|
|
53
73
|
if (a === '--version' || a === '-v') {
|
|
54
74
|
version = args[++i];
|
|
55
75
|
if (version == null) return { error: '--version requires a value' };
|
|
76
|
+
} else if (a === '--criterion' || a === '-c') {
|
|
77
|
+
criterion = args[++i];
|
|
78
|
+
if (criterion == null) return { error: '--criterion requires a value' };
|
|
56
79
|
} else if (a === '--json') json = true;
|
|
57
80
|
else if (a === '--widget') widget = true;
|
|
58
81
|
else if (a === '--hook') hook = true;
|
|
59
82
|
else if (a === '--help' || a === '-h') return { help: true };
|
|
60
|
-
else if (/^[Cc]\d+$/.test(a)) criterion = a.toUpperCase();
|
|
61
83
|
else if (a.startsWith('-')) return { error: `unknown flag: ${a}` };
|
|
62
|
-
|
|
63
|
-
else
|
|
84
|
+
// A bare token is a VERSION only if it is shaped like one (V1, GDS-V4,
|
|
85
|
+
// BONGOS-V2); everything else is a criterion ref, so a slug can be typed
|
|
86
|
+
// where only `Cn` used to fit. Shape, not order: `status.js
|
|
87
|
+
// wa6-kernel-and-packs` used to be swallowed as a version id and 404 the
|
|
88
|
+
// progress fetch. `--version` / `--criterion` are the exact forms that skip
|
|
89
|
+
// this test entirely, for a caller who will not be guessed at.
|
|
90
|
+
else if (!version && looksLikeVersionId(a)) version = a;
|
|
91
|
+
else if (!criterion) criterion = a;
|
|
92
|
+
else if (!version) version = a;
|
|
64
93
|
else return { error: `unexpected argument: ${a}` };
|
|
65
94
|
}
|
|
66
95
|
// No `versionExplicit` companion flag: `version !== null` already IS that
|
|
67
96
|
// fact, and resolveVersion returns `defaulted`/`source` for anyone who needs
|
|
68
97
|
// to act on it. A second field carrying the same truth is one that can drift.
|
|
69
|
-
|
|
98
|
+
// `criterion` is kept VERBATIM — never uppercased, which is what made a slug
|
|
99
|
+
// unrepresentable here. Case-folding belongs to the resolver, which matches
|
|
100
|
+
// slugs case-insensitively and `Cn` case-insensitively too.
|
|
101
|
+
return { version: version || null, criterion: criterion || null, json, widget, hook };
|
|
70
102
|
}
|
|
71
103
|
|
|
72
104
|
// Resolve which version this invocation is about. Precedence: an explicit
|
|
@@ -95,6 +127,18 @@ function defaultedVersionNote(r) {
|
|
|
95
127
|
return `(no version given — showing ${r.version}, ${why}. Pass --version to pick another.)`;
|
|
96
128
|
}
|
|
97
129
|
|
|
130
|
+
// One line naming the STABLE name a positional ref landed on, so whatever the
|
|
131
|
+
// reader quotes next survives the next criterion anyone inserts. `Cn` is
|
|
132
|
+
// recomputed from sort_order on every read: creating `wa6-noise-limited` moved
|
|
133
|
+
// `wa10-execution-integrity` from C11 to C13, and every note, doc and click
|
|
134
|
+
// handler still saying C11 now points at a different criterion. Empty when the
|
|
135
|
+
// user already named the slug — they are holding the durable form.
|
|
136
|
+
function positionalRefNote(c, kind) {
|
|
137
|
+
if (!c || kind !== 'cnum') return '';
|
|
138
|
+
return `(C${c.cnum} is a position, recomputed whenever a criterion is added or reordered.`
|
|
139
|
+
+ ` The stable name is \`${c.criterion_id}\` — quote that.)`;
|
|
140
|
+
}
|
|
141
|
+
|
|
98
142
|
async function fetchProgress(version) {
|
|
99
143
|
let res;
|
|
100
144
|
try {
|
|
@@ -145,6 +189,8 @@ function bar(shipped, total, width = 10) {
|
|
|
145
189
|
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
146
190
|
}
|
|
147
191
|
|
|
192
|
+
// The POSITION column only. It is deliberately narrow and deliberately not the
|
|
193
|
+
// row's name: the criterion_id beside it is what a reader should carry away.
|
|
148
194
|
function clabel(c) {
|
|
149
195
|
return `C${c.cnum}`.padEnd(3);
|
|
150
196
|
}
|
|
@@ -160,6 +206,13 @@ function clabel(c) {
|
|
|
160
206
|
// or the ship card's `.cc-*`.
|
|
161
207
|
const sesc = (s) => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
162
208
|
|
|
209
|
+
// A value going inside a single-quoted JS string inside an HTML attribute needs
|
|
210
|
+
// BOTH escapes, in that order. Task ids are digits so the existing handlers get
|
|
211
|
+
// away with `sesc` alone; a criterion_id is free text from the DB, and an
|
|
212
|
+
// apostrophe in one would close the sendPrompt argument early and break every
|
|
213
|
+
// row's click. Escape for JS first, then for the attribute.
|
|
214
|
+
const jsq = (s) => sesc(String(s == null ? '' : s).replace(/\\/g, '\\\\').replace(/'/g, "\\'"));
|
|
215
|
+
|
|
163
216
|
const STATUS_WIDGET_STYLE = `<style>
|
|
164
217
|
.st-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;}
|
|
165
218
|
.st-card{background:var(--color-background-primary);border:0.5px solid var(--color-border-tertiary);border-radius:var(--border-radius-lg);padding:1rem 1.25rem;font-family:var(--font-sans);}
|
|
@@ -207,9 +260,9 @@ function renderStatusWidget(data, criterion) {
|
|
|
207
260
|
: `<div class="st-crit">No tasks linked to this criterion yet.</div>`;
|
|
208
261
|
const claimHint = c.tasks.some((t) => t.status === 'ready') ? ' · click a ready task to claim it' : '';
|
|
209
262
|
return `${STATUS_WIDGET_STYLE}
|
|
210
|
-
<h2 class="st-sr">${sesc(data.version_id)} criterion
|
|
263
|
+
<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>
|
|
211
264
|
<div class="st-card">
|
|
212
|
-
<div class="st-head"><span class="st-title"><i class="ti ti-checklist" aria-hidden="true"></i>${sesc(data.version_id)} ·
|
|
265
|
+
<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">satisfied</span>' : 'not satisfied'} · ${c.counts.shipped}/${c.counts.total} shipped</span></div>
|
|
213
266
|
<div class="st-crit">${sesc(c.criterion_md.replace(/\*\*/g, '').slice(0, 160))}</div>
|
|
214
267
|
${body}
|
|
215
268
|
<div class="st-foot">remaining: ${sesc(remainingSummary(c.remaining))}${claimHint}</div>
|
|
@@ -219,8 +272,13 @@ function renderStatusWidget(data, criterion) {
|
|
|
219
272
|
// Version → Goals → Criteria (ADR 0086 §2 / BV1.R58). Render each criterion
|
|
220
273
|
// row under its goal's header. Fall back to a flat list if the rollup carries
|
|
221
274
|
// no goals (older payload).
|
|
275
|
+
// The click sends the SLUG, never the position. A rendered card outlives the
|
|
276
|
+
// ordering it was rendered from: every handler here used to carry `/status
|
|
277
|
+
// C11`, so one criterion inserted anywhere above turned every drill-in on a
|
|
278
|
+
// card already in someone's transcript into a different criterion, silently.
|
|
279
|
+
// The slug survives an insert, a reorder, and a card read tomorrow.
|
|
222
280
|
const critRow = (c) =>
|
|
223
|
-
`<tr class="st-tr" role="button" tabindex="0" title="Drill into
|
|
281
|
+
`<tr class="st-tr" role="button" tabindex="0" title="Drill into ${sesc(c.criterion_id)} (C${c.cnum})" onclick="sendPrompt('/status ${jsq(c.criterion_id)}')" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();sendPrompt('/status ${jsq(c.criterion_id)}')}"><td class="st-td st-ok st-nowrap">${c.satisfied ? '✓' : ''}</td><td class="st-td st-ellip" title="${sesc(c.criterion_id)}">${sesc(c.criterion_id)}</td><td class="st-td st-id">C${c.cnum}</td><td class="st-td st-nowrap">${widgetBar(c.counts.shipped, c.counts.total)}${c.counts.shipped}/${c.counts.total}</td><td class="st-td st-ellip st-sub">${c.remaining.length ? sesc(remainingSummary(c.remaining)) : '—'}</td></tr>`;
|
|
224
282
|
const goals = (data.goals && data.goals.length) ? data.goals : null;
|
|
225
283
|
const rows = goals
|
|
226
284
|
? goals.map((g) => {
|
|
@@ -237,7 +295,7 @@ function renderStatusWidget(data, criterion) {
|
|
|
237
295
|
<h2 class="st-sr">${sesc(data.version_id)} status: ${satisfied} of ${data.criteria.length} done-when criteria satisfied across ${goals ? data.goals_total : 0} goal(s). Click a criterion to drill in.</h2>
|
|
238
296
|
<div class="st-card">
|
|
239
297
|
<div class="st-head"><span class="st-title"><i class="ti ti-checklist" aria-hidden="true"></i>${sesc(data.version_id)} status</span><span class="st-meta">${satisfied}/${data.criteria.length} criteria${goalMeta}</span></div>
|
|
240
|
-
<table class="st-table"><colgroup><col style="width:24px;"><col style="width:44px;"><col
|
|
298
|
+
<table class="st-table"><colgroup><col style="width:24px;"><col><col style="width:44px;"><col style="width:96px;"><col style="width:120px;"></colgroup><thead><tr class="st-thead"><th class="st-th"></th><th class="st-th">Criterion</th><th class="st-th">Pos</th><th class="st-th">Shipped</th><th class="st-th">Remaining</th></tr></thead><tbody>${rows}</tbody></table>
|
|
241
299
|
${foot}
|
|
242
300
|
</div>`;
|
|
243
301
|
}
|
|
@@ -248,7 +306,7 @@ function renderStatusMarkdown(data, criterion) {
|
|
|
248
306
|
const lines = [];
|
|
249
307
|
if (criterion) {
|
|
250
308
|
const c = criterion;
|
|
251
|
-
lines.push(`**/status · ${data.version_id} ·
|
|
309
|
+
lines.push(`**/status · ${data.version_id} · ${criterionLabel(c)}** — ${c.satisfied ? 'satisfied ✓' : 'not satisfied'}`);
|
|
252
310
|
lines.push(`_${c.criterion_md.replace(/\*\*/g, '').slice(0, 160)}_`);
|
|
253
311
|
lines.push('');
|
|
254
312
|
if (c.tasks.length) {
|
|
@@ -272,8 +330,10 @@ function renderStatusMarkdown(data, criterion) {
|
|
|
272
330
|
lines.push('');
|
|
273
331
|
// Version → Goals → Criteria (ADR 0086 §2): a subheading per goal, its criteria
|
|
274
332
|
// beneath. Falls back to a single flat table if the payload carries no goals.
|
|
275
|
-
|
|
276
|
-
|
|
333
|
+
// Slug first, position second — the column a reader copies out of a card is
|
|
334
|
+
// the one that still means the same thing next week.
|
|
335
|
+
const critRow = (c) => `| ${c.satisfied ? '✓' : ''} | ${c.criterion_id} | C${c.cnum} | ${bar(c.counts.shipped, c.counts.total)} ${c.counts.shipped}/${c.counts.total} | ${c.remaining.length ? remainingSummary(c.remaining) : '—'} |`;
|
|
336
|
+
const tableHeader = () => { lines.push('| | Criterion | Pos | Shipped | Remaining |'); lines.push('|---|---|---|---|---|'); };
|
|
277
337
|
if (goals) {
|
|
278
338
|
for (const g of goals) {
|
|
279
339
|
lines.push(`**🎯 ${g.title}** — ${g.status === 'achieved' ? 'achieved ✓' : g.status}${g.criteria.length ? '' : ' · no criteria yet'}`);
|
|
@@ -289,7 +349,10 @@ function renderStatusMarkdown(data, criterion) {
|
|
|
289
349
|
lines.push(`_${data.unattributed_tasks} task(s) not linked to any criterion._`);
|
|
290
350
|
}
|
|
291
351
|
lines.push('');
|
|
292
|
-
|
|
352
|
+
// The hint quotes a REAL slug off this payload rather than a frozen `C8`,
|
|
353
|
+
// which is both an example of the stable form and immune to going stale.
|
|
354
|
+
const example = data.criteria.length ? data.criteria[0].criterion_id : 'wa6-kernel-and-packs';
|
|
355
|
+
lines.push(`→ Drill into one: \`/status ${example}\``);
|
|
293
356
|
return lines.join('\n');
|
|
294
357
|
}
|
|
295
358
|
|
|
@@ -313,7 +376,7 @@ function printAll(data) {
|
|
|
313
376
|
// Version → Goals → Criteria (ADR 0086 §2): a heading per goal, criteria beneath.
|
|
314
377
|
const printCrit = (c) => {
|
|
315
378
|
const flag = c.satisfied ? '✓' : ' ';
|
|
316
|
-
const line = `${flag} ${
|
|
379
|
+
const line = `${flag} ${c.criterion_id.padEnd(34)} ${clabel(c)} ${bar(c.counts.shipped, c.counts.total)} `
|
|
317
380
|
+ `${c.counts.shipped}/${c.counts.total} shipped`;
|
|
318
381
|
const rem = c.remaining.length ? ` remaining: ${remainingSummary(c.remaining)}` : '';
|
|
319
382
|
console.log(line + rem);
|
|
@@ -328,13 +391,13 @@ function printAll(data) {
|
|
|
328
391
|
for (const c of data.criteria) printCrit(c);
|
|
329
392
|
}
|
|
330
393
|
if (data.unattributed_tasks > 0) {
|
|
331
|
-
console.log(`\n${data.unattributed_tasks} task(s) in ${data.version_id} not linked to any criterion (run /status <
|
|
394
|
+
console.log(`\n${data.unattributed_tasks} task(s) in ${data.version_id} not linked to any criterion (run /status <criterion> to drill in).`);
|
|
332
395
|
}
|
|
333
396
|
console.log('');
|
|
334
397
|
}
|
|
335
398
|
|
|
336
399
|
function printOne(data, c) {
|
|
337
|
-
console.log(`\n${data.version_id} ·
|
|
400
|
+
console.log(`\n${data.version_id} · ${c.criterion_id} (C${c.cnum}) [${c.satisfied ? 'satisfied ✓' : 'not satisfied'}]`);
|
|
338
401
|
console.log(`"${c.criterion_md.replace(/\*\*/g, '').slice(0, 120)}"\n`);
|
|
339
402
|
if (c.tasks.length === 0) {
|
|
340
403
|
console.log(' (no tasks linked — this criterion has no structured gating tasks yet)\n');
|
|
@@ -352,11 +415,11 @@ async function main() {
|
|
|
352
415
|
const opts = parseArgs(process.argv);
|
|
353
416
|
if (opts.error) {
|
|
354
417
|
console.error(`status: ${opts.error}`);
|
|
355
|
-
console.error('Usage: node scripts/gds/status.js [--version ID] [Cn] [--json]');
|
|
418
|
+
console.error('Usage: node scripts/gds/status.js [--version ID] [<criterion-slug>|Cn] [--json]');
|
|
356
419
|
process.exit(2);
|
|
357
420
|
}
|
|
358
421
|
if (opts.help) {
|
|
359
|
-
console.log('Usage: node scripts/gds/status.js [--version ID] [Cn] [--json]');
|
|
422
|
+
console.log('Usage: node scripts/gds/status.js [--version ID] [<criterion-slug>|Cn] [--json]');
|
|
360
423
|
process.exit(0);
|
|
361
424
|
}
|
|
362
425
|
// Fail closed if this repo's configured instance != the signed-in session's (task
|
|
@@ -398,16 +461,31 @@ async function main() {
|
|
|
398
461
|
const data = await fetchProgress(picked.version);
|
|
399
462
|
|
|
400
463
|
// Resolve the requested criterion (if any) once — every output mode needs it.
|
|
464
|
+
// Slug, positional `Cn`, or numeric id, through the SAME vocabulary the write
|
|
465
|
+
// path uses. The rows come from ONE version's payload, which is what makes the
|
|
466
|
+
// version scoping structural rather than remembered: a bare `Cn` can only ever
|
|
467
|
+
// rank inside the version we just fetched, and the versions number
|
|
468
|
+
// independently (BONGOS-V1 C8 ≠ BONGOS-V2 C8).
|
|
401
469
|
let c = null;
|
|
470
|
+
let refKind = null;
|
|
402
471
|
if (opts.criterion) {
|
|
403
|
-
const
|
|
404
|
-
c =
|
|
472
|
+
const hit = resolveCriterionIn(data.criteria, opts.criterion);
|
|
473
|
+
c = hit.criterion;
|
|
474
|
+
refKind = hit.kind;
|
|
405
475
|
if (!c) {
|
|
406
476
|
// Name the version, not just the criterion: on a DEFAULTED version this is
|
|
407
477
|
// the reader's only clue that C13 was looked up somewhere they didn't pick.
|
|
408
|
-
console.error(`status: ${picked.version} has no criterion ${opts.criterion}
|
|
478
|
+
console.error(`status: ${picked.version} has no criterion "${opts.criterion}".`);
|
|
479
|
+
if (refKind === 'cnum') {
|
|
480
|
+
console.error(` It has C1–C${data.criteria.length}. Positions shift when a criterion is added — prefer the slug.`);
|
|
481
|
+
} else {
|
|
482
|
+
// A miss on a slug is usually a typo or the wrong version, and the cure
|
|
483
|
+
// is the list of legal names, not a count.
|
|
484
|
+
const names = data.criteria.map((x) => x.criterion_id);
|
|
485
|
+
console.error(` Known criteria: ${names.join(', ')}`);
|
|
486
|
+
}
|
|
409
487
|
if (picked.defaulted) console.error(` ${defaultedVersionNote(picked)}`);
|
|
410
|
-
|
|
488
|
+
return cliExit(3);
|
|
411
489
|
}
|
|
412
490
|
}
|
|
413
491
|
|
|
@@ -433,8 +511,9 @@ async function main() {
|
|
|
433
511
|
// the version was resolved rather than chosen (--json/--widget/--hook are
|
|
434
512
|
// machine interfaces and stay byte-identical).
|
|
435
513
|
if (c) printOne(data, c); else printAll(data);
|
|
436
|
-
const
|
|
437
|
-
|
|
514
|
+
const notes = [positionalRefNote(c, refKind), defaultedVersionNote(picked)].filter(Boolean);
|
|
515
|
+
for (const note of notes) console.log(` ${note}`);
|
|
516
|
+
if (notes.length) console.log('');
|
|
438
517
|
}
|
|
439
518
|
|
|
440
519
|
if (require.main === module) {
|
|
@@ -444,4 +523,4 @@ if (require.main === module) {
|
|
|
444
523
|
});
|
|
445
524
|
}
|
|
446
525
|
|
|
447
|
-
module.exports = { parseArgs, resolveVersion, defaultedVersionNote, renderStatusWidget, renderStatusMarkdown };
|
|
526
|
+
module.exports = { parseArgs, resolveVersion, defaultedVersionNote, positionalRefNote, renderStatusWidget, renderStatusMarkdown };
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.894'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// tests/criterion_ref_status.mjs
|
|
2
|
+
//
|
|
3
|
+
// The READ side of the criterion-ref vocabulary (task 1004069): what
|
|
4
|
+
// scripts/gds/status.js accepts on input and what it calls a criterion in
|
|
5
|
+
// output. The write half is tests/criterion_ref_parse.mjs.
|
|
6
|
+
//
|
|
7
|
+
// WHY THESE CASES. `Cn` is ROW_NUMBER() over a reorderable sort_order, so it is
|
|
8
|
+
// a presentation ordinal, not a name — inserting a criterion renumbers every one
|
|
9
|
+
// after it, version-wide, silently. `resolveCriterionIds` has refused a
|
|
10
|
+
// positional ref on a multi-goal version since task 1003108, while status.js
|
|
11
|
+
// PRINTED that same form as each row's identity and accepted nothing else. So
|
|
12
|
+
// the assertions below are about the two halves agreeing: a slug must resolve, a
|
|
13
|
+
// position must still resolve but say what it landed on, and nothing the
|
|
14
|
+
// renderers emit may carry a bare position where a reader will copy it.
|
|
15
|
+
//
|
|
16
|
+
// Run: node tests/criterion_ref_status.mjs
|
|
17
|
+
|
|
18
|
+
import { strict as assert } from 'node:assert';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const status = require('../scripts/gds/status.js');
|
|
23
|
+
const refs = require('../modules/lifecycle/criterion-ref.js');
|
|
24
|
+
|
|
25
|
+
let passed = 0;
|
|
26
|
+
let failed = 0;
|
|
27
|
+
function test(name, fn) {
|
|
28
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
29
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// A miniature version payload in the shape GET /versions/:id/progress returns.
|
|
33
|
+
const crit = (cnum, criterion_id, id) => ({
|
|
34
|
+
cnum,
|
|
35
|
+
id,
|
|
36
|
+
criterion_id,
|
|
37
|
+
criterion_md: `**${criterion_id}** — prose`,
|
|
38
|
+
satisfied: false,
|
|
39
|
+
goal_id: '1000095',
|
|
40
|
+
tasks: [],
|
|
41
|
+
counts: { shipped: 0, total: 0 },
|
|
42
|
+
remaining: [],
|
|
43
|
+
});
|
|
44
|
+
const DATA = {
|
|
45
|
+
version_id: 'BONGOS-V2',
|
|
46
|
+
criteria: [
|
|
47
|
+
crit(1, 'wa10-security-enforced', '1000364'),
|
|
48
|
+
crit(8, 'wa6-kernel-and-packs', '1000369'),
|
|
49
|
+
crit(11, 'wa6-noise-limited', '1000380'),
|
|
50
|
+
],
|
|
51
|
+
goals: [],
|
|
52
|
+
goals_achieved: 0,
|
|
53
|
+
goals_total: 0,
|
|
54
|
+
unattributed_tasks: 0,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ---------- input: a bare token is a version or a criterion by SHAPE ----------
|
|
58
|
+
|
|
59
|
+
test('a bare SLUG is a criterion, not a version id', () => {
|
|
60
|
+
// The whole defect in one case: this used to set version='wa6-kernel-and-packs'
|
|
61
|
+
// and 404 the progress fetch, because the first bare token was always a version.
|
|
62
|
+
const o = status.parseArgs(['node', 'status.js', 'wa6-kernel-and-packs']);
|
|
63
|
+
assert.equal(o.criterion, 'wa6-kernel-and-packs');
|
|
64
|
+
assert.equal(o.version, null);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('a bare version-shaped token is still a version', () => {
|
|
68
|
+
for (const v of ['V1', 'PMS-V2', 'GDS-V4', 'BONGOS-V2', 'CB-V1']) {
|
|
69
|
+
const o = status.parseArgs(['node', 'status.js', v]);
|
|
70
|
+
assert.equal(o.version, v, `${v} should parse as a version`);
|
|
71
|
+
assert.equal(o.criterion, null, `${v} should not parse as a criterion`);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('a bare Cn is a criterion even though it is neither lowercase nor kebab', () => {
|
|
76
|
+
assert.equal(status.parseArgs(['node', 'status.js', 'C8']).criterion, 'C8');
|
|
77
|
+
assert.equal(status.parseArgs(['node', 'status.js', 'C8']).version, null);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('version and criterion together, in either order', () => {
|
|
81
|
+
const a = status.parseArgs(['node', 'status.js', 'BONGOS-V2', 'wa6-noise-limited']);
|
|
82
|
+
assert.equal(a.version, 'BONGOS-V2');
|
|
83
|
+
assert.equal(a.criterion, 'wa6-noise-limited');
|
|
84
|
+
const b = status.parseArgs(['node', 'status.js', 'wa6-noise-limited', 'BONGOS-V2']);
|
|
85
|
+
assert.equal(b.version, 'BONGOS-V2');
|
|
86
|
+
assert.equal(b.criterion, 'wa6-noise-limited');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('--criterion is the EXACT form and skips the shape test entirely', () => {
|
|
90
|
+
// A criterion whose slug happens to be version-shaped is unreachable by the
|
|
91
|
+
// bare-token path by construction; the flag is how a caller refuses to be guessed at.
|
|
92
|
+
const o = status.parseArgs(['node', 'status.js', '--criterion', 'V2', '--version', 'BONGOS-V2']);
|
|
93
|
+
assert.equal(o.criterion, 'V2');
|
|
94
|
+
assert.equal(o.version, 'BONGOS-V2');
|
|
95
|
+
assert.deepEqual(status.parseArgs(['node', 'status.js', '--criterion']), { error: '--criterion requires a value' });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('a criterion ref is NOT uppercased on the way through', () => {
|
|
99
|
+
// The old parser did `a.toUpperCase()`, which is what made a slug
|
|
100
|
+
// unrepresentable: 'WA6-KERNEL-AND-PACKS' matches no row.
|
|
101
|
+
assert.equal(status.parseArgs(['node', 'status.js', 'wa6-kernel-and-packs']).criterion, 'wa6-kernel-and-packs');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ---------- resolution: both forms land, only one is stable ----------
|
|
105
|
+
|
|
106
|
+
test('a slug resolves, case-insensitively', () => {
|
|
107
|
+
assert.equal(refs.resolveCriterionIn(DATA.criteria, 'wa6-kernel-and-packs').criterion.cnum, 8);
|
|
108
|
+
assert.equal(refs.resolveCriterionIn(DATA.criteria, 'WA6-Kernel-And-Packs').criterion.cnum, 8);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('a positional Cn resolves BY POSITION, not by the number in the slug', () => {
|
|
112
|
+
const hit = refs.resolveCriterionIn(DATA.criteria, 'C11');
|
|
113
|
+
assert.equal(hit.kind, 'cnum');
|
|
114
|
+
assert.equal(hit.criterion.criterion_id, 'wa6-noise-limited');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('a numeric done_when id resolves', () => {
|
|
118
|
+
assert.equal(refs.resolveCriterionIn(DATA.criteria, '1000369').criterion.criterion_id, 'wa6-kernel-and-packs');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('a miss is a miss, and carries the kind so the caller can say the right thing', () => {
|
|
122
|
+
assert.equal(refs.resolveCriterionIn(DATA.criteria, 'C99').criterion, null);
|
|
123
|
+
assert.equal(refs.resolveCriterionIn(DATA.criteria, 'C99').kind, 'cnum');
|
|
124
|
+
assert.equal(refs.resolveCriterionIn(DATA.criteria, 'no-such-thing').criterion, null);
|
|
125
|
+
assert.equal(refs.resolveCriterionIn(DATA.criteria, 'no-such-thing').kind, 'slug');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('resolution is scoped to the rows handed in — it cannot reach another version', () => {
|
|
129
|
+
// BONGOS-V1 and BONGOS-V2 both have a C8 and they are different criteria. The
|
|
130
|
+
// scoping is structural: there is no argument by which a caller could ask this
|
|
131
|
+
// function about a version it was not given the rows for.
|
|
132
|
+
assert.equal(refs.resolveCriterionIn([], 'C8').criterion, null);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ---------- the note: a position must announce what it landed on ----------
|
|
136
|
+
|
|
137
|
+
test('a positional drill-in names the stable slug', () => {
|
|
138
|
+
const note = status.positionalRefNote(DATA.criteria[1], 'cnum');
|
|
139
|
+
assert.match(note, /wa6-kernel-and-packs/);
|
|
140
|
+
assert.match(note, /position/i);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('a slug drill-in emits NO note — the reader already holds the durable form', () => {
|
|
144
|
+
assert.equal(status.positionalRefNote(DATA.criteria[1], 'slug'), '');
|
|
145
|
+
assert.equal(status.positionalRefNote(null, 'cnum'), '');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ---------- output: the slug is the identity everywhere ----------
|
|
149
|
+
|
|
150
|
+
test('criterionLabel leads with the slug and trails the position', () => {
|
|
151
|
+
assert.equal(refs.criterionLabel(DATA.criteria[1]), 'wa6-kernel-and-packs (C8)');
|
|
152
|
+
assert.equal(refs.criterionLabel({ criterion_id: 'x' }), 'x');
|
|
153
|
+
assert.equal(refs.criterionLabel(null), '');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('every widget drill-in click sends the SLUG, never a position', () => {
|
|
157
|
+
// The case that made this worth a task: a rendered card outlives the ordering
|
|
158
|
+
// it was rendered from, so a handler carrying `/status C11` starts naming a
|
|
159
|
+
// different criterion the moment anyone inserts one above it.
|
|
160
|
+
const html = status.renderStatusWidget(DATA, null);
|
|
161
|
+
for (const c of DATA.criteria) {
|
|
162
|
+
assert.ok(html.includes(`sendPrompt('/status ${c.criterion_id}')`),
|
|
163
|
+
`expected a slug click handler for ${c.criterion_id}`);
|
|
164
|
+
}
|
|
165
|
+
assert.equal(/sendPrompt\('\/status C\d+'\)/.test(html), false,
|
|
166
|
+
'a positional click handler survived in the widget');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('the markdown card never suggests a positional drill-in', () => {
|
|
170
|
+
const md = status.renderStatusMarkdown(DATA, null);
|
|
171
|
+
assert.equal(/`\/status C\d+`/.test(md), false, 'the drill-in hint quotes a position');
|
|
172
|
+
assert.match(md, /`\/status wa10-security-enforced`/);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('the markdown table puts the criterion before the position', () => {
|
|
176
|
+
const md = status.renderStatusMarkdown(DATA, null);
|
|
177
|
+
assert.match(md, /\| \| Criterion \| Pos \|/);
|
|
178
|
+
// …and every row follows the header it declares.
|
|
179
|
+
assert.match(md, /\|\s*\|\s*wa6-kernel-and-packs\s*\|\s*C8\s*\|/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('a criterion card headlines the slug', () => {
|
|
183
|
+
const md = status.renderStatusMarkdown(DATA, DATA.criteria[1]);
|
|
184
|
+
assert.match(md, /BONGOS-V2 · wa6-kernel-and-packs \(C8\)/);
|
|
185
|
+
const html = status.renderStatusWidget(DATA, DATA.criteria[1]);
|
|
186
|
+
assert.match(html, /wa6-kernel-and-packs \(C8\)/);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ---------- the escaping the slug column newly needs ----------
|
|
190
|
+
|
|
191
|
+
test('a slug carrying an apostrophe cannot break the click handler', () => {
|
|
192
|
+
// Task ids are digits, so the existing handlers get away with HTML-escaping
|
|
193
|
+
// alone; a criterion_id is free text from the DB and goes inside a
|
|
194
|
+
// single-quoted JS string inside an HTML attribute.
|
|
195
|
+
const odd = { ...crit(2, "o'hara-rule", '1000999') };
|
|
196
|
+
const html = status.renderStatusWidget({ ...DATA, criteria: [odd] }, null);
|
|
197
|
+
assert.ok(html.includes("sendPrompt('/status o\\'hara-rule')"),
|
|
198
|
+
'the apostrophe was not escaped for the JS string');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
console.log(`\ncriterion ref — read side (task 1004069): ${passed} passed, ${failed} failed`);
|
|
202
|
+
process.exitCode = failed > 0 ? 1 : 0;
|
package/tests/session-cards.mjs
CHANGED
|
@@ -78,12 +78,45 @@ test('status args extract an explicit version', () => {
|
|
|
78
78
|
assert.deepEqual(statusCard.args("what's left in PMS-V1"), ['--hook', '--version', 'PMS-V1']);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
// Both criterion forms ride the EXACT flag (task 1004069) — status.js then never
|
|
82
|
+
// has to guess whether a bare token was a version id or a criterion ref.
|
|
83
|
+
test('status args extract a positional criterion token', () => {
|
|
84
|
+
assert.deepEqual(statusCard.args("what's left for C8"), ['--hook', '--criterion', 'C8']);
|
|
83
85
|
});
|
|
84
86
|
|
|
85
87
|
test('status args extract version AND criterion together', () => {
|
|
86
|
-
assert.deepEqual(statusCard.args('status of GDS-V4 C2'), ['--hook', '--version', 'GDS-V4', 'C2']);
|
|
88
|
+
assert.deepEqual(statusCard.args('status of GDS-V4 C2'), ['--hook', '--version', 'GDS-V4', '--criterion', 'C2']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// A card's own click handler sends the SLUG, so the hook that feeds the card has
|
|
92
|
+
// to recognise the form its own output produces — otherwise every drill-in click
|
|
93
|
+
// on a rendered card falls back to the whole-version overview.
|
|
94
|
+
test('status args extract a criterion SLUG after the slash command', () => {
|
|
95
|
+
assert.deepEqual(statusCard.args('/status wa6-kernel-and-packs'),
|
|
96
|
+
['--hook', '--criterion', 'wa6-kernel-and-packs']);
|
|
97
|
+
assert.deepEqual(statusCard.args('please run /status `wa11-legible`'),
|
|
98
|
+
['--hook', '--criterion', 'wa11-legible']);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('a slug after the word "criterion" is picked up too', () => {
|
|
102
|
+
assert.deepEqual(statusCard.args('how far along is criterion wa6-noise-limited'),
|
|
103
|
+
['--hook', '--criterion', 'wa6-noise-limited']);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('the SLUG wins over a positional token in the same prompt', () => {
|
|
107
|
+
// The slug cannot have gone stale; C11 may name a different criterion than it
|
|
108
|
+
// did when whatever the builder is quoting was written.
|
|
109
|
+
assert.deepEqual(statusCard.args('/status wa6-noise-limited (was C11)'),
|
|
110
|
+
['--hook', '--criterion', 'wa6-noise-limited']);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('hyphenated ENGLISH in a status prompt is not read as a slug', () => {
|
|
114
|
+
// The pattern only fires where a prompt actually POINTS at a criterion. A
|
|
115
|
+
// loose one would 404 the drill-in on ordinary prose — and a status card
|
|
116
|
+
// answering about nothing is worse than one answering about the version.
|
|
117
|
+
assert.deepEqual(statusCard.args("what's left for the half-finished work"), ['--hook']);
|
|
118
|
+
assert.deepEqual(statusCard.args('what tasks remain on the module-economy side of things'), ['--hook']);
|
|
119
|
+
assert.equal(hook.criterionSlugIn('status of the multi-goal version'), null);
|
|
87
120
|
});
|
|
88
121
|
|
|
89
122
|
// ---------- buildAdditionalContext: widgets ON ----------
|