@nextclaw/x-harness 0.1.0 → 0.1.1

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.
Files changed (94) hide show
  1. package/README.md +2 -2
  2. package/cli/main.js +23 -11
  3. package/dist/api.js +53 -1
  4. package/dist/browser-dom-capabilities.d.ts +4 -0
  5. package/dist/browser-dom-capabilities.js +110 -0
  6. package/dist/browser-dom-dependencies.d.ts +16 -0
  7. package/dist/browser-dom-editing.d.ts +148 -0
  8. package/dist/browser-dom-editing.js +386 -0
  9. package/dist/browser-dom-navigation.d.ts +9 -0
  10. package/dist/browser-dom-navigation.js +165 -0
  11. package/dist/browser-dom-network.d.ts +1 -0
  12. package/dist/browser-dom-network.js +128 -0
  13. package/dist/browser-dom-submission.d.ts +2 -0
  14. package/dist/browser-dom-submission.js +82 -0
  15. package/dist/browser-dom.d.ts +1 -1
  16. package/dist/browser-dom.js +51 -802
  17. package/dist/browser-native.js +32 -28
  18. package/dist/browser.js +6 -2
  19. package/dist/creator.js +1 -1
  20. package/dist/growth-batch.d.ts +5 -0
  21. package/dist/growth-batch.js +168 -0
  22. package/dist/growth-constants.d.ts +5 -0
  23. package/dist/growth-constants.js +13 -0
  24. package/dist/growth-dependencies.d.ts +33 -0
  25. package/dist/growth-execution.d.ts +11 -0
  26. package/dist/growth-execution.js +315 -0
  27. package/dist/growth-parallel.d.ts +4 -0
  28. package/dist/growth-parallel.js +301 -0
  29. package/dist/growth-plan.d.ts +4 -0
  30. package/dist/growth-plan.js +207 -0
  31. package/dist/growth-policy.d.ts +9 -0
  32. package/dist/growth-policy.js +181 -0
  33. package/dist/growth-publishing.d.ts +5 -0
  34. package/dist/growth-publishing.js +240 -0
  35. package/dist/growth-reading.d.ts +4 -0
  36. package/dist/growth-reading.js +135 -0
  37. package/dist/growth-recovery.d.ts +6 -0
  38. package/dist/growth-recovery.js +158 -0
  39. package/dist/growth-state.d.ts +8 -0
  40. package/dist/growth-state.js +183 -0
  41. package/dist/growth-tasks.d.ts +17 -0
  42. package/dist/growth-tasks.js +101 -0
  43. package/dist/growth.d.ts +1 -0
  44. package/dist/growth.js +122 -1902
  45. package/dist/platform-capabilities.d.ts +4 -0
  46. package/dist/platform-capabilities.js +19 -0
  47. package/dist/platform-collections.d.ts +7 -0
  48. package/dist/platform-collections.js +223 -0
  49. package/dist/platform-dependencies.d.ts +43 -0
  50. package/dist/platform-diagnostics.d.ts +9 -0
  51. package/dist/platform-diagnostics.js +51 -0
  52. package/dist/platform-observation.d.ts +8 -0
  53. package/dist/platform-observation.js +75 -0
  54. package/dist/platform-rules.d.ts +17 -0
  55. package/dist/platform-rules.js +41 -0
  56. package/dist/platform-targets.d.ts +7 -0
  57. package/dist/platform-targets.js +129 -0
  58. package/dist/platform-writing.d.ts +8 -0
  59. package/dist/platform-writing.js +171 -0
  60. package/dist/platform.d.ts +1 -1
  61. package/dist/platform.js +56 -677
  62. package/dist/readers-actions.d.ts +2 -0
  63. package/dist/readers-actions.js +66 -0
  64. package/dist/readers-ax-host.d.ts +2 -0
  65. package/dist/readers-ax-host.js +125 -0
  66. package/dist/readers-ax-parser.d.ts +1 -0
  67. package/dist/readers-ax-parser.js +69 -0
  68. package/dist/readers-ax-program.d.ts +1 -0
  69. package/dist/readers-ax-tree.d.ts +19 -0
  70. package/dist/readers-ax-tree.js +110 -0
  71. package/dist/readers-ax-views.d.ts +87 -0
  72. package/dist/readers-ax-views.js +247 -0
  73. package/dist/readers-cli.d.ts +6 -0
  74. package/dist/readers-composer.d.ts +2 -0
  75. package/dist/readers-composer.js +131 -0
  76. package/dist/readers-navigation.d.ts +2 -0
  77. package/dist/readers-navigation.js +66 -0
  78. package/dist/readers-posts.d.ts +2 -0
  79. package/dist/readers-posts.js +141 -0
  80. package/dist/readers-profile.d.ts +3 -0
  81. package/dist/readers-profile.js +157 -0
  82. package/dist/readers.d.ts +9 -20
  83. package/dist/results.js +1 -1
  84. package/dist/round-state.js +2 -2
  85. package/dist/types.d.ts +10 -0
  86. package/generated/observation.mjs +2 -1
  87. package/package.json +1 -1
  88. package/skill/SKILL.md +17 -36
  89. package/skill/references/api.md +7 -34
  90. package/skill/references/reads.md +23 -0
  91. package/skill/references/recovery.md +25 -0
  92. package/skill/references/setup.md +23 -0
  93. package/skill/references/writes.md +51 -0
  94. package/dist/readers.js +0 -1045
