@fitlab-ai/agent-infra 0.11.5 → 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/merge.js +62 -10
- package/dist/lib/platform/github-provider.js +9 -14
- package/dist/lib/platform/github-release-notes.js +80 -11
- package/dist/lib/platform/none-provider.js +1 -1
- package/dist/lib/platform/provider-contract.d.ts +39 -16
- package/dist/lib/platform/provider-contract.js +1 -1
- package/dist/lib/platform/provider-validation.js +83 -13
- package/dist/lib/platform/pull-requests.js +29 -3
- package/dist/lib/platform/release-notes.js +7 -8
- package/dist/lib/sandbox/capture.js +1 -1
- package/dist/lib/sandbox/commands/create.js +46 -1
- package/dist/lib/sandbox/commands/enter.js +1 -1
- package/dist/lib/sandbox/commands/start.js +1 -1
- package/dist/lib/sandbox/config.js +10 -0
- package/dist/lib/sandbox/control/lifecycle.js +18 -13
- package/dist/lib/sandbox/control/server.js +6 -2
- package/dist/lib/sandbox/task-resolver.js +62 -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/resolve-ref.js +3 -3
- 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/lib/task/write.js +121 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/git/primary-worktree.ts +62 -0
- package/lib/merge.ts +59 -10
- package/lib/platform/github-provider.ts +11 -18
- package/lib/platform/github-release-notes.ts +89 -14
- package/lib/platform/none-provider.ts +1 -1
- package/lib/platform/provider-contract.ts +38 -12
- package/lib/platform/provider-validation.ts +83 -12
- package/lib/platform/pull-requests.ts +27 -3
- package/lib/platform/release-notes.ts +11 -12
- package/lib/sandbox/capture.ts +1 -1
- package/lib/sandbox/commands/create.ts +52 -1
- package/lib/sandbox/commands/enter.ts +1 -1
- package/lib/sandbox/commands/start.ts +1 -1
- package/lib/sandbox/config.ts +12 -0
- package/lib/sandbox/control/lifecycle.ts +22 -13
- package/lib/sandbox/control/server.ts +5 -2
- package/lib/sandbox/task-resolver.ts +51 -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/resolve-ref.ts +3 -3
- 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/lib/task/write.ts +115 -1
- 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/evidence-reporting.en.md +1 -2
- package/templates/.agents/rules/evidence-reporting.zh-CN.md +1 -2
- 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/archive-tasks/SKILL.en.md +2 -2
- package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +109 -6
- 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-release-note/SKILL.en.md +17 -11
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +17 -11
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { parseArtifactName } from './artifact-name.js';
|
|
2
3
|
import { findSectionRange, parseTable } from './sections.js';
|
|
3
4
|
import { scanVisibleMarkdown } from './markdown.js';
|
|
4
5
|
const INVALIDATION_HEADINGS = ['产物失效记录', 'Artifact Invalidation'];
|
|
@@ -7,6 +8,10 @@ const OPERATION_COLUMNS = [
|
|
|
7
8
|
'status', 'processed', 'total', 'created_at', 'updated_at', 'completed_at', 'error'
|
|
8
9
|
];
|
|
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 = [
|
|
10
15
|
'target_id', 'operation_id', 'target_kind', 'target_family', 'target_artifact',
|
|
11
16
|
'target_round', 'target_sha256', 'status', 'reason_code', 'updated_at'
|
|
12
17
|
];
|
|
@@ -20,7 +25,7 @@ function operationIdFor(source) {
|
|
|
20
25
|
return `INV-${hashIdentity([source.sourceFamily, source.sourceArtifact, source.sourceRound, source.sourceSha256])}`;
|
|
21
26
|
}
|
|
22
27
|
function targetIdFor(operationId, target) {
|
|
23
|
-
return `INV-T-${hashIdentity([operationId, target.targetKind, target.targetFamily, target.targetArtifact, target.targetRound, target.targetSha256])}`;
|
|
28
|
+
return `INV-T-${hashIdentity([operationId, target.targetKind, target.targetFamily, target.targetArtifact, ...(target.targetKind === 'receipt' && target.targetInput ? [target.targetInput] : []), target.targetRound, target.targetSha256])}`;
|
|
24
29
|
}
|
|
25
30
|
function createInvalidationOperation(source, targets = []) {
|
|
26
31
|
const operationId = source.operationId ?? operationIdFor(source);
|
|
@@ -31,14 +36,28 @@ function createInvalidationOperation(source, targets = []) {
|
|
|
31
36
|
createdAt: source.createdAt, updatedAt: source.updatedAt, completedAt: '', error: ''
|
|
32
37
|
};
|
|
33
38
|
}
|
|
34
|
-
function parseInvalidationTable(body, heading,
|
|
39
|
+
function parseInvalidationTable(body, heading, columnChoices) {
|
|
35
40
|
const section = findSectionRange(body, [heading], 3);
|
|
36
41
|
const lines = scanVisibleMarkdown(body).lines.filter((line) => line.text.trim());
|
|
37
|
-
|
|
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
|
+
}
|
|
38
57
|
if (!section || !table || lines.filter((line) => line.start >= section.bodyStart && line.start < section.end).length !== table.rows.length + 2) {
|
|
39
58
|
throw new Error(`invalidation ${heading} section must contain only its canonical table`);
|
|
40
59
|
}
|
|
41
|
-
return table.rows;
|
|
60
|
+
return { rows: table.rows, legacy };
|
|
42
61
|
}
|
|
43
62
|
function required(value, field) {
|
|
44
63
|
if (!value)
|
|
@@ -62,9 +81,10 @@ function parseInvalidationDocument(content) {
|
|
|
62
81
|
return { ok: true, present: false, document: { operations: [], targets: [] } };
|
|
63
82
|
try {
|
|
64
83
|
const body = content.slice(section.bodyStart, section.end);
|
|
65
|
-
const operationRows = parseInvalidationTable(body, 'Operations', OPERATION_COLUMNS);
|
|
66
|
-
|
|
67
|
-
const
|
|
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 }) => ({
|
|
68
88
|
operationId: required(row.operation_id, 'operation_id'), sourceFamily: required(row.source_family, 'source_family'),
|
|
69
89
|
sourceArtifact: required(row.source_artifact, 'source_artifact'), sourceRound: numberValue(row.source_round, 'source_round', false),
|
|
70
90
|
sourceSha256: required(row.source_sha256, 'source_sha256'), status: status(row.status, 'operation status'),
|
|
@@ -72,10 +92,11 @@ function parseInvalidationDocument(content) {
|
|
|
72
92
|
createdAt: required(row.created_at, 'created_at'), updatedAt: required(row.updated_at, 'updated_at'),
|
|
73
93
|
completedAt: row.completed_at ?? '', error: row.error ?? ''
|
|
74
94
|
}));
|
|
75
|
-
const targets = targetRows.map(({ values: row }) => ({
|
|
95
|
+
const targets = targetRows.rows.map(({ values: row }) => ({
|
|
76
96
|
targetId: required(row.target_id, 'target_id'), operationId: required(row.operation_id, 'operation_id'),
|
|
77
97
|
targetKind: row.target_kind, targetFamily: required(row.target_family, 'target_family'),
|
|
78
98
|
targetArtifact: required(row.target_artifact, 'target_artifact'), targetRound: numberValue(row.target_round, 'target_round', false),
|
|
99
|
+
...(row.target_input ? { targetInput: row.target_input } : {}),
|
|
79
100
|
targetSha256: required(row.target_sha256, 'target_sha256'), status: status(row.status, 'target status'),
|
|
80
101
|
reasonCode: required(row.reason_code, 'reason_code'), updatedAt: required(row.updated_at, 'updated_at')
|
|
81
102
|
}));
|
|
@@ -100,6 +121,14 @@ function parseInvalidationDocument(content) {
|
|
|
100
121
|
targetIds.add(target.targetId);
|
|
101
122
|
if (!operationIds.has(target.operationId))
|
|
102
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`);
|
|
103
132
|
if (target.targetId !== targetIdFor(target.operationId, target))
|
|
104
133
|
throw new Error(`target '${target.targetId}' has a non-canonical identity`);
|
|
105
134
|
if (!/^[a-f0-9]{64}$/.test(target.targetSha256))
|
|
@@ -143,7 +172,7 @@ function renderInvalidation(document) {
|
|
|
143
172
|
'### Targets', '',
|
|
144
173
|
table(TARGET_COLUMNS, targets.map((row) => [
|
|
145
174
|
row.targetId, row.operationId, row.targetKind, row.targetFamily, row.targetArtifact,
|
|
146
|
-
row.targetRound, row.targetSha256, row.status, row.reasonCode, row.updatedAt
|
|
175
|
+
row.targetInput ?? '', row.targetRound, row.targetSha256, row.status, row.reasonCode, row.updatedAt
|
|
147
176
|
]))
|
|
148
177
|
].join('\n');
|
|
149
178
|
}
|
|
@@ -6,6 +6,7 @@ import { parseLifecyclePathDecision } from './lifecycle-path.js';
|
|
|
6
6
|
import { resolveTaskRef } from './resolve-ref.js';
|
|
7
7
|
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
|
|
8
8
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
9
|
+
import { validateQualificationAudit } from './qualification-audit.js';
|
|
9
10
|
const LOCAL_ARTIFACT_REQUIRED_SECTIONS = {
|
|
10
11
|
analysis: getArtifactSchema('analysis').sections.map((section) => section.headings.zh),
|
|
11
12
|
plan: getArtifactSchema('plan').sections.map((section) => section.headings.zh),
|
|
@@ -82,19 +83,32 @@ function prepareLocalArtifact(request, candidate) {
|
|
|
82
83
|
const validation = validateLocalArtifact(content, {
|
|
83
84
|
family: request.family, requiredSections: request.requiredSections
|
|
84
85
|
});
|
|
86
|
+
let qualificationError = null;
|
|
87
|
+
try {
|
|
88
|
+
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
89
|
+
const qualification = validateQualificationAudit(taskContent, content, { family: request.family, artifact: request.artifact });
|
|
90
|
+
if (!qualification.ok)
|
|
91
|
+
qualificationError = `${qualification.code}: ${qualification.message}`;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
qualificationError = `QUALIFICATION_AUDIT_INVALID: ${String(error)}`;
|
|
95
|
+
}
|
|
85
96
|
const artifactSha256 = sha256Content(content);
|
|
97
|
+
const diagnostics = qualificationError
|
|
98
|
+
? [...validation.diagnostics, { code: 'LOCAL_STRUCTURAL_INVALID', message: qualificationError, line: null }]
|
|
99
|
+
: validation.diagnostics;
|
|
86
100
|
const result = failedFinalization(request, {
|
|
87
101
|
code: 'LOCAL_ARTIFACT_INVALID',
|
|
88
|
-
message:
|
|
102
|
+
message: diagnostics.map((item) => `${item.code}: ${item.message}`).join('; ')
|
|
89
103
|
}, {
|
|
90
104
|
taskId: resolved.taskId, taskDir: resolved.taskDir, artifactSha256,
|
|
91
|
-
semanticDigest: validation.semanticDigest, diagnostics
|
|
105
|
+
semanticDigest: validation.semanticDigest, diagnostics
|
|
92
106
|
});
|
|
93
107
|
// Trusted local operators edit the canonical artifact directly. A
|
|
94
108
|
// finalization result is a fresh observation of those bytes, never a
|
|
95
109
|
// capability minted by an earlier recovery attempt or generation.
|
|
96
110
|
return {
|
|
97
|
-
result: validation.ok ? { ...result, status: 'passed', error: null } : result,
|
|
111
|
+
result: validation.ok && !qualificationError ? { ...result, status: 'passed', error: null } : result,
|
|
98
112
|
content,
|
|
99
113
|
repoRoot: resolved.repoRoot,
|
|
100
114
|
lockAlreadyHeld: request.lockAlreadyHeld
|
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { parseArtifactName } from './artifact-name.js';
|
|
3
|
-
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
4
3
|
import { findSectionRange, parseTable } from './sections.js';
|
|
5
4
|
import { scanVisibleMarkdown } from './markdown.js';
|
|
6
5
|
const TASK_CONSTRAINT_HEADINGS = ['约束', 'Constraints'];
|
|
7
6
|
const TASK_CANDIDATE_HEADINGS = ['候选与否决方案', 'Candidate and Rejected Options', 'Candidates and Rejected Options', 'Candidates and Rejected Alternatives'];
|
|
8
7
|
const AUDIT_HEADINGS = ['资格审计', '资格审计复核', 'Qualification Audit', 'Qualification Audit Review'];
|
|
9
8
|
const CONFIRMATION_HEADINGS = ['资格确认记录', 'Qualification Confirmations'];
|
|
10
|
-
const AUDIT_SUBSECTIONS = ['约束依赖', '候选资格', '分类结果', '
|
|
9
|
+
const AUDIT_SUBSECTIONS = ['约束依赖', '候选资格', '分类结果', '资格快照'];
|
|
11
10
|
const AUDIT_SUBSECTION_HEADINGS = {
|
|
12
11
|
'约束依赖': ['约束依赖', 'Constraint Dependencies'],
|
|
13
12
|
'候选资格': ['候选资格', 'Candidate Qualification'],
|
|
14
13
|
'分类结果': ['分类结果', 'Classification Results'],
|
|
15
|
-
'
|
|
16
|
-
'依赖快照': ['依赖快照', 'Dependency Snapshot']
|
|
14
|
+
'资格快照': ['资格快照', 'Qualification Snapshot']
|
|
17
15
|
};
|
|
18
16
|
const CONSTRAINT_COLUMNS = ['constraint_id', 'statement', 'status', 'authority', 'source', 'evidence', 'derived_from', 'approval_evidence'];
|
|
19
17
|
const CANDIDATE_COLUMNS = ['candidate_id', 'statement', 'status', 'constraint_ids', 'impact', 'evidence'];
|
|
20
18
|
const DEPENDENCY_COLUMNS = ['constraint_id', 'constraint_digest', 'role', 'evidence'];
|
|
21
19
|
const QUALIFICATION_COLUMNS = ['candidate_id', 'status', 'impact', 'constraint_ids', 'evidence'];
|
|
22
20
|
const CLASSIFICATION_COLUMNS = ['decision_id', 'classification', 'evidence'];
|
|
23
|
-
const
|
|
24
|
-
const SNAPSHOT_COLUMNS = ['task_input_digest', 'non_constraint_input_digest', 'upstream_artifact_digest'];
|
|
21
|
+
const SNAPSHOT_COLUMNS = ['task_input_digest', 'non_constraint_input_digest'];
|
|
25
22
|
const CONFIRMATION_COLUMNS = ['qcr_id', 'constraint_id', 'actor', 'entrypoint', 'request_id', 'approved_digest', 'confirmed_at', 'rationale'];
|
|
26
23
|
const ARTIFACT_FAMILIES = ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code'];
|
|
27
|
-
const RELATIONS = ['required-input', 'reviewed-input', 'review-context', 'approval-context'];
|
|
28
24
|
const CONSTRAINT_STATUSES = ['confirmed', 'derived', 'assumption', 'open', 'conflicted', 'superseded'];
|
|
29
25
|
const CANDIDATE_STATUSES = ['qualified', 'rejected', 'pending', 'needs-human-decision', 'confirmed', 'excluded'];
|
|
30
26
|
const CLASSIFICATIONS = ['deterministic', 'qualified', 'rejected', 'needs-human-decision', 'not-applicable'];
|
|
@@ -215,86 +211,20 @@ function parseAuditTable(content, heading, columns) {
|
|
|
215
211
|
const table = parseTable(`## Qualification Audit Body\n${subsection}`, { sectionAliases: ['Qualification Audit Body'], columns });
|
|
216
212
|
return table?.rows.map((row) => ({ ...row.values })) ?? null;
|
|
217
213
|
}
|
|
218
|
-
function parseAuditArtifactName(value) {
|
|
219
|
-
const identity = parseArtifactName(value);
|
|
220
|
-
return identity && ARTIFACT_FAMILIES.includes(identity.family)
|
|
221
|
-
? { family: identity.family, round: identity.round } : null;
|
|
222
|
-
}
|
|
223
|
-
function upstreamArtifactDigest(rows) {
|
|
224
|
-
return digest([...rows].map((row) => ({ ...row })).sort((a, b) => `${a.upstreamFamily}/${a.upstreamArtifact}/${a.relation}`.localeCompare(`${b.upstreamFamily}/${b.upstreamArtifact}/${b.relation}`)));
|
|
225
|
-
}
|
|
226
|
-
function validateUpstreamRelation(row) {
|
|
227
|
-
const identity = parseAuditArtifactName(row.upstreamArtifact);
|
|
228
|
-
if (!ARTIFACT_FAMILIES.includes(row.upstreamFamily) || !identity || identity.family !== row.upstreamFamily
|
|
229
|
-
|| identity.round !== row.upstreamRound || !Number.isSafeInteger(row.upstreamRound)
|
|
230
|
-
|| !/^[a-f0-9]{64}$/i.test(row.upstreamSha256) || !RELATIONS.includes(row.relation)) {
|
|
231
|
-
throw new Error(`invalid qualification upstream relation '${row.upstreamArtifact}'`);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
function expectedQualificationRelations(taskContent, family) {
|
|
235
|
-
let frontmatter;
|
|
236
|
-
try {
|
|
237
|
-
frontmatter = parseTypedTaskFrontmatter(taskContent);
|
|
238
|
-
}
|
|
239
|
-
catch (error) {
|
|
240
|
-
return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: error instanceof Error ? error.message : String(error) };
|
|
241
|
-
}
|
|
242
|
-
const encoded = frontmatter.qualification_input_relations;
|
|
243
|
-
if (typeof encoded === 'string' && encoded) {
|
|
244
|
-
try {
|
|
245
|
-
const parsed = JSON.parse(encoded);
|
|
246
|
-
if (!Array.isArray(parsed))
|
|
247
|
-
throw new Error('qualification_input_relations must be an array');
|
|
248
|
-
const relations = parsed.map((value) => {
|
|
249
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
250
|
-
throw new Error('qualification input relation must be an object');
|
|
251
|
-
const row = value;
|
|
252
|
-
const relation = {
|
|
253
|
-
upstreamFamily: row.upstreamFamily,
|
|
254
|
-
upstreamArtifact: String(row.upstreamArtifact ?? ''),
|
|
255
|
-
upstreamRound: Number(row.upstreamRound),
|
|
256
|
-
upstreamSha256: String(row.upstreamSha256 ?? ''),
|
|
257
|
-
relation: row.relation
|
|
258
|
-
};
|
|
259
|
-
validateUpstreamRelation(relation);
|
|
260
|
-
return relation;
|
|
261
|
-
});
|
|
262
|
-
return { ok: true, relations };
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: error instanceof Error ? error.message : String(error) };
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
const inputName = family === 'code' ? frontmatter.code_input_artifact : family.startsWith('review-') ? frontmatter.review_input_artifact : '';
|
|
269
|
-
const inputSha256 = family === 'code' ? frontmatter.code_input_sha256 : family.startsWith('review-') ? frontmatter.review_input_sha256 : '';
|
|
270
|
-
if (typeof inputName !== 'string' || !inputName || typeof inputSha256 !== 'string' || !inputSha256)
|
|
271
|
-
return { ok: true, relations: undefined };
|
|
272
|
-
const identity = parseAuditArtifactName(inputName);
|
|
273
|
-
if (!identity)
|
|
274
|
-
return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: `started input '${inputName}' is not canonical` };
|
|
275
|
-
const relation = {
|
|
276
|
-
upstreamFamily: identity.family, upstreamArtifact: inputName, upstreamRound: identity.round,
|
|
277
|
-
upstreamSha256: inputSha256, relation: family === 'code' ? 'required-input' : 'reviewed-input'
|
|
278
|
-
};
|
|
279
|
-
try {
|
|
280
|
-
validateUpstreamRelation(relation);
|
|
281
|
-
}
|
|
282
|
-
catch (error) {
|
|
283
|
-
return { ok: false, code: 'QUALIFICATION_STARTED_INPUT_INVALID', message: error instanceof Error ? error.message : String(error) };
|
|
284
|
-
}
|
|
285
|
-
return { ok: true, relations: [relation] };
|
|
286
|
-
}
|
|
287
214
|
function parseQualificationAudit(content) {
|
|
288
|
-
|
|
289
|
-
|
|
215
|
+
const auditBody = sectionBody(content, AUDIT_HEADINGS);
|
|
216
|
+
if (auditBody === null)
|
|
217
|
+
return { ok: true, audit: { present: false, constraintDependencies: [], candidateQualifications: [], classifications: [], snapshot: null } };
|
|
290
218
|
try {
|
|
219
|
+
if (sectionBody(auditBody, ['上游关系', 'Upstream Relations'], 3) !== null) {
|
|
220
|
+
throw new Error('qualification audit uses the obsolete lifecycle-relation format; regenerate it using the current format');
|
|
221
|
+
}
|
|
291
222
|
const dependencies = parseAuditTable(content, '约束依赖', DEPENDENCY_COLUMNS);
|
|
292
223
|
const qualifications = parseAuditTable(content, '候选资格', QUALIFICATION_COLUMNS);
|
|
293
224
|
const classifications = parseAuditTable(content, '分类结果', CLASSIFICATION_COLUMNS);
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
throw new Error('qualification audit requires all five canonical tables and exactly one dependency snapshot');
|
|
225
|
+
const snapshots = parseAuditTable(content, '资格快照', SNAPSHOT_COLUMNS);
|
|
226
|
+
if (!dependencies || !qualifications || !classifications || !snapshots || snapshots.length !== 1) {
|
|
227
|
+
throw new Error('qualification audit requires three decision tables and exactly one qualification snapshot');
|
|
298
228
|
}
|
|
299
229
|
const constraintDependencies = dependencies.map((row) => {
|
|
300
230
|
if (!/^C-[1-9]\d*$/.test(row.constraint_id ?? '') || !/^[a-f0-9]{64}$/i.test(row.constraint_digest ?? '') || !ROLES.includes(row.role) || !row.evidence?.trim())
|
|
@@ -311,20 +241,15 @@ function parseQualificationAudit(content) {
|
|
|
311
241
|
throw new Error(`invalid qualification classification '${row.decision_id ?? ''}'`);
|
|
312
242
|
return { decisionId: row.decision_id, classification: row.classification, evidence: row.evidence };
|
|
313
243
|
});
|
|
314
|
-
const upstreamRelations = relations.map((row) => {
|
|
315
|
-
const relation = { upstreamFamily: row.upstream_family, upstreamArtifact: row.upstream_artifact ?? '', upstreamRound: Number(row.upstream_round), upstreamSha256: row.upstream_sha256 ?? '', relation: row.relation };
|
|
316
|
-
validateUpstreamRelation(relation);
|
|
317
|
-
return relation;
|
|
318
|
-
});
|
|
319
244
|
const snapshotRow = snapshots[0];
|
|
320
|
-
if (!/^[a-f0-9]{64}$/i.test(snapshotRow.task_input_digest ?? '') || !/^[a-f0-9]{64}$/i.test(snapshotRow.non_constraint_input_digest ?? '')
|
|
321
|
-
throw new Error('qualification
|
|
245
|
+
if (!/^[a-f0-9]{64}$/i.test(snapshotRow.task_input_digest ?? '') || !/^[a-f0-9]{64}$/i.test(snapshotRow.non_constraint_input_digest ?? ''))
|
|
246
|
+
throw new Error('qualification snapshot has invalid digest');
|
|
322
247
|
return {
|
|
323
248
|
ok: true,
|
|
324
249
|
audit: {
|
|
325
250
|
present: true, constraintDependencies, candidateQualifications,
|
|
326
|
-
classifications: classificationRows,
|
|
327
|
-
snapshot: { taskInputDigest: snapshotRow.task_input_digest, nonConstraintInputDigest: snapshotRow.non_constraint_input_digest
|
|
251
|
+
classifications: classificationRows,
|
|
252
|
+
snapshot: { taskInputDigest: snapshotRow.task_input_digest, nonConstraintInputDigest: snapshotRow.non_constraint_input_digest }
|
|
328
253
|
}
|
|
329
254
|
};
|
|
330
255
|
}
|
|
@@ -367,7 +292,7 @@ function validateQualificationAudit(taskContent, artifactContent, options = {})
|
|
|
367
292
|
const audit = parseQualificationAudit(artifactContent);
|
|
368
293
|
if (!audit.ok)
|
|
369
294
|
return audit;
|
|
370
|
-
if (!
|
|
295
|
+
if (!audit.audit.present && !options.require)
|
|
371
296
|
return { ok: true, qualification: task.qualification, audit: audit.audit };
|
|
372
297
|
if (!task.qualification.present)
|
|
373
298
|
return { ok: false, code: 'QUALIFICATION_TASK_CONTRACT_MISSING', message: 'task qualification contract is missing' };
|
|
@@ -424,13 +349,8 @@ function validateQualificationAudit(taskContent, artifactContent, options = {})
|
|
|
424
349
|
return { ok: false, code: 'QUALIFICATION_TASK_DIGEST_MISMATCH', message: 'qualification audit task input digest is stale' };
|
|
425
350
|
if (snapshot.nonConstraintInputDigest !== task.qualification.nonConstraintInputDigest)
|
|
426
351
|
return { ok: false, code: 'QUALIFICATION_NON_CONSTRAINT_DIGEST_MISMATCH', message: 'qualification audit non-constraint input digest is stale' };
|
|
427
|
-
if (snapshot.upstreamArtifactDigest !== upstreamArtifactDigest(audit.audit.upstreamRelations))
|
|
428
|
-
return { ok: false, code: 'QUALIFICATION_UPSTREAM_DIGEST_MISMATCH', message: 'qualification audit upstream digest does not match its relation rows' };
|
|
429
|
-
if (options.expectedUpstreamRelations && (options.expectedUpstreamRelations.length !== audit.audit.upstreamRelations.length
|
|
430
|
-
|| upstreamArtifactDigest(options.expectedUpstreamRelations) !== upstreamArtifactDigest(audit.audit.upstreamRelations)))
|
|
431
|
-
return { ok: false, code: 'QUALIFICATION_UPSTREAM_RELATION_MISMATCH', message: 'qualification audit upstream relations do not match the started artifact inputs' };
|
|
432
352
|
if (options.family && options.artifact) {
|
|
433
|
-
const identity =
|
|
353
|
+
const identity = parseArtifactName(options.artifact);
|
|
434
354
|
if (!identity || identity.family !== options.family)
|
|
435
355
|
return { ok: false, code: 'QUALIFICATION_ARTIFACT_IDENTITY_INVALID', message: `artifact '${options.artifact}' is not canonical for '${options.family}'` };
|
|
436
356
|
}
|
|
@@ -443,13 +363,12 @@ function renderQualificationAudit(audit) {
|
|
|
443
363
|
...rows.map((row) => `| ${row.map((value) => value.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/[\r\n]/g, ' ')).join(' | ')} |`)
|
|
444
364
|
].join('\n');
|
|
445
365
|
const list = (values) => values.join(',');
|
|
446
|
-
const snapshot = audit.snapshot ?? { taskInputDigest: '', nonConstraintInputDigest: ''
|
|
366
|
+
const snapshot = audit.snapshot ?? { taskInputDigest: '', nonConstraintInputDigest: '' };
|
|
447
367
|
return [
|
|
448
368
|
'### 约束依赖', '', table(DEPENDENCY_COLUMNS, audit.constraintDependencies.map((row) => [row.constraintId, row.constraintDigest, row.role, row.evidence])), '',
|
|
449
369
|
'### 候选资格', '', table(QUALIFICATION_COLUMNS, audit.candidateQualifications.map((row) => [row.candidateId, row.status, row.impact, list(row.constraintIds), row.evidence])), '',
|
|
450
370
|
'### 分类结果', '', table(CLASSIFICATION_COLUMNS, audit.classifications.map((row) => [row.decisionId, row.classification, row.evidence])), '',
|
|
451
|
-
'###
|
|
452
|
-
'### 依赖快照', '', table(SNAPSHOT_COLUMNS, [[snapshot.taskInputDigest, snapshot.nonConstraintInputDigest, snapshot.upstreamArtifactDigest]])
|
|
371
|
+
'### 资格快照', '', table(SNAPSHOT_COLUMNS, [[snapshot.taskInputDigest, snapshot.nonConstraintInputDigest]])
|
|
453
372
|
].join('\n');
|
|
454
373
|
}
|
|
455
374
|
function buildQualificationAudit(taskContent, input = {}) {
|
|
@@ -462,15 +381,14 @@ function buildQualificationAudit(taskContent, input = {}) {
|
|
|
462
381
|
const candidates = input.candidates ?? parsed.qualification.candidates.map((row) => row.candidateId);
|
|
463
382
|
const constraintDependencies = parsed.qualification.constraints.filter((row) => ids.has(row.constraintId)).map((row) => ({ constraintId: row.constraintId, constraintDigest: row.digest, role: 'required', evidence: row.evidence }));
|
|
464
383
|
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 }));
|
|
465
|
-
const upstreamRelations = [...(input.upstreamRelations ?? [])];
|
|
466
384
|
return {
|
|
467
385
|
ok: true,
|
|
468
386
|
audit: {
|
|
469
387
|
present: true, constraintDependencies, candidateQualifications,
|
|
470
|
-
classifications: input.classifications ?? [],
|
|
471
|
-
snapshot: { taskInputDigest: parsed.qualification.taskInputDigest, nonConstraintInputDigest: parsed.qualification.nonConstraintInputDigest
|
|
388
|
+
classifications: input.classifications ?? [],
|
|
389
|
+
snapshot: { taskInputDigest: parsed.qualification.taskInputDigest, nonConstraintInputDigest: parsed.qualification.nonConstraintInputDigest }
|
|
472
390
|
}
|
|
473
391
|
};
|
|
474
392
|
}
|
|
475
|
-
export { ARTIFACT_FAMILIES, AUDIT_HEADINGS, AUDIT_SUBSECTIONS, CONFIRMATION_COLUMNS, CONFIRMATION_HEADINGS, CANDIDATE_COLUMNS, CONSTRAINT_COLUMNS, DEPENDENCY_COLUMNS, QUALIFICATION_COLUMNS, CLASSIFICATION_COLUMNS,
|
|
393
|
+
export { ARTIFACT_FAMILIES, AUDIT_HEADINGS, AUDIT_SUBSECTIONS, CONFIRMATION_COLUMNS, CONFIRMATION_HEADINGS, CANDIDATE_COLUMNS, CONSTRAINT_COLUMNS, DEPENDENCY_COLUMNS, QUALIFICATION_COLUMNS, CLASSIFICATION_COLUMNS, SNAPSHOT_COLUMNS, constraintDigest, buildQualificationAudit, nonConstraintInputDigest, parseQualificationAudit, parseQualificationConfirmations, parseTaskQualification, renderQualificationAudit, taskInputDigest, validateQualificationAudit };
|
|
476
394
|
//# sourceMappingURL=qualification-audit.js.map
|
|
@@ -41,7 +41,7 @@ function listSortedNumeric(dir, width) {
|
|
|
41
41
|
.reverse();
|
|
42
42
|
}
|
|
43
43
|
function findInArchive(repoRoot, taskId) {
|
|
44
|
-
// archive-tasks
|
|
44
|
+
// archive-tasks stores local material at .agents/workspace/archive/YYYY/MM/DD/{taskId}/local/task.md
|
|
45
45
|
// where YYYY/MM/DD comes from completed_at (or updated_at fallback) — NOT from
|
|
46
46
|
// the task id's creation date. So we cannot derive the path from taskId alone;
|
|
47
47
|
// walk the bounded YYYY/MM/DD tree instead. Newest-first to favor recent archives.
|
|
@@ -51,7 +51,7 @@ function findInArchive(repoRoot, taskId) {
|
|
|
51
51
|
for (const month of listSortedNumeric(yearDir, 2)) {
|
|
52
52
|
const monthDir = path.join(yearDir, month);
|
|
53
53
|
for (const day of listSortedNumeric(monthDir, 2)) {
|
|
54
|
-
const candidate = path.join(monthDir, day, taskId, 'task.md');
|
|
54
|
+
const candidate = path.join(monthDir, day, taskId, 'local', 'task.md');
|
|
55
55
|
if (fs.existsSync(candidate))
|
|
56
56
|
return candidate;
|
|
57
57
|
}
|
|
@@ -125,7 +125,7 @@ function enumerateAllTaskDirs(repoRoot) {
|
|
|
125
125
|
for (const day of listSortedNumeric(monthDir, 2).reverse()) {
|
|
126
126
|
const dayDir = path.join(monthDir, day);
|
|
127
127
|
for (const taskId of fs.readdirSync(dayDir).sort())
|
|
128
|
-
add(taskId, path.join(dayDir, taskId), 'archive');
|
|
128
|
+
add(taskId, path.join(dayDir, taskId, 'local'), 'archive');
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
}
|
|
@@ -10,6 +10,7 @@ import { resolveTaskRef } from './resolve-ref.js';
|
|
|
10
10
|
import { validateLifecycleExecution } from './lifecycle-execution.js';
|
|
11
11
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
|
|
12
12
|
import { getArtifactSchema } from './artifact-schema.js';
|
|
13
|
+
import { validateQualificationAudit } from './qualification-audit.js';
|
|
13
14
|
import { canonicalSemanticDigest, inspectArtifactContract, sha256Content } from './artifact-operations.js';
|
|
14
15
|
const STAGES = {
|
|
15
16
|
analysis: { family: 'review-analysis' },
|
|
@@ -186,6 +187,12 @@ function prepareReviewSummaryCandidate(request, artifactContent, options = {}) {
|
|
|
186
187
|
artifactSha256, semanticDigest: structure.semanticDigest
|
|
187
188
|
});
|
|
188
189
|
}
|
|
190
|
+
const qualification = validateQualificationAudit(taskContent, artifactContent, { family: spec.family, artifact: request.artifact });
|
|
191
|
+
if (!qualification.ok) {
|
|
192
|
+
return reject('REVIEW_ARTIFACT_STRUCTURE_INVALID', `${qualification.code}: ${qualification.message}`, {
|
|
193
|
+
artifactSha256, semanticDigest
|
|
194
|
+
});
|
|
195
|
+
}
|
|
189
196
|
const transformed = finalizeReviewSummaryContent(artifactContent, stageStatus.unresolvedFindingCounts, {
|
|
190
197
|
refreshNumericCounts: true
|
|
191
198
|
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { resolveBranchWorktree } from '../git/branch-worktree.js';
|
|
5
|
+
import { snapshotReview } from '../git/review-snapshot.js';
|
|
6
|
+
import { resolveDeliveryTarget } from './delivery-target.js';
|
|
7
|
+
import { parseTypedTaskFrontmatter } from './frontmatter.js';
|
|
8
|
+
import { extractReviewDiffBase, extractReviewDiffFingerprint, extractReviewedHead, extractReviewTargetHead, extractReviewedSnapshotTree, loadPostReviewConfig, resolvePostReviewGlobs } from './review-fingerprint.js';
|
|
9
|
+
function git(cwd, args) {
|
|
10
|
+
return execFileSync('git', ['-C', cwd, ...args], { encoding: 'utf8' }).trim();
|
|
11
|
+
}
|
|
12
|
+
function result(status, message, reviewedHead = null, currentHead = null) {
|
|
13
|
+
return { status, message, reviewedHead, currentHead };
|
|
14
|
+
}
|
|
15
|
+
export function inspectReviewIdentity(taskDir, reportContent, repositoryRoot) {
|
|
16
|
+
const reviewedHead = extractReviewedHead(reportContent);
|
|
17
|
+
const targetHead = extractReviewTargetHead(reportContent);
|
|
18
|
+
const diffBase = extractReviewDiffBase(reportContent);
|
|
19
|
+
const fingerprint = extractReviewDiffFingerprint(reportContent);
|
|
20
|
+
const reviewedTree = extractReviewedSnapshotTree(reportContent);
|
|
21
|
+
if (!reviewedHead || !targetHead || !diffBase || !fingerprint || !reviewedTree) {
|
|
22
|
+
return result('indeterminate', 'Review report is missing commit or snapshot identity', reviewedHead || null);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const metadata = parseTypedTaskFrontmatter(fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8'));
|
|
26
|
+
const taskRepositoryRoot = git(taskDir, ['rev-parse', '--show-toplevel']);
|
|
27
|
+
const branch = String(metadata.branch ?? '').trim();
|
|
28
|
+
const gitRoot = branch ? resolveBranchWorktree(taskRepositoryRoot, branch) : taskRepositoryRoot;
|
|
29
|
+
if (!gitRoot)
|
|
30
|
+
return result('indeterminate', `Task branch '${branch}' is not checked out in a registered worktree`, reviewedHead);
|
|
31
|
+
const currentHead = git(gitRoot, ['rev-parse', 'HEAD']);
|
|
32
|
+
const baseline = git(gitRoot, ['rev-parse', `${reviewedHead}^{commit}`]);
|
|
33
|
+
if (baseline !== currentHead)
|
|
34
|
+
return result('different', 'Reviewed commit differs from current HEAD', baseline, currentHead);
|
|
35
|
+
const remote = String(metadata.delivery_remote ?? '').trim();
|
|
36
|
+
const baseRef = String(metadata.delivery_base_ref ?? '').trim();
|
|
37
|
+
if (!remote || !baseRef)
|
|
38
|
+
return result('indeterminate', 'Task delivery target is not bound', baseline, currentHead);
|
|
39
|
+
const target = resolveDeliveryTarget(gitRoot, { remote, baseRef });
|
|
40
|
+
if (!target.ok)
|
|
41
|
+
return result('indeterminate', target.message, baseline, currentHead);
|
|
42
|
+
const currentTarget = git(gitRoot, ['rev-parse', `${targetHead}^{commit}`]);
|
|
43
|
+
const computedDiffBase = git(gitRoot, ['merge-base', baseline, currentTarget]);
|
|
44
|
+
if (git(gitRoot, ['rev-parse', `${diffBase}^{commit}`]) !== computedDiffBase) {
|
|
45
|
+
return result('different', 'Saved review diff base no longer matches the current target', baseline, currentHead);
|
|
46
|
+
}
|
|
47
|
+
const globs = resolvePostReviewGlobs({}, loadPostReviewConfig(repositoryRoot ?? taskRepositoryRoot));
|
|
48
|
+
const trackedChanges = git(gitRoot, ['diff', '--name-only', '-z', baseline, '--', ...globs]);
|
|
49
|
+
const untrackedChanges = git(gitRoot, ['ls-files', '-o', '--exclude-standard', '-z', '--', ...globs]);
|
|
50
|
+
if (trackedChanges || untrackedChanges)
|
|
51
|
+
return result('different', 'Reviewed worktree contains uncommitted changes', baseline, currentHead);
|
|
52
|
+
const snapshot = snapshotReview({
|
|
53
|
+
cwd: gitRoot,
|
|
54
|
+
mode: 'worktree',
|
|
55
|
+
baseline,
|
|
56
|
+
diffBase: computedDiffBase,
|
|
57
|
+
globs
|
|
58
|
+
});
|
|
59
|
+
if (snapshot.fingerprint !== fingerprint)
|
|
60
|
+
return result('different', 'Reviewed diff fingerprint differs from the current worktree', baseline, currentHead);
|
|
61
|
+
if (snapshot.tree !== reviewedTree)
|
|
62
|
+
return result('different', 'Reviewed snapshot tree differs from the current worktree', baseline, currentHead);
|
|
63
|
+
return result('matched', 'Review report matches the current commit and clean worktree', baseline, currentHead);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return result('indeterminate', error instanceof Error ? error.message : String(error), reviewedHead);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=review-identity.js.map
|
|
@@ -17,8 +17,8 @@ function parseTable(content, input) {
|
|
|
17
17
|
new Set(input.columns).size !== input.columns.length) {
|
|
18
18
|
throw new DocumentMutationError('MUTATION_INVALID', 'table columns are invalid');
|
|
19
19
|
}
|
|
20
|
-
const keyColumn = input.keyColumn
|
|
21
|
-
if (!input.columns.includes(keyColumn)) {
|
|
20
|
+
const keyColumn = input.keyColumn === undefined ? input.columns[0] : input.keyColumn;
|
|
21
|
+
if (keyColumn !== null && !input.columns.includes(keyColumn)) {
|
|
22
22
|
throw new DocumentMutationError('MUTATION_INVALID', 'table key column is invalid');
|
|
23
23
|
}
|
|
24
24
|
const sections = matchingSections(content, input.sectionAliases);
|
|
@@ -68,11 +68,12 @@ function parseTable(content, input) {
|
|
|
68
68
|
column,
|
|
69
69
|
decodeCell(parsed.cells[columnIndex]).trim()
|
|
70
70
|
]));
|
|
71
|
-
const key = values[keyColumn];
|
|
72
|
-
if (seen.has(key)) {
|
|
71
|
+
const key = keyColumn === null ? null : values[keyColumn];
|
|
72
|
+
if (key !== null && seen.has(key)) {
|
|
73
73
|
throw new DocumentMutationError('TABLE_DUPLICATE_KEY', `duplicate table key '${key}'`);
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
if (key !== null)
|
|
76
|
+
seen.add(key);
|
|
76
77
|
rows.push({ values, sourceLine: index - Math.max(firstContent, 0) });
|
|
77
78
|
}
|
|
78
79
|
return { heading: section.heading, rows };
|
|
@@ -29,9 +29,12 @@ function mapLinkError(operation, error, key) {
|
|
|
29
29
|
}
|
|
30
30
|
return new TaskExecutionLockError('ORCHESTRATION_LOCK_FAILED', `lifecycle lock hard-link operation '${operation}' failed`, detail);
|
|
31
31
|
}
|
|
32
|
-
function lockKey(repoRoot, taskId) {
|
|
32
|
+
function lockKey(repoRoot, taskId, identityScope) {
|
|
33
33
|
const canonicalRepoRoot = fs.realpathSync.native(repoRoot);
|
|
34
|
-
const
|
|
34
|
+
const identity = identityScope === undefined
|
|
35
|
+
? `${canonicalRepoRoot}\0${taskId}`
|
|
36
|
+
: `${identityScope}\0${taskId}`;
|
|
37
|
+
const key = createHash('sha256').update(identity).digest('hex');
|
|
35
38
|
return { canonicalRepoRoot, key };
|
|
36
39
|
}
|
|
37
40
|
function parseOwner(raw, key) {
|
|
@@ -133,7 +136,7 @@ function cleanStaleCandidates(lockRoot, key, identityMatches) {
|
|
|
133
136
|
function withTaskExecutionLock(repoRoot, taskId, ownerName, callback, options = {}) {
|
|
134
137
|
let identity;
|
|
135
138
|
try {
|
|
136
|
-
identity = lockKey(repoRoot, taskId);
|
|
139
|
+
identity = lockKey(repoRoot, taskId, options.identityScope);
|
|
137
140
|
}
|
|
138
141
|
catch (error) {
|
|
139
142
|
throw new TaskExecutionLockError('ORCHESTRATION_LOCK_FAILED', 'failed to canonicalize the repository for lifecycle locking', { operation: 'canonicalize', errno: errno(error), key: 'unresolved' });
|