@rigour-labs/core 6.11.2-rc.1 → 6.12.0-rc.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.
@@ -36,7 +36,8 @@ export declare function buildBriefing(cwd: string, input?: BriefingInput): Brief
36
36
  export declare function briefingText(briefing: Briefing): string;
37
37
  /**
38
38
  * The briefing for one file, the first time an agent edits it: the requirement rules that name it (or its folder), the
39
- * lessons the team learned on it, and the points the team settled against on it; at most three. At session start the
39
+ * lessons the team learned on it, then those a person widened to its folder or the whole repository, and the points
40
+ * the team settled against on it; at most three. At session start the
40
41
  * task's files are often unknown; the first edit of a file is when they are, and when a briefing can be specific.
41
42
  */
42
43
  export declare function buildFileBriefing(cwd: string, file: string, input?: {
@@ -7,6 +7,7 @@
7
7
  * matchers, before the code exists instead of after. It is deterministic (no model call), local, and it never blocks.
8
8
  */
9
9
  import { spawnSync } from 'child_process';
10
+ import path from 'path';
10
11
  import { rulesForDiff } from '../review-learning/repo-rules.js';
11
12
  import { describeLesson, lessonsForDiff, lessonView, rejectedForDiff } from '../review-learning/team-lessons.js';
12
13
  import { appendTaskEvent, taskOf } from '../task/thread.js';
@@ -52,7 +53,8 @@ export function briefingText(briefing) {
52
53
  }
53
54
  /**
54
55
  * The briefing for one file, the first time an agent edits it: the requirement rules that name it (or its folder), the
55
- * lessons the team learned on it, and the points the team settled against on it; at most three. At session start the
56
+ * lessons the team learned on it, then those a person widened to its folder or the whole repository, and the points
57
+ * the team settled against on it; at most three. At session start the
56
58
  * task's files are often unknown; the first edit of a file is when they are, and when a briefing can be specific.
57
59
  */
58
60
  export function buildFileBriefing(cwd, file, input = {}) {
@@ -61,12 +63,14 @@ export function buildFileBriefing(cwd, file, input = {}) {
61
63
  const shape = shapeOf([file], '');
62
64
  const mode = input.lessons ?? 'verified';
63
65
  const rules = rulesForDiff(cwd, shape, true, BRIEFING_MAX_ITEMS, true).filter(r => r.requirement);
64
- const lessons = lessonsForDiff(cwd, shape, mode, 0, BRIEFING_MAX_ITEMS, BRIEFING_MAX_ITEMS).filter(l => l.file === file && (l.state === 'verified' || mode === 'all'));
66
+ const reaches = (l) => l.file === file || l.scope === 'repo' || (l.scope === 'folder' && file.startsWith(`${path.posix.dirname(l.file)}/`));
67
+ const lessons = lessonsForDiff(cwd, shape, mode, 0, BRIEFING_MAX_ITEMS, BRIEFING_MAX_ITEMS).filter(l => reaches(l) && (l.state === 'verified' || mode === 'all'));
65
68
  const settled = rejectedForDiff(cwd, shape).filter(l => l.file === file);
66
69
  const cited = (prs) => (prs.length ? `learned in PR ${prs.map(p => `#${p}`).join(', ')}` : 'the team\'s decision');
67
70
  const items = [
68
71
  ...rules.map(r => ({ kind: 'rule', text: r.text, cite: ruleCite(r.source, r.scope), requirement: true, id: `rule:${r.id}` })),
69
- ...lessons.map(l => ({ kind: 'lesson', text: lessonView(l).text, cite: cited(lessonView(l).prs), id: `lesson:${l.id}` })),
72
+ // This file's own lessons read as they are; a widened one says where it reaches (a folder, the team).
73
+ ...lessons.map(l => ({ kind: 'lesson', text: l.file === file && !l.scope ? lessonView(l).text : describeLesson({ ...lessonView(l), prs: [] }), cite: cited(lessonView(l).prs), id: `lesson:${l.id}` })),
70
74
  ...settled.map(l => ({ kind: 'settled', text: `settled against, do not do or raise it: ${lessonView(l).text}`, cite: cited(lessonView(l).prs), id: `settled:${l.id}` })),
71
75
  ];
72
76
  return { ...(task ? { task: task.key } : {}), goal: '', files: [file], items: items.slice(0, limit) };
package/dist/index.d.ts CHANGED
@@ -49,7 +49,7 @@ export { decideCompiledCheck, proposeCompiledChecks, readCompiledChecks, suspens
49
49
  export { ruleWriterFor } from './review/reviewer/rule-writer.js';
50
50
  export { backtestLast, formatLast, scoreLast, LAST_LEDGER_PATH, type LastReport } from './review/backtest-last.js';
51
51
  export { buildRecord, recordLines, recordIntact, type ReviewRecord } from './review/reviewer/record.js';
52
- export { readLessons, writeLessons, decideLesson, acceptSuggestedText, lessonState, pendingDecision, matchLessons, lessonText, lessonsPath, type ReviewLesson, type LessonEvidence } from './review-learning/lessons.js';
52
+ export { readLessons, writeLessons, decideLesson, acceptSuggestedText, scopeLesson, lessonState, pendingDecision, matchLessons, lessonText, lessonsPath, type ReviewLesson, type LessonEvidence } from './review-learning/lessons.js';
53
53
  export { lessonsForDiff, lessonsSection, type LessonMode } from './review-learning/team-lessons.js';
54
54
  export { recordAgentWrites, captureHumanEdits } from './review-learning/human-edits.js';
55
55
  export { readRepoRules, rulesForDiff, rulesSection, splitRules, type RepoRule } from './review-learning/repo-rules.js';
package/dist/index.js CHANGED
@@ -47,7 +47,7 @@ export { decideCompiledCheck, proposeCompiledChecks, readCompiledChecks, suspens
47
47
  export { ruleWriterFor } from './review/reviewer/rule-writer.js';
48
48
  export { backtestLast, formatLast, scoreLast, LAST_LEDGER_PATH } from './review/backtest-last.js';
49
49
  export { buildRecord, recordLines, recordIntact } from './review/reviewer/record.js';
50
- export { readLessons, writeLessons, decideLesson, acceptSuggestedText, lessonState, pendingDecision, matchLessons, lessonText, lessonsPath } from './review-learning/lessons.js';
50
+ export { readLessons, writeLessons, decideLesson, acceptSuggestedText, scopeLesson, lessonState, pendingDecision, matchLessons, lessonText, lessonsPath } from './review-learning/lessons.js';
51
51
  export { lessonsForDiff, lessonsSection } from './review-learning/team-lessons.js';
52
52
  export { recordAgentWrites, captureHumanEdits } from './review-learning/human-edits.js';
53
53
  export { readRepoRules, rulesForDiff, rulesSection, splitRules } from './review-learning/repo-rules.js';
@@ -18,7 +18,7 @@ import type { Git, ReviewBody, ReviewComment } from './acted-on.js';
18
18
  * until a person promotes it.
19
19
  * A record from before evidence kinds has none: it is a `point`.
20
20
  */
21
- export type EvidenceKind = 'point' | 'outcome' | 'counter' | 'correction' | 'accepted' | 'rejected' | 'norule' | 'followup' | 'lines' | 'dismissed' | 'against' | 'demoted' | 'reclassified' | 'compiled' | 'reworded';
21
+ export type EvidenceKind = 'point' | 'outcome' | 'counter' | 'correction' | 'accepted' | 'rejected' | 'norule' | 'followup' | 'lines' | 'dismissed' | 'against' | 'demoted' | 'reclassified' | 'compiled' | 'reworded' | 'scoped';
22
22
  export interface LessonEvidence {
23
23
  kind?: EvidenceKind;
24
24
  pr: number;
@@ -49,6 +49,11 @@ export interface ReviewLesson {
49
49
  state: 'candidate' | 'verified' | 'rejected';
50
50
  /** Which evidence made it a lesson. */
51
51
  promotedBy?: 'outcome' | 'correction' | 'person' | 'recurrence' | 'legacy';
52
+ /**
53
+ * How far a person widened it (scopeLesson): `folder`, every change in the lesson's folder; `repo`, every change,
54
+ * a standard for the whole repository. Absent: its file (or, with no file, a standard served by its words).
55
+ */
56
+ scope?: 'folder' | 'repo';
52
57
  evidence: LessonEvidence[];
53
58
  /**
54
59
  * A corrected wording a newer version derived from the lesson's own comment, waiting for a person. A lesson
@@ -116,6 +121,11 @@ export declare function matchLessons(lessons: ReviewLesson[], change: ChangeShap
116
121
  perFile?: number;
117
122
  excludePr?: number;
118
123
  }): ReviewLesson[];
124
+ /**
125
+ * A person sets how far a lesson reaches: `repo` (every change), `folder` (every change in its folder) or `file` (its
126
+ * own file, as learned). Recorded as evidence with who and why; it says nothing about whether the lesson is right.
127
+ */
128
+ export declare function scopeLesson(cwd: string, id: string, scope: 'file' | 'folder' | 'repo', by: string, why?: string): ReviewLesson | undefined;
119
129
  /** RIGOUR_REVIEW_LESSONS points at a lessons file outside the clone (CI, or a team's shared copy). */
120
130
  export declare function lessonsPath(cwd: string): string;
121
131
  export declare function readLessons(cwd: string): ReviewLesson[];
@@ -28,6 +28,8 @@ const RECUR_PRS = 2;
28
28
  const RECUR_AUTHORS = 2;
29
29
  /** Team standards served with a change, on top of the lessons about its files (an agent's question; a judge takes more), and the words they must share with it. */
30
30
  const MAX_STANDARDS = 3;
31
+ /** Repository standards (scope `repo`) served with every change, most-raised first. */
32
+ const REPO_STANDARDS = 10;
31
33
  const STANDARD_WORDS = 2;
32
34
  /** Words that say nothing about what a rule or a change is about. */
33
35
  const PLAIN_WORDS = new Set(['when', 'that', 'this', 'with', 'from', 'before', 'after', 'every', 'their', 'them', 'than', 'each', 'only', 'into', 'rather', 'instead', 'should', 'make', 'keep', 'sure', 'does', 'what', 'which', 'there', 'these', 'those', 'such', 'more', 'other', 'same', 'then', 'also', 'both', 'must', 'never', 'always', 'once', 'been', 'have', 'will', 'your', 'about', 'over', 'under', 'change', 'changes', 'code', 'value', 'values', 'data', 'true', 'false', 'null', 'undefined', 'return', 'const', 'function', 'export', 'import', 'await', 'async', 'string', 'number', 'type']);
@@ -233,12 +235,20 @@ export function matchLessons(lessons, change, options = {}) {
233
235
  // A lesson whose only evidence is the pull request under review is already in front of the judge as the reviewer's own points.
234
236
  if (options.excludePr !== undefined)
235
237
  lessons = lessons.filter(l => !l.evidence.length || l.evidence.some(e => e.pr !== options.excludePr));
238
+ const inPlay = (l) => l.state === 'verified' || (!!options.includeCandidates && l.state === 'candidate');
239
+ // A person made these the repository's standards: every change gets them, whatever its files or words; most-raised first.
240
+ const repo = lessons
241
+ .filter(l => l.scope === 'repo' && inPlay(l))
242
+ .sort((a, b) => prCount(b) - prCount(a) || b.evidence.length - a.evidence.length)
243
+ .slice(0, REPO_STANDARDS);
244
+ lessons = lessons.filter(l => l.scope !== 'repo');
236
245
  const dirs = new Set(change.files.map(f => path.posix.dirname(f)));
246
+ const inFolder = (l) => l.scope === 'folder' && change.files.some(f => f.startsWith(`${path.posix.dirname(l.file)}/`));
237
247
  const scored = lessons
238
- .filter(l => !!l.file && (l.state === 'verified' || (options.includeCandidates && l.state === 'candidate')) && !NOT_CODE.test(l.file))
248
+ .filter(l => !!l.file && inPlay(l) && !NOT_CODE.test(l.file))
239
249
  .map(l => ({
240
250
  lesson: l,
241
- score: (change.files.includes(l.file) ? 3 : 0) + (dirs.has(path.posix.dirname(l.file)) ? 0.5 : 0)
251
+ score: (change.files.includes(l.file) || inFolder(l) ? 3 : 0) + (dirs.has(path.posix.dirname(l.file)) ? 0.5 : 0)
242
252
  + 2 * l.symbols.filter(s => isSpecific(s) && change.symbols.has(s)).length,
243
253
  }))
244
254
  .filter(s => s.score >= 3)
@@ -266,7 +276,32 @@ export function matchLessons(lessons, change, options = {}) {
266
276
  perFileCount.set(lesson.file, n + 1);
267
277
  taken.push(lesson);
268
278
  }
269
- return [...taken, ...standards];
279
+ return [...taken, ...repo, ...standards];
280
+ }
281
+ /** Pull requests a lesson was raised or acted on in. */
282
+ function prCount(lesson) {
283
+ return new Set(lesson.evidence.filter(e => (e.kind ?? 'point') === 'point').map(e => e.pr)).size;
284
+ }
285
+ /**
286
+ * A person sets how far a lesson reaches: `repo` (every change), `folder` (every change in its folder) or `file` (its
287
+ * own file, as learned). Recorded as evidence with who and why; it says nothing about whether the lesson is right.
288
+ */
289
+ export function scopeLesson(cwd, id, scope, by, why = '') {
290
+ const lessons = readLessons(cwd);
291
+ const lesson = lessons.find(l => l.id === id);
292
+ if (!lesson)
293
+ return undefined;
294
+ if (scope === 'folder' && !lesson.file)
295
+ throw new Error('a team standard has no folder: scope it to the repo or leave it');
296
+ const at = new Date().toISOString();
297
+ if (scope === 'file')
298
+ delete lesson.scope;
299
+ else
300
+ lesson.scope = scope;
301
+ lesson.evidence.push({ kind: 'scoped', pr: lesson.evidence[0]?.pr ?? 0, comment: `scoped-${at}`, author: by, detail: why ? `${scope}: ${why}` : scope, at });
302
+ lesson.updatedAt = at;
303
+ writeLessons(cwd, lessons);
304
+ return lesson;
270
305
  }
271
306
  /** RIGOUR_REVIEW_LESSONS points at a lessons file outside the clone (CI, or a team's shared copy). */
272
307
  export function lessonsPath(cwd) {
@@ -14,6 +14,7 @@ export interface LessonView {
14
14
  text: string;
15
15
  prs: number[];
16
16
  said?: string;
17
+ scope?: ReviewLesson['scope'];
17
18
  }
18
19
  export declare function lessonView(l: ReviewLesson): LessonView;
19
20
  /**
@@ -3,6 +3,7 @@
3
3
  * agent or a model) to check before the PR: "your team has asked for this
4
4
  * before".
5
5
  */
6
+ import path from 'path';
6
7
  import { parseDiff } from '../utils/diff.js';
7
8
  import { matchLessons, readLessons } from './lessons.js';
8
9
  /** Lessons a person or repeated evidence verified reach the reviewer unless a team turns them off. */
@@ -36,14 +37,16 @@ function changeShape(diff) {
36
37
  }
37
38
  export function lessonView(l) {
38
39
  const said = l.evidence.find(e => e.said && e.said !== l.text)?.said;
39
- return { file: l.file, text: l.text, prs: [...new Set(l.evidence.map(e => e.pr))], ...(said ? { said } : {}) };
40
+ return { file: l.file, text: l.text, prs: [...new Set(l.evidence.map(e => e.pr))], ...(said ? { said } : {}), ...(l.scope ? { scope: l.scope } : {}) };
40
41
  }
41
42
  /**
42
43
  * One line per lesson: where it applies, the rule, and, when a model wrote the rule from a person's
43
44
  * point, their own words, so anyone can see when the rule went beyond what was said.
44
45
  */
45
46
  export function describeLesson(l) {
46
- const where = l.file ? `${l.file}: ` : 'team standard: ';
47
+ const where = l.scope === 'repo' ? `team standard${l.file ? ` (learned on ${l.file})` : ''}: `
48
+ : l.scope === 'folder' ? `${path.posix.dirname(l.file)}/ (every file): `
49
+ : l.file ? `${l.file}: ` : 'team standard: ';
47
50
  const said = l.said ? ` (in their words: "${l.said}")` : '';
48
51
  const prs = l.prs.length ? ` (acted on in PR ${l.prs.map(p => `#${p}`).join(', ')})` : '';
49
52
  return `${where}${l.text}${said}${prs}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "6.11.2-rc.1",
3
+ "version": "6.12.0-rc.1",
4
4
  "description": "Rigour's review engine: deterministic gates on changed lines, rules and lessons learned from your team's fixes, and per-check precision from what you fix versus dismiss, across TypeScript, JavaScript, Python, Go, Ruby and C#.",
5
5
  "engines": {
6
6
  "node": ">=22.13"
@@ -75,11 +75,11 @@
75
75
  "@anthropic-ai/sdk": "^0.132.1",
76
76
  "pg": "^8.16.3",
77
77
  "openai": "^5.23.2",
78
- "@rigour-labs/brain-darwin-arm64": "6.11.2-rc.1",
79
- "@rigour-labs/brain-darwin-x64": "6.11.2-rc.1",
80
- "@rigour-labs/brain-linux-arm64": "6.11.2-rc.1",
81
- "@rigour-labs/brain-linux-x64": "6.11.2-rc.1",
82
- "@rigour-labs/brain-win-x64": "6.11.2-rc.1"
78
+ "@rigour-labs/brain-darwin-x64": "6.12.0-rc.1",
79
+ "@rigour-labs/brain-linux-x64": "6.12.0-rc.1",
80
+ "@rigour-labs/brain-linux-arm64": "6.12.0-rc.1",
81
+ "@rigour-labs/brain-darwin-arm64": "6.12.0-rc.1",
82
+ "@rigour-labs/brain-win-x64": "6.12.0-rc.1"
83
83
  },
84
84
  "devDependencies": {
85
85
  "@types/fs-extra": "^11.0.4",