@eviano/tribunal 0.1.2 → 0.2.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/README.md +177 -11
- package/dist/analyzers/claimReconciliation.js +43 -22
- package/dist/analyzers/claimReconciliation.js.map +1 -1
- package/dist/analyzers/commentCodeDrift.d.ts +37 -0
- package/dist/analyzers/commentCodeDrift.js +241 -0
- package/dist/analyzers/commentCodeDrift.js.map +1 -0
- package/dist/analyzers/hallucinatedSymbol.js +22 -0
- package/dist/analyzers/hallucinatedSymbol.js.map +1 -1
- package/dist/analyzers/index.d.ts +5 -1
- package/dist/analyzers/index.js +12 -2
- package/dist/analyzers/index.js.map +1 -1
- package/dist/analyzers/riskyDiffNoTest.d.ts +36 -0
- package/dist/analyzers/riskyDiffNoTest.js +221 -0
- package/dist/analyzers/riskyDiffNoTest.js.map +1 -0
- package/dist/cli.js +167 -24
- package/dist/cli.js.map +1 -1
- package/dist/config/loadConfig.d.ts +19 -0
- package/dist/config/loadConfig.js +89 -0
- package/dist/config/loadConfig.js.map +1 -0
- package/dist/config/parseYaml.d.ts +17 -0
- package/dist/config/parseYaml.js +131 -0
- package/dist/config/parseYaml.js.map +1 -0
- package/dist/config/types.d.ts +17 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/diff/parseUnifiedDiff.d.ts +4 -0
- package/dist/diff/parseUnifiedDiff.js +45 -18
- package/dist/diff/parseUnifiedDiff.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +27 -0
- package/dist/paths.js +112 -0
- package/dist/paths.js.map +1 -0
- package/dist/propose.d.ts +103 -0
- package/dist/propose.js +175 -0
- package/dist/propose.js.map +1 -0
- package/dist/report/render.d.ts +5 -0
- package/dist/report/render.js +78 -0
- package/dist/report/render.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,39 +4,66 @@
|
|
|
4
4
|
*
|
|
5
5
|
* We deliberately track only new-file line numbers: analyzers reason about the post-change tree, so a
|
|
6
6
|
* "touched" node is one whose new-file line range overlaps `addedLines`.
|
|
7
|
+
*
|
|
8
|
+
* Both git-style diffs (`diff --git`) and plain unified diffs (no `diff --git` header, e.g. `diff -u` or
|
|
9
|
+
* a `git format-patch` body) are supported. A plain diff opens a file record on its first `---`/`+++`
|
|
10
|
+
* header so a hand-written or piped patch is never silently dropped to `[]`.
|
|
7
11
|
*/
|
|
8
12
|
export function parseUnifiedDiff(diff) {
|
|
9
13
|
const files = [];
|
|
14
|
+
// `current`/`newLine`/`seenPlus` are reassigned across iterations; we read them into a narrowed local
|
|
15
|
+
// (`rec`) where we use them, since TS can't keep a closure-mutated `let` narrowed through the loop.
|
|
10
16
|
let current = null;
|
|
11
17
|
let newLine = 0;
|
|
18
|
+
// True once the current record has consumed its `+++` header. A subsequent `---` then starts the next
|
|
19
|
+
// file (plain unified diffs have no `diff --git` line to delimit files).
|
|
20
|
+
let seenPlus = false;
|
|
12
21
|
for (const line of diff.split('\n')) {
|
|
13
22
|
if (line.startsWith('diff --git')) {
|
|
14
|
-
|
|
15
|
-
files.push(
|
|
23
|
+
const rec = { path: '', status: 'modified', addedLines: new Set() };
|
|
24
|
+
files.push(rec);
|
|
25
|
+
current = rec;
|
|
26
|
+
seenPlus = false;
|
|
16
27
|
const m = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
17
28
|
if (m)
|
|
18
|
-
|
|
29
|
+
rec.path = m[2];
|
|
19
30
|
continue;
|
|
20
31
|
}
|
|
21
|
-
|
|
32
|
+
// A `---` header opens a record for plain (non-git) unified diffs, and separates consecutive files.
|
|
33
|
+
// In git format `diff --git` already opened the record, and the first `---` arrives before any `+++`
|
|
34
|
+
// (seenPlus === false), so it does not re-open.
|
|
35
|
+
if (line.startsWith('--- ')) {
|
|
36
|
+
if (!current || seenPlus) {
|
|
37
|
+
const rec = { path: '', status: 'modified', addedLines: new Set() };
|
|
38
|
+
files.push(rec);
|
|
39
|
+
current = rec;
|
|
40
|
+
seenPlus = false;
|
|
41
|
+
const p = line.slice(4).trim();
|
|
42
|
+
// Provisional old-path; kept only when there is no `+++` (e.g. a plain deletion) so the record
|
|
43
|
+
// isn't left pathless. `+++` overrides it for normal adds/modifies.
|
|
44
|
+
if (p && p !== '/dev/null')
|
|
45
|
+
rec.path = p.replace(/^a\//, '');
|
|
46
|
+
}
|
|
22
47
|
continue;
|
|
23
|
-
if (line.startsWith('new file mode')) {
|
|
24
|
-
current.status = 'added';
|
|
25
|
-
}
|
|
26
|
-
else if (line.startsWith('deleted file mode')) {
|
|
27
|
-
current.status = 'deleted';
|
|
28
|
-
}
|
|
29
|
-
else if (line.startsWith('rename to ')) {
|
|
30
|
-
current.path = line.slice('rename to '.length).trim();
|
|
31
|
-
current.status = 'renamed';
|
|
32
48
|
}
|
|
33
|
-
|
|
49
|
+
const rec = current;
|
|
50
|
+
if (!rec)
|
|
51
|
+
continue;
|
|
52
|
+
if (line.startsWith('+++ ')) {
|
|
34
53
|
const p = line.slice(4).trim();
|
|
35
54
|
if (p !== '/dev/null')
|
|
36
|
-
|
|
55
|
+
rec.path = p.replace(/^b\//, '');
|
|
56
|
+
seenPlus = true;
|
|
57
|
+
}
|
|
58
|
+
else if (line.startsWith('new file mode')) {
|
|
59
|
+
rec.status = 'added';
|
|
37
60
|
}
|
|
38
|
-
else if (line.startsWith('
|
|
39
|
-
|
|
61
|
+
else if (line.startsWith('deleted file mode')) {
|
|
62
|
+
rec.status = 'deleted';
|
|
63
|
+
}
|
|
64
|
+
else if (line.startsWith('rename to ')) {
|
|
65
|
+
rec.path = line.slice('rename to '.length).trim();
|
|
66
|
+
rec.status = 'renamed';
|
|
40
67
|
}
|
|
41
68
|
else if (line.startsWith('@@')) {
|
|
42
69
|
const m = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
@@ -44,7 +71,7 @@ export function parseUnifiedDiff(diff) {
|
|
|
44
71
|
newLine = parseInt(m[1], 10);
|
|
45
72
|
}
|
|
46
73
|
else if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
47
|
-
|
|
74
|
+
rec.addedLines.add(newLine);
|
|
48
75
|
newLine++;
|
|
49
76
|
}
|
|
50
77
|
else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parseUnifiedDiff.js","sourceRoot":"","sources":["../../src/diff/parseUnifiedDiff.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"parseUnifiedDiff.js","sourceRoot":"","sources":["../../src/diff/parseUnifiedDiff.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { type DiffSource } from './diff/gitDiff.js';
|
|
2
2
|
import type { Analyzer, AnalyzerContext, Report } from './types.js';
|
|
3
3
|
export type { Analyzer, AnalyzerContext, ChangedFile, Claim, Finding, Report, Verdict } from './types.js';
|
|
4
|
-
export { analyzers, assertionFreeTest, hallucinatedSymbol, claimReconciliation } from './analyzers/index.js';
|
|
4
|
+
export { analyzers, assertionFreeTest, hallucinatedSymbol, riskyDiffNoTest, commentCodeDrift, claimReconciliation } from './analyzers/index.js';
|
|
5
5
|
export { recognizedClaims } from './analyzers/claimReconciliation.js';
|
|
6
6
|
export { parseClaims, type ParseClaimsOptions } from './claims.js';
|
|
7
|
+
export { buildPrompt, extractClaimsFromResponse, renderClaimsBlock, runPropose, PROPOSABLE_CLAIMS, type ProposeProvider, type FetchLike, type RunProposeOptions, type RunProposeResult, } from './propose.js';
|
|
7
8
|
export { parseUnifiedDiff } from './diff/parseUnifiedDiff.js';
|
|
8
9
|
export { getChangedFiles, makeContext, type DiffSource } from './diff/gitDiff.js';
|
|
9
|
-
export { buildReport, exitCode, renderJson, renderMarkdown } from './report/render.js';
|
|
10
|
+
export { buildReport, exitCode, renderJson, renderMarkdown, renderSarif } from './report/render.js';
|
|
10
11
|
export interface RunOptions {
|
|
11
12
|
analyzers?: Analyzer[];
|
|
12
13
|
}
|
package/dist/index.js
CHANGED
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
import { analyzers as defaultAnalyzers } from './analyzers/index.js';
|
|
7
7
|
import { makeContext } from './diff/gitDiff.js';
|
|
8
8
|
import { buildReport } from './report/render.js';
|
|
9
|
-
export { analyzers, assertionFreeTest, hallucinatedSymbol, claimReconciliation } from './analyzers/index.js';
|
|
9
|
+
export { analyzers, assertionFreeTest, hallucinatedSymbol, riskyDiffNoTest, commentCodeDrift, claimReconciliation } from './analyzers/index.js';
|
|
10
10
|
export { recognizedClaims } from './analyzers/claimReconciliation.js';
|
|
11
11
|
export { parseClaims } from './claims.js';
|
|
12
|
+
export { buildPrompt, extractClaimsFromResponse, renderClaimsBlock, runPropose, PROPOSABLE_CLAIMS, } from './propose.js';
|
|
12
13
|
export { parseUnifiedDiff } from './diff/parseUnifiedDiff.js';
|
|
13
14
|
export { getChangedFiles, makeContext } from './diff/gitDiff.js';
|
|
14
|
-
export { buildReport, exitCode, renderJson, renderMarkdown } from './report/render.js';
|
|
15
|
+
export { buildReport, exitCode, renderJson, renderMarkdown, renderSarif } from './report/render.js';
|
|
15
16
|
/** Run analyzers over an AnalyzerContext and aggregate into a Report. */
|
|
16
17
|
export function runAnalyzers(ctx, opts = {}) {
|
|
17
18
|
const list = opts.analyzers ?? defaultAnalyzers;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,SAAS,IAAI,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EAAE,WAAW,EAAmB,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAIjD,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,SAAS,IAAI,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EAAE,WAAW,EAAmB,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAIjD,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,eAAe,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChJ,OAAO,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AACtE,OAAO,EAAE,WAAW,EAA2B,MAAM,aAAa,CAAC;AACnE,OAAO,EACL,WAAW,EACX,yBAAyB,EACzB,iBAAiB,EACjB,UAAU,EACV,iBAAiB,GAKlB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,WAAW,EAAmB,MAAM,mBAAmB,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAMpG,yEAAyE;AACzE,MAAM,UAAU,YAAY,CAAC,GAAoB,EAAE,OAAmB,EAAE;IACtE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,IAAI,gBAAgB,CAAC;IAChD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IACjD,OAAO,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;AAC5C,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,WAAW,CAAC,GAAe,EAAE,OAAmB,EAAE;IAChE,OAAO,YAAY,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC;AAC9C,CAAC"}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated/build-output path detection.
|
|
3
|
+
*
|
|
4
|
+
* A bundled artifact (e.g. `action-dist/cli.cjs`) carries the project's own source — including risky
|
|
5
|
+
* vocab like `token` / `secret` — and isn't hand-written source a reviewer can act on. Flagging it is a
|
|
6
|
+
* false-ish positive: technically correct, operationally noisy. `riskyDiffNoTest` skips generated paths
|
|
7
|
+
* so the signal lands on human-authored code.
|
|
8
|
+
*
|
|
9
|
+
* This list is a deliberate, conservative default. It is overridable: set `skipGenerated = false` on
|
|
10
|
+
* the analyzer (via the CLI's `--no-skip-generated` / `TRIBUNAL_NO_SKIP_GENERATED=1`) to disable it
|
|
11
|
+
* entirely — an opinionated default must never silently suppress a file the user wants checked.
|
|
12
|
+
*
|
|
13
|
+
* Scope: advisory, on `riskyDiffNoTest` only. It does NOT affect the verdict path, `exitCode`, or other
|
|
14
|
+
* analyzers.
|
|
15
|
+
*/
|
|
16
|
+
/** The full default pattern set, exported so a future config file can extend or replace it. */
|
|
17
|
+
export declare const GENERATED_PATH_PATTERNS: readonly string[];
|
|
18
|
+
/**
|
|
19
|
+
* True if a repo-relative path looks like generated/build output. Normalizes to forward slashes so it
|
|
20
|
+
* works regardless of platform. A path matches if any segment begins with a generated dir prefix, the
|
|
21
|
+
* basename ends with a generated suffix, OR it matches an extra pattern (from tribunal.yml config).
|
|
22
|
+
*
|
|
23
|
+
* Extra patterns are appended to the built-ins at match time — config can only ADD coverage, never drop
|
|
24
|
+
* a safety net. An extra pattern is matched as: a dir-prefix (vendor-gen with trailing slash), a suffix
|
|
25
|
+
* (.gen.ts), or a simple glob with single-segment star and double-star across segments.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isGeneratedPath(path: string, extraPatterns?: readonly string[]): boolean;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated/build-output path detection.
|
|
3
|
+
*
|
|
4
|
+
* A bundled artifact (e.g. `action-dist/cli.cjs`) carries the project's own source — including risky
|
|
5
|
+
* vocab like `token` / `secret` — and isn't hand-written source a reviewer can act on. Flagging it is a
|
|
6
|
+
* false-ish positive: technically correct, operationally noisy. `riskyDiffNoTest` skips generated paths
|
|
7
|
+
* so the signal lands on human-authored code.
|
|
8
|
+
*
|
|
9
|
+
* This list is a deliberate, conservative default. It is overridable: set `skipGenerated = false` on
|
|
10
|
+
* the analyzer (via the CLI's `--no-skip-generated` / `TRIBUNAL_NO_SKIP_GENERATED=1`) to disable it
|
|
11
|
+
* entirely — an opinionated default must never silently suppress a file the user wants checked.
|
|
12
|
+
*
|
|
13
|
+
* Scope: advisory, on `riskyDiffNoTest` only. It does NOT affect the verdict path, `exitCode`, or other
|
|
14
|
+
* analyzers.
|
|
15
|
+
*/
|
|
16
|
+
/** Directory prefixes whose contents are treated as generated (trailing slash = path segment match). */
|
|
17
|
+
const GENERATED_DIRS = [
|
|
18
|
+
'dist/',
|
|
19
|
+
'action-dist/',
|
|
20
|
+
'build/',
|
|
21
|
+
'out/',
|
|
22
|
+
'.next/',
|
|
23
|
+
'.output/',
|
|
24
|
+
'.svelte-kit/',
|
|
25
|
+
'coverage/',
|
|
26
|
+
'.turbo/',
|
|
27
|
+
'.cache/',
|
|
28
|
+
'node_modules/',
|
|
29
|
+
];
|
|
30
|
+
/** File suffixes treated as generated (minified/bundled output). */
|
|
31
|
+
const GENERATED_SUFFIXES = ['.min.js', '.min.cjs', '.min.mjs', '.bundle.js'];
|
|
32
|
+
/** The full default pattern set, exported so a future config file can extend or replace it. */
|
|
33
|
+
export const GENERATED_PATH_PATTERNS = [...GENERATED_DIRS, ...GENERATED_SUFFIXES];
|
|
34
|
+
/**
|
|
35
|
+
* True if a repo-relative path looks like generated/build output. Normalizes to forward slashes so it
|
|
36
|
+
* works regardless of platform. A path matches if any segment begins with a generated dir prefix, the
|
|
37
|
+
* basename ends with a generated suffix, OR it matches an extra pattern (from tribunal.yml config).
|
|
38
|
+
*
|
|
39
|
+
* Extra patterns are appended to the built-ins at match time — config can only ADD coverage, never drop
|
|
40
|
+
* a safety net. An extra pattern is matched as: a dir-prefix (vendor-gen with trailing slash), a suffix
|
|
41
|
+
* (.gen.ts), or a simple glob with single-segment star and double-star across segments.
|
|
42
|
+
*/
|
|
43
|
+
export function isGeneratedPath(path, extraPatterns = []) {
|
|
44
|
+
if (!path)
|
|
45
|
+
return false;
|
|
46
|
+
// Normalize backslashes to forward slashes so matching is platform-independent.
|
|
47
|
+
const norm = path.indexOf('\\') >= 0 ? path.split('\\').join('/') : path;
|
|
48
|
+
for (const dir of GENERATED_DIRS) {
|
|
49
|
+
// match `dir` as the first segment (e.g. dist/...) OR any nested segment (e.g. pkg/dist/...)
|
|
50
|
+
if (norm === dir.slice(0, -1) || norm.startsWith(dir) || norm.includes('/' + dir))
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
const base = norm.split('/').pop() ?? norm;
|
|
54
|
+
for (const suffix of GENERATED_SUFFIXES) {
|
|
55
|
+
if (base.endsWith(suffix))
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
for (const pat of extraPatterns) {
|
|
59
|
+
if (matchExtraPattern(norm, pat))
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Match a config-supplied extra pattern. Supports plain dir-prefix (`vendor-gen/`), plain suffix
|
|
66
|
+
* (`.gen.ts`), and `*`/`**` globs. Kept deliberately simple — covers the common "this dir/file is
|
|
67
|
+
* generated" cases without a full glob library.
|
|
68
|
+
*/
|
|
69
|
+
function matchExtraPattern(normPath, pattern) {
|
|
70
|
+
const p = pattern.trim();
|
|
71
|
+
if (p === '')
|
|
72
|
+
return false;
|
|
73
|
+
// No glob chars: treat as dir-prefix or suffix (same heuristic as the built-ins).
|
|
74
|
+
if (!/[*?]/.test(p)) {
|
|
75
|
+
if (p.endsWith('/'))
|
|
76
|
+
return normPath === p.slice(0, -1) || normPath.startsWith(p) || normPath.includes('/' + p);
|
|
77
|
+
const base = normPath.split('/').pop() ?? normPath;
|
|
78
|
+
return base.endsWith(p) || normPath === p;
|
|
79
|
+
}
|
|
80
|
+
// Glob: convert to a regex. `**` → match across segments; `*` → within one segment; escape the rest.
|
|
81
|
+
const re = globToRegex(p);
|
|
82
|
+
// Match the path itself, or any path under a matched dir (so `**/*.gen.ts` and `vendor-gen/**` both
|
|
83
|
+
// behave intuitively).
|
|
84
|
+
return re.test(normPath) || re.test(normPath + '/');
|
|
85
|
+
}
|
|
86
|
+
/** Convert a simple `*`/`**` glob into a RegExp. */
|
|
87
|
+
function globToRegex(glob) {
|
|
88
|
+
let out = '^';
|
|
89
|
+
let i = 0;
|
|
90
|
+
while (i < glob.length) {
|
|
91
|
+
const c = glob[i];
|
|
92
|
+
if (glob.startsWith('**', i)) {
|
|
93
|
+
out += '.*';
|
|
94
|
+
i += 2;
|
|
95
|
+
}
|
|
96
|
+
else if (c === '*') {
|
|
97
|
+
out += '[^/]*';
|
|
98
|
+
i += 1;
|
|
99
|
+
}
|
|
100
|
+
else if (/[a-zA-Z0-9_]/.test(c)) {
|
|
101
|
+
out += c;
|
|
102
|
+
i += 1;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Escape any other punctuation (., /, -, etc.) for regex safety.
|
|
106
|
+
out += '\\' + c;
|
|
107
|
+
i += 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return new RegExp(out + '$');
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tribunal propose` — an LLM-*proposer* that turns a diff (and optional PR body) into a
|
|
3
|
+
* ```tribunal claims block of CANDIDATE claims, for the existing deterministic `tribunal check` to
|
|
4
|
+
* adjudicate.
|
|
5
|
+
*
|
|
6
|
+
* ── The Trust-Contract boundary (SPEC §1): a model may PROPOSE a claim to check; a model may never
|
|
7
|
+
* ADJUDICATE. This module honors that with two guarantees:
|
|
8
|
+
*
|
|
9
|
+
* 1. SEPARATION: nothing here imports or calls `runAnalyzers`, `runTribunal`, `exitCode`, or any
|
|
10
|
+
* verifier. `propose` only reads a diff and writes a claims block. The verdict path is untouched.
|
|
11
|
+
* 2. DEFENSE-IN-DEPTH: even if the model hallucinates a key or is prompt-injected, the *existing*
|
|
12
|
+
* `claimReconciliation` verifier map degrades every unrecognized key to 🟡 UNVERIFIED (never 🔴).
|
|
13
|
+
* So the worst a malicious LLM can do is emit noise that the deterministic checker labels yellow.
|
|
14
|
+
* It cannot flip a build red. We additionally constrain the prompt to the recognized set and
|
|
15
|
+
* validate the response, but those are convenience, not the safety boundary — the boundary is
|
|
16
|
+
* architectural.
|
|
17
|
+
*
|
|
18
|
+
* ── The send-guard: the full diff is source code, and sending it to an external LLM endpoint is an
|
|
19
|
+
* outward-facing publish. `propose` refuses to send unless the caller opts in with `allowSendDiff`.
|
|
20
|
+
* Without it, the prompt is printed for review and nothing leaves the machine.
|
|
21
|
+
*/
|
|
22
|
+
import type { Claim } from './types.js';
|
|
23
|
+
/** The closed claim vocabulary the model is allowed to propose from, deduped & sorted for the prompt. */
|
|
24
|
+
export declare const PROPOSABLE_CLAIMS: readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* Build the { system, user } prompt for the proposer. Pure & deterministic — no I/O. The prompt:
|
|
27
|
+
* - tells the model the ONLY keys it may emit (the recognized set),
|
|
28
|
+
* - explains that any other output is useless (it degrades to UNVERIFIED in the checker),
|
|
29
|
+
* - asks for a JSON object so parsing is robust,
|
|
30
|
+
* - includes the full diff (per the chosen design) + any PR body as context.
|
|
31
|
+
*
|
|
32
|
+
* Note on the diff in the prompt: prompt-injection from diff content cannot escalate here, because the
|
|
33
|
+
* model's output is never trusted (see module doc + `validateAndNormalize`). It can at worst produce a
|
|
34
|
+
* claim key, which is either recognized (and then deterministically verified by `check`) or ignored.
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildPrompt(diff: string, prBody: string | undefined, proposals?: readonly string[]): {
|
|
37
|
+
system: string;
|
|
38
|
+
user: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Parse a model response into candidate claims. Pure & fault-tolerant:
|
|
42
|
+
* 1. Try JSON first (preferred). Accept {"claims": [...]}.
|
|
43
|
+
* 2. Fall back to scanning for claim-key lines (covers endpoints without JSON mode).
|
|
44
|
+
* Then VALIDATE every key against the recognized set — unknown keys are dropped, never trusted. This
|
|
45
|
+
* is defense-in-depth: even a malicious/injected response cannot smuggle through an unrecognized key,
|
|
46
|
+
* because the *checker* would label it UNVERIFIED anyway, and we drop it here for cleanliness.
|
|
47
|
+
*/
|
|
48
|
+
export declare function extractClaimsFromResponse(text: string, proposals?: readonly string[]): Claim[];
|
|
49
|
+
/**
|
|
50
|
+
* Render claims as a ```tribunal fenced block. This is the exact format `tribunal check --pr-body`
|
|
51
|
+
* consumes, so `propose` → `check` is a clean round-trip:
|
|
52
|
+
*
|
|
53
|
+
* tribunal propose --diff pr.diff --allow-send-diff --out claims.md
|
|
54
|
+
* tribunal check --diff pr.diff --pr-body claims.md
|
|
55
|
+
*
|
|
56
|
+
* The empty case uses a `#` comment (which `parseClaims` ignores) rather than a sentinel word, so an
|
|
57
|
+
* empty block round-trips to zero claims instead of becoming a bogus claim.
|
|
58
|
+
*/
|
|
59
|
+
export declare function renderClaimsBlock(claims: readonly Claim[]): string;
|
|
60
|
+
/** An OpenAI-compatible chat-completions endpoint, abstracted for testability + provider choice. */
|
|
61
|
+
export interface ProposeProvider {
|
|
62
|
+
endpoint: string;
|
|
63
|
+
model: string;
|
|
64
|
+
apiKey?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Fetch-like function signature. Matches the global `fetch`. Injected so tests never touch the network
|
|
68
|
+
* and so the module stays deterministic.
|
|
69
|
+
*/
|
|
70
|
+
export type FetchLike = (url: string, init: {
|
|
71
|
+
method: string;
|
|
72
|
+
headers: Record<string, string>;
|
|
73
|
+
body: string;
|
|
74
|
+
}) => Promise<{
|
|
75
|
+
ok: boolean;
|
|
76
|
+
status: number;
|
|
77
|
+
text: () => Promise<string>;
|
|
78
|
+
}>;
|
|
79
|
+
export interface RunProposeOptions {
|
|
80
|
+
diff: string;
|
|
81
|
+
prBody?: string;
|
|
82
|
+
provider: ProposeProvider;
|
|
83
|
+
/** The fetch implementation to use (defaults to global fetch). Inject for tests. */
|
|
84
|
+
fetch?: FetchLike;
|
|
85
|
+
/** When false (default), the prompt is printed for review and NOTHING is sent. */
|
|
86
|
+
allowSendDiff?: boolean;
|
|
87
|
+
/** Sink for human-facing notices (warnings, send confirmations). Defaults to stderr. */
|
|
88
|
+
notice?: (msg: string) => void;
|
|
89
|
+
}
|
|
90
|
+
export interface RunProposeResult {
|
|
91
|
+
/** The ```tribunal claims block to feed to `tribunal check`. */
|
|
92
|
+
block: string;
|
|
93
|
+
/** The parsed claims (empty if the model proposed nothing valid). */
|
|
94
|
+
claims: Claim[];
|
|
95
|
+
/** True if the model was actually called; false if the send-guard withheld the request. */
|
|
96
|
+
sent: boolean;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Orchestrate a propose run. With `allowSendDiff` false (the default) this prints the prompt and returns
|
|
100
|
+
* an empty claims block without contacting any endpoint — review-first. With it true, it calls the
|
|
101
|
+
* OpenAI-compatible endpoint, parses the response, and renders the claims block.
|
|
102
|
+
*/
|
|
103
|
+
export declare function runPropose(opts: RunProposeOptions): Promise<RunProposeResult>;
|
package/dist/propose.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tribunal propose` — an LLM-*proposer* that turns a diff (and optional PR body) into a
|
|
3
|
+
* ```tribunal claims block of CANDIDATE claims, for the existing deterministic `tribunal check` to
|
|
4
|
+
* adjudicate.
|
|
5
|
+
*
|
|
6
|
+
* ── The Trust-Contract boundary (SPEC §1): a model may PROPOSE a claim to check; a model may never
|
|
7
|
+
* ADJUDICATE. This module honors that with two guarantees:
|
|
8
|
+
*
|
|
9
|
+
* 1. SEPARATION: nothing here imports or calls `runAnalyzers`, `runTribunal`, `exitCode`, or any
|
|
10
|
+
* verifier. `propose` only reads a diff and writes a claims block. The verdict path is untouched.
|
|
11
|
+
* 2. DEFENSE-IN-DEPTH: even if the model hallucinates a key or is prompt-injected, the *existing*
|
|
12
|
+
* `claimReconciliation` verifier map degrades every unrecognized key to 🟡 UNVERIFIED (never 🔴).
|
|
13
|
+
* So the worst a malicious LLM can do is emit noise that the deterministic checker labels yellow.
|
|
14
|
+
* It cannot flip a build red. We additionally constrain the prompt to the recognized set and
|
|
15
|
+
* validate the response, but those are convenience, not the safety boundary — the boundary is
|
|
16
|
+
* architectural.
|
|
17
|
+
*
|
|
18
|
+
* ── The send-guard: the full diff is source code, and sending it to an external LLM endpoint is an
|
|
19
|
+
* outward-facing publish. `propose` refuses to send unless the caller opts in with `allowSendDiff`.
|
|
20
|
+
* Without it, the prompt is printed for review and nothing leaves the machine.
|
|
21
|
+
*/
|
|
22
|
+
import { recognizedClaims } from './analyzers/claimReconciliation.js';
|
|
23
|
+
/** The closed claim vocabulary the model is allowed to propose from, deduped & sorted for the prompt. */
|
|
24
|
+
export const PROPOSABLE_CLAIMS = Array.from(new Set(recognizedClaims)).sort();
|
|
25
|
+
/**
|
|
26
|
+
* Build the { system, user } prompt for the proposer. Pure & deterministic — no I/O. The prompt:
|
|
27
|
+
* - tells the model the ONLY keys it may emit (the recognized set),
|
|
28
|
+
* - explains that any other output is useless (it degrades to UNVERIFIED in the checker),
|
|
29
|
+
* - asks for a JSON object so parsing is robust,
|
|
30
|
+
* - includes the full diff (per the chosen design) + any PR body as context.
|
|
31
|
+
*
|
|
32
|
+
* Note on the diff in the prompt: prompt-injection from diff content cannot escalate here, because the
|
|
33
|
+
* model's output is never trusted (see module doc + `validateAndNormalize`). It can at worst produce a
|
|
34
|
+
* claim key, which is either recognized (and then deterministically verified by `check`) or ignored.
|
|
35
|
+
*/
|
|
36
|
+
export function buildPrompt(diff, prBody, proposals = PROPOSABLE_CLAIMS) {
|
|
37
|
+
const list = proposals.map((k) => ` - ${k}`).join('\n');
|
|
38
|
+
const system = [
|
|
39
|
+
'You are a strict claim-proposer for Tribunal, a deterministic PR-check tool.',
|
|
40
|
+
'You PROPOSE candidate claims; you NEVER decide whether a claim holds. A separate deterministic',
|
|
41
|
+
'engine will verify each claim. Your job is only to suggest which claims a reviewer should ask the',
|
|
42
|
+
'engine to check, based on what the PR appears to do.',
|
|
43
|
+
'',
|
|
44
|
+
`You may ONLY propose claim keys from this closed set (emit nothing else):`,
|
|
45
|
+
list,
|
|
46
|
+
'',
|
|
47
|
+
'Rules:',
|
|
48
|
+
'- Output ONLY a JSON object: {"claims": ["<key>", ...], "rationale": {"<key>": "<short reason>"}}.',
|
|
49
|
+
'- Use each key at most once. Use the exact spelling from the set above.',
|
|
50
|
+
'- Propose ONLY claims plausibly relevant to the diff. An empty claims array is a valid answer.',
|
|
51
|
+
'- Any key not in the set above is useless: it will be ignored by the engine. Do not invent keys.',
|
|
52
|
+
'- Do not output markdown, fences, commentary, or any text outside the JSON object.',
|
|
53
|
+
].join('\n');
|
|
54
|
+
const userParts = [];
|
|
55
|
+
if (prBody && prBody.trim()) {
|
|
56
|
+
userParts.push('--- PR BODY (author description) ---', prBody.trim(), '');
|
|
57
|
+
}
|
|
58
|
+
userParts.push('--- DIFF (unified) ---', diff || '(empty diff)');
|
|
59
|
+
userParts.push('', 'Based on the above, output the JSON object of candidate claims. Remember: propose only from the', 'closed set, and only if plausibly relevant.');
|
|
60
|
+
const user = userParts.join('\n');
|
|
61
|
+
return { system, user };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Parse a model response into candidate claims. Pure & fault-tolerant:
|
|
65
|
+
* 1. Try JSON first (preferred). Accept {"claims": [...]}.
|
|
66
|
+
* 2. Fall back to scanning for claim-key lines (covers endpoints without JSON mode).
|
|
67
|
+
* Then VALIDATE every key against the recognized set — unknown keys are dropped, never trusted. This
|
|
68
|
+
* is defense-in-depth: even a malicious/injected response cannot smuggle through an unrecognized key,
|
|
69
|
+
* because the *checker* would label it UNVERIFIED anyway, and we drop it here for cleanliness.
|
|
70
|
+
*/
|
|
71
|
+
export function extractClaimsFromResponse(text, proposals = PROPOSABLE_CLAIMS) {
|
|
72
|
+
const allowed = new Set(proposals);
|
|
73
|
+
const keys = [];
|
|
74
|
+
// 1) JSON object.
|
|
75
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
76
|
+
if (jsonMatch) {
|
|
77
|
+
try {
|
|
78
|
+
const obj = JSON.parse(jsonMatch[0]);
|
|
79
|
+
if (Array.isArray(obj.claims)) {
|
|
80
|
+
for (const c of obj.claims) {
|
|
81
|
+
if (typeof c === 'string')
|
|
82
|
+
keys.push(c);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// fall through to line scan
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// 2) Fallback: tokenize the plain text on any run of chars that aren't [a-z0-9-], then keep only
|
|
91
|
+
// tokens that are exactly a recognized key. This handles endpoints without JSON mode that emit
|
|
92
|
+
// plain text like "added-test\nno-public-api-change". Only triggers if JSON parsing yielded
|
|
93
|
+
// nothing. Keys contain hyphens, so we tokenize on `[^a-z0-9-]+` (not whitespace→hyphen, which
|
|
94
|
+
// would merge keys into their neighbors and never match).
|
|
95
|
+
if (keys.length === 0) {
|
|
96
|
+
const tokenSet = new Set(text.toLowerCase().split(/[^a-z0-9-]+/).filter(Boolean));
|
|
97
|
+
for (const k of proposals) {
|
|
98
|
+
if (tokenSet.has(k))
|
|
99
|
+
keys.push(k);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Validate + dedupe, preserving first-seen order.
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
const claims = [];
|
|
105
|
+
for (const raw of keys) {
|
|
106
|
+
const key = raw.trim().toLowerCase().replace(/[_\s]+/g, '-');
|
|
107
|
+
if (!allowed.has(key) || seen.has(key))
|
|
108
|
+
continue;
|
|
109
|
+
seen.add(key);
|
|
110
|
+
claims.push({ key, arg: undefined, raw: key });
|
|
111
|
+
}
|
|
112
|
+
return claims;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Render claims as a ```tribunal fenced block. This is the exact format `tribunal check --pr-body`
|
|
116
|
+
* consumes, so `propose` → `check` is a clean round-trip:
|
|
117
|
+
*
|
|
118
|
+
* tribunal propose --diff pr.diff --allow-send-diff --out claims.md
|
|
119
|
+
* tribunal check --diff pr.diff --pr-body claims.md
|
|
120
|
+
*
|
|
121
|
+
* The empty case uses a `#` comment (which `parseClaims` ignores) rather than a sentinel word, so an
|
|
122
|
+
* empty block round-trips to zero claims instead of becoming a bogus claim.
|
|
123
|
+
*/
|
|
124
|
+
export function renderClaimsBlock(claims) {
|
|
125
|
+
if (claims.length === 0) {
|
|
126
|
+
return '```tribunal\n# (no claims proposed)\n```';
|
|
127
|
+
}
|
|
128
|
+
return '```tribunal\n' + claims.map((c) => c.key).join('\n') + '\n```';
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Orchestrate a propose run. With `allowSendDiff` false (the default) this prints the prompt and returns
|
|
132
|
+
* an empty claims block without contacting any endpoint — review-first. With it true, it calls the
|
|
133
|
+
* OpenAI-compatible endpoint, parses the response, and renders the claims block.
|
|
134
|
+
*/
|
|
135
|
+
export async function runPropose(opts) {
|
|
136
|
+
const notice = opts.notice ?? ((m) => process.stderr.write(`${m}\n`));
|
|
137
|
+
const { system, user } = buildPrompt(opts.diff, opts.prBody);
|
|
138
|
+
if (!opts.allowSendDiff) {
|
|
139
|
+
const lineCount = opts.diff ? opts.diff.split('\n').length : 0;
|
|
140
|
+
const byteCount = Buffer.byteLength(`${system}\n\n${user}`, 'utf8');
|
|
141
|
+
notice(`propose: send-guard active — NOT sending. Would send ${lineCount} diff lines / ${byteCount} bytes ` +
|
|
142
|
+
`to ${opts.provider.endpoint} (${opts.provider.model}).`);
|
|
143
|
+
notice('propose: review the prompt above; rerun with --allow-send-diff to actually send.');
|
|
144
|
+
// Print the full prompt to stdout for review (before the empty block).
|
|
145
|
+
process.stdout.write(`${system}\n\n${user}\n`);
|
|
146
|
+
return { block: renderClaimsBlock([]), claims: [], sent: false };
|
|
147
|
+
}
|
|
148
|
+
notice(`propose: sending diff to ${opts.provider.endpoint} (model: ${opts.provider.model}).`);
|
|
149
|
+
const fetchFn = opts.fetch ?? globalThis.fetch;
|
|
150
|
+
const res = await fetchFn(`${opts.provider.endpoint.replace(/\/$/, '')}/chat/completions`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
'content-type': 'application/json',
|
|
154
|
+
...(opts.provider.apiKey ? { authorization: `Bearer ${opts.provider.apiKey}` } : {}),
|
|
155
|
+
},
|
|
156
|
+
body: JSON.stringify({
|
|
157
|
+
model: opts.provider.model,
|
|
158
|
+
messages: [
|
|
159
|
+
{ role: 'system', content: system },
|
|
160
|
+
{ role: 'user', content: user },
|
|
161
|
+
],
|
|
162
|
+
temperature: 0,
|
|
163
|
+
response_format: { type: 'json_object' },
|
|
164
|
+
}),
|
|
165
|
+
});
|
|
166
|
+
if (!res.ok) {
|
|
167
|
+
const body = await res.text().catch(() => '<no body>');
|
|
168
|
+
throw new Error(`propose: provider returned HTTP ${res.status}: ${body.slice(0, 500)}`);
|
|
169
|
+
}
|
|
170
|
+
const data = JSON.parse(await res.text());
|
|
171
|
+
const content = data.choices?.[0]?.message?.content ?? '';
|
|
172
|
+
const claims = extractClaimsFromResponse(content);
|
|
173
|
+
return { block: renderClaimsBlock(claims), claims, sent: true };
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=propose.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"propose.js","sourceRoot":"","sources":["../src/propose.ts"],"names":[],"mappings":"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"}
|
package/dist/report/render.d.ts
CHANGED
|
@@ -8,3 +8,8 @@ export declare function exitCode(report: Report, hardFail: boolean): number;
|
|
|
8
8
|
export declare function renderJson(report: Report): string;
|
|
9
9
|
/** Markdown intended for a PR comment: blocking findings first, PASS collapsed to a count. */
|
|
10
10
|
export declare function renderMarkdown(report: Report, hardFail: boolean): string;
|
|
11
|
+
/**
|
|
12
|
+
* Render the report as a SARIF Log v2.1.0 JSON string. Each analyzer becomes a `rule`; each
|
|
13
|
+
* CONTRADICTED/UNVERIFIED finding becomes a `result` (PASS findings are omitted).
|
|
14
|
+
*/
|
|
15
|
+
export declare function renderSarif(report: Report): string;
|