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", quorum?, approvers_so_far? }) grows additively. Quorum-aware (ADR-0041): for a request carrying quorum >= 2 a promoted approval.granted@1 or approval.denied@1 is one approver's vote and does not resolve it — a host promotes an authentic vote before it knows the vote is eligible, and only an eligible deny vetoes, so treating a deny as terminal would let a pinned-but-ineligible approver hide a live hold from the queue. The item stays queued until a host-authored verdict: approval.quorum_reached@1 (the authorization), approval.expired@1 (which expires a partially satisfied hold too), or the action.rejected the host writes when a veto lands. approvers_so_far is advisory progress, never authorization: the projection holds no pinned keys and no approver authority records, so it cannot tell a genuine vote from one the host will refuse nor whether an approver clears required_authority, and can only ever overstate. 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.
ApprovalQuorumReachedPayloadSchema, ApprovalQuorumReachedPayload, QuorumApprovalRefSchema, QuorumApprovalRef, APPROVAL_QUORUM_REACHED_EVENT_TYPE, APPROVAL_QUORUM_REACHED_SCHEMA_VERSION @qmilab/lodestar-core The approval.quorum_reached@1 wire format (ADR-0041) — M distinct verified approvals satisfy the threshold, and here they are. { request_id, action_id, quorum, approvals: QuorumApprovalRef[], at }; a QuorumApprovalRef is { approver_id, at, payload_hash, granted_event_id }. Versioned from birth ("1"). The load-bearing separation is contractual: approval.granted@1 is one approver's vote (unchanged shape, unchanged meaning) and this event is the authorization — a request with no quorum emits none, so the single-approver path is byte-identical. Two invariants are enforced structurally, not left to the emitter, so a bogus quorum is not representable on the wire: approvals.length >= quorum and every approver_id distinct. The record names its evidence rather than asserting a count — payload_hash pins the canonical resolution that approver signed and granted_event_id locates the promoted grant — so a reader re-verifies against their own pinned keys instead of trusting the emitter. Host-authored only: emitting it requires the operator's pinned keys. Additive growth only.
evaluateQuorum, QuorumVote, QuorumEvaluation, EvaluateQuorumOptions, QuorumVeto, RejectedQuorumVote, QuorumRejectionCode, ApproverAuthority @qmilab/lodestar-policy-kernel evaluateQuorum(request: ApprovalRequest, resolutions: readonly QuorumVote[], options: EvaluateQuorumOptions): QuorumEvaluation — the M-of-N adjudicator (ADR-0041). Pure: no I/O, no clock, no key access of its own, which is what keeps -policy-kernel importable without -guard so a read-side consumer can re-adjudicate a logged quorum against its own roster. Two orthogonal checks with two inputs is contractual: authorizedKeys proves authenticity (via verifyApprovalSignature) and approverAuthority (actor_id → ApproverAuthority, a structural narrowing of Actor — a full Actor is assignable) proves eligibility against request.required_authority. Keys alone cannot answer the second, because the signed resolution deliberately carries no authority. Also contractual: no unsigned path (there is no allowUnsigned); fail closed on a missing or short authority record; dedup by actor_id; a single valid deny vetoes regardless of grants collected; the action's proposedBy is excluded from grants at quorum >= 2 only. Votes are walked chronologically by their signed at. QuorumEvaluation is { satisfied, required, approvals: QuorumApprovalRef[], vetoed?, rejected[] } and grows additively; only satisfied is authorization.
quorumRecords, QuorumRecord @qmilab/lodestar-trace quorumRecords(events: EventEnvelope[]): QuorumRecord[]. Pure, no I/O — the same family as projectChain / pendingApprovals. Projects every approval.quorum_reached@1 verbatim, oldest-first, schema-parsed on the way out (a malformed record is skipped, never surfaced as a partial quorum). QuorumRecord is { project_id, session_id, request_id, action_id, quorum, approvals, at, event_id } and grows additively. Verbatim rather than recomputed by design: the projection has no access to the operator's pinned approver keys or authority roster — the same boundary that stops pendingApprovals re-verifying signatures — so it cannot itself decide whether a set of grants meets a threshold. The emitting host could, and each constituent names its evidence, so a consumer holding its own pinned keys re-verifies rather than trusts. Read-only by construction.
QuorumRoster, ApproverAuthoritySchema, ApproverAuthorityConfig, ApproverRosterEntry, approverRosterFrom, policyDeclaresQuorum, needsQuorum, assertQuorumRoster, resolutionIsAuthentic, promotedVotePayload, promotedVoteEventType, voteFromResolution, quorumOptions, quorumGrantedOutcome, quorumDeniedOutcome, quorumReachedPayload, quorumShortfallReason, voteIsBoundTo, maxDeclaredQuorum, quorumCapacity, quorumRosterShortfall, QUORUM_REACHED_EVENT @qmilab/lodestar-guard The M-of-N quorum host seam (ADR-0041) — everything a governance host that is not one of the three bundled ones (guard.wrap(), the MCP proxy, the runtime gate) needs to adjudicate a quorum correctly, and the reason those three cannot drift from each other. Adjudication itself is never here: it is the pure evaluateQuorum above, and this seam is the vocabulary around it. The host flow is contractual — promote, then adjudicate: obtain a signed resolution; check it with resolutionIsAuthentic and promote it to its own approval.granted@1 / approval.denied@1 (promotedVoteEventType + promotedVotePayload), which is what gives the vote a citable event_id; turn it into a QuorumVote (voteFromResolution); run evaluateQuorum over everything accumulated; and only on satisfied emit approval.quorum_reached@1 (quorumReachedPayload + QUORUM_REACHED_EVENT) and resolve with quorumGrantedOutcome. Security semantics are contractual, not just signatures: resolutionIsAuthentic has no unsigned path (deliberately narrower than the single-approver resolutionVerified, which honours a no-keys + explicit allow_unsigned legacy mode) — an unsigned vote is the synthesized artifact ADR-0041 refuses; promotedVotePayload carries the signature, without which the quorum record's granted_event_id pointers become unverifiable; assertQuorumRoster throws rather than falling back to the single-approver path, because a silent downgrade would turn "three approvers" into "the first grant wins"; quorumGrantedOutcome throws on an unsatisfied evaluation rather than minting a grant; approverRosterFrom returns undefined only when no entry declares an authority (a partial roster is valid and fail-closed per approver — a pinned approver with no authority record can still cast a promotable vote, it just does not count); quorumRosterShortfall is the construction-time guard both out-of-process hosts share — it returns the operator-facing reason a roster could never satisfy the policy, covering both provably-unsatisfiable shapes: no authority records at all, and a roster smaller than the largest threshold the policy can open (maxDeclaredQuorum vs quorumCapacity, which counts only approvers holding BOTH a pinned key and an authority record — either alone is useless). Both are deterministic misconfigurations rather than runtime conditions, and unchecked they present as every held action stalling to an approval timeout with nothing explaining why; policyDeclaresQuorum remains the boolean form. ApproverAuthoritySchema is { trust_baseline, sensitivity_clearance, authority_scope? } — exactly the fields the authority predicate reads, with authority_scope defaulting to [] ("holds no named scope", the fail-closed direction); it carries no id because the roster entry already supplies actor_id, so the two cannot disagree. Host config, so it lives in -guard beside ApprovalChannelConfigSchema rather than in -core. voteIsBoundTo is the request/action binding check every host must run before promoting, not only before counting: a signature proves who signed, not what for, so a stale-but-validly-signed resolution for another hold would otherwise be written into this session's log as that hold's resolution. Not contractual: the text quorumShortfallReason returns — its signature is stable, the wording is not, exactly as with renderReport.
QuorumCollector @qmilab/lodestar-guard The in-process collector seam for a quorum >= 2 hold: (request: ApprovalRequest) => Promise<readonly ApprovalResolution[]>, attached as GuardConfig.quorum.collect. It returns the approvers' own signed resolutions, never a synthesized ApprovalOutcome — and that difference from ApprovalResolver is the contract, not an implementation detail. A resolver returns a verdict the host applies; a collector returns votes the host adjudicates. If a collector could hand back one synthesized outcome the log would record a single-approver decision and quorum would degrade into an unverifiable claim by whatever gathered the signatures. Also contractual: it is called once and owns any waiting (there is no deadline in-process — that is the proxy's concern), and votes that do not satisfy the threshold expire the hold as a soft denial rather than un-parking it. Note GuardConfig as a whole is not in this table; the quorum field's shape (QuorumRoster & { collect: QuorumCollector }) is.
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), an ApprovalRequirement is { required_authority?, quorum? }, and a RequiredAuthority is { min_trust_baseline?, sensitivity_clearance?, scope? }. quorum (integer >= 1, ADR-0041) is how many distinct approvers must each satisfy required_authority; absent or 1 is the single-approver path, unchanged. It is deliberately a sibling of required_authority rather than a field inside it: the authority is a predicate on one approver and is max-merged with the action's own sensitivity, so a count there would be meaningless and unmergeable. 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.