@rulvar/plan 1.34.0 → 1.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -503,7 +503,16 @@ declare function rebasePlanRevision(request: PlanReviseRequest, context: RebaseC
503
503
  //#region src/guards.d.ts
504
504
  /** RevisionGuards configuration. */
505
505
  interface RevisionGuardsOptions {
506
- /** Default 'finish-with-partial'; the chain is non-HITL and terminating. */
506
+ /**
507
+ * Default 'finish-with-partial'; the chain is non-HITL and
508
+ * terminating. 'reject-revision' and 'finish-with-partial' freeze the
509
+ * plan and steer the orchestrator to finish with the partial result
510
+ * (run outcome 'ok'). 'fail-run' closes the run as a FAILURE: after
511
+ * the journaled guard verdict the PlanRunner terminates the
512
+ * orchestration with FailRunError (code 'fail_run', data.source
513
+ * 'plan_guards', data.verdictRef), no further model turn is consulted,
514
+ * and resume rolls the same failure forward from the verdict entry.
515
+ */
507
516
  fallback?: "reject-revision" | "finish-with-partial" | "fail-run";
508
517
  /** Default 3 consecutive fully-dropped revisions. */
509
518
  droppedRevisionLimit?: number;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { AdmissionRejectedError, BudgetExhaustedError, CURRENT_HASH_VERSION, ConfigError, DedupIndex, InMemoryStore, LEGACY_LTID_PREFIX, LEGACY_SIGNATURE_INPUTS, LineageIndex, PlanInvariantError, ROOT_ACCOUNT, ReplayPlanHashMismatch, TerminationAccount, approachSigCoarse, buildTerminationInitValue, canonicalIsolationTag, canonicalizeLadder, checkpointRefFor, countsAgainstLimit, createEngine, defineWorkflow, deriverV2, evaluateReuse, foldTermination, kMaxOf, knowledgeHash, ladderLengthOf, ladderRungChoice, makeOrchestratorWorkflow, nodeLinkKey, normalizeApproachTag, orchestrate, planNodeScope, profileRegistrySnapshotHash, tool, validateTerminationLimits } from "@rulvar/core";
1
+ import { AdmissionRejectedError, BudgetExhaustedError, CURRENT_HASH_VERSION, ConfigError, DedupIndex, FailRunError, InMemoryStore, LEGACY_LTID_PREFIX, LEGACY_SIGNATURE_INPUTS, LineageIndex, PlanInvariantError, ROOT_ACCOUNT, ReplayPlanHashMismatch, TerminationAccount, approachSigCoarse, buildTerminationInitValue, canonicalIsolationTag, canonicalizeLadder, checkpointRefFor, countsAgainstLimit, createEngine, defineWorkflow, deriverV2, evaluateReuse, foldTermination, kMaxOf, knowledgeHash, ladderLengthOf, ladderRungChoice, makeOrchestratorWorkflow, nodeLinkKey, normalizeApproachTag, orchestrate, planNodeScope, profileRegistrySnapshotHash, tool, validateTerminationLimits } from "@rulvar/core";
2
2
  //#region src/plan-state.ts
3
3
  /**
4
4
  * Plan scope substrate (M7-T01): TaskPlan as engine-owned typed data, the
@@ -923,6 +923,49 @@ function cascadeOf(plan, root) {
923
923
  }
924
924
  //#endregion
925
925
  //#region src/guards.ts
926
+ /**
927
+ * RevisionGuards, the oscillation detector, and hysteresis (M7-T06).
928
+ *
929
+ * Guide: https://docs.rulvar.com/guide/adaptive-orchestration
930
+ * (DEF-8; DEF-2/DEF-5 interactions). The guards are non-HITL and
931
+ * terminating: a human in the loop is NEVER required for a run to end.
932
+ * Every guard verdict is a decision entry written strictly BEFORE its
933
+ * effects; verdict state is a pure fold of those entries, so replay
934
+ * re-derives the same freezes with zero live calls.
935
+ *
936
+ * Three detectors:
937
+ *
938
+ * - droppedRevisionStreak: `effectiveDroppedStreak` (the hashed counter
939
+ * plus trailing bad_base entries) reaching `droppedRevisionLimit`
940
+ * (default 3) fires the configured terminating fallback
941
+ * (reject-revision -> finish-with-partial -> fail-run; default
942
+ * finish-with-partial).
943
+ * - Oscillation: keyed on approachSigCoarse ACROSS LogicalTaskId
944
+ * boundaries (a content-identical rebirth under a fresh lineage root
945
+ * is still flagged): a re-add after a severing cancel of the same
946
+ * coarse signature counts one oscillation; at the per-key limit
947
+ * further re-adds FREEZE (the per-SpawnKey osc_guard of DEF-5 lands
948
+ * in M7-T07 and keys the DedupIndex instead).
949
+ * - Stall replans: hard-bounded per run; the streak already excludes
950
+ * transient and environment classes.
951
+ *
952
+ * Hysteresis on almost-done nodes is structural: park and cancel against
953
+ * RUNNING nodes only ever land as boundary flags (requestOnly), so a
954
+ * nearly-finished child is never killed mid-turn, and park/unpark churn
955
+ * feeds the oscillation counter.
956
+ */
957
+ /**
958
+ * Local mirror of the core numeric-intake idiom (v1.34.0 review P2-3):
959
+ * guard limits are compared with `<` and `!==`, and every comparison
960
+ * with NaN is false, so an unvalidated NaN limit made the dropped and
961
+ * oscillation guards trip immediately and the stall cap never trip.
962
+ */
963
+ function requireCount(value, site, min) {
964
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min) throw new ConfigError(`${site} must be ${min === 1 ? "a positive integer" : "a nonnegative integer"}; got ${String(value)}`);
965
+ }
966
+ function requireGuardFraction(value, site) {
967
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1) throw new ConfigError(`${site} must be a fraction in (0, 1]; got ${String(value)}`);
968
+ }
926
969
  /** Appendix A: osc_guard reject threshold per key (shared default). */
