@akagilnc/pi-workflow-roles 0.1.4422 → 0.1.4489

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 (53) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/dist/acp-host/description.js +2 -3
  4. package/dist/acp-host/production-host.js +333 -258
  5. package/dist/auditor-soul.js +8 -1
  6. package/dist/diarist-contracts.js +2 -11
  7. package/dist/headless-host/description.js +3 -3
  8. package/dist/headless-host/production-host.js +340 -227
  9. package/dist/host-descriptions.js +3 -18
  10. package/dist/method-host-plugin/.claude-plugin/plugin.json +5 -0
  11. package/dist/method-host-plugin/skills/ak-cross-m-review/CONTEXT.md +48 -0
  12. package/dist/method-host-plugin/skills/ak-cross-m-review/LICENSE +21 -0
  13. package/dist/method-host-plugin/skills/ak-cross-m-review/SKILL.md +170 -0
  14. package/dist/method-host-plugin/skills/ak-cross-m-review/prompts/cmr-completeness.md +118 -0
  15. package/dist/method-host-plugin/skills/ak-cross-m-review/prompts/cmr-reviewer.md +128 -0
  16. package/dist/method-host-plugin/skills/ak-cross-m-review/provenance.json +41 -0
  17. package/dist/method-host-plugin/skills/diagnosing-bugs/SKILL.md +134 -0
  18. package/dist/method-host-plugin/skills/diagnosing-bugs/agents/openai.yaml +3 -0
  19. package/dist/method-host-plugin/skills/diagnosing-bugs/provenance.json +31 -0
  20. package/dist/method-host-plugin/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  21. package/dist/method-host-plugin/skills/resolving-merge-conflicts/SKILL.md +14 -0
  22. package/dist/method-host-plugin/skills/resolving-merge-conflicts/agents/openai.yaml +3 -0
  23. package/dist/method-host-plugin/skills/resolving-merge-conflicts/provenance.json +26 -0
  24. package/dist/method-host-plugin/skills/tdd/SKILL.md +38 -0
  25. package/dist/method-host-plugin/skills/tdd/agents/openai.yaml +3 -0
  26. package/dist/method-host-plugin/skills/tdd/mocking.md +59 -0
  27. package/dist/method-host-plugin/skills/tdd/provenance.json +36 -0
  28. package/dist/method-host-plugin/skills/tdd/tests.md +77 -0
  29. package/dist/public-cli/main.js +14 -21
  30. package/dist/session-opening-materials.js +17 -5
  31. package/dist/ticket-provenance-contracts.js +6 -25
  32. package/dist/ticket-provenance.js +223 -90
  33. package/extensions/role-runtime.ts +16 -6
  34. package/package.json +1 -1
  35. package/resources/method-host-plugin/.claude-plugin/plugin.json +5 -0
  36. package/scripts/build-package.mjs +6 -1
  37. package/src/acp-host/description.ts +2 -4
  38. package/src/acp-host/production-host.ts +0 -1
  39. package/src/auditor-soul.ts +10 -1
  40. package/src/diarist-contracts.ts +1 -19
  41. package/src/diarist-role.ts +3 -31
  42. package/src/diarist.ts +5 -19
  43. package/src/headless-host/description.ts +4 -3
  44. package/src/headless-host/role-turn-host.ts +27 -4
  45. package/src/host-descriptions.ts +3 -23
  46. package/src/host-native-method.ts +67 -0
  47. package/src/ledger-session-read.ts +1 -2
  48. package/src/role-envelope.ts +17 -47
  49. package/src/role-runtime-dependencies.ts +17 -2
  50. package/src/role-runtime.ts +14 -31
  51. package/src/session-opening-materials.ts +27 -11
  52. package/src/ticket-provenance-contracts.ts +11 -38
  53. package/src/ticket-provenance.ts +228 -109
