@domandigital/craft 0.10.0 → 0.11.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/dist/index.d.ts CHANGED
@@ -1188,6 +1188,190 @@ declare function namedSources(sentence: string): string[];
1188
1188
  declare function findClaims(files: SourceFile[]): ClaimsReport;
1189
1189
  declare function formatClaims(report: ClaimsReport, label?: string): string;
1190
1190
 
1191
+ /**
1192
+ * Baselines: adopt craft on a repo that already has findings, and fail only
1193
+ * on new ones.
1194
+ *
1195
+ * A finding is keyed on its tell, its path and a hash of its excerpt with the
1196
+ * whitespace collapsed, never on its line: an edit above a known finding
1197
+ * moves it, and a moved finding is not a new one. Keys are counted, so a file
1198
+ * with two identical findings and a baseline of one reports the second.
1199
+ *
1200
+ * Pure. The command line reads and writes the file.
1201
+ */
1202
+
1203
+ declare const BASELINE_VERSION = 1;
1204
+ interface BaselineEntry {
1205
+ tell: string;
1206
+ path: string;
1207
+ /** FNV-1a of the excerpt, whitespace collapsed. */
1208
+ excerpt: string;
1209
+ count: number;
1210
+ }
1211
+ interface Baseline {
1212
+ version: typeof BASELINE_VERSION;
1213
+ catalogueVersion: string;
1214
+ entries: BaselineEntry[];
1215
+ }
1216
+ /** The key a finding is matched on. Also the SARIF partial fingerprint. */
1217
+ declare function findingKey(f: Finding): string;
1218
+ /** Every finding in a report, as a baseline. */
1219
+ declare function createBaseline(report: CheckReport): Baseline;
1220
+ /** Validate a parsed baseline file. */
1221
+ declare function parseBaseline(data: unknown, source?: string): Baseline;
1222
+ /**
1223
+ * The report without the findings the baseline already knows about.
1224
+ * `baselined` is how many were removed, so the output can say so: a
1225
+ * baseline that hides findings silently is a hole.
1226
+ */
1227
+ declare function applyBaseline(report: CheckReport, baseline: Baseline): CheckReport & {
1228
+ baselined: number;
1229
+ };
1230
+
1231
+ /**
1232
+ * `craft.config.json`: what a repo tells craft about itself.
1233
+ *
1234
+ * Pure: the command line reads the file and hands the parsed JSON here. The
1235
+ * design choices live in `art-direction.json`; this file is about the tool:
1236
+ * which paths are not the site, where the copy is, and a repo's own decisions
1237
+ * about a tell's severity.
1238
+ *
1239
+ * {
1240
+ * "ignore": ["docs/archive/**", "*.stories.tsx"],
1241
+ * "copyPaths": ["content", "src/app"],
1242
+ * "severity": {
1243
+ * "ai-violet": { "level": "off", "because": "The client's shopfront has been this violet since 1998." },
1244
+ * "em-dash": { "level": "block", "because": "House style for this client bans them outright." }
1245
+ * }
1246
+ * }
1247
+ *
1248
+ * A severity change needs a `because`, like an exception in
1249
+ * `art-direction.json`, and for the same reason: a tell switched off with no
1250
+ * reason is a guard nobody can audit. `off` becomes an exception, so the
1251
+ * report still lists what it silenced.
1252
+ */
1253
+
1254
+ type SeverityLevel = "warn" | "block" | "off";
1255
+ interface SeverityOverride {
1256
+ level: SeverityLevel;
1257
+ because: string;
1258
+ }
1259
+ interface CraftConfig {
1260
+ /** Glob patterns for paths that are not the site. Added to `DEFAULT_IGNORE`. */
1261
+ ignore: string[];
1262
+ /** Where `craft copy` looks when no path is given. */
1263
+ copyPaths?: string[];
1264
+ severity: Record<string, SeverityOverride>;
1265
+ }
1266
+ /**
1267
+ * Agent and tool folders: instructions for the people and models building
1268
+ * the site, not copy a visitor reads. Always ignored by a directory walk.
1269
+ */
1270
+ declare const DEFAULT_IGNORE: string[];
1271
+ /**
1272
+ * Validate a parsed `craft.config.json`. Throws with the file name and the
1273
+ * key at fault: a config that half-applies is worse than one that stops the
1274
+ * run.
1275
+ */
1276
+ declare function parseCraftConfig(data: unknown, knownTells: ReadonlySet<string>, source?: string): CraftConfig;
1277
+ /**
1278
+ * A glob as a regular expression over a `/`-separated relative path.
1279
+ * `*` is one segment's worth, `**` any depth, `?` one character. A pattern
1280
+ * with no `/` matches a file or folder of that name anywhere; otherwise it is
1281
+ * anchored at the repo root. Anything under a matched folder matches too.
1282
+ */
1283
+ declare function globToRegExp(glob: string): RegExp;
1284
+ /** A matcher for a set of globs. Paths are normalised to `/` before matching. */
1285
+ declare function ignoreMatcher(patterns: string[]): (path: string) => boolean;
1286
+ /** `off` entries, as exceptions: applied, and listed as silenced in the report. */
1287
+ declare function severityExceptions(config: CraftConfig): TellException[];
1288
+ /** `warn` and `block` entries, applied to a finished report. */
1289
+ declare function applySeverity(report: CheckReport, config: CraftConfig): CheckReport;
1290
+
1291
+ /**
1292
+ * A report as SARIF 2.1.0, for GitHub code scanning and any other tool that
1293
+ * reads it. Pure.
1294
+ *
1295
+ * One rule per tell the report names, carrying the tell's `why` and `fix`.
1296
+ * `block` findings are errors, `warn` findings warnings. A finding on a
1297
+ * rendered page has the URL as its location and no line: GitHub code
1298
+ * scanning only shows results in files of the repo, so those appear in the
1299
+ * SARIF file but not in the Security tab.
1300
+ */
1301
+
1302
+ interface SarifTell {
1303
+ id: string;
1304
+ name: string;
1305
+ why: string;
1306
+ fix: string;
1307
+ }
1308
+ interface SarifOptions {
1309
+ /** The catalogue, for each rule's description. */
1310
+ tells: readonly SarifTell[];
1311
+ /** Where a reader learns more. */
1312
+ informationUri?: string;
1313
+ }
1314
+ declare function toSarif(report: CheckReport, options: SarifOptions): {
1315
+ $schema: string;
1316
+ version: "2.1.0";
1317
+ runs: {
1318
+ tool: {
1319
+ driver: {
1320
+ name: string;
1321
+ version: string;
1322
+ informationUri: string;
1323
+ rules: {
1324
+ fullDescription?: {
1325
+ text: string;
1326
+ } | undefined;
1327
+ help?: {
1328
+ text: string;
1329
+ } | undefined;
1330
+ id: string;
1331
+ name: string;
1332
+ shortDescription: {
1333
+ text: string;
1334
+ };
1335
+ }[];
1336
+ };
1337
+ };
1338
+ results: {
1339
+ ruleId: string;
1340
+ ruleIndex: number;
1341
+ level: "error" | "warning";
1342
+ message: {
1343
+ text: string;
1344
+ };
1345
+ locations: {
1346
+ physicalLocation: {
1347
+ region?: {
1348
+ startLine: number;
1349
+ snippet: {
1350
+ text: string;
1351
+ };
1352
+ } | undefined;
1353
+ artifactLocation: {
1354
+ uri: string;
1355
+ };
1356
+ };
1357
+ }[];
1358
+ partialFingerprints: {
1359
+ "craftFinding/v1": string;
1360
+ };
1361
+ }[];
1362
+ }[];
1363
+ };
1364
+
1365
+ /**
1366
+ * The `--json` contract. Every JSON document a craft command prints carries
1367
+ * `schemaVersion`, so a script reading it can refuse a shape it does not
1368
+ * know instead of misreading it. Bump it when a field is removed or changes
1369
+ * meaning; adding a field is not a bump.
1370
+ */
1371
+ declare const JSON_SCHEMA_VERSION = 1;
1372
+ /** A command's JSON output, versioned. */
1373
+ declare function toJson(data: object): string;
1374
+
1191
1375
  /**
1192
1376
  * The rendered detection path for tells that can be seen on a live page.
1193
1377
  *
@@ -1820,4 +2004,4 @@ declare function characterReport(input: CharacterInputs, copyTells?: Set<string>
1820
2004
 
1821
2005
  declare function retrofitPlan(report: CharacterReport, direction?: ArtDirection | null): string;
1822
2006
 
1823
- export { AI_PHRASES, AI_VIOLET, AI_WORDS, type AccentFork, type AccentForkOptions, type Action, type Area, type ArtDirection, BUZZWORDS, CATALOGUE, CATALOGUE_VERSION, CHOICE_KEYS, type CandidateKind, type CharacterInputs, type CharacterReport, type CheckOptions, type CheckReport, type CheckRestraintInput, type ChoiceKey, type Claim, type ClaimsReport, type ContrastCheck, type ContrastReport, type CopyTell, type CraftTokensInput, type CraftTokensResult, DENSITIES, DENSITY_NAMES, DIRECTION_VERSION, DISTANCE_WEIGHTS, DURATION_MS, DURATION_S, type DensityCssOptions, type DensityName, type DirectionChoice, type DirectionProblem, type DirectionReport, type DirectionSource, type DurationName, EASE, EASE_TUPLE, ESTATE_VERSION, EXIT_RATIO, type EaseName, type EmitCssOptions, type EstateMatch, type EstatePair, type EstateRegister, type EstateSite, FINGERPRINT_VERSION, type Fact, type FactComparison, type FactKind, type Finding, type Fingerprint, type FingerprintDistance, type FluidSpaceOptions, type FluidTypeOptions, type FluidTypeResult, type FontClass, type Gamut, type Generation, HOUSE, HOUSE_BUDGET, HOUSE_TYPE, type HarvestCandidate, type HarvestOptions, type Hit, type HouseRule, type HouseTier, LIGHTNESS_CURVE, MIN_DECIDED, MIN_RUNS, type MotionDecision, type MotionKind, NEGATIVE_REASSURANCE, NULL_VERSION, type NullModel, type NullRun, type Oklch, type OklchToHexOptions, PILL_LIMIT, PLAINER_WORDS, PROPOSED, type PaletteColour, type Proposal, type ProposeInput, RAMP_STEPS, REFLEX_FONTS_1, REFLEX_FONTS_2, RENDERED_PATHS, REVEAL_LIMIT, REVIEW_PHRASES, type Ramp, type RampFromAnchorsOptions, type RampOptions, type RampStep, type RestraintBudget, type RestraintReport, type RestraintSeverity, type RestraintViolation, type Rgb, SCALE, SHADCN_LIMIT, SHADCN_PRIMITIVES, SIBLING_AT, SNAPSHOT_VERSION, SOURCE_KINDS, SPACE_STEPS, SPRING, STOCK_PHRASES, type SectionKind, type SectionRhythmOptions, type SemanticInput, type SemanticResult, type Severity, type ShouldAnimateInput, type SignalResult, type Snapshot, type SnapshotColour, type SnapshotControl, type SnapshotEffects, type SnapshotFont, type SnapshotGradient, type SnapshotHeading, type SnapshotMotion, type SnapshotPatch, type SnapshotSection, type SourceFile, type SourceKind, type SourceTell, type SpaceStep, type Surface, TELL_HEAVY, TYPE_STEPS, TYPICAL_AT, type TailwindPreset, type Tell, type TellException, type TypeStep, type Typicality, VAGUE_WORDS, type ValidateContext, type Verdict, accentFork, addSite, apcaContrast, applyHouseGate, auditSnapshot, catalogueTable, characterReport, checkCopy, checkPair, checkRestraint, choicesOf, compareFacts, compareToEstate, craftTokens, deltaEOk, deltaEOkHex, densityCss, densityTokens, emitCss, emptyEstate, estatePairs, exitDuration, extractHtml, findClaims, findColours, fingerprint, fingerprintDistance, fluidClamp, fluidSpace, fluidType, fontClass, formatClaims, formatComparison, formatHex, formatReport, groundName, harvest, hexToOklch, houseRule, hueName, hueSwatch, inP3Gamut, inSrgbGamut, initDirection, isAiViolet, isCream, makeSnapshot, motionTokens, namedSources, nearest, normaliseFamily, nullPrompt, oklchToHex, oklchToRgb, paletteFromPixels, parseColour, parseHex, propose, protectedFacts, ramp, rampFromAnchors, retrofitPlan, runTell, scanSource, sectionRhythm, semantic, shapeName, sharedParts, shouldAnimate, siblingLine, tailwindV3Preset, tailwindV4Theme, tellById, toGamut, typeFeatureTokens, typicality, validateDirection, visibleText, wcagContrast, wordListsJson };
2007
+ export { AI_PHRASES, AI_VIOLET, AI_WORDS, type AccentFork, type AccentForkOptions, type Action, type Area, type ArtDirection, BASELINE_VERSION, BUZZWORDS, type Baseline, type BaselineEntry, CATALOGUE, CATALOGUE_VERSION, CHOICE_KEYS, type CandidateKind, type CharacterInputs, type CharacterReport, type CheckOptions, type CheckReport, type CheckRestraintInput, type ChoiceKey, type Claim, type ClaimsReport, type ContrastCheck, type ContrastReport, type CopyTell, type CraftConfig, type CraftTokensInput, type CraftTokensResult, DEFAULT_IGNORE, DENSITIES, DENSITY_NAMES, DIRECTION_VERSION, DISTANCE_WEIGHTS, DURATION_MS, DURATION_S, type DensityCssOptions, type DensityName, type DirectionChoice, type DirectionProblem, type DirectionReport, type DirectionSource, type DurationName, EASE, EASE_TUPLE, ESTATE_VERSION, EXIT_RATIO, type EaseName, type EmitCssOptions, type EstateMatch, type EstatePair, type EstateRegister, type EstateSite, FINGERPRINT_VERSION, type Fact, type FactComparison, type FactKind, type Finding, type Fingerprint, type FingerprintDistance, type FluidSpaceOptions, type FluidTypeOptions, type FluidTypeResult, type FontClass, type Gamut, type Generation, HOUSE, HOUSE_BUDGET, HOUSE_TYPE, type HarvestCandidate, type HarvestOptions, type Hit, type HouseRule, type HouseTier, JSON_SCHEMA_VERSION, LIGHTNESS_CURVE, MIN_DECIDED, MIN_RUNS, type MotionDecision, type MotionKind, NEGATIVE_REASSURANCE, NULL_VERSION, type NullModel, type NullRun, type Oklch, type OklchToHexOptions, PILL_LIMIT, PLAINER_WORDS, PROPOSED, type PaletteColour, type Proposal, type ProposeInput, RAMP_STEPS, REFLEX_FONTS_1, REFLEX_FONTS_2, RENDERED_PATHS, REVEAL_LIMIT, REVIEW_PHRASES, type Ramp, type RampFromAnchorsOptions, type RampOptions, type RampStep, type RestraintBudget, type RestraintReport, type RestraintSeverity, type RestraintViolation, type Rgb, SCALE, SHADCN_LIMIT, SHADCN_PRIMITIVES, SIBLING_AT, SNAPSHOT_VERSION, SOURCE_KINDS, SPACE_STEPS, SPRING, STOCK_PHRASES, type SarifOptions, type SarifTell, type SectionKind, type SectionRhythmOptions, type SemanticInput, type SemanticResult, type Severity, type SeverityLevel, type SeverityOverride, type ShouldAnimateInput, type SignalResult, type Snapshot, type SnapshotColour, type SnapshotControl, type SnapshotEffects, type SnapshotFont, type SnapshotGradient, type SnapshotHeading, type SnapshotMotion, type SnapshotPatch, type SnapshotSection, type SourceFile, type SourceKind, type SourceTell, type SpaceStep, type Surface, TELL_HEAVY, TYPE_STEPS, TYPICAL_AT, type TailwindPreset, type Tell, type TellException, type TypeStep, type Typicality, VAGUE_WORDS, type ValidateContext, type Verdict, accentFork, addSite, apcaContrast, applyBaseline, applyHouseGate, applySeverity, auditSnapshot, catalogueTable, characterReport, checkCopy, checkPair, checkRestraint, choicesOf, compareFacts, compareToEstate, craftTokens, createBaseline, deltaEOk, deltaEOkHex, densityCss, densityTokens, emitCss, emptyEstate, estatePairs, exitDuration, extractHtml, findClaims, findColours, findingKey, fingerprint, fingerprintDistance, fluidClamp, fluidSpace, fluidType, fontClass, formatClaims, formatComparison, formatHex, formatReport, globToRegExp, groundName, harvest, hexToOklch, houseRule, hueName, hueSwatch, ignoreMatcher, inP3Gamut, inSrgbGamut, initDirection, isAiViolet, isCream, makeSnapshot, motionTokens, namedSources, nearest, normaliseFamily, nullPrompt, oklchToHex, oklchToRgb, paletteFromPixels, parseBaseline, parseColour, parseCraftConfig, parseHex, propose, protectedFacts, ramp, rampFromAnchors, retrofitPlan, runTell, scanSource, sectionRhythm, semantic, severityExceptions, shapeName, sharedParts, shouldAnimate, siblingLine, tailwindV3Preset, tailwindV4Theme, tellById, toGamut, toJson, toSarif, typeFeatureTokens, typicality, validateDirection, visibleText, wcagContrast, wordListsJson };
package/dist/index.js CHANGED
@@ -4599,6 +4599,194 @@ function formatClaims(report2, label = "craft copy claims") {
4599
4599
  return lines.join("\n");
4600
4600
  }
4601
4601
 
4602
+ // src/character/baseline.ts
4603
+ var BASELINE_VERSION = 1;
4604
+ function fnv1a(text) {
4605
+ let hash = 2166136261;
4606
+ for (let i = 0; i < text.length; i += 1) {
4607
+ hash ^= text.charCodeAt(i);
4608
+ hash = Math.imul(hash, 16777619);
4609
+ }
4610
+ return (hash >>> 0).toString(16).padStart(8, "0");
4611
+ }
4612
+ var excerptHash = (f4) => fnv1a(f4.excerpt.replace(/\s+/g, " ").trim());
4613
+ var keyOf = (tell, path, excerpt) => `${tell}\0${path}\0${excerpt}`;
4614
+ function findingKey(f4) {
4615
+ return keyOf(f4.tell, f4.path, excerptHash(f4));
4616
+ }
4617
+ function createBaseline(report2) {
4618
+ const counts = /* @__PURE__ */ new Map();
4619
+ for (const f4 of report2.findings) {
4620
+ const excerpt = excerptHash(f4);
4621
+ const key = keyOf(f4.tell, f4.path, excerpt);
4622
+ const entry = counts.get(key);
4623
+ if (entry) entry.count += 1;
4624
+ else counts.set(key, { tell: f4.tell, path: f4.path, excerpt, count: 1 });
4625
+ }
4626
+ const entries = [...counts.values()].sort((a, b) => a.path.localeCompare(b.path) || a.tell.localeCompare(b.tell) || a.excerpt.localeCompare(b.excerpt));
4627
+ return { version: BASELINE_VERSION, catalogueVersion: report2.catalogueVersion, entries };
4628
+ }
4629
+ function parseBaseline(data, source = "baseline") {
4630
+ const b = data;
4631
+ if (!b || typeof b !== "object" || b.version !== BASELINE_VERSION || !Array.isArray(b.entries)) {
4632
+ throw new Error(`${source}: not a craft baseline (version ${BASELINE_VERSION})`);
4633
+ }
4634
+ for (const e of b.entries) {
4635
+ if (typeof e?.tell !== "string" || typeof e.path !== "string" || typeof e.excerpt !== "string" || !Number.isInteger(e.count) || e.count < 1) {
4636
+ throw new Error(`${source}: malformed entry ${JSON.stringify(e)}`);
4637
+ }
4638
+ }
4639
+ return b;
4640
+ }
4641
+ function recount(report2, findings) {
4642
+ const byGeneration = { 1: 0, 2: 0, 3: 0 };
4643
+ const byTell = {};
4644
+ for (const f4 of findings) {
4645
+ byGeneration[f4.generation] += 1;
4646
+ byTell[f4.tell] = (byTell[f4.tell] ?? 0) + 1;
4647
+ }
4648
+ return {
4649
+ ...report2,
4650
+ findings,
4651
+ summary: { ...report2.summary, findings: findings.length, byGeneration, byTell, blocking: findings.filter((f4) => f4.severity === "block").length }
4652
+ };
4653
+ }
4654
+ function applyBaseline(report2, baseline) {
4655
+ const remaining = /* @__PURE__ */ new Map();
4656
+ for (const e of baseline.entries) {
4657
+ const key = keyOf(e.tell, e.path, e.excerpt);
4658
+ remaining.set(key, (remaining.get(key) ?? 0) + e.count);
4659
+ }
4660
+ const kept = [];
4661
+ let baselined = 0;
4662
+ for (const f4 of report2.findings) {
4663
+ const key = findingKey(f4);
4664
+ const left = remaining.get(key) ?? 0;
4665
+ if (left > 0) {
4666
+ remaining.set(key, left - 1);
4667
+ baselined += 1;
4668
+ } else kept.push(f4);
4669
+ }
4670
+ return { ...recount(report2, kept), baselined };
4671
+ }
4672
+
4673
+ // src/character/config.ts
4674
+ var DEFAULT_IGNORE = [".claude", ".agents", ".cursor"];
4675
+ var KEYS = /* @__PURE__ */ new Set(["$schema", "ignore", "copyPaths", "severity"]);
4676
+ var LEVELS = /* @__PURE__ */ new Set(["warn", "block", "off"]);
4677
+ var MIN_BECAUSE = 12;
4678
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4679
+ var isStringArray = (v) => Array.isArray(v) && v.every((s) => typeof s === "string");
4680
+ function parseCraftConfig(data, knownTells, source = "craft.config.json") {
4681
+ if (!isRecord(data)) throw new Error(`${source}: expected an object`);
4682
+ for (const key of Object.keys(data)) if (!KEYS.has(key)) throw new Error(`${source}: unknown key "${key}"`);
4683
+ const ignore = data.ignore ?? [];
4684
+ if (!isStringArray(ignore)) throw new Error(`${source}: "ignore" must be an array of glob strings`);
4685
+ if (data.copyPaths !== void 0 && !isStringArray(data.copyPaths)) throw new Error(`${source}: "copyPaths" must be an array of paths`);
4686
+ const severity = {};
4687
+ const raw = data.severity ?? {};
4688
+ if (!isRecord(raw)) throw new Error(`${source}: "severity" must map a tell id to { level, because }`);
4689
+ for (const [tell, value] of Object.entries(raw)) {
4690
+ if (!knownTells.has(tell)) throw new Error(`${source}: severity names no tell called "${tell}"`);
4691
+ if (!isRecord(value) || !LEVELS.has(value.level)) {
4692
+ throw new Error(`${source}: severity.${tell} needs a level of "warn", "block" or "off"`);
4693
+ }
4694
+ if (typeof value.because !== "string" || value.because.trim().length < MIN_BECAUSE) {
4695
+ throw new Error(`${source}: severity.${tell} needs a because of at least a sentence`);
4696
+ }
4697
+ severity[tell] = { level: value.level, because: value.because };
4698
+ }
4699
+ return { ignore, severity, ...data.copyPaths ? { copyPaths: data.copyPaths } : {} };
4700
+ }
4701
+ function globToRegExp(glob) {
4702
+ const pattern = glob.replace(/^\.\//, "").replace(/\/$/, "").replace(/\/\*\*$/, "");
4703
+ let body = "";
4704
+ for (let i = 0; i < pattern.length; i += 1) {
4705
+ const c = pattern[i];
4706
+ if (c === "*" && pattern[i + 1] === "*") {
4707
+ const slash = pattern[i + 2] === "/";
4708
+ body += slash ? "(?:.*/)?" : ".*";
4709
+ i += slash ? 2 : 1;
4710
+ } else if (c === "*") body += "[^/]*";
4711
+ else if (c === "?") body += "[^/]";
4712
+ else body += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
4713
+ }
4714
+ const anywhere = !pattern.includes("/");
4715
+ return new RegExp(`^${anywhere ? "(?:.*/)?" : ""}${body}(?:/.*)?$`);
4716
+ }
4717
+ function ignoreMatcher(patterns) {
4718
+ const res = patterns.map(globToRegExp);
4719
+ return (path) => {
4720
+ const p = path.replace(/\\/g, "/").replace(/^\.\//, "");
4721
+ return res.some((re) => re.test(p));
4722
+ };
4723
+ }
4724
+ function severityExceptions(config) {
4725
+ return Object.entries(config.severity).filter(([, o]) => o.level === "off").map(([tell, o]) => ({ tell, because: `craft.config.json: ${o.because}` }));
4726
+ }
4727
+ function applySeverity(report2, config) {
4728
+ const findings = report2.findings.map((f4) => {
4729
+ const o = config.severity[f4.tell];
4730
+ return o && o.level !== "off" ? { ...f4, severity: o.level } : f4;
4731
+ });
4732
+ return { ...report2, findings, summary: { ...report2.summary, blocking: findings.filter((f4) => f4.severity === "block").length } };
4733
+ }
4734
+
4735
+ // src/character/sarif.ts
4736
+ var isUrl = (path) => /^[a-z][a-z0-9+.-]*:\/\//i.test(path);
4737
+ function toSarif(report2, options) {
4738
+ const byId = new Map(options.tells.map((t) => [t.id, t]));
4739
+ const ids = [...new Set(report2.findings.map((f4) => f4.tell))].sort();
4740
+ const index = new Map(ids.map((id, i) => [id, i]));
4741
+ const rules = ids.map((id) => {
4742
+ const t = byId.get(id);
4743
+ return {
4744
+ id,
4745
+ name: t?.name ?? id,
4746
+ shortDescription: { text: t?.name ?? id },
4747
+ ...t ? { fullDescription: { text: t.why }, help: { text: t.fix } } : {}
4748
+ };
4749
+ });
4750
+ const results = report2.findings.map((f4) => ({
4751
+ ruleId: f4.tell,
4752
+ ruleIndex: index.get(f4.tell),
4753
+ level: f4.severity === "block" ? "error" : "warning",
4754
+ message: { text: `${f4.message}. ${f4.fix}` },
4755
+ locations: [
4756
+ {
4757
+ physicalLocation: {
4758
+ artifactLocation: { uri: isUrl(f4.path) ? f4.path : f4.path.replace(/\\/g, "/") },
4759
+ ...f4.line > 0 ? { region: { startLine: f4.line, snippet: { text: f4.excerpt } } } : {}
4760
+ }
4761
+ }
4762
+ ],
4763
+ partialFingerprints: { "craftFinding/v1": fnv1a(findingKey(f4)) }
4764
+ }));
4765
+ return {
4766
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
4767
+ version: "2.1.0",
4768
+ runs: [
4769
+ {
4770
+ tool: {
4771
+ driver: {
4772
+ name: "craft",
4773
+ version: report2.catalogueVersion,
4774
+ informationUri: options.informationUri ?? "https://www.npmjs.com/package/@domandigital/craft",
4775
+ rules
4776
+ }
4777
+ },
4778
+ results
4779
+ }
4780
+ ]
4781
+ };
4782
+ }
4783
+
4784
+ // src/character/json.ts
4785
+ var JSON_SCHEMA_VERSION = 1;
4786
+ function toJson(data) {
4787
+ return JSON.stringify({ schemaVersion: JSON_SCHEMA_VERSION, ...data }, null, 2);
4788
+ }
4789
+
4602
4790
  // src/fingerprint/index.ts
4603
4791
  var FINGERPRINT_VERSION = 1;
4604
4792
  var CHROMATIC = 0.03;
@@ -5493,10 +5681,12 @@ export {
5493
5681
  AI_PHRASES,
5494
5682
  AI_VIOLET,
5495
5683
  AI_WORDS,
5684
+ BASELINE_VERSION,
5496
5685
  BUZZWORDS,
5497
5686
  CATALOGUE,
5498
5687
  CATALOGUE_VERSION,
5499
5688
  CHOICE_KEYS,
5689
+ DEFAULT_IGNORE,
5500
5690
  DENSITIES,
5501
5691
  DENSITY_NAMES,
5502
5692
  DIRECTION_VERSION,
@@ -5511,6 +5701,7 @@ export {
5511
5701
  HOUSE,
5512
5702
  HOUSE_BUDGET,
5513
5703
  HOUSE_TYPE,
5704
+ JSON_SCHEMA_VERSION,
5514
5705
  LIGHTNESS_CURVE,
5515
5706
  MIN_DECIDED,
5516
5707
  MIN_RUNS,
@@ -5541,7 +5732,9 @@ export {
5541
5732
  accentFork,
5542
5733
  addSite,
5543
5734
  apcaContrast,
5735
+ applyBaseline,
5544
5736
  applyHouseGate,
5737
+ applySeverity,
5545
5738
  auditSnapshot,
5546
5739
  catalogueTable,
5547
5740
  characterReport,
@@ -5552,6 +5745,7 @@ export {
5552
5745
  compareFacts,
5553
5746
  compareToEstate,
5554
5747
  craftTokens,
5748
+ createBaseline,
5555
5749
  deltaEOk,
5556
5750
  deltaEOkHex,
5557
5751
  densityCss,
@@ -5563,6 +5757,7 @@ export {
5563
5757
  extractHtml,
5564
5758
  findClaims,
5565
5759
  findColours,
5760
+ findingKey,
5566
5761
  fingerprint,
5567
5762
  fingerprintDistance,
5568
5763
  fluidClamp,
@@ -5573,12 +5768,14 @@ export {
5573
5768
  formatComparison,
5574
5769
  formatHex,
5575
5770
  formatReport,
5771
+ globToRegExp,
5576
5772
  groundName,
5577
5773
  harvest,
5578
5774
  hexToOklch,
5579
5775
  houseRule,
5580
5776
  hueName,
5581
5777
  hueSwatch,
5778
+ ignoreMatcher,
5582
5779
  inP3Gamut,
5583
5780
  inSrgbGamut,
5584
5781
  initDirection,
@@ -5593,7 +5790,9 @@ export {
5593
5790
  oklchToHex,
5594
5791
  oklchToRgb,
5595
5792
  paletteFromPixels,
5793
+ parseBaseline,
5596
5794
  parseColour,
5795
+ parseCraftConfig,
5597
5796
  parseHex,
5598
5797
  propose,
5599
5798
  protectedFacts,
@@ -5604,6 +5803,7 @@ export {
5604
5803
  scanSource,
5605
5804
  sectionRhythm,
5606
5805
  semantic,
5806
+ severityExceptions,
5607
5807
  shapeName,
5608
5808
  sharedParts,
5609
5809
  shouldAnimate,
@@ -5612,6 +5812,8 @@ export {
5612
5812
  tailwindV4Theme,
5613
5813
  tellById,
5614
5814
  toGamut,
5815
+ toJson,
5816
+ toSarif,
5615
5817
  typeFeatureTokens,
5616
5818
  typicality,
5617
5819
  validateDirection,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domandigital/craft",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "private": false,
5
5
  "description": "The Doman Digital house craft standard as numbers: OKLCh colour ramps that snap to existing anchors, derived semantic tokens with measured WCAG and APCA contrast, one shared motion vocabulary for CSS and JS, and a base stylesheet. Zero dependencies.",
6
6
  "license": "Apache-2.0",
@@ -8,21 +8,28 @@
8
8
  "main": "dist/index.cjs",
9
9
  "module": "dist/index.js",
10
10
  "types": "dist/index.d.ts",
11
+ "sideEffects": [
12
+ "./css/*.css"
13
+ ],
11
14
  "bin": {
12
15
  "craft": "dist/cli.js"
13
16
  },
14
17
  "exports": {
15
18
  ".": {
16
- "types": "./dist/index.d.ts",
17
- "import": "./dist/index.js",
19
+ "import": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ },
18
23
  "require": {
19
24
  "types": "./dist/index.d.cts",
20
25
  "default": "./dist/index.cjs"
21
26
  }
22
27
  },
23
28
  "./audit": {
24
- "types": "./dist/audit.d.ts",
25
- "import": "./dist/audit.js",
29
+ "import": {
30
+ "types": "./dist/audit.d.ts",
31
+ "default": "./dist/audit.js"
32
+ },
26
33
  "require": {
27
34
  "types": "./dist/audit.d.cts",
28
35
  "default": "./dist/audit.cjs"
@@ -52,7 +59,7 @@
52
59
  "vitest": "^2.1.9"
53
60
  },
54
61
  "engines": {
55
- "node": ">=20"
62
+ "node": ">=22.12"
56
63
  },
57
64
  "repository": {
58
65
  "type": "git",
@@ -1,112 +0,0 @@
1
- import {
2
- snapshotUrl
3
- } from "./chunk-TSKRBIOC.js";
4
- import {
5
- describeTypicality,
6
- loadNull,
7
- typicality
8
- } from "./chunk-APO4BT2F.js";
9
- import {
10
- SOURCE_FILE,
11
- finish,
12
- loadExceptions,
13
- parseFlags,
14
- readPaths
15
- } from "./chunk-JKIDWHG3.js";
16
- import {
17
- fingerprint
18
- } from "./chunk-QMYERCBB.js";
19
- import {
20
- SNAPSHOT_VERSION,
21
- auditSnapshot,
22
- scanSource
23
- } from "./chunk-3NGWXZZE.js";
24
-
25
- // src/audit/cli.ts
26
- import { existsSync, readFileSync, writeFileSync } from "fs";
27
- import { resolve } from "path";
28
- var fmt = (o) => o ? `oklch(${o.l.toFixed(3)} ${o.c.toFixed(3)} ${o.h.toFixed(1)})` : "none";
29
- function describeFingerprint(fp) {
30
- return [
31
- "Fingerprint",
32
- ` accent ${fmt(fp.accent)}`,
33
- ` ground ${fmt(fp.ground)}`,
34
- ` display ${fp.display.family} (${fp.display.class})`,
35
- ` body ${fp.body.family} (${fp.body.class})`,
36
- ` roundness ${fp.roundness === null ? "no buttons" : fp.roundness.toFixed(2)}`,
37
- ` motion ${Math.round(fp.motion * 100)}% of sections below the fold reveal on scroll`,
38
- ` effects ${fp.effects.join(", ") || "none"}`,
39
- ` layout ${fp.layout.join(" > ") || "none"}`
40
- ].join("\n");
41
- }
42
- function merge(a, b) {
43
- const byTell = { ...a.summary.byTell };
44
- for (const [k, v] of Object.entries(b.summary.byTell)) byTell[k] = (byTell[k] ?? 0) + v;
45
- return {
46
- catalogueVersion: a.catalogueVersion,
47
- findings: [...a.findings, ...b.findings],
48
- rejectedExceptions: a.rejectedExceptions,
49
- excepted: [...a.excepted, ...b.excepted],
50
- suppressed: [...a.suppressed, ...b.suppressed],
51
- summary: {
52
- files: a.summary.files + b.summary.files,
53
- findings: a.summary.findings + b.summary.findings,
54
- byGeneration: { 1: a.summary.byGeneration[1] + b.summary.byGeneration[1], 2: a.summary.byGeneration[2] + b.summary.byGeneration[2], 3: a.summary.byGeneration[3] + b.summary.byGeneration[3] },
55
- byTell,
56
- blocking: a.summary.blocking + b.summary.blocking
57
- }
58
- };
59
- }
60
- function readSnapshot(path) {
61
- const data = JSON.parse(readFileSync(path, "utf8"));
62
- if (data.version !== SNAPSHOT_VERSION) throw new Error(`${path} is snapshot version ${String(data.version)}; this craft reads ${SNAPSHOT_VERSION}`);
63
- return data;
64
- }
65
- async function runAudit(command, args, io) {
66
- try {
67
- const flags = parseFlags(args);
68
- if (typeof flags === "string") throw new Error(flags);
69
- const target = flags.positional[0];
70
- if (!target) throw new Error(`usage: craft ${command} <url${command === "audit" ? " | snapshot.json" : ""}>`);
71
- const viewport = flags.width || flags.height ? { width: Number(flags.width ?? 1440), height: Number(flags.height ?? 900) } : void 0;
72
- const local = resolve(io.cwd, target);
73
- const snapshot = command === "audit" && /\.json$/i.test(target) && existsSync(local) ? readSnapshot(local) : await snapshotUrl(target, { viewport });
74
- if (flags.out) writeFileSync(resolve(io.cwd, flags.out), `${JSON.stringify(snapshot, null, 2)}
75
- `);
76
- if (command === "snapshot") {
77
- if (!flags.out) io.out(JSON.stringify(snapshot, null, 2));
78
- else io.out(`craft snapshot: ${snapshot.url} saved to ${flags.out}`);
79
- return 0;
80
- }
81
- const exceptions = loadExceptions(flags, flags.repo ? resolve(io.cwd, flags.repo) : io.cwd);
82
- let report = auditSnapshot(snapshot, { exceptions });
83
- if (flags.repo) {
84
- const repo = resolve(io.cwd, flags.repo);
85
- report = merge(scanSource(readPaths(["."], repo, SOURCE_FILE), { exceptions }), report);
86
- }
87
- const fp = fingerprint(snapshot);
88
- const nulls = flags.null ? flags.null.split(",").map((p) => loadNull(p.trim(), io.cwd)) : [];
89
- const typical = nulls.length ? typicality(fp, nulls.flatMap((m, i) => m.runs.map((r) => nulls.length > 1 ? { ...r, id: `${i + 1}/${r.id}` } : r))) : null;
90
- if (flags.json) {
91
- io.out(JSON.stringify({ report, fingerprint: fp, typicality: typical, snapshot: flags.out ?? null }, null, 2));
92
- return report.summary.blocking > 0 || flags.strict && report.summary.findings > 0 ? 1 : 0;
93
- }
94
- const code = finish(report, { ...flags, json: false }, `craft audit ${snapshot.url}`, io);
95
- io.out("");
96
- io.out(describeFingerprint(fp));
97
- if (typical) {
98
- io.out("");
99
- io.out(describeTypicality(typical, nulls.reduce((s, m) => s + m.runs.length, 0), nulls.length));
100
- }
101
- return code;
102
- } catch (error) {
103
- io.err(`craft: ${error.message}`);
104
- return 2;
105
- }
106
- }
107
-
108
- export {
109
- describeFingerprint,
110
- merge,
111
- runAudit
112
- };