@rmyndharis/aimhooman 0.1.6 → 0.1.8

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.
@@ -215,21 +215,39 @@ function scanCommit(repo, options) {
215
215
  if (floor && !isVersionedStrict(rawPolicy)) {
216
216
  accumulator.add([protectedPolicyFinding(rawPolicy, policy, snapshot)]);
217
217
  }
218
- scanEntryGroup(repo, loaded.engine, snapshot.entries, policy, accumulator, {
218
+ // A commit is judged by what it changes, not by the whole tree it inherits.
219
+ // Scanning snapshot.entries (a full `ls-tree`) re-applied path rules to
220
+ // every file the commit merely carried forward from its parent, so a path
221
+ // allowed into history once (under an override that was later removed, or
222
+ // before the guard existed) blocked every later commit on the branch. The
223
+ // change set already drives content scanning; routing path rules through it
224
+ // too means a newly added `.env` still fires while an inherited one stays
225
+ // silent, matching how scanRange has always judged history.
226
+ scanEntryGroup(repo, loaded.engine, snapshot.changes, policy, accumulator, {
219
227
  reviewPathEntries: snapshot.changes,
220
228
  contentEntries: snapshot.changes,
221
229
  allowMissingPolicy: acknowledged && !rawPolicy.policy_object_id,
222
230
  transition: snapshot.commit,
223
231
  });
224
- accumulator.add(loaded.engine.checkMessage(snapshot.message)
225
- .map((finding) => decorate(finding, snapshot, policy)));
226
- accumulator.addSkipped(loaded.engine.takeSkipped());
232
+ // The message is the author's words, so it is scanned for attribution and
233
+ // markers only when the commit was written here. Fetched history (a PR
234
+ // checked out for review, a branch pulled from a remote) carries other
235
+ // people's commit text that a local developer cannot edit, so flagging it
236
+ // only blocks the review. cmdRefcheck passes messageScope='changes-only'
237
+ // for commits that were imported rather than authored; a direct `aimhooman
238
+ // check --commit` still defaults to scanning the message.
239
+ const scanMessageText = options.messageScope !== 'changes-only';
240
+ if (scanMessageText) {
241
+ accumulator.add(loaded.engine.checkMessage(snapshot.message)
242
+ .map((finding) => decorate(finding, snapshot, policy)));
243
+ accumulator.addSkipped(loaded.engine.takeSkipped());
244
+ }
227
245
  return result({
228
246
  target: `commit:${snapshot.commit}`,
229
247
  policy,
230
248
  accumulator,
231
249
  diagnostics,
232
- messageScanned: true,
250
+ messageScanned: scanMessageText,
233
251
  commit: snapshot.commit,
234
252
  });
235
253
  }
package/src/scan.mjs CHANGED
@@ -51,13 +51,35 @@ export class Engine {
51
51
  decide(base, target, rule, context = {}) {
52
52
  if (this.denyPaths.has(target) || this.denyRules.has(rule.id)) return 'block';
53
53
  if (context.transientAllowRules?.has(rule.id)) return 'allow';
54
- if (this.scopedAllow.some((entry) => (
55
- entry.target === target
56
- && entry.ruleId === rule.id
57
- && entry.transition === context.transition
58
- && entry.newObjectId === context.objectId
59
- && entry.newMode === context.mode
60
- ))) return 'allow';
54
+ // W11 (F2): review binding is profile-dependent.
55
+ // - strict: review binds the EXACT reviewed blob. A changed/copied/
56
+ // symlinked/mode-flipped target is NOT covered — it must be re-
57
+ // reviewed. This is the security property the strict lifecycle tests
58
+ // pin, and strict is the profile that exits 11 on review.
59
+ // - clean/compliance: review is ADVISORY (a stderr message, exit 0),
60
+ // so re-surfacing it on every edit of a reviewed agent-instruction
61
+ // file (CLAUDE.md, AGENTS.md) is pure friction with no security
62
+ // payoff — the security boundary is secret scanning, strict blocks,
63
+ // and reference-transaction. A LIVE-FILE review (newObjectId set)
64
+ // therefore persists per path + rule across edits. A TOMBSTONE
65
+ // review (newObjectId null — a reviewed deletion) keeps the exact
66
+ // match in every profile: a deletion review must not silently cover
67
+ // a later re-adding of the file.
68
+ const strictReview = this.profile === 'strict';
69
+ if (this.scopedAllow.some((entry) => {
70
+ if (entry.target !== target || entry.ruleId !== rule.id) return false;
71
+ if (entry.newObjectId === null) {
72
+ // Tombstone: exact match in every profile.
73
+ return entry.transition === context.transition
74
+ && entry.newObjectId === context.objectId
75
+ && entry.newMode === context.mode;
76
+ }
77
+ // Live-file review: exact in strict; path-anchored in clean/compliance.
78
+ return !strictReview
79
+ || (entry.transition === context.transition
80
+ && entry.newObjectId === context.objectId
81
+ && entry.newMode === context.mode);
82
+ })) return 'allow';
61
83
  // A rule-level allow cannot bypass the secret-category guard that the
62
84
  // path-level allow below enforces: secret rules require an explicit
63
85
  // --scope secret-path override on a specific path, never a blanket rule allow.
@@ -119,7 +141,18 @@ export class Engine {
119
141
  const categories = Array.isArray(options.categories)
120
142
  ? new Set(options.categories)
121
143
  : null;
144
+ // lineRanges narrows content scanning to changed hunks (W4, bug 12d-F1).
145
+ // When provided (an array of inclusive 1-based {start,end} ranges), only
146
+ // lines falling in a range are evaluated; this stops a file that contains
147
+ // a secret-bearing line ELSEWHERE (a PEM header inside a test string on
148
+ // line 200) from blocking a commit that only edited line 50. Findings
149
+ // keep their real line numbers because lineRecords already anchors them.
150
+ // Absent = scan every line (the pre-W4 behaviour).
151
+ const ranges = Array.isArray(options.lineRanges) && options.lineRanges.length
152
+ ? options.lineRanges
153
+ : null;
122
154
  for (const record of lineRecords(String(content))) {
155
+ if (ranges && !ranges.some((range) => record.line >= range.start && record.line <= range.end)) continue;
123
156
  this.#markLocalInputSkip('code', p, record.text, categories);
124
157
  const result = this.#evaluate('code', p, record.text, { categories });
125
158
  if (result) out.push(finding(result, { path: p, line: record.line, text: record.text }));