Add Orca.validate() — static graph analysis of registered actions

Third batch of v1 features. With the gates honored, configuration
mistakes (orphan tokens, dependency cycles, ordering bugs) became
silently fatal: the action skips/blocks at runtime with a misleading
"reason" like "after-not-met:foo" without telling the developer that
"foo" is never produced by anything. validate() catches those before
the first event arrives.

API: EngineOrca.validate(): OrcaValidateResult
  - ok: false iff any issue has severity 'error'
  - issues: array of OrcaValidateIssue { kind, severity, event,
    actionId?, token?, cycle?, message }
  - Never throws. Read-only over the registered set; safe to call
    multiple times during config; engine does not gate runs on result.

Issue kinds:

  - unsatisfiable-after (ERROR) — an action's `after: [T]` is not
    produced by any earlier action in canonical order. Action would
    always skip at runtime.

  - orphan-unless / orphan-abort-on (WARN) — gate token has no
    upstream producer. May be deliberate (forward-compat / typo
    guard); demoted to warning so `ok` stays true.

  - dependency-cycle (ERROR) — actions block on each other through
    `after` / `provides`. DFS over an action-level adjacency map
    (A → set of action ids it depends on); cycles deduped via a
    canonical fingerprint (rotated to lexicographically smallest id
    first).

Canonical order matters: actions are sorted by (stage, registeredAt)
so the validator's "did any earlier action provide T" matches what
the engine does at runtime. Concrete consequences:
  - A producer registered AFTER the consumer in the same stage is
    NOT considered upstream — runtime would skip the consumer, and
    validate() reports it.
  - A producer in a LATER stage (e.g. POST when consumer is MAIN)
    is NOT considered upstream either.

Tests (+11):
  - empty engine returns { ok: true, issues: [] }
  - happy path: provider in earlier stage satisfies consumer
  - reports unsatisfiable-after for missing token
  - reports unsatisfiable-after when producer registered AFTER consumer
    in the same stage (subtle ordering bug)
  - reports unsatisfiable-after when producer is in a later stage
  - reports orphan-unless as warning (ok stays true)
  - reports orphan-abort-on as warning
  - detects direct 2-action cycle
  - detects indirect 3-action cycle (cycle.length === 4 with closure)
  - warnings do not flip ok to false
  - issues are isolated per event (cross-event tokens don't satisfy
    each other)

Constants exported from $orca:
  ORCA_VALIDATE_UNSATISFIABLE_AFTER
  ORCA_VALIDATE_ORPHAN_UNLESS
  ORCA_VALIDATE_ORPHAN_ABORT_ON
  ORCA_VALIDATE_DEPENDENCY_CYCLE
  ORCA_VALIDATE_SEVERITY_ERROR
  ORCA_VALIDATE_SEVERITY_WARN

Types exported: OrcaValidateIssue, OrcaValidateIssueKind,
OrcaValidateResult, OrcaValidateSeverity.

README updated: validate() moved from "Roadmap v1" into "Ya en el motor
(de v1)". Remaining v1 items: compensate, commit() configuration freeze,
queue policies (commit/replace), transaction groups, parallel,
payload-bearing tokens, fan-in, bus interception (Option B), and
createActiveOrca() reactive wrapper.

