Skip to content

Releases: footprintjs/hcifootprint

v2.5.0 — the question is on the record

Choose a tag to compare

@sanjay1909 sanjay1909 released this 26 Aug 10:24

requestInput() — the HITL request lifecycle: the offered-set law (refusal names the list, request stays open), absence established never assumed, first-terminal-wins with late answers kept and quoted, kind-governed and surface-routed with counted gaps. declareLifecycle() — declared state charts that own WHETHER while the host owns WHEN; terminals never reopen, structurally. See CHANGELOG.md.

v2.4.1 — the core, decomposed to the mantra

Choose a tag to compare

@sanjay1909 sanjay1909 released this 26 Aug 10:02

[2.4.1] - 2026-08-26

Changed

  • The core, decomposed to the mantra: connection.ts 2,614 → 992 lines.
    Phase two of the split — composed units, not just moved leaves. The
    runtime now composes KindGovernor, SurfaceBoard, and
    TransitionLedger as owned fields (the ecosystem rule: compose a store
    as a field, no base classes), and the ~740-line heart of connect()
    moved behind a spelled-out seam: ConnectionCore lists every capability
    the builder may use, so a capability not listed is one provably unused.
    The closure web inside the builder stays together BY DESIGN — it shares
    per-connection state, and splitting it would mean turning shared locals
    into a context object: a rewrite wearing a refactor's clothes. Verbatim
    behaviour, all 3,033 tests green, the React treeshake boundary and byte
    cap unchanged.

v2.4.0 — the breakable walk, in a module split to the library's own mantra

Choose a tag to compare

@sanjay1909 sanjay1909 released this 26 Aug 09:51

[2.4.0] - 2026-08-26

Added

  • The breakable walk — the human door, with the reason on the record.
    walk.interrupt({ by, reason }) stops a plan at the next step boundary;
    the in-flight step finishes and settles normally, because an L1 transition
    is atomic — L2 stops future steps, L1 transitions are never torn. The
    manifest carries interrupted: { by, reason, beforeStep }, distinct from a
    refused row, because "the plan was wrong" and "the person knows something
    the plan didn't" must be treated completely differently by whoever
    replans. The reason is REQUIRED — a break without one leaves the model
    replanning blind, which is how it replans the same thing. The walk
    survives the break (replanning continues under the same walk id), an
    interrupt armed between plans is consumed at the next run's step zero —
    the intent stands — and run(steps, { onRow }) streams rows as they land:
    the FE's live loop, and where the stop control belongs, beside it. A
    throwing listener never changes what the walk does or records.

Changed

  • connection.ts split by concern: 2,614 → 1,891 lines, nine small units
    extracted
    stored (the internal data shapes; data separated from
    logic, one shape one owner many operators), progress-ledger,
    settlement, input-validation, principals, authoring,
    binding-facts, declarations, observer-capture — each with its own
    header stating why it exists, mapped in src/action/README.md. Verbatim
    moves, zero behaviour change, all 3,033 tests green. Walk ids are now
    per-runtime rather than module-global (a correlation nicety, not a
    security fix). And the 3.0 subpath-door decision is RECORDED with a CI
    fence: test/barrel-surface.test.ts pins the root surface so growth is a
    deliberate edit and shrink waits for the major — a decision that cannot
    drift is one that does not need re-arguing.

v2.3.1 — the 2.x story, documented where each audience looks

Choose a tag to compare

@sanjay1909 sanjay1909 released this 26 Aug 09:17

[2.3.1] - 2026-08-26

Documentation

  • The 2.x story, documented where each audience looks — because docs are
    the adoption surface.
    Guide pages for the walk, kind governance, and
    surfaces-and-gaps join bring-your-own-skin in the docs directory, each
    opening with WHY before HOW and carrying an example that COMPILES against
    the published package (verified, not assumed). Folder READMEs land beside
    the two newest modules, matching the house convention every other module
    already follows: src/action/README.md maps one file to one concern with
    the reason each exists, and src/react/README.md states what the hook is
    (one skin over the framework-neutral core), what it deliberately is not
    (no strategy option — a seam inside a skin would duplicate the seam the
    core already is), and the four laws any bring-your-own skin must uphold.

v2.3.0 — channels: surfaces serve kinds, and a miss is a counted fact

Choose a tag to compare

@sanjay1909 sanjay1909 released this 26 Aug 09:10

