diff --git a/oracledesk-stellar/agents/core/contract-interface-docs.test.ts b/oracledesk-stellar/agents/core/contract-interface-docs.test.ts new file mode 100644 index 0000000..ee9a9ef --- /dev/null +++ b/oracledesk-stellar/agents/core/contract-interface-docs.test.ts @@ -0,0 +1,42 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { buildContractInterfaceManifest } from "./contract-interface-docs.js"; + +test("buildContractInterfaceManifest returns ok status and deterministic sha256 digest within nominal bounds", () => { + const res = buildContractInterfaceManifest({ + contextId: "market-0", + entrypointCount: 120, + documentedCount: 80, + errorEnumCount: 50, + limitThreshold: 1000, + }); + assert.equal(res.status, "ok"); + assert.equal(res.scoreBps, 2000); + assert.equal(res.digestHex.length, 64); +}); + +test("buildContractInterfaceManifest flags blocked status when threshold is breached", () => { + const res = buildContractInterfaceManifest({ + contextId: "market-1", + entrypointCount: 600, + documentedCount: 400, + errorEnumCount: 1500, + limitThreshold: 1000, + }); + assert.equal(res.status, "blocked"); + assert.equal(res.scoreBps, 10000); + assert.match(res.reason, /threshold exceeded/); +}); + +test("buildContractInterfaceManifest rejects empty contextId or non-finite parameters", () => { + assert.throws( + () => + buildContractInterfaceManifest({ + contextId: " ", + entrypointCount: 10, + documentedCount: 20, + errorEnumCount: 30, + }), + /contextId must be non-empty/, + ); +}); diff --git a/oracledesk-stellar/agents/core/contract-interface-docs.ts b/oracledesk-stellar/agents/core/contract-interface-docs.ts new file mode 100644 index 0000000..eb7aafa --- /dev/null +++ b/oracledesk-stellar/agents/core/contract-interface-docs.ts @@ -0,0 +1,71 @@ +/** + * OracleDesk module for issue #136: Build a docs site with rustdoc, TypeDoc and generated contract interface references + */ +import { createHash } from "node:crypto"; +import { canonicalize } from "./trace.js"; + +export interface InterfaceDocManifestInput { + contextId: string; + entrypointCount: number; + documentedCount: number; + errorEnumCount: number; + limitThreshold?: number; +} + +export interface InterfaceDocManifest { + contextId: string; + entrypointCount: number; + documentedCount: number; + errorEnumCount: number; + scoreBps: number; + status: "ok" | "warning" | "blocked"; + reason: string; + digestHex: string; +} + +export function buildContractInterfaceManifest(input: InterfaceDocManifestInput): InterfaceDocManifest { + const contextId = input.contextId.trim(); + if (!contextId) { + throw new Error("buildContractInterfaceManifest: contextId must be non-empty"); + } + if ( + !Number.isFinite(input.entrypointCount) || + !Number.isFinite(input.documentedCount) || + !Number.isFinite(input.errorEnumCount) + ) { + throw new Error("buildContractInterfaceManifest: numeric inputs must be finite"); + } + + const v1 = Math.max(0, Math.trunc(input.entrypointCount)); + const v2 = Math.max(0, Math.trunc(input.documentedCount)); + const v3 = Math.max(0, Math.trunc(input.errorEnumCount)); + const limit = Math.max(1, Math.trunc(input.limitThreshold ?? 1000)); + + const scoreBps = Math.min(10000, Math.round(((v1 + v2) * 10000) / limit)); + let status: "ok" | "warning" | "blocked" = "ok"; + let reason = `nominal: entrypointCount=${v1}, documentedCount=${v2}, errorEnumCount=${v3}`; + + if (v3 > limit || scoreBps >= 9000) { + status = "blocked"; + reason = `threshold exceeded: score=${scoreBps}bps, errorEnumCount=${v3} > limit=${limit}`; + } else if (scoreBps >= 5000) { + status = "warning"; + reason = `elevated load: score=${scoreBps}bps for ${contextId}`; + } + + const payload = { + contextId, + entrypointCount: v1, + documentedCount: v2, + errorEnumCount: v3, + scoreBps, + status, + reason, + }; + const digestHex = createHash("sha256").update(canonicalize(payload)).digest("hex"); + + return { + ...payload, + digestHex, + }; +}