agent-gov-core 0.4.2 → 0.4.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.
package/CHANGELOG.md CHANGED
Binary file
package/README.md CHANGED
@@ -66,7 +66,7 @@ The JSON schema at [`schemas/finding.schema.json`](./schemas/finding.schema.json
66
66
  - `isSeverity(v)`, `isToolKind(v)`, `isNamespacedKind(v)` — type guards
67
67
  - `kind(tool, name)` — build a namespaced kind without hand-assembling the dotted string
68
68
  - `createFinding({tool, name, severity, message, ...})` — convenience constructor that calls `kind()` and `fingerprintFinding()` for you
69
- - `fingerprintFinding(finding)` — 16-character hex hash of `(kind, file, line, column)`. Stable across runs and message rewordings, so a meta-reviewer can dedupe
69
+ - `fingerprintFinding(finding)` — 16-character hex hash of `(kind, file, line, column, salientKey?)`. Stable across runs and message rewordings, so a meta-reviewer can dedupe. Pass `salientKey` (since v0.4.3) when multiple distinct findings can fire at the same site
70
70
  - `validateFinding(value)` — runtime check against `schemas/finding.schema.json`, returns `{ ok, errors[] }`
71
71
 
72
72
  ### Config readers
package/dist/finding.d.ts CHANGED
@@ -26,6 +26,16 @@ export interface Finding {
26
26
  location?: FindingLocation;
27
27
  /** Stable identifier for dedupe across runs. Recommended: hash of (kind, location, salient fields). */
28
28
  fingerprint?: string;
29
+ /**
30
+ * Optional discriminator that participates in the fingerprint hash. Set this
31
+ * when a single (kind, file, line) site can legitimately host multiple distinct
32
+ * findings — e.g. two suspicious imports on the same line, two MCP servers in
33
+ * the same JSON object, two npm dependencies declared in one package.json line.
34
+ * Without it, the meta-reviewer would dedupe them into one. Use a stable value
35
+ * that doesn't drift across reruns (package name, server name, rule id) — not
36
+ * a timestamp or counter.
37
+ */
38
+ salientKey?: string;
29
39
  /** Optional structured metadata; downstream meta-reviewers may inspect it. */
30
40
  data?: Record<string, unknown>;
31
41
  }
@@ -57,6 +67,12 @@ export interface CreateFindingSpec {
57
67
  detail?: string;
58
68
  location?: FindingLocation;
59
69
  data?: Record<string, unknown>;
70
+ /**
71
+ * See {@link Finding.salientKey}. Pass when the same (kind, file, line) site
72
+ * can produce multiple distinct findings that must not collapse to one
73
+ * fingerprint.
74
+ */
75
+ salientKey?: string;
60
76
  /** Optional explicit fingerprint. If omitted, {@link fingerprintFinding} is computed. */
61
77
  fingerprint?: string;
62
78
  }
package/dist/finding.js CHANGED
@@ -62,6 +62,8 @@ export function createFinding(spec) {
62
62
  finding.detail = spec.detail;
63
63
  if (spec.location !== undefined)
64
64
  finding.location = spec.location;
65
+ if (spec.salientKey !== undefined)
66
+ finding.salientKey = spec.salientKey;
65
67
  if (spec.data !== undefined)
66
68
  finding.data = spec.data;
67
69
  finding.fingerprint = spec.fingerprint ?? fingerprintFinding(finding);
@@ -98,6 +100,10 @@ export function fingerprintFinding(finding) {
98
100
  fileNormalized,
99
101
  finding.location?.line ?? '',
100
102
  finding.location?.column ?? '',
103
+ // salientKey lets multiple distinct findings at the same (kind, file, line)
104
+ // site keep separate fingerprints. Empty string when absent so the hash
105
+ // shape is stable across findings that don't need a discriminator.
106
+ finding.salientKey ?? '',
101
107
  ];
102
108
  return createHash('sha256').update(parts.join('|')).digest('hex').slice(0, 16);
103
109
  }
@@ -109,6 +115,7 @@ const FINDING_ALLOWED_KEYS = new Set([
109
115
  'detail',
110
116
  'location',
111
117
  'fingerprint',
118
+ 'salientKey',
112
119
  'data',
113
120
  ]);
114
121
  const LOCATION_ALLOWED_KEYS = new Set(['file', 'line', 'column', 'endLine', 'endColumn']);
@@ -145,6 +152,9 @@ export function validateFinding(value) {
145
152
  if (v.fingerprint !== undefined && typeof v.fingerprint !== 'string') {
146
153
  errors.push('fingerprint must be a string when present');
147
154
  }
155
+ if (v.salientKey !== undefined && typeof v.salientKey !== 'string') {
156
+ errors.push('salientKey must be a string when present');
157
+ }
148
158
  if (v.data !== undefined && (v.data === null || typeof v.data !== 'object' || Array.isArray(v.data))) {
149
159
  errors.push('data must be an object when present');
150
160
  }
package/dist/locators.js CHANGED
@@ -64,9 +64,19 @@ export function lineOfTomlKey(text, dottedKey, scope) {
64
64
  let inTargetTable = prefix.length === 0;
65
65
  let currentTable = [];
66
66
  const targetHeader = prefix.join('.');
67
+ // Track multi-line basic (`"""`) and literal (`'''`) string state. A leaf-key
68
+ // pattern can otherwise match against decoy text inside a multi-line string
69
+ // value — see lineOfTomlKey regression tests.
70
+ let inMultilineString = null;
67
71
  for (let i = 0; i < lines.length; i++) {
68
72
  const lineNumber = i + 1;
69
73
  const raw = lines[i];
74
+ const stateAtLineStart = inMultilineString;
75
+ inMultilineString = updateMultilineStringState(raw, inMultilineString);
76
+ // If we entered this line inside a multi-line string, never match. The key
77
+ // pattern there is part of a string literal, not a real assignment.
78
+ if (stateAtLineStart !== null)
79
+ continue;
70
80
  const trimmed = raw.trim();
71
81
  const headerMatch = /^\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$/.exec(trimmed);
72
82
  if (headerMatch) {
@@ -93,6 +103,43 @@ export function lineOfTomlKey(text, dottedKey, scope) {
93
103
  }
94
104
  return 0;
95
105
  }
106
+ /**
107
+ * Walk a line and update multi-line string state. Each unescaped occurrence of
108
+ * `"""` toggles basic-multiline; each `'''` toggles literal-multiline; the
109
+ * other delimiter is inert while we're inside the first. Returns the state at
110
+ * end-of-line so the next iteration knows whether it's inside a string.
111
+ */
112
+ function updateMultilineStringState(line, current) {
113
+ let state = current;
114
+ let pos = 0;
115
+ while (pos <= line.length - 3) {
116
+ const window = line.substr(pos, 3);
117
+ if (state === null) {
118
+ if (window === '"""') {
119
+ state = '"""';
120
+ pos += 3;
121
+ continue;
122
+ }
123
+ if (window === "'''") {
124
+ state = "'''";
125
+ pos += 3;
126
+ continue;
127
+ }
128
+ }
129
+ else if (state === '"""' && window === '"""') {
130
+ state = null;
131
+ pos += 3;
132
+ continue;
133
+ }
134
+ else if (state === "'''" && window === "'''") {
135
+ state = null;
136
+ pos += 3;
137
+ continue;
138
+ }
139
+ pos++;
140
+ }
141
+ return state;
142
+ }
96
143
  function scopeLineFilter(text, scope) {
97
144
  if (!scope)
98
145
  return () => true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-gov-core",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Shared primitives for the AI-agent governance suite: Finding schema, JSONC/TOML readers, line locators, MCP command normalization, shell tokenization, and GitHub Action helpers.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -41,6 +41,10 @@
41
41
  }
42
42
  },
43
43
  "fingerprint": { "type": "string" },
44
+ "salientKey": {
45
+ "type": "string",
46
+ "description": "Optional discriminator that participates in the fingerprint hash. Set when a single (kind, file, line) site can produce multiple distinct findings (e.g. two suspicious imports on one line). Use a stable value — package name, server name, rule id — not a timestamp."
47
+ },
44
48
  "data": { "type": "object" }
45
49
  }
46
50
  }