@decantr/verifier 1.0.5 → 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Decantr AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -16,7 +16,8 @@ npm install @decantr/verifier
16
16
  - `auditProject()` for project-level Decantr audits
17
17
  - `auditBuiltDist()` for built-output runtime verification against emitted HTML, assets, and route hints
18
18
  - `critiqueFile()` for file-level review against compiled review-pack contracts
19
- - schema-backed report types for project audits, file critiques, and showcase verification
19
+ - schema-backed report types for project audits, Project Health, file critiques, and showcase verification
20
+ - `ProjectHealthReport`, `ProjectHealthFinding`, and `ProjectHealthRemediation` types for the CLI's end-user health surface
20
21
  - published verifier report schemas are exercised by AJV-backed round-trip tests against real audit, critique, and shortlist-report outputs
21
22
  - project audits include runtime evidence when a built `dist/` output is present:
22
23
  - root document
@@ -30,16 +31,21 @@ npm install @decantr/verifier
30
31
  ## Example
31
32
 
32
33
  ```ts
33
- import { auditProject, critiqueFile } from '@decantr/verifier';
34
+ import { auditProject, critiqueFile, type ProjectHealthReport } from '@decantr/verifier';
34
35
 
35
36
  const audit = await auditProject(process.cwd());
36
37
  const critique = await critiqueFile('./src/pages/overview.tsx', process.cwd());
38
+
39
+ function isBlocking(report: ProjectHealthReport) {
40
+ return report.status === 'error';
41
+ }
37
42
  ```
38
43
 
39
44
  ## Schema Exports
40
45
 
41
46
  - `@decantr/verifier/schema/verification-report.common.v1.json`
42
47
  - `@decantr/verifier/schema/project-audit-report.v1.json`
48
+ - `@decantr/verifier/schema/project-health-report.v1.json`
43
49
  - `@decantr/verifier/schema/file-critique-report.v1.json`
44
50
  - `@decantr/verifier/schema/showcase-shortlist-report.v1.json`
45
51
 
package/dist/index.d.ts CHANGED
@@ -118,10 +118,13 @@ declare function listKnownInteractions(): string[];
118
118
  declare const VERIFICATION_SCHEMA_URLS: {
119
119
  readonly common: "https://decantr.ai/schemas/verification-report.common.v1.json";
120
120
  readonly projectAudit: "https://decantr.ai/schemas/project-audit-report.v1.json";
121
+ readonly projectHealth: "https://decantr.ai/schemas/project-health-report.v1.json";
121
122
  readonly fileCritique: "https://decantr.ai/schemas/file-critique-report.v1.json";
122
123
  readonly showcaseShortlist: "https://decantr.ai/schemas/showcase-shortlist-report.v1.json";
123
124
  };
124
125
  type VerificationSeverity = 'error' | 'warn' | 'info';
126
+ type ProjectHealthStatus = 'healthy' | 'warning' | 'error';
127
+ type ProjectHealthFindingSource = 'audit' | 'check' | 'brownfield' | 'runtime' | 'pack' | 'interaction';
125
128
  interface VerificationFinding {
126
129
  id: string;
127
130
  category: string;
@@ -183,6 +186,68 @@ interface ProjectAuditReport {
183
186
  pageCount: number;
184
187
  };
185
188
  }
