@fitlab-ai/agent-infra 0.11.3 → 0.11.5

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.
Files changed (172) hide show
  1. package/README.md +17 -8
  2. package/README.zh-CN.md +17 -8
  3. package/dist/bin/internal-cli.js +1 -6
  4. package/dist/lib/decide.js +4 -41
  5. package/dist/lib/git/branch-worktree.js +45 -0
  6. package/dist/lib/internal/cli-route-inventory.js +1 -4
  7. package/dist/lib/internal/task-activity.js +7 -40
  8. package/dist/lib/internal/task-artifact.js +1 -1
  9. package/dist/lib/internal/task-event.js +4 -37
  10. package/dist/lib/internal/task-finalization.js +48 -1
  11. package/dist/lib/internal/task-ledger.js +14 -44
  12. package/dist/lib/internal/task-lifecycle.js +1 -1
  13. package/dist/lib/internal/task-operation-registry.js +2 -4
  14. package/dist/lib/internal/task-review.js +5 -37
  15. package/dist/lib/internal/task-verify.js +3 -47
  16. package/dist/lib/internal/task-warning.js +4 -40
  17. package/dist/lib/platform/completion-backfill.js +63 -17
  18. package/dist/lib/platform/pr-summary.js +4 -23
  19. package/dist/lib/platform/verification-sync.js +12 -49
  20. package/dist/lib/sandbox/control/client.js +4 -1
  21. package/dist/lib/sandbox/control/executor.js +12 -4
  22. package/dist/lib/sandbox/control/protocol.js +2 -1
  23. package/dist/lib/sandbox/control/server.js +26 -143
  24. package/dist/lib/sandbox/removal.js +15 -8
  25. package/dist/lib/sandbox/task-cleanup.js +13 -20
  26. package/dist/lib/task/activity-intent.js +1 -2
  27. package/dist/lib/task/artifact-command.js +11 -4
  28. package/dist/lib/task/artifact-lifecycle.js +217 -85
  29. package/dist/lib/task/artifact-operations.js +37 -28
  30. package/dist/lib/task/artifact-receipts.js +4 -1
  31. package/dist/lib/task/artifact-schema.js +8 -7
  32. package/dist/lib/task/artifact-selection.js +25 -0
  33. package/dist/lib/task/capabilities.js +158 -39
  34. package/dist/lib/task/commands/status.js +9 -2
  35. package/dist/lib/task/control-authority.js +12 -168
  36. package/dist/lib/task/decision-intents.js +14 -4
  37. package/dist/lib/task/delivery.js +1 -43
  38. package/dist/lib/task/events.js +68 -73
  39. package/dist/lib/task/finalization-handoff.js +92 -0
  40. package/dist/lib/task/finalization.js +423 -154
  41. package/dist/lib/task/intermediate-cleanup.js +9 -148
  42. package/dist/lib/task/ledger-intents.js +159 -20
  43. package/dist/lib/task/lifecycle-path.js +75 -0
  44. package/dist/lib/task/lifecycle.js +60 -11
  45. package/dist/lib/task/local-artifact-finalization.js +13 -47
  46. package/dist/lib/task/orchestration.js +17 -7
  47. package/dist/lib/task/review-command.js +1 -2
  48. package/dist/lib/task/review-finalization.js +4 -18
  49. package/dist/lib/task/rework-intent.js +70 -4
  50. package/dist/lib/task/snapshot.js +6 -6
  51. package/dist/lib/task/verification-engine.js +25 -30
  52. package/dist/lib/task/verification.js +2 -13
  53. package/dist/lib/task/workflow-warning-intents.js +23 -10
  54. package/dist/lib/task/workspace-snapshot.js +7 -2
  55. package/dist/lib/task/write.js +1 -2
  56. package/dist/lifecycle-build-manifest.json +3 -3
  57. package/lib/decide.ts +5 -35
  58. package/lib/git/branch-worktree.ts +41 -0
  59. package/lib/internal/cli-route-inventory.ts +1 -3
  60. package/lib/internal/task-activity.ts +6 -34
  61. package/lib/internal/task-artifact.ts +1 -1
  62. package/lib/internal/task-event.ts +4 -37
  63. package/lib/internal/task-finalization.ts +47 -1
  64. package/lib/internal/task-ledger.ts +15 -41
  65. package/lib/internal/task-lifecycle.ts +2 -2
  66. package/lib/internal/task-operation-registry.ts +2 -4
  67. package/lib/internal/task-review.ts +5 -31
  68. package/lib/internal/task-verify.ts +3 -39
  69. package/lib/internal/task-warning.ts +3 -32
  70. package/lib/platform/completion-backfill.ts +78 -16
  71. package/lib/platform/pr-summary.ts +4 -25
  72. package/lib/platform/verification-sync.ts +13 -53
  73. package/lib/sandbox/control/client.ts +5 -1
  74. package/lib/sandbox/control/executor.ts +13 -4
  75. package/lib/sandbox/control/protocol.ts +3 -2
  76. package/lib/sandbox/control/server.ts +21 -146
  77. package/lib/sandbox/removal.ts +23 -7
  78. package/lib/sandbox/task-cleanup.ts +12 -20
  79. package/lib/task/activity-intent.ts +1 -2
  80. package/lib/task/artifact-command.ts +14 -4
  81. package/lib/task/artifact-lifecycle.ts +236 -85
  82. package/lib/task/artifact-operations.ts +59 -28
  83. package/lib/task/artifact-receipts.ts +4 -1
  84. package/lib/task/artifact-schema.ts +10 -7
  85. package/lib/task/artifact-selection.ts +53 -0
  86. package/lib/task/capabilities.ts +154 -40
  87. package/lib/task/commands/status.ts +11 -2
  88. package/lib/task/control-authority.ts +17 -189
  89. package/lib/task/decision-intents.ts +11 -4
  90. package/lib/task/delivery.ts +1 -40
  91. package/lib/task/events.ts +73 -74
  92. package/lib/task/finalization-handoff.ts +104 -0
  93. package/lib/task/finalization.ts +388 -122
  94. package/lib/task/intermediate-cleanup.ts +9 -125
  95. package/lib/task/ledger-intents.ts +157 -20
  96. package/lib/task/lifecycle-path.ts +98 -0
  97. package/lib/task/lifecycle.ts +58 -13
  98. package/lib/task/local-artifact-finalization.ts +14 -56
  99. package/lib/task/orchestration.ts +26 -8
  100. package/lib/task/review-command.ts +2 -5
  101. package/lib/task/review-finalization.ts +4 -20
  102. package/lib/task/rework-intent.ts +75 -5
  103. package/lib/task/snapshot.ts +6 -6
  104. package/lib/task/verification-engine.ts +29 -30
  105. package/lib/task/verification.ts +3 -16
  106. package/lib/task/workflow-warning-intents.ts +20 -10
  107. package/lib/task/workspace-snapshot.ts +3 -2
  108. package/lib/task/write.ts +1 -4
  109. package/package.json +1 -1
  110. package/templates/.agents/rules/review-handshake.en.md +4 -4
  111. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  112. package/templates/.agents/skills/analyze-task/SKILL.en.md +21 -5
  113. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +21 -5
  114. package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
  115. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
  116. package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
  117. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
  118. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
  119. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
  120. package/templates/.agents/skills/code-task/reference/report-template.en.md +7 -7
  121. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +7 -7
  122. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
  123. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
  124. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
  125. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
  126. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  127. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  128. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  129. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  130. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
  131. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
  132. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
  133. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
  134. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
  135. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
  136. package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
  137. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
  138. package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
  139. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
  140. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
  141. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
  142. package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
  143. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
  144. package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
  145. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
  146. package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
  147. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
  148. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
  149. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
  150. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  151. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  152. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
  153. package/templates/.agents/templates/task.en.md +2 -2
  154. package/templates/.agents/templates/task.zh-CN.md +2 -2
  155. package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
  156. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
  157. package/templates/.agents/workflows/feature-development.en.yaml +11 -6
  158. package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
  159. package/templates/.agents/workflows/refactoring.en.yaml +11 -6
  160. package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
  161. package/dist/lib/internal/task-override.js +0 -138
  162. package/dist/lib/sandbox/control/finalization-response.js +0 -33
  163. package/dist/lib/task/artifact-recovery.js +0 -476
  164. package/dist/lib/task/artifact-repair-intent.js +0 -181
  165. package/dist/lib/task/guard-override.js +0 -31
  166. package/dist/lib/task/human-override.js +0 -1259
  167. package/lib/internal/task-override.ts +0 -144
  168. package/lib/sandbox/control/finalization-response.ts +0 -35
  169. package/lib/task/artifact-recovery.ts +0 -572
  170. package/lib/task/artifact-repair-intent.ts +0 -227
  171. package/lib/task/guard-override.ts +0 -50
  172. package/lib/task/human-override.ts +0 -1522
