@cejel/cejel 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -278,9 +278,11 @@ separately produced structured report. We neither score you on evidence the publ
278
278
  cannot collect nor award ourselves points for evidence you cannot contest.
279
279
 
280
280
  **Where we were wrong.** Calibrating this rubric against real repositories — and running the
281
- board itself like a stranger would — surfaced seven errors in our own tool, all found before
282
- (or, in one case, on) publication day, and all fixed. We list them because a scoring tool
283
- that has never been wrong is a scoring tool that has not been checked:
281
+ board itself like a stranger would — surfaced the errors below in our own tool, every one found
282
+ by us and every one fixed. The list is its own count; we do not keep a tally in this sentence,
283
+ because a number typed by hand beside a list that grows is exactly the kind of unchecked claim
284
+ this tool exists to catch. We publish them because a scoring tool that has never been wrong is a
285
+ scoring tool that has not been checked:
284
286
 
285
287
  - **Django, scored a false critical on dependency hygiene.** We applied application-shaped
286
288
  expectations to a library. Archetype now gates the dimension.
@@ -315,6 +317,23 @@ that has never been wrong is a scoring tool that has not been checked:
315
317
  - **Version 0.1.1 rejected `--help` and `--version` as unknown flags, and read `-h` as a
316
318
  directory path.** Version 0.1.2 handles both aliases before positional-path parsing and
317
319
  derives its printed version from the package manifest.
320
+ - **We reported a timing side-channel in our own test fixtures.** Pointed at its own
321
+ repository, Cejel's secret-handling dimension flagged a "non-constant-time secret
322
+ comparison" — in string literals inside a test file, the deliberately-insecure example code
323
+ that exists to prove the detector fires. It read a *file*, not a comparison, and reported a
324
+ vulnerability in the test written to catch that vulnerability. Two sub-rules of the same
325
+ dimension held two different private notions of what "production code" meant, and the one
326
+ that had none was the louder. Version 0.1.3 gives every rule one shared, derived answer to
327
+ that question, and reports the real line of a real match or nothing at all.
328
+ - **We fixed the COBOL false-verdict, shipped it, and it was still broken.** An earlier release
329
+ corrected a repository written in a language we cannot read being scored anyway. The fixture
330
+ that proved the fix contained COBOL and nothing else. The first real COBOL repository we
331
+ pointed it at — AWS's mainframe credit-card demo — carried nine incidental shell scripts
332
+ among 329 files, and those nine were enough to short-circuit the abstention path entirely:
333
+ a 99%-COBOL codebase was scored on its deploy scripts and drew a confident `0.0`,
334
+ "Unverified". Every real legacy repository has a deploy script, so the fix that passed every
335
+ test was unreachable for essentially all of them. Recognised source must now be *dominant*,
336
+ not merely present. **A fixture cleaner than reality proves nothing.**
318
337
 
319
338
  Every one was a trust failure produced by *us* — false alarms about other people's code,
320
339
  silent omissions, inconsistent presentation, or a home-only scoring path — and we would
