@brainervirus/workit-core 0.10.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.
- package/README.md +5 -3
- package/package.json +8 -3
- package/scripts/analyze-release-scope.ts +4 -1
- package/scripts/doctor-check.ts +3 -1
- package/scripts/install-codex-plugin.sh +28 -0
- package/scripts/install-cursor-plugin.sh +1 -0
- package/scripts/install-opencode-plugin.sh +1 -0
- package/scripts/install-pi-package.sh +56 -0
- package/scripts/rewrite-workspace-deps.ts +22 -5
- package/scripts/sync-release-manifests.ts +8 -5
- package/scripts/sync-runtime.sh +7 -3
- package/scripts/validate-cursor-marketplace.ts +16 -62
- package/skills/workit-babysit/SKILL.md +33 -0
- package/skills/workit-behavioral-tdd/SKILL.md +53 -0
- package/skills/workit-blast-radius/SKILL.md +31 -0
- package/skills/workit-challenge/SKILL.md +62 -0
- package/skills/workit-debug/SKILL.md +61 -0
- package/skills/workit-deslop/SKILL.md +36 -0
- package/skills/workit-diagram/SKILL.md +32 -0
- package/skills/workit-green-run/SKILL.md +29 -0
- package/skills/workit-handoff/SKILL.md +43 -0
- package/skills/workit-implement/SKILL.md +46 -0
- package/skills/workit-mockup/SKILL.md +28 -0
- package/skills/workit-plan/SKILL.md +66 -0
- package/skills/workit-review/SKILL.md +60 -0
- package/skills/workit-steer/SKILL.md +32 -0
- package/src/core/authority.ts +918 -0
- package/src/core/boundary.ts +29 -0
- package/src/core/branch.ts +26 -255
- package/src/core/changelog.ts +95 -18
- package/src/core/commit-flavors.ts +65 -0
- package/src/core/config-conversion.ts +183 -0
- package/src/core/config.ts +42 -0
- package/src/core/cutover.ts +721 -0
- package/src/core/detect-hosts.ts +107 -0
- package/src/core/doctor.ts +428 -18
- package/src/core/external-action-effects.ts +1171 -0
- package/src/core/external-action.ts +541 -0
- package/src/core/init.ts +83 -32
- package/src/core/methods.ts +125 -0
- package/src/core/policy-resolver.ts +523 -0
- package/src/core/pr-create.ts +7 -1
- package/src/core/registration.ts +69 -4
- package/src/core/repo-context.ts +59 -1
- package/src/core/setup.ts +28 -0
- package/src/core/skill-manifests.ts +40 -42
- package/src/core/support-matrix.ts +8 -3
- package/src/core/sync-runtime.ts +15 -3
- package/src/core/task-context.ts +179 -0
- package/src/core/task-contract.ts +1136 -0
- package/src/core/task-engine.ts +2562 -0
- package/src/core/task-evaluation.ts +679 -0
- package/src/core/task-store.ts +1071 -0
- package/src/core/tracker-issues.ts +237 -0
- package/src/core/uninstall.ts +67 -3
- package/src/core/vcs-config.ts +53 -22
- package/src/core/workers.ts +166 -0
- package/src/core/workspaces.ts +3 -1
- package/src/core/youtrack-tools.ts +2 -0
- package/src/core/youtrack.ts +145 -10
- package/src/core.ts +126 -0
- package/templates/execution-contract.md +17 -50
- package/templates/plan-template.md +3 -3
- package/templates/spec-template.md +15 -3
- package/templates/workit-contract.md +12 -0
- package/commands/wk-changelog.md +0 -2
- package/commands/wk-commit.md +0 -2
- package/commands/wk-docs-refresh.md +0 -2
- package/commands/wk-handoff.md +0 -2
- package/commands/wk-implement.md +0 -2
- package/commands/wk-init.md +0 -2
- package/commands/wk-issue-update.md +0 -2
- package/commands/wk-meetings.md +0 -2
- package/commands/wk-pr.md +0 -2
- package/commands/wk-release-notes.md +0 -2
- package/commands/wk-status.md +0 -2
- package/commands/wk-verify.md +0 -2
- package/scripts/update-superpowers.sh +0 -82
- package/scripts/vendor-assets.ts +0 -37
- package/skills/wk-changelog/SKILL.md +0 -15
- package/skills/wk-commit/SKILL.md +0 -16
- package/skills/wk-docs-refresh/SKILL.md +0 -15
- package/skills/wk-handoff/SKILL.md +0 -20
- package/skills/wk-implement/SKILL.md +0 -47
- package/skills/wk-init/SKILL.md +0 -31
- package/skills/wk-issue-update/SKILL.md +0 -27
- package/skills/wk-issue-update/references/youtrack-update-style.md +0 -81
- package/skills/wk-meetings/SKILL.md +0 -17
- package/skills/wk-pr/SKILL.md +0 -27
- package/skills/wk-release-notes/SKILL.md +0 -15
- package/skills/wk-status/SKILL.md +0 -16
- package/skills/wk-verify/SKILL.md +0 -16
- package/src/core/detector.ts +0 -239
- package/src/core/flow-state.ts +0 -3048
- package/src/core/handoff-context.ts +0 -136
- package/src/core/handoff-tools.ts +0 -133
- package/src/core/menu.ts +0 -70
- package/src/core/plan-tasks.ts +0 -43
- package/src/core/reminder.ts +0 -124
- package/src/core/sdd.ts +0 -353
- package/src/state.ts +0 -22
- package/templates/superpowers-doc-contract.md +0 -75
- package/vendor/superpowers/skills/brainstorming/SKILL.md +0 -159
- package/vendor/superpowers/skills/brainstorming/scripts/frame-template.html +0 -213
- package/vendor/superpowers/skills/brainstorming/scripts/helper.js +0 -167
- package/vendor/superpowers/skills/brainstorming/scripts/server.cjs +0 -723
- package/vendor/superpowers/skills/brainstorming/scripts/start-server.sh +0 -209
- package/vendor/superpowers/skills/brainstorming/scripts/stop-server.sh +0 -120
- package/vendor/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +0 -49
- package/vendor/superpowers/skills/brainstorming/visual-companion.md +0 -291
- package/vendor/superpowers/skills/dispatching-parallel-agents/SKILL.md +0 -185
- package/vendor/superpowers/skills/executing-plans/SKILL.md +0 -70
- package/vendor/superpowers/skills/finishing-a-development-branch/SKILL.md +0 -241
- package/vendor/superpowers/skills/receiving-code-review/SKILL.md +0 -213
- package/vendor/superpowers/skills/requesting-code-review/SKILL.md +0 -103
- package/vendor/superpowers/skills/requesting-code-review/code-reviewer.md +0 -172
- package/vendor/superpowers/skills/subagent-driven-development/SKILL.md +0 -426
- package/vendor/superpowers/skills/subagent-driven-development/implementer-prompt.md +0 -139
- package/vendor/superpowers/skills/subagent-driven-development/scripts/review-package +0 -44
- package/vendor/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +0 -22
- package/vendor/superpowers/skills/subagent-driven-development/scripts/task-brief +0 -40
- package/vendor/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +0 -188
- package/vendor/superpowers/skills/systematic-debugging/CREATION-LOG.md +0 -119
- package/vendor/superpowers/skills/systematic-debugging/SKILL.md +0 -296
- package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +0 -158
- package/vendor/superpowers/skills/systematic-debugging/condition-based-waiting.md +0 -115
- package/vendor/superpowers/skills/systematic-debugging/defense-in-depth.md +0 -122
- package/vendor/superpowers/skills/systematic-debugging/find-polluter.sh +0 -63
- package/vendor/superpowers/skills/systematic-debugging/root-cause-tracing.md +0 -169
- package/vendor/superpowers/skills/systematic-debugging/test-academic.md +0 -14
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-1.md +0 -58
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-2.md +0 -68
- package/vendor/superpowers/skills/systematic-debugging/test-pressure-3.md +0 -69
- package/vendor/superpowers/skills/test-driven-development/SKILL.md +0 -371
- package/vendor/superpowers/skills/test-driven-development/testing-anti-patterns.md +0 -299
- package/vendor/superpowers/skills/using-git-worktrees/SKILL.md +0 -202
- package/vendor/superpowers/skills/using-superpowers/SKILL.md +0 -62
- package/vendor/superpowers/skills/using-superpowers/references/antigravity-tools.md +0 -23
- package/vendor/superpowers/skills/using-superpowers/references/codex-tools.md +0 -39
- package/vendor/superpowers/skills/using-superpowers/references/pi-tools.md +0 -16
- package/vendor/superpowers/skills/verification-before-completion/SKILL.md +0 -139
- package/vendor/superpowers/skills/writing-plans/SKILL.md +0 -174
- package/vendor/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
- package/vendor/superpowers/skills/writing-skills/SKILL.md +0 -689
- package/vendor/superpowers/skills/writing-skills/anthropic-best-practices.md +0 -1150
- package/vendor/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +0 -189
- package/vendor/superpowers/skills/writing-skills/graphviz-conventions.dot +0 -172
- package/vendor/superpowers/skills/writing-skills/persuasion-principles.md +0 -187
- package/vendor/superpowers/skills/writing-skills/render-graphs.js +0 -168
- 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
|
+
}
|