task-pipeline-skill 1.101.1 → 1.102.2
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/CHANGELOG.md +68 -0
- package/README.md +2 -1
- package/SKILL-CARD.md +1 -1
- package/package.json +3 -3
- package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
- package/plugins/task-pipeline/skills/evidence-docs/SKILL.md +1 -1
- package/plugins/task-pipeline/skills/project-audit/SKILL.md +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/pipeline.example.json +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +8 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/artifacts.md +2 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/doctrine-map.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/release-impact.md +154 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/stage-07-lint-deploy.md +6 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/stage-08-post-deploy.md +6 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +13 -0
- package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/scripts/release_impact.py +278 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/README.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/release-impact.md +50 -0
- package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,71 @@
|
|
|
1
|
+
## v1.102.2: v1.102.1 republished
|
|
2
|
+
|
|
3
|
+
- **v1.102.1 republished.** Its tag was pushed and the release run (40241842429) refused it before
|
|
4
|
+
publishing: `docs/evidence/retro.md` → *Releases that carry no stamp* did not name v1.102.1. That
|
|
5
|
+
section now names v1.102.1 and v1.102.2, and v1.102.2 ships v1.102.1's content (the three
|
|
6
|
+
descriptions within a 300-character budget). `v1.102.1` stays as an unpublished tag.
|
|
7
|
+
|
|
8
|
+
## v1.102.1: descriptions within a 300-character budget
|
|
9
|
+
|
|
10
|
+
- **Descriptions within a 300-character budget** (hub board row B-255 (a)): Claude Code lists every
|
|
11
|
+
skill's description in one ~30,000-character budget, and on the operator's machine the listing
|
|
12
|
+
stood at 51,178. Measured the hub's way (whitespace collapsed, front matter only): task-pipeline
|
|
13
|
+
388 to 294, project-audit 401 to 295, evidence-docs 397 to 283. The full trigger lists stay in each
|
|
14
|
+
`## Triggers`; B-001 and the README conformance sentence carry the new figure. Release impact card:
|
|
15
|
+
`docs/evidence/specs/2026-10-25-v11021-release-impact.md`.
|
|
16
|
+
|
|
17
|
+
## v1.102.0 (2026-10-24)
|
|
18
|
+
|
|
19
|
+
Guards: 530 → **531**; property checks 15 → **15**. Negative 550 is new; the release impact
|
|
20
|
+
checks are `test/release_impact_test.py` (47 cases), wired into `npm test` and `test:all`.
|
|
21
|
+
|
|
22
|
+
- **B-155:** `_is_dated_record()` read a section as dated when its heading and the first two
|
|
23
|
+
blocks under it carried an ISO date; under `## Closed` the second block is the newest closed
|
|
24
|
+
item, so one dated item exempted every number in the section and negative 442 stopped firing.
|
|
25
|
+
The section stamp is now the heading and the one block beneath it. Negative 550 plants a
|
|
26
|
+
dated first Closed item beside a skewed fixture count and must fail.
|
|
27
|
+
|
|
28
|
+
**Every release in a project with users owes a release impact card** (operator, 2026-10-24):
|
|
29
|
+
before a release harms anyone, it says who it touches and how, how it is watched, what tells
|
|
30
|
+
someone it went wrong, and the way back.
|
|
31
|
+
|
|
32
|
+
- **`references/release-impact.md`** and **`templates/release-impact.md`**: the card
|
|
33
|
+
(`release-impact/1`) is owed at stage 7 before any outward act (tag, publish, deploy) when
|
|
34
|
+
`pipeline.json` → `release.users` is `external` or `internal`; absent is unknown and treated as
|
|
35
|
+
`external`; `none` owes no card and prints `users: none` beside the stage 7 verdict. Four
|
|
36
|
+
sections, each answered or `n/a: <reason>`: who is affected and how (segments, changes,
|
|
37
|
+
breakage, rollout, timing), tracking (signals, baseline, where, watch window), alerting
|
|
38
|
+
(condition, receiver, whether it was ever seen firing), rollback or hotfix (procedure, time,
|
|
39
|
+
data reversibility, kill switch, hotfix path, decision trigger). Worked examples for a package
|
|
40
|
+
release and for a web or mobile product.
|
|
41
|
+
- **`scripts/release_impact.py check`** refuses a missing, empty or duplicated section, `n/a`
|
|
42
|
+
without a reason, a `TODO`, tracking without a watch window, alerting that does not say
|
|
43
|
+
whether it ever fired, a rollback without a decision trigger, tracking, alerting or rollback
|
|
44
|
+
answered `n/a` on a release with users, and a card whose `users:` disagrees with the project;
|
|
45
|
+
no card where one is owed is NOT_RUN (exit 3). `--outcome` checks stage 8's record.
|
|
46
|
+
- **Stages:** the stage 7 GATE in `references/stages.md` owes the card; stage 8 watches its
|
|
47
|
+
signals for the window, records the outcome and acts on the decision trigger per the card.
|
|
48
|
+
`pipeline.schema.json` declares `release.users`; this repository's own `pipeline.json` says
|
|
49
|
+
`external`.
|
|
50
|
+
|
|
51
|
+
## v1.101.2 (2026-10-24)
|
|
52
|
+
|
|
53
|
+
**Not tagged.** Its candidate failed the tagged documentation gate: the B-150 closure cited
|
|
54
|
+
the two fw07 branch heads by SHA, and after the branches were deleted a clone cannot follow
|
|
55
|
+
them. The citation now names the pull request refs; everything below ships in v1.102.0.
|
|
56
|
+
|
|
57
|
+
Guards: 530 → **530**; property checks 15 → **15**. No negative was added.
|
|
58
|
+
|
|
59
|
+
- **The family member gate** (sshlg-skills B-108, B-109): `test/family_gate.py`, a byte-for-byte
|
|
60
|
+
copy of the hub's `member-gate/family_gate.py` with its `# shared-mechanism: family-gate` and
|
|
61
|
+
`# diverges: none` declaration, runs with `--self-test` from
|
|
62
|
+
`test/audit_regressions/family_gate_member.py` (the board parses; `release.yml` keeps the
|
|
63
|
+
tag-ancestry check).
|
|
64
|
+
- **B-156:** `test/lessons_test.py` cleanup tolerates a file git's background maintenance
|
|
65
|
+
already removed (`_rm()`), the race that failed the v1.101.1 release run's first attempt.
|
|
66
|
+
- **B-150 closed:** the two kept fw07 branches were deleted; their heads stay reachable as
|
|
67
|
+
`refs/pull/123/head` and `refs/pull/124/head`.
|
|
68
|
+
|
|
1
69
|
## v1.101.1 (2026-10-23)
|
|
2
70
|
|
|
3
71
|
Guards: 530 → **530**; property checks 15 → **15**. No negative was added: the new checks are
|
package/README.md
CHANGED
|
@@ -187,6 +187,7 @@ and the inline fallback satisfy the same [provider contract](plugins/task-pipeli
|
|
|
187
187
|
| 5–6 TDD | [`tdd.md`](plugins/task-pipeline/skills/task-pipeline/references/tdd.md) — the iron law, red/green/refactor, the suite gate |
|
|
188
188
|
| 5, 6, 8 The browser | [`browser.md`](plugins/task-pipeline/skills/task-pipeline/references/browser.md) — the ref model both channels share, the four commands the look is made of, sessions, and the three different things *"tested in a browser"* means |
|
|
189
189
|
| 3, 6 The design review | [`design-review.md`](plugins/task-pipeline/skills/task-pipeline/references/design-review.md): the `design-tester` agent walks the ten-portrait test panel through the changed scenarios over the Figma frames or mockups before build and in the recorded browser after it, judges every step on a fixed checklist, and closes everything it started (`scripts/design_gate.py`, `scripts/resource_guard.py`) |
|
|
190
|
+
| 7, 8 Release impact | [`release-impact.md`](plugins/task-pipeline/skills/task-pipeline/references/release-impact.md): a release in a project with users owes a card before the tag, the publish or the deploy (who is affected and how, the tracking and its watch window, the alerting and whether it ever fired, the rollback or hotfix and its decision trigger), and stage 8 watches it and acts on the trigger (`scripts/release_impact.py`) |
|
|
190
191
|
| 10 Acceptance | [`acceptance.md`](plugins/task-pipeline/skills/task-pipeline/references/acceptance.md) — REQ coverage table, evidence rules, the closing question |
|
|
191
192
|
| 10 + any audit | [`audit.md`](plugins/task-pipeline/skills/task-pipeline/references/audit.md) — the L0→L7 ladder and its seams, axis rotation, ratchets, proven checks |
|
|
192
193
|
| any loop | [`loop-guard.md`](plugins/task-pipeline/skills/task-pipeline/references/loop-guard.md) — churn detection, caps, the break protocol |
|
|
@@ -642,7 +643,7 @@ must say so.
|
|
|
642
643
|
### Held to Anthropic's own Skill authoring guidance
|
|
643
644
|
|
|
644
645
|
Audited against the four Agent Skills pages. Most of it already held — `name`
|
|
645
|
-
13/64 chars, `description`
|
|
646
|
+
13/64 chars, `description` 292/1024 chars, `SKILL.md` 207/500 lines, all 55 references
|
|
646
647
|
reachable from `SKILL.md` through explicit reference routes, and the bundle far under the 30 MB ceiling. What
|
|
647
648
|
did not, now does:
|
|
648
649
|
|
package/SKILL-CARD.md
CHANGED
|
@@ -12,7 +12,7 @@ harmless.
|
|
|
12
12
|
|---|---|
|
|
13
13
|
| **Purpose** | Runs a substantial task through ten gated delivery stages — intake grill, docs study, brainstorm, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs+registers, acceptance — refusing to advance until each gate passes |
|
|
14
14
|
| **Owner** | ssheleg ([github.com/ssheleg/task-pipeline](https://github.com/ssheleg/task-pipeline)) |
|
|
15
|
-
| **Version** | 1.
|
|
15
|
+
| **Version** | 1.102.2 |
|
|
16
16
|
| **Surface** | Claude Code (filesystem skill + plugin) and the vercel `skills` CLI. **Not** uploaded to the Skills API; custom Skills do not sync across surfaces |
|
|
17
17
|
| **Dependencies** | None required. Optional: `context7` (MCP), `figma` (MCP), super-ux, agent-sync, graphify, obsidian-wiki, and **one of two browser channels** — `playwright` (CLI or MCP) or `chrome-devtools` (MCP); either satisfies the browser step and neither is required. The engine is WebKit (Safari) first; `chrome-devtools` is Chrome-only, a stated exception. Every stage's doctrine ships in-repo; user-facing stage 3 requires a valid scenario artifact; super-ux is preferred, with equivalent-provider and inline-fallback paths under the provider contract |
|
|
18
18
|
| **Evaluation status** | Suite authored, 5 categories. One recorded run, **self-observed by the author**; **zero blind runs on zero of three models** — the split, and the numbers, live in [`evals/RESULTS.md`](evals/RESULTS.md) and are computed by `evals/run.py` |
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "task-pipeline-skill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.102.2",
|
|
4
4
|
"description": "Full-cycle delivery pipeline for coding agents: a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine ships inside the skill — no companion plugin required. This package is the installer CLI.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"task-pipeline": "bin/task-pipeline.js"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"test": "python3 test/validate.py && python3 test/graph_test.py && python3 test/project_audit_test.py && python3 test/packet_schema_test.py && python3 test/browser_claims_test.py && python3 test/visual_gate_test.py && python3 test/stage_checkpoint_test.py && python3 test/backlog_sources_test.py && python3 test/release_approval_test.py && python3 test/memory_nudge_test.py && python3 test/lessons_test.py && python3 test/waves_test.py && python3 test/design_gate_test.py && python3 test/continuity_doc_test.py && bash test/install_test.sh && npm run test:audit-regressions",
|
|
10
|
-
"test:all": "python3 test/validate.py && python3 test/graph_test.py && python3 test/project_audit_test.py && python3 test/packet_schema_test.py && python3 test/browser_claims_test.py && python3 test/visual_gate_test.py && python3 test/stage_checkpoint_test.py && python3 test/backlog_sources_test.py && python3 test/release_approval_test.py && python3 test/memory_nudge_test.py && python3 test/lessons_test.py && python3 test/waves_test.py && python3 test/design_gate_test.py && python3 test/continuity_doc_test.py && bash test/install_test.sh && python3 test/negatives.py && npm run test:certify && npm run test:exposure && npm run test:probe && npm run test:anchors && npm run test:runner && npm run test:hooks && npm run test:artifacts && npm run test:docs && npm run test:audit-regressions",
|
|
9
|
+
"test": "python3 test/validate.py && python3 test/graph_test.py && python3 test/project_audit_test.py && python3 test/packet_schema_test.py && python3 test/browser_claims_test.py && python3 test/visual_gate_test.py && python3 test/stage_checkpoint_test.py && python3 test/backlog_sources_test.py && python3 test/release_approval_test.py && python3 test/memory_nudge_test.py && python3 test/lessons_test.py && python3 test/waves_test.py && python3 test/design_gate_test.py && python3 test/release_impact_test.py && python3 test/continuity_doc_test.py && bash test/install_test.sh && npm run test:audit-regressions",
|
|
10
|
+
"test:all": "python3 test/validate.py && python3 test/graph_test.py && python3 test/project_audit_test.py && python3 test/packet_schema_test.py && python3 test/browser_claims_test.py && python3 test/visual_gate_test.py && python3 test/stage_checkpoint_test.py && python3 test/backlog_sources_test.py && python3 test/release_approval_test.py && python3 test/memory_nudge_test.py && python3 test/lessons_test.py && python3 test/waves_test.py && python3 test/design_gate_test.py && python3 test/release_impact_test.py && python3 test/continuity_doc_test.py && bash test/install_test.sh && python3 test/negatives.py && npm run test:certify && npm run test:exposure && npm run test:probe && npm run test:anchors && npm run test:runner && npm run test:hooks && npm run test:artifacts && npm run test:docs && npm run test:audit-regressions",
|
|
11
11
|
"test:negatives": "python3 test/negatives.py",
|
|
12
12
|
"test:exposure": "python3 test/exposure_test.py",
|
|
13
13
|
"test:probe": "python3 test/probe.py --self-test",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "task-pipeline",
|
|
4
4
|
"displayName": "Task Pipeline",
|
|
5
5
|
"description": "Runs a substantial task through a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine is built into the skill — no companion plugin required — with typed auto/judgment/manual gates, a frozen requirement spine that closes with evidence, a work board and a verification ledger that outlive a run, an exposure line naming what shipped unconfirmed, a progress rail computed from the project's own config, a proportional short path for small changes, a loop guard whose ceilings stop and record a blocker, and stage-3 tracks for what a product does, how it sounds and how it looks. Two modes need no task: `checkup` (what is unverified) and `setup` (audit existing docs); a third, `waves`, takes the whole board and closes it in waves that each finish in one pass. Retro insights can publish upstream as issues, opt-in and redacted.",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.102.2",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "ssheleg",
|
|
9
9
|
"url": "https://x.com/sshlg93"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: evidence-docs
|
|
3
|
-
description: "Use when writing or reviewing anything read as true: a decision record, ADR, README, acceptance report, runbook, changelog, audit finding,
|
|
3
|
+
description: "Use when writing or reviewing anything read as true: a decision record, ADR, README, acceptance report, runbook, changelog, audit finding, a claim of verification. Applies the ten canons of evidence-backed docs. Not for: drafts, chat answers, commit messages; 'без доков' opts out."
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: "Doctrine runs on any agent. No bundled scripts — nothing to install and nothing to degrade."
|
|
6
6
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: project-audit
|
|
3
|
-
description: "Use when someone asks what is true of a whole project
|
|
3
|
+
description: "Use when someone asks what is true of a whole project ('project audit', 'аудит проекта'): finished, broken, unexplored. Probes it, reads production evidence, leaves a JSON sidecar. Read-only. Not for: one deliverable in a run, a diff review, a skill's construction; 'без диагностики' opts out."
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: "The collector (scripts/audit.py) needs python3 and reads committed state, so it needs git. Probes needing gh, npm, network or a browser declare it and report blind when it is absent — degraded, never silent."
|
|
6
6
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: task-pipeline
|
|
3
|
-
description: "Use when work changes the repository: feature, fix,
|
|
3
|
+
description: "Use when work changes the repository: feature, fix, refactor, migration, integration, audit, PR review; фича, баг, интеграция, аудит. Runs a substantial task through gated stages to acceptance; 'waves'/'волнами' closes the board. Not for questions or one-line edits; 'без пайплайна' opts out."
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: "Doctrine runs on any agent. The bundled scripts need python3; the run needs git. Missing either degrades, never blocks — the graph verbs and seeded gates go unused, and the run says so."
|
|
6
6
|
---
|
|
@@ -188,6 +188,7 @@
|
|
|
188
188
|
"enabled": true,
|
|
189
189
|
"trigger": "tag",
|
|
190
190
|
"approval": "checks",
|
|
191
|
+
"users": "external",
|
|
191
192
|
"steps": [
|
|
192
193
|
"create a GitHub release from the matching CHANGELOG section",
|
|
193
194
|
"npm publish (task-pipeline-skill) — a second job in the same workflow, armed per repository by the PUBLISH_NPMJS variable (NPM_TOKEN granular automation token, or trusted publishing via OIDC); unarmed it falls back to a human 2FA step"
|
|
@@ -310,6 +310,14 @@
|
|
|
310
310
|
},
|
|
311
311
|
"description": "Optional single-line source locator (for example docs/DECISIONS.md:42) for the standing authorization covering the tag and its publication. A locator, not a grant: how it is read and verified lives in references/deploy-targets.md, When the release waits for an approval."
|
|
312
312
|
},
|
|
313
|
+
"users": {
|
|
314
|
+
"enum": [
|
|
315
|
+
"external",
|
|
316
|
+
"internal",
|
|
317
|
+
"none"
|
|
318
|
+
],
|
|
319
|
+
"description": "Who uses what this project releases: `external` (people or systems outside the team install, call or open it), `internal` (only the team or its own agents), `none` (nothing ships to anyone). `external` and `internal` owe a release impact card at stage 7 before any outward act (scripts/release_impact.py check); `none` owes no card and the run prints `users: none` beside the stage 7 verdict. Absent means unknown and is treated as `external` (references/release-impact.md)."
|
|
320
|
+
},
|
|
313
321
|
"approval": {
|
|
314
322
|
"enum": [
|
|
315
323
|
"checks",
|
|
@@ -36,6 +36,7 @@ docs/
|
|
|
36
36
|
YYYY-MM-DD-<topic>-inventory.json # wave mode, stage 0 — every open item by id, verified
|
|
37
37
|
YYYY-MM-DD-<topic>-canvas.md # wave mode, stages 2–3 — the target state over the current logic
|
|
38
38
|
YYYY-MM-DD-<topic>-design.md # stage 3 — the spec / module dossier (locks shared contracts)
|
|
39
|
+
YYYY-MM-DD-<topic>-release-impact.md # stage 7 card (users, tracking, alerting, rollback); 8 adds its outcome
|
|
39
40
|
YYYY-MM-DD-<topic>-acceptance.md # stage 10 — REQ coverage table + evidence
|
|
40
41
|
plans/
|
|
41
42
|
YYYY-MM-DD-<topic>.md # stage 4 — the implementation plan
|
|
@@ -177,6 +178,7 @@ them is a finding, not a tie-break ([`knowledge-sources.md`](knowledge-sources.m
|
|
|
177
178
|
| 3 Spec | `specs/<topic>-design.md` — module dossier for a decomposed platform (+ links `docs/ux/*` for UI) | stage 4 |
|
|
178
179
|
| 4 Plan | `plans/<topic>.md` | stage 5 |
|
|
179
180
|
| 3 UX track | `docs/ux/{foundation,flows,screens,scenarios}.md` | stages 4–9 + `/ux-lint` |
|
|
181
|
+
| 7 Release impact | `specs/<topic>-release-impact.md`: the card owed before an outward act when the project has users ([`release-impact.md`](release-impact.md)) | stage 8 watches it and writes its `## Outcome`; stage 10 reads it |
|
|
180
182
|
| 8 Post-deploy | log/health notes (in the run, not a committed file) | stage 9 |
|
|
181
183
|
| 9 Docs+wiki | host module docs + wiki pages + the refreshed `graphify-out/graph.json` (`knowledge-graph.md`) | the **next** run's stage-0 harvest |
|
|
182
184
|
|
|
@@ -38,6 +38,7 @@ Custom profiles use their own declared stages and doctrine, with the kernel inta
|
|
|
38
38
|
| any stage · The canons, and where each is enforced | [`evidence-docs`](../../evidence-docs/SKILL.md) — the sibling skill in this plugin |
|
|
39
39
|
| 6–10 · Gates | `references/gates.md` |
|
|
40
40
|
| 7–8 · Deploy targets | `references/deploy-targets.md` |
|
|
41
|
+
| 7–8 · Release impact: who a release with users touches, its tracking, alerting and way back, the card owed before the outward act and the watch after it | `references/release-impact.md` |
|
|
41
42
|
| any stage · Hooks | `references/hooks.md` |
|
|
42
43
|
| 0 Knowledge harvest (pre-grill) | `references/knowledge-sources.md` |
|
|
43
44
|
| 0, 9 The code graph (graphify — recommended, never required) | `references/knowledge-graph.md` |
|
|
@@ -59,6 +59,7 @@ a row pointing outside the bundle is the defect this file exists to catch.
|
|
|
59
59
|
| The documentation system, the Doc Loop, supersede semantics | `references/documentation.md` |
|
|
60
60
|
| Gate types, the enforcement ladder, degrees of freedom, probing | `references/gates.md` |
|
|
61
61
|
| Deploy runbook template, per-platform verbs, the verification trio | `references/deploy-targets.md` |
|
|
62
|
+
| **Release impact**: who has users, the card owed before an outward act (affected, tracking, alerting, rollback), the stage 8 watch and its decision trigger | `references/release-impact.md`, `templates/release-impact.md`, `scripts/release_impact.py` |
|
|
62
63
|
| The Claude Code hook contract | `references/hooks.md` |
|
|
63
64
|
| First run in a project: greenfield and brownfield | `references/adoption.md` |
|
|
64
65
|
| The entry audit and what it inspects | `references/setup.md` |
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Release impact: who a release touches, how it is watched, and the way back
|
|
2
|
+
|
|
3
|
+
Load this at stage 7, before any outward act of a release (a tag, a publish, a deploy), and at
|
|
4
|
+
stage 8, while the release is watched. Its gate is `scripts/release_impact.py`; its card is
|
|
5
|
+
`templates/release-impact.md`. The rule is the operator's (2026-10-24): whenever a release is
|
|
6
|
+
decided in a project that has users, the release first makes sure the current users are not
|
|
7
|
+
harmed and says how they are affected; and there is always tracking that shows it goes as it
|
|
8
|
+
should, an alert when it does not, and a plan to roll back or ship an urgent fix.
|
|
9
|
+
|
|
10
|
+
## Contents
|
|
11
|
+
|
|
12
|
+
- Who has users
|
|
13
|
+
- When the card is owed
|
|
14
|
+
- The card
|
|
15
|
+
- What the checker refuses
|
|
16
|
+
- Stage 8: the watch and the decision
|
|
17
|
+
- Worked example: a skill or package release
|
|
18
|
+
- Worked example: a web or mobile product
|
|
19
|
+
- Companions and single homes
|
|
20
|
+
- Rationalizations
|
|
21
|
+
|
|
22
|
+
## Who has users
|
|
23
|
+
|
|
24
|
+
`pipeline.json` → `release.users` says it, once per project:
|
|
25
|
+
|
|
26
|
+
| Value | Means | The card |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `external` | people or systems outside the team install, call or open what ships | owed |
|
|
29
|
+
| `internal` | only the team or the operator's own agents use it | owed (the segments are small, the sections are the same) |
|
|
30
|
+
| `none` | nothing ships to anyone: a scratch repository, a spike, an archive | not owed; the run writes `users: none` beside the stage 7 verdict |
|
|
31
|
+
| absent | **unknown**, treated as `external` for safety | owed, and the run says the value was unknown, asks the operator once and records the answer |
|
|
32
|
+
|
|
33
|
+
An answer is a property of the project, not of one release. A release that ships nothing to
|
|
34
|
+
anyone in a project with users (a docs-only commit that is never tagged) is not a release here.
|
|
35
|
+
|
|
36
|
+
## When the card is owed
|
|
37
|
+
|
|
38
|
+
Stage 7, before the first outward act, whenever `release.users` is not `none`. The card lives
|
|
39
|
+
beside the run's other records: `<artifacts>/specs/YYYY-MM-DD-<topic>-release-impact.md`, seeded
|
|
40
|
+
from `templates/release-impact.md`, one per release (a release iteration writes one for the
|
|
41
|
+
release it cuts, not one per wave). The stage 7 gate is:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python3 scripts/release_impact.py check <card.md> --pipeline pipeline.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Exit 0 is PASS, 1 FAIL, 2 usage or an unreadable input, 3 NOT_RUN (no card where one is owed).
|
|
48
|
+
NOT_RUN stops the release exactly as FAIL does: it is never green.
|
|
49
|
+
|
|
50
|
+
## The card
|
|
51
|
+
|
|
52
|
+
Schema `release-impact/1`. A header (`schema`, `release`, `users`) and four sections, each
|
|
53
|
+
**answered** or `n/a: <reason>`. Every line the template marks `TODO` is replaced.
|
|
54
|
+
|
|
55
|
+
1. **Who is affected and how.** The user segments and a rough count of each; what changes for
|
|
56
|
+
them (behaviour, data, API or file format compatibility, migrations); whether anything breaks
|
|
57
|
+
for an existing user, and for whom; the rollout shape (all at once, a staged percentage, a
|
|
58
|
+
canary, a feature flag); the timing (when, and why then).
|
|
59
|
+
2. **Tracking.** The signals that show the release is going well (metrics, dashboards, logs,
|
|
60
|
+
error rates, install counts); their current **baseline**, so a change can be read as one;
|
|
61
|
+
where each is read (a URL, a command, a query); and the **watch window**, how long after the
|
|
62
|
+
release the signals are watched.
|
|
63
|
+
3. **Alerting.** What fires when it goes wrong (the threshold or condition), who receives it and
|
|
64
|
+
where; and whether that alert has **ever been seen firing**. An alert nobody has seen fire is
|
|
65
|
+
written as `never`: it is a hypothesis about an alert, and the card says so rather than
|
|
66
|
+
letting it read as a tested one.
|
|
67
|
+
4. **Rollback or hotfix.** The exact rollback procedure and how long it takes; whether data
|
|
68
|
+
changes are reversible (a backup, a down migration) or not, said plainly; any kill switch or
|
|
69
|
+
flag; the hotfix path; and the **decision trigger**: `roll back if <signal crosses X> within
|
|
70
|
+
<Y>`. The project's runbook owns the procedure itself
|
|
71
|
+
([`deploy-targets.md`](deploy-targets.md) → *Runbook template*, its `Rollback` section):
|
|
72
|
+
the card links it and adds what is specific to this release, never a second copy.
|
|
73
|
+
|
|
74
|
+
## What the checker refuses
|
|
75
|
+
|
|
76
|
+
- No card where one is owed: NOT_RUN. A card for `users: none` is checked if it exists.
|
|
77
|
+
- A header without `schema: release-impact/1`, or a `users:` value outside the three, or a card
|
|
78
|
+
whose `users:` disagrees with `pipeline.json` (or `--users`).
|
|
79
|
+
- A missing section, a section that appears twice, or an empty one (an HTML comment is empty).
|
|
80
|
+
- `n/a` with no reason, in a section or on any field line: `n/a: <reason>` or nothing.
|
|
81
|
+
- A `TODO` left anywhere in the card.
|
|
82
|
+
- Section 2 without a `Watch window:` value. Tracking cannot be `n/a` on a release with users.
|
|
83
|
+
- Section 3 without a `Seen firing:` line, or answered `n/a`: something must say it went wrong.
|
|
84
|
+
- Section 4 without a `Decision trigger:` that states its condition (`if` or `when`), or
|
|
85
|
+
answered `n/a`: there is always a way back, even when the way back is a hotfix.
|
|
86
|
+
- At stage 8 (`--outcome`): no `## Outcome` section, or a `Result:` that is not one of `held`,
|
|
87
|
+
`rolled back`, `hotfixed`, `watching`.
|
|
88
|
+
|
|
89
|
+
## Stage 8: the watch and the decision
|
|
90
|
+
|
|
91
|
+
Stage 8 reads the card's signals for its watch window and records what it read in the card's
|
|
92
|
+
`## Outcome` section: the signals with their values against the baseline, whether any alert
|
|
93
|
+
fired, and a `Result:`. When the decision trigger fires, the run rolls back or ships the hotfix
|
|
94
|
+
**per the card**, without a new debate: the trigger was agreed before the release precisely so
|
|
95
|
+
that the moment it fires is not the moment it is argued about. A rollback is itself outward and
|
|
96
|
+
is verified like a deploy ([`deploy-targets.md`](deploy-targets.md) → *The verification trio*).
|
|
97
|
+
|
|
98
|
+
A watch window longer than the session is a wait: `Result: watching`, the window's end, and the
|
|
99
|
+
watcher that reads the signals then (a scheduled job, a monitor, a named person). With no watcher
|
|
100
|
+
available the card says so; it never claims a watch that nobody runs.
|
|
101
|
+
`python3 scripts/release_impact.py check <card.md> --outcome` gates the record.
|
|
102
|
+
|
|
103
|
+
## Worked example: a skill or package release
|
|
104
|
+
|
|
105
|
+
The users are installers. Section 1 names them (`npm` weekly downloads, plugin marketplace
|
|
106
|
+
installs, the operator's own machines) and what changes for them (a renamed command, a new
|
|
107
|
+
required field, a dropped Node version); the rollout is all at once on the tag. Section 2 tracks
|
|
108
|
+
the registry readback (`npm view <pkg> version` equals the tag), a clean-directory install, the
|
|
109
|
+
installed version read back on the operator's machine, and new issues; baseline: open issues
|
|
110
|
+
today; watch window: 72 h. Section 3: a failed install in the release workflow's smoke test and a
|
|
111
|
+
new issue on the repository, received by the maintainer by mail; seen firing: yes, the smoke test
|
|
112
|
+
failed a release once. Section 4: re-pin the previous version in every consumer, `npm deprecate
|
|
113
|
+
<pkg>@<bad>` with a message pointing at the fixed one, then a patch release; minutes; nothing to
|
|
114
|
+
reverse in data; decision trigger: roll back if an install fails or a breaking issue is filed
|
|
115
|
+
within 72 h.
|
|
116
|
+
|
|
117
|
+
## Worked example: a web or mobile product
|
|
118
|
+
|
|
119
|
+
Section 1: signed-in customers on the web app (about 12,000 weekly), checkout changes to a new
|
|
120
|
+
payment form; existing saved cards keep working, a stored draft order is migrated; rollout behind
|
|
121
|
+
a flag at 5%, then 50%, then 100% over three days, starting on a Tuesday morning. Section 2:
|
|
122
|
+
checkout conversion, payment error rate and p95 latency on the product dashboard, baselines from
|
|
123
|
+
the last 14 days; the error tracker's release view; watch window: 24 h per step. Section 3: the
|
|
124
|
+
error tracker alerts on a new issue class or an error rate over twice the baseline for 10
|
|
125
|
+
minutes, to the on-call channel; the payment error alert has fired before, the latency one never
|
|
126
|
+
has. Section 4: switch the flag off (seconds); the migration has a down step tested on a staging
|
|
127
|
+
copy; the hotfix path is a patch through the normal pipeline with the flag held at its last good
|
|
128
|
+
step; decision trigger: roll back if payment errors exceed 2% or conversion falls 10% below
|
|
129
|
+
baseline within any step's window. A mobile release adds the store's staged rollout percentage,
|
|
130
|
+
the halt button as the kill switch, and the fact that a shipped binary cannot be recalled: its
|
|
131
|
+
way back is a hotfix build, so its section 4 names the server-side flag that disables the
|
|
132
|
+
feature for builds already installed.
|
|
133
|
+
|
|
134
|
+
## Companions and single homes
|
|
135
|
+
|
|
136
|
+
- Wiring the alerts in section 3 for a service: sheleg-dev's `error-tracking` skill (releases
|
|
137
|
+
tied to commits, the release view the watch reads). Recommended, never required; absent, the
|
|
138
|
+
card names what fires on the project's own tooling.
|
|
139
|
+
- The rollback procedure: the project's runbook ([`deploy-targets.md`](deploy-targets.md) →
|
|
140
|
+
*If there is no runbook, write one first*). No runbook means stage 7 writes one first, and the
|
|
141
|
+
card points at its rollback.
|
|
142
|
+
- The release's authority and approval mode: [`deploy-targets.md`](deploy-targets.md) → *When
|
|
143
|
+
the release waits for an approval*. The card does not authorise anything; it is owed before
|
|
144
|
+
the act the authority allows.
|
|
145
|
+
|
|
146
|
+
## Rationalizations
|
|
147
|
+
|
|
148
|
+
| The excuse | Why it fails |
|
|
149
|
+
|---|---|
|
|
150
|
+
| "It is a small release, nobody is affected." | Then section 1 says so in one line, with the reason. The card is short when the release is; it is never absent. |
|
|
151
|
+
| "We will notice if it breaks." | Noticing is section 3. If nothing is named that fires, nobody notices until a user writes. |
|
|
152
|
+
| "Rollback is obvious." | Obvious to the person who shipped it, at the moment they shipped it. The trigger and the procedure are written before the release so the person at the incident does not reconstruct them. |
|
|
153
|
+
| "The alert exists, it just never fired." | Then `Seen firing: never`. An alert nobody has seen fire is untested, and the card says that instead of implying the opposite. |
|
|
154
|
+
| "We do not know if there are users." | Unknown is treated as `external`. Ask once and record the answer in `pipeline.json`. |
|
|
@@ -13,3 +13,9 @@ The full gate and freedom level remain in [stages.md](stages.md).
|
|
|
13
13
|
from inference about the project is one nobody can repeat or roll back, and the
|
|
14
14
|
operator is standing at this gate anyway. That reference also carries the CLI
|
|
15
15
|
verbs per target for when the runbook names a platform you have to recall.
|
|
16
|
+
- **Before the tag, the publish or the deploy, write the release impact card** when the
|
|
17
|
+
project has users (`pipeline.json` → `release.users` is not `none`; absent counts as
|
|
18
|
+
`external`): seed `templates/release-impact.md` to
|
|
19
|
+
`<artifacts>/specs/YYYY-MM-DD-<topic>-release-impact.md`, answer its four sections, link
|
|
20
|
+
the runbook's rollback rather than copy it, and run `python3 scripts/release_impact.py
|
|
21
|
+
check <card.md> --pipeline pipeline.json` to exit 0 ([`release-impact.md`](release-impact.md)).
|
|
@@ -28,3 +28,9 @@ The full gate and freedom level remain in [stages.md](stages.md).
|
|
|
28
28
|
search and answer-engine visibility with `seo-aeo-audit`
|
|
29
29
|
([`companion-skills.md`](companion-skills.md)) — recommended, never a gate;
|
|
30
30
|
absent, visibility ships unaudited and the close-out says so.
|
|
31
|
+
- **Watch the release impact card** ([`release-impact.md`](release-impact.md) → *Stage 8:
|
|
32
|
+
the watch and the decision*): read each signal against its baseline for the card's watch
|
|
33
|
+
window, write the card's `## Outcome`, and run `release_impact.py check <card.md>
|
|
34
|
+
--outcome`. A decision trigger that fires is acted on per the card, not debated; a window
|
|
35
|
+
that outlives the session is `Result: watching` with the watcher named, or the absence of
|
|
36
|
+
one said.
|
|
@@ -337,6 +337,14 @@ never that the work was skipped quietly.
|
|
|
337
337
|
tagging, the CI verdict for what was just pushed is READ, not assumed**
|
|
338
338
|
([`conventions.md`](conventions.md) → *The CI verdict*) — a tag on a commit whose
|
|
339
339
|
run nobody read is how a red `main` ships.
|
|
340
|
+
- **A release with users owes its impact card before the outward act** (since the
|
|
341
|
+
operator's rule of 2026-10-24): who is affected and how, the tracking with its watch
|
|
342
|
+
window, the alerting and whether it was ever seen firing, the rollback or hotfix with its
|
|
343
|
+
decision trigger. `python3 scripts/release_impact.py check <card.md> --pipeline
|
|
344
|
+
pipeline.json` exits 0 before the tag, the publish or the deploy; NOT_RUN (no card) stops
|
|
345
|
+
the release like FAIL. `release.users: none` owes no card and prints `users: none` beside
|
|
346
|
+
this verdict; absent is unknown and treated as `external`
|
|
347
|
+
([`release-impact.md`](release-impact.md)).
|
|
340
348
|
- **The independent reader is dispatched by this stage, and read by its output**
|
|
341
349
|
([`review.md`](review.md) → *The independent reader*). On any change that adds or
|
|
342
350
|
widens a check, the run dispatches a reader — a subagent it can watch, a bot whose
|
|
@@ -364,6 +372,11 @@ never that the work was skipped quietly.
|
|
|
364
372
|
steps — never silent success. **The CI verdict is one of the reported facts, with
|
|
365
373
|
its run id** — "CI is green" written without a command behind it prints the same
|
|
366
374
|
whether it looked or not ([`gates.md`](gates.md) → *False success*).
|
|
375
|
+
- **Watch the release impact card's signals for its window** and record what they read in
|
|
376
|
+
the card's `## Outcome` (`Result:` held, rolled back, hotfixed, or watching with its
|
|
377
|
+
watcher); `release_impact.py check <card.md> --outcome` exits 0. When the card's decision
|
|
378
|
+
trigger fires, roll back or ship the hotfix **per the card**, then verify that act as a
|
|
379
|
+
deploy ([`release-impact.md`](release-impact.md) → *Stage 8: the watch and the decision*).
|
|
367
380
|
- **Write the verification row.** One line per REQ this run shipped, into
|
|
368
381
|
`<artifacts>/verification.md` ([`verification.md`](verification.md)) — the run,
|
|
369
382
|
the tag or commit it went out in, what the gate said, and `Human: never` unless the
|
|
@@ -1597,7 +1597,7 @@ def cmd_producer(graph, args):
|
|
|
1597
1597
|
def cmd_doctrine(graph, args):
|
|
1598
1598
|
"""Which doctrine this run actually read — B-061.
|
|
1599
1599
|
|
|
1600
|
-
The bundle is
|
|
1600
|
+
The bundle is 55 reference files. A run reads some subset and nothing recorded which,
|
|
1601
1601
|
so **a skipped file and a read one were indistinguishable** — the class every guard in
|
|
1602
1602
|
this repository exists to catch, left standing over the doctrine itself.
|
|
1603
1603
|
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The release impact card: who a release touches, how it is watched, and the way back, as a
|
|
3
|
+
command with an exit code. Stdlib only.
|
|
4
|
+
|
|
5
|
+
release_impact.py check <card.md> [--users external|internal|none]
|
|
6
|
+
[--pipeline pipeline.json] [--outcome] [--json]
|
|
7
|
+
stage 7, before any outward act: the card (`release-impact/1`) answers its four
|
|
8
|
+
sections (who is affected and how, tracking, alerting, rollback or hotfix), each
|
|
9
|
+
answered or `n/a: <reason>`. Tracking names its watch window, alerting says whether it
|
|
10
|
+
was ever seen firing, and rollback states its decision trigger; none of the three is
|
|
11
|
+
`n/a` on a release with users. `--outcome` is stage 8: the card also carries its
|
|
12
|
+
`## Outcome` with a `Result:` of held, rolled back, hotfixed or watching.
|
|
13
|
+
|
|
14
|
+
Who has users: `--users`, else `pipeline.json` → `release.users`, else the card's own `users:`
|
|
15
|
+
line. Absent everywhere means unknown, treated as `external` and said so. On `none` the card
|
|
16
|
+
is not owed; a card that exists is still checked.
|
|
17
|
+
|
|
18
|
+
**A check that could not run says NOT_RUN, and NOT_RUN is never PASS.** No card where one is
|
|
19
|
+
owed is NOT_RUN.
|
|
20
|
+
|
|
21
|
+
Exit codes: 0 PASS · 1 FAIL · 2 usage, or an input that cannot be read · 3 NOT_RUN.
|
|
22
|
+
The doctrine is `references/release-impact.md`.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import re
|
|
30
|
+
import sys
|
|
31
|
+
|
|
32
|
+
SCHEMA = "release-impact/1"
|
|
33
|
+
USERS = ("external", "internal", "none")
|
|
34
|
+
RESULTS = ("held", "rolled back", "hotfixed", "watching")
|
|
35
|
+
# The four sections, in order, by the words their heading opens with (after an optional number).
|
|
36
|
+
SECTIONS = (
|
|
37
|
+
("who", "who is affected"),
|
|
38
|
+
("tracking", "tracking"),
|
|
39
|
+
("alerting", "alerting"),
|
|
40
|
+
("rollback", "rollback"),
|
|
41
|
+
)
|
|
42
|
+
TITLES = {"who": "1. Who is affected and how", "tracking": "2. Tracking",
|
|
43
|
+
"alerting": "3. Alerting", "rollback": "4. Rollback or hotfix"}
|
|
44
|
+
# Sections the operator's rule never lets be n/a on a release with users.
|
|
45
|
+
NEVER_NA = ("tracking", "alerting", "rollback")
|
|
46
|
+
COMMENT = re.compile(r"<!--.*?-->", re.S)
|
|
47
|
+
HEADING = re.compile(r"^##\s+(.+?)\s*$")
|
|
48
|
+
HEADER = re.compile(r"^(schema|release|users|date)\s*:\s*(.*?)\s*$", re.I)
|
|
49
|
+
# A field line: `- **Label:** value`, `**Label**: value` or `Label: value`.
|
|
50
|
+
FIELD = re.compile(r"^\s*(?:[-*]\s+)?(?:\*\*([^*:]+?)\s*:?\s*\*\*\s*:?|([A-Za-z][A-Za-z /]*?)\s*:)\s*(.*?)\s*$")
|
|
51
|
+
NA = re.compile(r"^n/?a\b\.?\s*[:;,(-]?\s*(.*?)\)?\s*$", re.I)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _usage(msg):
|
|
55
|
+
print("release_impact: %s" % msg, file=sys.stderr)
|
|
56
|
+
return 2
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def read_text(path):
|
|
60
|
+
with open(path, encoding="utf-8") as fh:
|
|
61
|
+
return fh.read()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _is_na(value):
|
|
65
|
+
"""(True, reason) when the value is an n/a answer; (False, None) otherwise. A reason is a
|
|
66
|
+
word of three letters or more, so `n/a`, `n/a:` and `n/a: -` carry none."""
|
|
67
|
+
m = NA.match(value.strip())
|
|
68
|
+
if not m:
|
|
69
|
+
return False, None
|
|
70
|
+
reason = m.group(1).strip()
|
|
71
|
+
return True, (reason if re.search(r"[^\W\d_]{3,}", reason) else "")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _section_key(title):
|
|
75
|
+
t = re.sub(r"^\d+[.)]?\s*", "", title.strip()).lower()
|
|
76
|
+
for key, opener in SECTIONS:
|
|
77
|
+
if t.startswith(opener):
|
|
78
|
+
return key
|
|
79
|
+
if t.startswith("outcome"):
|
|
80
|
+
return "outcome"
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def parse(text):
|
|
85
|
+
"""{'header': {...}, 'sections': {key: [body lines]}, 'duplicates': [...], 'todo': [...]}.
|
|
86
|
+
HTML comments are removed first: guidance in a comment is never an answer."""
|
|
87
|
+
text = COMMENT.sub("", text)
|
|
88
|
+
header, sections, dups, todo = {}, {}, [], []
|
|
89
|
+
cur, infence = None, False
|
|
90
|
+
for n, line in enumerate(text.splitlines(), 1):
|
|
91
|
+
if re.match(r"^\s*(```|~~~)", line):
|
|
92
|
+
infence = not infence
|
|
93
|
+
if "TODO" in line:
|
|
94
|
+
todo.append(n)
|
|
95
|
+
m = None if infence else HEADING.match(line)
|
|
96
|
+
if m:
|
|
97
|
+
cur = _section_key(m.group(1))
|
|
98
|
+
if cur is not None:
|
|
99
|
+
if cur in sections:
|
|
100
|
+
dups.append(cur)
|
|
101
|
+
sections[cur] = []
|
|
102
|
+
continue
|
|
103
|
+
if cur is None:
|
|
104
|
+
h = HEADER.match(line)
|
|
105
|
+
if h and h.group(1).lower() not in header:
|
|
106
|
+
header[h.group(1).lower()] = h.group(2)
|
|
107
|
+
continue
|
|
108
|
+
sections[cur].append(line)
|
|
109
|
+
return {"header": header, "sections": sections, "duplicates": dups, "todo": todo}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def fields(lines):
|
|
113
|
+
"""[(label lowercased, value)] for every field-shaped line of a section body."""
|
|
114
|
+
out = []
|
|
115
|
+
for line in lines:
|
|
116
|
+
m = FIELD.match(line)
|
|
117
|
+
if m:
|
|
118
|
+
out.append(((m.group(1) or m.group(2)).strip().lower(), m.group(3).strip()))
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def field(lines, label):
|
|
123
|
+
for k, v in fields(lines):
|
|
124
|
+
if k == label:
|
|
125
|
+
return v
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def section_state(lines):
|
|
130
|
+
"""'empty', ('na', reason) or 'answered'."""
|
|
131
|
+
body = [l.strip() for l in lines if l.strip()]
|
|
132
|
+
if not body:
|
|
133
|
+
return "empty", None
|
|
134
|
+
na, reason = _is_na(re.sub(r"^[-*]\s+", "", body[0]))
|
|
135
|
+
if na and len(body) == 1:
|
|
136
|
+
return "na", reason
|
|
137
|
+
return "answered", None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def check(text, users=None, outcome=False):
|
|
141
|
+
"""(verdict, problems, notes, states, users). `users` is the project's value, or None."""
|
|
142
|
+
doc = parse(text)
|
|
143
|
+
problems, notes = [], []
|
|
144
|
+
head = doc["header"]
|
|
145
|
+
if head.get("schema", "").strip() != SCHEMA:
|
|
146
|
+
problems.append("the card is not %s: its header has no `schema: %s` line" % (SCHEMA, SCHEMA))
|
|
147
|
+
card_users = head.get("users", "").strip().lower() or None
|
|
148
|
+
if card_users and card_users not in USERS:
|
|
149
|
+
problems.append("users is one of %s, not %r" % (", ".join(USERS), card_users))
|
|
150
|
+
card_users = None
|
|
151
|
+
if users and card_users and users != card_users:
|
|
152
|
+
problems.append("the card says users: %s and the project says %s; one of them is wrong"
|
|
153
|
+
% (card_users, users))
|
|
154
|
+
if not users:
|
|
155
|
+
users = card_users
|
|
156
|
+
if not users:
|
|
157
|
+
users = "external"
|
|
158
|
+
notes.append("who has users is unknown (no --users, no release.users, no users: line): "
|
|
159
|
+
"treated as external; record the answer in pipeline.json")
|
|
160
|
+
for d in sorted(set(doc["duplicates"])):
|
|
161
|
+
problems.append("section %r appears twice; the card holds one of each" % TITLES.get(d, d))
|
|
162
|
+
if doc["todo"]:
|
|
163
|
+
problems.append("TODO left on line(s) %s: every placeholder is replaced"
|
|
164
|
+
% ", ".join(map(str, doc["todo"][:10])))
|
|
165
|
+
states = {}
|
|
166
|
+
secs = doc["sections"]
|
|
167
|
+
for key, _ in SECTIONS:
|
|
168
|
+
title = TITLES[key]
|
|
169
|
+
if key not in secs:
|
|
170
|
+
states[key] = "missing"
|
|
171
|
+
problems.append("no section %r" % title)
|
|
172
|
+
continue
|
|
173
|
+
st, reason = section_state(secs[key])
|
|
174
|
+
states[key] = st
|
|
175
|
+
if st == "empty":
|
|
176
|
+
problems.append("section %r is empty: answer it, or `n/a: <reason>`" % title)
|
|
177
|
+
elif st == "na":
|
|
178
|
+
if not reason:
|
|
179
|
+
problems.append("section %r is n/a with no reason: `n/a: <reason>`" % title)
|
|
180
|
+
elif key in NEVER_NA and users != "none":
|
|
181
|
+
problems.append("section %r cannot be n/a on a release with users: there is always "
|
|
182
|
+
"tracking, an alert and a way back" % title)
|
|
183
|
+
else:
|
|
184
|
+
notes.append("%s: n/a: %s" % (title, reason))
|
|
185
|
+
# n/a on any field line carries its reason too.
|
|
186
|
+
for label, value in fields(secs[key]):
|
|
187
|
+
na, why = _is_na(value) if value else (False, None)
|
|
188
|
+
if na and not why:
|
|
189
|
+
problems.append("%s: `%s` is n/a with no reason: `n/a: <reason>`" % (title, label))
|
|
190
|
+
if states.get("tracking") == "answered":
|
|
191
|
+
w = field(secs["tracking"], "watch window")
|
|
192
|
+
if not w or _is_na(w)[0]:
|
|
193
|
+
problems.append("section %r names no watch window (`Watch window: <how long>`)"
|
|
194
|
+
% TITLES["tracking"])
|
|
195
|
+
if states.get("alerting") == "answered":
|
|
196
|
+
s = field(secs["alerting"], "seen firing")
|
|
197
|
+
if not s:
|
|
198
|
+
problems.append("section %r does not say whether the alert was ever seen firing "
|
|
199
|
+
"(`Seen firing: yes (when)` or `never`)" % TITLES["alerting"])
|
|
200
|
+
elif re.match(r"^never\b", s, re.I):
|
|
201
|
+
notes.append("the alert has never been seen firing: it is untested")
|
|
202
|
+
if states.get("rollback") == "answered":
|
|
203
|
+
t = field(secs["rollback"], "decision trigger")
|
|
204
|
+
if not t or _is_na(t)[0] or not re.search(r"\b(if|when|если|когда)\b", t, re.I):
|
|
205
|
+
problems.append("section %r states no decision trigger (`Decision trigger: roll back "
|
|
206
|
+
"if <X> within <Y>`)" % TITLES["rollback"])
|
|
207
|
+
if outcome:
|
|
208
|
+
if "outcome" not in secs:
|
|
209
|
+
problems.append("stage 8: no `## Outcome` section recording what the watch read")
|
|
210
|
+
else:
|
|
211
|
+
r = (field(secs["outcome"], "result") or "").strip().lower()
|
|
212
|
+
if not any(r.startswith(x) for x in RESULTS):
|
|
213
|
+
problems.append("stage 8: `Result:` is one of %s, not %r" % (", ".join(RESULTS), r))
|
|
214
|
+
elif r.startswith("watching"):
|
|
215
|
+
notes.append("the watch window is still open: %s" % r)
|
|
216
|
+
return ("FAIL" if problems else "PASS"), problems, notes, states, users
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def pipeline_users(path):
|
|
220
|
+
"""release.users from pipeline.json; None when absent. Raises ValueError on bad input."""
|
|
221
|
+
cfg = json.loads(read_text(path))
|
|
222
|
+
rel = cfg.get("release") if isinstance(cfg, dict) else None
|
|
223
|
+
u = rel.get("users") if isinstance(rel, dict) else None
|
|
224
|
+
if u is not None and u not in USERS:
|
|
225
|
+
raise ValueError("release.users is one of %s, not %r" % (", ".join(USERS), u))
|
|
226
|
+
return u
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def emit(verdict, problems, notes, as_json, extra=None):
|
|
230
|
+
if as_json:
|
|
231
|
+
print(json.dumps(dict(verdict=verdict, problems=problems, notes=notes, **(extra or {})), indent=2))
|
|
232
|
+
else:
|
|
233
|
+
print("release_impact: %s" % verdict)
|
|
234
|
+
for p in problems:
|
|
235
|
+
print(" - %s" % p)
|
|
236
|
+
for n in notes:
|
|
237
|
+
print(" note: %s" % n)
|
|
238
|
+
return {"PASS": 0, "FAIL": 1, "NOT_RUN": 3}[verdict]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def main(argv=None):
|
|
242
|
+
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
|
243
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
244
|
+
p = sub.add_parser("check")
|
|
245
|
+
p.add_argument("path")
|
|
246
|
+
p.add_argument("--users", choices=USERS)
|
|
247
|
+
p.add_argument("--pipeline", help="read release.users from this pipeline.json (--users wins)")
|
|
248
|
+
p.add_argument("--outcome", action="store_true", help="stage 8: the card carries its Outcome")
|
|
249
|
+
p.add_argument("--json", action="store_true")
|
|
250
|
+
try:
|
|
251
|
+
a = ap.parse_args(argv)
|
|
252
|
+
except SystemExit as exc:
|
|
253
|
+
return 2 if exc.code else 0
|
|
254
|
+
users = a.users
|
|
255
|
+
if not users and a.pipeline:
|
|
256
|
+
try:
|
|
257
|
+
users = pipeline_users(a.pipeline)
|
|
258
|
+
except OSError as exc:
|
|
259
|
+
return _usage("cannot read %s (%s)" % (a.pipeline, exc.strerror or exc))
|
|
260
|
+
except ValueError as exc:
|
|
261
|
+
return _usage("%s: %s" % (a.pipeline, exc))
|
|
262
|
+
if not os.path.exists(a.path):
|
|
263
|
+
if users == "none":
|
|
264
|
+
return emit("PASS", [], ["users: none: the card is not owed; write `users: none` "
|
|
265
|
+
"beside the stage 7 verdict"], a.json, {"users": "none"})
|
|
266
|
+
why = "" if users else " (who has users is unknown: treated as external)"
|
|
267
|
+
return emit("NOT_RUN", ["no release impact card at %s; one is owed before the outward "
|
|
268
|
+
"act%s" % (a.path, why)], [], a.json, {"users": users or "external"})
|
|
269
|
+
try:
|
|
270
|
+
text = read_text(a.path)
|
|
271
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
272
|
+
return _usage("cannot read %s (%s)" % (a.path, getattr(exc, "strerror", None) or exc))
|
|
273
|
+
verdict, probs, notes, states, resolved = check(text, users, a.outcome)
|
|
274
|
+
return emit(verdict, probs, notes, a.json, {"users": resolved, "sections": states})
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
sys.exit(main())
|
|
@@ -27,6 +27,7 @@ from `super-ux`.
|
|
|
27
27
|
| `hooks.example.json` | the project's `.claude/settings.json` | 0 — offered, never installed silently |
|
|
28
28
|
| `browser-claims.json` | stage 6, beside the look — one row per browser claim | REQ/scenario id + state + kind (look/suite/library); validated by `test/browser_claims_test.py`, stdlib only |
|
|
29
29
|
| `finding-evidence.json` | one record per audit finding, wherever the finding lives | the five-axes evidence schema: mechanism / reproduction / exposure / incidence / impact uncertainty, observations apart from assumptions |
|
|
30
|
+
| `release-impact.md` | `docs/evidence/specs/YYYY-MM-DD-<topic>-release-impact.md`, one per release | 7 writes it before any outward act when the project has users, checked by `scripts/release_impact.py check`; 8 adds its `## Outcome` (`--outcome`) |
|
|
30
31
|
| `routing-rule.md` | the operator's `CLAUDE.md` — **offered by `setup`, never written silently** | 0 / `setup` |
|
|
31
32
|
| `retro.md` | `docs/evidence/retro.md` — **one per project, not per run** | 10 writes (stamp → prune → entry), 0 reads it in full |
|
|
32
33
|
| `retro-archive.md` | `docs/evidence/retro/YYYY-QN.md` | 10 rotates into it, 0 **queries** it |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Release impact: TODO release name or version
|
|
2
|
+
|
|
3
|
+
schema: release-impact/1
|
|
4
|
+
release: TODO the tag or version this card is for
|
|
5
|
+
users: TODO external | internal | none (the value in pipeline.json, release.users)
|
|
6
|
+
date: TODO YYYY-MM-DD
|
|
7
|
+
|
|
8
|
+
<!-- Seeded to <artifacts>/specs/YYYY-MM-DD-<topic>-release-impact.md at stage 7, before the
|
|
9
|
+
first outward act. Replace every TODO. A section or a field that does not apply says
|
|
10
|
+
`n/a: <reason>`; a bare `n/a` is refused. Gate:
|
|
11
|
+
python3 scripts/release_impact.py check <this file> --pipeline pipeline.json
|
|
12
|
+
The doctrine is references/release-impact.md in the task-pipeline skill. -->
|
|
13
|
+
|
|
14
|
+
## 1. Who is affected and how
|
|
15
|
+
|
|
16
|
+
- **Segments:** TODO who uses what ships, with a rough count of each
|
|
17
|
+
- **What changes:** TODO behaviour, data, API or file format compatibility, migrations
|
|
18
|
+
- **Breaks for existing users:** TODO no, or what breaks and for whom
|
|
19
|
+
- **Rollout:** TODO all at once, staged percentage, canary or feature flag
|
|
20
|
+
- **Timing:** TODO when it goes out, and why then
|
|
21
|
+
|
|
22
|
+
## 2. Tracking
|
|
23
|
+
|
|
24
|
+
- **Signals:** TODO the metrics, dashboards, logs or error rates that show it going well
|
|
25
|
+
- **Baseline:** TODO each signal's value now
|
|
26
|
+
- **Where:** TODO the URL, command or query that reads each signal
|
|
27
|
+
- **Watch window:** TODO how long after the release the signals are watched
|
|
28
|
+
|
|
29
|
+
## 3. Alerting
|
|
30
|
+
|
|
31
|
+
- **Fires when:** TODO the threshold or condition
|
|
32
|
+
- **Receives:** TODO who gets it, and where
|
|
33
|
+
- **Seen firing:** TODO yes (when), or never
|
|
34
|
+
|
|
35
|
+
## 4. Rollback or hotfix
|
|
36
|
+
|
|
37
|
+
- **Rollback:** TODO the exact procedure, or a link to the runbook's rollback section
|
|
38
|
+
- **Time to roll back:** TODO minutes or hours
|
|
39
|
+
- **Data reversible:** TODO yes (backup, down migration), or no and what is lost
|
|
40
|
+
- **Kill switch:** TODO the flag or switch, or n/a: <reason>
|
|
41
|
+
- **Hotfix path:** TODO how an urgent fix ships
|
|
42
|
+
- **Decision trigger:** TODO roll back if <signal crosses X> within <Y>
|
|
43
|
+
|
|
44
|
+
<!-- Stage 8 adds the section below once the watch window is read; check it with --outcome.
|
|
45
|
+
|
|
46
|
+
## Outcome
|
|
47
|
+
|
|
48
|
+
- **Read:** the signals against their baselines, and any alert that fired
|
|
49
|
+
- **Result:** held | rolled back | hotfixed | watching (until <when>, watcher <who or what>)
|
|
50
|
+
-->
|
|
@@ -20,7 +20,7 @@ Run: `<topic>` · started `<YYYY-MM-DD>` · module map: `<path or "none">`
|
|
|
20
20
|
|
|
21
21
|
## `read:` — which doctrine this run actually opened
|
|
22
22
|
|
|
23
|
-
The bundle is
|
|
23
|
+
The bundle is 55 reference files and nothing recorded which of them a run read, so **a
|
|
24
24
|
skipped file and a read one were indistinguishable** — the class every guard in this
|
|
25
25
|
pipeline exists to catch, left standing over the doctrine itself.
|
|
26
26
|
|
|
@@ -48,7 +48,7 @@ same claim one level down.
|
|
|
48
48
|
|---|---|---|
|
|
49
49
|
| `unmeasured — no run ledger` | there is no ledger | nothing to read from |
|
|
50
50
|
| `unmeasured — the ledger carries no read: lines` | the hook is absent, **or** the run opened no doctrine | two opposite facts, and the ledger cannot separate them, so neither is claimed |
|
|
51
|
-
| `N of
|
|
51
|
+
| `N of 55 reference files read — unattested`, then each unread one | the hook is installed and fired | the count alone says there is a gap, not where — and `unattested` says the ledger cannot name who wrote the lines |
|
|
52
52
|
|
|
53
53
|
**It is a disclosure: no floor, no direction, never a target.** A run that needs four files
|
|
54
54
|
and reads four is not worse than one that reads thirty — and the moment the number becomes
|