opencode-skills-collection 4.0.56 → 4.0.57
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/bundled-skills/.antigravity-install-manifest.json +3 -1
- package/bundled-skills/antigravity-maintainer-batch-release/SKILL.md +20 -0
- package/bundled-skills/break-ai-fix-loops/LICENSE +21 -0
- package/bundled-skills/break-ai-fix-loops/SKILL.md +211 -0
- package/bundled-skills/break-ai-fix-loops/references/evidence-ledger.md +90 -0
- package/bundled-skills/break-ai-fix-loops/scripts/fingerprint.py +122 -0
- package/bundled-skills/break-ai-fix-loops/scripts/test_fingerprint.py +126 -0
- package/bundled-skills/linkedin-profile-optimizer/SKILL.md +125 -106
- package/bundled-skills/skill-porter/LICENSE +21 -0
- package/bundled-skills/skill-porter/SKILL.md +100 -0
- package/bundled-skills/skill-porter/scripts/port_skill.py +173 -0
- package/bundled-skills/skill-porter/scripts/test_port_skill.py +85 -0
- package/package.json +1 -1
- package/skills_index.json +67 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"updatedAt": "2026-09-
|
|
3
|
+
"updatedAt": "2026-09-09T01:53:48.864Z",
|
|
4
4
|
"entries": [
|
|
5
5
|
"00-andruia-consultant",
|
|
6
6
|
"007",
|
|
@@ -368,6 +368,7 @@
|
|
|
368
368
|
"brand-guidelines-community",
|
|
369
369
|
"brand-perception-psychologist",
|
|
370
370
|
"brave-man",
|
|
371
|
+
"break-ai-fix-loops",
|
|
371
372
|
"brendangregg-use-tsa",
|
|
372
373
|
"brevo-automation",
|
|
373
374
|
"broken-authentication",
|
|
@@ -1750,6 +1751,7 @@
|
|
|
1750
1751
|
"skill-installer",
|
|
1751
1752
|
"skill-issue",
|
|
1752
1753
|
"skill-optimizer",
|
|
1754
|
+
"skill-porter",
|
|
1753
1755
|
"skill-rails-upgrade",
|
|
1754
1756
|
"skill-router",
|
|
1755
1757
|
"skill-scanner",
|
|
@@ -78,6 +78,26 @@ Before changing anything:
|
|
|
78
78
|
- Verify its managed-only diff, required checks, merge result, and the resulting `origin/main`.
|
|
79
79
|
- If an unmanaged repair remains, use a topic PR; never patch `main` directly.
|
|
80
80
|
|
|
81
|
+
|
|
82
|
+
### Reviewed fork bundle exceptions
|
|
83
|
+
|
|
84
|
+
`tools/config/reviewed-fork-skills.json` is a protected-base ledger for the two
|
|
85
|
+
explicitly reviewed fork contributions #1337 and #1413. Each entry binds the
|
|
86
|
+
base repository, fork repository, PR number, original full reviewed head and
|
|
87
|
+
complete Git skill-tree object. It permits only Python files under that skill's
|
|
88
|
+
`scripts/` subtree and its root `LICENSE`, with a read-only Git copy origin when
|
|
89
|
+
needed. It does not allow workflows, arbitrary script types, generated-file
|
|
90
|
+
mutations, unsafe modes, links, invalid paths/objects or oversized content.
|
|
91
|
+
|
|
92
|
+
Both CI intake and `merge:batch` load the ledger from their trusted evaluator
|
|
93
|
+
checkout, never the PR's repository directory. Any change anywhere in the skill
|
|
94
|
+
subtree invalidates the exception. A base-only merge may reuse identical content,
|
|
95
|
+
but the maintainer must inspect the new complete PR diff and attest its exact
|
|
96
|
+
current head with `--reviewed-head`. Evidence, source-only checks, truthful skill
|
|
97
|
+
review, immutable PR/workflow binding and strict branch protection all remain
|
|
98
|
+
mandatory. Missing or malformed ledger data fails closed. Further exceptions or
|
|
99
|
+
policy expansions need explicit maintainer authorization and protected review.
|
|
100
|
+
|
|
81
101
|
## Workflow Contract Change Gate
|
|
82
102
|
|
|
83
103
|
When changing maintainer scripts, workflows, or policy, update the canonical skill, maintainer documentation, and regression tests in the same source PR. Add a negative test for every failure mode being fixed, run the relevant dry-run path, and reject any implementation/documentation mismatch. Source PRs must exclude generated registries and plugin mirrors; the protected canonical-sync PR owns that derived state, except for files intentionally staged by the scripted protected-release flow.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright GitHub, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: break-ai-fix-loops
|
|
3
|
+
description: "Stop ineffective AI coding repair loops with stable failure fingerprints, a three-attempt budget, real-path proof, negative controls, and tested rollback."
|
|
4
|
+
category: code-quality
|
|
5
|
+
risk: critical
|
|
6
|
+
source: community
|
|
7
|
+
source_repo: twoicewoo/awesome-copilot
|
|
8
|
+
source_type: community
|
|
9
|
+
date_added: "2026-09-04"
|
|
10
|
+
author: twoicewoo
|
|
11
|
+
tags: [ai-agents, debugging, verification, negative-control, rollback]
|
|
12
|
+
tools: [claude-code, codex-cli, copilot, cursor, gemini-cli]
|
|
13
|
+
license: "MIT"
|
|
14
|
+
license_source: "https://github.com/twoicewoo/awesome-copilot/blob/886bf799bb05501bfd1afa7aae9cc5a77dedb03e/LICENSE"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Break AI Fix Loops
|
|
18
|
+
|
|
19
|
+
Replace patch-and-retry behavior with a bounded, evidence-producing repair. Treat a changed patch as progress only when an observable state changes.
|
|
20
|
+
|
|
21
|
+
## When to Use This Skill
|
|
22
|
+
|
|
23
|
+
- Use when an AI coding agent cycles through similar patches without changing the observed failure.
|
|
24
|
+
- Use when a focused test passes but the installed, deployed, UI, API, persistence, or other real execution path still fails.
|
|
25
|
+
- Use when a repair claim needs a verifier that can reject a known-bad state and a rollback that has actually restored the baseline on a disposable copy.
|
|
26
|
+
- Do not use for a one-shot, already-understood edit whose acceptance check directly exercises the complete claimed behavior.
|
|
27
|
+
|
|
28
|
+
## Establish the repair contract
|
|
29
|
+
|
|
30
|
+
Before the first edit, record:
|
|
31
|
+
|
|
32
|
+
- the exact defect and the behavior that would disprove it;
|
|
33
|
+
- the revision, configuration, input, and execution path under test;
|
|
34
|
+
- the baseline command, literal result, and exit status;
|
|
35
|
+
- the strongest check that directly observes the claimed behavior;
|
|
36
|
+
- the rollback command and the state it must restore.
|
|
37
|
+
|
|
38
|
+
Save raw evidence before normalizing it. Redact credentials, tokens, cookies, personal data, and private URLs. Never put secrets into a fingerprint record or committed ledger.
|
|
39
|
+
|
|
40
|
+
If the defect cannot be reproduced, stop editing. Report `INCONCLUSIVE` with the missing observation instead of guessing at a fix.
|
|
41
|
+
|
|
42
|
+
## Use a three-attempt budget
|
|
43
|
+
|
|
44
|
+
Allow at most **three repair attempts for one acceptance claim**. An attempt begins when code, configuration, dependencies, generated artifacts, or test expectations change. Inspections and read-only probes do not consume an attempt.
|
|
45
|
+
|
|
46
|
+
Do not reset the budget because the agent restarts, opens a new session, rewrites the same patch, changes models, clears a cache, or renames the hypothesis. A newly exposed downstream failure still belongs to the same three-attempt budget unless it is a separately accepted task.
|
|
47
|
+
|
|
48
|
+
For every attempt, write these fields before the next edit:
|
|
49
|
+
|
|
50
|
+
| Field | Required evidence |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| Hypothesis | One causal mechanism, not a restatement of the symptom |
|
|
53
|
+
| Prediction | An observation that would distinguish this hypothesis from the previous one |
|
|
54
|
+
| Change | Exact changed paths and a patch or before/after hash |
|
|
55
|
+
| Focused check | Exact command, input, literal output, and exit status |
|
|
56
|
+
| Real-path check | Direct observation, or `NOT_RUN` with a reason |
|
|
57
|
+
| Symptom fingerprint | Stable fingerprint described below |
|
|
58
|
+
| Decision | `ADVANCE`, `SHIFT_CAUSE`, `PROVEN`, or `STOP` |
|
|
59
|
+
|
|
60
|
+
Use [the evidence ledger](references/evidence-ledger.md) as a copyable record.
|
|
61
|
+
|
|
62
|
+
## Fingerprint the observable failure
|
|
63
|
+
|
|
64
|
+
Fingerprint what the system did, not the agent's explanation. Build a canonical record from:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"schema_version": 1,
|
|
69
|
+
"command": "the exact verification command",
|
|
70
|
+
"input_digest": "digest or stable identifier of the tested input",
|
|
71
|
+
"exit_code": 1,
|
|
72
|
+
"failure_class": "stable-machine-readable-class",
|
|
73
|
+
"stable_excerpt": "the smallest decisive output with volatile values removed",
|
|
74
|
+
"real_path_state": "the directly observed state, or NOT_OBSERVED"
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Keep the unedited output beside this sanitized record. Remove timestamps, run IDs, ANSI codes, random ports, and temporary paths from `stable_excerpt` only when they do not affect the defect. Do not normalize away values that could distinguish two causes.
|
|
79
|
+
|
|
80
|
+
Optionally compute the canonical SHA-256 fingerprint from this skill directory:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
python3 scripts/fingerprint.py evidence/attempt-1.json
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The helper validates the record, rejects unknown fields, and prints the fingerprint. It does not execute commands or redact evidence.
|
|
87
|
+
|
|
88
|
+
The helper uses only the Python 3.9+ standard library. When changing it, run its bundled regression tests:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest scripts/test_fingerprint.py -v
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The same fingerprint after a different patch means the observable failure did not move. A cosmetically different message with the same failure class, input, command, and real-path state also counts as a repeated failure when the changed text is only volatile data. Do not use a patch hash in the symptom fingerprint; record it separately so different edits cannot masquerade as different outcomes.
|
|
95
|
+
|
|
96
|
+
## Shift the root-cause strategy
|
|
97
|
+
|
|
98
|
+
Set the decision to `SHIFT_CAUSE` immediately when any of these occurs:
|
|
99
|
+
|
|
100
|
+
- a symptom fingerprint repeats;
|
|
101
|
+
- the patch changes but the decisive state does not;
|
|
102
|
+
- a focused test passes while the real path still fails;
|
|
103
|
+
- a retry produces no new discriminating evidence.
|
|
104
|
+
|
|
105
|
+
Then stop editing and perform this sequence:
|
|
106
|
+
|
|
107
|
+
1. List the attempted mechanisms and the observation that falsified or failed to distinguish each one.
|
|
108
|
+
2. Identify the next unobserved owner boundary along the live path: input, dispatch, configuration, dependency, generated artifact, process, persistence, network, or presentation.
|
|
109
|
+
3. Collect one new observation at that boundary with tracing, logging, inspection, or a minimal probe.
|
|
110
|
+
4. Form a replacement hypothesis that predicts a different observation and targets a different causal mechanism.
|
|
111
|
+
5. Resume only if the new evidence can discriminate the replacement hypothesis. Otherwise return `BLOCKED`.
|
|
112
|
+
|
|
113
|
+
Do not spend an attempt on the same mechanism with broader edits. Do not weaken the assertion, skip the failing path, add a silent fallback, or update expected output merely to obtain green tests.
|
|
114
|
+
|
|
115
|
+
## Prove the real execution path
|
|
116
|
+
|
|
117
|
+
Match proof to the claim. Bind every result to the exact revision, configuration, and input.
|
|
118
|
+
|
|
119
|
+
| Claim | Required direct observation |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| CLI behavior | Invoke the installed or built entry point as a user would |
|
|
122
|
+
| API or integration | Send a real request and observe response plus the responsible service boundary |
|
|
123
|
+
| UI behavior | Perform the real interaction and observe UI state plus relevant network or console evidence |
|
|
124
|
+
| Persistence | Write, reload in a new read path or process, and observe the stored value |
|
|
125
|
+
| Deployment | Exercise the deployed revision and prove which revision served the result |
|
|
126
|
+
| Agent or tool action | Observe the actual tool call and its external state change, not the agent's narration |
|
|
127
|
+
|
|
128
|
+
A unit test, mock, type check, build, open port, process liveness check, or model-written summary is supporting evidence only when the claim crosses a boundary it does not exercise.
|
|
129
|
+
|
|
130
|
+
## Make the verifier prove it can fail
|
|
131
|
+
|
|
132
|
+
After the modified path passes, run a negative control on a disposable copy:
|
|
133
|
+
|
|
134
|
+
1. Copy the verified modified state to a separate worktree or directory.
|
|
135
|
+
2. Reintroduce the original defect or substitute a known-bad input that violates the same acceptance claim.
|
|
136
|
+
3. Run the **same primary verification command** with the same relevant configuration.
|
|
137
|
+
4. Require a non-zero exit status caused by the intended assertion.
|
|
138
|
+
5. Record the exact command, input, literal output, exit status, and failure classification.
|
|
139
|
+
|
|
140
|
+
An unrelated crash, missing dependency, timeout, syntax error, or test-discovery failure is not a valid negative control. If the known-bad state exits zero, the verifier is false-green: return `INCONCLUSIVE`, repair the verifier, and do not claim the product fix is proven.
|
|
141
|
+
|
|
142
|
+
Return to the untouched modified tree and rerun the primary verification after the negative control.
|
|
143
|
+
|
|
144
|
+
## Test rollback on another copy
|
|
145
|
+
|
|
146
|
+
Never test rollback only by undoing the working repair. Instead:
|
|
147
|
+
|
|
148
|
+
1. Copy the verified modified state to another disposable worktree or directory.
|
|
149
|
+
2. Run the documented rollback command there.
|
|
150
|
+
3. Verify changed paths and hashes match the recorded baseline.
|
|
151
|
+
4. Run the baseline command and confirm the prior behavior or status is restored.
|
|
152
|
+
5. Leave the primary modified tree unchanged.
|
|
153
|
+
|
|
154
|
+
A rollback script that parses, prints help, or exits zero without restoring behavior has not been tested.
|
|
155
|
+
|
|
156
|
+
## Finish with an evidence status
|
|
157
|
+
|
|
158
|
+
Use exactly one status:
|
|
159
|
+
|
|
160
|
+
- `PROVEN`: baseline defect observed; responsible change identified; focused and real-path checks pass; the known-bad negative control exits non-zero for the intended reason; rollback succeeds on another copy; the primary tree remains modified and passing.
|
|
161
|
+
- `INCONCLUSIVE`: some useful evidence exists, but a decisive gate is missing, false-green, or ambiguous.
|
|
162
|
+
- `BLOCKED`: the three-attempt budget is exhausted, a repeated fingerprint has no new discriminator, or a named external condition prevents the next observation.
|
|
163
|
+
|
|
164
|
+
Report exact commands, inputs, literal results, exit statuses, fingerprints, changed paths, revision, and remaining gaps. A passing proxy check or the phrase "tests pass" is never a substitute for those fields.
|
|
165
|
+
|
|
166
|
+
## Examples
|
|
167
|
+
|
|
168
|
+
### Repeated patch with no state change
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
Attempt 1: patch hash changed; focused test passed; real path still shows disabled.
|
|
172
|
+
Fingerprint: 08b4...; decision: SHIFT_CAUSE.
|
|
173
|
+
Next action: stop editing and observe the configuration-to-process boundary.
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Valid negative control
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
Modified copy: primary verifier exits 0 and observes the expected UI state.
|
|
180
|
+
Known-bad disposable copy: the same verifier exits 1 on the intended assertion.
|
|
181
|
+
Rollback copy: baseline hashes match and the baseline command restores its prior result.
|
|
182
|
+
Decision: PROVEN.
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Limitations
|
|
186
|
+
|
|
187
|
+
- This workflow cannot prove a repair when the defect is not reproducible, the real execution path is inaccessible, or the primary verifier cannot observe the acceptance claim.
|
|
188
|
+
- A three-attempt budget exposes stagnation; it does not identify the correct architecture or replace domain expertise.
|
|
189
|
+
- A known-bad control demonstrates that one verifier catches one defect class. It does not prove complete test coverage.
|
|
190
|
+
- Rollback verification covers the recorded paths and baseline behavior only; external systems need their own provider-side readback.
|
|
191
|
+
|
|
192
|
+
## Security & Safety Notes
|
|
193
|
+
|
|
194
|
+
- This skill can guide changes to code, configuration, dependencies, generated artifacts, and files, so its risk is `critical`.
|
|
195
|
+
- Confirm the repository, target environment, accepted paths, and approval boundary before modifying state. Ask before destructive, irreversible, production, financial, credential, or external-message actions.
|
|
196
|
+
- Keep negative controls and rollback trials on disposable copies. Never inject a known defect into the primary working tree or a live environment.
|
|
197
|
+
- Keep raw evidence private when it may contain credentials, personal data, internal URLs, or customer content. Commit only sanitized records.
|
|
198
|
+
- `scripts/fingerprint.py` is a Python standard-library helper that reads one local JSON record and prints a digest; it does not run commands, access the network, redact data, or modify the record.
|
|
199
|
+
|
|
200
|
+
## Related Skills
|
|
201
|
+
|
|
202
|
+
- `systematic-debugging` focuses on root-cause investigation before a fix; use this skill when attempts must also be fingerprinted, bounded, falsified with a negative control, and made reversible.
|
|
203
|
+
- `verification-before-completion` gates success claims on fresh evidence; this skill adds repair-attempt accounting and rollback proof.
|
|
204
|
+
- `closed-loop-delivery` spans acceptance through delivery; this skill is the narrower anti-stagnation and verifier-falsification protocol for repair loops.
|
|
205
|
+
- `audit-agent-run-evidence` performs a read-only post-run audit; this skill governs the repair while it is happening.
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
## Source and license
|
|
209
|
+
|
|
210
|
+
The upstream MIT copyright and permission notice is preserved in
|
|
211
|
+
[LICENSE](LICENSE), alongside the commit-pinned provenance above.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Repair evidence ledger
|
|
2
|
+
|
|
3
|
+
Copy this template to a task-owned path. Do not commit runtime evidence unless the project requires it. Preserve full raw output separately and keep this ledger free of secrets and personal data.
|
|
4
|
+
|
|
5
|
+
```markdown
|
|
6
|
+
# Repair ledger
|
|
7
|
+
|
|
8
|
+
## Contract
|
|
9
|
+
- Acceptance claim:
|
|
10
|
+
- Defect-disproving behavior:
|
|
11
|
+
- Baseline revision:
|
|
12
|
+
- Baseline configuration:
|
|
13
|
+
- Baseline input and digest:
|
|
14
|
+
- Baseline command:
|
|
15
|
+
- Baseline literal output/result:
|
|
16
|
+
- Baseline exit status:
|
|
17
|
+
- Real execution path:
|
|
18
|
+
- Primary verification command:
|
|
19
|
+
- Rollback command:
|
|
20
|
+
- Expected restored behavior/status:
|
|
21
|
+
|
|
22
|
+
## Attempts
|
|
23
|
+
| # | Hypothesis | Discriminating prediction | Changed paths / patch hash | Focused result + exit | Real-path result + exit | Symptom fingerprint | Decision |
|
|
24
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
25
|
+
| 1 | | | | | | | |
|
|
26
|
+
| 2 | | | | | | | |
|
|
27
|
+
| 3 | | | | | | | |
|
|
28
|
+
|
|
29
|
+
## Root-cause shifts
|
|
30
|
+
### Shift after attempt <n>
|
|
31
|
+
- Repeated fingerprint or unchanged state:
|
|
32
|
+
- Mechanisms already attempted:
|
|
33
|
+
- Evidence against each mechanism:
|
|
34
|
+
- Next unobserved owner boundary:
|
|
35
|
+
- New observation:
|
|
36
|
+
- Replacement hypothesis:
|
|
37
|
+
- Different predicted observation:
|
|
38
|
+
|
|
39
|
+
## Modified proof
|
|
40
|
+
- Revision:
|
|
41
|
+
- Exact command:
|
|
42
|
+
- Input/configuration:
|
|
43
|
+
- Literal output/result:
|
|
44
|
+
- Exit status:
|
|
45
|
+
- Real-path observation:
|
|
46
|
+
|
|
47
|
+
## Negative control on disposable copy
|
|
48
|
+
- Copy path or worktree:
|
|
49
|
+
- Known-bad mutation/input:
|
|
50
|
+
- Exact primary verification command:
|
|
51
|
+
- Literal output/result:
|
|
52
|
+
- Exit status (must be non-zero):
|
|
53
|
+
- Intended failure classification:
|
|
54
|
+
- Untouched modified tree rerun result:
|
|
55
|
+
- Untouched modified tree rerun exit status:
|
|
56
|
+
|
|
57
|
+
## Rollback on another copy
|
|
58
|
+
- Copy path or worktree:
|
|
59
|
+
- Exact rollback command:
|
|
60
|
+
- Literal rollback output/result:
|
|
61
|
+
- Rollback exit status:
|
|
62
|
+
- Baseline hash comparison:
|
|
63
|
+
- Restored behavior/status:
|
|
64
|
+
- Baseline command rerun exit status:
|
|
65
|
+
- Primary modified tree status:
|
|
66
|
+
|
|
67
|
+
## Decision
|
|
68
|
+
- Status: PROVEN | INCONCLUSIVE | BLOCKED
|
|
69
|
+
- Attempts consumed: <0-3>
|
|
70
|
+
- Decisive evidence:
|
|
71
|
+
- Remaining gap or next discriminating observation:
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Fingerprint record
|
|
75
|
+
|
|
76
|
+
Create one sanitized JSON record for every observed symptom:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"schema_version": 1,
|
|
81
|
+
"command": "npm test -- --runInBand path/to/regression.test.js",
|
|
82
|
+
"input_digest": "sha256:replace-with-real-input-digest",
|
|
83
|
+
"exit_code": 1,
|
|
84
|
+
"failure_class": "assertion-mismatch",
|
|
85
|
+
"stable_excerpt": "expected enabled; observed disabled",
|
|
86
|
+
"real_path_state": "settings page still shows disabled after reload"
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Keep the primary verification command unchanged across attempts unless the contract was wrong. If it changes, record why and preserve results from both commands; otherwise a changed verifier can hide an unchanged defect.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Create a stable symptom fingerprint from a validated JSON record."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Mapping
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
REQUIRED_FIELDS = (
|
|
16
|
+
"schema_version",
|
|
17
|
+
"command",
|
|
18
|
+
"input_digest",
|
|
19
|
+
"exit_code",
|
|
20
|
+
"failure_class",
|
|
21
|
+
"stable_excerpt",
|
|
22
|
+
"real_path_state",
|
|
23
|
+
)
|
|
24
|
+
FAILURE_CLASS_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._:-]*$")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RecordError(ValueError):
|
|
28
|
+
"""Raised when a record cannot produce a trustworthy fingerprint."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _reject_duplicate_fields(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
32
|
+
"""Build a JSON object while rejecting ambiguous duplicate field names."""
|
|
33
|
+
value: dict[str, Any] = {}
|
|
34
|
+
for field, field_value in pairs:
|
|
35
|
+
if field in value:
|
|
36
|
+
raise RecordError(f"duplicate field: {field}")
|
|
37
|
+
value[field] = field_value
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _normalize_text(value: str) -> str:
|
|
42
|
+
"""Normalize transport-only differences without deleting meaningful values."""
|
|
43
|
+
return value.replace("\r\n", "\n").replace("\r", "\n")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def canonicalize(record: Mapping[str, Any]) -> dict[str, Any]:
|
|
47
|
+
unknown = sorted(set(record) - set(REQUIRED_FIELDS))
|
|
48
|
+
missing = sorted(set(REQUIRED_FIELDS) - set(record))
|
|
49
|
+
if missing:
|
|
50
|
+
raise RecordError(f"missing field(s): {', '.join(missing)}")
|
|
51
|
+
if unknown:
|
|
52
|
+
raise RecordError(f"unknown field(s): {', '.join(unknown)}")
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
isinstance(record["schema_version"], bool)
|
|
56
|
+
or not isinstance(record["schema_version"], int)
|
|
57
|
+
or record["schema_version"] != 1
|
|
58
|
+
):
|
|
59
|
+
raise RecordError("schema_version must be 1")
|
|
60
|
+
if isinstance(record["exit_code"], bool) or not isinstance(record["exit_code"], int):
|
|
61
|
+
raise RecordError("exit_code must be an integer")
|
|
62
|
+
|
|
63
|
+
canonical: dict[str, Any] = {
|
|
64
|
+
"schema_version": 1,
|
|
65
|
+
"exit_code": record["exit_code"],
|
|
66
|
+
}
|
|
67
|
+
for field in REQUIRED_FIELDS:
|
|
68
|
+
if field in ("schema_version", "exit_code"):
|
|
69
|
+
continue
|
|
70
|
+
value = record[field]
|
|
71
|
+
if not isinstance(value, str):
|
|
72
|
+
raise RecordError(f"{field} must be a string")
|
|
73
|
+
value = _normalize_text(value)
|
|
74
|
+
if not value:
|
|
75
|
+
raise RecordError(f"{field} must not be empty")
|
|
76
|
+
canonical[field] = value
|
|
77
|
+
|
|
78
|
+
if not FAILURE_CLASS_PATTERN.fullmatch(canonical["failure_class"]):
|
|
79
|
+
raise RecordError(
|
|
80
|
+
"failure_class must use lowercase letters, numbers, dot, underscore, colon, or hyphen"
|
|
81
|
+
)
|
|
82
|
+
return canonical
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def fingerprint(record: Mapping[str, Any]) -> str:
|
|
86
|
+
payload = json.dumps(
|
|
87
|
+
canonicalize(record), ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
|
88
|
+
).encode("utf-8")
|
|
89
|
+
return f"sha256:{hashlib.sha256(payload).hexdigest()}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_record(path: str) -> Mapping[str, Any]:
|
|
93
|
+
try:
|
|
94
|
+
if path == "-":
|
|
95
|
+
value = json.load(sys.stdin, object_pairs_hook=_reject_duplicate_fields)
|
|
96
|
+
else:
|
|
97
|
+
with Path(path).open("r", encoding="utf-8") as handle:
|
|
98
|
+
value = json.load(handle, object_pairs_hook=_reject_duplicate_fields)
|
|
99
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
100
|
+
raise RecordError(str(error)) from error
|
|
101
|
+
if not isinstance(value, dict):
|
|
102
|
+
raise RecordError("record must be a JSON object")
|
|
103
|
+
return value
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main(argv: list[str] | None = None) -> int:
|
|
107
|
+
parser = argparse.ArgumentParser(
|
|
108
|
+
description="Validate a symptom record and print its canonical SHA-256 fingerprint."
|
|
109
|
+
)
|
|
110
|
+
parser.add_argument("record", help="JSON record path, or - to read standard input")
|
|
111
|
+
args = parser.parse_args(argv)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
print(fingerprint(load_record(args.record)))
|
|
115
|
+
except RecordError as error:
|
|
116
|
+
print(f"fingerprint: {error}", file=sys.stderr)
|
|
117
|
+
return 2
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Regression tests for fingerprint.py."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import importlib.util
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
import unittest
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SCRIPT = Path(__file__).with_name("fingerprint.py")
|
|
16
|
+
SPEC = importlib.util.spec_from_file_location("fix_loop_fingerprint", SCRIPT)
|
|
17
|
+
assert SPEC and SPEC.loader
|
|
18
|
+
MODULE = importlib.util.module_from_spec(SPEC)
|
|
19
|
+
SPEC.loader.exec_module(MODULE)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def record() -> dict[str, object]:
|
|
23
|
+
return {
|
|
24
|
+
"schema_version": 1,
|
|
25
|
+
"command": "python -m pytest tests/test_regression.py",
|
|
26
|
+
"input_digest": "sha256:0123456789abcdef",
|
|
27
|
+
"exit_code": 1,
|
|
28
|
+
"failure_class": "assertion-mismatch",
|
|
29
|
+
"stable_excerpt": "expected enabled\nobserved disabled",
|
|
30
|
+
"real_path_state": "settings remain disabled after reload",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FingerprintTests(unittest.TestCase):
|
|
35
|
+
def test_key_order_and_transport_newlines_do_not_change_fingerprint(self) -> None:
|
|
36
|
+
first = record()
|
|
37
|
+
second = dict(reversed(list(first.items())))
|
|
38
|
+
second["stable_excerpt"] = "expected enabled\r\nobserved disabled"
|
|
39
|
+
self.assertEqual(MODULE.fingerprint(first), MODULE.fingerprint(second))
|
|
40
|
+
|
|
41
|
+
def test_meaningful_boundary_blank_lines_change_fingerprint(self) -> None:
|
|
42
|
+
first = record()
|
|
43
|
+
second = record()
|
|
44
|
+
second["stable_excerpt"] = "\nexpected enabled\nobserved disabled\n"
|
|
45
|
+
self.assertNotEqual(MODULE.fingerprint(first), MODULE.fingerprint(second))
|
|
46
|
+
|
|
47
|
+
def test_meaningful_trailing_whitespace_changes_fingerprint(self) -> None:
|
|
48
|
+
first = record()
|
|
49
|
+
second = record()
|
|
50
|
+
second["stable_excerpt"] = "expected enabled \nobserved disabled"
|
|
51
|
+
self.assertNotEqual(MODULE.fingerprint(first), MODULE.fingerprint(second))
|
|
52
|
+
|
|
53
|
+
def test_observable_state_change_changes_fingerprint(self) -> None:
|
|
54
|
+
first = record()
|
|
55
|
+
second = record()
|
|
56
|
+
second["real_path_state"] = "settings are enabled after reload"
|
|
57
|
+
self.assertNotEqual(MODULE.fingerprint(first), MODULE.fingerprint(second))
|
|
58
|
+
|
|
59
|
+
def test_unknown_field_fails_closed(self) -> None:
|
|
60
|
+
value = record()
|
|
61
|
+
value["patch_hash"] = "must-be-recorded-outside-the-symptom-fingerprint"
|
|
62
|
+
with self.assertRaisesRegex(MODULE.RecordError, "unknown field"):
|
|
63
|
+
MODULE.fingerprint(value)
|
|
64
|
+
|
|
65
|
+
def test_missing_field_fails_closed(self) -> None:
|
|
66
|
+
value = record()
|
|
67
|
+
del value["input_digest"]
|
|
68
|
+
with self.assertRaisesRegex(MODULE.RecordError, "missing field"):
|
|
69
|
+
MODULE.fingerprint(value)
|
|
70
|
+
|
|
71
|
+
def test_boolean_schema_version_fails_closed(self) -> None:
|
|
72
|
+
value = record()
|
|
73
|
+
value["schema_version"] = True
|
|
74
|
+
with self.assertRaisesRegex(MODULE.RecordError, "schema_version must be 1"):
|
|
75
|
+
MODULE.fingerprint(value)
|
|
76
|
+
|
|
77
|
+
def test_cli_rejects_duplicate_fields_with_exit_two(self) -> None:
|
|
78
|
+
duplicate_record = (
|
|
79
|
+
'{"schema_version":1,"command":"pytest","input_digest":"sha256:abc",'
|
|
80
|
+
'"exit_code":1,"failure_class":"assertion-mismatch",'
|
|
81
|
+
'"stable_excerpt":"first","stable_excerpt":"second",'
|
|
82
|
+
'"real_path_state":"disabled"}'
|
|
83
|
+
)
|
|
84
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
85
|
+
path = Path(directory, "duplicate.json")
|
|
86
|
+
path.write_text(duplicate_record, encoding="utf-8")
|
|
87
|
+
result = subprocess.run(
|
|
88
|
+
[sys.executable, str(SCRIPT), str(path)],
|
|
89
|
+
check=False,
|
|
90
|
+
capture_output=True,
|
|
91
|
+
text=True,
|
|
92
|
+
)
|
|
93
|
+
self.assertEqual(result.returncode, 2)
|
|
94
|
+
self.assertIn("duplicate field: stable_excerpt", result.stderr)
|
|
95
|
+
self.assertNotIn("Traceback", result.stderr)
|
|
96
|
+
|
|
97
|
+
def test_cli_rejects_invalid_utf8_with_exit_two(self) -> None:
|
|
98
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
99
|
+
path = Path(directory, "invalid-utf8.json")
|
|
100
|
+
path.write_bytes(b"\xff")
|
|
101
|
+
result = subprocess.run(
|
|
102
|
+
[sys.executable, str(SCRIPT), str(path)],
|
|
103
|
+
check=False,
|
|
104
|
+
capture_output=True,
|
|
105
|
+
text=True,
|
|
106
|
+
)
|
|
107
|
+
self.assertEqual(result.returncode, 2)
|
|
108
|
+
self.assertIn("invalid start byte", result.stderr)
|
|
109
|
+
self.assertNotIn("Traceback", result.stderr)
|
|
110
|
+
|
|
111
|
+
def test_cli_rejects_invalid_record_with_exit_two(self) -> None:
|
|
112
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
113
|
+
path = Path(directory, "invalid.json")
|
|
114
|
+
path.write_text(json.dumps({"schema_version": 1}), encoding="utf-8")
|
|
115
|
+
result = subprocess.run(
|
|
116
|
+
[sys.executable, str(SCRIPT), str(path)],
|
|
117
|
+
check=False,
|
|
118
|
+
capture_output=True,
|
|
119
|
+
text=True,
|
|
120
|
+
)
|
|
121
|
+
self.assertEqual(result.returncode, 2)
|
|
122
|
+
self.assertIn("missing field", result.stderr)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
unittest.main()
|