@brainervirus/workit-core 0.11.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +5 -3
  2. package/package.json +8 -3
  3. package/scripts/analyze-release-scope.ts +4 -1
  4. package/scripts/doctor-check.ts +3 -1
  5. package/scripts/install-codex-plugin.sh +28 -0
  6. package/scripts/install-cursor-plugin.sh +1 -0
  7. package/scripts/install-opencode-plugin.sh +1 -0
  8. package/scripts/install-pi-package.sh +56 -0
  9. package/scripts/rewrite-workspace-deps.ts +22 -5
  10. package/scripts/sync-release-manifests.ts +8 -5
  11. package/scripts/sync-runtime.sh +7 -3
  12. package/scripts/validate-cursor-marketplace.ts +16 -62
  13. package/skills/workit-babysit/SKILL.md +33 -0
  14. package/skills/workit-behavioral-tdd/SKILL.md +53 -0
  15. package/skills/workit-blast-radius/SKILL.md +31 -0
  16. package/skills/workit-challenge/SKILL.md +62 -0
  17. package/skills/workit-debug/SKILL.md +61 -0
  18. package/skills/workit-deslop/SKILL.md +36 -0
  19. package/skills/workit-diagram/SKILL.md +32 -0
  20. package/skills/workit-green-run/SKILL.md +29 -0
  21. package/skills/workit-handoff/SKILL.md +43 -0
  22. package/skills/workit-implement/SKILL.md +46 -0
  23. package/skills/workit-mockup/SKILL.md +28 -0
  24. package/skills/workit-plan/SKILL.md +66 -0
  25. package/skills/workit-review/SKILL.md +60 -0
  26. package/skills/workit-steer/SKILL.md +32 -0
  27. package/src/core/authority.ts +918 -0
  28. package/src/core/boundary.ts +29 -0
  29. package/src/core/branch.ts +26 -255
  30. package/src/core/changelog.ts +95 -18
  31. package/src/core/commit-flavors.ts +65 -0
  32. package/src/core/config-conversion.ts +183 -0
  33. package/src/core/config.ts +42 -0
  34. package/src/core/cutover.ts +721 -0
  35. package/src/core/detect-hosts.ts +107 -0
  36. package/src/core/doctor.ts +428 -18
  37. package/src/core/external-action-effects.ts +1171 -0
  38. package/src/core/external-action.ts +541 -0
  39. package/src/core/init.ts +83 -32
  40. package/src/core/methods.ts +125 -0
  41. package/src/core/policy-resolver.ts +523 -0
  42. package/src/core/pr-create.ts +7 -1
  43. package/src/core/registration.ts +69 -4
  44. package/src/core/repo-context.ts +59 -1
  45. package/src/core/setup.ts +28 -0
  46. package/src/core/skill-manifests.ts +40 -42
  47. package/src/core/support-matrix.ts +8 -3
  48. package/src/core/sync-runtime.ts +15 -3
  49. package/src/core/task-context.ts +179 -0
  50. package/src/core/task-contract.ts +1136 -0
  51. package/src/core/task-engine.ts +2562 -0
  52. package/src/core/task-evaluation.ts +679 -0
  53. package/src/core/task-store.ts +1071 -0
  54. package/src/core/tracker-issues.ts +237 -0
  55. package/src/core/uninstall.ts +67 -3
  56. package/src/core/vcs-config.ts +53 -22
  57. package/src/core/workers.ts +166 -0
  58. package/src/core/workspaces.ts +3 -1
  59. package/src/core/youtrack-tools.ts +2 -0
  60. package/src/core/youtrack.ts +145 -10
  61. package/src/core.ts +126 -0
  62. package/templates/execution-contract.md +17 -50
  63. package/templates/plan-template.md +3 -3
  64. package/templates/spec-template.md +15 -3
  65. package/templates/workit-contract.md +12 -0
  66. package/commands/wk-changelog.md +0 -2
  67. package/commands/wk-commit.md +0 -2
  68. package/commands/wk-docs-refresh.md +0 -2
  69. package/commands/wk-handoff.md +0 -2
  70. package/commands/wk-implement.md +0 -2
  71. package/commands/wk-init.md +0 -2
  72. package/commands/wk-issue-update.md +0 -2
  73. package/commands/wk-meetings.md +0 -2
  74. package/commands/wk-pr.md +0 -2
  75. package/commands/wk-release-notes.md +0 -2
  76. package/commands/wk-status.md +0 -2
  77. package/commands/wk-verify.md +0 -2
  78. package/scripts/update-superpowers.sh +0 -82
  79. package/scripts/vendor-assets.ts +0 -37
  80. package/skills/wk-changelog/SKILL.md +0 -15
  81. package/skills/wk-commit/SKILL.md +0 -16
  82. package/skills/wk-docs-refresh/SKILL.md +0 -15
  83. package/skills/wk-handoff/SKILL.md +0 -20
  84. package/skills/wk-implement/SKILL.md +0 -47
  85. package/skills/wk-init/SKILL.md +0 -31
  86. package/skills/wk-issue-update/SKILL.md +0 -27
  87. package/skills/wk-issue-update/references/youtrack-update-style.md +0 -81
  88. package/skills/wk-meetings/SKILL.md +0 -17
  89. package/skills/wk-pr/SKILL.md +0 -27
  90. package/skills/wk-release-notes/SKILL.md +0 -15
  91. package/skills/wk-status/SKILL.md +0 -16
  92. package/skills/wk-verify/SKILL.md +0 -16
  93. package/src/core/detector.ts +0 -239
  94. package/src/core/flow-state.ts +0 -3308
  95. package/src/core/handoff-context.ts +0 -136
  96. package/src/core/handoff-tools.ts +0 -133
  97. package/src/core/menu.ts +0 -70
  98. package/src/core/plan-tasks.ts +0 -43
  99. package/src/core/reminder.ts +0 -124
  100. package/src/core/sdd.ts +0 -353
  101. package/src/state.ts +0 -22
  102. package/templates/superpowers-doc-contract.md +0 -75
  103. package/vendor/superpowers/skills/brainstorming/SKILL.md +0 -159
  104. package/vendor/superpowers/skills/brainstorming/scripts/frame-template.html +0 -213
  105. package/vendor/superpowers/skills/brainstorming/scripts/helper.js +0 -167
  106. package/vendor/superpowers/skills/brainstorming/scripts/server.cjs +0 -723
  107. package/vendor/superpowers/skills/brainstorming/scripts/start-server.sh +0 -209
  108. package/vendor/superpowers/skills/brainstorming/scripts/stop-server.sh +0 -120
  109. package/vendor/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +0 -49
  110. package/vendor/superpowers/skills/brainstorming/visual-companion.md +0 -291
  111. package/vendor/superpowers/skills/dispatching-parallel-agents/SKILL.md +0 -185
  112. package/vendor/superpowers/skills/executing-plans/SKILL.md +0 -70
  113. package/vendor/superpowers/skills/finishing-a-development-branch/SKILL.md +0 -241
  114. package/vendor/superpowers/skills/receiving-code-review/SKILL.md +0 -213
  115. package/vendor/superpowers/skills/requesting-code-review/SKILL.md +0 -103
  116. package/vendor/superpowers/skills/requesting-code-review/code-reviewer.md +0 -172
  117. package/vendor/superpowers/skills/subagent-driven-development/SKILL.md +0 -428
  118. package/vendor/superpowers/skills/subagent-driven-development/implementer-prompt.md +0 -139
  119. package/vendor/superpowers/skills/subagent-driven-development/scripts/review-package +0 -44
  120. package/vendor/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +0 -22
  121. package/vendor/superpowers/skills/subagent-driven-development/scripts/task-brief +0 -40
  122. package/vendor/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +0 -188
  123. package/vendor/superpowers/skills/systematic-debugging/CREATION-LOG.md +0 -119
  124. package/vendor/superpowers/skills/systematic-debugging/SKILL.md +0 -296
  125. package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +0 -158
  126. package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting.md +0 -115
  127. package/vendor/superpowers/skills/systematic-debugging/defense-in-depth.md +0 -122
  128. package/vendor/superpowers/skills/systematic-debugging/find-polluter.sh +0 -63
  129. package/vendor/superpowers/skills/systematic-debugging/root-cause-tracing.md +0 -169
  130. package/vendor/superpowers/skills/systematic-debugging/test-academic.md +0 -14
  131. package/vendor/superpowers/skills/systematic-debugging/test-pressure-1.md +0 -58
  132. package/vendor/superpowers/skills/systematic-debugging/test-pressure-2.md +0 -68
  133. package/vendor/superpowers/skills/systematic-debugging/test-pressure-3.md +0 -69
  134. package/vendor/superpowers/skills/test-driven-development/SKILL.md +0 -371
  135. package/vendor/superpowers/skills/test-driven-development/testing-anti-patterns.md +0 -299
  136. package/vendor/superpowers/skills/using-git-worktrees/SKILL.md +0 -202
  137. package/vendor/superpowers/skills/using-superpowers/SKILL.md +0 -62
  138. package/vendor/superpowers/skills/using-superpowers/references/antigravity-tools.md +0 -23
  139. package/vendor/superpowers/skills/using-superpowers/references/codex-tools.md +0 -39
  140. package/vendor/superpowers/skills/using-superpowers/references/pi-tools.md +0 -16
  141. package/vendor/superpowers/skills/verification-before-completion/SKILL.md +0 -139
  142. package/vendor/superpowers/skills/writing-plans/SKILL.md +0 -174
  143. package/vendor/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
  144. package/vendor/superpowers/skills/writing-skills/SKILL.md +0 -689
  145. package/vendor/superpowers/skills/writing-skills/anthropic-best-practices.md +0 -1150
  146. package/vendor/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +0 -189
  147. package/vendor/superpowers/skills/writing-skills/graphviz-conventions.dot +0 -172
  148. package/vendor/superpowers/skills/writing-skills/persuasion-principles.md +0 -187
  149. package/vendor/superpowers/skills/writing-skills/render-graphs.js +0 -168
  150. package/vendor/superpowers/skills/writing-skills/testing-skills-with-subagents.md +0 -384
