@aligndottech/cli 0.66.1 → 0.67.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/commands/check.d.ts +19 -1
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +132 -22
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/import/docs.js +2 -2
- package/dist/commands/import/docs.js.map +1 -1
- package/dist/commands/registry.d.ts.map +1 -1
- package/dist/commands/registry.js +3 -0
- package/dist/commands/registry.js.map +1 -1
- package/dist/commands/rule.d.ts +28 -0
- package/dist/commands/rule.d.ts.map +1 -0
- package/dist/commands/rule.js +100 -0
- package/dist/commands/rule.js.map +1 -0
- package/dist/lib/co-change-check.d.ts +33 -0
- package/dist/lib/co-change-check.d.ts.map +1 -0
- package/dist/lib/co-change-check.js +95 -0
- package/dist/lib/co-change-check.js.map +1 -0
- package/dist/lib/co-change-rule.d.ts +11 -0
- package/dist/lib/co-change-rule.d.ts.map +1 -0
- package/dist/lib/co-change-rule.js +2 -0
- package/dist/lib/co-change-rule.js.map +1 -0
- package/dist/lib/co-change-store.d.ts +25 -0
- package/dist/lib/co-change-store.d.ts.map +1 -0
- package/dist/lib/co-change-store.js +142 -0
- package/dist/lib/co-change-store.js.map +1 -0
- package/dist/lib/co-change.d.ts +54 -0
- package/dist/lib/co-change.d.ts.map +1 -0
- package/dist/lib/co-change.js +120 -0
- package/dist/lib/co-change.js.map +1 -0
- package/dist/lib/commit-shape.d.ts +18 -0
- package/dist/lib/commit-shape.d.ts.map +1 -1
- package/dist/lib/commit-shape.js +40 -2
- package/dist/lib/commit-shape.js.map +1 -1
- package/dist/lib/fetchers/docs-conventions.d.ts +29 -0
- package/dist/lib/fetchers/docs-conventions.d.ts.map +1 -0
- package/dist/lib/fetchers/docs-conventions.js +95 -0
- package/dist/lib/fetchers/docs-conventions.js.map +1 -0
- package/dist/lib/fetchers/docs.d.ts +6 -0
- package/dist/lib/fetchers/docs.d.ts.map +1 -1
- package/dist/lib/fetchers/docs.js +79 -11
- package/dist/lib/fetchers/docs.js.map +1 -1
- package/dist/lib/git.d.ts.map +1 -1
- package/dist/lib/git.js +39 -10
- package/dist/lib/git.js.map +1 -1
- package/dist/lib/launch/pi-state.d.ts.map +1 -1
- package/dist/lib/launch/pi-state.js +9 -3
- package/dist/lib/launch/pi-state.js.map +1 -1
- package/dist/lib/local-db-cochange.d.ts +17 -0
- package/dist/lib/local-db-cochange.d.ts.map +1 -0
- package/dist/lib/local-db-cochange.js +23 -0
- package/dist/lib/local-db-cochange.js.map +1 -0
- package/dist/lib/local-db-migrate.d.ts.map +1 -1
- package/dist/lib/local-db-migrate.js +19 -0
- package/dist/lib/local-db-migrate.js.map +1 -1
- package/dist/lib/local-db.js +1 -1
- package/dist/lib/local-db.js.map +1 -1
- package/dist/lib/local-gateway-client.d.ts.map +1 -1
- package/dist/lib/local-gateway-client.js +20 -0
- package/dist/lib/local-gateway-client.js.map +1 -1
- package/dist/lib/local-ingest.d.ts +2 -0
- package/dist/lib/local-ingest.d.ts.map +1 -1
- package/dist/lib/local-ingest.js.map +1 -1
- package/dist/lib/mcp-instructions.shared.d.ts +1 -1
- package/dist/lib/mcp-instructions.shared.d.ts.map +1 -1
- package/dist/lib/mcp-instructions.shared.js +3 -3
- package/dist/lib/mcp-instructions.shared.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { addCoChangeRule, listCoChangeRules, removeCoChangeRule } from '../lib/co-change-store.js';
|
|
3
|
+
import { parseCoChangeRule } from '../lib/co-change.js';
|
|
4
|
+
import { currentRepoIdentity } from '../lib/repo-identity.js';
|
|
5
|
+
import { localGraphPath } from '../lib/sync/real-env.js';
|
|
6
|
+
const NO_GRAPH = 'There is no local graph yet. Run `align local start` (or `align setup --local`) first.';
|
|
7
|
+
export async function runRuleAdd(title, opts, deps) {
|
|
8
|
+
if (title === undefined || title.trim() === '') {
|
|
9
|
+
deps.err('Give the rule a title: it is the decision a flag will cite. Put the title first, before the flags, because --when and --require take every word after them: align rule add "<title>" --when <glob...> --require <path...>');
|
|
10
|
+
return 2;
|
|
11
|
+
}
|
|
12
|
+
if (!opts.when || opts.when.length === 0) {
|
|
13
|
+
deps.err('--when needs at least one path or glob: the files that trigger the rule.');
|
|
14
|
+
return 2;
|
|
15
|
+
}
|
|
16
|
+
if (!opts.require || opts.require.length === 0) {
|
|
17
|
+
deps.err('--require needs at least one path or glob: the file a change must also touch.');
|
|
18
|
+
return 2;
|
|
19
|
+
}
|
|
20
|
+
let rule;
|
|
21
|
+
try {
|
|
22
|
+
rule = parseCoChangeRule({ when_paths: opts.when, require_paths: opts.require, scope: 'same_pr' });
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
deps.err(e.message);
|
|
26
|
+
return 2;
|
|
27
|
+
}
|
|
28
|
+
const dbPath = deps.graphPath();
|
|
29
|
+
if (dbPath === undefined) {
|
|
30
|
+
deps.err(NO_GRAPH);
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
const { decisionId, created } = addCoChangeRule(dbPath, { title, summary: opts.summary, rule, repo: await deps.repo() });
|
|
34
|
+
deps.out(created ? `Rule recorded (decision ${decisionId}).` : `That rule already exists (decision ${decisionId}).`);
|
|
35
|
+
deps.out(`A change touching ${rule.when_paths.join(', ')} must also touch ${rule.require_paths.join(' or ')} in the same PR. \`align check\` flags it when it does not.`);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
export async function runRuleList(deps) {
|
|
39
|
+
const dbPath = deps.graphPath();
|
|
40
|
+
const rules = dbPath === undefined ? [] : listCoChangeRules(dbPath);
|
|
41
|
+
if (rules.length === 0) {
|
|
42
|
+
deps.out('No co-change rules yet. `align rule add --help` shows how to write one.');
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
for (const r of rules) {
|
|
46
|
+
deps.out(`${r.title}\n touching ${r.rule.when_paths.join(', ')} requires ${r.rule.require_paths.join(' or ')} (same PR)\n repo ${r.repo ?? 'any'} rule ${r.ruleId}`);
|
|
47
|
+
}
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
export async function runRuleRemove(id, deps) {
|
|
51
|
+
if (id === undefined || id.trim() === '') {
|
|
52
|
+
deps.err('Give the rule id `align rule list` printed: align rule remove <id>');
|
|
53
|
+
return 2;
|
|
54
|
+
}
|
|
55
|
+
const dbPath = deps.graphPath();
|
|
56
|
+
if (dbPath === undefined) {
|
|
57
|
+
deps.err(NO_GRAPH);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
if (!removeCoChangeRule(dbPath, id.trim())) {
|
|
61
|
+
deps.err(`No rule has the id ${id}. \`align rule list\` shows them.`);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
deps.out('Rule removed. Its decision stays in your graph.');
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
export function registerRuleCommand(program) {
|
|
68
|
+
const rule = program
|
|
69
|
+
.command('rule')
|
|
70
|
+
.description('Co-change rules: a PR touching some files must also touch another, checked by file list with no model (local)');
|
|
71
|
+
const deps = { out: (l) => console.log(l), err: (l) => console.error(chalk.red(l)), graphPath: () => localGraphPath(), repo: () => currentRepoIdentity() };
|
|
72
|
+
rule
|
|
73
|
+
.command('add [title]')
|
|
74
|
+
.description('Record a rule: a change touching --when must also touch one of --require in the same PR')
|
|
75
|
+
.option('--when <globs...>', 'Paths or globs that trigger the rule (e.g. "openapi/**")')
|
|
76
|
+
.option('--require <paths...>', 'Paths or globs, any one of which a triggering change must also touch (e.g. docs/changelog.md)')
|
|
77
|
+
.option('--summary <text>', 'Why the rule exists, in a sentence')
|
|
78
|
+
.action(async (title, opts) => {
|
|
79
|
+
const code = await runRuleAdd(title, opts, deps);
|
|
80
|
+
if (code !== 0)
|
|
81
|
+
process.exitCode = code;
|
|
82
|
+
});
|
|
83
|
+
rule
|
|
84
|
+
.command('remove [id]')
|
|
85
|
+
.description('Remove a co-change rule by the id `align rule list` prints. Its decision stays')
|
|
86
|
+
.action(async (id) => {
|
|
87
|
+
const code = await runRuleRemove(id, deps);
|
|
88
|
+
if (code !== 0)
|
|
89
|
+
process.exitCode = code;
|
|
90
|
+
});
|
|
91
|
+
rule
|
|
92
|
+
.command('list')
|
|
93
|
+
.description('Show the co-change rules in your local graph')
|
|
94
|
+
.action(async () => {
|
|
95
|
+
const code = await runRuleList(deps);
|
|
96
|
+
if (code !== 0)
|
|
97
|
+
process.exitCode = code;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=rule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule.js","sourceRoot":"","sources":["../../src/commands/rule.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { AlignmentResult } from './gateway-client.js';
|
|
2
|
+
import type { CoChangeViolation } from './co-change.js';
|
|
3
|
+
/** Which change `align check` is looking at: the same three sources it takes its diff from. */
|
|
4
|
+
export type ChangeSource = {
|
|
5
|
+
kind: 'base';
|
|
6
|
+
ref: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'staged';
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'head';
|
|
11
|
+
};
|
|
12
|
+
/** Throws when git cannot answer: "I could not look" must never read as "nothing changed". */
|
|
13
|
+
export declare function changedFiles(source: ChangeSource, cwd?: string): Promise<string[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Fold violations into the judge's answer. `llm` is null when the judge threw; `llmError` says why.
|
|
16
|
+
* A rule conflict is critical: it has one right answer, so it carries no confidence to discount.
|
|
17
|
+
* It also outranks `unknown`: "the judge could not decide" is no reason to hide a violation.
|
|
18
|
+
*/
|
|
19
|
+
export declare function mergeCoChange(llm: AlignmentResult | null, llmError: unknown, violations: CoChangeViolation[]): AlignmentResult;
|
|
20
|
+
/**
|
|
21
|
+
* The violations for this check, or [] when there is nothing to evaluate. Rules live in the local
|
|
22
|
+
* graph, so any other environment reads nothing (the hosted gateway holds its own). A graph with no
|
|
23
|
+
* rules returns before git is asked anything, so the feature costs a user who has no rules nothing.
|
|
24
|
+
* Throws when rules exist and the file list cannot be read: a rule that could not be evaluated must
|
|
25
|
+
* not read as one that was satisfied.
|
|
26
|
+
*/
|
|
27
|
+
export declare function coChangeViolations(env: {
|
|
28
|
+
mode: string;
|
|
29
|
+
localDbPath?: string;
|
|
30
|
+
}, source: ChangeSource): Promise<CoChangeViolation[]>;
|
|
31
|
+
/** Run the judge, then fold in the violations. With none, the judge's answer or failure passes through untouched. */
|
|
32
|
+
export declare function judgeWithCoChange(judge: () => Promise<AlignmentResult>, violations: CoChangeViolation[]): Promise<AlignmentResult>;
|
|
33
|
+
//# sourceMappingURL=co-change-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"co-change-check.d.ts","sourceRoot":"","sources":["../../src/lib/co-change-check.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,+FAA+F;AAC/F,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAYjG,8FAA8F;AAC9F,wBAAsB,YAAY,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAGvG;AAaD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,GAAG,eAAe,CA2B9H;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAUxI;AAED,qHAAqH;AACrH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,EAAE,UAAU,EAAE,iBAAiB,EAAE,GAAG,OAAO,CAAC,eAAe,CAAC,CAOxI"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Co-change rules inside `align check`: the changed-file list, and the fold of a violation into the
|
|
3
|
+
* result. The judge never sees any of this. It runs beside the LLM, needs no model call, and is
|
|
4
|
+
* evaluated whether the judge answered, abstained or threw.
|
|
5
|
+
*/
|
|
6
|
+
import { execa } from 'execa';
|
|
7
|
+
function argsFor(source) {
|
|
8
|
+
// --no-renames: a rename is a delete plus an add, so the OLD path counts as touched too. With rename
|
|
9
|
+
// detection on, `--name-only` prints only the new name and a moved changelog would hide the move.
|
|
10
|
+
// -z: no path quoting, so a name with a space or a non-ASCII character comes back verbatim.
|
|
11
|
+
const base = ['diff', '--name-only', '--no-renames', '-z'];
|
|
12
|
+
if (source.kind === 'base')
|
|
13
|
+
return [...base, `${source.ref}...HEAD`];
|
|
14
|
+
if (source.kind === 'staged')
|
|
15
|
+
return [...base, '--staged'];
|
|
16
|
+
return [...base, 'HEAD'];
|
|
17
|
+
}
|
|
18
|
+
/** Throws when git cannot answer: "I could not look" must never read as "nothing changed". */
|
|
19
|
+
export async function changedFiles(source, cwd = process.cwd()) {
|
|
20
|
+
const { stdout } = await execa('git', argsFor(source), { cwd });
|
|
21
|
+
return stdout.split('\0').filter((f) => f !== '');
|
|
22
|
+
}
|
|
23
|
+
const SHOWN_FILES = 3;
|
|
24
|
+
function listFiles(files) {
|
|
25
|
+
const shown = files.slice(0, SHOWN_FILES).join(', ');
|
|
26
|
+
return files.length > SHOWN_FILES ? `${shown} and ${files.length - SHOWN_FILES} more` : shown;
|
|
27
|
+
}
|
|
28
|
+
function reasonFor(v) {
|
|
29
|
+
return `This change touches ${listFiles(v.triggeredBy)} but none of ${v.missing.join(', ')}. The rule asks for a change to ${v.missing.join(' or ')} in the same PR.`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Fold violations into the judge's answer. `llm` is null when the judge threw; `llmError` says why.
|
|
33
|
+
* A rule conflict is critical: it has one right answer, so it carries no confidence to discount.
|
|
34
|
+
* It also outranks `unknown`: "the judge could not decide" is no reason to hide a violation.
|
|
35
|
+
*/
|
|
36
|
+
export function mergeCoChange(llm, llmError, violations) {
|
|
37
|
+
const ruleConflicts = violations.map((v) => ({
|
|
38
|
+
decision_id: v.decisionId,
|
|
39
|
+
title: v.title,
|
|
40
|
+
summary: v.summary,
|
|
41
|
+
...(v.url ? { url: v.url } : {}),
|
|
42
|
+
reason: reasonFor(v),
|
|
43
|
+
severity: 'critical',
|
|
44
|
+
}));
|
|
45
|
+
const ruleIds = new Set(ruleConflicts.map((c) => c.decision_id));
|
|
46
|
+
const judged = (llm?.conflicts ?? []).filter((c) => !ruleIds.has(c.decision_id));
|
|
47
|
+
const judgeNote = llm === null
|
|
48
|
+
? [`The decision-graph judge did not run (${llmError instanceof Error ? llmError.message : String(llmError)}). The file rule above needed no judge.`]
|
|
49
|
+
: [];
|
|
50
|
+
const message = `This change breaks ${ruleConflicts.length} file rule(s) from your decision graph: a file that should have changed with it did not.`;
|
|
51
|
+
// `reason` and `reason_class` describe an `unknown` verdict, which this result no longer is.
|
|
52
|
+
const { reason: _reason, reason_class: _reasonClass, ...kept } = llm ?? { relevant_decisions: [] };
|
|
53
|
+
const related = kept.relevant_decisions.filter((d) => !ruleIds.has(d.id));
|
|
54
|
+
return {
|
|
55
|
+
...kept,
|
|
56
|
+
relevant_decisions: [...violations.map((v) => ({ id: v.decisionId, title: v.title, summary: v.summary, similarity: 1, ...(v.url ? { url: v.url } : {}) })), ...related],
|
|
57
|
+
status: 'conflicting',
|
|
58
|
+
confidence: 1,
|
|
59
|
+
conflicts: [...ruleConflicts, ...judged],
|
|
60
|
+
notes: [...(llm?.notes ?? []), ...judgeNote],
|
|
61
|
+
message,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The violations for this check, or [] when there is nothing to evaluate. Rules live in the local
|
|
66
|
+
* graph, so any other environment reads nothing (the hosted gateway holds its own). A graph with no
|
|
67
|
+
* rules returns before git is asked anything, so the feature costs a user who has no rules nothing.
|
|
68
|
+
* Throws when rules exist and the file list cannot be read: a rule that could not be evaluated must
|
|
69
|
+
* not read as one that was satisfied.
|
|
70
|
+
*/
|
|
71
|
+
export async function coChangeViolations(env, source) {
|
|
72
|
+
if (env.mode !== 'local-embedded')
|
|
73
|
+
return [];
|
|
74
|
+
const [{ listCoChangeRules }, { evaluateCoChange }, { getLocalDbPath }, { currentRepoIdentity }] = await Promise.all([
|
|
75
|
+
import('./co-change-store.js'), import('./co-change.js'), import('./local-mode.js'), import('./repo-identity.js'),
|
|
76
|
+
]);
|
|
77
|
+
// Rules belong to the repo they were added in (the identity decisions.repo carries): one machine-wide
|
|
78
|
+
// graph must not make a rule from repo A fire in repo B.
|
|
79
|
+
const rules = listCoChangeRules(env.localDbPath ?? getLocalDbPath(), { repo: await currentRepoIdentity() });
|
|
80
|
+
if (rules.length === 0)
|
|
81
|
+
return [];
|
|
82
|
+
return evaluateCoChange(rules, await changedFiles(source));
|
|
83
|
+
}
|
|
84
|
+
/** Run the judge, then fold in the violations. With none, the judge's answer or failure passes through untouched. */
|
|
85
|
+
export async function judgeWithCoChange(judge, violations) {
|
|
86
|
+
if (violations.length === 0)
|
|
87
|
+
return judge();
|
|
88
|
+
try {
|
|
89
|
+
return mergeCoChange(await judge(), null, violations);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
return mergeCoChange(null, err, violations);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=co-change-check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"co-change-check.js","sourceRoot":"","sources":["../../src/lib/co-change-check.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "A change under these paths must also change those paths, in the same PR." The shape
|
|
3
|
+
* `align check` evaluates deterministically against a PR's file list. Defined once here so
|
|
4
|
+
* an importer that emits it and a checker that reads it cannot spell it two ways.
|
|
5
|
+
*/
|
|
6
|
+
export interface CoChangeRule {
|
|
7
|
+
when_paths: string[];
|
|
8
|
+
require_paths: string[];
|
|
9
|
+
scope: 'same_pr';
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=co-change-rule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"co-change-rule.d.ts","sourceRoot":"","sources":["../../src/lib/co-change-rule.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,EAAE,SAAS,CAAC;CAClB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"co-change-rule.js","sourceRoot":"","sources":["../../src/lib/co-change-rule.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type CoChangeRule, type StoredCoChangeRule } from './co-change.js';
|
|
2
|
+
export declare function addCoChangeRule(dbPath: string, input: {
|
|
3
|
+
title: string;
|
|
4
|
+
summary?: string;
|
|
5
|
+
rule: CoChangeRule;
|
|
6
|
+
repo?: string | null;
|
|
7
|
+
}): {
|
|
8
|
+
decisionId: string;
|
|
9
|
+
created: boolean;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The docs import's writer: the rules on `decisionId` become exactly `raw` (one rule, or none).
|
|
13
|
+
* Replacing rather than adding is what lets a README that drops a rule drop it here too. Only the
|
|
14
|
+
* imported decision's own rules are touched; a hand-added rule lives on its own decision
|
|
15
|
+
* (addCoChangeRule). A rule that does not parse is skipped, never thrown: one bad section must not
|
|
16
|
+
* fail the import of every other one. True when a rule was stored.
|
|
17
|
+
*/
|
|
18
|
+
export declare function syncImportedCoChangeRule(dbPath: string, decisionId: string, repo: string | null, raw: unknown): boolean;
|
|
19
|
+
/** Every rule whose decision still exists. A missing graph, or one that predates the table, lists nothing. */
|
|
20
|
+
export declare function listCoChangeRules(dbPath: string, scope?: {
|
|
21
|
+
repo?: string | null;
|
|
22
|
+
}): StoredCoChangeRule[];
|
|
23
|
+
/** Deletes the rule and keeps its decision. False when no rule has that id. */
|
|
24
|
+
export declare function removeCoChangeRule(dbPath: string, ruleId: string): boolean;
|
|
25
|
+
//# sourceMappingURL=co-change-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"co-change-store.d.ts","sourceRoot":"","sources":["../../src/lib/co-change-store.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,KAAK,YAAY,EAAqB,KAAK,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAkC/F,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAuB9K;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAuBvH;AAED,8GAA8G;AAC9G,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAAG,kBAAkB,EAAE,CAoB5G;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAQ1E"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads and writes of `co_change_rules` (local-db-cochange.ts). Same short-lived-handle pattern as
|
|
3
|
+
* curation/judgements-db.ts, so the MCP server and a sync child can both be live. A reader never
|
|
4
|
+
* creates a missing graph.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
9
|
+
import { assertSchemaSupported } from './local-db-migrate.js';
|
|
10
|
+
import { tightenGraphFiles } from './private-files.js';
|
|
11
|
+
import { createLocalDb } from './local-db.js';
|
|
12
|
+
import { parseCoChangeRule } from './co-change.js';
|
|
13
|
+
/** A graph from before the repo column has the table but not the column. The write-lock migration lives in createLocalDb, so run that once and read again. */
|
|
14
|
+
function ensureCurrent(dbPath) {
|
|
15
|
+
const db = new DatabaseSync(dbPath);
|
|
16
|
+
let stale = false;
|
|
17
|
+
try {
|
|
18
|
+
stale = db.prepare('PRAGMA table_info(co_change_rules)').all().length > 0
|
|
19
|
+
&& !db.prepare('PRAGMA table_info(co_change_rules)').all().some((c) => c.name === 'repo');
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
db.close();
|
|
23
|
+
}
|
|
24
|
+
if (stale)
|
|
25
|
+
createLocalDb(dbPath).close();
|
|
26
|
+
}
|
|
27
|
+
function openRead(dbPath) {
|
|
28
|
+
ensureCurrent(dbPath);
|
|
29
|
+
tightenGraphFiles(dbPath);
|
|
30
|
+
const db = new DatabaseSync(dbPath);
|
|
31
|
+
db.exec('PRAGMA busy_timeout = 30000');
|
|
32
|
+
assertSchemaSupported(db.prepare('PRAGMA user_version').get().user_version, db);
|
|
33
|
+
return db;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The decision's identity is (source_url, title), so the url is derived from the rule: the same title
|
|
37
|
+
* and rule re-added is the same decision, and the same title with a different rule is another one.
|
|
38
|
+
* It is an opaque key, never a link, and the listing does not surface it as one.
|
|
39
|
+
*/
|
|
40
|
+
function ruleSourceKey(title, rule, repo) {
|
|
41
|
+
const digest = createHash('sha256').update(JSON.stringify([title, repo, rule.when_paths, rule.require_paths, rule.scope])).digest('hex').slice(0, 16);
|
|
42
|
+
return `align-rule://co-change/${digest}`;
|
|
43
|
+
}
|
|
44
|
+
export function addCoChangeRule(dbPath, input) {
|
|
45
|
+
const rule = parseCoChangeRule(input.rule);
|
|
46
|
+
const title = input.title.trim();
|
|
47
|
+
const repo = input.repo ?? null;
|
|
48
|
+
if (title === '')
|
|
49
|
+
throw new Error('A co-change rule needs a title: the decision it records');
|
|
50
|
+
const summary = input.summary?.trim() || `A change touching ${rule.when_paths.join(', ')} must also touch ${rule.require_paths.join(' or ')} in the same PR.`;
|
|
51
|
+
const store = createLocalDb(dbPath);
|
|
52
|
+
try {
|
|
53
|
+
const sourceUrl = ruleSourceKey(title, rule, repo);
|
|
54
|
+
const existing = store.findIdBySource(sourceUrl, title);
|
|
55
|
+
const decisionId = store.insertDecision({ title, summary, sourceUrl, platform: 'cli', repo, deciderKind: 'human' });
|
|
56
|
+
const db = new DatabaseSync(dbPath);
|
|
57
|
+
try {
|
|
58
|
+
db.exec('PRAGMA busy_timeout = 30000');
|
|
59
|
+
db.prepare(`INSERT OR IGNORE INTO co_change_rules (id, decision_id, repo, when_paths, require_paths, scope) VALUES (?, ?, ?, ?, ?, ?)`)
|
|
60
|
+
.run(randomUUID(), decisionId, repo, JSON.stringify(rule.when_paths), JSON.stringify(rule.require_paths), rule.scope);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
db.close();
|
|
64
|
+
}
|
|
65
|
+
return { decisionId, created: existing === null };
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
store.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The docs import's writer: the rules on `decisionId` become exactly `raw` (one rule, or none).
|
|
73
|
+
* Replacing rather than adding is what lets a README that drops a rule drop it here too. Only the
|
|
74
|
+
* imported decision's own rules are touched; a hand-added rule lives on its own decision
|
|
75
|
+
* (addCoChangeRule). A rule that does not parse is skipped, never thrown: one bad section must not
|
|
76
|
+
* fail the import of every other one. True when a rule was stored.
|
|
77
|
+
*/
|
|
78
|
+
export function syncImportedCoChangeRule(dbPath, decisionId, repo, raw) {
|
|
79
|
+
let rule;
|
|
80
|
+
try {
|
|
81
|
+
rule = raw === undefined ? undefined : parseCoChangeRule(raw);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
rule = undefined;
|
|
85
|
+
}
|
|
86
|
+
const db = new DatabaseSync(dbPath);
|
|
87
|
+
try {
|
|
88
|
+
db.exec('PRAGMA busy_timeout = 30000');
|
|
89
|
+
const stored = db.prepare('SELECT repo, when_paths AS w, require_paths AS r, scope FROM co_change_rules WHERE decision_id = ?')
|
|
90
|
+
.all(decisionId);
|
|
91
|
+
// The same rule imported again keeps its row, so its id (what `align rule remove` takes) is stable.
|
|
92
|
+
if (rule !== undefined && stored.length === 1 && stored[0].repo === repo && stored[0].w === JSON.stringify(rule.when_paths)
|
|
93
|
+
&& stored[0].r === JSON.stringify(rule.require_paths) && stored[0].scope === rule.scope)
|
|
94
|
+
return true;
|
|
95
|
+
db.prepare('DELETE FROM co_change_rules WHERE decision_id = ?').run(decisionId);
|
|
96
|
+
if (rule === undefined)
|
|
97
|
+
return false;
|
|
98
|
+
db.prepare(`INSERT INTO co_change_rules (id, decision_id, repo, when_paths, require_paths, scope) VALUES (?, ?, ?, ?, ?, ?)`)
|
|
99
|
+
.run(randomUUID(), decisionId, repo, JSON.stringify(rule.when_paths), JSON.stringify(rule.require_paths), rule.scope);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
db.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Every rule whose decision still exists. A missing graph, or one that predates the table, lists nothing. */
|
|
107
|
+
export function listCoChangeRules(dbPath, scope = {}) {
|
|
108
|
+
if (dbPath === '' || !fs.existsSync(dbPath))
|
|
109
|
+
return [];
|
|
110
|
+
const db = openRead(dbPath);
|
|
111
|
+
try {
|
|
112
|
+
const rows = db.prepare(`SELECT r.id AS ruleId, r.repo AS repo, d.id AS id, d.title AS title, d.summary AS summary, r.when_paths AS whenPaths, r.require_paths AS requirePaths, r.scope AS scope
|
|
113
|
+
FROM co_change_rules r JOIN decisions d ON d.id = r.decision_id ORDER BY r.created_at, r.rowid`).all();
|
|
114
|
+
// Asking for a repo keeps that repo's rules and the ones with no repo; asking for none keeps all.
|
|
115
|
+
const wanted = scope.repo === undefined ? rows : rows.filter((r) => r.repo === null || r.repo === scope.repo);
|
|
116
|
+
return wanted.map((r) => ({
|
|
117
|
+
ruleId: r.ruleId, repo: r.repo, decisionId: r.id, title: r.title, summary: r.summary,
|
|
118
|
+
rule: parseCoChangeRule({ when_paths: JSON.parse(r.whenPaths), require_paths: JSON.parse(r.requirePaths), scope: r.scope }),
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
if (/no such table/i.test(e.message))
|
|
123
|
+
return [];
|
|
124
|
+
throw e;
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
db.close();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Deletes the rule and keeps its decision. False when no rule has that id. */
|
|
131
|
+
export function removeCoChangeRule(dbPath, ruleId) {
|
|
132
|
+
if (dbPath === '' || !fs.existsSync(dbPath))
|
|
133
|
+
return false;
|
|
134
|
+
const db = openRead(dbPath);
|
|
135
|
+
try {
|
|
136
|
+
return Number(db.prepare('DELETE FROM co_change_rules WHERE id = ?').run(ruleId).changes) > 0;
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
db.close();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=co-change-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"co-change-store.js","sourceRoot":"","sources":["../../src/lib/co-change-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Co-change rules: "a change that touches X must also touch Y, in the same PR".
|
|
3
|
+
*
|
|
4
|
+
* This is a question about WHICH FILES a change touches, so it has one right answer and is computed
|
|
5
|
+
* from the changed-file list, never judged. The LLM check cannot do it: it reads a slice of the diff,
|
|
6
|
+
* and a file a PR forgot to touch is not in a diff at all.
|
|
7
|
+
*
|
|
8
|
+
* The rule shape is shared with the import lane (README rules) and the hosted gateway. Do not rename
|
|
9
|
+
* its fields.
|
|
10
|
+
*/
|
|
11
|
+
export type CoChangeScope = 'same_pr';
|
|
12
|
+
export interface CoChangeRule {
|
|
13
|
+
/** Globs. The rule applies when the change touches any of them. */
|
|
14
|
+
when_paths: string[];
|
|
15
|
+
/** Globs. The rule is met when the change touches any of them. */
|
|
16
|
+
require_paths: string[];
|
|
17
|
+
scope: CoChangeScope;
|
|
18
|
+
}
|
|
19
|
+
/** A rule with the decision it belongs to, as the local graph returns it. */
|
|
20
|
+
export interface StoredCoChangeRule {
|
|
21
|
+
ruleId: string;
|
|
22
|
+
repo: string | null;
|
|
23
|
+
decisionId: string;
|
|
24
|
+
title: string;
|
|
25
|
+
summary: string;
|
|
26
|
+
url?: string;
|
|
27
|
+
rule: CoChangeRule;
|
|
28
|
+
}
|
|
29
|
+
export interface CoChangeViolation {
|
|
30
|
+
decisionId: string;
|
|
31
|
+
title: string;
|
|
32
|
+
summary: string;
|
|
33
|
+
url?: string;
|
|
34
|
+
/** The changed files that matched a when path. */
|
|
35
|
+
triggeredBy: string[];
|
|
36
|
+
/** The require globs, none of which matched a changed file. */
|
|
37
|
+
missing: string[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Not a RegExp on purpose. `**a**a**a...**b` compiled to `.*a.*a.*a...b` backtracks exponentially
|
|
41
|
+
* (68s measured at eight repeats against sixty characters), and a rule is data a person or another
|
|
42
|
+
* lane wrote. This walks the tokens once, keeping the set of path positions still reachable, so the
|
|
43
|
+
* cost is tokens x path length whatever the glob looks like.
|
|
44
|
+
*/
|
|
45
|
+
export declare function pathMatches(glob: string, path: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Validates a rule from outside (a flag, a stored row, another lane's writer). An empty list is
|
|
48
|
+
* refused rather than stored: empty require_paths could never be met, so the rule would flag every
|
|
49
|
+
* matching PR forever, and empty when_paths could never trigger.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseCoChangeRule(raw: unknown): CoChangeRule;
|
|
52
|
+
/** Every rule the change violates: it touches a when path and touches none of the require paths. */
|
|
53
|
+
export declare function evaluateCoChange(rules: StoredCoChangeRule[], changed: string[]): CoChangeViolation[];
|
|
54
|
+
//# sourceMappingURL=co-change.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"co-change.d.ts","sourceRoot":"","sources":["../../src/lib/co-change.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,CAAC;AAEtC,MAAM,WAAW,YAAY;IAC3B,mEAAmE;IACnE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,kEAAkE;IAClE,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,KAAK,EAAE,aAAa,CAAC;CACtB;AAED,6EAA6E;AAC7E,MAAM,WAAW,kBAAkB;IAAG,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,YAAY,CAAA;CAAE;AAEjK,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,+DAA+D;IAC/D,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAkCD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CA0B/D;AAeD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,CAK5D;AAED,oGAAoG;AACpG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,kBAAkB,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,iBAAiB,EAAE,CASpG"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
function normalise(p) {
|
|
2
|
+
return p.replace(/^(?:\.\/)+/, '').replace(/^\/+/, '');
|
|
3
|
+
}
|
|
4
|
+
const tokenCache = new Map();
|
|
5
|
+
/** `*` and `?` stay inside one path segment; `**` crosses segments; a trailing `/` means everything under it. */
|
|
6
|
+
function tokenise(glob) {
|
|
7
|
+
const cached = tokenCache.get(glob);
|
|
8
|
+
if (cached)
|
|
9
|
+
return cached;
|
|
10
|
+
let g = normalise(glob);
|
|
11
|
+
if (g.endsWith('/'))
|
|
12
|
+
g += '**';
|
|
13
|
+
const tokens = [];
|
|
14
|
+
for (let i = 0; i < g.length; i++) {
|
|
15
|
+
const c = g[i];
|
|
16
|
+
if (c === '*' && g[i + 1] === '*') {
|
|
17
|
+
if (g[i + 2] === '/') {
|
|
18
|
+
tokens.push({ kind: 'dirs' });
|
|
19
|
+
i += 2;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
tokens.push({ kind: 'globstar' });
|
|
23
|
+
i += 1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else if (c === '*')
|
|
27
|
+
tokens.push({ kind: 'star' });
|
|
28
|
+
else if (c === '?')
|
|
29
|
+
tokens.push({ kind: 'one' });
|
|
30
|
+
else
|
|
31
|
+
tokens.push({ kind: 'lit', ch: c });
|
|
32
|
+
}
|
|
33
|
+
tokenCache.set(glob, tokens);
|
|
34
|
+
return tokens;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Not a RegExp on purpose. `**a**a**a...**b` compiled to `.*a.*a.*a...b` backtracks exponentially
|
|
38
|
+
* (68s measured at eight repeats against sixty characters), and a rule is data a person or another
|
|
39
|
+
* lane wrote. This walks the tokens once, keeping the set of path positions still reachable, so the
|
|
40
|
+
* cost is tokens x path length whatever the glob looks like.
|
|
41
|
+
*/
|
|
42
|
+
export function pathMatches(glob, path) {
|
|
43
|
+
const p = normalise(path);
|
|
44
|
+
const n = p.length;
|
|
45
|
+
let cur = new Uint8Array(n + 1);
|
|
46
|
+
cur[0] = 1;
|
|
47
|
+
for (const t of tokenise(glob)) {
|
|
48
|
+
const next = new Uint8Array(n + 1);
|
|
49
|
+
if (t.kind === 'lit') {
|
|
50
|
+
for (let j = 0; j < n; j++)
|
|
51
|
+
if (cur[j] && p[j] === t.ch)
|
|
52
|
+
next[j + 1] = 1;
|
|
53
|
+
}
|
|
54
|
+
else if (t.kind === 'one') {
|
|
55
|
+
for (let j = 0; j < n; j++)
|
|
56
|
+
if (cur[j] && p[j] !== '/')
|
|
57
|
+
next[j + 1] = 1;
|
|
58
|
+
}
|
|
59
|
+
else if (t.kind === 'star') {
|
|
60
|
+
for (let j = 0; j <= n; j++)
|
|
61
|
+
next[j] = cur[j] || (j > 0 && next[j - 1] && p[j - 1] !== '/') ? 1 : 0;
|
|
62
|
+
}
|
|
63
|
+
else if (t.kind === 'globstar') {
|
|
64
|
+
for (let j = 0; j <= n; j++)
|
|
65
|
+
next[j] = cur[j] || (j > 0 && next[j - 1]) ? 1 : 0;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// zero directories, or any run of characters that ends in a slash
|
|
69
|
+
let seen = false; // some cur[k] with k <= j - 1
|
|
70
|
+
for (let j = 0; j <= n; j++) {
|
|
71
|
+
if (j > 0 && cur[j - 1])
|
|
72
|
+
seen = true;
|
|
73
|
+
next[j] = cur[j] || (j > 0 && p[j - 1] === '/' && seen) ? 1 : 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
cur = next;
|
|
77
|
+
}
|
|
78
|
+
return cur[n] === 1;
|
|
79
|
+
}
|
|
80
|
+
/** Syntax from fuller glob dialects. The matcher would treat each as a literal character, so the rule would never trigger or never be met. */
|
|
81
|
+
const UNSUPPORTED_GLOB = /[{}[\]!\\]/;
|
|
82
|
+
function pathList(raw, field) {
|
|
83
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
84
|
+
throw new Error(`${field} must be a non-empty list of paths or globs`);
|
|
85
|
+
for (const p of raw) {
|
|
86
|
+
if (typeof p !== 'string' || p.trim() === '')
|
|
87
|
+
throw new Error(`${field} must contain only non-blank strings`);
|
|
88
|
+
const bad = UNSUPPORTED_GLOB.exec(p);
|
|
89
|
+
if (bad)
|
|
90
|
+
throw new Error(`${field}: "${p}" uses ${bad[0] === '\\' ? 'a backslash' : `"${bad[0]}"`}, which is not supported. Only *, ** and ? are: no {a,b} lists, [abc] classes, ! negation or escapes. Write one path per alternative.`);
|
|
91
|
+
}
|
|
92
|
+
return raw;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Validates a rule from outside (a flag, a stored row, another lane's writer). An empty list is
|
|
96
|
+
* refused rather than stored: empty require_paths could never be met, so the rule would flag every
|
|
97
|
+
* matching PR forever, and empty when_paths could never trigger.
|
|
98
|
+
*/
|
|
99
|
+
export function parseCoChangeRule(raw) {
|
|
100
|
+
if (typeof raw !== 'object' || raw === null)
|
|
101
|
+
throw new Error('A co-change rule must be an object');
|
|
102
|
+
const r = raw;
|
|
103
|
+
if (r['scope'] !== 'same_pr')
|
|
104
|
+
throw new Error("scope must be 'same_pr'");
|
|
105
|
+
return { when_paths: pathList(r['when_paths'], 'when_paths'), require_paths: pathList(r['require_paths'], 'require_paths'), scope: 'same_pr' };
|
|
106
|
+
}
|
|
107
|
+
/** Every rule the change violates: it touches a when path and touches none of the require paths. */
|
|
108
|
+
export function evaluateCoChange(rules, changed) {
|
|
109
|
+
const violations = [];
|
|
110
|
+
for (const { decisionId, title, summary, url, rule } of rules) {
|
|
111
|
+
const triggeredBy = changed.filter((f) => rule.when_paths.some((g) => pathMatches(g, f)));
|
|
112
|
+
if (triggeredBy.length === 0)
|
|
113
|
+
continue;
|
|
114
|
+
if (changed.some((f) => rule.require_paths.some((g) => pathMatches(g, f))))
|
|
115
|
+
continue;
|
|
116
|
+
violations.push({ decisionId, title, summary, ...(url ? { url } : {}), triggeredBy, missing: [...rule.require_paths] });
|
|
117
|
+
}
|
|
118
|
+
return violations;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=co-change.js.map
|