aienvmp 0.1.46 → 0.1.48
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 +12 -0
- package/CHANGELOG.md +20 -0
- package/README.md +4 -0
- package/package.json +1 -1
- package/src/cli.js +6 -1
- package/src/commands/sbom.js +143 -0
- package/src/commands/sync.js +5 -0
- package/src/contract.js +10 -0
- package/src/paths.js +8 -0
- package/src/preflight.js +2 -0
- package/src/render.js +3 -0
package/BUGFIXES.md
CHANGED
|
@@ -64,6 +64,18 @@ Short record of bugs, fixes, and follow-up checks.
|
|
|
64
64
|
- Fix: `lightSbom.riskSummary` and preflight `sbomRisk` now provide a compact risk score, signals, review targets, and advisory next steps.
|
|
65
65
|
- Verification: tests cover risk scoring, top-risk severity fallback, scanner-off guidance, status/context exposure, dashboard rendering, and recommended actions.
|
|
66
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
|
+
|
|
73
|
+
### Light SBOM needed a lightweight standard export
|
|
74
|
+
|
|
75
|
+
- Issue: the standalone SBOM was AI-friendly, but less convenient for tools expecting CycloneDX-shaped data.
|
|
76
|
+
- Fix: `aienvmp sbom --format cyclonedx-lite` and `.aienvmp/sbom.cdx.json` now export project-manifest packages in a CycloneDX-compatible shape with explicit limitations.
|
|
77
|
+
- Verification: tests cover component mapping, vulnerability hints, sync output, schema metadata, and dashboard links.
|
|
78
|
+
|
|
67
79
|
## Template
|
|
68
80
|
|
|
69
81
|
### Title
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.48
|
|
4
|
+
|
|
5
|
+
- Added `aienvmp sbom --format cyclonedx-lite` for a lightweight CycloneDX-compatible export.
|
|
6
|
+
- Added `.aienvmp/sbom.cdx.json` writing through the default `sync` flow.
|
|
7
|
+
- Added CycloneDX-lite artifact paths to status/preflight outputs and schema metadata.
|
|
8
|
+
- Added dashboard links for `sbom.cdx.json` beside the native light SBOM artifact.
|
|
9
|
+
- Added README guidance for the CycloneDX-lite command and output.
|
|
10
|
+
- Included explicit metadata limitations so consumers know no install or dependency resolver was run.
|
|
11
|
+
- Added regression tests for CycloneDX-lite component mapping, vulnerability hints, sync output, schema contract, and dashboard rendering.
|
|
12
|
+
|
|
13
|
+
## 0.1.47
|
|
14
|
+
|
|
15
|
+
- Added `aienvmp sbom` for standalone AI-readable light SBOM output without deep manifest parsing.
|
|
16
|
+
- Added `.aienvmp/sbom.json` writing through `aienvmp sbom --write` and the default `sync` flow.
|
|
17
|
+
- Added the SBOM artifact path to the shared preflight artifacts map.
|
|
18
|
+
- Added SBOM output metadata to `aienvmp schema --json`.
|
|
19
|
+
- Added a dashboard Light SBOM Artifact card linking to `sbom.json`.
|
|
20
|
+
- Updated README outputs and commands with the standalone SBOM artifact.
|
|
21
|
+
- Added regression tests for SBOM artifact building, writing, sync output, schema contract, and dashboard rendering.
|
|
22
|
+
|
|
3
23
|
## 0.1.46
|
|
4
24
|
|
|
5
25
|
- Added `lightSbom.riskSummary` with compact risk level, score, scanner state, signals, review targets, and next commands.
|
package/README.md
CHANGED
|
@@ -35,6 +35,8 @@ 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
|
|
39
|
+
.aienvmp/sbom.cdx.json # CycloneDX-lite export from project manifests
|
|
38
40
|
.aienvmp/intents.jsonl # planned env changes
|
|
39
41
|
.aienvmp/timeline.jsonl # append-only change ledger
|
|
40
42
|
.aienvmp/plan.md # read-only action plan
|
|
@@ -72,6 +74,8 @@ npx aienvmp snippet gemini
|
|
|
72
74
|
aienvmp sync # update env map, status, ledger, dashboard
|
|
73
75
|
aienvmp status --write # refresh compact AI status
|
|
74
76
|
aienvmp context --json # AI decision contract
|
|
77
|
+
aienvmp sbom --json # standalone light SBOM
|
|
78
|
+
aienvmp sbom --format cyclonedx-lite --json
|
|
75
79
|
aienvmp schema --json # stable output contract for AI/CI consumers
|
|
76
80
|
aienvmp plan --write # read-only action plan
|
|
77
81
|
aienvmp handoff --record # next-agent summary
|
package/package.json
CHANGED
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
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { readJson, writeJson } from "../fsutil.js";
|
|
2
|
+
import { cyclonedxSbomPath, 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 format = normalizeFormat(args.format);
|
|
9
|
+
const sbom = format === "cyclonedx-lite" ? buildCycloneDxLite(manifest) : buildSbomArtifact(manifest);
|
|
10
|
+
const artifact = args.write ? await writeSbomArtifact(dir, sbom, format) : "";
|
|
11
|
+
const output = artifact ? { ...sbom, artifact } : sbom;
|
|
12
|
+
if (args.json || args.write || args.quiet) {
|
|
13
|
+
if (args.json) console.log(JSON.stringify(output, null, 2));
|
|
14
|
+
} else {
|
|
15
|
+
console.log(`sbom: ${sbom.riskSummary.level}/${sbom.riskSummary.score}`);
|
|
16
|
+
console.log(`packages: ${sbom.summary.packages || 0}`);
|
|
17
|
+
console.log(`vulnerabilities: ${sbom.summary.vulnerabilities || 0}`);
|
|
18
|
+
console.log(`next: ${sbom.riskSummary.next}`);
|
|
19
|
+
}
|
|
20
|
+
return output;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildSbomArtifact(manifest = {}) {
|
|
24
|
+
const lightSbom = manifest.lightSbom || {};
|
|
25
|
+
return {
|
|
26
|
+
schemaVersion: 1,
|
|
27
|
+
schemaName: "aienvmp.light-sbom",
|
|
28
|
+
generatedAt: manifest.generatedAt || "",
|
|
29
|
+
workspace: manifest.workspace || {},
|
|
30
|
+
mode: lightSbom.mode || "light-sbom",
|
|
31
|
+
source: lightSbom.source || {},
|
|
32
|
+
confidence: lightSbom.confidence || {},
|
|
33
|
+
limitations: lightSbom.limitations || [],
|
|
34
|
+
summary: lightSbom.summary || { packages: 0, vulnerabilities: 0 },
|
|
35
|
+
riskSummary: lightSbom.riskSummary || { level: "clear", score: 0, signals: [], commands: [] },
|
|
36
|
+
topRisk: (lightSbom.topRisk || []).slice(0, 20),
|
|
37
|
+
packageManagerPolicy: lightSbom.packageManagerPolicy || {},
|
|
38
|
+
dependencyChangeHints: (lightSbom.dependencyChangeHints || []).slice(0, 20),
|
|
39
|
+
aiUse: {
|
|
40
|
+
purpose: "Standalone AI-readable light SBOM artifact.",
|
|
41
|
+
readBefore: "Dependency changes, vulnerability remediation, release review, or shared AI handoff.",
|
|
42
|
+
nextCommand: lightSbom.riskSummary?.commands?.[0] || "aienvmp context --json",
|
|
43
|
+
rule: "Use as a lightweight planning map; verify security claims with dedicated scanners."
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function writeSbomArtifact(dir, sbom) {
|
|
49
|
+
const out = sbom.bomFormat === "CycloneDX" ? cyclonedxSbomPath(dir) : sbomJsonPath(dir);
|
|
50
|
+
await writeJson(out, sbom);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildCycloneDxLite(manifest = {}) {
|
|
55
|
+
const snapshot = manifest.dependencySnapshot || {};
|
|
56
|
+
const packages = snapshot.packages || [];
|
|
57
|
+
const lightSbom = manifest.lightSbom || {};
|
|
58
|
+
return {
|
|
59
|
+
bomFormat: "CycloneDX",
|
|
60
|
+
specVersion: "1.6",
|
|
61
|
+
serialNumber: `urn:uuid:aienvmp-${hashText(`${manifest.workspace?.path || ""}:${manifest.generatedAt || ""}`)}`,
|
|
62
|
+
version: 1,
|
|
63
|
+
metadata: {
|
|
64
|
+
timestamp: manifest.generatedAt || "",
|
|
65
|
+
tools: {
|
|
66
|
+
components: [{
|
|
67
|
+
type: "application",
|
|
68
|
+
name: "aienvmp",
|
|
69
|
+
version: manifest.generatedBy?.version || "unknown"
|
|
70
|
+
}]
|
|
71
|
+
},
|
|
72
|
+
component: {
|
|
73
|
+
type: "application",
|
|
74
|
+
name: manifest.workspace?.name || "workspace",
|
|
75
|
+
bomRef: "workspace"
|
|
76
|
+
},
|
|
77
|
+
properties: [
|
|
78
|
+
{ name: "aienvmp:format", value: "cyclonedx-lite" },
|
|
79
|
+
{ name: "aienvmp:source", value: lightSbom.source?.dependencies || "project manifests" },
|
|
80
|
+
{ name: "aienvmp:confidence:transitiveDependencies", value: lightSbom.confidence?.transitiveDependencies || "not-resolved" },
|
|
81
|
+
{ name: "aienvmp:risk:level", value: lightSbom.riskSummary?.level || "clear" },
|
|
82
|
+
{ name: "aienvmp:risk:score", value: String(lightSbom.riskSummary?.score || 0) }
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
components: packages.slice(0, 200).map(cycloneComponent),
|
|
86
|
+
vulnerabilities: (lightSbom.topRisk || []).slice(0, 50).map(cycloneVulnerability),
|
|
87
|
+
properties: [
|
|
88
|
+
{ name: "aienvmp:limitation", value: "Light SBOM from project manifests only; no install or dependency resolver was run." },
|
|
89
|
+
{ name: "aienvmp:verifyWith", value: "CycloneDX, Syft, Trivy, npm audit, pip-audit, or another dedicated scanner before security claims." }
|
|
90
|
+
]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function cycloneComponent(pkg = {}) {
|
|
95
|
+
const version = String(pkg.version || "unspecified");
|
|
96
|
+
return {
|
|
97
|
+
type: "library",
|
|
98
|
+
name: pkg.name || "unknown",
|
|
99
|
+
version,
|
|
100
|
+
purl: packageUrl(pkg, version),
|
|
101
|
+
bomRef: `${pkg.ecosystem || "pkg"}:${pkg.name || "unknown"}@${version}`,
|
|
102
|
+
properties: [
|
|
103
|
+
{ name: "aienvmp:ecosystem", value: pkg.ecosystem || "unknown" },
|
|
104
|
+
{ name: "aienvmp:manager", value: pkg.manager || "unknown" },
|
|
105
|
+
{ name: "aienvmp:manifest", value: pkg.manifest || "" },
|
|
106
|
+
{ name: "aienvmp:group", value: pkg.group || "" }
|
|
107
|
+
]
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function cycloneVulnerability(pkg = {}) {
|
|
112
|
+
return {
|
|
113
|
+
id: pkg.name || "unknown",
|
|
114
|
+
source: { name: "aienvmp-light-sbom" },
|
|
115
|
+
ratings: [{ severity: pkg.severity || "unknown" }],
|
|
116
|
+
affects: [{
|
|
117
|
+
ref: `${pkg.ecosystem || "pkg"}:${pkg.name || "unknown"}@${pkg.version || "unspecified"}`
|
|
118
|
+
}],
|
|
119
|
+
properties: [
|
|
120
|
+
{ name: "aienvmp:priority", value: pkg.priority || "low" },
|
|
121
|
+
{ name: "aienvmp:score", value: String(pkg.score || 0) },
|
|
122
|
+
{ name: "aienvmp:directDependency", value: String(pkg.directDependency === true) },
|
|
123
|
+
{ name: "aienvmp:manifest", value: pkg.manifest || "" }
|
|
124
|
+
]
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function packageUrl(pkg = {}, version = "") {
|
|
129
|
+
const type = pkg.ecosystem === "python" ? "pypi" : "npm";
|
|
130
|
+
return `pkg:${type}/${encodeURIComponent(pkg.name || "unknown")}@${encodeURIComponent(version)}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeFormat(format = "") {
|
|
134
|
+
const value = String(format || "aienvmp").toLowerCase();
|
|
135
|
+
if (["cyclonedx", "cyclonedx-lite", "cdx"].includes(value)) return "cyclonedx-lite";
|
|
136
|
+
return "aienvmp";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function hashText(text = "") {
|
|
140
|
+
let hash = 0;
|
|
141
|
+
for (const ch of String(text)) hash = ((hash << 5) - hash + ch.charCodeAt(0)) >>> 0;
|
|
142
|
+
return `${hash.toString(16).padStart(8, "0")}-0000-4000-8000-000000000000`;
|
|
143
|
+
}
|
package/src/commands/sync.js
CHANGED
|
@@ -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,8 @@ 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 });
|
|
18
|
+
const cyclonedx = await sbomWorkspace({ ...next, json: false, write: true, quiet: true, format: "cyclonedx-lite" });
|
|
16
19
|
|
|
17
20
|
const result = {
|
|
18
21
|
status: "ok",
|
|
@@ -21,6 +24,8 @@ export async function syncWorkspace(args) {
|
|
|
21
24
|
manifest: scanned.manifest,
|
|
22
25
|
timeline: scanned.timeline,
|
|
23
26
|
status: status.artifact,
|
|
27
|
+
sbom: sbom.artifact,
|
|
28
|
+
cyclonedx: cyclonedx.artifact,
|
|
24
29
|
dashboard: dashboard.dashboard
|
|
25
30
|
},
|
|
26
31
|
changes: scanned.changes,
|
package/src/contract.js
CHANGED
|
@@ -31,6 +31,16 @@ 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"]
|
|
39
|
+
},
|
|
40
|
+
cyclonedxLite: {
|
|
41
|
+
file: ".aienvmp/sbom.cdx.json",
|
|
42
|
+
command: "aienvmp sbom --format cyclonedx-lite --json",
|
|
43
|
+
rootFields: ["bomFormat", "specVersion", "metadata", "components", "vulnerabilities", "properties"]
|
|
34
44
|
}
|
|
35
45
|
},
|
|
36
46
|
compatibility: {
|
package/src/paths.js
CHANGED
|
@@ -16,6 +16,14 @@ 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
|
+
|
|
23
|
+
export function cyclonedxSbomPath(dir) {
|
|
24
|
+
return path.join(stateDir(dir), "sbom.cdx.json");
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
export function previousManifestPath(dir) {
|
|
20
28
|
return path.join(stateDir(dir), "manifest.previous.json");
|
|
21
29
|
}
|
package/src/preflight.js
CHANGED
|
@@ -353,6 +353,8 @@ export function preflightArtifacts() {
|
|
|
353
353
|
status: ".aienvmp/status.json",
|
|
354
354
|
envMap: "AIENV.md",
|
|
355
355
|
manifest: ".aienvmp/manifest.json",
|
|
356
|
+
sbom: ".aienvmp/sbom.json",
|
|
357
|
+
cyclonedx: ".aienvmp/sbom.cdx.json",
|
|
356
358
|
dashboard: ".aienvmp/dashboard.html",
|
|
357
359
|
planJson: ".aienvmp/plan.json",
|
|
358
360
|
planMarkdown: ".aienvmp/plan.md",
|
package/src/render.js
CHANGED
|
@@ -442,6 +442,7 @@ const actions=manifest.recommendedActions||[];
|
|
|
442
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>';
|
|
443
443
|
const plan=manifest.planArtifacts||{};
|
|
444
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>CDX Lite</th><td><a href="sbom.cdx.json">sbom.cdx.json</a></td></tr><tr><th>Command</th><td><code>aienvmp sbom --write</code></td></tr></table>';
|
|
445
446
|
const remediation=manifest.planRemediation||[];
|
|
446
447
|
const remediationFix=r=>r.fixVersions?.length?\`fix \${r.fixVersions.join(', ')}\`:(r.fixAvailable?'fix available':'review required');
|
|
447
448
|
const remediationRefs=r=>r.advisories?.length?\` - \${r.advisories.join(', ')}\`:'';
|
|
@@ -530,6 +531,8 @@ document.getElementById('app').innerHTML=\`
|
|
|
530
531
|
<div style="height:14px"></div>
|
|
531
532
|
\${card('AI Plan Artifacts',plan.markdown||plan.json?'<span class="pill">written</span>':'<span class="pill off">not written</span>',planHtml)}
|
|
532
533
|
<div style="height:14px"></div>
|
|
534
|
+
\${card('Light SBOM Artifact','<span class="pill">json</span>',sbomArtifactHtml)}
|
|
535
|
+
<div style="height:14px"></div>
|
|
533
536
|
\${card('Remediation Steps',remediation.length?'<span class="pill warn">'+remediation.length+' items</span>':'<span class="pill off">none</span>',remediationHtml)}
|
|
534
537
|
<div style="height:14px"></div>
|
|
535
538
|
\${card('Environment Steps',envSteps.length?'<span class="pill warn">'+envSteps.length+' items</span>':'<span class="pill off">none</span>',envStepsHtml)}
|