@akagilnc/pi-workflow-roles 0.1.5212 → 0.1.5216

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.
@@ -1,20 +1,16 @@
1
1
  /**
2
- * 起居录 volume helpers — ADR 0075「2026-09-14 修订」/ #901 / #918。
2
+ * 起居录 volume helpers — ADR 0075 / ADR 0081 / ADR 0086 / #1090.
3
3
  * 一册=一个追加式 records.jsonl。每轮新投影经司天台 appender 追加为不可变提交;
4
- * 读取时折叠全部提交得到累计 sessions 与对话视图。既有首行册子头+裸对话行的
5
- * snapshot 继续可读,但后续不为升级回写旧卷。
4
+ * 纯追加、不回读历史去重、不折叠。
6
5
  */
7
- import { createHash } from "node:crypto";
8
- import { appendFileSync } from "node:fs";
9
6
  import { readFile, unlink } from "node:fs/promises";
10
7
  import { basename, dirname, join, resolve } from "node:path";
11
- import { resolveBookKeyFromGit } from "./activation-ledger-git.js";
12
- import { ensureRealDirectoryTree, errnoCode, packageMachineHome, physicalPathIdentity, physicallyContainedIn, resolveActivationLedgerHome, } from "./activation-ledger-topology.js";
8
+ import { errnoCode, packageMachineHome, physicallyContainedIn, resolveActivationLedgerHome, } from "./activation-ledger-topology.js";
13
9
  import { readLedgerSessionJsonlLines, } from "./ledger-session-read.js";
14
10
  import { isSafePositiveTicketNumber } from "./run-ticket-number.js";
15
11
  import { adaptSessionDialogue, nativeEventId } from "./session-dialogue.js";
16
12
  import { appendSitianRecordBlock, appendSitianRecord, resolveSitianRecordPath, } from "./sitian-facade.js";
17
- import { TICKET_PROVENANCE_KIND, projectTicketProvenanceHeader, projectTicketProvenanceLine, projectTicketProvenanceSessions, } from "./ticket-provenance-contracts.js";
13
+ import { TICKET_PROVENANCE_KIND, } from "./ticket-provenance-contracts.js";
18
14
  /**
19
15
  * Typed input failure for diarist bounds/session path (reask, not infrastructure).
20
16
  * Accept hook discriminates with instanceof — never Error.message prefixes.
@@ -30,7 +26,7 @@ export class TicketProvenanceInputError extends Error {
30
26
  * Host-owned session source roots (ADR 0038 / ADR 0081 cross-host).
31
27
  * Narrow directory identities — not whole `.pi` / whole `.ak-roles`.
32
28
  */
