@fitlab-ai/agent-infra 0.11.6 → 0.11.7
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/lib/git/primary-worktree.js +58 -0
- package/dist/lib/platform/pull-requests.js +29 -3
- package/dist/lib/sandbox/control/server.js +6 -2
- package/dist/lib/server/config.js +7 -27
- package/dist/lib/server/daemon.js +4 -3
- package/dist/lib/server/process-control.js +4 -2
- package/dist/lib/server/runner.js +4 -3
- package/dist/lib/task/artifact-lifecycle.js +42 -10
- package/dist/lib/task/artifact-receipts.js +62 -22
- package/dist/lib/task/capabilities.js +22 -4
- package/dist/lib/task/completion-facts.js +56 -0
- package/dist/lib/task/create-service.js +6 -2
- package/dist/lib/task/create.js +46 -82
- package/dist/lib/task/events.js +268 -195
- package/dist/lib/task/finalization-handoff.js +2 -1
- package/dist/lib/task/invalidation-command.js +4 -2
- package/dist/lib/task/invalidation.js +38 -9
- package/dist/lib/task/local-artifact-finalization.js +17 -3
- package/dist/lib/task/qualification-audit.js +23 -105
- package/dist/lib/task/review-finalization.js +7 -0
- package/dist/lib/task/review-identity.js +69 -0
- package/dist/lib/task/sections.js +6 -5
- package/dist/lib/task/task-execution-lock.js +6 -3
- package/dist/lib/task/verification-engine.js +14 -63
- package/dist/lib/task/verification.js +35 -38
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/git/primary-worktree.ts +62 -0
- package/lib/platform/pull-requests.ts +27 -3
- package/lib/sandbox/control/server.ts +5 -2
- package/lib/server/config.ts +7 -29
- package/lib/server/daemon.ts +5 -3
- package/lib/server/process-control.ts +4 -2
- package/lib/server/runner.ts +7 -4
- package/lib/server/server.schema.json +0 -4
- package/lib/task/artifact-lifecycle.ts +35 -11
- package/lib/task/artifact-receipts.ts +55 -15
- package/lib/task/capabilities.ts +17 -3
- package/lib/task/completion-facts.ts +66 -0
- package/lib/task/create-service.ts +7 -3
- package/lib/task/create.ts +44 -71
- package/lib/task/events.ts +248 -182
- package/lib/task/finalization-handoff.ts +2 -1
- package/lib/task/invalidation-command.ts +4 -2
- package/lib/task/invalidation.ts +35 -10
- package/lib/task/local-artifact-finalization.ts +13 -3
- package/lib/task/qualification-audit.ts +21 -114
- package/lib/task/review-finalization.ts +7 -0
- package/lib/task/review-identity.ts +85 -0
- package/lib/task/sections.ts +6 -6
- package/lib/task/task-execution-lock.ts +7 -3
- package/lib/task/verification-engine.ts +13 -86
- package/lib/task/verification.ts +34 -38
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -0
- package/templates/.agents/rules/README.zh-CN.md +1 -0
- package/templates/.agents/rules/decision-qualification.en.md +4 -3
- package/templates/.agents/rules/decision-qualification.zh-CN.md +4 -3
- package/templates/.agents/rules/validation-output.en.md +19 -0
- package/templates/.agents/rules/validation-output.zh-CN.md +19 -0
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.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 +2 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.en.md +2 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +2 -0
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/review-analysis/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +4 -8
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +4 -8
- package/templates/.agents/skills/review-code/SKILL.en.md +13 -12
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +11 -12
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -3
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -3
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +23 -46
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +27 -48
- package/templates/.agents/skills/review-code/reference/report-template.en.md +8 -11
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +8 -11
- package/templates/.agents/skills/review-code/reference/review-criteria.en.md +2 -2
- package/templates/.agents/skills/review-code/reference/review-criteria.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/SKILL.en.md +3 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +5 -9
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +5 -9
- package/templates/.agents/skills/review-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +2 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -0
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/templates/task.en.md +2 -2
- package/templates/.agents/templates/task.zh-CN.md +2 -2
package/lib/task/invalidation.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { parseArtifactName } from './artifact-name.ts';
|
|
2
3
|
import { findSectionRange, parseTable } from './sections.ts';
|
|
3
4
|
import { scanVisibleMarkdown } from './markdown.ts';
|
|
4
5
|
|
|
@@ -8,6 +9,10 @@ const OPERATION_COLUMNS = [
|
|
|
8
9
|
'status', 'processed', 'total', 'created_at', 'updated_at', 'completed_at', 'error'
|
|
9
10
|
] as const;
|
|
10
11
|
const TARGET_COLUMNS = [
|
|
12
|
+
'target_id', 'operation_id', 'target_kind', 'target_family', 'target_artifact', 'target_input',
|
|
13
|
+
'target_round', 'target_sha256', 'status', 'reason_code', 'updated_at'
|
|
14
|
+
] as const;
|
|
15
|
+
const LEGACY_TARGET_COLUMNS = [
|
|
11
16
|
'target_id', 'operation_id', 'target_kind', 'target_family', 'target_artifact',
|
|
12
17
|
'target_round', 'target_sha256', 'status', 'reason_code', 'updated_at'
|
|
13
18
|
] as const;
|
|
@@ -34,6 +39,7 @@ type InvalidationTarget = {
|
|
|
34
39
|
targetKind: InvalidationTargetKind;
|
|
35
40
|
targetFamily: string;
|
|
36
41
|
targetArtifact: string;
|
|
42
|
+
targetInput?: string;
|
|
37
43
|
targetRound: number;
|
|
38
44
|
targetSha256: string;
|
|
39
45
|
status: InvalidationStatus;
|
|
@@ -69,8 +75,8 @@ function operationIdFor(source: Pick<InvalidationOperation, 'sourceFamily' | 'so
|
|
|
69
75
|
return `INV-${hashIdentity([source.sourceFamily, source.sourceArtifact, source.sourceRound, source.sourceSha256])}`;
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
function targetIdFor(operationId: string, target: Pick<InvalidationTarget, 'targetKind' | 'targetFamily' | 'targetArtifact' | 'targetRound' | 'targetSha256'>): string {
|
|
73
|
-
return `INV-T-${hashIdentity([operationId, target.targetKind, target.targetFamily, target.targetArtifact, target.targetRound, target.targetSha256])}`;
|
|
78
|
+
function targetIdFor(operationId: string, target: Pick<InvalidationTarget, 'targetKind' | 'targetFamily' | 'targetArtifact' | 'targetInput' | 'targetRound' | 'targetSha256'>): string {
|
|
79
|
+
return `INV-T-${hashIdentity([operationId, target.targetKind, target.targetFamily, target.targetArtifact, ...(target.targetKind === 'receipt' && target.targetInput ? [target.targetInput] : []), target.targetRound, target.targetSha256])}`;
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
function createInvalidationOperation(
|
|
@@ -86,14 +92,27 @@ function createInvalidationOperation(
|
|
|
86
92
|
};
|
|
87
93
|
}
|
|
88
94
|
|
|
89
|
-
function parseInvalidationTable(body: string, heading: string,
|
|
95
|
+
function parseInvalidationTable(body: string, heading: string, columnChoices: readonly (readonly string[])[]) {
|
|
90
96
|
const section = findSectionRange(body, [heading], 3);
|
|
91
97
|
const lines = scanVisibleMarkdown(body).lines.filter((line) => line.text.trim());
|
|
92
|
-
|
|
98
|
+
let table: ReturnType<typeof parseTable> = null;
|
|
99
|
+
let legacy = false;
|
|
100
|
+
for (const columns of columnChoices) {
|
|
101
|
+
try {
|
|
102
|
+
const candidate = parseTable(lines.map((line) => line.text).join('\n'), { sectionAliases: [heading], columns });
|
|
103
|
+
if (candidate) {
|
|
104
|
+
table = candidate;
|
|
105
|
+
legacy = columns === LEGACY_TARGET_COLUMNS;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// Try the next explicitly supported schema.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
93
112
|
if (!section || !table || lines.filter((line) => line.start >= section.bodyStart && line.start < section.end).length !== table.rows.length + 2) {
|
|
94
113
|
throw new Error(`invalidation ${heading} section must contain only its canonical table`);
|
|
95
114
|
}
|
|
96
|
-
return table.rows;
|
|
115
|
+
return { rows: table.rows, legacy };
|
|
97
116
|
}
|
|
98
117
|
|
|
99
118
|
function required(value: string, field: string): string {
|
|
@@ -117,9 +136,10 @@ function parseInvalidationDocument(content: string): InvalidationParseResult {
|
|
|
117
136
|
if (section === null) return { ok: true, present: false, document: { operations: [], targets: [] } };
|
|
118
137
|
try {
|
|
119
138
|
const body = content.slice(section.bodyStart, section.end);
|
|
120
|
-
const operationRows = parseInvalidationTable(body, 'Operations', OPERATION_COLUMNS);
|
|
121
|
-
|
|
122
|
-
const
|
|
139
|
+
const operationRows = parseInvalidationTable(body, 'Operations', [OPERATION_COLUMNS]);
|
|
140
|
+
// 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.
|
|
141
|
+
const targetRows = parseInvalidationTable(body, 'Targets', [TARGET_COLUMNS, LEGACY_TARGET_COLUMNS]);
|
|
142
|
+
const operations = operationRows.rows.map(({ values: row }) => ({
|
|
123
143
|
operationId: required(row.operation_id!, 'operation_id'), sourceFamily: required(row.source_family!, 'source_family'),
|
|
124
144
|
sourceArtifact: required(row.source_artifact!, 'source_artifact'), sourceRound: numberValue(row.source_round!, 'source_round', false),
|
|
125
145
|
sourceSha256: required(row.source_sha256!, 'source_sha256'), status: status(row.status!, 'operation status'),
|
|
@@ -127,10 +147,11 @@ function parseInvalidationDocument(content: string): InvalidationParseResult {
|
|
|
127
147
|
createdAt: required(row.created_at!, 'created_at'), updatedAt: required(row.updated_at!, 'updated_at'),
|
|
128
148
|
completedAt: row.completed_at ?? '', error: row.error ?? ''
|
|
129
149
|
} satisfies InvalidationOperation));
|
|
130
|
-
const targets = targetRows.map(({ values: row }) => ({
|
|
150
|
+
const targets = targetRows.rows.map(({ values: row }) => ({
|
|
131
151
|
targetId: required(row.target_id!, 'target_id'), operationId: required(row.operation_id!, 'operation_id'),
|
|
132
152
|
targetKind: row.target_kind as InvalidationTargetKind, targetFamily: required(row.target_family!, 'target_family'),
|
|
133
153
|
targetArtifact: required(row.target_artifact!, 'target_artifact'), targetRound: numberValue(row.target_round!, 'target_round', false),
|
|
154
|
+
...(row.target_input ? { targetInput: row.target_input } : {}),
|
|
134
155
|
targetSha256: required(row.target_sha256!, 'target_sha256'), status: status(row.status!, 'target status'),
|
|
135
156
|
reasonCode: required(row.reason_code!, 'reason_code'), updatedAt: required(row.updated_at!, 'updated_at')
|
|
136
157
|
} satisfies InvalidationTarget));
|
|
@@ -148,6 +169,10 @@ function parseInvalidationDocument(content: string): InvalidationParseResult {
|
|
|
148
169
|
if (targetIds.has(target.targetId)) throw new Error(`duplicate target '${target.targetId}'`);
|
|
149
170
|
targetIds.add(target.targetId);
|
|
150
171
|
if (!operationIds.has(target.operationId)) throw new Error(`target '${target.targetId}' references an unknown operation`);
|
|
172
|
+
if (target.targetKind === 'receipt') {
|
|
173
|
+
if (!target.targetInput && target.status !== 'completed') throw new Error(`receipt target '${target.targetId}' input identity is required`);
|
|
174
|
+
if (target.targetInput && !parseArtifactName(target.targetInput)) throw new Error(`receipt target '${target.targetId}' input identity is invalid`);
|
|
175
|
+
} else if (target.targetInput) throw new Error(`non-receipt target '${target.targetId}' cannot have an input identity`);
|
|
151
176
|
if (target.targetId !== targetIdFor(target.operationId, target)) throw new Error(`target '${target.targetId}' has a non-canonical identity`);
|
|
152
177
|
if (!/^[a-f0-9]{64}$/.test(target.targetSha256)) throw new Error(`target '${target.targetId}' hash is invalid`);
|
|
153
178
|
}
|
|
@@ -187,7 +212,7 @@ function renderInvalidation(document: InvalidationDocument): string {
|
|
|
187
212
|
'### Targets', '',
|
|
188
213
|
table(TARGET_COLUMNS, targets.map((row) => [
|
|
189
214
|
row.targetId, row.operationId, row.targetKind, row.targetFamily, row.targetArtifact,
|
|
190
|
-
row.targetRound, row.targetSha256, row.status, row.reasonCode, row.updatedAt
|
|
215
|
+
row.targetInput ?? '', row.targetRound, row.targetSha256, row.status, row.reasonCode, row.updatedAt
|
|
191
216
|
]))
|
|
192
217
|
].join('\n');
|
|
193
218
|
}
|
|
@@ -7,6 +7,7 @@ import { parseLifecyclePathDecision } from './lifecycle-path.ts';
|
|
|
7
7
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
8
8
|
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
|
|
9
9
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
10
|
+
import { validateQualificationAudit } from './qualification-audit.ts';
|
|
10
11
|
|
|
11
12
|
type LocalArtifactFamily = 'analysis' | 'plan' | 'code';
|
|
12
13
|
|
|
@@ -155,20 +156,29 @@ function prepareLocalArtifact(
|
|
|
155
156
|
const validation = validateLocalArtifact(content, {
|
|
156
157
|
family: request.family, requiredSections: request.requiredSections
|
|
157
158
|
});
|
|
159
|
+
let qualificationError: string | null = null;
|
|
160
|
+
try {
|
|
161
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
162
|
+
const qualification = validateQualificationAudit(taskContent, content, { family: request.family, artifact: request.artifact });
|
|
163
|
+
if (!qualification.ok) qualificationError = `${qualification.code}: ${qualification.message}`;
|
|
164
|
+
} catch (error) { qualificationError = `QUALIFICATION_AUDIT_INVALID: ${String(error)}`; }
|
|
158
165
|
const artifactSha256 = sha256Content(content);
|
|
166
|
+
const diagnostics = qualificationError
|
|
167
|
+
? [...validation.diagnostics, { code: 'LOCAL_STRUCTURAL_INVALID' as const, message: qualificationError, line: null }]
|
|
168
|
+
: validation.diagnostics;
|
|
159
169
|
const result = failedFinalization(request, {
|
|
160
170
|
code: 'LOCAL_ARTIFACT_INVALID',
|
|
161
|
-
message:
|
|
171
|
+
message: diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
|
|
162
172
|
}, {
|
|
163
173
|
taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256,
|
|
164
|
-
semanticDigest: validation.semanticDigest, diagnostics
|
|
174
|
+
semanticDigest: validation.semanticDigest, diagnostics
|
|
165
175
|
});
|
|
166
176
|
|
|
167
177
|
// Trusted local operators edit the canonical artifact directly. A
|
|
168
178
|
// finalization result is a fresh observation of those bytes, never a
|
|
169
179
|
// capability minted by an earlier recovery attempt or generation.
|
|
170
180
|
return {
|
|
171
|
-
result: validation.ok ? { ...result, status: 'passed', error: null } : result,
|
|
181
|
+
result: validation.ok && !qualificationError ? { ...result, status: 'passed', error: null } : result,
|
|
172
182
|
content,
|
|
173
183
|
repoRoot: resolved.repoRoot,
|
|
174
184
|
lockAlreadyHeld: request.lockAlreadyHeld
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
import { parseArtifactName } from './artifact-name.ts';
|
|
4
|
-
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
5
4
|
import { findSectionRange, parseTable } from './sections.ts';
|
|
6
5
|
import { scanVisibleMarkdown } from './markdown.ts';
|
|
7
6
|
|
|
@@ -9,13 +8,12 @@ const TASK_CONSTRAINT_HEADINGS = ['约束', 'Constraints'] as const;
|
|
|
9
8
|
const TASK_CANDIDATE_HEADINGS = ['候选与否决方案', 'Candidate and Rejected Options', 'Candidates and Rejected Options', 'Candidates and Rejected Alternatives'] as const;
|
|
10
9
|
const AUDIT_HEADINGS = ['资格审计', '资格审计复核', 'Qualification Audit', 'Qualification Audit Review'] as const;
|
|
11
10
|
const CONFIRMATION_HEADINGS = ['资格确认记录', 'Qualification Confirmations'] as const;
|
|
12
|
-
const AUDIT_SUBSECTIONS = ['约束依赖', '候选资格', '分类结果', '
|
|
11
|
+
const AUDIT_SUBSECTIONS = ['约束依赖', '候选资格', '分类结果', '资格快照'] as const;
|
|
13
12
|
const AUDIT_SUBSECTION_HEADINGS: Record<(typeof AUDIT_SUBSECTIONS)[number], readonly string[]> = {
|
|
14
13
|
'约束依赖': ['约束依赖', 'Constraint Dependencies'],
|
|
15
14
|
'候选资格': ['候选资格', 'Candidate Qualification'],
|
|
16
15
|
'分类结果': ['分类结果', 'Classification Results'],
|
|
17
|
-
'
|
|
18
|
-
'依赖快照': ['依赖快照', 'Dependency Snapshot']
|
|
16
|
+
'资格快照': ['资格快照', 'Qualification Snapshot']
|
|
19
17
|
};
|
|
20
18
|
|
|
21
19
|
const CONSTRAINT_COLUMNS = ['constraint_id', 'statement', 'status', 'authority', 'source', 'evidence', 'derived_from', 'approval_evidence'] as const;
|
|
@@ -23,12 +21,10 @@ const CANDIDATE_COLUMNS = ['candidate_id', 'statement', 'status', 'constraint_id
|
|
|
23
21
|
const DEPENDENCY_COLUMNS = ['constraint_id', 'constraint_digest', 'role', 'evidence'] as const;
|
|
24
22
|
const QUALIFICATION_COLUMNS = ['candidate_id', 'status', 'impact', 'constraint_ids', 'evidence'] as const;
|
|
25
23
|
const CLASSIFICATION_COLUMNS = ['decision_id', 'classification', 'evidence'] as const;
|
|
26
|
-
const
|
|
27
|
-
const SNAPSHOT_COLUMNS = ['task_input_digest', 'non_constraint_input_digest', 'upstream_artifact_digest'] as const;
|
|
24
|
+
const SNAPSHOT_COLUMNS = ['task_input_digest', 'non_constraint_input_digest'] as const;
|
|
28
25
|
const CONFIRMATION_COLUMNS = ['qcr_id', 'constraint_id', 'actor', 'entrypoint', 'request_id', 'approved_digest', 'confirmed_at', 'rationale'] as const;
|
|
29
26
|
|
|
30
27
|
const ARTIFACT_FAMILIES = ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'] as const;
|
|
31
|
-
const RELATIONS = ['required-input', 'reviewed-input', 'review-context', 'approval-context'] as const;
|
|
32
28
|
const CONSTRAINT_STATUSES = ['confirmed', 'derived', 'assumption', 'open', 'conflicted', 'superseded'] as const;
|
|
33
29
|
const CANDIDATE_STATUSES = ['qualified', 'rejected', 'pending', 'needs-human-decision', 'confirmed', 'excluded'] as const;
|
|
34
30
|
const CLASSIFICATIONS = ['deterministic', 'qualified', 'rejected', 'needs-human-decision', 'not-applicable'] as const;
|
|
@@ -38,7 +34,6 @@ type ConstraintStatus = (typeof CONSTRAINT_STATUSES)[number];
|
|
|
38
34
|
type CandidateStatus = (typeof CANDIDATE_STATUSES)[number];
|
|
39
35
|
type QualificationClassification = (typeof CLASSIFICATIONS)[number];
|
|
40
36
|
type ArtifactFamily = (typeof ARTIFACT_FAMILIES)[number];
|
|
41
|
-
type QualificationRelation = (typeof RELATIONS)[number];
|
|
42
37
|
type QualificationRole = (typeof ROLES)[number];
|
|
43
38
|
|
|
44
39
|
type Constraint = {
|
|
@@ -86,24 +81,15 @@ type QualificationClassificationRow = {
|
|
|
86
81
|
classification: QualificationClassification;
|
|
87
82
|
evidence: string;
|
|
88
83
|
};
|
|
89
|
-
type UpstreamRelation = {
|
|
90
|
-
upstreamFamily: ArtifactFamily;
|
|
91
|
-
upstreamArtifact: string;
|
|
92
|
-
upstreamRound: number;
|
|
93
|
-
upstreamSha256: string;
|
|
94
|
-
relation: QualificationRelation;
|
|
95
|
-
};
|
|
96
84
|
type DependencySnapshot = {
|
|
97
85
|
taskInputDigest: string;
|
|
98
86
|
nonConstraintInputDigest: string;
|
|
99
|
-
upstreamArtifactDigest: string;
|
|
100
87
|
};
|
|
101
88
|
type QualificationAudit = {
|
|
102
89
|
present: boolean;
|
|
103
90
|
constraintDependencies: readonly ConstraintDependency[];
|
|
104
91
|
candidateQualifications: readonly CandidateQualification[];
|
|
105
92
|
classifications: readonly QualificationClassificationRow[];
|
|
106
|
-
upstreamRelations: readonly UpstreamRelation[];
|
|
107
93
|
snapshot: DependencySnapshot | null;
|
|
108
94
|
};
|
|
109
95
|
type QualificationConfirmation = {
|
|
@@ -304,80 +290,19 @@ function parseAuditTable(content: string, heading: (typeof AUDIT_SUBSECTIONS)[nu
|
|
|
304
290
|
return table?.rows.map((row) => ({ ...row.values })) ?? null;
|
|
305
291
|
}
|
|
306
292
|
|
|
307
|
-
function parseAuditArtifactName(value: string): { family: ArtifactFamily; round: number } | null {
|
|
308
|
-
const identity = parseArtifactName(value);
|
|
309
|
-
return identity && ARTIFACT_FAMILIES.includes(identity.family as ArtifactFamily)
|
|
310
|
-
? { family: identity.family as ArtifactFamily, round: identity.round } : null;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function upstreamArtifactDigest(rows: readonly UpstreamRelation[]): string {
|
|
314
|
-
return digest([...rows].map((row) => ({ ...row })).sort((a, b) =>
|
|
315
|
-
`${a.upstreamFamily}/${a.upstreamArtifact}/${a.relation}`.localeCompare(`${b.upstreamFamily}/${b.upstreamArtifact}/${b.relation}`)));
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function validateUpstreamRelation(row: UpstreamRelation): void {
|
|
319
|
-
const identity = parseAuditArtifactName(row.upstreamArtifact);
|
|
320
|
-
if (!ARTIFACT_FAMILIES.includes(row.upstreamFamily) || !identity || identity.family !== row.upstreamFamily
|
|
321
|
-
|| identity.round !== row.upstreamRound || !Number.isSafeInteger(row.upstreamRound)
|
|
322
|
-
|| !/^[a-f0-9]{64}$/i.test(row.upstreamSha256) || !RELATIONS.includes(row.relation)) {
|
|
323
|
-
throw new Error(`invalid qualification upstream relation '${row.upstreamArtifact}'`);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function expectedQualificationRelations(
|
|
328
|
-
taskContent: string,
|
|
329
|
-
family: ArtifactFamily
|
|
330
|
-
): { ok: true; relations: readonly UpstreamRelation[] | undefined } | { ok: false; code: string; message: string } {
|
|
331
|
-
let frontmatter;
|
|
332
|
-
try { frontmatter = parseTypedTaskFrontmatter(taskContent); }
|
|
333
|
-
catch (error) { return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: error instanceof Error ? error.message : String(error) }; }
|
|
334
|
-
const encoded = frontmatter.qualification_input_relations;
|
|
335
|
-
if (typeof encoded === 'string' && encoded) {
|
|
336
|
-
try {
|
|
337
|
-
const parsed = JSON.parse(encoded) as unknown;
|
|
338
|
-
if (!Array.isArray(parsed)) throw new Error('qualification_input_relations must be an array');
|
|
339
|
-
const relations = parsed.map((value) => {
|
|
340
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('qualification input relation must be an object');
|
|
341
|
-
const row = value as Record<string, unknown>;
|
|
342
|
-
const relation: UpstreamRelation = {
|
|
343
|
-
upstreamFamily: row.upstreamFamily as ArtifactFamily,
|
|
344
|
-
upstreamArtifact: String(row.upstreamArtifact ?? ''),
|
|
345
|
-
upstreamRound: Number(row.upstreamRound),
|
|
346
|
-
upstreamSha256: String(row.upstreamSha256 ?? ''),
|
|
347
|
-
relation: row.relation as QualificationRelation
|
|
348
|
-
};
|
|
349
|
-
validateUpstreamRelation(relation);
|
|
350
|
-
return relation;
|
|
351
|
-
});
|
|
352
|
-
return { ok: true, relations };
|
|
353
|
-
} catch (error) {
|
|
354
|
-
return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: error instanceof Error ? error.message : String(error) };
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
const inputName = family === 'code' ? frontmatter.code_input_artifact : family.startsWith('review-') ? frontmatter.review_input_artifact : '';
|
|
358
|
-
const inputSha256 = family === 'code' ? frontmatter.code_input_sha256 : family.startsWith('review-') ? frontmatter.review_input_sha256 : '';
|
|
359
|
-
if (typeof inputName !== 'string' || !inputName || typeof inputSha256 !== 'string' || !inputSha256) return { ok: true, relations: undefined };
|
|
360
|
-
const identity = parseAuditArtifactName(inputName);
|
|
361
|
-
if (!identity) return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: `started input '${inputName}' is not canonical` };
|
|
362
|
-
const relation: UpstreamRelation = {
|
|
363
|
-
upstreamFamily: identity.family, upstreamArtifact: inputName, upstreamRound: identity.round,
|
|
364
|
-
upstreamSha256: inputSha256, relation: family === 'code' ? 'required-input' : 'reviewed-input'
|
|
365
|
-
};
|
|
366
|
-
try { validateUpstreamRelation(relation); }
|
|
367
|
-
catch (error) { return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: error instanceof Error ? error.message : String(error) }; }
|
|
368
|
-
return { ok: true, relations: [relation] };
|
|
369
|
-
}
|
|
370
|
-
|
|
371
293
|
function parseQualificationAudit(content: string): { ok: true; audit: QualificationAudit } | { ok: false; code: string; message: string } {
|
|
372
|
-
|
|
294
|
+
const auditBody = sectionBody(content, AUDIT_HEADINGS);
|
|
295
|
+
if (auditBody === null) return { ok: true, audit: { present: false, constraintDependencies: [], candidateQualifications: [], classifications: [], snapshot: null } };
|
|
373
296
|
try {
|
|
297
|
+
if (sectionBody(auditBody, ['上游关系', 'Upstream Relations'], 3) !== null) {
|
|
298
|
+
throw new Error('qualification audit uses the obsolete lifecycle-relation format; regenerate it using the current format');
|
|
299
|
+
}
|
|
374
300
|
const dependencies = parseAuditTable(content, '约束依赖', DEPENDENCY_COLUMNS);
|
|
375
301
|
const qualifications = parseAuditTable(content, '候选资格', QUALIFICATION_COLUMNS);
|
|
376
302
|
const classifications = parseAuditTable(content, '分类结果', CLASSIFICATION_COLUMNS);
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
throw new Error('qualification audit requires all five canonical tables and exactly one dependency snapshot');
|
|
303
|
+
const snapshots = parseAuditTable(content, '资格快照', SNAPSHOT_COLUMNS);
|
|
304
|
+
if (!dependencies || !qualifications || !classifications || !snapshots || snapshots.length !== 1) {
|
|
305
|
+
throw new Error('qualification audit requires three decision tables and exactly one qualification snapshot');
|
|
381
306
|
}
|
|
382
307
|
const constraintDependencies = dependencies.map((row) => {
|
|
383
308
|
if (!/^C-[1-9]\d*$/.test(row.constraint_id ?? '') || !/^[a-f0-9]{64}$/i.test(row.constraint_digest ?? '') || !ROLES.includes(row.role as QualificationRole) || !row.evidence?.trim()) throw new Error(`invalid qualification constraint dependency '${row.constraint_id ?? ''}'`);
|
|
@@ -392,19 +317,14 @@ function parseQualificationAudit(content: string): { ok: true; audit: Qualificat
|
|
|
392
317
|
if (!row.decision_id?.trim() || !CLASSIFICATIONS.includes(row.classification as QualificationClassification) || !row.evidence?.trim()) throw new Error(`invalid qualification classification '${row.decision_id ?? ''}'`);
|
|
393
318
|
return { decisionId: row.decision_id, classification: row.classification as QualificationClassification, evidence: row.evidence };
|
|
394
319
|
});
|
|
395
|
-
const upstreamRelations = relations.map((row) => {
|
|
396
|
-
const relation = { upstreamFamily: row.upstream_family as ArtifactFamily, upstreamArtifact: row.upstream_artifact ?? '', upstreamRound: Number(row.upstream_round), upstreamSha256: row.upstream_sha256 ?? '', relation: row.relation as QualificationRelation };
|
|
397
|
-
validateUpstreamRelation(relation);
|
|
398
|
-
return relation;
|
|
399
|
-
});
|
|
400
320
|
const snapshotRow = snapshots[0]!;
|
|
401
|
-
if (!/^[a-f0-9]{64}$/i.test(snapshotRow.task_input_digest ?? '') || !/^[a-f0-9]{64}$/i.test(snapshotRow.non_constraint_input_digest ?? '')
|
|
321
|
+
if (!/^[a-f0-9]{64}$/i.test(snapshotRow.task_input_digest ?? '') || !/^[a-f0-9]{64}$/i.test(snapshotRow.non_constraint_input_digest ?? '')) throw new Error('qualification snapshot has invalid digest');
|
|
402
322
|
return {
|
|
403
323
|
ok: true,
|
|
404
324
|
audit: {
|
|
405
325
|
present: true, constraintDependencies, candidateQualifications,
|
|
406
|
-
classifications: classificationRows,
|
|
407
|
-
snapshot: { taskInputDigest: snapshotRow.task_input_digest!, nonConstraintInputDigest: snapshotRow.non_constraint_input_digest
|
|
326
|
+
classifications: classificationRows,
|
|
327
|
+
snapshot: { taskInputDigest: snapshotRow.task_input_digest!, nonConstraintInputDigest: snapshotRow.non_constraint_input_digest! }
|
|
408
328
|
}
|
|
409
329
|
};
|
|
410
330
|
} catch (error) {
|
|
@@ -443,14 +363,13 @@ function validateQualificationAudit(
|
|
|
443
363
|
family?: ArtifactFamily;
|
|
444
364
|
artifact?: string;
|
|
445
365
|
require?: boolean;
|
|
446
|
-
expectedUpstreamRelations?: readonly UpstreamRelation[];
|
|
447
366
|
} = {}
|
|
448
367
|
): QualificationValidationResult {
|
|
449
368
|
const task = parseTaskQualification(taskContent);
|
|
450
369
|
if (!task.ok) return task;
|
|
451
370
|
const audit = parseQualificationAudit(artifactContent);
|
|
452
371
|
if (!audit.ok) return audit;
|
|
453
|
-
if (!
|
|
372
|
+
if (!audit.audit.present && !options.require) return { ok: true, qualification: task.qualification, audit: audit.audit };
|
|
454
373
|
if (!task.qualification.present) return { ok: false, code: 'QUALIFICATION_TASK_CONTRACT_MISSING', message: 'task qualification contract is missing' };
|
|
455
374
|
if (!audit.audit.present) return { ok: false, code: 'QUALIFICATION_AUDIT_MISSING', message: 'artifact qualification audit is missing' };
|
|
456
375
|
const constraintMap = new Map(task.qualification.constraints.map((row) => [row.constraintId, row]));
|
|
@@ -490,13 +409,8 @@ function validateQualificationAudit(
|
|
|
490
409
|
const snapshot = audit.audit.snapshot!;
|
|
491
410
|
if (snapshot.taskInputDigest !== task.qualification.taskInputDigest) return { ok: false, code: 'QUALIFICATION_TASK_DIGEST_MISMATCH', message: 'qualification audit task input digest is stale' };
|
|
492
411
|
if (snapshot.nonConstraintInputDigest !== task.qualification.nonConstraintInputDigest) return { ok: false, code: 'QUALIFICATION_NON_CONSTRAINT_DIGEST_MISMATCH', message: 'qualification audit non-constraint input digest is stale' };
|
|
493
|
-
if (snapshot.upstreamArtifactDigest !== upstreamArtifactDigest(audit.audit.upstreamRelations)) return { ok: false, code: 'QUALIFICATION_UPSTREAM_DIGEST_MISMATCH', message: 'qualification audit upstream digest does not match its relation rows' };
|
|
494
|
-
if (options.expectedUpstreamRelations && (
|
|
495
|
-
options.expectedUpstreamRelations.length !== audit.audit.upstreamRelations.length
|
|
496
|
-
|| upstreamArtifactDigest(options.expectedUpstreamRelations) !== upstreamArtifactDigest(audit.audit.upstreamRelations)
|
|
497
|
-
)) return { ok: false, code: 'QUALIFICATION_UPSTREAM_RELATION_MISMATCH', message: 'qualification audit upstream relations do not match the started artifact inputs' };
|
|
498
412
|
if (options.family && options.artifact) {
|
|
499
|
-
const identity =
|
|
413
|
+
const identity = parseArtifactName(options.artifact);
|
|
500
414
|
if (!identity || identity.family !== options.family) return { ok: false, code: 'QUALIFICATION_ARTIFACT_IDENTITY_INVALID', message: `artifact '${options.artifact}' is not canonical for '${options.family}'` };
|
|
501
415
|
}
|
|
502
416
|
return { ok: true, qualification: task.qualification, audit: audit.audit };
|
|
@@ -509,19 +423,18 @@ function renderQualificationAudit(audit: QualificationAudit): string {
|
|
|
509
423
|
...rows.map((row) => `| ${row.map((value) => value.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/[\r\n]/g, ' ')).join(' | ')} |`)
|
|
510
424
|
].join('\n');
|
|
511
425
|
const list = (values: readonly string[]) => values.join(',');
|
|
512
|
-
const snapshot = audit.snapshot ?? { taskInputDigest: '', nonConstraintInputDigest: ''
|
|
426
|
+
const snapshot = audit.snapshot ?? { taskInputDigest: '', nonConstraintInputDigest: '' };
|
|
513
427
|
return [
|
|
514
428
|
'### 约束依赖', '', table(DEPENDENCY_COLUMNS, audit.constraintDependencies.map((row) => [row.constraintId, row.constraintDigest, row.role, row.evidence])), '',
|
|
515
429
|
'### 候选资格', '', table(QUALIFICATION_COLUMNS, audit.candidateQualifications.map((row) => [row.candidateId, row.status, row.impact, list(row.constraintIds), row.evidence])), '',
|
|
516
430
|
'### 分类结果', '', table(CLASSIFICATION_COLUMNS, audit.classifications.map((row) => [row.decisionId, row.classification, row.evidence])), '',
|
|
517
|
-
'###
|
|
518
|
-
'### 依赖快照', '', table(SNAPSHOT_COLUMNS, [[snapshot.taskInputDigest, snapshot.nonConstraintInputDigest, snapshot.upstreamArtifactDigest]])
|
|
431
|
+
'### 资格快照', '', table(SNAPSHOT_COLUMNS, [[snapshot.taskInputDigest, snapshot.nonConstraintInputDigest]])
|
|
519
432
|
].join('\n');
|
|
520
433
|
}
|
|
521
434
|
|
|
522
435
|
function buildQualificationAudit(
|
|
523
436
|
taskContent: string,
|
|
524
|
-
input: { constraints?: readonly string[]; candidates?: readonly string[]; classifications?: readonly QualificationClassificationRow[]
|
|
437
|
+
input: { constraints?: readonly string[]; candidates?: readonly string[]; classifications?: readonly QualificationClassificationRow[] } = {}
|
|
525
438
|
): { ok: true; audit: QualificationAudit } | { ok: false; code: string; message: string } {
|
|
526
439
|
const parsed = parseTaskQualification(taskContent);
|
|
527
440
|
if (!parsed.ok) return parsed;
|
|
@@ -530,13 +443,12 @@ function buildQualificationAudit(
|
|
|
530
443
|
const candidates = input.candidates ?? parsed.qualification.candidates.map((row) => row.candidateId);
|
|
531
444
|
const constraintDependencies = parsed.qualification.constraints.filter((row) => ids.has(row.constraintId)).map((row) => ({ constraintId: row.constraintId, constraintDigest: row.digest, role: 'required' as const, evidence: row.evidence }));
|
|
532
445
|
const candidateQualifications = parsed.qualification.candidates.filter((row) => candidates.includes(row.candidateId)).map((row) => ({ candidateId: row.candidateId, status: row.status, impact: row.impact, constraintIds: row.constraintIds, evidence: row.evidence }));
|
|
533
|
-
const upstreamRelations = [...(input.upstreamRelations ?? [])];
|
|
534
446
|
return {
|
|
535
447
|
ok: true,
|
|
536
448
|
audit: {
|
|
537
449
|
present: true, constraintDependencies, candidateQualifications,
|
|
538
|
-
classifications: input.classifications ?? [],
|
|
539
|
-
snapshot: { taskInputDigest: parsed.qualification.taskInputDigest, nonConstraintInputDigest: parsed.qualification.nonConstraintInputDigest
|
|
450
|
+
classifications: input.classifications ?? [],
|
|
451
|
+
snapshot: { taskInputDigest: parsed.qualification.taskInputDigest, nonConstraintInputDigest: parsed.qualification.nonConstraintInputDigest }
|
|
540
452
|
}
|
|
541
453
|
};
|
|
542
454
|
}
|
|
@@ -552,10 +464,8 @@ export {
|
|
|
552
464
|
DEPENDENCY_COLUMNS,
|
|
553
465
|
QUALIFICATION_COLUMNS,
|
|
554
466
|
CLASSIFICATION_COLUMNS,
|
|
555
|
-
RELATION_COLUMNS,
|
|
556
467
|
SNAPSHOT_COLUMNS,
|
|
557
468
|
constraintDigest,
|
|
558
|
-
expectedQualificationRelations,
|
|
559
469
|
buildQualificationAudit,
|
|
560
470
|
nonConstraintInputDigest,
|
|
561
471
|
parseQualificationAudit,
|
|
@@ -563,7 +473,6 @@ export {
|
|
|
563
473
|
parseTaskQualification,
|
|
564
474
|
renderQualificationAudit,
|
|
565
475
|
taskInputDigest,
|
|
566
|
-
upstreamArtifactDigest,
|
|
567
476
|
validateQualificationAudit
|
|
568
477
|
};
|
|
569
478
|
export type {
|
|
@@ -578,11 +487,9 @@ export type {
|
|
|
578
487
|
QualificationConfirmation,
|
|
579
488
|
QualificationClassification,
|
|
580
489
|
QualificationClassificationRow,
|
|
581
|
-
QualificationRelation,
|
|
582
490
|
QualificationRole,
|
|
583
491
|
QualificationValidationError,
|
|
584
492
|
QualificationValidationResult,
|
|
585
493
|
TaskQualification,
|
|
586
|
-
UpstreamRelation,
|
|
587
494
|
ArtifactFamily
|
|
588
495
|
};
|
|
@@ -13,6 +13,7 @@ import { validateLifecycleExecution } from './lifecycle-execution.ts';
|
|
|
13
13
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
14
14
|
import type { ResolveTaskRefErrorCode } from './resolve-ref.ts';
|
|
15
15
|
import { getArtifactSchema } from './artifact-schema.ts';
|
|
16
|
+
import { validateQualificationAudit } from './qualification-audit.ts';
|
|
16
17
|
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.ts';
|
|
17
18
|
|
|
18
19
|
type ReviewFinalizationErrorCode =
|
|
@@ -258,6 +259,12 @@ function prepareReviewSummaryCandidate(
|
|
|
258
259
|
artifactSha256, semanticDigest: structure.semanticDigest
|
|
259
260
|
});
|
|
260
261
|
}
|
|
262
|
+
const qualification = validateQualificationAudit(taskContent, artifactContent, { family: spec.family, artifact: request.artifact });
|
|
263
|
+
if (!qualification.ok) {
|
|
264
|
+
return reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', `${qualification.code}: ${qualification.message}`, {
|
|
265
|
+
artifactSha256, semanticDigest
|
|
266
|
+
});
|
|
267
|
+
}
|
|
261
268
|
const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts, {
|
|
262
269
|
refreshNumericCounts: true
|
|
263
270
|
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { resolveBranchWorktree } from '../git/branch-worktree.ts';
|
|
6
|
+
import { snapshotReview } from '../git/review-snapshot.ts';
|
|
7
|
+
import { resolveDeliveryTarget } from './delivery-target.ts';
|
|
8
|
+
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
9
|
+
import {
|
|
10
|
+
extractReviewDiffBase,
|
|
11
|
+
extractReviewDiffFingerprint,
|
|
12
|
+
extractReviewedHead,
|
|
13
|
+
extractReviewTargetHead,
|
|
14
|
+
extractReviewedSnapshotTree,
|
|
15
|
+
loadPostReviewConfig,
|
|
16
|
+
resolvePostReviewGlobs
|
|
17
|
+
} from './review-fingerprint.ts';
|
|
18
|
+
|
|
19
|
+
export type ReviewIdentityResult = Readonly<{
|
|
20
|
+
status: 'matched' | 'different' | 'indeterminate';
|
|
21
|
+
message: string;
|
|
22
|
+
reviewedHead: string | null;
|
|
23
|
+
currentHead: string | null;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
function git(cwd: string, args: string[]): string {
|
|
27
|
+
return execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8' }).trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function result(
|
|
31
|
+
status: ReviewIdentityResult['status'], message: string,
|
|
32
|
+
reviewedHead: string | null = null, currentHead: string | null = null
|
|
33
|
+
): ReviewIdentityResult {
|
|
34
|
+
return { status, message, reviewedHead, currentHead };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function inspectReviewIdentity(taskDir: string, reportContent: string, repositoryRoot?: string): ReviewIdentityResult {
|
|
38
|
+
const reviewedHead = extractReviewedHead(reportContent);
|
|
39
|
+
const targetHead = extractReviewTargetHead(reportContent);
|
|
40
|
+
const diffBase = extractReviewDiffBase(reportContent);
|
|
41
|
+
const fingerprint = extractReviewDiffFingerprint(reportContent);
|
|
42
|
+
const reviewedTree = extractReviewedSnapshotTree(reportContent);
|
|
43
|
+
if (!reviewedHead || !targetHead || !diffBase || !fingerprint || !reviewedTree) {
|
|
44
|
+
return result('indeterminate', 'Review report is missing commit or snapshot identity', reviewedHead || null);
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const metadata = parseTypedTaskFrontmatter(fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'));
|
|
48
|
+
const taskRepositoryRoot = git(taskDir, ['rev-parse', '--show-toplevel']);
|
|
49
|
+
const branch = String(metadata.branch ?? '').trim();
|
|
50
|
+
const gitRoot = branch ? resolveBranchWorktree(taskRepositoryRoot, branch) : taskRepositoryRoot;
|
|
51
|
+
if (!gitRoot) return result('indeterminate', `Task branch '${branch}' is not checked out in a registered worktree`, reviewedHead);
|
|
52
|
+
const currentHead = git(gitRoot, ['rev-parse', 'HEAD']);
|
|
53
|
+
const baseline = git(gitRoot, ['rev-parse', `${reviewedHead}^{commit}`]);
|
|
54
|
+
if (baseline !== currentHead) return result('different', 'Reviewed commit differs from current HEAD', baseline, currentHead);
|
|
55
|
+
|
|
56
|
+
const remote = String(metadata.delivery_remote ?? '').trim();
|
|
57
|
+
const baseRef = String(metadata.delivery_base_ref ?? '').trim();
|
|
58
|
+
if (!remote || !baseRef) return result('indeterminate', 'Task delivery target is not bound', baseline, currentHead);
|
|
59
|
+
const target = resolveDeliveryTarget(gitRoot, { remote, baseRef });
|
|
60
|
+
if (!target.ok) return result('indeterminate', target.message, baseline, currentHead);
|
|
61
|
+
const currentTarget = git(gitRoot, ['rev-parse', `${targetHead}^{commit}`]);
|
|
62
|
+
const computedDiffBase = git(gitRoot, ['merge-base', baseline, currentTarget]);
|
|
63
|
+
if (git(gitRoot, ['rev-parse', `${diffBase}^{commit}`]) !== computedDiffBase) {
|
|
64
|
+
return result('different', 'Saved review diff base no longer matches the current target', baseline, currentHead);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const globs = resolvePostReviewGlobs({}, loadPostReviewConfig(repositoryRoot ?? taskRepositoryRoot));
|
|
68
|
+
const trackedChanges = git(gitRoot, ['diff', '--name-only', '-z', baseline, '--', ...globs]);
|
|
69
|
+
const untrackedChanges = git(gitRoot, ['ls-files', '-o', '--exclude-standard', '-z', '--', ...globs]);
|
|
70
|
+
if (trackedChanges || untrackedChanges) return result('different', 'Reviewed worktree contains uncommitted changes', baseline, currentHead);
|
|
71
|
+
|
|
72
|
+
const snapshot = snapshotReview({
|
|
73
|
+
cwd: gitRoot,
|
|
74
|
+
mode: 'worktree',
|
|
75
|
+
baseline,
|
|
76
|
+
diffBase: computedDiffBase,
|
|
77
|
+
globs
|
|
78
|
+
});
|
|
79
|
+
if (snapshot.fingerprint !== fingerprint) return result('different', 'Reviewed diff fingerprint differs from the current worktree', baseline, currentHead);
|
|
80
|
+
if (snapshot.tree !== reviewedTree) return result('different', 'Reviewed snapshot tree differs from the current worktree', baseline, currentHead);
|
|
81
|
+
return result('matched', 'Review report matches the current commit and clean worktree', baseline, currentHead);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return result('indeterminate', error instanceof Error ? error.message : String(error), reviewedHead);
|
|
84
|
+
}
|
|
85
|
+
}
|
package/lib/task/sections.ts
CHANGED
|
@@ -74,7 +74,7 @@ class DocumentMutationError extends Error {
|
|
|
74
74
|
|
|
75
75
|
function parseTable(
|
|
76
76
|
content: string,
|
|
77
|
-
input: { sectionAliases: readonly string[]; columns: readonly string[]; keyColumn?: string }
|
|
77
|
+
input: { sectionAliases: readonly string[]; columns: readonly string[]; keyColumn?: string | null }
|
|
78
78
|
): ParsedTable | null {
|
|
79
79
|
validateAliases(input.sectionAliases, 'table section aliases');
|
|
80
80
|
if (
|
|
@@ -84,8 +84,8 @@ function parseTable(
|
|
|
84
84
|
) {
|
|
85
85
|
throw new DocumentMutationError('MUTATION_INVALID', 'table columns are invalid');
|
|
86
86
|
}
|
|
87
|
-
const keyColumn = input.keyColumn
|
|
88
|
-
if (!input.columns.includes(keyColumn)) {
|
|
87
|
+
const keyColumn = input.keyColumn === undefined ? input.columns[0]! : input.keyColumn;
|
|
88
|
+
if (keyColumn !== null && !input.columns.includes(keyColumn)) {
|
|
89
89
|
throw new DocumentMutationError('MUTATION_INVALID', 'table key column is invalid');
|
|
90
90
|
}
|
|
91
91
|
const sections = matchingSections(content, input.sectionAliases);
|
|
@@ -132,11 +132,11 @@ function parseTable(
|
|
|
132
132
|
column,
|
|
133
133
|
decodeCell(parsed.cells[columnIndex]!).trim()
|
|
134
134
|
]));
|
|
135
|
-
const key = values[keyColumn]!;
|
|
136
|
-
if (seen.has(key)) {
|
|
135
|
+
const key = keyColumn === null ? null : values[keyColumn]!;
|
|
136
|
+
if (key !== null && seen.has(key)) {
|
|
137
137
|
throw new DocumentMutationError('TABLE_DUPLICATE_KEY', `duplicate table key '${key}'`);
|
|
138
138
|
}
|
|
139
|
-
seen.add(key);
|
|
139
|
+
if (key !== null) seen.add(key);
|
|
140
140
|
rows.push({ values, sourceLine: index - Math.max(firstContent, 0) });
|
|
141
141
|
}
|
|
142
142
|
return { heading: section.heading, rows };
|
|
@@ -27,6 +27,7 @@ type LinkDisposition = 'exists' | 'missing' | TaskExecutionLockError;
|
|
|
27
27
|
|
|
28
28
|
type TaskExecutionLockOptions = Readonly<{
|
|
29
29
|
lockRoot?: string;
|
|
30
|
+
identityScope?: string;
|
|
30
31
|
token?: () => string;
|
|
31
32
|
now?: () => string;
|
|
32
33
|
getStartTime?: (pid: number) => number | null;
|
|
@@ -74,9 +75,12 @@ function mapLinkError(operation: LinkOperation, error: unknown, key: string): Li
|
|
|
74
75
|
);
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
function lockKey(repoRoot: string, taskId: string): Readonly<{ canonicalRepoRoot: string; key: string }> {
|
|
78
|
+
function lockKey(repoRoot: string, taskId: string, identityScope?: string): Readonly<{ canonicalRepoRoot: string; key: string }> {
|
|
78
79
|
const canonicalRepoRoot = fs.realpathSync.native(repoRoot);
|
|
79
|
-
const
|
|
80
|
+
const identity = identityScope === undefined
|
|
81
|
+
? `${canonicalRepoRoot}\0${taskId}`
|
|
82
|
+
: `${identityScope}\0${taskId}`;
|
|
83
|
+
const key = createHash('sha256').update(identity).digest('hex');
|
|
80
84
|
return { canonicalRepoRoot, key };
|
|
81
85
|
}
|
|
82
86
|
|
|
@@ -190,7 +194,7 @@ function withTaskExecutionLock<T>(
|
|
|
190
194
|
): T {
|
|
191
195
|
let identity: Readonly<{ canonicalRepoRoot: string; key: string }>;
|
|
192
196
|
try {
|
|
193
|
-
identity = lockKey(repoRoot, taskId);
|
|
197
|
+
identity = lockKey(repoRoot, taskId, options.identityScope);
|
|
194
198
|
} catch (error) {
|
|
195
199
|
throw new TaskExecutionLockError(
|
|
196
200
|
'ORCHESTRATION_LOCK_FAILED',
|