@geoql/doctor-core 1.0.1 → 1.1.1

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/index.d.ts CHANGED
@@ -485,5 +485,100 @@ interface ListRulesFilter {
485
485
  }
486
486
  declare function listRules(filter?: ListRulesFilter): RegisteredRule[];
487
487
  //#endregion
488
- export { type ApplyInlineDisablesOptions, type AuditConfig, type AuditReport, type AuditTimings, BUILT_IN_RECOMMENDED, type Capability, type CliOverrides, ConfigCycleError, ConfigFileNotFoundError, type ConfigSource, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, type Diagnostic, type DiagnosticSource, type DirectiveLine, type DirectiveRange, type DirectiveSet, type DoctorReport, type DoctorUserConfig, type Framework, type GitScopeMode, type InitConfigFormat, type InitFileWrite, type InitOptions, type InitPlan, type InitPreset, InvalidConfigError, type ListRulesFilter, type LoadRuleDocOptions, type MonorepoKind, type MonorepoResult, OxlintOutputTooLarge, OxlintSpawnFailed, type ProjectInfo, type ProjectInfoLite, RULE_REGISTRY, type RawInitAnswers, type RegisteredRule, type ReporterFormat, type ReporterInput, type ReporterOptions, type ResolvedDoctorConfig, type ResolvedInitAnswers, type RuleCategory, type RuleDoc, type RuleSource, type ScoreBreakdownEntry, type ScoreConfig, type ScoreResult, type Severity, type VerboseTraceOptions, type WorkspacePackage, agentReport, applyInlineDisables, audit, buildDoctorReport, checkBuildQuality, checkDeadCode, checkDeps, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectProject, detectSummary, docsUrl, encodeAnnotation, encodeAnnotations, findMonorepoRoot, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, jsonCompactReport, jsonReport, listChangedFiles, listRules, listWorkspacePackages, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, normalizeInitAnswers, parseDirectives, parseExcludeList, planInit, prCommentReport, prettyReport, renderDetectSummary, renderVerboseTrace, runCrossFilePass, sarifReport, scoreDiagnostics, validateConfig };
488
+ //#region src/audit-filter.d.ts
489
+ /**
490
+ * Filter `report.diagnostics` to a known set of allowed rule ids, then
491
+ * recompute `score`, `errorCount`, `warnCount`, `infoCount`, `scoreResult`,
492
+ * `ruleCounts`, and `exitCode` from the filtered array.
493
+ *
494
+ * Why: the CLI layer calls `audit()` and then applies its own
495
+ * `allowedRuleIds` filter to drop suppressed (`off`) and preset-excluded
496
+ * (`info` rules not in `recommended`) diagnostics. The audit's own
497
+ * `score`/`counts` are computed BEFORE the CLI filter, so they still
498
+ * include the suppressed findings. That desync surfaces to consumers
499
+ * (the SaaS `--push` payload, the dashboard's counts-but-no-findings bug)
500
+ * and to the `score` itself (suppressed findings still penalize).
501
+ *
502
+ * Invariant enforced here:
503
+ * report.errorCount + report.warnCount + report.infoCount === report.diagnostics.length
504
+ * report.scoreResult.totalFindings === report.diagnostics.length
505
+ *
506
+ * `exitCode=2` (oxlint crash) is preserved verbatim; `exitCode=1` is
507
+ * recomputed from the surviving counts + the `failOn` gate.
508
+ */
509
+ declare function filterReportByRules(report: AuditReport, allowedRuleIds: ReadonlySet<string>, failOn?: 'error' | 'warn' | 'none', oxlintStderr?: string): AuditReport;
510
+ //#endregion
511
+ //#region src/push.d.ts
512
+ /**
513
+ * The 6 fields the SaaS receives for each finding.
514
+ * Everything else (message, codeSnippet, recommendation, etc.) is privacy-sensitive
515
+ * and MUST be stripped before transmission.
516
+ */
517
+ interface PushedFinding {
518
+ file: string;
519
+ line: number;
520
+ col: number;
521
+ ruleId: string;
522
+ severity: Severity;
523
+ category: string;
524
+ }
525
+ interface PushPayloadInput {
526
+ project: string;
527
+ score: number;
528
+ errorCount: number;
529
+ warnCount: number;
530
+ infoCount: number;
531
+ findings: PushedFinding[];
532
+ }
533
+ interface PushPayload extends PushPayloadInput {
534
+ commitSha?: string;
535
+ branch?: string;
536
+ prNumber?: number;
537
+ }
538
+ interface PushOptions {
539
+ project: string;
540
+ score: number;
541
+ errorCount: number;
542
+ warnCount: number;
543
+ infoCount: number;
544
+ diagnostics: Diagnostic[];
545
+ /** Project root used to relativize absolute finding file paths. */
546
+ rootDir?: string;
547
+ url: string;
548
+ apiKey: string;
549
+ fetchImpl?: typeof fetch;
550
+ /** Overrides for the request timeout (ms). Defaults to 5000. Test-only. */
551
+ timeoutMs?: number;
552
+ }
553
+ type PushResult = {
554
+ ok: true;
555
+ status?: number;
556
+ error?: undefined;
557
+ } | {
558
+ ok: false;
559
+ status?: number;
560
+ error: string;
561
+ };
562
+ /**
563
+ * Strip a list of Diagnostics down to the 6 allowed push fields.
564
+ * Privacy boundary: this function is the single point of enforcement.
565
+ * If you add a new privacy-sensitive field to Diagnostic, add it to the deny list here.
566
+ *
567
+ * Absolute file paths are relativized against rootDir so dashboards show
568
+ * `src/Foo.vue` rather than the CI runner's `/home/runner/work/<repo>/…`.
569
+ */
570
+ declare function stripFindings(diagnostics: Diagnostic[], rootDir?: string): PushedFinding[];
571
+ /**
572
+ * Build the top-level POST body. CI fields (commitSha, branch, prNumber) are
573
+ * included ONLY when the matching GITHUB_* env var is set.
574
+ */
575
+ declare function buildPushPayload(input: PushPayloadInput): PushPayload;
576
+ /**
577
+ * Strip + POST findings to a SaaS endpoint. Never throws — returns
578
+ * `{ ok: false, error }` on any HTTP / network failure so the audit's
579
+ * exit code is not affected by SaaS downtime.
580
+ */
581
+ declare function pushFindings(opts: PushOptions): Promise<PushResult>;
582
+ //#endregion
583
+ export { type ApplyInlineDisablesOptions, type AuditConfig, type AuditReport, type AuditTimings, BUILT_IN_RECOMMENDED, type Capability, type CliOverrides, ConfigCycleError, ConfigFileNotFoundError, type ConfigSource, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, type Diagnostic, type DiagnosticSource, type DirectiveLine, type DirectiveRange, type DirectiveSet, type DoctorReport, type DoctorUserConfig, type Framework, type GitScopeMode, type InitConfigFormat, type InitFileWrite, type InitOptions, type InitPlan, type InitPreset, InvalidConfigError, type ListRulesFilter, type LoadRuleDocOptions, type MonorepoKind, type MonorepoResult, OxlintOutputTooLarge, OxlintSpawnFailed, type ProjectInfo, type ProjectInfoLite, type PushOptions, type PushPayload, type PushPayloadInput, type PushResult, type PushedFinding, RULE_REGISTRY, type RawInitAnswers, type RegisteredRule, type ReporterFormat, type ReporterInput, type ReporterOptions, type ResolvedDoctorConfig, type ResolvedInitAnswers, type RuleCategory, type RuleDoc, type RuleSource, type ScoreBreakdownEntry, type ScoreConfig, type ScoreResult, type Severity, type VerboseTraceOptions, type WorkspacePackage, agentReport, applyInlineDisables, audit, buildDoctorReport, buildPushPayload, checkBuildQuality, checkDeadCode, checkDeps, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectProject, detectSummary, docsUrl, encodeAnnotation, encodeAnnotations, filterReportByRules, findMonorepoRoot, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, jsonCompactReport, jsonReport, listChangedFiles, listRules, listWorkspacePackages, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, normalizeInitAnswers, parseDirectives, parseExcludeList, planInit, prCommentReport, prettyReport, pushFindings, renderDetectSummary, renderVerboseTrace, runCrossFilePass, sarifReport, scoreDiagnostics, stripFindings, validateConfig };
489
584
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as listRules, c as InvalidConfigError, i as RULE_REGISTRY, l as BUILT_IN_RECOMMENDED, o as ConfigCycleError, r as validateConfig, s as ConfigFileNotFoundError, t as loadDoctorConfig } from "./load-yXDi-5ti.js";
2
- import { dirname, join, relative, resolve } from "node:path";
2
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { performance } from "node:perf_hooks";
4
4
  import { access, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
5
5
  import { createRequire } from "node:module";
@@ -3011,7 +3011,7 @@ async function listWorkspacePackages(rootDir) {
3011
3011
  //#endregion
3012
3012
  //#region src/reporters/docs-url.ts
3013
3013
  function docsUrl(ruleId) {
3014
- return `https://docs.doctor.geoql.in/rules/${ruleId}`;
3014
+ return `https://docs.the-doctor.report/rules/${ruleId}`;
3015
3015
  }
3016
3016
  //#endregion
3017
3017
  //#region src/reporters/render.ts
@@ -3374,7 +3374,7 @@ function jsonCompactReport(input) {
3374
3374
  }
3375
3375
  //#endregion
3376
3376
  //#region src/reporters/pr-comment.ts
3377
- const DOCS_BASE = "https://docs.doctor.geoql.in/rules/";
3377
+ const DOCS_BASE = "https://docs.the-doctor.report/rules/";
3378
3378
  const MAX_PER_SECTION = 5;
3379
3379
  function binName(toolName) {
3380
3380
  return toolName.replace("@geoql/", "");
@@ -3474,6 +3474,165 @@ function format(input, kind = "agent", options) {
3474
3474
  return agentReport(input, options);
3475
3475
  }
3476
3476
  //#endregion
3477
- export { BUILT_IN_RECOMMENDED, ConfigCycleError, ConfigFileNotFoundError, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, InvalidConfigError, OxlintOutputTooLarge, OxlintSpawnFailed, RULE_REGISTRY, agentReport, applyInlineDisables, audit, buildDoctorReport, checkBuildQuality, checkDeadCode, checkDeps, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectProject, detectSummary, docsUrl, encodeAnnotation, encodeAnnotations, findMonorepoRoot, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, jsonCompactReport, jsonReport, listChangedFiles, listRules, listWorkspacePackages, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, normalizeInitAnswers, parseDirectives, parseExcludeList, planInit, prCommentReport, prettyReport, renderDetectSummary, renderVerboseTrace, runCrossFilePass, sarifReport, scoreDiagnostics, validateConfig };
3477
+ //#region src/audit-filter.ts
3478
+ /**
3479
+ * Filter `report.diagnostics` to a known set of allowed rule ids, then
3480
+ * recompute `score`, `errorCount`, `warnCount`, `infoCount`, `scoreResult`,
3481
+ * `ruleCounts`, and `exitCode` from the filtered array.
3482
+ *
3483
+ * Why: the CLI layer calls `audit()` and then applies its own
3484
+ * `allowedRuleIds` filter to drop suppressed (`off`) and preset-excluded
3485
+ * (`info` rules not in `recommended`) diagnostics. The audit's own
3486
+ * `score`/`counts` are computed BEFORE the CLI filter, so they still
3487
+ * include the suppressed findings. That desync surfaces to consumers
3488
+ * (the SaaS `--push` payload, the dashboard's counts-but-no-findings bug)
3489
+ * and to the `score` itself (suppressed findings still penalize).
3490
+ *
3491
+ * Invariant enforced here:
3492
+ * report.errorCount + report.warnCount + report.infoCount === report.diagnostics.length
3493
+ * report.scoreResult.totalFindings === report.diagnostics.length
3494
+ *
3495
+ * `exitCode=2` (oxlint crash) is preserved verbatim; `exitCode=1` is
3496
+ * recomputed from the surviving counts + the `failOn` gate.
3497
+ */
3498
+ function filterReportByRules(report, allowedRuleIds, failOn = "error", oxlintStderr) {
3499
+ const filtered = [];
3500
+ for (const d of report.diagnostics) if (allowedRuleIds.has(d.ruleId)) filtered.push(d);
3501
+ const threshold = report.scoreResult.threshold;
3502
+ const scoreResult = scoreDiagnostics(filtered, { threshold });
3503
+ const ruleCounts = {};
3504
+ for (const d of filtered) ruleCounts[d.ruleId] = (ruleCounts[d.ruleId] ?? 0) + 1;
3505
+ let exitCode = report.exitCode;
3506
+ if (exitCode !== 2) if (failOn === "none") exitCode = 0;
3507
+ else exitCode = (failOn === "warn" ? scoreResult.errorCount + scoreResult.warnCount : scoreResult.errorCount) > 0 ? 1 : 0;
3508
+ return {
3509
+ ...report,
3510
+ diagnostics: filtered,
3511
+ score: scoreResult.score,
3512
+ errorCount: scoreResult.errorCount,
3513
+ warnCount: scoreResult.warnCount,
3514
+ infoCount: scoreResult.infoCount,
3515
+ scoreResult,
3516
+ ruleCounts,
3517
+ exitCode
3518
+ };
3519
+ }
3520
+ //#endregion
3521
+ //#region src/push.ts
3522
+ const PUSH_TIMEOUT_MS = 5e3;
3523
+ const categoryByRuleId = new Map(RULE_REGISTRY.map((r) => [r.id, r.category]));
3524
+ function lookupCategory(ruleId) {
3525
+ return categoryByRuleId.get(ruleId) ?? "unknown";
3526
+ }
3527
+ function toRepoRelative(file, rootDir) {
3528
+ if (!rootDir || !isAbsolute(file)) return file;
3529
+ const rel = relative(rootDir, file);
3530
+ if (rel.startsWith("..")) return file;
3531
+ return rel;
3532
+ }
3533
+ /**
3534
+ * Strip a list of Diagnostics down to the 6 allowed push fields.
3535
+ * Privacy boundary: this function is the single point of enforcement.
3536
+ * If you add a new privacy-sensitive field to Diagnostic, add it to the deny list here.
3537
+ *
3538
+ * Absolute file paths are relativized against rootDir so dashboards show
3539
+ * `src/Foo.vue` rather than the CI runner's `/home/runner/work/<repo>/…`.
3540
+ */
3541
+ function stripFindings(diagnostics, rootDir) {
3542
+ const out = new Array(diagnostics.length);
3543
+ for (let i = 0; i < diagnostics.length; i++) {
3544
+ const d = diagnostics[i];
3545
+ out[i] = {
3546
+ file: toRepoRelative(d.file, rootDir),
3547
+ line: d.line,
3548
+ col: d.column,
3549
+ ruleId: d.ruleId,
3550
+ severity: d.severity,
3551
+ category: lookupCategory(d.ruleId)
3552
+ };
3553
+ }
3554
+ return out;
3555
+ }
3556
+ function readOptionalEnv(name) {
3557
+ const v = process.env[name];
3558
+ return v === void 0 || v === "" ? void 0 : v;
3559
+ }
3560
+ function readPrNumber() {
3561
+ const raw = readOptionalEnv("GITHUB_PR_NUMBER");
3562
+ if (raw === void 0) return void 0;
3563
+ const n = Number(raw);
3564
+ return Number.isFinite(n) ? n : void 0;
3565
+ }
3566
+ /**
3567
+ * Build the top-level POST body. CI fields (commitSha, branch, prNumber) are
3568
+ * included ONLY when the matching GITHUB_* env var is set.
3569
+ */
3570
+ function buildPushPayload(input) {
3571
+ const payload = {
3572
+ project: input.project,
3573
+ score: input.score,
3574
+ errorCount: input.errorCount,
3575
+ warnCount: input.warnCount,
3576
+ infoCount: input.infoCount,
3577
+ findings: input.findings
3578
+ };
3579
+ const commitSha = readOptionalEnv("GITHUB_SHA");
3580
+ const branch = readOptionalEnv("GITHUB_REF_NAME");
3581
+ const prNumber = readPrNumber();
3582
+ if (commitSha !== void 0) payload.commitSha = commitSha;
3583
+ if (branch !== void 0) payload.branch = branch;
3584
+ if (prNumber !== void 0) payload.prNumber = prNumber;
3585
+ return payload;
3586
+ }
3587
+ /**
3588
+ * Strip + POST findings to a SaaS endpoint. Never throws — returns
3589
+ * `{ ok: false, error }` on any HTTP / network failure so the audit's
3590
+ * exit code is not affected by SaaS downtime.
3591
+ */
3592
+ async function pushFindings(opts) {
3593
+ const findings = stripFindings(opts.diagnostics, opts.rootDir);
3594
+ const payload = buildPushPayload({
3595
+ project: opts.project,
3596
+ score: opts.score,
3597
+ errorCount: opts.errorCount,
3598
+ warnCount: opts.warnCount,
3599
+ infoCount: opts.infoCount,
3600
+ findings
3601
+ });
3602
+ const fetchImpl = opts.fetchImpl ?? fetch;
3603
+ const controller = new AbortController();
3604
+ const timeoutMs = opts.timeoutMs ?? PUSH_TIMEOUT_MS;
3605
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3606
+ timer.unref?.();
3607
+ try {
3608
+ const res = await fetchImpl(opts.url, {
3609
+ method: "POST",
3610
+ headers: {
3611
+ "Content-Type": "application/json",
3612
+ "x-api-key": opts.apiKey
3613
+ },
3614
+ body: JSON.stringify(payload),
3615
+ signal: controller.signal
3616
+ });
3617
+ clearTimeout(timer);
3618
+ if (res.ok) return {
3619
+ ok: true,
3620
+ status: res.status
3621
+ };
3622
+ return {
3623
+ ok: false,
3624
+ status: res.status,
3625
+ error: `HTTP ${res.status} ${res.statusText}`.trim()
3626
+ };
3627
+ } catch (err) {
3628
+ clearTimeout(timer);
3629
+ return {
3630
+ ok: false,
3631
+ error: err instanceof Error ? err.message : String(err)
3632
+ };
3633
+ }
3634
+ }
3635
+ //#endregion
3636
+ export { BUILT_IN_RECOMMENDED, ConfigCycleError, ConfigFileNotFoundError, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, InvalidConfigError, OxlintOutputTooLarge, OxlintSpawnFailed, RULE_REGISTRY, agentReport, applyInlineDisables, audit, buildDoctorReport, buildPushPayload, checkBuildQuality, checkDeadCode, checkDeps, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectProject, detectSummary, docsUrl, encodeAnnotation, encodeAnnotations, filterReportByRules, findMonorepoRoot, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, jsonCompactReport, jsonReport, listChangedFiles, listRules, listWorkspacePackages, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, normalizeInitAnswers, parseDirectives, parseExcludeList, planInit, prCommentReport, prettyReport, pushFindings, renderDetectSummary, renderVerboseTrace, runCrossFilePass, sarifReport, scoreDiagnostics, stripFindings, validateConfig };
3478
3637
 
3479
3638
  //# sourceMappingURL=index.js.map