@rmyndharis/aimhooman 0.1.1 → 0.1.3

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.
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { gitEnvironment } from './git-environment.mjs';
2
+ import { gitEnvironment, GIT_TIMEOUT_MS } from './git-environment.mjs';
3
3
 
4
4
  export const DEFAULT_SCAN_LIMITS = Object.freeze({
5
5
  maxFileBytes: 2 << 20,
@@ -67,9 +67,11 @@ export function scanEntries(repo, engine, entries, options = {}) {
67
67
  increment(stats.skipped, 'binary');
68
68
  // Binary classification only skips text-oriented policy rules. Secret
69
69
  // signatures are ASCII byte sequences, so latin1 preserves a
70
- // one-byte-to-one-code-unit view and prevents one NUL from hiding
71
- // credential material while keeping the existing byte limits.
72
- matched = engine.checkContent(entry.path, blob.toString('latin1'), {
70
+ // one-byte-to-one-code-unit view and keeps the existing byte limits.
71
+ // Stripping NULs is what defeats hiding credential material behind
72
+ // them: one injected NUL breaks a signature, and a multi-byte
73
+ // encoding like UTF-16 injects one per character.
74
+ matched = engine.checkContent(entry.path, blob.toString('latin1').replace(/\0/g, ''), {
73
75
  categories: ['secret'],
74
76
  });
75
77
  } else {
@@ -112,6 +114,11 @@ function readObjects(repo, objectIds, expectedBytes = 0) {
112
114
  input: Buffer.from(unique.join('\n') + '\n'),
113
115
  encoding: 'buffer',
114
116
  maxBuffer: Math.max(2 * 1024 * 1024, expectedBytes + unique.length * 256 + 1024),
117
+ timeout: GIT_TIMEOUT_MS,
118
+ // Same reason as gitBuf in gitx.mjs: without an explicit stdio,
119
+ // execFileSync echoes the child's stderr before it checks the exit
120
+ // status, so git's raw output reaches the terminal even on success.
121
+ stdio: ['pipe', 'pipe', 'pipe'],
115
122
  });
116
123
  const objects = new Map();
117
124
  const failures = [];
@@ -11,7 +11,16 @@ import { commitChanges, commitMessage, commitSnapshot, historyRange } from './hi
11
11
  import { DEFAULT_SCAN_LIMITS, scanEntries } from './scan-session.mjs';
12
12
  import { loadRulesWithDiagnostics } from './rules.mjs';
13
13
 
14
- const PROFILE_RANK = { clean: 0, compliance: 1, strict: 2 };
14
+ // A tiebreak for the one profile a range report has to name, not a strength
15
+ // lattice. Clean and compliance are not ordered against each other: compliance
16
+ // allows the six attribution rules clean blocks or reviews, no shipped rule runs
17
+ // the other way, and applyExplicitProfile refuses to move between them in either
18
+ // direction. No single profile is truthful for a mixed range, so this only
19
+ // settles which policy object gets reported — clean outranks compliance because
20
+ // a value read as the strongest must not name the profile that allows what the
21
+ // other blocks. Enforcement never reads this: each commit is scanned under its
22
+ // own policy and the exit code uses each finding's own scanProfile.
23
+ const PROFILE_RANK = { compliance: 0, clean: 1, strict: 2 };
15
24
  const REVIEW_REQUIRED_PATH_RULES = new Set([
16
25
  'generic.agent-instructions',
17
26
  'generic.project-policy',
@@ -49,6 +58,7 @@ export function scanMessage(repo, text, options = {}) {
49
58
  const { policy, head } = resolved;
50
59
  const loaded = engineForPolicy(repo, policy, options.overrideHead || head);
51
60
  const accumulator = createAccumulator(options.limits);
61
+ accumulator.addSkipped(loaded.skipped);
52
62
  accumulator.add(loaded.engine.checkMessage(text).map((finding) => decorate(finding, null, policy)));
53
63
  accumulator.addSkipped(loaded.engine.takeSkipped());
54
64
  return {
@@ -68,6 +78,7 @@ function scanIndex(repo, options) {
68
78
  const { rawPolicy, floor, policy, head, acknowledged } = resolveStagedPolicy(repo, options.explicitProfile);
69
79
  const loaded = engineForPolicy(repo, policy, options.overrideHead || head);
70
80
  const accumulator = createAccumulator(options.limits);
81
+ accumulator.addSkipped(loaded.skipped);
71
82
  let entries = kind === 'tracked' ? trackedEntries(repo) : stagedEntries(repo);
72
83
  const conflicts = unmergedPaths(repo);
73
84
  const diagnostics = [...loaded.diagnostics];
@@ -189,6 +200,7 @@ function scanCommit(repo, options) {
189
200
  reviewContexts,
190
201
  );
191
202
  const accumulator = createAccumulator(options.limits);
203
+ accumulator.addSkipped(loaded.skipped);
192
204
  const diagnostics = [...loaded.diagnostics];
193
205
 
194
206
  if (snapshot.shallowBoundary) {
@@ -261,6 +273,9 @@ function scanRange(repo, options) {
261
273
  // only constructs an Engine and applies overrides (see engineForPolicy).
262
274
  const preloaded = loadRulesWithDiagnostics(repo.stateDir);
263
275
  const preloadedOverrides = loadOverrides(repo.stateDir);
276
+ // Counted once for the range, not once per commit: the same packs back every
277
+ // commit's engine, so the per-commit loaded.skipped would inflate the tally.
278
+ accumulator.addSkipped(packSkipped(preloaded.errors));
264
279
 
265
280
  for (const commit of history.commits) {
266
281
  const changes = commitChanges(repo, commit.commit, commit.commit, commit.parents);
@@ -452,7 +467,14 @@ export function engineForPolicy(
452
467
  [...reviewedInstructions, ...reviewedPolicies],
453
468
  );
454
469
  const diagnostics = loaded.errors.map((error) => ({ level: 'warning', message: `${error.message}; pack skipped` }));
455
- return { engine: loaded.engine, diagnostics };
470
+ return { engine: loaded.engine, diagnostics, skipped: packSkipped(loaded.errors) };
471
+ }
472
+
473
+ // packSkipped turns rule-pack load failures into a counted skip reason. A pack
474
+ // that never loaded is a hole in coverage, not an empty result: strict throws
475
+ // above, and clean/compliance proceed but must not report a complete scan.
476
+ function packSkipped(errors) {
477
+ return errors.length ? { 'local-pack-error': errors.length } : {};
456
478
  }
457
479
 
458
480
  function reviewedEngineEntry(entry, ruleId, transition) {
@@ -589,6 +611,10 @@ function rangeReportPolicy(basePolicy, policies, usedStrictFloor, explicitProfil
589
611
  };
590
612
  }
591
613
 
614
+ // Skip reasons that mean a rule never ran, as opposed to a rule running and
615
+ // matching nothing. Either way the scan cannot claim to have covered its input.
616
+ const INCOMPLETE_SKIP_REASONS = new Set(['local-input-limit', 'local-pack-error']);
617
+
592
618
  function createAccumulator(limits = {}) {
593
619
  const effectiveLimits = { ...DEFAULT_SCAN_LIMITS, ...limits };
594
620
  const findings = [];
@@ -644,7 +670,7 @@ function createAccumulator(limits = {}) {
644
670
  addSkipped(skipped = {}) {
645
671
  for (const [reason, count] of Object.entries(skipped)) {
646
672
  stats.skipped[reason] = (stats.skipped[reason] || 0) + count;
647
- if (reason === 'local-input-limit') complete = false;
673
+ if (INCOMPLETE_SKIP_REASONS.has(reason)) complete = false;
648
674
  }
649
675
  },
650
676
  markIncomplete() {