@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.
- package/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/dist/acp-host/description.js +2 -3
- package/dist/acp-host/production-host.js +333 -258
- package/dist/auditor-soul.js +8 -1
- package/dist/diarist-contracts.js +2 -11
- package/dist/headless-host/description.js +3 -3
- package/dist/headless-host/production-host.js +340 -227
- package/dist/host-descriptions.js +3 -18
- package/dist/method-host-plugin/.claude-plugin/plugin.json +5 -0
- package/dist/method-host-plugin/skills/ak-cross-m-review/CONTEXT.md +48 -0
- package/dist/method-host-plugin/skills/ak-cross-m-review/LICENSE +21 -0
- package/dist/method-host-plugin/skills/ak-cross-m-review/SKILL.md +170 -0
- package/dist/method-host-plugin/skills/ak-cross-m-review/prompts/cmr-completeness.md +118 -0
- package/dist/method-host-plugin/skills/ak-cross-m-review/prompts/cmr-reviewer.md +128 -0
- package/dist/method-host-plugin/skills/ak-cross-m-review/provenance.json +41 -0
- package/dist/method-host-plugin/skills/diagnosing-bugs/SKILL.md +134 -0
- package/dist/method-host-plugin/skills/diagnosing-bugs/agents/openai.yaml +3 -0
- package/dist/method-host-plugin/skills/diagnosing-bugs/provenance.json +31 -0
- package/dist/method-host-plugin/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
- package/dist/method-host-plugin/skills/resolving-merge-conflicts/SKILL.md +14 -0
- package/dist/method-host-plugin/skills/resolving-merge-conflicts/agents/openai.yaml +3 -0
- package/dist/method-host-plugin/skills/resolving-merge-conflicts/provenance.json +26 -0
- package/dist/method-host-plugin/skills/tdd/SKILL.md +38 -0
- package/dist/method-host-plugin/skills/tdd/agents/openai.yaml +3 -0
- package/dist/method-host-plugin/skills/tdd/mocking.md +59 -0
- package/dist/method-host-plugin/skills/tdd/provenance.json +36 -0
- package/dist/method-host-plugin/skills/tdd/tests.md +77 -0
- package/dist/public-cli/main.js +14 -21
- package/dist/session-opening-materials.js +17 -5
- package/dist/ticket-provenance-contracts.js +6 -25
- package/dist/ticket-provenance.js +223 -90
- package/extensions/role-runtime.ts +16 -6
- package/package.json +1 -1
- package/resources/method-host-plugin/.claude-plugin/plugin.json +5 -0
- package/scripts/build-package.mjs +6 -1
- package/src/acp-host/description.ts +2 -4
- package/src/acp-host/production-host.ts +0 -1
- package/src/auditor-soul.ts +10 -1
- package/src/diarist-contracts.ts +1 -19
- package/src/diarist-role.ts +3 -31
- package/src/diarist.ts +5 -19
- package/src/headless-host/description.ts +4 -3
- package/src/headless-host/role-turn-host.ts +27 -4
- package/src/host-descriptions.ts +3 -23
- package/src/host-native-method.ts +67 -0
- package/src/ledger-session-read.ts +1 -2
- package/src/role-envelope.ts +17 -47
- package/src/role-runtime-dependencies.ts +17 -2
- package/src/role-runtime.ts +14 -31
- package/src/session-opening-materials.ts +27 -11
- package/src/ticket-provenance-contracts.ts +11 -38
- package/src/ticket-provenance.ts +228 -109
package/src/ticket-provenance.ts
CHANGED
|
@@ -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
|
|
169
|
-
|
|
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 {
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
|
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: {
|
|
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
|
|
396
|
-
|
|
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
|
-
*
|
|
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
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (
|
|
447
|
-
|
|
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
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
582
|
+
facts.push({ position, identity });
|
|
455
583
|
}
|
|
456
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
567
|
-
seenIds.
|
|
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
|
-
|
|
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,
|
|
705
|
+
return { lines, raw, sourcePositionByIdentity };
|
|
580
706
|
}
|
|
581
707
|
|
|
582
708
|
/**
|
|
583
|
-
* Reproject the unique diary from cumulative bounds
|
|
584
|
-
* #918
|
|
585
|
-
*
|
|
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) =>
|
|
755
|
+
prior.lines.flatMap((line) =>
|
|
756
|
+
line.id === undefined ? [] : [dialogueIdentity(line.s, line.id)]
|
|
757
|
+
),
|
|
644
758
|
);
|
|
645
759
|
const fresh: TicketProvenanceLine[] = [];
|
|
646
|
-
const
|
|
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
|
-
|
|
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
|
}
|