@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,154 @@
1
+ import { xURL, sameHandle, handlePattern, source } from "./platform-rules.js";
2
+ export class PlatformTargets {
3
+ #observed;
4
+ #open;
5
+ #browser;
6
+ #ready;
7
+ readAccount;
8
+ readFeedback;
9
+ readPerson;
10
+ readPost;
11
+ constructor(dependencies) {
12
+ const { observed, open, browser, ready } = dependencies;
13
+ this.#observed = observed;
14
+ this.#open = open;
15
+ this.#browser = browser;
16
+ this.#ready = ready;
17
+ this.readAccount = this.handleReadAccount.bind(this);
18
+ this.readFeedback = this.handleReadFeedback.bind(this);
19
+ this.readPerson = this.handleReadPerson.bind(this);
20
+ this.readPost = this.handleReadPost.bind(this);
21
+ }
22
+ async handleReadFeedback({ targets, replyLimit = 3, } = {}) {
23
+ if (!Array.isArray(targets) || targets.length < 1 || targets.length > 5)
24
+ throw new Error("反馈窗口须为1–5个已发布ID");
25
+ if (!Number.isInteger(replyLimit) || replyLimit < 0 || replyLimit > 3)
26
+ throw new Error("回复核验窗口须为0–3");
27
+ const feedback = [];
28
+ for (const target of targets) {
29
+ const value = await this.handleReadPost(target), post = value.posts?.find((p) => p.id === target.postId);
30
+ if (value.needsStateCheck || value.alerts?.length || !post) {
31
+ feedback.push({
32
+ id: target.postId,
33
+ missing: ["published-post-unconfirmed"],
34
+ });
35
+ continue;
36
+ }
37
+ const thread = await this.#observed({ view: "posts", limit: 5 });
38
+ const replies = [], unconfirmedReplyIds = [];
39
+ const targetIndex = (thread.posts ?? []).findIndex((p) => p.id === post.id);
40
+ const candidates = targetIndex < 0
41
+ ? []
42
+ : thread.posts.slice(targetIndex + 1, targetIndex + 1 + replyLimit);
43
+ for (const candidate of candidates) {
44
+ if (!candidate.primary || !candidate.id)
45
+ continue;
46
+ const url = new URL(candidate.primary, thread.page).href;
47
+ const detail = await this.#open({
48
+ url,
49
+ evidenceRef: {
50
+ page: thread.page,
51
+ observedAt: thread.observedAt,
52
+ publicId: candidate.id,
53
+ primary: candidate.primary,
54
+ },
55
+ }, {
56
+ view: "posts",
57
+ targetId: candidate.id,
58
+ replyTargetId: target.postId,
59
+ limit: 1,
60
+ });
61
+ const reply = detail.posts?.find((p) => p.id === candidate.id);
62
+ if (!detail.needsStateCheck &&
63
+ !detail.alerts?.length &&
64
+ reply?.replyToId === target.postId &&
65
+ reply.body &&
66
+ !reply.needsExpansion)
67
+ replies.push(source(reply, detail.page));
68
+ else
69
+ unconfirmedReplyIds.push(candidate.id);
70
+ }
71
+ feedback.push({
72
+ id: post.id,
73
+ author: post.author,
74
+ metrics: [...(post.buttons ?? []), ...(post.metrics ?? [])],
75
+ replies,
76
+ unconfirmedReplyIds,
77
+ pageComplete: false,
78
+ observedAt: value.observedAt,
79
+ });
80
+ }
81
+ return {
82
+ feedback,
83
+ fresh: true,
84
+ complete: feedback.every((f) => !f.missing),
85
+ pageComplete: false,
86
+ };
87
+ }
88
+ async handleReadPerson(target) {
89
+ if (!handlePattern.test(target.handle ?? ""))
90
+ throw new Error("无效已观察handle");
91
+ if (xURL(target.url).pathname.toLowerCase() !==
92
+ `/${target.handle}`.toLowerCase())
93
+ throw new Error("人物URL与handle不一致");
94
+ const value = await this.#open(target, {
95
+ view: "person",
96
+ handle: target.handle,
97
+ });
98
+ return sameHandle(value.handle, target.handle)
99
+ ? { ...value, url: target.url }
100
+ : {
101
+ ...value,
102
+ needsStateCheck: true,
103
+ missing: ["person-identity-mismatch"],
104
+ };
105
+ }
106
+ async handleReadPost(target) {
107
+ if (!/^\d+$/.test(target.postId ?? "") ||
108
+ xURL(target.url).pathname.match(/\/status\/(\d+)$/)?.[1] !== target.postId)
109
+ throw new Error("帖子URL与公开ID不一致");
110
+ let value = await this.#open(target, {
111
+ view: "posts",
112
+ targetId: target.postId,
113
+ });
114
+ if (!target.expand)
115
+ return value;
116
+ let posts = (value.posts ?? []).filter((p) => p.id === target.postId);
117
+ if (!value.needsStateCheck &&
118
+ !value.alerts?.length &&
119
+ posts.length === 1 &&
120
+ posts[0].needsExpansion) {
121
+ await this.#browser.act({
122
+ operation: "expandPost",
123
+ postId: target.postId,
124
+ });
125
+ value = await this.#ready({ view: "posts", targetId: target.postId });
126
+ posts = (value.posts ?? []).filter((p) => p.id === target.postId);
127
+ }
128
+ const missing = [...(value.missing ?? [])];
129
+ if (posts.length !== 1)
130
+ missing.push("unique-target-post-unconfirmed");
131
+ else {
132
+ if (!posts[0].body)
133
+ missing.push("exact-body-unconfirmed");
134
+ if (posts[0].needsExpansion)
135
+ missing.push("post-expansion-unconfirmed");
136
+ if (!posts[0].author)
137
+ missing.push("post-author-unconfirmed");
138
+ if (!Number.isFinite(Date.parse(posts[0].datetime)))
139
+ missing.push("absolute-time-unconfirmed");
140
+ }
141
+ return {
142
+ ...value,
143
+ posts,
144
+ complete: !missing.length && !value.needsStateCheck && !value.alerts?.length,
145
+ missing,
146
+ };
147
+ }
148
+ handleReadAccount() {
149
+ return this.#observed({ view: "account" });
150
+ }
151
+ }
152
+ export function createPlatformTargets(...args) {
153
+ return new PlatformTargets(...args);
154
+ }
@@ -0,0 +1,16 @@
1
+ import type { PlatformDependencies } from "./platform-dependencies.js";
2
+ export declare class PlatformWriting {
3
+ #private;
4
+ compose: (action: any) => Promise<any>;
5
+ findSubmission: (action: any) => Promise<any>;
6
+ submitFollow: (action: any) => Promise<any>;
7
+ submitPost: (action: any) => Promise<any>;
8
+ submitReply: (action: any) => Promise<any>;
9
+ constructor(dependencies: Pick<PlatformDependencies, "observed" | "browser" | "followStage" | "readPerson" | "open" | "clock" | "readPost">);
10
+ private handleCompose;
11
+ private handleFindSubmission;
12
+ private handleSubmitFollow;
13
+ private handleSubmitPost;
14
+ private handleSubmitReply;
15
+ }
16
+ export declare function createPlatformWriting(...args: ConstructorParameters<typeof PlatformWriting>): PlatformWriting;
@@ -0,0 +1,202 @@
1
+ import { xURL, sameHandle, sameText, samePublishedText, } from "./platform-rules.js";
2
+ export class PlatformWriting {
3
+ #observed;
4
+ #browser;
5
+ #followStage;
6
+ #readPerson;
7
+ #open;
8
+ #clock;
9
+ #readPost;
10
+ compose;
11
+ findSubmission;
12
+ submitFollow;
13
+ submitPost;
14
+ submitReply;
15
+ constructor(dependencies) {
16
+ const { observed, browser, followStage, readPerson, open, clock, readPost, } = dependencies;
17
+ this.#observed = observed;
18
+ this.#browser = browser;
19
+ this.#followStage = followStage;
20
+ this.#readPerson = readPerson;
21
+ this.#open = open;
22
+ this.#clock = clock;
23
+ this.#readPost = readPost;
24
+ this.compose = this.handleCompose.bind(this);
25
+ this.findSubmission = this.handleFindSubmission.bind(this);
26
+ this.submitFollow = this.handleSubmitFollow.bind(this);
27
+ this.submitPost = this.handleSubmitPost.bind(this);
28
+ this.submitReply = this.handleSubmitReply.bind(this);
29
+ }
30
+ async handleCompose(action) {
31
+ let value = await this.#observed({ view: "composer" });
32
+ if (value.needsStateCheck ||
33
+ value.alerts?.length ||
34
+ typeof value.editorText !== "string" ||
35
+ value.editorText.trim() !== "" ||
36
+ (action.kind === "reply" && value.replyToId !== action.postId))
37
+ throw new Error("编辑器不为空或回复上下文未绑定");
38
+ if (action.assets?.length)
39
+ await this.#browser.act({
40
+ operation: "attachAssets",
41
+ assets: action.assets,
42
+ });
43
+ await this.#browser.act({ operation: "fillComposer", text: action.text });
44
+ value = await this.#observed({ view: "composer" });
45
+ if (!sameText(value.editorText, action.text) ||
46
+ value.needsStateCheck ||
47
+ value.alerts?.length ||
48
+ (action.kind === "reply" && value.replyToId !== action.postId) ||
49
+ (action.accountHandle &&
50
+ !sameHandle(value.accountHandle, action.accountHandle)))
51
+ throw new Error(`提交前账号/正文/目标未核实 ${JSON.stringify({ textMatches: sameText(value.editorText, action.text), needsStateCheck: value.needsStateCheck, alerts: value.alerts, accountHandle: value.accountHandle, replyToId: value.replyToId })}`);
52
+ return this.#browser.act({
53
+ operation: action.kind === "reply" ? "submitReply" : "submitPost",
54
+ beforeSubmit: action.beforeSubmit,
55
+ ...(action.activation ? { activation: action.activation } : {}),
56
+ });
57
+ }
58
+ async handleFindSubmission(action) {
59
+ if (action.kind === "follow") {
60
+ let value = await this.#followStage(action, "verify-read", () => this.#readPerson(action));
61
+ if (!value.needsStateCheck) {
62
+ const absent = this.#browser.capabilities?.("refreshPerson") ?? [
63
+ "refreshPerson",
64
+ ];
65
+ if (absent.length)
66
+ return {
67
+ ...value,
68
+ verified: false,
69
+ missing: absent,
70
+ needsStateCheck: true,
71
+ };
72
+ await this.#followStage(action, "verify-refresh", () => this.#browser.act({
73
+ operation: "refreshPerson",
74
+ handle: action.handle,
75
+ url: action.url,
76
+ evidenceRef: action.evidenceRef,
77
+ }));
78
+ value = await this.#followStage(action, "verify-after-refresh", () => this.#readPerson(action));
79
+ }
80
+ return {
81
+ ...value,
82
+ verified: !value.needsStateCheck &&
83
+ !value.alerts?.length &&
84
+ value.relationship === "following" &&
85
+ (!action.accountHandle ||
86
+ sameHandle(value.accountHandle, action.accountHandle)),
87
+ publicId: null,
88
+ };
89
+ }
90
+ // A bounded window cannot prove absence. Reply receipts use the observed target conversation.
91
+ const observedReceipt = ["reply", "original"].includes(action.kind) &&
92
+ action.observedPublicURL &&
93
+ action.observedPublicEvidenceRef;
94
+ const receiptPath = observedReceipt
95
+ ? xURL(action.observedPublicURL).pathname.split("/")
96
+ : [];
97
+ if (observedReceipt &&
98
+ (receiptPath.length !== 4 ||
99
+ !sameHandle(receiptPath[1], action.accountHandle) ||
100
+ receiptPath[2] !== "status" ||
101
+ !/^[0-9]+$/.test(receiptPath[3])))
102
+ throw new Error("已观察回复回执须属于原账号");
103
+ const receiptTarget = observedReceipt
104
+ ? {
105
+ url: action.observedPublicURL,
106
+ evidenceRef: action.observedPublicEvidenceRef,
107
+ }
108
+ : action.kind === "reply" && action.url && action.evidenceRef
109
+ ? { url: action.url, evidenceRef: action.evidenceRef }
110
+ : { url: action.accountURL, evidenceRef: action.accountEvidenceRef };
111
+ let value = await this.#open(receiptTarget, { view: "posts", limit: 5 });
112
+ if (!(action.kind === "original" && observedReceipt) &&
113
+ !value.needsStateCheck &&
114
+ !(this.#browser.capabilities?.("refreshPosts") ?? ["refreshPosts"]).length) {
115
+ await this.#browser.act({
116
+ operation: "refreshPosts",
117
+ url: receiptTarget.url,
118
+ evidenceRef: receiptTarget.evidenceRef,
119
+ });
120
+ value = await this.#observed({ view: "posts", limit: 5 });
121
+ }
122
+ const matchesContent = (post) => sameHandle(post.author, action.accountHandle) &&
123
+ samePublishedText(post.body, action.text) &&
124
+ /^\d+$/.test(post.id ?? "") &&
125
+ (!action.assets?.length ||
126
+ action.assets.every((asset) => post.images?.some((image) => image.alt === asset.alt))) &&
127
+ !(action.beforeIds ?? []).includes(post.id) &&
128
+ Number.isFinite(Date.parse(post.datetime)) &&
129
+ Date.parse(post.datetime) >= Date.parse(action.intentAt) &&
130
+ Date.parse(post.datetime) <= this.#clock() + 5000 &&
131
+ !post.needsExpansion;
132
+ let matches = (value.posts ?? []).filter(matchesContent);
133
+ if (action.kind === "reply" &&
134
+ matches.length === 1 &&
135
+ matches[0].replyToId == null &&
136
+ matches[0].primary) {
137
+ // The candidate URL comes from this fresh public observation, not from a guessed ID path.
138
+ const candidate = matches[0];
139
+ const url = xURL(new URL(candidate.primary, xURL(value.page).origin).href);
140
+ if (url.pathname.toLowerCase() ===
141
+ `/${action.accountHandle}/status/${candidate.id}`.toLowerCase()) {
142
+ value = await this.#open({
143
+ url: url.href,
144
+ evidenceRef: {
145
+ page: value.page,
146
+ observedAt: value.observedAt,
147
+ publicId: candidate.id,
148
+ primary: candidate.primary,
149
+ },
150
+ }, {
151
+ view: "posts",
152
+ targetId: candidate.id,
153
+ replyTargetId: action.postId,
154
+ });
155
+ matches = (value.posts ?? []).filter(matchesContent);
156
+ }
157
+ }
158
+ if (action.kind === "reply")
159
+ matches = matches.filter((post) => post.replyToId === action.postId);
160
+ return {
161
+ ...value,
162
+ verified: !value.needsStateCheck && !value.alerts?.length && matches.length === 1,
163
+ publicId: matches.length === 1 ? matches[0].id : null,
164
+ missing: matches.length === 1 ? [] : ["unique-public-submission-unconfirmed"],
165
+ };
166
+ }
167
+ async handleSubmitFollow(action) {
168
+ const value = await this.#followStage(action, "submit-preflight-read", () => this.#readPerson(action));
169
+ if (value.needsStateCheck ||
170
+ value.alerts?.length ||
171
+ !["not-following", "follows-you"].includes(value.relationship) ||
172
+ (action.accountHandle &&
173
+ !sameHandle(value.accountHandle, action.accountHandle)))
174
+ throw new Error("fresh账号/人物关系不允许关注");
175
+ await this.#followStage(action, "submit-guard", () => action.beforeSubmit?.());
176
+ // A returned click is not a receipt from X; durable success still requires fresh verification.
177
+ const clicked = await this.#followStage(action, "click", () => this.#browser.act({ operation: "follow", handle: action.handle }));
178
+ try {
179
+ await this.#followStage(action, "click-ui-observation", () => this.#observed({ view: "person", handle: action.handle }));
180
+ }
181
+ catch {
182
+ /* The click may have committed; retain diagnostic failure and use the original fresh receipt path. */
183
+ }
184
+ return clicked;
185
+ }
186
+ async handleSubmitPost(action) {
187
+ await this.#open({ url: action.composeURL, evidenceRef: action.composeEvidenceRef }, { view: "composer" });
188
+ return this.handleCompose(action);
189
+ }
190
+ async handleSubmitReply(action) {
191
+ const value = await this.#readPost(action);
192
+ if (value.needsStateCheck ||
193
+ value.alerts?.length ||
194
+ !(value.posts ?? []).some((p) => p.id === action.postId))
195
+ throw new Error("回复主帖身份未核实");
196
+ await this.#browser.act({ operation: "openReply", postId: action.postId });
197
+ return this.handleCompose(action);
198
+ }
199
+ }
200
+ export function createPlatformWriting(...args) {
201
+ return new PlatformWriting(...args);
202
+ }
@@ -1,2 +1,23 @@
1
- export declare function xURL(value: any): any;
2
- export declare function createXPlatform(browser: any, { clock }?: any): any;
1
+ export { xURL } from "./platform-rules.js";
2
+ export declare class XPlatform {
3
+ #private;
4
+ preflight: ({ operation, input }: any) => any;
5
+ readCreator: (args: import("./types.js").CreatorOptions) => Promise<any>;
6
+ readAccount: () => Promise<any>;
7
+ readBookmarks: ({ folder, ...window }?: any) => Promise<any>;
8
+ searchSources: ({ query, limit, maxPages, processedIds, }?: any) => Promise<any>;
9
+ readFeedback: ({ targets, replyLimit }?: any) => Promise<any>;
10
+ readPerson: (target: any) => Promise<any>;
11
+ readPost: (target: any) => Promise<any>;
12
+ findSubmission: (action: any) => Promise<any>;
13
+ submitFollow: (action: any) => Promise<any>;
14
+ submitPost: (action: any) => Promise<any>;
15
+ submitReply: (action: any) => Promise<any>;
16
+ captureProof: ({ observation }: any) => {
17
+ followDiagnostics?: any[] | undefined;
18
+ capturedAt: string;
19
+ observation: any;
20
+ };
21
+ constructor(browser: any, { clock }?: any);
22
+ }
23
+ export declare function createXPlatform(...args: ConstructorParameters<typeof XPlatform>): any;