@bongos/core 1.19.342 → 1.19.343
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 +31 -16
- package/docs/adr/0189-baseline-floors-and-the-freeze-detector.md +106 -0
- package/docs/adr/README.md +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/baseline-staleness.js +217 -0
- package/scripts/gds/control-manifest.js +19 -0
- package/scripts/gds/fitness-ratchets.js +19 -3
- package/scripts/gds/fitness.js +1 -0
- package/scripts/gds/redteam-patrol.js +11 -0
- package/src/module-api.js +3 -1
- package/tests/baseline_staleness.mjs +198 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.343",
|
|
6
|
+
"core_contract": "1.19.343",
|
|
7
|
+
"source_commit": "5c168bafbc0d541f30c65cfec7805745bebee25f",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-25T01:
|
|
9
|
+
"built_at": "2026-08-25T01:53:23.065Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 327,
|
|
13
13
|
"agent_docs_stubbed": 22,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1489,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1838,
|
|
20
|
+
"tree_sha256": "dc54f604edbfff9d11a92f6ebc16d5d66a0cdb7763a5bfc8b9aae48481dea3d7",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1424,10 +1424,15 @@
|
|
|
1424
1424
|
"mode": "0000644",
|
|
1425
1425
|
"sha256": "4feacf7bc809cdff1a23a1ef5b57d5d28aa448b80bd1ad24fc70860c0fe6739f"
|
|
1426
1426
|
},
|
|
1427
|
+
{
|
|
1428
|
+
"path": "docs/adr/0189-baseline-floors-and-the-freeze-detector.md",
|
|
1429
|
+
"mode": "0000644",
|
|
1430
|
+
"sha256": "3067e87c4a2a1d63cb7b16526314b643f1940c4333cafe36627cd9bb5a293c0c"
|
|
1431
|
+
},
|
|
1427
1432
|
{
|
|
1428
1433
|
"path": "docs/adr/README.md",
|
|
1429
1434
|
"mode": "0000644",
|
|
1430
|
-
"sha256": "
|
|
1435
|
+
"sha256": "06421240d64adf0e3058cafd2a5ddd8281c1b7c103f532aa22e76cab10a71170"
|
|
1431
1436
|
},
|
|
1432
1437
|
{
|
|
1433
1438
|
"path": "docs/api-reference.md",
|
|
@@ -4732,12 +4737,12 @@
|
|
|
4732
4737
|
{
|
|
4733
4738
|
"path": "package-lock.json",
|
|
4734
4739
|
"mode": "0000644",
|
|
4735
|
-
"sha256": "
|
|
4740
|
+
"sha256": "eef73b9771578d1077947c10da76b7f5c489104e905467f552e1174a0f33456c"
|
|
4736
4741
|
},
|
|
4737
4742
|
{
|
|
4738
4743
|
"path": "package.json",
|
|
4739
4744
|
"mode": "0000644",
|
|
4740
|
-
"sha256": "
|
|
4745
|
+
"sha256": "7acbbd119d4aba33fb6ec0a9bd7d60af7695efc2c4b71ababc6532be40dd1197"
|
|
4741
4746
|
},
|
|
4742
4747
|
{
|
|
4743
4748
|
"path": "public-docs/index.html",
|
|
@@ -5154,6 +5159,11 @@
|
|
|
5154
5159
|
"mode": "0000644",
|
|
5155
5160
|
"sha256": "0983e3a8b36c8e93d65ef1baa9fec9197e02425d411779752909e3656965f5c2"
|
|
5156
5161
|
},
|
|
5162
|
+
{
|
|
5163
|
+
"path": "scripts/gds/baseline-staleness.js",
|
|
5164
|
+
"mode": "0000644",
|
|
5165
|
+
"sha256": "8a5d2b6578f1fe83e97fd112620ea643571441d8c196d74659284014df067f48"
|
|
5166
|
+
},
|
|
5157
5167
|
{
|
|
5158
5168
|
"path": "scripts/gds/bfg-principal-token.js",
|
|
5159
5169
|
"mode": "0000644",
|
|
@@ -5267,7 +5277,7 @@
|
|
|
5267
5277
|
{
|
|
5268
5278
|
"path": "scripts/gds/control-manifest.js",
|
|
5269
5279
|
"mode": "0000644",
|
|
5270
|
-
"sha256": "
|
|
5280
|
+
"sha256": "270101b46772cc8e280bc5a36f66cfae578a901f07b4b5e1f959c9a566ebb5fd"
|
|
5271
5281
|
},
|
|
5272
5282
|
{
|
|
5273
5283
|
"path": "scripts/gds/copy-inventory.js",
|
|
@@ -5407,12 +5417,12 @@
|
|
|
5407
5417
|
{
|
|
5408
5418
|
"path": "scripts/gds/fitness-ratchets.js",
|
|
5409
5419
|
"mode": "0000644",
|
|
5410
|
-
"sha256": "
|
|
5420
|
+
"sha256": "fa16d4c62a16b7827fe82d556dd6a5de327362ba17fc54cc9d86e1d34e2a45f5"
|
|
5411
5421
|
},
|
|
5412
5422
|
{
|
|
5413
5423
|
"path": "scripts/gds/fitness.js",
|
|
5414
5424
|
"mode": "0000644",
|
|
5415
|
-
"sha256": "
|
|
5425
|
+
"sha256": "b07c9489b073233d5e3a83bb59a7ebda0fc1a2bd0defcd3d5a69efc77ff24b86"
|
|
5416
5426
|
},
|
|
5417
5427
|
{
|
|
5418
5428
|
"path": "scripts/gds/gate-review.js",
|
|
@@ -5782,7 +5792,7 @@
|
|
|
5782
5792
|
{
|
|
5783
5793
|
"path": "scripts/gds/redteam-patrol.js",
|
|
5784
5794
|
"mode": "0000644",
|
|
5785
|
-
"sha256": "
|
|
5795
|
+
"sha256": "b47919e09dae54b94a9f90c81bf535c00918dbd916f0d51c4bc29517dec9f7e9"
|
|
5786
5796
|
},
|
|
5787
5797
|
{
|
|
5788
5798
|
"path": "scripts/gds/regen-instance-docs.js",
|
|
@@ -6572,7 +6582,7 @@
|
|
|
6572
6582
|
{
|
|
6573
6583
|
"path": "src/module-api.js",
|
|
6574
6584
|
"mode": "0000644",
|
|
6575
|
-
"sha256": "
|
|
6585
|
+
"sha256": "ebd235444356b82184e91bd8a416c9e0a401d83c550d855aace738d57c142026"
|
|
6576
6586
|
},
|
|
6577
6587
|
{
|
|
6578
6588
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6829,6 +6839,11 @@
|
|
|
6829
6839
|
"mode": "0000644",
|
|
6830
6840
|
"sha256": "ba91d6431c1a579ec078f2aa64265d8afbf97ac635b5cb408d06b8bae93e4b71"
|
|
6831
6841
|
},
|
|
6842
|
+
{
|
|
6843
|
+
"path": "tests/baseline_staleness.mjs",
|
|
6844
|
+
"mode": "0000644",
|
|
6845
|
+
"sha256": "44a2469b0f18c62fab6d9b3fe5a5125b8a9a351b5b1b48cb88cd812994d86b10"
|
|
6846
|
+
},
|
|
6832
6847
|
{
|
|
6833
6848
|
"path": "tests/bfg.mjs",
|
|
6834
6849
|
"mode": "0000644",
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# 0189 — A frozen baseline must justify itself: declared floors and the freeze-forever detector
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-08-24
|
|
5
|
+
- **Tasks:** [#1003210](https://cloudbongos.com/builders#/task/1003210) (goal 1000079 — criterion C4 `reduction-durable`)
|
|
6
|
+
- **Builds on:** [ADR 0180](<redacted>.md) (shrink-only baselines), [ADR 0188](<redacted>.md) (the weekly patrol that now files this)
|
|
7
|
+
- **Related:** [ADR 0091](<redacted>.md) §2 (the boundary-forced duplication that gets the first declared floor), [ADR 0162](<redacted>.md) (findings are advisory)
|
|
8
|
+
|
|
9
|
+
## 1. Context
|
|
10
|
+
|
|
11
|
+
`config/fitness-baselines.json` is a shrink-only ratchet: a metric may never
|
|
12
|
+
exceed its baseline, and lowering one is how debt gets paid down. It makes
|
|
13
|
+
regression impossible.
|
|
14
|
+
|
|
15
|
+
It also makes standing still invisible. The follow-up audit measured the file at
|
|
16
|
+
*"every metric equal to baseline, 0 reduced"* — and the ratchet reported a clean
|
|
17
|
+
**PASS**, because that is what it is for. The same green covers the team actively
|
|
18
|
+
paying debt down and nobody having looked in a year:
|
|
19
|
+
|
|
20
|
+
> A shrink-only baseline that never shrinks is indistinguishable from unmanaged debt.
|
|
21
|
+
|
|
22
|
+
The ratchet has no concept of time, so it cannot tell those apart. Nothing else
|
|
23
|
+
did either.
|
|
24
|
+
|
|
25
|
+
## 2. Decision
|
|
26
|
+
|
|
27
|
+
### 2.1 Age comes from git, never from a stored timestamp
|
|
28
|
+
|
|
29
|
+
A `since:` field beside each metric would be faster and hermetic. It would also
|
|
30
|
+
be the field that silently stops being updated — leaving old debt looking
|
|
31
|
+
permanently fresh. That is *worse* than no check, because it produces confident
|
|
32
|
+
green. Git history cannot be forgotten. Where history is unavailable (a shallow
|
|
33
|
+
clone), the age is reported **UNKNOWN**, never fresh — the `main-audit.js
|
|
34
|
+
--require-roster` doctrine.
|
|
35
|
+
|
|
36
|
+
A metric's age is the date of the **oldest commit still carrying its current
|
|
37
|
+
value**; a metric present unchanged in every commit dates to its introduction,
|
|
38
|
+
which is exactly the "recorded once and never revisited" case being hunted.
|
|
39
|
+
|
|
40
|
+
### 2.2 A metric may declare a FLOOR, and a floor must say why
|
|
41
|
+
|
|
42
|
+
The naive check — flag anything unmoved for N weeks — fires forever on metrics
|
|
43
|
+
that are *finished*. `require_cycle_count: 4` is four deliberate lazy requires
|
|
44
|
+
with written rationale. `duplicate_window_count` cannot go below ~28 because that
|
|
45
|
+
much duplication is forced by the module boundary (ADR 0091 §2); merging the two
|
|
46
|
+
files would break ADR 0083. Nagging weekly about those is how a check becomes one
|
|
47
|
+
everybody skips.
|
|
48
|
+
|
|
49
|
+
So a metric may declare `floors: { name: { value, why } }`, and a metric **at its
|
|
50
|
+
floor is done, not stale**. The `why` is mandatory and length-checked: a floor
|
|
51
|
+
exempts a metric from the clock permanently, so it must be a written, reviewable
|
|
52
|
+
claim. That is the whole point — **silence was the problem**, and this replaces
|
|
53
|
+
silence with a sentence someone can disagree with.
|
|
54
|
+
|
|
55
|
+
A floor exempts only *down to* the floor. `duplicate_window_count` is 59 with a
|
|
56
|
+
floor of 28, so the ~31 reducible windows keep counting against the clock. A
|
|
57
|
+
floor is not a blanket pardon.
|
|
58
|
+
|
|
59
|
+
### 2.3 Two severities, split by whose fault it is
|
|
60
|
+
|
|
61
|
+
| what | where | severity |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| a floor names a metric that doesn't exist; a floor has no reason; a floor the metric already dropped below | merge gate (fitness Check 26) | **hard fail** |
|
|
64
|
+
| a metric above its floor unmoved for > 8 weeks | fitness **warning** + weekly patrol report | advisory |
|
|
65
|
+
|
|
66
|
+
Declaration rot is hermetic, needs no git, and is caused by the diff at hand —
|
|
67
|
+
that is what a merge gate is for. **Age is nobody's diff.** Failing a PR because
|
|
68
|
+
ambient debt turned eight weeks old punishes whoever pushed next and gets the gate
|
|
69
|
+
switched off. The audit asked for *"the repeatable, scheduled version"* of the
|
|
70
|
+
review it had just done by hand, and the patrol is that: history-wide, time-based,
|
|
71
|
+
filed for adjudication.
|
|
72
|
+
|
|
73
|
+
Eight weeks (`BASELINE_STALE_DAYS`) is long enough that a metric anyone is
|
|
74
|
+
actively working is never flagged.
|
|
75
|
+
|
|
76
|
+
### 2.4 `--update-baselines` had to stop eating the file
|
|
77
|
+
|
|
78
|
+
The refresh path wrote a fixed object literal — `{_comment: '<hardcoded>',
|
|
79
|
+
metrics}` — so a single run silently destroyed the accumulated `_comment` that
|
|
80
|
+
documents *why* each number sits where it does. Once floors live in the same file
|
|
81
|
+
the stakes change: losing a floor turns a finished metric into permanent weekly
|
|
82
|
+
noise **with its justification deleted from the repo**. The writer now recomputes
|
|
83
|
+
only `metrics` and passes every other key through untouched.
|
|
84
|
+
|
|
85
|
+
## 3. Consequences
|
|
86
|
+
|
|
87
|
+
- Nothing is flagged today: the baseline file is six days old, so no metric can be
|
|
88
|
+
stale yet. This is a tripwire for the future, verified by shrinking the window
|
|
89
|
+
(a 1-day window flags four real metrics) rather than by waiting.
|
|
90
|
+
- Paying a metric to its true limit now ends in a written floor rather than in the
|
|
91
|
+
metric quietly sitting at a number forever.
|
|
92
|
+
- `duplicate_window_count`, `doc_stale_links`, `doc_broken_anchors`,
|
|
93
|
+
`doc_duplicate_prose`, `console_call_count` and `knip_issue_count` all carry no
|
|
94
|
+
floor, which is the correct claim: every one of them is still reducible.
|
|
95
|
+
|
|
96
|
+
## 4. Alternatives rejected
|
|
97
|
+
|
|
98
|
+
- **A stored `since:` per metric.** §2.1 — the field that stops being updated.
|
|
99
|
+
- **Flag every unmoved baseline, no exemptions.** Weekly noise about finished
|
|
100
|
+
metrics; the check gets ignored, which is the failure it exists to prevent.
|
|
101
|
+
- **A floor without a required reason.** Then a floor is just a mute-button, and
|
|
102
|
+
muting is exactly the silence this replaces.
|
|
103
|
+
- **Hard-failing the merge gate on staleness.** Blocks unrelated work on ambient
|
|
104
|
+
debt age; the predictable outcome is the check being removed.
|
|
105
|
+
- **Putting it only in the patrol.** A builder editing baselines would never see
|
|
106
|
+
declaration rot until the following Monday, when it is cheap to catch instantly.
|
package/docs/adr/README.md
CHANGED
|
@@ -262,5 +262,6 @@ This keeps the decision history honest and traceable.
|
|
|
262
262
|
| 0186 | [**Per-project idea templates: the seam is already built; the economics are the open problem** ([task 1002932](https://cloudbongos.com/builders#/task/1002932), BV1.R156, goal 1000062 — a SPIKE, output is this ADR plus a task list, no implementation). The literal question — how does a founder define their own idea shape without a schema change or forking the scorer — is **already answered by shipped work**: R141 gave every template function an optional `registry` defaulting to the built-in one, so validation and scoring both read the declaration as data; `ideas_grades.template_key` is per-row with a format CHECK whose own comment says a project key "stays addressable"; and `provisioning_instances.settings jsonb` is an existing per-project home. **No schema change, no fork.** Decision A: a **`project-settings` kernel port**, sibling of `builder-settings`, read via `resolveOptional` so a vanilla instance with no provisioning module degrades to the built-in registry; `base-v1` stays the default and the fallback for an unresolvable key. Rejected a new `ideas_templates` table (second home for data that has one) and a config file (per-instance, but one instance hosts many projects). **Decision B is the finding that matters.** Completeness is normalised 0–100 WITHIN its template (mean of per-field ratios), while karma reads it against an absolute bar (`IDEA_FULL_PASS_BAR = 60`, `IDEA_FULL_PASS_CREDIT = 5`) — so template choice becomes a credit-minting lever: `base-v1` needs ~200 characters for 5 credits, a 1-field template with `substanceMin` 10 needs ~10, a ~20× arbitrage handed to exactly the projects this ADR empowers. Shipping A without B would make the feature a way to print credits. The fix separates the two questions one number is answering: **completeness stays relative** (it is the right signal for triage ordering and the hall meter) and **karma stops reading it**, switching to an absolute substance total the scorer already computes per field and currently discards after averaging. An interim guard (≥3 required fields, ≥120 total `substanceMin`) bounds the arbitrage without fixing it. Decision C: an optional per-field `weight` (default 1) turning the unweighted mean into a weighted one — today's behaviour is the all-weights-1 case, so it is a data addition and one arithmetic line. Decision D: the five base fields are **not** mandatory (forcing them would make "define your own" cosmetic), replaced by two constraints — a template must declare ≥1 required field, and a stored template is validated at WRITE time so a malformed one is refused there rather than discovered inside the capture path, which must never throw. **Boundary note:** goal 1000062's wall is `[ideas, hall-ui]`, so the port task (provisioning + kernel) and the karma task (economy) cannot be claimed under it.](<redacted>.md) | ideas / templates |
|
|
263
263
|
| 0187 | [**The OAuth web-handshake cookies live 30 minutes, and move together** ([task 1003234](https://cloudbongos.com/builders#/task/1003234), goal 1000058). The three cookies `/auth/web/start` sets — the CSRF state, the validated return target, the repo-scope opt-in — were 10 minutes each, a hall builder's timescale; a founder's first sign-in can include a GitHub SIGNUP with email verification and came back past ten minutes to "State mismatch" with the return + repo scope gone, and the page's retry door was a bare start. Decision: one `HANDSHAKE_COOKIE_MS = 30 min` for all three (they MUST move together — a state outliving its return cookie lands on the default landing with no scope, a return outliving its state lands on State mismatch), and every OAuth error page plus the access-pending auto-walk-in rebuilds its door from whatever cookies are still valid (`recoveryStartHref`, through the same open-redirect allowlist). What did NOT change: the state is still single-use, still `httpOnly`/`sameSite=lax`/apex-scoped, the code still the bearer — only the window for a legitimate handshake grew. Rejected: lengthening only return + repos (keeps the dead end and breaks the one-lifetime invariant the other way), a server-side state store, sixty minutes. The Discord-link state cookie is a different flow and keeps 10.](<redacted>.md) | auth / security posture |
|
|
264
264
|
| 0188 | [**The weekly patrol runs on CI, and a declared cadence must have something that fires it** ([task 1003209](https://cloudbongos.com/builders#/task/1003209), goal 1000079). Goal 1000076 shipped a weekly red-team patrol, a routine describing how to run it, a recipe, and a manifest row — and nothing ever ran it; the follow-up audit §7 named all three halves (no committed scheduler, no cadence for a scheduler to read, and a manifest green anyway because its wiring predicate was satisfied by the SKILL.md mentioning its own scanner). The symmetric fix — an in-process timer beside `peer-vote-tally` in `routine-timers.js` — is **wrong on correctness, not cost**: all three patrol checks read things only the REPO has (full git history, `.github/workflows/`, an auditable lockfile), and a deployed instance runs a VENDORED core out of `node_modules/@bongos/core` that has none of them, so the timer would have scanned nothing and reported clean — worse than unscheduled. The runner is therefore CI: a weekly Actions lane with `fetch-depth: 0` and a pinned, checksum-verified gitleaks (the missing binary the audit listed separately), invoking `run-routine.js redteam-weekly` so the SKILL.md's steps stay the single source of truth. `cadence:` becomes a real frontmatter field — declarative only, scheduler-agnostic. The load-bearing piece is `routine-schedule.js` (fitness Check 25), whose predicate is **relational, not existential**: it classifies the cron into the period it actually realizes and fails when that disagrees with the declared cadence — the one rot mode no substring or existence check can EVER see, since editing weekly→monthly leaves every "is it wired?" predicate green. The manifest row was re-pointed to match ("wired" now means the SCHEDULER names the routine). Finally the exit code says **a finding was LOST, not that a finding exists**: findings are advisory (ADR 0162), so filing three is a SUCCESS and red is reserved for findings that cannot be recorded — failing on findings would have gone red on run one and stayed there (the unrotated staging credential in `092e473` is a standing history hit), and an always-red weekly job is one nobody reads. Filing is credential-gated but OPTIONAL — with no session secret the patrol still runs and writes its findings to the job summary, which matters because this goal's C1 is about eliminating long-lived tokens, not adding one. Rejected: the in-process timer; teaching `routine-timers.js` generic cadence discovery (a scheduler built for nobody — its only candidate user cannot run there); failing whenever findings exist; requiring the secret; a deeper manifest predicate instead of a new check.](<redacted>.md) | security / enforcement durability |
|
|
265
|
+
| 0189 | [**A frozen baseline must justify itself: declared floors and the freeze-forever detector** ([task 1003210](https://cloudbongos.com/builders#/task/1003210), goal 1000079). The shrink-only ratchet makes a quality metric unable to REGRESS and, by the same design, unable to sit still noticeably: the follow-up audit measured the whole file at "every metric equal to baseline, 0 reduced" and the ratchet reported a clean PASS, because one green covers both actively-paying-down and nobody-has-looked-in-a-year. The ratchet has no concept of time; this adds it. **Age comes from git, never a stored `since:` field** — a timestamp beside each metric would be faster and hermetic and would also be the field that silently stops being updated, leaving old debt permanently fresh-looking, which is worse than no check because it produces confident green; an unavailable history reports UNKNOWN, not fresh (the `--require-roster` doctrine). A metric's age is the date of the OLDEST commit still carrying its current value, so a baseline recorded once and never revisited dates to its introduction — exactly the case being hunted. **The half that matters is the declared floor.** The naive "flag anything unmoved for N weeks" fires forever on metrics that are FINISHED (`require_cycle_count`'s four deliberate lazy requires; the ~28 duplicate windows ADR 0091 §2 forces because a module may not import core), and a check that cries wolf weekly is one everyone learns to skip — the same alert-fatigue failure ADR 0188 §2.4 avoids. So a metric may declare `floors: {name: {value, why}}` and a metric AT its floor is done, not stale; the `why` is mandatory and length-checked, because a floor exempts a metric from the clock permanently and must therefore be a written claim someone can disagree with — **silence was the problem**, and a floor without a reason is just a mute button. A floor exempts only DOWN TO the floor, so `duplicate_window_count`'s 59-vs-28 keeps its ~31 reducible windows on the clock. **Two severities, split by whose fault it is:** declaration rot (a floor naming a dead metric, a floor with no reason, a floor the metric already dropped below) is hermetic and caused by the diff at hand, so it HARD-FAILS the merge gate; staleness is nobody's diff and would punish whoever pushed next, so it warns in fitness and is FILED weekly by the patrol — the "repeatable, scheduled version" the audit asked for. Also fixed a latent eater: `--update-baselines` wrote a fixed object literal, so one refresh destroyed the accumulated `_comment` and (once floors landed) every floor, deleting a finished metric's justification from the repo while turning it into permanent weekly noise. Nothing is flagged today — the file is six days old — so the detector is verified by shrinking the window, not by waiting. Rejected: a stored `since:`; no exemptions; a floor with no required reason; hard-failing on staleness; patrol-only placement.](<redacted>.md) | quality ratchets / debt governance |
|
|
265
266
|
|
|
266
267
|
> ⚠️ **Numbering collision (known):** two ADRs share `0008`. The numbers were assigned in parallel sessions before the lock could be enforced. The files are kept under their existing filenames to preserve git history, but the table disambiguates them as `0008-a` / `0008-b`. New ADRs should resume at `0040` to avoid further collisions. (`0024` is also a real on-disk duplicate: the cloneable-repo/local-first-memory ADR and the multi-agent-system-architecture ADR were both numbered `0024` in parallel; both keep their filenames, disambiguated as `0024-a` / `0024-b` like `0008`. Likewise `0025` ended up with two on-disk files — the offsite-backup ADR and the structured-criterion↔task-link ADR — both kept under their existing filenames and disambiguated as `0025-a` / `0025-b`; separately, the BFG ADR had also claimed `0025` on 2026-05-30 and yielded to `0026`. A third happened 2026-06-06: `0036` was claimed in parallel by the builders-subdomain ADR ([#726](https://example.com/builders#/task/726)) and this Discord-channels ADR ([#727](https://example.com/builders#/task/727)); the Discord-channels ADR yielded to `0037`. A fourth landed 2026-06-10: `0044` was claimed in parallel by the palette-replacement ADR and the per-box game-preview ADR ([#897](https://example.com/builders#/task/897)); both keep their filenames, disambiguated as `0044-a` / `0044-b` like `0008`. A fifth was caught pre-land 2026-06-13: `0050` was claimed in parallel by the device-flow-admission ADR ([#1000](https://example.com/builders#/task/1000)) and the sandbox-for-everyone ADR ([#1001](https://example.com/builders#/task/1001)); the latter yielded to `0052` before merging, so there is no `0050` duplicate. A sixth was caught pre-land 2026-06-21: the tool-agnostic-design-layer ADR ([#1375](https://example.com/builders#/task/1375)) first collided at `0079` with the value-gated-autonomous-engagement ADR, then at `0080` with the llm-cache-api-transport ADR (both part of the 0075–0080 LLM-cost-reduction series, landed on `main` in parallel); it yielded to `0081` before merging, so there is no `0079`/`0080` design-layer duplicate. A seventh landed 2026-06-23: `0084` was claimed in parallel by the auto-derived-per-task-rank-floor ADR and the remove-default-spend-cap ADR (task 1499); the rank-floor ADR landed on `main` first, so the spend-cap ADR yielded to `0085`, leaving no `0084` duplicate. An eighth landed 2026-06-27: `0095` was claimed in parallel by the borrowed-memory-and-retrieval-concepts ADR (PR #459, landed first) and the cross-agent-context-management ADR ([#1651](https://example.com/builders#/task/1651), PR #460); both keep their filenames, disambiguated as `0095-a` / `0095-b` like `0008`. These two are companions — `0095-a` adopts the memory/retrieval concepts and explicitly defers the grep-vs-semantic question to `0095-b`.)
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.343",
|
|
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.343",
|
|
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.343",
|
|
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",
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/baseline-staleness.js — the freeze-forever detector (task 1003210,
|
|
3
|
+
// goal 1000079; docs/audits/ai-dev-redteam-followup.md §9 item 2).
|
|
4
|
+
//
|
|
5
|
+
// THE PROBLEM. `config/fitness-baselines.json` is a SHRINK-ONLY ratchet: a metric
|
|
6
|
+
// may never exceed its baseline, and lowering one is how debt gets paid down. That
|
|
7
|
+
// makes regression impossible — and makes standing still invisible. The follow-up
|
|
8
|
+
// audit measured the whole file at "every metric equal to baseline, 0 reduced" and
|
|
9
|
+
// named the consequence exactly:
|
|
10
|
+
//
|
|
11
|
+
// "A shrink-only baseline that never shrinks is indistinguishable from
|
|
12
|
+
// unmanaged debt."
|
|
13
|
+
//
|
|
14
|
+
// The ratchet is GREEN in that state. It is green in the state where the team is
|
|
15
|
+
// actively paying down, and equally green in the state where nobody has looked in
|
|
16
|
+
// a year. This check separates them by adding the one axis the ratchet has no
|
|
17
|
+
// concept of: TIME.
|
|
18
|
+
//
|
|
19
|
+
// WHY A DECLARED FLOOR IS THE HALF THAT MATTERS. The naive version — "flag every
|
|
20
|
+
// baseline that hasn't moved in N weeks" — fires on metrics that are *finished*.
|
|
21
|
+
// `require_cycle_count: 4` is four deliberate lazy requires with written rationale;
|
|
22
|
+
// `duplicate_window_count` cannot go below ~28 because that much duplication is
|
|
23
|
+
// forced by the module boundary (ADR 0091 §2). Those will never move again, and a
|
|
24
|
+
// check that nags about them weekly is a check everyone learns to skip — the same
|
|
25
|
+
// alert-fatigue failure ADR 0188 §2.4 avoids in the patrol.
|
|
26
|
+
//
|
|
27
|
+
// So a metric may declare a FLOOR with a written reason, and a metric at its floor
|
|
28
|
+
// is DONE, not stale. That converts "this is frozen forever" from silence into an
|
|
29
|
+
// explicit, reviewable claim — which is the actual goal. Silence was the problem.
|
|
30
|
+
//
|
|
31
|
+
// TWO CHECKS, TWO SEVERITIES, ON PURPOSE:
|
|
32
|
+
//
|
|
33
|
+
// • Floor-declaration rot is HERMETIC and HARD-FAILS. A floor naming a metric
|
|
34
|
+
// that no longer exists, a floor with no reason, or a floor ABOVE the metric's
|
|
35
|
+
// current value (the metric improved past the claim and nobody revisited it)
|
|
36
|
+
// are all defects in the declaration itself — cheap to detect, caused by the
|
|
37
|
+
// diff at hand, and exactly what a merge gate is for.
|
|
38
|
+
//
|
|
39
|
+
// • Staleness WARNS, and is filed weekly by the patrol. Age is not a property of
|
|
40
|
+
// anybody's diff. Hard-failing a PR because ambient debt turned eight weeks old
|
|
41
|
+
// punishes whoever pushed next and gets the gate disabled. The audit asked for
|
|
42
|
+
// "the repeatable, SCHEDULED version" of its own manual review, and the patrol
|
|
43
|
+
// is that: history-wide, time-based, filed for adjudication (ADR 0162).
|
|
44
|
+
//
|
|
45
|
+
// AGE COMES FROM GIT, NOT FROM A STORED TIMESTAMP. A `since:` field in the JSON
|
|
46
|
+
// would be faster and hermetic, and it would also be the thing that silently
|
|
47
|
+
// stops being updated — leaving old debt looking permanently fresh, which is a
|
|
48
|
+
// worse failure than no check at all. Git history cannot be forgotten. When it is
|
|
49
|
+
// unavailable (a shallow clone), this reports DID-NOT-RUN rather than a clean
|
|
50
|
+
// green: the main-audit.js --require-roster doctrine.
|
|
51
|
+
|
|
52
|
+
'use strict';
|
|
53
|
+
|
|
54
|
+
const fs = require('fs');
|
|
55
|
+
const path = require('path');
|
|
56
|
+
const { execFileSync } = require('child_process');
|
|
57
|
+
|
|
58
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
59
|
+
const BASELINE_REL = 'config/fitness-baselines.json';
|
|
60
|
+
|
|
61
|
+
// Eight weeks. Long enough that a metric someone is actively working is never
|
|
62
|
+
// flagged, short enough that "we'll get to it" cannot quietly become "never".
|
|
63
|
+
const DEFAULT_MAX_AGE_DAYS = Number(process.env.BASELINE_STALE_DAYS) || 56;
|
|
64
|
+
const DAY_MS = 86400000;
|
|
65
|
+
|
|
66
|
+
// Read the tracked file's value at every commit that touched it, newest first.
|
|
67
|
+
// Committer date, not author date: the question is how long this value has been
|
|
68
|
+
// LANDED, and a rebased author date answers a different one. Returns null when
|
|
69
|
+
// git or the history is unavailable — the caller must treat that as did-not-run.
|
|
70
|
+
function readHistory(rel = BASELINE_REL, { cwd = ROOT, exec = execFileSync } = {}) {
|
|
71
|
+
let lines;
|
|
72
|
+
try {
|
|
73
|
+
lines = exec('git', ['log', '--format=%H|%cI', '--', rel], { cwd, encoding: 'utf8', stdio: 'pipe' })
|
|
74
|
+
.trim().split('\n').filter(Boolean);
|
|
75
|
+
} catch (_) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (!lines.length) return null;
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
const [sha, date] = line.split('|');
|
|
82
|
+
let parsed;
|
|
83
|
+
try { parsed = JSON.parse(exec('git', ['show', `${sha}:${rel}`], { cwd, encoding: 'utf8', stdio: 'pipe' })); }
|
|
84
|
+
catch (_) { continue; } // a commit that deleted/renamed the file — skip, don't guess
|
|
85
|
+
out.push({ sha, date, metrics: (parsed && parsed.metrics) || {} });
|
|
86
|
+
}
|
|
87
|
+
return out.length ? out : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Pure: the ISO date at which each metric's CURRENT value was first set. Walks
|
|
91
|
+
// newest→oldest while the value is unchanged; the oldest commit still carrying it
|
|
92
|
+
// is when it last moved. A metric absent from an older commit was introduced at
|
|
93
|
+
// the one after it, and its introduction date IS its age — a baseline recorded
|
|
94
|
+
// once and never revisited is precisely what this looks for.
|
|
95
|
+
function lastChangedByMetric(history) {
|
|
96
|
+
const out = {};
|
|
97
|
+
if (!history || !history.length) return out;
|
|
98
|
+
for (const [name, value] of Object.entries(history[0].metrics)) {
|
|
99
|
+
let i = 0;
|
|
100
|
+
while (i + 1 < history.length && history[i + 1].metrics[name] === value) i += 1;
|
|
101
|
+
out[name] = history[i].date;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ageDays(iso, now) {
|
|
107
|
+
const t = Date.parse(iso);
|
|
108
|
+
return Number.isFinite(t) ? (now - t) / DAY_MS : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Pure verdict — exported for the hermetic test.
|
|
112
|
+
//
|
|
113
|
+
// `floors[name]` is `{ value, why }`. An undeclared metric floors at 0, so a
|
|
114
|
+
// metric already at 0 is never stale: it cannot shrink and there is nothing to
|
|
115
|
+
// declare. `lastChanged === null` (no history) is reported as unknown, never as
|
|
116
|
+
// fresh — an unmeasurable age must not read as a passing one.
|
|
117
|
+
function evaluate({ metrics = {}, floors = {}, lastChanged = {}, now = 0, maxAgeDays = DEFAULT_MAX_AGE_DAYS } = {}) {
|
|
118
|
+
const violations = [];
|
|
119
|
+
const stale = [];
|
|
120
|
+
const atFloor = []; // exempt by a WRITTEN floor claim
|
|
121
|
+
const atZero = []; // exempt because there is nothing left to reduce
|
|
122
|
+
|
|
123
|
+
for (const [name, spec] of Object.entries(floors)) {
|
|
124
|
+
if (!(name in metrics)) {
|
|
125
|
+
violations.push(`floor declared for "${name}", which is not a metric in ${BASELINE_REL} — remove the floor or restore the metric`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const value = spec && typeof spec === 'object' ? spec.value : spec;
|
|
129
|
+
const why = spec && typeof spec === 'object' ? spec.why : null;
|
|
130
|
+
if (!Number.isFinite(value)) {
|
|
131
|
+
violations.push(`floor for "${name}" has no numeric value — a floor is a claim about what cannot be reduced, so it must be a number`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!why || String(why).trim().length < 20) {
|
|
135
|
+
violations.push(`floor for "${name}" has no reason — a floor exempts a metric from staleness forever, so it must say WHY it cannot go lower`);
|
|
136
|
+
}
|
|
137
|
+
if (metrics[name] < value) {
|
|
138
|
+
violations.push(`floor for "${name}" is ${value} but the metric is already ${metrics[name]} — the floor was passed and never revisited; lower it to the real limit`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const [name, value] of Object.entries(metrics)) {
|
|
143
|
+
if (value === null || value === undefined) continue;
|
|
144
|
+
const spec = floors[name];
|
|
145
|
+
const floor = spec && typeof spec === 'object' ? spec.value : (Number.isFinite(spec) ? spec : 0);
|
|
146
|
+
if (value <= floor) { (floor > 0 ? atFloor : atZero).push(name); continue; }
|
|
147
|
+
const since = lastChanged[name];
|
|
148
|
+
if (!since) { stale.push({ name, value, floor, days: null, since: null }); continue; }
|
|
149
|
+
const days = ageDays(since, now);
|
|
150
|
+
if (days !== null && days > maxAgeDays) stale.push({ name, value, floor, days, since });
|
|
151
|
+
}
|
|
152
|
+
return { violations, stale, atFloor, atZero };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readBaselineFile(rel = BASELINE_REL) {
|
|
156
|
+
try { return JSON.parse(fs.readFileSync(path.join(ROOT, rel), 'utf8')) || {}; }
|
|
157
|
+
catch (_) { return {}; }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// One line per stale metric, in the voice the reader needs: what it is, how long
|
|
161
|
+
// it has not moved, and the two legitimate ways out (reduce it, or declare a floor
|
|
162
|
+
// saying why it cannot be reduced). "Do something" beats "something is wrong".
|
|
163
|
+
function describe(s) {
|
|
164
|
+
const age = s.days === null ? 'age UNKNOWN (no git history here)' : `unchanged for ${Math.floor(s.days)} days`;
|
|
165
|
+
return `${s.name}=${s.value} ${age}${s.floor ? ` (floor ${s.floor})` : ''} — reduce it, or declare a floor in ${BASELINE_REL} saying why it cannot go lower`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function run({ now = Date.now(), maxAgeDays = DEFAULT_MAX_AGE_DAYS } = {}) {
|
|
169
|
+
const file = readBaselineFile();
|
|
170
|
+
const history = readHistory();
|
|
171
|
+
const lastChanged = history ? lastChangedByMetric(history) : {};
|
|
172
|
+
const r = evaluate({ metrics: file.metrics || {}, floors: file.floors || {}, lastChanged, now, maxAgeDays });
|
|
173
|
+
return { ...r, historyAvailable: history !== null, maxAgeDays };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The fitness.js check. Hard-fails ONLY on floor-declaration rot (hermetic, caused
|
|
177
|
+
// by the diff at hand); staleness is a warning here and a filed report in the
|
|
178
|
+
// weekly patrol — see the header.
|
|
179
|
+
function checkBaselineFloors() {
|
|
180
|
+
const r = run();
|
|
181
|
+
const warnings = r.stale.map(describe);
|
|
182
|
+
if (!r.historyAvailable) {
|
|
183
|
+
warnings.push(`baseline age could not be measured — no git history for ${BASELINE_REL} here (shallow clone?). Reported as unknown rather than passing.`);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
name: 'a shrink-only baseline cannot silently freeze forever (task 1003210)',
|
|
187
|
+
ok: r.violations.length === 0,
|
|
188
|
+
hardFail: r.violations.length > 0,
|
|
189
|
+
violations: r.violations,
|
|
190
|
+
warnings,
|
|
191
|
+
note: `${Object.keys(readBaselineFile().metrics || {}).length} baseline(s): ${r.atZero.length} at zero, ${r.atFloor.length} at a declared floor (done), ${r.stale.length} unchanged for >${r.maxAgeDays}d. A floor must name a reason; age comes from git, never a stored timestamp.`,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The patrol class (ADR 0188): the SCHEDULED version of the audit's manual review.
|
|
196
|
+
// Severity is low — this is a prompt to look, not a defect. Returns [] when there
|
|
197
|
+
// is nothing to say, so the patrol files no report on a healthy week.
|
|
198
|
+
function stalenessFindings(opts = {}) {
|
|
199
|
+
const r = run(opts);
|
|
200
|
+
return r.stale.map((s) => ({ severity: 'low', item: describe(s) }));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function main() {
|
|
204
|
+
const r = run();
|
|
205
|
+
for (const v of r.violations) console.error(`✗ ${v}`);
|
|
206
|
+
for (const w of r.stale.map(describe)) console.warn(`⚠ ${w}`);
|
|
207
|
+
if (!r.historyAvailable) console.warn('⚠ no git history for the baseline file — age is UNKNOWN, not fresh');
|
|
208
|
+
console.log(`${r.violations.length ? 'FAIL' : 'PASS'} ${r.atZero.length} at zero · ${r.atFloor.length} at a declared floor · ${r.stale.length} stale (>${r.maxAgeDays}d)`);
|
|
209
|
+
process.exit(r.violations.length ? 1 : 0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (require.main === module) main();
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
checkBaselineFloors, stalenessFindings, evaluate, lastChangedByMetric,
|
|
216
|
+
readHistory, run, describe, DEFAULT_MAX_AGE_DAYS, BASELINE_REL,
|
|
217
|
+
};
|
|
@@ -82,6 +82,25 @@ const CONTROLS = [
|
|
|
82
82
|
alsoExists: ['scripts/gds/redteam-patrol.js', 'scripts/gds/routine-schedule.js', 'tests/routine_schedule.mjs'],
|
|
83
83
|
},
|
|
84
84
|
|
|
85
|
+
{
|
|
86
|
+
// Task 1003210. The ratchet (above) makes a metric unable to REGRESS; this
|
|
87
|
+
// makes it unable to sit still unnoticed. Both are needed — the audit measured
|
|
88
|
+
// the whole file at "every metric equal to baseline, 0 reduced", a state the
|
|
89
|
+
// ratchet reports as a clean pass. Wired in BOTH directions on purpose: the
|
|
90
|
+
// merge gate catches rot in a floor DECLARATION (hermetic, caused by the diff),
|
|
91
|
+
// and the weekly patrol files the time-based half, which is nobody's diff.
|
|
92
|
+
label: 'assurance: a frozen baseline cannot silently become permanent',
|
|
93
|
+
file: 'scripts/gds/baseline-staleness.js',
|
|
94
|
+
wiredIn: { file: 'scripts/gds/fitness.js', needle: 'baseline-staleness' },
|
|
95
|
+
alsoExists: ['config/fitness-baselines.json', 'tests/baseline_staleness.mjs'],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
label: 'assurance: the staleness check is filed weekly, not only on merge',
|
|
99
|
+
file: 'scripts/gds/baseline-staleness.js',
|
|
100
|
+
wiredIn: { file: 'scripts/gds/redteam-patrol.js', needle: 'stalenessFindings' },
|
|
101
|
+
alsoExists: ['.github/workflows/redteam-patrol.yml'],
|
|
102
|
+
},
|
|
103
|
+
|
|
85
104
|
// ── goal 1000077's four guards (task 1003169 / G05) ──────────────────────
|
|
86
105
|
//
|
|
87
106
|
// Same reasoning as the goal-1000076 controls above, applied to the goal that
|
|
@@ -270,7 +270,9 @@ function readBaselines() {
|
|
|
270
270
|
if (!metrics) {
|
|
271
271
|
return { metrics: {}, error: 'config/fitness-baselines.json has no "metrics" object — failing CLOSED.' };
|
|
272
272
|
}
|
|
273
|
-
|
|
273
|
+
// `parsed` rides along so --update-baselines can PRESERVE the non-metric keys it
|
|
274
|
+
// used to overwrite — see the writer in main().
|
|
275
|
+
return { metrics, error: null, parsed };
|
|
274
276
|
}
|
|
275
277
|
|
|
276
278
|
// Pure ratchet verdict — exported for the hermetic test.
|
|
@@ -330,11 +332,25 @@ function main() {
|
|
|
330
332
|
if (knip !== null) metrics.knip_issue_count = knip;
|
|
331
333
|
// The bootstrap path deliberately tolerates an absent file — writing it is the
|
|
332
334
|
// whole point — so it reads the metrics and ignores any error (task 1003227).
|
|
333
|
-
const { metrics: existing } = readBaselines();
|
|
335
|
+
const { metrics: existing, parsed: existingFile } = readBaselines();
|
|
334
336
|
if (knip === null && existing.knip_issue_count !== undefined) metrics.knip_issue_count = existing.knip_issue_count;
|
|
337
|
+
// PRESERVE everything that is not a metric. This used to write a fixed object
|
|
338
|
+
// literal, so one --update-baselines silently destroyed both the accumulated
|
|
339
|
+
// `_comment` (which documents WHY each number is where it is) and, once task
|
|
340
|
+
// 1003210 added them, the `floors` — the written claims that exempt a metric
|
|
341
|
+
// from the staleness check. Losing a floor is not cosmetic: the metric silently
|
|
342
|
+
// becomes "stale forever" and its reason is gone from the repo. Only `metrics`
|
|
343
|
+
// is recomputed; unknown keys pass through untouched, so a later addition to
|
|
344
|
+
// this file cannot be eaten by a refresh either.
|
|
345
|
+
const preserved = existingFile && typeof existingFile === 'object' ? { ...existingFile } : {};
|
|
346
|
+
delete preserved.metrics;
|
|
347
|
+
if (!preserved._comment) {
|
|
348
|
+
preserved._comment = 'fitness-ratchets baselines (task 1003129): quality debt recorded at gate-adoption. Shrink-only — lower a value when the metric improves; raising one to pass CI is out of policy.';
|
|
349
|
+
}
|
|
350
|
+
const { _comment, ...rest } = preserved;
|
|
335
351
|
fs.writeFileSync(
|
|
336
352
|
BASELINE_PATH,
|
|
337
|
-
JSON.stringify({ _comment
|
|
353
|
+
JSON.stringify({ _comment, metrics, ...rest }, null, 2) + '\n',
|
|
338
354
|
);
|
|
339
355
|
console.log(`fitness-ratchets: baselines written — ${Object.entries(metrics).map(([k, v]) => `${k}=${v}`).join(' · ')}`);
|
|
340
356
|
return;
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -1306,6 +1306,7 @@ const CHECKS = [
|
|
|
1306
1306
|
require('./version-literal-guard.js').checkNoVersionLiterals,
|
|
1307
1307
|
require('./control-manifest.js').checkControlsExist, // Check 21 — task 1003131: goal-1000076 controls assert their own existence (the ADR 0022 assurance-of-assurance)
|
|
1308
1308
|
require('./routine-schedule.js').checkRoutineCadencesScheduled, // Check 25 — task 1003209: a routine that declares a cadence must have a scheduler firing it AT that period (ADR 0188)
|
|
1309
|
+
require('./baseline-staleness.js').checkBaselineFloors, // Check 26 — task 1003210: a shrink-only baseline that never shrinks is unmanaged debt; a floor exempting one must say why
|
|
1309
1310
|
];
|
|
1310
1311
|
|
|
1311
1312
|
function runAll() {
|
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
// young direct deps (<90 days — the slopsquat shape), stale overrides.
|
|
19
19
|
// 3. action-pins — every workflow `uses:` must be pinned to a 40-hex SHA
|
|
20
20
|
// (the tj-actions lesson).
|
|
21
|
+
// 4. baseline-staleness — a shrink-only quality baseline that has not moved in
|
|
22
|
+
// 8 weeks (task 1003210). Belongs HERE and not in the merge gate for the
|
|
23
|
+
// same reason the other three do: it is history-wide and time-based, so it
|
|
24
|
+
// is not a property of anyone's diff — failing a PR because ambient debt
|
|
25
|
+
// turned eight weeks old punishes whoever pushed next. The audit asked for
|
|
26
|
+
// "the repeatable, scheduled version" of a review it had done by hand; this
|
|
27
|
+
// is it.
|
|
21
28
|
//
|
|
22
29
|
// Filing: DRY-RUN by default (prints what it would file). --file POSTs one
|
|
23
30
|
// report PER CLASS (max severity within the class, details in repro_steps_md)
|
|
@@ -340,6 +347,10 @@ async function main() {
|
|
|
340
347
|
const pins = actionPinCheck();
|
|
341
348
|
if (pins.findings.length) classes.push(buildClassReport('action-pins', pins.findings));
|
|
342
349
|
|
|
350
|
+
// Deterministic and local (git history + one JSON file) — no network, no model.
|
|
351
|
+
const stale = require('./baseline-staleness.js').stalenessFindings();
|
|
352
|
+
if (stale.length) classes.push(buildClassReport('baseline-staleness', stale));
|
|
353
|
+
|
|
343
354
|
if (secrets.degraded) stepSummary(`> **DEGRADED** — ${secrets.degraded}`);
|
|
344
355
|
|
|
345
356
|
if (!classes.length) {
|
package/src/module-api.js
CHANGED
|
@@ -1114,8 +1114,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1114
1114
|
// landed since 1.19.340 with no explicit bump. run 32798507686. (task 1002620)
|
|
1115
1115
|
// 1.19.342 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1116
1116
|
// landed since 1.19.341 with no explicit bump. run 32799103427. (task 1002620)
|
|
1117
|
+
// 1.19.343 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1118
|
+
// landed since 1.19.342 with no explicit bump. run 32799245758. (task 1002620)
|
|
1117
1119
|
// ---------------------------------------------------------------------------
|
|
1118
|
-
const CORE_VERSION = '1.19.
|
|
1120
|
+
const CORE_VERSION = '1.19.343'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1119
1121
|
|
|
1120
1122
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1121
1123
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// tests/baseline_staleness.mjs — the freeze-forever detector (task 1003210,
|
|
2
|
+
// goal 1000079; follow-up audit §9 item 2).
|
|
3
|
+
//
|
|
4
|
+
// The state this exists to detect is one the ratchet reports as a clean PASS:
|
|
5
|
+
// every metric equal to its baseline, nothing reduced, nobody looking. So the
|
|
6
|
+
// tests are written around the two ways such a check quietly becomes useless —
|
|
7
|
+
// flagging things that are legitimately finished (until everyone ignores it), and
|
|
8
|
+
// reading an unmeasurable age as a fresh one.
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { test } from 'node:test';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const bs = require(path.join(ROOT, 'scripts', 'gds', 'baseline-staleness.js'));
|
|
20
|
+
|
|
21
|
+
const NOW = Date.parse('2026-08-24T00:00:00Z');
|
|
22
|
+
const daysAgo = (n) => new Date(NOW - n * 86400000).toISOString();
|
|
23
|
+
|
|
24
|
+
// ------------------------------------------------------------------ the verdict
|
|
25
|
+
|
|
26
|
+
test('a metric that has not moved past the window is stale', () => {
|
|
27
|
+
const r = bs.evaluate({
|
|
28
|
+
metrics: { doc_stale_links: 1133 },
|
|
29
|
+
lastChanged: { doc_stale_links: daysAgo(120) },
|
|
30
|
+
now: NOW,
|
|
31
|
+
});
|
|
32
|
+
assert.equal(r.stale.length, 1);
|
|
33
|
+
assert.equal(r.stale[0].name, 'doc_stale_links');
|
|
34
|
+
assert.equal(Math.floor(r.stale[0].days), 120);
|
|
35
|
+
assert.deepEqual(r.violations, []);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('a metric that moved inside the window is not stale', () => {
|
|
39
|
+
const r = bs.evaluate({
|
|
40
|
+
metrics: { console_call_count: 161 },
|
|
41
|
+
lastChanged: { console_call_count: daysAgo(3) },
|
|
42
|
+
now: NOW,
|
|
43
|
+
});
|
|
44
|
+
assert.deepEqual(r.stale, []);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('a metric at zero is never stale — there is nothing left to reduce', () => {
|
|
48
|
+
const r = bs.evaluate({
|
|
49
|
+
metrics: { oversized_file_count: 0 },
|
|
50
|
+
lastChanged: { oversized_file_count: daysAgo(900) },
|
|
51
|
+
now: NOW,
|
|
52
|
+
});
|
|
53
|
+
assert.deepEqual(r.stale, []);
|
|
54
|
+
assert.deepEqual(r.atZero, ['oversized_file_count']);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('a metric at a DECLARED floor is done, not stale — the anti-nag rule', () => {
|
|
58
|
+
// require_cycle_count's four cycles are deliberate lazy requires. Without this
|
|
59
|
+
// exemption the check would flag them every week forever, and a check that cries
|
|
60
|
+
// wolf weekly is one everybody learns to skip (ADR 0188 §2.4, same reasoning).
|
|
61
|
+
const r = bs.evaluate({
|
|
62
|
+
metrics: { require_cycle_count: 4 },
|
|
63
|
+
floors: { require_cycle_count: { value: 4, why: 'all four are deliberate lazy requires with in-file rationale' } },
|
|
64
|
+
lastChanged: { require_cycle_count: daysAgo(900) },
|
|
65
|
+
now: NOW,
|
|
66
|
+
});
|
|
67
|
+
assert.deepEqual(r.stale, []);
|
|
68
|
+
assert.deepEqual(r.atFloor, ['require_cycle_count']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('a floor exempts only down TO the floor — reducible debt above it still goes stale', () => {
|
|
72
|
+
// 59 duplicate windows with a floor of 28: ~31 are reducible seeder scaffolding
|
|
73
|
+
// and must keep counting against the clock. A floor is not a blanket pardon.
|
|
74
|
+
const r = bs.evaluate({
|
|
75
|
+
metrics: { duplicate_window_count: 59 },
|
|
76
|
+
floors: { duplicate_window_count: { value: 28, why: 'ADR 0091 §2 boundary-forced duplication a module cannot merge away' } },
|
|
77
|
+
lastChanged: { duplicate_window_count: daysAgo(120) },
|
|
78
|
+
now: NOW,
|
|
79
|
+
});
|
|
80
|
+
assert.equal(r.stale.length, 1);
|
|
81
|
+
assert.equal(r.stale[0].floor, 28);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// --------------------------------------------------------- rot in a declaration
|
|
85
|
+
|
|
86
|
+
test('a floor with no reason is a hard violation — silence is what this replaces', () => {
|
|
87
|
+
const r = bs.evaluate({ metrics: { x: 5 }, floors: { x: { value: 3 } }, now: NOW });
|
|
88
|
+
assert.equal(r.violations.length, 1);
|
|
89
|
+
assert.match(r.violations[0], /no reason/);
|
|
90
|
+
// A one-word reason is not a reason either.
|
|
91
|
+
assert.match(bs.evaluate({ metrics: { x: 5 }, floors: { x: { value: 3, why: 'boundary' } }, now: NOW })
|
|
92
|
+
.violations[0], /no reason/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('a floor naming a metric that no longer exists is rot, caught at merge time', () => {
|
|
96
|
+
const r = bs.evaluate({ metrics: { a: 1 }, floors: { gone: { value: 1, why: 'a reason long enough to pass the length bar' } }, now: NOW });
|
|
97
|
+
assert.equal(r.violations.length, 1);
|
|
98
|
+
assert.match(r.violations[0], /not a metric/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('a floor the metric already dropped below was passed and never revisited', () => {
|
|
102
|
+
const r = bs.evaluate({
|
|
103
|
+
metrics: { dup: 20 },
|
|
104
|
+
floors: { dup: { value: 28, why: 'a reason long enough to pass the length bar' } },
|
|
105
|
+
now: NOW,
|
|
106
|
+
});
|
|
107
|
+
assert.equal(r.violations.length, 1);
|
|
108
|
+
assert.match(r.violations[0], /already 20/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('a non-numeric floor is refused — a floor is a numeric claim', () => {
|
|
112
|
+
const r = bs.evaluate({ metrics: { x: 5 }, floors: { x: { why: 'a reason long enough to pass the length bar' } }, now: NOW });
|
|
113
|
+
assert.match(r.violations[0], /no numeric value/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ------------------------------------------------------------------- unknown age
|
|
117
|
+
|
|
118
|
+
test('an unmeasurable age is reported as UNKNOWN, never as fresh', () => {
|
|
119
|
+
// The failure mode: a shallow clone yields no history, every metric looks
|
|
120
|
+
// brand-new, and the check quietly passes forever. Unknown must surface.
|
|
121
|
+
const r = bs.evaluate({ metrics: { doc_stale_links: 1133 }, lastChanged: {}, now: NOW });
|
|
122
|
+
assert.equal(r.stale.length, 1);
|
|
123
|
+
assert.equal(r.stale[0].days, null);
|
|
124
|
+
assert.match(bs.describe(r.stale[0]), /age UNKNOWN/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// --------------------------------------------------------- reading git history
|
|
128
|
+
|
|
129
|
+
test('lastChangedByMetric dates a value from the OLDEST commit still carrying it', () => {
|
|
130
|
+
// newest → oldest. `a` has been 5 across the top two commits, so it last moved
|
|
131
|
+
// at the second one, not the first.
|
|
132
|
+
const history = [
|
|
133
|
+
{ sha: 'c3', date: daysAgo(1), metrics: { a: 5, b: 1 } },
|
|
134
|
+
{ sha: 'c2', date: daysAgo(30), metrics: { a: 5, b: 2 } },
|
|
135
|
+
{ sha: 'c1', date: daysAgo(90), metrics: { a: 9, b: 3 } },
|
|
136
|
+
];
|
|
137
|
+
const t = bs.lastChangedByMetric(history);
|
|
138
|
+
assert.equal(t.a, daysAgo(30));
|
|
139
|
+
assert.equal(t.b, daysAgo(1));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('a metric present in every commit dates to its introduction — recorded once, never revisited', () => {
|
|
143
|
+
const history = [
|
|
144
|
+
{ sha: 'c2', date: daysAgo(1), metrics: { a: 7 } },
|
|
145
|
+
{ sha: 'c1', date: daysAgo(200), metrics: { a: 7 } },
|
|
146
|
+
];
|
|
147
|
+
assert.equal(bs.lastChangedByMetric(history).a, daysAgo(200));
|
|
148
|
+
assert.deepEqual(bs.lastChangedByMetric([]), {});
|
|
149
|
+
assert.deepEqual(bs.lastChangedByMetric(null), {});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('readHistory returns null when git cannot answer, rather than an empty history', () => {
|
|
153
|
+
// null and [] must not be conflated: [] would flow through as "no commits, so
|
|
154
|
+
// no ages", which reads as fresh. null is what makes the caller say DID NOT RUN.
|
|
155
|
+
const boom = () => { throw new Error('not a git repository'); };
|
|
156
|
+
assert.equal(bs.readHistory('config/fitness-baselines.json', { exec: boom }), null);
|
|
157
|
+
assert.equal(bs.readHistory('config/fitness-baselines.json', { exec: () => '' }), null);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// --------------------------------------------------------------- the live tree
|
|
161
|
+
|
|
162
|
+
test('LIVE: every floor in the shipped baseline file carries a reason', () => {
|
|
163
|
+
const r = bs.run();
|
|
164
|
+
assert.deepEqual(r.violations, [], r.violations.join(' | '));
|
|
165
|
+
assert.equal(r.historyAvailable, true, 'this checkout should have git history for the baseline file');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('LIVE: shrink the window to 1 day and the real baselines DO get flagged', () => {
|
|
169
|
+
// Proof the check is not passing because it cannot see. Every current baseline
|
|
170
|
+
// was written days ago, so at a one-day window the reducible ones must surface —
|
|
171
|
+
// if this returns nothing, the detector is inert.
|
|
172
|
+
const r = bs.run({ maxAgeDays: 1 });
|
|
173
|
+
assert.ok(r.stale.length > 0, 'a 1-day window must flag the real baselines');
|
|
174
|
+
const names = r.stale.map((s) => s.name);
|
|
175
|
+
assert.ok(names.includes('doc_stale_links'), `expected doc_stale_links among ${names.join(', ')}`);
|
|
176
|
+
// …and the exempt ones must STILL be exempt at a 1-day window: the exemption is
|
|
177
|
+
// the floor, not the clock.
|
|
178
|
+
assert.ok(!names.includes('require_cycle_count'), 'a metric at its declared floor must never be flagged');
|
|
179
|
+
assert.ok(!names.includes('oversized_file_count'), 'a metric at zero must never be flagged');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('LIVE: --update-baselines preserves the floors instead of overwriting them', () => {
|
|
183
|
+
// The regression this guards: the writer used to emit a fixed object literal
|
|
184
|
+
// ({_comment: '<hardcoded>', metrics}), so one refresh silently destroyed both
|
|
185
|
+
// the accumulated comment and every floor — and a lost floor turns a finished
|
|
186
|
+
// metric into permanent weekly noise with its reason gone from the repo.
|
|
187
|
+
const rat = require(path.join(ROOT, 'scripts', 'gds', 'fitness-ratchets.js'));
|
|
188
|
+
const before = JSON.parse(fs.readFileSync(rat.BASELINE_PATH, 'utf8'));
|
|
189
|
+
assert.ok(before.floors && Object.keys(before.floors).length > 0, 'the shipped file must carry floors');
|
|
190
|
+
// Simulate the writer's preserve step on a temp copy rather than running the
|
|
191
|
+
// real refresh (which recomputes metrics and would be slow + tree-dependent).
|
|
192
|
+
const tmp = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'bl-')), 'baselines.json');
|
|
193
|
+
const preserved = { ...before };
|
|
194
|
+
delete preserved.metrics;
|
|
195
|
+
const { _comment, ...rest } = preserved;
|
|
196
|
+
fs.writeFileSync(tmp, JSON.stringify({ _comment, metrics: before.metrics, ...rest }, null, 2) + '\n');
|
|
197
|
+
assert.deepEqual(JSON.parse(fs.readFileSync(tmp, 'utf8')), before);
|
|
198
|
+
});
|