instar 1.3.1195 → 1.3.1197
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/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/package.json +1 -1
- package/scripts/standards-coverage.mjs +48 -20
- package/src/data/builtin-manifest.json +2 -2
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/upgrades/1.3.1196.md +32 -0
- package/upgrades/1.3.1197.md +32 -0
- package/upgrades/ci-rerun-on-review.eli16.md +31 -0
- package/upgrades/mirror-staleness-is-not-an-aliveness-fact.eli16.md +35 -0
- package/upgrades/side-effects/ci-rerun-on-review.md +84 -0
- package/upgrades/side-effects/mirror-staleness-is-not-an-aliveness-fact.md +78 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "8e6606a6722433c5ffc15b3870402283c5406083a14cdbd0e18e735731e2aae2",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1197",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "a4dfe2bf7d36670a4e46e566bb3eed53479031f12f38a5450f4c2badd1754192",
|
|
3
3
|
"registrySha256": "8e6606a6722433c5ffc15b3870402283c5406083a14cdbd0e18e735731e2aae2",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1197"
|
|
5
5
|
}
|
package/package.json
CHANGED
|
@@ -604,10 +604,22 @@ function validateRootSelfWiring() {
|
|
|
604
604
|
return exactKeys(config, ['branches']) &&
|
|
605
605
|
Array.isArray(config.branches) && config.branches.length === 1 && config.branches[0] === 'main';
|
|
606
606
|
};
|
|
607
|
-
|
|
607
|
+
// `pull_request_review` added 2026-08-22 and pinned, not merely permitted.
|
|
608
|
+
// The direction guard accepts the operator's approval as proof, but CI fired
|
|
609
|
+
// on push — before any approval could exist — so approving left the check
|
|
610
|
+
// red and finished, with nothing to re-run it. `dismissed` is pinned beside
|
|
611
|
+
// `submitted` because withdrawing an approval must re-run the guard and turn
|
|
612
|
+
// the check red again: a gate that only re-evaluates toward passing is not a
|
|
613
|
+
// gate. Pinning the types means neither can be dropped silently.
|
|
614
|
+
const reviewTrigger = workflow.on?.pull_request_review;
|
|
615
|
+
const reviewTriggerWired = exactKeys(reviewTrigger, ['types']) &&
|
|
616
|
+
Array.isArray(reviewTrigger.types) && reviewTrigger.types.length === 2 &&
|
|
617
|
+
reviewTrigger.types.includes('submitted') && reviewTrigger.types.includes('dismissed');
|
|
618
|
+
if (!exactKeys(workflow.on, ['push', 'pull_request', 'pull_request_review', 'workflow_dispatch']) ||
|
|
608
619
|
!(workflow.on.workflow_dispatch === null || exactKeys(workflow.on.workflow_dispatch, [])) ||
|
|
620
|
+
!reviewTriggerWired ||
|
|
609
621
|
!eventTargetsMain('push') || !eventTargetsMain('pull_request')) {
|
|
610
|
-
errors.push('The Root self-wiring requires top-level push and
|
|
622
|
+
errors.push('The Root self-wiring requires top-level push, pull_request and pull_request_review (submitted+dismissed) CI triggers targeting main');
|
|
611
623
|
}
|
|
612
624
|
const job = workflow.jobs?.['standards-coverage'];
|
|
613
625
|
const steps = Array.isArray(job?.steps) ? job.steps : [];
|
|
@@ -666,31 +678,47 @@ function validateRootSelfWiring() {
|
|
|
666
678
|
// protected-base step's is, so a behaviour edit here is a DECLARED edit.
|
|
667
679
|
const expectedReviewRun = [
|
|
668
680
|
'set -u',
|
|
669
|
-
'
|
|
670
|
-
"
|
|
671
|
-
|
|
681
|
+
'assoc_failed=0',
|
|
682
|
+
'if [ "$EVENT_NAME" != "pull_request" ] && [ "$EVENT_NAME" != "pull_request_review" ]; then',
|
|
683
|
+
' if associated=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${PUSH_SHA}/pulls" 2>/dev/null); then',
|
|
684
|
+
` match=$(SHA="$PUSH_SHA" jq -c '[.[] | select(.merge_commit_sha == env.SHA)] | if length == 1 then .[0] else empty end' <<< "\${associated:-[]}" 2>/dev/null || echo '')`,
|
|
685
|
+
' else',
|
|
686
|
+
" associated='[]'",
|
|
687
|
+
" match=''",
|
|
688
|
+
' assoc_failed=1',
|
|
689
|
+
' fi',
|
|
672
690
|
" PR=''",
|
|
673
691
|
" HEAD_SHA=''",
|
|
674
692
|
" PR_AUTHOR=''",
|
|
675
693
|
' if [ -n "$match" ]; then',
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
694
|
+
` PR=$(jq -r '.number // empty' <<< "$match" 2>/dev/null || echo '')`,
|
|
695
|
+
` HEAD_SHA=$(jq -r '.head.sha // empty' <<< "$match" 2>/dev/null || echo '')`,
|
|
696
|
+
` PR_AUTHOR=$(jq -r '.user.login // empty' <<< "$match" 2>/dev/null || echo '')`,
|
|
679
697
|
' fi',
|
|
680
|
-
|
|
698
|
+
` echo "path-b: event=\${EVENT_NAME} associated=$(jq -r 'length' <<< "\${associated:-[]}" 2>/dev/null || echo unreadable) matched-pr=\${PR:-none} assoc-failed=\${assoc_failed}"`,
|
|
681
699
|
'fi',
|
|
682
|
-
"
|
|
683
|
-
'if [
|
|
684
|
-
|
|
700
|
+
'echo "path-b: head-sha=${HEAD_SHA:-none} owner=${OWNER_LOGIN:-none}/${OWNER_TYPE:-none}"',
|
|
701
|
+
'if [ "$assoc_failed" = "1" ]; then',
|
|
702
|
+
` printf '%s' '{"fetchError":"the commit-to-pull-request association call failed"}' > "$OUT"`,
|
|
703
|
+
'elif [ -z "$PR" ]; then',
|
|
704
|
+
' jq -n --arg headSha "$HEAD_SHA" \\',
|
|
705
|
+
' --arg ownerLogin "$OWNER_LOGIN" \\',
|
|
706
|
+
' --arg ownerType "$OWNER_TYPE" \\',
|
|
707
|
+
' --arg prAuthorLogin "$PR_AUTHOR" \\',
|
|
708
|
+
" '{reviews:[], headSha:$headSha, ownerLogin:$ownerLogin, ownerType:$ownerType, prAuthorLogin:$prAuthorLogin}' \\",
|
|
709
|
+
` > "$OUT" || printf '%s' '{"fetchError":"jq could not build the review context"}' > "$OUT"`,
|
|
710
|
+
'elif reviews=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR}/reviews?per_page=100" 2>/dev/null); then',
|
|
711
|
+
` echo "path-b: reviews=$(jq -r 'length' <<< "$reviews" 2>/dev/null || echo unreadable)"`,
|
|
712
|
+
' jq -n --argjson reviews "$reviews" \\',
|
|
713
|
+
' --arg headSha "$HEAD_SHA" \\',
|
|
714
|
+
' --arg ownerLogin "$OWNER_LOGIN" \\',
|
|
715
|
+
' --arg ownerType "$OWNER_TYPE" \\',
|
|
716
|
+
' --arg prAuthorLogin "$PR_AUTHOR" \\',
|
|
717
|
+
" '{reviews:$reviews, headSha:$headSha, ownerLogin:$ownerLogin, ownerType:$ownerType, prAuthorLogin:$prAuthorLogin}' \\",
|
|
718
|
+
` > "$OUT" || printf '%s' '{"fetchError":"jq could not build the review context"}' > "$OUT"`,
|
|
719
|
+
'else',
|
|
720
|
+
` printf '%s' '{"fetchError":"the reviews API call failed"}' > "$OUT"`,
|
|
685
721
|
'fi',
|
|
686
|
-
"echo \"path-b: reviews=$(jq -r 'length' <<< \"${reviews:-[]}\" 2>/dev/null || echo unreadable) head-sha=${HEAD_SHA:-none} owner=${OWNER_LOGIN:-none}/${OWNER_TYPE:-none}\"",
|
|
687
|
-
'jq -n --argjson reviews "${reviews:-[]}" \\',
|
|
688
|
-
' --arg headSha "$HEAD_SHA" \\',
|
|
689
|
-
' --arg ownerLogin "$OWNER_LOGIN" \\',
|
|
690
|
-
' --arg ownerType "$OWNER_TYPE" \\',
|
|
691
|
-
' --arg prAuthorLogin "$PR_AUTHOR" \\',
|
|
692
|
-
" '{reviews:$reviews, headSha:$headSha, ownerLogin:$ownerLogin, ownerType:$ownerType, prAuthorLogin:$prAuthorLogin}' \\",
|
|
693
|
-
" > \"$OUT\" || echo '{}' > \"$OUT\"",
|
|
694
722
|
'',
|
|
695
723
|
].join('\n');
|
|
696
724
|
const expectedBaseSha = "${{ github.event.pull_request.base.sha || github.event.before || format('{0}^', github.sha) }}";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-08-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-08-24T02:50:42.124Z",
|
|
5
|
+
"instarVersion": "1.3.1197",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "8e6606a6722433c5ffc15b3870402283c5406083a14cdbd0e18e735731e2aae2",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1197",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "a4dfe2bf7d36670a4e46e566bb3eed53479031f12f38a5450f4c2badd1754192",
|
|
3
3
|
"registrySha256": "8e6606a6722433c5ffc15b3870402283c5406083a14cdbd0e18e735731e2aae2",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1197"
|
|
5
5
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
`tests/e2e/benchmark-divergence-alive.test.ts` no longer asserts `mirror.stale === false`. It asserts the wiring facts it owns — the mirror resolves on the production init path, carries a `capturedAt`, and reports a `stale` verdict CONSISTENT with the `staleDays` it computed — and leaves the threshold itself to a unit test with an injected clock.
|
|
9
|
+
|
|
10
|
+
`tests/unit/BenchmarkDivergenceAnalyzer.test.ts` gains that test: with a PRESENT mirror, `capturedAt + 30d` ⇒ `stale: false`, `capturedAt + 31d` ⇒ `stale: true`.
|
|
11
|
+
|
|
12
|
+
No runtime behaviour changes — tests only.
|
|
13
|
+
|
|
14
|
+
## What to Tell Your User
|
|
15
|
+
|
|
16
|
+
None — internal change (no user-facing surface).
|
|
17
|
+
|
|
18
|
+
## Summary of New Capabilities
|
|
19
|
+
|
|
20
|
+
None — internal change (no user-facing surface).
|
|
21
|
+
|
|
22
|
+
## Evidence
|
|
23
|
+
|
|
24
|
+
The shipped baseline `src/data/benchmarkPredictions.json` carries `capturedAt: 2026-07-24T01:20:00.000Z`; `mirrorStalenessMaxDays` defaults to 30. The assertion therefore passed for thirty days and turned RED at 2026-08-24T01:20Z on every branch simultaneously — main included — with no commit involved. Confirmed by running the e2e against `origin/main` at `9f83c19ca`: 1 failed, 4 passed, `expected { present: true, …(3) } to match object { present: true, stale: false }`. It blocked every merge, since `safe-merge` refuses on any red check.
|
|
25
|
+
|
|
26
|
+
Both sides of the boundary are pinned deliberately: a change that made nothing ever stale would satisfy a one-sided test while deleting the signal. Negative control — with `staleDays > mirrorStalenessMaxDays` removed from `mirrorStatus()`, the new test fails (1 of 26); restored, 26/26. E2E 5/5.
|
|
27
|
+
|
|
28
|
+
## Known Limits
|
|
29
|
+
|
|
30
|
+
The shipped baseline IS now stale by the default threshold, so a fresh install correctly reports `stale: true`. That is the feature working and it is deliberately NOT papered over here — refreshing `capturedAt` to make a test green would fabricate a capture that never happened. Whether to ship a newer capture on a cadence is a release decision, named rather than taken.
|
|
31
|
+
|
|
32
|
+
This fixes the one assertion found. No sweep was run for other tests whose verdict depends on wall-clock aging of a committed artifact, and none is claimed.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
`.github/workflows/ci.yml` gains a `pull_request_review` trigger (`submitted`, `dismissed`), and The Root's self-wiring contract in `scripts/standards-coverage.mjs` pins it.
|
|
9
|
+
|
|
10
|
+
The constitutional direction guard accepts the operator's approving review as proof that a registry change was authorised. CI fired on `push`, before any approval could exist, so the guard ran, correctly found none, and failed — and approving afterwards changed nothing, because the check had already finished. Approval had no event to re-run it.
|
|
11
|
+
|
|
12
|
+
The trigger alone is not the fix. A `pull_request_review` event is a THIRD event shape, and the evidence step plus the three `AUDIT_CHANGE_*` expressions each assumed exactly two. The step branched on `!= "pull_request"`, so a review event took the PUSH path, where the pull request is found by matching `github.sha` against `merge_commit_sha` — and on a review event `github.sha` is the merge ref, which matches nothing. Left alone, the trigger fires, the job runs, no pull request resolves, an empty context is written and the guard refuses: approving would have turned the check red *reliably* rather than leaving it stale. The event is handled explicitly in all four places and the branch test is pinned as a fourth regression route.
|
|
13
|
+
|
|
14
|
+
## What to Tell Your User
|
|
15
|
+
|
|
16
|
+
None — internal change (no user-facing surface).
|
|
17
|
+
|
|
18
|
+
## Summary of New Capabilities
|
|
19
|
+
|
|
20
|
+
None — internal change (no user-facing surface).
|
|
21
|
+
|
|
22
|
+
## Evidence
|
|
23
|
+
|
|
24
|
+
Observed live on PR #1960: approved at 22:03 with the standards check red and finished; a manual re-run of the same job then passed with no other change. That difference was the defect.
|
|
25
|
+
|
|
26
|
+
`scripts/standards-coverage.mjs --check` passes against the branch worktree, so the pinned literal matches the workflow rather than a description of it. `dismissed` is pinned alongside `submitted` so withdrawing an approval re-runs the guard and turns the check red again — a gate that re-evaluates only toward passing is not a gate. Ratchet suite 38/38, including the negative controls asserting that a workflow missing the required triggers FAILS; their fixtures and expected error text moved with the contract.
|
|
27
|
+
|
|
28
|
+
## Known Limits
|
|
29
|
+
|
|
30
|
+
A review EDITED in place does not re-trigger (`pull_request_review` fires on submit/dismiss). The guard binds to the head sha, so the exposure is an edited review body rather than a state change, and any new push re-runs everything via `synchronize`.
|
|
31
|
+
|
|
32
|
+
More broadly: this closes the gap for the review path only. Any other check whose inputs are not repository content has the same shape, and nothing enumerates those.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Making an approval actually do something — Plain-English Overview
|
|
2
|
+
|
|
3
|
+
## The problem in one breath
|
|
4
|
+
|
|
5
|
+
Tonight the operator approved a rulebook change and nothing happened. The check that was supposed to notice his approval had already run — minutes earlier, before he tapped — and finished red. Nothing told it to look again. It only didn't leave him stuck because someone was watching and pushed the button by hand.
|
|
6
|
+
|
|
7
|
+
## What already exists
|
|
8
|
+
|
|
9
|
+
The automated checks run when code changes. That has always been enough, because until today every input they cared about was code.
|
|
10
|
+
|
|
11
|
+
## What this adds
|
|
12
|
+
|
|
13
|
+
The checks now also run when someone approves a change, or withdraws an approval. That's it — one line saying "an approval is also a reason to look again."
|
|
14
|
+
|
|
15
|
+
## The new pieces
|
|
16
|
+
|
|
17
|
+
Nothing new is built. An existing set of checks gains one more reason to run.
|
|
18
|
+
|
|
19
|
+
## The safeguards
|
|
20
|
+
|
|
21
|
+
**Withdrawing counts too, and that's the important half.** If the operator approves and then changes his mind, the check runs again and goes back to red. A check that only re-runs when the news is good isn't a check — it would let a change of mind be quietly ignored while a stale green sat on the screen.
|
|
22
|
+
|
|
23
|
+
**The new trigger is pinned in place.** There's a guard that records the exact shape of the automated checks, so this can't be quietly removed later. If it were, the symptom would be an operator approving into silence again — the same bug, returning without a trace. Now its absence breaks the build instead.
|
|
24
|
+
|
|
25
|
+
## What ships when
|
|
26
|
+
|
|
27
|
+
Immediately, and it needs nothing from the operator. It's the missing half of the approval path that shipped an hour ago.
|
|
28
|
+
|
|
29
|
+
## What you actually need to decide
|
|
30
|
+
|
|
31
|
+
Nothing. This is a defect found on first real use, fixed in the direction of your time rather than mine.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# A test that turned red at a specific minute, on every branch, with nobody touching anything — Plain-English Overview
|
|
2
|
+
|
|
3
|
+
## The problem in one breath
|
|
4
|
+
|
|
5
|
+
One end-to-end test checked that a reference file shipped inside instar was "not stale". That file was captured on 24 July, and the rule is that it goes stale after thirty days. So at **01:20 UTC today** — thirty-one days later — the test started failing everywhere at once. No commit caused it. The clock did.
|
|
6
|
+
|
|
7
|
+
## Why that is worse than an ordinary failure
|
|
8
|
+
|
|
9
|
+
Nothing in the code changed, so nothing in the code looks guilty. Every branch went red simultaneously, including the main one, which makes it read like an infrastructure outage rather than a test making a claim it had no business making.
|
|
10
|
+
|
|
11
|
+
And it blocked everything: nothing could merge, because merges wait on a green build.
|
|
12
|
+
|
|
13
|
+
## What the test was actually entitled to check
|
|
14
|
+
|
|
15
|
+
That the feature is **wired up** — that the reference file is found when the server really boots, that it carries a capture date, and that the "is it stale?" answer it reports agrees with the age it worked out.
|
|
16
|
+
|
|
17
|
+
What it was **not** entitled to check is whether that file happens to be under thirty days old on the day the test runs. That is a fact about how often we ship a fresh capture, not about whether the code works.
|
|
18
|
+
|
|
19
|
+
## What changes
|
|
20
|
+
|
|
21
|
+
The end-to-end test now checks the wiring and the internal consistency of the answer. The staleness rule itself moved to a test where the clock is a dial we set: at exactly thirty days it must report *not stale*, and at thirty-one it must report *stale*.
|
|
22
|
+
|
|
23
|
+
Both sides are pinned deliberately. A "fix" that made nothing ever stale would have satisfied a one-sided test while quietly deleting the warning the feature exists to give.
|
|
24
|
+
|
|
25
|
+
## The safeguard
|
|
26
|
+
|
|
27
|
+
Checked against deliberately broken code: with the staleness rule removed, the new test fails. So it is a test, not a decoration.
|
|
28
|
+
|
|
29
|
+
## One thing worth knowing separately
|
|
30
|
+
|
|
31
|
+
The shipped reference capture really is more than thirty days old now. That means a fresh install will correctly report it as stale — which is the feature working, not breaking. Whether to ship a newer capture is a release-cadence decision, and it is deliberately not made here: silently refreshing the date would have hidden the same information this change exists to preserve.
|
|
32
|
+
|
|
33
|
+
## What you actually need to decide
|
|
34
|
+
|
|
35
|
+
Nothing about the test. Separately, and not urgently: whether the shipped reference capture should be refreshed on a cadence so installs do not inherit a stale one.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Side-Effects Review — CI re-runs when a review is submitted or dismissed
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `ci-rerun-on-review`
|
|
4
|
+
**Date:** `2026-08-22`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required — one workflow trigger and its pinned contract`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`.github/workflows/ci.yml` gains a `pull_request_review` trigger (`submitted`, `dismissed`), and `scripts/standards-coverage.mjs` pins that trigger in The Root's self-wiring contract.
|
|
11
|
+
|
|
12
|
+
**Found in production, on the first real use of the feature it completes.** The direction guard now accepts the operator's approving review as proof that a constitutional change was authorised. But CI fired on `push`, minutes before any approval could exist, so the guard ran, correctly found no approval, and failed. The operator then approved — and nothing happened. The check was already finished and red, with no event to re-run it. It only did not strand him because someone was watching and re-ran the job by hand.
|
|
13
|
+
|
|
14
|
+
An approval flow whose result depends on a human remembering to re-run the job is not a flow.
|
|
15
|
+
|
|
16
|
+
## Decision-point inventory
|
|
17
|
+
|
|
18
|
+
- CI trigger set — **modify** — adds the event that makes an approval re-evaluate the guard.
|
|
19
|
+
- The Root self-wiring contract in `standards-coverage.mjs` — **modify** — pins the new trigger and its two types.
|
|
20
|
+
|
|
21
|
+
No agent-runtime decision point is touched.
|
|
22
|
+
|
|
23
|
+
## 1. Over-block
|
|
24
|
+
|
|
25
|
+
`dismissed` is included deliberately and is the only arm that can newly turn a check RED. That is the point: withdrawing an approval must re-run the guard and fail it again. **A gate that re-evaluates only in the direction of passing is not a gate** — it would let an operator's change of mind be silently ignored while the stale green sat there. This is over-blocking in the correct direction and is the whole reason `dismissed` is pinned rather than merely allowed.
|
|
26
|
+
|
|
27
|
+
## 2. Under-block
|
|
28
|
+
|
|
29
|
+
`pull_request_review` fires for `submitted`/`dismissed`, so a review EDITED in place (rare) does not re-trigger. Named rather than papered over: the guard binds to the head sha, so the practical exposure is an edited review body — not a state change — which cannot flip approval either way. A `synchronize` on any new push already re-runs everything.
|
|
30
|
+
|
|
31
|
+
## 3. Level-of-abstraction fit
|
|
32
|
+
|
|
33
|
+
One trigger on the workflow that already runs the check. No new job, no polling, no bot. The alternative — a scheduled job re-running checks — would be strictly worse: slower, noisier, and it would not know what it was waiting for.
|
|
34
|
+
|
|
35
|
+
## 4. Signal vs authority compliance
|
|
36
|
+
|
|
37
|
+
Unchanged. This alters WHEN the check runs, never what it decides. The guard's authority and the ruleset's authority are untouched.
|
|
38
|
+
|
|
39
|
+
## 4b. Judgment-point check
|
|
40
|
+
|
|
41
|
+
None added. A workflow trigger is a deterministic event subscription.
|
|
42
|
+
|
|
43
|
+
## 5. Interactions
|
|
44
|
+
|
|
45
|
+
- **The direction guard's path B** is the feature this completes; without it, path B is only usable by someone manually re-running jobs.
|
|
46
|
+
- **The self-wiring contract** pins the trigger so it cannot be dropped silently. Dropping it would not fail any test otherwise, and the symptom would be an operator approving into a void — exactly the failure being fixed.
|
|
47
|
+
- **CI cost:** one extra run per review event on open PRs. Reviews are rare here (the operator is asked only for constitutional changes), so this is not a meaningful load.
|
|
48
|
+
- **The event SHAPE, found on rebase (2026-08-23) and the reason this branch is not a two-line change.** Adding a trigger gives the workflow a THIRD event shape, and three places assumed exactly two. The evidence step branched on `!= "pull_request"`, so a review event fell into the push branch, where the pull request is resolved by matching `github.sha` against `merge_commit_sha` — on a review event `github.sha` is the merge REF and matches nothing. The step would have resolved no pull request, written an empty context, and refused. The trigger added to make an approval count would have reliably rejected it, while every assertion about the trigger's presence stayed green: the event fires, the job runs, the check goes red. `AUDIT_CHANGE_KIND` / `_BASE_SHA` / `_HEAD_SHA` had the same two-shape assumption and would have handed the decision audit an EMPTY change kind — an unscoped run reporting clean. Both are fixed here and the first is pinned as a fourth regression route. This is the producer/consumer split in its exact shape: the trigger is the producer, and firing it is not the same fact as the checks handling it.
|
|
49
|
+
|
|
50
|
+
## 6. External surfaces
|
|
51
|
+
|
|
52
|
+
None. No route, no message, no notification.
|
|
53
|
+
|
|
54
|
+
## 6b. Operator-surface quality
|
|
55
|
+
|
|
56
|
+
This IS an operator-surface fix. Before: approve, and the red check stays red with no explanation. After: approve, and the check re-runs and goes green on its own.
|
|
57
|
+
|
|
58
|
+
## 7. Multi-machine posture
|
|
59
|
+
|
|
60
|
+
`unified` — trivially. A CI trigger; no state, no per-machine behaviour.
|
|
61
|
+
|
|
62
|
+
## 8. Rollback cost
|
|
63
|
+
|
|
64
|
+
The trigger, the event-shape handling in the evidence step and the three `AUDIT_CHANGE_*` expressions, plus their pinned counterparts in the contract. Reverting restores today's behaviour, in which an operator's approval requires a manual re-run to take effect — which is why the revert should not happen quietly. Reverting the event-shape handling ALONE is worse than reverting the whole change: the trigger would still fire and the check would then fail on every approval.
|
|
65
|
+
|
|
66
|
+
## Conclusion
|
|
67
|
+
|
|
68
|
+
Ship. It closes a gap found on the first real use of the approval path, in the direction of the operator's time rather than the agent's.
|
|
69
|
+
|
|
70
|
+
## Second-pass review
|
|
71
|
+
|
|
72
|
+
Ran, and it found something. Rebasing onto `main` after #1970 landed put this branch's trigger next to that change's push-path resolution for the first time, and the two together produced a third event shape neither had handled. See §5. The design question worth a second look — whether `dismissed` belongs — is answered under §1 and is the load-bearing half.
|
|
73
|
+
|
|
74
|
+
## Evidence pointers
|
|
75
|
+
|
|
76
|
+
- Observed live: PR #1960, approved at 22:03, standards check still red and finished; a manual re-run of the same job then passed with no other change. That difference is the bug.
|
|
77
|
+
- `tests/unit/standards-coverage-ratchet.test.ts` 38/38, including the negative controls that assert a workflow missing the required triggers FAILS, whose fixtures and expected error text moved with the contract.
|
|
78
|
+
- **Correction, on rebase.** An earlier version of this document cited moving the suite's population literals 88/89/89 → 89/89/89 as evidence. Those literals are GONE — #1970 removed them on `main`, because `protectedBase` is measured against the merge base and therefore depends on WHERE the test runs, so no literal can satisfy both the branch and the merged state. The rebase takes `main`'s removal. The claim is withdrawn rather than restated.
|
|
79
|
+
- The fourth regression route (`pull_request_review` folded back into the push branch) is asserted in the same suite and fails the check when applied, alongside the three routes #1970 pinned.
|
|
80
|
+
- `scripts/standards-coverage.mjs --check` run against this worktree: PASSED, so the pinned literal matches the workflow as it now stands rather than as it was described.
|
|
81
|
+
|
|
82
|
+
## Class-Closure Declaration (display-only mirror)
|
|
83
|
+
|
|
84
|
+
The class is "a check whose input can change without the check re-running." Closed for the review path specifically. NOT closed generally: any other check whose inputs are not repository content has the same shape, and nothing enumerates those. Named, not claimed.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Side-Effects Review — an aliveness test stops asserting a fact about the calendar
|
|
2
|
+
|
|
3
|
+
**Slug:** `mirror-staleness-is-not-an-aliveness-fact`
|
|
4
|
+
**Date:** 2026-08-24
|
|
5
|
+
**Risk floor:** 1 (tests only; no runtime file touched)
|
|
6
|
+
|
|
7
|
+
## Summary of the change
|
|
8
|
+
|
|
9
|
+
An e2e aliveness test asserted `mirror.stale === false` against a baseline shipped inside the package. The baseline's `capturedAt` is 2026-07-24T01:20Z and the staleness threshold defaults to 30 days, so the assertion turned red at 2026-08-24T01:20Z on every branch at once, main included, with no commit involved. The e2e now asserts the wiring (present, `capturedAt` set, `stale` consistent with `staleDays`); the threshold moves to a unit test with an injected clock, pinned on both sides.
|
|
10
|
+
|
|
11
|
+
## Decision-point inventory
|
|
12
|
+
|
|
13
|
+
None. No runtime decision changes; `mirrorStatus()` is untouched.
|
|
14
|
+
|
|
15
|
+
## 1. Over-block
|
|
16
|
+
|
|
17
|
+
None. The e2e can no longer fail for a reason unrelated to the code under test — which is the point, and is a REDUCTION in false red, not in coverage: the staleness rule is now checked more strictly than before (two points, controlled clock) rather than incidentally.
|
|
18
|
+
|
|
19
|
+
## 2. Under-block
|
|
20
|
+
|
|
21
|
+
Named honestly: the e2e no longer notices if the SHIPPED baseline ages out. That was never something it could report usefully — it could only say "red today, green yesterday, same code" — but the information is real and now has no automated home. It is recorded under Known Limits in the release fragment rather than silently dropped.
|
|
22
|
+
|
|
23
|
+
## 3. Level-of-abstraction fit
|
|
24
|
+
|
|
25
|
+
This is the whole finding. An aliveness test answers "is the feature wired on the production boot path?". Whether a shipped artifact is under thirty days old is a fact about release cadence and belongs where the clock is an input. Putting it in the e2e put a calendar dependency in a layer that cannot control the calendar.
|
|
26
|
+
|
|
27
|
+
## 4. Signal vs authority compliance
|
|
28
|
+
|
|
29
|
+
Not applicable — tests only, no runtime authority.
|
|
30
|
+
|
|
31
|
+
## 4b. Judgment-point check
|
|
32
|
+
|
|
33
|
+
None.
|
|
34
|
+
|
|
35
|
+
## 5. Interactions
|
|
36
|
+
|
|
37
|
+
- **`safe-merge`** refuses on any red check, so this single assertion blocked EVERY merge from 01:20Z. Two open PRs (#1967, and anything opened later) were stuck behind it. That is why the fix is worth doing now rather than filing.
|
|
38
|
+
- **`mirrorStatus()`** is unchanged, so the `precondition-failed / stale-mirror` verdict path and the unit tests around it keep their existing behaviour.
|
|
39
|
+
|
|
40
|
+
## 6. External surfaces
|
|
41
|
+
|
|
42
|
+
None.
|
|
43
|
+
|
|
44
|
+
## 6b. Operator-surface quality
|
|
45
|
+
|
|
46
|
+
Improves it indirectly: the operator stops seeing a repo-wide red that no commit explains.
|
|
47
|
+
|
|
48
|
+
## 7. Multi-machine posture
|
|
49
|
+
|
|
50
|
+
`unified` — tests only, no per-machine state.
|
|
51
|
+
|
|
52
|
+
## 8. Rollback cost
|
|
53
|
+
|
|
54
|
+
Revert two test files. Reverting restores a build that goes red on a timer, so it should not be reverted quietly.
|
|
55
|
+
|
|
56
|
+
## Conclusion
|
|
57
|
+
|
|
58
|
+
Ship.
|
|
59
|
+
|
|
60
|
+
## Phase-5 second pass
|
|
61
|
+
|
|
62
|
+
**Not required, and not run** — no block/allow decision, no session lifecycle, no gate/sentinel/guard/watchdog, no runtime file touched. Stated explicitly so the section below is not mistaken for an independent reviewer's concurrence.
|
|
63
|
+
|
|
64
|
+
## The finding that outlives the fix
|
|
65
|
+
|
|
66
|
+
This is the second time today the same shape has appeared, in two unrelated subsystems: a correctness claim resting on wall-clock timing. This morning it was a thread id whose uniqueness came only from the millisecond (fixed in #1971). Tonight it is a test whose verdict came only from the date. Both fail without a commit, both look like flake, and both get *more* likely with time or speed rather than less.
|
|
67
|
+
|
|
68
|
+
The tempting cheap fix here was to bump `capturedAt` in the shipped baseline. That would have made the suite green in one line and fabricated a capture that never happened — turning a true signal ("the shipped baseline is old") into a false one. Worth naming because it was the obvious move and it was wrong.
|
|
69
|
+
|
|
70
|
+
## Evidence pointers
|
|
71
|
+
|
|
72
|
+
- `origin/main` at `9f83c19ca`, e2e run locally: 1 failed / 4 passed, `expected { present: true, …(3) } to match object { present: true, stale: false }`. Main was red on the clock, not on a change.
|
|
73
|
+
- `src/data/benchmarkPredictions.json` → `capturedAt: 2026-07-24T01:20:00.000Z`; age 31 days; `mirrorStalenessMaxDays` default 30 (`BenchmarkDivergenceAnalyzer.ts:110`).
|
|
74
|
+
- Negative control: with the threshold comparison removed from `mirrorStatus()`, the new unit test fails (1/26). Restored: 26/26. E2E 5/5.
|
|
75
|
+
|
|
76
|
+
## Class-Closure Declaration (display-only mirror)
|
|
77
|
+
|
|
78
|
+
The class is "a test whose verdict depends on wall-clock aging of a committed artifact." Closed for this one assertion. NOT closed generally — no sweep was run for other date-sensitive assertions, and the sibling class named above (correctness resting on wall-clock timing, in runtime code as well as tests) is not swept for either. Named, not claimed.
|