toolroll 0.9.36 → 0.9.38

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 (175) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/assignment-adapters.d.ts +12 -48
  3. package/dist/assignment-adapters.js +33 -43
  4. package/dist/browser/THIRD_PARTY_NOTICES.txt +3 -3
  5. package/dist/browser/workspace.css +1 -1
  6. package/dist/browser/workspace.js +26 -26
  7. package/dist/browser-crew.js +5 -0
  8. package/dist/browser-workspace.d.ts +2 -0
  9. package/dist/build-review.d.ts +1 -1
  10. package/dist/build-review.js +2 -1
  11. package/dist/chat-acceptance.d.ts +1 -1
  12. package/dist/chat-actions.d.ts +18 -12
  13. package/dist/chat-actions.js +15 -65
  14. package/dist/chat-ask.js +5 -5
  15. package/dist/chat-channel.js +20 -9
  16. package/dist/chat-decide.d.ts +18 -4
  17. package/dist/chat-decide.js +31 -11
  18. package/dist/chat-delivery-state.d.ts +12 -70
  19. package/dist/chat-delivery-state.js +17 -135
  20. package/dist/chat-delivery.js +58 -30
  21. package/dist/chat-flow.js +8 -6
  22. package/dist/chat-inbox.js +1 -1
  23. package/dist/coding-context.d.ts +2 -39
  24. package/dist/coding-context.js +2 -1
  25. package/dist/coding-handoff.d.ts +6 -25
  26. package/dist/coding-handoff.js +11 -5
  27. package/dist/coding-types.d.ts +2 -26
  28. package/dist/coding-workspace.d.ts +2 -0
  29. package/dist/coding-workspace.js +50 -14
  30. package/dist/contracts/acceptance-terms.d.ts +14 -0
  31. package/dist/contracts/acceptance-terms.js +16 -0
  32. package/dist/contracts/chat-actions.d.ts +328 -0
  33. package/dist/contracts/chat-actions.js +133 -0
  34. package/dist/contracts/chat-callback-rows.d.ts +59 -0
  35. package/dist/contracts/chat-callback-rows.js +46 -0
  36. package/dist/contracts/chat-content.d.ts +276 -0
  37. package/dist/contracts/chat-content.js +127 -0
  38. package/dist/contracts/chat-tables.d.ts +6 -0
  39. package/dist/contracts/chat-tables.js +136 -0
  40. package/dist/contracts/coding-context.d.ts +97 -0
  41. package/dist/contracts/coding-context.js +60 -0
  42. package/dist/contracts/coding-handoff.d.ts +44 -0
  43. package/dist/contracts/coding-handoff.js +47 -0
  44. package/dist/contracts/coding-workspace.d.ts +165 -0
  45. package/dist/contracts/coding-workspace.js +71 -0
  46. package/dist/contracts/contract.d.ts +7 -2
  47. package/dist/contracts/contract.js +21 -12
  48. package/dist/contracts/decision.d.ts +45 -0
  49. package/dist/contracts/decision.js +45 -0
  50. package/dist/contracts/discord-callback.d.ts +45 -0
  51. package/dist/contracts/discord-callback.js +34 -0
  52. package/dist/contracts/flow-card.d.ts +50 -0
  53. package/dist/contracts/flow-card.js +46 -0
  54. package/dist/contracts/flow-send.d.ts +119 -0
  55. package/dist/contracts/flow-send.js +112 -0
  56. package/dist/contracts/flow.d.ts +5 -0
  57. package/dist/contracts/flow.js +5 -0
  58. package/dist/contracts/gateway-tools.d.ts +374 -0
  59. package/dist/contracts/gateway-tools.js +94 -0
  60. package/dist/contracts/lead-context.d.ts +165 -0
  61. package/dist/contracts/lead-context.js +73 -0
  62. package/dist/contracts/lead-tools.d.ts +1905 -0
  63. package/dist/contracts/lead-tools.js +265 -0
  64. package/dist/contracts/memory-pass.d.ts +158 -0
  65. package/dist/contracts/memory-pass.js +102 -0
  66. package/dist/contracts/project-knowledge.d.ts +126 -0
  67. package/dist/contracts/project-knowledge.js +93 -0
  68. package/dist/contracts/project-memory.d.ts +117 -0
  69. package/dist/contracts/project-memory.js +51 -0
  70. package/dist/contracts/project-skills.d.ts +121 -0
  71. package/dist/contracts/project-skills.js +76 -0
  72. package/dist/contracts/review-findings.d.ts +46 -0
  73. package/dist/contracts/review-findings.js +48 -0
  74. package/dist/contracts/route.d.ts +484 -0
  75. package/dist/contracts/route.js +128 -0
  76. package/dist/contracts/scope.d.ts +105 -0
  77. package/dist/contracts/scope.js +114 -0
  78. package/dist/contracts/scout-report.d.ts +234 -0
  79. package/dist/contracts/scout-report.js +141 -0
  80. package/dist/contracts/slack-callback.d.ts +43 -0
  81. package/dist/contracts/slack-callback.js +35 -0
  82. package/dist/contracts/sort-answer.d.ts +83 -0
  83. package/dist/contracts/sort-answer.js +77 -0
  84. package/dist/contracts/stage-output.d.ts +127 -0
  85. package/dist/contracts/stage-output.js +166 -0
  86. package/dist/contracts/task-sizing.d.ts +24 -0
  87. package/dist/contracts/task-sizing.js +28 -0
  88. package/dist/contracts/teammate-turn.d.ts +43 -0
  89. package/dist/contracts/teammate-turn.js +51 -0
  90. package/dist/contracts/teams-callback.d.ts +14 -0
  91. package/dist/contracts/teams-callback.js +14 -0
  92. package/dist/contracts/telegram-callback.d.ts +120 -0
  93. package/dist/contracts/telegram-callback.js +97 -0
  94. package/dist/decision.d.ts +11 -20
  95. package/dist/decision.js +86 -167
  96. package/dist/desktop-update.d.ts +1 -1
  97. package/dist/discord-chat.js +31 -20
  98. package/dist/flow-engine.js +14 -17
  99. package/dist/flow-items.d.ts +4 -13
  100. package/dist/flow-items.js +1 -15
  101. package/dist/flow-send.d.ts +6 -37
  102. package/dist/flow-send.js +18 -14
  103. package/dist/flow-share.js +7 -1
  104. package/dist/flow-sort.d.ts +7 -26
  105. package/dist/flow-sort.js +25 -20
  106. package/dist/flow-steps.js +3 -2
  107. package/dist/flows.d.ts +2 -4
  108. package/dist/flows.js +9 -5
  109. package/dist/invoke.js +4 -4
  110. package/dist/lead-context.d.ts +4 -2
  111. package/dist/lead-context.js +24 -17
  112. package/dist/mate-progress.d.ts +26 -0
  113. package/dist/mate-progress.js +24 -0
  114. package/dist/mate-tools.d.ts +4 -0
  115. package/dist/mate-tools.js +108 -275
  116. package/dist/mate.js +11 -2
  117. package/dist/mcp-connect.d.ts +24 -4
  118. package/dist/mcp-connect.js +78 -15
  119. package/dist/mcp.js +84 -216
  120. package/dist/memory-pass.d.ts +17 -42
  121. package/dist/memory-pass.js +54 -26
  122. package/dist/phase-routing.d.ts +19 -72
  123. package/dist/phase-routing.js +34 -164
  124. package/dist/project-knowledge.d.ts +16 -35
  125. package/dist/project-knowledge.js +42 -21
  126. package/dist/project-memory.d.ts +2 -28
  127. package/dist/project-memory.js +28 -13
  128. package/dist/project-skills.d.ts +16 -38
  129. package/dist/project-skills.js +41 -32
  130. package/dist/project-tools.d.ts +1 -0
  131. package/dist/project-tools.js +4 -4
  132. package/dist/provider.d.ts +2 -0
  133. package/dist/provider.js +4 -16
  134. package/dist/result-shots.d.ts +1 -1
  135. package/dist/result-shots.js +2 -1
  136. package/dist/review-switch.d.ts +3 -8
  137. package/dist/reviewer.d.ts +8 -12
  138. package/dist/reviewer.js +11 -35
  139. package/dist/scope.d.ts +21 -43
  140. package/dist/scope.js +42 -128
  141. package/dist/scout-report.d.ts +9 -194
  142. package/dist/scout-report.js +113 -264
  143. package/dist/scout.js +15 -11
  144. package/dist/serve.js +23 -5
  145. package/dist/setup-guide.d.ts +2 -2
  146. package/dist/slack-chat.js +26 -20
  147. package/dist/slack-state.js +2 -1
  148. package/dist/store.d.ts +9 -32
  149. package/dist/store.js +18 -15
  150. package/dist/structured-output.js +2 -1
  151. package/dist/subscription-chat.d.ts +4 -0
  152. package/dist/subscription-chat.js +4 -1
  153. package/dist/task-review-brief.d.ts +1 -7
  154. package/dist/task-review-brief.js +6 -6
  155. package/dist/task-sizing.d.ts +3 -27
  156. package/dist/task-sizing.js +11 -26
  157. package/dist/task-text.d.ts +0 -5
  158. package/dist/task-text.js +1 -3
  159. package/dist/teammate-question.js +7 -5
  160. package/dist/teammate-work.js +9 -5
  161. package/dist/teammates.d.ts +13 -64
  162. package/dist/teammates.js +32 -33
  163. package/dist/teams-chat.js +20 -11
  164. package/dist/telegram-flow.d.ts +9 -0
  165. package/dist/telegram-flow.js +44 -8
  166. package/dist/telegram-mate.d.ts +4 -14
  167. package/dist/telegram-mate.js +9 -8
  168. package/dist/telegram-team.d.ts +2 -19
  169. package/dist/telegram.d.ts +1 -1
  170. package/dist/telegram.js +94 -68
  171. package/dist/text-limits.d.ts +142 -1
  172. package/dist/text-limits.js +144 -1
  173. package/dist/tools-ui.d.ts +2 -1
  174. package/dist/tools-ui.js +7 -4
  175. package/package.json +17 -12
