toolroll 0.9.36 → 0.9.37

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 (165) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/assignment-adapters.d.ts +12 -48
  3. package/dist/assignment-adapters.js +33 -43
  4. package/dist/build-review.d.ts +1 -1
  5. package/dist/build-review.js +2 -1
  6. package/dist/chat-acceptance.d.ts +1 -1
  7. package/dist/chat-actions.d.ts +18 -12
  8. package/dist/chat-actions.js +15 -65
  9. package/dist/chat-ask.js +5 -5
  10. package/dist/chat-decide.d.ts +14 -1
  11. package/dist/chat-decide.js +12 -6
  12. package/dist/chat-delivery-state.d.ts +12 -70
  13. package/dist/chat-delivery-state.js +17 -135
  14. package/dist/chat-delivery.js +58 -30
  15. package/dist/chat-flow.js +8 -6
  16. package/dist/chat-inbox.js +1 -1
  17. package/dist/coding-context.d.ts +2 -39
  18. package/dist/coding-context.js +2 -1
  19. package/dist/coding-handoff.d.ts +6 -25
  20. package/dist/coding-handoff.js +11 -5
  21. package/dist/coding-types.d.ts +2 -26
  22. package/dist/coding-workspace.d.ts +2 -0
  23. package/dist/coding-workspace.js +50 -14
  24. package/dist/contracts/acceptance-terms.d.ts +14 -0
  25. package/dist/contracts/acceptance-terms.js +16 -0
  26. package/dist/contracts/chat-actions.d.ts +328 -0
  27. package/dist/contracts/chat-actions.js +133 -0
  28. package/dist/contracts/chat-callback-rows.d.ts +59 -0
  29. package/dist/contracts/chat-callback-rows.js +46 -0
  30. package/dist/contracts/chat-content.d.ts +276 -0
  31. package/dist/contracts/chat-content.js +127 -0
  32. package/dist/contracts/chat-tables.d.ts +6 -0
  33. package/dist/contracts/chat-tables.js +136 -0
  34. package/dist/contracts/coding-context.d.ts +97 -0
  35. package/dist/contracts/coding-context.js +60 -0
  36. package/dist/contracts/coding-handoff.d.ts +44 -0
  37. package/dist/contracts/coding-handoff.js +47 -0
  38. package/dist/contracts/coding-workspace.d.ts +165 -0
  39. package/dist/contracts/coding-workspace.js +71 -0
  40. package/dist/contracts/contract.d.ts +7 -2
  41. package/dist/contracts/contract.js +21 -12
  42. package/dist/contracts/decision.d.ts +45 -0
  43. package/dist/contracts/decision.js +45 -0
  44. package/dist/contracts/discord-callback.d.ts +45 -0
  45. package/dist/contracts/discord-callback.js +34 -0
  46. package/dist/contracts/flow-card.d.ts +50 -0
  47. package/dist/contracts/flow-card.js +46 -0
  48. package/dist/contracts/flow-send.d.ts +119 -0
  49. package/dist/contracts/flow-send.js +112 -0
  50. package/dist/contracts/flow.d.ts +5 -0
  51. package/dist/contracts/flow.js +5 -0
  52. package/dist/contracts/gateway-tools.d.ts +374 -0
  53. package/dist/contracts/gateway-tools.js +94 -0
  54. package/dist/contracts/lead-context.d.ts +165 -0
  55. package/dist/contracts/lead-context.js +73 -0
  56. package/dist/contracts/lead-tools.d.ts +1905 -0
  57. package/dist/contracts/lead-tools.js +265 -0
  58. package/dist/contracts/memory-pass.d.ts +158 -0
  59. package/dist/contracts/memory-pass.js +102 -0
  60. package/dist/contracts/project-knowledge.d.ts +126 -0
  61. package/dist/contracts/project-knowledge.js +93 -0
  62. package/dist/contracts/project-memory.d.ts +117 -0
  63. package/dist/contracts/project-memory.js +51 -0
  64. package/dist/contracts/project-skills.d.ts +121 -0
  65. package/dist/contracts/project-skills.js +76 -0
  66. package/dist/contracts/review-findings.d.ts +46 -0
  67. package/dist/contracts/review-findings.js +48 -0
  68. package/dist/contracts/route.d.ts +484 -0
  69. package/dist/contracts/route.js +128 -0
  70. package/dist/contracts/scope.d.ts +105 -0
  71. package/dist/contracts/scope.js +114 -0
  72. package/dist/contracts/scout-report.d.ts +234 -0
  73. package/dist/contracts/scout-report.js +141 -0
  74. package/dist/contracts/slack-callback.d.ts +43 -0
  75. package/dist/contracts/slack-callback.js +35 -0
  76. package/dist/contracts/sort-answer.d.ts +83 -0
  77. package/dist/contracts/sort-answer.js +77 -0
  78. package/dist/contracts/stage-output.d.ts +127 -0
  79. package/dist/contracts/stage-output.js +166 -0
  80. package/dist/contracts/task-sizing.d.ts +24 -0
  81. package/dist/contracts/task-sizing.js +28 -0
  82. package/dist/contracts/teammate-turn.d.ts +43 -0
  83. package/dist/contracts/teammate-turn.js +51 -0
  84. package/dist/contracts/teams-callback.d.ts +14 -0
  85. package/dist/contracts/teams-callback.js +14 -0
  86. package/dist/contracts/telegram-callback.d.ts +120 -0
  87. package/dist/contracts/telegram-callback.js +97 -0
  88. package/dist/decision.d.ts +11 -20
  89. package/dist/decision.js +86 -167
  90. package/dist/desktop-update.d.ts +2 -2
  91. package/dist/discord-chat.js +31 -20
  92. package/dist/flow-engine.js +14 -17
  93. package/dist/flow-items.d.ts +4 -13
  94. package/dist/flow-items.js +1 -15
  95. package/dist/flow-send.d.ts +6 -37
  96. package/dist/flow-send.js +18 -14
  97. package/dist/flow-share.js +7 -1
  98. package/dist/flow-sort.d.ts +7 -26
  99. package/dist/flow-sort.js +25 -20
  100. package/dist/flow-steps.js +3 -2
  101. package/dist/flows.d.ts +2 -4
  102. package/dist/flows.js +9 -5
  103. package/dist/invoke.js +4 -4
  104. package/dist/lead-context.d.ts +4 -2
  105. package/dist/lead-context.js +24 -17
  106. package/dist/mate-tools.d.ts +4 -0
  107. package/dist/mate-tools.js +108 -275
  108. package/dist/mcp-connect.d.ts +24 -4
  109. package/dist/mcp-connect.js +78 -15
  110. package/dist/mcp.js +84 -216
  111. package/dist/memory-pass.d.ts +17 -42
  112. package/dist/memory-pass.js +54 -26
  113. package/dist/phase-routing.d.ts +19 -72
  114. package/dist/phase-routing.js +34 -164
  115. package/dist/project-knowledge.d.ts +16 -35
  116. package/dist/project-knowledge.js +42 -21
  117. package/dist/project-memory.d.ts +2 -28
  118. package/dist/project-memory.js +28 -13
  119. package/dist/project-skills.d.ts +16 -38
  120. package/dist/project-skills.js +41 -32
  121. package/dist/project-tools.d.ts +1 -0
  122. package/dist/project-tools.js +4 -4
  123. package/dist/provider.d.ts +2 -0
  124. package/dist/provider.js +4 -16
  125. package/dist/result-shots.d.ts +1 -1
  126. package/dist/result-shots.js +2 -1
  127. package/dist/review-switch.d.ts +3 -8
  128. package/dist/reviewer.d.ts +8 -12
  129. package/dist/reviewer.js +11 -35
  130. package/dist/scope.d.ts +21 -43
  131. package/dist/scope.js +42 -128
  132. package/dist/scout-report.d.ts +9 -194
  133. package/dist/scout-report.js +113 -264
  134. package/dist/scout.js +15 -11
  135. package/dist/serve.js +9 -2
  136. package/dist/setup-guide.d.ts +2 -2
  137. package/dist/slack-chat.js +26 -20
  138. package/dist/slack-state.js +2 -1
  139. package/dist/store.d.ts +9 -32
  140. package/dist/store.js +18 -15
  141. package/dist/structured-output.js +2 -1
  142. package/dist/subscription-chat.d.ts +4 -0
  143. package/dist/subscription-chat.js +4 -1
  144. package/dist/task-review-brief.d.ts +1 -7
  145. package/dist/task-review-brief.js +6 -6
  146. package/dist/task-sizing.d.ts +3 -27
  147. package/dist/task-sizing.js +11 -26
  148. package/dist/task-text.d.ts +0 -5
  149. package/dist/task-text.js +1 -3
  150. package/dist/teammate-question.js +7 -5
  151. package/dist/teammate-work.js +9 -5
  152. package/dist/teammates.d.ts +13 -64
  153. package/dist/teammates.js +32 -33
  154. package/dist/teams-chat.js +20 -11
  155. package/dist/telegram-flow.js +4 -3
  156. package/dist/telegram-mate.d.ts +4 -14
  157. package/dist/telegram-mate.js +8 -7
  158. package/dist/telegram-team.d.ts +2 -19
  159. package/dist/telegram.d.ts +1 -1
  160. package/dist/telegram.js +52 -42
  161. package/dist/text-limits.d.ts +142 -1
  162. package/dist/text-limits.js +144 -1
  163. package/dist/tools-ui.d.ts +2 -1
  164. package/dist/tools-ui.js +7 -4
  165. package/package.json +1 -1
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A Teams button tap (teams-chat.ts `receiveTeams`): an Adaptive Card `Action.Submit` arrives as a message activity
3
+ * whose `value` is the data Toolroll put on the button, `{ so: <one-time token> }`. The activity is Microsoft's (read
4
+ * by `receiveTeams`); only `value.so` is read, as before, and extra submit keys are ignored. A tap from the paired person whose data can't be read
5
+ * is answered with why (`value.so: must be a Toolroll button token`), and nothing is done.
6
+ */
7
+ import { z } from "zod";
8
+ import { parseContract } from "./contract.js";
9
+ export const teamsSubmitSchema = z.object({ so: z.string().regex(/^[a-f0-9]{32}$/, { error: "must be a Toolroll button token" }) });
10
+ /** A submit's one-time token. */
11
+ export function readTeamsSubmit(value) {
12
+ const read = parseContract(z.object({ value: teamsSubmitSchema }), { value });
13
+ return read.ok ? { ok: true, value: read.value.value.so } : read;
14
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Telegram (telegram.ts): one schema for an update as Telegram sends it — polled, or pushed to /hooks/telegram and kept
3
+ * in the inbox until applied — and one for the data a Toolroll button carries.
4
+ *
5
+ * The update is Telegram's: only the fields Toolroll reads are described, and any others are ignored. A button's data
6
+ * is Toolroll's: one of the forms it mints, within Telegram's 64 bytes. A tap whose data isn't one of them is
7
+ * answered, after the sender and chat are proved, with the line that says why, and nothing is done.
8
+ */
9
+ import { z } from "zod";
10
+ import { type ContractResult } from "./contract.js";
11
+ export declare const telegramMessageSchema: z.ZodObject<{
12
+ message_id: z.ZodInt;
13
+ text: z.ZodOptional<z.ZodString>;
14
+ chat: z.ZodOptional<z.ZodObject<{
15
+ id: z.ZodInt;
16
+ type: z.ZodOptional<z.ZodString>;
17
+ }, z.core.$strip>>;
18
+ from: z.ZodOptional<z.ZodObject<{
19
+ id: z.ZodInt;
20
+ username: z.ZodOptional<z.ZodString>;
21
+ first_name: z.ZodOptional<z.ZodString>;
22
+ }, z.core.$strip>>;
23
+ reply_to_message: z.ZodOptional<z.ZodObject<{
24
+ message_id: z.ZodInt;
25
+ }, z.core.$strip>>;
26
+ forward_origin: z.ZodOptional<z.ZodUnknown>;
27
+ forward_date: z.ZodOptional<z.ZodUnknown>;
28
+ via_bot: z.ZodOptional<z.ZodUnknown>;
29
+ sender_chat: z.ZodOptional<z.ZodUnknown>;
30
+ caption: z.ZodOptional<z.ZodString>;
31
+ }, z.core.$strip>;
32
+ export declare const telegramCallbackQuerySchema: z.ZodObject<{
33
+ id: z.ZodString;
34
+ data: z.ZodOptional<z.ZodString>;
35
+ from: z.ZodOptional<z.ZodObject<{
36
+ id: z.ZodInt;
37
+ }, z.core.$strip>>;
38
+ message: z.ZodOptional<z.ZodObject<{
39
+ message_id: z.ZodInt;
40
+ chat: z.ZodOptional<z.ZodObject<{
41
+ id: z.ZodInt;
42
+ }, z.core.$strip>>;
43
+ text: z.ZodOptional<z.ZodString>;
44
+ entities: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
45
+ }, z.core.$strip>>;
46
+ }, z.core.$strip>;
47
+ export declare const telegramUpdateSchema: z.ZodObject<{
48
+ update_id: z.ZodInt;
49
+ message: z.ZodOptional<z.ZodObject<{
50
+ message_id: z.ZodInt;
51
+ text: z.ZodOptional<z.ZodString>;
52
+ chat: z.ZodOptional<z.ZodObject<{
53
+ id: z.ZodInt;
54
+ type: z.ZodOptional<z.ZodString>;
55
+ }, z.core.$strip>>;
56
+ from: z.ZodOptional<z.ZodObject<{
57
+ id: z.ZodInt;
58
+ username: z.ZodOptional<z.ZodString>;
59
+ first_name: z.ZodOptional<z.ZodString>;
60
+ }, z.core.$strip>>;
61
+ reply_to_message: z.ZodOptional<z.ZodObject<{
62
+ message_id: z.ZodInt;
63
+ }, z.core.$strip>>;
64
+ forward_origin: z.ZodOptional<z.ZodUnknown>;
65
+ forward_date: z.ZodOptional<z.ZodUnknown>;
66
+ via_bot: z.ZodOptional<z.ZodUnknown>;
67
+ sender_chat: z.ZodOptional<z.ZodUnknown>;
68
+ caption: z.ZodOptional<z.ZodString>;
69
+ }, z.core.$strip>>;
70
+ callback_query: z.ZodOptional<z.ZodObject<{
71
+ id: z.ZodString;
72
+ data: z.ZodOptional<z.ZodString>;
73
+ from: z.ZodOptional<z.ZodObject<{
74
+ id: z.ZodInt;
75
+ }, z.core.$strip>>;
76
+ message: z.ZodOptional<z.ZodObject<{
77
+ message_id: z.ZodInt;
78
+ chat: z.ZodOptional<z.ZodObject<{
79
+ id: z.ZodInt;
80
+ }, z.core.$strip>>;
81
+ text: z.ZodOptional<z.ZodString>;
82
+ entities: z.ZodOptional<z.ZodArray<z.ZodUnknown>>;
83
+ }, z.core.$strip>>;
84
+ }, z.core.$strip>>;
85
+ }, z.core.$strip>;
86
+ export type TelegramUpdate = z.infer<typeof telegramUpdateSchema>;
87
+ export type TelegramMessage = z.infer<typeof telegramMessageSchema>;
88
+ /** One update, as Telegram sent it (refused by path: `callback_query.message.chat.id: must be a number`). */
89
+ export declare function readTelegramUpdate(input: unknown): ContractResult<TelegramUpdate>;
90
+ /** An inbox row's saved update: JSON first, then the schema. */
91
+ export declare function parseTelegramUpdate(raw: string): ContractResult<TelegramUpdate>;
92
+ /**
93
+ * Every form of data a Toolroll button carries: a one-time token (16 random bytes as hex) for a proposal, decision,
94
+ * flow, choice or question button; a decide token (chat-decide.ts); a task from /tasks, or back to the lead; and an
95
+ * option of the lead's question to its owner (by index, or `x` for "Something else").
96
+ */
97
+ export declare const TELEGRAM_BUTTON_DATA: RegExp;
98
+ export declare const telegramButtonDataSchema: z.ZodString;
99
+ /** A button that calls back (the other kind opens a link). */
100
+ export declare const telegramCallbackButtonSchema: z.ZodObject<{
101
+ text: z.ZodString;
102
+ callback_data: z.ZodString;
103
+ }, z.core.$strict>;
104
+ export type TelegramCallbackButton = z.infer<typeof telegramCallbackButtonSchema>;
105
+ /** A tapped button's data: one Toolroll mints, or the path-named reason it isn't (`callback_data: required`). */
106
+ export declare function readTelegramButtonData(data: unknown): ContractResult<string>;
107
+ /**
108
+ * A button as Toolroll sends it. Data Telegram would refuse (over 64 bytes) or that a tap could not be read back is a
109
+ * fault in the code that made it, so it throws before anything is sent.
110
+ */
111
+ export declare function telegramButton(text: string, data: string): TelegramCallbackButton;
112
+ /** A /tasks pick: the task's ref id, or back to the lead (null); null for anything else. */
113
+ export declare function pickOf(data: string): {
114
+ ref: number | null;
115
+ } | null;
116
+ /** An option of the lead's question to its owner: its turn and index, or `x` for "Something else"; null for anything else. */
117
+ export declare function askOf(data: string): {
118
+ turn: number;
119
+ option: number | "x";
120
+ } | null;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Telegram (telegram.ts): one schema for an update as Telegram sends it — polled, or pushed to /hooks/telegram and kept
3
+ * in the inbox until applied — and one for the data a Toolroll button carries.
4
+ *
5
+ * The update is Telegram's: only the fields Toolroll reads are described, and any others are ignored. A button's data
6
+ * is Toolroll's: one of the forms it mints, within Telegram's 64 bytes. A tap whose data isn't one of them is
7
+ * answered, after the sender and chat are proved, with the line that says why, and nothing is done.
8
+ */
9
+ import { z } from "zod";
10
+ import { lengthIn, TEXT_LIMITS } from "../text-limits.js";
11
+ import { limited, parseContract } from "./contract.js";
12
+ const chat = z.object({ id: z.int(), type: z.string().optional() });
13
+ const sender = z.object({ id: z.int(), username: z.string().optional(), first_name: z.string().optional() });
14
+ export const telegramMessageSchema = z.object({
15
+ message_id: z.int(),
16
+ text: z.string().optional(),
17
+ chat: chat.optional(),
18
+ from: sender.optional(),
19
+ reply_to_message: z.object({ message_id: z.int() }).optional(),
20
+ /** Presence of any of these disqualifies a note: only direct, initial, plain text counts as authored-and-confirmed
21
+ * by the paired operator. */
22
+ forward_origin: z.unknown().optional(),
23
+ forward_date: z.unknown().optional(),
24
+ via_bot: z.unknown().optional(),
25
+ sender_chat: z.unknown().optional(),
26
+ caption: z.string().optional(),
27
+ });
28
+ export const telegramCallbackQuerySchema = z.object({
29
+ id: z.string(),
30
+ /** Read by `readTelegramButtonData`, after the sender and chat are proved. */
31
+ data: z.string().optional(),
32
+ from: z.object({ id: z.int() }).optional(),
33
+ message: z.object({ message_id: z.int(), chat: z.object({ id: z.int() }).optional(), text: z.string().optional(), entities: z.array(z.unknown()).optional() }).optional(),
34
+ });
35
+ export const telegramUpdateSchema = z.object({
36
+ update_id: z.int().min(1),
37
+ message: telegramMessageSchema.optional(),
38
+ callback_query: telegramCallbackQuerySchema.optional(),
39
+ });
40
+ /** One update, as Telegram sent it (refused by path: `callback_query.message.chat.id: must be a number`). */
41
+ export function readTelegramUpdate(input) {
42
+ return parseContract(telegramUpdateSchema, input);
43
+ }
44
+ /** An inbox row's saved update: JSON first, then the schema. */
45
+ export function parseTelegramUpdate(raw) {
46
+ let body;
47
+ try {
48
+ body = JSON.parse(raw);
49
+ }
50
+ catch {
51
+ return { ok: false, issues: [{ path: "payload", kind: "invalid", line: "payload: not JSON" }] };
52
+ }
53
+ return readTelegramUpdate(body);
54
+ }
55
+ /**
56
+ * Every form of data a Toolroll button carries: a one-time token (16 random bytes as hex) for a proposal, decision,
57
+ * flow, choice or question button; a decide token (chat-decide.ts); a task from /tasks, or back to the lead; and an
58
+ * option of the lead's question to its owner (by index, or `x` for "Something else").
59
+ */
60
+ export const TELEGRAM_BUTTON_DATA = /^(?:[a-f0-9]{32}|d:[a-f0-9]{24}|pick:(?:lead|[1-9][0-9]{0,14})|ask:[1-9][0-9]{0,14}:(?:[0-3]|x))$/;
61
+ export const telegramButtonDataSchema = limited("callback_data", "telegramCallbackDataBytes")
62
+ .regex(TELEGRAM_BUTTON_DATA, { error: "not a Toolroll button" });
63
+ /** A button that calls back (the other kind opens a link). */
64
+ export const telegramCallbackButtonSchema = z.strictObject({ text: z.string(), callback_data: telegramButtonDataSchema });
65
+ /** The byte bound, which counts UTF-8 bytes where the schema counts characters. */
66
+ function withinBytes(data) {
67
+ const limit = TEXT_LIMITS.telegramCallbackDataBytes, bytes = lengthIn(data, "bytes");
68
+ return bytes <= limit ? { ok: true, value: data } : { ok: false, issues: [{ path: "callback_data", kind: "too-long", line: `callback_data: over ${limit} bytes (${bytes})` }] };
69
+ }
70
+ /** A tapped button's data: one Toolroll mints, or the path-named reason it isn't (`callback_data: required`). */
71
+ export function readTelegramButtonData(data) {
72
+ const read = parseContract(z.strictObject({ callback_data: telegramButtonDataSchema }), { callback_data: data });
73
+ return read.ok ? withinBytes(read.value.callback_data) : read;
74
+ }
75
+ /**
76
+ * A button as Toolroll sends it. Data Telegram would refuse (over 64 bytes) or that a tap could not be read back is a
77
+ * fault in the code that made it, so it throws before anything is sent.
78
+ */
79
+ export function telegramButton(text, data) {
80
+ const read = parseContract(telegramCallbackButtonSchema, { text, callback_data: data });
81
+ const bytes = read.ok ? withinBytes(read.value.callback_data) : read;
82
+ if (!read.ok || !bytes.ok)
83
+ throw new Error(`a Telegram button: ${(read.ok ? bytes.ok ? [] : bytes.issues : read.issues).map(issue => issue.line).join("; ")}`);
84
+ return read.value;
85
+ }
86
+ /** A /tasks pick: the task's ref id, or back to the lead (null); null for anything else. */
87
+ export function pickOf(data) {
88
+ if (data === "pick:lead")
89
+ return { ref: null };
90
+ const ref = /^pick:([1-9][0-9]{0,14})$/.exec(data)?.[1];
91
+ return ref === undefined ? null : { ref: Number(ref) };
92
+ }
93
+ /** An option of the lead's question to its owner: its turn and index, or `x` for "Something else"; null for anything else. */
94
+ export function askOf(data) {
95
+ const tap = /^ask:([1-9][0-9]{0,14}):([0-3]|x)$/.exec(data);
96
+ return tap === null ? null : { turn: Number(tap[1]), option: tap[2] === "x" ? "x" : Number(tap[2]) };
97
+ }
@@ -14,18 +14,11 @@
14
14
  * caps also keep one screen one screen: an option list that needs scrolling
15
15
  * has already failed the milestone sentence.
16
16
  */
17
- export type ParsedOption = {
18
- id: string;
19
- label: string;
20
- consequence: string;
21
- reversible: boolean;
22
- };
23
- export type ParsedDecision = {
24
- urgency: "blocking";
25
- recap: string;
26
- question: string;
27
- options: ParsedOption[];
28
- recommendation: string;
17
+ import { type DecisionOption, type DecisionPayload } from "./contracts/decision.js";
18
+ /** One option as a decision keeps it (src/contracts/decision.ts). */
19
+ export type ParsedOption = DecisionOption;
20
+ /** A decision as read: the payload's fields, its text trimmed, and the optional ones present or null. */
21
+ export type ParsedDecision = Omit<DecisionPayload, "assignee" | "deadline"> & {
29
22
  assignee: string | null;
30
23
  /** Normalized to toISOString(), like every timestamp in the store. */
31
24
  deadline: string | null;
@@ -41,17 +34,10 @@ export type ParseResult = {
41
34
  ok: false;
42
35
  problems: Problem[];
43
36
  };
44
- /** One screen's worth, enforced rather than hoped for. */
37
+ /** One screen's worth, enforced rather than hoped for. The payload's fields are bounded by its schema (src/contracts/decision.ts, from TEXT_LIMITS). */
45
38
  export declare const LIMITS: {
46
39
  /** Bytes, before parsing. Applied by the reader too; this is the backstop. */
47
40
  readonly payload: number;
48
- readonly recap: 2000;
49
- readonly question: 2000;
50
- readonly consequence: 500;
51
- readonly label: 120;
52
- readonly optionId: 40;
53
- readonly assignee: 120;
54
- readonly options: 6;
55
41
  /** UTF-16 code units (TEXT_LIMITS.note: revise feedback, steering and decision notes); the byte backstop lives in validateNote. */
56
42
  readonly note: 4000;
57
43
  };
@@ -79,6 +65,11 @@ export declare function hasForbiddenControls(text: string): boolean;
79
65
  * backwards is not a title, it is a costume.
80
66
  */
81
67
  export declare function hasDisguisedText(text: string): boolean;
68
+ /**
69
+ * A parked decision, read against its contract (src/contracts/decision.ts): fail closed, every problem at once, each
70
+ * naming its path (`options[1].reversible: required`) under the reason code the repair turn, incidents and tests have
71
+ * always used (`missing-reversible`). A decision written without `version` reads as it always did.
72
+ */
82
73
  export declare function parseDecision(raw: string): ParseResult;
83
74
  /**
84
75
  * The terminal handoff: how every non-parking attempt says how it ended.
package/dist/decision.js CHANGED
@@ -14,18 +14,12 @@
14
14
  * caps also keep one screen one screen: an option list that needs scrolling
15
15
  * has already failed the milestone sentence.
16
16
  */
17
+ import { readDecisionPayload } from "./contracts/decision.js";
17
18
  import { TEXT_LIMITS } from "./text-limits.js";
18
- /** One screen's worth, enforced rather than hoped for. */
19
+ /** One screen's worth, enforced rather than hoped for. The payload's fields are bounded by its schema (src/contracts/decision.ts, from TEXT_LIMITS). */
19
20
  export const LIMITS = {
20
21
  /** Bytes, before parsing. Applied by the reader too; this is the backstop. */
21
22
  payload: 64 * 1024,
22
- recap: 2_000,
23
- question: 2_000,
24
- consequence: 500,
25
- label: 120,
26
- optionId: 40,
27
- assignee: 120,
28
- options: 6,
29
23
  /** UTF-16 code units (TEXT_LIMITS.note: revise feedback, steering and decision notes); the byte backstop lives in validateNote. */
30
24
  note: TEXT_LIMITS.note,
31
25
  };
@@ -58,8 +52,6 @@ export function validateNote(raw) {
58
52
  }
59
53
  return { ok: true, note };
60
54
  }
61
- /** Option ids travel in URLs, CLI arguments, and CAS updates — they are identifiers, not prose. */
62
- const OPTION_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
63
55
  /**
64
56
  * Multi-line prose may contain newlines and tabs; nothing anywhere in a
65
57
  * decision may contain the rest of C0/C1 — those are how text stops being
@@ -82,9 +74,14 @@ export function hasForbiddenControls(text) {
82
74
  export function hasDisguisedText(text) {
83
75
  return FORBIDDEN_MULTILINE.test(text) || FORBIDDEN_INVISIBLES.test(text);
84
76
  }
77
+ /**
78
+ * A parked decision, read against its contract (src/contracts/decision.ts): fail closed, every problem at once, each
79
+ * naming its path (`options[1].reversible: required`) under the reason code the repair turn, incidents and tests have
80
+ * always used (`missing-reversible`). A decision written without `version` reads as it always did.
81
+ */
85
82
  export function parseDecision(raw) {
86
83
  if (Buffer.byteLength(raw, "utf8") > LIMITS.payload) {
87
- return refuse("too-large", `the payload is over ${LIMITS.payload} bytes — one screen does not need that`);
84
+ return refuse("too-large", `payload: over ${LIMITS.payload} bytes — one screen does not need that`);
88
85
  }
89
86
  let parsed;
90
87
  try {
@@ -94,187 +91,109 @@ export function parseDecision(raw) {
94
91
  return refuse("not-json", `the payload is not JSON: ${error instanceof Error ? error.message : String(error)}`);
95
92
  }
96
93
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
97
- return refuse("not-an-object", "the payload must be one JSON object");
94
+ return refuse("not-an-object", "payload: must be one JSON object");
98
95
  }
96
+ const read = readDecisionPayload(parsed);
97
+ const contract = read.ok ? [] : read.issues.map(decisionProblemOf);
99
98
  const body = parsed;
100
- const problems = [];
101
- // Urgency is required rather than defaulted: an agent that did not say
102
- // whether the loop can continue has not composed a decision. Only
103
- // 'blocking' exists in M3 — advisory needs a commit-and-complete lifecycle
104
- // this park-before-commit path does not have.
105
- if (body["urgency"] !== "blocking") {
106
- problems.push({
107
- reason: "bad-urgency",
108
- message: `urgency must be the string "blocking" (got ${describe(body["urgency"])})`,
109
- });
110
- }
111
- const recap = prose(body["recap"], "recap", LIMITS.recap, problems);
112
- const question = prose(body["question"], "question", LIMITS.question, problems);
113
- const options = parseOptions(body["options"], problems);
114
- let recommendation = null;
115
- if (typeof body["recommendation"] !== "string" || body["recommendation"] === "") {
116
- problems.push({
117
- reason: "missing-recommendation",
118
- message: "recommendation is required and must be an option id — a decision without one is a shrug",
119
- });
120
- }
121
- else if (options !== null && !options.some(option => option.id === body["recommendation"])) {
122
- problems.push({
123
- reason: "bad-recommendation",
124
- message: `recommendation "${truncate(String(body["recommendation"]), 60)}" does not match any option id`,
125
- });
126
- }
127
- else {
128
- recommendation = body["recommendation"];
129
- }
130
- let assignee = null;
131
- if (body["assignee"] !== undefined && body["assignee"] !== null) {
132
- const line = singleLine(body["assignee"], "assignee", LIMITS.assignee, problems);
133
- if (line !== null)
134
- assignee = line;
135
- }
136
- let deadline = null;
137
- if (body["deadline"] !== undefined && body["deadline"] !== null) {
138
- const stamp = typeof body["deadline"] === "string" ? Date.parse(body["deadline"]) : NaN;
139
- if (Number.isNaN(stamp)) {
140
- problems.push({
141
- reason: "bad-deadline",
142
- message: `deadline must be an ISO 8601 timestamp (got ${describe(body["deadline"])})`,
143
- });
144
- }
145
- else {
146
- // Normalized so the store's lexicographic-comparison invariant holds.
147
- deadline = new Date(stamp).toISOString();
148
- }
149
- }
150
- if (problems.length > 0)
99
+ const problems = [...contract, ...decisionRuleProblems(body, contract)];
100
+ if (!read.ok || problems.length > 0)
151
101
  return { ok: false, problems };
102
+ const payload = read.value;
152
103
  return {
153
104
  ok: true,
154
105
  decision: {
155
- urgency: "blocking",
156
- recap: recap,
157
- question: question,
158
- options: options,
159
- recommendation: recommendation,
160
- assignee,
161
- deadline,
106
+ urgency: payload.urgency,
107
+ recap: payload.recap.trim(),
108
+ question: payload.question.trim(),
109
+ options: payload.options.map(option => ({ id: option.id, label: option.label.trim(), consequence: option.consequence.trim(), reversible: option.reversible })),
110
+ recommendation: recommendedId(payload.recommendation),
111
+ assignee: payload.assignee === undefined || payload.assignee === null ? null : payload.assignee.trim(),
112
+ // Normalized so the store's lexicographic-comparison invariant holds.
113
+ deadline: payload.deadline === undefined || payload.deadline === null ? null : new Date(Date.parse(payload.deadline)).toISOString(),
162
114
  },
163
115
  };
164
116
  }
165
- function parseOptions(value, problems) {
166
- if (!Array.isArray(value)) {
167
- problems.push({ reason: "no-options", message: "options must be an array" });
168
- return null;
117
+ /** The reason code a contract issue has always had: `missing-recap`, `too-few-options`, `bad-option-id`. */
118
+ function decisionReasonOf(issue) {
119
+ const at = issue.path;
120
+ const text = (field) => (issue.kind === "too-long" ? `${field}-too-long` : `missing-${field}`);
121
+ switch (at) {
122
+ case "urgency": return "bad-urgency";
123
+ case "options": return issue.kind === "too-few" ? "too-few-options" : issue.kind === "too-many" ? "too-many-options" : "no-options";
124
+ case "recommendation": return issue.kind === "too-long" ? "bad-recommendation" : "missing-recommendation";
125
+ case "deadline": return "bad-deadline";
169
126
  }
170
- if (value.length < 2) {
171
- // One option is not a decision, it is a notification wearing one's
172
- // clothes — and the operator's real alternative ("do neither") deserves
173
- // to be an option with a stated consequence, not an implied one.
174
- problems.push({ reason: "too-few-options", message: "a decision needs at least 2 options" });
175
- return null;
176
- }
177
- if (value.length > LIMITS.options) {
178
- problems.push({
179
- reason: "too-many-options",
180
- message: `at most ${LIMITS.options} options fit on one screen (got ${value.length})`,
181
- });
182
- return null;
127
+ const option = /^options\[(\d+)\](?:\.(\w+))?$/.exec(at);
128
+ if (option !== null) {
129
+ const [, index, field] = option;
130
+ if (field === undefined)
131
+ return "bad-option";
132
+ if (field === "id")
133
+ return "bad-option-id";
134
+ if (field === "reversible")
135
+ return "missing-reversible";
136
+ return text(`option-${index}-${field}`);
183
137
  }
184
- const options = [];
185
- const seen = new Set();
186
- let sound = true;
187
- value.forEach((entry, index) => {
188
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
189
- problems.push({ reason: "bad-option", message: `option ${index} must be an object` });
190
- sound = false;
138
+ return text(at);
139
+ }
140
+ const decisionProblemOf = (issue) => ({ reason: decisionReasonOf(issue), message: issue.line });
141
+ /** True when `problems` name `field` or anything under it. */
142
+ const touched = (problems, field) => problems.some(problem => problem.message.startsWith(`${field}:`) || problem.message.startsWith(`${field}[`) || problem.message.startsWith(`${field}.`));
143
+ /**
144
+ * The rules JSON Schema cannot state, in plain code with named errors: text not blank once trimmed and free of control
145
+ * characters (a label or assignee on one line), unique option ids, a recommendation naming one of them, and a deadline
146
+ * that is a timestamp. Fields the contract already refused are skipped, so every problem is reported once and all at
147
+ * once.
148
+ */
149
+ function decisionRuleProblems(body, contract) {
150
+ const problems = [];
151
+ const text = (value, path, slug, oneLine) => {
152
+ if (typeof value !== "string" || touched(contract, path))
191
153
  return;
154
+ if (value.trim() === "")
155
+ problems.push({ reason: `missing-${slug}`, message: `${path}: must not be blank` });
156
+ else if ((oneLine ? FORBIDDEN_SINGLE_LINE : FORBIDDEN_MULTILINE).test(value)) {
157
+ problems.push({ reason: `${slug}-control-characters`, message: `${path}: ${oneLine ? "must be one line with no control characters" : "contains control characters — text only"}` });
192
158
  }
159
+ };
160
+ text(body["recap"], "recap", "recap", false);
161
+ text(body["question"], "question", "question", false);
162
+ const options = Array.isArray(body["options"]) ? body["options"] : [];
163
+ const seen = new Set();
164
+ options.forEach((entry, index) => {
165
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
166
+ return;
193
167
  const option = entry;
194
168
  const id = option["id"];
195
- if (typeof id !== "string" || !OPTION_ID.test(id) || id.length > LIMITS.optionId) {
196
- problems.push({
197
- reason: "bad-option-id",
198
- message: `option ${index} needs an id of 1-${LIMITS.optionId} letters, digits, - or _ (got ${describe(id)})`,
199
- });
200
- sound = false;
201
- }
202
- else if (seen.has(id)) {
203
- problems.push({ reason: "duplicate-option-id", message: `option id "${id}" appears twice` });
204
- sound = false;
205
- }
206
- else {
169
+ if (typeof id === "string" && !touched(contract, `options[${index}].id`)) {
170
+ if (seen.has(id))
171
+ problems.push({ reason: "duplicate-option-id", message: `options[${index}].id: "${id}" appears twice` });
207
172
  seen.add(id);
208
173
  }
209
- const label = singleLine(option["label"], `option ${index} label`, LIMITS.label, problems);
210
- const consequence = prose(option["consequence"], `option ${index} consequence`, LIMITS.consequence, problems);
211
- // Absent is invalid, never defaulted: a defaulted reversibility is a
212
- // guess wearing a schema, and reversible=false is the field the
213
- // scheduler's refusal to auto-apply rests on.
214
- if (typeof option["reversible"] !== "boolean") {
215
- problems.push({
216
- reason: "missing-reversible",
217
- message: `option ${index} must say reversible: true or false — unstated is not reversible, it is unstated`,
218
- });
219
- sound = false;
220
- }
221
- if (label === null || consequence === null) {
222
- sound = false;
223
- return;
224
- }
225
- if (sound) {
226
- options.push({
227
- id: id,
228
- label,
229
- consequence,
230
- reversible: option["reversible"],
231
- });
232
- }
174
+ text(option["label"], `options[${index}].label`, `option-${index}-label`, true);
175
+ text(option["consequence"], `options[${index}].consequence`, `option-${index}-consequence`, false);
233
176
  });
234
- return sound && options.length === value.length ? options : null;
235
- }
236
- function prose(value, field, cap, problems) {
237
- if (typeof value !== "string" || value.trim() === "") {
238
- problems.push({ reason: `missing-${slug(field)}`, message: `${field} is required` });
239
- return null;
177
+ const recommendation = body["recommendation"];
178
+ if (typeof recommendation === "string" && !touched(contract, "recommendation") && !touched(contract, "options") && !options.some(option => typeof option === "object" && option !== null && option["id"] === recommendedId(recommendation))) {
179
+ problems.push({ reason: "bad-recommendation", message: `recommendation: "${truncate(recommendation, 60)}" does not match any option id` });
240
180
  }
241
- if (value.length > cap) {
242
- problems.push({ reason: `${slug(field)}-too-long`, message: `${field} is over ${cap} characters` });
243
- return null;
244
- }
245
- if (FORBIDDEN_MULTILINE.test(value)) {
246
- problems.push({
247
- reason: `${slug(field)}-control-characters`,
248
- message: `${field} contains control characters — text only`,
249
- });
250
- return null;
181
+ if (body["assignee"] !== null)
182
+ text(body["assignee"], "assignee", "assignee", true);
183
+ const deadline = body["deadline"];
184
+ if (typeof deadline === "string" && !touched(contract, "deadline") && Number.isNaN(Date.parse(deadline))) {
185
+ problems.push({ reason: "bad-deadline", message: `deadline: must be an ISO 8601 timestamp (got ${describe(deadline)})` });
251
186
  }
252
- return value.trim();
187
+ return problems;
253
188
  }
254
- function singleLine(value, field, cap, problems) {
255
- if (typeof value !== "string" || value.trim() === "") {
256
- problems.push({ reason: `missing-${slug(field)}`, message: `${field} is required` });
257
- return null;
258
- }
259
- if (value.length > cap) {
260
- problems.push({ reason: `${slug(field)}-too-long`, message: `${field} is over ${cap} characters` });
261
- return null;
262
- }
263
- if (FORBIDDEN_SINGLE_LINE.test(value)) {
264
- problems.push({
265
- reason: `${slug(field)}-control-characters`,
266
- message: `${field} must be one line with no control characters`,
267
- });
268
- return null;
269
- }
270
- return value.trim();
189
+ /** The option id a recommendation names: a bare id, or the id before the first ":" in "<id>: why". */
190
+ function recommendedId(recommendation) {
191
+ const colon = recommendation.indexOf(":");
192
+ return (colon === -1 ? recommendation : recommendation.slice(0, colon)).trim();
271
193
  }
272
194
  function refuse(reason, message) {
273
195
  return { ok: false, problems: [{ reason, message }] };
274
196
  }
275
- function slug(field) {
276
- return field.replace(/[^a-z0-9]+/gi, "-").toLowerCase();
277
- }
278
197
  function describe(value) {
279
198
  if (value === undefined)
280
199
  return "nothing";
@@ -125,20 +125,20 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
125
125
  export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
126
126
  export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
127
127
  export declare function desktopUpdateStatus(stateDir: string): {
128
- version?: never;
128
+ error?: never;
129
129
  active: boolean;
130
130
  phase: string;
131
131
  detail: string;
132
132
  running: boolean;
133
133
  canResume: boolean;
134
134
  canCancel: boolean;
135
- error?: never;
136
135
  automaticRecovery?: never;
137
136
  recoveryAttempts?: never;
138
137
  recoveryRepairs?: never;
139
138
  wasRunning?: never;
140
139
  backupPath?: never;
141
140
  updateId?: never;
141
+ version?: never;
142
142
  buildId?: never;
143
143
  workDir?: never;
144
144
  } | {