mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-14 21:26:54 +00:00
chore: publish from main
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: bug-receipt
|
||||
description: 'Fix software defects with an auditable proof receipt: reproduce, trace root cause, repair, verify, and report VERIFIED, PARTIAL, or BLOCKED. Use for bug fixes and regressions.'
|
||||
---
|
||||
|
||||
# Bug Receipt
|
||||
|
||||
Treat the receipt as the completion gate, not as decoration added after a conclusion.
|
||||
|
||||
## Define proof before editing
|
||||
|
||||
Write a compact working ledger with the observed problem, intended behavior, strongest direct acceptance check, and proof layers required by the affected surface. Keep it current while investigating.
|
||||
|
||||
Choose proof that can falsify the fix. A green build is not a substitute for a browser interaction, API round trip, persistence reload, or concurrency sequence when one of those is the user-visible contract.
|
||||
|
||||
## Establish the baseline
|
||||
|
||||
1. Restate the observed defect and the intended behavior in one sentence each.
|
||||
2. Run the narrowest safe reproduction before editing whenever the environment permits it.
|
||||
3. Record the exact command or interaction and the decisive failing observation.
|
||||
4. If reproduction is unavailable, state why and cap the final status at `PARTIAL` or `BLOCKED`.
|
||||
|
||||
Do not convert an assumption, stale log, source read, or passing build into a reproduced baseline.
|
||||
|
||||
## Trace the cause
|
||||
|
||||
Follow the live owner path far enough to distinguish the responsible cause from a nearby symptom. Cite concrete evidence such as a file and line, stack frame, request/response, state transition, or runtime observation.
|
||||
|
||||
Separate:
|
||||
|
||||
- facts directly observed;
|
||||
- bounded inferences supported by those facts;
|
||||
- remaining gaps.
|
||||
|
||||
Do not claim root cause from plausibility alone.
|
||||
|
||||
## Repair the responsible layer
|
||||
|
||||
Make the smallest change that fixes the responsible behavior and preserves adjacent contracts. Avoid unrelated cleanup, silent fallbacks, fixture-specific exceptions, retries, or post-processing unless the product contract requires them.
|
||||
|
||||
Record every changed file or artifact and its role in the repair.
|
||||
|
||||
## Close the proof loop
|
||||
|
||||
Run, in proportion to the defect:
|
||||
|
||||
1. the original reproduction or direct acceptance check;
|
||||
2. the nearest relevant negative or regression check;
|
||||
3. the affected build, type, lint, or integration gate when applicable;
|
||||
4. the live UI, network, backend, or runtime path when the user-visible claim depends on it.
|
||||
|
||||
Record exact commands and observed results. Never invent a test, command, count, file location, or runtime observation.
|
||||
|
||||
Use these minimum direct checks when applicable:
|
||||
|
||||
| Defect surface | Direct proof |
|
||||
| --- | --- |
|
||||
| Logic or failing test | Original failing input or focused test now passes |
|
||||
| UI behavior | Real interaction plus relevant console and network observation |
|
||||
| API or integration | Request, response, and responsible service behavior |
|
||||
| Persistence | Write/read or reload round trip through the real owner path |
|
||||
| Race or lifecycle | Repeated triggering sequence and the violated invariant |
|
||||
| Build or configuration | Affected build, startup, or deployment path |
|
||||
|
||||
## Assign status
|
||||
|
||||
- Use `VERIFIED` only when the baseline failure was observed, root-cause evidence is concrete, the responsible change is identified, every declared verification passed, and no material gap remains.
|
||||
- Use `PARTIAL` when useful evidence exists but at least one required proof layer is missing or inconclusive.
|
||||
- Use `BLOCKED` when the fix or its proof cannot proceed because of a specific external condition.
|
||||
|
||||
For `BLOCKED`, name the single next evidence package or experiment that closes the causal chain. When the failure spans systems, require correlated evidence from every relevant owner rather than an isolated capture.
|
||||
|
||||
Passing syntax, compilation, one narrow unit test, or source inspection alone does not prove downstream behavior unless it is the complete acceptance contract.
|
||||
|
||||
## Return the receipt
|
||||
|
||||
Finish with this compact structure:
|
||||
|
||||
```text
|
||||
BUG RECEIPT · VERIFIED | PARTIAL | BLOCKED
|
||||
|
||||
Problem <observed defect and intended behavior>
|
||||
Baseline <exact command or interaction>
|
||||
<decisive observed result>
|
||||
Root cause <location and evidence-backed mechanism>
|
||||
Change <file or artifact — responsible repair>
|
||||
Proof <check: result · check: result>
|
||||
Gaps <none, or the exact missing proof>
|
||||
```
|
||||
|
||||
Use `not run` explicitly where applicable. Do not omit a row to make the receipt look complete.
|
||||
|
||||
For a machine-readable receipt or CI integration, read [references/receipt-contract.md](references/receipt-contract.md) and conform to its JSON fields and status invariants.
|
||||
|
||||
When a JSON artifact is requested, start from [assets/receipt.template.json](assets/receipt.template.json), write it to a task-owned path, and validate it with `node scripts/validate-receipt.mjs <receipt.json>` from this skill directory. Do not commit the generated receipt unless the user requests it.
|
||||
|
||||
## Source and license
|
||||
|
||||
Originally published at https://github.com/lMysticl/bug-receipt under the MIT License.
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"version": 1,
|
||||
"status": "partial",
|
||||
"problem": "Describe the observed defect and intended behavior.",
|
||||
"baseline": {
|
||||
"command": "Record the exact reproduction command or interaction.",
|
||||
"result": "not-run",
|
||||
"evidence": "State the decisive observation, or why it could not be obtained."
|
||||
},
|
||||
"rootCause": {
|
||||
"summary": "State the evidence-backed mechanism, or mark it unresolved.",
|
||||
"evidence": []
|
||||
},
|
||||
"changes": [],
|
||||
"verification": [],
|
||||
"gaps": [
|
||||
"Replace this with the exact missing proof layer."
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# Machine-readable receipt contract
|
||||
|
||||
Use JSON only when the user, CI, or another tool needs a structured artifact. Keep the normal final answer human-readable.
|
||||
|
||||
## Required fields
|
||||
|
||||
- `version`: integer `1`.
|
||||
- `status`: `verified`, `partial`, or `blocked`.
|
||||
- `problem`: concise defect and intended behavior.
|
||||
- `baseline`: object with `command`, `result`, and `evidence`.
|
||||
- `rootCause`: object with `summary` and at least one evidence item for `verified`.
|
||||
- `changes`: array of `{ "file", "summary" }` objects.
|
||||
- `verification`: array of `{ "command", "result", "evidence" }` objects.
|
||||
- `gaps`: array of explicit missing proof statements.
|
||||
|
||||
Baseline results are `failed`, `observed`, or `not-run`. Verification results are `passed`, `failed`, or `not-run`.
|
||||
|
||||
## Status invariants
|
||||
|
||||
For `verified`:
|
||||
|
||||
- Require an observed baseline: `failed` or `observed`, never `not-run`.
|
||||
- Require at least one concrete root-cause evidence item with `location` and `observation`.
|
||||
- Require at least one changed file or artifact.
|
||||
- Require at least one verification item.
|
||||
- Require every verification result to be `passed`.
|
||||
- Require `gaps` to be empty.
|
||||
|
||||
For `partial`:
|
||||
|
||||
- Preserve all evidence obtained.
|
||||
- Put every missing or inconclusive proof layer in `gaps`.
|
||||
- Never convert an unrun check into `passed`.
|
||||
|
||||
For `blocked`:
|
||||
|
||||
- Require at least one gap naming the external blocking condition.
|
||||
- Leave unperformed work empty or mark it `not-run`; do not speculate about the result.
|
||||
|
||||
Validate against [receipt.schema.json](receipt.schema.json), run `node scripts/validate-receipt.mjs <file>` from the skill directory, or pipe JSON to `node scripts/validate-receipt.mjs - --json`.
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://lmysticl.github.io/bug-receipt/receipt.schema.json",
|
||||
"title": "Bug Receipt",
|
||||
"description": "A machine-readable evidence receipt for a software bug fix.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["version", "status", "problem", "baseline", "rootCause", "changes", "verification", "gaps"],
|
||||
"properties": {
|
||||
"version": { "const": 1 },
|
||||
"status": { "enum": ["verified", "partial", "blocked"] },
|
||||
"problem": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"baseline": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["command", "result", "evidence"],
|
||||
"properties": {
|
||||
"command": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"result": { "enum": ["failed", "observed", "not-run"] },
|
||||
"evidence": { "$ref": "#/$defs/nonEmptyString" }
|
||||
}
|
||||
},
|
||||
"rootCause": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["summary", "evidence"],
|
||||
"properties": {
|
||||
"summary": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"evidence": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["location", "observation"],
|
||||
"properties": {
|
||||
"location": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"observation": { "$ref": "#/$defs/nonEmptyString" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"changes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["file", "summary"],
|
||||
"properties": {
|
||||
"file": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"summary": { "$ref": "#/$defs/nonEmptyString" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"verification": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["command", "result", "evidence"],
|
||||
"properties": {
|
||||
"command": { "$ref": "#/$defs/nonEmptyString" },
|
||||
"result": { "enum": ["passed", "failed", "not-run"] },
|
||||
"evidence": { "$ref": "#/$defs/nonEmptyString" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"gaps": { "type": "array", "items": { "$ref": "#/$defs/nonEmptyString" } }
|
||||
},
|
||||
"$defs": {
|
||||
"nonEmptyString": { "type": "string", "minLength": 1, "pattern": "\\S" }
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": { "properties": { "status": { "const": "verified" } }, "required": ["status"] },
|
||||
"then": {
|
||||
"properties": {
|
||||
"baseline": { "type": "object", "properties": { "result": { "enum": ["failed", "observed"] } } },
|
||||
"rootCause": { "type": "object", "properties": { "evidence": { "type": "array", "minItems": 1 } } },
|
||||
"changes": { "type": "array", "minItems": 1 },
|
||||
"verification": { "type": "array", "minItems": 1, "items": { "type": "object", "properties": { "result": { "const": "passed" } } } },
|
||||
"gaps": { "type": "array", "maxItems": 0 }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": { "properties": { "status": { "enum": ["partial", "blocked"] } }, "required": ["status"] },
|
||||
"then": { "properties": { "gaps": { "type": "array", "minItems": 1 } } }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const statuses = new Set(['verified', 'partial', 'blocked'])
|
||||
const baselineResults = new Set(['failed', 'observed', 'not-run'])
|
||||
const verificationResults = new Set(['passed', 'failed', 'not-run'])
|
||||
|
||||
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
||||
const nonEmpty = (value) => typeof value === 'string' && value.trim().length > 0
|
||||
|
||||
export const sampleReceipt = {
|
||||
version: 1,
|
||||
status: 'verified',
|
||||
problem: 'A 10% checkout discount returns 100 instead of 90 after currency rounding.',
|
||||
baseline: {
|
||||
command: 'npm test -- discount.test.ts',
|
||||
result: 'failed',
|
||||
evidence: 'Expected 90, received 100.',
|
||||
},
|
||||
rootCause: {
|
||||
summary: 'The subtotal was rounded before the percentage discount was applied.',
|
||||
evidence: [{ location: 'src/pricing.ts:42', observation: 'roundCurrency(subtotal) was passed into applyDiscount().' }],
|
||||
},
|
||||
changes: [{ file: 'src/pricing.ts', summary: 'Apply the discount to the subtotal before currency rounding.' }],
|
||||
verification: [
|
||||
{ command: 'npm test -- discount.test.ts', result: 'passed', evidence: '1 test passed.' },
|
||||
{ command: 'npm test', result: 'passed', evidence: '42 tests passed.' },
|
||||
],
|
||||
gaps: [],
|
||||
}
|
||||
|
||||
export function validateReceipt(receipt) {
|
||||
const issues = []
|
||||
const add = (path, message) => issues.push({ path, message })
|
||||
const rejectUnknown = (value, allowed, path) => {
|
||||
if (!isObject(value)) return
|
||||
for (const key of Object.keys(value)) {
|
||||
if (!allowed.has(key)) add(path ? `${path}.${key}` : key, 'Unknown field.')
|
||||
}
|
||||
}
|
||||
|
||||
if (!isObject(receipt)) return { valid: false, issues: [{ path: '$', message: 'Receipt must be a JSON object.' }] }
|
||||
rejectUnknown(receipt, new Set(['version', 'status', 'problem', 'baseline', 'rootCause', 'changes', 'verification', 'gaps']), '')
|
||||
|
||||
if (receipt.version !== 1) add('version', 'Must equal 1.')
|
||||
if (!statuses.has(receipt.status)) add('status', 'Must be verified, partial, or blocked.')
|
||||
if (!nonEmpty(receipt.problem)) add('problem', 'Must be a non-empty string.')
|
||||
|
||||
if (!isObject(receipt.baseline)) {
|
||||
add('baseline', 'Must be an object.')
|
||||
} else {
|
||||
rejectUnknown(receipt.baseline, new Set(['command', 'result', 'evidence']), 'baseline')
|
||||
if (!nonEmpty(receipt.baseline.command)) add('baseline.command', 'Must be a non-empty string.')
|
||||
if (!baselineResults.has(receipt.baseline.result)) add('baseline.result', 'Must be failed, observed, or not-run.')
|
||||
if (!nonEmpty(receipt.baseline.evidence)) add('baseline.evidence', 'Must be a non-empty string.')
|
||||
}
|
||||
|
||||
if (!isObject(receipt.rootCause)) {
|
||||
add('rootCause', 'Must be an object.')
|
||||
} else {
|
||||
rejectUnknown(receipt.rootCause, new Set(['summary', 'evidence']), 'rootCause')
|
||||
if (!nonEmpty(receipt.rootCause.summary)) add('rootCause.summary', 'Must be a non-empty string.')
|
||||
if (!Array.isArray(receipt.rootCause.evidence)) {
|
||||
add('rootCause.evidence', 'Must be an array.')
|
||||
} else {
|
||||
receipt.rootCause.evidence.forEach((entry, index) => {
|
||||
if (!isObject(entry)) return add(`rootCause.evidence[${index}]`, 'Must be an object.')
|
||||
rejectUnknown(entry, new Set(['location', 'observation']), `rootCause.evidence[${index}]`)
|
||||
if (!nonEmpty(entry.location)) add(`rootCause.evidence[${index}].location`, 'Must be a non-empty string.')
|
||||
if (!nonEmpty(entry.observation)) add(`rootCause.evidence[${index}].observation`, 'Must be a non-empty string.')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(receipt.changes)) {
|
||||
add('changes', 'Must be an array.')
|
||||
} else {
|
||||
receipt.changes.forEach((entry, index) => {
|
||||
if (!isObject(entry)) return add(`changes[${index}]`, 'Must be an object.')
|
||||
rejectUnknown(entry, new Set(['file', 'summary']), `changes[${index}]`)
|
||||
if (!nonEmpty(entry.file)) add(`changes[${index}].file`, 'Must be a non-empty string.')
|
||||
if (!nonEmpty(entry.summary)) add(`changes[${index}].summary`, 'Must be a non-empty string.')
|
||||
})
|
||||
}
|
||||
|
||||
if (!Array.isArray(receipt.verification)) {
|
||||
add('verification', 'Must be an array.')
|
||||
} else {
|
||||
receipt.verification.forEach((entry, index) => {
|
||||
if (!isObject(entry)) return add(`verification[${index}]`, 'Must be an object.')
|
||||
rejectUnknown(entry, new Set(['command', 'result', 'evidence']), `verification[${index}]`)
|
||||
if (!nonEmpty(entry.command)) add(`verification[${index}].command`, 'Must be a non-empty string.')
|
||||
if (!verificationResults.has(entry.result)) add(`verification[${index}].result`, 'Must be passed, failed, or not-run.')
|
||||
if (!nonEmpty(entry.evidence)) add(`verification[${index}].evidence`, 'Must be a non-empty string.')
|
||||
})
|
||||
}
|
||||
|
||||
if (!Array.isArray(receipt.gaps) || receipt.gaps.some((gap) => !nonEmpty(gap))) add('gaps', 'Must be an array of non-empty strings.')
|
||||
|
||||
if (receipt.status === 'verified') {
|
||||
if (receipt.baseline?.result === 'not-run') add('baseline.result', 'Verified requires an observed baseline.')
|
||||
if (!Array.isArray(receipt.rootCause?.evidence) || receipt.rootCause.evidence.length === 0) add('rootCause.evidence', 'Verified requires concrete root-cause evidence.')
|
||||
if (!Array.isArray(receipt.changes) || receipt.changes.length === 0) add('changes', 'Verified requires at least one changed file or artifact.')
|
||||
if (!Array.isArray(receipt.verification) || receipt.verification.length === 0) add('verification', 'Verified requires at least one verification check.')
|
||||
if (receipt.verification?.some((entry) => entry?.result !== 'passed')) add('verification', 'Every verification check must pass for verified status.')
|
||||
if (Array.isArray(receipt.gaps) && receipt.gaps.length > 0) add('gaps', 'Verified status cannot contain proof gaps.')
|
||||
}
|
||||
|
||||
if (receipt.status === 'partial' && Array.isArray(receipt.gaps) && receipt.gaps.length === 0) add('gaps', 'Partial status must name at least one missing proof layer.')
|
||||
if (receipt.status === 'blocked' && Array.isArray(receipt.gaps) && receipt.gaps.length === 0) add('gaps', 'Blocked status must name the external blocking condition.')
|
||||
|
||||
return { valid: issues.length === 0, issues }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const path = process.argv[2]
|
||||
if (!path) throw new Error('Usage: node scripts/validate-receipt.mjs <receipt.json> [--json]')
|
||||
|
||||
let input = ''
|
||||
if (path === '-') {
|
||||
process.stdin.setEncoding('utf8')
|
||||
for await (const chunk of process.stdin) input += chunk
|
||||
} else {
|
||||
input = await readFile(resolve(path), 'utf8')
|
||||
}
|
||||
const receipt = JSON.parse(input)
|
||||
const result = validateReceipt(receipt)
|
||||
|
||||
if (process.argv.includes('--json')) {
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`)
|
||||
} else if (result.valid) {
|
||||
process.stdout.write(`✓ ${path} is a valid ${receipt.status.toUpperCase()} bug receipt.\n`)
|
||||
} else {
|
||||
process.stderr.write(`✗ ${path} is not a valid bug receipt:\n`)
|
||||
for (const issue of result.issues) process.stderr.write(` ${issue.path}: ${issue.message}\n`)
|
||||
}
|
||||
|
||||
process.exitCode = result.valid ? 0 : 1
|
||||
}
|
||||
|
||||
const invokedUrl = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : ''
|
||||
if (import.meta.url === invokedUrl) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`bug-receipt: ${error.message}\n`)
|
||||
process.exitCode = 2
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user