review-council 1.0.0 → 1.4.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.
Files changed (76) hide show
  1. package/README.md +4 -4
  2. package/dist/ci.d.ts +14 -0
  3. package/dist/ci.js +25 -0
  4. package/dist/ci.js.map +1 -0
  5. package/dist/config/defaults.d.ts +13 -1
  6. package/dist/config/defaults.js +17 -1
  7. package/dist/config/defaults.js.map +1 -1
  8. package/dist/config/loader.d.ts +9 -1
  9. package/dist/config/loader.js +48 -27
  10. package/dist/config/loader.js.map +1 -1
  11. package/dist/config/schema.d.ts +1 -0
  12. package/dist/config/schema.js +1 -0
  13. package/dist/config/schema.js.map +1 -1
  14. package/dist/consensus/deduper.d.ts +26 -2
  15. package/dist/consensus/deduper.js +225 -62
  16. package/dist/consensus/deduper.js.map +1 -1
  17. package/dist/consensus/parser.js +67 -28
  18. package/dist/consensus/parser.js.map +1 -1
  19. package/dist/consensus/types.d.ts +2 -1
  20. package/dist/consensus/voter.d.ts +26 -1
  21. package/dist/consensus/voter.js +198 -78
  22. package/dist/consensus/voter.js.map +1 -1
  23. package/dist/dispatch/adapter.d.ts +0 -5
  24. package/dist/dispatch/adapter.js +1 -23
  25. package/dist/dispatch/adapter.js.map +1 -1
  26. package/dist/dispatch/anthropic.js +93 -82
  27. package/dist/dispatch/anthropic.js.map +1 -1
  28. package/dist/dispatch/google.js +67 -48
  29. package/dist/dispatch/google.js.map +1 -1
  30. package/dist/dispatch/merge.d.ts +12 -0
  31. package/dist/dispatch/merge.js +54 -0
  32. package/dist/dispatch/merge.js.map +1 -0
  33. package/dist/dispatch/openai-compat.js +62 -44
  34. package/dist/dispatch/openai-compat.js.map +1 -1
  35. package/dist/dispatch/openai.js +59 -46
  36. package/dist/dispatch/openai.js.map +1 -1
  37. package/dist/dispatch/runner.d.ts +3 -0
  38. package/dist/dispatch/runner.js +50 -38
  39. package/dist/dispatch/runner.js.map +1 -1
  40. package/dist/dispatch/utils.d.ts +10 -1
  41. package/dist/dispatch/utils.js +18 -2
  42. package/dist/dispatch/utils.js.map +1 -1
  43. package/dist/help/environment.d.ts +4 -0
  44. package/dist/help/environment.js +67 -0
  45. package/dist/help/environment.js.map +1 -0
  46. package/dist/index.js +49 -22
  47. package/dist/index.js.map +1 -1
  48. package/dist/output/github.d.ts +11 -2
  49. package/dist/output/github.js +126 -26
  50. package/dist/output/github.js.map +1 -1
  51. package/dist/output/markdown.js +4 -3
  52. package/dist/output/markdown.js.map +1 -1
  53. package/dist/output/sanitize.d.ts +18 -0
  54. package/dist/output/sanitize.js +46 -0
  55. package/dist/output/sanitize.js.map +1 -0
  56. package/dist/output/terminal.js +4 -1
  57. package/dist/output/terminal.js.map +1 -1
  58. package/dist/prepare/chunker.js +9 -2
  59. package/dist/prepare/chunker.js.map +1 -1
  60. package/dist/prepare/prompt-builder.d.ts +0 -1
  61. package/dist/prepare/prompt-builder.js +22 -7
  62. package/dist/prepare/prompt-builder.js.map +1 -1
  63. package/dist/prompts/hardening.d.ts +9 -0
  64. package/dist/prompts/hardening.js +24 -2
  65. package/dist/prompts/hardening.js.map +1 -1
  66. package/dist/resolver/github.d.ts +2 -1
  67. package/dist/resolver/github.js +9 -7
  68. package/dist/resolver/github.js.map +1 -1
  69. package/dist/roles/builtin.js +92 -0
  70. package/dist/roles/builtin.js.map +1 -1
  71. package/dist/roles/dispatcher.d.ts +4 -3
  72. package/dist/roles/dispatcher.js +17 -10
  73. package/dist/roles/dispatcher.js.map +1 -1
  74. package/dist/roles/loader.js +36 -15
  75. package/dist/roles/loader.js.map +1 -1
  76. package/package.json +1 -2
