task-pipeline-skill 1.99.1 → 1.100.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 +31 -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/agents/design-tester.md +80 -0
- package/plugins/task-pipeline/skills/task-pipeline/pipeline.schema.json +73 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/browser.md +28 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/design-review.md +64 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/doctrine-map.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/finish-line.md +9 -3
- package/plugins/task-pipeline/skills/task-pipeline/references/loop-guard.md +5 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/portability.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +15 -2
- package/plugins/task-pipeline/skills/task-pipeline/scripts/backlog_sources.py +2 -3
- package/plugins/task-pipeline/skills/task-pipeline/scripts/design_gate.py +362 -0
- package/plugins/task-pipeline/skills/task-pipeline/scripts/graph.py +1 -1
- package/plugins/task-pipeline/skills/task-pipeline/scripts/resource_guard.py +338 -0
- package/plugins/task-pipeline/skills/task-pipeline/scripts/waves.py +51 -4
- package/plugins/task-pipeline/skills/task-pipeline/templates/run.md +6 -4
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The design-review gate: the test panel and the design-tester's report, as commands
|
|
3
|
+
with an exit code. Stdlib only.
|
|
4
|
+
|
|
5
|
+
design_gate.py panel <docs/ux/test-panel.md> [--min 10] [--json]
|
|
6
|
+
the test panel the design-tester walks: ten portraits (or --min), each with its
|
|
7
|
+
basis, situation, goals, features, composite scenarios, activation funnel,
|
|
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.
|
|
10
|
+
design_gate.py report <design-review.json> --class <surface_class>
|
|
11
|
+
[--panel <test-panel.md>] [--scope SCR-01,SCR-02] [--require build]
|
|
12
|
+
[--max-passes 1] [--json]
|
|
13
|
+
stage 3 (`--require design`) and stage 6 (`--require build`): the report covers
|
|
14
|
+
every scope scenario × every panel persona that uses it, every step carries the
|
|
15
|
+
full checklist, no `breaks` finding is open, each round holds at most --max-passes
|
|
16
|
+
passes of each kind, and the resource receipt shows every process it started closed.
|
|
17
|
+
|
|
18
|
+
design_gate.py browser <pipeline.json> [--repo DIR] [--json]
|
|
19
|
+
the recorded test browser: `browser.primary` is set (NOT_RUN when the block is
|
|
20
|
+
absent: stage 0 asks the operator once), the fallback names known drivers, and
|
|
21
|
+
`browser.storageState`, a credential, sits outside the repository.
|
|
22
|
+
|
|
23
|
+
`<surface_class>` is the brief's stage-0 answer: flagship | product | internal | ad. On
|
|
24
|
+
flagship, product and ad the review is a GATE; on internal the report is checked for honesty
|
|
25
|
+
and its coverage only reported (`references/design-review.md`).
|
|
26
|
+
|
|
27
|
+
**A check that could not run says NOT_RUN, and NOT_RUN is never PASS.** No report on a gated
|
|
28
|
+
class is NOT_RUN; a step whose capture could not be taken is NOT_RUN with its reason.
|
|
29
|
+
|
|
30
|
+
Exit codes: 0 PASS · 1 FAIL · 2 usage, or an input that cannot be read · 3 NOT_RUN.
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import argparse
|
|
35
|
+
import json
|
|
36
|
+
import os
|
|
37
|
+
import re
|
|
38
|
+
import sys
|
|
39
|
+
|
|
40
|
+
SURFACE_CLASSES = ("flagship", "product", "internal", "ad")
|
|
41
|
+
GATED = ("flagship", "product", "ad")
|
|
42
|
+
PASSES = ("design", "build")
|
|
43
|
+
# The fixed checklist every step is judged on (spec DR-05). A person can read it in a row.
|
|
44
|
+
CHECKS = ("frame", "primary_action", "controls", "focus", "states", "text_fit", "outcome")
|
|
45
|
+
CHECK_VALUES = ("pass", "fail", "na", "NOT_RUN")
|
|
46
|
+
SEVERITIES = ("breaks", "risk", "nit")
|
|
47
|
+
STATUSES = ("open", "fixed") # absent means open; anything else is refused, never read as closed
|
|
48
|
+
FIELDS = ("basis", "situation", "goals", "features", "scenarios", "activation", "retention", "needs")
|
|
49
|
+
PORTRAIT = re.compile(r"^##\s+(P-\d{2})\b(.*)$")
|
|
50
|
+
ALIASES = {"composite scenarios": "scenarios", "activation funnel": "activation",
|
|
51
|
+
"retention loop": "retention"}
|
|
52
|
+
FIELD = re.compile(r"^\s*[-*]\s+\*\*(%s)\s*:?\*\*\s*[:—–-]?\s*(.+?)\s*$"
|
|
53
|
+
% "|".join(sorted(list(FIELDS) + list(ALIASES), key=len, reverse=True)), re.I)
|
|
54
|
+
SCR = re.compile(r"\bSCR-\d+\b")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def read_text(path):
|
|
58
|
+
try:
|
|
59
|
+
with open(path, encoding="utf-8") as fh:
|
|
60
|
+
return fh.read()
|
|
61
|
+
except OSError as exc:
|
|
62
|
+
raise SystemExit(_usage("cannot read %s (%s)" % (path, exc.strerror or exc)))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _usage(msg):
|
|
66
|
+
print("design_gate: %s" % msg, file=sys.stderr)
|
|
67
|
+
return 2
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def parse_panel(text):
|
|
71
|
+
"""{persona id: {field: value}} from the panel's `## P-NN` sections. A repeated id is
|
|
72
|
+
kept under `__duplicates__` so the check can name it rather than count it once."""
|
|
73
|
+
panel, cur, dups = {}, None, []
|
|
74
|
+
for line in text.splitlines():
|
|
75
|
+
m = PORTRAIT.match(line)
|
|
76
|
+
if m:
|
|
77
|
+
cur = m.group(1)
|
|
78
|
+
if cur in panel:
|
|
79
|
+
dups.append(cur)
|
|
80
|
+
panel[cur] = {"title": m.group(2).strip(" —-:")}
|
|
81
|
+
continue
|
|
82
|
+
if line.startswith("## "):
|
|
83
|
+
cur = None
|
|
84
|
+
continue
|
|
85
|
+
if cur:
|
|
86
|
+
f = FIELD.match(line)
|
|
87
|
+
if f:
|
|
88
|
+
key = f.group(1).lower()
|
|
89
|
+
panel[cur][ALIASES.get(key, key)] = f.group(2).strip()
|
|
90
|
+
if dups:
|
|
91
|
+
panel["__duplicates__"] = dups
|
|
92
|
+
return panel
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def portraits(panel):
|
|
96
|
+
return {k: v for k, v in panel.items() if not k.startswith("__")}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def panel_problems(panel, minimum):
|
|
100
|
+
out = ["%s appears twice; every portrait has its own id" % d for d in panel.get("__duplicates__", [])]
|
|
101
|
+
panel = portraits(panel)
|
|
102
|
+
if len(panel) < minimum:
|
|
103
|
+
out.append("the panel holds %d portrait(s); it owes %d" % (len(panel), minimum))
|
|
104
|
+
for pid, p in sorted(panel.items()):
|
|
105
|
+
for f in FIELDS:
|
|
106
|
+
if not p.get(f):
|
|
107
|
+
out.append("%s: no **%s** line" % (pid, f))
|
|
108
|
+
basis = p.get("basis", "")
|
|
109
|
+
if basis and not re.match(r"^(evidence\s+RE-\d+|hypothesis)\b", basis, re.I):
|
|
110
|
+
out.append("%s: basis is `evidence RE-NN` or `hypothesis`, not %r" % (pid, basis[:40]))
|
|
111
|
+
if p.get("scenarios") and not SCR.search(p["scenarios"]):
|
|
112
|
+
out.append("%s: composite scenarios cite no SCR- id" % pid)
|
|
113
|
+
return out
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def personas_using(panel, scenario):
|
|
117
|
+
return sorted(pid for pid, p in portraits(panel).items() if scenario in SCR.findall(p.get("scenarios", "")))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _posint(v):
|
|
121
|
+
"""A round or step as a positive int: 1, "1" and "01" are 1; "1.0", "first", 0 are None."""
|
|
122
|
+
if isinstance(v, bool):
|
|
123
|
+
return None
|
|
124
|
+
if isinstance(v, int):
|
|
125
|
+
return v if v > 0 else None
|
|
126
|
+
if isinstance(v, str) and v.strip().isascii() and v.strip().isdigit() and int(v) > 0:
|
|
127
|
+
return int(v)
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def row_problems(r, i):
|
|
132
|
+
out, rid = [], "row %d (%s %s/%s %s)" % (i, r.get("scenario"), r.get("step"), r.get("persona"), r.get("pass"))
|
|
133
|
+
for k in ("scenario", "step", "persona", "pass", "round"):
|
|
134
|
+
if r.get(k) in (None, ""):
|
|
135
|
+
out.append("%s: no `%s`" % (rid, k))
|
|
136
|
+
if r.get("round") not in (None, "") and _posint(r.get("round")) is None:
|
|
137
|
+
out.append("%s: round is a positive integer, not %r" % (rid, r.get("round")))
|
|
138
|
+
if r.get("pass") not in PASSES:
|
|
139
|
+
out.append("%s: pass is design or build" % rid)
|
|
140
|
+
checks = r.get("checks") or {}
|
|
141
|
+
if not isinstance(checks, dict):
|
|
142
|
+
return out + ["%s: `checks` is an object of the seven checklist items" % rid]
|
|
143
|
+
if not isinstance(r.get("findings") or [], list):
|
|
144
|
+
return out + ["%s: `findings` is a list" % rid]
|
|
145
|
+
for c in CHECKS:
|
|
146
|
+
v = checks.get(c)
|
|
147
|
+
if v not in CHECK_VALUES:
|
|
148
|
+
out.append("%s: checklist item `%s` is %r, not one of %s" % (rid, c, v, "/".join(CHECK_VALUES)))
|
|
149
|
+
for c in ("frame", "outcome"):
|
|
150
|
+
if checks.get(c) == "na":
|
|
151
|
+
out.append("%s: `%s` is never na: every step has a screen and an outcome" % (rid, c))
|
|
152
|
+
if any(v == "NOT_RUN" for v in checks.values()) and not r.get("reason"):
|
|
153
|
+
out.append("%s: a NOT_RUN item carries its reason" % rid)
|
|
154
|
+
if r.get("pass") == "design" and not r.get("frame"):
|
|
155
|
+
out.append("%s: a design step names its Figma node or mockup (`frame`)" % rid)
|
|
156
|
+
if r.get("pass") == "build":
|
|
157
|
+
if not r.get("capture") and checks.get("frame") != "NOT_RUN":
|
|
158
|
+
out.append("%s: a build step carries its `capture`" % rid)
|
|
159
|
+
if not r.get("engine"):
|
|
160
|
+
out.append("%s: a build step names its `engine`" % rid)
|
|
161
|
+
failed = any(checks.get(c) == "fail" for c in CHECKS)
|
|
162
|
+
for f in r.get("findings") or []:
|
|
163
|
+
if not isinstance(f, dict):
|
|
164
|
+
out.append("%s: a finding is an object with severity, what and status" % rid)
|
|
165
|
+
continue
|
|
166
|
+
if f.get("status", "open") not in STATUSES:
|
|
167
|
+
out.append("%s: finding status is open or fixed, not %r" % (rid, f.get("status")))
|
|
168
|
+
if f.get("severity") not in SEVERITIES:
|
|
169
|
+
out.append("%s: finding severity is breaks, risk or nit" % rid)
|
|
170
|
+
elif f.get("severity") == "breaks" and not failed:
|
|
171
|
+
out.append("%s: a `breaks` finding names the checklist item that failed" % rid)
|
|
172
|
+
if any(v == "fail" for v in checks.values()) and not (r.get("findings") or []):
|
|
173
|
+
out.append("%s: a failed checklist item names its finding" % rid)
|
|
174
|
+
return out
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def report_check(doc, surface_class, panel=None, scope=(), require=None, max_passes=1):
|
|
178
|
+
problems, notes = [], []
|
|
179
|
+
if not isinstance(doc, dict) or doc.get("schema") != "design-review/1":
|
|
180
|
+
return "FAIL", ["the report is not design-review/1"], notes
|
|
181
|
+
rows = doc.get("rows") or []
|
|
182
|
+
if not isinstance(rows, list) or any(not isinstance(r, dict) for r in rows):
|
|
183
|
+
return "FAIL", ["`rows` is a list of objects"], notes
|
|
184
|
+
if not rows:
|
|
185
|
+
return "NOT_RUN", ["the report holds no rows"], notes
|
|
186
|
+
for i, r in enumerate(rows):
|
|
187
|
+
problems += row_problems(r, i)
|
|
188
|
+
open_breaks = [(r.get("scenario"), r.get("step"), r.get("persona")) for r in rows
|
|
189
|
+
for f in (r.get("findings") if isinstance(r.get("findings"), list) else [])
|
|
190
|
+
if isinstance(f, dict) and f.get("severity") == "breaks" and f.get("status", "open") != "fixed"]
|
|
191
|
+
for s in open_breaks:
|
|
192
|
+
problems.append("open `breaks` finding at %s step %s for %s" % s)
|
|
193
|
+
# DR-10: one pass of each kind per surface per round. A second pass shows as the same
|
|
194
|
+
# step walked again in the same round; the round is read as a number.
|
|
195
|
+
walks = {}
|
|
196
|
+
for r in rows:
|
|
197
|
+
rnd = _posint(r.get("round"))
|
|
198
|
+
if rnd is None:
|
|
199
|
+
continue
|
|
200
|
+
step = _posint(r.get("step"))
|
|
201
|
+
key = (rnd, r.get("pass"), r.get("scenario"), step if step is not None else str(r.get("step")), r.get("persona"))
|
|
202
|
+
walks[key] = walks.get(key, 0) + 1
|
|
203
|
+
over = {}
|
|
204
|
+
for (rnd, kind, *_), n in walks.items():
|
|
205
|
+
if n > max_passes:
|
|
206
|
+
over[(rnd, kind)] = max(over.get((rnd, kind), 0), n)
|
|
207
|
+
for (rnd, kind), n in sorted(over.items()):
|
|
208
|
+
problems.append("round %d walks a %s step %d times; the budget is %d pass(es) per round" % (rnd, kind, n, max_passes))
|
|
209
|
+
# Coverage: each scope scenario × each persona that uses it, for each required pass.
|
|
210
|
+
kinds = PASSES if require == "build" else ("design",) if require == "design" else ()
|
|
211
|
+
no_panel = panel is None or not portraits(panel)
|
|
212
|
+
coverage_gap = None
|
|
213
|
+
if kinds and surface_class in GATED and (no_panel or not scope):
|
|
214
|
+
missing = ("a test panel with portraits" if no_panel else "") + (" and " if no_panel and not scope else "") + \
|
|
215
|
+
("the scope (--scope SCR ids)" if not scope else "")
|
|
216
|
+
coverage_gap = "coverage cannot be computed without %s; the gate does not pass on it" % missing
|
|
217
|
+
if panel and portraits(panel) and scope:
|
|
218
|
+
seen = {(r.get("scenario"), r.get("persona"), r.get("pass")) for r in rows}
|
|
219
|
+
for s in scope:
|
|
220
|
+
users = personas_using(panel, s)
|
|
221
|
+
if not users:
|
|
222
|
+
problems.append("%s: no panel persona uses it; the panel does not cover the change" % s)
|
|
223
|
+
for p in users:
|
|
224
|
+
for k in kinds:
|
|
225
|
+
if (s, p, k) not in seen:
|
|
226
|
+
problems.append("%s × %s: no %s pass" % (s, p, k))
|
|
227
|
+
elif kinds:
|
|
228
|
+
notes.append("coverage not computed: --panel and --scope are both needed")
|
|
229
|
+
# DR-08: the resource receipt.
|
|
230
|
+
res = doc.get("resources")
|
|
231
|
+
if not isinstance(res, dict):
|
|
232
|
+
problems.append("no resource receipt (`resources`): what the review started, and that it closed")
|
|
233
|
+
else:
|
|
234
|
+
if res.get("live"):
|
|
235
|
+
problems.append("processes the review started are still running: %s" % ", ".join(map(str, res["live"])))
|
|
236
|
+
if res.get("closed") is not True:
|
|
237
|
+
problems.append("the resource receipt does not say `closed: true`")
|
|
238
|
+
if surface_class not in GATED:
|
|
239
|
+
notes.append("class %s: the review is recommended; problems are reported, not gated" % surface_class)
|
|
240
|
+
return "PASS", problems, notes
|
|
241
|
+
if problems:
|
|
242
|
+
return "FAIL", problems + ([coverage_gap] if coverage_gap else []), notes
|
|
243
|
+
if coverage_gap:
|
|
244
|
+
return "NOT_RUN", [coverage_gap], notes
|
|
245
|
+
# NOT_RUN is judged per required pass: a stage-6 gate whose build steps were never driven
|
|
246
|
+
# is NOT_RUN, whatever the design rows say. Partial NOT_RUN (a sign-in wall) is listed.
|
|
247
|
+
def idle(r):
|
|
248
|
+
return all(v == "NOT_RUN" for v in (r.get("checks") or {}).values())
|
|
249
|
+
for k in kinds or PASSES:
|
|
250
|
+
mine = [r for r in rows if r.get("pass") == k]
|
|
251
|
+
if mine and all(idle(r) for r in mine):
|
|
252
|
+
return "NOT_RUN", ["every %s step is NOT_RUN: the %s pass did not run" % (k, k)], notes
|
|
253
|
+
if k in kinds and not mine:
|
|
254
|
+
return "NOT_RUN", ["no %s step in the report" % k], notes
|
|
255
|
+
partial = ["%s step %s for %s (%s)" % (r.get("scenario"), r.get("step"), r.get("persona"), r.get("reason", "no reason"))
|
|
256
|
+
for r in rows if any(v == "NOT_RUN" for v in (r.get("checks") or {}).values())]
|
|
257
|
+
if partial:
|
|
258
|
+
notes.append("NOT_RUN steps: " + "; ".join(partial))
|
|
259
|
+
return "PASS", problems, notes
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
DRIVERS = ("safari", "playwright-webkit", "chrome-dev", "playwright-chromium")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def browser_check(cfg, repo):
|
|
266
|
+
b = cfg.get("browser") if isinstance(cfg, dict) else None
|
|
267
|
+
if not isinstance(b, dict):
|
|
268
|
+
return "NOT_RUN", ["no `browser` block: stage 0 asks the operator which browser the tests use, once"], []
|
|
269
|
+
probs = []
|
|
270
|
+
if b.get("primary") not in DRIVERS:
|
|
271
|
+
probs.append("browser.primary is one of %s, not %r" % (", ".join(DRIVERS), b.get("primary")))
|
|
272
|
+
fb = b.get("fallback") or []
|
|
273
|
+
if not isinstance(fb, list):
|
|
274
|
+
probs.append("browser.fallback is a list of drivers, not %r" % (fb,))
|
|
275
|
+
fb = []
|
|
276
|
+
for f in fb:
|
|
277
|
+
if f not in DRIVERS:
|
|
278
|
+
probs.append("browser.fallback names an unknown driver %r" % f)
|
|
279
|
+
st = b.get("storageState")
|
|
280
|
+
if st is not None and not isinstance(st, str):
|
|
281
|
+
probs.append("browser.storageState is a path string")
|
|
282
|
+
st = None
|
|
283
|
+
if st:
|
|
284
|
+
exp = os.path.expanduser(st)
|
|
285
|
+
full = os.path.normcase(os.path.realpath(exp if os.path.isabs(exp) else os.path.join(repo, exp)))
|
|
286
|
+
root = os.path.normcase(os.path.realpath(repo))
|
|
287
|
+
if sys.platform == "darwin": # APFS is case-insensitive by default; normcase is not
|
|
288
|
+
full, root = full.lower(), root.lower()
|
|
289
|
+
if full == root or full.startswith(root + os.sep):
|
|
290
|
+
probs.append("browser.storageState %s is inside the repository; a signed-in state is a credential" % st)
|
|
291
|
+
notes = ["primary %s, fallback %s" % (b.get("primary"), ", ".join(map(str, fb)) or "none")]
|
|
292
|
+
return ("FAIL" if probs else "PASS"), probs, notes
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def emit(verdict, problems, notes, as_json, extra=None):
|
|
296
|
+
if as_json:
|
|
297
|
+
print(json.dumps(dict(verdict=verdict, problems=problems, notes=notes, **(extra or {})), indent=2))
|
|
298
|
+
else:
|
|
299
|
+
print("design_gate: %s" % verdict)
|
|
300
|
+
for p in problems:
|
|
301
|
+
print(" - %s" % p)
|
|
302
|
+
for n in notes:
|
|
303
|
+
print(" note: %s" % n)
|
|
304
|
+
return {"PASS": 0, "FAIL": 1, "NOT_RUN": 3}[verdict]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def main(argv=None):
|
|
308
|
+
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
|
309
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
310
|
+
p1 = sub.add_parser("panel")
|
|
311
|
+
p1.add_argument("path")
|
|
312
|
+
p1.add_argument("--min", type=int, default=10)
|
|
313
|
+
p1.add_argument("--json", action="store_true")
|
|
314
|
+
p3 = sub.add_parser("browser")
|
|
315
|
+
p3.add_argument("path")
|
|
316
|
+
p3.add_argument("--repo", help="the repository root; default: the git top level of the pipeline.json's folder")
|
|
317
|
+
p3.add_argument("--json", action="store_true")
|
|
318
|
+
p2 = sub.add_parser("report")
|
|
319
|
+
p2.add_argument("path")
|
|
320
|
+
p2.add_argument("--class", dest="cls", required=True, choices=SURFACE_CLASSES)
|
|
321
|
+
p2.add_argument("--panel")
|
|
322
|
+
p2.add_argument("--scope", default="")
|
|
323
|
+
p2.add_argument("--require", choices=PASSES)
|
|
324
|
+
p2.add_argument("--max-passes", type=int, default=1)
|
|
325
|
+
p2.add_argument("--json", action="store_true")
|
|
326
|
+
a = ap.parse_args(argv)
|
|
327
|
+
if a.cmd == "panel":
|
|
328
|
+
if not os.path.exists(a.path):
|
|
329
|
+
return emit("NOT_RUN", ["no test panel at %s: super-ux `ux-foundation` builds it" % a.path], [], a.json)
|
|
330
|
+
panel = parse_panel(read_text(a.path))
|
|
331
|
+
probs = panel_problems(panel, a.min)
|
|
332
|
+
return emit("FAIL" if probs else "PASS", probs, ["%d portrait(s)" % len(portraits(panel))], a.json)
|
|
333
|
+
if a.cmd == "browser":
|
|
334
|
+
try:
|
|
335
|
+
cfg = json.loads(read_text(a.path))
|
|
336
|
+
except ValueError as exc:
|
|
337
|
+
print("design_gate: %s is not JSON (%s)" % (a.path, exc), file=sys.stderr)
|
|
338
|
+
return 2
|
|
339
|
+
repo = a.repo
|
|
340
|
+
if not repo:
|
|
341
|
+
import subprocess
|
|
342
|
+
here = os.path.dirname(os.path.abspath(a.path))
|
|
343
|
+
r = subprocess.run(["git", "-C", here, "rev-parse", "--show-toplevel"], capture_output=True, text=True)
|
|
344
|
+
repo = r.stdout.strip() if r.returncode == 0 and r.stdout.strip() else here
|
|
345
|
+
v, probs, notes = browser_check(cfg, repo)
|
|
346
|
+
return emit(v, probs, notes, a.json)
|
|
347
|
+
if not os.path.exists(a.path):
|
|
348
|
+
v = "NOT_RUN" if a.cls in GATED else "PASS"
|
|
349
|
+
return emit(v, ["no design review at %s" % a.path], [], a.json)
|
|
350
|
+
try:
|
|
351
|
+
doc = json.loads(read_text(a.path))
|
|
352
|
+
except ValueError as exc:
|
|
353
|
+
print("design_gate: %s is not JSON (%s)" % (a.path, exc), file=sys.stderr)
|
|
354
|
+
return 2
|
|
355
|
+
panel = parse_panel(read_text(a.panel)) if a.panel and os.path.exists(a.panel) else None
|
|
356
|
+
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)
|
|
358
|
+
return emit(verdict, probs, notes, a.json)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
if __name__ == "__main__":
|
|
362
|
+
sys.exit(main())
|
|
@@ -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 54 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
|
|