@olegkoval/agent-skills 1.71.0 → 1.71.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/adapters/claude/olko-github-pr/skills/lekker-review/SKILL.md +1 -1
- package/adapters/claude/olko-github-pr/skills/lekker-review/references/agents/verifier.md +7 -1
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/project-rule-findings.mjs +51 -6
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +93 -5
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/setup-worktree.sh +22 -11
- package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/validate-lines.mjs +2 -0
- package/package.json +1 -1
- package/plugins/olko-apple-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-creative/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-garmin-kit/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-git-tools/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-github-pr/skills/lekker-review/SKILL.md +1 -1
- package/plugins/olko-github-pr/skills/lekker-review/references/agents/verifier.md +7 -1
- package/plugins/olko-github-pr/skills/lekker-review/scripts/project-rule-findings.mjs +51 -6
- package/plugins/olko-github-pr/skills/lekker-review/scripts/selftest.mjs +93 -5
- package/plugins/olko-github-pr/skills/lekker-review/scripts/setup-worktree.sh +22 -11
- package/plugins/olko-github-pr/skills/lekker-review/scripts/validate-lines.mjs +2 -0
- package/plugins/olko-github-pr/skills/lekker-review/workflow.js +24 -6
- package/plugins/olko-obsidian/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-product/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-reflection/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-release/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-skill-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/olko-web-ops/.claude-plugin/plugin.json +1 -1
|
@@ -476,7 +476,7 @@ LEKKER_RULES_ONLY=1 LEKKER_BASE_REF=origin/<BASE_BRANCH> \
|
|
|
476
476
|
```
|
|
477
477
|
|
|
478
478
|
Read `projectRules` from `rules.json` into `context.json`. This archive-only
|
|
479
|
-
mode fetches the PR base
|
|
479
|
+
mode fetches the PR base and reads `baseProjectRules` at the merge base with the PR branch (as full mode does), not at the base tip. If the base cannot be fetched, resolved or merge-based, `baseProjectRules` is `null` and `notes` carries `base-rules-unavailable`; the scan still succeeds. It does not run static checks.
|
|
480
480
|
|
|
481
481
|
Run via Bash with `run_in_background`:
|
|
482
482
|
|
|
@@ -44,7 +44,13 @@ For `rule: "weakens-project-rule"`, use this dedicated path:
|
|
|
44
44
|
`node <this prompt's directory>/../../scripts/project-rule-findings.mjs <finding.json> <CONTEXT_FILE>`.
|
|
45
45
|
The validator requires the exact quote to exist in `baseProjectRules`; a
|
|
46
46
|
missing base copy or rejected quote fails this check. Do not validate against
|
|
47
|
-
`projectRules`, which may already reflect the PR's weakened rules.
|
|
47
|
+
`projectRules`, which may already reflect the PR's weakened rules. The validator also
|
|
48
|
+
rejects a rule line that is still present unchanged in `projectRules` (the rule line was neither removed nor changed).
|
|
49
|
+
Exit 1 means `dropped`. Exit 2 means the validator could not run (for example
|
|
50
|
+
`baseProjectRules` is missing): check by hand that the exact quote appears in
|
|
51
|
+
`baseProjectRules` ONLY, never in `projectRules`; if `baseProjectRules` is not a string
|
|
52
|
+
or the quote is absent there, `dropped`. Otherwise keep the finding, say in `reasoning`
|
|
53
|
+
that the validator was unavailable, and set `validatorUnavailable: true`.
|
|
48
54
|
|
|
49
55
|
Both checks pass -> `confirmed` at the finding's Important severity. Either
|
|
50
56
|
check fails -> `dropped`. Do not downgrade a confirmed weakening.
|
package/adapters/claude/olko-github-pr/skills/lekker-review/scripts/project-rule-findings.mjs
CHANGED
|
@@ -41,9 +41,8 @@ const RECALLED_PATTERNS_HEADING = /^\s{0,3}#{1,6}\s+recalled patterns\s*$/gim
|
|
|
41
41
|
// SKILL.md appends that heading at the end; an earlier one inside a rules file must not drop the rules after it.
|
|
42
42
|
export function stripRecalledPatterns(text) {
|
|
43
43
|
const source = String(text ?? '')
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return cut === -1 ? source : source.slice(0, cut)
|
|
44
|
+
const last = [...source.matchAll(RECALLED_PATTERNS_HEADING)].at(-1)
|
|
45
|
+
return last ? source.slice(0, last.index) : source
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
export function quoteAppearsIn(quote, projectRules) {
|
|
@@ -71,12 +70,54 @@ export function validateProjectRuleFinding(finding, projectRules) {
|
|
|
71
70
|
return { ok: true, reason: 'quote found in projectRules' }
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
|
|
73
|
+
// Still present means the whole rule block is unchanged; any wording change in the block
|
|
74
|
+
// counts as possibly weakened and is left to the verifier.
|
|
75
|
+
const LIST_MARKER = /^\s*([-+*]|\d+[.)])\s+/
|
|
76
|
+
|
|
77
|
+
const indentOf = line => line.length - line.trimStart().length
|
|
78
|
+
|
|
79
|
+
// A deeper marker or indented text belongs to its parent bullet, even after a blank line (loose list),
|
|
80
|
+
// so an exception there must change the parent block.
|
|
81
|
+
const isHeading = line => /^\s{0,3}#{1,6}\s/.test(line)
|
|
82
|
+
const isListItem = line => LIST_MARKER.test(line)
|
|
83
|
+
|
|
84
|
+
function startsNewBlock(line, current) {
|
|
85
|
+
if (isHeading(line)) return true
|
|
86
|
+
const [first] = current
|
|
87
|
+
if (first === undefined) return true
|
|
88
|
+
if (current[current.length - 1] === '') return !(isListItem(first) && indentOf(line) > indentOf(first))
|
|
89
|
+
if (!isListItem(line)) return false
|
|
90
|
+
if (isListItem(first)) return indentOf(line) <= indentOf(first)
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ruleBlocks(text) {
|
|
95
|
+
const groups = stripRecalledPatterns(text).split(/\r?\n/).reduce((acc, line) => {
|
|
96
|
+
const current = acc[acc.length - 1]
|
|
97
|
+
if (line.trim() === '') return current === undefined || current.length === 0 ? acc : [...acc.slice(0, -1), [...current, '']]
|
|
98
|
+
if (current === undefined || startsNewBlock(line, current)) return [...acc, [line]]
|
|
99
|
+
return [...acc.slice(0, -1), [...current, line]]
|
|
100
|
+
}, [])
|
|
101
|
+
return groups
|
|
102
|
+
.map(lines => lines.map((line, i) => normalizeQuote(i === 0 ? line.replace(LIST_MARKER, '') : line)).filter(line => line !== '').join(' '))
|
|
103
|
+
.filter(block => block !== '')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function ruleLineStillPresent(quote, baseProjectRules, projectRules) {
|
|
107
|
+
const needle = normalizeQuote(quote)
|
|
108
|
+
const baseHits = needle ? ruleBlocks(baseProjectRules).filter(block => block.includes(needle)) : []
|
|
109
|
+
if (baseHits.length === 0) return false
|
|
110
|
+
const prBlocks = new Set(ruleBlocks(projectRules))
|
|
111
|
+
return baseHits.some(block => prBlocks.has(block))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// `unavailable` marks a missing input, not a judgement on the finding: the CLI maps it to exit 2.
|
|
115
|
+
export function validateWeakenedProjectRuleFinding(finding, baseProjectRules, projectRules) {
|
|
75
116
|
if (finding?.rule !== 'weakens-project-rule') {
|
|
76
117
|
return { ok: true, reason: 'not a weakened project rule' }
|
|
77
118
|
}
|
|
78
119
|
if (typeof baseProjectRules !== 'string') {
|
|
79
|
-
return { ok: false, reason: 'baseProjectRules unavailable' }
|
|
120
|
+
return { ok: false, unavailable: true, reason: 'baseProjectRules unavailable' }
|
|
80
121
|
}
|
|
81
122
|
if (typeof finding.ruleQuote !== 'string' || finding.ruleQuote.trim() === '') {
|
|
82
123
|
return { ok: false, reason: 'missing ruleQuote: quote the removed project rule' }
|
|
@@ -84,6 +125,9 @@ export function validateWeakenedProjectRuleFinding(finding, baseProjectRules) {
|
|
|
84
125
|
if (!quoteAppearsIn(finding.ruleQuote, baseProjectRules)) {
|
|
85
126
|
return { ok: false, reason: 'ruleQuote not found in baseProjectRules' }
|
|
86
127
|
}
|
|
128
|
+
if (typeof projectRules === 'string' && ruleLineStillPresent(finding.ruleQuote, baseProjectRules, projectRules)) {
|
|
129
|
+
return { ok: false, reason: 'rule still present in projectRules' }
|
|
130
|
+
}
|
|
87
131
|
return { ok: true, reason: 'quote found in baseProjectRules' }
|
|
88
132
|
}
|
|
89
133
|
|
|
@@ -97,8 +141,9 @@ function main(argv) {
|
|
|
97
141
|
const finding = JSON.parse(readFileSync(findingPath, 'utf8'))
|
|
98
142
|
const context = JSON.parse(readFileSync(contextPath, 'utf8'))
|
|
99
143
|
if (finding?.rule === 'weakens-project-rule') {
|
|
100
|
-
const result = validateWeakenedProjectRuleFinding(finding, context.baseProjectRules)
|
|
144
|
+
const result = validateWeakenedProjectRuleFinding(finding, context.baseProjectRules, context.projectRules)
|
|
101
145
|
console.log(JSON.stringify(result))
|
|
146
|
+
if (result.unavailable) return 2
|
|
102
147
|
return result.ok ? 0 : 1
|
|
103
148
|
}
|
|
104
149
|
// Base-branch rules are authoritative; PR-tree rules only when no base was resolved.
|
|
@@ -52,11 +52,13 @@ const preamble = [
|
|
|
52
52
|
"const SEVERITY_RANK = { observation: 0, idiomatic: 1, important: 2, critical: 3 }",
|
|
53
53
|
// hardRuleCorroborated is likewise gone from the current file and kept in this
|
|
54
54
|
// list for the same reason: an older workflow.js calls it from isHardRule.
|
|
55
|
-
...['titleTokens', 'sameIssue', 'nearbyLines', 'spanWithinWindow', 'hardRuleCorroborated',
|
|
55
|
+
...['titleTokens', 'normalizeRuleQuote', 'weakenedRuleSeverity', 'sameIssue', 'nearbyLines', 'spanWithinWindow', 'hardRuleCorroborated',
|
|
56
56
|
'isHardRule', 'longest', 'mergeFindings', 'dedup', 'shouldVerify', 'isProveAttempt', 'countProveAttempt'].map(n => {
|
|
57
57
|
// countProveAttempt is called directly by the checks, so its stub returns NaN and the
|
|
58
58
|
// checks FAIL with a diff instead of the run crashing on an older workflow.js.
|
|
59
59
|
try { return lift(n) } catch {
|
|
60
|
+
if (n === 'normalizeRuleQuote') return "function normalizeRuleQuote() { return 'absent from this workflow.js' }"
|
|
61
|
+
if (n === 'weakenedRuleSeverity') return "function weakenedRuleSeverity() { return 'absent from this workflow.js' }"
|
|
60
62
|
return n === 'countProveAttempt'
|
|
61
63
|
? 'function countProveAttempt() { return NaN }'
|
|
62
64
|
: `function ${n}() { throw new Error('${n} absent from this workflow.js') }`
|
|
@@ -64,8 +66,8 @@ const preamble = [
|
|
|
64
66
|
}),
|
|
65
67
|
].join('\n')
|
|
66
68
|
|
|
67
|
-
const { dedup, isHardRule, shouldVerify, sameIssue, isProveAttempt, countProveAttempt } =
|
|
68
|
-
new Function(preamble + '\nreturn { dedup, isHardRule, shouldVerify, sameIssue, isProveAttempt, countProveAttempt }')()
|
|
69
|
+
const { dedup, isHardRule, shouldVerify, sameIssue, isProveAttempt, countProveAttempt, normalizeRuleQuote, weakenedRuleSeverity } =
|
|
70
|
+
new Function(preamble + '\nreturn { dedup, isHardRule, shouldVerify, sameIssue, isProveAttempt, countProveAttempt, normalizeRuleQuote, weakenedRuleSeverity }')()
|
|
69
71
|
|
|
70
72
|
let failed = 0
|
|
71
73
|
function check(name, actual, expected) {
|
|
@@ -700,7 +702,7 @@ console.log('\nproject rules: PROJECT:<slug> findings must quote a rule that exi
|
|
|
700
702
|
const { spawnSync: spawnPr } = await import('node:child_process')
|
|
701
703
|
const { mkdtempSync: mkPr, cpSync: cpPr, readFileSync: rdPr, rmSync: rmPr, symlinkSync: syPr, writeFileSync: wfPr } = await import('node:fs')
|
|
702
704
|
const { tmpdir: tmpPr } = await import('node:os')
|
|
703
|
-
const { validateProjectRuleFinding, validateWeakenedProjectRuleFinding, isProjectRuleTag } =
|
|
705
|
+
const { validateProjectRuleFinding, validateWeakenedProjectRuleFinding, isProjectRuleTag, normalizeQuote } =
|
|
704
706
|
await import('./project-rule-findings.mjs')
|
|
705
707
|
|
|
706
708
|
const fixtureDir = join(SKILL, 'scripts', 'fixtures', 'project-rules')
|
|
@@ -740,6 +742,65 @@ check('rejects a weakened rule quote absent from baseProjectRules',
|
|
|
740
742
|
validateWeakenedProjectRuleFinding(weakenedFinding, 'NEVER call eval.').ok, false)
|
|
741
743
|
check('rejects a weakened rule when baseProjectRules is unavailable',
|
|
742
744
|
validateWeakenedProjectRuleFinding(weakenedFinding, undefined).ok, false)
|
|
745
|
+
check('rejects a weakened rule whose quote is still in projectRules',
|
|
746
|
+
validateWeakenedProjectRuleFinding(weakenedFinding, planted, planted).ok, false)
|
|
747
|
+
check('accepts a weakened rule whose quote is gone from projectRules',
|
|
748
|
+
validateWeakenedProjectRuleFinding(weakenedFinding, planted, 'nothing here').ok, true)
|
|
749
|
+
const evalBase = '- NEVER call eval in src'
|
|
750
|
+
const evalFinding = prFinding('weakens-project-rule', 'NEVER call eval in src')
|
|
751
|
+
check('accepts a weakening that appends an exception to the rule line',
|
|
752
|
+
validateWeakenedProjectRuleFinding(evalFinding, evalBase, '- NEVER call eval in src unless input is trusted').ok, true)
|
|
753
|
+
check('accepts a weakening that adds a period and an Exception sentence on the same line',
|
|
754
|
+
validateWeakenedProjectRuleFinding(evalFinding, evalBase, '- NEVER call eval in src. Exception: trusted input.').ok, true)
|
|
755
|
+
check('rejects a weakened rule whose line is unchanged among other lines',
|
|
756
|
+
validateWeakenedProjectRuleFinding(evalFinding, evalBase, '# Rules\n- NEVER call eval in src\n- Be kind').reason, 'rule still present in projectRules')
|
|
757
|
+
const wrappedBase = '- After ANY catalog change, run build and commit\n the result'
|
|
758
|
+
const wrappedFinding = prFinding('weakens-project-rule', 'After ANY catalog change, run build and commit the result')
|
|
759
|
+
check('accepts a weakening appended to the last line of a wrapped rule',
|
|
760
|
+
validateWeakenedProjectRuleFinding(wrappedFinding, wrappedBase, '- After ANY catalog change, run build and commit\n the result, except docs-only changes').ok, true)
|
|
761
|
+
check('rejects an unchanged wrapped rule',
|
|
762
|
+
validateWeakenedProjectRuleFinding(wrappedFinding, wrappedBase, wrappedBase).reason, 'rule still present in projectRules')
|
|
763
|
+
check('rejects a wrapped rule re-wrapped at a different column',
|
|
764
|
+
validateWeakenedProjectRuleFinding(wrappedFinding, wrappedBase, '- After ANY catalog change, run build\n and commit the result').reason, 'rule still present in projectRules')
|
|
765
|
+
check('rejects a rule whose bullet marker changed to an asterisk',
|
|
766
|
+
validateWeakenedProjectRuleFinding(evalFinding, evalBase, '* NEVER call eval in src').reason, 'rule still present in projectRules')
|
|
767
|
+
check('rejects an unchanged numbered rule',
|
|
768
|
+
validateWeakenedProjectRuleFinding(evalFinding, '1. NEVER call eval in src', '1. NEVER call eval in src').reason, 'rule still present in projectRules')
|
|
769
|
+
check('rejects a bullet rule renumbered in the PR',
|
|
770
|
+
validateWeakenedProjectRuleFinding(evalFinding, evalBase, '1. NEVER call eval in src').reason, 'rule still present in projectRules')
|
|
771
|
+
check('accepts a weakening that adds a prefix qualifier before the quote',
|
|
772
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), '- NEVER force-push', '- Except on your own branch, NEVER force-push').ok, true)
|
|
773
|
+
check('accepts a weakening added on a new continuation line of the same bullet',
|
|
774
|
+
validateWeakenedProjectRuleFinding(wrappedFinding, wrappedBase + '.', wrappedBase + '.\n Docs-only changes are exempt.').ok, true)
|
|
775
|
+
check('rejects a single-line rule re-wrapped over two lines',
|
|
776
|
+
validateWeakenedProjectRuleFinding(wrappedFinding, '- After ANY catalog change, run build and commit the result', wrappedBase).reason, 'rule still present in projectRules')
|
|
777
|
+
check('rejects a rule moved to another section unchanged',
|
|
778
|
+
validateWeakenedProjectRuleFinding(evalFinding, '# A\n' + evalBase + '\n# B\n- x', '# B\n- x\n# A\n' + evalBase).reason, 'rule still present in projectRules')
|
|
779
|
+
check('accepts an exception appended to a two-line blockquote rule',
|
|
780
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push to main or release branches.'), '> NEVER force-push to main\n> or release branches.', '> NEVER force-push to main\n> or release branches. Exception: your own branch.').ok, true)
|
|
781
|
+
check('accepts an exception added as an indented sub-bullet',
|
|
782
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), '- NEVER force-push', '- NEVER force-push\n - except on your own branch').ok, true)
|
|
783
|
+
check('accepts a parent bullet narrowed above an unchanged sub-bullet',
|
|
784
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER call eval'), '- In src:\n - NEVER call eval', '- In src/legacy only:\n - NEVER call eval').ok, true)
|
|
785
|
+
check('accepts a quote spanning two base bullets when the second gains an exception',
|
|
786
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push\n- NEVER delete main'), '- NEVER force-push\n- NEVER delete main', '- NEVER force-push\n- NEVER delete main unless asked').ok, true)
|
|
787
|
+
check('rejects an unchanged rule with an unchanged nested sub-bullet',
|
|
788
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), '- NEVER force-push\n - on shared branches', '- NEVER force-push\n - on shared branches').reason, 'rule still present in projectRules')
|
|
789
|
+
check('accepts an exception sub-bullet added after a blank line in a loose list',
|
|
790
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), '- NEVER force-push', '- NEVER force-push\n\n - except on your own branch').ok, true)
|
|
791
|
+
check('accepts an indented exception paragraph added after a blank line',
|
|
792
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), '- NEVER force-push', '- NEVER force-push\n\n Except on your own branch.').ok, true)
|
|
793
|
+
const introBase = 'These apply everywhere.\n- In domain code:\n - NEVER mutate state'
|
|
794
|
+
check('accepts a narrowed parent bullet under an intro paragraph',
|
|
795
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER mutate state'), introBase, 'These apply everywhere.\n- In domain code, except scripts:\n - NEVER mutate state').ok, true)
|
|
796
|
+
check('accepts an exception sibling added under a parent bullet below an intro paragraph',
|
|
797
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER mutate state'), introBase, introBase + '\n - Mutation is fine in reducers').ok, true)
|
|
798
|
+
check('rejects an unchanged loose-list rule with an unchanged indented paragraph',
|
|
799
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), '- NEVER force-push\n\n on shared branches', '- NEVER force-push\n\n on shared branches').reason, 'rule still present in projectRules')
|
|
800
|
+
check('rejects unchanged blank-separated sibling bullets when another bullet is edited',
|
|
801
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), '- NEVER force-push\n\n- NEVER call eval', '- NEVER force-push\n\n- NEVER call eval in src').reason, 'rule still present in projectRules')
|
|
802
|
+
check('rejects an unchanged bullet under a lead-in when a sibling is edited',
|
|
803
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), 'Rules:\n- NEVER force-push\n- NEVER call eval', 'Rules:\n- NEVER force-push\n- NEVER call eval, except tests').reason, 'rule still present in projectRules')
|
|
743
804
|
check('rejects a weakened rule with no quote',
|
|
744
805
|
validateWeakenedProjectRuleFinding({ rule: 'weakens-project-rule' }, planted).ok, false)
|
|
745
806
|
const verifierInstructions = rdPr(join(SKILL, 'references', 'agents', 'verifier.md'), 'utf8')
|
|
@@ -772,6 +833,25 @@ const quoteA = { file: 'src/total.js', line: 2, title: 'Missing null check on to
|
|
|
772
833
|
const quoteB = { ...quoteA, ruleQuote: 'ALWAYS validate total before use.' }
|
|
773
834
|
check('two different quotes of one PROJECT rule are not sameIssue', sameIssue(quoteA, quoteB), false)
|
|
774
835
|
check('the same quote of one PROJECT rule is still sameIssue', sameIssue(quoteA, { ...quoteA }), true)
|
|
836
|
+
check('the same slug with quotes equal after normalisation is sameIssue',
|
|
837
|
+
sameIssue(quoteA, { ...quoteA, ruleQuote: ' never SKIP the null check on total. ' }), true)
|
|
838
|
+
const slugB = { ...quoteA, rule: 'PROJECT:null-total-guard', ruleQuote: '**Never skip** the null check on total.' }
|
|
839
|
+
check('two PROJECT slugs with one quote modulo case, whitespace and emphasis are sameIssue', sameIssue(quoteA, slugB), true)
|
|
840
|
+
const [mergedSlugs] = dedup([{ ...quoteA, severity: 'important' }, { ...slugB, severity: 'important' }])
|
|
841
|
+
check('different-slug same-quote findings merge into one finding that keeps rule and quote',
|
|
842
|
+
[dedup([{ ...quoteA, severity: 'important' }, { ...slugB, severity: 'important' }]).length, mergedSlugs.rule, typeof mergedSlugs.ruleQuote],
|
|
843
|
+
[1, 'PROJECT:no-null-total', 'string'])
|
|
844
|
+
check('different-slug different-quote findings stay separate',
|
|
845
|
+
sameIssue(quoteA, { ...slugB, ruleQuote: 'ALWAYS validate total before use.' }), false)
|
|
846
|
+
const quoteSamples = ['NEVER call `console.log` in src/.', '> **Never** skip\n the check', '- Use _early_ returns', '## Heading rule text']
|
|
847
|
+
check('workflow normalizeRuleQuote agrees with the validator normalizeQuote',
|
|
848
|
+
quoteSamples.map(q => normalizeRuleQuote(q)), quoteSamples.map(q => normalizeQuote(q)))
|
|
849
|
+
check('quotes differing only by trailing punctuation are not sameIssue',
|
|
850
|
+
sameIssue(quoteA, { ...quoteA, ruleQuote: 'NEVER skip the null check on total' }), false)
|
|
851
|
+
console.log('\nworkflow: a confirmed weakened-rule finding is always Important')
|
|
852
|
+
check('critical from the lens becomes important for weakens-project-rule',
|
|
853
|
+
weakenedRuleSeverity({ rule: 'weakens-project-rule' }, 'critical'), 'important')
|
|
854
|
+
check('other rules keep the severity', weakenedRuleSeverity({ rule: 'TS-1' }, 'critical'), 'critical')
|
|
775
855
|
|
|
776
856
|
// Prompt guard: every lens that reads projectRules is told to raise the tag.
|
|
777
857
|
const lensFiles = ['quality', 'implementation', 'simplification', 'conventions', 'test-quality']
|
|
@@ -787,9 +867,11 @@ function liftBlock(name) {
|
|
|
787
867
|
if (!m) throw new Error(`selftest: const ${name} not found in workflow.js`)
|
|
788
868
|
return m[0]
|
|
789
869
|
}
|
|
870
|
+
// An older workflow.js lacks these constants; fall back so the checks FAIL instead of the suite crashing.
|
|
871
|
+
const liftBlockOr = name => { try { return liftBlock(name) } catch { return `const ${name} = ''\n` } }
|
|
790
872
|
const buildPrompt = new Function(
|
|
791
873
|
'dim', 'SUPPRESSED', 'prevSha',
|
|
792
|
-
[
|
|
874
|
+
[liftBlockOr('PROJECT_RULE_DIMENSIONS'), liftBlockOr('PROJECT_RULE_INSTRUCTION'), liftConst('BUG_CLASS_INSTRUCTION'),
|
|
793
875
|
`const targetMetadata = '{}', promptDir = 'p', repoSlug = 'o/r', prNumber = 1, prUrl = 'u',
|
|
794
876
|
diffFile = 'd', contextFile = 'c', wtDisplay = 'w'`,
|
|
795
877
|
lift('reviewPrompt'), 'return reviewPrompt(dim)'].join('\n'),
|
|
@@ -846,6 +928,12 @@ check('validator accepts a weakened-rule quote found in baseProjectRules',
|
|
|
846
928
|
check('validator rejects a weakened-rule quote found only in projectRules',
|
|
847
929
|
runV(weakenedFile, addedCtx), 1)
|
|
848
930
|
wfPr(join(crashDir, 'rej.json'), JSON.stringify({ rule: 'PROJECT:no-eval', ruleQuote: 'NEVER call eval anywhere in code' }))
|
|
931
|
+
const noBaseCtx = join(crashDir, 'nobase.json')
|
|
932
|
+
wfPr(noBaseCtx, JSON.stringify({ projectRules: 'nothing here' }))
|
|
933
|
+
check('exit 2 on a weakened finding when baseProjectRules is missing', runV(weakenedFile, noBaseCtx), 2)
|
|
934
|
+
const stillCtx = join(crashDir, 'still.json')
|
|
935
|
+
wfPr(stillCtx, JSON.stringify({ projectRules: 'NEVER call console.log in src/.', baseProjectRules: 'NEVER call console.log in src/.' }))
|
|
936
|
+
check('exit 1 on a weakened finding whose rule is still in projectRules', runV(weakenedFile, stillCtx), 1)
|
|
849
937
|
check('exit 1 on a rejected finding', runV(join(crashDir, 'rej.json'), okCtx), 1)
|
|
850
938
|
const symlinkValidator = join(crashDir, 'project-rule-findings.mjs')
|
|
851
939
|
syPr(join(SKILL, 'scripts', 'project-rule-findings.mjs'), symlinkValidator)
|
|
@@ -157,24 +157,33 @@ if [[ "${LEKKER_RULES_ONLY:-}" == "1" ]]; then
|
|
|
157
157
|
if git -C "$REPO_ROOT" fetch origin "+refs/heads/${PR_BRANCH}:refs/remotes/origin/${PR_BRANCH}" 2>/dev/null; then
|
|
158
158
|
RULES_REF="origin/$PR_BRANCH"
|
|
159
159
|
elif git -C "$REPO_ROOT" fetch origin "$PR_BRANCH" 2>/dev/null; then
|
|
160
|
-
|
|
160
|
+
# The base fetch below overwrites FETCH_HEAD, so pin the PR tip now.
|
|
161
|
+
RULES_REF="$(git -C "$REPO_ROOT" rev-parse FETCH_HEAD)"
|
|
161
162
|
else
|
|
162
163
|
printf 'ERROR: could not fetch PR branch %s for rules-only scan\n' "$PR_BRANCH" >&2
|
|
163
164
|
exit 1
|
|
164
165
|
fi
|
|
165
166
|
fi
|
|
167
|
+
# Base rules are best effort: a failure here keeps projectRules and nulls baseProjectRules,
|
|
168
|
+
# because the base copy only hardens validation and must not abort the scan.
|
|
169
|
+
RULES_BASE_SHA=""
|
|
166
170
|
if [[ -n "${LEKKER_BASE_REF:-}" ]]; then
|
|
171
|
+
BASE_FETCH_OK=1
|
|
167
172
|
if [[ -z "$LOCAL_REPO" && "$LEKKER_BASE_REF" == origin/* ]]; then
|
|
168
173
|
BASE_BRANCH="${LEKKER_BASE_REF#origin/}"
|
|
169
|
-
|
|
170
|
-
"+refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH}" 2>/dev/null
|
|
171
|
-
printf 'ERROR: could not fetch base ref %s for rules-only scan\n' "$LEKKER_BASE_REF" >&2
|
|
172
|
-
exit 1
|
|
173
|
-
fi
|
|
174
|
+
git -C "$REPO_ROOT" fetch origin \
|
|
175
|
+
"+refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH}" 2>/dev/null || BASE_FETCH_OK=0
|
|
174
176
|
fi
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
# Fork point, as in full mode: the base tip may carry rules added after the PR branched.
|
|
178
|
+
if [[ "$BASE_FETCH_OK" == "1" ]] \
|
|
179
|
+
&& git -C "$REPO_ROOT" rev-parse --verify -q "${LEKKER_BASE_REF}^{commit}" >/dev/null; then
|
|
180
|
+
RULES_BASE_SHA="$(git -C "$REPO_ROOT" merge-base "$LEKKER_BASE_REF" "$RULES_REF" 2>/dev/null || true)"
|
|
181
|
+
# A fork older than the shallow clone depth has no common ancestor until history is deepened.
|
|
182
|
+
if [[ -z "$RULES_BASE_SHA" ]] \
|
|
183
|
+
&& [[ "$(git -C "$REPO_ROOT" rev-parse --is-shallow-repository 2>/dev/null)" == "true" ]]; then
|
|
184
|
+
git -C "$REPO_ROOT" fetch -q --deepen=200 origin 2>/dev/null || true
|
|
185
|
+
RULES_BASE_SHA="$(git -C "$REPO_ROOT" merge-base "$LEKKER_BASE_REF" "$RULES_REF" 2>/dev/null || true)"
|
|
186
|
+
fi
|
|
178
187
|
fi
|
|
179
188
|
fi
|
|
180
189
|
RULES_SOURCE="/tmp/lekker-rules-source-$$"
|
|
@@ -184,17 +193,19 @@ if [[ "${LEKKER_RULES_ONLY:-}" == "1" ]]; then
|
|
|
184
193
|
BASE_PROJECT_RULES=""
|
|
185
194
|
HAVE_BASE_RULES=0
|
|
186
195
|
if [[ -n "${LEKKER_BASE_REF:-}" ]]; then
|
|
187
|
-
if
|
|
196
|
+
if [[ -n "$RULES_BASE_SHA" ]] \
|
|
197
|
+
&& BASE_PROJECT_RULES="$(collect_project_rules_at_ref "$REPO_ROOT" "$RULES_BASE_SHA")"; then
|
|
188
198
|
HAVE_BASE_RULES=1
|
|
189
199
|
else
|
|
190
200
|
BASE_PROJECT_RULES=""
|
|
191
201
|
NOTES="${NOTES}base-rules-unavailable;"
|
|
192
202
|
fi
|
|
193
203
|
fi
|
|
194
|
-
python3 - "$OUT_JSON" "$PROJECT_RULES" "$BASE_PROJECT_RULES" "$HAVE_BASE_RULES" <<'PYEOF'
|
|
204
|
+
python3 - "$OUT_JSON" "$PROJECT_RULES" "$BASE_PROJECT_RULES" "$HAVE_BASE_RULES" "$NOTES" <<'PYEOF'
|
|
195
205
|
import json, sys
|
|
196
206
|
out = {"projectRules": sys.argv[2]}
|
|
197
207
|
out["baseProjectRules"] = sys.argv[3] if sys.argv[4] == "1" else None
|
|
208
|
+
out["notes"] = sys.argv[5]
|
|
198
209
|
with open(sys.argv[1], "w") as f:
|
|
199
210
|
json.dump(out, f, indent=2)
|
|
200
211
|
PYEOF
|
|
@@ -75,6 +75,8 @@ export function loadDedup(workflowPath = join(SKILL, 'workflow.js')) {
|
|
|
75
75
|
liftConst('HARD_RULES', 'const HARD_RULES = new Set()'),
|
|
76
76
|
liftConst('SAME_ISSUE_LINE_WINDOW', 'const SAME_ISSUE_LINE_WINDOW = 30'),
|
|
77
77
|
liftConst('SEVERITY_RANK', 'const SEVERITY_RANK = { critical: 3, important: 2, observation: 1, idiomatic: 0 }'),
|
|
78
|
+
// Absent from an older workflow.js, whose sameIssue does not call it.
|
|
79
|
+
(() => { try { return lift('normalizeRuleQuote') } catch { return '' } })(),
|
|
78
80
|
...['titleTokens', 'sameIssue', 'nearbyLines', 'spanWithinWindow',
|
|
79
81
|
'isHardRule', 'longest', 'mergeFindings', 'dedup'].map(lift),
|
|
80
82
|
].join('\n')
|
package/package.json
CHANGED
|
@@ -475,7 +475,7 @@ LEKKER_RULES_ONLY=1 LEKKER_BASE_REF=origin/<BASE_BRANCH> \
|
|
|
475
475
|
```
|
|
476
476
|
|
|
477
477
|
Read `projectRules` from `rules.json` into `context.json`. This archive-only
|
|
478
|
-
mode fetches the PR base
|
|
478
|
+
mode fetches the PR base and reads `baseProjectRules` at the merge base with the PR branch (as full mode does), not at the base tip. If the base cannot be fetched, resolved or merge-based, `baseProjectRules` is `null` and `notes` carries `base-rules-unavailable`; the scan still succeeds. It does not run static checks.
|
|
479
479
|
|
|
480
480
|
Run via Bash with `run_in_background`:
|
|
481
481
|
|
|
@@ -44,7 +44,13 @@ For `rule: "weakens-project-rule"`, use this dedicated path:
|
|
|
44
44
|
`node <this prompt's directory>/../../scripts/project-rule-findings.mjs <finding.json> <CONTEXT_FILE>`.
|
|
45
45
|
The validator requires the exact quote to exist in `baseProjectRules`; a
|
|
46
46
|
missing base copy or rejected quote fails this check. Do not validate against
|
|
47
|
-
`projectRules`, which may already reflect the PR's weakened rules.
|
|
47
|
+
`projectRules`, which may already reflect the PR's weakened rules. The validator also
|
|
48
|
+
rejects a rule line that is still present unchanged in `projectRules` (the rule line was neither removed nor changed).
|
|
49
|
+
Exit 1 means `dropped`. Exit 2 means the validator could not run (for example
|
|
50
|
+
`baseProjectRules` is missing): check by hand that the exact quote appears in
|
|
51
|
+
`baseProjectRules` ONLY, never in `projectRules`; if `baseProjectRules` is not a string
|
|
52
|
+
or the quote is absent there, `dropped`. Otherwise keep the finding, say in `reasoning`
|
|
53
|
+
that the validator was unavailable, and set `validatorUnavailable: true`.
|
|
48
54
|
|
|
49
55
|
Both checks pass -> `confirmed` at the finding's Important severity. Either
|
|
50
56
|
check fails -> `dropped`. Do not downgrade a confirmed weakening.
|
|
@@ -41,9 +41,8 @@ const RECALLED_PATTERNS_HEADING = /^\s{0,3}#{1,6}\s+recalled patterns\s*$/gim
|
|
|
41
41
|
// SKILL.md appends that heading at the end; an earlier one inside a rules file must not drop the rules after it.
|
|
42
42
|
export function stripRecalledPatterns(text) {
|
|
43
43
|
const source = String(text ?? '')
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return cut === -1 ? source : source.slice(0, cut)
|
|
44
|
+
const last = [...source.matchAll(RECALLED_PATTERNS_HEADING)].at(-1)
|
|
45
|
+
return last ? source.slice(0, last.index) : source
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
export function quoteAppearsIn(quote, projectRules) {
|
|
@@ -71,12 +70,54 @@ export function validateProjectRuleFinding(finding, projectRules) {
|
|
|
71
70
|
return { ok: true, reason: 'quote found in projectRules' }
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
|
|
73
|
+
// Still present means the whole rule block is unchanged; any wording change in the block
|
|
74
|
+
// counts as possibly weakened and is left to the verifier.
|
|
75
|
+
const LIST_MARKER = /^\s*([-+*]|\d+[.)])\s+/
|
|
76
|
+
|
|
77
|
+
const indentOf = line => line.length - line.trimStart().length
|
|
78
|
+
|
|
79
|
+
// A deeper marker or indented text belongs to its parent bullet, even after a blank line (loose list),
|
|
80
|
+
// so an exception there must change the parent block.
|
|
81
|
+
const isHeading = line => /^\s{0,3}#{1,6}\s/.test(line)
|
|
82
|
+
const isListItem = line => LIST_MARKER.test(line)
|
|
83
|
+
|
|
84
|
+
function startsNewBlock(line, current) {
|
|
85
|
+
if (isHeading(line)) return true
|
|
86
|
+
const [first] = current
|
|
87
|
+
if (first === undefined) return true
|
|
88
|
+
if (current[current.length - 1] === '') return !(isListItem(first) && indentOf(line) > indentOf(first))
|
|
89
|
+
if (!isListItem(line)) return false
|
|
90
|
+
if (isListItem(first)) return indentOf(line) <= indentOf(first)
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ruleBlocks(text) {
|
|
95
|
+
const groups = stripRecalledPatterns(text).split(/\r?\n/).reduce((acc, line) => {
|
|
96
|
+
const current = acc[acc.length - 1]
|
|
97
|
+
if (line.trim() === '') return current === undefined || current.length === 0 ? acc : [...acc.slice(0, -1), [...current, '']]
|
|
98
|
+
if (current === undefined || startsNewBlock(line, current)) return [...acc, [line]]
|
|
99
|
+
return [...acc.slice(0, -1), [...current, line]]
|
|
100
|
+
}, [])
|
|
101
|
+
return groups
|
|
102
|
+
.map(lines => lines.map((line, i) => normalizeQuote(i === 0 ? line.replace(LIST_MARKER, '') : line)).filter(line => line !== '').join(' '))
|
|
103
|
+
.filter(block => block !== '')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function ruleLineStillPresent(quote, baseProjectRules, projectRules) {
|
|
107
|
+
const needle = normalizeQuote(quote)
|
|
108
|
+
const baseHits = needle ? ruleBlocks(baseProjectRules).filter(block => block.includes(needle)) : []
|
|
109
|
+
if (baseHits.length === 0) return false
|
|
110
|
+
const prBlocks = new Set(ruleBlocks(projectRules))
|
|
111
|
+
return baseHits.some(block => prBlocks.has(block))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// `unavailable` marks a missing input, not a judgement on the finding: the CLI maps it to exit 2.
|
|
115
|
+
export function validateWeakenedProjectRuleFinding(finding, baseProjectRules, projectRules) {
|
|
75
116
|
if (finding?.rule !== 'weakens-project-rule') {
|
|
76
117
|
return { ok: true, reason: 'not a weakened project rule' }
|
|
77
118
|
}
|
|
78
119
|
if (typeof baseProjectRules !== 'string') {
|
|
79
|
-
return { ok: false, reason: 'baseProjectRules unavailable' }
|
|
120
|
+
return { ok: false, unavailable: true, reason: 'baseProjectRules unavailable' }
|
|
80
121
|
}
|
|
81
122
|
if (typeof finding.ruleQuote !== 'string' || finding.ruleQuote.trim() === '') {
|
|
82
123
|
return { ok: false, reason: 'missing ruleQuote: quote the removed project rule' }
|
|
@@ -84,6 +125,9 @@ export function validateWeakenedProjectRuleFinding(finding, baseProjectRules) {
|
|
|
84
125
|
if (!quoteAppearsIn(finding.ruleQuote, baseProjectRules)) {
|
|
85
126
|
return { ok: false, reason: 'ruleQuote not found in baseProjectRules' }
|
|
86
127
|
}
|
|
128
|
+
if (typeof projectRules === 'string' && ruleLineStillPresent(finding.ruleQuote, baseProjectRules, projectRules)) {
|
|
129
|
+
return { ok: false, reason: 'rule still present in projectRules' }
|
|
130
|
+
}
|
|
87
131
|
return { ok: true, reason: 'quote found in baseProjectRules' }
|
|
88
132
|
}
|
|
89
133
|
|
|
@@ -97,8 +141,9 @@ function main(argv) {
|
|
|
97
141
|
const finding = JSON.parse(readFileSync(findingPath, 'utf8'))
|
|
98
142
|
const context = JSON.parse(readFileSync(contextPath, 'utf8'))
|
|
99
143
|
if (finding?.rule === 'weakens-project-rule') {
|
|
100
|
-
const result = validateWeakenedProjectRuleFinding(finding, context.baseProjectRules)
|
|
144
|
+
const result = validateWeakenedProjectRuleFinding(finding, context.baseProjectRules, context.projectRules)
|
|
101
145
|
console.log(JSON.stringify(result))
|
|
146
|
+
if (result.unavailable) return 2
|
|
102
147
|
return result.ok ? 0 : 1
|
|
103
148
|
}
|
|
104
149
|
// Base-branch rules are authoritative; PR-tree rules only when no base was resolved.
|
|
@@ -52,11 +52,13 @@ const preamble = [
|
|
|
52
52
|
"const SEVERITY_RANK = { observation: 0, idiomatic: 1, important: 2, critical: 3 }",
|
|
53
53
|
// hardRuleCorroborated is likewise gone from the current file and kept in this
|
|
54
54
|
// list for the same reason: an older workflow.js calls it from isHardRule.
|
|
55
|
-
...['titleTokens', 'sameIssue', 'nearbyLines', 'spanWithinWindow', 'hardRuleCorroborated',
|
|
55
|
+
...['titleTokens', 'normalizeRuleQuote', 'weakenedRuleSeverity', 'sameIssue', 'nearbyLines', 'spanWithinWindow', 'hardRuleCorroborated',
|
|
56
56
|
'isHardRule', 'longest', 'mergeFindings', 'dedup', 'shouldVerify', 'isProveAttempt', 'countProveAttempt'].map(n => {
|
|
57
57
|
// countProveAttempt is called directly by the checks, so its stub returns NaN and the
|
|
58
58
|
// checks FAIL with a diff instead of the run crashing on an older workflow.js.
|
|
59
59
|
try { return lift(n) } catch {
|
|
60
|
+
if (n === 'normalizeRuleQuote') return "function normalizeRuleQuote() { return 'absent from this workflow.js' }"
|
|
61
|
+
if (n === 'weakenedRuleSeverity') return "function weakenedRuleSeverity() { return 'absent from this workflow.js' }"
|
|
60
62
|
return n === 'countProveAttempt'
|
|
61
63
|
? 'function countProveAttempt() { return NaN }'
|
|
62
64
|
: `function ${n}() { throw new Error('${n} absent from this workflow.js') }`
|
|
@@ -64,8 +66,8 @@ const preamble = [
|
|
|
64
66
|
}),
|
|
65
67
|
].join('\n')
|
|
66
68
|
|
|
67
|
-
const { dedup, isHardRule, shouldVerify, sameIssue, isProveAttempt, countProveAttempt } =
|
|
68
|
-
new Function(preamble + '\nreturn { dedup, isHardRule, shouldVerify, sameIssue, isProveAttempt, countProveAttempt }')()
|
|
69
|
+
const { dedup, isHardRule, shouldVerify, sameIssue, isProveAttempt, countProveAttempt, normalizeRuleQuote, weakenedRuleSeverity } =
|
|
70
|
+
new Function(preamble + '\nreturn { dedup, isHardRule, shouldVerify, sameIssue, isProveAttempt, countProveAttempt, normalizeRuleQuote, weakenedRuleSeverity }')()
|
|
69
71
|
|
|
70
72
|
let failed = 0
|
|
71
73
|
function check(name, actual, expected) {
|
|
@@ -700,7 +702,7 @@ console.log('\nproject rules: PROJECT:<slug> findings must quote a rule that exi
|
|
|
700
702
|
const { spawnSync: spawnPr } = await import('node:child_process')
|
|
701
703
|
const { mkdtempSync: mkPr, cpSync: cpPr, readFileSync: rdPr, rmSync: rmPr, symlinkSync: syPr, writeFileSync: wfPr } = await import('node:fs')
|
|
702
704
|
const { tmpdir: tmpPr } = await import('node:os')
|
|
703
|
-
const { validateProjectRuleFinding, validateWeakenedProjectRuleFinding, isProjectRuleTag } =
|
|
705
|
+
const { validateProjectRuleFinding, validateWeakenedProjectRuleFinding, isProjectRuleTag, normalizeQuote } =
|
|
704
706
|
await import('./project-rule-findings.mjs')
|
|
705
707
|
|
|
706
708
|
const fixtureDir = join(SKILL, 'scripts', 'fixtures', 'project-rules')
|
|
@@ -740,6 +742,65 @@ check('rejects a weakened rule quote absent from baseProjectRules',
|
|
|
740
742
|
validateWeakenedProjectRuleFinding(weakenedFinding, 'NEVER call eval.').ok, false)
|
|
741
743
|
check('rejects a weakened rule when baseProjectRules is unavailable',
|
|
742
744
|
validateWeakenedProjectRuleFinding(weakenedFinding, undefined).ok, false)
|
|
745
|
+
check('rejects a weakened rule whose quote is still in projectRules',
|
|
746
|
+
validateWeakenedProjectRuleFinding(weakenedFinding, planted, planted).ok, false)
|
|
747
|
+
check('accepts a weakened rule whose quote is gone from projectRules',
|
|
748
|
+
validateWeakenedProjectRuleFinding(weakenedFinding, planted, 'nothing here').ok, true)
|
|
749
|
+
const evalBase = '- NEVER call eval in src'
|
|
750
|
+
const evalFinding = prFinding('weakens-project-rule', 'NEVER call eval in src')
|
|
751
|
+
check('accepts a weakening that appends an exception to the rule line',
|
|
752
|
+
validateWeakenedProjectRuleFinding(evalFinding, evalBase, '- NEVER call eval in src unless input is trusted').ok, true)
|
|
753
|
+
check('accepts a weakening that adds a period and an Exception sentence on the same line',
|
|
754
|
+
validateWeakenedProjectRuleFinding(evalFinding, evalBase, '- NEVER call eval in src. Exception: trusted input.').ok, true)
|
|
755
|
+
check('rejects a weakened rule whose line is unchanged among other lines',
|
|
756
|
+
validateWeakenedProjectRuleFinding(evalFinding, evalBase, '# Rules\n- NEVER call eval in src\n- Be kind').reason, 'rule still present in projectRules')
|
|
757
|
+
const wrappedBase = '- After ANY catalog change, run build and commit\n the result'
|
|
758
|
+
const wrappedFinding = prFinding('weakens-project-rule', 'After ANY catalog change, run build and commit the result')
|
|
759
|
+
check('accepts a weakening appended to the last line of a wrapped rule',
|
|
760
|
+
validateWeakenedProjectRuleFinding(wrappedFinding, wrappedBase, '- After ANY catalog change, run build and commit\n the result, except docs-only changes').ok, true)
|
|
761
|
+
check('rejects an unchanged wrapped rule',
|
|
762
|
+
validateWeakenedProjectRuleFinding(wrappedFinding, wrappedBase, wrappedBase).reason, 'rule still present in projectRules')
|
|
763
|
+
check('rejects a wrapped rule re-wrapped at a different column',
|
|
764
|
+
validateWeakenedProjectRuleFinding(wrappedFinding, wrappedBase, '- After ANY catalog change, run build\n and commit the result').reason, 'rule still present in projectRules')
|
|
765
|
+
check('rejects a rule whose bullet marker changed to an asterisk',
|
|
766
|
+
validateWeakenedProjectRuleFinding(evalFinding, evalBase, '* NEVER call eval in src').reason, 'rule still present in projectRules')
|
|
767
|
+
check('rejects an unchanged numbered rule',
|
|
768
|
+
validateWeakenedProjectRuleFinding(evalFinding, '1. NEVER call eval in src', '1. NEVER call eval in src').reason, 'rule still present in projectRules')
|
|
769
|
+
check('rejects a bullet rule renumbered in the PR',
|
|
770
|
+
validateWeakenedProjectRuleFinding(evalFinding, evalBase, '1. NEVER call eval in src').reason, 'rule still present in projectRules')
|
|
771
|
+
check('accepts a weakening that adds a prefix qualifier before the quote',
|
|
772
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), '- NEVER force-push', '- Except on your own branch, NEVER force-push').ok, true)
|
|
773
|
+
check('accepts a weakening added on a new continuation line of the same bullet',
|
|
774
|
+
validateWeakenedProjectRuleFinding(wrappedFinding, wrappedBase + '.', wrappedBase + '.\n Docs-only changes are exempt.').ok, true)
|
|
775
|
+
check('rejects a single-line rule re-wrapped over two lines',
|
|
776
|
+
validateWeakenedProjectRuleFinding(wrappedFinding, '- After ANY catalog change, run build and commit the result', wrappedBase).reason, 'rule still present in projectRules')
|
|
777
|
+
check('rejects a rule moved to another section unchanged',
|
|
778
|
+
validateWeakenedProjectRuleFinding(evalFinding, '# A\n' + evalBase + '\n# B\n- x', '# B\n- x\n# A\n' + evalBase).reason, 'rule still present in projectRules')
|
|
779
|
+
check('accepts an exception appended to a two-line blockquote rule',
|
|
780
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push to main or release branches.'), '> NEVER force-push to main\n> or release branches.', '> NEVER force-push to main\n> or release branches. Exception: your own branch.').ok, true)
|
|
781
|
+
check('accepts an exception added as an indented sub-bullet',
|
|
782
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), '- NEVER force-push', '- NEVER force-push\n - except on your own branch').ok, true)
|
|
783
|
+
check('accepts a parent bullet narrowed above an unchanged sub-bullet',
|
|
784
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER call eval'), '- In src:\n - NEVER call eval', '- In src/legacy only:\n - NEVER call eval').ok, true)
|
|
785
|
+
check('accepts a quote spanning two base bullets when the second gains an exception',
|
|
786
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push\n- NEVER delete main'), '- NEVER force-push\n- NEVER delete main', '- NEVER force-push\n- NEVER delete main unless asked').ok, true)
|
|
787
|
+
check('rejects an unchanged rule with an unchanged nested sub-bullet',
|
|
788
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), '- NEVER force-push\n - on shared branches', '- NEVER force-push\n - on shared branches').reason, 'rule still present in projectRules')
|
|
789
|
+
check('accepts an exception sub-bullet added after a blank line in a loose list',
|
|
790
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), '- NEVER force-push', '- NEVER force-push\n\n - except on your own branch').ok, true)
|
|
791
|
+
check('accepts an indented exception paragraph added after a blank line',
|
|
792
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), '- NEVER force-push', '- NEVER force-push\n\n Except on your own branch.').ok, true)
|
|
793
|
+
const introBase = 'These apply everywhere.\n- In domain code:\n - NEVER mutate state'
|
|
794
|
+
check('accepts a narrowed parent bullet under an intro paragraph',
|
|
795
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER mutate state'), introBase, 'These apply everywhere.\n- In domain code, except scripts:\n - NEVER mutate state').ok, true)
|
|
796
|
+
check('accepts an exception sibling added under a parent bullet below an intro paragraph',
|
|
797
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER mutate state'), introBase, introBase + '\n - Mutation is fine in reducers').ok, true)
|
|
798
|
+
check('rejects an unchanged loose-list rule with an unchanged indented paragraph',
|
|
799
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), '- NEVER force-push\n\n on shared branches', '- NEVER force-push\n\n on shared branches').reason, 'rule still present in projectRules')
|
|
800
|
+
check('rejects unchanged blank-separated sibling bullets when another bullet is edited',
|
|
801
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), '- NEVER force-push\n\n- NEVER call eval', '- NEVER force-push\n\n- NEVER call eval in src').reason, 'rule still present in projectRules')
|
|
802
|
+
check('rejects an unchanged bullet under a lead-in when a sibling is edited',
|
|
803
|
+
validateWeakenedProjectRuleFinding(prFinding('weakens-project-rule', 'NEVER force-push'), 'Rules:\n- NEVER force-push\n- NEVER call eval', 'Rules:\n- NEVER force-push\n- NEVER call eval, except tests').reason, 'rule still present in projectRules')
|
|
743
804
|
check('rejects a weakened rule with no quote',
|
|
744
805
|
validateWeakenedProjectRuleFinding({ rule: 'weakens-project-rule' }, planted).ok, false)
|
|
745
806
|
const verifierInstructions = rdPr(join(SKILL, 'references', 'agents', 'verifier.md'), 'utf8')
|
|
@@ -772,6 +833,25 @@ const quoteA = { file: 'src/total.js', line: 2, title: 'Missing null check on to
|
|
|
772
833
|
const quoteB = { ...quoteA, ruleQuote: 'ALWAYS validate total before use.' }
|
|
773
834
|
check('two different quotes of one PROJECT rule are not sameIssue', sameIssue(quoteA, quoteB), false)
|
|
774
835
|
check('the same quote of one PROJECT rule is still sameIssue', sameIssue(quoteA, { ...quoteA }), true)
|
|
836
|
+
check('the same slug with quotes equal after normalisation is sameIssue',
|
|
837
|
+
sameIssue(quoteA, { ...quoteA, ruleQuote: ' never SKIP the null check on total. ' }), true)
|
|
838
|
+
const slugB = { ...quoteA, rule: 'PROJECT:null-total-guard', ruleQuote: '**Never skip** the null check on total.' }
|
|
839
|
+
check('two PROJECT slugs with one quote modulo case, whitespace and emphasis are sameIssue', sameIssue(quoteA, slugB), true)
|
|
840
|
+
const [mergedSlugs] = dedup([{ ...quoteA, severity: 'important' }, { ...slugB, severity: 'important' }])
|
|
841
|
+
check('different-slug same-quote findings merge into one finding that keeps rule and quote',
|
|
842
|
+
[dedup([{ ...quoteA, severity: 'important' }, { ...slugB, severity: 'important' }]).length, mergedSlugs.rule, typeof mergedSlugs.ruleQuote],
|
|
843
|
+
[1, 'PROJECT:no-null-total', 'string'])
|
|
844
|
+
check('different-slug different-quote findings stay separate',
|
|
845
|
+
sameIssue(quoteA, { ...slugB, ruleQuote: 'ALWAYS validate total before use.' }), false)
|
|
846
|
+
const quoteSamples = ['NEVER call `console.log` in src/.', '> **Never** skip\n the check', '- Use _early_ returns', '## Heading rule text']
|
|
847
|
+
check('workflow normalizeRuleQuote agrees with the validator normalizeQuote',
|
|
848
|
+
quoteSamples.map(q => normalizeRuleQuote(q)), quoteSamples.map(q => normalizeQuote(q)))
|
|
849
|
+
check('quotes differing only by trailing punctuation are not sameIssue',
|
|
850
|
+
sameIssue(quoteA, { ...quoteA, ruleQuote: 'NEVER skip the null check on total' }), false)
|
|
851
|
+
console.log('\nworkflow: a confirmed weakened-rule finding is always Important')
|
|
852
|
+
check('critical from the lens becomes important for weakens-project-rule',
|
|
853
|
+
weakenedRuleSeverity({ rule: 'weakens-project-rule' }, 'critical'), 'important')
|
|
854
|
+
check('other rules keep the severity', weakenedRuleSeverity({ rule: 'TS-1' }, 'critical'), 'critical')
|
|
775
855
|
|
|
776
856
|
// Prompt guard: every lens that reads projectRules is told to raise the tag.
|
|
777
857
|
const lensFiles = ['quality', 'implementation', 'simplification', 'conventions', 'test-quality']
|
|
@@ -787,9 +867,11 @@ function liftBlock(name) {
|
|
|
787
867
|
if (!m) throw new Error(`selftest: const ${name} not found in workflow.js`)
|
|
788
868
|
return m[0]
|
|
789
869
|
}
|
|
870
|
+
// An older workflow.js lacks these constants; fall back so the checks FAIL instead of the suite crashing.
|
|
871
|
+
const liftBlockOr = name => { try { return liftBlock(name) } catch { return `const ${name} = ''\n` } }
|
|
790
872
|
const buildPrompt = new Function(
|
|
791
873
|
'dim', 'SUPPRESSED', 'prevSha',
|
|
792
|
-
[
|
|
874
|
+
[liftBlockOr('PROJECT_RULE_DIMENSIONS'), liftBlockOr('PROJECT_RULE_INSTRUCTION'), liftConst('BUG_CLASS_INSTRUCTION'),
|
|
793
875
|
`const targetMetadata = '{}', promptDir = 'p', repoSlug = 'o/r', prNumber = 1, prUrl = 'u',
|
|
794
876
|
diffFile = 'd', contextFile = 'c', wtDisplay = 'w'`,
|
|
795
877
|
lift('reviewPrompt'), 'return reviewPrompt(dim)'].join('\n'),
|
|
@@ -846,6 +928,12 @@ check('validator accepts a weakened-rule quote found in baseProjectRules',
|
|
|
846
928
|
check('validator rejects a weakened-rule quote found only in projectRules',
|
|
847
929
|
runV(weakenedFile, addedCtx), 1)
|
|
848
930
|
wfPr(join(crashDir, 'rej.json'), JSON.stringify({ rule: 'PROJECT:no-eval', ruleQuote: 'NEVER call eval anywhere in code' }))
|
|
931
|
+
const noBaseCtx = join(crashDir, 'nobase.json')
|
|
932
|
+
wfPr(noBaseCtx, JSON.stringify({ projectRules: 'nothing here' }))
|
|
933
|
+
check('exit 2 on a weakened finding when baseProjectRules is missing', runV(weakenedFile, noBaseCtx), 2)
|
|
934
|
+
const stillCtx = join(crashDir, 'still.json')
|
|
935
|
+
wfPr(stillCtx, JSON.stringify({ projectRules: 'NEVER call console.log in src/.', baseProjectRules: 'NEVER call console.log in src/.' }))
|
|
936
|
+
check('exit 1 on a weakened finding whose rule is still in projectRules', runV(weakenedFile, stillCtx), 1)
|
|
849
937
|
check('exit 1 on a rejected finding', runV(join(crashDir, 'rej.json'), okCtx), 1)
|
|
850
938
|
const symlinkValidator = join(crashDir, 'project-rule-findings.mjs')
|
|
851
939
|
syPr(join(SKILL, 'scripts', 'project-rule-findings.mjs'), symlinkValidator)
|
|
@@ -157,24 +157,33 @@ if [[ "${LEKKER_RULES_ONLY:-}" == "1" ]]; then
|
|
|
157
157
|
if git -C "$REPO_ROOT" fetch origin "+refs/heads/${PR_BRANCH}:refs/remotes/origin/${PR_BRANCH}" 2>/dev/null; then
|
|
158
158
|
RULES_REF="origin/$PR_BRANCH"
|
|
159
159
|
elif git -C "$REPO_ROOT" fetch origin "$PR_BRANCH" 2>/dev/null; then
|
|
160
|
-
|
|
160
|
+
# The base fetch below overwrites FETCH_HEAD, so pin the PR tip now.
|
|
161
|
+
RULES_REF="$(git -C "$REPO_ROOT" rev-parse FETCH_HEAD)"
|
|
161
162
|
else
|
|
162
163
|
printf 'ERROR: could not fetch PR branch %s for rules-only scan\n' "$PR_BRANCH" >&2
|
|
163
164
|
exit 1
|
|
164
165
|
fi
|
|
165
166
|
fi
|
|
167
|
+
# Base rules are best effort: a failure here keeps projectRules and nulls baseProjectRules,
|
|
168
|
+
# because the base copy only hardens validation and must not abort the scan.
|
|
169
|
+
RULES_BASE_SHA=""
|
|
166
170
|
if [[ -n "${LEKKER_BASE_REF:-}" ]]; then
|
|
171
|
+
BASE_FETCH_OK=1
|
|
167
172
|
if [[ -z "$LOCAL_REPO" && "$LEKKER_BASE_REF" == origin/* ]]; then
|
|
168
173
|
BASE_BRANCH="${LEKKER_BASE_REF#origin/}"
|
|
169
|
-
|
|
170
|
-
"+refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH}" 2>/dev/null
|
|
171
|
-
printf 'ERROR: could not fetch base ref %s for rules-only scan\n' "$LEKKER_BASE_REF" >&2
|
|
172
|
-
exit 1
|
|
173
|
-
fi
|
|
174
|
+
git -C "$REPO_ROOT" fetch origin \
|
|
175
|
+
"+refs/heads/${BASE_BRANCH}:refs/remotes/origin/${BASE_BRANCH}" 2>/dev/null || BASE_FETCH_OK=0
|
|
174
176
|
fi
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
# Fork point, as in full mode: the base tip may carry rules added after the PR branched.
|
|
178
|
+
if [[ "$BASE_FETCH_OK" == "1" ]] \
|
|
179
|
+
&& git -C "$REPO_ROOT" rev-parse --verify -q "${LEKKER_BASE_REF}^{commit}" >/dev/null; then
|
|
180
|
+
RULES_BASE_SHA="$(git -C "$REPO_ROOT" merge-base "$LEKKER_BASE_REF" "$RULES_REF" 2>/dev/null || true)"
|
|
181
|
+
# A fork older than the shallow clone depth has no common ancestor until history is deepened.
|
|
182
|
+
if [[ -z "$RULES_BASE_SHA" ]] \
|
|
183
|
+
&& [[ "$(git -C "$REPO_ROOT" rev-parse --is-shallow-repository 2>/dev/null)" == "true" ]]; then
|
|
184
|
+
git -C "$REPO_ROOT" fetch -q --deepen=200 origin 2>/dev/null || true
|
|
185
|
+
RULES_BASE_SHA="$(git -C "$REPO_ROOT" merge-base "$LEKKER_BASE_REF" "$RULES_REF" 2>/dev/null || true)"
|
|
186
|
+
fi
|
|
178
187
|
fi
|
|
179
188
|
fi
|
|
180
189
|
RULES_SOURCE="/tmp/lekker-rules-source-$$"
|
|
@@ -184,17 +193,19 @@ if [[ "${LEKKER_RULES_ONLY:-}" == "1" ]]; then
|
|
|
184
193
|
BASE_PROJECT_RULES=""
|
|
185
194
|
HAVE_BASE_RULES=0
|
|
186
195
|
if [[ -n "${LEKKER_BASE_REF:-}" ]]; then
|
|
187
|
-
if
|
|
196
|
+
if [[ -n "$RULES_BASE_SHA" ]] \
|
|
197
|
+
&& BASE_PROJECT_RULES="$(collect_project_rules_at_ref "$REPO_ROOT" "$RULES_BASE_SHA")"; then
|
|
188
198
|
HAVE_BASE_RULES=1
|
|
189
199
|
else
|
|
190
200
|
BASE_PROJECT_RULES=""
|
|
191
201
|
NOTES="${NOTES}base-rules-unavailable;"
|
|
192
202
|
fi
|
|
193
203
|
fi
|
|
194
|
-
python3 - "$OUT_JSON" "$PROJECT_RULES" "$BASE_PROJECT_RULES" "$HAVE_BASE_RULES" <<'PYEOF'
|
|
204
|
+
python3 - "$OUT_JSON" "$PROJECT_RULES" "$BASE_PROJECT_RULES" "$HAVE_BASE_RULES" "$NOTES" <<'PYEOF'
|
|
195
205
|
import json, sys
|
|
196
206
|
out = {"projectRules": sys.argv[2]}
|
|
197
207
|
out["baseProjectRules"] = sys.argv[3] if sys.argv[4] == "1" else None
|
|
208
|
+
out["notes"] = sys.argv[5]
|
|
198
209
|
with open(sys.argv[1], "w") as f:
|
|
199
210
|
json.dump(out, f, indent=2)
|
|
200
211
|
PYEOF
|
|
@@ -75,6 +75,8 @@ export function loadDedup(workflowPath = join(SKILL, 'workflow.js')) {
|
|
|
75
75
|
liftConst('HARD_RULES', 'const HARD_RULES = new Set()'),
|
|
76
76
|
liftConst('SAME_ISSUE_LINE_WINDOW', 'const SAME_ISSUE_LINE_WINDOW = 30'),
|
|
77
77
|
liftConst('SEVERITY_RANK', 'const SEVERITY_RANK = { critical: 3, important: 2, observation: 1, idiomatic: 0 }'),
|
|
78
|
+
// Absent from an older workflow.js, whose sameIssue does not call it.
|
|
79
|
+
(() => { try { return lift('normalizeRuleQuote') } catch { return '' } })(),
|
|
78
80
|
...['titleTokens', 'sameIssue', 'nearbyLines', 'spanWithinWindow',
|
|
79
81
|
'isHardRule', 'longest', 'mergeFindings', 'dedup'].map(lift),
|
|
80
82
|
].join('\n')
|
|
@@ -166,12 +166,31 @@ function titleTokens(title) {
|
|
|
166
166
|
return words.filter(function(w) { return w.length > 2 })
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
// Same normalisation as normalizeQuote in scripts/project-rule-findings.mjs; workflow.js cannot import it.
|
|
170
|
+
// The selftest asserts the two agree.
|
|
171
|
+
function normalizeRuleQuote(text) {
|
|
172
|
+
return String(text == null ? '' : text)
|
|
173
|
+
.replace(/(^|\n)\s{0,3}#{1,6}\s+/g, '$1')
|
|
174
|
+
.replace(/(^|\n)\s{0,3}>\s?/g, '$1')
|
|
175
|
+
.replace(/`([^`\n]+)`/g, '$1')
|
|
176
|
+
.replace(/(^|[\s([{])(\*{1,2}|_{1,2})(\S(?:.*?\S)?)\2(?=$|[\s.,;:!?)}\]])/g, '$1$3')
|
|
177
|
+
.replace(/^\s*[-+]\s+/gm, ' ')
|
|
178
|
+
.replace(/\s+/g, ' ')
|
|
179
|
+
.trim()
|
|
180
|
+
.toLowerCase()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function weakenedRuleSeverity(finding, severity) {
|
|
184
|
+
return finding.rule === 'weakens-project-rule' ? 'important' : severity
|
|
185
|
+
}
|
|
186
|
+
|
|
169
187
|
function sameIssue(a, b) {
|
|
188
|
+
const qa = normalizeRuleQuote(a.ruleQuote)
|
|
189
|
+
const qb = normalizeRuleQuote(b.ruleQuote)
|
|
170
190
|
// Lens-generated slugs can differ even when they identify the same project rule.
|
|
171
191
|
const sameProjectQuote = typeof a.rule === 'string' && a.rule.startsWith('PROJECT:')
|
|
172
192
|
&& typeof b.rule === 'string' && b.rule.startsWith('PROJECT:')
|
|
173
|
-
&&
|
|
174
|
-
&& a.ruleQuote === b.ruleQuote
|
|
193
|
+
&& qa !== '' && qa === qb
|
|
175
194
|
if (typeof a.rule === 'string' && typeof b.rule === 'string' && a.rule !== b.rule
|
|
176
195
|
&& (a.rule.startsWith('PROJECT:') || b.rule.startsWith('PROJECT:'))
|
|
177
196
|
&& !sameProjectQuote) {
|
|
@@ -179,9 +198,7 @@ function sameIssue(a, b) {
|
|
|
179
198
|
}
|
|
180
199
|
// Two quotes of one PROJECT rule that differ are two different rule passages, not one issue.
|
|
181
200
|
if (typeof a.rule === 'string' && a.rule === b.rule && a.rule.startsWith('PROJECT:')
|
|
182
|
-
&&
|
|
183
|
-
&& typeof b.ruleQuote === 'string' && b.ruleQuote.trim() !== ''
|
|
184
|
-
&& a.ruleQuote !== b.ruleQuote) {
|
|
201
|
+
&& qa !== '' && qb !== '' && qa !== qb) {
|
|
185
202
|
return false
|
|
186
203
|
}
|
|
187
204
|
// Merging two classes would send one class's instances to the other's fix.
|
|
@@ -701,7 +718,7 @@ const budgetAtStart = budget.spent()
|
|
|
701
718
|
`FINDING (JSON): ${JSON.stringify(finding)}.`,
|
|
702
719
|
`DIFF_FILE=${diffFile}, CONTEXT_FILE=${contextFile}, WORKTREE_PATH=${wtDisplay}.`,
|
|
703
720
|
`HOUSE_RULES_FILE: read key "houseRulesFile" from CONTEXT_FILE, then read that exact path before validating a hard rule.`,
|
|
704
|
-
`Default to dropping when uncertain, except when the PROJECT-rule validator was unavailable: then check by hand that the exact quote appears in baseProjectRules when it is a string, otherwise projectRules (drop if it does not), keep the finding only if it does, set validatorUnavailable true, and judge applicability.`,
|
|
721
|
+
`Default to dropping when uncertain, except when the PROJECT-rule validator was unavailable: then check by hand that the exact quote appears in baseProjectRules when it is a string, otherwise projectRules (drop if it does not), keep the finding only if it does, set validatorUnavailable true, and judge applicability. For rule "weakens-project-rule" check baseProjectRules ONLY: drop it when baseProjectRules is not a string or the quote is absent there, and never consult projectRules.`,
|
|
705
722
|
].join(' ')
|
|
706
723
|
}
|
|
707
724
|
|
|
@@ -839,6 +856,7 @@ const budgetAtStart = budget.spent()
|
|
|
839
856
|
|
|
840
857
|
// confirmed
|
|
841
858
|
const confirmed = Object.assign({}, finding)
|
|
859
|
+
confirmed.severity = weakenedRuleSeverity(finding, confirmed.severity)
|
|
842
860
|
confirmed.verificationStatus = isHardRule(finding) ? 'hard-rule-confirmed' : 'confirmed'
|
|
843
861
|
confirmed.verifierReasoning = verdict.reasoning
|
|
844
862
|
return confirmed
|