@bongos/core 1.19.545 → 1.19.547
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 +39 -34
- package/docs/copy-inventory.md +5 -5
- package/docs/copy-registry.json +7 -7
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/hall-kit.js +76 -41
- package/modules/hall-ui/public/profile-charts.js +8 -2
- package/modules/ui-design/skills/brandkit/SKILL.md +5 -1
- package/modules/ui-design/skills/design-taste-frontend/SKILL.md +5 -1
- package/modules/ui-design/skills/design-taste-frontend-v1/SKILL.md +5 -1
- package/modules/ui-design/skills/gpt-taste/SKILL.md +5 -1
- package/modules/ui-design/skills/high-end-visual-design/SKILL.md +5 -1
- package/modules/ui-design/skills/image-to-code/SKILL.md +5 -1
- package/modules/ui-design/skills/imagegen-frontend-mobile/SKILL.md +5 -1
- package/modules/ui-design/skills/imagegen-frontend-web/SKILL.md +5 -1
- package/modules/ui-design/skills/impeccable/SKILL.md +5 -1
- package/modules/ui-design/skills/industrial-brutalist-ui/SKILL.md +5 -1
- package/modules/ui-design/skills/minimalist-ui/SKILL.md +5 -1
- package/modules/ui-design/skills/redesign-existing-projects/SKILL.md +5 -1
- package/modules/ui-design/skills/stitch-design-taste/SKILL.md +5 -1
- package/modules/ui-design/skills/style/SKILL.md +5 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +10 -3
- package/scripts/gds/seed-strict-versioning-tasks.js +275 -0
- package/scripts/gds/skill-lint.js +43 -9
- package/src/module-api.js +1 -1
- package/tests/hall_kit_charts.mjs +59 -0
- package/tests/skill_lint.mjs +51 -2
- package/tests/ui_design_skills.mjs +12 -2
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: imagegen-frontend-mobile
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
The plate-making skill for the narrow end of a web surface, under the owner's hero-only rule. Text-free
|
|
5
|
+
portrait plates and hero objects for the 390 and 320 compositions the kit renders, sized for the fold and
|
|
6
|
+
clear of what a phone reserves, with a sidecar beside every file. Triggers on "a plate for the mobile hero",
|
|
7
|
+
"the masthead at 390". Never a screen, a phone mockup or code.
|
|
4
8
|
---
|
|
5
9
|
|
|
6
10
|
<!-- BEGIN PLATFORM PREAMBLE · ui-design module · one block, byte-identical in every skill the module ships (vendored or rebuilt in-house) so an upstream refresh is a three-way merge; the source is modules/ui-design/skills/PREAMBLE.md and tests/ui_design_skills.mjs fails on drift -->
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: imagegen-frontend-web
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
The plate-making skill for a web surface, under the owner's hero-only rule. Text-free hero plates, hero
|
|
5
|
+
objects and atmosphere backdrops for the slots a composition already has — never one per section, never a
|
|
6
|
+
screen, never a UI comp, and no page code. One material, light and camera across the family, landed with a
|
|
7
|
+
provenance sidecar. Triggers on "make a hero plate", "an atmosphere backdrop".
|
|
4
8
|
---
|
|
5
9
|
|
|
6
10
|
<!-- BEGIN PLATFORM PREAMBLE · ui-design module · one block, byte-identical in every skill the module ships (vendored or rebuilt in-house) so an upstream refresh is a three-way merge; the source is modules/ui-design/skills/PREAMBLE.md and tests/ui_design_skills.mjs fails on drift -->
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: impeccable
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
The design-craft method for any surface, invoked as one skill plus an optional sub-command and target
|
|
5
|
+
("impeccable audit /projects"). Sixteen playbooks — shape, audit, critique, polish, harden, animate,
|
|
6
|
+
typeset, colorize, layout, adapt, distill, clarify, onboard, optimize, document, extract — one per request.
|
|
7
|
+
Triggers on "impeccable", "critique this page", "audit the UI". Web surfaces only.
|
|
4
8
|
---
|
|
5
9
|
|
|
6
10
|
<!-- BEGIN PLATFORM PREAMBLE · ui-design module · one block, byte-identical in every skill the module ships (vendored or rebuilt in-house) so an upstream refresh is a three-way merge; the source is modules/ui-design/skills/PREAMBLE.md and tests/ui_design_skills.mjs fails on drift -->
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: industrial-brutalist-ui
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Raw mechanical interfaces that read like a technical document, inside the instance's world. One archetype
|
|
5
|
+
per project (Swiss industrial print or dark tactical terminal, never mixed), massive uppercase display
|
|
6
|
+
against tracked-out mono metadata, strict grid anchoring, zero corner rounding, halftone and scanline
|
|
7
|
+
degradation inside the Kill Switch. Triggers on "brutalist", "industrial", "make it raw".
|
|
4
8
|
---
|
|
5
9
|
|
|
6
10
|
<!-- BEGIN PLATFORM PREAMBLE · ui-design module · one block, byte-identical in every skill the module ships (vendored or rebuilt in-house) so an upstream refresh is a three-way merge; the source is modules/ui-design/skills/PREAMBLE.md and tests/ui_design_skills.mjs fails on drift -->
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: minimalist-ui
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Quiet editorial product interfaces built out of type and white space, inside the instance's world. Macro
|
|
5
|
+
whitespace first, a capped measure, one display job and one body job, exactly one hairline weight for every
|
|
6
|
+
border, flat tile cards with no shadow, entry motion inside the Kill Switch. Triggers on "make it minimal",
|
|
7
|
+
"clean and editorial", "this feels busy". Declines dense surfaces.
|
|
4
8
|
---
|
|
5
9
|
|
|
6
10
|
<!-- BEGIN PLATFORM PREAMBLE · ui-design module · one block, byte-identical in every skill the module ships (vendored or rebuilt in-house) so an upstream refresh is a three-way merge; the source is modules/ui-design/skills/PREAMBLE.md and tests/ui_design_skills.mjs fails on drift -->
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: redesign-existing-projects
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Upgrades an existing site or app in place, in whatever framework and styling method it already uses, without
|
|
5
|
+
breaking it. Three fixed steps — scan the stack, diagnose against a written audit, apply targeted fixes —
|
|
6
|
+
with the instance's world as the answer key and a fixed impact-per-risk fix order. Triggers on "redesign
|
|
7
|
+
this", "upgrade the existing page", "audit this UI".
|
|
4
8
|
---
|
|
5
9
|
|
|
6
10
|
<!-- BEGIN PLATFORM PREAMBLE · ui-design module · one block, byte-identical in every skill the module ships (vendored or rebuilt in-house) so an upstream refresh is a three-way merge; the source is modules/ui-design/skills/PREAMBLE.md and tests/ui_design_skills.mjs fails on drift -->
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stitch-design-taste
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Writes a design contract document for an external screen-generating tool such as Google Stitch, in the
|
|
5
|
+
platform's own frontmatter shape. Four dials with named bands and stated defaults, then eight fixed sections
|
|
6
|
+
pairing descriptive language with exact values. The thirteen colours are transcribed from the instance's
|
|
7
|
+
pack, never invented. Triggers on "a design brief for Stitch".
|
|
4
8
|
---
|
|
5
9
|
|
|
6
10
|
<!-- BEGIN PLATFORM PREAMBLE · ui-design module · one block, byte-identical in every skill the module ships (vendored or rebuilt in-house) so an upstream refresh is a three-way merge; the source is modules/ui-design/skills/PREAMBLE.md and tests/ui_design_skills.mjs fails on drift -->
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: style
|
|
3
|
-
description:
|
|
3
|
+
description: >-
|
|
4
|
+
Runs a bounded session in which an owner authors their own look for the instance, as a branding pack inside
|
|
5
|
+
the fifteen-token contract, and proves it clean before it lands. Four steps and no more — SEARCH, CREATE,
|
|
6
|
+
PROVE, ADOPT. Triggers on "/style", "make a look", "author a style", "I want our own theme", "add a style to
|
|
7
|
+
the library".
|
|
4
8
|
---
|
|
5
9
|
|
|
6
10
|
<!-- BEGIN PLATFORM PREAMBLE · ui-design module · one block, byte-identical in every skill the module ships (vendored or rebuilt in-house) so an upstream refresh is a three-way merge; the source is modules/ui-design/skills/PREAMBLE.md and tests/ui_design_skills.mjs fails on drift -->
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.547",
|
|
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.547",
|
|
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.547",
|
|
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/scripts/gds/fitness.js
CHANGED
|
@@ -1311,7 +1311,11 @@ function checkErrorEnvelope() {
|
|
|
1311
1311
|
// truncated EVERY field, the skill matched on its body text, and nothing in CI noticed. A parse
|
|
1312
1312
|
// error is a hard fail; description length and the listing budget are advisory (the listing is
|
|
1313
1313
|
// resident in every session, ~1% of context, and gets truncated past ~8k chars). The check reads
|
|
1314
|
-
// the instance's .claude/skills/
|
|
1314
|
+
// every skill Claude Code can load — the instance's .claude/skills/ AND the module-owned
|
|
1315
|
+
// modules/*/skills/ that claude-materialize.js copies into it (task 1003587: scanning only the
|
|
1316
|
+
// tracked root meant 14 ui-design skills went unlinted until the untracked copy existed, so four
|
|
1317
|
+
// unparseable frontmatters shipped and fitness only went red on a checkout that had materialized).
|
|
1318
|
+
// A quiet pass when a checkout carries none (published core).
|
|
1315
1319
|
function checkSkillFrontmatter() {
|
|
1316
1320
|
const violations = [];
|
|
1317
1321
|
const warnings = [];
|
|
@@ -1329,7 +1333,10 @@ function checkSkillFrontmatter() {
|
|
|
1329
1333
|
}
|
|
1330
1334
|
if (long.length) {
|
|
1331
1335
|
long.sort((a, b) => b.n - a.n);
|
|
1332
|
-
|
|
1336
|
+
// The skill's own name is its PARENT dir — a fixed path index would read "skills"
|
|
1337
|
+
// for a module-owned path (modules/<key>/skills/<name>/SKILL.md).
|
|
1338
|
+
const skillName = (f) => f.split('/').slice(-2, -1)[0] || f;
|
|
1339
|
+
warnings.push(`${long.length} description(s) over ${lint.DESC_WARN_CHARS} chars (longest: ${long.slice(0, 4).map((l) => `${skillName(l.file)} ${l.n}`).join(', ')}) — every char is resident in every session; run node scripts/gds/skill-lint.js.`);
|
|
1333
1340
|
}
|
|
1334
1341
|
if (report.overBudget) warnings.push(`the skill listing is ${report.listingChars} chars (~${estTokens(report.listingChars)} tokens) against a ~${report.budgetChars}-char budget — Claude Code truncates entries past it and routing degrades.`);
|
|
1335
1342
|
return {
|
|
@@ -1338,7 +1345,7 @@ function checkSkillFrontmatter() {
|
|
|
1338
1345
|
hardFail: violations.length > 0,
|
|
1339
1346
|
violations,
|
|
1340
1347
|
warnings,
|
|
1341
|
-
note: `${report.results.length} SKILL.md file(s) under .claude/skills/ parse as YAML (name + description present, no ": " / " #" in a plain scalar); length + listing budget advisory.`,
|
|
1348
|
+
note: `${report.results.length} SKILL.md file(s) under .claude/skills/ + modules/*/skills/ parse as YAML (name + description present, no ": " / " #" in a plain scalar); length + listing budget advisory.`,
|
|
1342
1349
|
};
|
|
1343
1350
|
}
|
|
1344
1351
|
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/seed-strict-versioning-tasks.js — seeds goal 1000086 ("Strict
|
|
3
|
+
// versioning: a goal cannot outlive its tasks") from the spec at
|
|
4
|
+
// docs/specs/bongos-v1-strict-versioning.md. Idempotent on source_ref, throttled
|
|
5
|
+
// against the per-IP write limiter, and self-verifying: the last step diffs the
|
|
6
|
+
// criterion links that actually landed against the plan and exits non-zero on
|
|
7
|
+
// any drift (task 1003076).
|
|
8
|
+
|
|
9
|
+
const { execFile } = require('child_process');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { expectedCriterionCounts, verifySeedCriteriaLinks } = require('./seed-verify-criteria');
|
|
12
|
+
|
|
13
|
+
const VERSION = 'BONGOS-V1';
|
|
14
|
+
const GOAL = 1000086;
|
|
15
|
+
const PREFIX = 'strict-versioning';
|
|
16
|
+
const API = path.join(__dirname, 'api.js');
|
|
17
|
+
const WRITE_DELAY_MS = 4200;
|
|
18
|
+
|
|
19
|
+
const C1 = 'sv-goal-set-closed';
|
|
20
|
+
const C2 = 'sv-goal-outlives-nothing';
|
|
21
|
+
const C3 = 'sv-killing-a-goal-is-deliberate';
|
|
22
|
+
const C4 = 'sv-every-task-has-a-goal';
|
|
23
|
+
const C5 = 'sv-a-version-can-close';
|
|
24
|
+
const C6 = 'sv-one-train-per-project';
|
|
25
|
+
const C7 = 'sv-cutover-proven-live';
|
|
26
|
+
|
|
27
|
+
// TASKS — ref is the V##.R## rank suffix; deps are refs resolved to real task
|
|
28
|
+
// ids after every task exists. Descriptions stay short and cross-reference by
|
|
29
|
+
// ref rather than restating the spec (which is the durable context).
|
|
30
|
+
const TASKS = [
|
|
31
|
+
{
|
|
32
|
+
ref: 'R01', kind: 'refactor', priority: 5, est: 90, criteria: [C2], deps: [],
|
|
33
|
+
title: 'BV1.R01 — One terminal-status vocabulary: shipped or abandoned, declared once',
|
|
34
|
+
desc: '`goal-rollup.js:37` counts shipped|confirmed|completed as done; `done-when.js:429` counts only shipped and treats abandoned as accounted-for. Two definitions of "finished" in one module is what lets a goal read closed on one surface and open on another. Declare the terminal set once and have both read it.\n\n**Done when:** a single exported constant is the only spelling of the terminal set; `goal-rollup.js` and `done-when.js` both consume it; `tests/goal_sort_staleness.mjs` (which pins the rollup against the client `goals-lib.js`) still passes, or is updated with the reason.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
ref: 'R02', kind: 'decision', priority: 5, est: 120, criteria: [C1], deps: [],
|
|
38
|
+
title: 'BV1.R02 — ADR: strict versioning — the version boundary is the scope gate',
|
|
39
|
+
desc: 'Write the ADR behind goal 1000086. Records the model: goals are scoped onto a `planning` version, the goal set closes when the version starts building, an Archon override admits a named goal with a reason, a goal cannot close while it holds non-terminal work, and exactly one version builds per project. Cite the measured state that motivated it (62 goals, 10 closed while holding 28 open tasks, 502 open tasks across 37 open goals, two building versions on one project).\n\n**Done when:** the ADR exists, is indexed in `docs/adr/README.md`, and is linked from goal 1000086. Take the next free ADR number immediately before pushing — concurrent agents collide on it.',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
ref: 'R05', kind: 'feature', priority: 5, est: 90, criteria: [C4], deps: [],
|
|
43
|
+
title: 'BV1.R05 — POST /tasks and PATCH /tasks/:id read goal.status',
|
|
44
|
+
desc: '`POST /goals/:id/tasks` already refuses a non-open goal (`goal-route-authz.js:47`, 409 `goal_not_open`), but `POST /tasks` (`task-write-routes.js:167`) and `PATCH /tasks/:id` (`:392`) validate only that the goal exists and belongs to the version. A task can be created directly into an achieved or archived goal today. Close the asymmetry.\n\n**Done when:** both vectors refuse a non-open goal with the same named 409 the goal-scoped route uses; a test covers each vector.',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
ref: 'R03', kind: 'feature', priority: 5, est: 120, criteria: [C1], deps: ['R02'],
|
|
48
|
+
title: 'BV1.R03 — POST /goals refuses a version that is not in planning',
|
|
49
|
+
desc: '`routes/goals.js:101` fetches the version row and discards it after the null check — `version.status` is never read, so a goal can be created on a shipped version. Add the gate: goals may be created on a `planning` version (that is where the next version is scoped in advance); a `building` version refuses without the R07 override.\n\n**Done when:** a pure `authorizeGoalCreate` lives beside `authorizeGoalTaskCreate` in `goal-route-authz.js` and is unit-tested DB-free; the refusal is a named 409 an agent can act on; reuse `versionAcceptsNewWork`\'s status vocabulary rather than respelling it.',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
ref: 'R04', kind: 'feature', priority: 5, est: 90, criteria: [C1], deps: ['R02'],
|
|
53
|
+
title: 'BV1.R04 — POST /versions refuses a second planning version for a project',
|
|
54
|
+
desc: 'With R03 in place, a builder blocked from adding a goal to the building version could cut a second planning version and put it there. One planning version per project means exactly one queue for future goals and no parallel escape hatches.\n\n**Done when:** `POST /versions` returns a named 409 when a planning version already exists for the project; a test covers it.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
ref: 'R06', kind: 'spike', priority: 5, est: 180, criteria: [C5], deps: ['R02'],
|
|
58
|
+
title: 'BV1.R06 — Design pass: how a version closes (auto, early, roll-forward, maintenance exemption)',
|
|
59
|
+
desc: 'No route ships a version — `scripts/gds/version-close.js:290` prints that the API exposes no version status transitions, and the flip is hand-run SQL on the droplet. The `version.close` permission exists at the archon floor (`government/catalog.js:103`) and nothing references it. Design the lifecycle before building it: auto-close when the last non-maintenance goal achieves, Archon early-close with a per-goal disposition, roll-forward via `succeeds_goal_id` lineage, and the maintenance goal\'s exemption from the close count. Reuse `version-close.js:66` `suggestClose` and `getCarryOverCount` — the judgement already exists and is merely advisory.\n\n**Done when:** an ADR states the model and the sub-tasks it implies are filed. Spike — will not auto-promote; open it manually.',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
ref: 'R07', kind: 'feature', priority: 5, est: 150, criteria: [C1], deps: ['R03'],
|
|
63
|
+
title: 'BV1.R07 — Archon override: admit one named goal to a building version, with a reason',
|
|
64
|
+
desc: 'The escape valve for R03. An Archon may admit a specific net-new goal into an in-flight version and must record why. Per-goal, never a blanket unlock.\n\n**Done when:** an override row stores the goal, the actor, the reason and the timestamp; only an Archon can create one; a goal admitted this way is created normally in every other respect.',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
ref: 'R08', kind: 'feature', priority: 5, est: 120, criteria: [C2], deps: ['R01'],
|
|
68
|
+
title: 'BV1.R08 — achieveGoalIfComplete refuses a goal that still holds non-terminal tasks',
|
|
69
|
+
desc: '`db-goals.js:193-213` decides achievement from `done_when_criteria` alone; tasks are never queried. A goal whose criteria are all satisfied auto-achieves while holding any number of backlog or ready tasks, because those tasks are linked to no criterion. Add the second condition to the manual-satisfy path.\n\n**Done when:** the check uses R01\'s terminal set; a goal holding a ready/backlog/blocked/active/completed/confirmed task cannot reach achieved via `POST /done-when/:id/satisfy`.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
ref: 'R09', kind: 'feature', priority: 5, est: 120, criteria: [C2], deps: ['R01'],
|
|
73
|
+
title: 'BV1.R09 — The achievement cascade gains the same check, on the caller exec',
|
|
74
|
+
desc: 'The sibling of R08 in `done-when.js:451-460` — the cascade inside `autoSatisfyShippedCriteria`, which is the path that actually fires on every ship (`db-ship.js:434`) and every reconciler tick (`publish-reconciler.js:847`). It is a deliberate duplicate of the `db-goals.js` rule (collapsing them creates a require cycle), so both must change or neither does.\n\n**Done when:** the same predicate is a third NOT EXISTS clause on the cascade UPDATE, run on `exec` and never the module pool, so the ship transaction sees its own uncommitted flips. Shipping itself must remain unblocked — only the goal flip is gated.',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
ref: 'R10', kind: 'feature', priority: 5, est: 120, criteria: [C3], deps: ['R01'],
|
|
78
|
+
title: 'BV1.R10 — Archive previews its open tasks and refuses without a disposition',
|
|
79
|
+
desc: '`POST /goals/:id/archive` (`routes/goals.js:1043`) refuses only if the goal is already archived — no criterion check, no task check. It is the hole that bypasses R08/R09 entirely, and how goal 1000018 was archived with all 7 tasks still open. Archiving a goal that holds open work must first return that work.\n\n**Done when:** archiving a goal with non-terminal tasks returns them and a named 409; archiving one with none stays a single call.',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
ref: 'R11', kind: 'feature', priority: 5, est: 150, criteria: [C4], deps: ['R05'],
|
|
83
|
+
title: 'BV1.R11 — goal_id becomes required; delete the catch-all fallback',
|
|
84
|
+
desc: '`db-tasks.js:150` silently falls back to `generalGoalIdForVersion` when no goal is named, and the two resulting catch-alls hold 53 open tasks. Goal-less work is the hole through which unscoped work accumulates invisibly.\n\n**Done when:** `POST /tasks` requires `goal_id`; the fallback is deleted; every in-repo caller passes a goal. R25 drains the existing catch-alls.',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
ref: 'R12', kind: 'feature', priority: 5, est: 180, criteria: [C5], deps: ['R06'],
|
|
88
|
+
title: 'BV1.R12 — POST /versions/:id/close, behind the version.close permission',
|
|
89
|
+
desc: 'Build what R06 designed. Archon-only, wired to the already-defined `version.close` permission. Any goal still open at close time takes an explicit decision — roll forward (R17) or abandon.\n\n**Done when:** the route exists, refuses a non-Archon, refuses an early close that leaves a goal without a disposition, and sets `shipped_at`.',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
ref: 'R13', kind: 'verify', priority: 4, est: 150, criteria: [C2], deps: ['R08', 'R09'],
|
|
93
|
+
title: 'BV1.R13 — Goal-closure invariant suite',
|
|
94
|
+
desc: 'Pin the C2 invariant across both writers so it cannot regress the way the two vocabularies drifted apart.\n\n**Done when:** tests cover the manual-satisfy path (R08) and the cascade (R09); one test proves the cascade sees the ship transaction\'s uncommitted flip; one test proves a ship still lands while its goal is held open by a sibling backlog task; the existing `goal_achievement.mjs`, `auto_satisfy_criteria.mjs` and `criterion_progress.mjs` all still pass.',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
ref: 'R14', kind: 'feature', priority: 4, est: 180, criteria: [C3], deps: ['R10'],
|
|
98
|
+
title: 'BV1.R14 — Archive applies the disposition: abandon with a reason, or move to a named goal',
|
|
99
|
+
desc: 'The second half of R10. The caller re-submits naming what happens to each open task; nothing is discarded without a decision, which was the owner\'s explicit constraint.\n\n**Done when:** each task is either abandoned (stamped with the archive reason) or moved to a named surviving goal; the disposition is recorded per task; the goal ends archived holding zero non-terminal tasks.',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
ref: 'R15', kind: 'feature', priority: 4, est: 120, criteria: [C1], deps: ['R07'],
|
|
103
|
+
title: 'BV1.R15 — Overrides are countable and visible on the version',
|
|
104
|
+
desc: 'An override nobody can count is indistinguishable from no gate at all. Surface them so abuse is measurable.\n\n**Done when:** the version rollup exposes the override count; the hall shows which goals were admitted mid-version, by whom, and why.',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
ref: 'R16', kind: 'feature', priority: 4, est: 150, criteria: [C5], deps: ['R12', 'R08', 'R09'],
|
|
108
|
+
title: 'BV1.R16 — A version closes itself when its last non-maintenance goal achieves',
|
|
109
|
+
desc: 'The automatic finish line. Fires from the goal-achievement path (R08/R09), skipping the maintenance goal (R18) — without that exemption a version can never auto-close, because the maintenance goal is open by design.\n\n**Done when:** achieving the last non-exempt goal flips the version to closed; a version whose only open goal is the maintenance goal closes; the auto-promotion of the next version (R20) fires from the same event.',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
ref: 'R17', kind: 'feature', priority: 4, est: 150, criteria: [C5], deps: ['R12'],
|
|
113
|
+
title: 'BV1.R17 — Roll-forward: an unfinished goal becomes a successor goal in the next version',
|
|
114
|
+
desc: 'The sanctioned exit for a goal that is not done when its version closes. `POST /goals` already accepts `succeeds_goal_id` and stamps the predecessor\'s `succeeded_by_goal_id` (`routes/goals.js:108,130`) — reuse it rather than inventing a second lineage.\n\n**Done when:** rolling a goal forward creates the successor in the target version with its open tasks, sets both lineage pointers, and archives the predecessor; the chain is queryable across versions.',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
ref: 'R18', kind: 'feature', priority: 4, est: 180, criteria: [C6], deps: ['R06', 'R12'],
|
|
118
|
+
title: 'BV1.R18 — The maintenance goal: auto-created per version, exempt from close, carries bugs forward',
|
|
119
|
+
desc: 'Where a defect against already-shipped work lands. Auto-created with each version, flagged exempt so it never blocks the auto-close count, and on close its unfinished bugs move to the successor version\'s maintenance goal. The one goal that legitimately spans versions.\n\n**Done when:** creating a version creates its maintenance goal; the exempt flag is read by R16; close carries the open bugs forward; the goal is otherwise an ordinary goal.',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
ref: 'R19', kind: 'feature', priority: 4, est: 90, criteria: [C6], deps: ['R04'],
|
|
123
|
+
title: 'BV1.R19 — Refuse a second building version for a project',
|
|
124
|
+
desc: 'BONGOS-V1 and CB-V1 are both building on the same project today, which is why "the current version" has no meaning and R03\'s gate would have two answers. This is the invariant every other rule depends on.\n\n**Done when:** the server refuses to put a second version into `building` for a project; a named 409; a test covers it. R27 resolves the existing violation.',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
ref: 'R20', kind: 'feature', priority: 4, est: 120, criteria: [C6], deps: ['R12', 'R19'],
|
|
128
|
+
title: 'BV1.R20 — The planning version promotes itself when the building version closes',
|
|
129
|
+
desc: 'Keeps the "exactly one building version" invariant machine-maintained and leaves no window where the project has no live train and nothing is claimable.\n\n**Done when:** closing the building version flips the single planning version to building in the same transaction; with no planning version, close still succeeds and the project is simply idle.',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
ref: 'R21', kind: 'feature', priority: 4, est: 90, criteria: [C6], deps: ['R04'],
|
|
133
|
+
title: 'BV1.R21 — Raise version.create back to the Archon floor',
|
|
134
|
+
desc: 'ADR 0157 delegated `version.create` to Metic. Once the version boundary gates all scope (R03), the power to cut a version is the power to bypass every gate in this design.\n\n**Done when:** the catalog floor is archon; an ADR records the partial reversal of ADR 0157 and its reason; `/planning-session` and `/goal-create` docs reflect the new floor.',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
ref: 'R22', kind: 'feature', priority: 4, est: 120, criteria: [C4], deps: ['R11'],
|
|
138
|
+
title: 'BV1.R22 — Capture and idea-routing vectors name a goal',
|
|
139
|
+
desc: 'With R11 landed, any vector that creates a task without a goal simply breaks. `routeIdeaToGoal` (`modules/ideas/inbox.js:328`) already requires an open goal on a building version; `capture.js` and the other creation paths need the same discipline.\n\n**Done when:** every in-repo task-creation vector names a goal or fails loudly with an actionable message; no path silently produces an orphan.',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
ref: 'R23', kind: 'cleanup', priority: 4, est: 60, criteria: [C5], deps: ['R12'],
|
|
143
|
+
title: 'BV1.R23 — version-close.js drives the route instead of printing that the API cannot',
|
|
144
|
+
desc: '`scripts/gds/version-close.js:290` tells the operator the API exposes no version status transitions and to run SQL on the droplet. After R12 that text is false and the SQL path is the bypass.\n\n**Done when:** the script calls `POST /versions/:id/close`; the droplet-SQL instruction is deleted; the advisory `suggestClose` output is kept.',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
ref: 'R24', kind: 'feature', priority: 3, est: 150, criteria: [C3], deps: ['R14'],
|
|
148
|
+
title: 'BV1.R24 — Hall surface: the archive two-step, and why a goal is held open',
|
|
149
|
+
desc: 'The gates are only usable if a builder can see what is holding a goal open before they try to close it, and can walk the archive disposition without curl.\n\n**Done when:** a goal page shows the non-terminal tasks blocking its closure; the archive flow presents the preview and collects the disposition; both follow the hall\'s existing row idioms.',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
ref: 'R25', kind: 'cleanup', priority: 4, est: 180, criteria: [C4], deps: ['R11', 'R14'],
|
|
153
|
+
title: 'BV1.R25 — Rehome the 53 catch-all tasks; drain and archive both catch-alls',
|
|
154
|
+
desc: 'Goal 1000003 ("Work with no goal yet") holds 37 open tasks and CB-V1\'s general goal holds 16. Under R11 no new ones can appear; these are the existing population.\n\n**Done when:** every one of the 53 is moved to a real goal or abandoned with a reason via R14\'s flow; both catch-alls hold zero tasks and are archived; a query proving zero catch-all tasks is captured in the notes.',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
ref: 'R26', kind: 'cleanup', priority: 4, est: 180, criteria: [C7], deps: ['R14'],
|
|
158
|
+
title: 'BV1.R26 — Disposition the 10 closed goals that still hold 28 open tasks',
|
|
159
|
+
desc: 'The existing violations of C2, measured 2026-09-04: goals 1000018 (archived, all 7 tasks open), 1000072 (achieved, 7), 1000051 (4), 1000041 (3), 1000060 (2), 1000031, 1000034, 1000056, 1000061, 1000071 (1 each). Walk each through R14\'s preview-and-dispose flow so the new rule starts from a true state.\n\n**Done when:** all 28 are moved or abandoned with a reason; a query returns zero closed goals holding non-terminal tasks.',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
ref: 'R27', kind: 'infra', priority: 4, est: 180, criteria: [C7], deps: ['R12', 'R17'],
|
|
163
|
+
title: 'BV1.R27 — Fold CB-V1 into BONGOS-V1 and close CB-V1',
|
|
164
|
+
desc: 'CB-V1 is the same project as BONGOS-V1 ("CB" is Cloud Bongos), and both are building — the violation R19 forbids. Its 4 open goals (1000043, 1000066, 1000084 and its general catch-all) move to BONGOS-V1; its 2 achieved goals stay with the closed version.\n\n**Done when:** the open goals are in BONGOS-V1 with lineage intact; CB-V1 is closed via R12; its 98 non-terminal tasks are all accounted for under a real goal; exactly one version is building.',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
ref: 'R28', kind: 'decision', priority: 4, est: 120, criteria: [C7], deps: ['R27', 'R26'],
|
|
168
|
+
title: 'BV1.R28 — Owner names the goals that ARE BONGOS-V1; everything else moves to V2',
|
|
169
|
+
desc: 'Measured during the planning session: a 14-day activity window keeps 28 of 37 open goals and 454 of 502 open tasks in V1 — nothing is stale because the instance is busy, so no mechanical rule produces a finish line. The owner names the handful of goals that define what V1 means (target ~5-8); every other open goal is targeted at BONGOS-V2 regardless of activity.\n\n**Done when:** the list is recorded in the task notes with each goal\'s open-task count; every open goal is labelled stay or move. Needs the owner — file a blocker if it stalls.',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
ref: 'R29', kind: 'feature', priority: 4, est: 180, criteria: [C7], deps: ['R28', 'R03'],
|
|
173
|
+
title: 'BV1.R29 — Scope BONGOS-V2 in planning, with its goal set and criteria',
|
|
174
|
+
desc: 'The first version created under the new rules, and the proof that scoping-before-building works. Created in `planning` so its goals can be authored (R03), holding every goal R28 marked move.\n\n**Done when:** BONGOS-V2 exists in `planning`; the moved goals are on it as successors with lineage; every goal on it has at least one done-when criterion; it is the only planning version.',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
ref: 'R30', kind: 'verify', priority: 4, est: 120, criteria: [C7], deps: ['R29', 'R25', 'R20', 'R13'],
|
|
178
|
+
title: 'BV1.R30 — Proof: the four invariants hold on the live instance',
|
|
179
|
+
desc: 'The single live demonstration that goal 1000086 is met. Run each query against live and capture its output verbatim in the task notes:\n1. zero goals in achieved/archived holding a task that is not shipped or abandoned;\n2. zero tasks in a catch-all goal, and zero tasks with a null goal;\n3. exactly one version in `building` for the project, and at most one in `planning`;\n4. BONGOS-V2 exists in `planning` with criteria on every goal.\nThen attempt one refusal of each gate (add a goal to the building version; close a goal holding a backlog task; archive without a disposition) and record the 409s.\n\n**Done when:** all four queries return the expected result and all three refusals fire, with transcripts in the notes.',
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
function api(method, route, body) {
|
|
184
|
+
return new Promise((resolve, reject) => {
|
|
185
|
+
const args = [API, method, route];
|
|
186
|
+
if (body) args.push('--body', JSON.stringify(body));
|
|
187
|
+
execFile('node', args, { maxBuffer: 16 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
188
|
+
const raw = String(stdout || '');
|
|
189
|
+
const start = raw.indexOf('{');
|
|
190
|
+
let json = null;
|
|
191
|
+
if (start >= 0) { try { json = JSON.parse(raw.slice(start)); } catch { /* non-JSON body */ } }
|
|
192
|
+
const status = Number((String(stderr || '') + raw).match(/→ (\d{3})/)?.[1] || 0);
|
|
193
|
+
if (err && !json) return reject(new Error(`${method} ${route} failed: ${err.message}\n${stderr}`));
|
|
194
|
+
resolve({ ok: status >= 200 && status < 300, status, json });
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
200
|
+
|
|
201
|
+
// throttledWrite — the per-IP limiter kills a long seed halfway unless each
|
|
202
|
+
// write is paced and a 429 backs off on the server's own retry_after.
|
|
203
|
+
async function throttledWrite(method, route, body) {
|
|
204
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
205
|
+
const r = await api(method, route, body);
|
|
206
|
+
if (r.status === 429) {
|
|
207
|
+
// The limiter answers res.fail('rate_limited', 429, {scope, retry_after_seconds}),
|
|
208
|
+
// and the error envelope nests that payload under error.details — same path
|
|
209
|
+
// seed-bongos-v1-idea-routing-tasks.js reads. Any other spelling silently
|
|
210
|
+
// falls back to the default and ignores the server's computed wait.
|
|
211
|
+
const wait = (Number(r.json?.error?.details?.retry_after_seconds) || 30) * 1000 + 1000;
|
|
212
|
+
console.log(` 429 — backing off ${wait}ms`);
|
|
213
|
+
await sleep(wait);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
await sleep(WRITE_DELAY_MS);
|
|
217
|
+
return r;
|
|
218
|
+
}
|
|
219
|
+
throw new Error(`${method} ${route} still rate-limited after 5 attempts`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// The meta planning task (1003586) was created before this seed ran and links
|
|
223
|
+
// to C1. It is a real link on the goal, so the plan must count it or the
|
|
224
|
+
// verification below reports a false mismatch.
|
|
225
|
+
const PRE_EXISTING_LINKS = [[C1]];
|
|
226
|
+
|
|
227
|
+
async function main() {
|
|
228
|
+
const ids = {};
|
|
229
|
+
const planned = [...PRE_EXISTING_LINKS];
|
|
230
|
+
|
|
231
|
+
console.log(`Seeding ${TASKS.length} tasks into goal ${GOAL} (${VERSION})\n`);
|
|
232
|
+
|
|
233
|
+
for (const t of TASKS) {
|
|
234
|
+
const body = {
|
|
235
|
+
version_id: VERSION,
|
|
236
|
+
goal_id: GOAL,
|
|
237
|
+
title: t.title,
|
|
238
|
+
description: t.desc,
|
|
239
|
+
kind: t.kind,
|
|
240
|
+
discipline: 'engineer',
|
|
241
|
+
priority: t.priority,
|
|
242
|
+
est_minutes: t.est,
|
|
243
|
+
source: 'planning-session',
|
|
244
|
+
source_ref: `${PREFIX}-${t.ref}`,
|
|
245
|
+
criterion_ids: t.criteria,
|
|
246
|
+
};
|
|
247
|
+
const r = await throttledWrite('POST', '/api/gds/tasks', body);
|
|
248
|
+
const id = r.json?.task?.id;
|
|
249
|
+
if (!id) throw new Error(`No task id back for ${t.ref} (status ${r.status}): ${JSON.stringify(r.json)}`);
|
|
250
|
+
ids[t.ref] = id;
|
|
251
|
+
planned.push(t.criteria);
|
|
252
|
+
console.log(` ${t.ref} -> task ${id} [${t.criteria.join(',')}] ${r.status}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
console.log('\nWiring dependencies...');
|
|
256
|
+
for (const t of TASKS) {
|
|
257
|
+
for (const dep of t.deps) {
|
|
258
|
+
if (!ids[dep]) throw new Error(`${t.ref} depends on unknown ref ${dep}`);
|
|
259
|
+
const r = await throttledWrite('POST', `/api/gds/tasks/${ids[t.ref]}/dependencies`, { depends_on_task_id: ids[dep] });
|
|
260
|
+
console.log(` ${t.ref} depends on ${dep} (${ids[dep]}) ${r.status}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log('\nVerifying criterion links landed as planned...');
|
|
265
|
+
await verifySeedCriteriaLinks({
|
|
266
|
+
goalId: GOAL,
|
|
267
|
+
expectedCounts: expectedCriterionCounts(planned),
|
|
268
|
+
fetchGoal: (goalId) => api('GET', `/api/gds/goals/${goalId}`),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
console.log('\nSeed complete. Task ids:');
|
|
272
|
+
for (const t of TASKS) console.log(` ${t.ref} = ${ids[t.ref]}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
main().catch((e) => { console.error(e.message); process.exit(1); });
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
// verdict as authoritative over this one.
|
|
24
24
|
//
|
|
25
25
|
// Usage:
|
|
26
|
-
// node scripts/gds/skill-lint.js lint every
|
|
26
|
+
// node scripts/gds/skill-lint.js lint every skill Claude Code can load — .claude/skills/*
|
|
27
|
+
// plus the module-owned modules/*/skills/* that materialize
|
|
28
|
+
// into it; exit 1 on any error
|
|
27
29
|
// node scripts/gds/skill-lint.js <file...> lint specific files
|
|
28
30
|
// node scripts/gds/skill-lint.js --hook PostToolUse mode: read the hook JSON on stdin, lint the
|
|
29
31
|
// edited file if it is a SKILL.md, print findings, exit 0
|
|
@@ -108,14 +110,40 @@ function lintSkillFile(text, { file = 'SKILL.md' } = {}) {
|
|
|
108
110
|
return res;
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
// Every SKILL.md
|
|
112
|
-
function
|
|
113
|
-
const dir = path.join(root, '.claude', 'skills');
|
|
113
|
+
// Every SKILL.md in one skills root (one level deep, plus a `vendor/` tier).
|
|
114
|
+
function skillsIn(dir) {
|
|
114
115
|
if (!fs.existsSync(dir)) return [];
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.
|
|
116
|
+
const out = [];
|
|
117
|
+
for (const d of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
118
|
+
if (!d.isDirectory()) continue;
|
|
119
|
+
const own = path.join(dir, d.name, 'SKILL.md');
|
|
120
|
+
if (fs.existsSync(own)) { out.push(own); continue; }
|
|
121
|
+
if (d.name === 'vendor') out.push(...skillsIn(path.join(dir, d.name)));
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Every skill Claude Code can end up loading — the tracked `.claude/skills/` PLUS the
|
|
127
|
+
// module-owned `modules/<key>/skills/`, because claude-materialize.js copies those into
|
|
128
|
+
// `.claude/skills/` as untracked copies (task 1003330) and a defect in one only shows up
|
|
129
|
+
// after materialization. Scanning only the tracked root left 14 ui-design skills unlinted
|
|
130
|
+
// until the copy existed, which is exactly how four unparseable frontmatters shipped
|
|
131
|
+
// (task 1003587). A name already present in `.claude/skills/` wins and its module copy is
|
|
132
|
+
// skipped — the same precedence the materializer applies — so nothing is counted twice.
|
|
133
|
+
function listSkillFiles(root = REPO_ROOT) {
|
|
134
|
+
const core = skillsIn(path.join(root, '.claude', 'skills'));
|
|
135
|
+
const seen = new Set(core.map((f) => path.basename(path.dirname(f))));
|
|
136
|
+
const modulesDir = path.join(root, 'modules');
|
|
137
|
+
const owned = [];
|
|
138
|
+
if (fs.existsSync(modulesDir)) {
|
|
139
|
+
for (const d of fs.readdirSync(modulesDir, { withFileTypes: true })) {
|
|
140
|
+
if (!d.isDirectory()) continue;
|
|
141
|
+
for (const f of skillsIn(path.join(modulesDir, d.name, 'skills'))) {
|
|
142
|
+
if (!seen.has(path.basename(path.dirname(f)))) owned.push(f);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return [...core, ...owned];
|
|
119
147
|
}
|
|
120
148
|
|
|
121
149
|
// Lint a set of files; returns per-file results plus the listing total vs budget.
|
|
@@ -125,7 +153,13 @@ function lintSkills(files) {
|
|
|
125
153
|
return { results, listingChars, budgetChars: LISTING_BUDGET_CHARS, overBudget: listingChars > LISTING_BUDGET_CHARS };
|
|
126
154
|
}
|
|
127
155
|
|
|
128
|
-
|
|
156
|
+
// The PostToolUse hook's gate. It must match a module-owned SKILL.md too — that is the
|
|
157
|
+
// file a builder actually edits; the `.claude/skills/` copy is generated (task 1003587).
|
|
158
|
+
function isSkillMd(p) {
|
|
159
|
+
const s = String(p || '');
|
|
160
|
+
return /[\\/]\.claude[\\/]skills[\\/](?:vendor[\\/])?[^\\/]+[\\/]SKILL\.md$/.test(s)
|
|
161
|
+
|| /[\\/]modules[\\/][^\\/]+[\\/]skills[\\/](?:vendor[\\/])?[^\\/]+[\\/]SKILL\.md$/.test(s);
|
|
162
|
+
}
|
|
129
163
|
|
|
130
164
|
function format(report, { relTo = REPO_ROOT } = {}) {
|
|
131
165
|
const out = [];
|
package/src/module-api.js
CHANGED
|
@@ -49,7 +49,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
49
49
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
50
50
|
// the entry to that file. Look for a version's history there, not here.
|
|
51
51
|
// ---------------------------------------------------------------------------
|
|
52
|
-
const CORE_VERSION = '1.19.
|
|
52
|
+
const CORE_VERSION = '1.19.547'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
53
53
|
|
|
54
54
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
55
55
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -623,3 +623,62 @@ test('CAL: the grid gets its own horizontal strip — it never widens the docume
|
|
|
623
623
|
const html = kit.calendarGridHtml([{ local_date: '2026-09-03', count: 1 }], { today: '2026-09-03' });
|
|
624
624
|
assert.match(html, /kit-cal__scroll/, 'the wide grid scrolls inside its own strip');
|
|
625
625
|
});
|
|
626
|
+
|
|
627
|
+
// --- task 1003583: the summary and the caption must describe the DRAWN window ------
|
|
628
|
+
|
|
629
|
+
test('CAL: a narrowed window is described as weeks, not as "the last year"', () => {
|
|
630
|
+
// The bug: the default aria summary hardcoded "over the last year" whatever
|
|
631
|
+
// opts.weeks said, so a caller asking for six weeks was handed a label describing
|
|
632
|
+
// fifty-three.
|
|
633
|
+
const html = kit.calendarGridHtml([{ local_date: '2026-09-03', count: 2 }], { today: '2026-09-03', weeks: 6 });
|
|
634
|
+
assert.match(html, /over the last 6 weeks/, 'the aria summary still claims a year');
|
|
635
|
+
assert.doesNotMatch(html, /over the last year/);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test('CAL: a one-week window says "week", and the full window still says "the last year"', () => {
|
|
639
|
+
const one = kit.calendarGridHtml([{ local_date: '2026-09-03', count: 1 }], { today: '2026-09-03', weeks: 1 });
|
|
640
|
+
assert.match(one, /over the last 1 week:/, 'singular grammar');
|
|
641
|
+
const full = kit.calendarGridHtml([{ local_date: '2026-09-03', count: 1 }], { today: '2026-09-03', weeks: 53 });
|
|
642
|
+
assert.match(full, /over the last year/, 'the 53-week window is a year and should read like one');
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test('CAL: the summary counts only the days the grid actually draws', () => {
|
|
646
|
+
// 2026-09-03 is a Thursday, so a 2-week grid starts at Sunday 2026-08-23. A ship on
|
|
647
|
+
// 2026-08-01 is real history the caller supplied, but it is off this picture, and it
|
|
648
|
+
// used to be counted in this picture's summary anyway.
|
|
649
|
+
const days = [
|
|
650
|
+
{ local_date: '2026-08-01', count: 4 }, // before the first column
|
|
651
|
+
{ local_date: '2026-08-30', count: 2 }, // inside
|
|
652
|
+
{ local_date: '2026-09-03', count: 1 }, // inside
|
|
653
|
+
];
|
|
654
|
+
const html = kit.calendarGridHtml(days, { today: '2026-09-03', weeks: 2 });
|
|
655
|
+
assert.match(html, /3 ships across 2 days/, 'the out-of-window day leaked into the summary');
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test('CAL: calendarWindow is the one source of truth the caller reads too', () => {
|
|
659
|
+
// profile-charts.js used to recompute "N days shipped" with its own filter over the
|
|
660
|
+
// whole array; it now reads activeDays from here, so the caption and the picture
|
|
661
|
+
// cannot disagree.
|
|
662
|
+
const days = [
|
|
663
|
+
{ local_date: '2026-08-01', count: 4 },
|
|
664
|
+
{ local_date: '2026-08-30', count: 2 },
|
|
665
|
+
{ local_date: '2026-09-03', count: 1 },
|
|
666
|
+
];
|
|
667
|
+
const opts = { today: '2026-09-03', weeks: 2 };
|
|
668
|
+
const win = kit.calendarWindow(days, opts);
|
|
669
|
+
assert.equal(win.activeDays, 2);
|
|
670
|
+
assert.equal(win.totalShips, 3);
|
|
671
|
+
assert.equal(win.weeks, 2);
|
|
672
|
+
// The window's own bounds are what the grid iterates, so the naive filter the caller
|
|
673
|
+
// used to run disagrees with it — which is exactly the bug.
|
|
674
|
+
assert.notEqual(days.filter((d) => d.count > 0).length, win.activeDays);
|
|
675
|
+
const html = kit.calendarGridHtml(days, opts);
|
|
676
|
+
assert.match(html, new RegExp(`across ${win.activeDays} days`), 'grid and window disagree');
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test('CAL: calendarWindow returns null on nothing to draw, matching the grid empty state', () => {
|
|
680
|
+
assert.equal(kit.calendarWindow([], {}), null);
|
|
681
|
+
assert.equal(kit.calendarWindow(null, {}), null);
|
|
682
|
+
assert.equal(kit.calendarWindow([{ local_date: '2026-09-03', count: 0 }], {}), null,
|
|
683
|
+
'a zero-count day is not a ship day');
|
|
684
|
+
});
|