package/README.md CHANGED
@@ -150,7 +150,7 @@ thresholds:
150
150
  minConsensusScore: 0.4 # 0–1; findings below this are dropped
151
151
  minConfidence: 0.2
152
152
  dedupeLineWindow: 5 # lines within which findings are merged
153
- jaccardThreshold: 0.3 # token-overlap threshold for deduplication
153
+ jaccardThreshold: 0.3 # weighted title+description similarity threshold for dedup
154
154
 
155
155
  # Output defaults
156
156
  output:
@@ -182,9 +182,9 @@ Supported config file names: `.review-council.yml`, `.review-council.yaml`, `.re
182
182
 
183
183
  When multiple models and roles review the same diff, their findings are:
184
184
 
185
- 1. **Deduplicated** — findings on the same file, overlapping line range, and similar category are grouped together using Jaccard token overlap.
186
- 2. **Scored** — each group receives a consensus score based on three dimensions: reviewer diversity (how many distinct models and roles flagged it), role relevance (whether the reporting role specialises in that finding type), and isolation (what fraction of relevant reviewers flagged it).
187
- 3. **Classified** — groups are assigned a confidence band (Very High → Minimal) and a final severity, with upward elevation when multiple high-relevance roles agree.
185
+ 1. **Deduplicated** — findings on the same file and overlapping line range are grouped by weighted title+description token similarity; findings in different categories can still merge, but need stronger similarity (models disagree on category boundaries constantly). Repeats within a single review are collapsed first. Findings that clearly reach opposite conclusions are kept as separate, disputed findings; subtler contradictions merge but are flagged as disputed.
186
+ 2. **Scored** — each group receives a consensus score based on three dimensions: reviewer diversity (how many distinct models and roles flagged it, saturating at half the fleet so large configurations aren't penalized), role relevance (whether a role specialised in that finding type confirmed it), and isolation (what fraction of relevant reviewers flagged it).
187
+ 3. **Classified** — groups are assigned a confidence band (Very High → Minimal) and a final severity. Severity is the most common rating across reviewers; when reviewers disagree, high-confidence agreement elevates it, but only to a severity at least two reviewers independently assigned — a lone outlier rating is surfaced as a dispute instead.
188
188
  4. **Filtered** — groups below `minConsensusScore` or `minConfidence` are dropped.
189
189
 
190
190
  The result is a ranked list of findings that rewards agreement across independent reviewers and penalises noise from a single model.
package/dist/ci.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { ReviewResult } from './consensus/types.js';
2
+ export interface CiVerdict {
3
+ exitCode: number;
4
+ message?: string;
5
+ }
6
+ /**
7
+ * Decide the CI exit status for a completed review.
8
+ *
9
+ * A run where every reviewer errored produces zero findings — which without
10
+ * this check looks identical to "clean" and exits 0, greenlighting code that
11
+ * was never actually reviewed. That case fails first; blocking findings fail
12
+ * second.
13
+ */
14
+ export declare function evaluateCiGate(result: ReviewResult): CiVerdict;
package/dist/ci.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Decide the CI exit status for a completed review.
3
+ *
4
+ * A run where every reviewer errored produces zero findings — which without
5
+ * this check looks identical to "clean" and exits 0, greenlighting code that
6
+ * was never actually reviewed. That case fails first; blocking findings fail
7
+ * second.
8
+ */
9
+ export function evaluateCiGate(result) {
10
+ if (result.stats.successfulReviews === 0) {
11
+ return {
12
+ exitCode: 1,
13
+ message: `CI: 0/${result.stats.totalReviews} reviewers succeeded — nothing was reviewed. Exiting with code 1.`,
14
+ };
15
+ }
16
+ const blocking = result.findings.filter((f) => f.severity === 'critical' || f.severity === 'important');
17
+ if (blocking.length > 0) {
18
+ return {
19
+ exitCode: 1,
20
+ message: `CI: ${blocking.length} blocking finding(s) found. Exiting with code 1.`,
21
+ };
22
+ }
23
+ return { exitCode: 0 };
24
+ }
25
+ //# sourceMappingURL=ci.js.map
package/dist/ci.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ci.js","sourceRoot":"","sources":["../src/ci.ts"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,MAAoB;IACjD,IAAI,MAAM,CAAC,KAAK,CAAC,iBAAiB,KAAK,CAAC,EAAE,CAAC;QACzC,OAAO;YACL,QAAQ,EAAE,CAAC;YACX,OAAO,EAAE,SAAS,MAAM,CAAC,KAAK,CAAC,YAAY,mEAAmE;SAC/G,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CACrC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,UAAU,IAAI,CAAC,CAAC,QAAQ,KAAK,WAAW,CAC/D,CAAC;IACF,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,QAAQ,EAAE,CAAC;YACX,OAAO,EAAE,OAAO,QAAQ,CAAC,MAAM,kDAAkD;SAClF,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;AACzB,CAAC"}
@@ -1,8 +1,20 @@
1
- export declare const DEFAULT_MODELS: readonly ["anthropic/claude-opus-4-6", "openai/gpt-5.4", "google/gemini-2.5-pro"];
1
+ /** SOTA models — run the general role + participate in specialized round-robin */
2
+ export declare const DEFAULT_MODELS: readonly ["anthropic/claude-fable-5", "openai/gpt-5.5", "google/gemini-3.5-flash"];
3
+ /** Previous-gen models — specialized round-robin only, no general role */
4
+ export declare const DEFAULT_SECONDARY_MODELS: readonly ["anthropic/claude-opus-4-8", "openai/gpt-5.4", "google/gemini-2.5-pro"];
2
5
  export declare const DEFAULT_THRESHOLDS: {
3
6
  readonly minConsensusScore: 0.4;
4
7
  readonly minConfidence: 0.2;
5
8
  readonly dedupeLineWindow: 5;
9
+ /**
10
+ * Threshold for the weighted title+description similarity
11
+ * (0.6 * title + 0.4 * description). Calibrated against the fixture
12
+ * corpus: genuine cross-model duplicates score 0.29-0.55 (descriptions
13
+ * diverge heavily across models), so higher thresholds split real
14
+ * duplicates. The strictness gain over the old max(title, desc) check
15
+ * comes from the formula: a title-only match now needs 0.5+ title
16
+ * overlap to merge instead of 0.3.
17
+ */
6
18
  readonly jaccardThreshold: 0.3;
7
19
  };
8
20
  export declare const DEFAULT_TIMEOUT_MS = 120000;
@@ -1,5 +1,12 @@
1
+ /** SOTA models — run the general role + participate in specialized round-robin */
1
2
  export const DEFAULT_MODELS = [
2
- 'anthropic/claude-opus-4-6',
3
+ 'anthropic/claude-fable-5',
4
+ 'openai/gpt-5.5',
5
+ 'google/gemini-3.5-flash',
6
+ ];
7
+ /** Previous-gen models — specialized round-robin only, no general role */
8
+ export const DEFAULT_SECONDARY_MODELS = [
9
+ 'anthropic/claude-opus-4-8',
3
10
  'openai/gpt-5.4',
4
11
  'google/gemini-2.5-pro',
5
12
  ];
@@ -7,6 +14,15 @@ export const DEFAULT_THRESHOLDS = {
7
14
  minConsensusScore: 0.4,
8
15
  minConfidence: 0.2,
9
16
  dedupeLineWindow: 5,
17
+ /**
18
+ * Threshold for the weighted title+description similarity
19
+ * (0.6 * title + 0.4 * description). Calibrated against the fixture
20
+ * corpus: genuine cross-model duplicates score 0.29-0.55 (descriptions
21
+ * diverge heavily across models), so higher thresholds split real
22
+ * duplicates. The strictness gain over the old max(title, desc) check
23
+ * comes from the formula: a title-only match now needs 0.5+ title
24
+ * overlap to merge instead of 0.3.
25
+ */
10
26
  jaccardThreshold: 0.3,
11
27
  };
12
28
  export const DEFAULT_TIMEOUT_MS = 120_000;
@@ -1 +1 @@
1
- {"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,2BAA2B;IAC3B,gBAAgB;IAChB,uBAAuB;CACf,CAAC;AAEX,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,iBAAiB,EAAE,GAAG;IACtB,aAAa,EAAE,GAAG;IAClB,gBAAgB,EAAE,CAAC;IACnB,gBAAgB,EAAE,GAAG;CACb,CAAC;AAEX,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAC1C,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AACrC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAErC,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,UAAU;IACV,WAAW;IACX,OAAO;IACP,SAAS;CACD,CAAC;AAEX,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,QAAQ,EAAE,GAAG;IACb,IAAI,EAAE,GAAG;IACT,MAAM,EAAE,GAAG;IACX,GAAG,EAAE,GAAG;CACA,CAAC"}
1
+ {"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,0BAA0B;IAC1B,gBAAgB;IAChB,yBAAyB;CACjB,CAAC;AAEX,0EAA0E;AAC1E,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,2BAA2B;IAC3B,gBAAgB;IAChB,uBAAuB;CACf,CAAC;AAEX,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,iBAAiB,EAAE,GAAG;IACtB,aAAa,EAAE,GAAG;IAClB,gBAAgB,EAAE,CAAC;IACnB;;;;;;;;OAQG;IACH,gBAAgB,EAAE,GAAG;CACb,CAAC;AAEX,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAC1C,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AACrC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAErC,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,UAAU;IACV,WAAW;IACX,OAAO;IACP,SAAS;CACD,CAAC;AAEX,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,QAAQ,EAAE,GAAG;IACb,IAAI,EAAE,GAAG;IACT,MAAM,EAAE,GAAG;IACX,GAAG,EAAE,GAAG;CACA,CAAC"}
@@ -1,2 +1,10 @@
1
1
  import { type Config } from './schema.js';
2
- export declare function loadConfig(configPath?: string): Promise<Config>;
2
+ /**
3
+ * A config file was found but could not be used. Never silently recover
4
+ * from this: falling back to defaults would send code to cloud providers
5
+ * the user may have explicitly configured away from.
6
+ */
7
+ export declare class ConfigError extends Error {
8
+ constructor(message: string);
9
+ }
10
+ export declare function loadConfig(configPath?: string, searchFrom?: string): Promise<Config>;
@@ -1,42 +1,62 @@
1
1
  import { cosmiconfig } from 'cosmiconfig';
2
2
  import { ConfigSchema } from './schema.js';
3
- import { DEFAULT_MODELS, DEFAULT_THRESHOLDS, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_CONCURRENCY, } from './defaults.js';
4
- const explorer = cosmiconfig('review-council', {
5
- searchPlaces: [
6
- '.review-council.yml',
7
- '.review-council.yaml',
8
- '.review-council.json',
9
- '.review-council.js',
10
- '.review-council.cjs',
11
- 'review-council.config.js',
12
- 'review-council.config.cjs',
13
- ],
14
- });
15
- export async function loadConfig(configPath) {
3
+ import { DEFAULT_MODELS, DEFAULT_SECONDARY_MODELS, DEFAULT_THRESHOLDS, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_CONCURRENCY, } from './defaults.js';
4
+ /**
5
+ * A config file was found but could not be used. Never silently recover
6
+ * from this: falling back to defaults would send code to cloud providers
7
+ * the user may have explicitly configured away from.
8
+ */
9
+ export class ConfigError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'ConfigError';
13
+ }
14
+ }
15
+ // Declarative formats only. Executable config (.js/.cjs) is deliberately
16
+ // unsupported for discovery: rcl is routinely run inside untrusted
17
+ // checkouts, and search-loading attacker-controlled JS would execute it
18
+ // with the user's API keys in env.
19
+ const SEARCH_PLACES = [
20
+ '.review-council.yml',
21
+ '.review-council.yaml',
22
+ '.review-council.json',
23
+ ];
24
+ export async function loadConfig(configPath, searchFrom) {
25
+ const cwd = searchFrom ?? process.cwd();
26
+ const explorer = cosmiconfig('review-council', {
27
+ searchPlaces: SEARCH_PLACES,
28
+ // 'none' searches only the starting directory — no parent-directory walk
29
+ // and no global (~/.config) dir, which would otherwise be probed with
30
+ // cosmiconfig's default executable-config loaders. (stopDir is rejected
31
+ // alongside a non-'global' strategy, and is unnecessary here.)
32
+ searchStrategy: 'none',
33
+ });
34
+ let result;
16
35
  try {
17
- const result = configPath
36
+ result = configPath
18
37
  ? await explorer.load(configPath)
19
- : await explorer.search();
20
- if (!result || result.isEmpty) {
21
- return buildDefaultConfig();
22
- }
23
- const parsed = ConfigSchema.safeParse(result.config);
24
- if (!parsed.success) {
25
- console.warn('Warning: config validation errors:', parsed.error.format());
26
- return buildDefaultConfig();
27
- }
28
- return mergeWithDefaults(parsed.data);
38
+ : await explorer.search(cwd);
29
39
  }
30
40
  catch (err) {
31
- if (configPath) {
32
- throw new Error(`Failed to load config from ${configPath}: ${String(err)}`);
33
- }
41
+ const source = configPath ?? 'discovered config';
42
+ throw new ConfigError(`Failed to load config from ${source}: ${String(err)}`);
43
+ }
44
+ if (!result || result.isEmpty) {
34
45
  return buildDefaultConfig();
35
46
  }
47
+ const parsed = ConfigSchema.safeParse(result.config);
48
+ if (!parsed.success) {
49
+ const issues = parsed.error.issues
50
+ .map((issue) => ` ${issue.path.join('.') || '(root)'}: ${issue.message}`)
51
+ .join('\n');
52
+ throw new ConfigError(`Invalid config at ${result.filepath}:\n${issues}`);
53
+ }
54
+ return mergeWithDefaults(parsed.data);
36
55
  }
37
56
  function buildDefaultConfig() {
38
57
  return {
39
58
  models: [...DEFAULT_MODELS],
59
+ secondaryModels: [...DEFAULT_SECONDARY_MODELS],
40
60
  thresholds: { ...DEFAULT_THRESHOLDS },
41
61
  timeout: DEFAULT_TIMEOUT_MS,
42
62
  maxRetries: DEFAULT_MAX_RETRIES,
@@ -46,6 +66,7 @@ function buildDefaultConfig() {
46
66
  function mergeWithDefaults(config) {
47
67
  return {
48
68
  models: config.models ?? [...DEFAULT_MODELS],
69
+ secondaryModels: config.secondaryModels ?? (config.models ? [] : [...DEFAULT_SECONDARY_MODELS]),
49
70
  roles: config.roles,
50
71
  reviewers: config.reviewers,
51
72
  customRoles: config.customRoles,
@@ -1 +1 @@
1
- {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAe,MAAM,aAAa,CAAC;AACxD,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AAEvB,MAAM,QAAQ,GAAG,WAAW,CAAC,gBAAgB,EAAE;IAC7C,YAAY,EAAE;QACZ,qBAAqB;QACrB,sBAAsB;QACtB,sBAAsB;QACtB,oBAAoB;QACpB,qBAAqB;QACrB,0BAA0B;QAC1B,2BAA2B;KAC5B;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,UAAmB;IAClD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,UAAU;YACvB,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC;YACjC,CAAC,CAAC,MAAM,QAAQ,CAAC,MAAM,EAAE,CAAC;QAE5B,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YAC9B,OAAO,kBAAkB,EAAE,CAAC;QAC9B,CAAC;QAED,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACrD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,CAAC,IAAI,CAAC,oCAAoC,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;YAC1E,OAAO,kBAAkB,EAAE,CAAC;QAC9B,CAAC;QAED,OAAO,iBAAiB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,8BAA8B,UAAU,KAAK,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9E,CAAC;QACD,OAAO,kBAAkB,EAAE,CAAC;IAC9B,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB;IACzB,OAAO;QACL,MAAM,EAAE,CAAC,GAAG,cAAc,CAAC;QAC3B,UAAU,EAAE,EAAE,GAAG,kBAAkB,EAAE;QACrC,OAAO,EAAE,kBAAkB;QAC3B,UAAU,EAAE,mBAAmB;QAC/B,WAAW,EAAE,mBAAmB;KACjC,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,MAAc;IACvC,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,GAAG,cAAc,CAAC;QAC5C,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,UAAU,EAAE;YACV,GAAG,kBAAkB;YACrB,GAAG,MAAM,CAAC,UAAU;SACrB;QACD,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,kBAAkB;QAC7C,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,mBAAmB;QACpD,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,mBAAmB;QACtD,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;QAC9D,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"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"}
@@ -38,6 +38,7 @@ export declare const OutputSchema: z.ZodObject<{
38
38
  }, z.core.$strip>;
39
39
  export declare const ConfigSchema: z.ZodObject<{
40
40
  models: z.ZodOptional<z.ZodArray<z.ZodString>>;
41
+ secondaryModels: z.ZodOptional<z.ZodArray<z.ZodString>>;
41
42
  roles: z.ZodOptional<z.ZodArray<z.ZodString>>;
42
43
  reviewers: z.ZodOptional<z.ZodArray<z.ZodObject<{
43
44
  model: z.ZodString;
@@ -33,6 +33,7 @@ export const OutputSchema = z.object({
33
33
  });
34
34
  export const ConfigSchema = z.object({
35
35
  models: z.array(z.string()).optional(),
36
+ secondaryModels: z.array(z.string()).optional(),
36
37
  roles: z.array(z.string()).optional(),
37
38
  reviewers: z.array(ReviewerPairSchema).optional(),
38
39
  customRoles: z.array(RoleConfigSchema).optional(),
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;AACpF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,IAAI,CAAC;IACnC,UAAU;IACV,aAAa;IACb,gBAAgB;IAChB,OAAO;IACP,YAAY;CACb,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;CACjB,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACnC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACrC,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;CAC1D,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACtD,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAClD,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACxD,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CACtD,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;IAChC,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;IAC5B,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;IAChC,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE;IAC9B,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACpC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACtC,eAAe,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IAC/C,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACrC,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,QAAQ,EAAE;IACjD,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,QAAQ,EAAE;IACjD,UAAU,EAAE,gBAAgB,CAAC,QAAQ,EAAE;IACvC,MAAM,EAAE,YAAY,CAAC,QAAQ,EAAE;IAC/B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACzC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE;IACrD,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACnD,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;IACvC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC3B,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE;CACtC,CAAC,CAAC"}
@@ -1,6 +1,30 @@
1
- import type { ModelReview, DeduplicatedGroup } from './types.js';
1
+ import type { Finding, ModelReview, DeduplicatedGroup } from './types.js';
2
2
  /**
3
- * Compute Jaccard similarity between two strings (word-level tokenization)
3
+ * Compute Jaccard similarity between two strings (word-level tokenization).
4
+ * Single characters and stopwords are ignored; short signal tokens like
5
+ * "xss", "id", or "no" are kept.
4
6
  */
5
7
  export declare function jaccardSimilarity(a: string, b: string): number;
8
+ /**
9
+ * Weighted title+description similarity. Both fields contribute, so a short
10
+ * generic title alone can no longer merge two unrelated findings, and a
11
+ * verbose description alone can't either.
12
+ *
13
+ * A field where either side has no usable tokens carries no signal and is
14
+ * excluded, with weights renormalized over the remaining fields — empty
15
+ * descriptions neither grant free similarity nor penalize a strong title
16
+ * match. If no field has usable tokens on both sides, similarity is 0.
17
+ */
18
+ export declare function combinedSimilarity(a: Finding, b: Finding): number;
19
+ /**
20
+ * Detect findings that reach opposite conclusions. Word-boundary matching
21
+ * prevents substring traps ("unsafe" does not match "safe").
22
+ *
23
+ * With `specificOnly`, only high-precision pairs count — used for the merge
24
+ * veto, where a false positive fragments a genuine duplicate group. The
25
+ * default (all pairs) is for dispute flagging, where a false positive just
26
+ * adds a warning.
27
+ */
28
+ export declare function hasOpposingSentiment(a: Finding, b: Finding, specificOnly?: boolean): boolean;
29
+ export declare function linesOverlap(a: Finding, b: Finding, window: number): boolean;
6
30
  export declare function deduplicateFindings(reviews: ModelReview[], jaccardThreshold?: number, lineWindow?: number): DeduplicatedGroup[];