[2.3.0] - 2026-08-26

Added

  • Channels — surfaces declare what they can serve, and a miss is a counted
    fact.
    runtime.declareSurface({ surface, node, collects, shows }) says by
    KIND what one frontend surface can obtain from a person and what it can
    render; runtime.surfacesFor({ collects } | { shows }) answers who can
    serve a kind. An action never names a page and a surface never names an
    action — both name kinds, governed by the mounted catalog with the same
    connect-time refusal a definition gets, and a QUERY is governed too,
    because otherwise the gap record fills with typos and stops meaning
    anything. The degradation record is the point, not the fallback:
    runtime.channelGaps() holds every kind somebody needed served when
    nothing could, COUNTED — asked-once and asked-forty-times-a-day are
    different priorities wearing the same row — so a month of degraded turns
    reads back as a product backlog written by actual usage. One live surface
    per id, duplicate refused; retirement is idempotent, owned by its handle,
    and can never take down a successor declared under the same id.
  • The skin contract (docs: bring your own skin). The React hook is
    sugar, not the seam — a consumer writes their own framework skin against
    connectAction and the host adapter, and the library owes them the four
    laws a skin must uphold: publish only after commit; advance the generation
    when committed input changes (the shipped stale-input defect, by name);
    retire on unmount and revision change; never hold "the current
    transition". The dependency-free Angular test is named as the template for
    proving one. There is deliberately no strategy option on the hook: a
    strategy parameter inside a skin would be a second extension point
    duplicating the seam the core already is.

v2.2.0 — kind governance: the vocabulary, governed before matching exists

Choose a tag to compare

@sanjay1909 sanjay1909 released this 26 Aug 09:01

[2.2.0] - 2026-08-26

Added

  • Kind governance — the vocabulary, governed before matching exists.
    needs and produces name kinds, and two teams declaring array to mean
    different things is the stringly-typed failure rebuilt one level above ids
    (the reference app contains exactly this collision, live). declareKinds()
    builds the default catalog from per-team contributions — the runtime owns
    the merge, and a duplicate is a refusal naming BOTH contributors, never a
    silent last-writer-wins. Mount it with createActionRuntime({ kinds });
    an unknown kind is then refused at CONNECT time, where the developer is
    looking, in the catalog vocabulary. The interface is two methods (has,
    describe) plus a fingerprint — enumeration lives on the concrete default
    only, so a future remote adapter is never forced to promise a listing it
    cannot give synchronously. A mounted catalog is immutable, which makes it
    memoizable: consulted once per kind, ever, so an adapter can never cost the
    offer-serving path. KindRecord.revision rides the fingerprint, because
    kind names are forever while meanings evolve, and two sides holding
    different meanings of one name should refuse loudly at a seam instead of
    matching silently. And no catalog mounted is a VISIBLE state:
    runtime.kindGovernance() reports every kind seen and the ungoverned
    remainder — an unarmed check indistinguishable from a passing one is the
    disease this family keeps curing.

v2.1.0 — the walk: many actions as one correlated route

Choose a tag to compare

@sanjay1909 sanjay1909 released this 26 Aug 00:05

[2.1.0] - 2026-08-25

