@danypops/papyrus 0.42.0 → 0.42.2

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 (73) hide show
  1. package/package.json +2 -2
  2. package/src/adapters/sqlite-artifact-scope-store.ts +18 -9
  3. package/src/adapters/sqlite-artifact-store.ts +7 -5
  4. package/src/adapters/sqlite-discussion-round-store.ts +26 -17
  5. package/src/adapters/sqlite-gate-runner.ts +1 -1
  6. package/src/adapters/sqlite-graph-projection-store.ts +14 -10
  7. package/src/adapters/sqlite-log-store.ts +36 -17
  8. package/src/adapters/sqlite-note-event-store.ts +20 -16
  9. package/src/adapters/sqlite-session-identity-store.ts +13 -7
  10. package/src/adapters/sqlite-task-event-store.ts +29 -21
  11. package/src/adapters/sqlite-task-focus-store.ts +36 -10
  12. package/src/adapters/sqlite-task-lease-store.ts +12 -6
  13. package/src/adapters/sqlite-task-scope-store.ts +25 -14
  14. package/src/artifact-relationship-view.ts +3 -3
  15. package/src/artifact-subtree.ts +4 -2
  16. package/src/authority-registry.ts +2 -1
  17. package/src/cli.ts +785 -179
  18. package/src/client.ts +46 -20
  19. package/src/constants.ts +15 -4
  20. package/src/daemon-state.ts +4 -12
  21. package/src/daemon.ts +31 -9
  22. package/src/db.ts +119 -98
  23. package/src/discussion-service.ts +109 -44
  24. package/src/domain/artifact-event.ts +17 -4
  25. package/src/domain/artifact.ts +3 -1
  26. package/src/domain/blueprint-definition.ts +26 -31
  27. package/src/domain/checklist.ts +20 -17
  28. package/src/domain/discussion.ts +37 -18
  29. package/src/domain/gate.ts +7 -7
  30. package/src/domain/log-entry.ts +1 -1
  31. package/src/domain/note-event.ts +20 -7
  32. package/src/domain/task-event.ts +17 -7
  33. package/src/domain-services.ts +241 -110
  34. package/src/graph-projection-service.ts +34 -8
  35. package/src/id-migration.ts +17 -4
  36. package/src/index.ts +16 -11
  37. package/src/log-service.ts +6 -5
  38. package/src/log.ts +19 -0
  39. package/src/modules/discuss.ts +63 -28
  40. package/src/modules/docs.ts +74 -17
  41. package/src/modules/graph-projection.ts +20 -9
  42. package/src/modules/logs.ts +33 -21
  43. package/src/modules/notes.ts +66 -28
  44. package/src/modules/playbooks.ts +88 -29
  45. package/src/modules/rules.ts +57 -15
  46. package/src/modules/session-identity.ts +6 -2
  47. package/src/modules/tasks.ts +142 -67
  48. package/src/note-service.ts +11 -7
  49. package/src/ops.ts +131 -68
  50. package/src/playbook-definition.ts +56 -17
  51. package/src/playbook-execution.ts +13 -3
  52. package/src/ports/note-event-store.ts +6 -4
  53. package/src/ports/task-event-store.ts +9 -6
  54. package/src/ports/task-focus-store.ts +17 -4
  55. package/src/ports/task-lease-store.ts +9 -4
  56. package/src/ports/task-scope-store.ts +3 -1
  57. package/src/service.ts +143 -89
  58. package/src/session-identity-service.ts +10 -2
  59. package/src/task-context.ts +28 -16
  60. package/src/task-execution.ts +4 -12
  61. package/src/task-graph-view.ts +12 -12
  62. package/src/task-relationship-view.ts +1 -3
  63. package/src/task-service.ts +167 -72
  64. package/src/vehicle/artifact-trash-vehicle.ts +25 -13
  65. package/src/vehicle/artifact-vehicle-shared.ts +28 -7
  66. package/src/vehicle/docs-vehicle.ts +48 -16
  67. package/src/vehicle/notes-vehicle.ts +24 -6
  68. package/src/vehicle/papyrus-vehicle.ts +14 -3
  69. package/src/vehicle/playbooks-vehicle.ts +87 -18
  70. package/src/vehicle/rules-vehicle.ts +58 -21
  71. package/src/vehicle/tasks-vehicle.ts +366 -54
  72. package/src/version.ts +1 -1
  73. package/src/workflow-execution.ts +71 -52
@@ -4,8 +4,13 @@
4
4
  * design rationale.
5
5
  */
6
6
  import { DISCUSSION_LIST_DEFAULT_LIMIT, DISCUSSION_LIST_MAX_LIMIT, DISCUSSION_MAX_ROUNDS } from "./constants.ts";