33
- export function dialogueSessionSourceRoots(home) {
29
+ function dialogueSessionSourceRoots(home) {
34
30
  const machineHome = typeof home === "string" && home.trim() !== ""
35
31
  ? home
36
32
  : packageMachineHome();
@@ -66,7 +62,7 @@ function assertDialogueSessionSourcePath(path, home) {
66
62
  throw new TicketProvenanceInputError(`session unreadable: ${path} (outside authorized source roots)`);
67
63
  }
68
64
  /** Subject string for ticket-keyed volumes — history follows the ticket. */
69
- export function ticketProvenanceSubject(ticketNumber) {
65
+ function ticketProvenanceSubject(ticketNumber) {
70
66
  if (!isSafePositiveTicketNumber(ticketNumber)) {
71
67
  throw new Error(`ticket-provenance subject requires a positive ticket number, got ${String(ticketNumber)}`);
72
68
  }
@@ -88,214 +84,6 @@ export function resolveTicketProvenanceVolume(ticketNumber, cwd, home) {
88
84
  const path = resolveSitianRecordPath(ticketProvenanceRecordInput(ticketNumber, cwd, home));
89
85
  return { recordFile: path.recordFile, volumeDir: path.sessionDir };
90
86
  }
91
- function projectTicketProvenanceRaw(value) {
92
- if (typeof value !== "object" || value === null || Array.isArray(value))
93
- return undefined;
94
- const raw = value;
95
- if (typeof raw.raw !== "string" || !Number.isInteger(raw.s) || raw.s < 0 ||
96
- !Number.isInteger(raw.sourcePosition) || raw.sourcePosition < 0) {
97
- return undefined;
98
- }
99
- const sourceIdentity = typeof raw.sourceIdentity === "string" && raw.sourceIdentity !== ""
100
- ? raw.sourceIdentity
101
- : undefined;
102
- return {
103
- raw: raw.raw,
104
- s: raw.s,
105
- sourcePosition: raw.sourcePosition,
106
- ...(sourceIdentity === undefined ? {} : { sourceIdentity }),
107
- };
108
- }
109
- function projectTicketProvenanceCommit(value) {
110
- if (typeof value !== "object" || value === null || Array.isArray(value))
111
- return undefined;
112
- const record = value;
113
- if (record.kind !== TICKET_PROVENANCE_KIND || typeof record.timestamp !== "string") {
114
- return undefined;
115
- }
116
- const payload = record.payload;
117
- if (typeof payload !== "object" || payload === null || Array.isArray(payload))
118
- return undefined;
119
- const body = payload;
120
- if (body.type !== "ticket-provenance-append")
121
- return undefined;
122
- const sessions = projectTicketProvenanceSessions(body.sessions);
123
- if (sessions === undefined || !Array.isArray(body.lines))
124
- return undefined;
125
- const lines = [];
126
- for (const raw of body.lines) {
127
- if (typeof raw === "string") {
128
- lines.push(raw);
129
- continue;
130
- }
131
- const line = projectTicketProvenanceLine(raw);
132
- if (line !== undefined) {
133
- lines.push(line);
134
- continue;
135
- }
136
- const preserved = projectTicketProvenanceRaw(raw);
137
- if (preserved === undefined)
138
- return undefined;
139
- lines.push(preserved);
140
- }
141
- return { timestamp: record.timestamp, sessions, lines };
142
- }
143
- /** Read the unique diary file (empty/absent → no header, no lines). */
144
- export async function readTicketProvenance(ticketNumber, cwd, home, runDirectory) {
145
- const { recordFile } = ticketNumber === null
146
- ? resolveSitianRecordPath(ticketProvenanceRecordInput(null, cwd, home, runDirectory))
147
- : resolveTicketProvenanceVolume(ticketNumber, cwd, home);
148
- let text;
149
- try {
150
- text = await readFile(recordFile, "utf8");
151
- }
152
- catch (error) {
153
- if (error.code === "ENOENT") {
154
- return {
155
- header: undefined,
156
- lines: [],
157
- unprojectedRaw: [],
158
- recordFile,
159
- sourceIdentities: new Map(),
160
- legacySourcePositions: new Map(),
161
- };
162
- }
163
- throw error;
164
- }
165
- const physical = text.split("\n");
166
- let header;
167
- const lines = [];
168
- const unprojectedRaw = [];
169
- const sourceIdentities = new Map();
170
- const legacySourcePositions = new Map();
171
- const rememberIdentity = (s, identity) => {
172
- const seen = sourceIdentities.get(s) ?? new Set();
173
- const fresh = !seen.has(identity);
174
- seen.add(identity);
175
- sourceIdentities.set(s, seen);
176
- return fresh;
177
- };
178
- const rememberLegacyPosition = (s, position) => {
179
- const seen = legacySourcePositions.get(s) ?? new Set();
180
- seen.add(position);
181
- legacySourcePositions.set(s, seen);
182
- };
183
- const remapCoverage = (coverage, priorIndexes) => {
184
- const normalized = new Map();
185
- for (const [s, values] of coverage) {
186
- const target = priorIndexes[s] ?? s;
187
- const union = normalized.get(target) ?? new Set();
188
- for (const value of values)
189
- union.add(value);
190
- normalized.set(target, union);
191
- }
192
- coverage.clear();
193
- for (const [s, values] of normalized)
194
- coverage.set(s, values);
195
- };
196
- let sawFirst = false;
197
- for (let index = 0; index < physical.length; index += 1) {
198
- const raw = physical[index];
199
- if (!raw.trim())
200
- continue;
201
- let parsed;
202
- try {
203
- parsed = JSON.parse(raw);
204
- }
205
- catch {
206
- // Stock / damaged rows stay unprojected — keep raw bytes on rewrite.
207
- unprojectedRaw.push(raw);
208
- continue;
209
- }
210
- if (!sawFirst) {
211
- sawFirst = true;
212
- header = projectTicketProvenanceHeader(parsed);
213
- // Legacy snapshot: first line header followed by bare dialogue rows.
214
- if (header !== undefined)
215
- continue;
216
- }
217
- const line = projectTicketProvenanceLine(parsed);
218
- if (line !== undefined) {
219
- lines.push(line);
220
- continue;
221
- }
222
- const commit = projectTicketProvenanceCommit(parsed);
223
- if (commit !== undefined) {
224
- const merged = mergeSessionBounds(header?.sessions, commit.sessions);
225
- remapCoverage(sourceIdentities, merged.priorIndexes);
226
- remapCoverage(legacySourcePositions, merged.priorIndexes);
227
- const remapped = [];
228
- for (const entry of commit.lines) {
229
- if (typeof entry === "string") {
230
- unprojectedRaw.push(entry);
231
- continue;
232
- }
233
- const s = merged.incomingIndexes[entry.s] ?? merged.priorIndexes[entry.s] ?? entry.s;
234
- if ("raw" in entry) {
235
- if (entry.sourceIdentity !== undefined) {
236
- if (rememberIdentity(s, entry.sourceIdentity))
237
- unprojectedRaw.push(entry.raw);
238
- }
239
- else {
240
- rememberLegacyPosition(s, entry.sourcePosition);
241
- unprojectedRaw.push(entry.raw);
242
- }
243
- continue;
244
- }
245
- remapped.push({ ...entry, s });
246
- if (entry.id === undefined) {
247
- if (entry.sourceIdentity !== undefined)
248
- rememberIdentity(s, entry.sourceIdentity);
249
- else if (entry.sourcePosition !== undefined)
250
- rememberLegacyPosition(s, entry.sourcePosition);
251
- }
252
- }
253
- const now = commit.timestamp;
254
- header = {
255
- repo: header?.repo ?? resolveBookKeyFromGit(cwd),
256
- ticket: ticketNumber,
257
- createdAt: header?.createdAt ?? now,
258
- updatedAt: header?.updatedAt !== undefined && header.updatedAt > now ? header.updatedAt : now,
259
- sessions: merged.sessions,
260
- };
261
- const carried = lines.map((entry) => ({
262
- ...entry,
263
- s: merged.priorIndexes[entry.s] ?? entry.s,
264
- }));
265
- lines.splice(0, lines.length, ...mergeFreshIntoCarried(carried, remapped));
266
- continue;
267
- }
268
- // Unknown stock / damaged rows remain readable and are never upgraded in place.
269
- unprojectedRaw.push(raw);
270
- }
271
- for (const line of lines) {
272
- if (line.id !== undefined)
273
- continue;
274
- if (line.sourceIdentity !== undefined)
275
- rememberIdentity(line.s, line.sourceIdentity);
276
- else if (line.sourcePosition !== undefined)
277
- rememberLegacyPosition(line.s, line.sourcePosition);
278
- }
279
- return {
280
- header,
281
- lines,
282
- unprojectedRaw,
283
- recordFile,
284
- sourceIdentities,
285
- legacySourcePositions,
286
- };
287
- }
288
- /**
289
- * Ensure the per-ticket directory + volume file exist (ADR 0075 ticket-provenance-file).
290
- * Empty file is lawful (bound court with no dialogue yet). Does not forge entries.
291
- */
292
- export function ensureTicketProvenanceVolume(ticketNumber, cwd, home) {
293
- const input = ticketProvenanceRecordInput(ticketNumber, cwd, home);
294
- const path = resolveSitianRecordPath(input);
295
- ensureRealDirectoryTree(path.ledgerHome, path.sessionDir);
296
- appendFileSync(path.recordFile, "", "utf8");
297
- return { recordFile: path.recordFile, volumeDir: path.sessionDir };
298
- }
299
87
  /** Assign a run-owned unbound diary as its original block through Sitian. */
300
88
  export async function rehomeUnboundTicketProvenance(runDirectory, ticketNumber, cwd, home) {
301
89
  const source = join(runDirectory, "records.jsonl");
@@ -336,139 +124,6 @@ function resolveBoundIndex(bound, sessionLines) {
336
124
  }
337
125
  return undefined;
338
126
  }
339
- /** Exact range identity for cumulative header dedupe (idempotent resubmit). */
340
- function dialogueIdentity(s, id) {
341
- return `${s}\u0000${id}`;
342
- }
343
- function rangeDeclarationKey(range) {
344
- return JSON.stringify({
345
- from: range.from,
346
- to: range.to,
347
- });
348
- }
349
- /**
350
- * #918 甲案:prior ranges ∪ 本轮 ranges。按 path 合并;先保 prior 序,再追加新 path。
351
- * path identity 与 I/O 接缝同用 `physicalPathIdentity`(symlink-stable),故
352
- * `p` / `p/./` / 经 symlink 祖先的别名合为一条;保留首见 path 字面与累计 ranges。
353
- * 即使 prior 缺失 / sessions=[],incoming 自身也必须按 identity 归并——首轮同一
354
- * 物理卷不得铸出多个 s(#918 C5)。遗漏不代表删除。
355
- */
356
- function mergeSessionBounds(prior, incoming) {
357
- const merged = [];
358
- const indexByIdentity = new Map();
359
- const absorb = (sessions) => {
360
- const indexes = [];
361
- for (const session of sessions) {
362
- const identity = physicalPathIdentity(session.path);
363
- let targetIndex = indexByIdentity.get(identity);
364
- if (targetIndex === undefined) {
365
- targetIndex = merged.length;
366
- indexByIdentity.set(identity, targetIndex);
367
- merged.push({ path: session.path, ranges: [] });
368
- }
369
- indexes.push(targetIndex);
370
- const target = merged[targetIndex];
371
- const seen = new Set(target.ranges.map(rangeDeclarationKey));
372
- for (const range of session.ranges) {
373
- const key = rangeDeclarationKey(range);
374
- if (seen.has(key))
375
- continue;
376
- seen.add(key);
377
- target.ranges.push({ from: { ...range.from }, to: { ...range.to } });
378
- }
379
- }
380
- return indexes;
381
- };
382
- const priorIndexes = absorb(prior ?? []);
383
- const incomingIndexes = absorb(incoming);
384
- return { sessions: merged, priorIndexes, incomingIndexes };
385
- }
386
- /**
387
- * #918 第一节:已投影行即卷宗。只对 prior 未声明的 range(按 path identity + 精确
388
- * from/to)读源;已声明的投影原样结转,不重读历史源。
389
- */
390
- function undeclaredSessionRanges(prior, merged) {
391
- const priorKeys = new Map();
392
- for (const session of prior ?? []) {
393
- const identity = physicalPathIdentity(session.path);
394
- let keys = priorKeys.get(identity);
395
- if (keys === undefined) {
396
- keys = new Set();
397
- priorKeys.set(identity, keys);
398
- }
399
- for (const range of session.ranges)
400
- keys.add(rangeDeclarationKey(range));
401
- }
402
- const out = [];
403
- for (let s = 0; s < merged.length; s += 1) {
404
- const session = merged[s];
405
- const identity = physicalPathIdentity(session.path);
406
- const declared = priorKeys.get(identity) ?? new Set();
407
- const fresh = session.ranges.filter((range) => !declared.has(rangeDeclarationKey(range)));
408
- if (fresh.length === 0)
409
- continue;
410
- out.push({
411
- s,
412
- session: {
413
- path: session.path,
414
- ranges: fresh.map((range) => ({
415
- from: { ...range.from },
416
- to: { ...range.to },
417
- })),
418
- },
419
- });
420
- }
421
- return out;
422
- }
423
- /**
424
- * Merge newly projected rows into the carried archive.
425
- * Native id remains the typed first-seen identity; exact range resubmission is a no-op.
426
- */
427
- function mergeFreshIntoCarried(carried, fresh) {
428
- const seenSources = new Map();
429
- const bySession = new Map();
430
- const absorb = (lines) => {
431
- for (const line of lines) {
432
- const sourceIdentity = line.id === undefined
433
- ? line.sourceIdentity
434
- : `id\u0000${line.id}`;
435
- const identity = sourceIdentity === undefined
436
- ? undefined
437
- : `${line.s}\u0000${sourceIdentity}`;
438
- const seen = identity === undefined ? undefined : seenSources.get(identity);
439
- if (seen !== undefined) {
440
- if (line.sourcePosition !== undefined) {
441
- Object.assign(seen, { sourcePosition: line.sourcePosition });
442
- }
443
- continue;
444
- }
445
- const bucket = bySession.get(line.s) ?? [];
446
- bucket.push({ ...line });
447
- if (identity !== undefined)
448
- seenSources.set(identity, bucket.at(-1));
449
- bySession.set(line.s, bucket);
450
- }
451
- };
452
- absorb(carried);
453
- absorb(fresh);
454
- return [...bySession.entries()]
455
- .sort(([left], [right]) => left - right)
456
- .flatMap(([, lines]) => lines
457
- .map((line, archiveIndex) => ({ line, archiveIndex }))
458
- .sort((left, right) => {
459
- const leftPosition = left.line.sourcePosition;
460
- const rightPosition = right.line.sourcePosition;
461
- if (leftPosition === undefined && rightPosition === undefined) {
462
- return left.archiveIndex - right.archiveIndex;
463
- }
464
- if (leftPosition === undefined)
465
- return 1;
466
- if (rightPosition === undefined)
467
- return -1;
468
- return leftPosition - rightPosition || left.archiveIndex - right.archiveIndex;
469
- })
470
- .map(({ line }) => line));
471
- }
472
127
  /**
473
128
  * Give every logical source row a replay-invariant ordinal. Native ids identify
474
129
  * replayed rows directly; idless rows use their ordinal after the preceding
@@ -542,9 +197,6 @@ async function projectSessionRanges(input) {
542
197
  catch (error) {
543
198
  if (error instanceof TicketProvenanceInputError)
544
199
  throw error;
545
- // Only absence/path-shape misses are model input errors → reask.
546
- // EISDIR = authorized-root path that is a directory, not a session file.
547
- // EIO / EMFILE / EACCES / other runtime faults keep native identity + cause.
548
200
  const code = errnoCode(error);
549
201
  if (code === "ENOENT" || code === "ENOTDIR" || code === "EISDIR") {
550
202
  const detail = error instanceof Error ? error.message : String(error);
@@ -555,33 +207,18 @@ async function projectSessionRanges(input) {
555
207
  const rows = sessionLines.map((entry) => entry.row);
556
208
  const dialogue = adaptSessionDialogue(rows);
557
209
  const sourceFacts = stableSourceFacts(sessionLines);
558
- const sourcePositionByIdentity = new Map();
559
- for (const fact of sourceFacts) {
560
- sourcePositionByIdentity.set(fact.identity, fact.position);
561
- }
562
- const seenIds = input.seenIds;
563
- const lines = [];
564
- const raw = [];
210
+ const entries = [];
565
211
  const ranges = normalizeResolvedRanges(input.session.ranges, sessionLines, input.session.path);
566
212
  for (const range of ranges) {
567
213
  for (let index = range.fromIndex; index <= range.toIndex; index += 1) {
568
214
  const { position: sourcePosition, identity: sourceIdentity } = sourceFacts[index];
569
- if (input.coveredSourceIdentities.has(sourceIdentity) ||
570
- input.legacyCoveredSourcePositions.has(sourcePosition))
571
- continue;
572
215
  const entry = sessionLines[index];
573
216
  if (entry.row === undefined) {
574
- raw.push({ raw: entry.raw, s: input.s, sourcePosition, sourceIdentity });
217
+ entries.push({ raw: entry.raw, s: input.s, sourcePosition, sourceIdentity });
575
218
  continue;
576
219
  }
577
220
  for (const event of dialogue[index] ?? []) {
578
- if (event.id !== undefined) {
579
- const identity = dialogueIdentity(input.s, event.id);
580
- if (seenIds.has(identity))
581
- continue;
582
- seenIds.add(identity);
583
- }
584
- lines.push({
221
+ entries.push({
585
222
  speaker: event.speaker,
586
223
  s: input.s,
587
224
  sourcePosition,
@@ -591,108 +228,42 @@ async function projectSessionRanges(input) {
591
228
  }
592
229
  }
593
230
  }
594
- return { lines, raw, sourcePositionByIdentity };
231
+ return { entries };
595
232
  }
596
233
  /**
597
- * Reproject the unique diary from cumulative bounds.
598
- * #918:已投影行即卷宗;本轮只读 prior 未声明的 range(或新 session 区间)。
599
- * 精确重复提交=幂等 no-op,不因历史源不可读而失败;空 sessions 保持 no-op。
600
- * 证不出的 body 原字节经 unprojectedRaw 原样留存。
601
- * Persistence appends one immutable commit through the Sitian appender seam.
234
+ * Append dialogue projection from LLM-submitted bounds into records.jsonl.
235
+ * #1090: 纯追加——不回读已成录历史查重、不折叠、不刷新;同一区间再次提交也留下新的追加记录。
236
+ * 证不出的 body 原字节原样留卷。
602
237
  */
603
238
  export async function reprojectTicketProvenance(input) {
604
- const prior = await readTicketProvenance(input.ticketNumber, input.cwd, input.home, input.runDirectory);
239
+ const recordInput = ticketProvenanceRecordInput(input.ticketNumber, input.cwd, input.home, input.runDirectory);
240
+ const resolved = resolveSitianRecordPath(recordInput);
605
241
  if (input.sessions.length === 0) {
606
- const now = new Date().toISOString();
607
- return {
608
- recordFile: prior.recordFile,
609
- header: prior.header ?? {
610
- repo: resolveBookKeyFromGit(input.cwd),
611
- ticket: input.ticketNumber,
612
- createdAt: now,
613
- updatedAt: now,
614
- sessions: [],
615
- },
616
- lines: prior.lines,
617
- };
618
- }
619
- const merged = mergeSessionBounds(prior.header?.sessions, input.sessions);
620
- const deltas = undeclaredSessionRanges(prior.header?.sessions, merged.sessions);
621
- if (deltas.length === 0) {
622
- const now = new Date().toISOString();
623
242
  return {
624
- recordFile: prior.recordFile,
625
- header: prior.header ?? {
626
- repo: resolveBookKeyFromGit(input.cwd),
627
- ticket: input.ticketNumber,
628
- createdAt: now,
629
- updatedAt: now,
630
- sessions: merged.sessions,
631
- },
632
- lines: prior.lines,
243
+ recordFile: resolved.recordFile,
244
+ lines: [],
633
245
  };
634
246
  }
635
- const seenIds = new Set(prior.lines.flatMap((line) => line.id === undefined ? [] : [dialogueIdentity(line.s, line.id)]));
636
- const fresh = [];
637
- const raw = [];
638
- for (const delta of deltas) {
247
+ const entries = [];
248
+ for (let s = 0; s < input.sessions.length; s += 1) {
249
+ const session = input.sessions[s];
639
250
  const projected = await projectSessionRanges({
640
- s: delta.s,
641
- session: delta.session,
642
- seenIds,
643
- coveredSourceIdentities: prior.sourceIdentities.get(delta.s) ?? new Set(),
644
- legacyCoveredSourcePositions: prior.legacySourcePositions.get(delta.s) ?? new Set(),
251
+ s,
252
+ session,
645
253
  ...(input.home === undefined ? {} : { home: input.home }),
646
254
  });
647
- fresh.push(...prior.lines.flatMap((line) => {
648
- if (line.s !== delta.s)
649
- return [];
650
- const sourceIdentity = line.id === undefined
651
- ? line.sourceIdentity
652
- : `id\u0000${line.id}`;
653
- if (sourceIdentity === undefined)
654
- return [];
655
- const sourcePosition = projected.sourcePositionByIdentity.get(sourceIdentity);
656
- return sourcePosition === undefined ? [] : [{ ...line, sourcePosition }];
657
- }));
658
- fresh.push(...projected.lines);
659
- raw.push(...projected.raw);
255
+ entries.push(...projected.entries);
660
256
  }
661
- const lines = mergeFreshIntoCarried(prior.lines, fresh);
662
- const now = new Date().toISOString();
663
- const header = {
664
- repo: prior.header?.repo ?? resolveBookKeyFromGit(input.cwd),
665
- ticket: input.ticketNumber,
666
- createdAt: prior.header?.createdAt ?? now,
667
- updatedAt: now,
668
- sessions: merged.sessions,
669
- };
670
- // One immutable projection commit. Its deterministic identity makes retries of
671
- // the same logical increment converge, while unrelated concurrent increments
672
- // append independently and are folded by readTicketProvenance.
673
- const identityMaterial = JSON.stringify({
674
- ticket: input.ticketNumber,
675
- deltas: deltas.map(({ s, session }) => ({
676
- s,
677
- path: physicalPathIdentity(session.path),
678
- ranges: session.ranges,
679
- })),
680
- lines: [...fresh, ...raw],
681
- });
682
- const identity = `ticket-provenance:${createHash("sha256").update(identityMaterial).digest("hex")}`;
683
257
  const pointer = appendSitianRecord({
684
- ...ticketProvenanceRecordInput(input.ticketNumber, input.cwd, input.home, input.runDirectory),
685
- identity,
258
+ ...recordInput,
686
259
  payload: {
687
260
  type: "ticket-provenance-append",
688
- sessions: merged.sessions,
689
- lines: [...fresh, ...raw],
261
+ sessions: input.sessions,
262
+ lines: entries,
690
263
  },
691
264
  });
692
- const folded = await readTicketProvenance(input.ticketNumber, input.cwd, input.home, input.runDirectory);
693
265
  return {
694
266
  recordFile: pointer.recordFile,
695
- header: folded.header ?? header,
696
- lines: folded.lines,
267
+ lines: entries.filter((entry) => "speaker" in entry),
697
268
  };
698
269
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.5212",
3
+ "version": "0.1.5216",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/src/diarist.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * 起居郎 volume commit — ADR 0075 `diarist-is-role` / #901.
3
- * LLM submits bounds; mechanical layer reprojects the
4
- * unique records.jsonl from session volumes. No human view. No lifecycle here
5
- * (ADR 0018): the seat prepares, the role envelope commits.
2
+ * 起居郎 volume commit — ADR 0075 `diarist-is-role` / #901 / #1090.
3
+ * LLM submits bounds; mechanical layer appends a projection commit from
4
+ * session volumes (pure append, no cross-submit dedupe). No human view.
5
+ * No lifecycle here (ADR 0018): the seat prepares, the role envelope commits.
6
6
  */
7
7
  import { reprojectTicketProvenance } from "./ticket-provenance.ts";
8
8
  import type { TicketProvenanceSession } from "./ticket-provenance-contracts.ts";
@@ -19,7 +19,7 @@ export type DiaristCommitFacts = {
19
19
  };
20
20
 
21
21
  /**
22
- * Reproject the unique diary from LLM-submitted bounds.
22
+ * Append a diary projection from LLM-submitted bounds (#1090).
23
23
  * Shape projection of sessions is the caller's job (accept hook);
24
24
  * this seam only copies source facts into the volume.
25
25
  */