Added

  • Layer 2 — the walk: many actions as one correlated route. beginWalk()
    admits a PLAN up front and executes it with no model call between steps,
    answering with a MANIFEST — ran, refused, never-reached, per row — because a
    screen action cannot be rolled back, so partial execution must be legible
    rather than summarized as "failed". Admission refuses from declarations
    alone, before anything runs: a step whose decisionOwner the walking
    principal does not own ("an agent may not pre-plan a decision it does not
    own"), a principal outside mayInvoke ("refused at plan time rather than at
    step three of a half-executed screen"), and a carry whose producer
    declares no produces — a carried value must come from a declared output,
    never from whatever a handler happened to return. Execution re-derives every
    step's offer at its own turn: batched is not blind, and a guard that stopped
    holding mid-plan is a refused row with L1's own teaching sentence. One turn,
    one walk, MANY plans — a refused step means the caller replans under the
    same walk id, and walk.record() holds the route actually taken. L2
    introduces no primitive L1 lacks; the walk identity is a correlation, not a
    concept.

v2.0.0 — the Action Binding Protocol

Choose a tag to compare

@sanjay1909 sanjay1909 released this 25 Aug 23:48

[2.0.0] - 2026-08-25

The Action Binding Protocol. Declare an action once; bind it to every live
control that can perform it. Four identities with four cardinalities — a
definition is what a capability IS, a binding is where it is connected, an
offer is the facts it was exposed under, a transition is one invocation — and
no consumer ever recovers one from another by string surgery. This is the v2
contract: createActionBindingRuntime is now createActionRuntime, offers are
principal-scoped (runtime.forPrincipal(p).offers() — the principal belongs to
the reader, not the binding), progress reports declared/observed/unreported
at close, abandoned requires an explicit authority (never inferred from a
detach), and offered inputs are revision-exact: an invocation can never read a
newer committed input while carrying an older offer.

Fixed

  • Late evidence is kept, marked late, and reopens nothing. A settlement
    arriving after the terminal used to vanish into a return of the first one.
    It now rides the transition as lateSettlements — a QUOTATION of what the
    late caller claimed and the payload it brought, never validated as if
    accepted and never allowed to reopen a terminal. Absent when none arrived,
    because an empty list would claim "we watched and none came."
  • A stale offer is no longer called a forgery. Invalidation deletes a
    retired offer from every map, so the old refusal accused a slow caller
    replaying yesterday's offer and an attacker of the same crime. The refusal
    now says what the runtime can still establish: a binding serving a current
    offer names it as the next move; a registered but offerless binding says the
    surface moved on; an unknown binding admits it cannot tell never-ours from
    since-detached, and will not guess.

Added

  • Action Binding now joins what was offered, invoked, and settled.
    defineAction() declares one callable action, framework-neutral connections
    bind its live instances, and runtime.available() returns self-describing
    none, bound, or open offers. runtime.invoke() is the single broker for
    offered execution: it accepts only the exact retained offer, captures bound
    input when that offer is minted, validates caller input for open offers, and
    carries a non-secret input receipt through the invocation, transition, and
    settlement history. Definitions explicitly declare invocation: 'inputless' | 'scalar' | 'host'; broker eligibility is never guessed from
    Function.length. Open offers require one caller slot and are withheld when
    no schema tells a UI/agent how to construct that input.
  • Channel declarations have a stable Layer 1 home. Callable definitions can
    store typed needs and produces kind declarations. They are validated and
    frozen but deliberately inert until the later channel/surface broker exists.
  • Input contracts have an enforcement port. Schemas with synchronous
    .safeParse() or .parse() methods are enforced directly; applications can
    supply inputSchemaAdapter for formats such as JSON Schema plus Ajv. Strict
    activation refuses an unenforceable schema instead of presenting metadata as
    an active gate.
  • Live fact generations are explicit. connection.update() is inert when
    facts do not change, while connection.touch() deliberately retires existing
    offers without running application readers. React input bindings require an
    inputKey; changing it atomically publishes the newly committed input reader
    and invalidates the prior offer without changing binding identity.
  • Invocation outcomes distinguish refusal from failure. refused means the
    application handler never started; failed means it started and then
    threw/rejected. Preflight input refusals immediately settle their effect as
    not attempted, and connection observers now receive direct, brokered, and
    host-continuation invocations through one narrow settlement capability.
  • React availability generations are explicit. availabilityKey names the
    committed facts read by adapter enabled/busy readers. Omitting it chooses a
    conservative revision after every commit; simultaneous input and availability
    changes still publish one revision.

This will be the first public callable Action Binding shape. Its cross-copy brand
is hcifootprint.action-definition.v1; that suffix versions the frozen branded
record, independently of the npm major version. The existing navigation-graph
ActionDef.input field remains unchanged; callable definitions deliberately use
inputSchema.

v1.13.0 — actions that happen once, and ledgers that tell the truth about what they dropped

Choose a tag to compare

@sanjay1909 sanjay1909 released this 20 Aug 21:51

[1.13.0] - 2026-08-20

Added

  • concurrency: { mode: 'once' } — the receipt survives settlement
    (Context Integrity, the choice seam). single-flight suppresses a repeat
    while the first occurrence is unresolved; the measured duplicate-execution
    failure happened after it settled. 'once' is a third mode at the same
    declaration door: one EXECUTED occurrence per scope for the session, refused
    DUPLICATE_EXECUTION with the receipt (priorTransitionId) and the authored
    howToRepeat sentence. Two field lessons are the law's shape: only a person
    acting on the screen (a user-attributed transition after the receipt) reopens
    the action, and the reopened repeat FIRES carrying repeated on the result
    AND the transition record — report, don't refuse. A refused first occurrence
    minted no receipt; an 'unobservable' one counts (unprovable non-execution
    is not non-execution). While the first is unsettled, 'once' answers exactly
    as single-flight (PRIOR_FIRE_PENDING). The served row carries the verdict
    (alreadyPerformed, action scope only) and Mode B greys the control out —
    never send a person to approve a fire about to be turned away. Scopes,
    payload identity and the matcher are single-flight's own (sameFire, one
    owner of "what counts as the same fire again").

  • TransitionRecord.instance — the repeats-container card a fire named now
    rides its receipt. A record that says what was pressed but not which row
    cannot answer "did we already cancel order #57?".

  • Bounded ledgers state their retention window, and every silent cut now
    speaks
    (Context Integrity, phase 0). session.offersRetention() /
    session.acknowledgementsRetention() answer { minted, dropped, firstRetained, lastRetained } — the window that is still answerable, not
    only the loss — and session.offerStanding(id) asks the three-way
    retained/evicted/unknown question without firing anything. Oversized
    sensing trails join the house standard: evictions are counted
    (sensedTrailsDropped()), and sensedTrail() on an evicted trail says
    EVICTED with the surviving count instead of an answer indistinguishable
    from "never sensed". The data channel's silent caps now announce
    themselves the way its string cap always has: an array cut at 30 appends
    … N more omitted, an object cut at 40 keys carries "…": "N more key(s) omitted". And a repeats row's render cap states its size:
    instancesTotal rides the served row and both instance refusals, so 50
    keys of 200 can never read as 50 of 50 (fireability was always uncapped;
    now the served row says so).

  • The unknown-instance refusal says which kind of wrong it is (Context
    Integrity). INSTANCE_UNKNOWN now carries
    verdict: 'never-existed' | 'unsupported', decided by the coverage of the
    set the id was compared against — the same source the served row's
    enumeration states. Absent from the app's declared existence source (a
    selector set enumerates everything that exists) → 'never-existed', and
    the refusal may finally say so. Absent from the mounted window →
    'unsupported': not backed by anything served, and that is ALL the refusal
    knows — a window has an edge, the world continues past it, and under a
    capped window an id past the render cap is exactly the case
    'unsupported' protects. A selector that throws or answers a non-array
    falls back to the mounted window, and the verdict falls back WITH it: a
    nonexistence claim never outlives its evidence.

