@fieldwangai/agentflow 0.1.124 → 0.1.126

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.
@@ -8141,7 +8141,7 @@ function prdWorkflowReviewRenderFrontmatter(frontmatter) {
8141
8141
  }
8142
8142
  }
8143
8143
  flush();
8144
- return `<details class="frontmatter" open><summary>文档元数据</summary><table>${rows}</table></details>`;
8144
+ return `<details class="frontmatter"><summary>文档元数据</summary><table><colgroup><col class="frontmatter-key-column"><col></colgroup>${rows}</table></details>`;
8145
8145
  }
8146
8146
 
8147
8147
  function prdWorkflowReviewRenderTable(lines) {
@@ -8386,7 +8386,8 @@ function prdWorkflowReviewParseChangeIntent(lines, startIndex) {
8386
8386
  base: "",
8387
8387
  insertNear: "",
8388
8388
  destination: "",
8389
- reference: null,
8389
+ references: [],
8390
+ annotations: [],
8390
8391
  proposals: [],
8391
8392
  };
8392
8393
  for (let i = startIndex + 1; i < lines.length; i += 1) {
@@ -8415,11 +8416,31 @@ function prdWorkflowReviewParseChangeIntent(lines, startIndex) {
8415
8416
  end += 1;
8416
8417
  }
8417
8418
  if (end >= lines.length) return null;
8418
- change.reference = {
8419
+ change.references.push({
8419
8420
  startLine: Number(reference[1]),
8420
8421
  endLine: Number(reference[2] || reference[1]),
8421
8422
  lines: body,
8422
- };
8423
+ });
8424
+ i = end;
8425
+ continue;
8426
+ }
8427
+ const annotation = trimmed.match(
8428
+ /^#annotation\s+line\s*(\d+)(?:\s*-\s*(\d+))?\s+(problem|change|preserve)\s*$/i,
8429
+ );
8430
+ if (annotation) {
8431
+ const body = [];
8432
+ let end = i + 1;
8433
+ while (end < lines.length && !/^#annotationend\s*$/i.test(String(lines[end] || "").trim())) {
8434
+ body.push(lines[end]);
8435
+ end += 1;
8436
+ }
8437
+ if (end >= lines.length) return null;
8438
+ change.annotations.push({
8439
+ startLine: Number(annotation[1]),
8440
+ endLine: Number(annotation[2] || annotation[1]),
8441
+ type: annotation[3].toLowerCase(),
8442
+ lines: body,
8443
+ });
8423
8444
  i = end;
8424
8445
  continue;
8425
8446
  }
@@ -8461,6 +8482,11 @@ function prdWorkflowReviewRenderChangeIntent(change) {
8461
8482
  pseudocode: "方案伪代码",
8462
8483
  code: "拟议代码 · 未写入",
8463
8484
  };
8485
+ const annotationLabels = {
8486
+ problem: "当前问题",
8487
+ change: "计划修改",
8488
+ preserve: "保持不变",
8489
+ };
8464
8490
  const operation = operationLabels[change.operation] || "变更";
8465
8491
  const target = targetLabels[change.target] || "目标";
8466
8492
  const locator = change.module || change.file || "未定位";
@@ -8475,24 +8501,66 @@ function prdWorkflowReviewRenderChangeIntent(change) {
8475
8501
  safeDestination ? `<span><strong>移动到</strong> <code>${safeDestination}</code></span>` : "",
8476
8502
  ].filter(Boolean).join("");
8477
8503
 
8504
+ const references = Array.isArray(change.references)
8505
+ ? change.references
8506
+ : (change.reference ? [change.reference] : []);
8507
+ const annotations = Array.isArray(change.annotations) ? change.annotations : [];
8508
+ const sourceLanguage = prdWorkflowReviewCodeLanguage(change.file || change.module);
8509
+ const sourceLineLabel = (item) => `L${item.startLine}${
8510
+ item.endLine !== item.startLine
8511
+ ? `–L${item.endLine}`
8512
+ : ""
8513
+ }`;
8478
8514
  let referenceHtml = "";
8479
- if (change.reference) {
8480
- const normalized = prdWorkflowReviewDedentPlannedCode(change.reference.lines);
8481
- const highlighted = prdWorkflowReviewHighlightCodeLines(normalized, change.file || change.module);
8482
- const lineLabel = `L${change.reference.startLine}${
8483
- change.reference.endLine !== change.reference.startLine
8484
- ? `–L${change.reference.endLine}`
8485
- : ""
8486
- }`;
8487
- const rows = highlighted.lines.map((line, index) => (
8488
- `<span class="change-intent__source-line">`
8489
- + `<span class="change-intent__source-number">${change.reference.startLine + index}</span>`
8490
- + `<span class="change-intent__source-text">${line}</span>`
8491
- + "</span>"
8492
- )).join("");
8493
- referenceHtml = `<details class="change-intent__context" data-language="${highlighted.language}" open>
8494
- <summary><span>当前上下文</span><span class="change-intent__line-anchor">${lineLabel}</span></summary>
8495
- <pre class="change-intent__source"><code>${rows}</code></pre>
8515
+ if (references.length) {
8516
+ const totalLines = references.reduce(
8517
+ (total, reference) => total + Math.max(0, reference.endLine - reference.startLine + 1),
8518
+ 0,
8519
+ );
8520
+ const hunks = references.map((reference, referenceIndex) => {
8521
+ const normalized = prdWorkflowReviewDedentPlannedCode(reference.lines);
8522
+ const highlighted = prdWorkflowReviewHighlightCodeLines(normalized, change.file || change.module);
8523
+ const rows = highlighted.lines.map((line, index) => (
8524
+ `<span class="change-intent__source-line">`
8525
+ + `<span class="change-intent__source-number">${reference.startLine + index}</span>`
8526
+ + `<span class="change-intent__source-text">${line}</span>`
8527
+ + "</span>"
8528
+ )).join("");
8529
+ const annotationHtml = annotations
8530
+ .filter((annotation) => (
8531
+ reference.startLine <= annotation.startLine
8532
+ && annotation.endLine <= reference.endLine
8533
+ ))
8534
+ .map((annotation) => {
8535
+ const type = annotation.type || "change";
8536
+ const label = annotationLabels[type] || "Review 指引";
8537
+ const body = prdWorkflowReviewMarkdownLinesToHtml(
8538
+ prdWorkflowReviewDedentPlannedCode(annotation.lines),
8539
+ );
8540
+ return `<aside class="change-intent__annotation is-${htmlEscapeAttribute(type)}">
8541
+ <div class="change-intent__annotation-header">
8542
+ <span class="change-intent__annotation-title">Review 指引</span>
8543
+ <span class="change-intent__annotation-badge">${htmlEscapeAttribute(label)}</span>
8544
+ <span class="change-intent__annotation-anchor">${htmlEscapeAttribute(sourceLineLabel(annotation))}</span>
8545
+ </div>
8546
+ <div class="change-intent__annotation-body">${body}</div>
8547
+ </aside>`;
8548
+ }).join("");
8549
+ return `<section class="change-intent__hunk">
8550
+ <div class="change-intent__hunk-header">
8551
+ <span>片段 ${referenceIndex + 1}</span>
8552
+ <span class="change-intent__line-anchor">${htmlEscapeAttribute(sourceLineLabel(reference))}</span>
8553
+ </div>
8554
+ <pre class="change-intent__source" data-language="${highlighted.language}"><code>${rows}</code></pre>
8555
+ ${annotationHtml}
8556
+ </section>`;
8557
+ }).join("");
8558
+ referenceHtml = `<details class="change-intent__context" data-language="${sourceLanguage}" open>
8559
+ <summary>
8560
+ <span>当前上下文</span>
8561
+ <span class="change-intent__context-stats">${references.length} 个片段 · ${totalLines} 行</span>
8562
+ </summary>
8563
+ <div class="change-intent__hunks">${hunks}</div>
8496
8564
  </details>`;
8497
8565
  }
8498
8566
 
@@ -8546,7 +8614,7 @@ function prdWorkflowReviewRenderChangeIntent(change) {
8546
8614
  function prdWorkflowReviewMarkdownLinesToHtml(lines) {
8547
8615
  const html = [];
8548
8616
  let paragraph = [];
8549
- let list = [];
8617
+ let list = null;
8550
8618
  let code = null;
8551
8619
  const flushParagraph = () => {
8552
8620
  if (!paragraph.length) return;
@@ -8554,9 +8622,11 @@ function prdWorkflowReviewMarkdownLinesToHtml(lines) {
8554
8622
  paragraph = [];
8555
8623
  };
8556
8624
  const flushList = () => {
8557
- if (!list.length) return;
8558
- html.push(`<ul>${list.map((item) => `<li>${item}</li>`).join("")}</ul>`);
8559
- list = [];
8625
+ if (!list?.items?.length) return;
8626
+ const tag = list.ordered ? "ol" : "ul";
8627
+ const start = list.ordered && list.start !== 1 ? ` start="${list.start}"` : "";
8628
+ html.push(`<${tag}${start}>${list.items.map((item) => `<li>${item}</li>`).join("")}</${tag}>`);
8629
+ list = null;
8560
8630
  };
8561
8631
  const flushBlocks = () => {
8562
8632
  flushParagraph();
@@ -8645,13 +8715,31 @@ function prdWorkflowReviewMarkdownLinesToHtml(lines) {
8645
8715
  html.push(`<blockquote>${prdWorkflowReviewInlineMarkdown(quote[1])}</blockquote>`);
8646
8716
  continue;
8647
8717
  }
8648
- const bullet = trimmed.match(/^[-*]\s+(?:\[( |x|X)\]\s+)?(.+)$/);
8649
- if (bullet) {
8718
+ const listItem = line.match(/^(\s*)([-*]|\d+[.)])\s+(?:\[( |x|X)\]\s+)?(.+)$/);
8719
+ if (listItem) {
8650
8720
  flushParagraph();
8651
- const checked = bullet[1] ? `<input type="checkbox" disabled${bullet[1].toLowerCase() === "x" ? " checked" : ""}> ` : "";
8652
- list.push(`${checked}${prdWorkflowReviewInlineMarkdown(bullet[2])}`);
8721
+ const ordered = /^\d/.test(listItem[2]);
8722
+ if (list && list.ordered !== ordered) flushList();
8723
+ if (!list) {
8724
+ list = {
8725
+ ordered,
8726
+ start: ordered ? Number.parseInt(listItem[2], 10) || 1 : 1,
8727
+ indent: listItem[1].length,
8728
+ items: [],
8729
+ };
8730
+ }
8731
+ const checked = listItem[3]
8732
+ ? `<input type="checkbox" disabled${listItem[3].toLowerCase() === "x" ? " checked" : ""}> `
8733
+ : "";
8734
+ list.items.push(`${checked}${prdWorkflowReviewInlineMarkdown(listItem[4])}`);
8735
+ continue;
8736
+ }
8737
+ if (list?.items?.length && /^\s{2,}\S/.test(line)) {
8738
+ const lastIndex = list.items.length - 1;
8739
+ list.items[lastIndex] += ` ${prdWorkflowReviewInlineMarkdown(trimmed)}`;
8653
8740
  continue;
8654
8741
  }
8742
+ if (list) flushList();
8655
8743
  paragraph.push(trimmed);
8656
8744
  }
8657
8745
  if (code) html.push(`<pre><code>${htmlEscapeAttribute(prdWorkflowReviewNormalizeText(code.lines.join("\n")))}</code></pre>`);
@@ -8947,18 +9035,20 @@ export function prdWorkflowReviewHtml(title, markdown, meta = {}) {
8947
9035
  .raw:hover, .theme-toggle:hover { border-color: var(--interactive); color: var(--link); }
8948
9036
  .theme-toggle { cursor: pointer; font-family: inherit; }
8949
9037
  .lifecycle { margin-top: 10px; display: inline-flex; max-width: 100%; border: 1px solid var(--border); border-radius: 999px; background: var(--button); color: var(--muted); padding: 6px 10px; font-size: 12px; font-weight: 800; line-height: 1.35; overflow-wrap: anywhere; }
8950
- article { min-width: 0; border: 1px solid var(--border); border-radius: 14px; background: var(--panel); box-shadow: 0 18px 50px var(--shadow); padding: clamp(20px, 4vw, 34px); }
9038
+ article { min-width: 0; border: 0; border-radius: 14px; background: var(--panel); box-shadow: 0 18px 50px var(--shadow); padding: clamp(20px, 4vw, 34px); }
8951
9039
  article > *:first-child { margin-top: 0; }
8952
9040
  article > *:last-child { margin-bottom: 0; }
8953
9041
  h2, h3, h4, h5, h6 { margin: 1.7em 0 .65em; line-height: 1.25; letter-spacing: 0; color: var(--heading); overflow-wrap: anywhere; }
8954
9042
  h2 { padding-bottom: .4rem; border-bottom: 1px solid var(--border-soft); font-size: 1.5rem; }
8955
9043
  h3 { font-size: 1.2rem; }
8956
- p, li, td, th, blockquote { font-size: 15px; line-height: 1.8; overflow-wrap: anywhere; word-break: break-word; }
9044
+ p, li, td, th, blockquote { font-size: 15px; line-height: 1.75; overflow-wrap: anywhere; word-break: break-word; }
8957
9045
  p { margin: .75rem 0; color: var(--body); }
8958
- ul { margin: .65rem 0 1rem; padding-left: 1.35rem; }
8959
- li { margin: .28rem 0; color: var(--body); }
9046
+ ul, ol { margin: .75rem 0 1.1rem; padding-left: 1.55rem; }
9047
+ ol { padding-left: 1.8rem; }
9048
+ li { margin: .48rem 0; padding-left: .12rem; color: var(--body); }
9049
+ li::marker { color: var(--interactive); font-weight: 750; }
8960
9050
  li input { margin-right: .38rem; transform: translateY(1px); }
8961
- code { display: inline; max-width: 100%; border: 1px solid var(--border-soft); border-radius: 6px; background: var(--code-bg); color: var(--code-inline); padding: .1rem .34rem; font-family: "SFMono-Regular", Consolas, monospace; font-size: .92em; white-space: normal; overflow-wrap: anywhere; word-break: break-word; }
9051
+ code { display: inline; max-width: 100%; border: 1px solid var(--border-soft); border-radius: 4px; background: color-mix(in srgb, var(--interactive) 8%, transparent); color: var(--code-inline); padding: .06rem .24rem; font-family: "SFMono-Regular", Consolas, monospace; font-size: .9em; white-space: normal; overflow-wrap: anywhere; word-break: break-word; }
8962
9052
  pre { max-width: 100%; overflow: auto; border: 1px solid var(--border); border-radius: 10px; background: var(--code-block); padding: 16px; line-height: 1.65; }
8963
9053
  pre code { border: 0; background: transparent; color: var(--code-block-text); padding: 0; white-space: pre; overflow-wrap: normal; word-break: normal; }
8964
9054
  .planned-code { max-width: 100%; margin: 1rem 0 1.2rem; overflow: hidden; border: 1px solid var(--planned-border); border-radius: 10px; background: var(--code-block); }
@@ -8986,13 +9076,26 @@ export function prdWorkflowReviewHtml(title, markdown, meta = {}) {
8986
9076
  .change-intent__meta code { border: 0; background: transparent; padding: 0; color: var(--muted); }
8987
9077
  .change-intent__context { border-bottom: 1px solid var(--border-soft); background: var(--change-context); }
8988
9078
  .change-intent__context summary { display: flex; align-items: center; justify-content: space-between; gap: 10px; cursor: pointer; padding: 10px 14px; color: var(--body); font-size: 13px; font-weight: 900; list-style-position: inside; }
8989
- .change-intent__line-anchor, .change-intent__proposal-badge { margin-left: auto; border: 1px solid var(--border); border-radius: 999px; padding: 3px 8px; color: var(--muted); font: 800 11px/1.35 "SFMono-Regular", Consolas, monospace; white-space: nowrap; }
9079
+ .change-intent__context-stats, .change-intent__line-anchor, .change-intent__proposal-badge, .change-intent__annotation-anchor { margin-left: auto; border: 1px solid var(--border); border-radius: 999px; padding: 3px 8px; color: var(--muted); font: 800 11px/1.35 "SFMono-Regular", Consolas, monospace; white-space: nowrap; }
9080
+ .change-intent__hunk + .change-intent__hunk { border-top: 8px solid var(--panel-strong); }
9081
+ .change-intent__hunk-header { display: flex; align-items: center; gap: 10px; border-top: 1px solid var(--border-soft); padding: 8px 14px; color: var(--muted); font-size: 12px; font-weight: 800; }
8990
9082
  .change-intent__source { margin: 0; border: 0; border-top: 1px solid var(--border-soft); border-radius: 0; padding: 10px 0; background: var(--code-block); font: 500 13px/1.55 "SFMono-Regular", "JetBrains Mono", Consolas, monospace; }
8991
9083
  .change-intent__source code, .change-intent__proposal-body code { display: block; font: inherit; }
8992
9084
  .change-intent__source-line { display: grid; grid-template-columns: 3.75rem minmax(max-content, 1fr); min-height: 1.55em; }
8993
9085
  .change-intent__source-line:hover { background: var(--planned-line); }
8994
9086
  .change-intent__source-number { border-right: 1px solid var(--border-soft); color: var(--planned-gutter); padding: 0 .8rem 0 .5rem; text-align: right; user-select: none; }
8995
9087
  .change-intent__source-text { padding: 0 1rem; white-space: pre; }
9088
+ .change-intent__annotation { border-top: 1px solid var(--border-soft); border-left: 3px solid var(--change-modify); background: var(--change-proposal); padding: 10px 14px 11px; }
9089
+ .change-intent__annotation.is-problem { border-left-color: var(--change-remove); }
9090
+ .change-intent__annotation.is-preserve { border-left-color: var(--change-add); }
9091
+ .change-intent__annotation-header { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; color: var(--body); }
9092
+ .change-intent__annotation-title { font-size: 12px; font-weight: 900; }
9093
+ .change-intent__annotation-badge { border: 1px solid currentColor; border-radius: 999px; color: var(--change-modify); padding: 3px 8px; font-size: 11px; font-weight: 900; line-height: 1.3; }
9094
+ .change-intent__annotation.is-problem .change-intent__annotation-badge { color: var(--change-remove); }
9095
+ .change-intent__annotation.is-preserve .change-intent__annotation-badge { color: var(--change-add); }
9096
+ .change-intent__annotation-body { margin-top: 7px; }
9097
+ .change-intent__annotation-body > *:first-child { margin-top: 0; }
9098
+ .change-intent__annotation-body > *:last-child { margin-bottom: 0; }
8996
9099
  .change-intent__proposal { background: var(--change-proposal); }
8997
9100
  .change-intent__proposal + .change-intent__proposal { border-top: 1px solid var(--border-soft); }
8998
9101
  .change-intent__proposal-header { display: flex; align-items: center; gap: 10px; padding: 10px 14px; color: var(--heading); font-size: 13px; font-weight: 900; }
@@ -9027,7 +9130,8 @@ export function prdWorkflowReviewHtml(title, markdown, meta = {}) {
9027
9130
  .frontmatter { margin: 0 0 1.35rem; border: 1px solid var(--border-soft); border-radius: 10px; background: var(--panel-strong); padding: .75rem .9rem; }
9028
9131
  .frontmatter summary { cursor: pointer; color: var(--body); font-weight: 800; }
9029
9132
  .frontmatter table { min-width: 0; margin-top: .7rem; }
9030
- .frontmatter th { width: min(34%, 12rem); background: var(--panel-soft); color: var(--body); }
9133
+ .frontmatter-key-column { width: clamp(10rem, 22%, 16rem); }
9134
+ .frontmatter th { background: var(--panel-soft); color: var(--body); }
9031
9135
  .frontmatter-list { margin: 0; padding-left: 1.1rem; }
9032
9136
  .action-index { position: sticky; top: 10px; z-index: 4; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 1rem 0 1.25rem; border: 1px solid var(--border); border-radius: 12px; background: var(--panel-strong); box-shadow: 0 8px 22px var(--shadow); padding: 10px 12px; }
9033
9137
  .action-index__label { margin-right: 2px; color: var(--muted); font-size: 12px; font-weight: 800; }
@@ -9210,6 +9314,50 @@ function prdWorkflowAppendAudit(scopedRoot, tapdId, event = {}) {
9210
9314
  } catch (_) {}
9211
9315
  }
9212
9316
 
9317
+ function prdWorkflowReadAuditEntries(scopedRoot, tapdId, limit = 500) {
9318
+ try {
9319
+ const p = prdWorkflowAuditPath(scopedRoot, tapdId);
9320
+ if (!fs.existsSync(p)) return [];
9321
+ return fs.readFileSync(p, "utf-8")
9322
+ .split(/\r?\n/)
9323
+ .filter(Boolean)
9324
+ .slice(-Math.max(1, Number(limit) || 500))
9325
+ .map((line) => {
9326
+ try {
9327
+ return JSON.parse(line);
9328
+ } catch {
9329
+ return null;
9330
+ }
9331
+ })
9332
+ .filter(Boolean);
9333
+ } catch {
9334
+ return [];
9335
+ }
9336
+ }
9337
+
9338
+ function prdWorkflowReadRecentActionAudit(scopedRoot, tapdId, limit = 24) {
9339
+ return prdWorkflowReadAuditEntries(scopedRoot, tapdId, 500)
9340
+ .filter((item) => item?.type === "snapshot-action-change")
9341
+ .slice(-Math.max(1, Number(limit) || 24));
9342
+ }
9343
+
9344
+ function prdWorkflowFirstPointerObservation(scopedRoot, tapdId, snapshot = {}) {
9345
+ const phase = String(snapshot?.phase || "").trim();
9346
+ const pointer = String(snapshot?.pointer || "").trim();
9347
+ if (!phase && !pointer) return "";
9348
+ let earliest = "";
9349
+ for (const entry of prdWorkflowReadAuditEntries(scopedRoot, tapdId, 5000)) {
9350
+ if (entry?.type !== "client-observation-stored") continue;
9351
+ if (phase && String(entry.phase || "").trim() !== phase) continue;
9352
+ if (pointer && String(entry.pointer || "").trim() !== pointer) continue;
9353
+ const candidate = String(entry.observedAt || entry.reportedAt || entry.at || "").trim();
9354
+ const candidateTime = Date.parse(candidate);
9355
+ if (!Number.isFinite(candidateTime)) continue;
9356
+ if (!earliest || candidateTime < Date.parse(earliest)) earliest = candidate;
9357
+ }
9358
+ return earliest;
9359
+ }
9360
+
9213
9361
  function prdWorkflowReadProjectState(scopedRoot, tapdId) {
9214
9362
  return prdWorkflowReadJsonFile(prdWorkflowProjectPath(scopedRoot, tapdId), {
9215
9363
  version: 1,
@@ -9302,6 +9450,160 @@ function prdWorkflowWriteClientObservation(scopedRoot, tapdId, meta, snapshot) {
9302
9450
  });
9303
9451
  }
9304
9452
 
9453
+ const PRD_WORKFLOW_SNAPSHOT_ACTION_ARRAY_KEYS = [
9454
+ "actions",
9455
+ "workflowActions",
9456
+ "workflow_actions",
9457
+ "timeline",
9458
+ "history",
9459
+ ];
9460
+
9461
+ function prdWorkflowSnapshotActionKey(action = {}) {
9462
+ const stageKey = String(
9463
+ action.stageKey ||
9464
+ action.stage_key ||
9465
+ action.stage ||
9466
+ action.actionId ||
9467
+ action.action_id ||
9468
+ action.action ||
9469
+ action.id ||
9470
+ "",
9471
+ ).trim();
9472
+ const issueKey = String(action.issueKey || action.issue_key || action.issue || "").trim();
9473
+ const platform = String(action.platform || "").trim().toLowerCase();
9474
+ return [stageKey, issueKey, platform].filter(Boolean).join("|");
9475
+ }
9476
+
9477
+ function prdWorkflowSnapshotActionTime(action = {}) {
9478
+ return String(
9479
+ action.stageEnteredAt ||
9480
+ action.stage_entered_at ||
9481
+ action.time ||
9482
+ action.at ||
9483
+ action.observedAt ||
9484
+ action.observed_at ||
9485
+ action.startedAt ||
9486
+ action.started_at ||
9487
+ action.completedAt ||
9488
+ action.completed_at ||
9489
+ action.updatedAt ||
9490
+ action.updated_at ||
9491
+ action.createdAt ||
9492
+ action.created_at ||
9493
+ "",
9494
+ ).trim();
9495
+ }
9496
+
9497
+ function prdWorkflowSnapshotSourceActionTime(action = {}) {
9498
+ return String(
9499
+ action.time ||
9500
+ action.at ||
9501
+ action.observedAt ||
9502
+ action.observed_at ||
9503
+ action.startedAt ||
9504
+ action.started_at ||
9505
+ action.completedAt ||
9506
+ action.completed_at ||
9507
+ action.updatedAt ||
9508
+ action.updated_at ||
9509
+ action.createdAt ||
9510
+ action.created_at ||
9511
+ "",
9512
+ ).trim();
9513
+ }
9514
+
9515
+ function prdWorkflowSnapshotActionMap(snapshot = {}) {
9516
+ const out = new Map();
9517
+ for (const key of PRD_WORKFLOW_SNAPSHOT_ACTION_ARRAY_KEYS) {
9518
+ const rows = Array.isArray(snapshot?.[key]) ? snapshot[key] : [];
9519
+ for (const action of rows) {
9520
+ if (!action || typeof action !== "object" || Array.isArray(action)) continue;
9521
+ const actionKey = prdWorkflowSnapshotActionKey(action);
9522
+ if (actionKey && !out.has(actionKey)) out.set(actionKey, action);
9523
+ }
9524
+ }
9525
+ return out;
9526
+ }
9527
+
9528
+ function prdWorkflowSnapshotActionChanges(previousSnapshot = {}, nextSnapshot = {}) {
9529
+ const previous = prdWorkflowSnapshotActionMap(previousSnapshot);
9530
+ const next = prdWorkflowSnapshotActionMap(nextSnapshot);
9531
+ const changes = [];
9532
+ const compact = (kind, action, previousAction = null) => ({
9533
+ kind,
9534
+ stageKey: String(action?.stageKey || action?.stage_key || action?.stage || action?.id || "").trim(),
9535
+ issueKey: String(action?.issueKey || action?.issue_key || action?.issue || "").trim(),
9536
+ platform: String(action?.platform || "").trim(),
9537
+ title: String(action?.title || action?.label || action?.name || "").trim(),
9538
+ status: String(action?.status || "").trim(),
9539
+ previousStatus: String(previousAction?.status || "").trim(),
9540
+ actionAt: prdWorkflowSnapshotActionTime(action),
9541
+ previousActionAt: prdWorkflowSnapshotActionTime(previousAction || {}),
9542
+ sourceActionAt: prdWorkflowSnapshotSourceActionTime(action),
9543
+ previousSourceActionAt: prdWorkflowSnapshotSourceActionTime(previousAction || {}),
9544
+ });
9545
+ for (const [key, action] of next) {
9546
+ const previousAction = previous.get(key);
9547
+ if (!previousAction) {
9548
+ changes.push(compact("added", action));
9549
+ continue;
9550
+ }
9551
+ const statusChanged = String(previousAction.status || "") !== String(action.status || "");
9552
+ const timeChanged =
9553
+ prdWorkflowSnapshotActionTime(previousAction) !== prdWorkflowSnapshotActionTime(action) ||
9554
+ prdWorkflowSnapshotSourceActionTime(previousAction) !== prdWorkflowSnapshotSourceActionTime(action);
9555
+ const titleChanged = String(previousAction.title || previousAction.label || "") !== String(action.title || action.label || "");
9556
+ if (statusChanged || timeChanged || titleChanged) {
9557
+ changes.push(compact(
9558
+ statusChanged ? "status-changed" : timeChanged ? "time-changed" : "title-changed",
9559
+ action,
9560
+ previousAction,
9561
+ ));
9562
+ }
9563
+ }
9564
+ for (const [key, action] of previous) {
9565
+ if (!next.has(key)) changes.push(compact("removed", action, action));
9566
+ }
9567
+ return changes.slice(0, 80);
9568
+ }
9569
+
9570
+ function prdWorkflowStampCurrentActionEntryTimes(scopedRoot, tapdId, snapshot = {}, clientState = {}, meta = {}) {
9571
+ const existingTimes = new Map();
9572
+ for (const client of Object.values(clientState?.clients || {})) {
9573
+ const observedAt = String(client?.observedAt || client?.reportedAt || "").trim();
9574
+ for (const [key, action] of prdWorkflowSnapshotActionMap(client?.snapshot || {})) {
9575
+ if (String(action?.status || "").trim().toLowerCase() !== "current") continue;
9576
+ const value = String(action.stageEnteredAt || action.stage_entered_at || observedAt).trim();
9577
+ if (!value || !Number.isFinite(Date.parse(value))) continue;
9578
+ const previous = existingTimes.get(key);
9579
+ if (!previous || Date.parse(value) < Date.parse(previous)) existingTimes.set(key, value);
9580
+ }
9581
+ }
9582
+ const auditedAt = prdWorkflowFirstPointerObservation(scopedRoot, tapdId, snapshot);
9583
+ const observedAt = String(meta.observedAt || meta.reportedAt || new Date().toISOString()).trim();
9584
+ const stamp = (action) => {
9585
+ if (!action || typeof action !== "object" || Array.isArray(action)) return action;
9586
+ if (String(action.status || "").trim().toLowerCase() !== "current") return action;
9587
+ const actionKey = prdWorkflowSnapshotActionKey(action);
9588
+ const candidates = [
9589
+ String(action.stageEnteredAt || action.stage_entered_at || "").trim(),
9590
+ existingTimes.get(actionKey) || "",
9591
+ auditedAt,
9592
+ observedAt,
9593
+ ].filter((value) => Number.isFinite(Date.parse(value)));
9594
+ const stageEnteredAt = candidates.sort((left, right) => Date.parse(left) - Date.parse(right))[0] || observedAt;
9595
+ return {
9596
+ ...action,
9597
+ stageEnteredAt,
9598
+ };
9599
+ };
9600
+ const next = { ...snapshot };
9601
+ for (const key of PRD_WORKFLOW_SNAPSHOT_ACTION_ARRAY_KEYS) {
9602
+ if (Array.isArray(snapshot?.[key])) next[key] = snapshot[key].map(stamp);
9603
+ }
9604
+ return next;
9605
+ }
9606
+
9305
9607
  const PRD_WORKFLOW_PROJECTION_SOURCE_KEYS = new Set([
9306
9608
  "projectionMode",
9307
9609
  "projectCacheScope",
@@ -9331,6 +9633,7 @@ function prdWorkflowStoredObservationSnapshot(snapshot, sourcePatch = {}) {
9331
9633
  delete clean.collaboration;
9332
9634
  delete clean.clientObservations;
9333
9635
  delete clean.clients;
9636
+ delete clean.snapshotAudit;
9334
9637
  clean.sources = prdWorkflowStoredObservationSources(snapshot.sources, sourcePatch);
9335
9638
  return clean;
9336
9639
  }
@@ -9561,6 +9864,7 @@ function prdWorkflowMaterializeSnapshot(root, scopedRoot, tapdId, userCtx = {},
9561
9864
  revision: String(materialized.revision || ""),
9562
9865
  },
9563
9866
  ];
9867
+ materialized.snapshotAudit = prdWorkflowReadRecentActionAudit(scopedRoot, tapdId);
9564
9868
  prdWorkflowAppendAudit(scopedRoot, tapdId, {
9565
9869
  type: "projection-materialized",
9566
9870
  flowSource,
@@ -12179,7 +12483,21 @@ export function startUiServer({
12179
12483
  issueKey: reportMeta.issueKey,
12180
12484
  stageKey: reportMeta.stageKey,
12181
12485
  };
12182
- const storedObservationSnapshot = prdWorkflowStoredObservationSnapshot(normalizedSnapshot, reportSource);
12486
+ const existingClientState = prdWorkflowReadClientState(scopedRoot, tapdId);
12487
+ const existingClientId = prdWorkflowSafeStateId(reportMeta.clientId || "anonymous");
12488
+ const previousClientSnapshot = existingClientState.clients?.[existingClientId]?.snapshot || null;
12489
+ const stampedSnapshot = prdWorkflowStampCurrentActionEntryTimes(
12490
+ scopedRoot,
12491
+ tapdId,
12492
+ normalizedSnapshot,
12493
+ existingClientState,
12494
+ reportMeta,
12495
+ );
12496
+ const storedObservationSnapshot = prdWorkflowStoredObservationSnapshot(stampedSnapshot, reportSource);
12497
+ const actionChanges = prdWorkflowSnapshotActionChanges(
12498
+ previousClientSnapshot || {},
12499
+ storedObservationSnapshot,
12500
+ );
12183
12501
  prdWorkflowWriteClientObservation(scopedRoot, tapdId, reportMeta, storedObservationSnapshot);
12184
12502
  prdWorkflowAppendAudit(scopedRoot, tapdId, {
12185
12503
  type: "client-observation-stored",
@@ -12198,12 +12516,51 @@ export function startUiServer({
12198
12516
  persistence: "runtime",
12199
12517
  note: "ordinary current snapshot stored as client observation; it must not overwrite project state",
12200
12518
  });
12519
+ for (const change of actionChanges) {
12520
+ const changeLabel = {
12521
+ added: "新增",
12522
+ removed: "移除",
12523
+ "status-changed": "状态变更",
12524
+ "time-changed": "时间更正",
12525
+ "title-changed": "标题变更",
12526
+ }[change.kind] || "变更";
12527
+ prdWorkflowAppendAudit(scopedRoot, tapdId, {
12528
+ type: "snapshot-action-change",
12529
+ change: change.kind,
12530
+ title: `Workflow Action ${changeLabel}${change.title ? `:${change.title}` : ""}`,
12531
+ detail: [
12532
+ change.stageKey,
12533
+ change.previousStatus && change.previousStatus !== change.status
12534
+ ? `${change.previousStatus} -> ${change.status}`
12535
+ : change.status,
12536
+ change.previousActionAt && change.previousActionAt !== change.actionAt
12537
+ ? `${change.previousActionAt} -> ${change.actionAt || "无时间"}`
12538
+ : change.actionAt,
12539
+ change.previousSourceActionAt !== change.sourceActionAt
12540
+ ? `来源时间 ${change.previousSourceActionAt || "无"} -> ${change.sourceActionAt || "无"}`
12541
+ : "",
12542
+ ].filter(Boolean).join(" · "),
12543
+ auditStatus: "observed",
12544
+ truth: "audit",
12545
+ authority: "agentflow",
12546
+ persistence: "runtime",
12547
+ clientId: reportMeta.clientId,
12548
+ userId: reportMeta.userId,
12549
+ observedAt: reportMeta.observedAt,
12550
+ reportedAt: reportMeta.reportedAt,
12551
+ revision: String(storedObservationSnapshot.revision || ""),
12552
+ previousRevision: String(previousClientSnapshot?.revision || ""),
12553
+ pointer: String(storedObservationSnapshot.pointer || ""),
12554
+ previousPointer: String(previousClientSnapshot?.pointer || ""),
12555
+ ...change,
12556
+ });
12557
+ }
12201
12558
 
12202
12559
  const projectFactSource = reportMeta.scope === "project"
12203
12560
  ? prdWorkflowProjectFactSource(payload, rawSnapshot)
12204
12561
  : null;
12205
12562
  const projectFactSnapshot = projectFactSource
12206
- ? prdWorkflowStoredObservationSnapshot(normalizedSnapshot, {
12563
+ ? prdWorkflowStoredObservationSnapshot(stampedSnapshot, {
12207
12564
  ...reportSource,
12208
12565
  ...projectFactSource,
12209
12566
  })