@bridge-ds/core 0.12.0 → 0.15.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.
@@ -63,11 +63,18 @@ export interface ApprovalSummary {
63
63
  design: boolean;
64
64
  tech: boolean;
65
65
  }
66
+ export interface ApprovalRules {
67
+ /**
68
+ * `governance.soloOwnerCanApproveBoth`: when the owners resolve to one person holding both roles, that person's
69
+ * approval counts for design and tech. Default false. The author still never approves their own change.
70
+ */
71
+ soloOwnerCanApproveBoth?: boolean | undefined;
72
+ }
66
73
  /** Latest decisive review per person (comments do not reset an approval), then owner coverage. */
67
74
  export declare function summarizeApprovals(reviews: ReviewLike[], owners: {
68
75
  design: string[];
69
76
  tech: string[];
70
- }, author?: string): ApprovalSummary;
77
+ }, author?: string, rules?: ApprovalRules): ApprovalSummary;
71
78
  export type MissingApproval = 'design approval' | 'tech approval' | 'approval';
72
79
  export interface MergeGate {
73
80
  allowed: boolean;
package/dist/rights.d.ts CHANGED
@@ -27,6 +27,26 @@ export declare function holdersOf(right: Right, owners: Owners, rights: Rights |
27
27
  export declare function approverLists(owners: Owners, rights: Rights | null | undefined, teams?: TeamStates | null): Owners;
28
28
  /** approverLists before team expansion: the entries as the config names them, for sentences. */
29
29
  export declare function approverEntries(owners: Owners, rights: Rights | null | undefined): Owners;
30
+ /**
31
+ * The one person who holds both approver places, or null: the design and tech lists (after rights, teams expanded)
32
+ * name exactly one login between them, and it sits in both. A team, read or not, never counts as one person.
33
+ */
34
+ export declare function soloApprover(owners: {
35
+ design: string[];
36
+ tech: string[];
37
+ }): string | null;
38
+ /**
39
+ * The only owner whose approval covers design and tech, or null when the rule is off or does not apply:
40
+ * `governance.soloOwnerCanApproveBoth` is true and the approver lists (after rights, teams expanded) name one person
41
+ * in both places. Studio Settings, the review screens, the MCP server and the CLI word it from this.
42
+ */
43
+ export declare function soloOwnerOf(gov: {
44
+ owners: Owners;
45
+ rights?: Rights | null | undefined;
46
+ soloOwnerCanApproveBoth?: boolean | undefined;
47
+ }, teams?: TeamStates | null): string | null;
48
+ /** The rule in one sentence, for Settings, the CLI and the MCP server. `you`: the viewer is that owner. */
49
+ export declare function soloOwnerSentence(login: string, you?: boolean): string;
30
50
  /** Who may ship, or null when nobody is singled out (no `rights`, or its groups list no logins): anyone with write access. */
31
51
  export interface ShipPolicy {
32
52
  /** Logins allowed to ship: people, and team members once read. A team not read keeps its "@org/slug" key. */
@@ -0,0 +1,132 @@
1
+ import type { AuditMetrics, AuditReport } from './audit/types';
2
+ import type { ContrastPairResult } from './contrast-model';
3
+ import type { SpecCoverage } from './component-spec';
4
+ import { type RepoSource } from './repo-source';
5
+ import type { RepoModel } from './repo-model';
6
+ export declare const SNAPSHOT_SCHEMA_VERSION = 1;
7
+ /** Branch the bridge-data workflow publishes to, and the files on it. */
8
+ export declare const SNAPSHOT_BRANCH = "bridge-data";
9
+ export declare const SNAPSHOT_FILE = "snapshot.json";
10
+ export declare const SNAPSHOT_AUDITS_DIR = "audits";
11
+ /** Above this, a snapshot is refused (a published file the browser downloads whole). */
12
+ export declare const SNAPSHOT_MAX_BYTES = 5000000;
13
+ /** One adoption audit: the summary lives in snapshot.json, the full report in audits/<id>.json next to it. */
14
+ export interface SnapshotAudit {
15
+ id: string;
16
+ /** Path of the full report on the bridge-data branch ("audits/admin.json"). */
17
+ path: string;
18
+ label?: string;
19
+ generatedAt: string;
20
+ target: {
21
+ path: string;
22
+ files: number;
23
+ };
24
+ metrics: AuditMetrics;
25
+ }
26
+ /** A contrast pair as computed when the snapshot was written, without the token rows (the model has them). */
27
+ export type SnapshotContrastPair = Omit<ContrastPairResult, 'fgToken' | 'bgToken'>;
28
+ /** Counts for a header or a card, readable without loading the model. */
29
+ export interface SnapshotSummary {
30
+ tokens: number;
31
+ /** Null when the component scan was skipped. */
32
+ components: number | null;
33
+ contrast: {
34
+ pairs: number;
35
+ checks: number;
36
+ failing: number;
37
+ };
38
+ /** Components in Figma (generated or drafted) out of the visual ones, null without a specs file. */
39
+ figma: {
40
+ inFigma: number;
41
+ visual: number;
42
+ } | null;
43
+ }
44
+ export interface RepoSnapshot {
45
+ kind: 'bridge-snapshot';
46
+ schemaVersion: typeof SNAPSHOT_SCHEMA_VERSION;
47
+ tool: {
48
+ name: 'bridge';
49
+ version: string;
50
+ };
51
+ generatedAt: string;
52
+ repo: {
53
+ owner: string;
54
+ name: string;
55
+ branch: string;
56
+ };
57
+ /** Commit the snapshot was computed from. Fresh when it equals the default branch head. */
58
+ sourceSha: string;
59
+ /** Commit date of sourceSha (ISO), when known. */
60
+ committedAt?: string;
61
+ /** Every path the model listed (token root, components dir), so list() answers as the repository did. */
62
+ paths: string[];
63
+ /** Bodies of the files the model read, keyed by repo-relative path. */
64
+ files: Record<string, string>;
65
+ /**
66
+ * Figma coverage of component-specs.json without the specs themselves (the file is over 1 MB here, too big for
67
+ * the GitHub contents source). Null when the repository has no specs file.
68
+ */
69
+ figma: {
70
+ path: string;
71
+ generated?: string;
72
+ coverage: SpecCoverage[];
73
+ } | null;
74
+ summary: SnapshotSummary;
75
+ /** Contrast results as computed when the snapshot was written (the model recomputes the same from files). */
76
+ contrast: {
77
+ path?: string;
78
+ pairs: SnapshotContrastPair[];
79
+ };
80
+ audits: SnapshotAudit[];
81
+ /** What could not be read when the snapshot was written. */
82
+ problems: string[];
83
+ }
84
+ export declare class SnapshotError extends Error {
85
+ }
86
+ /** Validates a snapshot (text or parsed JSON). Throws SnapshotError naming the first problem. */
87
+ export declare function parseSnapshot(input: string | unknown): RepoSnapshot;
88
+ /** True when the snapshot was computed from `headSha` (the default branch head): use it instead of a live read. */
89
+ export declare const snapshotIsFresh: (snapshot: Pick<RepoSnapshot, "sourceSha">, headSha: string | undefined) => boolean;
90
+ /**
91
+ * A RepoSource over a snapshot. The Figma specs path serves a compact file with the coverage only (no component
92
+ * specs), so the model reads the coverage and leaves `specs.file` null.
93
+ */
94
+ export declare function snapshotSource(snapshot: RepoSnapshot): RepoSource;
95
+ /**
96
+ * Wraps a source and records what it lists and reads, so a snapshot carries exactly the files the model needs and
97
+ * nothing else of the repository.
98
+ */
99
+ export declare function recordingSource(source: RepoSource): {
100
+ source: RepoSource;
101
+ paths: Set<string>;
102
+ files: Map<string, string>;
103
+ };
104
+ /** Summary of an audit report for snapshot.json; the full report is written to `path`. */
105
+ export declare function snapshotAudit(id: string, report: AuditReport): SnapshotAudit;
106
+ /** A safe audit id from a label or path ("apps/admin/src" -> "apps-admin-src"). */
107
+ export declare const auditId: (s: string) => string;
108
+ export interface BuildSnapshotInput {
109
+ model: RepoModel;
110
+ /** From recordingSource(): what loadRepoModel listed and read. */
111
+ paths: Iterable<string>;
112
+ files: Map<string, string> | Record<string, string>;
113
+ version: string;
114
+ branch?: string;
115
+ committedAt?: string;
116
+ generatedAt?: string;
117
+ audits?: SnapshotAudit[];
118
+ }
119
+ /** Counts of a model for the snapshot summary. */
120
+ export declare function snapshotSummary(model: RepoModel): SnapshotSummary;
121
+ /** Assembles the snapshot from a model and the files it was read from. The specs file becomes the coverage list. */
122
+ export declare function buildSnapshot(input: BuildSnapshotInput): RepoSnapshot;
123
+ /** llms.txt next to snapshot.json on the bridge-data branch: the published design system in plain text, for agents. */
124
+ export declare const SNAPSHOT_LLMS_FILE = "llms.txt";
125
+ /** Above this many tokens, llms.txt lists the first ones and points at snapshot.json for the rest. */
126
+ export declare const LLMS_MAX_TOKENS = 600;
127
+ /**
128
+ * The design system of a snapshot as llms.txt (https://llmstxt.org): what it is, every token with its CSS variable and
129
+ * values per theme, and every component with its usage guidance. Written by `bridge snapshot`; the hosted Library
130
+ * links to it. Pure: the model the snapshot was computed from goes in, text comes out.
131
+ */
132
+ export declare function snapshotLlmsTxt(model: RepoModel, snapshot: Pick<RepoSnapshot, 'repo' | 'sourceSha' | 'committedAt' | 'generatedAt'>): string;
@@ -49,6 +49,8 @@ export interface SuggestionGovernance {
49
49
  rights?: Rights | null | undefined;
50
50
  /** Team members once read (teams.ts). Undefined: owner entries are used as listed. */
51
51
  teams?: TeamStates | null | undefined;
52
+ /** `governance.soloOwnerCanApproveBoth`: the only owner's approval covers design and tech (review-gate summarizeApprovals). */
53
+ soloOwnerCanApproveBoth?: boolean | undefined;
52
54
  }
53
55
  export interface SuggestionApprovals extends ApprovalSummary {
54
56
  /** People whose latest review is on an older payload: it no longer counts. */
@@ -60,7 +62,8 @@ export interface SuggestionApprovals extends ApprovalSummary {
60
62
  * Approvals on a suggestion, as summarizeApprovals counts them on a pull request, from the review comments: only
61
63
  * comments by people in the approver lists (governance owners and rights) count; with no owner configured at all, only
62
64
  * repository owners, members and collaborators (GitHub's author association). Only reviews of the current payload
63
- * sha count; the suggestion's author never approves their own suggestion.
65
+ * sha count; the suggestion's author never approves their own suggestion. With `soloOwnerCanApproveBoth` and one
66
+ * owner holding both roles, that owner's approval covers design and tech.
64
67
  */
65
68
  export declare function summarizeSuggestionApprovals(comments: readonly SuggestionCommentLike[], gov: SuggestionGovernance, opts: {
66
69
  payloadSha: string;
@@ -0,0 +1,122 @@
1
+ import { type BridgeConfig } from './config';
2
+ /** Token tiers of the rows, in resolution order: primitive <- brand <- semantic <- component. Density sits beside semantic. */
3
+ export type TokenRowTier = 'primitive' | 'brand' | 'semantic' | 'density' | 'component';
4
+ type Json = Record<string, unknown>;
5
+ export interface RawTokenEntry {
6
+ path: string[];
7
+ type: string;
8
+ value: unknown;
9
+ description?: string;
10
+ /** DTCG $deprecated: true, or a string explaining what to use instead. */
11
+ deprecated?: boolean | string;
12
+ /** Repo-relative file the token is declared in. */
13
+ file: string;
14
+ }
15
+ export interface PrimitiveRow {
16
+ path: string;
17
+ segments: string[];
18
+ type: string;
19
+ /** Resolved CSS value. */
20
+ css: string;
21
+ hex?: string;
22
+ cssVar: string;
23
+ figma: string;
24
+ description?: string;
25
+ deprecated?: boolean | string;
26
+ raw: unknown;
27
+ file: string;
28
+ }
29
+ export interface TokenRowMode {
30
+ /** First alias target, or null when the value is literal. */
31
+ alias: string | null;
32
+ /** Every alias hop, from the token to the final primitive (palette roles included). */
33
+ chain: string[];
34
+ css: string;
35
+ hex?: string;
36
+ }
37
+ export interface TokenRow {
38
+ path: string;
39
+ segments: string[];
40
+ type: string;
41
+ cssVar: string;
42
+ figma: string;
43
+ description?: string;
44
+ /** e.g. "color.action" or "elevation". */
45
+ group: string;
46
+ deprecated?: boolean | string;
47
+ tier: Exclude<TokenRowTier, 'primitive'>;
48
+ /** Source file(s) the token is declared in, repo-relative. */
49
+ file: string;
50
+ /** Values per theme in the default brand and default density. */
51
+ modes: Record<string, TokenRowMode>;
52
+ /** Values per brand x theme (default density). */
53
+ brands: Record<string, Record<string, TokenRowMode>>;
54
+ /** Density tokens only: value per density. */
55
+ densities?: Record<string, TokenRowMode>;
56
+ /** True when the brand x theme values differ from the default brand. */
57
+ brandSpecific: boolean;
58
+ }
59
+ /** Brand, theme and density lists the rows are computed for, with their defaults and display names. */
60
+ export interface TokenRowAxes {
61
+ brands: string[];
62
+ defaultBrand: string;
63
+ /** Display name per brand: the name a brand file records ($extensions dev.bridge.brand.name), else the slug in title case. */
64
+ brandLabels: Record<string, string>;
65
+ themes: string[];
66
+ densities: string[];
67
+ defaultDensity: string;
68
+ }
69
+ /** Repo-relative token files the rows read, from the tier folders of the config. */
70
+ export interface TokenRowFiles {
71
+ primitivesDir: string;
72
+ semanticDir: string;
73
+ brandsDir: string;
74
+ densityDir: string;
75
+ componentsDir: string;
76
+ baseFile: string;
77
+ semanticFile(theme: string): string;
78
+ brandFile(brand: string): string;
79
+ overrideFile(brand: string, theme: string): string;
80
+ densityFile(density: string): string;
81
+ }
82
+ export interface TokenRows extends TokenRowAxes {
83
+ primitives: PrimitiveRow[];
84
+ /** Semantic tier: themed (one file per theme) and theme-independent (base). */
85
+ semantic: TokenRow[];
86
+ /** Brand tier: palette, radius and font roles per brand. */
87
+ brand: TokenRow[];
88
+ density: TokenRow[];
89
+ component: TokenRow[];
90
+ /** Every non-primitive token: brand, semantic, density, component. */
91
+ all: TokenRow[];
92
+ /** Repo-relative paths of every token source file, in tier order (what the "last updated" line watches). */
93
+ sources: string[];
94
+ }
95
+ /** Brand, theme and density lists from bridge.config.json, as the docs pages iterate them. */
96
+ export declare function tokenRowAxes(config: BridgeConfig, readText?: (path: string) => string | undefined): TokenRowAxes;
97
+ /** The token files of each tier, repo-relative, from the config's tokens.root and tier folders. */
98
+ export declare function tokenRowFiles(config: BridgeConfig): TokenRowFiles;
99
+ /** Flattens one DTCG document into raw tokens, inheriting $type from groups. */
100
+ export declare function flattenTokenFile(node: Json, file: string, path?: string[], inheritedType?: string): RawTokenEntry[];
101
+ /** CSS text (and hex for colors) of a literal DTCG value. */
102
+ export declare function tokenValueCss(type: string, value: unknown): {
103
+ css: string;
104
+ hex?: string;
105
+ };
106
+ /** CSS variable of a token path ("color.bg.default" segments -> "--color-bg-default"). */
107
+ export declare const tokenRowCssVar: (segments: string[]) => string;
108
+ /** Figma variable name of a token path. */
109
+ export declare const tokenRowFigmaName: (segments: string[]) => string;
110
+ /**
111
+ * Builds the rows from the token files of a repo. `readText` returns a repo-relative file body (undefined when
112
+ * missing); `listFiles` returns the repo-relative files directly inside a directory. Throws on an alias that does not
113
+ * resolve and on a themed token missing from another theme, as the token build does.
114
+ */
115
+ export declare function buildTokenRows(config: BridgeConfig, readText: (path: string) => string | undefined, listFiles: (dir: string) => string[]): TokenRows;
116
+ /** Every CSS variable the rows define: cssVar -> token path, tier and type. */
117
+ export declare function tokenRowIndex(rows: Pick<TokenRows, 'primitives' | 'all'>): Map<string, {
118
+ path: string;
119
+ kind: TokenRowTier;
120
+ type: string;
121
+ }>;
122
+ export {};
@@ -12,5 +12,8 @@ export declare function productPreview(apps: PreviewApps, ctx: {
12
12
  sha: string;
13
13
  fromFork?: boolean;
14
14
  }, sources: BranchPreviewSources): string | null;
15
- /** The preview with the toolbar on, at that screen (shop and theme in the query, the hash route ra-core reads), else home. */
15
+ /**
16
+ * The preview with the toolbar on, at that screen (shop and theme in the query, the hash route ra-core reads), else home.
17
+ * Only http(s) previews: anything else (a javascript: or data: URL in a hostile comment) gives ''.
18
+ */
16
19
  export declare function tryInProductUrl(preview: string, pr: number, screen: ProductScreen | null): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge-ds/core",
3
- "version": "0.12.0",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "description": "Bridge core: bridge.config.json, DTCG token loading (tiers and layered modes), alias resolution, WCAG contrast, diffs, minimal-diff edits, PR body, the shared risk tier classifier, changesets and a fetch-based GitHub client. No Figma API, no DOM; Node-only helpers (git, reading a product folder) under @bridge-ds/core/node; the adoption audit under @bridge-ds/core/audit.",
6
6
  "license": "MIT",