Verification: 1392/1392 tests pass (82 in orca, +11 from this commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
master
dev 5 months ago
parent 79d6d1f776
commit 7aebe7c673

@ -25,7 +25,7 @@ que el acoplamiento inter-modulo quede escondido en `buss`, `connection` o
## Estado Del Documento
Este README es la referencia de diseño de orca. La **v0-kernel** está
implementada y testeada (114 archivos / 1381 tests pasan, 71 de ellos
implementada y testeada (114 archivos / 1392 tests pasan, 82 de ellos
sobre orca). El kernel expone:
- `createEngineOrca({ bus, timers, logger?, maxRuns?, reentry? })`
@ -51,6 +51,11 @@ sobre orca). El kernel expone:
`unless` (idempotencia) gana sobre `abortOn` (halt) gana sobre
`after` (dependencia faltante). El stage `FINALLY` bypasa los
gates — la limpieza siempre corre.
- **`validate()`:** análisis estático del grafo de acciones por
evento. Detecta `after` cuyas dependencias ningún `provides`
upstream produce (error), `unless` y `abortOn` huérfanos
(warnings), y ciclos en la cadena `after`/`provides` (error).
No lanza; devuelve `{ ok, issues }`.
- Diagnostics estructurados (`orca.run.*`, `orca.action.*`,
`orca.event.emitted`, `orca.reentry.blocked`, `orca.trace.aborted`,
`orca.action.fatal`, `orca.action.timeout`, `orca.action.blocked`)
@ -1335,13 +1340,13 @@ Marcado como `@v1+` en el código fuente:
| Pieza | Qué falta para v1 |
|---|---|
| `compensate` | Invocar la función compensatoria cuando el run aborta tras un éxito previo. |
| `commit()` / `replace` | Políticas de cola por evento (drop-prev / replace / parallel). |
| `commit()` configuración | Congelar el grafo (rechazar `onEvent` posteriores) tras `validate()`. |
| `commit()` / `replace` cola | Políticas de cola por evento (drop-prev / replace / parallel). |
| `transaction` (atómico) | Grupos atómicos cuyo fallo lanza compensaciones en orden inverso. |
| `parallel` | Ejecución concurrente dentro de un mismo stage cuando no hay `after`. |
| Tokens con payload | Hoy son `string`; con payload abren coordinación más rica intra-run. |
| `fan-in` | Acción que dispara cuando _varios_ tokens están presentes. |
| Bus interception (Opción B) | Atribuir `emittedByAction` perfecto cuando un módulo hace `bus.publish` durante un run. |
| `validate()` / `commit()` configuración | Validación estática del grafo (ciclos, tokens imposibles, IDs duplicados). |
| `createActiveOrca()` | Wrapper reactivo (`$state` para `running`, `recentRuns`, etc.) — el `EngineOrca` ya está. |
### Ya en el motor (de v1)
@ -1362,3 +1367,14 @@ Marcado como `@v1+` en el código fuente:
`unless-triggered:<token>` / `abort-on-triggered:<token>` /
`after-not-met:<tok1>,<tok2>,…`. El stage `FINALLY` los bypasa
siempre.
- ✅ **`validate()`** — análisis estático que recorre las acciones
registradas por evento, en orden canónico `(stage, registeredAt)`,
acumulando los `provides` upstream. Para cada acción reporta:
`unsatisfiable-after` (error: token no producido por nadie ni en
un stage anterior ni más arriba en el mismo stage),
`orphan-unless` / `orphan-abort-on` (warnings: gate inútil),
`dependency-cycle` (error: A.after necesita lo que B.provides y
viceversa). Devuelve `{ ok, issues }`; nunca lanza. La aplicación
decide si tratar las issues como bloqueantes — el motor no congela
registros ni runs en función del resultado (el `commit()`
configuracional queda para v1.x).

@ -105,6 +105,41 @@ export const ORCA_GATE_REASON_ABORT_ON_TRIGGERED = 'abort-on-triggered' as const
/** The action's signal was already aborted when it would have run. */
export const ORCA_GATE_REASON_RUN_ABORTED = 'run-aborted' as const;
// ── Validation issue kinds ─────────────────────────────────────────────
//
// `Orca.validate()` walks the registered actions and reports
// configuration problems before the first event arrives. Issues carry a
// machine-readable kind plus a human-readable message; the engine never
// throws from validate() — the caller decides what to do with the
// result.
/**
* An action declared `after: [T]` but no action registered earlier in
* canonical order (stage then registration time) for the same event
* declares `provides: [T]`. The action will always be skipped at
* runtime.
*/
export const ORCA_VALIDATE_UNSATISFIABLE_AFTER = 'unsatisfiable-after' as const;
/**
* An action declared `unless: [T]` but no upstream action provides T.
* The unless gate cannot fire, so the field is effectively dead. May
* be intentional (forward-compat / typo guard) — emitted as a warning.
*/
export const ORCA_VALIDATE_ORPHAN_UNLESS = 'orphan-unless' as const;
/**
* An action declared `abortOn: [T]` but no upstream action provides T.
* Same severity as orphan-unless — likely a typo or future-proofing.
*/
export const ORCA_VALIDATE_ORPHAN_ABORT_ON = 'orphan-abort-on' as const;
/**
* A cycle exists in the `after`/`provides` dependency graph for an
* event. Two actions block on each other; both would always skip.
*/
export const ORCA_VALIDATE_DEPENDENCY_CYCLE = 'dependency-cycle' as const;
export const ORCA_VALIDATE_SEVERITY_ERROR = 'error' as const;
export const ORCA_VALIDATE_SEVERITY_WARN = 'warn' as const;
// ── Error policies ──────────────────────────────────────────────────────
// v0.0 implements CONTINUE and ABORT_RUN. The remaining policies are
// accepted in the type and treated as CONTINUE.

@ -69,7 +69,13 @@ import {
ORCA_RUN_SUCCESS,
ORCA_RUN_TIMEOUT,
ORCA_STAGE_FINALLY,
ORCA_STAGES_CANONICAL_ORDER
ORCA_STAGES_CANONICAL_ORDER,
ORCA_VALIDATE_DEPENDENCY_CYCLE,
ORCA_VALIDATE_ORPHAN_ABORT_ON,
ORCA_VALIDATE_ORPHAN_UNLESS,
ORCA_VALIDATE_SEVERITY_ERROR,
ORCA_VALIDATE_SEVERITY_WARN,
ORCA_VALIDATE_UNSATISFIABLE_AFTER
} from './consts.ts';
import {
OrcaDisposedError,
@ -98,7 +104,9 @@ import type {
OrcaRunResult,
OrcaStage,
OrcaToken,
OrcaTraceId
OrcaTraceId,
OrcaValidateIssue,
OrcaValidateResult
} from './types.ts';
import type { Logger } from '$libs/logger';
import type { BusSubscription } from '$libs/bus';
@ -832,6 +840,9 @@ export function createEngineOrca(options: EngineOrcaOptions): EngineOrca {
recentRuns(): readonly OrcaRunResult[] {
return recentRuns.slice();
},
validate(): OrcaValidateResult {
return validateConfiguration(actionsByEvent);
},
get running(): boolean {
return running;
},
@ -1017,3 +1028,183 @@ function createNoopLogger(): Logger {
fatal: noop
};
}
// ── Validation ────────────────────────────────────────────────────────
interface RegisteredActionEntry {
readonly action: OrcaAction;
readonly registeredAt: number;
}
/**
* Walk every registered event and return configuration issues. Each
* event is analysed in isolation because actions can only react to
* tokens emitted by sibling actions of the same run.
*
* Within an event, canonical order is `(stage, registeredAt)`: stages
* in `ORCA_STAGES_CANONICAL_ORDER`, ties broken by registration order.
* That mirrors what the engine does at runtime, so the validator's
* "did any earlier action provide token T" question matches reality.
*/
function validateConfiguration(
actionsByEvent: Map<string, RegisteredActionEntry[]>
): OrcaValidateResult {
const issues: OrcaValidateIssue[] = [];
for (const [event, registered] of actionsByEvent) {
const sorted = sortActionsCanonical(registered);
validateGatesForEvent(event, sorted, issues);
validateCyclesForEvent(event, sorted, issues);
}
const ok = issues.every((i) => i.severity !== ORCA_VALIDATE_SEVERITY_ERROR);
return { ok, issues };
}
function sortActionsCanonical(
registered: readonly RegisteredActionEntry[]
): readonly RegisteredActionEntry[] {
const stageRank = new Map<OrcaStage, number>(
ORCA_STAGES_CANONICAL_ORDER.map((s, i) => [s, i])
);
return [...registered].sort((a, b) => {
const aRank = stageRank.get(a.action.stage) ?? Number.MAX_SAFE_INTEGER;
const bRank = stageRank.get(b.action.stage) ?? Number.MAX_SAFE_INTEGER;
if (aRank !== bRank) return aRank - bRank;
return a.registeredAt - b.registeredAt;
});
}
/**
* For each action, check every gate token against the cumulative set of
* tokens that earlier actions (in canonical order) declare via
* `provides`. A gate token without any upstream producer is reported
* with the appropriate kind.
*/
function validateGatesForEvent(
event: string,
sorted: readonly RegisteredActionEntry[],
issues: OrcaValidateIssue[]
): void {
const providedSoFar = new Set<OrcaToken>();
for (const entry of sorted) {
const action = entry.action;
if (action.after) {
for (const token of action.after) {
if (!providedSoFar.has(token)) {
issues.push({
kind: ORCA_VALIDATE_UNSATISFIABLE_AFTER,
severity: ORCA_VALIDATE_SEVERITY_ERROR,
event,
actionId: action.id,
token,
message: `action "${action.id}" waits for token "${token}" but no earlier action on event "${event}" declares it in \`provides\``
});
}
}
}
if (action.unless) {
for (const token of action.unless) {
if (!providedSoFar.has(token)) {
issues.push({
kind: ORCA_VALIDATE_ORPHAN_UNLESS,
severity: ORCA_VALIDATE_SEVERITY_WARN,
event,
actionId: action.id,
token,
message: `action "${action.id}" declares \`unless: [${token}]\` but no earlier action on event "${event}" provides it — gate cannot fire`
});
}
}
}
if (action.abortOn) {
for (const token of action.abortOn) {
if (!providedSoFar.has(token)) {
issues.push({
kind: ORCA_VALIDATE_ORPHAN_ABORT_ON,
severity: ORCA_VALIDATE_SEVERITY_WARN,
event,
actionId: action.id,
token,
message: `action "${action.id}" declares \`abortOn: [${token}]\` but no earlier action on event "${event}" provides it — gate cannot fire`
});
}
}
}
// After this action, its `provides` join the upstream set.
if (action.provides) for (const t of action.provides) providedSoFar.add(t);
}
}
/**
* Detect dependency cycles among actions of the same event: A.after
* contains a token that B provides; B.after contains a token that A
* provides. DFS over an action-level adjacency map; cycle path is
* returned as the action ids in encounter order.
*/
function validateCyclesForEvent(
event: string,
sorted: readonly RegisteredActionEntry[],
issues: OrcaValidateIssue[]
): void {
// Map action id → set of action ids it depends on (must run before).
const adjacency = new Map<OrcaActionId, Set<OrcaActionId>>();
for (const { action } of sorted) {
adjacency.set(action.id, new Set());
}
for (const { action } of sorted) {
if (!action.after) continue;
for (const token of action.after) {
for (const candidate of sorted) {
if (candidate.action.id === action.id) continue;
if (candidate.action.provides?.includes(token)) {
adjacency.get(action.id)?.add(candidate.action.id);
}
}
}
}
const visiting = new Set<OrcaActionId>();
const visited = new Set<OrcaActionId>();
const stack: OrcaActionId[] = [];
const reportedCycles = new Set<string>();
function dfs(actionId: OrcaActionId): void {
if (visited.has(actionId)) return;
if (visiting.has(actionId)) {
const start = stack.indexOf(actionId);
const cycle = [...stack.slice(start), actionId];
const fingerprint = canonicalCycleFingerprint(cycle);
if (!reportedCycles.has(fingerprint)) {
reportedCycles.add(fingerprint);
issues.push({
kind: ORCA_VALIDATE_DEPENDENCY_CYCLE,
severity: ORCA_VALIDATE_SEVERITY_ERROR,
event,
cycle,
message: `dependency cycle on event "${event}": ${cycle.join(' -> ')}`
});
}
return;
}
visiting.add(actionId);
stack.push(actionId);
for (const dep of adjacency.get(actionId) ?? []) dfs(dep);
stack.pop();
visiting.delete(actionId);
visited.add(actionId);
}
for (const id of adjacency.keys()) dfs(id);
}
/** Rotate the cycle so the smallest id comes first; used to dedupe. */
function canonicalCycleFingerprint(cycle: readonly OrcaActionId[]): string {
if (cycle.length === 0) return '';
// Last element is the loop closure (same as first). Drop it for the
// fingerprint, then rotate.
const trimmed = cycle.slice(0, -1);
let minIndex = 0;
for (let i = 1; i < trimmed.length; i++) {
if (trimmed[i] < trimmed[minIndex]) minIndex = i;
}
const rotated = trimmed.slice(minIndex).concat(trimmed.slice(0, minIndex));
return rotated.join('|');
}

@ -57,6 +57,12 @@ export {
ORCA_GATE_REASON_UNLESS_TRIGGERED,
ORCA_GATE_REASON_ABORT_ON_TRIGGERED,
ORCA_GATE_REASON_RUN_ABORTED,
ORCA_VALIDATE_UNSATISFIABLE_AFTER,
ORCA_VALIDATE_ORPHAN_UNLESS,
ORCA_VALIDATE_ORPHAN_ABORT_ON,
ORCA_VALIDATE_DEPENDENCY_CYCLE,
ORCA_VALIDATE_SEVERITY_ERROR,
ORCA_VALIDATE_SEVERITY_WARN,
ORCA_DIAGNOSTIC_EVENTS,
ORCA_DEFAULT_MAX_RUNS,
ORCA_DEFAULT_MAX_DEPTH,
@ -112,7 +118,11 @@ export type {
OrcaSuccess,
OrcaTimeout,
OrcaToken,
OrcaTraceId
OrcaTraceId,
OrcaValidateIssue,
OrcaValidateIssueKind,
OrcaValidateResult,
OrcaValidateSeverity
} from './types.ts';
export type {

@ -1965,3 +1965,242 @@ describe('EngineOrca v1 — gate precedence and FINALLY bypass', () => {
});
});
// ── v1: validate() static graph analysis ──────────────────────────────
describe('EngineOrca v1 — validate()', () => {
let bus: FakeBus;
let timers: ReturnType<typeof createFakeTimers>;
beforeEach(() => {
bus = createFakeBus();
timers = createFakeTimers();
});
it('returns ok=true with no issues for an empty engine', () => {
const orca = createEngineOrca({ bus, timers });
const result = orca.validate();
expect(result.ok).toBe(true);
expect(result.issues).toEqual([]);
});
it('returns ok=true when after tokens are all produced upstream', () => {
const orca = createEngineOrca({ bus, timers });
orca.onEvent('e', {
id: 'producer',
stage: ORCA_STAGE_PRE,
provides: ['ready'],
action: () => orcaSuccess({ emits: ['ready'] })
});
orca.onEvent('e', {
id: 'consumer',
stage: ORCA_STAGE_MAIN,
after: ['ready'],
action: () => orcaSuccess()
});
const result = orca.validate();
expect(result.ok).toBe(true);
expect(result.issues).toEqual([]);
});
it('reports unsatisfiable-after when no upstream action provides the token', () => {
const orca = createEngineOrca({ bus, timers });
orca.onEvent('e', {
id: 'consumer',
stage: ORCA_STAGE_MAIN,
after: ['never-produced'],
action: () => orcaSuccess()
});
const result = orca.validate();
expect(result.ok).toBe(false);
expect(result.issues).toHaveLength(1);
expect(result.issues[0].kind).toBe('unsatisfiable-after');
expect(result.issues[0].severity).toBe('error');
expect(result.issues[0].actionId).toBe('consumer');
expect(result.issues[0].token).toBe('never-produced');
});
it('reports unsatisfiable-after when the producer is registered AFTER the consumer in the same stage', () => {
const orca = createEngineOrca({ bus, timers });
// consumer registered first; producer registered after — order
// matters within a stage.
orca.onEvent('e', {
id: 'consumer',
stage: ORCA_STAGE_MAIN,
after: ['ready'],
action: () => orcaSuccess()
});
orca.onEvent('e', {
id: 'producer',
stage: ORCA_STAGE_MAIN,
provides: ['ready'],
action: () => orcaSuccess({ emits: ['ready'] })
});
const result = orca.validate();
expect(result.ok).toBe(false);
expect(result.issues[0].kind).toBe('unsatisfiable-after');
expect(result.issues[0].actionId).toBe('consumer');
});
it('reports unsatisfiable-after when the producer is in a LATER stage', () => {
const orca = createEngineOrca({ bus, timers });
// producer in POST cannot satisfy consumer in MAIN.
orca.onEvent('e', {
id: 'consumer',
stage: ORCA_STAGE_MAIN,
after: ['ready'],
action: () => orcaSuccess()
});
orca.onEvent('e', {
id: 'producer',
stage: ORCA_STAGE_POST,
provides: ['ready'],
action: () => orcaSuccess({ emits: ['ready'] })
});
const result = orca.validate();
expect(result.ok).toBe(false);
expect(result.issues[0].kind).toBe('unsatisfiable-after');
});
it('reports orphan-unless as a warning, not an error', () => {
const orca = createEngineOrca({ bus, timers });
orca.onEvent('e', {
id: 'idempotent',
stage: ORCA_STAGE_MAIN,
unless: ['nonexistent'],
action: () => orcaSuccess()
});
const result = orca.validate();
expect(result.ok).toBe(true); // warnings don't flip ok
expect(result.issues).toHaveLength(1);
expect(result.issues[0].kind).toBe('orphan-unless');
expect(result.issues[0].severity).toBe('warn');
});
it('reports orphan-abort-on as a warning', () => {
const orca = createEngineOrca({ bus, timers });
orca.onEvent('e', {
id: 'cautious',
stage: ORCA_STAGE_MAIN,
abortOn: ['nonexistent'],
action: () => orcaSuccess()
});
const result = orca.validate();
expect(result.ok).toBe(true);
expect(result.issues[0].kind).toBe('orphan-abort-on');
expect(result.issues[0].severity).toBe('warn');
});
it('detects a direct dependency cycle between two actions', () => {
const orca = createEngineOrca({ bus, timers });
// A.after = [B's token]; B.after = [A's token]. Both deadlock.
orca.onEvent('e', {
id: 'A',
stage: ORCA_STAGE_MAIN,
after: ['B-token'],
provides: ['A-token'],
action: () => orcaSuccess({ emits: ['A-token'] })
});
orca.onEvent('e', {
id: 'B',
stage: ORCA_STAGE_MAIN,
after: ['A-token'],
provides: ['B-token'],
action: () => orcaSuccess({ emits: ['B-token'] })
});
const result = orca.validate();
// Cycle issue + unsatisfiable-after issues for both actions
// (since each waits for a token only the other provides).
expect(result.ok).toBe(false);
const cycle = result.issues.find((i) => i.kind === 'dependency-cycle');
expect(cycle).toBeDefined();
expect(cycle?.cycle).toBeDefined();
expect(cycle?.cycle?.length).toBeGreaterThanOrEqual(2);
});
it('detects an indirect (3-action) dependency cycle', () => {
const orca = createEngineOrca({ bus, timers });
orca.onEvent('e', {
id: 'A',
stage: ORCA_STAGE_MAIN,
after: ['C-token'],
provides: ['A-token'],
action: () => orcaSuccess()
});
orca.onEvent('e', {
id: 'B',
stage: ORCA_STAGE_MAIN,
after: ['A-token'],
provides: ['B-token'],
action: () => orcaSuccess()
});
orca.onEvent('e', {
id: 'C',
stage: ORCA_STAGE_MAIN,
after: ['B-token'],
provides: ['C-token'],
action: () => orcaSuccess()
});
const result = orca.validate();
const cycle = result.issues.find((i) => i.kind === 'dependency-cycle');
expect(cycle).toBeDefined();
expect(cycle?.cycle?.length).toBe(4); // A,B,C,A (loop closure)
});
it('does not flag warnings as errors in `ok`', () => {
const orca = createEngineOrca({ bus, timers });
orca.onEvent('e', {
id: 'a',
stage: ORCA_STAGE_MAIN,
unless: ['unknown'],
action: () => orcaSuccess()
});
orca.onEvent('e', {
id: 'b',
stage: ORCA_STAGE_MAIN,
abortOn: ['also-unknown'],
action: () => orcaSuccess()
});
const result = orca.validate();
expect(result.ok).toBe(true);
expect(result.issues).toHaveLength(2);
expect(result.issues.every((i) => i.severity === 'warn')).toBe(true);
});
it('isolates events: a problem on one event does not bleed into another', () => {
const orca = createEngineOrca({ bus, timers });
orca.onEvent('event-a', {
id: 'broken',
stage: ORCA_STAGE_MAIN,
after: ['missing'],
action: () => orcaSuccess()
});
orca.onEvent('event-b', {
id: 'fine',
stage: ORCA_STAGE_MAIN,
action: () => orcaSuccess()
});
const result = orca.validate();
expect(result.issues).toHaveLength(1);
expect(result.issues[0].event).toBe('event-a');
});
});

@ -40,7 +40,13 @@ import type {
ORCA_REENTRY_REASON_DEDUPED,
ORCA_REENTRY_REASON_TRACE_ABORTED,
ORCA_REENTRY_REASON_RUN_ABORTED,
ORCA_REENTRY_REASON_DISPOSED
ORCA_REENTRY_REASON_DISPOSED,
ORCA_VALIDATE_DEPENDENCY_CYCLE,
ORCA_VALIDATE_ORPHAN_ABORT_ON,
ORCA_VALIDATE_ORPHAN_UNLESS,
ORCA_VALIDATE_SEVERITY_ERROR,
ORCA_VALIDATE_SEVERITY_WARN,
ORCA_VALIDATE_UNSATISFIABLE_AFTER
} from './consts.ts';
// ── Identifiers ───────────────────────────────────────────────────────
@ -404,6 +410,41 @@ export interface OrcaRunResult {
readonly actions: readonly OrcaActionRun[];
}
// ── Validation (Orca.validate) ────────────────────────────────────────
export type OrcaValidateIssueKind =
| typeof ORCA_VALIDATE_UNSATISFIABLE_AFTER
| typeof ORCA_VALIDATE_ORPHAN_UNLESS
| typeof ORCA_VALIDATE_ORPHAN_ABORT_ON
| typeof ORCA_VALIDATE_DEPENDENCY_CYCLE;
export type OrcaValidateSeverity =
| typeof ORCA_VALIDATE_SEVERITY_ERROR
| typeof ORCA_VALIDATE_SEVERITY_WARN;
/**
* One configuration problem detected by `Orca.validate()`. Each issue
* carries enough machine-readable context (`event`, `actionId`,
* `token` or `cycle`) for a tool to navigate to the offending
* registration; `message` is for humans.
*/
export interface OrcaValidateIssue {
readonly kind: OrcaValidateIssueKind;
readonly severity: OrcaValidateSeverity;
readonly event: string;
readonly actionId?: OrcaActionId;
readonly token?: OrcaToken;
/** For dependency cycles: the action ids around the loop, in order. */
readonly cycle?: readonly OrcaActionId[];
readonly message: string;
}
export interface OrcaValidateResult {
/** True iff no issue has severity `'error'`. Warnings allow `ok: true`. */
readonly ok: boolean;
readonly issues: readonly OrcaValidateIssue[];
}
// ── Engine ────────────────────────────────────────────────────────────
/**
@ -475,6 +516,21 @@ export interface EngineOrca {
*/
recentRuns(): readonly OrcaRunResult[];
/**
* Static analysis of the registered actions. Detects:
* - `unsatisfiable-after` — an action's `after` token is not
* produced by any earlier action in canonical order.
* - `orphan-unless` / `orphan-abort-on` — gate tokens that no
* upstream action provides (warning).
* - `dependency-cycle` — actions block on each other through
* `after`/`provides`.
*
* Returns `{ ok, issues }`. Never throws. The engine does not gate
* future registrations or runs based on the result; the caller
* decides whether to treat issues as blockers.
*/
validate(): OrcaValidateResult;
/**
* True while the engine is currently executing a run.
*/

Loading…
Cancel
Save

Powered by TurnKey Linux.