@@ -34,7 +34,6 @@ import {
34
34
  projectTicketProvenanceHeader,
35
35
  projectTicketProvenanceLine,
36
36
  projectTicketProvenanceSessions,
37
- type TicketProvenanceAmendment,
38
37
  type TicketProvenanceBound,
39
38
  type TicketProvenanceHeader,
40
39
  type TicketProvenanceLine,
@@ -144,12 +143,37 @@ export function resolveTicketProvenanceVolume(
144
143
  return { recordFile: path.recordFile, volumeDir: path.sessionDir };
145
144
  }
146
145
 
146
+ type TicketProvenanceRaw = {
147
+ readonly raw: string;
148
+ readonly s: number;
149
+ readonly sourcePosition: number;
150
+ readonly sourceIdentity?: string;
151
+ };
152
+
147
153
  type TicketProvenanceCommit = {
148
154
  readonly timestamp: string;
149
155
  readonly sessions: readonly TicketProvenanceSession[];
150
- readonly lines: readonly TicketProvenanceLine[];
156
+ readonly lines: readonly (TicketProvenanceLine | TicketProvenanceRaw | string)[];
151
157
  };
152
158
 
159
+ function projectTicketProvenanceRaw(value: unknown): TicketProvenanceRaw | undefined {
160
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
161
+ const raw = value as Record<string, unknown>;
162
+ if (typeof raw.raw !== "string" || !Number.isInteger(raw.s) || (raw.s as number) < 0 ||
163
+ !Number.isInteger(raw.sourcePosition) || (raw.sourcePosition as number) < 0) {
164
+ return undefined;
165
+ }
166
+ const sourceIdentity = typeof raw.sourceIdentity === "string" && raw.sourceIdentity !== ""
167
+ ? raw.sourceIdentity
168
+ : undefined;
169
+ return {
170
+ raw: raw.raw,
171
+ s: raw.s as number,
172
+ sourcePosition: raw.sourcePosition as number,
173
+ ...(sourceIdentity === undefined ? {} : { sourceIdentity }),
174
+ };
175
+ }
176
+
153
177
  function projectTicketProvenanceCommit(value: unknown): TicketProvenanceCommit | undefined {
154
178
  if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
155
179
  const record = value as Record<string, unknown>;
@@ -162,11 +186,14 @@ function projectTicketProvenanceCommit(value: unknown): TicketProvenanceCommit |
162
186
  if (body.type !== "ticket-provenance-append") return undefined;
163
187
  const sessions = projectTicketProvenanceSessions(body.sessions);
164
188
  if (sessions === undefined || !Array.isArray(body.lines)) return undefined;
165
- const lines: TicketProvenanceLine[] = [];
189
+ const lines: (TicketProvenanceLine | TicketProvenanceRaw | string)[] = [];
166
190
  for (const raw of body.lines) {
191
+ if (typeof raw === "string") { lines.push(raw); continue; }
167
192
  const line = projectTicketProvenanceLine(raw);
168
- if (line === undefined) return undefined;
169
- lines.push(line);
193
+ if (line !== undefined) { lines.push(line); continue; }
194
+ const preserved = projectTicketProvenanceRaw(raw);
195
+ if (preserved === undefined) return undefined;
196
+ lines.push(preserved);
170
197
  }
171
198
  return { timestamp: record.timestamp, sessions, lines };
172
199
  }
@@ -180,6 +207,10 @@ export type ReadTicketProvenanceResult = {
180
207
  */
181
208
  readonly unprojectedRaw: readonly string[];
182
209
  readonly recordFile: string;
210
+ /** Internal cumulative coverage truth, keyed by normalized session index. */
211
+ readonly sourceIdentities: ReadonlyMap<number, ReadonlySet<string>>;
212
+ /** Upgrade fallback for records written before sourceIdentity existed. */
213
+ readonly legacySourcePositions: ReadonlyMap<number, ReadonlySet<number>>;
183
214
  };
184
215
 
185
216
  /** Read the unique diary file (empty/absent → no header, no lines). */
@@ -194,7 +225,14 @@ export async function readTicketProvenance(
194
225
  text = await readFile(recordFile, "utf8");
195
226
  } catch (error) {
196
227
  if ((error as NodeJS.ErrnoException).code === "ENOENT") {
197
- return { header: undefined, lines: [], unprojectedRaw: [], recordFile };
228
+ return {
229
+ header: undefined,
230
+ lines: [],
231
+ unprojectedRaw: [],
232
+ recordFile,
233
+ sourceIdentities: new Map(),
234
+ legacySourcePositions: new Map(),
235
+ };
198
236
  }
199
237
  throw error;
200
238
  }
@@ -202,6 +240,34 @@ export async function readTicketProvenance(
202
240
  let header: TicketProvenanceHeader | undefined;
203
241
  const lines: TicketProvenanceLine[] = [];
204
242
  const unprojectedRaw: string[] = [];
243
+ const sourceIdentities = new Map<number, Set<string>>();
244
+ const legacySourcePositions = new Map<number, Set<number>>();
245
+ const rememberIdentity = (s: number, identity: string): boolean => {
246
+ const seen = sourceIdentities.get(s) ?? new Set<string>();
247
+ const fresh = !seen.has(identity);
248
+ seen.add(identity);
249
+ sourceIdentities.set(s, seen);
250
+ return fresh;
251
+ };
252
+ const rememberLegacyPosition = (s: number, position: number): void => {
253
+ const seen = legacySourcePositions.get(s) ?? new Set<number>();
254
+ seen.add(position);
255
+ legacySourcePositions.set(s, seen);
256
+ };
257
+ const remapCoverage = <T>(
258
+ coverage: Map<number, Set<T>>,
259
+ priorIndexes: readonly number[],
260
+ ): void => {
261
+ const normalized = new Map<number, Set<T>>();
262
+ for (const [s, values] of coverage) {
263
+ const target = priorIndexes[s] ?? s;
264
+ const union = normalized.get(target) ?? new Set<T>();
265
+ for (const value of values) union.add(value);
266
+ normalized.set(target, union);
267
+ }
268
+ coverage.clear();
269
+ for (const [s, values] of normalized) coverage.set(s, values);
270
+ };
205
271
  let sawFirst = false;
206
272
  for (let index = 0; index < physical.length; index += 1) {
207
273
  const raw = physical[index]!;
@@ -228,10 +294,27 @@ export async function readTicketProvenance(
228
294
  const commit = projectTicketProvenanceCommit(parsed);
229
295
  if (commit !== undefined) {
230
296
  const merged = mergeSessionBounds(header?.sessions, commit.sessions);
231
- const remapped = commit.lines.map((entry) => ({
232
- ...entry,
233
- s: merged.incomingIndexes[entry.s] ?? entry.s,
234
- }));
297
+ remapCoverage(sourceIdentities, merged.priorIndexes);
298
+ remapCoverage(legacySourcePositions, merged.priorIndexes);
299
+ const remapped: TicketProvenanceLine[] = [];
300
+ for (const entry of commit.lines) {
301
+ if (typeof entry === "string") { unprojectedRaw.push(entry); continue; }
302
+ const s = merged.incomingIndexes[entry.s] ?? merged.priorIndexes[entry.s] ?? entry.s;
303
+ if ("raw" in entry) {
304
+ if (entry.sourceIdentity !== undefined) {
305
+ if (rememberIdentity(s, entry.sourceIdentity)) unprojectedRaw.push(entry.raw);
306
+ } else {
307
+ rememberLegacyPosition(s, entry.sourcePosition);
308
+ unprojectedRaw.push(entry.raw);
309
+ }
310
+ continue;
311
+ }
312
+ remapped.push({ ...entry, s });
313
+ if (entry.id === undefined) {
314
+ if (entry.sourceIdentity !== undefined) rememberIdentity(s, entry.sourceIdentity);
315
+ else if (entry.sourcePosition !== undefined) rememberLegacyPosition(s, entry.sourcePosition);
316
+ }
317
+ }
235
318
  const now = commit.timestamp;
236
319
  header = {
237
320
  repo: header?.repo ?? resolveBookKeyFromGit(cwd),
@@ -250,7 +333,19 @@ export async function readTicketProvenance(
250
333
  // Unknown stock / damaged rows remain readable and are never upgraded in place.
251
334
  unprojectedRaw.push(raw);
252
335
  }
253
- return { header, lines, unprojectedRaw, recordFile };
336
+ for (const line of lines) {
337
+ if (line.id !== undefined) continue;
338
+ if (line.sourceIdentity !== undefined) rememberIdentity(line.s, line.sourceIdentity);
339
+ else if (line.sourcePosition !== undefined) rememberLegacyPosition(line.s, line.sourcePosition);
340
+ }
341
+ return {
342
+ header,
343
+ lines,
344
+ unprojectedRaw,
345
+ recordFile,
346
+ sourceIdentities,
347
+ legacySourcePositions,
348
+ };
254
349
  }
255
350
 
256
351
  /**
@@ -269,19 +364,10 @@ export function ensureTicketProvenanceVolume(
269
364
  return { recordFile: path.recordFile, volumeDir: path.sessionDir };
270
365
  }
271
366
 
272
- /** One session line the mechanical projector could not enter. */
273
- export type UnparsableSessionLine = {
274
- readonly s: number;
275
- readonly line: number;
276
- readonly raw: string;
277
- };
278
-
279
367
  export type ReprojectTicketProvenanceResult = {
280
368
  readonly recordFile: string;
281
369
  readonly header: TicketProvenanceHeader;
282
370
  readonly lines: readonly TicketProvenanceLine[];
283
- /** Still-open gaps after applying the submitted amendment set. */
284
- readonly unparsable: readonly UnparsableSessionLine[];
285
371
  };
286
372
 
287
373
  /**
@@ -310,11 +396,11 @@ function resolveBoundIndex(
310
396
  return undefined;
311
397
  }
312
398
 
313
- function amendmentKey(s: number, line: number): string {
314
- return `${s}:${line}`;
399
+ /** Exact range identity for cumulative header dedupe (idempotent resubmit). */
400
+ function dialogueIdentity(s: number, id: string): string {
401
+ return `${s}\u0000${id}`;
315
402
  }
316
403
 
317
- /** Exact range identity for cumulative header dedupe (idempotent resubmit). */
318
404
  function rangeDeclarationKey(range: TicketProvenanceRange): string {
319
405
  return JSON.stringify({
320
406
  from: range.from,
@@ -370,7 +456,7 @@ function mergeSessionBounds(
370
456
 
371
457
  /**
372
458
  * #918 第一节:已投影行即卷宗。只对 prior 未声明的 range(按 path identity + 精确
373
- * from/to)读源;已声明的按 s,line 结转,不重读历史源。
459
+ * from/to)读源;已声明的投影原样结转,不重读历史源。
374
460
  */
375
461
  function undeclaredSessionRanges(
376
462
  prior: readonly TicketProvenanceSession[] | undefined,
@@ -389,11 +475,14 @@ function undeclaredSessionRanges(
389
475
  }
390
476
  for (const range of session.ranges) keys.add(rangeDeclarationKey(range));
391
477
  }
392
- const out: { s: number; session: TicketProvenanceSession }[] = [];
478
+ const out: {
479
+ s: number;
480
+ session: TicketProvenanceSession;
481
+ }[] = [];
393
482
  for (let s = 0; s < merged.length; s += 1) {
394
483
  const session = merged[s]!;
395
- const declared =
396
- priorKeys.get(physicalPathIdentity(session.path)) ?? new Set();
484
+ const identity = physicalPathIdentity(session.path);
485
+ const declared = priorKeys.get(identity) ?? new Set();
397
486
  const fresh = session.ranges.filter(
398
487
  (range) => !declared.has(rangeDeclarationKey(range)),
399
488
  );
@@ -412,49 +501,87 @@ function undeclaredSessionRanges(
412
501
  return out;
413
502
  }
414
503
 
415
- /** Sort archive rows by session index then source line (stable within equal keys). */
416
- function compareLinePosition(
417
- left: TicketProvenanceLine,
418
- right: TicketProvenanceLine,
419
- ): number {
420
- if (left.s !== right.s) return left.s - right.s;
421
- const leftLine = left.line ?? Number.MAX_SAFE_INTEGER;
422
- const rightLine = right.line ?? Number.MAX_SAFE_INTEGER;
423
- return leftLine - rightLine;
424
- }
425
-
426
504
  /**
427
505
  * Merge newly projected rows into the carried archive.
428
- * 既有卷宗按 (s,line) 优先;fresh 不得复制无 id 行(#918 C2/G1)。
429
- * id 去重仍保留(跨 path 同 id 首见胜)。
506
+ * Native id remains the typed first-seen identity; exact range resubmission is a no-op.
430
507
  */
431
508
  function mergeFreshIntoCarried(
432
509
  carried: readonly TicketProvenanceLine[],
433
510
  fresh: readonly TicketProvenanceLine[],
434
511
  ): TicketProvenanceLine[] {
435
- const seenIds = new Set<string>();
436
- const seenPositions = new Set<string>();
437
- const out: TicketProvenanceLine[] = [];
438
- for (const line of carried) {
439
- if (line.id !== undefined) seenIds.add(line.id);
440
- if (line.line !== undefined)
441
- seenPositions.add(amendmentKey(line.s, line.line));
442
- out.push(line);
443
- }
444
- for (const line of fresh) {
445
- if (line.id !== undefined) {
446
- if (seenIds.has(line.id)) continue;
447
- seenIds.add(line.id);
512
+ const seenSources = new Map<string, TicketProvenanceLine>();
513
+ const bySession = new Map<number, TicketProvenanceLine[]>();
514
+ const absorb = (lines: readonly TicketProvenanceLine[]): void => {
515
+ for (const line of lines) {
516
+ const sourceIdentity = line.id === undefined
517
+ ? line.sourceIdentity
518
+ : `id\u0000${line.id}`;
519
+ const identity = sourceIdentity === undefined
520
+ ? undefined
521
+ : `${line.s}\u0000${sourceIdentity}`;
522
+ const seen = identity === undefined ? undefined : seenSources.get(identity);
523
+ if (seen !== undefined) {
524
+ if (line.sourcePosition !== undefined) {
525
+ Object.assign(seen, { sourcePosition: line.sourcePosition });
526
+ }
527
+ continue;
528
+ }
529
+ const bucket = bySession.get(line.s) ?? [];
530
+ bucket.push({ ...line });
531
+ if (identity !== undefined) seenSources.set(identity, bucket.at(-1)!);
532
+ bySession.set(line.s, bucket);
533
+ }
534
+ };
535
+ absorb(carried);
536
+ absorb(fresh);
537
+ return [...bySession.entries()]
538
+ .sort(([left], [right]) => left - right)
539
+ .flatMap(([, lines]) => lines
540
+ .map((line, archiveIndex) => ({ line, archiveIndex }))
541
+ .sort((left, right) => {
542
+ const leftPosition = left.line.sourcePosition;
543
+ const rightPosition = right.line.sourcePosition;
544
+ if (leftPosition === undefined && rightPosition === undefined) {
545
+ return left.archiveIndex - right.archiveIndex;
546
+ }
547
+ if (leftPosition === undefined) return 1;
548
+ if (rightPosition === undefined) return -1;
549
+ return leftPosition - rightPosition || left.archiveIndex - right.archiveIndex;
550
+ })
551
+ .map(({ line }) => line));
552
+ }
553
+
554
+ /**
555
+ * Give every logical source row a replay-invariant ordinal. Native ids identify
556
+ * replayed rows directly; idless rows use their ordinal after the preceding
557
+ * native id. Neither key inspects dialogue text or physical line numbers.
558
+ */
559
+ function stableSourceFacts(
560
+ sessionLines: readonly LedgerSessionLine[],
561
+ ): readonly { readonly position: number; readonly identity: string }[] {
562
+ const positionByIdentity = new Map<string, number>();
563
+ const facts: { position: number; identity: string }[] = [];
564
+ let precedingId = "<start>";
565
+ let idlessOffset = 0;
566
+ for (const entry of sessionLines) {
567
+ const id = entry.row === undefined ? undefined : nativeEventId(entry.row);
568
+ if (id !== undefined) {
569
+ precedingId = id;
570
+ idlessOffset = 0;
571
+ } else {
572
+ idlessOffset += 1;
448
573
  }
449
- if (line.line !== undefined) {
450
- const position = amendmentKey(line.s, line.line);
451
- if (seenPositions.has(position)) continue;
452
- seenPositions.add(position);
574
+ const identity = id === undefined
575
+ ? `after\u0000${precedingId}\u0000${idlessOffset}`
576
+ : `id\u0000${id}`;
577
+ let position = positionByIdentity.get(identity);
578
+ if (position === undefined) {
579
+ position = positionByIdentity.size;
580
+ positionByIdentity.set(identity, position);
453
581
  }
454
- out.push(line);
582
+ facts.push({ position, identity });
455
583
  }
456
- out.sort(compareLinePosition);
457
- return out;
584
+ return facts;
458
585
  }
459
586
 
460
587
  /**
@@ -505,12 +632,14 @@ function normalizeResolvedRanges(
505
632
  async function projectSessionRanges(input: {
506
633
  readonly s: number;
507
634
  readonly session: TicketProvenanceSession;
508
- readonly amendmentsByKey: ReadonlyMap<string, TicketProvenanceAmendment>;
509
635
  readonly seenIds: Set<string>;
636
+ readonly coveredSourceIdentities: ReadonlySet<string>;
637
+ readonly legacyCoveredSourcePositions: ReadonlySet<number>;
510
638
  readonly home?: string;
511
639
  }): Promise<{
512
640
  readonly lines: TicketProvenanceLine[];
513
- readonly unparsable: UnparsableSessionLine[];
641
+ readonly raw: TicketProvenanceRaw[];
642
+ readonly sourcePositionByIdentity: ReadonlyMap<string, number>;
514
643
  }> {
515
644
  assertDialogueSessionSourcePath(input.session.path, input.home);
516
645
  let sessionLines: LedgerSessionLine[];
@@ -534,56 +663,52 @@ async function projectSessionRanges(input: {
534
663
 
535
664
  const rows = sessionLines.map((entry) => entry.row);
536
665
  const dialogue = adaptSessionDialogue(rows);
666
+ const sourceFacts = stableSourceFacts(sessionLines);
667
+ const sourcePositionByIdentity = new Map<string, number>();
668
+ for (const fact of sourceFacts) {
669
+ sourcePositionByIdentity.set(fact.identity, fact.position);
670
+ }
537
671
  const seenIds = input.seenIds;
538
672
  const lines: TicketProvenanceLine[] = [];
539
- const unparsable: UnparsableSessionLine[] = [];
673
+ const raw: TicketProvenanceRaw[] = [];
540
674
  const ranges = normalizeResolvedRanges(
541
675
  input.session.ranges,
542
676
  sessionLines,
543
677
  input.session.path,
544
678
  );
545
-
546
679
  for (const range of ranges) {
547
680
  for (let index = range.fromIndex; index <= range.toIndex; index += 1) {
681
+ const { position: sourcePosition, identity: sourceIdentity } = sourceFacts[index]!;
682
+ if (input.coveredSourceIdentities.has(sourceIdentity) ||
683
+ input.legacyCoveredSourcePositions.has(sourcePosition)) continue;
548
684
  const entry = sessionLines[index]!;
549
685
  if (entry.row === undefined) {
550
- const key = amendmentKey(input.s, entry.line);
551
- const amendment = input.amendmentsByKey.get(key);
552
- if (amendment !== undefined) {
553
- lines.push({
554
- speaker: amendment.speaker,
555
- s: input.s,
556
- line: entry.line,
557
- text: amendment.text,
558
- });
559
- } else {
560
- unparsable.push({ s: input.s, line: entry.line, raw: entry.raw });
561
- }
686
+ raw.push({ raw: entry.raw, s: input.s, sourcePosition, sourceIdentity });
562
687
  continue;
563
688
  }
564
689
  for (const event of dialogue[index] ?? []) {
565
690
  if (event.id !== undefined) {
566
- if (seenIds.has(event.id)) continue;
567
- seenIds.add(event.id);
691
+ const identity = dialogueIdentity(input.s, event.id);
692
+ if (seenIds.has(identity)) continue;
693
+ seenIds.add(identity);
568
694
  }
569
695
  lines.push({
570
696
  speaker: event.speaker,
571
697
  s: input.s,
572
- line: entry.line,
573
- ...(event.id === undefined ? {} : { id: event.id }),
698
+ sourcePosition,
699
+ ...(event.id === undefined ? { sourceIdentity } : { id: event.id }),
574
700
  text: event.text,
575
701
  });
576
702
  }
577
703
  }
578
704
  }
579
- return { lines, unparsable };
705
+ return { lines, raw, sourcePositionByIdentity };
580
706
  }
581
707
 
582
708
  /**
583
- * Reproject the unique diary from cumulative bounds + optional amendments.
584
- * #918:已投影行即卷宗——按 s,line 结转;本轮只读 prior 未声明的 range(或新
585
- * session 区间)。精确重复提交=幂等 no-op,不因历史源不可读而失败。
586
- * amendments 只在本轮读取的新范围确有对应坏行时生效;空 sessions 保持 no-op。
709
+ * Reproject the unique diary from cumulative bounds.
710
+ * #918:已投影行即卷宗;本轮只读 prior 未声明的 range(或新 session 区间)。
711
+ * 精确重复提交=幂等 no-op,不因历史源不可读而失败;空 sessions 保持 no-op。
587
712
  * 证不出的 body 原字节经 unprojectedRaw 原样留存。
588
713
  * Persistence appends one immutable commit through the Sitian appender seam.
589
714
  */
@@ -592,7 +717,6 @@ export async function reprojectTicketProvenance(input: {
592
717
  readonly cwd: string;
593
718
  readonly home?: string;
594
719
  readonly sessions: readonly TicketProvenanceSession[];
595
- readonly amendments?: readonly TicketProvenanceAmendment[];
596
720
  }): Promise<ReprojectTicketProvenanceResult> {
597
721
  const prior = await readTicketProvenance(input.ticketNumber, input.cwd, input.home);
598
722
  if (input.sessions.length === 0) {
@@ -607,7 +731,6 @@ export async function reprojectTicketProvenance(input: {
607
731
  sessions: [],
608
732
  },
609
733
  lines: prior.lines,
610
- unparsable: [],
611
734
  };
612
735
  }
613
736
 
@@ -625,35 +748,36 @@ export async function reprojectTicketProvenance(input: {
625
748
  sessions: merged.sessions,
626
749
  },
627
750
  lines: prior.lines,
628
- unparsable: [],
629
751
  };
630
752
  }
631
753
 
632
- const amendmentsByKey = new Map<string, TicketProvenanceAmendment>();
633
- for (const amendment of input.amendments ?? []) {
634
- const cumulativeIndex = merged.incomingIndexes[amendment.s];
635
- if (cumulativeIndex === undefined) continue;
636
- amendmentsByKey.set(amendmentKey(cumulativeIndex, amendment.line), {
637
- ...amendment,
638
- s: cumulativeIndex,
639
- });
640
- }
641
-
642
754
  const seenIds = new Set(
643
- prior.lines.flatMap((line) => line.id === undefined ? [] : [line.id]),
755
+ prior.lines.flatMap((line) =>
756
+ line.id === undefined ? [] : [dialogueIdentity(line.s, line.id)]
757
+ ),
644
758
  );
645
759
  const fresh: TicketProvenanceLine[] = [];
646
- const unparsable: UnparsableSessionLine[] = [];
760
+ const raw: TicketProvenanceRaw[] = [];
647
761
  for (const delta of deltas) {
648
762
  const projected = await projectSessionRanges({
649
763
  s: delta.s,
650
764
  session: delta.session,
651
- amendmentsByKey,
652
765
  seenIds,
766
+ coveredSourceIdentities: prior.sourceIdentities.get(delta.s) ?? new Set(),
767
+ legacyCoveredSourcePositions: prior.legacySourcePositions.get(delta.s) ?? new Set(),
653
768
  ...(input.home === undefined ? {} : { home: input.home }),
654
769
  });
770
+ fresh.push(...prior.lines.flatMap((line) => {
771
+ if (line.s !== delta.s) return [];
772
+ const sourceIdentity = line.id === undefined
773
+ ? line.sourceIdentity
774
+ : `id\u0000${line.id}`;
775
+ if (sourceIdentity === undefined) return [];
776
+ const sourcePosition = projected.sourcePositionByIdentity.get(sourceIdentity);
777
+ return sourcePosition === undefined ? [] : [{ ...line, sourcePosition }];
778
+ }));
655
779
  fresh.push(...projected.lines);
656
- unparsable.push(...projected.unparsable);
780
+ raw.push(...projected.raw);
657
781
  }
658
782
  const lines = mergeFreshIntoCarried(prior.lines, fresh);
659
783
  const now = new Date().toISOString();
@@ -664,10 +788,6 @@ export async function reprojectTicketProvenance(input: {
664
788
  updatedAt: now,
665
789
  sessions: merged.sessions,
666
790
  };
667
- if (unparsable.length > 0) {
668
- return { recordFile: prior.recordFile, header, lines, unparsable };
669
- }
670
-
671
791
  // One immutable projection commit. Its deterministic identity makes retries of
672
792
  // the same logical increment converge, while unrelated concurrent increments
673
793
  // append independently and are folded by readTicketProvenance.
@@ -678,7 +798,7 @@ export async function reprojectTicketProvenance(input: {
678
798
  path: physicalPathIdentity(session.path),
679
799
  ranges: session.ranges,
680
800
  })),
681
- lines: fresh,
801
+ lines: [...fresh, ...raw],
682
802
  });
683
803
  const identity = `ticket-provenance:${createHash("sha256").update(identityMaterial).digest("hex")}`;
684
804
  const pointer = appendSitianRecord({
@@ -687,7 +807,7 @@ export async function reprojectTicketProvenance(input: {
687
807
  payload: {
688
808
  type: "ticket-provenance-append",
689
809
  sessions: merged.sessions,
690
- lines: fresh,
810
+ lines: [...fresh, ...raw],
691
811
  },
692
812
  });
693
813
  const folded = await readTicketProvenance(input.ticketNumber, input.cwd, input.home);
@@ -695,6 +815,5 @@ export async function reprojectTicketProvenance(input: {
695
815
  recordFile: pointer.recordFile,
696
816
  header: folded.header ?? header,
697
817
  lines: folded.lines,
698
- unparsable: [],
699
818
  };
700
819
  }