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.
@@ -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.