What changed in each release, and what it breaks. Generated from CHANGELOG.md, which is also the source for the GitHub release notes.
Notable changes to Capstead. Versions follow 0.MINOR.PATCH while the library is pre-1.0: a minor bump
may change behaviour or a public shape, a patch never does.
This file is the source for GitHub release notes.
scripts/changelog-section.sh <version>extracts a
section verbatim andgh release create --notes-filepublishes it, so an entry has to read as notes: open
with one line saying what the release gives you, then the categorised detail. Anything written only on the
release page will be lost the next time the notes are regenerated.
Entries before 0.8.0 came the other way. This file did not exist until 0.8.0, so 0.3.1 through
0.7.0 were written on the GitHub release pages first and are transcribed here — condensed to the
categories used above, with the code samples left on the releases. Where the two differ, the release
page is the original record.
Publish one interoperable, privacy-safe incident reproduction contract that Capstead Platform and authorized
lab runners can validate instead of maintaining two prose descriptions that drift silently.
IncidentReproductionBundle v1 JSON Schema. The authoritative draft-2020-12 schema is packaged incapstead-core at schema/incident-reproduction-bundle-v1.json. It separates observed facts,schema/incident-reproduction-bundle-v1.fixture.json travels inintegrityDigest field. The fixture contains a real digest, and tests prove aunknownFacts is required even when empty, so uncertainty cannot disappear by omission.CAPSTEAD_EXECUTION references; an external identifier cannot beRecord what an execution decided, not only how it went — namespaced, allow-listed attributes that persist
with the execution and come back on every read path.
CapabilityExecution has always answered how did this go: timing, success, retries, tokens, cost. It could
not answer what did it decide, and on which version of its inputs. A policy outcome, a sanitized reason
code, the revision of the source or fixture an execution acted on had nowhere to live, and overloading
version (the capability's own) or errorType (how it failed) would have been worse than adding nothing.
Execution attributes. Declare the names your application may use, then record them from inside a
capability:
capstead:
attributes:
allowed:
- policy.authorization.outcome
- policy.authorization.reason
- evidence.sourceRevisionCapabilityExecutionContext.recordAttribute("policy.authorization.outcome", "DENY");
CapabilityExecutionContext.recordAttribute("evidence.sourceRevision", "17");Read them back from CapabilityExecution.attributes() and attribute(name), from
GET /actuator/capabilityexecutions, or from the durable store.
capstead_execution_attribute, written in the same transaction as the execution row. Not a separate
write: an execution row without its attributes is a record saying a decision was made that cannot say what
it was, which is worse than no record for anything auditing it. Keyed (execution_id, attr_name) — one
value per name — and indexed on (attr_name, attr_value), because "which executions denied for this
reason" is the question the table exists to serve.
Attributes are rehydrated by every read path — byId, recent, recentFor, childrenOf and subtree —
rather than by byId alone.
Attributes in the actuator view. CapabilityExecutionView carries an attributes map, added as the
last component so field order is unchanged for anything reading it positionally. Always present, empty
when none were recorded: an absent key and an empty object mean the same thing to a consumer, and one of
them is a null check.
Redaction now applies to attribute values, not only captured input and output. It was applied to
capturedInput and capturedOutput only, which left attributes as a second, uncovered path into durable
storage — an application recording a reason code that happened to carry a token stored it raw, however
carefully it had configured a CapabilityDataRedactor. The captured-I/O path is safe largely because
capture is off by default; attributes have no such default, because recording them is the point.
A value the redactor cannot clean is removed rather than stored raw or replaced with a placeholder — a
placeholder would claim the cleaning succeeded.
Attributes are not Micrometer dimensions, and never will be by default. A run or trace identifier
promoted to a metric tag multiplies your time series by its cardinality and can take a metrics backend
down. CapabilityMetrics builds a fixed four-tag set from explicit fields and cannot see attributes; a
test asserts both that the tag keys are exactly those four and that no tag value equals an attribute
value, so wiring one in later fails a build rather than a Prometheus.
Correlation-identifier lookup. Attributes make correlation ids recordable; findByAttribute — finding
the executions carrying one — is not in this release.
Declaring nothing allows nothing. An empty allow-list records no attributes, which is exactly how an
application behaved before this release. An empty list meaning "anything goes" would make the allow-list
something you have to remember to switch on.
Names are namespaced and matched exactly. At least one dot, a lower-case root segment, camelCase
allowed inside (evidence.sourceRevision). There is no policy.* wildcard, deliberately — it would
re-admit what the allow-list exists to exclude, one namespace at a time. A malformed declared name is
dropped rather than allowed, so a typo fails closed.
Nothing here can fail your business method. A refused attribute is skipped and logged once per name.
Recording is instrumentation; instrumentation that can fail the code it measures is a worse defect than a
missing attribute.
Values are never truncated. Over-length values are refused whole. These are evidence, and a silently
shortened revision reads as a real value while not being one — harder to notice than its absence. The
ceilings are 512 characters per value and 32 attributes per execution.
The columns are attr_name / attr_value. value is a reserved word in H2 and the CREATE TABLE
was rejected outright. Renamed rather than quoted: a quoted identifier has to stay quoted everywhere
forever, and one missed usage fails only on the vendor that reserves the word.
@Capability(name = "Answer Question", domain = "Support")
public Answer answer(String question) {
Retrieval retrieval = retriever.retrieve(question);
CapabilityExecutionContext.recordAttribute("evidence.sourceRevision", retrieval.revision());
CapabilityExecutionContext.recordAttribute("evidence.state", retrieval.state().name());
if (!policy.permits(retrieval)) {
CapabilityExecutionContext.recordAttribute("policy.authorization.outcome", "DENY");
CapabilityExecutionContext.recordAttribute("policy.authorization.reason", "ACCESS_REVOKED");
return Answer.refused();
}
CapabilityExecutionContext.recordAttribute("policy.authorization.outcome", "ALLOW");
return generator.generate(question, retrieval);
}Two executions of that capability can now be told apart by what they decided, not only by what they cost.
Walk a whole capability execution tree in one call, and get the actual tree from the actuator instead of one
level of it.
GET /actuator/capabilityexecutions/{id} nests to full depth, and its children array changed shape.
-{ "execution": { … }, "children": [ { … }, { … } ] }
+{ "execution": { … }, "children": [ { "execution": { … }, "children": [ … ] } ] }children now holds ExecutionTree objects rather than CapabilityExecutionView objects, so a consumer
reading children[].executionId must read children[].execution.executionId instead.
The response type has always been called ExecutionTree and previously returned one execution plus its
direct children. A capability that called a capability that called another reported the middle layer and
stopped, so a caller who did not know to walk it themselves received a complete-looking answer that was
missing everything below depth one. The type now matches its name.
An unknown or aged-out id still returns null, so callers already handling that case are unaffected.
CapabilityExecutionQuery gained subtree(String executionId). Any third-party implementation of that
interface will not compile until it implements the new method. Both bundled implementations
(InMemoryCapabilityExecutionStore, JdbcCapabilityExecutionReader) do.
subtree(executionId) — a whole execution tree in one call. childrenOf answers a single level;
reassembling a tree from it cost a round trip per level and left every caller writing the same traversal.
In-memory this is one pass to index by parent then a breadth-first walk; over JDBC it is a recursive CTE,
exercised against both H2 and MySQL 8.
Ordering is part of the contract: the root comes first and every node appears after its own parent, so a
consumer can build the nested shape in a single pass with no sorting and no second index. Siblings are
most-recent-first, tie-broken on execution id so that executions recorded in the same millisecond — a
capability fanning out to several tools does exactly that — still come back in a fixed order. Both
implementations produce the same order, and a test asserts the property rather than a fixed list.
Malformed graphs are bounded rather than fatal. Nothing validates that parent links form a tree, so a
record whose ancestor claims it as a parent would otherwise loop forever on a read path serving an actuator
endpoint: the in-memory store tracks visited ids, the CTE bounds depth at 50, and each returns a truncated
tree instead of hanging. A duplicated id cannot detach already-attached children from the nested response.
A build. mvn -B -ntp verify now runs on every pull request and push to main, on a runner with
Docker, so the MySQL Testcontainers round-trips actually execute. They are
@Testcontainers(disabledWithoutDocker = true) and therefore skip on a machine without a daemon, which
meant a green local run could be several tests that never ran. The build reports totals and names any suite
that skipped.
JdbcCapabilityExecutionRoundTripTest. Every test opened the same H2 in-memorysubtree returns the same CapabilityExecution records the rest of CapabilityExecutionQuery returns
rather than a new export type. An export shape that consumers persist and diff is a contract worth settling
once, alongside the ordered execution events it will need to carry, so findByAttribute and a versioned
export shape remain open.
// Every descendant, ordered root-first, parents before children.
List<CapabilityExecution> tree = query.subtree(rootExecutionId);GET /actuator/capabilityexecutions/{id}
{
"execution": { "capabilityName": "Generate Course", "executionId": "exec-1", … },
"children": [
{ "execution": { "capabilityName": "Generate Lesson", … },
"children": [ { "execution": { "capabilityName": "Score Lesson", … }, "children": [] } ] }
]
}
io.capstead:capstead-starter:0.8.0 · Maven Central
Meter and price model calls that are not billed by token — text-to-speech characters, transcription
seconds, per-request APIs — with no change to client code.
capstead.capabilities[].usage: declare model, unit (tokens | characters | seconds |requests) and input-from-arg, and the interceptor synthesises one ModelInvocation from the declaredCapabilityUsageRule in the runtime, priced through the existing capstead.cost estimator like anycapstead.cost.models accepts input-only or output-only rates. Usage-metered models have exactly oneDescribe a multi-step workflow in configuration and read its runs back, without client code or schema
changes.
capstead.pipelines: named steps with a bounded max-gap. Runs are assembled at read time from/actuator/capabilitypipelines: pipeline scorecards and per-run drill-down — wall time, tokens, costPipelineDefinition, PipelineRun, PipelineScorecard and PipelineAssembler in the core module.ROLE_INFRASTRUCTURE beans, which silences theDeclarative capabilities stop requiring Spring AI, and the dashboard is worth opening before anything has
run.
CapabilityModelInvoker and CapabilityModelRequest: a one-method SPI in the runtime. Capstead renderscapstead-spring-ai provides the default ChatClient-backed invoker, so Spring AI users need no bean.capstead-starter and no longer depends on Spring AI.Capstead can write and govern a capability, not only observe a hand-written one.
@CapabilityClient on a bodyless interface method: Capstead renders the prompt, routes the model, callsChatClient and binds the response to the return type, while @Capability governs it@Prompt, @SystemPrompt, @ModelProfile and @P.capstead.ai.profiles: model routing lives in configuration, so capability code never names a model./actuator/capabilities, in the dashboard and in domain groupingcapstead-spring-ai alongside the starter. No breaking changes to existing APIs.Completes the 0.3.x line: the dashboard and the scorecards read the durable store, so what they report
survives a restart and covers every instance rather than the one that answered.
/actuator/capabilityscorecard, /actuator/capabilityexecutions and the /capstead dashboard read thecapstead-jdbc is enabled. In 0.3.1 the dashboard read only the in-memory store.Turns @Capability from static metadata into a durable execution recorder.
@Capability calls are linked automatically, andGET /actuator/capabilityexecutions/{id} returns the tree.capstead-jdbc, with a vendor-appropriate schema forcapstead.executions.recording-mode: best-effort, sync or async. Recording never fails theGET /actuator/capabilityexecutions, and /{id} for the tree.CapabilityDataRedactor and CapabilityPrincipalProvider.capstead-jdbc.capstead.capabilities in YAML, and the two coexist.We count visits using a daily rotating token, without analytics cookies. Do Not Track and Global Privacy Control are respected.