task-pipeline-skill 1.89.1 → 1.90.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 +45 -0
- package/README.md +1 -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/skills/task-pipeline/SKILL.md +1 -1
- 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/companion-skills.md +1 -0
- 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,48 @@
|
|
|
1
|
+
## v1.90.0 — Code Connect is kept, and token names are measured against the file
|
|
2
|
+
|
|
3
|
+
The build stage already said a component with a Code Connect mapping is used, never
|
|
4
|
+
rewritten. Nothing said the mapping had to stay true. A mapping that still names a
|
|
5
|
+
component's old props hands the next agent a snippet that does not compile, and the agent
|
|
6
|
+
reimplements the component, which is the fork the rule exists to prevent. Token names had
|
|
7
|
+
the same gap: rule 4 made `get_variable_defs` the canon, and no check compared it with the
|
|
8
|
+
code. A variable that is `color/primary` in the file and `--brand-primary` in CSS split in
|
|
9
|
+
two without anything noticing. The scope comes from a 2026-10-08 reading of Figma's own
|
|
10
|
+
guidance ([Code Connect integration][fcc-cl], [Figma MCP use cases][fuc-cl], both accessed
|
|
11
|
+
2026-10-08).
|
|
12
|
+
|
|
13
|
+
Guards: 430 → **430**. The new check is a verb of a shipped script with a suite of its
|
|
14
|
+
own, and the validator is unchanged.
|
|
15
|
+
|
|
16
|
+
- **`references/build.md` → *Code Connect, kept*.** A component whose API changes updates
|
|
17
|
+
its Code Connect mapping in the same change, quoting Figma's own line. A core component
|
|
18
|
+
the file draws and nothing maps gets an **offer** to map it with `/figma-code-connect`,
|
|
19
|
+
Figma's public plugin skill. Mapping publishes to the file, so it runs only on a go. The
|
|
20
|
+
rule against rewriting a mapped component stands unchanged. Figma's measured benefit is
|
|
21
|
+
quoted as **Figma's unaudited number**: a vendor's eval of its own feature, not
|
|
22
|
+
reproduced here.
|
|
23
|
+
- **`visual_gate.py tokens --figma <variables.json> --css <tokens.css>`, the token drift
|
|
24
|
+
probe.** It reads the `get_variable_defs` map, the REST export (`meta.variables`) or a
|
|
25
|
+
`variables` list, and the custom properties the pack's token file declares (comments
|
|
26
|
+
stripped, `var()` uses not counted). It names each variable with no property, each
|
|
27
|
+
property with no variable, each variable whose WEB code syntax is not the property the
|
|
28
|
+
file declares, and two variables that land on one property. Exit `0` PASS · `1` FAIL ·
|
|
29
|
+
`2` unreadable · `3` NOT_RUN. No export, or an export holding no variables, is NOT_RUN
|
|
30
|
+
and never PASS. A nested token tree is refused as unreadable, so the check never
|
|
31
|
+
compares the code with group names.
|
|
32
|
+
- **Wired in** to `references/browser.md` → *The visual half* (the deterministic floor),
|
|
33
|
+
`references/stages.md` stage 5 (after a task that touches tokens, beside the project
|
|
34
|
+
linter) and stage 6 (over the whole token file, inside the contact-sheet check). It is
|
|
35
|
+
also a row in `companion-skills.md` → *Visual lanes* for `figma-code-connect`.
|
|
36
|
+
- **Routing words.** The description now carries `bug` as its own word next to `fix`, so
|
|
37
|
+
"fix this bug" is claimed by name and not through `bug hunt`. 903 → 908 of 1024 chars,
|
|
38
|
+
inside the 970 working limit. The body is unchanged at 4748/4750 tokens.
|
|
39
|
+
- **Tests:** `test/visual_gate_test.py` 24 → **40** cases. The 16 new ones are planted
|
|
40
|
+
defects, and all 16 were watched failing before `tokens` existed. A sweep of 15
|
|
41
|
+
mutations of the new code was run, and every mutation was killed.
|
|
42
|
+
|
|
43
|
+
[fcc-cl]: https://developers.figma.com/docs/figma-mcp-server/code-connect-integration/
|
|
44
|
+
[fuc-cl]: https://www.figma.com/resource-library/figma-mcp-use-cases/
|
|
45
|
+
|
|
1
46
|
## v1.89.1 — v1.89.0's payload, released with its no-stamp declaration
|
|
2
47
|
|
|
3
48
|
`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` 279/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
|
|
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.0 |
|
|
16
16
|
| **Surface** | Claude Code (filesystem skill + plugin) and the vercel `skills` CLI. **Not** uploaded to the Skills API; custom Skills do not sync across surfaces |
|
|
17
17
|
| **Dependencies** | None required. Optional: `context7` (MCP), `figma` (MCP), super-ux, agent-sync, graphify, obsidian-wiki, and **one of two browser channels** — `playwright` (CLI or MCP) or `chrome-devtools` (MCP); either satisfies the browser step and neither is required. 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.0",
|
|
4
4
|
"description": "Full-cycle delivery pipeline for coding agents: a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine ships inside the skill — no companion plugin required. This package is the installer CLI.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"task-pipeline": "bin/task-pipeline.js"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "task-pipeline",
|
|
4
4
|
"displayName": "Task Pipeline",
|
|
5
5
|
"description": "Runs a substantial task through a mandatory built-in intake grill, then 10 gated stages (docs, brainstorm+decompose, spec, plan, subagent build, tests, lint/deploy, post-deploy, docs/wiki, acceptance). Every stage's doctrine is built into the skill — no companion plugin required — with typed auto/judgment/manual gates, a frozen requirement spine that closes with evidence, a work board and a verification ledger that outlive a run, an exposure line naming what shipped unconfirmed, a progress rail computed from the project's own config, a 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.0",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "ssheleg",
|
|
9
9
|
"url": "https://x.com/sshlg93"
|
|
@@ -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
|
---
|
|
@@ -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`
|
|
@@ -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.
|
|
@@ -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__":
|