@zq-silk/yui 0.14.0 → 0.14.2
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/ARCHITECTURE.md +64 -35
- package/README.md +258 -125
- package/dist/agent/argumentPolicy.js +2 -1
- package/dist/agent/managedRuntimeEnvironment.js +5 -8
- package/dist/cli/commandCatalog.js +74 -109
- package/dist/cli/helpRenderer.js +1 -2
- package/dist/cli/interactionCandidates.js +10 -0
- package/dist/cli/interactionPolicy.js +76 -37
- package/dist/cli/roleWizard.js +5 -6
- package/dist/cli/updateOrchestrator.js +10 -8
- package/dist/cli/updatePorts.js +17 -13
- package/dist/cli/upgradeCommand.js +2 -5
- package/dist/cli.js +268 -231
- package/dist/commands/agentCommands.js +13 -0
- package/dist/commands/configCommands.js +7 -19
- package/dist/commands/configOverview.js +3 -2
- package/dist/commands/durableJobCommands.js +1 -1
- package/dist/commands/executionAuditCommands.js +4 -9
- package/dist/commands/globalRoleCommands.js +17 -0
- package/dist/commands/profileCommands.js +200 -30
- package/dist/commands/sessionCommands.js +1 -1
- package/dist/commands/taskActor.js +29 -127
- package/dist/commands/taskChangeSetCommands.js +1 -3
- package/dist/commands/taskCommands.js +1771 -2107
- package/dist/commands/taskCompletionGate.js +27 -103
- package/dist/commands/taskContextCommand.js +47 -80
- package/dist/commands/taskExecutionCommands.js +9 -27
- package/dist/commands/taskInputCommands.js +10 -10
- package/dist/commands/taskIntegrationCommands.js +69 -61
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskNextActionCommand.js +8 -29
- package/dist/commands/taskOverviewCommand.js +25 -3
- package/dist/commands/taskPublicationCommands.js +151 -85
- package/dist/commands/taskPublicationVerifyCommand.js +181 -0
- package/dist/commands/taskRemoteDeliveryCommand.js +134 -0
- package/dist/commands/taskRoleRuntimeStatus.js +5 -6
- package/dist/commands/taskUpstreamCommands.js +79 -95
- package/dist/config/configCatalog.js +1 -1
- package/dist/context/contextSnapshot.js +17 -2
- package/dist/context/sessionBootstrapManifest.js +18 -1
- package/dist/context/sourceTurnContext.js +30 -0
- package/dist/context/turnContextPack.js +107 -48
- package/dist/context/turnInputContract.js +11 -4
- package/dist/context/wakeNotification.js +84 -22
- package/dist/controller/agentRuntimeObserver.js +9 -12
- package/dist/controller/clientRuntime.js +13 -13
- package/dist/controller/controller.js +11 -11
- package/dist/controller/fileSchedulerStoreAdapter.js +262 -219
- package/dist/controller/jobControl.js +11 -10
- package/dist/controller/resourceInventory.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +2 -2
- package/dist/controller/runtime.js +18 -61
- package/dist/controller/runtimeEventInbox.js +57 -34
- package/dist/controller/runtimeEventProcessor.js +36 -96
- package/dist/controller/runtimeHookTurnFence.js +33 -57
- package/dist/controller/runtimeLaunchCoordinator.js +44 -40
- package/dist/controller/runtimeObservationHook.js +18 -12
- package/dist/controller/sessionNotify.js +43 -28
- package/dist/controller/sessionOwnerReconciliation.js +21 -21
- package/dist/controller/structuredProviderObservation.js +13 -16
- package/dist/coordination/workMailboxQueue.js +132 -0
- package/dist/core/controllerServer.js +1 -0
- package/dist/domain/agentResultTransport.js +60 -0
- package/dist/execution/executionHealth.js +111 -118
- package/dist/execution/reviewMainTurn.js +161 -0
- package/dist/execution/workItemExecution.js +454 -0
- package/dist/execution/workItemExecutionProjection.js +309 -0
- package/dist/execution/workItemMainTurn.js +164 -0
- package/dist/executor/agentExecutor.js +19 -19
- package/dist/executor/executorRegistry.js +7 -28
- package/dist/executor/fileRoleLaunchPlanner.js +51 -83
- package/dist/external/githubPublicationVerifier.js +114 -0
- package/dist/external/gitlabPublicationVerifier.js +158 -0
- package/dist/external/pinnedCommandRunner.js +73 -0
- package/dist/integration/changeSetManifest.js +0 -3
- package/dist/integration/deliveryObligation.js +43 -6
- package/dist/integration/gitIntegrationService.js +284 -101
- package/dist/integration/integrationAttempt.js +64 -14
- package/dist/integration/integrationQueueEntry.js +5 -35
- package/dist/integration/integrationQueueService.js +65 -194
- package/dist/lifecycle/canonicalLifecycleEvent.js +16 -15
- package/dist/lifecycle/exactTurnTerminalization.js +100 -97
- package/dist/message/message.js +16 -0
- package/dist/observability/executionAudit.js +4 -25
- package/dist/observability/faultClassification.js +58 -70
- package/dist/observability/orchestrationMetrics.js +8 -50
- package/dist/profile/agentProfile.js +60 -15
- package/dist/profile/agentProfileRuntime.js +100 -0
- package/dist/release/releaseWorkflowPorts.js +5 -68
- package/dist/repository/executionLaneGitSnapshot.js +80 -0
- package/dist/repository/gitWorkspace.js +9 -67
- package/dist/repository/taskBaseFreshness.js +3 -1
- package/dist/repository/taskWorkspaceCoordinator.js +32 -4
- package/dist/repository/taskWorkspacePreparer.js +252 -411
- package/dist/resources/liveReferences.js +6 -55
- package/dist/resources/resourceDiscovery.js +1 -30
- package/dist/resources/resourceRegistrar.js +0 -23
- package/dist/review/deltaRecheck.js +8 -65
- package/dist/review/reviewAcceptance.js +22 -19
- package/dist/review/reviewConfig.js +0 -10
- package/dist/review/reviewDecision.js +2 -2
- package/dist/review/reviewRound.js +147 -273
- package/dist/review/reviewerAvailability.js +13 -23
- package/dist/role/role.js +3 -0
- package/dist/runtime/agentDriverObservation.js +1 -1
- package/dist/runtime/agentHost.js +51 -51
- package/dist/runtime/builtinAgentDrivers.js +9 -2
- package/dist/runtime/codexAppServerRuntime.js +23 -5
- package/dist/runtime/exactControlPlane.js +2 -267
- package/dist/runtime/firstProgressAdvisory.js +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/launchBroker.js +11 -11
- package/dist/runtime/lifecycleReservation.js +2 -2
- package/dist/runtime/managedCaller.js +111 -0
- package/dist/runtime/ports.js +16 -3
- package/dist/runtime/preallocatedNativeSession.js +2 -2
- package/dist/runtime/processExitObservation.js +3 -2
- package/dist/runtime/providerContinuationReconciliationService.js +1 -1
- package/dist/runtime/runtimeBinding.js +1 -1
- package/dist/runtime/runtimeObservation.js +33 -16
- package/dist/runtime/runtimeSessionCandidate.js +2 -2
- package/dist/runtime/runtimeStopReceipt.js +11 -11
- package/dist/runtime/sessionLaunchRequest.js +2 -2
- package/dist/runtime/sessionOwnerIdentity.js +9 -9
- package/dist/runtime/sessionOwnerRegistry.js +13 -13
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/sessionTerminationGuard.js +9 -9
- package/dist/runtime/sessionTokenMetrics.js +5 -7
- package/dist/runtime/structuredProviderHost.js +5 -5
- package/dist/runtime/taskRuntimeIsolation.js +24 -24
- package/dist/runtime/tmuxAdapters.js +32 -30
- package/dist/scheduler/activeRoleTurnDelivery.js +37 -15
- package/dist/scheduler/leaderWakeupProcessor.js +78 -55
- package/dist/scheduler/roleTurnLiveness.js +6 -6
- package/dist/scheduler/roleTurnStall.js +8 -9
- package/dist/scheduler/taskExecutionProjection.js +19 -41
- package/dist/scheduler/taskObservabilityProjection.js +75 -122
- package/dist/storage/sqliteSchema.js +1 -21
- package/dist/storage/sqliteStore.js +62 -47
- package/dist/storage/storageSchema.js +1 -8
- package/dist/storage/storageVersions.js +7 -4
- package/dist/storage/taskStore.js +8 -8
- package/dist/storage/upgrade/upgradeOrchestrator.js +112 -83
- package/dist/task/completionReadiness.js +17 -98
- package/dist/task/deliveryGuard.js +9 -11
- package/dist/task/draftPlan.js +142 -0
- package/dist/task/nextAction.js +179 -477
- package/dist/task/publicationVerification.js +1 -0
- package/dist/task/remoteDelivery.js +233 -0
- package/dist/task/task.js +108 -4
- package/dist/task/taskRecordReference.js +0 -1
- package/dist/turn/turn.js +109 -16
- package/dist/verification/verificationGateService.js +0 -26
- package/dist/web/assets/client/components.js +99 -121
- package/dist/web/assets/client/i18n.js +136 -58
- package/dist/web/assets/client/view.js +44 -0
- package/dist/web/assets/styles/cards.js +3 -15
- package/dist/web/assets/styles/widgets.js +4 -0
- package/dist/web/webSnapshot.js +29 -14
- package/dist/workItem/dependencyGate.js +34 -0
- package/dist/workItem/workItem.js +99 -266
- package/dist/workspace/workItemChangeSetManager.js +15 -59
- package/docs/task-local-identity.md +1 -1
- package/i18n/README.zh-CN.md +165 -59
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +272 -782
- package/skills/yui-operator/SKILL.md +190 -365
- package/skills/yui-reviewer/SKILL.md +40 -33
- package/skills/yui-runtime/SKILL.md +27 -6
- package/skills/yui-worker/SKILL.md +19 -6
- package/dist/execution/candidateConvergence.js +0 -623
- package/dist/execution/executionGroup.js +0 -854
- package/dist/execution/resourceBroker.js +0 -426
- package/dist/executor/launchPlan.js +0 -16
- package/dist/review/reviewFinding.js +0 -240
- package/dist/review/reviewFindingLedger.js +0 -550
- package/dist/review/reviewOutcomeClassifier.js +0 -255
- package/dist/scheduler/resourceQueueProjection.js +0 -71
- package/dist/task/repairWave.js +0 -150
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
import { isDeepStrictEqual } from "node:util";
|
|
2
|
-
import { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
3
|
-
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
4
|
-
import { validateExecutionGroup } from "./executionGroup.js";
|
|
5
|
-
export const RESOURCE_BROKER_POLICY_SCHEMA_VERSION = 1;
|
|
6
|
-
/** Compatibility/display default; observed tokens never close execution spend. */
|
|
7
|
-
export const DEFAULT_EXECUTION_STAGE_TOKEN_BUDGET_PER_LANE = 120_000;
|
|
8
|
-
/** One shared hard-stop predicate for admission, recovery, and Leader routing. */
|
|
9
|
-
export function executionStageSpendClosed(resources) {
|
|
10
|
-
return resources.deadlineReached
|
|
11
|
-
|| resources.exhaustedBudgets.includes("tool-calls")
|
|
12
|
-
|| resources.exhaustedBudgets.includes("wall-clock");
|
|
13
|
-
}
|
|
14
|
-
const DEFAULT_ACTIVE_LANES = 4;
|
|
15
|
-
const DEFAULT_ACTIVE_LANES_PER_TASK = 2;
|
|
16
|
-
const DEFAULT_ACTIVE_LANES_PER_WORK_ITEM = 2;
|
|
17
|
-
const DEFAULT_ACTIVE_LANES_PER_GROUP = 2;
|
|
18
|
-
const DEFAULT_ACTIVE_LANES_PER_PROVIDER = 4;
|
|
19
|
-
const DEFAULT_ACTIVE_LANES_PER_AGENT = 2;
|
|
20
|
-
const DEFAULT_ACTIVE_LANES_PER_MODEL = 2;
|
|
21
|
-
const DEFAULT_QUEUED_LANES_PER_GROUP = 4;
|
|
22
|
-
export function resolveResourceBrokerPolicy(configured) {
|
|
23
|
-
const value = configured ?? {};
|
|
24
|
-
return Object.freeze({
|
|
25
|
-
schemaVersion: RESOURCE_BROKER_POLICY_SCHEMA_VERSION,
|
|
26
|
-
maxActiveLanes: positive(value.maxActiveLanes, DEFAULT_ACTIVE_LANES, "maxActiveLanes"),
|
|
27
|
-
maxActiveLanesPerTask: positive(value.maxActiveLanesPerTask, DEFAULT_ACTIVE_LANES_PER_TASK, "maxActiveLanesPerTask"),
|
|
28
|
-
maxActiveLanesPerWorkItem: positive(value.maxActiveLanesPerWorkItem, DEFAULT_ACTIVE_LANES_PER_WORK_ITEM, "maxActiveLanesPerWorkItem"),
|
|
29
|
-
maxActiveLanesPerGroup: positive(value.maxActiveLanesPerGroup, DEFAULT_ACTIVE_LANES_PER_GROUP, "maxActiveLanesPerGroup"),
|
|
30
|
-
maxActiveLanesPerProvider: positive(value.maxActiveLanesPerProvider, DEFAULT_ACTIVE_LANES_PER_PROVIDER, "maxActiveLanesPerProvider"),
|
|
31
|
-
maxActiveLanesPerAgent: positive(value.maxActiveLanesPerAgent, DEFAULT_ACTIVE_LANES_PER_AGENT, "maxActiveLanesPerAgent"),
|
|
32
|
-
maxActiveLanesPerModel: positive(value.maxActiveLanesPerModel, DEFAULT_ACTIVE_LANES_PER_MODEL, "maxActiveLanesPerModel"),
|
|
33
|
-
maxQueuedLanesPerGroup: positive(value.maxQueuedLanesPerGroup, DEFAULT_QUEUED_LANES_PER_GROUP, "maxQueuedLanesPerGroup")
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Deterministic admission over the caller's stable request order. Capacity
|
|
38
|
-
* pressure queues a Lane; it never terminalizes that Lane or its siblings.
|
|
39
|
-
*/
|
|
40
|
-
export function planResourceAdmissions(input) {
|
|
41
|
-
const policy = resolveResourceBrokerPolicy(input.policy);
|
|
42
|
-
const active = input.active.map(validateLaneIdentity);
|
|
43
|
-
const queued = input.queued.map(validateLaneIdentity);
|
|
44
|
-
const decisions = [];
|
|
45
|
-
for (const raw of input.requests) {
|
|
46
|
-
const request = validateLaneIdentity(raw);
|
|
47
|
-
const directLimits = activeLimits(policy, active, request);
|
|
48
|
-
const activeLaneKeys = new Set(active.map(resourceLaneKey));
|
|
49
|
-
const queuedBefore = [
|
|
50
|
-
...queued,
|
|
51
|
-
...decisions.filter(({ decision }) => decision === "queued").map(({ request }) => request)
|
|
52
|
-
].filter((candidate) => !activeLaneKeys.has(resourceLaneKey(candidate)));
|
|
53
|
-
const fairReservations = directLimits.length === 0
|
|
54
|
-
? reservableOlderQueuedLanes(policy, active, queuedBefore, request)
|
|
55
|
-
: [];
|
|
56
|
-
const fairLimits = activeLimits(policy, [...active, ...fairReservations], request);
|
|
57
|
-
const limitedBy = fairLimits.length === 0
|
|
58
|
-
? directLimits
|
|
59
|
-
: directLimits.length === 0 && fairReservations.length > 0
|
|
60
|
-
? [...fairLimits, "fair-queue"]
|
|
61
|
-
: fairLimits;
|
|
62
|
-
if (limitedBy.length === 0) {
|
|
63
|
-
active.push(request);
|
|
64
|
-
decisions.push(Object.freeze({
|
|
65
|
-
request,
|
|
66
|
-
decision: "admitted",
|
|
67
|
-
limitedBy: [],
|
|
68
|
-
reason: "capacity is available at every resource scope"
|
|
69
|
-
}));
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
const groupQueued = queuedBefore.filter(({ taskId, executionGroupId }) => (taskId === request.taskId && executionGroupId === request.executionGroupId)).length;
|
|
73
|
-
if (groupQueued >= policy.maxQueuedLanesPerGroup) {
|
|
74
|
-
decisions.push(Object.freeze({
|
|
75
|
-
request,
|
|
76
|
-
decision: "blocked",
|
|
77
|
-
limitedBy: [...limitedBy, "group-queue"],
|
|
78
|
-
reason: `resource capacity is unavailable and the Group queue is full (${groupQueued}/${policy.maxQueuedLanesPerGroup})`
|
|
79
|
-
}));
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
decisions.push(Object.freeze({
|
|
83
|
-
request,
|
|
84
|
-
decision: "queued",
|
|
85
|
-
limitedBy,
|
|
86
|
-
reason: `resource backpressure: ${limitedBy.join(", ")}`
|
|
87
|
-
}));
|
|
88
|
-
}
|
|
89
|
-
return decisions;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Reserve only older queued Lanes that could start now. This gives released
|
|
93
|
-
* capacity to the oldest compatible waiter without letting a Provider- or
|
|
94
|
-
* Agent-blocked head prevent independent work from using other scopes.
|
|
95
|
-
*/
|
|
96
|
-
function reservableOlderQueuedLanes(policy, active, queued, request) {
|
|
97
|
-
const projected = [...active];
|
|
98
|
-
const reserved = [];
|
|
99
|
-
const seen = new Set();
|
|
100
|
-
for (const candidate of [...queued].sort(compareResourceQueueOrder)) {
|
|
101
|
-
const key = resourceLaneKey(candidate);
|
|
102
|
-
if (seen.has(key))
|
|
103
|
-
continue;
|
|
104
|
-
seen.add(key);
|
|
105
|
-
if (sameResourceLane(candidate, request)
|
|
106
|
-
|| compareResourceQueueOrder(candidate, request) >= 0
|
|
107
|
-
|| activeLimits(policy, projected, candidate).length > 0)
|
|
108
|
-
continue;
|
|
109
|
-
projected.push(candidate);
|
|
110
|
-
reserved.push(candidate);
|
|
111
|
-
}
|
|
112
|
-
return reserved;
|
|
113
|
-
}
|
|
114
|
-
function compareResourceQueueOrder(left, right) {
|
|
115
|
-
return left.requestedAt.localeCompare(right.requestedAt)
|
|
116
|
-
|| left.taskId.localeCompare(right.taskId, undefined, { numeric: true })
|
|
117
|
-
|| left.executionGroupId.localeCompare(right.executionGroupId, undefined, { numeric: true })
|
|
118
|
-
|| left.executionLaneId.localeCompare(right.executionLaneId, undefined, { numeric: true });
|
|
119
|
-
}
|
|
120
|
-
function sameResourceLane(left, right) {
|
|
121
|
-
return resourceLaneKey(left) === resourceLaneKey(right);
|
|
122
|
-
}
|
|
123
|
-
function resourceLaneKey(value) {
|
|
124
|
-
return `${value.taskId}\0${value.executionGroupId}\0${value.executionLaneId}`;
|
|
125
|
-
}
|
|
126
|
-
/** Current stage spend/completion projection used by CLI, Web, and routing. */
|
|
127
|
-
export function projectExecutionStageResources(input) {
|
|
128
|
-
const group = validateExecutionGroup(input.group);
|
|
129
|
-
const stageGroups = executionStageRetryLineage(group, input.stageGroups);
|
|
130
|
-
const stage = group.stage;
|
|
131
|
-
const resources = stage?.resources;
|
|
132
|
-
const tokens = nonNegative(input.usage.tokens, "Execution token usage");
|
|
133
|
-
const toolCalls = nonNegative(input.usage.toolCalls, "Execution tool-call usage");
|
|
134
|
-
const nowMs = input.now.getTime();
|
|
135
|
-
if (!Number.isFinite(nowMs))
|
|
136
|
-
throw new Error("Execution resource projection time is invalid.");
|
|
137
|
-
const stageStartedAt = stageGroups
|
|
138
|
-
.map(({ createdAt }) => createdAt)
|
|
139
|
-
.sort()[0] ?? group.createdAt;
|
|
140
|
-
const wallClockSeconds = Math.max(0, Math.floor((nowMs - Date.parse(stageStartedAt)) / 1_000));
|
|
141
|
-
const usable = group.lanes.filter(({ status }) => isUsable(status))
|
|
142
|
-
.sort((left, right) => (left.endedAt ?? left.updatedAt).localeCompare(right.endedAt ?? right.updatedAt));
|
|
143
|
-
const activeLaneIds = group.lanes.filter(({ status }) => status === "running").map(({ id }) => id);
|
|
144
|
-
const pendingLaneIds = group.lanes.filter(({ status }) => status === "pending").map(({ id }) => id);
|
|
145
|
-
const skippedLaneIds = group.lanes.filter(({ status }) => status === "skipped").map(({ id }) => id);
|
|
146
|
-
const quorum = resources?.quorum ?? group.lanes.length;
|
|
147
|
-
const quorumMet = usable.length >= quorum;
|
|
148
|
-
const quorumReachedAt = quorumMet
|
|
149
|
-
? (usable[quorum - 1].endedAt ?? usable[quorum - 1].updatedAt)
|
|
150
|
-
: undefined;
|
|
151
|
-
const tokensObservable = input.usage.tokensObservable ?? true;
|
|
152
|
-
const toolCallsObservable = input.usage.toolCallsObservable ?? true;
|
|
153
|
-
const exhaustedBudgets = [];
|
|
154
|
-
if (toolCallsObservable && stage?.budget.maxToolCalls !== undefined
|
|
155
|
-
&& toolCalls >= stage.budget.maxToolCalls)
|
|
156
|
-
exhaustedBudgets.push("tool-calls");
|
|
157
|
-
if (stage?.budget.maxWallClockSeconds !== undefined
|
|
158
|
-
&& wallClockSeconds >= stage.budget.maxWallClockSeconds)
|
|
159
|
-
exhaustedBudgets.push("wall-clock");
|
|
160
|
-
const deadlineReached = resources === undefined
|
|
161
|
-
? false
|
|
162
|
-
: nowMs >= Date.parse(resources.deadlineAt);
|
|
163
|
-
const stragglerLaneIds = resources === undefined || quorumReachedAt === undefined
|
|
164
|
-
? []
|
|
165
|
-
: nowMs - Date.parse(quorumReachedAt) < resources.stragglerAfterSeconds * 1_000
|
|
166
|
-
? []
|
|
167
|
-
: [...activeLaneIds];
|
|
168
|
-
return Object.freeze({
|
|
169
|
-
tokens,
|
|
170
|
-
toolCalls,
|
|
171
|
-
wallClockSeconds,
|
|
172
|
-
...(stage?.budget.maxTokens === undefined
|
|
173
|
-
? {}
|
|
174
|
-
: { tokensRemaining: Math.max(0, stage.budget.maxTokens - tokens) }),
|
|
175
|
-
...(stage?.budget.maxToolCalls === undefined
|
|
176
|
-
? {}
|
|
177
|
-
: { toolCallsRemaining: Math.max(0, stage.budget.maxToolCalls - toolCalls) }),
|
|
178
|
-
...(stage?.budget.maxWallClockSeconds === undefined
|
|
179
|
-
? {}
|
|
180
|
-
: {
|
|
181
|
-
wallClockSecondsRemaining: Math.max(0, stage.budget.maxWallClockSeconds - wallClockSeconds)
|
|
182
|
-
}),
|
|
183
|
-
tokensObservable,
|
|
184
|
-
toolCallsObservable,
|
|
185
|
-
usableLaneCount: usable.length,
|
|
186
|
-
activeLaneIds,
|
|
187
|
-
pendingLaneIds,
|
|
188
|
-
skippedLaneIds,
|
|
189
|
-
quorumMet,
|
|
190
|
-
...(quorumReachedAt === undefined ? {} : { quorumReachedAt }),
|
|
191
|
-
deadlineReached,
|
|
192
|
-
exhaustedBudgets,
|
|
193
|
-
stragglerLaneIds
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Parallel-vs-sequential decision support. Evidence sufficiency is an explicit
|
|
198
|
-
* semantic input: budget pressure can block or defer work, but can never turn
|
|
199
|
-
* insufficient evidence into a successful early stop.
|
|
200
|
-
*/
|
|
201
|
-
export function routeExecutionStage(input) {
|
|
202
|
-
const group = validateExecutionGroup(input.group);
|
|
203
|
-
const stage = group.stage;
|
|
204
|
-
const policy = stage?.resources;
|
|
205
|
-
const marginal = input.marginalValuePercent === undefined
|
|
206
|
-
? undefined
|
|
207
|
-
: percentage(input.marginalValuePercent, "Execution marginal value");
|
|
208
|
-
const budgetPressure = executionStageSpendClosed(input.resources);
|
|
209
|
-
const marginalLow = marginal !== undefined
|
|
210
|
-
&& policy !== undefined
|
|
211
|
-
&& marginal < policy.minimumMarginalValuePercent;
|
|
212
|
-
const earlyTerminationAllowed = input.evidenceSufficient && input.resources.quorumMet;
|
|
213
|
-
const stopSpending = earlyTerminationAllowed && (budgetPressure || marginalLow);
|
|
214
|
-
const cancelPendingLaneIds = stopSpending ? input.resources.pendingLaneIds : [];
|
|
215
|
-
const retainActiveLaneIds = stopSpending ? input.resources.activeLaneIds : [];
|
|
216
|
-
if (earlyTerminationAllowed) {
|
|
217
|
-
if (input.resources.activeLaneIds.length > 0) {
|
|
218
|
-
return Object.freeze({
|
|
219
|
-
action: "wait",
|
|
220
|
-
earlyTerminationAllowed,
|
|
221
|
-
cancelPendingLaneIds,
|
|
222
|
-
retainActiveLaneIds,
|
|
223
|
-
reason: stopSpending
|
|
224
|
-
? "quorum and sufficient evidence allow pending work to stop, but active stragglers are retained"
|
|
225
|
-
: "evidence is sufficient, but active Lanes remain inside the stage policy"
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
if (!stopSpending && input.resources.pendingLaneIds.length > 0) {
|
|
229
|
-
return Object.freeze({
|
|
230
|
-
action: "wait",
|
|
231
|
-
earlyTerminationAllowed,
|
|
232
|
-
cancelPendingLaneIds: [],
|
|
233
|
-
retainActiveLaneIds: [],
|
|
234
|
-
reason: "evidence is sufficient, but pending Lanes remain inside the configured marginal-value policy"
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
return Object.freeze({
|
|
238
|
-
action: "resolve",
|
|
239
|
-
earlyTerminationAllowed,
|
|
240
|
-
cancelPendingLaneIds,
|
|
241
|
-
retainActiveLaneIds,
|
|
242
|
-
reason: stopSpending
|
|
243
|
-
? "quorum and sufficient evidence make further pending spend uneconomic"
|
|
244
|
-
: "quorum and sufficient evidence are complete"
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
if (input.resources.activeLaneIds.length > 0) {
|
|
248
|
-
return Object.freeze({
|
|
249
|
-
action: "wait",
|
|
250
|
-
earlyTerminationAllowed: false,
|
|
251
|
-
cancelPendingLaneIds: [],
|
|
252
|
-
retainActiveLaneIds: [],
|
|
253
|
-
reason: budgetPressure
|
|
254
|
-
? "evidence is insufficient; active work is retained despite budget pressure"
|
|
255
|
-
: "evidence is insufficient and active Lanes may still add evidence"
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
if (budgetPressure) {
|
|
259
|
-
return Object.freeze({
|
|
260
|
-
action: "blocked",
|
|
261
|
-
earlyTerminationAllowed: false,
|
|
262
|
-
cancelPendingLaneIds: [],
|
|
263
|
-
retainActiveLaneIds: [],
|
|
264
|
-
reason: "the stage budget or deadline is exhausted before evidence sufficiency"
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
if (input.resources.pendingLaneIds.length > 0) {
|
|
268
|
-
return Object.freeze({
|
|
269
|
-
action: "wait",
|
|
270
|
-
earlyTerminationAllowed: false,
|
|
271
|
-
cancelPendingLaneIds: [],
|
|
272
|
-
retainActiveLaneIds: [],
|
|
273
|
-
reason: "evidence is insufficient and scheduled Lanes are waiting for resource capacity"
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
const capacity = group.strategy.mode === "fixed" ? group.strategy.count : group.strategy.max;
|
|
277
|
-
if (group.strategy.mode === "adaptive"
|
|
278
|
-
&& group.lanes.length < capacity
|
|
279
|
-
&& (!input.resources.quorumMet || input.disagreement === "high")) {
|
|
280
|
-
return Object.freeze({
|
|
281
|
-
action: "expand-parallel",
|
|
282
|
-
earlyTerminationAllowed: false,
|
|
283
|
-
cancelPendingLaneIds: [],
|
|
284
|
-
retainActiveLaneIds: [],
|
|
285
|
-
reason: input.resources.quorumMet
|
|
286
|
-
? "material disagreement and available Lane capacity favor another independent route"
|
|
287
|
-
: "stage quorum is open and available Lane capacity requires another independent route"
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
return Object.freeze({
|
|
291
|
-
action: "deepen-sequential",
|
|
292
|
-
earlyTerminationAllowed: false,
|
|
293
|
-
cancelPendingLaneIds: [],
|
|
294
|
-
retainActiveLaneIds: [],
|
|
295
|
-
reason: "available evidence favors the next bounded stage over more parallel fan-out"
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
/** Fold exact runtime observations into the spend dimensions Yui can prove. */
|
|
299
|
-
export function observedExecutionResourceUsage(input) {
|
|
300
|
-
const group = validateExecutionGroup(input.group);
|
|
301
|
-
const stageGroups = executionStageRetryLineage(group, input.stageGroups);
|
|
302
|
-
const groupIds = new Set(stageGroups.map(({ id }) => id));
|
|
303
|
-
const turnIds = new Set([
|
|
304
|
-
...stageGroups.flatMap((candidate) => candidate.lanes
|
|
305
|
-
.flatMap(({ turnId }) => turnId === undefined ? [] : [turnId])),
|
|
306
|
-
...(input.turns ?? []).filter((turn) => (turn.taskId === group.taskId
|
|
307
|
-
&& turn.purpose === "execution"
|
|
308
|
-
&& turn.executionGroupId !== undefined
|
|
309
|
-
&& groupIds.has(turn.executionGroupId))).map(({ id }) => id)
|
|
310
|
-
]);
|
|
311
|
-
const observations = input.events.map(runtimeObservationFromTaskEvent)
|
|
312
|
-
.filter((value) => (value !== null
|
|
313
|
-
&& value.fence.taskId === group.taskId
|
|
314
|
-
&& value.fence.turnId !== undefined
|
|
315
|
-
&& turnIds.has(value.fence.turnId)));
|
|
316
|
-
let tokens = 0;
|
|
317
|
-
let tokensObservable = true;
|
|
318
|
-
for (const turnId of turnIds) {
|
|
319
|
-
const usage = observations.filter(({ fence, payload }) => (fence.turnId === turnId && payload.usage !== undefined)).sort((left, right) => (left.receivedAt.localeCompare(right.receivedAt)
|
|
320
|
-
|| (left.sequence ?? 0) - (right.sequence ?? 0)
|
|
321
|
-
|| (left.ordinal ?? 0) - (right.ordinal ?? 0)));
|
|
322
|
-
if (usage.length === 0) {
|
|
323
|
-
tokensObservable = false;
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
const requestSnapshots = new Map(usage
|
|
327
|
-
.filter(({ payload }) => payload.usage.semantics === "request-context")
|
|
328
|
-
.map((observation) => [observation.semanticKey, observation.payload.usage]));
|
|
329
|
-
if (requestSnapshots.size > 0) {
|
|
330
|
-
tokens += [...requestSnapshots.values()]
|
|
331
|
-
.reduce((sum, value) => sum + value.inputTokens + value.outputTokens, 0);
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
const cumulative = usage
|
|
335
|
-
.filter(({ payload }) => payload.usage.semantics === "cumulative-session")
|
|
336
|
-
.map(({ payload }) => payload.usage.inputTokens + payload.usage.outputTokens);
|
|
337
|
-
if (cumulative.length >= 2 && cumulative[0] === 0) {
|
|
338
|
-
tokens += Math.max(...cumulative);
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
if (cumulative.length >= 2) {
|
|
342
|
-
tokens += Math.max(0, Math.max(...cumulative) - cumulative[0]);
|
|
343
|
-
}
|
|
344
|
-
if (cumulative.length > 0 || usage.some(({ payload }) => (payload.usage.semantics === "remaining-context")))
|
|
345
|
-
tokensObservable = false;
|
|
346
|
-
}
|
|
347
|
-
const toolOperationIds = new Set(observations.flatMap((observation) => (observation.kind === "operation.started" && observation.payload.operation === "tool"
|
|
348
|
-
? [`${observation.fence.turnId}\0${observation.payload.operationId ?? observation.semanticKey}`]
|
|
349
|
-
: [])));
|
|
350
|
-
const toolCallsObservable = observations.some(({ kind, payload }) => (kind === "operation.started" && payload.operation === "tool"));
|
|
351
|
-
return Object.freeze({
|
|
352
|
-
tokens,
|
|
353
|
-
toolCalls: toolOperationIds.size,
|
|
354
|
-
tokensObservable,
|
|
355
|
-
toolCallsObservable
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
function activeLimits(policy, active, request) {
|
|
359
|
-
const checks = [
|
|
360
|
-
["home", policy.maxActiveLanes, () => true],
|
|
361
|
-
["task", policy.maxActiveLanesPerTask, (value) => value.taskId === request.taskId],
|
|
362
|
-
["work-item", policy.maxActiveLanesPerWorkItem, (value) => (request.workItemId !== undefined && value.taskId === request.taskId
|
|
363
|
-
&& value.workItemId === request.workItemId)],
|
|
364
|
-
["group", policy.maxActiveLanesPerGroup, (value) => (value.taskId === request.taskId && value.executionGroupId === request.executionGroupId)],
|
|
365
|
-
["provider", policy.maxActiveLanesPerProvider, (value) => value.providerId === request.providerId],
|
|
366
|
-
["agent", policy.maxActiveLanesPerAgent, (value) => value.agentId === request.agentId],
|
|
367
|
-
["model", policy.maxActiveLanesPerModel, (value) => modelKey(value) === modelKey(request)]
|
|
368
|
-
];
|
|
369
|
-
return checks.filter(([, limit, matches]) => active.filter(matches).length >= limit)
|
|
370
|
-
.map(([scope]) => scope);
|
|
371
|
-
}
|
|
372
|
-
function executionStageRetryLineage(group, candidates) {
|
|
373
|
-
const stage = group.stage;
|
|
374
|
-
if (stage === undefined || candidates === undefined)
|
|
375
|
-
return [group];
|
|
376
|
-
const lineage = candidates
|
|
377
|
-
.map(validateExecutionGroup)
|
|
378
|
-
.filter((candidate) => (candidate.taskId === group.taskId
|
|
379
|
-
&& candidate.purpose === "execution"
|
|
380
|
-
&& candidate.stage?.mode === stage.mode
|
|
381
|
-
&& candidate.stage.stage === stage.stage
|
|
382
|
-
&& candidate.stage.round === stage.round
|
|
383
|
-
&& candidate.stage.stageAttempt <= stage.stageAttempt
|
|
384
|
-
&& isDeepStrictEqual(candidate.stage.budget, stage.budget)
|
|
385
|
-
&& isDeepStrictEqual(candidate.stage.resources, stage.resources)));
|
|
386
|
-
if (!lineage.some(({ id }) => id === group.id))
|
|
387
|
-
lineage.push(group);
|
|
388
|
-
return lineage;
|
|
389
|
-
}
|
|
390
|
-
function validateLaneIdentity(value) {
|
|
391
|
-
requireIdentity(value.taskId, "Resource Lane Task id");
|
|
392
|
-
if (value.workItemId !== undefined)
|
|
393
|
-
requireIdentity(value.workItemId, "Resource Lane WorkItem id");
|
|
394
|
-
requireIdentity(value.executionGroupId, "Resource Lane ExecutionGroup id");
|
|
395
|
-
requireIdentity(value.executionLaneId, "Resource Lane id");
|
|
396
|
-
requireIdentity(value.providerId, "Resource Lane Provider id");
|
|
397
|
-
requireIdentity(value.agentId, "Resource Lane Agent id");
|
|
398
|
-
if (value.model !== undefined)
|
|
399
|
-
requireText(value.model, "Resource Lane model");
|
|
400
|
-
requireTimestamp(value.requestedAt, "Resource Lane request time");
|
|
401
|
-
return Object.freeze({ ...value });
|
|
402
|
-
}
|
|
403
|
-
function modelKey(value) {
|
|
404
|
-
return `${value.providerId}\0${value.model ?? "default"}`;
|
|
405
|
-
}
|
|
406
|
-
function positive(value, fallback, label) {
|
|
407
|
-
const resolved = value ?? fallback;
|
|
408
|
-
if (!Number.isSafeInteger(resolved) || resolved < 1) {
|
|
409
|
-
throw new TypeError(`${label} must be a positive integer.`);
|
|
410
|
-
}
|
|
411
|
-
return resolved;
|
|
412
|
-
}
|
|
413
|
-
function nonNegative(value, label) {
|
|
414
|
-
if (!Number.isSafeInteger(value) || value < 0)
|
|
415
|
-
throw new Error(`${label} must be a non-negative integer.`);
|
|
416
|
-
return value;
|
|
417
|
-
}
|
|
418
|
-
function percentage(value, label) {
|
|
419
|
-
if (!Number.isSafeInteger(value) || value < 0 || value > 100) {
|
|
420
|
-
throw new Error(`${label} must be an integer from 0 to 100.`);
|
|
421
|
-
}
|
|
422
|
-
return value;
|
|
423
|
-
}
|
|
424
|
-
function isUsable(status) {
|
|
425
|
-
return status === "completed";
|
|
426
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export function withYuiTurnEnvironment(launch, yuiHome, role, turn, nativeSessionId, nativeSessionRoot) {
|
|
2
|
-
return {
|
|
3
|
-
...launch,
|
|
4
|
-
env: {
|
|
5
|
-
...launch.env,
|
|
6
|
-
YUI_HOME: yuiHome,
|
|
7
|
-
YUI_TASK_ID: turn.taskId,
|
|
8
|
-
YUI_ROLE: role.name,
|
|
9
|
-
YUI_AGENT_ID: role.activeAgentId ?? role.agent ?? "",
|
|
10
|
-
YUI_TURN_ID: turn.id,
|
|
11
|
-
YUI_WORKSPACE: role.workspace,
|
|
12
|
-
...(nativeSessionRoot === undefined ? {} : { YUI_NATIVE_SESSION_ROOT: nativeSessionRoot }),
|
|
13
|
-
...(nativeSessionId === undefined ? {} : { YUI_NATIVE_SESSION_ID: nativeSessionId })
|
|
14
|
-
}
|
|
15
|
-
};
|
|
16
|
-
}
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { requireIdentity, requireText, requireTimestamp, normalizedUniqueText } from "../domain/validation.js";
|
|
3
|
-
import { validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
4
|
-
/** Dispositions that still block Task completion under `enforce`. */
|
|
5
|
-
export const BLOCKING_FINDING_DISPOSITIONS = [
|
|
6
|
-
"open",
|
|
7
|
-
"fixed-pending-review"
|
|
8
|
-
];
|
|
9
|
-
/** Dispositions a Task-control Agent may set explicitly. */
|
|
10
|
-
export const TASK_CONTROL_FINDING_DISPOSITIONS = [
|
|
11
|
-
"fixed-pending-review",
|
|
12
|
-
"verified-fixed",
|
|
13
|
-
"accepted-risk",
|
|
14
|
-
"not-actionable",
|
|
15
|
-
"superseded"
|
|
16
|
-
];
|
|
17
|
-
/** Normalizes the severity vocabularies reviewers may use to P1/P2/P3. */
|
|
18
|
-
export function normalizeReviewFindingSeverity(value) {
|
|
19
|
-
if (typeof value !== "string")
|
|
20
|
-
throw new Error("Review finding severity is required.");
|
|
21
|
-
const normalized = value.trim().toLowerCase();
|
|
22
|
-
if (normalized === "p1" || normalized === "critical" || normalized === "blocker")
|
|
23
|
-
return "p1";
|
|
24
|
-
if (normalized === "p2" || normalized === "high" || normalized === "major")
|
|
25
|
-
return "p2";
|
|
26
|
-
if (normalized === "p3"
|
|
27
|
-
|| normalized === "medium"
|
|
28
|
-
|| normalized === "moderate"
|
|
29
|
-
|| normalized === "low"
|
|
30
|
-
|| normalized === "minor"
|
|
31
|
-
|| normalized === "nit"
|
|
32
|
-
|| normalized === "info"
|
|
33
|
-
|| normalized === "style") {
|
|
34
|
-
return "p3";
|
|
35
|
-
}
|
|
36
|
-
throw new Error(`Review finding severity is invalid: ${String(value)}.`);
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Derives the stable ledger key for one reported finding. The key is
|
|
40
|
-
* deterministic for the same invariant/path/symbol so a repeated finding in a
|
|
41
|
-
* later Round updates the existing record instead of creating a duplicate.
|
|
42
|
-
*/
|
|
43
|
-
export function reviewFindingStableKey(input) {
|
|
44
|
-
const invariant = input.invariant.trim().toLowerCase();
|
|
45
|
-
const primaryPath = (input.primaryPath ?? "").trim().toLowerCase();
|
|
46
|
-
const primarySymbol = (input.primarySymbol ?? "").trim().toLowerCase();
|
|
47
|
-
const basis = [
|
|
48
|
-
invariant,
|
|
49
|
-
primaryPath,
|
|
50
|
-
primarySymbol
|
|
51
|
-
].join("|");
|
|
52
|
-
// Reports without path/symbol provenance cannot safely collapse different
|
|
53
|
-
// titles under one invariant. Use the title as the fallback identity instead
|
|
54
|
-
// of merging every such finding into a single ledger record.
|
|
55
|
-
const material = primaryPath.length === 0 && primarySymbol.length === 0
|
|
56
|
-
? `${invariant}|${input.title.trim().toLowerCase()}`
|
|
57
|
-
: basis;
|
|
58
|
-
return `rf-${createHash("sha256").update(material).digest("hex").slice(0, 16)}`;
|
|
59
|
-
}
|
|
60
|
-
export function createReviewFinding(id, taskId, input, now) {
|
|
61
|
-
const timestamp = now.toISOString();
|
|
62
|
-
return validateReviewFinding({
|
|
63
|
-
schemaVersion: 1,
|
|
64
|
-
id: requireIdentity(id, "ReviewFinding id"),
|
|
65
|
-
taskId: requireIdentity(taskId, "Task id"),
|
|
66
|
-
...(input.sourceId === undefined ? {} : { sourceId: requireIdentity(input.sourceId, "ReviewFinding source id") }),
|
|
67
|
-
stableKey: requireIdentity(input.stableKey, "ReviewFinding stable key"),
|
|
68
|
-
severity: input.severity,
|
|
69
|
-
invariant: requireText(input.invariant, "ReviewFinding invariant"),
|
|
70
|
-
title: requireText(input.title, "ReviewFinding title"),
|
|
71
|
-
affectedPaths: normalizedUniqueText(input.affectedPaths ?? [], "ReviewFinding affected path"),
|
|
72
|
-
affectedSymbols: normalizedUniqueText(input.affectedSymbols ?? [], "ReviewFinding affected symbol"),
|
|
73
|
-
evidence: normalizedUniqueText(input.evidence ?? [], "ReviewFinding evidence"),
|
|
74
|
-
firstReviewRoundId: requireIdentity(input.reviewRoundId, "ReviewFinding first ReviewRound"),
|
|
75
|
-
lastReviewRoundId: requireIdentity(input.reviewRoundId, "ReviewFinding last ReviewRound"),
|
|
76
|
-
disposition: "open",
|
|
77
|
-
createdAt: timestamp,
|
|
78
|
-
updatedAt: timestamp
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Applies one Leader disposition. Terminal dispositions
|
|
83
|
-
* (verified-fixed/accepted-risk/not-actionable/superseded) are explicit Leader
|
|
84
|
-
* decisions and stay terminal; `fixed-pending-review` may still regress to
|
|
85
|
-
* `open` when a later Round reproduces the finding.
|
|
86
|
-
*/
|
|
87
|
-
export function disposeReviewFinding(finding, input) {
|
|
88
|
-
validateReviewFinding(finding);
|
|
89
|
-
if (!TASK_CONTROL_FINDING_DISPOSITIONS.includes(input.disposition)) {
|
|
90
|
-
throw new Error(`ReviewFinding disposition is not a Leader decision: ${input.disposition}.`);
|
|
91
|
-
}
|
|
92
|
-
if (finding.disposition !== "open" && finding.disposition !== "fixed-pending-review") {
|
|
93
|
-
throw new Error(`ReviewFinding ${finding.id} is already terminal: ${finding.disposition}.`);
|
|
94
|
-
}
|
|
95
|
-
if (input.disposition === "superseded" && input.supersededBy === undefined) {
|
|
96
|
-
throw new Error(`ReviewFinding ${finding.id} supersession requires a successor stable key.`);
|
|
97
|
-
}
|
|
98
|
-
const repair = input.repair === undefined
|
|
99
|
-
? finding.repair
|
|
100
|
-
: validateReviewFindingRepair(input.repair);
|
|
101
|
-
return validateReviewFinding({
|
|
102
|
-
...finding,
|
|
103
|
-
disposition: input.disposition,
|
|
104
|
-
...(repair === undefined ? {} : { repair }),
|
|
105
|
-
dispositionBy: requireIdentity(input.by, "ReviewFinding disposition actor"),
|
|
106
|
-
...(input.note === undefined ? {} : { dispositionNote: requireText(input.note, "ReviewFinding disposition note") }),
|
|
107
|
-
...(input.supersededBy === undefined ? {} : { supersededBy: requireIdentity(input.supersededBy, "ReviewFinding successor stable key") }),
|
|
108
|
-
mergeRequired: false,
|
|
109
|
-
updatedAt: input.now.toISOString()
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Reconciles a re-detection of this finding in a later Round. A regression
|
|
114
|
-
* reopens a `fixed-pending-review` finding; a `verified-fixed` finding that
|
|
115
|
-
* reproduces also reopens (the fix did not hold). Explicit Leader decisions
|
|
116
|
-
* (accepted-risk/not-actionable/superseded) stay terminal but still get their
|
|
117
|
-
* evidence and last-Round pointer refreshed.
|
|
118
|
-
*/
|
|
119
|
-
export function redetectReviewFinding(finding, input) {
|
|
120
|
-
validateReviewFinding(finding);
|
|
121
|
-
const evidence = normalizedUniqueText([...finding.evidence, ...(input.evidence ?? [])], "ReviewFinding evidence");
|
|
122
|
-
const reopen = finding.disposition === "fixed-pending-review"
|
|
123
|
-
|| finding.disposition === "verified-fixed";
|
|
124
|
-
return validateReviewFinding({
|
|
125
|
-
...finding,
|
|
126
|
-
lastReviewRoundId: requireIdentity(input.reviewRoundId, "ReviewFinding last ReviewRound"),
|
|
127
|
-
evidence,
|
|
128
|
-
...(reopen ? { disposition: "open", repair: undefined } : {}),
|
|
129
|
-
...(reopen ? { mergeRequired: finding.mergeRequired } : {}),
|
|
130
|
-
updatedAt: input.now.toISOString()
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
/** Refreshes last-Round/evidence for a terminal finding without reopening it. */
|
|
134
|
-
export function touchReviewFinding(finding, input) {
|
|
135
|
-
validateReviewFinding(finding);
|
|
136
|
-
const evidence = normalizedUniqueText([...finding.evidence, ...(input.evidence ?? [])], "ReviewFinding evidence");
|
|
137
|
-
if (finding.lastReviewRoundId === input.reviewRoundId
|
|
138
|
-
&& evidence.length === finding.evidence.length) {
|
|
139
|
-
return finding;
|
|
140
|
-
}
|
|
141
|
-
return validateReviewFinding({
|
|
142
|
-
...finding,
|
|
143
|
-
lastReviewRoundId: requireIdentity(input.reviewRoundId, "ReviewFinding last ReviewRound"),
|
|
144
|
-
evidence,
|
|
145
|
-
updatedAt: input.now.toISOString()
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Issue 06: a Reviewer reports a previously-open finding as resolved in a
|
|
150
|
-
* later semantic Round. The claim is only accepted when the ledger already
|
|
151
|
-
* has repair refs; otherwise the old finding remains open (a changed candidate
|
|
152
|
-
* without repair evidence must not advance disposition). With repair refs,
|
|
153
|
-
* this transitions `open` to `fixed-pending-review` (the fix claim is recorded
|
|
154
|
-
* but the Leader still owns the final disposition) and refreshes
|
|
155
|
-
* evidence/last-Round for `fixed-pending-review`. Explicit Leader decisions
|
|
156
|
-
* (accepted-risk/not-actionable/superseded) and `verified-fixed` are untouched.
|
|
157
|
-
*/
|
|
158
|
-
export function resolveReviewFinding(finding, input) {
|
|
159
|
-
validateReviewFinding(finding);
|
|
160
|
-
const evidence = normalizedUniqueText([...finding.evidence, ...(input.evidence ?? [])], "ReviewFinding evidence");
|
|
161
|
-
const transition = finding.disposition === "open" && finding.repair !== undefined;
|
|
162
|
-
if (!transition
|
|
163
|
-
&& finding.lastReviewRoundId === input.reviewRoundId
|
|
164
|
-
&& evidence.length === finding.evidence.length) {
|
|
165
|
-
return finding;
|
|
166
|
-
}
|
|
167
|
-
return validateReviewFinding({
|
|
168
|
-
...finding,
|
|
169
|
-
lastReviewRoundId: requireIdentity(input.reviewRoundId, "ReviewFinding last ReviewRound"),
|
|
170
|
-
evidence,
|
|
171
|
-
...(transition ? { disposition: "fixed-pending-review" } : {}),
|
|
172
|
-
updatedAt: input.now.toISOString()
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
export function isReviewFindingBlocking(finding) {
|
|
176
|
-
return BLOCKING_FINDING_DISPOSITIONS.includes(finding.disposition)
|
|
177
|
-
&& (finding.severity === "p1" || finding.severity === "p2");
|
|
178
|
-
}
|
|
179
|
-
export function validateReviewFinding(finding) {
|
|
180
|
-
if (finding.schemaVersion !== 1) {
|
|
181
|
-
throw new Error("ReviewFinding must use schemaVersion 1.");
|
|
182
|
-
}
|
|
183
|
-
validateTaskRecordReference({ taskId: finding.taskId, localId: finding.id }, "reviewFinding");
|
|
184
|
-
requireIdentity(finding.stableKey, "ReviewFinding stable key");
|
|
185
|
-
if (finding.severity !== "p1" && finding.severity !== "p2" && finding.severity !== "p3") {
|
|
186
|
-
throw new Error(`ReviewFinding severity is invalid: ${String(finding.severity)}.`);
|
|
187
|
-
}
|
|
188
|
-
requireText(finding.invariant, "ReviewFinding invariant");
|
|
189
|
-
requireText(finding.title, "ReviewFinding title");
|
|
190
|
-
normalizedUniqueText(finding.affectedPaths, "ReviewFinding affected path");
|
|
191
|
-
normalizedUniqueText(finding.affectedSymbols, "ReviewFinding affected symbol");
|
|
192
|
-
normalizedUniqueText(finding.evidence, "ReviewFinding evidence");
|
|
193
|
-
validateTaskRecordReference({ taskId: finding.taskId, localId: finding.firstReviewRoundId }, "reviewRound");
|
|
194
|
-
validateTaskRecordReference({ taskId: finding.taskId, localId: finding.lastReviewRoundId }, "reviewRound");
|
|
195
|
-
if (![
|
|
196
|
-
"open",
|
|
197
|
-
"fixed-pending-review",
|
|
198
|
-
"verified-fixed",
|
|
199
|
-
"accepted-risk",
|
|
200
|
-
"not-actionable",
|
|
201
|
-
"superseded"
|
|
202
|
-
].includes(finding.disposition)) {
|
|
203
|
-
throw new Error(`ReviewFinding disposition is invalid: ${String(finding.disposition)}.`);
|
|
204
|
-
}
|
|
205
|
-
if (finding.repair !== undefined)
|
|
206
|
-
validateReviewFindingRepair(finding.repair);
|
|
207
|
-
if (finding.dispositionBy !== undefined) {
|
|
208
|
-
requireIdentity(finding.dispositionBy, "ReviewFinding disposition actor");
|
|
209
|
-
}
|
|
210
|
-
if (finding.dispositionNote !== undefined) {
|
|
211
|
-
requireText(finding.dispositionNote, "ReviewFinding disposition note");
|
|
212
|
-
}
|
|
213
|
-
if (finding.supersededBy !== undefined) {
|
|
214
|
-
requireIdentity(finding.supersededBy, "ReviewFinding successor stable key");
|
|
215
|
-
if (finding.disposition !== "superseded") {
|
|
216
|
-
throw new Error(`ReviewFinding ${finding.id} carries a successor but is not superseded.`);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (finding.disposition === "superseded" && finding.supersededBy === undefined) {
|
|
220
|
-
throw new Error(`ReviewFinding ${finding.id} is superseded without a successor.`);
|
|
221
|
-
}
|
|
222
|
-
requireTimestamp(finding.createdAt, "ReviewFinding createdAt");
|
|
223
|
-
requireTimestamp(finding.updatedAt, "ReviewFinding updatedAt");
|
|
224
|
-
return finding;
|
|
225
|
-
}
|
|
226
|
-
function validateReviewFindingRepair(repair) {
|
|
227
|
-
if (repair.workItemId !== undefined) {
|
|
228
|
-
requireIdentity(repair.workItemId, "ReviewFinding repair WorkItem");
|
|
229
|
-
}
|
|
230
|
-
if (repair.commit !== undefined) {
|
|
231
|
-
const commit = requireText(repair.commit, "ReviewFinding repair commit").toLowerCase();
|
|
232
|
-
if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(commit)) {
|
|
233
|
-
throw new Error("ReviewFinding repair commit is invalid.");
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (repair.verification !== undefined) {
|
|
237
|
-
requireText(repair.verification, "ReviewFinding repair verification");
|
|
238
|
-
}
|
|
239
|
-
return repair;
|
|
240
|
-
}
|