@domandigital/craft 0.5.0 → 0.7.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 +110 -0
- package/CHARACTER.md +578 -0
- package/PRINCIPLES.md +14 -0
- package/README.md +36 -1
- package/STANDARD.md +4 -1
- package/art-direction.schema.json +59 -0
- package/dist/audit-YNODYB3U.js +20 -0
- package/dist/audit.cjs +2472 -0
- package/dist/audit.d.cts +389 -0
- package/dist/audit.d.ts +389 -0
- package/dist/audit.js +2439 -0
- package/dist/chunk-3AP4PJMX.js +2352 -0
- package/dist/chunk-CU5TZHOV.js +163 -0
- package/dist/chunk-GV3QOTQU.js +458 -0
- package/dist/chunk-IQTZWNEK.js +112 -0
- package/dist/chunk-J2BOAIHJ.js +175 -0
- package/dist/chunk-OA7SGPN4.js +419 -0
- package/dist/chunk-Q54JSNC5.js +105 -0
- package/dist/chunk-YYC4EGER.js +249 -0
- package/dist/cli-6Y5E32FS.js +334 -0
- package/dist/cli-IQT6QZ6U.js +16 -0
- package/dist/cli-MW3JAGX3.js +21 -0
- package/dist/cli-PD43TWX7.js +15 -0
- package/dist/cli-TM2HWYN7.js +312 -0
- package/dist/cli.js +22 -0
- package/dist/index.cjs +3385 -27
- package/dist/index.d.cts +1011 -1
- package/dist/index.d.ts +1011 -1
- package/dist/index.js +3311 -24
- package/package.json +28 -4
- package/word-lists.json +155 -0
package/dist/index.d.cts
CHANGED
|
@@ -693,4 +693,1014 @@ declare function tailwindV4Theme(): string;
|
|
|
693
693
|
/** Density names, re-exported so a consumer can type its shell prop. */
|
|
694
694
|
declare const DENSITY_NAMES: DensityName[];
|
|
695
695
|
|
|
696
|
-
|
|
696
|
+
/**
|
|
697
|
+
* A snapshot: what a page looks like once it has rendered, as plain data.
|
|
698
|
+
*
|
|
699
|
+
* Source tells read what a site writes. A snapshot reads what a visitor gets:
|
|
700
|
+
* the font that actually loaded, the colour that actually covers the page,
|
|
701
|
+
* the sections that actually fade in. The two disagree often enough to need
|
|
702
|
+
* both. A Tailwind class list can name indigo and a CMS can override it; a
|
|
703
|
+
* source can name Fraunces and the font can fail to load.
|
|
704
|
+
*
|
|
705
|
+
* Everything here is plain JSON so a snapshot can be saved, diffed, attached
|
|
706
|
+
* to a report and judged again later without a browser. Capturing one needs a
|
|
707
|
+
* browser (see `@domandigital/craft/audit`); judging one does not.
|
|
708
|
+
*/
|
|
709
|
+
declare const SNAPSHOT_VERSION = 1;
|
|
710
|
+
interface SnapshotColour {
|
|
711
|
+
/** `rgb(r, g, b)` or `rgba(r, g, b, a)`, as the browser computed it. */
|
|
712
|
+
value: string;
|
|
713
|
+
/** Painted area in CSS px², summed over every element using it. */
|
|
714
|
+
area: number;
|
|
715
|
+
/** Characters of text drawn in it, for text colours. */
|
|
716
|
+
chars: number;
|
|
717
|
+
}
|
|
718
|
+
interface SnapshotFont {
|
|
719
|
+
/** The first family in the computed stack, unquoted. */
|
|
720
|
+
family: string;
|
|
721
|
+
/** Characters of visible text set in it. */
|
|
722
|
+
chars: number;
|
|
723
|
+
/** Characters of it in headings (h1 to h3 and elements at 28px or more). */
|
|
724
|
+
displayChars: number;
|
|
725
|
+
italicChars: number;
|
|
726
|
+
weights: number[];
|
|
727
|
+
}
|
|
728
|
+
interface SnapshotHeading {
|
|
729
|
+
level: number;
|
|
730
|
+
family: string;
|
|
731
|
+
sizePx: number;
|
|
732
|
+
weight: number;
|
|
733
|
+
italic: boolean;
|
|
734
|
+
/** An `<em>`, `<i>` or italic span inside a non-italic heading: "Beauty, *reimagined*". */
|
|
735
|
+
italicPart: boolean;
|
|
736
|
+
text: string;
|
|
737
|
+
top: number;
|
|
738
|
+
}
|
|
739
|
+
interface SnapshotControl {
|
|
740
|
+
/** `button` for a <button> or submit, `link` for an anchor styled as a button. */
|
|
741
|
+
kind: "button" | "link";
|
|
742
|
+
radiusPx: number;
|
|
743
|
+
heightPx: number;
|
|
744
|
+
background: string;
|
|
745
|
+
text: string;
|
|
746
|
+
}
|
|
747
|
+
type SectionKind = "hero" | "logos" | "stats" | "cards" | "marquee" | "text" | "other";
|
|
748
|
+
interface SnapshotSection {
|
|
749
|
+
top: number;
|
|
750
|
+
height: number;
|
|
751
|
+
kind: SectionKind;
|
|
752
|
+
/** The first heading's text, or the first words, so a report can name it. */
|
|
753
|
+
label: string;
|
|
754
|
+
/** Cards laid out side by side: siblings of similar size in one row. */
|
|
755
|
+
cards: number;
|
|
756
|
+
/** Of those cards, how many open with an icon (an <svg> or small image). */
|
|
757
|
+
iconCards: number;
|
|
758
|
+
/** Hidden at load, waiting for a scroll to reveal it. */
|
|
759
|
+
hiddenAtLoad: boolean;
|
|
760
|
+
}
|
|
761
|
+
interface SnapshotGradient {
|
|
762
|
+
/** Colour stops, as computed. */
|
|
763
|
+
stops: string[];
|
|
764
|
+
area: number;
|
|
765
|
+
radial: boolean;
|
|
766
|
+
}
|
|
767
|
+
interface SnapshotEffects {
|
|
768
|
+
/** Elements with backdrop-filter blur and a translucent background. */
|
|
769
|
+
glass: number;
|
|
770
|
+
/** Text painted with a gradient (background-clip: text). */
|
|
771
|
+
gradientText: number;
|
|
772
|
+
gradients: SnapshotGradient[];
|
|
773
|
+
/** Large blurred shapes or radial gradients behind content with no content of their own. */
|
|
774
|
+
glows: number;
|
|
775
|
+
/** Repeating 1px line or dot backgrounds. */
|
|
776
|
+
gridBackgrounds: number;
|
|
777
|
+
/** Infinite horizontal translate animations. */
|
|
778
|
+
marquees: number;
|
|
779
|
+
/** Cards with a near-invisible border and a wide soft shadow. */
|
|
780
|
+
hairlineShadowCards: number;
|
|
781
|
+
/** A small bordered pill directly above the first h1. */
|
|
782
|
+
eyebrowChip: boolean;
|
|
783
|
+
}
|
|
784
|
+
interface SnapshotMotion {
|
|
785
|
+
/** Sections hidden at load (opacity near 0 or translated) that appear on scroll. */
|
|
786
|
+
hiddenSections: number;
|
|
787
|
+
sections: number;
|
|
788
|
+
/** Running CSS or Web Animations at capture time. */
|
|
789
|
+
animations: number;
|
|
790
|
+
/** A full-screen layer over the page at load that is gone a few seconds later. */
|
|
791
|
+
introOverlay: boolean;
|
|
792
|
+
}
|
|
793
|
+
interface Snapshot {
|
|
794
|
+
version: typeof SNAPSHOT_VERSION;
|
|
795
|
+
url: string;
|
|
796
|
+
capturedAt: string;
|
|
797
|
+
viewport: {
|
|
798
|
+
width: number;
|
|
799
|
+
height: number;
|
|
800
|
+
};
|
|
801
|
+
pageHeight: number;
|
|
802
|
+
/** The page ground: the body's computed background, or the html's. */
|
|
803
|
+
ground: string;
|
|
804
|
+
colours: {
|
|
805
|
+
backgrounds: SnapshotColour[];
|
|
806
|
+
text: SnapshotColour[];
|
|
807
|
+
};
|
|
808
|
+
fonts: SnapshotFont[];
|
|
809
|
+
headings: SnapshotHeading[];
|
|
810
|
+
controls: SnapshotControl[];
|
|
811
|
+
sections: SnapshotSection[];
|
|
812
|
+
effects: SnapshotEffects;
|
|
813
|
+
motion: SnapshotMotion;
|
|
814
|
+
/** The design-system measures trawl has collected since v7, kept so it can switch to this. */
|
|
815
|
+
metrics: {
|
|
816
|
+
fontSizes: number[];
|
|
817
|
+
fontWeights: number[];
|
|
818
|
+
radii: string[];
|
|
819
|
+
shadows: string[];
|
|
820
|
+
spacingOnScalePct: number | null;
|
|
821
|
+
bodyLineLengthCh: number | null;
|
|
822
|
+
bodyFontSizePx: number | null;
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* The shapes shared by the tell catalogue, the source scanner and the copy
|
|
828
|
+
* engine.
|
|
829
|
+
*
|
|
830
|
+
* A tell is a choice an AI model makes when nobody told it otherwise. It is not
|
|
831
|
+
* a mistake. Every tell here would be fine as a decision; it is flagged because
|
|
832
|
+
* it is the answer the model reaches for first, so a site full of them reads as
|
|
833
|
+
* nobody having decided anything.
|
|
834
|
+
*/
|
|
835
|
+
/**
|
|
836
|
+
* Which wave of model defaults a tell belongs to.
|
|
837
|
+
*
|
|
838
|
+
* 1: the first wave (indigo, blue-to-purple gradients, Inter, glass cards).
|
|
839
|
+
* 2: what models moved to once the first wave was named and banned (cream and
|
|
840
|
+
* italic serif, eyebrow chips, bento grids, the marquee).
|
|
841
|
+
* 3: reserved for what the harvester finds next.
|
|
842
|
+
*
|
|
843
|
+
* The tag exists because any fixed list goes stale. Knowing which wave a tell
|
|
844
|
+
* came from is what lets a report say "this site was built in 2024" rather
|
|
845
|
+
* than only "this site has tells".
|
|
846
|
+
*/
|
|
847
|
+
type Generation = 1 | 2 | 3;
|
|
848
|
+
/** Where a tell is detected first. A source tell may also carry a rendered path. */
|
|
849
|
+
type Surface = "source" | "copy";
|
|
850
|
+
/**
|
|
851
|
+
* `warn` is reported and never fails a run unless `--strict` is passed.
|
|
852
|
+
* `block` fails a run.
|
|
853
|
+
*
|
|
854
|
+
* Every entry ships as `warn`. A rule blocks only after its hits across the
|
|
855
|
+
* estate have been read by a person and the rule tuned against them. A rule
|
|
856
|
+
* that blocks on day one gets switched off by day three.
|
|
857
|
+
*/
|
|
858
|
+
type Severity = "warn" | "block";
|
|
859
|
+
interface SourceFile {
|
|
860
|
+
/** Repo-relative path. The extension decides how the file is read. */
|
|
861
|
+
path: string;
|
|
862
|
+
text: string;
|
|
863
|
+
}
|
|
864
|
+
type FixtureCase = SourceFile | SourceFile[];
|
|
865
|
+
/** A detector's raw result, before it is placed on a line. */
|
|
866
|
+
interface Hit {
|
|
867
|
+
path: string;
|
|
868
|
+
/** Character offset into the file's text. */
|
|
869
|
+
offset: number;
|
|
870
|
+
message: string;
|
|
871
|
+
/** Overrides the excerpt taken from the offset's line. */
|
|
872
|
+
excerpt?: string;
|
|
873
|
+
}
|
|
874
|
+
interface Finding {
|
|
875
|
+
tell: string;
|
|
876
|
+
name: string;
|
|
877
|
+
generation: Generation;
|
|
878
|
+
severity: Severity;
|
|
879
|
+
path: string;
|
|
880
|
+
/** 1-indexed. 0 for a finding on a rendered page, where `path` is the URL. */
|
|
881
|
+
line: number;
|
|
882
|
+
excerpt: string;
|
|
883
|
+
message: string;
|
|
884
|
+
fix: string;
|
|
885
|
+
}
|
|
886
|
+
/** A class list as it applies to one element, with where it starts. */
|
|
887
|
+
interface ClassUnit {
|
|
888
|
+
classes: string[];
|
|
889
|
+
offset: number;
|
|
890
|
+
}
|
|
891
|
+
interface CssDeclaration {
|
|
892
|
+
property: string;
|
|
893
|
+
value: string;
|
|
894
|
+
/** The innermost rule's prelude: a selector, `@theme`, `@font-face`. */
|
|
895
|
+
selector: string;
|
|
896
|
+
offset: number;
|
|
897
|
+
}
|
|
898
|
+
/** A file read once, so every detector works from the same parse. */
|
|
899
|
+
interface ParsedFile extends SourceFile {
|
|
900
|
+
kind: "markup" | "script" | "css" | "prose" | "other";
|
|
901
|
+
classUnits: ClassUnit[];
|
|
902
|
+
declarations: CssDeclaration[];
|
|
903
|
+
}
|
|
904
|
+
interface ScanContext {
|
|
905
|
+
files: ParsedFile[];
|
|
906
|
+
}
|
|
907
|
+
interface CopyBlock {
|
|
908
|
+
path: string;
|
|
909
|
+
text: string;
|
|
910
|
+
/** Offset of `text[0]` in the original file, so hits land on real lines. */
|
|
911
|
+
offset: number;
|
|
912
|
+
/** A quoted string in code, as opposed to text a reader sees in place. */
|
|
913
|
+
literal?: boolean;
|
|
914
|
+
}
|
|
915
|
+
interface CopyContext {
|
|
916
|
+
files: SourceFile[];
|
|
917
|
+
/** Sentences: what word and phrase tells read. */
|
|
918
|
+
blocks: CopyBlock[];
|
|
919
|
+
/** Every visible string on its own, however short: what badge, dash and emoji tells read. */
|
|
920
|
+
strings: CopyBlock[];
|
|
921
|
+
}
|
|
922
|
+
interface TellBase {
|
|
923
|
+
/** Stable kebab-case id. Exceptions, suppressions and reports refer to it. */
|
|
924
|
+
id: string;
|
|
925
|
+
name: string;
|
|
926
|
+
generation: Generation;
|
|
927
|
+
severity: Severity;
|
|
928
|
+
/** Why this is a default rather than a decision, in one or two sentences. */
|
|
929
|
+
why: string;
|
|
930
|
+
/** What to do instead. Specific enough to act on without reading anything else. */
|
|
931
|
+
fix: string;
|
|
932
|
+
/**
|
|
933
|
+
* Proof the rule can fire and can stay quiet. Each entry needs at least one
|
|
934
|
+
* of each, and the catalogue test runs both: a guard that cannot fail is not
|
|
935
|
+
* a guard.
|
|
936
|
+
*
|
|
937
|
+
* Each item is one case, run on its own: a file, or an array of files for a
|
|
938
|
+
* tell that only exists across several. One case per detection path, so a
|
|
939
|
+
* path that breaks cannot hide behind one that still works.
|
|
940
|
+
*/
|
|
941
|
+
fixtures: {
|
|
942
|
+
flag: FixtureCase[];
|
|
943
|
+
pass: FixtureCase[];
|
|
944
|
+
};
|
|
945
|
+
/** Present when the tell can also be seen on a live page. */
|
|
946
|
+
rendered?: RenderedPath;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* The same tell measured on a rendered page: pure over a Snapshot, with its
|
|
950
|
+
* own flag and pass snapshots, proved on their own like any other path.
|
|
951
|
+
*/
|
|
952
|
+
interface RenderedPath {
|
|
953
|
+
detect(snapshot: Snapshot): Hit[];
|
|
954
|
+
fixtures: {
|
|
955
|
+
flag: Snapshot[];
|
|
956
|
+
pass: Snapshot[];
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
interface SourceTell extends TellBase {
|
|
960
|
+
surface: "source";
|
|
961
|
+
detect(ctx: ScanContext): Hit[];
|
|
962
|
+
}
|
|
963
|
+
interface CopyTell extends TellBase {
|
|
964
|
+
surface: "copy";
|
|
965
|
+
detect(ctx: CopyContext): Hit[];
|
|
966
|
+
}
|
|
967
|
+
type Tell = SourceTell | CopyTell;
|
|
968
|
+
/**
|
|
969
|
+
* A declared reason to keep a tell. Lives in the site's `art-direction.json`.
|
|
970
|
+
*
|
|
971
|
+
* DD's own brand is violet. A hue rule with no way to say so flags the agency's
|
|
972
|
+
* own site on every commit, and gets switched off. An exception without a
|
|
973
|
+
* `because` is not applied: the reason is the point.
|
|
974
|
+
*/
|
|
975
|
+
interface TellException {
|
|
976
|
+
tell: string;
|
|
977
|
+
because: string;
|
|
978
|
+
/** Where the reason comes from: the shopfront, the van, the trade's own look. */
|
|
979
|
+
evidence?: string;
|
|
980
|
+
/** Limit the exception to paths containing this string. */
|
|
981
|
+
path?: string;
|
|
982
|
+
}
|
|
983
|
+
interface CheckOptions {
|
|
984
|
+
exceptions?: TellException[];
|
|
985
|
+
/** Run only these tell ids. */
|
|
986
|
+
only?: string[];
|
|
987
|
+
}
|
|
988
|
+
interface CheckReport {
|
|
989
|
+
catalogueVersion: string;
|
|
990
|
+
findings: Finding[];
|
|
991
|
+
/** Exceptions that were declared but not applied, and why. */
|
|
992
|
+
rejectedExceptions: {
|
|
993
|
+
exception: TellException;
|
|
994
|
+
reason: string;
|
|
995
|
+
}[];
|
|
996
|
+
/** Findings each applied exception silenced, so an exception stays visible. */
|
|
997
|
+
excepted: {
|
|
998
|
+
tell: string;
|
|
999
|
+
because: string;
|
|
1000
|
+
count: number;
|
|
1001
|
+
}[];
|
|
1002
|
+
/**
|
|
1003
|
+
* Findings silenced by a `copy-ok` or `craft-ok` marker on the line or the
|
|
1004
|
+
* line above. Listed, never hidden: a marker nobody can see is a hole.
|
|
1005
|
+
*/
|
|
1006
|
+
suppressed: {
|
|
1007
|
+
tell: string;
|
|
1008
|
+
path: string;
|
|
1009
|
+
line: number;
|
|
1010
|
+
}[];
|
|
1011
|
+
summary: {
|
|
1012
|
+
files: number;
|
|
1013
|
+
findings: number;
|
|
1014
|
+
byGeneration: Record<Generation, number>;
|
|
1015
|
+
byTell: Record<string, number>;
|
|
1016
|
+
blocking: number;
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Running the catalogue: source scan and copy check.
|
|
1022
|
+
*
|
|
1023
|
+
* Both are pure over `{ path, text }[]`. The CLI reads files and git; a hook,
|
|
1024
|
+
* a CI job and trawl all call these with the same input and get the same
|
|
1025
|
+
* answer.
|
|
1026
|
+
*/
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Bumped whenever an entry is added, removed or its detection changes, so a
|
|
1030
|
+
* report can say which list it was judged against.
|
|
1031
|
+
*/
|
|
1032
|
+
declare const CATALOGUE_VERSION = "2026.09.3";
|
|
1033
|
+
declare const CATALOGUE: readonly Tell[];
|
|
1034
|
+
declare function tellById(id: string): Tell | undefined;
|
|
1035
|
+
/** Scan markup, component code and stylesheets for design tells. */
|
|
1036
|
+
declare function scanSource(files: SourceFile[], options?: CheckOptions): CheckReport;
|
|
1037
|
+
/** Check the prose in files for copy tells. */
|
|
1038
|
+
declare function checkCopy(files: SourceFile[], options?: CheckOptions): CheckReport;
|
|
1039
|
+
/**
|
|
1040
|
+
* Judge a rendered page. Every tell with a rendered path runs over the
|
|
1041
|
+
* snapshot; findings carry the URL as their path and line 0.
|
|
1042
|
+
*/
|
|
1043
|
+
declare function auditSnapshot(snapshot: Snapshot, options?: CheckOptions): CheckReport;
|
|
1044
|
+
/** Run one tell's detector over some files. What the fixture test calls. */
|
|
1045
|
+
declare function runTell(tell: Tell, files: SourceFile[]): Hit[];
|
|
1046
|
+
/**
|
|
1047
|
+
* The catalogue as a Markdown table. CHARACTER.md carries this between
|
|
1048
|
+
* markers, and a test fails if the two differ.
|
|
1049
|
+
*/
|
|
1050
|
+
declare function catalogueTable(): string;
|
|
1051
|
+
/**
|
|
1052
|
+
* The copy word and phrase lists, as portable JSON.
|
|
1053
|
+
*
|
|
1054
|
+
* These are the only part of the copy catalogue a non-TypeScript consumer can
|
|
1055
|
+
* use directly: a word list is data, a regex-shaped tell (contrastive
|
|
1056
|
+
* negation, the "No X" badge, em dash, emoji) is not something worth
|
|
1057
|
+
* re-deriving from a JSON export, so those stay TypeScript-only here.
|
|
1058
|
+
*
|
|
1059
|
+
* `claude-kit`'s `copy_check.py` is the reason this exists. It hardcoded its
|
|
1060
|
+
* own copies of these same lists rather than reading this package, and the
|
|
1061
|
+
* two drifted (documented in claude-kit's `house-style/copy-rules.md`, "Note
|
|
1062
|
+
* the enforcement gap"). `word-lists.json` is the file that ends the drift:
|
|
1063
|
+
* `install/sync-copy-wordlists.sh` on that side copies it in verbatim, same
|
|
1064
|
+
* shape as `sync-craft-standard.sh` does for `STANDARD.md`. Regenerate the
|
|
1065
|
+
* checked-in `word-lists.json` with `pnpm --filter @domandigital/craft run
|
|
1066
|
+
* docs` after changing any list in `tells/copy.ts`.
|
|
1067
|
+
*/
|
|
1068
|
+
declare function wordListsJson(): string;
|
|
1069
|
+
|
|
1070
|
+
/** Plain-text rendering of a report, for a terminal or a hook. */
|
|
1071
|
+
|
|
1072
|
+
declare function formatReport(report: CheckReport, title: string): string;
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* The rendered detection path for tells that can be seen on a live page.
|
|
1076
|
+
*
|
|
1077
|
+
* Same tells, same ids, measured on what the visitor gets rather than on what
|
|
1078
|
+
* the source says. A report can then say "Inter in source and on the page",
|
|
1079
|
+
* or catch the violet a CMS sets that no source file mentions.
|
|
1080
|
+
*
|
|
1081
|
+
* Pure over a Snapshot. Thresholds are here, next to their fixtures, and not
|
|
1082
|
+
* in the in-page collector, so each one can be proved without a browser.
|
|
1083
|
+
*/
|
|
1084
|
+
|
|
1085
|
+
declare const RENDERED_PATHS: Record<string, RenderedPath>;
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* A plain, decided-looking page as a snapshot, for fixtures.
|
|
1089
|
+
*
|
|
1090
|
+
* Every rendered tell's pass case starts from this and its flag case changes
|
|
1091
|
+
* one thing, so a detector is proved against the difference it claims to see.
|
|
1092
|
+
* Exported so a client repo can write the same kind of test.
|
|
1093
|
+
*/
|
|
1094
|
+
|
|
1095
|
+
type SnapshotPatch = {
|
|
1096
|
+
[K in keyof Snapshot]?: Snapshot[K] extends object ? Partial<Snapshot[K]> | Snapshot[K] : Snapshot[K];
|
|
1097
|
+
};
|
|
1098
|
+
declare function makeSnapshot(patch?: SnapshotPatch): Snapshot;
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Font family names as a browser reports them, turned back into the names a
|
|
1102
|
+
* person would use.
|
|
1103
|
+
*
|
|
1104
|
+
* `next/font` renames every family it loads: Inter arrives as
|
|
1105
|
+
* `__Inter_d65c78` and its metric-matched fallback as `__Inter_Fallback_d65c78`.
|
|
1106
|
+
* A rendered check that compared raw names would never see a single reflex
|
|
1107
|
+
* font on a Next.js site, which is most of the estate.
|
|
1108
|
+
*/
|
|
1109
|
+
type FontClass = "serif" | "sans" | "mono" | "script" | "system";
|
|
1110
|
+
declare function normaliseFamily(raw: string): string;
|
|
1111
|
+
declare function fontClass(family: string): FontClass;
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* A fingerprint: the handful of choices that make a site look like itself,
|
|
1115
|
+
* reduced to something two sites can be compared on.
|
|
1116
|
+
*
|
|
1117
|
+
* Signals 2 and 3 of CHARACTER.md both need a distance. "Would a model have
|
|
1118
|
+
* built this anyway" is a distance from what the model builds; "does this
|
|
1119
|
+
* look like the agency's other sites" is a distance from the estate. Neither
|
|
1120
|
+
* cares about the exact pixels. They care whether the accent, the type, the
|
|
1121
|
+
* ground, the shape language, the motion and the running order were chosen
|
|
1122
|
+
* or defaulted, so that is what a fingerprint records.
|
|
1123
|
+
*
|
|
1124
|
+
* Pure. Built from a Snapshot, so it describes what a visitor gets.
|
|
1125
|
+
*/
|
|
1126
|
+
|
|
1127
|
+
declare const FINGERPRINT_VERSION = 1;
|
|
1128
|
+
interface Fingerprint {
|
|
1129
|
+
version: typeof FINGERPRINT_VERSION;
|
|
1130
|
+
/** The most prominent chromatic colour: button fills first, then painted area, then text. */
|
|
1131
|
+
accent: Oklch | null;
|
|
1132
|
+
ground: Oklch;
|
|
1133
|
+
display: {
|
|
1134
|
+
family: string;
|
|
1135
|
+
class: FontClass;
|
|
1136
|
+
};
|
|
1137
|
+
body: {
|
|
1138
|
+
family: string;
|
|
1139
|
+
class: FontClass;
|
|
1140
|
+
};
|
|
1141
|
+
/** Median button corner radius over its height, 0 (square) to 0.5 (pill). */
|
|
1142
|
+
roundness: number | null;
|
|
1143
|
+
/** Share of below-the-fold sections that wait for a scroll to appear. */
|
|
1144
|
+
motion: number;
|
|
1145
|
+
/** Which second-look effects are present, as tell ids. */
|
|
1146
|
+
effects: string[];
|
|
1147
|
+
/** Section kinds in running order, hero first. */
|
|
1148
|
+
layout: SectionKind[];
|
|
1149
|
+
}
|
|
1150
|
+
declare function fingerprint(s: Snapshot): Fingerprint;
|
|
1151
|
+
interface FingerprintDistance {
|
|
1152
|
+
/** 0 identical, 1 nothing in common. A weighted mean of the parts. */
|
|
1153
|
+
total: number;
|
|
1154
|
+
parts: {
|
|
1155
|
+
accent: number;
|
|
1156
|
+
ground: number;
|
|
1157
|
+
type: number;
|
|
1158
|
+
shape: number;
|
|
1159
|
+
motion: number;
|
|
1160
|
+
effects: number;
|
|
1161
|
+
layout: number;
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* How much each part counts. Accent and type are where a model's defaults
|
|
1166
|
+
* show first and where a real business has something of its own, so they
|
|
1167
|
+
* carry half the weight between them.
|
|
1168
|
+
*/
|
|
1169
|
+
declare const DISTANCE_WEIGHTS: {
|
|
1170
|
+
readonly accent: 0.25;
|
|
1171
|
+
readonly type: 0.25;
|
|
1172
|
+
readonly ground: 0.1;
|
|
1173
|
+
readonly shape: 0.1;
|
|
1174
|
+
readonly motion: 0.1;
|
|
1175
|
+
readonly effects: 0.1;
|
|
1176
|
+
readonly layout: 0.1;
|
|
1177
|
+
};
|
|
1178
|
+
declare function fingerprintDistance(a: Fingerprint, b: Fingerprint): FingerprintDistance;
|
|
1179
|
+
/**
|
|
1180
|
+
* The nearest neighbours of a fingerprint in a set, closest first. Typicality
|
|
1181
|
+
* (phase D) and estate distance (phase E) are both this, against different sets.
|
|
1182
|
+
*/
|
|
1183
|
+
declare function nearest<T>(target: Fingerprint, set: {
|
|
1184
|
+
id: T;
|
|
1185
|
+
fingerprint: Fingerprint;
|
|
1186
|
+
}[], k?: number): {
|
|
1187
|
+
id: T;
|
|
1188
|
+
distance: FingerprintDistance;
|
|
1189
|
+
}[];
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Every colour syntax the estate actually writes, read into OKLCh.
|
|
1193
|
+
*
|
|
1194
|
+
* A hue rule that only knows Tailwind class names misses the site that sets its
|
|
1195
|
+
* violet as `#5a35d1`, which is exactly what the old checker did. Hue is judged
|
|
1196
|
+
* in OKLCh because that is where "violet" is one range of angles; in RGB and HSL
|
|
1197
|
+
* the same perceived hue drifts with lightness.
|
|
1198
|
+
*/
|
|
1199
|
+
|
|
1200
|
+
interface FoundColour {
|
|
1201
|
+
raw: string;
|
|
1202
|
+
oklch: Oklch;
|
|
1203
|
+
alpha: number;
|
|
1204
|
+
/** Offset of `raw` inside the searched string. */
|
|
1205
|
+
index: number;
|
|
1206
|
+
}
|
|
1207
|
+
declare function parseColour(raw: string): {
|
|
1208
|
+
oklch: Oklch;
|
|
1209
|
+
alpha: number;
|
|
1210
|
+
} | null;
|
|
1211
|
+
declare function findColours(value: string): FoundColour[];
|
|
1212
|
+
/**
|
|
1213
|
+
* The indigo-to-purple band models default to, in OKLCh hue degrees.
|
|
1214
|
+
*
|
|
1215
|
+
* Tailwind's blue-600 sits at 262, indigo-600 at 277, violet-600 at 293,
|
|
1216
|
+
* purple-600 at 304 and fuchsia at 322. The band starts past blue, so a
|
|
1217
|
+
* plumber's navy is left alone, and stops before fuchsia and pink, which are
|
|
1218
|
+
* a different conversation. Chroma must be high enough to read as a colour
|
|
1219
|
+
* choice rather than a tinted grey.
|
|
1220
|
+
*/
|
|
1221
|
+
declare const AI_VIOLET: {
|
|
1222
|
+
readonly hueMin: 268;
|
|
1223
|
+
readonly hueMax: 312;
|
|
1224
|
+
readonly minChroma: 0.1;
|
|
1225
|
+
readonly minL: 0.3;
|
|
1226
|
+
readonly maxL: 0.85;
|
|
1227
|
+
};
|
|
1228
|
+
declare function isAiViolet({ l, c, h }: Oklch): boolean;
|
|
1229
|
+
/**
|
|
1230
|
+
* The warm off-white the second wave moved to once violet was named: parchment,
|
|
1231
|
+
* linen, oat. Very light, faintly saturated, yellow-orange hue.
|
|
1232
|
+
*/
|
|
1233
|
+
declare function isCream({ l, c, h }: Oklch): boolean;
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Tells found in source: markup, component code and stylesheets.
|
|
1237
|
+
*
|
|
1238
|
+
* Detectors are deliberately plain. Each reads the parse every other detector
|
|
1239
|
+
* reads, looks for one pattern, and says where. Thresholds for the counted
|
|
1240
|
+
* tells (reveals, pills) come from the estate survey: they sit below the sites
|
|
1241
|
+
* that read as templated and above the ones that do not.
|
|
1242
|
+
*/
|
|
1243
|
+
|
|
1244
|
+
/** The faces a model picks when the brief does not name one. */
|
|
1245
|
+
declare const REFLEX_FONTS_1: readonly ["Inter", "Roboto", "Open Sans", "Poppins", "Montserrat", "Lato"];
|
|
1246
|
+
/** What models moved to once Inter was named and banned. */
|
|
1247
|
+
declare const REFLEX_FONTS_2: readonly ["Inter Tight", "DM Sans", "Manrope", "Space Grotesk", "Plus Jakarta Sans", "Outfit", "Geist", "Sora", "Instrument Sans", "Instrument Serif", "Fraunces", "Playfair Display", "Cormorant Garamond", "DM Serif Display"];
|
|
1248
|
+
/** Above this many scroll reveals in one scan, reveal is the default, not a choice. */
|
|
1249
|
+
declare const REVEAL_LIMIT = 12;
|
|
1250
|
+
/** Above this many fully rounded shapes in one scan, the pill is the shape language by accident. */
|
|
1251
|
+
declare const PILL_LIMIT = 30;
|
|
1252
|
+
/** A components/ui folder with this many stock primitives is a generator's dump. */
|
|
1253
|
+
declare const SHADCN_LIMIT = 12;
|
|
1254
|
+
/** The stock shadcn/ui primitives, as their files are named. */
|
|
1255
|
+
declare const SHADCN_PRIMITIVES: readonly ["accordion", "alert", "alert-dialog", "aspect-ratio", "avatar", "badge", "breadcrumb", "button", "calendar", "card", "carousel", "chart", "checkbox", "collapsible", "command", "context-menu", "dialog", "drawer", "dropdown-menu", "form", "hover-card", "input", "input-otp", "label", "menubar", "navigation-menu", "pagination", "popover", "progress", "radio-group", "resizable", "scroll-area", "select", "separator", "sheet", "sidebar", "skeleton", "slider", "sonner", "switch", "table", "tabs", "textarea", "toast", "toaster", "toggle", "toggle-group", "tooltip"];
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* Tells found in copy.
|
|
1259
|
+
*
|
|
1260
|
+
* The rule lists live here as data, with fixtures, rather than hardcoded in a
|
|
1261
|
+
* checker in another repo where they had already drifted from the written
|
|
1262
|
+
* rules. Since 2026.09.2 this is also where the house copy rules live: the
|
|
1263
|
+
* blocking tier of claude-kit's `house-style/copy-rules.md` is carried by its
|
|
1264
|
+
* own tells here, and `copy-check` is a wrapper that decides which of them
|
|
1265
|
+
* fail a commit. craft itself ships every tell as `warn`.
|
|
1266
|
+
*
|
|
1267
|
+
* Word and phrase tells read sentences (`ctx.blocks`). Tells that key on a
|
|
1268
|
+
* shape short enough to sit in a label, an em dash, an emoji, a "No catch."
|
|
1269
|
+
* badge, read every visible string (`ctx.strings`).
|
|
1270
|
+
*/
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Words a model reaches for that a person writing about their own business
|
|
1274
|
+
* rarely uses. The house-blocking words (seamless, empower, delve, leverage,
|
|
1275
|
+
* harness) are not here: they have tells of their own, so a finding says
|
|
1276
|
+
* which tier it belongs to.
|
|
1277
|
+
*/
|
|
1278
|
+
declare const AI_WORDS: readonly ["tapestry", "testament", "realm", "embark", "embarking", "unleash", "unlocks", "unlocking", "elevate", "elevates", "elevated", "elevating", "bustling", "nestled", "meticulous", "meticulously", "paramount", "furthermore", "moreover", "navigating", "ever-evolving", "unparalleled"];
|
|
1279
|
+
declare const STOCK_PHRASES: readonly ["in today's digital age", "to the next level", "one-stop shop", "second to none", "we pride ourselves", "passionate about", "your journey", "our journey", "curated", "crafted with care", "tailored to your", "peace of mind"];
|
|
1280
|
+
/**
|
|
1281
|
+
* The phrase list from the blocking tier of copy-rules.md, plus the phrases
|
|
1282
|
+
* the old copy-check blocked that the rules had never written down. Each is a
|
|
1283
|
+
* phrase generated copy uses far more than people do.
|
|
1284
|
+
*/
|
|
1285
|
+
declare const AI_PHRASES: readonly ["let's dive in", "dive into", "let's unpack", "let's break this down", "let's break it down", "here's the thing", "it's important to note", "it's important to remember", "it is worth noting that", "at its core", "the key takeaway", "the reality is", "the truth is", "increasingly digital", "seamless", "seamlessly", "unlock your", "unlock the power", "meaningful impact", "drive meaningful", "foster collaboration", "fosters collaboration", "fostering collaboration", "comprehensive guide to", "look no further", "in the event that", "at no additional cost to you", "rest assured", "when it comes to", "we have got you covered", "we've got you covered"];
|
|
1286
|
+
/** Blocked because a plainer word almost always exists. */
|
|
1287
|
+
declare const PLAINER_WORDS: readonly ["empower", "empowers", "empowered", "empowering", "harness", "harnesses", "harnessed", "harnessing", "delve", "delves", "delved", "delving", "leverage", "leverages", "leveraged", "leveraging"];
|
|
1288
|
+
/** The corporate buzzwords of the house list. Older than any model; models still reach for them. */
|
|
1289
|
+
declare const BUZZWORDS: readonly ["synergy", "synergise", "synergize", "cutting-edge", "cutting edge", "best-in-class", "best in class", "best of breed", "game-changer", "game changer", "paradigm shift", "world-class", "state of the art", "state-of-the-art", "mission-critical", "low-hanging fruit", "move the needle", "circle back", "touch base", "reach out", "value-add", "robust", "turnkey", "holistic", "disruptive"];
|
|
1290
|
+
declare const NEGATIVE_REASSURANCE: readonly ["no risk", "never locked in", "nothing goes wrong", "no hidden fees", "never pay", "without the hassle", "no surprises"];
|
|
1291
|
+
/** Banned only when vague. Reported for review, never blocking. */
|
|
1292
|
+
declare const VAGUE_WORDS: readonly ["innovative", "scalable", "end-to-end", "streamline", "solutions"];
|
|
1293
|
+
/** The review tier of copy-rules.md: normal English once, a tell when stacked. */
|
|
1294
|
+
declare const REVIEW_PHRASES: readonly ["great question", "of course!", "absolutely!", "absolutely,", "truly", "genuinely", "deeply", "ultimately", "one-size-fits-all", "one size fits all", "let me know if you'd", "the single most", "by far the most", "hands down the", "arguably the best", "bar none"];
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* `art-direction.json`: what a site decided about how it looks, and why.
|
|
1298
|
+
*
|
|
1299
|
+
* CHARACTER.md's reason rule lives here. Every expressive choice records a
|
|
1300
|
+
* `because` drawn from the client's real world (the shopfront, the van, the
|
|
1301
|
+
* trade's own lettering, the place, the materials) and points at a source
|
|
1302
|
+
* that shows it. A choice with no reason is a default, however good it looks.
|
|
1303
|
+
*
|
|
1304
|
+
* The file also carries the tell exceptions craft has read since phase A, in
|
|
1305
|
+
* the same shape, so an existing file stays valid.
|
|
1306
|
+
*/
|
|
1307
|
+
|
|
1308
|
+
declare const DIRECTION_VERSION = 1;
|
|
1309
|
+
/** Where a reason comes from. Deliberately physical: a mood board is not a source. */
|
|
1310
|
+
declare const SOURCE_KINDS: readonly ["livery", "shopfront", "signage", "packaging", "product", "uniform", "interior", "place", "material", "trade", "print", "photo", "founder"];
|
|
1311
|
+
type SourceKind = (typeof SOURCE_KINDS)[number];
|
|
1312
|
+
interface DirectionSource {
|
|
1313
|
+
/** Short id a choice refers to: "van", "fascia", "price-list". */
|
|
1314
|
+
id: string;
|
|
1315
|
+
kind: SourceKind;
|
|
1316
|
+
/** What it is and where, in a sentence. */
|
|
1317
|
+
note: string;
|
|
1318
|
+
/** A photo or file in the repo, or a URL. */
|
|
1319
|
+
path?: string;
|
|
1320
|
+
/** Colours read off it by a person or by `craft direction propose`, as hex. */
|
|
1321
|
+
colours?: string[];
|
|
1322
|
+
/** Lettering seen on it: "hand-painted sign-writing", "stencilled capitals". */
|
|
1323
|
+
lettering?: string;
|
|
1324
|
+
}
|
|
1325
|
+
/** The expressive choices. Layout and navigation are not here: they stay conventional. */
|
|
1326
|
+
declare const CHOICE_KEYS: readonly ["accent", "ground", "display", "body", "shape", "motif", "signature"];
|
|
1327
|
+
type ChoiceKey = (typeof CHOICE_KEYS)[number];
|
|
1328
|
+
interface DirectionChoice {
|
|
1329
|
+
/** A colour as hex for accent and ground, a family name for display and body, words otherwise. */
|
|
1330
|
+
value: string;
|
|
1331
|
+
because: string;
|
|
1332
|
+
/** Source ids this reason rests on. */
|
|
1333
|
+
evidence: string[];
|
|
1334
|
+
}
|
|
1335
|
+
interface ArtDirection {
|
|
1336
|
+
$schema?: string;
|
|
1337
|
+
version: typeof DIRECTION_VERSION;
|
|
1338
|
+
client: string;
|
|
1339
|
+
/** Who, where, and what they do, in a sentence or two. The brief a model would have been given. */
|
|
1340
|
+
brief: string;
|
|
1341
|
+
sources: DirectionSource[];
|
|
1342
|
+
choices: Partial<Record<ChoiceKey, DirectionChoice>>;
|
|
1343
|
+
exceptions?: TellException[];
|
|
1344
|
+
}
|
|
1345
|
+
type DirectionSeverity = "error" | "warn";
|
|
1346
|
+
interface DirectionProblem {
|
|
1347
|
+
severity: DirectionSeverity;
|
|
1348
|
+
/** Where in the file: "choices.accent.because", "sources[2].kind". */
|
|
1349
|
+
at: string;
|
|
1350
|
+
message: string;
|
|
1351
|
+
}
|
|
1352
|
+
interface DirectionReport {
|
|
1353
|
+
valid: boolean;
|
|
1354
|
+
problems: DirectionProblem[];
|
|
1355
|
+
/** How many of the seven choices carry an accepted reason. */
|
|
1356
|
+
decided: number;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/**
|
|
1360
|
+
* Checking an art direction: the file's shape, the reason rule, and whether
|
|
1361
|
+
* the site actually does what the file says.
|
|
1362
|
+
*
|
|
1363
|
+
* Pure. The CLI reads the file, any snapshot and the paths it cites.
|
|
1364
|
+
*/
|
|
1365
|
+
|
|
1366
|
+
interface ValidateContext {
|
|
1367
|
+
/** The fingerprint of the rendered site, to check the file against what ships. */
|
|
1368
|
+
fingerprint?: Fingerprint;
|
|
1369
|
+
/** Which cited paths exist, when the caller can look. */
|
|
1370
|
+
pathExists?: (path: string) => boolean;
|
|
1371
|
+
}
|
|
1372
|
+
declare function validateDirection(input: unknown, ctx?: ValidateContext): DirectionReport;
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* A first art-direction.json for a site that exists: what it does today,
|
|
1376
|
+
* written down, with every reason left empty.
|
|
1377
|
+
*
|
|
1378
|
+
* Empty on purpose. `validateDirection` fails the file until each choice has
|
|
1379
|
+
* a source and a reason, so the first thing init produces is the list of
|
|
1380
|
+
* things nobody decided.
|
|
1381
|
+
*/
|
|
1382
|
+
|
|
1383
|
+
declare function initDirection(input: {
|
|
1384
|
+
client?: string;
|
|
1385
|
+
brief?: string;
|
|
1386
|
+
current?: Fingerprint;
|
|
1387
|
+
exceptions?: ArtDirection["exceptions"];
|
|
1388
|
+
}): ArtDirection;
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* Proposing an art direction from the client's world.
|
|
1392
|
+
*
|
|
1393
|
+
* A proposal is a draft, never a decision. Every choice it writes carries a
|
|
1394
|
+
* `because` that starts "PROPOSED:", and `validateDirection` rejects that
|
|
1395
|
+
* prefix, so the file cannot pass until a person has looked at the van, agreed
|
|
1396
|
+
* the green is the van's green, and said so in their own words.
|
|
1397
|
+
*
|
|
1398
|
+
* Pure. Pixels come in as numbers; the CLI decodes the images.
|
|
1399
|
+
*/
|
|
1400
|
+
|
|
1401
|
+
declare const PROPOSED = "PROPOSED:";
|
|
1402
|
+
interface PaletteColour {
|
|
1403
|
+
hex: string;
|
|
1404
|
+
/** Share of sampled pixels, 0 to 1. */
|
|
1405
|
+
share: number;
|
|
1406
|
+
oklch: Oklch;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* The dominant colours in an RGBA pixel buffer: k-means in OKLab, so colours
|
|
1410
|
+
* cluster the way they look alike. Deterministic: seeded by lightness order,
|
|
1411
|
+
* not at random, so the same photo always proposes the same colours.
|
|
1412
|
+
*/
|
|
1413
|
+
declare function paletteFromPixels(rgba: ArrayLike<number>, k?: number, maxSamples?: number): PaletteColour[];
|
|
1414
|
+
interface ProposeInput {
|
|
1415
|
+
client: string;
|
|
1416
|
+
brief: string;
|
|
1417
|
+
sources: DirectionSource[];
|
|
1418
|
+
/** What the site ships today, if it exists. */
|
|
1419
|
+
current?: Fingerprint;
|
|
1420
|
+
/** The rest of the estate, so a proposal does not land on a sibling. */
|
|
1421
|
+
estate?: {
|
|
1422
|
+
id: string;
|
|
1423
|
+
fingerprint: Fingerprint;
|
|
1424
|
+
}[];
|
|
1425
|
+
}
|
|
1426
|
+
interface Proposal {
|
|
1427
|
+
key: ChoiceKey;
|
|
1428
|
+
candidates: {
|
|
1429
|
+
value: string;
|
|
1430
|
+
evidence: string[];
|
|
1431
|
+
note: string;
|
|
1432
|
+
}[];
|
|
1433
|
+
}
|
|
1434
|
+
declare function propose(input: ProposeInput): {
|
|
1435
|
+
proposals: Proposal[];
|
|
1436
|
+
direction: ArtDirection;
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* The counterfactual: what a model builds for this brief when nobody tells it
|
|
1441
|
+
* anything else. Signal 2 of CHARACTER.md.
|
|
1442
|
+
*
|
|
1443
|
+
* A null model is about twenty pages generated from one brief, each reduced to
|
|
1444
|
+
* a fingerprint. A site is typical when it sits as close to those pages as they
|
|
1445
|
+
* sit to each other. That question keeps working when model defaults move,
|
|
1446
|
+
* which a fixed list of tells cannot: ban Inter and the null model moves with
|
|
1447
|
+
* the model, so the comparison moves too.
|
|
1448
|
+
*
|
|
1449
|
+
* `harvest` reads the same pages the other way round: which choices recur
|
|
1450
|
+
* across them that the catalogue does not know yet. Those are the candidates
|
|
1451
|
+
* for the next generation of tells. A person decides; nothing is added here.
|
|
1452
|
+
*
|
|
1453
|
+
* Pure. Generating pages and snapshotting them lives in the CLI.
|
|
1454
|
+
*/
|
|
1455
|
+
|
|
1456
|
+
declare const NULL_VERSION = 1;
|
|
1457
|
+
/**
|
|
1458
|
+
* The prompt a null run gets: the brief, and nothing about how it should look.
|
|
1459
|
+
* Changing this changes every null model built after it, so it is versioned
|
|
1460
|
+
* by being recorded in each one.
|
|
1461
|
+
*/
|
|
1462
|
+
declare function nullPrompt(brief: string): string;
|
|
1463
|
+
interface NullRun {
|
|
1464
|
+
/** The page's file name without extension: "01". */
|
|
1465
|
+
id: string;
|
|
1466
|
+
fingerprint: Fingerprint;
|
|
1467
|
+
/** Catalogue tells found on the page, source and rendered, each once. */
|
|
1468
|
+
tells: string[];
|
|
1469
|
+
/** Every visible string on the page, for the phrase harvest. */
|
|
1470
|
+
copy: string[];
|
|
1471
|
+
}
|
|
1472
|
+
interface NullModel {
|
|
1473
|
+
version: typeof NULL_VERSION;
|
|
1474
|
+
brief: string;
|
|
1475
|
+
prompt: string;
|
|
1476
|
+
/** What was asked for with --model, or "default". */
|
|
1477
|
+
model: string;
|
|
1478
|
+
catalogueVersion: string;
|
|
1479
|
+
builtAt: string;
|
|
1480
|
+
runs: NullRun[];
|
|
1481
|
+
}
|
|
1482
|
+
/** The HTML document in a model's reply, without code fences or commentary. */
|
|
1483
|
+
declare function extractHtml(reply: string): string | null;
|
|
1484
|
+
/** Fewer runs than this and the spread of the null is noise, so nothing is judged. */
|
|
1485
|
+
declare const MIN_RUNS = 5;
|
|
1486
|
+
/** Typical when at least this share of the null's own pages sit further out. */
|
|
1487
|
+
declare const TYPICAL_AT = 0.1;
|
|
1488
|
+
interface Typicality {
|
|
1489
|
+
/**
|
|
1490
|
+
* 0 to 1: the share of the null's own pages that sit further from the rest
|
|
1491
|
+
* than this page sits from them. 1 is more typical than anything the model
|
|
1492
|
+
* built; 0 is further out than every one of them.
|
|
1493
|
+
*/
|
|
1494
|
+
score: number;
|
|
1495
|
+
typical: boolean;
|
|
1496
|
+
/** Mean distance to the k nearest null pages. */
|
|
1497
|
+
distance: number;
|
|
1498
|
+
/** The same measure for each null page against the others, median. */
|
|
1499
|
+
baseline: number;
|
|
1500
|
+
nearest: {
|
|
1501
|
+
id: string;
|
|
1502
|
+
distance: number;
|
|
1503
|
+
}[];
|
|
1504
|
+
/** The choices this page shares with most of the null: why it reads as typical. */
|
|
1505
|
+
shared: {
|
|
1506
|
+
part: string;
|
|
1507
|
+
value: string;
|
|
1508
|
+
runs: number;
|
|
1509
|
+
of: number;
|
|
1510
|
+
}[];
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* How typical a page is against one or more null models (pooled). Every null
|
|
1514
|
+
* page is scored against the others the same way, leaving itself out, so the
|
|
1515
|
+
* threshold comes from the model's own spread and not from a number picked
|
|
1516
|
+
* here: a page drawn from the null is flagged nine times in ten by
|
|
1517
|
+
* construction, and the calibration checks whether real sites are not.
|
|
1518
|
+
*/
|
|
1519
|
+
declare function typicality(target: Fingerprint, runs: NullRun[], k?: number): Typicality;
|
|
1520
|
+
declare function hueName(o: Oklch | null): string;
|
|
1521
|
+
declare function groundName(o: Oklch): string;
|
|
1522
|
+
declare function shapeName(roundness: number | null): string;
|
|
1523
|
+
/** The discrete choices a fingerprint makes, as part → value. What a person would name. */
|
|
1524
|
+
declare function choicesOf(fp: Fingerprint): Record<string, string[]>;
|
|
1525
|
+
type CandidateKind = "face" | "accent" | "ground" | "shape" | "motion" | "opening" | "tell" | "phrase";
|
|
1526
|
+
interface HarvestCandidate {
|
|
1527
|
+
kind: CandidateKind;
|
|
1528
|
+
value: string;
|
|
1529
|
+
/** Null pages that make this choice, across every model given. */
|
|
1530
|
+
runs: number;
|
|
1531
|
+
of: number;
|
|
1532
|
+
/** Distinct briefs it turned up for. A habit shows across briefs; a brief's own words do not. */
|
|
1533
|
+
briefs: number;
|
|
1534
|
+
/** The catalogue tell that already catches it, or null when nothing does. */
|
|
1535
|
+
known: string | null;
|
|
1536
|
+
/** An example, for a phrase: the string it came from. */
|
|
1537
|
+
example?: string;
|
|
1538
|
+
}
|
|
1539
|
+
interface HarvestOptions {
|
|
1540
|
+
/** Report a choice made by at least this share of pages. Default 0.25. */
|
|
1541
|
+
minShare?: number;
|
|
1542
|
+
/** Which copy tell, if any, already catches a phrase. The CLI passes the catalogue's. */
|
|
1543
|
+
copyTell?: (phrase: string) => string | null;
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Choices that recur across null pages, with the tell that already catches
|
|
1547
|
+
* each, if any. The ones with `known: null` are the next generation's
|
|
1548
|
+
* candidates. Nothing here decides: a person reads them, and a candidate only
|
|
1549
|
+
* becomes a tell with a flag case and a pass case like every other.
|
|
1550
|
+
*/
|
|
1551
|
+
declare function harvest(models: Pick<NullModel, "brief" | "runs">[], options?: HarvestOptions): HarvestCandidate[];
|
|
1552
|
+
/** A representative colour for a hue family, for a report. */
|
|
1553
|
+
declare function hueSwatch(runs: NullRun[], hue: string): string | null;
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* The estate register: every site the agency has shipped, fingerprinted, so a
|
|
1557
|
+
* new one can be checked against its siblings. Signal 3 of CHARACTER.md.
|
|
1558
|
+
*
|
|
1559
|
+
* An agency's biggest tell is its clients looking like each other. No single
|
|
1560
|
+
* site shows it; only the set does. So the register holds one fingerprint per
|
|
1561
|
+
* site, and `compareToEstate` says which sites a page sits nearest and what
|
|
1562
|
+
* it shares with them.
|
|
1563
|
+
*
|
|
1564
|
+
* The line for "siblings" is not a number picked here. Two different
|
|
1565
|
+
* businesses are siblings when they sit closer together than two pages Claude
|
|
1566
|
+
* builds for the same brief usually do: if a barber and an electrician look
|
|
1567
|
+
* more alike than two drafts of one barber, nobody chose either look. The
|
|
1568
|
+
* median comes from null models when they are given, and from the 2026-09-23
|
|
1569
|
+
* calibration when they are not.
|
|
1570
|
+
*
|
|
1571
|
+
* Pure. The CLI reads and writes the register file.
|
|
1572
|
+
*/
|
|
1573
|
+
|
|
1574
|
+
declare const ESTATE_VERSION = 1;
|
|
1575
|
+
/**
|
|
1576
|
+
* Median fingerprint distance between two null pages built from the same
|
|
1577
|
+
* brief, across seven briefs and 1,330 pairs (calibration/null, 2026-09-23).
|
|
1578
|
+
*/
|
|
1579
|
+
declare const SIBLING_AT = 0.31;
|
|
1580
|
+
interface EstateSite {
|
|
1581
|
+
/** Short kebab-case id: "rmp", "chair-and-blade". */
|
|
1582
|
+
id: string;
|
|
1583
|
+
client: string;
|
|
1584
|
+
url: string;
|
|
1585
|
+
/** Where the site's source lives, if known: "rmp474/RMP-Electrical". */
|
|
1586
|
+
repo?: string;
|
|
1587
|
+
fingerprint: Fingerprint;
|
|
1588
|
+
/** Rendered tells found when it was added, for the record. */
|
|
1589
|
+
tells: string[];
|
|
1590
|
+
addedAt: string;
|
|
1591
|
+
}
|
|
1592
|
+
interface EstateRegister {
|
|
1593
|
+
version: typeof ESTATE_VERSION;
|
|
1594
|
+
sites: EstateSite[];
|
|
1595
|
+
}
|
|
1596
|
+
declare function emptyEstate(): EstateRegister;
|
|
1597
|
+
/** Add a site, or replace the one with the same id. Sorted by id so the file diffs cleanly. */
|
|
1598
|
+
declare function addSite(register: EstateRegister, site: EstateSite): EstateRegister;
|
|
1599
|
+
/**
|
|
1600
|
+
* What two fingerprints effectively share, named the way a person would see
|
|
1601
|
+
* it: "Fraunces headline", "no accent", "pill buttons". A part counts when its
|
|
1602
|
+
* distance is 0.2 or less.
|
|
1603
|
+
*/
|
|
1604
|
+
declare function sharedParts(a: Fingerprint, b: Fingerprint, d?: FingerprintDistance): string[];
|
|
1605
|
+
interface EstateMatch {
|
|
1606
|
+
id: string;
|
|
1607
|
+
distance: number;
|
|
1608
|
+
sibling: boolean;
|
|
1609
|
+
shared: string[];
|
|
1610
|
+
}
|
|
1611
|
+
/** The sites nearest a fingerprint, closest first, leaving out `exclude` (the site itself). */
|
|
1612
|
+
declare function compareToEstate(target: Fingerprint, register: EstateRegister, options?: {
|
|
1613
|
+
exclude?: string;
|
|
1614
|
+
siblingAt?: number;
|
|
1615
|
+
}): EstateMatch[];
|
|
1616
|
+
interface EstatePair {
|
|
1617
|
+
a: string;
|
|
1618
|
+
b: string;
|
|
1619
|
+
distance: number;
|
|
1620
|
+
sibling: boolean;
|
|
1621
|
+
shared: string[];
|
|
1622
|
+
}
|
|
1623
|
+
/** Every pair in the register, closest first. */
|
|
1624
|
+
declare function estatePairs(register: EstateRegister, siblingAt?: number): EstatePair[];
|
|
1625
|
+
/**
|
|
1626
|
+
* The sibling line from null models: the median distance between two pages
|
|
1627
|
+
* built from the same brief. Each inner array is one brief's fingerprints.
|
|
1628
|
+
*/
|
|
1629
|
+
declare function siblingLine(briefs: Fingerprint[][]): number;
|
|
1630
|
+
|
|
1631
|
+
/**
|
|
1632
|
+
* The character report: the three signals and the reason rule, read together.
|
|
1633
|
+
*
|
|
1634
|
+
* No one signal decides. A site can carry no catalogued tell and still be what
|
|
1635
|
+
* Claude builds for its brief (signal 2); it can be nothing like the null and
|
|
1636
|
+
* still wear last year's reflexes (signal 1); it can pass both and be a twin of
|
|
1637
|
+
* the agency's other client (signal 3). And a site that passes all three has
|
|
1638
|
+
* still only avoided defaults: "decided" needs the reasons written down.
|
|
1639
|
+
*
|
|
1640
|
+
* So the verdict is:
|
|
1641
|
+
* default two or more signals raised
|
|
1642
|
+
* mixed one raised
|
|
1643
|
+
* decided none raised, and all measured, reasons included
|
|
1644
|
+
* unproven none raised, but something was not measured. Unverified is
|
|
1645
|
+
* never a pass.
|
|
1646
|
+
*
|
|
1647
|
+
* The actions change the surface: type, colour, shape, effects, motion, copy.
|
|
1648
|
+
* Never the page grammar. A layout tell says what to change about how a
|
|
1649
|
+
* section looks, not that it should move.
|
|
1650
|
+
*
|
|
1651
|
+
* Pure. The CLI gathers the inputs.
|
|
1652
|
+
*/
|
|
1653
|
+
|
|
1654
|
+
/** Three or more distinct design tells: the rule fixed for the 2026-09-23 calibration. */
|
|
1655
|
+
declare const TELL_HEAVY = 3;
|
|
1656
|
+
/** Fewer choices decided than this and the reasons are not yet a direction. */
|
|
1657
|
+
declare const MIN_DECIDED = 5;
|
|
1658
|
+
type Verdict = "default" | "mixed" | "decided" | "unproven";
|
|
1659
|
+
type Area = "direction" | "type" | "colour" | "shape" | "effects" | "motion" | "layout" | "copy";
|
|
1660
|
+
interface SignalResult {
|
|
1661
|
+
id: "tells" | "typicality" | "estate" | "reasons";
|
|
1662
|
+
/** null when it was not measured. */
|
|
1663
|
+
raised: boolean | null;
|
|
1664
|
+
detail: string;
|
|
1665
|
+
}
|
|
1666
|
+
interface Action {
|
|
1667
|
+
area: Area;
|
|
1668
|
+
what: string;
|
|
1669
|
+
why: string;
|
|
1670
|
+
/** 1 first. Deciding comes before changing, so direction actions are always 1. */
|
|
1671
|
+
priority: 1 | 2 | 3;
|
|
1672
|
+
/** The art-direction choice that settles it, when there is one. */
|
|
1673
|
+
choice?: ChoiceKey;
|
|
1674
|
+
}
|
|
1675
|
+
interface CharacterReport {
|
|
1676
|
+
site: string;
|
|
1677
|
+
verdict: Verdict;
|
|
1678
|
+
summary: string;
|
|
1679
|
+
signals: SignalResult[];
|
|
1680
|
+
actions: Action[];
|
|
1681
|
+
}
|
|
1682
|
+
interface CharacterInputs {
|
|
1683
|
+
site: string;
|
|
1684
|
+
/** Source and rendered findings, merged. */
|
|
1685
|
+
findings?: CheckReport | null;
|
|
1686
|
+
typicality?: Typicality | null;
|
|
1687
|
+
estate?: EstateMatch[] | null;
|
|
1688
|
+
direction?: DirectionReport | null;
|
|
1689
|
+
}
|
|
1690
|
+
declare function characterReport(input: CharacterInputs, copyTells?: Set<string>): CharacterReport;
|
|
1691
|
+
|
|
1692
|
+
/**
|
|
1693
|
+
* `craft retrofit`: a character report turned into the checklist a retrofit
|
|
1694
|
+
* session works through, in the order that works: decide, then change.
|
|
1695
|
+
*
|
|
1696
|
+
* Every change is tied to the art-direction choice that settles it, and shows
|
|
1697
|
+
* the decided value and its reason when there is one. A change with no
|
|
1698
|
+
* decided value waits for the decision; otherwise the retrofit swaps one
|
|
1699
|
+
* default for the next.
|
|
1700
|
+
*
|
|
1701
|
+
* Pure. Returns Markdown.
|
|
1702
|
+
*/
|
|
1703
|
+
|
|
1704
|
+
declare function retrofitPlan(report: CharacterReport, direction?: ArtDirection | null): string;
|
|
1705
|
+
|
|
1706
|
+
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 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 Finding, type Fingerprint, type FingerprintDistance, type FluidSpaceOptions, type FluidTypeOptions, type FluidTypeResult, type FontClass, type Gamut, type Generation, HOUSE_BUDGET, HOUSE_TYPE, type HarvestCandidate, type HarvestOptions, type Hit, 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, auditSnapshot, catalogueTable, characterReport, checkCopy, checkPair, checkRestraint, choicesOf, compareToEstate, craftTokens, deltaEOk, deltaEOkHex, densityCss, densityTokens, emitCss, emptyEstate, estatePairs, exitDuration, extractHtml, findColours, fingerprint, fingerprintDistance, fluidClamp, fluidSpace, fluidType, fontClass, formatHex, formatReport, groundName, harvest, hexToOklch, hueName, hueSwatch, inP3Gamut, inSrgbGamut, initDirection, isAiViolet, isCream, makeSnapshot, motionTokens, nearest, normaliseFamily, nullPrompt, oklchToHex, oklchToRgb, paletteFromPixels, parseColour, parseHex, propose, ramp, rampFromAnchors, retrofitPlan, runTell, scanSource, sectionRhythm, semantic, shapeName, sharedParts, shouldAnimate, siblingLine, tailwindV3Preset, tailwindV4Theme, tellById, toGamut, typeFeatureTokens, typicality, validateDirection, wcagContrast, wordListsJson };
|