@gotcos/glasses-server 6.95.0 → 6.98.1

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 (123) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/README.md +4 -0
  3. package/managed-runtime-contract.json +13 -0
  4. package/package.json +3 -1
  5. package/server/index.ts +12 -0
  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/decision-provider.ts +4 -2
  12. package/server/lib/dictation-clean-models.ts +2 -0
  13. package/server/lib/dictation-clean.ts +2 -2
  14. package/server/lib/dictation-polish-pipeline.ts +22 -2
  15. package/server/lib/dictation-prewarm.ts +16 -0
  16. package/server/lib/direct-bridge.ts +14 -2
  17. package/server/lib/direct-sse.ts +6 -1
  18. package/server/lib/display-bus.ts +1 -1
  19. package/server/lib/fork-thread.ts +32 -1
  20. package/server/lib/held-naming-batches.ts +44 -7
  21. package/server/lib/held-voice-groups.ts +5 -4
  22. package/server/lib/job-audit-caller.ts +20 -0
  23. package/server/lib/keyed-sessions.ts +21 -1
  24. package/server/lib/lens-gist-engines.ts +17 -5
  25. package/server/lib/live-cos-ask.ts +29 -6
  26. package/server/lib/live-cue-verdicts.ts +43 -8
  27. package/server/lib/live-cues-engine.ts +4 -1
  28. package/server/lib/live-cues-filter.ts +50 -0
  29. package/server/lib/live-cues-general-prompts.ts +137 -0
  30. package/server/lib/live-cues-general-topic.ts +245 -0
  31. package/server/lib/live-cues-general.ts +715 -0
  32. package/server/lib/live-cues-v2-data.ts +763 -0
  33. package/server/lib/live-cues-v2-grounded.ts +262 -0
  34. package/server/lib/live-cues-v2-judge-b.ts +59 -0
  35. package/server/lib/live-cues-v2-names.ts +49 -0
  36. package/server/lib/live-cues-v2-notes.ts +8 -4
  37. package/server/lib/live-cues-v2-open-items.ts +132 -0
  38. package/server/lib/live-cues-v2-pacing.ts +45 -0
  39. package/server/lib/live-cues-v2-prompts-kinds.ts +9 -4
  40. package/server/lib/live-cues-v2-settings.ts +89 -10
  41. package/server/lib/live-cues-v2.ts +572 -47
  42. package/server/lib/mac-recording-asr.ts +6 -3
  43. package/server/lib/mac-speaker-call-human.ts +371 -0
  44. package/server/lib/mac-speaker-call-interval-human.ts +499 -0
  45. package/server/lib/mac-speaker-call-interval-projection.ts +233 -0
  46. package/server/lib/mac-speaker-call-original-view.ts +328 -0
  47. package/server/lib/mac-speaker-call-producer-worker.ts +615 -0
  48. package/server/lib/mac-speaker-call-producer.ts +539 -0
  49. package/server/lib/mac-speaker-call-store.ts +264 -0
  50. package/server/lib/mac-speaker-call-view.ts +373 -0
  51. package/server/lib/mac-speaker-live-compute.ts +72 -0
  52. package/server/lib/mac-speaker-live-memory.ts +414 -0
  53. package/server/lib/mac-speaker-live-projection.ts +126 -0
  54. package/server/lib/mac-speaker-live-save.ts +90 -0
  55. package/server/lib/mac-speaker-live-store.ts +71 -0
  56. package/server/lib/mac-speaker-live-worker.ts +58 -0
  57. package/server/lib/mac-speaker-live.ts +175 -0
  58. package/server/lib/mac-speaker-profile-extractor.ts +15 -10
  59. package/server/lib/mac-speaker-shadow-repair.ts +307 -0
  60. package/server/lib/mac-voice-assignment.ts +46 -27
  61. package/server/lib/mac-voice-display.ts +195 -0
  62. package/server/lib/mac-voice-naming-batch.ts +1067 -0
  63. package/server/lib/mac-voice-suggestions.ts +548 -0
  64. package/server/lib/maintenance-lifecycle.ts +2 -0
  65. package/server/lib/media-store.ts +2 -1
  66. package/server/lib/meeting-ask-pasted.ts +135 -0
  67. package/server/lib/meeting-ask-review.ts +8 -1
  68. package/server/lib/meeting-ask-runner.ts +4 -1
  69. package/server/lib/meeting-ask-runtime.ts +189 -17
  70. package/server/lib/meeting-ask-store.ts +2 -0
  71. package/server/lib/meeting-ask-web.ts +284 -13
  72. package/server/lib/meeting-batch-progress.ts +51 -5
  73. package/server/lib/meeting-batch-transcribe.ts +35 -2
  74. package/server/lib/meeting-corrections.ts +2 -1
  75. package/server/lib/meeting-fact-check.ts +3 -0
  76. package/server/lib/meeting-finalization-jobs.ts +69 -2
  77. package/server/lib/meeting-public-topic.ts +6 -1
  78. package/server/lib/meeting-relabel-enrolment.ts +2 -1
  79. package/server/lib/meeting-relabel.ts +17 -10
  80. package/server/lib/meeting-speaker-review.ts +187 -21
  81. package/server/lib/meeting-store.ts +6 -1
  82. package/server/lib/parakeet.ts +51 -5
  83. package/server/lib/pending-batch-retention.ts +58 -0
  84. package/server/lib/private-reference.ts +38 -0
  85. package/server/lib/provider-usage.ts +561 -0
  86. package/server/lib/public-https-url.ts +32 -0
  87. package/server/lib/query-job-runtime.ts +3 -0
  88. package/server/lib/rich-media-safety.ts +23 -2
  89. package/server/lib/session-find-ask.ts +288 -0
  90. package/server/lib/session-find-collect.ts +715 -0
  91. package/server/lib/session-find-redact.ts +254 -0
  92. package/server/lib/session-find.ts +345 -0
  93. package/server/lib/speaker-calibration-log.ts +3 -2
  94. package/server/lib/speaker-embeddings.ts +46 -35
  95. package/server/lib/speaker-name-nfc.ts +37 -0
  96. package/server/lib/speaker-rename-fanout.ts +7 -6
  97. package/server/lib/speaker-shadow-decide.ts +59 -22
  98. package/server/lib/speaker-shadow-pair-source.ts +91 -0
  99. package/server/lib/speaker-shadow-paired.ts +268 -0
  100. package/server/lib/speaker-shadow-report.ts +2 -2
  101. package/server/lib/speaker-shadow-worker.ts +50 -15
  102. package/server/lib/speaker-shadow.ts +80 -19
  103. package/server/lib/speaker-trainer.ts +2 -1
  104. package/server/lib/token-audit.ts +36 -6
  105. package/server/lib/transcript-cleanup.ts +3 -2
  106. package/server/lib/voice-directory.ts +10 -5
  107. package/server/lib/voice-profile-store.ts +24 -14
  108. package/server/lib/web-lookup-ledger.ts +151 -0
  109. package/server/lib/work-review-runtime.ts +2 -0
  110. package/server/lib/work-search.ts +3 -1
  111. package/server/lib/xlsx-text.ts +277 -0
  112. package/server/routes/decision-provider.ts +4 -1
  113. package/server/routes/jev.ts +5 -1
  114. package/server/routes/live-cues.ts +36 -14
  115. package/server/routes/meeting.ts +515 -61
  116. package/server/routes/session-find.ts +73 -0
  117. package/server/routes/transcribe-stream.ts +114 -33
  118. package/server/routes/voice.ts +28 -18
  119. package/server/scripts/live-cues-v3-mutation-gate.mjs +311 -0
  120. package/server/scripts/mac-voice-assignment-6930-mutation-gate.mjs +5 -5
  121. package/shared/live-cues-v2.ts +38 -5
  122. package/shared/media-attachment.ts +2 -0
  123. package/shared/meeting-ask.ts +11 -0
