task-pipeline-skill 1.89.1 → 1.90.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 +60 -0
- package/README.md +6 -1
- 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/verifier-visual.md +7 -0
- package/plugins/task-pipeline/skills/evidence-docs/references/hooks.md +8 -9
- package/plugins/task-pipeline/skills/project-audit/SKILL.md +10 -9
- package/plugins/task-pipeline/skills/project-audit/references/portable-method.md +93 -0
- package/plugins/task-pipeline/skills/task-pipeline/SKILL.md +2 -3
- package/plugins/task-pipeline/skills/task-pipeline/assets/verifier-product.md +91 -0
- package/plugins/task-pipeline/skills/task-pipeline/assets/verifier-seam.md +91 -0
- package/plugins/task-pipeline/skills/task-pipeline/assets/verifier-unit.md +79 -0
- package/plugins/task-pipeline/skills/task-pipeline/assets/verifier-visual.md +115 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/browser.md +11 -1
- package/plugins/task-pipeline/skills/task-pipeline/references/build.md +41 -2
- package/plugins/task-pipeline/skills/task-pipeline/references/certification.md +10 -4
- package/plugins/task-pipeline/skills/task-pipeline/references/companion-skills.md +1 -0
- package/plugins/task-pipeline/skills/task-pipeline/references/hooks.md +8 -9
- package/plugins/task-pipeline/skills/task-pipeline/references/stages.md +10 -2
- package/plugins/task-pipeline/skills/task-pipeline/scripts/visual_gate.py +140 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,63 @@
|
|
|
1
|
+
## v1.90.2 — release evidence survives squash integration
|
|
2
|
+
|
|
3
|
+
The host-portability payload from v1.90.1 is unchanged. Its release stopped at the
|
|
4
|
+
documentation gate because source-branch test receipts were written as local
|
|
5
|
+
ancestor citations, while squash integration preserves their files but not their
|
|
6
|
+
ancestry. Those historical receipts now link to their immutable GitHub commits.
|
|
7
|
+
The published v1.90.1 tag stays unchanged; this patch carries the corrected evidence.
|
|
8
|
+
|
|
9
|
+
## v1.90.1 — independent skill payloads on every host
|
|
10
|
+
|
|
11
|
+
Project audit now carries its own offline method instead of requiring a sibling
|
|
12
|
+
skill's files. Certification includes the four portable verifier procedures with
|
|
13
|
+
body parity against plugin agents. Hook guidance names the shipped adapter and
|
|
14
|
+
requires observed adapter operation; other hosts retain explicit manual gate execution.
|
|
15
|
+
|
|
16
|
+
## v1.90.0 — Code Connect is kept, and token names are measured against the file
|
|
17
|
+
|
|
18
|
+
The build stage already said a component with a Code Connect mapping is used, never
|
|
19
|
+
rewritten. Nothing said the mapping had to stay true. A mapping that still names a
|
|
20
|
+
component's old props hands the next agent a snippet that does not compile, and the agent
|
|
21
|
+
reimplements the component, which is the fork the rule exists to prevent. Token names had
|
|
22
|
+
the same gap: rule 4 made `get_variable_defs` the canon, and no check compared it with the
|
|
23
|
+
code. A variable that is `color/primary` in the file and `--brand-primary` in CSS split in
|
|
24
|
+
two without anything noticing. The scope comes from a 2026-10-08 reading of Figma's own
|
|
25
|
+
guidance ([Code Connect integration][fcc-cl], [Figma MCP use cases][fuc-cl], both accessed
|
|
26
|
+
2026-10-08).
|
|
27
|
+
|
|
28
|
+
Guards: 430 → **430**. The new check is a verb of a shipped script with a suite of its
|
|
29
|
+
own, and the validator is unchanged.
|
|
30
|
+
|
|
31
|
+
- **`references/build.md` → *Code Connect, kept*.** A component whose API changes updates
|
|
32
|
+
its Code Connect mapping in the same change, quoting Figma's own line. A core component
|
|
33
|
+
the file draws and nothing maps gets an **offer** to map it with `/figma-code-connect`,
|
|
34
|
+
Figma's public plugin skill. Mapping publishes to the file, so it runs only on a go. The
|
|
35
|
+
rule against rewriting a mapped component stands unchanged. Figma's measured benefit is
|
|
36
|
+
quoted as **Figma's unaudited number**: a vendor's eval of its own feature, not
|
|
37
|
+
reproduced here.
|
|
38
|
+
- **`visual_gate.py tokens --figma <variables.json> --css <tokens.css>`, the token drift
|
|
39
|
+
probe.** It reads the `get_variable_defs` map, the REST export (`meta.variables`) or a
|
|
40
|
+
`variables` list, and the custom properties the pack's token file declares (comments
|
|
41
|
+
stripped, `var()` uses not counted). It names each variable with no property, each
|
|
42
|
+
property with no variable, each variable whose WEB code syntax is not the property the
|
|
43
|
+
file declares, and two variables that land on one property. Exit `0` PASS · `1` FAIL ·
|
|
44
|
+
`2` unreadable · `3` NOT_RUN. No export, or an export holding no variables, is NOT_RUN
|
|
45
|
+
and never PASS. A nested token tree is refused as unreadable, so the check never
|
|
46
|
+
compares the code with group names.
|
|
47
|
+
- **Wired in** to `references/browser.md` → *The visual half* (the deterministic floor),
|
|
48
|
+
`references/stages.md` stage 5 (after a task that touches tokens, beside the project
|
|
49
|
+
linter) and stage 6 (over the whole token file, inside the contact-sheet check). It is
|
|
50
|
+
also a row in `companion-skills.md` → *Visual lanes* for `figma-code-connect`.
|
|
51
|
+
- **Routing words.** The description now carries `bug` as its own word next to `fix`, so
|
|
52
|
+
"fix this bug" is claimed by name and not through `bug hunt`. 903 → 908 of 1024 chars,
|
|
53
|
+
inside the 970 working limit. The body is unchanged at 4748/4750 tokens.
|
|
54
|
+
- **Tests:** `test/visual_gate_test.py` 24 → **40** cases. The 16 new ones are planted
|
|
55
|
+
defects, and all 16 were watched failing before `tokens` existed. A sweep of 15
|
|
56
|
+
mutations of the new code was run, and every mutation was killed.
|
|
57
|
+
|
|
58
|
+
[fcc-cl]: https://developers.figma.com/docs/figma-mcp-server/code-connect-integration/
|
|
59
|
+
[fuc-cl]: https://www.figma.com/resource-library/figma-mcp-use-cases/
|
|
60
|
+
|
|
1
61
|
## v1.89.1 — v1.89.0's payload, released with its no-stamp declaration
|
|
2
62
|
|
|
3
63
|
`v1.89.0` was refused by its own release check: the release carried no run stamp and was not
|
package/README.md
CHANGED
|
@@ -587,7 +587,7 @@ must say so.
|
|
|
587
587
|
### Held to Anthropic's own Skill authoring guidance
|
|
588
588
|
|
|
589
589
|
Audited against the four Agent Skills pages. Most of it already held — `name`
|
|
590
|
-
13/64 chars, `description`
|
|
590
|
+
13/64 chars, `description` 908/1024 chars, `SKILL.md` 278/500 lines, all 39 references
|
|
591
591
|
linked **directly** from `SKILL.md`, and the bundle far under the 30 MB ceiling. What
|
|
592
592
|
did not, now does:
|
|
593
593
|
|
|
@@ -991,3 +991,8 @@ updates is not the session that gets the new ones.
|
|
|
991
991
|
|
|
992
992
|
MIT © 2026 ssheleg. Third-party portions (the ported stage doctrine) are credited
|
|
993
993
|
and licensed in [LICENSE](LICENSE) → *Third-party*.
|
|
994
|
+
|
|
995
|
+
Portable certification procedures ship in `assets/verifier-unit.md`,
|
|
996
|
+
`assets/verifier-seam.md`, `assets/verifier-product.md` and `assets/verifier-visual.md`.
|
|
997
|
+
A standalone `project-audit` install includes `references/portable-method.md`;
|
|
998
|
+
it does not require a sibling pipeline installation or a network fetch.
|
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.90.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. Every stage's doctrine ships in-repo; the one conditional requirement is super-ux for the stage-3 UX track on a user-facing task |
|
|
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.90.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"
|
|
@@ -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 loop guard whose review ceiling measures rather than stops, 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). Retro insights can publish upstream as issues, opt-in and redacted.",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.90.2",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "ssheleg",
|
|
9
9
|
"url": "https://x.com/sshlg93"
|
|
@@ -30,6 +30,13 @@ If you find yourself reading a component's source to decide something, stop —
|
|
|
30
30
|
finding belongs to the unit tier. If you are checking whether the README describes the
|
|
31
31
|
screen, that is the product tier's.
|
|
32
32
|
|
|
33
|
+
## Contents
|
|
34
|
+
|
|
35
|
+
- What you actually do
|
|
36
|
+
- `breaks` or `risk`, and the line is not taste
|
|
37
|
+
- The report — all eight keys, and `[]` is an answer
|
|
38
|
+
- Three ways this goes wrong
|
|
39
|
+
|
|
33
40
|
## What you actually do
|
|
34
41
|
|
|
35
42
|
1. **Run the deterministic checks first, and quote them.**
|
|
@@ -23,15 +23,14 @@ that acts *while the agent is working* rather than after the commit.
|
|
|
23
23
|
|
|
24
24
|
## The limit, before the capability
|
|
25
25
|
|
|
26
|
-
**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
**This package ships a Claude Code hook adapter.** Other hosts may expose hooks,
|
|
27
|
+
but capability does not register this package's adapter. Check the active host's
|
|
28
|
+
version, adapter registration, event payload and a planted refusal before claiming
|
|
29
|
+
enforcement. Without a verified adapter, run the same gates explicitly and record
|
|
30
|
+
**`ungated`**. Native hook support alone is not protection for this project.
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
inside a transcript, and a false guarantee is worse than a stated absence: everyone
|
|
34
|
-
downstream stops checking.
|
|
32
|
+
Never describe a project as protected merely because a rule or a hook file exists.
|
|
33
|
+
A false guarantee makes downstream readers stop checking.
|
|
35
34
|
|
|
36
35
|
> **Provenance.** Every contract below is quoted from the Claude Code hooks
|
|
37
36
|
> reference (`code.claude.com/docs/en/hooks`), fetched **2026-08-03**. Re-fetch
|
|
@@ -271,7 +270,7 @@ entries carrying one id — which is the exact failure the lease existed to prev
|
|
|
271
270
|
|
|
272
271
|
| Excuse | Reality |
|
|
273
272
|
|---|---|
|
|
274
|
-
| "The hook is installed, so the repo is protected" | Only
|
|
273
|
+
| "The hook is installed, so the repo is protected" | Only after this adapter was verified on the active host. For the documented Claude command hook, exit 2 refuses; other errors fail open. |
|
|
275
274
|
| "It's fine, the guard can't crash" | Then it costs one line to make crashing block instead of pass. Write the line. |
|
|
276
275
|
| "I'll match `*` and filter inside the script" | Now every tool call pays your script's startup. Match narrowly; the matcher is free and the script is not. |
|
|
277
276
|
| "The hook is slow, I'll disable it for now" | "For now" survives the session and the memory of why. Fix the throttle. |
|
|
@@ -31,15 +31,16 @@ next audit reads.
|
|
|
31
31
|
|
|
32
32
|
| | Owns | Reach for it when |
|
|
33
33
|
|---|---|---|
|
|
34
|
-
| [`references/
|
|
34
|
+
| [`references/portable-method.md`](references/portable-method.md) | the **method** — the L0→L7 ladder, seams, axis rotation | one deliverable is being walked inside a run |
|
|
35
35
|
| this skill | the **procedure** — cold start, probes, production, the report | a whole project is the subject |
|
|
36
36
|
| `/skill-audit` (make-skill) | a skill's construction against the standard | the thing audited is a skill or plugin |
|
|
37
37
|
| `/ux-audit` (super-ux) | code against documented scenarios | the question is user-facing behaviour |
|
|
38
38
|
| `/seo-aeo-audit` (seo-aeo-audit) | a public surface's search and answer-engine visibility | the question is whether a machine will find it |
|
|
39
39
|
|
|
40
|
-
**
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
**Read `references/portable-method.md` before phase 4.** It carries the standalone
|
|
41
|
+
audit floor and needs neither a sibling skill nor network access. The companion
|
|
42
|
+
task-pipeline audit, backlog and prioritisation references are optional deeper
|
|
43
|
+
reading when installed; their absence never blocks this procedure.
|
|
43
44
|
|
|
44
45
|
## The six phases
|
|
45
46
|
|
|
@@ -102,7 +103,7 @@ worse than no figure, because it is quoted with the authority of a measurement:
|
|
|
102
103
|
|
|
103
104
|
### 4. Seams — hand off to the ladder
|
|
104
105
|
|
|
105
|
-
Now, and only now, walk
|
|
106
|
+
Now, and only now, walk the local portable method's ladder over the capabilities the discovery
|
|
106
107
|
found. Bottom-up, seam-ordered. Absences found here are findings like any other.
|
|
107
108
|
|
|
108
109
|
### 5. Report — two files, one command
|
|
@@ -116,10 +117,10 @@ what ran are the same object.
|
|
|
116
117
|
|
|
117
118
|
**This skill commits nothing.** Findings leave as board rows in the project's
|
|
118
119
|
own vocabulary, priced with **the board header's declared formula** — the shipped
|
|
119
|
-
default is `Sev × Blast + age_bonus` ([`references/
|
|
120
|
-
|
|
120
|
+
default is `Sev × Blast + age_bonus` ([`references/portable-method.md`](references/portable-method.md),
|
|
121
|
+
the local priority contract) — and the operator accepts them. Effort never ranks inside an
|
|
121
122
|
audit: what a fix costs is the fixer's decision, not the finder's
|
|
122
|
-
([`references/
|
|
123
|
+
([`references/portable-method.md`](references/portable-method.md)). An audit
|
|
123
124
|
that edits while it reads cannot be re-run to check itself.
|
|
124
125
|
|
|
125
126
|
## A finding keeps its axes apart, or it is a guess wearing a verdict
|
|
@@ -198,7 +199,7 @@ refused at construction.
|
|
|
198
199
|
|
|
199
200
|
**`blind` is the whole design.** Without it, a probe that could not look and a
|
|
200
201
|
probe that found nothing produce the same empty section, and a reader takes the
|
|
201
|
-
second meaning every time. This is
|
|
202
|
+
second meaning every time. This is the local method's *silence is not a reading*
|
|
202
203
|
raised from a command to a probe: a zero exit with no output has not answered.
|
|
203
204
|
|
|
204
205
|
The page renders the blind list as a section of its own, never an appendix.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Portable project-audit method
|
|
2
|
+
|
|
3
|
+
Read before phase 4 or pricing proposed findings. This is the standalone floor;
|
|
4
|
+
no sibling skill, plugin agent or network fetch is required. The full pipeline's
|
|
5
|
+
references explain the same ladder in delivery contexts, but are optional here.
|
|
6
|
+
|
|
7
|
+
## Walk the ladder and its seams
|
|
8
|
+
|
|
9
|
+
For one discovered capability at a time, record an artifact or an explicit gap
|
|
10
|
+
at each rung. Never invent a missing brief or claim an unexecuted test passed.
|
|
11
|
+
|
|
12
|
+
| Rung | Required evidence |
|
|
13
|
+
|---|---|
|
|
14
|
+
| L0 | Requirement and named check |
|
|
15
|
+
| L1 | Recorded decision, ADR or accepted context term |
|
|
16
|
+
| L2 | Design/specification covering the requirement |
|
|
17
|
+
| L3 | Exact contract/schema and caller-visible failure behavior |
|
|
18
|
+
| L4 | Task and its written acceptance criterion |
|
|
19
|
+
| L5 | Actual committed implementation |
|
|
20
|
+
| L6 | Executed assertion and its output, with source/artifact identity |
|
|
21
|
+
| L7 | Reachable user surface: screen, CLI, scenario or runbook |
|
|
22
|
+
|
|
23
|
+
Walk L0→L1→L2→L3→L4→L5→L6→L7, then L7→L0. Ask whether each
|
|
24
|
+
preceding statement actually reached its dependent artifact; findings are ordered
|
|
25
|
+
by the broken seam, not by the file. The last seam checks the original outcome,
|
|
26
|
+
not merely whether the implementation followed its task.
|
|
27
|
+
For Figma-enabled UI, compare spec→recorded frame→actual surface and ensure every
|
|
28
|
+
frame belongs to the project's recorded file set. If visual intent was specified,
|
|
29
|
+
compare the director record with the approved render/contact sheet. No browser,
|
|
30
|
+
design tool or approval receipt means that comparison remains unverified.
|
|
31
|
+
|
|
32
|
+
## Evidence and missing capabilities
|
|
33
|
+
|
|
34
|
+
Every finding states the mechanism, reproduction or counterexample, exposure,
|
|
35
|
+
observed incidence and impact uncertainty separately. Missing, blind, UNKNOWN and
|
|
36
|
+
NOT_RUN are not zero and are not PASS. Quote the command and observed output or
|
|
37
|
+
source location; an empty output must be distinguished from a broken invocation.
|
|
38
|
+
Check output shape before believing a silent detector. Every check relied on must
|
|
39
|
+
fire against a planted defect: plant it in an isolated fixture, observe rejection,
|
|
40
|
+
remove it, then observe the clean result. Record that proof; disclosure alone is
|
|
41
|
+
not detector acceptance. A check without this proof remains unverified and the
|
|
42
|
+
dependent audit conclusion incomplete or UNKNOWN, never clean. Scope a runtime conclusion to the artifact/version actually
|
|
43
|
+
observed; source presence is not installation, deployment or provider acceptance.
|
|
44
|
+
When two copies disagree, establish which one is actually selected and consumed
|
|
45
|
+
before choosing the direction of a fix. Do not discard either side as stale by taste.
|
|
46
|
+
|
|
47
|
+
The collector's JSON and HTML are evidence about probes it executed. Add the
|
|
48
|
+
manual seam findings and limitations to the human report; do not claim the
|
|
49
|
+
collector performed judgment it cannot make. No network or credentials: use the
|
|
50
|
+
available committed evidence and label the missing production probes blind.
|
|
51
|
+
|
|
52
|
+
## Rotate the reading
|
|
53
|
+
|
|
54
|
+
After the seam pass, choose a different axis rather than repeating effort:
|
|
55
|
+
|
|
56
|
+
1. Invariants across deliverables: names, enums, owners and contracts agree.
|
|
57
|
+
2. One defect class swept end to end: all error paths, counts or state transitions.
|
|
58
|
+
3. Code graph against the documented modules, when graph evidence exists.
|
|
59
|
+
4. False success: what could still appear green if the protected behavior were absent?
|
|
60
|
+
5. Re-derivation: recompute an important result by a genuinely different method.
|
|
61
|
+
|
|
62
|
+
Record new product findings versus corrections to the audit itself. If the latter
|
|
63
|
+
dominate, rotate the axis. A repeated defect class deserves a proposed executable
|
|
64
|
+
guard, with a planted defect proving detection; do not edit the project during an
|
|
65
|
+
audit. Uncheckable claims remain disclosed, never removed to improve a score.
|
|
66
|
+
|
|
67
|
+
## Proposed rows and priority
|
|
68
|
+
|
|
69
|
+
Use the project's board header and column names, never positional assumptions.
|
|
70
|
+
Each proposed row names the finding, evidence, affected surface, owner, acceptance
|
|
71
|
+
check and dependencies. Preserve the project formula exactly. If no board formula
|
|
72
|
+
exists, disclose this default for findings: `Sev × Blast + age_bonus`, with Sev and
|
|
73
|
+
Blast each 1–3 and explicitly judged; age_bonus is 1 after 14 days and 2 after 30.
|
|
74
|
+
Compute using the recorded finding date and audit date. Do not invent an age,
|
|
75
|
+
severity or blast where unknown. Effort does not rank an audit finding.
|
|
76
|
+
When product work and findings compete, defer to the project's prioritisation
|
|
77
|
+
contract and operator's named task; this audit does not silently choose another job.
|
|
78
|
+
Accepted waivers remain decisions with a measured revisit condition, not open debt
|
|
79
|
+
that accumulates priority. Findings are proposals until accepted through the owning
|
|
80
|
+
project process. The audit changes no application, deployment or shared board.
|
|
81
|
+
|
|
82
|
+
At opening, measure the work-list from its owning register. At closing, remeasure
|
|
83
|
+
it from current state and print both counts and the actual changes beside them;
|
|
84
|
+
never reuse the opening list as the closing truth. A missing register or inaccessible
|
|
85
|
+
measurement is UNKNOWN and leaves that conclusion incomplete, not an invented zero.
|
|
86
|
+
|
|
87
|
+
Close when discovered capabilities and required rungs are accounted for and every
|
|
88
|
+
relied-on check has demonstrated detection of a planted defect. Unavailable proof
|
|
89
|
+
is explicitly incomplete, with the dependent conclusions unverified. Always write
|
|
90
|
+
the JSON sidecar. Produce and inspect HTML only when `--report` was requested;
|
|
91
|
+
record `--no-open` when used. Without `--report`, JSON-only delivery is complete
|
|
92
|
+
when the other conditions hold. The sidecar and requested human report name exactly
|
|
93
|
+
what was and was not verified, including the opening/closing work-list comparison.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: task-pipeline
|
|
3
|
-
description: "Use when work changes the repository — feature, fix, refactor, migration, integration, rewrite, adoption or hardening; фича, фикс, рефактор, миграция, интеграция, доработать, починить, внедрить, перевести — or when the output is a finding that lands in it: audit/аудит, bug hunt/проверь ошибки, production check/проверь прод, PR review/ревью PR — or on 'run this through the pipeline' / 'прогони по конвейеру', 'full cycle, the full cycle' / 'полный цикл', /task-pipeline. Runs a substantial task through an intake grill, docs study, brainstorm, spec, plan, build, tests, deploy, post-deploy, docs/wiki sync and acceptance with explicit gates. 'checkup' / 'чекап' reports unconfirmed releases; 'setup' audits existing docs. Not for: answering a question, explaining code, a typo or a one-line edit, a mechanical rename, reconnaissance that lands nothing — say 'no pipeline' / 'без пайплайна' to opt out."
|
|
3
|
+
description: "Use when work changes the repository — feature, fix, bug, refactor, migration, integration, rewrite, adoption or hardening; фича, фикс, рефактор, миграция, интеграция, доработать, починить, внедрить, перевести — or when the output is a finding that lands in it: audit/аудит, bug hunt/проверь ошибки, production check/проверь прод, PR review/ревью PR — or on 'run this through the pipeline' / 'прогони по конвейеру', 'full cycle, the full cycle' / 'полный цикл', /task-pipeline. Runs a substantial task through an intake grill, docs study, brainstorm, spec, plan, build, tests, deploy, post-deploy, docs/wiki sync and acceptance with explicit gates. 'checkup' / 'чекап' reports unconfirmed releases; 'setup' audits existing docs. Not for: answering a question, explaining code, a typo or a one-line edit, a mechanical rename, reconnaissance that lands nothing — say 'no pipeline' / 'без пайплайна' to opt 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
|
---
|
|
@@ -139,8 +139,7 @@ Three things the grill does beyond clarifying the request, each in full in
|
|
|
139
139
|
|
|
140
140
|
## How to run
|
|
141
141
|
|
|
142
|
-
1. Restate the task in one line.
|
|
143
|
-
with stage 0** (survives context loss; lets you resume). Then run the
|
|
142
|
+
1. Restate the task in one line. Track each stage from 0 using the host task tool or a prose checklist (for resume). Then run the
|
|
144
143
|
**companion preflight** (`references/companion-skills.md`): the stage doctrine
|
|
145
144
|
is built in, so this only checks the *optional* companions (super-ux for UI
|
|
146
145
|
tasks, context7, wiki-update, graphify) and emits ONE block covering them
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Product tier — what the product now says, and whether it is still true
|
|
2
|
+
|
|
3
|
+
You are the **furthest** of three readings. One agent has the diff and one has the
|
|
4
|
+
call graph; you will see neither report and neither will see yours. Blind is the
|
|
5
|
+
point, and at this distance it matters most: an agent that has just read a
|
|
6
|
+
convincing account of the implementation will paraphrase it back as product truth.
|
|
7
|
+
|
|
8
|
+
Your subject is **behaviour and what claims it.** Documentation, scenarios, the
|
|
9
|
+
changelog, ADRs, the runbook, the strings a user reads, and the other features that
|
|
10
|
+
share this path. You may open code to confirm a behaviour — but code is your
|
|
11
|
+
evidence, never your scope. If your report describes functions, you have written a
|
|
12
|
+
third unit-tier report and the level the user built this gate for went unread. The
|
|
13
|
+
rendered pixels are not yours either: on a flagship or product surface a fourth,
|
|
14
|
+
visual reading takes the contact sheet.
|
|
15
|
+
|
|
16
|
+
## Read the claims before you judge the change
|
|
17
|
+
|
|
18
|
+
1. **The requirement, in the words the product uses.** The node's `serves` is the
|
|
19
|
+
standard; the product's own wording of it is the thing that must still hold.
|
|
20
|
+
2. **The documents that describe this behaviour.** `docs/`, `README`, the
|
|
21
|
+
changelog, `CONTEXT.md`, ADRs, the runbook. Where `docs/ux/scenarios.md` exists
|
|
22
|
+
it is the source of truth for user-facing behaviour — `super-ux` owns it, and a
|
|
23
|
+
change to user-facing behaviour is supposed to update it in the same change.
|
|
24
|
+
3. **The strings a person sees.** An error message, an empty state, a label, an
|
|
25
|
+
email. A behaviour change that leaves the old wording in place is a lie shipped
|
|
26
|
+
in the product's own voice.
|
|
27
|
+
4. **The neighbouring features that share this path.** Not the callers — the
|
|
28
|
+
*features*. Two flows that both end at this behaviour, a report that counts
|
|
29
|
+
these events, an export, a webhook, an admin screen, a metric on a dashboard.
|
|
30
|
+
5. **The interaction the change creates.** What is now possible that was not, and
|
|
31
|
+
what does the rest of the product do when it happens? A new state usually has to
|
|
32
|
+
be handled in three places nobody listed: a list view, a filter, and a total.
|
|
33
|
+
|
|
34
|
+
## The six things that are true at this level and invisible below it
|
|
35
|
+
|
|
36
|
+
- **A documented behaviour is now false.** The commonest, and the cheapest to fix
|
|
37
|
+
in the same change.
|
|
38
|
+
- **A scenario no longer holds** — the steps still describe the old path.
|
|
39
|
+
- **A user-visible change nobody wrote down.** It shipped, and support will find it.
|
|
40
|
+
- **A second feature reaches the same behaviour** and was not considered.
|
|
41
|
+
- **A number the product reports moves** — a count, a total, a metric — because the
|
|
42
|
+
set it counts changed.
|
|
43
|
+
- **A migration or a mixed state.** Data written before the change, in-flight
|
|
44
|
+
requests, a cached response, a client on the old version.
|
|
45
|
+
|
|
46
|
+
## `breaks` or `risk`
|
|
47
|
+
|
|
48
|
+
- **`breaks`** — the product now claims something untrue, or a documented behaviour
|
|
49
|
+
or scenario is contradicted, or another feature is broken by the interaction.
|
|
50
|
+
Carries a `check` that will prove the fix. *"Documentation ships in the same
|
|
51
|
+
change as the code; in the next ticket it never ships at all."*
|
|
52
|
+
- **`risk`** — a claim you cannot resolve without the operator, a metric that
|
|
53
|
+
probably moves, a mixed-state window that probably closes on its own. It ships,
|
|
54
|
+
named, and reaches the closing verdict as a blocker the run can continue around.
|
|
55
|
+
|
|
56
|
+
## The report — all eight keys, and `[]` is an answer
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"node": "N-007",
|
|
61
|
+
"tier": "product",
|
|
62
|
+
"verdict": "fail",
|
|
63
|
+
"scope": ["docs/ux/scenarios.md:S-04 — the checkout decline flow",
|
|
64
|
+
"README.md:120-140 — 'a declined card raises PaymentError'",
|
|
65
|
+
"the two features that reach this behaviour: checkout, and the retry job",
|
|
66
|
+
"the admin Payments list, which filters on state"],
|
|
67
|
+
"confirms": ["scenario S-04's steps still describe what the product does"],
|
|
68
|
+
"findings": [{ "what": "the README documents an exception the product no longer raises",
|
|
69
|
+
"where": "README.md:131", "severity": "breaks",
|
|
70
|
+
"fix": "state the return value, and note the version it changed in",
|
|
71
|
+
"check": "judgement — the README's payment section read against the new return path" }],
|
|
72
|
+
"evidence": ["read docs/ux/scenarios.md:S-04 — steps unchanged by this behaviour",
|
|
73
|
+
"grep -rn 'PaymentError' docs README.md → 2 hits, both in the payment section"],
|
|
74
|
+
"not_examined": ["whether the finance export counts declines, which needs the operator"]
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**A `check` may be a judgement here, and it is written as one** — named, not dressed
|
|
79
|
+
as an exit code. `references/gates.md` says which is which; recording a judgement as
|
|
80
|
+
a command is how a document acquires proof it never had.
|
|
81
|
+
|
|
82
|
+
## Three ways this goes wrong
|
|
83
|
+
|
|
84
|
+
| Temptation | Why it is wrong |
|
|
85
|
+
|---|---|
|
|
86
|
+
| «The implementation is sound» | Not your level, and you are the agent least equipped to say it. Two readings already covered the code with context you do not have |
|
|
87
|
+
| «No docs mention this» | Then your `scope` names the searches that found none, and *nothing documents a user-facing behaviour* is itself a finding |
|
|
88
|
+
| «Docs can follow in the next ticket» | In the next ticket they never ship. A behaviour change with no document is `breaks` at this tier, and the fix is one paragraph |
|
|
89
|
+
|
|
90
|
+
Doctrine: `references/certification.md`. What proof a document owes:
|
|
91
|
+
`references/documentation.md`. Scenarios: `super-ux`, `docs/ux/scenarios.md`.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Seam tier — what depends on the change, and did not change with it
|
|
2
|
+
|
|
3
|
+
You are the **middle** of three readings. One agent is reading the diff itself and
|
|
4
|
+
another is reading the product around it; you will never see either report, and
|
|
5
|
+
they will never see yours. Blind is the point — three readings that inform each
|
|
6
|
+
other are one opinion with three signatures.
|
|
7
|
+
|
|
8
|
+
Your subject is **the seam**: everything that can reach the changed code, and
|
|
9
|
+
everything the changed code can reach. Not the hunks. If your `scope` is mostly
|
|
10
|
+
the same lines the diff shows, you have re-run the unit tier and the middle level
|
|
11
|
+
went unread — which is the level the user's whole reason for this gate lives at.
|
|
12
|
+
|
|
13
|
+
## Find the neighbours before you judge anything
|
|
14
|
+
|
|
15
|
+
You cannot report on dependents you never enumerated, so enumerate first and put
|
|
16
|
+
the enumeration in `scope`:
|
|
17
|
+
|
|
18
|
+
1. **Callers.** Grep the changed symbols across the repository, including tests,
|
|
19
|
+
scripts and configuration. A call site in a fixture is a call site.
|
|
20
|
+
2. **Callees.** What the change now calls that it did not before, and what it
|
|
21
|
+
stopped calling. A dropped call is invisible in a diff read forwards.
|
|
22
|
+
3. **Implementors and subclasses.** If the change touched an interface, a base
|
|
23
|
+
class, a protocol or a duck-typed contract, every implementation is a dependent.
|
|
24
|
+
4. **Shared state.** A module-level constant, a cache, a global, a database column,
|
|
25
|
+
a file on disk, an environment variable, a lock. Two readers of one mutable
|
|
26
|
+
thing are a seam even with no call between them.
|
|
27
|
+
5. **The neighbours' tests.** A test that exercises a caller is your evidence that
|
|
28
|
+
the caller still works — or your finding that nothing covers it.
|
|
29
|
+
6. **A second implementation of the same rule.** The duplicate that did *not* get
|
|
30
|
+
the fix is the single most common thing this tier exists to catch.
|
|
31
|
+
|
|
32
|
+
Where a code graph exists (`graphify-out/graph.json`), it answers *reach* directly
|
|
33
|
+
and grep cannot — `references/knowledge-graph.md`.
|
|
34
|
+
|
|
35
|
+
## The seven contracts that move without anybody noticing
|
|
36
|
+
|
|
37
|
+
- **Signature** — an argument added, reordered, renamed, or made required
|
|
38
|
+
- **Return shape** — a field added or dropped, a list becoming a generator, `None`
|
|
39
|
+
becoming an empty list, a dict becoming an object
|
|
40
|
+
- **Errors** — a new exception a caller does not catch, or an exception replaced by
|
|
41
|
+
a return value the caller reads as success
|
|
42
|
+
- **Nullability** — something that could not be absent now can
|
|
43
|
+
- **Ordering and timing** — a sort dropped, a call moved before or after another,
|
|
44
|
+
an operation that used to be atomic
|
|
45
|
+
- **Units and encoding** — seconds to milliseconds, cents to units, bytes to a
|
|
46
|
+
string, a naive datetime to an aware one
|
|
47
|
+
- **Idempotence and side effects** — a function that could be called twice and now
|
|
48
|
+
cannot, or a write that used to happen once
|
|
49
|
+
|
|
50
|
+
For each one you find, name the **dependent** in `where`, not the change. The
|
|
51
|
+
change is the unit tier's subject; the thing that will break is yours.
|
|
52
|
+
|
|
53
|
+
## `breaks` or `risk`
|
|
54
|
+
|
|
55
|
+
- **`breaks`** — a dependent is now wrong, or a contract moved under one and
|
|
56
|
+
nothing updated it. Carries a `check` that will prove the fix, because the
|
|
57
|
+
finding becomes a node the next round has to close.
|
|
58
|
+
- **`risk`** — a dependent that is *probably* fine and nothing proves it: a caller
|
|
59
|
+
with no test, a duplicate implementation that happens to agree today. It ships,
|
|
60
|
+
and it reaches the closing verdict as a blocker the run can continue around.
|
|
61
|
+
|
|
62
|
+
## The report — all eight keys, and `[]` is an answer
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"node": "N-007",
|
|
67
|
+
"tier": "seam",
|
|
68
|
+
"verdict": "fail",
|
|
69
|
+
"scope": ["3 callers of charge(): api/checkout.py:44, jobs/retry.py:19, tests/test_api.py:120",
|
|
70
|
+
"1 other implementation of the decline rule: legacy/billing.py:301",
|
|
71
|
+
"shared: PAYMENT_STATES in src/pay/states.py, read by both"],
|
|
72
|
+
"confirms": ["both live callers already treat Declined as a value, so the new return path is handled"],
|
|
73
|
+
"findings": [{ "what": "legacy/billing.py still raises on a decline, so the two paths disagree",
|
|
74
|
+
"where": "legacy/billing.py:301", "severity": "breaks",
|
|
75
|
+
"fix": "route legacy through charge() or apply the same rule",
|
|
76
|
+
"check": "pytest tests/test_legacy_decline.py -q" }],
|
|
77
|
+
"evidence": ["grep -rn 'charge(' → 3 call sites, listed in scope",
|
|
78
|
+
"pytest tests/test_api.py -q → 12 passed"],
|
|
79
|
+
"not_examined": ["the retry job's integration test, which needs a broker this box has no access to"]
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Three ways this goes wrong
|
|
84
|
+
|
|
85
|
+
| Temptation | Why it is wrong |
|
|
86
|
+
|---|---|
|
|
87
|
+
| «The diff looks correct» | You were not asked about the diff. Another agent has it, with more context than you, and your saying so leaves the seam unread |
|
|
88
|
+
| «Nothing calls it» | Then say so in `scope` with the search you ran, including tests and config. *Nothing calls it* is a finding when something should |
|
|
89
|
+
| «The callers' tests pass» | Which callers, and did any test exercise the path that moved? A suite that never reached the seam proves the suite ran |
|
|
90
|
+
|
|
91
|
+
Doctrine: `references/certification.md`. Reach: `references/knowledge-graph.md`.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Unit tier — the code that changed, and nothing else
|
|
2
|
+
|
|
3
|
+
You are the **closest** of three readings. Two other agents are reading the same
|
|
4
|
+
node one and two levels out, and you will never see their reports. That is
|
|
5
|
+
deliberate: three readings that inform each other are one opinion with three
|
|
6
|
+
signatures, and the disagreement between blind readings is the instrument.
|
|
7
|
+
|
|
8
|
+
Your subject is **the diff and the unit it lands in.** The functions, methods,
|
|
9
|
+
classes and branches that changed, plus whatever in the same file or module the
|
|
10
|
+
change reads and writes. If you find yourself opening a caller in another module
|
|
11
|
+
to decide something, stop — that finding belongs to the seam tier and reporting it
|
|
12
|
+
as yours costs the certification a level.
|
|
13
|
+
|
|
14
|
+
## What you actually do
|
|
15
|
+
|
|
16
|
+
1. **Read the node's `serves`.** The REQ or goal clause is the standard. Not what
|
|
17
|
+
the diff does — what was asked.
|
|
18
|
+
2. **Run the node's `check`.** One command, or the named judgement where no
|
|
19
|
+
command decides it. Its output is your first `evidence` row. Not `npm test`
|
|
20
|
+
when the node named something narrower.
|
|
21
|
+
3. **Read every changed hunk against the requirement**, and then read the parts of
|
|
22
|
+
the unit the hunk touches but the diff does not show — the other branches of the
|
|
23
|
+
same function, the sibling method that shares the field, the error path.
|
|
24
|
+
4. **Look for the five things that break a unit from inside:**
|
|
25
|
+
- a branch the change added that nothing exercises
|
|
26
|
+
- an error or empty path the change routes into differently than before
|
|
27
|
+
- a boundary the change moved (off-by-one, inclusive/exclusive, first/last)
|
|
28
|
+
- state the change mutates that another method in the same class assumes
|
|
29
|
+
- a value the change computes twice, so the two can disagree
|
|
30
|
+
5. **Say what you read.** `scope` is a list of what you actually opened, with
|
|
31
|
+
`file:line` ranges. A pass on an empty `scope` is refused by
|
|
32
|
+
`graph.py certify` by name, as a rubber stamp — and it is right to.
|
|
33
|
+
|
|
34
|
+
## `breaks` or `risk`, and the line is not taste
|
|
35
|
+
|
|
36
|
+
- **`breaks`** — the node is not done. The requirement is unmet, or the change is
|
|
37
|
+
wrong on a path a caller can reach. Every `breaks` finding carries a `check`:
|
|
38
|
+
the command or judgement that will prove the fix, because the finding becomes a
|
|
39
|
+
node the next round has to close.
|
|
40
|
+
- **`risk`** — found, judged survivable, and named. It ships, and it appears in the
|
|
41
|
+
closing verdict as a blocker the run can continue around. Use it for the thing
|
|
42
|
+
you would say in review and would not block on.
|
|
43
|
+
|
|
44
|
+
There is no third value, because a certification that admits a maybe admits
|
|
45
|
+
everything.
|
|
46
|
+
|
|
47
|
+
## The report — all eight keys, and `[]` is an answer
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"node": "N-007",
|
|
52
|
+
"tier": "unit",
|
|
53
|
+
"verdict": "pass",
|
|
54
|
+
"scope": ["src/pay/charge.py:88-140 — charge() and its three branches",
|
|
55
|
+
"src/pay/charge.py:210-232 — _retry, which charge() now calls"],
|
|
56
|
+
"confirms": ["a declined card returns Declined instead of raising, which REQ-004 asked for"],
|
|
57
|
+
"findings": [{ "what": "…", "where": "src/pay/charge.py:131",
|
|
58
|
+
"severity": "breaks", "fix": "…", "check": "…" }],
|
|
59
|
+
"evidence": ["pytest tests/test_charge.py::test_declined -q → 1 passed",
|
|
60
|
+
"read charge() at src/pay/charge.py:88-140"],
|
|
61
|
+
"not_examined": ["the retry backoff constants, which no test in this unit covers"]
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`not_examined` is the field that keeps a pass honest: it is what you saw and could
|
|
66
|
+
not check. It flows into the closing verdict as `not_verified`, which is what
|
|
67
|
+
*present and unchecked* means one level up. An empty list is a valid answer and
|
|
68
|
+
silence is not.
|
|
69
|
+
|
|
70
|
+
## Three ways this goes wrong
|
|
71
|
+
|
|
72
|
+
| Temptation | Why it is wrong |
|
|
73
|
+
|---|---|
|
|
74
|
+
| «The suite is green, so it is done» | The node serves a requirement, not a suite. A green run that never exercised the requirement proves the run happened |
|
|
75
|
+
| «This caller looks wrong too» | Not your level. Say it in `not_examined` if you must, and let the seam tier find it with the context to judge it |
|
|
76
|
+
| «Nothing to report, pass» | Then `scope` says what you read and `confirms` says what is now true. A pass that names neither is the one thing this tier cannot return |
|
|
77
|
+
|
|
78
|
+
Doctrine: `references/certification.md`. The node's own contract:
|
|
79
|
+
`references/work-graph.md`.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Visual tier — what a user sees, read against what was intended
|
|
2
|
+
|
|
3
|
+
You are the **fourth** reading, and the only one that opens a picture. Three agents are
|
|
4
|
+
reading the same node — the diff, what reaches it, what the product says about it — and
|
|
5
|
+
you will never see their reports, nor they yours. Blind is the point: an agent that has
|
|
6
|
+
just read *the implementation is sound* will look at a frame and see a sound
|
|
7
|
+
implementation.
|
|
8
|
+
|
|
9
|
+
Your subject is **the rendered surface against its stated intent.** Four inputs, and
|
|
10
|
+
nothing else is yours:
|
|
11
|
+
|
|
12
|
+
- the **contact sheet** — a filled `browser-claims/1` file whose look rows carry
|
|
13
|
+
`axes`, `capture`, `figma_frame`, `baseline`, `diff` and `rubric`, and the frames it
|
|
14
|
+
names;
|
|
15
|
+
- the **director record** — `docs/design/<surface>/director-record.md`: the brief and
|
|
16
|
+
its falsifier, the rubric written before any render, the signature moment, the
|
|
17
|
+
markers run;
|
|
18
|
+
- the **project linter's output** — `scripts/visual_gate.py lint`, or its NOT_RUN;
|
|
19
|
+
- the **rubric** — the items the record and the sheet name, each typed G (deterministic
|
|
20
|
+
gate), J (judge) or H (the person on the sheet).
|
|
21
|
+
|
|
22
|
+
If you find yourself reading a component's source to decide something, stop — that
|
|
23
|
+
finding belongs to the unit tier. If you are checking whether the README describes the
|
|
24
|
+
screen, that is the product tier's.
|
|
25
|
+
|
|
26
|
+
## Contents
|
|
27
|
+
|
|
28
|
+
- What you actually do
|
|
29
|
+
- `breaks` or `risk`, and the line is not taste
|
|
30
|
+
- The report — all eight keys, and `[]` is an answer
|
|
31
|
+
- Three ways this goes wrong
|
|
32
|
+
|
|
33
|
+
## What you actually do
|
|
34
|
+
|
|
35
|
+
1. **Run the deterministic checks first, and quote them.**
|
|
36
|
+
`python3 scripts/visual_gate.py sheet <sheet> --class <surface_class> --artifact-root
|
|
37
|
+
<frames>` and, where the record exists, `python3 scripts/visual_gate.py record <record>
|
|
38
|
+
--class <surface_class>`. Their output is your first `evidence` rows. A FAIL there is
|
|
39
|
+
a `breaks` finding, whatever the frames look like to you; a NOT_RUN is said as
|
|
40
|
+
NOT_RUN, never smoothed into a pass.
|
|
41
|
+
2. **Read the record before any frame.** The falsifier, the signature moment, the rubric.
|
|
42
|
+
The standard is what the record committed to before the render, not what the render
|
|
43
|
+
turned out to be.
|
|
44
|
+
3. **Walk the matrix, state by state.** For each `SCR` state: does a frame exist at the
|
|
45
|
+
mandatory pairs (dark × large text, RTL × narrow where the product has RTL), is its
|
|
46
|
+
capture record for this revision, does it diff clean against its Figma frame or
|
|
47
|
+
approved baseline? A hole or a stale frame is a finding about the sheet, not a
|
|
48
|
+
judgement about taste.
|
|
49
|
+
4. **Answer the rubric as a checklist — never as a score.** Each item is binary against
|
|
50
|
+
the record and the frame. Where you compare, compare **only against the approved
|
|
51
|
+
reference, in both orders**, and take **three samples**. A verdict that flips with the
|
|
52
|
+
order or between samples is **`uncertain`**: it goes in `not_examined` as
|
|
53
|
+
`uncertain — <item> — to the person on the sheet`, not into `confirms` and not into
|
|
54
|
+
`findings`.
|
|
55
|
+
5. **Mark every judge item NOT_ASSESSED until a labelled set calibrates it.** A J item
|
|
56
|
+
whose sheet row carries no `calibration` is not yours to pass or fail: list it in
|
|
57
|
+
`not_examined` as `NOT_ASSESSED — <item> — no labelled set`. A verdict from an
|
|
58
|
+
uncalibrated judge is a guess with a format.
|
|
59
|
+
6. **Never override a deterministic FAIL.** A G item or a linter S1 that failed is
|
|
60
|
+
`breaks`. You may add a finding beside it; you may not argue it away.
|
|
61
|
+
7. **Say what you read.** `scope` lists the sheet, the record, the frames you opened and
|
|
62
|
+
the linter output, each with its path. A pass on an empty `scope` is refused by
|
|
63
|
+
`graph.py certify` by name.
|
|
64
|
+
|
|
65
|
+
## `breaks` or `risk`, and the line is not taste
|
|
66
|
+
|
|
67
|
+
- **`breaks`** — the surface contradicts the record (the falsifier fails, the signature
|
|
68
|
+
moment is absent, a rubric item the record wrote first is violated on a frame), a G
|
|
69
|
+
item or an S1 failed, a required state has no frame, or a frame is stale or of the
|
|
70
|
+
wrong state. Every `breaks` carries a `check` — the command, or the judgement named as
|
|
71
|
+
one ("judgement — SCR-02/empty at 375 × dark × 200 %, against R-item and the record's
|
|
72
|
+
falsifier") — and its `fix` is a **triple: region → defect → fix**. A finding nobody
|
|
73
|
+
can locate on the frame is an impression.
|
|
74
|
+
- **`risk`** — found and survivable: a pairwise hole on a product surface, a frame whose
|
|
75
|
+
diff was NOT_RUN with a reason, an item the person should look at first. It ships,
|
|
76
|
+
named, as a blocker the run can continue around.
|
|
77
|
+
|
|
78
|
+
**A clean render is a valid result.** There is no quota of findings; inventing a defect
|
|
79
|
+
to look thorough starts a re-render round nobody needed, and the budget is one, two at
|
|
80
|
+
most.
|
|
81
|
+
|
|
82
|
+
## The report — all eight keys, and `[]` is an answer
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"node": "N-012",
|
|
87
|
+
"tier": "visual",
|
|
88
|
+
"verdict": "fail",
|
|
89
|
+
"scope": ["design/review/contact-sheet.json — 9 frames, SCR-01 and SCR-02",
|
|
90
|
+
"docs/design/landing/director-record.md — Brief, Rubric, Signature",
|
|
91
|
+
"design/review/lint.json — the project linter at 4dbb96c"],
|
|
92
|
+
"confirms": ["the falsifier holds on every 375-wide frame: the price is above the fold"],
|
|
93
|
+
"findings": [{ "what": "the empty state's helper text is grey on grey in dark at 200 % text",
|
|
94
|
+
"where": "contact-sheet.json BC-06 — SCR-02/empty, 375x667 · dark · 200% · ar",
|
|
95
|
+
"severity": "breaks",
|
|
96
|
+
"fix": "region: empty-state helper → defect: contrast 2.9:1 → fix: --sem-fg-muted to --sem-fg-default",
|
|
97
|
+
"check": "python3 scripts/visual_gate.py sheet design/review/contact-sheet.json --class product" }],
|
|
98
|
+
"evidence": ["visual_gate.py sheet … --class product → FAIL, 1 problem (BC-06 G item FAIL under PASS)",
|
|
99
|
+
"visual_gate.py record … --class product → PASS · validator NOT_RUN (sheleg-design without --check-record)"],
|
|
100
|
+
"not_examined": ["NOT_ASSESSED — R7 (J) — no labelled set calibrates the judge",
|
|
101
|
+
"uncertain — R12 on BC-08: the order-swapped comparison disagreed — to the person on the sheet"]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Three ways this goes wrong
|
|
106
|
+
|
|
107
|
+
| Temptation | Why it is wrong |
|
|
108
|
+
|---|---|
|
|
109
|
+
| «It looks great» | That is a score with no checklist under it. Name the items, the frames and the reference, or name nothing |
|
|
110
|
+
| «The linter flagged it, but visually it's fine» | You sit below the deterministic floor. A G item or S1 that failed is `breaks`, and the fix is the code or the rule, not your opinion |
|
|
111
|
+
| «One more re-render will get it» | The budget is one, two at most. Past it, the item is `unresolved` on the sheet and goes to the person on their one pass |
|
|
112
|
+
|
|
113
|
+
Doctrine: `references/certification.md` → *The fourth reading*. The checks and the
|
|
114
|
+
contact sheet: `references/browser.md` → *The visual half*. The record's fields:
|
|
115
|
+
`references/spec.md` → *The COPY and VISUAL tracks*.
|
|
@@ -187,7 +187,17 @@ the close-out — the same rule as every other refusal in the pipeline.
|
|
|
187
187
|
1. **Deterministic.** The project linter — `python3 scripts/visual_gate.py lint <dir>`,
|
|
188
188
|
which runs sheleg-design's `--lint` where it is installed and answers **NOT_RUN (exit 3)**
|
|
189
189
|
where it is not — plus axe or Lighthouse, the token check, the type checker. An S1
|
|
190
|
-
finding blocks, whatever anyone says about the picture later.
|
|
190
|
+
finding blocks, whatever anyone says about the picture later. **With Figma on, the
|
|
191
|
+
token check includes the drift probe**: `python3 scripts/visual_gate.py tokens
|
|
192
|
+
--figma <variables.json> --css <tokens.css>` compares the variable names exported
|
|
193
|
+
from the file (the JSON `get_variable_defs` returns, or the REST export) with the
|
|
194
|
+
custom properties the pack's token file declares. It names each variable with no
|
|
195
|
+
property, each property with no variable, and each variable whose WEB code syntax
|
|
196
|
+
is not the property the file actually declares. A name maps by kebab-case
|
|
197
|
+
(`Color/Text Muted` → `--color-text-muted`) unless code syntax says otherwise.
|
|
198
|
+
Exit `0` PASS · `1` FAIL · `2` unreadable · `3` NOT_RUN. No export is NOT_RUN,
|
|
199
|
+
never PASS: save the export beside the frames and run again, or record the probe
|
|
200
|
+
as not run.
|
|
191
201
|
2. **Regression** against the approved baseline, where one exists (`toHaveScreenshot`, or
|
|
192
202
|
the platform's snapshot test).
|
|
193
203
|
3. **The matrix.** One frame per `SCR-NN` state the screen map lists (default, loading,
|
|
@@ -532,10 +532,13 @@ it invents what was already decided.
|
|
|
532
532
|
approximately, and approximate is indistinguishable from exact in a report.
|
|
533
533
|
3. **A component with a Code Connect mapping is not rewritten.** If
|
|
534
534
|
`get_code_connect_map` names a code component for that node, the screen uses it.
|
|
535
|
-
Reimplementing it is a silent fork of the design system.
|
|
535
|
+
Reimplementing it is a silent fork of the design system. *Code Connect, kept*, below,
|
|
536
|
+
is what keeps that mapping true.
|
|
536
537
|
4. **A token names its variable.** A raw hex or px where the file has a variable is a
|
|
537
538
|
token that has quietly split in two. `get_variable_defs` is the canon; a screenshot
|
|
538
|
-
is a way to *look*, never a way to *know*.
|
|
539
|
+
is a way to *look*, never a way to *know*. Whether the names still agree is
|
|
540
|
+
measured, not assumed: `visual_gate.py tokens` ([`browser.md`](browser.md) →
|
|
541
|
+
*The visual half*).
|
|
539
542
|
5. **The frame is a contract at its own width.** It is one width and said nothing about
|
|
540
543
|
the others, so behaviour at other breakpoints — and states the frame does not draw,
|
|
541
544
|
like error, empty and loading — is a **decision that gets recorded**, not guessed.
|
|
@@ -559,6 +562,42 @@ same false confidence as an unproven green.
|
|
|
559
562
|
platform constraint, an accessibility floor, a breakpoint — write what and why.
|
|
560
563
|
Otherwise *"built from the frame"* and *"built to look like it"* read identically.
|
|
561
564
|
|
|
565
|
+
### Code Connect, kept
|
|
566
|
+
|
|
567
|
+
Rule 3 trusts the mapping. It is only worth trusting while it stays true. A mapping
|
|
568
|
+
that still names a component's old props hands the next agent a snippet that does
|
|
569
|
+
not compile, so it is followed by reimplementing the component. That is the fork
|
|
570
|
+
rule 3 exists to prevent, reached by obeying it.
|
|
571
|
+
|
|
572
|
+
- **A component whose API changes updates its Code Connect mapping in the same
|
|
573
|
+
change.** That covers a renamed or removed prop, a new variant, or a moved import
|
|
574
|
+
path. The mapping file is part of the component's diff and reviewed with it. A
|
|
575
|
+
mapping left for later is wrong from the next merge onwards. Figma's own guidance
|
|
576
|
+
says the same: *"When component APIs change in your codebase, update the
|
|
577
|
+
corresponding Code Connect mappings"* ([Code Connect integration][fcc], accessed
|
|
578
|
+
2026-10-08).
|
|
579
|
+
- **A core component with no mapping gets an offer, not a silent skip.** Where a
|
|
580
|
+
task builds on a design-system component that the file draws and Code Connect does
|
|
581
|
+
not map, the run offers to map it with `/figma-code-connect`, Figma's public
|
|
582
|
+
plugin skill ([`companion-skills.md`](companion-skills.md)). The offer names which
|
|
583
|
+
components and which file. Mapping publishes to the Figma file, so it runs only on
|
|
584
|
+
an explicit go, like drawing a missing frame. Absent the skill, the run names the
|
|
585
|
+
unmapped components in the close-out and goes on.
|
|
586
|
+
- **Never rewrite a mapped component instead of using it** — rule 3, unchanged. An
|
|
587
|
+
API that no longer fits the frame is a change to the component and its mapping
|
|
588
|
+
together, never a local copy.
|
|
589
|
+
|
|
590
|
+
Why it pays, in Figma's words and on Figma's measurement: their own evals report *"a
|
|
591
|
+
19.6% reduction in median task duration, a full point of improvement in code quality
|
|
592
|
+
on a 1–4 scale, and a 29.5% reduction in token usage"* with Code Connect
|
|
593
|
+
([Figma MCP use cases][fuc], accessed 2026-10-08). **That is Figma's unaudited
|
|
594
|
+
number** — a vendor's eval of its own feature, with no published protocol here and
|
|
595
|
+
not reproduced by this pipeline. Quote it as theirs, never as a measured property
|
|
596
|
+
of a run.
|
|
597
|
+
|
|
598
|
+
[fcc]: https://developers.figma.com/docs/figma-mcp-server/code-connect-integration/
|
|
599
|
+
[fuc]: https://www.figma.com/resource-library/figma-mcp-use-cases/
|
|
600
|
+
|
|
562
601
|
## 5. Final whole-branch review
|
|
563
602
|
|
|
564
603
|
After the last task: build a package over `MERGE_BASE`..`HEAD`
|
|
@@ -59,10 +59,16 @@ that reads no code is not the soft one.
|
|
|
59
59
|
| `seam` | everything that can reach the change — callers, callees, implementors, shared state, the neighbours' tests | a contract that moved under a dependent; the duplicate that did not get the fix |
|
|
60
60
|
| `product` | documentation, scenarios, user-visible strings, and the neighbouring features that share this path | a documented behaviour that is now false; an interaction nobody listed |
|
|
61
61
|
|
|
62
|
-
Agents: [
|
|
63
|
-
[`verifier-seam.md`](
|
|
64
|
-
[`verifier-product.md`](
|
|
65
|
-
[`verifier-visual.md`](
|
|
62
|
+
Agents: [`verifier-unit.md`](../assets/verifier-unit.md),
|
|
63
|
+
[`verifier-seam.md`](../assets/verifier-seam.md),
|
|
64
|
+
[`verifier-product.md`](../assets/verifier-product.md) — and, on a visual surface,
|
|
65
|
+
[`verifier-visual.md`](../assets/verifier-visual.md), below. These portable procedures live inside the skill.
|
|
66
|
+
Use host-native delegation when available; without it, execute the same tier
|
|
67
|
+
procedures inline and disclose that self-review lacks independent contexts.
|
|
68
|
+
Do not count inline passes as independent certification or close the graph on
|
|
69
|
+
that basis: independence-dependent gates remain incomplete/unverified until
|
|
70
|
+
independent review is available.
|
|
71
|
+
The Claude plugin agent definitions carry the same bodies with host frontmatter.
|
|
66
72
|
|
|
67
73
|
## The fourth reading — `visual`, on a flagship or product surface
|
|
68
74
|
|
|
@@ -100,6 +100,7 @@ required; an absent one is a check recorded as NOT_RUN with its reason.
|
|
|
100
100
|
| Compose `enableAccessibilityChecks` | Android's accessibility checks in Compose UI tests | stage 6, a native Android surface |
|
|
101
101
|
| Playwright `toHaveScreenshot` | screenshot regression against the approved baseline, per state and theme | stage 6 (regression), and the nightly or release baseline after |
|
|
102
102
|
| axe-core | the deterministic accessibility floor of a web surface | stage 6, first in the cheap-first order |
|
|
103
|
+
| `figma-code-connect` (Figma's public plugin skill) | maps a design-system component in the file to its code component, so `get_design_context` hands the agent the real import instead of approximated markup | stage 5, **offered** for a core component the file draws and nothing maps — publishes to the file, so it needs a go ([`build.md`](build.md) → *Code Connect, kept*) |
|
|
103
104
|
|
|
104
105
|
A native screen is captured on a simulator or a device; a web render styled as a phone is
|
|
105
106
|
a mockup, and the native rows of the sheet stay NOT_RUN without one.
|
|
@@ -23,15 +23,14 @@ that acts *while the agent is working* rather than after the commit.
|
|
|
23
23
|
|
|
24
24
|
## The limit, before the capability
|
|
25
25
|
|
|
26
|
-
**
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
**This package ships a Claude Code hook adapter.** Other hosts may expose hooks,
|
|
27
|
+
but capability does not register this package's adapter. Check the active host's
|
|
28
|
+
version, adapter registration, event payload and a planted refusal before claiming
|
|
29
|
+
enforcement. Without a verified adapter, run the same gates explicitly and record
|
|
30
|
+
**`ungated`**. Native hook support alone is not protection for this project.
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
inside a transcript, and a false guarantee is worse than a stated absence: everyone
|
|
34
|
-
downstream stops checking.
|
|
32
|
+
Never describe a project as protected merely because a rule or a hook file exists.
|
|
33
|
+
A false guarantee makes downstream readers stop checking.
|
|
35
34
|
|
|
36
35
|
> **Provenance.** Every contract below is quoted from the Claude Code hooks
|
|
37
36
|
> reference (`code.claude.com/docs/en/hooks`), fetched **2026-08-03**. Re-fetch
|
|
@@ -271,7 +270,7 @@ entries carrying one id — which is the exact failure the lease existed to prev
|
|
|
271
270
|
|
|
272
271
|
| Excuse | Reality |
|
|
273
272
|
|---|---|
|
|
274
|
-
| "The hook is installed, so the repo is protected" | Only
|
|
273
|
+
| "The hook is installed, so the repo is protected" | Only after this adapter was verified on the active host. For the documented Claude command hook, exit 2 refuses; other errors fail open. |
|
|
275
274
|
| "It's fine, the guard can't crash" | Then it costs one line to make crashing block instead of pass. Write the line. |
|
|
276
275
|
| "I'll match `*` and filter inside the script" | Now every tool call pays your script's startup. Match narrowly; the matcher is free and the script is not. |
|
|
277
276
|
| "The hook is slow, I'll disable it for now" | "For now" survives the session and the memory of why. Fix the throttle. |
|
|
@@ -473,7 +473,13 @@ never that the work was skipped quietly.
|
|
|
473
473
|
`surface_class`, the project linter runs here too**, after each task that changes a
|
|
474
474
|
rendered surface — `python3 scripts/visual_gate.py lint <dir>`; an S1 finding is
|
|
475
475
|
fixed in the task, and NOT_RUN (exit 3) is recorded as such
|
|
476
|
-
([`browser.md`](browser.md) → *The visual half*).
|
|
476
|
+
([`browser.md`](browser.md) → *The visual half*). **With Figma on, the token drift
|
|
477
|
+
probe runs beside it** after a task that touches the token file or a tokenised
|
|
478
|
+
component: `python3 scripts/visual_gate.py tokens --figma <variables.json> --css
|
|
479
|
+
<tokens.css>`. A FAIL is fixed in the task, renaming one side to match the other.
|
|
480
|
+
No export is NOT_RUN, recorded as such. A component whose API the task changed
|
|
481
|
+
updates its Code Connect mapping in the same change
|
|
482
|
+
([`build.md`](build.md) → *Code Connect, kept*). A slop marker caught while the
|
|
477
483
|
implementer is dispatched costs a line; caught on the contact sheet it costs a round. Stage 6 repeats this over the whole tree; this one catches it while the
|
|
478
484
|
implementer that wrote it is still dispatched. The matrix pointed this companion at
|
|
479
485
|
stages 5–6 from the day it was added and **this stage had never named it** — found by
|
|
@@ -550,7 +556,9 @@ never that the work was skipped quietly.
|
|
|
550
556
|
`ad` surface, stage 6 also owes **the contact sheet** — frames over the `SCR-NN` states
|
|
551
557
|
× viewport × theme × text × locale (pairwise, plus the mandatory pairs), each with its
|
|
552
558
|
capture record, diffed against its Figma frame or approved baseline, the project
|
|
553
|
-
linter run,
|
|
559
|
+
linter run, the token drift probe run over the whole token file where Figma is on
|
|
560
|
+
(`visual_gate.py tokens`; NOT_RUN without an export, never PASS), and the rubric
|
|
561
|
+
read by a judge that is not the builder — and its command
|
|
554
562
|
exits 0: `python3 scripts/visual_gate.py sheet <contact-sheet.json> --class
|
|
555
563
|
<surface_class> --artifact-root <frames>` (NOT_RUN, exit 3, is not green). On
|
|
556
564
|
`internal` it is recommended and the linter is the floor. The re-render budget is
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
visual_gate.py filekeys --record <foundation.md or brief> --screens <screens.md> [--json]
|
|
16
16
|
stage 3 with Figma on: every frame link's file key is one of the files the project
|
|
17
17
|
recorded — one per surface (App, Web, ASO), never a file nobody recorded
|
|
18
|
+
visual_gate.py tokens --figma <variables.json> --css <tokens.css> [--json]
|
|
19
|
+
stages 5–6 with Figma on, the token drift probe: every variable name exported from
|
|
20
|
+
the file (`get_variable_defs`, or the REST export) has its CSS custom property in
|
|
21
|
+
the pack's token file and the reverse, and WEB code syntax, where set, names the
|
|
22
|
+
property the file actually declares. No export is NOT_RUN (exit 3), never PASS
|
|
18
23
|
|
|
19
24
|
`<surface_class>` is the brief's stage-0 answer: flagship | product | internal | ad. It
|
|
20
25
|
selects what each check owes (`references/stages.md` → stage 0, *The surface class*).
|
|
@@ -536,6 +541,134 @@ def cmd_filekeys(a):
|
|
|
536
541
|
return _emit(a, "filekeys", "FAIL" if problems else "PASS", problems, notes)
|
|
537
542
|
|
|
538
543
|
|
|
544
|
+
# --- token-name drift: Figma variables against the pack's CSS custom properties ---
|
|
545
|
+
# A token that has one name in the file and another in code has quietly split in two, and
|
|
546
|
+
# nothing downstream notices: `get_design_context` hands the agent the Figma name, the agent
|
|
547
|
+
# writes a raw value because no property answers to it, and the screen still looks right.
|
|
548
|
+
|
|
549
|
+
CSS_DECL = re.compile(r"(?<![\w-])(--[A-Za-z0-9_-]+)\s*:")
|
|
550
|
+
CS_PROP = re.compile(r"\s*(?:var\(\s*)?(--[A-Za-z0-9_-]+)\s*(?:,[^)]*)?\)?\s*")
|
|
551
|
+
# Keys a variable's own record can carry. A dict value with none of them is a token GROUP
|
|
552
|
+
# (a nested design-token tree), not a variable, and reading its key as a name would compare
|
|
553
|
+
# the code against the group names.
|
|
554
|
+
VAR_KEYS = ("codeSyntax", "value", "$value", "resolvedType", "type", "valuesByMode")
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def css_property(name):
|
|
558
|
+
"""The CSS custom property a Figma variable name maps to when it carries no code syntax:
|
|
559
|
+
`Color/Text Muted` → `--color-text-muted`, `fontSize/bodyLarge` → `--font-size-body-large`.
|
|
560
|
+
A convention, not a law — WEB code syntax, where the file sets it, overrides it."""
|
|
561
|
+
s = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", name)
|
|
562
|
+
return "--" + re.sub(r"[^A-Za-z0-9]+", "-", s).strip("-").lower()
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def figma_variables(doc):
|
|
566
|
+
"""[(name, web_code_syntax or None)] from a variable export. Accepts the map
|
|
567
|
+
`get_variable_defs` returns (name → value), the REST export (`meta.variables`, id →
|
|
568
|
+
variable) and a `variables` list. Raises ValueError on any other shape."""
|
|
569
|
+
if isinstance(doc, dict) and isinstance(doc.get("meta"), dict) and "variables" in doc["meta"]:
|
|
570
|
+
doc = doc["meta"]["variables"]
|
|
571
|
+
elif isinstance(doc, dict) and isinstance(doc.get("variables"), (list, dict)):
|
|
572
|
+
doc = doc["variables"]
|
|
573
|
+
|
|
574
|
+
def one(v):
|
|
575
|
+
cs = v.get("codeSyntax")
|
|
576
|
+
return v["name"], (cs.get("WEB") if isinstance(cs, dict) else None)
|
|
577
|
+
|
|
578
|
+
if isinstance(doc, list):
|
|
579
|
+
if not all(isinstance(v, dict) and isinstance(v.get("name"), str) for v in doc):
|
|
580
|
+
raise ValueError("a `variables` list whose items are not {name, …} objects")
|
|
581
|
+
return [one(v) for v in doc]
|
|
582
|
+
if not isinstance(doc, dict):
|
|
583
|
+
raise ValueError(f"a {type(doc).__name__}, not a variable map")
|
|
584
|
+
if doc and all(isinstance(v, dict) and isinstance(v.get("name"), str) for v in doc.values()):
|
|
585
|
+
return [one(v) for v in doc.values()]
|
|
586
|
+
out = []
|
|
587
|
+
for k, v in doc.items():
|
|
588
|
+
if isinstance(v, dict):
|
|
589
|
+
if not any(key in v for key in VAR_KEYS):
|
|
590
|
+
raise ValueError(f"{k!r} holds a group, not a variable — a nested token tree")
|
|
591
|
+
cs = v.get("codeSyntax")
|
|
592
|
+
out.append((k, cs.get("WEB") if isinstance(cs, dict) else None))
|
|
593
|
+
elif isinstance(v, list):
|
|
594
|
+
raise ValueError(f"{k!r} holds a list, not a variable's value")
|
|
595
|
+
else:
|
|
596
|
+
out.append((k, None))
|
|
597
|
+
return out
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def css_properties(text):
|
|
601
|
+
"""Every custom property the token file DECLARES. Comments are stripped first, and a
|
|
602
|
+
`var(--x)` use is not a declaration."""
|
|
603
|
+
return set(CSS_DECL.findall(re.sub(r"/\*.*?\*/", "", text, flags=re.S)))
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def token_drift(variables, props):
|
|
607
|
+
"""(problems, report) — names in one side and not the other, and code syntax that
|
|
608
|
+
disagrees with the property the token file actually has."""
|
|
609
|
+
problems, figma_only, syntax, used, owner = [], [], [], set(), {}
|
|
610
|
+
for name, cs in variables:
|
|
611
|
+
derived = css_property(name)
|
|
612
|
+
target = derived
|
|
613
|
+
if cs is not None:
|
|
614
|
+
m = CS_PROP.fullmatch(cs)
|
|
615
|
+
if not m:
|
|
616
|
+
syntax.append(f"{name}: WEB code syntax {cs!r} is not a CSS custom property")
|
|
617
|
+
continue
|
|
618
|
+
target = m.group(1)
|
|
619
|
+
if target in owner:
|
|
620
|
+
problems.append(f"{owner[target]!r} and {name!r} both map to {target} — one "
|
|
621
|
+
"property cannot hold both")
|
|
622
|
+
continue
|
|
623
|
+
owner[target] = name
|
|
624
|
+
if target in props:
|
|
625
|
+
used.add(target)
|
|
626
|
+
elif cs is not None and derived in props:
|
|
627
|
+
used.add(derived)
|
|
628
|
+
syntax.append(f"{name}: code syntax names {target}, and the token file calls it "
|
|
629
|
+
f"{derived}")
|
|
630
|
+
else:
|
|
631
|
+
figma_only.append(f"{name} → {target}")
|
|
632
|
+
css_only = sorted(props - used)
|
|
633
|
+
problems += [f"in Figma, not in the token file: {x}" for x in figma_only]
|
|
634
|
+
problems += [f"in the token file, not in Figma: {x}" for x in css_only]
|
|
635
|
+
problems += [f"code syntax: {x}" for x in syntax]
|
|
636
|
+
return problems, {"figma_only": figma_only, "css_only": css_only, "code_syntax": syntax,
|
|
637
|
+
"matched": len(used)}
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def cmd_tokens(a):
|
|
641
|
+
try:
|
|
642
|
+
css = open(a.css, encoding="utf-8").read()
|
|
643
|
+
except OSError as e:
|
|
644
|
+
print(f"visual_gate: cannot read the token file {a.css} ({type(e).__name__})",
|
|
645
|
+
file=sys.stderr)
|
|
646
|
+
return 2
|
|
647
|
+
if not os.path.isfile(a.figma):
|
|
648
|
+
return _emit(a, "tokens", "NOT_RUN", [], [
|
|
649
|
+
f"no Figma variable export at {a.figma} — export it (`get_variable_defs`, or the "
|
|
650
|
+
"REST variables endpoint) and run again; until then the drift is unmeasured"])
|
|
651
|
+
try:
|
|
652
|
+
doc = json.load(open(a.figma, encoding="utf-8"))
|
|
653
|
+
except (OSError, ValueError) as e: # JSONDecodeError and UnicodeDecodeError included
|
|
654
|
+
print(f"visual_gate: cannot read {a.figma} ({type(e).__name__})", file=sys.stderr)
|
|
655
|
+
return 2
|
|
656
|
+
try:
|
|
657
|
+
variables = figma_variables(doc)
|
|
658
|
+
except ValueError as e:
|
|
659
|
+
print(f"visual_gate: {a.figma} is not a variable export — {e}", file=sys.stderr)
|
|
660
|
+
return 2
|
|
661
|
+
if not variables:
|
|
662
|
+
return _emit(a, "tokens", "NOT_RUN", [], [
|
|
663
|
+
f"{a.figma} holds no variables — there is nothing to compare, and nothing "
|
|
664
|
+
"compared is not a pass"])
|
|
665
|
+
props = css_properties(css)
|
|
666
|
+
problems, rep = token_drift(variables, props)
|
|
667
|
+
notes = [f"{len(variables)} Figma variable(s), {len(props)} CSS custom propert"
|
|
668
|
+
f"{'y' if len(props) == 1 else 'ies'}, {rep['matched']} matched"]
|
|
669
|
+
return _emit(a, "tokens", "FAIL" if problems else "PASS", problems, notes, extra=rep)
|
|
670
|
+
|
|
671
|
+
|
|
539
672
|
EXIT = {"PASS": 0, "FAIL": 1, "NOT_RUN": 3}
|
|
540
673
|
|
|
541
674
|
|
|
@@ -580,9 +713,15 @@ def main(argv):
|
|
|
580
713
|
f.add_argument("--record", required=True)
|
|
581
714
|
f.add_argument("--screens", required=True)
|
|
582
715
|
f.add_argument("--json", action="store_true")
|
|
716
|
+
t = sub.add_parser("tokens", help="stages 5–6: Figma variable names against the CSS "
|
|
717
|
+
"custom properties of the pack's token file")
|
|
718
|
+
t.add_argument("--figma", required=True,
|
|
719
|
+
help="the variable export (get_variable_defs JSON, or the REST export)")
|
|
720
|
+
t.add_argument("--css", required=True, help="the pack's token file")
|
|
721
|
+
t.add_argument("--json", action="store_true")
|
|
583
722
|
a = p.parse_args(argv)
|
|
584
723
|
return {"record": cmd_record, "sheet": cmd_sheet, "lint": cmd_lint,
|
|
585
|
-
"filekeys": cmd_filekeys}[a.cmd](a)
|
|
724
|
+
"filekeys": cmd_filekeys, "tokens": cmd_tokens}[a.cmd](a)
|
|
586
725
|
|
|
587
726
|
|
|
588
727
|
if __name__ == "__main__":
|