atris 3.69.0 → 3.71.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 +40 -1
- package/atris/skills/README.md +1 -0
- package/atris/skills/campaigns/SKILL.md +81 -0
- package/atris/skills/meta/SKILL.md +52 -8
- package/ax +226 -42
- package/bin/atris.js +26 -6
- package/commands/autoland.js +176 -7
- package/commands/autopilot.js +71 -11
- package/commands/campaign.js +759 -0
- package/commands/compile.js +19 -8
- package/commands/doctor.js +58 -0
- package/commands/dream.js +18 -3
- package/commands/engine.js +147 -5
- package/commands/improve.js +119 -9
- package/commands/init.js +18 -1
- package/commands/interview.js +15 -3
- package/commands/land.js +22 -6
- package/commands/link.js +1 -0
- package/commands/member.js +433 -99
- package/commands/mission.js +332 -41
- package/commands/report.js +42 -8
- package/commands/run.js +9 -2
- package/commands/scout.js +9 -1
- package/commands/task.js +132 -16
- package/commands/team.js +212 -4
- package/commands/unknowns.js +15 -1
- package/commands/verify.js +46 -0
- package/commands/worktree.js +148 -18
- package/commands/write.js +12 -1
- package/commands/youtube.js +17 -2
- package/lib/accept-verify-gate.js +1 -0
- package/lib/auto-accept-certified.js +1 -0
- package/lib/autoland.js +13 -4
- package/lib/campaign-cadence.js +131 -0
- package/lib/campaign-core.js +330 -0
- package/lib/campaign-holdout.js +65 -0
- package/lib/campaign-loop.js +242 -0
- package/lib/campaign-missions.js +237 -0
- package/lib/campaign-new.js +251 -0
- package/lib/campaign-protect.js +75 -0
- package/lib/campaign-reading.js +147 -0
- package/lib/campaign-schema.js +196 -0
- package/lib/campaign-store.js +380 -0
- package/lib/campaign-sync.js +302 -0
- package/lib/campaign-trace.js +300 -0
- package/lib/conductor-artifacts.js +15 -1
- package/lib/daily-log.js +11 -1
- package/lib/dispatch-scout.js +18 -0
- package/lib/engine-ask.js +35 -4
- package/lib/engine-registry.js +2 -0
- package/lib/engine-resource.js +518 -0
- package/lib/engine-validate.js +189 -9
- package/lib/fleet.js +125 -16
- package/lib/init-commit.js +80 -0
- package/lib/intents.js +8 -0
- package/lib/known-commands.js +1 -1
- package/lib/land-green.js +58 -7
- package/lib/landing-proof.js +352 -0
- package/lib/member-autoland.js +479 -10
- package/lib/member-board.js +86 -10
- package/lib/member-map-fold.js +126 -0
- package/lib/member-map-inbox.js +80 -0
- package/lib/member-progress.js +48 -1
- package/lib/member-report.js +433 -0
- package/lib/member-scaffold.js +1 -2
- package/lib/member-self-next-job.js +97 -0
- package/lib/member-switches.js +7 -0
- package/lib/member-task-pick.js +41 -14
- package/lib/member-up.js +249 -16
- package/lib/roster-prep.js +25 -2
- package/lib/runner-command.js +5 -0
- package/lib/task-db.js +63 -11
- package/lib/task-explanation.js +7 -0
- package/lib/task-list-keeper.js +8 -1
- package/lib/task-receipt.js +6 -1
- package/mcp/atris-mcp/index.mjs +212 -10
- package/package.json +2 -1
- package/scripts/det/engine-reading.js +37 -0
- package/scripts/det/ytnotes +29 -5
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Graded judge readings: a number from 0 to 1 pulled from a judge's output,
|
|
2
|
+
// so a campaign learns "3 of 5 tests pass" instead of only "failed". Pure, no
|
|
3
|
+
// I/O. The reading rides on judge_ran as data.reading (lib/campaign-schema.js
|
|
4
|
+
// already allows it); traces use it as the reward signal
|
|
5
|
+
// (atris/features/campaign/traces.md).
|
|
6
|
+
//
|
|
7
|
+
// Formats, first match wins:
|
|
8
|
+
// reading: 0.6 a judge that states its own reading
|
|
9
|
+
// 3 of 5 live / 3/5 ok the judge's final line
|
|
10
|
+
// node --test ℹ pass 3 / ℹ fail 2 (also TAP "# pass 3", and "pass 3, fail 2")
|
|
11
|
+
// jest/vitest Tests: 1 failed, 4 passed, 5 total / Tests 1 failed | 4 passed (5)
|
|
12
|
+
// pytest 1 failed, 4 passed in 0.12s
|
|
13
|
+
// mocha 4 passing / 1 failing
|
|
14
|
+
// Output with no counts gives null: a reading is never invented.
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
function round(value) {
|
|
19
|
+
return Math.round(Math.min(1, Math.max(0, value)) * 10000) / 10000;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ratio(good, total) {
|
|
23
|
+
return total > 0 ? round(good / total) : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function count(line, word) {
|
|
27
|
+
const match = line.match(new RegExp(`(\\d+)\\s+${word}\\b`, 'i'));
|
|
28
|
+
return match ? Number(match[1]) : 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function stated(lines) {
|
|
32
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
33
|
+
const match = lines[i].match(/^reading\s*[:=]\s*(\d*\.?\d+)\s*$/i);
|
|
34
|
+
if (match) return round(Number(match[1]));
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function finalOf(lines) {
|
|
40
|
+
const last = lines[lines.length - 1] || '';
|
|
41
|
+
const match = last.match(/^(\d+)\s*(?:of|\/)\s*(\d+)\b/i);
|
|
42
|
+
if (!match || Number(match[1]) > Number(match[2])) return null;
|
|
43
|
+
return ratio(Number(match[1]), Number(match[2]));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// node --test prints one summary block per run; a judge that chains runs
|
|
47
|
+
// (a && b) gets the sum of every block.
|
|
48
|
+
function nodeCounts(text) {
|
|
49
|
+
const tokens = /(?:^|,)[ \t]*(?:ℹ|#)?[ \t]*(pass|fail|cancelled)[ \t]+(\d+)[ \t]*(?=,|$)/gim;
|
|
50
|
+
const sums = { pass: 0, fail: 0, cancelled: 0 };
|
|
51
|
+
let seen = false;
|
|
52
|
+
for (const match of text.matchAll(tokens)) {
|
|
53
|
+
sums[match[1].toLowerCase()] += Number(match[2]);
|
|
54
|
+
seen = true;
|
|
55
|
+
}
|
|
56
|
+
return seen ? ratio(sums.pass, sums.pass + sums.fail + sums.cancelled) : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function lastLine(lines, pattern) {
|
|
60
|
+
for (let i = lines.length - 1; i >= 0; i--) if (pattern.test(lines[i])) return lines[i];
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function jestCounts(lines) {
|
|
65
|
+
const line = lastLine(lines, /^Tests:?\s+.*\d+\s+(passed|failed)/i);
|
|
66
|
+
if (!line) return null;
|
|
67
|
+
return ratio(count(line, 'passed'), count(line, 'passed') + count(line, 'failed'));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function pytestCounts(lines) {
|
|
71
|
+
const line = lastLine(lines, /\d+\s+(passed|failed)\b/i);
|
|
72
|
+
if (!line) return null;
|
|
73
|
+
const errors = count(line, 'errors') || count(line, 'error');
|
|
74
|
+
return ratio(count(line, 'passed'), count(line, 'passed') + count(line, 'failed') + errors);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function mochaCounts(lines) {
|
|
78
|
+
const passing = lastLine(lines, /^\d+\s+passing\b/i);
|
|
79
|
+
if (!passing) return null;
|
|
80
|
+
const failing = lastLine(lines, /^\d+\s+failing\b/i);
|
|
81
|
+
const good = count(passing, 'passing');
|
|
82
|
+
return ratio(good, good + (failing ? count(failing, 'failing') : 0));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The reading in [0, 1], or null when the output has no counts.
|
|
86
|
+
function judgeReading(output) {
|
|
87
|
+
const text = String(output || '').replace(/\r/g, '');
|
|
88
|
+
// Strip color codes so a colored reporter reads the same as a piped one.
|
|
89
|
+
const plain = text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
90
|
+
const lines = plain.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
91
|
+
if (!lines.length) return null;
|
|
92
|
+
for (const read of [stated, finalOf]) {
|
|
93
|
+
const value = read(lines);
|
|
94
|
+
if (value !== null) return value;
|
|
95
|
+
}
|
|
96
|
+
const node = nodeCounts(lines.join('\n'));
|
|
97
|
+
if (node !== null) return node;
|
|
98
|
+
for (const read of [jestCounts, pytestCounts, mochaCounts]) {
|
|
99
|
+
const value = read(lines);
|
|
100
|
+
if (value !== null) return value;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const TAIL_LINES = 40;
|
|
106
|
+
const TAIL_BYTES = 4096;
|
|
107
|
+
|
|
108
|
+
// The end of the judge's output, so the planner sees more than one line:
|
|
109
|
+
// the last 40 lines, at most 4 KB, color codes removed.
|
|
110
|
+
function outputTail(output) {
|
|
111
|
+
const lines = String(output || '').replace(/\r/g, '').replace(/\x1b\[[0-9;]*m/g, '').split('\n');
|
|
112
|
+
while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
|
|
113
|
+
let tail = lines.slice(-TAIL_LINES).join('\n');
|
|
114
|
+
const bytes = Buffer.from(tail, 'utf8');
|
|
115
|
+
if (bytes.length > TAIL_BYTES) {
|
|
116
|
+
// Cut on a character boundary: drop continuation bytes at the cut.
|
|
117
|
+
let start = bytes.length - TAIL_BYTES;
|
|
118
|
+
while (start < bytes.length && (bytes[start] & 0xc0) === 0x80) start++;
|
|
119
|
+
tail = bytes.subarray(start).toString('utf8');
|
|
120
|
+
}
|
|
121
|
+
return tail;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The data a judge_ran event carries for one run of the judge command.
|
|
125
|
+
// A judge that prints no pass/fail counts still said something; its last line
|
|
126
|
+
// beats a bare exit code.
|
|
127
|
+
function lastSaidLine(text) {
|
|
128
|
+
const lines = String(text || '').split('\n').map((line) => line.trim()).filter(Boolean);
|
|
129
|
+
return lines.length ? lines[lines.length - 1].slice(0, 200) : '';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function judgeRanData(result) {
|
|
133
|
+
const data = { passed: result.passed, summary: result.summary || lastSaidLine(result.output_tail) || `exit ${result.exit}`, seconds: result.seconds };
|
|
134
|
+
if (typeof result.reading === 'number' && Number.isFinite(result.reading)) data.reading = result.reading;
|
|
135
|
+
if (typeof result.output_tail === 'string' && result.output_tail) data.output_tail = result.output_tail;
|
|
136
|
+
return data;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The output_tail of the latest judge run in fold order, or null.
|
|
140
|
+
function latestJudgeOutput(events) {
|
|
141
|
+
const { compareEvents } = require('./campaign-core');
|
|
142
|
+
const runs = (events || []).filter((e) => e.type === 'judge_ran').sort(compareEvents);
|
|
143
|
+
const last = runs[runs.length - 1];
|
|
144
|
+
return last && typeof last.data.output_tail === 'string' && last.data.output_tail ? last.data.output_tail : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { judgeReading, judgeRanData, latestJudgeOutput, outputTail };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Campaign schema: the struct, event types, and actions shared by every
|
|
2
|
+
// surface (CLI, backend, desktop, web, terrace). Pure, no I/O.
|
|
3
|
+
//
|
|
4
|
+
// The fold rules that turn events into state live in
|
|
5
|
+
// test/fixtures/campaign/README.md, which is the cross-surface contract.
|
|
6
|
+
// Spec: atris/features/campaign/idea.md.
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// idea.md "Events" list, plus two the spec's actions need but never named:
|
|
11
|
+
// add_budget writes budget_added, veto writes mission_vetoed, and judge_set
|
|
12
|
+
// changes the judge (found running real campaigns: a judge is not forever).
|
|
13
|
+
const EVENT_TYPES = Object.freeze([
|
|
14
|
+
'campaign_started', 'seat_filled', 'bet_set', 'attached',
|
|
15
|
+
'mission_proposed', 'mission_claimed', 'mission_started', 'step', 'kept', 'reverted', 'mission_ended',
|
|
16
|
+
'mission_vetoed',
|
|
17
|
+
'judge_ran', 'ask_opened', 'ask_answered',
|
|
18
|
+
'budget_reserved', 'budget_released', 'budget_added',
|
|
19
|
+
'paused', 'resumed', 'stopped', 'home_moved', 'done',
|
|
20
|
+
'judge_set',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const ACTIONS = Object.freeze([
|
|
24
|
+
'start', 'pause', 'resume', 'answer_ask', 'change_bet', 'add_budget', 'fill_seat', 'veto', 'stop',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const CONFIDENCE = Object.freeze(['open', 'leaning', 'stronger', 'answered']);
|
|
28
|
+
|
|
29
|
+
const STATES = Object.freeze([
|
|
30
|
+
'running', 'needs_you', 'resting', 'queued', 'done', 'out_of_budget', 'paused',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const STOP_REASONS = Object.freeze(['judge_passed', 'stopped', 'done_by_human', 'budget_spent']);
|
|
34
|
+
|
|
35
|
+
const MISSION_OUTCOMES = Object.freeze(['kept', 'reverted', 'failed']);
|
|
36
|
+
|
|
37
|
+
// What a person or agent writes. Everything else is folded from events.
|
|
38
|
+
const CAMPAIGN_FIELDS = Object.freeze({
|
|
39
|
+
title: { required: true, type: 'string' },
|
|
40
|
+
judge: { required: true, type: 'string' },
|
|
41
|
+
budget: { required: true, type: 'number' },
|
|
42
|
+
owner: { required: false, type: 'string' },
|
|
43
|
+
seats: { required: false, type: 'array' },
|
|
44
|
+
bet: { required: false, type: 'string' },
|
|
45
|
+
standing: { required: false, type: 'boolean' },
|
|
46
|
+
parent: { required: false, type: 'string' },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const CAMPAIGN_ID = /^cmp-[a-z0-9][a-z0-9-]{0,63}$/;
|
|
50
|
+
const EVENT_ID = /^[\x21-\x7e]{1,128}$/;
|
|
51
|
+
const ORIGIN = /^[A-Za-z0-9._:-]{1,64}$/;
|
|
52
|
+
// RFC 3339 with a zone: no surface may read a timestamp in its own timezone.
|
|
53
|
+
const TIMESTAMP = /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/;
|
|
54
|
+
|
|
55
|
+
function isObject(value) {
|
|
56
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function nonEmpty(value) {
|
|
60
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function positiveNumber(value) {
|
|
64
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function nonNegativeNumber(value) {
|
|
68
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// A seat is "member" (role comes from the member's own job) or { member, role }.
|
|
72
|
+
function seatErrors(seats) {
|
|
73
|
+
if (!Array.isArray(seats)) return ['seats_not_list'];
|
|
74
|
+
const errors = [];
|
|
75
|
+
for (const seat of seats) {
|
|
76
|
+
if (nonEmpty(seat)) continue;
|
|
77
|
+
if (isObject(seat) && nonEmpty(seat.member) && (seat.role === undefined || seat.role === null || nonEmpty(seat.role))) continue;
|
|
78
|
+
errors.push('bad_seat');
|
|
79
|
+
}
|
|
80
|
+
return errors;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// record: the hand-written struct. options.lookup(id) returns another
|
|
84
|
+
// campaign record (or null) so the one-level nesting rule can be checked.
|
|
85
|
+
function validateCampaign(record, options = {}) {
|
|
86
|
+
const errors = [];
|
|
87
|
+
if (!isObject(record)) return { ok: false, errors: ['not_object'] };
|
|
88
|
+
for (const key of Object.keys(record)) {
|
|
89
|
+
if (!Object.prototype.hasOwnProperty.call(CAMPAIGN_FIELDS, key)) errors.push(`unknown_field:${key}`);
|
|
90
|
+
}
|
|
91
|
+
if (!nonEmpty(record.title)) errors.push('missing_title');
|
|
92
|
+
if (!nonEmpty(record.judge)) errors.push('missing_judge');
|
|
93
|
+
if (!positiveNumber(record.budget)) errors.push('bad_budget');
|
|
94
|
+
if (record.owner !== undefined && !nonEmpty(record.owner)) errors.push('bad_owner');
|
|
95
|
+
if (record.bet !== undefined && !nonEmpty(record.bet)) errors.push('bad_bet');
|
|
96
|
+
if (record.standing !== undefined && typeof record.standing !== 'boolean') errors.push('bad_standing');
|
|
97
|
+
if (record.seats !== undefined) errors.push(...seatErrors(record.seats));
|
|
98
|
+
if (record.parent !== undefined) {
|
|
99
|
+
if (!nonEmpty(record.parent)) {
|
|
100
|
+
errors.push('bad_parent');
|
|
101
|
+
} else if (typeof options.lookup === 'function') {
|
|
102
|
+
const parent = options.lookup(record.parent);
|
|
103
|
+
if (!parent) errors.push('unknown_parent');
|
|
104
|
+
else if (parent.parent) errors.push('nested_parent');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { ok: errors.length === 0, errors };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Fields each event type must carry in its data.
|
|
111
|
+
const EVENT_DATA_RULES = Object.freeze({
|
|
112
|
+
campaign_started: (d) => [
|
|
113
|
+
nonEmpty(d.title) ? null : 'missing_title',
|
|
114
|
+
nonEmpty(d.judge) ? null : 'missing_judge',
|
|
115
|
+
positiveNumber(d.budget) ? null : 'bad_budget',
|
|
116
|
+
d.seats === undefined ? null : (seatErrors(d.seats)[0] || null),
|
|
117
|
+
d.standing === undefined || typeof d.standing === 'boolean' ? null : 'bad_standing',
|
|
118
|
+
],
|
|
119
|
+
seat_filled: (d) => [nonEmpty(d.member) ? null : 'missing_member', nonEmpty(d.role) ? null : 'missing_role'],
|
|
120
|
+
bet_set: (d) => [nonEmpty(d.bet) ? null : 'missing_bet'],
|
|
121
|
+
judge_set: (d) => [nonEmpty(d.judge) ? null : 'missing_judge'],
|
|
122
|
+
attached: (d) => [nonEmpty(d.parent_id) ? null : 'missing_parent_id'],
|
|
123
|
+
mission_proposed: (d) => [nonEmpty(d.mission_id) ? null : 'missing_mission_id'],
|
|
124
|
+
mission_claimed: (d) => [
|
|
125
|
+
nonEmpty(d.mission_id) ? null : 'missing_mission_id',
|
|
126
|
+
nonEmpty(d.claim_key) ? null : 'missing_claim_key',
|
|
127
|
+
],
|
|
128
|
+
mission_started: (d) => [
|
|
129
|
+
nonEmpty(d.mission_id) ? null : 'missing_mission_id',
|
|
130
|
+
nonEmpty(d.member) ? null : 'missing_member',
|
|
131
|
+
],
|
|
132
|
+
step: (d) => [nonEmpty(d.mission_id) ? null : 'missing_mission_id'],
|
|
133
|
+
kept: (d) => [nonEmpty(d.mission_id) ? null : 'missing_mission_id'],
|
|
134
|
+
reverted: (d) => [nonEmpty(d.mission_id) ? null : 'missing_mission_id'],
|
|
135
|
+
// Every mission reports its cost (idea.md review rule 1).
|
|
136
|
+
mission_ended: (d) => [
|
|
137
|
+
nonEmpty(d.mission_id) ? null : 'missing_mission_id',
|
|
138
|
+
MISSION_OUTCOMES.includes(d.outcome) ? null : 'bad_outcome',
|
|
139
|
+
nonNegativeNumber(d.cost_usd) ? null : 'missing_cost',
|
|
140
|
+
],
|
|
141
|
+
mission_vetoed: (d) => [nonEmpty(d.mission_id) ? null : 'missing_mission_id'],
|
|
142
|
+
judge_ran: (d) => [
|
|
143
|
+
typeof d.passed === 'boolean' ? null : 'missing_passed',
|
|
144
|
+
d.reading === undefined || (typeof d.reading === 'number' && Number.isFinite(d.reading)) ? null : 'bad_reading',
|
|
145
|
+
d.confidence === undefined || ['open', 'leaning', 'stronger'].includes(d.confidence) ? null : 'bad_confidence',
|
|
146
|
+
],
|
|
147
|
+
ask_opened: (d) => [nonEmpty(d.ask_id) ? null : 'missing_ask_id', nonEmpty(d.question) ? null : 'missing_question'],
|
|
148
|
+
ask_answered: (d) => [nonEmpty(d.ask_id) ? null : 'missing_ask_id', nonEmpty(d.answer) ? null : 'missing_answer'],
|
|
149
|
+
budget_reserved: (d) => [
|
|
150
|
+
nonEmpty(d.mission_id) ? null : 'missing_mission_id',
|
|
151
|
+
positiveNumber(d.amount_usd) ? null : 'bad_amount',
|
|
152
|
+
],
|
|
153
|
+
budget_released: (d) => [nonEmpty(d.mission_id) ? null : 'missing_mission_id'],
|
|
154
|
+
budget_added: (d) => [positiveNumber(d.amount_usd) ? null : 'bad_amount'],
|
|
155
|
+
paused: () => [],
|
|
156
|
+
resumed: () => [],
|
|
157
|
+
stopped: () => [],
|
|
158
|
+
home_moved: (d) => [nonEmpty(d.to) ? null : 'missing_to'],
|
|
159
|
+
done: () => [],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
function validateEvent(event) {
|
|
163
|
+
if (!isObject(event)) return { ok: false, errors: ['not_object'] };
|
|
164
|
+
const errors = [];
|
|
165
|
+
if (typeof event.id !== 'string' || !EVENT_ID.test(event.id)) errors.push('bad_id');
|
|
166
|
+
if (typeof event.campaign_id !== 'string' || !CAMPAIGN_ID.test(event.campaign_id)) errors.push('bad_campaign_id');
|
|
167
|
+
if (!EVENT_TYPES.includes(event.type)) errors.push('unknown_type');
|
|
168
|
+
if (typeof event.at !== 'string' || !TIMESTAMP.test(event.at) || Number.isNaN(Date.parse(event.at))) errors.push('bad_at');
|
|
169
|
+
if (typeof event.origin !== 'string' || !ORIGIN.test(event.origin)) errors.push('bad_origin');
|
|
170
|
+
if (!Number.isInteger(event.seq) || event.seq < 1) errors.push('bad_seq');
|
|
171
|
+
if (!nonEmpty(event.actor)) errors.push('missing_actor');
|
|
172
|
+
if (!isObject(event.data)) errors.push('bad_data');
|
|
173
|
+
if (errors.length === 0) {
|
|
174
|
+
for (const code of EVENT_DATA_RULES[event.type](event.data)) if (code) errors.push(code);
|
|
175
|
+
}
|
|
176
|
+
return { ok: errors.length === 0, errors };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isHumanActor(actor) {
|
|
180
|
+
return typeof actor === 'string' && actor.startsWith('human:');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
ACTIONS,
|
|
185
|
+
CAMPAIGN_FIELDS,
|
|
186
|
+
CAMPAIGN_ID,
|
|
187
|
+
CONFIDENCE,
|
|
188
|
+
EVENT_TYPES,
|
|
189
|
+
TIMESTAMP,
|
|
190
|
+
MISSION_OUTCOMES,
|
|
191
|
+
STATES,
|
|
192
|
+
STOP_REASONS,
|
|
193
|
+
isHumanActor,
|
|
194
|
+
validateCampaign,
|
|
195
|
+
validateEvent,
|
|
196
|
+
};
|