tryassay 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +21 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/catalog-push.d.ts +7 -0
- package/dist/commands/catalog-push.js +47 -0
- package/dist/commands/catalog-push.js.map +1 -0
- package/dist/commands/generate.js +1 -0
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/harvest.d.ts +9 -0
- package/dist/commands/harvest.js +76 -0
- package/dist/commands/harvest.js.map +1 -0
- package/dist/lib/__tests__/learned-rules.test.d.ts +1 -0
- package/dist/lib/__tests__/learned-rules.test.js +260 -0
- package/dist/lib/__tests__/learned-rules.test.js.map +1 -0
- package/dist/lib/__tests__/pr-harvester-types.test.d.ts +1 -0
- package/dist/lib/__tests__/pr-harvester-types.test.js +43 -0
- package/dist/lib/__tests__/pr-harvester-types.test.js.map +1 -0
- package/dist/lib/__tests__/pr-harvester.test.d.ts +1 -0
- package/dist/lib/__tests__/pr-harvester.test.js +341 -0
- package/dist/lib/__tests__/pr-harvester.test.js.map +1 -0
- package/dist/lib/__tests__/rule-harvester.test.d.ts +1 -0
- package/dist/lib/__tests__/rule-harvester.test.js +526 -0
- package/dist/lib/__tests__/rule-harvester.test.js.map +1 -0
- package/dist/lib/learned-rules/category-map.d.ts +28 -0
- package/dist/lib/learned-rules/category-map.js +110 -0
- package/dist/lib/learned-rules/category-map.js.map +1 -0
- package/dist/lib/learned-rules/index.d.ts +107 -0
- package/dist/lib/learned-rules/index.js +198 -0
- package/dist/lib/learned-rules/index.js.map +1 -0
- package/dist/lib/learned-rules/learned-catalog.d.ts +62 -0
- package/dist/lib/learned-rules/learned-catalog.js +161 -0
- package/dist/lib/learned-rules/learned-catalog.js.map +1 -0
- package/dist/lib/learned-rules/pattern-extractor.d.ts +25 -0
- package/dist/lib/learned-rules/pattern-extractor.js +351 -0
- package/dist/lib/learned-rules/pattern-extractor.js.map +1 -0
- package/dist/lib/learned-rules/rule-codifier.d.ts +41 -0
- package/dist/lib/learned-rules/rule-codifier.js +138 -0
- package/dist/lib/learned-rules/rule-codifier.js.map +1 -0
- package/dist/lib/learned-rules/starter-catalog.d.ts +16 -0
- package/dist/lib/learned-rules/starter-catalog.js +402 -0
- package/dist/lib/learned-rules/starter-catalog.js.map +1 -0
- package/dist/lib/learned-rules/types.d.ts +196 -0
- package/dist/lib/learned-rules/types.js +9 -0
- package/dist/lib/learned-rules/types.js.map +1 -0
- package/dist/lib/learned-rules/validation-harness.d.ts +26 -0
- package/dist/lib/learned-rules/validation-harness.js +260 -0
- package/dist/lib/learned-rules/validation-harness.js.map +1 -0
- package/dist/lib/rule-harvester/diff-parser.d.ts +9 -0
- package/dist/lib/rule-harvester/diff-parser.js +77 -0
- package/dist/lib/rule-harvester/diff-parser.js.map +1 -0
- package/dist/lib/rule-harvester/file-selector.d.ts +10 -0
- package/dist/lib/rule-harvester/file-selector.js +59 -0
- package/dist/lib/rule-harvester/file-selector.js.map +1 -0
- package/dist/lib/rule-harvester/ground-truth.d.ts +19 -0
- package/dist/lib/rule-harvester/ground-truth.js +156 -0
- package/dist/lib/rule-harvester/ground-truth.js.map +1 -0
- package/dist/lib/rule-harvester/harvest.d.ts +26 -0
- package/dist/lib/rule-harvester/harvest.js +307 -0
- package/dist/lib/rule-harvester/harvest.js.map +1 -0
- package/dist/lib/rule-harvester/pr-discovery.d.ts +49 -0
- package/dist/lib/rule-harvester/pr-discovery.js +168 -0
- package/dist/lib/rule-harvester/pr-discovery.js.map +1 -0
- package/dist/lib/rule-harvester/pr-harvest.d.ts +53 -0
- package/dist/lib/rule-harvester/pr-harvest.js +326 -0
- package/dist/lib/rule-harvester/pr-harvest.js.map +1 -0
- package/dist/lib/rule-harvester/progress.d.ts +13 -0
- package/dist/lib/rule-harvester/progress.js +50 -0
- package/dist/lib/rule-harvester/progress.js.map +1 -0
- package/dist/lib/rule-harvester/reporter.d.ts +35 -0
- package/dist/lib/rule-harvester/reporter.js +46 -0
- package/dist/lib/rule-harvester/reporter.js.map +1 -0
- package/dist/lib/rule-harvester/rule-generalizer.d.ts +25 -0
- package/dist/lib/rule-harvester/rule-generalizer.js +135 -0
- package/dist/lib/rule-harvester/rule-generalizer.js.map +1 -0
- package/dist/lib/rule-harvester/scanner.d.ts +20 -0
- package/dist/lib/rule-harvester/scanner.js +37 -0
- package/dist/lib/rule-harvester/scanner.js.map +1 -0
- package/dist/sdk/api-client.d.ts +65 -0
- package/dist/sdk/api-client.js +41 -0
- package/dist/sdk/api-client.js.map +1 -0
- package/dist/sdk/forward-verify.d.ts +3 -1
- package/dist/sdk/forward-verify.js +138 -5
- package/dist/sdk/forward-verify.js.map +1 -1
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/types.d.ts +21 -0
- package/package.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Category Map — normalizes the fragmented claim categories
|
|
3
|
+
* (37 variants) down to ~12 canonical categories.
|
|
4
|
+
*
|
|
5
|
+
* Used in two places:
|
|
6
|
+
* 1. Rule creation (harvester + pattern extractor) — new rules get canonical categories
|
|
7
|
+
* 2. Rule loading (catalog) — existing rules get mapped at load time
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The canonical set of claim categories for all learned rules.
|
|
11
|
+
*/
|
|
12
|
+
export const CANONICAL_CATEGORIES = [
|
|
13
|
+
'security',
|
|
14
|
+
'error-handling',
|
|
15
|
+
'data-validation',
|
|
16
|
+
'async',
|
|
17
|
+
'resource-management',
|
|
18
|
+
'api-contract',
|
|
19
|
+
'state-management',
|
|
20
|
+
'i18n',
|
|
21
|
+
'performance',
|
|
22
|
+
'type-safety',
|
|
23
|
+
'ui-logic',
|
|
24
|
+
'code-quality',
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Maps every known raw category string to its canonical form.
|
|
28
|
+
*
|
|
29
|
+
* When adding new mappings, keep the keys lowercase and hyphenated.
|
|
30
|
+
*/
|
|
31
|
+
export const CATEGORY_MAP = {
|
|
32
|
+
// ── security ────────────────────────────────────────────────
|
|
33
|
+
'security': 'security',
|
|
34
|
+
// ── error-handling ──────────────────────────────────────────
|
|
35
|
+
'error-handling': 'error-handling',
|
|
36
|
+
'null-safety': 'error-handling',
|
|
37
|
+
'logging': 'error-handling',
|
|
38
|
+
// ── data-validation ─────────────────────────────────────────
|
|
39
|
+
'data-validation': 'data-validation',
|
|
40
|
+
'validation-safety': 'data-validation',
|
|
41
|
+
'data-integrity': 'data-validation',
|
|
42
|
+
'data-accuracy': 'data-validation',
|
|
43
|
+
'data-consistency': 'data-validation',
|
|
44
|
+
'database-integrity': 'data-validation',
|
|
45
|
+
// ── async ───────────────────────────────────────────────────
|
|
46
|
+
'async': 'async',
|
|
47
|
+
'concurrency': 'async',
|
|
48
|
+
'race-condition': 'async',
|
|
49
|
+
'race-conditions': 'async',
|
|
50
|
+
// ── resource-management ─────────────────────────────────────
|
|
51
|
+
'resource-management': 'resource-management',
|
|
52
|
+
// ── api-contract ────────────────────────────────────────────
|
|
53
|
+
'api-contract': 'api-contract',
|
|
54
|
+
'dependency-injection': 'api-contract',
|
|
55
|
+
// ── state-management ────────────────────────────────────────
|
|
56
|
+
'state-management': 'state-management',
|
|
57
|
+
// ── i18n ────────────────────────────────────────────────────
|
|
58
|
+
'i18n': 'i18n',
|
|
59
|
+
'i18n-compliance': 'i18n',
|
|
60
|
+
'i18n-safety': 'i18n',
|
|
61
|
+
'i18n-support': 'i18n',
|
|
62
|
+
'internationalization': 'i18n',
|
|
63
|
+
'localization': 'i18n',
|
|
64
|
+
// ── performance ─────────────────────────────────────────────
|
|
65
|
+
'performance': 'performance',
|
|
66
|
+
'react-optimization': 'performance',
|
|
67
|
+
// ── type-safety ─────────────────────────────────────────────
|
|
68
|
+
'type-safety': 'type-safety',
|
|
69
|
+
// ── ui-logic ────────────────────────────────────────────────
|
|
70
|
+
'ui-logic': 'ui-logic',
|
|
71
|
+
'ui-ux': 'ui-logic',
|
|
72
|
+
'react-antipatterns': 'ui-logic',
|
|
73
|
+
'react-best-practices': 'ui-logic',
|
|
74
|
+
'react-patterns': 'ui-logic',
|
|
75
|
+
// ── code-quality ────────────────────────────────────────────
|
|
76
|
+
'code-quality': 'code-quality',
|
|
77
|
+
'code-convention': 'code-quality',
|
|
78
|
+
'documentation': 'code-quality',
|
|
79
|
+
'test-reliability': 'code-quality',
|
|
80
|
+
'test-robustness': 'code-quality',
|
|
81
|
+
// ── catch-all for logic variants ────────────────────────────
|
|
82
|
+
'logic': 'error-handling',
|
|
83
|
+
'logic-error': 'error-handling',
|
|
84
|
+
'logic-flow': 'error-handling',
|
|
85
|
+
'logic-control-flow': 'error-handling',
|
|
86
|
+
'bug-fix': 'error-handling',
|
|
87
|
+
'business-logic': 'data-validation',
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Normalize a raw category string to its canonical form.
|
|
91
|
+
*
|
|
92
|
+
* Lookup order:
|
|
93
|
+
* 1. Exact match in CATEGORY_MAP
|
|
94
|
+
* 2. Lowercase + trim exact match
|
|
95
|
+
* 3. Fallback to 'code-quality' (the broadest bucket)
|
|
96
|
+
*/
|
|
97
|
+
export function normalizeCategory(raw) {
|
|
98
|
+
// 1. Exact match
|
|
99
|
+
const exact = CATEGORY_MAP[raw];
|
|
100
|
+
if (exact)
|
|
101
|
+
return exact;
|
|
102
|
+
// 2. Normalized match (lowercase, trimmed)
|
|
103
|
+
const normalized = raw.toLowerCase().trim();
|
|
104
|
+
const normMatch = CATEGORY_MAP[normalized];
|
|
105
|
+
if (normMatch)
|
|
106
|
+
return normMatch;
|
|
107
|
+
// 3. Fallback
|
|
108
|
+
return 'code-quality';
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=category-map.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"category-map.js","sourceRoot":"","sources":["../../../src/lib/learned-rules/category-map.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,UAAU;IACV,gBAAgB;IAChB,iBAAiB;IACjB,OAAO;IACP,qBAAqB;IACrB,cAAc;IACd,kBAAkB;IAClB,MAAM;IACN,aAAa;IACb,aAAa;IACb,UAAU;IACV,cAAc;CACN,CAAC;AAIX;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAgD;IACvE,+DAA+D;IAC/D,UAAU,EAAE,UAAU;IAEtB,+DAA+D;IAC/D,gBAAgB,EAAE,gBAAgB;IAClC,aAAa,EAAE,gBAAgB;IAC/B,SAAS,EAAE,gBAAgB;IAE3B,+DAA+D;IAC/D,iBAAiB,EAAE,iBAAiB;IACpC,mBAAmB,EAAE,iBAAiB;IACtC,gBAAgB,EAAE,iBAAiB;IACnC,eAAe,EAAE,iBAAiB;IAClC,kBAAkB,EAAE,iBAAiB;IACrC,oBAAoB,EAAE,iBAAiB;IAEvC,+DAA+D;IAC/D,OAAO,EAAE,OAAO;IAChB,aAAa,EAAE,OAAO;IACtB,gBAAgB,EAAE,OAAO;IACzB,iBAAiB,EAAE,OAAO;IAE1B,+DAA+D;IAC/D,qBAAqB,EAAE,qBAAqB;IAE5C,+DAA+D;IAC/D,cAAc,EAAE,cAAc;IAC9B,sBAAsB,EAAE,cAAc;IAEtC,+DAA+D;IAC/D,kBAAkB,EAAE,kBAAkB;IAEtC,+DAA+D;IAC/D,MAAM,EAAE,MAAM;IACd,iBAAiB,EAAE,MAAM;IACzB,aAAa,EAAE,MAAM;IACrB,cAAc,EAAE,MAAM;IACtB,sBAAsB,EAAE,MAAM;IAC9B,cAAc,EAAE,MAAM;IAEtB,+DAA+D;IAC/D,aAAa,EAAE,aAAa;IAC5B,oBAAoB,EAAE,aAAa;IAEnC,+DAA+D;IAC/D,aAAa,EAAE,aAAa;IAE5B,+DAA+D;IAC/D,UAAU,EAAE,UAAU;IACtB,OAAO,EAAE,UAAU;IACnB,oBAAoB,EAAE,UAAU;IAChC,sBAAsB,EAAE,UAAU;IAClC,gBAAgB,EAAE,UAAU;IAE5B,+DAA+D;IAC/D,cAAc,EAAE,cAAc;IAC9B,iBAAiB,EAAE,cAAc;IACjC,eAAe,EAAE,cAAc;IAC/B,kBAAkB,EAAE,cAAc;IAClC,iBAAiB,EAAE,cAAc;IAEjC,+DAA+D;IAC/D,OAAO,EAAE,gBAAgB;IACzB,aAAa,EAAE,gBAAgB;IAC/B,YAAY,EAAE,gBAAgB;IAC9B,oBAAoB,EAAE,gBAAgB;IACtC,SAAS,EAAE,gBAAgB;IAC3B,gBAAgB,EAAE,iBAAiB;CACpC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,iBAAiB;IACjB,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC;IAExB,2CAA2C;IAC3C,MAAM,UAAU,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;IAC5C,MAAM,SAAS,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;IAC3C,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAEhC,cAAc;IACd,OAAO,cAAc,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Expanding Formal Verification — Phase 1 Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Pipeline: LLM finds bug → pattern extractor → rule codifier → validation → catalog
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* // After a scan confirms a finding:
|
|
8
|
+
* const result = await learnFromFinding(projectPath, input);
|
|
9
|
+
*
|
|
10
|
+
* // During a scan, run learned rules alongside hand-crafted ones:
|
|
11
|
+
* const findings = await runLearnedRules(projectPath, code, language);
|
|
12
|
+
*/
|
|
13
|
+
export { extractPattern, getSupportedCategories, resetPatternCounter } from './pattern-extractor.js';
|
|
14
|
+
export { normalizeCategory, CANONICAL_CATEGORIES, CATEGORY_MAP } from './category-map.js';
|
|
15
|
+
export type { CanonicalCategory } from './category-map.js';
|
|
16
|
+
export { codifyRule, executeRule, updateRuleStatus, recordRuleFire, resetRuleCounter } from './rule-codifier.js';
|
|
17
|
+
export { validateRule, getValidationThresholds } from './validation-harness.js';
|
|
18
|
+
export { STARTER_RULES } from './starter-catalog.js';
|
|
19
|
+
export { loadRules, saveRules, addRule, updateRule, getPromotedRules, getCandidateRules, getRulesByCategory, loadStats, } from './learned-catalog.js';
|
|
20
|
+
export type { ExtractedPattern, LearnedRule, LearnedRuleStatus, PatternExtractionInput, PatternExtractionResult, SourceFinding, ValidationResult, TestCase, PatternKind, PRReviewComment, DiscoveredPR, DiffHunk, ParsedPRDiff, GeneralizedRule, } from './types.js';
|
|
21
|
+
import type { PatternExtractionInput, LearnedRule } from './types.js';
|
|
22
|
+
/**
|
|
23
|
+
* Tracks per-rule fire counts across files within a single `assay assess` run.
|
|
24
|
+
*
|
|
25
|
+
* After a rule fires in >30% of files scanned so far (minimum 5 files),
|
|
26
|
+
* it is suppressed for the remainder of the scan.
|
|
27
|
+
*
|
|
28
|
+
* Create one per scan run and pass it to `runLearnedRules()`.
|
|
29
|
+
*/
|
|
30
|
+
export declare class ScanSession {
|
|
31
|
+
/** Total files processed so far. */
|
|
32
|
+
private fileCount;
|
|
33
|
+
/** ruleId → number of files where the rule fired. */
|
|
34
|
+
private readonly fireCounts;
|
|
35
|
+
/** Rules that have been permanently suppressed for this session. */
|
|
36
|
+
private readonly suppressed;
|
|
37
|
+
/** Call once per file, before running rules on that file. */
|
|
38
|
+
recordFile(): void;
|
|
39
|
+
/** Record that a rule fired on the current file. */
|
|
40
|
+
recordFire(ruleId: string): void;
|
|
41
|
+
/** Check whether a rule should be suppressed. */
|
|
42
|
+
isSuppressed(ruleId: string): boolean;
|
|
43
|
+
/** Snapshot of session stats (useful for diagnostics). */
|
|
44
|
+
stats(): ScanSessionStats;
|
|
45
|
+
}
|
|
46
|
+
/** Diagnostic snapshot of a scan session. */
|
|
47
|
+
export interface ScanSessionStats {
|
|
48
|
+
readonly filesScanned: number;
|
|
49
|
+
readonly suppressedRules: string[];
|
|
50
|
+
readonly fireCounts: Record<string, number>;
|
|
51
|
+
}
|
|
52
|
+
export interface LearnResult {
|
|
53
|
+
/** Whether a new rule was created. */
|
|
54
|
+
readonly learned: boolean;
|
|
55
|
+
/** The rule, if created. */
|
|
56
|
+
readonly rule?: LearnedRule;
|
|
57
|
+
/** Why learning didn't produce a rule. */
|
|
58
|
+
readonly reason?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Full pipeline: extract pattern → codify rule → validate → store.
|
|
62
|
+
*
|
|
63
|
+
* Call this after confirming an LLM finding is correct.
|
|
64
|
+
* The system will try to extract a generalizable pattern and
|
|
65
|
+
* create a formal rule that catches future instances.
|
|
66
|
+
*
|
|
67
|
+
* @param projectPath - Root of the project being scanned.
|
|
68
|
+
* @param input - The confirmed finding with code context.
|
|
69
|
+
* @returns Whether a rule was learned and stored.
|
|
70
|
+
*/
|
|
71
|
+
export declare function learnFromFinding(projectPath: string, input: PatternExtractionInput): Promise<LearnResult>;
|
|
72
|
+
/**
|
|
73
|
+
* Run all promoted learned rules against code.
|
|
74
|
+
*
|
|
75
|
+
* Returns findings from rules that fire.
|
|
76
|
+
* These run alongside the hand-crafted formal checks.
|
|
77
|
+
*
|
|
78
|
+
* @param session - Optional scan session for noise suppression.
|
|
79
|
+
* When provided, rules that fire on >30% of files (min 5 files)
|
|
80
|
+
* are suppressed for the remainder of the scan. The caller must
|
|
81
|
+
* call `session.recordFile()` before each invocation.
|
|
82
|
+
*/
|
|
83
|
+
export declare function runLearnedRules(projectPath: string, code: string, language: string, session?: ScanSession): Promise<LearnedRuleFinding[]>;
|
|
84
|
+
/** A finding produced by a learned rule. */
|
|
85
|
+
export interface LearnedRuleFinding {
|
|
86
|
+
readonly ruleId: string;
|
|
87
|
+
readonly description: string;
|
|
88
|
+
readonly severity: 'critical' | 'high' | 'medium';
|
|
89
|
+
readonly category: string;
|
|
90
|
+
readonly evidence: string;
|
|
91
|
+
readonly matches: string[];
|
|
92
|
+
/** Confidence based on the rule's track record. */
|
|
93
|
+
readonly confidence: number;
|
|
94
|
+
/** Whether this finding came from the server catalog or local starter rules. */
|
|
95
|
+
readonly source?: 'server' | 'local';
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get a summary of the learned rule system status.
|
|
99
|
+
*/
|
|
100
|
+
export declare function getLearnedRulesSummary(projectPath: string): Promise<{
|
|
101
|
+
total: number;
|
|
102
|
+
promoted: number;
|
|
103
|
+
candidates: number;
|
|
104
|
+
rejected: number;
|
|
105
|
+
categories: string[];
|
|
106
|
+
totalFires: number;
|
|
107
|
+
}>;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Expanding Formal Verification — Phase 1 Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Pipeline: LLM finds bug → pattern extractor → rule codifier → validation → catalog
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* // After a scan confirms a finding:
|
|
8
|
+
* const result = await learnFromFinding(projectPath, input);
|
|
9
|
+
*
|
|
10
|
+
* // During a scan, run learned rules alongside hand-crafted ones:
|
|
11
|
+
* const findings = await runLearnedRules(projectPath, code, language);
|
|
12
|
+
*/
|
|
13
|
+
export { extractPattern, getSupportedCategories, resetPatternCounter } from './pattern-extractor.js';
|
|
14
|
+
export { normalizeCategory, CANONICAL_CATEGORIES, CATEGORY_MAP } from './category-map.js';
|
|
15
|
+
export { codifyRule, executeRule, updateRuleStatus, recordRuleFire, resetRuleCounter } from './rule-codifier.js';
|
|
16
|
+
export { validateRule, getValidationThresholds } from './validation-harness.js';
|
|
17
|
+
export { STARTER_RULES } from './starter-catalog.js';
|
|
18
|
+
export { loadRules, saveRules, addRule, updateRule, getPromotedRules, getCandidateRules, getRulesByCategory, loadStats, } from './learned-catalog.js';
|
|
19
|
+
import { extractPattern } from './pattern-extractor.js';
|
|
20
|
+
import { codifyRule, executeRule } from './rule-codifier.js';
|
|
21
|
+
import { validateRule } from './validation-harness.js';
|
|
22
|
+
import { addRule, getPromotedRules, updateRule } from './learned-catalog.js';
|
|
23
|
+
import { updateRuleStatus } from './rule-codifier.js';
|
|
24
|
+
// ── Scan Session (Noise Suppression) ────────────────────────
|
|
25
|
+
/** Minimum files scanned before noise suppression kicks in. */
|
|
26
|
+
const NOISE_MIN_FILES = 5;
|
|
27
|
+
/** If a rule fires on more than this fraction of files, suppress it. */
|
|
28
|
+
const NOISE_THRESHOLD = 0.3;
|
|
29
|
+
/**
|
|
30
|
+
* Tracks per-rule fire counts across files within a single `assay assess` run.
|
|
31
|
+
*
|
|
32
|
+
* After a rule fires in >30% of files scanned so far (minimum 5 files),
|
|
33
|
+
* it is suppressed for the remainder of the scan.
|
|
34
|
+
*
|
|
35
|
+
* Create one per scan run and pass it to `runLearnedRules()`.
|
|
36
|
+
*/
|
|
37
|
+
export class ScanSession {
|
|
38
|
+
/** Total files processed so far. */
|
|
39
|
+
fileCount = 0;
|
|
40
|
+
/** ruleId → number of files where the rule fired. */
|
|
41
|
+
fireCounts = new Map();
|
|
42
|
+
/** Rules that have been permanently suppressed for this session. */
|
|
43
|
+
suppressed = new Set();
|
|
44
|
+
/** Call once per file, before running rules on that file. */
|
|
45
|
+
recordFile() {
|
|
46
|
+
this.fileCount++;
|
|
47
|
+
}
|
|
48
|
+
/** Record that a rule fired on the current file. */
|
|
49
|
+
recordFire(ruleId) {
|
|
50
|
+
this.fireCounts.set(ruleId, (this.fireCounts.get(ruleId) ?? 0) + 1);
|
|
51
|
+
}
|
|
52
|
+
/** Check whether a rule should be suppressed. */
|
|
53
|
+
isSuppressed(ruleId) {
|
|
54
|
+
if (this.suppressed.has(ruleId))
|
|
55
|
+
return true;
|
|
56
|
+
if (this.fileCount >= NOISE_MIN_FILES) {
|
|
57
|
+
const fires = this.fireCounts.get(ruleId) ?? 0;
|
|
58
|
+
if (fires / this.fileCount > NOISE_THRESHOLD) {
|
|
59
|
+
this.suppressed.add(ruleId);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
/** Snapshot of session stats (useful for diagnostics). */
|
|
66
|
+
stats() {
|
|
67
|
+
return {
|
|
68
|
+
filesScanned: this.fileCount,
|
|
69
|
+
suppressedRules: [...this.suppressed],
|
|
70
|
+
fireCounts: Object.fromEntries(this.fireCounts),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Full pipeline: extract pattern → codify rule → validate → store.
|
|
76
|
+
*
|
|
77
|
+
* Call this after confirming an LLM finding is correct.
|
|
78
|
+
* The system will try to extract a generalizable pattern and
|
|
79
|
+
* create a formal rule that catches future instances.
|
|
80
|
+
*
|
|
81
|
+
* @param projectPath - Root of the project being scanned.
|
|
82
|
+
* @param input - The confirmed finding with code context.
|
|
83
|
+
* @returns Whether a rule was learned and stored.
|
|
84
|
+
*/
|
|
85
|
+
export async function learnFromFinding(projectPath, input) {
|
|
86
|
+
// Step 1: Extract pattern
|
|
87
|
+
const extraction = extractPattern(input);
|
|
88
|
+
if (!extraction.success || !extraction.pattern) {
|
|
89
|
+
return {
|
|
90
|
+
learned: false,
|
|
91
|
+
reason: extraction.failureReason ?? 'Pattern extraction failed',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Step 2: Codify as a rule
|
|
95
|
+
const rule = codifyRule(extraction.pattern, input);
|
|
96
|
+
// Step 3: Validate against synthetic test cases
|
|
97
|
+
const validation = validateRule(rule);
|
|
98
|
+
const validatedRule = {
|
|
99
|
+
...rule,
|
|
100
|
+
validationResults: validation,
|
|
101
|
+
};
|
|
102
|
+
if (!validation.passed) {
|
|
103
|
+
// Store as rejected for record-keeping
|
|
104
|
+
const rejectedRule = updateRuleStatus(validatedRule, 'rejected');
|
|
105
|
+
await addRule(projectPath, rejectedRule);
|
|
106
|
+
return {
|
|
107
|
+
learned: false,
|
|
108
|
+
rule: rejectedRule,
|
|
109
|
+
reason: `Validation failed: precision=${validation.precision.toFixed(2)}, recall=${validation.recall.toFixed(2)}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// Step 4: Promote to validated and store
|
|
113
|
+
const promotedRule = updateRuleStatus(validatedRule, 'promoted');
|
|
114
|
+
await addRule(projectPath, promotedRule);
|
|
115
|
+
return {
|
|
116
|
+
learned: true,
|
|
117
|
+
rule: promotedRule,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Run all promoted learned rules against code.
|
|
122
|
+
*
|
|
123
|
+
* Returns findings from rules that fire.
|
|
124
|
+
* These run alongside the hand-crafted formal checks.
|
|
125
|
+
*
|
|
126
|
+
* @param session - Optional scan session for noise suppression.
|
|
127
|
+
* When provided, rules that fire on >30% of files (min 5 files)
|
|
128
|
+
* are suppressed for the remainder of the scan. The caller must
|
|
129
|
+
* call `session.recordFile()` before each invocation.
|
|
130
|
+
*/
|
|
131
|
+
export async function runLearnedRules(projectPath, code, language, session) {
|
|
132
|
+
const rules = await getPromotedRules(projectPath);
|
|
133
|
+
const findings = [];
|
|
134
|
+
for (const rule of rules) {
|
|
135
|
+
// Skip rules the session has already flagged as noise
|
|
136
|
+
if (session?.isSuppressed(rule.id))
|
|
137
|
+
continue;
|
|
138
|
+
const { fires, matches } = executeRule(rule, code, language);
|
|
139
|
+
if (fires) {
|
|
140
|
+
// Record the fire in the session before checking suppression
|
|
141
|
+
if (session) {
|
|
142
|
+
session.recordFire(rule.id);
|
|
143
|
+
// Re-check: this fire may have pushed the rule over the threshold
|
|
144
|
+
if (session.isSuppressed(rule.id))
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
findings.push({
|
|
148
|
+
ruleId: rule.id,
|
|
149
|
+
description: rule.pattern.description,
|
|
150
|
+
severity: rule.pattern.severity,
|
|
151
|
+
category: rule.pattern.claimCategory,
|
|
152
|
+
evidence: rule.pattern.evidenceTemplate
|
|
153
|
+
.replace('{match}', matches[0] ?? 'pattern detected')
|
|
154
|
+
.replace('{file}', 'current file'),
|
|
155
|
+
matches,
|
|
156
|
+
confidence: computeConfidence(rule),
|
|
157
|
+
});
|
|
158
|
+
// Track the fire (async, don't block)
|
|
159
|
+
updateRule(projectPath, rule.id, r => ({
|
|
160
|
+
...r,
|
|
161
|
+
fireCount: r.fireCount + 1,
|
|
162
|
+
updatedAt: new Date().toISOString(),
|
|
163
|
+
})).catch(() => { });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return findings;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Compute confidence for a rule based on its track record.
|
|
170
|
+
* New rules start at 0.7, confidence grows with confirmed true positives.
|
|
171
|
+
*/
|
|
172
|
+
function computeConfidence(rule) {
|
|
173
|
+
if (rule.fireCount === 0)
|
|
174
|
+
return 0.7; // New rule, moderate confidence
|
|
175
|
+
const precision = rule.truePositiveCount + rule.falsePositiveCount > 0
|
|
176
|
+
? rule.truePositiveCount / (rule.truePositiveCount + rule.falsePositiveCount)
|
|
177
|
+
: 0.7;
|
|
178
|
+
// Scale: min 0.5, max 0.95
|
|
179
|
+
// More fires with high precision → higher confidence
|
|
180
|
+
const fireBoost = Math.min(rule.fireCount / 100, 0.15);
|
|
181
|
+
return Math.min(0.95, Math.max(0.5, precision * 0.8 + fireBoost + 0.1));
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get a summary of the learned rule system status.
|
|
185
|
+
*/
|
|
186
|
+
export async function getLearnedRulesSummary(projectPath) {
|
|
187
|
+
const { loadRules } = await import('./learned-catalog.js');
|
|
188
|
+
const rules = await loadRules(projectPath);
|
|
189
|
+
return {
|
|
190
|
+
total: rules.length,
|
|
191
|
+
promoted: rules.filter(r => r.status === 'promoted').length,
|
|
192
|
+
candidates: rules.filter(r => r.status === 'candidate').length,
|
|
193
|
+
rejected: rules.filter(r => r.status === 'rejected').length,
|
|
194
|
+
categories: [...new Set(rules.map(r => r.pattern.claimCategory))],
|
|
195
|
+
totalFires: rules.reduce((sum, r) => sum + r.fireCount, 0),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/learned-rules/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learned Rule Catalog — stores, loads, and queries auto-generated formal rules.
|
|
3
|
+
*
|
|
4
|
+
* Rules are stored as JSON in the `learned/` subdirectory alongside
|
|
5
|
+
* hand-crafted rules in check-catalog.ts. This separation ensures
|
|
6
|
+
* auto-generated rules are clearly distinguishable.
|
|
7
|
+
*
|
|
8
|
+
* Storage structure:
|
|
9
|
+
* .assay/learned/
|
|
10
|
+
* rules.json — all learned rules
|
|
11
|
+
* stats.json — catalog statistics over time
|
|
12
|
+
*/
|
|
13
|
+
import type { LearnedRule, LearnedRuleStatus } from './types.js';
|
|
14
|
+
export interface CatalogStats {
|
|
15
|
+
totalRules: number;
|
|
16
|
+
byStatus: Record<LearnedRuleStatus, number>;
|
|
17
|
+
byCategory: Record<string, number>;
|
|
18
|
+
bySeverity: Record<string, number>;
|
|
19
|
+
totalFires: number;
|
|
20
|
+
totalTruePositives: number;
|
|
21
|
+
totalFalsePositives: number;
|
|
22
|
+
overallPrecision: number;
|
|
23
|
+
lastUpdated: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Load all learned rules from the project's catalog,
|
|
27
|
+
* merged with the bundled starter catalog.
|
|
28
|
+
*
|
|
29
|
+
* Local rules (from .assay/learned/rules.json) take precedence over
|
|
30
|
+
* starter rules when IDs collide. Starter rules provide out-of-the-box
|
|
31
|
+
* coverage even when no local catalog exists.
|
|
32
|
+
*/
|
|
33
|
+
export declare function loadRules(projectPath: string): Promise<LearnedRule[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Save all learned rules to the project's catalog.
|
|
36
|
+
*/
|
|
37
|
+
export declare function saveRules(projectPath: string, rules: LearnedRule[]): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Add a new rule to the catalog.
|
|
40
|
+
* Returns the updated rule list.
|
|
41
|
+
*/
|
|
42
|
+
export declare function addRule(projectPath: string, rule: LearnedRule): Promise<LearnedRule[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Update a rule in the catalog by ID.
|
|
45
|
+
*/
|
|
46
|
+
export declare function updateRule(projectPath: string, ruleId: string, updater: (rule: LearnedRule) => LearnedRule): Promise<LearnedRule | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Get all promoted rules (ready to run in the formal verifier).
|
|
49
|
+
*/
|
|
50
|
+
export declare function getPromotedRules(projectPath: string): Promise<LearnedRule[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Get all candidate rules (awaiting validation).
|
|
53
|
+
*/
|
|
54
|
+
export declare function getCandidateRules(projectPath: string): Promise<LearnedRule[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Get rules by category.
|
|
57
|
+
*/
|
|
58
|
+
export declare function getRulesByCategory(projectPath: string, category: string): Promise<LearnedRule[]>;
|
|
59
|
+
/**
|
|
60
|
+
* Load catalog statistics.
|
|
61
|
+
*/
|
|
62
|
+
export declare function loadStats(projectPath: string): Promise<CatalogStats | null>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learned Rule Catalog — stores, loads, and queries auto-generated formal rules.
|
|
3
|
+
*
|
|
4
|
+
* Rules are stored as JSON in the `learned/` subdirectory alongside
|
|
5
|
+
* hand-crafted rules in check-catalog.ts. This separation ensures
|
|
6
|
+
* auto-generated rules are clearly distinguishable.
|
|
7
|
+
*
|
|
8
|
+
* Storage structure:
|
|
9
|
+
* .assay/learned/
|
|
10
|
+
* rules.json — all learned rules
|
|
11
|
+
* stats.json — catalog statistics over time
|
|
12
|
+
*/
|
|
13
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { normalizeCategory } from './category-map.js';
|
|
16
|
+
import { STARTER_RULES } from './starter-catalog.js';
|
|
17
|
+
// ── File Paths ───────────────────────────────────────────────
|
|
18
|
+
function rulesPath(projectPath) {
|
|
19
|
+
return join(projectPath, '.assay', 'learned', 'rules.json');
|
|
20
|
+
}
|
|
21
|
+
function statsPath(projectPath) {
|
|
22
|
+
return join(projectPath, '.assay', 'learned', 'stats.json');
|
|
23
|
+
}
|
|
24
|
+
// ── CRUD Operations ──────────────────────────────────────────
|
|
25
|
+
/**
|
|
26
|
+
* Load all learned rules from the project's catalog,
|
|
27
|
+
* merged with the bundled starter catalog.
|
|
28
|
+
*
|
|
29
|
+
* Local rules (from .assay/learned/rules.json) take precedence over
|
|
30
|
+
* starter rules when IDs collide. Starter rules provide out-of-the-box
|
|
31
|
+
* coverage even when no local catalog exists.
|
|
32
|
+
*/
|
|
33
|
+
export async function loadRules(projectPath) {
|
|
34
|
+
// Load local rules from disk
|
|
35
|
+
let localRules = [];
|
|
36
|
+
try {
|
|
37
|
+
const content = await readFile(rulesPath(projectPath), 'utf-8');
|
|
38
|
+
const data = JSON.parse(content);
|
|
39
|
+
if (Array.isArray(data)) {
|
|
40
|
+
localRules = data.map((rule) => ({
|
|
41
|
+
...rule,
|
|
42
|
+
pattern: {
|
|
43
|
+
...rule.pattern,
|
|
44
|
+
claimCategory: normalizeCategory(rule.pattern.claimCategory),
|
|
45
|
+
},
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// No local catalog yet — starter rules will provide base coverage
|
|
51
|
+
}
|
|
52
|
+
// Merge: local rules take precedence over starter rules with same ID
|
|
53
|
+
const localIds = new Set(localRules.map(r => r.id));
|
|
54
|
+
const starterRulesFiltered = STARTER_RULES.filter(r => !localIds.has(r.id));
|
|
55
|
+
return [...localRules, ...starterRulesFiltered];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Save all learned rules to the project's catalog.
|
|
59
|
+
*/
|
|
60
|
+
export async function saveRules(projectPath, rules) {
|
|
61
|
+
const dir = join(projectPath, '.assay', 'learned');
|
|
62
|
+
await mkdir(dir, { recursive: true });
|
|
63
|
+
await writeFile(rulesPath(projectPath), JSON.stringify(rules, null, 2));
|
|
64
|
+
await saveStats(projectPath, computeStats(rules));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Add a new rule to the catalog.
|
|
68
|
+
* Returns the updated rule list.
|
|
69
|
+
*/
|
|
70
|
+
export async function addRule(projectPath, rule) {
|
|
71
|
+
const rules = await loadRules(projectPath);
|
|
72
|
+
// Check for duplicate patterns
|
|
73
|
+
const isDuplicate = rules.some(r => r.pattern.regexPattern === rule.pattern.regexPattern &&
|
|
74
|
+
r.pattern.claimCategory === rule.pattern.claimCategory &&
|
|
75
|
+
r.status !== 'rejected' && r.status !== 'deprecated');
|
|
76
|
+
if (isDuplicate) {
|
|
77
|
+
return rules; // Don't add duplicates
|
|
78
|
+
}
|
|
79
|
+
rules.push(rule);
|
|
80
|
+
await saveRules(projectPath, rules);
|
|
81
|
+
return rules;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Update a rule in the catalog by ID.
|
|
85
|
+
*/
|
|
86
|
+
export async function updateRule(projectPath, ruleId, updater) {
|
|
87
|
+
const rules = await loadRules(projectPath);
|
|
88
|
+
const index = rules.findIndex(r => r.id === ruleId);
|
|
89
|
+
if (index === -1)
|
|
90
|
+
return null;
|
|
91
|
+
rules[index] = updater(rules[index]);
|
|
92
|
+
await saveRules(projectPath, rules);
|
|
93
|
+
return rules[index];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get all promoted rules (ready to run in the formal verifier).
|
|
97
|
+
*/
|
|
98
|
+
export async function getPromotedRules(projectPath) {
|
|
99
|
+
const rules = await loadRules(projectPath);
|
|
100
|
+
return rules.filter(r => r.status === 'promoted');
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get all candidate rules (awaiting validation).
|
|
104
|
+
*/
|
|
105
|
+
export async function getCandidateRules(projectPath) {
|
|
106
|
+
const rules = await loadRules(projectPath);
|
|
107
|
+
return rules.filter(r => r.status === 'candidate');
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get rules by category.
|
|
111
|
+
*/
|
|
112
|
+
export async function getRulesByCategory(projectPath, category) {
|
|
113
|
+
const rules = await loadRules(projectPath);
|
|
114
|
+
return rules.filter(r => r.pattern.claimCategory === category);
|
|
115
|
+
}
|
|
116
|
+
// ── Statistics ───────────────────────────────────────────────
|
|
117
|
+
function computeStats(rules) {
|
|
118
|
+
const byStatus = {};
|
|
119
|
+
const byCategory = {};
|
|
120
|
+
const bySeverity = {};
|
|
121
|
+
let totalFires = 0;
|
|
122
|
+
let totalTP = 0;
|
|
123
|
+
let totalFP = 0;
|
|
124
|
+
for (const rule of rules) {
|
|
125
|
+
byStatus[rule.status] = (byStatus[rule.status] ?? 0) + 1;
|
|
126
|
+
byCategory[rule.pattern.claimCategory] = (byCategory[rule.pattern.claimCategory] ?? 0) + 1;
|
|
127
|
+
bySeverity[rule.pattern.severity] = (bySeverity[rule.pattern.severity] ?? 0) + 1;
|
|
128
|
+
totalFires += rule.fireCount;
|
|
129
|
+
totalTP += rule.truePositiveCount;
|
|
130
|
+
totalFP += rule.falsePositiveCount;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
totalRules: rules.length,
|
|
134
|
+
byStatus: byStatus,
|
|
135
|
+
byCategory,
|
|
136
|
+
bySeverity,
|
|
137
|
+
totalFires,
|
|
138
|
+
totalTruePositives: totalTP,
|
|
139
|
+
totalFalsePositives: totalFP,
|
|
140
|
+
overallPrecision: totalTP + totalFP > 0 ? totalTP / (totalTP + totalFP) : 1,
|
|
141
|
+
lastUpdated: new Date().toISOString(),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async function saveStats(projectPath, stats) {
|
|
145
|
+
const dir = join(projectPath, '.assay', 'learned');
|
|
146
|
+
await mkdir(dir, { recursive: true });
|
|
147
|
+
await writeFile(statsPath(projectPath), JSON.stringify(stats, null, 2));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Load catalog statistics.
|
|
151
|
+
*/
|
|
152
|
+
export async function loadStats(projectPath) {
|
|
153
|
+
try {
|
|
154
|
+
const content = await readFile(statsPath(projectPath), 'utf-8');
|
|
155
|
+
return JSON.parse(content);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=learned-catalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"learned-catalog.js","sourceRoot":"","sources":["../../../src/lib/learned-rules/learned-catalog.ts"],"names":[],"mappings":"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"}
|