@akagilnc/pi-workflow-roles 0.1.4422 → 0.1.4444

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.
@@ -87,6 +87,24 @@ export function resolveTicketProvenanceVolume(ticketNumber, cwd, home) {
87
87
  const path = resolveSitianRecordPath(ticketProvenanceRecordInput(ticketNumber, cwd, home));
88
88
  return { recordFile: path.recordFile, volumeDir: path.sessionDir };
89
89
  }
90
+ function projectTicketProvenanceRaw(value) {
91
+ if (typeof value !== "object" || value === null || Array.isArray(value))
92
+ return undefined;
93
+ const raw = value;
94
+ if (typeof raw.raw !== "string" || !Number.isInteger(raw.s) || raw.s < 0 ||
95
+ !Number.isInteger(raw.sourcePosition) || raw.sourcePosition < 0) {
96
+ return undefined;
97
+ }
98
+ const sourceIdentity = typeof raw.sourceIdentity === "string" && raw.sourceIdentity !== ""
99
+ ? raw.sourceIdentity
100
+ : undefined;
101
+ return {
102
+ raw: raw.raw,
103
+ s: raw.s,
104
+ sourcePosition: raw.sourcePosition,
105
+ ...(sourceIdentity === undefined ? {} : { sourceIdentity }),
106
+ };
107
+ }
90
108
  function projectTicketProvenanceCommit(value) {
91
109
  if (typeof value !== "object" || value === null || Array.isArray(value))
92
110
  return undefined;
@@ -105,10 +123,19 @@ function projectTicketProvenanceCommit(value) {
105
123
  return undefined;
106
124
  const lines = [];
107
125
  for (const raw of body.lines) {
126
+ if (typeof raw === "string") {
127
+ lines.push(raw);
128
+ continue;
129
+ }
108
130
  const line = projectTicketProvenanceLine(raw);
109
- if (line === undefined)
131
+ if (line !== undefined) {
132
+ lines.push(line);
133
+ continue;
134
+ }
135
+ const preserved = projectTicketProvenanceRaw(raw);
136
+ if (preserved === undefined)
110
137
  return undefined;
111
- lines.push(line);
138
+ lines.push(preserved);
112
139
  }
113
140
  return { timestamp: record.timestamp, sessions, lines };
114
141
  }
@@ -121,7 +148,14 @@ export async function readTicketProvenance(ticketNumber, cwd, home) {
121
148
  }
122
149
  catch (error) {
123
150
  if (error.code === "ENOENT") {
124
- return { header: undefined, lines: [], unprojectedRaw: [], recordFile };
151
+ return {
152
+ header: undefined,
153
+ lines: [],
154
+ unprojectedRaw: [],
155
+ recordFile,
156
+ sourceIdentities: new Map(),
157
+ legacySourcePositions: new Map(),
158
+ };
125
159
  }
126
160
  throw error;
127
161
  }
@@ -129,6 +163,33 @@ export async function readTicketProvenance(ticketNumber, cwd, home) {
129
163
  let header;
130
164
  const lines = [];
131
165
  const unprojectedRaw = [];
166
+ const sourceIdentities = new Map();
167
+ const legacySourcePositions = new Map();
168
+ const rememberIdentity = (s, identity) => {
169
+ const seen = sourceIdentities.get(s) ?? new Set();
170
+ const fresh = !seen.has(identity);
171
+ seen.add(identity);
172
+ sourceIdentities.set(s, seen);
173
+ return fresh;
174
+ };
175
+ const rememberLegacyPosition = (s, position) => {
176
+ const seen = legacySourcePositions.get(s) ?? new Set();
177
+ seen.add(position);
178
+ legacySourcePositions.set(s, seen);
179
+ };
180
+ const remapCoverage = (coverage, priorIndexes) => {
181
+ const normalized = new Map();
182
+ for (const [s, values] of coverage) {
183
+ const target = priorIndexes[s] ?? s;
184
+ const union = normalized.get(target) ?? new Set();
185
+ for (const value of values)
186
+ union.add(value);
187
+ normalized.set(target, union);
188
+ }
189
+ coverage.clear();
190
+ for (const [s, values] of normalized)
191
+ coverage.set(s, values);
192
+ };
132
193
  let sawFirst = false;
133
194
  for (let index = 0; index < physical.length; index += 1) {
134
195
  const raw = physical[index];
@@ -158,10 +219,34 @@ export async function readTicketProvenance(ticketNumber, cwd, home) {
158
219
  const commit = projectTicketProvenanceCommit(parsed);
159
220
  if (commit !== undefined) {
160
221
  const merged = mergeSessionBounds(header?.sessions, commit.sessions);
161
- const remapped = commit.lines.map((entry) => ({
162
- ...entry,
163
- s: merged.incomingIndexes[entry.s] ?? entry.s,
164
- }));
222
+ remapCoverage(sourceIdentities, merged.priorIndexes);
223
+ remapCoverage(legacySourcePositions, merged.priorIndexes);
224
+ const remapped = [];
225
+ for (const entry of commit.lines) {
226
+ if (typeof entry === "string") {
227
+ unprojectedRaw.push(entry);
228
+ continue;
229
+ }
230
+ const s = merged.incomingIndexes[entry.s] ?? merged.priorIndexes[entry.s] ?? entry.s;
231
+ if ("raw" in entry) {
232
+ if (entry.sourceIdentity !== undefined) {
233
+ if (rememberIdentity(s, entry.sourceIdentity))
234
+ unprojectedRaw.push(entry.raw);
235
+ }
236
+ else {
237
+ rememberLegacyPosition(s, entry.sourcePosition);
238
+ unprojectedRaw.push(entry.raw);
239
+ }
240
+ continue;
241
+ }
242
+ remapped.push({ ...entry, s });
243
+ if (entry.id === undefined) {
244
+ if (entry.sourceIdentity !== undefined)
245
+ rememberIdentity(s, entry.sourceIdentity);
246
+ else if (entry.sourcePosition !== undefined)
247
+ rememberLegacyPosition(s, entry.sourcePosition);
248
+ }
249
+ }
165
250
  const now = commit.timestamp;
166
251
  header = {
167
252
  repo: header?.repo ?? resolveBookKeyFromGit(cwd),
@@ -180,7 +265,22 @@ export async function readTicketProvenance(ticketNumber, cwd, home) {
180
265
  // Unknown stock / damaged rows remain readable and are never upgraded in place.
181
266
  unprojectedRaw.push(raw);
182
267
  }
183
- return { header, lines, unprojectedRaw, recordFile };
268
+ for (const line of lines) {
269
+ if (line.id !== undefined)
270
+ continue;
271
+ if (line.sourceIdentity !== undefined)
272
+ rememberIdentity(line.s, line.sourceIdentity);
273
+ else if (line.sourcePosition !== undefined)
274
+ rememberLegacyPosition(line.s, line.sourcePosition);
275
+ }
276
+ return {
277
+ header,
278
+ lines,
279
+ unprojectedRaw,
280
+ recordFile,
281
+ sourceIdentities,
282
+ legacySourcePositions,
283
+ };
184
284
  }
185
285
  /**
186
286
  * Ensure the per-ticket directory + volume file exist (ADR 0075 ticket-provenance-file).
@@ -218,10 +318,10 @@ function resolveBoundIndex(bound, sessionLines) {
218
318
  }
219
319
  return undefined;
220
320
  }
221
- function amendmentKey(s, line) {
222
- return `${s}:${line}`;
223
- }
224
321
  /** Exact range identity for cumulative header dedupe (idempotent resubmit). */
322
+ function dialogueIdentity(s, id) {
323
+ return `${s}\u0000${id}`;
324
+ }
225
325
  function rangeDeclarationKey(range) {
226
326
  return JSON.stringify({
227
327
  from: range.from,
@@ -267,7 +367,7 @@ function mergeSessionBounds(prior, incoming) {
267
367
  }
268
368
  /**
269
369
  * #918 第一节:已投影行即卷宗。只对 prior 未声明的 range(按 path identity + 精确
270
- * from/to)读源;已声明的按 s,line 结转,不重读历史源。
370
+ * from/to)读源;已声明的投影原样结转,不重读历史源。
271
371
  */
272
372
  function undeclaredSessionRanges(prior, merged) {
273
373
  const priorKeys = new Map();
@@ -284,7 +384,8 @@ function undeclaredSessionRanges(prior, merged) {
284
384
  const out = [];
285
385
  for (let s = 0; s < merged.length; s += 1) {
286
386
  const session = merged[s];
287
- const declared = priorKeys.get(physicalPathIdentity(session.path)) ?? new Set();
387
+ const identity = physicalPathIdentity(session.path);
388
+ const declared = priorKeys.get(identity) ?? new Set();
288
389
  const fresh = session.ranges.filter((range) => !declared.has(rangeDeclarationKey(range)));
289
390
  if (fresh.length === 0)
290
391
  continue;
@@ -301,46 +402,85 @@ function undeclaredSessionRanges(prior, merged) {
301
402
  }
302
403
  return out;
303
404
  }
304
- /** Sort archive rows by session index then source line (stable within equal keys). */
305
- function compareLinePosition(left, right) {
306
- if (left.s !== right.s)
307
- return left.s - right.s;
308
- const leftLine = left.line ?? Number.MAX_SAFE_INTEGER;
309
- const rightLine = right.line ?? Number.MAX_SAFE_INTEGER;
310
- return leftLine - rightLine;
311
- }
312
405
  /**
313
406
  * Merge newly projected rows into the carried archive.
314
- * 既有卷宗按 (s,line) 优先;fresh 不得复制无 id 行(#918 C2/G1)。
315
- * id 去重仍保留(跨 path 同 id 首见胜)。
407
+ * Native id remains the typed first-seen identity; exact range resubmission is a no-op.
316
408
  */
317
409
  function mergeFreshIntoCarried(carried, fresh) {
318
- const seenIds = new Set();
319
- const seenPositions = new Set();
320
- const out = [];
321
- for (const line of carried) {
322
- if (line.id !== undefined)
323
- seenIds.add(line.id);
324
- if (line.line !== undefined)
325
- seenPositions.add(amendmentKey(line.s, line.line));
326
- out.push(line);
327
- }
328
- for (const line of fresh) {
329
- if (line.id !== undefined) {
330
- if (seenIds.has(line.id))
410
+ const seenSources = new Map();
411
+ const bySession = new Map();
412
+ const absorb = (lines) => {
413
+ for (const line of lines) {
414
+ const sourceIdentity = line.id === undefined
415
+ ? line.sourceIdentity
416
+ : `id\u0000${line.id}`;
417
+ const identity = sourceIdentity === undefined
418
+ ? undefined
419
+ : `${line.s}\u0000${sourceIdentity}`;
420
+ const seen = identity === undefined ? undefined : seenSources.get(identity);
421
+ if (seen !== undefined) {
422
+ if (line.sourcePosition !== undefined) {
423
+ Object.assign(seen, { sourcePosition: line.sourcePosition });
424
+ }
331
425
  continue;
332
- seenIds.add(line.id);
426
+ }
427
+ const bucket = bySession.get(line.s) ?? [];
428
+ bucket.push({ ...line });
429
+ if (identity !== undefined)
430
+ seenSources.set(identity, bucket.at(-1));
431
+ bySession.set(line.s, bucket);
333
432
  }
334
- if (line.line !== undefined) {
335
- const position = amendmentKey(line.s, line.line);
336
- if (seenPositions.has(position))
337
- continue;
338
- seenPositions.add(position);
433
+ };
434
+ absorb(carried);
435
+ absorb(fresh);
436
+ return [...bySession.entries()]
437
+ .sort(([left], [right]) => left - right)
438
+ .flatMap(([, lines]) => lines
439
+ .map((line, archiveIndex) => ({ line, archiveIndex }))
440
+ .sort((left, right) => {
441
+ const leftPosition = left.line.sourcePosition;
442
+ const rightPosition = right.line.sourcePosition;
443
+ if (leftPosition === undefined && rightPosition === undefined) {
444
+ return left.archiveIndex - right.archiveIndex;
445
+ }
446
+ if (leftPosition === undefined)
447
+ return 1;
448
+ if (rightPosition === undefined)
449
+ return -1;
450
+ return leftPosition - rightPosition || left.archiveIndex - right.archiveIndex;
451
+ })
452
+ .map(({ line }) => line));
453
+ }
454
+ /**
455
+ * Give every logical source row a replay-invariant ordinal. Native ids identify
456
+ * replayed rows directly; idless rows use their ordinal after the preceding
457
+ * native id. Neither key inspects dialogue text or physical line numbers.
458
+ */
459
+ function stableSourceFacts(sessionLines) {
460
+ const positionByIdentity = new Map();
461
+ const facts = [];
462
+ let precedingId = "<start>";
463
+ let idlessOffset = 0;
464
+ for (const entry of sessionLines) {
465
+ const id = entry.row === undefined ? undefined : nativeEventId(entry.row);
466
+ if (id !== undefined) {
467
+ precedingId = id;
468
+ idlessOffset = 0;
469
+ }
470
+ else {
471
+ idlessOffset += 1;
339
472
  }
340
- out.push(line);
473
+ const identity = id === undefined
474
+ ? `after\u0000${precedingId}\u0000${idlessOffset}`
475
+ : `id\u0000${id}`;
476
+ let position = positionByIdentity.get(identity);
477
+ if (position === undefined) {
478
+ position = positionByIdentity.size;
479
+ positionByIdentity.set(identity, position);
480
+ }
481
+ facts.push({ position, identity });
341
482
  }
342
- out.sort(compareLinePosition);
343
- return out;
483
+ return facts;
344
484
  }
345
485
  /**
346
486
  * Resolve submitted ranges against the session, then sort by source position and
@@ -396,52 +536,49 @@ async function projectSessionRanges(input) {
396
536
  }
397
537
  const rows = sessionLines.map((entry) => entry.row);
398
538
  const dialogue = adaptSessionDialogue(rows);
539
+ const sourceFacts = stableSourceFacts(sessionLines);
540
+ const sourcePositionByIdentity = new Map();
541
+ for (const fact of sourceFacts) {
542
+ sourcePositionByIdentity.set(fact.identity, fact.position);
543
+ }
399
544
  const seenIds = input.seenIds;
400
545
  const lines = [];
401
- const unparsable = [];
546
+ const raw = [];
402
547
  const ranges = normalizeResolvedRanges(input.session.ranges, sessionLines, input.session.path);
403
548
  for (const range of ranges) {
404
549
  for (let index = range.fromIndex; index <= range.toIndex; index += 1) {
550
+ const { position: sourcePosition, identity: sourceIdentity } = sourceFacts[index];
551
+ if (input.coveredSourceIdentities.has(sourceIdentity) ||
552
+ input.legacyCoveredSourcePositions.has(sourcePosition))
553
+ continue;
405
554
  const entry = sessionLines[index];
406
555
  if (entry.row === undefined) {
407
- const key = amendmentKey(input.s, entry.line);
408
- const amendment = input.amendmentsByKey.get(key);
409
- if (amendment !== undefined) {
410
- lines.push({
411
- speaker: amendment.speaker,
412
- s: input.s,
413
- line: entry.line,
414
- text: amendment.text,
415
- });
416
- }
417
- else {
418
- unparsable.push({ s: input.s, line: entry.line, raw: entry.raw });
419
- }
556
+ raw.push({ raw: entry.raw, s: input.s, sourcePosition, sourceIdentity });
420
557
  continue;
421
558
  }
422
559
  for (const event of dialogue[index] ?? []) {
423
560
  if (event.id !== undefined) {
424
- if (seenIds.has(event.id))
561
+ const identity = dialogueIdentity(input.s, event.id);
562
+ if (seenIds.has(identity))
425
563
  continue;
426
- seenIds.add(event.id);
564
+ seenIds.add(identity);
427
565
  }
428
566
  lines.push({
429
567
  speaker: event.speaker,
430
568
  s: input.s,
431
- line: entry.line,
432
- ...(event.id === undefined ? {} : { id: event.id }),
569
+ sourcePosition,
570
+ ...(event.id === undefined ? { sourceIdentity } : { id: event.id }),
433
571
  text: event.text,
434
572
  });
435
573
  }
436
574
  }
437
575
  }
438
- return { lines, unparsable };
576
+ return { lines, raw, sourcePositionByIdentity };
439
577
  }
440
578
  /**
441
- * Reproject the unique diary from cumulative bounds + optional amendments.
442
- * #918:已投影行即卷宗——按 s,line 结转;本轮只读 prior 未声明的 range(或新
443
- * session 区间)。精确重复提交=幂等 no-op,不因历史源不可读而失败。
444
- * amendments 只在本轮读取的新范围确有对应坏行时生效;空 sessions 保持 no-op。
579
+ * Reproject the unique diary from cumulative bounds.
580
+ * #918:已投影行即卷宗;本轮只读 prior 未声明的 range(或新 session 区间)。
581
+ * 精确重复提交=幂等 no-op,不因历史源不可读而失败;空 sessions 保持 no-op。
445
582
  * 证不出的 body 原字节经 unprojectedRaw 原样留存。
446
583
  * Persistence appends one immutable commit through the Sitian appender seam.
447
584
  */
@@ -459,7 +596,6 @@ export async function reprojectTicketProvenance(input) {
459
596
  sessions: [],
460
597
  },
461
598
  lines: prior.lines,
462
- unparsable: [],
463
599
  };
464
600
  }
465
601
  const merged = mergeSessionBounds(prior.header?.sessions, input.sessions);
@@ -476,32 +612,33 @@ export async function reprojectTicketProvenance(input) {
476
612
  sessions: merged.sessions,
477
613
  },
478
614
  lines: prior.lines,
479
- unparsable: [],
480
615
  };
481
616
  }
482
- const amendmentsByKey = new Map();
483
- for (const amendment of input.amendments ?? []) {
484
- const cumulativeIndex = merged.incomingIndexes[amendment.s];
485
- if (cumulativeIndex === undefined)
486
- continue;
487
- amendmentsByKey.set(amendmentKey(cumulativeIndex, amendment.line), {
488
- ...amendment,
489
- s: cumulativeIndex,
490
- });
491
- }
492
- const seenIds = new Set(prior.lines.flatMap((line) => line.id === undefined ? [] : [line.id]));
617
+ const seenIds = new Set(prior.lines.flatMap((line) => line.id === undefined ? [] : [dialogueIdentity(line.s, line.id)]));
493
618
  const fresh = [];
494
- const unparsable = [];
619
+ const raw = [];
495
620
  for (const delta of deltas) {
496
621
  const projected = await projectSessionRanges({
497
622
  s: delta.s,
498
623
  session: delta.session,
499
- amendmentsByKey,
500
624
  seenIds,
625
+ coveredSourceIdentities: prior.sourceIdentities.get(delta.s) ?? new Set(),
626
+ legacyCoveredSourcePositions: prior.legacySourcePositions.get(delta.s) ?? new Set(),
501
627
  ...(input.home === undefined ? {} : { home: input.home }),
502
628
  });
629
+ fresh.push(...prior.lines.flatMap((line) => {
630
+ if (line.s !== delta.s)
631
+ return [];
632
+ const sourceIdentity = line.id === undefined
633
+ ? line.sourceIdentity
634
+ : `id\u0000${line.id}`;
635
+ if (sourceIdentity === undefined)
636
+ return [];
637
+ const sourcePosition = projected.sourcePositionByIdentity.get(sourceIdentity);
638
+ return sourcePosition === undefined ? [] : [{ ...line, sourcePosition }];
639
+ }));
503
640
  fresh.push(...projected.lines);
504
- unparsable.push(...projected.unparsable);
641
+ raw.push(...projected.raw);
505
642
  }
506
643
  const lines = mergeFreshIntoCarried(prior.lines, fresh);
507
644
  const now = new Date().toISOString();
@@ -512,9 +649,6 @@ export async function reprojectTicketProvenance(input) {
512
649
  updatedAt: now,
513
650
  sessions: merged.sessions,
514
651
  };
515
- if (unparsable.length > 0) {
516
- return { recordFile: prior.recordFile, header, lines, unparsable };
517
- }
518
652
  // One immutable projection commit. Its deterministic identity makes retries of
519
653
  // the same logical increment converge, while unrelated concurrent increments
520
654
  // append independently and are folded by readTicketProvenance.
@@ -525,7 +659,7 @@ export async function reprojectTicketProvenance(input) {
525
659
  path: physicalPathIdentity(session.path),
526
660
  ranges: session.ranges,
527
661
  })),
528
- lines: fresh,
662
+ lines: [...fresh, ...raw],
529
663
  });
530
664
  const identity = `ticket-provenance:${createHash("sha256").update(identityMaterial).digest("hex")}`;
531
665
  const pointer = appendSitianRecord({
@@ -534,7 +668,7 @@ export async function reprojectTicketProvenance(input) {
534
668
  payload: {
535
669
  type: "ticket-provenance-append",
536
670
  sessions: merged.sessions,
537
- lines: fresh,
671
+ lines: [...fresh, ...raw],
538
672
  },
539
673
  });
540
674
  const folded = await readTicketProvenance(input.ticketNumber, input.cwd, input.home);
@@ -542,6 +676,5 @@ export async function reprojectTicketProvenance(input) {
542
676
  recordFile: pointer.recordFile,
543
677
  header: folded.header ?? header,
544
678
  lines: folded.lines,
545
- unparsable: [],
546
679
  };
547
680
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.4422",
3
+ "version": "0.1.4444",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -4,13 +4,11 @@
4
4
  * Machine facts about the volume come from the mechanical reproject seam, never
5
5
  * from model self-report (锚定宪法); this module owns the receipt shape only.
6
6
  *
7
- * #901: LLM submits bounds (+ optional amendments); mechanical layer projects.
7
+ * #901: LLM submits bounds; the mechanical layer projects source bytes.
8
8
  */
9
9
 
10
10
  import {
11
- projectTicketProvenanceAmendments,
12
11
  projectTicketProvenanceSessions,
13
- type TicketProvenanceAmendment,
14
12
  type TicketProvenanceSession,
15
13
  } from "./ticket-provenance-contracts.ts";
16
14
  import {
@@ -45,11 +43,6 @@ export type DiaristOutput =
45
43
  * Malformed endpoints → accept hook reasks (`reask-not-explode`).
46
44
  */
47
45
  readonly sessions?: readonly TicketProvenanceSession[];
48
- /**
49
- * Optional amendments for unparsable source lines (s + line + speaker + text).
50
- * Absent = no amendments this turn (not a shape rejection).
51
- */
52
- readonly amendments?: readonly TicketProvenanceAmendment[];
53
46
  }
54
47
  | {
55
48
  /** LLM cannot tell which ticket this summons is about — escalate, never wash into 无录. */
@@ -187,14 +180,3 @@ export function projectDiaristSessions(
187
180
  if (raw === undefined) return [];
188
181
  return projectTicketProvenanceSessions(raw);
189
182
  }
190
-
191
- /**
192
- * Project optional amendments. Absent / non-array → empty (not a rejection).
193
- */
194
- export function projectDiaristAmendments(
195
- value: unknown,
196
- ): readonly TicketProvenanceAmendment[] {
197
- const raw = (value as { amendments?: unknown } | null)?.amendments;
198
- if (raw === undefined) return [];
199
- return projectTicketProvenanceAmendments(raw);
200
- }
@@ -18,7 +18,7 @@ export { validateRecordedDiaristOutput };
18
18
 
19
19
  /**
20
20
  * 起居郎交卷形状;形状指引,非 schema 闸。
21
- * #901:交边界(sessions)+ 可选坏行补写(amendments);正文由机械投影。
21
+ * #901:交边界(sessions);正文与不可解析原字节由机械投影。
22
22
  */
23
23
  export const diaristOutputSchema = withTerminatingOutputDeclarations(
24
24
  openToolObject(
@@ -93,34 +93,6 @@ export const diaristOutputSchema = withTerminatingOutputDeclarations(
93
93
  },
94
94
  ),
95
95
  ),
96
- amendments: Type.Optional(
97
- Type.Array(
98
- Type.Object(
99
- {
100
- s: Type.Optional(
101
- Type.Unknown({ description: "卷下标(sessions 位置)" }),
102
- ),
103
- line: Type.Optional(
104
- Type.Unknown({ description: "源卷 1-based 行号" }),
105
- ),
106
- speaker: Type.Optional(
107
- Type.String({ description: "owner | runner" }),
108
- ),
109
- text: Type.Optional(
110
- Type.String({ description: "补写正文(原话)" }),
111
- ),
112
- },
113
- {
114
- additionalProperties: true,
115
- description: "一条坏行补写;缺字段的成员机械跳过",
116
- },
117
- ),
118
- {
119
- description:
120
- "可选;机械解析不了的行交此补写。缺本字段=无补写,不拒收。",
121
- },
122
- ),
123
- ),
124
96
  }),
125
97
  ),
126
98
  );
@@ -138,7 +110,7 @@ export const DIARIST_TOOL_SPEC = {
138
110
  name: DIARIST_OUTPUT_TOOL_NAME,
139
111
  label: "起居郎输出",
140
112
  description:
141
- "起居郎交本票对话边界(sessions)与可选坏行补写(amendments);认不出本庭对象则 escalate。",
142
- promptSnippet: "起居郎交边界与可选补写",
113
+ "起居郎交本票对话边界(sessions);认不出本庭对象则 escalate。",
114
+ promptSnippet: "起居郎交边界",
143
115
  parameters: diaristOutputSchema,
144
116
  } as const;
package/src/diarist.ts CHANGED
@@ -1,17 +1,11 @@
1
1
  /**
2
2
  * 起居郎 volume commit — ADR 0075 `diarist-is-role` / #901.
3
- * LLM submits bounds (+ optional amendments); mechanical layer reprojects the
3
+ * LLM submits bounds; mechanical layer reprojects the
4
4
  * unique records.jsonl from session volumes. No human view. No lifecycle here
5
5
  * (ADR 0018): the seat prepares, the role envelope commits.
6
6
  */
7
- import {
8
- reprojectTicketProvenance,
9
- type UnparsableSessionLine,
10
- } from "./ticket-provenance.ts";
11
- import type {
12
- TicketProvenanceAmendment,
13
- TicketProvenanceSession,
14
- } from "./ticket-provenance-contracts.ts";
7
+ import { reprojectTicketProvenance } from "./ticket-provenance.ts";
8
+ import type { TicketProvenanceSession } from "./ticket-provenance-contracts.ts";
15
9
 
16
10
  /**
17
11
  * Honest machine facts about what this turn actually committed to the volume.
@@ -22,16 +16,11 @@ export type DiaristCommitFacts = {
22
16
  readonly volumeRecordFile: string;
23
17
  /** Dialogue lines written by this reproject. */
24
18
  readonly lineCount: number;
25
- /**
26
- * Session lines inside the bounds that still have no amendment.
27
- * Accept hook turns a non-empty list into ParentQueueReaskError.
28
- */
29
- readonly unparsable: readonly UnparsableSessionLine[];
30
19
  };
31
20
 
32
21
  /**
33
- * Reproject the unique diary from LLM-submitted bounds + optional amendments.
34
- * Shape projection of sessions/amendments is the caller's job (accept hook);
22
+ * Reproject the unique diary from LLM-submitted bounds.
23
+ * Shape projection of sessions is the caller's job (accept hook);
35
24
  * this seam only copies source facts into the volume.
36
25
  */
37
26
  export async function commitDiaristProjection(input: {
@@ -39,19 +28,16 @@ export async function commitDiaristProjection(input: {
39
28
  readonly cwd: string;
40
29
  readonly home?: string;
41
30
  readonly sessions: readonly TicketProvenanceSession[];
42
- readonly amendments?: readonly TicketProvenanceAmendment[];
43
31
  }): Promise<DiaristCommitFacts> {
44
32
  const result = await reprojectTicketProvenance({
45
33
  ticketNumber: input.ticketNumber,
46
34
  cwd: input.cwd,
47
35
  ...(input.home === undefined ? {} : { home: input.home }),
48
36
  sessions: input.sessions,
49
- ...(input.amendments === undefined ? {} : { amendments: input.amendments }),
50
37
  });
51
38
  return {
52
39
  ticketNumber: input.ticketNumber,
53
40
  volumeRecordFile: result.recordFile,
54
41
  lineCount: result.lines.length,
55
- unparsable: result.unparsable,
56
42
  };
57
43
  }
@@ -40,8 +40,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
40
40
  * One physical JSONL line as the single kernel saw it.
41
41
  * `row` present = syntactically complete session object at that 1-based line.
42
42
  * `error` present = the line is complete but unreadable as a session object;
43
- * `raw` carries its original bytes so a reader that must keep going (起居录
44
- * projection, ADR 0075 `unparsable-line-to-diarist`) can hand the line back
43
+ * `raw` carries its original bytes so readers can preserve source evidence
45
44
  * instead of inventing content. The unfinished final fragment at EOF is not a
46
45
  * line — live tail is honest absence, never a bad line.
47
46
  */