@domandigital/craft 0.10.0 → 0.12.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 +35 -0
- package/CHARACTER.md +9 -1
- package/COPY.md +6 -2
- package/README.md +40 -0
- package/dist/{audit-EGDNWSQB.js → audit-5QWFLZ6L.js} +6 -3
- package/dist/audit.cjs +279 -16
- package/dist/audit.d.cts +81 -4
- package/dist/audit.d.ts +81 -4
- package/dist/audit.js +278 -16
- package/dist/{chunk-3NGWXZZE.js → chunk-AL6MRG6V.js} +40 -3
- package/dist/{chunk-2NQFIDLE.js → chunk-CDAJUS5U.js} +13 -11
- package/dist/{chunk-JKIDWHG3.js → chunk-J3QUROJ5.js} +299 -32
- package/dist/chunk-JYNXGCYZ.js +75 -0
- package/dist/{chunk-APO4BT2F.js → chunk-LHGY43LL.js} +11 -7
- package/dist/chunk-PU5G5DUC.js +206 -0
- package/dist/{chunk-QMYERCBB.js → chunk-PZ4MF2VV.js} +19 -7
- package/dist/{chunk-JWHC67ZS.js → chunk-S3NZAVU6.js} +121 -1
- package/dist/{chunk-TSKRBIOC.js → chunk-ULPI335S.js} +181 -13
- package/dist/chunk-ZKVJ7K6X.js +19 -0
- package/dist/cli-2DY3V3SF.js +17 -0
- package/dist/{cli-PRWTNFVP.js → cli-C3JLFXMG.js} +5 -4
- package/dist/cli-QM2AQQMF.js +19 -0
- package/dist/{cli-ADQQURNR.js → cli-ZB2VJ7CY.js} +25 -17
- package/dist/{cli-2WBGTBKD.js → cli-ZPJIWVEP.js} +15 -80
- package/dist/cli.js +2 -2
- package/dist/index.cjs +486 -9
- package/dist/index.d.cts +364 -6
- package/dist/index.d.ts +364 -6
- package/dist/index.js +462 -9
- package/licences.json +104 -0
- package/package.json +15 -7
- package/word-lists.json +1 -1
- package/dist/chunk-U2JTFQKO.js +0 -112
- package/dist/cli-EK3GSAOW.js +0 -15
- package/dist/cli-VQUNNIZM.js +0 -16
|
@@ -1,20 +1,210 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CATALOGUE,
|
|
3
3
|
CATALOGUE_VERSION,
|
|
4
|
+
HOUSE_LOCKED,
|
|
4
5
|
applyHouseGate,
|
|
5
6
|
checkCopy,
|
|
6
7
|
extractCopy,
|
|
7
8
|
extractStrings,
|
|
8
9
|
fileKind,
|
|
10
|
+
holdHouseBlocks,
|
|
9
11
|
houseRule,
|
|
10
12
|
scanSource
|
|
11
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-AL6MRG6V.js";
|
|
14
|
+
|
|
15
|
+
// src/character/config.ts
|
|
16
|
+
var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
|
|
17
|
+
var KEYS = /* @__PURE__ */ new Set(["$schema", "ignore", "copyPaths", "severity"]);
|
|
18
|
+
var LEVELS = /* @__PURE__ */ new Set(["warn", "block", "off"]);
|
|
19
|
+
var MIN_BECAUSE = 12;
|
|
20
|
+
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
21
|
+
var isStringArray = (v) => Array.isArray(v) && v.every((s) => typeof s === "string");
|
|
22
|
+
function parseCraftConfig(data, knownTells, source = "craft.config.json") {
|
|
23
|
+
if (!isRecord(data)) throw new Error(`${source}: expected an object`);
|
|
24
|
+
for (const key of Object.keys(data)) if (!KEYS.has(key)) throw new Error(`${source}: unknown key "${key}"`);
|
|
25
|
+
const ignore = data.ignore ?? [];
|
|
26
|
+
if (!isStringArray(ignore)) throw new Error(`${source}: "ignore" must be an array of glob strings`);
|
|
27
|
+
if (data.copyPaths !== void 0 && !isStringArray(data.copyPaths)) throw new Error(`${source}: "copyPaths" must be an array of paths`);
|
|
28
|
+
const severity = {};
|
|
29
|
+
const raw = data.severity ?? {};
|
|
30
|
+
if (!isRecord(raw)) throw new Error(`${source}: "severity" must map a tell id to { level, because }`);
|
|
31
|
+
for (const [tell, value] of Object.entries(raw)) {
|
|
32
|
+
if (!knownTells.has(tell)) throw new Error(`${source}: severity names no tell called "${tell}"`);
|
|
33
|
+
if (!isRecord(value) || !LEVELS.has(value.level)) {
|
|
34
|
+
throw new Error(`${source}: severity.${tell} needs a level of "warn", "block" or "off"`);
|
|
35
|
+
}
|
|
36
|
+
if (typeof value.because !== "string" || value.because.trim().length < MIN_BECAUSE) {
|
|
37
|
+
throw new Error(`${source}: severity.${tell} needs a because of at least a sentence`);
|
|
38
|
+
}
|
|
39
|
+
severity[tell] = { level: value.level, because: value.because };
|
|
40
|
+
}
|
|
41
|
+
return { ignore, severity, ...data.copyPaths ? { copyPaths: data.copyPaths } : {} };
|
|
42
|
+
}
|
|
43
|
+
function globToRegExp(glob) {
|
|
44
|
+
const pattern = glob.replace(/^\.\//, "").replace(/\/$/, "").replace(/\/\*\*$/, "");
|
|
45
|
+
let body = "";
|
|
46
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
47
|
+
const c = pattern[i];
|
|
48
|
+
if (c === "*" && pattern[i + 1] === "*") {
|
|
49
|
+
const slash = pattern[i + 2] === "/";
|
|
50
|
+
body += slash ? "(?:.*/)?" : ".*";
|
|
51
|
+
i += slash ? 2 : 1;
|
|
52
|
+
} else if (c === "*") body += "[^/]*";
|
|
53
|
+
else if (c === "?") body += "[^/]";
|
|
54
|
+
else body += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
55
|
+
}
|
|
56
|
+
const anywhere = !pattern.includes("/");
|
|
57
|
+
return new RegExp(`^${anywhere ? "(?:.*/)?" : ""}${body}(?:/.*)?$`);
|
|
58
|
+
}
|
|
59
|
+
function ignoreMatcher(patterns) {
|
|
60
|
+
const res = patterns.map(globToRegExp);
|
|
61
|
+
return (path) => {
|
|
62
|
+
const p = path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
63
|
+
return res.some((re) => re.test(p));
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function severityExceptions(config) {
|
|
67
|
+
return Object.entries(config.severity).filter(([, o]) => o.level === "off").map(([tell, o]) => ({ tell, because: `craft.config.json: ${o.because}` }));
|
|
68
|
+
}
|
|
69
|
+
function applySeverity(report, config) {
|
|
70
|
+
const findings = report.findings.map((f) => {
|
|
71
|
+
const o = config.severity[f.tell];
|
|
72
|
+
return o && o.level !== "off" ? { ...f, severity: o.level } : f;
|
|
73
|
+
});
|
|
74
|
+
return { ...report, findings, summary: { ...report.summary, blocking: findings.filter((f) => f.severity === "block").length } };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/character/json.ts
|
|
78
|
+
var JSON_SCHEMA_VERSION = 1;
|
|
79
|
+
function toJson(data) {
|
|
80
|
+
return JSON.stringify({ schemaVersion: JSON_SCHEMA_VERSION, ...data }, null, 2);
|
|
81
|
+
}
|
|
12
82
|
|
|
13
83
|
// src/character/cli.ts
|
|
14
84
|
import { execFileSync } from "child_process";
|
|
15
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
85
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
16
86
|
import { join, relative, resolve } from "path";
|
|
17
87
|
|
|
88
|
+
// src/character/baseline.ts
|
|
89
|
+
var BASELINE_VERSION = 1;
|
|
90
|
+
function fnv1a(text) {
|
|
91
|
+
let hash = 2166136261;
|
|
92
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
93
|
+
hash ^= text.charCodeAt(i);
|
|
94
|
+
hash = Math.imul(hash, 16777619);
|
|
95
|
+
}
|
|
96
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
97
|
+
}
|
|
98
|
+
var excerptHash = (f) => fnv1a(f.excerpt.replace(/\s+/g, " ").trim());
|
|
99
|
+
var keyOf = (tell, path, excerpt) => `${tell}\0${path}\0${excerpt}`;
|
|
100
|
+
function findingKey(f) {
|
|
101
|
+
return keyOf(f.tell, f.path, excerptHash(f));
|
|
102
|
+
}
|
|
103
|
+
function createBaseline(report) {
|
|
104
|
+
const counts = /* @__PURE__ */ new Map();
|
|
105
|
+
for (const f of report.findings) {
|
|
106
|
+
const excerpt = excerptHash(f);
|
|
107
|
+
const key = keyOf(f.tell, f.path, excerpt);
|
|
108
|
+
const entry = counts.get(key);
|
|
109
|
+
if (entry) entry.count += 1;
|
|
110
|
+
else counts.set(key, { tell: f.tell, path: f.path, excerpt, count: 1 });
|
|
111
|
+
}
|
|
112
|
+
const entries = [...counts.values()].sort((a, b) => a.path.localeCompare(b.path) || a.tell.localeCompare(b.tell) || a.excerpt.localeCompare(b.excerpt));
|
|
113
|
+
return { version: BASELINE_VERSION, catalogueVersion: report.catalogueVersion, entries };
|
|
114
|
+
}
|
|
115
|
+
function parseBaseline(data, source = "baseline") {
|
|
116
|
+
const b = data;
|
|
117
|
+
if (!b || typeof b !== "object" || b.version !== BASELINE_VERSION || !Array.isArray(b.entries)) {
|
|
118
|
+
throw new Error(`${source}: not a craft baseline (version ${BASELINE_VERSION})`);
|
|
119
|
+
}
|
|
120
|
+
for (const e of b.entries) {
|
|
121
|
+
if (typeof e?.tell !== "string" || typeof e.path !== "string" || typeof e.excerpt !== "string" || !Number.isInteger(e.count) || e.count < 1) {
|
|
122
|
+
throw new Error(`${source}: malformed entry ${JSON.stringify(e)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return b;
|
|
126
|
+
}
|
|
127
|
+
function recount(report, findings) {
|
|
128
|
+
const byGeneration = { 1: 0, 2: 0, 3: 0 };
|
|
129
|
+
const byTell = {};
|
|
130
|
+
for (const f of findings) {
|
|
131
|
+
byGeneration[f.generation] += 1;
|
|
132
|
+
byTell[f.tell] = (byTell[f.tell] ?? 0) + 1;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
...report,
|
|
136
|
+
findings,
|
|
137
|
+
summary: { ...report.summary, findings: findings.length, byGeneration, byTell, blocking: findings.filter((f) => f.severity === "block").length }
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function applyBaseline(report, baseline) {
|
|
141
|
+
const remaining = /* @__PURE__ */ new Map();
|
|
142
|
+
for (const e of baseline.entries) {
|
|
143
|
+
const key = keyOf(e.tell, e.path, e.excerpt);
|
|
144
|
+
remaining.set(key, (remaining.get(key) ?? 0) + e.count);
|
|
145
|
+
}
|
|
146
|
+
const kept = [];
|
|
147
|
+
let baselined = 0;
|
|
148
|
+
for (const f of report.findings) {
|
|
149
|
+
const key = findingKey(f);
|
|
150
|
+
const left = remaining.get(key) ?? 0;
|
|
151
|
+
if (left > 0) {
|
|
152
|
+
remaining.set(key, left - 1);
|
|
153
|
+
baselined += 1;
|
|
154
|
+
} else kept.push(f);
|
|
155
|
+
}
|
|
156
|
+
return { ...recount(report, kept), baselined };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/character/sarif.ts
|
|
160
|
+
var isUrl = (path) => /^[a-z][a-z0-9+.-]*:\/\//i.test(path);
|
|
161
|
+
function toSarif(report, options) {
|
|
162
|
+
const byId = new Map(options.tells.map((t) => [t.id, t]));
|
|
163
|
+
const ids = [...new Set(report.findings.map((f) => f.tell))].sort();
|
|
164
|
+
const index = new Map(ids.map((id, i) => [id, i]));
|
|
165
|
+
const rules = ids.map((id) => {
|
|
166
|
+
const t = byId.get(id);
|
|
167
|
+
return {
|
|
168
|
+
id,
|
|
169
|
+
name: t?.name ?? id,
|
|
170
|
+
shortDescription: { text: t?.name ?? id },
|
|
171
|
+
...t ? { fullDescription: { text: t.why }, help: { text: t.fix } } : {}
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
const results = report.findings.map((f) => ({
|
|
175
|
+
ruleId: f.tell,
|
|
176
|
+
ruleIndex: index.get(f.tell),
|
|
177
|
+
level: f.severity === "block" ? "error" : "warning",
|
|
178
|
+
message: { text: `${f.message}. ${f.fix}` },
|
|
179
|
+
locations: [
|
|
180
|
+
{
|
|
181
|
+
physicalLocation: {
|
|
182
|
+
artifactLocation: { uri: isUrl(f.path) ? f.path : f.path.replace(/\\/g, "/") },
|
|
183
|
+
...f.line > 0 ? { region: { startLine: f.line, snippet: { text: f.excerpt } } } : {}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
partialFingerprints: { "craftFinding/v1": fnv1a(findingKey(f)) }
|
|
188
|
+
}));
|
|
189
|
+
return {
|
|
190
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
191
|
+
version: "2.1.0",
|
|
192
|
+
runs: [
|
|
193
|
+
{
|
|
194
|
+
tool: {
|
|
195
|
+
driver: {
|
|
196
|
+
name: "craft",
|
|
197
|
+
version: report.catalogueVersion,
|
|
198
|
+
informationUri: options.informationUri ?? "https://www.npmjs.com/package/@domandigital/craft",
|
|
199
|
+
rules
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
results
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
18
208
|
// src/character/facts.ts
|
|
19
209
|
var MONTH = "(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)";
|
|
20
210
|
var PATTERNS = [
|
|
@@ -252,14 +442,15 @@ var NOT_SITE_COPY = /^(?:README|CHANGELOG|CLAUDE|AGENTS|CONTRIBUTING|LICENSE|COD
|
|
|
252
442
|
var HELP = `craft: find the AI look and say what to do instead.
|
|
253
443
|
|
|
254
444
|
Usage
|
|
255
|
-
craft scan [paths...] [--staged] [--json] [--strict] [--direction <file>]
|
|
256
|
-
craft copy
|
|
445
|
+
craft scan [paths...] [--staged] [--json] [--strict] [--direction <file>] [ci options]
|
|
446
|
+
craft copy [paths...] [--gate] [--json] [--strict] [--direction <file>] [ci options]
|
|
257
447
|
craft copy compare <before> <after> [--json]
|
|
258
448
|
craft copy claims <paths...> [--json]
|
|
259
449
|
craft tells list [--json]
|
|
260
450
|
craft tells harvest <null.json | dir>... [--share 0.25] [--json]
|
|
261
451
|
craft snapshot <url> [--out <file>] [--width <px>] [--height <px>]
|
|
262
|
-
craft audit <url | snapshot.json> [--repo <dir>] [--null <null.json>] [--out <file>] [--json] [--strict]
|
|
452
|
+
craft audit <url | snapshot.json> [--repo <dir>] [--null <null.json>] [--out <file>] [--json] [--strict] [ci options]
|
|
453
|
+
craft audit --pages <sitemap.xml | urls.txt> [--viewport 390,768,1440] [the same]
|
|
263
454
|
craft direction init [--snapshot <file>] [--client <name>] [--out <file>]
|
|
264
455
|
craft direction validate [--snapshot <file>] [--direction <file>] [--json]
|
|
265
456
|
craft direction propose [--snapshot <file>] [--estate <estate.json | dir>] [--out <file>]
|
|
@@ -287,6 +478,8 @@ audit Judge a rendered page (live, or a saved snapshot) and fingerprint it.
|
|
|
287
478
|
--repo also scans that site's source, so one report covers both.
|
|
288
479
|
--null scores how typical it is against a null model (comma-separate
|
|
289
480
|
several to pool them).
|
|
481
|
+
--pages audits every URL in a sitemap (a URL or a file) or a file of
|
|
482
|
+
URLs, one per line. --viewport audits each page at these widths.
|
|
290
483
|
snapshot and audit need Playwright: npm i -D playwright.
|
|
291
484
|
direction The site's art-direction.json: every choice with a reason from the
|
|
292
485
|
client's world. init writes today's choices with empty reasons;
|
|
@@ -303,8 +496,18 @@ report Every signal for one site: tells, typicality, the estate, the reasons.
|
|
|
303
496
|
retrofit The report as a checklist: decide, then change type, colour, shape,
|
|
304
497
|
effects, motion and copy. Never the page grammar.
|
|
305
498
|
|
|
499
|
+
ci options (scan, copy, audit)
|
|
500
|
+
--baseline <file> Report only findings the baseline does not already
|
|
501
|
+
hold. Keyed on tell, path and excerpt, not line.
|
|
502
|
+
--update-baseline Write every current finding to --baseline, exit 0.
|
|
503
|
+
--sarif <file> Also write SARIF 2.1.0, for GitHub code scanning.
|
|
504
|
+
--config <file> craft.config.json, default: the working directory.
|
|
505
|
+
|
|
306
506
|
Exceptions come from art-direction.json in the working directory, or --direction.
|
|
307
|
-
|
|
507
|
+
craft.config.json holds ignore globs, copyPaths, and severity changes, each
|
|
508
|
+
with a because. .claude, .agents and .cursor are never walked.
|
|
509
|
+
Every tell ships as warn: exit 1 only on a block, or on any finding with --strict.
|
|
510
|
+
--json output always carries schemaVersion.`;
|
|
308
511
|
var VALUE_FLAGS = {
|
|
309
512
|
"--direction": "direction",
|
|
310
513
|
"--out": "out",
|
|
@@ -320,16 +523,22 @@ var VALUE_FLAGS = {
|
|
|
320
523
|
"--model": "model",
|
|
321
524
|
"--share": "share",
|
|
322
525
|
"--null": "null",
|
|
323
|
-
"--id": "id"
|
|
526
|
+
"--id": "id",
|
|
527
|
+
"--config": "config",
|
|
528
|
+
"--baseline": "baseline",
|
|
529
|
+
"--sarif": "sarif",
|
|
530
|
+
"--pages": "pages",
|
|
531
|
+
"--viewport": "viewport"
|
|
324
532
|
};
|
|
325
533
|
function parseFlags(args) {
|
|
326
|
-
const flags = { positional: [], json: false, strict: false, staged: false, gate: false };
|
|
534
|
+
const flags = { positional: [], json: false, strict: false, staged: false, gate: false, updateBaseline: false };
|
|
327
535
|
for (let i = 0; i < args.length; i += 1) {
|
|
328
536
|
const a = args[i];
|
|
329
537
|
if (a === "--json") flags.json = true;
|
|
330
538
|
else if (a === "--strict") flags.strict = true;
|
|
331
539
|
else if (a === "--staged") flags.staged = true;
|
|
332
540
|
else if (a === "--gate") flags.gate = true;
|
|
541
|
+
else if (a === "--update-baseline") flags.updateBaseline = true;
|
|
333
542
|
else if (a in VALUE_FLAGS) {
|
|
334
543
|
const value = args[i + 1];
|
|
335
544
|
i += 1;
|
|
@@ -340,48 +549,86 @@ function parseFlags(args) {
|
|
|
340
549
|
}
|
|
341
550
|
return flags;
|
|
342
551
|
}
|
|
343
|
-
function walk(root, cwd, into, wanted, skipDir = (_) => false) {
|
|
552
|
+
function walk(root, cwd, into, wanted, skipDir = (_) => false, ignored = (_) => false) {
|
|
344
553
|
const stat = statSync(root);
|
|
345
554
|
if (stat.isFile()) {
|
|
346
555
|
if (stat.size <= MAX_BYTES) into.push({ path: relative(cwd, root) || root, text: readFileSync(root, "utf8") });
|
|
347
556
|
return;
|
|
348
557
|
}
|
|
349
558
|
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
559
|
+
const path = join(root, entry.name);
|
|
560
|
+
if (ignored(relative(cwd, path))) continue;
|
|
350
561
|
if (entry.isDirectory()) {
|
|
351
|
-
if (!SKIP_DIRS.has(entry.name) && !skipDir(entry.name)) walk(
|
|
562
|
+
if (!SKIP_DIRS.has(entry.name) && !skipDir(entry.name)) walk(path, cwd, into, wanted, skipDir, ignored);
|
|
352
563
|
} else if (entry.isFile() && wanted(entry.name)) {
|
|
353
|
-
walk(
|
|
564
|
+
walk(path, cwd, into, wanted, skipDir, ignored);
|
|
354
565
|
}
|
|
355
566
|
}
|
|
356
567
|
}
|
|
357
|
-
function readPaths(paths, cwd, wanted, skipDir) {
|
|
568
|
+
function readPaths(paths, cwd, wanted, skipDir, ignore = []) {
|
|
358
569
|
const files = [];
|
|
570
|
+
const ignored = ignoreMatcher([...DEFAULT_IGNORE, ...ignore]);
|
|
359
571
|
for (const p of paths) {
|
|
360
572
|
const abs = resolve(cwd, p);
|
|
361
573
|
if (!existsSync(abs)) throw new Error(`no such path: ${p}`);
|
|
362
|
-
walk(abs, cwd, files, wanted, skipDir);
|
|
574
|
+
walk(abs, cwd, files, wanted, skipDir, ignored);
|
|
363
575
|
}
|
|
364
576
|
return files;
|
|
365
577
|
}
|
|
366
|
-
function readStaged(cwd, wanted) {
|
|
367
|
-
const
|
|
578
|
+
function readStaged(cwd, wanted, ignore) {
|
|
579
|
+
const ignored = ignoreMatcher([...DEFAULT_IGNORE, ...ignore]);
|
|
580
|
+
const names = execFileSync("git", ["diff", "--cached", "--name-only", "--diff-filter=ACMR", "-z"], { cwd, encoding: "utf8" }).split("\0").filter((p) => p && wanted(p) && !ignored(p));
|
|
368
581
|
return names.map((path) => ({
|
|
369
582
|
path,
|
|
370
583
|
// The index, not the working tree: the commit is what gets checked.
|
|
371
584
|
text: execFileSync("git", ["show", `:${path}`], { cwd, encoding: "utf8", maxBuffer: MAX_BYTES * 4 })
|
|
372
585
|
}));
|
|
373
586
|
}
|
|
587
|
+
var NO_CONFIG = { ignore: [], severity: {} };
|
|
588
|
+
function loadConfig(flags, root) {
|
|
589
|
+
const path = resolve(root, flags.config ?? "craft.config.json");
|
|
590
|
+
if (!existsSync(path)) {
|
|
591
|
+
if (flags.config) throw new Error(`no such file: ${flags.config}`);
|
|
592
|
+
return NO_CONFIG;
|
|
593
|
+
}
|
|
594
|
+
return parseCraftConfig(JSON.parse(readFileSync(path, "utf8")), new Set(CATALOGUE.map((t) => t.id)), flags.config ?? "craft.config.json");
|
|
595
|
+
}
|
|
374
596
|
function loadExceptions(flags, cwd) {
|
|
597
|
+
const fromConfig = severityExceptions(loadConfig(flags, cwd));
|
|
375
598
|
const path = resolve(cwd, flags.direction ?? "art-direction.json");
|
|
376
599
|
if (!existsSync(path)) {
|
|
377
600
|
if (flags.direction) throw new Error(`no such file: ${flags.direction}`);
|
|
378
|
-
return
|
|
601
|
+
return fromConfig;
|
|
379
602
|
}
|
|
380
603
|
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
381
|
-
return Array.isArray(data.exceptions) ? data.exceptions : [];
|
|
604
|
+
return [...Array.isArray(data.exceptions) ? data.exceptions : [], ...fromConfig];
|
|
605
|
+
}
|
|
606
|
+
function prepareReport(report, flags, io, config) {
|
|
607
|
+
let result = applySeverity(report, config);
|
|
608
|
+
if (flags.updateBaseline) {
|
|
609
|
+
if (!flags.baseline) throw new Error("--update-baseline needs --baseline <file>");
|
|
610
|
+
const baseline = createBaseline(result);
|
|
611
|
+
writeFileSync(resolve(io.cwd, flags.baseline), `${JSON.stringify(baseline, null, 2)}
|
|
612
|
+
`);
|
|
613
|
+
io.out(`craft: baseline of ${result.findings.length} finding${result.findings.length === 1 ? "" : "s"} written to ${flags.baseline}`);
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
if (flags.baseline) {
|
|
617
|
+
const path = resolve(io.cwd, flags.baseline);
|
|
618
|
+
if (!existsSync(path)) throw new Error(`no such file: ${flags.baseline} (create it with --update-baseline)`);
|
|
619
|
+
result = applyBaseline(result, parseBaseline(JSON.parse(readFileSync(path, "utf8")), flags.baseline));
|
|
620
|
+
}
|
|
621
|
+
if (flags.sarif) {
|
|
622
|
+
writeFileSync(resolve(io.cwd, flags.sarif), `${JSON.stringify(toSarif(result, { tells: CATALOGUE }), null, 2)}
|
|
623
|
+
`);
|
|
624
|
+
}
|
|
625
|
+
return result;
|
|
382
626
|
}
|
|
383
627
|
function finish(report, flags, title, io) {
|
|
384
|
-
io.out(flags.json ?
|
|
628
|
+
io.out(flags.json ? toJson(report) : formatReport(report, title));
|
|
629
|
+
if (!flags.json && report.baselined) {
|
|
630
|
+
io.out(`${report.baselined} known finding${report.baselined === 1 ? "" : "s"} not shown: already in ${flags.baseline}.`);
|
|
631
|
+
}
|
|
385
632
|
if (report.summary.blocking > 0) return 1;
|
|
386
633
|
if (flags.strict && report.summary.findings > 0) return 1;
|
|
387
634
|
return 0;
|
|
@@ -392,25 +639,25 @@ var NOT_COPY_DIR = (name) => name.startsWith("_");
|
|
|
392
639
|
function run(argv, io) {
|
|
393
640
|
const [command, ...rest] = argv;
|
|
394
641
|
if (command === "direction") {
|
|
395
|
-
return import("./cli-
|
|
642
|
+
return import("./cli-ZPJIWVEP.js").then((m) => m.runDirection(rest, io));
|
|
396
643
|
}
|
|
397
644
|
if (command === "report" || command === "retrofit") {
|
|
398
|
-
return import("./cli-
|
|
645
|
+
return import("./cli-ZB2VJ7CY.js").then((m) => m.runReport(command, rest, io));
|
|
399
646
|
}
|
|
400
647
|
if (command === "estate") {
|
|
401
|
-
return import("./cli-
|
|
648
|
+
return import("./cli-2DY3V3SF.js").then((m) => m.runEstate(rest, io));
|
|
402
649
|
}
|
|
403
650
|
if (command === "null") {
|
|
404
|
-
return import("./cli-
|
|
651
|
+
return import("./cli-C3JLFXMG.js").then((m) => m.runNull(rest, io));
|
|
405
652
|
}
|
|
406
653
|
if (command === "tells" && rest[0] === "harvest") {
|
|
407
|
-
return import("./cli-
|
|
654
|
+
return import("./cli-C3JLFXMG.js").then((m) => m.runHarvest(rest, io)).catch((error) => {
|
|
408
655
|
io.err(`craft: ${error.message}`);
|
|
409
656
|
return 2;
|
|
410
657
|
});
|
|
411
658
|
}
|
|
412
659
|
if (command === "snapshot" || command === "audit") {
|
|
413
|
-
return import("./cli-
|
|
660
|
+
return import("./cli-QM2AQQMF.js").then((m) => m.runAudit(command, rest, io));
|
|
414
661
|
}
|
|
415
662
|
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
416
663
|
io.out(HELP);
|
|
@@ -432,7 +679,7 @@ function run(argv, io) {
|
|
|
432
679
|
fix,
|
|
433
680
|
...surface === "copy" ? { house: houseRule(id, name).tier, houseLabel: houseRule(id, name).label } : {}
|
|
434
681
|
}));
|
|
435
|
-
io.out(
|
|
682
|
+
io.out(toJson({ version: CATALOGUE_VERSION, tells: data }));
|
|
436
683
|
} else {
|
|
437
684
|
io.out(`Catalogue ${CATALOGUE_VERSION}`);
|
|
438
685
|
for (const t of CATALOGUE) io.out(` ${t.id.padEnd(24)} gen ${t.generation} ${t.surface.padEnd(6)} ${t.name}`);
|
|
@@ -449,7 +696,7 @@ function run(argv, io) {
|
|
|
449
696
|
return visibleText({ path: p, text: readFileSync(abs, "utf8") });
|
|
450
697
|
});
|
|
451
698
|
const result = compareFacts(before, after);
|
|
452
|
-
io.out(flags.json ?
|
|
699
|
+
io.out(flags.json ? toJson(result) : formatComparison(result, flags.positional[0], flags.positional[1]));
|
|
453
700
|
return result.lost.length || result.added.length ? 1 : 0;
|
|
454
701
|
}
|
|
455
702
|
if (command === "copy" && rest[0] === "claims") {
|
|
@@ -457,19 +704,35 @@ function run(argv, io) {
|
|
|
457
704
|
if (typeof flags === "string") throw new Error(flags);
|
|
458
705
|
if (flags.positional.length === 0) throw new Error("usage: craft copy claims <paths...> [--json]");
|
|
459
706
|
const report = findClaims(readPaths(flags.positional, io.cwd, COPY_FILE, NOT_COPY_DIR));
|
|
460
|
-
io.out(flags.json ?
|
|
707
|
+
io.out(flags.json ? toJson(report) : formatClaims(report));
|
|
461
708
|
return 0;
|
|
462
709
|
}
|
|
463
710
|
if (command === "scan" || command === "copy") {
|
|
464
711
|
const flags = parseFlags(rest);
|
|
465
712
|
if (typeof flags === "string") throw new Error(flags);
|
|
466
713
|
const wanted = command === "scan" ? SOURCE_FILE : COPY_FILE;
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
|
|
714
|
+
const config = loadConfig(flags, io.cwd);
|
|
715
|
+
const paths = flags.positional.length > 0 ? flags.positional : command === "copy" ? config.copyPaths ?? [] : ["."];
|
|
716
|
+
if (command === "copy" && paths.length === 0 && !flags.staged) throw new Error("usage: craft copy <paths...> (or copyPaths in craft.config.json)");
|
|
717
|
+
const files = flags.staged ? readStaged(io.cwd, wanted, config.ignore) : readPaths(paths, io.cwd, wanted, command === "copy" ? NOT_COPY_DIR : void 0, config.ignore);
|
|
718
|
+
let exceptions = loadExceptions(flags, io.cwd);
|
|
719
|
+
let effective = config;
|
|
720
|
+
let refused = [];
|
|
721
|
+
if (command === "copy" && flags.gate) {
|
|
722
|
+
const held = holdHouseBlocks(exceptions, config.severity);
|
|
723
|
+
exceptions = held.exceptions;
|
|
724
|
+
effective = { ...config, severity: held.severity };
|
|
725
|
+
const warned = held.refusedSeverity.filter((tell) => config.severity[tell].level === "warn").map((tell) => ({ tell, because: `craft.config.json: ${config.severity[tell].because}` }));
|
|
726
|
+
refused = [...held.refused, ...warned];
|
|
727
|
+
}
|
|
470
728
|
let report = command === "scan" ? scanSource(files, { exceptions }) : checkCopy(files, { exceptions });
|
|
471
|
-
if (command === "copy" && flags.gate)
|
|
472
|
-
|
|
729
|
+
if (command === "copy" && flags.gate) {
|
|
730
|
+
report = applyHouseGate(report);
|
|
731
|
+
report = { ...report, rejectedExceptions: [...report.rejectedExceptions, ...refused.map((exception) => ({ exception, reason: HOUSE_LOCKED }))] };
|
|
732
|
+
}
|
|
733
|
+
const prepared = prepareReport(report, flags, io, effective);
|
|
734
|
+
if (!prepared) return 0;
|
|
735
|
+
return finish(prepared, flags, command === "scan" ? "craft scan" : "craft copy", io);
|
|
473
736
|
}
|
|
474
737
|
throw new Error(`unknown command "${command}". Run craft --help.`);
|
|
475
738
|
} catch (error) {
|
|
@@ -479,9 +742,13 @@ function run(argv, io) {
|
|
|
479
742
|
}
|
|
480
743
|
|
|
481
744
|
export {
|
|
745
|
+
applySeverity,
|
|
746
|
+
toJson,
|
|
482
747
|
parseFlags,
|
|
483
748
|
readPaths,
|
|
749
|
+
loadConfig,
|
|
484
750
|
loadExceptions,
|
|
751
|
+
prepareReport,
|
|
485
752
|
finish,
|
|
486
753
|
SOURCE_FILE,
|
|
487
754
|
COPY_FILE,
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/direction/png.ts
|
|
2
|
+
import { inflateSync } from "zlib";
|
|
3
|
+
function decodePng(buf) {
|
|
4
|
+
const sig = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
5
|
+
if (!sig.every((b, i) => buf[i] === b)) throw new Error("not a PNG");
|
|
6
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
7
|
+
let pos = 8;
|
|
8
|
+
let width = 0;
|
|
9
|
+
let height = 0;
|
|
10
|
+
let depth = 0;
|
|
11
|
+
let colour = 0;
|
|
12
|
+
let interlace = 0;
|
|
13
|
+
const idat = [];
|
|
14
|
+
while (pos < buf.length) {
|
|
15
|
+
const len = view.getUint32(pos);
|
|
16
|
+
const type = String.fromCharCode(...buf.subarray(pos + 4, pos + 8));
|
|
17
|
+
const data = buf.subarray(pos + 8, pos + 8 + len);
|
|
18
|
+
if (type === "IHDR") {
|
|
19
|
+
width = view.getUint32(pos + 8);
|
|
20
|
+
height = view.getUint32(pos + 12);
|
|
21
|
+
depth = data[8];
|
|
22
|
+
colour = data[9];
|
|
23
|
+
interlace = data[12];
|
|
24
|
+
} else if (type === "IDAT") idat.push(data);
|
|
25
|
+
else if (type === "IEND") break;
|
|
26
|
+
pos += 12 + len;
|
|
27
|
+
}
|
|
28
|
+
const channels = { 0: 1, 2: 3, 4: 2, 6: 4 }[colour];
|
|
29
|
+
if (depth !== 8 || !channels || interlace !== 0) throw new Error(`unsupported PNG (depth ${depth}, colour type ${colour}${interlace ? ", interlaced" : ""}); export it as 8-bit RGB`);
|
|
30
|
+
const raw = inflateSync(Buffer.concat(idat.map((d) => Buffer.from(d))));
|
|
31
|
+
const stride = width * channels;
|
|
32
|
+
const out = new Uint8Array(width * height * 4);
|
|
33
|
+
const prev = new Uint8Array(stride);
|
|
34
|
+
const line = new Uint8Array(stride);
|
|
35
|
+
for (let y = 0; y < height; y += 1) {
|
|
36
|
+
const filter = raw[y * (stride + 1)];
|
|
37
|
+
const src = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1));
|
|
38
|
+
for (let x = 0; x < stride; x += 1) {
|
|
39
|
+
const a = x >= channels ? line[x - channels] : 0;
|
|
40
|
+
const b = prev[x];
|
|
41
|
+
const c = x >= channels ? prev[x - channels] : 0;
|
|
42
|
+
let v = src[x];
|
|
43
|
+
if (filter === 1) v += a;
|
|
44
|
+
else if (filter === 2) v += b;
|
|
45
|
+
else if (filter === 3) v += a + b >> 1;
|
|
46
|
+
else if (filter === 4) {
|
|
47
|
+
const p = a + b - c;
|
|
48
|
+
const pa = Math.abs(p - a);
|
|
49
|
+
const pb = Math.abs(p - b);
|
|
50
|
+
const pc = Math.abs(p - c);
|
|
51
|
+
v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
|
52
|
+
}
|
|
53
|
+
line[x] = v & 255;
|
|
54
|
+
}
|
|
55
|
+
for (let x = 0; x < width; x += 1) {
|
|
56
|
+
const i = x * channels;
|
|
57
|
+
const o = (y * width + x) * 4;
|
|
58
|
+
if (channels >= 3) {
|
|
59
|
+
out[o] = line[i];
|
|
60
|
+
out[o + 1] = line[i + 1];
|
|
61
|
+
out[o + 2] = line[i + 2];
|
|
62
|
+
out[o + 3] = channels === 4 ? line[i + 3] : 255;
|
|
63
|
+
} else {
|
|
64
|
+
out[o] = out[o + 1] = out[o + 2] = line[i];
|
|
65
|
+
out[o + 3] = channels === 2 ? line[i + 1] : 255;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
prev.set(line);
|
|
69
|
+
}
|
|
70
|
+
return { width, height, rgba: out };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export {
|
|
74
|
+
decodePng
|
|
75
|
+
};
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "./chunk-
|
|
2
|
+
readSnapshot
|
|
3
|
+
} from "./chunk-ZKVJ7K6X.js";
|
|
4
|
+
import {
|
|
5
|
+
parseFlags,
|
|
6
|
+
toJson
|
|
7
|
+
} from "./chunk-J3QUROJ5.js";
|
|
4
8
|
import {
|
|
5
9
|
fingerprint,
|
|
6
10
|
fingerprintDistance
|
|
7
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-PZ4MF2VV.js";
|
|
8
12
|
import {
|
|
9
13
|
CATALOGUE_VERSION,
|
|
10
14
|
REFLEX_FONTS_1,
|
|
@@ -18,7 +22,7 @@ import {
|
|
|
18
22
|
isCream,
|
|
19
23
|
oklchToRgb,
|
|
20
24
|
scanSource
|
|
21
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-AL6MRG6V.js";
|
|
22
26
|
|
|
23
27
|
// src/null/cli.ts
|
|
24
28
|
import { spawn } from "child_process";
|
|
@@ -352,7 +356,7 @@ async function build(flags, io) {
|
|
|
352
356
|
const toShoot = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && !existsSync(join(shots, `${id}.json`)));
|
|
353
357
|
if (toShoot.length) {
|
|
354
358
|
io.out(`craft null: snapshotting ${toShoot.length} page${toShoot.length === 1 ? "" : "s"}`);
|
|
355
|
-
const { snapshotUrls } = await import("./audit-
|
|
359
|
+
const { snapshotUrls } = await import("./audit-5QWFLZ6L.js");
|
|
356
360
|
const results = await snapshotUrls(toShoot.map((id) => pathToFileURL(join(pages, `${id}.html`)).href));
|
|
357
361
|
results.forEach((r, i) => {
|
|
358
362
|
const id = toShoot[i];
|
|
@@ -361,7 +365,7 @@ async function build(flags, io) {
|
|
|
361
365
|
else failed.push(`${id}: the page would not render: ${r.error}`);
|
|
362
366
|
});
|
|
363
367
|
}
|
|
364
|
-
const measured = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && existsSync(join(shots, `${id}.json`))).map((id) => measureRun(id, readFileSync(join(pages, `${id}.html`), "utf8"), readJson(join(shots, `${id}.json`))));
|
|
368
|
+
const measured = ids.filter((id) => existsSync(join(pages, `${id}.html`)) && existsSync(join(shots, `${id}.json`))).map((id) => measureRun(id, readFileSync(join(pages, `${id}.html`), "utf8"), readSnapshot(readJson(join(shots, `${id}.json`)), `${id}.json`)));
|
|
365
369
|
const model = {
|
|
366
370
|
version: NULL_VERSION,
|
|
367
371
|
brief: brief.trim(),
|
|
@@ -439,7 +443,7 @@ function runHarvest(args, io) {
|
|
|
439
443
|
const minShare = flags.share === void 0 ? void 0 : Number(flags.share);
|
|
440
444
|
if (minShare !== void 0 && !(minShare > 0 && minShare <= 1)) throw new Error("--share needs a number above 0 and at most 1");
|
|
441
445
|
const candidates = harvest(models, { minShare, copyTell });
|
|
442
|
-
io.out(flags.json ?
|
|
446
|
+
io.out(flags.json ? toJson({ briefs: models.map((m) => m.brief), candidates }) : formatHarvest(candidates, models));
|
|
443
447
|
return 0;
|
|
444
448
|
}
|
|
445
449
|
|