@weatherboard/gyde-design 0.4.3 → 0.5.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/README.md +82 -0
- package/cli.mjs +66 -1
- package/floor.mjs +479 -0
- package/htmlscan.mjs +149 -0
- package/index.mjs +1 -0
- package/package.json +5 -1
- package/theme-entry.mjs +120 -16
- package/themehost.mjs +14 -40
- package/vendor/README.md +95 -0
- package/vendor/acorn-typescript.mjs +1 -0
- package/vendor/acorn.mjs +6313 -0
package/README.md
CHANGED
|
@@ -137,6 +137,7 @@ against all five.
|
|
|
137
137
|
| `agentdocs.mjs` | What an agent building the product reads before writing UI. |
|
|
138
138
|
| `ruleindex.mjs` | Every rule as data — id, name, intent, fix shape. Describes; never decides. |
|
|
139
139
|
| `selfgate.mjs` | Emits a scaffold and gates it. Gyde held to its own rules. |
|
|
140
|
+
| `floor.mjs` | Opt-in scoped adoption floors and their named structural exceptions. Fails closed on a scope that matches nothing. |
|
|
140
141
|
|
|
141
142
|
## Measured, on the three repositories it was built from
|
|
142
143
|
|
|
@@ -314,6 +315,87 @@ This command updates only `.gyde/design-system.md` and refuses to replace a
|
|
|
314
315
|
handwritten file. It lists component names without guessing their props or
|
|
315
316
|
claiming theme APIs that Gyde has not verified in that product.
|
|
316
317
|
|
|
318
|
+
## A floor over a scope you have finished
|
|
319
|
+
|
|
320
|
+
The ledger answers *has this got worse?* That is the right instrument for a
|
|
321
|
+
repository carrying four thousand findings and the wrong one for the opposite
|
|
322
|
+
case: a product that has finished the work over a defined slice of its tree and
|
|
323
|
+
wants the finishing to stay finished. An empty ledger cannot say that — "nothing
|
|
324
|
+
was recorded here" is also what an unmeasured scope says.
|
|
325
|
+
|
|
326
|
+
So a product may declare a **floor**, and nothing more happens without one:
|
|
327
|
+
|
|
328
|
+
```json
|
|
329
|
+
{
|
|
330
|
+
"design": {
|
|
331
|
+
"adoptionFloors": [
|
|
332
|
+
{
|
|
333
|
+
"scope": "apps/console/src",
|
|
334
|
+
"minimum": 100,
|
|
335
|
+
"exceptions": [
|
|
336
|
+
{
|
|
337
|
+
"file": "apps/console/src/frame/MediaFrame.tsx",
|
|
338
|
+
"element": "iframe",
|
|
339
|
+
"because": "A browser-owned element the design system cannot render or restyle from inside."
|
|
340
|
+
}
|
|
341
|
+
]
|
|
342
|
+
}
|
|
343
|
+
]
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
| key | |
|
|
349
|
+
| --- | --- |
|
|
350
|
+
| `scope` | A path inside the repository. Not an npm scope — that is `design.scope`, a different key answering a different question. |
|
|
351
|
+
| `minimum` | A whole number, 0 to 100. The fraction of eligible visual elements under the scope that must be the system's. |
|
|
352
|
+
| `exceptions` | Elements excluded from the denominator. Each names one element, in one file, with one reason. |
|
|
353
|
+
|
|
354
|
+
`gyde-design gate` prints the figure with its numerator, its denominator and the
|
|
355
|
+
scope, and prints the exceptions underneath it rather than inside it. A working
|
|
356
|
+
example is in `examples/scoped-floor`.
|
|
357
|
+
|
|
358
|
+
**100% means zero.** The verdict is taken on the counts, never on the rendered
|
|
359
|
+
percentage: 999 of 1,000 floors to 99% and does not meet a floor of 100. That is
|
|
360
|
+
the whole reason this exists — a report reading "100%" over a denominator that
|
|
361
|
+
still holds a bespoke element is a lie that looks like an achievement.
|
|
362
|
+
|
|
363
|
+
**Exceptions cannot grow quietly.** One entry excuses one element. Until an
|
|
364
|
+
element is declared it is an ordinary bespoke element and it fails the floor, so
|
|
365
|
+
the seventh exception costs a diff a reviewer reads. And an exception that stops
|
|
366
|
+
matching anything is an **error**: a stale exemption is a line sitting open for
|
|
367
|
+
whatever lands under that name next, and it is the only way the list could
|
|
368
|
+
shrink invisibly. The same discipline the allowance ledger has — it may only
|
|
369
|
+
ever get shorter, and never by accident.
|
|
370
|
+
|
|
371
|
+
**Every path fails closed.** A malformed entry, an unknown key, a scope that
|
|
372
|
+
does not resolve, a scope that matches no markup files, a scope whose files make
|
|
373
|
+
no visual decision, an unreadable component barrel, an exception naming
|
|
374
|
+
something that is gone — each is an ERROR that names the cause, and none of them
|
|
375
|
+
is allowanceable or recordable as a baseline. The zero-file case is the one that
|
|
376
|
+
mattered most: a floor over an empty denominator would report success forever,
|
|
377
|
+
and a rule that matches nothing reads exactly like a rule that passes.
|
|
378
|
+
|
|
379
|
+
### What a floor does not protect against
|
|
380
|
+
|
|
381
|
+
- **Gyde does not judge the exceptions.** It checks that each names a real
|
|
382
|
+
bespoke element and carries a reason somebody wrote. It cannot tell a true
|
|
383
|
+
reason from a plausible sentence. What it buys is that every one of them is
|
|
384
|
+
on the artefact, in the diff, with a name and a line number attached.
|
|
385
|
+
- **It measures markup, not rendering.** An element is the system's if its tag
|
|
386
|
+
is one of the design system's exports. A component that imports the system
|
|
387
|
+
and then restyles it at the call site still counts — that is the styling
|
|
388
|
+
layer's question, not this one.
|
|
389
|
+
- **The scope is a path, not a semantic boundary.** Code moved out from under
|
|
390
|
+
it leaves the denominator silently. The figure stays true and the coverage
|
|
391
|
+
shrinks; nothing here notices. The scope is printed beside the figure
|
|
392
|
+
precisely so that a reader can.
|
|
393
|
+
- **A small denominator is still a denominator.** A scope of two hundred files
|
|
394
|
+
where three carry markup reports three. Whether three is enough evidence is a
|
|
395
|
+
judgement Gyde does not make for you — which is why the number is never
|
|
396
|
+
printed without it.
|
|
397
|
+
|
|
398
|
+
|
|
317
399
|
## Not built yet
|
|
318
400
|
|
|
319
401
|
Build-pipeline wiring for a compiled token layer — moot while the emitted tokens
|
package/cli.mjs
CHANGED
|
@@ -50,6 +50,7 @@ import { record, gate, formatGate, adopt, trim, formatTrim, LEDGER_NOTE } from "
|
|
|
50
50
|
import { loadRules } from "./rules.mjs";
|
|
51
51
|
import { validateScopeShape, checkScopeAgreement, formatScope } from "./scope.mjs";
|
|
52
52
|
import { checkThemeHosts, formatThemeHosts } from "./themehost.mjs";
|
|
53
|
+
import { checkFloors, formatFloors, validateFloors, KEY as FLOORS_KEY } from "./floor.mjs";
|
|
53
54
|
|
|
54
55
|
/**
|
|
55
56
|
* The single source of what would be written.
|
|
@@ -222,6 +223,22 @@ function readConfig(root) {
|
|
|
222
223
|
"A scope names the packages the emitted code imports, so a wrong one produces imports that resolve to nothing.");
|
|
223
224
|
}
|
|
224
225
|
|
|
226
|
+
/**
|
|
227
|
+
* G-133. The schema half of the adoption floors, at the same one place.
|
|
228
|
+
*
|
|
229
|
+
* Here rather than in `gate` alone for the reason above and one more: a floor
|
|
230
|
+
* is the only configuration in this file that can make a gate PASS, by
|
|
231
|
+
* excluding an element from a denominator. A malformed one must therefore be
|
|
232
|
+
* refused everywhere the config is read, and must never be recoverable by
|
|
233
|
+
* falling back to a default — a floor Gyde half-understood would hold a line
|
|
234
|
+
* nobody drew.
|
|
235
|
+
*/
|
|
236
|
+
const floors = validateFloors(design.adoptionFloors);
|
|
237
|
+
if (!floors.ok) {
|
|
238
|
+
throw new Error(`gyde.config.json: ${FLOORS_KEY} is malformed and Gyde will not guess at it:\n ` +
|
|
239
|
+
floors.errors.join("\n "));
|
|
240
|
+
}
|
|
241
|
+
|
|
225
242
|
return { present: true, design };
|
|
226
243
|
}
|
|
227
244
|
|
|
@@ -346,6 +363,13 @@ function cmdScan(root, config) {
|
|
|
346
363
|
console.log(formatUsage(usage, found));
|
|
347
364
|
}
|
|
348
365
|
|
|
366
|
+
// G-133. Reported wherever adoption is reported, and reported even when no
|
|
367
|
+
// floor is declared — "none declared" and "all held" are different facts.
|
|
368
|
+
console.log("");
|
|
369
|
+
console.log(formatFloors(checkFloors(root, design, {
|
|
370
|
+
systemNames: scanRoots ? new Set(scanRoots) : null,
|
|
371
|
+
})));
|
|
372
|
+
|
|
349
373
|
if (!config.present) {
|
|
350
374
|
console.log(
|
|
351
375
|
"\nnote no gyde.config.json — every path was treated as product code.\n" +
|
|
@@ -618,7 +642,7 @@ function cmdGuidance(root, config, { write }) {
|
|
|
618
642
|
* report the wrong one, confidently. So the evidence carries `why`, and every
|
|
619
643
|
* path writes it.
|
|
620
644
|
*/
|
|
621
|
-
function writeEvidence(root, result, { ok, why, clientBoundary = null, enforcement = null }) {
|
|
645
|
+
function writeEvidence(root, result, { ok, why, clientBoundary = null, enforcement = null, floors = null }) {
|
|
622
646
|
mkdirSync(join(root, ".gyde"), { recursive: true });
|
|
623
647
|
writeFileSync(join(root, ".gyde", "last-verdict.json"), JSON.stringify({
|
|
624
648
|
"//": "Written by `gyde-design gate`. Proof the gate ran, not just that a job went green.",
|
|
@@ -648,9 +672,24 @@ function writeEvidence(root, result, { ok, why, clientBoundary = null, enforceme
|
|
|
648
672
|
rulesThatMatchedNothing: result ? result.coverage.filter((c) => !c.matched).map((c) => c.rule) : [],
|
|
649
673
|
clientBoundary,
|
|
650
674
|
enforcement,
|
|
675
|
+
/**
|
|
676
|
+
* G-133. The floors, with their denominators, so the action's summary can
|
|
677
|
+
* print the fraction rather than a percentage somebody has to trust.
|
|
678
|
+
*/
|
|
679
|
+
floors,
|
|
651
680
|
}, null, 2) + "\n");
|
|
652
681
|
}
|
|
653
682
|
|
|
683
|
+
/** The floors as data: never a bare percentage, always the fraction and the scope. */
|
|
684
|
+
function floorEvidence(floors) {
|
|
685
|
+
if (!floors.declared) return null;
|
|
686
|
+
return floors.results.map((r) => ({
|
|
687
|
+
scope: r.scope, minimum: r.minimum, used: r.used, of: r.of, percent: r.percent,
|
|
688
|
+
exceptions: r.exceptions.length, bespoke: r.bespoke.length,
|
|
689
|
+
ok: r.ok, errors: r.errors,
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
|
|
654
693
|
/** The design system package's own `name`, read from disk. Null when there is no package there. */
|
|
655
694
|
function systemPackageName(root, systemPath) {
|
|
656
695
|
try {
|
|
@@ -700,6 +739,31 @@ function cmdGate(root, config, { recordNewRules = false, trimLedger = false } =
|
|
|
700
739
|
}
|
|
701
740
|
|
|
702
741
|
const roots = exportedComponents(root, systemPath);
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* G-133. Before the baseline path, for the same reason theme hosts are.
|
|
745
|
+
*
|
|
746
|
+
* A floor is a line the product drew over a scope it says is finished. It is
|
|
747
|
+
* not style debt, so it is never an allowance entry — and recording a
|
|
748
|
+
* baseline underneath a floor that could not be measured would turn "Gyde
|
|
749
|
+
* could not tell" into a committed number and then into a pass.
|
|
750
|
+
*/
|
|
751
|
+
const floors = checkFloors(root, design, { systemNames: roots ? new Set(roots) : null });
|
|
752
|
+
if (floors.declared) {
|
|
753
|
+
console.log(formatFloors(floors));
|
|
754
|
+
console.log("");
|
|
755
|
+
}
|
|
756
|
+
if (!floors.ok) {
|
|
757
|
+
writeEvidence(root, null, {
|
|
758
|
+
ok: false,
|
|
759
|
+
why: floors.errors.length
|
|
760
|
+
? `an adoption floor could not be measured: ${floors.errors[0]}`
|
|
761
|
+
: `an adoption floor was not held: ${floors.failures.map((f) => `${f.scope} at ${f.used}/${f.of}, floor ${f.minimum}%`).join("; ")}`,
|
|
762
|
+
floors: floorEvidence(floors),
|
|
763
|
+
});
|
|
764
|
+
return 1;
|
|
765
|
+
}
|
|
766
|
+
|
|
703
767
|
const result = scan(root, { config: { ...design, componentRoots: roots ? new Set(roots) : null } });
|
|
704
768
|
|
|
705
769
|
// G-113. `--trim` on a repository with no ledger must NOT fall through to the
|
|
@@ -866,6 +930,7 @@ function cmdGate(root, config, { recordNewRules = false, trimLedger = false } =
|
|
|
866
930
|
|
|
867
931
|
writeEvidence(root, result, {
|
|
868
932
|
ok, why,
|
|
933
|
+
floors: floorEvidence(floors),
|
|
869
934
|
clientBoundary: { crossings: cb.crossings.length, unresolved: cb.unresolved.length, blocking: cbBlocks },
|
|
870
935
|
enforcement: {
|
|
871
936
|
templateVersion,
|