@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.
Files changed (29) hide show
  1. package/.bongos-core.json +39 -34
  2. package/docs/copy-inventory.md +5 -5
  3. package/docs/copy-registry.json +7 -7
  4. package/docs/module-api-changelog.md +4 -0
  5. package/modules/hall-ui/public/hall-kit.js +76 -41
  6. package/modules/hall-ui/public/profile-charts.js +8 -2
  7. package/modules/ui-design/skills/brandkit/SKILL.md +5 -1
  8. package/modules/ui-design/skills/design-taste-frontend/SKILL.md +5 -1
  9. package/modules/ui-design/skills/design-taste-frontend-v1/SKILL.md +5 -1
  10. package/modules/ui-design/skills/gpt-taste/SKILL.md +5 -1
  11. package/modules/ui-design/skills/high-end-visual-design/SKILL.md +5 -1
  12. package/modules/ui-design/skills/image-to-code/SKILL.md +5 -1
  13. package/modules/ui-design/skills/imagegen-frontend-mobile/SKILL.md +5 -1
  14. package/modules/ui-design/skills/imagegen-frontend-web/SKILL.md +5 -1
  15. package/modules/ui-design/skills/impeccable/SKILL.md +5 -1
  16. package/modules/ui-design/skills/industrial-brutalist-ui/SKILL.md +5 -1
  17. package/modules/ui-design/skills/minimalist-ui/SKILL.md +5 -1
  18. package/modules/ui-design/skills/redesign-existing-projects/SKILL.md +5 -1
  19. package/modules/ui-design/skills/stitch-design-taste/SKILL.md +5 -1
  20. package/modules/ui-design/skills/style/SKILL.md +5 -1
  21. package/package-lock.json +2 -2
  22. package/package.json +1 -1
  23. package/scripts/gds/fitness.js +10 -3
  24. package/scripts/gds/seed-strict-versioning-tasks.js +275 -0
  25. package/scripts/gds/skill-lint.js +43 -9
  26. package/src/module-api.js +1 -1
  27. package/tests/hall_kit_charts.mjs +59 -0
  28. package/tests/skill_lint.mjs +51 -2
  29. package/tests/ui_design_skills.mjs +12 -2
@@ -1,6 +1,10 @@
1
1
  ---
2
2
  name: imagegen-frontend-mobile
