@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.
- package/dist/acp-host/production-host.js +193 -151
- package/dist/diarist-contracts.js +2 -11
- package/dist/headless-host/production-host.js +193 -151
- package/dist/ticket-provenance-contracts.js +6 -25
- package/dist/ticket-provenance.js +223 -90
- package/package.json +1 -1
- package/src/diarist-contracts.ts +1 -19
- package/src/diarist-role.ts +3 -31
- package/src/diarist.ts +5 -19
- package/src/ledger-session-read.ts +1 -2
- package/src/role-runtime.ts +2 -24
- package/src/ticket-provenance-contracts.ts +11 -38
- package/src/ticket-provenance.ts +228 -109
|
@@ -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
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
561
|
+
const identity = dialogueIdentity(input.s, event.id);
|
|
562
|
+
if (seenIds.has(identity))
|
|
425
563
|
continue;
|
|
426
|
-
seenIds.add(
|
|
564
|
+
seenIds.add(identity);
|
|
427
565
|
}
|
|
428
566
|
lines.push({
|
|
429
567
|
speaker: event.speaker,
|
|
430
568
|
s: input.s,
|
|
431
|
-
|
|
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,
|
|
576
|
+
return { lines, raw, sourcePositionByIdentity };
|
|
439
577
|
}
|
|
440
578
|
/**
|
|
441
|
-
* Reproject the unique diary from cumulative bounds
|
|
442
|
-
* #918
|
|
443
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
package/src/diarist-contracts.ts
CHANGED
|
@@ -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
|
|
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
|
-
}
|
package/src/diarist-role.ts
CHANGED
|
@@ -18,7 +18,7 @@ export { validateRecordedDiaristOutput };
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* 起居郎交卷形状;形状指引,非 schema 闸。
|
|
21
|
-
* #901:交边界(sessions
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
34
|
-
* Shape projection of sessions
|
|
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
|
|
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
|
*/
|