@@ -5446,7 +5446,10 @@ var WitanEvidencePointerSchema = external_exports.object({
5446
5446
  label: external_exports.string().min(1).max(180),
5447
5447
  url: external_exports.string().url().optional(),
5448
5448
  path: external_exports.string().min(1).max(700).optional(),
5449
- line: external_exports.number().int().positive().optional(),
5449
+ // Explicit null means the finding is file-scoped and no real match position was measured —
5450
+ // never a fabricated line (e.g. `1`) standing in for a position the detector never found
5451
+ // (goal_cejel_a2_one_notion_of_production_code_2026-07-13).
5452
+ line: external_exports.number().int().positive().nullable().optional(),
5450
5453
  contentHash: external_exports.string().min(8).max(160).optional(),
5451
5454
  capturedAt: external_exports.string().datetime({ offset: true }).optional()
5452
5455
  }).strict().refine((value) => Boolean(value.url ?? value.path ?? value.contentHash), {
@@ -6600,6 +6603,7 @@ var BUNDLED_BINARY_EXTENSION_PATTERN = /\.(vsix|zip|tgz|whl|exe|dmg|pkg|msi|jar|
6600
6603
  var DOCS_OR_MEDIA_EXTENSION_PATTERN = /\.(md|mdx|rst|png|jpe?g|gif|svg|webp|ico|pdf)$/i;
6601
6604
  var KNOWN_NON_SOURCE_EXTENSION_PATTERN = /\.(json|jsonc|json5|ya?ml|toml|txt|lock|xml|csv|tsv|ini|cfg|conf|properties|env|sum|mod|graphql|gql|proto|editorconfig|gitignore|gitattributes|npmrc|nvmrc|dockerignore|log|txt\.license)$/i;
6602
6605
  var INGEST_POINTER = "To assess a closed/bundled tool, ingest its scanner output via --ingest <sarif|scorecard>.";
6606
+ var SOURCE_DOMINANCE_RATIO_THRESHOLD = 0.2;
6603
6607
  function classifyRepoArchetype(repoFiles) {
6604
6608
  const totalFileCount = repoFiles.length;
6605
6609
  const sourceFileCount = repoFiles.filter((file) => SOURCE_EXTENSION_PATTERN.test(file)).length;
@@ -6611,57 +6615,83 @@ function classifyRepoArchetype(repoFiles) {
6611
6615
  insufficientSourceReason: "Repository has no tracked files \u2014 there is nothing to certify."
6612
6616
  };
6613
6617
  }
6614
- if (sourceFileCount > 0) {
6618
+ const unrecognisedSourceFiles = collectUnrecognisedSourceFiles(repoFiles);
6619
+ if (sourceFileCount === 0) {
6620
+ const binaryFiles = repoFiles.filter((file) => BUNDLED_BINARY_EXTENSION_PATTERN.test(file));
6621
+ if (binaryFiles.length > 0) {
6622
+ const sample = binaryFiles[0];
6623
+ return {
6624
+ archetype: "binary_only",
6625
+ sourceFileCount,
6626
+ totalFileCount,
6627
+ insufficientSourceReason: `${sourceFileCount} source file(s) found among ${totalFileCount} tracked file(s); repo appears to be a binary/bundled-distribution tree (e.g. ${sample ? basename(sample) : "a packaged artifact"}) \u2014 cejel rates source, not binaries. ${INGEST_POINTER}`
6628
+ };
6629
+ }
6630
+ const docsFiles = repoFiles.filter((file) => DOCS_OR_MEDIA_EXTENSION_PATTERN.test(file));
6631
+ if (docsFiles.length > 0 && docsFiles.length === totalFileCount) {
6632
+ return {
6633
+ archetype: "docs_only",
6634
+ sourceFileCount,
6635
+ totalFileCount,
6636
+ insufficientSourceReason: `${sourceFileCount} source file(s) found among ${totalFileCount} tracked file(s); repo appears to be a docs/distribution tree (README/markdown/media), not a source tree \u2014 cejel rates source, not docs. ${INGEST_POINTER}`
6637
+ };
6638
+ }
6639
+ if (unrecognisedSourceFiles.length > 0) {
6640
+ return unrecognisedEcosystemResult(sourceFileCount, totalFileCount, unrecognisedSourceFiles);
6641
+ }
6642
+ return { archetype: "source", sourceFileCount, totalFileCount };
6643
+ }
6644
+ if (unrecognisedSourceFiles.length === 0) {
6615
6645
  return {
6616
6646
  archetype: isLikelyMonorepo(repoFiles) ? "monorepo" : "source",
6617
6647
  sourceFileCount,
6618
6648
  totalFileCount
6619
6649
  };
6620
6650
  }
6621
- const binaryFiles = repoFiles.filter((file) => BUNDLED_BINARY_EXTENSION_PATTERN.test(file));
6622
- if (binaryFiles.length > 0) {
6623
- const sample = binaryFiles[0];
6651
+ const dominanceRatio = sourceFileCount / (sourceFileCount + unrecognisedSourceFiles.length);
6652
+ if (dominanceRatio >= SOURCE_DOMINANCE_RATIO_THRESHOLD) {
6624
6653
  return {
6625
- archetype: "binary_only",
6626
- sourceFileCount,
6627
- totalFileCount,
6628
- insufficientSourceReason: `${sourceFileCount} source file(s) found among ${totalFileCount} tracked file(s); repo appears to be a binary/bundled-distribution tree (e.g. ${sample ? basename(sample) : "a packaged artifact"}) \u2014 cejel rates source, not binaries. ${INGEST_POINTER}`
6629
- };
6630
- }
6631
- const docsFiles = repoFiles.filter((file) => DOCS_OR_MEDIA_EXTENSION_PATTERN.test(file));
6632
- if (docsFiles.length > 0 && docsFiles.length === totalFileCount) {
6633
- return {
6634
- archetype: "docs_only",
6635
- sourceFileCount,
6636
- totalFileCount,
6637
- insufficientSourceReason: `${sourceFileCount} source file(s) found among ${totalFileCount} tracked file(s); repo appears to be a docs/distribution tree (README/markdown/media), not a source tree \u2014 cejel rates source, not docs. ${INGEST_POINTER}`
6638
- };
6639
- }
6640
- const unrecognisedExtensions = collectUnrecognisedSourceExtensions(repoFiles);
6641
- if (unrecognisedExtensions.length > 0) {
6642
- const sample = unrecognisedExtensions.slice(0, 6).join(", ");
6643
- return {
6644
- archetype: "unrecognised_ecosystem",
6654
+ archetype: isLikelyMonorepo(repoFiles) ? "monorepo" : "source",
6645
6655
  sourceFileCount,
6646
- totalFileCount,
6647
- insufficientSourceReason: `Cejel does not yet read this repository's source language(s) (${sample}) \u2014 0 of ${totalFileCount} tracked file(s) matched a recognised source extension. Cejel abstains from a verdict rather than score a language it cannot parse; the Criterion Profile and Measured coverage below show exactly which dimensions were and were not measured. ${INGEST_POINTER}`
6656
+ totalFileCount
6648
6657
  };
6649
6658
  }
6650
- return { archetype: "source", sourceFileCount, totalFileCount };
6659
+ return unrecognisedEcosystemResult(sourceFileCount, totalFileCount, unrecognisedSourceFiles);
6651
6660
  }
6652
- function collectUnrecognisedSourceExtensions(repoFiles) {
6653
- const found = /* @__PURE__ */ new Set();
6654
- for (const file of repoFiles) {
6661
+ function collectUnrecognisedSourceFiles(repoFiles) {
6662
+ return repoFiles.filter((file) => {
6655
6663
  const base = basename(file);
6656
- const match = /\.([a-zA-Z0-9_]+)$/.exec(base);
6664
+ if (!/\.([a-zA-Z0-9_]+)$/.test(base)) return false;
6665
+ if (KNOWN_NON_SOURCE_EXTENSION_PATTERN.test(base)) return false;
6666
+ if (DOCS_OR_MEDIA_EXTENSION_PATTERN.test(base)) return false;
6667
+ if (BUNDLED_BINARY_EXTENSION_PATTERN.test(base)) return false;
6668
+ if (SOURCE_EXTENSION_PATTERN.test(base)) return false;
6669
+ return true;
6670
+ });
6671
+ }
6672
+ function extensionsByFileCountDescending(files) {
6673
+ const counts = /* @__PURE__ */ new Map();
6674
+ for (const file of files) {
6675
+ const match = /\.([a-zA-Z0-9_]+)$/.exec(basename(file));
6657
6676
  const ext = match?.[1];
6658
6677
  if (!ext) continue;
6659
- if (KNOWN_NON_SOURCE_EXTENSION_PATTERN.test(base)) continue;
6660
- if (DOCS_OR_MEDIA_EXTENSION_PATTERN.test(base)) continue;
6661
- if (BUNDLED_BINARY_EXTENSION_PATTERN.test(base)) continue;
6662
- found.add(`.${ext.toLowerCase()}`);
6678
+ const key = `.${ext.toLowerCase()}`;
6679
+ counts.set(key, (counts.get(key) ?? 0) + 1);
6663
6680
  }
6664
- return [...found].sort();
6681
+ return [...counts.entries()].sort(([extA, countA], [extB, countB]) => countB - countA || extA.localeCompare(extB)).map(([ext]) => ext);
6682
+ }
6683
+ function unrecognisedEcosystemResult(sourceFileCount, totalFileCount, unrecognisedSourceFiles) {
6684
+ const sample = extensionsByFileCountDescending(unrecognisedSourceFiles).slice(0, 6).join(", ");
6685
+ const candidateCount = sourceFileCount + unrecognisedSourceFiles.length;
6686
+ const dominancePct = candidateCount > 0 ? (sourceFileCount / candidateCount * 100).toFixed(1) : "0.0";
6687
+ const thresholdPct = (SOURCE_DOMINANCE_RATIO_THRESHOLD * 100).toFixed(0);
6688
+ const measuredClause = sourceFileCount > 0 ? `${sourceFileCount} of ${candidateCount} source-shaped file(s) (${dominancePct}%) are in a language Cejel reads \u2014 below the ${thresholdPct}% dominance threshold a score would need to be meaningful (${totalFileCount} tracked files in total; manifests, lockfiles, docs, media and bundled binaries are excluded from both sides of the ratio)` : `0 of ${totalFileCount} tracked file(s) matched a recognised source extension`;
6689
+ return {
6690
+ archetype: "unrecognised_ecosystem",
6691
+ sourceFileCount,
6692
+ totalFileCount,
6693
+ insufficientSourceReason: `Cejel does not yet read this repository's dominant source language(s) (${sample}) \u2014 ${measuredClause}. Cejel abstains from a verdict rather than score a repository whose recognised source is incidental rather than dominant; the Criterion Profile and Measured coverage below show exactly which dimensions were and were not measured. ${INGEST_POINTER}`
6694
+ };
6665
6695
  }
6666
6696
  function isLikelyMonorepo(repoFiles) {
6667
6697
  const packageRoots = new Set(
@@ -7106,7 +7136,8 @@ function collectCryptoHygieneEvidence(repoPath, implFiles) {
7106
7136
  const findings = [];
7107
7137
  let hasSurface = false;
7108
7138
  let insecureFound = false;
7109
- const timingSafeFile = implFiles.find(
7139
+ const productionImplFiles = implFiles.filter(isProductionSourcePath);
7140
+ const timingSafeFile = productionImplFiles.find(
7110
7141
  (file) => fileContains(repoPath, file, TIMING_SAFE_COMPARE_PATTERN)
7111
7142
  );
7112
7143
  if (timingSafeFile) {
@@ -7120,7 +7151,7 @@ function collectCryptoHygieneEvidence(repoPath, implFiles) {
7120
7151
  )
7121
7152
  );
7122
7153
  }
7123
- const canonicalSignFile = implFiles.find(
7154
+ const canonicalSignFile = productionImplFiles.find(
7124
7155
  (file) => fileContains(repoPath, file, CANONICAL_SERIALIZE_PATTERN) && fileContains(repoPath, file, SIGN_OR_HMAC_CALL_PATTERN)
7125
7156
  );
7126
7157
  if (canonicalSignFile) {
@@ -7134,20 +7165,31 @@ function collectCryptoHygieneEvidence(repoPath, implFiles) {
7134
7165
  )
7135
7166
  );
7136
7167
  }
7137
- const insecureCompareFile = implFiles.find(
7138
- (file) => fileContains(repoPath, file, INSECURE_SECRET_COMPARE_PATTERN)
7139
- );
7168
+ let insecureCompareFile = null;
7169
+ let insecureCompareLine = null;
7170
+ for (const file of implFiles) {
7171
+ const line = findInsecureSecretCompareLine(repoPath, file);
7172
+ if (line != null) {
7173
+ insecureCompareFile = file;
7174
+ insecureCompareLine = line;
7175
+ break;
7176
+ }
7177
+ }
7140
7178
  if (insecureCompareFile) {
7141
- hasSurface = true;
7142
- insecureFound = true;
7179
+ const isTestPath = isTestOrFixturePath(insecureCompareFile);
7180
+ if (!isTestPath) {
7181
+ hasSurface = true;
7182
+ insecureFound = true;
7183
+ }
7143
7184
  findings.push({
7144
- severity: "warning",
7145
- summary: "A secret/HMAC/signature value appears compared with a plain equality operator instead of a constant-time comparison \u2014 potential timing side-channel.",
7146
- evidence: evidenceForRelative(
7185
+ severity: isTestPath ? "info" : "warning",
7186
+ summary: isTestPath ? `A secret/HMAC/signature comparison via plain equality appears in a test/fixture file (${insecureCompareFile}) \u2014 likely a test assertion, not a production timing leak; verify.` : "A secret/HMAC/signature value appears compared with a plain equality operator instead of a constant-time comparison \u2014 potential timing side-channel.",
7187
+ evidence: evidenceForRelativeAtLine(
7147
7188
  repoPath,
7148
7189
  insecureCompareFile,
7149
7190
  "artifact",
7150
- "Non-constant-time secret comparison"
7191
+ "Non-constant-time secret comparison",
7192
+ insecureCompareLine
7151
7193
  )
7152
7194
  });
7153
7195
  }
@@ -7155,16 +7197,25 @@ function collectCryptoHygieneEvidence(repoPath, implFiles) {
7155
7197
  (file) => fileContains(repoPath, file, UNSORTED_JSON_SIGN_PATTERN) && !fileContains(repoPath, file, CANONICAL_SERIALIZE_PATTERN)
7156
7198
  );
7157
7199
  if (unsortedSignFile) {
7158
- hasSurface = true;
7159
- insecureFound = true;
7200
+ const isTestPath = isTestOrFixturePath(unsortedSignFile);
7201
+ if (!isTestPath) {
7202
+ hasSurface = true;
7203
+ insecureFound = true;
7204
+ }
7205
+ const unsortedSignLine = findFirstMatchingLine(
7206
+ repoPath,
7207
+ unsortedSignFile,
7208
+ UNSORTED_JSON_SIGN_PATTERN
7209
+ );
7160
7210
  findings.push({
7161
- severity: "warning",
7162
- summary: "A signature/HMAC appears computed directly over JSON.stringify output with no canonical key ordering in the same file \u2014 signatures may not verify across equivalent payloads.",
7163
- evidence: evidenceForRelative(
7211
+ severity: isTestPath ? "info" : "warning",
7212
+ summary: isTestPath ? `Signing over unsorted JSON serialization appears in a test/fixture file (${unsortedSignFile}) \u2014 likely a test assertion, not a production signing gap; verify.` : "A signature/HMAC appears computed directly over JSON.stringify output with no canonical key ordering in the same file \u2014 signatures may not verify across equivalent payloads.",
7213
+ evidence: evidenceForRelativeAtLine(
7164
7214
  repoPath,
7165
7215
  unsortedSignFile,
7166
7216
  "artifact",
7167
- "Signing over unsorted JSON serialization"
7217
+ "Signing over unsorted JSON serialization",
7218
+ unsortedSignLine
7168
7219
  )
7169
7220
  });
7170
7221
  }
@@ -7880,11 +7931,20 @@ function collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles) {
7880
7931
  var CPP_TEST_FRAMEWORK_PATTERN = /<gtest\/gtest\.h>|<catch2\/|<doctest\/|<boost\/test\/|"gtest\/gtest\.h"|"catch2\//;
7881
7932
  var CPP_TEST_MACRO_PATTERN = /\bTEST\s*\(|\bTEST_F\s*\(|\bTEST_CASE\s*\(|\bSECTION\s*\(|\bCATCH_|BOOST_AUTO_TEST_CASE\b|BOOST_FIXTURE_TEST_CASE\b/;
7882
7933
  var CPP_CONTENT_SCAN_LIMIT = 150;
7883
- var TEST_OR_FIXTURE_DIR_PATTERN = /(^|\/)(__tests__|__mocks__|__fixtures__|tests|test|fixtures|testdata|e2e|cypress)(\/|$)/i;
7884
- var TEST_OR_FIXTURE_FILE_PATTERN = /\.(test|spec|stories)\.[^/]+$/i;
7934
+ var NON_PRODUCTION_DIR_PATTERN = /(^|\/)(__tests__|__mocks__|__fixtures__|tests?|fixtures?|testdata|e2e|cypress|examples?|samples?|demos?)(\/|$)/i;
7935
+ var NON_PRODUCTION_FILE_SUFFIX_PATTERN = /\.(test|spec|stories)\.[^/]+$/i;
7885
7936
  function isTestOrFixturePath(path) {
7886
- return TEST_OR_FIXTURE_DIR_PATTERN.test(path) || TEST_OR_FIXTURE_FILE_PATTERN.test(path);
7937
+ return NON_PRODUCTION_DIR_PATTERN.test(path) || NON_PRODUCTION_FILE_SUFFIX_PATTERN.test(path);
7938
+ }
7939
+ function isProductionSourcePath(path) {
7940
+ return !isTestOrFixturePath(path);
7887
7941
  }
7942
+ function isTestFile(file) {
7943
+ return /(^|\/)(__tests__|test|tests|spec)\/.*\.(?:[cm]?[jt]sx?|py|go|rs|java|rb|cpp|cc|cxx)$/.test(
7944
+ file
7945
+ ) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(file) || /(^|\/)test_.*\.py$/.test(file) || /_test\.go$/.test(file) || /(^|\/)test_.*\.(cpp|cc|cxx)$/.test(file) || /_(test|tests)\.(cpp|cc|cxx)$/.test(file) || /Tests?\.(java|kt)$/.test(file) || /(_test|_spec)\.(rs|rb|php|swift|kt)$/.test(file);
7946
+ }
7947
+ var NAME_SHAPED_TEST_FILE_PATTERN = /\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)__tests__\/|(^|\/)test_[^/]*\.py$|(^|\/)tests?\.py$|_test\.go$|(^|\/)test_[^/]*\.(cpp|cc|cxx)$|_(test|tests)\.(cpp|cc|cxx)$|Tests?\.(java|kt)$|(_test|_spec)\.(rs|rb|php|swift|kt)$/;
7888
7948
  var TEST_RUNNER_PATTERN = /\b(vitest|jest|mocha|ava|tap|pytest|go test|cargo test|rspec|phpunit|gradle test|mvn test)\b/i;
7889
7949
  var CI_TEST_COMMAND_PATTERN = /\b(pytest|go\s+test|cargo\s+test|mvn\s+test|gradle\s+test|ctest|make\s+test|jest|vitest|mocha|rspec|phpunit|pnpm\s+test|npm\s+test|yarn\s+test|node\s+--test|node:test)\b/i;
7890
7950
  var CI_LINT_COMMAND_PATTERN = /\b(eslint|pylint|flake8|black|ruff)\b/i;
@@ -7917,7 +7977,28 @@ var HEAVY_TEST_DEPENDENCY_NAME_PATTERN = /^(?:jest|mocha|ava|tape|jasmine|karma)
7917
7977
  var TIMING_SAFE_COMPARE_PATTERN = /\btiming[-_]?safe[-_]?(?:equal|compare)\b|crypto\.timingSafeEqual\s*\(/i;
7918
7978
  var CANONICAL_SERIALIZE_PATTERN = /\bcanonicalize\s*\(|\bcanonicalJson\b|\bstableStringify\s*\(|json-stable-stringify|safe-stable-stringify|\bsortKeys\s*\(/i;
7919
7979
  var SIGN_OR_HMAC_CALL_PATTERN = /\bcreateHmac\s*\(|\bcreateSign\s*\(|\.sign\s*\(/;
7920
- var INSECURE_SECRET_COMPARE_PATTERN = /\b\w*(?:hmac|signature|\bmac|digest)\w*\s*(?:===|!==)\s*(?!['"`]|undefined\b|null\b)\w/i;
7980
+ var INSECURE_SECRET_COMPARE_LINE_PATTERN = /\b\w*(?:hmac|signature|\bmac|digest)\w*\s*(===|!==)\s*(?!['"`]|undefined\b|null\b)([\w.]+(?:\s*[-+]\s*\d+)?)/i;
7981
+ var NUMERIC_SHAPED_OPERAND_PATTERN = /^-?\d+$|\.(?:length|len|size|count)\b|(?:length|index|idx|offset|count|size|len)$/i;
7982
+ function lineHasInsecureSecretCompare(line) {
7983
+ const match = INSECURE_SECRET_COMPARE_LINE_PATTERN.exec(line);
7984
+ const operand = match?.[2];
7985
+ if (!operand) return false;
7986
+ return !NUMERIC_SHAPED_OPERAND_PATTERN.test(operand.trim());
7987
+ }
7988
+ function findInsecureSecretCompareLine(repoPath, file) {
7989
+ const fullPath = join3(repoPath, file);
7990
+ if (!isRegularFile(fullPath)) return null;
7991
+ const lines = readFileSync6(fullPath, "utf8").split("\n");
7992
+ const index = lines.findIndex((line) => lineHasInsecureSecretCompare(line));
7993
+ return index === -1 ? null : index + 1;
7994
+ }
7995
+ function findFirstMatchingLine(repoPath, file, pattern) {
7996
+ const fullPath = join3(repoPath, file);
7997
+ if (!isRegularFile(fullPath)) return null;
7998
+ const lines = readFileSync6(fullPath, "utf8").split("\n");
7999
+ const index = lines.findIndex((line) => pattern.test(line));
8000
+ return index === -1 ? null : index + 1;
8001
+ }
7921
8002
  var UNSORTED_JSON_SIGN_PATTERN = /\.update\s*\(\s*JSON\.stringify\s*\(|\.sign\s*\([^)]*JSON\.stringify\s*\(/;
7922
8003
  var KILL_SWITCH_NAME_FRAGMENT = "kill[_-]?switch|emergency[_-]?stop|circuit[_-]?breaker|safety[_-]?(?:toggle|gate|switch)|governance[_-]?(?:toggle|gate)";
7923
8004
  var KILL_SWITCH_NAME_PATTERN = new RegExp(`\\b(?:${KILL_SWITCH_NAME_FRAGMENT})\\b`, "i");
@@ -8071,11 +8152,6 @@ function shouldSkipDir(name) {
8071
8152
  function isHardExcludedPath(path) {
8072
8153
  return HARD_EXCLUDED_PATH_PATTERN.test(path);
8073
8154
  }
8074
- function isTestFile(file) {
8075
- return /(^|\/)(__tests__|test|tests|spec)\/.*\.(?:[cm]?[jt]sx?|py|go|rs|java|rb|cpp|cc|cxx)$/.test(
8076
- file
8077
- ) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(file) || /(^|\/)test_.*\.py$/.test(file) || /_test\.go$/.test(file) || /(^|\/)test_.*\.(cpp|cc|cxx)$/.test(file) || /_(test|tests)\.(cpp|cc|cxx)$/.test(file) || /Tests?\.(java|kt)$/.test(file) || /(_test|_spec)\.(rs|rb|php|swift|kt)$/.test(file);
8078
- }
8079
8155
  function collectContentBasedCppTestFiles(repoPath, repoFiles) {
8080
8156
  const candidates = repoFiles.filter((file) => /\.(cpp|cc|cxx)$/.test(file) && !isTestFile(file)).slice(0, CPP_CONTENT_SCAN_LIMIT);
8081
8157
  return candidates.filter((file) => {
@@ -8095,7 +8171,6 @@ var JS_TEST_ASSERTION_PATTERN = /(expect\(|assert\.|should\.|toEqual\(|toBe\(|as
8095
8171
  var CPP_TEST_ASSERTION_PATTERN = /\bEXPECT_|ASSERT_EQ\b|ASSERT_TRUE\b|ASSERT_FALSE\b|\bREQUIRE\s*\(|\bCHECK\s*\(|\bTEST\s*\(|\bTEST_F\s*\(|\bTEST_CASE\s*\(|\bSECTION\s*\(/;
8096
8172
  var MULTI_ECOSYSTEM_TEST_ASSERTION_PATTERN = /\.assert[A-Z_]\w*\s*\(|\bassert[A-Z]\w*\s*\(|\bpytest\.raises\b|\bself\.fail\s*\(|\bt\.(?:Error|Errorf|Fatal|Fatalf|Fail|FailNow)\b|\brequire\.[A-Z]\w*\s*\(|\b(?:debug_)?assert(?:_eq|_ne)?!\s*\(|\bassert_\w+\b|\$this->assert\w+\s*\(/;
8097
8173
  var SUITE_LEVEL_SKIP_PATTERN = /\b(?:describe|suite|context)\.skip\s*\(/;
8098
- var NAME_SHAPED_TEST_FILE_PATTERN = /\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)__tests__\/|(^|\/)test_[^/]*\.py$|(^|\/)tests?\.py$|_test\.go$|(^|\/)test_[^/]*\.(cpp|cc|cxx)$|_(test|tests)\.(cpp|cc|cxx)$|Tests?\.(java|kt)$|(_test|_spec)\.(rs|rb|php|swift|kt)$/;
8099
8174
  function measureNonHollowTestShare(repoPath, files) {
8100
8175
  let nonHollowCount = 0;
8101
8176
  let ratedCount = 0;
@@ -8450,7 +8525,6 @@ function isExplicitDeployTarget(file) {
8450
8525
  return /^(vercel|netlify|fly|render|railway)\.(json|toml|ya?ml)$/.test(file) || /^docker-compose\.ya?ml$/.test(file) || /^Procfile$/.test(file) || /^app\.ya?ml$/.test(file) || /^serverless\.ya?ml$/.test(file) || /^(k8s|kubernetes|manifests?)\/.+\.ya?ml$/i.test(file) || /^(charts?|helm)\/.+\.ya?ml$/i.test(file);
8451
8526
  }
8452
8527
  var MAIN_ENTRYPOINT_FILE_PATTERN = /(^|\/)(main|server|app|wsgi|asgi|index|entry|start|run)\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs)$/i;
8453
- var EXAMPLE_OR_TEST_DIR_PATTERN = /(?:^|\/)(examples?|samples?|demos?|tests?|__tests__|spec|fixtures?)(?:\/|$)/i;
8454
8528
  var SERVER_ENTRYPOINT_PATTERN = /\bapp\.listen\s*\(|\bserver\.listen\s*\(\s*(?:PORT|port|\d+)|http\.ListenAndServe\s*\(|http\.ListenAndServeTLS\s*\(|axum::Server(?:::|\.)|actix_web::HttpServer(?:::|\.)|uvicorn\.run\s*\(/;
8455
8529
  var CI_DEPLOY_JOB_PATTERN = /(?:^\s{2,4}deploy\s*:|\b(?:fly|heroku|vercel|netlify|railway|render)\s+deploy\b|kubectl\s+apply\b|helm\s+(?:install|upgrade|deploy)\b|docker\s+push\b)/im;
8456
8530
  function isDeployableService(repoPath, repoFiles) {
@@ -8467,9 +8541,7 @@ function detectServerEntrypoint(repoPath, repoFiles) {
8467
8541
  return findServerEntrypointFile(repoPath, repoFiles) !== null;
8468
8542
  }
8469
8543
  function findServerEntrypointFile(repoPath, repoFiles) {
8470
- const candidates = repoFiles.filter(
8471
- (file) => MAIN_ENTRYPOINT_FILE_PATTERN.test(file) && !EXAMPLE_OR_TEST_DIR_PATTERN.test(file)
8472
- ).slice(0, 30);
8544
+ const candidates = repoFiles.filter((file) => MAIN_ENTRYPOINT_FILE_PATTERN.test(file) && isProductionSourcePath(file)).slice(0, 30);
8473
8545
  return candidates.find((file) => fileContains(repoPath, file, SERVER_ENTRYPOINT_PATTERN)) ?? null;
8474
8546
  }
8475
8547
  function isHealthCheckSignalFile(repoPath, file) {
@@ -8818,6 +8890,17 @@ function evidenceForRelative(repoPath, path, kind, label) {
8818
8890
  } : {}
8819
8891
  };
8820
8892
  }
8893
+ function evidenceForRelativeAtLine(repoPath, path, kind, label, line) {
8894
+ const fullPath = join3(repoPath, path);
8895
+ const contents = isRegularFile(fullPath) ? readFileSync6(fullPath, "utf8") : "";
8896
+ return {
8897
+ kind,
8898
+ label,
8899
+ path,
8900
+ line,
8901
+ ...contents ? { contentHash: createHash("sha256").update(contents).digest("hex") } : {}
8902
+ };
8903
+ }
8821
8904
  function metric(name, label, value, max, weight, unit, description, kind) {
8822
8905
  return {
8823
8906
  name,
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  renderWitanBadgeSvg,
6
6
  renderWitanHtmlReport,
7
7
  runCejelScan
8
- } from "./chunk-MBYVOPJR.js";
8
+ } from "./chunk-QQVCIG4N.js";
9
9
 
10
10
  // src/index.ts
11
11
  import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
package/dist/mcp/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  renderWitanBadgeSvg,
11
11
  renderWitanHtmlReport,
12
12
  runCejelScan
13
- } from "../chunk-MBYVOPJR.js";
13
+ } from "../chunk-QQVCIG4N.js";
14
14
 
15
15
  // ../../node_modules/.pnpm/ajv@8.20.0/node_modules/ajv/dist/compile/codegen/code.js
16
16
  var require_code = __commonJS({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cejel/cejel",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },