instar 1.3.976 → 1.3.978
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/dist/core/StageTransitionValidator.d.ts +9 -0
- package/dist/core/StageTransitionValidator.d.ts.map +1 -1
- package/dist/core/StageTransitionValidator.js +55 -2
- package/dist/core/StageTransitionValidator.js.map +1 -1
- package/dist/core/StandardsEnforcementAuditor.d.ts +64 -3
- package/dist/core/StandardsEnforcementAuditor.d.ts.map +1 -1
- package/dist/core/StandardsEnforcementAuditor.js +58 -1
- package/dist/core/StandardsEnforcementAuditor.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +66 -5
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +46 -46
- package/upgrades/1.3.977.md +53 -0
- package/upgrades/1.3.978.md +88 -0
- package/upgrades/side-effects/assessment-confidence-unverified.md +125 -0
- package/upgrades/side-effects/ci-green-latest-run.md +119 -0
- package/upgrades/side-effects/learning-velocity-counts-finishing.md +102 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Side-Effects Review — assessmentConfidence: my own honesty field was overclaiming
|
|
2
|
+
|
|
3
|
+
## Summary of the change
|
|
4
|
+
|
|
5
|
+
`CoverageSummary.assessmentTrustworthy` (shipped hours earlier in PR #1641) reported **`true` over
|
|
6
|
+
the exact defect it was added to expose.** Verified on the LIVE endpoint after v1.3.975 deployed:
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
total: 22 | enforcedRatio: 0.0455 | assessmentTrustworthy: TRUE | canaryOk: true | canaryFailures: 0
|
|
10
|
+
registry.parsed: 22 | articleHeadings: 22 | bytes: 46606 | families: [5 of the 6]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The source registry carries **81** articles. The stale agent-home copy is not malformed — it is a
|
|
14
|
+
self-consistent document that is a quarter of the real one, so it passes every INTERNAL check by
|
|
15
|
+
construction (all headings parse, none dropped, anchors present, count above the floor).
|
|
16
|
+
|
|
17
|
+
`assessmentTrustworthy` was computed as `total > 0 && registry.canaryOk` — i.e. "my internal checks
|
|
18
|
+
passed". It READ as "this assessment is trustworthy". Conflating those is the failure the whole
|
|
19
|
+
instrument was being fixed for.
|
|
20
|
+
|
|
21
|
+
Replaced by a tri-state `assessmentConfidence` (`'verified' | 'unverified' | 'untrustworthy'`) plus
|
|
22
|
+
an always-populated `confidenceReason`. The boolean is retained, deprecated, TRUE only on
|
|
23
|
+
`'verified'`.
|
|
24
|
+
|
|
25
|
+
**Root insight:** nothing INSIDE a 22-rule document says it should have 81. Trustworthiness is not
|
|
26
|
+
obtainable by looking harder at the file — it needs an EXTERNAL expectation. That mechanism is the
|
|
27
|
+
blocked `standards-registry-snapshot-refresh` spec, so `'verified'` is unreachable today, and the
|
|
28
|
+
honest verdict for every passing read is `'unverified'`.
|
|
29
|
+
|
|
30
|
+
Proven against the real live registry (all four states):
|
|
31
|
+
|
|
32
|
+
| input | before | after |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| the live 22-of-81 copy | `trustworthy: true` | `unverified` + reason |
|
|
35
|
+
| the real 81-article registry | `true` | `unverified` — still correct; nothing to check against |
|
|
36
|
+
| a MATCHING expectation | unreachable | `verified` |
|
|
37
|
+
| a MISMATCHED expectation | unreachable | `untrustworthy` |
|
|
38
|
+
|
|
39
|
+
## Decision-point inventory
|
|
40
|
+
|
|
41
|
+
- `computeCoverage` / `GET /conformance/coverage{,/health}` — OBSERVE-ONLY. Gates nothing, blocks
|
|
42
|
+
nothing, no code path branches on the verdict. This changes what is REPORTED, never what anything
|
|
43
|
+
DOES.
|
|
44
|
+
- `deriveAssessmentConfidence` — new, exported, pure. Extracted deliberately: inline, TypeScript's
|
|
45
|
+
flow analysis proved `'verified'` unreachable and REJECTED the comparison deriving the deprecated
|
|
46
|
+
boolean. That rejection is TRUE and is the point; a function keeps the union open for when an
|
|
47
|
+
expectation ships without pretending it is reachable now. (TS caught this, which is worth noting —
|
|
48
|
+
the compiler objected to a claim I could not yet honour.)
|
|
49
|
+
- No gate, hook, sentinel, reaper, scheduler, migration or config surface touched.
|
|
50
|
+
|
|
51
|
+
## 1. Over-block
|
|
52
|
+
|
|
53
|
+
Nothing blocks. The adjacent risk is over-alarming: `assessmentTrustworthy` is now `false`
|
|
54
|
+
EVERYWHERE, including for a complete and correct registry. That is intentional and argued in the
|
|
55
|
+
ELI16: the instrument genuinely cannot verify completeness today, so anything stronger is the same
|
|
56
|
+
overclaim in nicer clothes.
|
|
57
|
+
|
|
58
|
+
The tri-state exists precisely so this is not alarming: `'unverified'` says *I do not know*, which is
|
|
59
|
+
neither reassuring nor a red flag. A consumer reading only the deprecated boolean sees `false` and
|
|
60
|
+
may over-read it — which is why the boolean is deprecated in its doc comment and the reason string is
|
|
61
|
+
always populated.
|
|
62
|
+
|
|
63
|
+
## 2. Under-block
|
|
64
|
+
|
|
65
|
+
No detection is weakened: `'untrustworthy'` still fires on every case the boolean fired on (nothing
|
|
66
|
+
parsed, canary objected), asserted in the updated tests. The change only ADDS a state between "fine"
|
|
67
|
+
and "broken" where none existed.
|
|
68
|
+
|
|
69
|
+
The residual gap, stated plainly: **a coherent stale registry is still reported with real-looking
|
|
70
|
+
figures** (`total: 22, enforcedRatio: 0.0455`). It is now labelled `unverified` with a reason naming
|
|
71
|
+
the possibility of "a coherent older copy", but the instrument cannot say *which* is the case. Only
|
|
72
|
+
the blocked spec closes that. This change makes the uncertainty visible; it does not resolve it, and
|
|
73
|
+
it must not be described as resolving it.
|
|
74
|
+
|
|
75
|
+
## 3. Blast radius
|
|
76
|
+
|
|
77
|
+
One field's semantics in `src/core/StandardsEnforcementAuditor.ts`; the route spreads the summary so
|
|
78
|
+
the new fields flow automatically. Consumers checked with a full-text scan (`grep` silently returned
|
|
79
|
+
nothing for a pattern present twice in this very file — python was used instead, and that tooling
|
|
80
|
+
anomaly is recorded):
|
|
81
|
+
|
|
82
|
+
- `assessmentTrustworthy` appears in exactly three test files (all updated) and nowhere in `src/`
|
|
83
|
+
outside its declaration and derivation. No runtime branch reads it.
|
|
84
|
+
- The response is additive apart from the boolean's meaning narrowing; no field renamed or removed.
|
|
85
|
+
|
|
86
|
+
## 4. Rollback plan
|
|
87
|
+
|
|
88
|
+
Single-commit revert; no state, config, migration or persisted artifact. The audit recomputes per
|
|
89
|
+
request.
|
|
90
|
+
|
|
91
|
+
## 5. Test coverage (all three tiers)
|
|
92
|
+
|
|
93
|
+
- **Unit** (`standards-enforcement-auditor.test.ts`, 16 pass): a coherent-but-stale registry (a real
|
|
94
|
+
whole-family subset of the live constitution) has zero dropped headings and `parsed ===
|
|
95
|
+
articleHeadings` — i.e. passes every internal check — and STILL must not read `verified`; plus
|
|
96
|
+
`deriveAssessmentConfidence`'s four verdicts in both directions, including that a mismatched
|
|
97
|
+
expectation is `untrustworthy` rather than merely `unverified`.
|
|
98
|
+
- **Integration** (`conformance-dev-gate-route.test.ts`): the HTTP body carries
|
|
99
|
+
`assessmentConfidence: 'untrustworthy'` for an empty registry.
|
|
100
|
+
- **E2E** (`standards-coverage-lifecycle.test.ts`): the verdict and its reason survive the production
|
|
101
|
+
initialization path.
|
|
102
|
+
|
|
103
|
+
## 6. A fourth test found encoding the defect
|
|
104
|
+
|
|
105
|
+
`standards-enforcement-auditor.test.ts` asserted `assessmentTrustworthy === true` for the full
|
|
106
|
+
registry — pinning the overclaim as a specification. That is the FOURTH test found this evening
|
|
107
|
+
asserting the behaviour it should have been challenging (the others: the standards parser's
|
|
108
|
+
five-family allowlist, a fixture whose meaning changed with the wall clock, and the
|
|
109
|
+
learning-velocity route's filed-items-count-as-learning fixtures).
|
|
110
|
+
|
|
111
|
+
Four in one day is not a coincidence about those four tests. It is a property of how tests get
|
|
112
|
+
written here: they pin what the code currently does. A green suite therefore attests to
|
|
113
|
+
self-consistency, not correctness — which is the same shape as everything else this project has
|
|
114
|
+
found, applied to our own verification. Recorded as a class; not fixed here, because the fix is a
|
|
115
|
+
standard about how assertions are chosen, not a code change.
|
|
116
|
+
|
|
117
|
+
## 7. How this was found, which is the transferable part
|
|
118
|
+
|
|
119
|
+
Not by a test. By deploying and reading the live surface. The suite was fully green — it tested the
|
|
120
|
+
cases I had thought of, which were exactly the cases my fix handled. The stale-but-coherent case was
|
|
121
|
+
outside my imagination, and no amount of internal testing would have surfaced it.
|
|
122
|
+
|
|
123
|
+
Two hours from shipping to discovery. The generalisable rule: **after shipping an honesty fix, read
|
|
124
|
+
the live surface for the case you were fixing.** If the number you set out to correct still looks
|
|
125
|
+
fine, you have not corrected it.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Side-Effects Review — CI greenness must use the LATEST run per check
|
|
2
|
+
|
|
3
|
+
## Summary of the change
|
|
4
|
+
|
|
5
|
+
`StageTransitionValidator.ciIsGreen` iterated EVERY entry in GitHub's `statusCheckRollup` and refused
|
|
6
|
+
on any non-success conclusion. GitHub returns every run of every check, **including superseded ones**,
|
|
7
|
+
so a check that failed and was then re-run to green appears twice and the failed entry never
|
|
8
|
+
disappears.
|
|
9
|
+
|
|
10
|
+
Verified on the live PR #1641 (merged legitimately at 02:07:44Z):
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
rollup entries: 31 | names appearing more than once: eli16 (2), UX impact declaration (2),
|
|
14
|
+
UX assertions (messaging E2E) (2), UX merge safety (2)
|
|
15
|
+
eli16 COMPLETED FAILURE completedAt 2026-07-26T01:44:01Z
|
|
16
|
+
eli16 COMPLETED SUCCESS completedAt 2026-07-26T01:45:32Z
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Branch protection merged the PR because it evaluates each check's CURRENT state. The tracker refused
|
|
20
|
+
it with `CI_NOT_GREEN` because it evaluated the stale run too — so **no correct merge whose CI had
|
|
21
|
+
ever been red could ever be recorded.**
|
|
22
|
+
|
|
23
|
+
Fix: `latestRunPerCheck` collapses the rollup to the newest run per check name (by `completedAt`,
|
|
24
|
+
falling back to `startedAt`) before `ciIsGreen` evaluates it. `GhPrView.statusCheckRollup`'s type
|
|
25
|
+
gains the `name`/`context`/`startedAt`/`completedAt` fields the dedupe needs; the route's existing
|
|
26
|
+
`gh pr view --json …statusCheckRollup` already returns them, so no query change.
|
|
27
|
+
|
|
28
|
+
## Decision-point inventory
|
|
29
|
+
|
|
30
|
+
- `ciIsGreen` feeds the `building → merged` GATE. This change makes the gate ACCEPT a case it
|
|
31
|
+
previously refused, so it is the direction that needs the most scrutiny — see §1/§2.
|
|
32
|
+
- No other decision point, hook, sentinel, reaper, scheduler, migration or config surface is touched.
|
|
33
|
+
- `latestRunPerCheck` is a pure function over the rollup array.
|
|
34
|
+
|
|
35
|
+
## 1. Over-block
|
|
36
|
+
|
|
37
|
+
Strictly reduced, and that is the intent: a merged PR with a superseded failure is now recordable.
|
|
38
|
+
Nothing that used to pass now fails.
|
|
39
|
+
|
|
40
|
+
The one preserved refusal worth naming: a **latest** run that is still in flight (`status` not
|
|
41
|
+
`COMPLETED`) is still not green, asserted in its own test, so the dedupe cannot be used to skip a
|
|
42
|
+
pending check by pairing it with an older success.
|
|
43
|
+
|
|
44
|
+
## 2. Under-block — the part that matters
|
|
45
|
+
|
|
46
|
+
A dedupe is a potential laundering mechanism, so three properties are enforced and each is tested:
|
|
47
|
+
|
|
48
|
+
1. **Latest is authoritative in BOTH directions.** SUCCESS followed by a later FAILURE is NOT green.
|
|
49
|
+
Without this, "keep the latest" would let anyone bury a red by arranging run order.
|
|
50
|
+
2. **Unorderable runs FAIL CLOSED.** Equal or missing timestamps ⇒ the FAILING run wins. An undatable
|
|
51
|
+
success must never mask a red. (Three variants tested: no timestamps either way round, and equal
|
|
52
|
+
timestamps.)
|
|
53
|
+
3. **Unnamed entries are keyed individually.** Bare status contexts with neither `name` nor `context`
|
|
54
|
+
are never collapsed into each other, so a failing one cannot be replaced by a passing one.
|
|
55
|
+
|
|
56
|
+
Residual, stated: the dedupe trusts GitHub's timestamps. A platform that reported them wrongly could
|
|
57
|
+
mis-order runs — but the fail-closed rule bounds the damage to the unorderable case, and the
|
|
58
|
+
alternative (ignoring the rollup entirely, or trusting merge status alone) is strictly weaker.
|
|
59
|
+
|
|
60
|
+
## 3. Blast radius
|
|
61
|
+
|
|
62
|
+
One function plus one type widening in `src/core/StageTransitionValidator.ts`. `ciIsGreen` is private
|
|
63
|
+
to that module (`grep` for callers: only the `to === 'merged'` branch). The route passes the rollup
|
|
64
|
+
through unchanged. No response shape changes; no new code.
|
|
65
|
+
|
|
66
|
+
## 4. Rollback plan
|
|
67
|
+
|
|
68
|
+
Single-commit revert; no state, config, migration or artifact. Reverting restores the previous
|
|
69
|
+
(over-refusing) behaviour immediately.
|
|
70
|
+
|
|
71
|
+
## 5. Test coverage
|
|
72
|
+
|
|
73
|
+
`tests/unit/StageTransitionValidator.test.ts` (46 pass), five new cases: the live #1641 shape
|
|
74
|
+
(superseded FAILURE + later SUCCESS ⇒ green); the reverse (SUCCESS + later FAILURE ⇒ not green);
|
|
75
|
+
unorderable-runs-fail-closed across three variants; unnamed entries not collapsed; and an in-flight
|
|
76
|
+
latest run still not green.
|
|
77
|
+
|
|
78
|
+
No integration/e2e tier added: this is a pure predicate inside an existing gate whose route path is
|
|
79
|
+
already covered, and the decisive evidence is the end-to-end check below.
|
|
80
|
+
|
|
81
|
+
## 6. End-to-end evidence, gathered BEFORE the fix and after
|
|
82
|
+
|
|
83
|
+
The validator's real logic was run against the two real merged PRs with the route's exact helper
|
|
84
|
+
wiring:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
before: item 1 (PR 1641) -> CI_NOT_GREEN
|
|
88
|
+
item 2 (PR 1644) -> MERGE_BASE_UNVERIFIABLE (stale local ref; resolved by git fetch)
|
|
89
|
+
after: item 1 (PR 1641) -> WOULD SUCCEED
|
|
90
|
+
item 2 (PR 1644) -> WOULD SUCCEED
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Note the second line: the `MERGE_BASE_UNVERIFIABLE` code shipped hours earlier in #1643 did its job
|
|
94
|
+
here on a real case — an unknown local SHA reported as *unverifiable* rather than as a false "not an
|
|
95
|
+
ancestor".
|
|
96
|
+
|
|
97
|
+
## 7. Third instance in one code path, in one evening
|
|
98
|
+
|
|
99
|
+
The completion-recording path has now yielded three independent defects tonight, each the same
|
|
100
|
+
underlying error:
|
|
101
|
+
|
|
102
|
+
| | defect | what it reported |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| #1640 | the `gh` binary was unreachable | a flat refusal |
|
|
105
|
+
| #1643 | a SourceTreeGuard refusal was caught and returned as `false` | "the merge is not on main" |
|
|
106
|
+
| this | a superseded check run read as the check's current state | "CI is not green" |
|
|
107
|
+
|
|
108
|
+
All three presented identically from outside — an opaque refusal — and each masked the next. That is
|
|
109
|
+
worth recording as a property of the path rather than three coincidences: **a code path whose failures
|
|
110
|
+
all render as one indistinguishable refusal will hide its own defects in series.** The general fix is
|
|
111
|
+
the one #1643 started: distinguish "could not determine" from "determined, and the answer is no".
|
|
112
|
+
|
|
113
|
+
## 8. How it was found
|
|
114
|
+
|
|
115
|
+
By refusing to assert. I was about to report that recording would work once the server updated, and
|
|
116
|
+
instead ran the validator's own logic against the real PRs. It returned two different refusals. That
|
|
117
|
+
cost two minutes and avoided discovering both blockers serially, each appearing to be a fresh
|
|
118
|
+
mystery. Habit worth keeping: **before claiming something will work once a dependency lands, execute
|
|
119
|
+
its logic now and read what it returns.**
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Side-Effects Review — learning velocity counts finishing, not filing (ACT-1244)
|
|
2
|
+
|
|
3
|
+
## Summary of the change
|
|
4
|
+
|
|
5
|
+
`GET /metrics/learning-velocity` counted EVERY evolution action at its `createdAt`, so the metric
|
|
6
|
+
answering "are we learning?" was almost purely a measure of FILING RATE. Verified 2026-07-25: 739 of
|
|
7
|
+
771 events (96%) were filed items; on the real queue 494 of those carry the sweep's resolution
|
|
8
|
+
"Abandoned without active tracking since creation date". The metric read 88/100 "accelerating" on
|
|
9
|
+
the morning the operator halted work because the opposite was visibly true — **the faster work was
|
|
10
|
+
abandoned, the higher the score climbed.**
|
|
11
|
+
|
|
12
|
+
An action now contributes a learning event ONLY when `status === 'completed'`, stamped at
|
|
13
|
+
`completedAt` (falling back to `updatedAt`). Everything else is excluded and ACCOUNTED FOR by
|
|
14
|
+
reason. The response gains `counting` (the rule, in one line) and `evolutionActions`
|
|
15
|
+
(`considered` / `counted` / `excluded` by reason, with `auto-abandoned` named specifically).
|
|
16
|
+
|
|
17
|
+
Measured before/after on this agent's live queue, 30-day window, rule change only:
|
|
18
|
+
**784 events → 18.** Considered 1,288; excluded 743 `not-completed:pending`, 494 `auto-abandoned`,
|
|
19
|
+
29 `not-completed:cancelled`, 2 `not-completed:in_progress`.
|
|
20
|
+
|
|
21
|
+
## Decision-point inventory
|
|
22
|
+
|
|
23
|
+
- `GET /metrics/learning-velocity` — a READ-ONLY observability route. It gates nothing, blocks
|
|
24
|
+
nothing, and no code path branches on its output. The change alters what it REPORTS, never what
|
|
25
|
+
anything DOES.
|
|
26
|
+
- `computeLearningVelocity` (`src/core/LearningVelocityScorer.ts`) — **untouched.** It remains a
|
|
27
|
+
pure function over an event list; only the route's event GATHERING changed. Its 6 unit tests pass
|
|
28
|
+
unmodified, which is the evidence that the scorer's contract is unchanged.
|
|
29
|
+
- No gate, hook, reaper, sentinel, scheduler, migration or config surface is touched.
|
|
30
|
+
|
|
31
|
+
## 1. Over-block
|
|
32
|
+
|
|
33
|
+
Not applicable — nothing here blocks. The adjacent risk is over-EXCLUSION: an action that genuinely
|
|
34
|
+
represents learning but is not marked `completed` no longer counts. That is the intended semantics
|
|
35
|
+
(the whole finding is that filing ≠ learning), and it is not silent: every exclusion is itemised by
|
|
36
|
+
reason in the response, so a reader can see exactly what was set aside and disagree with the rule if
|
|
37
|
+
they wish.
|
|
38
|
+
|
|
39
|
+
Two exclusions were chosen deliberately and could be argued:
|
|
40
|
+
|
|
41
|
+
- **`completed` with no `completedAt`/`updatedAt` → excluded, not back-dated to `createdAt`.**
|
|
42
|
+
Back-dating would re-import the entire filing bias through the one remaining gap. On the live
|
|
43
|
+
queue this bucket is EMPTY (all 20 completions carry `completedAt`), so the strict choice costs
|
|
44
|
+
nothing today and closes the door for later.
|
|
45
|
+
- **`cancelled` for a considered reason → still excluded.** A deliberate cancellation may well
|
|
46
|
+
embody a lesson, but it is not a completion, and admitting it would reopen the "closing a row
|
|
47
|
+
counts as learning" path that produced the inversion.
|
|
48
|
+
|
|
49
|
+
## 2. Under-block
|
|
50
|
+
|
|
51
|
+
The score can now READ LOWER than reality if completions are recorded without status/timestamp
|
|
52
|
+
hygiene. That is a reporting risk, not a safety one, and it is bounded by the accounting block: a
|
|
53
|
+
low score with `counted: 1, excluded: {not-completed:pending: 743}` is legible as "almost nothing
|
|
54
|
+
has finished", which is the true statement. The previous behaviour had the opposite failure — a high
|
|
55
|
+
score that was structurally unfalsifiable — and an honest low number is strictly better than a
|
|
56
|
+
flattering unfalsifiable one.
|
|
57
|
+
|
|
58
|
+
`byType.learning` (registered learnings) and `byType.correction` (the correction ledger) are
|
|
59
|
+
unchanged: both are genuine learning artifacts rather than filings, so neither was touched.
|
|
60
|
+
|
|
61
|
+
## 3. Blast radius
|
|
62
|
+
|
|
63
|
+
One route handler in `src/server/routes.ts`. Consumers checked:
|
|
64
|
+
|
|
65
|
+
- `grep -rn "learning-velocity"` across `src/`, `scripts/`, `tests/` — the route is read by the
|
|
66
|
+
CLAUDE.md template docs and the dashboard's plain-language surface; no code branches on
|
|
67
|
+
`adaptabilityScore` or `byType.evolution`.
|
|
68
|
+
- The response is ADDITIVE (`counting`, `evolutionActions`); no existing field was renamed or
|
|
69
|
+
removed, so a consumer reading `totalEvents`/`byType`/`adaptabilityScore` still parses.
|
|
70
|
+
- `tests/unit/LearningVelocityScorer.test.ts` — 6 tests, unmodified, pass. The scorer is untouched.
|
|
71
|
+
|
|
72
|
+
## 4. Rollback plan
|
|
73
|
+
|
|
74
|
+
Single-commit revert. No state, no config key, no migration, no persisted artifact; the metric is
|
|
75
|
+
recomputed from disk on every request. Reverting restores the old counting immediately.
|
|
76
|
+
|
|
77
|
+
## 5. Test coverage (all three tiers)
|
|
78
|
+
|
|
79
|
+
- **Unit** — `LearningVelocityScorer.test.ts` unchanged and passing (proves the pure scorer's
|
|
80
|
+
contract did not move).
|
|
81
|
+
- **Integration** — `tests/integration/learning-velocity-routes.test.ts`, 7 tests. Three existing
|
|
82
|
+
cases UPDATED (see §6) and three added: the real queue in miniature (5 filed-or-abandoned + 1
|
|
83
|
+
completion → exactly 1 event, `auto-abandoned` counted as 2 and named); a `completed` action with
|
|
84
|
+
no timestamp excluded rather than back-dated; and a completion outside the window excluded by
|
|
85
|
+
DATE while still appearing in `counted`, so "counts completions" cannot become "counts every
|
|
86
|
+
completion ever".
|
|
87
|
+
- **E2E** — `tests/e2e/learning-velocity-lifecycle.test.ts`: the `counting` rule and the
|
|
88
|
+
`evolutionActions` accounting must survive the PRODUCTION initialization path, because the live
|
|
89
|
+
endpoint is where a reader actually reads the number.
|
|
90
|
+
|
|
91
|
+
## 6. Three existing tests were encoding the defect
|
|
92
|
+
|
|
93
|
+
`learning-velocity-routes.test.ts` used action fixtures carrying only `createdAt` and asserted they
|
|
94
|
+
counted as learning events. Those assertions were a specification of the inversion: they would have
|
|
95
|
+
failed any correct implementation. Rewritten to use completions, with the filed-but-unfinished case
|
|
96
|
+
retained and asserted as an EXCLUSION rather than deleted.
|
|
97
|
+
|
|
98
|
+
This is the **third** time in one day a passing test was found protecting the defect beside it (the
|
|
99
|
+
others: the standards parser's five-family allowlist, and a fixture whose meaning changed with the
|
|
100
|
+
wall clock). Recorded rather than tidied away, because three instances in a day is a pattern about
|
|
101
|
+
how our tests get written — they pin current behaviour, and current behaviour is what the audit
|
|
102
|
+
found wanting. The class is worth a standard; it is not fixed here.
|