189
+ interface ProjectHealthRemediation {
190
+ summary: string;
191
+ prompt: string;
192
+ commands: string[];
193
+ }
194
+ interface ProjectHealthFinding {
195
+ id: string;
196
+ source: ProjectHealthFindingSource;
197
+ category: string;
198
+ severity: VerificationSeverity;
199
+ message: string;
200
+ evidence: string[];
201
+ target?: string;
202
+ file?: string;
203
+ rule?: string;
204
+ suggestedFix?: string;
205
+ remediation: ProjectHealthRemediation;
206
+ }
207
+ interface ProjectHealthRouteSummary {
208
+ declared: string[];
209
+ runtimeChecked: string[];
210
+ runtimeMatched: number;
211
+ runtimeCoverageOk: boolean | null;
212
+ issues: string[];
213
+ }
214
+ interface ProjectHealthPackSummary {
215
+ manifestPresent: boolean;
216
+ reviewPackPresent: boolean;
217
+ scaffoldPackPresent: boolean;
218
+ sectionPackCount: number;
219
+ pagePackCount: number;
220
+ mutationPackCount: number;
221
+ generatedAt: string | null;
222
+ }
223
+ interface ProjectHealthReport {
224
+ $schema: string;
225
+ generatedAt: string;
226
+ projectRoot: string;
227
+ status: ProjectHealthStatus;
228
+ score: number;
229
+ summary: {
230
+ errorCount: number;
231
+ warnCount: number;
232
+ infoCount: number;
233
+ findingCount: number;
234
+ workflowMode: string | null;
235
+ adoptionMode: string | null;
236
+ essenceVersion: string | null;
237
+ pageCount: number;
238
+ runtimeAuditChecked: boolean;
239
+ runtimePassed: boolean | null;
240
+ packManifestPresent: boolean;
241
+ reviewPackPresent: boolean;
242
+ };
243
+ routes: ProjectHealthRouteSummary;
244
+ packs: ProjectHealthPackSummary;
245
+ ci: {
246
+ recommendedCommand: string;
247
+ failOn: 'error' | 'warn' | 'none';
248
+ };
249
+ findings: ProjectHealthFinding[];
250
+ }
186
251
  interface FileCritiqueReport {
187
252
  $schema: string;
188
253
  file: string;
@@ -297,4 +362,4 @@ declare function auditProject(projectRoot: string): Promise<ProjectAuditReport>;
297
362
  declare function critiqueSource({ filePath, code, reviewPack, packManifest, treatmentsCss, }: CritiqueSourceInput): FileCritiqueReport;
298
363
  declare function critiqueFile(filePath: string, projectRoot: string): Promise<FileCritiqueReport>;
299
364
 
300
- export { type BuiltDistAuditOptions, type CritiqueSourceInput, type FileCritiqueReport, INTERACTION_SIGNALS, type InteractionMissingFinding, type InteractionRequirement, type InteractionSignal, type PackManifest, type ProjectAuditReport, type RuntimeAudit, type ShowcaseShortlistVerificationEntry, type ShowcaseShortlistVerificationReport, VERIFICATION_SCHEMA_URLS, type VerificationFinding, type VerificationScore, type VerificationSeverity, auditBuiltDist, auditProject, critiqueFile, critiqueSource, emptyRuntimeAudit, extractRouteHintsFromEssence, listKnownInteractions, verifyInteractionsInSource };
365
+ export { type BuiltDistAuditOptions, type CritiqueSourceInput, type FileCritiqueReport, INTERACTION_SIGNALS, type InteractionMissingFinding, type InteractionRequirement, type InteractionSignal, type PackManifest, type ProjectAuditReport, type ProjectHealthFinding, type ProjectHealthFindingSource, type ProjectHealthPackSummary, type ProjectHealthRemediation, type ProjectHealthReport, type ProjectHealthRouteSummary, type ProjectHealthStatus, type RuntimeAudit, type ShowcaseShortlistVerificationEntry, type ShowcaseShortlistVerificationReport, VERIFICATION_SCHEMA_URLS, type VerificationFinding, type VerificationScore, type VerificationSeverity, auditBuiltDist, auditProject, critiqueFile, critiqueSource, emptyRuntimeAudit, extractRouteHintsFromEssence, listKnownInteractions, verifyInteractionsInSource };
package/dist/index.js CHANGED
@@ -610,6 +610,7 @@ function listKnownInteractions() {
610
610
  var VERIFICATION_SCHEMA_URLS = {
611
611
  common: "https://decantr.ai/schemas/verification-report.common.v1.json",
612
612
  projectAudit: "https://decantr.ai/schemas/project-audit-report.v1.json",
613
+ projectHealth: "https://decantr.ai/schemas/project-health-report.v1.json",
613
614
  fileCritique: "https://decantr.ai/schemas/file-critique-report.v1.json",
614
615
  showcaseShortlist: "https://decantr.ai/schemas/showcase-shortlist-report.v1.json"
615
616
  };
@@ -3312,8 +3313,30 @@ async function auditProject(projectRoot) {
3312
3313
  };
3313
3314
  }
3314
3315
  function buildDecoratorInventory(treatmentsCss) {
3315
- const decoratorMatches = treatmentsCss.match(/\.([\w-]+)\s*\{/g) || [];
3316
- return decoratorMatches.map((match) => match.slice(1).replace(/\s*\{$/, "")).filter((name) => !name.startsWith("d-") && !PERSONALITY_UTILS.includes(name));
3316
+ const decoratorNames = /* @__PURE__ */ new Set();
3317
+ for (let index = treatmentsCss.indexOf("."); index !== -1; index = treatmentsCss.indexOf(".", index + 1)) {
3318
+ let cursor = index + 1;
3319
+ while (cursor < treatmentsCss.length && isCssClassNameChar(treatmentsCss[cursor])) {
3320
+ cursor += 1;
3321
+ }
3322
+ if (cursor === index + 1) continue;
3323
+ const name = treatmentsCss.slice(index + 1, cursor);
3324
+ let whitespaceCursor = cursor;
3325
+ while (whitespaceCursor < treatmentsCss.length && whitespaceCursor - cursor <= 64 && isCssWhitespace(treatmentsCss[whitespaceCursor])) {
3326
+ whitespaceCursor += 1;
3327
+ }
3328
+ if (treatmentsCss[whitespaceCursor] === "{") {
3329
+ decoratorNames.add(name);
3330
+ }
3331
+ }
3332
+ return [...decoratorNames].filter((name) => !name.startsWith("d-") && !PERSONALITY_UTILS.includes(name));
3333
+ }
3334
+ function isCssClassNameChar(char) {
3335
+ const codePoint = char.charCodeAt(0);
3336
+ return char === "_" || char === "-" || codePoint >= 48 && codePoint <= 57 || codePoint >= 65 && codePoint <= 90 || codePoint >= 97 && codePoint <= 122;
3337
+ }
3338
+ function isCssWhitespace(char) {
3339
+ return char === " " || char === " " || char === "\n" || char === "\r" || char === "\f";
3317
3340
  }
3318
3341
  function resolveFocusAreas(reviewPack) {
3319
3342
  return reviewPack?.data.focusAreas?.length ? reviewPack.data.focusAreas : DEFAULT_FOCUS_AREAS;
@@ -3323,9 +3346,49 @@ function resolveSeverityFromChecks(reviewPack, fallback, checkIds) {
3323
3346
  return match?.severity ?? fallback;
3324
3347
  }
3325
3348
  function findHardcodedColors(code) {
3326
- const matches = code.match(/#[0-9a-fA-F]{3,8}\b|rgba?\([^)]*\)|hsla?\([^)]*\)/g) ?? [];
3349
+ const matches = [
3350
+ ...findHexColors(code),
3351
+ ...findColorFunctionCalls(code, "rgb("),
3352
+ ...findColorFunctionCalls(code, "rgba("),
3353
+ ...findColorFunctionCalls(code, "hsl("),
3354
+ ...findColorFunctionCalls(code, "hsla(")
3355
+ ];
3327
3356
  return [...new Set(matches)];
3328
3357
  }
3358
+ function findHexColors(code) {
3359
+ const matches = [];
3360
+ for (let index = 0; index < code.length; index += 1) {
3361
+ if (code[index] !== "#") continue;
3362
+ let cursor = index + 1;
3363
+ while (cursor < code.length && cursor - index <= 8 && isHexDigit(code[cursor])) {
3364
+ cursor += 1;
3365
+ }
3366
+ const length = cursor - index - 1;
3367
+ if (length >= 3 && length <= 8 && !isHexDigit(code[cursor] ?? "")) {
3368
+ matches.push(code.slice(index, cursor));
3369
+ }
3370
+ }
3371
+ return matches;
3372
+ }
3373
+ function findColorFunctionCalls(code, functionName) {
3374
+ const matches = [];
3375
+ const lowerCode = code.toLowerCase();
3376
+ for (let index = lowerCode.indexOf(functionName); index !== -1; index = lowerCode.indexOf(functionName, index + functionName.length)) {
3377
+ let cursor = index + functionName.length;
3378
+ const maxCursor = Math.min(code.length, cursor + 256);
3379
+ while (cursor < maxCursor && code[cursor] !== ")" && code[cursor] !== "\n" && code[cursor] !== "\r") {
3380
+ cursor += 1;
3381
+ }
3382
+ if (code[cursor] === ")") {
3383
+ matches.push(code.slice(index, cursor + 1));
3384
+ }
3385
+ }
3386
+ return matches;
3387
+ }
3388
+ function isHexDigit(char) {
3389
+ const codePoint = char.charCodeAt(0);
3390
+ return codePoint >= 48 && codePoint <= 57 || codePoint >= 65 && codePoint <= 70 || codePoint >= 97 && codePoint <= 102;
3391
+ }
3329
3392
  function findUtilityFrameworkSignals(code) {
3330
3393
  const matches = code.match(
3331
3394
  /\b(?:bg|text|border|shadow|rounded|px|py|mx|my|gap|grid-cols|col-span|row-span|sm|md|lg|xl|hover):[-\w/.[\]]+/g
@@ -4789,18 +4852,10 @@ function countAuthCallbackStateValidationSignals(code) {
4789
4852
  if (/\b(?:validateState|verifyState|assertState|compareState|checkState)\s*\(/i.test(code)) {
4790
4853
  count += 1;
4791
4854
  }
4792
- if (/\b(?:sessionStorage|localStorage)\.getItem\s*\(\s*['"`][^'"`]*(?:state|csrf)[^'"`]*['"`]\s*\)/i.test(
4793
- code
4794
- ) && /(?:state|providerState|callbackState|returnedState|expectedState|storedState|sessionState|oauthState|csrfState)\s*(?:===|==|!==|!=)/i.test(
4795
- code
4796
- )) {
4855
+ if (countStateLikeMemberCalls(code, ["sessionStorage", "localStorage"], ["getItem"]) > 0 && hasStateComparison(code)) {
4797
4856
  count += 1;
4798
4857
  }
4799
- if (/\b(?:cookies?|cookieStore)\.(?:get|getAll)\s*\(\s*['"`][^'"`]*(?:state|csrf)[^'"`]*['"`]\s*\)/i.test(
4800
- code
4801
- ) && /(?:state|providerState|callbackState|returnedState|expectedState|storedState|sessionState|oauthState|csrfState)\s*(?:===|==|!==|!=)/i.test(
4802
- code
4803
- )) {
4858
+ if (countStateLikeMemberCalls(code, ["cookies", "cookie", "cookieStore"], ["get", "getAll"]) > 0 && hasStateComparison(code)) {
4804
4859
  count += 1;
4805
4860
  }
4806
4861
  if (/\b(?:state|providerState|callbackState|returnedState)\b\s*(?:===|==|!==|!=)\s*\b(?:expectedState|storedState|sessionState|oauthState|csrfState)\b/i.test(
@@ -4812,21 +4867,142 @@ function countAuthCallbackStateValidationSignals(code) {
4812
4867
  }
4813
4868
  return count;
4814
4869
  }
4870
+ function hasStateComparison(code) {
4871
+ return /(?:state|providerState|callbackState|returnedState|expectedState|storedState|sessionState|oauthState|csrfState)\s*(?:===|==|!==|!=)/i.test(
4872
+ code
4873
+ );
4874
+ }
4875
+ function countStateLikeMemberCalls(code, receivers, methods) {
4876
+ const lowerCode = code.toLowerCase();
4877
+ let count = 0;
4878
+ for (const receiver of receivers) {
4879
+ const receiverLower = receiver.toLowerCase();
4880
+ for (let index = lowerCode.indexOf(receiverLower); index !== -1; index = lowerCode.indexOf(receiverLower, index + receiverLower.length)) {
4881
+ let cursor = index + receiverLower.length;
4882
+ cursor = skipInlineWhitespace(lowerCode, cursor);
4883
+ if (lowerCode[cursor] !== ".") continue;
4884
+ cursor = skipInlineWhitespace(lowerCode, cursor + 1);
4885
+ for (const method of methods) {
4886
+ const methodLower = method.toLowerCase();
4887
+ if (!lowerCode.startsWith(methodLower, cursor)) continue;
4888
+ const afterMethod = skipInlineWhitespace(lowerCode, cursor + methodLower.length);
4889
+ if (lowerCode[afterMethod] !== "(") continue;
4890
+ if (callHasStateLikeStringArgument(code, afterMethod)) {
4891
+ count += 1;
4892
+ }
4893
+ }
4894
+ }
4895
+ }
4896
+ return count;
4897
+ }
4898
+ function countStateLikeFunctionCalls(code, names) {
4899
+ const lowerCode = code.toLowerCase();
4900
+ let count = 0;
4901
+ for (const name of names) {
4902
+ const nameLower = name.toLowerCase();
4903
+ for (let index = lowerCode.indexOf(nameLower); index !== -1; index = lowerCode.indexOf(nameLower, index + nameLower.length)) {
4904
+ const afterName = skipInlineWhitespace(lowerCode, index + nameLower.length);
4905
+ if (lowerCode[afterName] === "(" && callHasStateLikeStringArgument(code, afterName)) {
4906
+ count += 1;
4907
+ }
4908
+ }
4909
+ }
4910
+ return count;
4911
+ }
4912
+ function skipInlineWhitespace(value, index) {
4913
+ let cursor = index;
4914
+ while (cursor < value.length) {
4915
+ const char = value[cursor];
4916
+ if (char !== " " && char !== " " && char !== "\n" && char !== "\r") break;
4917
+ cursor += 1;
4918
+ }
4919
+ return cursor;
4920
+ }
4921
+ function callHasStateLikeStringArgument(code, openParenIndex) {
4922
+ const closeParenIndex = findCallCloseParen(code, openParenIndex, 240);
4923
+ if (closeParenIndex === -1) return false;
4924
+ return containsStateLikeStringLiteral(code.slice(openParenIndex + 1, closeParenIndex));
4925
+ }
4926
+ function findCallCloseParen(code, openParenIndex, maxLength) {
4927
+ const maxIndex = Math.min(code.length, openParenIndex + maxLength);
4928
+ let quote = null;
4929
+ let escaped = false;
4930
+ for (let index = openParenIndex + 1; index < maxIndex; index += 1) {
4931
+ const char = code[index];
4932
+ if (quote) {
4933
+ if (escaped) {
4934
+ escaped = false;
4935
+ } else if (char === "\\") {
4936
+ escaped = true;
4937
+ } else if (char === quote) {
4938
+ quote = null;
4939
+ }
4940
+ continue;
4941
+ }
4942
+ if (char === "'" || char === '"' || char === "`") {
4943
+ quote = char;
4944
+ } else if (char === ")") {
4945
+ return index;
4946
+ }
4947
+ }
4948
+ return -1;
4949
+ }
4950
+ function containsStateLikeStringLiteral(value) {
4951
+ let quote = null;
4952
+ let literal = "";
4953
+ let escaped = false;
4954
+ for (const char of value) {
4955
+ if (!quote) {
4956
+ if (char === "'" || char === '"' || char === "`") {
4957
+ quote = char;
4958
+ literal = "";
4959
+ }
4960
+ continue;
4961
+ }
4962
+ if (escaped) {
4963
+ literal += char;
4964
+ escaped = false;
4965
+ } else if (char === "\\") {
4966
+ escaped = true;
4967
+ } else if (char === quote) {
4968
+ const lowerLiteral = literal.toLowerCase();
4969
+ if (lowerLiteral.includes("state") || lowerLiteral.includes("csrf")) return true;
4970
+ quote = null;
4971
+ } else {
4972
+ literal += char;
4973
+ }
4974
+ }
4975
+ return false;
4976
+ }
4815
4977
  function countAuthCallbackStateStorageSignals(code) {
4816
- const patterns = [
4817
- /\b(?:sessionStorage|localStorage)\.getItem\s*\(\s*['"`][^'"`]*(?:state|csrf)[^'"`]*['"`]\s*\)/gi,
4818
- /\b(?:cookies?|cookieStore)\.(?:get|getAll)\s*\(\s*['"`][^'"`]*(?:state|csrf)[^'"`]*['"`]\s*\)/gi
4819
- ];
4820
- return patterns.reduce((total, pattern) => total + (code.match(pattern)?.length ?? 0), 0);
4978
+ return countStateLikeMemberCalls(code, ["sessionStorage", "localStorage"], ["getItem"]) + countStateLikeMemberCalls(code, ["cookies", "cookie", "cookieStore"], ["get", "getAll"]);
4821
4979
  }
4822
4980
  function countAuthCallbackStateStorageClearSignals(code) {
4823
- const patterns = [
4824
- /\b(?:sessionStorage|localStorage)\.removeItem\s*\(\s*['"`][^'"`]*(?:state|csrf)[^'"`]*['"`]\s*\)/gi,
4825
- /\b(?:deleteCookie|clearCookie|removeCookie)\s*\(\s*['"`][^'"`]*(?:state|csrf)[^'"`]*['"`]\s*\)/gi,
4826
- /\b(?:cookies?|cookieStore)\.(?:delete|remove)\s*\(\s*['"`][^'"`]*(?:state|csrf)[^'"`]*['"`]\s*\)/gi,
4827
- /\bdocument\.cookie\s*=\s*[^;\n]*(?:state|csrf)[^;\n]*(?:expires\s*=\s*Thu,\s*01 Jan 1970|max-age\s*=\s*0)/gi
4828
- ];
4829
- return patterns.reduce((total, pattern) => total + (code.match(pattern)?.length ?? 0), 0);
4981
+ return countStateLikeMemberCalls(code, ["sessionStorage", "localStorage"], ["removeItem"]) + countStateLikeFunctionCalls(code, ["deleteCookie", "clearCookie", "removeCookie"]) + countStateLikeMemberCalls(code, ["cookies", "cookie", "cookieStore"], ["delete", "remove"]) + countDocumentCookieStateClears(code);
4982
+ }
4983
+ function countDocumentCookieStateClears(code) {
4984
+ const lowerCode = code.toLowerCase();
4985
+ let count = 0;
4986
+ for (let index = lowerCode.indexOf("document.cookie"); index !== -1; index = lowerCode.indexOf("document.cookie", index + "document.cookie".length)) {
4987
+ const assignmentIndex = lowerCode.indexOf("=", index + "document.cookie".length);
4988
+ if (assignmentIndex === -1 || assignmentIndex - index > 48) continue;
4989
+ const endIndex = findCookieAssignmentEnd(lowerCode, assignmentIndex + 1);
4990
+ const assignment = lowerCode.slice(assignmentIndex + 1, endIndex);
4991
+ const mentionsStateToken = assignment.includes("state") || assignment.includes("csrf");
4992
+ const expiresImmediately = assignment.includes("max-age=0") || assignment.includes("max-age = 0") || assignment.includes("thu, 01 jan 1970");
4993
+ if (mentionsStateToken && expiresImmediately) {
4994
+ count += 1;
4995
+ }
4996
+ }
4997
+ return count;
4998
+ }
4999
+ function findCookieAssignmentEnd(code, startIndex) {
5000
+ const maxIndex = Math.min(code.length, startIndex + 320);
5001
+ for (let index = startIndex; index < maxIndex; index += 1) {
5002
+ const char = code[index];
5003
+ if (char === "\n" || char === "\r") return index;
5004
+ }
5005
+ return maxIndex;
4830
5006
  }
4831
5007
  function countAuthCallbackUrlScrubSignals(code) {
4832
5008
  const patterns = [
@@ -8731,12 +8907,34 @@ function countHardcodedSecretSignals(code) {
8731
8907
  return patterns.reduce((count, pattern) => count + (code.match(pattern)?.length ?? 0), 0);
8732
8908
  }
8733
8909
  function countClientSecretEnvReferenceSignals(code) {
8734
- const patterns = [
8735
- /\bimport\.meta\.env\.[A-Z0-9_]*(?:SERVICE_ROLE|SECRET|PRIVATE_KEY)[A-Z0-9_]*\b/g,
8736
- /\bprocess\.env\.NEXT_PUBLIC_[A-Z0-9_]*(?:SERVICE_ROLE|SECRET|PRIVATE_KEY)[A-Z0-9_]*\b/g
8737
- ];
8738
- const useClientCount = /^\s*['"]use client['"]/m.test(code) ? code.match(/\bprocess\.env\.[A-Z0-9_]*(?:SERVICE_ROLE|SECRET|PRIVATE_KEY)[A-Z0-9_]*\b/g)?.length ?? 0 : 0;
8739
- return patterns.reduce((count, pattern) => count + (code.match(pattern)?.length ?? 0), 0) + useClientCount;
8910
+ const useClientCount = /^\s*['"]use client['"]/m.test(code) ? countSensitiveEnvReferences(code, ["process.env."]) : 0;
8911
+ return countSensitiveEnvReferences(code, ["import.meta.env.", "process.env.NEXT_PUBLIC_"]) + useClientCount;
8912
+ }
8913
+ function countSensitiveEnvReferences(code, prefixes) {
8914
+ let count = 0;
8915
+ for (const prefix of prefixes) {
8916
+ for (let index = code.indexOf(prefix); index !== -1; index = code.indexOf(prefix, index + prefix.length)) {
8917
+ const envName = readEnvIdentifier(code, index + prefix.length);
8918
+ if (envName && isSensitiveEnvName(envName)) {
8919
+ count += 1;
8920
+ }
8921
+ }
8922
+ }
8923
+ return count;
8924
+ }
8925
+ function readEnvIdentifier(code, startIndex) {
8926
+ let cursor = startIndex;
8927
+ while (cursor < code.length && isEnvIdentifierChar(code[cursor])) {
8928
+ cursor += 1;
8929
+ }
8930
+ return code.slice(startIndex, cursor);
8931
+ }
8932
+ function isEnvIdentifierChar(char) {
8933
+ const codePoint = char.charCodeAt(0);
8934
+ return char === "_" || codePoint >= 48 && codePoint <= 57 || codePoint >= 65 && codePoint <= 90;
8935
+ }
8936
+ function isSensitiveEnvName(envName) {
8937
+ return envName.includes("SERVICE_ROLE") || envName.includes("SECRET") || envName.includes("PRIVATE_KEY");
8740
8938
  }
8741
8939
  function countLocalhostEndpointSignals(code) {
8742
8940
  return code.match(