@fitlab-ai/agent-infra 0.9.12 → 0.9.13
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 +11 -11
- package/README.zh-CN.md +11 -11
- package/dist/bin/internal-cli.js +24 -19
- package/dist/lib/agent-clients/next-steps.js +6 -1
- package/dist/lib/decide.js +3 -3
- package/dist/lib/internal/platform-checks.js +3 -3
- package/dist/lib/internal/platform-comment.js +9 -9
- package/dist/lib/internal/platform-context.js +2 -2
- package/dist/lib/internal/platform-issue.js +8 -9
- package/dist/lib/internal/platform-pr-review.js +10 -10
- package/dist/lib/internal/platform-pr.js +63 -24
- package/dist/lib/internal/platform-release-notes.js +3 -3
- package/dist/lib/internal/pr-review-grade.js +11 -9
- package/dist/lib/internal/release-workflow.js +4 -4
- package/dist/lib/internal/sandbox-control.js +30 -4
- package/dist/lib/internal/task-activity.js +3 -3
- package/dist/lib/internal/task-artifact.js +76 -24
- package/dist/lib/internal/task-create.js +2 -2
- package/dist/lib/internal/task-event.js +6 -3
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-invalidation.js +60 -0
- package/dist/lib/internal/task-ledger.js +9 -6
- package/dist/lib/internal/task-lifecycle.js +2 -2
- package/dist/lib/internal/task-override.js +4 -4
- package/dist/lib/internal/task-review.js +3 -3
- package/dist/lib/internal/task-verify.js +6 -6
- package/dist/lib/internal/task-warning.js +3 -3
- package/dist/lib/platform/adapters.js +1 -3
- package/dist/lib/platform/change-request-git-evidence.js +27 -7
- package/dist/lib/platform/completion-backfill.js +6 -5
- package/dist/lib/platform/context.js +70 -130
- package/dist/lib/platform/github-client.js +1 -1
- package/dist/lib/platform/github-provider.js +903 -0
- package/dist/lib/platform/in-label-sync.js +143 -0
- package/dist/lib/platform/issue-comments.js +94 -41
- package/dist/lib/platform/issues.js +335 -189
- package/dist/lib/platform/metadata-labels.js +6 -2
- package/dist/lib/platform/none-provider.js +28 -0
- package/dist/lib/platform/pr-change-report.js +551 -0
- package/dist/lib/platform/pr-checks.js +119 -104
- package/dist/lib/platform/pr-review.js +88 -72
- package/dist/lib/platform/pr-summary.js +520 -50
- package/dist/lib/platform/provider-bridge.js +45 -0
- package/dist/lib/platform/provider-contract.d.ts +487 -0
- package/dist/lib/platform/provider-contract.js +95 -0
- package/dist/lib/platform/provider-loader.js +251 -0
- package/dist/lib/platform/provider-validation.js +504 -0
- package/dist/lib/platform/pull-request-metadata.js +8 -6
- package/dist/lib/platform/pull-requests.js +783 -537
- package/dist/lib/platform/release-notes.js +78 -55
- package/dist/lib/platform/releases.js +79 -50
- package/dist/lib/platform/resource-identity.d.ts +26 -0
- package/dist/lib/platform/resource-identity.js +92 -0
- package/dist/lib/platform/task-identities.js +31 -0
- package/dist/lib/platform/verification-required.js +11 -12
- package/dist/lib/platform/verification-sync.js +112 -238
- package/dist/lib/pr-review/evidence-grading.js +19 -6
- package/dist/lib/run/index.js +63 -23
- package/dist/lib/sandbox/control/client.js +96 -15
- package/dist/lib/sandbox/control/executor.js +3 -3
- package/dist/lib/sandbox/control/protocol.js +77 -0
- package/dist/lib/sandbox/control/server.js +398 -25
- package/dist/lib/sandbox/control/state.js +204 -1
- package/dist/lib/server/protocol.js +38 -1
- package/dist/lib/task/activity-log.js +8 -1
- package/dist/lib/task/artifact-lifecycle.js +33 -5
- package/dist/lib/task/capabilities.js +239 -0
- package/dist/lib/task/commands/status.js +32 -0
- package/dist/lib/task/commit-operation.js +12 -1
- package/dist/lib/task/control-authority.js +8 -5
- package/dist/lib/task/create-service.js +8 -8
- package/dist/lib/task/current-contract.js +5 -1
- package/dist/lib/task/decision-details.js +1 -1
- package/dist/lib/task/events.js +297 -17
- package/dist/lib/task/finalization.js +37 -13
- package/dist/lib/task/human-override.js +58 -36
- package/dist/lib/task/invalidation-command.js +86 -0
- package/dist/lib/task/invalidation.js +260 -0
- package/dist/lib/task/ledger-intents.js +37 -0
- package/dist/lib/task/local-artifact-finalization.js +374 -0
- package/dist/lib/task/orchestration.js +49 -93
- package/dist/lib/task/pr-delivery-fact.js +102 -101
- package/dist/lib/task/review-fingerprint.js +24 -2
- package/dist/lib/task/rework-intent.js +138 -0
- package/dist/lib/task/skill-scope.js +23 -0
- package/dist/lib/task/task-execution-lock.js +27 -17
- package/dist/lib/task/verification-engine.js +43 -16
- package/dist/lib/task/verification.js +3 -3
- package/dist/lib/task/workflow-warning-intents.js +1 -1
- package/dist/lib/task/write.js +11 -0
- package/dist/lib/version.js +11 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/next-steps.ts +6 -1
- package/lib/decide.ts +3 -3
- package/lib/init.ts +1 -1
- package/lib/internal/platform-checks.ts +3 -3
- package/lib/internal/platform-comment.ts +8 -8
- package/lib/internal/platform-context.ts +2 -2
- package/lib/internal/platform-issue.ts +7 -8
- package/lib/internal/platform-pr-review.ts +9 -9
- package/lib/internal/platform-pr.ts +47 -22
- package/lib/internal/platform-release-notes.ts +3 -3
- package/lib/internal/pr-review-grade.ts +10 -8
- package/lib/internal/release-workflow.ts +4 -4
- package/lib/internal/sandbox-control.ts +27 -3
- package/lib/internal/task-activity.ts +3 -3
- package/lib/internal/task-artifact.ts +65 -22
- package/lib/internal/task-create.ts +2 -2
- package/lib/internal/task-event.ts +6 -3
- package/lib/internal/task-finalization.ts +3 -3
- package/lib/internal/task-invalidation.ts +39 -0
- package/lib/internal/task-ledger.ts +11 -8
- package/lib/internal/task-lifecycle.ts +2 -2
- package/lib/internal/task-override.ts +6 -6
- package/lib/internal/task-review.ts +3 -3
- package/lib/internal/task-verify.ts +6 -6
- package/lib/internal/task-warning.ts +3 -3
- package/lib/platform/adapters.ts +3 -2
- package/lib/platform/change-request-git-evidence.ts +36 -9
- package/lib/platform/completion-backfill.ts +9 -8
- package/lib/platform/context.ts +97 -134
- package/lib/platform/github-client.ts +2 -2
- package/lib/platform/github-provider.ts +910 -0
- package/lib/platform/in-label-sync.ts +208 -0
- package/lib/platform/issue-comments.ts +97 -47
- package/lib/platform/issues.ts +344 -174
- package/lib/platform/metadata-labels.ts +5 -2
- package/lib/platform/none-provider.ts +35 -0
- package/lib/platform/pr-change-report.ts +645 -0
- package/lib/platform/pr-checks.ts +126 -106
- package/lib/platform/pr-review.ts +88 -85
- package/lib/platform/pr-summary.ts +560 -53
- package/lib/platform/provider-bridge.ts +82 -0
- package/lib/platform/provider-contract.ts +536 -0
- package/lib/platform/provider-loader.ts +307 -0
- package/lib/platform/provider-validation.ts +529 -0
- package/lib/platform/pull-request-metadata.ts +10 -7
- package/lib/platform/pull-requests.ts +826 -543
- package/lib/platform/release-notes.ts +76 -59
- package/lib/platform/releases.ts +77 -43
- package/lib/platform/resource-identity.ts +121 -0
- package/lib/platform/task-identities.ts +40 -0
- package/lib/platform/types.ts +4 -2
- package/lib/platform/verification-required.ts +11 -13
- package/lib/platform/verification-sync.ts +112 -292
- package/lib/pr-review/evidence-grading.ts +25 -8
- package/lib/run/index.ts +57 -22
- package/lib/sandbox/control/client.ts +105 -15
- package/lib/sandbox/control/executor.ts +4 -4
- package/lib/sandbox/control/protocol.ts +113 -0
- package/lib/sandbox/control/server.ts +431 -23
- package/lib/sandbox/control/state.ts +251 -1
- package/lib/server/protocol.ts +36 -1
- package/lib/task/activity-log.ts +8 -0
- package/lib/task/artifact-lifecycle.ts +32 -6
- package/lib/task/capabilities.ts +262 -0
- package/lib/task/commands/status.ts +38 -0
- package/lib/task/commit-operation.ts +12 -1
- package/lib/task/control-authority.ts +11 -8
- package/lib/task/create-service.ts +8 -8
- package/lib/task/current-contract.ts +6 -1
- package/lib/task/decision-details.ts +6 -2
- package/lib/task/events.ts +296 -16
- package/lib/task/finalization.ts +50 -14
- package/lib/task/human-override.ts +70 -46
- package/lib/task/invalidation-command.ts +86 -0
- package/lib/task/invalidation.ts +332 -0
- package/lib/task/ledger-intents.ts +35 -1
- package/lib/task/local-artifact-finalization.ts +563 -0
- package/lib/task/orchestration.ts +52 -85
- package/lib/task/pr-delivery-fact.ts +131 -148
- package/lib/task/review-fingerprint.ts +23 -2
- package/lib/task/rework-intent.ts +165 -0
- package/lib/task/skill-scope.ts +24 -0
- package/lib/task/task-execution-lock.ts +23 -14
- package/lib/task/verification-engine.ts +52 -17
- package/lib/task/verification.ts +4 -4
- package/lib/task/workflow-warning-intents.ts +1 -1
- package/lib/task/write.ts +19 -0
- package/lib/update.ts +1 -1
- package/lib/version.ts +15 -1
- package/package.json +7 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.en.md +2 -0
- package/templates/.agents/rules/issue-sync.zh-CN.md +2 -0
- package/templates/.agents/rules/local-artifact-repair.en.md +9 -2
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +9 -2
- package/templates/.agents/rules/pr-sync.en.md +10 -4
- package/templates/.agents/rules/pr-sync.zh-CN.md +17 -4
- package/templates/.agents/rules/task-short-id.en.md +1 -4
- package/templates/.agents/rules/task-short-id.zh-CN.md +1 -3
- package/templates/.agents/skills/analyze-task/SKILL.en.md +13 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +10 -3
- package/templates/.agents/skills/check-task/SKILL.en.md +16 -37
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +16 -37
- package/templates/.agents/skills/code-task/SKILL.en.md +27 -8
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +29 -10
- package/templates/.agents/skills/code-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.en.md +2 -0
- package/templates/.agents/skills/commit/reference/issue-metadata-sync.zh-CN.md +2 -0
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +3 -3
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +3 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -3
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +4 -3
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +4 -3
- package/templates/.agents/skills/complete-task/SKILL.en.md +21 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +20 -3
- package/templates/.agents/skills/create-pr/SKILL.en.md +7 -6
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +7 -7
- package/templates/.agents/skills/create-pr/config/verify.json +1 -0
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +16 -4
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +16 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +5 -3
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +5 -3
- package/templates/.agents/skills/create-pr/reference/pr-body-template.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/pr-body-template.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/scripts/change-report.mjs +3 -0
- package/templates/.agents/skills/plan-task/SKILL.en.md +13 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +10 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -3
- 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-plan/SKILL.en.md +3 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -2
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -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 +6 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +6 -0
- package/templates/.agents/templates/task.en.md +18 -1
- package/templates/.agents/templates/task.zh-CN.md +18 -1
- package/templates/.github/workflows/pr-label.yml +7 -26
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const INVALIDATION_HEADINGS = ['产物失效记录', 'Artifact Invalidation'] as const;
|
|
4
|
+
const OPERATION_COLUMNS = [
|
|
5
|
+
'operation_id', 'source_family', 'source_artifact', 'source_round', 'source_sha256',
|
|
6
|
+
'status', 'processed', 'total', 'created_at', 'updated_at', 'completed_at', 'error'
|
|
7
|
+
] as const;
|
|
8
|
+
const TARGET_COLUMNS = [
|
|
9
|
+
'target_id', 'operation_id', 'target_kind', 'target_family', 'target_artifact',
|
|
10
|
+
'target_round', 'target_sha256', 'status', 'reason_code', 'updated_at'
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
type InvalidationStatus = 'pending' | 'in-progress' | 'completed';
|
|
14
|
+
type InvalidationTargetKind = 'artifact' | 'receipt' | 'approval' | 'reviewed-snapshot';
|
|
15
|
+
type InvalidationOperation = {
|
|
16
|
+
operationId: string;
|
|
17
|
+
sourceFamily: string;
|
|
18
|
+
sourceArtifact: string;
|
|
19
|
+
sourceRound: number;
|
|
20
|
+
sourceSha256: string;
|
|
21
|
+
status: InvalidationStatus;
|
|
22
|
+
processed: number;
|
|
23
|
+
total: number;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
updatedAt: string;
|
|
26
|
+
completedAt: string;
|
|
27
|
+
error: string;
|
|
28
|
+
};
|
|
29
|
+
type InvalidationTarget = {
|
|
30
|
+
targetId: string;
|
|
31
|
+
operationId: string;
|
|
32
|
+
targetKind: InvalidationTargetKind;
|
|
33
|
+
targetFamily: string;
|
|
34
|
+
targetArtifact: string;
|
|
35
|
+
targetRound: number;
|
|
36
|
+
targetSha256: string;
|
|
37
|
+
status: InvalidationStatus;
|
|
38
|
+
reasonCode: string;
|
|
39
|
+
updatedAt: string;
|
|
40
|
+
};
|
|
41
|
+
type InvalidationDocument = {
|
|
42
|
+
operations: readonly InvalidationOperation[];
|
|
43
|
+
targets: readonly InvalidationTarget[];
|
|
44
|
+
};
|
|
45
|
+
type InvalidationParseResult =
|
|
46
|
+
| { ok: true; present: boolean; document: InvalidationDocument }
|
|
47
|
+
| { ok: false; code: 'TASK_INVALIDATION_INVALID'; message: string };
|
|
48
|
+
type InvalidationMutationResult = {
|
|
49
|
+
ok: true;
|
|
50
|
+
changed: boolean;
|
|
51
|
+
document: InvalidationDocument;
|
|
52
|
+
} | {
|
|
53
|
+
ok: false;
|
|
54
|
+
code: 'TASK_INVALIDATION_INVALID' | 'INVALIDATION_IDENTITY_CONFLICT';
|
|
55
|
+
message: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function invalid(message: string): InvalidationParseResult {
|
|
59
|
+
return { ok: false, code: 'TASK_INVALIDATION_INVALID', message };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hashIdentity(value: unknown): string {
|
|
63
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 16);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function operationIdFor(source: Pick<InvalidationOperation, 'sourceFamily' | 'sourceArtifact' | 'sourceRound' | 'sourceSha256'>): string {
|
|
67
|
+
return `INV-${hashIdentity([source.sourceFamily, source.sourceArtifact, source.sourceRound, source.sourceSha256])}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function targetIdFor(operationId: string, target: Pick<InvalidationTarget, 'targetKind' | 'targetFamily' | 'targetArtifact' | 'targetRound' | 'targetSha256'>): string {
|
|
71
|
+
return `INV-T-${hashIdentity([operationId, target.targetKind, target.targetFamily, target.targetArtifact, target.targetRound, target.targetSha256])}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createInvalidationOperation(
|
|
75
|
+
source: Partial<Pick<InvalidationOperation, 'operationId'>> & Omit<InvalidationOperation, 'operationId' | 'status' | 'processed' | 'total' | 'completedAt' | 'error'>,
|
|
76
|
+
targets: readonly InvalidationTarget[] = []
|
|
77
|
+
): InvalidationOperation {
|
|
78
|
+
const operationId = source.operationId ?? operationIdFor(source);
|
|
79
|
+
return {
|
|
80
|
+
operationId, sourceFamily: source.sourceFamily, sourceArtifact: source.sourceArtifact,
|
|
81
|
+
sourceRound: source.sourceRound, sourceSha256: source.sourceSha256,
|
|
82
|
+
status: 'pending', processed: 0, total: targets.length,
|
|
83
|
+
createdAt: source.createdAt, updatedAt: source.updatedAt, completedAt: '', error: ''
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function lineCells(line: string): string[] | null {
|
|
88
|
+
const first = line.indexOf('|');
|
|
89
|
+
const last = line.lastIndexOf('|');
|
|
90
|
+
if (first < 0 || first === last || line.slice(0, first).trim() || line.slice(last + 1).trim()) return null;
|
|
91
|
+
const inner = line.slice(first + 1, last);
|
|
92
|
+
const cells: string[] = [];
|
|
93
|
+
let start = 0;
|
|
94
|
+
let escaped = false;
|
|
95
|
+
for (let index = 0; index < inner.length; index += 1) {
|
|
96
|
+
const char = inner[index]!;
|
|
97
|
+
if (char === '|' && !escaped) {
|
|
98
|
+
cells.push(inner.slice(start, index));
|
|
99
|
+
start = index + 1;
|
|
100
|
+
}
|
|
101
|
+
escaped = char === '\\' && !escaped;
|
|
102
|
+
if (char !== '\\') escaped = false;
|
|
103
|
+
}
|
|
104
|
+
cells.push(inner.slice(start));
|
|
105
|
+
return cells.map((cell) => cell.replace(/\\([\\|])/g, '$1').trim());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isSeparator(cells: readonly string[], count: number): boolean {
|
|
109
|
+
return cells.length === count && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sectionBody(content: string): string | null {
|
|
113
|
+
const heading = INVALIDATION_HEADINGS.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
114
|
+
const match = new RegExp(`^##\\s+(${heading})\\s*$`, 'm').exec(content);
|
|
115
|
+
if (!match) return null;
|
|
116
|
+
const start = (match.index ?? 0) + match[0].length;
|
|
117
|
+
const rest = content.slice(start);
|
|
118
|
+
const end = rest.search(/^##\s+/m);
|
|
119
|
+
return rest.slice(0, end < 0 ? rest.length : end);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseTable(body: string, heading: string, columns: readonly string[]): Record<string, string>[] | null | false {
|
|
123
|
+
const marker = new RegExp(`^###\\s+${heading}\\s*$`, 'm').exec(body);
|
|
124
|
+
if (!marker) return null;
|
|
125
|
+
const start = (marker.index ?? 0) + marker[0].length;
|
|
126
|
+
const rest = body.slice(start);
|
|
127
|
+
const end = rest.search(/^###\s+/m);
|
|
128
|
+
const lines = rest.slice(0, end < 0 ? rest.length : end).split(/\r?\n/).filter((line) => line.trim());
|
|
129
|
+
const header = lines.length > 0 ? lineCells(lines[0]!) : null;
|
|
130
|
+
const separator = lines.length > 1 ? lineCells(lines[1]!) : null;
|
|
131
|
+
if (!header || !separator || header.length !== columns.length || !header.every((value, index) => value === columns[index]) || !isSeparator(separator, columns.length)) return false;
|
|
132
|
+
const rows: Record<string, string>[] = [];
|
|
133
|
+
for (const line of lines.slice(2)) {
|
|
134
|
+
const cells = lineCells(line);
|
|
135
|
+
if (!cells || cells.length !== columns.length) return false;
|
|
136
|
+
rows.push(Object.fromEntries(columns.map((column, index) => [column, cells[index]!])));
|
|
137
|
+
}
|
|
138
|
+
return rows;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function required(value: string, field: string): string {
|
|
142
|
+
if (!value) throw new Error(`${field} is required`);
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function status(value: string, field: string): InvalidationStatus {
|
|
147
|
+
if (value !== 'pending' && value !== 'in-progress' && value !== 'completed') throw new Error(`${field} has an invalid status`);
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function numberValue(value: string, field: string, allowZero = true): number {
|
|
152
|
+
const parsed = Number(value);
|
|
153
|
+
if (!Number.isInteger(parsed) || (allowZero ? parsed < 0 : parsed < 1)) throw new Error(`${field} must be an integer`);
|
|
154
|
+
return parsed;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseInvalidationDocument(content: string): InvalidationParseResult {
|
|
158
|
+
const body = sectionBody(content);
|
|
159
|
+
if (body === null) return { ok: true, present: false, document: { operations: [], targets: [] } };
|
|
160
|
+
try {
|
|
161
|
+
const operationRows = parseTable(body, 'Operations', OPERATION_COLUMNS);
|
|
162
|
+
const targetRows = parseTable(body, 'Targets', TARGET_COLUMNS);
|
|
163
|
+
if (operationRows === null || targetRows === null || operationRows === false || targetRows === false) {
|
|
164
|
+
return invalid('invalidation section must contain Operations and Targets tables');
|
|
165
|
+
}
|
|
166
|
+
const operations = operationRows.map((row) => ({
|
|
167
|
+
operationId: required(row.operation_id!, 'operation_id'), sourceFamily: required(row.source_family!, 'source_family'),
|
|
168
|
+
sourceArtifact: required(row.source_artifact!, 'source_artifact'), sourceRound: numberValue(row.source_round!, 'source_round', false),
|
|
169
|
+
sourceSha256: required(row.source_sha256!, 'source_sha256'), status: status(row.status!, 'operation status'),
|
|
170
|
+
processed: numberValue(row.processed!, 'processed'), total: numberValue(row.total!, 'total'),
|
|
171
|
+
createdAt: required(row.created_at!, 'created_at'), updatedAt: required(row.updated_at!, 'updated_at'),
|
|
172
|
+
completedAt: row.completed_at ?? '', error: row.error ?? ''
|
|
173
|
+
} satisfies InvalidationOperation));
|
|
174
|
+
const targets = targetRows.map((row) => ({
|
|
175
|
+
targetId: required(row.target_id!, 'target_id'), operationId: required(row.operation_id!, 'operation_id'),
|
|
176
|
+
targetKind: row.target_kind as InvalidationTargetKind, targetFamily: required(row.target_family!, 'target_family'),
|
|
177
|
+
targetArtifact: required(row.target_artifact!, 'target_artifact'), targetRound: numberValue(row.target_round!, 'target_round', false),
|
|
178
|
+
targetSha256: required(row.target_sha256!, 'target_sha256'), status: status(row.status!, 'target status'),
|
|
179
|
+
reasonCode: required(row.reason_code!, 'reason_code'), updatedAt: required(row.updated_at!, 'updated_at')
|
|
180
|
+
} satisfies InvalidationTarget));
|
|
181
|
+
const operationIds = new Set<string>();
|
|
182
|
+
for (const operation of operations) {
|
|
183
|
+
if (operationIds.has(operation.operationId)) throw new Error(`duplicate operation '${operation.operationId}'`);
|
|
184
|
+
operationIds.add(operation.operationId);
|
|
185
|
+
if (operation.operationId !== operationIdFor(operation)) throw new Error(`operation '${operation.operationId}' has a non-canonical identity`);
|
|
186
|
+
if (operation.processed > operation.total) throw new Error(`operation '${operation.operationId}' progress is invalid`);
|
|
187
|
+
if (!/^[a-f0-9]{64}$/.test(operation.sourceSha256)) throw new Error(`operation '${operation.operationId}' source hash is invalid`);
|
|
188
|
+
}
|
|
189
|
+
const targetIds = new Set<string>();
|
|
190
|
+
for (const target of targets) {
|
|
191
|
+
if (!['artifact', 'receipt', 'approval', 'reviewed-snapshot'].includes(target.targetKind)) throw new Error(`target '${target.targetId}' kind is invalid`);
|
|
192
|
+
if (targetIds.has(target.targetId)) throw new Error(`duplicate target '${target.targetId}'`);
|
|
193
|
+
targetIds.add(target.targetId);
|
|
194
|
+
if (!operationIds.has(target.operationId)) throw new Error(`target '${target.targetId}' references an unknown operation`);
|
|
195
|
+
if (target.targetId !== targetIdFor(target.operationId, target)) throw new Error(`target '${target.targetId}' has a non-canonical identity`);
|
|
196
|
+
if (!/^[a-f0-9]{64}$/.test(target.targetSha256)) throw new Error(`target '${target.targetId}' hash is invalid`);
|
|
197
|
+
}
|
|
198
|
+
for (const operation of operations) {
|
|
199
|
+
const scoped = targets.filter((target) => target.operationId === operation.operationId);
|
|
200
|
+
const completed = scoped.filter((target) => target.status === 'completed').length;
|
|
201
|
+
if (operation.total !== scoped.length || operation.processed !== completed) throw new Error(`operation '${operation.operationId}' progress does not match its targets`);
|
|
202
|
+
if (operation.status === 'completed' && (completed !== scoped.length || !operation.completedAt)) throw new Error(`completed operation '${operation.operationId}' has incomplete targets`);
|
|
203
|
+
if (operation.status === 'pending' && completed !== 0) throw new Error(`pending operation '${operation.operationId}' has completed targets`);
|
|
204
|
+
if (operation.status === 'in-progress' && (completed === scoped.length || (completed === 0 && !scoped.some((target) => target.status === 'in-progress')))) throw new Error(`in-progress operation '${operation.operationId}' has invalid progress`);
|
|
205
|
+
}
|
|
206
|
+
return { ok: true, present: true, document: { operations, targets } };
|
|
207
|
+
} catch (error) {
|
|
208
|
+
return invalid(error instanceof Error ? error.message : String(error));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function cell(value: string | number): string {
|
|
213
|
+
return String(value).replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/[\r\n]/g, ' ');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function table(columns: readonly string[], rows: readonly (readonly (string | number)[])[]): string {
|
|
217
|
+
const header = `| ${columns.join(' | ')} |`;
|
|
218
|
+
const separator = `| ${columns.map(() => '---').join(' | ')} |`;
|
|
219
|
+
return [header, separator, ...rows.map((row) => `| ${row.map(cell).join(' | ')} |`)].join('\n');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function renderInvalidation(document: InvalidationDocument): string {
|
|
223
|
+
const operations = [...document.operations].sort((left, right) => left.operationId.localeCompare(right.operationId));
|
|
224
|
+
const targets = [...document.targets].sort((left, right) => left.targetId.localeCompare(right.targetId));
|
|
225
|
+
return [
|
|
226
|
+
'### Operations', '',
|
|
227
|
+
table(OPERATION_COLUMNS, operations.map((row) => [
|
|
228
|
+
row.operationId, row.sourceFamily, row.sourceArtifact, row.sourceRound, row.sourceSha256,
|
|
229
|
+
row.status, row.processed, row.total, row.createdAt, row.updatedAt, row.completedAt, row.error
|
|
230
|
+
])), '',
|
|
231
|
+
'### Targets', '',
|
|
232
|
+
table(TARGET_COLUMNS, targets.map((row) => [
|
|
233
|
+
row.targetId, row.operationId, row.targetKind, row.targetFamily, row.targetArtifact,
|
|
234
|
+
row.targetRound, row.targetSha256, row.status, row.reasonCode, row.updatedAt
|
|
235
|
+
]))
|
|
236
|
+
].join('\n');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function reconcileInvalidation(document: InvalidationDocument, now: string, maxTargets = Number.POSITIVE_INFINITY): InvalidationMutationResult {
|
|
240
|
+
const nextTargets = document.targets.map((target) => {
|
|
241
|
+
if (target.status === 'completed' || maxTargets <= 0) return target;
|
|
242
|
+
maxTargets -= 1;
|
|
243
|
+
return { ...target, status: 'completed' as const, updatedAt: now };
|
|
244
|
+
});
|
|
245
|
+
const changed = nextTargets.some((target, index) => target !== document.targets[index]);
|
|
246
|
+
const nextOperations = document.operations.map((operation) => {
|
|
247
|
+
const scoped = nextTargets.filter((target) => target.operationId === operation.operationId);
|
|
248
|
+
const processed = scoped.filter((target) => target.status === 'completed').length;
|
|
249
|
+
const total = scoped.length;
|
|
250
|
+
const complete = total === processed;
|
|
251
|
+
const nextStatus: InvalidationStatus = complete ? 'completed' : processed > 0 ? 'in-progress' : 'pending';
|
|
252
|
+
if (operation.status === nextStatus && operation.processed === processed && operation.total === total && (!complete || operation.completedAt)) return operation;
|
|
253
|
+
return {
|
|
254
|
+
...operation, status: nextStatus, processed, total, updatedAt: changed ? now : operation.updatedAt,
|
|
255
|
+
completedAt: complete ? (operation.completedAt || now) : ''
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
const operationChanged = nextOperations.some((operation, index) => operation !== document.operations[index]);
|
|
259
|
+
return { ok: true, changed: changed || operationChanged, document: { operations: nextOperations, targets: nextTargets } };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function upsertInvalidation(
|
|
263
|
+
document: InvalidationDocument,
|
|
264
|
+
operation: InvalidationOperation,
|
|
265
|
+
targets: readonly InvalidationTarget[]
|
|
266
|
+
): InvalidationMutationResult {
|
|
267
|
+
const existing = document.operations.find((candidate) => candidate.operationId === operation.operationId);
|
|
268
|
+
if (existing && JSON.stringify(existing) !== JSON.stringify(operation)) {
|
|
269
|
+
return { ok: false, code: 'INVALIDATION_IDENTITY_CONFLICT', message: `operation '${operation.operationId}' identity conflicts with existing state` };
|
|
270
|
+
}
|
|
271
|
+
const existingTargets = document.targets.filter((target) => target.operationId === operation.operationId);
|
|
272
|
+
for (const target of targets) {
|
|
273
|
+
const old = existingTargets.find((candidate) => candidate.targetId === target.targetId);
|
|
274
|
+
if (old && JSON.stringify(old) !== JSON.stringify(target)) {
|
|
275
|
+
return { ok: false, code: 'INVALIDATION_IDENTITY_CONFLICT', message: `target '${target.targetId}' identity conflicts with existing state` };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (existing) return { ok: true, changed: false, document };
|
|
279
|
+
return {
|
|
280
|
+
ok: true, changed: true,
|
|
281
|
+
document: { operations: [...document.operations, operation], targets: [...document.targets, ...targets] }
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function invalidationBlocks(document: InvalidationDocument): boolean {
|
|
286
|
+
return document.operations.some((operation) => operation.status !== 'completed')
|
|
287
|
+
|| document.targets.some((target) => target.status !== 'completed');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function isArtifactInvalidated(document: InvalidationDocument, family: string, artifact: string): boolean {
|
|
291
|
+
return document.targets.some((target) =>
|
|
292
|
+
target.status === 'completed'
|
|
293
|
+
&& target.targetKind === 'artifact'
|
|
294
|
+
&& target.targetFamily === family
|
|
295
|
+
&& target.targetArtifact === artifact
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function invalidationMutation(content: string, document: InvalidationDocument) {
|
|
300
|
+
const existing = sectionBody(content) !== null;
|
|
301
|
+
return {
|
|
302
|
+
kind: 'section' as const,
|
|
303
|
+
aliases: INVALIDATION_HEADINGS,
|
|
304
|
+
heading: existing && /^##\s+Artifact Invalidation\s*$/m.test(content) ? INVALIDATION_HEADINGS[1] : INVALIDATION_HEADINGS[0],
|
|
305
|
+
body: renderInvalidation(document)
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export {
|
|
310
|
+
INVALIDATION_HEADINGS,
|
|
311
|
+
OPERATION_COLUMNS,
|
|
312
|
+
TARGET_COLUMNS,
|
|
313
|
+
createInvalidationOperation,
|
|
314
|
+
isArtifactInvalidated,
|
|
315
|
+
invalidationBlocks,
|
|
316
|
+
invalidationMutation,
|
|
317
|
+
operationIdFor,
|
|
318
|
+
parseInvalidationDocument,
|
|
319
|
+
reconcileInvalidation,
|
|
320
|
+
renderInvalidation,
|
|
321
|
+
targetIdFor,
|
|
322
|
+
upsertInvalidation
|
|
323
|
+
};
|
|
324
|
+
export type {
|
|
325
|
+
InvalidationDocument,
|
|
326
|
+
InvalidationMutationResult,
|
|
327
|
+
InvalidationOperation,
|
|
328
|
+
InvalidationParseResult,
|
|
329
|
+
InvalidationStatus,
|
|
330
|
+
InvalidationTarget,
|
|
331
|
+
InvalidationTargetKind
|
|
332
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
declareImplementationInput,
|
|
@@ -13,6 +14,8 @@ import { resolveTaskRef } from './resolve-ref.ts';
|
|
|
13
14
|
import { writeTask } from './write.ts';
|
|
14
15
|
import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
|
|
15
16
|
import { allowsManualOverride } from './guard-override.ts';
|
|
17
|
+
import { parseReworkIntentDocument, reworkIntentMutation, upsertReworkIntent } from './rework-intent.ts';
|
|
18
|
+
import type { ReworkIntent, ReworkTarget } from './rework-intent.ts';
|
|
16
19
|
|
|
17
20
|
type ReviewSeverity = 'blocker' | 'major' | 'minor';
|
|
18
21
|
type ExecutorResponse = 'accepted' | 'adjusted' | 'refuted' | 'cannot-judge';
|
|
@@ -22,7 +25,8 @@ type LedgerIntent =
|
|
|
22
25
|
| { kind: 'finding-respond'; taskRef: string; id: string; round: number; status: ExecutorResponse; evidence: string; dryRun?: boolean }
|
|
23
26
|
| { kind: 'finding-review'; taskRef: string; id: string; status: ReviewDisposition; evidence: string; needsImplementation?: boolean; dryRun?: boolean }
|
|
24
27
|
| { kind: 'decision-next-id'; taskRef: string }
|
|
25
|
-
| { kind: 'decision-upsert'; taskRef: string; id: string; stage: ReviewStage; artifact: string; needsImplementation?: boolean; dryRun?: boolean }
|
|
28
|
+
| { kind: 'decision-upsert'; taskRef: string; id: string; stage: ReviewStage; artifact: string; needsImplementation?: boolean; dryRun?: boolean }
|
|
29
|
+
| { kind: 'rework-intent-upsert'; taskRef: string; intentId: string; findingId: string; sourceArtifact: string; sourceSha256: string; target: ReworkTarget; dryRun?: boolean };
|
|
26
30
|
|
|
27
31
|
type LedgerIntentError = { code: string; message: string };
|
|
28
32
|
type LedgerIntentResult = {
|
|
@@ -136,6 +140,36 @@ function applyLedgerIntent(intent: LedgerIntent, options: TaskWriteOptions = {})
|
|
|
136
140
|
return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: nextHdId(rows), before: null, after: null, operations: [], error: null };
|
|
137
141
|
}
|
|
138
142
|
|
|
143
|
+
if (intent.kind === 'rework-intent-upsert') {
|
|
144
|
+
if (!/^RI-[1-9]\d*$/.test(intent.intentId) || !/^(AN|PL|CD)-[1-9]\d*$/.test(intent.findingId) || !['analysis', 'plan', 'code'].includes(intent.target) || !/^[a-f0-9]{64}$/.test(intent.sourceSha256)) {
|
|
145
|
+
return failed(intent, 'LEDGER_PAYLOAD_INVALID', 'rework intent identity, target, or source hash is invalid', resolved.taskId, intent.intentId);
|
|
146
|
+
}
|
|
147
|
+
const finding = rows.find((row) => row.id === intent.findingId);
|
|
148
|
+
if (!finding || finding.severity === 'decision') return failed(intent, 'LEDGER_NOT_FOUND', `finding '${intent.findingId}' was not found`, resolved.taskId, intent.findingId);
|
|
149
|
+
const sourceFromEvidence = finding.evidence.split('#')[0];
|
|
150
|
+
if (sourceFromEvidence !== intent.sourceArtifact || !REVIEW_ARTIFACT[finding.stage as ReviewStage]?.test(intent.sourceArtifact)) {
|
|
151
|
+
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact does not match finding evidence', resolved.taskId, intent.intentId);
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const actualHash = createHash('sha256').update(fs.readFileSync(path.join(resolved.taskDir, intent.sourceArtifact))).digest('hex');
|
|
155
|
+
if (actualHash !== intent.sourceSha256) return failed(intent, 'LEDGER_IDENTITY_CONFLICT', 'rework intent source artifact hash does not match', resolved.taskId, intent.intentId);
|
|
156
|
+
const parsed = parseReworkIntentDocument(content);
|
|
157
|
+
if (!parsed.ok) return failed(intent, parsed.code, parsed.message, resolved.taskId, intent.intentId);
|
|
158
|
+
const nextIntent: ReworkIntent = {
|
|
159
|
+
intentId: intent.intentId, findingId: intent.findingId, sourceArtifact: intent.sourceArtifact,
|
|
160
|
+
sourceSha256: intent.sourceSha256, target: intent.target, status: 'pending',
|
|
161
|
+
declaredAt: new Date().toISOString(), consumedAt: ''
|
|
162
|
+
};
|
|
163
|
+
const next = upsertReworkIntent(parsed.intents, nextIntent);
|
|
164
|
+
if (!next.changed) return { status: 'no-op', changed: false, intent: intent.kind, taskId: resolved.taskId, entityId: intent.intentId, before: null, after: null, operations: [], error: null };
|
|
165
|
+
const result = writeTask({ taskRef: intent.taskRef, expectedState: 'active', dryRun: intent.dryRun, mutations: [reworkIntentMutation(content, next.intents)] }, { ...options, taskLocation: { repoRoot: resolved.repoRoot, taskId: resolved.taskId, taskMdPath: resolved.taskMdPath, state: resolved.state } });
|
|
166
|
+
if (result.status === 'failed') return failed(intent, result.error.code, result.error.message, result.taskId, intent.intentId);
|
|
167
|
+
return { status: result.status, changed: result.changed, intent: intent.kind, taskId: result.taskId, entityId: intent.intentId, before: null, after: null, operations: result.operations, error: null };
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return failed(intent, 'LEDGER_IDENTITY_CONFLICT', error instanceof Error ? error.message : String(error), resolved.taskId, intent.intentId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
139
173
|
let before: LedgerRow | null = null;
|
|
140
174
|
let after: LedgerRow;
|
|
141
175
|
if (intent.kind === 'finding-upsert') {
|