aienvmp 0.1.47 → 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 CHANGED
@@ -70,6 +70,12 @@ Short record of bugs, fixes, and follow-up checks.
70
70
  - Fix: `aienvmp sbom --json` and `.aienvmp/sbom.json` now expose a standalone light SBOM artifact.
71
71
  - Verification: tests cover standalone SBOM construction, writing, sync output, schema metadata, and dashboard linking.
72
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
+
73
79
  ## Template
74
80
 
75
81
  ### Title
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
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
+
3
13
  ## 0.1.47
4
14
 
5
15
  - Added `aienvmp sbom` for standalone AI-readable light SBOM output without deep manifest parsing.
package/README.md CHANGED
@@ -36,6 +36,7 @@ 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
38
  .aienvmp/sbom.json # standalone AI-readable light SBOM
39
+ .aienvmp/sbom.cdx.json # CycloneDX-lite export from project manifests
39
40
  .aienvmp/intents.jsonl # planned env changes
40
41
  .aienvmp/timeline.jsonl # append-only change ledger
41
42
  .aienvmp/plan.md # read-only action plan
@@ -74,6 +75,7 @@ aienvmp sync # update env map, status, ledger, dashboard
74
75
  aienvmp status --write # refresh compact AI status
75
76
  aienvmp context --json # AI decision contract
76
77
  aienvmp sbom --json # standalone light SBOM
78
+ aienvmp sbom --format cyclonedx-lite --json
77
79
  aienvmp schema --json # stable output contract for AI/CI consumers
78
80
  aienvmp plan --write # read-only action plan
79
81
  aienvmp handoff --record # next-agent summary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.47",
3
+ "version": "0.1.48",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,12 +1,13 @@
1
1
  import { readJson, writeJson } from "../fsutil.js";
2
- import { manifestPath, sbomJsonPath, workspaceDir } from "../paths.js";
2
+ import { cyclonedxSbomPath, manifestPath, sbomJsonPath, workspaceDir } from "../paths.js";
3
3
 
4
4
  export async function sbomWorkspace(args = {}) {
5
5
  const dir = workspaceDir(args);
6
6
  const manifest = await readJson(manifestPath(dir));
7
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) : "";
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) : "";
10
11
  const output = artifact ? { ...sbom, artifact } : sbom;
11
12
  if (args.json || args.write || args.quiet) {
12
13
  if (args.json) console.log(JSON.stringify(output, null, 2));
@@ -45,7 +46,98 @@ export function buildSbomArtifact(manifest = {}) {
45
46
  }
46
47
 
47
48
  export async function writeSbomArtifact(dir, sbom) {
48
- const out = sbomJsonPath(dir);
49
+ const out = sbom.bomFormat === "CycloneDX" ? cyclonedxSbomPath(dir) : sbomJsonPath(dir);
49
50
  await writeJson(out, sbom);
50
51
  return out;
51
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
+ }
@@ -15,6 +15,7 @@ export async function syncWorkspace(args) {
15
15
  const dashboard = await dashWorkspace(next);
16
16
  const status = await statusWorkspace({ ...next, json: false, write: true, quiet: true });
17
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" });
18
19
 
19
20
  const result = {
20
21
  status: "ok",
@@ -24,6 +25,7 @@ export async function syncWorkspace(args) {
24
25
  timeline: scanned.timeline,
25
26
  status: status.artifact,
26
27
  sbom: sbom.artifact,
28
+ cyclonedx: cyclonedx.artifact,
27
29
  dashboard: dashboard.dashboard
28
30
  },
29
31
  changes: scanned.changes,
package/src/contract.js CHANGED
@@ -36,6 +36,11 @@ export function schemaContract() {
36
36
  file: ".aienvmp/sbom.json",
37
37
  command: "aienvmp sbom --json",
38
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"]
39
44
  }
40
45
  },
41
46
  compatibility: {
package/src/paths.js CHANGED
@@ -20,6 +20,10 @@ export function sbomJsonPath(dir) {
20
20
  return path.join(stateDir(dir), "sbom.json");
21
21
  }
22
22
 
23
+ export function cyclonedxSbomPath(dir) {
24
+ return path.join(stateDir(dir), "sbom.cdx.json");
25
+ }
26
+
23
27
  export function previousManifestPath(dir) {
24
28
  return path.join(stateDir(dir), "manifest.previous.json");
25
29
  }
package/src/preflight.js CHANGED
@@ -354,6 +354,7 @@ export function preflightArtifacts() {
354
354
  envMap: "AIENV.md",
355
355
  manifest: ".aienvmp/manifest.json",
356
356
  sbom: ".aienvmp/sbom.json",
357
+ cyclonedx: ".aienvmp/sbom.cdx.json",
357
358
  dashboard: ".aienvmp/dashboard.html",
358
359
  planJson: ".aienvmp/plan.json",
359
360
  planMarkdown: ".aienvmp/plan.md",
package/src/render.js CHANGED
@@ -442,7 +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>Command</th><td><code>aienvmp sbom --write</code></td></tr></table>';
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>';
446
446
  const remediation=manifest.planRemediation||[];
447
447
  const remediationFix=r=>r.fixVersions?.length?\`fix \${r.fixVersions.join(', ')}\`:(r.fixAvailable?'fix available':'review required');
448
448
  const remediationRefs=r=>r.advisories?.length?\` - \${r.advisories.join(', ')}\`:'';