aienvmp 0.1.45 → 0.1.47

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/BUGFIXES.md CHANGED
@@ -58,6 +58,18 @@ Short record of bugs, fixes, and follow-up checks.
58
58
  - Fix: `aienvmp checkpoint` now performs the post-change loop and records an explicit sync ledger entry so follow-ups can be closed.
59
59
  - Verification: checkpoint tests cover text/JSON output, status artifact refresh, timeline entries, handoff recording, and cleared follow-ups.
60
60
 
61
+ ### Light SBOM risk required too much parsing
62
+
63
+ - Issue: AI consumers could see package and vulnerability details, but had to infer risk level and next commands from several nested fields.
64
+ - Fix: `lightSbom.riskSummary` and preflight `sbomRisk` now provide a compact risk score, signals, review targets, and advisory next steps.
65
+ - Verification: tests cover risk scoring, top-risk severity fallback, scanner-off guidance, status/context exposure, dashboard rendering, and recommended actions.
66
+
67
+ ### Light SBOM was only nested inside larger artifacts
68
+
69
+ - Issue: CI tools and AI agents that only need dependency/SBOM context had to read the full manifest or context payload.
70
+ - Fix: `aienvmp sbom --json` and `.aienvmp/sbom.json` now expose a standalone light SBOM artifact.
71
+ - Verification: tests cover standalone SBOM construction, writing, sync output, schema metadata, and dashboard linking.
72
+
61
73
  ## Template
62
74
 
63
75
  ### Title
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.47
4
+
5
+ - Added `aienvmp sbom` for standalone AI-readable light SBOM output without deep manifest parsing.
6
+ - Added `.aienvmp/sbom.json` writing through `aienvmp sbom --write` and the default `sync` flow.
7
+ - Added the SBOM artifact path to the shared preflight artifacts map.
8
+ - Added SBOM output metadata to `aienvmp schema --json`.
9
+ - Added a dashboard Light SBOM Artifact card linking to `sbom.json`.
10
+ - Updated README outputs and commands with the standalone SBOM artifact.
11
+ - Added regression tests for SBOM artifact building, writing, sync output, schema contract, and dashboard rendering.
12
+
13
+ ## 0.1.46
14
+
15
+ - Added `lightSbom.riskSummary` with compact risk level, score, scanner state, signals, review targets, and next commands.
16
+ - Added `sbomRisk` to the shared AI preflight contract so status/context consumers can read SBOM risk without deep parsing.
17
+ - Surfaced SBOM risk in `context --json`, `AIENV.md`, and the dashboard Light SBOM card.
18
+ - Added recommended actions for read-only security scans and high light-SBOM risk review.
19
+ - Used top-risk package severity as a fallback when scanner summary severity counts are incomplete.
20
+ - Updated dependency change hints to point agents to `aienvmp checkpoint` after accepted dependency changes.
21
+ - Added regression tests for risk scoring, scanner-off guidance, status/context outputs, dashboard rendering, and recommended actions.
22
+
3
23
  ## 0.1.45
4
24
 
5
25
  - Added `aienvmp checkpoint` as a one-command post-environment-change flow for record, sync, status, and handoff.
package/README.md CHANGED
@@ -35,6 +35,7 @@ Warnings are advisory by default. Use `doctor --strict <scope>` only when you wa
35
35
  AIENV.md # Markdown env map for AI agents
36
36
  .aienvmp/status.json # first AI read: clear/review, next command, nextAgent hint
37
37
  .aienvmp/manifest.json # runtime map + light SBOM
38
+ .aienvmp/sbom.json # standalone AI-readable light SBOM
38
39
  .aienvmp/intents.jsonl # planned env changes
39
40
  .aienvmp/timeline.jsonl # append-only change ledger
40
41
  .aienvmp/plan.md # read-only action plan
@@ -47,6 +48,7 @@ AIENV.md # Markdown env map for AI agents
47
48
  - `schema --json` prints the stable AI-readable output contract without scanning.