3
- description: The plate-making skill for the narrow end of a web surface on ANY Cloud Bongos instance, under the owner's hero-only rule: it produces text-free portrait plates and hero objects for the 390 and 320 compositions the kit renders, the masthead above the words and the field behind them, sized for the fold and clear of the regions a phone reserves. Never a screen, never a phone mockup, never a flow of screens as images, and never code: a screen concept is the surface's states file rendered by the kit at 390, and a native app target declines to /design. Locks one material, light and camera across the family from the world's DESIGN.md, keeps the first view to one focal point, puts a scrim under any text that sits over a plate, and lands the pair convention with a sidecar beside every file. Triggers on "a plate for the mobile hero", "the masthead at 390", "the hero object for the phone collapse", "a portrait plate", or when adapt or design-taste-frontend names a narrow-width slot with no plate. Ported in-house from the taste-skill rebuild spec (tasks 1003325 and 1003328, ADR 0198) with the hero-only rewrite the upstream never had.
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: The plate-making skill for a web surface on ANY Cloud Bongos instance, under the owner's hero-only rule: it produces text-free hero plates, hero objects and atmosphere backdrops for the slots a composition already has, never one image per section, never a screen, never a UI comp, and it writes no page code. Commits to the plate list out loud, chooses a composition anchor per plate that is not the text-left image-right default, locks one material, one light and one camera across the family, matches every plate tonally to the instance's pack, runs the generation through the instance's art pipeline key, and lands each plate as the pair convention (a black-ground plate for dark, an alpha cutout for light) with a provenance sidecar beside every file. Triggers on "make a hero plate", "we need a hero object for", "an atmosphere backdrop for the landing", "the project orbs need a new material", or when design-taste-frontend or /design names a plate slot with no plate. Ported in-house from the taste-skill rebuild spec (tasks 1003325 and 1003328, ADR 0198) with the hero-only rewrite the upstream never had.
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: The design-craft method for ANY surface on ANY Cloud Bongos instance, invoked as one skill plus an optional sub-command and an optional target ("impeccable audit /projects", "impeccable polish the hall's ranks page", "impeccable harden the wizard"). Sixteen sub-command playbooks (shape, audit, critique, polish, harden, animate, typeset, colorize, layout, adapt, distill, clarify, onboard, optimize, document, extract) plus the two directions (bolder, quieter), exactly one loaded per request; a bare description with no sub-command is ordinary design work routed to the closest playbook; no argument prints a menu built from live project signals and never auto-runs anything. Carries the craft floor (loaded right before UI is edited, never recited to the user) and the bounded finish (build fully, one render round through the kit, one batch of fixes, at most one confirming round, stop). Triggers on "impeccable", "critique this page", "audit the UI", "polish the whole path", "make it survive long text", "animate this", "tighten the copy", "document the design system", "extract the repeated pattern". Rebuilt in-house from the impeccable rebuild spec (task 1003326, ADR 0198) with no hooks, no bypass agent, no telemetry and no live server; the live loop is the playwright plugin and the headless loop is the module's kit. Web surfaces only; a native mobile target declines to /design.
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: Raw mechanical interfaces that read like a technical document, inside the instance's world. One archetype per project (Swiss industrial print or dark tactical terminal, never mixed), massive uppercase clamped display against fixed tracked-out mono metadata, the pack's terracotta as the single hazard accent and its sea as one terminal green, strict grid anchoring with the hairline as a one-pixel grid gap, zero corner rounding, bracket and crosshair framing glyphs, and static halftone, scanline and noise degradation inside the Kill Switch. Triggers on "brutalist", "industrial", "terminal", "make it raw", "like a technical document". Rebuilt in-house from the taste-skill rebuild spec (task 1003327, ADR 0198).
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: Quiet editorial product interfaces built out of type and white space, inside the instance's world. Macro whitespace first, a capped measure, one display job and one body job, exactly one hairline weight for every border in the sheet, washed accents measured against their own ink, flat tile cards with no shadow, box-less accordions, keycap shortcuts, one depth treatment per band, entry motion inside the Kill Switch. Triggers on "make it minimal", "clean and editorial", "quiet", "document-like", "this feels busy". Declines dense surfaces like dashboards and ledgers. Rebuilt in-house from the taste-skill rebuild spec (task 1003327, ADR 0198); the palette is the pack and the nearest library look is expedition.
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: Upgrades an existing site or app in place, in whatever framework and styling method it already uses, without breaking it. A fixed three-step sequence (scan the stack, diagnose against a written audit, apply targeted fixes), the seven-group audit (typography, colour and surface, layout, interaction, content, code and omissions) with the instance's world as the answer key, a catalogue of upgrade techniques marked CSS-only or not available, and a fixed impact-per-risk fix order. Triggers on "redesign this", "upgrade the existing page", "make the current X premium", "audit this UI", "this page looks generic, fix it". Rebuilt in-house from the taste-skill rebuild spec (task 1003325, ADR 0198); keeps the stack, never migrates frameworks, images are hero plates only.
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: Writes a design contract document for an external screen-generating tool such as Google Stitch, in the platform's own frontmatter shape. Four dials (creativity, density, variance, motion intent) with named bands and stated defaults, then eight fixed sections (atmosphere, colour roles, typography, components, layout, responsive rules, motion intent, banned patterns) written as descriptive language paired with exact values. The thirteen colours are transcribed from the instance's pack and diffed, never invented; motion is an intent brief inside the Kill Switch; the screens that come back are a reference, not a source of truth. Triggers on "write a design brief for Stitch", "a design contract document", "a prompt-to-screen tool". Rebuilt in-house from the taste-skill rebuild spec (task 1003327, ADR 0198).
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: Runs a bounded session in which an owner authors their OWN look for the instance, as a branding pack inside the fifteen-token contract, and proves it clean before it lands. Four steps and no more. SEARCH reads the instance's world and the style library, renders the nearest two entries so the owner looks rather than imagines, and asks two or three questions about feel. CREATE drafts modules/ui-design/styles/<name>/ in the library's contract shape (pack.json, DESIGN.md, mock.html), never a fork of a sheet and never a sixteenth token. PROVE retunes the thirteen against contrast.js, the styles suite, check-mock and the kit's renders until every named pair is green, showing the shots each round. ADOPT hands the owner the one-step switch and files the look into the catalogue. Triggers on "/style", "make a look", "author a style", "I want our own theme", "can we have a different look", "add a style to the library".
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.545",
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.545",
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.545",
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",
@@ -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/ and is a quiet pass when a checkout carries none (published core).
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
- warnings.push(`${long.length} description(s) over ${lint.DESC_WARN_CHARS} chars (longest: ${long.slice(0, 4).map((l) => `${l.file.split('/')[2]} ${l.n}`).join(', ')}) — every char is resident in every session; run node scripts/gds/skill-lint.js.`);
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 .claude/skills/*/SKILL.md; exit 1 on any error
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 under .claude/skills/ (one level deep).
112
- function listSkillFiles(root = REPO_ROOT) {
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
- return fs.readdirSync(dir, { withFileTypes: true })
116
- .filter((d) => d.isDirectory())
117
- .map((d) => path.join(dir, d.name, 'SKILL.md'))
118
- .filter((f) => fs.existsSync(f));
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
- function isSkillMd(p) { return /[\\/]\.claude[\\/]skills[\\/][^\\/]+[\\/]SKILL\.md$/.test(String(p || '')); }
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.545'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
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
+ });