@brainervirus/workit-core 6.0.1 → 7.1.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.
@@ -267,96 +267,27 @@ export const entrySchema = <T extends z.ZodType>(data: T) =>
267
267
  z.object({ id, recordedAt: utc, provenance: provenanceSchema, data }).strict();
268
268
  export type Entry<T> = { id: Id; recordedAt: Utc; provenance: Provenance; data: T };
269
269
 
270
- export const factSchema = z
271
- .object({
272
- statement: text,
273
- basis: z.enum(["observed", "inferred", "unknown"]),
274
- refs: z.array(refSchema),
275
- })
276
- .strict()
277
- .check((ctx) => {
278
- if (ctx.value.basis === "observed" && ctx.value.refs.length === 0)
279
- ctx.issues.push({
280
- code: "custom",
281
- input: ctx.value,
282
- message: "observed facts require supporting references",
283
- path: ["refs"],
284
- });
285
- });
286
- export type Fact = z.infer<typeof factSchema>;
287
- export const signalSchema = z
270
+ /** Risk tiers an agent judges; aliases (low, medium, …) are mapped on input. */
271
+ export const RISK_TIERS = ["trivial", "normal", "high"] as const;
272
+ export type RiskTier = (typeof RISK_TIERS)[number];
273
+ /**
274
+ * The agent's policy judgment (S17): four flat calls plus an optional note
275
+ * and supporting refs. Everything else is derived deterministically
276
+ * (core/policy/derive.ts).
277
+ */
278
+ export const judgmentSchema = z
288
279
  .object({
289
- value: z.union([z.boolean(), z.literal("unknown")]),
290
- basis: z.enum(["observed", "inferred", "unknown"]),
291
- reason: text,
280
+ riskTier: z.enum(RISK_TIERS),
281
+ behaviorChange: z.boolean(),
282
+ productChoiceOpen: z.boolean(),
283
+ needsPlan: z.boolean(),
284
+ note: text.nullable(),
292
285
  refs: z.array(refSchema),
293
286
  })
294
- .strict()
295
- .check((ctx) => {
296
- const signal = ctx.value;
297
- const valid =
298
- signal.value === "unknown"
299
- ? signal.basis === "unknown"
300
- : signal.basis === "observed" || signal.basis === "inferred";
301
- if (!valid)
302
- ctx.issues.push({
303
- code: "custom",
304
- input: signal,
305
- message: "signal value and basis disagree",
306
- path: [],
307
- });
308
- if (signal.basis === "observed" && signal.refs.length === 0)
309
- ctx.issues.push({
310
- code: "custom",
311
- input: signal,
312
- message: "observed signals require supporting references",
313
- path: ["refs"],
314
- });
315
- });
316
- export type Signal = z.infer<typeof signalSchema>;
317
- const signalSet = z
318
- .object({
319
- approachUnknown: signalSchema,
320
- productChoiceOpen: signalSchema,
321
- behaviorChange: signalSchema,
322
- mechanicalLowRisk: signalSchema,
323
- durableAgreementNeeded: signalSchema,
324
- coordinationPlanNeeded: signalSchema,
325
- helperUseful: signalSchema,
326
- testFirstPractical: signalSchema,
327
- })
328
- .strict();
329
- const consequenceSchema = z
330
- .object({
331
- area: z.enum([
332
- "behavior",
333
- "public_contract",
334
- "data",
335
- "security",
336
- "operations",
337
- "reversibility",
338
- "consumers",
339
- ]),
340
- fact: factSchema,
341
- })
342
- .strict();
343
- const verificationSchema = z
344
- .object({
345
- claim: text,
346
- scope: scopeSchema,
347
- availableChecks: z.array(refSchema),
348
- gaps: z.array(text),
349
- })
350
287
  .strict();
351
- export const assessmentSchema = z
352
- .object({
353
- facts: z.array(factSchema),
354
- signals: signalSet,
355
- consequences: z.array(consequenceSchema),
356
- verification: z.array(verificationSchema),
357
- })
358
- .strict();
359
- export type Assessment = z.infer<typeof assessmentSchema>;
288
+ export type Judgment = z.infer<typeof judgmentSchema>;
289
+ /** A ≤6.x assessment as stored (facts/signals/…): kept verbatim, never written. */
290
+ export const legacyAssessmentSchema = z.looseObject({});
360
291
 
