@gotcos/glasses-server 6.95.0 → 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 (123) hide show
  1. package/CHANGELOG.md +157 -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 +453 -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 +570 -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,414 @@
1
+ /** Capture-long192d identity evidence. Pure transitions; never enrollment or text edits. */
2
+ import { checkSpeakerName } from './speaker-name.js'
3
+ import { isVoiceLabel } from './mac-voice-labels.js'
4
+ import { createHash } from 'node:crypto'
5
+ import { DEFAULT_MAC_SPEAKER_THRESHOLDS, createMacSpeakerMemory, validateMacSpeakerMemory, reconcileMacSpeakerObservation, matchMacSpeakerProfile, normalizedSpeakerEmbedding, normalizedSpeakerCosine,
6
+ type MacSpeakerMemory, type MacSpeakerObservation, type MacSpeakerProfile } from './mac-speaker-reconcile.js'
7
+ import type { PreparedMacSpeakerWindow, MacWindowEmbeddingReceipt } from './mac-speaker-windows.js'
8
+ import { inspectMacSpeakerShadowCallSource, type MacShadowRepairInput } from './mac-speaker-shadow-repair.js'
9
+ import { exactOverlap } from './speaker-shadow-decide.js'
10
+ import { pyRoundInt } from './speaker-shadow-numeric.js'
11
+
12
+ const SHA=/^[a-f0-9]{64}$/
13
+ export const MAC_SPEAKER_LIVE_MAX_WINDOWS=256
14
+ /** Control accepts counters below10m; the last value is withdrawal-only. */
15
+ export const MAC_SPEAKER_LIVE_TERMINAL_REVISION=9999999
16
+ export const hashMacSpeakerLive=(value:unknown):string=>createHash('sha256').update(JSON.stringify(value)).digest('hex')
17
+ export interface MacSpeakerLiveSpan {
18
+ rawChunkIndex:number;audioSHA256:string;pcmSHA256:string
19
+ /** Capture-absolute, half-open samples at16kHz; exact whole original, never a crop. */
20
+ startSample:number;endSample:number;identityId:string;displayLabel:string;profileName?:string;evidenceIds:string[]
21
+ }
22
+ export interface MacSpeakerLiveEvidence {
23
+ window:PreparedMacSpeakerWindow;receipt:MacWindowEmbeddingReceipt
24
+ /** Hash of the complete model/source/vector receipt, computed after owned child close. */
25
+ nativeReceiptSHA256:string
26
+ cohortId?:number
27
+ }
28
+ export interface MacSpeakerLiveState {
29
+ version:1;captureId:string;generation:string;modelSHA256:string;profileSHA256:string;revision:number
30
+ memory:MacSpeakerMemory;evidence:MacSpeakerLiveEvidence[]
31
+ identities:Array<{cohortId:number;identityId:string;displayLabel:string;retired:boolean}>
32
+ voiceHighWater:number;projections:MacSpeakerLiveSpan[];withheldReason:string|null;stateSHA256:string
33
+ }
34
+ const seal=(state:MacSpeakerLiveState):MacSpeakerLiveState=>{
35
+ if(state.revision>=MAC_SPEAKER_LIVE_TERMINAL_REVISION){state.revision=MAC_SPEAKER_LIVE_TERMINAL_REVISION;state.projections=[];state.voiceHighWater=99;state.withheldReason='revision_counter_exhausted';for(const identity of state.identities)identity.retired=true}
36
+ const{stateSHA256:_hash,...body}=state;state.stateSHA256=hashMacSpeakerLive(body);return state}
37
+ export function createMacSpeakerLiveState(captureId:string,generation:string,modelSHA256:string,profileSHA256:string,voiceHighWater=0):MacSpeakerLiveState {
38
+ if(!generation||generation.length>128||!SHA.test(profileSHA256)||!Number.isSafeInteger(voiceHighWater)||voiceHighWater<0||voiceHighWater>99)throw Error('live_binding')
39
+ return seal({version:1,captureId,generation,modelSHA256,profileSHA256,revision:0,memory:createMacSpeakerMemory(captureId,modelSHA256,{minimumSamples:320000}),evidence:[],identities:[],voiceHighWater,projections:[],withheldReason:null,stateSHA256:''})
40
+ }
41
+ function validEvidence(e:MacSpeakerLiveEvidence,state:MacSpeakerLiveState):void {
42
+ const w=e.window,r=e.receipt
43
+ if(!w||!r||!Number.isSafeInteger(w.startSample)||w.startSample<0||!Number.isSafeInteger(w.endSample)||!isVoiceLabel(w.originalVoiceHint)||!Array.isArray(w.sources)||!w.sources.length||w.sources.length>30||w.captureId!==state.captureId||r.modelSHA256!==state.modelSHA256||r.windowId!==w.id||r.windowBindingSHA256!==w.bindingSHA256
44
+ ||r.sampleCount!==w.sampleCount||w.endSample-w.startSample!==w.sampleCount||w.sampleCount<448000||w.sampleCount>480000||!SHA.test(r.audioSHA256)||!SHA.test(r.pcmSHA256)
45
+ ||!normalizedSpeakerEmbedding(r.embedding,192)||!Array.isArray(r.halfEmbeddings)||r.halfEmbeddings.length!==2||r.halfEmbeddings.some(v=>!normalizedSpeakerEmbedding(v,192)))throw Error('live_evidence_binding')
46
+ const{id:_,bindingSHA256,...body}=w
47
+ if(hashMacSpeakerLive(body)!==bindingSHA256||w.id!==`window_${bindingSHA256}`||e.nativeReceiptSHA256!==hashMacSpeakerLive({window:w,receipt:r}))throw Error('live_receipt_hash')
48
+ let at=w.startSample,speech=0;const indices=new Set<number>(),hashes=new Set<string>()
49
+ for(const s of w.sources){
50
+ if(!Number.isSafeInteger(s.chunkIndex)||s.chunkIndex<0||s.startSample!==at||s.endSample-s.startSample!==s.sampleCount||!Number.isSafeInteger(s.sampleCount)||s.sampleCount<=0
51
+ ||s.sampleRate!==16000||s.channels!==1||s.format!=='pcm_s16le'||s.verified!==true||!SHA.test(s.audioSHA256)||!SHA.test(s.pcmSHA256)||!SHA.test(s.textSHA256)||!SHA.test(s.segmentsSHA256)
52
+ ||indices.has(s.chunkIndex)||hashes.has(s.pcmSHA256)||!Number.isSafeInteger(s.speechSamples)||s.speechSamples<0||s.speechSamples>s.sampleCount)throw Error('live_source_geometry')
53
+ at=s.endSample;speech+=s.speechSamples;indices.add(s.chunkIndex);hashes.add(s.pcmSHA256)
54
+ }
55
+ if(at!==w.endSample||speech!==w.speechSamples||speech<320000)throw Error('live_source_speech')
56
+ }
57
+ export function validateMacSpeakerLiveState(state:MacSpeakerLiveState):void {
58
+ if(!state||state.version!==1||typeof state.generation!=='string'||!state.generation||state.generation.length>128||!SHA.test(state.modelSHA256)||!SHA.test(state.profileSHA256)||!Number.isSafeInteger(state.revision)||state.revision<0||state.revision>MAC_SPEAKER_LIVE_TERMINAL_REVISION
59
+ ||!Array.isArray(state.evidence)||state.evidence.length>MAC_SPEAKER_LIVE_MAX_WINDOWS||!Array.isArray(state.identities)||state.identities.length>99||!Array.isArray(state.projections)||state.projections.length>8192
60
+ ||!Number.isSafeInteger(state.voiceHighWater)||state.voiceHighWater<0||state.voiceHighWater>99||Buffer.byteLength(JSON.stringify(state))>8*1024*1024)throw Error('live_state_bound')
61
+ if(state.revision===MAC_SPEAKER_LIVE_TERMINAL_REVISION&&(state.projections.length||state.voiceHighWater!==99||!state.withheldReason||state.identities.some(i=>!i.retired)))throw Error('live_terminal_revision')
62
+ const{stateSHA256,...body}=state;if(hashMacSpeakerLive(body)!==stateSHA256)throw Error('live_state_hash')
63
+ validateMacSpeakerMemory(state.memory)
64
+ if(hashMacSpeakerLive(state.memory.thresholds)!==hashMacSpeakerLive({...DEFAULT_MAC_SPEAKER_THRESHOLDS,minimumSamples:320000}))throw Error('live_threshold_drift')
65
+ if(state.memory.captureId!==state.captureId||state.memory.modelSHA256!==state.modelSHA256||state.memory.thresholds.dimensions!==192||state.memory.thresholds.minimumSamples!==320000)throw Error('live_memory_binding')
66
+ const seen=new Set<string>(),raw=new Set<number>(),pcm=new Set<string>()
67
+ for(const e of state.evidence){validEvidence(e,state);if(e.cohortId!==undefined&&(!Number.isSafeInteger(e.cohortId)||!state.memory.identities.some(i=>i.id===e.cohortId)))throw Error('live_cohort_reference');if(seen.has(e.window.id))throw Error('live_duplicate_window');seen.add(e.window.id)
68
+ for(const source of e.window.sources){if(raw.has(source.chunkIndex)||pcm.has(source.pcmSHA256))throw Error('live_duplicate_source');raw.add(source.chunkIndex);pcm.add(source.pcmSHA256)}}
69
+ const ids=new Set<string>(),labels=new Set<string>(),cohorts=new Set<number>()
70
+ for(const i of state.identities){if(ids.has(i.identityId)||labels.has(i.displayLabel)||cohorts.has(i.cohortId)||!/^acoustic_[a-f0-9]{64}$/.test(i.identityId)||!/^Voice [1-9]\d?$/.test(i.displayLabel)
71
+ ||Number(i.displayLabel.slice(6))>state.voiceHighWater||!state.memory.identities.some(c=>c.id===i.cohortId)||typeof i.retired!=='boolean')throw Error('live_namespace');ids.add(i.identityId);labels.add(i.displayLabel);cohorts.add(i.cohortId)}
72
+ const receipts=new Map(state.evidence.map(e=>[e.nativeReceiptSHA256,e])),projected=new Set<number>()
73
+ for(const p of state.projections){
74
+ const identity=state.identities.find(i=>i.identityId===p.identityId)
75
+ if(!identity||identity.retired||projected.has(p.rawChunkIndex)||!Array.isArray(p.evidenceIds)||p.evidenceIds.length<2||p.evidenceIds.length>3||new Set(p.evidenceIds).size!==p.evidenceIds.length)throw Error('live_projection_identity')
76
+ const support=p.evidenceIds.map(id=>receipts.get(id))
77
+ if(support.some(e=>!e||e.cohortId!==identity.cohortId||!coherent(e)))throw Error('live_projection_support')
78
+ const source=support.flatMap(e=>e!.window.sources).find(s=>s.chunkIndex===p.rawChunkIndex)
79
+ if(!source||source.audioSHA256!==p.audioSHA256||source.pcmSHA256!==p.pcmSHA256||source.startSample!==p.startSample||source.endSample!==p.endSample)throw Error('live_projection_source')
80
+ if(p.profileName!==undefined?typeof p.profileName!=='string'||!checkSpeakerName(p.profileName).ok||/^(?:ext|unknown|mu|speaker[\s_-]*\d+|voice[\s_-]*\d+)$/iu.test(p.profileName)||p.displayLabel!==p.profileName:p.displayLabel!==identity.displayLabel)throw Error('live_projection_label')
81
+ projected.add(p.rawChunkIndex)
82
+ }
83
+ if(state.withheldReason!==null&&state.projections.length)throw Error('live_withheld_projection')
84
+ // Every retained native witness assigned to memory must have the same exact
85
+ // aggregate original as its corresponding evidence receipt.
86
+ const expectedOriginals=state.evidence.filter(e=>e.cohortId!==undefined).map(e=>observation(state,e).original)
87
+ if(hashMacSpeakerLive(expectedOriginals)!==hashMacSpeakerLive(state.memory.originals))throw Error('live_memory_lineage')
88
+ // Rebuild deterministic receipt membership, rather than trusting a caller-
89
+ // recomputed unkeyed state hash or arbitrary cohort references on recovery.
90
+ let rebuilt=createMacSpeakerMemory(state.captureId,state.modelSHA256,{minimumSamples:320000})
91
+ for(const e of state.evidence){
92
+ let expected:number|undefined
93
+ if(coherent(e)){const next=reconcileMacSpeakerObservation(rebuilt,observation(state,e));rebuilt=next.state;expected=next.decision.identityId}
94
+ if(expected!==e.cohortId)throw Error('live_receipt_membership')
95
+ }
96
+ if(hashMacSpeakerLive(rebuilt)!==hashMacSpeakerLive(state.memory))throw Error('live_memory_replay')
97
+ for(const identity of state.identities){const group=state.evidence.filter(e=>e.cohortId===identity.cohortId)
98
+ if(group.length<2||identity.identityId!==`acoustic_${hashMacSpeakerLive(group.slice(0,2).map(e=>e.nativeReceiptSHA256))}`)throw Error('live_identity_lineage')
99
+ }
100
+ }
101
+ function observation(state:MacSpeakerLiveState,e:MacSpeakerLiveEvidence):MacSpeakerObservation {
102
+ const w=e.window,r=e.receipt
103
+ return{captureId:state.captureId,modelSHA256:state.modelSHA256,original:{chunkIndex:w.sources[0].chunkIndex,audioSHA256:r.audioSHA256,pcmSHA256:r.pcmSHA256,startSample:w.startSample,sampleCount:w.sampleCount},
104
+ startSample:w.startSample,endSample:w.endSample,exclusiveSamples:w.speechSamples,purity:1,overlapSamples:0,mixedSpeakers:false,embedding:r.embedding}
105
+ }
106
+ /** Labels/segments establish scheduling hints only. Halves are dependent checks, never independent votes. */
107
+ function coherent(e:MacSpeakerLiveEvidence):boolean {const h=e.receipt.halfEmbeddings!;return normalizedSpeakerCosine(h[0],h[1])!>=.90&&h.every(v=>normalizedSpeakerCosine(v,e.receipt.embedding)!>=.90)}
108
+ function refresh(state:MacSpeakerLiveState,profiles:readonly MacSpeakerProfile[]):void {
109
+ state.projections=[]
110
+ if(state.withheldReason)return
111
+ for(const cohort of state.memory.identities){
112
+ if(cohort.voice===null)continue
113
+ const group=state.evidence.filter(e=>e.cohortId===cohort.id)
114
+ if(group.length<2||group.some(a=>group.some(b=>normalizedSpeakerCosine(a.receipt.embedding,b.receipt.embedding)!<.90)))continue
115
+ const minimum=Math.min(...group.flatMap(a=>group.filter(b=>a!==b).map(b=>normalizedSpeakerCosine(a.receipt.embedding,b.receipt.embedding)!)))
116
+ const outside=state.evidence.filter(e=>e.cohortId!==cohort.id)
117
+ // Failed-half evidence can be compatible non-support only: BOTH dependent
118
+ // halves must clear EVERY established full anchor and every other coherent
119
+ // outsider (including unassigned singletons). It never supports/projects.
120
+ const compatibleNonSupport=(o:MacSpeakerLiveEvidence):boolean=>{
121
+ if(coherent(o))return false
122
+ const competitors=outside.filter(other=>other!==o&&coherent(other))
123
+ return o.receipt.halfEmbeddings!.every(half=>{
124
+ const same=Math.min(...group.map(g=>normalizedSpeakerCosine(half,g.receipt.embedding)!))
125
+ const different=Math.max(-1,...competitors.map(other=>normalizedSpeakerCosine(half,other.receipt.embedding)!))
126
+ return same>=.90&&same-different>=.06
127
+ })
128
+ }
129
+ // Every full observation remains a veto unless that narrow, non-support
130
+ // compatibility check passes. Later coherent confusers revoke the exception.
131
+ if(outside.some(o=>!compatibleNonSupport(o)&&group.some(g=>minimum-normalizedSpeakerCosine(g.receipt.embedding,o.receipt.embedding)!<.06)))continue
132
+ let identity=state.identities.find(i=>i.cohortId===cohort.id)
133
+ if(identity?.retired)continue
134
+ if(!identity){
135
+ if(state.voiceHighWater>=99){state.withheldReason='voice_namespace_exhausted';state.projections=[];return}
136
+ const identityId=`acoustic_${hashMacSpeakerLive(group.slice(0,2).map(e=>e.nativeReceiptSHA256))}`
137
+ identity={cohortId:cohort.id,identityId,displayLabel:`Voice ${++state.voiceHighWater}`,retired:false};state.identities.push(identity)
138
+ }
139
+ const named=matchMacSpeakerProfile(state.memory,group.map(e=>observation(state,e)),profiles)
140
+ // MU remains an enrolled full-cohort competitor, never a human display claim.
141
+ const profileName=named.reason==='matched_profile'&&named.label?.normalize('NFKC').trim().toLowerCase()!=='mu'?named.label??undefined:undefined
142
+ for(const e of group)for(const s of e.window.sources)state.projections.push({rawChunkIndex:s.chunkIndex,audioSHA256:s.audioSHA256,pcmSHA256:s.pcmSHA256,startSample:s.startSample,endSample:s.endSample,
143
+ identityId:identity.identityId,displayLabel:profileName??identity.displayLabel,...(profileName?{profileName}:{}),evidenceIds:[...new Set([...group.slice(0,2).map(x=>x.nativeReceiptSHA256),e.nativeReceiptSHA256])]})
144
+ }
145
+ state.projections.sort((a,b)=>a.rawChunkIndex-b.rawChunkIndex)
146
+ }
147
+ /** Immutable append; exact source overlaps/crops cannot supply a second witness. */
148
+ export function appendMacSpeakerLiveEvidence(input:MacSpeakerLiveState,evidence:MacSpeakerLiveEvidence,profiles:readonly MacSpeakerProfile[],reservedHighWater=input.voiceHighWater):MacSpeakerLiveState {
149
+ validateMacSpeakerLiveState(input);if(input.revision===MAC_SPEAKER_LIVE_TERMINAL_REVISION)return structuredClone(input)
150
+ validEvidence(evidence,input)
151
+ validateMacSpeakerLiveProfiles(profiles,input.profileSHA256)
152
+ const state=structuredClone(input)
153
+ if(!Number.isSafeInteger(reservedHighWater)||reservedHighWater<state.voiceHighWater||reservedHighWater>99)throw Error('live_namespace_drift')
154
+ state.voiceHighWater=reservedHighWater
155
+ if(state.evidence.some(e=>e.window.id===evidence.window.id))return seal(state)
156
+ if(state.evidence.length>=MAC_SPEAKER_LIVE_MAX_WINDOWS){state.withheldReason='evidence_budget_exhausted';state.projections=[];state.revision++;return seal(state)}
157
+ const sources=state.evidence.flatMap(e=>e.window.sources)
158
+ if(evidence.window.sources.some(s=>sources.some(p=>s.chunkIndex===p.chunkIndex||s.audioSHA256===p.audioSHA256||s.pcmSHA256===p.pcmSHA256||Math.max(s.startSample,p.startSample)<Math.min(s.endSample,p.endSample))))throw Error('live_repeated_or_crossed_source')
159
+ const e=structuredClone(evidence);delete e.cohortId
160
+ if(coherent(e)){
161
+ const next=reconcileMacSpeakerObservation(state.memory,observation(state,e));state.memory=next.state
162
+ if(next.decision.identityId!==undefined)e.cohortId=next.decision.identityId
163
+ }
164
+ state.evidence.push(e);refresh(state,profiles);state.revision++;return seal(state)
165
+ }
166
+ export function withholdMacSpeakerLiveState(input:MacSpeakerLiveState,reason:string):MacSpeakerLiveState {
167
+ validateMacSpeakerLiveState(input);const state=structuredClone(input)
168
+ if(!reason||reason.length>200)throw Error('live_withheld_reason')
169
+ for(const i of state.identities)i.retired=true
170
+ state.projections=[];state.withheldReason=reason;state.revision++;return seal(state)
171
+ }
172
+
173
+ /** Offline/replay seam: caller must independently prove the canonical full source
174
+ * universe. Completeness here is only relative to supplied source-bound receipts. */
175
+ export function replayMacSpeakerLiveEvidence(input:{captureId:string;generation:string;modelSHA256:string;profileSHA256:string;voiceHighWater?:number;evidence:readonly MacSpeakerLiveEvidence[];profiles:readonly MacSpeakerProfile[]}):MacSpeakerLiveState {
176
+ let state=createMacSpeakerLiveState(input.captureId,input.generation,input.modelSHA256,input.profileSHA256,input.voiceHighWater)
177
+ if(input.evidence.length>MAC_SPEAKER_LIVE_MAX_WINDOWS)return withholdMacSpeakerLiveState(state,'evidence_budget_exhausted')
178
+ for(const e of input.evidence)state=appendMacSpeakerLiveEvidence(state,e,input.profiles)
179
+ return state
180
+ }
181
+
182
+ export function validateMacSpeakerLiveProfiles(profiles:readonly MacSpeakerProfile[],profileSHA256:string):void {
183
+ if(!Array.isArray(profiles)||profiles.length>1024||profiles.some(p=>!p||p.profileSHA256!==profileSHA256))throw Error('live_profile_drift')
184
+ const aliases=new Set<string>();let count=0
185
+ for(const p of profiles){const alias=p.name?.normalize('NFKC').trim().replace(/\s+/gu,' ').toLowerCase()
186
+ if(!checkSpeakerName(p.name).ok||/^(?:ext|unknown|speaker[\s_-]*(?:\d+|n)|voice[\s_-]*(?:\d+|n))$/iu.test(p.name)||aliases.has(alias)||!Array.isArray(p.embeddings)||!p.embeddings.length||p.embeddings.length>128||p.embeddings.some((v:readonly number[])=>!normalizedSpeakerEmbedding(v,192)))throw Error('live_profile_cohort_invalid')
187
+ aliases.add(alias);count+=p.embeddings.length
188
+ }
189
+ if(count>8192)throw Error('live_profile_cohort_invalid')
190
+ }
191
+ /** Advance only the public snapshot cursor (e.g. source/manual revocation). */
192
+ export function advanceMacSpeakerLiveRevision(input:MacSpeakerLiveState):MacSpeakerLiveState {
193
+ validateMacSpeakerLiveState(input);const state=structuredClone(input);state.revision++;return seal(state)
194
+ }
195
+ export function refreshMacSpeakerLiveState(input:MacSpeakerLiveState,profiles:readonly MacSpeakerProfile[]):MacSpeakerLiveState {
196
+ validateMacSpeakerLiveState(input);validateMacSpeakerLiveProfiles(profiles,input.profileSHA256);const state=structuredClone(input);refresh(state,profiles);return seal(state)
197
+ }
198
+
199
+ /** Separate model space and candidate protocol. This does NOT enable the192d
200
+ * runtime or authenticate native execution. Consumers must verify the complete
201
+ * source/native packet and run this full-cohort work OFF the HTTP/main thread. */
202
+ export interface MacSpeaker256SnapshotInput extends MacShadowRepairInput {
203
+ generation:string
204
+ /** Optimistic concurrency fence; mandatory when transitioning an existing state. */
205
+ expectedStateSHA256?:string
206
+ }
207
+ export interface MacSpeaker256Witness {
208
+ evidenceId:string;startSample:number;endSample:number;originalVoice:string
209
+ rawChunkIndices:number[];pcmSHA256s:string[]
210
+ }
211
+ export interface MacSpeaker256Interval {
212
+ rawChunkIndex:number;audioSHA256:string;pcmSHA256:string;startSample:number;endSample:number
213
+ originalVoice:string;identityId:string;evidenceIds:string[]
214
+ /** Machine localization agreement, never independently verified speaker purity. */
215
+ timingBasis:'native-exclusive-intersect-machine-production'
216
+ }
217
+ export interface MacSpeaker256Group {
218
+ identityId:string;nativeCluster:string;memberVoices:string[];keepVoiceHint:string;groupSHA256:string
219
+ witnesses:MacSpeaker256Witness[];intervals:MacSpeaker256Interval[]
220
+ }
221
+ interface MacSpeaker256RawBinding {
222
+ rawChunkIndex:number;audioSHA256:string;pcmSHA256:string;startSample:number;endSample:number;originalRowSHA256:string
223
+ }
224
+ export interface MacSpeaker256State {
225
+ version:1;policy:'fluid256-recurrence-v1';captureId:string;generation:string;modelSHA256:string;cliSHA256:string
226
+ sourceBindingSHA256:string;snapshotSHA256:string;revision:number;identityHighWater:number
227
+ identities:Array<{identityId:string;ordinal:number;retired:boolean;birthSnapshotSHA256:string;birthGroupSHA256:string;birthIntervals:MacSpeaker256Interval[];lastIntervals:MacSpeaker256Interval[]}>
228
+ groups:MacSpeaker256Group[];withdrawn:Array<{identityId:string;reason:string}>
229
+ withheld:Array<{rawChunkIndex:number;startSample:number;endSample:number;reason:string}>
230
+ rawBindings:MacSpeaker256RawBinding[]
231
+ coverage:{rawOriginals:number;textOriginals:number;protectedOriginals:number;acceptedRecurrenceGroups:number;independentQualifiedGroups:number;machineSpeechSamples:number;supportedSamples:number}
232
+ /** Hash integrity only; native qualification belongs to the trusted producer. */
233
+ provenance:{nativeAuthenticated:false;productionQualified:false;originalSHA256:string;currentSHA256:string;ledgerSHA256:string;nativeSHA256:string;embeddingsSHA256:string}
234
+ excludedEmbeddingWindows:Array<{originalIndex:number;startTime:number;endTime:number;windowSHA256:string;reason:'window-crosses-capture-end'}>
235
+ stateSHA256:string
236
+ }
237
+ const FLUID256_POLICY='fluid256-recurrence-v1' as const
238
+ const fluidHash=(value:string)=>createHash('sha256').update(value).digest('hex')
239
+ const fluidSeal=(s:MacSpeaker256State)=>{const{stateSHA256:_,...body}=s;s.stateSHA256=hashMacSpeakerLive(body);return s}
240
+ const fluidFail=(reason:string):never=>{throw Error(`fluid256_${reason}`)}
241
+ const fluidGroupBody=(g:MacSpeaker256Group)=>({nativeCluster:g.nativeCluster,memberVoices:g.memberVoices,keepVoiceHint:g.keepVoiceHint,witnesses:g.witnesses,
242
+ intervals:g.intervals.map(({identityId:_,...i})=>i)})
243
+ const fluidOverlap=(a:readonly MacSpeaker256Interval[],b:readonly MacSpeaker256Interval[])=>a.some(x=>b.some(y=>x.rawChunkIndex===y.rawChunkIndex&&x.audioSHA256===y.audioSHA256&&x.pcmSHA256===y.pcmSHA256&&Math.max(x.startSample,y.startSample)<Math.min(x.endSample,y.endSample)))
244
+ function fluidShape(state:MacSpeaker256State):void {
245
+ if(!state||state.version!==1||state.policy!==FLUID256_POLICY||typeof state.captureId!=='string'||!/^mac_/.test(state.captureId)||state.captureId.length>200||typeof state.generation!=='string'||!state.generation||state.generation.length>128
246
+ ||![state.modelSHA256,state.cliSHA256,state.sourceBindingSHA256,state.snapshotSHA256,state.stateSHA256].every(x=>SHA.test(x))
247
+ ||!Number.isSafeInteger(state.revision)||state.revision<1||state.revision>=MAC_SPEAKER_LIVE_TERMINAL_REVISION||!Number.isSafeInteger(state.identityHighWater)||state.identityHighWater<0||state.identityHighWater>1024
248
+ ||!Array.isArray(state.identities)||state.identities.length!==state.identityHighWater||!Array.isArray(state.groups)||state.groups.length>99||!Array.isArray(state.rawBindings)||state.rawBindings.length>6000
249
+ ||!Array.isArray(state.withheld)||state.withheld.length>100000||!Array.isArray(state.withdrawn)||state.withdrawn.length>1024||Buffer.byteLength(JSON.stringify(state))>16*1024*1024)fluidFail('state_bound')
250
+ const{stateSHA256,...body}=state;if(hashMacSpeakerLive(body)!==stateSHA256)fluidFail('state_hash')
251
+ const raw=new Map(state.rawBindings.map(r=>[r.rawChunkIndex,r]))
252
+ if(raw.size!==state.rawBindings.length)fluidFail('raw_binding')
253
+ for(const[r,i]of state.rawBindings.map((r,i)=>[r,i] as const))if(r.rawChunkIndex!==i||![r.audioSHA256,r.pcmSHA256,r.originalRowSHA256].every(x=>SHA.test(x))||!Number.isSafeInteger(r.startSample)||!Number.isSafeInteger(r.endSample)||r.startSample<0||r.endSample<=r.startSample)fluidFail('raw_binding')
254
+ const validSpan=(span:MacSpeaker256Interval,identityId:string)=>{
255
+ const r=raw.get(span.rawChunkIndex)
256
+ return r&&span.identityId===identityId&&span.audioSHA256===r.audioSHA256&&span.pcmSHA256===r.pcmSHA256&&Number.isSafeInteger(span.startSample)&&Number.isSafeInteger(span.endSample)&&span.startSample>=r.startSample&&span.endSample<=r.endSample&&span.endSample>span.startSample
257
+ &&span.timingBasis==='native-exclusive-intersect-machine-production'&&isVoiceLabel(span.originalVoice)&&Array.isArray(span.evidenceIds)&&span.evidenceIds.length===2&&span.evidenceIds.every(h=>SHA.test(h))&&span.evidenceIds[0]!==span.evidenceIds[1]
258
+ }
259
+ const ids=new Set<string>()
260
+ for(const[i,id]of state.identities.entries()){
261
+ if(id.ordinal!==i+1||!/^fluid256_[a-f0-9]{64}$/.test(id.identityId)||ids.has(id.identityId)||typeof id.retired!=='boolean'||!Array.isArray(id.lastIntervals)||!id.lastIntervals.length||id.lastIntervals.length>40000||!Array.isArray(id.birthIntervals)||!id.birthIntervals.length||id.birthIntervals.length>40000
262
+ ||!SHA.test(id.birthSnapshotSHA256)||!SHA.test(id.birthGroupSHA256)||id.identityId!==`fluid256_${hashMacSpeakerLive({captureId:state.captureId,generation:state.generation,snapshotSHA256:id.birthSnapshotSHA256,groupSHA256:id.birthGroupSHA256,ordinal:id.ordinal})}`)fluidFail('namespace')
263
+ if([...id.birthIntervals,...id.lastIntervals].some(span=>!validSpan(span,id.identityId)))fluidFail('identity_interval_binding')
264
+ ids.add(id.identityId)
265
+ }
266
+ const groups=new Set<string>()
267
+ for(const group of state.groups){
268
+ const identity=state.identities.find(i=>i.identityId===group.identityId)
269
+ if(!identity||identity.retired||groups.has(group.identityId)||group.groupSHA256!==hashMacSpeakerLive(fluidGroupBody(group))||group.witnesses.length!==2||!group.intervals.length
270
+ ||hashMacSpeakerLive(group.intervals)!==hashMacSpeakerLive(identity.lastIntervals)||!fluidOverlap(group.intervals,identity.birthIntervals))fluidFail('group_binding')
271
+ const[a,b]=group.witnesses
272
+ if(a.evidenceId===b.evidenceId||a.originalVoice===b.originalVoice||Math.max(a.startSample,b.startSample)<Math.min(a.endSample,b.endSample)
273
+ ||a.rawChunkIndices.some(r=>b.rawChunkIndices.includes(r))||a.pcmSHA256s.some(h=>b.pcmSHA256s.includes(h)))fluidFail('dependent_witnesses')
274
+ for(const span of group.intervals)if(!validSpan(span,group.identityId)||hashMacSpeakerLive(span.evidenceIds)!==hashMacSpeakerLive(group.witnesses.map(w=>w.evidenceId)))fluidFail('interval_binding')
275
+ groups.add(group.identityId)
276
+ }
277
+ if(state.identities.some(id=>id.retired===groups.has(id.identityId)))fluidFail('active_namespace')
278
+ const withdrawals=new Set<string>()
279
+ for(const w of state.withdrawn){if(withdrawals.has(w.identityId)||!state.identities.some(id=>id.identityId===w.identityId&&id.retired)||w.reason!=='recurrence-withdrawn-or-split-merge')fluidFail('withdrawal_binding');withdrawals.add(w.identityId)}
280
+ }
281
+ type FluidCandidate=Omit<MacSpeaker256Group,'identityId'>
282
+ function fluidCandidates(input:MacSpeaker256SnapshotInput){
283
+ if(typeof input.generation!=='string'||!input.generation||input.generation.length>128)fluidFail('generation')
284
+ const{generation:_,expectedStateSHA256:__,...source}=input
285
+ // Full-source inspection binds exports, protects human/prior-proof labels and
286
+ // rejects source/schema/clock drift before the unchanged full-cohort decision.
287
+ const inspected=inspectMacSpeakerShadowCallSource(source)
288
+ const{original,current,entries,production,embeddings,decision:D}=inspected
289
+ const protectedRaw=new Set(inspected.protection),raw=new Map(input.rawAudio.map(r=>[r.rawChunkIndex,r])),placed=new Map(input.timeline.rows.map(r=>[r.rawChunkIndex,r]))
290
+ const currentRaw=new Map<number,Record<string,any>>(current.chunkEntries.map((e:any)=>[e.chunkIndex,e.chunk]))
291
+ const rows=(c:any)=>[c,...(c.segments??[]),...(c.finalSegments??[]),...(c.words??[]).filter((r:any)=>'speaker'in r)]
292
+ const sourceRows=entries.map(e=>{const r=raw.get(e.chunkIndex)!,p=placed.get(e.chunkIndex)!;return{rawChunkIndex:e.chunkIndex,audioSHA256:r.wavSHA256,pcmSHA256:r.pcmSHA256,startSample:p.startSample,endSample:p.startSample+p.sampleCount,originalRowSHA256:hashMacSpeakerLive(e.chunk)}})
293
+ const segmentsByRaw=new Map<number,Array<[number,number,string]>>()
294
+ for(let i=0;i<production.segs.length;i++){const id=production.sources[i].chunkIndex,list=segmentsByRaw.get(id)??[];list.push([...production.segs[i]]);segmentsByRaw.set(id,list)}
295
+ // Overlapping machine segments are ambiguous localization, not extra speech
296
+ // seconds or independent evidence. Refuse rather than double-count them.
297
+ for(const list of segmentsByRaw.values()){
298
+ list.sort((a,b)=>a[0]-b[0]||a[1]-b[1])
299
+ for(let i=1;i<list.length;i++)if(list[i][0]<list[i-1][1])fluidFail('production_overlap')
300
+ }
301
+ const clean=(index:number,voice:string)=>{const e=entries[index],p=placed.get(index)!,now=currentRaw.get(index)!;return !protectedRaw.has(index)&&p.usedSampleCount===p.sampleCount
302
+ &&rows(e.chunk).every(r=>r.speaker===voice)&&rows(now).every(r=>r.speaker===voice)&&hashMacSpeakerLive(rows(e.chunk))===hashMacSpeakerLive(rows(now))}
303
+ const candidates:FluidCandidate[]=[],withheld:MacSpeaker256State['withheld']=[];let accepted=0,machineSpeechSamples=0
304
+ const acceptedVoices=new Map<string,{cluster:string;hint:string;members:string[]}>()
305
+ for(const g of D.groups){if(!g.anchor||!g.mergedIntoAnchor.length)continue;accepted++;const members=[g.anchor,...g.mergedIntoAnchor].sort();for(const voice of members)acceptedVoices.set(voice,{cluster:g.cluster,hint:g.anchor,members})}
306
+ const uniqueGroups=[...new Map([...acceptedVoices.values()].map(g=>[g.cluster,g])).values()]
307
+ for(const group of uniqueGroups){
308
+ const witnesses:MacSpeaker256Witness[]=[]
309
+ for(const e of embeddings){
310
+ if(`S${e.cluster+1}`!==group.cluster)continue
311
+ const lo=pyRoundInt(e.startTime*16000),hi=pyRoundInt(e.endTime*16000)
312
+ if(hi<=lo||[...exactOverlap(lo,hi,D.index)].some(([key])=>key!==group.cluster))continue
313
+ const touched=sourceRows.filter(r=>r.startSample<hi&&r.endSample>lo)
314
+ if(!touched.length)continue
315
+ const speech=production.segs.flatMap((s,i)=>s[0]<hi&&s[1]>lo?[{segment:s,raw:production.sources[i].chunkIndex}]:[])
316
+ const voices=new Set(speech.map(s=>s.segment[2]));if(voices.size!==1)continue
317
+ const voice=[...voices][0];if(!group.members.includes(voice)||speech.some(s=>!clean(s.raw,voice))||touched.some(r=>!clean(r.rawChunkIndex,voice)))continue
318
+ // The unchanged port attributes embeddings at50percent production coverage.
319
+ // Repeated/overlapping exports are still only ONE source witness below.
320
+ const speechSamples=speech.reduce((n,s)=>n+Math.max(0,Math.min(hi,s.segment[1])-Math.max(lo,s.segment[0])),0)
321
+ if(speechSamples<.5*(hi-lo))continue
322
+ witnesses.push({evidenceId:hashMacSpeakerLive({modelSHA256:input.cohort.modelSHA256,cliSHA256:input.cohort.cliSHA256,embedding:e,raw:touched}),startSample:lo,endSample:hi,originalVoice:voice,rawChunkIndices:touched.map(r=>r.rawChunkIndex),pcmSHA256s:touched.map(r=>r.pcmSHA256)})
323
+ }
324
+ witnesses.sort((a,b)=>a.startSample-b.startSample||a.endSample-b.endSample||a.evidenceId.localeCompare(b.evidenceId))
325
+ let pair:MacSpeaker256Witness[]|undefined
326
+ for(let i=0;i<witnesses.length&&!pair;i++)for(let j=i+1;j<witnesses.length;j++){
327
+ const a=witnesses[i],b=witnesses[j]
328
+ if(a.originalVoice!==b.originalVoice&&a.endSample<=b.startSample&&!a.rawChunkIndices.some(r=>b.rawChunkIndices.includes(r))&&!a.pcmSHA256s.some(h=>b.pcmSHA256s.includes(h))){pair=[a,b];break}
329
+ }
330
+ if(!pair)continue
331
+ const intervals:MacSpeaker256Interval[]=[]
332
+ for(const e of entries){
333
+ const voice=e.chunk.speaker as string;if(!group.members.includes(voice)||!clean(e.chunkIndex,voice))continue
334
+ const r=raw.get(e.chunkIndex)!
335
+ for(const[lo,hi,label]of segmentsByRaw.get(e.chunkIndex)??[]){
336
+ if(label!==voice)continue
337
+ const boundaries=new Set<number>([lo,hi]);for(const[a,b]of D.index.overlapping(lo,hi)){boundaries.add(Math.max(lo,a));boundaries.add(Math.min(hi,b))}
338
+ const points=[...boundaries].sort((a,b)=>a-b)
339
+ for(let i=0;i+1<points.length;i++){const startSample=points[i],endSample=points[i+1],cover=exactOverlap(startSample,endSample,D.index)
340
+ if(cover.size===1&&cover.has(group.cluster))intervals.push({rawChunkIndex:e.chunkIndex,audioSHA256:r.wavSHA256,pcmSHA256:r.pcmSHA256,startSample,endSample,originalVoice:voice,identityId:'',evidenceIds:pair.map(w=>w.evidenceId),timingBasis:'native-exclusive-intersect-machine-production'})
341
+ }
342
+ }
343
+ }
344
+ intervals.sort((a,b)=>a.rawChunkIndex-b.rawChunkIndex||a.startSample-b.startSample||a.endSample-b.endSample)
345
+ if(!intervals.length)continue
346
+ const body={nativeCluster:group.cluster,memberVoices:group.members,keepVoiceHint:group.hint,witnesses:pair,intervals}
347
+ const groupSHA256=hashMacSpeakerLive(fluidGroupBody({...body,identityId:'',groupSHA256:''}))
348
+ candidates.push({...body,groupSHA256})
349
+ }
350
+ for(const e of entries){
351
+ for(const[lo,hi]of segmentsByRaw.get(e.chunkIndex)??[]){
352
+ machineSpeechSamples+=hi-lo
353
+ const spans=candidates.flatMap(g=>g.intervals.filter(i=>i.rawChunkIndex===e.chunkIndex&&i.startSample<hi&&i.endSample>lo))
354
+ const points=[...new Set([lo,hi,...spans.flatMap(i=>[Math.max(lo,i.startSample),Math.min(hi,i.endSample)])])].sort((a,b)=>a-b)
355
+ for(let i=0;i+1<points.length;i++)if(!spans.some(s=>s.startSample<=points[i]&&s.endSample>=points[i+1]))withheld.push({rawChunkIndex:e.chunkIndex,startSample:points[i],endSample:points[i+1],reason:protectedRaw.has(e.chunkIndex)?'protected-source':'no-independent-exclusive-recurrence'})
356
+ }
357
+ }
358
+ return{captureId:original.sessionId as string,candidates,withheld,sourceRows,excludedEmbeddingWindows:inspected.excludedEmbeddingWindows,coverage:{rawOriginals:entries.length,textOriginals:original.chunks.length,protectedOriginals:protectedRaw.size,acceptedRecurrenceGroups:accepted,independentQualifiedGroups:candidates.length,machineSpeechSamples,supportedSamples:candidates.reduce((n,g)=>n+g.intervals.reduce((m,i)=>m+i.endSample-i.startSample,0),0)}}
359
+ }
360
+ /** Whole supplied-snapshot reconciliation, never a global old-label rename.
361
+ * The trusted caller must prove full native/source completeness independently. */
362
+ export function reconcileMacSpeaker256Snapshot(previous:MacSpeaker256State|null,input:MacSpeaker256SnapshotInput):MacSpeaker256State {
363
+ const evaluated=fluidCandidates(input),snapshotSHA256=hashMacSpeakerLive({sourceBindingSHA256:input.sourceBindingSHA256,generation:input.generation,policy:FLUID256_POLICY})
364
+ if(previous){
365
+ fluidShape(previous)
366
+ if(input.expectedStateSHA256!==previous.stateSHA256)fluidFail('state_cas')
367
+ if(previous.captureId!==evaluated.captureId||previous.generation!==input.generation||previous.modelSHA256!==input.cohort.modelSHA256||previous.cliSHA256!==input.cohort.cliSHA256)fluidFail('model_or_capture_drift')
368
+ if(previous.rawBindings.some((r,i)=>hashMacSpeakerLive(r)!==hashMacSpeakerLive(evaluated.sourceRows[i])))fluidFail('original_source_drift')
369
+ if(previous.snapshotSHA256===snapshotSHA256){validateMacSpeaker256State(previous,input);return structuredClone(previous)}
370
+ if(previous.revision>=MAC_SPEAKER_LIVE_TERMINAL_REVISION-1)fluidFail('revision_exhausted')
371
+ }else if(input.expectedStateSHA256!==undefined)fluidFail('unexpected_state_cas')
372
+ const identities=structuredClone(previous?.identities??[]),groups:MacSpeaker256Group[]=[],withdrawn:MacSpeaker256State['withdrawn']=[],withheld=[...evaluated.withheld]
373
+ let identityHighWater=previous?.identityHighWater??0
374
+ const old=previous?.groups??[]
375
+ const oldLinks=old.map(g=>evaluated.candidates.flatMap((n,i)=>fluidOverlap(g.intervals,n.intervals)?[i]:[]))
376
+ const newLinks=evaluated.candidates.map(g=>old.flatMap((o,i)=>fluidOverlap(g.intervals,o.intervals)?[i]:[]))
377
+ for(let i=0;i<evaluated.candidates.length;i++){
378
+ const candidate=evaluated.candidates[i],links=newLinks[i]
379
+ const ambiguous=links.length>1||links.some(j=>oldLinks[j].length!==1)||identities.some(id=>id.retired&&fluidOverlap(id.lastIntervals,candidate.intervals))
380
+ if(ambiguous){for(const span of candidate.intervals)withheld.push({rawChunkIndex:span.rawChunkIndex,startSample:span.startSample,endSample:span.endSample,reason:'split-merge-or-retired-lineage'});continue}
381
+ let identity=links.length?identities.find(id=>id.identityId===old[links[0]].identityId):undefined
382
+ if(!identity){
383
+ if(identityHighWater>=1024)fluidFail('identity_budget_exhausted')
384
+ const ordinal=++identityHighWater,identityId=`fluid256_${hashMacSpeakerLive({captureId:evaluated.captureId,generation:input.generation,snapshotSHA256,groupSHA256:candidate.groupSHA256,ordinal})}`
385
+ identity={identityId,ordinal,retired:false,birthSnapshotSHA256:snapshotSHA256,birthGroupSHA256:candidate.groupSHA256,birthIntervals:candidate.intervals.map(i=>({...i,identityId})),lastIntervals:[]};identities.push(identity)
386
+ }
387
+ const intervals=candidate.intervals.map(i=>({...i,identityId:identity!.identityId}));identity.lastIntervals=intervals
388
+ groups.push({...candidate,identityId:identity.identityId,intervals})
389
+ }
390
+ for(const identity of identities)if(!identity.retired&&!groups.some(g=>g.identityId===identity.identityId)){
391
+ identity.retired=true;withdrawn.push({identityId:identity.identityId,reason:'recurrence-withdrawn-or-split-merge'})
392
+ }
393
+ const supportedSamples=groups.reduce((n,g)=>n+g.intervals.reduce((m,i)=>m+i.endSample-i.startSample,0),0)
394
+ const state=fluidSeal({version:1,policy:FLUID256_POLICY,captureId:evaluated.captureId,generation:input.generation,modelSHA256:input.cohort.modelSHA256,cliSHA256:input.cohort.cliSHA256,sourceBindingSHA256:input.sourceBindingSHA256,snapshotSHA256,
395
+ revision:(previous?.revision??0)+1,identityHighWater,identities,groups,withdrawn,withheld,rawBindings:evaluated.sourceRows,coverage:{...evaluated.coverage,supportedSamples},
396
+ provenance:{nativeAuthenticated:false,productionQualified:false,originalSHA256:fluidHash(input.originalJSON),currentSHA256:fluidHash(input.currentJSON),ledgerSHA256:fluidHash(input.ledgerJSONL),nativeSHA256:fluidHash(input.cohort.nativeJSON),embeddingsSHA256:fluidHash(input.cohort.embeddingsJSON)},excludedEmbeddingWindows:evaluated.excludedEmbeddingWindows,stateSHA256:''})
397
+ fluidShape(state);return state
398
+ }
399
+ /** Recompute current complete-cohort membership/intervals; a resealed hash alone
400
+ * cannot invent acoustic support or move an interval on restart. */
401
+ export function validateMacSpeaker256State(state:MacSpeaker256State,input:MacSpeaker256SnapshotInput):void {
402
+ fluidShape(state);const expected=fluidCandidates(input)
403
+ if(state.captureId!==expected.captureId||state.generation!==input.generation||state.sourceBindingSHA256!==input.sourceBindingSHA256||state.modelSHA256!==input.cohort.modelSHA256||state.cliSHA256!==input.cohort.cliSHA256
404
+ ||state.snapshotSHA256!==hashMacSpeakerLive({sourceBindingSHA256:input.sourceBindingSHA256,generation:input.generation,policy:FLUID256_POLICY})||hashMacSpeakerLive(state.rawBindings)!==hashMacSpeakerLive(expected.sourceRows))fluidFail('snapshot_binding')
405
+ for(const g of state.groups){
406
+ const candidate=expected.candidates.find(c=>c.groupSHA256===g.groupSHA256)
407
+ if(!candidate||hashMacSpeakerLive(fluidGroupBody(g))!==hashMacSpeakerLive(fluidGroupBody({...candidate,identityId:''})))fluidFail('membership_or_interval_drift')
408
+ }
409
+ const supportedSamples=state.groups.reduce((n,g)=>n+g.intervals.reduce((m,i)=>m+i.endSample-i.startSample,0),0)
410
+ const withheld=[...expected.withheld,...expected.candidates.filter(c=>!state.groups.some(g=>g.groupSHA256===c.groupSHA256)).flatMap(c=>c.intervals.map(i=>({rawChunkIndex:i.rawChunkIndex,startSample:i.startSample,endSample:i.endSample,reason:'split-merge-or-retired-lineage'})))]
411
+ const provenance={nativeAuthenticated:false,productionQualified:false,originalSHA256:fluidHash(input.originalJSON),currentSHA256:fluidHash(input.currentJSON),ledgerSHA256:fluidHash(input.ledgerJSONL),nativeSHA256:fluidHash(input.cohort.nativeJSON),embeddingsSHA256:fluidHash(input.cohort.embeddingsJSON)}
412
+ if(hashMacSpeakerLive(state.coverage)!==hashMacSpeakerLive({...expected.coverage,supportedSamples})||hashMacSpeakerLive(state.excludedEmbeddingWindows)!==hashMacSpeakerLive(expected.excludedEmbeddingWindows)
413
+ ||hashMacSpeakerLive(state.withheld)!==hashMacSpeakerLive(withheld)||hashMacSpeakerLive(state.provenance)!==hashMacSpeakerLive(provenance))fluidFail('coverage_or_provenance_drift')
414
+ }
@@ -0,0 +1,126 @@
1
+ // Label-only presentation of already-qualified Mac acoustic evidence. The writer
2
+ // authenticates native/profile receipts before calling this pure projection;
3
+ // source geometry is checked again here and the original ASR stays immutable.
4
+ import { createHash } from 'node:crypto'
5
+ import { isVoiceLabel } from './mac-voice-labels.js'
6
+ import { hasMacSpeakerProtectedRows } from './mac-speaker-protection.js'
7
+ import { checkSpeakerName } from './speaker-name.js'
8
+
9
+ export interface MacSpeakerLiveProjectionSpan {
10
+ rawChunkIndex: number
11
+ audioSHA256: string
12
+ pcmSHA256: string
13
+ /** Capture-absolute 16 kHz sample coordinates for one entire original. */
14
+ startSample: number
15
+ endSample: number
16
+ identityId: string
17
+ displayLabel: string
18
+ profileName?: string
19
+ evidenceIds: string[]
20
+ }
21
+ export interface MacSpeakerLiveSourceChunk {
22
+ text: string
23
+ speaker: string
24
+ elapsed: number
25
+ audioSha256?: string
26
+ audioDurationMs?: number
27
+ }
28
+ export interface MacSpeakerLiveIndexedChunk<T extends MacSpeakerLiveSourceChunk = MacSpeakerLiveSourceChunk> { i: number; c: T }
29
+ const sha = (v: unknown) => createHash('sha256').update(JSON.stringify(v)).digest('hex')
30
+ const HASH = /^[a-f0-9]{64}$/
31
+ const uint = (v: unknown): v is number => Number.isSafeInteger(v) && Number(v) >= 0
32
+ const anonymous = (v: unknown) => typeof v === 'string' && (['', 'Ext', 'Unknown', 'Unidentified'].includes(v) || isVoiceLabel(v))
33
+ const samples = (ms: unknown): number | null => {
34
+ if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) return null
35
+ const n = ms * 16, rounded = Math.round(n)
36
+ return Number.isSafeInteger(rounded) && Math.abs(n - rounded) <= 1e-6 ? rounded : null
37
+ }
38
+ type Row = Record<string, unknown>
39
+ function canProject(c: MacSpeakerLiveSourceChunk): boolean {
40
+ if (!anonymous(c.speaker) || hasMacSpeakerProtectedRows(c)) return false
41
+ const root = c as unknown as Row
42
+ if (root.overlap === true || root.mixedSpeakers === true) return false
43
+ for (const key of ['segments', 'finalSegments', 'words']) {
44
+ if (root[key] === undefined) continue
45
+ if (!(root[key] as unknown[]).every(value => {
46
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false
47
+ const row = value as Row
48
+ return row.overlap !== true && row.mixedSpeakers !== true
49
+ && (key === 'words' && row.speaker === undefined || anonymous(row.speaker))
50
+ // Different automatic Voice labels in one original are mixed evidence.
51
+ && (row.speaker === undefined || row.speaker === c.speaker)
52
+ })) return false
53
+ }
54
+ return true
55
+ }
56
+
57
+ export function projectMacSpeakerLiveChunks<T extends MacSpeakerLiveSourceChunk>(input: {
58
+ captureId: string
59
+ generation: string
60
+ chunksIndexed: readonly MacSpeakerLiveIndexedChunk<T>[]
61
+ projections: readonly MacSpeakerLiveProjectionSpan[]
62
+ }): {
63
+ speakerProjectedChunksIndexed: MacSpeakerLiveIndexedChunk<T>[]
64
+ effectiveOverlaySHA256: string
65
+ sourceBindingSHA256: string
66
+ appliedRawChunkIndices: number[]
67
+ withheld: Array<{ rawChunkIndex: number; reason: string }>
68
+ } {
69
+ if (!/^mac_[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(input.captureId) || typeof input.generation !== 'string' || !input.generation.length || input.generation.length > 128
70
+ || !Array.isArray(input.chunksIndexed) || input.chunksIndexed.length > 10_000 || !Array.isArray(input.projections) || input.projections.length > 10_000) throw Error('invalid Mac live projection envelope')
71
+ const originals = [...input.chunksIndexed].sort((a, b) => a.i - b.i)
72
+ const byIndex = new Map<number, MacSpeakerLiveIndexedChunk<T>>()
73
+ for (const row of originals) {
74
+ if (!row || !uint(row.i) || row.i >= 10_000 || byIndex.has(row.i) || !row.c || typeof row.c.text !== 'string' || !row.c.text.trim()
75
+ || typeof row.c.speaker !== 'string' || samples(row.c.elapsed) === null) throw Error('invalid Mac live original chunks')
76
+ byIndex.set(row.i, row)
77
+ }
78
+ const spans = new Map<number, MacSpeakerLiveProjectionSpan>(), labels = new Map<string, string>(), identities = new Map<string, string>()
79
+ for (const p of input.projections) {
80
+ if (!p || !uint(p.rawChunkIndex) || p.rawChunkIndex >= 10_000 || spans.has(p.rawChunkIndex) || !HASH.test(p.audioSHA256) || !HASH.test(p.pcmSHA256)
81
+ || !uint(p.startSample) || !uint(p.endSample) || p.endSample <= p.startSample || p.endSample - p.startSample > 480_000
82
+ || !/^acoustic_[a-f0-9]{16,64}$/.test(p.identityId) || !Array.isArray(p.evidenceIds) || p.evidenceIds.length < 2 || p.evidenceIds.length > 64
83
+ || new Set(p.evidenceIds).size !== p.evidenceIds.length || p.evidenceIds.some((id: unknown) => typeof id !== 'string' || !/^[A-Za-z0-9_:-]{1,160}$/.test(id))
84
+ || (p.profileName !== undefined ? !checkSpeakerName(p.profileName).ok || p.displayLabel !== p.profileName : !isVoiceLabel(p.displayLabel))) throw Error('invalid Mac live projection span')
85
+ if (identities.has(p.identityId) && identities.get(p.identityId) !== p.displayLabel) throw Error('inconsistent Mac live identity label')
86
+ identities.set(p.identityId, p.displayLabel)
87
+ if (p.profileName === undefined) {
88
+ if (labels.has(p.displayLabel) && labels.get(p.displayLabel) !== p.identityId) throw Error('duplicate Mac live voice label')
89
+ labels.set(p.displayLabel, p.identityId)
90
+ }
91
+ spans.set(p.rawChunkIndex, p)
92
+ }
93
+ const ordered = [...spans.values()].sort((a, b) => a.startSample - b.startSample)
94
+ if (ordered.some((p, i) => i > 0 && ordered[i - 1].endSample > p.startSample)) throw Error('overlapping Mac live originals')
95
+ // Old Voice labels are scheduling hints, never acoustic identity. An unused
96
+ // namespace prevents a newly settled group from swallowing unsupported rows.
97
+ for (const row of originals) if (labels.has(row.c.speaker)) {
98
+ const p = spans.get(row.i)
99
+ if (!p || p.identityId !== labels.get(row.c.speaker) || !canProject(row.c)
100
+ || row.c.audioSha256 !== p.audioSHA256 || samples(row.c.elapsed) !== p.startSample
101
+ || samples(row.c.audioDurationMs) !== p.endSample - p.startSample) throw Error('Mac live voice label collides with original')
102
+ }
103
+ const withheld: Array<{ rawChunkIndex: number; reason: string }> = [], appliedRawChunkIndices: number[] = [], changes: unknown[] = []
104
+ for (const p of input.projections) if (!byIndex.has(p.rawChunkIndex)) withheld.push({ rawChunkIndex: p.rawChunkIndex, reason: 'original_missing' })
105
+ const projected = originals.map(({ i, c }) => {
106
+ const next = structuredClone(c), p = spans.get(i)
107
+ if (!p) return { i, c: next }
108
+ const start = samples(c.elapsed), count = samples(c.audioDurationMs)
109
+ let reason: string | undefined
110
+ if (c.audioSha256 !== p.audioSHA256 || start !== p.startSample || count === null || count !== p.endSample - p.startSample) reason = 'original_binding_changed'
111
+ else if (!canProject(c)) reason = 'protected_or_mixed_original'
112
+ if (reason) { withheld.push({ rawChunkIndex: i, reason }); return { i, c: next } }
113
+ next.speaker = p.displayLabel
114
+ for (const key of ['segments', 'finalSegments', 'words']) {
115
+ const rows = (next as unknown as Row)[key] as Row[] | undefined
116
+ for (const row of rows ?? []) if (row.speaker !== undefined) row.speaker = p.displayLabel
117
+ }
118
+ appliedRawChunkIndices.push(i)
119
+ // Do not include unrelated appended ASR in this digest. The writer advances
120
+ // speakerRevision only when an effective claim changes, including withdrawal.
121
+ changes.push({ i, source: sha(c), projection: p })
122
+ return { i, c: next }
123
+ })
124
+ return { speakerProjectedChunksIndexed: projected, sourceBindingSHA256: sha({ captureId: input.captureId, generation: input.generation, originals }),
125
+ effectiveOverlaySHA256: sha({ captureId: input.captureId, generation: input.generation, changes }), appliedRawChunkIndices, withheld }
126
+ }