48
49
  - `status.json.nextAgent` tells the next AI what to read and whether to review first.
49
50
  - `dependencyReadSet` lists manifests and lockfiles before package or security changes.
51
+ - `sbomRisk` gives AI a compact light-SBOM risk level, signals, and next command.
50
52
  - `coordination.conflictTargets` shows where multiple agents are planning changes.
51
53
  - `agentActivity.multiActorTargets` shows where multiple agents actually recorded env changes.
52
54
  - `followUps` shows records that still need `sync`, `status`, or `handoff`.
@@ -71,6 +73,7 @@ npx aienvmp snippet gemini
71
73
  aienvmp sync # update env map, status, ledger, dashboard
72
74
  aienvmp status --write # refresh compact AI status
73
75
  aienvmp context --json # AI decision contract
76
+ aienvmp sbom --json # standalone light SBOM
74
77
  aienvmp schema --json # stable output contract for AI/CI consumers
75
78
  aienvmp plan --write # read-only action plan
76
79
  aienvmp handoff --record # next-agent summary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/actions.js CHANGED
@@ -20,6 +20,7 @@ export function recommendedActions(manifest = {}, context = {}) {
20
20
  }
21
21
 
22
22
  actions.push(...securityActions(manifest.security));
23
+ actions.push(...sbomRiskActions(manifest.lightSbom?.riskSummary));
23
24
 
