@rigour-labs/core 6.8.0 → 6.8.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/inference/cloud-provider.js +12 -2
- package/package.json +11 -8
- package/dist/brief/briefing.test.d.ts +0 -1
- package/dist/brief/briefing.test.js +0 -107
- package/dist/context/automatic-index-cache.test.d.ts +0 -1
- package/dist/context/automatic-index-cache.test.js +0 -30
- package/dist/context/automatic-index.test.d.ts +0 -1
- package/dist/context/automatic-index.test.js +0 -45
- package/dist/context/cache-engine.test.d.ts +0 -1
- package/dist/context/cache-engine.test.js +0 -99
- package/dist/context/dependency-graph.test.d.ts +0 -1
- package/dist/context/dependency-graph.test.js +0 -13
- package/dist/context/index-status.test.d.ts +0 -1
- package/dist/context/index-status.test.js +0 -26
- package/dist/context.test.d.ts +0 -1
- package/dist/context.test.js +0 -228
- package/dist/deep/agent-review.test.d.ts +0 -1
- package/dist/deep/agent-review.test.js +0 -53
- package/dist/deep/code-context.test.d.ts +0 -1
- package/dist/deep/code-context.test.js +0 -48
- package/dist/deep/code-pass.test.d.ts +0 -1
- package/dist/deep/code-pass.test.js +0 -112
- package/dist/deep/code-review-prompt.test.d.ts +0 -1
- package/dist/deep/code-review-prompt.test.js +0 -20
- package/dist/deep/code-verifier.test.d.ts +0 -1
- package/dist/deep/code-verifier.test.js +0 -54
- package/dist/deep/diff-tests/calls.test.d.ts +0 -1
- package/dist/deep/diff-tests/calls.test.js +0 -33
- package/dist/deep/diff-tests/run.test.d.ts +0 -1
- package/dist/deep/diff-tests/run.test.js +0 -58
- package/dist/deep/fact-extractor.test.d.ts +0 -1
- package/dist/deep/fact-extractor.test.js +0 -581
- package/dist/deep/parse-findings.test.d.ts +0 -1
- package/dist/deep/parse-findings.test.js +0 -25
- package/dist/deep/pr-review.test.d.ts +0 -1
- package/dist/deep/pr-review.test.js +0 -80
- package/dist/deep/prompts.test.d.ts +0 -1
- package/dist/deep/prompts.test.js +0 -235
- package/dist/deep/reference-pack.test.d.ts +0 -1
- package/dist/deep/reference-pack.test.js +0 -46
- package/dist/deep/related-changes.test.d.ts +0 -1
- package/dist/deep/related-changes.test.js +0 -33
- package/dist/deep/review-context-export.test.d.ts +0 -1
- package/dist/deep/review-context-export.test.js +0 -32
- package/dist/deep/review-tools.test.d.ts +0 -1
- package/dist/deep/review-tools.test.js +0 -39
- package/dist/deep/risk.test.d.ts +0 -1
- package/dist/deep/risk.test.js +0 -101
- package/dist/deep/verifier.test.d.ts +0 -1
- package/dist/deep/verifier.test.js +0 -635
- package/dist/discovery.test.d.ts +0 -1
- package/dist/discovery.test.js +0 -93
- package/dist/environment.test.d.ts +0 -1
- package/dist/environment.test.js +0 -94
- package/dist/firewall/firewall.test.d.ts +0 -1
- package/dist/firewall/firewall.test.js +0 -117
- package/dist/firewall/trust-boundaries.test.d.ts +0 -1
- package/dist/firewall/trust-boundaries.test.js +0 -64
- package/dist/firewall/trusted-control.test.d.ts +0 -1
- package/dist/firewall/trusted-control.test.js +0 -188
- package/dist/gates/agent-team.test.d.ts +0 -1
- package/dist/gates/agent-team.test.js +0 -113
- package/dist/gates/ast.test.d.ts +0 -1
- package/dist/gates/ast.test.js +0 -112
- package/dist/gates/checkpoint.test.d.ts +0 -1
- package/dist/gates/checkpoint.test.js +0 -105
- package/dist/gates/content.test.d.ts +0 -1
- package/dist/gates/content.test.js +0 -73
- package/dist/gates/coverage.test.d.ts +0 -1
- package/dist/gates/coverage.test.js +0 -53
- package/dist/gates/dedupe-failures.test.d.ts +0 -1
- package/dist/gates/dedupe-failures.test.js +0 -12
- package/dist/gates/deep-analysis.test.d.ts +0 -1
- package/dist/gates/deep-analysis.test.js +0 -86
- package/dist/gates/deep-intent.test.d.ts +0 -1
- package/dist/gates/deep-intent.test.js +0 -51
- package/dist/gates/deep-timeout.test.d.ts +0 -1
- package/dist/gates/deep-timeout.test.js +0 -10
- package/dist/gates/deprecated-apis.test.d.ts +0 -1
- package/dist/gates/deprecated-apis.test.js +0 -318
- package/dist/gates/deprecated-dependencies.test.d.ts +0 -1
- package/dist/gates/deprecated-dependencies.test.js +0 -55
- package/dist/gates/frontend-secret-exposure.test.d.ts +0 -1
- package/dist/gates/frontend-secret-exposure.test.js +0 -148
- package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports/framework-modules-nuxt.test.js +0 -27
- package/dist/gates/hallucinated-imports/js-resolver-types.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports/js-resolver-types.test.js +0 -14
- package/dist/gates/hallucinated-imports-sveltekit.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports-sveltekit.test.js +0 -132
- package/dist/gates/hallucinated-imports.test.d.ts +0 -1
- package/dist/gates/hallucinated-imports.test.js +0 -1206
- package/dist/gates/js-style-context.test.d.ts +0 -1
- package/dist/gates/js-style-context.test.js +0 -35
- package/dist/gates/logic-drift.test.d.ts +0 -1
- package/dist/gates/logic-drift.test.js +0 -52
- package/dist/gates/phantom-apis.test.d.ts +0 -1
- package/dist/gates/phantom-apis.test.js +0 -396
- package/dist/gates/promise-safety.test.d.ts +0 -1
- package/dist/gates/promise-safety.test.js +0 -34
- package/dist/gates/runner.test.d.ts +0 -1
- package/dist/gates/runner.test.js +0 -77
- package/dist/gates/scoped-gates.test.d.ts +0 -1
- package/dist/gates/scoped-gates.test.js +0 -52
- package/dist/gates/security-patterns-owasp.test.d.ts +0 -1
- package/dist/gates/security-patterns-owasp.test.js +0 -186
- package/dist/gates/security-patterns.test.d.ts +0 -1
- package/dist/gates/security-patterns.test.js +0 -194
- package/dist/gates/semantic-bugs.test.d.ts +0 -1
- package/dist/gates/semantic-bugs.test.js +0 -76
- package/dist/gates/side-effect-analysis.test.d.ts +0 -1
- package/dist/gates/side-effect-analysis.test.js +0 -162
- package/dist/gates/style-drift.test.d.ts +0 -1
- package/dist/gates/style-drift.test.js +0 -26
- package/dist/gates/test-quality.test.d.ts +0 -1
- package/dist/gates/test-quality.test.js +0 -325
- package/dist/gates/trusted-reviews.test.d.ts +0 -1
- package/dist/gates/trusted-reviews.test.js +0 -16
- package/dist/gates/unindexed-reads/queries.test.d.ts +0 -1
- package/dist/gates/unindexed-reads/queries.test.js +0 -54
- package/dist/gates/unindexed-reads/schema.test.d.ts +0 -1
- package/dist/gates/unindexed-reads/schema.test.js +0 -81
- package/dist/gates/unindexed-reads/unindexed-reads.test.d.ts +0 -1
- package/dist/gates/unindexed-reads/unindexed-reads.test.js +0 -90
- package/dist/hooks/checker.test.d.ts +0 -1
- package/dist/hooks/checker.test.js +0 -159
- package/dist/hooks/dlp-confidence.test.d.ts +0 -1
- package/dist/hooks/dlp-confidence.test.js +0 -51
- package/dist/hooks/dlp-feedback.test.d.ts +0 -1
- package/dist/hooks/dlp-feedback.test.js +0 -131
- package/dist/hooks/input-validator.test.d.ts +0 -1
- package/dist/hooks/input-validator.test.js +0 -329
- package/dist/hooks/templates.test.d.ts +0 -1
- package/dist/hooks/templates.test.js +0 -27
- package/dist/inference/brain-placeholder.test.d.ts +0 -1
- package/dist/inference/brain-placeholder.test.js +0 -28
- package/dist/inference/cloud-provider.test.d.ts +0 -1
- package/dist/inference/cloud-provider.test.js +0 -139
- package/dist/inference/executable.test.d.ts +0 -1
- package/dist/inference/executable.test.js +0 -41
- package/dist/inference/http-download.test.d.ts +0 -1
- package/dist/inference/http-download.test.js +0 -109
- package/dist/inference/llama-engine-checksum.test.d.ts +0 -1
- package/dist/inference/llama-engine-checksum.test.js +0 -27
- package/dist/inference/llama-engine.test.d.ts +0 -1
- package/dist/inference/llama-engine.test.js +0 -51
- package/dist/inference/llama-process.test.d.ts +0 -1
- package/dist/inference/llama-process.test.js +0 -61
- package/dist/inference/local-model.test.d.ts +0 -1
- package/dist/inference/local-model.test.js +0 -23
- package/dist/inference/model-download.test.d.ts +0 -1
- package/dist/inference/model-download.test.js +0 -125
- package/dist/inference/model-manager.test.d.ts +0 -1
- package/dist/inference/model-manager.test.js +0 -24
- package/dist/inference/types.test.d.ts +0 -1
- package/dist/inference/types.test.js +0 -19
- package/dist/memory/recall.test.d.ts +0 -1
- package/dist/memory/recall.test.js +0 -36
- package/dist/pattern-index/indexer.test.d.ts +0 -6
- package/dist/pattern-index/indexer.test.js +0 -197
- package/dist/pattern-index/matcher.test.d.ts +0 -6
- package/dist/pattern-index/matcher.test.js +0 -238
- package/dist/pattern-index/pattern-reuse.test.d.ts +0 -1
- package/dist/pattern-index/pattern-reuse.test.js +0 -76
- package/dist/pattern-index/semantic-runtime.test.d.ts +0 -1
- package/dist/pattern-index/semantic-runtime.test.js +0 -31
- package/dist/pattern-index/staleness.test.d.ts +0 -6
- package/dist/pattern-index/staleness.test.js +0 -211
- package/dist/review/agent-fixes.test.d.ts +0 -1
- package/dist/review/agent-fixes.test.js +0 -32
- package/dist/review/backtest-init.test.d.ts +0 -1
- package/dist/review/backtest-init.test.js +0 -136
- package/dist/review/backtest-judges.test.d.ts +0 -1
- package/dist/review/backtest-judges.test.js +0 -32
- package/dist/review/backtest-last.test.d.ts +0 -1
- package/dist/review/backtest-last.test.js +0 -109
- package/dist/review/backtest.test.d.ts +0 -1
- package/dist/review/backtest.test.js +0 -183
- package/dist/review/baseline.test.d.ts +0 -1
- package/dist/review/baseline.test.js +0 -22
- package/dist/review/branch-checks.test.d.ts +0 -1
- package/dist/review/branch-checks.test.js +0 -48
- package/dist/review/check-outcomes.test.d.ts +0 -1
- package/dist/review/check-outcomes.test.js +0 -56
- package/dist/review/code-patterns.test.d.ts +0 -1
- package/dist/review/code-patterns.test.js +0 -142
- package/dist/review/dead-code.test.d.ts +0 -1
- package/dist/review/dead-code.test.js +0 -154
- package/dist/review/deep-runs.test.d.ts +0 -1
- package/dist/review/deep-runs.test.js +0 -23
- package/dist/review/effectiveness.test.d.ts +0 -1
- package/dist/review/effectiveness.test.js +0 -42
- package/dist/review/fix-scope.test.d.ts +0 -1
- package/dist/review/fix-scope.test.js +0 -104
- package/dist/review/generated-files.test.d.ts +0 -1
- package/dist/review/generated-files.test.js +0 -25
- package/dist/review/migration-order.test.d.ts +0 -1
- package/dist/review/migration-order.test.js +0 -62
- package/dist/review/quiet.test.d.ts +0 -1
- package/dist/review/quiet.test.js +0 -38
- package/dist/review/receipt.test.d.ts +0 -1
- package/dist/review/receipt.test.js +0 -60
- package/dist/review/review-task.test.d.ts +0 -1
- package/dist/review/review-task.test.js +0 -89
- package/dist/review/review.test.d.ts +0 -1
- package/dist/review/review.test.js +0 -203
- package/dist/review/reviewer/adapters.test.d.ts +0 -1
- package/dist/review/reviewer/adapters.test.js +0 -100
- package/dist/review/reviewer/api-judge.test.d.ts +0 -1
- package/dist/review/reviewer/api-judge.test.js +0 -108
- package/dist/review/reviewer/background.test.d.ts +0 -1
- package/dist/review/reviewer/background.test.js +0 -118
- package/dist/review/reviewer/context.test.d.ts +0 -1
- package/dist/review/reviewer/context.test.js +0 -45
- package/dist/review/reviewer/exec.test.d.ts +0 -1
- package/dist/review/reviewer/exec.test.js +0 -59
- package/dist/review/reviewer/inputs.test.d.ts +0 -1
- package/dist/review/reviewer/inputs.test.js +0 -52
- package/dist/review/reviewer/panel.test.d.ts +0 -1
- package/dist/review/reviewer/panel.test.js +0 -110
- package/dist/review/reviewer/record.test.d.ts +0 -1
- package/dist/review/reviewer/record.test.js +0 -40
- package/dist/review/reviewer/rule-writer.test.d.ts +0 -1
- package/dist/review/reviewer/rule-writer.test.js +0 -52
- package/dist/review/reviewer/settings.test.d.ts +0 -1
- package/dist/review/reviewer/settings.test.js +0 -57
- package/dist/review/reviewer/usage.test.d.ts +0 -1
- package/dist/review/reviewer/usage.test.js +0 -14
- package/dist/review/reviewer.test.d.ts +0 -1
- package/dist/review/reviewer.test.js +0 -792
- package/dist/review/stories.test.d.ts +0 -1
- package/dist/review/stories.test.js +0 -58
- package/dist/review/toolchain.test.d.ts +0 -1
- package/dist/review/toolchain.test.js +0 -108
- package/dist/review/typed/redundancy.test.d.ts +0 -1
- package/dist/review/typed/redundancy.test.js +0 -315
- package/dist/review/typed/schema-nullability.test.d.ts +0 -1
- package/dist/review/typed/schema-nullability.test.js +0 -63
- package/dist/review-learning/human-edits.test.d.ts +0 -1
- package/dist/review-learning/human-edits.test.js +0 -47
- package/dist/review-learning/repo-rules.test.d.ts +0 -1
- package/dist/review-learning/repo-rules.test.js +0 -93
- package/dist/review-learning/review-learning.test.d.ts +0 -1
- package/dist/review-learning/review-learning.test.js +0 -249
- package/dist/safety.test.d.ts +0 -1
- package/dist/safety.test.js +0 -42
- package/dist/semantic/benchmark.test.d.ts +0 -1
- package/dist/semantic/benchmark.test.js +0 -22
- package/dist/semantic/intent/intent.test.d.ts +0 -1
- package/dist/semantic/intent/intent.test.js +0 -46
- package/dist/semantic/learn/learn.test.d.ts +0 -1
- package/dist/semantic/learn/learn.test.js +0 -67
- package/dist/semantic/origins.test.d.ts +0 -1
- package/dist/semantic/origins.test.js +0 -77
- package/dist/semantic/project-facts.test.d.ts +0 -1
- package/dist/semantic/project-facts.test.js +0 -45
- package/dist/semantic/sites/call-sites.test.d.ts +0 -1
- package/dist/semantic/sites/call-sites.test.js +0 -46
- package/dist/services/adaptive-thresholds.test.d.ts +0 -1
- package/dist/services/adaptive-thresholds.test.js +0 -53
- package/dist/services/agent-history.test.d.ts +0 -1
- package/dist/services/agent-history.test.js +0 -69
- package/dist/services/context-scope-summary.test.d.ts +0 -1
- package/dist/services/context-scope-summary.test.js +0 -17
- package/dist/services/context-telemetry-service.test.d.ts +0 -1
- package/dist/services/context-telemetry-service.test.js +0 -182
- package/dist/services/cursor-usage-sync.test.d.ts +0 -1
- package/dist/services/cursor-usage-sync.test.js +0 -173
- package/dist/services/engineering-knowledge-graph.test.d.ts +0 -1
- package/dist/services/engineering-knowledge-graph.test.js +0 -76
- package/dist/services/model-pricing.test.d.ts +0 -1
- package/dist/services/model-pricing.test.js +0 -44
- package/dist/services/observed-savings.test.d.ts +0 -1
- package/dist/services/observed-savings.test.js +0 -37
- package/dist/services/score-history.test.d.ts +0 -1
- package/dist/services/score-history.test.js +0 -61
- package/dist/smoke.test.d.ts +0 -1
- package/dist/smoke.test.js +0 -17
- package/dist/storage/cache-cleanup.test.d.ts +0 -1
- package/dist/storage/cache-cleanup.test.js +0 -54
- package/dist/storage/context-telemetry.test.d.ts +0 -1
- package/dist/storage/context-telemetry.test.js +0 -80
- package/dist/storage/db.test.d.ts +0 -1
- package/dist/storage/db.test.js +0 -46
- package/dist/storage/fix-lessons.test.d.ts +0 -1
- package/dist/storage/fix-lessons.test.js +0 -61
- package/dist/storage/lessons.test.d.ts +0 -1
- package/dist/storage/lessons.test.js +0 -81
- package/dist/storage/local-encryption.test.d.ts +0 -1
- package/dist/storage/local-encryption.test.js +0 -34
- package/dist/storage/local-memory.test.d.ts +0 -1
- package/dist/storage/local-memory.test.js +0 -55
- package/dist/storage/share-memory.test.d.ts +0 -1
- package/dist/storage/share-memory.test.js +0 -34
- package/dist/storage/team-diagnostics.test.d.ts +0 -1
- package/dist/storage/team-diagnostics.test.js +0 -51
- package/dist/storage/team-scope.test.d.ts +0 -1
- package/dist/storage/team-scope.test.js +0 -22
- package/dist/storage/team-store.test.d.ts +0 -1
- package/dist/storage/team-store.test.js +0 -11
- package/dist/storage/team-sync-scope.test.d.ts +0 -1
- package/dist/storage/team-sync-scope.test.js +0 -100
- package/dist/storage/team-vector-store.test.d.ts +0 -1
- package/dist/storage/team-vector-store.test.js +0 -56
- package/dist/storage/telemetry-scope.test.d.ts +0 -1
- package/dist/storage/telemetry-scope.test.js +0 -38
- package/dist/task/thread.test.d.ts +0 -1
- package/dist/task/thread.test.js +0 -133
- package/dist/telemetry/telemetry.test.d.ts +0 -1
- package/dist/telemetry/telemetry.test.js +0 -64
- package/dist/types/index.test.d.ts +0 -1
- package/dist/types/index.test.js +0 -33
- package/dist/utils/command-line.test.d.ts +0 -1
- package/dist/utils/command-line.test.js +0 -8
- package/dist/utils/diff-removed.test.d.ts +0 -1
- package/dist/utils/diff-removed.test.js +0 -29
- package/dist/utils/diff.test.d.ts +0 -1
- package/dist/utils/diff.test.js +0 -38
- package/dist/utils/glob-paths.test.d.ts +0 -1
- package/dist/utils/glob-paths.test.js +0 -40
- package/dist/utils/profile.test.d.ts +0 -1
- package/dist/utils/profile.test.js +0 -68
- package/dist/utils/scanner.test.d.ts +0 -1
- package/dist/utils/scanner.test.js +0 -48
- package/dist/utils/scope.test.d.ts +0 -1
- package/dist/utils/scope.test.js +0 -38
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'child_process';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
-
import { ConfigSchema } from '../types/index.js';
|
|
7
|
-
import { backtestPassed, formatBacktest, ledgerProblems, loadLedger, runBacktest, score, LEDGER_PATH } from './backtest.js';
|
|
8
|
-
const item = (over = {}) => ({ gate: 'unused-export', file: 'src/lib/server/billing/invoices.ts', line: 40, text: 'Unused export `isRefund` is not imported anywhere', blocking: true, ...over });
|
|
9
|
-
describe('a ledger point matches a finding', () => {
|
|
10
|
-
const caught = (row, finding) => score({ id: 'r', commit: 'abcdef0', base: 'main', points: [{ id: 'P', point: 'p', ...row }], must_not_flag: [] }, 'h', [finding], 0, undefined).points[0].caught;
|
|
11
|
-
it('by file, then by line window or text pattern, case-insensitively', () => {
|
|
12
|
-
expect(caught({ file: 'invoices', lines: [30, 50] }, item())).toBe(true);
|
|
13
|
-
expect(caught({ file: 'invoices', lines: [41, 50] }, item())).toBe(false);
|
|
14
|
-
expect(caught({ file: 'invoices', text: 'isrefund|twice' }, item())).toBe(true);
|
|
15
|
-
expect(caught({ file: 'invoices', lines: [41, 50], text: 'isRefund' }, item())).toBe(true); // either is enough
|
|
16
|
-
expect(caught({ file: 'receipts', text: 'isRefund' }, item())).toBe(false); // the file must match
|
|
17
|
-
expect(caught({ file: 'invoices', lines: [30, 50] }, item({ line: undefined }))).toBe(false); // a lineless finding cannot be placed in a window
|
|
18
|
-
});
|
|
19
|
-
it('rejects rows that can match nothing before any round runs', () => {
|
|
20
|
-
const ledger = { rounds: [{ id: 'r1', commit: 'abcdef0', base: 'main', points: [
|
|
21
|
-
{ id: 'A', point: 'p', file: 'x', lines: [1, 2] },
|
|
22
|
-
{ id: 'B', point: 'p', file: '', text: 'x' },
|
|
23
|
-
{ id: 'C', point: 'p', file: 'x' },
|
|
24
|
-
{ id: 'D', point: 'p', file: 'x', text: 'x', needs: 'a text pattern' },
|
|
25
|
-
{ id: 'E', point: 'p', file: '[', text: 'x' },
|
|
26
|
-
], must_not_flag: [{ file: 'x' }] }] };
|
|
27
|
-
expect(ledgerProblems(ledger)).toEqual([
|
|
28
|
-
'r1 B: no file pattern',
|
|
29
|
-
'r1 C: needs a line window or a text pattern',
|
|
30
|
-
'r1 D: needs a text pattern',
|
|
31
|
-
'r1 E: not a regular expression',
|
|
32
|
-
'r1 must_not_flag[0]: needs a line window or a text pattern',
|
|
33
|
-
]);
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
describe('scoring a round', () => {
|
|
37
|
-
const round = {
|
|
38
|
-
id: 'r1', commit: 'abcdef0', base: 'main',
|
|
39
|
-
points: [
|
|
40
|
-
{ id: 'P1', point: 'dead export', file: 'invoices', lines: [30, 50] },
|
|
41
|
-
{ id: 'P2', point: 'copies the accumulator', file: 'totals', text: 'copied|quadratic' },
|
|
42
|
-
{ id: 'P3', point: 'drops the currency', file: 'checkout', text: 'currency' },
|
|
43
|
-
],
|
|
44
|
-
must_not_flag: [{ file: 'session', text: 'cookie' }],
|
|
45
|
-
};
|
|
46
|
-
it('counts a point as caught only by a blocking finding; an advisory match is noted and still missed', () => {
|
|
47
|
-
const items = [item(), item({ gate: 'quadratic-copy', file: 'src/totals.ts', line: 128, text: 'accumulator copied whole', blocking: false }), item({ gate: 'reviewer', file: 'src/hooks/session.ts', line: 53, text: 'the cookie is cleared' })];
|
|
48
|
-
const result = score(round, 'abcdef0123456', items, 1200, undefined);
|
|
49
|
-
expect(result.points).toEqual([
|
|
50
|
-
{ id: 'P1', point: 'dead export', caught: true, noted: false, by: 'unused-export src/lib/server/billing/invoices.ts:40' },
|
|
51
|
-
{ id: 'P2', point: 'copies the accumulator', caught: false, noted: true },
|
|
52
|
-
{ id: 'P3', point: 'drops the currency', caught: false, noted: false },
|
|
53
|
-
]);
|
|
54
|
-
expect(result.falseBlocks).toHaveLength(1);
|
|
55
|
-
expect(backtestPassed([result])).toBe(false);
|
|
56
|
-
expect(formatBacktest([result])).toContain('r1 at abcdef012: 1/3 caught, 1 false block(s), 3 finding(s), 1s; caught by unused-export 1');
|
|
57
|
-
expect(formatBacktest([result])).toContain('noted P2 copies the accumulator (advisory only)');
|
|
58
|
-
expect(formatBacktest([result])).toContain('MISSED P3');
|
|
59
|
-
expect(formatBacktest([result])).toContain('FALSE reviewer src/hooks/session.ts:53');
|
|
60
|
-
});
|
|
61
|
-
it('passes only when every point is caught, nothing good is flagged and the reviewer answered', () => {
|
|
62
|
-
const all = [item(), item({ gate: 'quadratic-copy', file: 'src/totals.ts', text: 'copied whole' }), item({ gate: 'reviewer', file: 'src/checkout.ts', text: 'drops the currency' })];
|
|
63
|
-
expect(backtestPassed([score(round, 'h', all, 1, undefined)])).toBe(true);
|
|
64
|
-
expect(backtestPassed([score(round, 'h', all, 1, 'the reviewer did not answer')])).toBe(false);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
describe('rigour backtest on a repository', () => {
|
|
68
|
-
let repo;
|
|
69
|
-
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
|
|
70
|
-
const write = (rel, body) => {
|
|
71
|
-
fs.mkdirSync(path.dirname(path.join(repo, rel)), { recursive: true });
|
|
72
|
-
fs.writeFileSync(path.join(repo, rel), body);
|
|
73
|
-
};
|
|
74
|
-
beforeEach(() => {
|
|
75
|
-
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'backtest-'));
|
|
76
|
-
git('init', '-q', '-b', 'main');
|
|
77
|
-
git('config', 'user.email', 't@example.com');
|
|
78
|
-
git('config', 'user.name', 't');
|
|
79
|
-
git('config', 'commit.gpgsign', 'false');
|
|
80
|
-
write('src/app.ts', 'export const app = 1;\n');
|
|
81
|
-
git('add', '-A');
|
|
82
|
-
git('commit', '-qm', 'base');
|
|
83
|
-
git('checkout', '-qb', 'feature');
|
|
84
|
-
write('src/lib/util.ts', 'export function helper(): number {\n return 1;\n}\n');
|
|
85
|
-
git('add', '-A');
|
|
86
|
-
git('commit', '-qm', 'add a helper nothing uses');
|
|
87
|
-
});
|
|
88
|
-
afterEach(() => {
|
|
89
|
-
try {
|
|
90
|
-
git('worktree', 'prune');
|
|
91
|
-
}
|
|
92
|
-
catch { /* the repository may be gone */ }
|
|
93
|
-
fs.rmSync(repo, { recursive: true, force: true });
|
|
94
|
-
});
|
|
95
|
-
it('hides the review itself for a round, and shows the approval for an approved head', async () => {
|
|
96
|
-
const commit = git('rev-parse', 'HEAD');
|
|
97
|
-
const base = git('rev-parse', 'main');
|
|
98
|
-
const lines = [];
|
|
99
|
-
const ledger = { rounds: [
|
|
100
|
-
{ id: 'r1', commit, base, reviewed_at: '2026-09-28T15:12:53Z', points: [], must_not_flag: [] },
|
|
101
|
-
{ id: 'pr1-approved', commit, base, reviewed_at: '2026-09-28T15:12:53Z', approved: true, points: [], must_not_flag: [] },
|
|
102
|
-
{ id: 'odd', commit, base, reviewed_at: 'not a date', approved: true, points: [], must_not_flag: [] },
|
|
103
|
-
] };
|
|
104
|
-
await runBacktest(repo, ConfigSchema.parse({ version: 1 }), ledger, { progress: line => lines.push(line), collect: async () => ({ items: [] }) });
|
|
105
|
-
expect(lines.filter(l => l.includes('reviews hidden from')).map(l => l.replace(/^.*reviews hidden from /, ''))).toEqual(['2026-09-28T15:12:53Z', '2026-09-28T15:12:54.000Z', 'not a date']);
|
|
106
|
-
}, 60_000);
|
|
107
|
-
it('reviews the reviewed commit in a worktree with the review hidden, scores it, and leaves the checkout alone', async () => {
|
|
108
|
-
const reviewed = git('rev-parse', 'HEAD');
|
|
109
|
-
const base = git('rev-parse', 'main');
|
|
110
|
-
git('checkout', '-q', 'main'); // the person runs it from wherever they are
|
|
111
|
-
write(LEDGER_PATH, JSON.stringify({ rounds: [{
|
|
112
|
-
id: 'r1', commit: reviewed, base,
|
|
113
|
-
points: [
|
|
114
|
-
{ id: 'R1-1', point: 'helper is never imported', file: 'util\\.ts', lines: [1, 3] },
|
|
115
|
-
{ id: 'R1-2', point: 'something no gate knows', file: 'util', text: 'quadratic' },
|
|
116
|
-
],
|
|
117
|
-
must_not_flag: [{ file: 'app\\.ts', lines: [1, 1] }],
|
|
118
|
-
}] }));
|
|
119
|
-
const config = ConfigSchema.parse({ version: 1, gates: { unused_exports: { block: true } } });
|
|
120
|
-
const results = await runBacktest(repo, config, loadLedger(repo), { progress: () => undefined });
|
|
121
|
-
expect(results).toHaveLength(1);
|
|
122
|
-
expect(results[0].points).toMatchObject([{ id: 'R1-1', caught: true, by: 'unused-export src/lib/util.ts:1' }, { id: 'R1-2', caught: false, noted: false }]);
|
|
123
|
-
expect(results[0].falseBlocks).toEqual([]);
|
|
124
|
-
expect(results[0].head).toBe(reviewed);
|
|
125
|
-
expect(backtestPassed(results)).toBe(false);
|
|
126
|
-
expect(git('branch', '--show-current')).toBe('main');
|
|
127
|
-
expect(fs.existsSync(path.join(repo, 'src/lib/util.ts'))).toBe(false); // the checkout did not move
|
|
128
|
-
expect(fs.existsSync(path.join(repo, '.git/rigour-backtest/checkout/src/lib/util.ts'))).toBe(true);
|
|
129
|
-
expect(fs.readdirSync(path.join(repo, '.rigour/backtest'))).toEqual([`r1-${reviewed.slice(0, 12)}.json`]);
|
|
130
|
-
// The worktree is reused on the next run.
|
|
131
|
-
const again = await runBacktest(repo, config, loadLedger(repo), { round: 'r1', progress: () => undefined });
|
|
132
|
-
expect(again[0].points[0].caught).toBe(true);
|
|
133
|
-
}, 60_000); // a real worktree and a typed review: over 10s on a Windows runner
|
|
134
|
-
it('warns when the base is older than the main the head already merged in', async () => {
|
|
135
|
-
const forked = git('rev-parse', 'main');
|
|
136
|
-
git('checkout', '-q', 'main');
|
|
137
|
-
write('src/main-only.ts', 'export const later = 1;\n');
|
|
138
|
-
git('add', '-A');
|
|
139
|
-
git('commit', '-qm', 'main moves on');
|
|
140
|
-
const merged = git('rev-parse', 'HEAD');
|
|
141
|
-
git('checkout', '-q', 'feature');
|
|
142
|
-
git('merge', '-q', '--no-edit', 'main');
|
|
143
|
-
const reviewed = git('rev-parse', 'HEAD');
|
|
144
|
-
const config = ConfigSchema.parse({ version: 1 });
|
|
145
|
-
const run = async (base) => {
|
|
146
|
-
const lines = [];
|
|
147
|
-
const ledger = { rounds: [{ id: 'r1', commit: reviewed, base, points: [], must_not_flag: [] }] };
|
|
148
|
-
await runBacktest(repo, config, ledger, { progress: line => lines.push(line), collect: async () => ({ items: [] }) });
|
|
149
|
-
return lines.filter(line => line.includes('warning'));
|
|
150
|
-
};
|
|
151
|
-
expect(await run(forked)).toEqual([expect.stringContaining(`set the base to ${merged.slice(0, 9)}`)]);
|
|
152
|
-
expect(await run(merged)).toEqual([]);
|
|
153
|
-
git('update-ref', 'refs/heads/main', forked); // a local main older than the round's base: the ref is stale, not the base
|
|
154
|
-
expect(await run(merged)).toEqual([]);
|
|
155
|
-
git('update-ref', 'refs/heads/main', merged);
|
|
156
|
-
git('checkout', '-q', 'main');
|
|
157
|
-
git('merge', '-q', '--no-edit', 'feature'); // the pull request is merged: its head is on main now
|
|
158
|
-
expect(await run(merged)).toEqual([]);
|
|
159
|
-
}, 60_000);
|
|
160
|
-
it('moves one worktree from round to round, leaving nothing of the last round behind', async () => {
|
|
161
|
-
const reviewed = git('rev-parse', 'HEAD');
|
|
162
|
-
const base = git('rev-parse', 'main');
|
|
163
|
-
const seen = [];
|
|
164
|
-
const ledger = { rounds: [
|
|
165
|
-
{ id: 'after', commit: reviewed, base, points: [], must_not_flag: [] },
|
|
166
|
-
{ id: 'before', commit: base, base, points: [], must_not_flag: [] },
|
|
167
|
-
] };
|
|
168
|
-
await runBacktest(repo, ConfigSchema.parse({ version: 1 }), ledger, { progress: () => undefined, collect: async (tree) => {
|
|
169
|
-
seen.push({ tree, util: fs.existsSync(path.join(tree, 'src/lib/util.ts')), leftover: fs.existsSync(path.join(tree, 'scratch.txt')) });
|
|
170
|
-
fs.writeFileSync(path.join(tree, 'scratch.txt'), 'left by a round');
|
|
171
|
-
return { items: [] };
|
|
172
|
-
} });
|
|
173
|
-
expect(seen.map(s => s.tree)).toEqual([seen[0].tree, seen[0].tree]);
|
|
174
|
-
expect(seen.map(s => [s.util, s.leftover])).toEqual([[true, false], [false, false]]);
|
|
175
|
-
}, 60_000);
|
|
176
|
-
it('names a round or a commit it cannot find', async () => {
|
|
177
|
-
const config = ConfigSchema.parse({ version: 1 });
|
|
178
|
-
const ledger = { rounds: [{ id: 'r1', commit: '0000000000000000000000000000000000000000', base: 'main', points: [{ id: 'P', point: 'p', file: 'x', text: 'x' }], must_not_flag: [] }] };
|
|
179
|
-
await expect(runBacktest(repo, config, ledger, { round: 'r9' })).rejects.toThrow('no round "r9"');
|
|
180
|
-
await expect(runBacktest(repo, config, ledger, { progress: () => undefined })).rejects.toThrow('is not in this repository');
|
|
181
|
-
expect(() => loadLedger(repo)).toThrow('no ledger');
|
|
182
|
-
}, 60_000); // a real worktree and a typed review: over 10s on a Windows runner
|
|
183
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { splitIntroduced } from './baseline.js';
|
|
3
|
-
const finding = (file, title, details = 'd') => ({ id: 'AST_COMPLEXITY', title, details, files: [file] });
|
|
4
|
-
describe('splitIntroduced', () => {
|
|
5
|
-
it("drops what the base already had, even when the change moved its numbers", () => {
|
|
6
|
-
const base = { failures: [finding('a.ts', "Function 'f' has complexity of 105 (max: 10)", 'L12: x')], added: new Set() };
|
|
7
|
-
const head = [finding('a.ts', "Function 'f' has complexity of 109 (max: 10)", 'L19: x'), finding('a.ts', "Function 'g' has complexity of 11 (max: 10)")];
|
|
8
|
-
const split = splitIntroduced(head, base);
|
|
9
|
-
expect(split.preexisting.map(f => f.title)).toEqual(["Function 'f' has complexity of 109 (max: 10)"]);
|
|
10
|
-
expect(split.introduced.map(f => f.title)).toEqual(["Function 'g' has complexity of 11 (max: 10)"]);
|
|
11
|
-
});
|
|
12
|
-
it('matches each base finding once, so a second copy of an old problem is new', () => {
|
|
13
|
-
const old = finding('a.ts', 'Unhandled promise', 'L3: then');
|
|
14
|
-
const split = splitIntroduced([old, finding('a.ts', 'Unhandled promise', 'L9: then')], { failures: [old], added: new Set() });
|
|
15
|
-
expect(split.preexisting).toHaveLength(1);
|
|
16
|
-
expect(split.introduced).toHaveLength(1);
|
|
17
|
-
});
|
|
18
|
-
it('never treats a finding in a file the change adds as old', () => {
|
|
19
|
-
const f = finding('new.ts', 'Naming drift');
|
|
20
|
-
expect(splitIntroduced([f], { failures: [f], added: new Set(['new.ts']) }).introduced).toEqual([f]);
|
|
21
|
-
});
|
|
22
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'child_process';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
-
import { ConfigSchema } from '../types/index.js';
|
|
7
|
-
import { mergeConflicts, staleReferences } from './branch-checks.js';
|
|
8
|
-
let repo;
|
|
9
|
-
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' });
|
|
10
|
-
const write = (rel, body) => {
|
|
11
|
-
fs.mkdirSync(path.dirname(path.join(repo, rel)), { recursive: true });
|
|
12
|
-
fs.writeFileSync(path.join(repo, rel), body);
|
|
13
|
-
};
|
|
14
|
-
const config = ConfigSchema.parse({ version: 1 });
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'branch-checks-'));
|
|
17
|
-
git('init', '-q', '-b', 'main');
|
|
18
|
-
git('config', 'user.email', 't@example.com');
|
|
19
|
-
git('config', 'user.name', 't');
|
|
20
|
-
git('config', 'commit.gpgsign', 'false');
|
|
21
|
-
write('src/limits.ts', 'export const LIMIT = 1;\n');
|
|
22
|
-
write('scripts/seed.ts', 'console.log(1);\n');
|
|
23
|
-
write('scripts/seed.tsx', 'console.log(2);\n');
|
|
24
|
-
git('add', '-A');
|
|
25
|
-
git('commit', '-qm', 'init');
|
|
26
|
-
});
|
|
27
|
-
afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
|
|
28
|
-
describe('branch checks', () => {
|
|
29
|
-
it('reports the files that conflict with main, and nothing when it merges cleanly', () => {
|
|
30
|
-
git('checkout', '-q', '-b', 'feature');
|
|
31
|
-
write('src/limits.ts', 'export const LIMIT = 2;\n');
|
|
32
|
-
git('commit', '-qam', 'feature');
|
|
33
|
-
expect(mergeConflicts(repo, 'main')).toEqual([]);
|
|
34
|
-
git('checkout', '-q', 'main');
|
|
35
|
-
write('src/limits.ts', 'export const LIMIT = 3;\n');
|
|
36
|
-
git('commit', '-qam', 'main moved');
|
|
37
|
-
git('checkout', '-q', 'feature');
|
|
38
|
-
expect(mergeConflicts(repo, 'main').map(f => [f.id, f.files?.[0]])).toEqual([['merge-conflict', 'src/limits.ts']]);
|
|
39
|
-
});
|
|
40
|
-
it('reports a mention of a file the branch deleted, but not of a longer path that starts with it', () => {
|
|
41
|
-
const base = git('rev-parse', 'HEAD').trim();
|
|
42
|
-
git('rm', '-q', 'scripts/seed.ts');
|
|
43
|
-
write('README.md', 'Run `scripts/seed.ts` first.\nThen scripts/seed.tsx.\n');
|
|
44
|
-
write('src/notes.ts', '// see scripts/seed.tsx\nexport const n = 1;\n');
|
|
45
|
-
const found = staleReferences(repo, base, config).map(f => [f.files?.[0], f.line]);
|
|
46
|
-
expect(found).toEqual([['README.md', 1]]);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
-
import { checkId, checkPrecisions, isMuted, precisionOf, readOutcomes, recordOutcome, rememberReported } from './check-outcomes.js';
|
|
6
|
-
import { dismissFinding, findingKey, quietSplit } from './quiet.js';
|
|
7
|
-
import { recordReviewOutcome } from './agent-fixes.js';
|
|
8
|
-
let cwd;
|
|
9
|
-
beforeEach(() => { cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'outcomes-')); });
|
|
10
|
-
afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
|
|
11
|
-
const finding = (id, title, file = 'src/a.ts', details = `${title} here`) => ({ id, title, details, severity: 'low', provenance: 'traditional', files: [file], line: 1 });
|
|
12
|
-
describe('check precision', () => {
|
|
13
|
-
it('starts at an honest 0.5 and follows the evidence', () => {
|
|
14
|
-
expect(precisionOf({ fixed: 0, dismissed: 0 })).toBe(0.5);
|
|
15
|
-
expect(precisionOf({ fixed: 0, dismissed: 8 })).toBe(0.1);
|
|
16
|
-
expect(precisionOf({ fixed: 8, dismissed: 0 })).toBe(0.9);
|
|
17
|
-
});
|
|
18
|
-
it('mutes only on clear evidence: enough outcomes and a low posterior', () => {
|
|
19
|
-
expect(isMuted({ fixed: 0, dismissed: 3 })).toBe(false);
|
|
20
|
-
expect(isMuted({ fixed: 0, dismissed: 5 })).toBe(true);
|
|
21
|
-
expect(isMuted({ fixed: 2, dismissed: 4 })).toBe(false);
|
|
22
|
-
});
|
|
23
|
-
it('names a check by gate and rule, so one rule in a gate never mutes its neighbours', () => {
|
|
24
|
-
expect(checkId(finding('ast', 'Function too complex'))).toBe('ast: Function too complex');
|
|
25
|
-
expect(checkId({ rule: 'ast' })).toBe('ast');
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
describe('outcomes from real decisions', () => {
|
|
29
|
-
it('attributes a dismissal by key to the check review reported it from', () => {
|
|
30
|
-
const f = finding('ast', 'Too many parameters');
|
|
31
|
-
rememberReported(cwd, [{ key: findingKey(f), check: checkId(f) }]);
|
|
32
|
-
expect(dismissFinding(cwd, findingKey(f), 'builder needs these')).toBe(true);
|
|
33
|
-
expect(dismissFinding(cwd, findingKey(f), 'again')).toBe(true);
|
|
34
|
-
expect(readOutcomes(cwd)).toEqual({ 'ast: Too many parameters': { fixed: 0, dismissed: 1 } });
|
|
35
|
-
});
|
|
36
|
-
it('counts a fix an agent made against the check that reported it', () => {
|
|
37
|
-
fs.mkdirSync(path.join(cwd, 'src'));
|
|
38
|
-
fs.writeFileSync(path.join(cwd, 'src/a.ts'), 'before\n');
|
|
39
|
-
recordReviewOutcome(cwd, [finding('semantic-bugs', 'Unbounded read')], ['src/a.ts']);
|
|
40
|
-
fs.writeFileSync(path.join(cwd, 'src/a.ts'), 'after\n');
|
|
41
|
-
recordReviewOutcome(cwd, [], ['src/a.ts']);
|
|
42
|
-
expect(checkPrecisions(cwd)).toEqual([expect.objectContaining({ check: 'semantic-bugs: Unbounded read', fixed: 1, dismissed: 0 })]);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
describe('quietSplit with precision', () => {
|
|
46
|
-
it('mutes an advisory check the team keeps dismissing, but never a proven one', () => {
|
|
47
|
-
for (let i = 0; i < 5; i++) {
|
|
48
|
-
recordOutcome(cwd, 'ast: Too many parameters', 'dismissed');
|
|
49
|
-
recordOutcome(cwd, 'security-patterns: Hard-coded secret', 'dismissed');
|
|
50
|
-
}
|
|
51
|
-
const split = quietSplit(cwd, [finding('ast', 'Too many parameters'), finding('ast', 'Function too complex'), finding('security-patterns', 'Hard-coded secret')]);
|
|
52
|
-
expect(split.muted).toBe(1);
|
|
53
|
-
expect(split.advisory.map(f => f.title)).toEqual(['Function too complex']);
|
|
54
|
-
expect(split.speaking.map(f => f.title)).toEqual(['Hard-coded secret']);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'child_process';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
-
import { ConfigSchema } from '../types/index.js';
|
|
7
|
-
import { parseDiff } from '../utils/diff.js';
|
|
8
|
-
import { diffFromGit } from './git-diff.js';
|
|
9
|
-
import { duplicateFunctionFailures } from './duplicate-functions.js';
|
|
10
|
-
import { loopCopyFailures } from './loop-copies.js';
|
|
11
|
-
import { partialFixFailures } from './partial-fixes.js';
|
|
12
|
-
import { partialWiringFailures } from './partial-wiring.js';
|
|
13
|
-
import { optionalParamFailures } from './optional-params.js';
|
|
14
|
-
import { queryPatternFailures } from './query-patterns.js';
|
|
15
|
-
let repo;
|
|
16
|
-
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' });
|
|
17
|
-
const write = (rel, body) => {
|
|
18
|
-
fs.mkdirSync(path.dirname(path.join(repo, rel)), { recursive: true });
|
|
19
|
-
fs.writeFileSync(path.join(repo, rel), body);
|
|
20
|
-
};
|
|
21
|
-
const config = ConfigSchema.parse({ version: 1 });
|
|
22
|
-
const changed = () => parseDiff(diffFromGit(repo));
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'code-patterns-'));
|
|
25
|
-
git('init', '-q', '-b', 'main');
|
|
26
|
-
git('config', 'user.email', 't@example.com');
|
|
27
|
-
git('config', 'user.name', 't');
|
|
28
|
-
git('config', 'commit.gpgsign', 'false');
|
|
29
|
-
write('README.md', 'x\n');
|
|
30
|
-
git('add', '-A');
|
|
31
|
-
git('commit', '-qm', 'init');
|
|
32
|
-
});
|
|
33
|
-
afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
|
|
34
|
-
describe('query patterns', () => {
|
|
35
|
-
it('reports offset paging in a loop and in a pager callback, not a single page a request asks for', () => {
|
|
36
|
-
write('src/sweep.ts', [
|
|
37
|
-
'export async function sweep(db: any) {',
|
|
38
|
-
' for (let from = 0; ; from += 500) {',
|
|
39
|
-
" const { data } = await db.from('rows').select('id').order('id').range(from, from + 499);",
|
|
40
|
-
' if (!data?.length) return;',
|
|
41
|
-
' }',
|
|
42
|
-
'}',
|
|
43
|
-
"export const all = (db: any) => readAllPages((from: number, to: number) => db.from('rows').select('id').order('id').range(from, to));",
|
|
44
|
-
"export function page(db: any, from: number, to: number) { return db.from('rows').select('id').range(from, to); }",
|
|
45
|
-
'declare function readAllPages(f: (from: number, to: number) => unknown): unknown;',
|
|
46
|
-
].join('\n'));
|
|
47
|
-
expect(queryPatternFailures(repo, changed(), config).map(f => [f.id, f.line])).toEqual([['offset-paging', 3], ['offset-paging', 7]]);
|
|
48
|
-
});
|
|
49
|
-
it('reports a window read only from its start, not a bounded window or everything since an event', () => {
|
|
50
|
-
write('src/window.ts', [
|
|
51
|
-
"export const open = (db: any, window: { from: string; to: string }) => db.from('events').select('id').gte('created_at', window.from);",
|
|
52
|
-
"export const closed = (db: any, window: { from: string; to: string }) => db.from('events').select('id').gte('created_at', window.from).lte('created_at', window.to);",
|
|
53
|
-
"export const since = (db: any, since: string) => db.from('events').select('id').gt('started_at', since);",
|
|
54
|
-
].join('\n'));
|
|
55
|
-
expect(queryPatternFailures(repo, changed(), config).map(f => [f.id, f.line])).toEqual([['unbounded-window', 1]]);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
describe('optional for tests', () => {
|
|
59
|
-
it('reports an optional parameter or option that every production call passes and only a test omits', () => {
|
|
60
|
-
write('src/build.ts', 'export function build(items: string[], opts: { resumes?: string[] }, limit?: number) { return [items, opts, limit]; }\n');
|
|
61
|
-
write('src/run.ts', "import { build } from './build';\nbuild(['a'], { resumes: [] }, 5);\n");
|
|
62
|
-
write('src/build.test.ts', "import { build } from './build';\nbuild(['a'], {});\n");
|
|
63
|
-
const found = optionalParamFailures(repo, changed(), config).map(f => f.details.match(/`([^`]+)`/)[1]).sort();
|
|
64
|
-
expect(found).toEqual(['limit', 'resumes']);
|
|
65
|
-
});
|
|
66
|
-
it('stays quiet when production itself omits it, or when tests pass it too', () => {
|
|
67
|
-
write('src/build.ts', 'export function build(items: string[], limit?: number) { return [items, limit]; }\n');
|
|
68
|
-
write('src/run.ts', "import { build } from './build';\nbuild(['a']);\nbuild(['b'], 2);\n");
|
|
69
|
-
write('src/build.test.ts', "import { build } from './build';\nbuild(['a']);\n");
|
|
70
|
-
expect(optionalParamFailures(repo, changed(), config)).toEqual([]);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
describe('duplicate functions', () => {
|
|
74
|
-
const body = (name) => `export function ${name}(x: number) {\n const a = x + 1;\n const b = a * 2;\n if (b > 10) return b;\n console.log(a, b);\n return a;\n}\n`;
|
|
75
|
-
it('reports a changed function whose body copies another, in TypeScript and Svelte alike', () => {
|
|
76
|
-
write('src/a.ts', body('first'));
|
|
77
|
-
write('src/b.svelte', `<script lang="ts">\n${body('second').replace('export ', '')}</script>\n<p>hi</p>\n`);
|
|
78
|
-
const found = duplicateFunctionFailures(repo, changed(), config);
|
|
79
|
-
expect(found.map(f => [f.files?.[0], f.line])).toEqual([['src/b.svelte', 2]]);
|
|
80
|
-
expect(found[0].details).toContain('`first` in `src/a.ts:1`');
|
|
81
|
-
});
|
|
82
|
-
it('ignores small look-alike functions', () => {
|
|
83
|
-
write('src/a.ts', 'export const one = (x: number) => { return x + 1; };\n');
|
|
84
|
-
write('src/b.ts', 'export const two = (x: number) => { return x + 1; };\n');
|
|
85
|
-
expect(duplicateFunctionFailures(repo, changed(), config)).toEqual([]);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
describe('quadratic copies', () => {
|
|
89
|
-
it('reports an accumulator spread into a new copy on every step, not a push', () => {
|
|
90
|
-
write('src/group.ts', [
|
|
91
|
-
'export function group(items: { day: string }[]) {',
|
|
92
|
-
' const groups = new Map<string, { day: string }[]>();',
|
|
93
|
-
' for (const item of items) groups.set(item.day, [...(groups.get(item.day) ?? []), item]);',
|
|
94
|
-
' const ids = items.reduce((acc: string[], item) => [...acc, item.day], []);',
|
|
95
|
-
' const fast = new Map<string, string[]>();',
|
|
96
|
-
' for (const item of items) (fast.get(item.day) ?? fast.set(item.day, []).get(item.day)!).push(item.day);',
|
|
97
|
-
' return { groups, ids, fast };',
|
|
98
|
-
'}',
|
|
99
|
-
].join('\n'));
|
|
100
|
-
expect(loopCopyFailures(repo, changed(), config).map(f => f.line)).toEqual([3, 4]);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
describe('partial fixes', () => {
|
|
104
|
-
it('names the places that still test the narrower condition a new predicate widened', () => {
|
|
105
|
-
write('src/routes/session/page.server.ts', 'export function load(snapshot: any) {\n if (snapshot.answeredCount > 0) return 1;\n return 0;\n}\n');
|
|
106
|
-
write('src/routes/session/page.svelte', '<script lang="ts">\n const show = data.snapshot.answeredCount > 0;\n</script>\n');
|
|
107
|
-
git('add', '-A');
|
|
108
|
-
git('commit', '-qm', 'before');
|
|
109
|
-
write('src/routes/session/page.server.ts', 'export function load(snapshot: any) {\n if (snapshot.answeredCount > 0) return 1;\n return 0;\n}\nexport const resume = (snapshot: any) => {\n const hasSaved = snapshot.answers.length > 0 || snapshot.answeredCount > 0;\n return hasSaved;\n};\n');
|
|
110
|
-
const found = partialFixFailures(repo, changed(), config);
|
|
111
|
-
expect(found.map(f => [f.id, f.line])).toEqual([['partial-fix', 6]]);
|
|
112
|
-
expect(found[0].details).toContain('page.server.ts:2');
|
|
113
|
-
expect(found[0].details).toContain('page.svelte:2');
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
describe('partial wiring', () => {
|
|
117
|
-
const mount = (extra) => `<script lang="ts">\n import Player from '$lib/Player.svelte';\n</script>\n<Player\n mode="quiz"\n {questions}${extra}\n/>\n`;
|
|
118
|
-
it('reports a callback added to most same-kind mounts of a local component but not one', () => {
|
|
119
|
-
write('src/lib/Player.svelte', '<script lang="ts">let { onResume } = $props();</script>\n');
|
|
120
|
-
for (const route of ['a', 'b', 'c'])
|
|
121
|
-
write(`src/routes/${route}/+page.svelte`, mount(''));
|
|
122
|
-
write('src/routes/d/+page.svelte', mount('').replace('mode="quiz"', 'mode="exam"'));
|
|
123
|
-
git('add', '-A');
|
|
124
|
-
git('commit', '-qm', 'mounts');
|
|
125
|
-
write('src/routes/a/+page.svelte', mount('\n {onResume}'));
|
|
126
|
-
write('src/routes/b/+page.svelte', mount('\n {onResume}'));
|
|
127
|
-
const found = partialWiringFailures(repo, changed(), config);
|
|
128
|
-
expect(found).toHaveLength(1);
|
|
129
|
-
expect(found[0].details).toContain('src/routes/c/+page.svelte');
|
|
130
|
-
expect(found[0].details).not.toContain('routes/d'); // another kind of mount
|
|
131
|
-
});
|
|
132
|
-
it('ignores styling props and components from packages', () => {
|
|
133
|
-
const icon = (extra) => `<script lang="ts">\n import { Star } from 'lucide-svelte';\n</script>\n<Star class="h-4"${extra} />\n`;
|
|
134
|
-
for (const route of ['a', 'b', 'c'])
|
|
135
|
-
write(`src/routes/${route}/+page.svelte`, icon(''));
|
|
136
|
-
git('add', '-A');
|
|
137
|
-
git('commit', '-qm', 'icons');
|
|
138
|
-
write('src/routes/a/+page.svelte', icon(' onHover={go}'));
|
|
139
|
-
write('src/routes/b/+page.svelte', icon(' onHover={go}'));
|
|
140
|
-
expect(partialWiringFailures(repo, changed(), config)).toEqual([]);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'child_process';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
-
import { ConfigSchema } from '../types/index.js';
|
|
7
|
-
import { diffFromGit } from './git-diff.js';
|
|
8
|
-
import { mustFix } from './quiet.js';
|
|
9
|
-
import { orphanFileFailures } from './orphan-files.js';
|
|
10
|
-
import { unusedExportFailures } from './unused-exports.js';
|
|
11
|
-
let repo;
|
|
12
|
-
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' });
|
|
13
|
-
const write = (rel, body) => {
|
|
14
|
-
fs.mkdirSync(path.dirname(path.join(repo, rel)), { recursive: true });
|
|
15
|
-
fs.writeFileSync(path.join(repo, rel), body);
|
|
16
|
-
};
|
|
17
|
-
const config = (gates = {}) => ConfigSchema.parse({ version: 1, gates });
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'dead-code-'));
|
|
20
|
-
git('init', '-q', '-b', 'main');
|
|
21
|
-
git('config', 'user.email', 't@example.com');
|
|
22
|
-
git('config', 'user.name', 't');
|
|
23
|
-
git('config', 'commit.gpgsign', 'false');
|
|
24
|
-
write('src/index.ts', "import { start } from './app';\nstart();\n");
|
|
25
|
-
write('src/app.ts', 'export function start() {}\n');
|
|
26
|
-
write('package.json', '{"scripts":{"seed":"node scripts/seed.mjs"}}\n');
|
|
27
|
-
git('add', '-A');
|
|
28
|
-
git('commit', '-qm', 'init');
|
|
29
|
-
});
|
|
30
|
-
afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
|
|
31
|
-
describe('unused exports', () => {
|
|
32
|
-
it('reports an added export no other file names, on its own line', () => {
|
|
33
|
-
write('src/app.ts', 'export function start() {}\n\nexport const LIMIT = 5;\nexport function lonely() { return LIMIT; }\n');
|
|
34
|
-
write('src/use.ts', "import { LIMIT } from './app';\nconsole.log(LIMIT);\n");
|
|
35
|
-
const failures = unusedExportFailures(repo, diffFromGit(repo), config());
|
|
36
|
-
expect(failures.map(f => [f.files?.[0], f.line, f.id])).toEqual([['src/app.ts', 4, 'unused-export']]);
|
|
37
|
-
expect(failures[0].details).toContain('`lonely`');
|
|
38
|
-
});
|
|
39
|
-
it('does not count a test as a consumer: an export only its tests import is dead in production', () => {
|
|
40
|
-
write('src/app.ts', 'export function start() {}\n\nexport function onlyTested() { return 1; }\nexport function used() { return 2; }\n');
|
|
41
|
-
write('src/app.test.ts', "import { onlyTested, used } from './app';\nonlyTested();\nused();\n");
|
|
42
|
-
write('tests/helpers.ts', "import { onlyTested } from '../src/app';\nonlyTested();\n");
|
|
43
|
-
write('src/use.ts', "import { used } from './app';\nused();\n");
|
|
44
|
-
const names = unusedExportFailures(repo, diffFromGit(repo), config()).map(f => f.details.match(/`([^`]+)`/)[1]);
|
|
45
|
-
expect(names).toEqual(['onlyTested']);
|
|
46
|
-
});
|
|
47
|
-
it('reports a re-export nothing imports, without counting the module it comes from as a use', () => {
|
|
48
|
-
write('src/types.ts', 'export type Row = { id: string };\nexport type Used = { n: number };\n');
|
|
49
|
-
write('src/index.ts', "import { start } from './app';\nstart();\nexport type { Row, Used } from './types';\n");
|
|
50
|
-
write('src/consumer.ts', "import type { Used } from './index';\nexport const u: Used = { n: 1 };\nconsole.log(u);\n");
|
|
51
|
-
const names = unusedExportFailures(repo, diffFromGit(repo), config()).map(f => [f.files?.[0], f.details.match(/`([^`]+)`/)[1]]);
|
|
52
|
-
expect(names).toContainEqual(['src/index.ts', 'Row']);
|
|
53
|
-
expect(names).not.toContainEqual(['src/index.ts', 'Used']);
|
|
54
|
-
});
|
|
55
|
-
it('never reports a type-test file as orphaned: tsd runs test-d/ and *.test-d.ts by itself', () => {
|
|
56
|
-
write('test-d/types.ts', "import { start } from '../src/app';\nstart();\n");
|
|
57
|
-
write('src/app.test-d.ts', "import { start } from './app';\nstart();\n");
|
|
58
|
-
const orphans = orphanFileFailures(repo, diffFromGit(repo), config()).map(f => f.files?.[0]);
|
|
59
|
-
expect(orphans).not.toContain('test-d/types.ts');
|
|
60
|
-
expect(orphans).not.toContain('src/app.test-d.ts');
|
|
61
|
-
});
|
|
62
|
-
it('skips what a framework calls by name, and names the team allows', () => {
|
|
63
|
-
write('src/routes/+page.server.ts', 'export const load = () => ({});\nexport const helper = 1;\n');
|
|
64
|
-
write('src/plugin.ts', 'export const register = () => {};\n');
|
|
65
|
-
const names = unusedExportFailures(repo, diffFromGit(repo), config({ unused_exports: { allow: ['register'] } }))
|
|
66
|
-
.map(f => f.details.match(/`([^`]+)`/)[1]);
|
|
67
|
-
expect(names).toEqual(['helper']);
|
|
68
|
-
});
|
|
69
|
-
it('does not count the same word in a file that never imports the module', () => {
|
|
70
|
-
write('src/payload.ts', 'export function eventId(kind: string) { return kind; }\nexport const used = eventId("x");\n');
|
|
71
|
-
write('src/main2.ts', "import { used } from './payload';\nconsole.log(used);\n");
|
|
72
|
-
write('src/routes/[eventId]/handler.ts', "const eventId = 'route-param';\nconsole.log(eventId);\n");
|
|
73
|
-
write('src/schedule.ts', 'const eventId = 7;\nconsole.log(eventId);\n');
|
|
74
|
-
const names = unusedExportFailures(repo, diffFromGit(repo), config()).map(f => f.details.match(/`([^`]+)`/)[1]);
|
|
75
|
-
expect(names).toContain('eventId');
|
|
76
|
-
expect(names).not.toContain('used');
|
|
77
|
-
});
|
|
78
|
-
it('reports a type imported then exported again that nothing imports from here', () => {
|
|
79
|
-
write('src/entry.ts', "export type Entry = 'a' | 'b';\n");
|
|
80
|
-
write('src/resume.ts', "import type { Entry } from './entry';\nexport type { Entry };\nexport const pick = (e: Entry) => e;\n");
|
|
81
|
-
write('src/main.ts', "import { pick } from './resume';\nconsole.log(pick('a'));\n");
|
|
82
|
-
const found = unusedExportFailures(repo, diffFromGit(repo), config()).map(f => [f.files?.[0], f.details.match(/`([^`]+)`/)[1]]);
|
|
83
|
-
expect(found).toContainEqual(['src/resume.ts', 'Entry']);
|
|
84
|
-
expect(found).not.toContainEqual(['src/entry.ts', 'Entry']);
|
|
85
|
-
});
|
|
86
|
-
it("never counts Rigour's own report as a use", () => {
|
|
87
|
-
write('src/app.ts', 'export function start() {}\nexport const orphanName = 1;\n');
|
|
88
|
-
write('rigour-report.json', '{"failures":[{"details":"orphanName"}]}\n');
|
|
89
|
-
expect(unusedExportFailures(repo, diffFromGit(repo), config())).toHaveLength(1);
|
|
90
|
-
});
|
|
91
|
-
it('reports dead code as a note unless the team blocks on it', () => {
|
|
92
|
-
write('src/app.ts', 'export function start() {}\nexport function lonely() { return 1; }\n');
|
|
93
|
-
write('src/new-tool.ts', 'export const tool = 1;\n');
|
|
94
|
-
const found = (gates = {}) => [
|
|
95
|
-
...unusedExportFailures(repo, diffFromGit(repo), config(gates)),
|
|
96
|
-
...orphanFileFailures(repo, diffFromGit(repo), config(gates)),
|
|
97
|
-
];
|
|
98
|
-
expect(found().map(f => [f.id, mustFix(f)])).toEqual(expect.arrayContaining([['unused-export', false], ['orphan-file', false]]));
|
|
99
|
-
const strict = found({ unused_exports: { block: true }, orphan_files: { block: true } });
|
|
100
|
-
expect(strict.length).toBeGreaterThan(0);
|
|
101
|
-
expect(strict.every(mustFix)).toBe(true);
|
|
102
|
-
});
|
|
103
|
-
it('is off when disabled', () => {
|
|
104
|
-
write('src/app.ts', 'export function start() {}\nexport const x = 1;\n');
|
|
105
|
-
expect(unusedExportFailures(repo, diffFromGit(repo), config({ unused_exports: { enabled: false } }))).toEqual([]);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
describe('package layout', () => {
|
|
109
|
-
it("never reports a package entry point's exports, which are its public API", () => {
|
|
110
|
-
write('packages/lib/package.json', '{"name":"lib","main":"dist/index.js","types":"dist/index.d.ts"}\n');
|
|
111
|
-
write('packages/lib/src/index.ts', "export { helper, type Shape } from './helper';\n");
|
|
112
|
-
write('packages/lib/src/helper.ts', 'export type Shape = { a: 1 };\nexport const helper = 1;\n');
|
|
113
|
-
write('packages/lib/src/tools/index.ts', "export { toolA } from './a';\n");
|
|
114
|
-
write('packages/lib/src/tools/a.ts', 'export const toolA = 1;\n');
|
|
115
|
-
write('packages/lib/src/index.ts', "export { helper, type Shape } from './helper';\nexport * from './tools/index.js';\n");
|
|
116
|
-
const reported = unusedExportFailures(repo, diffFromGit(repo), config()).map(f => f.files?.[0]);
|
|
117
|
-
expect(reported).not.toContain('packages/lib/src/index.ts');
|
|
118
|
-
expect(reported).not.toContain('packages/lib/src/tools/index.ts'); // re-exported whole by the entry
|
|
119
|
-
});
|
|
120
|
-
it('keeps an exported type in a signature when the project emits declarations, and only then', () => {
|
|
121
|
-
write('src/run.ts', 'export interface RunResult { ok: boolean }\nexport function run(): RunResult { return { ok: true }; }\n');
|
|
122
|
-
write('src/main.ts', "import { run } from './run';\nrun();\n");
|
|
123
|
-
const names = () => unusedExportFailures(repo, diffFromGit(repo), config()).map(f => f.details.match(/`([^`]+)`/)[1]);
|
|
124
|
-
expect(names()).toContain('RunResult'); // an application: drop the export
|
|
125
|
-
write('tsconfig.base.json', '{ "compilerOptions": { "declaration": true } }\n');
|
|
126
|
-
write('tsconfig.json', '{ "extends": "./tsconfig.base.json" }\n');
|
|
127
|
-
expect(names()).not.toContain('RunResult'); // a library: TypeScript needs it exported
|
|
128
|
-
});
|
|
129
|
-
it("counts a package.json bin or main path as running its source file", () => {
|
|
130
|
-
write('packages/tool/package.json', '{"name":"tool","bin":{"tool":"dist/bin.js"}}\n');
|
|
131
|
-
write('packages/tool/src/bin.ts', 'console.log("hi");\n');
|
|
132
|
-
expect(orphanFileFailures(repo, diffFromGit(repo), config())).toEqual([]);
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
describe('orphaned files', () => {
|
|
136
|
-
it('reports new files nothing outside the new files reaches, including a pair that only import each other', () => {
|
|
137
|
-
write('src/wired.ts', 'export const w = 1;\n');
|
|
138
|
-
write('src/index.ts', "import { start } from './app';\nimport { w } from './wired';\nstart(w);\n");
|
|
139
|
-
write('src/lonely.ts', 'export const l = 1;\n');
|
|
140
|
-
write('src/pair/a.ts', "import { b } from './b';\nexport const a = b;\n");
|
|
141
|
-
write('src/pair/b.ts', "import { a } from './a';\nexport const b = a;\n");
|
|
142
|
-
write('scripts/seed.mjs', 'console.log(1);\n');
|
|
143
|
-
write('src/tested.ts', 'export const t = 1;\n');
|
|
144
|
-
write('src/tested.test.ts', "import { t } from './tested';\n"); // a test runs it: reached
|
|
145
|
-
const files = orphanFileFailures(repo, diffFromGit(repo), config()).map(f => f.files?.[0]).sort();
|
|
146
|
-
expect(files).toEqual(['src/lonely.ts', 'src/pair/a.ts', 'src/pair/b.ts']);
|
|
147
|
-
});
|
|
148
|
-
it('lets a file through when a docs page is its only mention, and honours allow globs', () => {
|
|
149
|
-
write('src/tool.ts', 'export const t = 1;\n');
|
|
150
|
-
write('docs/tool.md', 'Run `src/tool.ts` by hand.\n');
|
|
151
|
-
expect(orphanFileFailures(repo, diffFromGit(repo), config()).map(f => f.files?.[0])).toEqual(['src/tool.ts']);
|
|
152
|
-
expect(orphanFileFailures(repo, diffFromGit(repo), config({ orphan_files: { allow: ['src/tool.ts'] } }))).toEqual([]);
|
|
153
|
-
});
|
|
154
|
-
});
|