instar 1.3.1023 → 1.3.1024
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/package.json +1 -1
- package/scripts/check-rule3-coverage.cjs +43 -0
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.1024.md +42 -0
- package/upgrades/rule3-gate-judges-author-contribution.eli16.md +34 -0
- package/upgrades/side-effects/rule3-gate-judges-author-contribution.md +53 -0
package/package.json
CHANGED
|
@@ -113,7 +113,50 @@ const STATE_DETECTION_PATTERNS = [
|
|
|
113
113
|
const RULE3_EXEMPT_COMMENT_RE = /RULE\s*3\s*:\s*EXEMPT/i;
|
|
114
114
|
const RULE3_RATIONALE_COMMENT_RE = /RULE\s*3\.1\s*RATIONALE/i;
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* The incoming ref, if a merge is in progress. `MERGE_HEAD` exists only between
|
|
118
|
+
* `git merge` starting and the merge commit being written — exactly the window
|
|
119
|
+
* this hook runs in.
|
|
120
|
+
*/
|
|
121
|
+
function mergeHeadIfMerging() {
|
|
122
|
+
try {
|
|
123
|
+
const out = execSync('git rev-parse -q --verify MERGE_HEAD', {
|
|
124
|
+
encoding: 'utf-8',
|
|
125
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
126
|
+
});
|
|
127
|
+
return out.trim() || null;
|
|
128
|
+
} catch {
|
|
129
|
+
return null; // not a merge — `rev-parse -q --verify` exits non-zero
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
116
133
|
function getStagedFiles() {
|
|
134
|
+
// During a MERGE the index holds every file that differs between the branch
|
|
135
|
+
// base and the incoming ref — i.e. effectively all of the incoming branch.
|
|
136
|
+
// Judging that as the committer's work makes anyone merging `main` into an
|
|
137
|
+
// older branch the author of all of `main`, so they are refused for
|
|
138
|
+
// pre-existing violations that are absent from their own diff and that they
|
|
139
|
+
// did not write. That refusal names a file they cannot see, so the natural
|
|
140
|
+
// responses are to edit someone else's code or reach for --no-verify.
|
|
141
|
+
//
|
|
142
|
+
// A committer's real contribution to a merge is what differs from the
|
|
143
|
+
// INCOMING ref: a file taken verbatim from MERGE_HEAD was not authored here.
|
|
144
|
+
// A conflict resolution DOES differ from MERGE_HEAD, so genuinely authored
|
|
145
|
+
// content is still evaluated — see the merge-semantics tests, which assert
|
|
146
|
+
// both directions.
|
|
147
|
+
const mergeHead = mergeHeadIfMerging();
|
|
148
|
+
if (mergeHead) {
|
|
149
|
+
try {
|
|
150
|
+
const out = execSync(
|
|
151
|
+
`git diff --cached --name-only --diff-filter=ACMR ${mergeHead}`,
|
|
152
|
+
{ encoding: 'utf-8' },
|
|
153
|
+
);
|
|
154
|
+
return out.split('\n').filter((l) => l.trim().length > 0);
|
|
155
|
+
} catch {
|
|
156
|
+
// Fall through to the full staged list — the STRICTER reading, so a
|
|
157
|
+
// failure here can only over-report, never silently let code past.
|
|
158
|
+
}
|
|
159
|
+
}
|
|
117
160
|
try {
|
|
118
161
|
const out = execSync('git diff --cached --name-only --diff-filter=ACMR', {
|
|
119
162
|
encoding: 'utf-8',
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-07-28T09:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-07-28T09:55:54.824Z",
|
|
5
|
+
"instarVersion": "1.3.1024",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The Rule 3 pre-commit gate judged **everything a merge stages** as the committer's work.
|
|
9
|
+
|
|
10
|
+
An ordinary commit's index is the author's change. A merge's index is every file that differs between
|
|
11
|
+
the branch base and the incoming ref — so merging `main` into an older branch made the committer the
|
|
12
|
+
author of all of `main`, and they were refused for pre-existing violations absent from their own diff.
|
|
13
|
+
Measured on a branch based at 2026-06-01: 905 staged `src/*.ts`, refused on two files the merger never
|
|
14
|
+
touched.
|
|
15
|
+
|
|
16
|
+
While a merge is in progress the gate now diffs against `MERGE_HEAD`. A file taken verbatim from the
|
|
17
|
+
incoming ref was not authored by the committer; a conflict resolution was, and is still evaluated.
|
|
18
|
+
Outside a merge nothing changes. If the comparison fails, the gate falls back to the full index — the
|
|
19
|
+
stricter reading, so a failure can only over-report.
|
|
20
|
+
|
|
21
|
+
## What to Tell Your User
|
|
22
|
+
|
|
23
|
+
Nothing. A contributor-facing pre-commit script — not shipped, not executed at runtime, no
|
|
24
|
+
user-visible behaviour.
|
|
25
|
+
|
|
26
|
+
## Summary of New Capabilities
|
|
27
|
+
|
|
28
|
+
None. This narrows *who* a rule is enforced against, not what the rule requires. It removes a class of
|
|
29
|
+
refusal that named a file the author could not see — the response to which was to edit someone else's
|
|
30
|
+
code or skip the check.
|
|
31
|
+
|
|
32
|
+
## Evidence
|
|
33
|
+
|
|
34
|
+
- Red → green with a control: the verbatim-from-incoming test failed while the author-resolved control
|
|
35
|
+
passed (1 failed / 25 passed); 26/26 after. The control is what distinguishes scoping the gate from
|
|
36
|
+
weakening it — content matching neither parent is still judged.
|
|
37
|
+
- The verbatim test asserts the offending file **is staged** before expecting a pass, so the pass
|
|
38
|
+
cannot come from an empty file list.
|
|
39
|
+
- All 24 pre-existing tests pass unchanged.
|
|
40
|
+
- Harness bug fixed en route: the new tests hardcoded `master` while `init.defaultBranch` is `main`
|
|
41
|
+
here; the branch name is now read from the repo rather than assumed.
|
|
42
|
+
- Side-effects review: `upgrades/side-effects/rule3-gate-judges-author-contribution.md`.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# ELI16 — you were being judged as the author of everything you merged
|
|
2
|
+
|
|
3
|
+
A pre-commit check looks at "the files you are committing" to decide whether new code that reads
|
|
4
|
+
outside systems ships with its required justification. It gets that list from the git index.
|
|
5
|
+
|
|
6
|
+
For an ordinary edit, the index *is* your change. **For a merge it is not.** Merging `main` into an
|
|
7
|
+
older branch stages every file that differs between your branch and `main` — hundreds or thousands of
|
|
8
|
+
them. The check then treats all of it as yours.
|
|
9
|
+
|
|
10
|
+
So you get refused for a rule broken by a file you have never opened, which does not appear in your
|
|
11
|
+
diff, in code somebody else wrote months ago. Measured on a branch based at 1 June: the merge stages
|
|
12
|
+
905 TypeScript files and the check refuses on two of them, neither touched by the merger.
|
|
13
|
+
|
|
14
|
+
That refusal is worse than annoying. It names a file you cannot see, so the two natural responses are
|
|
15
|
+
to go and edit someone else's code, or to reach for the flag that skips the check. Neither is what
|
|
16
|
+
anyone wanted.
|
|
17
|
+
|
|
18
|
+
**The fix.** While a merge is in progress git records the incoming side as `MERGE_HEAD`. Your actual
|
|
19
|
+
contribution to a merge is whatever differs from *that* — a file taken verbatim from the incoming
|
|
20
|
+
branch was not written by you. So during a merge the check now compares against the incoming ref
|
|
21
|
+
instead of the whole index.
|
|
22
|
+
|
|
23
|
+
**It does not go soft.** If you resolve a conflict, your resolution matches neither parent, so it is
|
|
24
|
+
still yours and still checked. There is a test for exactly that, and it fails if the fix is too
|
|
25
|
+
permissive. Outside a merge nothing changes at all — same list, same behaviour, and the 24 existing
|
|
26
|
+
tests pass untouched.
|
|
27
|
+
|
|
28
|
+
If the comparison itself fails for any reason, the check falls back to the old full list. That is the
|
|
29
|
+
stricter reading, so a failure can only ever over-report — it can never quietly let something past.
|
|
30
|
+
|
|
31
|
+
**Why this is the right layer.** A sweep finds 26 files on `main` that would trip this rule. Which of
|
|
32
|
+
them bites you depends on how old your branch is, because only files changed since your branch point
|
|
33
|
+
get staged. Adding justifications one file at a time is chasing a moving target; making the check
|
|
34
|
+
judge your own work fixes every version of the problem at once.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Side-effects review — Rule 3 gate judges the author's contribution
|
|
2
|
+
|
|
3
|
+
**Change:** `getStagedFiles()` in `scripts/check-rule3-coverage.cjs` diffs against `MERGE_HEAD` while
|
|
4
|
+
a merge is in progress; a new `mergeHeadIfMerging()` helper detects that state. Two merge-semantics
|
|
5
|
+
tests added.
|
|
6
|
+
|
|
7
|
+
## Direction of effect — the only question that matters for a gate
|
|
8
|
+
|
|
9
|
+
More permissive **during a merge only**, so the risk to weigh is a false ACCEPT.
|
|
10
|
+
|
|
11
|
+
| situation | before | after |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Ordinary commit (no merge) | full index | **identical** — `MERGE_HEAD` absent, original path taken |
|
|
14
|
+
| Merge: file verbatim from incoming ref | judged as yours ❌ | not judged ✅ **(the fix)** |
|
|
15
|
+
| Merge: file you resolved / authored | judged | **still judged** — differs from `MERGE_HEAD` |
|
|
16
|
+
| Merge: unrelated file you also staged | judged | **still judged** — differs from `MERGE_HEAD` |
|
|
17
|
+
| `git diff` against `MERGE_HEAD` fails | n/a | falls back to the full index — the **stricter** reading |
|
|
18
|
+
|
|
19
|
+
The load-bearing claim is row 3, and it is asserted by a test that fails if the fix is too permissive.
|
|
20
|
+
It is the reason this is scoping rather than weakening: content that exists on neither parent is
|
|
21
|
+
authored content, and it is still evaluated.
|
|
22
|
+
|
|
23
|
+
## What could go wrong, honestly
|
|
24
|
+
|
|
25
|
+
**A file the author deliberately reverts to the incoming version.** If you resolve a conflict by
|
|
26
|
+
taking the incoming side wholesale, that file no longer differs from `MERGE_HEAD` and is not judged.
|
|
27
|
+
That is correct by the definition used here — you contributed no content — but it does mean a merge
|
|
28
|
+
cannot be used to *re-introduce* an incoming violation under your name. Since the incoming ref already
|
|
29
|
+
contains it, the rule was already not being enforced there; this changes who is asked to fix it, not
|
|
30
|
+
whether it exists.
|
|
31
|
+
|
|
32
|
+
**Octopus merges** (>1 `MERGE_HEAD`) — `rev-parse --verify MERGE_HEAD` returns the first only. Rare in
|
|
33
|
+
this repo and it degrades toward the stricter side.
|
|
34
|
+
|
|
35
|
+
**Not addressed:** the 26 latent violators on `main` remain. This stops them landing on whoever merges
|
|
36
|
+
next; it does not add their missing justifications.
|
|
37
|
+
|
|
38
|
+
## Blast radius
|
|
39
|
+
|
|
40
|
+
Pre-commit script only. Not in `src/`, not bundled, not executed at runtime, no state, no migration,
|
|
41
|
+
no config. `git revert` restores the previous behaviour exactly.
|
|
42
|
+
|
|
43
|
+
## Verification
|
|
44
|
+
|
|
45
|
+
- **Red → green with a control**: before the fix the verbatim-from-incoming test failed while the
|
|
46
|
+
author-resolved control passed (1 failed / 25 passed) — the control is what proves the fix targets
|
|
47
|
+
the right thing rather than disabling the check. 26/26 after.
|
|
48
|
+
- The verbatim test asserts the file **is staged** before expecting a pass, so a pass cannot come from
|
|
49
|
+
an empty file list.
|
|
50
|
+
- All 24 pre-existing tests pass unchanged.
|
|
51
|
+
- A harness bug found and fixed en route: the tests hardcoded `master`, but `init.defaultBranch` is
|
|
52
|
+
`main` here. The branch name is now read from the repo, so the tests do not depend on the author's
|
|
53
|
+
git config.
|