task-pipeline-skill 1.100.0 → 1.101.0
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 +29 -0
- package/SKILL-CARD.md +1 -1
- package/package.json +1 -1
- package/plugins/task-pipeline/.claude-plugin/plugin.json +1 -1
- package/plugins/task-pipeline/agents/design-tester.md +16 -3
- package/plugins/task-pipeline/skills/task-pipeline/references/design-review.md +15 -3
- package/plugins/task-pipeline/skills/task-pipeline/references/retrospective.md +5 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +3 -1
- package/plugins/task-pipeline/skills/task-pipeline/scripts/design_gate.py +66 -4
- package/plugins/task-pipeline/skills/task-pipeline/scripts/retro_budget.py +46 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
## v1.101.0 (2026-10-22)
|
|
2
|
+
|
|
3
|
+
Guards: 530 → **530**; property checks 15 → **15**. No negative was added: the new checks are
|
|
4
|
+
sixteen cases in `test/design_gate_test.py` (75 to 91) and two in
|
|
5
|
+
`test/audit_regressions/diet_retro_budget.py`.
|
|
6
|
+
|
|
7
|
+
**Cancel parity and one clear offer on every subscription surface** (hub milestone M2, G8 and
|
|
8
|
+
R2, from GP Bullhound's *Consumer Subscription Software*, October 2025, p. 18).
|
|
9
|
+
|
|
10
|
+
- **`design_gate.py report --subscription`** (or a `subscription` block in the report): the
|
|
11
|
+
surface sells, renews or cancels a subscription, so the report counts the steps to subscribe
|
|
12
|
+
and to cancel. Cancelling in more steps fails, a `pass` over a longer cancel path is refused, a
|
|
13
|
+
paywall without one clear offer fails, a check not walked is NOT_RUN with its reason, and a
|
|
14
|
+
missing pause is reported, not gated. `agents/design-tester.md` step 7 walks it;
|
|
15
|
+
`references/design-review.md` and the stage 6 gate in `references/stages.md` state it.
|
|
16
|
+
- **The panel counts locals and tourists** (M2 R3): `design_gate.py panel` requires each
|
|
17
|
+
retention line to open with `local` or `tourist` and at least two of each, the rule super-ux
|
|
18
|
+
0.65.0's test panel template states.
|
|
19
|
+
- **One rotation line per archive** (B-151, found as sshlg-skills B-246): `retro_budget.py
|
|
20
|
+
migrate` re-archives a quarter's earlier rotated stamps with the new ones as one block and
|
|
21
|
+
keeps one `Rotated out:` line per archive, so a floor that rotates every run no longer grows by
|
|
22
|
+
a line per run. A line whose block no longer hashes stays as its own line.
|
|
23
|
+
- **Board:** B-044 collapsed into B-040 (the same finding); B-150 files the two fw07 branches
|
|
24
|
+
kept on purpose.
|
|
25
|
+
- **A test that raced the clock:** `test/lessons_test.py` H1 waited a fixed 1.5 s for a rebuild
|
|
26
|
+
to reach its read; under a load average near 300 the add ran first and the tagged gate failed
|
|
27
|
+
on negative 502. The rebuild's read now blocks on a handshake until the add has landed (431
|
|
28
|
+
cases).
|
|
29
|
+
|
|
1
30
|
## v1.100.0 (2026-10-22)
|
|
2
31
|
|
|
3
32
|
Guards: 530 → **530**; property checks 15 → **15**. No negative was added: the new checks are
|
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.101.0 |
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "task-pipeline-skill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.101.0",
|
|
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"
|
|
@@ -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.101.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "ssheleg",
|
|
9
9
|
"url": "https://x.com/sshlg93"
|
|
@@ -49,8 +49,14 @@ Your inputs, and nothing else is yours:
|
|
|
49
49
|
5. **Close what you opened.** `resource_guard.py close` then `resource_guard.py verify --json`;
|
|
50
50
|
its output is the report's `resources` block. A live process is a failed gate, not a note.
|
|
51
51
|
6. **Gate yourself.** `python3 scripts/design_gate.py report <report> --class <surface_class>
|
|
52
|
-
--panel docs/ux/test-panel.md --scope <ids> --require design|build
|
|
53
|
-
|
|
52
|
+
--panel docs/ux/test-panel.md --scope <ids> --require design|build`, plus `--subscription`
|
|
53
|
+
when the surface sells, renews or cancels a subscription. Quote its output as your first
|
|
54
|
+
evidence. NOT_RUN is said as NOT_RUN.
|
|
55
|
+
7. **A subscription surface: walk the way out.** Count the steps from the paywall to an active
|
|
56
|
+
subscription and from the account to a confirmed cancellation, as one persona, in the same
|
|
57
|
+
pass. Cancelling takes no more steps than subscribing (California's click-to-cancel rule
|
|
58
|
+
from 2025-07-01 says the same), the paywall makes one clear offer, and note whether a pause
|
|
59
|
+
is offered. This is the report's `subscription` block.
|
|
54
60
|
|
|
55
61
|
## The report, `design-review/1`
|
|
56
62
|
|
|
@@ -63,9 +69,16 @@ Your inputs, and nothing else is yours:
|
|
|
63
69
|
"focus": "pass", "states": "na", "text_fit": "pass", "outcome": "pass"},
|
|
64
70
|
"findings": [{"severity": "breaks", "what": "no way back from the summary",
|
|
65
71
|
"where": "summary screen", "status": "open"}]}],
|
|
66
|
-
"resources": {"started": [], "live": [], "closed": true}
|
|
72
|
+
"resources": {"started": [], "live": [], "closed": true},
|
|
73
|
+
"subscription": {"cancel_parity": "pass", "subscribe_steps": 3, "cancel_steps": 2,
|
|
74
|
+
"cancel_path": "Settings > Subscription > Cancel", "single_offer": "pass",
|
|
75
|
+
"pause": "offered"}}
|
|
67
76
|
```
|
|
68
77
|
|
|
78
|
+
`subscription` is present only on a subscription surface. `cancel_parity` and `single_offer` are
|
|
79
|
+
pass, fail or NOT_RUN (with `reason`); `pause` is offered, absent or na. A `pass` over a cancel
|
|
80
|
+
path longer than the subscribe path is refused.
|
|
81
|
+
|
|
69
82
|
`breaks` stops the user in a scenario step or contradicts the scenario's outcome. `risk` makes a
|
|
70
83
|
step slower, unclear or easy to get wrong. `nit` is polish. Taste is none of them: a finding
|
|
71
84
|
names the step, the persona and the checklist item it failed.
|
|
@@ -26,9 +26,11 @@ unmentioned one does not. Waves and iterations owe it per UI node, never once pe
|
|
|
26
26
|
`docs/ux/test-panel.md`, owned by super-ux (`ux-foundation` → *The test panel*): ten portraits,
|
|
27
27
|
each with **basis** (`evidence RE-NN` or `hypothesis`), **situation**, **goals**, **features**,
|
|
28
28
|
**scenarios** (composite chains by `SCR-` id), **activation** (steps to first value, and where
|
|
29
|
-
it drops), **retention** (trigger → action → reward →
|
|
30
|
-
(device, engine, locale, accessibility).
|
|
31
|
-
|
|
29
|
+
it drops), **retention** (opening with `local` or `tourist`, then trigger → action → reward →
|
|
30
|
+
what brings it back) and **needs** (device, engine, locale, accessibility). At least two
|
|
31
|
+
portraits are locals and two are tourists: lifetime value rests on the first, churn on the
|
|
32
|
+
second (since 1.101.0). The operator confirms it once; a later change of the product revises
|
|
33
|
+
it. `python3 scripts/design_gate.py panel docs/ux/test-panel.md` gates it.
|
|
32
34
|
|
|
33
35
|
A portrait marked `hypothesis` chooses what to test, never what to build: the super-ux rule
|
|
34
36
|
that a synthetic persona is never evidence still holds.
|
|
@@ -47,6 +49,16 @@ finding; a `breaks` finding names its failed item. NOT_RUN is never PASS.
|
|
|
47
49
|
With Figma, pass D judges what the designer drew and pass B how it was built against it.
|
|
48
50
|
Without Figma, pass D judges the mockups and pass B the build against them.
|
|
49
51
|
|
|
52
|
+
## A subscription surface: cancel parity and one clear offer
|
|
53
|
+
|
|
54
|
+
A surface that sells, renews or cancels a subscription (paywall, trial, pricing, cancel) adds
|
|
55
|
+
`--subscription` to both gates. The report's `subscription` block counts the steps to subscribe
|
|
56
|
+
and to cancel, walked by one persona in the same pass: cancelling takes no more steps than
|
|
57
|
+
subscribing (the click-to-cancel standard, law in California from 2025-07-01), the paywall makes
|
|
58
|
+
one clear offer, and whether a pause is offered is noted. A missing block, a longer cancel path
|
|
59
|
+
or a paywall without one offer fails; a check not walked is NOT_RUN with its reason; a missing
|
|
60
|
+
pause is reported, not gated. The block's fields are in `agents/design-tester.md`.
|
|
61
|
+
|
|
50
62
|
## The budget, and where findings go
|
|
51
63
|
|
|
52
64
|
One pass D and one pass B per surface per round; a re-review reads only the steps the fix
|
|
@@ -208,7 +208,11 @@ narrative are how a capped list grows anyway.
|
|
|
208
208
|
An older retro whose standing instructions are a numbered list and whose stamps are
|
|
209
209
|
`- **date: title**` items is read the same way: the item keeps its bold rule and its
|
|
210
210
|
`*(Retire ...)*` trigger, a stamp keeps its title, stamps past the cap rotate out by date,
|
|
211
|
-
and, over budget, the Run stamps section's loose prune paragraphs move.
|
|
211
|
+
and, over budget, the Run stamps section's loose prune paragraphs move. Rotation keeps
|
|
212
|
+
**one `Rotated out:` line per archive**: a later rotation re-archives the quarter's
|
|
213
|
+
earlier rotated stamps with its own as one block and the line names that block, so a
|
|
214
|
+
floor that rotates every run does not grow by a line per run. A line whose block no
|
|
215
|
+
longer hashes stays as its own line.
|
|
212
216
|
- **A prune that retires nothing writes its count into the stamp row**, as
|
|
213
217
|
`prune: 0 retired`, and no paragraph. Repeated "nothing to prune" notes were a
|
|
214
218
|
measured share of the floor.
|
|
@@ -308,7 +308,9 @@ never that the work was skipped quietly.
|
|
|
308
308
|
**On any class but `none` the design review's pass B ran over the build** (since
|
|
309
309
|
1.100.0): the same `design_gate.py report` with `--require build` exits 0, every capture
|
|
310
310
|
names its engine, and `python3 scripts/resource_guard.py verify <ledger>` is clean
|
|
311
|
-
([`design-review.md`](design-review.md)).
|
|
311
|
+
([`design-review.md`](design-review.md)). A subscription surface (paywall, trial, pricing,
|
|
312
|
+
cancel) adds `--subscription`: cancelling takes no more steps than subscribing and the
|
|
313
|
+
paywall makes one clear offer (since 1.101.0).
|
|
312
314
|
- **What the look finds is fixed here.** A rendering defect found at stage 6 is a
|
|
313
315
|
stage-6 finding: fix it, look again, then call the stage green. Filing it to the
|
|
314
316
|
board and advancing is how a run reports *checked in a browser* for a page it has
|
|
@@ -6,14 +6,18 @@ with an exit code. Stdlib only.
|
|
|
6
6
|
the test panel the design-tester walks: ten portraits (or --min), each with its
|
|
7
7
|
basis, situation, goals, features, composite scenarios, activation funnel,
|
|
8
8
|
retention loop and needs. A portrait without a basis line is refused: a panel is a
|
|
9
|
-
hypothesis instrument, and saying so is part of the portrait.
|
|
9
|
+
hypothesis instrument, and saying so is part of the portrait. Each retention line
|
|
10
|
+
opens with `local` or `tourist`, and the panel holds at least two of each (1.101.0).
|
|
10
11
|
design_gate.py report <design-review.json> --class <surface_class>
|
|
11
12
|
[--panel <test-panel.md>] [--scope SCR-01,SCR-02] [--require build]
|
|
12
|
-
[--max-passes 1] [--json]
|
|
13
|
+
[--max-passes 1] [--subscription] [--json]
|
|
13
14
|
stage 3 (`--require design`) and stage 6 (`--require build`): the report covers
|
|
14
15
|
every scope scenario × every panel persona that uses it, every step carries the
|
|
15
16
|
full checklist, no `breaks` finding is open, each round holds at most --max-passes
|
|
16
17
|
passes of each kind, and the resource receipt shows every process it started closed.
|
|
18
|
+
`--subscription` (or a `subscription` block in the report): the surface sells, renews
|
|
19
|
+
or cancels a subscription, so cancelling takes no more steps than subscribing and the
|
|
20
|
+
paywall makes one clear offer (1.101.0).
|
|
17
21
|
|
|
18
22
|
design_gate.py browser <pipeline.json> [--repo DIR] [--json]
|
|
19
23
|
the recorded test browser: `browser.primary` is set (NOT_RUN when the block is
|
|
@@ -52,6 +56,7 @@ ALIASES = {"composite scenarios": "scenarios", "activation funnel": "activation"
|
|
|
52
56
|
FIELD = re.compile(r"^\s*[-*]\s+\*\*(%s)\s*:?\*\*\s*[:—–-]?\s*(.+?)\s*$"
|
|
53
57
|
% "|".join(sorted(list(FIELDS) + list(ALIASES), key=len, reverse=True)), re.I)
|
|
54
58
|
SCR = re.compile(r"\bSCR-\d+\b")
|
|
59
|
+
SEGMENT = re.compile(r"^\s*(local|tourist)\b\s*:?", re.I)
|
|
55
60
|
|
|
56
61
|
|
|
57
62
|
def read_text(path):
|
|
@@ -110,6 +115,17 @@ def panel_problems(panel, minimum):
|
|
|
110
115
|
out.append("%s: basis is `evidence RE-NN` or `hypothesis`, not %r" % (pid, basis[:40]))
|
|
111
116
|
if p.get("scenarios") and not SCR.search(p["scenarios"]):
|
|
112
117
|
out.append("%s: composite scenarios cite no SCR- id" % pid)
|
|
118
|
+
# M2 R3 (1.101.0): lifetime value rests on locals and churn on tourists, so the panel
|
|
119
|
+
# walks both. The retention line opens with the segment (super-ux templates/test-panel.md).
|
|
120
|
+
seg = {pid: SEGMENT.match(p.get("retention", "")) for pid, p in panel.items()}
|
|
121
|
+
for pid, m in sorted(seg.items()):
|
|
122
|
+
if panel[pid].get("retention") and not m:
|
|
123
|
+
out.append("%s: the retention line opens with `local` or `tourist`" % pid)
|
|
124
|
+
locals_ = sum(1 for m in seg.values() if m and m.group(1).lower() == "local")
|
|
125
|
+
tourists = sum(1 for m in seg.values() if m and m.group(1).lower() == "tourist")
|
|
126
|
+
if panel and (locals_ < 2 or tourists < 2):
|
|
127
|
+
out.append("the panel carries %d local%s and %d tourist%s; it owes at least two of each"
|
|
128
|
+
% (locals_, "" if locals_ == 1 else "s", tourists, "" if tourists == 1 else "s"))
|
|
113
129
|
return out
|
|
114
130
|
|
|
115
131
|
|
|
@@ -174,10 +190,52 @@ def row_problems(r, i):
|
|
|
174
190
|
return out
|
|
175
191
|
|
|
176
192
|
|
|
177
|
-
|
|
193
|
+
PARITY = ("pass", "fail", "NOT_RUN")
|
|
194
|
+
PAUSE = ("offered", "absent", "na")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def subscription_problems(sub):
|
|
198
|
+
"""M2 G8/R2: cancelling is as easy as subscribing, and the paywall makes one clear offer.
|
|
199
|
+
Returns (problems, notes, not_run)."""
|
|
200
|
+
if sub is None:
|
|
201
|
+
return (["a subscription surface carries its cancel parity check (`subscription` block: "
|
|
202
|
+
"cancel_parity, subscribe_steps, cancel_steps, single_offer, pause)"], [], False)
|
|
203
|
+
if not isinstance(sub, dict):
|
|
204
|
+
return ["`subscription` is an object, not %r" % (sub,)], [], False
|
|
205
|
+
out, notes = [], []
|
|
206
|
+
for k in ("cancel_parity", "single_offer"):
|
|
207
|
+
if sub.get(k) not in PARITY:
|
|
208
|
+
out.append("subscription.%s is %r, not one of %s" % (k, sub.get(k), "/".join(PARITY)))
|
|
209
|
+
steps = {k: _posint(sub.get(k)) for k in ("subscribe_steps", "cancel_steps")}
|
|
210
|
+
for k, v in steps.items():
|
|
211
|
+
if v is None and sub.get("cancel_parity") != "NOT_RUN":
|
|
212
|
+
out.append("subscription.%s is a positive integer, not %r" % (k, sub.get(k)))
|
|
213
|
+
s, c = steps["subscribe_steps"], steps["cancel_steps"]
|
|
214
|
+
if sub.get("cancel_parity") == "fail":
|
|
215
|
+
out.append("cancelling takes %s steps against %s to subscribe: cancel parity fails" % (c, s))
|
|
216
|
+
elif sub.get("cancel_parity") == "pass" and s and c and c > s:
|
|
217
|
+
out.append("cancel_parity `pass` contradicts %d cancel steps against %d to subscribe" % (c, s))
|
|
218
|
+
if sub.get("single_offer") == "fail":
|
|
219
|
+
out.append("the paywall makes no single clear offer")
|
|
220
|
+
if sub.get("pause") not in PAUSE:
|
|
221
|
+
out.append("subscription.pause is offered, absent or na, not %r" % (sub.get("pause"),))
|
|
222
|
+
elif sub.get("pause") == "absent":
|
|
223
|
+
notes.append("no pause offered on the cancel path (a save offer, not gated)")
|
|
224
|
+
idle = [k for k in ("cancel_parity", "single_offer") if sub.get(k) == "NOT_RUN"]
|
|
225
|
+
if idle and not sub.get("reason"):
|
|
226
|
+
out.append("a NOT_RUN %s carries its reason" % " and ".join(idle))
|
|
227
|
+
return out, notes, bool(idle)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def report_check(doc, surface_class, panel=None, scope=(), require=None, max_passes=1, subscription=False):
|
|
178
231
|
problems, notes = [], []
|
|
179
232
|
if not isinstance(doc, dict) or doc.get("schema") != "design-review/1":
|
|
180
233
|
return "FAIL", ["the report is not design-review/1"], notes
|
|
234
|
+
sub_idle = False
|
|
235
|
+
if subscription or "subscription" in doc:
|
|
236
|
+
sp, sn, sub_idle = subscription_problems(doc.get("subscription"))
|
|
237
|
+
problems += sp
|
|
238
|
+
notes += sn
|
|
181
239
|
rows = doc.get("rows") or []
|
|
182
240
|
if not isinstance(rows, list) or any(not isinstance(r, dict) for r in rows):
|
|
183
241
|
return "FAIL", ["`rows` is a list of objects"], notes
|
|
@@ -252,6 +310,8 @@ def report_check(doc, surface_class, panel=None, scope=(), require=None, max_pas
|
|
|
252
310
|
return "NOT_RUN", ["every %s step is NOT_RUN: the %s pass did not run" % (k, k)], notes
|
|
253
311
|
if k in kinds and not mine:
|
|
254
312
|
return "NOT_RUN", ["no %s step in the report" % k], notes
|
|
313
|
+
if sub_idle:
|
|
314
|
+
return "NOT_RUN", ["the subscription checks were not walked: %s" % doc["subscription"].get("reason")], notes
|
|
255
315
|
partial = ["%s step %s for %s (%s)" % (r.get("scenario"), r.get("step"), r.get("persona"), r.get("reason", "no reason"))
|
|
256
316
|
for r in rows if any(v == "NOT_RUN" for v in (r.get("checks") or {}).values())]
|
|
257
317
|
if partial:
|
|
@@ -322,6 +382,8 @@ def main(argv=None):
|
|
|
322
382
|
p2.add_argument("--scope", default="")
|
|
323
383
|
p2.add_argument("--require", choices=PASSES)
|
|
324
384
|
p2.add_argument("--max-passes", type=int, default=1)
|
|
385
|
+
p2.add_argument("--subscription", action="store_true",
|
|
386
|
+
help="the surface sells, renews or cancels a subscription: cancel parity and one clear offer are owed")
|
|
325
387
|
p2.add_argument("--json", action="store_true")
|
|
326
388
|
a = ap.parse_args(argv)
|
|
327
389
|
if a.cmd == "panel":
|
|
@@ -354,7 +416,7 @@ def main(argv=None):
|
|
|
354
416
|
return 2
|
|
355
417
|
panel = parse_panel(read_text(a.panel)) if a.panel and os.path.exists(a.panel) else None
|
|
356
418
|
scope = [s.strip() for s in a.scope.split(",") if s.strip()]
|
|
357
|
-
verdict, probs, notes = report_check(doc, a.cls, panel, scope, a.require, a.max_passes)
|
|
419
|
+
verdict, probs, notes = report_check(doc, a.cls, panel, scope, a.require, a.max_passes, a.subscription)
|
|
358
420
|
return emit(verdict, probs, notes, a.json)
|
|
359
421
|
|
|
360
422
|
|
|
@@ -252,6 +252,32 @@ def block(kind, label, text):
|
|
|
252
252
|
% (datetime.date.today().isoformat(), kind, label, h, text))
|
|
253
253
|
|
|
254
254
|
|
|
255
|
+
ROTATED = re.compile(r"^Rotated out: ([0-9]+) oldest run stamps, verbatim in (\S+) sha256:([0-9a-f]{12,64})$")
|
|
256
|
+
ARCHIVED = re.compile(r"<!-- sha256:([0-9a-f]{64}) -->\n(.*?)\n(?=\n#{2,3} |\Z)", re.S)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def rotation_lines(lines, span, rel):
|
|
260
|
+
"""(line index, stamp count, short sha) of each rotation pointer into `rel`."""
|
|
261
|
+
out = []
|
|
262
|
+
for k in range(span[0], span[1]):
|
|
263
|
+
r = ROTATED.match(lines[k])
|
|
264
|
+
if r and r.group(2) == rel:
|
|
265
|
+
out.append((k, int(r.group(1)), r.group(3)))
|
|
266
|
+
return out
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def archived_text(path, short):
|
|
270
|
+
"""(offset, text) of the archive block whose text hashes to `short`, or None."""
|
|
271
|
+
try:
|
|
272
|
+
arch = read(path)
|
|
273
|
+
except Unmeasurable:
|
|
274
|
+
return None
|
|
275
|
+
for m in ARCHIVED.finditer(arch):
|
|
276
|
+
if m.group(1).startswith(short) and digest(m.group(2)) == m.group(1):
|
|
277
|
+
return m.start(), m.group(2)
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
|
|
255
281
|
def cmd_check(a):
|
|
256
282
|
m = measure(read(a.retro), a.budget, a.row, a.stamp)
|
|
257
283
|
print(summary(m, a.row, a.stamp))
|
|
@@ -332,11 +358,28 @@ def cmd_migrate(a):
|
|
|
332
358
|
j += 1
|
|
333
359
|
if all((r, r + 1) in rotated for r in range(k + 2, j)):
|
|
334
360
|
moved.append((k, k + 2, [], None, None)) # archived above, with its rows
|
|
335
|
-
|
|
361
|
+
# B-246: one pointer line per archive, not one per rotation. Earlier rotations
|
|
362
|
+
# into the same archive are re-archived with this one as a single block, and
|
|
363
|
+
# their lines give way to one line naming it. The archive stays append-only:
|
|
364
|
+
# the earlier blocks remain, only the floor stops pointing at them.
|
|
365
|
+
count, earlier = len(rotated), []
|
|
366
|
+
for k, n, short in rotation_lines(lines, m["spans"][STAMPS], rel):
|
|
367
|
+
prior = archived_text(archive, short)
|
|
368
|
+
if prior is None:
|
|
369
|
+
print("migrate: the rotation line sha256:%s does not resolve in %s; "
|
|
370
|
+
"it stays as its own line" % (short, rel))
|
|
371
|
+
continue
|
|
372
|
+
earlier.append((prior[0], k, prior[1]))
|
|
373
|
+
count += n
|
|
374
|
+
for _, k, _ in earlier:
|
|
375
|
+
moved.append((k, k + 1, [], None, None))
|
|
376
|
+
if earlier:
|
|
377
|
+
text = "\n".join([t for _, _, t in sorted(earlier)] + [text])
|
|
378
|
+
h, b = block("stamps", "%d oldest rotated" % count, text)
|
|
336
379
|
for sp in rotated:
|
|
337
380
|
moved.append((sp[0], sp[1], [], None, None))
|
|
338
381
|
notes.append(("Rotated out: %d oldest run stamps, verbatim in %s sha256:%s"
|
|
339
|
-
% (
|
|
382
|
+
% (count, rel, h[:12]), b, "%d oldest run stamps rotated" % len(rotated)))
|
|
340
383
|
for sp in m["stm"]:
|
|
341
384
|
i = sp[0]
|
|
342
385
|
if sp in rotated or len("\n".join(lines[sp[0]:sp[1]])) <= a.stamp:
|
|
@@ -502,7 +545,7 @@ def verify(retro):
|
|
|
502
545
|
bad.append("sha256:%s points at %s, which does not exist" % (short, rel))
|
|
503
546
|
continue
|
|
504
547
|
ok = False
|
|
505
|
-
for m in
|
|
548
|
+
for m in ARCHIVED.finditer(arch):
|
|
506
549
|
if m.group(1).startswith(short) and digest(m.group(2)) == m.group(1):
|
|
507
550
|
ok = True
|
|
508
551
|
break
|