@westbayberry/dg 2.0.11 → 2.2.0

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.
Files changed (143) hide show
  1. package/README.md +216 -226
  2. package/dist/agents/claude-code.js +113 -0
  3. package/dist/agents/codex.js +65 -0
  4. package/dist/agents/copilot-cli.js +115 -0
  5. package/dist/agents/cursor.js +113 -0
  6. package/dist/agents/gemini.js +107 -0
  7. package/dist/agents/persistence.js +285 -0
  8. package/dist/agents/registry.js +127 -0
  9. package/dist/agents/types.js +1 -0
  10. package/dist/agents/windsurf.js +93 -0
  11. package/dist/api/analyze.js +6 -4
  12. package/dist/audit/detectors.js +0 -11
  13. package/dist/audit/events.js +5 -21
  14. package/dist/audit-ui/AuditApp.js +2 -0
  15. package/dist/audit-ui/components/AuditResultsView.js +55 -92
  16. package/dist/audit-ui/export.js +0 -4
  17. package/dist/audit-ui/format.js +0 -3
  18. package/dist/audit-ui/launch.js +10 -1
  19. package/dist/auth/device-login.js +4 -5
  20. package/dist/auth/login-app.js +7 -7
  21. package/dist/auth/store.js +8 -3
  22. package/dist/bin/dg.js +59 -52
  23. package/dist/commands/agents.js +231 -0
  24. package/dist/commands/audit.js +22 -3
  25. package/dist/commands/config.js +26 -10
  26. package/dist/commands/cooldown.js +389 -0
  27. package/dist/commands/decisions.js +169 -0
  28. package/dist/commands/doctor.js +1 -1
  29. package/dist/commands/help.js +1 -1
  30. package/dist/commands/licenses.js +10 -22
  31. package/dist/commands/logout.js +4 -11
  32. package/dist/commands/router.js +8 -4
  33. package/dist/commands/sbom.js +206 -0
  34. package/dist/commands/scan.js +3 -2
  35. package/dist/commands/service.js +32 -13
  36. package/dist/commands/setup.js +197 -27
  37. package/dist/commands/status.js +5 -2
  38. package/dist/commands/types.js +1 -0
  39. package/dist/commands/update.js +17 -8
  40. package/dist/commands/verify.js +8 -5
  41. package/dist/config/settings.js +154 -65
  42. package/dist/decisions/apply.js +128 -0
  43. package/dist/decisions/remember-prompt.js +92 -0
  44. package/dist/export-ui/ExportDialog.js +198 -0
  45. package/dist/install-ui/LiveInstall.js +2 -2
  46. package/dist/install-ui/block-render.js +21 -4
  47. package/dist/install-ui/prep-spinner.js +32 -0
  48. package/dist/install-ui/prompt.js +14 -0
  49. package/dist/launcher/agent-check.js +466 -0
  50. package/dist/launcher/agent-hook-exec.js +70 -0
  51. package/dist/launcher/agent-hook-io.js +31 -0
  52. package/dist/launcher/cargo-cache.js +40 -0
  53. package/dist/launcher/classify.js +17 -6
  54. package/dist/launcher/env.js +71 -24
  55. package/dist/launcher/install-preflight.js +167 -17
  56. package/dist/launcher/live-install.js +25 -5
  57. package/dist/launcher/output-redaction.js +7 -4
  58. package/dist/launcher/preflight-prompt.js +43 -3
  59. package/dist/launcher/run.js +100 -86
  60. package/dist/launcher/spawn-invocation.js +21 -0
  61. package/dist/policy/cooldown.js +117 -0
  62. package/dist/policy/evaluate.js +5 -21
  63. package/dist/policy/pypi-name.js +17 -0
  64. package/dist/presentation/mode.js +3 -2
  65. package/dist/presentation/package-page.js +9 -0
  66. package/dist/presentation/provenance.js +23 -0
  67. package/dist/presentation/theme.js +7 -7
  68. package/dist/project/dgfile.js +446 -0
  69. package/dist/proxy/auth.js +42 -0
  70. package/dist/proxy/ca.js +29 -9
  71. package/dist/proxy/cooldown-exemptions-file.js +33 -0
  72. package/dist/proxy/enforcement.js +57 -17
  73. package/dist/proxy/metadata-map.js +66 -4
  74. package/dist/proxy/preverified.js +55 -0
  75. package/dist/proxy/server.js +473 -45
  76. package/dist/proxy/worker.js +16 -1
  77. package/dist/publish-set/collect.js +1 -4
  78. package/dist/publish-set/npm.js +8 -5
  79. package/dist/publish-set/pack.js +9 -3
  80. package/dist/runtime/cli.js +0 -4
  81. package/dist/runtime/fatal.js +31 -0
  82. package/dist/runtime/first-run.js +12 -11
  83. package/dist/runtime/node-version.js +43 -6
  84. package/dist/runtime/nudges.js +35 -2
  85. package/dist/sbom/cyclonedx.js +211 -0
  86. package/dist/sbom-ui/SbomApp.js +158 -0
  87. package/dist/sbom-ui/components/SbomHeader.js +32 -0
  88. package/dist/sbom-ui/components/SbomList.js +52 -0
  89. package/dist/sbom-ui/inventory.js +128 -0
  90. package/dist/sbom-ui/launch.js +51 -0
  91. package/dist/sbom-ui/run.js +55 -0
  92. package/dist/sbom-ui/store.js +26 -0
  93. package/dist/scan/collect.js +10 -6
  94. package/dist/scan/command.js +51 -17
  95. package/dist/scan/discovery.js +11 -2
  96. package/dist/scan/render.js +63 -8
  97. package/dist/scan/scanner-report.js +42 -9
  98. package/dist/scan/staged.js +71 -11
  99. package/dist/scan-ui/LegacyApp.js +12 -16
  100. package/dist/scan-ui/alt-screen.js +5 -8
  101. package/dist/scan-ui/components/InteractiveResultsView.js +193 -123
  102. package/dist/scan-ui/components/ProgressBar.js +3 -14
  103. package/dist/scan-ui/components/ProjectSelector.js +1 -1
  104. package/dist/scan-ui/components/ScoreHeader.js +2 -3
  105. package/dist/scan-ui/components/SetupBanner.js +0 -6
  106. package/dist/scan-ui/format-helpers.js +61 -5
  107. package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
  108. package/dist/scan-ui/hooks/useScan.js +46 -4
  109. package/dist/scan-ui/launch.js +7 -4
  110. package/dist/scan-ui/shims.js +14 -4
  111. package/dist/scripts/detect.js +158 -0
  112. package/dist/scripts/gate.js +170 -0
  113. package/dist/service/state.js +27 -8
  114. package/dist/service/trust-refresh.js +92 -0
  115. package/dist/service/worker.js +23 -1
  116. package/dist/setup/activate-shell.js +28 -0
  117. package/dist/setup/git-hook.js +49 -4
  118. package/dist/setup/plan.js +98 -29
  119. package/dist/setup-ui/gate.js +39 -0
  120. package/dist/setup-ui/offer.js +42 -0
  121. package/dist/setup-ui/selector.js +27 -0
  122. package/dist/setup-ui/tasks.js +56 -0
  123. package/dist/setup-ui/wizard.js +225 -0
  124. package/dist/state/cooldown-held.js +66 -0
  125. package/dist/state/index.js +1 -0
  126. package/dist/state/locks.js +4 -2
  127. package/dist/state/store.js +2 -1
  128. package/dist/util/external-tool.js +25 -0
  129. package/dist/util/git.js +10 -3
  130. package/dist/util/json-file.js +24 -0
  131. package/dist/util/report-writer.js +57 -0
  132. package/dist/util/tty-prompt.js +13 -6
  133. package/dist/verify/local.js +240 -42
  134. package/dist/verify/package-check.js +86 -18
  135. package/dist/verify/preflight.js +242 -49
  136. package/dist/verify/render.js +15 -1
  137. package/npm-shrinkwrap.json +2383 -0
  138. package/package.json +14 -8
  139. package/NOTICE +0 -5
  140. package/dist/commands/completion.js +0 -116
  141. package/dist/commands/explain.js +0 -232
  142. package/dist/commands/unavailable.js +0 -11
  143. package/dist/telemetry/events.js +0 -40