361
292
  export const constraintSchema = z
362
293
  .object({
@@ -714,7 +645,10 @@ export const taskRecordSchema = z
714
645
  progress: progressSchema,
715
646
  pauseReason: text.nullable().optional(),
716
647
  runtime: runtimeSchema.optional(),
717
- assessments: z.array(entrySchema(assessmentSchema)),
648
+ /** ≤6.x assessments, read-only history; judgments replace them. */
649
+ assessments: z.array(entrySchema(legacyAssessmentSchema)),
650
+ /** Listed in `critical` once written, so an older reader fails closed. */
651
+ judgments: z.array(entrySchema(judgmentSchema)).optional(),
718
652
  policy: policySchema.nullable(),
719
653
  policyChanges: z.array(policyChangeSchema),
720
654
  candidates: z.array(candidateSchema),
@@ -969,14 +903,16 @@ const taskOperations = {
969
903
  decisionIds: z.array(id).optional(),
970
904
  }),
971
905
  };
906
+ /** Judgment values as sent; core/policy/judgment.ts maps aliases and validates. */
907
+ const rawJudgment = z.record(z.string(), z.unknown());
972
908
  const policyOperations = {
973
909
  assess: operation({
974
910
  action: z.literal("assess"),
975
911
  ...taskId,
976
912
  expectedRevision: revision.optional(),
977
- assessment: assessmentSchema,
913
+ judgment: rawJudgment,
978
914
  }),
979
- preview: operation({ action: z.literal("preview"), ...taskId, assessment: assessmentSchema }),
915
+ preview: operation({ action: z.literal("preview"), ...taskId, judgment: rawJudgment }),
980
916
  explain: operation({ action: z.literal("explain"), ...taskId }),
981
917
  };
982
918
  const evidenceOperations = {
@@ -1015,7 +951,11 @@ const decisionOperations = {
1015
951
  ...taskId,
1016
952
  expectedRevision: revision.optional(),
1017
953
  purpose: decisionSchema.shape.purpose,
1018
- binding: decisionSchema.shape.binding,
954
+ // The engine binds the decision to the task and workspace it records on.
955
+ binding: decisionSchema.shape.binding.extend({
956
+ taskId: id.optional(),
957
+ workspaceId: id.optional(),
958
+ }),
1019
959
  response: decisionSchema.shape.response,
1020
960
  requirementIds: z.array(digest),
1021
961
  }),
@@ -1071,6 +1011,16 @@ export const operationSchemas = {
1071
1011
  state: z.discriminatedUnion("action", Object.values(stateOperations) as any),
1072
1012
  } as const;
1073
1013
  export type OperationRequest = z.infer<(typeof operationSchemas)[OperationFamily]>;
1014
+ /** Every action of each family, in contract order: the one action table. */
1015
+ export const OPERATION_ACTIONS: Record<OperationFamily, readonly string[]> = {
1016
+ task: Object.keys(taskOperations),
1017
+ policy: Object.keys(policyOperations),
1018
+ evidence: Object.keys(evidenceOperations),
1019
+ finding: Object.keys(findingOperations),
1020
+ decision: Object.keys(decisionOperations),
1021
+ worker: Object.keys(workerOperations),
1022
+ state: Object.keys(stateOperations),
1023
+ };
1074
1024
 
