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