@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
package/dist/cli.js
CHANGED
|
@@ -1,30 +1,65 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { runTribunal } from './index.js';
|
|
2
|
+
import { runTribunal, analyzers as defaultAnalyzers } from './index.js';
|
|
3
3
|
import { exitCode, renderJson, renderMarkdown } from './report/render.js';
|
|
4
4
|
import { parseClaims } from './claims.js';
|
|
5
|
-
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { runPropose } from './propose.js';
|
|
7
|
+
import { renderSarif } from './report/render.js';
|
|
8
|
+
import { setSkipGenerated, setExtraGeneratedPaths } from './analyzers/riskyDiffNoTest.js';
|
|
9
|
+
import { loadConfig, setKnownAnalyzerIds } from './config/loadConfig.js';
|
|
10
|
+
// Register the known analyzer ids so loadConfig can validate `analyzers:` keys (fail-loud on typos).
|
|
11
|
+
setKnownAnalyzerIds(defaultAnalyzers.map((a) => a.id));
|
|
6
12
|
const HELP = `tribunal — a deterministic, no-LLM CI gate for agent-authored PRs
|
|
7
13
|
|
|
8
14
|
Usage:
|
|
9
|
-
tribunal check [options]
|
|
15
|
+
tribunal check [options] Deterministically verify a PR (default).
|
|
16
|
+
tribunal propose [options] Ask an LLM to PROPOSE claims; never adjudicates.
|
|
10
17
|
|
|
11
|
-
|
|
18
|
+
check — options:
|
|
12
19
|
--base <ref> Base ref for a range diff (e.g. the PR base branch).
|
|
13
20
|
--head <ref> Head ref for a range diff. Defaults to working tree vs HEAD.
|
|
14
21
|
--diff <file> Read a unified diff from a file instead of invoking git.
|
|
15
22
|
--claims <file> Read machine-readable claims from a file (whole file, or a tribunal fenced block).
|
|
16
23
|
--pr-body <file> Read claims ONLY from a \`\`\`tribunal fenced block in a PR-body file.
|
|
17
24
|
--cwd <dir> Repo root to run in (default: current directory).
|
|
18
|
-
--format <fmt> Output format: md (default) or
|
|
25
|
+
--format <fmt> Output format: md (default), json, or sarif (upload via github/codeql-action/upload-sarif).
|
|
19
26
|
--hard-fail Exit non-zero when there is at least one CONTRADICTED finding.
|
|
20
27
|
Off by default (report-only). Gates ONLY on CONTRADICTED.
|
|
21
|
-
-
|
|
28
|
+
--no-skip-generated Don't skip generated/build-output paths in risky-diff-no-test
|
|
29
|
+
(dist/, *.min.js, …). Default skips them; this re-enables flagging them.
|
|
30
|
+
--config <file> Path to tribunal.yml (default: <repo-root>/tribunal.yml). Configures per-analyzer
|
|
31
|
+
enable/disable and extra generated-path patterns. Absent = all defaults.
|
|
32
|
+
|
|
33
|
+
propose — options:
|
|
34
|
+
--diff <file> Diff to propose claims for (required for propose).
|
|
35
|
+
--pr-body <file> Optional PR body to give the model extra context.
|
|
36
|
+
--endpoint <url> OpenAI-compatible chat-completions base URL (env: TRIBUNAL_ENDPOINT).
|
|
37
|
+
--model <name> Model name (env: TRIBUNAL_MODEL).
|
|
38
|
+
--api-key <key> Bearer token (env: TRIBUNAL_API_KEY).
|
|
39
|
+
--allow-send-diff Actually send the diff to the endpoint. WITHOUT this flag, propose prints the
|
|
40
|
+
prompt and sends NOTHING (the diff is source code — review before publishing).
|
|
41
|
+
Also set via TRIBUNAL_ALLOW_SEND_DIFF=1.
|
|
42
|
+
--out <file> Write the claims block to a file (default: stdout).
|
|
43
|
+
--cwd <dir> Repo root (default: current directory).
|
|
44
|
+
|
|
45
|
+
The propose → check loop:
|
|
46
|
+
tribunal propose --diff pr.diff --allow-send-diff --out claims.md
|
|
47
|
+
tribunal check --diff pr.diff --pr-body claims.md
|
|
22
48
|
|
|
23
49
|
Tribunal gates only on CONTRADICTED (a syntactic certainty). UNVERIFIED never blocks,
|
|
24
|
-
and no LLM is in the verification path.
|
|
50
|
+
and no LLM is in the verification path. propose only suggests claims for check to verify.
|
|
51
|
+
|
|
52
|
+
-h, --help Show this help.
|
|
25
53
|
`;
|
|
26
54
|
function parseArgs(argv) {
|
|
27
|
-
const opts = {
|
|
55
|
+
const opts = {
|
|
56
|
+
command: '',
|
|
57
|
+
cwd: process.cwd(),
|
|
58
|
+
format: 'md',
|
|
59
|
+
hardFail: false,
|
|
60
|
+
allowSendDiff: false,
|
|
61
|
+
noSkipGenerated: false,
|
|
62
|
+
};
|
|
28
63
|
const rest = [...argv];
|
|
29
64
|
while (rest.length) {
|
|
30
65
|
const a = rest.shift();
|
|
@@ -46,22 +81,46 @@ function parseArgs(argv) {
|
|
|
46
81
|
opts.claimsFile = rest.shift();
|
|
47
82
|
break;
|
|
48
83
|
case '--pr-body':
|
|
84
|
+
// In `propose`, --pr-body is context for the model; in `check`, it's a fenced claims source.
|
|
85
|
+
// We resolve which meaning applies at dispatch time; stash the path on both fields.
|
|
49
86
|
opts.prBodyFile = rest.shift();
|
|
87
|
+
opts.proposePrBodyFile = opts.prBodyFile;
|
|
88
|
+
break;
|
|
89
|
+
case '--endpoint':
|
|
90
|
+
opts.endpoint = rest.shift();
|
|
91
|
+
break;
|
|
92
|
+
case '--model':
|
|
93
|
+
opts.model = rest.shift();
|
|
94
|
+
break;
|
|
95
|
+
case '--api-key':
|
|
96
|
+
opts.apiKey = rest.shift();
|
|
97
|
+
break;
|
|
98
|
+
case '--allow-send-diff':
|
|
99
|
+
opts.allowSendDiff = true;
|
|
100
|
+
break;
|
|
101
|
+
case '--out':
|
|
102
|
+
opts.outFile = rest.shift();
|
|
50
103
|
break;
|
|
51
104
|
case '--cwd':
|
|
52
105
|
opts.cwd = rest.shift() ?? opts.cwd;
|
|
53
106
|
break;
|
|
54
107
|
case '--format': {
|
|
55
108
|
const f = rest.shift();
|
|
56
|
-
if (f === 'md' || f === 'json')
|
|
109
|
+
if (f === 'md' || f === 'json' || f === 'sarif')
|
|
57
110
|
opts.format = f;
|
|
58
111
|
else
|
|
59
|
-
throw new Error(`Unknown format: ${f} (expected md or
|
|
112
|
+
throw new Error(`Unknown format: ${f} (expected md, json, or sarif)`);
|
|
60
113
|
break;
|
|
61
114
|
}
|
|
62
115
|
case '--hard-fail':
|
|
63
116
|
opts.hardFail = true;
|
|
64
117
|
break;
|
|
118
|
+
case '--no-skip-generated':
|
|
119
|
+
opts.noSkipGenerated = true;
|
|
120
|
+
break;
|
|
121
|
+
case '--config':
|
|
122
|
+
opts.configFile = rest.shift();
|
|
123
|
+
break;
|
|
65
124
|
default:
|
|
66
125
|
if (!a.startsWith('-') && !opts.command)
|
|
67
126
|
opts.command = a;
|
|
@@ -71,21 +130,70 @@ function parseArgs(argv) {
|
|
|
71
130
|
}
|
|
72
131
|
return opts;
|
|
73
132
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
133
|
+
/** Resolve the propose provider config from flags, then env, failing loudly if incomplete. */
|
|
134
|
+
function resolveProvider(opts) {
|
|
135
|
+
const endpoint = opts.endpoint ?? process.env.TRIBUNAL_ENDPOINT;
|
|
136
|
+
const model = opts.model ?? process.env.TRIBUNAL_MODEL;
|
|
137
|
+
const apiKey = opts.apiKey ?? process.env.TRIBUNAL_API_KEY;
|
|
138
|
+
if (!endpoint) {
|
|
139
|
+
throw new Error('propose: no endpoint. Pass --endpoint <url> or set TRIBUNAL_ENDPOINT ' +
|
|
140
|
+
'(an OpenAI-compatible chat-completions base URL).');
|
|
78
141
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
process.exit(2);
|
|
142
|
+
if (!model) {
|
|
143
|
+
throw new Error('propose: no model. Pass --model <name> or set TRIBUNAL_MODEL.');
|
|
82
144
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
145
|
+
return { endpoint, model, apiKey };
|
|
146
|
+
}
|
|
147
|
+
async function runProposeCmd(opts) {
|
|
148
|
+
if (!opts.diffFile) {
|
|
149
|
+
throw new Error('propose: --diff <file> is required (the diff to propose claims for).');
|
|
86
150
|
}
|
|
87
|
-
|
|
88
|
-
|
|
151
|
+
const diff = readFileSync(opts.diffFile, 'utf8');
|
|
152
|
+
const prBody = opts.proposePrBodyFile ? readFileSync(opts.proposePrBodyFile, 'utf8') : undefined;
|
|
153
|
+
const allowSendDiff = opts.allowSendDiff || process.env.TRIBUNAL_ALLOW_SEND_DIFF === '1' ||
|
|
154
|
+
process.env.TRIBUNAL_ALLOW_SEND_DIFF === 'true';
|
|
155
|
+
const provider = resolveProvider(opts);
|
|
156
|
+
const result = await runPropose({ diff, prBody, provider, allowSendDiff });
|
|
157
|
+
if (opts.outFile) {
|
|
158
|
+
writeFileSync(opts.outFile, `${result.block}\n`);
|
|
159
|
+
process.stderr.write(result.sent
|
|
160
|
+
? `propose: wrote ${result.claims.length} candidate claim(s) to ${opts.outFile}\n`
|
|
161
|
+
: `propose: send-guard withheld the request; wrote an empty block to ${opts.outFile}\n`);
|
|
162
|
+
}
|
|
163
|
+
else if (result.sent) {
|
|
164
|
+
// When the prompt was already printed by the send-guard path, avoid duplicating it; only print the
|
|
165
|
+
// block when we actually sent and got a response.
|
|
166
|
+
process.stdout.write(`${result.block}\n`);
|
|
167
|
+
}
|
|
168
|
+
process.exit(0);
|
|
169
|
+
}
|
|
170
|
+
function runCheckCmd(opts) {
|
|
171
|
+
// riskyDiffNoTest skips generated/build-output paths by default. Disable via --no-skip-generated or
|
|
172
|
+
// TRIBUNAL_NO_SKIP_GENERATED=1 (an opinionated default must never silently suppress a wanted file).
|
|
173
|
+
if (opts.noSkipGenerated ||
|
|
174
|
+
process.env.TRIBUNAL_NO_SKIP_GENERATED === '1' ||
|
|
175
|
+
process.env.TRIBUNAL_NO_SKIP_GENERATED === 'true') {
|
|
176
|
+
setSkipGenerated(false);
|
|
177
|
+
}
|
|
178
|
+
// Load tribunal.yml (auto-discovered at repo root, or --config / TRIBUNAL_CONFIG). Absent = all
|
|
179
|
+
// defaults, no behavior change. Present-but-malformed fails loud. Applied: extra generated-paths are
|
|
180
|
+
// appended to the built-ins (extend-defaults); analyzers: false disables that analyzer.
|
|
181
|
+
let analyzerOverride;
|
|
182
|
+
try {
|
|
183
|
+
const config = loadConfig(opts.cwd, opts.configFile);
|
|
184
|
+
if (config) {
|
|
185
|
+
if (config.generatedPaths && config.generatedPaths.length) {
|
|
186
|
+
setExtraGeneratedPaths(config.generatedPaths);
|
|
187
|
+
}
|
|
188
|
+
if (config.analyzers) {
|
|
189
|
+
const disabled = Object.entries(config.analyzers).filter(([, v]) => v === false).map(([k]) => k);
|
|
190
|
+
if (disabled.length)
|
|
191
|
+
analyzerOverride = defaultAnalyzers.filter((a) => !disabled.includes(a.id));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
process.stderr.write(`tribunal: ${err.message}\n`);
|
|
89
197
|
process.exit(2);
|
|
90
198
|
}
|
|
91
199
|
let claims;
|
|
@@ -102,15 +210,50 @@ function main() {
|
|
|
102
210
|
};
|
|
103
211
|
let report;
|
|
104
212
|
try {
|
|
105
|
-
report = runTribunal(src);
|
|
213
|
+
report = runTribunal(src, analyzerOverride ? { analyzers: analyzerOverride } : {});
|
|
106
214
|
}
|
|
107
215
|
catch (err) {
|
|
108
216
|
process.stderr.write(`tribunal: ${err.message}\n`);
|
|
109
217
|
process.exit(2);
|
|
110
218
|
}
|
|
111
|
-
|
|
219
|
+
let out;
|
|
220
|
+
if (opts.format === 'json')
|
|
221
|
+
out = renderJson(report);
|
|
222
|
+
else if (opts.format === 'sarif')
|
|
223
|
+
out = renderSarif(report);
|
|
224
|
+
else
|
|
225
|
+
out = renderMarkdown(report, opts.hardFail);
|
|
112
226
|
process.stdout.write(`${out}\n`);
|
|
113
227
|
process.exit(exitCode(report, opts.hardFail));
|
|
114
228
|
}
|
|
229
|
+
async function main() {
|
|
230
|
+
let opts;
|
|
231
|
+
try {
|
|
232
|
+
opts = parseArgs(process.argv.slice(2));
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
process.stderr.write(`${err.message}\n\n${HELP}`);
|
|
236
|
+
process.exit(2);
|
|
237
|
+
}
|
|
238
|
+
if (opts.command === 'help' || opts.command === '') {
|
|
239
|
+
process.stdout.write(HELP);
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
242
|
+
if (opts.command === 'propose') {
|
|
243
|
+
try {
|
|
244
|
+
await runProposeCmd(opts);
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
process.stderr.write(`tribunal: ${err.message}\n`);
|
|
248
|
+
process.exit(2);
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (opts.command !== 'check') {
|
|
253
|
+
process.stderr.write(`Unknown command: ${opts.command}\n\n${HELP}`);
|
|
254
|
+
process.exit(2);
|
|
255
|
+
}
|
|
256
|
+
runCheckCmd(opts);
|
|
257
|
+
}
|
|
115
258
|
main();
|
|
116
259
|
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TribunalConfig } from './types.js';
|
|
2
|
+
export declare const CONFIG_FILENAME = "tribunal.yml";
|
|
3
|
+
/**
|
|
4
|
+
* Register the known analyzer ids (once, at CLI/program startup). loadConfig validates `analyzers:` keys
|
|
5
|
+
* against this set so a typo fails loudly instead of silently no-op'ing.
|
|
6
|
+
*/
|
|
7
|
+
export declare function setKnownAnalyzerIds(ids: readonly string[]): void;
|
|
8
|
+
/**
|
|
9
|
+
* Load + validate a TribunalConfig.
|
|
10
|
+
*
|
|
11
|
+
* Resolution order for the file path:
|
|
12
|
+
* 1. an explicit `configPath` argument (the `--config` flag value)
|
|
13
|
+
* 2. the `TRIBUNAL_CONFIG` env var
|
|
14
|
+
* 3. `<repoRoot>/tribunal.yml` (auto-discovery)
|
|
15
|
+
*
|
|
16
|
+
* Returns `null` when no file is present (≡ all defaults). Throws on a present-but-malformed file or on
|
|
17
|
+
* an unknown analyzer id (fail-loud, never silently degrade).
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadConfig(repoRoot: string, configPath?: string): TribunalConfig | null;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { parseYamlSubset } from './parseYaml.js';
|
|
4
|
+
export const CONFIG_FILENAME = 'tribunal.yml';
|
|
5
|
+
/** The set of analyzer ids accepted under `analyzers:` — injected so loadConfig has no analyzer import cycle. */
|
|
6
|
+
let knownAnalyzerIds = [];
|
|
7
|
+
/**
|
|
8
|
+
* Register the known analyzer ids (once, at CLI/program startup). loadConfig validates `analyzers:` keys
|
|
9
|
+
* against this set so a typo fails loudly instead of silently no-op'ing.
|
|
10
|
+
*/
|
|
11
|
+
export function setKnownAnalyzerIds(ids) {
|
|
12
|
+
knownAnalyzerIds = ids;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Load + validate a TribunalConfig.
|
|
16
|
+
*
|
|
17
|
+
* Resolution order for the file path:
|
|
18
|
+
* 1. an explicit `configPath` argument (the `--config` flag value)
|
|
19
|
+
* 2. the `TRIBUNAL_CONFIG` env var
|
|
20
|
+
* 3. `<repoRoot>/tribunal.yml` (auto-discovery)
|
|
21
|
+
*
|
|
22
|
+
* Returns `null` when no file is present (≡ all defaults). Throws on a present-but-malformed file or on
|
|
23
|
+
* an unknown analyzer id (fail-loud, never silently degrade).
|
|
24
|
+
*/
|
|
25
|
+
export function loadConfig(repoRoot, configPath) {
|
|
26
|
+
const explicit = configPath ?? process.env.TRIBUNAL_CONFIG;
|
|
27
|
+
const file = explicit ? resolve(explicit) : join(repoRoot, CONFIG_FILENAME);
|
|
28
|
+
if (!existsSync(file))
|
|
29
|
+
return null;
|
|
30
|
+
let raw;
|
|
31
|
+
try {
|
|
32
|
+
raw = readFileSync(file, 'utf8');
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
throw new Error(`tribunal.yml: could not read ${file}: ${err.message}`);
|
|
36
|
+
}
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = parseYamlSubset(raw);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
throw new Error(`${err.message} (in ${file})`);
|
|
43
|
+
}
|
|
44
|
+
const config = {};
|
|
45
|
+
// analyzers: map of id → boolean, validated against the known registry.
|
|
46
|
+
if ('analyzers' in parsed && parsed.analyzers != null) {
|
|
47
|
+
const a = parsed.analyzers;
|
|
48
|
+
if (typeof a !== 'object' || Array.isArray(a)) {
|
|
49
|
+
throw new Error(`tribunal.yml: 'analyzers' must be a map of id: true|false (in ${file})`);
|
|
50
|
+
}
|
|
51
|
+
const known = new Set(knownAnalyzerIds);
|
|
52
|
+
if (known.size === 0) {
|
|
53
|
+
// Defensive: caller forgot to register ids. Allow through without validation rather than block.
|
|
54
|
+
}
|
|
55
|
+
const out = {};
|
|
56
|
+
for (const [k, v] of Object.entries(a)) {
|
|
57
|
+
if (known.size > 0 && !known.has(k)) {
|
|
58
|
+
throw new Error(`tribunal.yml: unknown analyzer '${k}'. Known: ${[...known].sort().join(', ')} (in ${file})`);
|
|
59
|
+
}
|
|
60
|
+
if (typeof v !== 'boolean') {
|
|
61
|
+
throw new Error(`tribunal.yml: analyzers.${k} must be true|false, got ${JSON.stringify(v)} (in ${file})`);
|
|
62
|
+
}
|
|
63
|
+
out[k] = v;
|
|
64
|
+
}
|
|
65
|
+
config.analyzers = out;
|
|
66
|
+
}
|
|
67
|
+
// generated-paths: list of strings, appended to built-ins at match time.
|
|
68
|
+
if ('generated-paths' in parsed && parsed['generated-paths'] != null) {
|
|
69
|
+
const gp = parsed['generated-paths'];
|
|
70
|
+
if (!Array.isArray(gp)) {
|
|
71
|
+
throw new Error(`tribunal.yml: 'generated-paths' must be a list (in ${file})`);
|
|
72
|
+
}
|
|
73
|
+
config.generatedPaths = gp.map((item, idx) => {
|
|
74
|
+
if (typeof item !== 'string') {
|
|
75
|
+
throw new Error(`tribunal.yml: generated-paths[${idx}] must be a string (in ${file})`);
|
|
76
|
+
}
|
|
77
|
+
return item;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// Reject unknown top-level keys (fail-loud — typos shouldn't silently no-op).
|
|
81
|
+
const knownKeys = new Set(['analyzers', 'generated-paths']);
|
|
82
|
+
for (const k of Object.keys(parsed)) {
|
|
83
|
+
if (!knownKeys.has(k)) {
|
|
84
|
+
throw new Error(`tribunal.yml: unknown key '${k}'. Supported: ${[...knownKeys].join(', ')} (in ${file})`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return config;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=loadConfig.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loadConfig.js","sourceRoot":"","sources":["../../src/config/loadConfig.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny, zero-dependency parser for the narrow YAML *subset* that tribunal.yml uses.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately NOT a general YAML parser — it covers exactly what the config schema needs and
|
|
5
|
+
* rejects anything else with a clear error, so a malformed config never silently degrades to defaults:
|
|
6
|
+
*
|
|
7
|
+
* - top-level `key: value` (value = scalar: string | boolean | number)
|
|
8
|
+
* - inline list value: `key: [a, b, "c"]`
|
|
9
|
+
* - block list: a `key:` line followed by ` - item` lines (two-space indent)
|
|
10
|
+
* - one level of nesting: a `key:` line followed by ` child: value` lines (the `analyzers:` map)
|
|
11
|
+
* - `#` line comments and trailing comments, blank lines
|
|
12
|
+
*
|
|
13
|
+
* No anchors, no multiline strings, no flow mappings, no deeper nesting. If the schema grows, either
|
|
14
|
+
* extend this or swap in a real parser (js-yaml) — but for now keeping zero runtime deps is a project
|
|
15
|
+
* value, and this subset is fully covered by unit tests.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseYamlSubset(text: string): Record<string, unknown>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny, zero-dependency parser for the narrow YAML *subset* that tribunal.yml uses.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately NOT a general YAML parser — it covers exactly what the config schema needs and
|
|
5
|
+
* rejects anything else with a clear error, so a malformed config never silently degrades to defaults:
|
|
6
|
+
*
|
|
7
|
+
* - top-level `key: value` (value = scalar: string | boolean | number)
|
|
8
|
+
* - inline list value: `key: [a, b, "c"]`
|
|
9
|
+
* - block list: a `key:` line followed by ` - item` lines (two-space indent)
|
|
10
|
+
* - one level of nesting: a `key:` line followed by ` child: value` lines (the `analyzers:` map)
|
|
11
|
+
* - `#` line comments and trailing comments, blank lines
|
|
12
|
+
*
|
|
13
|
+
* No anchors, no multiline strings, no flow mappings, no deeper nesting. If the schema grows, either
|
|
14
|
+
* extend this or swap in a real parser (js-yaml) — but for now keeping zero runtime deps is a project
|
|
15
|
+
* value, and this subset is fully covered by unit tests.
|
|
16
|
+
*/
|
|
17
|
+
/** Parse a scalar token (after stripping comments) into string | boolean | number. */
|
|
18
|
+
function parseScalar(raw) {
|
|
19
|
+
const s = raw.trim();
|
|
20
|
+
if (s === '')
|
|
21
|
+
return '';
|
|
22
|
+
// strip surrounding quotes (single or double) → always a string
|
|
23
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
24
|
+
return s.slice(1, -1);
|
|
25
|
+
}
|
|
26
|
+
if (s === 'true' || s === 'True' || s === 'TRUE')
|
|
27
|
+
return true;
|
|
28
|
+
if (s === 'false' || s === 'False' || s === 'FALSE')
|
|
29
|
+
return false;
|
|
30
|
+
// integer/float only — reject hex/leading-zero quirks by deferring to Number
|
|
31
|
+
if (/^-?\d+$/.test(s))
|
|
32
|
+
return parseInt(s, 10);
|
|
33
|
+
if (/^-?\d+\.\d+$/.test(s))
|
|
34
|
+
return parseFloat(s);
|
|
35
|
+
return s; // bare string
|
|
36
|
+
}
|
|
37
|
+
/** Parse an inline flow list `[a, b, "c"]` into a (string|number|boolean)[]. */
|
|
38
|
+
function parseInlineList(raw) {
|
|
39
|
+
const inner = raw.trim();
|
|
40
|
+
if (!inner.startsWith('[') || !inner.endsWith(']')) {
|
|
41
|
+
throw new Error(`Expected an inline list '[...]', got: ${raw}`);
|
|
42
|
+
}
|
|
43
|
+
const body = inner.slice(1, -1).trim();
|
|
44
|
+
if (body === '')
|
|
45
|
+
return [];
|
|
46
|
+
return body.split(',').map((part) => parseScalar(part));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Strip a trailing `# comment` from a value-bearing line. A `#` only counts as a comment start when
|
|
50
|
+
* preceded by whitespace or at line start (so `#fff` in a value is preserved, ` # note` is stripped).
|
|
51
|
+
*/
|
|
52
|
+
function stripTrailingComment(line) {
|
|
53
|
+
// naive but sufficient for this subset: a `#` that has a space before it (or is at col 0) is a comment.
|
|
54
|
+
const hash = line.search(/\s#|\s$|^#/);
|
|
55
|
+
if (hash === -1)
|
|
56
|
+
return line;
|
|
57
|
+
if (line[hash] === '#')
|
|
58
|
+
return ''; // whole line is a comment
|
|
59
|
+
return line.slice(0, hash);
|
|
60
|
+
}
|
|
61
|
+
export function parseYamlSubset(text) {
|
|
62
|
+
const result = {};
|
|
63
|
+
const lines = text.split('\n');
|
|
64
|
+
// State for the current top-level key we're filling (a list or nested map).
|
|
65
|
+
let listKey = null;
|
|
66
|
+
let mapKey = null;
|
|
67
|
+
const finishBlock = () => {
|
|
68
|
+
listKey = null;
|
|
69
|
+
mapKey = null;
|
|
70
|
+
};
|
|
71
|
+
for (let i = 0; i < lines.length; i++) {
|
|
72
|
+
const original = lines[i];
|
|
73
|
+
const stripped = stripTrailingComment(original);
|
|
74
|
+
// Skip blank / full-comment lines.
|
|
75
|
+
if (stripped.trim() === '' || stripped.trim().startsWith('#'))
|
|
76
|
+
continue;
|
|
77
|
+
const indent = stripped.length - stripped.trimStart().length;
|
|
78
|
+
// Indented block item under the current list key: ` - value`.
|
|
79
|
+
if (listKey && /^-\s+/.test(stripped.trim())) {
|
|
80
|
+
const item = parseScalar(stripped.trim().replace(/^-\s+/, ''));
|
|
81
|
+
result[listKey].push(item);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Indented child under the current map key: ` child: value`.
|
|
85
|
+
if (mapKey && indent >= 2 && /^[\w-]+:\s*/.test(stripped.trim())) {
|
|
86
|
+
const m = stripped.trim().match(/^([\w-]+):\s*(.*)$/);
|
|
87
|
+
if (!m)
|
|
88
|
+
throw new Error(`tribunal.yml:${i + 1}: malformed map entry: ${original.trim()}`);
|
|
89
|
+
const [, childKey, childValRaw] = m;
|
|
90
|
+
if (childValRaw.trim() === '') {
|
|
91
|
+
throw new Error(`tribunal.yml:${i + 1}: nested map under '${mapKey}.${childKey}' is not supported`);
|
|
92
|
+
}
|
|
93
|
+
result[mapKey][childKey] = parseScalar(childValRaw);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Otherwise: a top-level `key: value` or `key:` (opening a block). Must be at indent 0.
|
|
97
|
+
if (indent !== 0) {
|
|
98
|
+
throw new Error(`tribunal.yml:${i + 1}: unexpected indentation (no open block): ${original.trim()}`);
|
|
99
|
+
}
|
|
100
|
+
finishBlock();
|
|
101
|
+
const m = stripped.trim().match(/^([\w-]+):\s*(.*)$/);
|
|
102
|
+
if (!m)
|
|
103
|
+
throw new Error(`tribunal.yml:${i + 1}: expected 'key: value', got: ${original.trim()}`);
|
|
104
|
+
const [, key, valRaw] = m;
|
|
105
|
+
const val = valRaw.trim();
|
|
106
|
+
if (val === '') {
|
|
107
|
+
// Opens a block — we don't yet know if it's a list or a map; decide on the next non-blank line.
|
|
108
|
+
// For our schema, `analyzers:` is a map and `generated-paths:` is a list. Peek ahead.
|
|
109
|
+
const nextNonBlank = lines
|
|
110
|
+
.slice(i + 1)
|
|
111
|
+
.find((l) => stripTrailingComment(l).trim() !== '' && !stripTrailingComment(l).trim().startsWith('#'));
|
|
112
|
+
if (nextNonBlank && /^-\s+/.test(nextNonBlank.trim())) {
|
|
113
|
+
result[key] = [];
|
|
114
|
+
listKey = key;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
result[key] = {};
|
|
118
|
+
mapKey = key;
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (val.startsWith('[')) {
|
|
123
|
+
result[key] = parseInlineList(val);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
result[key] = parseScalar(val);
|
|
127
|
+
}
|
|
128
|
+
finishBlock();
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=parseYaml.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseYaml.js","sourceRoot":"","sources":["../../src/config/parseYaml.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tribunal.yml config shape. All fields optional — an absent file means "all defaults" and is
|
|
3
|
+
* indistinguishable from no config (config is purely additive).
|
|
4
|
+
*/
|
|
5
|
+
export interface TribunalConfig {
|
|
6
|
+
/**
|
|
7
|
+
* Per-analyzer enable/disable, keyed by analyzer id. Default: every analyzer enabled. Unknown ids are
|
|
8
|
+
* rejected at load time (typo guard), so a misspelled key fails loudly rather than silently no-op'ing.
|
|
9
|
+
*/
|
|
10
|
+
analyzers?: Record<string, boolean>;
|
|
11
|
+
/**
|
|
12
|
+
* EXTRA generated/build-output paths, appended to the built-in defaults. The built-ins (`dist/`,
|
|
13
|
+
* `action-dist/`, …) are always present — config can only ADD coverage, never drop a safety net.
|
|
14
|
+
* See src/paths.ts for the defaults and src/config/loadConfig.ts for merge semantics.
|
|
15
|
+
*/
|
|
16
|
+
generatedPaths?: string[];
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":""}
|
|
@@ -5,5 +5,9 @@ import type { ChangedFile } from '../types.js';
|
|
|
5
5
|
*
|
|
6
6
|
* We deliberately track only new-file line numbers: analyzers reason about the post-change tree, so a
|
|
7
7
|
* "touched" node is one whose new-file line range overlaps `addedLines`.
|
|
8
|
+
*
|
|
9
|
+
* Both git-style diffs (`diff --git`) and plain unified diffs (no `diff --git` header, e.g. `diff -u` or
|
|
10
|
+
* a `git format-patch` body) are supported. A plain diff opens a file record on its first `---`/`+++`
|
|
11
|
+
* header so a hand-written or piped patch is never silently dropped to `[]`.
|
|
8
12
|
*/
|
|
9
13
|
export declare function parseUnifiedDiff(diff: string): ChangedFile[];
|