@brainervirus/workit-core 1.0.10 → 1.0.12
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/package.json +1 -1
- package/src/core/authority.ts +53 -11
- package/src/core/branch.ts +39 -2
- package/src/core/doctor.ts +25 -12
- package/src/core/external-action-effects.ts +107 -24
- package/src/core/external-action.ts +343 -27
- package/src/core/methods.ts +8 -8
- package/src/core/route-intent.ts +28 -0
- package/src/core/runtime-identity.ts +33 -0
- package/src/core/task-contract.ts +109 -1
- package/src/core/task-engine.ts +207 -240
- package/src/core/task-evaluation.ts +129 -24
- package/src/core/task-store.ts +4 -6
- package/src/core/workers.ts +11 -1
- package/src/core.ts +15 -1
package/src/core/task-engine.ts
CHANGED
|
@@ -5,8 +5,14 @@ import {
|
|
|
5
5
|
failure,
|
|
6
6
|
decisionDigest,
|
|
7
7
|
exportBundleSchema,
|
|
8
|
+
assessmentSchema,
|
|
9
|
+
decisionSchema,
|
|
10
|
+
evidenceSchema,
|
|
11
|
+
findingSchema,
|
|
12
|
+
intentSchema,
|
|
8
13
|
newId,
|
|
9
14
|
parseOperation,
|
|
15
|
+
rewriteRecordRefs,
|
|
10
16
|
success,
|
|
11
17
|
type Assessment,
|
|
12
18
|
type Capability,
|
|
@@ -59,6 +65,7 @@ import {
|
|
|
59
65
|
import {
|
|
60
66
|
assertProductWriteAllowed,
|
|
61
67
|
isUncertainWorker,
|
|
68
|
+
workerBlocksTransition,
|
|
62
69
|
type CallerContext,
|
|
63
70
|
type NativeWorkerObservation,
|
|
64
71
|
type NativeWorkerVerification,
|
|
@@ -70,9 +77,12 @@ import {
|
|
|
70
77
|
} from "./workers";
|
|
71
78
|
import {
|
|
72
79
|
captureCandidate,
|
|
80
|
+
checkPin,
|
|
73
81
|
evaluateClosure,
|
|
74
82
|
evaluateEvidence,
|
|
75
83
|
evaluateRequirements,
|
|
84
|
+
findingVerificationPasses,
|
|
85
|
+
resolveBinding,
|
|
76
86
|
type CandidateEnvironment,
|
|
77
87
|
} from "./task-evaluation";
|
|
78
88
|
|
|
@@ -342,6 +352,8 @@ const applyWorkerLifecycle = (input: ObserveWorkerLifecycleInput): Result<Entry<
|
|
|
342
352
|
if (task.data.status === "paused" && (input.state === "running" || input.state === "cancelling"))
|
|
343
353
|
return failure("invalid_transition", "paused task cannot run a worker");
|
|
344
354
|
const notStarted = input.dispatch === true;
|
|
355
|
+
if (notStarted && entry.data.state === "stopped" && input.session === null)
|
|
356
|
+
return success(task.data.revision, workspace.data.revision, entry);
|
|
345
357
|
if (notStarted && (input.state !== "stopped" || input.session !== null))
|
|
346
358
|
return failure("invalid_input", "a never-dispatched worker stops with no session");
|
|
347
359
|
if (notStarted && entry.data.session !== null)
|
|
@@ -395,7 +407,7 @@ const applyWorkerLifecycle = (input: ObserveWorkerLifecycleInput): Result<Entry<
|
|
|
395
407
|
return success(task.data.revision, workspace.data.revision, entry);
|
|
396
408
|
const changed = input.store.mutateTaskAndWorkspace({
|
|
397
409
|
taskId: task.data.id,
|
|
398
|
-
|
|
410
|
+
expectedRevision: input.expectedRevision,
|
|
399
411
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
400
412
|
now: input.now,
|
|
401
413
|
workspace: (current, mutation) =>
|
|
@@ -469,96 +481,28 @@ const importedProvenance = (context: OperationContext): Provenance => ({
|
|
|
469
481
|
});
|
|
470
482
|
|
|
471
483
|
const portableTask = (task: TaskRecord): TaskRecord => {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
};
|
|
492
|
-
clone.constraints = clone.constraints.flatMap((constraint) => {
|
|
493
|
-
const source = portableRef(constraint.source);
|
|
494
|
-
return source
|
|
495
|
-
? [
|
|
496
|
-
{
|
|
497
|
-
...constraint,
|
|
498
|
-
source,
|
|
499
|
-
requires: constraint.requires.map((requirement) => ({
|
|
500
|
-
...requirement,
|
|
501
|
-
refs: portableRefs(requirement.refs),
|
|
502
|
-
})),
|
|
503
|
-
},
|
|
504
|
-
]
|
|
505
|
-
: [];
|
|
506
|
-
});
|
|
507
|
-
clone.assessments = clone.assessments.map((entry) => ({
|
|
508
|
-
...entry,
|
|
509
|
-
data: {
|
|
510
|
-
...entry.data,
|
|
511
|
-
facts: entry.data.facts.map(portableFact),
|
|
512
|
-
signals: Object.fromEntries(
|
|
513
|
-
Object.entries(entry.data.signals).map(([name, signal]) => [name, portableSignal(signal)]),
|
|
514
|
-
) as typeof entry.data.signals,
|
|
515
|
-
consequences: entry.data.consequences.map((consequence) => ({
|
|
516
|
-
...consequence,
|
|
517
|
-
fact: portableFact(consequence.fact),
|
|
518
|
-
})),
|
|
519
|
-
verification: entry.data.verification.map((verification) => ({
|
|
520
|
-
...verification,
|
|
521
|
-
availableChecks: portableRefs(verification.availableChecks),
|
|
522
|
-
})),
|
|
523
|
-
},
|
|
524
|
-
}));
|
|
525
|
-
clone.progress = {
|
|
526
|
-
...clone.progress,
|
|
527
|
-
blockers: clone.progress.blockers.map((blocker) => ({
|
|
528
|
-
...blocker,
|
|
529
|
-
refs: portableRefs(blocker.refs),
|
|
530
|
-
})),
|
|
484
|
+
// Schema-driven ref stripping: every Ref anywhere in the record is mapped
|
|
485
|
+
// by shape (arrays drop unportable refs, nullable fields null them), so a
|
|
486
|
+
// new ref-bearing field can never silently leak across checkouts.
|
|
487
|
+
const stripped = rewriteRecordRefs(task, taskRecordSchema, portableRef);
|
|
488
|
+
if (typeof stripped !== "object" || stripped === null || Array.isArray(stripped))
|
|
489
|
+
throw new TypeError("portable task rewrite produced a non-record");
|
|
490
|
+
const clone = structuredClone(stripped) as TaskRecord;
|
|
491
|
+
const demote = (
|
|
492
|
+
facts: Assessment["facts"][number][],
|
|
493
|
+
signals: Assessment["signals"],
|
|
494
|
+
consequences: Assessment["consequences"],
|
|
495
|
+
): void => {
|
|
496
|
+
for (const fact of facts) Object.assign(fact, portableFact(fact));
|
|
497
|
+
for (const name of Object.keys(signals) as (keyof Assessment["signals"])[]) {
|
|
498
|
+
const signal = signals[name];
|
|
499
|
+
Object.assign(signal, portableSignal(signal));
|
|
500
|
+
}
|
|
501
|
+
for (const consequence of consequences)
|
|
502
|
+
Object.assign(consequence.fact, portableFact(consequence.fact));
|
|
531
503
|
};
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
data: {
|
|
535
|
-
...entry.data,
|
|
536
|
-
refs: portableRefs(entry.data.refs),
|
|
537
|
-
reviewContext: portableRef(entry.data.reviewContext),
|
|
538
|
-
},
|
|
539
|
-
}));
|
|
540
|
-
clone.decisions = clone.decisions.map((entry) => ({
|
|
541
|
-
...entry,
|
|
542
|
-
data: {
|
|
543
|
-
...entry.data,
|
|
544
|
-
consumption: entry.data.consumption
|
|
545
|
-
? portableRef(entry.data.consumption.actionRef)
|
|
546
|
-
? {
|
|
547
|
-
...entry.data.consumption,
|
|
548
|
-
actionRef: portableRef(entry.data.consumption.actionRef)!,
|
|
549
|
-
}
|
|
550
|
-
: null
|
|
551
|
-
: null,
|
|
552
|
-
binding: {
|
|
553
|
-
...entry.data.binding,
|
|
554
|
-
contentRefs: portableRefs(entry.data.binding.contentRefs),
|
|
555
|
-
},
|
|
556
|
-
},
|
|
557
|
-
}));
|
|
558
|
-
clone.findings = clone.findings.map((entry) => ({
|
|
559
|
-
...entry,
|
|
560
|
-
data: { ...entry.data, refs: portableRefs(entry.data.refs) },
|
|
561
|
-
}));
|
|
504
|
+
for (const entry of clone.assessments)
|
|
505
|
+
demote(entry.data.facts, entry.data.signals, entry.data.consequences);
|
|
562
506
|
// Candidate metadata can contain environment-derived paths and digests. The destination
|
|
563
507
|
// must recapture its own candidate instead of receiving source checkout material.
|
|
564
508
|
clone.candidates = [];
|
|
@@ -591,15 +535,13 @@ const importedTask = (
|
|
|
591
535
|
allocate(entry.id);
|
|
592
536
|
const fresh = (id: string) => allocate(id);
|
|
593
537
|
const provenance = importedProvenance(context);
|
|
538
|
+
const remap = (ref: Ref): Ref | null => mapRef(ref, ids);
|
|
594
539
|
const intent = {
|
|
595
540
|
...source.intent,
|
|
596
541
|
id: fresh(source.intent.id),
|
|
597
542
|
recordedAt: timestamp,
|
|
598
543
|
provenance,
|
|
599
|
-
data:
|
|
600
|
-
...source.intent.data,
|
|
601
|
-
authorityRefs: source.intent.data.authorityRefs.map((ref) => mapRef(ref, ids)),
|
|
602
|
-
},
|
|
544
|
+
data: rewriteRecordRefs(source.intent.data, intentSchema, remap) as typeof source.intent.data,
|
|
603
545
|
};
|
|
604
546
|
const assessments = source.assessments.map((entry) => {
|
|
605
547
|
const data = entry.data;
|
|
@@ -608,30 +550,7 @@ const importedTask = (
|
|
|
608
550
|
id: fresh(entry.id),
|
|
609
551
|
recordedAt: timestamp,
|
|
610
552
|
provenance,
|
|
611
|
-
data:
|
|
612
|
-
...data,
|
|
613
|
-
facts: data.facts.map((fact) => ({
|
|
614
|
-
...fact,
|
|
615
|
-
refs: fact.refs.map((ref) => mapRef(ref, ids)),
|
|
616
|
-
})),
|
|
617
|
-
signals: Object.fromEntries(
|
|
618
|
-
Object.entries(data.signals).map(([name, signal]) => [
|
|
619
|
-
name,
|
|
620
|
-
{ ...signal, refs: signal.refs.map((ref) => mapRef(ref, ids)) },
|
|
621
|
-
]),
|
|
622
|
-
) as typeof data.signals,
|
|
623
|
-
consequences: data.consequences.map((consequence) => ({
|
|
624
|
-
...consequence,
|
|
625
|
-
fact: {
|
|
626
|
-
...consequence.fact,
|
|
627
|
-
refs: consequence.fact.refs.map((ref) => mapRef(ref, ids)),
|
|
628
|
-
},
|
|
629
|
-
})),
|
|
630
|
-
verification: data.verification.map((verification) => ({
|
|
631
|
-
...verification,
|
|
632
|
-
availableChecks: verification.availableChecks.map((ref) => mapRef(ref, ids)),
|
|
633
|
-
})),
|
|
634
|
-
},
|
|
553
|
+
data: rewriteRecordRefs(data, assessmentSchema, remap) as typeof data,
|
|
635
554
|
};
|
|
636
555
|
});
|
|
637
556
|
const evidence = source.evidence.map((entry) => ({
|
|
@@ -639,11 +558,7 @@ const importedTask = (
|
|
|
639
558
|
id: fresh(entry.id),
|
|
640
559
|
recordedAt: timestamp,
|
|
641
560
|
provenance,
|
|
642
|
-
data:
|
|
643
|
-
...entry.data,
|
|
644
|
-
refs: entry.data.refs.map((ref) => mapRef(ref, ids)),
|
|
645
|
-
reviewContext: entry.data.reviewContext ? mapRef(entry.data.reviewContext, ids) : null,
|
|
646
|
-
},
|
|
561
|
+
data: rewriteRecordRefs(entry.data, evidenceSchema, remap) as typeof entry.data,
|
|
647
562
|
}));
|
|
648
563
|
const decisions = source.decisions.map((entry) => ({
|
|
649
564
|
...entry,
|
|
@@ -651,23 +566,25 @@ const importedTask = (
|
|
|
651
566
|
recordedAt: timestamp,
|
|
652
567
|
provenance,
|
|
653
568
|
}));
|
|
654
|
-
const findings = source.findings.map((entry) =>
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
569
|
+
const findings = source.findings.map((entry) => {
|
|
570
|
+
const data = rewriteRecordRefs(entry.data, findingSchema, remap) as typeof entry.data;
|
|
571
|
+
return {
|
|
572
|
+
...entry,
|
|
573
|
+
id: fresh(entry.id),
|
|
574
|
+
recordedAt: timestamp,
|
|
575
|
+
provenance,
|
|
576
|
+
data: {
|
|
577
|
+
...data,
|
|
578
|
+
resolution: data.resolution
|
|
579
|
+
? {
|
|
580
|
+
...data.resolution,
|
|
581
|
+
evidenceIds: data.resolution.evidenceIds.map((id) => ids.get(id) ?? id),
|
|
582
|
+
decisionIds: data.resolution.decisionIds.map((id) => ids.get(id) ?? id),
|
|
583
|
+
}
|
|
584
|
+
: null,
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
});
|
|
671
588
|
const workers = source.workers.map((entry) => ({
|
|
672
589
|
...entry,
|
|
673
590
|
id: fresh(entry.id),
|
|
@@ -712,19 +629,21 @@ const importedTask = (
|
|
|
712
629
|
closure: null,
|
|
713
630
|
assessments,
|
|
714
631
|
evidence,
|
|
715
|
-
decisions: decisions.map((entry) =>
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
...entry
|
|
719
|
-
|
|
720
|
-
...
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
632
|
+
decisions: decisions.map((entry) => {
|
|
633
|
+
const data = rewriteRecordRefs(entry.data, decisionSchema, remap) as typeof entry.data;
|
|
634
|
+
return {
|
|
635
|
+
...entry,
|
|
636
|
+
data: {
|
|
637
|
+
...data,
|
|
638
|
+
binding: {
|
|
639
|
+
...data.binding,
|
|
640
|
+
taskId: "",
|
|
641
|
+
workspaceId: destinationWorkspaceId,
|
|
642
|
+
},
|
|
643
|
+
consumption: null,
|
|
724
644
|
},
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
})),
|
|
645
|
+
};
|
|
646
|
+
}),
|
|
728
647
|
...(actionProgress ? { actionProgress } : {}),
|
|
729
648
|
findings,
|
|
730
649
|
workers,
|
|
@@ -829,7 +748,7 @@ export class WorkitCore {
|
|
|
829
748
|
) {
|
|
830
749
|
if (workspace.writer)
|
|
831
750
|
return failure("recovery_required", "writer ownership must be released first");
|
|
832
|
-
if (task.workers.some((entry) =>
|
|
751
|
+
if (task.workers.some((entry) => workerBlocksTransition("close", entry.data.state)))
|
|
833
752
|
return failure("recovery_required", "worker state requires reconciliation");
|
|
834
753
|
return null;
|
|
835
754
|
}
|
|
@@ -1080,30 +999,23 @@ export class WorkitCore {
|
|
|
1080
999
|
environment(),
|
|
1081
1000
|
);
|
|
1082
1001
|
if (!currentCandidate.ok) return currentCandidate as Result<never>;
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
evidence.candidateId ??= currentCandidate.data.id;
|
|
1089
|
-
} else {
|
|
1090
|
-
evidence.beforeCandidateId ??= null;
|
|
1091
|
-
evidence.candidateId ??= null;
|
|
1092
|
-
}
|
|
1002
|
+
// Resolve the binding first (explicit IDs, else the worker pin, else the
|
|
1003
|
+
// current candidate), then validate and pin-check the resolved values —
|
|
1004
|
+
// never the caller's possibly-empty fields. The input itself is untouched.
|
|
1005
|
+
const pin = helper && helper.ok ? helper.data.data.assignment.candidateId : null;
|
|
1006
|
+
const bound = resolveBinding(evidence, pin, currentCandidate.data.id);
|
|
1093
1007
|
if (helper?.ok) {
|
|
1094
1008
|
const assignment = helper.data.data.assignment;
|
|
1095
1009
|
if (
|
|
1096
1010
|
evidence.requirementIds.some((id: string) => !assignment.requirementIds.includes(id)) ||
|
|
1097
|
-
(assignment.candidateId
|
|
1098
|
-
evidence.candidateId !== assignment.candidateId &&
|
|
1099
|
-
evidence.beforeCandidateId !== assignment.candidateId) ||
|
|
1011
|
+
!checkPin(assignment, bound.beforeCandidateId, bound.candidateId) ||
|
|
1100
1012
|
!refsWithinScope(evidence.refs, assignment.scope) ||
|
|
1101
1013
|
!refsWithinScope(evidence.refs, task.data.intent.data.scope)
|
|
1102
1014
|
)
|
|
1103
1015
|
return failure("permission_denied", "evidence is outside the worker assignment");
|
|
1104
1016
|
}
|
|
1105
1017
|
const knownCandidates = new Set(task.data.candidates.map((candidate) => candidate.id));
|
|
1106
|
-
for (const candidateId of [
|
|
1018
|
+
for (const candidateId of [bound.beforeCandidateId, bound.candidateId]) {
|
|
1107
1019
|
if (
|
|
1108
1020
|
candidateId &&
|
|
1109
1021
|
candidateId !== currentCandidate.data.id &&
|
|
@@ -1111,6 +1023,7 @@ export class WorkitCore {
|
|
|
1111
1023
|
)
|
|
1112
1024
|
return failure("invalid_input", "evidence candidate is not a captured candidate");
|
|
1113
1025
|
}
|
|
1026
|
+
const boundEvidence = { ...evidence, ...bound };
|
|
1114
1027
|
const changed = this.store.mutateTask(
|
|
1115
1028
|
task.data.id,
|
|
1116
1029
|
input.expectedRevision,
|
|
@@ -1122,14 +1035,14 @@ export class WorkitCore {
|
|
|
1122
1035
|
id: newId(),
|
|
1123
1036
|
recordedAt: mutation.now,
|
|
1124
1037
|
provenance: provenance(this.context, "agent_reported"),
|
|
1125
|
-
data:
|
|
1038
|
+
data: boundEvidence,
|
|
1126
1039
|
};
|
|
1127
1040
|
return success(mutation.revision, null, {
|
|
1128
1041
|
...current,
|
|
1129
1042
|
candidates,
|
|
1130
1043
|
evidence: [...current.evidence, entry],
|
|
1131
1044
|
findings: current.findings.map((finding) => {
|
|
1132
|
-
if (finding.data.disposition === "open" ||
|
|
1045
|
+
if (finding.data.disposition === "open" || boundEvidence.result === "skipped")
|
|
1133
1046
|
return finding;
|
|
1134
1047
|
if (finding.data.disposition !== "fixed") return finding;
|
|
1135
1048
|
// A fix stands while its verification still passes on the current
|
|
@@ -1142,17 +1055,17 @@ export class WorkitCore {
|
|
|
1142
1055
|
item.status,
|
|
1143
1056
|
]),
|
|
1144
1057
|
);
|
|
1145
|
-
const verified = withEntry.evidence.some(
|
|
1146
|
-
(
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
(
|
|
1150
|
-
|
|
1058
|
+
const verified = withEntry.evidence.some((item) =>
|
|
1059
|
+
findingVerificationPasses(finding.data.candidateId, {
|
|
1060
|
+
kind: item.data.kind,
|
|
1061
|
+
candidateId: item.data.candidateId,
|
|
1062
|
+
status: statuses.get(item.id) ?? "missing",
|
|
1063
|
+
}),
|
|
1151
1064
|
);
|
|
1152
1065
|
const contradicted =
|
|
1153
|
-
|
|
1066
|
+
boundEvidence.result === "failed" &&
|
|
1154
1067
|
finding.data.refs.some((left: Ref) =>
|
|
1155
|
-
|
|
1068
|
+
boundEvidence.refs.some((right: Ref) => sameValue(left, right)),
|
|
1156
1069
|
);
|
|
1157
1070
|
return verified && !contradicted
|
|
1158
1071
|
? finding
|
|
@@ -1189,7 +1102,15 @@ export class WorkitCore {
|
|
|
1189
1102
|
"permission_denied",
|
|
1190
1103
|
"native observation is only valid for recording decisions",
|
|
1191
1104
|
);
|
|
1192
|
-
|
|
1105
|
+
// Stated choices carry user-settled decisions without a native receipt.
|
|
1106
|
+
// They never authorize mutating actions; native verification is skipped
|
|
1107
|
+
// and provenance stays agent-reported.
|
|
1108
|
+
const stated = input.action === "record" && input.response === "stated";
|
|
1109
|
+
if (stated && input.purpose === "action")
|
|
1110
|
+
return failure("permission_denied", "stated choices cannot authorize mutating actions");
|
|
1111
|
+
if (stated && !input.binding?.statedChoice)
|
|
1112
|
+
return failure("invalid_input", "stated choices require binding.statedChoice");
|
|
1113
|
+
if (nativeRequired && nativeObservation === undefined && !stated)
|
|
1193
1114
|
return failure("permission_denied", "native decision observation is required");
|
|
1194
1115
|
const task = this.store.readTask(input.taskId);
|
|
1195
1116
|
if (!task.ok) return task as Result<never>;
|
|
@@ -1220,26 +1141,27 @@ export class WorkitCore {
|
|
|
1220
1141
|
consumption: null,
|
|
1221
1142
|
} satisfies Omit<Decision, "digest">;
|
|
1222
1143
|
const data: Decision = { ...base, digest: decisionDigest(base) };
|
|
1223
|
-
const native =
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1144
|
+
const native =
|
|
1145
|
+
nativeRequired && !stated
|
|
1146
|
+
? verifyNativeDecision(
|
|
1147
|
+
this.context.nativeAuthority,
|
|
1148
|
+
{
|
|
1149
|
+
observation: nativeObservation,
|
|
1150
|
+
expected: {
|
|
1151
|
+
taskId: task.data.id,
|
|
1152
|
+
workspaceId: workspace.data.id,
|
|
1153
|
+
purpose: input.purpose,
|
|
1154
|
+
response: input.response,
|
|
1155
|
+
binding: input.binding,
|
|
1156
|
+
bindingBytes: canonicalJson(input.binding),
|
|
1157
|
+
digest: data.digest,
|
|
1158
|
+
requirementIds: [...input.requirementIds],
|
|
1159
|
+
},
|
|
1160
|
+
caller: this.context.caller,
|
|
1237
1161
|
},
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
)
|
|
1242
|
-
: null;
|
|
1162
|
+
{ owner: this.authorityOwner, store: this.store, root: this.store.root },
|
|
1163
|
+
)
|
|
1164
|
+
: null;
|
|
1243
1165
|
if (native && !native.ok) return native as Result<never>;
|
|
1244
1166
|
const nativeProvenance = native?.ok ? retireNativeAuthority(native.data) : null;
|
|
1245
1167
|
if (native?.ok && !nativeProvenance)
|
|
@@ -1337,7 +1259,7 @@ export class WorkitCore {
|
|
|
1337
1259
|
if (
|
|
1338
1260
|
!bindingCovers(assignment.scope, input.scope) ||
|
|
1339
1261
|
!bindingCovers(task.data.intent.data.scope, input.scope) ||
|
|
1340
|
-
(assignment
|
|
1262
|
+
!checkPin(assignment, null, input.candidateId ?? null) ||
|
|
1341
1263
|
!refsWithinScope(input.refs, assignment.scope) ||
|
|
1342
1264
|
!refsWithinScope(input.refs, task.data.intent.data.scope)
|
|
1343
1265
|
)
|
|
@@ -1394,11 +1316,11 @@ export class WorkitCore {
|
|
|
1394
1316
|
const evaluations = evaluateEvidence(task.data, current.data);
|
|
1395
1317
|
const verified = evidence.some((entry) => {
|
|
1396
1318
|
const evaluation = evaluations.find((item) => item.evidenceId === entry.id);
|
|
1397
|
-
return (
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
);
|
|
1319
|
+
return findingVerificationPasses(finding.data.candidateId, {
|
|
1320
|
+
kind: entry.data.kind,
|
|
1321
|
+
candidateId: entry.data.candidateId,
|
|
1322
|
+
status: evaluation?.status ?? "missing",
|
|
1323
|
+
});
|
|
1402
1324
|
});
|
|
1403
1325
|
if (!verified)
|
|
1404
1326
|
return failure("permission_denied", "fixed findings require passing verification evidence");
|
|
@@ -1414,7 +1336,7 @@ export class WorkitCore {
|
|
|
1414
1336
|
: entry.data.candidateId === finding.data.candidateId ||
|
|
1415
1337
|
entry.data.beforeCandidateId === finding.data.candidateId;
|
|
1416
1338
|
const referenceMatches = finding.data.refs.some((left) =>
|
|
1417
|
-
entry.data.refs.some((right) =>
|
|
1339
|
+
entry.data.refs.some((right) => sameValue(left, right)),
|
|
1418
1340
|
);
|
|
1419
1341
|
return candidateMatches && (referenceMatches || entry.data.claim === finding.data.claim);
|
|
1420
1342
|
});
|
|
@@ -1537,7 +1459,7 @@ export class WorkitCore {
|
|
|
1537
1459
|
const workerId = newId();
|
|
1538
1460
|
const changed = this.store.mutateTaskAndWorkspace({
|
|
1539
1461
|
taskId: task.data.id,
|
|
1540
|
-
|
|
1462
|
+
expectedRevision: input.expectedRevision,
|
|
1541
1463
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
1542
1464
|
now: trustedNow(this.context),
|
|
1543
1465
|
workspace: (current, mutation) => success(mutation.revision, mutation.revision, current),
|
|
@@ -1597,7 +1519,7 @@ export class WorkitCore {
|
|
|
1597
1519
|
return failure("permission_denied", "worker report is outside the worker assignment");
|
|
1598
1520
|
const changed = this.store.mutateTaskAndWorkspace({
|
|
1599
1521
|
taskId: task.data.id,
|
|
1600
|
-
|
|
1522
|
+
expectedRevision: input.expectedRevision,
|
|
1601
1523
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
1602
1524
|
now: trustedNow(this.context),
|
|
1603
1525
|
workspace: (current, mutation) => success(mutation.revision, mutation.revision, current),
|
|
@@ -1630,20 +1552,28 @@ export class WorkitCore {
|
|
|
1630
1552
|
return failure("invalid_transition", "closed task cannot cancel workers");
|
|
1631
1553
|
if (entry.data.state === "unknown")
|
|
1632
1554
|
return failure("recovery_required", "worker state requires recovery");
|
|
1633
|
-
if (entry.data.state === "stopped")
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
//
|
|
1640
|
-
|
|
1555
|
+
if (entry.data.state === "stopped") {
|
|
1556
|
+
// Idempotent cancel: repeating a settled stop succeeds with the
|
|
1557
|
+
// current entry instead of failing the retry.
|
|
1558
|
+
const stopped = task.data.workers.find((candidate) => candidate.id === input.workerId)!;
|
|
1559
|
+
return success(task.data.revision, workspace.data.revision, stopped);
|
|
1560
|
+
}
|
|
1561
|
+
// Lead-attested cancel is terminal for every live state: the lead saw
|
|
1562
|
+
// the run end, so no host observation is awaited. A later live-child
|
|
1563
|
+
// sighting against a stopped worker is a new anomaly for the host to
|
|
1564
|
+
// flag, not a reason to strand the worker short of stopped. A writer
|
|
1565
|
+
// held by the cancelled worker dies with it; anything else would brick
|
|
1566
|
+
// the checkout behind a stopped owner.
|
|
1641
1567
|
const changed = this.store.mutateTaskAndWorkspace({
|
|
1642
1568
|
taskId: task.data.id,
|
|
1643
|
-
|
|
1569
|
+
expectedRevision: input.expectedRevision,
|
|
1644
1570
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
1645
1571
|
now: trustedNow(this.context),
|
|
1646
|
-
workspace: (current, mutation) =>
|
|
1572
|
+
workspace: (current, mutation) =>
|
|
1573
|
+
success(mutation.revision, mutation.revision, {
|
|
1574
|
+
...current,
|
|
1575
|
+
writer: current.writer?.owner.workerId === input.workerId ? null : current.writer,
|
|
1576
|
+
}),
|
|
1647
1577
|
task: (current, mutation) =>
|
|
1648
1578
|
success(mutation.revision, null, {
|
|
1649
1579
|
...current,
|
|
@@ -1652,10 +1582,7 @@ export class WorkitCore {
|
|
|
1652
1582
|
? {
|
|
1653
1583
|
...candidate,
|
|
1654
1584
|
recordedAt: mutation.now,
|
|
1655
|
-
data: {
|
|
1656
|
-
...candidate.data,
|
|
1657
|
-
state: settles ? ("stopped" as const) : ("cancelling" as const),
|
|
1658
|
-
},
|
|
1585
|
+
data: { ...candidate.data, state: "stopped" as const },
|
|
1659
1586
|
}
|
|
1660
1587
|
: candidate,
|
|
1661
1588
|
),
|
|
@@ -1767,7 +1694,7 @@ export class WorkitCore {
|
|
|
1767
1694
|
if (!attested.ok) return attested as Result<never>;
|
|
1768
1695
|
const changed = this.store.mutateTaskAndWorkspace({
|
|
1769
1696
|
taskId: task.data.id,
|
|
1770
|
-
|
|
1697
|
+
expectedRevision: input.expectedRevision,
|
|
1771
1698
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
1772
1699
|
now: trustedNow(this.context),
|
|
1773
1700
|
workspace: (current, mutation) => success(mutation.revision, mutation.revision, current),
|
|
@@ -1949,6 +1876,26 @@ export class WorkitCore {
|
|
|
1949
1876
|
if (!sameSession(helper.data.data.session, this.context))
|
|
1950
1877
|
return failure("permission_denied", "worker session does not match the caller");
|
|
1951
1878
|
}
|
|
1879
|
+
// Write-timed requirements gate acquisition, not just close: a spec
|
|
1880
|
+
// gate is enforced where writes begin, never as advisory prose.
|
|
1881
|
+
const gate = this.view(task.data);
|
|
1882
|
+
if (gate.ok) {
|
|
1883
|
+
const writeGates = (task.data.policy?.requirements ?? []).filter(
|
|
1884
|
+
(requirement) =>
|
|
1885
|
+
requirement.before === "write" &&
|
|
1886
|
+
gate.data.requirements.some(
|
|
1887
|
+
(evaluation) =>
|
|
1888
|
+
evaluation.requirementId === requirement.id &&
|
|
1889
|
+
(evaluation.status === "unsatisfied" || evaluation.status === "unavailable"),
|
|
1890
|
+
),
|
|
1891
|
+
);
|
|
1892
|
+
if (writeGates.length)
|
|
1893
|
+
return failure(
|
|
1894
|
+
"requirements_unsatisfied",
|
|
1895
|
+
"writer ownership is gated by unsatisfied requirements; record their evidence or an approved limitation waiver first",
|
|
1896
|
+
{ requirementIds: writeGates.map((requirement) => requirement.id) },
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1952
1899
|
const owner = {
|
|
1953
1900
|
taskId: task.data.id,
|
|
1954
1901
|
workerId: helperId,
|
|
@@ -1960,7 +1907,7 @@ export class WorkitCore {
|
|
|
1960
1907
|
};
|
|
1961
1908
|
const changed = this.store.mutateTaskAndWorkspace({
|
|
1962
1909
|
taskId: task.data.id,
|
|
1963
|
-
|
|
1910
|
+
expectedRevision: input.expectedRevision,
|
|
1964
1911
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
1965
1912
|
now: trustedNow(this.context),
|
|
1966
1913
|
workspace: (current, mutation) =>
|
|
@@ -1997,7 +1944,7 @@ export class WorkitCore {
|
|
|
1997
1944
|
}
|
|
1998
1945
|
const changed = this.store.mutateTaskAndWorkspace({
|
|
1999
1946
|
taskId: task.data.id,
|
|
2000
|
-
|
|
1947
|
+
expectedRevision: input.expectedRevision,
|
|
2001
1948
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
2002
1949
|
now: trustedNow(this.context),
|
|
2003
1950
|
workspace: (currentWorkspace, mutation) =>
|
|
@@ -2308,10 +2255,18 @@ export class WorkitCore {
|
|
|
2308
2255
|
if (seen.has(observation.workerId))
|
|
2309
2256
|
return failure("invalid_input", "worker observations must be unique");
|
|
2310
2257
|
seen.add(observation.workerId);
|
|
2258
|
+
// Anchor on fresh reads, not the passed view: a view captured before
|
|
2259
|
+
// recent mutations must not stale-fail observations that match the
|
|
2260
|
+
// current state. Genuinely stale observations still conflict below.
|
|
2261
|
+
const freshTask = this.store.readTask(view.task.id);
|
|
2262
|
+
const freshWorkspace = this.store.readWorkspace();
|
|
2263
|
+
if (!freshTask.ok) return freshTask as Result<never>;
|
|
2264
|
+
if (!freshWorkspace.ok) return freshWorkspace as Result<never>;
|
|
2265
|
+
if (!freshWorkspace.data) return failure("not_found", "workspace not found");
|
|
2311
2266
|
if (
|
|
2312
2267
|
observation.taskId !== view.task.id ||
|
|
2313
|
-
observation.expectedRevision !==
|
|
2314
|
-
observation.expectedWorkspaceRevision !==
|
|
2268
|
+
observation.expectedRevision !== freshTask.data.revision ||
|
|
2269
|
+
observation.expectedWorkspaceRevision !== freshWorkspace.data.revision
|
|
2315
2270
|
)
|
|
2316
2271
|
return failure("revision_conflict", "worker observation is stale");
|
|
2317
2272
|
if (!view.task.workers.some((entry) => entry.id === observation.workerId))
|
|
@@ -2424,11 +2379,27 @@ export class WorkitCore {
|
|
|
2424
2379
|
return failure("permission_denied", "imported resume requires native destination approval");
|
|
2425
2380
|
resumeCandidate = current.data.candidate;
|
|
2426
2381
|
}
|
|
2427
|
-
const blocker =
|
|
2382
|
+
const blocker =
|
|
2383
|
+
status === "paused"
|
|
2384
|
+
? workspace.data.writer
|
|
2385
|
+
? failure("recovery_required", "writer ownership must be released first")
|
|
2386
|
+
: null
|
|
2387
|
+
: this.activeWorkerBlocker(task.data, workspace.data);
|
|
2428
2388
|
if (blocker) return blocker as Result<never>;
|
|
2389
|
+
let pauseCandidate: import("./task-contract").Candidate | null = null;
|
|
2390
|
+
if (status === "paused") {
|
|
2391
|
+
// Freezing records the tree state the task resumes from.
|
|
2392
|
+
const captured = captureCandidate(
|
|
2393
|
+
this.store.root,
|
|
2394
|
+
task.data.intent.data.scope,
|
|
2395
|
+
environment(),
|
|
2396
|
+
);
|
|
2397
|
+
if (!captured.ok) return captured as Result<never>;
|
|
2398
|
+
pauseCandidate = captured.data;
|
|
2399
|
+
}
|
|
2429
2400
|
const changed = this.store.mutateTaskAndWorkspace({
|
|
2430
2401
|
taskId: task.data.id,
|
|
2431
|
-
|
|
2402
|
+
expectedRevision: input.expectedRevision,
|
|
2432
2403
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
2433
2404
|
now: trustedNow(this.context),
|
|
2434
2405
|
workspace: (workspace, context) => success(context.revision, context.revision, workspace),
|
|
@@ -2437,9 +2408,11 @@ export class WorkitCore {
|
|
|
2437
2408
|
...current,
|
|
2438
2409
|
status,
|
|
2439
2410
|
candidates:
|
|
2440
|
-
resumeCandidate &&
|
|
2441
|
-
!current.candidates.some(
|
|
2442
|
-
|
|
2411
|
+
(resumeCandidate ?? pauseCandidate) &&
|
|
2412
|
+
!current.candidates.some(
|
|
2413
|
+
(candidate) => candidate.id === (resumeCandidate ?? pauseCandidate)!.id,
|
|
2414
|
+
)
|
|
2415
|
+
? [...current.candidates, (resumeCandidate ?? pauseCandidate)!]
|
|
2443
2416
|
: current.candidates,
|
|
2444
2417
|
progress: current.progress,
|
|
2445
2418
|
pauseReason: status === "paused" ? (input.reason ?? null) : null,
|
|
@@ -2478,18 +2451,12 @@ export class WorkitCore {
|
|
|
2478
2451
|
const workspace = this.store.readWorkspace();
|
|
2479
2452
|
if (!workspace.ok) return workspace as Result<never>;
|
|
2480
2453
|
if (!workspace.data) return failure("not_found", "workspace not found");
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
task.data.workers.some((entry) => isUncertainWorker(entry.data.state))
|
|
2484
|
-
)
|
|
2485
|
-
return failure(
|
|
2486
|
-
"recovery_required",
|
|
2487
|
-
"scope revision requires worker ownership reconciliation",
|
|
2488
|
-
);
|
|
2454
|
+
const revisionBlocker = this.activeWorkerBlocker(task.data, workspace.data);
|
|
2455
|
+
if (revisionBlocker) return revisionBlocker as Result<never>;
|
|
2489
2456
|
this.fillRevisions(input, task.data, workspace.data);
|
|
2490
2457
|
const changed = this.store.mutateTaskAndWorkspace({
|
|
2491
2458
|
taskId: task.data.id,
|
|
2492
|
-
|
|
2459
|
+
expectedRevision: input.expectedRevision,
|
|
2493
2460
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
2494
2461
|
now: trustedNow(this.context),
|
|
2495
2462
|
workspace: (workspace, context) => success(context.revision, context.revision, workspace),
|
|
@@ -2549,7 +2516,7 @@ export class WorkitCore {
|
|
|
2549
2516
|
if (!closure.ok) return closure as Result<never>;
|
|
2550
2517
|
const changed = this.store.mutateTaskAndWorkspace({
|
|
2551
2518
|
taskId: task.data.id,
|
|
2552
|
-
|
|
2519
|
+
expectedRevision: input.expectedRevision,
|
|
2553
2520
|
expectedWorkspaceRevision: input.expectedWorkspaceRevision,
|
|
2554
2521
|
now: trustedNow(this.context),
|
|
2555
2522
|
workspace: (value, context) => success(context.revision, context.revision, value),
|