@rigour-labs/core 6.8.0 → 6.8.1-rc.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,93 +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 { execFileSync } from 'child_process';
|
|
6
|
-
import { readRepoRules, rulesForDiff, rulesSection, splitRules } from './repo-rules.js';
|
|
7
|
-
const AGENTS = `# Conventions
|
|
8
|
-
|
|
9
|
-
@SHARED.md
|
|
10
|
-
|
|
11
|
-
- **Migrations are append-only.** Add new files under \`migrations/\`; never edit applied ones.
|
|
12
|
-
- Every outbound send goes through \`deliverOrder()\` in \`src/lib/delivery.ts\`, which checks the permit.
|
|
13
|
-
- Keep components small.
|
|
14
|
-
|
|
15
|
-
Prefer \`fetchWithTimeout\` over a bare call when talking to partner APIs; a hung request blocks the queue.
|
|
16
|
-
`;
|
|
17
|
-
let repo;
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'repo-rules-'));
|
|
20
|
-
fs.writeFileSync(path.join(repo, 'AGENTS.md'), AGENTS);
|
|
21
|
-
fs.mkdirSync(path.join(repo, '.cursor/rules'), { recursive: true });
|
|
22
|
-
fs.writeFileSync(path.join(repo, '.cursor/rules/ui.mdc'), 'Never set `fullWidth` on `<Button>`; use `maxWidth` caps instead of stretching.\n');
|
|
23
|
-
});
|
|
24
|
-
afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
|
|
25
|
-
const diff = (file, added) => `+++ b/${file}\n@@ -1,0 +1,1 @@\n+${added}\n`;
|
|
26
|
-
describe('repository rules', () => {
|
|
27
|
-
it('splits a rules file into rules with the paths and specific identifiers they name', () => {
|
|
28
|
-
const rules = splitRules('AGENTS.md', AGENTS);
|
|
29
|
-
expect(rules.map(r => r.text.slice(0, 30))).toEqual(['**Migrations are append-only.*', 'Every outbound send goes throu', 'Prefer `fetchWithTimeout` over']);
|
|
30
|
-
expect(rules[1]).toMatchObject({ paths: ['src/lib/delivery.ts'], symbols: ['deliverOrder'] });
|
|
31
|
-
expect(rules[0].paths).toEqual(['migrations/']);
|
|
32
|
-
expect(rules.map(r => r.requirement)).toEqual([true, true, false]); // "never", "every"; "prefer" is guidance
|
|
33
|
-
// A section that only describes an exception, with no imperative, is guidance; one that ends in an imperative is a requirement.
|
|
34
|
-
const [exceptionOnly, withImperative] = splitRules('AGENTS.md', '- **One narrow exception:** the queue table is still literally named `study_jobs`, not renamed with the feature.\n\n- **One narrow exception:** the queue table is still literally named `study_jobs`. Import the `JOBS_TABLE` constant; do not inline the raw table name again.\n');
|
|
35
|
-
expect([exceptionOnly.requirement, withImperative.requirement]).toEqual([false, true]);
|
|
36
|
-
expect(rules[0].id).toMatch(/^[0-9a-f]{10}$/);
|
|
37
|
-
expect(splitRules('AGENTS.md', AGENTS)[0].id).toBe(rules[0].id); // stable across runs
|
|
38
|
-
});
|
|
39
|
-
it('keeps a rule\'s "Why" and "How to apply" paragraphs with it, and ranks a rule naming the change above one that only shares its words', () => {
|
|
40
|
-
const text = '- **Use the design system.** Every control comes from `src/lib/ui`.\n\n**Why:** one source of styling.\n\n**How to apply:** import from `$lib/ui`, never a raw `<button>`.\n\n- Bound both ends of every time window a scheduled job reads.\n\n- Name the index a new query relies on in the migrations.\n';
|
|
41
|
-
const rules = splitRules('AGENTS.md', text);
|
|
42
|
-
expect(rules.map(r => r.text.slice(0, 24))).toEqual(['**Use the design system.', 'Bound both ends of every', 'Name the index a new que']);
|
|
43
|
-
expect(rules[0].text).toContain('**How to apply:**');
|
|
44
|
-
fs.writeFileSync(path.join(repo, 'AGENTS.md'), text);
|
|
45
|
-
const change = diff('src/lib/ui/Button.svelte', 'const timeWindow = scheduledJob.readsRows();');
|
|
46
|
-
expect(rulesForDiff(repo, change, true, 10).map(r => r.text.slice(0, 12))).toEqual(['**Use the de', 'Bound both e']); // the path hit first, then shared words; the index rule shares nothing
|
|
47
|
-
expect(rulesForDiff(repo, change, true, 1)).toHaveLength(1);
|
|
48
|
-
});
|
|
49
|
-
it('shows only the rules that name what the change touches, and nothing when disabled', () => {
|
|
50
|
-
expect(rulesForDiff(repo, diff('migrations/2026_add.sql', 'alter table x;'), true).map(r => r.text.slice(0, 20))).toEqual(['**Migrations are app']);
|
|
51
|
-
expect(rulesForDiff(repo, diff('src/jobs/send.ts', 'await deliverOrder(order);'), true).map(r => r.text.slice(0, 20))).toEqual(['Every outbound send ']);
|
|
52
|
-
expect(rulesForDiff(repo, diff('src/ui/List.tsx', 'const x = 1;'), true)).toEqual([]);
|
|
53
|
-
expect(rulesForDiff(repo, diff('migrations/2026_add.sql', 'x'), false)).toEqual([]);
|
|
54
|
-
expect(rulesSection(rulesForDiff(repo, diff('src/a.ts', 'await fetchWithTimeout(url);'), true))).toContain('[AGENTS.md] Prefer `fetchWithTimeout`');
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
describe('the rule files a judge is given', () => {
|
|
58
|
-
it('follows @ imports from a rule file and reads AGENTS.md and CLAUDE.md in folders below the root, once each', () => {
|
|
59
|
-
fs.writeFileSync(path.join(repo, 'CLAUDE.md'), '@AGENTS.md\n@docs/conventions.md\n@../outside.md\n@/etc/hosts\n@docs/missing.md\n');
|
|
60
|
-
fs.mkdirSync(path.join(repo, 'docs'));
|
|
61
|
-
fs.writeFileSync(path.join(repo, 'docs/conventions.md'), '@../AGENTS.md\n\n- Never log `apiToken` from `src/auth/session.ts`, even partly.\n');
|
|
62
|
-
fs.writeFileSync(path.join(repo, 'SHARED.md'), '- Every job in `src/jobs/` takes `withLock()` before the first read.\n');
|
|
63
|
-
fs.mkdirSync(path.join(repo, 'services/billing'), { recursive: true });
|
|
64
|
-
fs.writeFileSync(path.join(repo, 'services/billing/AGENTS.md'), '- Amounts in `services/billing/` are integer cents via `toCents()`; never a float.\n');
|
|
65
|
-
execFileSync('git', ['-C', repo, 'init', '-q']);
|
|
66
|
-
execFileSync('git', ['-C', repo, 'add', '-A']);
|
|
67
|
-
const rules = readRepoRules(repo);
|
|
68
|
-
const by = (source) => rules.filter(r => r.source === source).map(r => r.text);
|
|
69
|
-
expect(by('docs/conventions.md')).toEqual(['Never log `apiToken` from `src/auth/session.ts`, even partly.']);
|
|
70
|
-
expect(by('SHARED.md')).toEqual(['Every job in `src/jobs/` takes `withLock()` before the first read.']); // imported by AGENTS.md
|
|
71
|
-
expect(by('services/billing/AGENTS.md')).toEqual(['Amounts in `services/billing/` are integer cents via `toCents()`; never a float.']);
|
|
72
|
-
expect(rules.filter(r => r.source === 'AGENTS.md')).toHaveLength(3); // imported twice, read once
|
|
73
|
-
expect(rules.some(r => /hosts|outside/.test(r.source))).toBe(false); // nothing outside the repository
|
|
74
|
-
});
|
|
75
|
-
it("applies a folder's own rules, and the rules it imports, only to changes in that folder; vendored folders add none", () => {
|
|
76
|
-
fs.mkdirSync(path.join(repo, 'services/billing'), { recursive: true });
|
|
77
|
-
fs.writeFileSync(path.join(repo, 'services/billing/AGENTS.md'), '@money.md\n\n- Never call `stripe.charges.create` directly from `services/billing/`; go through `ledgerClient`.\n');
|
|
78
|
-
fs.writeFileSync(path.join(repo, 'services/billing/money.md'), '- Every amount in `services/billing/` is integer cents via `toCents()`; never a float.\n');
|
|
79
|
-
fs.mkdirSync(path.join(repo, 'vendor/somelib'), { recursive: true });
|
|
80
|
-
fs.writeFileSync(path.join(repo, 'vendor/somelib/AGENTS.md'), '- Always run `make vendor-test` in `vendor/somelib/` before every commit.\n');
|
|
81
|
-
execFileSync('git', ['-C', repo, 'init', '-q']);
|
|
82
|
-
execFileSync('git', ['-C', repo, 'add', '-A']);
|
|
83
|
-
const all = readRepoRules(repo);
|
|
84
|
-
expect(all.filter(r => r.scope === 'services/billing/').map(r => r.source)).toEqual(['services/billing/AGENTS.md', 'services/billing/money.md']);
|
|
85
|
-
expect(all.some(r => r.source.startsWith('vendor/'))).toBe(false);
|
|
86
|
-
expect(all.filter(r => r.source === 'AGENTS.md').every(r => r.scope === undefined)).toBe(true);
|
|
87
|
-
// A change only in web/ that even names the billing words: the billing rules are not served, so never checked or broken.
|
|
88
|
-
const web = rulesForDiff(repo, diff('web/src/Price.tsx', 'const total = stripe.charges.create(toCents(amount));'), true, 15);
|
|
89
|
-
expect(web.some(r => r.scope)).toBe(false);
|
|
90
|
-
const billing = rulesForDiff(repo, diff('services/billing/charge.ts', 'const total = stripe.charges.create(toCents(amount));'), true, 15);
|
|
91
|
-
expect(billing.filter(r => r.scope).map(r => r.source).sort()).toEqual(['services/billing/AGENTS.md', 'services/billing/money.md']);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,249 +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 { gitIn, withActedOn } from './acted-on.js';
|
|
7
|
-
import { learnFromReviews } from './learn-from-reviews.js';
|
|
8
|
-
import { outcomeFor } from './outcomes.js';
|
|
9
|
-
import { rulesFromReviews } from './rules-from-reviews.js';
|
|
10
|
-
import { describeLesson, lessonView } from './team-lessons.js';
|
|
11
|
-
import { decideLesson, isSpecific, lessonState, lessonText, lessonsFromReview, matchLessons, mergeLessons, readLessons, writeLessons } from './lessons.js';
|
|
12
|
-
import { lessonsForDiff, lessonsSection } from './team-lessons.js';
|
|
13
|
-
let repo;
|
|
14
|
-
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
|
|
15
|
-
const write = (body) => fs.writeFileSync(path.join(repo, 'src/orders.ts'), body);
|
|
16
|
-
const commit = (message) => { git('add', '-A'); git('commit', '-qm', message); return git('rev-parse', 'HEAD'); };
|
|
17
|
-
const V1 = 'export async function save(db, order) {\n await db.insert(order);\n return order.id;\n}\n';
|
|
18
|
-
const V2 = 'export async function save(db, order) {\n await db.upsert(order, { onConflict: "id" });\n return order.id;\n}\n';
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'review-learning-'));
|
|
21
|
-
git('init', '-q', '-b', 'main');
|
|
22
|
-
git('config', 'user.email', 't@example.com');
|
|
23
|
-
git('config', 'user.name', 't');
|
|
24
|
-
git('config', 'commit.gpgsign', 'false');
|
|
25
|
-
fs.mkdirSync(path.join(repo, 'src'));
|
|
26
|
-
});
|
|
27
|
-
afterEach(() => { fs.rmSync(repo, { recursive: true, force: true }); });
|
|
28
|
-
const comment = (id, sha, start, body, prNumber = 7) => ({ id, prNumber, path: 'src/orders.ts', start, end: start, commit: sha, body, author: 'reviewer' });
|
|
29
|
-
describe('acted on', () => {
|
|
30
|
-
it('marks a comment whose lines changed before merge, and one nobody acted on, keeping both', () => {
|
|
31
|
-
write(V1);
|
|
32
|
-
const reviewed = commit('pr head');
|
|
33
|
-
write(V2);
|
|
34
|
-
const merged = commit('address review');
|
|
35
|
-
const marked = withActedOn(gitIn(repo), { number: 7, mergeSha: merged, mergedAt: '', comments: [
|
|
36
|
-
comment('a', reviewed, 2, '**Use an upsert keyed on `id` so a retry does not duplicate the order.**'),
|
|
37
|
-
comment('b', reviewed, 3, 'Nit: return the whole order?'),
|
|
38
|
-
], reviews: [] });
|
|
39
|
-
expect(marked.map(c => [c.id, c.actedOn])).toEqual([['a', true], ['b', false]]);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
describe('lessons', () => {
|
|
43
|
-
it('never makes a lesson of a bare path or a review bot\'s line-range scaffolding', () => {
|
|
44
|
-
expect(lessonText('src/routes/learner/+layout.ts')).toBe('');
|
|
45
|
-
expect(lessonText('`src/routes/learner/api/[planId]/+server.ts`')).toBe('');
|
|
46
|
-
expect(lessonText('In src/a.ts around lines 10-12: Check the lock before the first read.')).toBe('Check the lock before the first read.');
|
|
47
|
-
expect(lessonText('Around line 124-141: Add a test for the failed save.')).toBe('Add a test for the failed save.');
|
|
48
|
-
expect(lessonsFromReview({ id: '1', prNumber: 3, commit: 'c', author: 'rabbit[bot]', source: 'bot', body: '- `src/a/b.ts`\n- Bound both ends of the time window.' }, ['src/a/b.ts']).map(l => l.text))
|
|
49
|
-
.toEqual(['Bound both ends of the time window.']);
|
|
50
|
-
});
|
|
51
|
-
it('keeps the point of a review comment and drops tool output and markup', () => {
|
|
52
|
-
const body = '_⚠️ Potential issue_ | _🟠 Major_\n\n**Use an upsert keyed on `id`.**\n\n<details>\n<summary>🏁 Script executed</summary>\nrg -n insert\n</details>';
|
|
53
|
-
expect(lessonText(body)).toBe('Use an upsert keyed on `id`.');
|
|
54
|
-
expect(lessonText('Callers retry on timeout, so this insert duplicates orders. Please use upsert.')).toBe('Callers retry on timeout, so this insert duplicates orders.');
|
|
55
|
-
expect(lessonText('### Medium Severity\n\nThe retry loop never resets `attempt`, so a second failure gives up at once.')).toBe('The retry loop never resets `attempt`, so a second failure gives up at once.');
|
|
56
|
-
});
|
|
57
|
-
it('makes a candidate a lesson only on evidence, and keeps every piece of it', () => {
|
|
58
|
-
const point = (pr, prAuthor, source = 'person', reviewer = `reviewer-of-${pr}`) => ({ kind: 'point', pr, comment: `c${pr}${prAuthor}`, author: reviewer, source, prAuthor });
|
|
59
|
-
const lesson = (...evidence) => ({ id: 'x', text: 'Use upsert', file: 'src/orders.ts', symbols: [], state: 'candidate', evidence, createdAt: '', updatedAt: '' });
|
|
60
|
-
expect(lessonState(lesson(point(1, 'ann')))).toEqual({ state: 'candidate' });
|
|
61
|
-
expect(lessonState(lesson(point(1, 'ann'), point(2, 'ann')))).toEqual({ state: 'candidate' }); // two PRs, one author: weak, not enough
|
|
62
|
-
expect(lessonState(lesson(point(1, 'ann'), point(2, 'bob', 'bot')))).toEqual({ state: 'verified', promotedBy: 'recurrence' }); // a bot's point counts the same
|
|
63
|
-
const said = (e, text) => ({ ...e, text });
|
|
64
|
-
expect(lessonState(lesson(said(point(1, 'ann', 'bot', 'helper[bot]'), 'Around line 60-103: update the callers.'), said(point(2, 'bob', 'bot', 'helper[bot]'), 'Around line 12-14: update the loader.')))).toEqual({ state: 'candidate' }); // a bot rewording its own point is one source
|
|
65
|
-
expect(lessonState(lesson(said(point(1, 'ann', 'bot', 'helper[bot]'), 'Consider more tests.'), said(point(2, 'bob', 'bot', 'helper[bot]'), 'Consider more tests.')))).toEqual({ state: 'candidate' }); // one bot's template on every PR is one source
|
|
66
|
-
expect(lessonState(lesson(said(point(1, 'ann', 'person', 'lead'), 'Regenerate the API client after changing the schema.'), said(point(2, 'bob', 'person', 'lead'), 'The generated client is stale again.')))).toEqual({ state: 'verified', promotedBy: 'recurrence' }); // a senior re-raising it in their own words
|
|
67
|
-
expect(lessonState(lesson(point(1, 'ann'), point(2, 'bob'), { kind: 'norule', pr: 1, comment: 'n', author: '' }))).toEqual({ state: 'candidate' }); // no rule in it: never promoted again
|
|
68
|
-
const outcome = { kind: 'outcome', pr: 1, comment: 'o', author: '', detail: 'fixed later by abc' };
|
|
69
|
-
const counter = { kind: 'counter', pr: 1, comment: 'k', author: '', detail: 'unchanged 40 days' };
|
|
70
|
-
expect(lessonState(lesson(point(1, 'ann', 'bot'), outcome))).toEqual({ state: 'verified', promotedBy: 'outcome' });
|
|
71
|
-
expect(lessonState(lesson(point(1, 'ann'), point(2, 'bob'), counter))).toEqual({ state: 'candidate' }); // counter-evidence holds recurrence back
|
|
72
|
-
const accepted = { kind: 'accepted', pr: 1, comment: 'y', author: 'lead@x' };
|
|
73
|
-
const rejected = { kind: 'rejected', pr: 1, comment: 'n', author: 'lead@x' };
|
|
74
|
-
expect(lessonState(lesson(point(1, 'ann'), outcome, rejected))).toEqual({ state: 'rejected' }); // a person's decision wins
|
|
75
|
-
expect(lessonState(lesson(point(1, 'ann'), rejected, accepted))).toEqual({ state: 'verified', promotedBy: 'person' }); // the latest decision
|
|
76
|
-
expect(lessonState({ ...lesson({ pr: 1, comment: 'old', author: 'r' }), state: 'verified' })).toEqual({ state: 'verified', promotedBy: 'legacy' }); // a record from before evidence keeps its state
|
|
77
|
-
});
|
|
78
|
-
it('records a person accepting or rejecting a lesson as evidence, with who and why', () => {
|
|
79
|
-
writeLessons(repo, [{ id: 'x', text: 'Use upsert', file: 'src/orders.ts', symbols: [], state: 'candidate', evidence: [{ kind: 'point', pr: 1, comment: 'c', author: 'r' }], createdAt: '', updatedAt: '' }]);
|
|
80
|
-
expect(decideLesson(repo, 'x', 'rejected', 'lead@x', 'we accept duplicates here')).toMatchObject({ state: 'rejected' });
|
|
81
|
-
expect(readLessons(repo)[0].evidence.at(-1)).toMatchObject({ kind: 'rejected', author: 'lead@x', detail: 'we accept duplicates here' });
|
|
82
|
-
expect(matchLessons(readLessons(repo), { files: ['src/orders.ts'], symbols: new Set() }, { includeCandidates: true })).toEqual([]); // never served as a lesson
|
|
83
|
-
expect(decideLesson(repo, 'nope', 'accepted', 'lead@x')).toBeUndefined();
|
|
84
|
-
});
|
|
85
|
-
it('matches lessons by file, symbols and directory, verified only unless asked', () => {
|
|
86
|
-
const base = { symbols: ['insert'], evidence: [{ pr: 1, comment: 'c', author: 'r' }], createdAt: '', updatedAt: '' };
|
|
87
|
-
const lessons = [
|
|
88
|
-
{ ...base, id: '1', text: 'same file', file: 'src/orders.ts', state: 'verified' },
|
|
89
|
-
{ ...base, id: '2', text: 'other dir, two shared symbols', file: 'lib/x.ts', state: 'verified', symbols: ['insertOrder', 'orderId'] },
|
|
90
|
-
{ ...base, id: '6', text: 'a lesson about a runbook', file: 'docs/runbook.md', state: 'verified', symbols: ['insertOrder', 'orderId'] },
|
|
91
|
-
{ ...base, id: '5', text: 'other dir, one generic shared symbol', file: 'lib/z.ts', state: 'verified' },
|
|
92
|
-
{ ...base, id: '3', text: 'candidate', file: 'src/orders.ts', state: 'candidate' },
|
|
93
|
-
{ ...base, id: '4', text: 'unrelated', file: 'lib/y.ts', state: 'verified', symbols: ['other'] },
|
|
94
|
-
];
|
|
95
|
-
const change = { files: ['src/orders.ts'], symbols: new Set(['insert', 'insertOrder', 'orderId']) };
|
|
96
|
-
expect(matchLessons(lessons, change).map(l => l.id)).toEqual(['2', '1']); // two specific shared names outrank a same-file lesson with only generic ones
|
|
97
|
-
expect(matchLessons(lessons, change, { includeCandidates: true }).map(l => l.id)).toEqual(['2', '1', '3']);
|
|
98
|
-
// A lesson learned only from the pull request under review is its own reviews, already in front of the judge.
|
|
99
|
-
const own = { ...base, id: '7', text: 'from this very pull request', file: 'src/orders.ts', state: 'verified', evidence: [{ pr: 42, comment: 'c', author: 'r' }] };
|
|
100
|
-
const also = { ...own, id: '8', text: 'from this and another', evidence: [{ pr: 42, comment: 'c', author: 'r' }, { pr: 3, comment: 'd', author: 'r' }] };
|
|
101
|
-
expect(matchLessons([...lessons, own, also], change, { excludePr: 42 }).map(l => l.id).sort()).toEqual(['1', '2', '8']); // '7' is left out; '8' has evidence elsewhere too
|
|
102
|
-
// One file's many lessons never crowd out another file's only one.
|
|
103
|
-
const many = Array.from({ length: 6 }, (_, i) => ({ ...base, id: `m${i}`, text: `orders lesson ${i}`, file: 'src/orders.ts', state: 'verified', symbols: ['insertOrder', 'orderId'] }));
|
|
104
|
-
const lone = { ...base, id: 'lone', text: 'the only lesson about the manifest', file: 'src/manifest.sha', state: 'verified' };
|
|
105
|
-
const served = matchLessons([...many, lone], { files: ['src/orders.ts', 'src/manifest.sha'], symbols: new Set(['insertOrder', 'orderId']) }, { limit: 4, perFile: 3 });
|
|
106
|
-
expect(served.map(l => l.id)).toEqual(['m0', 'm1', 'm2', 'lone']);
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
describe('learning from one pull request as it goes', () => {
|
|
110
|
-
it('reads its reviews up to a time, and counts a point acted on by the head as of then', async () => {
|
|
111
|
-
write(V1);
|
|
112
|
-
const reviewed = commit('pr head');
|
|
113
|
-
write(V2);
|
|
114
|
-
const fixed = commit('address review');
|
|
115
|
-
const api = 'https://api.github.com/repos/acme/app';
|
|
116
|
-
const pages = {
|
|
117
|
-
[`${api}/pulls/42`]: { number: 42, user: { login: 'dev' }, head: { sha: fixed } },
|
|
118
|
-
[`${api}/pulls/42/commits?per_page=100&page=1`]: [
|
|
119
|
-
{ sha: reviewed, commit: { committer: { date: '2026-09-01T00:00:00Z' } } },
|
|
120
|
-
{ sha: fixed, commit: { committer: { date: '2026-09-02T00:00:00Z' } } },
|
|
121
|
-
],
|
|
122
|
-
[`${api}/pulls/42/comments?per_page=100&page=1`]: [],
|
|
123
|
-
[`${api}/pulls/42/reviews?per_page=100&page=1`]: [
|
|
124
|
-
{ id: 1, commit_id: reviewed, submitted_at: '2026-09-01T12:00:00Z', user: { login: 'priya' }, body: '- In src/orders.ts make the insert idempotent on retry.' },
|
|
125
|
-
{ id: 2, commit_id: fixed, submitted_at: '2026-09-06T00:00:00Z', user: { login: 'priya' }, body: '- A later point the learner must not see yet.' },
|
|
126
|
-
],
|
|
127
|
-
};
|
|
128
|
-
const fetchImpl = async (url) => ({ ok: url in pages, status: url in pages ? 200 : 404, json: async () => pages[url] });
|
|
129
|
-
const early = await learnFromReviews(repo, { token: 't', repo: 'acme/app', fetch: fetchImpl, pr: 42, until: '2026-09-01T18:00:00Z' });
|
|
130
|
-
expect(early).toMatchObject({ prs: 1, reviewBodies: 1, total: 1 }); // the later review is not seen yet
|
|
131
|
-
expect(readLessons(repo)[0]).toMatchObject({ file: 'src/orders.ts', text: 'In src/orders.ts make the insert idempotent on retry.', evidence: [{ pr: 42, author: 'priya', actedOn: false }] });
|
|
132
|
-
fs.rmSync(path.join(repo, '.rigour', 'review-lessons.json'));
|
|
133
|
-
await learnFromReviews(repo, { token: 't', repo: 'acme/app', fetch: fetchImpl, pr: 42, until: '2026-09-03T00:00:00Z' });
|
|
134
|
-
expect(readLessons(repo)[0].evidence[0]).toMatchObject({ actedOn: true }); // the fix landed before this cut
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
describe('outcome evidence from git', () => {
|
|
138
|
-
it('finds a later fix to the lines a point named, counts lines left alone against it, and never looks past the cut', () => {
|
|
139
|
-
write('export function total(order) {\n return order.items.length;\n}\n');
|
|
140
|
-
commit('base');
|
|
141
|
-
git('checkout', '-qb', 'feature');
|
|
142
|
-
write('export function total(order) {\n const n = order.items.length;\n return n;\n}\n');
|
|
143
|
-
const reviewed = commit('pr head');
|
|
144
|
-
git('checkout', '-q', 'main');
|
|
145
|
-
git('merge', '-q', '--no-ff', '--no-edit', 'feature');
|
|
146
|
-
const merge = git('rev-parse', 'HEAD');
|
|
147
|
-
write('// header\nexport function total(order) {\n const n = order.items?.length ?? 0;\n return n;\n}\n');
|
|
148
|
-
execFileSync('git', ['-C', repo, 'commit', '-qam', 'fix: total crashes on an order with no items'], { env: { ...process.env, GIT_COMMITTER_DATE: '2026-09-20T00:00:00Z' } });
|
|
149
|
-
const point = (start, actedOn = false) => ({ id: `p${start}`, text: 'Guard a missing items list.', file: 'src/orders.ts', symbols: [], state: 'candidate', at: { commit: reviewed, start, end: start }, evidence: [{ kind: 'point', pr: 7, comment: `c${start}`, author: 'bot', source: 'bot', actedOn }], createdAt: '', updatedAt: '' });
|
|
150
|
-
const pr = { number: 7, mergeSha: merge, mergedAt: '2026-09-01T00:00:00Z' };
|
|
151
|
-
const later = outcomeFor(gitIn(repo), point(2), pr, { mainRef: 'main' });
|
|
152
|
-
expect(later).toMatchObject({ kind: 'outcome', detail: expect.stringContaining('fix: total crashes on an order with no items') });
|
|
153
|
-
expect(outcomeFor(gitIn(repo), point(2), pr, { mainRef: 'main', until: '2026-09-10T00:00:00Z' })).toBeUndefined(); // before the fix, within the window: no verdict yet
|
|
154
|
-
expect(outcomeFor(gitIn(repo), point(3), pr, { mainRef: 'main' })).toMatchObject({ kind: 'counter' }); // `return n;` shipped and stayed, past the window
|
|
155
|
-
expect(outcomeFor(gitIn(repo), point(2, true), pr, { mainRef: 'main' })).toBeUndefined(); // acted on in the PR: no outcome to read
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
describe('team standards', () => {
|
|
159
|
-
it('verifies a standard when the same point recurs in another author\'s PR, and serves it only to a change it is about', () => {
|
|
160
|
-
const standard = (pr, text) => ({ id: `s${pr}`, text, file: '', symbols: [], state: 'candidate', evidence: [{ kind: 'point', pr, comment: `r${pr}`, author: `reviewer${pr}`, prAuthor: `dev${pr}` }], createdAt: '', updatedAt: '' });
|
|
161
|
-
const merged = mergeLessons([], [standard(1, 'Bound both ends of every time window a job reads.'), standard(2, 'Bound both ends of the time window this job reads.')]);
|
|
162
|
-
expect(merged.lessons).toHaveLength(1);
|
|
163
|
-
expect(merged.lessons[0].state).toBe('verified');
|
|
164
|
-
const unrelated = mergeLessons(merged.lessons, [standard(3, 'Log the request id on every error.')]);
|
|
165
|
-
expect(unrelated.lessons).toHaveLength(2);
|
|
166
|
-
expect(matchLessons(unrelated.lessons, { files: ['src/jobs/scan.ts'], symbols: new Set(['timeWindowStart', 'readRows']) }).map(l => l.text)).toEqual(['Bound both ends of every time window a job reads.']);
|
|
167
|
-
expect(matchLessons(unrelated.lessons, { files: ['src/ui/Dialog.svelte'], symbols: new Set(['onKeydown']) })).toEqual([]); // nothing in common: not served
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
describe('turning reviews into rules', () => {
|
|
171
|
-
const point = (id, text, file = '') => ({ id, text, file, symbols: [], state: 'candidate', evidence: [{ pr: 7, comment: `c-${id}`, author: 'priya' }], createdAt: '', updatedAt: '' });
|
|
172
|
-
it('keeps the rule behind a point, drops a status note, keeps an unanswered point as said, and keeps the words beside the rule', async () => {
|
|
173
|
-
const prompts = [];
|
|
174
|
-
const write = async (prompt) => {
|
|
175
|
-
prompts.push(prompt);
|
|
176
|
-
return 'Here you go:\n{"rules":[{"id":"a","rule":"Apply discounts before computing an invoice total.","file":null},{"id":"b","rule":null,"file":null}]}';
|
|
177
|
-
};
|
|
178
|
-
const points = [point('a', 'The invoice total is computed before the discount is applied.'), point('b', 'npm run check: 0 errors, lint clean.'), point('c', 'A retry posts the payment twice.')];
|
|
179
|
-
const out = await rulesFromReviews(points, write);
|
|
180
|
-
expect(out).toMatchObject({ rules: 1, dropped: 1, unanswered: 1 });
|
|
181
|
-
expect(out.lessons.map(l => [l.text, l.evidence.at(-1)?.kind])).toEqual([
|
|
182
|
-
['Apply discounts before computing an invoice total.', undefined], ['npm run check: 0 errors, lint clean.', 'norule'], ['A retry posts the payment twice.', undefined],
|
|
183
|
-
]); // the no-rule point is kept and marked, never sent again
|
|
184
|
-
expect(out.lessons[0].evidence[0].said).toBe('The invoice total is computed before the discount is applied.');
|
|
185
|
-
expect(prompts[0]).toContain('"said": "npm run check: 0 errors, lint clean."');
|
|
186
|
-
expect(describeLesson(lessonView({ ...out.lessons[0], evidence: [...out.lessons[0].evidence, { pr: 9, comment: 'x', author: 'sam' }] })))
|
|
187
|
-
.toBe('team standard: Apply discounts before computing an invoice total. (in their words: "The invoice total is computed before the discount is applied.") (acted on in PR #7, #9)');
|
|
188
|
-
});
|
|
189
|
-
it('never loses a point when the model cannot run, and keeps a file only when the point was about it', async () => {
|
|
190
|
-
const out = await rulesFromReviews([point('a', 'In src/run.ts the flag is read last.', 'src/run.ts')], async () => undefined);
|
|
191
|
-
expect(out.lessons.map(l => [l.text, l.file])).toEqual([['In src/run.ts the flag is read last.', 'src/run.ts']]);
|
|
192
|
-
const moved = await rulesFromReviews([point('a', 'A point about one place.', 'src/a.ts')], async () => '{"rules":[{"id":"a","rule":"A rule the model placed in another file.","file":"src/b.ts"}]}');
|
|
193
|
-
expect(moved.lessons[0].file).toBe('');
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
describe('isSpecific', () => {
|
|
197
|
-
it('counts identifiers that name something, not common words', () => {
|
|
198
|
-
expect(['orderId', 'max_rows', 'renderTransactionalMessage'].map(isSpecific)).toEqual([true, true, true]);
|
|
199
|
-
expect(['data', 'error', 'route', 'insert'].map(isSpecific)).toEqual([false, false, false, false]);
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
describe('learnFromReviews', () => {
|
|
203
|
-
it('reads merged PRs from GitHub, learns from acted-on comments, and shows them for the next change', async () => {
|
|
204
|
-
write(V1);
|
|
205
|
-
const reviewed = commit('pr head');
|
|
206
|
-
write(V2);
|
|
207
|
-
const merged = commit('address review');
|
|
208
|
-
const api = 'https://api.github.com/repos/acme/app';
|
|
209
|
-
const pages = {
|
|
210
|
-
[`${api}/pulls?state=closed&sort=updated&direction=desc&per_page=100&page=1`]: [
|
|
211
|
-
{ number: 7, merged_at: '2026-09-01T00:00:00Z', merge_commit_sha: merged, user: { login: 'dev' } },
|
|
212
|
-
{ number: 8, merged_at: null, merge_commit_sha: null },
|
|
213
|
-
],
|
|
214
|
-
[`${api}/pulls?state=closed&sort=updated&direction=desc&per_page=100&page=2`]: [],
|
|
215
|
-
[`${api}/pulls/7/comments?per_page=100`]: [
|
|
216
|
-
{ id: 11, path: 'src/orders.ts', original_line: 2, original_commit_id: reviewed, body: '**Use an upsert keyed on `id` so retries do not duplicate orders.**', user: { login: 'priya' } },
|
|
217
|
-
{ id: 12, path: 'src/orders.ts', original_line: 2, original_commit_id: reviewed, body: 'agreed', in_reply_to_id: 11, user: { login: 'sam' } },
|
|
218
|
-
{ id: 13, path: 'src/orders.ts', original_line: 2, original_commit_id: reviewed, body: '**Rename the variable to orderRow for clarity.**', user: { login: 'review-helper[bot]', type: 'Bot' } },
|
|
219
|
-
{ id: 14, path: 'src/orders.ts', original_line: 2, original_commit_id: reviewed, body: '**Self note: tidy this later.**', user: { login: 'dev' } },
|
|
220
|
-
],
|
|
221
|
-
[`${api}/pulls/7/reviews?per_page=100`]: [
|
|
222
|
-
{ id: 21, commit_id: reviewed, user: { login: 'priya' }, body: '## Review\n\n- Bound both ends of every time window a scheduled job reads.\n- In src/orders.ts the insert needs the idempotency key.\nThanks!' },
|
|
223
|
-
{ id: 22, commit_id: reviewed, user: { login: 'review-helper[bot]', type: 'Bot' }, body: '- Consider adding more unit tests for orders.' },
|
|
224
|
-
{ id: 23, commit_id: reviewed, user: { login: 'dev' }, body: '- Addressed every point in the next commit.' },
|
|
225
|
-
],
|
|
226
|
-
};
|
|
227
|
-
const fetchImpl = async (url) => ({ ok: url in pages, status: url in pages ? 200 : 404, json: async () => pages[url] });
|
|
228
|
-
const result = await learnFromReviews(repo, { token: 't', repo: 'acme/app', fetch: fetchImpl });
|
|
229
|
-
// Every point is a candidate, the bot's too; the author's own comments are not review points.
|
|
230
|
-
expect(result).toMatchObject({ prs: 1, comments: 2, actedOn: 2, reviewBodies: 2, candidates: { person: 3, bot: 2 }, verified: 0 });
|
|
231
|
-
const lessons = readLessons(repo);
|
|
232
|
-
expect(lessons.every(l => l.state === 'candidate')).toBe(true); // nothing is a lesson without evidence
|
|
233
|
-
const sources = new Set(lessons.flatMap(l => l.evidence.map(e => `${e.author}/${e.source}/${e.prAuthor}`)));
|
|
234
|
-
expect([...sources].sort()).toEqual(['priya/person/dev', 'review-helper[bot]/bot/dev']);
|
|
235
|
-
expect(lessons.map(l => l.text)).toEqual(expect.arrayContaining(['Bound both ends of every time window a scheduled job reads.', 'Consider adding more unit tests for orders.']));
|
|
236
|
-
const diff = '+++ b/src/orders.ts\n@@ -1,0 +1,1 @@\n+ await db.insert(other);\n';
|
|
237
|
-
expect(lessonsForDiff(repo, diff, 'verified')).toEqual([]); // candidates are not served by default
|
|
238
|
-
expect(lessonsSection(lessonsForDiff(repo, diff, 'all'))).toContain('src/orders.ts: Use an upsert keyed on `id` so retries do not duplicate orders.');
|
|
239
|
-
const again = await learnFromReviews(repo, { token: 't', repo: 'acme/app', fetch: fetchImpl, writeRules: async () => { throw new Error('nothing promoted: no rule is written'); } });
|
|
240
|
-
expect(again).toMatchObject({ added: 0, total: lessons.length });
|
|
241
|
-
const upsert = lessons.find(l => l.text.startsWith('Use an upsert'));
|
|
242
|
-
decideLesson(repo, upsert.id, 'accepted', 'lead@acme');
|
|
243
|
-
const ruled = await learnFromReviews(repo, { token: 't', repo: 'acme/app', fetch: fetchImpl, writeRules: async (prompt) => JSON.stringify({ rules: [...prompt.matchAll(/"id": "(\w+)"/g)].map(m => ({ id: m[1], rule: 'Make every write idempotent on retry.', file: 'src/orders.ts' })) }) });
|
|
244
|
-
expect(ruled).toMatchObject({ rules: 1, notRules: 0, promoted: { person: 1 } }); // written only for the promoted lesson
|
|
245
|
-
expect(readLessons(repo).find(l => l.state === 'verified')).toMatchObject({ text: 'Make every write idempotent on retry.', promotedBy: 'person' });
|
|
246
|
-
const audit = fs.readFileSync(path.join(repo, '.rigour', 'review-rules-log.jsonl'), 'utf8').trim().split('\n').map(l => JSON.parse(l));
|
|
247
|
-
expect(audit.map(a => [a.said, a.rule])).toEqual([[upsert.text, 'Make every write idempotent on retry.']]);
|
|
248
|
-
});
|
|
249
|
-
});
|
package/dist/safety.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/safety.test.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { FileGuardGate } from './gates/safety.js';
|
|
3
|
-
import { execa } from 'execa';
|
|
4
|
-
vi.mock('execa');
|
|
5
|
-
describe('FileGuardGate', () => {
|
|
6
|
-
const config = {
|
|
7
|
-
safety: {
|
|
8
|
-
protected_paths: ['docs/'],
|
|
9
|
-
max_files_changed_per_cycle: 10
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
it('should flag modified (M) protected files', async () => {
|
|
13
|
-
const gate = new FileGuardGate(config);
|
|
14
|
-
vi.mocked(execa).mockResolvedValueOnce({ stdout: ' M docs/SPEC.md\n' });
|
|
15
|
-
const failures = await gate.run({ cwd: '/test', record: {} });
|
|
16
|
-
expect(failures).toHaveLength(1);
|
|
17
|
-
expect(failures[0].title).toContain("Protected file 'docs/SPEC.md' was modified.");
|
|
18
|
-
});
|
|
19
|
-
it('should flag added (A) protected files', async () => {
|
|
20
|
-
const gate = new FileGuardGate(config);
|
|
21
|
-
vi.mocked(execa).mockResolvedValueOnce({ stdout: 'A docs/NEW.md\n' });
|
|
22
|
-
const failures = await gate.run({ cwd: '/test', record: {} });
|
|
23
|
-
expect(failures).toHaveLength(1);
|
|
24
|
-
expect(failures[0].title).toContain("Protected file 'docs/NEW.md' was modified.");
|
|
25
|
-
});
|
|
26
|
-
it('should NOT flag untracked (??) protected files', async () => {
|
|
27
|
-
const gate = new FileGuardGate(config);
|
|
28
|
-
vi.mocked(execa).mockResolvedValueOnce({ stdout: '?? docs/UNTRAKED.md\n' });
|
|
29
|
-
const failures = await gate.run({ cwd: '/test', record: {} });
|
|
30
|
-
expect(failures).toHaveLength(0);
|
|
31
|
-
});
|
|
32
|
-
it('should correctly handle multiple mixed statuses', async () => {
|
|
33
|
-
const gate = new FileGuardGate(config);
|
|
34
|
-
vi.mocked(execa).mockResolvedValueOnce({
|
|
35
|
-
stdout: ' M docs/MODIFIED.md\n?? docs/NEW_UNTRACKED.md\n D docs/DELETED.md\n'
|
|
36
|
-
});
|
|
37
|
-
const failures = await gate.run({ cwd: '/test', record: {} });
|
|
38
|
-
expect(failures).toHaveLength(2);
|
|
39
|
-
expect(failures.map(f => f.title)).toContain("Protected file 'docs/MODIFIED.md' was modified.");
|
|
40
|
-
expect(failures.map(f => f.title)).toContain("Protected file 'docs/DELETED.md' was modified.");
|
|
41
|
-
});
|
|
42
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { CANDIDATE_RULES, ECOSYSTEM_RULES } from './rules/ecosystem/index.js';
|
|
4
|
-
import { loadCases, runCase } from './benchmark.js';
|
|
5
|
-
const ROOT = path.resolve(__dirname, '../../../../benchmarks/semantic');
|
|
6
|
-
describe('semantic benchmark', () => {
|
|
7
|
-
const cases = loadCases(ROOT);
|
|
8
|
-
it('has cases for every built-in rule, including negatives', () => {
|
|
9
|
-
const rules = new Set(cases.map(c => c.rule));
|
|
10
|
-
expect(rules).toEqual(new Set([
|
|
11
|
-
'in-memory-aggregation', 'credential-redirect', 'degraded-response-cached',
|
|
12
|
-
...[...ECOSYSTEM_RULES, ...CANDIDATE_RULES].map(rule => rule.id), null,
|
|
13
|
-
]));
|
|
14
|
-
});
|
|
15
|
-
for (const bench of cases) {
|
|
16
|
-
it(`${bench.name}: ${bench.note}`, () => {
|
|
17
|
-
const result = runCase(ROOT, bench);
|
|
18
|
-
expect(result.falsePositives).toEqual([]);
|
|
19
|
-
expect(result.caught).toBe(true);
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { loadIntentCases, runIntentCase, scoreIntent } from './benchmark.js';
|
|
4
|
-
import { verdictOf } from './intent-check.js';
|
|
5
|
-
const ROOT = path.resolve(__dirname, '../../../../../benchmarks/intent');
|
|
6
|
-
/** Answers from the labels: an oracle, optionally contradicting itself on one read. */
|
|
7
|
-
function labelledModel(labels, contradict) {
|
|
8
|
-
return {
|
|
9
|
-
async ask(prompt) {
|
|
10
|
-
const label = Object.keys(labels).find(name => prompt.includes(`Question: If \`${name}\``) || prompt.includes(`result of \`${name}\``));
|
|
11
|
-
if (!label)
|
|
12
|
-
return null;
|
|
13
|
-
const optional = labels[label] === 'optional';
|
|
14
|
-
const firstQuestion = prompt.includes('Question: If');
|
|
15
|
-
if (label === contradict)
|
|
16
|
-
return 'yes';
|
|
17
|
-
return firstQuestion === optional ? 'yes' : 'no';
|
|
18
|
-
},
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
describe('verdictOf', () => {
|
|
22
|
-
it('counts only consistent flipped answers', () => {
|
|
23
|
-
expect(verdictOf(['yes', 'no'])).toBe('optional');
|
|
24
|
-
expect(verdictOf(['no', 'yes'])).toBe('required');
|
|
25
|
-
expect(verdictOf(['yes', 'yes'])).toBe('unknown');
|
|
26
|
-
expect(verdictOf(['no', 'no'])).toBe('unknown');
|
|
27
|
-
expect(verdictOf([null, 'no'])).toBe('unknown');
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
describe('intent benchmark with a labelled oracle', () => {
|
|
31
|
-
const cases = loadIntentCases(ROOT);
|
|
32
|
-
it('finds every labelled site, skips the fixed one, and scores the oracle perfectly', async () => {
|
|
33
|
-
const results = await Promise.all(cases.map(bench => runIntentCase(ROOT, bench, labelledModel(bench.labels))));
|
|
34
|
-
const score = scoreIntent(results);
|
|
35
|
-
expect(score.finderErrors).toEqual([]);
|
|
36
|
-
expect(score.falsePositives).toBe(0);
|
|
37
|
-
expect(score.falseNegatives).toBe(0);
|
|
38
|
-
expect(score.rawCorrect).toBe(score.rawTotal);
|
|
39
|
-
});
|
|
40
|
-
it('drops a read whose answers contradict each other instead of guessing', async () => {
|
|
41
|
-
const bench = cases.find(c => c.name === 'dashboard');
|
|
42
|
-
const result = await runIntentCase(ROOT, bench, labelledModel(bench.labels, 'getRecommendedCourses'));
|
|
43
|
-
expect(result.verdicts.find(v => v.read.label === 'getRecommendedCourses')?.verdict).toBe('unknown');
|
|
44
|
-
expect(result.findings).toBe(0);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import ts from 'typescript';
|
|
5
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
|
-
import { forEachNode } from '../ast.js';
|
|
8
|
-
import { loadLearnCases, runLearnCase } from './benchmark.js';
|
|
9
|
-
import { guardsOf, namesIn, propertiesNamed } from './conditions.js';
|
|
10
|
-
import { functionKeyOf, isInvocation } from './identity.js';
|
|
11
|
-
import { loadLearnedRules, saveLearnedRule } from './store.js';
|
|
12
|
-
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../../benchmarks/learn');
|
|
13
|
-
describe('learn benchmark', () => {
|
|
14
|
-
for (const bench of loadLearnCases(ROOT)) {
|
|
15
|
-
it(`${bench.name}: ${bench.note}`, async () => {
|
|
16
|
-
const result = await runLearnCase(ROOT, bench);
|
|
17
|
-
expect(result.problems).toEqual([]);
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
function parse(text) {
|
|
22
|
-
return ts.createSourceFile('x.ts', text, ts.ScriptTarget.Latest, true);
|
|
23
|
-
}
|
|
24
|
-
function firstCall(sf) {
|
|
25
|
-
let call;
|
|
26
|
-
forEachNode(sf, (n) => { if (!call && isInvocation(n))
|
|
27
|
-
call = n; });
|
|
28
|
-
return call;
|
|
29
|
-
}
|
|
30
|
-
describe('guardsOf', () => {
|
|
31
|
-
it('collects the spread condition and the value condition', () => {
|
|
32
|
-
const sf = parse("h({ ...(ok ? { 'cache-control': fresh ? 'max-age=1' : 'no-store' } : {}) });");
|
|
33
|
-
const [prop] = propertiesNamed(sf, 'cache-control');
|
|
34
|
-
expect(namesIn(guardsOf(prop, firstCall(sf)))).toEqual(['fresh', 'ok']);
|
|
35
|
-
});
|
|
36
|
-
it('lists property names before receivers', () => {
|
|
37
|
-
const sf = parse("h({ c: status === 200 && body.stepsAvailable !== false ? 'a' : 'b' });");
|
|
38
|
-
const [prop] = propertiesNamed(sf, 'c');
|
|
39
|
-
expect(namesIn(guardsOf(prop, firstCall(sf)))[0]).toBe('stepsAvailable');
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
describe('functionKeyOf', () => {
|
|
43
|
-
it('names nested arrows, methods and the module level', () => {
|
|
44
|
-
const sf = parse('class A { run() { const inner = () => f(); } }\ng();\n');
|
|
45
|
-
const keys = [];
|
|
46
|
-
forEachNode(sf, (n) => { if (isInvocation(n))
|
|
47
|
-
keys.push(functionKeyOf(n)); });
|
|
48
|
-
expect(keys).toEqual(['A.run>inner', '<module>']);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
describe('learned rule store', () => {
|
|
52
|
-
let dir;
|
|
53
|
-
afterEach(() => { if (dir)
|
|
54
|
-
fs.rmSync(dir, { recursive: true, force: true }); dir = undefined; });
|
|
55
|
-
it('round-trips a rule and skips files that are not rules', () => {
|
|
56
|
-
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'learned-'));
|
|
57
|
-
const rule = {
|
|
58
|
-
id: 'require-option-redirect-abcd1234', version: 1,
|
|
59
|
-
pattern: { template: 'require-option', callee: { level: 'name', key: 'fetch' }, argIndex: 1, property: 'redirect' },
|
|
60
|
-
message: 'm', severity: 'medium', source: { file: 'http.ts', function: 'post' },
|
|
61
|
-
validation: { firesBefore: 1, firesAfter: 0, maxHits: 3, repoHits: [] },
|
|
62
|
-
};
|
|
63
|
-
saveLearnedRule(dir, rule);
|
|
64
|
-
fs.writeFileSync(path.join(dir, '.rigour', 'rules', 'broken.json'), '{"id": 1}');
|
|
65
|
-
expect(loadLearnedRules(dir)).toEqual([rule]);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|