Skip to main content
·4 min read

Swap a derivation while the system is running

Directive Labs·

Directive lets you replace a definition on a live system. Swap a constraint's condition, redefine a computed value, unregister a resolver — all while the thing is running. It's the feature that makes hot reload, feature flags on rules, and live-tuned thresholds possible without a restart.

In 1.28.0, replacing a derivation stopped leaving await system.settle() waiting.

const system = createSystem({ module });
system.start();
await system.settle();

system.derive.assign('riskScore', (facts) => facts.amount * 3);

await system.settle();   // now returns

What was happening

The system schedules a pass whenever there's work to announce. Its definition of "work" was a fact that changed.

Replacing a derivation doesn't change a fact. It changes what a fact means — the value goes stale, everything downstream of it goes stale, and the system knows it. But nothing was scheduled to say so, and settle() waits for the system to go quiet after saying everything it has to say.

So the announcement sat with nowhere to go, and the wait went on. It came unstuck only if unrelated traffic happened to arrive and drag a pass along with it.

Definition changes now schedule a pass when one is owed. If you're using derive.assign, derive.register, or derive.unregister on a running system, this is worth the upgrade on its own.


system.meta.revision()

Also new, and useful if you've ever cached anything about your own system's shape.

Directive lets you tag definitions and search them:

system.meta.byTag('pii');       // every definition tagged 'pii'
system.meta.byCategory('auth'); // every definition in a category

Both walk every definition in the system. That's fine occasionally and expensive on a hot path — so anything asking per-operation caches the answer. And then has no way to learn the answer went stale, short of asking again, which is the thing it was avoiding.

revision() is an integer you can compare instead:

let tagged = system.meta.byTag('pii');
let seenAt = system.meta.revision();

function currentTags() {
  const now = system.meta.revision();
  if (now !== seenAt) {          // one integer compare
    tagged = system.meta.byTag('pii');
    seenAt = now;
  }
  return tagged;
}

Comparing costs nothing. Rebuilding costs a walk. Now you only pay the second one when something actually moved.

Read the contract narrowly. Only equality is meaningful — the starting value, the step size, and whether it moves for a change that turns out not to affect your tag are all unspecified. A rebuild you didn't need is harmless; a rebuild you skipped is not, so the number is deliberately generous about moving.

Today it moves when a module is registered. Treat it as a same-instance, same-process signal: don't persist it, don't compare one system's revision against another's, and don't assume it increments by one.

Our own PII guardrail uses it — that's what it was built for. It screens fact writes against a set of tagged keys, and that set used to be built once at startup, which meant a module registered later brought facts it never learned about.


Also in 1.28.0

Effects stop pinning values they no longer read. Switching an effect off used to leave the computed values it had read marked as watched for the life of the system — work the runtime kept doing for a reader that had gone. Disabling an effect now releases them, the same way disabling a constraint always has.

A derivation can be named after something on Object.prototype. Call one toString, valueOf or hasOwnProperty and reading it used to resolve the inherited built-in function instead of your value — so a constraint gated on it was unconditionally truthy, with no error anywhere. Reads are own-property now.


Upgrading

npm install @directive-run/core@latest

No API changes. If you replace derivations at runtime, or cache anything derived from meta.byTag, this one's for you.


The theme, if there is one: a system that lets you change its rules while it runs has to be equally good at noticing that you did. Facts were always watched closely. Definitions are catching up.


Related

  • Test doubles that can say no createMockAgentRunner now honours AbortSignal, so cancellation is something you can actually test. Plus race telling a cancelled agent apart from a failed one.
  • The PII screen sees more of your data now The personal-data guardrail keeps up with modules registered at runtime, and no longer skips a whole value because one member is awkward.
  • Pricing tables that know their own age Every rate table in @directive-run/ai now ships the date a human last checked it against the provider — and a check that fails when that date gets old.

Directive is free and open source. If this was useful, consider supporting the project.

Stay in the loop. Sign up for our newsletter.

We care about your data. We'll never share your email.

Powered by Directive. This signup uses a Directive module with facts, derivations, constraints, and resolvers – zero useState, zero useEffect. Read how it works