@tacuchi/agent-workflow-cli 22.4.1 → 23.0.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/README.md +2 -2
- package/dist/adapters/git-cli.js +59 -17
- package/dist/adapters/git-cli.js.map +1 -1
- package/dist/adapters/node-file-system.js +55 -1
- package/dist/adapters/node-file-system.js.map +1 -1
- package/dist/application/artifacts-service.js +2 -2
- package/dist/application/artifacts-service.js.map +1 -1
- package/dist/application/capability/dispatcher.js +2 -2
- package/dist/application/capability/dispatcher.js.map +1 -1
- package/dist/application/capability/installed-inventory.js +2 -2
- package/dist/application/capability/installed-inventory.js.map +1 -1
- package/dist/application/capability/readiness.js +1 -1
- package/dist/application/capability/readiness.js.map +1 -1
- package/dist/application/check-branch-service.js +2 -2
- package/dist/application/check-branch-service.js.map +1 -1
- package/dist/application/checkpoint/files-touched.js +271 -0
- package/dist/application/checkpoint/files-touched.js.map +1 -0
- package/dist/application/checkpoint/markdown.js +61 -8
- package/dist/application/checkpoint/markdown.js.map +1 -1
- package/dist/application/checkpoint/state-reader.js +8 -2
- package/dist/application/checkpoint/state-reader.js.map +1 -1
- package/dist/application/checkpoint-write-service.js +16 -5
- package/dist/application/checkpoint-write-service.js.map +1 -1
- package/dist/application/claims-ledger.js +330 -0
- package/dist/application/claims-ledger.js.map +1 -0
- package/dist/application/claims-recovery.js +338 -0
- package/dist/application/claims-recovery.js.map +1 -0
- package/dist/application/dev-only-services.js +154 -44
- package/dist/application/dev-only-services.js.map +1 -1
- package/dist/application/elicitation-server.js +382 -0
- package/dist/application/elicitation-server.js.map +1 -0
- package/dist/application/elicitation-stdio.js +48 -0
- package/dist/application/elicitation-stdio.js.map +1 -0
- package/dist/application/flow/advance.js +86 -5
- package/dist/application/flow/advance.js.map +1 -1
- package/dist/application/flow/checkout-observation.js +152 -0
- package/dist/application/flow/checkout-observation.js.map +1 -0
- package/dist/application/flow/flow-service.js +72 -13
- package/dist/application/flow/flow-service.js.map +1 -1
- package/dist/application/flow/internal-actions.js +460 -1
- package/dist/application/flow/internal-actions.js.map +1 -1
- package/dist/application/flow/internal-drive.js +22 -9
- package/dist/application/flow/internal-drive.js.map +1 -1
- package/dist/application/flow/prove.js +205 -0
- package/dist/application/flow/prove.js.map +1 -0
- package/dist/application/flow/run-journey.js +15 -0
- package/dist/application/flow/run-journey.js.map +1 -0
- package/dist/application/flow/run-projection.js +15 -3
- package/dist/application/flow/run-projection.js.map +1 -1
- package/dist/application/flow/run-state-service.js +19 -4
- package/dist/application/flow/run-state-service.js.map +1 -1
- package/dist/application/flow/submit.js +250 -72
- package/dist/application/flow/submit.js.map +1 -1
- package/dist/application/generate-launch-service.js +4 -4
- package/dist/application/generate-launch-service.js.map +1 -1
- package/dist/application/git-flow-service.js +9 -0
- package/dist/application/git-flow-service.js.map +1 -1
- package/dist/application/local-proposal.js +9 -0
- package/dist/application/local-proposal.js.map +1 -1
- package/dist/application/lock-service.js +6 -3
- package/dist/application/lock-service.js.map +1 -1
- package/dist/application/mcp-doctor-service.js.map +1 -1
- package/dist/application/mcp-host-reader.js +30 -13
- package/dist/application/mcp-host-reader.js.map +1 -1
- package/dist/application/mcp-host-writer.js +136 -112
- package/dist/application/mcp-host-writer.js.map +1 -1
- package/dist/application/mcp-remove-service.js +7 -3
- package/dist/application/mcp-remove-service.js.map +1 -1
- package/dist/application/mcp-scope-common.js +1 -3
- package/dist/application/mcp-scope-common.js.map +1 -1
- package/dist/application/mcp-setup-service.js +7 -3
- package/dist/application/mcp-setup-service.js.map +1 -1
- package/dist/application/merge-state-service.js +10 -16
- package/dist/application/merge-state-service.js.map +1 -1
- package/dist/application/multiroot/claude.js +34 -15
- package/dist/application/multiroot/claude.js.map +1 -1
- package/dist/application/multiroot/codex.js +9 -5
- package/dist/application/multiroot/codex.js.map +1 -1
- package/dist/application/multiroot-service.js +20 -11
- package/dist/application/multiroot-service.js.map +1 -1
- package/dist/application/parsers/project-block.js +25 -2
- package/dist/application/parsers/project-block.js.map +1 -1
- package/dist/application/paths-service.js +30 -31
- package/dist/application/paths-service.js.map +1 -1
- package/dist/application/plan-exec-batch-service.js +549 -0
- package/dist/application/plan-exec-batch-service.js.map +1 -0
- package/dist/application/plan-exec-decision-service.js +273 -0
- package/dist/application/plan-exec-decision-service.js.map +1 -0
- package/dist/application/project-md-service.js +2 -2
- package/dist/application/project-md-service.js.map +1 -1
- package/dist/application/project-md-upsert-service.js +4 -4
- package/dist/application/project-md-upsert-service.js.map +1 -1
- package/dist/application/project-tab-data.js +12 -7
- package/dist/application/project-tab-data.js.map +1 -1
- package/dist/application/release-data-service.js +2 -2
- package/dist/application/release-data-service.js.map +1 -1
- package/dist/application/resume-service.js +32 -11
- package/dist/application/resume-service.js.map +1 -1
- package/dist/application/retirement/apply.js +84 -0
- package/dist/application/retirement/apply.js.map +1 -1
- package/dist/application/retirement/attribution.js +31 -1
- package/dist/application/retirement/attribution.js.map +1 -1
- package/dist/application/retirement/graph.js +19 -1
- package/dist/application/retirement/graph.js.map +1 -1
- package/dist/application/retirement/journal.js +11 -0
- package/dist/application/retirement/journal.js.map +1 -1
- package/dist/application/retirement/prepare.js +89 -3
- package/dist/application/retirement/prepare.js.map +1 -1
- package/dist/application/retirement/preview.js +16 -0
- package/dist/application/retirement/preview.js.map +1 -1
- package/dist/application/retirement/resolve.js +65 -0
- package/dist/application/retirement/resolve.js.map +1 -1
- package/dist/application/self/host-states.js +2 -0
- package/dist/application/self/host-states.js.map +1 -1
- package/dist/application/self/install-skill.js +53 -19
- package/dist/application/self/install-skill.js.map +1 -1
- package/dist/application/self/mcp-config.js +28 -30
- package/dist/application/self/mcp-config.js.map +1 -1
- package/dist/application/self/mcp-offer.js +96 -0
- package/dist/application/self/mcp-offer.js.map +1 -0
- package/dist/application/self/mcp-via-state.js +59 -0
- package/dist/application/self/mcp-via-state.js.map +1 -0
- package/dist/application/self/uninstall-skill.js +18 -3
- package/dist/application/self/uninstall-skill.js.map +1 -1
- package/dist/application/self/uninstall.js +37 -8
- package/dist/application/self/uninstall.js.map +1 -1
- package/dist/application/session-close-service.js +24 -1
- package/dist/application/session-close-service.js.map +1 -1
- package/dist/application/session-create-service.js +7 -0
- package/dist/application/session-create-service.js.map +1 -1
- package/dist/application/session-resume-service.js +2 -2
- package/dist/application/session-resume-service.js.map +1 -1
- package/dist/application/sessions-service.js +2 -4
- package/dist/application/sessions-service.js.map +1 -1
- package/dist/application/skills-resolver-service.js +4 -4
- package/dist/application/skills-resolver-service.js.map +1 -1
- package/dist/application/source-boundary-policy.js +18 -7
- package/dist/application/source-boundary-policy.js.map +1 -1
- package/dist/application/source-remove-service.js +12 -1
- package/dist/application/source-remove-service.js.map +1 -1
- package/dist/application/sources-service.js +2 -2
- package/dist/application/sources-service.js.map +1 -1
- package/dist/application/status-service.js +4 -0
- package/dist/application/status-service.js.map +1 -1
- package/dist/application/visibility-doctor-service.js +1 -1
- package/dist/application/visibility-doctor-service.js.map +1 -1
- package/dist/application/workline-index-service.js +182 -27
- package/dist/application/workline-index-service.js.map +1 -1
- package/dist/application/workspace-init-service.js +85 -265
- package/dist/application/workspace-init-service.js.map +1 -1
- package/dist/application/workspace-materialization-service.js +300 -0
- package/dist/application/workspace-materialization-service.js.map +1 -0
- package/dist/application/worktree-service.js +32 -3
- package/dist/application/worktree-service.js.map +1 -1
- package/dist/cli/commands/capability.js +4 -7
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/claims.js +72 -0
- package/dist/cli/commands/claims.js.map +1 -0
- package/dist/cli/commands/dev-only.js +73 -25
- package/dist/cli/commands/dev-only.js.map +1 -1
- package/dist/cli/commands/flow.js +145 -42
- package/dist/cli/commands/flow.js.map +1 -1
- package/dist/cli/commands/index.js +2 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/mcp.js +124 -35
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/multiroot.js +40 -7
- package/dist/cli/commands/multiroot.js.map +1 -1
- package/dist/cli/commands/resume.js +9 -3
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/commands/retirement.js +9 -0
- package/dist/cli/commands/retirement.js.map +1 -1
- package/dist/cli/commands/session-create.js +1 -1
- package/dist/cli/commands/session-create.js.map +1 -1
- package/dist/cli/commands/skills.js +1 -1
- package/dist/cli/commands/skills.js.map +1 -1
- package/dist/cli/commands/status.js +72 -24
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/workspace-init.js +5 -5
- package/dist/cli/commands/workspace-init.js.map +1 -1
- package/dist/cli/help-groups.js +5 -0
- package/dist/cli/help-groups.js.map +1 -1
- package/dist/cli/main.js +112 -39
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/parser.js +3 -0
- package/dist/cli/parser.js.map +1 -1
- package/dist/cli/tui/app.js +3 -2
- package/dist/cli/tui/app.js.map +1 -1
- package/dist/cli/tui/components/host-admin-section.js +46 -2
- package/dist/cli/tui/components/host-admin-section.js.map +1 -1
- package/dist/cli/tui/components/workspace-init-form.js +7 -7
- package/dist/cli/tui/components/workspace-init-form.js.map +1 -1
- package/dist/cli/tui/data/workflow-content.js +2 -2
- package/dist/cli/tui/data/workflow-content.js.map +1 -1
- package/dist/cli/tui/tabs/config-tab.js +4 -3
- package/dist/cli/tui/tabs/config-tab.js.map +1 -1
- package/dist/cli/tui/tabs/mcp-tab-helpers.js +1 -1
- package/dist/cli/tui/tabs/mcp-tab-helpers.js.map +1 -1
- package/dist/cli/tui/tabs/project-tab.js +18 -21
- package/dist/cli/tui/tabs/project-tab.js.map +1 -1
- package/dist/cli/tui/workspace-root.js +13 -0
- package/dist/cli/tui/workspace-root.js.map +1 -0
- package/dist/domain/capability/protocol.js +2 -2
- package/dist/domain/capability/protocol.js.map +1 -1
- package/dist/domain/degradation-notice.js +60 -0
- package/dist/domain/degradation-notice.js.map +1 -0
- package/dist/domain/elicitation.js +152 -0
- package/dist/domain/elicitation.js.map +1 -0
- package/dist/domain/flow/answer.js +173 -42
- package/dist/domain/flow/answer.js.map +1 -1
- package/dist/domain/flow/authority.js +180 -127
- package/dist/domain/flow/authority.js.map +1 -1
- package/dist/domain/flow/directive.js +47 -1
- package/dist/domain/flow/directive.js.map +1 -1
- package/dist/domain/flow/rules.js +7 -4
- package/dist/domain/flow/rules.js.map +1 -1
- package/dist/domain/flow/run-state.js +636 -31
- package/dist/domain/flow/run-state.js.map +1 -1
- package/dist/domain/harnesses.js +17 -1
- package/dist/domain/harnesses.js.map +1 -1
- package/dist/domain/mcp-entry.js +36 -14
- package/dist/domain/mcp-entry.js.map +1 -1
- package/dist/domain/retirement/proposal.js +1 -0
- package/dist/domain/retirement/proposal.js.map +1 -1
- package/dist/domain/structured-choice-stamp.js +32 -4
- package/dist/domain/structured-choice-stamp.js.map +1 -1
- package/dist/domain/workline-mcp-entry.js +37 -0
- package/dist/domain/workline-mcp-entry.js.map +1 -0
- package/dist/runtime/namespace-resolver.js +104 -18
- package/dist/runtime/namespace-resolver.js.map +1 -1
- package/package.json +1 -1
- package/skills/w/README.md +2 -2
- package/skills/w/SKILL.md +9 -10
- package/skills/w/artifacts/README.md +1 -1
- package/skills/w/commands/README.md +3 -3
- package/skills/w/commands/persist.md +3 -1
- package/skills/w/commands/resume.md +1 -1
- package/skills/w/commands/spec-new.md +2 -2
- package/skills/w/commands/status.md +1 -1
- package/skills/w/commands/workspace-init.md +9 -10
- package/skills/w/context/MANIFEST.json +1 -4
- package/skills/w/harness/HARNESS.md +1 -1
- package/skills/w/loops/quick-loop/LOOP.md +1 -1
- package/skills/w/modules/PROMPT-CONTINUITY.md +1 -1
- package/skills/w/modules/WORKSPACE-SCAFFOLD.md +17 -20
- package/skills/w/schemas/capability-descriptor.schema.json +1 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The durable trace of a reservation's whole life — and the only place it lives.
|
|
3
|
+
*
|
|
4
|
+
* A claim used to leave exactly one artifact: the marker file itself. That made
|
|
5
|
+
* the reservation visible while it existed and completely unaccountable once it
|
|
6
|
+
* did not. Nothing recorded who had claimed a correlative, why it went away, or
|
|
7
|
+
* whether it had ever been published — so a released number was
|
|
8
|
+
* indistinguishable from one that never existed, and a recovery had no evidence
|
|
9
|
+
* to stand on.
|
|
10
|
+
*
|
|
11
|
+
* This ledger is append-only and lives under `.workflow/`, deliberately OUTSIDE
|
|
12
|
+
* `docs/`: the corpus is for documents somebody published, and a reservation's
|
|
13
|
+
* history is not one. Putting it in `docs/` would make the trace itself look like
|
|
14
|
+
* a spec or a plan, which is the confusion the whole change exists to end.
|
|
15
|
+
*
|
|
16
|
+
* It answers three different questions with one file, which is why there is one
|
|
17
|
+
* file and not three:
|
|
18
|
+
* - **what happened** — the audit trail, still readable after the session's
|
|
19
|
+
* folder is gone, because a session is deleted and its history should not be;
|
|
20
|
+
* - **which correlatives came back** — a number released and never published is
|
|
21
|
+
* eligible again, and only this record knows the difference;
|
|
22
|
+
* - **what may no longer be published** — a revocation is a durable fence, and
|
|
23
|
+
* a fence that lives in memory is not one.
|
|
24
|
+
*
|
|
25
|
+
* Append-only is load-bearing for the third: a record that can be rewritten is a
|
|
26
|
+
* fence somebody can lift, and the revocation has to be irrevocable to be worth
|
|
27
|
+
* anything.
|
|
28
|
+
*/
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import { compareCorrelatives, isCorrelative } from "../domain/correlative.js";
|
|
31
|
+
/** Lives next to HISTORY.md: workspace state, never workspace corpus. */
|
|
32
|
+
const LEDGER_FILE = "claims.jsonl";
|
|
33
|
+
const LEDGER_VERSION = 1;
|
|
34
|
+
/** The identity as one comparable string. Order is fixed so it is stable. */
|
|
35
|
+
export function claimKey(claim) {
|
|
36
|
+
return `${claim.category}/${claim.correlative}-${claim.name}@${claim.owner}`;
|
|
37
|
+
}
|
|
38
|
+
export function ledgerPath(paths) {
|
|
39
|
+
return join(paths.cwdRoot(), LEDGER_FILE);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Add one record. Append-only by construction: there is no update and no delete.
|
|
43
|
+
*
|
|
44
|
+
* Two callers, two different guarantees, and the difference is worth stating
|
|
45
|
+
* rather than glossing:
|
|
46
|
+
*
|
|
47
|
+
* - the **claim** appends inside the workspace lock that mints the slot, so the
|
|
48
|
+
* reservation and its record cannot separate;
|
|
49
|
+
* - the **release** at session close does NOT hold that lock — the close's
|
|
50
|
+
* reservation sweep is deliberately outside it and non-fatal — so its record
|
|
51
|
+
* is ordered by `O_APPEND` alone.
|
|
52
|
+
*
|
|
53
|
+
* That is sufficient here only because a record is one short line: `O_APPEND`
|
|
54
|
+
* makes a single small write atomic, so two processes appending cannot interleave
|
|
55
|
+
* halves of a record. It would stop being sufficient the day a record grew past a
|
|
56
|
+
* pipe buffer, which is the reason a record is one line and not a pretty-printed
|
|
57
|
+
* object.
|
|
58
|
+
*/
|
|
59
|
+
export async function appendClaimEvent(fs, paths, event) {
|
|
60
|
+
const record = { version: LEDGER_VERSION, ...event };
|
|
61
|
+
await fs.appendText(ledgerPath(paths), `${JSON.stringify(record)}\n`);
|
|
62
|
+
}
|
|
63
|
+
/** Every record, oldest first. A missing ledger reads as empty, never as an error. */
|
|
64
|
+
export async function readClaimEvents(fs, paths) {
|
|
65
|
+
const path = ledgerPath(paths);
|
|
66
|
+
if (!(await fs.exists(path)))
|
|
67
|
+
return { events: [], unreadable: 0 };
|
|
68
|
+
const raw = await fs.readText(path);
|
|
69
|
+
const events = [];
|
|
70
|
+
let unreadable = 0;
|
|
71
|
+
for (const line of raw.split("\n")) {
|
|
72
|
+
const trimmed = line.trim();
|
|
73
|
+
if (trimmed.length === 0)
|
|
74
|
+
continue;
|
|
75
|
+
const parsed = parseEvent(trimmed);
|
|
76
|
+
if (parsed === null)
|
|
77
|
+
unreadable += 1;
|
|
78
|
+
else
|
|
79
|
+
events.push(parsed);
|
|
80
|
+
}
|
|
81
|
+
return { events, unreadable };
|
|
82
|
+
}
|
|
83
|
+
function parseEvent(line) {
|
|
84
|
+
let value;
|
|
85
|
+
try {
|
|
86
|
+
value = JSON.parse(line);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (typeof value !== "object" || value === null)
|
|
92
|
+
return null;
|
|
93
|
+
const candidate = value;
|
|
94
|
+
const claim = candidate.claim;
|
|
95
|
+
if (typeof candidate.at !== "string" ||
|
|
96
|
+
typeof candidate.event !== "string" ||
|
|
97
|
+
typeof claim !== "object" ||
|
|
98
|
+
claim === null ||
|
|
99
|
+
typeof claim.category !== "string" ||
|
|
100
|
+
typeof claim.correlative !== "string" ||
|
|
101
|
+
typeof claim.name !== "string" ||
|
|
102
|
+
typeof claim.owner !== "string") {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The claims this owner still holds: a `claimed` with no terminal record after it.
|
|
109
|
+
*
|
|
110
|
+
* Derived rather than stored, because the ledger is append-only and a "still
|
|
111
|
+
* open" flag would be exactly the kind of mutable state that makes an
|
|
112
|
+
* append-only log pointless. `published`, `released` and `revoked` are all
|
|
113
|
+
* terminal — the first spends the number, the other two end the owner's hold on
|
|
114
|
+
* it — so any of them closes the claim for this reading.
|
|
115
|
+
*/
|
|
116
|
+
export function openClaimsOf(events, owner) {
|
|
117
|
+
const open = new Map();
|
|
118
|
+
for (const event of events) {
|
|
119
|
+
if (event.claim.owner !== owner)
|
|
120
|
+
continue;
|
|
121
|
+
const key = claimKey(event.claim);
|
|
122
|
+
if (event.event === "claimed")
|
|
123
|
+
open.set(key, event.claim);
|
|
124
|
+
else
|
|
125
|
+
open.delete(key);
|
|
126
|
+
}
|
|
127
|
+
return [...open.values()];
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The claim a workspace-relative `docs/<category>/<NNN>-<name>` path would be.
|
|
131
|
+
*
|
|
132
|
+
* `null` for anything that is not a numbered document inside a category, which
|
|
133
|
+
* is what keeps this from reading a claim out of an unrelated destination.
|
|
134
|
+
*
|
|
135
|
+
* Exported because it is the ONE place that decides how a path splits into a
|
|
136
|
+
* claim identity: the publication reads it to credit a completion and the
|
|
137
|
+
* retirement reads it to name what it is giving back, and two spellings of that
|
|
138
|
+
* split would join to two different histories for the same slot.
|
|
139
|
+
*/
|
|
140
|
+
export function claimOfDocsPath(path, owner) {
|
|
141
|
+
const parts = path.split("/").filter((segment) => segment.length > 0);
|
|
142
|
+
const docsAt = parts.indexOf("docs");
|
|
143
|
+
if (docsAt === -1 || parts.length - docsAt !== 3)
|
|
144
|
+
return null;
|
|
145
|
+
const category = parts[docsAt + 1];
|
|
146
|
+
const file = parts[docsAt + 2];
|
|
147
|
+
if (category === undefined || file === undefined)
|
|
148
|
+
return null;
|
|
149
|
+
const match = /^(\d{3,})-(.+)$/.exec(file);
|
|
150
|
+
if (match?.[1] === undefined || match[2] === undefined)
|
|
151
|
+
return null;
|
|
152
|
+
return { category, correlative: match[1], name: match[2], owner };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Which claims of this owner a publication just completed.
|
|
156
|
+
*
|
|
157
|
+
* The whole decision of "did this write finish a reservation of mine?" lives
|
|
158
|
+
* here rather than at the publication site, and getting it wrong is durable in
|
|
159
|
+
* both directions:
|
|
160
|
+
*
|
|
161
|
+
* - **Under-crediting** leaves a claim open about a correlative that is holding a
|
|
162
|
+
* published document, which invites a later recovery to release live bytes.
|
|
163
|
+
* That is why `already_applied` is handled here: the re-entry that finds the
|
|
164
|
+
* document already on disk reports `written: []`, and crediting only `written`
|
|
165
|
+
* left the claim open FOREVER — every retry answers the same way, so nothing
|
|
166
|
+
* could ever correct it. When the apply says already-applied, every destination
|
|
167
|
+
* holds exactly the proposed bytes, so the destinations are the candidates.
|
|
168
|
+
* - **Over-crediting** writes a permanent "spent forever" fence on a correlative
|
|
169
|
+
* the session never reserved. So a candidate only counts when it closes one of
|
|
170
|
+
* THIS owner's open claims, read from the ledger — not from the shape of
|
|
171
|
+
* whatever proposal happened to carry the write. Every flow's publication goes
|
|
172
|
+
* through this same path, not only the one whose single artifact is its own slot.
|
|
173
|
+
*
|
|
174
|
+
* Both directions had a surviving mutant when this logic sat inlined at the call
|
|
175
|
+
* site, which is the other reason it is here: it is testable in isolation.
|
|
176
|
+
*/
|
|
177
|
+
export function completedClaimsIn(events, owner, publication) {
|
|
178
|
+
const candidates = publication.already_applied ? publication.destinations : publication.written;
|
|
179
|
+
if (candidates.length === 0)
|
|
180
|
+
return [];
|
|
181
|
+
const open = new Set(openClaimsOf(events, owner).map((claim) => claimKey(claim)));
|
|
182
|
+
if (open.size === 0)
|
|
183
|
+
return [];
|
|
184
|
+
const completed = [];
|
|
185
|
+
for (const path of candidates) {
|
|
186
|
+
const claim = claimOfDocsPath(path, owner);
|
|
187
|
+
if (claim === null || !open.has(claimKey(claim)))
|
|
188
|
+
continue;
|
|
189
|
+
completed.push(claim);
|
|
190
|
+
}
|
|
191
|
+
return completed;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Every claim this ledger has ever revoked.
|
|
195
|
+
*
|
|
196
|
+
* Membership is permanent by construction: one `revoked` record fences the claim
|
|
197
|
+
* forever, and no later record lifts it. That is what "irrevocable" has to mean
|
|
198
|
+
* to be worth anything — a fence somebody can reopen is not a fence, and the
|
|
199
|
+
* whole reason a recovery may free a correlative is that no late publication can
|
|
200
|
+
* still land on it.
|
|
201
|
+
*/
|
|
202
|
+
function revokedKeys(events) {
|
|
203
|
+
const revoked = new Set();
|
|
204
|
+
for (const event of events) {
|
|
205
|
+
if (event.event === "revoked")
|
|
206
|
+
revoked.add(claimKey(event.claim));
|
|
207
|
+
}
|
|
208
|
+
return revoked;
|
|
209
|
+
}
|
|
210
|
+
/** Whether this exact claim is fenced. */
|
|
211
|
+
export function isRevoked(events, claim) {
|
|
212
|
+
return revokedKeys(events).has(claimKey(claim));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The destinations of a publication that a revocation forbids.
|
|
216
|
+
*
|
|
217
|
+
* Checked at the publication point rather than at the release: once a recovery
|
|
218
|
+
* has freed a correlative, the slot no longer exists on disk, so a late sealed
|
|
219
|
+
* proposal would read its destination as a plain creation and land a document on
|
|
220
|
+
* a number that may already belong to somebody else. The fence is the only thing
|
|
221
|
+
* standing between "the reservation was recovered" and "two documents share a
|
|
222
|
+
* correlative".
|
|
223
|
+
*/
|
|
224
|
+
export function revokedAmong(events, owner, destinations) {
|
|
225
|
+
const revoked = revokedKeys(events);
|
|
226
|
+
if (revoked.size === 0)
|
|
227
|
+
return [];
|
|
228
|
+
const blocked = [];
|
|
229
|
+
for (const path of destinations) {
|
|
230
|
+
const claim = claimOfDocsPath(path, owner);
|
|
231
|
+
if (claim === null || !revoked.has(claimKey(claim)))
|
|
232
|
+
continue;
|
|
233
|
+
blocked.push(claim);
|
|
234
|
+
}
|
|
235
|
+
return blocked;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Destinations that look like a numbered document of this owner's category space.
|
|
239
|
+
*
|
|
240
|
+
* Used only to scope the fail-closed: a ledger with unreadable lines cannot prove
|
|
241
|
+
* the ABSENCE of a revocation, so a publication that could be completing a
|
|
242
|
+
* reservation must refuse rather than guess. A write that is not a numbered
|
|
243
|
+
* document in a category cannot be a reservation, so it is never held up by a
|
|
244
|
+
* ledger it does not depend on.
|
|
245
|
+
*/
|
|
246
|
+
export function claimShapedAmong(owner, destinations) {
|
|
247
|
+
const shaped = [];
|
|
248
|
+
for (const path of destinations) {
|
|
249
|
+
const claim = claimOfDocsPath(path, owner);
|
|
250
|
+
if (claim !== null)
|
|
251
|
+
shaped.push(claim);
|
|
252
|
+
}
|
|
253
|
+
return shaped;
|
|
254
|
+
}
|
|
255
|
+
function sameSlot(claim, slot) {
|
|
256
|
+
return (claim.category === slot.category &&
|
|
257
|
+
claim.correlative === slot.correlative &&
|
|
258
|
+
claim.name === slot.name);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Whether this slot was ever published, by anyone.
|
|
262
|
+
*
|
|
263
|
+
* Asked BEFORE the bytes are interpreted, because bytes lie in one direction that
|
|
264
|
+
* matters: a published document whose content happens to be empty looks exactly
|
|
265
|
+
* like the legacy placeholder a recovery is allowed to delete. The record knows
|
|
266
|
+
* the difference and the file does not, so the record decides.
|
|
267
|
+
*/
|
|
268
|
+
export function wasPublished(events, slot) {
|
|
269
|
+
return events.some((event) => event.event === "published" && sameSlot(event.claim, slot));
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* The owner whose claim on this slot is still open, or `null`.
|
|
273
|
+
*
|
|
274
|
+
* Lets a file that is no longer its own intact marker — emptied by an editor, a
|
|
275
|
+
* `> file`, a checkout — still be attributed to the session that reserved it.
|
|
276
|
+
* Without this the same file reads as ownerless, and freeing it would give the
|
|
277
|
+
* correlative back with no fence for the owner that is still holding it.
|
|
278
|
+
*/
|
|
279
|
+
export function openOwnerOfSlot(events, slot) {
|
|
280
|
+
let open = null;
|
|
281
|
+
for (const event of events) {
|
|
282
|
+
if (!sameSlot(event.claim, slot))
|
|
283
|
+
continue;
|
|
284
|
+
open = event.event === "claimed" ? event.claim : null;
|
|
285
|
+
}
|
|
286
|
+
return open;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Correlatives of this category that were released and never published, ascending.
|
|
290
|
+
*
|
|
291
|
+
* The second reason this ledger exists. Minting used to compute `max + 1` and
|
|
292
|
+
* probe forward only, so a correlative given back in the middle of the range was
|
|
293
|
+
* lost forever — this workspace's own `docs/plans` has a permanent hole at `033`
|
|
294
|
+
* from exactly that. Only the record can tell a number that came back from one
|
|
295
|
+
* that never existed, because the disk looks identical either way.
|
|
296
|
+
*
|
|
297
|
+
* Judged per (category, correlative) and by the LAST terminal record, not by the
|
|
298
|
+
* presence of any: a number can be released, re-claimed and then published, and
|
|
299
|
+
* after that it is spent. `published` is the one state that never becomes eligible
|
|
300
|
+
* again — a correlative holding a document is not a free number, whatever else
|
|
301
|
+
* the history says about it.
|
|
302
|
+
*
|
|
303
|
+
* Ascending because the rule has to be deterministic and reproducible, and
|
|
304
|
+
* lowest-first also fills the holes rather than growing the range. The caller is
|
|
305
|
+
* still responsible for skipping anything taken on disk: this answers "did the
|
|
306
|
+
* record give it back", never "is the name free right now".
|
|
307
|
+
*/
|
|
308
|
+
export function eligibleCorrelatives(events, category) {
|
|
309
|
+
const state = new Map();
|
|
310
|
+
for (const event of events) {
|
|
311
|
+
if (event.claim.category !== category)
|
|
312
|
+
continue;
|
|
313
|
+
if (event.event === "claimed")
|
|
314
|
+
continue;
|
|
315
|
+
state.set(event.claim.correlative, event.event);
|
|
316
|
+
}
|
|
317
|
+
const eligible = [];
|
|
318
|
+
for (const [correlative, last] of state) {
|
|
319
|
+
// Validated before it can be handed out. A single semi-valid ledger line
|
|
320
|
+
// would otherwise put a non-correlative into the mint, where it becomes an
|
|
321
|
+
// unrecognizable filename or a throw on the comparator.
|
|
322
|
+
if (last === "released" && isCorrelative(correlative))
|
|
323
|
+
eligible.push(correlative);
|
|
324
|
+
}
|
|
325
|
+
// `compareCorrelatives` and not a hand-rolled numeric sort: it is bigint-based,
|
|
326
|
+
// so it stays correct past the width where `parseInt` loses precision — and a
|
|
327
|
+
// second ordering rule for the same domain type is a second thing to keep true.
|
|
328
|
+
return eligible.sort(compareCorrelatives);
|
|
329
|
+
}
|
|
330
|
+
//# sourceMappingURL=claims-ledger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claims-ledger.js","sourceRoot":"","sources":["../../src/application/claims-ledger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovering a reservation whose owner never came back.
|
|
3
|
+
*
|
|
4
|
+
* The normal cycle needs none of this: completing, closing and cancelling all
|
|
5
|
+
* resolve a reservation through its owner. What is left is the run that simply
|
|
6
|
+
* died — no publication, no close, nothing terminal — and left a correlative held
|
|
7
|
+
* by a session that is never going to finish it. There is deliberately NO clock
|
|
8
|
+
* here: a reservation does not expire, because "it has been a while" is not
|
|
9
|
+
* evidence that nobody is coming, and a timer that frees a slot somebody is still
|
|
10
|
+
* writing into would be worse than the stranded number it fixes.
|
|
11
|
+
*
|
|
12
|
+
* So the release is explicit, authorized, and ORDERED. The order is the whole
|
|
13
|
+
* safety argument and it runs one way only:
|
|
14
|
+
*
|
|
15
|
+
* 1. the reservation is still exactly its own intact marker — otherwise the
|
|
16
|
+
* bytes are somebody's work and this is not a recovery;
|
|
17
|
+
* 2. a revocation is SEALED, durably and irrevocably, scoped to that one claim;
|
|
18
|
+
* 3. and only then is the slot released.
|
|
19
|
+
*
|
|
20
|
+
* Step 2 before step 3 is not a preference. Once the file is gone the correlative
|
|
21
|
+
* is eligible again, so a late sealed proposal from the dead owner — one that was
|
|
22
|
+
* approved before the recovery and lands after it — would write a document onto a
|
|
23
|
+
* number somebody else may already hold. The fence is what makes that publication
|
|
24
|
+
* fail instead of collide, and a fence written after the release is a window. If
|
|
25
|
+
* the seal cannot be written, the recovery FAILS and releases nothing: a released
|
|
26
|
+
* slot with no fence is the one outcome this must never produce.
|
|
27
|
+
*
|
|
28
|
+
* The revocation belongs to the CLAIM, never to the session. The owner may be
|
|
29
|
+
* alive and working on other things, and revoking its whole session to reclaim
|
|
30
|
+
* one number would destroy work to tidy up a correlative.
|
|
31
|
+
*/
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { leadingCorrelative } from "../domain/correlative.js";
|
|
34
|
+
import { reservationOwnerOf } from "../domain/reservation.js";
|
|
35
|
+
import { appendClaimEvent, claimKey, isRevoked, openOwnerOfSlot, readClaimEvents, wasPublished, } from "./claims-ledger.js";
|
|
36
|
+
import { withCwdLock } from "./lock-service.js";
|
|
37
|
+
import { semanticDigest } from "./semantic-operation/protocol.js";
|
|
38
|
+
import { CLOSED_MARKER, listSessionFolders } from "./session-resolver.js";
|
|
39
|
+
/** The identity a reservation's slot maps to, or `null` for an unattributable one. */
|
|
40
|
+
function claimOfSlot(slot) {
|
|
41
|
+
if (slot.owner === null)
|
|
42
|
+
return null;
|
|
43
|
+
return {
|
|
44
|
+
category: slot.category,
|
|
45
|
+
correlative: slot.correlative,
|
|
46
|
+
name: slot.name,
|
|
47
|
+
owner: slot.owner,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The slot a numbered file is, or `null` when it is a document.
|
|
52
|
+
*
|
|
53
|
+
* The LEDGER decides before the bytes do, and the order is the fix for a real
|
|
54
|
+
* defect: classifying from bytes alone made every empty numbered file an
|
|
55
|
+
* ownerless legacy placeholder — releasable with no fence — including a published
|
|
56
|
+
* document whose content happens to be empty, and including a reservation whose
|
|
57
|
+
* marker somebody emptied. In the first case a recovery deleted a published
|
|
58
|
+
* document; in the second it gave a correlative back while its real owner still
|
|
59
|
+
* held an open claim on it. Both times the record held the answer and only the
|
|
60
|
+
* bytes were consulted.
|
|
61
|
+
*/
|
|
62
|
+
function slotOf(category, fileName, text, events, activeOwners) {
|
|
63
|
+
const correlative = leadingCorrelative(fileName);
|
|
64
|
+
if (correlative === null)
|
|
65
|
+
return null;
|
|
66
|
+
const name = fileName.slice(correlative.length + 1);
|
|
67
|
+
const slot = { category, correlative, name };
|
|
68
|
+
// Published is terminal and terminal means "a document lives here": whatever
|
|
69
|
+
// its bytes look like, it is not a slot anybody may free.
|
|
70
|
+
if (wasPublished(events, slot))
|
|
71
|
+
return null;
|
|
72
|
+
const markerOwner = reservationOwnerOf(text);
|
|
73
|
+
const base = { path: `docs/${category}/${fileName}`, category, correlative, name };
|
|
74
|
+
if (markerOwner !== null) {
|
|
75
|
+
return {
|
|
76
|
+
...base,
|
|
77
|
+
kind: "reservation",
|
|
78
|
+
owner: markerOwner,
|
|
79
|
+
ownerActive: activeOwners.has(markerOwner),
|
|
80
|
+
revoked: false,
|
|
81
|
+
intact: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// Real content and no marker: a document, by its bytes this time.
|
|
85
|
+
if (text.trim().length > 0)
|
|
86
|
+
return null;
|
|
87
|
+
// Empty. If the ledger still holds an open claim on this slot, it is that
|
|
88
|
+
// owner's reservation with damaged bytes — not nobody's placeholder.
|
|
89
|
+
const open = openOwnerOfSlot(events, slot);
|
|
90
|
+
if (open !== null) {
|
|
91
|
+
return {
|
|
92
|
+
...base,
|
|
93
|
+
kind: "reservation",
|
|
94
|
+
owner: open.owner,
|
|
95
|
+
ownerActive: activeOwners.has(open.owner),
|
|
96
|
+
revoked: false,
|
|
97
|
+
intact: false,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
...base,
|
|
102
|
+
kind: "legacy-placeholder",
|
|
103
|
+
owner: null,
|
|
104
|
+
ownerActive: null,
|
|
105
|
+
revoked: false,
|
|
106
|
+
intact: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Walk `docs/` into `into`, throwing on the first unreadable thing.
|
|
111
|
+
*
|
|
112
|
+
* The accumulator is the caller's so a failure still reports what it managed to
|
|
113
|
+
* see: an unreadable category is a different fact from an empty one, and folding
|
|
114
|
+
* the two would let the board say "no reservations" about a directory nobody
|
|
115
|
+
* could open.
|
|
116
|
+
*/
|
|
117
|
+
async function walkSlots(fs, docs, events, activeOwners, into) {
|
|
118
|
+
for (const category of await fs.list(docs)) {
|
|
119
|
+
if (category.type !== "dir")
|
|
120
|
+
continue;
|
|
121
|
+
for (const entry of await fs.list(category.path)) {
|
|
122
|
+
// The filename decides whether the bytes are worth reading at all. Reading
|
|
123
|
+
// every file in every docs/ subdirectory to then discard most of them on the
|
|
124
|
+
// first line of `slotOf` made a scan that runs on EVERY board projection pay
|
|
125
|
+
// for the whole corpus.
|
|
126
|
+
if (entry.type !== "file" || leadingCorrelative(entry.name) === null)
|
|
127
|
+
continue;
|
|
128
|
+
const slot = slotOf(category.name, entry.name, await fs.readText(entry.path), events, activeOwners);
|
|
129
|
+
if (slot === null)
|
|
130
|
+
continue;
|
|
131
|
+
const claim = claimOfSlot(slot);
|
|
132
|
+
slot.revoked = claim !== null && isRevoked(events, claim);
|
|
133
|
+
into.push(slot);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The session folders that are still open.
|
|
139
|
+
*
|
|
140
|
+
* Read once per scan, because the sanctioned action for a slot depends on it: a
|
|
141
|
+
* reservation of a live session is that session's to finish or close, and only a
|
|
142
|
+
* slot nobody is finishing may be recovered.
|
|
143
|
+
*/
|
|
144
|
+
async function activeSessionFolders(fs, paths) {
|
|
145
|
+
const active = new Set();
|
|
146
|
+
try {
|
|
147
|
+
for (const folder of await listSessionFolders(fs, paths.cwdSessionsDir())) {
|
|
148
|
+
if (await fs.exists(join(folder.path, CLOSED_MARKER)))
|
|
149
|
+
continue;
|
|
150
|
+
active.add(folder.name);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// An unreadable sessions dir means nobody can be proven alive. The fallback is
|
|
155
|
+
// the conservative one: with no owner known to be active, no slot is offered
|
|
156
|
+
// for recovery on the strength of liveness it could not check.
|
|
157
|
+
}
|
|
158
|
+
return active;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Every reservation and legacy placeholder under `docs/`.
|
|
162
|
+
*
|
|
163
|
+
* Walks each immediate subdirectory rather than a list of categories, for the
|
|
164
|
+
* same reason the close's sweep does: the claim mechanism is category-agnostic,
|
|
165
|
+
* and a hardcoded list is a second place to update the day something else claims
|
|
166
|
+
* a number.
|
|
167
|
+
*
|
|
168
|
+
* A failure comes back WITH its reason and with whatever was already seen. An
|
|
169
|
+
* unreadable `docs/` is not an empty one, and swallowing the difference would let
|
|
170
|
+
* the board answer "no reservations" about a directory nobody could open.
|
|
171
|
+
*/
|
|
172
|
+
export async function scanSlots(fs, paths) {
|
|
173
|
+
const docs = join(paths.workspaceDir(), "docs");
|
|
174
|
+
const slots = [];
|
|
175
|
+
const ledger = await readClaimEvents(fs, paths);
|
|
176
|
+
const activeOwners = await activeSessionFolders(fs, paths);
|
|
177
|
+
const sorted = () => slots.sort((a, b) => a.path.localeCompare(b.path));
|
|
178
|
+
if (!(await fs.exists(docs)))
|
|
179
|
+
return { slots };
|
|
180
|
+
try {
|
|
181
|
+
await walkSlots(fs, docs, ledger.events, activeOwners, slots);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
return {
|
|
185
|
+
slots: sorted(),
|
|
186
|
+
error: `no se pudo revisar docs/ en busca de reservas: ${error instanceof Error ? error.message : String(error)}`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return { slots: sorted() };
|
|
190
|
+
}
|
|
191
|
+
function sealRecovery(body) {
|
|
192
|
+
return { ...body, digest: semanticDigest(body) };
|
|
193
|
+
}
|
|
194
|
+
/** The proposal for one slot, or why there is none. */
|
|
195
|
+
export async function previewRecovery(fs, paths, target) {
|
|
196
|
+
const scan = await scanSlots(fs, paths);
|
|
197
|
+
const slot = scan.slots.find((candidate) => candidate.path === target);
|
|
198
|
+
if (slot === undefined) {
|
|
199
|
+
return {
|
|
200
|
+
error: `'${target}' no es una reserva ni un placeholder legacy de este workspace`,
|
|
201
|
+
action: scan.error ??
|
|
202
|
+
"corré 'aw claims' para ver los correlativos recuperables; un documento publicado no se recupera",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
proposal: sealRecovery({
|
|
207
|
+
version: 1,
|
|
208
|
+
target: slot.path,
|
|
209
|
+
kind: slot.kind,
|
|
210
|
+
claim: claimOfSlot(slot),
|
|
211
|
+
requires_no_producer_confirmation: !slot.intact,
|
|
212
|
+
resuming: slot.revoked,
|
|
213
|
+
}),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Seal the fence, then release — and never the other way round.
|
|
218
|
+
*
|
|
219
|
+
* The whole thing runs under the workspace lock, which is not decoration: the
|
|
220
|
+
* check that the slot is still what the preview saw, the seal and the removal are
|
|
221
|
+
* three separate awaits, and without the lock a sanctioned publication could pass
|
|
222
|
+
* its own fence, take the lock, write its document and have this function delete
|
|
223
|
+
* it a moment later — leaving the correlative recorded as both spent-forever and
|
|
224
|
+
* eligible-again, with the published document silently gone. Its two siblings hold
|
|
225
|
+
* the lock across exactly this span for exactly this reason.
|
|
226
|
+
*
|
|
227
|
+
* It also re-derives its own proposal inside the lock instead of trusting the
|
|
228
|
+
* digest it was handed: the approval proves a person authorized THIS recovery, not
|
|
229
|
+
* that the world still looks the way it did when they read it.
|
|
230
|
+
*/
|
|
231
|
+
export async function applyRecovery(fs, paths, input) {
|
|
232
|
+
const outcome = await withCwdLock(fs, paths, () => recoverUnderLock(fs, paths, input));
|
|
233
|
+
if ("error" in outcome && "action" in outcome)
|
|
234
|
+
return outcome;
|
|
235
|
+
if ("error" in outcome) {
|
|
236
|
+
return {
|
|
237
|
+
error: `no se pudo tomar el candado del workspace: ${outcome.error}`,
|
|
238
|
+
action: "esperá a que otro flujo lo libere y volvé a aplicar la recuperación",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return outcome;
|
|
242
|
+
}
|
|
243
|
+
async function recoverUnderLock(fs, paths, input) {
|
|
244
|
+
const preview = await previewRecovery(fs, paths, input.target);
|
|
245
|
+
if ("error" in preview)
|
|
246
|
+
return preview;
|
|
247
|
+
const proposal = preview.proposal;
|
|
248
|
+
if (proposal.digest !== input.approval) {
|
|
249
|
+
return {
|
|
250
|
+
error: "la aprobación no corresponde a la recuperación vigente",
|
|
251
|
+
action: `volvé a mirarla con 'aw claims recover ${input.target}' y aprobá el digest que devuelve`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (proposal.requires_no_producer_confirmation && input.noProducerConfirmed !== true) {
|
|
255
|
+
return {
|
|
256
|
+
error: proposal.kind === "legacy-placeholder"
|
|
257
|
+
? `'${input.target}' es un placeholder legacy ambiguo: sus bytes no prueban que nadie vaya a escribirlo`
|
|
258
|
+
: `'${input.target}' ya no tiene el marcador intacto de su dueño: alguien escribió ahí`,
|
|
259
|
+
action: "confirmá explícitamente que no queda productor capaz de escribir ese destino antes de liberar su correlativo",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const claim = proposal.claim;
|
|
263
|
+
if (claim !== null && !proposal.resuming) {
|
|
264
|
+
// The fence FIRST. If this throws, nothing was released and the slot is
|
|
265
|
+
// exactly as it was: the recovery failed, which is the correct outcome.
|
|
266
|
+
// Releasing first would leave a window where the correlative is eligible and
|
|
267
|
+
// a late sealed publication can still land on it.
|
|
268
|
+
await appendClaimEvent(fs, paths, {
|
|
269
|
+
at: new Date().toISOString(),
|
|
270
|
+
event: "revoked",
|
|
271
|
+
claim,
|
|
272
|
+
cause: `aw claims recover: recuperación autorizada de ${proposal.target}`,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
// Recorded before the file goes, same reason as the close's release: a slot
|
|
276
|
+
// freed with no line saying so is the state this ledger exists to end. The
|
|
277
|
+
// identity is the SLOT's, so it joins to its own history by claimKey — an
|
|
278
|
+
// ownerless placeholder still records the category, correlative and name it
|
|
279
|
+
// gave back, and only its owner field says nobody held it.
|
|
280
|
+
await appendClaimEvent(fs, paths, {
|
|
281
|
+
at: new Date().toISOString(),
|
|
282
|
+
event: "released",
|
|
283
|
+
claim: claim ?? {
|
|
284
|
+
category: slotIdentityOf(proposal.target).category,
|
|
285
|
+
correlative: slotIdentityOf(proposal.target).correlative,
|
|
286
|
+
name: slotIdentityOf(proposal.target).name,
|
|
287
|
+
owner: LEGACY_OWNERLESS,
|
|
288
|
+
},
|
|
289
|
+
cause: claim === null
|
|
290
|
+
? "aw claims recover: placeholder legacy liberado con confirmación explícita de que no queda productor"
|
|
291
|
+
: `aw claims recover: liberado tras revocar ${claimKey(claim)}`,
|
|
292
|
+
});
|
|
293
|
+
await fs.remove(join(paths.workspaceDir(), proposal.target));
|
|
294
|
+
return {
|
|
295
|
+
applied: {
|
|
296
|
+
target: proposal.target,
|
|
297
|
+
revoked: claim,
|
|
298
|
+
released: true,
|
|
299
|
+
resumed: proposal.resuming,
|
|
300
|
+
digest: proposal.digest,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/** The owner field of a slot that never had one. Never a session folder. */
|
|
305
|
+
const LEGACY_OWNERLESS = "(placeholder legacy sin dueño)";
|
|
306
|
+
/** `docs/<cat>/<NNN>-<name>` split the same way every ledger record splits it. */
|
|
307
|
+
function slotIdentityOf(target) {
|
|
308
|
+
const parts = target.split("/");
|
|
309
|
+
const category = parts[1] ?? "";
|
|
310
|
+
const file = parts[2] ?? "";
|
|
311
|
+
const correlative = leadingCorrelative(file) ?? "";
|
|
312
|
+
return {
|
|
313
|
+
category,
|
|
314
|
+
correlative,
|
|
315
|
+
name: correlative.length > 0 ? file.slice(correlative.length + 1) : file,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The one action this slot sanctions, named per slot.
|
|
320
|
+
*
|
|
321
|
+
* A reservation of a live session is resumed or closed by its owner; only a slot
|
|
322
|
+
* nobody is finishing gets recovered. And a slot whose bytes are not its own
|
|
323
|
+
* intact marker carries the confirmation flag in the command itself, so the
|
|
324
|
+
* operator sees the extra assertion being asked of them before they type it.
|
|
325
|
+
*/
|
|
326
|
+
export function sanctionedActionFor(slot) {
|
|
327
|
+
// A live owner's reservation is NOT a recovery candidate. Naming the recovery
|
|
328
|
+
// here was destructive: the board handed the running session the one command
|
|
329
|
+
// that revokes its own slot irrevocably, and the flow reads this very field as
|
|
330
|
+
// "the sanctioned next command". Closing the owner releases an intact
|
|
331
|
+
// reservation as part of closing, which is the action that actually resolves it.
|
|
332
|
+
if (slot.ownerActive === true && slot.owner !== null) {
|
|
333
|
+
return `aw session-close --code ${slot.owner}`;
|
|
334
|
+
}
|
|
335
|
+
const confirm = slot.intact ? "" : " --confirm-no-producer";
|
|
336
|
+
return `aw claims recover ${slot.path}${confirm}`;
|
|
337
|
+
}
|
|
338
|
+
//# sourceMappingURL=claims-recovery.js.map
|