7
+ import type { Artifact } from "./domain/artifact.ts";
8
+ import type { ArtifactEventContext } from "./domain/artifact-event.ts";
7
9
  import {
8
10
  DISCUSSION_SUBTYPE,
11
+ type DiscussionExtra,
12
+ type DiscussionOptionsMode,
13
+ type DiscussionRound,
9
14
  isDiscussionArtifact,
10
15
  readDiscussionExtra,
11
16
  validateDeferReason,
@@ -14,12 +19,7 @@ import {
14
19
  validateDiscussionOptions,
15
20
  validateSelectedOptions,
16
21
  validateSettlement,
17
- type DiscussionExtra,
18
- type DiscussionOptionsMode,
19
- type DiscussionRound,
20
22
  } from "./domain/discussion.ts";
21
- import type { Artifact } from "./domain/artifact.ts";
22
- import type { ArtifactEventContext } from "./domain/artifact-event.ts";
23
23
  import type { AtomicArtifactStore } from "./ports/atomic-artifact-store.ts";
24
24
  import type { DiscussionRoundStore } from "./ports/discussion-round-store.ts";
25
25
 
@@ -72,7 +72,11 @@ export class Discussions {
72
72
  }
73
73
 
74
74
  /** Validates a freshly-posed choice; undefined when neither field is given (nothing posed), since both/neither is the only valid shape. */
75
- private validatePosedOptions(options: string[] | undefined, optionsMode: DiscussionOptionsMode | undefined, optionDescriptions: string[] | undefined): { options: string[]; mode: DiscussionOptionsMode; optionDescriptions?: string[] } | undefined {
75
+ private validatePosedOptions(
76
+ options: string[] | undefined,
77
+ optionsMode: DiscussionOptionsMode | undefined,
78
+ optionDescriptions: string[] | undefined,
79
+ ): { options: string[]; mode: DiscussionOptionsMode; optionDescriptions?: string[] } | undefined {
76
80
  if (options === undefined && optionsMode === undefined) return undefined;
77
81
  return validateDiscussionOptions(options ?? [], optionsMode ?? "", optionDescriptions);
78
82
  }
@@ -82,25 +86,46 @@ export class Discussions {
82
86
  const content = validateDiscussionContent(input.content);
83
87
  const posed = this.validatePosedOptions(input.options, input.optionsMode, input.optionDescriptions);
84
88
  return this.artifacts.atomic(() => {
85
- const discussion = this.artifacts.create({
86
- kind: "task",
87
- subtype: DISCUSSION_SUBTYPE,
88
- title: input.title,
89
- body: input.body ?? "",
90
- status: "in-progress",
91
- labels: input.labels,
92
- extra: {
93
- discussion: {
94
- state: "active",
95
- roundCount: 1,
96
- ...(posed ? { pendingOptions: posed.options, pendingOptionsMode: posed.mode, ...(posed.optionDescriptions ? { pendingOptionDescriptions: posed.optionDescriptions } : {}) } : {}),
89
+ const discussion = this.artifacts.create(
90
+ {
91
+ kind: "task",
92
+ subtype: DISCUSSION_SUBTYPE,
93
+ title: input.title,
94
+ body: input.body ?? "",
95
+ status: "in-progress",
96
+ labels: input.labels,
97
+ extra: {
98
+ discussion: {
99
+ state: "active",
100
+ roundCount: 1,
101
+ ...(posed
102
+ ? {
103
+ pendingOptions: posed.options,
104
+ pendingOptionsMode: posed.mode,
105
+ ...(posed.optionDescriptions ? { pendingOptionDescriptions: posed.optionDescriptions } : {}),
106
+ }
107
+ : {}),
108
+ },
97
109
  },
98
110
  },
99
- }, context);
100
- const round = this.rounds.append({
101
- discussionId: discussion.id, roundNumber: 1, actor, content,
102
- ...(posed ? { options: posed.options, optionsMode: posed.mode, ...(posed.optionDescriptions ? { optionDescriptions: posed.optionDescriptions } : {}) } : {}),
103
- }, new Date().toISOString());
111
+ context,
112
+ );
113
+ const round = this.rounds.append(
114
+ {
115
+ discussionId: discussion.id,
116
+ roundNumber: 1,
117
+ actor,
118
+ content,
119
+ ...(posed
120
+ ? {
121
+ options: posed.options,
122
+ optionsMode: posed.mode,
123
+ ...(posed.optionDescriptions ? { optionDescriptions: posed.optionDescriptions } : {}),
124
+ }
125
+ : {}),
126
+ },
127
+ new Date().toISOString(),
128
+ );
104
129
  for (const taskId of input.blocksTaskIds ?? []) this.block(discussion.id, taskId, context);
105
130
  return { discussion: this.artifacts.get(discussion.id)!, rounds: [round] };
106
131
  });
@@ -114,24 +139,49 @@ export class Discussions {
114
139
  const discussion = requireDiscussion(this.artifacts.get(discussionId), discussionId);
115
140
  const state = this.extra(discussion);
116
141
  if (state.state !== "active") throw new DiscussionError(`discussion "${discussionId}" is ${state.state}; resume it before replying`);
117
- if (state.roundCount >= DISCUSSION_MAX_ROUNDS) throw new DiscussionError(`discussion "${discussionId}" has reached its ${DISCUSSION_MAX_ROUNDS}-round limit; settle or defer it`);
118
- const selected = input.selected !== undefined ? validateSelectedOptions(input.selected, state.pendingOptions, state.pendingOptionsMode) : undefined;
142
+ if (state.roundCount >= DISCUSSION_MAX_ROUNDS)
143
+ throw new DiscussionError(`discussion "${discussionId}" has reached its ${DISCUSSION_MAX_ROUNDS}-round limit; settle or defer it`);
144
+ const selected =
145
+ input.selected !== undefined ? validateSelectedOptions(input.selected, state.pendingOptions, state.pendingOptionsMode) : undefined;
119
146
  const nextRound = state.roundCount + 1;
120
- const round = this.rounds.append({
121
- discussionId, roundNumber: nextRound, actor: validActor, content: validContent,
122
- ...(posed ? { options: posed.options, optionsMode: posed.mode, ...(posed.optionDescriptions ? { optionDescriptions: posed.optionDescriptions } : {}) } : {}),
123
- ...(selected ? { selected } : {}),
124
- }, new Date().toISOString());
147
+ const round = this.rounds.append(
148
+ {
149
+ discussionId,
150
+ roundNumber: nextRound,
151
+ actor: validActor,
152
+ content: validContent,
153
+ ...(posed
154
+ ? {
155
+ options: posed.options,
156
+ optionsMode: posed.mode,
157
+ ...(posed.optionDescriptions ? { optionDescriptions: posed.optionDescriptions } : {}),
158
+ }
159
+ : {}),
160
+ ...(selected ? { selected } : {}),
161
+ },
162
+ new Date().toISOString(),
163
+ );
125
164
  // Whenever this round answers the pending choice OR poses a new one, the base must drop ALL
126
165
  // three pending* fields first -- otherwise a re-pose that omits descriptions this time would
127
166
  // leave a stale pendingOptionDescriptions array (sized for the OLD options) spread through
128
167
  // unchanged, no longer aligned 1:1 with the new pendingOptions. Only a plain reply that
129
168
  // neither answers nor re-poses leaves the existing pending state untouched.
130
- const { pendingOptions: _clearedOptions, pendingOptionsMode: _clearedMode, pendingOptionDescriptions: _clearedDescriptions, ...withoutPending } = state;
169
+ const {
170
+ pendingOptions: _clearedOptions,
171
+ pendingOptionsMode: _clearedMode,
172
+ pendingOptionDescriptions: _clearedDescriptions,
173
+ ...withoutPending
174
+ } = state;
131
175
  const nextState = {
132
176
  ...(selected || posed ? withoutPending : state),
133
177
  roundCount: nextRound,
134
- ...(posed ? { pendingOptions: posed.options, pendingOptionsMode: posed.mode, ...(posed.optionDescriptions ? { pendingOptionDescriptions: posed.optionDescriptions } : {}) } : {}),
178
+ ...(posed
179
+ ? {
180
+ pendingOptions: posed.options,
181
+ pendingOptionsMode: posed.mode,
182
+ ...(posed.optionDescriptions ? { pendingOptionDescriptions: posed.optionDescriptions } : {}),
183
+ }
184
+ : {}),
135
185
  };
136
186
  const updated = this.artifacts.setExtra(discussionId, { ...discussion.extra, discussion: nextState }, context)!;
137
187
  return { discussion: updated, rounds: [round] };
@@ -143,11 +193,16 @@ export class Discussions {
143
193
  return this.artifacts.atomic(() => {
144
194
  const discussion = requireDiscussion(this.artifacts.get(discussionId), discussionId);
145
195
  const state = this.extra(discussion);
146
- if (state.state !== "active") throw new DiscussionError(`discussion "${discussionId}" is ${state.state}; only an active Discussion can be deferred`);
147
- return this.artifacts.setExtra(discussionId, {
148
- ...discussion.extra,
149
- discussion: { ...state, state: "deferred", ...(validReason === undefined ? {} : { deferredReason: validReason }) },
150
- }, context)!;
196
+ if (state.state !== "active")
197
+ throw new DiscussionError(`discussion "${discussionId}" is ${state.state}; only an active Discussion can be deferred`);
198
+ return this.artifacts.setExtra(
199
+ discussionId,
200
+ {
201
+ ...discussion.extra,
202
+ discussion: { ...state, state: "deferred", ...(validReason === undefined ? {} : { deferredReason: validReason }) },
203
+ },
204
+ context,
205
+ )!;
151
206
  });
152
207
  }
153
208
 
@@ -155,7 +210,8 @@ export class Discussions {
155
210
  return this.artifacts.atomic(() => {
156
211
  const discussion = requireDiscussion(this.artifacts.get(discussionId), discussionId);
157
212
  const state = this.extra(discussion);
158
- if (state.state !== "deferred") throw new DiscussionError(`discussion "${discussionId}" is ${state.state}; only a deferred Discussion can be resumed`);
213
+ if (state.state !== "deferred")
214
+ throw new DiscussionError(`discussion "${discussionId}" is ${state.state}; only a deferred Discussion can be resumed`);
159
215
  const { deferredReason: _deferredReason, ...rest } = state;
160
216
  return this.artifacts.setExtra(discussionId, { ...discussion.extra, discussion: { ...rest, state: "active" } }, context)!;
161
217
  });
@@ -167,10 +223,14 @@ export class Discussions {
167
223
  const discussion = requireDiscussion(this.artifacts.get(discussionId), discussionId);
168
224
  const state = this.extra(discussion);
169
225
  if (state.state === "settled") throw new DiscussionError(`discussion "${discussionId}" is already settled`);
170
- const updated = this.artifacts.setExtra(discussionId, {
171
- ...discussion.extra,
172
- discussion: { ...state, state: "settled", settlement: validSettlement, settledAt: new Date().toISOString() },
173
- }, context)!;
226
+ const updated = this.artifacts.setExtra(
227
+ discussionId,
228
+ {
229
+ ...discussion.extra,
230
+ discussion: { ...state, state: "settled", settlement: validSettlement, settledAt: new Date().toISOString() },
231
+ },
232
+ context,
233
+ )!;
174
234
  return this.artifacts.setStatus(discussionId, "done", context) ?? updated;
175
235
  });
176
236
  }
@@ -178,7 +238,8 @@ export class Discussions {
178
238
  /** Links an existing active Discussion to a Task it blocks; refuses a non-task target or an already-settled Discussion. */
179
239
  block(discussionId: string, taskId: string, context?: ArtifactEventContext): void {
180
240
  const discussion = requireDiscussion(this.artifacts.get(discussionId), discussionId);
181
- if (this.extra(discussion).state === "settled") throw new DiscussionError(`discussion "${discussionId}" is settled; it can no longer block anything`);
241
+ if (this.extra(discussion).state === "settled")
242
+ throw new DiscussionError(`discussion "${discussionId}" is settled; it can no longer block anything`);
182
243
  const task = this.artifacts.get(taskId);
183
244
  if (!task) throw new DiscussionError(`task "${taskId}" not found`);
184
245
  if (task.kind !== "task" || isDiscussionArtifact(task)) throw new DiscussionError(`artifact "${taskId}" is not a task`);
@@ -211,7 +272,11 @@ export class Discussions {
211
272
  const rows = this.artifacts.query({ kind: "task", subtype: DISCUSSION_SUBTYPE, limit });
212
273
  if (!filter.state) return rows;
213
274
  return rows.filter((row) => {
214
- try { return this.extra(row).state === filter.state; } catch { return false; }
275
+ try {
276
+ return this.extra(row).state === filter.state;
277
+ } catch {
278
+ return false;
279
+ }
215
280
  });
216
281
  }
217
282
  }
@@ -10,8 +10,17 @@
10
10
  */
11
11
  import { ARTIFACT_EVENT_ACTOR_MAX_LENGTH, ARTIFACT_EVENT_HISTORY_DEFAULT_LIMIT, ARTIFACT_EVENT_HISTORY_MAX_LIMIT } from "../constants.ts";
12
12
 
13
- export const ARTIFACT_EVENT_TYPES = ["created", "updated", "status_changed", "extra_set", "linked", "unlinked", "trashed", "restored"] as const;
14
- export type ArtifactEventType = typeof ARTIFACT_EVENT_TYPES[number];
13
+ export const ARTIFACT_EVENT_TYPES = [
14
+ "created",
15
+ "updated",
16
+ "status_changed",
17
+ "extra_set",
18
+ "linked",
19
+ "unlinked",
20
+ "trashed",
21
+ "restored",
22
+ ] as const;
23
+ export type ArtifactEventType = (typeof ARTIFACT_EVENT_TYPES)[number];
15
24
  export type ArtifactEventDirection = "asc" | "desc";
16
25
 
17
26
  /** Caller-supplied identity for a mutation. All fields are advisory (self-reported), not cryptographically verified. */
@@ -73,7 +82,9 @@ function boundedString(value: string, field: string, maximum: number): string {
73
82
  }
74
83
 
75
84
  /** Fills defaults and enforces bounds. The one place every appended event is normalized. */
76
- export function resolveArtifactEvent(input: AppendArtifactEvent): Required<Pick<AppendArtifactEvent, "actor" | "source">> & AppendArtifactEvent {
85
+ export function resolveArtifactEvent(
86
+ input: AppendArtifactEvent,
87
+ ): Required<Pick<AppendArtifactEvent, "actor" | "source">> & AppendArtifactEvent {
77
88
  if (!input.artifactId) throw new Error("artifactId is required");
78
89
  const actor = boundedString(input.actor ?? ARTIFACT_EVENT_DEFAULT_ACTOR, "actor", ARTIFACT_EVENT_ACTOR_MAX_LENGTH);
79
90
  const source = boundedString(input.source ?? ARTIFACT_EVENT_DEFAULT_SOURCE, "source", ARTIFACT_EVENT_ACTOR_MAX_LENGTH);
@@ -81,7 +92,9 @@ export function resolveArtifactEvent(input: AppendArtifactEvent): Required<Pick<
81
92
  return { ...input, actor, source };
82
93
  }
83
94
 
84
- export function normalizeArtifactEventQuery(query: ArtifactEventQuery): Required<Pick<ArtifactEventQuery, "limit" | "direction">> & ArtifactEventQuery {
95
+ export function normalizeArtifactEventQuery(
96
+ query: ArtifactEventQuery,
97
+ ): Required<Pick<ArtifactEventQuery, "limit" | "direction">> & ArtifactEventQuery {
85
98
  if (!query.artifactId && !query.actor && !query.sessionId) {
86
99
  throw new Error("artifact event query requires artifactId, actor, or sessionId to stay bounded");
87
100
  }
@@ -94,7 +94,9 @@ export function externalSourceOf(artifact: Pick<Artifact, "labels">): string | u
94
94
  export function requireLocallyOwnedContent(artifact: Artifact): Artifact {
95
95
  const system = externalSourceOf(artifact);
96
96
  if (system !== undefined) {
97
- throw new Error(`"${artifact.title}" is a read-only projection from ${system}; edit it there, or capture a correction as a new linked Doc, until a write-back capability is integrated`);
97
+ throw new Error(
98
+ `"${artifact.title}" is a read-only projection from ${system}; edit it there, or capture a correction as a new linked Doc, until a write-back capability is integrated`,
99
+ );
98
100
  }
99
101
  return artifact;
100
102
  }
@@ -1,10 +1,4 @@
1
- import {
2
- SEED_RELATIONS,
3
- SKILL_MAX_BLUEPRINTS,
4
- SKILL_MAX_ENUM_VALUES,
5
- SKILL_MAX_INPUTS,
6
- SKILL_MAX_LINKS,
7
- } from "../constants.ts";
1
+ import { SEED_RELATIONS, SKILL_MAX_BLUEPRINTS, SKILL_MAX_ENUM_VALUES, SKILL_MAX_INPUTS, SKILL_MAX_LINKS } from "../constants.ts";
8
2
 
9
3
  export type BlueprintArgumentValue = string | number | boolean;
10
4
  export type BlueprintInputType = "string" | "number" | "boolean";
@@ -124,17 +118,18 @@ function validateInputs(value: unknown): Record<string, BlueprintInputDefinition
124
118
  if (RESERVED_KEYS.has(name)) throw new Error(`reserved input name "${name}"`);
125
119
  if (!NAME_PATTERN.test(name)) throw new Error(`invalid input name "${name}"`);
126
120
  const input = record(raw, `input "${name}"`);
127
- if (!BLUEPRINT_INPUT_TYPES.has(input["type"] as BlueprintInputType)) throw new Error(`input "${name}" has unsupported type`);
128
- const type = input["type"] as BlueprintInputType;
129
- if (input["required"] !== undefined && typeof input["required"] !== "boolean") {
121
+ if (!BLUEPRINT_INPUT_TYPES.has(input.type as BlueprintInputType)) throw new Error(`input "${name}" has unsupported type`);
122
+ const type = input.type as BlueprintInputType;
123
+ if (input.required !== undefined && typeof input.required !== "boolean") {
130
124
  throw new Error(`input "${name}" required must be boolean`);
131
125
  }
132
126
  const normalized: BlueprintInputDefinition = { type };
133
- if (input["required"] !== undefined) normalized.required = input["required"] as boolean;
134
- if (input["default"] !== undefined) normalized.default = validateArgumentValue(name, type, input["default"]);
135
- if (input["enum"] !== undefined) {
136
- const values = array(input["enum"], `input "${name}" enum`);
137
- if (values.length === 0 || values.length > SKILL_MAX_ENUM_VALUES) throw new Error(`input "${name}" enum must contain 1-${SKILL_MAX_ENUM_VALUES} values`);
127
+ if (input.required !== undefined) normalized.required = input.required as boolean;
128
+ if (input.default !== undefined) normalized.default = validateArgumentValue(name, type, input.default);
129
+ if (input.enum !== undefined) {
130
+ const values = array(input.enum, `input "${name}" enum`);
131
+ if (values.length === 0 || values.length > SKILL_MAX_ENUM_VALUES)
132
+ throw new Error(`input "${name}" enum must contain 1-${SKILL_MAX_ENUM_VALUES} values`);
138
133
  normalized.enum = values.map((entry) => validateArgumentValue(name, type, entry));
139
134
  if (normalized.default !== undefined && !normalized.enum.includes(normalized.default)) {
140
135
  throw new Error(`input "${name}" default must be one of its enum values`);
@@ -147,9 +142,9 @@ function validateInputs(value: unknown): Record<string, BlueprintInputDefinition
147
142
 
148
143
  function validateBlueprint<T extends { ref: string; title: string }>(value: unknown, kind: string): T {
149
144
  const source = record(value, `${kind} blueprint`);
150
- const ref = string(source["ref"], `${kind} blueprint ref`);
145
+ const ref = string(source.ref, `${kind} blueprint ref`);
151
146
  if (!NAME_PATTERN.test(ref)) throw new Error(`invalid blueprint ref "${ref}"`);
152
- const title = string(source["title"], `${kind} blueprint title`);
147
+ const title = string(source.title, `${kind} blueprint title`);
153
148
  return { ...source, ref, title } as T;
154
149
  }
155
150
 
@@ -187,22 +182,22 @@ function assertAcyclic(steps: DependentStep[]): void {
187
182
 
188
183
  function validateCallBlueprint(value: unknown): CallBlueprint {
189
184
  const source = record(value, "call blueprint");
190
- const ref = string(source["ref"], "call blueprint ref");
185
+ const ref = string(source.ref, "call blueprint ref");
191
186
  if (!NAME_PATTERN.test(ref)) throw new Error(`invalid blueprint ref "${ref}"`);
192
- const title = string(source["title"], "call blueprint title");
193
- const targetId = string(source["targetId"] ?? source["skillId"], "call blueprint targetId");
187
+ const title = string(source.title, "call blueprint title");
188
+ const targetId = string(source.targetId ?? source.skillId, "call blueprint targetId");
194
189
  return { ...source, ref, title, targetId } as CallBlueprint;
195
190
  }
196
191
 
197
192
  export function validateBlueprintDefinition(value: unknown): BlueprintDefinition {
198
193
  const source = record(value, "blueprint definition");
199
- if (source["version"] !== 1) throw new Error("blueprint definition version must be 1");
200
- const inputs = validateInputs(source["inputs"]);
201
- const rawBlueprints = record(source["blueprints"], "blueprints");
202
- const docs = array(rawBlueprints["docs"] ?? [], "doc blueprints").map((entry) => validateBlueprint<DocBlueprint>(entry, "doc"));
203
- const rules = array(rawBlueprints["rules"] ?? [], "rule blueprints").map((entry) => validateBlueprint<RuleBlueprint>(entry, "rule"));
204
- const tasks = array(rawBlueprints["tasks"] ?? [], "task blueprints").map((entry) => validateBlueprint<TaskBlueprint>(entry, "task"));
205
- const calls = array(rawBlueprints["skills"] ?? [], "call blueprints").map(validateCallBlueprint);
194
+ if (source.version !== 1) throw new Error("blueprint definition version must be 1");
195
+ const inputs = validateInputs(source.inputs);
196
+ const rawBlueprints = record(source.blueprints, "blueprints");
197
+ const docs = array(rawBlueprints.docs ?? [], "doc blueprints").map((entry) => validateBlueprint<DocBlueprint>(entry, "doc"));
198
+ const rules = array(rawBlueprints.rules ?? [], "rule blueprints").map((entry) => validateBlueprint<RuleBlueprint>(entry, "rule"));
199
+ const tasks = array(rawBlueprints.tasks ?? [], "task blueprints").map((entry) => validateBlueprint<TaskBlueprint>(entry, "task"));
200
+ const calls = array(rawBlueprints.skills ?? [], "call blueprints").map(validateCallBlueprint);
206
201
  const all = [...docs, ...rules, ...tasks, ...calls];
207
202
  if (all.length === 0 || all.length > SKILL_MAX_BLUEPRINTS) throw new Error(`blueprints must contain 1-${SKILL_MAX_BLUEPRINTS} artifacts`);
208
203
  const refs = new Set<string>();
@@ -237,11 +232,11 @@ export function validateBlueprintDefinition(value: unknown): BlueprintDefinition
237
232
  for (const name of placeholders(all)) {
238
233
  if (!Object.hasOwn(inputs, name)) throw new Error(`unknown input placeholder "${name}"`);
239
234
  }
240
- const links = array(source["links"] ?? [], "links").map((entry) => {
235
+ const links = array(source.links ?? [], "links").map((entry) => {
241
236
  const link = record(entry, "link");
242
- const from = string(link["from"], "link from");
243
- const relation = string(link["relation"], "link relation");
244
- const to = string(link["to"], "link to");
237
+ const from = string(link.from, "link from");
238
+ const relation = string(link.relation, "link relation");
239
+ const to = string(link.to, "link to");
245
240
  if (!refs.has(from)) throw new Error(`unknown blueprint ref "${from}"`);
246
241
  if (!refs.has(to)) throw new Error(`unknown blueprint ref "${to}"`);
247
242
  if (!RELATIONS.has(relation)) throw new Error(`unknown link relation "${relation}"`);
@@ -1,6 +1,6 @@
1
1
  export const PROOF_TYPES = ["file", "symbol", "code", "test", "command", "artifact", "url"] as const;
2
2
 
3
- export type ProofType = typeof PROOF_TYPES[number];
3
+ export type ProofType = (typeof PROOF_TYPES)[number];
4
4
 
5
5
  export interface ProofReference {
6
6
  type: ProofType;
@@ -25,13 +25,13 @@ function isRecord(value: unknown): value is Record<string, unknown> {
25
25
  }
26
26
 
27
27
  function proofReference(value: unknown): ProofReference | undefined {
28
- if (!isRecord(value) || !PROOF_TYPES.includes(value["type"] as ProofType)) return undefined;
29
- if (typeof value["target"] !== "string" || value["target"].trim().length === 0) return undefined;
30
- if (value["expect"] !== undefined && typeof value["expect"] !== "string") return undefined;
28
+ if (!isRecord(value) || !PROOF_TYPES.includes(value.type as ProofType)) return undefined;
29
+ if (typeof value.target !== "string" || value.target.trim().length === 0) return undefined;
30
+ if (value.expect !== undefined && typeof value.expect !== "string") return undefined;
31
31
  return {
32
- type: value["type"] as ProofType,
33
- target: value["target"],
34
- ...(typeof value["expect"] === "string" ? { expect: value["expect"] } : {}),
32
+ type: value.type as ProofType,
33
+ target: value.target,
34
+ ...(typeof value.expect === "string" ? { expect: value.expect } : {}),
35
35
  };
36
36
  }
37
37
 
@@ -40,10 +40,10 @@ export function validateChecklist(value: unknown): Checklist {
40
40
  const checklist: Checklist = {};
41
41
  for (const [item, criterion] of Object.entries(value)) {
42
42
  if (item.trim().length === 0) throw new Error("checklist item must not be empty");
43
- if (!isRecord(criterion) || !Array.isArray(criterion["proof"]) || criterion["proof"].length === 0) {
43
+ if (!isRecord(criterion) || !Array.isArray(criterion.proof) || criterion.proof.length === 0) {
44
44
  throw new Error(`checklist item "${item}" requires at least one proof reference`);
45
45
  }
46
- const proof = criterion["proof"].map(proofReference);
46
+ const proof = criterion.proof.map(proofReference);
47
47
  if (proof.some((reference) => reference === undefined)) {
48
48
  throw new Error(`checklist item "${item}" requires a typed, non-empty proof target`);
49
49
  }
@@ -54,17 +54,20 @@ export function validateChecklist(value: unknown): Checklist {
54
54
 
55
55
  export function checklistEntries(value: unknown): ChecklistEntry[] {
56
56
  if (Array.isArray(value)) {
57
- return value.flatMap((item) => typeof item === "string"
58
- ? [{ item, proof: [], legacy: true }]
59
- : isRecord(item) && typeof item["title"] === "string"
60
- ? [{ item: item["title"], proof: [], legacy: true }]
61
- : []);
57
+ return value.flatMap((item) =>
58
+ typeof item === "string"
59
+ ? [{ item, proof: [], legacy: true }]
60
+ : isRecord(item) && typeof item.title === "string"
61
+ ? [{ item: item.title, proof: [], legacy: true }]
62
+ : [],
63
+ );
62
64
  }
63
65
  if (!isRecord(value)) return [];
64
66
  return Object.entries(value).map(([item, criterion]) => {
65
- const references = isRecord(criterion) && Array.isArray(criterion["proof"])
66
- ? criterion["proof"].map(proofReference).filter((proof): proof is ProofReference => proof !== undefined)
67
- : [];
67
+ const references =
68
+ isRecord(criterion) && Array.isArray(criterion.proof)
69
+ ? criterion.proof.map(proofReference).filter((proof): proof is ProofReference => proof !== undefined)
70
+ : [];
68
71
  return { item, proof: references, legacy: false };
69
72
  });
70
73
  }
@@ -32,7 +32,7 @@ import {
32
32
  export const DISCUSSION_SUBTYPE = "discussion";
33
33
 
34
34
  export const DISCUSSION_STATES = ["active", "deferred", "settled"] as const;
35
- export type DiscussionState = typeof DISCUSSION_STATES[number];
35
+ export type DiscussionState = (typeof DISCUSSION_STATES)[number];
36
36
 
37
37
  /**
38
38
  * A round can pose a choice (options + optionsMode) the way opencode's QuestionV2 poses
@@ -40,7 +40,7 @@ export type DiscussionState = typeof DISCUSSION_STATES[number];
40
40
  * (exactly one pick); "multi" allows several -- see constants.ts.
41
41
  */
42
42
  export const DISCUSSION_OPTIONS_MODES = ["single", "multi"] as const;
43
- export type DiscussionOptionsMode = typeof DISCUSSION_OPTIONS_MODES[number];
43
+ export type DiscussionOptionsMode = (typeof DISCUSSION_OPTIONS_MODES)[number];
44
44
 
45
45
  /** Persisted in a discussion Doc's `extra.discussion`. pendingOptions/-Mode is the current-state cache of "is there an unanswered posed choice right now" -- cleared once answered, set again whenever a round poses a new one. */
46
46
  export interface DiscussionExtra {
@@ -113,7 +113,11 @@ export function validateSettlement(settlement: string): string {
113
113
  /** Validates a freshly-posed choice: 2..DISCUSSION_OPTIONS_MAX_COUNT unique, bounded-length
114
114
  * options, a real mode, and a non-empty description for every option once
115
115
  * DISCUSSION_OPTION_DESCRIPTION_REQUIRED_FROM_COUNT or more are posed. */
116
- export function validateDiscussionOptions(options: string[], mode: string, optionDescriptions?: string[]): { options: string[]; mode: DiscussionOptionsMode; optionDescriptions?: string[] } {
116
+ export function validateDiscussionOptions(
117
+ options: string[],
118
+ mode: string,
119
+ optionDescriptions?: string[],
120
+ ): { options: string[]; mode: DiscussionOptionsMode; optionDescriptions?: string[] } {
117
121
  if (!(DISCUSSION_OPTIONS_MODES as readonly string[]).includes(mode)) {
118
122
  throw new Error(`options_mode must be one of ${DISCUSSION_OPTIONS_MODES.join(", ")}`);
119
123
  }
@@ -123,20 +127,35 @@ export function validateDiscussionOptions(options: string[], mode: string, optio
123
127
  for (const option of options) boundedString(option, "option", DISCUSSION_OPTION_MAX_LENGTH);
124
128
  if (new Set(options).size !== options.length) throw new Error("options must not repeat an entry");
125
129
  const descriptionsRequired = options.length >= DISCUSSION_OPTION_DESCRIPTION_REQUIRED_FROM_COUNT;
126
- if (descriptionsRequired && (optionDescriptions === undefined || optionDescriptions.some((description) => description.trim().length === 0))) {
127
- throw new Error(`option_descriptions is required, with a non-empty entry for every option, once ${DISCUSSION_OPTION_DESCRIPTION_REQUIRED_FROM_COUNT} or more options are posed`);
130
+ if (
131
+ descriptionsRequired &&
132
+ (optionDescriptions === undefined || optionDescriptions.some((description) => description.trim().length === 0))
133
+ ) {
134
+ throw new Error(
135
+ `option_descriptions is required, with a non-empty entry for every option, once ${DISCUSSION_OPTION_DESCRIPTION_REQUIRED_FROM_COUNT} or more options are posed`,
136
+ );
128
137
  }
129
138
  if (optionDescriptions !== undefined) {
130
- if (optionDescriptions.length !== options.length) throw new Error("option_descriptions must have exactly one entry per option (use an empty string for none)");
139
+ if (optionDescriptions.length !== options.length)
140
+ throw new Error("option_descriptions must have exactly one entry per option (use an empty string for none)");
131
141
  for (const description of optionDescriptions) {
132
- if (description.length > DISCUSSION_OPTION_DESCRIPTION_MAX_LENGTH) throw new Error(`option description must be at most ${DISCUSSION_OPTION_DESCRIPTION_MAX_LENGTH} characters`);
142
+ if (description.length > DISCUSSION_OPTION_DESCRIPTION_MAX_LENGTH)
143
+ throw new Error(`option description must be at most ${DISCUSSION_OPTION_DESCRIPTION_MAX_LENGTH} characters`);
133
144
  }
134
145
  }
135
- return { options: [...options], mode: mode as DiscussionOptionsMode, ...(optionDescriptions !== undefined ? { optionDescriptions: [...optionDescriptions] } : {}) };
146
+ return {
147
+ options: [...options],
148
+ mode: mode as DiscussionOptionsMode,
149
+ ...(optionDescriptions !== undefined ? { optionDescriptions: [...optionDescriptions] } : {}),
150
+ };
136
151
  }
137
152
 
138
153
  /** Validates an answer against the Discussion's currently pending posed choice, if any. */
139
- export function validateSelectedOptions(selected: string[], pendingOptions: string[] | undefined, pendingMode: DiscussionOptionsMode | undefined): string[] {
154
+ export function validateSelectedOptions(
155
+ selected: string[],
156
+ pendingOptions: string[] | undefined,
157
+ pendingMode: DiscussionOptionsMode | undefined,
158
+ ): string[] {
140
159
  if (!pendingOptions || pendingOptions.length === 0 || !pendingMode) {
141
160
  throw new Error("this Discussion has no pending options to select from");
142
161
  }
@@ -155,26 +174,26 @@ export function isDiscussionArtifact(artifact: { kind: string; subtype: string }
155
174
 
156
175
  /** Reads and defensively validates the extra.discussion shape; throws on a corrupt/foreign shape rather than silently treating it as some default state. */
157
176
  export function readDiscussionExtra(extra: Record<string, unknown>): DiscussionExtra {
158
- const raw = extra["discussion"];
177
+ const raw = extra.discussion;
159
178
  if (typeof raw !== "object" || raw === null) throw new Error("artifact is not a Discussion (missing extra.discussion)");
160
179
  const record = raw as Record<string, unknown>;
161
- const state = record["state"];
180
+ const state = record.state;
162
181
  if (typeof state !== "string" || !(DISCUSSION_STATES as readonly string[]).includes(state)) {
163
182
  throw new Error(`invalid Discussion state "${String(state)}"`);
164
183
  }
165
- const roundCount = record["roundCount"];
184
+ const roundCount = record.roundCount;
166
185
  if (typeof roundCount !== "number" || !Number.isInteger(roundCount) || roundCount < 0) {
167
186
  throw new Error("invalid Discussion roundCount");
168
187
  }
169
- const pendingOptions = record["pendingOptions"];
188
+ const pendingOptions = record.pendingOptions;
170
189
  if (pendingOptions !== undefined && (!Array.isArray(pendingOptions) || pendingOptions.some((entry) => typeof entry !== "string"))) {
171
190
  throw new Error("invalid Discussion pendingOptions");
172
191
  }
173
- const pendingOptionsMode = record["pendingOptionsMode"];
192
+ const pendingOptionsMode = record.pendingOptionsMode;
174
193
  if (pendingOptionsMode !== undefined && !(DISCUSSION_OPTIONS_MODES as readonly unknown[]).includes(pendingOptionsMode)) {
175
194
  throw new Error("invalid Discussion pendingOptionsMode");
176
195
  }
177
- const pendingOptionDescriptions = record["pendingOptionDescriptions"];
196
+ const pendingOptionDescriptions = record.pendingOptionDescriptions;
178
197
  if (pendingOptionDescriptions !== undefined) {
179
198
  if (!Array.isArray(pendingOptionDescriptions) || pendingOptionDescriptions.some((entry) => typeof entry !== "string")) {
180
199
  throw new Error("invalid Discussion pendingOptionDescriptions");
@@ -186,9 +205,9 @@ export function readDiscussionExtra(extra: Record<string, unknown>): DiscussionE
186
205
  return {
187
206
  state: state as DiscussionState,
188
207
  roundCount,
189
- ...(typeof record["deferredReason"] === "string" ? { deferredReason: record["deferredReason"] } : {}),
190
- ...(typeof record["settlement"] === "string" ? { settlement: record["settlement"] } : {}),
191
- ...(typeof record["settledAt"] === "string" ? { settledAt: record["settledAt"] } : {}),
208
+ ...(typeof record.deferredReason === "string" ? { deferredReason: record.deferredReason } : {}),
209
+ ...(typeof record.settlement === "string" ? { settlement: record.settlement } : {}),
210
+ ...(typeof record.settledAt === "string" ? { settledAt: record.settledAt } : {}),
192
211
  ...(pendingOptions !== undefined ? { pendingOptions: pendingOptions as string[] } : {}),
193
212
  ...(pendingOptionsMode !== undefined ? { pendingOptionsMode: pendingOptionsMode as DiscussionOptionsMode } : {}),
194
213
  ...(pendingOptionDescriptions !== undefined ? { pendingOptionDescriptions: pendingOptionDescriptions as string[] } : {}),
@@ -1,5 +1,5 @@
1
1
  export const GATE_TYPES = ["file-exists", "command", "contains", "test"] as const;
2
- export type GateType = typeof GATE_TYPES[number];
2
+ export type GateType = (typeof GATE_TYPES)[number];
3
3
 
4
4
  export interface Gate {
5
5
  type: GateType;
@@ -20,19 +20,19 @@ function isRecord(value: unknown): value is Record<string, unknown> {
20
20
  export function validateGates(value: unknown): Gate[] {
21
21
  if (!Array.isArray(value)) throw new Error("gates must be an array");
22
22
  return value.map((entry, index) => {
23
- if (!isRecord(entry) || !GATE_TYPES.includes(entry["type"] as GateType)) {
23
+ if (!isRecord(entry) || !GATE_TYPES.includes(entry.type as GateType)) {
24
24
  throw new Error(`gate at index ${index} requires a valid type (${GATE_TYPES.join(", ")})`);
25
25
  }
26
- if (typeof entry["target"] !== "string" || entry["target"].trim().length === 0) {
26
+ if (typeof entry.target !== "string" || entry.target.trim().length === 0) {
27
27
  throw new Error(`gate at index ${index} requires a non-empty target`);
28
28
  }
29
- if (entry["expect"] !== undefined && typeof entry["expect"] !== "string") {
29
+ if (entry.expect !== undefined && typeof entry.expect !== "string") {
30
30
  throw new Error(`gate at index ${index} expect must be a string`);
31
31
  }
32
32
  return {
33
- type: entry["type"] as GateType,
34
- target: entry["target"],
35
- ...(typeof entry["expect"] === "string" ? { expect: entry["expect"] } : {}),
33
+ type: entry.type as GateType,
34
+ target: entry.target,
35
+ ...(typeof entry.expect === "string" ? { expect: entry.expect } : {}),
36
36
  };
37
37
  });
38
38
  }
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  export const LOG_LEVELS = ["debug", "info", "warning", "error"] as const;
21
- export type LogLevel = typeof LOG_LEVELS[number];
21
+ export type LogLevel = (typeof LOG_LEVELS)[number];
22
22
 
23
23
  export const LOG_SOURCE_ID_MAX_LENGTH = 256;
24
24
  export const LOG_MESSAGE_MAX_CHARACTERS = 4000;