@@ -8,7 +8,9 @@ import { parseVerdict } from './review-artifacts.js';
8
8
  import { extractSection, findSectionHeading } from './sections.js';
9
9
  import { receiptForOutput, sha256File } from './artifact-receipts.js';
10
10
  import { isArtifactInvalidated, parseInvalidationDocument } from './invalidation.js';
11
- import { validateQualificationAudit } from './qualification-audit.js';
11
+ import { parseLifecyclePathDecision } from './lifecycle-path.js';
12
+ import { parseReworkIntentDocument } from './rework-intent.js';
13
+ import { selectArtifactDisposition } from './artifact-selection.js';
12
14
  const artifactFamilyCatalog = ARTIFACT_FAMILY_CATALOG;
13
15
  const ARTIFACT_STEPS = {
14
16
  analysis: 'Analyze Task', 'review-analysis': 'Review Analysis',
@@ -27,6 +29,14 @@ export function hasOpenArtifactRound(content, family, round) {
27
29
  const expected = new RegExp(`^${step} \\(Round ${round}${qualifier}\\)$`);
28
30
  return startedBackedRows(pairEntries(activity.entries)).filter((row) => expected.test(row.step) && !row.done).length === 1;
29
31
  }
32
+ function hasCompletedArtifactRound(content, family, round, name) {
33
+ const activity = locateActivityLog(content);
34
+ const step = ARTIFACT_STEPS[family];
35
+ if (!activity || !step)
36
+ return false;
37
+ const expected = new RegExp(`^${escapeRegExp(step)} \\(Round ${round}(?:, [^)]+)?\\)$`);
38
+ return startedBackedRows(pairEntries(activity.entries)).some((row) => expected.test(row.step) && Boolean(row.done) && row.note.includes(`→ ${name}`));
39
+ }
30
40
  const BLOCKING_DIAGNOSTICS = new Set([
31
41
  'NONCANONICAL_NAME', 'ROUND_OUT_OF_RANGE', 'MISSING_BASE', 'ROUND_GAP',
32
42
  'DUPLICATE_LOGICAL_ROUND', 'NON_REGULAR_FILE', 'SYMBOLIC_LINK',
@@ -205,15 +215,29 @@ function resolveCodeInputReceipt(taskDir, code) {
205
215
  }
206
216
  if (!receipt || receipt.event !== 'code.completed')
207
217
  return null;
208
- const plan = inspectArtifactDirectory(taskDir, 'plan');
209
- if (plan.status !== 'ready')
218
+ const identity = parseArtifactName(receipt.input);
219
+ if (!identity || !['analysis', 'plan'].includes(identity.family))
220
+ return null;
221
+ const source = inspectArtifactDirectory(taskDir, identity.family);
222
+ if (source.status !== 'ready')
210
223
  return null;
211
- const input = plan.artifacts.find((artifact) => artifact.name === receipt.input);
224
+ const input = source.artifacts.find((artifact) => artifact.name === receipt.input);
212
225
  return input ? { input, inputSha256: receipt.inputSha256 } : null;
213
226
  }
214
227
  function resolveCodePlanInput(taskDir, code) {
215
228
  return resolveCodeInputReceipt(taskDir, code);
216
229
  }
230
+ function lifecyclePathState(taskDir) {
231
+ const analysis = inspectArtifactDirectory(taskDir, 'analysis');
232
+ if (analysis.status !== 'ready' || !analysis.latest)
233
+ return { status: 'missing', decision: null, message: 'analysis artifact is missing' };
234
+ try {
235
+ return parseLifecyclePathDecision(fs.readFileSync(analysis.latest.path, 'utf8'), analysis.latest.name, sha256File(analysis.latest.path));
236
+ }
237
+ catch (error) {
238
+ return { status: 'invalid', decision: null, message: error instanceof Error ? error.message : String(error) };
239
+ }
240
+ }
217
241
  function assertWritableInventory(inventory) {
218
242
  if (inventory.status === 'failed')
219
243
  return inventory.error;
@@ -227,13 +251,87 @@ const REQUIRED_INPUT = {
227
251
  'review-analysis': 'analysis', plan: 'analysis', 'review-plan': 'plan', 'review-code': 'code'
228
252
  };
229
253
  const OPTIONAL_CONTEXT = {
230
- analysis: { family: 'review-analysis', requireReference: true },
231
- plan: { family: 'review-plan', requireReference: true },
254
+ analysis: { family: 'review-analysis' },
255
+ plan: { family: 'review-plan' },
232
256
  'review-code': { family: 'review-plan' },
233
257
  'manual-validation': { family: 'review-code' },
234
258
  'validation-run': { family: 'review-code' }
235
259
  };
236
- const QUALIFICATION_RECOVERY_OPTIONAL_CONTEXT_CONSUMERS = new Set(['analysis', 'plan']);
260
+ function hasSelectionEvidence(content, family, options, context) {
261
+ if (options.sourceFinding && options.sourceArtifact && options.sourceSha256)
262
+ return true;
263
+ if (options.reasonCode === 'new-requirement' || options.reasonCode === 'upstream-fact-doubt')
264
+ return true;
265
+ if (family.startsWith('review-')) {
266
+ const reviewedFamily = family === 'review-analysis' ? 'analysis' : family === 'review-plan' ? 'plan' : 'code';
267
+ const currentInput = context.inputs.find((input) => input.family === reviewedFamily);
268
+ if (currentInput && context.reviewedInput?.name !== currentInput.name)
269
+ return true;
270
+ }
271
+ const target = family === 'analysis' ? 'analysis' : family === 'plan' ? 'plan' : family === 'code' ? 'code' : null;
272
+ if (target) {
273
+ const parsed = parseReworkIntentDocument(content);
274
+ if (!parsed.ok)
275
+ throw new Error(`${parsed.code}: ${parsed.message}`);
276
+ if (parsed.intents.some((intent) => intent.status === 'pending'
277
+ && (intent.target === target || family === 'analysis' && intent.target === 'plan')))
278
+ return true;
279
+ }
280
+ if (family === 'code') {
281
+ if (context.latest && context.codeMode?.mode === 'init')
282
+ return true;
283
+ if (parseImplementationInputs(content).rows.some((input) => input.needsImplementation && input.status === 'pending'))
284
+ return true;
285
+ }
286
+ return context.inputs.filter((candidate) => candidate.family.startsWith('review-')).some((input) => {
287
+ const verdict = parseVerdict(input.path);
288
+ if (!verdict.ok || verdict.verdict !== 'Changes Requested' || !context.latest)
289
+ return false;
290
+ const receipt = receiptForOutput(content, input.name);
291
+ return receipt?.event === reviewEventName(input.family)
292
+ && receipt.input === context.latest.name
293
+ && receipt.inputSha256 === sha256File(context.latest.path);
294
+ });
295
+ }
296
+ function attachArtifactSelection(context, pathState, options) {
297
+ if (context.status !== 'ready' || !context.taskDir || !context.taskId
298
+ || (context.family !== 'analysis' && pathState.status !== 'valid'))
299
+ return context;
300
+ const family = context.family;
301
+ const fail = (code, message) => ({
302
+ ...context, status: 'failed', selection: null, error: { code, message }
303
+ });
304
+ if (!context.next)
305
+ return fail('ARTIFACT_SELECTION_INDETERMINATE', 'artifact inventory did not provide a writable identity');
306
+ let taskContent;
307
+ try {
308
+ taskContent = fs.readFileSync(path.join(context.taskDir, 'task.md'), 'utf8');
309
+ }
310
+ catch (error) {
311
+ return fail('ARTIFACT_SELECTION_INDETERMINATE', error instanceof Error ? error.message : String(error));
312
+ }
313
+ const openLatest = context.latest ? hasOpenArtifactRound(taskContent, family, context.latest.round) : false;
314
+ const openNext = hasOpenArtifactRound(taskContent, family, context.next.round);
315
+ const open = openLatest || openNext;
316
+ const selectedIdentity = openLatest ? context.latest : context.next;
317
+ try {
318
+ const hasChangeEvidence = hasSelectionEvidence(taskContent, family, options, context);
319
+ const selection = selectArtifactDisposition({
320
+ next: { family, round: selectedIdentity.round, name: selectedIdentity.name },
321
+ latest: context.latest,
322
+ open,
323
+ hasChangeEvidence
324
+ });
325
+ return {
326
+ ...context,
327
+ selection,
328
+ next: selection.writeRequired ? { round: selection.artifact.round, name: selection.artifact.name } : null
329
+ };
330
+ }
331
+ catch (error) {
332
+ return fail('ARTIFACT_SELECTION_INDETERMINATE', error instanceof Error ? error.message : String(error));
333
+ }
334
+ }
237
335
  function resolveArtifactContext(taskRef, family, options = {}) {
238
336
  const inventory = inspectTaskArtifacts(taskRef, family, options);
239
337
  if (inventory.status === 'failed')
@@ -241,8 +339,43 @@ function resolveArtifactContext(taskRef, family, options = {}) {
241
339
  const writableError = assertWritableInventory(inventory);
242
340
  if (writableError)
243
341
  return { ...inventory, status: 'failed', inputs: [], codeMode: null, error: writableError };
244
- if (inventory.family === 'code')
245
- return resolveCodeContext(inventory, options);
342
+ const pathState = lifecyclePathState(inventory.taskDir);
343
+ if (inventory.family !== 'analysis' && pathState.status !== 'valid') {
344
+ return contextFailure(inventory, 'LIFECYCLE_PATH_INVALID', pathState.message);
345
+ }
346
+ if (inventory.family === 'code') {
347
+ const code = resolveCodeContext(inventory, options);
348
+ const selected = attachArtifactSelection(code, pathState, options);
349
+ if (selected.status !== 'ready' || !selected.selection)
350
+ return selected;
351
+ if (selected.selection.disposition === 'reuse')
352
+ return selected;
353
+ if (selected.codeMode?.mode === 'error') {
354
+ return {
355
+ ...selected,
356
+ status: 'ready',
357
+ codeMode: {
358
+ ...selected.codeMode,
359
+ mode: 'init',
360
+ message: 'A new implementation round was requested.'
361
+ },
362
+ error: null
363
+ };
364
+ }
365
+ if (selected.codeMode?.mode === 'refused') {
366
+ return {
367
+ ...selected,
368
+ status: 'ready',
369
+ codeMode: {
370
+ ...selected.codeMode,
371
+ mode: 'init',
372
+ message: 'A new implementation round was requested.'
373
+ },
374
+ error: null
375
+ };
376
+ }
377
+ return selected;
378
+ }
246
379
  const required = REQUIRED_INPUT[inventory.family];
247
380
  const inputs = [];
248
381
  if (required) {
@@ -250,98 +383,71 @@ function resolveArtifactContext(taskRef, family, options = {}) {
250
383
  if (input.status === 'failed' || !input.latest) {
251
384
  return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_INPUT_MISSING', message: `latest ${required} artifact is required` } };
252
385
  }
253
- const qualificationError = qualificationErrorForArtifact(input.latest);
254
- if (qualificationError)
255
- return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_REFERENCE_INVALID', message: qualificationError } };
256
386
  inputs.push(input.latest);
257
387
  }
258
388
  const optional = OPTIONAL_CONTEXT[inventory.family];
259
389
  if (optional) {
260
390
  const context = inspectTaskArtifacts(taskRef, optional.family, options);
261
391
  if (context.status === 'ready' && context.latest) {
262
- if (optional.requireReference && !context.reviewedInput) {
263
- return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_REFERENCE_INVALID', message: `${context.latest.name} has no valid reviewed input` } };
264
- }
265
- const qualificationError = qualificationErrorForArtifact(context.latest);
266
- if (qualificationError && !QUALIFICATION_RECOVERY_OPTIONAL_CONTEXT_CONSUMERS.has(inventory.family)) {
267
- return { ...inventory, status: 'failed', inputs, codeMode: null, error: { code: 'ARTIFACT_REFERENCE_INVALID', message: qualificationError } };
268
- }
269
392
  inputs.push(context.latest);
270
393
  }
271
394
  }
272
- return { ...inventory, inputs, codeMode: null };
273
- }
274
- function qualificationErrorForArtifact(artifact) {
275
- try {
276
- const taskContent = fs.readFileSync(path.join(path.dirname(artifact.path), 'task.md'), 'utf8');
277
- const artifactContent = fs.readFileSync(artifact.path, 'utf8');
278
- const result = validateQualificationAudit(taskContent, artifactContent);
279
- return result.ok ? null : `${result.code}: ${result.message}`;
280
- }
281
- catch (error) {
282
- return error instanceof Error ? error.message : String(error);
395
+ if (inventory.family === 'manual-validation' || inventory.family === 'validation-run') {
396
+ return { ...inventory, inputs, codeMode: null };
283
397
  }
398
+ return attachArtifactSelection({ ...inventory, inputs, codeMode: null }, pathState, options);
284
399
  }
285
400
  function resolveCodeContext(inventory, options) {
286
401
  const taskRef = inventory.requestRef;
287
- const plan = inspectTaskArtifacts(taskRef, 'plan', options);
288
- if (plan.status === 'failed' || !plan.latest)
289
- return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', 'latest plan artifact is required');
290
- const reviewPlan = inspectTaskArtifacts(taskRef, 'review-plan', options);
402
+ const pathState = lifecyclePathState(inventory.taskDir);
403
+ if (pathState.status !== 'valid')
404
+ return contextFailure(inventory, 'LIFECYCLE_PATH_INVALID', pathState.message);
405
+ const inputFamily = pathState.decision.path === 'streamlined' ? 'analysis' : 'plan';
406
+ const source = inspectTaskArtifacts(taskRef, inputFamily, options);
407
+ if (source.status === 'failed' || !source.latest)
408
+ return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest ${inputFamily} artifact is required`);
291
409
  const reviewCode = inspectTaskArtifacts(taskRef, 'review-code', options);
292
410
  const latestCode = inventory.latest;
293
411
  const codeMax = latestCode?.round ?? 0;
294
412
  const reviewMax = reviewCode.latest?.round ?? 0;
295
- const inputs = [plan.latest];
296
- const planQualificationError = qualificationErrorForArtifact(plan.latest);
297
- if (planQualificationError)
298
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', planQualificationError);
413
+ const inputs = [source.latest];
299
414
  if (latestCode) {
300
- const qualificationError = qualificationErrorForArtifact(latestCode);
301
- if (qualificationError)
302
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', qualificationError, latestCode.name);
303
- }
304
- if (reviewCode.latest) {
305
- const qualificationError = qualificationErrorForArtifact(reviewCode.latest);
306
- if (qualificationError)
307
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', qualificationError, reviewCode.latest.name);
308
- }
309
- if (!latestCode) {
310
- if (!reviewPlan.latest || reviewPlan.reviewedInput?.name !== plan.latest.name) {
311
- return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `latest plan '${plan.latest.name}' requires a matching approved review-plan`);
415
+ try {
416
+ const taskContent = fs.readFileSync(path.join(inventory.taskDir, 'task.md'), 'utf8');
417
+ if (hasOpenArtifactRound(taskContent, 'code', latestCode.round)) {
418
+ const review = reviewCode.latest;
419
+ const openInputs = review ? [...inputs, review] : inputs;
420
+ const openVerdict = review ? parseVerdict(review.path) : null;
421
+ const openRow = locateActivityLog(taskContent)
422
+ ? startedBackedRows(pairEntries(locateActivityLog(taskContent).entries))
423
+ .find((row) => row.step.startsWith(`Code Task (Round ${latestCode.round}`) && !row.done)
424
+ : null;
425
+ const decisionInput = /, decision (II-[1-9]\d*)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
426
+ const fixInput = /, fix for (review-code(?:-r\d+)?\.md)\)$/u.exec(openRow?.step ?? '')?.[1] ?? null;
427
+ const mode = decisionInput ? 'decision'
428
+ : fixInput || (openVerdict?.ok && openVerdict.verdict === 'Changes Requested') ? 'fix'
429
+ : 'init';
430
+ return withCodeMode(inventory, openInputs, 'ready', mode, codeMax, reviewMax, openVerdict?.ok ? openVerdict.verdict : null, fixInput ?? review?.name ?? null, `Resuming open code round ${latestCode.round}.`, decisionInput);
431
+ }
312
432
  }
313
- const verdict = parseVerdict(reviewPlan.latest.path);
314
- if (!verdict.ok)
315
- return contextFailure(inventory, 'ARTIFACT_VERDICT_INVALID', `${verdict.code}: ${verdict.message}`, reviewPlan.latest.name);
316
- if (verdict.verdict !== 'Approved') {
317
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `latest ${reviewPlan.latest.name} is not approved`, reviewPlan.latest.name);
433
+ catch (error) {
434
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error));
318
435
  }
436
+ }
437
+ if (!latestCode) {
319
438
  return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null, 'No prior code artifact. Starting initial implementation (round 1 -> code.md).');
320
439
  }
321
440
  const codePlanInput = resolveCodePlanInput(inventory.taskDir, latestCode);
322
441
  if (!codePlanInput) {
323
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `code completion receipt for ${latestCode.name} is missing or does not match the current plan`, latestCode.name);
324
- }
325
- if (reviewPlan.latest && !reviewPlan.reviewedInput) {
326
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', `${reviewPlan.latest.name} has no valid reviewed input`, reviewPlan.latest.name);
327
- }
328
- if (reviewPlan.latest) {
329
- const qualificationError = qualificationErrorForArtifact(reviewPlan.latest);
330
- if (qualificationError)
331
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', qualificationError, reviewPlan.latest.name);
332
- }
333
- if (reviewPlan.latest && reviewPlan.reviewedInput?.name === plan.latest.name) {
334
- const verdict = parseVerdict(reviewPlan.latest.path);
335
- if (!verdict.ok)
336
- return contextFailure(inventory, 'ARTIFACT_VERDICT_INVALID', `${verdict.code}: ${verdict.message}`, reviewPlan.latest.name);
337
- const reviewedPlanSha256 = sha256File(reviewPlan.reviewedInput.path);
338
- if (verdict.ok && verdict.verdict === 'Approved' && (codePlanInput.input.name !== plan.latest.name || codePlanInput.inputSha256 !== reviewedPlanSha256)) {
339
- return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, verdict.verdict, reviewPlan.latest.name, `Latest ${reviewPlan.latest.name} approves plan content not captured by the latest code input receipt. Entering replan-driven init.`);
340
- }
442
+ return withCodeMode(inventory, inputs, 'ready', 'error', codeMax, reviewMax, null, null, `Code input receipt for ${latestCode.name} is unavailable.`);
443
+ }
444
+ const currentInputSha256 = sha256File(source.latest.path);
445
+ if (codePlanInput.input.name !== source.latest.name || codePlanInput.inputSha256 !== currentInputSha256) {
446
+ return withCodeMode(inventory, inputs, 'ready', 'init', codeMax, reviewMax, null, null, `Latest ${source.latest.name} is not captured by the latest code input receipt. Entering replan-driven init.`);
341
447
  }
342
448
  if (!reviewCode.latest || reviewCode.reviewedInput?.name !== latestCode.name) {
343
449
  const expected = reviewCode.next?.name ?? artifactName('review-code', reviewMax + 1);
344
- return contextFailure(inventory, 'ARTIFACT_INPUT_MISSING', `${expected} is required before another code round`, expected);
450
+ return withCodeMode(inventory, inputs, 'ready', 'error', codeMax, reviewMax, null, expected, `${expected} is required before another code round`);
345
451
  }
346
452
  const review = reviewCode.latest;
347
453
  if (!review)
@@ -349,18 +455,31 @@ function resolveCodeContext(inventory, options) {
349
455
  const verdict = parseVerdict(review.path);
350
456
  if (!verdict.ok)
351
457
  return contextFailure(inventory, 'ARTIFACT_VERDICT_INVALID', `${verdict.code}: ${verdict.message}`, review.name);
352
- if (verdict.verdict === 'Approved') {
353
- const decision = resolveDecisionImplementationInput(inventory, review);
354
- if ('error' in decision)
355
- return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', decision.error, review.name);
356
- if (decision.input) {
357
- return withCodeMode(inventory, [...inputs, review], 'ready', 'decision', codeMax, reviewMax, verdict.verdict, review.name, `Implementation input ${decision.input.id} requires a new code round.`, decision.input.id, decision.input.ledgerId, decision.input.decisionEvidence);
458
+ const decision = resolveDecisionImplementationInput(inventory, review);
459
+ if ('error' in decision)
460
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', decision.error, review.name);
461
+ if (decision.input) {
462
+ return withCodeMode(inventory, [...inputs, review], 'ready', 'decision', codeMax, reviewMax, verdict.verdict, review.name, `Implementation input ${decision.input.id} requires a new code round.`, decision.input.id, decision.input.ledgerId, decision.input.decisionEvidence);
463
+ }
464
+ try {
465
+ const taskContent = fs.readFileSync(path.join(inventory.taskDir, 'task.md'), 'utf8');
466
+ const rework = parseReworkIntentDocument(taskContent);
467
+ const reviewSha256 = sha256File(review.path);
468
+ if (rework.ok && rework.intents.some((intent) => intent.status === 'pending'
469
+ && intent.target === 'code'
470
+ && intent.sourceArtifact === review.name
471
+ && intent.sourceSha256 === reviewSha256)) {
472
+ return withCodeMode(inventory, [...inputs, review], 'ready', 'fix', codeMax, reviewMax, verdict.verdict, review.name, `Pending rework evidence requires a new code round after ${review.name}.`);
358
473
  }
359
474
  }
360
- if (verdict.verdict === 'Approved' || verdict.verdict === 'Rejected') {
361
- return withCodeMode(inventory, [...inputs, review], 'refused', 'refused', codeMax, reviewMax, verdict.verdict, review.name, verdict.verdict === 'Approved'
362
- ? `Latest ${review.name} verdict is Approved with no findings. Nothing to fix. Run /commit to proceed.`
363
- : `Latest ${review.name} verdict is Rejected. Re-plan before re-running code-task.`);
475
+ catch (error) {
476
+ return contextFailure(inventory, 'ARTIFACT_REFERENCE_INVALID', error instanceof Error ? error.message : String(error), review.name);
477
+ }
478
+ if (verdict.verdict === 'Approved') {
479
+ return withCodeMode(inventory, [...inputs, review], 'ready', 'refused', codeMax, reviewMax, verdict.verdict, review.name, `Latest ${review.name} verdict is Approved with no findings. Nothing to fix. Run /commit to proceed.`);
480
+ }
481
+ if (verdict.verdict === 'Rejected') {
482
+ return withCodeMode(inventory, [...inputs, review], 'ready', 'refused', codeMax, reviewMax, verdict.verdict, review.name, `Latest ${review.name} verdict is Rejected. Re-plan before re-running code-task.`);
364
483
  }
365
484
  return withCodeMode(inventory, [...inputs, review], 'ready', 'fix', codeMax, reviewMax, verdict.verdict, review.name, `Latest ${review.name} requires or permits fixes. Entering fix mode.`);
366
485
  }
@@ -439,8 +558,21 @@ export function validateArtifactPublication(taskDir, family, name) {
439
558
  const error = assertWritableInventory(inventory);
440
559
  if (error)
441
560
  return error;
442
- if (inventory.next?.name === name || inventory.artifacts.some((artifact) => artifact.name === name))
561
+ if (inventory.next?.name === name)
443
562
  return null;
563
+ const existing = inventory.artifacts.find((artifact) => artifact.name === name);
564
+ if (existing) {
565
+ try {
566
+ const task = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
567
+ if (hasCompletedArtifactRound(task, family, existing.round, name)) {
568
+ return { code: 'ARTIFACT_IDENTITY_INVALID', message: `completed artifact '${name}' is immutable` };
569
+ }
570
+ }
571
+ catch (error) {
572
+ return { code: 'ARTIFACT_SELECTION_INDETERMINATE', message: error instanceof Error ? error.message : String(error) };
573
+ }
574
+ return null;
575
+ }
444
576
  return { code: 'ARTIFACT_IDENTITY_INVALID', message: `artifact '${name}' is not a current or next ${family} slot` };
445
577
  }
446
578
  function buildArtifactLinkSection(content, artifact) {
@@ -6,7 +6,6 @@ import { parseArtifactName } from './artifact-name.js';
6
6
  import { scanVisibleMarkdown } from './markdown.js';
7
7
  import { getArtifactSchema, renderArtifactSkeleton } from './artifact-schema.js';
8
8
  import { withTaskExecutionLock } from './task-execution-lock.js';
9
- import { buildQualificationAudit, expectedQualificationRelations, parseTaskQualification, renderQualificationAudit } from './qualification-audit.js';
10
9
  function sha256Content(content) {
11
10
  return createHash('sha256').update(content, 'utf8').digest('hex');
12
11
  }
@@ -167,10 +166,44 @@ function inspectArtifactPatterns(content, schema) {
167
166
  diagnostics
168
167
  };
169
168
  }
170
- function inspectArtifactContract(content, schema) {
169
+ function inspectArtifactStateCheck(content, schema, scopeToStateCheckSection) {
170
+ const stateCheck = schema.sections.find((section) => section.id === 'state-check');
171
+ if (!stateCheck || schema.stateCheckPatterns.length === 0) {
172
+ return { ok: true, family: schema.family, semanticDigest: canonicalSemanticDigest(content), diagnostics: [] };
173
+ }
174
+ if (!scopeToStateCheckSection) {
175
+ const diagnostics = schema.stateCheckPatterns
176
+ .filter((pattern) => !new RegExp(pattern, 'm').test(content))
177
+ .map((pattern) => diagnostic('ARTIFACT_STATE_CHECK_PATTERN_MISSING', `artifact is missing required state-check command '${pattern}'`, stateCheck.id, null));
178
+ return {
179
+ ok: diagnostics.length === 0,
180
+ family: schema.family,
181
+ semanticDigest: canonicalSemanticDigest(content),
182
+ diagnostics
183
+ };
184
+ }
185
+ const scanned = scanVisibleMarkdown(content);
186
+ const headings = scanned.headings.filter((heading) => (heading.level === 2 && sectionHeadings(schema, stateCheck).includes(heading.text)));
187
+ if (headings.length !== 1) {
188
+ return { ok: true, family: schema.family, semanticDigest: canonicalSemanticDigest(content), diagnostics: [] };
189
+ }
190
+ const bodyBounds = sectionBodyBounds(content, headings[0], scanned);
191
+ const body = content.slice(bodyBounds.start, bodyBounds.end);
192
+ const diagnostics = schema.stateCheckPatterns
193
+ .filter((pattern) => !new RegExp(pattern, 'm').test(body))
194
+ .map((pattern) => diagnostic('ARTIFACT_STATE_CHECK_PATTERN_MISSING', `state-check section is missing required command '${pattern}'`, stateCheck.id, lineNumber(content, headings[0].start)));
195
+ return {
196
+ ok: diagnostics.length === 0,
197
+ family: schema.family,
198
+ semanticDigest: canonicalSemanticDigest(content),
199
+ diagnostics
200
+ };
201
+ }
202
+ function inspectArtifactContract(content, schema, options = {}) {
171
203
  const structure = inspectArtifactStructure(content, schema);
204
+ const stateCheck = inspectArtifactStateCheck(content, schema, options.scopeStateCheckSection === true);
172
205
  const patterns = inspectArtifactPatterns(content, schema);
173
- const diagnostics = [...structure.diagnostics, ...patterns.diagnostics];
206
+ const diagnostics = [...structure.diagnostics, ...stateCheck.diagnostics, ...patterns.diagnostics];
174
207
  return {
175
208
  ok: diagnostics.length === 0,
176
209
  family: schema.family,
@@ -194,27 +227,6 @@ function artifactRound(family, artifact) {
194
227
  const identity = parseArtifactName(artifact);
195
228
  return identity?.family === family ? identity.round : null;
196
229
  }
197
- function qualificationAuditForInit(request) {
198
- let taskContent;
199
- try {
200
- taskContent = readStableFileSync(path.join(request.taskDir, 'task.md'), { maxBytes: 1024 * 1024 }).bytes.toString('utf8');
201
- }
202
- catch (error) {
203
- return { ok: false, code: 'ARTIFACT_INIT_TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error) };
204
- }
205
- const qualification = parseTaskQualification(taskContent);
206
- if (!qualification.ok)
207
- return qualification;
208
- if (!qualification.qualification.present)
209
- return { ok: true, content: '' };
210
- const relations = expectedQualificationRelations(taskContent, request.family);
211
- if (!relations.ok)
212
- return relations;
213
- const audit = buildQualificationAudit(taskContent, { upstreamRelations: relations.relations ?? [] });
214
- if (!audit.ok)
215
- return audit;
216
- return { ok: true, content: `\n## 资格审计\n\n${renderQualificationAudit(audit.audit)}\n` };
217
- }
218
230
  function initializeArtifactSkeleton(request) {
219
231
  const schema = getArtifactSchema(request.family);
220
232
  if (!schema)
@@ -244,10 +256,7 @@ function initializeArtifactSkeleton(request) {
244
256
  artifact: request.artifact,
245
257
  ...(request.locale ? { locale: request.locale } : {})
246
258
  });
247
- const audit = qualificationAuditForInit(request);
248
- if (!audit.ok)
249
- return resultFailure(audit.code, audit.message);
250
- const content = `${skeleton.trimEnd()}${audit.content}`;
259
+ const content = skeleton.trimEnd();
251
260
  const tempPath = path.join(request.taskDir, `.${request.artifact}.init-${process.pid}-${Date.now()}.tmp`);
252
261
  try {
253
262
  fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
@@ -36,7 +36,10 @@ function validateReceiptShape(event, output, input) {
36
36
  if (!outputIdentity || !inputIdentity)
37
37
  throw new ArtifactReceiptError(`receipt artifact identity is invalid: ${output} -> ${input}`);
38
38
  const shape = RECEIPT_SHAPES[event];
39
- if (outputIdentity.family !== shape.output || inputIdentity.family !== shape.input) {
39
+ const inputMatches = event === 'code.completed'
40
+ ? inputIdentity.family === 'analysis' || inputIdentity.family === 'plan'
41
+ : inputIdentity.family === shape.input;
42
+ if (outputIdentity.family !== shape.output || !inputMatches) {
40
43
  throw new ArtifactReceiptError(`receipt event '${event}' does not match ${output} -> ${input}`);
41
44
  }
42
45
  }
@@ -7,6 +7,7 @@ const LOCAL_SECTIONS = {
7
7
  ['impact', '影响评估', 'Impact Assessment'],
8
8
  ['risks', '技术风险', 'Technical Risks'],
9
9
  ['effort', '工作量和复杂度评估', 'Effort and Complexity Assessment'],
10
+ ['flow-decision', '流程裁定', 'Flow Decision'],
10
11
  ['state-check', '状态核对', 'State Check']
11
12
  ],
12
13
  plan: [
@@ -80,21 +81,21 @@ function buildSections(family) {
80
81
  marker: `artifact-section:${family}:${id}`
81
82
  }));
82
83
  }
84
+ const TASK_SNAPSHOT_PATTERN = 'agent-infra-internal[ \\t]+task-snapshot\\b';
83
85
  const ARTIFACT_SCHEMAS = [
84
- { family: 'analysis', title: { zh: '需求分析报告', en: 'Requirements Analysis' }, sections: buildSections('analysis'), requiredPatterns: ['^\\$ '] },
85
- { family: 'review-analysis', title: { zh: '需求分析审查报告', en: 'Analysis Review Report' }, sections: buildSections('review-analysis'), requiredPatterns: ['^### (?:审查决定|Approval Decision)$', '^\\$ '] },
86
- { family: 'plan', title: { zh: '技术方案', en: 'Technical Plan' }, sections: buildSections('plan'), requiredPatterns: ['^\\$ '] },
87
- { family: 'review-plan', title: { zh: '技术方案审查报告', en: 'Plan Review Report' }, sections: buildSections('review-plan'), requiredPatterns: ['^### (?:审查决定|Approval Decision)$', '^\\$ '] },
88
- { family: 'code', title: { zh: '实现报告', en: 'Implementation Report' }, sections: buildSections('code'), requiredPatterns: ['^\\$ '] },
86
+ { family: 'analysis', title: { zh: '需求分析报告', en: 'Requirements Analysis' }, sections: buildSections('analysis'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
87
+ { family: 'review-analysis', title: { zh: '需求分析审查报告', en: 'Analysis Review Report' }, sections: buildSections('review-analysis'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: ['^### (?:审查决定|Approval Decision)$'] },
88
+ { family: 'plan', title: { zh: '技术方案', en: 'Technical Plan' }, sections: buildSections('plan'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
89
+ { family: 'review-plan', title: { zh: '技术方案审查报告', en: 'Plan Review Report' }, sections: buildSections('review-plan'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: ['^### (?:审查决定|Approval Decision)$'] },
90
+ { family: 'code', title: { zh: '实现报告', en: 'Implementation Report' }, sections: buildSections('code'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
89
91
  { family: 'review-code', title: { zh: '代码审查报告', en: 'Code Review Report' }, sections: buildSections('review-code'), requiredPatterns: [
90
92
  '^### (?:审查决定|Approval Decision)$',
91
- '^\\$ ',
92
93
  '^- \\*\\*(?:总体结论|Overall Verdict)\\*\\*[::]\\s*(?:通过|需要修改|拒绝|Approved|Changes Requested|Rejected)\\s*$',
93
94
  '^- \\*\\*(?:审查基线提交|Review Baseline Commit)\\*\\*[::]\\s*\\S',
94
95
  '^- \\*\\*(?:审查差异基线|Reviewed Diff Base)\\*\\*[::]\\s*[0-9a-f]{40,64}\\s*$',
95
96
  '^- \\*\\*(?:审查差异指纹|Reviewed Diff Fingerprint)\\*\\*[::]\\s*sha256:[0-9a-f]{64}\\s*$',
96
97
  '^- \\*\\*(?:审查快照树|Reviewed Snapshot Tree)\\*\\*[::]\\s*[0-9a-f]{40,64}\\s*$'
97
- ] }
98
+ ], stateCheckPatterns: [TASK_SNAPSHOT_PATTERN] }
98
99
  ];
99
100
  function getArtifactSchema(family) {
100
101
  return ARTIFACT_SCHEMAS.find((schema) => schema.family === family) ?? null;
@@ -0,0 +1,25 @@
1
+ function selectArtifactDisposition(request) {
2
+ if (request.open) {
3
+ return {
4
+ disposition: 'resume', reasonCode: 'open-round', artifact: request.next,
5
+ writeRequired: true
6
+ };
7
+ }
8
+ if (!request.latest) {
9
+ return {
10
+ disposition: 'create',
11
+ reasonCode: request.next.round === 1 ? 'no-history' : 'invalidated-history',
12
+ artifact: request.next, writeRequired: true
13
+ };
14
+ }
15
+ const reasonCode = request.hasChangeEvidence ? 'change-evidence' : 'history-matched';
16
+ const reuse = reasonCode === 'history-matched';
17
+ return {
18
+ disposition: reuse ? 'reuse' : 'create',
19
+ reasonCode,
20
+ artifact: reuse ? request.latest : request.next,
21
+ writeRequired: !reuse
22
+ };
23
+ }
24
+ export { selectArtifactDisposition };
25
+ //# sourceMappingURL=artifact-selection.js.map