@fitlab-ai/agent-infra 0.11.9 → 0.11.11
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/dist/bin/internal-cli.js +0 -6
- package/dist/lib/agent-clients/adapter.js +20 -0
- package/dist/lib/agent-clients/adapters/codex-sandbox.js +13 -0
- package/dist/lib/agent-clients/adapters/codex.js +4 -0
- package/dist/lib/git/workflow.js +1 -1
- package/dist/lib/internal/cli-route-inventory.js +0 -3
- package/dist/lib/internal/manual-validation.js +2 -2
- package/dist/lib/internal/platform-pr.js +4 -5
- package/dist/lib/internal/sandbox-control.js +30 -5
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-create.js +15 -1
- package/dist/lib/internal/task-finalization.js +14 -25
- package/dist/lib/internal/task-operation-registry.js +6 -141
- package/dist/lib/internal/task-platform-recovery.js +9 -4
- package/dist/lib/platform/completion-backfill.js +2 -1
- package/dist/lib/platform/context.js +4 -2
- package/dist/lib/platform/github-client.js +20 -17
- package/dist/lib/platform/github-provider.js +42 -22
- package/dist/lib/platform/github-release-notes.js +4 -2
- package/dist/lib/platform/in-label-sync.js +2 -1
- package/dist/lib/platform/issue-comments.js +13 -9
- package/dist/lib/platform/operation-coordinator.js +1 -1
- package/dist/lib/platform/pr-review.js +2 -1
- package/dist/lib/platform/pr-summary.js +45 -8
- package/dist/lib/platform/provider-bridge.js +4 -5
- package/dist/lib/platform/provider-contract.d.ts +3 -6
- package/dist/lib/platform/provider-contract.js +3 -15
- package/dist/lib/platform/provider-validation.js +9 -4
- package/dist/lib/platform/pull-requests.js +83 -16
- package/dist/lib/platform/verification-sync.js +17 -14
- package/dist/lib/sandbox/commands/create.js +18 -2
- package/dist/lib/sandbox/commands/enter.js +47 -33
- package/dist/lib/sandbox/control/client.js +99 -17
- package/dist/lib/sandbox/control/executor.js +12 -14
- package/dist/lib/sandbox/control/host-recovery.js +218 -0
- package/dist/lib/sandbox/control/protocol.js +2 -3
- package/dist/lib/sandbox/control/server.js +42 -2
- package/dist/lib/sandbox/environment.js +150 -0
- package/dist/lib/sandbox/recovery.js +8 -0
- package/dist/lib/sandbox/removal.js +0 -1
- package/dist/lib/sandbox/workspace-identity.js +0 -14
- package/dist/lib/sandbox/workspace-view.js +33 -6
- package/dist/lib/task/artifact-command.js +18 -3
- package/dist/lib/task/artifact-lifecycle.js +121 -63
- package/dist/lib/task/artifact-selection.js +1 -1
- package/dist/lib/task/capabilities.js +59 -22
- package/dist/lib/task/commands/status.js +0 -27
- package/dist/lib/task/commit-identity.js +22 -7
- package/dist/lib/task/commit-intent.js +67 -26
- package/dist/lib/task/commit-operation.js +19 -8
- package/dist/lib/task/completion-facts.js +32 -10
- package/dist/lib/task/control-authority.js +2 -6
- package/dist/lib/task/current-contract.js +1 -5
- package/dist/lib/task/events.js +9 -275
- package/dist/lib/task/execution-inputs.js +14 -0
- package/dist/lib/task/finalization-envelope.js +4 -0
- package/dist/lib/task/finalization.js +43 -86
- package/dist/lib/task/frontmatter.js +25 -2
- package/dist/lib/task/ledger-intents.js +6 -8
- package/dist/lib/task/orchestration.js +11 -10
- package/dist/lib/task/platform-operation-journal.js +29 -10
- package/dist/lib/task/platform-operation-recovery.js +109 -35
- package/dist/lib/task/resolve-ref.js +8 -3
- package/dist/lib/task/review-fingerprint.js +4 -26
- package/dist/lib/task/verification-engine.js +95 -6
- package/dist/lib/task/write.js +0 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +22 -0
- package/lib/agent-clients/adapters/codex-sandbox.ts +13 -0
- package/lib/agent-clients/adapters/codex.ts +4 -0
- package/lib/git/workflow.ts +1 -1
- package/lib/internal/cli-route-inventory.ts +0 -2
- package/lib/internal/manual-validation.ts +2 -2
- package/lib/internal/platform-pr.ts +3 -4
- package/lib/internal/sandbox-control.ts +39 -6
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-create.ts +15 -2
- package/lib/internal/task-finalization.ts +16 -26
- package/lib/internal/task-operation-registry.ts +6 -153
- package/lib/internal/task-platform-recovery.ts +9 -4
- package/lib/platform/completion-backfill.ts +2 -1
- package/lib/platform/context.ts +8 -4
- package/lib/platform/github-client.ts +24 -20
- package/lib/platform/github-provider.ts +47 -26
- package/lib/platform/github-release-notes.ts +5 -3
- package/lib/platform/in-label-sync.ts +2 -1
- package/lib/platform/issue-comments.ts +14 -9
- package/lib/platform/operation-coordinator.ts +1 -1
- package/lib/platform/pr-review.ts +2 -1
- package/lib/platform/pr-summary.ts +43 -7
- package/lib/platform/provider-bridge.ts +10 -9
- package/lib/platform/provider-contract.ts +6 -21
- package/lib/platform/provider-loader.ts +7 -6
- package/lib/platform/provider-validation.ts +30 -8
- package/lib/platform/pull-requests.ts +78 -15
- package/lib/platform/repository-metadata.ts +3 -2
- package/lib/platform/security-alerts.ts +1 -1
- package/lib/platform/types.ts +2 -0
- package/lib/platform/verification-sync.ts +18 -14
- package/lib/sandbox/commands/create.ts +24 -2
- package/lib/sandbox/commands/enter.ts +42 -39
- package/lib/sandbox/control/client.ts +107 -19
- package/lib/sandbox/control/executor.ts +13 -15
- package/lib/sandbox/control/host-recovery.ts +195 -0
- package/lib/sandbox/control/protocol.ts +4 -5
- package/lib/sandbox/control/server.ts +42 -1
- package/lib/sandbox/environment.ts +162 -0
- package/lib/sandbox/recovery.ts +10 -0
- package/lib/sandbox/removal.ts +2 -10
- package/lib/sandbox/tool-types.ts +1 -0
- package/lib/sandbox/workspace-identity.ts +0 -26
- package/lib/sandbox/workspace-view.ts +33 -8
- package/lib/task/artifact-command.ts +19 -3
- package/lib/task/artifact-lifecycle.ts +102 -53
- package/lib/task/artifact-selection.ts +1 -1
- package/lib/task/capabilities.ts +52 -26
- package/lib/task/commands/status.ts +0 -31
- package/lib/task/commit-identity.ts +24 -7
- package/lib/task/commit-intent.ts +58 -24
- package/lib/task/commit-operation.ts +17 -8
- package/lib/task/completion-facts.ts +31 -9
- package/lib/task/control-authority.ts +1 -8
- package/lib/task/current-contract.ts +1 -6
- package/lib/task/events.ts +9 -248
- package/lib/task/execution-inputs.ts +25 -0
- package/lib/task/finalization-envelope.ts +21 -0
- package/lib/task/finalization.ts +40 -82
- package/lib/task/frontmatter.ts +26 -2
- package/lib/task/ledger-intents.ts +6 -8
- package/lib/task/orchestration.ts +11 -17
- package/lib/task/platform-operation-journal.ts +47 -13
- package/lib/task/platform-operation-recovery.ts +102 -37
- package/lib/task/resolve-ref.ts +8 -3
- package/lib/task/review-fingerprint.ts +4 -26
- package/lib/task/verification-engine.ts +103 -9
- package/lib/task/write.ts +0 -19
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/commit-and-pr.en.md +5 -5
- package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
- package/templates/.agents/rules/decision-qualification.en.md +1 -1
- package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +1 -1
- package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.en.md +19 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +19 -0
- package/templates/.agents/rules/review-handshake.en.md +3 -0
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -0
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -0
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -0
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -0
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -0
- package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +27 -10
- package/templates/.agents/skills/code-task/reference/branch-management.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +2 -0
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +2 -0
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +5 -16
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
- package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +15 -12
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
- package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/reference/branch-strategy.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +3 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -4
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test/SKILL.en.md +4 -1
- package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/test-integration/SKILL.en.md +5 -2
- package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +5 -2
- package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +0 -12
- package/templates/.github/workflows/pr-label.yml +1 -2
- package/dist/lib/internal/task-invalidation.js +0 -63
- package/dist/lib/task/finalization-handoff.js +0 -93
- package/dist/lib/task/intermediate-cleanup.js +0 -327
- package/dist/lib/task/invalidation-command.js +0 -88
- package/dist/lib/task/invalidation.js +0 -242
- package/lib/internal/task-invalidation.ts +0 -41
- package/lib/task/finalization-handoff.ts +0 -105
- package/lib/task/intermediate-cleanup.ts +0 -393
- package/lib/task/invalidation-command.ts +0 -88
- package/lib/task/invalidation.ts +0 -313
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
import { inspectOwnedPath } from '../owned-path.js';
|
|
2
|
-
import { taskFinalizationReceiptState } from './finalization-state.js';
|
|
3
|
-
import { withTaskExecutionLock } from './task-execution-lock.js';
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { checkpointCommitMatches } from './commit-identity.js';
|
|
7
|
-
import { readCheckpointIntent } from './commit-intent.js';
|
|
8
|
-
import { parseTaskFrontmatter } from './frontmatter.js';
|
|
9
|
-
import { readTaskFinalizationReceipt } from './finalization.js';
|
|
10
|
-
import { enumerateAllTaskDirs } from './resolve-ref.js';
|
|
11
|
-
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
12
|
-
const COMMIT_INTENT_RE = /^(TASK-\d{8}-\d{6})\.json$/;
|
|
13
|
-
const AUXILIARY_ROOTS = ['.task-commit-intents'];
|
|
14
|
-
function auxiliaryRoot(repoRoot, name) {
|
|
15
|
-
return path.join(repoRoot, '.agents', 'workspace', name);
|
|
16
|
-
}
|
|
17
|
-
function taskRoot(repoRoot) {
|
|
18
|
-
return path.join(repoRoot, '.agents', 'workspace');
|
|
19
|
-
}
|
|
20
|
-
function fileIdentity(stat) {
|
|
21
|
-
return {
|
|
22
|
-
dev: String(stat.dev),
|
|
23
|
-
ino: String(stat.ino),
|
|
24
|
-
size: stat.size,
|
|
25
|
-
mtimeMs: stat.mtimeMs
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
function sameFileIdentity(left, right) {
|
|
29
|
-
return left.dev === right.dev && left.ino === right.ino
|
|
30
|
-
&& left.size === right.size && left.mtimeMs === right.mtimeMs;
|
|
31
|
-
}
|
|
32
|
-
function errorCode(error) {
|
|
33
|
-
const code = error?.code;
|
|
34
|
-
return typeof code === 'string' ? code : null;
|
|
35
|
-
}
|
|
36
|
-
function safeLstat(root, target, expect) {
|
|
37
|
-
const stat = inspectOwnedPath(root, target, expect);
|
|
38
|
-
return stat ? fileIdentity(stat) : null;
|
|
39
|
-
}
|
|
40
|
-
function inspectTaskRecords(repoRoot, taskIds) {
|
|
41
|
-
const records = new Map();
|
|
42
|
-
for (const entry of enumerateAllTaskDirs(repoRoot)) {
|
|
43
|
-
if (taskIds && !taskIds.has(entry.taskId))
|
|
44
|
-
continue;
|
|
45
|
-
const taskMdPath = path.join(entry.taskDir, 'task.md');
|
|
46
|
-
if (safeLstat(taskRoot(repoRoot), taskMdPath, 'file') === null)
|
|
47
|
-
continue;
|
|
48
|
-
const content = fs.readFileSync(taskMdPath, 'utf8');
|
|
49
|
-
records.set(entry.taskId, {
|
|
50
|
-
taskId: entry.taskId,
|
|
51
|
-
taskDir: entry.taskDir,
|
|
52
|
-
state: entry.state,
|
|
53
|
-
frontmatter: parseTaskFrontmatter(content)
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return records;
|
|
57
|
-
}
|
|
58
|
-
function item(kind, taskId, filePath, disposition, reason, identity = null) {
|
|
59
|
-
return { kind, taskId, path: path.resolve(filePath), disposition, reason, identity };
|
|
60
|
-
}
|
|
61
|
-
function taskGate(task) {
|
|
62
|
-
if (!task)
|
|
63
|
-
return 'TASK_NOT_FOUND';
|
|
64
|
-
if (task.state !== 'completed' && task.state !== 'archive')
|
|
65
|
-
return 'TASK_STATE_PROTECTED';
|
|
66
|
-
if (task.frontmatter.id !== task.taskId || task.frontmatter.status !== 'completed')
|
|
67
|
-
return 'TASK_STATE_PROTECTED';
|
|
68
|
-
if (!task.frontmatter.branch)
|
|
69
|
-
return 'TASK_IDENTITY_INVALID';
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
function receiptGate(repoRoot, task, options) {
|
|
73
|
-
let receipt;
|
|
74
|
-
try {
|
|
75
|
-
receipt = readTaskFinalizationReceipt(repoRoot, task.taskId);
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
return 'FINALIZATION_RECEIPT_INVALID';
|
|
79
|
-
}
|
|
80
|
-
if (!receipt)
|
|
81
|
-
return 'FINALIZATION_RECEIPT_MISSING';
|
|
82
|
-
const state = taskFinalizationReceiptState(receipt);
|
|
83
|
-
if (state === 'pending')
|
|
84
|
-
return 'FINALIZATION_RECEIPT_PENDING';
|
|
85
|
-
if (state === 'unresolved')
|
|
86
|
-
return 'LFAI_RETRY_OR_WARNING_OPEN';
|
|
87
|
-
if (receipt.controlBinding) {
|
|
88
|
-
const evidence = options.controlBindingEvidence?.(task.taskId, receipt.controlBinding);
|
|
89
|
-
if (evidence !== 'terminal' && !(options.preflight && evidence === 'pending'))
|
|
90
|
-
return 'CONTROL_BINDING_MISMATCH';
|
|
91
|
-
}
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
function commitIntentCandidate(repoRoot, task, filePath, taskId, identity, options) {
|
|
95
|
-
const protectedReason = taskGate(task);
|
|
96
|
-
if (protectedReason)
|
|
97
|
-
return item('COMMIT-SYNCED', taskId, filePath, 'protected', protectedReason, identity);
|
|
98
|
-
let intent;
|
|
99
|
-
try {
|
|
100
|
-
intent = readCheckpointIntent(repoRoot, taskId);
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
return item('COMMIT-SYNCED', taskId, filePath, 'protected', 'COMMIT_INTENT_INVALID', identity);
|
|
104
|
-
}
|
|
105
|
-
if (!intent || intent.state !== 'synced' || intent.taskId !== taskId
|
|
106
|
-
|| intent.branch !== task.frontmatter.branch || !intent.committedHead
|
|
107
|
-
|| task.frontmatter.checkpoint_commit !== intent.committedHead
|
|
108
|
-
|| !checkpointCommitMatches(repoRoot, intent, intent.committedHead)) {
|
|
109
|
-
return item('COMMIT-SYNCED', taskId, filePath, 'protected', 'COMMIT_EVIDENCE_MISMATCH', identity);
|
|
110
|
-
}
|
|
111
|
-
const receiptReason = receiptGate(repoRoot, task, options);
|
|
112
|
-
if (receiptReason)
|
|
113
|
-
return item('COMMIT-SYNCED', taskId, filePath, 'protected', receiptReason, identity);
|
|
114
|
-
return item('COMMIT-SYNCED', taskId, filePath, 'planned', 'COMMIT_SYNCED_AND_VERIFIED', identity);
|
|
115
|
-
}
|
|
116
|
-
function readAuxiliaryCandidates(repoRoot, tasks, options) {
|
|
117
|
-
const candidates = [];
|
|
118
|
-
const selected = options.taskIds ? new Set(options.taskIds) : null;
|
|
119
|
-
for (const rootName of AUXILIARY_ROOTS) {
|
|
120
|
-
const root = auxiliaryRoot(repoRoot, rootName);
|
|
121
|
-
let rootIdentity;
|
|
122
|
-
try {
|
|
123
|
-
rootIdentity = safeLstat(taskRoot(repoRoot), root, 'directory');
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
rootIdentity = null;
|
|
127
|
-
}
|
|
128
|
-
if (rootIdentity === null) {
|
|
129
|
-
let stat = null;
|
|
130
|
-
try {
|
|
131
|
-
stat = fs.lstatSync(root);
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
stat = null;
|
|
135
|
-
}
|
|
136
|
-
if (stat?.isSymbolicLink())
|
|
137
|
-
candidates.push(item('AUXILIARY-UNKNOWN', null, root, 'protected', 'PATH_SYMLINK'));
|
|
138
|
-
else if (stat)
|
|
139
|
-
candidates.push(item('AUXILIARY-UNKNOWN', null, root, 'protected', 'PATH_IDENTITY_MISMATCH'));
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
let entries;
|
|
143
|
-
try {
|
|
144
|
-
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
145
|
-
}
|
|
146
|
-
catch {
|
|
147
|
-
candidates.push(item('AUXILIARY-UNKNOWN', null, root, 'protected', 'AUXILIARY_ROOT_UNREADABLE', rootIdentity));
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
151
|
-
const filePath = path.join(root, entry.name);
|
|
152
|
-
if (entry.isSymbolicLink()) {
|
|
153
|
-
candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_SYMLINK'));
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (!entry.isFile()) {
|
|
157
|
-
candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH'));
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
const identity = safeLstat(root, filePath, 'file');
|
|
161
|
-
if (!identity) {
|
|
162
|
-
candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH'));
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
const match = COMMIT_INTENT_RE.exec(entry.name);
|
|
166
|
-
if (!match || (selected && !selected.has(match[1]))) {
|
|
167
|
-
if (!selected || !match)
|
|
168
|
-
candidates.push(item('AUXILIARY-UNKNOWN', null, filePath, 'protected', 'PATH_IDENTITY_MISMATCH', identity));
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
candidates.push(commitIntentCandidate(repoRoot, tasks.get(match[1]), filePath, match[1], identity, options));
|
|
172
|
-
}
|
|
173
|
-
if (entries.length === 0) {
|
|
174
|
-
candidates.push(item('EMPTY-AUX-PARENT', null, root, 'planned', 'EMPTY_CANONICAL_AUXILIARY_ROOT', rootIdentity));
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return candidates;
|
|
178
|
-
}
|
|
179
|
-
function buildIntermediateCleanupReport(items, dryRun = false, partial = false) {
|
|
180
|
-
const remaining = items.filter((candidate) => candidate.disposition !== 'deleted' && candidate.disposition !== 'skipped');
|
|
181
|
-
return {
|
|
182
|
-
status: dryRun ? 'dry-run' : partial || remaining.some((candidate) => candidate.disposition === 'failed') ? 'partial' : 'completed',
|
|
183
|
-
items,
|
|
184
|
-
remaining
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
function scanInternal(repoRootInput, options) {
|
|
188
|
-
const repoRoot = path.resolve(repoRootInput);
|
|
189
|
-
if (safeLstat(path.dirname(repoRoot), repoRoot, 'directory') === null) {
|
|
190
|
-
throw new Error('INTERMEDIATE_CLEANUP_REPOSITORY_INVALID');
|
|
191
|
-
}
|
|
192
|
-
const selected = options.taskIds ? new Set(options.taskIds.filter((taskId) => TASK_ID_RE.test(taskId))) : undefined;
|
|
193
|
-
const tasks = inspectTaskRecords(repoRoot, selected);
|
|
194
|
-
return readAuxiliaryCandidates(repoRoot, tasks, options);
|
|
195
|
-
}
|
|
196
|
-
function scanIntermediateCleanup(repoRoot, options = {}) {
|
|
197
|
-
const candidates = scanInternal(repoRoot, options);
|
|
198
|
-
return buildIntermediateCleanupReport(candidates, true);
|
|
199
|
-
}
|
|
200
|
-
function removeCandidate(repoRoot, candidate) {
|
|
201
|
-
if (candidate.disposition !== 'planned')
|
|
202
|
-
return candidate;
|
|
203
|
-
const root = candidate.kind === 'EMPTY-AUX-PARENT'
|
|
204
|
-
? path.dirname(path.dirname(candidate.path))
|
|
205
|
-
: path.dirname(candidate.path);
|
|
206
|
-
const expected = candidate.identity;
|
|
207
|
-
if (!expected)
|
|
208
|
-
return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_MISSING' };
|
|
209
|
-
let raw;
|
|
210
|
-
try {
|
|
211
|
-
raw = fs.lstatSync(candidate.path);
|
|
212
|
-
}
|
|
213
|
-
catch (error) {
|
|
214
|
-
if (errorCode(error) === 'ENOENT')
|
|
215
|
-
return { ...candidate, disposition: 'skipped', reason: 'ALREADY_GONE' };
|
|
216
|
-
return { ...candidate, disposition: 'failed', reason: 'CLEANUP_RECHECK_FAILED' };
|
|
217
|
-
}
|
|
218
|
-
if (raw.isSymbolicLink())
|
|
219
|
-
return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
220
|
-
const actual = safeLstat(root, candidate.path, candidate.kind === 'EMPTY-AUX-PARENT' ? 'directory' : 'file');
|
|
221
|
-
if (!actual)
|
|
222
|
-
return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
223
|
-
if (!sameFileIdentity(expected, actual))
|
|
224
|
-
return { ...candidate, disposition: 'protected', reason: 'PATH_IDENTITY_CHANGED' };
|
|
225
|
-
try {
|
|
226
|
-
if (candidate.kind === 'EMPTY-AUX-PARENT') {
|
|
227
|
-
if (fs.readdirSync(candidate.path).length !== 0)
|
|
228
|
-
return { ...candidate, disposition: 'protected', reason: 'AUXILIARY_ROOT_NOT_EMPTY' };
|
|
229
|
-
fs.rmdirSync(candidate.path);
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
fs.unlinkSync(candidate.path);
|
|
233
|
-
}
|
|
234
|
-
return { ...candidate, disposition: 'deleted', reason: 'DELETED_AFTER_LOCKED_RECHECK' };
|
|
235
|
-
}
|
|
236
|
-
catch (error) {
|
|
237
|
-
if (errorCode(error) === 'ENOENT')
|
|
238
|
-
return { ...candidate, disposition: 'skipped', reason: 'ALREADY_GONE' };
|
|
239
|
-
return { ...candidate, disposition: 'failed', reason: 'CLEANUP_DELETE_FAILED' };
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
function intermediateCleanupKey(item) {
|
|
243
|
-
return `${item.kind}\0${item.path}`;
|
|
244
|
-
}
|
|
245
|
-
function cleanupIntermediateUnderRemovalCoordinator(repoRoot, options = {}) {
|
|
246
|
-
const scanOptions = {
|
|
247
|
-
...(options.taskIds === undefined ? {} : { taskIds: options.taskIds }),
|
|
248
|
-
...(options.controlBindingEvidence === undefined ? {} : { controlBindingEvidence: options.controlBindingEvidence })
|
|
249
|
-
};
|
|
250
|
-
const initial = scanIntermediateCleanup(repoRoot, scanOptions);
|
|
251
|
-
if (options.dryRun)
|
|
252
|
-
return initial;
|
|
253
|
-
const items = [...initial.items];
|
|
254
|
-
const lockedTaskIds = options.lockedTaskIds ?? new Set();
|
|
255
|
-
const byTask = new Map();
|
|
256
|
-
for (const [index, candidate] of items.entries()) {
|
|
257
|
-
if (!candidate.taskId || candidate.disposition !== 'planned')
|
|
258
|
-
continue;
|
|
259
|
-
const indexes = byTask.get(candidate.taskId) ?? [];
|
|
260
|
-
indexes.push(index);
|
|
261
|
-
byTask.set(candidate.taskId, indexes);
|
|
262
|
-
}
|
|
263
|
-
const tasks = inspectTaskRecords(repoRoot, new Set(byTask.keys()));
|
|
264
|
-
for (const taskId of [...byTask.keys()].sort()) {
|
|
265
|
-
const execute = () => {
|
|
266
|
-
const refreshed = scanIntermediateCleanup(repoRoot, {
|
|
267
|
-
...scanOptions,
|
|
268
|
-
taskIds: [taskId]
|
|
269
|
-
});
|
|
270
|
-
const refreshedByKey = new Map(refreshed.items.map((candidate) => [intermediateCleanupKey(candidate), candidate]));
|
|
271
|
-
for (const index of byTask.get(taskId) ?? []) {
|
|
272
|
-
const initialCandidate = items[index];
|
|
273
|
-
if (initialCandidate.disposition !== 'planned')
|
|
274
|
-
continue;
|
|
275
|
-
const current = refreshedByKey.get(intermediateCleanupKey(initialCandidate));
|
|
276
|
-
items[index] = current
|
|
277
|
-
? removeCandidate(repoRoot, current)
|
|
278
|
-
: { ...initialCandidate, disposition: 'protected', reason: 'CANDIDATE_NO_LONGER_ELIGIBLE' };
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
try {
|
|
282
|
-
if (lockedTaskIds.has(taskId))
|
|
283
|
-
execute();
|
|
284
|
-
else
|
|
285
|
-
withTaskExecutionLock(repoRoot, taskId, 'sandbox-removal', execute);
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
for (const index of byTask.get(taskId) ?? []) {
|
|
289
|
-
const candidate = items[index];
|
|
290
|
-
if (candidate.disposition === 'planned')
|
|
291
|
-
items[index] = { ...candidate, disposition: 'protected', reason: 'TASK_LOCK_BUSY' };
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
for (const [index, candidate] of items.entries()) {
|
|
296
|
-
if (candidate.taskId || candidate.disposition !== 'planned')
|
|
297
|
-
continue;
|
|
298
|
-
items[index] = removeCandidate(repoRoot, candidate);
|
|
299
|
-
}
|
|
300
|
-
const knownPaths = new Set(items.map((candidate) => intermediateCleanupKey(candidate)));
|
|
301
|
-
for (const candidate of scanIntermediateCleanup(repoRoot, scanOptions).items) {
|
|
302
|
-
if (candidate.kind !== 'EMPTY-AUX-PARENT' || candidate.disposition !== 'planned'
|
|
303
|
-
|| knownPaths.has(intermediateCleanupKey(candidate)))
|
|
304
|
-
continue;
|
|
305
|
-
items.push(removeCandidate(repoRoot, candidate));
|
|
306
|
-
}
|
|
307
|
-
return buildIntermediateCleanupReport(items);
|
|
308
|
-
}
|
|
309
|
-
function protectIntermediateCleanupReport(report, reason) {
|
|
310
|
-
const items = report.items.map((item) => item.disposition === 'planned'
|
|
311
|
-
? { ...item, disposition: 'protected', reason }
|
|
312
|
-
: item);
|
|
313
|
-
return buildIntermediateCleanupReport(items, false, true);
|
|
314
|
-
}
|
|
315
|
-
function mergeIntermediateCleanupReports(reports) {
|
|
316
|
-
const items = reports.flatMap((report) => report.items);
|
|
317
|
-
return buildIntermediateCleanupReport(items, false, reports.some((report) => report.status === 'partial'));
|
|
318
|
-
}
|
|
319
|
-
function formatIntermediateCleanupReport(report) {
|
|
320
|
-
return [
|
|
321
|
-
`remaining ${report.remaining.length} item(s)`,
|
|
322
|
-
...report.items.map((candidate) => (`${candidate.disposition} ${candidate.kind} ${candidate.path}`
|
|
323
|
-
+ ` (${candidate.taskId ?? 'unbound'}; ${candidate.reason})`))
|
|
324
|
-
];
|
|
325
|
-
}
|
|
326
|
-
export { formatIntermediateCleanupReport, cleanupIntermediateUnderRemovalCoordinator, protectIntermediateCleanupReport, mergeIntermediateCleanupReports, scanIntermediateCleanup };
|
|
327
|
-
//# sourceMappingURL=intermediate-cleanup.js.map
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { parseArtifactReceipts, sha256File } from './artifact-receipts.js';
|
|
4
|
-
import { invalidationMutation, parseInvalidationDocument, reconcileInvalidation } from './invalidation.js';
|
|
5
|
-
import { resolveTaskRef } from './resolve-ref.js';
|
|
6
|
-
import { writeTask } from './write.js';
|
|
7
|
-
function reconcileTaskInvalidation(taskRef, options = {}) {
|
|
8
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
9
|
-
if (!resolved.ok)
|
|
10
|
-
return { status: 'failed', changed: false, taskId: resolved.taskId, processed: 0, remaining: 0, error: { code: resolved.code, message: resolved.message } };
|
|
11
|
-
let content;
|
|
12
|
-
try {
|
|
13
|
-
content = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
14
|
-
}
|
|
15
|
-
catch (error) {
|
|
16
|
-
return { status: 'failed', changed: false, taskId: resolved.taskId, processed: 0, remaining: 0, error: { code: 'TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error) } };
|
|
17
|
-
}
|
|
18
|
-
const parsed = parseInvalidationDocument(content);
|
|
19
|
-
if (!parsed.ok)
|
|
20
|
-
return { status: 'failed', changed: false, taskId: resolved.taskId, processed: 0, remaining: 0, error: { code: parsed.code, message: parsed.message } };
|
|
21
|
-
if (!parsed.present)
|
|
22
|
-
return { status: 'no-op', changed: false, taskId: resolved.taskId, processed: 0, remaining: 0, error: null };
|
|
23
|
-
const beforePending = parsed.document.targets.filter((target) => target.status !== 'completed').length;
|
|
24
|
-
let receipts;
|
|
25
|
-
try {
|
|
26
|
-
receipts = parseArtifactReceipts(content).rows;
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
if (parsed.document.targets.some((target) => target.status !== 'completed' && target.targetKind === 'receipt')) {
|
|
30
|
-
return {
|
|
31
|
-
status: 'failed', changed: false, taskId: resolved.taskId, processed: 0, remaining: beforePending,
|
|
32
|
-
error: { code: 'INVALIDATION_TARGET_HASH_CONFLICT', message: error instanceof Error ? error.message : String(error) }
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
receipts = [];
|
|
36
|
-
}
|
|
37
|
-
for (const target of parsed.document.targets) {
|
|
38
|
-
if (target.status === 'completed')
|
|
39
|
-
continue;
|
|
40
|
-
if (target.targetKind === 'receipt') {
|
|
41
|
-
const receiptExists = Boolean(target.targetInput) && receipts.some((candidate) => candidate.output === target.targetArtifact
|
|
42
|
-
&& candidate.input === target.targetInput
|
|
43
|
-
&& candidate.inputSha256 === target.targetSha256);
|
|
44
|
-
if (!receiptExists) {
|
|
45
|
-
return {
|
|
46
|
-
status: 'failed', changed: false, taskId: resolved.taskId, processed: 0, remaining: beforePending,
|
|
47
|
-
error: { code: 'INVALIDATION_TARGET_HASH_CONFLICT', message: `invalidation receipt target '${target.targetArtifact}' changed before reconcile` }
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
const targetPath = path.join(resolved.taskDir, target.targetArtifact);
|
|
53
|
-
try {
|
|
54
|
-
const stat = fs.lstatSync(targetPath);
|
|
55
|
-
if (!stat.isFile() || stat.isSymbolicLink() || sha256File(targetPath) !== target.targetSha256) {
|
|
56
|
-
return {
|
|
57
|
-
status: 'failed', changed: false, taskId: resolved.taskId, processed: 0, remaining: beforePending,
|
|
58
|
-
error: { code: 'INVALIDATION_TARGET_HASH_CONFLICT', message: `invalidation target '${target.targetArtifact}' changed before reconcile` }
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
return {
|
|
64
|
-
status: 'failed', changed: false, taskId: resolved.taskId, processed: 0, remaining: beforePending,
|
|
65
|
-
error: { code: 'INVALIDATION_TARGET_HASH_CONFLICT', message: `invalidation target '${target.targetArtifact}' is unavailable before reconcile` }
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const reconciled = reconcileInvalidation(parsed.document, (options.now ?? (() => new Date().toISOString()))(), options.maxTargets);
|
|
70
|
-
if (!reconciled.ok)
|
|
71
|
-
return { status: 'failed', changed: false, taskId: resolved.taskId, processed: 0, remaining: beforePending, error: { code: reconciled.code, message: reconciled.message } };
|
|
72
|
-
if (!reconciled.changed)
|
|
73
|
-
return { status: 'no-op', changed: false, taskId: resolved.taskId, processed: 0, remaining: beforePending, error: null };
|
|
74
|
-
const result = writeTask({
|
|
75
|
-
taskRef, expectedState: resolved.state, dryRun: options.dryRun,
|
|
76
|
-
mutations: [invalidationMutation(content, reconciled.document)]
|
|
77
|
-
}, { ...options, invalidationContext: 'reconcile', taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
|
|
78
|
-
if (result.status === 'failed')
|
|
79
|
-
return { status: 'failed', changed: false, taskId: resolved.taskId, processed: beforePending - reconciled.document.targets.filter((target) => target.status !== 'completed').length, remaining: reconciled.document.targets.filter((target) => target.status !== 'completed').length, error: result.error };
|
|
80
|
-
return {
|
|
81
|
-
status: result.status, changed: result.changed, taskId: resolved.taskId,
|
|
82
|
-
processed: beforePending - reconciled.document.targets.filter((target) => target.status !== 'completed').length,
|
|
83
|
-
remaining: reconciled.document.targets.filter((target) => target.status !== 'completed').length,
|
|
84
|
-
error: null
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
export { reconcileTaskInvalidation };
|
|
88
|
-
//# sourceMappingURL=invalidation-command.js.map
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { parseArtifactName } from './artifact-name.js';
|
|
3
|
-
import { findSectionRange, parseTable } from './sections.js';
|
|
4
|
-
import { scanVisibleMarkdown } from './markdown.js';
|
|
5
|
-
const INVALIDATION_HEADINGS = ['产物失效记录', 'Artifact Invalidation'];
|
|
6
|
-
const OPERATION_COLUMNS = [
|
|
7
|
-
'operation_id', 'source_family', 'source_artifact', 'source_round', 'source_sha256',
|
|
8
|
-
'status', 'processed', 'total', 'created_at', 'updated_at', 'completed_at', 'error'
|
|
9
|
-
];
|
|
10
|
-
const TARGET_COLUMNS = [
|
|
11
|
-
'target_id', 'operation_id', 'target_kind', 'target_family', 'target_artifact', 'target_input',
|
|
12
|
-
'target_round', 'target_sha256', 'status', 'reason_code', 'updated_at'
|
|
13
|
-
];
|
|
14
|
-
const LEGACY_TARGET_COLUMNS = [
|
|
15
|
-
'target_id', 'operation_id', 'target_kind', 'target_family', 'target_artifact',
|
|
16
|
-
'target_round', 'target_sha256', 'status', 'reason_code', 'updated_at'
|
|
17
|
-
];
|
|
18
|
-
function invalid(message) {
|
|
19
|
-
return { ok: false, code: 'TASK_INVALIDATION_INVALID', message };
|
|
20
|
-
}
|
|
21
|
-
function hashIdentity(value) {
|
|
22
|
-
return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 16);
|
|
23
|
-
}
|
|
24
|
-
function operationIdFor(source) {
|
|
25
|
-
return `INV-${hashIdentity([source.sourceFamily, source.sourceArtifact, source.sourceRound, source.sourceSha256])}`;
|
|
26
|
-
}
|
|
27
|
-
function targetIdFor(operationId, target) {
|
|
28
|
-
return `INV-T-${hashIdentity([operationId, target.targetKind, target.targetFamily, target.targetArtifact, ...(target.targetKind === 'receipt' && target.targetInput ? [target.targetInput] : []), target.targetRound, target.targetSha256])}`;
|
|
29
|
-
}
|
|
30
|
-
function createInvalidationOperation(source, targets = []) {
|
|
31
|
-
const operationId = source.operationId ?? operationIdFor(source);
|
|
32
|
-
return {
|
|
33
|
-
operationId, sourceFamily: source.sourceFamily, sourceArtifact: source.sourceArtifact,
|
|
34
|
-
sourceRound: source.sourceRound, sourceSha256: source.sourceSha256,
|
|
35
|
-
status: 'pending', processed: 0, total: targets.length,
|
|
36
|
-
createdAt: source.createdAt, updatedAt: source.updatedAt, completedAt: '', error: ''
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
function parseInvalidationTable(body, heading, columnChoices) {
|
|
40
|
-
const section = findSectionRange(body, [heading], 3);
|
|
41
|
-
const lines = scanVisibleMarkdown(body).lines.filter((line) => line.text.trim());
|
|
42
|
-
let table = null;
|
|
43
|
-
let legacy = false;
|
|
44
|
-
for (const columns of columnChoices) {
|
|
45
|
-
try {
|
|
46
|
-
const candidate = parseTable(lines.map((line) => line.text).join('\n'), { sectionAliases: [heading], columns });
|
|
47
|
-
if (candidate) {
|
|
48
|
-
table = candidate;
|
|
49
|
-
legacy = columns === LEGACY_TARGET_COLUMNS;
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// Try the next explicitly supported schema.
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (!section || !table || lines.filter((line) => line.start >= section.bodyStart && line.start < section.end).length !== table.rows.length + 2) {
|
|
58
|
-
throw new Error(`invalidation ${heading} section must contain only its canonical table`);
|
|
59
|
-
}
|
|
60
|
-
return { rows: table.rows, legacy };
|
|
61
|
-
}
|
|
62
|
-
function required(value, field) {
|
|
63
|
-
if (!value)
|
|
64
|
-
throw new Error(`${field} is required`);
|
|
65
|
-
return value;
|
|
66
|
-
}
|
|
67
|
-
function status(value, field) {
|
|
68
|
-
if (value !== 'pending' && value !== 'in-progress' && value !== 'completed')
|
|
69
|
-
throw new Error(`${field} has an invalid status`);
|
|
70
|
-
return value;
|
|
71
|
-
}
|
|
72
|
-
function numberValue(value, field, allowZero = true) {
|
|
73
|
-
const parsed = Number(value);
|
|
74
|
-
if (!Number.isInteger(parsed) || (allowZero ? parsed < 0 : parsed < 1))
|
|
75
|
-
throw new Error(`${field} must be an integer`);
|
|
76
|
-
return parsed;
|
|
77
|
-
}
|
|
78
|
-
function parseInvalidationDocument(content) {
|
|
79
|
-
const section = findSectionRange(content, INVALIDATION_HEADINGS);
|
|
80
|
-
if (section === null)
|
|
81
|
-
return { ok: true, present: false, document: { operations: [], targets: [] } };
|
|
82
|
-
try {
|
|
83
|
-
const body = content.slice(section.bodyStart, section.end);
|
|
84
|
-
const operationRows = parseInvalidationTable(body, 'Operations', [OPERATION_COLUMNS]);
|
|
85
|
-
// TODO(compat): Remove legacy target table parsing and completed receipt targets without target_input once a workspace inventory confirms all persisted targets have input identities.
|
|
86
|
-
const targetRows = parseInvalidationTable(body, 'Targets', [TARGET_COLUMNS, LEGACY_TARGET_COLUMNS]);
|
|
87
|
-
const operations = operationRows.rows.map(({ values: row }) => ({
|
|
88
|
-
operationId: required(row.operation_id, 'operation_id'), sourceFamily: required(row.source_family, 'source_family'),
|
|
89
|
-
sourceArtifact: required(row.source_artifact, 'source_artifact'), sourceRound: numberValue(row.source_round, 'source_round', false),
|
|
90
|
-
sourceSha256: required(row.source_sha256, 'source_sha256'), status: status(row.status, 'operation status'),
|
|
91
|
-
processed: numberValue(row.processed, 'processed'), total: numberValue(row.total, 'total'),
|
|
92
|
-
createdAt: required(row.created_at, 'created_at'), updatedAt: required(row.updated_at, 'updated_at'),
|
|
93
|
-
completedAt: row.completed_at ?? '', error: row.error ?? ''
|
|
94
|
-
}));
|
|
95
|
-
const targets = targetRows.rows.map(({ values: row }) => ({
|
|
96
|
-
targetId: required(row.target_id, 'target_id'), operationId: required(row.operation_id, 'operation_id'),
|
|
97
|
-
targetKind: row.target_kind, targetFamily: required(row.target_family, 'target_family'),
|
|
98
|
-
targetArtifact: required(row.target_artifact, 'target_artifact'), targetRound: numberValue(row.target_round, 'target_round', false),
|
|
99
|
-
...(row.target_input ? { targetInput: row.target_input } : {}),
|
|
100
|
-
targetSha256: required(row.target_sha256, 'target_sha256'), status: status(row.status, 'target status'),
|
|
101
|
-
reasonCode: required(row.reason_code, 'reason_code'), updatedAt: required(row.updated_at, 'updated_at')
|
|
102
|
-
}));
|
|
103
|
-
const operationIds = new Set();
|
|
104
|
-
for (const operation of operations) {
|
|
105
|
-
if (operationIds.has(operation.operationId))
|
|
106
|
-
throw new Error(`duplicate operation '${operation.operationId}'`);
|
|
107
|
-
operationIds.add(operation.operationId);
|
|
108
|
-
if (operation.operationId !== operationIdFor(operation))
|
|
109
|
-
throw new Error(`operation '${operation.operationId}' has a non-canonical identity`);
|
|
110
|
-
if (operation.processed > operation.total)
|
|
111
|
-
throw new Error(`operation '${operation.operationId}' progress is invalid`);
|
|
112
|
-
if (!/^[a-f0-9]{64}$/.test(operation.sourceSha256))
|
|
113
|
-
throw new Error(`operation '${operation.operationId}' source hash is invalid`);
|
|
114
|
-
}
|
|
115
|
-
const targetIds = new Set();
|
|
116
|
-
for (const target of targets) {
|
|
117
|
-
if (!['artifact', 'receipt', 'approval', 'reviewed-snapshot'].includes(target.targetKind))
|
|
118
|
-
throw new Error(`target '${target.targetId}' kind is invalid`);
|
|
119
|
-
if (targetIds.has(target.targetId))
|
|
120
|
-
throw new Error(`duplicate target '${target.targetId}'`);
|
|
121
|
-
targetIds.add(target.targetId);
|
|
122
|
-
if (!operationIds.has(target.operationId))
|
|
123
|
-
throw new Error(`target '${target.targetId}' references an unknown operation`);
|
|
124
|
-
if (target.targetKind === 'receipt') {
|
|
125
|
-
if (!target.targetInput && target.status !== 'completed')
|
|
126
|
-
throw new Error(`receipt target '${target.targetId}' input identity is required`);
|
|
127
|
-
if (target.targetInput && !parseArtifactName(target.targetInput))
|
|
128
|
-
throw new Error(`receipt target '${target.targetId}' input identity is invalid`);
|
|
129
|
-
}
|
|
130
|
-
else if (target.targetInput)
|
|
131
|
-
throw new Error(`non-receipt target '${target.targetId}' cannot have an input identity`);
|
|
132
|
-
if (target.targetId !== targetIdFor(target.operationId, target))
|
|
133
|
-
throw new Error(`target '${target.targetId}' has a non-canonical identity`);
|
|
134
|
-
if (!/^[a-f0-9]{64}$/.test(target.targetSha256))
|
|
135
|
-
throw new Error(`target '${target.targetId}' hash is invalid`);
|
|
136
|
-
}
|
|
137
|
-
for (const operation of operations) {
|
|
138
|
-
const scoped = targets.filter((target) => target.operationId === operation.operationId);
|
|
139
|
-
const completed = scoped.filter((target) => target.status === 'completed').length;
|
|
140
|
-
if (operation.total !== scoped.length || operation.processed !== completed)
|
|
141
|
-
throw new Error(`operation '${operation.operationId}' progress does not match its targets`);
|
|
142
|
-
if (operation.status === 'completed' && (completed !== scoped.length || !operation.completedAt))
|
|
143
|
-
throw new Error(`completed operation '${operation.operationId}' has incomplete targets`);
|
|
144
|
-
if (operation.status === 'pending' && completed !== 0)
|
|
145
|
-
throw new Error(`pending operation '${operation.operationId}' has completed targets`);
|
|
146
|
-
if (operation.status === 'in-progress' && (completed === scoped.length || (completed === 0 && !scoped.some((target) => target.status === 'in-progress'))))
|
|
147
|
-
throw new Error(`in-progress operation '${operation.operationId}' has invalid progress`);
|
|
148
|
-
}
|
|
149
|
-
return { ok: true, present: true, document: { operations, targets } };
|
|
150
|
-
}
|
|
151
|
-
catch (error) {
|
|
152
|
-
return invalid(error instanceof Error ? error.message : String(error));
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
function cell(value) {
|
|
156
|
-
return String(value).replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/[\r\n]/g, ' ');
|
|
157
|
-
}
|
|
158
|
-
function table(columns, rows) {
|
|
159
|
-
const header = `| ${columns.join(' | ')} |`;
|
|
160
|
-
const separator = `| ${columns.map(() => '---').join(' | ')} |`;
|
|
161
|
-
return [header, separator, ...rows.map((row) => `| ${row.map(cell).join(' | ')} |`)].join('\n');
|
|
162
|
-
}
|
|
163
|
-
function renderInvalidation(document) {
|
|
164
|
-
const operations = [...document.operations].sort((left, right) => left.operationId.localeCompare(right.operationId));
|
|
165
|
-
const targets = [...document.targets].sort((left, right) => left.targetId.localeCompare(right.targetId));
|
|
166
|
-
return [
|
|
167
|
-
'### Operations', '',
|
|
168
|
-
table(OPERATION_COLUMNS, operations.map((row) => [
|
|
169
|
-
row.operationId, row.sourceFamily, row.sourceArtifact, row.sourceRound, row.sourceSha256,
|
|
170
|
-
row.status, row.processed, row.total, row.createdAt, row.updatedAt, row.completedAt, row.error
|
|
171
|
-
])), '',
|
|
172
|
-
'### Targets', '',
|
|
173
|
-
table(TARGET_COLUMNS, targets.map((row) => [
|
|
174
|
-
row.targetId, row.operationId, row.targetKind, row.targetFamily, row.targetArtifact,
|
|
175
|
-
row.targetInput ?? '', row.targetRound, row.targetSha256, row.status, row.reasonCode, row.updatedAt
|
|
176
|
-
]))
|
|
177
|
-
].join('\n');
|
|
178
|
-
}
|
|
179
|
-
function reconcileInvalidation(document, now, maxTargets = Number.POSITIVE_INFINITY) {
|
|
180
|
-
const nextTargets = document.targets.map((target) => {
|
|
181
|
-
if (target.status === 'completed' || maxTargets <= 0)
|
|
182
|
-
return target;
|
|
183
|
-
maxTargets -= 1;
|
|
184
|
-
return { ...target, status: 'completed', updatedAt: now };
|
|
185
|
-
});
|
|
186
|
-
const changed = nextTargets.some((target, index) => target !== document.targets[index]);
|
|
187
|
-
const nextOperations = document.operations.map((operation) => {
|
|
188
|
-
const scoped = nextTargets.filter((target) => target.operationId === operation.operationId);
|
|
189
|
-
const processed = scoped.filter((target) => target.status === 'completed').length;
|
|
190
|
-
const total = scoped.length;
|
|
191
|
-
const complete = total === processed;
|
|
192
|
-
const nextStatus = complete ? 'completed' : processed > 0 ? 'in-progress' : 'pending';
|
|
193
|
-
if (operation.status === nextStatus && operation.processed === processed && operation.total === total && (!complete || operation.completedAt))
|
|
194
|
-
return operation;
|
|
195
|
-
return {
|
|
196
|
-
...operation, status: nextStatus, processed, total, updatedAt: changed ? now : operation.updatedAt,
|
|
197
|
-
completedAt: complete ? (operation.completedAt || now) : ''
|
|
198
|
-
};
|
|
199
|
-
});
|
|
200
|
-
const operationChanged = nextOperations.some((operation, index) => operation !== document.operations[index]);
|
|
201
|
-
return { ok: true, changed: changed || operationChanged, document: { operations: nextOperations, targets: nextTargets } };
|
|
202
|
-
}
|
|
203
|
-
function upsertInvalidation(document, operation, targets) {
|
|
204
|
-
const existing = document.operations.find((candidate) => candidate.operationId === operation.operationId);
|
|
205
|
-
if (existing && JSON.stringify(existing) !== JSON.stringify(operation)) {
|
|
206
|
-
return { ok: false, code: 'INVALIDATION_IDENTITY_CONFLICT', message: `operation '${operation.operationId}' identity conflicts with existing state` };
|
|
207
|
-
}
|
|
208
|
-
const existingTargets = document.targets.filter((target) => target.operationId === operation.operationId);
|
|
209
|
-
for (const target of targets) {
|
|
210
|
-
const old = existingTargets.find((candidate) => candidate.targetId === target.targetId);
|
|
211
|
-
if (old && JSON.stringify(old) !== JSON.stringify(target)) {
|
|
212
|
-
return { ok: false, code: 'INVALIDATION_IDENTITY_CONFLICT', message: `target '${target.targetId}' identity conflicts with existing state` };
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
if (existing)
|
|
216
|
-
return { ok: true, changed: false, document };
|
|
217
|
-
return {
|
|
218
|
-
ok: true, changed: true,
|
|
219
|
-
document: { operations: [...document.operations, operation], targets: [...document.targets, ...targets] }
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
function invalidationBlocks(document) {
|
|
223
|
-
return document.operations.some((operation) => operation.status !== 'completed')
|
|
224
|
-
|| document.targets.some((target) => target.status !== 'completed');
|
|
225
|
-
}
|
|
226
|
-
function isArtifactInvalidated(document, family, artifact) {
|
|
227
|
-
return document.targets.some((target) => target.status === 'completed'
|
|
228
|
-
&& target.targetKind === 'artifact'
|
|
229
|
-
&& target.targetFamily === family
|
|
230
|
-
&& target.targetArtifact === artifact);
|
|
231
|
-
}
|
|
232
|
-
function invalidationMutation(content, document) {
|
|
233
|
-
const existing = findSectionRange(content, INVALIDATION_HEADINGS);
|
|
234
|
-
return {
|
|
235
|
-
kind: 'section',
|
|
236
|
-
aliases: INVALIDATION_HEADINGS,
|
|
237
|
-
heading: existing?.heading ?? INVALIDATION_HEADINGS[0],
|
|
238
|
-
body: renderInvalidation(document)
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
export { INVALIDATION_HEADINGS, OPERATION_COLUMNS, TARGET_COLUMNS, createInvalidationOperation, isArtifactInvalidated, invalidationBlocks, invalidationMutation, operationIdFor, parseInvalidationDocument, reconcileInvalidation, renderInvalidation, targetIdFor, upsertInvalidation };
|
|
242
|
-
//# sourceMappingURL=invalidation.js.map
|