@@ -0,0 +1,499 @@
1
+ /** Exact HQ-word human decisions over immutable BASE. No enrollment, source,
2
+ * profile, Markdown, or shared corrections-ledger writes occur in this module. */
3
+ import { createHash, randomUUID } from 'node:crypto'
4
+ import { closeSync, constants, existsSync, fsyncSync, lstatSync, openSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
5
+ import { dirname } from 'node:path'
6
+ import { applyMacCallHumanAnnotation, isMacCallHumanName, macCallHumanPaths, readMacCallHumanFile, type MacCallHumanFiles } from './mac-speaker-call-human.js'
7
+ import { resolveMacSpeakerCallView, validateMacSpeakerCallPacket, type MacSpeakerCallPacket, type MacSpeakerCallView } from './mac-speaker-call-view.js'
8
+ import { createMacSpeakerCallIntervalProjector } from './mac-speaker-call-interval-projection.js'
9
+ import { isMacSpeakerHumanProtected } from './mac-speaker-protection.js'
10
+ import { isUnattributed } from './meeting-speaker-review.js'
11
+
12
+ const POLICY = 'mac-speaker-call-interval-human-v1'
13
+ const SHA = /^[a-f0-9]{64}$/
14
+ const MAX = 16 * 1024 * 1024
15
+ const hash = (s: string) => createHash('sha256').update(s).digest('hex')
16
+ const digest = (v: unknown) => hash(JSON.stringify(v))
17
+ const equal = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b)
18
+ function fail(s: string): never { throw new Error(`Mac interval human: ${s}`) }
19
+ export interface MacIntervalHumanMember {
20
+ id: string; text: string; elapsedMs: number; startElapsedMs: number; endElapsedMs: number; wordCount: 1
21
+ }
22
+ interface Word extends MacIntervalHumanMember {
23
+ batchIndex: number; wordIndex: number; wordSHA256: string; rawChunkIndices: number[]
24
+ }
25
+ interface Source {
26
+ baseSHA256: string; markdownSHA256: string; ledgerSHA256: string; legacyAnnotationSHA256: string | null
27
+ }
28
+ export interface MacIntervalHumanScope extends Source {
29
+ kind: 'interval-words' | 'unresolved-words' | 'human-words'; identityId: string; displayLabel: string; scopeSHA256: string
30
+ members: MacIntervalHumanMember[]; fieldCount: number; annotationSHA256: string | null
31
+ /** Each run contains consecutive actual HQ indices from exactly one batch. */
32
+ selectionRuns?: string[][]
33
+ /** Null records that the manual review had no applied automatic packet. */
34
+ projectionPacketSHA256?: string | null
35
+ /** Existing source labels cannot become a new exact-word confirmation. */
36
+ forbiddenNames: string[]
37
+ packetSHA256: string; supportSHA256: string; generation: string; packetRevision: number
38
+ }
39
+ export interface MacIntervalHumanInput {
40
+ baseJSON: string; markdown: string; ledgerJSONL: string; legacyAnnotationJSON: string | null
41
+ annotationJSON: string | null; packet: MacSpeakerCallPacket | null; view: MacSpeakerCallView
42
+ }
43
+ interface NameEvent {
44
+ kind: 'name'; id: string; at: string; identityId: string; displayLabel: string; to: string
45
+ scopeSHA256: string; packetSHA256: string; supportSHA256: string; generation: string; packetRevision: number
46
+ priorAnnotationSHA256: string | null; members: Word[]
47
+ reviewKind?: MacIntervalHumanScope['kind']; supersedesActionId?: string
48
+ projectionPacketSHA256?: string | null
49
+ }
50
+ interface UndoEvent { kind: 'undo'; id: string; at: string; actionId: string; priorAnnotationSHA256: string }
51
+ type Event = NameEvent | UndoEvent
52
+ interface Annotation extends Source {
53
+ version: 1; policy: typeof POLICY; captureId: string; revision: number
54
+ packets: MacSpeakerCallPacket[]; events: Event[]; annotationSHA256: string
55
+ }
56
+ export interface MacIntervalHumanMetadata {
57
+ revision: number; annotationSHA256: string
58
+ activeActions: Array<{ actionId: string; identityId: string; to: string; memberIds: string[] }>
59
+ confirmedWords: Array<{ batchIndex: number; wordIndex: number; label: string; wordSHA256: string }>
60
+ }
61
+ function source(input: Pick<MacIntervalHumanInput, 'baseJSON' | 'markdown' | 'ledgerJSONL' | 'legacyAnnotationJSON'>): Source {
62
+ if (Buffer.byteLength(input.baseJSON) > 32 * 1024 * 1024 || Buffer.byteLength(input.markdown) > MAX || Buffer.byteLength(input.ledgerJSONL) > 8 * 1024 * 1024 || Buffer.byteLength(input.legacyAnnotationJSON ?? '') > MAX) fail('source byte budget')
63
+ return { baseSHA256: hash(input.baseJSON), markdownSHA256: hash(input.markdown), ledgerSHA256: hash(input.ledgerJSONL), legacyAnnotationSHA256: input.legacyAnnotationJSON === null ? null : hash(input.legacyAnnotationJSON) }
64
+ }
65
+ function seal(body: Omit<Annotation, 'annotationSHA256'>): Annotation { return { ...body, annotationSHA256: digest(body) } }
66
+ function packetWords(baseJSON: string, ledger: string, packet: MacSpeakerCallPacket): Map<string, Word & { identityId: string }> {
67
+ validateMacSpeakerCallPacket(baseJSON, ledger, packet)
68
+ if (packet.version !== 2 || packet.projectionPolicy !== 'source-bound-intervals' || packet.admission !== 'qualified' || !packet.intervalSupport) fail('qualified interval packet required')
69
+ const base = JSON.parse(baseJSON)
70
+ const projector = createMacSpeakerCallIntervalProjector({ rawBindings: packet.rawBindings, groups: packet.groups, displayLabels: packet.displayLabels, protectedRawChunkIndices: packet.aliasProtectedRawChunkIndices }, packet.intervalSupport)
71
+ const words = projector.hqWords(base)
72
+ if (words.length > 40000) fail('word budget')
73
+ const out = new Map<string, Word & { identityId: string }>()
74
+ for (const w of words) {
75
+ const batch = base.batchSegments[w.batchIndex], row = batch.speakerWords[w.wordIndex], wordSHA256 = digest(row)
76
+ // The reviewer presents the stored batch clock. A supported capture-piece
77
+ // clock can differ after raw gaps/overlaps; naming must never display a
78
+ // different interval from the one explicitly reviewed. Keep stored timing
79
+ // untouched and admit only clocks agreeing within one 16kHz sample.
80
+ const offset = typeof batch.startElapsed === 'number' && Number.isFinite(batch.startElapsed) ? batch.startElapsed : 0
81
+ if (Math.abs(offset + row.start * 1000 - w.startElapsedMs) > 1 / 16 || Math.abs(offset + row.end * 1000 - w.endElapsedMs) > 1 / 16) continue
82
+ const id = digest({ policy: POLICY, baseSHA256: packet.baseSHA256, batchIndex: w.batchIndex, wordIndex: w.wordIndex, wordSHA256 })
83
+ const member: Word & { identityId: string } = { id, text: row.word.slice(0, 600), elapsedMs: w.startElapsedMs, startElapsedMs: w.startElapsedMs, endElapsedMs: w.endElapsedMs, wordCount: 1, batchIndex: w.batchIndex, wordIndex: w.wordIndex, wordSHA256, rawChunkIndices: w.rawChunkIndices, identityId: w.identityId }
84
+ if (out.has(id) || !Number.isFinite(member.startElapsedMs) || !Number.isFinite(member.endElapsedMs) || member.endElapsedMs <= member.startElapsedMs) fail('word geometry')
85
+ out.set(id, member)
86
+ }
87
+ return out
88
+ }
89
+ function publicMember(w: Word): MacIntervalHumanMember {
90
+ return { id: w.id, text: w.text, elapsedMs: w.elapsedMs, startElapsedMs: w.startElapsedMs, endElapsedMs: w.endElapsedMs, wordCount: 1 }
91
+ }
92
+ function cleanWord(w: Word & { identityId?: string }): Word { const { identityId: _, ...word } = w; return word }
93
+ const actionIdentity = (id: string) => `action-${id.slice(POLICY.length + 1)}`
94
+ function assertContiguousWords(members: Word[]): void {
95
+ if (!members.length || members.some((w, i) => w.batchIndex !== members[0].batchIndex || i > 0 && w.wordIndex !== members[i - 1].wordIndex + 1)) fail('manual selection must be one contiguous range in one batch')
96
+ }
97
+ function effectiveUnresolved(value: unknown): boolean {
98
+ if (!value || typeof value !== 'object' || isMacSpeakerHumanProtected(value)) return false
99
+ const speaker = (value as Record<string, unknown>).speaker ?? ''
100
+ return typeof speaker === 'string' && (isUnattributed(speaker) || /^(?:ext|unknown|unidentified(?: \d+| voice)?|speaker(?: \d+)?|voice [1-9]\d?)$/i.test(speaker.trim()))
101
+ }
102
+ function manualWords(baseJSON: string, packet: MacSpeakerCallPacket): Map<string, Word> {
103
+ const base = JSON.parse(baseJSON), out = new Map<string, Word>()
104
+ if (!packet.intervalSupport) fail('qualified interval packet required')
105
+ const projector = createMacSpeakerCallIntervalProjector({ rawBindings: packet.rawBindings, groups: packet.groups, displayLabels: packet.displayLabels, protectedRawChunkIndices: packet.aliasProtectedRawChunkIndices }, packet.intervalSupport)
106
+ const sources = projector.hqWordSources(base)
107
+ if (sources.length > 40000) fail('manual word budget')
108
+ for (const w of sources) {
109
+ const batch = base.batchSegments[w.batchIndex], row = batch.speakerWords[w.wordIndex]
110
+ if (!effectiveUnresolved(row)) continue
111
+ const offset = typeof batch.startElapsed === 'number' && Number.isFinite(batch.startElapsed) ? batch.startElapsed : 0
112
+ const startElapsedMs = offset + row.start * 1000, endElapsedMs = offset + row.end * 1000, wordSHA256 = digest(row)
113
+ if (!Number.isFinite(startElapsedMs) || !Number.isFinite(endElapsedMs) || startElapsedMs < 0 || endElapsedMs <= startElapsedMs) continue
114
+ const id = digest({ policy: POLICY, baseSHA256: packet.baseSHA256, batchIndex: w.batchIndex, wordIndex: w.wordIndex, wordSHA256 })
115
+ if (out.has(id)) fail('duplicate manual word')
116
+ out.set(id, { id, text: row.word.slice(0, 600), elapsedMs: startElapsedMs, startElapsedMs, endElapsedMs, wordCount: 1, batchIndex: w.batchIndex, wordIndex: w.wordIndex, wordSHA256, rawChunkIndices: w.rawChunkIndices })
117
+ }
118
+ return out
119
+ }
120
+ function qualifiedPacket(input: Pick<MacIntervalHumanInput, 'baseJSON' | 'ledgerJSONL' | 'packet'>): MacSpeakerCallPacket | null {
121
+ try {
122
+ const p = input.packet
123
+ if (!p || p.version !== 2 || p.admission !== 'qualified' || p.projectionPolicy !== 'source-bound-intervals' || !p.intervalSupport) return null
124
+ validateMacSpeakerCallPacket(input.baseJSON, input.ledgerJSONL, p)
125
+ return p
126
+ } catch { return null } // Automatic withdrawal never invalidates an audited human action.
127
+ }
128
+ function legacySource(input: Pick<MacIntervalHumanInput, 'baseJSON' | 'ledgerJSONL' | 'legacyAnnotationJSON'>): MacSpeakerCallView {
129
+ return applyMacCallHumanAnnotation(input.baseJSON, input.ledgerJSONL, input.legacyAnnotationJSON, resolveMacSpeakerCallView(input.baseJSON, input.ledgerJSONL, null))
130
+ }
131
+ /** Inspect all source speaker fields, including protected/nested HQ surfaces.
132
+ * Interval annotations are deliberately excluded, so disjoint exact words may
133
+ * share a name assigned by this mechanism without absorbing old source text. */
134
+ function forbiddenSourceNames(base: unknown, legacy: unknown): string[] {
135
+ const labels = new Set<string>(), pending: unknown[] = [base, legacy]
136
+ let count = 0
137
+ while (pending.length) {
138
+ const value = pending.pop()
139
+ if (!value || typeof value !== 'object') continue
140
+ if (++count > 500000) fail('source field budget')
141
+ if (Array.isArray(value)) { for (const child of value) if (child && typeof child === 'object') pending.push(child); continue }
142
+ const row = value as Record<string, unknown>
143
+ if (typeof row.speaker === 'string' && isMacCallHumanName(row.speaker)) labels.add(row.speaker)
144
+ for (const v of Object.values(row)) if (v && typeof v === 'object') pending.push(v)
145
+ }
146
+ if (labels.size > 40000) fail('source label budget')
147
+ return [...labels].sort()
148
+ }
149
+ function assertFreshTarget(to: string, forbiddenNames: readonly string[]): void {
150
+ const key = (s: string) => s.normalize('NFKC').toLowerCase()
151
+ if (forbiddenNames.some(name => key(name) === key(to))) fail('target name already exists in source')
152
+ }
153
+ function validate(input: Omit<MacIntervalHumanInput, 'packet' | 'view'>): { annotation: Annotation | null; active: NameEvent[]; undone: Set<string> } {
154
+ const binding = source(input)
155
+ if (input.annotationJSON === null) return { annotation: null, active: [], undone: new Set() }
156
+ if (Buffer.byteLength(input.annotationJSON) > MAX) fail('annotation byte budget')
157
+ const a = JSON.parse(input.annotationJSON) as Annotation, base = JSON.parse(input.baseJSON)
158
+ const { annotationSHA256, ...body } = a ?? {}
159
+ if (!a || a.version !== 1 || a.policy !== POLICY || a.captureId !== base.sessionId || !equal(binding, { baseSHA256: a.baseSHA256, markdownSHA256: a.markdownSHA256, ledgerSHA256: a.ledgerSHA256, legacyAnnotationSHA256: a.legacyAnnotationSHA256 }) || !SHA.test(annotationSHA256) || annotationSHA256 !== digest(body) || !Number.isSafeInteger(a.revision) || a.revision < 1 || !Array.isArray(a.events) || a.events.length !== a.revision || a.events.length > 6000 || !Array.isArray(a.packets) || !a.packets.length || a.packets.length > 32) fail('annotation source or integrity changed')
160
+ const referenced = new Set(a.events.filter(e => e?.kind === 'name').map(e => (e as NameEvent).packetSHA256))
161
+ if (a.packets.some(p => !p || !referenced.has(p.packetSHA256))) fail('unreferenced audited packet')
162
+ const packets = new Map<string, MacSpeakerCallPacket>(), words = new Map<string, ReturnType<typeof packetWords>>()
163
+ let projectedWordCount = 0
164
+ for (const p of a.packets) {
165
+ if (packets.has(p.packetSHA256)) fail('duplicate packet')
166
+ const projected = packetWords(input.baseJSON, input.ledgerJSONL, p)
167
+ if ((projectedWordCount += projected.size) > 40000) fail('aggregate audited word budget')
168
+ packets.set(p.packetSHA256, p); words.set(p.packetSHA256, projected)
169
+ }
170
+ // Validate v1 first, including its shared-ledger coverage and protected sources.
171
+ const legacy = legacySource(input), forbiddenNames = forbiddenSourceNames(base, legacy.doc)
172
+ const seen = new Set<string>(), names = new Map<string, NameEvent>(), active = new Map<string, NameEvent>(), occupied = new Set<string>(), usedPackets = new Set<string>(), undone = new Set<string>()
173
+ const manual = new Map<string, Map<string, Word>>()
174
+ const prefixPackets: MacSpeakerCallPacket[] = []
175
+ let memberCount = 0, auditVerificationBytes = 0
176
+ for (const [index, event] of a.events.entries()) {
177
+ if (!event || typeof event.id !== 'string' || !event.id.startsWith(`${POLICY}:`) || seen.has(event.id) || typeof event.at !== 'string' || !Number.isFinite(Date.parse(event.at))) fail('audit event')
178
+ seen.add(event.id)
179
+ if (event.priorAnnotationSHA256 !== null && !SHA.test(event.priorAnnotationSHA256) || index === 0 && event.priorAnnotationSHA256 !== null || index > 0 && event.priorAnnotationSHA256 === null) fail('audit chain')
180
+ if (index > 0) {
181
+ // Replay the exact preceding envelope, adding a retained packet only on
182
+ // its first name event. This authenticates the append-only prefix rather
183
+ // than accepting any syntactically valid predecessor hash. Bound total
184
+ // hashing work as well as retained bytes for adversarial local inputs.
185
+ const prefix = JSON.stringify({ version: 1, policy: POLICY, captureId: a.captureId, ...binding, revision: index, packets: prefixPackets, events: a.events.slice(0, index) })
186
+ if ((auditVerificationBytes += Buffer.byteLength(prefix)) > 256 * 1024 * 1024) fail('audit verification budget')
187
+ if (event.priorAnnotationSHA256 !== hash(prefix)) fail('audit chain predecessor changed')
188
+ }
189
+ if (event.kind === 'undo') {
190
+ const target = names.get(event.actionId)
191
+ if (!target || !active.has(event.actionId)) fail('undo action')
192
+ const ids = new Set(target.members.map(m => m.id))
193
+ if ([...active.values()].some(e => e !== target && a.events.indexOf(e) > a.events.indexOf(target) && e.members.some(m => ids.has(m.id)))) fail('undo overlaps later active edit')
194
+ active.delete(event.actionId)
195
+ target.members.forEach(m => occupied.delete(m.id))
196
+ undone.add(event.actionId)
197
+ if (target.supersedesActionId) {
198
+ const predecessor = names.get(target.supersedesActionId)
199
+ if (!predecessor || undone.has(predecessor.id) || predecessor.members.some(m => occupied.has(m.id))) fail('rename predecessor conflict')
200
+ active.set(predecessor.id, predecessor); predecessor.members.forEach(m => occupied.add(m.id))
201
+ }
202
+ continue
203
+ }
204
+ if (event.kind !== 'name' || !isMacCallHumanName(event.to) || !SHA.test(event.scopeSHA256) || !Array.isArray(event.members) || !event.members.length || (memberCount += event.members.length) > 40000 || new Set(event.members.map(m => m.id)).size !== event.members.length) fail('name event')
205
+ assertFreshTarget(event.to, forbiddenNames)
206
+ const packet = packets.get(event.packetSHA256)
207
+ if (!packet || packet.generation !== event.generation || packet.revision !== event.packetRevision || packet.intervalSupport!.supportSHA256 !== event.supportSHA256) fail('audited packet binding')
208
+ const reviewKind = event.reviewKind ?? 'interval-words'
209
+ let expectedWords: Map<string, Word | (Word & { identityId: string })> = words.get(packet.packetSHA256)!
210
+ if (reviewKind === 'human-words') {
211
+ const predecessor = event.supersedesActionId ? active.get(event.supersedesActionId) : undefined
212
+ if (!predecessor || event.identityId !== actionIdentity(predecessor.id) || event.displayLabel !== predecessor.to || event.packetSHA256 !== predecessor.packetSHA256 || !equal(event.members, predecessor.members)) fail('rename must supersede one whole active action')
213
+ active.delete(predecessor.id); predecessor.members.forEach(m => occupied.delete(m.id))
214
+ expectedWords = new Map(predecessor.members.map(m => [m.id, m]))
215
+ } else if (reviewKind === 'unresolved-words') {
216
+ if (event.supersedesActionId !== undefined || event.identityId !== 'unresolved' || event.displayLabel !== 'Unresolved speech' || event.projectionPacketSHA256 !== null && event.projectionPacketSHA256 !== event.packetSHA256) fail('manual review identity')
217
+ assertContiguousWords(event.members)
218
+ if (!manual.has(packet.packetSHA256)) {
219
+ const candidates = manualWords(input.baseJSON, packet)
220
+ if ((projectedWordCount += candidates.size) > 40000) fail('aggregate audited word budget')
221
+ manual.set(packet.packetSHA256, candidates)
222
+ }
223
+ expectedWords = manual.get(packet.packetSHA256)!
224
+ const projection = event.projectionPacketSHA256 ? resolveMacSpeakerCallView(input.baseJSON, input.ledgerJSONL, packet).doc : base
225
+ for (const m of event.members) if (!effectiveUnresolved(projection.batchSegments?.[m.batchIndex]?.speakerWords?.[m.wordIndex])) fail('manual word was not unresolved')
226
+ } else if (reviewKind !== 'interval-words' || event.supersedesActionId !== undefined || packet.displayLabels[event.identityId] !== event.displayLabel) fail('audited group binding')
227
+ if (!usedPackets.has(packet.packetSHA256)) prefixPackets.push(packet)
228
+ usedPackets.add(packet.packetSHA256)
229
+ for (const member of event.members) {
230
+ const actual = expectedWords.get(member.id)
231
+ const row = legacy.doc.batchSegments?.[member.batchIndex]?.speakerWords?.[member.wordIndex]
232
+ if (!actual || reviewKind === 'interval-words' && (!('identityId' in actual) || actual.identityId !== event.identityId) || !equal(cleanWord(actual), member) || !row || digest(row) !== member.wordSHA256 || isMacSpeakerHumanProtected(row)) fail('audited word source changed')
233
+ // Names are allowed only over anonymous projected words. Earlier active
234
+ // human decisions cannot be overwritten by manufacturing another event.
235
+ if (occupied.has(member.id)) fail('overlapping active human decision')
236
+ }
237
+ event.members.forEach(m => occupied.add(m.id))
238
+ names.set(event.id, event); active.set(event.id, event)
239
+ }
240
+ if (usedPackets.size !== packets.size) fail('unreferenced audited packet')
241
+ if (!equal(a.packets.map(p => p.packetSHA256), prefixPackets.map(p => p.packetSHA256))) fail('audited packet order changed')
242
+ return { annotation: a, active: [...active.values()], undone }
243
+ }
244
+ function canonical(input: Omit<MacIntervalHumanInput, 'view'>): MacSpeakerCallView {
245
+ const automatic = resolveMacSpeakerCallView(input.baseJSON, input.ledgerJSONL, input.packet)
246
+ const legacy = applyMacCallHumanAnnotation(input.baseJSON, input.ledgerJSONL, input.legacyAnnotationJSON, automatic)
247
+ return applyMacIntervalHumanAnnotation({ ...input, view: legacy })
248
+ }
249
+ export function applyMacIntervalHumanAnnotation(input: MacIntervalHumanInput): MacSpeakerCallView {
250
+ const { annotation, active } = validate(input)
251
+ if (!annotation) return input.view
252
+ const doc = structuredClone(input.view.doc), base = JSON.parse(input.baseJSON), confirmedWords: MacIntervalHumanMetadata['confirmedWords'] = []
253
+ for (const event of active) for (const m of event.members) {
254
+ const row = doc.batchSegments?.[m.batchIndex]?.speakerWords?.[m.wordIndex], original = base.batchSegments[m.batchIndex].speakerWords[m.wordIndex]
255
+ // The source-bound decision survives packet withdrawal, but never replaces
256
+ // a new protected claim, changed word, or differently named legacy field.
257
+ if (!row || isMacSpeakerHumanProtected(row) || !equal({ ...row, speaker: original.speaker }, original) || isMacCallHumanName(row.speaker) && row.speaker !== event.to && row.speaker !== original.speaker) fail('current word conflicts with human decision')
258
+ row.speaker = event.to
259
+ confirmedWords.push({ batchIndex: m.batchIndex, wordIndex: m.wordIndex, label: event.to, wordSHA256: m.wordSHA256 })
260
+ }
261
+ const metadata: MacIntervalHumanMetadata = { revision: annotation.revision, annotationSHA256: annotation.annotationSHA256, activeActions: active.map(e => ({ actionId: e.id, identityId: actionIdentity(e.id), to: e.to, memberIds: e.members.map(m => m.id) })), confirmedWords }
262
+ return { ...input.view, doc, metadata: { ...input.view.metadata, intervalHumanAnnotation: metadata } } as MacSpeakerCallView
263
+ }
264
+ export function buildMacIntervalHumanScopes(input: MacIntervalHumanInput): MacIntervalHumanScope[] {
265
+ const binding = source(input), checked = canonical(input), { annotation, active } = validate(input)
266
+ if (!equal(input.view.doc, checked.doc) || input.view.metadata.status !== checked.metadata.status || input.view.metadata.packetSHA256 !== checked.metadata.packetSHA256) fail('unvalidated reviewed view')
267
+ const current = qualifiedPacket(input), packet = current ?? annotation?.packets.at(-1) ?? null
268
+ const forbiddenNames = forbiddenSourceNames(JSON.parse(input.baseJSON), legacySource(input).doc)
269
+ Object.freeze(forbiddenNames)
270
+ const result: MacIntervalHumanScope[] = []
271
+ const add = (kind: MacIntervalHumanScope['kind'], identityId: string, displayLabel: string, members: MacIntervalHumanMember[], basis: MacSpeakerCallPacket, extra: Pick<MacIntervalHumanScope, 'selectionRuns' | 'projectionPacketSHA256'> = {}) => {
272
+ const body = { kind, identityId, displayLabel, members, fieldCount: members.length, forbiddenNames, ...extra, ...binding, annotationSHA256: input.annotationJSON === null ? null : hash(input.annotationJSON), packetSHA256: basis.packetSHA256, supportSHA256: basis.intervalSupport!.supportSHA256, generation: basis.generation, packetRevision: basis.revision }
273
+ const scope = { ...body, scopeSHA256: digest(body) }
274
+ members.forEach(Object.freeze); Object.freeze(members)
275
+ if (extra.selectionRuns) { extra.selectionRuns.forEach(Object.freeze); Object.freeze(extra.selectionRuns) }
276
+ result.push(Object.freeze(scope))
277
+ }
278
+ const applied = current && checked.metadata.status === 'applied' && checked.metadata.packetSHA256 === current.packetSHA256
279
+ if (applied) {
280
+ const words = packetWords(input.baseJSON, input.ledgerJSONL, current), groups = new Map<string, MacIntervalHumanMember[]>()
281
+ for (const w of words.values()) {
282
+ const row = checked.doc.batchSegments?.[w.batchIndex]?.speakerWords?.[w.wordIndex], label = current.displayLabels[w.identityId]
283
+ if (!row || row.speaker !== label || isMacSpeakerHumanProtected(row) || !checked.metadata.groups.some(g => g.identityId === w.identityId && g.label === label)) continue
284
+ const list = groups.get(w.identityId) ?? []; list.push(publicMember(w)); groups.set(w.identityId, list)
285
+ }
286
+ for (const [id, members] of groups) add('interval-words', id, current.displayLabels[id], members, current)
287
+ }
288
+ if (packet) {
289
+ const legacy = legacySource(input), members: MacIntervalHumanMember[] = [], runs: string[][] = []
290
+ let previous: Word | undefined
291
+ for (const word of manualWords(input.baseJSON, packet).values()) {
292
+ const row = checked.doc.batchSegments?.[word.batchIndex]?.speakerWords?.[word.wordIndex], old = legacy.doc.batchSegments?.[word.batchIndex]?.speakerWords?.[word.wordIndex]
293
+ if (!effectiveUnresolved(row) || !old || digest(old) !== word.wordSHA256 || isMacSpeakerHumanProtected(old)) continue
294
+ if (!previous || previous.batchIndex !== word.batchIndex || previous.wordIndex + 1 !== word.wordIndex) runs.push([])
295
+ runs.at(-1)!.push(word.id); members.push(publicMember(word)); previous = word
296
+ }
297
+ if (members.length) add('unresolved-words', 'unresolved', 'Unresolved speech', members, packet, { selectionRuns: runs, projectionPacketSHA256: applied ? current.packetSHA256 : null })
298
+ }
299
+ for (const action of active) {
300
+ const basis = annotation!.packets.find(p => p.packetSHA256 === action.packetSHA256)
301
+ if (!basis) fail('active action packet missing')
302
+ add('human-words', actionIdentity(action.id), action.to, action.members.map(publicMember), basis)
303
+ }
304
+ return result
305
+ }
306
+ export function previewMacIntervalHumanAnnotation(scope: MacIntervalHumanScope, memberIds: string[], to: string): { members: MacIntervalHumanMember[]; fields: number } {
307
+ if (!isMacCallHumanName(to) || !Array.isArray(memberIds) || !memberIds.length || memberIds.length > 40000 || memberIds.some(id => typeof id !== 'string' || !SHA.test(id)) || new Set(memberIds).size !== memberIds.length) fail('explicit name and word IDs required')
308
+ const { scopeSHA256, ...body } = scope
309
+ if (!['interval-words', 'unresolved-words', 'human-words'].includes(scope.kind) || scopeSHA256 !== digest(body)) fail('reviewed scope integrity')
310
+ if (!Array.isArray(scope.forbiddenNames) || scope.forbiddenNames.length > 40000 || scope.forbiddenNames.some(name => typeof name !== 'string')) fail('reviewed source labels')
311
+ assertFreshTarget(to, scope.forbiddenNames)
312
+ const byId = new Map(scope.members.map(m => [m.id, m]))
313
+ let members = memberIds.map(id => byId.get(id) ?? fail('word outside reviewed group'))
314
+ if (scope.kind === 'unresolved-words') {
315
+ const selected = new Set(memberIds), runs = scope.selectionRuns
316
+ members = scope.members.filter(m => selected.has(m.id))
317
+ if (scope.identityId !== 'unresolved' || scope.displayLabel !== 'Unresolved speech' || !Array.isArray(runs) || runs.length > 40000 || runs.some(r => !Array.isArray(r) || !r.length) || !equal(runs.flat(), scope.members.map(m => m.id))
318
+ || !runs.some(run => { const start = run.indexOf(members[0].id); return start >= 0 && equal(run.slice(start, start + members.length), members.map(m => m.id)) })) fail('manual selection must be one contiguous range in one batch')
319
+ } else if (scope.kind === 'human-words') {
320
+ if (!/^action-[a-f0-9-]{36}$/.test(scope.identityId) || members.length !== scope.members.length) fail('rename requires the whole active action')
321
+ members = scope.members.slice()
322
+ }
323
+ return { members, fields: members.length }
324
+ }
325
+
326
+ export type MacIntervalHumanFiles = MacCallHumanFiles
327
+ export function macIntervalHumanPaths(files: MacIntervalHumanFiles, captureId: string) {
328
+ const old = macCallHumanPaths(files, captureId), stem = files.sidecarPath.replace(/\.g2-chunks\.json$/, '')
329
+ return { annotation: `${stem}.speaker-call-interval-human.json`, journal: `${stem}.speaker-call-interval-human-transaction.json`, packet: old.packet, legacyAnnotation: old.annotation, ledger: old.ledger, lock: old.lock }
330
+ }
331
+ function directory(path: string) { const dir = dirname(path); if (realpathSync(dir) !== dir || !lstatSync(dir).isDirectory()) fail('canonical parent required'); return dir }
332
+ function syncDirectory(path: string) { const fd = openSync(directory(path), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW | constants.O_NONBLOCK); try { fsyncSync(fd) } finally { closeSync(fd) } }
333
+ function atomic(path: string, text: string, before: () => void) {
334
+ directory(path); const tmp = `${path}.${randomUUID()}.tmp`, fd = openSync(tmp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o600)
335
+ try { writeFileSync(fd, text); fsyncSync(fd) } finally { closeSync(fd) }
336
+ try { before(); renameSync(tmp, path); syncDirectory(path) } finally { if (existsSync(tmp)) unlinkSync(tmp) }
337
+ }
338
+ function acquire(path: string, journal: string, recover: boolean, expectedLockSHA256?: string) {
339
+ const old = readMacCallHumanFile(path, 4096, true)
340
+ if (expectedLockSHA256 !== undefined && old?.hash !== expectedLockSHA256) fail('lock drift')
341
+ if (old) {
342
+ if (!recover) fail('capture locked')
343
+ const owner = JSON.parse(old.text)
344
+ if (!Number.isSafeInteger(owner.pid) || owner.pid < 1 || owner.kind !== POLICY || owner.journal !== journal) fail('foreign lock')
345
+ try { process.kill(owner.pid, 0); fail('capture lock owner alive') } catch (e) { if ((e as NodeJS.ErrnoException).code !== 'ESRCH') throw e }
346
+ if (readMacCallHumanFile(path, 4096)!.hash !== old.hash) fail('lock drift')
347
+ unlinkSync(path); syncDirectory(path)
348
+ }
349
+ directory(path)
350
+ const text = JSON.stringify({ kind: POLICY, journal, pid: process.pid, token: randomUUID() }), fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o600)
351
+ try { writeFileSync(fd, text); fsyncSync(fd) } finally { closeSync(fd) }
352
+ syncDirectory(path)
353
+ const assertOwned = () => { if (readMacCallHumanFile(path, 4096, true)?.text !== text) fail('lock drift') }
354
+ return { assertOwned, release: () => { assertOwned(); unlinkSync(path); syncDirectory(path) } }
355
+ }
356
+ interface Pin { path: string; hash: string | null; maximum: number }
357
+ interface Journal { version: 1; policy: typeof POLICY; captureId: string; phase: 'prepared' | 'complete'; pins: Pin[]; before: string | null; after: string; journalSHA256: string }
358
+ function journalSeal(body: Omit<Journal, 'journalSHA256'>): Journal { return { ...body, journalSHA256: digest(body) } }
359
+ function readSources(files: MacIntervalHumanFiles, captureId: string) {
360
+ const paths = macIntervalHumanPaths(files, captureId)
361
+ const base = readMacCallHumanFile(files.sidecarPath, 32 * 1024 * 1024)!, md = readMacCallHumanFile(files.meetingPath, MAX)!, ledger = readMacCallHumanFile(paths.ledger, 8 * 1024 * 1024, true), legacy = readMacCallHumanFile(paths.legacyAnnotation, MAX, true), packet = readMacCallHumanFile(paths.packet, MAX, true), prior = readMacCallHumanFile(paths.annotation, MAX, true)
362
+ // An unreadable automatic JSON payload is unavailable projection, not a
363
+ // reason to strand a source-bound human undo. Keep its exact bytes pinned.
364
+ // Human/legacy annotations remain strict JSON in their own validators.
365
+ let parsedPacket: MacSpeakerCallPacket | null = null
366
+ if (packet) { try { parsedPacket = JSON.parse(packet.text) as MacSpeakerCallPacket } catch { /* unavailable current automatic packet */ } }
367
+ const input = { baseJSON: base.text, markdown: md.text, ledgerJSONL: ledger?.text ?? '', legacyAnnotationJSON: legacy?.text ?? null, annotationJSON: prior?.text ?? null, packet: parsedPacket }
368
+ const pins: Pin[] = [{ path: files.sidecarPath, hash: base.hash, maximum: 32 * 1024 * 1024 }, { path: files.meetingPath, hash: md.hash, maximum: MAX }, { path: paths.ledger, hash: ledger?.hash ?? null, maximum: 8 * 1024 * 1024 }, { path: paths.legacyAnnotation, hash: legacy?.hash ?? null, maximum: MAX }, { path: paths.packet, hash: packet?.hash ?? null, maximum: MAX }]
369
+ return { paths, input, pins }
370
+ }
371
+ function checkPins(pins: Pin[]) { for (const p of pins) if ((readMacCallHumanFile(p.path, p.maximum, p.hash === null)?.hash ?? null) !== p.hash) fail('source changed') }
372
+ function checkedJournal(files: MacIntervalHumanFiles, captureId: string, value: unknown): Journal {
373
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('journal scope or bounds')
374
+ const journal = value as Journal, paths = macIntervalHumanPaths(files, captureId), { journalSHA256, ...body } = journal
375
+ if (journal.version !== 1 || journal.policy !== POLICY || journal.captureId !== captureId || !SHA.test(journalSHA256) || digest(body) !== journalSHA256 || !['prepared', 'complete'].includes(journal.phase) || !Array.isArray(journal.pins) || !equal(journal.pins.map(p => p.path), [files.sidecarPath, files.meetingPath, paths.ledger, paths.legacyAnnotation, paths.packet]) || !equal(journal.pins.map(p => p.maximum), [32 * 1024 * 1024, MAX, 8 * 1024 * 1024, MAX, MAX]) || journal.pins.some(p => p.hash !== null && !SHA.test(p.hash)) || journal.before !== null && typeof journal.before !== 'string' || typeof journal.after !== 'string' || Buffer.byteLength(journal.before ?? '') > MAX || Buffer.byteLength(journal.after) > MAX) fail('journal scope or bounds')
376
+ return journal
377
+ }
378
+ /** Read-only receipt verification shared by readers, publication, and recovery.
379
+ * Completed receipts preserve the exact human postimage and immutable sources;
380
+ * a newer/missing/malformed current automatic packet is allowed after completion.
381
+ * A prepared transaction must still match every original source pin. */
382
+ function verifyJournalSnapshot(files: MacIntervalHumanFiles, captureId: string, value: unknown, expectedText: string): Journal {
383
+ const journal = checkedJournal(files, captureId, value), state = readSources(files, captureId)
384
+ const pins = journal.phase === 'complete' ? journal.pins.slice(0, 4) : journal.pins
385
+ checkPins(pins)
386
+ const current = state.input.annotationJSON
387
+ if (journal.phase === 'complete' ? current !== journal.after : current !== journal.before && current !== journal.after) fail('journal annotation postimage changed')
388
+ validate({ ...state.input, annotationJSON: journal.after })
389
+ if (current !== null && current !== journal.after) validate(state.input)
390
+ // Catch a concurrent replacement during the potentially expensive pure replay.
391
+ checkPins(pins)
392
+ if (readMacCallHumanFile(state.paths.journal, 40 * 1024 * 1024)!.text !== expectedText || (readMacCallHumanFile(state.paths.annotation, MAX, true)?.text ?? null) !== current) fail('journal snapshot changed')
393
+ return journal
394
+ }
395
+ export interface MacIntervalHumanJournalStatus {
396
+ status: 'absent' | 'complete' | 'prepared' | 'recovery-invalid'
397
+ reason?: string
398
+ }
399
+ /** Bounded inspection only: no locks, recovery, file creation, or mutation. */
400
+ export function inspectMacIntervalHumanJournal(files: MacIntervalHumanFiles, captureId: string): MacIntervalHumanJournalStatus {
401
+ try {
402
+ const paths = macIntervalHumanPaths(files, captureId), stored = readMacCallHumanFile(paths.journal, 40 * 1024 * 1024, true)
403
+ if (!stored) {
404
+ const lock = readMacCallHumanFile(paths.lock, 4096, true)
405
+ if (lock) {
406
+ let owner: any
407
+ try { owner = JSON.parse(lock.text) } catch { /* Foreign lock handling belongs to its owner. */ }
408
+ if (owner?.kind === POLICY && owner.journal === paths.journal) return { status: 'recovery-invalid', reason: 'Interval-human lock without a durable intent; explicit dead-owner recovery required' }
409
+ }
410
+ return { status: 'absent' }
411
+ }
412
+ const journal = verifyJournalSnapshot(files, captureId, JSON.parse(stored.text), stored.text)
413
+ return { status: journal.phase }
414
+ } catch (error) { return { status: 'recovery-invalid', reason: error instanceof Error ? error.message : 'Invalid interval annotation journal' } }
415
+ }
416
+ function runJournal(files: MacIntervalHumanFiles, captureId: string, value: Journal, expectedText: string, assertOwned: () => void, afterWrite?: (index: number) => void) {
417
+ const journal = checkedJournal(files, captureId, value), paths = macIntervalHumanPaths(files, captureId), { journalSHA256: _, ...body } = journal
418
+ const fence = () => { assertOwned(); if (readMacCallHumanFile(paths.journal, 40 * 1024 * 1024)!.text !== expectedText) fail('journal changed'); const text = readMacCallHumanFile(paths.annotation, MAX, true)?.text ?? null; if (text !== journal.before && text !== journal.after) fail('later human edit'); return text }
419
+ if (journal.phase === 'complete') { if (fence() !== journal.after) fail('completed annotation changed'); verifyJournalSnapshot(files, captureId, journal, expectedText); assertOwned(); return }
420
+ checkPins(journal.pins)
421
+ const current = readSources(files, captureId)
422
+ validate({ ...current.input, annotationJSON: journal.after })
423
+ if (fence() !== journal.after) atomic(paths.annotation, journal.after, () => { fence(); checkPins(journal.pins) })
424
+ afterWrite?.(0)
425
+ if (fence() !== journal.after) fail('publication changed')
426
+ checkPins(journal.pins)
427
+ const complete = `${JSON.stringify(journalSeal({ ...body, phase: 'complete' }))}\n`
428
+ atomic(paths.journal, complete, () => { if (fence() !== journal.after) fail('publication changed'); checkPins(journal.pins) })
429
+ if (readMacCallHumanFile(paths.journal, 40 * 1024 * 1024)!.text !== complete) fail('journal readback changed')
430
+ }
431
+ function publish(files: MacIntervalHumanFiles, captureId: string, state: ReturnType<typeof readSources>, annotation: Annotation, assertOwned: () => void, afterWrite?: (index: number) => void) {
432
+ const after = `${JSON.stringify(annotation)}\n`
433
+ if (Buffer.byteLength(after) > MAX) fail('annotation byte budget')
434
+ validate({ ...state.input, annotationJSON: after })
435
+ const priorJournal = readMacCallHumanFile(state.paths.journal, 40 * 1024 * 1024, true)
436
+ if (priorJournal) {
437
+ const parsed = JSON.parse(priorJournal.text) as Journal
438
+ if (parsed.phase !== 'complete') fail('annotation recovery required')
439
+ runJournal(files, captureId, parsed, priorJournal.text, assertOwned)
440
+ }
441
+ const journal = journalSeal({ version: 1, policy: POLICY, captureId, phase: 'prepared', pins: state.pins, before: state.input.annotationJSON, after }), text = `${JSON.stringify(journal)}\n`
442
+ atomic(state.paths.journal, text, () => { assertOwned(); checkPins(state.pins); if ((readMacCallHumanFile(state.paths.annotation, MAX, true)?.text ?? null) !== state.input.annotationJSON || (readMacCallHumanFile(state.paths.journal, 40 * 1024 * 1024, true)?.text ?? null) !== (priorJournal?.text ?? null)) fail('publication precondition changed') })
443
+ runJournal(files, captureId, journal, text, assertOwned, afterWrite)
444
+ }
445
+ export function commitMacIntervalHumanAnnotation(input: { files: MacIntervalHumanFiles; captureId: string; scope: MacIntervalHumanScope; memberIds: string[]; to: string; afterWrite?: (index: number) => void }): { correctionId: string; revision: number; members: number; fields: number } {
446
+ previewMacIntervalHumanAnnotation(input.scope, input.memberIds, input.to)
447
+ const paths = macIntervalHumanPaths(input.files, input.captureId), guard = acquire(paths.lock, paths.journal, false)
448
+ try {
449
+ const state = readSources(input.files, input.captureId), view = canonical(state.input), scopes = buildMacIntervalHumanScopes({ ...state.input, view }), scope = scopes.find(s => s.scopeSHA256 === input.scope.scopeSHA256)
450
+ if (!scope || !equal(scope, input.scope)) fail('reviewed source changed')
451
+ const selected = previewMacIntervalHumanAnnotation(scope, input.memberIds, input.to), { annotation: existing, active } = validate(state.input)
452
+ const packet = [qualifiedPacket(state.input), ...(existing?.packets ?? [])].find(p => p?.packetSHA256 === scope.packetSHA256)
453
+ if (!packet) fail('reviewed packet unavailable')
454
+ const predecessor = scope.kind === 'human-words' ? active.find(a => actionIdentity(a.id) === scope.identityId) : undefined
455
+ if (scope.kind === 'human-words' && !predecessor) fail('reviewed active action changed')
456
+ const words = predecessor ? new Map(predecessor.members.map(m => [m.id, m])) : scope.kind === 'unresolved-words' ? manualWords(state.input.baseJSON, packet) : packetWords(state.input.baseJSON, state.input.ledgerJSONL, packet)
457
+ const members = selected.members.map(m => { const word = words.get(m.id); if (!word) fail('reviewed word unavailable'); return cleanWord(word) })
458
+ const id = `${POLICY}:${randomUUID()}`, event: NameEvent = { kind: 'name', id, at: new Date().toISOString(), identityId: scope.identityId, displayLabel: scope.displayLabel, to: input.to, scopeSHA256: scope.scopeSHA256, packetSHA256: packet.packetSHA256, supportSHA256: scope.supportSHA256, generation: scope.generation, packetRevision: scope.packetRevision, priorAnnotationSHA256: existing?.annotationSHA256 ?? null, members, reviewKind: scope.kind, ...(predecessor ? { supersedesActionId: predecessor.id } : {}), ...(scope.kind === 'unresolved-words' ? { projectionPacketSHA256: scope.projectionPacketSHA256 } : {}) }
459
+ const annotation = seal({ version: 1, policy: POLICY, captureId: input.captureId, ...source(state.input), revision: (existing?.revision ?? 0) + 1, packets: existing?.packets.some(p => p.packetSHA256 === packet.packetSHA256) ? existing.packets : [...(existing?.packets ?? []), packet], events: [...(existing?.events ?? []), event] })
460
+ publish(input.files, input.captureId, state, annotation, guard.assertOwned, input.afterWrite)
461
+ return { correctionId: id, revision: annotation.revision, members: selected.members.length, fields: selected.fields }
462
+ } finally { guard.release() }
463
+ }
464
+ export function undoMacIntervalHumanAnnotation(input: { files: MacIntervalHumanFiles; captureId: string; actionId: string; revision: number; annotationSHA256: string; afterWrite?: (index: number) => void }): { correctionId: string; revision: number; undone: boolean } {
465
+ const paths = macIntervalHumanPaths(input.files, input.captureId), guard = acquire(paths.lock, paths.journal, false)
466
+ try {
467
+ const journal = inspectMacIntervalHumanJournal(input.files, input.captureId)
468
+ if (journal.status === 'prepared' || journal.status === 'recovery-invalid') fail('annotation recovery required')
469
+ const state = readSources(input.files, input.captureId), { annotation: existing, active, undone } = validate(state.input)
470
+ if (!existing || !existing.events.some(e => e.kind === 'name' && e.id === input.actionId)) fail('unknown undo action')
471
+ if (!active.some(e => e.id === input.actionId)) {
472
+ if (!undone.has(input.actionId)) fail('action has an active superseding edit; undo latest rename first')
473
+ return { correctionId: input.actionId, revision: existing.revision, undone: false }
474
+ }
475
+ if (existing.revision !== input.revision || existing.annotationSHA256 !== input.annotationSHA256) fail('reviewed annotation changed')
476
+ const event: UndoEvent = { kind: 'undo', id: `${POLICY}:${randomUUID()}`, at: new Date().toISOString(), actionId: input.actionId, priorAnnotationSHA256: existing.annotationSHA256 }
477
+ const { annotationSHA256: _, ...body } = existing, annotation = seal({ ...body, revision: existing.revision + 1, events: [...existing.events, event] })
478
+ publish(input.files, input.captureId, state, annotation, guard.assertOwned, input.afterWrite)
479
+ return { correctionId: input.actionId, revision: annotation.revision, undone: true }
480
+ } finally { guard.release() }
481
+ }
482
+ export function recoverMacIntervalHumanAnnotation(files: MacIntervalHumanFiles, captureId: string, afterWrite?: (index: number) => void): void {
483
+ const paths = macIntervalHumanPaths(files, captureId), journal = readMacCallHumanFile(paths.journal, 40 * 1024 * 1024, true)
484
+ if (!journal) {
485
+ const lock = readMacCallHumanFile(paths.lock, 4096, true)
486
+ if (!lock) return
487
+ let owner: any
488
+ try { owner = JSON.parse(lock.text) } catch { fail('foreign lock') }
489
+ if (owner?.kind !== POLICY || owner.journal !== paths.journal) fail('foreign lock')
490
+ // A crash before the first durable intent moved no annotation. Reclaim
491
+ // only this operation's unchanged lock after a signal-0 ESRCH; a live or
492
+ // unknown owner is never inferred dead, signalled, or automatically resumed.
493
+ const orphanGuard = acquire(paths.lock, paths.journal, true, lock.hash)
494
+ try { if (readMacCallHumanFile(paths.journal, 40 * 1024 * 1024, true)) fail('journal appeared during orphan recovery') } finally { orphanGuard.release() }
495
+ return
496
+ }
497
+ const guard = acquire(paths.lock, paths.journal, true)
498
+ try { runJournal(files, captureId, JSON.parse(journal.text), journal.text, guard.assertOwned, afterWrite) } finally { guard.release() }
499
+ }