@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
@@ -1,7 +1,36 @@
1
- import { createAXHarness, readAX } from "./readers.js";
2
- import { xURL } from "./platform.js";
1
+ import { createAXHarness } from "./readers-ax-host.js";
2
+ import { readAX } from "./readers-ax-parser.js";
3
+ import { xURL } from "./platform-rules.js";
3
4
  import { serialized, missingMethods, plain, valueOf, } from "./browser-common.js";
4
5
  export function createNativeXBrowser(app) {
6
+ async function navigateNativeTarget(command) {
7
+ xURL(command.url);
8
+ if (!command.evidenceRef)
9
+ throw new Error("导航URL缺原观察证据");
10
+ await low.observe({ mode: "navigation" }).then((raw) => {
11
+ current = { raw };
12
+ });
13
+ let index = one((e) => /^文本栏 .*地址和搜索栏/.test(e.label));
14
+ await low
15
+ .act({
16
+ index,
17
+ operation: "fill",
18
+ value: command.url,
19
+ read: { mode: "navigation" },
20
+ })
21
+ .then((raw) => {
22
+ current = { raw };
23
+ });
24
+ index = one((e) => /^文本栏 .*地址和搜索栏/.test(e.label));
25
+ await low.act({
26
+ index,
27
+ operation: "pressKey",
28
+ value: "Return",
29
+ read: { mode: "navigation" },
30
+ });
31
+ current = null;
32
+ return;
33
+ }
5
34
  let latestAX;
6
35
  const delegated = {};
7
36
  for (const name of [
@@ -124,32 +153,7 @@ export function createNativeXBrowser(app) {
124
153
  observe,
125
154
  async act(command) {
126
155
  if (command.operation === "openObserved") {
127
- xURL(command.url);
128
- if (!command.evidenceRef)
129
- throw new Error("导航URL缺原观察证据");
130
- await low.observe({ mode: "navigation" }).then((raw) => {
131
- current = { raw };
132
- });
133
- let index = one((e) => /^文本栏 .*地址和搜索栏/.test(e.label));
134
- await low
135
- .act({
136
- index,
137
- operation: "fill",
138
- value: command.url,
139
- read: { mode: "navigation" },
140
- })
141
- .then((raw) => {
142
- current = { raw };
143
- });
144
- index = one((e) => /^文本栏 .*地址和搜索栏/.test(e.label));
145
- await low.act({
146
- index,
147
- operation: "pressKey",
148
- value: "Return",
149
- read: { mode: "navigation" },
150
- });
151
- current = null;
152
- return;
156
+ return navigateNativeTarget(command);
153
157
  }
154
158
  if (command.operation === "refreshPerson") {
155
159
  xURL(command.url);
package/dist/browser.js CHANGED
@@ -1,4 +1,7 @@
1
- import { readProfile, readPosts, readComposer, readMediaEditor, readNavigation, readMenu, } from "./readers.js";
1
+ import { readProfile } from "./readers-profile.js";
2
+ import { readPosts } from "./readers-posts.js";
3
+ import { readComposer, readMediaEditor } from "./readers-composer.js";
4
+ import { readNavigation, readMenu } from "./readers-navigation.js";
2
5
  export { createDOMXBrowser, readXNetworkDiagnostics } from "./browser-dom.js";
3
6
  export { createNativeXBrowser } from "./browser-native.js";
4
7
  // Read-only supplement: account identity, composer context and exact tweet bodies.
@@ -106,7 +109,8 @@ export function readXSurface({ view, posts = [], targetId, replyTargetId, } = {}
106
109
  const read = (node) => {
107
110
  if (node.nodeType === 3)
108
111
  return node.textContent ?? "";
109
- if (node.nodeType === 1 && node.tagName === "A") {
112
+ if (node.nodeType === 1 &&
113
+ node.tagName === "A") {
110
114
  const href = node.getAttribute("href") ?? "";
111
115
  if (/^\/\w{1,15}\/status\/\d+$/.test(href))
112
116
  return `https://x.com${href}`;
package/dist/creator.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readProfile } from "./readers.js";
1
+ import { readProfile } from "./readers-profile.js";
2
2
  import { readXObservation } from "./observation.js";
3
3
  // Read-only DOM metadata. Counts retain the platform's displayed precision.
4
4
  export function readCreatorMeta() {
@@ -0,0 +1,5 @@
1
+ import type { BoundaryRecord } from "./boundary.js";
2
+ import type { GrowthDependencies } from "./growth-dependencies.js";
3
+ export declare function createFollowBatch(dependencies: Pick<GrowthDependencies, "prepareActionsForCall" | "store" | "scope" | "stamp" | "clock" | "evidenceExists" | "executor" | "parallelRound" | "round">): {
4
+ followBatch: ({ taskId, handles, preparedActions }?: any) => Promise<BoundaryRecord>;
5
+ };
@@ -0,0 +1,168 @@
1
+ import { followWindows, same } from "./growth-constants.js";
2
+ export function createFollowBatch(dependencies) {
3
+ const { prepareActionsForCall, store, scope, stamp, clock, evidenceExists, executor, parallelRound, round, } = dependencies;
4
+ return {
5
+ followBatch: async ({ taskId, handles, preparedActions } = {}) => {
6
+ if (!Array.isArray(handles) ||
7
+ handles.length < 5 ||
8
+ handles.length > 10 ||
9
+ handles.some((h) => !/^[A-Za-z0-9_]{1,15}$/.test(h)) ||
10
+ new Set(handles.map((h) => h.toLowerCase())).size !==
11
+ handles.length)
12
+ throw new Error("正常关注子批需5~10位不同的已准备对象");
13
+ if (preparedActions?.some((a) => a.kind !== "follow" ||
14
+ !handles.some((h) => same(h, a.handle))))
15
+ throw new Error("batch-action-target-mismatch");
16
+ await prepareActionsForCall(taskId, preparedActions);
17
+ let batchId, resumedBatch = false;
18
+ const original = await store.transaction(async (snapshot) => {
19
+ if (!scope(snapshot.state).includes("follow"))
20
+ throw new Error("kind-outside-round-scope");
21
+ if (snapshot.state.batchId !== taskId ||
22
+ handles.some((h) => !snapshot.state.actions?.some((a) => a.kind === "follow" && same(a.handle, h))))
23
+ throw new Error("子批名单必须归原任务队列");
24
+ const next = structuredClone(snapshot.state);
25
+ next.followBatches ??= [];
26
+ const previous = next.followBatches.findLast((b) => b.status !== "SETTLED" &&
27
+ b.handles.length === handles.length &&
28
+ b.handles.every((h, i) => same(h, handles[i])));
29
+ resumedBatch = Boolean(previous);
30
+ batchId = previous?.id ?? `${taskId}-FOLLOW-${stamp()}`;
31
+ if (previous)
32
+ Object.assign(previous, {
33
+ status: "RUNNING",
34
+ lastResumedAt: stamp(),
35
+ });
36
+ else
37
+ next.followBatches.push({
38
+ id: batchId,
39
+ handles,
40
+ planned: handles.length,
41
+ startedAt: stamp(),
42
+ status: "RUNNING",
43
+ });
44
+ next.updatedAt = stamp();
45
+ return next;
46
+ });
47
+ const preparedCount = original.state.actions.filter((a) => a.kind === "follow" &&
48
+ a.status === "prepared" &&
49
+ handles.some((h) => same(h, a.handle))).length;
50
+ let eligibleAt = clock();
51
+ const previousGroup = original.state.execution?.followGroup;
52
+ const lastAttempt = (original.state.attempts ?? [])
53
+ .filter((a) => a.kind === "follow")
54
+ .sort((a, b) => Date.parse(a.at) - Date.parse(b.at))
55
+ .at(-1);
56
+ const lastAction = original.state.actions.find((a) => a.id === lastAttempt?.actionId);
57
+ const sameBatchRecovery = resumedBatch &&
58
+ lastAction?.status === "verified" &&
59
+ evidenceExists(lastAction.resultEvidence) &&
60
+ handles.some((h) => same(h, lastAction.handle));
61
+ if (preparedCount && previousGroup?.attempts && !sameBatchRecovery)
62
+ eligibleAt = Math.max(eligibleAt, Date.parse(previousGroup.lastAt) + 180000);
63
+ for (const [duration, limit] of followWindows) {
64
+ const recent = (original.state.attempts ?? [])
65
+ .filter((a) => a.kind === "follow" && Date.parse(a.at) > clock() - duration)
66
+ .sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
67
+ if (preparedCount > limit - recent.length)
68
+ eligibleAt = Math.max(eligibleAt, Date.parse(recent[recent.length - limit + preparedCount - 1].at) +
69
+ duration);
70
+ }
71
+ if (eligibleAt > clock()) {
72
+ const resumeAt = new Date(eligibleAt).toISOString();
73
+ await store.transaction((snapshot) => {
74
+ const next = structuredClone(snapshot.state);
75
+ Object.assign(next.followBatches.find((b) => b.id === batchId), { status: "WAITING", remaining: preparedCount, resumeAt });
76
+ next.updatedAt = stamp();
77
+ return next;
78
+ });
79
+ return {
80
+ taskId,
81
+ batchId,
82
+ planned: handles.length,
83
+ attempted: 0,
84
+ verifiedThisCall: 0,
85
+ unknownInBatch: original.state.actions.filter((a) => handles.some((h) => same(h, a.handle)) &&
86
+ ["unknown", "intent"].includes(a.status)).length,
87
+ remaining: preparedCount,
88
+ mayEnd: false,
89
+ decision: [
90
+ {
91
+ kind: "follow",
92
+ status: "waiting",
93
+ reason: "normal-batch-capacity-or-cooldown",
94
+ resumeAt,
95
+ },
96
+ ],
97
+ evidenceRef: store.path,
98
+ };
99
+ }
100
+ let result;
101
+ const exceptional = [];
102
+ for (const handle of handles) {
103
+ // A resumed batch submits only its remaining prepared members. Recovery
104
+ // stays in the read-only API; a frozen member must not disturb that path.
105
+ if (!original.state.actions.some((a) => a.kind === "follow" &&
106
+ same(a.handle, handle) &&
107
+ a.status === "prepared"))
108
+ continue;
109
+ result = await (executor ? parallelRound : round)({
110
+ taskId,
111
+ requestedKind: "follow",
112
+ selector: (a) => a.kind === "follow" && same(a.handle, handle),
113
+ });
114
+ const relevant = (result.decision ?? []).filter((d) => d.kind === "follow" ||
115
+ ["harness-boundary-error", "original-ledger-conflict"].includes(d.reason) ||
116
+ original.state.actions.some((a) => a.id === d.actionId && a.kind === "follow"));
117
+ exceptional.push(...relevant);
118
+ // A new unconfirmed submission is a diagnostic boundary, not permission
119
+ // to consume the rest of the batch on the same failing write path.
120
+ if (relevant.length)
121
+ break;
122
+ }
123
+ const after = await store.load();
124
+ const summary = summarizeFollowBatch(taskId, batchId, handles, after, original, result, exceptional, store.path);
125
+ await store.transaction((snapshot) => {
126
+ const next = structuredClone(snapshot.state), batch = next.followBatches.find((b) => b.id === batchId);
127
+ Object.assign(batch, {
128
+ attempted: (batch.attempted ?? 0) + summary.attempted,
129
+ verifiedTotal: (batch.verifiedTotal ?? 0) + summary.verifiedThisCall,
130
+ verifiedThisCall: summary.verifiedThisCall,
131
+ unknown: summary.unknownInBatch,
132
+ remaining: summary.remaining,
133
+ finishedAt: stamp(),
134
+ status: summary.remaining || summary.unknownInBatch ? "PARTIAL" : "SETTLED",
135
+ });
136
+ next.updatedAt = stamp();
137
+ return next;
138
+ });
139
+ return summary;
140
+ },
141
+ };
142
+ }
143
+ function summarizeFollowBatch(taskId, batchId, handles, after, original, result, exceptional, evidenceRef) {
144
+ return {
145
+ taskId,
146
+ batchId,
147
+ planned: handles.length,
148
+ attempted: (after.state.attempts ?? [])
149
+ .slice(original.state.attempts?.length ?? 0)
150
+ .filter((a) => a.kind === "follow" &&
151
+ handles.some((h) => same(after.state.actions.find((x) => x.id === a.actionId)?.handle, h))).length,
152
+ verifiedThisCall: after.state.actions.filter((a) => a.kind === "follow" &&
153
+ a.status === "verified" &&
154
+ handles.some((h) => same(h, a.handle)) &&
155
+ original.state.actions.find((old) => old.id === a.id)?.status !==
156
+ "verified").length,
157
+ unknownInBatch: after.state.actions.filter((a) => a.kind === "follow" &&
158
+ handles.some((h) => same(a.handle, h)) &&
159
+ ["unknown", "intent"].includes(a.status)).length,
160
+ remaining: after.state.actions.filter((a) => a.kind === "follow" &&
161
+ handles.some((h) => same(a.handle, h)) &&
162
+ a.status === "prepared").length,
163
+ actual: result?.actual,
164
+ decision: exceptional,
165
+ mayEnd: result?.mayEnd ?? false,
166
+ evidenceRef,
167
+ };
168
+ }
@@ -0,0 +1,5 @@
1
+ import type { BoundaryRecord } from "./boundary.js";
2
+ export declare const kinds: string[];
3
+ export declare const followWindows: number[][];
4
+ export declare const operations: BoundaryRecord;
5
+ export declare const same: (a: any, b: any) => boolean;
@@ -0,0 +1,13 @@
1
+ export const kinds = ["follow", "original", "reply"];
2
+ export const followWindows = [
3
+ [600000, 20],
4
+ [3600000, 100],
5
+ ];
6
+ export const operations = {
7
+ follow: "submitFollow",
8
+ original: "submitPost",
9
+ reply: "submitReply",
10
+ };
11
+ export const same = (a, b) => typeof a === "string" &&
12
+ typeof b === "string" &&
13
+ a.toLowerCase() === b.toLowerCase();
@@ -0,0 +1,33 @@
1
+ import type { BoundaryRecord } from "./boundary.js";
2
+ import type { RoundStore } from "./store.js";
3
+ /** Explicit instance dependencies; each owner accepts only its declared subset. */
4
+ export interface GrowthDependencies {
5
+ platform: BoundaryRecord;
6
+ store: RoundStore;
7
+ clock: () => number;
8
+ executor: BoundaryRecord | undefined;
9
+ stamp: () => string;
10
+ evidenceExists: (path: string) => boolean;
11
+ runtimeId: string;
12
+ session: {
13
+ claim: any;
14
+ running: boolean;
15
+ };
16
+ authorizedWindow: (state: any, kind: string) => boolean;
17
+ scope: (state: any) => any;
18
+ project: (state: any) => any;
19
+ load: (taskId: any) => Promise<any>;
20
+ save: (snapshot: any, state: any) => Promise<any>;
21
+ result: (snapshot: any, needs?: any) => any;
22
+ capability: (operation: any, input?: any) => any;
23
+ waitBudget: (state: any) => any;
24
+ guardInput: (state: any, action: any, independentExecution?: any) => any;
25
+ settleProof: (snapshot: any, action: any, observation: any, { preserveStatus }?: any) => Promise<any>;
26
+ execute: (snapshot: any, action: any) => Promise<any>;
27
+ parallelRound: ({ taskId, decision, selector, requestedKind, maxActions, }?: any) => Promise<any>;
28
+ round: ({ taskId, decision, selector, requestedKind }?: any) => Promise<any>;
29
+ publishIndependent: ({ taskId, draftPath, preparedAction, independentExecution, }: any) => Promise<any>;
30
+ releaseSession: (taskId?: any) => Promise<any>;
31
+ readTask: (operation: any, { mode, ...input }?: any) => Promise<any>;
32
+ prepareActionsForCall: (taskId: string, actions?: any[]) => Promise<void>;
33
+ }
@@ -0,0 +1,11 @@
1
+ import type { BoundaryRecord } from "./boundary.js";
2
+ import type { RoundSnapshot } from "./store.js";
3
+ interface ExecutionStep {
4
+ snapshot: RoundSnapshot;
5
+ need: BoundaryRecord | null;
6
+ }
7
+ import type { GrowthDependencies } from "./growth-dependencies.js";
8
+ export declare function createGrowthExecution(dependencies: Pick<GrowthDependencies, "capability" | "platform" | "settleProof" | "clock" | "save" | "guardInput" | "waitBudget" | "authorizedWindow" | "stamp" | "store" | "executor" | "session">): {
9
+ execute: (snapshot: any, action: any) => Promise<ExecutionStep>;
10
+ };
11
+ export {};
@@ -0,0 +1,315 @@
1
+ import { operations, same } from "./growth-constants.js";
2
+ export function createGrowthExecution(dependencies) {
3
+ const { capability, platform, settleProof, clock, save, guardInput, waitBudget, authorizedWindow, stamp, store, executor, session, } = dependencies;
4
+ async function checkExecutionIdentity(snapshot, action) {
5
+ const state = snapshot.state;
6
+ let own;
7
+ try {
8
+ own = await platform.readAccount();
9
+ }
10
+ catch (error) {
11
+ return {
12
+ snapshot,
13
+ need: {
14
+ status: "needsDecision",
15
+ actionId: action.id,
16
+ reason: "account-read-failed",
17
+ error: error.message.slice(0, 240),
18
+ },
19
+ };
20
+ }
21
+ if (own.needsStateCheck ||
22
+ own.alerts?.length ||
23
+ !same(own.accountHandle, state.accountHandle))
24
+ return {
25
+ snapshot,
26
+ need: {
27
+ status: "needsDecision",
28
+ actionId: action.id,
29
+ reason: "fresh-account-unconfirmed",
30
+ },
31
+ };
32
+ return null;
33
+ }
34
+ async function admitAction(snapshot, action) {
35
+ const state = snapshot.state;
36
+ const item = state.items.find((i) => i.kind === action.kind);
37
+ if (!item.actionable)
38
+ return {
39
+ snapshot,
40
+ need: {
41
+ status: "needsDecision",
42
+ actionId: action.id,
43
+ reason: item.reason ?? "original-kind-not-actionable",
44
+ },
45
+ };
46
+ const inputError = guardInput(state, action);
47
+ if (inputError)
48
+ return {
49
+ snapshot,
50
+ need: {
51
+ status: "needsDecision",
52
+ actionId: action.id,
53
+ reason: inputError,
54
+ },
55
+ };
56
+ const identity = await checkExecutionIdentity(snapshot, action);
57
+ if (identity)
58
+ return identity;
59
+ if (action.kind === "follow") {
60
+ const budget = waitBudget(state);
61
+ if (budget)
62
+ return { snapshot, need: { ...budget, actionId: action.id } };
63
+ let value;
64
+ try {
65
+ value = await platform.readPerson(action);
66
+ }
67
+ catch (error) {
68
+ return {
69
+ snapshot,
70
+ need: {
71
+ status: "needsDecision",
72
+ actionId: action.id,
73
+ reason: "person-read-failed",
74
+ error: error.message.slice(0, 240),
75
+ },
76
+ };
77
+ }
78
+ if (value.needsStateCheck ||
79
+ value.alerts?.length ||
80
+ value.relationship === "unknown" ||
81
+ !same(value.accountHandle, state.accountHandle))
82
+ return {
83
+ snapshot,
84
+ need: {
85
+ status: "needsDecision",
86
+ actionId: action.id,
87
+ reason: "fresh-person-unconfirmed",
88
+ },
89
+ };
90
+ if (value.blue === false)
91
+ return {
92
+ snapshot,
93
+ need: {
94
+ status: "needsDecision",
95
+ actionId: action.id,
96
+ reason: "fresh-blue-qualification-contradicts-review",
97
+ },
98
+ };
99
+ if (value.relationship === "following") {
100
+ const next = structuredClone(state);
101
+ next.actions.find((a) => a.id === action.id).status =
102
+ "preexisting";
103
+ return { snapshot: await save(snapshot, next), need: null };
104
+ }
105
+ }
106
+ else {
107
+ const hour = Number(new Intl.DateTimeFormat("en-GB", {
108
+ timeZone: "Asia/Shanghai",
109
+ hour: "2-digit",
110
+ hourCycle: "h23",
111
+ }).format(clock()));
112
+ if ((hour < 7 || hour >= (action.kind === "reply" ? 24 : 22)) &&
113
+ !authorizedWindow(state, action.kind))
114
+ return {
115
+ snapshot,
116
+ need: {
117
+ status: "needsDecision",
118
+ actionId: action.id,
119
+ reason: "outside-contract-write-window",
120
+ },
121
+ };
122
+ }
123
+ let beforeIds = [];
124
+ if (action.kind !== "follow") {
125
+ let baseline;
126
+ try {
127
+ baseline = await platform.findSubmission({
128
+ ...action,
129
+ intentAt: stamp(),
130
+ });
131
+ }
132
+ catch (error) {
133
+ return {
134
+ snapshot,
135
+ need: {
136
+ status: "needsDecision",
137
+ actionId: action.id,
138
+ reason: "publication-baseline-read-failed",
139
+ error: error.message.slice(0, 240),
140
+ },
141
+ };
142
+ }
143
+ if (baseline.needsStateCheck || baseline.alerts?.length)
144
+ return {
145
+ snapshot,
146
+ need: {
147
+ status: "needsDecision",
148
+ actionId: action.id,
149
+ reason: "publication-baseline-unconfirmed",
150
+ },
151
+ };
152
+ beforeIds = (baseline.posts ?? []).map((p) => p.id).filter(Boolean);
153
+ }
154
+ return { beforeIds };
155
+ }
156
+ async function execute(snapshot, action) {
157
+ const recovering = ["intent", "unknown"].includes(action.status);
158
+ const caps = capability(recovering ? "read" : operations[action.kind], action);
159
+ if (!caps.covered)
160
+ return {
161
+ snapshot,
162
+ need: { status: "needsCapability", actionId: action.id, ...caps },
163
+ };
164
+ // Recovery is read-only even outside the write window. Never retry an intent.
165
+ if (recovering) {
166
+ let observation;
167
+ try {
168
+ observation = await platform.findSubmission(action);
169
+ }
170
+ catch (error) {
171
+ observation = {
172
+ verified: false,
173
+ missing: ["dedup-read-failed"],
174
+ error: error.message.slice(0, 240),
175
+ };
176
+ }
177
+ snapshot = await settleProof(snapshot, action, observation);
178
+ return {
179
+ snapshot,
180
+ need: observation.verified
181
+ ? null
182
+ : {
183
+ status: "needsDecision",
184
+ actionId: action.id,
185
+ reason: "submission-unknown-do-not-resubmit",
186
+ accountAlert: Boolean(observation.alerts?.length),
187
+ },
188
+ };
189
+ }
190
+ if (snapshot.state.items
191
+ .find((i) => i.kind === action.kind)
192
+ .reason?.startsWith("harness:") &&
193
+ Date.parse(snapshot.state.items.find((i) => i.kind === action.kind).resumeAt) <= clock()) {
194
+ const next = structuredClone(snapshot.state), pending = next.items.find((i) => i.kind === action.kind);
195
+ pending.actionable = true;
196
+ delete pending.reason;
197
+ delete pending.resumeAt;
198
+ delete pending.resumeWhen;
199
+ delete pending.blockEvidence;
200
+ next.status = "RUNNING";
201
+ snapshot = await save(snapshot, next);
202
+ }
203
+ const admission = await admitAction(snapshot, action);
204
+ if ("snapshot" in admission)
205
+ return admission;
206
+ const { beforeIds } = admission;
207
+ const state = snapshot.state;
208
+ // Persist intent BEFORE any externally visible submission. Even a failed attempt consumes budget.
209
+ const next = structuredClone(state);
210
+ const entry = next.actions.find((a) => a.id === action.id);
211
+ entry.status = "intent";
212
+ entry.intentAt = stamp();
213
+ entry.beforeIds = beforeIds;
214
+ next.items.find((i) => i.kind === action.kind).unknown += 1;
215
+ next.attempts ??= [];
216
+ next.attempts.push({
217
+ actionId: action.id,
218
+ kind: action.kind,
219
+ at: entry.intentAt,
220
+ });
221
+ if (action.kind === "follow") {
222
+ next.execution ??= {};
223
+ const old = next.execution.followGroup;
224
+ next.execution.followGroup = {
225
+ attempts: old?.attempts < 10 && clock() < Date.parse(old.lastAt) + 180000
226
+ ? old.attempts + 1
227
+ : 1,
228
+ lastAt: entry.intentAt,
229
+ };
230
+ }
231
+ snapshot = await save(snapshot, next);
232
+ let observation, submissionMayHaveStarted = false;
233
+ try {
234
+ await platform[operations[action.kind]]({
235
+ ...entry,
236
+ beforeSubmit: async () => {
237
+ const latest = await store.load();
238
+ if (executor) {
239
+ const current = latest.state.actions.find((a) => a.id === entry.id);
240
+ if (latest.state.batchId !== snapshot.state.batchId ||
241
+ current?.claim?.token !== session.claim.token ||
242
+ JSON.stringify(current) !==
243
+ JSON.stringify(snapshot.state.actions.find((a) => a.id === entry.id)))
244
+ throw new Error("提交前本动作或claim变化");
245
+ }
246
+ else if (latest.revision !== snapshot.revision)
247
+ throw new Error("提交前原台账变化,保留intent并重核执行权");
248
+ const inputError = guardInput(latest.state, latest.state.actions.find((a) => a.id === entry.id));
249
+ if (inputError)
250
+ throw new Error(inputError);
251
+ const hour = Number(new Intl.DateTimeFormat("en-GB", {
252
+ timeZone: "Asia/Shanghai",
253
+ hour: "2-digit",
254
+ hourCycle: "h23",
255
+ }).format(clock()));
256
+ if ((hour < 7 || hour >= (action.kind === "reply" ? 24 : 22)) &&
257
+ !authorizedWindow(latest.state, action.kind))
258
+ throw new Error("实际提交时已越过运营窗口");
259
+ if (action.kind === "follow") {
260
+ const check = structuredClone(latest.state);
261
+ check.attempts = check.attempts.filter((a) => a.actionId !== entry.id);
262
+ if (check.execution.followGroup)
263
+ check.execution.followGroup.attempts -= 1;
264
+ if (waitBudget(check))
265
+ throw new Error("实际提交前关注预算不再可用");
266
+ }
267
+ submissionMayHaveStarted = true;
268
+ },
269
+ });
270
+ observation = await platform.findSubmission(entry);
271
+ }
272
+ catch (error) {
273
+ if (!submissionMayHaveStarted) {
274
+ const proof = await store.recordProof({
275
+ capturedAt: stamp(),
276
+ submissionStarted: false,
277
+ error: error.message.slice(0, 240),
278
+ });
279
+ const unsubmitted = structuredClone(snapshot.state), target = unsubmitted.actions.find((a) => a.id === entry.id);
280
+ target.status = "prepared";
281
+ target.preparationEvidence = proof;
282
+ unsubmitted.items.find((i) => i.kind === action.kind).unknown -= 1;
283
+ snapshot = await save(snapshot, unsubmitted);
284
+ return {
285
+ snapshot,
286
+ need: {
287
+ status: "needsDecision",
288
+ actionId: action.id,
289
+ reason: "pre-submit-preparation-failed",
290
+ evidenceRef: proof,
291
+ },
292
+ };
293
+ }
294
+ // A transport error does not distinguish a rejected write from a successful write.
295
+ observation = {
296
+ verified: false,
297
+ missing: ["submission-or-verification-failed"],
298
+ error: error.message.slice(0, 240),
299
+ };
300
+ }
301
+ snapshot = await settleProof(snapshot, entry, observation);
302
+ return {
303
+ snapshot,
304
+ need: observation.verified
305
+ ? null
306
+ : {
307
+ status: "needsDecision",
308
+ actionId: action.id,
309
+ reason: "submission-unknown-do-not-resubmit",
310
+ accountAlert: Boolean(observation.alerts?.length),
311
+ },
312
+ };
313
+ }
314
+ return { execute };
315
+ }
@@ -0,0 +1,4 @@
1
+ import type { GrowthDependencies } from "./growth-dependencies.js";
2
+ export declare function createParallelRound(dependencies: Pick<GrowthDependencies, "load" | "scope" | "result" | "store" | "executor" | "runtimeId" | "stamp" | "evidenceExists" | "execute" | "clock" | "releaseSession" | "session">): {
3
+ parallelRound: ({ taskId, decision, selector, requestedKind, maxActions, }?: any) => Promise<any>;
4
+ };