@tacuchi/agent-workflow-cli 25.6.1 → 25.7.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/CHANGELOG.md +4368 -0
- package/README.md +1 -1
- package/dist/adapters/git-cli.js +74 -6
- package/dist/adapters/git-cli.js.map +1 -1
- package/dist/adapters/node-file-system.js +39 -2
- package/dist/adapters/node-file-system.js.map +1 -1
- package/dist/application/branch-resolver.js +34 -1
- package/dist/application/branch-resolver.js.map +1 -1
- package/dist/application/checkpoint-write-service.js +161 -39
- package/dist/application/checkpoint-write-service.js.map +1 -1
- package/dist/application/dates.js +12 -0
- package/dist/application/dates.js.map +1 -1
- package/dist/application/fix-git-service.js +83 -13
- package/dist/application/fix-git-service.js.map +1 -1
- package/dist/application/flow/advance.js +24 -3
- package/dist/application/flow/advance.js.map +1 -1
- package/dist/application/flow/annul-service.js +159 -0
- package/dist/application/flow/annul-service.js.map +1 -0
- package/dist/application/flow/flow-service.js +212 -49
- package/dist/application/flow/flow-service.js.map +1 -1
- package/dist/application/flow/internal-actions.js +23 -3
- package/dist/application/flow/internal-actions.js.map +1 -1
- package/dist/application/flow/internal-drive.js +51 -19
- package/dist/application/flow/internal-drive.js.map +1 -1
- package/dist/application/flow/run-projection.js +21 -5
- package/dist/application/flow/run-projection.js.map +1 -1
- package/dist/application/flow/run-state-service.js +108 -35
- package/dist/application/flow/run-state-service.js.map +1 -1
- package/dist/application/flow/submit.js +63 -144
- package/dist/application/flow/submit.js.map +1 -1
- package/dist/application/git-flow-service.js +325 -72
- package/dist/application/git-flow-service.js.map +1 -1
- package/dist/application/hook-sql-mutation-guard.js +155 -27
- package/dist/application/hook-sql-mutation-guard.js.map +1 -1
- package/dist/application/host-memory/claude-code.js +65 -0
- package/dist/application/host-memory/claude-code.js.map +1 -0
- package/dist/application/host-memory/codex.js +225 -0
- package/dist/application/host-memory/codex.js.map +1 -0
- package/dist/application/host-memory/destination.js +52 -0
- package/dist/application/host-memory/destination.js.map +1 -0
- package/dist/application/host-memory/reader.js +9 -0
- package/dist/application/host-memory/reader.js.map +1 -0
- package/dist/application/host-memory/report.js +136 -0
- package/dist/application/host-memory/report.js.map +1 -0
- package/dist/application/lifecycle-target.js +3 -1
- package/dist/application/lifecycle-target.js.map +1 -1
- package/dist/application/local-proposal.js +13 -3
- package/dist/application/local-proposal.js.map +1 -1
- package/dist/application/logging/log-events.js +22 -0
- package/dist/application/logging/log-events.js.map +1 -1
- package/dist/application/parsers/spec-relation.js +20 -7
- package/dist/application/parsers/spec-relation.js.map +1 -1
- package/dist/application/persist-service.js +45 -1
- package/dist/application/persist-service.js.map +1 -1
- package/dist/application/plan-exec-batch-service.js +47 -2
- package/dist/application/plan-exec-batch-service.js.map +1 -1
- package/dist/application/plan-lineage-seal.js +93 -0
- package/dist/application/plan-lineage-seal.js.map +1 -0
- package/dist/application/plan-lint-service.js +153 -0
- package/dist/application/plan-lint-service.js.map +1 -0
- package/dist/application/prod-consent.js +75 -0
- package/dist/application/prod-consent.js.map +1 -0
- package/dist/application/reseal-service.js +9 -3
- package/dist/application/reseal-service.js.map +1 -1
- package/dist/application/self/install-skill.js +5 -1
- package/dist/application/self/install-skill.js.map +1 -1
- package/dist/application/self/update-self.js +204 -8
- package/dist/application/self/update-self.js.map +1 -1
- package/dist/application/session-create-service.js +49 -0
- package/dist/application/session-create-service.js.map +1 -1
- package/dist/application/session-custody-recorder.js +4 -0
- package/dist/application/session-custody-recorder.js.map +1 -1
- package/dist/application/session-narrative.js +27 -0
- package/dist/application/session-narrative.js.map +1 -1
- package/dist/application/session-resolver.js +28 -9
- package/dist/application/session-resolver.js.map +1 -1
- package/dist/application/source-boundary-policy.js +168 -34
- package/dist/application/source-boundary-policy.js.map +1 -1
- package/dist/application/working-branch-service.js +68 -0
- package/dist/application/working-branch-service.js.map +1 -0
- package/dist/application/workline-graph.js +7 -2
- package/dist/application/workline-graph.js.map +1 -1
- package/dist/cli/commands/checkpoint-write.js +18 -8
- package/dist/cli/commands/checkpoint-write.js.map +1 -1
- package/dist/cli/commands/fix-git.js +45 -6
- package/dist/cli/commands/fix-git.js.map +1 -1
- package/dist/cli/commands/flow.js +50 -5
- package/dist/cli/commands/flow.js.map +1 -1
- package/dist/cli/commands/git-flow.js +103 -29
- package/dist/cli/commands/git-flow.js.map +1 -1
- package/dist/cli/commands/host-memory.js +33 -0
- package/dist/cli/commands/host-memory.js.map +1 -0
- package/dist/cli/commands/index.js +6 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/persist.js +5 -0
- package/dist/cli/commands/persist.js.map +1 -1
- package/dist/cli/commands/plan.js +50 -0
- package/dist/cli/commands/plan.js.map +1 -0
- package/dist/cli/commands/resume-summary.js +4 -2
- package/dist/cli/commands/resume-summary.js.map +1 -1
- package/dist/cli/commands/set-branch.js +40 -6
- package/dist/cli/commands/set-branch.js.map +1 -1
- package/dist/cli/help-groups.js +6 -0
- package/dist/cli/help-groups.js.map +1 -1
- package/dist/cli/main.js +2 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui/components/git-flow-actions.js +65 -20
- package/dist/cli/tui/components/git-flow-actions.js.map +1 -1
- package/dist/cli/tui/tabs/project-tab.js +101 -12
- package/dist/cli/tui/tabs/project-tab.js.map +1 -1
- package/dist/domain/changelog-contract.js +172 -0
- package/dist/domain/changelog-contract.js.map +1 -0
- package/dist/domain/database-tools.js +1 -189
- package/dist/domain/database-tools.js.map +1 -1
- package/dist/domain/flow/answer.js +3 -0
- package/dist/domain/flow/answer.js.map +1 -1
- package/dist/domain/flow/authority.js +12 -0
- package/dist/domain/flow/authority.js.map +1 -1
- package/dist/domain/flow/journey-baseline.js +131 -0
- package/dist/domain/flow/journey-baseline.js.map +1 -0
- package/dist/domain/flow/run-state.js +257 -62
- package/dist/domain/flow/run-state.js.map +1 -1
- package/dist/domain/host-memory/model.js +2 -0
- package/dist/domain/host-memory/model.js.map +1 -0
- package/dist/domain/host-memory/provenance.js +43 -0
- package/dist/domain/host-memory/provenance.js.map +1 -0
- package/dist/domain/host-memory/topic.js +26 -0
- package/dist/domain/host-memory/topic.js.map +1 -0
- package/dist/domain/lineage.js +11 -3
- package/dist/domain/lineage.js.map +1 -1
- package/dist/domain/proposal.js +7 -0
- package/dist/domain/proposal.js.map +1 -1
- package/dist/domain/session/custody.js.map +1 -1
- package/dist/domain/sql-lexer.js +227 -0
- package/dist/domain/sql-lexer.js.map +1 -0
- package/package.json +2 -2
- package/skills/w/SKILL.md +9 -9
- package/skills/w/commands/README.md +2 -1
- package/skills/w/commands/recall.md +39 -0
- package/skills/w/context/MANIFEST.json +4 -0
- package/skills/w/harness/HARNESS.md +9 -5
- package/skills/w/hooks/README.md +27 -16
- package/skills/w/loops/CHASSIS.md +16 -12
- package/skills/w/loops/CODE-POLICIES.md +11 -6
- package/skills/w/loops/plan-exec-loop/LOOP.md +9 -9
- package/skills/w/modules/COMPACTION.md +1 -1
|
@@ -26,6 +26,8 @@ import { canonicalJson, semanticDigest } from "../../application/semantic-operat
|
|
|
26
26
|
import { isEffectClass, touchesTheWorld } from "../capability/effects.js";
|
|
27
27
|
import { AttemptLedger } from "../capability/protocol.js";
|
|
28
28
|
import { normalizeObligations, validateDecisionNote } from "../decision-note.js";
|
|
29
|
+
import { journeyOfFlow } from "./authority.js";
|
|
30
|
+
import { V11_JOURNEY_BASE } from "./journey-baseline.js";
|
|
29
31
|
import { assuranceForRoute, isAssuranceStatus, isRouteDisposition, } from "./route.js";
|
|
30
32
|
/**
|
|
31
33
|
* The version this CLI WRITES. Every state it persists carries it.
|
|
@@ -38,17 +40,42 @@ import { assuranceForRoute, isAssuranceStatus, isRouteDisposition, } from "./rou
|
|
|
38
40
|
* turning the cap off in silence while somebody alternates CLI versions over one
|
|
39
41
|
* run. Failing with a cause is the requirement; failing silently is the defect.
|
|
40
42
|
*/
|
|
41
|
-
export const FLOW_RUN_STATE_VERSION =
|
|
43
|
+
export const FLOW_RUN_STATE_VERSION = 12;
|
|
44
|
+
/**
|
|
45
|
+
* The versions this CLI CONTINUES without adoption, newest first.
|
|
46
|
+
*
|
|
47
|
+
* v12 added the `aligned`, `restarted` and `annulled` trace kinds and `journey_base`, whose truthful value
|
|
48
|
+
* for a v11 run is the journey 25.6.1 walked. So a v11 run keeps walking after
|
|
49
|
+
* an upgrade: {@link upgradeContinuable} re-stamps it without touching the
|
|
50
|
+
* cursor, the answers or the attempts. Every later version joins this set with its own
|
|
51
|
+
* upgrade step, which is what keeps "v11 or later survives an update" true
|
|
52
|
+
* without an exception per release.
|
|
53
|
+
*/
|
|
54
|
+
export const FLOW_RUN_STATE_CONTINUABLE = [FLOW_RUN_STATE_VERSION, 11];
|
|
42
55
|
/**
|
|
43
56
|
* The versions this CLI READS, newest first.
|
|
44
57
|
*
|
|
45
|
-
* Versions
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
|
|
58
|
+
* Versions 10 to 7 remain readable for status and recovery evidence only. They
|
|
59
|
+
* are NOT continuable execution state: continuing an old cursor by merely
|
|
60
|
+
* changing `version` would invent a batch boundary the old run never declared,
|
|
61
|
+
* so their way out is `aw flow restart`.
|
|
62
|
+
*/
|
|
63
|
+
export const FLOW_RUN_STATE_READABLE = [
|
|
64
|
+
...FLOW_RUN_STATE_CONTINUABLE,
|
|
65
|
+
10,
|
|
66
|
+
9,
|
|
67
|
+
8,
|
|
68
|
+
7,
|
|
69
|
+
];
|
|
70
|
+
/**
|
|
71
|
+
* One upgrade step per continuable version, keyed by the version it upgrades FROM.
|
|
72
|
+
*
|
|
73
|
+
* A step may only add what the next version introduced with a truthful default;
|
|
74
|
+
* the cursor, the answers, the approvals and the attempts pass through untouched.
|
|
50
75
|
*/
|
|
51
|
-
|
|
76
|
+
const CONTINUABLE_UPGRADES = {
|
|
77
|
+
11: (state) => ({ ...state, version: 12, journey_base: [...V11_JOURNEY_BASE[state.flow]] }),
|
|
78
|
+
};
|
|
52
79
|
/** The CLI-owned run state inside the session folder. Machine-local, dotted. */
|
|
53
80
|
export const FLOW_RUN_STATE_FILE = ".flow-run.json";
|
|
54
81
|
/**
|
|
@@ -348,8 +375,12 @@ export function recoveryBlockedAt(state, transition) {
|
|
|
348
375
|
function declaresMaterialEffect(event) {
|
|
349
376
|
// A reconciliation moved the run's own counters and nothing else: it can no
|
|
350
377
|
// more block a recovery than reading the file could.
|
|
351
|
-
if (event.kind === "reconciled"
|
|
378
|
+
if (event.kind === "reconciled" ||
|
|
379
|
+
event.kind === "aligned" ||
|
|
380
|
+
event.kind === "restarted" ||
|
|
381
|
+
event.kind === "annulled") {
|
|
352
382
|
return false;
|
|
383
|
+
}
|
|
353
384
|
if (event.kind !== "failed")
|
|
354
385
|
return touchesTheWorld(event.effects);
|
|
355
386
|
if (event.effects === undefined)
|
|
@@ -405,6 +436,7 @@ export function newRunState(flow, session) {
|
|
|
405
436
|
flow,
|
|
406
437
|
session,
|
|
407
438
|
scope: null,
|
|
439
|
+
journey_base: journeyOfFlow(flow).map((decision) => decision.id),
|
|
408
440
|
applied: [],
|
|
409
441
|
skipped: [],
|
|
410
442
|
boundary: null,
|
|
@@ -525,32 +557,43 @@ export function grantAttempts(state, transition, attempts) {
|
|
|
525
557
|
granted[key] = (granted[key] ?? 0) + attempts;
|
|
526
558
|
return sealRunState({ ...withoutSeal(state), attempt_grants: granted });
|
|
527
559
|
}
|
|
560
|
+
/** Whether this exhausted boundary already spent the one re-run the CLI gives it. */
|
|
561
|
+
export function exhaustedRerunSpent(state, transition) {
|
|
562
|
+
return (state.exhausted_reruns ?? []).includes(attemptCounterKey(state, transition));
|
|
563
|
+
}
|
|
564
|
+
/** Record that the CLI's one re-run of this exhausted boundary came back refused. */
|
|
565
|
+
export function withExhaustedRerunSpent(state, transition) {
|
|
566
|
+
if (exhaustedRerunSpent(state, transition))
|
|
567
|
+
return state;
|
|
568
|
+
return sealRunState({
|
|
569
|
+
...withoutSeal(state),
|
|
570
|
+
exhausted_reruns: [...(state.exhausted_reruns ?? []), attemptCounterKey(state, transition)],
|
|
571
|
+
});
|
|
572
|
+
}
|
|
528
573
|
/**
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
* This function is intentionally NOT called by the reader. Calling it is the
|
|
532
|
-
* durable act of adoption after the caller has named the flow and accepted that
|
|
533
|
-
* no pre-v10 batch is being reconstructed. Empty batch arrays mean exactly
|
|
534
|
-
* "legacy history preserved; no v10 batch claimed", never "the old run had no
|
|
535
|
-
* batches".
|
|
574
|
+
* Forget a spent re-run: `aw flow recover` gives the boundary a fresh budget,
|
|
575
|
+
* and a later exhaustion is a new episode that is owed its own re-run.
|
|
536
576
|
*/
|
|
537
|
-
export function
|
|
538
|
-
if (state
|
|
577
|
+
export function withoutExhaustedRerun(state, transition) {
|
|
578
|
+
if (!exhaustedRerunSpent(state, transition))
|
|
539
579
|
return state;
|
|
580
|
+
const key = attemptCounterKey(state, transition);
|
|
540
581
|
return sealRunState({
|
|
541
582
|
...withoutSeal(state),
|
|
542
|
-
|
|
543
|
-
batches: [],
|
|
544
|
-
batch_trace: [],
|
|
545
|
-
batch_loop: { pending: true, iteration: 1 },
|
|
546
|
-
handoff: null,
|
|
547
|
-
selected_choice: null,
|
|
548
|
-
decision_preparation: null,
|
|
549
|
-
route_proposal: null,
|
|
550
|
-
route_decisions: null,
|
|
551
|
-
assurance: "verified",
|
|
583
|
+
exhausted_reruns: (state.exhausted_reruns ?? []).filter((spent) => spent !== key),
|
|
552
584
|
});
|
|
553
585
|
}
|
|
586
|
+
/**
|
|
587
|
+
* The one exit from a run the CLI cannot continue: archive it inside the session
|
|
588
|
+
* and adopt a new one. Named in every refusal whose state it resolves.
|
|
589
|
+
*/
|
|
590
|
+
export function restartInvocation(session) {
|
|
591
|
+
return `aw flow restart --session ${session}`;
|
|
592
|
+
}
|
|
593
|
+
/** The action of every refusal over a registry this CLI cannot use as it is. */
|
|
594
|
+
function unusableRemedy(session) {
|
|
595
|
+
return `corré '${restartInvocation(session)}': archiva el registro dentro de la sesión, re-adopta su flow y lo deja en la traza — o restaurá una copia íntegra del archivo`;
|
|
596
|
+
}
|
|
554
597
|
/** Keep the sealed preview the person is choosing over; no cursor moves here. */
|
|
555
598
|
export function withRouteProposal(state, proposal) {
|
|
556
599
|
return sealRunState({
|
|
@@ -568,9 +611,28 @@ export function withRouteDecisions(state, decisions) {
|
|
|
568
611
|
assurance: assuranceForRoute(decisions, state.applied),
|
|
569
612
|
});
|
|
570
613
|
}
|
|
571
|
-
/** Whether a parsed state
|
|
614
|
+
/** Whether a parsed state is outside the continuable set and cannot be mutated. */
|
|
572
615
|
export function legacyRunNeedsAdoption(state) {
|
|
573
|
-
return state.version
|
|
616
|
+
return !FLOW_RUN_STATE_CONTINUABLE.includes(state.version);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Walk a continuable state up to the version this CLI writes, one step at a time.
|
|
620
|
+
*
|
|
621
|
+
* Re-sealed once at the end: the file on disk was already verified against its
|
|
622
|
+
* own seal, and the upgraded state is persisted by the next write, never here.
|
|
623
|
+
* A legacy state comes back unchanged — reading one must never re-stamp it.
|
|
624
|
+
*/
|
|
625
|
+
export function upgradeContinuable(state) {
|
|
626
|
+
if (legacyRunNeedsAdoption(state) || state.version === FLOW_RUN_STATE_VERSION)
|
|
627
|
+
return state;
|
|
628
|
+
let current = state;
|
|
629
|
+
while (current.version !== FLOW_RUN_STATE_VERSION) {
|
|
630
|
+
const step = CONTINUABLE_UPGRADES[current.version];
|
|
631
|
+
if (step === undefined)
|
|
632
|
+
return state;
|
|
633
|
+
current = step(current);
|
|
634
|
+
}
|
|
635
|
+
return sealRunState(withoutSeal(current));
|
|
574
636
|
}
|
|
575
637
|
/** Record one inferred or declared batch, refusing duplicate ids at the caller's boundary. */
|
|
576
638
|
export function withPlanExecBatch(state, batch) {
|
|
@@ -1021,20 +1083,26 @@ function union(current, added) {
|
|
|
1021
1083
|
*
|
|
1022
1084
|
* Order matters: shape before version before seal. A file whose version is
|
|
1023
1085
|
* unknown must not be judged by a digest rule that version may not even use.
|
|
1086
|
+
*
|
|
1087
|
+
* Every refusal names the command that works IN that state: `--adopt` only adopts
|
|
1088
|
+
* a session with no registry at all, so an unreadable, tampered or too-old one
|
|
1089
|
+
* leaves through `aw flow restart`, and one written by a newer CLI through
|
|
1090
|
+
* updating this one. `session` names the run in those commands.
|
|
1024
1091
|
*/
|
|
1025
|
-
export function parseRunState(raw) {
|
|
1092
|
+
export function parseRunState(raw, session = "<código>") {
|
|
1093
|
+
const restart = unusableRemedy(session);
|
|
1026
1094
|
if (raw.trim().length === 0) {
|
|
1027
|
-
return refuse("FLOW_RUN_INVALID", "el estado de corrida está vacío",
|
|
1095
|
+
return refuse("FLOW_RUN_INVALID", "el estado de corrida está vacío", restart);
|
|
1028
1096
|
}
|
|
1029
1097
|
let parsed;
|
|
1030
1098
|
try {
|
|
1031
1099
|
parsed = JSON.parse(raw);
|
|
1032
1100
|
}
|
|
1033
1101
|
catch {
|
|
1034
|
-
return refuse("FLOW_RUN_INVALID", "el estado de corrida no es JSON válido",
|
|
1102
|
+
return refuse("FLOW_RUN_INVALID", "el estado de corrida no es JSON válido", restart);
|
|
1035
1103
|
}
|
|
1036
1104
|
if (!isRecord(parsed)) {
|
|
1037
|
-
return refuse("FLOW_RUN_INVALID", "el estado de corrida no es un objeto JSON",
|
|
1105
|
+
return refuse("FLOW_RUN_INVALID", "el estado de corrida no es un objeto JSON", restart);
|
|
1038
1106
|
}
|
|
1039
1107
|
if (!FLOW_RUN_STATE_READABLE.includes(parsed.version)) {
|
|
1040
1108
|
// No silent migration, in either direction: a version outside the readable
|
|
@@ -1042,14 +1110,17 @@ export function parseRunState(raw) {
|
|
|
1042
1110
|
// fabricate the very history this file exists to make trustworthy — or comes
|
|
1043
1111
|
// from a build ahead of this one, whose fields this build would ignore.
|
|
1044
1112
|
// Refusing with the cause is what keeps the second case from being silent.
|
|
1045
|
-
|
|
1113
|
+
const newer = typeof parsed.version === "number" && parsed.version > FLOW_RUN_STATE_VERSION;
|
|
1114
|
+
return refuse("FLOW_RUN_VERSION_UNSUPPORTED", `versión de estado de corrida no soportada: ${String(parsed.version)} (este CLI lee de la ${FLOW_RUN_STATE_READABLE.at(-1)} a la ${FLOW_RUN_STATE_VERSION})`, newer
|
|
1115
|
+
? `la escribió un CLI más nuevo que este: actualizalo con 'aw self update' y seguí la corrida — reiniciarla descartaría lo que esa versión sí continúa`
|
|
1116
|
+
: restart);
|
|
1046
1117
|
}
|
|
1047
|
-
const shape = checkShape(parsed);
|
|
1118
|
+
const shape = checkShape(parsed, restart);
|
|
1048
1119
|
if (shape !== null)
|
|
1049
1120
|
return { ok: false, failure: shape };
|
|
1050
1121
|
const state = parsed;
|
|
1051
1122
|
if (semanticDigest(withoutSeal(state)) !== state.digest) {
|
|
1052
|
-
return refuse("FLOW_RUN_TAMPERED", "el estado de corrida no coincide con su propio sello
|
|
1123
|
+
return refuse("FLOW_RUN_TAMPERED", "el estado de corrida no coincide con su propio sello: fue editado fuera del CLI", restart);
|
|
1053
1124
|
}
|
|
1054
1125
|
return { ok: true, state };
|
|
1055
1126
|
}
|
|
@@ -1069,10 +1140,15 @@ export function checkAgainstJourney(state, journey) {
|
|
|
1069
1140
|
for (const [index, applied] of state.applied.entries()) {
|
|
1070
1141
|
if (ids[index] === applied)
|
|
1071
1142
|
continue;
|
|
1143
|
+
// The cause decides the repair, so it is named: a row this build no longer
|
|
1144
|
+
// has was renamed or retired, one it has elsewhere was reordered.
|
|
1145
|
+
const cause = ids.includes(applied)
|
|
1146
|
+
? "el recorrido instalado la tiene en otra posición"
|
|
1147
|
+
: "el recorrido instalado ya no tiene esa frontera";
|
|
1072
1148
|
return {
|
|
1073
1149
|
code: "FLOW_RUN_AHEAD_OF_JOURNEY",
|
|
1074
|
-
message: `el estado dice haber aplicado '${applied}' donde el recorrido tiene '${ids[index] ?? "(nada)"}'`,
|
|
1075
|
-
action:
|
|
1150
|
+
message: `el estado dice haber aplicado '${applied}' donde el recorrido tiene '${ids[index] ?? "(nada)"}': ${cause}`,
|
|
1151
|
+
action: unusableRemedy(state.session),
|
|
1076
1152
|
};
|
|
1077
1153
|
}
|
|
1078
1154
|
const next = ids[state.applied.length] ?? null;
|
|
@@ -1080,16 +1156,111 @@ export function checkAgainstJourney(state, journey) {
|
|
|
1080
1156
|
return {
|
|
1081
1157
|
code: "FLOW_RUN_AHEAD_OF_JOURNEY",
|
|
1082
1158
|
message: `la frontera vigente dice '${state.boundary}' y el recorrido sigue en '${next ?? "(nada)"}'`,
|
|
1083
|
-
action:
|
|
1159
|
+
action: unusableRemedy(state.session),
|
|
1084
1160
|
};
|
|
1085
1161
|
}
|
|
1086
1162
|
return null;
|
|
1087
1163
|
}
|
|
1088
|
-
|
|
1164
|
+
/** The trace operation that names an alignment, so status and tests read one spelling. */
|
|
1165
|
+
export const JOURNEY_ALIGNMENT_OPERATION = "flow.journey-alignment";
|
|
1166
|
+
const ALIGNMENT_REASON = "la versión instalada agregó esta frontera en un tramo que la corrida ya había recorrido: entra omitida, sin pedirse";
|
|
1167
|
+
/**
|
|
1168
|
+
* Align a continuable run's cursor with the journey the installed build walks.
|
|
1169
|
+
*
|
|
1170
|
+
* The only difference this repairs is a boundary the installed build ADDED to
|
|
1171
|
+
* the base journey the run recorded (`journey_base`) and that sits before the
|
|
1172
|
+
* run's position. "Before the position" includes the rows ahead of the boundary
|
|
1173
|
+
* the run is standing on: that boundary is the one it was answering, and a
|
|
1174
|
+
* sealed or attempted action there must not be thrown away for a row nobody
|
|
1175
|
+
* asked it about. A row added AFTER the boundary is left alone and asked when
|
|
1176
|
+
* the run gets there.
|
|
1177
|
+
*
|
|
1178
|
+
* Every other difference comes back untouched, for {@link checkAgainstJourney}
|
|
1179
|
+
* to refuse with its cause: a row the recorded base already had and the cursor
|
|
1180
|
+
* lacks is a hole, not an addition; a reorder or a retirement moves an id that
|
|
1181
|
+
* keys attempts, counters, trace and approvals.
|
|
1182
|
+
*
|
|
1183
|
+
* `base` is the installed REGISTRY journey; `journey` is what the run walks
|
|
1184
|
+
* (plan-exec's expanded copies included). Pure: the aligned state is persisted
|
|
1185
|
+
* by the run's next write, never by a read.
|
|
1186
|
+
*/
|
|
1187
|
+
export function alignToJourney(state, journey, base) {
|
|
1188
|
+
const recorded = state.journey_base;
|
|
1189
|
+
if (recorded === undefined || sameIds(recorded, base))
|
|
1190
|
+
return state;
|
|
1191
|
+
const addedIds = new Set(base.filter((id) => !recorded.includes(id)));
|
|
1192
|
+
const ids = journey.map((decision) => decision.id);
|
|
1193
|
+
const applied = [];
|
|
1194
|
+
const added = [];
|
|
1195
|
+
let cursor = 0;
|
|
1196
|
+
// Everything from `cursor` up to the next occurrence of `target` must be rows
|
|
1197
|
+
// the installed build added; only then are they inserted, in journey order.
|
|
1198
|
+
const insertUntil = (target) => {
|
|
1199
|
+
let next = cursor;
|
|
1200
|
+
while (next < ids.length && ids[next] !== target) {
|
|
1201
|
+
if (!addedIds.has(ids[next]))
|
|
1202
|
+
return false;
|
|
1203
|
+
next += 1;
|
|
1204
|
+
}
|
|
1205
|
+
if (next >= ids.length)
|
|
1206
|
+
return false;
|
|
1207
|
+
for (; cursor < next; cursor += 1) {
|
|
1208
|
+
added.push({ transition: ids[cursor], position: applied.length });
|
|
1209
|
+
applied.push(ids[cursor]);
|
|
1210
|
+
}
|
|
1211
|
+
return true;
|
|
1212
|
+
};
|
|
1213
|
+
for (const id of state.applied) {
|
|
1214
|
+
if (ids[cursor] !== id && !insertUntil(id))
|
|
1215
|
+
return state;
|
|
1216
|
+
applied.push(id);
|
|
1217
|
+
cursor += 1;
|
|
1218
|
+
}
|
|
1219
|
+
if (state.boundary !== null && ids[cursor] !== state.boundary && !insertUntil(state.boundary)) {
|
|
1220
|
+
return state;
|
|
1221
|
+
}
|
|
1222
|
+
const events = added.map((row) => {
|
|
1223
|
+
const iteration = segmentIterationAt(state.flow, applied, row.position);
|
|
1224
|
+
return {
|
|
1225
|
+
kind: "aligned",
|
|
1226
|
+
transition: row.transition,
|
|
1227
|
+
...(iteration === null ? {} : { batch_iteration: iteration }),
|
|
1228
|
+
operation: JOURNEY_ALIGNMENT_OPERATION,
|
|
1229
|
+
position: row.position,
|
|
1230
|
+
reason: ALIGNMENT_REASON,
|
|
1231
|
+
};
|
|
1232
|
+
});
|
|
1233
|
+
return sealRunState({
|
|
1234
|
+
...withoutSeal(state),
|
|
1235
|
+
journey_base: [...base],
|
|
1236
|
+
applied,
|
|
1237
|
+
skipped: [...state.skipped, ...added.map((row) => row.transition)],
|
|
1238
|
+
events: [...state.events, ...events],
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
function sameIds(left, right) {
|
|
1242
|
+
return left.length === right.length && left.every((id, index) => id === right[index]);
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Which plan-exec batch copy a cursor position falls inside, or null outside one.
|
|
1246
|
+
*
|
|
1247
|
+
* Two copies of the same added row differ only by the batch they belong to, so
|
|
1248
|
+
* the trace names it the way every other per-batch event does.
|
|
1249
|
+
*/
|
|
1250
|
+
function segmentIterationAt(flow, applied, position) {
|
|
1251
|
+
if (flow !== "plan-exec")
|
|
1252
|
+
return null;
|
|
1253
|
+
const before = applied.slice(0, position);
|
|
1254
|
+
const opened = before.lastIndexOf("plan-exec.batch-eligibility-signal");
|
|
1255
|
+
if (opened < 0 || before.lastIndexOf("plan-exec.batch-close") > opened)
|
|
1256
|
+
return null;
|
|
1257
|
+
return before.filter((id) => id === "plan-exec.batch-eligibility-signal").length;
|
|
1258
|
+
}
|
|
1259
|
+
function checkShape(parsed, remedy) {
|
|
1089
1260
|
const invalid = (why) => ({
|
|
1090
1261
|
code: "FLOW_RUN_INVALID",
|
|
1091
1262
|
message: `el estado de corrida ${why}`,
|
|
1092
|
-
action:
|
|
1263
|
+
action: remedy,
|
|
1093
1264
|
});
|
|
1094
1265
|
if (!WORKLINE_FLOWS.includes(parsed.flow)) {
|
|
1095
1266
|
return invalid(`declara un flow desconocido: ${String(parsed.flow)}`);
|
|
@@ -1125,16 +1296,23 @@ function checkRecordShape(parsed, invalid) {
|
|
|
1125
1296
|
const common = checkCommonRecordShape(parsed, invalid);
|
|
1126
1297
|
if (common !== null)
|
|
1127
1298
|
return common;
|
|
1128
|
-
|
|
1299
|
+
const version = parsed.version;
|
|
1300
|
+
if (version >= 10) {
|
|
1129
1301
|
const v10 = checkV10RecordShape(parsed, invalid);
|
|
1130
1302
|
if (v10 !== null)
|
|
1131
1303
|
return v10;
|
|
1132
1304
|
}
|
|
1133
|
-
if (
|
|
1305
|
+
if (version >= 11) {
|
|
1134
1306
|
const v11 = checkV11RecordShape(parsed, invalid);
|
|
1135
1307
|
if (v11 !== null)
|
|
1136
1308
|
return v11;
|
|
1137
1309
|
}
|
|
1310
|
+
if (version >= 12 && !isStringArray(parsed.journey_base)) {
|
|
1311
|
+
return invalid("no dice contra qué recorrido se escribió su cursor");
|
|
1312
|
+
}
|
|
1313
|
+
if (parsed.exhausted_reruns !== undefined && !isStringArray(parsed.exhausted_reruns)) {
|
|
1314
|
+
return invalid("declara vueltas de fronteras agotadas que no son transiciones");
|
|
1315
|
+
}
|
|
1138
1316
|
if (typeof parsed.digest !== "string")
|
|
1139
1317
|
return invalid("no trae su sello");
|
|
1140
1318
|
return null;
|
|
@@ -1599,7 +1777,8 @@ function isDecisionPreview(value, baseline) {
|
|
|
1599
1777
|
return value.effects.entries.every((entry) => isRecord(entry) &&
|
|
1600
1778
|
typeof entry.path === "string" &&
|
|
1601
1779
|
Number.isInteger(entry.bytes) &&
|
|
1602
|
-
typeof entry.overwrite === "boolean"
|
|
1780
|
+
typeof entry.overwrite === "boolean" &&
|
|
1781
|
+
isReservedMark(entry.reserved));
|
|
1603
1782
|
}
|
|
1604
1783
|
/**
|
|
1605
1784
|
* The persisted preview's obligations, in EITHER form.
|
|
@@ -1634,7 +1813,12 @@ function isProposalArtifacts(value) {
|
|
|
1634
1813
|
return value.every((entry) => isRecord(entry) &&
|
|
1635
1814
|
typeof entry.path === "string" &&
|
|
1636
1815
|
typeof entry.content === "string" &&
|
|
1637
|
-
typeof entry.overwrite === "boolean"
|
|
1816
|
+
typeof entry.overwrite === "boolean" &&
|
|
1817
|
+
isReservedMark(entry.reserved));
|
|
1818
|
+
}
|
|
1819
|
+
/** The optional reservation mark: absent, or exactly `true`. */
|
|
1820
|
+
function isReservedMark(value) {
|
|
1821
|
+
return value === undefined || value === true;
|
|
1638
1822
|
}
|
|
1639
1823
|
function isProposalBases(value) {
|
|
1640
1824
|
if (!Array.isArray(value))
|
|
@@ -1682,24 +1866,35 @@ function isEvent(entry) {
|
|
|
1682
1866
|
(!Number.isInteger(entry.batch_iteration) || entry.batch_iteration < 1)) {
|
|
1683
1867
|
return false;
|
|
1684
1868
|
}
|
|
1685
|
-
|
|
1686
|
-
return (typeof entry.summary === "string" &&
|
|
1687
|
-
typeof entry.output_digest === "string" &&
|
|
1688
|
-
isEffectClassArray(entry.effects) &&
|
|
1689
|
-
isStringArray(entry.evidence));
|
|
1690
|
-
}
|
|
1691
|
-
if (entry.kind === "reconciled")
|
|
1692
|
-
return isRepairArray(entry.repairs);
|
|
1693
|
-
if (entry.kind === "failed") {
|
|
1694
|
-
return (typeof entry.code === "string" &&
|
|
1695
|
-
typeof entry.message === "string" &&
|
|
1696
|
-
typeof entry.recovery === "string" &&
|
|
1697
|
-
// ABSENT-or-well-formed, never defaulted: a version 7 trace has no such
|
|
1698
|
-
// field, and the guard that reads it treats its absence as the refusal.
|
|
1699
|
-
(entry.effects === undefined || isEffectClassArray(entry.effects)));
|
|
1700
|
-
}
|
|
1701
|
-
return false;
|
|
1869
|
+
return EVENT_BODIES.get(entry.kind)?.(entry) === true;
|
|
1702
1870
|
}
|
|
1871
|
+
const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0;
|
|
1872
|
+
/**
|
|
1873
|
+
* What each trace kind demands beyond the common fields; an unknown kind has no
|
|
1874
|
+
* entry. A Map, not an object: a kind spelled like an Object.prototype member
|
|
1875
|
+
* must not find a function nobody declared.
|
|
1876
|
+
*/
|
|
1877
|
+
const EVENT_BODIES = new Map(Object.entries({
|
|
1878
|
+
executed: (entry) => typeof entry.summary === "string" &&
|
|
1879
|
+
typeof entry.output_digest === "string" &&
|
|
1880
|
+
isEffectClassArray(entry.effects) &&
|
|
1881
|
+
isStringArray(entry.evidence),
|
|
1882
|
+
reconciled: (entry) => isRepairArray(entry.repairs),
|
|
1883
|
+
restarted: (entry) => nonEmpty(entry.archive) && nonEmpty(entry.cause),
|
|
1884
|
+
annulled: (entry) => isStringArray(entry.batches) &&
|
|
1885
|
+
entry.batches.length > 0 &&
|
|
1886
|
+
Array.isArray(entry.phases) &&
|
|
1887
|
+
entry.phases.every((phase) => Number.isInteger(phase) && phase > 0) &&
|
|
1888
|
+
isStringArray(entry.tasks) &&
|
|
1889
|
+
nonEmpty(entry.digest),
|
|
1890
|
+
aligned: (entry) => Number.isInteger(entry.position) && entry.position >= 0 && nonEmpty(entry.reason),
|
|
1891
|
+
failed: (entry) => typeof entry.code === "string" &&
|
|
1892
|
+
typeof entry.message === "string" &&
|
|
1893
|
+
typeof entry.recovery === "string" &&
|
|
1894
|
+
// ABSENT-or-well-formed, never defaulted: a version 7 trace has no such
|
|
1895
|
+
// field, and the guard that reads it treats its absence as the refusal.
|
|
1896
|
+
(entry.effects === undefined || isEffectClassArray(entry.effects)),
|
|
1897
|
+
}));
|
|
1703
1898
|
/** A repair is well-formed when its rule is one of the three and its values are numbers. */
|
|
1704
1899
|
function isRepairArray(value) {
|
|
1705
1900
|
if (!Array.isArray(value))
|