927
970
  const DEFAULT_MAX_OSCILLATIONS_PER_KEY = 2;
928
971
  /** The hard per-run stall replan bound. */
@@ -947,6 +990,10 @@ var RevisionGuards = class {
947
990
  frozen = /* @__PURE__ */ new Set();
948
991
  stallReplans = 0;
949
992
  constructor(options) {
993
+ if (options?.droppedRevisionLimit !== void 0) requireCount(options.droppedRevisionLimit, "RevisionGuards droppedRevisionLimit", 1);
994
+ if (options?.maxOscillationsPerKey !== void 0) requireCount(options.maxOscillationsPerKey, "RevisionGuards maxOscillationsPerKey", 1);
995
+ if (options?.stallReplanCap !== void 0) requireCount(options.stallReplanCap, "RevisionGuards stallReplanCap", 0);
996
+ if (options?.maxAbandonedNetUsdFraction !== void 0) requireGuardFraction(options.maxAbandonedNetUsdFraction, "RevisionGuards maxAbandonedNetUsdFraction");
950
997
  this.fallback = options?.fallback ?? "finish-with-partial";
951
998
  this.droppedRevisionLimit = options?.droppedRevisionLimit ?? 3;
952
999
  this.maxOscillationsPerKey = options?.maxOscillationsPerKey ?? 2;
@@ -1982,6 +2029,26 @@ function planRunner(options) {
1982
2029
  guard: verdict.guard,
1983
2030
  ...verdict.approachSigCoarse === void 0 ? {} : { approachSigCoarse: verdict.approachSigCoarse }
1984
2031
  });
2032
+ /**
2033
+ * The engaged 'fail-run' fallback closes the run as a typed failure
2034
+ * (v1.35.0 review P2-1: the verdict used to journal the fallback and
2035
+ * then behave exactly like finish-with-partial). The journaled verdict
2036
+ * is the decision; terminate() aborts the loop and the orchestrate
2037
+ * settle boundary rethrows the failure, so a crash after the verdict
2038
+ * rolls the SAME outcome forward from the journal on resume, with no
2039
+ * second decision and no model call.
2040
+ */
2041
+ let failRunSignalled = false;
2042
+ const enforceEngagedFailRun = () => {
2043
+ if (failRunSignalled || guards.state.engaged !== "fail-run") return;
2044
+ failRunSignalled = true;
2045
+ const verdictEntry = io.snapshot().find((entry) => entry.scope === planScope && entry.kind === "decision" && entry.value?.decisionType === "guard-verdict" && entry.value.fallback === "fail-run");
2046
+ const guardName = (verdictEntry?.value)?.guard;
2047
+ io.terminate?.(new FailRunError(`revision guards engaged (${guardName ?? "guard"}) with fallback 'fail-run': the plan is closed for adaptation and the run fails instead of finishing with the partial result`, { data: {
2048
+ source: "plan_guards",
2049
+ ...verdictEntry === void 0 ? {} : { verdictRef: verdictEntry.seq }
2050
+ } }));
2051
+ };
1985
2052
  /** Guard verdict state is a pure fold of journaled verdicts (M7-T06). */
1986
2053
  const absorbGuardVerdicts = () => {
1987
2054
  for (const entry of io.snapshot()) {
@@ -1993,6 +2060,7 @@ function planRunner(options) {
1993
2060
  }
1994
2061
  guardCursor = entry.seq;
1995
2062
  }
2063
+ enforceEngagedFailRun();
1996
2064
  };
1997
2065
  /**
1998
2066
  * Appends fold-fired verdicts strictly BEFORE their effects; a
@@ -2018,6 +2086,7 @@ function planRunner(options) {
2018
2086
  limit: verdict.oscillationCount ?? 0
2019
2087
  });
2020
2088
  }
2089
+ enforceEngagedFailRun();
2021
2090
  };
2022
2091
  /** The coarse approach signature of a spec (mirrors admission's inputs). */
2023
2092
  const coarseOf = (spec) => approachSigCoarse({
@@ -3293,7 +3362,13 @@ function planRunner(options) {
3293
3362
  await io.flush();
3294
3363
  absorbPlan();
3295
3364
  absorbGuardVerdicts();
3296
- if (guards.revisionsRejected) throw new ConfigError(`revision guards engaged (${guards.state.engaged ?? "unknown"}): the plan is closed for adaptation; call finish with the partial result`);
3365
+ if (guards.revisionsRejected) {
3366
+ if (guards.state.engaged === "fail-run") {
3367
+ enforceEngagedFailRun();
3368
+ throw new FailRunError("revision guards engaged with fallback 'fail-run': the plan is closed and the run fails instead of finishing with the partial result", { data: { source: "plan_guards" } });
3369
+ }
3370
+ throw new ConfigError(`revision guards engaged (${guards.state.engaged ?? "unknown"}): the plan is closed for adaptation; call finish with the partial result`);
3371
+ }
3297
3372
  if (vocabulary !== void 0) {
3298
3373
  for (const op of request.ops) if (op.op === "add_task") {
3299
3374
  const tag = op.approach ?? op.spec.approach;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/plan",
3
- "version": "1.34.0",
3
+ "version": "1.36.0",
4
4
  "description": "Rulvar adaptive orchestration extension: PlanRunner, RunLedger, escalation extensions, ModelLadder configuration.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -22,13 +22,13 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@rulvar/core": "1.34.0"
25
+ "@rulvar/core": "1.36.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.20.0",
29
29
  "tsdown": "^0.22.3",
30
30
  "typescript": "~6.0.3",
31
- "@rulvar/store-sqlite": "1.34.0"
31
+ "@rulvar/store-sqlite": "1.36.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",