v1.12.0 — an effect that is already true says so

Choose a tag to compare

@sanjay1909 sanjay1909 released this 20 Aug 04:51

An effect that is already true is not a pending one. When an action's declarative verify contract covers every key it declares it writes and already holds at fire time, the fire never waits for a state report that nothing will send — it settles on its own handler and answers alreadyTrue.

alreadyTrue — for an action whose effect the world already holds

What changes. Press a control while the thing it does is already the case, and the fire says so on the very first result instead of waiting forever. It carries the conditions that already hold, plus one sentence telling the reader nothing needed changing and not to press it again. Your handler still runs — deciding on your behalf that a press was pointless is not this library's call.

Why it wasn't there. An agent opened a domain view while already inside that domain. The app's store publishes when a value changes, so writing the value it already held notified nobody, the report never came, and the fire waited for it with no clock over it. did_it_work could only answer still-pending; the agent read that as the app still working, and spent fifteen of its thirty steps on an outcome that could never arrive. Nobody was at fault — the app's netting is correct and the library's refusal to guess is correct. The library is simply the only party standing where both halves are visible.

How it improves. Declare the postcondition beside the action and the library can see the outcome already holds before anything runs. An action with no declarative verify behaves exactly as it did in 1.11.0 — nothing is guessed on its behalf.

Four refusals learn to teach

ACTION_REQUIRED, JOURNEY_REQUIRED, TRANSITION_ID_REQUIRED and KEY_REQUIRED were raised as bare codes. Each now carries a plain sentence saying what is needed, the valid values where a valid set exists, and the fact that nothing was performed — a missing argument is not the app turning something down. KEY_REQUIRED deliberately serves no list and says why, because no ceiling of valid keys exists.

Every code is unchanged for consumers catching them today. Clean-room probed on the packed tarball: 66/66.