24
25
  if (hasRuntimePolicyWarning(warnings)) {
25
26
  actions.push(action("review-version-policy", "medium", "runtime", "Detected runtime or package manager policy drift. Prefer project-local version files and ask before global changes."));
@@ -36,6 +37,18 @@ export function recommendedActions(manifest = {}, context = {}) {
36
37
  return dedupeActions(actions).slice(0, 8);
37
38
  }
38
39
 
40
+ function sbomRiskActions(risk = {}) {
41
+ const actions = [];
42
+ if (!risk.level) return actions;
43
+ if (risk.scanner === "off" && risk.vulnerabilityCount === 0) {
44
+ actions.push(action("scan-sbom-risk", "medium", "security", "Run a read-only security scan before dependency or release decisions.", "aienvmp sync --security"));
45
+ }
46
+ if (["urgent", "high"].includes(risk.level)) {
47
+ actions.push(action("review-sbom-risk", "high", "security", `Review light SBOM risk summary before dependency changes: ${(risk.signals || []).slice(0, 2).join("; ") || risk.level}.`, "aienvmp plan --write"));
48
+ }
49
+ return actions;
50
+ }
51
+
39
52
  function securityActions(security = {}) {
40
53
  if (!security.enabled) return [];
41
54
 
package/src/cli.js CHANGED
@@ -15,6 +15,7 @@ import { planWorkspace } from "./commands/plan.js";
15
15
  import { statusWorkspace } from "./commands/status.js";
16
16
  import { schemaWorkspace } from "./commands/schema.js";
17
17
  import { checkpointWorkspace } from "./commands/checkpoint.js";
18
+ import { sbomWorkspace } from "./commands/sbom.js";
18
19
  import { readFileSync } from "node:fs";
19
20
 
20
21
  const commands = new Map([
@@ -34,7 +35,8 @@ const commands = new Map([
34
35
  ["plan", planWorkspace],
35
36
  ["status", statusWorkspace],
36
37
  ["schema", schemaWorkspace],
37
- ["checkpoint", checkpointWorkspace]
38
+ ["checkpoint", checkpointWorkspace],
39
+ ["sbom", sbomWorkspace]
38
40
  ]);
39
41
 
40
42
  const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
@@ -112,6 +114,7 @@ Usage:
112
114
  aienvmp handoff [--dir .] [--json] [--record --actor agent:id]
113
115
  aienvmp checkpoint [--dir .] --actor agent:id --summary "what changed" [--target dependency] [--json]
114
116
  aienvmp plan [--dir .] [--json] [--write]
117
+ aienvmp sbom [--dir .] [--json] [--write]
115
118
  aienvmp schema [--json]
116
119
 
117
120
  Common:
@@ -121,6 +124,7 @@ Common:
121
124
  aienvmp handoff print the next-agent handoff summary
122
125
  aienvmp checkpoint record, sync, status, and handoff after an env change
123
126
  aienvmp plan print a read-only AI environment action plan
127
+ aienvmp sbom print/write standalone light SBOM artifact
124
128
  aienvmp schema print the stable AI-readable output contract
125
129
  aienvmp snippet print an AGENTS.md pointer snippet
126
130
  aienvmp dash regenerate/open the lightweight dashboard
@@ -136,6 +140,7 @@ Advanced:
136
140
  aienvmp compile [--dir .]
137
141
  aienvmp diff [--dir .]
138
142
  aienvmp doctor [--dir .] [--json] [--ci] [--strict security|policy|coordination|all]
143
+ aienvmp sbom [--dir .] [--json] [--write]
139
144
  aienvmp dash [--dir .] [--open]
140
145
  `);
141
146
  }
@@ -27,6 +27,7 @@ export async function contextWorkspace(args) {
27
27
  status: warnings.length ? "review-required" : "clear",
28
28
  preflight,
29
29
  coordination: preflight.coordination,
30
+ sbomRisk: preflight.sbomRisk,
30
31
  followUps: preflight.followUps,
31
32
  decision,
32
33
  enforcement: enforcementAdvice(warnings),
@@ -75,6 +76,7 @@ function lightSbomSummary(lightSbom = {}) {
75
76
  guidance: "No lockfile policy detected."
76
77
  },
77
78
  dependencyChangeHints: (lightSbom.dependencyChangeHints || []).slice(0, 8),
79
+ riskSummary: lightSbom.riskSummary || {},
78
80
  source: lightSbom.source || {},
79
81
  confidence: lightSbom.confidence || {},
80
82
  limitations: lightSbom.limitations || [],
@@ -0,0 +1,51 @@
1
+ import { readJson, writeJson } from "../fsutil.js";
2
+ import { manifestPath, sbomJsonPath, workspaceDir } from "../paths.js";
3
+
4
+ export async function sbomWorkspace(args = {}) {
5
+ const dir = workspaceDir(args);
6
+ const manifest = await readJson(manifestPath(dir));
7
+ if (!manifest) throw new Error("missing manifest; run `aienvmp sync` first");
8
+ const sbom = buildSbomArtifact(manifest);
9
+ const artifact = args.write ? await writeSbomArtifact(dir, sbom) : "";
10
+ const output = artifact ? { ...sbom, artifact } : sbom;
11
+ if (args.json || args.write || args.quiet) {
12
+ if (args.json) console.log(JSON.stringify(output, null, 2));
13
+ } else {
14
+ console.log(`sbom: ${sbom.riskSummary.level}/${sbom.riskSummary.score}`);
15
+ console.log(`packages: ${sbom.summary.packages || 0}`);
16
+ console.log(`vulnerabilities: ${sbom.summary.vulnerabilities || 0}`);
17
+ console.log(`next: ${sbom.riskSummary.next}`);
18
+ }
19
+ return output;
20
+ }
21
+
22
+ export function buildSbomArtifact(manifest = {}) {
23
+ const lightSbom = manifest.lightSbom || {};
24
+ return {
25
+ schemaVersion: 1,
26
+ schemaName: "aienvmp.light-sbom",
27
+ generatedAt: manifest.generatedAt || "",
28
+ workspace: manifest.workspace || {},
29
+ mode: lightSbom.mode || "light-sbom",
30
+ source: lightSbom.source || {},
31
+ confidence: lightSbom.confidence || {},
32
+ limitations: lightSbom.limitations || [],
33
+ summary: lightSbom.summary || { packages: 0, vulnerabilities: 0 },
34
+ riskSummary: lightSbom.riskSummary || { level: "clear", score: 0, signals: [], commands: [] },
35
+ topRisk: (lightSbom.topRisk || []).slice(0, 20),
36
+ packageManagerPolicy: lightSbom.packageManagerPolicy || {},
37
+ dependencyChangeHints: (lightSbom.dependencyChangeHints || []).slice(0, 20),
38
+ aiUse: {
39
+ purpose: "Standalone AI-readable light SBOM artifact.",
40
+ readBefore: "Dependency changes, vulnerability remediation, release review, or shared AI handoff.",
41
+ nextCommand: lightSbom.riskSummary?.commands?.[0] || "aienvmp context --json",
42
+ rule: "Use as a lightweight planning map; verify security claims with dedicated scanners."
43
+ }
44
+ };
45
+ }
46
+
47
+ export async function writeSbomArtifact(dir, sbom) {
48
+ const out = sbomJsonPath(dir);
49
+ await writeJson(out, sbom);
50
+ return out;
51
+ }
@@ -3,6 +3,7 @@ import { scanWorkspace } from "./scan.js";
3
3
  import { compileWorkspace } from "./compile.js";
4
4
  import { dashWorkspace } from "./dash.js";
5
5
  import { statusWorkspace } from "./status.js";
6
+ import { sbomWorkspace } from "./sbom.js";
6
7
 
7
8
  export async function syncWorkspace(args) {
8
9
  const quiet = args.quiet || args.json;
@@ -13,6 +14,7 @@ export async function syncWorkspace(args) {
13
14
  const compiled = await compileWorkspace(next);
14
15
  const dashboard = await dashWorkspace(next);
15
16
  const status = await statusWorkspace({ ...next, json: false, write: true, quiet: true });
17
+ const sbom = await sbomWorkspace({ ...next, json: false, write: true, quiet: true });
16
18
 
17
19
  const result = {
18
20
  status: "ok",
@@ -21,6 +23,7 @@ export async function syncWorkspace(args) {
21
23
  manifest: scanned.manifest,
22
24
  timeline: scanned.timeline,
23
25
  status: status.artifact,
26
+ sbom: sbom.artifact,
24
27
  dashboard: dashboard.dashboard
25
28
  },
26
29
  changes: scanned.changes,
package/src/contract.js CHANGED
@@ -4,7 +4,7 @@ export function preflightContract() {
4
4
  version: 1,
5
5
  stability: "additive",
6
6
  requiredFields: ["schemaVersion", "state", "decision", "quickstart", "commands", "artifacts"],
7
- aiEntryFields: ["state", "summary", "nextAgent", "coordination", "agentActivity", "followUps", "dependencyReadSet", "dependencyChangeProtocol"],
7
+ aiEntryFields: ["state", "summary", "nextAgent", "coordination", "agentActivity", "sbomRisk", "followUps", "dependencyReadSet", "dependencyChangeProtocol"],
8
8
  rule: "Consumers should ignore unknown fields and treat missing optional fields as unavailable."
9
9
  };
10
10
  }
@@ -31,6 +31,11 @@ export function schemaContract() {
31
31
  manifest: {
32
32
  file: ".aienvmp/manifest.json",
33
33
  rootFields: ["schemaVersion", "workspace", "runtimes", "packageManagers", "dependencySnapshot", "lightSbom", "security", "trust"]
34
+ },
35
+ sbom: {
36
+ file: ".aienvmp/sbom.json",
37
+ command: "aienvmp sbom --json",
38
+ rootFields: ["schemaVersion", "schemaName", "workspace", "summary", "riskSummary", "topRisk", "packageManagerPolicy", "dependencyChangeHints"]
34
39
  }
35
40
  },
36
41
  compatibility: {
@@ -108,6 +108,7 @@ export function buildLightSbom(snapshot = {}, security = {}) {
108
108
  transitiveOrUnmatchedVulnerablePackages: transitiveOrUnmatched.length
109
109
  },
110
110
  topRisk,
111
+ riskSummary: lightSbomRiskSummary({ packages, security, directVulnerable, transitiveOrUnmatched, topRisk, lockfiles: snapshot.lockfiles || [] }),
111
112
  packageManagerPolicy: packageManagerPolicy(snapshot.lockfiles || []),
112
113
  dependencyChangeHints: dependencyChangeHints(packages, topRisk, snapshot.lockfiles || []),
113
114
  aiUse: {
@@ -119,6 +120,74 @@ export function buildLightSbom(snapshot = {}, security = {}) {
119
120
  };
120
121
  }
121
122
 
123
+ export function lightSbomRiskSummary({ packages = [], security = {}, directVulnerable = [], transitiveOrUnmatched = [], topRisk = [], lockfiles = [] } = {}) {
124
+ const signals = [];
125
+ let score = 0;
126
+ const critical = Math.max(Number(security.summary?.critical || 0), topRisk.filter((pkg) => pkg.severity === "critical").length);
127
+ const high = Math.max(Number(security.summary?.high || 0), topRisk.filter((pkg) => pkg.severity === "high").length);
128
+ const total = Number(security.summary?.total || 0);
129
+ if (critical > 0) {
130
+ score += 45;
131
+ signals.push(`${critical} critical vulnerability finding(s)`);
132
+ }
133
+ if (high > 0) {
134
+ score += 30;
135
+ signals.push(`${high} high vulnerability finding(s)`);
136
+ }
137
+ if (directVulnerable.length) {
138
+ score += 15;
139
+ signals.push(`${directVulnerable.length} vulnerable direct dependency package(s)`);
140
+ }
141
+ if (transitiveOrUnmatched.length) {
142
+ score += 8;
143
+ signals.push(`${transitiveOrUnmatched.length} vulnerable transitive or unmatched package(s)`);
144
+ }
145
+ if (packageManagerPolicy(lockfiles).status === "review-required") {
146
+ score += 10;
147
+ signals.push("mixed package manager lockfiles");
148
+ }
149
+ if (packages.length && security.enabled !== true) {
150
+ score += 5;
151
+ signals.push("security scanner summary is off");
152
+ }
153
+ const level = score >= 90 ? "urgent" : score >= 60 ? "high" : score >= 30 ? "medium" : score > 0 ? "low" : "clear";
154
+ const reviewTargets = [
155
+ ...new Set([
156
+ ...topRisk.map((pkg) => pkg.manifest).filter(Boolean),
157
+ ...topRisk.map((pkg) => pkg.name).filter(Boolean).slice(0, 5)
158
+ ])
159
+ ].slice(0, 8);
160
+ return {
161
+ level,
162
+ score,
163
+ signals,
164
+ scanner: security.enabled ? "enabled" : "off",
165
+ vulnerabilityCount: total,
166
+ directVulnerablePackages: directVulnerable.length,
167
+ reviewTargets,
168
+ next: riskNext(level, security.enabled, topRisk),
169
+ commands: riskCommands(level, security.enabled)
170
+ };
171
+ }
172
+
173
+ function riskNext(level, securityEnabled, topRisk = []) {
174
+ if (!securityEnabled) return "Run read-only security scan before dependency or release decisions.";
175
+ if (level === "urgent" || level === "high") return "Review dependency read set and topRisk before remediation; do not auto-fix without user approval.";
176
+ if (topRisk.length) return "Review topRisk packages before dependency changes.";
177
+ return "No SBOM risk signal requires action in the lightweight snapshot.";
178
+ }
179
+
180
+ function riskCommands(level, securityEnabled) {
181
+ const commands = [];
182
+ if (!securityEnabled) commands.push("aienvmp sync --security");
183
+ if (["urgent", "high", "medium"].includes(level)) {
184
+ commands.push("aienvmp intent --actor agent:id --action dependency-review --target dependency");
185
+ commands.push("aienvmp plan --write");
186
+ }
187
+ commands.push("aienvmp checkpoint --actor agent:id --summary dependency-review --target dependency");
188
+ return commands;
189
+ }
190
+
122
191
  function packageManagerPolicy(lockfiles = []) {
123
192
  const byEcosystem = {};
124
193
  for (const lockfile of lockfiles) {
@@ -196,8 +265,7 @@ function dependencyChangeHints(packages = [], topRisk = [], lockfiles = []) {
196
265
  ],
197
266
  afterChange: [
198
267
  "Run project tests or the narrowest relevant validation.",
199
- "Run aienvmp sync.",
200
- "Record the dependency change with aienvmp record."
268
+ "Run aienvmp checkpoint --actor agent:id --summary dependency-change --target dependency."
201
269
  ]
202
270
  }));
203
271
  }
package/src/paths.js CHANGED
@@ -16,6 +16,10 @@ export function statusJsonPath(dir) {
16
16
  return path.join(stateDir(dir), "status.json");
17
17
  }
18
18
 
19
+ export function sbomJsonPath(dir) {
20
+ return path.join(stateDir(dir), "sbom.json");
21
+ }
22
+
19
23
  export function previousManifestPath(dir) {
20
24
  return path.join(stateDir(dir), "manifest.previous.json");
21
25
  }
package/src/preflight.js CHANGED
@@ -16,6 +16,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = [], timel
16
16
  const coordination = coordinationSummary(intents);
17
17
  const followUps = pendingFollowUps(timeline);
18
18
  const agentActivity = agentActivitySummary(timeline);
19
+ const sbomRisk = manifest.lightSbom?.riskSummary || {};
19
20
  return {
20
21
  schemaVersion: 1,
21
22
  contract: preflightContract(),
@@ -57,6 +58,7 @@ export function buildPreflight(manifest = {}, warnings = [], intents = [], timel
57
58
  nextAgent: nextAgentHint(state, dependencyReadSet, dependencyChangeProtocol),
58
59
  coordination,
59
60
  agentActivity,
61
+ sbomRisk,
60
62
  followUps,
61
63
  intentTargets,
62
64
  dependencyReadSet,
@@ -351,6 +353,7 @@ export function preflightArtifacts() {
351
353
  status: ".aienvmp/status.json",
352
354
  envMap: "AIENV.md",
353
355
  manifest: ".aienvmp/manifest.json",
356
+ sbom: ".aienvmp/sbom.json",
354
357
  dashboard: ".aienvmp/dashboard.html",
355
358
  planJson: ".aienvmp/plan.json",
356
359
  planMarkdown: ".aienvmp/plan.md",
package/src/render.js CHANGED
@@ -416,10 +416,12 @@ const lightSbom=manifest.lightSbom||{};
416
416
  const lightSbomSummary=lightSbom.summary||{};
417
417
  const pmPolicy=lightSbom.packageManagerPolicy||{};
418
418
  const topRisk=lightSbom.topRisk||[];
419
+ const riskSummary=lightSbom.riskSummary||{};
419
420
  const dependencyHints=lightSbom.dependencyChangeHints||[];
420
421
  const dependencyHintsHtml=dependencyHints.length?'<div class="timeline">'+dependencyHints.slice(0,5).map(h=>\`<div class="event"><time>\${esc(h.ecosystem||'deps')}</time><div><b>\${esc(h.manifest)}</b> <code>\${esc(h.manager||'unknown')}</code> \${esc(h.packages||0)} packages\${h.riskPackages?.length?\`<div class="path">risk: \${esc(h.riskPackages.map(p=>p.name).join(', '))}</div>\`:''}<div class="path">\${esc((h.groups||[]).join(', ')||'no groups')}\${h.lockfiles?.length?\` / lockfiles: \${esc(h.lockfiles.map(l=>l.file).join(', '))}\`:''}</div></div></div>\`).join('')+'</div>':'<div class="okline">No dependency change hints available.</div>';
421
422
  const pmPolicyHtml='<table><tr><th>Status</th><td><code>'+esc(pmPolicy.status||'no-lockfile')+'</code></td></tr><tr><th>Guidance</th><td>'+esc(pmPolicy.guidance||'No lockfile policy detected.')+'</td></tr></table>';
422
- const lightSbomHtml=\`<table><tr><th>Packages</th><td><code>\${esc(lightSbomSummary.packages||0)}</code></td></tr><tr><th>Vulnerabilities</th><td><code>\${esc(lightSbomSummary.vulnerabilities||0)}</code></td></tr><tr><th>Direct vulnerable</th><td><code>\${esc(lightSbomSummary.directVulnerablePackages||0)}</code></td></tr><tr><th>Manifests</th><td><code>\${esc((lightSbomSummary.manifests||[]).join(', ')||'none')}</code></td></tr><tr><th>Lockfiles</th><td><code>\${esc((lightSbomSummary.lockfiles||[]).map(l=>l.file).join(', ')||'none')}</code></td></tr></table><h3 style="margin-top:12px">Package manager policy</h3>\${pmPolicyHtml}\${topRisk.length?'<div class="timeline">'+topRisk.slice(0,5).map(p=>\`<div class="event"><time>\${esc(p.priority)}</time><div><b>\${esc(p.name)}</b> \${esc(p.severity)} \${p.directDependency?'<code>direct</code>':'<code>transitive</code>'}<div class="path">\${esc(p.manifest||p.ecosystem)} \${esc(p.version||'')}</div></div></div>\`).join('')+'</div>':'<div class="okline">No high-risk package summary in the current light SBOM.</div>'}<h3 style="margin-top:12px">Dependency change hints</h3>\${dependencyHintsHtml}\`;
423
+ const riskSummaryHtml=riskSummary.level?\`<table><tr><th>Level</th><td><code>\${esc(riskSummary.level)}</code> \${esc(riskSummary.score||0)}</td></tr><tr><th>Scanner</th><td><code>\${esc(riskSummary.scanner||'unknown')}</code></td></tr><tr><th>Next</th><td>\${esc(riskSummary.next||'No SBOM action required.')}</td></tr><tr><th>Targets</th><td>\${esc((riskSummary.reviewTargets||[]).join(', ')||'none')}</td></tr></table>\${riskSummary.signals?.length?'<div class="timeline">'+riskSummary.signals.slice(0,5).map(s=>\`<div class="event"><time>risk</time><div>\${esc(s)}</div></div>\`).join('')+'</div>':''}\`:'<div class="okline">No risk summary available.</div>';
424
+ const lightSbomHtml=\`<table><tr><th>Packages</th><td><code>\${esc(lightSbomSummary.packages||0)}</code></td></tr><tr><th>Vulnerabilities</th><td><code>\${esc(lightSbomSummary.vulnerabilities||0)}</code></td></tr><tr><th>Direct vulnerable</th><td><code>\${esc(lightSbomSummary.directVulnerablePackages||0)}</code></td></tr><tr><th>Manifests</th><td><code>\${esc((lightSbomSummary.manifests||[]).join(', ')||'none')}</code></td></tr><tr><th>Lockfiles</th><td><code>\${esc((lightSbomSummary.lockfiles||[]).map(l=>l.file).join(', ')||'none')}</code></td></tr></table><h3 style="margin-top:12px">Risk summary</h3>\${riskSummaryHtml}<h3 style="margin-top:12px">Package manager policy</h3>\${pmPolicyHtml}\${topRisk.length?'<div class="timeline">'+topRisk.slice(0,5).map(p=>\`<div class="event"><time>\${esc(p.priority)}</time><div><b>\${esc(p.name)}</b> \${esc(p.severity)} \${p.directDependency?'<code>direct</code>':'<code>transitive</code>'}<div class="path">\${esc(p.manifest||p.ecosystem)} \${esc(p.version||'')}</div></div></div>\`).join('')+'</div>':'<div class="okline">No high-risk package summary in the current light SBOM.</div>'}<h3 style="margin-top:12px">Dependency change hints</h3>\${dependencyHintsHtml}\`;
423
425
  const sec=manifest.security||{};
424
426
  const secSummary=sec.summary||{total:0,critical:0,high:0,moderate:0,low:0,info:0};
425
427
  const secPackages=sec.topPackages||[];
@@ -440,6 +442,7 @@ const actions=manifest.recommendedActions||[];
440
442
  const actionsHtml=actions.length?'<div class="timeline">'+actions.slice(0,6).map(a=>\`<div class="event"><time>\${esc(a.priority)}</time><div><b>\${esc(a.category)}</b> \${esc(a.summary)}\${a.command?\`<div class="path">\${esc(a.command)}</div>\`:''}</div></div>\`).join('')+'</div>':'<div class="okline">No recommended actions. Continue project-local work.</div>';
441
443
  const plan=manifest.planArtifacts||{};
442
444
  const planHtml=plan.markdown||plan.json?\`<table><tr><th>Markdown</th><td>\${plan.markdown?'<a href="plan.md">plan.md</a>':'not written'}</td></tr><tr><th>JSON</th><td>\${plan.json?'<a href="plan.json">plan.json</a>':'not written'}</td></tr></table>\`:'<div class="okline">No plan artifacts yet. Run <code>aienvmp plan --write</code>.</div>';
445
+ const sbomArtifactHtml='<table><tr><th>JSON</th><td><a href="sbom.json">sbom.json</a></td></tr><tr><th>Command</th><td><code>aienvmp sbom --write</code></td></tr></table>';
443
446
  const remediation=manifest.planRemediation||[];
444
447
  const remediationFix=r=>r.fixVersions?.length?\`fix \${r.fixVersions.join(', ')}\`:(r.fixAvailable?'fix available':'review required');
445
448
  const remediationRefs=r=>r.advisories?.length?\` - \${r.advisories.join(', ')}\`:'';
@@ -528,6 +531,8 @@ document.getElementById('app').innerHTML=\`
528
531
  <div style="height:14px"></div>
529
532
  \${card('AI Plan Artifacts',plan.markdown||plan.json?'<span class="pill">written</span>':'<span class="pill off">not written</span>',planHtml)}
530
533
  <div style="height:14px"></div>
534
+ \${card('Light SBOM Artifact','<span class="pill">json</span>',sbomArtifactHtml)}
535
+ <div style="height:14px"></div>
531
536
  \${card('Remediation Steps',remediation.length?'<span class="pill warn">'+remediation.length+' items</span>':'<span class="pill off">none</span>',remediationHtml)}
532
537
  <div style="height:14px"></div>
533
538
  \${card('Environment Steps',envSteps.length?'<span class="pill warn">'+envSteps.length+' items</span>':'<span class="pill off">none</span>',envStepsHtml)}
@@ -633,6 +638,10 @@ function lightSbomLines(lightSbom = {}) {
633
638
  lines.push(`- Source: ${lightSbom.source?.dependencies || "project manifests"}; vulnerabilities: ${lightSbom.source?.vulnerabilities || "not scanned"}`);
634
639
  lines.push(`- Confidence: direct ${lightSbom.confidence?.directDependencies || "unknown"}; transitive ${lightSbom.confidence?.transitiveDependencies || "unknown"}`);
635
640
  }
641
+ if (lightSbom.riskSummary) {
642
+ lines.push(`- Risk summary: ${lightSbom.riskSummary.level || "clear"}/${lightSbom.riskSummary.score || 0}; ${lightSbom.riskSummary.next || "no action"}`);
643
+ if (lightSbom.riskSummary.signals?.length) lines.push(`- Risk signals: ${lightSbom.riskSummary.signals.join("; ")}`);
644
+ }
636
645
  if (lightSbom.packageManagerPolicy) {
637
646
  lines.push(`- Package manager policy: ${lightSbom.packageManagerPolicy.status}`);
638
647
  lines.push(`- Package manager guidance: ${lightSbom.packageManagerPolicy.guidance}`);