@brainervirus/workit-core 8.14.0 → 8.14.1
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/task-contract.ts +22 -4
- package/src/core/task-engine.ts +38 -3
- package/src/core/task-store.ts +8 -3
package/package.json
CHANGED
|
@@ -33,6 +33,9 @@ const text = z.string().check((ctx) => {
|
|
|
33
33
|
ctx.issues.push({ code: "custom", input: ctx.value, message: "invalid Unicode", path: [] });
|
|
34
34
|
});
|
|
35
35
|
const id = text.regex(uuidPattern, "expected lowercase UUID");
|
|
36
|
+
/** A full id or an unambiguous lowercase prefix of one (the 8-char short id other surfaces print). */
|
|
37
|
+
export const idRefPattern = /^[0-9a-f][0-9a-f-]{7,35}$/;
|
|
38
|
+
const idRef = text.regex(idRefPattern, "expected a UUID or an id prefix of 8+ characters");
|
|
36
39
|
const digest = text.regex(digestPattern, "expected lowercase SHA-256 digest");
|
|
37
40
|
const revision = text.regex(uuidPattern, "expected UUID revision");
|
|
38
41
|
const validUtc = (value: string): boolean => {
|
|
@@ -598,7 +601,7 @@ export const workerReportSchema = z
|
|
|
598
601
|
outcome: z.enum(["completed", "failed", "cancelled"]),
|
|
599
602
|
summary: text,
|
|
600
603
|
evidenceIds: z.array(id),
|
|
601
|
-
findingIds: z.array(
|
|
604
|
+
findingIds: z.array(idRef),
|
|
602
605
|
})
|
|
603
606
|
.strict();
|
|
604
607
|
export type WorkerReport = z.infer<typeof workerReportSchema>;
|
|
@@ -938,7 +941,7 @@ const findingOperations = {
|
|
|
938
941
|
action: z.literal("resolve"),
|
|
939
942
|
...taskId,
|
|
940
943
|
expectedRevision: revision.optional(),
|
|
941
|
-
findingId:
|
|
944
|
+
findingId: idRef,
|
|
942
945
|
disposition: z.enum(["open", "fixed", "dismissed", "deferred"]),
|
|
943
946
|
reason: text,
|
|
944
947
|
evidenceIds: z.array(id),
|
|
@@ -980,7 +983,7 @@ const workerOperations = {
|
|
|
980
983
|
...taskId,
|
|
981
984
|
expectedRevision: revision.optional(),
|
|
982
985
|
expectedWorkspaceRevision: revision.optional(),
|
|
983
|
-
workerId:
|
|
986
|
+
workerId: idRef,
|
|
984
987
|
report: workerReportSchema,
|
|
985
988
|
}),
|
|
986
989
|
cancel: operation({
|
|
@@ -988,7 +991,7 @@ const workerOperations = {
|
|
|
988
991
|
...taskId,
|
|
989
992
|
...revisions,
|
|
990
993
|
expectedWorkspaceRevision: revision.optional(),
|
|
991
|
-
workerId:
|
|
994
|
+
workerId: idRef,
|
|
992
995
|
reason: text,
|
|
993
996
|
}),
|
|
994
997
|
};
|
|
@@ -1118,6 +1121,21 @@ export const failure = (
|
|
|
1118
1121
|
details: ErrorDetails = {},
|
|
1119
1122
|
): Result<never> => ({ ok: false, schemaVersion: 1, code, error, details });
|
|
1120
1123
|
|
|
1124
|
+
/**
|
|
1125
|
+
* Resolve a full id or an unambiguous prefix against the ids a task holds.
|
|
1126
|
+
* An exact match always wins; several prefix matches fail listing the candidates.
|
|
1127
|
+
*/
|
|
1128
|
+
export function resolveIdRef(kind: string, ref: string, ids: readonly string[]): Result<string> {
|
|
1129
|
+
if (ids.includes(ref)) return success(null, null, ref);
|
|
1130
|
+
const matches = ids.filter((candidate) => candidate.startsWith(ref));
|
|
1131
|
+
if (matches.length === 1) return success(null, null, matches[0]);
|
|
1132
|
+
if (matches.length === 0) return failure("not_found", `${kind} not found`);
|
|
1133
|
+
return failure(
|
|
1134
|
+
"invalid_input",
|
|
1135
|
+
`${kind} prefix ${ref} is ambiguous; candidates: ${matches.join(", ")}`,
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1121
1139
|
export function parseOperation(family: OperationFamily, input: unknown): Result<OperationRequest> {
|
|
1122
1140
|
if (
|
|
1123
1141
|
typeof input === "object" &&
|
package/src/core/task-engine.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
legacyAssessmentSchema,
|
|
16
16
|
newId,
|
|
17
17
|
parseOperation,
|
|
18
|
+
resolveIdRef,
|
|
18
19
|
rewriteRecordRefs,
|
|
19
20
|
success,
|
|
20
21
|
type Candidate,
|
|
@@ -391,6 +392,8 @@ const TASK_SCOPED_ACTIONS: ReadonlySet<string> = new Set([
|
|
|
391
392
|
"state.export",
|
|
392
393
|
]);
|
|
393
394
|
/** Worker actions that name their worker: they resolve to that worker's task. */
|
|
395
|
+
const ONLY_OWN_REPORT = "workers can submit only their own report";
|
|
396
|
+
const NO_HELPER_CONTROL = "helpers cannot cancel or control workers";
|
|
394
397
|
const WORKER_ADDRESSED_ACTIONS: ReadonlySet<string> = new Set(["worker.cancel", "worker.report"]);
|
|
395
398
|
/**
|
|
396
399
|
* The exact CLI command that runs worker `request` (cancel or report, with a
|
|
@@ -1524,6 +1527,13 @@ export class WorkitCore {
|
|
|
1524
1527
|
return success(changed.data.revision, null, changed.data.findings.at(-1)!);
|
|
1525
1528
|
}
|
|
1526
1529
|
if (helper?.ok) return failure("permission_denied", "helpers cannot resolve findings");
|
|
1530
|
+
const findingRef = resolveIdRef(
|
|
1531
|
+
"finding",
|
|
1532
|
+
input.findingId,
|
|
1533
|
+
task.data.findings.map((entry) => entry.id),
|
|
1534
|
+
);
|
|
1535
|
+
if (!findingRef.ok) return findingRef;
|
|
1536
|
+
input.findingId = findingRef.data;
|
|
1527
1537
|
const finding = task.data.findings.find((entry) => entry.id === input.findingId);
|
|
1528
1538
|
if (!finding) return failure("not_found", "finding not found");
|
|
1529
1539
|
if (!input.reason.trim())
|
|
@@ -1738,13 +1748,39 @@ export class WorkitCore {
|
|
|
1738
1748
|
);
|
|
1739
1749
|
}
|
|
1740
1750
|
|
|
1751
|
+
// A helper may only address itself: resolving against its own id alone keeps
|
|
1752
|
+
// another worker's id out of any ambiguity error.
|
|
1753
|
+
const workerRef = resolveIdRef(
|
|
1754
|
+
"worker",
|
|
1755
|
+
input.workerId,
|
|
1756
|
+
helperId === null ? task.data.workers.map((candidate) => candidate.id) : [helperId],
|
|
1757
|
+
);
|
|
1758
|
+
if (!workerRef.ok)
|
|
1759
|
+
return helperId === null
|
|
1760
|
+
? workerRef
|
|
1761
|
+
: failure(
|
|
1762
|
+
"permission_denied",
|
|
1763
|
+
input.action === "report" ? ONLY_OWN_REPORT : NO_HELPER_CONTROL,
|
|
1764
|
+
);
|
|
1765
|
+
input.workerId = workerRef.data;
|
|
1741
1766
|
const entry = task.data.workers.find((candidate) => candidate.id === input.workerId);
|
|
1742
1767
|
if (!entry) return failure("not_found", "worker not found");
|
|
1743
1768
|
if (input.action === "report") {
|
|
1744
1769
|
if (helperId === null || helperId !== input.workerId)
|
|
1745
|
-
return failure("permission_denied",
|
|
1770
|
+
return failure("permission_denied", ONLY_OWN_REPORT);
|
|
1746
1771
|
const helper = this.helperEntry(task.data, true, true);
|
|
1747
1772
|
if (!helper.ok) return helper;
|
|
1773
|
+
const ownFindings = task.data.findings
|
|
1774
|
+
.filter((candidate) => candidate.provenance.workerId === helperId)
|
|
1775
|
+
.map((candidate) => candidate.id);
|
|
1776
|
+
const reported: string[] = [];
|
|
1777
|
+
for (const findingRef of input.report.findingIds) {
|
|
1778
|
+
const found = resolveIdRef("finding", findingRef, ownFindings);
|
|
1779
|
+
if (found.ok) reported.push(found.data);
|
|
1780
|
+
else if (found.code === "not_found") reported.push(findingRef);
|
|
1781
|
+
else return found;
|
|
1782
|
+
}
|
|
1783
|
+
input.report.findingIds = reported;
|
|
1748
1784
|
const evidence = task.data.evidence.filter((candidate) =>
|
|
1749
1785
|
input.report.evidenceIds.includes(candidate.id),
|
|
1750
1786
|
);
|
|
@@ -1800,8 +1836,7 @@ export class WorkitCore {
|
|
|
1800
1836
|
: failure("recovery_required", "worker disappeared during report");
|
|
1801
1837
|
}
|
|
1802
1838
|
|
|
1803
|
-
if (helperId !== null)
|
|
1804
|
-
return failure("permission_denied", "helpers cannot cancel or control workers");
|
|
1839
|
+
if (helperId !== null) return failure("permission_denied", NO_HELPER_CONTROL);
|
|
1805
1840
|
if (task.data.status === "closed")
|
|
1806
1841
|
return failure("invalid_transition", "closed task cannot cancel workers");
|
|
1807
1842
|
if (entry.data.state === "stopped") {
|
package/src/core/task-store.ts
CHANGED
|
@@ -47,6 +47,8 @@ import {
|
|
|
47
47
|
success,
|
|
48
48
|
taskRecordSchema,
|
|
49
49
|
workspaceRecordSchema,
|
|
50
|
+
idRefPattern,
|
|
51
|
+
resolveIdRef,
|
|
50
52
|
type Id,
|
|
51
53
|
type Intent,
|
|
52
54
|
type Provenance,
|
|
@@ -728,20 +730,23 @@ export class TaskStore {
|
|
|
728
730
|
* the branch checked out now (#264). Null when no open task has it.
|
|
729
731
|
*/
|
|
730
732
|
taskForWorker(workerId: unknown): Result<Id | null> {
|
|
731
|
-
if (typeof workerId !== "string" || !
|
|
733
|
+
if (typeof workerId !== "string" || !idRefPattern.test(workerId))
|
|
732
734
|
return failure(
|
|
733
735
|
"invalid_input",
|
|
734
736
|
"needs a worker id: `workit worker cancel|report <workerId>`",
|
|
735
737
|
);
|
|
736
738
|
const listed = this.listTaskIndex();
|
|
737
739
|
if (!listed.ok) return listed;
|
|
740
|
+
const owners = new Map<string, Id>();
|
|
738
741
|
for (const entry of listed.data) {
|
|
739
742
|
if (entry.status === "closed") continue;
|
|
740
743
|
const loaded = this.load(entry.id);
|
|
741
744
|
if (!loaded.ok) return loaded;
|
|
742
|
-
|
|
743
|
-
return success(null, null, entry.id);
|
|
745
|
+
for (const worker of loaded.data?.record.workers ?? []) owners.set(worker.id, entry.id);
|
|
744
746
|
}
|
|
747
|
+
const resolved = resolveIdRef("worker", workerId, [...owners.keys()]);
|
|
748
|
+
if (resolved.ok) return success(null, null, owners.get(resolved.data) ?? null);
|
|
749
|
+
if (resolved.code !== "not_found") return resolved;
|
|
745
750
|
return success(null, null, null);
|
|
746
751
|
}
|
|
747
752
|
|