@brandlab-digital/conformance 2.1.0 → 3.1.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/analytics-e2e.d.ts +52 -0
- package/dist/analytics-e2e.d.ts.map +1 -0
- package/dist/analytics-e2e.js +189 -0
- package/dist/analytics-e2e.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +40 -3
- package/dist/cli.js.map +1 -1
- package/dist/contact-e2e.d.ts +43 -0
- package/dist/contact-e2e.d.ts.map +1 -0
- package/dist/contact-e2e.js +193 -0
- package/dist/contact-e2e.js.map +1 -0
- package/dist/current-release.js +7 -7
- package/dist/current-release.js.map +1 -1
- package/dist/e2e-support.d.ts +42 -0
- package/dist/e2e-support.d.ts.map +1 -0
- package/dist/e2e-support.js +126 -0
- package/dist/e2e-support.js.map +1 -0
- package/dist/engineering.d.ts +14 -0
- package/dist/engineering.d.ts.map +1 -1
- package/dist/engineering.js +446 -50
- package/dist/engineering.js.map +1 -1
- package/dist/estate.d.ts +105 -0
- package/dist/estate.d.ts.map +1 -0
- package/dist/estate.js +391 -0
- package/dist/estate.js.map +1 -0
- package/dist/families-e2e.d.ts +45 -0
- package/dist/families-e2e.d.ts.map +1 -0
- package/dist/families-e2e.js +248 -0
- package/dist/families-e2e.js.map +1 -0
- package/dist/glob.d.ts +21 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +138 -0
- package/dist/glob.js.map +1 -0
- package/dist/knip.d.ts +47 -0
- package/dist/knip.d.ts.map +1 -0
- package/dist/knip.js +84 -0
- package/dist/knip.js.map +1 -0
- package/dist/project.d.ts +4 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +163 -147
- package/dist/project.js.map +1 -1
- package/dist/public-copy.d.ts +36 -0
- package/dist/public-copy.d.ts.map +1 -0
- package/dist/public-copy.js +517 -0
- package/dist/public-copy.js.map +1 -0
- package/dist/rule-registry.d.ts +292 -0
- package/dist/rule-registry.d.ts.map +1 -0
- package/dist/rule-registry.js +68 -0
- package/dist/rule-registry.js.map +1 -0
- package/dist/search-certification.d.ts +4 -0
- package/dist/search-certification.d.ts.map +1 -1
- package/dist/search-certification.js +17 -3
- package/dist/search-certification.js.map +1 -1
- package/dist/source-files.d.ts +54 -0
- package/dist/source-files.d.ts.map +1 -0
- package/dist/source-files.js +323 -0
- package/dist/source-files.js.map +1 -0
- package/dist/static-rules.d.ts +45 -0
- package/dist/static-rules.d.ts.map +1 -0
- package/dist/static-rules.js +762 -0
- package/dist/static-rules.js.map +1 -0
- package/dist/verification-policy.d.ts +13 -4
- package/dist/verification-policy.d.ts.map +1 -1
- package/dist/verification-policy.js +31 -12
- package/dist/verification-policy.js.map +1 -1
- package/package.json +37 -1
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import type { BrandlabVerificationCheckId } from "./verification-policy.js";
|
|
2
|
+
/**
|
|
3
|
+
* Every static rule the evaluator can run, the check it evaluates and what it
|
|
4
|
+
* proves. `<placeholder>` segments stand for the concrete package, capability,
|
|
5
|
+
* key or state named in each finding. The generated rule reference is built
|
|
6
|
+
* from this list, and tests keep it identical to the evaluator.
|
|
7
|
+
*/
|
|
8
|
+
export interface BrandlabStaticRule {
|
|
9
|
+
ruleId: string;
|
|
10
|
+
surface: string;
|
|
11
|
+
checkId: BrandlabVerificationCheckId;
|
|
12
|
+
summary: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const BRANDLAB_STATIC_RULES: readonly [{
|
|
15
|
+
readonly ruleId: "brandlab-release-package-json-missing";
|
|
16
|
+
readonly surface: "release";
|
|
17
|
+
readonly checkId: "release-consistency";
|
|
18
|
+
readonly summary: "package.json is present and readable.";
|
|
19
|
+
}, {
|
|
20
|
+
readonly ruleId: "brandlab-release-installed-version-unresolved";
|
|
21
|
+
readonly surface: "release";
|
|
22
|
+
readonly checkId: "release-consistency";
|
|
23
|
+
readonly summary: "All four Brandlab packages are pinned to one exact release.";
|
|
24
|
+
}, {
|
|
25
|
+
readonly ruleId: "brandlab-release-unsupported";
|
|
26
|
+
readonly surface: "release";
|
|
27
|
+
readonly checkId: "release-consistency";
|
|
28
|
+
readonly summary: "The installed release is the one this checker certifies.";
|
|
29
|
+
}, {
|
|
30
|
+
readonly ruleId: "brandlab-release-declared-installed-mismatch";
|
|
31
|
+
readonly surface: "release";
|
|
32
|
+
readonly checkId: "release-consistency";
|
|
33
|
+
readonly summary: "The declared and detected releases agree.";
|
|
34
|
+
}, {
|
|
35
|
+
readonly ruleId: "brandlab-release-package-lock-<state>";
|
|
36
|
+
readonly surface: "release";
|
|
37
|
+
readonly checkId: "release-consistency";
|
|
38
|
+
readonly summary: "A valid npm lockfile v2 or v3 is committed.";
|
|
39
|
+
}, {
|
|
40
|
+
readonly ruleId: "brandlab-release-package-pin-mismatch";
|
|
41
|
+
readonly surface: "release";
|
|
42
|
+
readonly checkId: "release-consistency";
|
|
43
|
+
readonly summary: "Each package is declared once and locked at the exact release.";
|
|
44
|
+
}, {
|
|
45
|
+
readonly ruleId: "brandlab-release-package-resolution-mismatch";
|
|
46
|
+
readonly surface: "release";
|
|
47
|
+
readonly checkId: "release-consistency";
|
|
48
|
+
readonly summary: "The lockfile resolves each package from the canonical npm registry with integrity.";
|
|
49
|
+
}, {
|
|
50
|
+
readonly ruleId: "brandlab-release-package-integrity-mismatch";
|
|
51
|
+
readonly surface: "release";
|
|
52
|
+
readonly checkId: "release-consistency";
|
|
53
|
+
readonly summary: "With --release-manifest, lockfile integrity equals the immutable release record.";
|
|
54
|
+
}, {
|
|
55
|
+
readonly ruleId: "brandlab-runtime-<package>";
|
|
56
|
+
readonly surface: "runtime";
|
|
57
|
+
readonly checkId: "runtime-supported";
|
|
58
|
+
readonly summary: "next, react, react-dom and typescript use versions the release permits.";
|
|
59
|
+
}, {
|
|
60
|
+
readonly ruleId: "brandlab-runtime-react-pair";
|
|
61
|
+
readonly surface: "runtime";
|
|
62
|
+
readonly checkId: "runtime-supported";
|
|
63
|
+
readonly summary: "react and react-dom are declared and locked at the same version.";
|
|
64
|
+
}, {
|
|
65
|
+
readonly ruleId: "brandlab-runtime-node";
|
|
66
|
+
readonly surface: "runtime";
|
|
67
|
+
readonly checkId: "runtime-supported";
|
|
68
|
+
readonly summary: "engines.node matches the release in package.json and the lockfile.";
|
|
69
|
+
}, {
|
|
70
|
+
readonly ruleId: "brandlab-runtime-package-manager";
|
|
71
|
+
readonly surface: "runtime";
|
|
72
|
+
readonly checkId: "runtime-supported";
|
|
73
|
+
readonly summary: "packageManager is the release's npm version.";
|
|
74
|
+
}, {
|
|
75
|
+
readonly ruleId: "brandlab-runtime-sharp";
|
|
76
|
+
readonly surface: "runtime";
|
|
77
|
+
readonly checkId: "runtime-supported";
|
|
78
|
+
readonly summary: "Sharp is pinned only by overrides.sharp with one lockfile resolution.";
|
|
79
|
+
}, {
|
|
80
|
+
readonly ruleId: "brandlab-runtime-image-initialisation";
|
|
81
|
+
readonly surface: "runtime";
|
|
82
|
+
readonly checkId: "runtime-supported";
|
|
83
|
+
readonly summary: "next.config imports the shared image-runtime guard first.";
|
|
84
|
+
}, {
|
|
85
|
+
readonly ruleId: "brandlab-runtime-next-config";
|
|
86
|
+
readonly surface: "runtime";
|
|
87
|
+
readonly checkId: "runtime-supported";
|
|
88
|
+
readonly summary: "next.config exports defineBrandlabNextConfig, which owns headers, image formats and Preview noindex.";
|
|
89
|
+
}, {
|
|
90
|
+
readonly ruleId: "brandlab-environment-<capability>-<key>";
|
|
91
|
+
readonly surface: "environment";
|
|
92
|
+
readonly checkId: "runtime-supported";
|
|
93
|
+
readonly summary: ".env.example documents each variable an enabled capability reads, including the contract's recipient key.";
|
|
94
|
+
}, {
|
|
95
|
+
readonly ruleId: "brandlab-environment-retired-contact-state";
|
|
96
|
+
readonly surface: "environment";
|
|
97
|
+
readonly checkId: "runtime-supported";
|
|
98
|
+
readonly summary: ".env.example lists no retired contact state variables.";
|
|
99
|
+
}, {
|
|
100
|
+
readonly ruleId: "brandlab-source-dangerously-set-inner-html";
|
|
101
|
+
readonly surface: "security";
|
|
102
|
+
readonly checkId: "runtime-supported";
|
|
103
|
+
readonly summary: "No project-owned dangerouslySetInnerHTML; only SchemaScript and SafeRichText inject markup.";
|
|
104
|
+
}, {
|
|
105
|
+
readonly ruleId: "brandlab-source-public-secret";
|
|
106
|
+
readonly surface: "security";
|
|
107
|
+
readonly checkId: "runtime-supported";
|
|
108
|
+
readonly summary: "No API key, secret or token behind a NEXT_PUBLIC_ name.";
|
|
109
|
+
}, {
|
|
110
|
+
readonly ruleId: "brandlab-config-missing";
|
|
111
|
+
readonly surface: "routes";
|
|
112
|
+
readonly checkId: "route-identity";
|
|
113
|
+
readonly summary: "brandlab.config.ts exists.";
|
|
114
|
+
}, {
|
|
115
|
+
readonly ruleId: "brandlab-config-invalid";
|
|
116
|
+
readonly surface: "routes";
|
|
117
|
+
readonly checkId: "route-identity";
|
|
118
|
+
readonly summary: "brandlab.config.ts is a literal defineBrandlabProject with projectId.";
|
|
119
|
+
}, {
|
|
120
|
+
readonly ruleId: "brandlab-routes-catalogue";
|
|
121
|
+
readonly surface: "routes";
|
|
122
|
+
readonly checkId: "route-identity";
|
|
123
|
+
readonly summary: "Exactly one module declares the route catalogue with defineRouteCatalog.";
|
|
124
|
+
}, {
|
|
125
|
+
readonly ruleId: "brandlab-routes-location-estate";
|
|
126
|
+
readonly surface: "routes";
|
|
127
|
+
readonly checkId: "route-identity";
|
|
128
|
+
readonly summary: "A project that publishes locations declares them in src/data/locations/estate.ts with defineLocationEstate.";
|
|
129
|
+
}, {
|
|
130
|
+
readonly ruleId: "brandlab-redirects-table";
|
|
131
|
+
readonly surface: "routes";
|
|
132
|
+
readonly checkId: "route-identity";
|
|
133
|
+
readonly summary: "Exactly one module declares the redirect table with defineRedirects.";
|
|
134
|
+
}, {
|
|
135
|
+
readonly ruleId: "brandlab-contact-recipient-env";
|
|
136
|
+
readonly surface: "contact";
|
|
137
|
+
readonly checkId: "contact-runtime";
|
|
138
|
+
readonly summary: "The contact contract declares delivery.recipientEnv as a literal key.";
|
|
139
|
+
}, {
|
|
140
|
+
readonly ruleId: "brandlab-source-local-contact-delivery";
|
|
141
|
+
readonly surface: "contact";
|
|
142
|
+
readonly checkId: "contact-runtime";
|
|
143
|
+
readonly summary: "Email delivery is left to the shared contact runtime.";
|
|
144
|
+
}, {
|
|
145
|
+
readonly ruleId: "brandlab-source-local-contact-email-renderer";
|
|
146
|
+
readonly surface: "contact";
|
|
147
|
+
readonly checkId: "contact-runtime";
|
|
148
|
+
readonly summary: "Contact email rendering is left to the shared contact runtime.";
|
|
149
|
+
}, {
|
|
150
|
+
readonly ruleId: "brandlab-source-local-contact-attachments";
|
|
151
|
+
readonly surface: "contact";
|
|
152
|
+
readonly checkId: "contact-runtime";
|
|
153
|
+
readonly summary: "Attachment preparation is left to the shared contact runtime.";
|
|
154
|
+
}, {
|
|
155
|
+
readonly ruleId: "brandlab-source-contact-draft-persistence";
|
|
156
|
+
readonly surface: "contact";
|
|
157
|
+
readonly checkId: "contact-runtime";
|
|
158
|
+
readonly summary: "Contact details are never persisted in browser storage.";
|
|
159
|
+
}, {
|
|
160
|
+
readonly ruleId: "brandlab-source-retired-contact-state";
|
|
161
|
+
readonly surface: "contact";
|
|
162
|
+
readonly checkId: "contact-runtime";
|
|
163
|
+
readonly summary: "No retired contact state service variables in source.";
|
|
164
|
+
}, {
|
|
165
|
+
readonly ruleId: "brandlab-contact-provider-route";
|
|
166
|
+
readonly surface: "contact";
|
|
167
|
+
readonly checkId: "contact-provider-errors";
|
|
168
|
+
readonly summary: "POST /api/contact is created by the shared handler, so provider errors are handled by the runtime.";
|
|
169
|
+
}, {
|
|
170
|
+
readonly ruleId: "brandlab-analytics-pii-payload";
|
|
171
|
+
readonly surface: "analytics";
|
|
172
|
+
readonly checkId: "analytics-privacy";
|
|
173
|
+
readonly summary: "No lead fields, messages or attachment details in analytics payloads, and no project-set user_data.";
|
|
174
|
+
}, {
|
|
175
|
+
readonly ruleId: "brandlab-source-retired-analytics-event";
|
|
176
|
+
readonly surface: "analytics";
|
|
177
|
+
readonly checkId: "analytics-conversion";
|
|
178
|
+
readonly summary: "Only current event names are sent: contact_form for accepted enquiries and phone_link_click as a tap diagnostic.";
|
|
179
|
+
}, {
|
|
180
|
+
readonly ruleId: "brandlab-source-local-google-tag-bootstrap";
|
|
181
|
+
readonly surface: "analytics";
|
|
182
|
+
readonly checkId: "analytics-conversion";
|
|
183
|
+
readonly summary: "The Google tag bootstrap is owned by the shared runtime.";
|
|
184
|
+
}, {
|
|
185
|
+
readonly ruleId: "brandlab-analytics-google-tag-owner";
|
|
186
|
+
readonly surface: "analytics";
|
|
187
|
+
readonly checkId: "analytics-conversion";
|
|
188
|
+
readonly summary: "Exactly one BrandlabGoogleTag, rendered by the root layout.";
|
|
189
|
+
}, {
|
|
190
|
+
readonly ruleId: "brandlab-analytics-contract";
|
|
191
|
+
readonly surface: "analytics";
|
|
192
|
+
readonly checkId: "analytics-conversion";
|
|
193
|
+
readonly summary: "Exactly one module declares the analytics binding with defineBrandlabAnalytics.";
|
|
194
|
+
}, {
|
|
195
|
+
readonly ruleId: "brandlab-analytics-instrumentation";
|
|
196
|
+
readonly surface: "analytics";
|
|
197
|
+
readonly checkId: "analytics-conversion";
|
|
198
|
+
readonly summary: "src/instrumentation-client.ts calls installBrandlabInstrumentation, which reports one phone_link_click per tap.";
|
|
199
|
+
}, {
|
|
200
|
+
readonly ruleId: "brandlab-analytics-retired-phone-api";
|
|
201
|
+
readonly surface: "analytics";
|
|
202
|
+
readonly checkId: "analytics-conversion";
|
|
203
|
+
readonly summary: "No trackPhoneCall; taps are reported once as phone_link_click.";
|
|
204
|
+
}, {
|
|
205
|
+
readonly ruleId: "brandlab-analytics-web-vitals";
|
|
206
|
+
readonly surface: "analytics";
|
|
207
|
+
readonly checkId: "analytics-conversion";
|
|
208
|
+
readonly summary: "No web-vitals reporter, web_vital event or webVitals flag.";
|
|
209
|
+
}, {
|
|
210
|
+
readonly ruleId: "brandlab-robots-package-route";
|
|
211
|
+
readonly surface: "robots";
|
|
212
|
+
readonly checkId: "search-routing";
|
|
213
|
+
readonly summary: "robots is a thin binding of @brandlab-digital/site-next/robots with no static public/robots.txt.";
|
|
214
|
+
}, {
|
|
215
|
+
readonly ruleId: "brandlab-sitemap-package-route";
|
|
216
|
+
readonly surface: "sitemap";
|
|
217
|
+
readonly checkId: "search-routing";
|
|
218
|
+
readonly summary: "The sitemap route uses the shared sitemap API over the route catalogue, with no static public/sitemap.xml.";
|
|
219
|
+
}, {
|
|
220
|
+
readonly ruleId: "brandlab-sitemap-split-mode";
|
|
221
|
+
readonly surface: "sitemap";
|
|
222
|
+
readonly checkId: "search-routing";
|
|
223
|
+
readonly summary: "/sitemaps/[chunk] exists exactly when brandlab.config sets sitemap.mode: 'split'.";
|
|
224
|
+
}, {
|
|
225
|
+
readonly ruleId: "brandlab-metadata-route-builder";
|
|
226
|
+
readonly surface: "metadata";
|
|
227
|
+
readonly checkId: "search-routing";
|
|
228
|
+
readonly summary: "Page metadata comes from createRouteMetadataBuilder(site, routes).";
|
|
229
|
+
}, {
|
|
230
|
+
readonly ruleId: "brandlab-site-identity";
|
|
231
|
+
readonly surface: "metadata";
|
|
232
|
+
readonly checkId: "search-routing";
|
|
233
|
+
readonly summary: "Exactly one module declares the site identity (canonical URL, locale, titles) with defineBrandlabSite.";
|
|
234
|
+
}, {
|
|
235
|
+
readonly ruleId: "brandlab-source-raw-json-ld";
|
|
236
|
+
readonly surface: "schema";
|
|
237
|
+
readonly checkId: "structured-data";
|
|
238
|
+
readonly summary: "JSON-LD is rendered only by the shared schema component.";
|
|
239
|
+
}, {
|
|
240
|
+
readonly ruleId: "brandlab-source-self-serving-rating-schema";
|
|
241
|
+
readonly surface: "schema";
|
|
242
|
+
readonly checkId: "structured-data";
|
|
243
|
+
readonly summary: "No self-serving AggregateRating schema.";
|
|
244
|
+
}, {
|
|
245
|
+
readonly ruleId: "brandlab-source-synthetic-offer-schema";
|
|
246
|
+
readonly surface: "schema";
|
|
247
|
+
readonly checkId: "structured-data";
|
|
248
|
+
readonly summary: "No invented Offer markup.";
|
|
249
|
+
}, {
|
|
250
|
+
readonly ruleId: "brandlab-source-local-google-reviews-fetch";
|
|
251
|
+
readonly surface: "reviews";
|
|
252
|
+
readonly checkId: "structured-data";
|
|
253
|
+
readonly summary: "Google Places reviews are fetched only by the shared review runtime.";
|
|
254
|
+
}, {
|
|
255
|
+
readonly ruleId: "brandlab-business-facts";
|
|
256
|
+
readonly surface: "business";
|
|
257
|
+
readonly checkId: "business-identity";
|
|
258
|
+
readonly summary: "Exactly one module declares the business facts with defineBrandlabBusiness.";
|
|
259
|
+
}, {
|
|
260
|
+
readonly ruleId: "brandlab-content-implementation-wording";
|
|
261
|
+
readonly surface: "content";
|
|
262
|
+
readonly checkId: "public-output-safety";
|
|
263
|
+
readonly summary: "Everything that renders publicly (pages, components, metadata, schema, alt text, MDX, Markdown and JSON content) carries finished copy, not template, helper, scaffold or placeholder wording.";
|
|
264
|
+
}, {
|
|
265
|
+
readonly ruleId: "brandlab-source-root-missing";
|
|
266
|
+
readonly surface: "source";
|
|
267
|
+
readonly checkId: "source-safety";
|
|
268
|
+
readonly summary: "Project source sits under a recognised root so the source rules can run.";
|
|
269
|
+
}, {
|
|
270
|
+
readonly ruleId: "brandlab-source-platform-reimplementation";
|
|
271
|
+
readonly surface: "source";
|
|
272
|
+
readonly checkId: "source-safety";
|
|
273
|
+
readonly summary: "Protected platform surfaces (SchemaScript, SafeRichText, contact, sitemap, analytics APIs) are imported, not redeclared.";
|
|
274
|
+
}, {
|
|
275
|
+
readonly ruleId: "brandlab-source-retired-api";
|
|
276
|
+
readonly surface: "source";
|
|
277
|
+
readonly checkId: "source-safety";
|
|
278
|
+
readonly summary: "No API, option or configuration key removed in 3.0 (TrackedTelephoneLink, shouldIndexSite, buildRobotsMetadata, buildSeoMetadata and the duplicate metadata builders, isBrandlabProductionDeployment, resolveBrowserAnalyticsConfig, resolveBrandlabGoogleTagConfig, the contact submission controller and its lead-tracking helpers (use useBrandlabContactForm), header profiles and isProduction, xRobotsTag, contentSecurityPolicyMode, leadDiagnostics, ALLOW_INDEXING, NEXT_PUBLIC_SITE_URL, profile, routes.aliases).";
|
|
279
|
+
}, {
|
|
280
|
+
readonly ruleId: "brandlab-ci-workflow-pinned";
|
|
281
|
+
readonly surface: "project-verification";
|
|
282
|
+
readonly checkId: "project-build";
|
|
283
|
+
readonly summary: "Every reused workflow and action is pinned to a 40-character commit SHA.";
|
|
284
|
+
}, {
|
|
285
|
+
readonly ruleId: "brandlab-deployment-vercelignore-lockfile";
|
|
286
|
+
readonly surface: "deployment";
|
|
287
|
+
readonly checkId: "deployment-binding";
|
|
288
|
+
readonly summary: ".vercelignore never excludes package.json or package-lock.json.";
|
|
289
|
+
}];
|
|
290
|
+
/** Whether a concrete rule ID (from a finding or evaluation) belongs to a registry entry. */
|
|
291
|
+
export declare function matchesBrandlabStaticRule(rule: BrandlabStaticRule, ruleId: string): boolean;
|
|
292
|
+
//# sourceMappingURL=rule-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule-registry.d.ts","sourceRoot":"","sources":["../src/rule-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAE5E;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,2BAA2B,CAAC;IACrC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwDgB,CAAC;AAEnD,6FAA6F;AAC7F,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,kBAAkB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAM3F"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const BRANDLAB_STATIC_RULES = [
|
|
2
|
+
{ ruleId: "brandlab-release-package-json-missing", surface: "release", checkId: "release-consistency", summary: "package.json is present and readable.", },
|
|
3
|
+
{ ruleId: "brandlab-release-installed-version-unresolved", surface: "release", checkId: "release-consistency", summary: "All four Brandlab packages are pinned to one exact release.", },
|
|
4
|
+
{ ruleId: "brandlab-release-unsupported", surface: "release", checkId: "release-consistency", summary: "The installed release is the one this checker certifies.", },
|
|
5
|
+
{ ruleId: "brandlab-release-declared-installed-mismatch", surface: "release", checkId: "release-consistency", summary: "The declared and detected releases agree.", },
|
|
6
|
+
{ ruleId: "brandlab-release-package-lock-<state>", surface: "release", checkId: "release-consistency", summary: "A valid npm lockfile v2 or v3 is committed.", },
|
|
7
|
+
{ ruleId: "brandlab-release-package-pin-mismatch", surface: "release", checkId: "release-consistency", summary: "Each package is declared once and locked at the exact release.", },
|
|
8
|
+
{ ruleId: "brandlab-release-package-resolution-mismatch", surface: "release", checkId: "release-consistency", summary: "The lockfile resolves each package from the canonical npm registry with integrity.", },
|
|
9
|
+
{ ruleId: "brandlab-release-package-integrity-mismatch", surface: "release", checkId: "release-consistency", summary: "With --release-manifest, lockfile integrity equals the immutable release record.", },
|
|
10
|
+
{ ruleId: "brandlab-runtime-<package>", surface: "runtime", checkId: "runtime-supported", summary: "next, react, react-dom and typescript use versions the release permits.", },
|
|
11
|
+
{ ruleId: "brandlab-runtime-react-pair", surface: "runtime", checkId: "runtime-supported", summary: "react and react-dom are declared and locked at the same version.", },
|
|
12
|
+
{ ruleId: "brandlab-runtime-node", surface: "runtime", checkId: "runtime-supported", summary: "engines.node matches the release in package.json and the lockfile.", },
|
|
13
|
+
{ ruleId: "brandlab-runtime-package-manager", surface: "runtime", checkId: "runtime-supported", summary: "packageManager is the release's npm version.", },
|
|
14
|
+
{ ruleId: "brandlab-runtime-sharp", surface: "runtime", checkId: "runtime-supported", summary: "Sharp is pinned only by overrides.sharp with one lockfile resolution.", },
|
|
15
|
+
{ ruleId: "brandlab-runtime-image-initialisation", surface: "runtime", checkId: "runtime-supported", summary: "next.config imports the shared image-runtime guard first.", },
|
|
16
|
+
{ ruleId: "brandlab-runtime-next-config", surface: "runtime", checkId: "runtime-supported", summary: "next.config exports defineBrandlabNextConfig, which owns headers, image formats and Preview noindex.", },
|
|
17
|
+
{ ruleId: "brandlab-environment-<capability>-<key>", surface: "environment", checkId: "runtime-supported", summary: ".env.example documents each variable an enabled capability reads, including the contract's recipient key.", },
|
|
18
|
+
{ ruleId: "brandlab-environment-retired-contact-state", surface: "environment", checkId: "runtime-supported", summary: ".env.example lists no retired contact state variables.", },
|
|
19
|
+
{ ruleId: "brandlab-source-dangerously-set-inner-html", surface: "security", checkId: "runtime-supported", summary: "No project-owned dangerouslySetInnerHTML; only SchemaScript and SafeRichText inject markup.", },
|
|
20
|
+
{ ruleId: "brandlab-source-public-secret", surface: "security", checkId: "runtime-supported", summary: "No API key, secret or token behind a NEXT_PUBLIC_ name.", },
|
|
21
|
+
{ ruleId: "brandlab-config-missing", surface: "routes", checkId: "route-identity", summary: "brandlab.config.ts exists.", },
|
|
22
|
+
{ ruleId: "brandlab-config-invalid", surface: "routes", checkId: "route-identity", summary: "brandlab.config.ts is a literal defineBrandlabProject with projectId.", },
|
|
23
|
+
{ ruleId: "brandlab-routes-catalogue", surface: "routes", checkId: "route-identity", summary: "Exactly one module declares the route catalogue with defineRouteCatalog.", },
|
|
24
|
+
{ ruleId: "brandlab-routes-location-estate", surface: "routes", checkId: "route-identity", summary: "A project that publishes locations declares them in src/data/locations/estate.ts with defineLocationEstate.", },
|
|
25
|
+
{ ruleId: "brandlab-redirects-table", surface: "routes", checkId: "route-identity", summary: "Exactly one module declares the redirect table with defineRedirects.", },
|
|
26
|
+
{ ruleId: "brandlab-contact-recipient-env", surface: "contact", checkId: "contact-runtime", summary: "The contact contract declares delivery.recipientEnv as a literal key.", },
|
|
27
|
+
{ ruleId: "brandlab-source-local-contact-delivery", surface: "contact", checkId: "contact-runtime", summary: "Email delivery is left to the shared contact runtime.", },
|
|
28
|
+
{ ruleId: "brandlab-source-local-contact-email-renderer", surface: "contact", checkId: "contact-runtime", summary: "Contact email rendering is left to the shared contact runtime.", },
|
|
29
|
+
{ ruleId: "brandlab-source-local-contact-attachments", surface: "contact", checkId: "contact-runtime", summary: "Attachment preparation is left to the shared contact runtime.", },
|
|
30
|
+
{ ruleId: "brandlab-source-contact-draft-persistence", surface: "contact", checkId: "contact-runtime", summary: "Contact details are never persisted in browser storage.", },
|
|
31
|
+
{ ruleId: "brandlab-source-retired-contact-state", surface: "contact", checkId: "contact-runtime", summary: "No retired contact state service variables in source.", },
|
|
32
|
+
{ ruleId: "brandlab-contact-provider-route", surface: "contact", checkId: "contact-provider-errors", summary: "POST /api/contact is created by the shared handler, so provider errors are handled by the runtime.", },
|
|
33
|
+
{ ruleId: "brandlab-analytics-pii-payload", surface: "analytics", checkId: "analytics-privacy", summary: "No lead fields, messages or attachment details in analytics payloads, and no project-set user_data.", },
|
|
34
|
+
{ ruleId: "brandlab-source-retired-analytics-event", surface: "analytics", checkId: "analytics-conversion", summary: "Only current event names are sent: contact_form for accepted enquiries and phone_link_click as a tap diagnostic.", },
|
|
35
|
+
{ ruleId: "brandlab-source-local-google-tag-bootstrap", surface: "analytics", checkId: "analytics-conversion", summary: "The Google tag bootstrap is owned by the shared runtime.", },
|
|
36
|
+
{ ruleId: "brandlab-analytics-google-tag-owner", surface: "analytics", checkId: "analytics-conversion", summary: "Exactly one BrandlabGoogleTag, rendered by the root layout.", },
|
|
37
|
+
{ ruleId: "brandlab-analytics-contract", surface: "analytics", checkId: "analytics-conversion", summary: "Exactly one module declares the analytics binding with defineBrandlabAnalytics.", },
|
|
38
|
+
{ ruleId: "brandlab-analytics-instrumentation", surface: "analytics", checkId: "analytics-conversion", summary: "src/instrumentation-client.ts calls installBrandlabInstrumentation, which reports one phone_link_click per tap.", },
|
|
39
|
+
{ ruleId: "brandlab-analytics-retired-phone-api", surface: "analytics", checkId: "analytics-conversion", summary: "No trackPhoneCall; taps are reported once as phone_link_click.", },
|
|
40
|
+
{ ruleId: "brandlab-analytics-web-vitals", surface: "analytics", checkId: "analytics-conversion", summary: "No web-vitals reporter, web_vital event or webVitals flag.", },
|
|
41
|
+
{ ruleId: "brandlab-robots-package-route", surface: "robots", checkId: "search-routing", summary: "robots is a thin binding of @brandlab-digital/site-next/robots with no static public/robots.txt.", },
|
|
42
|
+
{ ruleId: "brandlab-sitemap-package-route", surface: "sitemap", checkId: "search-routing", summary: "The sitemap route uses the shared sitemap API over the route catalogue, with no static public/sitemap.xml.", },
|
|
43
|
+
{ ruleId: "brandlab-sitemap-split-mode", surface: "sitemap", checkId: "search-routing", summary: "/sitemaps/[chunk] exists exactly when brandlab.config sets sitemap.mode: 'split'.", },
|
|
44
|
+
{ ruleId: "brandlab-metadata-route-builder", surface: "metadata", checkId: "search-routing", summary: "Page metadata comes from createRouteMetadataBuilder(site, routes).", },
|
|
45
|
+
{ ruleId: "brandlab-site-identity", surface: "metadata", checkId: "search-routing", summary: "Exactly one module declares the site identity (canonical URL, locale, titles) with defineBrandlabSite.", },
|
|
46
|
+
{ ruleId: "brandlab-source-raw-json-ld", surface: "schema", checkId: "structured-data", summary: "JSON-LD is rendered only by the shared schema component.", },
|
|
47
|
+
{ ruleId: "brandlab-source-self-serving-rating-schema", surface: "schema", checkId: "structured-data", summary: "No self-serving AggregateRating schema.", },
|
|
48
|
+
{ ruleId: "brandlab-source-synthetic-offer-schema", surface: "schema", checkId: "structured-data", summary: "No invented Offer markup.", },
|
|
49
|
+
{ ruleId: "brandlab-source-local-google-reviews-fetch", surface: "reviews", checkId: "structured-data", summary: "Google Places reviews are fetched only by the shared review runtime.", },
|
|
50
|
+
{ ruleId: "brandlab-business-facts", surface: "business", checkId: "business-identity", summary: "Exactly one module declares the business facts with defineBrandlabBusiness.", },
|
|
51
|
+
{ ruleId: "brandlab-content-implementation-wording", surface: "content", checkId: "public-output-safety", summary: "Everything that renders publicly (pages, components, metadata, schema, alt text, MDX, Markdown and JSON content) carries finished copy, not template, helper, scaffold or placeholder wording.", },
|
|
52
|
+
{ ruleId: "brandlab-source-root-missing", surface: "source", checkId: "source-safety", summary: "Project source sits under a recognised root so the source rules can run.", },
|
|
53
|
+
{ ruleId: "brandlab-source-platform-reimplementation", surface: "source", checkId: "source-safety", summary: "Protected platform surfaces (SchemaScript, SafeRichText, contact, sitemap, analytics APIs) are imported, not redeclared.", },
|
|
54
|
+
{ ruleId: "brandlab-source-retired-api", surface: "source", checkId: "source-safety", summary: "No API, option or configuration key removed in 3.0 (TrackedTelephoneLink, shouldIndexSite, buildRobotsMetadata, buildSeoMetadata and the duplicate metadata builders, isBrandlabProductionDeployment, resolveBrowserAnalyticsConfig, resolveBrandlabGoogleTagConfig, the contact submission controller and its lead-tracking helpers (use useBrandlabContactForm), header profiles and isProduction, xRobotsTag, contentSecurityPolicyMode, leadDiagnostics, ALLOW_INDEXING, NEXT_PUBLIC_SITE_URL, profile, routes.aliases).", },
|
|
55
|
+
{ ruleId: "brandlab-ci-workflow-pinned", surface: "project-verification", checkId: "project-build", summary: "Every reused workflow and action is pinned to a 40-character commit SHA.", },
|
|
56
|
+
{ ruleId: "brandlab-deployment-vercelignore-lockfile", surface: "deployment", checkId: "deployment-binding", summary: ".vercelignore never excludes package.json or package-lock.json.", },
|
|
57
|
+
];
|
|
58
|
+
/** Whether a concrete rule ID (from a finding or evaluation) belongs to a registry entry. */
|
|
59
|
+
export function matchesBrandlabStaticRule(rule, ruleId) {
|
|
60
|
+
if (rule.ruleId === ruleId)
|
|
61
|
+
return true;
|
|
62
|
+
if (!rule.ruleId.includes("<"))
|
|
63
|
+
return false;
|
|
64
|
+
const stem = rule.ruleId.slice(0, rule.ruleId.indexOf("-<"));
|
|
65
|
+
const pattern = new RegExp(`^${rule.ruleId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/<[a-z]+>/g, "[a-z0-9-]+")}$`);
|
|
66
|
+
return ruleId === stem || pattern.test(ruleId);
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=rule-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule-registry.js","sourceRoot":"","sources":["../src/rule-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BrandlabVerificationCheckId } from \"./verification-policy.js\";\n\n/**\n * Every static rule the evaluator can run, the check it evaluates and what it\n * proves. `<placeholder>` segments stand for the concrete package, capability,\n * key or state named in each finding. The generated rule reference is built\n * from this list, and tests keep it identical to the evaluator.\n */\nexport interface BrandlabStaticRule {\n ruleId: string;\n surface: string;\n checkId: BrandlabVerificationCheckId;\n summary: string;\n}\n\nexport const BRANDLAB_STATIC_RULES = [\n { ruleId: \"brandlab-release-package-json-missing\", surface: \"release\", checkId: \"release-consistency\", summary: \"package.json is present and readable.\", },\n { ruleId: \"brandlab-release-installed-version-unresolved\", surface: \"release\", checkId: \"release-consistency\", summary: \"All four Brandlab packages are pinned to one exact release.\", },\n { ruleId: \"brandlab-release-unsupported\", surface: \"release\", checkId: \"release-consistency\", summary: \"The installed release is the one this checker certifies.\", },\n { ruleId: \"brandlab-release-declared-installed-mismatch\", surface: \"release\", checkId: \"release-consistency\", summary: \"The declared and detected releases agree.\", },\n { ruleId: \"brandlab-release-package-lock-<state>\", surface: \"release\", checkId: \"release-consistency\", summary: \"A valid npm lockfile v2 or v3 is committed.\", },\n { ruleId: \"brandlab-release-package-pin-mismatch\", surface: \"release\", checkId: \"release-consistency\", summary: \"Each package is declared once and locked at the exact release.\", },\n { ruleId: \"brandlab-release-package-resolution-mismatch\", surface: \"release\", checkId: \"release-consistency\", summary: \"The lockfile resolves each package from the canonical npm registry with integrity.\", },\n { ruleId: \"brandlab-release-package-integrity-mismatch\", surface: \"release\", checkId: \"release-consistency\", summary: \"With --release-manifest, lockfile integrity equals the immutable release record.\", },\n { ruleId: \"brandlab-runtime-<package>\", surface: \"runtime\", checkId: \"runtime-supported\", summary: \"next, react, react-dom and typescript use versions the release permits.\", },\n { ruleId: \"brandlab-runtime-react-pair\", surface: \"runtime\", checkId: \"runtime-supported\", summary: \"react and react-dom are declared and locked at the same version.\", },\n { ruleId: \"brandlab-runtime-node\", surface: \"runtime\", checkId: \"runtime-supported\", summary: \"engines.node matches the release in package.json and the lockfile.\", },\n { ruleId: \"brandlab-runtime-package-manager\", surface: \"runtime\", checkId: \"runtime-supported\", summary: \"packageManager is the release's npm version.\", },\n { ruleId: \"brandlab-runtime-sharp\", surface: \"runtime\", checkId: \"runtime-supported\", summary: \"Sharp is pinned only by overrides.sharp with one lockfile resolution.\", },\n { ruleId: \"brandlab-runtime-image-initialisation\", surface: \"runtime\", checkId: \"runtime-supported\", summary: \"next.config imports the shared image-runtime guard first.\", },\n { ruleId: \"brandlab-runtime-next-config\", surface: \"runtime\", checkId: \"runtime-supported\", summary: \"next.config exports defineBrandlabNextConfig, which owns headers, image formats and Preview noindex.\", },\n { ruleId: \"brandlab-environment-<capability>-<key>\", surface: \"environment\", checkId: \"runtime-supported\", summary: \".env.example documents each variable an enabled capability reads, including the contract's recipient key.\", },\n { ruleId: \"brandlab-environment-retired-contact-state\", surface: \"environment\", checkId: \"runtime-supported\", summary: \".env.example lists no retired contact state variables.\", },\n { ruleId: \"brandlab-source-dangerously-set-inner-html\", surface: \"security\", checkId: \"runtime-supported\", summary: \"No project-owned dangerouslySetInnerHTML; only SchemaScript and SafeRichText inject markup.\", },\n { ruleId: \"brandlab-source-public-secret\", surface: \"security\", checkId: \"runtime-supported\", summary: \"No API key, secret or token behind a NEXT_PUBLIC_ name.\", },\n { ruleId: \"brandlab-config-missing\", surface: \"routes\", checkId: \"route-identity\", summary: \"brandlab.config.ts exists.\", },\n { ruleId: \"brandlab-config-invalid\", surface: \"routes\", checkId: \"route-identity\", summary: \"brandlab.config.ts is a literal defineBrandlabProject with projectId.\", },\n { ruleId: \"brandlab-routes-catalogue\", surface: \"routes\", checkId: \"route-identity\", summary: \"Exactly one module declares the route catalogue with defineRouteCatalog.\", },\n { ruleId: \"brandlab-routes-location-estate\", surface: \"routes\", checkId: \"route-identity\", summary: \"A project that publishes locations declares them in src/data/locations/estate.ts with defineLocationEstate.\", },\n { ruleId: \"brandlab-redirects-table\", surface: \"routes\", checkId: \"route-identity\", summary: \"Exactly one module declares the redirect table with defineRedirects.\", },\n { ruleId: \"brandlab-contact-recipient-env\", surface: \"contact\", checkId: \"contact-runtime\", summary: \"The contact contract declares delivery.recipientEnv as a literal key.\", },\n { ruleId: \"brandlab-source-local-contact-delivery\", surface: \"contact\", checkId: \"contact-runtime\", summary: \"Email delivery is left to the shared contact runtime.\", },\n { ruleId: \"brandlab-source-local-contact-email-renderer\", surface: \"contact\", checkId: \"contact-runtime\", summary: \"Contact email rendering is left to the shared contact runtime.\", },\n { ruleId: \"brandlab-source-local-contact-attachments\", surface: \"contact\", checkId: \"contact-runtime\", summary: \"Attachment preparation is left to the shared contact runtime.\", },\n { ruleId: \"brandlab-source-contact-draft-persistence\", surface: \"contact\", checkId: \"contact-runtime\", summary: \"Contact details are never persisted in browser storage.\", },\n { ruleId: \"brandlab-source-retired-contact-state\", surface: \"contact\", checkId: \"contact-runtime\", summary: \"No retired contact state service variables in source.\", },\n { ruleId: \"brandlab-contact-provider-route\", surface: \"contact\", checkId: \"contact-provider-errors\", summary: \"POST /api/contact is created by the shared handler, so provider errors are handled by the runtime.\", },\n { ruleId: \"brandlab-analytics-pii-payload\", surface: \"analytics\", checkId: \"analytics-privacy\", summary: \"No lead fields, messages or attachment details in analytics payloads, and no project-set user_data.\", },\n { ruleId: \"brandlab-source-retired-analytics-event\", surface: \"analytics\", checkId: \"analytics-conversion\", summary: \"Only current event names are sent: contact_form for accepted enquiries and phone_link_click as a tap diagnostic.\", },\n { ruleId: \"brandlab-source-local-google-tag-bootstrap\", surface: \"analytics\", checkId: \"analytics-conversion\", summary: \"The Google tag bootstrap is owned by the shared runtime.\", },\n { ruleId: \"brandlab-analytics-google-tag-owner\", surface: \"analytics\", checkId: \"analytics-conversion\", summary: \"Exactly one BrandlabGoogleTag, rendered by the root layout.\", },\n { ruleId: \"brandlab-analytics-contract\", surface: \"analytics\", checkId: \"analytics-conversion\", summary: \"Exactly one module declares the analytics binding with defineBrandlabAnalytics.\", },\n { ruleId: \"brandlab-analytics-instrumentation\", surface: \"analytics\", checkId: \"analytics-conversion\", summary: \"src/instrumentation-client.ts calls installBrandlabInstrumentation, which reports one phone_link_click per tap.\", },\n { ruleId: \"brandlab-analytics-retired-phone-api\", surface: \"analytics\", checkId: \"analytics-conversion\", summary: \"No trackPhoneCall; taps are reported once as phone_link_click.\", },\n { ruleId: \"brandlab-analytics-web-vitals\", surface: \"analytics\", checkId: \"analytics-conversion\", summary: \"No web-vitals reporter, web_vital event or webVitals flag.\", },\n { ruleId: \"brandlab-robots-package-route\", surface: \"robots\", checkId: \"search-routing\", summary: \"robots is a thin binding of @brandlab-digital/site-next/robots with no static public/robots.txt.\", },\n { ruleId: \"brandlab-sitemap-package-route\", surface: \"sitemap\", checkId: \"search-routing\", summary: \"The sitemap route uses the shared sitemap API over the route catalogue, with no static public/sitemap.xml.\", },\n { ruleId: \"brandlab-sitemap-split-mode\", surface: \"sitemap\", checkId: \"search-routing\", summary: \"/sitemaps/[chunk] exists exactly when brandlab.config sets sitemap.mode: 'split'.\", },\n { ruleId: \"brandlab-metadata-route-builder\", surface: \"metadata\", checkId: \"search-routing\", summary: \"Page metadata comes from createRouteMetadataBuilder(site, routes).\", },\n { ruleId: \"brandlab-site-identity\", surface: \"metadata\", checkId: \"search-routing\", summary: \"Exactly one module declares the site identity (canonical URL, locale, titles) with defineBrandlabSite.\", },\n { ruleId: \"brandlab-source-raw-json-ld\", surface: \"schema\", checkId: \"structured-data\", summary: \"JSON-LD is rendered only by the shared schema component.\", },\n { ruleId: \"brandlab-source-self-serving-rating-schema\", surface: \"schema\", checkId: \"structured-data\", summary: \"No self-serving AggregateRating schema.\", },\n { ruleId: \"brandlab-source-synthetic-offer-schema\", surface: \"schema\", checkId: \"structured-data\", summary: \"No invented Offer markup.\", },\n { ruleId: \"brandlab-source-local-google-reviews-fetch\", surface: \"reviews\", checkId: \"structured-data\", summary: \"Google Places reviews are fetched only by the shared review runtime.\", },\n { ruleId: \"brandlab-business-facts\", surface: \"business\", checkId: \"business-identity\", summary: \"Exactly one module declares the business facts with defineBrandlabBusiness.\", },\n { ruleId: \"brandlab-content-implementation-wording\", surface: \"content\", checkId: \"public-output-safety\", summary: \"Everything that renders publicly (pages, components, metadata, schema, alt text, MDX, Markdown and JSON content) carries finished copy, not template, helper, scaffold or placeholder wording.\", },\n { ruleId: \"brandlab-source-root-missing\", surface: \"source\", checkId: \"source-safety\", summary: \"Project source sits under a recognised root so the source rules can run.\", },\n { ruleId: \"brandlab-source-platform-reimplementation\", surface: \"source\", checkId: \"source-safety\", summary: \"Protected platform surfaces (SchemaScript, SafeRichText, contact, sitemap, analytics APIs) are imported, not redeclared.\", },\n { ruleId: \"brandlab-source-retired-api\", surface: \"source\", checkId: \"source-safety\", summary: \"No API, option or configuration key removed in 3.0 (TrackedTelephoneLink, shouldIndexSite, buildRobotsMetadata, buildSeoMetadata and the duplicate metadata builders, isBrandlabProductionDeployment, resolveBrowserAnalyticsConfig, resolveBrandlabGoogleTagConfig, the contact submission controller and its lead-tracking helpers (use useBrandlabContactForm), header profiles and isProduction, xRobotsTag, contentSecurityPolicyMode, leadDiagnostics, ALLOW_INDEXING, NEXT_PUBLIC_SITE_URL, profile, routes.aliases).\", },\n { ruleId: \"brandlab-ci-workflow-pinned\", surface: \"project-verification\", checkId: \"project-build\", summary: \"Every reused workflow and action is pinned to a 40-character commit SHA.\", },\n { ruleId: \"brandlab-deployment-vercelignore-lockfile\", surface: \"deployment\", checkId: \"deployment-binding\", summary: \".vercelignore never excludes package.json or package-lock.json.\", },\n] as const satisfies readonly BrandlabStaticRule[];\n\n/** Whether a concrete rule ID (from a finding or evaluation) belongs to a registry entry. */\nexport function matchesBrandlabStaticRule(rule: BrandlabStaticRule, ruleId: string): boolean {\n if (rule.ruleId === ruleId) return true;\n if (!rule.ruleId.includes(\"<\")) return false;\n const stem = rule.ruleId.slice(0, rule.ruleId.indexOf(\"-<\"));\n const pattern = new RegExp(`^${rule.ruleId.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/<[a-z]+>/g, \"[a-z0-9-]+\")}$`);\n return ruleId === stem || pattern.test(ruleId);\n}\n"]}
|
|
@@ -14,6 +14,10 @@ export interface SearchCertificationSnapshotComparison {
|
|
|
14
14
|
matches: boolean;
|
|
15
15
|
differences: string[];
|
|
16
16
|
}
|
|
17
|
+
/** Implementation-facing wording that must never reach public copy. */
|
|
18
|
+
export declare const BRANDLAB_IMPLEMENTATION_COPY_PATTERNS: readonly RegExp[];
|
|
19
|
+
/** Whether the pattern occurrence at index sits inside a literal trade phrase (benchtop template). */
|
|
20
|
+
export declare function isBrandlabTradePhrase(text: string, index: number, length: number): boolean;
|
|
17
21
|
export declare function extractSearchCertificationHtmlSnapshot(html: string): SearchCertificationHtmlSnapshot;
|
|
18
22
|
export declare function compareSearchCertificationHtmlSnapshots(expected: SearchCertificationHtmlSnapshot, actual: SearchCertificationHtmlSnapshot): SearchCertificationSnapshotComparison;
|
|
19
23
|
//# sourceMappingURL=search-certification.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-certification.d.ts","sourceRoot":"","sources":["../src/search-certification.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,+BAA+B;IAC9C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,MAAM,EAAE,OAAO,EAAE,CAAA;IACjB,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,gBAAgB,EAAE,OAAO,CAAA;IACzB,yBAAyB,EAAE,MAAM,EAAE,CAAA;CACpC;AAED,MAAM,WAAW,qCAAqC;IACpD,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB;
|
|
1
|
+
{"version":3,"file":"search-certification.d.ts","sourceRoot":"","sources":["../src/search-certification.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,+BAA+B;IAC9C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,MAAM,EAAE,OAAO,EAAE,CAAA;IACjB,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,gBAAgB,EAAE,OAAO,CAAA;IACzB,yBAAyB,EAAE,MAAM,EAAE,CAAA;CACpC;AAED,MAAM,WAAW,qCAAqC;IACpD,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,EAAE,MAAM,EAAE,CAAA;CACtB;AAED,uEAAuE;AACvE,eAAO,MAAM,qCAAqC,EAAE,SAAS,MAAM,EAOlE,CAAA;AAWD,sGAAsG;AACtG,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAI1F;AA+HD,wBAAgB,sCAAsC,CACpD,IAAI,EAAE,MAAM,GACX,+BAA+B,CAgBjC;AAED,wBAAgB,uCAAuC,CACrD,QAAQ,EAAE,+BAA+B,EACzC,MAAM,EAAE,+BAA+B,GACtC,qCAAqC,CAyBvC"}
|
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
/** Implementation-facing wording that must never reach public copy. */
|
|
2
|
+
export const BRANDLAB_IMPLEMENTATION_COPY_PATTERNS = [
|
|
2
3
|
/\btemplate\b/i,
|
|
3
4
|
/\bhelper\b/i,
|
|
4
5
|
/\bscaffold\b/i,
|
|
5
6
|
/\bplaceholder\b/i,
|
|
6
7
|
/\breplace with real\b/i,
|
|
8
|
+
/\blorem ipsum\b/i,
|
|
7
9
|
];
|
|
10
|
+
/**
|
|
11
|
+
* Trade phrases that use a wording pattern literally rather than as implementation
|
|
12
|
+
* language: a stone benchtop is measured with a template before it is cut.
|
|
13
|
+
*/
|
|
14
|
+
const TRADE_COPY_PHRASES = [
|
|
15
|
+
/\b(?:bench\s?tops?|counter\s?tops?|work\s?tops?|stone|splashbacks?|vanity\s+tops?)\s+templates?\b/gi,
|
|
16
|
+
/\btemplates?\s+(?:and|&)\s+(?:measure|install)\w*/gi,
|
|
17
|
+
];
|
|
18
|
+
/** Whether the pattern occurrence at index sits inside a literal trade phrase (benchtop template). */
|
|
19
|
+
export function isBrandlabTradePhrase(text, index, length) {
|
|
20
|
+
return TRADE_COPY_PHRASES.some((phrase) => [...text.matchAll(phrase)].some((match) => match.index <= index && index + length <= match.index + match[0].length));
|
|
21
|
+
}
|
|
8
22
|
function decodeHtml(value) {
|
|
9
23
|
return value
|
|
10
24
|
.replace(/&/g, '&')
|
|
@@ -101,8 +115,8 @@ function collectImplementationCopyMatches(html) {
|
|
|
101
115
|
.replace(/<style\b[\s\S]*?<\/style>/gi, ' ')
|
|
102
116
|
.replace(/<[^>]+>/g, ' ')
|
|
103
117
|
.replace(/\s+/g, ' ');
|
|
104
|
-
return
|
|
105
|
-
.filter((pattern) => pattern.
|
|
118
|
+
return BRANDLAB_IMPLEMENTATION_COPY_PATTERNS
|
|
119
|
+
.filter((pattern) => [...visibleText.matchAll(new RegExp(pattern.source, pattern.flags.replace('g', '') + 'g'))].some((match) => !isBrandlabTradePhrase(visibleText, match.index, match[0].length)))
|
|
106
120
|
.map((pattern) => pattern.source);
|
|
107
121
|
}
|
|
108
122
|
export function extractSearchCertificationHtmlSnapshot(html) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-certification.js","sourceRoot":"","sources":["../src/search-certification.ts"],"names":[],"mappings":"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","sourcesContent":["export interface SearchCertificationHtmlSnapshot {\n title: string | null\n description: string | null\n canonical: string | null\n robots: string | null\n openGraph: Record<string, string>\n twitter: Record<string, string>\n jsonLd: unknown[]\n jsonLdTypes: string[]\n googleTagPresent: boolean\n implementationCopyMatches: string[]\n}\n\nexport interface SearchCertificationSnapshotComparison {\n matches: boolean\n differences: string[]\n}\n\nconst IMPLEMENTATION_COPY_PATTERNS = [\n /\\btemplate\\b/i,\n /\\bhelper\\b/i,\n /\\bscaffold\\b/i,\n /\\bplaceholder\\b/i,\n /\\breplace with real\\b/i,\n]\n\nfunction decodeHtml(value: string) {\n return value\n .replace(/&/g, '&')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/'/gi, \"'\")\n .replace(/</g, '<')\n .replace(/>/g, '>')\n}\n\nfunction getAttribute(tag: string, name: string) {\n const pattern = new RegExp(`${name}\\\\s*=\\\\s*[\"']([^\"']*)[\"']`, 'i')\n return decodeHtml(tag.match(pattern)?.[1] ?? '')\n}\n\nfunction findMetaContent(html: string, attribute: 'name' | 'property', key: string) {\n const metaPattern = /<meta\\b[^>]*>/gi\n const tags = html.match(metaPattern) ?? []\n\n for (const tag of tags) {\n if (getAttribute(tag, attribute) === key) {\n return getAttribute(tag, 'content') || null\n }\n }\n\n return null\n}\n\nfunction findOpenGraph(html: string) {\n const values: Record<string, string> = {}\n const metaPattern = /<meta\\b[^>]*>/gi\n const tags = html.match(metaPattern) ?? []\n\n for (const tag of tags) {\n const property = getAttribute(tag, 'property')\n if (property.startsWith('og:')) {\n values[property] = getAttribute(tag, 'content')\n }\n }\n\n return values\n}\n\nfunction findTwitter(html: string) {\n const values: Record<string, string> = {}\n const metaPattern = /<meta\\b[^>]*>/gi\n const tags = html.match(metaPattern) ?? []\n\n for (const tag of tags) {\n const name = getAttribute(tag, 'name')\n if (name.startsWith('twitter:')) {\n values[name] = getAttribute(tag, 'content')\n }\n }\n\n return values\n}\n\nfunction findCanonical(html: string) {\n const linkPattern = /<link\\b[^>]*>/gi\n const tags = html.match(linkPattern) ?? []\n\n for (const tag of tags) {\n if (getAttribute(tag, 'rel').toLowerCase() === 'canonical') {\n return getAttribute(tag, 'href') || null\n }\n }\n\n return null\n}\n\nfunction parseJsonLd(html: string) {\n const payloads: unknown[] = []\n const scriptPattern =\n /<script\\b[^>]*type=[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script>/gi\n\n for (const match of html.matchAll(scriptPattern)) {\n try {\n payloads.push(JSON.parse(decodeHtml(match[1]).trim()))\n } catch {\n payloads.push({ parseError: true })\n }\n }\n\n return payloads\n}\n\nfunction collectJsonLdTypes(payloads: unknown[]) {\n const types = new Set<string>()\n\n function visit(value: unknown) {\n if (!value || typeof value !== 'object') return\n\n const record = value as Record<string, unknown>\n const type = record['@type']\n\n if (typeof type === 'string') {\n types.add(type)\n } else if (Array.isArray(type)) {\n type.filter((item): item is string => typeof item === 'string').forEach((item) => types.add(item))\n }\n\n if (Array.isArray(record['@graph'])) {\n record['@graph'].forEach(visit)\n }\n }\n\n payloads.forEach(visit)\n return [...types].sort()\n}\n\nfunction collectImplementationCopyMatches(html: string) {\n const visibleText = html\n .replace(/<script\\b[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style\\b[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n\n return IMPLEMENTATION_COPY_PATTERNS\n .filter((pattern) => pattern.test(visibleText))\n .map((pattern) => pattern.source)\n}\n\nexport function extractSearchCertificationHtmlSnapshot(\n html: string,\n): SearchCertificationHtmlSnapshot {\n const title = decodeHtml(html.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i)?.[1]?.trim() ?? '')\n const jsonLd = parseJsonLd(html)\n\n return {\n title: title || null,\n description: findMetaContent(html, 'name', 'description'),\n canonical: findCanonical(html),\n robots: findMetaContent(html, 'name', 'robots'),\n openGraph: findOpenGraph(html),\n twitter: findTwitter(html),\n jsonLd,\n jsonLdTypes: collectJsonLdTypes(jsonLd),\n googleTagPresent: /googletagmanager\\.com\\/gtag\\/js|gtag\\(/.test(html),\n implementationCopyMatches: collectImplementationCopyMatches(html),\n }\n}\n\nexport function compareSearchCertificationHtmlSnapshots(\n expected: SearchCertificationHtmlSnapshot,\n actual: SearchCertificationHtmlSnapshot,\n): SearchCertificationSnapshotComparison {\n const differences: string[] = []\n\n for (const field of ['title', 'description', 'canonical', 'robots'] as const) {\n if (expected[field] !== actual[field]) {\n differences.push(`${field} changed`)\n }\n }\n\n if (JSON.stringify(expected.jsonLdTypes) !== JSON.stringify(actual.jsonLdTypes)) {\n differences.push('jsonLdTypes changed')\n }\n\n if (expected.googleTagPresent !== actual.googleTagPresent) {\n differences.push('googleTagPresent changed')\n }\n\n if (actual.implementationCopyMatches.length > 0) {\n differences.push('implementation-facing copy detected')\n }\n\n return {\n matches: differences.length === 0,\n differences,\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"search-certification.js","sourceRoot":"","sources":["../src/search-certification.ts"],"names":[],"mappings":"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","sourcesContent":["export interface SearchCertificationHtmlSnapshot {\n title: string | null\n description: string | null\n canonical: string | null\n robots: string | null\n openGraph: Record<string, string>\n twitter: Record<string, string>\n jsonLd: unknown[]\n jsonLdTypes: string[]\n googleTagPresent: boolean\n implementationCopyMatches: string[]\n}\n\nexport interface SearchCertificationSnapshotComparison {\n matches: boolean\n differences: string[]\n}\n\n/** Implementation-facing wording that must never reach public copy. */\nexport const BRANDLAB_IMPLEMENTATION_COPY_PATTERNS: readonly RegExp[] = [\n /\\btemplate\\b/i,\n /\\bhelper\\b/i,\n /\\bscaffold\\b/i,\n /\\bplaceholder\\b/i,\n /\\breplace with real\\b/i,\n /\\blorem ipsum\\b/i,\n]\n\n/**\n * Trade phrases that use a wording pattern literally rather than as implementation\n * language: a stone benchtop is measured with a template before it is cut.\n */\nconst TRADE_COPY_PHRASES: readonly RegExp[] = [\n /\\b(?:bench\\s?tops?|counter\\s?tops?|work\\s?tops?|stone|splashbacks?|vanity\\s+tops?)\\s+templates?\\b/gi,\n /\\btemplates?\\s+(?:and|&)\\s+(?:measure|install)\\w*/gi,\n]\n\n/** Whether the pattern occurrence at index sits inside a literal trade phrase (benchtop template). */\nexport function isBrandlabTradePhrase(text: string, index: number, length: number): boolean {\n return TRADE_COPY_PHRASES.some((phrase) =>\n [...text.matchAll(phrase)].some((match) =>\n match.index <= index && index + length <= match.index + match[0].length))\n}\n\nfunction decodeHtml(value: string) {\n return value\n .replace(/&/g, '&')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/'/gi, \"'\")\n .replace(/</g, '<')\n .replace(/>/g, '>')\n}\n\nfunction getAttribute(tag: string, name: string) {\n const pattern = new RegExp(`${name}\\\\s*=\\\\s*[\"']([^\"']*)[\"']`, 'i')\n return decodeHtml(tag.match(pattern)?.[1] ?? '')\n}\n\nfunction findMetaContent(html: string, attribute: 'name' | 'property', key: string) {\n const metaPattern = /<meta\\b[^>]*>/gi\n const tags = html.match(metaPattern) ?? []\n\n for (const tag of tags) {\n if (getAttribute(tag, attribute) === key) {\n return getAttribute(tag, 'content') || null\n }\n }\n\n return null\n}\n\nfunction findOpenGraph(html: string) {\n const values: Record<string, string> = {}\n const metaPattern = /<meta\\b[^>]*>/gi\n const tags = html.match(metaPattern) ?? []\n\n for (const tag of tags) {\n const property = getAttribute(tag, 'property')\n if (property.startsWith('og:')) {\n values[property] = getAttribute(tag, 'content')\n }\n }\n\n return values\n}\n\nfunction findTwitter(html: string) {\n const values: Record<string, string> = {}\n const metaPattern = /<meta\\b[^>]*>/gi\n const tags = html.match(metaPattern) ?? []\n\n for (const tag of tags) {\n const name = getAttribute(tag, 'name')\n if (name.startsWith('twitter:')) {\n values[name] = getAttribute(tag, 'content')\n }\n }\n\n return values\n}\n\nfunction findCanonical(html: string) {\n const linkPattern = /<link\\b[^>]*>/gi\n const tags = html.match(linkPattern) ?? []\n\n for (const tag of tags) {\n if (getAttribute(tag, 'rel').toLowerCase() === 'canonical') {\n return getAttribute(tag, 'href') || null\n }\n }\n\n return null\n}\n\nfunction parseJsonLd(html: string) {\n const payloads: unknown[] = []\n const scriptPattern =\n /<script\\b[^>]*type=[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script>/gi\n\n for (const match of html.matchAll(scriptPattern)) {\n try {\n payloads.push(JSON.parse(decodeHtml(match[1]).trim()))\n } catch {\n payloads.push({ parseError: true })\n }\n }\n\n return payloads\n}\n\nfunction collectJsonLdTypes(payloads: unknown[]) {\n const types = new Set<string>()\n\n function visit(value: unknown) {\n if (!value || typeof value !== 'object') return\n\n const record = value as Record<string, unknown>\n const type = record['@type']\n\n if (typeof type === 'string') {\n types.add(type)\n } else if (Array.isArray(type)) {\n type.filter((item): item is string => typeof item === 'string').forEach((item) => types.add(item))\n }\n\n if (Array.isArray(record['@graph'])) {\n record['@graph'].forEach(visit)\n }\n }\n\n payloads.forEach(visit)\n return [...types].sort()\n}\n\nfunction collectImplementationCopyMatches(html: string) {\n const visibleText = html\n .replace(/<script\\b[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style\\b[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\s+/g, ' ')\n\n return BRANDLAB_IMPLEMENTATION_COPY_PATTERNS\n .filter((pattern) =>\n [...visibleText.matchAll(new RegExp(pattern.source, pattern.flags.replace('g', '') + 'g'))].some((match) =>\n !isBrandlabTradePhrase(visibleText, match.index, match[0].length)))\n .map((pattern) => pattern.source)\n}\n\nexport function extractSearchCertificationHtmlSnapshot(\n html: string,\n): SearchCertificationHtmlSnapshot {\n const title = decodeHtml(html.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i)?.[1]?.trim() ?? '')\n const jsonLd = parseJsonLd(html)\n\n return {\n title: title || null,\n description: findMetaContent(html, 'name', 'description'),\n canonical: findCanonical(html),\n robots: findMetaContent(html, 'name', 'robots'),\n openGraph: findOpenGraph(html),\n twitter: findTwitter(html),\n jsonLd,\n jsonLdTypes: collectJsonLdTypes(jsonLd),\n googleTagPresent: /googletagmanager\\.com\\/gtag\\/js|gtag\\(/.test(html),\n implementationCopyMatches: collectImplementationCopyMatches(html),\n }\n}\n\nexport function compareSearchCertificationHtmlSnapshots(\n expected: SearchCertificationHtmlSnapshot,\n actual: SearchCertificationHtmlSnapshot,\n): SearchCertificationSnapshotComparison {\n const differences: string[] = []\n\n for (const field of ['title', 'description', 'canonical', 'robots'] as const) {\n if (expected[field] !== actual[field]) {\n differences.push(`${field} changed`)\n }\n }\n\n if (JSON.stringify(expected.jsonLdTypes) !== JSON.stringify(actual.jsonLdTypes)) {\n differences.push('jsonLdTypes changed')\n }\n\n if (expected.googleTagPresent !== actual.googleTagPresent) {\n differences.push('googleTagPresent changed')\n }\n\n if (actual.implementationCopyMatches.length > 0) {\n differences.push('implementation-facing copy detected')\n }\n\n return {\n matches: differences.length === 0,\n differences,\n }\n}\n"]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
/** Directory roots that can hold project-owned application source. */
|
|
3
|
+
export declare const BRANDLAB_SOURCE_ROOT_DIRECTORIES: readonly ["src", "app", "components", "lib", "config", "hooks", "context", "utils", "data"];
|
|
4
|
+
export interface BrandlabSourceFile {
|
|
5
|
+
/** Project-relative path with forward slashes. */
|
|
6
|
+
path: string;
|
|
7
|
+
text: string;
|
|
8
|
+
/** The same text with comments blanked so offsets and lines are preserved. */
|
|
9
|
+
executable: string;
|
|
10
|
+
}
|
|
11
|
+
export interface BrandlabSourceIndex {
|
|
12
|
+
root: string;
|
|
13
|
+
/** Recognised directory roots present in the project. */
|
|
14
|
+
directoryRoots: string[];
|
|
15
|
+
/** Recognised root-level framework files present in the project. */
|
|
16
|
+
rootFiles: string[];
|
|
17
|
+
files: BrandlabSourceFile[];
|
|
18
|
+
byPath: ReadonlyMap<string, BrandlabSourceFile>;
|
|
19
|
+
}
|
|
20
|
+
export interface BrandlabImportBinding {
|
|
21
|
+
imported: string;
|
|
22
|
+
local: string;
|
|
23
|
+
}
|
|
24
|
+
export interface BrandlabModuleReference {
|
|
25
|
+
specifier: string;
|
|
26
|
+
kind: "import" | "export" | "dynamic";
|
|
27
|
+
typeOnly: boolean;
|
|
28
|
+
bindings: BrandlabImportBinding[];
|
|
29
|
+
line: number;
|
|
30
|
+
}
|
|
31
|
+
/** Collect every non-test source file under the recognised project roots. */
|
|
32
|
+
export declare function collectBrandlabSource(root: string): BrandlabSourceIndex;
|
|
33
|
+
/** List every file below a project-relative directory, skipping dependencies and dot entries. */
|
|
34
|
+
export declare function walkProjectFiles(root: string, directory: string): string[];
|
|
35
|
+
export declare function parseBrandlabSourceFile(file: BrandlabSourceFile): ts.SourceFile;
|
|
36
|
+
/** Static imports, re-exports and literal dynamic imports declared by one file. */
|
|
37
|
+
export declare function listModuleReferences(file: BrandlabSourceFile): BrandlabModuleReference[];
|
|
38
|
+
/** Local names bound to an export of a package module, for example BrandlabGoogleTag. */
|
|
39
|
+
export declare function importedLocalNames(file: BrandlabSourceFile, modulePattern: RegExp, importedName: string): string[];
|
|
40
|
+
/** Calls to a named export of a package module, following local import aliases. */
|
|
41
|
+
export declare function findImportedCalls(file: BrandlabSourceFile, modulePattern: RegExp, importedName: string): ts.CallExpression[];
|
|
42
|
+
export declare function lineOfNode(file: BrandlabSourceFile, node: ts.Node): number;
|
|
43
|
+
/** The value of a named property in an object literal, or undefined. */
|
|
44
|
+
export declare function objectPropertyValue(node: ts.Node | undefined, name: string): ts.Expression | undefined;
|
|
45
|
+
export declare function stringLiteralValue(node: ts.Node | undefined): string | null;
|
|
46
|
+
export type BrandlabModuleResolver = (fromPath: string, specifier: string) => string | null;
|
|
47
|
+
/** Resolve project-local specifiers (relative and tsconfig path aliases) to indexed files. */
|
|
48
|
+
export declare function createBrandlabModuleResolver(root: string, knownPaths: ReadonlySet<string>): BrandlabModuleResolver;
|
|
49
|
+
/** Blank comments while preserving string contents, offsets and line breaks. */
|
|
50
|
+
export declare function stripCommentsPreservingLayout(source: string): string;
|
|
51
|
+
export declare function lineAt(source: string, index: number): number;
|
|
52
|
+
export declare function isDirectory(path: string): boolean;
|
|
53
|
+
export declare function readOptionalText(path: string): string | null;
|
|
54
|
+
//# sourceMappingURL=source-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-files.d.ts","sourceRoot":"","sources":["../src/source-files.ts"],"names":[],"mappings":"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"}
|