@@ -1,44 +1,14 @@
1
1
  /**
2
- * The scout's terminal handoff (mate arc §10), parsed with the 422 rule:
3
- * fail closed, every problem reported at once, stable reasons, caps and
4
- * control-character rejection on every string. A report reaches the task
5
- * page, the ledger, the terminal, and — through mateView — the mate; and
6
- * each follow-up becomes a filing's title and goal at one tap, so it gets
7
- * the park discipline exactly as the plan does.
2
+ * The scout's terminal handoff (mate arc §10), read against the report contract (src/contracts/scout-report.ts) with
3
+ * the 422 rule: fail closed, every problem reported at once, each naming its path (`items[0].url: must be an http or
4
+ * https address`), stable reasons, caps and control-character rejection on every string. A report reaches the task
5
+ * page, the ledger, the terminal, and — through mateView — the mate; and each follow-up becomes a filing's title and
6
+ * goal at one tap, so it gets the park discipline exactly as the plan does.
8
7
  */
9
- export type ReportProblem = {
10
- reason: string;
11
- message: string;
12
- };
13
- export type ParsedReport = {
14
- title: string;
15
- summary: string;
16
- /** The report document, markdown, rendered fenced-inert everywhere. */
17
- report: string;
18
- /** Proposed follow-ups: each files as a task in the same repository. */
19
- followUps: {
20
- title: string;
21
- goal: string;
22
- }[];
23
- /** What the scout found, as a short list later steps can read: each cites its URL, and may show one of `images`. */
24
- items: ReportItem[];
25
- /** Screenshots the scout saved during the run. From the scout, `file` names a file in its output folder; once
26
- * stored, the runner adds the bytes' sha256 and the evidence row that holds them. */
27
- images: ReportImage[];
28
- };
29
- export type ReportItem = {
30
- title: string;
31
- why: string;
32
- url: string;
33
- image: string | null;
34
- };
35
- export type ReportImage = {
36
- file: string;
37
- caption: string;
38
- url: string;
39
- sha256?: string;
40
- artifact?: number;
41
- };
8
+ import { type ContractProblem } from "./contracts/contract.js";
9
+ import { type ParsedReport } from "./contracts/scout-report.js";
10
+ export { REPORT_IMAGE_FILE, REPORT_LIMITS, REPORT_VERSION, SCOUT_OUTPUT_JSON_SCHEMA, type ParsedReport, type ReportImage, type ReportItem, } from "./contracts/scout-report.js";
11
+ export type ReportProblem = ContractProblem;
42
12
  export type ReportParseResult = {
43
13
  ok: true;
44
14
  report: ParsedReport;
@@ -46,161 +16,6 @@ export type ReportParseResult = {
46
16
  ok: false;
47
17
  problems: ReportProblem[];
48
18
  };
49
- /** Caps are BYTES of UTF-8 (v4 review, finding 11): a 64 KiB report is
50
- * 64 KiB whatever script it is written in. They hold what a scout writes;
51
- * a stored report is read without them (parseReport's `stored`). */
52
- export declare const REPORT_LIMITS: {
53
- readonly payload: number;
54
- readonly title: 200;
55
- readonly summary: 2500;
56
- readonly document: number;
57
- readonly followUps: 5;
58
- readonly followUpTitle: 200;
59
- /** A follow-up files as a task: its goal is held to the task goal limit, in bytes of the same count. */
60
- readonly followUpGoal: 8000;
61
- readonly items: 6;
62
- readonly itemTitle: 200;
63
- readonly itemWhy: 1000;
64
- readonly url: 2000;
65
- readonly images: 8;
66
- readonly caption: 300;
67
- };
68
- /** A screenshot's name in the scout's output folder: one plain file name, PNG or JPEG, never a path. */
69
- export declare const REPORT_IMAGE_FILE: RegExp;
70
- /**
71
- * The scout's handback for Claude's `--json-schema` (run 2334's fix): plan
72
- * mode only lets a session write its own plan file, so a Claude scout
73
- * returns a report — or a question for the operator — as structured output
74
- * on the terminal result event. Shape only, never the validator: byte caps,
75
- * one-line titles and control characters stay `parseReport`'s and
76
- * `parseDecision`'s, applied to the re-serialized body.
77
- */
78
- export declare const SCOUT_OUTPUT_JSON_SCHEMA: {
79
- readonly type: "object";
80
- readonly properties: {
81
- readonly kind: {
82
- readonly type: "string";
83
- readonly enum: readonly ["report", "question"];
84
- };
85
- readonly report: {
86
- readonly type: "object";
87
- readonly properties: {
88
- readonly title: {
89
- readonly type: "string";
90
- };
91
- readonly summary: {
92
- readonly type: "string";
93
- };
94
- readonly report: {
95
- readonly type: "string";
96
- };
97
- readonly followUps: {
98
- readonly type: "array";
99
- readonly items: {
100
- readonly type: "object";
101
- readonly properties: {
102
- readonly title: {
103
- readonly type: "string";
104
- };
105
- readonly goal: {
106
- readonly type: "string";
107
- };
108
- };
109
- readonly required: readonly ["title", "goal"];
110
- readonly additionalProperties: false;
111
- };
112
- };
113
- readonly items: {
114
- readonly type: "array";
115
- readonly items: {
116
- readonly type: "object";
117
- readonly properties: {
118
- readonly title: {
119
- readonly type: "string";
120
- };
121
- readonly why: {
122
- readonly type: "string";
123
- };
124
- readonly url: {
125
- readonly type: "string";
126
- };
127
- readonly image: {
128
- readonly type: "string";
129
- };
130
- };
131
- readonly required: readonly ["title", "why", "url"];
132
- readonly additionalProperties: false;
133
- };
134
- };
135
- readonly images: {
136
- readonly type: "array";
137
- readonly items: {
138
- readonly type: "object";
139
- readonly properties: {
140
- readonly file: {
141
- readonly type: "string";
142
- };
143
- readonly caption: {
144
- readonly type: "string";
145
- };
146
- readonly url: {
147
- readonly type: "string";
148
- };
149
- };
150
- readonly required: readonly ["file", "caption", "url"];
151
- readonly additionalProperties: false;
152
- };
153
- };
154
- };
155
- readonly required: readonly ["title", "summary", "report"];
156
- readonly additionalProperties: false;
157
- };
158
- readonly decision: {
159
- readonly type: "object";
160
- readonly properties: {
161
- readonly urgency: {
162
- readonly type: "string";
163
- readonly enum: readonly ["blocking"];
164
- };
165
- readonly recap: {
166
- readonly type: "string";
167
- };
168
- readonly question: {
169
- readonly type: "string";
170
- };
171
- readonly options: {
172
- readonly type: "array";
173
- readonly items: {
174
- readonly type: "object";
175
- readonly properties: {
176
- readonly id: {
177
- readonly type: "string";
178
- };
179
- readonly label: {
180
- readonly type: "string";
181
- };
182
- readonly consequence: {
183
- readonly type: "string";
184
- };
185
- readonly reversible: {
186
- readonly type: "boolean";
187
- };
188
- };
189
- readonly required: readonly ["id", "label", "consequence", "reversible"];
190
- readonly additionalProperties: false;
191
- };
192
- };
193
- readonly recommendation: {
194
- readonly type: "string";
195
- };
196
- };
197
- readonly required: readonly ["urgency", "recap", "question", "options", "recommendation"];
198
- readonly additionalProperties: false;
199
- };
200
- };
201
- readonly required: readonly ["kind"];
202
- readonly additionalProperties: false;
203
- };
204
19
  /** A report, checked. `stored`: one already kept as evidence, read without the length caps (a report written under
205
20
  * higher limits stays readable); every other check still holds. */
206
21
  export declare function parseReport(raw: string, options?: {
@@ -1,285 +1,138 @@
1
1
  /**
2
- * The scout's terminal handoff (mate arc §10), parsed with the 422 rule:
3
- * fail closed, every problem reported at once, stable reasons, caps and
4
- * control-character rejection on every string. A report reaches the task
5
- * page, the ledger, the terminal, and — through mateView — the mate; and
6
- * each follow-up becomes a filing's title and goal at one tap, so it gets
7
- * the park discipline exactly as the plan does.
2
+ * The scout's terminal handoff (mate arc §10), read against the report contract (src/contracts/scout-report.ts) with
3
+ * the 422 rule: fail closed, every problem reported at once, each naming its path (`items[0].url: must be an http or
4
+ * https address`), stable reasons, caps and control-character rejection on every string. A report reaches the task
5
+ * page, the ledger, the terminal, and — through mateView — the mate; and each follow-up becomes a filing's title and
6
+ * goal at one tap, so it gets the park discipline exactly as the plan does.
8
7
  */
8
+ import { contractProblemOf } from "./contracts/contract.js";
9
+ import { FOLLOW_UP_TEXT_FIELDS, IMAGE_TEXT_FIELDS, ITEM_TEXT_FIELDS, readReportPayload, REPORT_LIMITS, REPORT_TEXT_FIELDS, reportPayloadBody, } from "./contracts/scout-report.js";
9
10
  import { hasForbiddenControls } from "./decision.js";
10
- import { TASK_TEXT_LIMITS } from "./task-text.js";
11
11
  import { TEXT_LIMITS } from "./text-limits.js";
12
- /** Caps are BYTES of UTF-8 (v4 review, finding 11): a 64 KiB report is
13
- * 64 KiB whatever script it is written in. They hold what a scout writes;
14
- * a stored report is read without them (parseReport's `stored`). */
15
- export const REPORT_LIMITS = {
16
- payload: 96 * 1024,
17
- title: 200,
18
- summary: TEXT_LIMITS.reportSummary,
19
- document: 64 * 1024,
20
- followUps: 5,
21
- followUpTitle: 200,
22
- /** A follow-up files as a task: its goal is held to the task goal limit, in bytes of the same count. */
23
- followUpGoal: TASK_TEXT_LIMITS.text,
24
- items: 6,
25
- itemTitle: 200,
26
- itemWhy: 1_000,
27
- url: 2_000,
28
- images: 8,
29
- caption: 300,
30
- };
31
- /** A screenshot's name in the scout's output folder: one plain file name, PNG or JPEG, never a path. */
32
- export const REPORT_IMAGE_FILE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}\.(?:png|jpe?g)$/i;
33
- const REPORT_SHAPE = {
34
- type: "object",
35
- properties: {
36
- title: { type: "string" },
37
- summary: { type: "string" },
38
- report: { type: "string" },
39
- followUps: {
40
- type: "array",
41
- items: {
42
- type: "object",
43
- properties: { title: { type: "string" }, goal: { type: "string" } },
44
- required: ["title", "goal"],
45
- additionalProperties: false,
46
- },
47
- },
48
- items: {
49
- type: "array",
50
- items: {
51
- type: "object",
52
- properties: { title: { type: "string" }, why: { type: "string" }, url: { type: "string" }, image: { type: "string" } },
53
- required: ["title", "why", "url"],
54
- additionalProperties: false,
55
- },
56
- },
57
- images: {
58
- type: "array",
59
- items: {
60
- type: "object",
61
- properties: { file: { type: "string" }, caption: { type: "string" }, url: { type: "string" } },
62
- required: ["file", "caption", "url"],
63
- additionalProperties: false,
64
- },
65
- },
66
- },
67
- required: ["title", "summary", "report"],
68
- additionalProperties: false,
69
- };
70
- /** The park mailbox's decision, the same fields `parseDecision` reads. */
71
- const DECISION_SHAPE = {
72
- type: "object",
73
- properties: {
74
- urgency: { type: "string", enum: ["blocking"] },
75
- recap: { type: "string" },
76
- question: { type: "string" },
77
- options: {
78
- type: "array",
79
- items: {
80
- type: "object",
81
- properties: {
82
- id: { type: "string" },
83
- label: { type: "string" },
84
- consequence: { type: "string" },
85
- reversible: { type: "boolean" },
86
- },
87
- required: ["id", "label", "consequence", "reversible"],
88
- additionalProperties: false,
89
- },
90
- },
91
- recommendation: { type: "string" },
92
- },
93
- required: ["urgency", "recap", "question", "options", "recommendation"],
94
- additionalProperties: false,
95
- };
96
- /**
97
- * The scout's handback for Claude's `--json-schema` (run 2334's fix): plan
98
- * mode only lets a session write its own plan file, so a Claude scout
99
- * returns a report — or a question for the operator — as structured output
100
- * on the terminal result event. Shape only, never the validator: byte caps,
101
- * one-line titles and control characters stay `parseReport`'s and
102
- * `parseDecision`'s, applied to the re-serialized body.
103
- */
104
- export const SCOUT_OUTPUT_JSON_SCHEMA = {
105
- type: "object",
106
- properties: {
107
- kind: { type: "string", enum: ["report", "question"] },
108
- report: REPORT_SHAPE,
109
- decision: DECISION_SHAPE,
110
- },
111
- required: ["kind"],
112
- additionalProperties: false,
113
- };
12
+ export { REPORT_IMAGE_FILE, REPORT_LIMITS, REPORT_VERSION, SCOUT_OUTPUT_JSON_SCHEMA, } from "./contracts/scout-report.js";
114
13
  function refuse(reason, message) {
115
14
  return { ok: false, problems: [{ reason, message }] };
116
15
  }
117
- function describe(value) {
118
- if (value === undefined)
119
- return "nothing";
120
- if (value === null)
121
- return "null";
122
- if (typeof value === "string")
123
- return `a ${value.length}-char string`;
124
- return `a ${Array.isArray(value) ? "array" : typeof value}`;
125
- }
126
- function prose(value, field, cap, problems) {
127
- if (value === undefined || value === null || value === "" || (typeof value === "string" && value.trim() === "")) {
128
- problems.push({ reason: `missing-${field}`, message: `${field} is required` });
129
- return null;
130
- }
131
- if (typeof value !== "string") {
132
- problems.push({ reason: `bad-${field}`, message: `${field} must be a string (got ${describe(value)})` });
133
- return null;
134
- }
135
- if (!readingStored && Buffer.byteLength(value, "utf8") > cap) {
136
- problems.push({ reason: `${field}-too-long`, message: `${field} is over ${cap} bytes` });
137
- return null;
138
- }
139
- if (hasForbiddenControls(value)) {
140
- problems.push({ reason: `${field}-controls`, message: `${field} carries control characters that could become terminal escapes` });
141
- return null;
142
- }
143
- return value;
144
- }
145
- /** True while a stored report is read: a limit is for writing, and reading never re-checks a text's length. */
146
- let readingStored = false;
147
16
  /** A report, checked. `stored`: one already kept as evidence, read without the length caps (a report written under
148
17
  * higher limits stays readable); every other check still holds. */
149
18
  export function parseReport(raw, options = {}) {
150
- readingStored = options.stored === true;
151
- try {
152
- return parseReportBody(raw);
153
- }
154
- finally {
155
- readingStored = false;
156
- }
157
- }
158
- function parseReportBody(raw) {
159
- if (!readingStored && Buffer.byteLength(raw, "utf8") > REPORT_LIMITS.payload) {
160
- return refuse("too-large", `the payload is over ${REPORT_LIMITS.payload} bytes`);
19
+ const stored = options.stored === true;
20
+ if (!stored && Buffer.byteLength(raw, "utf8") > REPORT_LIMITS.payload) {
21
+ return refuse("too-large", `payload: over ${REPORT_LIMITS.payload.toLocaleString("en-US")} bytes`);
161
22
  }
162
23
  let parsed;
163
24
  try {
164
25
  parsed = JSON.parse(raw);
165
26
  }
166
27
  catch (error) {
167
- return refuse("not-json", `the payload is not JSON: ${error instanceof Error ? error.message : String(error)}`);
168
- }
169
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
170
- return refuse("not-an-object", "the payload must be one JSON object");
171
- }
172
- const body = parsed;
173
- const problems = [];
174
- const title = prose(body["title"], "title", REPORT_LIMITS.title, problems);
175
- const summary = prose(body["summary"], "summary", REPORT_LIMITS.summary, problems);
176
- const document = prose(body["report"], "report", REPORT_LIMITS.document, problems);
177
- if (title !== null && /[\n\r]/.test(title)) {
178
- problems.push({ reason: "title-multiline", message: "title must be one line" });
179
- }
180
- // One paragraph: the summary is what the operator reads first, on a
181
- // phone, in a digest line — a blank line inside it is a second paragraph.
182
- if (summary !== null && /\n[ \t]*\n/.test(summary)) {
183
- problems.push({ reason: "summary-paragraphs", message: "summary is one paragraph — no blank lines" });
184
- }
185
- const followUps = [];
186
- if (body["followUps"] !== undefined && body["followUps"] !== null) {
187
- if (!Array.isArray(body["followUps"])) {
188
- problems.push({ reason: "bad-followUps", message: `followUps must be an array (got ${describe(body["followUps"])})` });
189
- }
190
- else if (body["followUps"].length > REPORT_LIMITS.followUps) {
191
- problems.push({ reason: "followUps-too-many", message: `followUps lists ${body["followUps"].length} — cap is ${REPORT_LIMITS.followUps}` });
192
- }
193
- else {
194
- for (const [index, one] of body["followUps"].entries()) {
195
- if (typeof one !== "object" || one === null || Array.isArray(one)) {
196
- problems.push({ reason: `followUps[${index}]-shape`, message: `followUps[${index}] must be {title, goal}` });
197
- continue;
198
- }
199
- const entry = one;
200
- const followTitle = prose(entry["title"], `followUps[${index}].title`, REPORT_LIMITS.followUpTitle, problems);
201
- const goal = prose(entry["goal"], `followUps[${index}].goal`, REPORT_LIMITS.followUpGoal, problems);
202
- if (followTitle !== null && /[\n\r]/.test(followTitle)) {
203
- problems.push({ reason: `followUps[${index}]-title-multiline`, message: `followUps[${index}].title must be one line` });
204
- }
205
- else if (followTitle !== null && goal !== null) {
206
- followUps.push({ title: followTitle, goal });
207
- }
208
- }
209
- }
210
- }
211
- const images = listOf(body["images"], "images", REPORT_LIMITS.images, problems, (entry, at) => {
212
- // Any one line here: the runner refuses an image that isn't a plain file name in its output folder, not the report.
213
- const file = prose(entry["file"], `${at}.file`, REPORT_LIMITS.caption, problems);
214
- const caption = prose(entry["caption"], `${at}.caption`, REPORT_LIMITS.caption, problems);
215
- const url = link(entry["url"], `${at}.url`, problems);
216
- if (file !== null && /[\n\r]/.test(file))
217
- problems.push({ reason: `${at}-file-multiline`, message: `${at}.file must be one line` });
218
- if (caption !== null && /[\n\r]/.test(caption))
219
- problems.push({ reason: `${at}-caption-multiline`, message: `${at}.caption must be one line` });
220
- const sha256 = entry["sha256"];
221
- if (sha256 !== undefined && (typeof sha256 !== "string" || !/^[0-9a-f]{64}$/.test(sha256)))
222
- problems.push({ reason: `${at}-sha256`, message: `${at}.sha256 must be a sha256 hex digest` });
223
- const artifact = entry["artifact"];
224
- if (artifact !== undefined && (typeof artifact !== "number" || !Number.isSafeInteger(artifact) || artifact <= 0))
225
- problems.push({ reason: `${at}-artifact`, message: `${at}.artifact must be an evidence id` });
226
- if (file === null || caption === null || url === null || /[\n\r]/.test(file) || /[\n\r]/.test(caption))
227
- return null;
228
- return { file, caption, url, ...(typeof sha256 === "string" ? { sha256 } : {}), ...(typeof artifact === "number" ? { artifact } : {}) };
229
- });
230
- const files = new Set();
231
- for (const one of images) {
232
- if (files.has(one.file))
233
- problems.push({ reason: "images-duplicate", message: `images names ${one.file} twice` });
234
- files.add(one.file);
235
- }
236
- const items = listOf(body["items"], "items", REPORT_LIMITS.items, problems, (entry, at) => {
237
- const itemTitle = prose(entry["title"], `${at}.title`, REPORT_LIMITS.itemTitle, problems);
238
- const why = prose(entry["why"], `${at}.why`, REPORT_LIMITS.itemWhy, problems);
239
- const url = link(entry["url"], `${at}.url`, problems);
240
- const image = entry["image"] === undefined || entry["image"] === null || entry["image"] === "" ? null : entry["image"];
241
- if (itemTitle !== null && /[\n\r]/.test(itemTitle))
242
- problems.push({ reason: `${at}-title-multiline`, message: `${at}.title must be one line` });
243
- if (image !== null && (typeof image !== "string" || !files.has(image)))
244
- problems.push({ reason: `${at}-image`, message: `${at}.image must name one of images by its file` });
245
- if (itemTitle === null || why === null || url === null || /[\n\r]/.test(itemTitle) || (image !== null && (typeof image !== "string" || !files.has(image))))
246
- return null;
247
- return { title: itemTitle, why, url, image: image };
248
- });
249
- if (problems.length > 0)
28
+ return refuse("not-json", `payload: not JSON: ${error instanceof Error ? error.message : String(error)}`);
29
+ }
30
+ const body = reportPayloadBody(parsed);
31
+ if (body === null)
32
+ return refuse("not-an-object", "payload: must be one JSON object");
33
+ const read = readReportPayload(parsed, { stored });
34
+ // Every report limit counts UTF-8 bytes, including those TEXT_LIMITS names without "Bytes" (a summary, a goal).
35
+ const issues = (read.ok ? [] : read.issues).map(issue => (issue.kind === "too-long" ? { ...issue, line: issue.line.replace(/ characters$/, " bytes") } : issue));
36
+ const contract = issues.map(contractProblemOf);
37
+ if (contract.some(problem => problem.reason === "newer-version" || problem.reason === "bad-version"))
38
+ return { ok: false, problems: contract };
39
+ const problems = [...contract, ...reportRuleProblems(body, issues, stored)];
40
+ if (!read.ok || problems.length > 0)
250
41
  return { ok: false, problems };
251
- return { ok: true, report: { title: title, summary: summary, report: document, followUps, items, images } };
42
+ const payload = read.value;
43
+ return {
44
+ ok: true,
45
+ report: {
46
+ title: payload.title,
47
+ summary: payload.summary,
48
+ report: payload.report,
49
+ followUps: (payload.followUps ?? []).map(one => ({ title: one.title, goal: one.goal })),
50
+ items: (payload.items ?? []).map(one => ({ title: one.title, why: one.why, url: one.url, image: one.image === undefined || one.image === null || one.image === "" ? null : one.image })),
51
+ images: (payload.images ?? []).map(one => ({
52
+ file: one.file,
53
+ caption: one.caption,
54
+ url: one.url,
55
+ ...(one.sha256 === undefined ? {} : { sha256: one.sha256 }),
56
+ ...(one.artifact === undefined ? {} : { artifact: one.artifact }),
57
+ })),
58
+ },
59
+ };
252
60
  }
253
- /** An optional capped list of objects, each read by `one`; problems are reported, never thrown. */
254
- function listOf(value, field, cap, problems, one) {
255
- if (value === undefined || value === null)
256
- return [];
257
- if (!Array.isArray(value)) {
258
- problems.push({ reason: `bad-${field}`, message: `${field} must be an array (got ${describe(value)})` });
259
- return [];
260
- }
261
- if (value.length > cap) {
262
- problems.push({ reason: `${field}-too-many`, message: `${field} lists ${value.length} — cap is ${cap}` });
263
- return [];
264
- }
265
- const kept = [];
266
- for (const [index, entry] of value.entries()) {
267
- const at = `${field}[${index}]`;
268
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
269
- problems.push({ reason: `${at}-shape`, message: `${at} must be an object` });
61
+ /**
62
+ * The rules JSON Schema cannot state, in plain code with path-named errors: blank text, byte caps (on write only),
63
+ * control characters, one-line fields, a one-paragraph summary, safe links, a screenshot named twice and an item
64
+ * naming one the report doesn't have. A field the contract already refused (or one inside it) is skipped, so every
65
+ * problem is reported once and all at once.
66
+ */
67
+ function reportRuleProblems(body, issues, stored) {
68
+ const problems = [];
69
+ // An unknown key is about its object, not the fields beside it.
70
+ const refused = issues.filter(issue => issue.kind !== "unknown-key").map(issue => issue.path);
71
+ const touched = (path) => refused.some(at => at === path || path.startsWith(`${at}.`) || path.startsWith(`${at}[`) || at.startsWith(`${path}.`) || at.startsWith(`${path}[`));
72
+ const say = (path, reason, what) => problems.push({ reason, message: `${path}: ${what}` });
73
+ /** One text field checked; true when it passed every rule. */
74
+ const text = (value, path, rule) => {
75
+ if (typeof value !== "string" || touched(path))
76
+ return false;
77
+ const limit = TEXT_LIMITS[rule.limit];
78
+ if (value.trim() === "")
79
+ say(path, `missing-${path}`, "must not be empty");
80
+ else if (!stored && Buffer.byteLength(value, "utf8") > limit)
81
+ say(path, `${path}-too-long`, `over ${limit.toLocaleString("en-US")} bytes`);
82
+ else if (hasForbiddenControls(value))
83
+ say(path, `${path}-controls`, "carries control characters that could become terminal escapes");
84
+ else if (rule.oneLine === true && /[\n\r]/.test(value))
85
+ say(path, `${path}-multiline`, "must be one line");
86
+ else if (rule.link === true && !isLink(value))
87
+ say(path, `${path}-not-a-link`, "must be an http or https address");
88
+ else
89
+ return true;
90
+ return false;
91
+ };
92
+ const entries = (list) => {
93
+ const value = body[list];
94
+ if (!Array.isArray(value) || touched(list))
95
+ return [];
96
+ return value.flatMap((entry, index) => typeof entry === "object" && entry !== null && !Array.isArray(entry) ? [[entry, `${list}[${index}]`]] : []);
97
+ };
98
+ const fields = (entry, at, rules) => {
99
+ for (const [field, rule] of Object.entries(rules))
100
+ text(entry[field], at === "" ? field : `${at}.${field}`, rule);
101
+ };
102
+ fields(body, "", REPORT_TEXT_FIELDS);
103
+ // One paragraph: the summary is what the operator reads first, on a phone, in a digest line — a blank line inside
104
+ // it is a second paragraph.
105
+ const summary = body["summary"];
106
+ if (typeof summary === "string" && !problems.some(problem => problem.message.startsWith("summary:")) && !touched("summary") && /\n[ \t]*\n/.test(summary)) {
107
+ say("summary", "summary-paragraphs", "one paragraph — no blank lines");
108
+ }
109
+ for (const [entry, at] of entries("followUps"))
110
+ fields(entry, at, FOLLOW_UP_TEXT_FIELDS);
111
+ const files = new Map();
112
+ for (const [entry, at] of entries("images")) {
113
+ fields(entry, at, IMAGE_TEXT_FIELDS);
114
+ const file = entry["file"];
115
+ if (typeof file !== "string" || touched(`${at}.file`))
270
116
  continue;
117
+ const first = files.get(file);
118
+ if (first !== undefined)
119
+ say(`${at}.file`, "images-duplicate", `names ${file} again (also ${first})`);
120
+ else
121
+ files.set(file, at);
122
+ }
123
+ // An item's picture is checked against the images as written: one naming a screenshot the report doesn't list is refused.
124
+ const imagesRefused = touched("images");
125
+ for (const [entry, at] of entries("items")) {
126
+ fields(entry, at, ITEM_TEXT_FIELDS);
127
+ const image = entry["image"];
128
+ if (typeof image === "string" && image !== "" && !touched(`${at}.image`) && !imagesRefused && !files.has(image)) {
129
+ say(`${at}.image`, `bad-${at}.image`, "must name one of images by its file");
271
130
  }
272
- const read = one(entry, at);
273
- if (read !== null)
274
- kept.push(read);
275
131
  }
276
- return kept;
132
+ return problems;
277
133
  }
278
134
  /** A cited web address: http or https, one line, no credentials. */
279
- function link(value, field, problems) {
280
- const text = prose(value, field, REPORT_LIMITS.url, problems);
281
- if (text === null)
282
- return null;
135
+ function isLink(text) {
283
136
  let url = null;
284
137
  try {
285
138
  url = new URL(text);
@@ -287,9 +140,5 @@ function link(value, field, problems) {
287
140
  catch {
288
141
  url = null;
289
142
  }
290
- if (url === null || (url.protocol !== "http:" && url.protocol !== "https:") || /\s/.test(text) || url.username !== "" || url.password !== "") {
291
- problems.push({ reason: `${field}-not-a-link`, message: `${field} must be an http or https address` });
292
- return null;
293
- }
294
- return text;
143
+ return url !== null && (url.protocol === "http:" || url.protocol === "https:") && !/\s/.test(text) && url.username === "" && url.password === "";
295
144
  }