Skip to content

Public API stability

Lodestar is pre-1.0 (0.x), where semver alone promises nothing. This page is the explicit contract on top of it: which exported surfaces external integrators can pin against, and which may still move.

The rule. A stable export changes shape only with a minor version bump and a CHANGELOG entry — never in a patch release. An experimental export may change in any release. Everything not listed here is experimental by default. The public-api-surface probe (in packs/lodestar-core/) imports every declared-stable symbol and pins its signature both ways — at compile time (the strict-TS typecheck:packs gate) and at runtime (each schema round-trips a valid payload and rejects an invalid one; each pure function is exercised for its documented behavior) — so a breaking drift fails CI the same way any other spec violation does. Every surface in the table below is pinned.

Stable

Export Package Contract
EventEnvelope, EventEnvelopeSchema @qmilab/lodestar-core The envelope: id, seq, type, schema_version, project_id, session_id, actor_id, timestamp, logical_clock, causal_parent_ids, payload_hash, payload, versions, signature?. Additive growth only; payload shapes are versioned per event type via schema_version.
EventLogReader @qmilab/lodestar-event-log new EventLogReader(rootDir); readAll(projectId): Promise<EventEnvelope[]> (seq order); readSession(projectId, sessionId): Promise<EventEnvelope[]> (logical-clock order). Log layout <root>/<project_id>/YYYY-MM-DD.ndjson is part of the contract.
canonicalHash @qmilab/lodestar-event-log sha-256 hex over canonical JSON (sorted keys). The tamper-evidence primitive; payload_hash === canonicalHash(payload) for every unredacted envelope.
projectChain @qmilab/lodestar-trace projectChain(events: EventEnvelope[], opts: { session_id, project_id }): ChainProjection. Pure, no I/O. Tolerant projection is contractual: unknown event types never throw — they are retained in raw_events. ChainProjection fields grow additively. Known sharp edge (documented, kept for now): actor_ids is a Set<string>, not JSON-safe — serialize via toWireProjection (@qmilab/lodestar-trace, the row below).
toWireProjection, WireProjection @qmilab/lodestar-trace toWireProjection(projection: ChainProjection): WireProjection. Pure, no I/O — the JSON-safe serialization of a ChainProjection (the companion the projectChain row points at). Converts the one non-JSON-safe field, actor_ids (Set<string> → string[]), and drops the heavy verbatim raw_events (WireProjection = Omit<ChainProjection, "actor_ids" \| "raw_events"> & { actor_ids: string[] }). Grows additively with ChainProjection. Re-exported unchanged from @qmilab/lodestar-viewer for source compatibility.
renderReport @qmilab/lodestar-trace renderReport(projection, opts?: RenderOptions): string. The signature is stable; the markdown text is explicitly not contractual (sections may be added or reworded). Parse the projection, not the report.
pendingApprovals, PendingApproval @qmilab/lodestar-trace pendingApprovals(events: EventEnvelope[]): PendingApproval[]. Pure, no I/O — the same family as projectChain. Derives the open-hold queue: every approval.requested@1 with no matching approval.granted@1 / approval.denied@1 / approval.expired@1, oldest-first. PendingApproval ({ project_id, session_id, request_id, action_id, reason, required_authority, requested_at, deadline?, status: "pending" }) grows additively. Read-only by construction — surfaces what is waiting, never resolves it (resolution is the separate write-side surface). Re-exported unchanged from @qmilab/lodestar-viewer for source compatibility.
harvestCandidates, MemoryCandidate, SupersededLesson @qmilab/lodestar-trace harvestCandidates(events: EventEnvelope[], filter?: { session_id?, project_id? }): MemoryCandidate[]. Pure, no I/O — the same family as projectChain / pendingApprovals. Derives the durable-memory harvest queue (ADR-0031/0033): the supported, clean, retrievable beliefs worth offering a human as keeper lessons, oldest-first by observed_at. A MemoryCandidate is { project_id, session_id, belief, claim?, evidence?, supersedes: SupersededLesson[], status: "candidate" } and SupersededLesson is { belief, claim? }; both grow additively. Contractual: a belief is harvested only when its adoption is firewall-authored — a belief.adopted record confirmed by a host-authored firewall.belief.adopted@1 audit for the same belief_id and matching claim_id (first-wins on the record, so a forged re-emit cannot overwrite content), and the candidate's evidence is the exact set the audit's evidence_id names (not the latest assessment for the claim); the surfaced claim + evidence content is first-wins per id (a later same-id claim.extracted / evidence.assessed cannot overwrite an authenticated belief's provenance); current lifecycle state is reconstructed from those records + firewall-authored firewall.belief.transitioned (canonical type + schema_version === FIREWALL_EVENT_SCHEMA_VERSION + strict payload — an agent ctx.emit is pinned below that version and cannot forge an adoption or a clearing transition; not snapshot-read); the candidacy gate is current truth_status: supported and security_status: clean and retrieval_status ∈ {normal, restricted} — a quarantined / hard-demoted belief never surfaces, not even as supersession history (the no-self-promotion guarantee, extended to durable memory); supersession is surfaced as the successor's newest-first supersedes audit trail, never a separate candidate. Freshness / sensitivity / scope are surfaced, not gated. Authentication is per-session — each (project_id, session_id) is processed independently, so a firewall audit from one session cannot authenticate a record from another (no cross-session laundering over a project-wide event list). Read-only by construction — surfaces what is worth keeping, never keeps it (the keep/discard decision is a separate write-side surface).
signApprovalResolution, verifyApprovalSignature, canonicalApprovalResolutionHash, generateApproverKeyPair, assertValidApproverKeys, ApprovalSignatureError @qmilab/lodestar-policy-kernel Ed25519 over the canonical resolution document { request_id, action_id, kind, approver_id, reason?, at } (reason omitted when unset). Keys: SPKI PEM public / PKCS#8 PEM private. The reject set of verifyApprovalSignature (unsigned, tampered hash, signer mismatch, unpinned signer, non-ed25519, bad bytes) is contractual.
buildTrace, toOtlpTraceJson, traceIdFor, spanIdFor, isoToUnixNano @qmilab/lodestar-otel-exporter The OTLP IR. Deterministic ids (pure function of project/session/local ids — re-export is idempotent) and the redaction marker shape (lodestar.redacted: true + lodestar.payload_hash) are contractual.
ApprovalResolutionSchema + the side-channel layout @qmilab/lodestar-guard (re-exported from -guard-mcp) The resolution wire shape { request_id, action_id, kind, approver_id, reason?, at, signature? } and the file channel layout <log_root>/.approvals/<project_id>/<request_id>.json with atomic temp-file + rename writes. Any external resolver writes exactly this. (Graduated to -guard by ADR-0024/0025 when the runtime gate became a second consumer; -guard-mcp re-exports it unchanged.) Importable writer-free from the @qmilab/lodestar-guard/approval-channel subpath (see the row below).
ApprovalChannel, FileApprovalChannel, HttpApprovalChannel, createApprovalChannel, ApprovalChannelConfigSchema, httpChannelForbidsUnsigned @qmilab/lodestar-guard (re-exported from -guard-mcp) The approval transport seam (ADR-0015): announce?(request) / fetch(ref) / consume?(ref). fetch returns an UNTRUSTED ApprovalResolution the consumer signature-verifies after transport — a hostile channel can only delay an approval, never forge one. FileApprovalChannel (default) wraps the .approvals/ file layout; HttpApprovalChannel reads a remote service whose route shapes (POST {endpoint}/v1/approvals, GET/DELETE {endpoint}/v1/approvals/{project_id}/{request_id}) version with it. An HTTP channel requires a pinned approver key (httpChannelForbidsUnsigned) — an unsigned remote channel is unrepresentable. Import path: these symbols (and the ApprovalResolutionSchema reader in the row above) are available from the writer-free subpath @qmilab/lodestar-guard/approval-channel for a consumer that must NOT link the write-side runtime (an external integrator, a relay or read-side consumer, an integration test exercising the real client) — the subpath's transitive runtime graph is { @qmilab/lodestar-core, zod, node:* }, never wrap / action-kernel / memory-firewall / cognitive-core / harness (ADR-0030). The . barrel still re-exports the same symbols unchanged.
CalibrationComputedPayloadSchema, CALIBRATION_COMPUTED_EVENT_TYPE, CALIBRATION_COMPUTED_SCHEMA_VERSION @qmilab/lodestar-core The calibration.computed@1 event payload — the wire surface, not the harness math. { computation_id, triggered_by, cursor: { from_seq, to_seq }, report, computed_at }, where report is { sample_count, classes[], overall, flagged_classes[], config } and a metrics block is { n, mean_confidence, empirical_accuracy, brier_score, ece, calibration_gap, overconfident }. Versioned from birth via the envelope schema_version ("1"); additive growth only. The Calibrator that computes these stays experimental in -harness — only the recorded event shape is stable.
SentinelAlertPayloadSchema, SentinelSubjectSchema, SentinelSeveritySchema, SENTINEL_ALERTED_EVENT_TYPE, SENTINEL_ALERTED_SCHEMA_VERSION @qmilab/lodestar-core The sentinel.alerted@1 alert wire format — what a sentinel emits, not the sentinels themselves. { alert_id, sentinel_name, rule, severity, subject: { kind, id }, message, observed_event_ids, detail, detected_at, rationale_id? }; severity ∈ {info, warning, critical}, subject.kind ∈ {belief, action, decision, tool_sequence}, and observed_event_ids are also the alert envelope's causal_parent_ids. detail is an open record by design, so a new sentinel ships without a core bump. Versioned from birth ("1"). The Sentinel/SentinelRunner in -harness stay experimental — only the emitted event shape is stable.
FirewallAuditPayloadSchema, FirewallClaimAcceptedPayloadSchema, FirewallBeliefAdoptedPayloadSchema, FirewallBeliefTransitionedPayloadSchema, FirewallLifecycleAxisSchema, firewallEventType, FIREWALL_CLAIM_ACCEPTED_EVENT_TYPE, FIREWALL_BELIEF_ADOPTED_EVENT_TYPE, FIREWALL_BELIEF_TRANSITIONED_EVENT_TYPE, FIREWALL_EVENT_SCHEMA_VERSION @qmilab/lodestar-core The firewall.*@1 audit-event wire format (ADR-0029) — how the Memory Firewall is observed. This is the decided answer to "stabilize the firewall store interface or emit firewall events": the firewall is read through its events, keeping "every read-side surface is a pure projection over EventEnvelope[]" true for it too; the store interfaces stay experimental (read the log, not the store). FirewallAuditPayloadSchema is a kind-discriminated union of three payloads carried by three two-segment envelope types — firewall.claim.accepted { kind, claim_id, at, by_actor_id }, firewall.belief.adopted { kind, belief_id, claim_id, evidence_id, rationale_id, by_authority, at, by_actor_id, causal_parent_ids? }, and firewall.belief.transitioned { kind, belief_id, axis, from_value, to_value, by_authority, rationale_id, at, by_actor_id, causal_parent_ids?, superseded_by? }. axis is the locked four-value enum (FirewallLifecycleAxisSchema: truth_status | retrieval_status | security_status | freshness_status); by_authority/from_value/to_value are opaque strings (open value sets — additive-safe). The payload is a structural supertype of the firewall's richer internal FirewallAuditEvent producer type, so a host (-guard, -guard-mcp, -runtime-core) parses + stamps schema_version "1" at the emit boundary without the contract weakening the firewall's internals. firewallEventType(kind) is the single kind → type mapping every emitter shares. Versioned from birth ("1"); additive growth only. The MemoryFirewall that produces these and its store interfaces stay experimental in -memory-firewall — only the recorded event shape is stable.
PolicySchema, Policy, PolicyRuleSchema, PolicyRule, PolicyMatchSchema, PolicyMatch, PolicyEffectSchema, PolicyEffect, RequiredAuthoritySchema, RequiredAuthority, ApprovalRequirementSchema, ApprovalRequirement @qmilab/lodestar-core The declarative action-policy document — the wire format an external policy author or distributor (a hosted editor, a linter, a registry-published policy bundle) pins against; the Policy Kernel compiles every policy through it (compile()PolicyGate, gate.ts). A Policy is { id, version, rules[], signature?, signed_by? }; each PolicyRule is { match, effect, approval?, reason }, a PolicyMatch is the all-present-fields-AND clause (tool glob, max_blast_radius, reversibility[], scope, data_sensitivity, required_level_lte; an empty match is a wildcard), and a RequiredAuthority is { min_trust_baseline?, sensitivity_clearance?, scope? }. Load-bearing refinements are contractual: effect ∈ {allow, deny, require_approval}; approval is accepted only on a require_approval rule; and signed_by must be present and equal signature.signer_id exactly when signature is present (an unsigned draft carries neither). Composes the trust-ladder level TrustLevelSchema (integer 0–5, the required_level_lte bound) and the signed-policy envelope SignatureSchema ({ signer_id, payload_hash, algorithm: "ed25519", signature, at }, shared with EventEnvelope.signature), both stable as embedded here. Additive growth only. Document shape only — rule evaluation semantics (ordered first-decisive over a structural deny default; the non-overridable L4-hold / L5-deny floor) are the engine's contract in @qmilab/lodestar-policy-kernel, not the document's. Pin the shape, not the verdict.
ProbePackManifestSchema, ProbePackManifest, ProbeEntrySchema, ProbeEntry, SentinelEntrySchema, SentinelEntry, PackContentDigestSchema, PackContentDigest, PackFileDigestSchema, PackFileDigest, ProbePackSourceTypeSchema, ProbePackSourceType, PROBE_PACK_SPEC_VERSION, PROBE_PACK_MANIFEST_FILENAME @qmilab/lodestar-core The lodestar.probe-pack.json pack manifest (ADR-0016/0017) — the on-wire contract every probe pack (first-party + external) is written against; declarative, names probes + files but carries no executable logic. A ProbePackManifest is { name (kebab), version, spec_version: "1", source_type, description?, coverage_areas[], invariants[], probes[], sentinels?, author_id?, content_digest?, signature? }. Each ProbeEntry is { name (kebab), file } where file must be a relative path inside the pack (absolute / drive-letter / UNC rejected); a SentinelEntry references a built-in by { id }. The three signing fields are additive-optional (an unsigned allow_unsigned pack omits all three; a signed pack carries all three together). content_digest (PackContentDigest = { algorithm: "sha256", files: PackFileDigest[] }, a sorted per-file sha-256 list) is inside the canonically-signed document, so the author signature transitively binds the probe bytes, not just their names — a swapped byte under a still-valid signature is caught on load. spec_version is the closed literal "1" (PROBE_PACK_SPEC_VERSION): adding an optional field is free; removing/re-typing one is a spec bump. The verify-on-load crypto lives in @qmilab/lodestar-core's crypto/; this row pins the wire shape.
PackSourceRefSchema, PackSourceRef, LocalPackSourceSchema, NpmPackSourceSchema, GitPackSourceSchema, PackIndexSchema, PackIndex, PackIndexEntrySchema, PackIndexEntry, PackIndexBadgeSummarySchema, PackIndexBadgeSummary, PackIndexPublisherKeySchema, PackIndexPublisherKey, PACK_INDEX_SPEC_VERSION, PACK_INDEX_FILENAME @qmilab/lodestar-core The immutable pack source descriptor + the static signed discovery index (ADR-0018/0021). A PackSourceRef is a type-discriminated union (local | npm | git); the load-bearing property is immutabilitynpm pins an exact version + SRI integrity (a range / dist-tag / latest is rejected), git pins a full 40-hex commit SHA (a branch / tag / short SHA is rejected) — so resolution is reproducible. A PackIndex (lodestar.pack-index.json) is { index_version: "1", description?, packs[], publisher_id?, generated_at?, signature? }; each PackIndexEntry carries the searchable identity + the immutable source (a PackSourceRef, consumed unchanged by pack add) + advisory badges[] (PackIndexBadgeSummary). Signing fields are additive-optional (an unsigned index loads only under an explicit allow_unsigned); verified against the operator-pinned index_publisher_keys (PackIndexPublisherKey) — a third trust root, distinct from author + attester keys. Load-bearing invariant: an index advertises, never authorizes — choosing a discovered pack still routes through resolution + verify-on-load against pinned author keys, so a hostile index can mis-list or omit but never make a forged pack verify.
PackBadgeSchema, PackBadge, ProbeResultsBadgeSchema, ProbeResultsBadge, SecurityScanBadgeSchema, SecurityScanBadge, PackBadgeSubjectSchema, PackBadgeSubject, PackBadgeKindSchema, PackBadgeKind, UnsignedPackBadge, PACK_BADGE_SPEC_VERSION, PACK_BADGES_DIRNAME, PACK_BADGE_FILE_SUFFIX @qmilab/lodestar-core The badges/<name>.badge.json signed verification badge (ADR-0020) — an attestation about a pack (the registry's second trust axis), a union discriminated by kind: probe_results (a harness-run summary { ok, total, passed, failed, harness_version, probes? }) | security_scan (a verdict { status: clean\|findings, findings_count, scanner?, summary? }). Attached, not baked in — a badge lives outside the manifest's content_digest, so badges accrue without re-signing the manifest. Two properties make it trustworthy without trusting any index: subject binding (subject.manifest_hash, the canonical manifest hash recomputed by the verifier) defeats mis-attach; the signature (REQUIRED — a badge is by definition signed, so an unsigned file is malformed, not a loose badge) defeats forgery. Verified against the operator-pinned attester keys (a separate trust root). Badges are advisory, never a runtime gate — the consumer surfaces verified-vs-unverified and never blocks. badge_version is the closed literal "1". UnsignedPackBadge is the pre-signing view (the badge minus its detached signature).

Stable from first release (planned surfaces)

Surface Where Contract
lodestar.session_ship@1 wire format @qmilab/lodestar-ship (ADR-0014) NDJSON: one manifest record, then one wrapper record per event ({ v, redacted, envelope }); redacted records keep the original payload_hash. Receiver dedupe key (project_id, session_id, seq); re-ship is idempotent. Versioned from birth. (ApprovalChannel, ADR-0015, has landed and moved to the Stable table above.)

Experimental

May change in any release; pin at your own risk:

  • loadSessionEvents, findProjectForSession, defaultLogRoot, describeEvent, findEventById (@qmilab/lodestar-trace) — CLI conveniences, deliberately sharper-edged than the projection core.
  • listSessions, readAllEvents, startViewer (@qmilab/lodestar-viewer) — CLI/server conveniences over the log root. (pendingApprovals / PendingApproval and toWireProjection / WireProjection have graduated to @qmilab/lodestar-trace's stable tier; the viewer re-exports them unchanged.)
  • exportSession options (@qmilab/lodestar-otel-exporter) — the CLI-shaped wrapper around the stable IR.
  • Everything in @qmilab/lodestar-harness, the firewall store interfaces, and the cognitive-core extractor/linker seams — evolving with the probe surface. The firewall store interfaces (ClaimStore / BeliefStore / EvidenceStore) stay experimental by design (ADR-0029): they are a mutable read+write API, so an external integrator observes the firewall through the stable firewall.*@1 events above (a pure projection over the log), not by binding to the store.

Notes for integrators

  • The event log is the source of truth; every read-side surface here is a pure projection over EventEnvelope[]. If a projection lacks something, read the envelopes.
  • The report markdown and the viewer SPA are presentation, not API. Build on projectChain / the OTLP IR / the ship wire format instead.
  • Sensitivity gating is load-bearing on every export path: content above the configured ceiling ships as structural metadata plus payload_hash only. Treat a redaction marker as a verifiable commitment, not an error.