@@ -0,0 +1,1136 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import * as z from "zod";
3
+
4
+ export const SCHEMA_VERSION = 1 as const;
5
+ export const POLICY_VERSION = "1.0.0" as const;
6
+ export const OPERATION_FAMILIES = [
7
+ "task",
8
+ "policy",
9
+ "evidence",
10
+ "finding",
11
+ "decision",
12
+ "worker",
13
+ "writer",
14
+ "state",
15
+ ] as const;
16
+ export type OperationFamily = (typeof OPERATION_FAMILIES)[number];
17
+
18
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
19
+ const digestPattern = /^[0-9a-f]{64}$/;
20
+ const invalidUnicode = (value: string): boolean => {
21
+ for (let i = 0; i < value.length; i += 1) {
22
+ const code = value.charCodeAt(i);
23
+ if (code >= 0xd800 && code <= 0xdbff) {
24
+ const next = value.charCodeAt(i + 1);
25
+ if (Number.isNaN(next) || next < 0xdc00 || next > 0xdfff) return true;
26
+ i += 1;
27
+ } else if (code >= 0xdc00 && code <= 0xdfff) return true;
28
+ }
29
+ return false;
30
+ };
31
+
32
+ const text = z.string().check((ctx) => {
33
+ if (invalidUnicode(ctx.value))
34
+ ctx.issues.push({ code: "custom", input: ctx.value, message: "invalid Unicode", path: [] });
35
+ });
36
+ const id = text.regex(uuidPattern, "expected lowercase UUID");
37
+ const digest = text.regex(digestPattern, "expected lowercase SHA-256 digest");
38
+ const revision = text.regex(uuidPattern, "expected UUID revision");
39
+ const validUtc = (value: string): boolean => {
40
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?Z$/.exec(value);
41
+ if (!match) return false;
42
+ const [, year, month, day, hour, minute, second] = match;
43
+ const date = new Date(0);
44
+ date.setUTCFullYear(Number(year), Number(month) - 1, Number(day));
45
+ date.setUTCHours(Number(hour), Number(minute), Number(second), 0);
46
+ return (
47
+ date.getUTCFullYear() === Number(year) &&
48
+ date.getUTCMonth() === Number(month) - 1 &&
49
+ date.getUTCDate() === Number(day) &&
50
+ date.getUTCHours() === Number(hour) &&
51
+ date.getUTCMinutes() === Number(minute) &&
52
+ date.getUTCSeconds() === Number(second)
53
+ );
54
+ };
55
+ export const utcSchema = text.check((ctx) => {
56
+ if (!validUtc(ctx.value))
57
+ ctx.issues.push({
58
+ code: "custom",
59
+ input: ctx.value,
60
+ message: "expected UTC timestamp",
61
+ path: [],
62
+ });
63
+ });
64
+ const utc = utcSchema;
65
+ const safeInteger = z.number().int().safe();
66
+ const nonEmpty = text.min(1);
67
+ const pathValue = nonEmpty.check((ctx) => {
68
+ if (ctx.value !== "." && (ctx.value.includes("\\") || ctx.value.split("/").includes(".."))) {
69
+ ctx.issues.push({
70
+ code: "custom",
71
+ input: ctx.value,
72
+ message: "invalid path",
73
+ path: [],
74
+ });
75
+ }
76
+ });
77
+ const nullableDigest = digest.nullable();
78
+ const nullableId = id.nullable();
79
+
80
+ export const hostSchema = z.enum([
81
+ "opencode",
82
+ "cursor",
83
+ "codex_cli",
84
+ "codex_desktop",
85
+ "pi",
86
+ "workit_cli",
87
+ ]);
88
+ export type Host = z.infer<typeof hostSchema>;
89
+ export const assuranceSchema = z.enum(["enforced", "agent_guided", "unavailable"]);
90
+ export type Assurance = z.infer<typeof assuranceSchema>;
91
+ export const outcomeSchema = z.enum(["verified", "accepted_limitations", "stopped"]);
92
+ export type Outcome = z.infer<typeof outcomeSchema>;
93
+
94
+ export const scopeSchema = z
95
+ .object({
96
+ description: text,
97
+ paths: z.array(pathValue),
98
+ exclusions: z.array(pathValue),
99
+ })
100
+ .strict();
101
+ export type Scope = z.infer<typeof scopeSchema>;
102
+
103
+ export const scopeCovers = (outer: Scope, inner: Scope): boolean => {
104
+ // Scope paths may carry trailing slashes; normalize so `docs/` covers `docs/x`.
105
+ const strip = (value: string): string => (value.length > 1 ? value.replace(/\/+$/, "") : value);
106
+ const normOuter: Scope = {
107
+ ...outer,
108
+ paths: outer.paths.map(strip),
109
+ exclusions: outer.exclusions.map(strip),
110
+ };
111
+ const innerPaths = (inner.paths.length ? inner.paths : ["."]).map(strip);
112
+ const innerExclusions = inner.exclusions.map(strip);
113
+ const excluded = (path: string, scope: Scope): boolean =>
114
+ scope.exclusions.some(
115
+ (excludedPath) => excludedPath === path || path.startsWith(`${excludedPath}/`),
116
+ );
117
+ const covers = (path: string): boolean =>
118
+ normOuter.paths.some((base) => base === "." || path === base || path.startsWith(`${base}/`)) &&
119
+ !excluded(path, normOuter);
120
+ const innerExcludes = (path: string): boolean =>
121
+ innerExclusions.some(
122
+ (excludedPath) => excludedPath === path || path.startsWith(`${excludedPath}/`),
123
+ );
124
+ return (
125
+ innerPaths.every(covers) &&
126
+ !normOuter.exclusions.some((excludedPath) =>
127
+ innerPaths.some(
128
+ (base) =>
129
+ (base === "." || excludedPath === base || excludedPath.startsWith(`${base}/`)) &&
130
+ !innerExcludes(excludedPath),
131
+ ),
132
+ )
133
+ );
134
+ };
135
+
136
+ export const refSchema = z.discriminatedUnion("kind", [
137
+ z.object({ kind: z.literal("file"), path: pathValue, digest: nullableDigest }).strict(),
138
+ z
139
+ .object({
140
+ kind: z.literal("record"),
141
+ collection: z.enum(["evidence", "decisions", "findings", "workers"]),
142
+ id,
143
+ })
144
+ .strict(),
145
+ z.object({ kind: z.literal("host"), host: hostSchema, handle: nonEmpty }).strict(),
146
+ z.object({ kind: z.literal("external"), url: text.url() }).strict(),
147
+ ]);
148
+ export type Ref = z.infer<typeof refSchema>;
149
+
150
+ export const provenanceSchema = z
151
+ .object({
152
+ kind: z.enum(["host_observed", "agent_reported", "imported"]),
153
+ host: hostSchema,
154
+ session: refSchema.nullable(),
155
+ workerId: nullableId,
156
+ receipts: z.array(refSchema),
157
+ })
158
+ .strict();
159
+ export type Provenance = z.infer<typeof provenanceSchema>;
160
+ export const entrySchema = <T extends z.ZodType>(data: T) =>
161
+ z.object({ id, recordedAt: utc, provenance: provenanceSchema, data }).strict();
162
+ export type Entry<T> = { id: Id; recordedAt: Utc; provenance: Provenance; data: T };
163
+
164
+ export const factSchema = z
165
+ .object({
166
+ statement: text,
167
+ basis: z.enum(["observed", "inferred", "unknown"]),
168
+ refs: z.array(refSchema),
169
+ })
170
+ .strict()
171
+ .check((ctx) => {
172
+ if (ctx.value.basis === "observed" && ctx.value.refs.length === 0)
173
+ ctx.issues.push({
174
+ code: "custom",
175
+ input: ctx.value,
176
+ message: "observed facts require supporting references",
177
+ path: ["refs"],
178
+ });
179
+ });
180
+ export type Fact = z.infer<typeof factSchema>;
181
+ export const signalSchema = z
182
+ .object({
183
+ value: z.union([z.boolean(), z.literal("unknown")]),
184
+ basis: z.enum(["observed", "inferred", "unknown"]),
185
+ reason: text,
186
+ refs: z.array(refSchema),
187
+ })
188
+ .strict()
189
+ .check((ctx) => {
190
+ const signal = ctx.value;
191
+ const valid =
192
+ signal.value === "unknown"
193
+ ? signal.basis === "unknown"
194
+ : signal.basis === "observed" || signal.basis === "inferred";
195
+ if (!valid)
196
+ ctx.issues.push({
197
+ code: "custom",
198
+ input: signal,
199
+ message: "signal value and basis disagree",
200
+ path: [],
201
+ });
202
+ if (signal.basis === "observed" && signal.refs.length === 0)
203
+ ctx.issues.push({
204
+ code: "custom",
205
+ input: signal,
206
+ message: "observed signals require supporting references",
207
+ path: ["refs"],
208
+ });
209
+ });
210
+ export type Signal = z.infer<typeof signalSchema>;
211
+ const signalSet = z
212
+ .object({
213
+ approachUnknown: signalSchema,
214
+ productChoiceOpen: signalSchema,
215
+ behaviorChange: signalSchema,
216
+ mechanicalLowRisk: signalSchema,
217
+ durableAgreementNeeded: signalSchema,
218
+ coordinationPlanNeeded: signalSchema,
219
+ helperUseful: signalSchema,
220
+ testFirstPractical: signalSchema,
221
+ })
222
+ .strict();
223
+ const consequenceSchema = z
224
+ .object({
225
+ area: z.enum([
226
+ "behavior",
227
+ "public_contract",
228
+ "data",
229
+ "security",
230
+ "operations",
231
+ "reversibility",
232
+ "consumers",
233
+ ]),
234
+ fact: factSchema,
235
+ })
236
+ .strict();
237
+ const verificationSchema = z
238
+ .object({
239
+ claim: text,
240
+ scope: scopeSchema,
241
+ availableChecks: z.array(refSchema),
242
+ gaps: z.array(text),
243
+ })
244
+ .strict();
245
+ export const assessmentSchema = z
246
+ .object({
247
+ facts: z.array(factSchema),
248
+ signals: signalSet,
249
+ consequences: z.array(consequenceSchema),
250
+ verification: z.array(verificationSchema),
251
+ })
252
+ .strict();
253
+ export type Assessment = z.infer<typeof assessmentSchema>;
254
+
255
+ export const constraintSchema = z
256
+ .object({
257
+ id: nonEmpty,
258
+ kind: z.enum(["host", "project", "user"]),
259
+ statement: text,
260
+ source: refSchema,
261
+ acceptanceAllowed: z.boolean(),
262
+ requires: z.array(
263
+ z
264
+ .object({
265
+ kind: z.enum(["check", "method", "decision"]),
266
+ scope: scopeSchema,
267
+ refs: z.array(refSchema),
268
+ method: z
269
+ .enum([
270
+ "investigation",
271
+ "challenge",
272
+ "artifacts",
273
+ "testing",
274
+ "verification",
275
+ "review",
276
+ "delegation",
277
+ "continuity",
278
+ "decisions",
279
+ ])
280
+ .nullable(),
281
+ before: z.enum(["dependent_action", "write", "close"]),
282
+ dependentAction: text.nullable(),
283
+ })
284
+ .strict()
285
+ .check((ctx) => {
286
+ const obligation = ctx.value;
287
+ const validMethod =
288
+ (obligation.kind === "method" && obligation.method !== null) ||
289
+ (obligation.kind !== "method" && obligation.method === null);
290
+ if (!validMethod)
291
+ ctx.issues.push({
292
+ code: "custom",
293
+ input: obligation,
294
+ message: "method obligations require a method; checks and decisions do not",
295
+ path: ["method"],
296
+ });
297
+ }),
298
+ ),
299
+ })
300
+ .strict();
301
+ export type Constraint = z.infer<typeof constraintSchema>;
302
+ export const intentSchema = z
303
+ .object({ objective: text, scope: scopeSchema, authorityRefs: z.array(refSchema) })
304
+ .strict();
305
+ export type Intent = z.infer<typeof intentSchema>;
306
+ export const progressSchema = z
307
+ .object({
308
+ summary: text,
309
+ nextAction: text.nullable(),
310
+ blockers: z.array(
311
+ z.object({ reason: text, dependentAction: text, refs: z.array(refSchema) }).strict(),
312
+ ),
313
+ })
314
+ .strict();
315
+ export type Progress = z.infer<typeof progressSchema>;
316
+
317
+ export const dimensionSchema = z.enum([
318
+ "investigation",
319
+ "challenge",
320
+ "artifacts",
321
+ "testing",
322
+ "verification",
323
+ "review",
324
+ "delegation",
325
+ "continuity",
326
+ "decisions",
327
+ ]);
328
+ export type Dimension = z.infer<typeof dimensionSchema>;
329
+
330
+ export const policySchema = z
331
+ .object({
332
+ policyVersion: text.regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/),
333
+ inputDigest: digest,
334
+ requirements: z.array(
335
+ z
336
+ .object({
337
+ id: digest,
338
+ ruleId: nonEmpty,
339
+ dimension: dimensionSchema,
340
+ scope: scopeSchema,
341
+ reason: text,
342
+ satisfaction: text,
343
+ before: z.enum(["dependent_action", "write", "close"]),
344
+ dependentAction: text.nullable(),
345
+ acceptanceAllowed: z.boolean(),
346
+ })
347
+ .strict(),
348
+ ),
349
+ })
350
+ .strict();
351
+ export type Policy = z.infer<typeof policySchema>;
352
+ export const requirementSchema = policySchema.shape.requirements.element;
353
+ export type Requirement = z.infer<typeof requirementSchema>;
354
+ export const policyChangeSchema = z
355
+ .object({
356
+ recordedAt: utc,
357
+ fromInputDigest: nullableDigest,
358
+ toInputDigest: digest,
359
+ added: z.array(digest),
360
+ retired: z.array(digest),
361
+ reason: text,
362
+ })
363
+ .strict();
364
+ export type PolicyChange = z.infer<typeof policyChangeSchema>;
365
+
366
+ export const candidateSchema = z
367
+ .object({
368
+ id: digest,
369
+ scope: scopeSchema,
370
+ completeness: z.enum(["known", "uncertain"]),
371
+ files: z.array(
372
+ z
373
+ .object({
374
+ path: pathValue,
375
+ kind: z.enum(["file", "symlink", "absent"]),
376
+ digest: nullableDigest,
377
+ executable: z.boolean().nullable(),
378
+ })
379
+ .strict()
380
+ .check((ctx) => {
381
+ const file = ctx.value;
382
+ const valid =
383
+ file.kind === "absent"
384
+ ? file.digest === null && file.executable === null
385
+ : file.kind === "file"
386
+ ? file.digest !== null && typeof file.executable === "boolean"
387
+ : file.digest !== null && file.executable === null;
388
+ if (!valid)
389
+ ctx.issues.push({
390
+ code: "custom",
391
+ input: file,
392
+ message: "invalid file metadata",
393
+ path: [],
394
+ });
395
+ }),
396
+ ),
397
+ environment: z
398
+ .array(
399
+ z.object({ name: nonEmpty, value: text.nullable(), refs: z.array(refSchema) }).strict(),
400
+ )
401
+ .check((ctx) => {
402
+ if (new Set(ctx.value.map((value) => value.name)).size !== ctx.value.length)
403
+ ctx.issues.push({
404
+ code: "custom",
405
+ input: ctx.value,
406
+ message: "duplicate environment name",
407
+ path: [],
408
+ });
409
+ }),
410
+ head: text.nullable(),
411
+ })
412
+ .strict()
413
+ .check((ctx) => {
414
+ if (ctx.value.id !== candidateDigest(ctx.value))
415
+ ctx.issues.push({
416
+ code: "custom",
417
+ input: ctx.value,
418
+ message: "candidate identity does not match its content",
419
+ path: ["id"],
420
+ });
421
+ });
422
+ export type Candidate = z.infer<typeof candidateSchema>;
423
+ export const evidenceSchema = z
424
+ .object({
425
+ kind: z.enum(["check", "review", "investigation", "artifact"]),
426
+ claim: text,
427
+ requirementIds: z.array(digest),
428
+ beforeCandidateId: nullableDigest.optional(),
429
+ candidateId: nullableDigest.optional(),
430
+ result: z.enum(["passed", "failed", "missing", "skipped"]),
431
+ summary: text,
432
+ refs: z.array(refSchema),
433
+ exitCode: safeInteger.nullable(),
434
+ reviewContext: refSchema.nullable(),
435
+ })
436
+ .strict();
437
+ export type Evidence = z.infer<typeof evidenceSchema>;
438
+ export const evidenceEvaluationSchema = z
439
+ .object({
440
+ evidenceId: id,
441
+ status: z.enum(["passed", "failed", "missing", "skipped", "stale"]),
442
+ reason: text,
443
+ })
444
+ .strict();
445
+ export type EvidenceEvaluation = z.infer<typeof evidenceEvaluationSchema>;
446
+ export const decisionSchema = z
447
+ .object({
448
+ purpose: z.enum(["design", "action", "limitation", "preference"]),
449
+ binding: z
450
+ .object({
451
+ taskId: id,
452
+ workspaceId: id,
453
+ scope: scopeSchema,
454
+ presented: text,
455
+ approvedContent: text,
456
+ contentRefs: z.array(refSchema),
457
+ })
458
+ .strict(),
459
+ digest,
460
+ response: z.enum(["approved", "rejected"]),
461
+ requirementIds: z.array(digest),
462
+ revoked: z.object({ at: utc, reason: text }).strict().nullable(),
463
+ consumption: z
464
+ .object({
465
+ state: z.enum(["reserved", "consumed", "uncertain"]),
466
+ at: utc,
467
+ actionRef: refSchema,
468
+ })
469
+ .strict()
470
+ .nullable(),
471
+ })
472
+ .strict();
473
+ export type Decision = z.infer<typeof decisionSchema>;
474
+ export const findingSchema = z
475
+ .object({
476
+ claim: text,
477
+ consequence: text,
478
+ scope: scopeSchema,
479
+ candidateId: nullableDigest,
480
+ refs: z.array(refSchema),
481
+ disposition: z.enum(["open", "fixed", "dismissed", "deferred"]),
482
+ resolution: z
483
+ .object({ reason: text, evidenceIds: z.array(id), decisionIds: z.array(id) })
484
+ .strict()
485
+ .nullable(),
486
+ })
487
+ .strict();
488
+ export type Finding = z.infer<typeof findingSchema>;
489
+ export const assignmentSchema = z
490
+ .object({
491
+ role: z.enum(["investigator", "reviewer", "implementer"]),
492
+ objective: text,
493
+ scope: scopeSchema,
494
+ decisionIds: z.array(id),
495
+ requirementIds: z.array(digest),
496
+ candidateId: nullableDigest.optional(),
497
+ stoppingCondition: text,
498
+ })
499
+ .strict();
500
+ export type Assignment = z.infer<typeof assignmentSchema>;
501
+ export const workerReportSchema = z
502
+ .object({
503
+ outcome: z.enum(["completed", "failed", "cancelled"]),
504
+ summary: text,
505
+ evidenceIds: z.array(id),
506
+ findingIds: z.array(id),
507
+ })
508
+ .strict();
509
+ export type WorkerReport = z.infer<typeof workerReportSchema>;
510
+ export const workerSchema = z
511
+ .object({
512
+ assignment: assignmentSchema,
513
+ state: z.enum(["assigned", "running", "cancelling", "stopped", "unknown"]),
514
+ session: refSchema.nullable(),
515
+ report: workerReportSchema.nullable(),
516
+ })
517
+ .strict();
518
+ export type Worker = z.infer<typeof workerSchema>;
519
+ export const ownerSchema = z
520
+ .object({
521
+ taskId: id,
522
+ workerId: nullableId,
523
+ session: z.object({ kind: z.literal("host"), host: hostSchema, handle: nonEmpty }).strict(),
524
+ })
525
+ .strict();
526
+ export type Owner = z.infer<typeof ownerSchema>;
527
+ export const closureSchema = z
528
+ .object({
529
+ outcome: outcomeSchema,
530
+ at: utc,
531
+ summary: text,
532
+ decisionIds: z.array(id),
533
+ evidenceIds: z.array(id),
534
+ })
535
+ .strict();
536
+ export type Closure = z.infer<typeof closureSchema>;
537
+ export const actionProgressSchema = z
538
+ .object({
539
+ decisionId: id,
540
+ steps: z.array(nonEmpty),
541
+ completedSteps: z.array(nonEmpty),
542
+ })
543
+ .strict()
544
+ .check((ctx) => {
545
+ const { steps, completedSteps } = ctx.value;
546
+ const valid =
547
+ new Set(steps).size === steps.length &&
548
+ new Set(completedSteps).size === completedSteps.length &&
549
+ completedSteps.every((step, index) => steps[index] === step);
550
+ if (!valid)
551
+ ctx.issues.push({
552
+ code: "custom",
553
+ input: ctx.value,
554
+ message: "completed action steps must be a unique prefix of approved steps",
555
+ path: ["completedSteps"],
556
+ });
557
+ });
558
+ export type ActionProgress = z.infer<typeof actionProgressSchema>;
559
+ export const actionProgressListSchema = z.array(actionProgressSchema).check((ctx) => {
560
+ if (new Set(ctx.value.map((progress) => progress.decisionId)).size !== ctx.value.length)
561
+ ctx.issues.push({
562
+ code: "custom",
563
+ input: ctx.value,
564
+ message: "action progress decision ids must be unique",
565
+ path: ["decisionId"],
566
+ });
567
+ });
568
+ export const taskRecordSchema = z
569
+ .object({
570
+ schemaVersion: z.literal(1),
571
+ id,
572
+ workspaceId: id,
573
+ revision,
574
+ createdAt: utc,
575
+ updatedAt: utc,
576
+ origin: z.object({ workspaceId: id, taskId: id, exportDigest: digest }).strict().nullable(),
577
+ intent: entrySchema(intentSchema),
578
+ constraints: z.array(constraintSchema),
579
+ status: z.enum(["active", "paused", "closed"]),
580
+ closure: closureSchema.nullable(),
581
+ progress: progressSchema,
582
+ assessments: z.array(entrySchema(assessmentSchema)),
583
+ policy: policySchema.nullable(),
584
+ policyChanges: z.array(policyChangeSchema),
585
+ candidates: z.array(candidateSchema),
586
+ evidence: z.array(entrySchema(evidenceSchema)),
587
+ decisions: z.array(entrySchema(decisionSchema)),
588
+ actionProgress: actionProgressListSchema.optional(),
589
+ findings: z.array(entrySchema(findingSchema)),
590
+ workers: z.array(entrySchema(workerSchema)),
591
+ })
592
+ .strict();
593
+ export type TaskRecord = z.infer<typeof taskRecordSchema>;
594
+ export const workspaceRecordSchema = z
595
+ .object({
596
+ schemaVersion: z.literal(1),
597
+ id,
598
+ revision,
599
+ root: nonEmpty,
600
+ writer: z
601
+ .object({ state: z.enum(["held", "uncertain"]), owner: ownerSchema, acquiredAt: utc })
602
+ .strict()
603
+ .nullable(),
604
+ })
605
+ .strict();
606
+ export type WorkspaceRecord = z.infer<typeof workspaceRecordSchema>;
607
+ export const capabilitySchema = z
608
+ .object({
609
+ name: nonEmpty,
610
+ surface: nonEmpty,
611
+ assurance: assuranceSchema,
612
+ reason: text,
613
+ refs: z.array(refSchema),
614
+ })
615
+ .strict();
616
+ export type Capability = z.infer<typeof capabilitySchema>;
617
+ export const requirementEvaluationSchema = z
618
+ .object({
619
+ requirementId: digest,
620
+ status: z.enum(["satisfied", "unsatisfied", "accepted_limitation", "unavailable"]),
621
+ evidenceIds: z.array(id),
622
+ decisionIds: z.array(id),
623
+ reason: text,
624
+ })
625
+ .strict();
626
+ export type RequirementEvaluation = z.infer<typeof requirementEvaluationSchema>;
627
+ export const taskViewSchema = z
628
+ .object({
629
+ task: taskRecordSchema,
630
+ workspace: workspaceRecordSchema,
631
+ capabilities: z.array(capabilitySchema),
632
+ requirements: z.array(requirementEvaluationSchema),
633
+ evidence: z.array(evidenceEvaluationSchema),
634
+ })
635
+ .strict();
636
+ export type TaskView = z.infer<typeof taskViewSchema>;
637
+ export const taskSummarySchema = z
638
+ .object({
639
+ id,
640
+ revision,
641
+ workspaceRevision: revision,
642
+ objective: text,
643
+ status: z.enum(["active", "paused", "closed"]),
644
+ closure: closureSchema.nullable(),
645
+ progress: progressSchema,
646
+ policy: policySchema.nullable(),
647
+ requirements: z.array(requirementEvaluationSchema),
648
+ writer: workspaceRecordSchema.shape.writer,
649
+ })
650
+ .strict();
651
+ export type TaskSummary = z.infer<typeof taskSummarySchema>;
652
+ export const exportBundleSchema = z
653
+ .object({
654
+ schemaVersion: z.literal(1),
655
+ exportedAt: utc,
656
+ sourceWorkspaceId: id,
657
+ task: taskRecordSchema,
658
+ digest,
659
+ })
660
+ .strict();
661
+ export type ExportBundle = z.infer<typeof exportBundleSchema>;
662
+
663
+ export const callerSchema = z.object({ host: hostSchema, actor: text }).strict();
664
+ export type Caller = z.infer<typeof callerSchema>;
665
+
666
+ const base = { schemaVersion: z.literal(1) };
667
+ const revisions = {
668
+ expectedRevision: revision.optional(),
669
+ expectedWorkspaceRevision: revision.optional(),
670
+ };
671
+ // Omitted revisions default to the current records inside the engine; explicit
672
+ // values are still CAS-enforced. This keeps single-call flows friction-free
673
+ // without weakening concurrent-write protection.
674
+ const taskId = { taskId: id };
675
+ const operation = <T extends z.ZodRawShape>(shape: T) => z.object({ ...base, ...shape }).strict();
676
+ const taskOperations = {
677
+ start: operation({
678
+ action: z.literal("start"),
679
+ expectedWorkspaceRevision: revision.nullable().optional(),
680
+ intent: intentSchema,
681
+ }),
682
+ list: operation({ action: z.literal("list") }),
683
+ inspect: operation({
684
+ action: z.literal("inspect"),
685
+ ...taskId,
686
+ view: z.enum(["summary", "full"]),
687
+ }),
688
+ revise: operation({
689
+ action: z.literal("revise"),
690
+ ...taskId,
691
+ ...revisions,
692
+ expectedWorkspaceRevision: revision.optional(),
693
+ intent: intentSchema,
694
+ reason: text,
695
+ }),
696
+ progress: operation({
697
+ action: z.literal("progress"),
698
+ ...taskId,
699
+ expectedRevision: revision.optional(),
700
+ progress: progressSchema,
701
+ }),
702
+ pause: operation({
703
+ action: z.literal("pause"),
704
+ ...taskId,
705
+ ...revisions,
706
+ expectedWorkspaceRevision: revision.optional(),
707
+ reason: text,
708
+ }),
709
+ resume: operation({
710
+ action: z.literal("resume"),
711
+ ...taskId,
712
+ ...revisions,
713
+ expectedWorkspaceRevision: revision.optional(),
714
+ authorityRefs: z.array(refSchema),
715
+ }),
716
+ close: operation({
717
+ action: z.literal("close"),
718
+ ...taskId,
719
+ ...revisions,
720
+ expectedWorkspaceRevision: revision.optional(),
721
+ outcome: outcomeSchema,
722
+ summary: text,
723
+ decisionIds: z.array(id),
724
+ }),
725
+ };
726
+ const policyOperations = {
727
+ assess: operation({
728
+ action: z.literal("assess"),
729
+ ...taskId,
730
+ expectedRevision: revision.optional(),
731
+ assessment: assessmentSchema,
732
+ }),
733
+ preview: operation({ action: z.literal("preview"), ...taskId, assessment: assessmentSchema }),
734
+ explain: operation({ action: z.literal("explain"), ...taskId }),
735
+ };
736
+ const evidenceOperations = {
737
+ record: operation({
738
+ action: z.literal("record"),
739
+ ...taskId,
740
+ expectedRevision: revision.optional(),
741
+ evidence: evidenceSchema,
742
+ }),
743
+ };
744
+ const findingOperations = {
745
+ record: operation({
746
+ action: z.literal("record"),
747
+ ...taskId,
748
+ expectedRevision: revision.optional(),
749
+ claim: text,
750
+ consequence: text,
751
+ scope: scopeSchema,
752
+ candidateId: nullableDigest.optional(),
753
+ refs: z.array(refSchema),
754
+ }),
755
+ resolve: operation({
756
+ action: z.literal("resolve"),
757
+ ...taskId,
758
+ expectedRevision: revision.optional(),
759
+ findingId: id,
760
+ disposition: z.enum(["open", "fixed", "dismissed", "deferred"]),
761
+ reason: text,
762
+ evidenceIds: z.array(id),
763
+ decisionIds: z.array(id),
764
+ }),
765
+ };
766
+ const decisionOperations = {
767
+ record: operation({
768
+ action: z.literal("record"),
769
+ ...taskId,
770
+ expectedRevision: revision.optional(),
771
+ purpose: decisionSchema.shape.purpose,
772
+ binding: decisionSchema.shape.binding,
773
+ response: decisionSchema.shape.response,
774
+ requirementIds: z.array(digest),
775
+ }),
776
+ revoke: operation({
777
+ action: z.literal("revoke"),
778
+ ...taskId,
779
+ expectedRevision: revision.optional(),
780
+ decisionId: id,
781
+ reason: text,
782
+ }),
783
+ };
784
+ const workerOperations = {
785
+ assign: operation({
786
+ action: z.literal("assign"),
787
+ ...taskId,
788
+ ...revisions,
789
+ expectedWorkspaceRevision: revision.optional(),
790
+ assignment: assignmentSchema,
791
+ }),
792
+ report: operation({
793
+ action: z.literal("report"),
794
+ ...taskId,
795
+ expectedRevision: revision.optional(),
796
+ expectedWorkspaceRevision: revision.optional(),
797
+ workerId: id,
798
+ report: workerReportSchema,
799
+ }),
800
+ cancel: operation({
801
+ action: z.literal("cancel"),
802
+ ...taskId,
803
+ ...revisions,
804
+ expectedWorkspaceRevision: revision.optional(),
805
+ workerId: id,
806
+ reason: text,
807
+ }),
808
+ };
809
+ const writerOperations = {
810
+ acquire: operation({
811
+ action: z.literal("acquire"),
812
+ ...taskId,
813
+ expectedRevision: revision.optional(),
814
+ expectedWorkspaceRevision: revision.optional(),
815
+ workerId: nullableId.optional(),
816
+ }),
817
+ release: operation({
818
+ action: z.literal("release"),
819
+ ...taskId,
820
+ expectedRevision: revision.optional(),
821
+ expectedWorkspaceRevision: revision.optional(),
822
+ reason: text,
823
+ }),
824
+ };
825
+ const stateOperations = {
826
+ export: operation({ action: z.literal("export"), ...taskId }),
827
+ import: operation({
828
+ action: z.literal("import"),
829
+ expectedWorkspaceRevision: revision.nullable().optional(),
830
+ bundle: exportBundleSchema,
831
+ authorityRefs: z.array(refSchema),
832
+ }),
833
+ recover: operation({
834
+ action: z.literal("recover"),
835
+ taskId: id.optional(),
836
+ expectedWorkspaceRevision: revision.optional(),
837
+ target: z.enum(["task", "workspace"]),
838
+ expectedBytes: digest,
839
+ snapshotDigest: digest,
840
+ reason: text,
841
+ authorityRefs: z.array(refSchema),
842
+ }),
843
+ };
844
+
845
+ export const operationSchemas = {
846
+ task: z.discriminatedUnion("action", Object.values(taskOperations) as any),
847
+ policy: z.discriminatedUnion("action", Object.values(policyOperations) as any),
848
+ evidence: z.discriminatedUnion("action", Object.values(evidenceOperations) as any),
849
+ finding: z.discriminatedUnion("action", Object.values(findingOperations) as any),
850
+ decision: z.discriminatedUnion("action", Object.values(decisionOperations) as any),
851
+ worker: z.discriminatedUnion("action", Object.values(workerOperations) as any),
852
+ writer: z.discriminatedUnion("action", Object.values(writerOperations) as any),
853
+ state: z.discriminatedUnion("action", Object.values(stateOperations) as any),
854
+ } as const;
855
+ export type OperationRequest = z.infer<(typeof operationSchemas)[OperationFamily]>;
856
+ export type TaskStartRequest = z.infer<typeof taskOperations.start>;
857
+
858
+ const compiledOperationSchemas = Object.fromEntries(
859
+ OPERATION_FAMILIES.map((family) => [family, z.compile(operationSchemas[family])]),
860
+ ) as typeof operationSchemas;
861
+
862
+ export type Id = string;
863
+ export type Revision = string;
864
+ export type Digest = string;
865
+ export type Utc = string;
866
+
867
+ export type ErrorCode =
868
+ | "invalid_input"
869
+ | "unsupported_version"
870
+ | "not_found"
871
+ | "invalid_transition"
872
+ | "revision_conflict"
873
+ | "needs_input"
874
+ | "permission_denied"
875
+ | "capability_unavailable"
876
+ | "requirements_unsatisfied"
877
+ | "writer_conflict"
878
+ | "recovery_required"
879
+ | "storage_error"
880
+ | "external_outcome_unknown";
881
+ export type ErrorDetails = {
882
+ fields?: { path: string; reason: string }[];
883
+ taskId?: Id;
884
+ expectedRevision?: Revision;
885
+ actualRevision?: Revision;
886
+ expectedWorkspaceRevision?: Revision | null;
887
+ actualWorkspaceRevision?: Revision | null;
888
+ requirementIds?: Digest[];
889
+ capability?: string;
890
+ owner?: Owner;
891
+ path?: string;
892
+ operation?: string;
893
+ outcome?: "not_started" | "pending" | "unknown";
894
+ };
895
+ export type Result<T> =
896
+ | {
897
+ ok: true;
898
+ schemaVersion: 1;
899
+ revision: Revision | null;
900
+ workspaceRevision: Revision | null;
901
+ data: T;
902
+ }
903
+ | { ok: false; schemaVersion: 1; code: ErrorCode; error: string; details: ErrorDetails };
904
+
905
+ export const success = <T>(
906
+ revisionValue: Revision | null,
907
+ workspaceRevisionValue: Revision | null,
908
+ data: T,
909
+ ): Result<T> => ({
910
+ ok: true,
911
+ schemaVersion: 1,
912
+ revision: revisionValue,
913
+ workspaceRevision: workspaceRevisionValue,
914
+ data,
915
+ });
916
+ export const failure = (
917
+ code: ErrorCode,
918
+ error: string,
919
+ details: ErrorDetails = {},
920
+ ): Result<never> => ({ ok: false, schemaVersion: 1, code, error, details });
921
+
922
+ export function parseOperation(family: OperationFamily, input: unknown): Result<OperationRequest> {
923
+ if (
924
+ typeof input === "object" &&
925
+ input !== null &&
926
+ "schemaVersion" in input &&
927
+ (input as { schemaVersion?: unknown }).schemaVersion !== SCHEMA_VERSION
928
+ )
929
+ return failure("unsupported_version", "unsupported schema version", { operation: family });
930
+ const compiled = compiledOperationSchemas[family].safeParse(input);
931
+ const parsed = compiled.success ? operationSchemas[family].safeParse(input) : compiled;
932
+ if (parsed.success) return success(null, null, parsed.data as OperationRequest);
933
+ const fields = parsed.error.issues.map((issue) => ({
934
+ path: issue.code === "unrecognized_keys" ? issue.keys.join(".") : issue.path.join("."),
935
+ reason: issue.message,
936
+ }));
937
+ return failure("invalid_input", "operation input is invalid", { fields });
938
+ }
939
+
940
+ export function operationJsonSchema(family: OperationFamily): z.core.JSONSchema.BaseSchema {
941
+ return z.toJSONSchema(operationSchemas[family], { target: "draft-2020-12" });
942
+ }
943
+
944
+ /**
945
+ * Advertised object depth shared by every host projection. Runtime validation
946
+ * stays full-depth in parseOperation; this bound only keeps provider tool
947
+ * schemas (e.g. Pi, MCP) within model nesting limits.
948
+ */
949
+ export const OPERATION_SCHEMA_DEPTH = 1;
950
+
951
+ /** Provider nesting limit every advertised Workit schema must stay within. */
952
+ export const OPERATION_SCHEMA_MAX_DEPTH = 10;
953
+
954
+ export const canonicalFieldsDescription = (fields: string[]): string =>
955
+ `Canonical object fields: ${fields.join(", ")}. Workit validates the complete nested value.`;
956
+
957
+ /**
958
+ * Provider-safe projection of an operation JSON schema. Objects deeper than
959
+ * maxDepth collapse to a described generic object that still names the
960
+ * canonical fields, so models know what to send while providers accept the
961
+ * shape. The full contract in operationJsonSchema is unchanged.
962
+ *
963
+ * With stringifiedObjects, every object, array, and primitive-const node also
964
+ * accepts its JSON-encoded string form. Pi's transport stringifies nested
965
+ * params before its own validation runs, so Pi publishes the tolerant shape
966
+ * and decodes after validation; transports that carry real JSON stay strict.
967
+ */
968
+ export function boundedOperationJsonSchema(
969
+ family: OperationFamily,
970
+ maxDepth: number = OPERATION_SCHEMA_DEPTH,
971
+ stringifiedObjects = false,
972
+ ): Record<string, unknown> {
973
+ const tolerate = (projected: Record<string, unknown>): Record<string, unknown> => {
974
+ if (!stringifiedObjects) return projected;
975
+ if (projected.properties && typeof projected.properties === "object")
976
+ return { ...projected, type: ["object", "string"] };
977
+ if (projected.items) return { ...projected, type: ["array", "string"] };
978
+ if (
979
+ "const" in projected &&
980
+ (typeof projected.const === "number" || typeof projected.const === "boolean")
981
+ )
982
+ return { anyOf: [projected, { const: String(projected.const) }] };
983
+ if (projected.type === "object") return { ...projected, type: ["object", "string"] };
984
+ if (projected.type === "array") return { ...projected, type: ["array", "string"] };
985
+ return projected;
986
+ };
987
+ const collapse = (node: unknown, currentDepth: number): unknown => {
988
+ if (Array.isArray(node)) return node.map((item) => collapse(item, currentDepth));
989
+ if (typeof node !== "object" || node === null) return node;
990
+ const record = node as Record<string, unknown>;
991
+ if (record.properties && typeof record.properties === "object") {
992
+ const fields = Object.keys(record.properties as Record<string, unknown>);
993
+ if (currentDepth >= maxDepth || fields.length === 0) {
994
+ const description = stringifiedObjects
995
+ ? `${canonicalFieldsDescription(fields)} A JSON-encoded string is also accepted.`
996
+ : canonicalFieldsDescription(fields);
997
+ return tolerate({ type: "object", description });
998
+ }
999
+ return tolerate({
1000
+ ...record,
1001
+ properties: Object.fromEntries(
1002
+ Object.entries(record.properties as Record<string, unknown>).map(([key, child]) => [
1003
+ key,
1004
+ collapse(child, currentDepth + 1),
1005
+ ]),
1006
+ ),
1007
+ });
1008
+ }
1009
+ if (record.items) return tolerate({ ...record, items: collapse(record.items, currentDepth) });
1010
+ const composed = (["anyOf", "oneOf", "allOf"] as const).filter((key) =>
1011
+ Array.isArray(record[key]),
1012
+ );
1013
+ if (composed.length > 0) {
1014
+ const projected: Record<string, unknown> = { ...record };
1015
+ for (const key of composed)
1016
+ projected[key] = (record[key] as unknown[]).map((item) => collapse(item, currentDepth));
1017
+ return projected;
1018
+ }
1019
+ if ("const" in record) return tolerate({ ...record });
1020
+ return node;
1021
+ };
1022
+ return collapse(operationJsonSchema(family), 0) as Record<string, unknown>;
1023
+ }
1024
+
1025
+ type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
1026
+ function canonical(value: unknown, seen: Set<object>): JsonValue {
1027
+ if (value === null || typeof value === "boolean" || typeof value === "string") {
1028
+ if (typeof value === "string" && invalidUnicode(value)) throw new TypeError("invalid Unicode");
1029
+ return value;
1030
+ }
1031
+ if (typeof value === "number") {
1032
+ if (!Number.isSafeInteger(value)) throw new TypeError("number must be a finite safe integer");
1033
+ return value;
1034
+ }
1035
+ if (typeof value !== "object") throw new TypeError("value is not JSON serializable");
1036
+ if (seen.has(value)) throw new TypeError("cyclic value");
1037
+ seen.add(value);
1038
+ let result: JsonValue;
1039
+ if (Array.isArray(value)) {
1040
+ for (let index = 0; index < value.length; index += 1) {
1041
+ if (!(index in value)) throw new TypeError("sparse array");
1042
+ }
1043
+ result = value.map((item) => canonical(item, seen));
1044
+ } else {
1045
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
1046
+ throw new TypeError("value is not a JSON object");
1047
+ if (Reflect.ownKeys(value).some((key) => typeof key !== "string"))
1048
+ throw new TypeError("symbol key is not JSON");
1049
+ if (Object.keys(value).some((key) => invalidUnicode(key)))
1050
+ throw new TypeError("invalid Unicode");
1051
+ result = Object.fromEntries(
1052
+ Object.keys(value)
1053
+ .sort()
1054
+ .map((key) => [key, canonical((value as Record<string, unknown>)[key], seen)]),
1055
+ );
1056
+ }
1057
+ seen.delete(value);
1058
+ return result;
1059
+ }
1060
+ export function canonicalJson(value: unknown): string {
1061
+ return JSON.stringify(canonical(value, new Set()));
1062
+ }
1063
+ export function sha256(value: unknown): Digest {
1064
+ if (typeof value === "string" && invalidUnicode(value)) throw new TypeError("invalid Unicode");
1065
+ return createHash("sha256")
1066
+ .update(typeof value === "string" ? value : canonicalJson(value), "utf8")
1067
+ .digest("hex");
1068
+ }
1069
+ export function newId(): Id {
1070
+ return randomUUID().toLowerCase();
1071
+ }
1072
+ export function newRevision(): Revision {
1073
+ return randomUUID().toLowerCase();
1074
+ }
1075
+ const compareCodeUnits = (left: string, right: string): number => {
1076
+ const length = Math.min(left.length, right.length);
1077
+ for (let index = 0; index < length; index += 1) {
1078
+ const difference = left.charCodeAt(index) - right.charCodeAt(index);
1079
+ if (difference !== 0) return difference;
1080
+ }
1081
+ return left.length - right.length;
1082
+ };
1083
+ const compareNullableText = (left: string | null, right: string | null): number =>
1084
+ left === right ? 0 : left === null ? -1 : right === null ? 1 : compareCodeUnits(left, right);
1085
+ const compareBooleanNullable = (left: boolean | null, right: boolean | null): number =>
1086
+ left === right ? 0 : left === null ? -1 : right === null ? 1 : Number(left) - Number(right);
1087
+ export function decisionDigest(input: Omit<Decision, "digest"> | Decision): Digest {
1088
+ const {
1089
+ digest: _ignored,
1090
+ revoked: _revoked,
1091
+ consumption: _consumption,
1092
+ ...value
1093
+ } = input as Decision;
1094
+ return sha256(value);
1095
+ }
1096
+ export function candidateDigest(input: Candidate): Digest {
1097
+ const normalizedScope = {
1098
+ description: input.scope.description,
1099
+ paths: [...input.scope.paths].sort(compareCodeUnits),
1100
+ exclusions: [...input.scope.exclusions].sort(compareCodeUnits),
1101
+ };
1102
+ return sha256({
1103
+ scope: normalizedScope,
1104
+ completeness: input.completeness,
1105
+ files: [...input.files].sort(
1106
+ (left, right) =>
1107
+ compareCodeUnits(left.path, right.path) ||
1108
+ compareCodeUnits(left.kind, right.kind) ||
1109
+ compareNullableText(left.digest, right.digest) ||
1110
+ compareBooleanNullable(left.executable, right.executable),
1111
+ ),
1112
+ environment: input.environment
1113
+ .map(({ name, value }) => ({ name, value }))
1114
+ .sort(
1115
+ (left, right) =>
1116
+ compareCodeUnits(left.name, right.name) || compareNullableText(left.value, right.value),
1117
+ ),
1118
+ head: input.head,
1119
+ });
1120
+ }
1121
+ export function requirementId(input: {
1122
+ ruleId: string;
1123
+ scope: Scope;
1124
+ satisfaction: string;
1125
+ before: string;
1126
+ dependentAction: string | null;
1127
+ }): Digest {
1128
+ return sha256({
1129
+ ...input,
1130
+ scope: {
1131
+ description: input.scope.description,
1132
+ paths: [...input.scope.paths].sort(compareCodeUnits),
1133
+ exclusions: [...input.scope.exclusions].sort(compareCodeUnits),
1134
+ },
1135
+ });
1136
+ }