@@ -1,6 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
- import { randomUUID } from "node:crypto";
3
- import { dirname, join } from "node:path";
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { writeJsonAtomic } from "../util/json-file.js";
4
4
  import { acquireLockSyncWithRetry, resolveDgPaths } from "../state/index.js";
5
5
  export const CONFIG_KEYS = Object.freeze([
6
6
  "api.baseUrl",
@@ -9,11 +9,17 @@ export const CONFIG_KEYS = Object.freeze([
9
9
  "policy.trustProjectAllowlists",
10
10
  "policy.allowForceOverride",
11
11
  "policy.scriptHardening",
12
+ "scriptGate.mode",
13
+ "scriptGate.observe",
12
14
  "gitHook.onWarn",
13
15
  "gitHook.onIncomplete",
14
- "audit.upload",
15
- "telemetry.enabled",
16
- "webhooks.enabled"
16
+ "cooldown.age",
17
+ "cooldown.npm.age",
18
+ "cooldown.pypi.age",
19
+ "cooldown.cargo.age",
20
+ "cooldown.onUnknown",
21
+ "cooldown.exempt",
22
+ "audit.upload"
17
23
  ]);
18
24
  export const DEFAULT_CONFIG = Object.freeze({
19
25
  version: 1,
@@ -29,18 +35,24 @@ export const DEFAULT_CONFIG = Object.freeze({
29
35
  allowForceOverride: true,
30
36
  scriptHardening: false
31
37
  },
38
+ scriptGate: {
39
+ mode: "observe",
40
+ observe: false
41
+ },
32
42
  gitHook: {
33
43
  onWarn: "prompt",
34
44
  onIncomplete: "allow"
35
45
  },
46
+ cooldown: {
47
+ age: "24h",
48
+ npmAge: "",
49
+ pypiAge: "",
50
+ cargoAge: "",
51
+ onUnknown: "allow",
52
+ exempt: ""
53
+ },
36
54
  audit: {
37
55
  upload: false
38
- },
39
- telemetry: {
40
- enabled: false
41
- },
42
- webhooks: {
43
- enabled: true
44
56
  }
45
57
  });
46
58
  export class ConfigError extends Error {
@@ -113,22 +125,48 @@ export function getConfigValue(config, key) {
113
125
  if (key === "policy.scriptHardening") {
114
126
  return String(config.policy.scriptHardening);
115
127
  }
128
+ if (key === "scriptGate.mode") {
129
+ return config.scriptGate.mode;
130
+ }
131
+ if (key === "scriptGate.observe") {
132
+ return String(config.scriptGate.observe);
133
+ }
116
134
  if (key === "gitHook.onWarn") {
117
135
  return config.gitHook.onWarn;
118
136
  }
119
137
  if (key === "gitHook.onIncomplete") {
120
138
  return config.gitHook.onIncomplete;
121
139
  }
122
- if (key === "audit.upload") {
123
- return String(config.audit.upload);
140
+ if (key === "cooldown.age") {
141
+ return config.cooldown.age;
142
+ }
143
+ if (key === "cooldown.npm.age") {
144
+ return config.cooldown.npmAge;
145
+ }
146
+ if (key === "cooldown.pypi.age") {
147
+ return config.cooldown.pypiAge;
148
+ }
149
+ if (key === "cooldown.cargo.age") {
150
+ return config.cooldown.cargoAge;
124
151
  }
125
- if (key === "telemetry.enabled") {
126
- return String(config.telemetry.enabled);
152
+ if (key === "cooldown.onUnknown") {
153
+ return config.cooldown.onUnknown;
127
154
  }
128
- return String(config.webhooks.enabled);
155
+ if (key === "cooldown.exempt") {
156
+ return config.cooldown.exempt;
157
+ }
158
+ return String(config.audit.upload);
129
159
  }
130
- export function listConfig(config) {
131
- return CONFIG_KEYS.map((key) => ({
160
+ export const ADVANCED_CONFIG_KEYS = new Set([
161
+ "org.id",
162
+ "policy.scriptHardening",
163
+ "scriptGate.observe",
164
+ "cooldown.npm.age",
165
+ "cooldown.pypi.age",
166
+ "cooldown.cargo.age"
167
+ ]);
168
+ export function listConfig(config, includeAdvanced = false) {
169
+ return CONFIG_KEYS.filter((key) => includeAdvanced || !ADVANCED_CONFIG_KEYS.has(key)).map((key) => ({
132
170
  key,
133
171
  value: getConfigValue(config, key)
134
172
  }));
@@ -168,44 +206,64 @@ export function setConfigValue(config, key, rawValue) {
168
206
  if (key === "policy.scriptHardening") {
169
207
  return withPolicyBoolean(config, "scriptHardening", rawValue);
170
208
  }
171
- if (key === "gitHook.onWarn") {
209
+ if (key === "scriptGate.mode") {
172
210
  return {
173
211
  ...config,
174
- gitHook: {
175
- ...config.gitHook,
176
- onWarn: parseOnWarn(rawValue)
212
+ scriptGate: {
213
+ ...config.scriptGate,
214
+ mode: parseScriptGateMode(rawValue)
177
215
  }
178
216
  };
179
217
  }
180
- if (key === "gitHook.onIncomplete") {
218
+ if (key === "scriptGate.observe") {
181
219
  return {
182
220
  ...config,
183
- gitHook: {
184
- ...config.gitHook,
185
- onIncomplete: parseOnIncomplete(rawValue)
221
+ scriptGate: {
222
+ ...config.scriptGate,
223
+ observe: parseBoolean(rawValue, key)
186
224
  }
187
225
  };
188
226
  }
189
- if (key === "audit.upload") {
227
+ if (key === "gitHook.onWarn") {
190
228
  return {
191
229
  ...config,
192
- audit: {
193
- upload: parseBoolean(rawValue, key)
230
+ gitHook: {
231
+ ...config.gitHook,
232
+ onWarn: parseOnWarn(rawValue)
194
233
  }
195
234
  };
196
235
  }
197
- if (key === "telemetry.enabled") {
236
+ if (key === "gitHook.onIncomplete") {
198
237
  return {
199
238
  ...config,
200
- telemetry: {
201
- enabled: parseBoolean(rawValue, key)
239
+ gitHook: {
240
+ ...config.gitHook,
241
+ onIncomplete: parseOnIncomplete(rawValue)
202
242
  }
203
243
  };
204
244
  }
245
+ if (key === "cooldown.age") {
246
+ return withCooldown(config, { age: parseCooldownAge(rawValue, key, false) });
247
+ }
248
+ if (key === "cooldown.npm.age") {
249
+ return withCooldown(config, { npmAge: parseCooldownAge(rawValue, key, true) });
250
+ }
251
+ if (key === "cooldown.pypi.age") {
252
+ return withCooldown(config, { pypiAge: parseCooldownAge(rawValue, key, true) });
253
+ }
254
+ if (key === "cooldown.cargo.age") {
255
+ return withCooldown(config, { cargoAge: parseCooldownAge(rawValue, key, true) });
256
+ }
257
+ if (key === "cooldown.onUnknown") {
258
+ return withCooldown(config, { onUnknown: parseCooldownOnUnknown(rawValue) });
259
+ }
260
+ if (key === "cooldown.exempt") {
261
+ return withCooldown(config, { exempt: parseCooldownExempt(rawValue) });
262
+ }
205
263
  return {
206
264
  ...config,
207
- webhooks: {
208
- enabled: parseBoolean(rawValue, key)
265
+ audit: {
266
+ upload: parseBoolean(rawValue, key)
209
267
  }
210
268
  };
211
269
  }
@@ -225,10 +283,11 @@ function normalizeConfig(raw) {
225
283
  const api = fieldObject(raw, "api");
226
284
  const org = fieldObject(raw, "org");
227
285
  const policy = fieldObject(raw, "policy");
286
+ const scriptGate = fieldObject(raw, "scriptGate");
228
287
  const gitHook = fieldObject(raw, "gitHook");
288
+ const cooldown = fieldObject(raw, "cooldown");
229
289
  const audit = fieldObject(raw, "audit");
230
- const telemetry = fieldObject(raw, "telemetry");
231
- const webhooks = fieldObject(raw, "webhooks");
290
+ const scriptHardening = fieldBoolean(policy, "policy.scriptHardening", "scriptHardening") ?? DEFAULT_CONFIG.policy.scriptHardening;
232
291
  return {
233
292
  version: 1,
234
293
  api: {
@@ -241,20 +300,26 @@ function normalizeConfig(raw) {
241
300
  mode: parsePolicyMode(fieldString(policy, "policy.mode", "mode") ?? DEFAULT_CONFIG.policy.mode),
242
301
  trustProjectAllowlists: fieldBoolean(policy, "policy.trustProjectAllowlists", "trustProjectAllowlists") ?? DEFAULT_CONFIG.policy.trustProjectAllowlists,
243
302
  allowForceOverride: fieldBoolean(policy, "policy.allowForceOverride", "allowForceOverride") ?? DEFAULT_CONFIG.policy.allowForceOverride,
244
- scriptHardening: fieldBoolean(policy, "policy.scriptHardening", "scriptHardening") ?? DEFAULT_CONFIG.policy.scriptHardening
303
+ scriptHardening
304
+ },
305
+ scriptGate: {
306
+ mode: parseScriptGateMode(fieldString(scriptGate, "scriptGate.mode", "mode") ?? (scriptHardening ? "enforce" : DEFAULT_CONFIG.scriptGate.mode)),
307
+ observe: fieldBoolean(scriptGate, "scriptGate.observe", "observe") ?? DEFAULT_CONFIG.scriptGate.observe
245
308
  },
246
309
  gitHook: {
247
310
  onWarn: parseOnWarn(fieldString(gitHook, "gitHook.onWarn", "onWarn") ?? DEFAULT_CONFIG.gitHook.onWarn),
248
311
  onIncomplete: parseOnIncomplete(fieldString(gitHook, "gitHook.onIncomplete", "onIncomplete") ?? DEFAULT_CONFIG.gitHook.onIncomplete)
249
312
  },
313
+ cooldown: {
314
+ age: parseCooldownAge(fieldString(cooldown, "cooldown.age", "age") ?? DEFAULT_CONFIG.cooldown.age, "cooldown.age", false),
315
+ npmAge: parseCooldownAge(fieldString(cooldown, "cooldown.npm.age", "npmAge") ?? DEFAULT_CONFIG.cooldown.npmAge, "cooldown.npm.age", true),
316
+ pypiAge: parseCooldownAge(fieldString(cooldown, "cooldown.pypi.age", "pypiAge") ?? DEFAULT_CONFIG.cooldown.pypiAge, "cooldown.pypi.age", true),
317
+ cargoAge: parseCooldownAge(fieldString(cooldown, "cooldown.cargo.age", "cargoAge") ?? DEFAULT_CONFIG.cooldown.cargoAge, "cooldown.cargo.age", true),
318
+ onUnknown: parseCooldownOnUnknown(fieldString(cooldown, "cooldown.onUnknown", "onUnknown") ?? DEFAULT_CONFIG.cooldown.onUnknown),
319
+ exempt: parseCooldownExempt(fieldString(cooldown, "cooldown.exempt", "exempt") ?? DEFAULT_CONFIG.cooldown.exempt)
320
+ },
250
321
  audit: {
251
322
  upload: fieldBoolean(audit, "audit.upload", "upload") ?? DEFAULT_CONFIG.audit.upload
252
- },
253
- telemetry: {
254
- enabled: fieldBoolean(telemetry, "telemetry.enabled", "enabled") ?? DEFAULT_CONFIG.telemetry.enabled
255
- },
256
- webhooks: {
257
- enabled: fieldBoolean(webhooks, "webhooks.enabled", "enabled") ?? DEFAULT_CONFIG.webhooks.enabled
258
323
  }
259
324
  };
260
325
  }
@@ -321,6 +386,12 @@ function parsePolicyMode(value) {
321
386
  }
322
387
  throw new ConfigError("policy.mode must be one of: off, warn, block, strict");
323
388
  }
389
+ function parseScriptGateMode(value) {
390
+ if (value === "observe" || value === "enforce" || value === "off") {
391
+ return value;
392
+ }
393
+ throw new ConfigError("scriptGate.mode must be one of: observe, enforce, off");
394
+ }
324
395
  function parseOnWarn(value) {
325
396
  if (value === "prompt" || value === "allow" || value === "block") {
326
397
  return value;
@@ -333,6 +404,45 @@ function parseOnIncomplete(value) {
333
404
  }
334
405
  throw new ConfigError("gitHook.onIncomplete must be one of: allow, block");
335
406
  }
407
+ const COOLDOWN_AGE_RE = /^([1-9]\d{0,3})(h|d)$/;
408
+ const COOLDOWN_EXEMPT_ENTRY_RE = /^[@A-Za-z0-9][@A-Za-z0-9._/-]*\*?$/;
409
+ export function parseCooldownAge(value, field, allowEmpty) {
410
+ const trimmed = value.trim();
411
+ if (trimmed === "" && allowEmpty) {
412
+ return "";
413
+ }
414
+ if (trimmed === "0" || trimmed === "off") {
415
+ return "0";
416
+ }
417
+ if (COOLDOWN_AGE_RE.test(trimmed)) {
418
+ return trimmed;
419
+ }
420
+ throw new ConfigError(`${field} must be a duration like 24h or 7d, or 0 to disable${allowEmpty ? ", or empty to inherit cooldown.age" : ""}`);
421
+ }
422
+ function parseCooldownOnUnknown(value) {
423
+ if (value === "allow" || value === "block") {
424
+ return value;
425
+ }
426
+ throw new ConfigError("cooldown.onUnknown must be one of: allow, block");
427
+ }
428
+ function parseCooldownExempt(value) {
429
+ const entries = value.split(",").map((entry) => entry.trim()).filter(Boolean);
430
+ for (const entry of entries) {
431
+ if (!COOLDOWN_EXEMPT_ENTRY_RE.test(entry)) {
432
+ throw new ConfigError(`cooldown.exempt entry '${entry}' is not a valid package name or pattern (use names or globs like @org/*)`);
433
+ }
434
+ }
435
+ return entries.join(",");
436
+ }
437
+ function withCooldown(config, patch) {
438
+ return {
439
+ ...config,
440
+ cooldown: {
441
+ ...config.cooldown,
442
+ ...patch
443
+ }
444
+ };
445
+ }
336
446
  function parseBoolean(value, field) {
337
447
  if (value === "true") {
338
448
  return true;
@@ -359,24 +469,3 @@ function parseUrl(value) {
359
469
  throw new ConfigError("api.baseUrl must be an absolute URL");
360
470
  }
361
471
  }
362
- function writeJsonAtomic(path, value) {
363
- mkdirSync(dirname(path), {
364
- recursive: true,
365
- mode: 0o700
366
- });
367
- const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
368
- try {
369
- writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, {
370
- encoding: "utf8",
371
- flag: "wx",
372
- mode: 0o600
373
- });
374
- renameSync(tempPath, path);
375
- }
376
- catch (error) {
377
- rmSync(tempPath, {
378
- force: true
379
- });
380
- throw error;
381
- }
382
- }
@@ -0,0 +1,128 @@
1
+ export function packageKey(name, version) {
2
+ return `${name}@${version}`;
3
+ }
4
+ export function findingCategory(finding) {
5
+ return finding.category ?? finding.id ?? "unknown";
6
+ }
7
+ export function findingFingerprint(findings) {
8
+ const fingerprint = {};
9
+ for (const finding of findings) {
10
+ const category = findingCategory(finding);
11
+ fingerprint[category] = Math.max(fingerprint[category] ?? 0, finding.severity);
12
+ }
13
+ return fingerprint;
14
+ }
15
+ export function matchDecision(pkg, ecosystem, entries, now = new Date()) {
16
+ if ((pkg.action ?? "pass") !== "warn") {
17
+ return { acknowledged: false };
18
+ }
19
+ const fingerprint = findingFingerprint(pkg.findings);
20
+ let nearestUncovered;
21
+ for (const entry of entries) {
22
+ if (entry.ecosystem !== ecosystem || entry.name !== pkg.name) {
23
+ continue;
24
+ }
25
+ if (!scopeMatches(entry, pkg.version)) {
26
+ continue;
27
+ }
28
+ if (isExpired(entry, now)) {
29
+ continue;
30
+ }
31
+ const uncovered = uncoveredFindings(fingerprint, entry.findings);
32
+ if (uncovered.length === 0) {
33
+ return { acknowledged: true, entry };
34
+ }
35
+ if (!nearestUncovered || uncovered.length < nearestUncovered.length) {
36
+ nearestUncovered = uncovered;
37
+ }
38
+ }
39
+ return nearestUncovered ? { acknowledged: false, newFindings: nearestUncovered } : { acknowledged: false };
40
+ }
41
+ export function applyDecisions(packages, ecosystemOf, file, rawAction, now = new Date()) {
42
+ const annotations = {};
43
+ let acknowledgedCount = 0;
44
+ let worst = 0;
45
+ for (const pkg of packages) {
46
+ const action = pkg.action ?? "pass";
47
+ if (action === "pass") {
48
+ continue;
49
+ }
50
+ const ecosystem = ecosystemOf(pkg);
51
+ if (!ecosystem) {
52
+ worst = Math.max(worst, actionSeverity(action));
53
+ continue;
54
+ }
55
+ const match = file.readable ? matchDecision(pkg, ecosystem, file.decisions, now) : { acknowledged: false };
56
+ const key = packageKey(pkg.name, pkg.version);
57
+ if (match.acknowledged) {
58
+ acknowledgedCount += 1;
59
+ annotations[key] = {
60
+ ecosystem,
61
+ acknowledged: {
62
+ decisionId: match.entry.id,
63
+ by: match.entry.acceptedBy,
64
+ at: match.entry.acceptedAt,
65
+ reason: match.entry.reason
66
+ }
67
+ };
68
+ }
69
+ else {
70
+ annotations[key] = {
71
+ ecosystem,
72
+ ...(match.newFindings ? { newFindings: match.newFindings } : {})
73
+ };
74
+ worst = Math.max(worst, actionSeverity(action));
75
+ }
76
+ }
77
+ const effectiveAction = acknowledgedCount === 0 || worst >= actionSeverity(rawAction) ? rawAction : actionFromSeverity(worst);
78
+ return {
79
+ file: file.path,
80
+ acknowledgedCount,
81
+ effectiveAction,
82
+ packages: annotations
83
+ };
84
+ }
85
+ const ACTION_SEVERITY = {
86
+ pass: 0,
87
+ analysis_incomplete: 1,
88
+ warn: 2,
89
+ block: 3
90
+ };
91
+ function actionSeverity(action) {
92
+ return ACTION_SEVERITY[action] ?? 0;
93
+ }
94
+ function actionFromSeverity(severity) {
95
+ if (severity >= 3) {
96
+ return "block";
97
+ }
98
+ if (severity === 2) {
99
+ return "warn";
100
+ }
101
+ if (severity === 1) {
102
+ return "analysis_incomplete";
103
+ }
104
+ return "pass";
105
+ }
106
+ function scopeMatches(entry, version) {
107
+ if (entry.scope.kind === "any") {
108
+ return true;
109
+ }
110
+ return entry.scope.version === version;
111
+ }
112
+ function isExpired(entry, now) {
113
+ if (!entry.expiresAt) {
114
+ return false;
115
+ }
116
+ const expiry = Date.parse(entry.expiresAt);
117
+ return !Number.isFinite(expiry) || expiry <= now.getTime();
118
+ }
119
+ function uncoveredFindings(fingerprint, accepted) {
120
+ const uncovered = [];
121
+ for (const [category, severity] of Object.entries(fingerprint)) {
122
+ const acceptedSeverity = accepted[category];
123
+ if (acceptedSeverity === undefined || severity > acceptedSeverity) {
124
+ uncovered.push(`${category}:${severity}`);
125
+ }
126
+ }
127
+ return uncovered.sort();
128
+ }
@@ -0,0 +1,92 @@
1
+ import { recordAuditEvent } from "../audit/events.js";
2
+ import { loadUserConfig } from "../config/settings.js";
3
+ import { promptText, promptYesNo } from "../install-ui/prompt.js";
4
+ import { dirname } from "node:path";
5
+ import { appendDecisions, mutateDgFile } from "../project/dgfile.js";
6
+ import { promptLine as ttyPromptLine, promptYesNo as ttyPromptYesNo } from "../util/tty-prompt.js";
7
+ import { findingFingerprint, packageKey } from "./apply.js";
8
+ const defaultSyncPrompts = {
9
+ yesNo: (question, defaultYes) => ttyPromptYesNo(question, defaultYes),
10
+ line: (question) => ttyPromptLine(question),
11
+ write: (text) => process.stderr.write(text)
12
+ };
13
+ export async function offerRememberOnIo(options) {
14
+ const { io, file, packages } = options;
15
+ if (!io.isTTY || !file.readable || packages.length === 0) {
16
+ return false;
17
+ }
18
+ const choice = (await promptText(" Remember this acceptance? [v] this version / [o] just once: ", io)).trim().toLowerCase();
19
+ if (choice !== "v" && choice !== "version") {
20
+ return false;
21
+ }
22
+ if (!file.exists) {
23
+ const create = await promptYesNo(` Create ${file.path}?`, io, false);
24
+ if (!create) {
25
+ return false;
26
+ }
27
+ }
28
+ const reason = (await promptText(" Reason (Enter to skip): ", io)).trim();
29
+ persistRemembered(file, packages, {
30
+ reason: reason || `accepted at ${options.surface}`,
31
+ acceptedBy: options.acceptedBy,
32
+ env: options.env ?? process.env
33
+ });
34
+ io.output.write(` ✓ remembered in ${file.path} — review with 'dg decisions'\n`);
35
+ return true;
36
+ }
37
+ export function offerRememberSync(options) {
38
+ const { file, packages } = options;
39
+ const prompts = options.prompts ?? defaultSyncPrompts;
40
+ if (!file.readable || packages.length === 0) {
41
+ return false;
42
+ }
43
+ const remember = prompts.yesNo(" Remember this acceptance in dg.json for future commits?", false);
44
+ if (remember !== true) {
45
+ return false;
46
+ }
47
+ if (!file.exists) {
48
+ const create = prompts.yesNo(` Create ${file.path}?`, false);
49
+ if (create !== true) {
50
+ return false;
51
+ }
52
+ }
53
+ const reason = (prompts.line(" Reason (Enter to skip): ") ?? "").trim();
54
+ persistRemembered(file, packages, {
55
+ reason: reason || `accepted at ${options.surface}`,
56
+ acceptedBy: options.acceptedBy,
57
+ env: options.env ?? process.env
58
+ });
59
+ prompts.write(` ✓ remembered in ${file.path} — review with 'dg decisions'\n`);
60
+ return true;
61
+ }
62
+ export function persistRemembered(file, packages, options) {
63
+ const additions = packages.map((pkg) => ({
64
+ ecosystem: pkg.ecosystem,
65
+ name: pkg.name,
66
+ scope: { kind: "exact", version: pkg.version },
67
+ findings: findingFingerprint(pkg.findings),
68
+ reason: options.reason,
69
+ acceptedBy: options.acceptedBy
70
+ }));
71
+ mutateDgFile(dirname(file.path), options.env, (current) => appendDecisions(current, additions));
72
+ recordDecisionEvents("decision.accepted", packages.map((pkg) => `${pkg.ecosystem}:${packageKey(pkg.name, pkg.version)}`), options.reason, options.env);
73
+ }
74
+ export function recordDecisionEvents(type, packageNames, reason, env) {
75
+ let policyMode = "unknown";
76
+ try {
77
+ policyMode = loadUserConfig(env).policy.mode;
78
+ }
79
+ catch {
80
+ // a corrupt user config must not block recording the decision trail
81
+ }
82
+ for (const packageName of packageNames) {
83
+ const event = {
84
+ type,
85
+ packageName,
86
+ reason,
87
+ policyMode,
88
+ createdAt: new Date().toISOString()
89
+ };
90
+ recordAuditEvent(event, env);
91
+ }
92
+ }