deepbom 2.0.0 → 2.1.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.
package/package.json CHANGED
@@ -1,15 +1,25 @@
1
1
  {
2
2
  "name": "deepbom",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "mcpName": "io.github.JunHwan-Kwon/deepbom",
5
5
  "description": "Local multi-format deployment-artifact analysis for on-device AI models",
6
6
  "type": "module",
7
+ "main": "./sdk/index.mjs",
8
+ "types": "./sdk/index.d.mts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./sdk/index.d.mts",
12
+ "import": "./sdk/index.mjs"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
7
16
  "bin": {
8
17
  "deepbom": "bin/deepbom.mjs"
9
18
  },
10
19
  "files": [
11
20
  "bin/",
12
21
  "pkg/",
22
+ "sdk/",
13
23
  "skills/deepbom/",
14
24
  "README.md",
15
25
  "LICENSE"
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "schema": "deepbom.npm_release.v1",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "source": {
5
- "git_commit": "520695232ab30a9eb3712388212542ea78d185e6",
5
+ "git_commit": "b161cd4c383e1133e97200a04c8459935f4c7d97",
6
6
  "git_state": "clean",
7
7
  "distribution": "public_channel"
8
8
  },
9
9
  "runtime": {
10
10
  "node": ">=20",
11
- "tflite_wasm_sha256": "352c71ffcd1ef4bd3918f0dbbff967cb1ec2cbc0293f8148d2804be56ce30cc8",
11
+ "tflite_wasm_sha256": "7a562be6d5daa3df1b8f5dd4d15fd2c05eae715eb805f9c99b2287294fc352ee",
12
12
  "self_test": {
13
13
  "file": "bin/deepbom-self-test.onnx",
14
14
  "sha256": "82a2feef00eb6ab03d82f2b30cd17f4d826e2d8307cb059eccd6a0f3120059b2"
Binary file
@@ -0,0 +1,62 @@
1
+ /** Stable SDK entry point; native documents retain their own schema versions. */
2
+ export type JsonDocument = Record<string, unknown>;
3
+ export interface Limits { timeoutMs?: number; maxOutputBytes?: number; }
4
+ export interface InspectOptions extends Limits {
5
+ scan?: "auto" | "structure" | "integrity" | "full";
6
+ expectedSha256?: string;
7
+ gate?: "defects";
8
+ policy?: "engineering" | "regulatory";
9
+ }
10
+ export interface AuditOptions extends InspectOptions {
11
+ output?: "analysis" | "envelope" | "cyclonedx" | "sarif";
12
+ sections?: string[];
13
+ }
14
+ export interface Finding extends JsonDocument {
15
+ id: string; title: string; severity: string; evidence_class: string;
16
+ source_pointers: string[]; recommendation: string | null;
17
+ }
18
+ export interface ReviewSummary extends JsonDocument {
19
+ schema: "deepbom.review_summary.v1";
20
+ artifact: { filename: string | null; format: string | null; sha256: string; byte_length: number | null; artifact_ir_sha256: string };
21
+ verdict: { status: "artifact_defects_observed" | "no_artifact_defect_observed"; artifact_defect_count: number; caution_count: number; evidence_needed_count: number; scope: string };
22
+ findings: { artifact_defects: Finding[]; cautions: Finding[]; evidence_needed: Finding[] };
23
+ coverage: { assessed: number; partial: number; unavailable: number; /** @deprecated Alias for unavailable; does not mean all missing capabilities require external evidence. */ needs_external_evidence: number; declared: number; applicability: Record<string, number> };
24
+ graph: { operator_count: number | null; tensor_count: number | null; total_macs: number | null; mac_confidence: "exact" | "partial" | "symbolic" | "not_applicable"; mac_assessment_status: string | null };
25
+ storage: JsonDocument | null;
26
+ quantization: { classification: string | null; max_risk: string; max_risk_op_index: number | null; max_risk_op_name: string | null; max_risk_detail: string | null };
27
+ target: { id: string | null; label: string | null; binding_source: string | null };
28
+ rulepack: { version: string | null; sha256: string | null };
29
+ reproduction: { schema: "deepbom.reproduction_command.v1"; executable: string; argv: string[]; package_version: string; artifact_filename: string; expected_sha256: string; shell_command: string; boundary: string };
30
+ applicability: Record<string, JsonDocument>;
31
+ next_actions: { action: string; command_hint: string }[];
32
+ evidence_envelope_sha256: string;
33
+ }
34
+ export interface EvidenceEnvelope extends JsonDocument {
35
+ schema: "deepbom.artifact_evidence_envelope.v1";
36
+ identity: { filename: string; format: string; sha256: string; hash_basis: string; byte_length: number | null; schema_or_opset: string | null };
37
+ capabilities: { schema: string; format: string; declared_capabilities: string[]; assessed: string[]; partial: string[]; unavailable: string[]; conservation: { declared: number; classified: number; valid: boolean } };
38
+ findings: (JsonDocument & { id: string; finding_kind: "artifact_defect" | "caution" | "evidence_gap"; severity: string })[];
39
+ provenance: JsonDocument & { analyzer: string; version: string; command: string };
40
+ evidence_boundary: string;
41
+ envelope_sha256: string;
42
+ }
43
+ export class DeepBomError extends Error {
44
+ constructor(message: string, options?: { exitCode?: number | null; document?: JsonDocument | null; cause?: unknown });
45
+ readonly code: string;
46
+ readonly exitCode: number | null;
47
+ document: JsonDocument | null;
48
+ }
49
+ export class DeepBomInvocationError extends DeepBomError {}
50
+ export class DeepBomPolicyBlocked extends DeepBomError {}
51
+ export class DeepBomIncompleteBinding extends DeepBomError {}
52
+ export class DeepBomIdentityMismatch extends DeepBomError {}
53
+ export class DeepBomTimeout extends DeepBomError {}
54
+ export class DeepBomOutputTooLarge extends DeepBomError {}
55
+ export function capabilities(options?: Limits): Promise<JsonDocument>;
56
+ export function inspect(artifact: string, options?: InspectOptions): Promise<ReviewSummary>;
57
+ export function audit(artifact: string, options?: InspectOptions & { output?: "envelope"; sections?: never }): Promise<EvidenceEnvelope>;
58
+ export function audit(artifact: string, options: AuditOptions): Promise<JsonDocument>;
59
+ export function diff(baseline: string, candidate: string, options?: Limits & { tensorsOnly?: boolean }): Promise<JsonDocument>;
60
+ export function captureContract(artifact: string, options?: Limits): Promise<JsonDocument>;
61
+ export function verifyContract(artifact: string, contract: string, options?: Limits): Promise<JsonDocument>;
62
+ export function verifyBom(artifact: string, bom: string, options?: Limits & { componentRef?: string }): Promise<JsonDocument>;
package/sdk/index.mjs ADDED
@@ -0,0 +1,172 @@
1
+ /** Public Node SDK. All analysis and policy evaluation run in the packaged CLI. */
2
+ import { spawn } from "node:child_process";
3
+ import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const cli = fileURLToPath(new URL("../bin/deepbom.mjs", import.meta.url));
9
+ const defaults = { timeoutMs: 300_000, maxOutputBytes: 64 * 1024 * 1024 };
10
+ const limitKeys = Object.keys(defaults);
11
+ const auditKeys = [...limitKeys, "scan", "output", "sections", "gate", "policy", "expectedSha256"];
12
+
13
+ export class DeepBomError extends Error {
14
+ constructor(message, { exitCode = null, document = null, cause } = {}) {
15
+ super(message, { cause });
16
+ this.name = this.constructor.name;
17
+ this.code = this.constructor.code;
18
+ this.exitCode = exitCode;
19
+ this.document = document;
20
+ }
21
+ static code = "DEEPBOM_ERROR";
22
+ }
23
+ export class DeepBomInvocationError extends DeepBomError { static code = "INVOCATION_FAILED"; }
24
+ export class DeepBomPolicyBlocked extends DeepBomError { static code = "POLICY_BLOCKED"; }
25
+ export class DeepBomIncompleteBinding extends DeepBomError { static code = "INCOMPLETE_BINDING"; }
26
+ export class DeepBomIdentityMismatch extends DeepBomError { static code = "IDENTITY_MISMATCH"; }
27
+ export class DeepBomTimeout extends DeepBomError { static code = "TIMEOUT"; }
28
+ export class DeepBomOutputTooLarge extends DeepBomError { static code = "OUTPUT_TOO_LARGE"; }
29
+
30
+ export async function capabilities(options = {}) {
31
+ checkOptions(options, limitKeys);
32
+ return invoke(["capabilities", "--compact"], options);
33
+ }
34
+
35
+ export async function audit(artifact, options = {}) {
36
+ checkOptions(options, auditKeys);
37
+ const sections = names(options.sections);
38
+ const output = choice(options.output ?? (sections.length ? "analysis" : "envelope"), "output", ["analysis", "envelope", "cyclonedx", "sarif"]);
39
+ if (sections.length && output !== "analysis") throw new TypeError("sections require output='analysis'");
40
+ if (options.gate != null && options.policy != null) throw new TypeError("gate and policy are mutually exclusive");
41
+ const argv = ["audit", localPath(artifact), "--scan", choice(options.scan ?? "auto", "scan", ["auto", "structure", "integrity", "full"])];
42
+ if (sections.length) argv.push("--section", sections.join(","));
43
+ if (output !== "analysis") argv.push("--output-format", output);
44
+ argv.push("--compact");
45
+ if (options.gate != null) argv.push("--gate", choice(options.gate, "gate", ["defects"]));
46
+ if (options.policy != null) argv.push("--policy", choice(options.policy, "policy", ["engineering", "regulatory"]));
47
+ if (options.expectedSha256 != null) {
48
+ if (typeof options.expectedSha256 !== "string" || !/^[0-9a-f]{64}$/i.test(options.expectedSha256)) throw new TypeError("expectedSha256 must contain 64 hexadecimal characters");
49
+ argv.push("--expected-sha256", options.expectedSha256.toLowerCase());
50
+ }
51
+ return invoke(argv, options);
52
+ }
53
+
54
+ export async function inspect(artifact, options = {}) {
55
+ checkOptions(options, auditKeys.filter(key => !["output", "sections"].includes(key)));
56
+ try {
57
+ return summaryFrom(await audit(artifact, { ...options, sections: ["summary"] }));
58
+ } catch (error) {
59
+ // A policy rejection still contains the same summary shape as a success.
60
+ if (error instanceof DeepBomPolicyBlocked && error.document) error.document = summaryFrom(error.document);
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ export async function diff(baseline, candidate, options = {}) {
66
+ checkOptions(options, [...limitKeys, "tensorsOnly"]);
67
+ if (options.tensorsOnly != null && typeof options.tensorsOnly !== "boolean") throw new TypeError("tensorsOnly must be boolean");
68
+ return invoke(["diff", localPath(baseline), localPath(candidate), "--compact", ...(options.tensorsOnly ? ["--tensors"] : [])], options);
69
+ }
70
+
71
+ export async function captureContract(artifact, options = {}) {
72
+ checkOptions(options, limitKeys);
73
+ return invoke(["contract", "capture", localPath(artifact), "--compact"], options);
74
+ }
75
+
76
+ export async function verifyContract(artifact, contract, options = {}) {
77
+ checkOptions(options, limitKeys);
78
+ return invoke(["verify", localPath(artifact), "--contract", localPath(contract), "--compact"], options);
79
+ }
80
+
81
+ export async function verifyBom(artifact, bom, options = {}) {
82
+ checkOptions(options, [...limitKeys, "componentRef"]);
83
+ const argv = ["verify", localPath(artifact), "--bom", localPath(bom), "--compact"];
84
+ if (options.componentRef != null) {
85
+ if (typeof options.componentRef !== "string" || !options.componentRef.trim() || options.componentRef.includes("\0")) throw new TypeError("componentRef must be a non-empty bom-ref");
86
+ argv.push("--component-ref", options.componentRef.trim());
87
+ }
88
+ return invoke(argv, options);
89
+ }
90
+
91
+ function summaryFrom(selection) {
92
+ const summary = selection.sections?.summary;
93
+ if (selection.schema !== "deepbom.analysis_selection.v1" || summary?.schema !== "deepbom.review_summary.v1") {
94
+ throw new DeepBomInvocationError("The engine returned an incompatible review summary.");
95
+ }
96
+ return summary;
97
+ }
98
+
99
+ function localPath(value) {
100
+ if (typeof value !== "string" || !value || value.includes("\0") || /^[a-z][a-z0-9+.-]*:\/\//i.test(value)) throw new TypeError("SDK paths must be non-empty local filesystem paths");
101
+ return path.resolve(value);
102
+ }
103
+ function checkOptions(options, allowed) {
104
+ if (!options || typeof options !== "object" || ![Object.prototype, null].includes(Object.getPrototypeOf(options))) throw new TypeError("options must be a plain object with own properties");
105
+ for (const key of Reflect.ownKeys(options)) {
106
+ if (typeof key !== "string" || !allowed.includes(key)) throw new TypeError(`Unsupported SDK option: ${String(key)}`);
107
+ if (!Object.getOwnPropertyDescriptor(options, key).enumerable) throw new TypeError(`SDK option must be enumerable: ${key}`);
108
+ }
109
+ }
110
+ function choice(value, field, allowed) {
111
+ if (!allowed.includes(value)) throw new TypeError(`${field} must be one of: ${allowed.join(", ")}`);
112
+ return value;
113
+ }
114
+ function names(values) {
115
+ if (values == null) return [];
116
+ if (!Array.isArray(values) || values.some(value => typeof value !== "string" || !/^[a-z0-9_.-]+$/i.test(value))) throw new TypeError("sections must be an array of section names");
117
+ return [...new Set(values)];
118
+ }
119
+
120
+ async function invoke(argv, options) {
121
+ const timeoutMs = options.timeoutMs ?? defaults.timeoutMs;
122
+ const maxOutputBytes = options.maxOutputBytes ?? defaults.maxOutputBytes;
123
+ for (const [key, value] of Object.entries({ timeoutMs, maxOutputBytes })) {
124
+ if (!Number.isSafeInteger(value) || value <= 0 || (key === "timeoutMs" && value > 2_147_483_647)) throw new TypeError(`${key} must be a positive bounded integer`);
125
+ }
126
+ let directory;
127
+ let failure;
128
+ try {
129
+ directory = await mkdtemp(path.join(tmpdir(), "deepbom-sdk-"));
130
+ const output = path.join(directory, "result.json");
131
+ const { exitCode, diagnostic } = await new Promise((resolve, reject) => {
132
+ const child = spawn(process.execPath, [cli, ...argv, "--output", output], { shell: false, windowsHide: true, stdio: ["ignore", "ignore", "pipe"] });
133
+ let diagnostic = Buffer.alloc(0);
134
+ let timedOut = false;
135
+ let spawnError;
136
+ const timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, timeoutMs);
137
+ child.stderr.on("data", chunk => { diagnostic = Buffer.concat([diagnostic, chunk]).subarray(-16_384); });
138
+ child.on("error", error => { spawnError = error; });
139
+ child.on("close", (exitCode, signal) => {
140
+ clearTimeout(timer);
141
+ if (timedOut) reject(new DeepBomTimeout(`DEEPBOM exceeded timeoutMs=${timeoutMs}.`));
142
+ else if (spawnError) reject(new DeepBomInvocationError("Could not start the packaged DEEPBOM engine.", { cause: spawnError }));
143
+ else if (signal) reject(new DeepBomInvocationError(`DEEPBOM was terminated by ${signal}.`));
144
+ else resolve({ exitCode, diagnostic: diagnostic.toString("utf8").trim() });
145
+ });
146
+ });
147
+ let document = null;
148
+ const info = await stat(output).catch(error => { if (error.code === "ENOENT") return null; throw error; });
149
+ if (info) {
150
+ if (info.size > maxOutputBytes) throw new DeepBomOutputTooLarge(`DEEPBOM result is ${info.size} bytes; maxOutputBytes is ${maxOutputBytes}.`, { exitCode });
151
+ try {
152
+ document = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(await readFile(output)), (_key, value) => {
153
+ if (typeof value === "number" && !Number.isFinite(value)) throw new Error("JSON number exceeds the finite floating-point range");
154
+ return value;
155
+ });
156
+ }
157
+ catch (cause) { throw new DeepBomInvocationError("DEEPBOM returned invalid JSON.", { exitCode, cause }); }
158
+ if (!document || typeof document !== "object" || Array.isArray(document)) throw new DeepBomInvocationError("DEEPBOM JSON result must be an object.", { exitCode });
159
+ }
160
+ if (exitCode === 0 && document) return document;
161
+ const Failure = { 2: DeepBomPolicyBlocked, 3: DeepBomIncompleteBinding, 4: DeepBomIdentityMismatch }[exitCode] || DeepBomInvocationError;
162
+ throw new Failure(diagnostic || `DEEPBOM exited ${exitCode} without a usable result.`, { exitCode, document });
163
+ } catch (error) {
164
+ failure = error instanceof DeepBomError ? error : new DeepBomInvocationError("Could not invoke or read the packaged DEEPBOM engine.", { cause: error });
165
+ throw failure;
166
+ } finally {
167
+ if (directory) {
168
+ try { await rm(directory, { recursive: true, force: true }); }
169
+ catch (cause) { if (!failure) throw new DeepBomInvocationError("Could not clean up DEEPBOM temporary output.", { cause }); }
170
+ }
171
+ }
172
+ }