@gotcos/glasses-server 6.94.4 → 6.98.0

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.
Files changed (109) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/README.md +52 -0
  3. package/managed-runtime-contract.json +13 -0
  4. package/package.json +3 -1
  5. package/server/index.ts +19 -1
  6. package/server/lib/attached-provider-adapter.ts +35 -1
  7. package/server/lib/claude-bridge.ts +47 -0
  8. package/server/lib/codex-bridge.ts +11 -1
  9. package/server/lib/codex-rollout-watermark.ts +42 -0
  10. package/server/lib/cursor-bridge.ts +2 -1
  11. package/server/lib/dictation-clean-models.ts +2 -0
  12. package/server/lib/dictation-clean.ts +2 -2
  13. package/server/lib/dictation-polish-pipeline.ts +22 -2
  14. package/server/lib/dictation-prewarm.ts +16 -0
  15. package/server/lib/direct-bridge.ts +14 -2
  16. package/server/lib/direct-sse.ts +6 -1
  17. package/server/lib/display-bus.ts +1 -1
  18. package/server/lib/fork-thread.ts +32 -1
  19. package/server/lib/held-naming-batches.ts +36 -0
  20. package/server/lib/job-audit-caller.ts +20 -0
  21. package/server/lib/lens-gist-engines.ts +17 -5
  22. package/server/lib/live-cos-ask.ts +29 -6
  23. package/server/lib/live-cue-verdicts.ts +43 -8
  24. package/server/lib/live-cues-engine.ts +4 -1
  25. package/server/lib/live-cues-filter.ts +50 -0
  26. package/server/lib/live-cues-general-prompts.ts +137 -0
  27. package/server/lib/live-cues-general-topic.ts +245 -0
  28. package/server/lib/live-cues-general.ts +715 -0
  29. package/server/lib/live-cues-v2-data.ts +453 -0
  30. package/server/lib/live-cues-v2-grounded.ts +262 -0
  31. package/server/lib/live-cues-v2-judge-b.ts +59 -0
  32. package/server/lib/live-cues-v2-names.ts +49 -0
  33. package/server/lib/live-cues-v2-notes.ts +8 -4
  34. package/server/lib/live-cues-v2-open-items.ts +132 -0
  35. package/server/lib/live-cues-v2-pacing.ts +45 -0
  36. package/server/lib/live-cues-v2-prompts-kinds.ts +9 -4
  37. package/server/lib/live-cues-v2-settings.ts +89 -10
  38. package/server/lib/live-cues-v2.ts +570 -47
  39. package/server/lib/mac-recording-asr.ts +6 -3
  40. package/server/lib/mac-speaker-call-human.ts +371 -0
  41. package/server/lib/mac-speaker-call-interval-human.ts +499 -0
  42. package/server/lib/mac-speaker-call-interval-projection.ts +233 -0
  43. package/server/lib/mac-speaker-call-original-view.ts +328 -0
  44. package/server/lib/mac-speaker-call-producer-worker.ts +615 -0
  45. package/server/lib/mac-speaker-call-producer.ts +539 -0
  46. package/server/lib/mac-speaker-call-store.ts +264 -0
  47. package/server/lib/mac-speaker-call-view.ts +373 -0
  48. package/server/lib/mac-speaker-live-compute.ts +72 -0
  49. package/server/lib/mac-speaker-live-memory.ts +414 -0
  50. package/server/lib/mac-speaker-live-projection.ts +126 -0
  51. package/server/lib/mac-speaker-live-save.ts +90 -0
  52. package/server/lib/mac-speaker-live-store.ts +71 -0
  53. package/server/lib/mac-speaker-live-worker.ts +58 -0
  54. package/server/lib/mac-speaker-live.ts +175 -0
  55. package/server/lib/mac-speaker-profile-extractor.ts +15 -10
  56. package/server/lib/mac-speaker-shadow-repair.ts +307 -0
  57. package/server/lib/mac-voice-assignment.ts +44 -26
  58. package/server/lib/mac-voice-display.ts +195 -0
  59. package/server/lib/mac-voice-naming-batch.ts +1067 -0
  60. package/server/lib/mac-voice-suggestions.ts +548 -0
  61. package/server/lib/maintenance-lifecycle.ts +2 -0
  62. package/server/lib/media-store.ts +2 -1
  63. package/server/lib/meeting-ask-pasted.ts +135 -0
  64. package/server/lib/meeting-ask-review.ts +8 -1
  65. package/server/lib/meeting-ask-runner.ts +4 -1
  66. package/server/lib/meeting-ask-runtime.ts +189 -17
  67. package/server/lib/meeting-ask-store.ts +2 -0
  68. package/server/lib/meeting-ask-web.ts +284 -13
  69. package/server/lib/meeting-batch-progress.ts +51 -5
  70. package/server/lib/meeting-batch-transcribe.ts +35 -2
  71. package/server/lib/meeting-fact-check.ts +3 -0
  72. package/server/lib/meeting-finalization-jobs.ts +69 -2
  73. package/server/lib/meeting-public-topic.ts +6 -1
  74. package/server/lib/meeting-speaker-review.ts +182 -17
  75. package/server/lib/meeting-store.ts +6 -1
  76. package/server/lib/parakeet.ts +51 -5
  77. package/server/lib/pending-batch-retention.ts +58 -0
  78. package/server/lib/private-reference.ts +38 -0
  79. package/server/lib/provider-usage.ts +561 -0
  80. package/server/lib/public-https-url.ts +32 -0
  81. package/server/lib/query-job-runtime.ts +3 -0
  82. package/server/lib/rich-media-safety.ts +23 -2
  83. package/server/lib/speaker-shadow-decide.ts +503 -0
  84. package/server/lib/speaker-shadow-models.ts +150 -0
  85. package/server/lib/speaker-shadow-numeric.ts +143 -0
  86. package/server/lib/speaker-shadow-pair-source.ts +91 -0
  87. package/server/lib/speaker-shadow-paired.ts +268 -0
  88. package/server/lib/speaker-shadow-report.ts +113 -0
  89. package/server/lib/speaker-shadow-timeline.ts +210 -0
  90. package/server/lib/speaker-shadow-worker.ts +312 -0
  91. package/server/lib/speaker-shadow.ts +788 -0
  92. package/server/lib/token-audit.ts +36 -6
  93. package/server/lib/transcript-cleanup.ts +3 -2
  94. package/server/lib/voice-directory.ts +8 -4
  95. package/server/lib/web-lookup-ledger.ts +151 -0
  96. package/server/lib/work-review-runtime.ts +2 -0
  97. package/server/lib/work-search.ts +3 -1
  98. package/server/lib/xlsx-text.ts +277 -0
  99. package/server/routes/health.ts +5 -0
  100. package/server/routes/live-cues.ts +36 -14
  101. package/server/routes/meeting.ts +506 -49
  102. package/server/routes/speaker-shadow.ts +79 -0
  103. package/server/routes/transcribe-stream.ts +114 -33
  104. package/server/routes/voice.ts +3 -0
  105. package/server/scripts/live-cues-v3-mutation-gate.mjs +311 -0
  106. package/server/scripts/mac-voice-assignment-6930-mutation-gate.mjs +5 -5
  107. package/shared/live-cues-v2.ts +38 -5
  108. package/shared/media-attachment.ts +2 -0
  109. package/shared/meeting-ask.ts +11 -0
