@zq-silk/yui 0.6.13 → 0.6.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -5
- package/dist/cli/commandCatalog.js +173 -57
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli.js +99 -11
- package/dist/commands/configCommands.js +521 -171
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/executionAuditCommands.js +56 -3
- package/dist/commands/projectCommands.js +504 -2
- package/dist/commands/releaseCommands.js +0 -1
- package/dist/commands/taskActor.js +17 -0
- package/dist/commands/taskBaseCommands.js +29 -0
- package/dist/commands/taskCommands.js +577 -126
- package/dist/commands/taskContextCommand.js +36 -2
- package/dist/commands/taskNextActionCommand.js +48 -3
- package/dist/commands/taskPublicationCommands.js +319 -0
- package/dist/commands/taskRoleRuntimeStatus.js +83 -59
- package/dist/commands/telemetryCommands.js +14 -13
- package/dist/config/yuiConfig.js +161 -8
- package/dist/context/sessionContextBudget.js +68 -0
- package/dist/context/wakeNotification.js +65 -0
- package/dist/controller/clientRuntime.js +2 -1
- package/dist/controller/ephemeralResourceReaper.js +2 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +183 -16
- package/dist/controller/jobSupervisor.js +5 -4
- package/dist/controller/resourceCleanupLinux.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +3 -3
- package/dist/controller/runtime.js +88 -10
- package/dist/controller/updateReconciliation.js +4 -3
- package/dist/doctor/doctor.js +26 -7
- package/dist/executor/agentConfigurationCatalog.js +18 -0
- package/dist/executor/fileRoleLaunchPlanner.js +3 -3
- package/dist/lifecycle/contextBudgetRollover.js +81 -0
- package/dist/lifecycle/exactRunTerminalization.js +11 -1
- package/dist/lifecycle/providerErrorClass.js +33 -12
- package/dist/observability/executionAudit.js +214 -6
- package/dist/output/table.js +18 -0
- package/dist/repository/gitWorkspace.js +92 -0
- package/dist/repository/project.js +218 -4
- package/dist/repository/taskBaseFreshness.js +318 -0
- package/dist/repository/taskWorkspacePreparer.js +16 -2
- package/dist/review/deltaRecheck.js +232 -0
- package/dist/review/reviewConfig.js +31 -0
- package/dist/review/reviewFindingLedger.js +5 -1
- package/dist/review/reviewRound.js +156 -1
- package/dist/run/providerRetry.js +21 -3
- package/dist/run/providerRetryConfig.js +13 -60
- package/dist/run/recoveryProjection.js +199 -0
- package/dist/runtime/builtinAgentDrivers.js +3 -0
- package/dist/runtime/builtinTranscriptUsage.js +76 -32
- package/dist/runtime/continuationManager.js +17 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/launchDiagnostics.js +154 -0
- package/dist/runtime/lifecycleReservation.js +13 -0
- package/dist/runtime/providerContinuation.js +38 -0
- package/dist/runtime/providerContinuationReconciliationService.js +1 -0
- package/dist/runtime/providerErrorCodes.js +278 -0
- package/dist/runtime/runtimeHealthPolicy.js +20 -0
- package/dist/runtime/runtimeObservation.js +1 -0
- package/dist/runtime/runtimeProjection.js +115 -23
- package/dist/runtime/tmuxAdapters.js +242 -48
- package/dist/scheduler/activeRoleRunDelivery.js +139 -3
- package/dist/scheduler/activeTaskProgress.js +4 -3
- package/dist/scheduler/leaderWakeupProcessor.js +105 -15
- package/dist/scheduler/roleRunLiveness.js +2 -1
- package/dist/scheduler/roleRunStall.js +3 -2
- package/dist/scheduler/taskWake.js +72 -0
- package/dist/scheduler/wakeReason.js +64 -0
- package/dist/scheduler/wakeupQueue.js +2 -1
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/migration/productionRegistry.js +325 -1
- package/dist/storage/sqliteSchema.js +61 -2
- package/dist/storage/sqliteStore.js +129 -2
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskStore.js +262 -5
- package/dist/storage/upgrade/recordVersions.js +6 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
- package/dist/task/completionReadiness.js +282 -0
- package/dist/task/publicationReference.js +123 -0
- package/dist/task/taskRecordReference.js +3 -1
- package/dist/telemetry/telemetryConfig.js +23 -18
- package/dist/telemetry/telemetryWiring.js +8 -8
- package/dist/tmux/tmuxManager.js +50 -9
- package/dist/web/assets/client/i18n.js +4 -0
- package/dist/web/assets/client/view.js +18 -0
- package/dist/web/webSnapshot.js +100 -10
- package/i18n/README.zh-CN.md +5 -5
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +49 -10
- package/skills/yui-operator/SKILL.md +13 -3
- package/skills/yui-worker/SKILL.md +8 -0
|
@@ -44,6 +44,7 @@ export function runTaskContextCommand(args, store) {
|
|
|
44
44
|
const reviewRounds = chronological(reader.listReviewRounds(task.id));
|
|
45
45
|
const changeSets = chronological(reader.listChangeSets(task.id));
|
|
46
46
|
const integrations = chronological(reader.listIntegrationAttempts(task.id));
|
|
47
|
+
const publications = chronological(reader.listPublicationReferences(task.id));
|
|
47
48
|
const nextActionFacts = reader.readNextActionFacts(task.id);
|
|
48
49
|
if (nextActionFacts === null) {
|
|
49
50
|
throw new Error(`Task next-action facts disappeared: ${task.id}.`);
|
|
@@ -66,6 +67,7 @@ export function runTaskContextCommand(args, store) {
|
|
|
66
67
|
reviewRounds,
|
|
67
68
|
changeSets,
|
|
68
69
|
integrations,
|
|
70
|
+
publications,
|
|
69
71
|
messages: reader.listMessages(task.id),
|
|
70
72
|
openInputRequests: inputRequests.filter((request) => request.status === "open"),
|
|
71
73
|
resolvedInputRequests: inputRequests.filter((request) => request.status !== "open"),
|
|
@@ -73,7 +75,7 @@ export function runTaskContextCommand(args, store) {
|
|
|
73
75
|
nextAction: projectNextAction(nextActionFacts)
|
|
74
76
|
};
|
|
75
77
|
});
|
|
76
|
-
const { task, execution, reviewConfig, brief, activeDecisions, milestones, roles, managedWorkspaces, roleSessionSets, coordinationMailboxes, roleSessionRecoveries, workItems, agentRuns, reviewRounds, changeSets, integrations, messages, openInputRequests, resolvedInputRequests, events, nextAction } = data;
|
|
78
|
+
const { task, execution, reviewConfig, brief, activeDecisions, milestones, roles, managedWorkspaces, roleSessionSets, coordinationMailboxes, roleSessionRecoveries, workItems, agentRuns, reviewRounds, changeSets, integrations, publications, messages, openInputRequests, resolvedInputRequests, events, nextAction } = data;
|
|
77
79
|
const timeZone = store.getConfig().timeZone;
|
|
78
80
|
const displayedActiveDecisions = activeDecisions.slice(-RECENT_RECORD_LIMIT);
|
|
79
81
|
const displayedWorkItems = currentAndRecentWorkItems(workItems);
|
|
@@ -283,6 +285,24 @@ export function runTaskContextCommand(args, store) {
|
|
|
283
285
|
? [" None."]
|
|
284
286
|
: integrations.slice(-RECENT_RECORD_LIMIT).map((integration) => (` ${integration.id} [${integration.status}/${integration.projectId}] — ${integration.targetRef}; ${integration.changeSetIds.join(", ")}`))),
|
|
285
287
|
"",
|
|
288
|
+
`Publication references (${publications.length}):`,
|
|
289
|
+
...(publications.length === 0
|
|
290
|
+
? [" None."]
|
|
291
|
+
: publications.slice(-RECENT_RECORD_LIMIT).map((reference) => [
|
|
292
|
+
` ${reference.id}: ${reference.provider}/${reference.repository}/${reference.externalId}`,
|
|
293
|
+
...(reference.title === undefined ? [] : [` Title: ${reference.title}`]),
|
|
294
|
+
...(reference.sourceBranch === undefined && reference.targetBranch === undefined
|
|
295
|
+
? []
|
|
296
|
+
: [` Branches: ${reference.sourceBranch ?? "unknown"} -> ${reference.targetBranch ?? "unknown"}`]),
|
|
297
|
+
` ${reference.state}/${reference.verification}; ${reference.localCommit ?? "unknown"} -> ${reference.remoteCommit ?? "unknown"}`,
|
|
298
|
+
...(reference.mergedAt === undefined
|
|
299
|
+
? []
|
|
300
|
+
: [` Merged: ${formatTimestamp(reference.mergedAt, timeZone)}`]),
|
|
301
|
+
...(reference.supersedes === undefined
|
|
302
|
+
? []
|
|
303
|
+
: [` Supersedes: ${reference.supersedes}`])
|
|
304
|
+
]).flat()),
|
|
305
|
+
"",
|
|
286
306
|
...recentSection("messages", messages, (message) => [
|
|
287
307
|
` ${message.id} [${taskMessageAuthorLabel(message.author)}] ${formatTimestamp(message.createdAt, timeZone)}`,
|
|
288
308
|
` ${compactText(message.body)}`
|
|
@@ -342,7 +362,21 @@ function renderWorkItemReviews(rounds) {
|
|
|
342
362
|
? [
|
|
343
363
|
` Frozen integrated Task heads: ${latest.taskCandidate?.projects
|
|
344
364
|
.map(({ projectId, commit }) => `${projectId}@${commit}`).join(", ") ?? "unavailable"}`,
|
|
345
|
-
` Task-final contract: ${latest.taskFinalReviewContract?.digest ?? "global policy"}
|
|
365
|
+
` Task-final contract: ${latest.taskFinalReviewContract?.digest ?? "global policy"}`,
|
|
366
|
+
...(latest.deltaRecheck === undefined
|
|
367
|
+
? [` Review mode: full`]
|
|
368
|
+
: [
|
|
369
|
+
` Review mode: delta-recheck (rechecks ${latest.deltaRecheck.previousReviewRoundId}@${latest.deltaRecheck.previousBaseCommit.slice(0, 12)})`,
|
|
370
|
+
` Delta: ${latest.deltaRecheck.changedFiles.length} file(s), +${latest.deltaRecheck.addedLines}/-${latest.deltaRecheck.deletedLines}, digest ${latest.deltaRecheck.diffDigest.slice(0, 12)}`,
|
|
371
|
+
...(latest.deltaRecheck.disposition === undefined
|
|
372
|
+
? []
|
|
373
|
+
: [
|
|
374
|
+
` Delta disposition: ${latest.deltaRecheck.disposition}`,
|
|
375
|
+
...(latest.deltaRecheck.escalatedToReviewRoundId === undefined
|
|
376
|
+
? []
|
|
377
|
+
: [` Escalated to: ${latest.deltaRecheck.escalatedToReviewRoundId}`])
|
|
378
|
+
])
|
|
379
|
+
])
|
|
346
380
|
]
|
|
347
381
|
: []),
|
|
348
382
|
` Review workspace: ${latest.workspace === undefined
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { taskNotFound, usageError } from "../errors/cliError.js";
|
|
2
2
|
import { projectNextAction } from "../task/nextAction.js";
|
|
3
|
+
import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
3
4
|
import { extractReviewFindings, planRepairWave } from "../task/repairWave.js";
|
|
4
5
|
/**
|
|
5
6
|
* Issue 07 (Leader convergence): read-only `yui task next-action <task>`.
|
|
@@ -8,6 +9,10 @@ import { extractReviewFindings, planRepairWave } from "../task/repairWave.js";
|
|
|
8
9
|
* the judgment that remains owned by the Leader.
|
|
9
10
|
* The command never mutates state; when the action is `route-review-findings`
|
|
10
11
|
* it also prints the minimal repair wave for the failing Review.
|
|
12
|
+
*
|
|
13
|
+
* Issue 06: when the recommended action is `complete-task`, the output also
|
|
14
|
+
* carries the full `completionReadiness` blocker list so the Leader sees every
|
|
15
|
+
* terminalization precondition before attempting `yui task complete`.
|
|
11
16
|
*/
|
|
12
17
|
export function runTaskNextActionCommand(args, store) {
|
|
13
18
|
const usage = "Task next-action usage: yui task next-action <task> [--json].";
|
|
@@ -18,12 +23,39 @@ export function runTaskNextActionCommand(args, store) {
|
|
|
18
23
|
}
|
|
19
24
|
const taskId = positionals[0].trim();
|
|
20
25
|
const data = store.transaction((reader) => {
|
|
26
|
+
// Issue 06: the lightweight facts drive the action on every call; the
|
|
27
|
+
// heavier readiness facts (full event fold, workspaces, jobs, ledger) are
|
|
28
|
+
// loaded only when the action is `complete-task`, keeping the per-turn
|
|
29
|
+
// read path cheap during execution.
|
|
21
30
|
const facts = reader.readNextActionFacts(taskId);
|
|
22
31
|
if (facts === null)
|
|
23
32
|
throw taskNotFound(taskId);
|
|
24
33
|
const action = projectNextAction(facts);
|
|
25
34
|
const repairWave = repairWaveFor(action, facts);
|
|
26
|
-
|
|
35
|
+
// Issue 12: surface pending Knowledge promotion proposals for the Task's
|
|
36
|
+
// bound Projects as a non-blocking advisory. The proposals are workflow
|
|
37
|
+
// state, not completion blockers: an Operator reviews them separately.
|
|
38
|
+
const knowledgeProposals = facts.task.projectBindings.flatMap((binding) => {
|
|
39
|
+
const project = reader.getProject(binding.projectId);
|
|
40
|
+
if (project === null)
|
|
41
|
+
return [];
|
|
42
|
+
return project.knowledgeProposals
|
|
43
|
+
.filter((proposal) => proposal.status === "pending")
|
|
44
|
+
.map((proposal) => ({
|
|
45
|
+
projectId: binding.projectId,
|
|
46
|
+
proposalId: proposal.id,
|
|
47
|
+
title: proposal.title,
|
|
48
|
+
sourceTaskId: proposal.source.taskId
|
|
49
|
+
}));
|
|
50
|
+
});
|
|
51
|
+
let completionReadiness = null;
|
|
52
|
+
if (action.kind === "complete-task") {
|
|
53
|
+
const readinessFacts = reader.readCompletionReadinessFacts(taskId);
|
|
54
|
+
if (readinessFacts === null)
|
|
55
|
+
throw taskNotFound(taskId);
|
|
56
|
+
completionReadiness = projectCompletionReadiness(readinessFacts);
|
|
57
|
+
}
|
|
58
|
+
return { action, repairWave, completionReadiness, knowledgeProposals };
|
|
27
59
|
});
|
|
28
60
|
if (asJson) {
|
|
29
61
|
return {
|
|
@@ -34,7 +66,7 @@ export function runTaskNextActionCommand(args, store) {
|
|
|
34
66
|
}
|
|
35
67
|
return {
|
|
36
68
|
kind: "output",
|
|
37
|
-
output: renderNextAction(data.action, data.repairWave),
|
|
69
|
+
output: renderNextAction(data.action, data.repairWave, data.completionReadiness, data.knowledgeProposals),
|
|
38
70
|
data
|
|
39
71
|
};
|
|
40
72
|
}
|
|
@@ -52,7 +84,7 @@ function repairWaveFor(action, facts) {
|
|
|
52
84
|
return null;
|
|
53
85
|
return planRepairWave(round.id, findings);
|
|
54
86
|
}
|
|
55
|
-
function renderNextAction(action, repairWave) {
|
|
87
|
+
function renderNextAction(action, repairWave, completionReadiness, knowledgeProposals) {
|
|
56
88
|
const lines = [
|
|
57
89
|
`Task: ${action.taskId}`,
|
|
58
90
|
`Next action: ${action.kind}`,
|
|
@@ -91,10 +123,23 @@ function renderNextAction(action, repairWave) {
|
|
|
91
123
|
...action.conflicts.map((conflict) => ` ${conflict.kind}: ${conflict.id}`)
|
|
92
124
|
])
|
|
93
125
|
];
|
|
126
|
+
if (completionReadiness !== null) {
|
|
127
|
+
if (completionReadiness.ready) {
|
|
128
|
+
lines.push("Completion readiness: ready (no blockers)");
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
lines.push(`Completion readiness: ${completionReadiness.blockers.length} blocker(s)`, ...completionReadiness.blockers.map((blocker) => ` ${blocker.code} (${blocker.ref.kind} ${blocker.ref.id}): ${blocker.reason}`
|
|
132
|
+
+ ` — fix: ${blocker.fix}`));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
94
135
|
if (repairWave !== null) {
|
|
95
136
|
lines.push(`Repair wave (${repairWave.openFindingCount} open finding(s), ${repairWave.groups.length} group(s)):`, ...repairWave.groups.map((group) => ` ${group.id}: findings ${group.findingIds.join(", ")}`
|
|
96
137
|
+ ` — paths ${group.paths.length === 0 ? "none" : group.paths.join(", ")}`
|
|
97
138
|
+ ` — ${group.reason}`));
|
|
98
139
|
}
|
|
140
|
+
if (knowledgeProposals.length > 0) {
|
|
141
|
+
lines.push(`Knowledge proposals (non-blocking): ${knowledgeProposals.length} pending`, ...knowledgeProposals.map((proposal) => ` ${proposal.projectId}/${proposal.proposalId}: ${proposal.title}`
|
|
142
|
+
+ ` — review: yui project knowledge proposals list ${proposal.projectId}`));
|
|
143
|
+
}
|
|
99
144
|
return `${lines.join("\n")}\n`;
|
|
100
145
|
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { taskActor } from "./taskActor.js";
|
|
3
|
+
import { dataError, taskNotFound, usageError } from "../errors/cliError.js";
|
|
4
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
5
|
+
import { defaultTableWidth, renderTable } from "../output/table.js";
|
|
6
|
+
import { formatTimestamp } from "../output/timePresentation.js";
|
|
7
|
+
import { resolveProject } from "../repository/project.js";
|
|
8
|
+
import { createPublicationReference } from "../task/publicationReference.js";
|
|
9
|
+
import { resolveTaskRecordReference } from "../task/taskRecordReference.js";
|
|
10
|
+
const PROVIDERS = new Set(["github", "gitlab"]);
|
|
11
|
+
const EXTERNAL_KINDS = new Set(["pull-request", "merge-request"]);
|
|
12
|
+
const STATES = new Set(["open", "merged", "closed"]);
|
|
13
|
+
export function runPublicationCommand(args, store, options) {
|
|
14
|
+
const [command, ...rest] = args;
|
|
15
|
+
switch (command) {
|
|
16
|
+
case "add": return addPublication(rest, store, options);
|
|
17
|
+
case "list": return listPublications(rest, store);
|
|
18
|
+
case "show": return showPublication(rest, store);
|
|
19
|
+
default:
|
|
20
|
+
throw usageError(command === undefined
|
|
21
|
+
? "Task publication command is required."
|
|
22
|
+
: `Unknown command: task publication ${command}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function addPublication(args, store, options) {
|
|
26
|
+
const usage = "Task publication add usage: yui task publication add <task> --project <project> --provider <github|gitlab> --repository <owner/name> --kind <pull-request|merge-request> --id <external-id> [--url <url>] [--title <text>] [--source-branch <branch>] [--target-branch <branch>] [--local-commit <sha>] [--remote-commit <sha>] [--state <open|merged|closed>] [--reported|--verified] [--evidence <text>] [--supersede <publication-id>] [--merged-at <iso-timestamp>].";
|
|
27
|
+
const parsed = parseAddArgs(args, usage);
|
|
28
|
+
const task = requireTask(store, parsed.positionals[0]);
|
|
29
|
+
const projectReference = requiredOption(parsed.options, "--project");
|
|
30
|
+
const project = resolveProject(store.listProjects(), projectReference);
|
|
31
|
+
if (project === null)
|
|
32
|
+
throw usageError(`Project not found: ${projectReference}.`, usage);
|
|
33
|
+
if (!task.projectBindings.some((binding) => binding.projectId === project.id)) {
|
|
34
|
+
throw usageError(`Project is not bound to Task ${task.id}: ${projectReference}.`, usage);
|
|
35
|
+
}
|
|
36
|
+
const actor = taskActor(options.environment, task.id);
|
|
37
|
+
if (actor === "leader") {
|
|
38
|
+
throw usageError("A managed Task Leader cannot record external publication evidence; "
|
|
39
|
+
+ "use a user or global Operator session.", usage);
|
|
40
|
+
}
|
|
41
|
+
const state = enumOption(parsed.options, "--state", STATES, usage) ?? "open";
|
|
42
|
+
const verification = parseVerification(parsed, usage);
|
|
43
|
+
const input = {
|
|
44
|
+
projectId: project.id,
|
|
45
|
+
provider: enumOption(parsed.options, "--provider", PROVIDERS, usage),
|
|
46
|
+
repository: requiredOption(parsed.options, "--repository"),
|
|
47
|
+
externalKind: enumOption(parsed.options, "--kind", EXTERNAL_KINDS, usage),
|
|
48
|
+
externalId: requiredOption(parsed.options, "--id"),
|
|
49
|
+
...(parsed.options.has("--url") ? { externalUrl: requiredOption(parsed.options, "--url") } : {}),
|
|
50
|
+
...(parsed.options.has("--title") ? { title: requiredOption(parsed.options, "--title") } : {}),
|
|
51
|
+
...(parsed.options.has("--source-branch")
|
|
52
|
+
? { sourceBranch: requiredOption(parsed.options, "--source-branch") }
|
|
53
|
+
: {}),
|
|
54
|
+
...(parsed.options.has("--target-branch")
|
|
55
|
+
? { targetBranch: requiredOption(parsed.options, "--target-branch") }
|
|
56
|
+
: {}),
|
|
57
|
+
...(parsed.options.has("--local-commit")
|
|
58
|
+
? { localCommit: requiredOption(parsed.options, "--local-commit") }
|
|
59
|
+
: {}),
|
|
60
|
+
...(parsed.options.has("--remote-commit")
|
|
61
|
+
? { remoteCommit: requiredOption(parsed.options, "--remote-commit") }
|
|
62
|
+
: {}),
|
|
63
|
+
state,
|
|
64
|
+
verification,
|
|
65
|
+
...(parsed.options.has("--evidence")
|
|
66
|
+
? { evidence: requiredOption(parsed.options, "--evidence") }
|
|
67
|
+
: {}),
|
|
68
|
+
...(parsed.options.has("--supersede")
|
|
69
|
+
? { supersedes: requiredOption(parsed.options, "--supersede") }
|
|
70
|
+
: {}),
|
|
71
|
+
...(parsed.options.has("--merged-at")
|
|
72
|
+
? { mergedAt: requiredOption(parsed.options, "--merged-at") }
|
|
73
|
+
: {}),
|
|
74
|
+
recordedBy: actor,
|
|
75
|
+
source: "manual"
|
|
76
|
+
};
|
|
77
|
+
const externalKey = `${input.provider}/${input.repository}/${input.externalId}`;
|
|
78
|
+
const now = clock(options);
|
|
79
|
+
const result = store.transaction((tx) => {
|
|
80
|
+
const existing = tx.findPublicationReferenceByExternalKey(externalKey);
|
|
81
|
+
if (existing !== null) {
|
|
82
|
+
if (existing.taskId !== task.id) {
|
|
83
|
+
throw dataError(`Publication external identity already belongs to Task ${existing.taskId}: ${externalKey}.`);
|
|
84
|
+
}
|
|
85
|
+
const semantic = createPublicationReference(existing.id, task.id, input.supersedes === existing.id
|
|
86
|
+
? {
|
|
87
|
+
projectId: input.projectId,
|
|
88
|
+
provider: input.provider,
|
|
89
|
+
repository: input.repository,
|
|
90
|
+
externalKind: input.externalKind,
|
|
91
|
+
externalId: input.externalId,
|
|
92
|
+
...(input.externalUrl === undefined ? {} : { externalUrl: input.externalUrl }),
|
|
93
|
+
...(input.title === undefined ? {} : { title: input.title }),
|
|
94
|
+
...(input.sourceBranch === undefined ? {} : { sourceBranch: input.sourceBranch }),
|
|
95
|
+
...(input.targetBranch === undefined ? {} : { targetBranch: input.targetBranch }),
|
|
96
|
+
...(input.localCommit === undefined ? {} : { localCommit: input.localCommit }),
|
|
97
|
+
...(input.remoteCommit === undefined ? {} : { remoteCommit: input.remoteCommit }),
|
|
98
|
+
state: input.state,
|
|
99
|
+
verification: input.verification,
|
|
100
|
+
...(input.evidence === undefined ? {} : { evidence: input.evidence }),
|
|
101
|
+
...(input.mergedAt === undefined ? {} : { mergedAt: input.mergedAt }),
|
|
102
|
+
recordedBy: input.recordedBy,
|
|
103
|
+
source: input.source
|
|
104
|
+
}
|
|
105
|
+
: input, new Date(existing.createdAt));
|
|
106
|
+
if (isDeepStrictEqual(semantic, existing)) {
|
|
107
|
+
return { reference: existing, idempotent: true };
|
|
108
|
+
}
|
|
109
|
+
if (input.supersedes === undefined || input.supersedes !== existing.id) {
|
|
110
|
+
throw usageError(`Publication ${externalKey} conflicts with current record ${existing.id}; `
|
|
111
|
+
+ "use --supersede with that id to append corrected evidence.", usage);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (input.supersedes !== undefined) {
|
|
115
|
+
throw usageError(`Publication supersede target has a different external identity: ${input.supersedes}.`, usage);
|
|
116
|
+
}
|
|
117
|
+
const reference = createPublicationReference(tx.nextPublicationReferenceId(task.id), task.id, input, now);
|
|
118
|
+
tx.savePublicationReference(task.id, reference);
|
|
119
|
+
recordTaskEvent(tx, task.id, "publication.recorded", {
|
|
120
|
+
publicationId: reference.id,
|
|
121
|
+
provider: reference.provider,
|
|
122
|
+
repository: reference.repository,
|
|
123
|
+
externalKind: reference.externalKind,
|
|
124
|
+
externalId: reference.externalId,
|
|
125
|
+
state: reference.state,
|
|
126
|
+
verification: reference.verification
|
|
127
|
+
}, now);
|
|
128
|
+
return { reference, idempotent: false };
|
|
129
|
+
});
|
|
130
|
+
return output(result.idempotent
|
|
131
|
+
? `Publication reference already recorded: ${result.reference.id}.\n`
|
|
132
|
+
: `Recorded publication ${result.reference.id} (${result.reference.provider}/${result.reference.repository}/${result.reference.externalKind}/${result.reference.externalId}) for ${result.reference.taskId}.\n`, result.reference);
|
|
133
|
+
}
|
|
134
|
+
function listPublications(args, store) {
|
|
135
|
+
const usage = "Task publication list usage: yui task publication list <task>.";
|
|
136
|
+
const parsed = parseTail(args, new Set(), usage);
|
|
137
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
138
|
+
const task = requireTask(store, parsed.positionals[0]);
|
|
139
|
+
const references = store.listPublicationReferences(task.id);
|
|
140
|
+
if (references.length === 0) {
|
|
141
|
+
return output("No publication references found.\n", []);
|
|
142
|
+
}
|
|
143
|
+
const rendered = `${renderTable(`Publication references: ${task.id}`, [
|
|
144
|
+
{ header: "ID", minWidth: 12, maxWidth: 22 },
|
|
145
|
+
{ header: "External", minWidth: 16, maxWidth: 40 },
|
|
146
|
+
{ header: "Title", minWidth: 16, maxWidth: 40 },
|
|
147
|
+
{ header: "State", minWidth: 7, maxWidth: 10 },
|
|
148
|
+
{ header: "Verification", minWidth: 11, maxWidth: 14 },
|
|
149
|
+
{ header: "Lineage", minWidth: 18, maxWidth: 58 },
|
|
150
|
+
{ header: "Supersedes", minWidth: 12, maxWidth: 22 }
|
|
151
|
+
], references.map((reference) => [
|
|
152
|
+
reference.id,
|
|
153
|
+
`${reference.provider}/${reference.repository}/${reference.externalId}`,
|
|
154
|
+
reference.title ?? "-",
|
|
155
|
+
reference.state,
|
|
156
|
+
reference.verification,
|
|
157
|
+
lineageSummary(reference),
|
|
158
|
+
reference.supersedes ?? "-"
|
|
159
|
+
]), defaultTableWidth())}\n`;
|
|
160
|
+
return output(rendered, references);
|
|
161
|
+
}
|
|
162
|
+
function showPublication(args, store) {
|
|
163
|
+
const usage = "Task publication show usage: yui task publication show (<task>/<publication-id> | <task> <publication-id>).";
|
|
164
|
+
const parsed = parseTail(args, new Set(), usage);
|
|
165
|
+
if (parsed.positionals.length !== 1 && parsed.positionals.length !== 2) {
|
|
166
|
+
throw usageError(usage);
|
|
167
|
+
}
|
|
168
|
+
const referenceId = parsed.positionals.length === 1
|
|
169
|
+
? resolveTaskRecordReference(parsed.positionals[0], {
|
|
170
|
+
kind: "publicationReference",
|
|
171
|
+
label: "Publication reference"
|
|
172
|
+
})
|
|
173
|
+
: resolveTaskRecordReference(`${parsed.positionals[0]}/${parsed.positionals[1]}`, { kind: "publicationReference", label: "Publication reference" });
|
|
174
|
+
requireTask(store, referenceId.taskId);
|
|
175
|
+
const reference = store.getPublicationReference(referenceId.taskId, referenceId.localId);
|
|
176
|
+
if (reference === null) {
|
|
177
|
+
throw dataError(`Publication reference not found: ${referenceId.taskId}/${referenceId.localId}.`);
|
|
178
|
+
}
|
|
179
|
+
return output(renderPublication(reference, store.getConfig().timeZone), reference);
|
|
180
|
+
}
|
|
181
|
+
function renderPublication(reference, timeZone) {
|
|
182
|
+
return [
|
|
183
|
+
`Publication: ${reference.id}`,
|
|
184
|
+
`Task: ${reference.taskId}`,
|
|
185
|
+
`Project: ${reference.projectId}`,
|
|
186
|
+
`External: ${reference.provider}/${reference.repository}/${reference.externalId} (${reference.externalKind})`,
|
|
187
|
+
...(reference.externalUrl === undefined ? [] : [`URL: ${reference.externalUrl}`]),
|
|
188
|
+
...(reference.title === undefined ? [] : [`Title: ${reference.title}`]),
|
|
189
|
+
...(reference.sourceBranch === undefined && reference.targetBranch === undefined
|
|
190
|
+
? []
|
|
191
|
+
: [`Branches: ${reference.sourceBranch ?? "unknown"} -> ${reference.targetBranch ?? "unknown"}`]),
|
|
192
|
+
`State: ${reference.state}`,
|
|
193
|
+
`Verification: ${reference.verification}`,
|
|
194
|
+
`Lineage: ${reference.localCommit ?? "unknown"} -> ${reference.externalId} -> ${reference.remoteCommit ?? "unknown"}`,
|
|
195
|
+
...(reference.mergedAt === undefined
|
|
196
|
+
? []
|
|
197
|
+
: [`Merged: ${formatTimestamp(reference.mergedAt, timeZone)}`]),
|
|
198
|
+
...(reference.evidence === undefined ? [] : [`Evidence: ${reference.evidence}`]),
|
|
199
|
+
...(reference.supersedes === undefined ? [] : [`Supersedes: ${reference.supersedes}`]),
|
|
200
|
+
`Recorded by: ${reference.recordedBy}`,
|
|
201
|
+
`Source: ${reference.source}`,
|
|
202
|
+
`Created: ${formatTimestamp(reference.createdAt, timeZone)}`
|
|
203
|
+
].join("\n").concat("\n");
|
|
204
|
+
}
|
|
205
|
+
function lineageSummary(reference) {
|
|
206
|
+
const local = reference.localCommit ?? "unknown";
|
|
207
|
+
const remote = reference.remoteCommit ?? "unknown";
|
|
208
|
+
return `${local.slice(0, 12)} -> ${reference.externalId} -> ${remote.slice(0, 12)}`;
|
|
209
|
+
}
|
|
210
|
+
function parseVerification(parsed, usage) {
|
|
211
|
+
if (parsed.flags.has("--reported") && parsed.flags.has("--verified")) {
|
|
212
|
+
throw usageError("--reported and --verified are mutually exclusive.", usage);
|
|
213
|
+
}
|
|
214
|
+
return parsed.flags.has("--verified") ? "verified" : "reported";
|
|
215
|
+
}
|
|
216
|
+
function enumOption(options, name, allowed, usage) {
|
|
217
|
+
if (!options.has(name))
|
|
218
|
+
return undefined;
|
|
219
|
+
const value = requiredOption(options, name);
|
|
220
|
+
if (!allowed.has(value)) {
|
|
221
|
+
throw usageError(`${name} must be one of: ${[...allowed].join(", ")}.`, usage);
|
|
222
|
+
}
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
function requireTask(store, taskId) {
|
|
226
|
+
const id = requiredText(taskId, "Task id");
|
|
227
|
+
const task = store.getTask(id);
|
|
228
|
+
if (task === null)
|
|
229
|
+
throw taskNotFound(id);
|
|
230
|
+
return task;
|
|
231
|
+
}
|
|
232
|
+
function recordTaskEvent(store, taskId, type, payload, now) {
|
|
233
|
+
store.saveEvent(taskId, createTaskEvent(store.nextEventId(taskId), taskId, type, payload, now));
|
|
234
|
+
}
|
|
235
|
+
function requiredOption(options, name) {
|
|
236
|
+
return requiredText(options.get(name), name);
|
|
237
|
+
}
|
|
238
|
+
function requiredText(value, label) {
|
|
239
|
+
const normalized = value?.trim();
|
|
240
|
+
if (normalized === undefined || normalized.length === 0) {
|
|
241
|
+
throw usageError(`${label} is required.`);
|
|
242
|
+
}
|
|
243
|
+
return normalized;
|
|
244
|
+
}
|
|
245
|
+
function clock(options) {
|
|
246
|
+
return options.now?.() ?? new Date();
|
|
247
|
+
}
|
|
248
|
+
function output(value, data) {
|
|
249
|
+
return data === undefined
|
|
250
|
+
? { kind: "output", output: value }
|
|
251
|
+
: { kind: "output", output: value, data };
|
|
252
|
+
}
|
|
253
|
+
function exactPositionals(values, count, usage) {
|
|
254
|
+
if (values.length !== count || values.some((value) => value.trim().length === 0)) {
|
|
255
|
+
throw usageError(usage);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function parseAddArgs(args, usage) {
|
|
259
|
+
const valueOptions = new Set([
|
|
260
|
+
"--project", "--provider", "--repository", "--kind", "--id", "--url",
|
|
261
|
+
"--title", "--source-branch", "--target-branch", "--local-commit",
|
|
262
|
+
"--remote-commit", "--state", "--evidence", "--supersede", "--merged-at"
|
|
263
|
+
]);
|
|
264
|
+
const flags = new Set(["--reported", "--verified"]);
|
|
265
|
+
const positionals = [];
|
|
266
|
+
const options = new Map();
|
|
267
|
+
const seenFlags = new Set();
|
|
268
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
269
|
+
const value = args[index];
|
|
270
|
+
if (!value.startsWith("--")) {
|
|
271
|
+
positionals.push(value);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (flags.has(value)) {
|
|
275
|
+
if (seenFlags.has(value)) {
|
|
276
|
+
throw usageError(`Flag may only be specified once: ${value}.`, usage);
|
|
277
|
+
}
|
|
278
|
+
seenFlags.add(value);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (!valueOptions.has(value)) {
|
|
282
|
+
throw usageError(`Unsupported option: ${value}.`, usage);
|
|
283
|
+
}
|
|
284
|
+
if (options.has(value)) {
|
|
285
|
+
throw usageError(`Option may only be specified once: ${value}.`, usage);
|
|
286
|
+
}
|
|
287
|
+
const optionValue = args[index + 1];
|
|
288
|
+
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
289
|
+
throw usageError(`${value} is required.`, usage);
|
|
290
|
+
}
|
|
291
|
+
options.set(value, optionValue);
|
|
292
|
+
index += 1;
|
|
293
|
+
}
|
|
294
|
+
return { positionals, options, flags: seenFlags };
|
|
295
|
+
}
|
|
296
|
+
function parseTail(args, valueOptions, usage) {
|
|
297
|
+
const positionals = [];
|
|
298
|
+
const options = new Map();
|
|
299
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
300
|
+
const value = args[index];
|
|
301
|
+
if (!value.startsWith("--")) {
|
|
302
|
+
positionals.push(value);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (!valueOptions.has(value)) {
|
|
306
|
+
throw usageError(`Unsupported option: ${value}.`, usage);
|
|
307
|
+
}
|
|
308
|
+
if (options.has(value)) {
|
|
309
|
+
throw usageError(`Option may only be specified once: ${value}.`, usage);
|
|
310
|
+
}
|
|
311
|
+
const optionValue = args[index + 1];
|
|
312
|
+
if (optionValue === undefined || optionValue.startsWith("--")) {
|
|
313
|
+
throw usageError(`${value} is required.`, usage);
|
|
314
|
+
}
|
|
315
|
+
options.set(value, optionValue);
|
|
316
|
+
index += 1;
|
|
317
|
+
}
|
|
318
|
+
return { positionals, options };
|
|
319
|
+
}
|