@@ -0,0 +1,181 @@
1
+ import { xURL } from "./platform-rules.js";
2
+ import { kinds, followWindows, operations, same } from "./growth-constants.js";
3
+ export function createGrowthPolicy(dependencies) {
4
+ const { executor, clock, evidenceExists, platform, store } = dependencies;
5
+ // Manual user authorization applies only to the named executor and action kind.
6
+ const authorizedWindow = (state, kind) => {
7
+ const permit = state.executions?.[executor?.id]?.windowAuthorization;
8
+ const now = clock();
9
+ return Boolean(permit &&
10
+ permit.kind === kind &&
11
+ permit.executorId === executor?.id &&
12
+ permit.userAuthorized === true &&
13
+ evidenceExists(permit.evidenceRef) &&
14
+ Date.parse(permit.startsAt) <= now &&
15
+ now < Date.parse(permit.expiresAt));
16
+ };
17
+ const scope = (state) => executor?.scopeKinds ?? state.scopeKinds ?? kinds;
18
+ const project = (state) => executor
19
+ ? {
20
+ ...state,
21
+ scopeKinds: executor.scopeKinds,
22
+ scopeEvidenceRef: executor.evidenceRef,
23
+ status: state.executions?.[executor.id]?.status ?? "RUNNING",
24
+ stop: state.executions?.[executor.id]?.stop,
25
+ }
26
+ : state;
27
+ function capability(operation, input = {}) {
28
+ const kind = input.kind ??
29
+ {
30
+ followOne: "follow",
31
+ followBatch: "follow",
32
+ submitFollow: "follow",
33
+ publishPost: "original",
34
+ submitPost: "original",
35
+ replyToPost: "reply",
36
+ submitReply: "reply",
37
+ }[operation];
38
+ const required = [
39
+ "preflight",
40
+ "readAccount",
41
+ "findSubmission",
42
+ "captureProof",
43
+ ...(operation === "read" ? [] : [operations[kind]]),
44
+ ];
45
+ if (kind === "follow")
46
+ required.push("readPerson");
47
+ const missing = required.filter((name) => typeof platform?.[name] !== "function");
48
+ const storage = ["load", "save", "withLock", "recordProof"].filter((name) => typeof store?.[name] !==
49
+ "function");
50
+ if (operation === "followBatch" && typeof store?.transaction !== "function")
51
+ storage.push("transaction");
52
+ if (kind === "original" &&
53
+ operation !== "read" &&
54
+ typeof store?.fingerprint !== "function")
55
+ storage.push("fingerprint");
56
+ const lower = typeof platform?.preflight === "function"
57
+ ? platform.preflight({
58
+ operation: operation === "read" ? "read" : operations[kind],
59
+ input,
60
+ })
61
+ : { covered: false, missing: [] };
62
+ return {
63
+ covered: !missing.length && !storage.length && lower.covered,
64
+ missing: [
65
+ ...missing.map((n) => `platform.${n}`),
66
+ ...storage.map((n) => `store.${n}`),
67
+ ...(lower.missing ?? []),
68
+ ],
69
+ owner: missing.length
70
+ ? "scripts/x-harness-platform.mjs"
71
+ : storage.length
72
+ ? "scripts/x-harness-store.mjs"
73
+ : lower.owner,
74
+ };
75
+ }
76
+ function waitBudget(state) {
77
+ const now = clock();
78
+ const hour = Number(new Intl.DateTimeFormat("en-GB", {
79
+ timeZone: "Asia/Shanghai",
80
+ hour: "2-digit",
81
+ hourCycle: "h23",
82
+ }).format(now));
83
+ if (hour < 7 || hour >= 22)
84
+ return {
85
+ status: "needsDecision",
86
+ reason: "outside-contract-write-window",
87
+ };
88
+ const coverage = state.attemptsCoverage;
89
+ if (!coverage?.complete ||
90
+ !Number.isFinite(Date.parse(coverage.since)) ||
91
+ Date.parse(coverage.since) > now - 3600000 ||
92
+ !evidenceExists(coverage.evidenceRef))
93
+ return {
94
+ status: "needsDecision",
95
+ reason: "shared-attempt-history-incomplete",
96
+ };
97
+ const attempts = state.attempts ?? [];
98
+ if (attempts.some((a) => !kinds.includes(a.kind) || !Number.isFinite(Date.parse(a.at))))
99
+ return { status: "needsDecision", reason: "attempt-history-invalid" };
100
+ const follows = attempts.filter((a) => a.kind === "follow");
101
+ for (const [duration, limit] of followWindows) {
102
+ const recent = follows
103
+ .filter((a) => Date.parse(a.at) > now - duration)
104
+ .sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
105
+ if (recent.length >= limit)
106
+ return {
107
+ status: "waiting",
108
+ reason: "follow-rolling-budget",
109
+ resumeAt: new Date(Date.parse(recent[recent.length - limit].at) + duration).toISOString(),
110
+ };
111
+ }
112
+ const group = state.execution?.followGroup;
113
+ if (group?.attempts >= 10 && now < Date.parse(group.lastAt) + 180000)
114
+ return {
115
+ status: "waiting",
116
+ reason: "follow-group-cooldown",
117
+ resumeAt: new Date(Date.parse(group.lastAt) + 180000).toISOString(),
118
+ };
119
+ return null;
120
+ }
121
+ function guardInput(state, action, independentExecution = undefined) {
122
+ if (!independentExecution && !scope(state).includes(action.kind))
123
+ return "kind-outside-round-scope";
124
+ if (independentExecution) {
125
+ if (action.kind !== "original" ||
126
+ !independentExecution.tabId ||
127
+ !evidenceExists(independentExecution.evidenceRef) ||
128
+ !evidenceExists(independentExecution.scopeEvidenceRef))
129
+ return "independent-execution-proof-missing";
130
+ }
131
+ else if (!executor &&
132
+ (!state.execution?.exclusive || state.execution.owner !== "harness"))
133
+ return "exclusive-account-execution-unconfirmed";
134
+ if (!same(state.accountHandle, action.accountHandle))
135
+ return "account-identity-mismatch";
136
+ if (!evidenceExists(action.accountEvidenceRef) ||
137
+ xURL(action.accountURL).pathname.toLowerCase() !==
138
+ `/${state.accountHandle}`.toLowerCase())
139
+ return "own-profile-proof-missing";
140
+ if (!action.quality?.approved ||
141
+ !evidenceExists(action.quality.evidenceRef))
142
+ return "quality-review-required";
143
+ if (action.kind === "follow") {
144
+ if (!same(action.quality.handle, action.handle) ||
145
+ action.quality.blue !== true ||
146
+ action.quality.chinese !== true)
147
+ return "follow-qualification-required";
148
+ if (!evidenceExists(action.evidenceRef))
149
+ return "person-observation-proof-missing";
150
+ }
151
+ else {
152
+ if (!action.text || action.quality.approvedText !== action.text)
153
+ return "exact-text-quality-review-required";
154
+ if (action.kind === "original" &&
155
+ (!action.draftPath || !evidenceExists(action.draftPath)))
156
+ return "draft-source-missing";
157
+ if (action.kind === "original" &&
158
+ (!action.draftPath.endsWith(".post.md") ||
159
+ action.quality.draftFingerprint !==
160
+ store.fingerprint(action.draftPath)))
161
+ return "draft-changed-since-quality-review";
162
+ if (action.kind === "original" &&
163
+ (!evidenceExists(action.composeEvidenceRef) ||
164
+ xURL(action.composeURL).pathname !== "/compose/post"))
165
+ return "observed-composer-proof-missing";
166
+ if (action.kind === "reply" &&
167
+ (!/^\d+$/.test(action.postId ?? "") ||
168
+ !evidenceExists(action.evidenceRef)))
169
+ return "reply-target-proof-missing";
170
+ }
171
+ return null;
172
+ }
173
+ return {
174
+ authorizedWindow,
175
+ scope,
176
+ project,
177
+ capability,
178
+ waitBudget,
179
+ guardInput,
180
+ };
181
+ }
@@ -0,0 +1,5 @@
1
+ import type { GrowthDependencies } from "./growth-dependencies.js";
2
+ export declare function createIndependentPublishing(dependencies: Pick<GrowthDependencies, "clock" | "evidenceExists" | "capability" | "store" | "load" | "guardInput" | "save" | "platform" | "stamp" | "settleProof">): {
3
+ publishIndependent: ({ taskId, draftPath, preparedAction, independentExecution, }: any) => Promise<any>;
4
+ prepareActionsForCall: (taskId: string, actions?: any[]) => Promise<void>;
5
+ };
@@ -0,0 +1,240 @@
1
+ import { kinds, same } from "./growth-constants.js";
2
+ export function createIndependentPublishing(dependencies) {
3
+ const { clock, evidenceExists, capability, store, load, guardInput, save, platform, stamp, settleProof, } = dependencies;
4
+ // Independent tabs share only short ledger transactions, never a browser-operation lock.
5
+ async function publishIndependent({ taskId, draftPath, preparedAction, independentExecution, }) {
6
+ let action, token, submissionStarted = false;
7
+ const inWindow = () => {
8
+ const hour = Number(new Intl.DateTimeFormat("en-GB", {
9
+ timeZone: "Asia/Shanghai",
10
+ hour: "2-digit",
11
+ hourCycle: "h23",
12
+ }).format(clock()));
13
+ const permit = independentExecution?.windowAuthorization;
14
+ return ((hour >= 7 && hour < 22) ||
15
+ Boolean(permit?.userAuthorized === true &&
16
+ permit.actionId === (action?.id ?? preparedAction?.id) &&
17
+ evidenceExists(permit.evidenceRef) &&
18
+ clock() < Date.parse(permit.expiresAt)));
19
+ };
20
+ try {
21
+ const caps = capability("submitPost", preparedAction ?? {});
22
+ if (!caps.covered)
23
+ return { status: "needsCapability", ...caps };
24
+ if (!inWindow())
25
+ throw new Error("outside-contract-write-window");
26
+ await store.withLock(async () => {
27
+ const snapshot = await load(taskId), state = structuredClone(snapshot.state);
28
+ state.actions ??= [];
29
+ action = state.actions.find((a) => a.kind === "original" && a.draftPath === draftPath);
30
+ if (!action) {
31
+ if (!preparedAction?.id ||
32
+ preparedAction.kind !== "original" ||
33
+ preparedAction.status !== "prepared" ||
34
+ preparedAction.draftPath !== draftPath ||
35
+ state.actions.some((a) => a.id === preparedAction.id))
36
+ throw new Error("unique-prepared-original-required");
37
+ const error = guardInput(state, preparedAction, independentExecution);
38
+ if (error)
39
+ throw new Error(error);
40
+ action = structuredClone(preparedAction);
41
+ state.actions.push(action);
42
+ await save(snapshot, state);
43
+ }
44
+ if (action.status !== "prepared")
45
+ throw new Error("existing-submission-do-not-resubmit");
46
+ if (action.claim && store.ownerAlive(action.claim.pid))
47
+ throw new Error("same-action-claimed");
48
+ const error = guardInput(state, action, independentExecution);
49
+ if (error)
50
+ throw new Error(error);
51
+ });
52
+ const own = await platform.readAccount();
53
+ if (own.needsStateCheck ||
54
+ own.alerts?.length ||
55
+ !same(own.accountHandle, action.accountHandle))
56
+ throw new Error("fresh-account-unconfirmed");
57
+ const baseline = await platform.findSubmission({
58
+ ...action,
59
+ intentAt: stamp(),
60
+ });
61
+ if (baseline.needsStateCheck || baseline.alerts?.length)
62
+ throw new Error("publication-baseline-unconfirmed");
63
+ token = `${action.id}:${stamp()}:${Math.random()}`;
64
+ await store.withLock(async () => {
65
+ const snapshot = await load(taskId), state = structuredClone(snapshot.state);
66
+ const target = state.actions.find((a) => a.id === action.id), item = state.items.find((i) => i.kind === "original");
67
+ if (target?.status !== "prepared" ||
68
+ (target.claim && store.ownerAlive(target.claim.pid)))
69
+ throw new Error("action-already-claimed-do-not-resubmit");
70
+ const error = guardInput(state, target, independentExecution);
71
+ if (error)
72
+ throw new Error(error);
73
+ if (!inWindow() || item.verified + item.unknown >= item.planned)
74
+ throw new Error("window-or-original-capacity-unavailable");
75
+ Object.assign(target, {
76
+ status: "intent",
77
+ intentAt: stamp(),
78
+ beforeIds: (baseline.posts ?? [])
79
+ .map((p) => p.id)
80
+ .filter(Boolean),
81
+ claimToken: token,
82
+ independentExecution,
83
+ claim: {
84
+ actionId: target.id,
85
+ token,
86
+ pid: store.processId,
87
+ tabId: independentExecution.tabId,
88
+ acquiredAt: stamp(),
89
+ },
90
+ });
91
+ item.unknown += 1;
92
+ state.attempts ??= [];
93
+ state.attempts.push({
94
+ actionId: target.id,
95
+ kind: "original",
96
+ at: target.intentAt,
97
+ });
98
+ await save(snapshot, state);
99
+ action = structuredClone(target);
100
+ });
101
+ let observation;
102
+ try {
103
+ await platform.submitPost({
104
+ ...action,
105
+ beforeSubmit: async () => {
106
+ const latest = await load(taskId), target = latest.state.actions.find((a) => a.id === action.id);
107
+ if (target?.status !== "intent" ||
108
+ target.claimToken !== token ||
109
+ !inWindow())
110
+ throw new Error("action-claim-or-window-changed");
111
+ const error = guardInput(latest.state, target, independentExecution);
112
+ if (error)
113
+ throw new Error(error);
114
+ submissionStarted = true;
115
+ },
116
+ });
117
+ observation = await platform.findSubmission(action);
118
+ }
119
+ catch (error) {
120
+ if (!submissionStarted)
121
+ throw error;
122
+ observation = {
123
+ verified: false,
124
+ missing: ["submission-or-verification-failed"],
125
+ error: error.message.slice(0, 240),
126
+ };
127
+ }
128
+ return await store.withLock(async () => {
129
+ const snapshot = await load(taskId), target = snapshot.state.actions.find((a) => a.id === action.id);
130
+ if (target?.claimToken !== token || target.status !== "intent")
131
+ throw new Error("action-settlement-conflict");
132
+ await settleProof(snapshot, target, observation, {
133
+ preserveStatus: true,
134
+ });
135
+ return {
136
+ status: observation.verified ? "verified" : "unknown",
137
+ taskId,
138
+ actionId: action.id,
139
+ publicId: observation.publicId,
140
+ evidenceRef: store.path,
141
+ mayEnd: false,
142
+ };
143
+ });
144
+ }
145
+ catch (error) {
146
+ if (token && !submissionStarted)
147
+ await store.withLock(async () => {
148
+ const snapshot = await load(taskId), state = structuredClone(snapshot.state), target = state.actions.find((a) => a.id === action.id);
149
+ if (target?.claimToken !== token || target.status !== "intent")
150
+ return;
151
+ target.status = "prepared";
152
+ target.preparationEvidence = await store.recordProof({
153
+ observedAt: stamp(),
154
+ submissionStarted: false,
155
+ error: error.message.slice(0, 240),
156
+ });
157
+ state.items.find((i) => i.kind === "original").unknown -= 1;
158
+ await save(snapshot, state);
159
+ });
160
+ return {
161
+ status: "needsDecision",
162
+ taskId,
163
+ mayEnd: false,
164
+ decision: [
165
+ {
166
+ reason: error.code === "CONFLICT"
167
+ ? "original-ledger-conflict"
168
+ : error.message.slice(0, 240),
169
+ },
170
+ ],
171
+ };
172
+ }
173
+ finally {
174
+ if (token)
175
+ await store.transaction((snapshot) => {
176
+ if (snapshot.state.batchId !== taskId)
177
+ return null;
178
+ const next = structuredClone(snapshot.state), target = next.actions.find((a) => a.id === action.id);
179
+ if (target?.claim?.token !== token)
180
+ return null;
181
+ delete target.claim;
182
+ return next;
183
+ });
184
+ }
185
+ }
186
+ // Stable prepared actions are adopted in the original ledger, never in a second queue.
187
+ async function prepareActionsForCall(taskId, actions = []) {
188
+ if (!actions.length)
189
+ return;
190
+ await store.transaction(async (snapshot) => {
191
+ if (snapshot.state.batchId !== taskId)
192
+ throw new Error("original-task-id-required");
193
+ const next = structuredClone(snapshot.state);
194
+ next.actions ??= [];
195
+ const ids = new Set();
196
+ for (const action of actions) {
197
+ if (!action?.id ||
198
+ ids.has(action.id) ||
199
+ action.status !== "prepared" ||
200
+ !kinds.includes(action.kind))
201
+ throw new Error("unique-prepared-action-required");
202
+ ids.add(action.id);
203
+ const inputError = guardInput(next, action);
204
+ if (inputError)
205
+ throw new Error(inputError);
206
+ const existing = next.actions.find((a) => a.id === action.id);
207
+ if (existing) {
208
+ const fields = [
209
+ "kind",
210
+ "accountHandle",
211
+ "handle",
212
+ "postId",
213
+ "text",
214
+ "draftPath",
215
+ ];
216
+ if (fields.some((field) => existing[field] !== action[field]))
217
+ throw new Error("original-action-input-changed");
218
+ continue; // intent/unknown/verified stay untouched and follow their original recovery.
219
+ }
220
+ const duplicate = next.actions.some((a) => a.kind === action.kind &&
221
+ (action.kind === "follow"
222
+ ? same(a.handle, action.handle)
223
+ : action.kind === "original"
224
+ ? a.draftPath === action.draftPath
225
+ : a.postId === action.postId && a.text === action.text));
226
+ if (duplicate)
227
+ throw new Error("business-object-already-prepared");
228
+ const item = next.items.find((i) => i.kind === action.kind);
229
+ const queued = next.actions.filter((a) => a.kind === action.kind &&
230
+ !["skipped", "preexisting"].includes(a.status)).length;
231
+ if (!item || queued >= item.planned)
232
+ throw new Error("prepared-actions-exceed-original-target");
233
+ next.actions.push(structuredClone(action));
234
+ }
235
+ next.updatedAt = stamp();
236
+ return next;
237
+ });
238
+ }
239
+ return { publishIndependent, prepareActionsForCall };
240
+ }
@@ -0,0 +1,4 @@
1
+ import type { GrowthDependencies } from "./growth-dependencies.js";
2
+ export declare function createGrowthReading(dependencies: Pick<GrowthDependencies, "platform" | "executor" | "store" | "runtimeId" | "stamp" | "releaseSession" | "session">): {
3
+ readTask: (operation: any, { mode, ...input }?: any) => Promise<any>;
4
+ };
@@ -0,0 +1,135 @@
1
+ export function createGrowthReading(dependencies) {
2
+ const { platform, executor, store, runtimeId, stamp, releaseSession, session, } = dependencies;
3
+ const readCache = new Map();
4
+ async function readTask(operation, { mode = "receipt", ...input } = {}) {
5
+ if (!["decision", "receipt", "delta"].includes(mode))
6
+ throw new Error("读取模式须为decision/receipt/delta");
7
+ if (session.running)
8
+ return {
9
+ status: "needsDecision",
10
+ complete: false,
11
+ reason: "same-tab-operation-running",
12
+ };
13
+ const caps = platform.preflight({
14
+ operation: ["readAccount", "readPerson", "readPost"].includes(operation)
15
+ ? "read"
16
+ : operation,
17
+ input,
18
+ });
19
+ if (!caps.covered || typeof platform[operation] !== "function")
20
+ return {
21
+ status: "needsCapability",
22
+ ...caps,
23
+ missing: [
24
+ ...(caps.missing ?? []),
25
+ ...(typeof platform[operation] === "function"
26
+ ? []
27
+ : [`platform.${operation}`]),
28
+ ],
29
+ };
30
+ session.running = true;
31
+ try {
32
+ if (executor)
33
+ await store.transaction((latest) => {
34
+ const next = structuredClone(latest.state);
35
+ next.executions ??= {};
36
+ if (Object.values(next.executions).some((e) => (e.tabId === executor.tabId || e.id === executor.id) &&
37
+ e.runtimeId !== runtimeId &&
38
+ (e.activity || e.actionId) &&
39
+ store.ownerAlive(e.pid)))
40
+ throw new Error("same-tab-or-executor-claimed");
41
+ next.executions[executor.id] = {
42
+ ...next.executions[executor.id],
43
+ ...executor,
44
+ runtimeId,
45
+ pid: store.processId,
46
+ activity: operation,
47
+ status: next.executions[executor.id]?.status ?? "RUNNING",
48
+ updatedAt: stamp(),
49
+ };
50
+ next.updatedAt = stamp();
51
+ return next;
52
+ });
53
+ const value = await platform[operation](input);
54
+ const evidenceRef = await store.recordProof(await platform.captureProof({ observation: value }));
55
+ const status = value.status ??
56
+ (value.complete === false ||
57
+ value.needsStateCheck ||
58
+ value.alerts?.length
59
+ ? "needsDecision"
60
+ : "observed");
61
+ const receipt = {
62
+ status,
63
+ fresh: true,
64
+ evidenceRef,
65
+ complete: status === "observed" && (value.complete ?? !value.needsStateCheck),
66
+ ...((value.sources ?? value.feedback)
67
+ ? { ids: (value.sources ?? value.feedback).map((p) => p.id) }
68
+ : {}),
69
+ ...(operation === "readAccount"
70
+ ? {
71
+ accountHandle: value.accountHandle ?? null,
72
+ accountURL: value.accountURL ?? null,
73
+ }
74
+ : {}),
75
+ ...(value.missing?.length ? { missing: value.missing } : {}),
76
+ };
77
+ if (mode === "receipt")
78
+ return receipt;
79
+ const collection = value.sources
80
+ ? "sources"
81
+ : value.feedback
82
+ ? "feedback"
83
+ : null;
84
+ if (mode === "delta" && collection) {
85
+ const unchangedIds = [], changed = [];
86
+ for (const entry of value[collection]) {
87
+ const key = JSON.stringify([operation, input, entry.id]);
88
+ const normalized = JSON.stringify(entry, (name, val) => name === "observedAt" ? undefined : val);
89
+ if (readCache.get(key) === normalized)
90
+ unchangedIds.push(entry.id);
91
+ else
92
+ changed.push(entry);
93
+ readCache.set(key, normalized);
94
+ }
95
+ return { ...receipt, [collection]: changed, unchangedIds };
96
+ }
97
+ return mode === "delta" ? receipt : { ...value, ...receipt };
98
+ }
99
+ catch (error) {
100
+ const category = /connection|transport|interrupted|extension/i.test(error.message)
101
+ ? "browser-connection"
102
+ : "ui-not-confirmed";
103
+ const code = error.code === "CONFLICT"
104
+ ? "ledger-conflict"
105
+ : /same-tab-or-executor-claimed/.test(error.message)
106
+ ? "tab-busy"
107
+ : category === "browser-connection"
108
+ ? "connection-failed"
109
+ : "observation-unconfirmed";
110
+ const diagnostic = { operation, category, code };
111
+ const evidenceRef = await store.recordProof({
112
+ capturedAt: stamp(),
113
+ diagnostic: { ...diagnostic, error: error.message },
114
+ });
115
+ return {
116
+ status: "needsDecision",
117
+ fresh: false,
118
+ complete: false,
119
+ evidenceRef,
120
+ diagnostic: mode === "decision"
121
+ ? { ...diagnostic, error: error.message.slice(0, 240) }
122
+ : diagnostic,
123
+ };
124
+ }
125
+ finally {
126
+ try {
127
+ await releaseSession();
128
+ }
129
+ finally {
130
+ session.running = false;
131
+ }
132
+ }
133
+ }
134
+ return { readTask };
135
+ }
@@ -0,0 +1,6 @@
1
+ import type { GrowthDependencies } from "./growth-dependencies.js";
2
+ export declare function createGrowthRecovery(dependencies: Pick<GrowthDependencies, "capability" | "platform" | "load" | "evidenceExists" | "clock" | "store" | "settleProof" | "executor" | "parallelRound" | "round" | "readTask">): {
3
+ verifyPublishedPost({ taskId, draftPath, publicURL, evidenceRef, sourceLink, }: any): Promise<any>;
4
+ verifySubmission: ({ taskId, actionId }: any) => Promise<any>;
5
+ verifyFollow: (target: any) => Promise<any>;
6
+ };