@@ -0,0 +1,233 @@
1
+ /** Pure derived presentation geometry. This module neither admits identities nor
2
+ * authenticates native execution. Its caller must validate the bound recurrence
3
+ * state/source before preparing support and qualify the resulting packet offline. */
4
+ import { createHash } from 'node:crypto'
5
+ import type { MacSpeaker256Group, MacSpeaker256State } from './mac-speaker-live-memory.js'
6
+ import { hasMacSpeakerProtectedRows, isMacSpeakerHumanProtected } from './mac-speaker-protection.js'
7
+ import { pyRoundInt } from './speaker-shadow-numeric.js'
8
+
9
+ type Doc = Record<string, any>
10
+ type RawBinding = MacSpeaker256State['rawBindings'][number]
11
+ export type MacSpeakerCallIntervalMembership = 'accepted-only' | 'native-exclusive-members'
12
+ export interface MacSpeakerCallNativeInterval { startSample: number; endSample: number; nativeCluster: string }
13
+ export interface MacSpeakerCallSupportInterval {
14
+ identityId: string; rawChunkIndex: number; startSample: number; endSample: number
15
+ audioSHA256: string; pcmSHA256: string; evidenceIds: string[]
16
+ origin: 'accepted-interval' | 'native-exclusive-member'
17
+ }
18
+ export interface MacSpeakerCallIntervalSupport {
19
+ version: 1; membership: MacSpeakerCallIntervalMembership
20
+ sourceSHA256: string; nativeSHA256: string; timelinePCMSHA256: string; totalSamples: number
21
+ rawUsedSamples: number[]; nativeIntervals: MacSpeakerCallNativeInterval[]
22
+ intervals: MacSpeakerCallSupportInterval[]; supportSHA256: string
23
+ }
24
+ export interface MacSpeakerCallIntervalContext {
25
+ rawBindings: readonly RawBinding[]; groups: readonly MacSpeaker256Group[]
26
+ protectedRawChunkIndices: readonly number[]; displayLabels: Readonly<Record<string, string>>
27
+ }
28
+ /** Exact HQ words wholly supported by one accepted identity. These coordinates
29
+ * are issued by the projector, never supplied as a caller's naming selector. */
30
+ export interface MacSpeakerCallHQWordBinding {
31
+ batchIndex: number; wordIndex: number; identityId: string
32
+ startElapsedMs: number; endElapsedMs: number; rawChunkIndices: number[]
33
+ pieces: Array<{ rawChunkIndex: number; startSample: number; endSample: number; audioSHA256: string; pcmSHA256: string; originalRowSHA256: string }>
34
+ }
35
+ /** Source geometry for explicit human word review. It carries no acoustic
36
+ * identity or admission claim and includes named/unknown source labels. */
37
+ export type MacSpeakerCallHQWordSource = Omit<MacSpeakerCallHQWordBinding, 'identityId'>
38
+ const HASH = /^[a-f0-9]{64}$/
39
+ const GENERIC = /^(?:Voice [1-9]\d?|Ext|Unknown|Unidentified(?: \d+)?|Speaker \d+)$/
40
+ const digest = (v: unknown) => createHash('sha256').update(JSON.stringify(v)).digest('hex')
41
+ const fail = (reason: string): never => { throw new Error(`Mac call interval view: ${reason}`) }
42
+ const range = (lo: number, hi: number) => Number.isSafeInteger(lo) && Number.isSafeInteger(hi) && lo >= 0 && hi > lo
43
+
44
+ /** Sweep ALL native clusters, including withheld identities. A competing native
45
+ * cluster removes support even if it never passed recurrence admission. */
46
+ function exclusive(intervals: readonly MacSpeakerCallNativeInterval[]): MacSpeakerCallNativeInterval[] {
47
+ const events = new Map<number, Array<[string, number]>>()
48
+ for (const row of intervals) for (const [point, delta] of [[row.startSample, 1], [row.endSample, -1]]) {
49
+ const list = events.get(point) ?? []; list.push([row.nativeCluster, delta]); events.set(point, list)
50
+ }
51
+ const points = [...events.keys()].sort((a, b) => a - b), active = new Map<string, number>(), out: MacSpeakerCallNativeInterval[] = []
52
+ for (let i = 0; i + 1 < points.length; i++) {
53
+ const lo = points[i], hi = points[i + 1]
54
+ for (const [cluster, delta] of events.get(lo)!) { const count = (active.get(cluster) ?? 0) + delta; if (count) active.set(cluster, count); else active.delete(cluster) }
55
+ if (active.size !== 1) continue
56
+ const nativeCluster = active.keys().next().value!, prior = out.at(-1)
57
+ if (prior?.nativeCluster === nativeCluster && prior.endSample === lo) prior.endSample = hi
58
+ else out.push({ startSample: lo, endSample: hi, nativeCluster })
59
+ }
60
+ return out
61
+ }
62
+
63
+ function expectedIntervals(context: MacSpeakerCallIntervalContext, support: Omit<MacSpeakerCallIntervalSupport, 'intervals' | 'supportSHA256'>): MacSpeakerCallSupportInterval[] {
64
+ const protectedRaw = new Set(context.protectedRawChunkIndices), admitted = new Map(context.groups.map(g => [g.nativeCluster, g]))
65
+ if (admitted.size !== context.groups.length) fail('duplicate admitted native cluster')
66
+ const native = exclusive(support.nativeIntervals), out: MacSpeakerCallSupportInterval[] = []
67
+ const add = (g: MacSpeaker256Group, r: RawBinding, startSample: number, endSample: number, origin: MacSpeakerCallSupportInterval['origin']) => {
68
+ if (protectedRaw.has(r.rawChunkIndex)) return
69
+ if (!range(startSample, endSample) || startSample < r.startSample || endSample > r.startSample + support.rawUsedSamples[r.rawChunkIndex]) fail('unsupported raw sample range')
70
+ out.push({ identityId: g.identityId, rawChunkIndex: r.rawChunkIndex, startSample, endSample, audioSHA256: r.audioSHA256, pcmSHA256: r.pcmSHA256, evidenceIds: g.witnesses.map(w => w.evidenceId), origin })
71
+ }
72
+ if (support.membership === 'accepted-only') {
73
+ for (const g of context.groups) for (const span of g.intervals) {
74
+ const r = context.rawBindings[span.rawChunkIndex]
75
+ if (!r || span.identityId !== g.identityId || span.audioSHA256 !== r.audioSHA256 || span.pcmSHA256 !== r.pcmSHA256) fail('accepted interval binding')
76
+ // Re-check exact native exclusivity instead of accepting a caller's span.
77
+ const cover = native.find(n => n.nativeCluster === g.nativeCluster && n.startSample <= span.startSample && n.endSample >= span.endSample)
78
+ if (!cover) fail('accepted interval outside exclusive native support')
79
+ add(g, r, span.startSample, span.endSample, 'accepted-interval')
80
+ }
81
+ } else {
82
+ for (const n of native) {
83
+ const g = admitted.get(n.nativeCluster); if (!g) continue
84
+ for (const r of context.rawBindings) {
85
+ const lo = Math.max(n.startSample, r.startSample), hi = Math.min(n.endSample, r.startSample + support.rawUsedSamples[r.rawChunkIndex])
86
+ if (hi > lo) add(g, r, lo, hi, 'native-exclusive-member')
87
+ }
88
+ }
89
+ }
90
+ return out.sort((a, b) => a.rawChunkIndex - b.rawChunkIndex || a.startSample - b.startSample || a.endSample - b.endSample || a.identityId.localeCompare(b.identityId))
91
+ }
92
+
93
+ function checkSource(context: MacSpeakerCallIntervalContext, support: Omit<MacSpeakerCallIntervalSupport, 'intervals' | 'supportSHA256'>): void {
94
+ if (support.version !== 1 || !['accepted-only', 'native-exclusive-members'].includes(support.membership) || !HASH.test(support.sourceSHA256) || !HASH.test(support.nativeSHA256) || !HASH.test(support.timelinePCMSHA256) || !Number.isSafeInteger(support.totalSamples) || support.totalSamples <= 0 || support.totalSamples > 4 * 3600 * 16000) fail('support identity')
95
+ if (!Array.isArray(support.rawUsedSamples) || support.rawUsedSamples.length !== context.rawBindings.length || !Array.isArray(support.nativeIntervals) || support.nativeIntervals.length > 20000 || context.rawBindings.length > 6000 || context.groups.length > 99) fail('support bounds')
96
+ for (const [i, r] of context.rawBindings.entries()) if (r.rawChunkIndex !== i || !HASH.test(r.audioSHA256) || !HASH.test(r.pcmSHA256) || !HASH.test(r.originalRowSHA256) || !range(r.startSample, r.endSample) || r.endSample > support.totalSamples || !Number.isSafeInteger(support.rawUsedSamples[i]) || support.rawUsedSamples[i] <= 0 || support.rawUsedSamples[i] > r.endSample - r.startSample) fail('raw PCM binding')
97
+ if (new Set(context.protectedRawChunkIndices).size !== context.protectedRawChunkIndices.length || context.protectedRawChunkIndices.some(i => !Number.isInteger(i) || !context.rawBindings[i])) fail('protected source indices')
98
+ // The used prefix must be the actual reconstructed raw intersection. A packet
99
+ // cannot hide an overlapping raw tail by supplying a larger usable count.
100
+ const ordered = [...context.rawBindings].sort((a, b) => a.startSample - b.startSample || a.rawChunkIndex - b.rawChunkIndex)
101
+ if (!ordered.length || Math.max(...ordered.map(r => r.endSample)) !== support.totalSamples) fail('timeline sample bound')
102
+ for (const [i, r] of ordered.entries()) if (support.rawUsedSamples[r.rawChunkIndex] !== Math.min(r.endSample - r.startSample, i + 1 < ordered.length ? ordered[i + 1].startSample - r.startSample : Infinity)) fail('raw used-sample geometry')
103
+ for (const n of support.nativeIntervals) if (!range(n.startSample, n.endSample) || n.endSample > support.totalSamples || typeof n.nativeCluster !== 'string' || !n.nativeCluster || n.nativeCluster.length > 100) fail('native geometry')
104
+ const identities = new Set<string>(), labels = new Set<string>()
105
+ for (const g of context.groups) {
106
+ const label = context.displayLabels[g.identityId]
107
+ if (!/^fluid256_[a-f0-9]{64}$/.test(g.identityId) || identities.has(g.identityId) || typeof label !== 'string' || !/^Anonymous group [1-9]\d{0,5}$/.test(label) || labels.has(label)) fail('anonymous identity namespace')
108
+ if (!Array.isArray(g.witnesses) || g.witnesses.length !== 2 || g.witnesses.some(w => !HASH.test(w.evidenceId) || !range(w.startSample, w.endSample) || !w.rawChunkIndices.length || w.rawChunkIndices.length !== w.pcmSHA256s.length || w.pcmSHA256s.some(h => !HASH.test(h)))) fail('missing identity witnesses')
109
+ const [a, b] = g.witnesses
110
+ if (a.startSample < b.endSample && b.startSample < a.endSample || a.rawChunkIndices.some(i => b.rawChunkIndices.includes(i)) || a.pcmSHA256s.some(h => b.pcmSHA256s.includes(h))) fail('dependent identity witnesses')
111
+ identities.add(g.identityId); labels.add(label)
112
+ }
113
+ if (Object.keys(context.displayLabels).length !== identities.size) fail('display identity cardinality')
114
+ }
115
+
116
+ /** Only call after full-source/state validation in the trusted offline producer.
117
+ * Hashes bind the supplied source; they are not a native-authentication grant. */
118
+ export function prepareMacSpeakerCallIntervalSupport(context: MacSpeakerCallIntervalContext, source: Omit<MacSpeakerCallIntervalSupport, 'intervals' | 'supportSHA256'>): MacSpeakerCallIntervalSupport {
119
+ checkSource(context, source)
120
+ const body = { ...structuredClone(source), intervals: expectedIntervals(context, source) }
121
+ if (body.intervals.length > 40000) fail('interval bound')
122
+ return { ...body, supportSHA256: digest(body) }
123
+ }
124
+
125
+ /** Reconstruct support from the bound packet geometry and admitted groups. A
126
+ * missing, expanded, reidentified or rehashed arbitrary interval list refuses. */
127
+ export function validateMacSpeakerCallIntervalSupport(context: MacSpeakerCallIntervalContext, support: MacSpeakerCallIntervalSupport): void {
128
+ if (!support || !Array.isArray(support.intervals) || support.intervals.length > 40000) fail('missing support')
129
+ const { supportSHA256, ...body } = support
130
+ if (!HASH.test(supportSHA256) || digest(body) !== supportSHA256) fail('support hash')
131
+ checkSource(context, support)
132
+ if (digest(expectedIntervals(context, support)) !== digest(support.intervals)) fail('support reconstruction')
133
+ }
134
+
135
+ /** Build a closed-over validated projector, avoiding caller-made label maps or
136
+ * mutable projections after validation. The output is a clone; BASE is untouched. */
137
+ export function createMacSpeakerCallIntervalProjector(contextInput: MacSpeakerCallIntervalContext, supportInput: MacSpeakerCallIntervalSupport) {
138
+ validateMacSpeakerCallIntervalSupport(contextInput, supportInput)
139
+ const context = structuredClone(contextInput), support = structuredClone(supportInput), protectedRaw = new Set(context.protectedRawChunkIndices)
140
+ const labels = context.displayLabels, byRaw = new Map<number, MacSpeakerCallSupportInterval[]>()
141
+ for (const s of support.intervals) { const list = byRaw.get(s.rawChunkIndex) ?? []; list.push(s); byRaw.set(s.rawChunkIndex, list) }
142
+ const identityFor = (raw: number, lo: number, hi: number): string | null => {
143
+ const r = context.rawBindings[raw]
144
+ if (!r || protectedRaw.has(raw) || !range(lo, hi) || lo < r.startSample || hi > r.endSample) return null
145
+ let end = lo, identity: string | null = null
146
+ for (const s of byRaw.get(raw) ?? []) {
147
+ if (s.endSample <= lo || s.startSample >= hi) continue
148
+ if (identity && identity !== s.identityId || s.startSample > end) return null
149
+ identity = s.identityId; end = Math.max(end, s.endSample)
150
+ }
151
+ return end >= hi ? identity : null
152
+ }
153
+ const eligible = (r: Doc) => !!r && typeof r.speaker === 'string' && GENERIC.test(r.speaker) && !isMacSpeakerHumanProtected(r)
154
+ const samples = (start: unknown, end: unknown): [number, number] | null => {
155
+ if (typeof start !== 'number' || typeof end !== 'number' || !Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end <= start) return null
156
+ if (!Number.isFinite(start * 16000) || !Number.isFinite(end * 16000)) return null
157
+ const lo = pyRoundInt(start * 16000), hi = pyRoundInt(end * 16000)
158
+ return range(lo, hi) ? [lo, hi] : null
159
+ }
160
+ // Presentation and human-review scopes must share this exact geometry. In
161
+ // particular a word crossing raw files cannot jump a gap or an overlap.
162
+ const hqSourceGeometry = (base: Doc): { usableHQWords: number; words: MacSpeakerCallHQWordSource[] } => {
163
+ let usableHQWords = 0
164
+ const words: MacSpeakerCallHQWordSource[] = []
165
+ for (const [batchIndex, batch] of (base.batchSegments ?? []).entries()) {
166
+ if (!Number.isInteger(batch.startChunkIdx) || !Number.isInteger(batch.endChunkIdx) || batch.startChunkIdx < 0 || batch.endChunkIdx < batch.startChunkIdx || batch.endChunkIdx >= context.rawBindings.length || !Array.isArray(batch.speakerWords)) fail('batch geometry')
167
+ const clock: Array<{ raw: number; lo: number; hi: number }> = []; let offset = 0
168
+ for (let raw = batch.startChunkIdx; raw <= batch.endChunkIdx; raw++) { const r = context.rawBindings[raw], n = r.endSample - r.startSample; clock.push({ raw, lo: offset, hi: offset + n }); offset += n }
169
+ for (const [wordIndex, word] of batch.speakerWords.entries()) {
170
+ if (typeof word?.word !== 'string' || !word.word.trim()) continue
171
+ const span = samples(word?.start, word?.end)
172
+ if (!span || span[1] > offset) continue
173
+ usableHQWords++
174
+ if (isMacSpeakerHumanProtected(word)) continue
175
+ const [lo, hi] = span
176
+ let left = 0, right = clock.length
177
+ while (left < right) { const mid = (left + right) >>> 1; if (clock[mid].hi <= lo) left = mid + 1; else right = mid }
178
+ let captureEnd: number | null = null, covered = 0, valid = true
179
+ const pieces: MacSpeakerCallHQWordBinding['pieces'] = []
180
+ for (let i = left; i < clock.length && clock[i].lo < hi; i++) {
181
+ const part = clock[i], r = context.rawBindings[part.raw], a = r.startSample + Math.max(0, lo - part.lo), b = r.startSample + Math.min(part.hi - part.lo, hi - part.lo)
182
+ if (protectedRaw.has(part.raw) || b > r.startSample + support.rawUsedSamples[part.raw] || captureEnd !== null && captureEnd !== a) { valid = false; break }
183
+ captureEnd = b; covered += b - a
184
+ pieces.push({ rawChunkIndex: part.raw, startSample: a, endSample: b, audioSHA256: r.audioSHA256, pcmSHA256: r.pcmSHA256, originalRowSHA256: r.originalRowSHA256 })
185
+ }
186
+ if (valid && pieces.length && covered === hi - lo) words.push({ batchIndex, wordIndex, startElapsedMs: pieces[0].startSample / 16, endElapsedMs: pieces.at(-1)!.endSample / 16, rawChunkIndices: pieces.map(p => p.rawChunkIndex), pieces })
187
+ }
188
+ }
189
+ return { usableHQWords, words }
190
+ }
191
+ const hqGeometry = (base: Doc): { usableHQWords: number; words: MacSpeakerCallHQWordBinding[] } => {
192
+ const source = hqSourceGeometry(base), words: MacSpeakerCallHQWordBinding[] = []
193
+ for (const word of source.words) {
194
+ if (!eligible(base.batchSegments[word.batchIndex].speakerWords[word.wordIndex])) continue
195
+ let identity: string | null = null, valid = true
196
+ for (const piece of word.pieces) {
197
+ const id = identityFor(piece.rawChunkIndex, piece.startSample, piece.endSample)
198
+ if (!id || identity && identity !== id) { valid = false; break }
199
+ identity = id
200
+ }
201
+ if (valid && identity) words.push({ ...word, identityId: identity })
202
+ }
203
+ return { usableHQWords: source.usableHQWords, words }
204
+ }
205
+ return {
206
+ identityFor,
207
+ hqWords(base: Doc): MacSpeakerCallHQWordBinding[] { return hqGeometry(base).words },
208
+ hqWordSources(base: Doc): MacSpeakerCallHQWordSource[] { return hqSourceGeometry(base).words },
209
+ project(base: Doc) {
210
+ const doc = structuredClone(base), changedRaw = new Set<number>()
211
+ let position = 0
212
+ for (const e of doc.chunkEntries) {
213
+ const raw = e.chunkIndex, c = e.chunk, r = context.rawBindings[raw]
214
+ if (typeof c.text !== 'string' || !c.text.trim()) continue
215
+ const root = doc.chunks[position++]
216
+ if (!root || !r || protectedRaw.has(raw) || hasMacSpeakerProtectedRows(c) || hasMacSpeakerProtectedRows(root)) continue
217
+ for (const row of [root, c]) {
218
+ const whole = identityFor(raw, r.startSample, r.endSample)
219
+ if (whole && eligible(row)) { row.speaker = labels[whole]; changedRaw.add(raw) }
220
+ for (const key of ['segments', 'finalSegments', 'words']) for (const nested of row[key] ?? []) {
221
+ if (!eligible(nested)) continue
222
+ const span = samples(key === 'words' ? nested.start : nested.startSec, key === 'words' ? nested.end : nested.endSec)
223
+ const identity = span ? identityFor(raw, r.startSample + span[0], r.startSample + span[1]) : null
224
+ if (identity) { nested.speaker = labels[identity]; changedRaw.add(raw) }
225
+ }
226
+ }
227
+ }
228
+ const hq = hqGeometry(base)
229
+ for (const word of hq.words) doc.batchSegments[word.batchIndex].speakerWords[word.wordIndex].speaker = labels[word.identityId]
230
+ return { doc, changedChunks: changedRaw.size, changedWords: hq.words.length, usableHQWords: hq.usableHQWords }
231
+ },
232
+ }
233
+ }
@@ -0,0 +1,328 @@
1
+ /** Explicit return to immutable BASE plus verified older human corrections.
2
+ * Only derived interval artifacts move. A durable archive precedes the prepared
3
+ * marker, which precedes every removal; recovery never invents a new intent. */
4
+ import { createHash, randomUUID } from 'node:crypto'
5
+ import { closeSync, constants, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
6
+ import { dirname, join, resolve } from 'node:path'
7
+ import { applyMacCallHumanAnnotation, macCallHumanPaths, readMacCallHumanFile, type MacCallHumanFiles } from './mac-speaker-call-human.js'
8
+ import { applyMacIntervalHumanAnnotation, inspectMacIntervalHumanJournal, macIntervalHumanPaths } from './mac-speaker-call-interval-human.js'
9
+ import type { MacSpeakerCallPacket, MacSpeakerCallView } from './mac-speaker-call-view.js'
10
+
11
+ const POLICY = 'mac-speaker-call-original-view-v1'
12
+ const SHA = /^[a-f0-9]{64}$/
13
+ const MB = 1024 * 1024
14
+ const hash = (s: string) => createHash('sha256').update(s).digest('hex')
15
+ const digest = (v: unknown) => hash(JSON.stringify(v))
16
+ const equal = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b)
17
+ function fail(reason: string): never { throw Error(`Mac original view: ${reason}`) }
18
+
19
+ type SourceKey = 'base' | 'markdown' | 'ledger' | 'legacyAnnotation' | 'legacyJournal'
20
+ type ArtifactKey = 'packet' | 'intervalAnnotation' | 'intervalJournal' | 'state'
21
+ interface Pin<K extends string> { key: K; hash: string | null }
22
+ interface Intent {
23
+ version: 1; policy: typeof POLICY; captureId: string; scopeSHA256: string; archiveId: string
24
+ sourcePins: Pin<SourceKey>[]; artifacts: Pin<ArtifactKey>[]; previousTransactionSHA256: string | null
25
+ }
26
+ interface Journal extends Intent { phase: 'prepared' | 'complete'; journalSHA256: string }
27
+ export interface OriginalViewPreview {
28
+ scopeSHA256: string; archiveId: string; artifacts: ArtifactKey[]; activeActions: 0
29
+ }
30
+ export interface OriginalViewTransactionStatus {
31
+ status: 'absent' | 'prepared' | 'complete' | 'recovery-invalid'; archiveId?: string; reason?: string
32
+ }
33
+ export function originalViewPaths(files: MacCallHumanFiles, captureId: string) {
34
+ const interval = macIntervalHumanPaths(files, captureId), stem = files.sidecarPath.replace(/\.g2-chunks\.json$/, '')
35
+ return { journal: `${stem}.speaker-call-original-view-transaction.json`, archiveRoot: `${stem}.speaker-call-original-view-archive`, lock: interval.lock }
36
+ }
37
+ function specifications(files: MacCallHumanFiles, captureId: string) {
38
+ const interval = macIntervalHumanPaths(files, captureId), legacy = macCallHumanPaths(files, captureId)
39
+ return {
40
+ sources: [
41
+ { key: 'base' as const, path: files.sidecarPath, maximum: 32 * MB, optional: false },
42
+ { key: 'markdown' as const, path: files.meetingPath, maximum: 16 * MB, optional: true },
43
+ { key: 'ledger' as const, path: legacy.ledger, maximum: 8 * MB, optional: true },
44
+ { key: 'legacyAnnotation' as const, path: legacy.annotation, maximum: 16 * MB, optional: true },
45
+ { key: 'legacyJournal' as const, path: legacy.journal, maximum: 64 * MB, optional: true },
46
+ ],
47
+ artifacts: [
48
+ { key: 'packet' as const, path: interval.packet, maximum: 16 * MB, archiveName: 'packet.json' },
49
+ { key: 'intervalAnnotation' as const, path: interval.annotation, maximum: 16 * MB, archiveName: 'interval-human.json' },
50
+ { key: 'intervalJournal' as const, path: interval.journal, maximum: 40 * MB, archiveName: 'interval-human-transaction.json' },
51
+ { key: 'state' as const, path: files.sidecarPath.replace(/\.g2-chunks\.json$/, '.speaker-call-state.json'), maximum: 16 * MB, archiveName: 'state.json' },
52
+ ],
53
+ }
54
+ }
55
+ function canonicalParent(path: string): string {
56
+ if (resolve(path) !== path) fail('absolute canonical path required')
57
+ const parent = dirname(path)
58
+ if (realpathSync(parent) !== parent || !lstatSync(parent).isDirectory()) fail('canonical parent required')
59
+ return parent
60
+ }
61
+ function syncParent(path: string) {
62
+ const fd = openSync(canonicalParent(path), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW | constants.O_NONBLOCK)
63
+ try { fsyncSync(fd) } finally { closeSync(fd) }
64
+ }
65
+ function safeRead(path: string, maximum: number, optional = false) {
66
+ const result = readMacCallHumanFile(path, maximum, optional)
67
+ if (result) canonicalParent(path)
68
+ return result
69
+ }
70
+ function privateDirectory(path: string, create: boolean) {
71
+ canonicalParent(path)
72
+ if (create) { try { mkdirSync(path, { mode: 0o700 }); syncParent(path) } catch (e) { if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e } }
73
+ const stat = lstatSync(path)
74
+ if (!stat.isDirectory() || stat.isSymbolicLink() || realpathSync(path) !== path || (stat.mode & 0o077) !== 0) fail('private canonical archive directory required')
75
+ }
76
+ function exclusiveCopy(path: string, text: string) {
77
+ canonicalParent(path)
78
+ const existing = safeRead(path, 80 * MB, true)
79
+ if (existing) { if (existing.text !== text) fail('archive collision'); return }
80
+ const temporary = `${path}.${randomUUID()}.tmp`
81
+ const fd = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o600)
82
+ try { writeFileSync(fd, text); fsyncSync(fd) } finally { closeSync(fd) }
83
+ try { linkSync(temporary, path); syncParent(path) } finally { unlinkSync(temporary); syncParent(path) }
84
+ if (safeRead(path, 80 * MB)!.text !== text) fail('archive copy changed')
85
+ }
86
+ function atomic(path: string, text: string, before: () => void) {
87
+ canonicalParent(path)
88
+ const temporary = `${path}.${randomUUID()}.tmp`
89
+ const fd = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o600)
90
+ try { writeFileSync(fd, text); fsyncSync(fd) } finally { closeSync(fd) }
91
+ try { before(); renameSync(temporary, path); syncParent(path) } finally { try { unlinkSync(temporary) } catch (e) { if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e } }
92
+ }
93
+ function acquire(files: MacCallHumanFiles, captureId: string, recover: boolean) {
94
+ const paths = originalViewPaths(files, captureId), old = safeRead(paths.lock, 4096, true)
95
+ if (old) {
96
+ if (!recover) fail('capture locked')
97
+ const owner = JSON.parse(old.text)
98
+ if (owner.kind !== POLICY || owner.journal !== paths.journal || !Number.isSafeInteger(owner.pid) || owner.pid < 1) fail('foreign capture lock')
99
+ try { process.kill(owner.pid, 0); fail('capture lock owner alive') } catch (e) { if ((e as NodeJS.ErrnoException).code !== 'ESRCH') throw e }
100
+ if (safeRead(paths.lock, 4096)!.text !== old.text) fail('capture lock drift')
101
+ unlinkSync(paths.lock); syncParent(paths.lock)
102
+ }
103
+ canonicalParent(paths.lock)
104
+ const text = JSON.stringify({ kind: POLICY, journal: paths.journal, pid: process.pid, token: randomUUID() })
105
+ const fd = openSync(paths.lock, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o600)
106
+ try { writeFileSync(fd, text); fsyncSync(fd) } finally { closeSync(fd) }
107
+ syncParent(paths.lock)
108
+ const assertOwned = () => { if (safeRead(paths.lock, 4096, true)?.text !== text) fail('capture lock drift') }
109
+ return { assertOwned, release: () => { assertOwned(); unlinkSync(paths.lock); syncParent(paths.lock) } }
110
+ }
111
+ function sealed(body: Omit<Journal, 'journalSHA256'>): Journal { return { ...body, journalSHA256: digest(body) } }
112
+ function checkedJournal(files: MacCallHumanFiles, captureId: string, value: unknown): Journal {
113
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('transaction integrity')
114
+ const journal = value as Journal, { journalSHA256, phase, ...intent } = journal
115
+ const { scopeSHA256, archiveId, ...scope } = intent
116
+ const specs = specifications(files, captureId)
117
+ if (journal.version !== 1 || journal.policy !== POLICY || journal.captureId !== captureId || !SHA.test(journalSHA256)
118
+ || digest({ ...intent, phase }) !== journalSHA256 || !['prepared', 'complete'].includes(phase)
119
+ || !SHA.test(scopeSHA256) || archiveId !== scopeSHA256 || digest(scope) !== scopeSHA256
120
+ || !Array.isArray(journal.sourcePins) || !Array.isArray(journal.artifacts)
121
+ || !equal(journal.sourcePins.map(p => p?.key), specs.sources.map(s => s.key))
122
+ || !equal(journal.artifacts.map(p => p?.key), specs.artifacts.map(s => s.key))
123
+ || [...journal.sourcePins, ...journal.artifacts].some(p => p.hash !== null && !SHA.test(p.hash))
124
+ || journal.sourcePins[0].hash === null
125
+ || journal.previousTransactionSHA256 !== null && !SHA.test(journal.previousTransactionSHA256)) fail('transaction integrity')
126
+ return journal
127
+ }
128
+ function archiveDirectory(files: MacCallHumanFiles, captureId: string, archiveId: string, create: boolean) {
129
+ if (!SHA.test(archiveId)) fail('archive identity')
130
+ const paths = originalViewPaths(files, captureId), directory = join(paths.archiveRoot, archiveId)
131
+ privateDirectory(paths.archiveRoot, create); privateDirectory(directory, create)
132
+ return directory
133
+ }
134
+ function checkSources(files: MacCallHumanFiles, captureId: string, pins: Pin<SourceKey>[]) {
135
+ for (const [index, spec] of specifications(files, captureId).sources.entries()) {
136
+ if ((safeRead(spec.path, spec.maximum, spec.optional)?.hash ?? null) !== pins[index].hash) fail('source changed')
137
+ }
138
+ }
139
+ /** Complete older journals are archival: their packet pin is intentionally not
140
+ * current. Their integrity and human/ledger postimages must still agree. */
141
+ function checkLegacyJournal(files: MacCallHumanFiles, captureId: string, text: string | null, ledger: string | null, annotation: string | null) {
142
+ if (text === null) return
143
+ const value = JSON.parse(text), { journalSHA256, ...body } = value ?? {}, legacy = macCallHumanPaths(files, captureId)
144
+ if (value?.version !== 1 || value.policy !== 'mac-speaker-call-human-v1' || value.captureId !== captureId || value.phase !== 'complete'
145
+ || !SHA.test(journalSHA256) || journalSHA256 !== digest(body) || !Array.isArray(value.images) || !Array.isArray(value.pins)
146
+ || !equal(value.pins.map((p: any) => p?.path), [files.sidecarPath, files.meetingPath, legacy.packet])
147
+ || !equal(value.pins.map((p: any) => p?.maximum), [32 * MB, 16 * MB, 16 * MB])
148
+ || value.pins.some((p: any) => p.hash !== null && !SHA.test(p.hash))
149
+ || !equal(value.images.map((i: any) => i?.path), [legacy.ledger, legacy.annotation])
150
+ || value.images.some((i: any) => i.before !== null && typeof i.before !== 'string' || typeof i.after !== 'string' || Buffer.byteLength(i.before ?? '') > 16 * MB || Buffer.byteLength(i.after) > 16 * MB)
151
+ || value.images[0].after !== ledger || value.images[1].after !== annotation) fail('legacy human recovery required')
152
+ }
153
+ function validateInactive(files: MacCallHumanFiles, captureId: string, sources: Array<string | null>, artifacts: Array<string | null>) {
154
+ const [baseJSON, markdownSource, ledger, legacyAnnotation, legacyJournal] = sources
155
+ if (baseJSON === null) fail('missing immutable source')
156
+ const markdown = markdownSource ?? ''
157
+ const base = JSON.parse(baseJSON)
158
+ if (base?.sessionId !== captureId) fail('capture source mismatch')
159
+ checkLegacyJournal(files, captureId, legacyJournal, ledger, legacyAnnotation)
160
+ const baseline: MacSpeakerCallView = { doc: base, metadata: { status: 'base', reason: 'original-view', mode: 'strict', changedChunks: 0, changedWords: 0, groups: [], localization: 'estimated-ASR-with-native-support', speakerPurityVerified: false } }
161
+ const legacy = applyMacCallHumanAnnotation(baseJSON, ledger ?? '', legacyAnnotation, baseline)
162
+ let packet: MacSpeakerCallPacket | null = null
163
+ if (artifacts[0] !== null) { try { packet = JSON.parse(artifacts[0]) } catch { /* Preserve unusable automatic bytes in the archive too. */ } }
164
+ const view = applyMacIntervalHumanAnnotation({ baseJSON, markdown, ledgerJSONL: ledger ?? '', legacyAnnotationJSON: legacyAnnotation, annotationJSON: artifacts[1], packet, view: legacy })
165
+ if (view.metadata.intervalHumanAnnotation?.activeActions.length) fail('active interval human actions must be undone first')
166
+ if (artifacts[2] !== null) {
167
+ const value = JSON.parse(artifacts[2]), { journalSHA256, ...body } = value ?? {}, interval = macIntervalHumanPaths(files, captureId)
168
+ if (value?.version !== 1 || value.policy !== 'mac-speaker-call-interval-human-v1' || value.captureId !== captureId || value.phase !== 'complete'
169
+ || !SHA.test(journalSHA256) || journalSHA256 !== digest(body) || value.after !== artifacts[1]
170
+ || value.before !== null && typeof value.before !== 'string' || typeof value.after !== 'string'
171
+ || Buffer.byteLength(value.before ?? '') > 16 * MB || Buffer.byteLength(value.after) > 16 * MB
172
+ || !equal(value.pins?.map((p: any) => p.path), [files.sidecarPath, files.meetingPath, interval.ledger, interval.legacyAnnotation, interval.packet])
173
+ || !equal(value.pins.map((p: any) => p.maximum), [32 * MB, 16 * MB, 8 * MB, 16 * MB, 16 * MB])
174
+ || value.pins.some((p: any) => p.hash !== null && !SHA.test(p.hash))
175
+ || !equal(value.pins.slice(0, 4).map((p: any) => p.hash), [hash(baseJSON), markdownSource === null ? null : hash(markdown), ledger === null ? null : hash(ledger), legacyAnnotation === null ? null : hash(legacyAnnotation)])) fail('interval human recovery required')
176
+ }
177
+ }
178
+ function snapshot(files: MacCallHumanFiles, captureId: string, ownedPreparation = false) {
179
+ const specs = specifications(files, captureId), paths = originalViewPaths(files, captureId)
180
+ canonicalParent(paths.journal)
181
+ const prior = safeRead(paths.journal, MB, true)
182
+ let status: OriginalViewTransactionStatus
183
+ if (ownedPreparation && prior === null) status = { status: 'absent' }
184
+ else if (ownedPreparation) {
185
+ const previous = checkedJournal(files, captureId, JSON.parse(prior!.text))
186
+ if (previous.phase === 'complete') { verifyArchive(files, captureId, previous); status = { status: 'complete' } }
187
+ else status = inspectOriginalViewTransaction(files, captureId)
188
+ } else status = inspectOriginalViewTransaction(files, captureId)
189
+ if (status.status === 'prepared' || status.status === 'recovery-invalid') fail('original view recovery required')
190
+ const sources = specs.sources.map(s => safeRead(s.path, s.maximum, s.optional))
191
+ const artifacts = specs.artifacts.map(s => safeRead(s.path, s.maximum, true))
192
+ const intervalJournal = inspectMacIntervalHumanJournal(files, captureId)
193
+ if (intervalJournal.status === 'prepared' || intervalJournal.status === 'recovery-invalid') fail('interval human recovery required')
194
+ validateInactive(files, captureId, sources.map(s => s?.text ?? null), artifacts.map(a => a?.text ?? null))
195
+ const body: Omit<Intent, 'scopeSHA256' | 'archiveId'> = { version: 1, policy: POLICY, captureId,
196
+ sourcePins: specs.sources.map((s, i) => ({ key: s.key, hash: sources[i]?.hash ?? null })),
197
+ artifacts: specs.artifacts.map((s, i) => ({ key: s.key, hash: artifacts[i]?.hash ?? null })), previousTransactionSHA256: prior?.hash ?? null }
198
+ const scopeSHA256 = digest(body), intent: Intent = { ...body, scopeSHA256, archiveId: scopeSHA256 }
199
+ checkSources(files, captureId, intent.sourcePins)
200
+ for (const [index, spec] of specs.artifacts.entries()) if ((safeRead(spec.path, spec.maximum, true)?.hash ?? null) !== intent.artifacts[index].hash) fail('artifact changed')
201
+ if ((safeRead(paths.journal, MB, true)?.hash ?? null) !== intent.previousTransactionSHA256) fail('transaction changed')
202
+ return { intent, sources, artifacts, prior }
203
+ }
204
+ export function previewOriginalViewTransaction(files: MacCallHumanFiles, captureId: string): OriginalViewPreview {
205
+ const { intent } = snapshot(files, captureId)
206
+ return { scopeSHA256: intent.scopeSHA256, archiveId: intent.archiveId, artifacts: intent.artifacts.filter(a => a.hash !== null).map(a => a.key), activeActions: 0 }
207
+ }
208
+ function verifyArchive(files: MacCallHumanFiles, captureId: string, journal: Journal) {
209
+ const archive = archiveDirectory(files, captureId, journal.archiveId, false), specs = specifications(files, captureId)
210
+ // Remove the old seal before resealing a phase; it is never part of the body.
211
+ const { journalSHA256: _, ...preparedBody } = journal
212
+ const intentText = `${JSON.stringify(sealed({ ...preparedBody, phase: 'prepared' }))}\n`
213
+ if (safeRead(join(archive, 'intent.json'), MB)!.text !== intentText) fail('archived intent changed')
214
+ const completion = safeRead(join(archive, 'complete.json'), MB, journal.phase !== 'complete')
215
+ if (completion && completion.text !== `${JSON.stringify(sealed({ ...preparedBody, phase: 'complete' }))}\n`) fail('archived completion changed')
216
+ const artifacts = specs.artifacts.map((s, i) => {
217
+ const read = safeRead(join(archive, s.archiveName), s.maximum, journal.artifacts[i].hash === null)
218
+ if ((read?.hash ?? null) !== journal.artifacts[i].hash) fail('archived artifact changed')
219
+ return read?.text ?? null
220
+ })
221
+ if ((safeRead(join(archive, 'previous-transaction.json'), MB, journal.previousTransactionSHA256 === null)?.hash ?? null) !== journal.previousTransactionSHA256) fail('archived prior transaction changed')
222
+ return artifacts
223
+ }
224
+ function verifyPrepared(files: MacCallHumanFiles, captureId: string, journal: Journal, expectedText: string) {
225
+ const paths = originalViewPaths(files, captureId), specs = specifications(files, captureId), artifacts = verifyArchive(files, captureId, journal)
226
+ checkSources(files, captureId, journal.sourcePins)
227
+ for (const [index, spec] of specs.artifacts.entries()) {
228
+ const current = safeRead(spec.path, spec.maximum, true)
229
+ if (current && current.hash !== journal.artifacts[index].hash) fail('later interval artifact changed')
230
+ }
231
+ const sources = specs.sources.map(s => safeRead(s.path, s.maximum, s.optional)?.text ?? null)
232
+ validateInactive(files, captureId, sources, artifacts)
233
+ checkSources(files, captureId, journal.sourcePins)
234
+ if (safeRead(paths.journal, MB)!.text !== expectedText) fail('transaction changed')
235
+ }
236
+ export function inspectOriginalViewTransaction(files: MacCallHumanFiles, captureId: string): OriginalViewTransactionStatus {
237
+ try {
238
+ const paths = originalViewPaths(files, captureId), stored = safeRead(paths.journal, MB, true)
239
+ if (!stored) {
240
+ const lock = safeRead(paths.lock, 4096, true)
241
+ if (lock) {
242
+ let owner: any
243
+ try { owner = JSON.parse(lock.text) } catch { /* Foreign lock handling belongs to its owner. */ }
244
+ if (owner?.kind === POLICY && owner.journal === paths.journal) return { status: 'recovery-invalid', reason: 'Original-view lock without a durable intent; explicit dead-owner recovery required' }
245
+ }
246
+ return { status: 'absent' }
247
+ }
248
+ const journal = checkedJournal(files, captureId, JSON.parse(stored.text))
249
+ if (journal.phase === 'prepared') verifyPrepared(files, captureId, journal, stored.text)
250
+ else {
251
+ verifyArchive(files, captureId, journal)
252
+ if (safeRead(paths.journal, MB)!.text !== stored.text) fail('transaction changed')
253
+ const lock = safeRead(paths.lock, 4096, true)
254
+ if (lock) {
255
+ let owner: any
256
+ try { owner = JSON.parse(lock.text) } catch { /* A different operation owns malformed/foreign locks. */ }
257
+ if (owner?.kind === POLICY && owner.journal === paths.journal) return { status: 'recovery-invalid', archiveId: journal.archiveId, reason: 'Original-view capture lock requires explicit owner recovery' }
258
+ }
259
+ }
260
+ // Completed receipts are historical. They deliberately do not pin current
261
+ // BASE, Markdown, ledger, legacy decisions, or newly generated packets.
262
+ return { status: journal.phase, archiveId: journal.archiveId }
263
+ } catch (error) { return { status: 'recovery-invalid', reason: error instanceof Error ? error.message : 'Invalid original-view transaction' } }
264
+ }
265
+ function run(files: MacCallHumanFiles, captureId: string, journal: Journal, expectedText: string, assertOwned: () => void, afterWrite?: (index: number) => void) {
266
+ const paths = originalViewPaths(files, captureId), specs = specifications(files, captureId)
267
+ const fence = () => { assertOwned(); verifyPrepared(files, captureId, journal, expectedText) }
268
+ if (journal.phase === 'complete') { assertOwned(); verifyArchive(files, captureId, journal); return }
269
+ fence()
270
+ for (const [index, spec] of specs.artifacts.entries()) {
271
+ fence()
272
+ if (safeRead(spec.path, spec.maximum, true)) { unlinkSync(spec.path); syncParent(spec.path) }
273
+ afterWrite?.(index)
274
+ fence()
275
+ for (const earlier of specs.artifacts.slice(0, index + 1)) if (safeRead(earlier.path, earlier.maximum, true)) fail('removed artifact reappeared')
276
+ }
277
+ const { journalSHA256: _, ...body } = journal, complete = `${JSON.stringify(sealed({ ...body, phase: 'complete' }))}\n`
278
+ fence()
279
+ for (const spec of specs.artifacts) if (safeRead(spec.path, spec.maximum, true)) fail('removed artifact reappeared')
280
+ exclusiveCopy(join(archiveDirectory(files, captureId, journal.archiveId, false), 'complete.json'), complete)
281
+ afterWrite?.(specs.artifacts.length)
282
+ atomic(paths.journal, complete, () => { fence(); for (const spec of specs.artifacts) if (safeRead(spec.path, spec.maximum, true)) fail('removed artifact reappeared') })
283
+ if (safeRead(paths.journal, MB)!.text !== complete) fail('completed transaction changed')
284
+ }
285
+ export function commitOriginalViewTransaction(input: { files: MacCallHumanFiles; captureId: string; expectedScopeSHA256: string; afterWrite?: (index: number) => void }): { archiveId: string; scopeSHA256: string } {
286
+ if (!SHA.test(input.expectedScopeSHA256)) fail('explicit reviewed scope required')
287
+ const guard = acquire(input.files, input.captureId, false)
288
+ try {
289
+ const state = snapshot(input.files, input.captureId, true)
290
+ if (state.intent.scopeSHA256 !== input.expectedScopeSHA256) fail('reviewed source changed')
291
+ const paths = originalViewPaths(input.files, input.captureId), archive = archiveDirectory(input.files, input.captureId, state.intent.archiveId, true)
292
+ const specs = specifications(input.files, input.captureId), journal = sealed({ ...state.intent, phase: 'prepared' }), text = `${JSON.stringify(journal)}\n`
293
+ state.artifacts.forEach((a, i) => { if (a) exclusiveCopy(join(archive, specs.artifacts[i].archiveName), a.text) })
294
+ if (state.prior) exclusiveCopy(join(archive, 'previous-transaction.json'), state.prior.text)
295
+ exclusiveCopy(join(archive, 'intent.json'), text)
296
+ verifyArchive(input.files, input.captureId, journal)
297
+ atomic(paths.journal, text, () => {
298
+ guard.assertOwned(); checkSources(input.files, input.captureId, state.intent.sourcePins)
299
+ for (const [i, spec] of specs.artifacts.entries()) if ((safeRead(spec.path, spec.maximum, true)?.hash ?? null) !== state.intent.artifacts[i].hash) fail('artifact changed')
300
+ if ((safeRead(paths.journal, MB, true)?.hash ?? null) !== state.intent.previousTransactionSHA256) fail('transaction changed')
301
+ })
302
+ input.afterWrite?.(-1) // The durable intent exists; no active artifact has moved.
303
+ run(input.files, input.captureId, journal, text, guard.assertOwned, input.afterWrite)
304
+ return { archiveId: state.intent.archiveId, scopeSHA256: state.intent.scopeSHA256 }
305
+ } finally { guard.release() }
306
+ }
307
+ export function recoverOriginalViewTransaction(files: MacCallHumanFiles, captureId: string, afterWrite?: (index: number) => void): void {
308
+ const paths = originalViewPaths(files, captureId), stored = safeRead(paths.journal, MB, true)
309
+ if (!stored) {
310
+ const lock = safeRead(paths.lock, 4096, true)
311
+ if (!lock) return
312
+ let owner: any
313
+ try { owner = JSON.parse(lock.text) } catch { return }
314
+ if (owner?.kind !== POLICY || owner.journal !== paths.journal) return
315
+ // A crash before the durable intent moved nothing. Only a proven dead lock
316
+ // owned by this operation may be cleared; no new intent is submitted.
317
+ const orphanGuard = acquire(files, captureId, true)
318
+ try { if (safeRead(paths.journal, MB, true)) fail('transaction appeared during orphan recovery') } finally { orphanGuard.release() }
319
+ return
320
+ }
321
+ const guard = acquire(files, captureId, true)
322
+ try {
323
+ if (safeRead(paths.journal, MB)!.text !== stored.text) fail('transaction changed')
324
+ const journal = checkedJournal(files, captureId, JSON.parse(stored.text))
325
+ if (journal.phase === 'complete') { verifyArchive(files, captureId, journal); return }
326
+ run(files, captureId, journal, stored.text, guard.assertOwned, afterWrite)
327
+ } finally { guard.release() }
328
+ }