design-contract-checker 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +58 -2
- package/bin/cli.js +175 -0
- package/lib/engine.js +265 -0
- package/package.json +41 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ogia Dormon / thedollartoolkit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
|
-
#
|
|
1
|
+
# design-contract-checker
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Command-line Guard Lite: compare CSS or HTML text against **your** declared design contract and get line-level findings with stable rule IDs. Five checks, zero dependencies, no account.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx design-contract-checker --contract design-contract.txt src/styles.css index.html
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## What it checks
|
|
10
|
+
|
|
11
|
+
| ID | Catches |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `PPG-COLOR-001` | Literal colour outside the declared palette |
|
|
14
|
+
| `PPG-TYPE-001` | Font family outside the declared type system |
|
|
15
|
+
| `PPG-TYPE-002` | Font size outside the declared type scale |
|
|
16
|
+
| `PPG-SPACE-001` | Spacing value outside the declared scale |
|
|
17
|
+
| `PPG-RADIUS-001` | Corner radius outside the declared set |
|
|
18
|
+
|
|
19
|
+
## Contract format
|
|
20
|
+
|
|
21
|
+
One line per category, comma-separated values:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
colors = #f2eee5, #18231d, #a95038
|
|
25
|
+
fonts = Georgia, Arial
|
|
26
|
+
font-sizes = 16px, 24px, 48px
|
|
27
|
+
spacing = 8px, 16px, 24px, 48px
|
|
28
|
+
radii = 0, 2px, 4px
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Options
|
|
32
|
+
|
|
33
|
+
| Flag | Meaning |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `--contract <file>` | Required. Path to the contract file. |
|
|
36
|
+
| `--json` | Machine-readable JSON (stable rule IDs, findings per file, summary). |
|
|
37
|
+
| `--help` | Usage. |
|
|
38
|
+
|
|
39
|
+
## Exit codes
|
|
40
|
+
|
|
41
|
+
`0` clean / `1` contract violations found / `2` usage error. CI-friendly: violations fail the run.
|
|
42
|
+
|
|
43
|
+
Agent-friendly: pipe `--json` findings into your coding agent and ask it to repair each `PPG-*` finding.
|
|
44
|
+
|
|
45
|
+
## Honest limitations
|
|
46
|
+
|
|
47
|
+
- Text analysis only: does not execute pasted HTML, load websites or inspect computed styles.
|
|
48
|
+
- Values through `var()`, `calc()`, `clamp()` and CSS keywords are skipped; verify those at the token layer.
|
|
49
|
+
- Not an accessibility audit and not a design-quality judge.
|
|
50
|
+
|
|
51
|
+
Full check definitions: [docs/checks.md](../docs/checks.md). Browser checker: https://pixel-perfect-system.netlify.app/guard/
|
|
52
|
+
|
|
53
|
+
## The system behind it
|
|
54
|
+
|
|
55
|
+
Guard Lite is the free checker of [Pixel Perfect](https://pixel-perfect-system.netlify.app/) — a design-contract toolkit (DESIGN.md, agent rules, tokens) for AI-built interfaces.
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const PACK = require(path.join(__dirname, "..", "package.json"));
|
|
8
|
+
const engine = require(path.join(__dirname, "..", "lib", "engine.js"));
|
|
9
|
+
|
|
10
|
+
const EXIT_CLEAN = 0;
|
|
11
|
+
const EXIT_FINDINGS = 1;
|
|
12
|
+
const EXIT_USAGE = 2;
|
|
13
|
+
|
|
14
|
+
function usage() {
|
|
15
|
+
const c = [
|
|
16
|
+
"",
|
|
17
|
+
" " + PACK.name + " v" + PACK.version + " — Guard Lite design-contract checker",
|
|
18
|
+
"",
|
|
19
|
+
" Usage:",
|
|
20
|
+
" design-contract-checker --contract <file> [source files...]",
|
|
21
|
+
" design-contract-checker --json --contract <file> [source files...]",
|
|
22
|
+
"",
|
|
23
|
+
" Contract file format (one line per category, comma separated):",
|
|
24
|
+
" colors = #f2eee5, #18231d, #a95038",
|
|
25
|
+
" fonts = Georgia, Arial",
|
|
26
|
+
" font-sizes = 16px, 24px, 48px",
|
|
27
|
+
" spacing = 8px, 16px, 24px, 48px",
|
|
28
|
+
" radii = 0, 2px, 4px",
|
|
29
|
+
"",
|
|
30
|
+
" Options:",
|
|
31
|
+
" --contract <file> Required. Path to the declared design contract.",
|
|
32
|
+
" --json Print machine-readable JSON instead of text.",
|
|
33
|
+
" -h, --help Show this help.",
|
|
34
|
+
"",
|
|
35
|
+
" Checks: PPG-COLOR-001 PPG-TYPE-001 PPG-TYPE-002 PPG-SPACE-001 PPG-RADIUS-001",
|
|
36
|
+
" Text analysis only: var()/calc()/clamp() values are skipped, not resolved.",
|
|
37
|
+
" Learn: https://pixel-perfect-system.netlify.app/guard/",
|
|
38
|
+
"",
|
|
39
|
+
].join("\n");
|
|
40
|
+
process.stdout.write(c + "\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fail(message, details) {
|
|
44
|
+
process.stderr.write("Error: " + message + (details ? "\n" + details : "") + "\n");
|
|
45
|
+
process.exit(EXIT_USAGE);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseArgs(argv) {
|
|
49
|
+
const out = { contractPath: null, json: false, help: false, sources: [] };
|
|
50
|
+
let i = 0;
|
|
51
|
+
while (i < argv.length) {
|
|
52
|
+
const arg = argv[i];
|
|
53
|
+
if (arg === "-h" || arg === "--help" || arg === "help") {
|
|
54
|
+
out.help = true;
|
|
55
|
+
i += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (arg === "--json") {
|
|
59
|
+
out.json = true;
|
|
60
|
+
i += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (arg === "--contract") {
|
|
64
|
+
const next = argv[i + 1];
|
|
65
|
+
if (!next) fail("--contract requires a file path.");
|
|
66
|
+
out.contractPath = next;
|
|
67
|
+
i += 2;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg.indexOf("--") === 0) fail("Unknown option: " + arg, "Run with --help.");
|
|
71
|
+
out.sources.push(arg);
|
|
72
|
+
i += 1;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readFileChecked(fullPath, maxChars, maxBytesLabel) {
|
|
78
|
+
let text;
|
|
79
|
+
try {
|
|
80
|
+
text = fs.readFileSync(fullPath, "utf8");
|
|
81
|
+
} catch (e) {
|
|
82
|
+
fail("Cannot read " + fullPath + ": " + e.message);
|
|
83
|
+
}
|
|
84
|
+
if (text.length > maxChars) {
|
|
85
|
+
fail(fullPath + " is too large for Guard Lite (" + text.length + " chars).", maxBytesLabel);
|
|
86
|
+
}
|
|
87
|
+
return text;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function outputText(summary, results, total) {
|
|
91
|
+
const out = [];
|
|
92
|
+
results.forEach(function (res) {
|
|
93
|
+
out.push(res.file + ": " + res.summary.lines + " lines inspected, " +
|
|
94
|
+
res.summary.violations + " contract violations, " + res.summary.reviews + " needs review\n");
|
|
95
|
+
res.findings.forEach(function (f) {
|
|
96
|
+
out.push("[" + f.id + "] " + res.file + ":" + f.line + " " + f.property);
|
|
97
|
+
if (f.status === "Review") {
|
|
98
|
+
out.push(" REVIEW — " + f.title);
|
|
99
|
+
out.push(" Found: " + f.found + " | " + f.expected);
|
|
100
|
+
} else {
|
|
101
|
+
out.push(" VIOLATION — " + f.title);
|
|
102
|
+
const allowed = f.expected.split(", ").filter(Boolean);
|
|
103
|
+
const list = allowed.length > 8 ? allowed.slice(0, 8).join(", ") + ", …" : allowed.join(", ");
|
|
104
|
+
out.push(" Found: " + f.found + " | Allowed: " + list);
|
|
105
|
+
}
|
|
106
|
+
out.push(" Repair: " + f.repair + "\n");
|
|
107
|
+
});
|
|
108
|
+
if (res.findings.length === 0) {
|
|
109
|
+
out.push(" No outside literal values were found. This does not prove the rendered interface follows the contract — only that these five text checks saw nothing outside it.\n");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
out.push("Summary: " + total.violations + " contract violations, " + total.reviews + " needs review.");
|
|
113
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function outputJson(summary, results, total) {
|
|
117
|
+
const payload = {
|
|
118
|
+
tool: PACK.name,
|
|
119
|
+
version: PACK.version,
|
|
120
|
+
note: "Text analysis only. Values through var(), calc(), clamp() or keywords are skipped. Rule IDs are stable identifiers.",
|
|
121
|
+
summary: total,
|
|
122
|
+
results: results
|
|
123
|
+
};
|
|
124
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function main() {
|
|
128
|
+
const args = parseArgs(process.argv.slice(2));
|
|
129
|
+
if (args.help) {
|
|
130
|
+
usage();
|
|
131
|
+
process.exit(EXIT_CLEAN);
|
|
132
|
+
}
|
|
133
|
+
if (!args.contractPath) fail("Missing --contract.", "Run with --help for the format.");
|
|
134
|
+
if (args.sources.length === 0) fail("No source files given.", "Pass one or more CSS, HTML or text files after --contract <file>.");
|
|
135
|
+
|
|
136
|
+
const contractText = readFileChecked(
|
|
137
|
+
path.resolve(args.contractPath),
|
|
138
|
+
engine.MAX_CONTRACT_CHARS,
|
|
139
|
+
"Limit: 12000 contract characters."
|
|
140
|
+
);
|
|
141
|
+
const contract = engine.parseContract(contractText);
|
|
142
|
+
const supplied = Object.keys(contract).filter(function (k) { return contract[k].length > 0; });
|
|
143
|
+
if (supplied.length === 0) {
|
|
144
|
+
fail("The contract format was not recognised.",
|
|
145
|
+
"Use lines like: colors = #f2eee5, #18231d (names: colors, fonts, font-sizes, spacing, radii).");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const results = [];
|
|
149
|
+
args.sources.forEach(function (sourcePath) {
|
|
150
|
+
const sourceText = readFileChecked(
|
|
151
|
+
path.resolve(sourcePath),
|
|
152
|
+
engine.MAX_SOURCE_CHARS,
|
|
153
|
+
"Limit: 100000 source characters per file."
|
|
154
|
+
);
|
|
155
|
+
const r = engine.scan(contract, sourceText);
|
|
156
|
+
results.push({
|
|
157
|
+
file: path.relative(process.cwd(), path.resolve(sourcePath)) || sourcePath,
|
|
158
|
+
summary: engine.summarize(r),
|
|
159
|
+
findings: r.findings
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const total = {
|
|
164
|
+
violations: results.reduce(function (s, r) { return s + r.summary.violations; }, 0),
|
|
165
|
+
reviews: results.reduce(function (s, r) { return s + r.summary.reviews; }, 0),
|
|
166
|
+
lines: results.reduce(function (s, r) { return s + r.summary.lines; }, 0)
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (args.json) outputJson(total, results, total);
|
|
170
|
+
else outputText(total, results, total);
|
|
171
|
+
|
|
172
|
+
process.exit(total.violations > 0 ? EXIT_FINDINGS : EXIT_CLEAN);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
main();
|
package/lib/engine.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Guard Lite engine — headless extraction of the checker core from
|
|
5
|
+
* guard-lite src/guard.js. Behavior parity with the web checker:
|
|
6
|
+
* same contracts, same normalization, same five stable rule IDs,
|
|
7
|
+
* same skip conditions. No DOM, no dependencies.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MAX_CONTRACT_CHARS = 12000;
|
|
11
|
+
const MAX_SOURCE_CHARS = 100000;
|
|
12
|
+
|
|
13
|
+
const SAMPLE_CONTRACT = `colors = #f2eee5, #18231d, #a95038, #ffffff
|
|
14
|
+
fonts = Georgia, Arial
|
|
15
|
+
font-sizes = 16px, 24px, 48px
|
|
16
|
+
spacing = 8px, 16px, 24px, 48px
|
|
17
|
+
radii = 0, 2px, 4px`;
|
|
18
|
+
|
|
19
|
+
const SAMPLE_SOURCE = `.hero {
|
|
20
|
+
color: #18231d;
|
|
21
|
+
background: #f2eee5;
|
|
22
|
+
padding: 48px 23px;
|
|
23
|
+
gap: 24px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.hero h1 {
|
|
27
|
+
font-family: Georgia, serif;
|
|
28
|
+
font-size: 52px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.hero .primary-action {
|
|
32
|
+
color: #ffffff;
|
|
33
|
+
background-color: #675ff0;
|
|
34
|
+
border-radius: 999px;
|
|
35
|
+
padding: 15px 24px;
|
|
36
|
+
}`;
|
|
37
|
+
|
|
38
|
+
const RULE_META = {
|
|
39
|
+
colors: {
|
|
40
|
+
id: "PPG-COLOR-001",
|
|
41
|
+
title: "Colour outside the declared palette",
|
|
42
|
+
repair: "Replace the literal colour with the correct named colour token. Confirm the semantic role before changing it."
|
|
43
|
+
},
|
|
44
|
+
fonts: {
|
|
45
|
+
id: "PPG-TYPE-001",
|
|
46
|
+
title: "Font family outside the declared type system",
|
|
47
|
+
repair: "Use an approved font-family token. Keep an appropriate fallback after the approved family."
|
|
48
|
+
},
|
|
49
|
+
"font-sizes": {
|
|
50
|
+
id: "PPG-TYPE-002",
|
|
51
|
+
title: "Font size outside the declared scale",
|
|
52
|
+
repair: "Map the text role to the closest approved type token. Review hierarchy before rounding the value."
|
|
53
|
+
},
|
|
54
|
+
spacing: {
|
|
55
|
+
id: "PPG-SPACE-001",
|
|
56
|
+
title: "Spacing value outside the declared scale",
|
|
57
|
+
repair: "Replace the raw spacing value with the intended spacing token. Check the surrounding rhythm at each viewport."
|
|
58
|
+
},
|
|
59
|
+
radii: {
|
|
60
|
+
id: "PPG-RADIUS-001",
|
|
61
|
+
title: "Corner radius outside the declared set",
|
|
62
|
+
repair: "Use the radius assigned to this component type. Do not replace every radius without checking component roles."
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function cleanLine(line) {
|
|
67
|
+
return line.trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeHex(value) {
|
|
71
|
+
const v = value.toLowerCase();
|
|
72
|
+
if (/^#[0-9a-f]{3}$/.test(v)) {
|
|
73
|
+
return "#" + v.slice(1).split("").map(function (c) { return c + c; }).join("");
|
|
74
|
+
}
|
|
75
|
+
if (/^#[0-9a-f]{4}$/.test(v)) {
|
|
76
|
+
return "#" + v.slice(1).split("").map(function (c) { return c + c; }).join("");
|
|
77
|
+
}
|
|
78
|
+
return v.replace(/\s+/g, "");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeMeasure(value) {
|
|
82
|
+
const v = value.toLowerCase().replace(/\s+/g, "");
|
|
83
|
+
if (/^-?0+(?:\.0+)?(?:px|rem|em)?$/.test(v)) return "0";
|
|
84
|
+
return v.replace(/^(-?)\./, "$10.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeFont(value) {
|
|
88
|
+
return value.trim().replace(/^['"]|['"]$/g, "").toLowerCase();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function splitContractValues(value) {
|
|
92
|
+
const values = [];
|
|
93
|
+
let current = "";
|
|
94
|
+
let depth = 0;
|
|
95
|
+
let quote = "";
|
|
96
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
97
|
+
const char = value[i];
|
|
98
|
+
if (quote) {
|
|
99
|
+
current += char;
|
|
100
|
+
if (char === quote && value[i - 1] !== "\\") quote = "";
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (char === "'" || char === '"') {
|
|
104
|
+
quote = char;
|
|
105
|
+
current += char;
|
|
106
|
+
} else if (char === "(") {
|
|
107
|
+
depth += 1;
|
|
108
|
+
current += char;
|
|
109
|
+
} else if (char === ")") {
|
|
110
|
+
depth = Math.max(0, depth - 1);
|
|
111
|
+
current += char;
|
|
112
|
+
} else if (char === "," && depth === 0) {
|
|
113
|
+
if (current.trim()) values.push(current.trim());
|
|
114
|
+
current = "";
|
|
115
|
+
} else {
|
|
116
|
+
current += char;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (current.trim()) values.push(current.trim());
|
|
120
|
+
return values;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseContract(text) {
|
|
124
|
+
const contract = { colors: [], fonts: [], "font-sizes": [], spacing: [], radii: [] };
|
|
125
|
+
text.split(/\r?\n/).forEach(function (rawLine) {
|
|
126
|
+
const line = cleanLine(rawLine);
|
|
127
|
+
if (!line) return;
|
|
128
|
+
const match = line.match(/^\s*(colors?|fonts?|font-sizes?|type-sizes?|spacing|radii|radius)\s*[:=]\s*(.+)$/i);
|
|
129
|
+
if (!match) return;
|
|
130
|
+
let category = match[1].toLowerCase();
|
|
131
|
+
if (category === "color") category = "colors";
|
|
132
|
+
if (category === "font") category = "fonts";
|
|
133
|
+
if (category === "font-size" || category === "type-size" || category === "type-sizes") category = "font-sizes";
|
|
134
|
+
if (category === "radius") category = "radii";
|
|
135
|
+
contract[category] = contract[category].concat(splitContractValues(match[2]));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
contract.colors = contract.colors.map(normalizeHex);
|
|
139
|
+
contract.fonts = contract.fonts.map(normalizeFont);
|
|
140
|
+
contract["font-sizes"] = contract["font-sizes"].map(normalizeMeasure);
|
|
141
|
+
contract.spacing = contract.spacing.map(normalizeMeasure);
|
|
142
|
+
contract.radii = contract.radii.map(normalizeMeasure);
|
|
143
|
+
return contract;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function inertSource(text) {
|
|
147
|
+
return text
|
|
148
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, function (block) { return block.replace(/[^\n]/g, " "); })
|
|
149
|
+
.replace(/\/\*[\s\S]*?\*\//g, function (block) { return block.replace(/[^\n]/g, " "); });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function declarationValues(line, propertyPattern) {
|
|
153
|
+
const values = [];
|
|
154
|
+
const valuePattern = "((?:\\\"[^\\\"\\n]*\\\"|'[^'\\n]*'|[^;}\\\"'\\n])+)";
|
|
155
|
+
const re = new RegExp("(?:^|[;{\\s\\\"])(" + propertyPattern + ")\\s*:\\s*" + valuePattern, "gi");
|
|
156
|
+
let match;
|
|
157
|
+
while ((match = re.exec(line)) !== null) {
|
|
158
|
+
values.push({ property: match[1].toLowerCase(), value: match[2].trim() });
|
|
159
|
+
}
|
|
160
|
+
return values;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function skipExpression(value) {
|
|
164
|
+
return /\b(?:var|calc|clamp|min|max)\s*\(/i.test(value);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function addFinding(list, seen, category, status, line, property, found, expected) {
|
|
168
|
+
const meta = RULE_META[category];
|
|
169
|
+
const key = [meta.id, status, line, property, found].join("|");
|
|
170
|
+
if (seen.has(key)) return;
|
|
171
|
+
seen.add(key);
|
|
172
|
+
list.push({
|
|
173
|
+
category: category,
|
|
174
|
+
id: meta.id,
|
|
175
|
+
title: meta.title,
|
|
176
|
+
status: status,
|
|
177
|
+
line: line,
|
|
178
|
+
property: property,
|
|
179
|
+
found: found,
|
|
180
|
+
expected: expected,
|
|
181
|
+
repair: meta.repair
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function compareValue(list, seen, contract, category, lineNumber, property, rawValue, normalized) {
|
|
186
|
+
const allowed = contract[category] || [];
|
|
187
|
+
if (allowed.length === 0) {
|
|
188
|
+
addFinding(list, seen, category, "Review", lineNumber, property, rawValue, "No approved values were supplied for this category.");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (!allowed.includes(normalized)) {
|
|
192
|
+
addFinding(list, seen, category, "Violation", lineNumber, property, rawValue, allowed.join(", "));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function scan(contract, source) {
|
|
197
|
+
const findings = [];
|
|
198
|
+
const seen = new Set();
|
|
199
|
+
const lines = inertSource(source).split(/\r?\n/);
|
|
200
|
+
|
|
201
|
+
lines.forEach(function (line, index) {
|
|
202
|
+
const lineNumber = index + 1;
|
|
203
|
+
|
|
204
|
+
declarationValues(line, "color|background|background-color|border(?:-(?:top|right|bottom|left|inline(?:-start|-end)?|block(?:-start|-end)?))?(?:-color)?|outline-color|fill|stroke").forEach(function (decl) {
|
|
205
|
+
if (skipExpression(decl.value)) return;
|
|
206
|
+
const matches = decl.value.match(/#[0-9a-f]{3,8}\b|(?:rgb|hsl)a?\([^)]*\)/gi) || [];
|
|
207
|
+
matches.forEach(function (value) {
|
|
208
|
+
compareValue(findings, seen, contract, "colors", lineNumber, decl.property, value, normalizeHex(value));
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
declarationValues(line, "font-family").forEach(function (decl) {
|
|
213
|
+
if (skipExpression(decl.value)) return;
|
|
214
|
+
const firstFamily = decl.value.split(",")[0].trim();
|
|
215
|
+
compareValue(findings, seen, contract, "fonts", lineNumber, decl.property, firstFamily, normalizeFont(firstFamily));
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
declarationValues(line, "font-size").forEach(function (decl) {
|
|
219
|
+
if (skipExpression(decl.value)) return;
|
|
220
|
+
const values = decl.value.match(/-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em)\b/gi) || [];
|
|
221
|
+
values.forEach(function (value) {
|
|
222
|
+
compareValue(findings, seen, contract, "font-sizes", lineNumber, decl.property, value, normalizeMeasure(value));
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
declarationValues(line, "margin(?:-[a-z]+)?|padding(?:-[a-z]+)?|gap|row-gap|column-gap").forEach(function (decl) {
|
|
227
|
+
if (skipExpression(decl.value)) return;
|
|
228
|
+
const values = decl.value.match(/-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em)\b|(?:^|\s)0(?=\s|$)/gi) || [];
|
|
229
|
+
values.forEach(function (value) {
|
|
230
|
+
const clean = value.trim();
|
|
231
|
+
compareValue(findings, seen, contract, "spacing", lineNumber, decl.property, clean, normalizeMeasure(clean));
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
declarationValues(line, "border-radius|border-(?:top|bottom)-(?:left|right)-radius").forEach(function (decl) {
|
|
236
|
+
if (skipExpression(decl.value)) return;
|
|
237
|
+
const values = decl.value.match(/(?:\d+\.?\d*|\.\d+)(?:px|rem|em)\b|(?:^|\s)0(?=\s|$)/gi) || [];
|
|
238
|
+
values.forEach(function (value) {
|
|
239
|
+
const clean = value.trim();
|
|
240
|
+
compareValue(findings, seen, contract, "radii", lineNumber, decl.property, clean, normalizeMeasure(clean));
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
return { findings: findings, lines: lines.length };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function summarize(result) {
|
|
249
|
+
return {
|
|
250
|
+
violations: result.findings.filter(function (f) { return f.status === "Violation"; }).length,
|
|
251
|
+
reviews: result.findings.filter(function (f) { return f.status === "Review"; }).length,
|
|
252
|
+
lines: result.lines
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = {
|
|
257
|
+
MAX_CONTRACT_CHARS: MAX_CONTRACT_CHARS,
|
|
258
|
+
MAX_SOURCE_CHARS: MAX_SOURCE_CHARS,
|
|
259
|
+
SAMPLE_CONTRACT: SAMPLE_CONTRACT,
|
|
260
|
+
SAMPLE_SOURCE: SAMPLE_SOURCE,
|
|
261
|
+
RULE_META: RULE_META,
|
|
262
|
+
parseContract: parseContract,
|
|
263
|
+
scan: scan,
|
|
264
|
+
summarize: summarize
|
|
265
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "design-contract-checker",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Guard Lite: check CSS or HTML text against your declared design contract (colors, fonts, font sizes, spacing, radii). Five stable checks, zero dependencies.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Pixel Perfect <https://pixel-perfect-system.netlify.app/>",
|
|
7
|
+
"homepage": "https://pixel-perfect-system.netlify.app/guard/",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/thedollartoolkit-boop/guard-lite.git",
|
|
11
|
+
"directory": "cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/thedollartoolkit-boop/guard-lite/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"design-system",
|
|
16
|
+
"design-tokens",
|
|
17
|
+
"css",
|
|
18
|
+
"lint",
|
|
19
|
+
"linting",
|
|
20
|
+
"checker",
|
|
21
|
+
"coding-agent",
|
|
22
|
+
"ai",
|
|
23
|
+
"guard-lite",
|
|
24
|
+
"pixel-perfect"
|
|
25
|
+
],
|
|
26
|
+
"type": "commonjs",
|
|
27
|
+
"main": "lib/engine.js",
|
|
28
|
+
"bin": {
|
|
29
|
+
"design-contract-checker": "bin/cli.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"lib/",
|
|
33
|
+
"bin/",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=14"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "node test/run.js"
|
|
42
|
+
}
|
|
43
|
+
}
|