1075
1025
  /**
1076
1026
  * Schemas advertised to hosts. Every `taskId` is optional there: an operation
@@ -1186,145 +1136,6 @@ export function parseOperation(family: OperationFamily, input: unknown): Result<
1186
1136
  return failure("invalid_input", "operation input is invalid", { fields });
1187
1137
  }
1188
1138
 
1189
- export function operationJsonSchema(family: OperationFamily): z.core.JSONSchema.BaseSchema {
1190
- return z.toJSONSchema(advertisedOperationSchemas[family], { target: "draft-2020-12" });
1191
- }
1192
-
1193
- /**
1194
- * Advertised object depth shared by every host projection. Runtime validation
1195
- * stays full-depth in parseOperation; this bound only keeps provider tool
1196
- * schemas (e.g. Pi, MCP) within model nesting limits.
1197
- */
1198
- export const OPERATION_SCHEMA_DEPTH = 1;
1199
-
1200
- /** Provider nesting limit every advertised Workit schema must stay within. */
1201
- export const OPERATION_SCHEMA_MAX_DEPTH = 10;
1202
-
1203
- const schemaShapeDescription = (value: unknown, depth = 0): string => {
1204
- if (!value || typeof value !== "object") return "value";
1205
- const schema = value as Record<string, unknown>;
1206
- if (Array.isArray(schema.oneOf) || Array.isArray(schema.anyOf)) {
1207
- const options = (Array.isArray(schema.oneOf) ? schema.oneOf : schema.anyOf) as unknown[];
1208
- return `(${options.map((option) => schemaShapeDescription(option, depth)).join("|")})`;
1209
- }
1210
- if (schema.const !== undefined) return `=${JSON.stringify(schema.const)}`;
1211
- if (Array.isArray(schema.enum)) return `[${schema.enum.map(String).join("|")}]`;
1212
- const type = Array.isArray(schema.type) ? schema.type.join("/") : schema.type;
1213
- if (type === "array") return `arr<${schemaShapeDescription(schema.items, depth)}>`;
1214
- if (type === "object" && schema.properties && typeof schema.properties === "object") {
1215
- if (depth >= 2) return "obj";
1216
- const required = new Set(Array.isArray(schema.required) ? schema.required : []);
1217
- const properties = schema.properties as Record<string, unknown>;
1218
- const fields = Object.entries(properties).map(
1219
- ([name, child]) =>
1220
- `${name}${required.has(name) ? "!" : ""}:${schemaShapeDescription(child, depth + 1)}`,
1221
- );
1222
- return `obj{${fields.join(",")}}`;
1223
- }
1224
- if (typeof type !== "string") return "?";
1225
- return type
1226
- .split("/")
1227
- .map((part) => ({ string: "str", boolean: "bool", number: "num", null: "null" })[part] ?? part)
1228
- .join("|");
1229
- };
1230
-
1231
- export const canonicalFieldsDescription = (
1232
- value: string[] | Record<string, unknown> | z.ZodType,
1233
- ): string => {
1234
- if (Array.isArray(value))
1235
- return `Canonical object fields: ${value.join(", ")}. Workit validates the complete nested value.`;
1236
- const isZodType = (schema: Record<string, unknown> | z.ZodType): schema is z.ZodType =>
1237
- "_zod" in schema && typeof schema._zod === "object";
1238
- const schema = isZodType(value)
1239
- ? (z.toJSONSchema(value, { target: "draft-2020-12" }) as Record<string, unknown>)
1240
- : value;
1241
- if (!schema.properties || typeof schema.properties !== "object") {
1242
- const variants = Array.isArray(schema.oneOf)
1243
- ? schema.oneOf
1244
- : Array.isArray(schema.anyOf)
1245
- ? schema.anyOf
1246
- : null;
1247
- return variants
1248
- ? `Canonical shapes: ${variants.map((variant) => schemaShapeDescription(variant)).join(" | ")}. Full nested value is validated.`
1249
- : "Canonical nested value; Workit validates the complete value.";
1250
- }
1251
- const properties = schema.properties as Record<string, unknown>;
1252
- const required = new Set(Array.isArray(schema.required) ? schema.required : []);
1253
- const fields = Object.entries(properties).map(
1254
- ([name, child]) => `${name}${required.has(name) ? "!" : ""}:${schemaShapeDescription(child)}`,
1255
- );
1256
- return `Fields (! required): ${fields.join(", ")}. Full nested value is validated.`;
1257
- };
1258
-
1259
- /**
1260
- * Provider-safe projection of an operation JSON schema. Objects deeper than
1261
- * maxDepth collapse to a described generic object that still names the
1262
- * canonical fields, so models know what to send while providers accept the
1263
- * shape. The full contract in operationJsonSchema is unchanged.
1264
- *
1265
- * With stringifiedObjects, every object, array, and primitive-const node also
1266
- * accepts its JSON-encoded string form. Pi's transport stringifies nested
1267
- * params before its own validation runs, so Pi publishes the tolerant shape
1268
- * and decodes after validation; transports that carry real JSON stay strict.
1269
- */
1270
- export function boundedOperationJsonSchema(
1271
- family: OperationFamily,
1272
- maxDepth: number = OPERATION_SCHEMA_DEPTH,
1273
- stringifiedObjects = false,
1274
- ): Record<string, unknown> {
1275
- const tolerate = (projected: Record<string, unknown>): Record<string, unknown> => {
1276
- if (!stringifiedObjects) return projected;
1277
- if (projected.properties && typeof projected.properties === "object")
1278
- return { ...projected, type: ["object", "string"] };
1279
- if (projected.items) return { ...projected, type: ["array", "string"] };
1280
- if (
1281
- "const" in projected &&
1282
- (typeof projected.const === "number" || typeof projected.const === "boolean")
1283
- )
1284
- return { anyOf: [projected, { const: String(projected.const) }] };
1285
- if (projected.type === "object") return { ...projected, type: ["object", "string"] };
1286
- if (projected.type === "array") return { ...projected, type: ["array", "string"] };
1287
- return projected;
1288
- };
1289
- const collapse = (node: unknown, currentDepth: number): unknown => {
1290
- if (Array.isArray(node)) return node.map((item) => collapse(item, currentDepth));
1291
- if (typeof node !== "object" || node === null) return node;
1292
- const record = node as Record<string, unknown>;
1293
- if (record.properties && typeof record.properties === "object") {
1294
- const properties = record.properties as Record<string, unknown>;
1295
- const fields = Object.keys(properties);
1296
- if (currentDepth >= maxDepth || fields.length === 0) {
1297
- const description = stringifiedObjects
1298
- ? `${canonicalFieldsDescription(record)} A JSON-encoded string is also accepted.`
1299
- : canonicalFieldsDescription(record);
1300
- return tolerate({ type: "object", description });
1301
- }
1302
- return tolerate({
1303
- ...record,
1304
- properties: Object.fromEntries(
1305
- Object.entries(record.properties as Record<string, unknown>).map(([key, child]) => [
1306
- key,
1307
- collapse(child, currentDepth + 1),
1308
- ]),
1309
- ),
1310
- });
1311
- }
1312
- if (record.items) return tolerate({ ...record, items: collapse(record.items, currentDepth) });
1313
- const composed = (["anyOf", "oneOf", "allOf"] as const).filter((key) =>
1314
- Array.isArray(record[key]),
1315
- );
1316
- if (composed.length > 0) {
1317
- const projected: Record<string, unknown> = { ...record };
1318
- for (const key of composed)
1319
- projected[key] = (record[key] as unknown[]).map((item) => collapse(item, currentDepth));
1320
- return projected;
1321
- }
1322
- if ("const" in record) return tolerate({ ...record });
1323
- return node;
1324
- };
1325
- return collapse(operationJsonSchema(family), 0) as Record<string, unknown>;
1326
- }
1327
-
1328
1139
  type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
