@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.
- package/dist/acp-host/production-host.js +30 -420
- package/dist/headless-host/production-host.js +30 -420
- package/dist/migrate-book-topology.js +0 -1
- package/dist/public-cli/main.js +0 -1
- package/dist/ticket-provenance-contracts.js +4 -42
- package/dist/ticket-provenance.js +27 -456
- package/package.json +1 -1
- package/src/diarist.ts +5 -5
- package/src/ticket-provenance-contracts.ts +10 -48
- package/src/ticket-provenance.ts +33 -499
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 起居录 volume helpers — ADR 0075
|
|
2
|
+
* 起居录 volume helpers — ADR 0075 / ADR 0081 / ADR 0086 / #1090.
|
|
3
3
|
* 一册=一个追加式 records.jsonl。每轮新投影经司天台 appender 追加为不可变提交;
|
|
4
|
-
*
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
231
|
+
return { entries };
|
|
595
232
|
}
|
|
596
233
|
/**
|
|
597
|
-
*
|
|
598
|
-
* #
|
|
599
|
-
*
|
|
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
|
|
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:
|
|
625
|
-
|
|
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
|
|
636
|
-
|
|
637
|
-
|
|
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
|
|
641
|
-
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
|
-
|
|
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
|
-
...
|
|
685
|
-
identity,
|
|
258
|
+
...recordInput,
|
|
686
259
|
payload: {
|
|
687
260
|
type: "ticket-provenance-append",
|
|
688
|
-
sessions:
|
|
689
|
-
lines:
|
|
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
|
-
|
|
696
|
-
lines: folded.lines,
|
|
267
|
+
lines: entries.filter((entry) => "speaker" in entry),
|
|
697
268
|
};
|
|
698
269
|
}
|
package/package.json
CHANGED
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
|
|
4
|
-
*
|
|
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
|
-
*
|
|
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
|
*/
|