@ixo/openhands-sdk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/dist/cjs/chain/entityShape.d.ts +208 -0
- package/dist/cjs/chain/entityShape.d.ts.map +1 -0
- package/dist/cjs/chain/entityShape.js +201 -0
- package/dist/cjs/chain/entityShape.js.map +1 -0
- package/dist/cjs/chain/env.d.ts +73 -6
- package/dist/cjs/chain/env.d.ts.map +1 -1
- package/dist/cjs/chain/env.js +156 -14
- package/dist/cjs/chain/env.js.map +1 -1
- package/dist/cjs/config.d.ts +29 -3
- package/dist/cjs/config.d.ts.map +1 -1
- package/dist/cjs/config.js +7 -2
- package/dist/cjs/config.js.map +1 -1
- package/dist/cjs/data/agent.d.ts.map +1 -1
- package/dist/cjs/data/agent.js +5 -4
- package/dist/cjs/data/agent.js.map +1 -1
- package/dist/cjs/data/install.d.ts +36 -7
- package/dist/cjs/data/install.d.ts.map +1 -1
- package/dist/cjs/data/install.js +54 -13
- package/dist/cjs/data/install.js.map +1 -1
- package/dist/cjs/data/queue.d.ts.map +1 -1
- package/dist/cjs/data/queue.js +3 -2
- package/dist/cjs/data/queue.js.map +1 -1
- package/dist/cjs/data/receipt.d.ts.map +1 -1
- package/dist/cjs/data/receipt.js +6 -2
- package/dist/cjs/data/receipt.js.map +1 -1
- package/dist/cjs/data/threads.d.ts.map +1 -1
- package/dist/cjs/data/threads.js +8 -3
- package/dist/cjs/data/threads.js.map +1 -1
- package/dist/cjs/geo/geohash.d.ts +14 -0
- package/dist/cjs/geo/geohash.d.ts.map +1 -0
- package/dist/cjs/geo/geohash.js +64 -0
- package/dist/cjs/geo/geohash.js.map +1 -0
- package/dist/cjs/rubric/marketPlan.d.ts +144 -0
- package/dist/cjs/rubric/marketPlan.d.ts.map +1 -0
- package/dist/cjs/rubric/marketPlan.js +319 -0
- package/dist/cjs/rubric/marketPlan.js.map +1 -0
- package/dist/cjs/rubric/observation.d.ts +53 -0
- package/dist/cjs/rubric/observation.d.ts.map +1 -0
- package/dist/cjs/rubric/observation.js +38 -0
- package/dist/cjs/rubric/observation.js.map +1 -0
- package/dist/cjs/rubric/pingLink.d.ts +45 -0
- package/dist/cjs/rubric/pingLink.d.ts.map +1 -0
- package/dist/cjs/rubric/pingLink.js +74 -0
- package/dist/cjs/rubric/pingLink.js.map +1 -0
- package/dist/cjs/rubric/pingMatch.d.ts +73 -0
- package/dist/cjs/rubric/pingMatch.d.ts.map +1 -0
- package/dist/cjs/rubric/pingMatch.js +65 -0
- package/dist/cjs/rubric/pingMatch.js.map +1 -0
- package/dist/cjs/rubric/terms.d.ts +8 -0
- package/dist/cjs/rubric/terms.d.ts.map +1 -1
- package/dist/cjs/rubric/terms.js +2 -0
- package/dist/cjs/rubric/terms.js.map +1 -1
- package/dist/esm/chain/entityShape.d.ts +208 -0
- package/dist/esm/chain/entityShape.d.ts.map +1 -0
- package/dist/esm/chain/entityShape.js +184 -0
- package/dist/esm/chain/entityShape.js.map +1 -0
- package/dist/esm/chain/env.d.ts +73 -6
- package/dist/esm/chain/env.d.ts.map +1 -1
- package/dist/esm/chain/env.js +146 -12
- package/dist/esm/chain/env.js.map +1 -1
- package/dist/esm/config.d.ts +29 -3
- package/dist/esm/config.d.ts.map +1 -1
- package/dist/esm/config.js +7 -3
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/data/agent.d.ts.map +1 -1
- package/dist/esm/data/agent.js +5 -4
- package/dist/esm/data/agent.js.map +1 -1
- package/dist/esm/data/install.d.ts +36 -7
- package/dist/esm/data/install.d.ts.map +1 -1
- package/dist/esm/data/install.js +52 -12
- package/dist/esm/data/install.js.map +1 -1
- package/dist/esm/data/queue.d.ts.map +1 -1
- package/dist/esm/data/queue.js +3 -2
- package/dist/esm/data/queue.js.map +1 -1
- package/dist/esm/data/receipt.d.ts.map +1 -1
- package/dist/esm/data/receipt.js +6 -2
- package/dist/esm/data/receipt.js.map +1 -1
- package/dist/esm/data/threads.d.ts.map +1 -1
- package/dist/esm/data/threads.js +8 -3
- package/dist/esm/data/threads.js.map +1 -1
- package/dist/esm/geo/geohash.d.ts +14 -0
- package/dist/esm/geo/geohash.d.ts.map +1 -0
- package/dist/esm/geo/geohash.js +60 -0
- package/dist/esm/geo/geohash.js.map +1 -0
- package/dist/esm/rubric/marketPlan.d.ts +144 -0
- package/dist/esm/rubric/marketPlan.d.ts.map +1 -0
- package/dist/esm/rubric/marketPlan.js +305 -0
- package/dist/esm/rubric/marketPlan.js.map +1 -0
- package/dist/esm/rubric/observation.d.ts +53 -0
- package/dist/esm/rubric/observation.d.ts.map +1 -0
- package/dist/esm/rubric/observation.js +33 -0
- package/dist/esm/rubric/observation.js.map +1 -0
- package/dist/esm/rubric/pingLink.d.ts +45 -0
- package/dist/esm/rubric/pingLink.d.ts.map +1 -0
- package/dist/esm/rubric/pingLink.js +69 -0
- package/dist/esm/rubric/pingLink.js.map +1 -0
- package/dist/esm/rubric/pingMatch.d.ts +73 -0
- package/dist/esm/rubric/pingMatch.d.ts.map +1 -0
- package/dist/esm/rubric/pingMatch.js +60 -0
- package/dist/esm/rubric/pingMatch.js.map +1 -0
- package/dist/esm/rubric/terms.d.ts +8 -0
- package/dist/esm/rubric/terms.d.ts.map +1 -1
- package/dist/esm/rubric/terms.js +2 -0
- package/dist/esm/rubric/terms.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Ask the World: a prediction market turned into a plan of checks people can
|
|
4
|
+
* do in public places (docs/verified-work-ask-the-world-plan.md).
|
|
5
|
+
*
|
|
6
|
+
* A **plan** is a frozen document: the market it informs, the checks, where
|
|
7
|
+
* each happens and how many answers each place buys, what each check beats,
|
|
8
|
+
* and one line on what the plan cannot tell you. It is written once, stored
|
|
9
|
+
* at a content address, and named by every ask it spawns (`terms.plan`), so
|
|
10
|
+
* the report can be rebuilt from the chain alone. The asks are filed under
|
|
11
|
+
* one tag, `plan-<slug>`, the way a call's legs are filed under `call-<slug>`.
|
|
12
|
+
*
|
|
13
|
+
* The agent that calls the MCP is the one that reads the market and drafts
|
|
14
|
+
* the checks; this module never guesses them. What it does is refuse what
|
|
15
|
+
* must never be bought (the **market screen**), check that a draft is whole
|
|
16
|
+
* (`validatePlan`), and price it (`planCost`). All pure, so the MCP, the
|
|
17
|
+
* worker and the app agree on one answer.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.askQuestionFor = exports.readMarketPlan = exports.validatePlan = exports.screenCheck = exports.screenMarket = exports.planCost = exports.planAnswerCount = exports.planAskCount = exports.PLAN_LIMITS = exports.MARKET_PLAN_KIND = exports.PLAN_ACTS = exports.planSlug = exports.slugOfPlanTag = exports.planTag = void 0;
|
|
21
|
+
/** The tag every ask of a plan is filed under. No colon: a tag is `[a-z0-9-]`. */
|
|
22
|
+
const planTag = (slug) => `plan-${slug}`;
|
|
23
|
+
exports.planTag = planTag;
|
|
24
|
+
/** The slug back out of a tag, or null when the tag is not a plan's. */
|
|
25
|
+
const slugOfPlanTag = (tag) => {
|
|
26
|
+
const match = /^plan-([a-z][a-z0-9-]{2,39})$/.exec(tag ?? "");
|
|
27
|
+
return match ? match[1] : null;
|
|
28
|
+
};
|
|
29
|
+
exports.slugOfPlanTag = slugOfPlanTag;
|
|
30
|
+
/** A slug from free text: lowercase words joined by dashes, 3–40 characters, starting with a letter. */
|
|
31
|
+
const planSlug = (text, suffix = "") => {
|
|
32
|
+
const words = text
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.normalize("NFKD")
|
|
35
|
+
.replace(/[̀-ͯ]/g, "")
|
|
36
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
37
|
+
.replace(/^-+|-+$/g, "");
|
|
38
|
+
const start = words.replace(/^[^a-z]+/, "") || "plan";
|
|
39
|
+
const tail = suffix ? `-${suffix}` : "";
|
|
40
|
+
const room = 40 - tail.length;
|
|
41
|
+
const body = start.slice(0, room).replace(/-+$/, "");
|
|
42
|
+
return `${body.length >= 3 ? body : `plan-${body}`.slice(0, room)}${tail}`;
|
|
43
|
+
};
|
|
44
|
+
exports.planSlug = planSlug;
|
|
45
|
+
exports.PLAN_ACTS = ["photo", "observation"];
|
|
46
|
+
exports.MARKET_PLAN_KIND = "openhands.market-plan";
|
|
47
|
+
/** The limits a plan is held to. The per-ask cap and the per-plan cap are the MCP's; these are shape. */
|
|
48
|
+
exports.PLAN_LIMITS = {
|
|
49
|
+
checks: 12,
|
|
50
|
+
placesPerCheck: 20,
|
|
51
|
+
asksPerPlan: 120,
|
|
52
|
+
perPlace: 10,
|
|
53
|
+
textMin: 12,
|
|
54
|
+
textMax: 300,
|
|
55
|
+
withinMin: 20,
|
|
56
|
+
withinMax: 20_000,
|
|
57
|
+
hoursMin: 1,
|
|
58
|
+
hoursMax: 720,
|
|
59
|
+
/** The smallest pay worth a person's walk, in dollars. */
|
|
60
|
+
payFloorUsd: 0.05,
|
|
61
|
+
lineMin: 8,
|
|
62
|
+
lineMax: 240,
|
|
63
|
+
};
|
|
64
|
+
/** How many asks a plan posts: one per place per check. */
|
|
65
|
+
const planAskCount = (plan) => plan.checks.reduce((n, check) => n + check.places.length, 0);
|
|
66
|
+
exports.planAskCount = planAskCount;
|
|
67
|
+
/** How many answers a plan buys at most. */
|
|
68
|
+
const planAnswerCount = (plan) => plan.checks.reduce((n, check) => n + check.places.length * check.perPlace, 0);
|
|
69
|
+
exports.planAnswerCount = planAnswerCount;
|
|
70
|
+
/** Cents, so totals add the way a person adds them. */
|
|
71
|
+
const cents = (usd) => Math.round(usd * 100) / 100;
|
|
72
|
+
/**
|
|
73
|
+
* Price a plan: every answer it can buy costs its pay plus the review of that
|
|
74
|
+
* answer (the oracle's check reward), and the fee is a share of that work.
|
|
75
|
+
*/
|
|
76
|
+
const planCost = (plan, opts) => {
|
|
77
|
+
const lines = plan.checks.map((check) => {
|
|
78
|
+
const answers = check.places.length * check.perPlace;
|
|
79
|
+
/* The review is priced per check: a photo is screened, a sentence is not, so the two cost differently. */
|
|
80
|
+
const reviewUsd = typeof opts.reviewUsd === "function" ? opts.reviewUsd(check) : opts.reviewUsd;
|
|
81
|
+
return { id: check.id, answers, payUsd: check.payUsd, reviewUsd, totalUsd: cents(answers * (check.payUsd + reviewUsd)) };
|
|
82
|
+
});
|
|
83
|
+
const workUsd = cents(lines.reduce((sum, line) => sum + line.totalUsd, 0));
|
|
84
|
+
const feeUsd = cents(workUsd * Math.max(0, opts.feeRate));
|
|
85
|
+
return { lines, workUsd, feeUsd, totalUsd: cents(workUsd + feeUsd) };
|
|
86
|
+
};
|
|
87
|
+
exports.planCost = planCost;
|
|
88
|
+
/*
|
|
89
|
+
* The market screen. Each list is a set of words that, found in a check or a
|
|
90
|
+
* market, mean the thing must not be bought. They are deliberately broad: a
|
|
91
|
+
* plan refused for a word that was innocent costs the agent one redraft; a
|
|
92
|
+
* plan that slips through can cost a person their safety or the network its
|
|
93
|
+
* standing. The refusal names the rule so the redraft is easy.
|
|
94
|
+
*/
|
|
95
|
+
/** Sport and esports markets settled by play. A person at the venue is courtsiding, whatever they look at. */
|
|
96
|
+
const SPORT = [
|
|
97
|
+
/\bvs\.?\b/i,
|
|
98
|
+
/\bwin on \d{4}-\d{2}-\d{2}\b/i,
|
|
99
|
+
/\bo\/u\b/i,
|
|
100
|
+
/\bover\/under\b/i,
|
|
101
|
+
/\bspread:/i,
|
|
102
|
+
/\bhandicap\b/i,
|
|
103
|
+
/\bmap handicap\b/i,
|
|
104
|
+
/\bgame \d\b/i,
|
|
105
|
+
/\b(bo3|bo5)\b/i,
|
|
106
|
+
/\b(league|cup|open|championship|grand prix|playoffs?|super bowl|world series|stanley cup|nba finals)\b.*\b(win|winner|beat|score|goals?|points|sets?|maps?)\b/i,
|
|
107
|
+
/\b(counter-strike|dota|league of legends|lol:|valorant|esports?)\b/i,
|
|
108
|
+
];
|
|
109
|
+
/** Places where nobody should be sent to look, whatever the money. */
|
|
110
|
+
const CONFLICT = [
|
|
111
|
+
/\biran\b/i,
|
|
112
|
+
/\bisrael\b/i,
|
|
113
|
+
/\bgaza\b/i,
|
|
114
|
+
/\bwest bank\b/i,
|
|
115
|
+
/\blebanon\b/i,
|
|
116
|
+
/\byemen\b/i,
|
|
117
|
+
/\bsyria\b/i,
|
|
118
|
+
/\bsudan\b/i,
|
|
119
|
+
/\bukrain/i,
|
|
120
|
+
/\brussia\b/i,
|
|
121
|
+
/\bhormuz\b/i,
|
|
122
|
+
/\bkharg\b/i,
|
|
123
|
+
/\bred sea\b/i,
|
|
124
|
+
/\btaiwan strait\b/i,
|
|
125
|
+
/\bceasefire\b/i,
|
|
126
|
+
/\bblockade\b/i,
|
|
127
|
+
/\binvade|invasion\b/i,
|
|
128
|
+
];
|
|
129
|
+
/** Polling places: the law differs by state and by distance, and the network does not litigate it per ask. */
|
|
130
|
+
const POLLING = [/\bpolling (place|station|site|location)s?\b/i, /\bvoting (booth|centre|center|location)s?\b/i, /\bballot\b/i, /\bvoters?\b/i];
|
|
131
|
+
/** The thing that settles a market: never visit, touch or photograph it. */
|
|
132
|
+
const SOURCE = [
|
|
133
|
+
/\bsensor\b/i,
|
|
134
|
+
/\bweather station\b/i,
|
|
135
|
+
/\bthermometer\b/i,
|
|
136
|
+
/\b(rain|river|stream|tide|snow) gauge\b/i,
|
|
137
|
+
/\bgauge\b/i,
|
|
138
|
+
/\bofficial (count|counter|tally|scoreboard|reading)\b/i,
|
|
139
|
+
/\bcounting (room|centre|center)\b/i,
|
|
140
|
+
/\btally\b/i,
|
|
141
|
+
/\bscoreboard\b/i,
|
|
142
|
+
];
|
|
143
|
+
/** Acts that change the world the market is about. A check looks; it never moves the outcome. */
|
|
144
|
+
const ACTS = [
|
|
145
|
+
/\bvote\b/i,
|
|
146
|
+
/\b(bet|wager)\b/i,
|
|
147
|
+
/\bprotest\b/i,
|
|
148
|
+
/\bblock(ade)?\b/i,
|
|
149
|
+
/\bsabotage\b/i,
|
|
150
|
+
/\bdamage\b/i,
|
|
151
|
+
/\bdeface\b/i,
|
|
152
|
+
/\bdestroy\b/i,
|
|
153
|
+
/\bremove the\b/i,
|
|
154
|
+
/\bbribe\b/i,
|
|
155
|
+
/\bthreaten\b/i,
|
|
156
|
+
/\bpersuade\b/i,
|
|
157
|
+
/\bconvince\b/i,
|
|
158
|
+
/\blobby\b/i,
|
|
159
|
+
/\binfluence\b/i,
|
|
160
|
+
/\bcampaign for\b/i,
|
|
161
|
+
];
|
|
162
|
+
/** Places a person has no right to be, and information only an insider has. */
|
|
163
|
+
const PRIVATE = [
|
|
164
|
+
/\bbehind the counter\b/i,
|
|
165
|
+
/\bbackstage\b/i,
|
|
166
|
+
/\bstaff[- ]only\b/i,
|
|
167
|
+
/\brestricted (area|zone)\b/i,
|
|
168
|
+
/\bprivate (property|land|area|office)\b/i,
|
|
169
|
+
/\binside the (office|factory|warehouse|plant)\b/i,
|
|
170
|
+
/\bask (an? )?(employee|staff member|manager|worker) (for|about|what)\b/i,
|
|
171
|
+
/\binternal (numbers|figures|data|memo)\b/i,
|
|
172
|
+
/\bconfidential\b/i,
|
|
173
|
+
/\btrespass/i,
|
|
174
|
+
];
|
|
175
|
+
/** Faces and anything that identifies a person. */
|
|
176
|
+
const FACES = [/(?<!\bno )\bfaces?\b/i, /\bselfie\b/i, /\bportrait\b/i, /\bidentify (the|a|who)\b/i, /\b(licen[cs]e|number) plates?\b/i, /\bwho (is|was) (inside|driving|in the)\b/i];
|
|
177
|
+
/** Photos of these need to say they take no faces. */
|
|
178
|
+
const CROWD = [/\bqueue\b/i, /\bline (of|outside|at)\b/i, /\bcrowd\b/i, /\bpeople\b/i, /\bshoppers\b/i, /\bcustomers\b/i, /\briders?\b/i, /\bpassengers?\b/i];
|
|
179
|
+
const NO_FACES = /\bno faces\b/i;
|
|
180
|
+
const hit = (patterns, text) => patterns.some((pattern) => pattern.test(text));
|
|
181
|
+
/** The market as one string the screen reads. */
|
|
182
|
+
const marketText = (market) => [market.question, market.rules ?? ""].join("\n");
|
|
183
|
+
/** The place names of a check, as one string. */
|
|
184
|
+
const placeText = (check) => check.places.map((place) => place.name).join("\n");
|
|
185
|
+
/** Refusals for the market as a whole: a sports market or a conflict market is never scoped. */
|
|
186
|
+
const screenMarket = (market) => {
|
|
187
|
+
const refusals = [];
|
|
188
|
+
if (hit(SPORT, market.question)) {
|
|
189
|
+
refusals.push({ rule: "in-play-sport", check: null, why: "This market is settled by play. Sending people to a venue to report it is courtsiding, so the network does not scope sports markets." });
|
|
190
|
+
}
|
|
191
|
+
if (hit(CONFLICT, market.question)) {
|
|
192
|
+
refusals.push({ rule: "conflict-zone", check: null, why: "This market is about a conflict. The network sends nobody to look where looking can get them hurt." });
|
|
193
|
+
}
|
|
194
|
+
return refusals;
|
|
195
|
+
};
|
|
196
|
+
exports.screenMarket = screenMarket;
|
|
197
|
+
/** Refusals for one check, read against the market it informs. */
|
|
198
|
+
const screenCheck = (check) => {
|
|
199
|
+
const refusals = [];
|
|
200
|
+
const text = check.text;
|
|
201
|
+
const where = placeText(check);
|
|
202
|
+
const id = check.id;
|
|
203
|
+
if (hit(CONFLICT, where)) {
|
|
204
|
+
refusals.push({ rule: "conflict-zone", check: id, why: "A place in this check is in a conflict area. Choose a place where looking is safe." });
|
|
205
|
+
}
|
|
206
|
+
if (hit(POLLING, `${text}\n${where}`)) {
|
|
207
|
+
refusals.push({ rule: "polling-place", check: id, why: "Polling places are refused: what may be recorded near one differs by state and distance." });
|
|
208
|
+
}
|
|
209
|
+
if (hit(SOURCE, text)) {
|
|
210
|
+
refusals.push({ rule: "resolution-source", check: id, why: "This check goes to an instrument or an official count. Never visit or photograph what settles a market." });
|
|
211
|
+
}
|
|
212
|
+
if (hit(ACTS, text)) {
|
|
213
|
+
refusals.push({ rule: "acts-on-outcome", check: id, why: "This check asks the person to do something that could change the outcome. A check only looks." });
|
|
214
|
+
}
|
|
215
|
+
if (hit(PRIVATE, text)) {
|
|
216
|
+
refusals.push({ rule: "non-public", check: id, why: "This check needs somewhere private or something only an insider knows. Ask only what anyone could see from a public place." });
|
|
217
|
+
}
|
|
218
|
+
if (hit(FACES, text)) {
|
|
219
|
+
refusals.push({ rule: "faces", check: id, why: "This check would record or identify a person. Ask for the thing, not the people." });
|
|
220
|
+
}
|
|
221
|
+
else if (check.act === "photo" && hit(CROWD, text) && !NO_FACES.test(text)) {
|
|
222
|
+
refusals.push({ rule: "faces", check: id, why: "A photo with people in it must say \"no faces\" in the check, so the person taking it frames it that way." });
|
|
223
|
+
}
|
|
224
|
+
if (check.beats.trim().length < exports.PLAN_LIMITS.lineMin) {
|
|
225
|
+
refusals.push({ rule: "no-beats", check: id, why: "Say what existing feed this check improves on. A check that beats nothing is not worth buying." });
|
|
226
|
+
}
|
|
227
|
+
return refusals;
|
|
228
|
+
};
|
|
229
|
+
exports.screenCheck = screenCheck;
|
|
230
|
+
/** Refusals for the shape of one check: the numbers and lengths a plan must keep. */
|
|
231
|
+
const shapeOfCheck = (check) => {
|
|
232
|
+
const L = exports.PLAN_LIMITS;
|
|
233
|
+
const out = [];
|
|
234
|
+
const bad = (why) => out.push({ rule: "shape", check: check.id, why });
|
|
235
|
+
if (!exports.PLAN_ACTS.includes(check.act))
|
|
236
|
+
bad(`The act must be one of ${exports.PLAN_ACTS.join(", ")}.`);
|
|
237
|
+
const text = check.text.trim();
|
|
238
|
+
if (text.length < L.textMin || text.length > L.textMax)
|
|
239
|
+
bad(`The check's words must be ${L.textMin}–${L.textMax} characters.`);
|
|
240
|
+
if (!check.places.length)
|
|
241
|
+
bad("A check needs at least one place.");
|
|
242
|
+
if (check.places.length > L.placesPerCheck)
|
|
243
|
+
bad(`A check holds at most ${L.placesPerCheck} places.`);
|
|
244
|
+
for (const place of check.places) {
|
|
245
|
+
if (!place.name?.trim())
|
|
246
|
+
bad("Every place needs a name a person recognises.");
|
|
247
|
+
if (!(Math.abs(place.lat) <= 90) || !(Math.abs(place.lon) <= 180) || (place.lat === 0 && place.lon === 0))
|
|
248
|
+
bad(`"${place.name}" has no usable coordinates.`);
|
|
249
|
+
if (!(place.withinM >= L.withinMin && place.withinM <= L.withinMax))
|
|
250
|
+
bad(`"${place.name}": the distance must be ${L.withinMin}–${L.withinMax} m.`);
|
|
251
|
+
}
|
|
252
|
+
if (!Number.isInteger(check.perPlace) || check.perPlace < 1 || check.perPlace > L.perPlace)
|
|
253
|
+
bad(`Answers per place must be a whole number from 1 to ${L.perPlace}.`);
|
|
254
|
+
if (!(check.payUsd >= L.payFloorUsd))
|
|
255
|
+
bad(`The pay must be at least $${L.payFloorUsd.toFixed(2)} an answer.`);
|
|
256
|
+
if (!(check.hours >= L.hoursMin && check.hours <= L.hoursMax))
|
|
257
|
+
bad(`A check stays open ${L.hoursMin}–${L.hoursMax} hours.`);
|
|
258
|
+
if (check.beats.trim().length > L.lineMax)
|
|
259
|
+
bad(`What the check beats fits in ${L.lineMax} characters.`);
|
|
260
|
+
return out;
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* Everything wrong with a plan, in the order a person would fix it: the
|
|
264
|
+
* market first, then the plan's own shape, then each check. An empty list is
|
|
265
|
+
* a plan that may be stored and funded.
|
|
266
|
+
*/
|
|
267
|
+
const validatePlan = (plan) => {
|
|
268
|
+
const L = exports.PLAN_LIMITS;
|
|
269
|
+
const refusals = [...(0, exports.screenMarket)(plan.market)];
|
|
270
|
+
const shape = (why) => refusals.push({ rule: "shape", check: null, why });
|
|
271
|
+
if (plan.kind !== exports.MARKET_PLAN_KIND || plan.v !== 1)
|
|
272
|
+
shape("This is not a version 1 plan.");
|
|
273
|
+
if (!(0, exports.slugOfPlanTag)((0, exports.planTag)(plan.slug)))
|
|
274
|
+
shape("The slug must be 3–40 characters of a–z, 0–9 and dashes, starting with a letter.");
|
|
275
|
+
if (!/^https?:\/\//.test(plan.market.url))
|
|
276
|
+
shape("The market needs its link.");
|
|
277
|
+
if (plan.market.question.trim().length < 8)
|
|
278
|
+
shape("The market needs its question.");
|
|
279
|
+
if (!plan.checks.length)
|
|
280
|
+
shape("A plan needs at least one check.");
|
|
281
|
+
if (plan.checks.length > L.checks)
|
|
282
|
+
shape(`A plan holds at most ${L.checks} checks.`);
|
|
283
|
+
if ((0, exports.planAskCount)(plan) > L.asksPerPlan)
|
|
284
|
+
shape(`A plan posts at most ${L.asksPerPlan} asks (one per place per check).`);
|
|
285
|
+
const ids = new Set();
|
|
286
|
+
for (const check of plan.checks) {
|
|
287
|
+
if (ids.has(check.id))
|
|
288
|
+
shape(`Two checks share the id ${check.id}.`);
|
|
289
|
+
ids.add(check.id);
|
|
290
|
+
}
|
|
291
|
+
if (plan.cannotTell.trim().length < L.lineMin) {
|
|
292
|
+
refusals.push({ rule: "no-cannot-tell", check: null, why: "Say what this plan cannot tell the trader. A plan without its limit reads as a promise." });
|
|
293
|
+
}
|
|
294
|
+
for (const check of plan.checks)
|
|
295
|
+
refusals.push(...shapeOfCheck(check), ...(0, exports.screenCheck)(check));
|
|
296
|
+
return refusals;
|
|
297
|
+
};
|
|
298
|
+
exports.validatePlan = validatePlan;
|
|
299
|
+
/** A plan read back from the store, or null when the document is not a version 1 plan. */
|
|
300
|
+
const readMarketPlan = (doc) => {
|
|
301
|
+
if (!doc || typeof doc !== "object")
|
|
302
|
+
return null;
|
|
303
|
+
const plan = doc;
|
|
304
|
+
if (plan.kind !== exports.MARKET_PLAN_KIND || plan.v !== 1)
|
|
305
|
+
return null;
|
|
306
|
+
if (typeof plan.slug !== "string" || !plan.market || !Array.isArray(plan.checks) || typeof plan.cannotTell !== "string")
|
|
307
|
+
return null;
|
|
308
|
+
return plan;
|
|
309
|
+
};
|
|
310
|
+
exports.readMarketPlan = readMarketPlan;
|
|
311
|
+
/** The words the asks of one check carry: the check, then its place, then the honest limit of a single look. */
|
|
312
|
+
const askQuestionFor = (check, place) => {
|
|
313
|
+
const text = check.text.trim().replace(/\s+/g, " ");
|
|
314
|
+
const at = ` At ${place.name}.`;
|
|
315
|
+
const room = exports.PLAN_LIMITS.textMax - at.length;
|
|
316
|
+
return (text.length > room ? `${text.slice(0, room - 1).trimEnd()}…` : text) + at;
|
|
317
|
+
};
|
|
318
|
+
exports.askQuestionFor = askQuestionFor;
|
|
319
|
+
//# sourceMappingURL=marketPlan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marketPlan.js","sourceRoot":"","sources":["../../../src/rubric/marketPlan.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;AAEH,kFAAkF;AAC3E,MAAM,OAAO,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,QAAQ,IAAI,EAAE,CAAC;AAAnD,QAAA,OAAO,WAA4C;AAEhE,wEAAwE;AACjE,MAAM,aAAa,GAAG,CAAC,GAA8B,EAAiB,EAAE;IAC7E,MAAM,KAAK,GAAG,+BAA+B,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;IAC9D,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAClC,CAAC,CAAC;AAHW,QAAA,aAAa,iBAGxB;AAEF,wGAAwG;AACjG,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAE,MAAM,GAAG,EAAE,EAAU,EAAE;IAC5D,MAAM,KAAK,GAAG,IAAI;SACf,WAAW,EAAE;SACb,SAAS,CAAC,MAAM,CAAC;SACjB,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;SACrB,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAC3B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC;IACtD,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACxC,MAAM,IAAI,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC;IAC9B,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACrD,OAAO,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC;AAC7E,CAAC,CAAC;AAZW,QAAA,QAAQ,YAYnB;AAKW,QAAA,SAAS,GAAuB,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;AAsDzD,QAAA,gBAAgB,GAAG,uBAAgC,CAAC;AAEjE,yGAAyG;AAC5F,QAAA,WAAW,GAAG;IACzB,MAAM,EAAE,EAAE;IACV,cAAc,EAAE,EAAE;IAClB,WAAW,EAAE,GAAG;IAChB,QAAQ,EAAE,EAAE;IACZ,OAAO,EAAE,EAAE;IACX,OAAO,EAAE,GAAG;IACZ,SAAS,EAAE,EAAE;IACb,SAAS,EAAE,MAAM;IACjB,QAAQ,EAAE,CAAC;IACX,QAAQ,EAAE,GAAG;IACb,0DAA0D;IAC1D,WAAW,EAAE,IAAI;IACjB,OAAO,EAAE,CAAC;IACV,OAAO,EAAE,GAAG;CACJ,CAAC;AAEX,2DAA2D;AACpD,MAAM,YAAY,GAAG,CAAC,IAAgC,EAAU,EAAE,CACvE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AADlD,QAAA,YAAY,gBACsC;AAE/D,4CAA4C;AACrC,MAAM,eAAe,GAAG,CAAC,IAAgC,EAAU,EAAE,CAC1E,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;AADnE,QAAA,eAAe,mBACoD;AAchF,uDAAuD;AACvD,MAAM,KAAK,GAAG,CAAC,GAAW,EAAU,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;AAEnE;;;GAGG;AACI,MAAM,QAAQ,GAAG,CAAC,IAAgC,EAAE,IAA6E,EAAY,EAAE;IACpJ,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACtC,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC;QACrD,0GAA0G;QAC1G,MAAM,SAAS,GAAG,OAAO,IAAI,CAAC,SAAS,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC;QAChG,OAAO,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,KAAK,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,EAAE,CAAC;IAC3H,CAAC,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3E,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1D,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,EAAE,CAAC;AACvE,CAAC,CAAC;AAVW,QAAA,QAAQ,YAUnB;AAsBF;;;;;;GAMG;AAEH,8GAA8G;AAC9G,MAAM,KAAK,GAAG;IACZ,YAAY;IACZ,+BAA+B;IAC/B,WAAW;IACX,kBAAkB;IAClB,YAAY;IACZ,eAAe;IACf,mBAAmB;IACnB,cAAc;IACd,gBAAgB;IAChB,gKAAgK;IAChK,qEAAqE;CACtE,CAAC;AAEF,sEAAsE;AACtE,MAAM,QAAQ,GAAG;IACf,WAAW;IACX,aAAa;IACb,WAAW;IACX,gBAAgB;IAChB,cAAc;IACd,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,WAAW;IACX,aAAa;IACb,aAAa;IACb,YAAY;IACZ,cAAc;IACd,oBAAoB;IACpB,gBAAgB;IAChB,eAAe;IACf,sBAAsB;CACvB,CAAC;AAEF,8GAA8G;AAC9G,MAAM,OAAO,GAAG,CAAC,8CAA8C,EAAE,8CAA8C,EAAE,aAAa,EAAE,cAAc,CAAC,CAAC;AAEhJ,4EAA4E;AAC5E,MAAM,MAAM,GAAG;IACb,aAAa;IACb,sBAAsB;IACtB,kBAAkB;IAClB,0CAA0C;IAC1C,YAAY;IACZ,wDAAwD;IACxD,oCAAoC;IACpC,YAAY;IACZ,iBAAiB;CAClB,CAAC;AAEF,iGAAiG;AACjG,MAAM,IAAI,GAAG;IACX,WAAW;IACX,kBAAkB;IAClB,cAAc;IACd,kBAAkB;IAClB,eAAe;IACf,aAAa;IACb,aAAa;IACb,cAAc;IACd,iBAAiB;IACjB,YAAY;IACZ,eAAe;IACf,eAAe;IACf,eAAe;IACf,YAAY;IACZ,gBAAgB;IAChB,mBAAmB;CACpB,CAAC;AAEF,+EAA+E;AAC/E,MAAM,OAAO,GAAG;IACd,yBAAyB;IACzB,gBAAgB;IAChB,oBAAoB;IACpB,6BAA6B;IAC7B,0CAA0C;IAC1C,kDAAkD;IAClD,yEAAyE;IACzE,2CAA2C;IAC3C,mBAAmB;IACnB,aAAa;CACd,CAAC;AAEF,mDAAmD;AACnD,MAAM,KAAK,GAAG,CAAC,uBAAuB,EAAC,aAAa,EAAE,eAAe,EAAE,2BAA2B,EAAE,kCAAkC,EAAE,2CAA2C,CAAC,CAAC;AAErL,sDAAsD;AACtD,MAAM,KAAK,GAAG,CAAC,YAAY,EAAE,2BAA2B,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,gBAAgB,EAAE,cAAc,EAAE,kBAAkB,CAAC,CAAC;AAE9J,MAAM,QAAQ,GAAG,eAAe,CAAC;AAEjC,MAAM,GAAG,GAAG,CAAC,QAAkB,EAAE,IAAY,EAAW,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AAE1G,iDAAiD;AACjD,MAAM,UAAU,GAAG,CAAC,MAAkB,EAAU,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEpG,iDAAiD;AACjD,MAAM,SAAS,GAAG,CAAC,KAAgB,EAAU,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEnG,gGAAgG;AACzF,MAAM,YAAY,GAAG,CAAC,MAAkB,EAAiB,EAAE;IAChE,MAAM,QAAQ,GAAkB,EAAE,CAAC;IACnC,IAAI,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,sIAAsI,EAAE,CAAC,CAAC;IACrM,CAAC;IACD,IAAI,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,oGAAoG,EAAE,CAAC,CAAC;IACnK,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AATW,QAAA,YAAY,gBASvB;AAEF,kEAAkE;AAC3D,MAAM,WAAW,GAAG,CAAC,KAAgB,EAAiB,EAAE;IAC7D,MAAM,QAAQ,GAAkB,EAAE,CAAC;IACnC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;IACxB,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,MAAM,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC;IACpB,IAAI,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC;QACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,oFAAoF,EAAE,CAAC,CAAC;IACjJ,CAAC;IACD,IAAI,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,KAAK,KAAK,EAAE,CAAC,EAAE,CAAC;QACtC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,0FAA0F,EAAE,CAAC,CAAC;IACvJ,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC;QACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,yGAAyG,EAAE,CAAC,CAAC;IAC1K,CAAC;IACD,IAAI,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;QACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,+FAA+F,EAAE,CAAC,CAAC;IAC9J,CAAC;IACD,IAAI,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC;QACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,4HAA4H,EAAE,CAAC,CAAC;IACtL,CAAC;IACD,IAAI,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC;QACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,kFAAkF,EAAE,CAAC,CAAC;IACvI,CAAC;SAAM,IAAI,KAAK,CAAC,GAAG,KAAK,OAAO,IAAI,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,2GAA2G,EAAE,CAAC,CAAC;IAChK,CAAC;IACD,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,mBAAW,CAAC,OAAO,EAAE,CAAC;QACpD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,gGAAgG,EAAE,CAAC,CAAC;IACxJ,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AA7BW,QAAA,WAAW,eA6BtB;AAEF,qFAAqF;AACrF,MAAM,YAAY,GAAG,CAAC,KAAgB,EAAiB,EAAE;IACvD,MAAM,CAAC,GAAG,mBAAW,CAAC;IACtB,MAAM,GAAG,GAAkB,EAAE,CAAC;IAC9B,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;IAC/E,IAAI,CAAC,iBAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC;QAAE,GAAG,CAAC,0BAA0B,iBAAS,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3F,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAC/B,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO;QAAE,GAAG,CAAC,6BAA6B,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,cAAc,CAAC,CAAC;IAC/H,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM;QAAE,GAAG,CAAC,mCAAmC,CAAC,CAAC;IACnE,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,cAAc;QAAE,GAAG,CAAC,yBAAyB,CAAC,CAAC,cAAc,UAAU,CAAC,CAAC;IACrG,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACjC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE;YAAE,GAAG,CAAC,+CAA+C,CAAC,CAAC;QAC9E,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,CAAC,GAAG,KAAK,CAAC,CAAC;YAAE,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,8BAA8B,CAAC,CAAC;QAC7J,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC,SAAS,IAAI,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC,SAAS,CAAC;YAAE,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,2BAA2B,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC;IACrJ,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ;QAAE,GAAG,CAAC,sDAAsD,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC;IACrK,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,WAAW,CAAC;QAAE,GAAG,CAAC,6BAA6B,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IAC9G,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC,QAAQ,IAAI,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC,QAAQ,CAAC;QAAE,GAAG,CAAC,sBAAsB,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,QAAQ,SAAS,CAAC,CAAC;IAC5H,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO;QAAE,GAAG,CAAC,gCAAgC,CAAC,CAAC,OAAO,cAAc,CAAC,CAAC;IACxG,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF;;;;GAIG;AACI,MAAM,YAAY,GAAG,CAAC,IAAgB,EAAiB,EAAE;IAC9D,MAAM,CAAC,GAAG,mBAAW,CAAC;IACtB,MAAM,QAAQ,GAAkB,CAAC,GAAG,IAAA,oBAAY,EAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/D,MAAM,KAAK,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;IAClF,IAAI,IAAI,CAAC,IAAI,KAAK,wBAAgB,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC;QAAE,KAAK,CAAC,+BAA+B,CAAC,CAAC;IAC3F,IAAI,CAAC,IAAA,qBAAa,EAAC,IAAA,eAAO,EAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAAE,KAAK,CAAC,kFAAkF,CAAC,CAAC;IAClI,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC;QAAE,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAC/E,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACpF,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACnE,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM;QAAE,KAAK,CAAC,wBAAwB,CAAC,CAAC,MAAM,UAAU,CAAC,CAAC;IACrF,IAAI,IAAA,oBAAY,EAAC,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW;QAAE,KAAK,CAAC,wBAAwB,CAAC,CAAC,WAAW,kCAAkC,CAAC,CAAC;IACvH,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChC,IAAI,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAAE,KAAK,CAAC,2BAA2B,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC;QACrE,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACpB,CAAC;IACD,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC;QAC9C,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,yFAAyF,EAAE,CAAC,CAAC;IACzJ,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM;QAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,EAAE,GAAG,IAAA,mBAAW,EAAC,KAAK,CAAC,CAAC,CAAC;IAC9F,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AArBW,QAAA,YAAY,gBAqBvB;AAEF,0FAA0F;AACnF,MAAM,cAAc,GAAG,CAAC,GAAY,EAAqB,EAAE;IAChE,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACjD,MAAM,IAAI,GAAG,GAA0B,CAAC;IACxC,IAAI,IAAI,CAAC,IAAI,KAAK,wBAAgB,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAChE,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACrI,OAAO,IAAkB,CAAC;AAC5B,CAAC,CAAC;AANW,QAAA,cAAc,kBAMzB;AAEF,gHAAgH;AACzG,MAAM,cAAc,GAAG,CAAC,KAAgB,EAAE,KAAgB,EAAU,EAAE;IAC3E,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACpD,MAAM,EAAE,GAAG,OAAO,KAAK,CAAC,IAAI,GAAG,CAAC;IAChC,MAAM,IAAI,GAAG,mBAAW,CAAC,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC;IAC7C,OAAO,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;AACpF,CAAC,CAAC;AALW,QAAA,cAAc,kBAKzB"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type AskTerms } from "./terms.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The terms of an observation bought from a stranger: what `ask_the_world`
|
|
4
|
+
* posts, and what every check of an Ask the World plan posts, so that the two
|
|
5
|
+
* can never drift apart (docs/verified-work-ask-the-world-plan.md, D2).
|
|
6
|
+
*
|
|
7
|
+
* A located ask carries the place gate, then the falsifiers (daylight always;
|
|
8
|
+
* weather on a photo only), then the relevance judge.
|
|
9
|
+
*
|
|
10
|
+
* **Weather only where the reviewer can weigh it.** On testnet, 2026-10-15, a
|
|
11
|
+
* typed observation ("It's dry and the streetlights are working", ask #44040)
|
|
12
|
+
* came back with location, daylight and the judge passed and the weather gate
|
|
13
|
+
* `unevaluated`, and the engine declines any decision with a gate it could
|
|
14
|
+
* not evaluate (reason code `weather`). The falsifier was meant to fail only
|
|
15
|
+
* an answer the world contradicts; on a typed answer it failed every one. A
|
|
16
|
+
* photo keeps it. The judge says its refusal
|
|
17
|
+
* first and the question last: the oracle fails any rule it cannot find proof
|
|
18
|
+
* for, so a sentence that never names its own failure reads as "prove it", and
|
|
19
|
+
* the question is a caller's 300 characters that the parser may cut.
|
|
20
|
+
*/
|
|
21
|
+
export type ObservationMode = "observation" | "photo" | "url-check";
|
|
22
|
+
export interface ObservationInput {
|
|
23
|
+
/** The real-world question, 12–300 characters. */
|
|
24
|
+
question: string;
|
|
25
|
+
mode: ObservationMode;
|
|
26
|
+
/** Where the answer must be taken. Null only for a url-check. */
|
|
27
|
+
centre: {
|
|
28
|
+
lat: number;
|
|
29
|
+
lon: number;
|
|
30
|
+
} | null;
|
|
31
|
+
/** How close, in metres. */
|
|
32
|
+
metres: number;
|
|
33
|
+
/** A label for the place, frozen beside the coordinates. Never the contract: the gate measures from lat/lon. */
|
|
34
|
+
place?: string | null;
|
|
35
|
+
/** Who rules, named in the terms so the ask is a contract about who decides. */
|
|
36
|
+
oracle: {
|
|
37
|
+
kind: "oracle";
|
|
38
|
+
address: string;
|
|
39
|
+
rewardUixo: number;
|
|
40
|
+
};
|
|
41
|
+
/** The tag the ask is filed under. */
|
|
42
|
+
tag?: string;
|
|
43
|
+
/** The content address of the plan this ask is one check of. */
|
|
44
|
+
plan?: string;
|
|
45
|
+
}
|
|
46
|
+
/** The relevance line every observation is judged on. */
|
|
47
|
+
export declare const relevanceLine: (question: string) => string;
|
|
48
|
+
/**
|
|
49
|
+
* Terms in the parser's own key order, so they read back byte for byte:
|
|
50
|
+
* v, task, deliverable, campaign, plan, criteria, judge, via.
|
|
51
|
+
*/
|
|
52
|
+
export declare function observationTerms(input: ObservationInput): AskTerms;
|
|
53
|
+
//# sourceMappingURL=observation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observation.d.ts","sourceRoot":"","sources":["../../../src/rubric/observation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,KAAK,QAAQ,EAAkB,MAAM,YAAY,CAAC;AAE5F;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG,OAAO,GAAG,WAAW,CAAC;AAEpE,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,eAAe,CAAC;IACtB,iEAAiE;IACjE,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5C,4BAA4B;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,gHAAgH;IAChH,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,gFAAgF;IAChF,MAAM,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAChE,sCAAsC;IACtC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,yDAAyD;AACzD,eAAO,MAAM,aAAa,GAAI,UAAU,MAAM,KAAG,MACqG,CAAC;AAEvJ;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,gBAAgB,GAAG,QAAQ,CAuBlE"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.relevanceLine = void 0;
|
|
4
|
+
exports.observationTerms = observationTerms;
|
|
5
|
+
const terms_ts_1 = require("./terms.js");
|
|
6
|
+
/** The relevance line every observation is judged on. */
|
|
7
|
+
const relevanceLine = (question) => `The answer addresses the question this ask asked. Fail this only if it answers something else, or is empty or evasive. The question: "${question}"`;
|
|
8
|
+
exports.relevanceLine = relevanceLine;
|
|
9
|
+
/**
|
|
10
|
+
* Terms in the parser's own key order, so they read back byte for byte:
|
|
11
|
+
* v, task, deliverable, campaign, plan, criteria, judge, via.
|
|
12
|
+
*/
|
|
13
|
+
function observationTerms(input) {
|
|
14
|
+
const base = (0, terms_ts_1.deriveTerms)(input.question, input.oracle);
|
|
15
|
+
const criteria = [...base.criteria];
|
|
16
|
+
if (input.centre) {
|
|
17
|
+
criteria.push({
|
|
18
|
+
id: `c${criteria.length + 1}`,
|
|
19
|
+
kind: "measure",
|
|
20
|
+
gate: { id: "within-radius", lat: input.centre.lat, lon: input.centre.lon, meters: input.metres, ...(input.place ? { place: input.place } : {}) }
|
|
21
|
+
});
|
|
22
|
+
if (input.mode === "photo")
|
|
23
|
+
criteria.push({ id: `c${criteria.length + 1}`, kind: "measure", gate: { id: "weather-consistent" } });
|
|
24
|
+
criteria.push({ id: `c${criteria.length + 1}`, kind: "measure", gate: { id: "daylight-consistent" } });
|
|
25
|
+
}
|
|
26
|
+
criteria.push({ id: `c${criteria.length + 1}`, kind: "judge", text: (0, exports.relevanceLine)(input.question), confidence: terms_ts_1.DEFAULT_CONFIDENCE });
|
|
27
|
+
return {
|
|
28
|
+
v: base.v,
|
|
29
|
+
task: input.question,
|
|
30
|
+
deliverable: { type: input.mode === "photo" ? "photo" : "text", prompt: input.question },
|
|
31
|
+
...(input.tag ? { campaign: input.tag } : {}),
|
|
32
|
+
...(input.plan ? { plan: input.plan } : {}),
|
|
33
|
+
criteria,
|
|
34
|
+
judge: base.judge,
|
|
35
|
+
via: "mcp"
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=observation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observation.js","sourceRoot":"","sources":["../../../src/rubric/observation.ts"],"names":[],"mappings":";;;AAkDA,4CAuBC;AAzED,yCAA4F;AA0C5F,yDAAyD;AAClD,MAAM,aAAa,GAAG,CAAC,QAAgB,EAAU,EAAE,CACxD,yIAAyI,QAAQ,GAAG,CAAC;AAD1I,QAAA,aAAa,iBAC6H;AAEvJ;;;GAGG;AACH,SAAgB,gBAAgB,CAAC,KAAuB;IACtD,MAAM,IAAI,GAAG,IAAA,sBAAW,EAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IACvD,MAAM,QAAQ,GAAgB,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjD,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACjB,QAAQ,CAAC,IAAI,CAAC;YACZ,EAAE,EAAE,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE;YAC7B,IAAI,EAAE,SAAS;YACf,IAAI,EAAE,EAAE,EAAE,EAAE,eAAe,EAAE,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE;SAClJ,CAAC,CAAC;QACH,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;YAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,oBAAoB,EAAE,EAAE,CAAC,CAAC;QAClI,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,qBAAqB,EAAE,EAAE,CAAC,CAAC;IACzG,CAAC;IACD,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAA,qBAAa,EAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,UAAU,EAAE,6BAAkB,EAAE,CAAC,CAAC;IACrI,OAAO;QACL,CAAC,EAAE,IAAI,CAAC,CAAC;QACT,IAAI,EAAE,KAAK,CAAC,QAAQ;QACpB,WAAW,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,QAAQ,EAAE;QACxF,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,QAAQ;QACR,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,GAAG,EAAE,KAAK;KACX,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signed personal link a ping carries (Ask the World, B11).
|
|
3
|
+
*
|
|
4
|
+
* A ping says "this ask, for this person, until this time". The link's token
|
|
5
|
+
* binds those three and the ping's own id under a secret only the network
|
|
6
|
+
* holds, so opening the link proves the ping was sent to that person: the
|
|
7
|
+
* submit page can then answer *as* them, reserve their slot, and file the
|
|
8
|
+
* claim in their name. The token proves who was pinged, nothing more; that
|
|
9
|
+
* a link is opened once is the registry's to enforce, because only it knows.
|
|
10
|
+
*
|
|
11
|
+
* Web Crypto HMAC-SHA-256, so the worker that mints and the app that checks
|
|
12
|
+
* run the same code.
|
|
13
|
+
*/
|
|
14
|
+
/** How long a ping's link stays good: long enough to walk there, short enough that a forwarded link dies. */
|
|
15
|
+
export declare const PING_LINK_MINUTES = 30;
|
|
16
|
+
/** What a valid token says. */
|
|
17
|
+
export interface PingClaim {
|
|
18
|
+
/** The person's address. */
|
|
19
|
+
person: string;
|
|
20
|
+
/** The ask (collection id). */
|
|
21
|
+
ask: string;
|
|
22
|
+
/** The ping's id, so a second open can be refused. */
|
|
23
|
+
ping: string;
|
|
24
|
+
/** Expiry, epoch seconds. */
|
|
25
|
+
exp: number;
|
|
26
|
+
}
|
|
27
|
+
/** A token for one ping. The secret must be the one the app checks with. */
|
|
28
|
+
export declare function mintPingToken(claim: PingClaim, secret: string): Promise<string>;
|
|
29
|
+
export type PingTokenCheck = {
|
|
30
|
+
ok: true;
|
|
31
|
+
claim: PingClaim;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
reason: "malformed" | "forged" | "expired" | "wrong-ask";
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Read a token back. Forged, expired, malformed or meant for another ask: refused, and the
|
|
38
|
+
* reason says which, so the page can tell the person something true.
|
|
39
|
+
*/
|
|
40
|
+
export declare function checkPingToken(token: string, secret: string, opts?: {
|
|
41
|
+
ask?: string;
|
|
42
|
+
now?: number;
|
|
43
|
+
graceSeconds?: number;
|
|
44
|
+
}): Promise<PingTokenCheck>;
|
|
45
|
+
//# sourceMappingURL=pingLink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pingLink.d.ts","sourceRoot":"","sources":["../../../src/rubric/pingLink.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,6GAA6G;AAC7G,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,+BAA+B;AAC/B,MAAM,WAAW,SAAS;IACxB,4BAA4B;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,GAAG,EAAE,MAAM,CAAC;CACb;AAoBD,4EAA4E;AAC5E,wBAAsB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMrF;AAED,MAAM,MAAM,cAAc,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,CAAA;CAAE,CAAC;AAEtI;;;GAGG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAmB7J"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The signed personal link a ping carries (Ask the World, B11).
|
|
4
|
+
*
|
|
5
|
+
* A ping says "this ask, for this person, until this time". The link's token
|
|
6
|
+
* binds those three and the ping's own id under a secret only the network
|
|
7
|
+
* holds, so opening the link proves the ping was sent to that person: the
|
|
8
|
+
* submit page can then answer *as* them, reserve their slot, and file the
|
|
9
|
+
* claim in their name. The token proves who was pinged, nothing more; that
|
|
10
|
+
* a link is opened once is the registry's to enforce, because only it knows.
|
|
11
|
+
*
|
|
12
|
+
* Web Crypto HMAC-SHA-256, so the worker that mints and the app that checks
|
|
13
|
+
* run the same code.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.PING_LINK_MINUTES = void 0;
|
|
17
|
+
exports.mintPingToken = mintPingToken;
|
|
18
|
+
exports.checkPingToken = checkPingToken;
|
|
19
|
+
/** How long a ping's link stays good: long enough to walk there, short enough that a forwarded link dies. */
|
|
20
|
+
exports.PING_LINK_MINUTES = 30;
|
|
21
|
+
const DOMAIN = "openhands-ping:v1";
|
|
22
|
+
const PART = /^[a-z0-9]{1,80}$/;
|
|
23
|
+
const b64url = (bytes) => {
|
|
24
|
+
let text = "";
|
|
25
|
+
for (const byte of new Uint8Array(bytes))
|
|
26
|
+
text += String.fromCharCode(byte);
|
|
27
|
+
return btoa(text).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
28
|
+
};
|
|
29
|
+
const message = (claim) => [DOMAIN, claim.person, claim.ask, claim.ping, String(claim.exp)].join("|");
|
|
30
|
+
const keyFor = (secret) => crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
|
|
31
|
+
const mac = async (claim, secret) => b64url(await crypto.subtle.sign("HMAC", await keyFor(secret), new TextEncoder().encode(message(claim))));
|
|
32
|
+
/** A token for one ping. The secret must be the one the app checks with. */
|
|
33
|
+
async function mintPingToken(claim, secret) {
|
|
34
|
+
if (!secret)
|
|
35
|
+
throw new Error("No signing secret: a ping link would be unverifiable.");
|
|
36
|
+
for (const part of [claim.person, claim.ask, claim.ping]) {
|
|
37
|
+
if (!PART.test(part))
|
|
38
|
+
throw new Error(`"${part}" cannot go in a ping link.`);
|
|
39
|
+
}
|
|
40
|
+
return [claim.person, claim.ask, claim.ping, String(claim.exp), await mac(claim, secret)].join(".");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Read a token back. Forged, expired, malformed or meant for another ask: refused, and the
|
|
44
|
+
* reason says which, so the page can tell the person something true.
|
|
45
|
+
*/
|
|
46
|
+
async function checkPingToken(token, secret, opts = {}) {
|
|
47
|
+
if (!secret)
|
|
48
|
+
return { ok: false, reason: "forged" };
|
|
49
|
+
const parts = token.split(".");
|
|
50
|
+
if (parts.length !== 5)
|
|
51
|
+
return { ok: false, reason: "malformed" };
|
|
52
|
+
const [person, ask, ping, expText, given] = parts;
|
|
53
|
+
const exp = Number(expText);
|
|
54
|
+
if (![person, ask, ping].every((part) => PART.test(part)) || !Number.isInteger(exp))
|
|
55
|
+
return { ok: false, reason: "malformed" };
|
|
56
|
+
const claim = { person, ask, ping, exp };
|
|
57
|
+
const expected = await mac(claim, secret);
|
|
58
|
+
if (expected.length !== given.length)
|
|
59
|
+
return { ok: false, reason: "forged" };
|
|
60
|
+
let diff = 0;
|
|
61
|
+
for (let i = 0; i < expected.length; i++)
|
|
62
|
+
diff |= expected.charCodeAt(i) ^ given.charCodeAt(i);
|
|
63
|
+
if (diff !== 0)
|
|
64
|
+
return { ok: false, reason: "forged" };
|
|
65
|
+
if (opts.ask && opts.ask !== ask)
|
|
66
|
+
return { ok: false, reason: "wrong-ask" };
|
|
67
|
+
/* The link is theirs for PING_LINK_MINUTES. What it led to can be recorded later: walking to
|
|
68
|
+
the place and taking the photo can outlast the link, and linking the answer to the ping
|
|
69
|
+
after that is not opening the link again (`graceSeconds`). */
|
|
70
|
+
if (Math.floor((opts.now ?? Date.now()) / 1000) >= exp + Math.max(0, opts.graceSeconds ?? 0))
|
|
71
|
+
return { ok: false, reason: "expired" };
|
|
72
|
+
return { ok: true, claim };
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=pingLink.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pingLink.js","sourceRoot":"","sources":["../../../src/rubric/pingLink.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who to ping when a plan's ask is posted (Ask the World, B9).
|
|
3
|
+
*
|
|
4
|
+
* A person in the registry said where they are most days (a coarse cell),
|
|
5
|
+
* how far they would go, what they know about, when they may be pinged and
|
|
6
|
+
* how often. An ask has a point, a radius and the plan's topics. The match
|
|
7
|
+
* is everyone near enough, interested, awake and under their cap, best first,
|
|
8
|
+
* and three times as many as the ask can pay: people miss pings, and the
|
|
9
|
+
* first to answer wins the slot.
|
|
10
|
+
*
|
|
11
|
+
* Pure and channel-blind: nothing here knows whether the ping goes by
|
|
12
|
+
* Telegram, WhatsApp or Web Push.
|
|
13
|
+
*/
|
|
14
|
+
/** What a person can say they know about. The onboarding asks the same list. */
|
|
15
|
+
export declare const PING_TOPICS: readonly ["transport", "shops", "prices", "events", "streets", "nature"];
|
|
16
|
+
export type PingTopic = (typeof PING_TOPICS)[number];
|
|
17
|
+
export type PingChannel = "telegram" | "whatsapp" | "webpush";
|
|
18
|
+
/** A person as the matcher reads them from the registry. */
|
|
19
|
+
export interface PingPerson {
|
|
20
|
+
/** Their address on the chain: claims are filed in this name. */
|
|
21
|
+
id: string;
|
|
22
|
+
channel: PingChannel;
|
|
23
|
+
/** A geohash cell, six characters: never a point. */
|
|
24
|
+
cell: string;
|
|
25
|
+
/** How far they would go from the cell, in metres. */
|
|
26
|
+
radiusM: number;
|
|
27
|
+
/** Empty means anything. */
|
|
28
|
+
topics: string[];
|
|
29
|
+
/** Local hours they may be pinged, [from, to) in 0–24; wraps past midnight when from > to. Absent means any hour. */
|
|
30
|
+
hours?: {
|
|
31
|
+
from: number;
|
|
32
|
+
to: number;
|
|
33
|
+
};
|
|
34
|
+
/** Minutes east of UTC for their local hours. */
|
|
35
|
+
tzOffsetMin?: number;
|
|
36
|
+
/** Pings a day at most. */
|
|
37
|
+
dailyCap: number;
|
|
38
|
+
/** Pings already sent today. */
|
|
39
|
+
sentToday: number;
|
|
40
|
+
/** Approved answers so far: the order of who hears first. */
|
|
41
|
+
approved: number;
|
|
42
|
+
/** Epoch ms before which they are paused (STOP, or "fewer"). */
|
|
43
|
+
pausedUntil?: number;
|
|
44
|
+
/** Asks this person was already pinged for: never twice. */
|
|
45
|
+
pingedAsks?: string[];
|
|
46
|
+
}
|
|
47
|
+
/** An ask as the matcher reads it. */
|
|
48
|
+
export interface PingAsk {
|
|
49
|
+
id: string;
|
|
50
|
+
lat: number;
|
|
51
|
+
lon: number;
|
|
52
|
+
/** How close the answer must be taken. */
|
|
53
|
+
withinM: number;
|
|
54
|
+
/** The plan's topics; empty means anyone. */
|
|
55
|
+
topics: string[];
|
|
56
|
+
/** Answers the ask pays. */
|
|
57
|
+
quota: number;
|
|
58
|
+
}
|
|
59
|
+
/** How many people to ping per answer the ask can pay. */
|
|
60
|
+
export declare const PINGS_PER_SLOT = 3;
|
|
61
|
+
/** Whether a local hour falls inside a person's window. */
|
|
62
|
+
export declare const inHours: (hours: PingPerson["hours"], now: number, tzOffsetMin?: number) => boolean;
|
|
63
|
+
/** One match: the person and how far their cell's centre is from the ask. */
|
|
64
|
+
export interface PingMatch {
|
|
65
|
+
person: PingPerson;
|
|
66
|
+
metres: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The people to ping for an ask, best first: most approved answers, then
|
|
70
|
+
* nearest. At most `quota × PINGS_PER_SLOT`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function matchPeople(ask: PingAsk, people: PingPerson[], now: number): PingMatch[];
|
|
73
|
+
//# sourceMappingURL=pingMatch.d.ts.map
|