1329
1140
  function canonical(value: unknown, seen: Set<object>): JsonValue {
1330
1141
  if (value === null || typeof value === "boolean" || typeof value === "string") {
@@ -7,16 +7,16 @@ import {
7
7
  failure,
8
8
  decisionDigest,
9
9
  exportBundleSchema,
10
- assessmentSchema,
11
10
  decisionSchema,
12
11
  evidenceSchema,
13
12
  findingSchema,
14
13
  intentSchema,
14
+ judgmentSchema,
15
+ legacyAssessmentSchema,
15
16
  newId,
16
17
  parseOperation,
17
18
  rewriteRecordRefs,
18
19
  success,
19
- type Assessment,
20
20
  type Candidate,
21
21
  type Capability,
22
22
  type Caller,
@@ -27,6 +27,7 @@ import {
27
27
  type Evidence,
28
28
  type ExportBundle,
29
29
  type Finding,
30
+ type Judgment,
30
31
  type Policy,
31
32
  provenanceSchema,
32
33
  type Ref,
@@ -45,6 +46,11 @@ import {
45
46
  scopeCovers as bindingCovers,
46
47
  } from "./task-contract";
47
48
  import { diffPolicy, resolvePolicy } from "./policy-resolver";
49
+ import { normalizeOperationInput } from "./operation-input";
50
+ import { latestJudgment } from "./policy/derive";
51
+ import { normalizeJudgment } from "./policy/judgment";
52
+ import { resolveAutonomy, type VerificationMode } from "../autonomy";
53
+
48
54
  import { sameDirectoryIdentity, TaskStore } from "./task-store";
49
55
  import { checkoutRootOf } from "../store/paths";
50
56
  import { defaultLockTimeout } from "./store-lock";
@@ -76,6 +82,15 @@ import {
76
82
  type CandidateEnvironment,
77
83
  } from "./task-evaluation";
78
84
 
85
+ /** The user-config verification mode for `root`; `self` when it cannot be read. */
86
+ const workspaceVerification = (root: string): VerificationMode => {
87
+ try {
88
+ return resolveAutonomy(root).verification;
89
+ } catch {
90
+ return "self";
91
+ }
92
+ };
93
+
79
94
  export type OperationContext = {
80
95
  root: string;
81
96
  caller: Caller;
@@ -429,29 +444,8 @@ const mapRef = (ref: Ref, ids: Map<string, string>): Ref => {
429
444
  return { ...ref, id: ids.get(ref.id) ?? ref.id };
430
445
  };
431
446
 
432
- const portableRefs = (refs: Ref[]): Ref[] =>
433
- refs.filter((ref) => ref.kind === "file" || ref.kind === "record");
434
447
  const portableRef = (ref: Ref | null): Ref | null =>
435
448
  ref && (ref.kind === "file" || ref.kind === "record") ? ref : null;
436
- const portableFact = (fact: Assessment["facts"][number]) => {
437
- const refs = portableRefs(fact.refs);
438
- return fact.basis === "observed" && refs.length === 0
439
- ? { ...fact, basis: "unknown" as const, refs }
440
- : { ...fact, refs };
441
- };
442
- const portableSignal = (signal: Assessment["signals"][keyof Assessment["signals"]]) => {
443
- const refs = portableRefs(signal.refs);
444
- return signal.basis === "observed" && refs.length === 0
445
- ? {
446
- ...signal,
447
- value: "unknown" as const,
448
- basis: "unknown" as const,
449
- reason: "portable supporting reference was removed",
450
- refs,
451
- }
452
- : { ...signal, refs };
453
- };
454
-
455
449
  const importedProvenance = (context: OperationContext): Provenance => ({
456
450
  kind: "imported",
457
451
  host: context.caller.host,
@@ -467,21 +461,6 @@ const portableTask = (task: TaskRecord): TaskRecord => {
467
461
  if (typeof stripped !== "object" || stripped === null || Array.isArray(stripped))
468
462
  throw new TypeError("portable task rewrite produced a non-record");
469
463
  const clone = structuredClone(stripped) as TaskRecord;
470
- const demote = (
471
- facts: Assessment["facts"][number][],
472
- signals: Assessment["signals"],
473
- consequences: Assessment["consequences"],
474
- ): void => {
475
- for (const fact of facts) Object.assign(fact, portableFact(fact));
476
- for (const name of Object.keys(signals) as (keyof Assessment["signals"])[]) {
477
- const signal = signals[name];
478
- Object.assign(signal, portableSignal(signal));
479
- }
480
- for (const consequence of consequences)
481
- Object.assign(consequence.fact, portableFact(consequence.fact));
482
- };
483
- for (const entry of clone.assessments)
484
- demote(entry.data.facts, entry.data.signals, entry.data.consequences);
485
464
  // Candidate metadata can contain environment-derived paths and digests. The destination
486
465
  // must recapture its own candidate instead of receiving source checkout material.
487
466
  clone.candidates = [];
@@ -506,6 +485,7 @@ const importedTask = (
506
485
  for (const entry of [
507
486
  source.intent,
508
487
  ...source.assessments,
488
+ ...(source.judgments ?? []),
509
489
  ...source.evidence,
510
490
  ...source.decisions,
511
491
  ...source.findings,
@@ -529,9 +509,16 @@ const importedTask = (
529
509
  id: fresh(entry.id),
530
510
  recordedAt: timestamp,
531
511
  provenance: imported,
532
- data: rewriteRecordRefs(data, assessmentSchema, remap) as typeof data,
512
+ data: rewriteRecordRefs(data, legacyAssessmentSchema, remap) as typeof data,
533
513
  };
534
514
  });
515
+ const judgments = source.judgments?.map((entry) => ({
516
+ ...entry,
517
+ id: fresh(entry.id),
518
+ recordedAt: timestamp,
519
+ provenance: imported,
520
+ data: rewriteRecordRefs(entry.data, judgmentSchema, remap) as typeof entry.data,
521
+ }));
535
522
  const evidence = source.evidence.map((entry) => ({
536
523
  ...entry,
537
524
  id: fresh(entry.id),
@@ -603,6 +590,7 @@ const importedTask = (
603
590
  status: "paused",
604
591
  closure: null,
605
592
  assessments,
593
+ ...(judgments ? { judgments } : {}),
606
594
  evidence,
607
595
  decisions: decisions.map((entry) => {
608
596
  const data = rewriteRecordRefs(entry.data, decisionSchema, remap) as typeof entry.data;
@@ -700,7 +688,9 @@ export class WorkitCore {
700
688
  * this checkout's branch (D3). Recording actions create that task on first
701
689
  * use; reads and lifecycle actions only look it up. Helpers never create.
702
690
  */
703
- private implicitTask(family: OperationFamily, request: unknown): Result<unknown> {
691
+ private implicitTask(family: OperationFamily, raw: unknown): Result<unknown> {
692
+ // Flat tool inputs become the canonical request first (S17).
693
+ const request = normalizeOperationInput(family, raw, this.context.caller.host);
704
694
  if (typeof request !== "object" || request === null || Array.isArray(request))
705
695
  return success(null, null, request);
706
696
  const value = request as { action?: unknown; taskId?: unknown };
@@ -969,7 +959,10 @@ export class WorkitCore {
969
959
  if ((this.context.workerId ?? null) !== null)
970
960
  return failure("permission_denied", "helpers cannot change task requirements");
971
961
  this.fillRevisions(input, task.data);
972
- const resolved = this.resolve(task.data, input.assessment);
962
+ const judged = normalizeJudgment(input.judgment, latestJudgment(task.data));
963
+ if (!judged.ok) return judged;
964
+ const judgment = judged.data.judgment;
965
+ const resolved = this.resolve(task.data, judgment);
973
966
  if (!resolved.ok) return resolved;
974
967
  if (input.action === "preview") return success(null, null, resolved.data);
975
968
  if (input.action !== "assess")
@@ -986,16 +979,18 @@ export class WorkitCore {
986
979
  "policy reassessed",
987
980
  mutation.now,
988
981
  );
989
- const assessment = {
982
+ const entry = {
990
983
  id: newId(),
991
984
  recordedAt: mutation.now,
992
985
  provenance: provenance(this.context),
993
- data: input.assessment as Assessment,
986
+ data: judgment,
994
987
  };
995
988
  const next = {
996
989
  ...current,
997
990
  constraints: this.context.constraints,
998
- assessments: [...current.assessments, assessment],
991
+ judgments: [...(current.judgments ?? []), entry],
992
+ // Older readers would drop judgments on rewrite: they fail closed instead.
993
+ critical: [...new Set([...(current.critical ?? []), "judgments"])],
999
994
  policy: resolved.data,
1000
995
  policyChanges: nextChange
1001
996
  ? [...current.policyChanges, nextChange]
@@ -1270,9 +1265,15 @@ export class WorkitCore {
1270
1265
  const workspace = this.store.readWorkspace();
1271
1266
  if (!workspace.ok) return workspace;
1272
1267
  if (!workspace.data) return failure("not_found", "workspace not found");
1273
- if (input.binding.taskId !== task.data.id || input.binding.workspaceId !== workspace.data.id)
1268
+ // Flat inputs omit the binding ids: a decision binds to where it is recorded.
1269
+ const binding = {
1270
+ ...input.binding,
1271
+ taskId: input.binding.taskId ?? task.data.id,
1272
+ workspaceId: input.binding.workspaceId ?? workspace.data.id,
1273
+ } as Decision["binding"];
1274
+ if (binding.taskId !== task.data.id || binding.workspaceId !== workspace.data.id)
1274
1275
  return failure("invalid_input", "decision task or workspace binding is invalid");
1275
- const content = verifyDecisionContentAtRoot(this.store.root, input.binding);
1276
+ const content = verifyDecisionContentAtRoot(this.store.root, binding);
1276
1277
  if (!content.ok) return content;
1277
1278
  const knownRequirements = new Set(
1278
1279
  task.data.policy?.requirements.map((item) => item.id) ?? [],
@@ -1281,7 +1282,7 @@ export class WorkitCore {
1281
1282
  return failure("invalid_input", "decision references an unknown requirement");
1282
1283
  const base = {
1283
1284
  purpose: input.purpose,
1284
- binding: input.binding,
1285
+ binding,
1285
1286
  response: input.response,
1286
1287
  requirementIds: input.requirementIds,
1287
1288
  revoked: null,
@@ -1765,20 +1766,12 @@ export class WorkitCore {
1765
1766
  });
1766
1767
  }
1767
1768
 
1768
- private resolve(task: TaskRecord, assessment: Assessment): Result<Policy> {
1769
+ private resolve(task: TaskRecord, judgment: Judgment): Result<Policy> {
1769
1770
  return resolvePolicy({
1770
1771
  intent: task.intent.data,
1771
- assessment,
1772
+ judgment,
1772
1773
  constraints: this.context.constraints,
1773
- prior: {
1774
- decisions: task.decisions
1775
- .filter((entry) => entry.provenance.kind !== "imported")
1776
- .map((entry) => entry.data),
1777
- findings: task.findings
1778
- .filter((entry) => entry.provenance.kind !== "imported")
1779
- .map((entry) => entry.data),
1780
- requirements: task.origin || task.policy === null ? [] : task.policy.requirements,
1781
- },
1774
+ verification: workspaceVerification(this.context.root),
1782
1775
  });
1783
1776
  }
1784
1777
 
@@ -34,6 +34,7 @@ import {
34
34
  } from "../check-config";
35
35
  import { worktreeSignal, worktreeTree } from "../git/rev";
36
36
  import { runtimeVersion } from "./task-store";
37
+ import { derivedRuleStatus, ledgerView } from "./policy/derive";
37
38
 
38
39
  export type CandidateEnvironment =
39
40
  | readonly (string | { name: string; value?: string | null })[]
@@ -556,7 +557,7 @@ export const verifyDecisionContentAtRoot = (
556
557
  return success(null, null, null);
557
558
  };
558
559
 
559
- const applicableDecision = (
560
+ export const applicableDecision = (
560
561
  task: TaskRecord,
561
562
  workspace: WorkspaceRecord,
562
563
  requirement: Requirement,
@@ -729,7 +730,40 @@ export function evaluateRequirements(
729
730
  decisionIds: [],
730
731
  reason: "stored policy version is unsupported; reassessment is required",
731
732
  }));
733
+ const ledger = ledgerView(checkoutRoot);
732
734
  return (task.policy?.requirements ?? []).map((requirement) => {
735
+ // Verdict, product-decision and plan rules read the ledger or the plan (S17).
736
+ const derived = derivedRuleStatus(task, requirement, checkoutRoot, ledger);
737
+ if (derived) {
738
+ if (derived.met)
739
+ return {
740
+ requirementId: requirement.id,
741
+ status: "satisfied" as const,
742
+ evidenceIds: [],
743
+ decisionIds: derived.decisionIds,
744
+ reason: derived.reason,
745
+ };
746
+ const waived = requirement.acceptanceAllowed
747
+ ? applicableDecision(task, workspace, requirement, checkoutRoot)
748
+ : [];
749
+ if (waived.length)
750
+ return {
751
+ requirementId: requirement.id,
752
+ status: "accepted_limitation" as const,
753
+ evidenceIds: [],
754
+ decisionIds: waived.map(
755
+ (decision) => task.decisions.find((entry) => entry.data === decision)!.id,
756
+ ),
757
+ reason: "an applicable approved limitation permits the missing evidence",
758
+ };
759
+ return {
760
+ requirementId: requirement.id,
761
+ status: "unsatisfied" as const,
762
+ evidenceIds: [],
763
+ decisionIds: [],
764
+ reason: derived.reason,
765
+ };
766
+ }
733
767
  const gate = observedGate(requirement)
734
768
  ? evaluateObservedGate(task, requirement, statuses, configFor())
735
769
  : null;